summaryrefslogblamecommitdiff
path: root/src/main.rs
blob: 1969398de90080b1859c28dda819fca27496a67f (plain) (tree)
1
2
3
4
5
6
7
8
9
10
            

                          


                     
                    
                                            

                                                 
                              
                          








                                         
                   

 

                                 
                      

 







                                           

                            
                    








                                            



                            
                    

          
                     

      
            








                                       
                     




                                                                                                   

                    

 
           

                                   

                                                                                                
                                               

                                                               
                                               
                                                                   



                                                                   
         
            
                                                                                                    

     



                                                        





                                        
         

                                                     
         

                                               
         

                                               
         

                                                       
         

                                      







                         
                                                                                       
         

                                     
         


      
                                                     



                                                         
 
                                                            
 

                                                             


                           




                                                  






                          





                                                                                



                                 
         

                                                                                     
         





                                                                        


                                                               



                                 
         
              




                                             





                                                                        


                                                               



                                
         
              




                                             


          
 










                                                                                  
         
              




                                             



          




                                                            
                                               


                      



                                                     



          








                           






                                                  
                                 










                                                   
         
              



                                             

         


          









                                                                   
                                                                                      
 

                                                                       
 



                                                                                   
 


                                                                                   
              



                                             




          
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(())
}