Skip to main content

hardpass/
cli.rs

1use clap::{Args as ClapArgs, Parser, Subcommand};
2
3use crate::state::{AccelMode, GuestArch};
4
5#[derive(Debug, Parser)]
6#[command(name = "hardpass")]
7#[command(about = "Manage local Ubuntu cloud-image VMs with QEMU")]
8pub struct Args {
9    #[command(subcommand)]
10    pub command: Command,
11}
12
13#[derive(Debug, Subcommand)]
14pub enum Command {
15    /// Check the local environment for required tools and firmware.
16    Doctor,
17    /// Create a named VM.
18    Create(CreateArgs),
19    /// Start a named VM.
20    Start(NameArgs),
21    /// Gracefully stop a named VM.
22    Stop(NameArgs),
23    /// Stop and remove a named VM.
24    Delete(NameArgs),
25    /// List known VMs.
26    List,
27    /// Show details for a named VM.
28    Info(InfoArgs),
29    /// Manage SSH config integration for Hardpass VMs.
30    SshConfig(SshConfigArgs),
31    /// Open an interactive SSH session to a running VM.
32    Ssh(SshArgs),
33    /// Execute a remote command over SSH.
34    Exec(ExecArgs),
35}
36
37#[derive(Debug, Clone, ClapArgs)]
38pub struct NameArgs {
39    pub name: String,
40}
41
42#[derive(Debug, Clone, ClapArgs)]
43pub struct InfoArgs {
44    pub name: String,
45    #[arg(long)]
46    pub json: bool,
47}
48
49#[derive(Debug, Clone, ClapArgs)]
50pub struct SshArgs {
51    pub name: String,
52    #[arg(last = true)]
53    pub ssh_args: Vec<String>,
54}
55
56#[derive(Debug, Clone, ClapArgs)]
57pub struct ExecArgs {
58    pub name: String,
59    #[arg(required = true, last = true)]
60    pub command: Vec<String>,
61}
62
63#[derive(Debug, Clone, ClapArgs)]
64pub struct SshConfigArgs {
65    #[command(subcommand)]
66    pub command: SshConfigCommand,
67}
68
69#[derive(Debug, Clone, Subcommand)]
70pub enum SshConfigCommand {
71    /// Install the managed Include block into ~/.ssh/config.
72    Install,
73    /// Rewrite the managed Hardpass SSH host entries.
74    Sync,
75}
76
77#[derive(Debug, Clone, ClapArgs)]
78pub struct CreateArgs {
79    pub name: String,
80    #[arg(long)]
81    pub release: Option<String>,
82    #[arg(long, value_enum)]
83    pub arch: Option<GuestArch>,
84    #[arg(long, value_enum)]
85    pub accel: Option<AccelMode>,
86    #[arg(long)]
87    pub cpus: Option<u8>,
88    #[arg(long)]
89    pub memory_mib: Option<u32>,
90    #[arg(long)]
91    pub disk_gib: Option<u32>,
92    #[arg(long)]
93    pub ssh_key: Option<String>,
94    #[arg(long = "forward", value_parser = parse_forward)]
95    pub forwards: Vec<(u16, u16)>,
96    #[arg(long)]
97    pub timeout_secs: Option<u64>,
98    #[arg(long)]
99    pub cloud_init_user_data: Option<String>,
100    #[arg(long)]
101    pub cloud_init_network_config: Option<String>,
102}
103
104fn parse_forward(value: &str) -> Result<(u16, u16), String> {
105    let (host, guest) = value
106        .split_once(':')
107        .ok_or_else(|| "forward must be HOST:GUEST".to_string())?;
108    let host = host
109        .parse::<u16>()
110        .map_err(|_| format!("invalid host port: {host}"))?;
111    let guest = guest
112        .parse::<u16>()
113        .map_err(|_| format!("invalid guest port: {guest}"))?;
114    Ok((host, guest))
115}
116
117#[cfg(test)]
118mod tests {
119    use clap::Parser;
120
121    use super::{Args, Command, SshConfigCommand};
122    use crate::state::{AccelMode, GuestArch};
123
124    #[test]
125    fn parses_create_command() {
126        let args = Args::parse_from([
127            "hardpass",
128            "create",
129            "dev",
130            "--release",
131            "24.04",
132            "--arch",
133            "arm64",
134            "--accel",
135            "tcg",
136            "--cpus",
137            "2",
138            "--memory-mib",
139            "2048",
140            "--disk-gib",
141            "12",
142            "--forward",
143            "8080:8080",
144        ]);
145        match args.command {
146            Command::Create(create) => {
147                assert_eq!(create.name, "dev");
148                assert_eq!(create.release.as_deref(), Some("24.04"));
149                assert_eq!(create.arch, Some(GuestArch::Arm64));
150                assert_eq!(create.accel, Some(AccelMode::Tcg));
151                assert_eq!(create.cpus, Some(2));
152                assert_eq!(create.memory_mib, Some(2048));
153                assert_eq!(create.disk_gib, Some(12));
154                assert_eq!(create.forwards, vec![(8080, 8080)]);
155            }
156            other => panic!("unexpected command: {other:?}"),
157        }
158    }
159
160    #[test]
161    fn parses_start_command() {
162        let args = Args::parse_from(["hardpass", "start", "dev"]);
163        match args.command {
164            Command::Start(start) => assert_eq!(start.name, "dev"),
165            other => panic!("unexpected command: {other:?}"),
166        }
167    }
168
169    #[test]
170    fn parses_exec_command() {
171        let args = Args::parse_from(["hardpass", "exec", "dev", "--", "uname", "-m"]);
172        match args.command {
173            Command::Exec(exec) => {
174                assert_eq!(exec.name, "dev");
175                assert_eq!(exec.command, vec!["uname", "-m"]);
176            }
177            other => panic!("unexpected command: {other:?}"),
178        }
179    }
180
181    #[test]
182    fn parses_ssh_config_install_command() {
183        let args = Args::parse_from(["hardpass", "ssh-config", "install"]);
184        match args.command {
185            Command::SshConfig(ssh_config) => match ssh_config.command {
186                SshConfigCommand::Install => {}
187                other => panic!("unexpected ssh-config command: {other:?}"),
188            },
189            other => panic!("unexpected command: {other:?}"),
190        }
191    }
192}