more stuff

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

18
Cargo.lock generated
View file

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

View file

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

View file

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

View file

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

View file

@ -1,5 +1,5 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{fmt, net::Ipv4Addr, str::FromStr}; use std::{fmt, net::Ipv4Addr};
use trust_dns_proto::rr::Name; use trust_dns_proto::rr::Name;
use crate::net::{cidr::CidrV4, mac::MacAddr}; use crate::net::{cidr::CidrV4, mac::MacAddr};
@ -60,6 +60,13 @@ impl fmt::Display for DomainState {
} }
} }
#[derive(Debug, Serialize, Deserialize)]
pub struct CreateStatus {
pub status_text: String,
pub completion: f32,
pub result: Option<Result<Instance, String>>,
}
/// Struct representing a VM instance. /// Struct representing a VM instance.
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct Instance { pub struct Instance {
@ -72,6 +79,8 @@ pub struct Instance {
/// Struct representing a logical "lease" held by a VM. /// Struct representing a logical "lease" held by a VM.
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct Lease { pub struct Lease {
/// Subnet name corresponding to the lease
pub subnet: String,
/// The IPv4 address held by the Lease /// The IPv4 address held by the Lease
pub addr: CidrV4, pub addr: CidrV4,
/// The MAC address associated by the Lease /// The MAC address associated by the Lease
@ -80,15 +89,17 @@ pub struct Lease {
/// Struct representing a subnet used by the host for virtual /// Struct representing a subnet used by the host for virtual
/// networking. /// networking.
#[derive(Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Subnet { pub struct Subnet {
/// The name of the interface the subnet is accessible via. /// The subnet short name.
pub ifname: IfaceStr, pub name: String,
pub data: SubnetData, pub data: SubnetData,
} }
#[derive(Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SubnetData { pub struct SubnetData {
/// The name of the interface the subnet is accessible via.
pub ifname: String,
/// The network information for the subnet. /// The network information for the subnet.
pub network: CidrV4, pub network: CidrV4,
/// The first host address that can be assigned dynamically /// The first host address that can be assigned dynamically
@ -98,65 +109,21 @@ pub struct SubnetData {
/// on the subnet. /// on the subnet.
pub end_host: Ipv4Addr, pub end_host: Ipv4Addr,
/// The default gateway for the subnet. /// The default gateway for the subnet.
pub gateway4: Option<Ipv4Addr>, pub gateway4: Ipv4Addr,
/// The primary DNS server for the subnet. /// The primary DNS server for the subnet.
pub dns: Vec<Ipv4Addr>, pub dns: Vec<Ipv4Addr>,
/// The base domain used for DNS lookup. /// The base domain used for DNS lookup.
pub domain_name: Option<Name>, pub domain_name: Option<Name>,
/// The VLAN ID used for the domain. If none, no VLAN is set.
pub vlan_id: Option<u32>,
} }
/// A wrapper struct for [u8; 16], representing the maximum length impl SubnetData {
/// for an interface's name. pub fn start_bytes(&self) -> u32 {
#[derive(Debug, Serialize, Deserialize)] self.network.host_bits(&self.start_host)
pub struct IfaceStr { }
name: [u8; 16],
}
impl AsRef<[u8]> for IfaceStr { pub fn end_bytes(&self) -> u32 {
fn as_ref(&self) -> &[u8] { self.network.host_bits(&self.end_host)
&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)?
)
} }
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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