Skip to content

Create server CLI interface (bssh-server binary) #131

@inureyes

Description

@inureyes

Summary

Create the bssh-server binary with a command-line interface for starting and managing the SSH server.

Parent Epic

Implementation Details

1. Binary Entry Point

// src/bin/bssh_server.rs
use clap::{Parser, Subcommand};
use bssh::server::{BsshServer, config::ServerConfig};

#[derive(Parser)]
#[command(name = "bssh-server")]
#[command(about = "Backend.AI SSH Server - A lightweight SSH server for containers")]
#[command(version)]
pub struct Cli {
    #[command(subcommand)]
    command: Option<Commands>,

    /// Configuration file path
    #[arg(short, long, global = true)]
    config: Option<PathBuf>,

    /// Bind address
    #[arg(short = 'b', long, global = true)]
    bind_address: Option<String>,

    /// Port to listen on
    #[arg(short, long, global = true)]
    port: Option<u16>,

    /// Host key file(s)
    #[arg(short = 'k', long = "host-key", global = true)]
    host_keys: Vec<PathBuf>,

    /// Verbosity level (-v, -vv, -vvv)
    #[arg(short, long, action = clap::ArgAction::Count, global = true)]
    verbose: u8,

    /// Run in foreground (don't daemonize)
    #[arg(short = 'D', long, global = true)]
    foreground: bool,

    /// PID file path
    #[arg(long, global = true)]
    pid_file: Option<PathBuf>,
}

#[derive(Subcommand)]
enum Commands {
    /// Start the SSH server (default)
    Run,

    /// Generate a configuration file template
    GenConfig {
        /// Output path (stdout if not specified)
        #[arg(short, long)]
        output: Option<PathBuf>,
    },

    /// Hash a password for configuration
    HashPassword,

    /// Check configuration file for errors
    CheckConfig,

    /// Generate host keys
    GenHostKey {
        /// Key type (ed25519 or rsa)
        #[arg(short = 't', long, default_value = "ed25519")]
        key_type: String,

        /// Output file path
        #[arg(short, long)]
        output: PathBuf,

        /// RSA key bits (only for rsa type)
        #[arg(short = 'b', long, default_value = "4096")]
        bits: u32,
    },

    /// Show version and build information
    Version,
}

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

    // Initialize logging
    bssh::utils::logging::init_logging_console_only(cli.verbose);

    match cli.command {
        None | Some(Commands::Run) => run_server(cli).await,
        Some(Commands::GenConfig { output }) => gen_config(output),
        Some(Commands::HashPassword) => hash_password(),
        Some(Commands::CheckConfig) => check_config(&cli),
        Some(Commands::GenHostKey { key_type, output, bits }) => {
            gen_host_key(&key_type, &output, bits)
        }
        Some(Commands::Version) => show_version(),
    }
}

async fn run_server(cli: Cli) -> anyhow::Result<()> {
    // Load configuration
    let config = bssh::server::config::load_config(
        cli.config.as_deref(),
        &cli.into(),
    )?;

    tracing::info!(
        "Starting bssh-server on {}:{}",
        config.server.bind_address,
        config.server.port
    );

    // Create and run server
    let server = BsshServer::new(config)?;
    
    // Setup signal handlers
    let shutdown = setup_signal_handlers()?;

    // Run server with graceful shutdown
    tokio::select! {
        result = server.run() => {
            result?;
        }
        _ = shutdown => {
            tracing::info!("Received shutdown signal");
        }
    }

    tracing::info!("Server stopped");
    Ok(())
}

fn gen_config(output: Option<PathBuf>) -> anyhow::Result<()> {
    let template = bssh::server::config::generate_config_template();
    
    if let Some(path) = output {
        std::fs::write(&path, &template)?;
        println!("Configuration template written to {:?}", path);
    } else {
        println!("{}", template);
    }
    
    Ok(())
}

fn hash_password() -> anyhow::Result<()> {
    bssh::server::auth::password::hash_password_cli()
}

fn check_config(cli: &Cli) -> anyhow::Result<()> {
    let config = bssh::server::config::load_config(
        cli.config.as_deref(),
        &cli.into(),
    )?;
    
    println!("Configuration is valid");
    println!("  Bind: {}:{}", config.server.bind_address, config.server.port);
    println!("  Host keys: {:?}", config.server.host_keys);
    println!("  Auth methods: {:?}", config.auth.methods);
    
    Ok(())
}

fn gen_host_key(key_type: &str, output: &Path, bits: u32) -> anyhow::Result<()> {
    use bssh::keygen;
    
    match key_type {
        "ed25519" => keygen::generate_ed25519(output)?,
        "rsa" => keygen::generate_rsa(output, bits)?,
        _ => anyhow::bail!("Unknown key type: {}. Use 'ed25519' or 'rsa'", key_type),
    }
    
    println!("Host key generated: {:?}", output);
    Ok(())
}

fn show_version() -> anyhow::Result<()> {
    println!("bssh-server {}", env!("CARGO_PKG_VERSION"));
    println!("Rust SSH server for containers");
    println!();
    println!("Build info:");
    println!("  rustc: {}", rustc_version_runtime::version());
    println!("  target: {}", env!("TARGET"));
    Ok(())
}

async fn setup_signal_handlers() -> anyhow::Result<impl std::future::Future<Output = ()>> {
    use tokio::signal;
    
    let ctrl_c = async {
        signal::ctrl_c().await.expect("Failed to install Ctrl+C handler");
    };
    
    #[cfg(unix)]
    let terminate = async {
        signal::unix::signal(signal::unix::SignalKind::terminate())
            .expect("Failed to install SIGTERM handler")
            .recv()
            .await;
    };
    
    #[cfg(not(unix))]
    let terminate = std::future::pending::<()>();
    
    Ok(async {
        tokio::select! {
            _ = ctrl_c => {}
            _ = terminate => {}
        }
    })
}

2. Update Cargo.toml

[[bin]]
name = "bssh-server"
path = "src/bin/bssh_server.rs"

[dependencies]
rustc_version_runtime = "0.3"  # For version info

3. CLI Help Output

bssh-server 1.0.0
Backend.AI SSH Server - A lightweight SSH server for containers

USAGE:
    bssh-server [OPTIONS] [COMMAND]

COMMANDS:
    run          Start the SSH server (default)
    gen-config   Generate a configuration file template
    hash-password Hash a password for configuration
    check-config Check configuration file for errors
    gen-host-key Generate host keys
    version      Show version and build information
    help         Print this message or the help of the given subcommand(s)

OPTIONS:
    -c, --config <FILE>         Configuration file path
    -b, --bind-address <ADDR>   Bind address [default: 0.0.0.0]
    -p, --port <PORT>           Port to listen on [default: 22]
    -k, --host-key <FILE>       Host key file(s)
    -v, --verbose...            Verbosity level
    -D, --foreground            Run in foreground
        --pid-file <FILE>       PID file path
    -h, --help                  Print help
    -V, --version               Print version

Files to Create/Modify

File Action
src/bin/bssh_server.rs Create - Server binary
Cargo.toml Modify - Add binary target

Testing Requirements

  1. CLI help output verification
  2. Config generation command
  3. Config check command
  4. Signal handling (SIGTERM, SIGINT)
# Test CLI
bssh-server --help
bssh-server gen-config > test-config.yaml
bssh-server check-config -c test-config.yaml
bssh-server hash-password
bssh-server gen-host-key -t ed25519 -o /tmp/test_host_key

Acceptance Criteria

  • Binary compiles and runs
  • Clap-based CLI with subcommands
  • Configuration file loading
  • CLI argument overrides
  • Signal handling for graceful shutdown
  • gen-config command works
  • hash-password command works
  • check-config command works
  • gen-host-key command works
  • Proper exit codes
  • Tests passing

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions