trifid/tfcli/src/host.rs

641 lines
25 KiB
Rust

use std::error::Error;
use std::fs;
use std::net::{Ipv4Addr, SocketAddrV4};
use serde::{Deserialize, Serialize};
use url::{Url};
use crate::api::APIErrorResponse;
use crate::{HostCommands, HostOverrideCommands};
pub async fn host_main(command: HostCommands, server: Url) -> Result<(), Box<dyn Error>> {
match command {
HostCommands::List {} => list_hosts(server).await,
HostCommands::Create { name, network_id, role_id, ip_address, listen_port, lighthouse, relay, static_address } => create_host(name, network_id, role_id, ip_address, listen_port, lighthouse, relay, static_address, server).await,
HostCommands::Lookup { id } => get_host(id, server).await,
HostCommands::Delete { id } => delete_host(id, server).await,
HostCommands::Update { id, listen_port, static_address, name, ip, role } => update_host(id, listen_port, static_address, name, ip, role, server).await,
HostCommands::Block { id } => block_host(id, server).await,
HostCommands::Enroll { id } => enroll_host(id, server).await,
HostCommands::Overrides { command } => match command {
HostOverrideCommands::List { id } => list_overrides(id, server).await,
HostOverrideCommands::Set { id, key, boolean, string, numeric } => set_override(id, key, boolean, numeric, string, server).await,
HostOverrideCommands::Unset { id, key } => unset_override(id, key, server).await
}
}
}
#[derive(Deserialize)]
pub struct HostListResp {
pub data: Vec<Host>
}
#[derive(Serialize, Deserialize)]
pub struct HostMetadata {
#[serde(rename = "lastSeenAt")]
pub last_seen_at: String,
pub version: String,
pub platform: String,
#[serde(rename = "updateAvailable")]
pub update_available: bool,
}
#[derive(Serialize, Deserialize)]
pub struct Host {
pub id: String,
#[serde(rename = "organizationID")]
pub organization_id: String,
#[serde(rename = "networkID")]
pub network_id: String,
#[serde(rename = "roleID")]
pub role_id: String,
pub name: String,
#[serde(rename = "ipAddress")]
pub ip_address: String,
#[serde(rename = "staticAddresses")]
pub static_addresses: Vec<SocketAddrV4>,
#[serde(rename = "listenPort")]
pub listen_port: i64,
#[serde(rename = "isLighthouse")]
pub is_lighthouse: bool,
#[serde(rename = "isRelay")]
pub is_relay: bool,
#[serde(rename = "createdAt")]
pub created_at: String,
#[serde(rename = "isBlocked")]
pub is_blocked: bool,
pub metadata: HostMetadata,
}
pub async fn list_hosts(server: Url) -> Result<(), Box<dyn Error>> {
let client = reqwest::Client::new();
// load session token
let sess_token_store = dirs::config_dir().unwrap().join("tfcli-session.token");
let session_token = fs::read_to_string(&sess_token_store)?;
let auth_token_store = dirs::config_dir().unwrap().join("tfcli-auth.token");
let auth_token = fs::read_to_string(&auth_token_store)?;
let token = format!("{} {}", session_token, auth_token);
let res = client.get(server.join("/v1/hosts?pageSize=5000")?).bearer_auth(token).send().await?;
if res.status().is_success() {
let resp: HostListResp = res.json().await?;
for host in &resp.data {
println!(" Host: {} ({})", host.name, host.id);
println!(" Organization: {}", host.organization_id);
println!(" Network: {}", host.network_id);
println!(" Role: {}", host.role_id);
println!(" IP Address: {}", host.ip_address);
println!(" Static Addresses: {}", host.static_addresses.iter().map(|u| u.to_string()).collect::<Vec<_>>().join(", "));
println!(" Listen Port: {}", host.listen_port);
println!(" Type: {}", if host.is_lighthouse { "Lighthouse" } else if host.is_relay { "Relay" } else { "Host" } );
println!(" Blocked: {}", host.is_blocked);
println!(" Last Seen: {}", host.metadata.last_seen_at);
println!(" Client Version: {}", host.metadata.version);
println!(" Platform: {}", host.metadata.platform);
println!("Client Update Available: {}", host.metadata.update_available);
println!(" Created: {}", host.created_at);
println!();
}
if resp.data.is_empty() {
println!("No hosts found");
}
} else {
let resp: APIErrorResponse = res.json().await?;
eprintln!("[error] Error listing hosts: {} {}", resp.errors[0].code, resp.errors[0].message);
std::process::exit(1);
}
Ok(())
}
#[derive(Serialize, Deserialize)]
pub struct HostCreateBody {
pub name: String,
#[serde(rename = "networkID")]
pub network_id: String,
#[serde(rename = "roleID")]
pub role_id: String,
#[serde(rename = "ipAddress")]
pub ip_address: Ipv4Addr,
#[serde(rename = "listenPort")]
pub listen_port: u16,
#[serde(rename = "isLighthouse")]
pub is_lighthouse: bool,
#[serde(rename = "isRelay")]
pub is_relay: bool,
#[serde(rename = "staticAddresses")]
pub static_addresses: Vec<SocketAddrV4>,
}
#[derive(Serialize, Deserialize)]
pub struct HostGetMetadata {}
#[derive(Serialize, Deserialize)]
pub struct HostGetResponse {
pub data: Host,
pub metadata: HostGetMetadata,
}
pub async fn create_host(name: String, network_id: String, role_id: String, ip_address: Ipv4Addr, listen_port: Option<u16>, lighthouse: bool, relay: bool, static_address: Option<SocketAddrV4>, server: Url) -> Result<(), Box<dyn Error>> {
if lighthouse && relay {
eprintln!("[error] Error creating host: a host cannot be both a lighthouse and a relay at the same time");
std::process::exit(1);
}
if (lighthouse || relay) && static_address.is_none() {
eprintln!("[error] Error creating host: a relay or lighthouse must have a static IP");
std::process::exit(1);
}
if (lighthouse || relay) && listen_port.is_none() {
eprintln!("[error] Error creating host: a relay or lighthouse must have a listen port");
std::process::exit(1);
}
let client = reqwest::Client::new();
// load session token
let sess_token_store = dirs::config_dir().unwrap().join("tfcli-session.token");
let session_token = fs::read_to_string(&sess_token_store)?;
let auth_token_store = dirs::config_dir().unwrap().join("tfcli-auth.token");
let auth_token = fs::read_to_string(&auth_token_store)?;
let token = format!("{} {}", session_token, auth_token);
let res = client.post(server.join(&format!("/v1/hosts"))?).json(&HostCreateBody {
name,
network_id,
role_id,
ip_address,
listen_port: listen_port.unwrap_or(0),
is_lighthouse: lighthouse,
is_relay: relay,
static_addresses: static_address.map_or(vec![], |u| vec![u]),
}).bearer_auth(token).send().await?;
if res.status().is_success() {
let host: Host = res.json::<HostGetResponse>().await?.data;
println!(" Host: {} ({})", host.name, host.id);
println!(" Organization: {}", host.organization_id);
println!(" Network: {}", host.network_id);
println!(" Role: {}", host.role_id);
println!(" IP Address: {}", host.ip_address);
println!(" Static Addresses: {}", host.static_addresses.iter().map(|u| u.to_string()).collect::<Vec<_>>().join(", "));
println!(" Listen Port: {}", host.listen_port);
println!(" Type: {}", if host.is_lighthouse { "Lighthouse" } else if host.is_relay { "Relay" } else { "Host" } );
println!(" Blocked: {}", host.is_blocked);
println!(" Last Seen: {}", host.metadata.last_seen_at);
println!(" Client Version: {}", host.metadata.version);
println!(" Platform: {}", host.metadata.platform);
println!("Client Update Available: {}", host.metadata.update_available);
println!(" Created: {}", host.created_at);
println!();
} else {
let resp: APIErrorResponse = res.json().await?;
eprintln!("[error] Error creating host: {} {}", resp.errors[0].code, resp.errors[0].message);
std::process::exit(1);
}
Ok(())
}
pub async fn get_host(id: String, server: Url) -> Result<(), Box<dyn Error>> {
let client = reqwest::Client::new();
// load session token
let sess_token_store = dirs::config_dir().unwrap().join("tfcli-session.token");
let session_token = fs::read_to_string(&sess_token_store)?;
let auth_token_store = dirs::config_dir().unwrap().join("tfcli-auth.token");
let auth_token = fs::read_to_string(&auth_token_store)?;
let token = format!("{} {}", session_token, auth_token);
let res = client.get(server.join(&format!("/v1/hosts/{}", id))?).bearer_auth(token).send().await?;
if res.status().is_success() {
let host: Host = res.json::<HostGetResponse>().await?.data;
println!(" Host: {} ({})", host.name, host.id);
println!(" Organization: {}", host.organization_id);
println!(" Network: {}", host.network_id);
println!(" Role: {}", host.role_id);
println!(" IP Address: {}", host.ip_address);
println!(" Static Addresses: {}", host.static_addresses.iter().map(|u| u.to_string()).collect::<Vec<_>>().join(", "));
println!(" Listen Port: {}", host.listen_port);
println!(" Type: {}", if host.is_lighthouse { "Lighthouse" } else if host.is_relay { "Relay" } else { "Host" } );
println!(" Blocked: {}", host.is_blocked);
println!(" Last Seen: {}", host.metadata.last_seen_at);
println!(" Client Version: {}", host.metadata.version);
println!(" Platform: {}", host.metadata.platform);
println!("Client Update Available: {}", host.metadata.update_available);
println!(" Created: {}", host.created_at);
println!();
} else {
let resp: APIErrorResponse = res.json().await?;
eprintln!("[error] Error listing hosts: {} {}", resp.errors[0].code, resp.errors[0].message);
std::process::exit(1);
}
Ok(())
}
pub async fn delete_host(id: String, server: Url) -> Result<(), Box<dyn Error>> {
let client = reqwest::Client::new();
// load session token
let sess_token_store = dirs::config_dir().unwrap().join("tfcli-session.token");
let session_token = fs::read_to_string(&sess_token_store)?;
let auth_token_store = dirs::config_dir().unwrap().join("tfcli-auth.token");
let auth_token = fs::read_to_string(&auth_token_store)?;
let token = format!("{} {}", session_token, auth_token);
let res = client.delete(server.join(&format!("/v1/hosts/{}", id))?).bearer_auth(token).send().await?;
if res.status().is_success() {
println!("Host removed");
} else {
let resp: APIErrorResponse = res.json().await?;
eprintln!("[error] Error removing host: {} {}", resp.errors[0].code, resp.errors[0].message);
std::process::exit(1);
}
Ok(())
}
#[derive(Serialize, Deserialize)]
pub struct HostUpdateBody {
#[serde(rename = "listenPort")]
pub listen_port: u16,
#[serde(rename = "staticAddresses")]
pub static_addresses: Vec<SocketAddrV4>,
pub name: Option<String>,
pub ip: Option<Ipv4Addr>,
pub role: Option<String>
}
pub async fn update_host(id: String, listen_port: Option<u16>, static_address: Option<SocketAddrV4>, name: Option<String>, ip: Option<Ipv4Addr>, role: Option<String>, server: Url) -> Result<(), Box<dyn Error>> {
let client = reqwest::Client::new();
// load session token
let sess_token_store = dirs::config_dir().unwrap().join("tfcli-session.token");
let session_token = fs::read_to_string(&sess_token_store)?;
let auth_token_store = dirs::config_dir().unwrap().join("tfcli-auth.token");
let auth_token = fs::read_to_string(&auth_token_store)?;
let token = format!("{} {}", session_token, auth_token);
let res = client.put(server.join(&format!("/v1/hosts/{}?extension=extended_hosts", id))?).json(&HostUpdateBody {
listen_port: listen_port.unwrap_or(0),
static_addresses: static_address.map_or_else(|| vec![], |u| vec![u]),
name,
ip,
role
}).bearer_auth(token).send().await?;
if res.status().is_success() {
let host: Host = res.json::<HostGetResponse>().await?.data;
println!(" Host: {} ({})", host.name, host.id);
println!(" Organization: {}", host.organization_id);
println!(" Network: {}", host.network_id);
println!(" Role: {}", host.role_id);
println!(" IP Address: {}", host.ip_address);
println!(" Static Addresses: {}", host.static_addresses.iter().map(|u| u.to_string()).collect::<Vec<_>>().join(", "));
println!(" Listen Port: {}", host.listen_port);
println!(" Type: {}", if host.is_lighthouse { "Lighthouse" } else if host.is_relay { "Relay" } else { "Host" } );
println!(" Blocked: {}", host.is_blocked);
println!(" Last Seen: {}", host.metadata.last_seen_at);
println!(" Client Version: {}", host.metadata.version);
println!(" Platform: {}", host.metadata.platform);
println!("Client Update Available: {}", host.metadata.update_available);
println!(" Created: {}", host.created_at);
println!();
} else {
let resp: APIErrorResponse = res.json().await?;
eprintln!("[error] Error updating host: {} {}", resp.errors[0].code, resp.errors[0].message);
std::process::exit(1);
}
Ok(())
}
#[derive(Serialize, Deserialize)]
pub struct EnrollmentCodeResponseMetadata {}
#[derive(Serialize, Deserialize)]
pub struct EnrollmentCode {
pub code: String,
#[serde(rename = "lifetimeSeconds")]
pub lifetime_seconds: i64,
}
#[derive(Serialize, Deserialize)]
pub struct EnrollmentResponseData {
#[serde(rename = "enrollmentCode")]
pub enrollment_code: EnrollmentCode,
}
#[derive(Serialize, Deserialize)]
pub struct EnrollmentResponse {
pub data: EnrollmentResponseData,
pub metadata: EnrollmentCodeResponseMetadata,
}
pub async fn enroll_host(id: String, server: Url) -> Result<(), Box<dyn Error>> {
let client = reqwest::Client::new();
// load session token
let sess_token_store = dirs::config_dir().unwrap().join("tfcli-session.token");
let session_token = fs::read_to_string(&sess_token_store)?;
let auth_token_store = dirs::config_dir().unwrap().join("tfcli-auth.token");
let auth_token = fs::read_to_string(&auth_token_store)?;
let token = format!("{} {}", session_token, auth_token);
let res = client.post(server.join(&format!("/v1/hosts/{}/enrollment-code", id))?).header("content-length", 0).bearer_auth(token).send().await?;
if res.status().is_success() {
let resp: EnrollmentResponse = res.json().await?;
println!("Enrollment code generated. Enroll the host with the following code: {}", resp.data.enrollment_code.code);
println!("This code will be valid for {} seconds, at which point you will need to generate a new code", resp.data.enrollment_code.lifetime_seconds);
println!("If this host is blocked, a successful re-enrollment will unblock it.");
} else {
let resp: APIErrorResponse = res.json().await?;
eprintln!("[error] Error blocking host: {} {}", resp.errors[0].code, resp.errors[0].message);
std::process::exit(1);
}
Ok(())
}
pub async fn block_host(id: String, server: Url) -> Result<(), Box<dyn Error>> {
let client = reqwest::Client::new();
// load session token
let sess_token_store = dirs::config_dir().unwrap().join("tfcli-session.token");
let session_token = fs::read_to_string(&sess_token_store)?;
let auth_token_store = dirs::config_dir().unwrap().join("tfcli-auth.token");
let auth_token = fs::read_to_string(&auth_token_store)?;
let token = format!("{} {}", session_token, auth_token);
let res = client.post(server.join(&format!("/v1/hosts/{}/block", id))?).bearer_auth(token).send().await?;
if res.status().is_success() {
println!("Host blocked. To unblock it, re-enroll the host.");
} else {
let resp: APIErrorResponse = res.json().await?;
eprintln!("[error] Error blocking host: {} {}", resp.errors[0].code, resp.errors[0].message);
std::process::exit(1);
}
Ok(())
}
#[derive(Serialize, Deserialize)]
pub struct HostConfigOverrideResponse {
pub data: HostConfigOverrideData
}
#[derive(Serialize, Deserialize)]
pub struct HostConfigOverrideData {
pub overrides: Vec<HostConfigOverrideDataOverride>
}
#[derive(Serialize, Deserialize)]
pub struct HostConfigOverrideDataOverride {
pub key: String,
pub value: HostConfigOverrideDataOverrideValue
}
#[derive(Serialize, Deserialize)]
#[serde(untagged)]
pub enum HostConfigOverrideDataOverrideValue {
Boolean(bool),
Numeric(i64),
Other(String)
}
pub async fn list_overrides(id: String, server: Url) -> Result<(), Box<dyn Error>> {
let client = reqwest::Client::new();
// load session token
let sess_token_store = dirs::config_dir().unwrap().join("tfcli-session.token");
let session_token = fs::read_to_string(&sess_token_store)?;
let auth_token_store = dirs::config_dir().unwrap().join("tfcli-auth.token");
let auth_token = fs::read_to_string(&auth_token_store)?;
let token = format!("{} {}", session_token, auth_token);
let res = client.get(server.join(&format!("/v1/hosts/{}/config-overrides", id))?).bearer_auth(token).send().await?;
if res.status().is_success() {
let resp: HostConfigOverrideResponse = res.json().await?;
for c_override in &resp.data.overrides {
println!(" Key: {}", c_override.key);
println!("Value: {}", match &c_override.value {
HostConfigOverrideDataOverrideValue::Boolean(v) => format!("bool:{}", v),
HostConfigOverrideDataOverrideValue::Numeric(v) => format!("numeric:{}", v),
HostConfigOverrideDataOverrideValue::Other(v) => format!("string:{}", v)
});
}
if resp.data.overrides.is_empty() {
println!("No overrides found");
}
} else {
let resp: APIErrorResponse = res.json().await?;
eprintln!("[error] Error looking up config overrides: {} {}", resp.errors[0].code, resp.errors[0].message);
std::process::exit(1);
}
Ok(())
}
#[derive(Serialize, Deserialize)]
pub struct SetOverrideRequest {
pub overrides: Vec<HostConfigOverrideDataOverride>
}
pub async fn set_override(id: String, key: String, boolean: Option<bool>, numeric: Option<i64>, other: Option<String>, server: Url) -> Result<(), Box<dyn Error>> {
if boolean.is_none() && numeric.is_none() && other.is_none() {
eprintln!("[error] no value provided: you must provide at least --boolean, --numeric, or --string");
std::process::exit(1);
} else if boolean.is_some() && numeric.is_some() || boolean.is_some() && other.is_some() || numeric.is_some() && other.is_some() {
eprintln!("[error] multiple values provided: you must provide only one of --boolean, --numeric, or --string");
std::process::exit(1);
}
let val;
if let Some(v) = boolean {
val = HostConfigOverrideDataOverrideValue::Boolean(v);
} else if let Some(v) = numeric {
val = HostConfigOverrideDataOverrideValue::Numeric(v);
} else if let Some(v) = other {
val = HostConfigOverrideDataOverrideValue::Other(v);
} else {
unreachable!();
}
let client = reqwest::Client::new();
// load session token
let sess_token_store = dirs::config_dir().unwrap().join("tfcli-session.token");
let session_token = fs::read_to_string(&sess_token_store)?;
let auth_token_store = dirs::config_dir().unwrap().join("tfcli-auth.token");
let auth_token = fs::read_to_string(&auth_token_store)?;
let token = format!("{} {}", session_token, auth_token);
let res = client.get(server.join(&format!("/v1/hosts/{}/config-overrides", id))?).bearer_auth(token.clone()).send().await?;
if res.status().is_success() {
let resp: HostConfigOverrideResponse = res.json().await?;
let mut others: Vec<HostConfigOverrideDataOverride> = vec![];
for c_override in resp.data.overrides {
if c_override.key != key {
others.push(c_override);
}
}
others.push(HostConfigOverrideDataOverride {
key,
value: val,
});
let res = client.put(server.join(&format!("/v1/hosts/{}/config-overrides", id))?).bearer_auth(token.clone()).json(&SetOverrideRequest {
overrides: others,
}).send().await?;
if res.status().is_success() {
let resp: HostConfigOverrideResponse = res.json().await?;
for c_override in &resp.data.overrides {
println!(" Key: {}", c_override.key);
println!("Value: {}", match &c_override.value {
HostConfigOverrideDataOverrideValue::Boolean(v) => format!("bool:{}", v),
HostConfigOverrideDataOverrideValue::Numeric(v) => format!("numeric:{}", v),
HostConfigOverrideDataOverrideValue::Other(v) => format!("string:{}", v)
});
}
if resp.data.overrides.is_empty() {
println!("No overrides found");
}
println!("Override set successfully");
} else {
let resp: APIErrorResponse = res.json().await?;
eprintln!("[error] Error setting config overrides: {} {}", resp.errors[0].code, resp.errors[0].message);
std::process::exit(1);
}
} else {
let resp: APIErrorResponse = res.json().await?;
eprintln!("[error] Error setting config overrides: {} {}", resp.errors[0].code, resp.errors[0].message);
std::process::exit(1);
}
Ok(())
}
pub async fn unset_override(id: String, key: String, server: Url) -> Result<(), Box<dyn Error>> {
let client = reqwest::Client::new();
// load session token
let sess_token_store = dirs::config_dir().unwrap().join("tfcli-session.token");
let session_token = fs::read_to_string(&sess_token_store)?;
let auth_token_store = dirs::config_dir().unwrap().join("tfcli-auth.token");
let auth_token = fs::read_to_string(&auth_token_store)?;
let token = format!("{} {}", session_token, auth_token);
let res = client.get(server.join(&format!("/v1/hosts/{}/config-overrides", id))?).bearer_auth(token.clone()).send().await?;
if res.status().is_success() {
let resp: HostConfigOverrideResponse = res.json().await?;
let mut others: Vec<HostConfigOverrideDataOverride> = vec![];
for c_override in resp.data.overrides {
if c_override.key != key {
others.push(c_override);
}
}
let res = client.put(server.join(&format!("/v1/hosts/{}/config-overrides", id))?).bearer_auth(token.clone()).json(&SetOverrideRequest {
overrides: others,
}).send().await?;
if res.status().is_success() {
let resp: HostConfigOverrideResponse = res.json().await?;
for c_override in &resp.data.overrides {
println!(" Key: {}", c_override.key);
println!("Value: {}", match &c_override.value {
HostConfigOverrideDataOverrideValue::Boolean(v) => format!("bool:{}", v),
HostConfigOverrideDataOverrideValue::Numeric(v) => format!("numeric:{}", v),
HostConfigOverrideDataOverrideValue::Other(v) => format!("string:{}", v)
});
}
if resp.data.overrides.is_empty() {
println!("No overrides found");
}
println!("Override unset successfully");
} else {
let resp: APIErrorResponse = res.json().await?;
eprintln!("[error] Error unsetting config overrides: {} {}", resp.errors[0].code, resp.errors[0].message);
std::process::exit(1);
}
} else {
let resp: APIErrorResponse = res.json().await?;
eprintln!("[error] Error unsetting config overrides: {} {}", resp.errors[0].code, resp.errors[0].message);
std::process::exit(1);
}
Ok(())
}