diff --git a/falcon-lab/src/main.rs b/falcon-lab/src/main.rs index ca19e925..3205a1ac 100644 --- a/falcon-lab/src/main.rs +++ b/falcon-lab/src/main.rs @@ -2,7 +2,8 @@ use crate::dendrite::NpuvmCommits; use crate::scenario::{ - InteropScenario, MgdDuoScenario, Scenario, ScenarioOptions, + Interop3LinkScenario, InteropScenario, MgdDuoScenario, Scenario, + ScenarioOptions, }; use clap::{Args, Parser, Subcommand, ValueEnum}; @@ -45,6 +46,8 @@ struct TopologyRun { enum RunTopology { MgdDuo(ScenarioRun), Interop(ScenarioRun), + #[command(name = "interop-3-link")] + Interop3Link(ScenarioRun), } #[derive(Debug, Args)] @@ -90,6 +93,8 @@ struct TopologyCleanup { enum CleanupTopology { MgdDuo(ScenarioCleanup), Interop(ScenarioCleanup), + #[command(name = "interop-3-link")] + Interop3Link(ScenarioCleanup), } #[derive(Debug, Args)] @@ -116,6 +121,9 @@ async fn run() -> anyhow::Result<()> { RunTopology::Interop(cmd) => { cmd.scenario.run(cmd.options.scenario_options()).await?; } + RunTopology::Interop3Link(cmd) => { + cmd.scenario.run(cmd.options.scenario_options()).await?; + } }, Command::Cleanup(cmd) => match cmd.topology { CleanupTopology::MgdDuo(cmd) => { @@ -124,6 +132,9 @@ async fn run() -> anyhow::Result<()> { CleanupTopology::Interop(cmd) => { cmd.scenario.cleanup()?; } + CleanupTopology::Interop3Link(cmd) => { + cmd.scenario.cleanup()?; + } }, Command::Serial(cmd) => { libfalcon::cli::console(&cmd.node, ".falcon".into()).await? diff --git a/falcon-lab/src/scenario.rs b/falcon-lab/src/scenario.rs index 9e6b1f17..310b2fb3 100644 --- a/falcon-lab/src/scenario.rs +++ b/falcon-lab/src/scenario.rs @@ -10,7 +10,7 @@ use crate::{ frr::FrrNode, juniper::{JuniperNode, clear_staged_routing_configs}, mgd::{MgdNode, wait_for_mgd}, - topo::{Interop, MgdDuo, Topology}, + topo::{Interop, Interop3Link, MgdDuo, Topology}, wait_for_eq, wait_for_eq_stable, }; use anyhow::{Context, Result}; @@ -70,6 +70,11 @@ const _: () = { assert_falcon_compatible(InteropScenario::VARIANTS[i].name()); i += 1; } + let mut i = 0; + while i < Interop3LinkScenario::VARIANTS.len() { + assert_falcon_compatible(Interop3LinkScenario::VARIANTS[i].name()); + i += 1; + } }; pub(crate) struct ScenarioOptions { @@ -198,6 +203,37 @@ impl Scenario for InteropScenario { } } +#[derive(Copy, Clone, Debug, ValueEnum, strum::VariantArray)] +pub(crate) enum Interop3LinkScenario { + Bare, +} + +impl Interop3LinkScenario { + pub const fn name(self) -> &'static str { + match self { + Self::Bare => "interop3_bare", + } + } +} + +impl Scenario for Interop3LinkScenario { + type Topology = Interop3Link; + + async fn run(self, options: ScenarioOptions) -> Result<()> { + match self { + Self::Bare => { + clear_staged_routing_configs() + .context("clear stale Junos config")?; + self.run_bare(options.persistent).await + } + } + } + + fn cleanup(self) -> Result<()> { + cleanup_interop_3link_deployment(self) + } +} + const CR1_BFD_FRR_CONFIG: &str = "cr1-bfd-frr.conf"; const OP_TIMEOUT: Duration = Duration::from_secs(10); const LAUNCH_TIMEOUT: Duration = Duration::from_secs(10 * 60); @@ -623,6 +659,16 @@ fn cleanup_interop_deployment(scenario: InteropScenario) -> Result<()> { config_result } +fn cleanup_interop_3link_deployment( + scenario: Interop3LinkScenario, +) -> Result<()> { + let deployment_result = Interop3Link::build(scenario).map(drop); + let config_result = + clear_staged_routing_configs().context("clear stale Junos config"); + deployment_result?; + config_result +} + async fn run_mgd_unnumbered( scenario: MgdDuoScenario, persistent: bool, diff --git a/falcon-lab/src/topo.rs b/falcon-lab/src/topo.rs index a91f6a8c..8b45417c 100644 --- a/falcon-lab/src/topo.rs +++ b/falcon-lab/src/topo.rs @@ -1,14 +1,14 @@ //! Testing topologies use anyhow::Result; -use libfalcon::{Runner, node, unit::gb}; +use libfalcon::{NodeRef, Runner, node, unit::gb}; use crate::{ eos::EosNode, frr::FrrNode, juniper::JuniperNode, mgd::MgdNode, - scenario::{InteropScenario, MgdDuoScenario}, + scenario::{Interop3LinkScenario, InteropScenario, MgdDuoScenario}, }; pub(crate) trait Topology: Sized { @@ -62,52 +62,80 @@ pub struct Interop { pub cr3: JuniperNode, } +// The bare scenario only drives the runner; node handles arrive with the +// first scenario that interacts with individual VMs. +pub struct Interop3Link { + pub d: Runner, +} + +struct InteropNodes { + d: Runner, + ox: NodeRef, + peer: NodeRef, + cr1: NodeRef, + cr2: NodeRef, + cr3: NodeRef, +} + +fn build_interop(name: &str, links_per_peer: usize) -> Result { + let mut d = Runner::new(name); + + node!(d, ox, "helios-3.0", 4, gb(4)); + node!(d, cr1, "debian-13.2", 4, gb(4)); + node!(d, cr2, "eos-4.35", 4, gb(4)); + node!(d, cr3, "junos-23.2", 4, gb(4)); + node!(d, peer, "helios-3.0", 4, gb(4)); + + let mut mac_counter = 0; + let mut new_mac = || { + mac_counter += 1; + format!("a8:40:25:00:00:{mac_counter:02}") + }; + + for remote in [cr1, cr2, cr3, peer] { + for _ in 0..links_per_peer { + d.softnpu_link(ox, remote, Some(new_mac()), None); + } + } + + d.default_ext_link(ox)?; + d.default_ext_link(cr1)?; + d.default_ext_link(cr2)?; + d.default_ext_link(cr3)?; + d.default_ext_link(peer)?; + + d.mount("cargo-bay", "/opt/cargo-bay", ox)?; + d.mount_linux("cargo-bay", "/opt/cargo-bay", cr1)?; + d.mount("cargo-bay", "/opt/cargo-bay", cr2)?; + d.mount_linux("cargo-bay", "/opt/cargo-bay", cr3)?; + d.mount("cargo-bay", "/opt/cargo-bay", peer)?; + // The Junos image mounts cargo-bay and applies staged configuration + // from guest-side systemd services. Keep the 9p device in the spec, + // but avoid Falcon's serial-driven setup/mount path for this node. + d.do_setup(cr3, false); + + Ok(InteropNodes { + d, + ox, + peer, + cr1, + cr2, + cr3, + }) +} + impl Topology for Interop { type Scenario = InteropScenario; fn build(scenario: InteropScenario) -> Result { - let mut d = Runner::new(scenario.name()); - - node!(d, ox, "helios-3.0", 4, gb(4)); - node!(d, cr1, "debian-13.2", 4, gb(4)); - node!(d, cr2, "eos-4.35", 4, gb(4)); - node!(d, cr3, "junos-23.2", 4, gb(4)); - node!(d, peer, "helios-3.0", 4, gb(4)); - - let mut mac_counter = 0; - let mut new_mac = || { - mac_counter += 1; - format!("a8:40:25:00:00:{mac_counter:02}") - }; - - d.softnpu_link(ox, cr1, Some(new_mac()), None); - d.softnpu_link(ox, cr2, Some(new_mac()), None); - d.softnpu_link(ox, cr3, Some(new_mac()), None); - d.softnpu_link(ox, peer, Some(new_mac()), None); - - d.default_ext_link(ox)?; - d.default_ext_link(cr1)?; - d.default_ext_link(cr2)?; - d.default_ext_link(cr3)?; - d.default_ext_link(peer)?; - - d.mount("cargo-bay", "/opt/cargo-bay", ox)?; - d.mount_linux("cargo-bay", "/opt/cargo-bay", cr1)?; - d.mount("cargo-bay", "/opt/cargo-bay", cr2)?; - d.mount_linux("cargo-bay", "/opt/cargo-bay", cr3)?; - d.mount("cargo-bay", "/opt/cargo-bay", peer)?; - // The Junos image mounts cargo-bay and applies staged configuration - // from guest-side systemd services. Keep the 9p device in the spec, - // but avoid Falcon's serial-driven setup/mount path for this node. - d.do_setup(cr3, false); - + let nodes = build_interop(scenario.name(), 1)?; Ok(Self { - d, - ox: MgdNode(ox), - peer: MgdNode(peer), - cr1: FrrNode(cr1), - cr2: EosNode(cr2), - cr3: JuniperNode(cr3), + d: nodes.d, + ox: MgdNode(nodes.ox), + peer: MgdNode(nodes.peer), + cr1: FrrNode(nodes.cr1), + cr2: EosNode(nodes.cr2), + cr3: JuniperNode(nodes.cr3), }) } @@ -115,3 +143,16 @@ impl Topology for Interop { &mut self.d } } + +impl Topology for Interop3Link { + type Scenario = Interop3LinkScenario; + + fn build(scenario: Interop3LinkScenario) -> Result { + let nodes = build_interop(scenario.name(), 3)?; + Ok(Self { d: nodes.d }) + } + + fn runner_mut(&mut self) -> &mut Runner { + &mut self.d + } +}