416 lines
16 KiB
Rust
416 lines
16 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};
|
||
|
|
||
|
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
|
||
|
}
|
||
|
}
|
||
|
|
||
|
#[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")?).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))?).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(())
|
||
|
}
|