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 = "hp")]
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    /// Manage cached Ubuntu cloud images.
18    Image(ImageArgs),
19    /// Create a named VM.
20    Create(CreateArgs),
21    /// Start a named VM.
22    Start(NameArgs),
23    /// Gracefully stop a named VM.
24    Stop(NameArgs),
25    /// Stop and remove a named VM.
26    Delete(NameArgs),
27    /// List known VMs.
28    List,
29    /// Show details for a named VM.
30    Info(InfoArgs),
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 ImageArgs {
44    #[command(subcommand)]
45    pub command: ImageCommand,
46}
47
48#[derive(Debug, Clone, Subcommand)]
49pub enum ImageCommand {
50    /// Download and verify a cloud image into the local cache.
51    Prefetch(PrefetchImageArgs),
52}
53
54#[derive(Debug, Clone, ClapArgs)]
55pub struct PrefetchImageArgs {
56    #[arg(long)]
57    pub release: Option<String>,
58    #[arg(long, value_enum)]
59    pub arch: Option<GuestArch>,
60}
61
62#[derive(Debug, Clone, ClapArgs)]
63pub struct InfoArgs {
64    pub name: String,
65    #[arg(long)]
66    pub json: bool,
67}
68
69#[derive(Debug, Clone, ClapArgs)]
70pub struct SshArgs {
71    pub name: String,
72    #[arg(last = true)]
73    pub ssh_args: Vec<String>,
74}
75
76#[derive(Debug, Clone, ClapArgs)]
77pub struct ExecArgs {
78    pub name: String,
79    #[arg(required = true, last = true)]
80    pub command: Vec<String>,
81}
82
83#[derive(Debug, Clone, ClapArgs)]
84pub struct CreateArgs {
85    pub name: String,
86    #[arg(long)]
87    pub release: Option<String>,
88    #[arg(long, value_enum)]
89    pub arch: Option<GuestArch>,
90    #[arg(long, value_enum)]
91    pub accel: Option<AccelMode>,
92    #[arg(long)]
93    pub cpus: Option<u8>,
94    #[arg(long)]
95    pub memory_mib: Option<u32>,
96    #[arg(long)]
97    pub disk_gib: Option<u32>,
98    #[arg(long)]
99    pub ssh_key: Option<String>,
100    #[arg(long = "forward", value_parser = parse_forward)]
101    pub forwards: Vec<(u16, u16)>,
102    #[arg(long)]
103    pub timeout_secs: Option<u64>,
104    #[arg(long)]
105    pub cloud_init_user_data: Option<String>,
106    #[arg(long)]
107    pub cloud_init_network_config: Option<String>,
108}
109
110fn parse_forward(value: &str) -> Result<(u16, u16), String> {
111    let (host, guest) = value
112        .split_once(':')
113        .ok_or_else(|| "forward must be HOST:GUEST".to_string())?;
114    let host = host
115        .parse::<u16>()
116        .map_err(|_| format!("invalid host port: {host}"))?;
117    let guest = guest
118        .parse::<u16>()
119        .map_err(|_| format!("invalid guest port: {guest}"))?;
120    Ok((host, guest))
121}
122
123#[cfg(test)]
124mod tests {
125    use clap::Parser;
126
127    use super::{Args, Command, ImageCommand};
128    use crate::state::{AccelMode, GuestArch};
129
130    #[test]
131    fn parses_create_command() {
132        let args = Args::parse_from([
133            "hp",
134            "create",
135            "dev",
136            "--release",
137            "24.04",
138            "--arch",
139            "arm64",
140            "--accel",
141            "tcg",
142            "--cpus",
143            "2",
144            "--memory-mib",
145            "2048",
146            "--disk-gib",
147            "12",
148            "--forward",
149            "8080:8080",
150        ]);
151        match args.command {
152            Command::Create(create) => {
153                assert_eq!(create.name, "dev");
154                assert_eq!(create.release.as_deref(), Some("24.04"));
155                assert_eq!(create.arch, Some(GuestArch::Arm64));
156                assert_eq!(create.accel, Some(AccelMode::Tcg));
157                assert_eq!(create.cpus, Some(2));
158                assert_eq!(create.memory_mib, Some(2048));
159                assert_eq!(create.disk_gib, Some(12));
160                assert_eq!(create.forwards, vec![(8080, 8080)]);
161            }
162            other => panic!("unexpected command: {other:?}"),
163        }
164    }
165
166    #[test]
167    fn parses_start_command() {
168        let args = Args::parse_from(["hp", "start", "dev"]);
169        match args.command {
170            Command::Start(start) => assert_eq!(start.name, "dev"),
171            other => panic!("unexpected command: {other:?}"),
172        }
173    }
174
175    #[test]
176    fn parses_image_prefetch_command() {
177        let args = Args::parse_from([
178            "hp",
179            "image",
180            "prefetch",
181            "--release",
182            "24.04",
183            "--arch",
184            "arm64",
185        ]);
186        match args.command {
187            Command::Image(image) => match image.command {
188                ImageCommand::Prefetch(prefetch) => {
189                    assert_eq!(prefetch.release.as_deref(), Some("24.04"));
190                    assert_eq!(prefetch.arch, Some(GuestArch::Arm64));
191                }
192            },
193            other => panic!("unexpected command: {other:?}"),
194        }
195    }
196
197    #[test]
198    fn parses_exec_command() {
199        let args = Args::parse_from(["hp", "exec", "dev", "--", "uname", "-m"]);
200        match args.command {
201            Command::Exec(exec) => {
202                assert_eq!(exec.name, "dev");
203                assert_eq!(exec.command, vec!["uname", "-m"]);
204            }
205            other => panic!("unexpected command: {other:?}"),
206        }
207    }
208}