1- //! High-level lab API: [`Lab`], [`LabOpts `], [`Ix`], topology types.
1+ //! High-level lab API: [`Lab`], [`LabBuilder `], [`Ix`], topology types.
22
33use std:: {
44 collections:: HashMap ,
@@ -20,6 +20,7 @@ use tracing::{debug, debug_span};
2020
2121pub use crate :: qdisc:: LinkLimits ;
2222use crate :: {
23+ config:: LabConfig ,
2324 core:: {
2425 self , CoreConfig , DeviceData , DownstreamPool , NetworkCore , NodeId , RouterData ,
2526 RA_DEFAULT_ENABLED , RA_DEFAULT_INTERVAL_SECS , RA_DEFAULT_LIFETIME_SECS ,
@@ -432,7 +433,7 @@ impl LabInner {
432433/// A virtual network lab running in Linux network namespaces.
433434///
434435/// `Lab` is cheaply cloneable. All methods take `&self` and use interior
435- /// mutability. Use [`Lab::new`] or [`Lab::with_opts `] to create a lab,
436+ /// mutability. Use [`Lab::new`] or [`Lab::builder() `] to create a lab,
436437/// then build routers and devices with [`Lab::add_router`] and
437438/// [`Lab::add_device`].
438439///
@@ -473,32 +474,33 @@ pub struct Lab {
473474 pub ( crate ) inner : Arc < LabDropGuard > ,
474475}
475476
476- /// Options for constructing a [`Lab`].
477+ /// Builder for constructing a [`Lab`] with custom configuration .
477478///
478- /// Use the builder methods to configure output directory and label, then pass
479- /// to [`Lab::with_opts`].
479+ /// Use [`Lab::builder()`] to obtain a `LabBuilder`, configure output directory
480+ /// and label with the builder methods, then call [`.build()`](LabBuilder::build)
481+ /// to create the lab.
480482///
481483/// # Example
482484/// ```no_run
483- /// # use patchbay::{Lab, LabOpts , OutDir};
485+ /// # use patchbay::{Lab, LabBuilder , OutDir};
484486/// # #[tokio::main(flavor = "current_thread")]
485487/// # async fn main() -> anyhow::Result<()> {
486- /// let lab = Lab::with_opts(
487- /// LabOpts::default()
488- /// .outdir(OutDir::Nested("/tmp/patchbay-out".into()))
489- /// .label("my-test"),
490- /// )
491- /// .await?;
488+ /// let lab = Lab::builder()
489+ /// .outdir(OutDir::Nested("/tmp/patchbay-out".into()))
490+ /// .label("my-test")
491+ /// .build()
492+ /// .await?;
492493/// # Ok(())
493494/// # }
494495/// ```
495496#[ derive( Default ) ]
496- pub struct LabOpts {
497+ pub struct LabBuilder {
497498 outdir : Option < OutDir > ,
498499 label : Option < String > ,
499500 ipv6_dad_mode : Ipv6DadMode ,
500501 ipv6_provisioning_mode : Ipv6ProvisioningMode ,
501502 allow_real_root : bool ,
503+ config : Option < LabConfig > ,
502504}
503505
504506/// Where the lab writes event logs and state files.
@@ -563,7 +565,7 @@ impl Ipv6Profile {
563565 }
564566}
565567
566- impl LabOpts {
568+ impl LabBuilder {
567569 /// Sets the output directory for event log and state files.
568570 pub fn outdir ( mut self , outdir : OutDir ) -> Self {
569571 self . outdir = Some ( outdir) ;
@@ -608,13 +610,30 @@ impl LabOpts {
608610 self
609611 }
610612
613+ /// Loads a TOML lab configuration. The config is applied during
614+ /// [`build`](Self::build), creating all routers and devices defined
615+ /// in the file. Builder options (outdir, label, etc.) take precedence
616+ /// over config defaults.
617+ pub fn config ( mut self , cfg : LabConfig ) -> Self {
618+ self . config = Some ( cfg) ;
619+ self
620+ }
621+
611622 /// Applies a deployment profile that sets both DAD and v6 provisioning mode.
612623 pub fn ipv6_profile ( mut self , profile : Ipv6Profile ) -> Self {
613624 let ( dad, provisioning) = profile. modes ( ) ;
614625 self . ipv6_dad_mode = dad;
615626 self . ipv6_provisioning_mode = provisioning;
616627 self
617628 }
629+
630+ /// Builds the [`Lab`] with the configured options.
631+ ///
632+ /// If a TOML [`config`](Self::config) was set, all routers and devices
633+ /// defined in the config are created during build.
634+ pub async fn build ( self ) -> Result < Lab > {
635+ Lab :: build ( self ) . await
636+ }
618637}
619638
620639impl Lab {
@@ -643,26 +662,32 @@ impl Lab {
643662 anyhow:: bail!(
644663 "refusing to run as real root (not inside a user namespace). \
645664 Call patchbay::init_userns() before creating a Lab, or use \
646- LabOpts::default ().allow_real_root() to bypass this check."
665+ Lab::builder ().allow_real_root() to bypass this check."
647666 ) ;
648667 }
649668 }
650669 Ok ( ( ) )
651670 }
652671
672+ /// Returns a [`LabBuilder`] for configuring and constructing a [`Lab`].
673+ pub fn builder ( ) -> LabBuilder {
674+ LabBuilder :: default ( )
675+ }
676+
653677 /// Creates a new lab with default address ranges and IX settings.
654678 ///
655679 /// Reads `PATCHBAY_OUTDIR` from the environment for event output.
656- /// Use [`Lab::with_opts `] for explicit configuration.
680+ /// Use [`Lab::builder() `] for explicit configuration.
657681 pub async fn new ( ) -> Result < Self > {
658- Self :: with_opts ( LabOpts :: default ( ) . outdir_from_env ( ) ) . await
682+ Self :: builder ( ) . outdir_from_env ( ) . build ( ) . await
659683 }
660684
661- /// Creates a new lab with the given options .
662- pub async fn with_opts ( opts : LabOpts ) -> Result < Self > {
685+ /// Internal constructor used by [`LabBuilder::build`] .
686+ async fn build ( mut opts : LabBuilder ) -> Result < Self > {
663687 if !opts. allow_real_root {
664688 Self :: refuse_real_root ( ) ?;
665689 }
690+ let config = opts. config . take ( ) ;
666691 let pid = std:: process:: id ( ) ;
667692 let pid_tag = pid % 9999 + 1 ;
668693 let lab_seq = LAB_COUNTER . fetch_add ( 1 , Ordering :: Relaxed ) ;
@@ -778,6 +803,11 @@ impl Lab {
778803 gw_v6 : cfg. ix_gw_v6 ,
779804 } ) ;
780805
806+ // Apply TOML config if provided.
807+ if let Some ( cfg) = config {
808+ lab. apply_config ( cfg) . await ?;
809+ }
810+
781811 Ok ( lab)
782812 }
783813
@@ -822,22 +852,13 @@ impl Lab {
822852 /// Parses `lab.toml`, builds the network, and returns a ready-to-use lab.
823853 pub async fn load ( path : impl AsRef < Path > ) -> Result < Self > {
824854 let text = std:: fs:: read_to_string ( path) . context ( "read lab config" ) ?;
825- let cfg: crate :: config:: LabConfig = toml:: from_str ( & text) . context ( "parse lab config" ) ?;
826- Self :: from_config ( cfg) . await
827- }
828-
829- /// Builds a `Lab` from a parsed config, creating all namespaces and links.
830- pub async fn from_config ( cfg : crate :: config:: LabConfig ) -> Result < Self > {
831- Self :: from_config_with_opts ( cfg, LabOpts :: default ( ) . outdir_from_env ( ) ) . await
855+ let cfg: LabConfig = toml:: from_str ( & text) . context ( "parse lab config" ) ?;
856+ Self :: builder ( ) . outdir_from_env ( ) . config ( cfg) . build ( ) . await
832857 }
833858
834- /// Builds a `Lab` from a parsed config with explicit options.
835- pub async fn from_config_with_opts (
836- cfg : crate :: config:: LabConfig ,
837- opts : LabOpts ,
838- ) -> Result < Self > {
839- let lab = Self :: with_opts ( opts) . await ?;
840-
859+ /// Applies a parsed TOML config to an already-built lab, creating all
860+ /// routers and devices defined in the config.
861+ async fn apply_config ( & self , cfg : LabConfig ) -> Result < ( ) > {
841862 // Region latency pairs from TOML config are ignored in the new region API.
842863 // TODO: support regions in TOML config via add_region / link_regions.
843864
@@ -865,12 +886,12 @@ impl Lab {
865886 for name in ready {
866887 let rcfg = pending. remove ( name) . unwrap ( ) ;
867888 let upstream = {
868- let inner = lab . inner . core . lock ( ) . unwrap ( ) ;
889+ let inner = self . inner . core . lock ( ) . unwrap ( ) ;
869890 rcfg. upstream
870891 . as_deref ( )
871892 . and_then ( |n| inner. router_id_by_name ( n) )
872893 } ;
873- let mut rb = lab
894+ let mut rb = self
874895 . add_router ( & rcfg. name )
875896 . nat ( rcfg. nat )
876897 . ip_support ( rcfg. ip_support )
@@ -953,7 +974,7 @@ impl Lab {
953974 . ok_or_else ( || {
954975 anyhow ! ( "device '{}' iface '{}' missing 'gateway'" , dev_name, ifname)
955976 } ) ?;
956- let router_id = lab
977+ let router_id = self
957978 . inner
958979 . core
959980 . lock ( )
@@ -999,7 +1020,7 @@ impl Lab {
9991020 result
10001021 } ;
10011022 for dev in dev_data {
1002- let mut builder = lab . add_device ( & dev. name ) ;
1023+ let mut builder = self . add_device ( & dev. name ) ;
10031024 for ( ifname, router_id, impair) in dev. ifaces {
10041025 let config = IfaceConfig :: routed ( router_id) ;
10051026 let config = if let Some ( cond) = impair {
@@ -1015,7 +1036,7 @@ impl Lab {
10151036 builder. build ( ) . await ?;
10161037 }
10171038
1018- Ok ( lab )
1039+ Ok ( ( ) )
10191040 }
10201041
10211042 // ── Builder methods (sync — just populate data structures) ──────────
0 commit comments