diff --git a/veilid-tools/src/lib.rs b/veilid-tools/src/lib.rs index 37bb62b6..7407c437 100644 --- a/veilid-tools/src/lib.rs +++ b/veilid-tools/src/lib.rs @@ -52,6 +52,7 @@ pub mod socket_tools; pub mod spawn; pub mod split_url; pub mod startup_lock; +pub mod static_string_table; pub mod tick_task; pub mod timeout; pub mod timeout_or; @@ -241,6 +242,8 @@ pub use split_url::*; #[doc(inline)] pub use startup_lock::*; #[doc(inline)] +pub use static_string_table::*; +#[doc(inline)] pub use tick_task::*; #[doc(inline)] pub use timeout::*; diff --git a/veilid-tools/src/static_string_table.rs b/veilid-tools/src/static_string_table.rs new file mode 100644 index 00000000..462c4d64 --- /dev/null +++ b/veilid-tools/src/static_string_table.rs @@ -0,0 +1,21 @@ +use super::*; + +static STRING_TABLE: std::sync::LazyLock>> = + std::sync::LazyLock::new(|| Mutex::new(BTreeSet::new())); + +pub trait ToStaticStr { + fn to_static_str(&self) -> &'static str; +} + +impl> ToStaticStr for T { + fn to_static_str(&self) -> &'static str { + let s = self.as_ref(); + let mut string_table = STRING_TABLE.lock(); + if let Some(v) = string_table.get(s) { + return v; + } + let ss = Box::leak(s.to_owned().into_boxed_str()); + string_table.insert(ss); + ss + } +} diff --git a/veilid-tools/src/virtual_network/router_server/config.rs b/veilid-tools/src/virtual_network/router_server/config.rs index 9dd89fed..2ca3fed9 100644 --- a/veilid-tools/src/virtual_network/router_server/config.rs +++ b/veilid-tools/src/virtual_network/router_server/config.rs @@ -28,14 +28,18 @@ impl Default for WeightedList { impl Validate for WeightedList { fn validate(&self) -> Result<(), ValidationErrors> { let mut errors = ValidationErrors::new(); - if let Self::List(v) = self { - if v.is_empty() { - errors.add( - "List", - ValidationError::new("len") - .with_message("weighted list must not be empty".into()), - ) + match self { + Self::List(v) => { + if v.is_empty() { + errors.add( + "List", + ValidationError::new("len") + .with_message("weighted list must not be empty".into()), + ) + } + errors.merge_self("List", v.validate()); } + Self::Single(_addr) => {} } if errors.is_empty() { @@ -46,6 +50,34 @@ impl Validate for WeightedList { } } +impl WeightedList { + fn validate_once(&self) -> Result<(), ValidationError> { + match self { + Self::List(v) => { + if v.is_empty() { + return Err(ValidationError::new("len") + .with_message("weighted list must not be empty".into())); + } + } + Self::Single(_addr) => {} + } + Ok(()) + } + + pub fn try_for_each Result<(), E>>(&self, mut f: F) -> Result<(), E> { + match self { + WeightedList::Single(v) => f(v), + WeightedList::List(vec) => vec + .iter() + .map(|v| match v { + Weighted::Weighted { item, weight: _ } => item, + Weighted::Unweighted(item) => item, + }) + .try_for_each(f), + } + } +} + pub type Probability = f32; #[derive(Debug, Clone, Serialize, Deserialize)] @@ -76,6 +108,21 @@ impl Validate for Weighted { } } +impl Weighted { + pub fn item(&self) -> &T { + match self { + Weighted::Weighted { item, weight: _ } => item, + Weighted::Unweighted(item) => item, + } + } + pub fn weight(&self) -> f32 { + match self { + Weighted::Weighted { item: _, weight } => *weight, + Weighted::Unweighted(_) => 1.0f32, + } + } +} + #[derive(Debug, Clone, Serialize, Deserialize, Validate)] #[validate(context = "ValidateContext<'v_a>")] pub struct Profile { @@ -94,7 +141,10 @@ pub enum Instance { } #[derive(Debug, Clone, Serialize, Deserialize, Validate)] -#[validate(context = "ValidateContext<'v_a>")] +#[validate( + context = "ValidateContext<'v_a>", + schema(function = "validate_machine", use_context) +)] pub struct Machine { #[serde(flatten)] #[validate(custom(function = "validate_location_exists", use_context))] @@ -109,23 +159,64 @@ pub struct Machine { pub bootstrap: bool, } +fn validate_machine(machine: &Machine, _context: &ValidateContext) -> Result<(), ValidationError> { + if machine.address4.is_none() && machine.address6.is_none() { + return Err(ValidationError::new("badaddr") + .with_message("machine must have at least one address".into())); + } + if machine.disable_capabilities.contains(&("".to_string())) { + return Err(ValidationError::new("badcap") + .with_message("machine has empty disabled capability".into())); + } + Ok(()) +} + #[derive(Debug, Clone, Serialize, Deserialize, Validate)] -#[validate(context = "ValidateContext<'v_a>")] +#[validate( + context = "ValidateContext<'v_a>", + schema(function = "validate_template", use_context) +)] pub struct Template { #[serde(flatten)] #[validate(custom(function = "validate_location_exists", use_context))] pub location: Location, #[serde(flatten)] + #[validate(nested)] pub limits: Limits, #[serde(default)] pub disable_capabilities: Vec, } +fn validate_template( + template: &Template, + _context: &ValidateContext, +) -> Result<(), ValidationError> { + if template.disable_capabilities.contains(&("".to_string())) { + return Err(ValidationError::new("badcap") + .with_message("template has empty disabled capability".into())); + } + Ok(()) +} + #[derive(Debug, Clone, Serialize, Deserialize, Validate)] +#[validate(schema(function = "validate_limits"))] pub struct Limits { + #[validate(nested)] pub machine_count: WeightedList, } +fn validate_limits(limits: &Limits) -> Result<(), ValidationError> { + limits.machine_count.try_for_each(|x| { + if *x == 0 { + return Err(ValidationError::new("badcount") + .with_message("limits has zero machine count".into())); + } + Ok(()) + })?; + + Ok(()) +} + #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] pub enum Location { @@ -136,28 +227,66 @@ pub enum Location { //////////////////////////////////////////////////////////////// #[derive(Debug, Clone, Serialize, Deserialize, Validate)] +#[validate( + context = "ValidateContext<'v_a>", + schema(function = "validate_network", use_context) +)] pub struct Network { #[serde(default)] + #[validate(custom(function = "validate_model_exists", use_context))] pub model: Option, #[serde(default)] + #[validate(custom(function = "validate_network_ipv4", use_context))] pub ipv4: Option, #[serde(default)] + #[validate(custom(function = "validate_network_ipv6", use_context))] pub ipv6: Option, } -#[derive(Debug, Clone, Serialize, Deserialize, Validate)] +fn validate_network(network: &Network, _context: &ValidateContext) -> Result<(), ValidationError> { + if network.ipv4.is_none() && network.ipv6.is_none() { + return Err(ValidationError::new("badaddr") + .with_message("network must support at least one address type".into())); + } + Ok(()) +} + +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct NetworkIpv4 { pub allocation: String, #[serde(default)] pub gateway: Option, } -#[derive(Debug, Clone, Serialize, Deserialize, Validate)] + +fn validate_network_ipv4( + network_ipv4: &NetworkIpv4, + context: &ValidateContext, +) -> Result<(), ValidationError> { + validate_allocation_exists(&network_ipv4.allocation, context)?; + if let Some(gateway) = &network_ipv4.gateway { + validate_network_gateway(gateway, context)?; + } + Ok(()) +} + +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct NetworkIpv6 { pub allocation: String, #[serde(default)] pub gateway: Option, } +fn validate_network_ipv6( + network_ipv6: &NetworkIpv6, + context: &ValidateContext, +) -> Result<(), ValidationError> { + validate_allocation_exists(&network_ipv6.allocation, context)?; + if let Some(gateway) = &network_ipv6.gateway { + validate_network_gateway(gateway, context)?; + } + Ok(()) +} + #[derive(Debug, Clone, Serialize, Deserialize, Validate)] pub struct NetworkGateway { pub translation: Translation, @@ -165,19 +294,46 @@ pub struct NetworkGateway { pub network: Option, } +fn validate_network_gateway( + gateway: &NetworkGateway, + context: &ValidateContext, +) -> Result<(), ValidationError> { + if let Some(network) = &gateway.network { + validate_network_exists(network, context)?; + } + Ok(()) +} //////////////////////////////////////////////////////////////// #[derive(Debug, Clone, Serialize, Deserialize, Validate)] +#[validate( + context = "ValidateContext<'v_a>", + schema(function = "validate_blueprint", use_context) +)] pub struct Blueprint { #[serde(default)] + #[validate(custom(function = "validate_models_exist", use_context))] pub model: WeightedList, #[serde(default)] + #[validate(custom(function = "validate_blueprint_ipv4", use_context))] pub ipv4: Option, #[serde(default)] + #[validate(custom(function = "validate_blueprint_ipv6", use_context))] pub ipv6: Option, } -#[derive(Debug, Clone, Serialize, Deserialize, Validate)] +fn validate_blueprint( + blueprint: &Blueprint, + _context: &ValidateContext, +) -> Result<(), ValidationError> { + if blueprint.ipv4.is_none() && blueprint.ipv6.is_none() { + return Err(ValidationError::new("badaddr") + .with_message("blueprint must support at least one address type".into())); + } + Ok(()) +} + +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct BlueprintIpv4 { #[serde(default)] pub allocation: Option, @@ -185,7 +341,28 @@ pub struct BlueprintIpv4 { #[serde(default)] pub gateway: Option, } -#[derive(Debug, Clone, Serialize, Deserialize, Validate)] + +fn validate_blueprint_ipv4( + blueprint_ipv4: &BlueprintIpv4, + context: &ValidateContext, +) -> Result<(), ValidationError> { + if let Some(allocation) = &blueprint_ipv4.allocation { + validate_allocation_exists(allocation, context)?; + } + + if blueprint_ipv4.prefix > 32 { + return Err( + ValidationError::new("badprefix").with_message("ipv4 blueprint prefix too long".into()) + ); + } + + if let Some(gateway) = &blueprint_ipv4.gateway { + validate_blueprint_gateway(gateway, context)?; + } + Ok(()) +} + +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct BlueprintIpv6 { #[serde(default)] pub allocation: Option, @@ -194,16 +371,47 @@ pub struct BlueprintIpv6 { pub gateway: Option, } -#[derive(Debug, Clone, Serialize, Deserialize, Validate)] +fn validate_blueprint_ipv6( + blueprint_ipv6: &BlueprintIpv6, + context: &ValidateContext, +) -> Result<(), ValidationError> { + if let Some(allocation) = &blueprint_ipv6.allocation { + validate_allocation_exists(allocation, context)?; + } + + if blueprint_ipv6.prefix > 128 { + return Err( + ValidationError::new("badprefix").with_message("ipv6 blueprint prefix too long".into()) + ); + } + + if let Some(gateway) = &blueprint_ipv6.gateway { + validate_blueprint_gateway(gateway, context)?; + } + Ok(()) +} + +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct BlueprintGateway { pub translation: WeightedList, pub upnp: Probability, pub network: Option, } +fn validate_blueprint_gateway( + gateway: &BlueprintGateway, + context: &ValidateContext, +) -> Result<(), ValidationError> { + gateway.translation.validate_once()?; + if let Some(network) = &gateway.network { + validate_network_exists(network, context)?; + } + Ok(()) +} //////////////////////////////////////////////////////////////// #[derive(Debug, Clone, Serialize, Deserialize, Validate)] +#[validate(schema(function = "validate_subnets"))] pub struct Subnets { #[serde(default)] pub subnet4: Vec, @@ -211,13 +419,35 @@ pub struct Subnets { pub subnet6: Vec, } +fn validate_subnets(subnets: &Subnets) -> Result<(), ValidationError> { + if subnets.subnet4.is_empty() && subnets.subnet6.is_empty() { + return Err(ValidationError::new("badsub") + .with_message("subnets must support at least one address type".into())); + } + Ok(()) +} + #[derive(Debug, Clone, Serialize, Deserialize, Validate)] +#[validate(schema(function = "validate_distance"))] pub struct Distance { pub min: f32, pub max: f32, } +fn validate_distance(distance: &Distance) -> Result<(), ValidationError> { + if distance.min < 0.0 { + return Err(ValidationError::new("baddist") + .with_message("distance minimum must not be negative".into())); + } + if distance.max < distance.min { + return Err(ValidationError::new("baddist") + .with_message("distance maximum must not be less than the minimum".into())); + } + Ok(()) +} + #[derive(Debug, Clone, Serialize, Deserialize, Validate)] +#[validate(schema(function = "validate_distribution"))] pub struct Distribution { pub mean: f32, pub sigma: f32, @@ -226,6 +456,22 @@ pub struct Distribution { pub max: f32, } +fn validate_distribution(distribution: &Distribution) -> Result<(), ValidationError> { + if distribution.mean < 0.0 { + return Err(ValidationError::new("baddistrib") + .with_message("distribution mean must not be negative".into())); + } + if distribution.sigma < distribution.mean { + return Err(ValidationError::new("baddistrib") + .with_message("distribution sigma must not be less than the mean".into())); + } + if distribution.max < distribution.min { + return Err(ValidationError::new("baddistrib") + .with_message("distribution maximum must not be less than the minimum".into())); + } + Ok(()) +} + #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum Translation { @@ -242,17 +488,23 @@ impl Default for Translation { } #[derive(Debug, Clone, Serialize, Deserialize, Validate)] +#[validate(context = "ValidateContext<'v_a>")] pub struct Model { + #[validate(nested)] pub latency: Distribution, #[serde(default)] + #[validate(nested)] pub distance: Option, #[serde(default)] + #[validate(range(min = 0.0, max = 1.0))] pub loss: Probability, } #[derive(Debug, Clone, Serialize, Deserialize, Validate)] +#[validate(context = "ValidateContext<'v_a>")] pub struct Allocation { #[serde(flatten)] + #[validate(nested)] pub subnets: Subnets, } @@ -306,22 +558,22 @@ impl Config { errors = e; } - errors.merge_self("profiles", validate_all_profiles(&out.profiles, &context)); - errors.merge_self("machines", validate_all_machines(&out.machines, &context)); + errors.merge_self("profiles", validate_all_with_args(&out.profiles, &context)); + errors.merge_self("machines", validate_all_with_args(&out.machines, &context)); errors.merge_self( "templates", - validate_all_templates(&out.templates, &context), + validate_all_with_args(&out.templates, &context), ); - errors.merge_self("networks", validate_all_networks(&out.networks, &context)); + errors.merge_self("networks", validate_all_with_args(&out.networks, &context)); errors.merge_self( "blueprints", - validate_all_blueprints(&out.blueprints, &context), + validate_all_with_args(&out.blueprints, &context), ); errors.merge_self( "allocation", - validate_all_allocations(&out.allocations, &context), + validate_all_with_args(&out.allocations, &context), ); - errors.merge_self("models", validate_all_models(&out.models, &context)); + errors.merge_self("models", validate_all_with_args(&out.models, &context)); if !errors.is_empty() { return Err(ConfigError::ValidateError(errors)); @@ -335,6 +587,12 @@ fn validate_instances_exist( value: &Vec, context: &ValidateContext, ) -> Result<(), ValidationError> { + for v in value { + match v { + Instance::Machine { machine } => validate_machines_exist(machine, context)?, + Instance::Template { template } => validate_templates_exist(template, context)?, + } + } Ok(()) } @@ -342,61 +600,103 @@ fn validate_location_exists( value: &Location, context: &ValidateContext, ) -> Result<(), ValidationError> { + match value { + Location::Network { network } => { + network.try_for_each(|m| validate_network_exists(m, context))?; + } + Location::Blueprint { blueprint } => { + blueprint.try_for_each(|t| validate_blueprint_exists(t, context))?; + } + } + Ok(()) } fn validate_network_exists(value: &str, context: &ValidateContext) -> Result<(), ValidationError> { - Ok(()) -} - -fn validate_model_exists(value: &str, context: &ValidateContext) -> Result<(), ValidationError> { - Ok(()) -} - -fn validate_all_profiles( - value: &HashMap, - context: &ValidateContext, -) -> Result<(), ValidationErrors> { - for x in value.values() { - x.validate_with_args(context)? + if !context.config.networks.contains_key(value) { + return Err(ValidationError::new("noexist").with_message("network does not exist".into())); } Ok(()) } -fn validate_all_machines( - value: &HashMap, +fn validate_blueprint_exists( + value: &str, context: &ValidateContext, -) -> Result<(), ValidationErrors> { +) -> Result<(), ValidationError> { + if !context.config.blueprints.contains_key(value) { + return Err(ValidationError::new("noexist").with_message("blueprint does not exist".into())); + } Ok(()) } -fn validate_all_templates( - value: &HashMap, + +fn validate_allocation_exists( + value: &str, context: &ValidateContext, -) -> Result<(), ValidationErrors> { +) -> Result<(), ValidationError> { + if !context.config.allocations.contains_key(value) { + return Err( + ValidationError::new("noexist").with_message("allocation does not exist".into()) + ); + } Ok(()) } -fn validate_all_networks( - value: &HashMap, - context: &ValidateContext, -) -> Result<(), ValidationErrors> { + +fn validate_model_exists(value: &str, context: &ValidateContext) -> Result<(), ValidationError> { + if !context.config.networks.contains_key(value) { + return Err(ValidationError::new("noexist").with_message("model does not exist".into())); + } Ok(()) } -fn validate_all_blueprints( - value: &HashMap, + +fn validate_models_exist( + value: &WeightedList, context: &ValidateContext, -) -> Result<(), ValidationErrors> { +) -> Result<(), ValidationError> { + value.try_for_each(|x| validate_model_exists(x, context)) +} + +fn validate_machine_exists(value: &str, context: &ValidateContext) -> Result<(), ValidationError> { + if !context.config.machines.contains_key(value) { + return Err(ValidationError::new("noexist").with_message("machine does not exist".into())); + } Ok(()) } -fn validate_all_allocations( - value: &HashMap, + +fn validate_machines_exist( + value: &WeightedList, context: &ValidateContext, -) -> Result<(), ValidationErrors> { +) -> Result<(), ValidationError> { + value.try_for_each(|x| validate_machine_exists(x, context)) +} + +fn validate_template_exists(value: &str, context: &ValidateContext) -> Result<(), ValidationError> { + if !context.config.templates.contains_key(value) { + return Err(ValidationError::new("noexist").with_message("template does not exist".into())); + } Ok(()) } -fn validate_all_models( - value: &HashMap, + +fn validate_templates_exist( + value: &WeightedList, context: &ValidateContext, +) -> Result<(), ValidationError> { + value.try_for_each(|x| validate_template_exists(x, context)) +} + +fn validate_all_with_args<'v_a, T: ValidateArgs<'v_a, Args = &'v_a ValidateContext<'v_a>>>( + value: &HashMap, + context: &'v_a ValidateContext, ) -> Result<(), ValidationErrors> { + let mut errors = ValidationErrors::new(); + for (n, x) in value.values().enumerate() { + errors.merge_self( + format!("[{n}]").to_static_str(), + x.validate_with_args(context), + ); + } + if !errors.is_empty() { + return Err(errors); + } Ok(()) } diff --git a/veilid-tools/src/virtual_network/router_server/machine_registry.rs b/veilid-tools/src/virtual_network/router_server/machine_registry.rs index c8f88c3d..700350a3 100644 --- a/veilid-tools/src/virtual_network/router_server/machine_registry.rs +++ b/veilid-tools/src/virtual_network/router_server/machine_registry.rs @@ -1,4 +1,5 @@ use super::*; +use rand::Rng; #[derive(Debug)] struct Machine {} @@ -71,12 +72,26 @@ impl MachineRegistry { match instance_def { config::Instance::Machine { machine } => { - self.create_machine(machine); + let machine = self.weighted_choice(machine); + let machine_def = self + .unlocked_inner + .config + .machines + .get(machine) + .expect("config validation is broken"); + self.create_machine(machine_def).await + } + config::Instance::Template { template } => { + let template = self.weighted_choice(template); + let template_def = self + .unlocked_inner + .config + .templates + .get(template) + .expect("config validation is broken"); + self.create_machine_from_template(template_def).await } - config::Instance::Template { template } => todo!(), } - - Ok(machine_id) } pub async fn release(&self, machine_id: MachineId) -> MachineRegistryResult<()> {} @@ -86,25 +101,88 @@ impl MachineRegistry { async fn create_machine( &self, - machine_def: config::Machine, + machine_def: &config::Machine, ) -> MachineRegistryResult { - // + // Get network from location + + Ok(0) } - fn weighted_choice( + async fn create_machine_from_template( &self, - weighted_list: &config::WeightedList, - ) -> &T { + template_def: &config::Template, + ) -> MachineRegistryResult { + Ok(0) + } + + async fn get_or_create_network_from_location( + &self, + location_def: &config::Location, + ) -> MachineRegistryResult { + match location_def { + config::Location::Network { network } => { + let network = self.weighted_choice(network); + let network_def = self + .unlocked_inner + .config + .networks + .get(network) + .expect("config validation is broken"); + self.get_or_create_network(network, network_def).await + } + config::Location::Blueprint { blueprint } => { + let blueprint = self.weighted_choice(blueprint); + let blueprint_def = self + .unlocked_inner + .config + .blueprints + .get(blueprint) + .expect("config validation is broken"); + self.get_or_create_network_from_blueprint(blueprint, blueprint_def) + .await + } + } + } + + async fn get_or_create_network( + &self, + network: &String, + network_def: &config::Network, + ) -> MachineRegistryResult { + Ok(0) + } + + async fn get_or_create_network_from_blueprint( + &self, + blueprint: &String, + blueprint_def: &config::Blueprint, + ) -> MachineRegistryResult { + Ok(0) + } + + fn weighted_choice<'a, T: fmt::Debug + Clone>( + &self, + weighted_list: &'a config::WeightedList, + ) -> &'a T { match weighted_list { config::WeightedList::Single(x) => x, config::WeightedList::List(vec) => { let total_weight = vec .iter() - .map(|x| match x { - config::Weighted::Weighted { item, weight } => weight, - config::Weighted::Unweighted(item) => 1.0, - }) - .reduce(|acc, x| acc + x); + .map(|x| x.weight()) + .reduce(|acc, x| acc + x) + .expect("config validation broken"); + + let r = rand::thread_rng().gen_range(0.0..=total_weight); + let mut current_weight = 0.0f32; + for x in vec { + current_weight += x.weight(); + if r < current_weight { + return x.item(); + } + } + // Catch f32 imprecision + vec.last().expect("config validation broken").item() } } } diff --git a/veilid-tools/src/virtual_network/router_server/mod.rs b/veilid-tools/src/virtual_network/router_server/mod.rs index e156024a..2916ba35 100644 --- a/veilid-tools/src/virtual_network/router_server/mod.rs +++ b/veilid-tools/src/virtual_network/router_server/mod.rs @@ -42,9 +42,7 @@ struct RouterServerUnlockedInner { } #[derive(Debug)] -struct RouterServerInner { - //tcp_connections: HashMap< -} +struct RouterServerInner {} /// Router server for virtual networking ///