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 Doctor,
17 Create(CreateArgs),
19 Start(NameArgs),
21 Stop(NameArgs),
23 Delete(NameArgs),
25 List,
27 Info(InfoArgs),
29 SshConfig(SshConfigArgs),
31 Ssh(SshArgs),
33 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,
73 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}