more stuff
This commit is contained in:
		
							parent
							
								
									27a0f247a4
								
							
						
					
					
						commit
						e7113d6772
					
				
					 19 changed files with 599 additions and 270 deletions
				
			
		
							
								
								
									
										18
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										18
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							|  | @ -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", | ||||
|  |  | |||
|  | @ -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, | ||||
|  |  | |||
|  | @ -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"), | ||||
|  |  | |||
|  | @ -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.
 | ||||
|  |  | |||
|  | @ -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) | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -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]>, | ||||
|  |  | |||
|  | @ -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" | ||||
| tabled = "0.10.0" | ||||
| serde_json = "1" | ||||
| log = "0.4.17" | ||||
| env_logger = "0.10.0" | ||||
|  | @ -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(()) | ||||
|  |  | |||
|  | @ -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(), | ||||
|         } | ||||
|     } | ||||
|  |  | |||
|  | @ -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) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -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) | ||||
|         } | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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(), | ||||
|         }; | ||||
|  |  | |||
|  | @ -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 | ||||
|     } | ||||
|  |  | |||
|  | @ -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>, | ||||
| } | ||||
| 
 | ||||
| //  =^..^=  =^..^=  =^..^=  =^..^=  =^..^=  =^..^=  =^..^=  =^..^=
 | ||||
|  |  | |||
|  | @ -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(), | ||||
|             }), | ||||
|  |  | |||
|  | @ -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() | ||||
|  |  | |||
|  | @ -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; | ||||
|                     } | ||||
|                 } | ||||
|  |  | |||
							
								
								
									
										185
									
								
								nzrd/src/rpc.rs
									
									
									
									
									
								
							
							
						
						
									
										185
									
								
								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<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>>, | ||||
| } | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue