Initial commit
This commit is contained in:
		
						commit
						4c02261015
					
				
					 35 changed files with 7969 additions and 0 deletions
				
			
		
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							|  | @ -0,0 +1,3 @@ | |||
| /target | ||||
| .DS_Store | ||||
| .vscode | ||||
							
								
								
									
										2332
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										2332
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							
							
								
								
									
										6
									
								
								Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								Cargo.toml
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,6 @@ | |||
| [workspace] | ||||
| members = [ | ||||
|     "nzrd", | ||||
|     "api", | ||||
|     "client", | ||||
| ] | ||||
							
								
								
									
										14
									
								
								api/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								api/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,14 @@ | |||
| [package] | ||||
| name = "nzr-api" | ||||
| version = "0.1.0" | ||||
| edition = "2021" | ||||
| 
 | ||||
| # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html | ||||
| 
 | ||||
| [dependencies] | ||||
| figment = { version = "0.10.8", features = ["json", "toml", "env"] } | ||||
| serde = { version = "1", features = ["derive"] } | ||||
| tarpc = { version = "0.31", features = ["tokio1", "unix"] } | ||||
| tokio = { version = "1.0", features = ["macros"] } | ||||
| uuid = "1.2.2" | ||||
| trust-dns-proto = { version = "0.22.0", features = ["serde-config"] } | ||||
							
								
								
									
										27
									
								
								api/src/args.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								api/src/args.rs
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,27 @@ | |||
| use serde::{Deserialize, Serialize}; | ||||
| 
 | ||||
| use crate::net::cidr::CidrV4; | ||||
| use std::net::Ipv4Addr; | ||||
| 
 | ||||
| #[derive(Debug, Serialize, Deserialize)] | ||||
| pub struct NewInstance { | ||||
|     pub name: String, | ||||
|     pub title: Option<String>, | ||||
|     pub description: Option<String>, | ||||
|     pub interface: String, | ||||
|     pub base_image: String, | ||||
|     pub cores: u8, | ||||
|     pub memory: u32, | ||||
|     pub disk_sizes: (u32, Option<u32>), | ||||
|     pub ssh_keys: Vec<String>, | ||||
| } | ||||
| 
 | ||||
| #[derive(Debug, Serialize, Deserialize)] | ||||
| pub struct NewSubnet { | ||||
|     pub if_name: String, | ||||
|     pub network: CidrV4, | ||||
|     pub start_addr: Option<Ipv4Addr>, | ||||
|     pub end_addr: Option<Ipv4Addr>, | ||||
|     pub gateway: Option<Ipv4Addr>, | ||||
|     pub dns: Vec<Ipv4Addr>, | ||||
| } | ||||
							
								
								
									
										112
									
								
								api/src/config.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								api/src/config.rs
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,112 @@ | |||
| use std::path::PathBuf; | ||||
| 
 | ||||
| use figment::{ | ||||
|     providers::{Env, Format, Json, Toml}, | ||||
|     Figment, Metadata, Provider, | ||||
| }; | ||||
| use serde::{Deserialize, Serialize}; | ||||
| use trust_dns_proto::rr::Name; | ||||
| 
 | ||||
| #[derive(Clone, Debug, Serialize, Deserialize)] | ||||
| pub struct StorageConfig { | ||||
|     pub primary_pool: String, | ||||
|     pub secondary_pool: String, | ||||
|     pub ci_image_pool: String, | ||||
|     pub base_image_pool: String, | ||||
| } | ||||
| 
 | ||||
| #[derive(Clone, Debug, Serialize, Deserialize)] | ||||
| pub struct SOAConfig { | ||||
|     pub nzr_domain: Name, | ||||
|     pub contact: Name, | ||||
|     pub refresh: i32, | ||||
|     pub retry: i32, | ||||
|     pub expire: i32, | ||||
| } | ||||
| 
 | ||||
| #[derive(Clone, Debug, Serialize, Deserialize)] | ||||
| pub struct DNSConfig { | ||||
|     pub listen_addr: String, | ||||
|     pub default_zone: Name, | ||||
|     pub soa: SOAConfig, | ||||
| } | ||||
| 
 | ||||
| #[derive(Clone, Debug, Serialize, Deserialize)] | ||||
| pub struct RPCConfig { | ||||
|     pub socket_path: PathBuf, | ||||
|     pub admin_group: Option<String>, | ||||
| } | ||||
| 
 | ||||
| #[derive(Debug, Serialize, Deserialize)] | ||||
| pub struct Config { | ||||
|     pub rpc: RPCConfig, | ||||
|     pub log_level: String, | ||||
|     pub db_path: PathBuf, | ||||
|     pub libvirt_uri: String, | ||||
|     pub storage: StorageConfig, | ||||
|     pub dns: DNSConfig, | ||||
| } | ||||
| 
 | ||||
| impl Default for Config { | ||||
|     fn default() -> Self { | ||||
|         Self { | ||||
|             log_level: "WARN".to_owned(), | ||||
|             rpc: RPCConfig { | ||||
|                 socket_path: PathBuf::from("/var/run/nazrin/nzrd.sock"), | ||||
|                 admin_group: None, | ||||
|             }, | ||||
|             db_path: PathBuf::from("/var/run/nazrin/nzr.db"), | ||||
|             libvirt_uri: match std::env::var("LIBVIRT_URI") { | ||||
|                 Ok(v) => v, | ||||
|                 Err(_) => String::from("qemu:///system"), | ||||
|             }, | ||||
|             storage: StorageConfig { | ||||
|                 primary_pool: "pri".to_owned(), | ||||
|                 secondary_pool: "data".to_owned(), | ||||
|                 ci_image_pool: "cidata".to_owned(), | ||||
|                 base_image_pool: "images".to_owned(), | ||||
|             }, | ||||
|             dns: DNSConfig { | ||||
|                 listen_addr: "127.0.0.1:5353".to_owned(), | ||||
|                 default_zone: Name::from_utf8("servers.local").unwrap(), | ||||
|                 soa: SOAConfig { | ||||
|                     nzr_domain: Name::from_utf8("nzr.local").unwrap(), | ||||
|                     contact: Name::from_utf8("admin.nzr.local").unwrap(), | ||||
|                     refresh: 86400, | ||||
|                     retry: 7200, | ||||
|                     expire: 3_600_000, | ||||
|                 }, | ||||
|             }, | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl Provider for Config { | ||||
|     fn metadata(&self) -> figment::Metadata { | ||||
|         Metadata::named("Nazrin config") | ||||
|     } | ||||
| 
 | ||||
|     fn data( | ||||
|         &self, | ||||
|     ) -> Result<figment::value::Map<figment::Profile, figment::value::Dict>, figment::Error> { | ||||
|         figment::providers::Serialized::defaults(Config::default()).data() | ||||
|     } | ||||
| 
 | ||||
|     fn profile(&self) -> Option<figment::Profile> { | ||||
|         None | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl Config { | ||||
|     pub fn figment() -> Figment { | ||||
|         let mut fig = Figment::from(Config::default()).merge(Toml::file("/etc/nazrin.conf")); | ||||
| 
 | ||||
|         #[allow(deprecated)] | ||||
|         if let Some(mut home) = std::env::home_dir() { | ||||
|             home.push(".nazrin.conf"); | ||||
|             fig = fig.merge(Json::file(home)); | ||||
|         } | ||||
| 
 | ||||
|         fig.merge(Env::prefixed("NZR_")) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										33
									
								
								api/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								api/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,33 @@ | |||
| use model::{Instance, Subnet}; | ||||
| 
 | ||||
| pub mod args; | ||||
| pub mod config; | ||||
| pub mod model; | ||||
| pub mod net; | ||||
| 
 | ||||
| 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>; | ||||
|     /// 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>; | ||||
|     /// Cleans up unusable entries in the database.
 | ||||
|     async fn garbage_collect() -> Result<(), String>; | ||||
|     /// Creates a new subnet.
 | ||||
|     ///
 | ||||
|     /// Unlike instances, subnets shouldn't perform any changes to the
 | ||||
|     /// 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>; | ||||
|     /// Gets a list of existing subnets.
 | ||||
|     async fn get_subnets() -> Result<Vec<Subnet>, String>; | ||||
|     /// Deletes an existing subnet.
 | ||||
|     async fn delete_subnet(interface: String) -> Result<(), String>; | ||||
| } | ||||
							
								
								
									
										162
									
								
								api/src/model.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										162
									
								
								api/src/model.rs
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,162 @@ | |||
| use serde::{Deserialize, Serialize}; | ||||
| use std::{fmt, net::Ipv4Addr, str::FromStr}; | ||||
| use trust_dns_proto::rr::Name; | ||||
| 
 | ||||
| use crate::net::{cidr::CidrV4, mac::MacAddr}; | ||||
| 
 | ||||
| #[derive(Copy, Clone, Debug, Serialize, Deserialize)] | ||||
| #[repr(u32)] | ||||
| pub enum DomainState { | ||||
|     NoState = 0u32, | ||||
|     Running, | ||||
|     Blocked, | ||||
|     Paused, | ||||
|     ShuttingDown, | ||||
|     ShutOff, | ||||
|     Crashed, | ||||
|     Suspended, | ||||
|     Unknown(u32), | ||||
| } | ||||
| 
 | ||||
| impl Default for DomainState { | ||||
|     fn default() -> Self { | ||||
|         Self::NoState | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl From<u32> for DomainState { | ||||
|     fn from(value: u32) -> Self { | ||||
|         match value { | ||||
|             0 => Self::NoState, | ||||
|             1 => Self::Running, | ||||
|             2 => Self::Blocked, | ||||
|             3 => Self::Paused, | ||||
|             4 => Self::ShuttingDown, | ||||
|             5 => Self::ShutOff, | ||||
|             6 => Self::Crashed, | ||||
|             7 => Self::Suspended, | ||||
|             other => Self::Unknown(other), | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl fmt::Display for DomainState { | ||||
|     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||||
|         write!( | ||||
|             f, | ||||
|             "{}", | ||||
|             match self { | ||||
|                 Self::NoState => "no state".to_owned(), | ||||
|                 Self::Running => "running".to_owned(), | ||||
|                 Self::Blocked => "blocked".to_owned(), | ||||
|                 Self::Paused => "paused".to_owned(), | ||||
|                 Self::ShuttingDown => "shutting down".to_owned(), | ||||
|                 Self::ShutOff => "shut off".to_owned(), | ||||
|                 Self::Crashed => "crashed".to_owned(), | ||||
|                 Self::Suspended => "suspended".to_owned(), | ||||
|                 Self::Unknown(code) => format!("unknown ({})", code), | ||||
|             } | ||||
|         ) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| /// Struct representing a VM instance.
 | ||||
| #[derive(Debug, Serialize, Deserialize)] | ||||
| pub struct Instance { | ||||
|     pub name: String, | ||||
|     pub uuid: uuid::Uuid, | ||||
|     pub lease: Option<Lease>, | ||||
|     pub state: DomainState, | ||||
| } | ||||
| 
 | ||||
| /// Struct representing a logical "lease" held by a VM.
 | ||||
| #[derive(Debug, Serialize, Deserialize)] | ||||
| pub struct Lease { | ||||
|     /// The IPv4 address held by the Lease
 | ||||
|     pub addr: CidrV4, | ||||
|     /// The MAC address associated by the Lease
 | ||||
|     pub mac_addr: MacAddr, | ||||
| } | ||||
| 
 | ||||
| /// Struct representing a subnet used by the host for virtual
 | ||||
| /// networking.
 | ||||
| #[derive(Debug, Serialize, Deserialize)] | ||||
| pub struct Subnet { | ||||
|     /// The name of the interface the subnet is accessible via.
 | ||||
|     pub ifname: IfaceStr, | ||||
|     pub data: SubnetData, | ||||
| } | ||||
| 
 | ||||
| #[derive(Debug, Serialize, Deserialize)] | ||||
| pub struct SubnetData { | ||||
|     /// The network information for the subnet.
 | ||||
|     pub network: CidrV4, | ||||
|     /// The first host address that can be assigned dynamically
 | ||||
|     /// on the subnet.
 | ||||
|     pub start_host: Ipv4Addr, | ||||
|     /// The last host address that can be assigned dynamically
 | ||||
|     /// on the subnet.
 | ||||
|     pub end_host: Ipv4Addr, | ||||
|     /// The default gateway for the subnet.
 | ||||
|     pub gateway4: Option<Ipv4Addr>, | ||||
|     /// The primary DNS server for the subnet.
 | ||||
|     pub dns: Vec<Ipv4Addr>, | ||||
|     /// The base domain used for DNS lookup.
 | ||||
|     pub domain_name: Option<Name>, | ||||
| } | ||||
| 
 | ||||
| /// 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 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)? | ||||
|         ) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										257
									
								
								api/src/net/cidr.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										257
									
								
								api/src/net/cidr.rs
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,257 @@ | |||
| use std::fmt; | ||||
| use std::net::{AddrParseError, Ipv4Addr}; | ||||
| use std::num::ParseIntError; | ||||
| use std::str::FromStr; | ||||
| 
 | ||||
| use serde::{de, Deserialize, Serialize}; | ||||
| 
 | ||||
| #[derive(Debug)] | ||||
| pub enum Error { | ||||
|     Malformed, | ||||
|     AddrParse(AddrParseError), | ||||
|     SuffixParse(ParseIntError), | ||||
|     InvalidSize, | ||||
|     HostBitsTooLarge, | ||||
| } | ||||
| 
 | ||||
| impl fmt::Display for Error { | ||||
|     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||||
|         match self { | ||||
|             Self::Malformed => write!(f, "Malformed address/cidr combination"), | ||||
|             Self::AddrParse(er) => write!(f, "Couldn't parse address: {}", er), | ||||
|             Self::SuffixParse(er) => write!(f, "Couldn't parse CIDR suffix: {}", er), | ||||
|             Self::InvalidSize => write!(f, "Byte array needs to be at least 5 bytes long"), | ||||
|             Self::HostBitsTooLarge => write!( | ||||
|                 f, | ||||
|                 "Provided host does not match bits allowed by subnet mask" | ||||
|             ), | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl std::error::Error for Error {} | ||||
| 
 | ||||
| #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] | ||||
| pub struct CidrV4 { | ||||
|     pub addr: Ipv4Addr, | ||||
|     cidr: u8, | ||||
|     netmask: u32, | ||||
| } | ||||
| 
 | ||||
| impl fmt::Display for CidrV4 { | ||||
|     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||||
|         write!(f, "{}/{}", self.addr, self.cidr) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl FromStr for CidrV4 { | ||||
|     type Err = Error; | ||||
|     fn from_str(s: &str) -> Result<Self, Self::Err> { | ||||
|         let parts = s.split('/').collect::<Vec<&str>>(); | ||||
| 
 | ||||
|         match parts.len() { | ||||
|             2 => { | ||||
|                 let addr = Ipv4Addr::from_str(parts[0]).map_err(Error::AddrParse)?; | ||||
|                 let cidr = u8::from_str(parts[1]).map_err(Error::SuffixParse)?; | ||||
|                 Ok(CidrV4::new(addr, cidr)) | ||||
|             } | ||||
|             _ => Err(Error::Malformed), | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl Serialize for CidrV4 { | ||||
|     fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> | ||||
|     where | ||||
|         S: serde::Serializer, | ||||
|     { | ||||
|         serializer.serialize_str(&self.to_string()) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl<'de> Deserialize<'de> for CidrV4 { | ||||
|     fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> | ||||
|     where | ||||
|         D: serde::Deserializer<'de>, | ||||
|     { | ||||
|         let s: String = Deserialize::deserialize(deserializer)?; | ||||
|         CidrV4::from_str(s.as_str()).map_err(de::Error::custom) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl From<[u8; 5]> for CidrV4 { | ||||
|     fn from(octets: [u8; 5]) -> Self { | ||||
|         let addr = Ipv4Addr::new(octets[0], octets[1], octets[2], octets[3]); | ||||
|         let cidr = octets[4]; | ||||
|         Self::new(addr, cidr) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl TryFrom<&[u8]> for CidrV4 { | ||||
|     type Error = Error; | ||||
|     fn try_from(value: &[u8]) -> Result<Self, Self::Error> { | ||||
|         if value.len() < 5 { | ||||
|             Err(Error::InvalidSize) | ||||
|         } else { | ||||
|             // unwrap should be fine here, since we already validate for size?
 | ||||
|             let arr: [u8; 5] = value[0..5].try_into().unwrap(); | ||||
|             Ok(Self::from(arr)) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl CidrV4 { | ||||
|     pub fn new(addr: Ipv4Addr, cidr: u8) -> Self { | ||||
|         let netmask = match cidr { | ||||
|             0 => 0, | ||||
|             32.. => u32::MAX, | ||||
|             c => ((1u32 << c) - 1) << (32 - c), | ||||
|         }; | ||||
| 
 | ||||
|         CidrV4 { | ||||
|             addr, | ||||
|             cidr, | ||||
|             netmask, | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /// Get the subnet mask as an Ipv4Addr.
 | ||||
|     pub fn netmask(&self) -> Ipv4Addr { | ||||
|         Ipv4Addr::from(self.netmask) | ||||
|     } | ||||
| 
 | ||||
|     /// Get the network address.
 | ||||
|     pub fn network(&self) -> CidrV4 { | ||||
|         CidrV4::new( | ||||
|             Ipv4Addr::from(u32::from(self.addr) & self.netmask), | ||||
|             self.cidr, | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     /// Get the network bit length in CIDR notation.
 | ||||
|     pub fn cidr(&self) -> u8 { | ||||
|         self.cidr | ||||
|     } | ||||
| 
 | ||||
|     /// Get the broadcast address for the network.
 | ||||
|     pub fn broadcast(&self) -> Ipv4Addr { | ||||
|         Ipv4Addr::from(u32::from(self.network().addr) | !self.netmask) | ||||
|     } | ||||
| 
 | ||||
|     /// Determine if a network contains a given address.
 | ||||
|     ///
 | ||||
|     /// This method is not affected by the object's host address.
 | ||||
|     pub fn contains(&self, addr: &Ipv4Addr) -> bool { | ||||
|         if self.cidr == 32 { | ||||
|             &self.addr == addr | ||||
|         } else { | ||||
|             &self.broadcast() > addr && addr > &self.network().addr | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /// Gets only the host bits for a given Ipv4 address.
 | ||||
|     ///
 | ||||
|     /// This method is not affected by the object's host address.
 | ||||
|     pub fn host_bits(&self, addr: &Ipv4Addr) -> u32 { | ||||
|         let addr_bits = u32::from(*addr); | ||||
|         addr_bits & !self.netmask | ||||
|     } | ||||
| 
 | ||||
|     /// Create an IP from a given u32 of host bits.
 | ||||
|     ///
 | ||||
|     /// This method is not affected by the object's host address.
 | ||||
|     pub fn make_ip(&self, host_bits: u32) -> Result<Ipv4Addr, Error> { | ||||
|         if host_bits > !self.netmask { | ||||
|             Err(Error::HostBitsTooLarge) | ||||
|         } else { | ||||
|             Ok((host_bits | u32::from(self.network().addr)).into()) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /// Get the address and CIDR bit length as an array of bytes.
 | ||||
|     ///
 | ||||
|     /// This is in big endian format, so e.g., 192.168.0.5/24 would
 | ||||
|     /// be returned as `[192, 168, 0, 5, 24]`.
 | ||||
|     pub fn octets(&self) -> [u8; 5] { | ||||
|         [self.network().addr.octets().as_slice(), &[self.cidr]] | ||||
|             .concat() | ||||
|             .try_into() | ||||
|             .unwrap() | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[cfg(test)] | ||||
| mod test { | ||||
|     use crate::net::cidr::CidrV4; | ||||
|     use std::{net::Ipv4Addr, str::FromStr}; | ||||
| 
 | ||||
|     const ADDR_BYTES: u32 = 0xc0_a8_02_a9; // 192.168.2.169
 | ||||
|     #[allow(clippy::unusual_byte_groupings)] | ||||
|     const NETWORK_TRUTH_MAP: &[(u8, u32, u32, [u8; 5])] = &[ | ||||
|         ( | ||||
|             8, | ||||
|             0b11111111_00000000_00000000_00000000, | ||||
|             0xc0_00_00_00, | ||||
|             [0xc0, 0, 0, 0, 8], | ||||
|         ), | ||||
|         ( | ||||
|             16, | ||||
|             0b11111111_11111111_00000000_00000000, | ||||
|             0xc0_a8_00_00, | ||||
|             [0xc0, 0xa8, 0, 0, 16], | ||||
|         ), | ||||
|         ( | ||||
|             25, | ||||
|             0b11111111_11111111_11111111_10000000, | ||||
|             0xc0_a8_02_80, | ||||
|             [0xc0, 0xa8, 0x02, 0x80, 25], | ||||
|         ), | ||||
|         (0, 0, 0, [0u8; 5]), | ||||
|         ( | ||||
|             32, | ||||
|             0b11111111_11111111_11111111_11111111, | ||||
|             0xc0_a8_02_a9, | ||||
|             [0xc0, 0xa8, 0x02, 0xa9, 32], | ||||
|         ), | ||||
|     ]; | ||||
| 
 | ||||
|     #[test] | ||||
|     fn v4_constructor() { | ||||
|         for (cidr, netmask, net_addr, _) in NETWORK_TRUTH_MAP { | ||||
|             let test = CidrV4::new(Ipv4Addr::from(ADDR_BYTES), *cidr); | ||||
|             assert_eq!(u32::from(test.netmask()), *netmask); | ||||
|             assert_eq!(test.cidr(), *cidr); | ||||
|             assert_eq!(u32::from(test.network().addr), *net_addr); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     #[test] | ||||
|     fn v4_fromstr() { | ||||
|         let addr = Ipv4Addr::from(ADDR_BYTES); | ||||
|         for (cidr, _netmask, net_addr, _) in NETWORK_TRUTH_MAP { | ||||
|             let addr_str = format!("{}/{}", addr, cidr); | ||||
|             println!(">> {}", addr_str); | ||||
|             let cidr = CidrV4::from_str(&addr_str).unwrap(); | ||||
|             assert_eq!(cidr.network().addr, Ipv4Addr::from(*net_addr)); | ||||
|             assert!(cidr.contains(&addr)); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     #[test] | ||||
|     fn v4_contains() { | ||||
|         let yes = Ipv4Addr::from(ADDR_BYTES); | ||||
|         let no = Ipv4Addr::from(0x08_08_08_08); | ||||
| 
 | ||||
|         let cidr = CidrV4::new(yes, 8); | ||||
|         assert!(cidr.contains(&yes)); | ||||
|         assert!(!cidr.contains(&no)); | ||||
|     } | ||||
| 
 | ||||
|     #[test] | ||||
|     fn v4_octets() { | ||||
|         for (cidr, _, _, netbytes) in NETWORK_TRUTH_MAP { | ||||
|             let test = CidrV4::new(Ipv4Addr::from(ADDR_BYTES), *cidr); | ||||
|             assert_eq!(&test.octets(), netbytes); | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										96
									
								
								api/src/net/mac.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								api/src/net/mac.rs
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,96 @@ | |||
| use std::{fmt, str::FromStr}; | ||||
| 
 | ||||
| use serde::{de, Deserialize, Serialize}; | ||||
| 
 | ||||
| #[derive(Clone, Debug, PartialEq, Eq)] | ||||
| pub struct MacAddr { | ||||
|     octets: [u8; 6], | ||||
| } | ||||
| 
 | ||||
| impl Serialize for MacAddr { | ||||
|     fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> | ||||
|     where | ||||
|         S: serde::Serializer, | ||||
|     { | ||||
|         serializer.serialize_str(&self.to_string()) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl<'de> Deserialize<'de> for MacAddr { | ||||
|     fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> | ||||
|     where | ||||
|         D: serde::Deserializer<'de>, | ||||
|     { | ||||
|         let s: String = Deserialize::deserialize(deserializer)?; | ||||
|         MacAddr::from_str(s.as_str()).map_err(de::Error::custom) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[derive(Debug)] | ||||
| pub enum Error { | ||||
|     ParseError(std::num::ParseIntError), | ||||
|     SizeError(usize), | ||||
| } | ||||
| 
 | ||||
| impl fmt::Display for Error { | ||||
|     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||||
|         match self { | ||||
|             Self::ParseError(er) => write!(f, "Couldn't parse octets: {}", er), | ||||
|             Self::SizeError(sz) => write!(f, "Too many octets; expected 6, string had {}", sz), | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl std::error::Error for Error {} | ||||
| 
 | ||||
| impl fmt::Display for MacAddr { | ||||
|     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||||
|         write!( | ||||
|             f, | ||||
|             "{}", | ||||
|             self.octets.map(|oct| format!("{:02x}", oct)).join(":") | ||||
|         ) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl FromStr for MacAddr { | ||||
|     type Err = Error; | ||||
|     fn from_str(s: &str) -> Result<Self, Self::Err> { | ||||
|         let octets = s | ||||
|             .split(':') | ||||
|             .map(|s| u8::from_str_radix(s, 16)) | ||||
|             .collect::<Result<Vec<u8>, std::num::ParseIntError>>() | ||||
|             .map_err(Error::ParseError)?; | ||||
|         Ok(MacAddr { | ||||
|             octets: octets | ||||
|                 .try_into() | ||||
|                 .map_err(|v: Vec<u8>| Error::SizeError(v.len()))?, | ||||
|         }) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl MacAddr { | ||||
|     pub fn new(a: u8, b: u8, c: u8, d: u8, e: u8, f: u8) -> MacAddr { | ||||
|         MacAddr { | ||||
|             octets: [a, b, c, d, e, f], | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     pub fn from_bytes<T>(value: T) -> Result<MacAddr, Error> | ||||
|     where | ||||
|         T: AsRef<[u8]>, | ||||
|     { | ||||
|         let slice = value.as_ref(); | ||||
|         if slice.len() < 6 { | ||||
|             Err(Error::SizeError(slice.len())) | ||||
|         } else { | ||||
|             Ok(MacAddr { | ||||
|                 octets: slice[0..6].try_into().unwrap(), | ||||
|             }) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     pub fn octets(&self) -> [u8; 6] { | ||||
|         self.octets | ||||
|     } | ||||
| } | ||||
							
								
								
									
										2
									
								
								api/src/net/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								api/src/net/mod.rs
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,2 @@ | |||
| pub mod cidr; | ||||
| pub mod mac; | ||||
							
								
								
									
										15
									
								
								client/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								client/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,15 @@ | |||
| [package] | ||||
| name = "nzr" | ||||
| version = "0.1.0" | ||||
| edition = "2021" | ||||
| 
 | ||||
| # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html | ||||
| 
 | ||||
| [dependencies] | ||||
| nzr-api = { path = "../api" } | ||||
| clap = { version = "4.0.26", features = ["derive"] } | ||||
| 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" | ||||
							
								
								
									
										290
									
								
								client/src/main.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										290
									
								
								client/src/main.rs
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,290 @@ | |||
| use clap::{Parser, Subcommand}; | ||||
| use nzr_api::model; | ||||
| use nzr_api::net::cidr::CidrV4; | ||||
| use nzr_api::trust_dns_proto::rr::Name; | ||||
| use nzr_api::{config, NazrinClient}; | ||||
| use std::any::{Any, TypeId}; | ||||
| use std::path::PathBuf; | ||||
| use std::str::FromStr; | ||||
| use tarpc::tokio_serde::formats::Bincode; | ||||
| use tarpc::tokio_util::codec::LengthDelimitedCodec; | ||||
| use tokio::net::UnixStream; | ||||
| 
 | ||||
| mod table; | ||||
| 
 | ||||
| #[derive(Debug, clap::Args)] | ||||
| pub struct NewInstanceArgs { | ||||
|     /// Name of the instance to be created
 | ||||
|     name: String, | ||||
|     /// Bridge the instance will initially run on
 | ||||
|     #[arg(short, long)] | ||||
|     interface: String, | ||||
|     /// Long description of the instance
 | ||||
|     #[arg(long)] | ||||
|     description: Option<String>, | ||||
|     /// Base image to use for the instance
 | ||||
|     #[arg(short, long)] | ||||
|     base: String, | ||||
|     ///How many cores to assign to the instance
 | ||||
|     #[arg(short, long, default_value_t = 2)] | ||||
|     cores: u8, | ||||
|     /// Memory to assign, in MiB
 | ||||
|     #[arg(short, long, default_value_t = 1024)] | ||||
|     mem: u32, | ||||
|     /// Primary HDD size, in MiB
 | ||||
|     #[arg(short, long, default_value_t = 20)] | ||||
|     primary_size: u32, | ||||
|     /// Secndary HDD size, in MiB
 | ||||
|     #[arg(short, long)] | ||||
|     secondary_size: Option<u32>, | ||||
|     /// File containing a list of SSH keys to use
 | ||||
|     #[arg(long)] | ||||
|     sshkey_file: Option<PathBuf>, | ||||
| } | ||||
| 
 | ||||
| #[derive(Debug, Subcommand)] | ||||
| enum InstanceCmd { | ||||
|     /// Create a new instance
 | ||||
|     New(NewInstanceArgs), | ||||
|     /// Delete an existing instance
 | ||||
|     Delete { name: String }, | ||||
|     /// List all instances
 | ||||
|     List, | ||||
|     /// Deletes all invalid instances from the database
 | ||||
|     Prune, | ||||
| } | ||||
| 
 | ||||
| #[derive(Debug, clap::Args)] | ||||
| pub struct AddNetArgs { | ||||
|     /// 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)
 | ||||
|     pub network: 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>, | ||||
|     #[arg(short, long)] | ||||
|     pub domain_name: Option<Name>, | ||||
| } | ||||
| 
 | ||||
| #[derive(Debug, Subcommand)] | ||||
| enum NetCmd { | ||||
|     /// Add a new network to the database
 | ||||
|     Add(AddNetArgs), | ||||
|     /// List all networks in the database
 | ||||
|     List, | ||||
|     /// Delete a network from the database
 | ||||
|     Delete { | ||||
|         #[arg(short, long)] | ||||
|         interface: String, | ||||
|     }, | ||||
| } | ||||
| 
 | ||||
| #[derive(Debug, Subcommand)] | ||||
| enum Commands { | ||||
|     /// Commands for managing instances
 | ||||
|     Instance { | ||||
|         #[command(subcommand)] | ||||
|         command: InstanceCmd, | ||||
|     }, | ||||
|     /// Commands for managing network assignments
 | ||||
|     Net { | ||||
|         #[command(subcommand)] | ||||
|         command: NetCmd, | ||||
|     }, | ||||
| } | ||||
| 
 | ||||
| #[derive(Parser, Debug)] | ||||
| #[command(version, about, long_about = "")] | ||||
| struct Args { | ||||
|     #[command(subcommand)] | ||||
|     command: Commands, | ||||
| } | ||||
| 
 | ||||
| #[derive(Debug)] | ||||
| struct CommandError { | ||||
|     inner: Option<Box<dyn std::error::Error>>, | ||||
|     message: String, | ||||
| } | ||||
| 
 | ||||
| impl std::fmt::Display for CommandError { | ||||
|     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||||
|         write!(f, "{}", &self.message)?; | ||||
|         if let Some(inner) = &self.inner { | ||||
|             write!(f, "\n  inner: {:?}", &inner)?; | ||||
|         } | ||||
|         Ok(()) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl std::error::Error for CommandError {} | ||||
| 
 | ||||
| impl From<String> for CommandError { | ||||
|     fn from(value: String) -> Self { | ||||
|         Self { | ||||
|             inner: None, | ||||
|             message: value, | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl From<&str> for CommandError { | ||||
|     fn from(value: &str) -> Self { | ||||
|         Self { | ||||
|             inner: None, | ||||
|             message: value.to_owned(), | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl CommandError { | ||||
|     fn new<S, E>(message: S, inner: E) -> Self | ||||
|     where | ||||
|         S: AsRef<str>, | ||||
|         E: std::error::Error + 'static, | ||||
|     { | ||||
|         Self { | ||||
|             message: message.as_ref().to_owned(), | ||||
|             inner: Some(Box::new(inner)), | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| async fn handle_command() -> Result<(), Box<dyn std::error::Error>> { | ||||
|     let cli = Args::parse(); | ||||
|     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 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, | ||||
|                         )?; | ||||
| 
 | ||||
|                         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, | ||||
|                         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?)?, | ||||
|             } | ||||
|         } | ||||
|         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)?, | ||||
|                     data: model::SubnetData { | ||||
|                         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, | ||||
|                         dns: args.dns_server.map_or(Vec::new(), |d| vec![d]), | ||||
|                         domain_name: args.domain_name, | ||||
|                     }, | ||||
|                 }; | ||||
|                 (client | ||||
|                     .new_subnet(tarpc::context::current(), build_args) | ||||
|                     .await?)?; | ||||
|             } | ||||
|             NetCmd::Delete { interface } => { | ||||
|                 (client | ||||
|                     .delete_subnet(tarpc::context::current(), interface) | ||||
|                     .await?)?; | ||||
|             } | ||||
|             NetCmd::List => { | ||||
|                 let subnets = client.get_subnets(tarpc::context::current()).await?; | ||||
| 
 | ||||
|                 let tabular: Vec<table::Subnet> = | ||||
|                     subnets?.iter().map(table::Subnet::from).collect(); | ||||
|                 let mut table = tabled::Table::new(&tabular); | ||||
|                 println!("{}", table.with(tabled::Style::psql())); | ||||
|             } | ||||
|         }, | ||||
|     }; | ||||
|     Ok(()) | ||||
| } | ||||
| 
 | ||||
| #[tokio::main] | ||||
| 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); | ||||
|         } else { | ||||
|             eprintln!("[err] {}", err); | ||||
|         } | ||||
|     } | ||||
|     Ok(()) | ||||
| } | ||||
							
								
								
									
										42
									
								
								client/src/table.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								client/src/table.rs
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,42 @@ | |||
| use nzr_api::model; | ||||
| use tabled::Tabled; | ||||
| 
 | ||||
| #[derive(Tabled)] | ||||
| pub struct Instance { | ||||
|     #[tabled(rename = "Hostname")] | ||||
|     hostname: String, | ||||
|     #[tabled(rename = "IP Address")] | ||||
|     ip_addr: String, | ||||
|     #[tabled(rename = "State")] | ||||
|     state: model::DomainState, | ||||
| } | ||||
| 
 | ||||
| impl From<&model::Instance> for Instance { | ||||
|     fn from(value: &model::Instance) -> Self { | ||||
|         Self { | ||||
|             hostname: value.name.to_owned(), | ||||
|             ip_addr: value | ||||
|                 .lease | ||||
|                 .as_ref() | ||||
|                 .map_or("(none)".to_owned(), |lease| lease.addr.to_string()), | ||||
|             state: value.state, | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[derive(Tabled)] | ||||
| pub struct Subnet { | ||||
|     #[tabled(rename = "Interface")] | ||||
|     interface: String, | ||||
|     #[tabled(rename = "Network")] | ||||
|     network: String, | ||||
| } | ||||
| 
 | ||||
| impl From<&model::Subnet> for Subnet { | ||||
|     fn from(value: &model::Subnet) -> Self { | ||||
|         Self { | ||||
|             interface: value.ifname.to_string(), | ||||
|             network: value.data.network.to_string(), | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										11
									
								
								deploy.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										11
									
								
								deploy.sh
									
									
									
									
									
										Executable file
									
								
							|  | @ -0,0 +1,11 @@ | |||
| #!/usr/bin/env bash | ||||
| 
 | ||||
| RHOST="$1" | ||||
| 
 | ||||
| if [ -z "$RHOST" ] ; then | ||||
|     echo "usage: $0 remote-host" >&2 | ||||
|     exit 1 | ||||
| fi | ||||
| 
 | ||||
| cargo build --profile $PROFILE && \ | ||||
|     scp "$PWD/target/${PROFILE:-debug}/nzr"{,d} "$RHOST:" | ||||
							
								
								
									
										1353
									
								
								nzrd/Cargo.lock
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										1353
									
								
								nzrd/Cargo.lock
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							
							
								
								
									
										44
									
								
								nzrd/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								nzrd/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,44 @@ | |||
| [package] | ||||
| name = "nzrd" | ||||
| version = "0.1.0" | ||||
| edition = "2021" | ||||
| 
 | ||||
| # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html | ||||
| 
 | ||||
| [dependencies] | ||||
| tarpc = { version = "0.31", features = ["tokio1", "unix", "serde-transport", "serde-transport-bincode"] } | ||||
| tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] } | ||||
| tokio-serde = { version = "0.8.0", features = ["bincode"] } | ||||
| sled = "0.34.7" | ||||
| # virt = "0.2.11" | ||||
| virt = { path = "../../libvirt-rust" } | ||||
| fatfs = "0.3" | ||||
| uuid = { version = "1.2.2", features = ["v4", "fast-rng", "serde", "macro-diagnostics"] } | ||||
| clap = { version = "4.0.26", features = ["derive"] } | ||||
| serde = { version = "1", features = ["derive"] } | ||||
| # for new @attr support, awaiting 0.27.0 | ||||
| quick-xml = { git = "https://github.com/tafia/quick-xml", rev = "fb079b6714d7238d5180aaa098c5f9b02dbcc7da", features = ["serialize"] } | ||||
| serde_with = "2.1.0" | ||||
| serde_yaml = "0.9.14" | ||||
| rand = "0.8.5" | ||||
| libc = "0.2.137" | ||||
| home = "0.5.4" | ||||
| stdext = "0.3.1" | ||||
| zerocopy = "0.6.1" | ||||
| nzr-api = { path = "../api" } | ||||
| futures = "0.3" | ||||
| async-trait = "0.1.60" | ||||
| ciborium = "0.2.0" | ||||
| ciborium-io = "0.2.0" | ||||
| trust-dns-server = "0.22.0" | ||||
| log = "0.4.17" | ||||
| syslog = "6.0.1" | ||||
| nix = "0.26.1" | ||||
| 
 | ||||
| [dev-dependencies] | ||||
| tempdir = "0.3.7" | ||||
| regex = "1" | ||||
| 
 | ||||
| [[bin]] | ||||
| name = "nzrd" | ||||
| path = "src/main.rs" | ||||
							
								
								
									
										192
									
								
								nzrd/src/cloud.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										192
									
								
								nzrd/src/cloud.rs
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,192 @@ | |||
| use std::net::Ipv4Addr; | ||||
| 
 | ||||
| use fatfs::FsOptions; | ||||
| use serde::Serialize; | ||||
| use serde_with::skip_serializing_none; | ||||
| use std::collections::HashMap; | ||||
| use std::io::{prelude::*, Cursor}; | ||||
| use trust_dns_server::proto::rr::Name; | ||||
| 
 | ||||
| use nzr_api::net::{cidr::CidrV4, mac::MacAddr}; | ||||
| 
 | ||||
| #[derive(Debug, Serialize)] | ||||
| #[serde(rename_all = "kebab-case")] | ||||
| pub struct Metadata<'a> { | ||||
|     instance_id: &'a str, | ||||
|     local_hostname: &'a str, | ||||
|     public_keys: Option<Vec<&'a String>>, | ||||
| } | ||||
| 
 | ||||
| impl<'a> Metadata<'a> { | ||||
|     pub fn new(instance_id: &'a str) -> Self { | ||||
|         Self { | ||||
|             instance_id, | ||||
|             local_hostname: instance_id, | ||||
|             public_keys: None, | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     pub fn ssh_pubkeys(mut self, pubkeys: &'a [String]) -> Self { | ||||
|         self.public_keys = Some(pubkeys.iter().filter(|i| !i.is_empty()).collect()); | ||||
|         self | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[derive(Debug, Serialize)] | ||||
| pub struct NetworkMeta<'a> { | ||||
|     version: u32, | ||||
|     ethernets: HashMap<String, EtherNic<'a>>, | ||||
|     #[serde(skip)] | ||||
|     ethnum: u8, | ||||
| } | ||||
| 
 | ||||
| impl<'a> NetworkMeta<'a> { | ||||
|     pub fn new() -> Self { | ||||
|         Self { | ||||
|             version: 2, | ||||
|             ethernets: HashMap::new(), | ||||
|             ethnum: 0, | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /// Define a NIC with a static address.
 | ||||
|     pub fn static_nic( | ||||
|         mut self, | ||||
|         match_data: EtherMatch<'a>, | ||||
|         cidr: &'a CidrV4, | ||||
|         gateway: &'a Ipv4Addr, | ||||
|         dns: DNSMeta<'a>, | ||||
|     ) -> Self { | ||||
|         self.ethernets.insert( | ||||
|             format!("eth{}", self.ethnum), | ||||
|             EtherNic { | ||||
|                 r#match: match_data, | ||||
|                 addresses: Some(vec![cidr]), | ||||
|                 gateway4: Some(gateway), | ||||
|                 dhcp4: false, | ||||
|                 nameservers: Some(dns), | ||||
|             }, | ||||
|         ); | ||||
|         self.ethnum += 1; | ||||
|         self | ||||
|     } | ||||
| 
 | ||||
|     #[allow(dead_code)] | ||||
|     pub fn dhcp_nic(mut self, match_data: EtherMatch<'a>) -> Self { | ||||
|         self.ethernets.insert( | ||||
|             format!("eth{}", self.ethnum), | ||||
|             EtherNic { | ||||
|                 r#match: match_data, | ||||
|                 addresses: None, | ||||
|                 gateway4: None, | ||||
|                 dhcp4: true, | ||||
|                 nameservers: None, | ||||
|             }, | ||||
|         ); | ||||
|         self.ethnum += 1; | ||||
|         self | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[derive(Debug, Serialize)] | ||||
| pub struct Ethernets<'a> { | ||||
|     nics: Vec<EtherNic<'a>>, | ||||
| } | ||||
| 
 | ||||
| #[derive(Debug, Serialize)] | ||||
| pub struct EtherNic<'a> { | ||||
|     r#match: EtherMatch<'a>, | ||||
|     addresses: Option<Vec<&'a CidrV4>>, | ||||
|     gateway4: Option<&'a Ipv4Addr>, | ||||
|     dhcp4: bool, | ||||
|     nameservers: Option<DNSMeta<'a>>, | ||||
| } | ||||
| 
 | ||||
| #[skip_serializing_none] | ||||
| #[derive(Default, Debug, Serialize)] | ||||
| pub struct EtherMatch<'a> { | ||||
|     name: Option<&'a str>, | ||||
|     macaddress: Option<&'a MacAddr>, | ||||
|     driver: Option<&'a str>, | ||||
| } | ||||
| 
 | ||||
| impl<'a> EtherMatch<'a> { | ||||
|     #[allow(dead_code)] | ||||
|     pub fn name(name: &'a str) -> Self { | ||||
|         Self { | ||||
|             name: Some(name), | ||||
|             ..Default::default() | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     pub fn mac_addr(addr: &'a MacAddr) -> Self { | ||||
|         Self { | ||||
|             macaddress: Some(addr), | ||||
|             ..Default::default() | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     #[allow(dead_code)] | ||||
|     pub fn driver(driver: &'a str) -> Self { | ||||
|         Self { | ||||
|             driver: Some(driver), | ||||
|             ..Default::default() | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[derive(Debug, Serialize)] | ||||
| pub struct DNSMeta<'a> { | ||||
|     search: Vec<Name>, | ||||
|     addresses: &'a Vec<Ipv4Addr>, | ||||
| } | ||||
| 
 | ||||
| impl<'a> DNSMeta<'a> { | ||||
|     pub fn with_addrs(search: Option<Vec<Name>>, addrs: &'a Vec<Ipv4Addr>) -> Self { | ||||
|         Self { | ||||
|             addresses: addrs, | ||||
|             search: search.unwrap_or_default(), | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| pub fn create_image<B>( | ||||
|     metadata: &Metadata, | ||||
|     netconfig: &NetworkMeta, | ||||
|     user_data: Option<&B>, | ||||
| ) -> Result<Cursor<Vec<u8>>, Box<dyn std::error::Error>> | ||||
| where | ||||
|     B: AsRef<[u8]>, | ||||
| { | ||||
|     let mut image: Cursor<Vec<u8>> = Cursor::new(Vec::new()); | ||||
| 
 | ||||
|     // format a:
 | ||||
|     fatfs::format_volume( | ||||
|         &mut image, | ||||
|         fatfs::FormatVolumeOptions::new() | ||||
|             .volume_label(*b"cidata     ") | ||||
|             .fat_type(fatfs::FatType::Fat12) | ||||
|             .total_sectors(2880), | ||||
|     )?; | ||||
| 
 | ||||
|     { | ||||
|         let fs = fatfs::FileSystem::new(&mut image, FsOptions::new())?; | ||||
|         let rootdir = fs.root_dir(); | ||||
| 
 | ||||
|         let md_data = serde_yaml::to_string(&metadata)?; | ||||
|         let mut md_fd = rootdir.create_file("meta-data")?; | ||||
|         md_fd.write_all(md_data.as_bytes())?; | ||||
| 
 | ||||
|         let net_data = serde_yaml::to_string(&netconfig)?; | ||||
|         let mut net_fd = rootdir.create_file("network-config")?; | ||||
|         net_fd.write_all(net_data.as_bytes())?; | ||||
| 
 | ||||
|         // user-data MUST exist, even if there is no user-data
 | ||||
|         let mut user_fd = rootdir.create_file("user-data")?; | ||||
|         if let Some(user_data) = user_data { | ||||
|             user_fd.write_all(user_data.as_ref())?; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     Ok(image) | ||||
| } | ||||
							
								
								
									
										23
									
								
								nzrd/src/cmd/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								nzrd/src/cmd/mod.rs
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,23 @@ | |||
| pub mod net; | ||||
| pub mod vm; | ||||
| 
 | ||||
| use std::fmt; | ||||
| 
 | ||||
| #[derive(Debug)] | ||||
| pub struct CommandError(String); | ||||
| 
 | ||||
| impl fmt::Display for CommandError { | ||||
|     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||||
|         write!(f, "{}", self.0) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl std::error::Error for CommandError {} | ||||
| 
 | ||||
| macro_rules! cmd_error { | ||||
|     ($($arg:tt)*) => { | ||||
|         Box::new(CommandError(format!($($arg)*))) | ||||
|     }; | ||||
| } | ||||
| 
 | ||||
| pub(crate) use cmd_error; | ||||
							
								
								
									
										49
									
								
								nzrd/src/cmd/net.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								nzrd/src/cmd/net.rs
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,49 @@ | |||
| use super::*; | ||||
| use crate::ctrl::net::Subnet; | ||||
| use crate::ctrl::Storable; | ||||
| use crate::ctx::Context; | ||||
| 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))?; | ||||
| 
 | ||||
|     let mut ent = Subnet::insert( | ||||
|         ctx.db.clone(), | ||||
|         subnet.clone(), | ||||
|         args.ifname.to_string().as_bytes(), | ||||
|     )?; | ||||
| 
 | ||||
|     ent.transient = true; | ||||
| 
 | ||||
|     if let Err(err) = ctx.zones.new_zone(&subnet).await { | ||||
|         Err(cmd_error!("Failed to create new DNS zone: {}", err)) | ||||
|     } else { | ||||
|         ent.transient = false; | ||||
|         Ok(subnet) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| pub fn delete_subnet(ctx: &Context, interface: &str) -> Result<(), Box<dyn std::error::Error>> { | ||||
|     match Subnet::get_by_key(ctx.db.clone(), interface.as_bytes()) | ||||
|         .map_err(|er| cmd_error!("Couldn't find subnet: {}", er))? | ||||
|     { | ||||
|         Some(subnet) => subnet | ||||
|             .delete() | ||||
|             .map_err(|er| cmd_error!("Couldn't fully delete subnet entry: {}", er)), | ||||
|         None => Err(cmd_error!("No subnet object found for {}", interface)), | ||||
|     }?; | ||||
| 
 | ||||
|     Ok(()) | ||||
| } | ||||
							
								
								
									
										220
									
								
								nzrd/src/cmd/vm.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										220
									
								
								nzrd/src/cmd/vm.rs
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,220 @@ | |||
| use virt::stream::Stream; | ||||
| 
 | ||||
| use super::*; | ||||
| 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::Storable; | ||||
| use crate::ctx::Context; | ||||
| use crate::prelude::*; | ||||
| use crate::virt::VirtVolume; | ||||
| use log::*; | ||||
| use nzr_api::args; | ||||
| use nzr_api::net::mac::MacAddr; | ||||
| use trust_dns_server::proto::rr::Name; | ||||
| 
 | ||||
| const VIRT_MAC_OUI: &[u8] = &[0x02, 0xf1, 0x0f]; | ||||
| 
 | ||||
| /// Creates a new instance
 | ||||
| pub async fn new_instance( | ||||
|     ctx: Context, | ||||
|     args: &args::NewInstance, | ||||
| ) -> Result<Instance, Box<dyn std::error::Error>> { | ||||
|     // find the subnet corresponding to the interface
 | ||||
|     let subnet = Subnet::get_by_key(ctx.db.clone(), args.interface.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 | ||||
|             )), | ||||
|             Ok, | ||||
|         )?; | ||||
| 
 | ||||
|     // bail if a domain already exists
 | ||||
|     if let Ok(dom) = virt::domain::Domain::lookup_by_name(&ctx.virt.conn, &args.name) { | ||||
|         Err(cmd_error!( | ||||
|             "Domain with name already exists (uuid {})", | ||||
|             dom.get_uuid_string().unwrap_or("unknown".to_owned()) | ||||
|         )) | ||||
|     } else { | ||||
|         // 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))?; | ||||
| 
 | ||||
|         // generate a new lease with a new MAC addr
 | ||||
|         let mac_addr = { | ||||
|             let bytes = [VIRT_MAC_OUI, rand::random::<[u8; 3]>().as_ref()].concat(); | ||||
|             MacAddr::from_bytes(bytes) | ||||
|         } | ||||
|         .map_err(|er| cmd_error!("Unable to create a new MAC address: {}", er))?; | ||||
|         let lease = subnet | ||||
|             .new_lease(&mac_addr, &args.name) | ||||
|             .map_err(|er| cmd_error!("Failed to generate a new lease: {}", er))?; | ||||
| 
 | ||||
|         // generate cloud-init data
 | ||||
|         let meta = Metadata::new(&args.name).ssh_pubkeys(&args.ssh_keys); | ||||
|         let netconfig = NetworkMeta::new().static_nic( | ||||
|             EtherMatch::mac_addr(&mac_addr), | ||||
|             &lease.ipv4_addr, | ||||
|             &subnet.gateway4, | ||||
|             DNSMeta::with_addrs( | ||||
|                 { | ||||
|                     let mut search: Vec<Name> = vec![ctx.config.dns.default_zone.clone()]; | ||||
|                     if let Some(zone) = &subnet.domain_name { | ||||
|                         search.push(zone.clone()); | ||||
|                     } | ||||
|                     Some(search) | ||||
|                 }, | ||||
|                 &subnet.dns, | ||||
|             ), | ||||
|         ); | ||||
|         let ci_data = crate::cloud::create_image(&meta, &netconfig, None as Option<&Vec<u8>>) | ||||
|             .map_err(|er| cmd_error!("Unable to create initial cloud-init image: {}", er))? | ||||
|             .into_inner(); | ||||
| 
 | ||||
|         // and upload it to a vol
 | ||||
|         let vol_data = Volume::new(&args.name, VolType::Raw, datasize!(1440 KiB)); | ||||
|         let mut cidata_vol = VirtVolume::create_xml(&ctx.virt.pools.cidata, vol_data, 0)?; | ||||
| 
 | ||||
|         let cistream = Stream::new(&cidata_vol.get_connect()?, 0)?; | ||||
|         if let Err(er) = cidata_vol.upload(&cistream, 0, datasize!(1440 KiB).into(), 0) { | ||||
|             cistream.abort().ok(); | ||||
|             cidata_vol.delete(0)?; | ||||
|             Err(cmd_error!("Failed to create cloud-init volume: {}", er)) | ||||
|         } else { | ||||
|             let mut idx: usize = 0; | ||||
|             while idx < ci_data.len() { | ||||
|                 match cistream.send(&ci_data[idx..ci_data.len()]) { | ||||
|                     Ok(sz) => idx += sz, | ||||
|                     Err(er) => { | ||||
|                         cistream.abort().ok(); | ||||
|                         cidata_vol.delete(0)?; | ||||
|                         return Err(cmd_error!("Failed uploading to cloud-init image: {}", er)); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             // mark the stream as finished
 | ||||
|             cistream.finish()?; | ||||
| 
 | ||||
|             // create primary volume from base image
 | ||||
|             let mut pri_vol = base_image | ||||
|                 .clone_vol( | ||||
|                     &ctx.virt.pools.primary, | ||||
|                     &args.name, | ||||
|                     datasize!((args.disk_sizes.0) GiB), | ||||
|                 ) | ||||
|                 .map_err(|er| cmd_error!("Failed to clone base image: {}", er))?; | ||||
| 
 | ||||
|             // and, if it exists: the second volume
 | ||||
|             let sec_vol = match args.disk_sizes.1 { | ||||
|                 Some(sec_size) => { | ||||
|                     let voldata = Volume::new( | ||||
|                         &args.name, | ||||
|                         ctx.virt.pools.secondary.xml.vol_type(), | ||||
|                         datasize!(sec_size GiB), | ||||
|                     ); | ||||
|                     Some(VirtVolume::create_xml( | ||||
|                         &ctx.virt.pools.secondary, | ||||
|                         voldata, | ||||
|                         0, | ||||
|                     )?) | ||||
|                 } | ||||
|                 None => None, | ||||
|             }; | ||||
| 
 | ||||
|             // build domain xml
 | ||||
|             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; | ||||
|                 let cidata_name = &ctx.virt.pools.cidata.xml.name; | ||||
| 
 | ||||
|                 let mut instdata = DomainBuilder::default() | ||||
|                     .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)) | ||||
|                     .disk_device(|dsk| { | ||||
|                         dsk.volume_source(pri_name, &pri_vol.name) | ||||
|                             .target("vda", "virtio") | ||||
|                             .boot_order(1) | ||||
|                     }) | ||||
|                     .disk_device(|fda| { | ||||
|                         fda.volume_source(cidata_name, &cidata_vol.name) | ||||
|                             .device_type(DiskDeviceType::Disk) | ||||
|                             .target("hda", "ide") | ||||
|                     }) | ||||
|                     .serial_device(SerialType::Pty); | ||||
| 
 | ||||
|                 // add desription, if provided
 | ||||
|                 instdata = match &args.description { | ||||
|                     Some(desc) => instdata.description(desc), | ||||
|                     None => instdata, | ||||
|                 }; | ||||
| 
 | ||||
|                 // add second volume, if provided
 | ||||
|                 match &sec_vol { | ||||
|                     Some(vol) => instdata.disk_device(|dsk| { | ||||
|                         dsk.volume_source(sec_name, &vol.name) | ||||
|                             .target("vdb", "virtio") | ||||
|                     }), | ||||
|                     None => instdata, | ||||
|                 } | ||||
|             }) | ||||
|             .await?; | ||||
| 
 | ||||
|             // not a fatal error, we can set autostart afterward
 | ||||
|             if let Err(er) = conn.set_autostart(true) { | ||||
|                 warn!("Couldn't set autostart for domain: {}", er); | ||||
|             } | ||||
| 
 | ||||
|             if let Err(er) = conn.create() { | ||||
|                 warn!("Domain defined, but couldn't be started! Error: {}", er); | ||||
|             } | ||||
| 
 | ||||
|             // set all volumes to persistent to avoid deletion
 | ||||
|             pri_vol.persist = true; | ||||
|             if let Some(mut sec_vol) = sec_vol { | ||||
|                 sec_vol.persist = true; | ||||
|             } | ||||
|             cidata_vol.persist = true; | ||||
|             inst.persist(); | ||||
| 
 | ||||
|             debug!("Domain {} created!", inst.xml().name.as_str()); | ||||
|             Ok(inst) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| pub async fn delete_instance(ctx: Context, name: String) -> Result<(), Box<dyn std::error::Error>> { | ||||
|     let mut inst = Instance::lookup_by_name(ctx.clone(), &name) | ||||
|         .await? | ||||
|         .ok_or(cmd_error!("No such domain!"))?; | ||||
| 
 | ||||
|     let conn = inst.virt()?; | ||||
|     if conn.is_active()? { | ||||
|         conn.destroy() | ||||
|             .map_err(|er| cmd_error!("Failed to destroy domain: {}", er))?; | ||||
|     } | ||||
| 
 | ||||
|     inst.undefine().await?; | ||||
| 
 | ||||
|     Ok(()) | ||||
| } | ||||
| 
 | ||||
| pub fn prune_instances(ctx: &Context) -> Result<(), Box<dyn std::error::Error>> { | ||||
|     for entity in InstDb::all(ctx.db.clone())? { | ||||
|         let entity = entity?; | ||||
|         if let Err(InstanceError::DomainNotFound(name)) = | ||||
|             Instance::from_entity(ctx.clone(), entity.clone()) | ||||
|         { | ||||
|             info!("Instance {} was invalid, deleting", name); | ||||
|             if let Err(err) = entity.delete() { | ||||
|                 warn!("Couldn't delete {}: {}", name, err); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|     Ok(()) | ||||
| } | ||||
							
								
								
									
										287
									
								
								nzrd/src/ctrl/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										287
									
								
								nzrd/src/ctrl/mod.rs
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,287 @@ | |||
| use std::{ | ||||
|     marker::PhantomData, | ||||
|     ops::{Deref, DerefMut}, | ||||
| }; | ||||
| 
 | ||||
| use serde::{Deserialize, Serialize}; | ||||
| 
 | ||||
| use log::*; | ||||
| use std::fmt; | ||||
| 
 | ||||
| pub mod net; | ||||
| pub mod virtxml; | ||||
| pub mod vm; | ||||
| 
 | ||||
| #[derive(Clone)] | ||||
| pub struct Entity<T> | ||||
| where | ||||
|     T: Storable + Serialize, | ||||
| { | ||||
|     inner: T, | ||||
|     key: Vec<u8>, | ||||
|     tree: sled::Tree, | ||||
|     db: sled::Db, | ||||
|     pub transient: bool, | ||||
| } | ||||
| 
 | ||||
| impl<T> Deref for Entity<T> | ||||
| where | ||||
|     T: Storable, | ||||
| { | ||||
|     type Target = T; | ||||
|     fn deref(&self) -> &Self::Target { | ||||
|         &self.inner | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl<T> DerefMut for Entity<T> | ||||
| where | ||||
|     T: Storable, | ||||
| { | ||||
|     fn deref_mut(&mut self) -> &mut Self::Target { | ||||
|         &mut self.inner | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl<T> Drop for Entity<T> | ||||
| where | ||||
|     T: Storable, | ||||
| { | ||||
|     fn drop(&mut self) { | ||||
|         if self.transient { | ||||
|             let key_str = String::from_utf8_lossy(&self.key); | ||||
|             debug!("Transient flag enabled for {}, dropping!", &key_str); | ||||
|             if let Err(err) = self.delete() { | ||||
|                 warn!("Couldn't delete {} from database: {}", &key_str, err); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl<T> Entity<T> | ||||
| where | ||||
|     T: Storable, | ||||
| { | ||||
|     pub fn transient<V>(inner: T, key: V, tree: sled::Tree, db: sled::Db) -> Self | ||||
|     where | ||||
|         V: AsRef<[u8]>, | ||||
|     { | ||||
|         Entity { | ||||
|             inner, | ||||
|             key: key.as_ref().to_owned(), | ||||
|             tree, | ||||
|             db, | ||||
|             transient: true, | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     pub fn key(&self) -> &[u8] { | ||||
|         &self.key | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl<T> Entity<T> | ||||
| where | ||||
|     T: Storable + Serialize, | ||||
| { | ||||
|     pub fn update(&self) -> Result<(), StorableError> { | ||||
|         let mut bytes: Vec<u8> = Vec::new(); | ||||
|         ciborium::ser::into_writer(&self.inner, &mut bytes) | ||||
|             .map_err(|e| StorableError::new(ErrType::SerializeFailed, e))?; | ||||
|         self.tree | ||||
|             .insert(&self.key, bytes.as_slice()) | ||||
|             .map_err(|e| StorableError::new(ErrType::DbError, e))?; | ||||
|         Ok(()) | ||||
|     } | ||||
| 
 | ||||
|     pub fn delete(&self) -> Result<(), StorableError> { | ||||
|         self.on_delete(&self.db)?; | ||||
|         self.tree | ||||
|             .remove(&self.key) | ||||
|             .map_err(|e| StorableError::new(ErrType::DbError, e))?; | ||||
|         Ok(()) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[derive(Debug)] | ||||
| pub enum ErrType { | ||||
|     DbError, | ||||
|     DeserializeFailed, | ||||
|     SerializeFailed, | ||||
| } | ||||
| 
 | ||||
| #[derive(Debug)] | ||||
| pub struct StorableError { | ||||
|     err_type: ErrType, | ||||
|     inner: Option<Box<dyn std::error::Error>>, | ||||
| } | ||||
| 
 | ||||
| impl StorableError { | ||||
|     fn new<E>(err_type: ErrType, inner: E) -> Self | ||||
|     where | ||||
|         E: std::error::Error + 'static, | ||||
|     { | ||||
|         Self { | ||||
|             err_type, | ||||
|             inner: Some(Box::new(inner)), | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl fmt::Display for ErrType { | ||||
|     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||||
|         match self { | ||||
|             Self::DbError => write!(f, "Database error"), | ||||
|             Self::DeserializeFailed => write!(f, "Deserialize failed"), | ||||
|             Self::SerializeFailed => write!(f, "Serialize failed"), | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl fmt::Display for StorableError { | ||||
|     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||||
|         self.err_type.fmt(f)?; | ||||
|         if let Some(inner) = &self.inner { | ||||
|             write!(f, ": {}", inner)?; | ||||
|         } | ||||
|         Ok(()) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl std::error::Error for StorableError {} | ||||
| 
 | ||||
| pub trait Storable | ||||
| where | ||||
|     for<'de> Self: Deserialize<'de> + Serialize, | ||||
| { | ||||
|     fn tree_name() -> Option<&'static [u8]>; | ||||
| 
 | ||||
|     fn get_by_key(db: sled::Db, key: &[u8]) -> Result<Option<Entity<Self>>, StorableError> { | ||||
|         let tree_name = match Self::tree_name() { | ||||
|             Some(tn) => tn, | ||||
|             None => unimplemented!(), | ||||
|         }; | ||||
|         let tree = db | ||||
|             .open_tree(tree_name) | ||||
|             .map_err(|e| StorableError::new(ErrType::DbError, e))?; | ||||
|         match tree | ||||
|             .get(key) | ||||
|             .map_err(|e| StorableError::new(ErrType::DbError, e))? | ||||
|         { | ||||
|             Some(vec) => { | ||||
|                 let deserialized: Self = ciborium::de::from_reader(&*vec) | ||||
|                     .map_err(|e| StorableError::new(ErrType::DeserializeFailed, e))?; | ||||
|                 Ok(Some(Entity { | ||||
|                     inner: deserialized, | ||||
|                     key: key.to_owned(), | ||||
|                     tree, | ||||
|                     db, | ||||
|                     transient: false, | ||||
|                 })) | ||||
|             } | ||||
|             None => Ok(None), | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     fn insert(db: sled::Db, item: Self, key: &[u8]) -> Result<Entity<Self>, StorableError> { | ||||
|         let tree_name = match Self::tree_name() { | ||||
|             Some(tn) => tn, | ||||
|             None => unimplemented!(), | ||||
|         }; | ||||
|         let tree = db | ||||
|             .open_tree(tree_name) | ||||
|             .map_err(|e| StorableError::new(ErrType::DbError, e))?; | ||||
|         let ent = Entity { | ||||
|             inner: item, | ||||
|             key: key.to_owned(), | ||||
|             tree, | ||||
|             db, | ||||
|             transient: false, | ||||
|         }; | ||||
|         ent.update()?; | ||||
|         Ok(ent) | ||||
|     } | ||||
| 
 | ||||
|     /// Requests all items from the database, as a [`StorIter`].
 | ||||
|     fn all(db: sled::Db) -> Result<StorIter<Self>, StorableError> { | ||||
|         let tree_name = match Self::tree_name() { | ||||
|             Some(tn) => tn, | ||||
|             None => unimplemented!(), | ||||
|         }; | ||||
|         let tree = db | ||||
|             .open_tree(tree_name) | ||||
|             .map_err(|e| StorableError::new(ErrType::DbError, e))?; | ||||
|         Ok(StorIter::new(db, tree)) | ||||
|     } | ||||
| 
 | ||||
|     /// Function to allow storable objects to perform actions on deletion.
 | ||||
|     fn on_delete(&self, _db: &sled::Db) -> Result<(), StorableError> { | ||||
|         // No-op
 | ||||
|         debug!("deleting; Storable no-op!"); | ||||
|         Ok(()) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| /// Iterator of [`Storable`]s in the running database.
 | ||||
| pub struct StorIter<T> | ||||
| where | ||||
|     T: Storable, | ||||
| { | ||||
|     db: sled::Db, | ||||
|     tree: sled::Tree, | ||||
|     iter: sled::Iter, | ||||
|     phantom: PhantomData<T>, | ||||
| } | ||||
| 
 | ||||
| impl<T> StorIter<T> | ||||
| where | ||||
|     T: Storable, | ||||
| { | ||||
|     /// Creates a new iterator of [`Storable`]s using a [`sled::Db`] and
 | ||||
|     /// [`sled::Tree`].
 | ||||
|     fn new(db: sled::Db, tree: sled::Tree) -> Self { | ||||
|         Self { | ||||
|             db, | ||||
|             tree: tree.clone(), | ||||
|             iter: tree.iter(), | ||||
|             phantom: PhantomData, | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl<T> Iterator for StorIter<T> | ||||
| where | ||||
|     T: Storable, | ||||
| { | ||||
|     type Item = Result<Entity<T>, StorableError>; | ||||
| 
 | ||||
|     fn next(&mut self) -> Option<Self::Item> { | ||||
|         if let Some(next) = self.iter.next() { | ||||
|             match next { | ||||
|                 Ok((key, val)) => { | ||||
|                     let inner = { | ||||
|                         let vec = val.to_vec(); | ||||
|                         let inner = ciborium::de::from_reader(vec.as_slice()) | ||||
|                             .map_err(|e| StorableError::new(ErrType::DeserializeFailed, e)); | ||||
|                         match inner { | ||||
|                             Ok(inner) => inner, | ||||
|                             Err(err) => { | ||||
|                                 return Some(Err(err)); | ||||
|                             } | ||||
|                         } | ||||
|                     }; | ||||
|                     Some(Ok(Entity { | ||||
|                         inner, | ||||
|                         key: key.to_vec(), | ||||
|                         tree: self.tree.clone(), | ||||
|                         db: self.db.clone(), | ||||
|                         transient: false, | ||||
|                     })) | ||||
|                 } | ||||
|                 Err(err) => Some(Err(StorableError::new(ErrType::DbError, err))), | ||||
|             } | ||||
|         } else { | ||||
|             None | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										198
									
								
								nzrd/src/ctrl/net.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										198
									
								
								nzrd/src/ctrl/net.rs
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,198 @@ | |||
| use super::{Entity, StorIter}; | ||||
| use nzr_api::model::IfaceStr; | ||||
| use nzr_api::net::cidr::CidrV4; | ||||
| use nzr_api::net::mac::MacAddr; | ||||
| use serde::{Deserialize, Serialize}; | ||||
| use std::fmt; | ||||
| use std::net::Ipv4Addr; | ||||
| use std::str::FromStr; | ||||
| 
 | ||||
| use trust_dns_server::proto::rr::Name; | ||||
| 
 | ||||
| use super::Storable; | ||||
| 
 | ||||
| #[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, | ||||
| } | ||||
| 
 | ||||
| impl From<&Subnet> for nzr_api::model::Subnet { | ||||
|     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(); | ||||
|         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(), | ||||
|             }, | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[derive(Clone, Serialize, Deserialize)] | ||||
| pub struct Lease { | ||||
|     pub ipv4_addr: CidrV4, | ||||
|     pub mac_addr: MacAddr, | ||||
|     pub inst_name: String, | ||||
| } | ||||
| 
 | ||||
| #[derive(Debug)] | ||||
| pub enum SubnetError { | ||||
|     DbError(sled::Error), | ||||
|     SubnetExists, | ||||
|     BadNetwork(nzr_api::net::cidr::Error), | ||||
|     BadData, | ||||
|     BadStartHost, | ||||
|     BadEndHost, | ||||
|     BadRange, | ||||
|     HostOutsideRange, | ||||
|     BadHost(nzr_api::net::cidr::Error), | ||||
|     CantDelete(sled::Error), | ||||
|     SubnetFull, | ||||
|     BadDomainName, | ||||
| } | ||||
| 
 | ||||
| impl fmt::Display for SubnetError { | ||||
|     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||||
|         match self { | ||||
|             Self::DbError(er) => write!(f, "Database error: {}", er), | ||||
|             Self::SubnetExists => write!(f, "Subnet already exists"), | ||||
|             Self::BadNetwork(er) => write!(f, "Error deserializing network from database: {}", er), | ||||
|             Self::BadData => write!(f, "Malformed data in database"), | ||||
|             Self::BadStartHost => write!(f, "Starting host is not in provided subnet"), | ||||
|             Self::BadEndHost => write!(f, "Ending host is not in provided subnet"), | ||||
|             Self::BadRange => write!(f, "Ending host is before starting host"), | ||||
|             Self::HostOutsideRange => write!(f, "Available host is outside defined host range"), | ||||
|             Self::BadHost(er) => write!( | ||||
|                 f, | ||||
|                 "Host is within range but couldn't be converted to IP: {}", | ||||
|                 er | ||||
|             ), | ||||
|             Self::CantDelete(de) => write!(f, "Error when trying to delete: {}", de), | ||||
|             Self::SubnetFull => write!(f, "No addresses are left to assign in subnet"), | ||||
|             Self::BadDomainName => { | ||||
|                 write!(f, "Invalid domain name. Must be in the format xx.yy.tld") | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl std::error::Error for SubnetError {} | ||||
| 
 | ||||
| impl Storable for Subnet { | ||||
|     fn tree_name() -> Option<&'static [u8]> { | ||||
|         Some(b"nets") | ||||
|     } | ||||
| 
 | ||||
|     fn on_delete(&self, db: &sled::Db) -> Result<(), super::StorableError> { | ||||
|         db.drop_tree(self.lease_tree()) | ||||
|             .map_err(|e| super::StorableError::new(super::ErrType::DbError, e))?; | ||||
|         Ok(()) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| 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> { | ||||
|         // validate start and end addresses
 | ||||
|         if end_addr < start_addr { | ||||
|             Err(SubnetError::BadRange) | ||||
|         } else if !network.contains(start_addr) { | ||||
|             Err(SubnetError::BadStartHost) | ||||
|         } else if !network.contains(end_addr) { | ||||
|             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, | ||||
|             }; | ||||
|             Ok(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 | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl Storable for Lease { | ||||
|     fn tree_name() -> Option<&'static [u8]> { | ||||
|         None | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl Entity<Subnet> { | ||||
|     /// Create a new lease associated with the subnet.
 | ||||
|     pub fn new_lease( | ||||
|         &self, | ||||
|         mac_addr: &MacAddr, | ||||
|         inst_name: &str, | ||||
|     ) -> Result<Entity<Lease>, Box<dyn std::error::Error>> { | ||||
|         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, | ||||
|         }; | ||||
|         let new_ip = self | ||||
|             .network | ||||
|             .make_ip(max_lease + 1) | ||||
|             .map_err(|_| SubnetError::SubnetFull)?; | ||||
|         let lease_data = Lease { | ||||
|             ipv4_addr: CidrV4::new(new_ip, self.network.cidr()), | ||||
|             mac_addr: mac_addr.clone(), | ||||
|             inst_name: inst_name.to_owned(), | ||||
|         }; | ||||
|         let lease_tree = self | ||||
|             .db | ||||
|             .open_tree(self.lease_tree()) | ||||
|             .map_err(SubnetError::DbError)?; | ||||
|         let octets = lease_data.ipv4_addr.addr.octets(); | ||||
|         let ent = Entity::transient(lease_data, octets, lease_tree, self.db.clone()); | ||||
|         ent.update()?; | ||||
|         Ok(ent) | ||||
|     } | ||||
| 
 | ||||
|     /// Get an iterator over all leases in the subnet.
 | ||||
|     pub fn leases(&self) -> Result<StorIter<Lease>, sled::Error> { | ||||
|         let lease_tree = self.db.open_tree(self.lease_tree())?; | ||||
|         Ok(StorIter::new(self.db.clone(), lease_tree)) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										239
									
								
								nzrd/src/ctrl/virtxml/build.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										239
									
								
								nzrd/src/ctrl/virtxml/build.rs
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,239 @@ | |||
| use log::*; | ||||
| 
 | ||||
| use super::*; | ||||
| 
 | ||||
| #[derive(Default)] | ||||
| pub struct DomainBuilder { | ||||
|     domain: Domain, | ||||
| } | ||||
| 
 | ||||
| impl DomainBuilder { | ||||
|     /// Sets the name attribute of the domain.
 | ||||
|     ///
 | ||||
|     /// From libvirt documentation:
 | ||||
|     ///
 | ||||
|     /// > The content of the name element provides a short
 | ||||
|     /// > name for the virtual machine. This name should consist
 | ||||
|     /// > only of alphanumeric characters and is required to be
 | ||||
|     /// > unique within the scope of a single host. It is often
 | ||||
|     /// > used to form the filename for storing the persistent
 | ||||
|     /// > configuration file.
 | ||||
|     pub fn name(mut self, name: &str) -> Self { | ||||
|         self.domain.name = name.to_owned(); | ||||
|         self | ||||
|     } | ||||
| 
 | ||||
|     #[cfg(test)] | ||||
|     pub fn uuid(mut self, uuid: uuid::Uuid) -> Self { | ||||
|         self.domain.uuid = uuid; | ||||
|         self | ||||
|     } | ||||
| 
 | ||||
|     /// Sets the title attribute of the domain.
 | ||||
|     ///
 | ||||
|     /// From libvirt documentation:
 | ||||
|     ///
 | ||||
|     /// > The optional element title provides space for a short
 | ||||
|     /// > description of the domain. The title should not
 | ||||
|     /// > contain any newlines.
 | ||||
|     pub fn title(mut self, title: &str) -> Self { | ||||
|         self.domain.title = Some(title.to_owned()); | ||||
|         self | ||||
|     } | ||||
| 
 | ||||
|     /// Sets the description attribute of the domain.
 | ||||
|     ///
 | ||||
|     /// From libvirt documentation:
 | ||||
|     ///
 | ||||
|     /// > The content of the description element provides a
 | ||||
|     /// > human readable description of the virtual machine.
 | ||||
|     /// > This data is not used by libvirt in any way, it can
 | ||||
|     /// > contain any information the user wants.
 | ||||
|     pub fn description(mut self, desc: &str) -> Self { | ||||
|         self.domain.description = Some(desc.to_owned()); | ||||
|         self | ||||
|     } | ||||
| 
 | ||||
|     /// Sets what action is performed on the domain when
 | ||||
|     /// powered off.
 | ||||
|     pub fn poweroff_action(mut self, action: PowerAction) -> Self { | ||||
|         self.domain.on_poweroff = Some(action); | ||||
|         self | ||||
|     } | ||||
| 
 | ||||
|     /// Sets what action is performed on the domain when
 | ||||
|     /// rebooted.
 | ||||
|     pub fn reboot_action(mut self, action: PowerAction) -> Self { | ||||
|         self.domain.on_reboot = Some(action); | ||||
|         self | ||||
|     } | ||||
| 
 | ||||
|     /// Sets what action is performed on the domain when
 | ||||
|     /// it crashes.
 | ||||
|     pub fn crash_action(mut self, action: PowerAction) -> Self { | ||||
|         self.domain.on_crash = Some(action); | ||||
|         self | ||||
|     } | ||||
| 
 | ||||
|     /// Adds a network interface to the domain.
 | ||||
|     pub fn net_device<P>(mut self, net_func: P) -> Self | ||||
|     where | ||||
|         P: Fn(IfaceBuilder) -> IfaceBuilder, | ||||
|     { | ||||
|         let netdev = net_func(IfaceBuilder::new()); | ||||
|         self.domain.devices.push(Device::Interface { | ||||
|             interface: netdev.build(), | ||||
|         }); | ||||
|         self | ||||
|     } | ||||
| 
 | ||||
|     /// Adds a disk device to the domain.
 | ||||
|     pub fn disk_device<P>(mut self, disk_func: P) -> Self | ||||
|     where | ||||
|         P: Fn(DiskBuilder) -> DiskBuilder, | ||||
|     { | ||||
|         let diskdev = disk_func(DiskBuilder::new()); | ||||
|         self.domain.devices.push(Device::Disk { | ||||
|             disk: diskdev.build(), | ||||
|         }); | ||||
|         self | ||||
|     } | ||||
| 
 | ||||
|     pub fn memory(mut self, size: SizeInfo) -> Self { | ||||
|         self.domain.memory = size; | ||||
|         self | ||||
|     } | ||||
| 
 | ||||
|     pub fn serial_device(mut self, serial_type: SerialType) -> Self { | ||||
|         self.domain.devices.push(Device::Console { | ||||
|             console: SerialDevice { | ||||
|                 r#type: serial_type, | ||||
|             }, | ||||
|         }); | ||||
|         self | ||||
|     } | ||||
| 
 | ||||
|     pub fn cpu_topology(mut self, sockets: u8, dies: u8, cores: u8, threads: u8) -> Self { | ||||
|         self.domain.cpu.topology = CpuTopology { | ||||
|             sockets, | ||||
|             dies, | ||||
|             cores, | ||||
|             threads, | ||||
|         }; | ||||
|         self.domain.vcpu.value = (sockets * dies * cores * threads).into(); | ||||
|         self | ||||
|     } | ||||
| 
 | ||||
|     pub fn build(mut self) -> Domain { | ||||
|         if self.domain.devices.disk.iter().any(|d| d.boot.is_some()) { | ||||
|             debug!("Disk has boot order, removing <os/> style boot..."); | ||||
|             self.domain.os.boot = None; | ||||
|         } | ||||
|         self.domain | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| pub struct IfaceBuilder { | ||||
|     iface: NetDevice, | ||||
| } | ||||
| 
 | ||||
| impl IfaceBuilder { | ||||
|     fn new() -> Self { | ||||
|         Self { | ||||
|             iface: NetDevice::default(), | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /// Uses a bridge as the source device.
 | ||||
|     pub fn with_bridge(mut self, bridge: &str) -> Self { | ||||
|         self.iface.r#type = IfaceType::Bridge; | ||||
|         self.iface.source.bridge = Some(bridge.to_owned()); | ||||
|         self | ||||
|     } | ||||
| 
 | ||||
|     /// Uses a libvirt-defined network as the source device.
 | ||||
|     pub fn with_network(mut self, network: &str) -> Self { | ||||
|         self.iface.r#type = IfaceType::Network; | ||||
|         self.iface.source.network = Some(network.to_owned()); | ||||
|         self | ||||
|     } | ||||
| 
 | ||||
|     /// Defines the MAC address the interface should use.
 | ||||
|     pub fn mac_addr(mut self, addr: &MacAddr) -> Self { | ||||
|         self.iface.mac = Some(NetMac { | ||||
|             address: addr.clone(), | ||||
|         }); | ||||
|         self | ||||
|     } | ||||
| 
 | ||||
|     /// Defines the model type to emulate.
 | ||||
|     ///
 | ||||
|     /// By default, interfaces will be created as virtio. To
 | ||||
|     /// see what models you can use on your hypervisor, run:
 | ||||
|     ///
 | ||||
|     /// ```
 | ||||
|     /// qemu-system-x86_64 -net nic,model=? /dev/null
 | ||||
|     /// ```
 | ||||
|     pub fn model(mut self, model: &str) -> Self { | ||||
|         self.iface.model.r#type = model.to_owned(); | ||||
|         self | ||||
|     } | ||||
| 
 | ||||
|     fn build(self) -> NetDevice { | ||||
|         self.iface | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[derive(Default)] | ||||
| pub struct DiskBuilder { | ||||
|     disk: DiskDevice, | ||||
| } | ||||
| 
 | ||||
| impl DiskBuilder { | ||||
|     pub fn new() -> Self { | ||||
|         Self::default() | ||||
|     } | ||||
| 
 | ||||
|     /// Set the source for a file
 | ||||
|     pub fn file_source(mut self, filename: &str) -> Self { | ||||
|         self.disk.r#type = DiskType::File; | ||||
|         self.disk.source.file = Some(filename.to_owned()); | ||||
|         self | ||||
|     } | ||||
| 
 | ||||
|     /// Set the source for a block device
 | ||||
|     pub fn block_source(mut self, dev: &str) -> Self { | ||||
|         self.disk.r#type = DiskType::Block; | ||||
|         self.disk.source.dev = Some(dev.to_owned()); | ||||
|         self | ||||
|     } | ||||
| 
 | ||||
|     /// Set the source for a volume drive (e.g., zfs)
 | ||||
|     pub fn volume_source(mut self, pool: &str, volume: &str) -> Self { | ||||
|         self.disk.r#type = DiskType::Volume; | ||||
|         self.disk.source.pool = Some(pool.to_owned()); | ||||
|         self.disk.source.volume = Some(volume.to_owned()); | ||||
|         self | ||||
|     } | ||||
| 
 | ||||
|     /// Set the target for the disk to attach to.
 | ||||
|     pub fn target(mut self, dev: &str, bus: &str) -> Self { | ||||
|         self.disk.target.bus = bus.to_owned(); | ||||
|         self.disk.target.dev = dev.to_owned(); | ||||
|         self | ||||
|     } | ||||
| 
 | ||||
|     pub fn boot_order(mut self, order: u32) -> Self { | ||||
|         self.disk.boot = Some(DiskBoot { order }); | ||||
|         self | ||||
|     } | ||||
| 
 | ||||
|     pub fn device_type(mut self, devtype: DiskDeviceType) -> Self { | ||||
|         self.disk.device = devtype; | ||||
|         self | ||||
|     } | ||||
| 
 | ||||
|     pub fn build(self) -> DiskDevice { | ||||
|         self.disk | ||||
|     } | ||||
| } | ||||
							
								
								
									
										549
									
								
								nzrd/src/ctrl/virtxml/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										549
									
								
								nzrd/src/ctrl/virtxml/mod.rs
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,549 @@ | |||
| pub mod build; | ||||
| #[cfg(test)] | ||||
| mod test; | ||||
| 
 | ||||
| use nzr_api::net::mac::MacAddr; | ||||
| use serde::{Deserialize, Serialize}; | ||||
| use serde_with::skip_serializing_none; | ||||
| use uuid::Uuid; | ||||
| 
 | ||||
| #[skip_serializing_none] | ||||
| #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] | ||||
| #[serde(rename = "domain")] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| pub struct Domain { | ||||
|     #[serde(rename = "@type")] | ||||
|     pub r#type: String, | ||||
|     pub name: String, | ||||
|     pub uuid: uuid::Uuid, | ||||
|     pub title: Option<String>, | ||||
|     pub description: Option<String>, | ||||
|     pub vcpu: Vcpu, | ||||
|     pub memory: SizeInfo, | ||||
|     pub current_memory: Option<SizeInfo>, | ||||
|     pub features: Option<FeatureList>, | ||||
|     pub cpu: Cpu, | ||||
|     pub devices: DeviceList, | ||||
|     pub os: OsData, | ||||
|     pub on_poweroff: Option<PowerAction>, | ||||
|     pub on_reboot: Option<PowerAction>, | ||||
|     pub on_crash: Option<PowerAction>, | ||||
| } | ||||
| 
 | ||||
| impl Default for Domain { | ||||
|     fn default() -> Self { | ||||
|         Domain { | ||||
|             r#type: "kvm".to_owned(), | ||||
|             name: String::default(), | ||||
|             memory: SizeInfo { | ||||
|                 unit: SizeUnit::MiB, | ||||
|                 amount: 512, | ||||
|             }, | ||||
|             current_memory: None, | ||||
|             features: Some(FeatureList { | ||||
|                 features: vec![Feature::APIC, Feature::ACPI], | ||||
|             }), | ||||
|             cpu: Cpu { | ||||
|                 mode: "host-passthrough".to_owned(), | ||||
|                 topology: CpuTopology { | ||||
|                     sockets: 1, | ||||
|                     dies: 1, | ||||
|                     cores: 2, | ||||
|                     threads: 1, | ||||
|                 }, | ||||
|             }, | ||||
|             uuid: Uuid::new_v4(), | ||||
|             title: None, | ||||
|             description: None, | ||||
|             vcpu: Vcpu { | ||||
|                 placement: "static".to_owned(), | ||||
|                 value: 2, | ||||
|             }, | ||||
|             os: OsData { | ||||
|                 boot: Some(BootNode { | ||||
|                     dev: BootDevice::HardDrive, | ||||
|                 }), | ||||
|                 r#type: OsType::default(), | ||||
|                 bios: BiosData { | ||||
|                     useserial: "yes".to_owned(), | ||||
|                     reboot_timeout: 0, | ||||
|                 }, | ||||
|             }, | ||||
|             on_poweroff: None, | ||||
|             on_reboot: None, | ||||
|             on_crash: None, | ||||
|             devices: DeviceList::default(), | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] | ||||
| pub struct FeatureList { | ||||
|     #[serde(rename = "$value")] | ||||
|     features: Vec<Feature>, | ||||
| } | ||||
| 
 | ||||
| impl std::ops::Deref for FeatureList { | ||||
|     type Target = Vec<Feature>; | ||||
|     fn deref(&self) -> &Self::Target { | ||||
|         &self.features | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl std::ops::DerefMut for FeatureList { | ||||
|     fn deref_mut(&mut self) -> &mut Self::Target { | ||||
|         &mut self.features | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] | ||||
| #[serde(rename_all = "lowercase")] | ||||
| #[allow(clippy::upper_case_acronyms)] | ||||
| pub enum Feature { | ||||
|     ACPI, | ||||
|     APIC, | ||||
| } | ||||
| 
 | ||||
| #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] | ||||
| pub enum PowerAction { | ||||
|     Destroy, | ||||
|     Restart, | ||||
|     Preserve, | ||||
| } | ||||
| 
 | ||||
| #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] | ||||
| pub struct Vcpu { | ||||
|     #[serde(rename = "@placement")] | ||||
|     placement: String, | ||||
|     #[serde(rename = "$value")] | ||||
|     value: u16, | ||||
| } | ||||
| 
 | ||||
| //  =^..^=  =^..^=  =^..^=  =^..^=  =^..^=  =^..^=  =^..^=  =^..^=
 | ||||
| 
 | ||||
| #[skip_serializing_none] | ||||
| #[derive(Default, Debug, Serialize, Deserialize, PartialEq, Eq)] | ||||
| pub struct DiskSource { | ||||
|     #[serde(rename = "@file")] | ||||
|     file: Option<String>, | ||||
|     #[serde(rename = "@dev")] | ||||
|     dev: Option<String>, | ||||
|     #[serde(rename = "@dir")] | ||||
|     dir: Option<String>, | ||||
|     // XXX: not supporting network type
 | ||||
|     #[serde(rename = "@pool")] | ||||
|     pub pool: Option<String>, | ||||
|     #[serde(rename = "@volume")] | ||||
|     pub volume: Option<String>, | ||||
| } | ||||
| 
 | ||||
| #[skip_serializing_none] | ||||
| #[derive(Default, Debug, Serialize, Deserialize, PartialEq, Eq)] | ||||
| pub struct DiskDriver { | ||||
|     #[serde(rename = "@name")] | ||||
|     name: String, | ||||
|     #[serde(rename = "@type")] | ||||
|     r#type: Option<String>, | ||||
| } | ||||
| 
 | ||||
| #[derive(Default, Debug, Serialize, Deserialize, PartialEq, Eq)] | ||||
| pub struct DiskTarget { | ||||
|     #[serde(rename = "@dev")] | ||||
|     dev: String, | ||||
|     #[serde(rename = "@bus")] | ||||
|     bus: String, | ||||
| } | ||||
| 
 | ||||
| #[derive(Clone, Copy, Default, Debug, Serialize, Deserialize, PartialEq, Eq)] | ||||
| #[serde(rename_all = "lowercase")] | ||||
| pub enum DiskType { | ||||
|     File, | ||||
|     Block, | ||||
|     Volume, | ||||
|     #[default] | ||||
|     None, | ||||
| } | ||||
| 
 | ||||
| #[derive(Default, Debug, Serialize, Deserialize, PartialEq, Eq)] | ||||
| #[serde(rename_all = "lowercase")] | ||||
| pub enum DiskDeviceType { | ||||
|     Floppy, | ||||
|     #[default] | ||||
|     Disk, | ||||
|     Cdrom, | ||||
| } | ||||
| 
 | ||||
| #[skip_serializing_none] | ||||
| #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] | ||||
| pub struct DiskBoot { | ||||
|     #[serde(rename = "@order")] | ||||
|     order: u32, | ||||
| } | ||||
| 
 | ||||
| #[skip_serializing_none] | ||||
| #[derive(Default, Debug, Serialize, Deserialize, PartialEq, Eq)] | ||||
| pub struct DiskDevice { | ||||
|     #[serde(rename = "@type")] | ||||
|     pub r#type: DiskType, | ||||
|     boot: Option<DiskBoot>, | ||||
|     #[serde(rename = "@device")] | ||||
|     device: DiskDeviceType, | ||||
|     pub source: DiskSource, | ||||
|     target: DiskTarget, | ||||
|     driver: Option<DiskDriver>, | ||||
| } | ||||
| 
 | ||||
| //  =^..^=  =^..^=  =^..^=  =^..^=  =^..^=  =^..^=  =^..^=  =^..^=
 | ||||
| 
 | ||||
| #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] | ||||
| pub struct NetMac { | ||||
|     #[serde(rename = "@address")] | ||||
|     address: MacAddr, | ||||
| } | ||||
| 
 | ||||
| #[skip_serializing_none] | ||||
| #[derive(Default, Debug, Serialize, Deserialize, PartialEq, Eq)] | ||||
| pub struct NetSource { | ||||
|     #[serde(rename = "@bridge")] | ||||
|     bridge: Option<String>, | ||||
|     #[serde(rename = "@network")] | ||||
|     network: Option<String>, | ||||
| } | ||||
| 
 | ||||
| #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] | ||||
| pub struct NetModel { | ||||
|     #[serde(rename = "@type")] | ||||
|     r#type: String, | ||||
| } | ||||
| 
 | ||||
| impl Default for NetModel { | ||||
|     fn default() -> Self { | ||||
|         Self { | ||||
|             r#type: "virtio".to_owned(), | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[derive(Default, Debug, Serialize, Deserialize, PartialEq, Eq)] | ||||
| #[serde(rename_all = "lowercase")] | ||||
| pub enum IfaceType { | ||||
|     Network, | ||||
|     Bridge, | ||||
|     #[default] | ||||
|     None, | ||||
| } | ||||
| 
 | ||||
| #[derive(Default, Debug, Serialize, Deserialize, PartialEq, Eq)] | ||||
| pub struct NetDevice { | ||||
|     #[serde(rename = "@type")] | ||||
|     r#type: IfaceType, | ||||
|     mac: Option<NetMac>, | ||||
|     source: NetSource, | ||||
|     model: NetModel, | ||||
| } | ||||
| 
 | ||||
| //  =^..^=  =^..^=  =^..^=  =^..^=  =^..^=  =^..^=  =^..^=  =^..^=
 | ||||
| 
 | ||||
| #[derive(Default, Debug, Serialize, Deserialize, PartialEq, Eq)] | ||||
| #[serde(rename_all = "lowercase")] | ||||
| pub enum SerialType { | ||||
|     #[default] | ||||
|     Pty, | ||||
|     Unix, | ||||
|     SpiceVMC, | ||||
|     StdIO, | ||||
| } | ||||
| 
 | ||||
| #[derive(Default, Debug, Serialize, Deserialize, PartialEq, Eq)] | ||||
| pub struct SerialDevice { | ||||
|     #[serde(rename = "@type")] | ||||
|     r#type: SerialType, | ||||
| } | ||||
| 
 | ||||
| //  =^..^=  =^..^=  =^..^=  =^..^=  =^..^=  =^..^=  =^..^=  =^..^=
 | ||||
| 
 | ||||
| #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] | ||||
| #[serde(untagged)] | ||||
| pub enum Device { | ||||
|     #[serde(rename = "disk")] | ||||
|     Disk { disk: DiskDevice }, | ||||
|     #[serde(rename = "interface")] | ||||
|     Interface { interface: NetDevice }, | ||||
|     #[serde(rename = "console")] | ||||
|     Console { console: SerialDevice }, | ||||
| } | ||||
| 
 | ||||
| #[derive(Default, Debug, Serialize, Deserialize, PartialEq, Eq)] | ||||
| pub struct DeviceList { | ||||
|     #[serde(skip_serializing_if = "Vec::is_empty")] | ||||
|     #[serde(default)] | ||||
|     disk: Vec<DiskDevice>, | ||||
|     #[serde(skip_serializing_if = "Vec::is_empty")] | ||||
|     #[serde(default)] | ||||
|     interface: Vec<NetDevice>, | ||||
|     #[serde(skip_serializing_if = "Vec::is_empty")] | ||||
|     #[serde(default)] | ||||
|     console: Vec<SerialDevice>, | ||||
| } | ||||
| 
 | ||||
| impl DeviceList { | ||||
|     pub fn push(&mut self, dev: Device) { | ||||
|         match dev { | ||||
|             Device::Disk { disk } => self.disk.push(disk), | ||||
|             Device::Interface { interface } => self.interface.push(interface), | ||||
|             Device::Console { console } => self.console.push(console), | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     pub fn disks(&self) -> &Vec<DiskDevice> { | ||||
|         &self.disk | ||||
|     } | ||||
| 
 | ||||
|     pub fn interfaces(&mut self) -> &Vec<NetDevice> { | ||||
|         &self.interface | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| //  =^..^=  =^..^=  =^..^=  =^..^=  =^..^=  =^..^=  =^..^=  =^..^=
 | ||||
| 
 | ||||
| #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] | ||||
| #[serde(rename_all = "lowercase")] | ||||
| pub enum BootDevice { | ||||
|     Floppy, | ||||
|     Cdrom, | ||||
|     #[serde(rename = "hd")] | ||||
|     HardDrive, | ||||
|     Network, | ||||
| } | ||||
| 
 | ||||
| #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] | ||||
| pub struct BootNode { | ||||
|     #[serde(rename = "@dev")] | ||||
|     dev: BootDevice, | ||||
| } | ||||
| 
 | ||||
| #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] | ||||
| pub struct BiosData { | ||||
|     #[serde(rename = "@useserial")] | ||||
|     useserial: String, | ||||
|     #[serde(rename = "@rebootTimeout")] | ||||
|     reboot_timeout: u32, | ||||
| } | ||||
| 
 | ||||
| #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] | ||||
| pub struct OsType { | ||||
|     #[serde(rename = "@arch")] | ||||
|     arch: String, | ||||
|     #[serde(rename = "@machine")] | ||||
|     machine: String, | ||||
|     #[serde(rename = "$value")] | ||||
|     value: String, | ||||
| } | ||||
| 
 | ||||
| impl Default for OsType { | ||||
|     fn default() -> Self { | ||||
|         Self { | ||||
|             arch: "x86_64".to_owned(), | ||||
|             machine: "pc-i440fx-5.2".to_owned(), | ||||
|             value: "hvm".to_owned(), | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[skip_serializing_none] | ||||
| #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] | ||||
| pub struct OsData { | ||||
|     boot: Option<BootNode>, | ||||
|     r#type: OsType, | ||||
|     // we will not be doing PV, no <bootloader>/<kernel>/<initrd>/etc
 | ||||
|     bios: BiosData, | ||||
| } | ||||
| 
 | ||||
| impl Default for OsData { | ||||
|     fn default() -> Self { | ||||
|         Self { | ||||
|             boot: Some(BootNode { | ||||
|                 dev: BootDevice::HardDrive, | ||||
|             }), | ||||
|             r#type: OsType::default(), | ||||
|             bios: BiosData { | ||||
|                 useserial: "yes".to_owned(), | ||||
|                 reboot_timeout: 0, | ||||
|             }, | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] | ||||
| pub enum SizeUnit { | ||||
|     #[serde(rename = "bytes")] | ||||
|     Bytes, | ||||
|     KiB, | ||||
|     MiB, | ||||
|     GiB, | ||||
|     TiB, | ||||
|     PiB, | ||||
| } | ||||
| 
 | ||||
| impl std::fmt::Display for SizeUnit { | ||||
|     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||||
|         write!( | ||||
|             f, | ||||
|             "{}", | ||||
|             match self { | ||||
|                 Self::Bytes => "bytes", | ||||
|                 Self::KiB => "KiB", | ||||
|                 Self::MiB => "MiB", | ||||
|                 Self::GiB => "GiB", | ||||
|                 Self::TiB => "TiB", | ||||
|                 Self::PiB => "PiB", | ||||
|             } | ||||
|         ) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] | ||||
| pub struct SizeInfo { | ||||
|     #[serde(rename = "@unit")] | ||||
|     pub unit: SizeUnit, | ||||
|     #[serde(rename = "$value")] | ||||
|     pub amount: u64, | ||||
| } | ||||
| 
 | ||||
| impl From<SizeInfo> for u64 { | ||||
|     fn from(value: SizeInfo) -> Self { | ||||
|         let pow = match value.unit { | ||||
|             SizeUnit::Bytes => 0, | ||||
|             SizeUnit::KiB => 1, | ||||
|             SizeUnit::MiB => 2, | ||||
|             SizeUnit::GiB => 3, | ||||
|             SizeUnit::TiB => 4, | ||||
|             SizeUnit::PiB => 5, | ||||
|         }; | ||||
|         value.amount * 1024u64.pow(pow) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl std::fmt::Display for SizeInfo { | ||||
|     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||||
|         write!(f, "{} {}", self.amount, self.unit) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] | ||||
| pub struct CpuTopology { | ||||
|     #[serde(rename = "@sockets")] | ||||
|     sockets: u8, | ||||
|     #[serde(rename = "@dies")] | ||||
|     dies: u8, | ||||
|     #[serde(rename = "@cores")] | ||||
|     cores: u8, | ||||
|     #[serde(rename = "@threads")] | ||||
|     threads: u8, | ||||
| } | ||||
| 
 | ||||
| #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] | ||||
| pub struct Cpu { | ||||
|     #[serde(rename = "@mode")] | ||||
|     mode: String, | ||||
|     topology: CpuTopology, | ||||
| } | ||||
| 
 | ||||
| //  =^..^=  =^..^=  =^..^=  =^..^=  =^..^=  =^..^=  =^..^=  =^..^=
 | ||||
| 
 | ||||
| #[skip_serializing_none] | ||||
| #[derive(Debug, Default, Serialize, Deserialize, PartialEq, Eq)] | ||||
| #[serde(rename = "volume")] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| pub struct Volume { | ||||
|     #[serde(rename = "@type")] | ||||
|     r#type: String, | ||||
|     pub name: String, | ||||
|     pub key: Option<String>, | ||||
|     allocation: Option<SizeInfo>, | ||||
|     pub capacity: Option<SizeInfo>, | ||||
|     pub target: Option<VolTarget>, | ||||
| } | ||||
| 
 | ||||
| pub enum VolType { | ||||
|     Qcow2, | ||||
|     Block, | ||||
|     Raw, | ||||
|     Invalid, | ||||
| } | ||||
| 
 | ||||
| impl Volume { | ||||
|     pub fn new(name: &str, voltype: VolType, size: SizeInfo) -> Self { | ||||
|         Self { | ||||
|             name: name.to_owned(), | ||||
|             r#type: match voltype { | ||||
|                 VolType::Qcow2 => "file", | ||||
|                 VolType::Block => "block", | ||||
|                 VolType::Raw => "file", | ||||
|                 VolType::Invalid => "invalid", | ||||
|             } | ||||
|             .to_owned(), | ||||
|             capacity: Some(size), | ||||
|             target: match voltype { | ||||
|                 VolType::Qcow2 => Some(VolTarget { | ||||
|                     path: None, | ||||
|                     format: Some(TargetFormat { | ||||
|                         r#type: "qcow2".to_owned(), | ||||
|                     }), | ||||
|                 }), | ||||
|                 VolType::Raw => Some(VolTarget { | ||||
|                     path: None, | ||||
|                     format: Some(TargetFormat { | ||||
|                         r#type: "raw".to_owned(), | ||||
|                     }), | ||||
|                 }), | ||||
|                 // block doesn't require a target aiui, libvirt is
 | ||||
|                 // able to infer
 | ||||
|                 VolType::Block => None, | ||||
|                 VolType::Invalid => None, | ||||
|             }, | ||||
|             allocation: Some(size), | ||||
|             key: None, | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[skip_serializing_none] | ||||
| #[derive(Debug, Default, Serialize, Deserialize, PartialEq, Eq)] | ||||
| pub struct VolTarget { | ||||
|     path: Option<String>, | ||||
|     format: Option<TargetFormat>, | ||||
| } | ||||
| 
 | ||||
| #[derive(Debug, Default, Serialize, Deserialize, PartialEq, Eq)] | ||||
| pub struct TargetFormat { | ||||
|     #[serde(rename = "@type")] | ||||
|     r#type: String, | ||||
| } | ||||
| 
 | ||||
| //  =^..^=  =^..^=  =^..^=  =^..^=  =^..^=  =^..^=  =^..^=  =^..^=
 | ||||
| 
 | ||||
| #[derive(Debug, Default, Serialize, Deserialize, PartialEq, Eq)] | ||||
| #[serde(rename = "pool")] | ||||
| pub struct Pool { | ||||
|     #[serde(rename = "@type")] | ||||
|     r#type: String, | ||||
|     pub name: String, | ||||
|     uuid: Option<uuid::Uuid>, | ||||
|     capacity: Option<SizeInfo>, | ||||
|     allocation: Option<SizeInfo>, | ||||
|     available: Option<SizeInfo>, | ||||
|     target: Option<VolTarget>, | ||||
| } | ||||
| 
 | ||||
| impl Pool { | ||||
|     pub fn vol_type(&self) -> VolType { | ||||
|         match self.r#type.as_str() { | ||||
|             "fs" => VolType::Qcow2, | ||||
|             "dir" => VolType::Qcow2, | ||||
|             "logical" => VolType::Block, | ||||
|             "zfs" => VolType::Block, | ||||
|             _ => VolType::Invalid, | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										125
									
								
								nzrd/src/ctrl/virtxml/test.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										125
									
								
								nzrd/src/ctrl/virtxml/test.rs
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,125 @@ | |||
| use uuid::uuid; | ||||
| 
 | ||||
| use super::*; | ||||
| use crate::ctrl::virtxml::build::DomainBuilder; | ||||
| use crate::prelude::*; | ||||
| 
 | ||||
| trait Unprettify { | ||||
|     fn unprettify(&self) -> String; | ||||
| } | ||||
| 
 | ||||
| impl Unprettify for &str { | ||||
|     fn unprettify(&self) -> String { | ||||
|         self.split('\n') | ||||
|             .map(|s| s.trim()) | ||||
|             .collect::<Vec<&str>>() | ||||
|             .concat() | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[test] | ||||
| fn domain_serde() { | ||||
|     let dom_str = r#"<domain type="kvm">
 | ||||
|   <name>test-vm</name> | ||||
|   <uuid>9a8f2611-a976-4d06-ac91-2750ac3462b3</uuid> | ||||
|   <description>This is a test</description> | ||||
|   <vcpu placement="static">2</vcpu> | ||||
|   <memory unit="MiB">512</memory> | ||||
|   <features> | ||||
|     <apic/> | ||||
|     <acpi/> | ||||
|   </features> | ||||
|   <cpu mode="host-passthrough"> | ||||
|     <topology sockets="1" dies="1" cores="2" threads="1"/> | ||||
|   </cpu> | ||||
|   <devices> | ||||
|     <disk type="volume" device="disk"> | ||||
|       <source pool="tank" volume="test-vm-root"/> | ||||
|       <target dev="sda" bus="virtio"/> | ||||
|     </disk> | ||||
|     <interface type="bridge"> | ||||
|       <mac address="02:0b:ee:ca:fe:42"/> | ||||
|       <source bridge="virbr0"/> | ||||
|       <model type="virtio"/> | ||||
|     </interface> | ||||
|   </devices> | ||||
|   <os> | ||||
|     <boot dev="hd"/> | ||||
|     <type arch="x86_64" machine="pc-i440fx-5.2">hvm</type> | ||||
|     <bios useserial="yes" rebootTimeout="0"/> | ||||
|   </os> | ||||
| </domain>"#
 | ||||
|         .unprettify(); | ||||
|     println!("Serializing domain..."); | ||||
|     let mac = MacAddr::new(0x02, 0x0b, 0xee, 0xca, 0xfe, 0x42); | ||||
|     let uuid = uuid!("9a8f2611-a976-4d06-ac91-2750ac3462b3"); | ||||
|     let domain = DomainBuilder::default() | ||||
|         .name("test-vm") | ||||
|         .uuid(uuid) | ||||
|         .description("This is a test") | ||||
|         .disk_device(|dsk| { | ||||
|             dsk.volume_source("tank", "test-vm-root") | ||||
|                 .target("sda", "virtio") | ||||
|         }) | ||||
|         .net_device(|net| net.with_bridge("virbr0").mac_addr(&mac)) | ||||
|         .build(); | ||||
|     let dom_xml = quick_xml::se::to_string(&domain).unwrap(); | ||||
|     println!("{}", dom_xml); | ||||
|     assert_eq!(&dom_xml, &dom_str); | ||||
|     println!("Deserializing domain..."); | ||||
|     let _new_dom: Domain = quick_xml::de::from_str(&dom_str).unwrap(); | ||||
| } | ||||
| 
 | ||||
| #[test] | ||||
| fn pool_serde() { | ||||
|     let pool_str = r#"<pool type="dir">
 | ||||
|   <name>default</name> | ||||
|   <uuid>4ad34b59-9418-483d-9533-2e8b51e7317e</uuid> | ||||
|   <capacity unit="GiB">200</capacity> | ||||
|   <allocation unit="GiB">120</allocation> | ||||
|   <available unit="GiB">80</available> | ||||
|   <target> | ||||
|     <path>/var/lib/libvirt/images</path> | ||||
|   </target> | ||||
| </pool>"#
 | ||||
|         .unprettify(); | ||||
|     println!("Serializing pool..."); | ||||
|     let pool: Pool = Pool { | ||||
|         r#type: "dir".to_owned(), | ||||
|         name: "default".to_owned(), | ||||
|         uuid: Some(uuid!("4ad34b59-9418-483d-9533-2e8b51e7317e")), | ||||
|         capacity: Some(datasize!(200 GiB)), | ||||
|         allocation: Some(datasize!(120 GiB)), | ||||
|         available: Some(datasize!(80 GiB)), | ||||
|         target: Some(VolTarget { | ||||
|             path: Some("/var/lib/libvirt/images".to_owned()), | ||||
|             format: None, | ||||
|         }), | ||||
|     }; | ||||
|     let pool_xml = quick_xml::se::to_string(&pool).unwrap(); | ||||
|     assert_eq!(&pool_xml, &pool_str); | ||||
|     println!("Deserializing pool..."); | ||||
|     let pool: Pool = quick_xml::de::from_str(&pool_str).unwrap(); | ||||
|     assert_eq!(pool.name, "default".to_owned()); | ||||
|     assert_eq!(pool.r#type, "dir".to_owned()); | ||||
| } | ||||
| 
 | ||||
| #[test] | ||||
| fn vol_serde() { | ||||
|     let vol_str = r#"<volume type="file">
 | ||||
|   <name>test</name> | ||||
|   <allocation unit="GiB">20</allocation> | ||||
|   <capacity unit="GiB">20</capacity> | ||||
|   <target> | ||||
|     <format type="qcow2"/> | ||||
|   </target> | ||||
| </volume>"#
 | ||||
|         .unprettify(); | ||||
|     println!("Serializing volume..."); | ||||
|     let vol: Volume = Volume::new("test", VolType::Qcow2, datasize!(20 GiB)); | ||||
|     let vol_xml = quick_xml::se::to_string(&vol).unwrap(); | ||||
|     assert_eq!(&vol_xml, &vol_str); | ||||
|     println!("Deserializing volume..."); | ||||
|     let vol_obj: Volume = quick_xml::de::from_str(&vol_str).unwrap(); | ||||
|     assert_eq!(vol_obj, vol); | ||||
| } | ||||
							
								
								
									
										307
									
								
								nzrd/src/ctrl/vm.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										307
									
								
								nzrd/src/ctrl/vm.rs
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,307 @@ | |||
| use crate::ctrl::net::Lease; | ||||
| use crate::ctx::Context; | ||||
| use log::*; | ||||
| use nzr_api::net::cidr::CidrV4; | ||||
| use std::net::Ipv4Addr; | ||||
| use std::str::{self, Utf8Error}; | ||||
| 
 | ||||
| use super::virtxml::{build::DomainBuilder, Domain}; | ||||
| use super::Storable; | ||||
| use super::{net::Subnet, Entity}; | ||||
| use crate::virt::*; | ||||
| use serde::{Deserialize, Serialize}; | ||||
| 
 | ||||
| #[derive(Clone, Serialize, Deserialize)] | ||||
| pub struct InstDb { | ||||
|     uuid: uuid::Uuid, | ||||
|     lease_if: String, | ||||
|     lease_addr: CidrV4, | ||||
| } | ||||
| 
 | ||||
| impl InstDb { | ||||
|     pub fn addr(&self) -> Ipv4Addr { | ||||
|         self.lease_addr.addr | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl Storable for InstDb { | ||||
|     fn tree_name() -> Option<&'static [u8]> { | ||||
|         Some(b"instances") | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| pub struct Instance { | ||||
|     db_data: Entity<InstDb>, | ||||
|     lease: Option<Entity<Lease>>, | ||||
|     ctx: Context, | ||||
|     domain_xml: Domain, | ||||
| } | ||||
| 
 | ||||
| impl Instance { | ||||
|     pub async fn new( | ||||
|         ctx: Context, | ||||
|         subnet: Entity<Subnet>, | ||||
|         lease: Entity<Lease>, | ||||
|         builder: DomainBuilder, | ||||
|     ) -> Result<(Self, virt::domain::Domain), InstanceError> { | ||||
|         let domain_xml = builder.build(); | ||||
| 
 | ||||
|         let virt_domain = { | ||||
|             let inst_xml = | ||||
|                 quick_xml::se::to_string(&domain_xml).map_err(InstanceError::CantSerialize)?; | ||||
|             virt::domain::Domain::define_xml(&ctx.virt.conn, &inst_xml) | ||||
|                 .map_err(InstanceError::CreationFailed)? | ||||
|         }; | ||||
| 
 | ||||
|         // Get the final XML data back from libvirt; this will contain the UUID and
 | ||||
|         // other auto-filled stuff
 | ||||
|         let real_xml = match virt_domain.get_xml_desc(0) { | ||||
|             Ok(xml_data) => match quick_xml::de::from_str::<Domain>(&xml_data) { | ||||
|                 Ok(xml_obj) => xml_obj, | ||||
|                 Err(err) => { | ||||
|                     error!("Failed to deserialize XML from libvirt: {}", err); | ||||
|                     if let Err(err) = virt_domain.undefine() { | ||||
|                         warn!("Couldn't undefine domain after failure: {}", err); | ||||
|                     } | ||||
|                     return Err(InstanceError::CantDeserialize(err)); | ||||
|                 } | ||||
|             }, | ||||
|             Err(err) => { | ||||
|                 error!("Failed to get XML data from libvirt: {}", err); | ||||
|                 if let Err(err) = virt_domain.undefine() { | ||||
|                     warn!("Couldn't undefine domain after failure: {}", err); | ||||
|                 } | ||||
|                 return Err(InstanceError::VirtError(err)); | ||||
|             } | ||||
|         }; | ||||
| 
 | ||||
|         debug!( | ||||
|             "Adding {} (interface: {}) to the instance tree...", | ||||
|             &lease.ipv4_addr, &subnet.interface, | ||||
|         ); | ||||
| 
 | ||||
|         let db_data = InstDb { | ||||
|             uuid: real_xml.uuid, | ||||
|             lease_if: subnet.interface.clone(), | ||||
|             lease_addr: lease.ipv4_addr.clone(), | ||||
|         }; | ||||
| 
 | ||||
|         let db_data = InstDb::insert(ctx.db.clone(), db_data, real_xml.name.as_bytes()) | ||||
|             .map_err(InstanceError::other)?; | ||||
| 
 | ||||
|         let inst_obj = Instance { | ||||
|             db_data, | ||||
|             lease: Some(lease), | ||||
|             ctx, | ||||
|             domain_xml, | ||||
|         }; | ||||
| 
 | ||||
|         Ok((inst_obj, virt_domain)) | ||||
|     } | ||||
| 
 | ||||
|     pub fn uuid(&self) -> uuid::Uuid { | ||||
|         self.db_data.uuid | ||||
|     } | ||||
| 
 | ||||
|     pub fn persist(&mut self) { | ||||
|         if let Some(lease) = &mut self.lease { | ||||
|             lease.transient = false; | ||||
|         } | ||||
| 
 | ||||
|         self.db_data.transient = false; | ||||
|     } | ||||
| 
 | ||||
|     pub async fn undefine(&mut self) -> Result<(), InstanceError> { | ||||
|         let virt_domain = self.virt()?; | ||||
|         let connect = virt_domain | ||||
|             .get_connect() | ||||
|             .map_err(InstanceError::VirtError)?; | ||||
| 
 | ||||
|         // delete volumes
 | ||||
|         for disk in self.domain_xml.devices.disks() { | ||||
|             if let (Some(pool), Some(vol)) = (&disk.source.pool, &disk.source.volume) { | ||||
|                 if let Ok(vpool) = VirtPool::lookup_by_name(&connect, pool) { | ||||
|                     match VirtVolume::lookup_by_name(vpool, vol) { | ||||
|                         Ok(virt_vol) => { | ||||
|                             if let Err(er) = virt_vol.delete(0) { | ||||
|                                 warn!("Can't delete {}/{}: {}", pool, vol, er); | ||||
|                             } | ||||
|                         } | ||||
|                         Err(er) => { | ||||
|                             warn!("Can't acquire handle to {}/{}: {}", pool, vol, er); | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         // undefine IP lease
 | ||||
|         if let Some(lease) = &mut self.lease { | ||||
|             lease.delete().map_err(InstanceError::other)?; | ||||
|         } | ||||
| 
 | ||||
|         // delete instance
 | ||||
|         virt_domain | ||||
|             .undefine() | ||||
|             .map_err(InstanceError::DomainDelete)?; | ||||
|         self.db_data.delete().map_err(InstanceError::other)?; | ||||
|         Ok(()) | ||||
|     } | ||||
| 
 | ||||
|     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) { | ||||
|             Ok(inst) => Ok(inst), | ||||
|             Err(err) => { | ||||
|                 if err.code() == virt::error::ErrorNumber::NoDomain { | ||||
|                     // domain not found
 | ||||
|                     Err(InstanceError::DomainNotFound(name.to_owned())) | ||||
|                 } else { | ||||
|                     Err(InstanceError::VirtError(err)) | ||||
|                 } | ||||
|             } | ||||
|         }?; | ||||
|         let domain_xml: Domain = { | ||||
|             let xml_str = virt_domain | ||||
|                 .get_xml_desc(0) | ||||
|                 .map_err(InstanceError::VirtError)?; | ||||
|             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()) | ||||
|             .map_err(InstanceError::other)? | ||||
|         { | ||||
|             Some(subnet) => subnet | ||||
|                 .leases() | ||||
|                 .map_err(InstanceError::other)? | ||||
|                 .find(|l| { | ||||
|                     if let Ok(lease) = l { | ||||
|                         lease.ipv4_addr == db_data.lease_addr | ||||
|                     } else { | ||||
|                         false | ||||
|                     } | ||||
|                 }) | ||||
|                 .map(|o| o.unwrap()), | ||||
|             None => None, | ||||
|         }; | ||||
| 
 | ||||
|         Ok(Self { | ||||
|             ctx, | ||||
|             domain_xml, | ||||
|             db_data, | ||||
|             lease, | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     pub async fn lookup_by_name(ctx: Context, name: &str) -> Result<Option<Self>, InstanceError> { | ||||
|         let db_data = match InstDb::get_by_key(ctx.db.clone(), name.as_bytes()) | ||||
|             .map_err(InstanceError::other)? | ||||
|         { | ||||
|             Some(data) => data, | ||||
|             None => { | ||||
|                 return Ok(None); | ||||
|             } | ||||
|         }; | ||||
| 
 | ||||
|         // TODO: handle from_instdb having None?
 | ||||
|         Self::from_entity(ctx, db_data).map(Some) | ||||
|     } | ||||
| 
 | ||||
|     pub fn virt(&self) -> Result<virt::domain::Domain, InstanceError> { | ||||
|         let name = self.domain_xml.name.as_str(); | ||||
|         match virt::domain::Domain::lookup_by_name(&self.ctx.virt.conn, name) { | ||||
|             Ok(inst) => Ok(inst), | ||||
|             Err(err) => { | ||||
|                 if err.code() == virt::error::ErrorNumber::NoDomain { | ||||
|                     // domain not found
 | ||||
|                     Err(InstanceError::DomainNotFound(name.to_owned())) | ||||
|                 } else { | ||||
|                     Err(InstanceError::VirtError(err)) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     pub fn xml(&self) -> &Domain { | ||||
|         &self.domain_xml | ||||
|     } | ||||
| 
 | ||||
|     pub fn ip_lease(&self) -> Option<&Lease> { | ||||
|         self.lease.as_deref() | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl From<&Instance> for nzr_api::model::Instance { | ||||
|     fn from(value: &Instance) -> Self { | ||||
|         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 { | ||||
|                 addr: l.ipv4_addr.clone(), | ||||
|                 mac_addr: l.mac_addr.clone(), | ||||
|             }), | ||||
|             state: value.virt().map_or(Default::default(), |domain| { | ||||
|                 domain | ||||
|                     .get_state() | ||||
|                     .map_or(Default::default(), |(code, _reason)| code.into()) | ||||
|             }), | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[derive(Debug)] | ||||
| pub enum InstanceError { | ||||
|     VirtError(virt::error::Error), | ||||
|     NotInDb, | ||||
|     CantDeserialize(quick_xml::de::DeError), | ||||
|     CantSerialize(quick_xml::de::DeError), | ||||
|     DbError(sled::Error), | ||||
|     MalformedData, | ||||
|     DomainNotFound(String), | ||||
|     CreationFailed(virt::error::Error), | ||||
|     BadInterface(Utf8Error), | ||||
|     NoSubnetForInterface, | ||||
|     Other(Box<dyn std::error::Error>), | ||||
|     LeaseNotInDb, | ||||
|     DomainDelete(virt::error::Error), | ||||
|     LeaseUndefined, | ||||
| } | ||||
| 
 | ||||
| impl std::fmt::Display for InstanceError { | ||||
|     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||||
|         match self { | ||||
|             Self::VirtError(er) => er.fmt(f), | ||||
|             Self::NotInDb => write!(f, "Domain exists in libvirt but is not in database"), | ||||
|             Self::CantDeserialize(er) => write!(f, "Deserializing domain XML failed: {}", er), | ||||
|             Self::CantSerialize(er) => write!(f, "Serializing domain XML failed: {}", er), | ||||
|             Self::DbError(er) => write!(f, "Database error: {}", er), | ||||
|             Self::DomainNotFound(name) => write!(f, "No domain {} found in libvirt", name), | ||||
|             Self::MalformedData => write!(f, "Entry has malformed data in database"), | ||||
|             Self::CreationFailed(er) => write!(f, "Error while creating domain: {}", er), | ||||
|             Self::BadInterface(er) => { | ||||
|                 write!(f, "Couldn't get interface name from database: {}", er) | ||||
|             } | ||||
|             Self::NoSubnetForInterface => { | ||||
|                 write!(f, "Interface associated with instance isn't in database!") | ||||
|             } | ||||
|             Self::LeaseNotInDb => write!( | ||||
|                 f, | ||||
|                 "Found IP address, but it doesn't correspond to a lease in the database" | ||||
|             ), | ||||
|             Self::DomainDelete(ve) => write!(f, "Couldn't delete libvirt domain: {}", ve), | ||||
|             Self::LeaseUndefined => write!(f, "Lease has been undefined by another function"), | ||||
|             Self::Other(er) => er.fmt(f), | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl InstanceError { | ||||
|     fn other<E>(err: E) -> Self | ||||
|     where | ||||
|         E: std::error::Error + 'static, | ||||
|     { | ||||
|         Self::Other(Box::new(err)) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl std::error::Error for InstanceError {} | ||||
							
								
								
									
										100
									
								
								nzrd/src/ctx.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										100
									
								
								nzrd/src/ctx.rs
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,100 @@ | |||
| use std::{fmt, ops::Deref}; | ||||
| use virt::connect::Connect; | ||||
| 
 | ||||
| use crate::{dns::ZoneData, virt::VirtPool}; | ||||
| use nzr_api::config::Config; | ||||
| use std::sync::Arc; | ||||
| 
 | ||||
| pub struct PoolRefs { | ||||
|     pub primary: VirtPool, | ||||
|     pub secondary: VirtPool, | ||||
|     pub cidata: VirtPool, | ||||
|     pub baseimg: VirtPool, | ||||
| } | ||||
| 
 | ||||
| impl PoolRefs { | ||||
|     pub fn find_pool(&self, name: &str) -> Option<&VirtPool> { | ||||
|         for pool in [&self.primary, &self.secondary, &self.baseimg, &self.cidata] { | ||||
|             if let Ok(pool_name) = pool.get_name() { | ||||
|                 if pool_name == name { | ||||
|                     return Some(pool); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         None | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| pub struct VirtCtx { | ||||
|     pub conn: virt::connect::Connect, | ||||
|     pub pools: PoolRefs, | ||||
| } | ||||
| 
 | ||||
| #[derive(Clone)] | ||||
| pub struct Context(Arc<InnerCtx>); | ||||
| 
 | ||||
| impl Deref for Context { | ||||
|     type Target = InnerCtx; | ||||
|     fn deref(&self) -> &Self::Target { | ||||
|         &self.0 | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| pub struct InnerCtx { | ||||
|     pub db: sled::Db, | ||||
|     pub config: Config, | ||||
|     pub zones: crate::dns::ZoneData, | ||||
|     pub virt: VirtCtx, | ||||
| } | ||||
| 
 | ||||
| #[derive(Debug)] | ||||
| pub enum ContextError { | ||||
|     Virt(virt::error::Error), | ||||
|     Db(sled::Error), | ||||
|     Pool(crate::virt::PoolError), | ||||
| } | ||||
| 
 | ||||
| impl fmt::Display for ContextError { | ||||
|     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||||
|         match self { | ||||
|             Self::Virt(ve) => write!(f, "Error connecting to libvirt: {}", ve), | ||||
|             Self::Db(de) => write!(f, "Error opening database: {}", de), | ||||
|             Self::Pool(pe) => write!(f, "Error opening pool: {}", pe), | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl std::error::Error for ContextError {} | ||||
| 
 | ||||
| impl InnerCtx { | ||||
|     fn new(config: Config) -> Result<Self, ContextError> { | ||||
|         let zones = ZoneData::new(&config.dns); | ||||
|         let conn = Connect::open(&config.libvirt_uri).map_err(ContextError::Virt)?; | ||||
|         virt::error::clear_error_callback(); | ||||
| 
 | ||||
|         let pools = PoolRefs { | ||||
|             primary: VirtPool::lookup_by_name(&conn, &config.storage.primary_pool) | ||||
|                 .map_err(ContextError::Pool)?, | ||||
|             secondary: VirtPool::lookup_by_name(&conn, &config.storage.secondary_pool) | ||||
|                 .map_err(ContextError::Pool)?, | ||||
|             cidata: VirtPool::lookup_by_name(&conn, &config.storage.ci_image_pool) | ||||
|                 .map_err(ContextError::Pool)?, | ||||
|             baseimg: VirtPool::lookup_by_name(&conn, &config.storage.base_image_pool) | ||||
|                 .map_err(ContextError::Pool)?, | ||||
|         }; | ||||
| 
 | ||||
|         Ok(Self { | ||||
|             db: sled::open(&config.db_path).map_err(ContextError::Db)?, | ||||
|             config, | ||||
|             zones, | ||||
|             virt: VirtCtx { conn, pools }, | ||||
|         }) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl Context { | ||||
|     pub fn new(config: Config) -> Result<Self, ContextError> { | ||||
|         let inner = InnerCtx::new(config)?; | ||||
|         Ok(Self(Arc::new(inner))) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										196
									
								
								nzrd/src/dns.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										196
									
								
								nzrd/src/dns.rs
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,196 @@ | |||
| use crate::ctrl::net::Subnet; | ||||
| use log::*; | ||||
| use nzr_api::config::DNSConfig; | ||||
| use std::borrow::Borrow; | ||||
| use std::collections::{BTreeMap, HashMap}; | ||||
| use std::net::Ipv4Addr; | ||||
| use std::ops::Deref; | ||||
| use std::str::FromStr; | ||||
| use std::sync::Arc; | ||||
| use tokio::sync::{Mutex, RwLock}; | ||||
| 
 | ||||
| use trust_dns_server::authority::{AuthorityObject, Catalog}; | ||||
| use trust_dns_server::client::rr::{LowerName, RrKey}; | ||||
| use trust_dns_server::proto::rr::{rdata::soa, RData, RecordSet}; | ||||
| use trust_dns_server::server::{Request, RequestHandler, ResponseHandler, ResponseInfo}; | ||||
| use trust_dns_server::{ | ||||
|     proto::rr::{Name, Record}, | ||||
|     store::in_memory::InMemoryAuthority, | ||||
| }; | ||||
| 
 | ||||
| #[derive(Clone)] | ||||
| pub struct CatalogRef(Arc<RwLock<Catalog>>); | ||||
| 
 | ||||
| macro_rules! make_serial { | ||||
|     () => {{ | ||||
|         use std::time::{SystemTime, UNIX_EPOCH}; | ||||
| 
 | ||||
|         SystemTime::now() | ||||
|             .duration_since(UNIX_EPOCH) | ||||
|             .expect("You know what? Fuck you. *unepochs your time*") | ||||
|             .as_secs() as u32 | ||||
|     }}; | ||||
| } | ||||
| 
 | ||||
| #[async_trait::async_trait] | ||||
| impl RequestHandler for CatalogRef { | ||||
|     async fn handle_request<R>(&self, request: &Request, response_handle: R) -> ResponseInfo | ||||
|     where | ||||
|         R: ResponseHandler, | ||||
|     { | ||||
|         self.0 | ||||
|             .read() | ||||
|             .await | ||||
|             .handle_request(request, response_handle) | ||||
|             .await | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[derive(Clone)] | ||||
| pub struct ZoneData(Arc<InnerZD>); | ||||
| 
 | ||||
| impl Deref for ZoneData { | ||||
|     type Target = InnerZD; | ||||
|     fn deref(&self) -> &Self::Target { | ||||
|         self.0.as_ref() | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl ZoneData { | ||||
|     pub fn new(dns_config: &DNSConfig) -> Self { | ||||
|         ZoneData(Arc::new(InnerZD::new(dns_config))) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| pub struct InnerZD { | ||||
|     default_zone: Arc<InMemoryAuthority>, | ||||
|     map: Mutex<HashMap<String, Arc<InMemoryAuthority>>>, | ||||
|     catalog: CatalogRef, | ||||
|     config: DNSConfig, | ||||
| } | ||||
| 
 | ||||
| pub fn make_rectree_with_soa(name: &Name, config: &DNSConfig) -> BTreeMap<RrKey, RecordSet> { | ||||
|     debug!("Creating initial SOA for {}", &name); | ||||
|     let mut records: BTreeMap<RrKey, RecordSet> = BTreeMap::new(); | ||||
|     let soa_key = RrKey::new( | ||||
|         LowerName::from(name), | ||||
|         trust_dns_server::proto::rr::RecordType::SOA, | ||||
|     ); | ||||
|     let soa_rec = Record::from_rdata( | ||||
|         name.clone(), | ||||
|         3600, | ||||
|         RData::SOA(soa::SOA::new( | ||||
|             name.clone(), | ||||
|             config.soa.contact.clone(), | ||||
|             make_serial!(), | ||||
|             config.soa.refresh, | ||||
|             config.soa.retry, | ||||
|             config.soa.expire, | ||||
|             3600, | ||||
|         )), | ||||
|     ); | ||||
|     records.insert(soa_key, RecordSet::from(soa_rec)); | ||||
|     records | ||||
| } | ||||
| 
 | ||||
| impl InnerZD { | ||||
|     pub fn new(dns_config: &DNSConfig) -> Self { | ||||
|         let default_zone = Arc::new({ | ||||
|             let records = make_rectree_with_soa(&dns_config.default_zone, dns_config); | ||||
|             InMemoryAuthority::new( | ||||
|                 dns_config.default_zone.clone(), | ||||
|                 records, | ||||
|                 trust_dns_server::authority::ZoneType::Primary, | ||||
|                 false, | ||||
|             ) | ||||
|             .unwrap() | ||||
|         }); | ||||
|         let mut catalog = Catalog::new(); | ||||
|         catalog.upsert( | ||||
|             dns_config.default_zone.borrow().into(), | ||||
|             Box::new(default_zone.clone()), | ||||
|         ); | ||||
|         Self { | ||||
|             default_zone, | ||||
|             map: Mutex::new(HashMap::new()), | ||||
|             catalog: CatalogRef(Arc::new(RwLock::new(catalog))), | ||||
|             config: dns_config.clone(), | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     pub async fn new_zone(&self, subnet: &Subnet) -> Result<(), Box<dyn std::error::Error>> { | ||||
|         if let Some(name) = &subnet.domain_name { | ||||
|             let auth = InMemoryAuthority::new( | ||||
|                 name.clone(), | ||||
|                 make_rectree_with_soa(name, &self.config), | ||||
|                 trust_dns_server::authority::ZoneType::Primary, | ||||
|                 false, | ||||
|             )?; | ||||
|             self.import(&subnet.interface, auth).await; | ||||
|         } | ||||
|         Ok(()) | ||||
|     } | ||||
| 
 | ||||
|     pub async fn import(&self, name: &str, auth: InMemoryAuthority) { | ||||
|         let auth_arc = Arc::new(auth); | ||||
|         self.map | ||||
|             .lock() | ||||
|             .await | ||||
|             .insert(name.to_owned(), auth_arc.clone()); | ||||
| 
 | ||||
|         self.catalog | ||||
|             .0 | ||||
|             .write() | ||||
|             .await | ||||
|             .upsert(auth_arc.origin().clone(), Box::new(auth_arc.clone())); | ||||
|     } | ||||
| 
 | ||||
|     pub async fn delete_zone(&self, interface: &str) -> bool { | ||||
|         self.map.lock().await.remove(interface).is_some() | ||||
|     } | ||||
| 
 | ||||
|     pub async fn new_record( | ||||
|         &self, | ||||
|         interface: &str, | ||||
|         name: &str, | ||||
|         addr: Ipv4Addr, | ||||
|     ) -> Result<(), Box<dyn std::error::Error>> { | ||||
|         let hostname = Name::from_str(name)?; | ||||
|         let zones = self.map.lock().await; | ||||
|         let zone = zones.get(interface).unwrap_or(&self.default_zone); | ||||
|         let fqdn = { | ||||
|             let origin: Name = zone.origin().into(); | ||||
|             hostname.append_domain(&origin)? | ||||
|         }; | ||||
| 
 | ||||
|         let record = Record::from_rdata(fqdn, 3600, RData::A(addr)); | ||||
|         zone.upsert(record, 0).await; | ||||
|         self.catalog() | ||||
|             .0 | ||||
|             .write() | ||||
|             .await | ||||
|             .upsert(zone.origin().clone(), Box::new(zone.clone())); | ||||
|         Ok(()) | ||||
|     } | ||||
| 
 | ||||
|     pub async fn delete_record( | ||||
|         &self, | ||||
|         interface: &str, | ||||
|         name: &str, | ||||
|     ) -> Result<bool, Box<dyn std::error::Error>> { | ||||
|         let hostname = Name::from_str(name)?; | ||||
|         let mut zones = self.map.lock().await; | ||||
|         if let Some(zone) = zones.get_mut(interface) { | ||||
|             let hostname: LowerName = hostname.into(); | ||||
|             self.catalog.0.write().await.remove(&hostname); | ||||
|             let key = RrKey::new(hostname, trust_dns_server::proto::rr::RecordType::A); | ||||
|             Ok(zone.records_mut().await.remove(&key).is_some()) | ||||
|         } else { | ||||
|             Ok(false) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     pub fn catalog(&self) -> CatalogRef { | ||||
|         self.catalog.clone() | ||||
|     } | ||||
| } | ||||
							
								
								
									
										99
									
								
								nzrd/src/main.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								nzrd/src/main.rs
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,99 @@ | |||
| mod cloud; | ||||
| mod cmd; | ||||
| mod ctrl; | ||||
| mod ctx; | ||||
| mod dns; | ||||
| mod prelude; | ||||
| mod rpc; | ||||
| #[cfg(test)] | ||||
| mod test; | ||||
| mod virt; | ||||
| 
 | ||||
| use crate::ctrl::{net::Subnet, Storable}; | ||||
| use log::LevelFilter; | ||||
| use log::*; | ||||
| use nzr_api::config; | ||||
| use std::str::FromStr; | ||||
| use tokio::net::UdpSocket; | ||||
| use trust_dns_server::ServerFuture; | ||||
| 
 | ||||
| #[tokio::main] | ||||
| async fn main() -> Result<(), Box<dyn std::error::Error>> { | ||||
|     let cfg: config::Config = config::Config::figment().extract()?; | ||||
|     let ctx = ctx::Context::new(cfg)?; | ||||
| 
 | ||||
|     syslog::init_unix( | ||||
|         syslog::Facility::LOG_DAEMON, | ||||
|         LevelFilter::from_str(ctx.config.log_level.as_str())?, | ||||
|     )?; | ||||
| 
 | ||||
|     info!("Hydrating initial zones..."); | ||||
|     for subnet in Subnet::all(ctx.db.clone())? { | ||||
|         match subnet { | ||||
|             Ok(subnet) => { | ||||
|                 // A records
 | ||||
|                 if let Err(err) = ctx.zones.new_zone(&subnet).await { | ||||
|                     error!("Couldn't create zone for {}: {}", &subnet.interface, err); | ||||
|                     continue; | ||||
|                 } | ||||
|                 match subnet.leases() { | ||||
|                     Ok(leases) => { | ||||
|                         for lease in leases { | ||||
|                             match lease { | ||||
|                                 Ok(lease) => { | ||||
|                                     if let Err(err) = ctx | ||||
|                                         .zones | ||||
|                                         .new_record( | ||||
|                                             &subnet.interface, | ||||
|                                             &lease.inst_name, | ||||
|                                             lease.ipv4_addr.addr, | ||||
|                                         ) | ||||
|                                         .await | ||||
|                                     { | ||||
|                                         error!( | ||||
|                                             "Failed to set up lease for {} in {}: {}", | ||||
|                                             &lease.inst_name, &subnet.interface, err | ||||
|                                         ); | ||||
|                                     } | ||||
|                                 } | ||||
|                                 Err(err) => { | ||||
|                                     warn!( | ||||
|                                         "Lease iterator error while hydrating {}: {}", | ||||
|                                         &subnet.interface, err | ||||
|                                     ); | ||||
|                                 } | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                     Err(err) => { | ||||
|                         error!("Couldn't get leases for {}: {}", &subnet.interface, err); | ||||
|                         continue; | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             Err(err) => { | ||||
|                 warn!("Error while iterating subnets: {}", err); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     // DNS init
 | ||||
|     let mut dns_listener = ServerFuture::new(ctx.zones.catalog()); | ||||
|     let dns_socket = UdpSocket::bind(ctx.config.dns.listen_addr.as_str()).await?; | ||||
|     dns_listener.register_socket(dns_socket); | ||||
| 
 | ||||
|     tokio::select! { | ||||
|         res = rpc::serve(ctx.clone(), ctx.zones.clone()) => { | ||||
|             if let Err(err) = res { | ||||
|                 error!("Error from RPC: {}", err); | ||||
|             } | ||||
|         }, | ||||
|         res = dns_listener.block_until_done() => { | ||||
|             if let Err(err) = res { | ||||
|                 error!("Error from DNS: {}", err); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     Ok(()) | ||||
| } | ||||
							
								
								
									
										10
									
								
								nzrd/src/prelude.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								nzrd/src/prelude.rs
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,10 @@ | |||
| macro_rules! datasize { | ||||
|     ($amt:tt $unit:tt) => { | ||||
|         $crate::ctrl::virtxml::SizeInfo { | ||||
|             amount: $amt as u64, | ||||
|             unit: $crate::ctrl::virtxml::SizeUnit::$unit, | ||||
|         } | ||||
|     }; | ||||
| } | ||||
| 
 | ||||
| pub(crate) use datasize; | ||||
							
								
								
									
										167
									
								
								nzrd/src/rpc.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										167
									
								
								nzrd/src/rpc.rs
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,167 @@ | |||
| use nzr_api::{args, model, Nazrin}; | ||||
| use tarpc::server::{BaseChannel, Channel}; | ||||
| use tarpc::tokio_serde::formats::Bincode; | ||||
| use tarpc::tokio_util::codec::LengthDelimitedCodec; | ||||
| use tokio::net::UnixListener; | ||||
| 
 | ||||
| use crate::ctrl::vm::InstDb; | ||||
| use crate::ctrl::{net::Subnet, Storable}; | ||||
| use crate::ctx::Context; | ||||
| use crate::dns::ZoneData; | ||||
| use crate::{cmd, ctrl::vm::Instance}; | ||||
| use log::*; | ||||
| use std::ops::Deref; | ||||
| 
 | ||||
| #[derive(Clone)] | ||||
| pub struct NzrServer { | ||||
|     ctx: Context, | ||||
|     zones: ZoneData, | ||||
| } | ||||
| 
 | ||||
| impl NzrServer { | ||||
|     pub fn new(ctx: Context, zones: ZoneData) -> Self { | ||||
|         Self { ctx, zones } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[tarpc::server] | ||||
| impl Nazrin for NzrServer { | ||||
|     async fn new_instance( | ||||
|         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) | ||||
|                 .await | ||||
|             { | ||||
|                 warn!("Instance created, but no DNS record was made: {}", err); | ||||
|             } | ||||
|         } | ||||
|         Ok((&inst).into()) | ||||
|     } | ||||
| 
 | ||||
|     async fn delete_instance(self, _: tarpc::context::Context, name: String) -> Result<(), String> { | ||||
|         cmd::vm::delete_instance(self.ctx.clone(), name) | ||||
|             .await | ||||
|             .map_err(|e| format!("Couldn't delete instance: {}", e))?; | ||||
|         Ok(()) | ||||
|     } | ||||
| 
 | ||||
|     async fn get_instances( | ||||
|         self, | ||||
|         _: tarpc::context::Context, | ||||
|     ) -> 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 | ||||
|                     } | ||||
|                 }, | ||||
|                 Err(err) => { | ||||
|                     warn!("Iterator error: {}", err); | ||||
|                     None | ||||
|                 } | ||||
|             }) | ||||
|             .collect(); | ||||
|         Ok(insts) | ||||
|     } | ||||
| 
 | ||||
|     async fn new_subnet( | ||||
|         self, | ||||
|         _: tarpc::context::Context, | ||||
|         build_args: model::Subnet, | ||||
|     ) -> Result<model::Subnet, String> { | ||||
|         let subnet = cmd::net::add_subnet(&self.ctx, build_args) | ||||
|             .await | ||||
|             .map_err(|e| e.to_string())?; | ||||
|         self.zones | ||||
|             .new_zone(&subnet) | ||||
|             .await | ||||
|             .map_err(|e| e.to_string())?; | ||||
|         Ok(<&Subnet as Into<model::Subnet>>::into(&subnet)) | ||||
|     } | ||||
| 
 | ||||
|     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())), | ||||
|                 Err(err) => { | ||||
|                     warn!("Iterator error: {}", err); | ||||
|                     None | ||||
|                 } | ||||
|             }) | ||||
|             .collect(); | ||||
|         Ok(subnets) | ||||
|     } | ||||
| 
 | ||||
|     async fn delete_subnet( | ||||
|         self, | ||||
|         _: tarpc::context::Context, | ||||
|         interface: String, | ||||
|     ) -> Result<(), String> { | ||||
|         cmd::net::delete_subnet(&self.ctx, &interface).map_err(|e| e.to_string())?; | ||||
|         self.zones.delete_zone(&interface).await; | ||||
|         Ok(()) | ||||
|     } | ||||
| 
 | ||||
|     async fn garbage_collect(self, _: tarpc::context::Context) -> Result<(), String> { | ||||
|         cmd::vm::prune_instances(&self.ctx).map_err(|e| e.to_string())?; | ||||
|         Ok(()) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[derive(Debug)] | ||||
| pub struct GroupError(String); | ||||
| 
 | ||||
| impl std::fmt::Display for GroupError { | ||||
|     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||||
|         write!(f, "Group {} does not exist", &self.0) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl std::error::Error for GroupError {} | ||||
| 
 | ||||
| pub async fn serve(ctx: Context, zones: ZoneData) -> Result<(), Box<dyn std::error::Error>> { | ||||
|     use std::os::unix::fs::PermissionsExt; | ||||
| 
 | ||||
|     if ctx.config.rpc.socket_path.exists() { | ||||
|         std::fs::remove_file(&ctx.config.rpc.socket_path)?; | ||||
|     } | ||||
|     let listener = UnixListener::bind(&ctx.config.rpc.socket_path)?; | ||||
| 
 | ||||
|     // setup permissions so admins can actually connect to it
 | ||||
|     std::fs::set_permissions( | ||||
|         &ctx.config.rpc.socket_path, | ||||
|         std::fs::Permissions::from_mode(0o770), | ||||
|     )?; | ||||
|     if let Some(group) = &ctx.config.rpc.admin_group { | ||||
|         let group = nix::unistd::Group::from_name(group)?.ok_or(GroupError(group.clone()))?; | ||||
|         nix::unistd::chown(&ctx.config.rpc.socket_path, None, Some(group.gid))?; | ||||
|     } | ||||
| 
 | ||||
|     let codec_builder = LengthDelimitedCodec::builder(); | ||||
|     loop { | ||||
|         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; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										126
									
								
								nzrd/src/schema.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										126
									
								
								nzrd/src/schema.rs
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,126 @@ | |||
| // @generated automatically by Diesel CLI.
 | ||||
| 
 | ||||
| diesel::table! { | ||||
|     comments (id) { | ||||
|         id -> Int4, | ||||
|         domain_id -> Int4, | ||||
|         name -> Varchar, | ||||
|         #[sql_name = "type"] | ||||
|         type_ -> Varchar, | ||||
|         modified_at -> Int4, | ||||
|         account -> Nullable<Varchar>, | ||||
|         comment -> Varchar, | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| diesel::table! { | ||||
|     cryptokeys (id) { | ||||
|         id -> Int4, | ||||
|         domain_id -> Nullable<Int4>, | ||||
|         flags -> Int4, | ||||
|         active -> Nullable<Bool>, | ||||
|         published -> Nullable<Bool>, | ||||
|         content -> Nullable<Text>, | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| diesel::table! { | ||||
|     domainmetadata (id) { | ||||
|         id -> Int4, | ||||
|         domain_id -> Nullable<Int4>, | ||||
|         kind -> Nullable<Varchar>, | ||||
|         content -> Nullable<Text>, | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| diesel::table! { | ||||
|     domains (id) { | ||||
|         id -> Int4, | ||||
|         name -> Varchar, | ||||
|         master -> Nullable<Varchar>, | ||||
|         last_check -> Nullable<Int4>, | ||||
|         #[sql_name = "type"] | ||||
|         type_ -> Text, | ||||
|         notified_serial -> Nullable<Int8>, | ||||
|         account -> Nullable<Varchar>, | ||||
|         options -> Nullable<Text>, | ||||
|         catalog -> Nullable<Text>, | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| diesel::table! { | ||||
|     instances (id) { | ||||
|         id -> Int4, | ||||
|         name -> Varchar, | ||||
|         uuid -> Uuid, | ||||
|         arec_id -> Int8, | ||||
|         ptr_id -> Int8, | ||||
|         lease -> Int8, | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| diesel::table! { | ||||
|     leases (id) { | ||||
|         id -> Int8, | ||||
|         subnet_id -> Int4, | ||||
|         host -> Int4, | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| diesel::table! { | ||||
|     records (id) { | ||||
|         id -> Int8, | ||||
|         domain_id -> Nullable<Int4>, | ||||
|         name -> Nullable<Varchar>, | ||||
|         #[sql_name = "type"] | ||||
|         type_ -> Nullable<Varchar>, | ||||
|         content -> Nullable<Varchar>, | ||||
|         ttl -> Nullable<Int4>, | ||||
|         prio -> Nullable<Int4>, | ||||
|         disabled -> Nullable<Bool>, | ||||
|         ordername -> Nullable<Varchar>, | ||||
|         auth -> Nullable<Bool>, | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| diesel::table! { | ||||
|     subnets (id) { | ||||
|         id -> Int4, | ||||
|         ifname -> Varchar, | ||||
|         network -> Cidr, | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| diesel::table! { | ||||
|     supermasters (ip, nameserver) { | ||||
|         ip -> Inet, | ||||
|         nameserver -> Varchar, | ||||
|         account -> Varchar, | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| diesel::table! { | ||||
|     tsigkeys (id) { | ||||
|         id -> Int4, | ||||
|         name -> Nullable<Varchar>, | ||||
|         algorithm -> Nullable<Varchar>, | ||||
|         secret -> Nullable<Varchar>, | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| diesel::joinable!(cryptokeys -> domains (domain_id)); | ||||
| diesel::joinable!(domainmetadata -> domains (domain_id)); | ||||
| diesel::joinable!(leases -> subnets (subnet_id)); | ||||
| 
 | ||||
| diesel::allow_tables_to_appear_in_same_query!( | ||||
|     comments, | ||||
|     cryptokeys, | ||||
|     domainmetadata, | ||||
|     domains, | ||||
|     instances, | ||||
|     leases, | ||||
|     records, | ||||
|     subnets, | ||||
|     supermasters, | ||||
|     tsigkeys, | ||||
| ); | ||||
							
								
								
									
										54
									
								
								nzrd/src/test.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								nzrd/src/test.rs
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,54 @@ | |||
| use std::{net::Ipv4Addr, str::FromStr}; | ||||
| 
 | ||||
| use crate::cloud::*; | ||||
| use nzr_api::net::{cidr::CidrV4, mac::MacAddr}; | ||||
| 
 | ||||
| #[test] | ||||
| fn cloud_metadata() { | ||||
|     let expected = r#" | ||||
| instance-id: my-instance | ||||
| local-hostname: my-instance | ||||
| public-keys: | ||||
| - ssh-key 123456 admin@laptop | ||||
| "#
 | ||||
|     .trim_start(); | ||||
|     let pubkeys = vec!["ssh-key 123456 admin@laptop".to_owned(), "".to_owned()]; | ||||
|     let meta = Metadata::new("my-instance").ssh_pubkeys(&pubkeys); | ||||
| 
 | ||||
|     let meta_xml = serde_yaml::to_string(&meta).unwrap(); | ||||
|     assert_eq!(meta_xml, expected); | ||||
| } | ||||
| 
 | ||||
| #[test] | ||||
| fn cloud_netdata() { | ||||
|     let expected = r#" | ||||
| version: 2 | ||||
| ethernets: | ||||
|   eth0: | ||||
|     match: | ||||
|       macaddress: 02:15:42:0b:ee:01 | ||||
|     addresses: | ||||
|     - 192.0.2.69/24 | ||||
|     gateway4: 192.0.2.1 | ||||
|     dhcp4: false | ||||
|     nameservers: | ||||
|       search: [] | ||||
|       addresses: | ||||
|       - 192.0.2.1 | ||||
| "#
 | ||||
|     .trim_start(); | ||||
|     let mac_addr = MacAddr::new(0x02, 0x15, 0x42, 0x0b, 0xee, 0x01); | ||||
|     let cidr = CidrV4::from_str("192.0.2.69/24").unwrap(); | ||||
|     let gateway = Ipv4Addr::from_str("192.0.2.1").unwrap(); | ||||
| 
 | ||||
|     let dns = vec![gateway]; | ||||
|     let netconfig = NetworkMeta::new().static_nic( | ||||
|         EtherMatch::mac_addr(&mac_addr), | ||||
|         &cidr, | ||||
|         &gateway, | ||||
|         DNSMeta::with_addrs(None, &dns), | ||||
|     ); | ||||
| 
 | ||||
|     let net_xml = serde_yaml::to_string(&netconfig).unwrap(); | ||||
|     assert_eq!(net_xml, expected); | ||||
| } | ||||
							
								
								
									
										229
									
								
								nzrd/src/virt.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										229
									
								
								nzrd/src/virt.rs
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,229 @@ | |||
| use std::io::{prelude::*, BufReader}; | ||||
| use std::{fmt::Display, ops::Deref}; | ||||
| use virt::{storage_pool::StoragePool, storage_vol::StorageVol, stream::Stream}; | ||||
| 
 | ||||
| use crate::{ | ||||
|     ctrl::virtxml::{Pool, SizeInfo, Volume}, | ||||
|     prelude::*, | ||||
| }; | ||||
| 
 | ||||
| use log::*; | ||||
| 
 | ||||
| pub struct VirtVolume { | ||||
|     inner: StorageVol, | ||||
|     pub persist: bool, | ||||
|     pub name: String, | ||||
| } | ||||
| 
 | ||||
| impl VirtVolume { | ||||
|     pub fn create_xml( | ||||
|         pool: &StoragePool, | ||||
|         xmldata: Volume, | ||||
|         flags: u32, | ||||
|     ) -> Result<Self, Box<dyn std::error::Error>> { | ||||
|         let xml = quick_xml::se::to_string(&xmldata)?; | ||||
| 
 | ||||
|         Ok(Self { | ||||
|             inner: StorageVol::create_xml(pool, &xml, flags)?, | ||||
|             persist: false, | ||||
|             name: xmldata.name, | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     pub fn lookup_by_name<P>(pool: P, name: &str) -> Result<Self, virt::error::Error> | ||||
|     where | ||||
|         P: AsRef<StoragePool>, | ||||
|     { | ||||
|         Ok(Self { | ||||
|             inner: StorageVol::lookup_by_name(pool.as_ref(), name)?, | ||||
|             // default to persisting when looking up by name
 | ||||
|             persist: true, | ||||
|             name: name.to_owned(), | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     pub fn clone_vol( | ||||
|         &mut self, | ||||
|         pool: &VirtPool, | ||||
|         vol_name: &str, | ||||
|         size: SizeInfo, | ||||
|     ) -> Result<Self, PoolError> { | ||||
|         debug!("Cloning volume to {} ({})", vol_name, &size); | ||||
| 
 | ||||
|         let src_path = self.get_path().map_err(PoolError::NoPath)?; | ||||
| 
 | ||||
|         let src_fd = std::fs::File::open(src_path).map_err(PoolError::FileError)?; | ||||
| 
 | ||||
|         let newvol = Volume::new(vol_name, pool.xml.vol_type(), size); | ||||
|         let newxml_str = quick_xml::se::to_string(&newvol).map_err(PoolError::SerdeError)?; | ||||
|         debug!("Creating new vol..."); | ||||
|         let cloned = StorageVol::create_xml(pool, &newxml_str, 0).map_err(PoolError::VirtError)?; | ||||
| 
 | ||||
|         match cloned.get_info() { | ||||
|             Ok(info) => { | ||||
|                 if info.capacity != u64::from(size) { | ||||
|                     debug!( | ||||
|                         "libvirt set wrong size {}, trying this again...", | ||||
|                         info.capacity | ||||
|                     ); | ||||
|                     if let Err(er) = cloned.resize(size.into(), 0) { | ||||
|                         if let Err(er) = cloned.delete(0) { | ||||
|                             warn!("Resizing disk failed, and couldn't clean up: {}", er); | ||||
|                         } | ||||
|                         return Err(PoolError::VirtError(er)); | ||||
|                     } | ||||
|                 } else { | ||||
|                     debug!( | ||||
|                         "capacity is correct ({} bytes), allocation = {} bytes", | ||||
|                         info.capacity, info.allocation, | ||||
|                     ); | ||||
|                 } | ||||
|             } | ||||
|             Err(er) => { | ||||
|                 if let Err(er) = cloned.delete(0) { | ||||
|                     warn!("Couldn't clean up destination volume: {}", er); | ||||
|                 } | ||||
|                 return Err(PoolError::VirtError(er)); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         let stream = match Stream::new(&cloned.get_connect().map_err(PoolError::VirtError)?, 0) { | ||||
|             Ok(s) => s, | ||||
|             Err(er) => { | ||||
|                 cloned.delete(0).ok(); | ||||
|                 return Err(PoolError::VirtError(er)); | ||||
|             } | ||||
|         }; | ||||
| 
 | ||||
|         let img_size = src_fd.metadata().unwrap().len(); | ||||
| 
 | ||||
|         if let Err(er) = cloned.upload(&stream, 0, img_size, 0) { | ||||
|             cloned.delete(0).ok(); | ||||
|             return Err(PoolError::CantUpload(er)); | ||||
|         } | ||||
| 
 | ||||
|         let buf_cap: u64 = datasize!(4 MiB).into(); | ||||
| 
 | ||||
|         let mut reader = BufReader::with_capacity(buf_cap as usize, src_fd); | ||||
|         loop { | ||||
|             let read_bytes = { | ||||
|                 // read from the source file...
 | ||||
|                 let data = match reader.fill_buf() { | ||||
|                     Ok(buf) => buf, | ||||
|                     Err(er) => { | ||||
|                         if let Err(er) = stream.abort() { | ||||
|                             warn!("Stream abort failed: {}", er); | ||||
|                         } | ||||
|                         if let Err(er) = cloned.delete(0) { | ||||
|                             warn!("Couldn't delete destination volume: {}", er); | ||||
|                         } | ||||
|                         return Err(PoolError::FileError(er)); | ||||
|                     } | ||||
|                 }; | ||||
| 
 | ||||
|                 if data.is_empty() { | ||||
|                     break; | ||||
|                 } | ||||
| 
 | ||||
|                 // ... and then send upstream
 | ||||
|                 let mut send_idx = 0; | ||||
|                 while send_idx < data.len() { | ||||
|                     match stream.send(&data[send_idx..]) { | ||||
|                         Ok(sz) => { | ||||
|                             send_idx += sz; | ||||
|                         } | ||||
|                         Err(er) => { | ||||
|                             if let Err(er) = stream.abort() { | ||||
|                                 warn!("Stream abort failed: {}", er); | ||||
|                             } | ||||
|                             if let Err(er) = cloned.delete(0) { | ||||
|                                 warn!("Couldn't delete destination volume: {}", er); | ||||
|                             } | ||||
|                             return Err(PoolError::UploadError(er)); | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|                 data.len() | ||||
|             }; | ||||
| 
 | ||||
|             reader.consume(read_bytes); | ||||
|         } | ||||
| 
 | ||||
|         Ok(Self { | ||||
|             inner: cloned, | ||||
|             persist: false, | ||||
|             name: vol_name.to_owned(), | ||||
|         }) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl Deref for VirtVolume { | ||||
|     type Target = StorageVol; | ||||
|     fn deref(&self) -> &Self::Target { | ||||
|         &self.inner | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl Drop for VirtVolume { | ||||
|     fn drop(&mut self) { | ||||
|         if !self.persist { | ||||
|             debug!("Deleting volume {}", &self.name); | ||||
|             self.inner.delete(0).ok(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[derive(Debug)] | ||||
| pub enum PoolError { | ||||
|     VirtError(virt::error::Error), | ||||
|     SerdeError(quick_xml::de::DeError), | ||||
|     NoPath(virt::error::Error), | ||||
|     FileError(std::io::Error), | ||||
|     CantUpload(virt::error::Error), | ||||
|     UploadError(virt::error::Error), | ||||
| } | ||||
| 
 | ||||
| impl Display for PoolError { | ||||
|     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||||
|         match self { | ||||
|             Self::VirtError(er) => er.fmt(f), | ||||
|             Self::SerdeError(er) => er.fmt(f), | ||||
|             Self::NoPath(er) => write!(f, "Couldn't get source image path: {}", er), | ||||
|             Self::FileError(er) => er.fmt(f), | ||||
|             Self::CantUpload(er) => write!(f, "Unable to start upload to image: {}", er), | ||||
|             Self::UploadError(er) => write!(f, "Failed to upload image: {}", er), | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl std::error::Error for PoolError {} | ||||
| 
 | ||||
| pub struct VirtPool { | ||||
|     inner: StoragePool, | ||||
|     pub xml: Pool, | ||||
| } | ||||
| 
 | ||||
| impl Deref for VirtPool { | ||||
|     type Target = StoragePool; | ||||
|     fn deref(&self) -> &Self::Target { | ||||
|         &self.inner | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl AsRef<StoragePool> for VirtPool { | ||||
|     fn as_ref(&self) -> &StoragePool { | ||||
|         &self.inner | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl VirtPool { | ||||
|     pub fn lookup_by_name(conn: &virt::connect::Connect, id: &str) -> Result<Self, PoolError> { | ||||
|         let inner = StoragePool::lookup_by_name(conn, id).map_err(PoolError::VirtError)?; | ||||
|         if !inner.is_active().map_err(PoolError::VirtError)? { | ||||
|             inner.create(0).map_err(PoolError::VirtError)?; | ||||
|         } | ||||
|         let xml_str = inner.get_xml_desc(0).map_err(PoolError::VirtError)?; | ||||
|         let xml = quick_xml::de::from_str(&xml_str).map_err(PoolError::SerdeError)?; | ||||
|         Ok(Self { inner, xml }) | ||||
|     } | ||||
| } | ||||
		Loading…
	
		Reference in a new issue