diff --git a/veilid-tools/src/virtual_network/router_server/config.rs b/veilid-tools/src/virtual_network/router_server/config.rs index 48a4644c..21844aac 100644 --- a/veilid-tools/src/virtual_network/router_server/config.rs +++ b/veilid-tools/src/virtual_network/router_server/config.rs @@ -217,6 +217,45 @@ fn validate_machine(machine: &Machine, _context: &ValidateContext) -> Result<(), Ok(()) } +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum MachineLocation { + Network { + network: String, + #[serde(default)] + address4: Option, + #[serde(default)] + address6: Option, + }, + Blueprint { + blueprint: String, + }, +} + +fn validate_machine_location( + value: &MachineLocation, + context: &ValidateContext, +) -> Result<(), ValidationError> { + match value { + MachineLocation::Network { + network, + address4, + address6, + } => { + if address4.is_none() && address6.is_none() { + return Err(ValidationError::new("badaddr") + .with_message("machine must have at least one address".into())); + } + validate_network_exists(network, context)?; + } + MachineLocation::Blueprint { blueprint } => { + validate_blueprint_exists(blueprint, context)?; + } + } + + Ok(()) +} + #[derive(Debug, Clone, Serialize, Deserialize, Validate)] #[validate( context = "ValidateContext<'v_a>", @@ -247,15 +286,28 @@ fn validate_template( #[derive(Debug, Clone, Serialize, Deserialize, Validate)] #[validate(schema(function = "validate_template_limits"))] pub struct TemplateLimits { + /// maximum number of machines this template will generate #[validate(nested)] - pub machine_count: WeightedList, + #[serde(default)] + pub machine_count: Option>, + #[validate(nested)] + pub machines_per_network: WeightedList, } fn validate_template_limits(limits: &TemplateLimits) -> Result<(), ValidationError> { - limits.machine_count.try_for_each(|x| { + if let Some(machine_count) = &limits.machine_count { + machine_count.try_for_each(|x| { + if *x == 0 { + return Err(ValidationError::new("badcount") + .with_message("template limits has zero machine count".into())); + } + Ok(()) + })?; + } + limits.machines_per_network.try_for_each(|x| { if *x == 0 { return Err(ValidationError::new("badcount") - .with_message("template limits has zero machine count".into())); + .with_message("template limits has zero machines per network count".into())); } Ok(()) })?; @@ -263,39 +315,6 @@ fn validate_template_limits(limits: &TemplateLimits) -> Result<(), ValidationErr Ok(()) } -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(untagged)] -pub enum MachineLocation { - Specific { - network: String, - #[serde(default)] - address4: Option, - #[serde(default)] - address6: Option, - }, -} - -fn validate_machine_location( - value: &MachineLocation, - context: &ValidateContext, -) -> Result<(), ValidationError> { - match value { - MachineLocation::Specific { - network, - address4, - address6, - } => { - if address4.is_none() && address6.is_none() { - return Err(ValidationError::new("badaddr") - .with_message("machine must have at least one address".into())); - } - validate_network_exists(network, context)?; - } - } - - Ok(()) -} - #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] pub enum TemplateLocation { @@ -433,18 +452,22 @@ fn validate_blueprint( #[derive(Debug, Clone, Serialize, Deserialize, Validate)] #[validate(schema(function = "validate_blueprint_limits"))] pub struct BlueprintLimits { + /// maximum number of networks this blueprint will generate #[validate(nested)] - pub network_count: WeightedList, + #[serde(default)] + pub network_count: Option>, } fn validate_blueprint_limits(limits: &BlueprintLimits) -> Result<(), ValidationError> { - limits.network_count.try_for_each(|x| { - if *x == 0 { - return Err(ValidationError::new("badcount") - .with_message("blueprint limits has zero machine count".into())); - } - Ok(()) - })?; + if let Some(network_count) = &limits.network_count { + network_count.try_for_each(|x| { + if *x == 0 { + return Err(ValidationError::new("badcount") + .with_message("blueprint limits has zero network count".into())); + } + Ok(()) + })?; + } Ok(()) } diff --git a/veilid-tools/src/virtual_network/router_server/default_config.yml b/veilid-tools/src/virtual_network/router_server/default_config.yml index ef5e394f..d6cd6fde 100644 --- a/veilid-tools/src/virtual_network/router_server/default_config.yml +++ b/veilid-tools/src/virtual_network/router_server/default_config.yml @@ -64,9 +64,11 @@ machines: ################################################################# # Templates # -# Templates are used to generate Machines that are all on a single -# network. A maximum number of machines are allocated on the -# network within the limits specified. +# Templates are used to generate Machines +# * if networks are specified, then all machines are created on that +# single network. A maximum number of machines are allocated on the +# network within the limits specified. +# * if a blueprint is spec templates: # Default servers on the boot network @@ -77,22 +79,23 @@ templates: bootrelay: network: "boot" machine_count: 4 + machines_per_network: 4 # Servers on subnets within the 'internet' network relayserver: blueprint: "direct" - machine_count: [1, 2, 3] + machines_per_network: [1, 2, 3] ipv4server: blueprint: "direct_ipv4_no_ipv6" - machine_count: [1, 2, 3] + machines_per_network: [1, 2, 3] ipv6server: blueprint: "direct_ipv6_no_ipv4" - machine_count: [1, 2, 3] + machines_per_network: [1, 2, 3] nat4home: blueprint: "nat_ipv4_no_ipv6" - machine_count: [1, 2, 3] + machines_per_network: [1, 2, 3] nat4+6home: blueprint: "nat_ipv4_direct_ipv6" - machine_count: [1, 2, 3] + machines_per_network: [1, 2, 3] ################################################################# # Networks diff --git a/veilid-tools/src/virtual_network/router_server/machine_registry/machine_registry_inner.rs b/veilid-tools/src/virtual_network/router_server/machine_registry/machine_registry_inner.rs index 5c4785a6..1d3836f3 100644 --- a/veilid-tools/src/virtual_network/router_server/machine_registry/machine_registry_inner.rs +++ b/veilid-tools/src/virtual_network/router_server/machine_registry/machine_registry_inner.rs @@ -264,51 +264,14 @@ impl MachineRegistryInner { let machine_def = { // Get the active template state let template_state = self.get_or_create_template_state(&name, template_def)?; - if !template_state.is_active()? { + if !template_state.is_active(self)? { return Err(MachineRegistryError::TemplateComplete); } // Pick or instantiate an available network - xxx add 'def()' selector to all types - let active_networks = match template_state.template_def.location.clone() { - config::TemplateLocation::Network { network } => { - // Filter the weighted list of networks to those that are still active or not yet started and can allocate - let Some(active_networks) = network.try_filter(|n| { - self.get_network_state_by_name(&n) - .map(|ns| ns.is_active()) - .unwrap_or(Ok(true)) - })? - else { - return Err(MachineRegistryError::NetworkComplete); - }; - } - config::TemplateLocation::Blueprint { blueprint } => { - // Filter the weighted list of blueprints to those that are still active or not yet started and can allocate - let Some(active_blueprints) = blueprint.try_filter(|b| { - self.get_blueprint_state(&b) - .map(|bs| bs.is_active(self)) - .unwrap_or(Ok(true)) - })? - else { - return Err(MachineRegistryError::BlueprintComplete); - }; + template_state.instantiate(self)? - // Activate some blueprint and pick a network - } - }; - - // Weighted choice of network now that we have a candidate list - //let network = - - config::Machine { - location: config::MachineLocation::Specific { - network: todo!(), - address4: None, - address6: None, - }, - disable_capabilities: template_state.template_def.disable_capabilities.clone(), - bootstrap: false, - } + xxx how to pass through per-network limits }; // Allocate a machine id @@ -321,9 +284,9 @@ impl MachineRegistryInner { // Create a new machine state let machine_state = match MachineState::try_new( self, + machine_id, MachineStateName::Template(name.clone()), machine_def.clone(), - machine_id, ) { Ok(v) => v, Err(e) => { @@ -345,12 +308,10 @@ impl MachineRegistryInner { // Return the unique id Ok(machine_id) } - pub(super) fn get_template_state( - &mut self, - name: &String, - ) -> MachineRegistryResult<&mut TemplateState> { + pub(super) fn get_template_state(&self, name: &String) -> MachineRegistryResult { self.template_state_by_name - .get_mut(name) + .get(name) + .cloned() .ok_or_else(|| MachineRegistryError::TemplateNotFound) } @@ -359,7 +320,7 @@ impl MachineRegistryInner { machine_location: &config::MachineLocation, ) -> MachineRegistryResult { match machine_location { - config::MachineLocation::Specific { + config::MachineLocation::Network { network: name, address4: _, address6: _, @@ -373,6 +334,17 @@ impl MachineRegistryInner { .expect("config validation is broken"); self.get_or_create_network_state(name.clone(), network_def) } + + config::MachineLocation::Blueprint { blueprint: name } => { + let blueprint_def = self + .unlocked_inner + .config + .blueprints + .get(name) + .cloned() + .expect("config validation is broken"); + self.get_or_create_network_state_from_blueprint(name.clone(), blueprint_def) + } } } pub(super) fn get_or_create_network_state_from_template_location( @@ -406,11 +378,12 @@ impl MachineRegistryInner { } pub(super) fn get_blueprint_state( - &mut self, + &self, name: &String, - ) -> MachineRegistryResult<&mut BlueprintState> { + ) -> MachineRegistryResult { self.blueprint_state_by_name - .get_mut(name) + .get(name) + .cloned() .ok_or_else(|| MachineRegistryError::BlueprintNotFound) } @@ -533,26 +506,18 @@ impl MachineRegistryInner { &mut self, name: String, blueprint_def: config::Blueprint, - ) -> MachineRegistryResult { + ) -> MachineRegistryResult { // Get the active blueprint state let blueprint_state = self.get_or_create_blueprint_state(&name, blueprint_def)?; if !blueprint_state.is_active(self)? { return Err(MachineRegistryError::BlueprintComplete); } - //xxx - // Make machine def from current template state - let machine_def = config::Machine { - location: match template_state.template_def.location.clone() { - config::TemplateLocation::Network { network } => { - config::MachineLocation::Network { network } - } - config::TemplateLocation::Blueprint { blueprint } => { - config::MachineLocation::Blueprint { blueprint } - } - }, - disable_capabilities: template_state.template_def.disable_capabilities.clone(), - bootstrap: false, + // Make network def from current blueprint state + let machine_def = config::Network { + model: self.unlocked_inner.srng.weighted_choice(blueprint_state), + ipv4: todo!(), + ipv6: todo!(), }; // Allocate a machine id diff --git a/veilid-tools/src/virtual_network/router_server/machine_registry/state/blueprint_state.rs b/veilid-tools/src/virtual_network/router_server/machine_registry/state/blueprint_state.rs index e9f3adec..f3723d53 100644 --- a/veilid-tools/src/virtual_network/router_server/machine_registry/state/blueprint_state.rs +++ b/veilid-tools/src/virtual_network/router_server/machine_registry/state/blueprint_state.rs @@ -8,7 +8,7 @@ struct BlueprintStateUnlockedInner { #[derive(Debug)] struct BlueprintStateInner { - limit_network_count: u32, + limit_network_count: Option, networks: HashSet, } @@ -24,10 +24,12 @@ impl BlueprintState { name: String, blueprint_def: config::Blueprint, ) -> MachineRegistryResult { - let limit_network_count = *machine_registry_inner - .unlocked_inner - .srng - .weighted_choice(&blueprint_def.limits.network_count); + let limit_network_count = blueprint_def.limits.network_count.as_ref().map(|nc| { + *machine_registry_inner + .unlocked_inner + .srng + .weighted_choice(nc) + }); Ok(Self { unlocked_inner: Arc::new(BlueprintStateUnlockedInner { @@ -41,6 +43,10 @@ impl BlueprintState { }) } + pub fn def(&self) -> &config::Blueprint { + &self.unlocked_inner.blueprint_def + } + pub fn is_active( &self, machine_registry_inner: &MachineRegistryInner, diff --git a/veilid-tools/src/virtual_network/router_server/machine_registry/state/machine_state.rs b/veilid-tools/src/virtual_network/router_server/machine_registry/state/machine_state.rs index cb1a46ba..6930525b 100644 --- a/veilid-tools/src/virtual_network/router_server/machine_registry/state/machine_state.rs +++ b/veilid-tools/src/virtual_network/router_server/machine_registry/state/machine_state.rs @@ -60,7 +60,7 @@ impl MachineState { // Make the default route interface let machine_location = machine_def.location.clone(); let (allocate_v4, opt_address4, allocate_v6, opt_address6) = match machine_location { - config::MachineLocation::Specific { + config::MachineLocation::Network { network: _, address4, address6, @@ -70,6 +70,9 @@ impl MachineState { network_state.is_ipv6() && address6.is_some(), address6, ), + config::MachineLocation::Blueprint { blueprint: _ } => { + (network_state.is_ipv4(), None, network_state.is_ipv6(), None) + } }; if allocate_v4 { @@ -163,6 +166,10 @@ impl MachineState { self.unlocked_inner.name.clone() } + pub fn def(&self) -> &config::Machine { + &self.unlocked_inner.machine_def + } + pub fn id(&self) -> MachineId { self.unlocked_inner.id } diff --git a/veilid-tools/src/virtual_network/router_server/machine_registry/state/network_state.rs b/veilid-tools/src/virtual_network/router_server/machine_registry/state/network_state.rs index 3f7abd43..5b60aabf 100644 --- a/veilid-tools/src/virtual_network/router_server/machine_registry/state/network_state.rs +++ b/veilid-tools/src/virtual_network/router_server/machine_registry/state/network_state.rs @@ -143,6 +143,10 @@ impl NetworkState { self.unlocked_inner.name.clone() } + pub fn def(&self) -> &config::Network { + &self.unlocked_inner.network_def + } + pub fn id(&self) -> NetworkId { self.unlocked_inner.id } diff --git a/veilid-tools/src/virtual_network/router_server/machine_registry/state/template_state.rs b/veilid-tools/src/virtual_network/router_server/machine_registry/state/template_state.rs index 7d4be729..8df65de7 100644 --- a/veilid-tools/src/virtual_network/router_server/machine_registry/state/template_state.rs +++ b/veilid-tools/src/virtual_network/router_server/machine_registry/state/template_state.rs @@ -8,7 +8,8 @@ struct TemplateStateUnlockedInner { #[derive(Debug)] struct TemplateStateInner { - limit_machine_count: u32, + limit_machine_count: Option, + limit_machines_per_network: u32, machines: HashSet, } @@ -24,15 +25,22 @@ impl TemplateState { name: String, template_def: config::Template, ) -> MachineRegistryResult { - let limit_machine_count = *machine_registry_inner + let limit_machine_count = template_def.limits.machine_count.as_ref().map(|mc| { + *machine_registry_inner + .unlocked_inner + .srng + .weighted_choice(mc) + }); + let limit_machines_per_network = *machine_registry_inner .unlocked_inner .srng - .weighted_choice(&template_def.limits.machine_count); + .weighted_choice(&template_def.limits.machines_per_network); Ok(Self { unlocked_inner: Arc::new(TemplateStateUnlockedInner { name, template_def }), inner: Arc::new(Mutex::new(TemplateStateInner { limit_machine_count, + limit_machines_per_network, machines: HashSet::new(), })), }) @@ -42,8 +50,116 @@ impl TemplateState { self.unlocked_inner.name.clone() } - pub fn is_active(&self) -> MachineRegistryResult { + pub fn def(&self) -> &config::Template { + &self.unlocked_inner.template_def + } + + pub fn is_active( + &self, + machine_registry_inner: &mut MachineRegistryInner, + ) -> MachineRegistryResult { let inner = self.inner.lock(); - Ok(inner.machines.len() < inner.limit_machine_count.try_into().unwrap_or(usize::MAX)) + if let Some(limit_machine_count) = inner.limit_machine_count { + if inner.machines.len() < limit_machine_count.try_into().unwrap_or(usize::MAX) { + return Ok(false); + } + } + + match self.def().location.clone() { + config::TemplateLocation::Network { network } => { + // Filter the weighted list of networks to those that are still active or not yet started and can allocate + if network + .try_filter(|n| { + machine_registry_inner + .get_network_state_by_name(&n) + .clone() + .map(|ns| ns.is_active()) + .unwrap_or(Ok(true)) + })? + .is_none() + { + return Ok(false); + }; + } + config::TemplateLocation::Blueprint { blueprint } => { + // Filter the weighted list of blueprints to those that are still active or not yet started and can allocate + if blueprint + .try_filter(|b| { + machine_registry_inner + .get_blueprint_state(&b) + .clone() + .map(|bs| bs.is_active(machine_registry_inner)) + .unwrap_or(Ok(true)) + })? + .is_none() + { + return Ok(false); + }; + } + }; + + Ok(true) + } + + pub fn instantiate( + &self, + machine_registry_inner: &mut MachineRegistryInner, + ) -> MachineRegistryResult { + // Pick or instantiate an available network + let location = match self.def().location.clone() { + config::TemplateLocation::Network { network } => { + // Filter the weighted list of networks to those that are still active or not yet started and can allocate + let Some(active_networks) = network.try_filter(|n| { + machine_registry_inner + .get_network_state_by_name(&n) + .clone() + .map(|ns| ns.is_active()) + .unwrap_or(Ok(true)) + })? + else { + return Err(MachineRegistryError::NetworkComplete); + }; + + // Weighted choice of network now that we have a candidate list + let network_name = machine_registry_inner + .unlocked_inner + .srng + .weighted_choice(&active_networks); + config::MachineLocation::Network { + network: network_name.clone(), + address4: None, + address6: None, + } + } + config::TemplateLocation::Blueprint { blueprint } => { + // Filter the weighted list of blueprints to those that are still active or not yet started and can allocate + let Some(active_blueprints) = blueprint.try_filter(|b| { + machine_registry_inner + .get_blueprint_state(&b) + .clone() + .map(|bs| bs.is_active(machine_registry_inner)) + .unwrap_or(Ok(true)) + })? + else { + return Err(MachineRegistryError::BlueprintComplete); + }; + + // Weighted choice of blueprint now that we have a candidate list + let blueprint_name = machine_registry_inner + .unlocked_inner + .srng + .weighted_choice(&active_blueprints); + + config::MachineLocation::Blueprint { + blueprint: blueprint_name.clone(), + } + } + }; + + Ok(config::Machine { + location, + disable_capabilities: self.def().disable_capabilities.clone(), + bootstrap: false, + }) } }