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.
This commit is contained in:
snow flurry 2024-08-12 00:19:24 -07:00
parent 51e72fed93
commit e4df2e5075
8 changed files with 604 additions and 15 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(),
},
}
}

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