Compare commits

..

5 commits

Author SHA1 Message Date
snow flurry e4df2e5075 omyacid: Old Mouse Yells At Cloud-init Daemon
HTTP daemon that interfaces with nzrd to get cloud-init metadata to
instances. The current iteration is completely untested.
2024-08-12 00:19:24 -07:00
snow flurry 51e72fed93 nzrdhcp: don't depend on tarpc
No longer needed, with nzr-api exposing the client bits.
2024-08-12 00:17:53 -07:00
snow flurry da51722c54 nzrd: don't store ci-metadata
This will be handled entirely in omyacid.
2024-08-11 23:48:34 -07:00
snow flurry 3d0ea1f2ef nzrdhcp: make it actually work
* Check the DHCP options for the requested IPv4 address
* Update yiaddr, not siaddr or ciaddr
* Read the RFC a tenth time. I think I've got it now
2024-08-10 18:20:53 -07:00
snow flurry 9ca1b0c821 Set up migrations properly 2024-08-10 01:33:17 -07:00
24 changed files with 819 additions and 303 deletions

347
Cargo.lock generated
View file

@ -108,6 +108,50 @@ version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
[[package]]
name = "askama"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b79091df18a97caea757e28cd2d5fda49c6cd4bd01ddffd7ff01ace0c0ad2c28"
dependencies = [
"askama_derive",
"askama_escape",
"humansize",
"num-traits",
"percent-encoding",
]
[[package]]
name = "askama_derive"
version = "0.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19fe8d6cb13c4714962c072ea496f3392015f0989b1a2847bb4b2d9effd71d83"
dependencies = [
"askama_parser",
"basic-toml",
"mime",
"mime_guess",
"proc-macro2",
"quote",
"serde",
"syn 2.0.72",
]
[[package]]
name = "askama_escape"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341"
[[package]]
name = "askama_parser"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acb1161c6b64d1c3d83108213c2a2533a342ac225aabd0bda218278c2ddb00c0"
dependencies = [
"nom",
]
[[package]]
name = "async-lock"
version = "3.4.0"
@ -121,13 +165,13 @@ dependencies = [
[[package]]
name = "async-trait"
version = "0.1.60"
version = "0.1.81"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d1d8ab452a3936018a687b20e6f7cf5363d713b732b8884001317b0e48aa3"
checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.106",
"syn 2.0.72",
]
[[package]]
@ -154,6 +198,61 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "axum"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a6c9af12842a67734c9a2e355436e5d03b22383ed60cf13cd0c18fbfe3dcbcf"
dependencies = [
"async-trait",
"axum-core",
"bytes",
"futures-util",
"http",
"http-body",
"http-body-util",
"hyper",
"hyper-util",
"itoa",
"matchit",
"memchr",
"mime",
"percent-encoding",
"pin-project-lite",
"rustversion",
"serde",
"serde_json",
"serde_path_to_error",
"serde_urlencoded",
"sync_wrapper 1.0.1",
"tokio",
"tower",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "axum-core"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a15c63fd72d41492dc4f497196f5da1fb04fb7529e631d73630d1b491e47a2e3"
dependencies = [
"async-trait",
"bytes",
"futures-util",
"http",
"http-body",
"http-body-util",
"mime",
"pin-project-lite",
"rustversion",
"sync_wrapper 0.1.2",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "backtrace"
version = "0.3.73"
@ -187,6 +286,15 @@ version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
[[package]]
name = "basic-toml"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "823388e228f614e9558c6804262db37960ec8821856535f5c3f59913140558f8"
dependencies = [
"serde",
]
[[package]]
name = "bincode"
version = "1.3.3"
@ -548,6 +656,16 @@ dependencies = [
"zeroize",
]
[[package]]
name = "deranged"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
dependencies = [
"powerfmt",
"serde",
]
[[package]]
name = "dhcproto"
version = "0.12.0"
@ -1145,12 +1263,101 @@ dependencies = [
"windows",
]
[[package]]
name = "http"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258"
dependencies = [
"bytes",
"fnv",
"itoa",
]
[[package]]
name = "http-body"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
dependencies = [
"bytes",
"http",
]
[[package]]
name = "http-body-util"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f"
dependencies = [
"bytes",
"futures-util",
"http",
"http-body",
"pin-project-lite",
]
[[package]]
name = "httparse"
version = "1.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9"
[[package]]
name = "httpdate"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]]
name = "humansize"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7"
dependencies = [
"libm",
]
[[package]]
name = "humantime"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]]
name = "hyper"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05"
dependencies = [
"bytes",
"futures-channel",
"futures-util",
"http",
"http-body",
"httparse",
"httpdate",
"itoa",
"pin-project-lite",
"smallvec",
"tokio",
]
[[package]]
name = "hyper-util"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cde7055719c54e36e95e8719f95883f22072a48ede39db7fc17a4e1d5281e9b9"
dependencies = [
"bytes",
"futures-util",
"http",
"http-body",
"hyper",
"pin-project-lite",
"tokio",
]
[[package]]
name = "iana-time-zone"
version = "0.1.53"
@ -1366,6 +1573,12 @@ version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5"
[[package]]
name = "matchit"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
[[package]]
name = "md-5"
version = "0.10.6"
@ -1403,6 +1616,22 @@ dependencies = [
"quote",
]
[[package]]
name = "mime"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "mime_guess"
version = "2.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
dependencies = [
"mime",
"unicase",
]
[[package]]
name = "minimal-lexical"
version = "0.2.1"
@ -1514,6 +1743,12 @@ dependencies = [
"zeroize",
]
[[package]]
name = "num-conv"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
[[package]]
name = "num-integer"
version = "0.1.45"
@ -1646,7 +1881,6 @@ dependencies = [
"moka",
"nzr-api",
"serde",
"tarpc",
"tokio",
"tracing",
"tracing-subscriber",
@ -1661,6 +1895,20 @@ dependencies = [
"memchr",
]
[[package]]
name = "omyacid"
version = "0.1.0"
dependencies = [
"anyhow",
"askama",
"axum",
"moka",
"nzr-api",
"tokio",
"tracing",
"tracing-subscriber",
]
[[package]]
name = "once_cell"
version = "1.19.0"
@ -1865,6 +2113,12 @@ version = "0.3.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160"
[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]]
name = "ppv-lite86"
version = "0.2.17"
@ -2098,6 +2352,12 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "rustversion"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6"
[[package]]
name = "ryu"
version = "1.0.12"
@ -2162,6 +2422,16 @@ dependencies = [
"serde",
]
[[package]]
name = "serde_path_to_error"
version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6"
dependencies = [
"itoa",
"serde",
]
[[package]]
name = "serde_spanned"
version = "0.6.7"
@ -2285,9 +2555,9 @@ dependencies = [
[[package]]
name = "smallvec"
version = "1.10.0"
version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
dependencies = [
"serde",
]
@ -2591,6 +2861,18 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "sync_wrapper"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
[[package]]
name = "sync_wrapper"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394"
[[package]]
name = "syslog"
version = "7.0.0"
@ -2720,13 +3002,16 @@ dependencies = [
[[package]]
name = "time"
version = "0.3.17"
version = "0.3.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a561bf4617eebd33bca6434b988f39ed798e527f51a1e797d0ee4f61c0a38376"
checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885"
dependencies = [
"deranged",
"itoa",
"libc",
"num-conv",
"num_threads",
"powerfmt",
"serde",
"time-core",
"time-macros",
@ -2734,16 +3019,17 @@ dependencies = [
[[package]]
name = "time-core"
version = "0.1.0"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd"
checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
[[package]]
name = "time-macros"
version = "0.2.6"
version = "0.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d967f99f534ca7e495c575c62638eebc2898a8c84c119b89e250477bc4ba16b2"
checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf"
dependencies = [
"num-conv",
"time-core",
]
@ -2877,6 +3163,34 @@ dependencies = [
"winnow",
]
[[package]]
name = "tower"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"
dependencies = [
"futures-core",
"futures-util",
"pin-project",
"pin-project-lite",
"tokio",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "tower-layer"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0"
[[package]]
name = "tower-service"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52"
[[package]]
name = "tracing"
version = "0.1.40"
@ -3005,6 +3319,15 @@ dependencies = [
"version_check",
]
[[package]]
name = "unicase"
version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89"
dependencies = [
"version_check",
]
[[package]]
name = "unicode-bidi"
version = "0.3.15"

View file

@ -1,3 +1,3 @@
[workspace]
members = ["nzrd", "nzr-api", "client", "nzrdhcp", "nzr-virt"]
members = ["nzrd", "nzr-api", "client", "nzrdhcp", "nzr-virt", "omyacid"]
resolver = "2"

View file

@ -32,6 +32,7 @@ pub struct SOAConfig {
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct DNSConfig {
pub listen_addr: String,
pub port: u16,
pub default_zone: Name,
pub soa: SOAConfig,
}
@ -40,6 +41,15 @@ pub struct DNSConfig {
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct DHCPConfig {
pub listen_addr: String,
pub port: u16,
}
/// Cloud-init configuration, used by omyacid.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct CloudConfig {
pub listen_addr: String,
pub port: u16,
pub admin_user: String,
}
/// Server<->Client RPC configuration.
@ -62,6 +72,7 @@ pub struct Config {
pub storage: StorageConfig,
pub dns: DNSConfig,
pub dhcp: DHCPConfig,
pub cloud: CloudConfig,
}
impl Default for Config {
@ -84,8 +95,9 @@ impl Default for Config {
base_image_pool: "images".to_owned(),
},
dns: DNSConfig {
listen_addr: "127.0.0.1:5353".to_owned(),
default_zone: Name::from_utf8("servers.locaddral").unwrap(),
listen_addr: "127.0.0.1".to_owned(),
port: 5353,
default_zone: Name::from_utf8("servers.local").unwrap(),
soa: SOAConfig {
nzr_domain: Name::from_utf8("nzr.local").unwrap(),
contact: Name::from_utf8("admin.nzr.local").unwrap(),
@ -96,6 +108,12 @@ impl Default for Config {
},
dhcp: DHCPConfig {
listen_addr: "127.0.0.1".to_owned(),
port: 67,
},
cloud: CloudConfig {
listen_addr: "0.0.0.0".to_owned(),
port: 80,
admin_user: "admin".to_owned(),
},
}
}

View file

@ -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<Option<Instance>, String>;
/// Gets a list of existing instances.
async fn get_instances(with_status: bool) -> Result<Vec<Instance>, String>;
/// Cleans up unusable entries in the database.
@ -34,4 +47,20 @@ pub trait Nazrin {
async fn get_subnets() -> Result<Vec<Subnet>, 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<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,2 @@
DROP TABLE instances;
DROP TABLE subnets;

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 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

View file

@ -3,6 +3,7 @@ use diesel::{
SqliteConnection,
};
use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness};
use log::trace;
use nzr_virt::{vol, Connection};
use std::ops::Deref;
use thiserror::Error;
@ -11,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 {
@ -64,6 +69,7 @@ impl InnerCtx {
baseimg: conn.get_pool(&config.storage.base_image_pool).await?,
};
trace!("Connecting to database");
let db_uri = config.db_uri.clone();
let sqldb = tokio::task::spawn_blocking(|| {
let manager = ConnectionManager::<SqliteConnection>::new(db_uri);
@ -74,6 +80,7 @@ impl InnerCtx {
.unwrap()?;
{
trace!("Running pending migrations");
let mut conn = sqldb.get()?;
tokio::task::spawn_blocking(move || {
conn.run_pending_migrations(MIGRATIONS)

View file

@ -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<dyn std::error::Error>> {
// 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! {

View file

@ -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<Binary>,
}
}
@ -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<Vec<u8>>,
}
@ -91,6 +91,17 @@ impl Instance {
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();
@ -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<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;
@ -157,7 +181,6 @@ impl Instance {
name: impl AsRef<str>,
subnet: &Subnet,
lease: nzr_api::model::Lease,
ci_meta: impl Into<String>,
ci_user: Option<Vec<u8>>,
) -> Result<Self, ModelError> {
// 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),
);

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 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<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(
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<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)]

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

View file

@ -11,11 +11,5 @@ tokio = { version = "1.39.2", features = ["rt-multi-thread", "net", "macros"] }
nzr-api = { path = "../nzr-api" }
tracing = { version = "0.1.40", features = ["log"] }
tracing-subscriber = "0.3.18"
tarpc = { version = "0.34", features = [
"tokio1",
"unix",
"serde-transport",
"serde-transport-bincode",
] }
moka = { version = "0.12.8", features = ["future"] }
anyhow = "1.0.86"

View file

@ -1,4 +1,5 @@
use std::hash::RandomState;
use std::net::IpAddr;
use std::net::SocketAddr;
use anyhow::Context as _;
@ -10,7 +11,6 @@ use nzr_api::{
net::mac::MacAddr,
NazrinClient,
};
use tarpc::{tokio_serde::formats::Bincode, tokio_util::codec::LengthDelimitedCodec};
use tokio::net::UdpSocket;
use tokio::net::UnixStream;
@ -26,7 +26,7 @@ impl Context {
async fn hydrate_hosts(&self) -> Result<()> {
let instances = self
.api_client
.get_instances(tarpc::context::current(), false)
.get_instances(nzr_api::default_ctx(), false)
.await?
.map_err(|e| anyhow::anyhow!("nzrd error: {e}"))?;
@ -52,7 +52,7 @@ impl Context {
async fn hydrate_nets(&self) -> Result<()> {
let subnets = self
.api_client
.get_subnets(tarpc::context::current())
.get_subnets(nzr_api::default_ctx())
.await?
.map_err(|e| anyhow::anyhow!("nzrd error: {e}"))?;
@ -68,21 +68,19 @@ impl Context {
let sock = UnixStream::connect(&cfg.rpc.socket_path)
.await
.context("Connection to nzrd failed")?;
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();
nzr_api::new_client(sock)
};
let listen_addr: SocketAddr = cfg
.dhcp
.listen_addr
.parse()
.context("Malformed listen address")?;
let listen_addr: SocketAddr = {
let ip: IpAddr = cfg
.dhcp
.listen_addr
.parse()
.context("Malformed listen address")?;
(ip, cfg.dhcp.port).into()
};
let server_sock = UdpSocket::bind(&listen_addr)
let server_sock = UdpSocket::bind(listen_addr)
.await
.context("Unable to listen")?;

View file

@ -5,7 +5,7 @@ use std::{net::Ipv4Addr, process::ExitCode};
use ctx::Context;
use dhcproto::{
v4::{DhcpOption, Message, MessageType, Opcode, OptionCode},
Decodable, Decoder,
Decodable, Decoder, Encodable, Encoder,
};
use nzr_api::{config::Config, net::mac::MacAddr};
use std::net::SocketAddr;
@ -16,9 +16,9 @@ const DEFAULT_LEASE: u32 = 86400;
fn make_reply(msg: &Message, msg_type: MessageType, lease_addr: Option<Ipv4Addr>) -> Message {
let mut resp = Message::new(
EMPTY_V4,
EMPTY_V4,
lease_addr.unwrap_or(EMPTY_V4),
EMPTY_V4,
msg.giaddr(),
msg.chaddr(),
);
@ -46,6 +46,7 @@ async fn handle_message(ctx: &Context, from: SocketAddr, msg: &Message) {
tracing::info!("Received DHCP payload with invalid addr (different media type?)");
return;
};
tracing::debug!("Client MAC is {client_mac}!!!");
let instance = match ctx.instance_by_mac(client_mac).await {
Ok(Some(i)) => i,
@ -59,6 +60,11 @@ async fn handle_message(ctx: &Context, from: SocketAddr, msg: &Message) {
}
};
tracing::info!(
"Recieved {msg_type:?} from {client_mac} (assuming {})",
&instance.name
);
let mut lease_time = None;
let mut nak = false;
@ -68,12 +74,18 @@ async fn handle_message(ctx: &Context, from: SocketAddr, msg: &Message) {
make_reply(msg, MessageType::Offer, Some(instance.lease.addr.addr))
}
MessageType::Request => {
if msg.ciaddr() != instance.lease.addr.addr {
if let Some(DhcpOption::RequestedIpAddress(addr)) =
msg.opts().get(OptionCode::RequestedIpAddress)
{
if *addr == instance.lease.addr.addr {
make_reply(msg, MessageType::Ack, Some(instance.lease.addr.addr))
} else {
nak = true;
make_reply(msg, MessageType::Nak, None)
}
} else {
nak = true;
make_reply(msg, MessageType::Nak, None)
} else {
lease_time = Some(DEFAULT_LEASE);
make_reply(msg, MessageType::Ack, Some(instance.lease.addr.addr))
}
}
MessageType::Decline => {
@ -86,57 +98,80 @@ async fn handle_message(ctx: &Context, from: SocketAddr, msg: &Message) {
}
MessageType::Release => {
// We only provide static leases
tracing::trace!("Ignoring DHCPRELEASE");
tracing::debug!("Ignoring DHCPRELEASE");
return;
}
MessageType::Inform => make_reply(msg, MessageType::Ack, None),
other => {
tracing::trace!("Received unhandled message {other:?}");
tracing::info!("Received unhandled message {other:?}");
return;
}
};
let opts = response.opts_mut();
let giaddr = if msg.giaddr().is_unspecified() {
todo!("no relay??")
} else {
msg.giaddr()
};
opts.insert(DhcpOption::ServerIdentifier(giaddr));
if let Some(time) = lease_time {
opts.insert(DhcpOption::AddressLeaseTime(time));
}
if !nak {
// Get general networking info
let subnet = match ctx.get_subnet(&instance.lease.subnet).await {
Ok(Some(net)) => net,
Ok(None) => {
tracing::error!("nzrd says '{}' isn't a subnet", &instance.lease.subnet);
return;
}
Err(err) => {
tracing::error!("Error getting subnet: {err}");
return;
}
{
let opts = response.opts_mut();
let giaddr = if msg.giaddr().is_unspecified() {
todo!("no relay??")
} else {
msg.giaddr()
};
opts.insert(DhcpOption::Hostname(instance.name.clone()));
if !subnet.dns.is_empty() {
opts.insert(DhcpOption::DomainNameServer(subnet.dns));
opts.insert(DhcpOption::ServerIdentifier(giaddr));
if let Some(time) = lease_time {
opts.insert(DhcpOption::AddressLeaseTime(time));
}
if let Some(name) = subnet.domain_name {
opts.insert(DhcpOption::DomainName(name.to_utf8()));
}
if !nak {
// Get general networking info
let subnet = match ctx.get_subnet(&instance.lease.subnet).await {
Ok(Some(net)) => net,
Ok(None) => {
tracing::error!("nzrd says '{}' isn't a subnet", &instance.lease.subnet);
return;
}
Err(err) => {
tracing::error!("Error getting subnet: {err}");
return;
}
};
if let Some(gw) = subnet.gateway4 {
opts.insert(DhcpOption::Router(Vec::from(&[gw])));
}
opts.insert(DhcpOption::Hostname(instance.name.clone()));
opts.insert(DhcpOption::SubnetMask(instance.lease.addr.netmask()));
if !subnet.dns.is_empty() {
opts.insert(DhcpOption::DomainNameServer(subnet.dns));
}
if let Some(name) = subnet.domain_name {
opts.insert(DhcpOption::DomainName(name.to_utf8()));
}
if let Some(gw) = subnet.gateway4 {
opts.insert(DhcpOption::Router(Vec::from(&[gw])));
}
opts.insert(DhcpOption::SubnetMask(instance.lease.addr.netmask()));
}
}
tracing::info!(
"Sending message {:?} with yiaddr {}",
response
.opts()
.get(OptionCode::MessageType)
.unwrap_or(&DhcpOption::End),
response.yiaddr()
);
// unicast it back
let mut resp_buf = Vec::new();
let mut enc = Encoder::new(&mut resp_buf);
if let Err(err) = response.encode(&mut enc) {
tracing::error!("Couldn't encode response: {err}");
return;
}
if let Err(err) = ctx.sock().send_to(&resp_buf, from).await {
tracing::error!("Couldn't send response: {err}");
}
}

14
omyacid/Cargo.toml Normal file
View file

@ -0,0 +1,14 @@
[package]
name = "omyacid"
version = "0.1.0"
edition = "2021"
[dependencies]
nzr-api = { path = "../nzr-api" }
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
axum = "0.7"
tracing = "0.1"
tracing-subscriber = "0.3"
anyhow = "1"
askama = "0.12"
moka = { version = "0.12.8", features = ["future"] }

95
omyacid/src/ctx.rs Normal file
View file

@ -0,0 +1,95 @@
use std::hash::RandomState;
use std::net::Ipv4Addr;
use std::sync::Arc;
use std::time::Duration;
use anyhow::Context as _;
use anyhow::Result;
use moka::future::Cache;
use nzr_api::config::Config;
use nzr_api::model::Instance;
use nzr_api::InstanceQuery;
use nzr_api::NazrinClient;
use tokio::net::UnixStream;
#[derive(Clone)]
struct InstanceMeta {
pub inst: Instance,
pub userdata: Vec<u8>,
}
#[derive(Clone)]
pub struct Context {
api_client: NazrinClient,
config: Arc<Config>,
host_cache: Cache<Ipv4Addr, InstanceMeta, RandomState>,
}
impl Context {
pub async fn new(cfg: Config) -> Result<Self> {
let api_client = {
let sock = UnixStream::connect(&cfg.rpc.socket_path)
.await
.context("Connection to nzrd failed")?;
nzr_api::new_client(sock)
};
let host_cache = Cache::builder()
.time_to_live(Duration::from_secs(15))
.max_capacity(5)
.build();
Ok(Self {
api_client,
host_cache,
config: Arc::new(cfg),
})
}
// Internal function to hydrate the instance metadata, if needed
async fn get_instmeta(&self, addr: Ipv4Addr) -> Result<Option<InstanceMeta>> {
if let Some(meta) = self.host_cache.get(&addr).await {
tracing::debug!("Cache hit!");
Ok(Some(meta))
} else {
let inst = self
.api_client
.find_instance(nzr_api::default_ctx(), InstanceQuery::Ipv4Addr(addr))
.await
.context("RPC error")?
.map_err(|e| anyhow::anyhow!("nzrd error: {e}"))?;
if let Some(inst) = inst {
let userdata = self
.api_client
.get_instance_userdata(nzr_api::default_ctx(), inst.id)
.await
.context("RPC error")?
.map_err(|e| anyhow::anyhow!("nzrd error: {e}"))?;
let meta = InstanceMeta { inst, userdata };
self.host_cache.insert(addr, meta.clone()).await;
Ok(Some(meta))
} else {
Ok(None)
}
}
}
pub async fn get_instance(&self, addr: Ipv4Addr) -> Result<Option<Instance>> {
self.get_instmeta(addr)
.await
.map(|opt| opt.map(|im| im.inst))
}
pub async fn get_inst_userdata(&self, addr: Ipv4Addr) -> Result<Option<Vec<u8>>> {
self.get_instmeta(addr)
.await
.map(|opt| opt.map(|im| im.userdata))
}
pub fn cfg(&self) -> &Config {
&self.config
}
}

120
omyacid/src/main.rs Normal file
View file

@ -0,0 +1,120 @@
mod ctx;
mod model;
use std::{
net::{IpAddr, SocketAddr},
process::ExitCode,
str::FromStr,
};
use askama::Template;
use axum::{
extract::{ConnectInfo, State},
http::StatusCode,
routing::get,
Router,
};
use model::Metadata;
use nzr_api::config::Config;
async fn get_meta_data(
State(ctx): State<ctx::Context>,
ConnectInfo(addr): ConnectInfo<SocketAddr>,
) -> Result<String, StatusCode> {
if let IpAddr::V4(ip) = addr.ip() {
match ctx.get_instance(ip).await {
Ok(Some(inst)) => {
let meta = Metadata {
inst_name: &inst.name,
ssh_pubkeys: Vec::new(), // TODO
username: Some(ctx.cfg().cloud.admin_user.as_ref()),
};
meta.render().map_err(|e| {
tracing::error!("Renderer error: {e}");
StatusCode::INTERNAL_SERVER_ERROR
})
}
Ok(None) => {
tracing::warn!("Request from unregistered server {ip}");
Err(StatusCode::FORBIDDEN)
}
Err(err) => {
tracing::warn!("{err}");
Err(StatusCode::INTERNAL_SERVER_ERROR)
}
}
} else {
Err(StatusCode::BAD_REQUEST)
}
}
async fn get_user_data(
State(ctx): State<ctx::Context>,
ConnectInfo(addr): ConnectInfo<SocketAddr>,
) -> Result<Vec<u8>, StatusCode> {
if let IpAddr::V4(ip) = addr.ip() {
match ctx.get_inst_userdata(ip).await {
Ok(Some(data)) => Ok(data),
Ok(None) => {
tracing::warn!("Request from unregistered server {ip}");
Err(StatusCode::FORBIDDEN)
}
Err(err) => {
tracing::warn!("{err}");
Err(StatusCode::INTERNAL_SERVER_ERROR)
}
}
} else {
Err(StatusCode::BAD_REQUEST)
}
}
#[tokio::main]
async fn main() -> ExitCode {
tracing_subscriber::fmt().init();
let cfg: Config = match Config::figment().extract() {
Ok(cfg) => cfg,
Err(err) => {
tracing::error!("Unable to get configuration: {err}");
return ExitCode::FAILURE;
}
};
let http_sock = {
let addr = match IpAddr::from_str(&cfg.cloud.listen_addr) {
Ok(addr) => addr,
Err(err) => {
tracing::error!("Invalid listen IP address ({err})");
return ExitCode::FAILURE;
}
};
match tokio::net::TcpListener::bind((addr, cfg.cloud.port)).await {
Ok(sock) => sock,
Err(err) => {
tracing::error!("Failed to bind to {addr}:{}: {err}", cfg.cloud.port);
return ExitCode::FAILURE;
}
}
};
let ctx = match ctx::Context::new(cfg).await {
Ok(ctx) => ctx,
Err(err) => {
tracing::error!("{err}");
return ExitCode::FAILURE;
}
};
let app = Router::new()
.route("/meta-data", get(get_meta_data))
.route("/user-data", get(get_user_data))
.with_state(ctx);
if let Err(err) = axum::serve(http_sock, app).await {
tracing::error!("axum error: {err}");
return ExitCode::FAILURE;
}
ExitCode::SUCCESS
}

8
omyacid/src/model.rs Normal file
View file

@ -0,0 +1,8 @@
use askama::Template;
#[derive(Template)]
#[template(path = "meta-data.yml")]
pub struct Metadata<'a> {
pub inst_name: &'a str,
pub ssh_pubkeys: Vec<&'a String>,
pub username: Option<&'a str>,
}

View file

@ -0,0 +1,11 @@
instance_id: "iid-{{ inst_name }}"
local_hostname: "{{ inst_name }}"
{% if !ssh_pubkeys.is_empty() -%}
public_keys:
{% for key in ssh_pubkeys -%}
- "{{ key }}"
{% endfor %}
{% endif -%}
{% if let Some(user) = username -%}
default_username: "{{ user }}"
{%- endif %}