1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
//! The command line's grammar.
use std::path::PathBuf;
use clap::{Args, Parser, Subcommand, ValueEnum};
/// Runs what Isoloom generates: the run manifest next to a target's files
/// (`<lab>/.isoloom/<target>/run.json`).
#[derive(Debug, Parser)]
#[command(name = "isothrum", version, about)]
pub(crate) struct Cli {
/// What to do.
#[command(subcommand)]
pub(crate) command: Command,
/// The runner's home, where runs are recorded (default: `$ISOTHRUM_HOME`, else `~/.isothrum`).
#[arg(long, global = true, value_name = "DIR")]
pub(crate) home: Option<PathBuf>,
/// Print the events as JSON, one per line, on standard output.
#[arg(long, global = true)]
pub(crate) json: bool,
}
/// The operations.
#[derive(Debug, Subcommand)]
pub(crate) enum Command {
/// Start the lab (a parked run is resumed; leftovers are removed first).
Up {
#[command(flatten)]
op: OpArgs,
/// Remove whatever of the run exists, then start afresh.
#[arg(long)]
replace: bool,
/// How long a run that stops itself (a cloud lab) may live, in minutes (default: its
/// manifest's).
#[arg(long, value_name = "MINUTES")]
lifetime: Option<u64>,
},
/// Remove everything the run created.
Down(OpArgs),
/// Stop the run, keeping what it created.
Park {
#[command(flatten)]
lab: LabArgs,
/// `pause` or `shutdown`.
#[arg(long, value_enum, default_value_t = Mode::Shutdown)]
mode: Mode,
},
/// Start a parked run again.
Resume(OpArgs),
/// Show the run: its state, machines, outputs and message.
Status(LabArgs),
/// Run the lab's checks (exit 1 when one fails).
Check(OpArgs),
/// Run the set-up again on the running machines (a VM lab's provisioners).
Provision {
#[command(flatten)]
op: OpArgs,
/// Only this machine.
#[arg(long)]
machine: Option<String>,
},
/// Print the run's recorded events, oldest first.
Events(LabArgs),
/// Destroy a cloud's runs past their expiry (each with the params it was applied with), then
/// list what the cloud still holds past its expiry that no run records. Exit 1 when a destroy
/// failed or something is left.
Reap(ReapArgs),
/// Print the runner's SSH public key (made when it has none): authorize it where a run's SSH
/// goes through (a Proxmox node, with `--ssh-jump`).
Key,
}
/// A cloud.
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub(crate) enum CloudName {
/// Amazon Web Services.
Aws,
/// Microsoft Azure.
Azure,
/// Google Cloud.
Gcp,
/// DigitalOcean.
Digitalocean,
/// Akamai Linode.
Linode,
/// Oracle Cloud Infrastructure.
Oci,
}
impl CloudName {
/// As the command line writes it (`digitalocean`).
pub(crate) fn id(self) -> String {
self.to_possible_value().map(|v| v.get_name().to_owned()).unwrap_or_default()
}
}
impl From<CloudName> for isothrum_runner::isoloom_manifest::Cloud {
fn from(c: CloudName) -> Self {
use isothrum_runner::isoloom_manifest::Cloud;
match c {
CloudName::Aws => Cloud::Aws,
CloudName::Azure => Cloud::Azure,
CloudName::Gcp => Cloud::Gcp,
CloudName::Digitalocean => Cloud::Digitalocean,
CloudName::Linode => Cloud::Linode,
CloudName::Oci => Cloud::Oci,
}
}
}
/// What `reap` sweeps.
#[derive(Debug, Args)]
pub(crate) struct ReapArgs {
/// The cloud.
#[arg(value_enum)]
pub(crate) cloud: CloudName,
/// Say what would be destroyed, destroying nothing.
#[arg(long)]
pub(crate) dry_run: bool,
/// An AWS region to look for leftovers in (repeatable; default: the AWS command line's).
#[arg(long = "region", value_name = "REGION")]
pub(crate) regions: Vec<String>,
/// The Google Cloud project to look for leftovers in (default: gcloud's).
#[arg(long, value_name = "PROJECT")]
pub(crate) project: Option<String>,
/// A connection slot's value (the account's keys), read from an environment variable
/// (default: the slot's own name). Repeatable. Values never go on the command line.
#[arg(long = "secret-from-env", value_name = "NAME[=VAR]")]
pub(crate) secrets: Vec<String>,
}
/// How a run is parked.
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub(crate) enum Mode {
/// Suspended where the target can.
Pause,
/// Shut down.
Shutdown,
}
/// Which run.
#[derive(Debug, Args)]
pub(crate) struct LabArgs {
/// The lab's folder (its `.isoloom/<target>/run.json` is read), or a run.json.
#[arg(default_value = ".")]
pub(crate) lab: PathBuf,
/// The target whose manifest to run, when the lab was generated for several.
#[arg(long)]
pub(crate) target: Option<String>,
/// The run's name (default: the lab's name).
#[arg(long)]
pub(crate) name: Option<String>,
}
/// Which run, and the values an operation runs with.
#[derive(Debug, Args)]
pub(crate) struct OpArgs {
#[command(flatten)]
pub(crate) lab: LabArgs,
/// A param's value (repeatable).
#[arg(long = "param", value_name = "NAME=VALUE")]
pub(crate) params: Vec<String>,
/// A secret or input slot's value, read from an environment variable (default: the slot's
/// own name). Repeatable. Values never go on the command line.
#[arg(long = "secret-from-env", value_name = "NAME[=VAR]")]
pub(crate) secrets: Vec<String>,
/// What to call the place the run is on (recorded; a later operation elsewhere is refused).
#[arg(long, value_name = "LABEL")]
pub(crate) host_label: Option<String>,
/// The Vagrant provider a VM lab runs on (`virtualbox`, `libvirt`, `hyperv`, …); a run keeps
/// the one it was started with.
#[arg(long, value_name = "PROVIDER")]
pub(crate) provider: Option<String>,
/// A login (`user@host`) the run's SSH goes through first: a Proxmox node whose lab bridge
/// this machine can't route to. Recorded at `up`.
#[arg(long, value_name = "USER@HOST")]
pub(crate) ssh_jump: Option<String>,
}
#[cfg(test)]
mod tests {
use clap::CommandFactory;
use super::*;
#[test]
fn the_grammar_is_consistent() {
Cli::command().debug_assert();
}
#[test]
fn options_parse_after_the_lab() {
let cli = Cli::parse_from([
"isothrum",
"up",
"labs/flag",
"--param",
"ISOLOOM_PUBLISH_FIXED=1",
"--secret-from-env",
"CTF_FLAG_MAIN",
"--provider",
"virtualbox",
"--json",
]);
let Command::Up { op, replace, .. } = cli.command else { panic!("{cli:?}") };
assert_eq!(op.provider.as_deref(), Some("virtualbox"));
assert_eq!(
(op.lab.lab, op.params, op.secrets, replace, cli.json),
(
"labs/flag".into(),
vec!["ISOLOOM_PUBLISH_FIXED=1".to_owned()],
vec!["CTF_FLAG_MAIN".to_owned()],
false,
true
)
);
let park = Cli::parse_from(["isothrum", "park", "--mode", "pause", "--home", "/tmp/h"]);
assert!(matches!(park.command, Command::Park { mode: Mode::Pause, .. }) && park.home == Some("/tmp/h".into()));
}
}