nzrd: don't store ci-metadata

This will be handled entirely in omyacid.
This commit is contained in:
snow flurry 2024-08-11 23:48:34 -07:00
parent 3d0ea1f2ef
commit da51722c54
11 changed files with 118 additions and 223 deletions

View file

@ -1,3 +1,5 @@
use std::net::Ipv4Addr;
use model::{CreateStatus, Instance, Subnet}; use model::{CreateStatus, Instance, Subnet};
pub mod args; pub mod args;
@ -6,6 +8,15 @@ pub mod model;
pub mod net; pub mod net;
pub use hickory_proto; 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] #[tarpc::service]
pub trait Nazrin { pub trait Nazrin {
@ -18,6 +29,8 @@ pub trait Nazrin {
/// 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 single instance by the given InstanceQuery.
async fn find_instance(query: InstanceQuery) -> Result<Option<Instance>, String>;
/// Gets a list of existing instances. /// Gets a list of existing instances.
async fn get_instances(with_status: bool) -> 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.
@ -34,4 +47,20 @@ pub trait Nazrin {
async fn get_subnets() -> Result<Vec<Subnet>, String>; async fn get_subnets() -> Result<Vec<Subnet>, String>;
/// Deletes an existing subnet. /// Deletes an existing subnet.
async fn delete_subnet(interface: String) -> Result<(), String>; 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<Vec<u8>, 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::<u32>()
.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;

View file

@ -0,0 +1 @@
ALTER TABLE instances ADD COLUMN ci_metadata TEXT NOT NULL;

View file

@ -0,0 +1 @@
ALTER TABLE instances DROP COLUMN ci_metadata;

View file

@ -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<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(),
}
}
}

View file

@ -6,7 +6,6 @@ use nzr_virt::{datasize, dom, vol};
use tokio::sync::RwLock; use tokio::sync::RwLock;
use super::*; use super::*;
use crate::cloud::Metadata;
use crate::ctrl::vm::Progress; use crate::ctrl::vm::Progress;
use crate::ctx::Context; use crate::ctx::Context;
use crate::model::{Instance, Subnet}; use crate::model::{Instance, Subnet};
@ -86,14 +85,7 @@ pub async fn new_instance(
}; };
// generate cloud-init data // generate cloud-init data
let ci_meta = { let db_inst = Instance::insert(&ctx, &args.name, &subnet, lease.clone(), None).await?;
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?;
progress!(prog_task, 30.0, "Creating instance images..."); progress!(prog_task, 30.0, "Creating instance images...");
// create primary volume from base image // create primary volume from base image

View file

@ -12,6 +12,10 @@ use crate::dns::ZoneData;
use nzr_api::config::Config; use nzr_api::config::Config;
use std::sync::Arc; use std::sync::Arc;
#[cfg(test)]
pub(crate) const MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations");
#[cfg(not(test))]
const MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations"); const MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations");
pub struct PoolRefs { pub struct PoolRefs {

View file

@ -1,19 +1,16 @@
mod cloud;
mod cmd; mod cmd;
mod ctrl; mod ctrl;
mod ctx; mod ctx;
mod dns; mod dns;
mod model; mod model;
mod rpc; mod rpc;
#[cfg(test)]
mod test;
use hickory_server::ServerFuture; use hickory_server::ServerFuture;
use log::LevelFilter; use log::LevelFilter;
use log::*; use log::*;
use model::{Instance, Subnet}; use model::{Instance, Subnet};
use nzr_api::config; use nzr_api::config;
use std::str::FromStr; use std::{net::IpAddr, str::FromStr};
use tokio::net::UdpSocket; use tokio::net::UdpSocket;
#[tokio::main(flavor = "multi_thread")] #[tokio::main(flavor = "multi_thread")]
@ -62,7 +59,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
// DNS init // DNS init
let mut dns_listener = ServerFuture::new(ctx.zones.catalog()); 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); dns_listener.register_socket(dns_socket);
tokio::select! { tokio::select! {

View file

@ -1,5 +1,7 @@
use std::{net::Ipv4Addr, str::FromStr}; use std::{net::Ipv4Addr, str::FromStr};
#[cfg(test)]
mod test;
pub mod tx; pub mod tx;
use diesel::{associations::HasTable, prelude::*}; use diesel::{associations::HasTable, prelude::*};
@ -33,7 +35,6 @@ diesel::table! {
mac_addr -> Text, mac_addr -> Text,
subnet_id -> Integer, subnet_id -> Integer,
host_num -> Integer, host_num -> Integer,
ci_metadata -> Text,
ci_userdata -> Nullable<Binary>, ci_userdata -> Nullable<Binary>,
} }
} }
@ -71,7 +72,6 @@ pub struct Instance {
pub mac_addr: MacAddr, pub mac_addr: MacAddr,
pub subnet_id: i32, pub subnet_id: i32,
pub host_num: i32, pub host_num: i32,
pub ci_metadata: String,
pub ci_userdata: Option<Vec<u8>>, pub ci_userdata: Option<Vec<u8>>,
} }
@ -91,6 +91,17 @@ impl Instance {
Ok(res) 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> { pub async fn all_in_subnet(ctx: &Context, net: &Subnet) -> Result<Vec<Self>, ModelError> {
let subnet = net.clone(); let subnet = net.clone();
@ -122,6 +133,19 @@ impl Instance {
Ok(res.into_iter().next()) 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. /// 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> { pub async fn get_by_ip4(ctx: &Context, ip_addr: Ipv4Addr) -> Result<Option<Self>, ModelError> {
use self::instances::dsl::host_num; use self::instances::dsl::host_num;
@ -157,7 +181,6 @@ impl Instance {
name: impl AsRef<str>, name: impl AsRef<str>,
subnet: &Subnet, subnet: &Subnet,
lease: nzr_api::model::Lease, lease: nzr_api::model::Lease,
ci_meta: impl Into<String>,
ci_user: Option<Vec<u8>>, ci_user: Option<Vec<u8>>,
) -> Result<Self, ModelError> { ) -> Result<Self, ModelError> {
// Get highest host addr + 1 for our addr // Get highest host addr + 1 for our addr
@ -169,7 +192,6 @@ impl Instance {
let wanted_name = name.as_ref().to_owned(); let wanted_name = name.as_ref().to_owned();
let netid = subnet.id; let netid = subnet.id;
let ci_meta = ci_meta.into();
if addr_num > subnet.end_host { if addr_num > subnet.end_host {
Err(cidr::Error::HostBitsTooLarge)?; Err(cidr::Error::HostBitsTooLarge)?;
@ -184,7 +206,6 @@ impl Instance {
mac_addr.eq(lease.mac_addr), mac_addr.eq(lease.mac_addr),
subnet_id.eq(netid), subnet_id.eq(netid),
host_num.eq(addr_num), host_num.eq(addr_num),
ci_metadata.eq(ci_meta),
ci_userdata.eq(ci_user), ci_userdata.eq(ci_user),
); );

14
nzrd/src/model/test.rs Normal file
View file

@ -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();
}

View file

@ -1,5 +1,5 @@
use futures::{future, StreamExt}; use futures::{future, StreamExt};
use nzr_api::{args, model, Nazrin}; use nzr_api::{args, model, InstanceQuery, Nazrin};
use std::sync::Arc; 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;
@ -114,6 +114,27 @@ impl Nazrin for NzrServer {
Ok(()) Ok(())
} }
async fn find_instance(
self,
_: tarpc::context::Context,
query: nzr_api::InstanceQuery,
) -> Result<Option<model::Instance>, 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( async fn get_instances(
self, self,
_: tarpc::context::Context, _: tarpc::context::Context,
@ -216,6 +237,21 @@ impl Nazrin for NzrServer {
.map_err(|e| e.to_string())?; .map_err(|e| e.to_string())?;
Ok(()) Ok(())
} }
async fn get_instance_userdata(
self,
_: tarpc::context::Context,
id: i32,
) -> Result<Vec<u8>, 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)] #[derive(Debug)]

View file

@ -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);
}