nazrin/client/src/main.rs

443 lines
16 KiB
Rust

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 std::any::{Any, TypeId};
use std::path::PathBuf;
use std::str::FromStr;
use tokio::net::UnixStream;
mod table;
#[derive(Debug, clap::Args)]
pub struct NewInstanceArgs {
/// Name of the instance to be created
name: String,
/// Subnet the instance will initially run on
#[arg(short, long)]
subnet: String,
/// Long description of the instance
#[arg(long)]
description: Option<String>,
/// Base image to use for the instance
#[arg(short, long)]
base: String,
///How many cores to assign to the instance
#[arg(short, long, default_value_t = 2)]
cores: u8,
/// Memory to assign, in MiB
#[arg(short, long, default_value_t = 1024)]
mem: u32,
/// Primary HDD size, in GiB
#[arg(short, long, default_value_t = 20)]
primary_size: u32,
/// Secndary HDD size, in GiB
#[arg(long)]
secondary_size: Option<u32>,
/// Path to cloud-init userdata, if any
#[arg(long)]
ci_userdata: Option<PathBuf>,
}
#[derive(Debug, Subcommand)]
enum InstanceCmd {
/// Create a new instance
New(NewInstanceArgs),
/// Delete an existing instance
Delete { name: String },
/// List all instances
List,
/// Deletes all invalid instances from the database
Prune,
/// Shows information on an instance
Dump {
name: Option<String>,
#[arg(short, long)]
quick: bool,
},
}
#[derive(Debug, clap::Args)]
pub struct AddNetArgs {
/// Short name for the subnet (e.g., `servers')
pub name: String,
/// Name of the bridge interface VMs will attach to
pub interface: String,
/// Subnet associated with the bridge interface, in CIDR notation (x.x.x.x/y)
pub network: String,
/// Default gateway for the subnet
#[arg(long)]
pub gateway: Option<std::net::Ipv4Addr>,
/// Default DNS address for the subnet
#[arg(long)]
pub dns_server: Option<std::net::Ipv4Addr>,
/// Start address for IP assignment
#[arg(short, long)]
pub start_addr: Option<std::net::Ipv4Addr>,
/// End address for IP assignment
#[arg(short, long)]
pub end_addr: Option<std::net::Ipv4Addr>,
#[arg(short, long)]
pub domain_name: Option<Name>,
/// VLAN ID for the VM, if any
#[arg(short, long)]
pub vlan_id: Option<u32>,
}
#[derive(Debug, clap::Args)]
pub struct EditNetArgs {
/// Short name of the subnet
pub name: String,
/// Default gateway for the subnet
#[arg(long)]
pub gateway: Option<std::net::Ipv4Addr>,
/// Default DNS address for the subnet
#[arg(long)]
pub dns_server: Option<std::net::Ipv4Addr>,
/// Start address for IP assignment
#[arg(short, long)]
pub start_addr: Option<std::net::Ipv4Addr>,
/// End address for IP assignment
#[arg(short, long)]
pub end_addr: Option<std::net::Ipv4Addr>,
/// Domain name associated with the subnet
#[arg(short, long)]
pub domain_name: Option<Name>,
/// VLAN ID for the VM, if any
#[arg(short, long)]
pub vlan_id: Option<u32>,
}
#[derive(Debug, Subcommand)]
enum NetCmd {
/// Add a new network to the database
Add(AddNetArgs),
/// Edit an existing network
Edit(EditNetArgs),
/// List all networks in the database
List,
/// Delete a network from the database
Delete { name: String },
/// Shows information on a subnet
Dump { name: Option<String> },
}
#[derive(Debug, Subcommand)]
enum Commands {
/// Commands for managing instances
Instance {
#[command(subcommand)]
command: InstanceCmd,
},
/// Commands for managing network assignments
Net {
#[command(subcommand)]
command: NetCmd,
},
}
#[derive(Parser, Debug)]
#[command(version, about, long_about = "")]
struct Args {
#[command(subcommand)]
command: Commands,
}
#[derive(Debug)]
struct CommandError {
inner: Option<Box<dyn std::error::Error>>,
message: String,
}
impl std::fmt::Display for CommandError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", &self.message)?;
if let Some(inner) = &self.inner {
write!(f, "\n inner: {:?}", &inner)?;
}
Ok(())
}
}
impl std::error::Error for CommandError {}
impl From<String> for CommandError {
fn from(value: String) -> Self {
Self {
inner: None,
message: value,
}
}
}
impl From<&str> for CommandError {
fn from(value: &str) -> Self {
Self {
inner: None,
message: value.to_owned(),
}
}
}
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();
let mut matches = Args::command().infer_subcommands(true).get_matches();
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 client = nzr_api::new_client(conn);
match cli.command {
Commands::Instance { command } => match command {
InstanceCmd::Dump { name, quick } => {
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)?);
}
} else {
println!("{}", serde_json::to_string(&instances)?);
}
}
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")),
)
},
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,
)),
}
}
}?;
*/
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}"))?,
)
}
} else {
None
}
};
let build_args = nzr_api::args::NewInstance {
name: args.name,
title: None,
description: args.description,
subnet: args.subnet,
base_image: args.base,
cores: args.cores,
memory: args.mem,
disk_sizes: (args.primary_size, args.secondary_size),
ci_userdata,
};
let task_id = (client
.new_instance(nzr_api::default_ctx(), build_args)
.await?)?;
const MAX_RETRIES: i32 = 5;
let mut retries = 0;
let mut current_pct: f32 = 0.0;
loop {
let status = client
.poll_new_instance(nzr_api::default_ctx(), task_id)
.await;
match status {
Ok(Some(status)) => {
if let Some(result) = status.result {
match result {
Ok(instance) => {
println!("Instance {} created!", &instance.name);
println!(
"You should be able to reach it with: ssh root@{}",
instance.lease.addr.addr,
);
}
Err(err) => {
log::error!("Error while creating instance: {}", err);
}
}
break;
} else if status.completion != current_pct {
println!("[remote] {}", &status.status_text);
current_pct = status.completion;
}
}
Ok(None) => {
log::error!("Task ID {} went AWOL??", task_id);
break;
}
Err(err) => {
log::error!("Got RPC error: {}", err);
retries += 1;
if retries >= MAX_RETRIES {
break;
} else {
log::error!("Retrying (attempt {}/{})...", retries, MAX_RETRIES);
}
}
}
}
}
InstanceCmd::Delete { name } => {
(client.delete_instance(nzr_api::default_ctx(), name).await?)?;
}
InstanceCmd::List => {
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(nzr_api::default_ctx()).await?)?,
},
Commands::Net { command } => match command {
NetCmd::Add(args) => {
let net_arg = CidrV4::from_str(&args.network)?;
let build_args = model::Subnet {
name: args.name,
data: model::SubnetData {
ifname: args.interface.clone(),
network: net_arg,
start_host: args.start_addr.unwrap_or(net_arg.make_ip(10)?),
end_host: args
.end_addr
.unwrap_or((u32::from(net_arg.broadcast()) - 1u32).into()),
gateway4: Some(args.gateway.unwrap_or(net_arg.make_ip(1)?)),
dns: args.dns_server.map_or(Vec::new(), |d| vec![d]),
domain_name: args.domain_name,
vlan_id: args.vlan_id,
},
};
(client
.new_subnet(nzr_api::default_ctx(), build_args)
.await?)?;
}
NetCmd::Edit(args) => {
let mut net = client
.get_subnets(nzr_api::default_ctx())
.await
.map_err(|e| e.to_string())
.and_then(|res| {
res?.iter()
.find_map(|ent| {
if ent.name == args.name {
Some(ent.clone())
} else {
None
}
})
.ok_or_else(|| format!("Couldn't find network {}", &args.name))
})?;
// merge in the new args
net.data.gateway4 = args.gateway;
if let Some(dns_server) = args.dns_server {
net.data.dns = vec![dns_server]
}
if let Some(start_addr) = args.start_addr {
net.data.start_host = start_addr;
}
if let Some(end_addr) = args.end_addr {
net.data.end_host = end_addr;
}
if let Some(domain_name) = args.domain_name {
net.data.domain_name = Some(domain_name);
}
if let Some(vlan_id) = args.vlan_id {
net.data.vlan_id = Some(vlan_id);
}
// run the update
client
.modify_subnet(nzr_api::default_ctx(), net)
.await
.map_err(|err| format!("RPC error: {}", err))
.and_then(|res| {
res.map(|e| {
println!("Subnet {} updated.", e.name);
})
})?;
}
NetCmd::Dump { name } => {
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)?);
}
} else {
println!("{}", serde_json::to_string(&subnets)?);
}
}
NetCmd::Delete { name } => {
(client.delete_subnet(nzr_api::default_ctx(), name).await?)?;
}
NetCmd::List => {
let subnets = client.get_subnets(nzr_api::default_ctx()).await?;
let tabular: Vec<table::Subnet> =
subnets?.iter().map(table::Subnet::from).collect();
let mut table = tabled::Table::new(tabular);
println!("{}", table.with(tabled::settings::Style::psql()));
}
},
};
Ok(())
}
#[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::<nzr_api::RpcError>() {
log::error!("Error communicating with server: {}", err);
} else {
log::error!("{}", err);
}
}
Ok(())
}