use std::fs;
use std::process::Command;
use clap::Parser;
mod errors;
use errors::CliError;
use reqwest::header;
use reqwest::{blocking::Client, StatusCode};
use serde::{Deserialize, Serialize};
use solarlib::planet::{CpuCount, Memory, Planet};
use solarlib::star::NewPlanet;
use tabular::{row, Table};
/// 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::<ServerConfig>(&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<Planet> = 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(())
}