diff --git a/nzr-api/src/lib.rs b/nzr-api/src/lib.rs index 7ff1a10..285fe96 100644 --- a/nzr-api/src/lib.rs +++ b/nzr-api/src/lib.rs @@ -1,3 +1,5 @@ +use std::net::Ipv4Addr; + use model::{CreateStatus, Instance, Subnet}; pub mod args; @@ -6,6 +8,15 @@ pub mod model; pub mod net; pub use hickory_proto; +use net::mac::MacAddr; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize)] +pub enum InstanceQuery { + Name(String), + MacAddr(MacAddr), + Ipv4Addr(Ipv4Addr), +} #[tarpc::service] pub trait Nazrin { @@ -18,6 +29,8 @@ pub trait Nazrin { /// 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 single instance by the given InstanceQuery. + async fn find_instance(query: InstanceQuery) -> Result, String>; /// Gets a list of existing instances. async fn get_instances(with_status: bool) -> Result, String>; /// Cleans up unusable entries in the database. @@ -34,4 +47,20 @@ pub trait Nazrin { async fn get_subnets() -> Result, String>; /// Deletes an existing subnet. async fn delete_subnet(interface: String) -> Result<(), String>; + // Gets the cloud-init user-data for the given instance. + async fn get_instance_userdata(id: i32) -> Result, String>; } + +/// Create a new NazrinClient. +pub fn new_client(sock: tokio::net::UnixStream) -> NazrinClient { + use tarpc::tokio_serde::formats::Bincode; + use tarpc::tokio_util::codec::LengthDelimitedCodec; + + let framed_io = LengthDelimitedCodec::builder() + .length_field_type::() + .new_framed(sock); + let transport = tarpc::serde_transport::new(framed_io, Bincode::default()); + NazrinClient::new(Default::default(), transport).spawn() +} + +pub use tarpc::context::current as default_ctx; diff --git a/nzrd/migrations/2024081101_no_cimeta/down.sql b/nzrd/migrations/2024081101_no_cimeta/down.sql new file mode 100644 index 0000000..f79ad1f --- /dev/null +++ b/nzrd/migrations/2024081101_no_cimeta/down.sql @@ -0,0 +1 @@ +ALTER TABLE instances ADD COLUMN ci_metadata TEXT NOT NULL; \ No newline at end of file diff --git a/nzrd/migrations/2024081101_no_cimeta/up.sql b/nzrd/migrations/2024081101_no_cimeta/up.sql new file mode 100644 index 0000000..17af58f --- /dev/null +++ b/nzrd/migrations/2024081101_no_cimeta/up.sql @@ -0,0 +1 @@ +ALTER TABLE instances DROP COLUMN ci_metadata; \ No newline at end of file diff --git a/nzrd/src/cloud.rs b/nzrd/src/cloud.rs deleted file mode 100644 index e0dc299..0000000 --- a/nzrd/src/cloud.rs +++ /dev/null @@ -1,149 +0,0 @@ -use std::net::Ipv4Addr; - -use hickory_server::proto::rr::Name; -use serde::Serialize; -use serde_with::skip_serializing_none; -use std::collections::HashMap; - -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>, -} - -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>, - #[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>, -} - -#[derive(Debug, Serialize)] -pub struct EtherNic<'a> { - r#match: EtherMatch<'a>, - addresses: Option>, - gateway4: Option<&'a Ipv4Addr>, - dhcp4: bool, - nameservers: Option>, -} - -#[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, - addresses: &'a Vec, -} - -impl<'a> DNSMeta<'a> { - pub fn with_addrs(search: Option>, addrs: &'a Vec) -> Self { - Self { - addresses: addrs, - search: search.unwrap_or_default(), - } - } -} diff --git a/nzrd/src/cmd/vm.rs b/nzrd/src/cmd/vm.rs index 07f3789..736800a 100644 --- a/nzrd/src/cmd/vm.rs +++ b/nzrd/src/cmd/vm.rs @@ -6,7 +6,6 @@ use nzr_virt::{datasize, dom, vol}; use tokio::sync::RwLock; use super::*; -use crate::cloud::Metadata; use crate::ctrl::vm::Progress; use crate::ctx::Context; use crate::model::{Instance, Subnet}; @@ -86,14 +85,7 @@ pub async fn new_instance( }; // generate cloud-init data - let ci_meta = { - let m = Metadata::new(&args.name).ssh_pubkeys(&args.ssh_keys); - serde_yaml::to_string(&m) - .map_err(|err| cmd_error!("Couldn't generate cloud-init metadata: {err}")) - }?; - - let db_inst = - Instance::insert(&ctx, &args.name, &subnet, lease.clone(), ci_meta, None).await?; + let db_inst = Instance::insert(&ctx, &args.name, &subnet, lease.clone(), None).await?; progress!(prog_task, 30.0, "Creating instance images..."); // create primary volume from base image diff --git a/nzrd/src/ctx.rs b/nzrd/src/ctx.rs index 59f9e24..e8c77cb 100644 --- a/nzrd/src/ctx.rs +++ b/nzrd/src/ctx.rs @@ -12,6 +12,10 @@ use crate::dns::ZoneData; use nzr_api::config::Config; use std::sync::Arc; +#[cfg(test)] +pub(crate) const MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations"); + +#[cfg(not(test))] const MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations"); pub struct PoolRefs { diff --git a/nzrd/src/main.rs b/nzrd/src/main.rs index 6a5cef7..9482f41 100644 --- a/nzrd/src/main.rs +++ b/nzrd/src/main.rs @@ -1,19 +1,16 @@ -mod cloud; mod cmd; mod ctrl; mod ctx; mod dns; mod model; mod rpc; -#[cfg(test)] -mod test; use hickory_server::ServerFuture; use log::LevelFilter; use log::*; use model::{Instance, Subnet}; use nzr_api::config; -use std::str::FromStr; +use std::{net::IpAddr, str::FromStr}; use tokio::net::UdpSocket; #[tokio::main(flavor = "multi_thread")] @@ -62,7 +59,10 @@ async fn main() -> Result<(), Box> { // DNS init let mut dns_listener = ServerFuture::new(ctx.zones.catalog()); - let dns_socket = UdpSocket::bind(ctx.config.dns.listen_addr.as_str()).await?; + let dns_socket = { + let dns_ip: IpAddr = ctx.config.dns.listen_addr.parse()?; + UdpSocket::bind((dns_ip, ctx.config.dns.port)).await? + }; dns_listener.register_socket(dns_socket); tokio::select! { diff --git a/nzrd/src/model/mod.rs b/nzrd/src/model/mod.rs index e134da5..042cc73 100644 --- a/nzrd/src/model/mod.rs +++ b/nzrd/src/model/mod.rs @@ -1,5 +1,7 @@ use std::{net::Ipv4Addr, str::FromStr}; +#[cfg(test)] +mod test; pub mod tx; use diesel::{associations::HasTable, prelude::*}; @@ -33,7 +35,6 @@ diesel::table! { mac_addr -> Text, subnet_id -> Integer, host_num -> Integer, - ci_metadata -> Text, ci_userdata -> Nullable, } } @@ -71,7 +72,6 @@ pub struct Instance { pub mac_addr: MacAddr, pub subnet_id: i32, pub host_num: i32, - pub ci_metadata: String, pub ci_userdata: Option>, } @@ -91,6 +91,17 @@ impl Instance { Ok(res) } + pub async fn get(ctx: &Context, id: i32) -> Result, ModelError> { + ctx.spawn_db(move |mut db| { + self::instances::table + .find(id) + .load::(&mut db) + .map(|m| m.into_iter().next()) + }) + .await? + .map_err(ModelError::Db) + } + pub async fn all_in_subnet(ctx: &Context, net: &Subnet) -> Result, ModelError> { let subnet = net.clone(); @@ -122,6 +133,19 @@ impl Instance { Ok(res.into_iter().next()) } + /// Gets an Instance model with the given MAC address. + pub async fn get_by_mac(ctx: &Context, addr: MacAddr) -> Result, ModelError> { + ctx.spawn_db(move |mut db| { + use self::instances::dsl::{instances, mac_addr}; + instances + .filter(mac_addr.eq(addr)) + .select(Instance::as_select()) + .load::(&mut db) + }) + .await? + .map_or_else(|e| Err(ModelError::Db(e)), |m| Ok(m.into_iter().next())) + } + /// Gets an Instance model by the IPv4 address that has been assigned to it. pub async fn get_by_ip4(ctx: &Context, ip_addr: Ipv4Addr) -> Result, ModelError> { use self::instances::dsl::host_num; @@ -157,7 +181,6 @@ impl Instance { name: impl AsRef, subnet: &Subnet, lease: nzr_api::model::Lease, - ci_meta: impl Into, ci_user: Option>, ) -> Result { // Get highest host addr + 1 for our addr @@ -169,7 +192,6 @@ impl Instance { let wanted_name = name.as_ref().to_owned(); let netid = subnet.id; - let ci_meta = ci_meta.into(); if addr_num > subnet.end_host { Err(cidr::Error::HostBitsTooLarge)?; @@ -184,7 +206,6 @@ impl Instance { mac_addr.eq(lease.mac_addr), subnet_id.eq(netid), host_num.eq(addr_num), - ci_metadata.eq(ci_meta), ci_userdata.eq(ci_user), ); diff --git a/nzrd/src/model/test.rs b/nzrd/src/model/test.rs new file mode 100644 index 0000000..f0f1945 --- /dev/null +++ b/nzrd/src/model/test.rs @@ -0,0 +1,14 @@ +use diesel::Connection; +use diesel_migrations::MigrationHarness; + +#[test] +fn migrations() { + let mut sql = diesel::SqliteConnection::establish(":memory:").unwrap(); + let pending = sql.pending_migrations(crate::ctx::MIGRATIONS).unwrap(); + assert!(!pending.is_empty(), "No migrations found"); + for migration in pending { + sql.run_migration(&migration).unwrap(); + } + + sql.revert_all_migrations(crate::ctx::MIGRATIONS).unwrap(); +} diff --git a/nzrd/src/rpc.rs b/nzrd/src/rpc.rs index 417516a..85e00e5 100644 --- a/nzrd/src/rpc.rs +++ b/nzrd/src/rpc.rs @@ -1,5 +1,5 @@ use futures::{future, StreamExt}; -use nzr_api::{args, model, Nazrin}; +use nzr_api::{args, model, InstanceQuery, Nazrin}; use std::sync::Arc; use tarpc::server::{BaseChannel, Channel}; use tarpc::tokio_serde::formats::Bincode; @@ -114,6 +114,27 @@ impl Nazrin for NzrServer { Ok(()) } + async fn find_instance( + self, + _: tarpc::context::Context, + query: nzr_api::InstanceQuery, + ) -> Result, String> { + let res = match query { + InstanceQuery::Name(name) => Instance::get_by_name(&self.ctx, name).await, + InstanceQuery::MacAddr(addr) => Instance::get_by_mac(&self.ctx, addr).await, + InstanceQuery::Ipv4Addr(addr) => Instance::get_by_ip4(&self.ctx, addr).await, + } + .map_err(|e| e.to_string())?; + + if let Some(inst) = res { + inst.api_model(&self.ctx) + .await + .map_or_else(|e| Err(e.to_string()), |m| Ok(Some(m))) + } else { + Ok(None) + } + } + async fn get_instances( self, _: tarpc::context::Context, @@ -216,6 +237,21 @@ impl Nazrin for NzrServer { .map_err(|e| e.to_string())?; Ok(()) } + + async fn get_instance_userdata( + self, + _: tarpc::context::Context, + id: i32, + ) -> Result, String> { + let Some(db_model) = Instance::get(&self.ctx, id) + .await + .map_err(|e| e.to_string())? + else { + return Err("Instance doesn't exist".to_owned()); + }; + + Ok(db_model.ci_userdata.unwrap_or_default()) + } } #[derive(Debug)] diff --git a/nzrd/src/test.rs b/nzrd/src/test.rs deleted file mode 100644 index a92d6df..0000000 --- a/nzrd/src/test.rs +++ /dev/null @@ -1,54 +0,0 @@ -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); -}