[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(())
}
#[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)]
#[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<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> {
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<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)]
#[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<u32>,
#[serde(default)]
pub network_count: Option<WeightedList<u32>>,
}
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(())
}

View File

@ -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

View File

@ -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<TemplateState> {
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<NetworkState> {
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<BlueprintState> {
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<NetworkId> {
) -> MachineRegistryResult<NetworkState> {
// 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

View File

@ -8,7 +8,7 @@ struct BlueprintStateUnlockedInner {
#[derive(Debug)]
struct BlueprintStateInner {
limit_network_count: u32,
limit_network_count: Option<u32>,
networks: HashSet<NetworkId>,
}
@ -24,10 +24,12 @@ impl BlueprintState {
name: String,
blueprint_def: config::Blueprint,
) -> MachineRegistryResult<BlueprintState> {
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,

View File

@ -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
}

View File

@ -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
}

View File

@ -8,7 +8,8 @@ struct TemplateStateUnlockedInner {
#[derive(Debug)]
struct TemplateStateInner {
limit_machine_count: u32,
limit_machine_count: Option<u32>,
limit_machines_per_network: u32,
machines: HashSet<MachineId>,
}
@ -24,15 +25,22 @@ impl TemplateState {
name: String,
template_def: config::Template,
) -> 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
.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<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();
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,
})
}
}