use std::fs; use std::process::Command; use clap::Parser; mod errors; use errors::CliError; use reqwest::header; use tabular::{row, Table}; use reqwest::{blocking::Client, StatusCode}; use solarlib::planet::{Planet, Memory, CpuCount}; use solarlib::star::NewPlanet; use serde::{Serialize, Deserialize}; /// Manage solard and homeworld instances #[derive(Parser)] #[clap(author, version, about)] struct Args { /// The solard server to connect to server: String, /// The action to be taken #[clap(subcommand)] action: Action } #[derive(Serialize, Deserialize)] struct ServerConfig { pub token: String } #[derive(clap::Subcommand)] enum Action { /// List planets on the server List, /// Shuts down a virtual machine Stop { /// The UUID of the machine to stop uuid: String, #[clap(long, short)] force: bool }, Start { /// The UUID of the machine to start uuid: String, }, Pause { /// The UUID of the machine to pause uuid: String, }, Reboot { uuid: String, #[clap(long, short)] force: bool }, View { uuid: String }, Create { max_mem: u64, max_cpus: u64, disk_size_mb: u64, name: String, /// The Sha256 hash of the ship ship: String }, /// Goes through the first-time authentication process to create a token that expires after one /// year, storing it in the process. Login { key: String } } fn main() { color_eyre::install().unwrap(); let args = Args::parse(); let xdg_dirs = xdg::BaseDirectories::with_profile("solarctl", args.server.clone()).unwrap(); let mut headers = header::HeaderMap::new(); if let Some(p) = xdg_dirs.find_config_file("config.json") { let c = fs::read_to_string(p).unwrap(); if let Ok(cfg) = serde_json::from_str::(&c) { headers.insert(header::AUTHORIZATION, header::HeaderValue::from_str(&cfg.token).unwrap()); } } else { println!("No config file found! You will need to authenticate with the `login` subcommand"); } let client = reqwest::blocking::ClientBuilder::new().default_headers(headers).build().unwrap(); let root = args.server.clone(); match args.action { Action::List => { list(args, client).unwrap(); }, Action::Stop { uuid, force } => { stop(root, client, uuid, force).unwrap(); }, Action::Start { uuid } => { start(root, client, uuid).unwrap(); }, Action::Pause { uuid } => { pause(root, client, uuid).unwrap(); }, Action::Reboot { uuid, force } => { reboot(root, client, uuid, force).unwrap(); } Action::View { uuid } => { view(root, uuid).unwrap(); }, Action::Create { max_mem, max_cpus, disk_size_mb, name, ship } => { create(root, client, max_mem, max_cpus, disk_size_mb, name, ship).unwrap(); }, Action::Login { key } => { login(root, client, key); } }; } fn list(a: Args, c: Client) -> Result<(), CliError> { let res: Vec = c.get(format!("http://{}/planets/list", a.server)).send()?.json()?; let mut table = Table::new("{:<} | {:<} | {:<} | {:<}"); table.add_row(row!( "name", "uuid", "status", "running") ); table.add_row(row!( "----", "----", "------", "-------") ); for p in res { table.add_row(row!( &p.name, &p.uuid, &p.status, if p.orbiting { "yes" } else { "no" }, )); } println!("{}", table); Ok(()) } fn stop(server: String, c: Client, u: String, f: bool) -> Result<(), CliError> { let mut url = format!("http://{}/planets/{}/shutdown", server, u); if f { url.push_str("/hard"); } let res = c.post(url).send()?; match res.status() { StatusCode::OK => { println!("Stopped."); }, _ => { return Err(CliError::Cli(format!("Could not stop VM: {}", res.text()?))); }, }; Ok(()) } fn start(server: String, c: Client, u: String) -> Result<(), CliError> { let res = c.post(format!("http://{}/planets/{}/start", server, u)).send()?; match res.status() { StatusCode::OK => { println!("Started."); }, _ => { return Err(CliError::Cli(format!("Could not start VM: {}", res.text()?))); }, }; Ok(()) } fn pause(server: String, c: Client, u: String) -> Result<(), CliError> { let res = c.post(format!("http://{}/planets/{}/pause", server, u)).send()?; match res.status() { StatusCode::OK => { println!("Paused."); }, _ => { return Err(CliError::Cli(format!("Could not pause VM: {}", res.text()?))); }, }; Ok(()) } fn reboot(server: String, c: Client, u: String, f: bool) -> Result<(), CliError> { let mut url = format!("http://{}/planets/{}/reboot", server, u); if f { url.push_str("/hard"); } let res = c.post(url).send()?; match res.status() { StatusCode::OK => { println!("Rebooted."); }, _ => { return Err(CliError::Cli(format!("Could not reboot VM: {}", res.text()?))); }, }; Ok(()) } fn view(server: String, u: String) -> Result<(), CliError> { let host = server.split(':').next().unwrap(); let qemu_url = format!("qemu+ssh://{}/system", host); if let Err(e) = Command::new("virt-viewer") .arg("-c") .arg(qemu_url) .arg(u) .output() { println!("Could not run virt-viewer: {}", e); } Ok(()) } fn create(s: String, c: Client, mem: u64, cpus: u64, disk_size: u64, name: String, ship: String) -> Result<(), CliError> { let url = format!("http://{}/planets/new", s); let new_p: NewPlanet = NewPlanet { name, ship, disk_size_mb: disk_size, max_mem: Memory(mem), max_cpus: CpuCount(cpus) }; println!("Creating new planet..."); let res = c.post(url).json(&new_p).send()?; match res.status() { StatusCode::OK => { let js: Planet = res.json()?; println!("Created. UUID: {}", js.uuid); }, _ => { return Err(CliError::Cli(format!("Could not create VM: {}", res.text()?))); } }; Ok(()) } fn login(s: String, c: Client, k: String) -> Result<(), CliError> { let url = format!("http://{}/auth/begin?key={}", s, k); println!("Obtaining token..."); let res = c.post(url).send()?; match res.status() { StatusCode::OK => { let token = res.text()?; let xdg_dirs = xdg::BaseDirectories::with_profile("solarctl", s).unwrap(); if let Some(p) = xdg_dirs.find_config_file("config.json") { let cfg = ServerConfig { token }; fs::write(p, serde_json::to_string_pretty(&cfg).unwrap()).unwrap(); } else { let p = xdg_dirs.place_config_file("config.json").unwrap(); let cfg = ServerConfig { token }; fs::write(p, serde_json::to_string_pretty(&cfg).unwrap()).unwrap(); } }, _ => { return Err(CliError::Cli(format!("Could not authenticate: {}", res.text()?))); } }; Ok(()) }