use std::{net::Ipv4Addr, str::FromStr}; #[cfg(test)] mod test; pub mod tx; use diesel::{associations::HasTable, prelude::*}; use hickory_proto::rr::Name; use nzr_api::{ model::SubnetData, net::{ cidr::{self, CidrV4}, mac::MacAddr, }, }; use thiserror::Error; use crate::ctx::Context; use tx::Transactable; #[derive(Debug, Error)] pub enum ModelError { #[error("Database error occured: {0}")] Db(#[from] diesel::result::Error), #[error("Unable to get database handle: {0}")] Pool(#[from] diesel::r2d2::PoolError), #[error("{0}")] Cidr(#[from] cidr::Error), } diesel::table! { instances { id -> Integer, name -> Text, mac_addr -> Text, subnet_id -> Integer, host_num -> Integer, ci_userdata -> Nullable, } } diesel::table! { subnets { id -> Integer, name -> Text, ifname -> Text, network -> Text, start_host -> Integer, end_host -> Integer, gateway4 -> Nullable, dns -> Nullable, domain_name -> Nullable, vlan_id -> Nullable, } } #[derive( AsChangeset, Clone, Insertable, Identifiable, Selectable, Queryable, Associations, PartialEq, Debug, )] #[diesel(table_name = instances, treat_none_as_default_value = false, belongs_to(Subnet))] pub struct Instance { pub id: i32, pub name: String, pub mac_addr: MacAddr, pub subnet_id: i32, pub host_num: i32, pub ci_userdata: Option>, } impl Instance { /// Gets all instances. pub async fn all(ctx: &Context) -> Result, ModelError> { use self::instances::dsl::instances; let res = ctx .spawn_db(move |mut db| { instances .select(Instance::as_select()) .load::(&mut db) }) .await??; 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(); let res = ctx .spawn_db(move |mut db| Instance::belonging_to(&subnet).load(&mut db)) .await??; Ok(res) } /// Gets an instance by its name. pub async fn get_by_name( ctx: &Context, inst_name: impl Into, ) -> Result, ModelError> { use self::instances::dsl::{instances, name}; let inst_name = inst_name.into(); let res: Vec = ctx .spawn_db(move |mut db| { instances .filter(name.eq(inst_name)) .select(Instance::as_select()) .load::(&mut db) }) .await??; 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; let Some(net) = Subnet::all(ctx) .await? .into_iter() .find(|net| net.network.contains(&ip_addr)) else { todo!("IP address not found"); }; let num = net.network.host_bits(&ip_addr) as i32; let Some(inst) = ctx .spawn_db(move |mut db| { Instance::belonging_to(&net) .filter(host_num.eq(num)) .load(&mut db) .map(|inst: Vec| inst.into_iter().next()) }) .await?? else { return Ok(None); }; Ok(Some(inst)) } /// Creates a new instance model. pub async fn insert( ctx: &Context, name: impl AsRef, subnet: &Subnet, lease: nzr_api::model::Lease, ci_user: Option>, ) -> Result { // Get highest host addr + 1 for our addr let addr_num = Self::all_in_subnet(ctx, subnet) .await? .into_iter() .max_by(|a, b| a.host_num.cmp(&b.host_num)) .map_or(subnet.start_host, |i| i.host_num + 1); let wanted_name = name.as_ref().to_owned(); let netid = subnet.id; if addr_num > subnet.end_host { Err(cidr::Error::HostBitsTooLarge)?; } let ent = ctx .spawn_db(move |mut db| { use self::instances::dsl::*; let values = ( name.eq(wanted_name), mac_addr.eq(lease.mac_addr), subnet_id.eq(netid), host_num.eq(addr_num), ci_userdata.eq(ci_user), ); diesel::insert_into(instances) .values(values) .returning(instances::all_columns()) .get_result::(&mut db) }) .await??; Ok(ent) } /// Updates the instance model. pub async fn update(&mut self, ctx: &Context) -> Result<(), ModelError> { let self_2 = self.clone(); ctx.spawn_db(move |mut db| diesel::update(&self_2).set(&self_2).execute(&mut db)) .await??; Ok(()) } /// Deletes the instance model from the database. pub async fn delete(self, ctx: &Context) -> Result<(), ModelError> { ctx.spawn_db(move |mut db| diesel::delete(&self).execute(&mut db)) .await??; Ok(()) } /// Creates an [nzr_api::model::Instance] from the information available in /// the database. pub async fn api_model( &self, ctx: &Context, ) -> Result> { let netid = self.subnet_id; let Some(subnet) = ctx .spawn_db(move |mut db| Subnet::table().find(netid).load::(&mut db)) .await?? .into_iter() .next() else { todo!("something went horribly wrong"); }; Ok(nzr_api::model::Instance { name: self.name.clone(), id: self.id, lease: nzr_api::model::Lease { subnet: subnet.name.clone(), addr: CidrV4::new( subnet.network.make_ip(self.host_num as u32)?, subnet.network.cidr(), ), mac_addr: self.mac_addr, }, state: Default::default(), }) } } impl Transactable for Instance { type Error = ModelError; async fn undo_tx(self, ctx: &Context) -> Result<(), Self::Error> { self.delete(ctx).await } } // // // #[derive(AsChangeset, Clone, Insertable, Identifiable, Selectable, Queryable, PartialEq, Debug)] pub struct Subnet { pub id: i32, pub name: String, pub ifname: String, pub network: CidrV4, pub start_host: i32, pub end_host: i32, pub gateway4: Option, pub dns: Option, pub domain_name: Option, pub vlan_id: Option, } impl Subnet { /// Gets all subnets. pub async fn all(ctx: &Context) -> Result, ModelError> { use self::subnets::dsl::subnets; let res = ctx .spawn_db(move |mut db| subnets.select(Subnet::as_select()).load::(&mut db)) .await??; Ok(res) } /// Gets a list of DNS servers used by the subnet. pub fn dns_servers(&self) -> Vec<&str> { if let Some(ref dns) = self.dns { dns.split(',').collect() } else { Vec::new() } } /// Gets a subnet model by its name. pub async fn get_by_name( ctx: &Context, net_name: impl Into, ) -> Result, ModelError> { use self::subnets::dsl::{name, subnets}; let net_name = net_name.into(); let res: Vec = ctx .spawn_db(move |mut db| { subnets .filter(name.eq(net_name)) .select(Subnet::as_select()) .load::(&mut db) }) .await??; Ok(res.into_iter().next()) } /// Creates a new subnet model. pub async fn insert( ctx: &Context, net_name: impl Into, data: SubnetData, ) -> Result { let net_name = net_name.into(); let ent = ctx .spawn_db(move |mut db| { use self::subnets::columns::*; let values = ( name.eq(net_name), ifname.eq(&data.ifname), network.eq(data.network.network()), start_host.eq(data.start_bytes() as i32), end_host.eq(data.end_bytes() as i32), gateway4.eq(data.gateway4.map(|g| data.network.host_bits(&g) as i32)), dns.eq(data .dns .iter() .map(|ip| ip.to_string()) .collect::>() .join(",")), domain_name.eq(data.domain_name.map(|n| n.to_utf8())), vlan_id.eq(data.vlan_id.map(|v| v as i32)), ); diesel::insert_into(Subnet::table()) .values(values) .returning(self::subnets::all_columns) .get_result::(&mut db) }) .await??; Ok(ent) } /// Generates an [nzr_api::model::Subnet]. pub fn api_model(&self) -> Result { Ok(nzr_api::model::Subnet { name: self.name.clone(), data: SubnetData { ifname: self.ifname.clone(), network: self.network, start_host: self.start_ip()?, end_host: self.end_ip()?, gateway4: self.gateway_ip()?, dns: self .dns_servers() .into_iter() .filter_map(|s| match Ipv4Addr::from_str(s) { // Instead of erroring when we get an unparseable DNS // server, report it as an error and continue. This // hopefully will avoid cases where a malformed DNS // entry makes its way into the DB and wreaks havoc on // the API. Ok(addr) => Some(addr), Err(err) => { log::error!( "Error parsing DNS server '{}' for {}: {}", s, &self.name, err ); None } }) .collect(), domain_name: self.domain_name.as_ref().map(|s| { Name::from_str(s).unwrap_or_else(|e| { log::error!("Error parsing DNS name for {}: {}", &self.name, e); Name::default() }) }), vlan_id: self.vlan_id.map(|v| v as u32), }, }) } /// Deletes the subnet model from the database. pub async fn delete(self, ctx: &Context) -> Result<(), ModelError> { ctx.spawn_db(move |mut db| diesel::delete(&self).execute(&mut db)) .await??; Ok(()) } /// Gets the first IPv4 address usable by hosts. pub fn start_ip(&self) -> Result { match self.start_host { host if !host.is_negative() => self.network.make_ip(host as u32), _ => Err(cidr::Error::Malformed), } } /// Gets the last IPv4 address usable by hosts. pub fn end_ip(&self) -> Result { match self.end_host { host if !host.is_negative() => self.network.make_ip(host as u32), _ => Err(cidr::Error::Malformed), } } /// Gets the default gateway IPv4 address, if defined. pub fn gateway_ip(&self) -> Result, cidr::Error> { match self.gateway4 { Some(host) if !host.is_negative() => self.network.make_ip(host as u32).map(Some), Some(_) => Err(cidr::Error::Malformed), None => Ok(None), } } } impl Transactable for Subnet { type Error = ModelError; async fn undo_tx(self, ctx: &Context) -> Result<(), Self::Error> { self.delete(ctx).await } }