Initial commit
This commit is contained in:
commit
4c02261015
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