more stuff

This commit is contained in:
snow flurry 2023-01-16 20:42:01 -08:00
parent 27a0f247a4
commit e7113d6772
19 changed files with 599 additions and 270 deletions

18
Cargo.lock generated
View file

@ -97,7 +97,7 @@ dependencies = [
"cexpr",
"clang-sys",
"clap 2.34.0",
"env_logger",
"env_logger 0.9.3",
"lazy_static",
"lazycell",
"log",
@ -472,6 +472,19 @@ dependencies = [
"termcolor",
]
[[package]]
name = "env_logger"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0"
dependencies = [
"humantime",
"is-terminal",
"log",
"regex",
"termcolor",
]
[[package]]
name = "errno"
version = "0.2.8"
@ -1057,8 +1070,11 @@ name = "nzr"
version = "0.1.0"
dependencies = [
"clap 4.0.29",
"env_logger 0.10.0",
"home",
"log",
"nzr-api",
"serde_json",
"tabled",
"tarpc",
"tokio",

View file

@ -8,7 +8,7 @@ pub struct NewInstance {
pub name: String,
pub title: Option<String>,
pub description: Option<String>,
pub interface: String,
pub subnet: String,
pub base_image: String,
pub cores: u8,
pub memory: u32,

View file

@ -55,7 +55,7 @@ impl Default for Config {
socket_path: PathBuf::from("/var/run/nazrin/nzrd.sock"),
admin_group: None,
},
db_path: PathBuf::from("/var/run/nazrin/nzr.db"),
db_path: PathBuf::from("/var/lib/nazrin/nzr.db"),
libvirt_uri: match std::env::var("LIBVIRT_URI") {
Ok(v) => v,
Err(_) => String::from("qemu:///system"),

View file

@ -1,4 +1,4 @@
use model::{Instance, Subnet};
use model::{CreateStatus, Instance, Subnet};
pub mod args;
pub mod config;
@ -10,14 +10,16 @@ pub use trust_dns_proto;
#[tarpc::service]
pub trait Nazrin {
/// Creates a new instance.
async fn new_instance(build_args: args::NewInstance) -> Result<Instance, String>;
async fn new_instance(build_args: args::NewInstance) -> Result<uuid::Uuid, String>;
/// Poll for the current status of an instance being created.
async fn poll_new_instance(task_id: uuid::Uuid) -> Option<CreateStatus>;
/// Deletes an existing instance.
///
/// This should involve deleting all related disks and clearing
/// the lease information from the subnet data, if any.
async fn delete_instance(name: String) -> Result<(), String>;
/// Gets a list of existing instances.
async fn get_instances() -> Result<Vec<Instance>, String>;
async fn get_instances(with_status: bool) -> Result<Vec<Instance>, String>;
/// Cleans up unusable entries in the database.
async fn garbage_collect() -> Result<(), String>;
/// Creates a new subnet.
@ -26,6 +28,8 @@ pub trait Nazrin {
/// interfaces they reference. This should be used primarily for
/// ease-of-use and bookkeeping (e.g., assigning dynamic leases).
async fn new_subnet(build_args: Subnet) -> Result<Subnet, String>;
/// Modifies an existing subnet.
async fn modify_subnet(edit_args: Subnet) -> Result<Subnet, String>;
/// Gets a list of existing subnets.
async fn get_subnets() -> Result<Vec<Subnet>, String>;
/// Deletes an existing subnet.

View file

@ -1,5 +1,5 @@
use serde::{Deserialize, Serialize};
use std::{fmt, net::Ipv4Addr, str::FromStr};
use std::{fmt, net::Ipv4Addr};
use trust_dns_proto::rr::Name;
use crate::net::{cidr::CidrV4, mac::MacAddr};
@ -60,6 +60,13 @@ impl fmt::Display for DomainState {
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct CreateStatus {
pub status_text: String,
pub completion: f32,
pub result: Option<Result<Instance, String>>,
}
/// Struct representing a VM instance.
#[derive(Debug, Serialize, Deserialize)]
pub struct Instance {
@ -72,6 +79,8 @@ pub struct Instance {
/// Struct representing a logical "lease" held by a VM.
#[derive(Debug, Serialize, Deserialize)]
pub struct Lease {
/// Subnet name corresponding to the lease
pub subnet: String,
/// The IPv4 address held by the Lease
pub addr: CidrV4,
/// The MAC address associated by the Lease
@ -80,15 +89,17 @@ pub struct Lease {
/// Struct representing a subnet used by the host for virtual
/// networking.
#[derive(Debug, Serialize, Deserialize)]
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Subnet {
/// The name of the interface the subnet is accessible via.
pub ifname: IfaceStr,
/// The subnet short name.
pub name: String,
pub data: SubnetData,
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SubnetData {
/// The name of the interface the subnet is accessible via.
pub ifname: String,
/// The network information for the subnet.
pub network: CidrV4,
/// The first host address that can be assigned dynamically
@ -98,65 +109,21 @@ pub struct SubnetData {
/// on the subnet.
pub end_host: Ipv4Addr,
/// The default gateway for the subnet.
pub gateway4: Option<Ipv4Addr>,
pub gateway4: Ipv4Addr,
/// The primary DNS server for the subnet.
pub dns: Vec<Ipv4Addr>,
/// The base domain used for DNS lookup.
pub domain_name: Option<Name>,
/// The VLAN ID used for the domain. If none, no VLAN is set.
pub vlan_id: Option<u32>,
}
/// A wrapper struct for [u8; 16], representing the maximum length
/// for an interface's name.
#[derive(Debug, Serialize, Deserialize)]
pub struct IfaceStr {
name: [u8; 16],
}
impl SubnetData {
pub fn start_bytes(&self) -> u32 {
self.network.host_bits(&self.start_host)
}
impl AsRef<[u8]> for IfaceStr {
fn as_ref(&self) -> &[u8] {
&self.name
}
}
#[derive(Debug, Serialize, Deserialize)]
pub enum ParseError {
BadSize(String),
}
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Interface name must be at most 15 characters")
}
}
impl std::error::Error for ParseError {}
impl FromStr for IfaceStr {
type Err = ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.bytes().len() > 15 {
Err(Self::Err::BadSize(s.to_owned()))
} else {
Ok(IfaceStr {
name: {
let mut ifstr = [0u8; 16];
let bytes = s.as_bytes();
ifstr[..bytes.len()].copy_from_slice(&bytes[..bytes.len()]);
ifstr
},
})
}
}
}
impl fmt::Display for IfaceStr {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}",
std::str::from_utf8(&self.name)
.map(|a| a.trim_end_matches(char::from(0)))
.map_err(|_| fmt::Error)?
)
pub fn end_bytes(&self) -> u32 {
self.network.host_bits(&self.end_host)
}
}

View file

@ -26,6 +26,14 @@ impl<'de> Deserialize<'de> for MacAddr {
}
}
impl std::ops::Index<usize> for MacAddr {
type Output = u8;
fn index(&self, index: usize) -> &Self::Output {
&self.octets[index]
}
}
#[derive(Debug)]
pub enum Error {
ParseError(std::num::ParseIntError),
@ -76,6 +84,10 @@ impl MacAddr {
}
}
pub fn invalid() -> MacAddr {
MacAddr { octets: [0u8; 6] }
}
pub fn from_bytes<T>(value: T) -> Result<MacAddr, Error>
where
T: AsRef<[u8]>,

View file

@ -13,3 +13,6 @@ tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] }
tokio-serde = { version = "0.8.0", features = ["bincode"] }
tarpc = { version = "0.31", features = ["tokio1", "unix", "serde-transport", "serde-transport-bincode"] }
tabled = "0.10.0"
serde_json = "1"
log = "0.4.17"
env_logger = "0.10.0"

View file

@ -1,4 +1,4 @@
use clap::{Parser, Subcommand};
use clap::{CommandFactory, FromArgMatches, Parser, Subcommand};
use nzr_api::model;
use nzr_api::net::cidr::CidrV4;
use nzr_api::trust_dns_proto::rr::Name;
@ -16,9 +16,9 @@ mod table;
pub struct NewInstanceArgs {
/// Name of the instance to be created
name: String,
/// Bridge the instance will initially run on
/// Subnet the instance will initially run on
#[arg(short, long)]
interface: String,
subnet: String,
/// Long description of the instance
#[arg(long)]
description: Option<String>,
@ -52,10 +52,18 @@ enum InstanceCmd {
List,
/// Deletes all invalid instances from the database
Prune,
/// Shows information on an instance
Dump {
name: Option<String>,
#[arg(short, long)]
quick: bool,
},
}
#[derive(Debug, clap::Args)]
pub struct AddNetArgs {
/// Short name for the subnet (e.g., `servers')
pub name: String,
/// Name of the bridge interface VMs will attach to
pub interface: String,
/// Subnet associated with the bridge interface, in CIDR notation (x.x.x.x/y)
@ -74,19 +82,47 @@ pub struct AddNetArgs {
pub end_addr: Option<std::net::Ipv4Addr>,
#[arg(short, long)]
pub domain_name: Option<Name>,
/// VLAN ID for the VM, if any
#[arg(short, long)]
pub vlan_id: Option<u32>,
}
#[derive(Debug, clap::Args)]
pub struct EditNetArgs {
/// Short name of the subnet
pub name: String,
/// Default gateway for the subnet
#[arg(long)]
pub gateway: Option<std::net::Ipv4Addr>,
/// Default DNS address for the subnet
#[arg(long)]
pub dns_server: Option<std::net::Ipv4Addr>,
/// Start address for IP assignment
#[arg(short, long)]
pub start_addr: Option<std::net::Ipv4Addr>,
/// End address for IP assignment
#[arg(short, long)]
pub end_addr: Option<std::net::Ipv4Addr>,
/// Domain name associated with the subnet
#[arg(short, long)]
pub domain_name: Option<Name>,
/// VLAN ID for the VM, if any
#[arg(short, long)]
pub vlan_id: Option<u32>,
}
#[derive(Debug, Subcommand)]
enum NetCmd {
/// Add a new network to the database
Add(AddNetArgs),
/// Edit an existing network
Edit(EditNetArgs),
/// List all networks in the database
List,
/// Delete a network from the database
Delete {
#[arg(short, long)]
interface: String,
},
Delete { name: String },
/// Shows information on a subnet
Dump { name: Option<String> },
}
#[derive(Debug, Subcommand)]
@ -160,108 +196,226 @@ impl CommandError {
}
async fn handle_command() -> Result<(), Box<dyn std::error::Error>> {
let cli = Args::parse();
env_logger::init();
let mut matches = Args::command().infer_subcommands(true).get_matches();
let cli = Args::from_arg_matches_mut(&mut matches)?;
let config: config::Config = nzr_api::config::Config::figment().extract()?;
let conn = UnixStream::connect(&config.rpc.socket_path).await?;
let codec_builder = LengthDelimitedCodec::builder();
let transport = tarpc::serde_transport::new(codec_builder.new_framed(conn), Bincode::default());
let framed_io = LengthDelimitedCodec::builder()
.length_field_type::<u32>()
.new_framed(conn);
let transport = tarpc::serde_transport::new(framed_io, Bincode::default());
let client = NazrinClient::new(Default::default(), transport).spawn();
match cli.command {
Commands::Instance { command } => {
match command {
InstanceCmd::New(args) => {
let ssh_keys: Vec<String> = {
let key_file =
args.sshkey_file.map_or_else(
|| {
home::home_dir().map_or_else(|| {
Err(CommandError::from("SSH keyfile not defined, and couldn't find home directory"))
}, |hd| {
Ok(hd.join(".ssh/authorized_keys"))
})
},
Ok,
)?;
Commands::Instance { command } => match command {
InstanceCmd::Dump { name, quick } => {
let instances = (client
.get_instances(tarpc::context::current(), !quick)
.await?)?;
if let Some(name) = name {
if let Some(inst) = instances.iter().find(|f| f.name == name) {
println!("{}", serde_json::to_string(inst)?);
}
} else {
println!("{}", serde_json::to_string(&instances)?);
}
}
InstanceCmd::New(args) => {
let ssh_keys: Vec<String> = {
let key_file = args.sshkey_file.map_or_else(
|| {
home::home_dir().map_or_else(
|| {
Err(CommandError::from(
"SSH keyfile not defined, and couldn't find home directory",
))
},
|hd| Ok(hd.join(".ssh/authorized_keys")),
)
},
Ok,
)?;
if !key_file.exists() {
Err("SSH keyfile doesn't exist".into())
} else {
match std::fs::read_to_string(&key_file) {
Ok(data) => {
let keys: Vec<String> =
data.split('\n').map(|s| s.trim().to_owned()).collect();
Ok(keys)
if !key_file.exists() {
Err("SSH keyfile doesn't exist".into())
} else {
match std::fs::read_to_string(&key_file) {
Ok(data) => {
let keys: Vec<String> =
data.split('\n').map(|s| s.trim().to_owned()).collect();
Ok(keys)
}
Err(err) => Err(CommandError::new(
format!("Couldn't read {} for SSH keys", &key_file.display()),
err,
)),
}
}
}?;
let build_args = nzr_api::args::NewInstance {
name: args.name,
title: None,
description: args.description,
subnet: args.subnet,
base_image: args.base,
cores: args.cores,
memory: args.mem,
disk_sizes: (args.primary_size, args.secondary_size),
ssh_keys,
};
let task_id = (client
.new_instance(tarpc::context::current(), build_args)
.await?)?;
const MAX_RETRIES: i32 = 5;
let mut retries = 0;
let mut current_pct: f32 = 0.0;
loop {
let status = client
.poll_new_instance(tarpc::context::current(), task_id)
.await;
match status {
Ok(Some(status)) => {
if let Some(result) = status.result {
match result {
Ok(instance) => {
println!("Instance {} created!", &instance.name);
if let Some(lease) = instance.lease {
println!(
"You should be able to reach it with: ssh root@{}",
lease.addr.addr,
);
}
}
Err(err) => {
log::error!("Error while creating instance: {}", err);
}
}
Err(err) => Err(CommandError::new(
format!("Couldn't read {} for SSH keys", &key_file.display()),
err,
)),
break;
} else if status.completion != current_pct {
println!("[remote] {}", &status.status_text);
current_pct = status.completion;
}
}
Ok(None) => {
log::error!("Task ID {} went AWOL??", task_id);
break;
}
Err(err) => {
log::error!("Got RPC error: {}", err);
retries += 1;
if retries >= MAX_RETRIES {
break;
} else {
log::error!("Retrying (attempt {}/{})...", retries, MAX_RETRIES);
}
}
}?;
let build_args = nzr_api::args::NewInstance {
name: args.name,
title: None,
description: args.description,
interface: args.interface,
base_image: args.base,
cores: args.cores,
memory: args.mem,
disk_sizes: (args.primary_size, args.secondary_size),
ssh_keys,
};
let instance = (client
.new_instance(tarpc::context::current(), build_args)
.await?)?;
println!("Instance {} created!", &instance.name);
if let Some(lease) = instance.lease {
println!(
"You should be able to reach it at:\n\n ssh root@{}",
lease.addr.addr,
);
}
}
InstanceCmd::Delete { name } => {
(client
.delete_instance(tarpc::context::current(), name)
.await?)?;
}
InstanceCmd::List => {
let instances = client.get_instances(tarpc::context::current()).await?;
let tabular: Vec<table::Instance> =
instances?.iter().map(table::Instance::from).collect();
let mut table = tabled::Table::new(&tabular);
println!("{}", table.with(tabled::Style::psql()));
}
InstanceCmd::Prune => (client.garbage_collect(tarpc::context::current()).await?)?,
}
}
InstanceCmd::Delete { name } => {
(client
.delete_instance(tarpc::context::current(), name)
.await?)?;
}
InstanceCmd::List => {
let instances = client
.get_instances(tarpc::context::current(), true)
.await?;
let tabular: Vec<table::Instance> =
instances?.iter().map(table::Instance::from).collect();
let mut table = tabled::Table::new(&tabular);
println!("{}", table.with(tabled::Style::psql()));
}
InstanceCmd::Prune => (client.garbage_collect(tarpc::context::current()).await?)?,
},
Commands::Net { command } => match command {
NetCmd::Add(args) => {
let net_arg = CidrV4::from_str(&args.network)?;
let build_args = model::Subnet {
ifname: model::IfaceStr::from_str(&args.interface)?,
name: args.name,
data: model::SubnetData {
ifname: args.interface.clone(),
network: net_arg.clone(),
start_host: args.start_addr.unwrap_or(net_arg.make_ip(10)?),
end_host: args
.end_addr
.unwrap_or((u32::from(net_arg.broadcast()) - 1u32).into()),
gateway4: args.gateway,
gateway4: args.gateway.unwrap_or(net_arg.make_ip(1)?),
dns: args.dns_server.map_or(Vec::new(), |d| vec![d]),
domain_name: args.domain_name,
vlan_id: args.vlan_id,
},
};
(client
.new_subnet(tarpc::context::current(), build_args)
.await?)?;
}
NetCmd::Delete { interface } => {
NetCmd::Edit(args) => {
let mut net = client
.get_subnets(tarpc::context::current())
.await
.map_err(|e| e.to_string())
.and_then(|res| {
res?.iter()
.find_map(|ent| {
if ent.name == args.name {
Some(ent.clone())
} else {
None
}
})
.ok_or_else(|| format!("Couldn't find network {}", &args.name))
})?;
// merge in the new args
if let Some(gateway) = args.gateway {
net.data.gateway4 = gateway;
}
if let Some(dns_server) = args.dns_server {
net.data.dns = vec![dns_server]
}
if let Some(start_addr) = args.start_addr {
net.data.start_host = start_addr;
}
if let Some(end_addr) = args.end_addr {
net.data.end_host = end_addr;
}
if let Some(domain_name) = args.domain_name {
net.data.domain_name = Some(domain_name);
}
if let Some(vlan_id) = args.vlan_id {
net.data.vlan_id = Some(vlan_id);
}
// run the update
client
.modify_subnet(tarpc::context::current(), net)
.await
.map_err(|err| format!("RPC error: {}", err))
.and_then(|res| {
res.map(|e| {
println!("Subnet {} updated.", e.name);
})
})?;
}
NetCmd::Dump { name } => {
let subnets = (client.get_subnets(tarpc::context::current()).await?)?;
if let Some(name) = name {
if let Some(net) = subnets.iter().find(|s| s.name == name) {
println!("{}", serde_json::to_string(net)?);
}
} else {
println!("{}", serde_json::to_string(&subnets)?);
}
}
NetCmd::Delete { name } => {
(client
.delete_subnet(tarpc::context::current(), interface)
.delete_subnet(tarpc::context::current(), name)
.await?)?;
}
NetCmd::List => {
@ -281,9 +435,9 @@ async fn handle_command() -> Result<(), Box<dyn std::error::Error>> {
async fn main() -> Result<(), Box<dyn std::error::Error>> {
if let Err(err) = handle_command().await {
if std::any::Any::type_id(&*err).type_id() == TypeId::of::<tarpc::client::RpcError>() {
eprintln!("[err] Error communicating with server: {}", err);
log::error!("Error communicating with server: {}", err);
} else {
eprintln!("[err] {}", err);
log::error!("{}", err);
}
}
Ok(())

View file

@ -26,6 +26,8 @@ impl From<&model::Instance> for Instance {
#[derive(Tabled)]
pub struct Subnet {
#[tabled(rename = "Name")]
name: String,
#[tabled(rename = "Interface")]
interface: String,
#[tabled(rename = "Network")]
@ -35,7 +37,8 @@ pub struct Subnet {
impl From<&model::Subnet> for Subnet {
fn from(value: &model::Subnet) -> Self {
Self {
interface: value.ifname.to_string(),
name: value.name.clone(),
interface: value.data.ifname.to_string(),
network: value.data.network.to_string(),
}
}

View file

@ -1,5 +1,6 @@
use super::*;
use crate::ctrl::net::Subnet;
use crate::ctrl::Entity;
use crate::ctrl::Storable;
use crate::ctx::Context;
use nzr_api::model;
@ -7,23 +8,11 @@ use nzr_api::model;
pub async fn add_subnet(
ctx: &Context,
args: model::Subnet,
) -> Result<Subnet, Box<dyn std::error::Error>> {
let subnet = Subnet::new(
&args.ifname.to_string(),
&args.data.network,
&args.data.start_host,
&args.data.end_host,
args.data.gateway4.as_ref(),
&args.data.dns,
args.data.domain_name,
)
.map_err(|er| cmd_error!("Couldn't generate subnet: {}", er))?;
) -> Result<Entity<Subnet>, Box<dyn std::error::Error>> {
let subnet = Subnet::from_model(&args.data)
.map_err(|er| cmd_error!("Couldn't generate subnet: {}", er))?;
let mut ent = Subnet::insert(
ctx.db.clone(),
subnet.clone(),
args.ifname.to_string().as_bytes(),
)?;
let mut ent = Subnet::insert(ctx.db.clone(), subnet.clone(), args.name.as_bytes())?;
ent.transient = true;
@ -31,7 +20,7 @@ pub async fn add_subnet(
Err(cmd_error!("Failed to create new DNS zone: {}", err))
} else {
ent.transient = false;
Ok(subnet)
Ok(ent)
}
}

View file

@ -1,3 +1,4 @@
use tokio::sync::RwLock;
use virt::stream::Stream;
use super::*;
@ -5,7 +6,7 @@ use crate::cloud::{DNSMeta, EtherMatch, Metadata, NetworkMeta};
use crate::ctrl::net::Subnet;
use crate::ctrl::virtxml::build::DomainBuilder;
use crate::ctrl::virtxml::{DiskDeviceType, SerialType, VolType, Volume};
use crate::ctrl::vm::{InstDb, Instance, InstanceError};
use crate::ctrl::vm::{InstDb, Instance, InstanceError, Progress};
use crate::ctrl::Storable;
use crate::ctx::Context;
use crate::prelude::*;
@ -13,22 +14,33 @@ use crate::virt::VirtVolume;
use log::*;
use nzr_api::args;
use nzr_api::net::mac::MacAddr;
use std::sync::Arc;
use trust_dns_server::proto::rr::Name;
const VIRT_MAC_OUI: &[u8] = &[0x02, 0xf1, 0x0f];
macro_rules! progress {
($task:ident, $pct:literal, $($arg:tt)*) => {{
let mut pt = $task.write().await;
pt.status_text = format!($($arg)*);
pt.percentage = $pct;
}};
}
/// Creates a new instance
pub async fn new_instance(
ctx: Context,
prog_task: Arc<RwLock<Progress>>,
args: &args::NewInstance,
) -> Result<Instance, Box<dyn std::error::Error>> {
progress!(prog_task, 0.0, "Starting...");
// find the subnet corresponding to the interface
let subnet = Subnet::get_by_key(ctx.db.clone(), args.interface.as_bytes())
let subnet = Subnet::get_by_key(ctx.db.clone(), args.subnet.as_bytes())
.map_err(|er| cmd_error!("Unable to get interface: {}", er))?
.map_or(
Err(cmd_error!(
"Interface {} wasn't found in database",
&args.interface
"Subnet {} wasn't found in database",
&args.subnet
)),
Ok,
)?;
@ -43,6 +55,7 @@ pub async fn new_instance(
// make sure the base image exists
let mut base_image = VirtVolume::lookup_by_name(&ctx.virt.pools.baseimg, &args.base_image)
.map_err(|er| cmd_error!("Couldn't find base image: {}", er))?;
progress!(prog_task, 10.0, "Generating metadata...");
// generate a new lease with a new MAC addr
let mac_addr = {
@ -99,6 +112,7 @@ pub async fn new_instance(
// mark the stream as finished
cistream.finish()?;
progress!(prog_task, 30.0, "Creating instance images...");
// create primary volume from base image
let mut pri_vol = base_image
.clone_vol(
@ -126,6 +140,12 @@ pub async fn new_instance(
};
// build domain xml
let ifname = subnet.ifname.clone();
let devname = format!(
"veth-{:02x}{:02x}{:02x}",
mac_addr[3], mac_addr[4], mac_addr[5]
);
progress!(prog_task, 60.0, "Initializing instance...");
let (mut inst, conn) = Instance::new(ctx.clone(), subnet, lease, {
let pri_name = &ctx.virt.pools.primary.xml.name;
let sec_name = &ctx.virt.pools.secondary.xml.name;
@ -135,7 +155,11 @@ pub async fn new_instance(
.name(&args.name)
.memory(datasize!((args.memory) MiB))
.cpu_topology(1, 1, args.cores, 1)
.net_device(|nd| nd.mac_addr(&mac_addr).with_bridge(&args.interface))
.net_device(|nd| {
nd.mac_addr(&mac_addr)
.with_bridge(&ifname)
.target_dev(&devname)
})
.disk_device(|dsk| {
dsk.volume_source(pri_name, &pri_vol.name)
.target("vda", "virtio")
@ -170,9 +194,12 @@ pub async fn new_instance(
warn!("Couldn't set autostart for domain: {}", er);
}
if let Err(er) = conn.create() {
warn!("Domain defined, but couldn't be started! Error: {}", er);
}
tokio::task::spawn_blocking(move || {
if let Err(er) = conn.create() {
warn!("Domain defined, but couldn't be started! Error: {}", er);
}
})
.await?;
// set all volumes to persistent to avoid deletion
pri_vol.persist = true;
@ -182,6 +209,7 @@ pub async fn new_instance(
cidata_vol.persist = true;
inst.persist();
progress!(prog_task, 80.0, "Domain created!");
debug!("Domain {} created!", inst.xml().name.as_str());
Ok(inst)
}

View file

@ -94,6 +94,11 @@ where
Ok(())
}
pub fn replace(&mut self, other: T) -> Result<(), StorableError> {
self.inner = other;
self.update()
}
pub fn delete(&self) -> Result<(), StorableError> {
self.on_delete(&self.db)?;
self.tree

View file

@ -1,47 +1,45 @@
use super::{Entity, StorIter};
use nzr_api::model::IfaceStr;
use nzr_api::model::SubnetData;
use nzr_api::net::cidr::CidrV4;
use nzr_api::net::mac::MacAddr;
use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;
use std::fmt;
use std::net::Ipv4Addr;
use std::str::FromStr;
use trust_dns_server::proto::rr::Name;
use std::ops::Deref;
use super::Storable;
#[skip_serializing_none]
#[derive(Clone, Serialize, Deserialize)]
pub struct Subnet {
pub interface: String,
pub network: CidrV4,
pub gateway4: Ipv4Addr,
pub dns: Vec<Ipv4Addr>,
pub domain_name: Option<Name>,
pub start_host: u32,
pub end_host: u32,
pub model: SubnetData,
}
impl From<&Subnet> for nzr_api::model::Subnet {
impl Deref for Subnet {
type Target = SubnetData;
fn deref(&self) -> &Self::Target {
&self.model
}
}
impl From<&Subnet> for SubnetData {
fn from(value: &Subnet) -> Self {
let start_host = value.network.make_ip(value.start_host).unwrap();
let end_host = value.network.make_ip(value.end_host).unwrap();
value.model.clone()
}
}
impl From<&SubnetData> for Subnet {
fn from(value: &SubnetData) -> Self {
Self {
ifname: IfaceStr::from_str(&value.interface).unwrap(),
data: nzr_api::model::SubnetData {
network: value.network.clone(),
start_host,
end_host,
gateway4: Some(value.gateway4),
dns: value.dns.clone(),
domain_name: value.domain_name.to_owned(),
},
model: value.clone(),
}
}
}
#[derive(Clone, Serialize, Deserialize)]
pub struct Lease {
pub subnet: String,
pub ipv4_addr: CidrV4,
pub mac_addr: MacAddr,
pub inst_name: String,
@ -103,43 +101,17 @@ impl Storable for Subnet {
}
impl Subnet {
pub fn new(
iface: &str,
network: &CidrV4,
start_addr: &Ipv4Addr,
end_addr: &Ipv4Addr,
gateway4: Option<&Ipv4Addr>,
dns: &[Ipv4Addr],
domain_name: Option<Name>,
) -> Result<Self, SubnetError> {
pub fn from_model(data: &nzr_api::model::SubnetData) -> Result<Self, SubnetError> {
// validate start and end addresses
if end_addr < start_addr {
if data.end_host < data.start_host {
Err(SubnetError::BadRange)
} else if !network.contains(start_addr) {
} else if !data.network.contains(&data.start_host) {
Err(SubnetError::BadStartHost)
} else if !network.contains(end_addr) {
} else if !data.network.contains(&data.end_host) {
Err(SubnetError::BadEndHost)
} else {
let gateway4 = gateway4.cloned().unwrap_or(
network
.make_ip(1)
.map_err(|_| SubnetError::HostOutsideRange)?,
);
let mut dns = dns.to_owned();
if dns.is_empty() {
// default DNS: quad9
dns.push(Ipv4Addr::new(9, 9, 9, 9));
}
let start_host = network.host_bits(start_addr);
let end_host = network.host_bits(end_addr);
let subnet = Subnet {
interface: iface.to_owned(),
network: network.clone(),
start_host,
end_host,
gateway4,
dns,
domain_name,
model: data.clone(),
};
Ok(subnet)
}
@ -148,7 +120,7 @@ impl Subnet {
/// Gets the lease tree from sled.
pub fn lease_tree(&self) -> Vec<u8> {
let mut lt_name: Vec<u8> = vec![b'L'];
lt_name.extend_from_slice(&self.network.octets());
lt_name.extend_from_slice(&self.model.network.octets());
lt_name
}
}
@ -169,14 +141,16 @@ impl Entity<Subnet> {
let tree = self.db.open_tree(self.lease_tree())?;
let max_lease = match tree.last()? {
Some(lease) => u32::from_be_bytes(lease.0[..4].try_into().unwrap()),
None => self.start_host,
None => self.model.start_bytes(),
};
let new_ip = self
.model
.network
.make_ip(max_lease + 1)
.map_err(|_| SubnetError::SubnetFull)?;
let lease_data = Lease {
ipv4_addr: CidrV4::new(new_ip, self.network.cidr()),
subnet: String::from_utf8_lossy(&self.key).to_string(),
ipv4_addr: CidrV4::new(new_ip, self.model.network.cidr()),
mac_addr: mac_addr.clone(),
inst_name: inst_name.to_owned(),
};

View file

@ -179,6 +179,13 @@ impl IfaceBuilder {
self
}
pub fn target_dev(mut self, name: &str) -> Self {
self.iface.target = Some(NetTarget {
dev: name.to_owned(),
});
self
}
fn build(self) -> NetDevice {
self.iface
}

View file

@ -224,6 +224,12 @@ impl Default for NetModel {
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct NetTarget {
#[serde(rename = "@dev")]
dev: String,
}
#[derive(Default, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum IfaceType {
@ -240,6 +246,7 @@ pub struct NetDevice {
mac: Option<NetMac>,
source: NetSource,
model: NetModel,
target: Option<NetTarget>,
}
// =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^=

View file

@ -2,6 +2,7 @@ use crate::ctrl::net::Lease;
use crate::ctx::Context;
use log::*;
use nzr_api::net::cidr::CidrV4;
use nzr_api::net::mac::MacAddr;
use std::net::Ipv4Addr;
use std::str::{self, Utf8Error};
@ -11,10 +12,16 @@ use super::{net::Subnet, Entity};
use crate::virt::*;
use serde::{Deserialize, Serialize};
#[derive(Clone)]
pub struct Progress {
pub status_text: String,
pub percentage: f32,
}
#[derive(Clone, Serialize, Deserialize)]
pub struct InstDb {
uuid: uuid::Uuid,
lease_if: String,
lease_subnet: Vec<u8>,
lease_addr: CidrV4,
}
@ -30,6 +37,21 @@ impl Storable for InstDb {
}
}
impl From<Entity<InstDb>> for nzr_api::model::Instance {
fn from(value: Entity<InstDb>) -> Self {
nzr_api::model::Instance {
name: String::from_utf8_lossy(&value.key).to_string(),
uuid: value.uuid,
lease: Some(nzr_api::model::Lease {
subnet: String::from_utf8_lossy(&value.lease_subnet).to_string(),
addr: value.lease_addr.clone(),
mac_addr: MacAddr::invalid(),
}),
state: nzr_api::model::DomainState::NoState,
}
}
}
pub struct Instance {
db_data: Entity<InstDb>,
lease: Option<Entity<Lease>>,
@ -77,12 +99,12 @@ impl Instance {
debug!(
"Adding {} (interface: {}) to the instance tree...",
&lease.ipv4_addr, &subnet.interface,
&lease.ipv4_addr, &subnet.ifname,
);
let db_data = InstDb {
uuid: real_xml.uuid,
lease_if: subnet.interface.clone(),
lease_subnet: subnet.key().to_vec(),
lease_addr: lease.ipv4_addr.clone(),
};
@ -148,6 +170,7 @@ impl Instance {
Ok(())
}
/// Create an Instance from a given InstDb entity.
pub fn from_entity(ctx: Context, db_data: Entity<InstDb>) -> Result<Self, InstanceError> {
let name = String::from_utf8_lossy(&db_data.key).into_owned();
let virt_domain = match virt::domain::Domain::lookup_by_name(&ctx.virt.conn, &name) {
@ -168,7 +191,7 @@ impl Instance {
quick_xml::de::from_str(&xml_str).map_err(InstanceError::CantDeserialize)?
};
let lease = match Subnet::get_by_key(ctx.db.clone(), db_data.lease_if.as_bytes())
let lease = match Subnet::get_by_key(ctx.db.clone(), &db_data.lease_subnet)
.map_err(InstanceError::other)?
{
Some(subnet) => subnet
@ -237,6 +260,7 @@ impl From<&Instance> for nzr_api::model::Instance {
name: value.domain_xml.name.clone(),
uuid: value.domain_xml.uuid,
lease: value.lease.as_ref().map(|l| nzr_api::model::Lease {
subnet: l.subnet.clone(),
addr: l.ipv4_addr.clone(),
mac_addr: l.mac_addr.clone(),
}),

View file

@ -126,13 +126,18 @@ impl InnerZD {
trust_dns_server::authority::ZoneType::Primary,
false,
)?;
self.import(&subnet.interface, auth).await;
self.import(&subnet.ifname.to_string(), auth).await;
}
Ok(())
}
pub async fn import(&self, name: &str, auth: InMemoryAuthority) {
let auth_arc = Arc::new(auth);
log::debug!(
"Importing {} with {} records...",
name,
auth_arc.records().await.len()
);
self.map
.lock()
.await
@ -163,6 +168,12 @@ impl InnerZD {
hostname.append_domain(&origin)?
};
log::debug!(
"Creating new host entry {} in zone {}...",
&fqdn,
zone.origin()
);
let record = Record::from_rdata(fqdn, 3600, RData::A(addr));
zone.upsert(record, 0).await;
self.catalog()

View file

@ -17,7 +17,7 @@ use std::str::FromStr;
use tokio::net::UdpSocket;
use trust_dns_server::ServerFuture;
#[tokio::main]
#[tokio::main(flavor = "multi_thread")]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let cfg: config::Config = config::Config::figment().extract()?;
let ctx = ctx::Context::new(cfg)?;
@ -33,7 +33,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
Ok(subnet) => {
// A records
if let Err(err) = ctx.zones.new_zone(&subnet).await {
error!("Couldn't create zone for {}: {}", &subnet.interface, err);
error!("Couldn't create zone for {}: {}", &subnet.ifname, err);
continue;
}
match subnet.leases() {
@ -44,7 +44,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
if let Err(err) = ctx
.zones
.new_record(
&subnet.interface,
&subnet.ifname.to_string(),
&lease.inst_name,
lease.ipv4_addr.addr,
)
@ -52,21 +52,21 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
{
error!(
"Failed to set up lease for {} in {}: {}",
&lease.inst_name, &subnet.interface, err
&lease.inst_name, &subnet.ifname, err
);
}
}
Err(err) => {
warn!(
"Lease iterator error while hydrating {}: {}",
&subnet.interface, err
&subnet.ifname, err
);
}
}
}
}
Err(err) => {
error!("Couldn't get leases for {}: {}", &subnet.interface, err);
error!("Couldn't get leases for {}: {}", &subnet.ifname, err);
continue;
}
}

View file

@ -1,8 +1,13 @@
use nzr_api::{args, model, Nazrin};
use std::borrow::Borrow;
use std::sync::Arc;
use tarpc::server::{BaseChannel, Channel};
use tarpc::tokio_serde::formats::Bincode;
use tarpc::tokio_util::codec::LengthDelimitedCodec;
use tokio::net::UnixListener;
use tokio::sync::RwLock;
use tokio::task::JoinHandle;
use uuid::Uuid;
use crate::ctrl::vm::InstDb;
use crate::ctrl::{net::Subnet, Storable};
@ -10,17 +15,23 @@ use crate::ctx::Context;
use crate::dns::ZoneData;
use crate::{cmd, ctrl::vm::Instance};
use log::*;
use std::collections::HashMap;
use std::ops::Deref;
#[derive(Clone)]
pub struct NzrServer {
ctx: Context,
zones: ZoneData,
create_tasks: Arc<RwLock<HashMap<Uuid, InstCreateStatus>>>,
}
impl NzrServer {
pub fn new(ctx: Context, zones: ZoneData) -> Self {
Self { ctx, zones }
Self {
ctx,
zones,
create_tasks: Arc::new(RwLock::new(HashMap::new())),
}
}
}
@ -30,21 +41,79 @@ impl Nazrin for NzrServer {
self,
_: tarpc::context::Context,
build_args: args::NewInstance,
) -> Result<model::Instance, String> {
let inst = cmd::vm::new_instance(self.ctx.clone(), &build_args)
.await
.map_err(|e| format!("Instance creation failed: {}", e))?;
let addr = inst.ip_lease().map(|l| l.ipv4_addr.addr);
if let Some(addr) = addr {
if let Err(err) = self
.zones
.new_record(&build_args.interface, &build_args.name, addr)
) -> Result<uuid::Uuid, String> {
let progress = Arc::new(RwLock::new(crate::ctrl::vm::Progress {
status_text: "Starting...".to_owned(),
percentage: 0.0,
}));
let prog_task = progress.clone();
let build_task = tokio::spawn(async move {
let inst = cmd::vm::new_instance(self.ctx.clone(), prog_task.clone(), &build_args)
.await
.map_err(|e| format!("Instance creation failed: {}", e))?;
let addr = inst.ip_lease().map(|l| l.ipv4_addr.addr);
{
warn!("Instance created, but no DNS record was made: {}", err);
let mut pt = prog_task.write().await;
pt.status_text = "Starting instance...".to_owned();
pt.percentage = 90.0;
}
}
Ok((&inst).into())
if let Some(addr) = addr {
if let Err(err) = self
.zones
.new_record(&build_args.subnet, &build_args.name, addr)
.await
{
warn!("Instance created, but no DNS record was made: {}", err);
}
}
Ok((&inst).into())
});
let task_id = uuid::Uuid::new_v4();
self.create_tasks.write().await.insert(
task_id,
InstCreateStatus {
inner: build_task,
progress,
},
);
Ok(task_id)
}
async fn poll_new_instance(
self,
_: tarpc::context::Context,
task_id: uuid::Uuid,
) -> Option<model::CreateStatus> {
let (progress, is_finished) = {
match self.create_tasks.read().await.get(&task_id) {
Some(st) => (st.progress.read().await.clone(), st.inner.is_finished()),
None => {
debug!("Task ID {} not found", task_id);
return None;
}
}
};
let result = if is_finished {
let task = self.create_tasks.write().await.remove(&task_id).unwrap();
Some(
task.inner
.await
.map_err(|err| format!("Task failed with panic: {}", err))
.and_then(|res| res),
)
} else {
None
};
Some(model::CreateStatus {
status_text: progress.status_text,
completion: progress.percentage,
result,
})
}
async fn delete_instance(self, _: tarpc::context::Context, name: String) -> Result<(), String> {
@ -57,21 +126,30 @@ impl Nazrin for NzrServer {
async fn get_instances(
self,
_: tarpc::context::Context,
with_status: bool,
) -> Result<Vec<model::Instance>, String> {
let insts: Vec<model::Instance> = InstDb::all(self.ctx.db.clone())
.map_err(|e| e.to_string())?
.filter_map(|i| match i {
Ok(entity) => match Instance::from_entity(self.ctx.clone(), entity.clone()) {
Ok(instance) => Some(<&Instance as Into<model::Instance>>::into(&instance)),
Err(err) => {
let ent_name = {
let key = entity.key();
String::from_utf8_lossy(key).to_string()
};
warn!("Couldn't get instance for {}: {}", err, ent_name);
None
Ok(entity) => {
if with_status {
match Instance::from_entity(self.ctx.clone(), entity.clone()) {
Ok(instance) => {
Some(<&Instance as Into<model::Instance>>::into(&instance))
}
Err(err) => {
let ent_name = {
let key = entity.key();
String::from_utf8_lossy(key).to_string()
};
warn!("Couldn't get instance for {}: {}", err, ent_name);
None
}
}
} else {
Some(entity.into())
}
},
}
Err(err) => {
warn!("Iterator error: {}", err);
None
@ -93,14 +171,51 @@ impl Nazrin for NzrServer {
.new_zone(&subnet)
.await
.map_err(|e| e.to_string())?;
Ok(<&Subnet as Into<model::Subnet>>::into(&subnet))
Ok(model::Subnet {
name: String::from_utf8_lossy(subnet.key()).to_string(),
data: <&Subnet as Into<model::SubnetData>>::into(&subnet),
})
}
async fn modify_subnet(
self,
_: tarpc::context::Context,
edit_args: model::Subnet,
) -> Result<model::Subnet, String> {
let subnet = Subnet::all(self.ctx.db.clone())
.map_err(|e| e.to_string())?
.find_map(|sub| {
if let Ok(sub) = sub {
if edit_args.name.as_str() == String::from_utf8_lossy(sub.key()) {
Some(sub)
} else {
None
}
} else {
None
}
});
if let Some(mut subnet) = subnet {
subnet
.replace(edit_args.data.borrow().into())
.map_err(|e| e.to_string())?;
Ok(model::Subnet {
name: edit_args.name,
data: subnet.deref().into(),
})
} else {
Err(format!("Subnet {} not found", &edit_args.name))
}
}
async fn get_subnets(self, _: tarpc::context::Context) -> Result<Vec<model::Subnet>, String> {
let subnets: Vec<model::Subnet> = Subnet::all(self.ctx.db.clone())
.map_err(|e| e.to_string())?
.filter_map(|s| match s {
Ok(s) => Some(<&Subnet as Into<model::Subnet>>::into(s.deref())),
Ok(s) => Some(model::Subnet {
name: String::from_utf8(s.key().to_vec()).unwrap(),
data: <&Subnet as Into<model::SubnetData>>::into(s.deref()),
}),
Err(err) => {
warn!("Iterator error: {}", err);
None
@ -157,11 +272,21 @@ pub async fn serve(ctx: Context, zones: ZoneData) -> Result<(), Box<dyn std::err
let codec_builder = LengthDelimitedCodec::builder();
loop {
debug!("Listening for new connection...");
let (conn, _addr) = listener.accept().await?;
let framed = codec_builder.new_framed(conn);
let transport = tarpc::serde_transport::new(framed, Bincode::default());
BaseChannel::with_defaults(transport)
.execute(NzrServer::new(ctx.clone(), zones.clone()).serve())
.await;
let (ctx, zones) = (ctx.clone(), zones.clone());
// hack?
tokio::spawn(async move {
let framed = codec_builder.new_framed(conn);
let transport = tarpc::serde_transport::new(framed, Bincode::default());
BaseChannel::with_defaults(transport)
.execute(NzrServer::new(ctx, zones).serve())
.await;
});
}
}
struct InstCreateStatus {
inner: JoinHandle<Result<model::Instance, String>>,
progress: Arc<RwLock<crate::ctrl::vm::Progress>>,
}