Skip to main content

isothrum_runner/run/
attach.rs

1//! Hot-adding the learner's attacker box into a running run, and hot-removing it. A lab no longer
2//! declares an access machine, so the box is stood up ad hoc on the running lab's network with no
3//! manifest or Compose machine behind it ([`crate::driver::Attacker::AdHoc`], Docker only).
4//!
5//! For backward compatibility, a lab that still carries an on-demand access machine (a manifest
6//! machine marked `on_demand`, which the initial `up` left down — a Compose `access` profile, a
7//! Vagrant `autostart: false`) keeps its old path: `attach` brings that machine up on a chosen
8//! network with the learner's picked image, `detach` takes it back out.
9
10use isoloom_manifest::{Machine, Native};
11use serde::Serialize;
12use serde_json::{Value, json};
13
14use super::Running;
15use super::operation::Done;
16use crate::Result;
17use crate::driver::{Attached, Attacker};
18use crate::error::Error;
19use crate::events::{Emitter, Events};
20use crate::options::RunOptions;
21use crate::secrets::sensitive_values;
22use crate::state::Op;
23use crate::status::{ShellCommand, any_target_running};
24
25/// What a hot-add produced: the attacker box brought up, and where to reach it.
26#[derive(Debug, Clone, Serialize)]
27pub struct AttachReport {
28    /// The box's name: the access machine's manifest name, or an ad-hoc box's container name.
29    pub machine: String,
30    /// Its native name (the Compose service / Vagrant define, or the ad-hoc container name).
31    pub native: String,
32    /// The lab network it is on (the chosen one, else its first).
33    pub network: Option<String>,
34    /// Its address on that network, for the learner to reach it.
35    pub address: Option<String>,
36    /// The image or box it was brought up with, when the learner picked one.
37    pub image: Option<String>,
38}
39
40/// Which attacker box a hot-add acts on: the manifest's on-demand access machine when the lab
41/// declares one, else an ad-hoc box with no machine (which the driver stands up itself).
42enum Target<'a> {
43    Machine(&'a Machine),
44    AdHoc,
45}
46
47impl Target<'_> {
48    /// How the driver addresses this box.
49    fn as_attacker(&self) -> Attacker<'_> {
50        match self {
51            Target::Machine(m) => Attacker::Machine(native_name(m)),
52            Target::AdHoc => Attacker::AdHoc,
53        }
54    }
55
56    /// The report for this box: a machine's identity and address come from the manifest, an ad-hoc
57    /// box's from what the driver stood up ([`Attached`]). `network` and `image` are the picks.
58    fn report(&self, network: Option<&str>, image: Option<&str>, attached: Attached) -> AttachReport {
59        let image = image.map(str::to_owned);
60        match self {
61            Target::Machine(m) => {
62                let (network, address) = address_on(m, network);
63                AttachReport {
64                    machine: m.name.as_str().to_owned(),
65                    native: native_name(m).to_owned(),
66                    network,
67                    address,
68                    image,
69                }
70            }
71            Target::AdHoc => {
72                let name = attached.name.unwrap_or_default();
73                AttachReport {
74                    machine: name.clone(),
75                    native: name,
76                    network: attached.network,
77                    address: attached.address,
78                    image,
79                }
80            }
81        }
82    }
83
84    /// The `result` event's data for a detach.
85    fn detach_data(&self) -> Value {
86        match self {
87            Target::Machine(m) => json!({ "machine": m.name.as_str(), "native": native_name(m) }),
88            Target::AdHoc => json!({ "machine": Value::Null, "native": Value::Null }),
89        }
90    }
91}
92
93impl Running<'_> {
94    /// Hot-adds the attacker box into the running run: an ad-hoc box on the lab's network (the
95    /// default now that labs declare no access machine), or the manifest's on-demand access machine
96    /// when the lab still has one. It is brought up with the learner-picked `image` (its baked or
97    /// Kali default otherwise), on `network` when given. `machine` names which on-demand machine,
98    /// needed only when the lab has more than one.
99    ///
100    /// # Errors
101    ///
102    /// Fails with `machine_unknown` for a named machine the lab doesn't have, `unsupported` when
103    /// the lab has several on-demand machines and none is named (or the target can't make an
104    /// ad-hoc box: a VM or cloud lab with no access machine), `not_running` when the run isn't up,
105    /// and as the other operations do for a step or the state.
106    pub async fn attach(&self, machine: Option<&str>, network: Option<&str>, image: Option<&str>, events: &dyn Events) -> Result<AttachReport> {
107        let opts = self.connected(&RunOptions::default());
108        let mask = sensitive_values(self.manifest(), &opts.secrets);
109        self.operate(Op::Attach, events, mask, async |e: &Emitter<'_>| {
110            let target = self.attacker(machine)?;
111            let (_, mut cx) = self.existing(&opts, e).await?;
112            if !any_target_running(&self.observe(&mut cx).await?.machines) {
113                return Err(Error::NotRunning { run: self.name().to_string() });
114            }
115            let attached = self.driver.attach(&cx, target.as_attacker(), image, network).await?;
116            let report = target.report(network, image, attached);
117            let data = json!({ "machine": report.machine, "native": report.native, "network": report.network, "address": report.address });
118            Ok(Done {
119                value: report,
120                ok: true,
121                data,
122                state: None,
123            })
124        })
125        .await
126    }
127
128    /// Hot-removes the attacker box from the running run, leaving the rest up.
129    ///
130    /// # Errors
131    ///
132    /// Fails as [`Running::attach`] does for the box choice and the target, and as the other
133    /// operations do for a step or the state.
134    pub async fn detach(&self, machine: Option<&str>, events: &dyn Events) -> Result<()> {
135        let opts = self.connected(&RunOptions::default());
136        let mask = sensitive_values(self.manifest(), &opts.secrets);
137        self.operate(Op::Detach, events, mask, async |e: &Emitter<'_>| {
138            let target = self.attacker(machine)?;
139            let (_, cx) = self.existing(&opts, e).await?;
140            self.driver.detach(&cx, target.as_attacker()).await?;
141            Ok(Done {
142                value: (),
143                ok: true,
144                data: target.detach_data(),
145                state: None,
146            })
147        })
148        .await
149    }
150
151    /// The command that opens an interactive shell into the attacker box: the runner builds it from
152    /// the run's runtime (the Compose project, the Vagrant define), so the embedder opens the box
153    /// by selection and never reconstructs a container name. A pure query — it runs nothing, emits
154    /// nothing, and doesn't check the box is up (the embedder runs what it returns in its own
155    /// terminal once the box is attached).
156    ///
157    /// # Errors
158    ///
159    /// Fails as [`Running::attach`] does for the box choice, and `unsupported` on a target that
160    /// makes no ad-hoc box (a VM or cloud lab with no access machine).
161    pub fn shell_command(&self, machine: Option<&str>) -> Result<ShellCommand> {
162        let target = self.attacker(machine)?;
163        let (_, provider, params) = self.query_prelude()?;
164        let silent = |_: &crate::events::Event| {};
165        let emit = Emitter::new(self.name().as_str(), Op::Check, self.runner.config.clock.as_ref(), &silent);
166        let cx = self.cx(params, &self.connection, &emit, provider)?;
167        self.driver.shell_command(&cx, target.as_attacker())
168    }
169
170    /// Which box an attach targets: the on-demand machine `machine` names, or the only one, else
171    /// an ad-hoc box when the lab declares no on-demand machine at all.
172    fn attacker(&self, machine: Option<&str>) -> Result<Target<'_>> {
173        let mut on_demand = self.manifest().machines.iter().filter(|m| m.on_demand);
174        if let Some(name) = machine {
175            return on_demand
176                .find(|m| m.name.as_str() == name)
177                .map(Target::Machine)
178                .ok_or_else(|| Error::MachineUnknown {
179                    run: self.name().to_string(),
180                    machine: name.to_owned(),
181                });
182        }
183        match (on_demand.next(), on_demand.next()) {
184            (Some(one), None) => Ok(Target::Machine(one)),
185            (None, _) => Ok(Target::AdHoc),
186            (Some(_), Some(_)) => Err(Error::Unsupported {
187                what: format!("hot-adding an access machine: this lab has several ({}); name which", self.on_demand_names()),
188            }),
189        }
190    }
191
192    /// The lab's on-demand access machines, by name, for the "name which" message.
193    fn on_demand_names(&self) -> String {
194        self.manifest()
195            .machines
196            .iter()
197            .filter(|m| m.on_demand)
198            .map(|m| m.name.as_str())
199            .collect::<Vec<_>>()
200            .join(", ")
201    }
202}
203
204/// The native name the driver addresses the box by.
205fn native_name(m: &Machine) -> &str {
206    match &m.native {
207        Some(Native::Compose { service }) => service.as_str(),
208        Some(Native::Vagrant { vagrant, .. }) => vagrant.as_str(),
209        _ => m.name.as_str(),
210    }
211}
212
213/// The box's static address on `network` (its first when none is named), from the manifest.
214fn address_on(m: &Machine, network: Option<&str>) -> (Option<String>, Option<String>) {
215    match network {
216        Some(n) => (
217            Some(n.to_owned()),
218            m.addresses.iter().find(|(k, _)| k.as_str() == n).map(|(_, a)| a.to_string()),
219        ),
220        None => m
221            .addresses
222            .iter()
223            .next()
224            .map_or((None, None), |(k, a)| (Some(k.as_str().to_owned()), Some(a.to_string()))),
225    }
226}