458 lines
13 KiB
Rust
458 lines
13 KiB
Rust
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<Binary>,
|
|
}
|
|
}
|
|
|
|
diesel::table! {
|
|
subnets {
|
|
id -> Integer,
|
|
name -> Text,
|
|
ifname -> Text,
|
|
network -> Text,
|
|
start_host -> Integer,
|
|
end_host -> Integer,
|
|
gateway4 -> Nullable<Integer>,
|
|
dns -> Nullable<Text>,
|
|
domain_name -> Nullable<Text>,
|
|
vlan_id -> Nullable<Integer>,
|
|
}
|
|
}
|
|
|
|
#[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<Vec<u8>>,
|
|
}
|
|
|
|
impl Instance {
|
|
/// Gets all instances.
|
|
pub async fn all(ctx: &Context) -> Result<Vec<Self>, ModelError> {
|
|
use self::instances::dsl::instances;
|
|
|
|
let res = ctx
|
|
.spawn_db(move |mut db| {
|
|
instances
|
|
.select(Instance::as_select())
|
|
.load::<Instance>(&mut db)
|
|
})
|
|
.await??;
|
|
|
|
Ok(res)
|
|
}
|
|
|
|
pub async fn get(ctx: &Context, id: i32) -> Result<Option<Self>, ModelError> {
|
|
ctx.spawn_db(move |mut db| {
|
|
self::instances::table
|
|
.find(id)
|
|
.load::<Instance>(&mut db)
|
|
.map(|m| m.into_iter().next())
|
|
})
|
|
.await?
|
|
.map_err(ModelError::Db)
|
|
}
|
|
|
|
pub async fn all_in_subnet(ctx: &Context, net: &Subnet) -> Result<Vec<Self>, 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<String>,
|
|
) -> Result<Option<Self>, ModelError> {
|
|
use self::instances::dsl::{instances, name};
|
|
|
|
let inst_name = inst_name.into();
|
|
|
|
let res: Vec<Instance> = ctx
|
|
.spawn_db(move |mut db| {
|
|
instances
|
|
.filter(name.eq(inst_name))
|
|
.select(Instance::as_select())
|
|
.load::<Instance>(&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<Option<Self>, 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::<Instance>(&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<Option<Self>, 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<Instance>| 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<str>,
|
|
subnet: &Subnet,
|
|
lease: nzr_api::model::Lease,
|
|
ci_user: Option<Vec<u8>>,
|
|
) -> Result<Self, ModelError> {
|
|
// 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::<Instance>(&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<nzr_api::model::Instance, Box<dyn std::error::Error>> {
|
|
let netid = self.subnet_id;
|
|
let Some(subnet) = ctx
|
|
.spawn_db(move |mut db| Subnet::table().find(netid).load::<Subnet>(&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<i32>,
|
|
pub dns: Option<String>,
|
|
pub domain_name: Option<String>,
|
|
pub vlan_id: Option<i32>,
|
|
}
|
|
|
|
impl Subnet {
|
|
/// Gets all subnets.
|
|
pub async fn all(ctx: &Context) -> Result<Vec<Self>, ModelError> {
|
|
use self::subnets::dsl::subnets;
|
|
|
|
let res = ctx
|
|
.spawn_db(move |mut db| subnets.select(Subnet::as_select()).load::<Subnet>(&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<String>,
|
|
) -> Result<Option<Self>, ModelError> {
|
|
use self::subnets::dsl::{name, subnets};
|
|
|
|
let net_name = net_name.into();
|
|
|
|
let res: Vec<Subnet> = ctx
|
|
.spawn_db(move |mut db| {
|
|
subnets
|
|
.filter(name.eq(net_name))
|
|
.select(Subnet::as_select())
|
|
.load::<Subnet>(&mut db)
|
|
})
|
|
.await??;
|
|
|
|
Ok(res.into_iter().next())
|
|
}
|
|
|
|
/// Creates a new subnet model.
|
|
pub async fn insert(
|
|
ctx: &Context,
|
|
net_name: impl Into<String>,
|
|
data: SubnetData,
|
|
) -> Result<Self, ModelError> {
|
|
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::<Vec<String>>()
|
|
.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::<Subnet>(&mut db)
|
|
})
|
|
.await??;
|
|
Ok(ent)
|
|
}
|
|
|
|
/// Generates an [nzr_api::model::Subnet].
|
|
pub fn api_model(&self) -> Result<nzr_api::model::Subnet, ModelError> {
|
|
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<Ipv4Addr, cidr::Error> {
|
|
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<Ipv4Addr, cidr::Error> {
|
|
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<Option<Ipv4Addr>, 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
|
|
}
|
|
}
|