Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 118 additions & 1 deletion wicket-common/src/inventory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,17 @@ use omicron_common::snake_case_result::SnakeCaseResult;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use sled_agent_types::early_networking::SwitchSlot;
use std::{collections::HashMap, time::Duration};
use sled_hardware_types::Baseboard;
use slog::debug;
use std::collections::{BTreeMap, BTreeSet, HashMap};
use std::net::Ipv6Addr;
use std::time::Duration;
use transceiver_controller::{
Datapath, Monitors, PowerMode, VendorInfo, message::ExtendedStatus,
};

use crate::rack_setup::BootstrapSledDescription;

/// The current state of the v1 Rack as known to wicketd
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "rack_inventory", rename_all = "snake_case")]
Expand All @@ -44,6 +50,117 @@ pub struct MgsV1Inventory {
pub sps: Vec<SpInventory>,
}

// Inventory about sleds derived from MGS inventory and DDM
#[derive(Default, Clone)]
pub struct SledInventory {
pub sleds: BTreeSet<BootstrapSledDescription>,
}

impl From<BTreeSet<BootstrapSledDescription>> for SledInventory {
fn from(sleds: BTreeSet<BootstrapSledDescription>) -> Self {
SledInventory { sleds }
}
}

impl SledInventory {
pub fn new(
inventory: &MgsV1Inventory,
bootstrap_sleds: &BTreeMap<Baseboard, Ipv6Addr>,
log: &slog::Logger,
) -> Self {
let sleds = inventory
.sps
.iter()
.filter_map(|sp| {
if sp.id.type_ != SpType::Sled {
return None;
}

let Some(state) = sp.state.as_ref() else {
debug!(
log,
"sled inventory: filtering out SP with no state";
"sp" => ?sp,
);
return None;
};
let baseboard = Baseboard::new_gimlet(
state.serial_number.clone(),
state.model.clone(),
state.revision,
);
let bootstrap_ip = bootstrap_sleds.get(&baseboard).copied();
Some(BootstrapSledDescription {
id: sp.id,
baseboard,
bootstrap_ip,
})
})
.collect();
SledInventory { sleds }
}

/// Ensure our baseboard is included in inventory, extract the slot for
/// our baseboard, and then ensure that the slot is included in the user
/// supplied `bootstrap_sled_slots`.
pub fn verify_our_baseboard_is_in_inventory_slot(
&self,
bootstrap_sled_slots: &BTreeSet<u16>,
our_baseboard: Option<&Baseboard>,
) -> Result<(), String> {
if let Some(our_baseboard @ Baseboard::Gimlet { .. }) = our_baseboard {
let our_slot = self
.sleds
.iter()
.find_map(|sled| {
if sled.baseboard == *our_baseboard {
Some(sled.id.slot)
} else {
None
}
})
.ok_or_else(|| {
format!(
"Inventory is missing the scrimlet where wicketd is \
running ({our_baseboard:?})",
)
})?;
if !bootstrap_sled_slots.contains(&our_slot) {
return Err(format!(
"Cannot remove the scrimlet where wicketd is running \
(sled {our_slot}: {our_baseboard:?}) \
from bootstrap_sleds"
));
}
}

Ok(())
}

/// Find all bootstrap sleds in inventory with slots matching
/// `bootstrap_sled_slots` and return them. If the slot doesn't exist in
/// inventory, then return an error.
pub fn load_bootstrap_sleds(
&self,
bootstrap_sled_slots: BTreeSet<u16>,
) -> Result<BTreeSet<BootstrapSledDescription>, String> {
let mut bootstrap_sleds = BTreeSet::new();
for slot in bootstrap_sled_slots {
let sled =
self.sleds
.iter()
.find(|sled| sled.id.slot == slot)
.ok_or_else(|| {
format!(
"cannot add unknown sled {slot} to bootstrap_sleds",
)
})?;
bootstrap_sleds.insert(sled.clone());
}
Ok(bootstrap_sleds)
}
}

/// SP-related data
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "sp_inventory", rename_all = "snake_case")]
Expand Down
1 change: 1 addition & 0 deletions wicket-common/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use std::time::Duration;

pub mod example;
pub mod inventory;
pub mod multirack_setup;
pub mod preflight_check;
pub mod rack_setup;
pub mod rack_update;
Expand Down
51 changes: 51 additions & 0 deletions wicket-common/src/multirack_setup.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

//! Types used for adopting an uninitialized rack into an existing regional
//! cluster.

use crate::rack_setup::{
BootstrapSledDescription, GetBgpAuthKeyInfoResponse,
UserSpecifiedRackNetworkConfig,
};
use omicron_common::api::internal::shared::AllowedSourceIps;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use std::collections::BTreeSet;

/// Input from a user for adopting an uninitialized rack into an existing
/// regional cluster.
///
/// This type is provided in the form of a TOML file uploaded via the wicket
/// CLI. It does not contain sensitive user input such as BGP keys. Those are
/// input separately.
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, JsonSchema)]
pub struct MultirackJoinConfigBaseUserInput {
/// List of slot numbers only.
pub bootstrap_slots: BTreeSet<u16>,
pub rack_network_config: UserSpecifiedRackNetworkConfig,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this will work for the scenario where racks are interconnected over BGP. However, for the DDM case where racks are either directly plugged into each other or are connected over a switch and are on the same broadcast domain, we need to identify what external switch ports to treat as underlay ports. One way to introduce this could be turning UserSpecifiedPortConfig into an enum like

enum UserSpecifiedPortConfig {
   MultirackDdmPortConfig,
   SoloRackPortConfig { ... }
}

Where the SoloRackPortConfig contains what UserSpecifiedPortConfig currently contains. I think the multi rack DDM ports should be completely self configuring and not require explicit configuration.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, that's great. This is the turning on all the ports to listen for router announcements/solicitations option that we discussed, right?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Listening for announcements/solicitations yes, but not necessarily all ports. If we take the above approach, just the ports the user indicates as having the MultirackDdmPortConfig.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make sense. Thanks. I had inferred from the lack of a data carrying enum variant on MultirackDdmPortConfig that it was all ports. But that's because I was referring to the UserSpecifiedRackNetworkConfig and not per port config. Poor reading comprehension on my part.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rcgoodfellow I'm looking closer at this as I'm about to start implementing, and I have a few questions.

Is the SoloRackPortConfig really only for the initial RSS rack? It seems like racks could have both types of port configs, and likely will.

What if instead we used something like the following:

enum UserSpecifiedPortConfig {
    DdmAutoPortConfig,
    BgpFullManualConfig
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went ahead and added some support for this in bb42a84

pub allowed_source_ips: AllowedSourceIps,
}

/// A version of the multirack join configuration which contains learned
/// bootstrap sleds and a redacted form of BGP auth keys if they exist.
///
/// This is returned to the user via wicketd and displayed in wicket.
///
/// Note that there are no optional fields here unlike in
/// `CurrentRssUserConfigInsensitive`. This is because wicketd defaults
/// to filling in an empty RSS config for backwards compatibility and
/// allows uploading BGP auth keys before actually uploading the RSS
/// config. However, since we never default to an empty version of the
/// `CurrentMultirackJoinConfig`, we can only fill in the BGP auth keys for
/// this structure after the `MultirackJoinConfigBaseUserInput` is uploaded from
/// a user.
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
pub struct CurrentMultirackJoinUserConfig {
pub bootstrap_sleds: BTreeSet<BootstrapSledDescription>,
pub rack_network_config: UserSpecifiedRackNetworkConfig,
pub allowed_source_ips: AllowedSourceIps,
pub bgp_auth_keys: GetBgpAuthKeyInfoResponse,
}
24 changes: 24 additions & 0 deletions wicketd-api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ use tufaceous_artifact::ArtifactHashId;
use wicket_common::inventory::RackV1Inventory;
use wicket_common::inventory::SpIdentifier;
use wicket_common::inventory::SpType;
use wicket_common::multirack_setup::CurrentMultirackJoinUserConfig;
use wicket_common::multirack_setup::MultirackJoinConfigBaseUserInput;
use wicket_common::preflight_check;
use wicket_common::rack_setup::BgpAuthKey;
use wicket_common::rack_setup::BgpAuthKeyId;
Expand Down Expand Up @@ -78,6 +80,28 @@ pub trait WicketdApi {
body: TypedBody<PutRssUserConfigInsensitive>,
) -> Result<HttpResponseUpdatedNoContent, HttpError>;

/// Get the current status of the multirack join configuration.
#[endpoint {
method = GET,
path = "/rack-setup/config/multirack"
}]
async fn get_multirack_join_config(
rqctx: RequestContext<Self::Context>,
) -> Result<HttpResponseOk<CurrentMultirackJoinUserConfig>, HttpError>;

/// Upload a configuration for joining this rack into an existing regional
/// cluster.
///
/// A multirack join configuration is mutually exclusive with an RSS config.
#[endpoint {
method = PUT,
path = "/rack-setup/config/multirack"
}]
async fn put_multirack_join_config(
rqctx: RequestContext<Self::Context>,
body: TypedBody<MultirackJoinConfigBaseUserInput>,
) -> Result<HttpResponseUpdatedNoContent, HttpError>;

/// Add an external certificate.
///
/// This must be paired with its private key. They may be posted in either
Expand Down
152 changes: 152 additions & 0 deletions wicketd/src/bgp_auth_keys.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

use std::collections::BTreeMap;
use std::collections::btree_map;
use thiserror::Error;
use wicket_common::rack_setup::BgpAuthKey;
use wicket_common::rack_setup::BgpAuthKeyId;
use wicket_common::rack_setup::BgpAuthKeyStatus;
use wicket_common::rack_setup::DisplaySlice;
use wicketd_api::SetBgpAuthKeyStatus;

#[derive(Clone, Debug, PartialEq, Eq, Error)]
pub(crate) enum BgpAuthKeyError {
#[error(
"rack network config not set (upload the config before setting a key)"
)]
RackNetworkConfigNotSet,

#[error(
"key IDs not found: {} (valid key IDs: {})",
DisplaySlice(.not_found),
DisplaySlice(.valid_keys),
)]
KeyIdsNotFound {
not_found: Vec<BgpAuthKeyId>,
valid_keys: Vec<BgpAuthKeyId>,
},
}

/// BGP auth keys are identified by the key ID.
///
/// It is an invariant that any key IDs defined in `rack_network_config` exist
/// here.
///
/// Currently these are always TCP-MD5 keys.
#[derive(Default, Clone)]
pub struct BgpAuthKeys {
keys: BTreeMap<BgpAuthKeyId, Option<BgpAuthKey>>,
}

impl BgpAuthKeys {
pub(crate) fn get(
&self,
key_id: &BgpAuthKeyId,
) -> Option<&Option<BgpAuthKey>> {
self.keys.get(key_id)
}

pub(crate) fn iter(
&self,
) -> btree_map::Iter<'_, BgpAuthKeyId, Option<BgpAuthKey>> {
self.keys.iter()
}

pub(crate) fn check_valid<'a>(
&self,
check_valid: impl IntoIterator<Item = &'a BgpAuthKeyId>,
) -> Result<(), BgpAuthKeyError> {
let not_found: Vec<_> = check_valid
.into_iter()
.filter(|key_id| !self.keys.contains_key(key_id))
.cloned()
.collect();
if !not_found.is_empty() {
return Err(self.make_key_ids_not_found_error(not_found));
}

Ok(())
}

pub(crate) fn get_data(&self) -> BTreeMap<BgpAuthKeyId, BgpAuthKeyStatus> {
self.keys
.iter()
.map(|(key_id, key)| {
let status = key
.as_ref()
.map(|key| BgpAuthKeyStatus::Set { info: key.info() })
.unwrap_or(BgpAuthKeyStatus::Unset);
(key_id.clone(), status)
})
.collect()
}

pub(crate) fn set_key(
&mut self,
key_id: BgpAuthKeyId,
key: BgpAuthKey,
) -> Result<SetBgpAuthKeyStatus, BgpAuthKeyError> {
match self.keys.entry(key_id.clone()) {
btree_map::Entry::Occupied(mut entry) => match entry.get() {
Some(old_key) if old_key == &key => {
Ok(SetBgpAuthKeyStatus::Unchanged)
}
Some(_) => {
entry.insert(Some(key));
Ok(SetBgpAuthKeyStatus::Replaced)
}
None => {
// This is a new key; we don't have it yet.
entry.insert(Some(key));
Ok(SetBgpAuthKeyStatus::Added)
}
},
btree_map::Entry::Vacant(_) => {
Err(self.make_key_ids_not_found_error(vec![key_id]))
}
}
}

/// Sync the key map with a new set of key IDs, preserving existing keys
/// where possible and dropping keys that are no longer referenced.
pub(crate) fn sync_keys(
&mut self,
new_key_ids: impl IntoIterator<Item = BgpAuthKeyId>,
) {
let mut old_keys = std::mem::take(&mut self.keys);
self.keys = new_key_ids
.into_iter()
.map(|key_id| {
(
key_id.clone(),
// For each new key, either grab the corresponding old key,
// or initialize to None.
old_keys.remove(&key_id).unwrap_or_else(|| None),
)
})
.collect();
}

#[must_use]
fn make_key_ids_not_found_error(
&self,
key_ids: Vec<BgpAuthKeyId>,
) -> BgpAuthKeyError {
let valid_key_ids = self.keys.keys().cloned().collect();
BgpAuthKeyError::KeyIdsNotFound {
not_found: key_ids,
valid_keys: valid_key_ids,
}
}

#[cfg(test)]
pub(crate) fn insert(
&mut self,
key_id: BgpAuthKeyId,
key: Option<BgpAuthKey>,
) {
self.keys.insert(key_id, key);
}
}
Loading
Loading