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 Doctor,
17 Image(ImageArgs),
19 Create(CreateArgs),
21 Start(NameArgs),
23 Stop(NameArgs),
25 Delete(NameArgs),
27 List,
29 Info(InfoArgs),
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 ImageArgs {
44 #[command(subcommand)]
45 pub command: ImageCommand,
46}
47
48#[derive(Debug, Clone, Subcommand)]
49pub enum ImageCommand {
50 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}