[skip ci] template work

This commit is contained in:
Christien Rioux 2024-12-11 20:44:23 -05:00
parent 29f1e2da11
commit 65629f03e9
7 changed files with 251 additions and 127 deletions

View File

@ -217,6 +217,45 @@ fn validate_machine(machine: &Machine, _context: &ValidateContext) -> Result<(),
Ok(()) Ok(())
} }
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum MachineLocation {
Network {
network: String,
#[serde(default)]
address4: Option<Ipv4Addr>,
#[serde(default)]
address6: Option<Ipv6Addr>,
},
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)] #[derive(Debug, Clone, Serialize, Deserialize, Validate)]
#[validate( #[validate(
context = "ValidateContext<'v_a>", context = "ValidateContext<'v_a>",
@ -247,15 +286,28 @@ fn validate_template(
#[derive(Debug, Clone, Serialize, Deserialize, Validate)] #[derive(Debug, Clone, Serialize, Deserialize, Validate)]
#[validate(schema(function = "validate_template_limits"))] #[validate(schema(function = "validate_template_limits"))]
pub struct TemplateLimits { pub struct TemplateLimits {
/// maximum number of machines this template will generate
#[validate(nested)] #[validate(nested)]
pub machine_count: WeightedList<u32>, #[serde(default)]
pub machine_count: Option<WeightedList<u32>>,
#[validate(nested)]
pub machines_per_network: WeightedList<u32>,
} }
fn validate_template_limits(limits: &TemplateLimits) -> Result<(), ValidationError> { 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 { if *x == 0 {
return Err(ValidationError::new("badcount") 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(()) Ok(())
})?; })?;
@ -263,39 +315,6 @@ fn validate_template_limits(limits: &TemplateLimits) -> Result<(), ValidationErr
Ok(()) Ok(())
} }
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum MachineLocation {
Specific {
network: String,
#[serde(default)]
address4: Option<Ipv4Addr>,
#[serde(default)]
address6: Option<Ipv6Addr>,
},
}
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)] #[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)] #[serde(untagged)]
pub enum TemplateLocation { pub enum TemplateLocation {
@ -433,18 +452,22 @@ fn validate_blueprint(
#[derive(Debug, Clone, Serialize, Deserialize, Validate)] #[derive(Debug, Clone, Serialize, Deserialize, Validate)]
#[validate(schema(function = "validate_blueprint_limits"))] #[validate(schema(function = "validate_blueprint_limits"))]
pub struct BlueprintLimits { pub struct BlueprintLimits {
/// maximum number of networks this blueprint will generate
#[validate(nested)] #[validate(nested)]
pub network_count: WeightedList<u32>, #[serde(default)]
pub network_count: Option<WeightedList<u32>>,
} }
fn validate_blueprint_limits(limits: &BlueprintLimits) -> Result<(), ValidationError> { fn validate_blueprint_limits(limits: &BlueprintLimits) -> Result<(), ValidationError> {
limits.network_count.try_for_each(|x| { if let Some(network_count) = &limits.network_count {
if *x == 0 { network_count.try_for_each(|x| {
return Err(ValidationError::new("badcount") if *x == 0 {
.with_message("blueprint limits has zero machine count".into())); return Err(ValidationError::new("badcount")
} .with_message("blueprint limits has zero network count".into()));
Ok(()) }
})?; Ok(())
})?;
}
Ok(()) Ok(())
} }

View File

@ -64,9 +64,11 @@ machines:
################################################################# #################################################################
# Templates # Templates
# #
# Templates are used to generate Machines that are all on a single # Templates are used to generate Machines
# network. A maximum number of machines are allocated on the # * if networks are specified, then all machines are created on that
# network within the limits specified. # single network. A maximum number of machines are allocated on the
# network within the limits specified.
# * if a blueprint is spec
templates: templates:
# Default servers on the boot network # Default servers on the boot network
@ -77,22 +79,23 @@ templates:
bootrelay: bootrelay:
network: "boot" network: "boot"
machine_count: 4 machine_count: 4
machines_per_network: 4
# Servers on subnets within the 'internet' network # Servers on subnets within the 'internet' network
relayserver: relayserver:
blueprint: "direct" blueprint: "direct"
machine_count: [1, 2, 3] machines_per_network: [1, 2, 3]
ipv4server: ipv4server:
blueprint: "direct_ipv4_no_ipv6" blueprint: "direct_ipv4_no_ipv6"
machine_count: [1, 2, 3] machines_per_network: [1, 2, 3]
ipv6server: ipv6server:
blueprint: "direct_ipv6_no_ipv4" blueprint: "direct_ipv6_no_ipv4"
machine_count: [1, 2, 3] machines_per_network: [1, 2, 3]
nat4home: nat4home:
blueprint: "nat_ipv4_no_ipv6" blueprint: "nat_ipv4_no_ipv6"
machine_count: [1, 2, 3] machines_per_network: [1, 2, 3]
nat4+6home: nat4+6home:
blueprint: "nat_ipv4_direct_ipv6" blueprint: "nat_ipv4_direct_ipv6"
machine_count: [1, 2, 3] machines_per_network: [1, 2, 3]
################################################################# #################################################################
# Networks # Networks

View File

@ -264,51 +264,14 @@ impl MachineRegistryInner {
let machine_def = { let machine_def = {
// Get the active template state // Get the active template state
let template_state = self.get_or_create_template_state(&name, template_def)?; 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); return Err(MachineRegistryError::TemplateComplete);
} }
// Pick or instantiate an available network // Pick or instantiate an available network
xxx add 'def()' selector to all types template_state.instantiate(self)?
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);
};
// Activate some blueprint and pick a network xxx how to pass through per-network limits
}
};
// 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,
}
}; };
// Allocate a machine id // Allocate a machine id
@ -321,9 +284,9 @@ impl MachineRegistryInner {
// Create a new machine state // Create a new machine state
let machine_state = match MachineState::try_new( let machine_state = match MachineState::try_new(
self, self,
machine_id,
MachineStateName::Template(name.clone()), MachineStateName::Template(name.clone()),
machine_def.clone(), machine_def.clone(),
machine_id,
) { ) {
Ok(v) => v, Ok(v) => v,
Err(e) => { Err(e) => {
@ -345,12 +308,10 @@ impl MachineRegistryInner {
// Return the unique id // Return the unique id
Ok(machine_id) Ok(machine_id)
} }
pub(super) fn get_template_state( pub(super) fn get_template_state(&self, name: &String) -> MachineRegistryResult<TemplateState> {
&mut self,
name: &String,
) -> MachineRegistryResult<&mut TemplateState> {
self.template_state_by_name self.template_state_by_name
.get_mut(name) .get(name)
.cloned()
.ok_or_else(|| MachineRegistryError::TemplateNotFound) .ok_or_else(|| MachineRegistryError::TemplateNotFound)
} }
@ -359,7 +320,7 @@ impl MachineRegistryInner {
machine_location: &config::MachineLocation, machine_location: &config::MachineLocation,
) -> MachineRegistryResult<NetworkState> { ) -> MachineRegistryResult<NetworkState> {
match machine_location { match machine_location {
config::MachineLocation::Specific { config::MachineLocation::Network {
network: name, network: name,
address4: _, address4: _,
address6: _, address6: _,
@ -373,6 +334,17 @@ impl MachineRegistryInner {
.expect("config validation is broken"); .expect("config validation is broken");
self.get_or_create_network_state(name.clone(), network_def) 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( pub(super) fn get_or_create_network_state_from_template_location(
@ -406,11 +378,12 @@ impl MachineRegistryInner {
} }
pub(super) fn get_blueprint_state( pub(super) fn get_blueprint_state(
&mut self, &self,
name: &String, name: &String,
) -> MachineRegistryResult<&mut BlueprintState> { ) -> MachineRegistryResult<BlueprintState> {
self.blueprint_state_by_name self.blueprint_state_by_name
.get_mut(name) .get(name)
.cloned()
.ok_or_else(|| MachineRegistryError::BlueprintNotFound) .ok_or_else(|| MachineRegistryError::BlueprintNotFound)
} }
@ -533,26 +506,18 @@ impl MachineRegistryInner {
&mut self, &mut self,
name: String, name: String,
blueprint_def: config::Blueprint, blueprint_def: config::Blueprint,
) -> MachineRegistryResult<NetworkId> { ) -> MachineRegistryResult<NetworkState> {
// Get the active blueprint state // Get the active blueprint state
let blueprint_state = self.get_or_create_blueprint_state(&name, blueprint_def)?; let blueprint_state = self.get_or_create_blueprint_state(&name, blueprint_def)?;
if !blueprint_state.is_active(self)? { if !blueprint_state.is_active(self)? {
return Err(MachineRegistryError::BlueprintComplete); return Err(MachineRegistryError::BlueprintComplete);
} }
//xxx // Make network def from current blueprint state
// Make machine def from current template state let machine_def = config::Network {
let machine_def = config::Machine { model: self.unlocked_inner.srng.weighted_choice(blueprint_state),
location: match template_state.template_def.location.clone() { ipv4: todo!(),
config::TemplateLocation::Network { network } => { ipv6: todo!(),
config::MachineLocation::Network { network }
}
config::TemplateLocation::Blueprint { blueprint } => {
config::MachineLocation::Blueprint { blueprint }
}
},
disable_capabilities: template_state.template_def.disable_capabilities.clone(),
bootstrap: false,
}; };
// Allocate a machine id // Allocate a machine id

View File

@ -8,7 +8,7 @@ struct BlueprintStateUnlockedInner {
#[derive(Debug)] #[derive(Debug)]
struct BlueprintStateInner { struct BlueprintStateInner {
limit_network_count: u32, limit_network_count: Option<u32>,
networks: HashSet<NetworkId>, networks: HashSet<NetworkId>,
} }
@ -24,10 +24,12 @@ impl BlueprintState {
name: String, name: String,
blueprint_def: config::Blueprint, blueprint_def: config::Blueprint,
) -> MachineRegistryResult<BlueprintState> { ) -> MachineRegistryResult<BlueprintState> {
let limit_network_count = *machine_registry_inner let limit_network_count = blueprint_def.limits.network_count.as_ref().map(|nc| {
.unlocked_inner *machine_registry_inner
.srng .unlocked_inner
.weighted_choice(&blueprint_def.limits.network_count); .srng
.weighted_choice(nc)
});
Ok(Self { Ok(Self {
unlocked_inner: Arc::new(BlueprintStateUnlockedInner { 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( pub fn is_active(
&self, &self,
machine_registry_inner: &MachineRegistryInner, machine_registry_inner: &MachineRegistryInner,

View File

@ -60,7 +60,7 @@ impl MachineState {
// Make the default route interface // Make the default route interface
let machine_location = machine_def.location.clone(); let machine_location = machine_def.location.clone();
let (allocate_v4, opt_address4, allocate_v6, opt_address6) = match machine_location { let (allocate_v4, opt_address4, allocate_v6, opt_address6) = match machine_location {
config::MachineLocation::Specific { config::MachineLocation::Network {
network: _, network: _,
address4, address4,
address6, address6,
@ -70,6 +70,9 @@ impl MachineState {
network_state.is_ipv6() && address6.is_some(), network_state.is_ipv6() && address6.is_some(),
address6, address6,
), ),
config::MachineLocation::Blueprint { blueprint: _ } => {
(network_state.is_ipv4(), None, network_state.is_ipv6(), None)
}
}; };
if allocate_v4 { if allocate_v4 {
@ -163,6 +166,10 @@ impl MachineState {
self.unlocked_inner.name.clone() self.unlocked_inner.name.clone()
} }
pub fn def(&self) -> &config::Machine {
&self.unlocked_inner.machine_def
}
pub fn id(&self) -> MachineId { pub fn id(&self) -> MachineId {
self.unlocked_inner.id self.unlocked_inner.id
} }

View File

@ -143,6 +143,10 @@ impl NetworkState {
self.unlocked_inner.name.clone() self.unlocked_inner.name.clone()
} }
pub fn def(&self) -> &config::Network {
&self.unlocked_inner.network_def
}
pub fn id(&self) -> NetworkId { pub fn id(&self) -> NetworkId {
self.unlocked_inner.id self.unlocked_inner.id
} }

View File

@ -8,7 +8,8 @@ struct TemplateStateUnlockedInner {
#[derive(Debug)] #[derive(Debug)]
struct TemplateStateInner { struct TemplateStateInner {
limit_machine_count: u32, limit_machine_count: Option<u32>,
limit_machines_per_network: u32,
machines: HashSet<MachineId>, machines: HashSet<MachineId>,
} }
@ -24,15 +25,22 @@ impl TemplateState {
name: String, name: String,
template_def: config::Template, template_def: config::Template,
) -> MachineRegistryResult<TemplateState> { ) -> MachineRegistryResult<TemplateState> {
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 .unlocked_inner
.srng .srng
.weighted_choice(&template_def.limits.machine_count); .weighted_choice(&template_def.limits.machines_per_network);
Ok(Self { Ok(Self {
unlocked_inner: Arc::new(TemplateStateUnlockedInner { name, template_def }), unlocked_inner: Arc::new(TemplateStateUnlockedInner { name, template_def }),
inner: Arc::new(Mutex::new(TemplateStateInner { inner: Arc::new(Mutex::new(TemplateStateInner {
limit_machine_count, limit_machine_count,
limit_machines_per_network,
machines: HashSet::new(), machines: HashSet::new(),
})), })),
}) })
@ -42,8 +50,116 @@ impl TemplateState {
self.unlocked_inner.name.clone() self.unlocked_inner.name.clone()
} }
pub fn is_active(&self) -> MachineRegistryResult<bool> { pub fn def(&self) -> &config::Template {
&self.unlocked_inner.template_def
}
pub fn is_active(
&self,
machine_registry_inner: &mut MachineRegistryInner,
) -> MachineRegistryResult<bool> {
let inner = self.inner.lock(); 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<config::Machine> {
// 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,
})
} }
} }