Compare commits
41 commits
e4df2e5075
...
0cb3aea62e
Author | SHA1 | Date | |
---|---|---|---|
snow flurry | 0cb3aea62e | ||
snow flurry | 811c3d1c72 | ||
snow flurry | 61c47d735a | ||
snow flurry | 926997c1d1 | ||
snow flurry | 8cca433f91 | ||
snow flurry | 66289b7c5b | ||
snow flurry | f0d37da26d | ||
snow flurry | a4c38c7d82 | ||
snow flurry | 8448a93b21 | ||
snow flurry | 4edbe1a46d | ||
snow flurry | deaaaa3d10 | ||
snow flurry | c35d9ccbed | ||
snow flurry | 693156dc3e | ||
snow flurry | 37a1b0f3a0 | ||
snow flurry | b0646410b9 | ||
snow flurry | 24a0c1cc68 | ||
snow flurry | 5040bc7b87 | ||
snow flurry | 7a9659eb9e | ||
snow flurry | ec8528abb5 | ||
snow flurry | 60b39a5045 | ||
snow flurry | 93655b9c42 | ||
snow flurry | 3d58c6c671 | ||
snow flurry | 267b924d7f | ||
snow flurry | 48bff395ca | ||
snow flurry | b350e73b8a | ||
snow flurry | d10d98de96 | ||
snow flurry | 8e9478ebc6 | ||
snow flurry | f5bf777b2e | ||
snow flurry | 29fc84e949 | ||
snow flurry | 04f4d625a6 | ||
snow flurry | a54204a1ee | ||
snow flurry | f9adaddbb5 | ||
snow flurry | c74cc70986 | ||
snow flurry | 660cf2e90d | ||
snow flurry | c5b4292f6a | ||
snow flurry | fff1ba672b | ||
snow flurry | aae15f34f8 | ||
snow flurry | e684b81660 | ||
snow flurry | 9ca4e87eb7 | ||
snow flurry | 957499c0a5 | ||
snow flurry | 997478801c |
722
Cargo.lock
generated
722
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -1,20 +1,14 @@
|
|||
[package]
|
||||
name = "nzr"
|
||||
version = "0.1.0"
|
||||
version = "0.9.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
nzr-api = { path = "../nzr-api" }
|
||||
clap = { version = "4.0.26", features = ["derive"] }
|
||||
home = "0.5.4"
|
||||
tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] }
|
||||
tokio = { version = "1.0", features = ["fs", "macros", "rt-multi-thread"] }
|
||||
tokio-serde = { version = "0.9", features = ["bincode"] }
|
||||
tarpc = { version = "0.34", features = [
|
||||
"tokio1",
|
||||
"unix",
|
||||
"serde-transport",
|
||||
"serde-transport-bincode",
|
||||
] }
|
||||
tabled = "0.15"
|
||||
serde_json = "1"
|
||||
log = "0.4.17"
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
use clap::{CommandFactory, FromArgMatches, Parser, Subcommand};
|
||||
use nzr_api::config;
|
||||
use nzr_api::hickory_proto::rr::Name;
|
||||
use nzr_api::model;
|
||||
use nzr_api::net::cidr::CidrV4;
|
||||
use nzr_api::{config, NazrinClient};
|
||||
use std::any::{Any, TypeId};
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
use tarpc::tokio_serde::formats::Bincode;
|
||||
use tarpc::tokio_util::codec::LengthDelimitedCodec;
|
||||
use tokio::net::UnixStream;
|
||||
|
||||
mod table;
|
||||
|
@ -35,11 +33,11 @@ pub struct NewInstanceArgs {
|
|||
#[arg(short, long, default_value_t = 20)]
|
||||
primary_size: u32,
|
||||
/// Secndary HDD size, in GiB
|
||||
#[arg(short, long)]
|
||||
secondary_size: Option<u32>,
|
||||
/// File containing a list of SSH keys to use
|
||||
#[arg(long)]
|
||||
sshkey_file: Option<PathBuf>,
|
||||
secondary_size: Option<u32>,
|
||||
/// Path to cloud-init userdata, if any
|
||||
#[arg(long)]
|
||||
ci_userdata: Option<PathBuf>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
|
@ -125,6 +123,16 @@ enum NetCmd {
|
|||
Dump { name: Option<String> },
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
enum KeyCmd {
|
||||
/// Add a new SSH key
|
||||
Add { path: PathBuf },
|
||||
/// List SSH keys
|
||||
List,
|
||||
/// Delete an SSH key
|
||||
Delete { id: i32 },
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
enum Commands {
|
||||
/// Commands for managing instances
|
||||
|
@ -137,6 +145,11 @@ enum Commands {
|
|||
#[command(subcommand)]
|
||||
command: NetCmd,
|
||||
},
|
||||
/// Commands for managing SSH public keys
|
||||
SshKey {
|
||||
#[command(subcommand)]
|
||||
command: KeyCmd,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
|
@ -182,19 +195,6 @@ impl From<&str> for CommandError {
|
|||
}
|
||||
}
|
||||
|
||||
impl CommandError {
|
||||
fn new<S, E>(message: S, inner: E) -> Self
|
||||
where
|
||||
S: AsRef<str>,
|
||||
E: std::error::Error + 'static,
|
||||
{
|
||||
Self {
|
||||
message: message.as_ref().to_owned(),
|
||||
inner: Some(Box::new(inner)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_command() -> Result<(), Box<dyn std::error::Error>> {
|
||||
env_logger::init();
|
||||
|
||||
|
@ -202,18 +202,12 @@ async fn handle_command() -> Result<(), Box<dyn std::error::Error>> {
|
|||
let cli = Args::from_arg_matches_mut(&mut matches)?;
|
||||
let config: config::Config = nzr_api::config::Config::figment().extract()?;
|
||||
let conn = UnixStream::connect(&config.rpc.socket_path).await?;
|
||||
let framed_io = LengthDelimitedCodec::builder()
|
||||
.length_field_type::<u32>()
|
||||
.new_framed(conn);
|
||||
let transport = tarpc::serde_transport::new(framed_io, Bincode::default());
|
||||
let client = NazrinClient::new(Default::default(), transport).spawn();
|
||||
let client = nzr_api::new_client(conn);
|
||||
|
||||
match cli.command {
|
||||
Commands::Instance { command } => match command {
|
||||
InstanceCmd::Dump { name, quick } => {
|
||||
let instances = (client
|
||||
.get_instances(tarpc::context::current(), !quick)
|
||||
.await?)?;
|
||||
let instances = (client.get_instances(nzr_api::default_ctx(), !quick).await?)?;
|
||||
if let Some(name) = name {
|
||||
if let Some(inst) = instances.iter().find(|f| f.name == name) {
|
||||
println!("{}", serde_json::to_string(inst)?);
|
||||
|
@ -223,37 +217,20 @@ async fn handle_command() -> Result<(), Box<dyn std::error::Error>> {
|
|||
}
|
||||
}
|
||||
InstanceCmd::New(args) => {
|
||||
let ssh_keys: Vec<String> = {
|
||||
let key_file = args.sshkey_file.map_or_else(
|
||||
|| {
|
||||
home::home_dir().map_or_else(
|
||||
|| {
|
||||
Err(CommandError::from(
|
||||
"SSH keyfile not defined, and couldn't find home directory",
|
||||
))
|
||||
},
|
||||
|hd| Ok(hd.join(".ssh/authorized_keys")),
|
||||
let ci_userdata = {
|
||||
if let Some(path) = &args.ci_userdata {
|
||||
if !path.exists() {
|
||||
return Err("cloud-init userdata file doesn't exist".into());
|
||||
} else {
|
||||
Some(
|
||||
std::fs::read(path)
|
||||
.map_err(|e| format!("Couldn't read userdata file: {e}"))?,
|
||||
)
|
||||
},
|
||||
Ok,
|
||||
)?;
|
||||
|
||||
if !key_file.exists() {
|
||||
Err("SSH keyfile doesn't exist".into())
|
||||
} else {
|
||||
match std::fs::read_to_string(&key_file) {
|
||||
Ok(data) => {
|
||||
let keys: Vec<String> =
|
||||
data.split('\n').map(|s| s.trim().to_owned()).collect();
|
||||
Ok(keys)
|
||||
}
|
||||
Err(err) => Err(CommandError::new(
|
||||
format!("Couldn't read {} for SSH keys", &key_file.display()),
|
||||
err,
|
||||
)),
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}?;
|
||||
};
|
||||
|
||||
let build_args = nzr_api::args::NewInstance {
|
||||
name: args.name,
|
||||
|
@ -264,10 +241,10 @@ async fn handle_command() -> Result<(), Box<dyn std::error::Error>> {
|
|||
cores: args.cores,
|
||||
memory: args.mem,
|
||||
disk_sizes: (args.primary_size, args.secondary_size),
|
||||
ssh_keys,
|
||||
ci_userdata,
|
||||
};
|
||||
let task_id = (client
|
||||
.new_instance(tarpc::context::current(), build_args)
|
||||
.new_instance(nzr_api::default_ctx(), build_args)
|
||||
.await?)?;
|
||||
|
||||
const MAX_RETRIES: i32 = 5;
|
||||
|
@ -275,7 +252,7 @@ async fn handle_command() -> Result<(), Box<dyn std::error::Error>> {
|
|||
let mut current_pct: f32 = 0.0;
|
||||
loop {
|
||||
let status = client
|
||||
.poll_new_instance(tarpc::context::current(), task_id)
|
||||
.poll_new_instance(nzr_api::default_ctx(), task_id)
|
||||
.await;
|
||||
match status {
|
||||
Ok(Some(status)) => {
|
||||
|
@ -284,8 +261,8 @@ async fn handle_command() -> Result<(), Box<dyn std::error::Error>> {
|
|||
Ok(instance) => {
|
||||
println!("Instance {} created!", &instance.name);
|
||||
println!(
|
||||
"You should be able to reach it with: ssh root@{}",
|
||||
instance.lease.addr.addr,
|
||||
"You should be able to reach it with: ssh {}@{}",
|
||||
&config.cloud.admin_user, instance.lease.addr.addr,
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
|
@ -315,21 +292,19 @@ async fn handle_command() -> Result<(), Box<dyn std::error::Error>> {
|
|||
}
|
||||
}
|
||||
InstanceCmd::Delete { name } => {
|
||||
(client
|
||||
.delete_instance(tarpc::context::current(), name)
|
||||
.await?)?;
|
||||
client
|
||||
.delete_instance(nzr_api::default_ctx(), name)
|
||||
.await??;
|
||||
}
|
||||
InstanceCmd::List => {
|
||||
let instances = client
|
||||
.get_instances(tarpc::context::current(), true)
|
||||
.await?;
|
||||
let instances = client.get_instances(nzr_api::default_ctx(), true).await?;
|
||||
|
||||
let tabular: Vec<table::Instance> =
|
||||
instances?.iter().map(table::Instance::from).collect();
|
||||
let mut table = tabled::Table::new(tabular);
|
||||
println!("{}", table.with(tabled::settings::Style::psql()));
|
||||
}
|
||||
InstanceCmd::Prune => (client.garbage_collect(tarpc::context::current()).await?)?,
|
||||
InstanceCmd::Prune => (client.garbage_collect(nzr_api::default_ctx()).await?)?,
|
||||
},
|
||||
Commands::Net { command } => match command {
|
||||
NetCmd::Add(args) => {
|
||||
|
@ -350,12 +325,12 @@ async fn handle_command() -> Result<(), Box<dyn std::error::Error>> {
|
|||
},
|
||||
};
|
||||
(client
|
||||
.new_subnet(tarpc::context::current(), build_args)
|
||||
.new_subnet(nzr_api::default_ctx(), build_args)
|
||||
.await?)?;
|
||||
}
|
||||
NetCmd::Edit(args) => {
|
||||
let mut net = client
|
||||
.get_subnets(tarpc::context::current())
|
||||
.get_subnets(nzr_api::default_ctx())
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
.and_then(|res| {
|
||||
|
@ -391,7 +366,7 @@ async fn handle_command() -> Result<(), Box<dyn std::error::Error>> {
|
|||
|
||||
// run the update
|
||||
client
|
||||
.modify_subnet(tarpc::context::current(), net)
|
||||
.modify_subnet(nzr_api::default_ctx(), net)
|
||||
.await
|
||||
.map_err(|err| format!("RPC error: {}", err))
|
||||
.and_then(|res| {
|
||||
|
@ -401,7 +376,7 @@ async fn handle_command() -> Result<(), Box<dyn std::error::Error>> {
|
|||
})?;
|
||||
}
|
||||
NetCmd::Dump { name } => {
|
||||
let subnets = (client.get_subnets(tarpc::context::current()).await?)?;
|
||||
let subnets = (client.get_subnets(nzr_api::default_ctx()).await?)?;
|
||||
if let Some(name) = name {
|
||||
if let Some(net) = subnets.iter().find(|s| s.name == name) {
|
||||
println!("{}", serde_json::to_string(net)?);
|
||||
|
@ -411,12 +386,10 @@ async fn handle_command() -> Result<(), Box<dyn std::error::Error>> {
|
|||
}
|
||||
}
|
||||
NetCmd::Delete { name } => {
|
||||
(client
|
||||
.delete_subnet(tarpc::context::current(), name)
|
||||
.await?)?;
|
||||
(client.delete_subnet(nzr_api::default_ctx(), name).await?)?;
|
||||
}
|
||||
NetCmd::List => {
|
||||
let subnets = client.get_subnets(tarpc::context::current()).await?;
|
||||
let subnets = client.get_subnets(nzr_api::default_ctx()).await?;
|
||||
|
||||
let tabular: Vec<table::Subnet> =
|
||||
subnets?.iter().map(table::Subnet::from).collect();
|
||||
|
@ -424,6 +397,30 @@ async fn handle_command() -> Result<(), Box<dyn std::error::Error>> {
|
|||
println!("{}", table.with(tabled::settings::Style::psql()));
|
||||
}
|
||||
},
|
||||
Commands::SshKey { command } => match command {
|
||||
KeyCmd::Add { path } => {
|
||||
if !path.exists() {
|
||||
return Err("Provided path doesn't exist".into());
|
||||
}
|
||||
|
||||
let keyfile = tokio::fs::read_to_string(&path).await?;
|
||||
let res = client
|
||||
.add_ssh_pubkey(nzr_api::default_ctx(), keyfile)
|
||||
.await??;
|
||||
println!("Key #{} added.", res.id.unwrap_or(-1));
|
||||
}
|
||||
KeyCmd::List => {
|
||||
let keys = client.get_ssh_pubkeys(nzr_api::default_ctx()).await??;
|
||||
let tabular = keys.iter().map(table::SshKey::from);
|
||||
let mut table = tabled::Table::new(tabular);
|
||||
println!("{}", table.with(tabled::settings::Style::psql()));
|
||||
}
|
||||
KeyCmd::Delete { id } => {
|
||||
client
|
||||
.delete_ssh_pubkey(nzr_api::default_ctx(), id)
|
||||
.await??;
|
||||
}
|
||||
},
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
@ -431,7 +428,7 @@ async fn handle_command() -> Result<(), Box<dyn std::error::Error>> {
|
|||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
if let Err(err) = handle_command().await {
|
||||
if std::any::Any::type_id(&*err).type_id() == TypeId::of::<tarpc::client::RpcError>() {
|
||||
if std::any::Any::type_id(&*err).type_id() == TypeId::of::<nzr_api::RpcError>() {
|
||||
log::error!("Error communicating with server: {}", err);
|
||||
} else {
|
||||
log::error!("{}", err);
|
||||
|
|
|
@ -40,3 +40,23 @@ impl From<&model::Subnet> for Subnet {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Tabled)]
|
||||
pub struct SshKey {
|
||||
#[tabled(rename = "ID")]
|
||||
id: i32,
|
||||
#[tabled(rename = "Comment")]
|
||||
comment: String,
|
||||
#[tabled(rename = "Key data")]
|
||||
key_data: String,
|
||||
}
|
||||
|
||||
impl From<&model::SshPubkey> for SshKey {
|
||||
fn from(value: &model::SshPubkey) -> Self {
|
||||
Self {
|
||||
id: value.id.unwrap_or(-1),
|
||||
comment: value.comment.clone().unwrap_or_default(),
|
||||
key_data: format!("{} {}", value.algorithm, value.key_data),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,13 +6,25 @@ edition = "2021"
|
|||
[dependencies]
|
||||
figment = { version = "0.10.8", features = ["json", "toml", "env"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
tarpc = { version = "0.34", features = ["tokio1", "unix"] }
|
||||
tarpc = { version = "0.34", features = [
|
||||
"tokio1",
|
||||
"unix",
|
||||
"serde-transport",
|
||||
"serde-transport-bincode",
|
||||
] }
|
||||
tokio = { version = "1.0", features = ["macros"] }
|
||||
uuid = { version = "1.2.2", features = ["serde"] }
|
||||
hickory-proto = { version = "0.24", features = ["serde-config"] }
|
||||
log = "0.4.17"
|
||||
sqlx = "0.8"
|
||||
diesel = { version = "2.2", optional = true }
|
||||
futures = { version = "0.3", optional = true }
|
||||
thiserror = "1"
|
||||
regex = "1"
|
||||
lazy_static = "1"
|
||||
|
||||
[dev-dependencies]
|
||||
uuid = { version = "1.2.2", features = ["serde", "v4"] }
|
||||
|
||||
[features]
|
||||
diesel = ["dep:diesel"]
|
||||
mock = ["dep:futures"]
|
||||
|
|
|
@ -13,7 +13,7 @@ pub struct NewInstance {
|
|||
pub cores: u8,
|
||||
pub memory: u32,
|
||||
pub disk_sizes: (u32, Option<u32>),
|
||||
pub ssh_keys: Vec<String>,
|
||||
pub ci_userdata: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
|
|
|
@ -49,9 +49,24 @@ pub struct DHCPConfig {
|
|||
pub struct CloudConfig {
|
||||
pub listen_addr: String,
|
||||
pub port: u16,
|
||||
pub http_addr: Option<String>,
|
||||
pub admin_user: String,
|
||||
}
|
||||
|
||||
impl CloudConfig {
|
||||
pub fn http_addr(&self) -> String {
|
||||
if let Some(http_addr) = &self.http_addr {
|
||||
if http_addr.ends_with('/') {
|
||||
http_addr.clone()
|
||||
} else {
|
||||
format!("{}/", http_addr)
|
||||
}
|
||||
} else {
|
||||
format!("http://{}:{}/", self.listen_addr, self.port)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Server<->Client RPC configuration.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct RPCConfig {
|
||||
|
@ -113,6 +128,7 @@ impl Default for Config {
|
|||
cloud: CloudConfig {
|
||||
listen_addr: "0.0.0.0".to_owned(),
|
||||
port: 80,
|
||||
http_addr: None,
|
||||
admin_user: "admin".to_owned(),
|
||||
},
|
||||
}
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
use std::net::Ipv4Addr;
|
||||
|
||||
use model::{CreateStatus, Instance, Subnet};
|
||||
use model::{CreateStatus, Instance, SshPubkey, Subnet};
|
||||
|
||||
pub mod args;
|
||||
pub mod config;
|
||||
#[cfg(feature = "mock")]
|
||||
pub mod mock;
|
||||
pub mod model;
|
||||
pub mod net;
|
||||
|
||||
|
@ -47,8 +49,14 @@ 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.
|
||||
/// Gets the cloud-init user-data for the given instance.
|
||||
async fn get_instance_userdata(id: i32) -> Result<Vec<u8>, String>;
|
||||
/// Gets all SSH keys stored in the database.
|
||||
async fn get_ssh_pubkeys() -> Result<Vec<SshPubkey>, String>;
|
||||
/// Adds a new SSH public key to the database.
|
||||
async fn add_ssh_pubkey(pub_key: String) -> Result<SshPubkey, String>;
|
||||
/// Deletes an SSH public key from the database.
|
||||
async fn delete_ssh_pubkey(id: i32) -> Result<(), String>;
|
||||
}
|
||||
|
||||
/// Create a new NazrinClient.
|
||||
|
@ -63,4 +71,5 @@ pub fn new_client(sock: tokio::net::UnixStream) -> NazrinClient {
|
|||
NazrinClient::new(Default::default(), transport).spawn()
|
||||
}
|
||||
|
||||
pub use tarpc::client::RpcError;
|
||||
pub use tarpc::context::current as default_ctx;
|
||||
|
|
70
nzr-api/src/mock/client.rs
Normal file
70
nzr-api/src/mock/client.rs
Normal file
|
@ -0,0 +1,70 @@
|
|||
use std::net::Ipv4Addr;
|
||||
|
||||
use crate::{args, model, net::cidr::CidrV4};
|
||||
|
||||
pub trait NzrClientExt {
|
||||
#[allow(async_fn_in_trait)]
|
||||
async fn new_mock_instance(
|
||||
&mut self,
|
||||
name: impl AsRef<str>,
|
||||
) -> Result<Result<model::Instance, String>, crate::RpcError>;
|
||||
}
|
||||
|
||||
impl NzrClientExt for crate::NazrinClient {
|
||||
async fn new_mock_instance(
|
||||
&mut self,
|
||||
name: impl AsRef<str>,
|
||||
) -> Result<Result<model::Instance, String>, crate::RpcError> {
|
||||
let name = name.as_ref().to_owned();
|
||||
|
||||
let subnet = self
|
||||
.new_subnet(
|
||||
crate::default_ctx(),
|
||||
model::Subnet {
|
||||
name: "mock".to_owned(),
|
||||
data: model::SubnetData {
|
||||
ifname: "eth0".to_string(),
|
||||
network: CidrV4::new(Ipv4Addr::new(192, 0, 2, 0), 24),
|
||||
start_host: Ipv4Addr::new(192, 0, 2, 10),
|
||||
end_host: Ipv4Addr::new(192, 0, 2, 254),
|
||||
gateway4: Some(Ipv4Addr::new(192, 0, 2, 1)),
|
||||
dns: vec![Ipv4Addr::new(192, 0, 2, 5)],
|
||||
domain_name: None,
|
||||
vlan_id: None,
|
||||
},
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.ok();
|
||||
|
||||
let uuid = self
|
||||
.new_instance(
|
||||
crate::default_ctx(),
|
||||
args::NewInstance {
|
||||
name: name.clone(),
|
||||
title: None,
|
||||
description: None,
|
||||
subnet: subnet.map_or_else(|| "mock".to_owned(), |m| m.name),
|
||||
base_image: "linux2".to_owned(),
|
||||
cores: 2,
|
||||
memory: 1024,
|
||||
disk_sizes: (10, None),
|
||||
ci_userdata: None,
|
||||
},
|
||||
)
|
||||
.await?
|
||||
.unwrap();
|
||||
// poll to "complete"
|
||||
self.poll_new_instance(crate::default_ctx(), uuid)
|
||||
.await?
|
||||
.unwrap();
|
||||
|
||||
let inst = self
|
||||
.poll_new_instance(crate::default_ctx(), uuid)
|
||||
.await?
|
||||
.and_then(|cs| cs.result)
|
||||
.unwrap();
|
||||
Ok(inst)
|
||||
}
|
||||
}
|
315
nzr-api/src/mock/mod.rs
Normal file
315
nzr-api/src/mock/mod.rs
Normal file
|
@ -0,0 +1,315 @@
|
|||
pub mod client;
|
||||
#[cfg(test)]
|
||||
mod test;
|
||||
|
||||
use std::{collections::HashMap, str::FromStr, sync::Arc};
|
||||
|
||||
use tarpc::server::{BaseChannel, Channel as _};
|
||||
|
||||
use futures::{future, StreamExt};
|
||||
use tokio::{sync::RwLock, task::JoinHandle};
|
||||
|
||||
use crate::{
|
||||
model,
|
||||
net::{cidr::CidrV4, mac::MacAddr},
|
||||
InstanceQuery, Nazrin, NazrinClient,
|
||||
};
|
||||
|
||||
pub struct MockServerHandle<T>(JoinHandle<T>);
|
||||
|
||||
impl<T> Drop for MockServerHandle<T> {
|
||||
fn drop(&mut self) {
|
||||
self.0.abort();
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<JoinHandle<T>> for MockServerHandle<T> {
|
||||
fn from(value: JoinHandle<T>) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct MockDb {
|
||||
instances: Vec<Option<model::Instance>>,
|
||||
subnets: Vec<Option<model::Subnet>>,
|
||||
subnet_lease: HashMap<i32, u32>,
|
||||
ci_userdatas: HashMap<String, Vec<u8>>,
|
||||
create_tasks: HashMap<uuid::Uuid, (model::Instance, bool)>,
|
||||
ssh_keys: Vec<Option<model::SshPubkey>>,
|
||||
}
|
||||
|
||||
/// Mock Nazrin RPC server for testing, where the full server isn't required.
|
||||
///
|
||||
/// Note that this intentionally does not perform SQL model testing!
|
||||
#[derive(Clone, Default)]
|
||||
pub struct MockServer {
|
||||
db: Arc<RwLock<MockDb>>,
|
||||
}
|
||||
|
||||
impl MockServer {
|
||||
/// Marks a create_task as complete, assuming it exists
|
||||
pub async fn complete_task(&mut self, task_id: uuid::Uuid) {
|
||||
let mut db = self.db.write().await;
|
||||
if let Some((_inst, done)) = db.create_tasks.get_mut(&task_id) {
|
||||
let _ = std::mem::replace(done, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Nazrin for MockServer {
|
||||
async fn new_instance(
|
||||
self,
|
||||
_: tarpc::context::Context,
|
||||
build_args: crate::args::NewInstance,
|
||||
) -> Result<uuid::Uuid, String> {
|
||||
let mut db = self.db.write().await;
|
||||
let Some(net_pos) = db
|
||||
.subnets
|
||||
.iter()
|
||||
.position(|s| s.as_ref().filter(|s| s.name == build_args.subnet).is_some())
|
||||
else {
|
||||
return Err("Subnet doesn't exist".to_owned());
|
||||
};
|
||||
let subnet = db.subnets[net_pos].as_ref().unwrap().clone();
|
||||
let cur_lease = *(db
|
||||
.subnet_lease
|
||||
.get(&(net_pos as i32))
|
||||
.unwrap_or(&(subnet.data.start_bytes() as u32)));
|
||||
let instance = model::Instance {
|
||||
name: build_args.name.clone(),
|
||||
id: -1,
|
||||
lease: model::Lease {
|
||||
subnet: build_args.subnet,
|
||||
addr: CidrV4::new(
|
||||
subnet
|
||||
.data
|
||||
.network
|
||||
.make_ip(cur_lease)
|
||||
.map_err(|e| e.to_string())?,
|
||||
subnet.data.network.cidr(),
|
||||
),
|
||||
mac_addr: MacAddr::new(0x02, 0x04, 0x08, 0x0a, 0x0c, 0x0f),
|
||||
},
|
||||
state: model::DomainState::NoState,
|
||||
};
|
||||
db.ci_userdatas
|
||||
.insert(build_args.name, build_args.ci_userdata.unwrap_or_default());
|
||||
let id = uuid::Uuid::new_v4();
|
||||
db.create_tasks.insert(id, (instance, false));
|
||||
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
async fn poll_new_instance(
|
||||
mut self,
|
||||
_: tarpc::context::Context,
|
||||
task_id: uuid::Uuid,
|
||||
) -> Option<crate::model::CreateStatus> {
|
||||
let db = self.db.read().await;
|
||||
let (inst, done) = db.create_tasks.get(&task_id)?;
|
||||
let done = *done;
|
||||
|
||||
if done {
|
||||
Some(model::CreateStatus {
|
||||
status_text: "Done!".to_owned(),
|
||||
completion: 1.0,
|
||||
result: Some(Ok(inst.clone())),
|
||||
})
|
||||
} else {
|
||||
let mut inst = inst.clone();
|
||||
// Drop the read-only DB to get a write lock
|
||||
std::mem::drop(db);
|
||||
let mut db = self.db.write().await;
|
||||
inst.id = (db.instances.len() + 1) as i32;
|
||||
db.instances.push(Some(inst.clone()));
|
||||
// Drop the writeable DB to avoid deadlock
|
||||
std::mem::drop(db);
|
||||
self.complete_task(task_id).await;
|
||||
Some(model::CreateStatus {
|
||||
status_text: "Working on it...".to_owned(),
|
||||
completion: 0.50,
|
||||
result: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async fn delete_instance(self, _: tarpc::context::Context, name: String) -> Result<(), String> {
|
||||
let mut db = self.db.write().await;
|
||||
let Some(inst) = db
|
||||
.instances
|
||||
.iter_mut()
|
||||
.find(|i| i.as_ref().filter(|i| i.name == name).is_some())
|
||||
.take()
|
||||
else {
|
||||
return Err("Instance doesn't exist".to_owned());
|
||||
};
|
||||
inst.take();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn find_instance(
|
||||
self,
|
||||
_: tarpc::context::Context,
|
||||
query: crate::InstanceQuery,
|
||||
) -> Result<Option<crate::model::Instance>, String> {
|
||||
let db = self.db.read().await;
|
||||
|
||||
let res = {
|
||||
db.instances
|
||||
.iter()
|
||||
.find(|opt| {
|
||||
opt.as_ref()
|
||||
.map(|inst| match &query {
|
||||
InstanceQuery::Ipv4Addr(addr) => &inst.lease.addr.addr == addr,
|
||||
InstanceQuery::MacAddr(addr) => &inst.lease.mac_addr == addr,
|
||||
InstanceQuery::Name(name) => &inst.name == name,
|
||||
})
|
||||
.is_some()
|
||||
})
|
||||
.and_then(|opt| opt.as_ref().cloned())
|
||||
};
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
async fn get_instance_userdata(
|
||||
self,
|
||||
_: tarpc::context::Context,
|
||||
id: i32,
|
||||
) -> Result<Vec<u8>, String> {
|
||||
let db = self.db.read().await;
|
||||
let Some(inst) = db
|
||||
.instances
|
||||
.iter()
|
||||
.find(|i| i.as_ref().map(|i| i.id == id).is_some())
|
||||
.and_then(|o| o.as_ref())
|
||||
else {
|
||||
return Err("No such instance".to_owned());
|
||||
};
|
||||
Ok(db.ci_userdatas.get(&inst.name).cloned().unwrap_or_default())
|
||||
}
|
||||
|
||||
async fn get_instances(
|
||||
self,
|
||||
_: tarpc::context::Context,
|
||||
_with_status: bool,
|
||||
) -> Result<Vec<crate::model::Instance>, String> {
|
||||
let db = self.db.read().await;
|
||||
Ok(db
|
||||
.instances
|
||||
.iter()
|
||||
.filter_map(|inst| inst.clone())
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn new_subnet(
|
||||
self,
|
||||
_: tarpc::context::Context,
|
||||
build_args: crate::model::Subnet,
|
||||
) -> Result<crate::model::Subnet, String> {
|
||||
let mut db = self.db.write().await;
|
||||
let subnet = build_args.clone();
|
||||
db.subnets.push(Some(build_args));
|
||||
Ok(subnet)
|
||||
}
|
||||
|
||||
async fn modify_subnet(
|
||||
self,
|
||||
_: tarpc::context::Context,
|
||||
_edit_args: crate::model::Subnet,
|
||||
) -> Result<crate::model::Subnet, String> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn get_subnets(
|
||||
self,
|
||||
_: tarpc::context::Context,
|
||||
) -> Result<Vec<crate::model::Subnet>, String> {
|
||||
let db = self.db.read().await;
|
||||
Ok(db.subnets.iter().filter_map(|net| net.clone()).collect())
|
||||
}
|
||||
|
||||
async fn delete_subnet(
|
||||
self,
|
||||
_: tarpc::context::Context,
|
||||
interface: String,
|
||||
) -> Result<(), String> {
|
||||
let mut db = self.db.write().await;
|
||||
db.instances
|
||||
.iter()
|
||||
.filter_map(|inst| inst.as_ref())
|
||||
.for_each(|inst| {
|
||||
if inst.lease.subnet == interface {
|
||||
todo!("what now")
|
||||
}
|
||||
});
|
||||
let Some(subnet) = db
|
||||
.subnets
|
||||
.iter_mut()
|
||||
.find(|net| net.as_ref().filter(|n| n.name == interface).is_some())
|
||||
else {
|
||||
return Err("Subnet doesn't exist".to_owned());
|
||||
};
|
||||
subnet.take();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn garbage_collect(self, _: tarpc::context::Context) -> Result<(), String> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn get_ssh_pubkeys(
|
||||
self,
|
||||
_: tarpc::context::Context,
|
||||
) -> Result<Vec<model::SshPubkey>, String> {
|
||||
let db = self.db.read().await;
|
||||
|
||||
Ok(db
|
||||
.ssh_keys
|
||||
.iter()
|
||||
.filter_map(|key| key.as_ref().cloned())
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn add_ssh_pubkey(
|
||||
self,
|
||||
_: tarpc::context::Context,
|
||||
pub_key: String,
|
||||
) -> Result<model::SshPubkey, String> {
|
||||
let mut key_model = model::SshPubkey::from_str(&pub_key).map_err(|e| e.to_string())?;
|
||||
let mut db = self.db.write().await;
|
||||
key_model.id = Some(db.ssh_keys.len() as i32);
|
||||
db.ssh_keys.push(Some(key_model.clone()));
|
||||
Ok(key_model)
|
||||
}
|
||||
|
||||
async fn delete_ssh_pubkey(self, _: tarpc::context::Context, id: i32) -> Result<(), String> {
|
||||
let mut db = self.db.write().await;
|
||||
if let Some(key) = db.ssh_keys.get_mut(id as usize) {
|
||||
key.take();
|
||||
Ok(())
|
||||
} else {
|
||||
Err("No such key".into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates a MockServer task and connected client.
|
||||
pub async fn spawn_c2s() -> (NazrinClient, MockServerHandle<()>) {
|
||||
let (client_transport, server_transport) = tarpc::transport::channel::unbounded();
|
||||
let server: MockServerHandle<()> = {
|
||||
tokio::spawn(async move {
|
||||
BaseChannel::with_defaults(server_transport)
|
||||
.execute(MockServer::default().serve())
|
||||
.for_each(|rpc| {
|
||||
tokio::spawn(rpc);
|
||||
future::ready(())
|
||||
})
|
||||
.await;
|
||||
})
|
||||
.into()
|
||||
};
|
||||
let client = NazrinClient::new(Default::default(), client_transport).spawn();
|
||||
(client, server)
|
||||
}
|
68
nzr-api/src/mock/test.rs
Normal file
68
nzr-api/src/mock/test.rs
Normal file
|
@ -0,0 +1,68 @@
|
|||
use crate::{args, model};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_the_tester() {
|
||||
let (client, _server) = super::spawn_c2s().await;
|
||||
client
|
||||
.new_subnet(
|
||||
crate::default_ctx(),
|
||||
model::Subnet {
|
||||
name: "test".to_owned(),
|
||||
data: model::SubnetData {
|
||||
ifname: "eth0".into(),
|
||||
network: "192.0.2.0/24".parse().unwrap(),
|
||||
start_host: "192.0.2.10".parse().unwrap(),
|
||||
end_host: "192.0.2.254".parse().unwrap(),
|
||||
gateway4: Some("192.0.2.1".parse().unwrap()),
|
||||
dns: Vec::new(),
|
||||
domain_name: None,
|
||||
vlan_id: None,
|
||||
},
|
||||
},
|
||||
)
|
||||
.await
|
||||
.expect("RPC error")
|
||||
.expect("create subnet failed");
|
||||
let task_id = client
|
||||
.new_instance(
|
||||
crate::default_ctx(),
|
||||
args::NewInstance {
|
||||
name: "my-inst".to_owned(),
|
||||
title: None,
|
||||
description: None,
|
||||
subnet: "test".to_owned(),
|
||||
base_image: "some-kinda-linux".to_owned(),
|
||||
cores: 42,
|
||||
memory: 1337,
|
||||
disk_sizes: (10, None),
|
||||
ci_userdata: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.expect("RPC error")
|
||||
.expect("create instance failed");
|
||||
// Poll the instance creation to "complete" it
|
||||
let poll_inst = client
|
||||
.poll_new_instance(crate::default_ctx(), task_id)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert!(poll_inst.result.is_none());
|
||||
assert!(poll_inst.completion < 1.0);
|
||||
let poll_inst = client
|
||||
.poll_new_instance(crate::default_ctx(), task_id)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert!(poll_inst.result.is_some());
|
||||
assert_eq!(poll_inst.completion, 1.0);
|
||||
let instances = client
|
||||
.get_instances(crate::default_ctx(), false)
|
||||
.await
|
||||
.expect("RPC error")
|
||||
.expect("get instances failed");
|
||||
|
||||
assert_eq!(instances.len(), 1);
|
||||
assert_eq!(&instances[0].name, "my-inst");
|
||||
assert_eq!(&instances[0].lease.subnet, "test");
|
||||
}
|
|
@ -1,6 +1,9 @@
|
|||
use hickory_proto::rr::Name;
|
||||
use lazy_static::lazy_static;
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{fmt, net::Ipv4Addr};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::net::{cidr::CidrV4, mac::MacAddr};
|
||||
|
||||
|
@ -127,3 +130,58 @@ impl SubnetData {
|
|||
self.network.host_bits(&self.end_host)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct SshPubkey {
|
||||
pub id: Option<i32>,
|
||||
pub algorithm: String,
|
||||
pub key_data: String,
|
||||
pub comment: Option<String>,
|
||||
}
|
||||
|
||||
impl fmt::Display for SshPubkey {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
if let Some(comment) = &self.comment {
|
||||
write!(f, "{} {} {}", &self.algorithm, &self.key_data, comment)
|
||||
} else {
|
||||
write!(f, "{} {}", &self.algorithm, &self.key_data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum SshPubkeyParseError {
|
||||
#[error("Key file is not of the expected format")]
|
||||
MissingField,
|
||||
#[error("Key data must be base64-encoded")]
|
||||
InvalidKeyData,
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref BASE64_RE: Regex = Regex::new(r"^[A-Za-z0-9+/=]+$").unwrap();
|
||||
}
|
||||
|
||||
impl std::str::FromStr for SshPubkey {
|
||||
type Err = SshPubkeyParseError;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let mut pieces = s.split(' ');
|
||||
let Some(algorithm) = pieces.next() else {
|
||||
return Err(SshPubkeyParseError::MissingField);
|
||||
};
|
||||
let Some(key_data) = pieces.next() else {
|
||||
return Err(SshPubkeyParseError::MissingField);
|
||||
};
|
||||
// Validate key data
|
||||
if !BASE64_RE.is_match(key_data) {
|
||||
return Err(SshPubkeyParseError::InvalidKeyData);
|
||||
}
|
||||
let comment = pieces.next().map(|s| s.trim().to_owned());
|
||||
|
||||
Ok(Self {
|
||||
id: None,
|
||||
algorithm: algorithm.to_owned(),
|
||||
key_data: key_data.to_owned(),
|
||||
comment,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
[package]
|
||||
name = "nzr-virt"
|
||||
version = "0.1.0"
|
||||
version = "0.9.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
tracing = "0.1"
|
||||
tracing = { version = "0.1", features = ["log"] }
|
||||
thiserror = "1"
|
||||
tokio = { version = "1", features = ["process"] }
|
||||
|
||||
|
|
|
@ -82,6 +82,13 @@ impl Domain {
|
|||
.unwrap()
|
||||
}
|
||||
|
||||
/// Stops the libvirt domain forcefully.
|
||||
///
|
||||
/// In libvirt terminology, this is equivalent to `virsh destroy <vm>`.
|
||||
pub async fn stop(&mut self) -> Result<(), VirtError> {
|
||||
self.spawn_virt(|virt| virt.destroy()).await
|
||||
}
|
||||
|
||||
/// Undefines the libvirt domain.
|
||||
/// If `deep` is set to true, all connected volumes are deleted.
|
||||
pub async fn undefine(&mut self, deep: bool) -> Result<(), VirtError> {
|
||||
|
|
|
@ -17,7 +17,8 @@ pub struct Volume {
|
|||
impl Volume {
|
||||
/// Upload a disk image from libvirt in a blocking task
|
||||
async fn upload_img(from: impl Read + Send + 'static, to: Stream) -> Result<(), PoolError> {
|
||||
let mut reader = BufReader::with_capacity(4294967296, from);
|
||||
// 33554408 is current hardcoded VIR_NET_MESSAGE_PAYLOAD_MAX
|
||||
let mut reader = BufReader::with_capacity(33554407, from);
|
||||
|
||||
tokio::task::spawn_blocking(move || {
|
||||
loop {
|
||||
|
@ -209,6 +210,8 @@ impl Volume {
|
|||
}
|
||||
}
|
||||
|
||||
tracing::debug!("Generating virt stream");
|
||||
|
||||
let stream = {
|
||||
let virt_conn = cloned.get_connect().map_err(PoolError::VirtError)?;
|
||||
let cloned = cloned.clone();
|
||||
|
@ -225,6 +228,8 @@ impl Volume {
|
|||
|
||||
let img_size = src_img.metadata().unwrap().len();
|
||||
|
||||
tracing::debug!("Informing virt we want to start uploading");
|
||||
|
||||
{
|
||||
let stream = stream.clone();
|
||||
let cloned = cloned.clone();
|
||||
|
@ -242,6 +247,8 @@ impl Volume {
|
|||
|
||||
let stream_fh = src_img.try_clone().map_err(PoolError::FileError)?;
|
||||
|
||||
tracing::debug!("Actually uploading!");
|
||||
|
||||
Self::upload_img(stream_fh, stream).await?;
|
||||
|
||||
Ok(Self {
|
||||
|
|
|
@ -113,6 +113,14 @@ impl DomainBuilder {
|
|||
self
|
||||
}
|
||||
|
||||
pub fn smbios(mut self, data: Sysinfo) -> Self {
|
||||
self.domain.os.smbios = Some(SmbiosInfo {
|
||||
mode: "sysinfo".into(),
|
||||
});
|
||||
self.domain.sysinfo = Some(data);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn cpu_topology(mut self, sockets: u8, dies: u8, cores: u8, threads: u8) -> Self {
|
||||
self.domain.cpu.topology = CpuTopology {
|
||||
sockets,
|
||||
|
|
|
@ -25,6 +25,7 @@ pub struct Domain {
|
|||
pub cpu: Cpu,
|
||||
pub devices: DeviceList,
|
||||
pub os: OsData,
|
||||
pub sysinfo: Option<Sysinfo>,
|
||||
pub on_poweroff: Option<PowerAction>,
|
||||
pub on_reboot: Option<PowerAction>,
|
||||
pub on_crash: Option<PowerAction>,
|
||||
|
@ -64,11 +65,13 @@ impl Default for Domain {
|
|||
dev: BootDevice::HardDrive,
|
||||
}),
|
||||
r#type: OsType::default(),
|
||||
bios: BiosData {
|
||||
bios: Some(BiosData {
|
||||
useserial: "yes".to_owned(),
|
||||
reboot_timeout: 0,
|
||||
},
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
sysinfo: None,
|
||||
on_poweroff: None,
|
||||
on_reboot: None,
|
||||
on_crash: None,
|
||||
|
@ -358,13 +361,20 @@ impl Default for OsType {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct SmbiosInfo {
|
||||
#[serde(rename = "@mode")]
|
||||
mode: String,
|
||||
}
|
||||
|
||||
#[skip_serializing_none]
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct OsData {
|
||||
boot: Option<BootNode>,
|
||||
r#type: OsType,
|
||||
// we will not be doing PV, no <bootloader>/<kernel>/<initrd>/etc
|
||||
bios: BiosData,
|
||||
bios: Option<BiosData>,
|
||||
smbios: Option<SmbiosInfo>,
|
||||
}
|
||||
|
||||
impl Default for OsData {
|
||||
|
@ -374,10 +384,11 @@ impl Default for OsData {
|
|||
dev: BootDevice::HardDrive,
|
||||
}),
|
||||
r#type: OsType::default(),
|
||||
bios: BiosData {
|
||||
bios: Some(BiosData {
|
||||
useserial: "yes".to_owned(),
|
||||
reboot_timeout: 0,
|
||||
},
|
||||
}),
|
||||
smbios: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -477,6 +488,75 @@ pub struct Cpu {
|
|||
topology: CpuTopology,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct InfoEntry {
|
||||
#[serde(rename = "@name")]
|
||||
name: Option<String>,
|
||||
#[serde(rename = "$value")]
|
||||
value: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct InfoMap {
|
||||
entry: Vec<InfoEntry>,
|
||||
}
|
||||
|
||||
impl InfoMap {
|
||||
pub fn new() -> Self {
|
||||
Self { entry: Vec::new() }
|
||||
}
|
||||
|
||||
pub fn push(&mut self, name: impl Into<String>, value: impl Into<String>) -> &mut Self {
|
||||
self.entry.push(InfoEntry {
|
||||
name: Some(name.into()),
|
||||
value: value.into(),
|
||||
});
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for InfoMap {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[skip_serializing_none]
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Sysinfo {
|
||||
#[serde(rename = "@type")]
|
||||
r#type: String,
|
||||
bios: Option<InfoMap>,
|
||||
system: Option<InfoMap>,
|
||||
base_board: Option<InfoMap>,
|
||||
chassis: Option<InfoMap>,
|
||||
oem_strings: Option<InfoMap>,
|
||||
}
|
||||
|
||||
impl Sysinfo {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
r#type: "smbios".into(),
|
||||
bios: None,
|
||||
system: None,
|
||||
base_board: None,
|
||||
chassis: None,
|
||||
oem_strings: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn system(&mut self, info: InfoMap) {
|
||||
self.system = Some(info);
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Sysinfo {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
// =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^=
|
||||
|
||||
#[skip_serializing_none]
|
||||
|
|
|
@ -47,12 +47,25 @@ fn domain_serde() {
|
|||
<boot dev="hd"/>
|
||||
<type arch="x86_64" machine="pc-i440fx-5.2">hvm</type>
|
||||
<bios useserial="yes" rebootTimeout="0"/>
|
||||
<smbios mode="sysinfo"/>
|
||||
</os>
|
||||
<sysinfo type="smbios">
|
||||
<system>
|
||||
<entry name="serial">hello!</entry>
|
||||
</system>
|
||||
</sysinfo>
|
||||
</domain>"#
|
||||
.unprettify();
|
||||
println!("Serializing domain...");
|
||||
let mac = MacAddr::new(0x02, 0x0b, 0xee, 0xca, 0xfe, 0x42);
|
||||
let uuid = uuid!("9a8f2611-a976-4d06-ac91-2750ac3462b3");
|
||||
let sysinfo = {
|
||||
let mut system_map = InfoMap::new();
|
||||
system_map.push("serial", "hello!");
|
||||
let mut sysinfo = Sysinfo::new();
|
||||
sysinfo.system(system_map);
|
||||
sysinfo
|
||||
};
|
||||
let domain = DomainBuilder::default()
|
||||
.name("test-vm")
|
||||
.uuid(uuid)
|
||||
|
@ -62,6 +75,7 @@ fn domain_serde() {
|
|||
.target("sda", "virtio")
|
||||
})
|
||||
.net_device(|net| net.with_bridge("virbr0").mac_addr(mac))
|
||||
.smbios(sysinfo)
|
||||
.build();
|
||||
let dom_xml = quick_xml::se::to_string(&domain).unwrap();
|
||||
println!("{}", dom_xml);
|
||||
|
|
|
@ -36,6 +36,7 @@ diesel = { version = "2.2", features = [
|
|||
"sqlite",
|
||||
"returning_clauses_for_sqlite_3_35",
|
||||
] }
|
||||
libsqlite3-sys = { version = "0.29.0", features = ["bundled"] }
|
||||
diesel_migrations = "2.2"
|
||||
|
||||
clap = { version = "4.0.26", features = ["derive"] }
|
||||
|
|
|
@ -20,5 +20,5 @@ CREATE TABLE instances (
|
|||
ci_metadata TEXT NOT NULL,
|
||||
ci_userdata BINARY,
|
||||
UNIQUE(subnet_id, host_num),
|
||||
FOREIGN KEY(subnet_id) REFERENCES subnet(id)
|
||||
FOREIGN KEY(subnet_id) REFERENCES subnets(id)
|
||||
);
|
1
nzrd/migrations/2024081401_ssh_keys/down.sql
Normal file
1
nzrd/migrations/2024081401_ssh_keys/down.sql
Normal file
|
@ -0,0 +1 @@
|
|||
DROP TABLE ssh_keys;
|
7
nzrd/migrations/2024081401_ssh_keys/up.sql
Normal file
7
nzrd/migrations/2024081401_ssh_keys/up.sql
Normal file
|
@ -0,0 +1,7 @@
|
|||
CREATE TABLE ssh_keys (
|
||||
id INTEGER PRIMARY KEY NOT NULL,
|
||||
algorithm TEXT NOT NULL,
|
||||
key_data TEXT NOT NULL,
|
||||
comment TEXT,
|
||||
UNIQUE(key_data)
|
||||
);
|
|
@ -1,7 +1,7 @@
|
|||
use nzr_api::net::cidr::CidrV4;
|
||||
use nzr_virt::error::DomainError;
|
||||
use nzr_virt::xml::build::DomainBuilder;
|
||||
use nzr_virt::xml::{self, SerialType};
|
||||
use nzr_virt::xml::{self, InfoMap, SerialType, Sysinfo};
|
||||
use nzr_virt::{datasize, dom, vol};
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
|
@ -121,6 +121,17 @@ pub async fn new_instance(
|
|||
let pri_name = &ctx.config.storage.primary_pool;
|
||||
let sec_name = &ctx.config.storage.secondary_pool;
|
||||
|
||||
let smbios_info = {
|
||||
let mut sysinfo = Sysinfo::new();
|
||||
let mut system_map = InfoMap::new();
|
||||
system_map.push(
|
||||
"serial",
|
||||
format!("ds=nocloud-net;s={}", ctx.config.cloud.http_addr()),
|
||||
);
|
||||
sysinfo.system(system_map);
|
||||
sysinfo
|
||||
};
|
||||
|
||||
let mut instdata = DomainBuilder::default()
|
||||
.name(&args.name)
|
||||
.memory(datasize!((args.memory) MiB))
|
||||
|
@ -136,6 +147,7 @@ pub async fn new_instance(
|
|||
.qcow2()
|
||||
.boot_order(1)
|
||||
})
|
||||
.smbios(smbios_info)
|
||||
.serial_device(SerialType::Pty);
|
||||
|
||||
// add desription, if provided
|
||||
|
@ -184,8 +196,18 @@ pub async fn delete_instance(ctx: Context, name: String) -> Result<(), Box<dyn s
|
|||
let Some(inst_db) = Instance::get_by_name(&ctx, &name).await? else {
|
||||
return Err(cmd_error!("Instance {name} not found"));
|
||||
};
|
||||
let mut inst = ctx.virt.conn.get_instance(name.clone()).await?;
|
||||
inst.undefine(true).await?;
|
||||
// First, destroy the instance
|
||||
match ctx.virt.conn.get_instance(name.clone()).await {
|
||||
Ok(mut inst) => {
|
||||
inst.stop().await?;
|
||||
inst.undefine(true).await?;
|
||||
}
|
||||
Err(DomainError::DomainNotFound) => {
|
||||
warn!("Deleting instance that exists in DB but not libvirt");
|
||||
}
|
||||
Err(err) => Err(err)?,
|
||||
}
|
||||
// Then, delete the DB entity
|
||||
inst_db.delete(&ctx).await?;
|
||||
|
||||
Ok(())
|
||||
|
|
|
@ -20,7 +20,7 @@ use tx::Transactable;
|
|||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ModelError {
|
||||
#[error("Database error occured: {0}")]
|
||||
#[error("Database error occurred: {0}")]
|
||||
Db(#[from] diesel::result::Error),
|
||||
#[error("Unable to get database handle: {0}")]
|
||||
Pool(#[from] diesel::r2d2::PoolError),
|
||||
|
@ -54,6 +54,15 @@ diesel::table! {
|
|||
}
|
||||
}
|
||||
|
||||
diesel::table! {
|
||||
ssh_keys {
|
||||
id -> Integer,
|
||||
algorithm -> Text,
|
||||
key_data -> Text,
|
||||
comment -> Nullable<Text>,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
AsChangeset,
|
||||
Clone,
|
||||
|
@ -281,6 +290,7 @@ impl Transactable for Instance {
|
|||
//
|
||||
|
||||
#[derive(AsChangeset, Clone, Insertable, Identifiable, Selectable, Queryable, PartialEq, Debug)]
|
||||
#[diesel(table_name = subnets, treat_none_as_default_value = false)]
|
||||
pub struct Subnet {
|
||||
pub id: i32,
|
||||
pub name: String,
|
||||
|
@ -456,3 +466,80 @@ impl Transactable for Subnet {
|
|||
self.delete(ctx).await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Insertable, Identifiable, Selectable, Queryable)]
|
||||
#[diesel(table_name = ssh_keys, treat_none_as_default_value = false)]
|
||||
pub struct SshPubkey {
|
||||
pub id: i32,
|
||||
pub algorithm: String,
|
||||
pub key_data: String,
|
||||
pub comment: Option<String>,
|
||||
}
|
||||
|
||||
impl SshPubkey {
|
||||
pub async fn all(ctx: &Context) -> Result<Vec<Self>, ModelError> {
|
||||
let res = ctx
|
||||
.spawn_db(move |mut db| {
|
||||
Self::table()
|
||||
.select(Self::as_select())
|
||||
.load::<Self>(&mut db)
|
||||
})
|
||||
.await??;
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub async fn get(ctx: &Context, id: i32) -> Result<Option<Self>, ModelError> {
|
||||
Ok(ctx
|
||||
.spawn_db(move |mut db| {
|
||||
Self::table()
|
||||
.find(id)
|
||||
.select(Self::as_select())
|
||||
.load::<Self>(&mut db)
|
||||
})
|
||||
.await??
|
||||
.into_iter()
|
||||
.next())
|
||||
}
|
||||
|
||||
pub async fn insert(
|
||||
ctx: &Context,
|
||||
algorithm: impl AsRef<str>,
|
||||
key_data: impl AsRef<str>,
|
||||
comment: Option<impl AsRef<str>>,
|
||||
) -> Result<Self, ModelError> {
|
||||
use self::ssh_keys::columns;
|
||||
let values = (
|
||||
columns::algorithm.eq(algorithm.as_ref().to_owned()),
|
||||
columns::key_data.eq(key_data.as_ref().to_owned()),
|
||||
columns::comment.eq(comment.map(|s| s.as_ref().to_owned())),
|
||||
);
|
||||
|
||||
let ent = ctx
|
||||
.spawn_db(move |mut db| {
|
||||
diesel::insert_into(Self::table())
|
||||
.values(values)
|
||||
.returning(ssh_keys::table::all_columns())
|
||||
.get_result::<Self>(&mut db)
|
||||
})
|
||||
.await??;
|
||||
|
||||
Ok(ent)
|
||||
}
|
||||
|
||||
pub fn api_model(&self) -> nzr_api::model::SshPubkey {
|
||||
nzr_api::model::SshPubkey {
|
||||
id: Some(self.id),
|
||||
algorithm: self.algorithm.clone(),
|
||||
key_data: self.key_data.clone(),
|
||||
comment: self.comment.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn delete(self, ctx: &Context) -> Result<(), ModelError> {
|
||||
ctx.spawn_db(move |mut db| diesel::delete(&self).execute(&mut db))
|
||||
.await??;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use futures::{future, StreamExt};
|
||||
use nzr_api::{args, model, InstanceQuery, Nazrin};
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
use tarpc::server::{BaseChannel, Channel};
|
||||
use tarpc::tokio_serde::formats::Bincode;
|
||||
|
@ -11,7 +12,7 @@ use uuid::Uuid;
|
|||
|
||||
use crate::cmd;
|
||||
use crate::ctx::Context;
|
||||
use crate::model::{Instance, Subnet};
|
||||
use crate::model::{Instance, SshPubkey, Subnet};
|
||||
use log::*;
|
||||
use std::collections::HashMap;
|
||||
|
||||
|
@ -252,6 +253,41 @@ impl Nazrin for NzrServer {
|
|||
|
||||
Ok(db_model.ci_userdata.unwrap_or_default())
|
||||
}
|
||||
|
||||
async fn get_ssh_pubkeys(
|
||||
self,
|
||||
_: tarpc::context::Context,
|
||||
) -> Result<Vec<model::SshPubkey>, String> {
|
||||
SshPubkey::all(&self.ctx).await.map_or_else(
|
||||
|e| Err(e.to_string()),
|
||||
|k| Ok(k.iter().map(|k| k.api_model()).collect()),
|
||||
)
|
||||
}
|
||||
|
||||
async fn add_ssh_pubkey(
|
||||
self,
|
||||
_: tarpc::context::Context,
|
||||
pub_key: String,
|
||||
) -> Result<model::SshPubkey, String> {
|
||||
let pubkey = model::SshPubkey::from_str(&pub_key).map_err(|e| e.to_string())?;
|
||||
|
||||
SshPubkey::insert(&self.ctx, pubkey.algorithm, pubkey.key_data, pubkey.comment)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
.map(|k| k.api_model())
|
||||
}
|
||||
|
||||
async fn delete_ssh_pubkey(self, _: tarpc::context::Context, id: i32) -> Result<(), String> {
|
||||
let Some(key) = SshPubkey::get(&self.ctx, id)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
else {
|
||||
return Err("SSH key with ID doesn't exist".into());
|
||||
};
|
||||
|
||||
key.delete(&self.ctx).await.map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
[package]
|
||||
name = "nzrdhcp"
|
||||
description = "Unicast-only static DHCP server for nazrin"
|
||||
version = "0.1.0"
|
||||
version = "0.9.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
|
|
|
@ -14,7 +14,12 @@ use tracing::instrument;
|
|||
const EMPTY_V4: Ipv4Addr = Ipv4Addr::new(0, 0, 0, 0);
|
||||
const DEFAULT_LEASE: u32 = 86400;
|
||||
|
||||
fn make_reply(msg: &Message, msg_type: MessageType, lease_addr: Option<Ipv4Addr>) -> Message {
|
||||
fn make_reply(
|
||||
msg: &Message,
|
||||
msg_type: MessageType,
|
||||
lease_addr: Option<Ipv4Addr>,
|
||||
broadcast: bool,
|
||||
) -> Message {
|
||||
let mut resp = Message::new(
|
||||
EMPTY_V4,
|
||||
lease_addr.unwrap_or(EMPTY_V4),
|
||||
|
@ -25,7 +30,11 @@ fn make_reply(msg: &Message, msg_type: MessageType, lease_addr: Option<Ipv4Addr>
|
|||
resp.set_opcode(Opcode::BootReply)
|
||||
.set_xid(msg.xid())
|
||||
.set_htype(msg.htype())
|
||||
.set_flags(msg.flags());
|
||||
.set_flags(if broadcast {
|
||||
msg.flags().set_broadcast()
|
||||
} else {
|
||||
msg.flags()
|
||||
});
|
||||
resp.opts_mut().insert(DhcpOption::MessageType(msg_type));
|
||||
resp
|
||||
}
|
||||
|
@ -71,21 +80,27 @@ async fn handle_message(ctx: &Context, from: SocketAddr, msg: &Message) {
|
|||
let mut response = match msg_type {
|
||||
MessageType::Discover => {
|
||||
lease_time = Some(DEFAULT_LEASE);
|
||||
make_reply(msg, MessageType::Offer, Some(instance.lease.addr.addr))
|
||||
make_reply(
|
||||
msg,
|
||||
MessageType::Offer,
|
||||
Some(instance.lease.addr.addr),
|
||||
true,
|
||||
)
|
||||
}
|
||||
MessageType::Request => {
|
||||
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))
|
||||
lease_time = Some(DEFAULT_LEASE);
|
||||
make_reply(msg, MessageType::Ack, Some(instance.lease.addr.addr), true)
|
||||
} else {
|
||||
nak = true;
|
||||
make_reply(msg, MessageType::Nak, None)
|
||||
make_reply(msg, MessageType::Nak, None, true)
|
||||
}
|
||||
} else {
|
||||
nak = true;
|
||||
make_reply(msg, MessageType::Nak, None)
|
||||
make_reply(msg, MessageType::Nak, None, true)
|
||||
}
|
||||
}
|
||||
MessageType::Decline => {
|
||||
|
@ -101,7 +116,7 @@ async fn handle_message(ctx: &Context, from: SocketAddr, msg: &Message) {
|
|||
tracing::debug!("Ignoring DHCPRELEASE");
|
||||
return;
|
||||
}
|
||||
MessageType::Inform => make_reply(msg, MessageType::Ack, None),
|
||||
MessageType::Inform => make_reply(msg, MessageType::Ack, None, false),
|
||||
other => {
|
||||
tracing::info!("Received unhandled message {other:?}");
|
||||
return;
|
||||
|
@ -177,7 +192,7 @@ async fn handle_message(ctx: &Context, from: SocketAddr, msg: &Message) {
|
|||
|
||||
#[tokio::main]
|
||||
async fn main() -> ExitCode {
|
||||
tracing_subscriber::fmt().init();
|
||||
tracing_subscriber::fmt::init();
|
||||
let cfg: Config = match Config::figment().extract() {
|
||||
Ok(cfg) => cfg,
|
||||
Err(err) => {
|
||||
|
@ -197,8 +212,8 @@ async fn main() -> ExitCode {
|
|||
tracing::info!("nzrdhcp ready! Listening on {}", ctx.addr());
|
||||
|
||||
loop {
|
||||
let mut buf = [0u8; 576];
|
||||
let (_, src) = match ctx.sock().recv_from(&mut buf).await {
|
||||
let mut buf = [0u8; 1500];
|
||||
let (sz, src) = match ctx.sock().recv_from(&mut buf).await {
|
||||
Ok(x) => x,
|
||||
Err(err) => {
|
||||
tracing::error!("recv_from error: {err}");
|
||||
|
@ -206,7 +221,7 @@ async fn main() -> ExitCode {
|
|||
}
|
||||
};
|
||||
|
||||
let msg = match Message::decode(&mut Decoder::new(&buf)) {
|
||||
let msg = match Message::decode(&mut Decoder::new(&buf[..sz])) {
|
||||
Ok(msg) => msg,
|
||||
Err(err) => {
|
||||
tracing::error!("Couldn't process message from {}: {}", src, err);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "omyacid"
|
||||
version = "0.1.0"
|
||||
version = "0.9.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
|
@ -12,3 +12,6 @@ tracing-subscriber = "0.3"
|
|||
anyhow = "1"
|
||||
askama = "0.12"
|
||||
moka = { version = "0.12.8", features = ["future"] }
|
||||
|
||||
[dev-dependencies]
|
||||
nzr-api = { path = "../nzr-api", features = ["mock"] }
|
||||
|
|
|
@ -8,6 +8,7 @@ use anyhow::Result;
|
|||
use moka::future::Cache;
|
||||
use nzr_api::config::Config;
|
||||
use nzr_api::model::Instance;
|
||||
use nzr_api::model::SshPubkey;
|
||||
use nzr_api::InstanceQuery;
|
||||
use nzr_api::NazrinClient;
|
||||
use tokio::net::UnixStream;
|
||||
|
@ -46,6 +47,26 @@ impl Context {
|
|||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn new_mock(cfg: Config, api_client: NazrinClient) -> Self {
|
||||
Self {
|
||||
api_client,
|
||||
config: Arc::new(cfg),
|
||||
host_cache: Cache::new(5),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_sshkeys(&self) -> Result<Vec<SshPubkey>> {
|
||||
// TODO: do we cache SSH keys? I don't like the idea of it
|
||||
let ssh_keys = self
|
||||
.api_client
|
||||
.get_ssh_pubkeys(nzr_api::default_ctx())
|
||||
.await
|
||||
.context("RPC Error")?
|
||||
.map_err(|e| anyhow::anyhow!("Couldn't get SSH keys: {e}"))?;
|
||||
Ok(ssh_keys)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
mod ctx;
|
||||
mod model;
|
||||
#[cfg(test)]
|
||||
mod test;
|
||||
|
||||
use std::{
|
||||
net::{IpAddr, SocketAddr},
|
||||
|
@ -16,18 +18,31 @@ use axum::{
|
|||
};
|
||||
use model::Metadata;
|
||||
use nzr_api::config::Config;
|
||||
use tracing::instrument;
|
||||
|
||||
#[instrument(skip(ctx))]
|
||||
async fn get_meta_data(
|
||||
State(ctx): State<ctx::Context>,
|
||||
ConnectInfo(addr): ConnectInfo<SocketAddr>,
|
||||
) -> Result<String, StatusCode> {
|
||||
tracing::info!("Handling /meta-data");
|
||||
if let IpAddr::V4(ip) = addr.ip() {
|
||||
let ssh_pubkeys: Vec<String> = ctx
|
||||
.get_sshkeys()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::error!("Couldn't get SSH keys: {e}");
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
})?
|
||||
.into_iter()
|
||||
.map(|k| k.to_string())
|
||||
.collect();
|
||||
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()),
|
||||
// XXX: this is very silly imo
|
||||
ssh_pubkeys: ssh_pubkeys.iter().collect(),
|
||||
};
|
||||
|
||||
meta.render().map_err(|e| {
|
||||
|
@ -49,10 +64,12 @@ async fn get_meta_data(
|
|||
}
|
||||
}
|
||||
|
||||
#[instrument(skip(ctx))]
|
||||
async fn get_user_data(
|
||||
State(ctx): State<ctx::Context>,
|
||||
ConnectInfo(addr): ConnectInfo<SocketAddr>,
|
||||
) -> Result<Vec<u8>, StatusCode> {
|
||||
tracing::info!("Handling /user-data");
|
||||
if let IpAddr::V4(ip) = addr.ip() {
|
||||
match ctx.get_inst_userdata(ip).await {
|
||||
Ok(Some(data)) => Ok(data),
|
||||
|
@ -70,9 +87,48 @@ async fn get_user_data(
|
|||
}
|
||||
}
|
||||
|
||||
#[instrument(skip(ctx))]
|
||||
async fn get_vendor_data(
|
||||
State(ctx): State<ctx::Context>,
|
||||
ConnectInfo(addr): ConnectInfo<SocketAddr>,
|
||||
) -> Result<String, StatusCode> {
|
||||
tracing::info!("Handling /vendor-data");
|
||||
|
||||
// All of the vendor data so far is handled globally, so this isn't really
|
||||
// necessary. But it might help avoid an attacker trying to sniff for the
|
||||
// admin username from an unknown instance.
|
||||
if let IpAddr::V4(ip) = addr.ip() {
|
||||
match ctx.get_instance(ip).await {
|
||||
Ok(_) => {
|
||||
let data = model::VendorData {
|
||||
username: Some(&ctx.cfg().cloud.admin_user),
|
||||
};
|
||||
data.render().map_err(|e| {
|
||||
tracing::error!("Renderer error: {e}");
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
})
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::error!("{err}");
|
||||
Err(StatusCode::INTERNAL_SERVER_ERROR)
|
||||
}
|
||||
_ => {
|
||||
tracing::warn!("Request from unregistered server {ip}");
|
||||
Err(StatusCode::FORBIDDEN)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Err(StatusCode::BAD_REQUEST)
|
||||
}
|
||||
}
|
||||
|
||||
async fn ignored() -> &'static str {
|
||||
""
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> ExitCode {
|
||||
tracing_subscriber::fmt().init();
|
||||
tracing_subscriber::fmt::init();
|
||||
let cfg: Config = match Config::figment().extract() {
|
||||
Ok(cfg) => cfg,
|
||||
Err(err) => {
|
||||
|
@ -109,9 +165,15 @@ async fn main() -> ExitCode {
|
|||
let app = Router::new()
|
||||
.route("/meta-data", get(get_meta_data))
|
||||
.route("/user-data", get(get_user_data))
|
||||
.route("/vendor-data", get(get_vendor_data))
|
||||
.route("/network-config", get(ignored))
|
||||
.with_state(ctx);
|
||||
|
||||
if let Err(err) = axum::serve(http_sock, app).await {
|
||||
if let Err(err) = axum::serve(
|
||||
http_sock,
|
||||
app.into_make_service_with_connect_info::<SocketAddr>(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::error!("axum error: {err}");
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
|
|
@ -4,5 +4,10 @@ use askama::Template;
|
|||
pub struct Metadata<'a> {
|
||||
pub inst_name: &'a str,
|
||||
pub ssh_pubkeys: Vec<&'a String>,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "vendor-data.yml")]
|
||||
pub struct VendorData<'a> {
|
||||
pub username: Option<&'a str>,
|
||||
}
|
||||
|
|
44
omyacid/src/test.rs
Normal file
44
omyacid/src/test.rs
Normal file
|
@ -0,0 +1,44 @@
|
|||
use std::net::SocketAddr;
|
||||
|
||||
use axum::extract::{ConnectInfo, State};
|
||||
use nzr_api::{
|
||||
config::{CloudConfig, Config},
|
||||
mock::{self, client::NzrClientExt},
|
||||
};
|
||||
|
||||
use crate::ctx;
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_metadata() {
|
||||
tracing_subscriber::fmt().init();
|
||||
let (mut client, _server) = mock::spawn_c2s().await;
|
||||
|
||||
let inst = client
|
||||
.new_mock_instance("something")
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
let cfg = Config {
|
||||
cloud: CloudConfig {
|
||||
listen_addr: "0.0.0.0".into(),
|
||||
port: 80,
|
||||
admin_user: "admin".to_owned(),
|
||||
http_addr: None,
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
let ctx = ctx::Context::new_mock(cfg, client);
|
||||
let inst_sock: SocketAddr = (inst.lease.addr.addr, 54545).into();
|
||||
|
||||
let metadata = crate::get_meta_data(State(ctx.clone()), ConnectInfo(inst_sock))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
metadata,
|
||||
"instance_id: \"iid-something\"\nlocal_hostname: \"something\"\ndefault_username: \"admin\""
|
||||
)
|
||||
|
||||
// TODO: Instance with SSH keys
|
||||
}
|
|
@ -1,11 +1,8 @@
|
|||
instance_id: "iid-{{ inst_name }}"
|
||||
local_hostname: "{{ inst_name }}"
|
||||
local-hostname: "{{ inst_name }}"
|
||||
{% if !ssh_pubkeys.is_empty() -%}
|
||||
public_keys:
|
||||
public-keys:
|
||||
{% for key in ssh_pubkeys -%}
|
||||
- "{{ key }}"
|
||||
{% endfor %}
|
||||
{% endif -%}
|
||||
{% if let Some(user) = username -%}
|
||||
default_username: "{{ user }}"
|
||||
{%- endif %}
|
||||
{%- endif -%}
|
6
omyacid/templates/vendor-data.yml
Normal file
6
omyacid/templates/vendor-data.yml
Normal file
|
@ -0,0 +1,6 @@
|
|||
#cloud-config
|
||||
{% if let Some(user) = username -%}
|
||||
system_info:
|
||||
default_user:
|
||||
name: "{{ user }}"
|
||||
{%- endif %}
|
Loading…
Reference in a new issue