diff --git a/Cargo.lock b/Cargo.lock index 07cf8a8..15be459 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/api/src/args.rs b/api/src/args.rs index b49836d..25dc560 100644 --- a/api/src/args.rs +++ b/api/src/args.rs @@ -8,7 +8,7 @@ pub struct NewInstance { pub name: String, pub title: Option, pub description: Option, - pub interface: String, + pub subnet: String, pub base_image: String, pub cores: u8, pub memory: u32, diff --git a/api/src/config.rs b/api/src/config.rs index 8ca1028..b31dc54 100644 --- a/api/src/config.rs +++ b/api/src/config.rs @@ -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"), diff --git a/api/src/lib.rs b/api/src/lib.rs index 07cd217..b53da7b 100644 --- a/api/src/lib.rs +++ b/api/src/lib.rs @@ -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; + async fn new_instance(build_args: args::NewInstance) -> Result; + /// Poll for the current status of an instance being created. + async fn poll_new_instance(task_id: uuid::Uuid) -> Option; /// 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, String>; + async fn get_instances(with_status: bool) -> Result, 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; + /// Modifies an existing subnet. + async fn modify_subnet(edit_args: Subnet) -> Result; /// Gets a list of existing subnets. async fn get_subnets() -> Result, String>; /// Deletes an existing subnet. diff --git a/api/src/model.rs b/api/src/model.rs index 7c5a94d..b5844f4 100644 --- a/api/src/model.rs +++ b/api/src/model.rs @@ -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>, +} + /// 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, + pub gateway4: Ipv4Addr, /// The primary DNS server for the subnet. pub dns: Vec, /// The base domain used for DNS lookup. pub domain_name: Option, + /// The VLAN ID used for the domain. If none, no VLAN is set. + pub vlan_id: Option, } -/// 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 { - 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) } } diff --git a/api/src/net/mac.rs b/api/src/net/mac.rs index 2c6bcb3..f8527bc 100644 --- a/api/src/net/mac.rs +++ b/api/src/net/mac.rs @@ -26,6 +26,14 @@ impl<'de> Deserialize<'de> for MacAddr { } } +impl std::ops::Index 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(value: T) -> Result where T: AsRef<[u8]>, diff --git a/client/Cargo.toml b/client/Cargo.toml index bd96a87..85301a3 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -12,4 +12,7 @@ home = "0.5.4" 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" \ No newline at end of file +tabled = "0.10.0" +serde_json = "1" +log = "0.4.17" +env_logger = "0.10.0" \ No newline at end of file diff --git a/client/src/main.rs b/client/src/main.rs index c809bde..76ee1db 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -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, @@ -52,10 +52,18 @@ enum InstanceCmd { List, /// Deletes all invalid instances from the database Prune, + /// Shows information on an instance + Dump { + name: Option, + #[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, #[arg(short, long)] pub domain_name: Option, + /// VLAN ID for the VM, if any + #[arg(short, long)] + pub vlan_id: Option, +} + +#[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, + /// Default DNS address for the subnet + #[arg(long)] + pub dns_server: Option, + /// Start address for IP assignment + #[arg(short, long)] + pub start_addr: Option, + /// End address for IP assignment + #[arg(short, long)] + pub end_addr: Option, + /// Domain name associated with the subnet + #[arg(short, long)] + pub domain_name: Option, + /// VLAN ID for the VM, if any + #[arg(short, long)] + pub vlan_id: Option, } #[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 }, } #[derive(Debug, Subcommand)] @@ -160,108 +196,226 @@ impl CommandError { } async fn handle_command() -> Result<(), Box> { - 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::() + .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 = { - 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 = { + 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 = - 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 = + 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 = - 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 = + 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> { async fn main() -> Result<(), Box> { if let Err(err) = handle_command().await { if std::any::Any::type_id(&*err).type_id() == TypeId::of::() { - eprintln!("[err] Error communicating with server: {}", err); + log::error!("Error communicating with server: {}", err); } else { - eprintln!("[err] {}", err); + log::error!("{}", err); } } Ok(()) diff --git a/client/src/table.rs b/client/src/table.rs index f2dd53f..e127064 100644 --- a/client/src/table.rs +++ b/client/src/table.rs @@ -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(), } } diff --git a/nzrd/src/cmd/net.rs b/nzrd/src/cmd/net.rs index dcbe011..688135e 100644 --- a/nzrd/src/cmd/net.rs +++ b/nzrd/src/cmd/net.rs @@ -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> { - 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, Box> { + 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) } } diff --git a/nzrd/src/cmd/vm.rs b/nzrd/src/cmd/vm.rs index 55264cd..6a2b2b3 100644 --- a/nzrd/src/cmd/vm.rs +++ b/nzrd/src/cmd/vm.rs @@ -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>, args: &args::NewInstance, ) -> Result> { + 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) } diff --git a/nzrd/src/ctrl/mod.rs b/nzrd/src/ctrl/mod.rs index 8ef1f2b..296b763 100644 --- a/nzrd/src/ctrl/mod.rs +++ b/nzrd/src/ctrl/mod.rs @@ -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 diff --git a/nzrd/src/ctrl/net.rs b/nzrd/src/ctrl/net.rs index c12b621..38d4d00 100644 --- a/nzrd/src/ctrl/net.rs +++ b/nzrd/src/ctrl/net.rs @@ -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, - pub domain_name: Option, - 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, - ) -> Result { + pub fn from_model(data: &nzr_api::model::SubnetData) -> Result { // 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 { let mut lt_name: Vec = 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 { 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(), }; diff --git a/nzrd/src/ctrl/virtxml/build.rs b/nzrd/src/ctrl/virtxml/build.rs index 7175ee4..ee2ef6a 100644 --- a/nzrd/src/ctrl/virtxml/build.rs +++ b/nzrd/src/ctrl/virtxml/build.rs @@ -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 } diff --git a/nzrd/src/ctrl/virtxml/mod.rs b/nzrd/src/ctrl/virtxml/mod.rs index d513cab..8da58aa 100644 --- a/nzrd/src/ctrl/virtxml/mod.rs +++ b/nzrd/src/ctrl/virtxml/mod.rs @@ -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, source: NetSource, model: NetModel, + target: Option, } // =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= diff --git a/nzrd/src/ctrl/vm.rs b/nzrd/src/ctrl/vm.rs index d34120e..579d825 100644 --- a/nzrd/src/ctrl/vm.rs +++ b/nzrd/src/ctrl/vm.rs @@ -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, lease_addr: CidrV4, } @@ -30,6 +37,21 @@ impl Storable for InstDb { } } +impl From> for nzr_api::model::Instance { + fn from(value: Entity) -> 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, lease: Option>, @@ -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) -> Result { 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(), }), diff --git a/nzrd/src/dns.rs b/nzrd/src/dns.rs index 2e269c8..ab1e496 100644 --- a/nzrd/src/dns.rs +++ b/nzrd/src/dns.rs @@ -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() diff --git a/nzrd/src/main.rs b/nzrd/src/main.rs index dc5d333..07cb3a5 100644 --- a/nzrd/src/main.rs +++ b/nzrd/src/main.rs @@ -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> { let cfg: config::Config = config::Config::figment().extract()?; let ctx = ctx::Context::new(cfg)?; @@ -33,7 +33,7 @@ async fn main() -> Result<(), Box> { 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> { 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> { { 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; } } diff --git a/nzrd/src/rpc.rs b/nzrd/src/rpc.rs index 9ce8047..9136bd0 100644 --- a/nzrd/src/rpc.rs +++ b/nzrd/src/rpc.rs @@ -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>>, } 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 { - 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 { + 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 { + 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, String> { let insts: Vec = 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>::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>::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>::into(&subnet)) + Ok(model::Subnet { + name: String::from_utf8_lossy(subnet.key()).to_string(), + data: <&Subnet as Into>::into(&subnet), + }) + } + + async fn modify_subnet( + self, + _: tarpc::context::Context, + edit_args: model::Subnet, + ) -> Result { + 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, String> { let subnets: Vec = Subnet::all(self.ctx.db.clone()) .map_err(|e| e.to_string())? .filter_map(|s| match s { - Ok(s) => Some(<&Subnet as Into>::into(s.deref())), + Ok(s) => Some(model::Subnet { + name: String::from_utf8(s.key().to_vec()).unwrap(), + data: <&Subnet as Into>::into(s.deref()), + }), Err(err) => { warn!("Iterator error: {}", err); None @@ -157,11 +272,21 @@ pub async fn serve(ctx: Context, zones: ZoneData) -> Result<(), Box>, + progress: Arc>, +}