focl 0.1.0

focl/focld - lightweight Rust BGP speaker
Documentation
use std::path::PathBuf;
use std::process::Stdio;

use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
use focl::types::{ControlRequest, ControlResponse};
use serde_json::json;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::net::UnixStream;

#[derive(Debug, Parser)]
#[command(name = "focl", about = "CLI for focld control plane")]
struct Cli {
    #[arg(long, default_value = "/tmp/focld.sock")]
    socket: PathBuf,

    #[command(subcommand)]
    command: Commands,
}

#[derive(Debug, Subcommand)]
enum Commands {
    Start {
        #[arg(short, long, default_value = "focl.toml")]
        config: PathBuf,
    },
    Stop,
    Reload,
    Peer {
        #[command(subcommand)]
        command: PeerCommands,
    },
    Rib {
        #[command(subcommand)]
        command: RibCommands,
    },
    Archive {
        #[command(subcommand)]
        command: ArchiveCommands,
    },
}

#[derive(Debug, Subcommand)]
enum PeerCommands {
    List,
    Show { peer: String },
    Reset { peer: String },
}

#[derive(Debug, Subcommand)]
enum RibCommands {
    Summary,
    In { peer: String },
    Out { peer: String },
}

#[derive(Debug, Subcommand)]
enum ArchiveCommands {
    Status,
    Rollover {
        #[arg(long, value_parser = ["updates", "ribs"])]
        stream: String,
    },
    Snapshot,
    Destinations,
    Retry,
}

#[tokio::main]
async fn main() -> Result<()> {
    let cli = Cli::parse();

    match cli.command {
        Commands::Start { config } => {
            let focld_bin = locate_focld_binary()?;
            let child = std::process::Command::new(focld_bin)
                .arg("--config")
                .arg(config)
                .stdin(Stdio::null())
                .stdout(Stdio::null())
                .stderr(Stdio::null())
                .spawn()
                .context("failed spawning focld")?;
            println!("{{\"started\":true,\"pid\":{}}}", child.id());
        }
        Commands::Stop => {
            let response = send_control_request(&cli.socket, "shutdown", json!({})).await?;
            print_response(response);
        }
        Commands::Reload => {
            let response = send_control_request(&cli.socket, "reload", json!({})).await?;
            print_response(response);
        }
        Commands::Peer { command } => match command {
            PeerCommands::List => {
                let response = send_control_request(&cli.socket, "peer_list", json!({})).await?;
                print_response(response);
            }
            PeerCommands::Show { peer } => {
                let response =
                    send_control_request(&cli.socket, "peer_show", json!({"peer": peer})).await?;
                print_response(response);
            }
            PeerCommands::Reset { peer } => {
                let response =
                    send_control_request(&cli.socket, "peer_reset", json!({"peer": peer})).await?;
                print_response(response);
            }
        },
        Commands::Rib { command } => match command {
            RibCommands::Summary => {
                let response = send_control_request(&cli.socket, "rib_summary", json!({})).await?;
                print_response(response);
            }
            RibCommands::In { peer } => {
                let response =
                    send_control_request(&cli.socket, "rib_in", json!({"peer": peer})).await?;
                print_response(response);
            }
            RibCommands::Out { peer } => {
                let response =
                    send_control_request(&cli.socket, "rib_out", json!({"peer": peer})).await?;
                print_response(response);
            }
        },
        Commands::Archive { command } => match command {
            ArchiveCommands::Status => {
                let response =
                    send_control_request(&cli.socket, "archive_status", json!({})).await?;
                print_response(response);
            }
            ArchiveCommands::Rollover { stream } => {
                let response = send_control_request(
                    &cli.socket,
                    "archive_rollover",
                    json!({"stream": stream}),
                )
                .await?;
                print_response(response);
            }
            ArchiveCommands::Snapshot => {
                let response =
                    send_control_request(&cli.socket, "archive_snapshot_now", json!({})).await?;
                print_response(response);
            }
            ArchiveCommands::Destinations => {
                let response =
                    send_control_request(&cli.socket, "archive_destinations", json!({})).await?;
                print_response(response);
            }
            ArchiveCommands::Retry => {
                let response =
                    send_control_request(&cli.socket, "archive_replicator_retry", json!({}))
                        .await?;
                print_response(response);
            }
        },
    }

    Ok(())
}

fn locate_focld_binary() -> Result<PathBuf> {
    let current = std::env::current_exe().context("failed resolving current executable")?;
    let sibling = current.with_file_name("focld");
    if sibling.exists() {
        return Ok(sibling);
    }
    Ok(PathBuf::from("focld"))
}

async fn send_control_request(
    socket: &PathBuf,
    cmd: &str,
    args: serde_json::Value,
) -> Result<ControlResponse> {
    let mut stream = UnixStream::connect(socket)
        .await
        .with_context(|| format!("failed connecting to {}", socket.display()))?;

    let req = ControlRequest {
        version: 1,
        id: uuid_like_id(),
        cmd: cmd.to_string(),
        args,
    };

    let payload = serde_json::to_string(&req)?;
    stream.write_all(payload.as_bytes()).await?;
    stream.write_all(b"\n").await?;

    let mut reader = BufReader::new(stream);
    let mut line = String::new();
    reader.read_line(&mut line).await?;

    let response: ControlResponse = serde_json::from_str(line.trim_end())?;
    Ok(response)
}

fn uuid_like_id() -> String {
    format!(
        "req-{}-{}",
        std::process::id(),
        chrono::Utc::now().timestamp_nanos_opt().unwrap_or_default()
    )
}

fn print_response(response: ControlResponse) {
    println!(
        "{}",
        serde_json::to_string_pretty(&response).unwrap_or_else(|_| "{}".to_string())
    );
}