diff --git a/Cargo.lock b/Cargo.lock
index a364393393..a362c64eef 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -10919,6 +10919,7 @@ dependencies = [
"sha2",
"sled-hardware-types",
"thiserror",
+ "toml 0.8.12",
"update-engine",
]
diff --git a/clients/wicketd-client/src/lib.rs b/clients/wicketd-client/src/lib.rs
index 38b0b99928..27ef2ad219 100644
--- a/clients/wicketd-client/src/lib.rs
+++ b/clients/wicketd-client/src/lib.rs
@@ -81,6 +81,7 @@ progenitor::generate_api!(
StepEventForWicketdEngineSpec = wicket_common::update_events::StepEvent,
SwitchLocation = omicron_common::api::internal::shared::SwitchLocation,
UserSpecifiedBgpPeerConfig = wicket_common::rack_setup::UserSpecifiedBgpPeerConfig,
+ UserSpecifiedImportExportPolicy = wicket_common::rack_setup::UserSpecifiedImportExportPolicy,
UserSpecifiedPortConfig = wicket_common::rack_setup::UserSpecifiedPortConfig,
UserSpecifiedRackNetworkConfig = wicket_common::rack_setup::UserSpecifiedRackNetworkConfig,
ImportExportPolicy = omicron_common::api::internal::shared::ImportExportPolicy,
diff --git a/openapi/wicketd.json b/openapi/wicketd.json
index 098412522e..f4c5b34a6e 100644
--- a/openapi/wicketd.json
+++ b/openapi/wicketd.json
@@ -1599,47 +1599,6 @@
}
}
},
- "ImportExportPolicy": {
- "description": "Define policy relating to the import and export of prefixes from a BGP peer.",
- "oneOf": [
- {
- "description": "Do not perform any filtering.",
- "type": "object",
- "properties": {
- "type": {
- "type": "string",
- "enum": [
- "no_filtering"
- ]
- }
- },
- "required": [
- "type"
- ]
- },
- {
- "type": "object",
- "properties": {
- "type": {
- "type": "string",
- "enum": [
- "allow"
- ]
- },
- "value": {
- "type": "array",
- "items": {
- "$ref": "#/components/schemas/IpNet"
- }
- }
- },
- "required": [
- "type",
- "value"
- ]
- }
- ]
- },
"InstallableArtifacts": {
"type": "object",
"properties": {
@@ -4891,24 +4850,20 @@
"format": "ipv4"
},
"allowed_export": {
- "description": "Apply import policy to this peer with an allow list.",
- "default": {
- "type": "no_filtering"
- },
+ "description": "Apply export policy to this peer with an allow list.",
+ "default": null,
"allOf": [
{
- "$ref": "#/components/schemas/ImportExportPolicy"
+ "$ref": "#/components/schemas/UserSpecifiedImportExportPolicy"
}
]
},
"allowed_import": {
- "description": "Apply export policy to this peer with an allow list.",
- "default": {
- "type": "no_filtering"
- },
+ "description": "Apply import policy to this peer with an allow list.",
+ "default": null,
"allOf": [
{
- "$ref": "#/components/schemas/ImportExportPolicy"
+ "$ref": "#/components/schemas/UserSpecifiedImportExportPolicy"
}
]
},
@@ -5030,6 +4985,13 @@
],
"additionalProperties": false
},
+ "UserSpecifiedImportExportPolicy": {
+ "nullable": true,
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/IpNet"
+ }
+ },
"UserSpecifiedPortConfig": {
"description": "User-specified version of [`PortConfigV1`].\n\nAll of [`PortConfigV1`] is user-specified. But we expect the port name to be a key, rather than a field as in [`PortConfigV1`]. So this has all of the fields other than the port name.\n\n[`PortConfigV1`]: omicron_common::api::internal::shared::PortConfigV1",
"type": "object",
@@ -5109,7 +5071,8 @@
"infra_ip_last",
"switch0",
"switch1"
- ]
+ ],
+ "additionalProperties": false
},
"IgnitionCommand": {
"description": "Ignition command.\n\nJSON schema
\n\n```json { \"description\": \"Ignition command.\", \"type\": \"string\", \"enum\": [ \"power_on\", \"power_off\", \"power_reset\" ] } ``` ",
diff --git a/wicket-common/Cargo.toml b/wicket-common/Cargo.toml
index 8909bb9e69..385cd5ab0a 100644
--- a/wicket-common/Cargo.toml
+++ b/wicket-common/Cargo.toml
@@ -19,3 +19,6 @@ sled-hardware-types.workspace = true
thiserror.workspace = true
update-engine.workspace = true
omicron-workspace-hack.workspace = true
+
+[dev-dependencies]
+toml.workspace = true
diff --git a/wicket-common/src/example.rs b/wicket-common/src/example.rs
index 2491dbea5e..e9e49b314b 100644
--- a/wicket-common/src/example.rs
+++ b/wicket-common/src/example.rs
@@ -10,11 +10,8 @@ use gateway_client::types::{SpIdentifier, SpType};
use maplit::{btreemap, btreeset};
use omicron_common::{
address::{IpRange, Ipv4Range},
- api::{
- external::ImportExportPolicy,
- internal::shared::{
- BgpConfig, BgpPeerConfig, PortFec, PortSpeed, RouteConfig,
- },
+ api::internal::shared::{
+ BgpConfig, BgpPeerConfig, PortFec, PortSpeed, RouteConfig,
},
};
use sled_hardware_types::Baseboard;
@@ -22,7 +19,8 @@ use sled_hardware_types::Baseboard;
use crate::rack_setup::{
BgpAuthKeyId, BootstrapSledDescription, CurrentRssUserConfigInsensitive,
PutRssUserConfigInsensitive, UserSpecifiedBgpPeerConfig,
- UserSpecifiedPortConfig, UserSpecifiedRackNetworkConfig,
+ UserSpecifiedImportExportPolicy, UserSpecifiedPortConfig,
+ UserSpecifiedRackNetworkConfig,
};
/// A collection of example data structures.
@@ -91,6 +89,78 @@ impl ExampleRackSetupData {
})];
let external_dns_ips = vec!["10.0.0.1".parse().unwrap()];
let ntp_servers = vec!["ntp1.com".into(), "ntp2.com".into()];
+
+ let switch0_port0_bgp_peers = vec![
+ UserSpecifiedBgpPeerConfig {
+ asn: 47,
+ addr: "10.2.3.4".parse().unwrap(),
+ port: "port0".into(),
+ hold_time: Some(BgpPeerConfig::DEFAULT_HOLD_TIME),
+ idle_hold_time: Some(BgpPeerConfig::DEFAULT_IDLE_HOLD_TIME),
+ connect_retry: Some(BgpPeerConfig::DEFAULT_CONNECT_RETRY),
+ delay_open: Some(BgpPeerConfig::DEFAULT_DELAY_OPEN),
+ keepalive: Some(BgpPeerConfig::DEFAULT_KEEPALIVE),
+ communities: Vec::new(),
+ enforce_first_as: false,
+ local_pref: None,
+ min_ttl: None,
+ auth_key_id: Some(bgp_key_1_id.clone()),
+ multi_exit_discriminator: None,
+ remote_asn: None,
+ allowed_import: UserSpecifiedImportExportPolicy::NoFiltering,
+ allowed_export: UserSpecifiedImportExportPolicy::Allow(vec![
+ "127.0.0.1/8".parse().unwrap(),
+ ]),
+ vlan_id: None,
+ },
+ UserSpecifiedBgpPeerConfig {
+ asn: 28,
+ addr: "10.2.3.5".parse().unwrap(),
+ port: "port0".into(),
+ remote_asn: Some(200),
+ hold_time: Some(10),
+ idle_hold_time: Some(20),
+ connect_retry: Some(30),
+ delay_open: Some(40),
+ keepalive: Some(50),
+ communities: vec![60, 70],
+ enforce_first_as: true,
+ local_pref: Some(80),
+ min_ttl: Some(90),
+ auth_key_id: Some(bgp_key_2_id.clone()),
+ multi_exit_discriminator: Some(100),
+ allowed_import: UserSpecifiedImportExportPolicy::Allow(vec![
+ "64:ff9b::/96".parse().unwrap(),
+ "255.255.0.0/16".parse().unwrap(),
+ ]),
+ allowed_export: UserSpecifiedImportExportPolicy::Allow(vec![]),
+ vlan_id: None,
+ },
+ ];
+
+ let switch1_port0_bgp_peers = vec![UserSpecifiedBgpPeerConfig {
+ asn: 47,
+ addr: "10.2.3.4".parse().unwrap(),
+ port: "port0".into(),
+ hold_time: Some(BgpPeerConfig::DEFAULT_HOLD_TIME),
+ idle_hold_time: Some(BgpPeerConfig::DEFAULT_IDLE_HOLD_TIME),
+ connect_retry: Some(BgpPeerConfig::DEFAULT_CONNECT_RETRY),
+ delay_open: Some(BgpPeerConfig::DEFAULT_DELAY_OPEN),
+ keepalive: Some(BgpPeerConfig::DEFAULT_KEEPALIVE),
+ communities: Vec::new(),
+ enforce_first_as: false,
+ local_pref: None,
+ min_ttl: None,
+ auth_key_id: Some(bgp_key_1_id.clone()),
+ multi_exit_discriminator: None,
+ remote_asn: None,
+ allowed_import: UserSpecifiedImportExportPolicy::Allow(vec![
+ "224.0.0.0/4".parse().unwrap(),
+ ]),
+ allowed_export: UserSpecifiedImportExportPolicy::NoFiltering,
+ vlan_id: None,
+ }];
+
let rack_network_config = UserSpecifiedRackNetworkConfig {
infra_ip_first: "172.30.0.1".parse().unwrap(),
infra_ip_last: "172.30.0.10".parse().unwrap(),
@@ -102,48 +172,7 @@ impl ExampleRackSetupData {
nexthop: "172.30.0.10".parse().unwrap(),
vlan_id: Some(1),
}],
- bgp_peers: vec![
- UserSpecifiedBgpPeerConfig {
- asn: 47,
- addr: "10.2.3.4".parse().unwrap(),
- port: "port0".into(),
- hold_time: Some(BgpPeerConfig::DEFAULT_HOLD_TIME),
- idle_hold_time: Some(BgpPeerConfig::DEFAULT_IDLE_HOLD_TIME),
- connect_retry: Some(BgpPeerConfig::DEFAULT_CONNECT_RETRY),
- delay_open: Some(BgpPeerConfig::DEFAULT_DELAY_OPEN),
- keepalive: Some(BgpPeerConfig::DEFAULT_KEEPALIVE),
- communities: Vec::new(),
- enforce_first_as: false,
- local_pref: None,
- min_ttl: None,
- auth_key_id: Some(bgp_key_1_id.clone()),
- multi_exit_discriminator: None,
- remote_asn: None,
- allowed_import: ImportExportPolicy::NoFiltering,
- allowed_export: ImportExportPolicy::NoFiltering,
- vlan_id: None,
- },
- UserSpecifiedBgpPeerConfig {
- asn: 28,
- addr: "10.2.3.5".parse().unwrap(),
- port: "port0".into(),
- remote_asn: Some(200),
- hold_time: Some(10),
- idle_hold_time: Some(20),
- connect_retry: Some(30),
- delay_open: Some(40),
- keepalive: Some(50),
- communities: vec![60, 70],
- enforce_first_as: true,
- local_pref: Some(80),
- min_ttl: Some(90),
- auth_key_id: Some(bgp_key_2_id.clone()),
- multi_exit_discriminator: Some(100),
- allowed_import: ImportExportPolicy::NoFiltering,
- allowed_export: ImportExportPolicy::NoFiltering,
- vlan_id: None,
- },
- ],
+ bgp_peers: switch0_port0_bgp_peers,
uplink_port_speed: PortSpeed::Speed400G,
uplink_port_fec: PortFec::Firecode,
autoneg: true,
@@ -159,28 +188,7 @@ impl ExampleRackSetupData {
nexthop: "172.33.0.10".parse().unwrap(),
vlan_id: Some(1),
}],
- bgp_peers: vec![
- UserSpecifiedBgpPeerConfig {
- asn: 47,
- addr: "10.2.3.4".parse().unwrap(),
- port: "port0".into(),
- hold_time: Some(BgpPeerConfig::DEFAULT_HOLD_TIME),
- idle_hold_time: Some(BgpPeerConfig::DEFAULT_IDLE_HOLD_TIME),
- connect_retry: Some(BgpPeerConfig::DEFAULT_CONNECT_RETRY),
- delay_open: Some(BgpPeerConfig::DEFAULT_DELAY_OPEN),
- keepalive: Some(BgpPeerConfig::DEFAULT_KEEPALIVE),
- communities: Vec::new(),
- enforce_first_as: false,
- local_pref: None,
- min_ttl: None,
- auth_key_id: Some(bgp_key_1_id.clone()),
- multi_exit_discriminator: None,
- remote_asn: None,
- allowed_import: ImportExportPolicy::NoFiltering,
- allowed_export: ImportExportPolicy::NoFiltering,
- vlan_id: None,
- },
- ],
+ bgp_peers: switch1_port0_bgp_peers,
uplink_port_speed: PortSpeed::Speed400G,
uplink_port_fec: PortFec::Firecode,
autoneg: true,
@@ -261,7 +269,7 @@ fn apply_tweak(
match tweak {
ExampleRackSetupDataTweak::OneBgpPeerPerPort => {
let rnc = current_insensitive.rack_network_config.as_mut().unwrap();
- for (_, _, port) in rnc.iter_ports_mut() {
+ for (_, _, port) in rnc.iter_uplinks_mut() {
// Remove all but the first BGP peer.
port.bgp_peers.drain(1..);
}
diff --git a/wicket-common/src/rack_setup.rs b/wicket-common/src/rack_setup.rs
index ef13505ca3..30574d281f 100644
--- a/wicket-common/src/rack_setup.rs
+++ b/wicket-common/src/rack_setup.rs
@@ -9,6 +9,7 @@ pub use gateway_client::types::SpType as GatewaySpType;
use ipnetwork::IpNetwork;
use omicron_common::address;
use omicron_common::api::external::ImportExportPolicy;
+use omicron_common::api::external::IpNet;
use omicron_common::api::external::Name;
use omicron_common::api::external::SwitchLocation;
use omicron_common::api::internal::shared::BgpConfig;
@@ -22,6 +23,7 @@ use owo_colors::Style;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
+use serde::Serializer;
use sha2::Digest;
use sha2::Sha256;
use sled_hardware_types::Baseboard;
@@ -92,6 +94,7 @@ pub struct BootstrapSledDescription {
/// User-specified parts of
/// [`RackNetworkConfig`](omicron_common::api::internal::shared::RackNetworkConfig).
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, JsonSchema)]
+#[serde(deny_unknown_fields)]
pub struct UserSpecifiedRackNetworkConfig {
pub infra_ip_first: Ipv4Addr,
pub infra_ip_last: Ipv4Addr,
@@ -105,7 +108,7 @@ pub struct UserSpecifiedRackNetworkConfig {
impl UserSpecifiedRackNetworkConfig {
/// Returns all BGP auth key IDs in the rack network config.
pub fn get_bgp_auth_key_ids(&self) -> BTreeSet {
- self.iter_ports()
+ self.iter_uplinks()
.flat_map(|(_, _, cfg)| cfg.bgp_peers.iter())
.filter_map(|peer| peer.auth_key_id.as_ref())
.cloned()
@@ -128,8 +131,8 @@ impl UserSpecifiedRackNetworkConfig {
!self.switch0.is_empty() || !self.switch1.is_empty()
}
- /// Returns an iterator over all ports.
- pub fn iter_ports(
+ /// Returns an iterator over all uplinks -- (switch, port, config) triples.
+ pub fn iter_uplinks(
&self,
) -> impl Iterator-
{
@@ -146,8 +149,8 @@ impl UserSpecifiedRackNetworkConfig {
iter0.chain(iter1)
}
- /// Returns a mutable iterator over all (switch, port, config) triples.
- pub fn iter_ports_mut(
+ /// Returns a mutable iterator over all uplinks -- (switch, port, config) triples.
+ pub fn iter_uplinks_mut(
&mut self,
) -> impl Iterator
-
{
@@ -236,12 +239,12 @@ pub struct UserSpecifiedBgpPeerConfig {
/// Enforce that the first AS in paths received from this peer is the peer's AS.
#[serde(default)]
pub enforce_first_as: bool,
- /// Apply export policy to this peer with an allow list.
- #[serde(default)]
- pub allowed_import: ImportExportPolicy,
/// Apply import policy to this peer with an allow list.
#[serde(default)]
- pub allowed_export: ImportExportPolicy,
+ pub allowed_import: UserSpecifiedImportExportPolicy,
+ /// Apply export policy to this peer with an allow list.
+ #[serde(default)]
+ pub allowed_export: UserSpecifiedImportExportPolicy,
/// Associate a VLAN ID with a BGP peer session.
#[serde(default)]
pub vlan_id: Option,
@@ -429,3 +432,137 @@ impl BgpAuthKeyStatus {
matches!(self, BgpAuthKeyStatus::Unset)
}
}
+
+/// User-friendly serializer and deserializer for `ImportExportPolicy`.
+///
+/// This serializes the "NoFiltering" variant as `null`, and the "Allow"
+/// variant as a list.
+///
+/// This would ordinarily just be a module used with `#[serde(with)]`, but
+/// schemars requires that it be a type since it needs to know the JSON schema
+/// corresponding to it.
+#[derive(Clone, Debug, Default, PartialEq, Eq)]
+pub enum UserSpecifiedImportExportPolicy {
+ #[default]
+ NoFiltering,
+ Allow(Vec),
+}
+
+impl From for ImportExportPolicy {
+ fn from(policy: UserSpecifiedImportExportPolicy) -> Self {
+ match policy {
+ UserSpecifiedImportExportPolicy::NoFiltering => {
+ ImportExportPolicy::NoFiltering
+ }
+ UserSpecifiedImportExportPolicy::Allow(list) => {
+ ImportExportPolicy::Allow(list)
+ }
+ }
+ }
+}
+
+impl Serialize for UserSpecifiedImportExportPolicy {
+ fn serialize
(&self, serializer: S) -> Result
+ where
+ S: Serializer,
+ {
+ match self {
+ UserSpecifiedImportExportPolicy::NoFiltering => {
+ serializer.serialize_none()
+ }
+ UserSpecifiedImportExportPolicy::Allow(list) => {
+ list.serialize(serializer)
+ }
+ }
+ }
+}
+
+impl<'de> Deserialize<'de> for UserSpecifiedImportExportPolicy {
+ fn deserialize(deserializer: D) -> Result
+ where
+ D: serde::Deserializer<'de>,
+ {
+ struct V;
+
+ impl<'de> serde::de::Visitor<'de> for V {
+ type Value = UserSpecifiedImportExportPolicy;
+
+ fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+ formatter.write_str("an array of IP prefixes, or null")
+ }
+
+ // Note: null is represented by visit_unit, not visit_none.
+ fn visit_unit(self) -> Result {
+ Ok(UserSpecifiedImportExportPolicy::NoFiltering)
+ }
+
+ fn visit_seq(self, mut seq: A) -> Result
+ where
+ A: serde::de::SeqAccess<'de>,
+ {
+ let mut list = Vec::new();
+ while let Some(ipnet) = seq.next_element::()? {
+ list.push(ipnet);
+ }
+ Ok(UserSpecifiedImportExportPolicy::Allow(list))
+ }
+ }
+
+ deserializer.deserialize_any(V)
+ }
+}
+
+impl JsonSchema for UserSpecifiedImportExportPolicy {
+ fn schema_name() -> String {
+ "UserSpecifiedImportExportPolicy".to_string()
+ }
+
+ fn json_schema(
+ gen: &mut schemars::gen::SchemaGenerator,
+ ) -> schemars::schema::Schema {
+ // The above is equivalent to an Option>.
+ Option::>::json_schema(gen)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn roundtrip_import_export_policy() {
+ let inputs = [
+ UserSpecifiedImportExportPolicy::Allow(vec![
+ "64:ff9b::/96".parse().unwrap(),
+ "255.255.0.0/16".parse().unwrap(),
+ ]),
+ UserSpecifiedImportExportPolicy::NoFiltering,
+ UserSpecifiedImportExportPolicy::Allow(vec![]),
+ ];
+
+ for input in &inputs {
+ let input = Wrapper { policy: input.clone() };
+
+ eprintln!("** input: {:?}, testing JSON", input);
+ // Check that serialization to JSON and back works.
+ let serialized = serde_json::to_string(&input).unwrap();
+ eprintln!("serialized JSON: {serialized}");
+ let deserialized: Wrapper =
+ serde_json::from_str(&serialized).unwrap();
+ assert_eq!(input, deserialized);
+
+ eprintln!("** input: {:?}, testing TOML", input);
+ // Check that serialization to TOML and back works.
+ let serialized = toml::to_string(&input).unwrap();
+ eprintln!("serialized TOML: {serialized}");
+ let deserialized: Wrapper = toml::from_str(&serialized).unwrap();
+ assert_eq!(input, deserialized);
+ }
+ }
+
+ #[derive(Debug, Deserialize, Serialize, PartialEq, Eq)]
+ struct Wrapper {
+ #[serde(default)]
+ policy: UserSpecifiedImportExportPolicy,
+ }
+}
diff --git a/wicket/src/cli/rack_setup/config_template.toml b/wicket/src/cli/rack_setup/config_template.toml
index 7b34285ee5..63b0fb4429 100644
--- a/wicket/src/cli/rack_setup/config_template.toml
+++ b/wicket/src/cli/rack_setup/config_template.toml
@@ -118,9 +118,23 @@ infra_ip_last = ""
# Apply a local preference to routes sent to the peer (optional).
# local_pref = 0
- # Enforce that the first AS in paths received from the peer is the peer's AS.
+ # Enforce that the first AS in paths received from the peer is the
+ # peer's AS.
enforce_first_as = false
+ # Apply import policy to this peer with an allowlist of prefixes
+ # (optional). Defaults to allowing all prefixes. Use an empty list to
+ # indicate that no prefixes are allowed.
+ # allowed_import = ["224.0.0.0/8"]
+
+ # Apply export policy to this peer with an allowlist of prefixes
+ # (optional). Defaults to allowing all prefixes. Use an empty list to
+ # indicate that no prefixes are allowed.
+ # allowed_export = []
+
+ # Associate a VLAN ID with this BGP session (optional).
+ # vlan_id = 0
+
[rack_network_config.switch1]
# Optional BGP configuration, as a list of entries. Duplicate or remove this
diff --git a/wicket/src/cli/rack_setup/config_toml.rs b/wicket/src/cli/rack_setup/config_toml.rs
index 4ef583ac93..641d09fe35 100644
--- a/wicket/src/cli/rack_setup/config_toml.rs
+++ b/wicket/src/cli/rack_setup/config_toml.rs
@@ -6,7 +6,6 @@
//! (most of) the rack setup configuration.
use omicron_common::address::IpRange;
-use omicron_common::api::external::ImportExportPolicy;
use omicron_common::api::internal::shared::BgpConfig;
use omicron_common::api::internal::shared::RouteConfig;
use serde::Serialize;
@@ -26,6 +25,7 @@ use wicket_common::rack_setup::BootstrapSledDescription;
use wicket_common::rack_setup::CurrentRssUserConfigInsensitive;
use wicket_common::rack_setup::GatewaySpType;
use wicket_common::rack_setup::UserSpecifiedBgpPeerConfig;
+use wicket_common::rack_setup::UserSpecifiedImportExportPolicy;
use wicket_common::rack_setup::UserSpecifiedPortConfig;
use wicket_common::rack_setup::UserSpecifiedRackNetworkConfig;
@@ -393,7 +393,7 @@ fn populate_uplink_table(cfg: &UserSpecifiedPortConfig) -> Table {
}
// allowed import policy
- if let ImportExportPolicy::Allow(list) = allowed_import {
+ if let UserSpecifiedImportExportPolicy::Allow(list) = allowed_import {
let mut out = Array::new();
for x in list.iter() {
out.push(string_value(x.to_string()));
@@ -402,7 +402,7 @@ fn populate_uplink_table(cfg: &UserSpecifiedPortConfig) -> Table {
}
// allowed export policy
- if let ImportExportPolicy::Allow(list) = allowed_export {
+ if let UserSpecifiedImportExportPolicy::Allow(list) = allowed_export {
let mut out = Array::new();
for x in list.iter() {
out.push(string_value(x.to_string()));
diff --git a/wicket/src/ui/panes/rack_setup.rs b/wicket/src/ui/panes/rack_setup.rs
index 21f42b496e..88c5891bb5 100644
--- a/wicket/src/ui/panes/rack_setup.rs
+++ b/wicket/src/ui/panes/rack_setup.rs
@@ -19,6 +19,8 @@ use crate::Control;
use crate::State;
use itertools::Itertools;
use omicron_common::address::IpRange;
+use omicron_common::api::internal::shared::BgpConfig;
+use omicron_common::api::internal::shared::RouteConfig;
use ratatui::layout::Constraint;
use ratatui::layout::Direction;
use ratatui::layout::Layout;
@@ -35,7 +37,13 @@ use sled_hardware_types::Baseboard;
use std::borrow::Cow;
use wicket_common::rack_setup::BgpAuthKeyInfo;
use wicket_common::rack_setup::BgpAuthKeyStatus;
+use wicket_common::rack_setup::CurrentRssUserConfigInsensitive;
+use wicket_common::rack_setup::UserSpecifiedBgpPeerConfig;
+use wicket_common::rack_setup::UserSpecifiedImportExportPolicy;
+use wicket_common::rack_setup::UserSpecifiedPortConfig;
+use wicket_common::rack_setup::UserSpecifiedRackNetworkConfig;
use wicketd_client::types::CurrentRssUserConfig;
+use wicketd_client::types::CurrentRssUserConfigSensitive;
use wicketd_client::types::RackOperationStatus;
#[derive(Debug)]
@@ -632,40 +640,52 @@ fn rss_config_text<'a>(
return Text::styled("Rack Setup Unavailable", label_style);
};
- let sensitive = &config.sensitive;
- let insensitive = &config.insensitive;
+ let CurrentRssUserConfigSensitive {
+ bgp_auth_keys,
+ num_external_certificates,
+ recovery_silo_password_set,
+ } = &config.sensitive;
+ let CurrentRssUserConfigInsensitive {
+ bootstrap_sleds,
+ ntp_servers,
+ dns_servers,
+ internal_services_ip_pool_ranges,
+ external_dns_ips,
+ external_dns_zone_name,
+ rack_network_config,
+ } = &config.insensitive;
// Special single-line values, where we convert some kind of condition into
// a user-appropriate string.
spans.push(Line::from(vec![
Span::styled("Uploaded cert/key pairs: ", label_style),
Span::styled(
- sensitive.num_external_certificates.to_string(),
- dyn_style(sensitive.num_external_certificates > 0),
+ num_external_certificates.to_string(),
+ dyn_style(*num_external_certificates > 0),
),
]));
spans.push(Line::from(vec![
Span::styled("Recovery password set: ", label_style),
- dyn_span(sensitive.recovery_silo_password_set, "Yes", "No"),
+ dyn_span(*recovery_silo_password_set, "Yes", "No"),
]));
- let net_config = insensitive.rack_network_config.as_ref();
-
// List of single-line values, each of which may or may not be set; if it's
// set we show its value, and if not we show "Not set" in bad_style.
for (label, contents) in [
(
"External DNS zone name: ",
- Cow::from(insensitive.external_dns_zone_name.as_str()),
+ Cow::from(external_dns_zone_name.as_str()),
),
(
"Infrastructure first IP: ",
- net_config
+ rack_network_config
+ .as_ref()
.map_or("".into(), |c| c.infra_ip_first.to_string().into()),
),
(
"Infrastructure last IP: ",
- net_config
+ rack_network_config
+ .as_ref()
.map_or("".into(), |c| c.infra_ip_last.to_string().into()),
),
] {
@@ -697,123 +717,195 @@ fn rss_config_text<'a>(
vec![Span::styled(" • ", label_style), Span::styled(item, ok_style)]
};
- if let Some(cfg) = insensitive.rack_network_config.as_ref() {
- for (i, (switch, _port, uplink)) in cfg.iter_ports().enumerate() {
- // TODO: show port, use exhaustive destructure
+ if let Some(cfg) = rack_network_config.as_ref() {
+ // This style ensures that if a new field is added to the struct, it
+ // fails to compile.
+ let UserSpecifiedRackNetworkConfig {
+ // infra_ip_first and infra_ip_last have already been handled above.
+ infra_ip_first: _,
+ infra_ip_last: _,
+ // switch0 and switch1 re handled via the iter_uplinks iterator.
+ switch0: _,
+ switch1: _,
+ bgp,
+ } = cfg;
+
+ for (i, (switch, port, uplink)) in cfg.iter_uplinks().enumerate() {
+ let UserSpecifiedPortConfig {
+ routes,
+ addresses,
+ uplink_port_speed,
+ uplink_port_fec,
+ autoneg,
+ bgp_peers,
+ } = uplink;
+
let mut items = vec![
vec![
- Span::styled(" • Switch : ", label_style),
+ Span::styled(" • Port : ", label_style),
+ Span::styled(port.to_string(), ok_style),
+ Span::styled(" on switch ", label_style),
Span::styled(switch.to_string(), ok_style),
],
vec![
- Span::styled(" • Speed : ", label_style),
- Span::styled(
- uplink.uplink_port_speed.to_string(),
- ok_style,
- ),
+ Span::styled(" • Speed : ", label_style),
+ Span::styled(uplink_port_speed.to_string(), ok_style),
+ ],
+ vec![
+ Span::styled(" • FEC : ", label_style),
+ Span::styled(uplink_port_fec.to_string(), ok_style),
],
vec![
- Span::styled(" • FEC : ", label_style),
- Span::styled(uplink.uplink_port_fec.to_string(), ok_style),
+ Span::styled(" • Autoneg : ", label_style),
+ if *autoneg {
+ Span::styled("enabled", ok_style)
+ } else {
+ // bad_style isn't right here because there's no
+ // necessary action item, but green/ok isn't also
+ // right. So use warn_style.
+ Span::styled("disabled", warn_style)
+ },
],
];
- let routes = uplink.routes.iter().map(|r| {
- vec![
- Span::styled(" • Route : ", label_style),
+ let routes = routes.iter().map(|r| {
+ let RouteConfig { destination, nexthop, vlan_id } = r;
+
+ let mut items = vec![
+ Span::styled(" • Route : ", label_style),
Span::styled(
- format!("{} -> {}", r.destination, r.nexthop),
+ format!("{} -> {}", destination, nexthop),
ok_style,
),
- ]
+ ];
+ if let Some(vlan_id) = vlan_id {
+ items.extend([
+ Span::styled(" (vlan_id=", label_style),
+ Span::styled(vlan_id.to_string(), ok_style),
+ Span::styled(")", label_style),
+ ]);
+ }
+
+ items
});
- let addresses = uplink.addresses.iter().map(|a| {
+ let addresses = addresses.iter().map(|a| {
vec![
- Span::styled(" • Address : ", label_style),
+ Span::styled(" • Address : ", label_style),
Span::styled(a.to_string(), ok_style),
]
});
- let peers = uplink.bgp_peers.iter().flat_map(|p| {
+ let peers = bgp_peers.iter().flat_map(|p| {
+ let UserSpecifiedBgpPeerConfig {
+ asn,
+ port,
+ addr,
+
+ // These values are accessed via methods, since they have
+ // defaults defined by the methods.
+ hold_time: _,
+ idle_hold_time: _,
+ delay_open: _,
+ connect_retry: _,
+ keepalive: _,
+
+ remote_asn,
+ min_ttl,
+ auth_key_id,
+ multi_exit_discriminator,
+ communities,
+ local_pref,
+ enforce_first_as,
+ allowed_import,
+ allowed_export,
+ vlan_id,
+ } = p;
+
let mut lines = vec![
vec![
- Span::styled(" • BGP peer : ", label_style),
- Span::styled(p.addr.to_string(), ok_style),
- Span::styled(" ASN=", label_style),
- Span::styled(p.asn.to_string(), ok_style),
- Span::styled(" Port=", label_style),
- Span::styled(p.port.clone(), ok_style),
+ Span::styled(" • BGP peer : ", label_style),
+ Span::styled(addr.to_string(), ok_style),
+ Span::styled(" asn=", label_style),
+ Span::styled(asn.to_string(), ok_style),
+ Span::styled(" port=", label_style),
+ Span::styled(port.clone(), ok_style),
],
vec![
- Span::styled(" Intervals :", label_style),
- Span::styled(" Hold=", label_style),
+ Span::styled(" Intervals :", label_style),
+ Span::styled(" hold=", label_style),
Span::styled(format!("{}s", p.hold_time()), ok_style),
- Span::styled(" Idle hold=", label_style),
+ Span::styled(" idle_hold=", label_style),
Span::styled(
format!("{}s", p.idle_hold_time()),
ok_style,
),
- Span::styled(" Delay open=", label_style),
+ Span::styled(" delay_open=", label_style),
Span::styled(format!("{}s", p.delay_open()), ok_style),
- Span::styled(" Connect retry=", label_style),
+ Span::styled(" connect_retry=", label_style),
Span::styled(
format!("{}s", p.connect_retry()),
ok_style,
),
- Span::styled(" Keepalive=", label_style),
+ Span::styled(" keepalive=", label_style),
Span::styled(format!("{}s", p.keepalive()), ok_style),
],
];
{
// These are all optional settings.
let mut settings =
- vec![Span::styled(" Settings :", label_style)];
+ vec![Span::styled(" Settings :", label_style)];
- if let Some(remote_asn) = &p.remote_asn {
+ if let Some(remote_asn) = remote_asn {
settings.extend([
- Span::styled(" Remote ASN=", label_style),
+ Span::styled(" remote_asn=", label_style),
Span::styled(remote_asn.to_string(), ok_style),
]);
}
- if let Some(min_ttl) = &p.min_ttl {
+ if let Some(min_ttl) = min_ttl {
settings.extend([
- Span::styled(" Min TTL=", label_style),
+ Span::styled(" min_ttl=", label_style),
Span::styled(min_ttl.to_string(), ok_style),
]);
}
if let Some(multi_exit_discriminator) =
- &p.multi_exit_discriminator
+ multi_exit_discriminator
{
settings.extend([
- Span::styled(" MED=", label_style),
+ Span::styled(" med=", label_style),
Span::styled(
multi_exit_discriminator.to_string(),
ok_style,
),
]);
}
- if let Some(local_pref) = &p.local_pref {
+ if let Some(local_pref) = local_pref {
settings.extend([
- Span::styled(" Local pref=", label_style),
+ Span::styled(" local_pref=", label_style),
Span::styled(local_pref.to_string(), ok_style),
]);
}
- if p.enforce_first_as {
+ if *enforce_first_as {
settings.extend([
- Span::styled(" Enforce first AS=", label_style),
+ Span::styled(" enforce_first_as=", label_style),
Span::styled("true", ok_style),
]);
}
- if !p.communities.is_empty() {
+ if !communities.is_empty() {
settings.extend([
- Span::styled(" Communities=", label_style),
+ Span::styled(" communities=", label_style),
Span::styled(
- p.communities.iter().join(" "),
+ communities.iter().join(","),
ok_style,
),
]);
}
+ if let Some(vlan_id) = vlan_id {
+ settings.extend([
+ Span::styled(" vlan_id=", label_style),
+ Span::styled(vlan_id.to_string(), ok_style),
+ ]);
+ }
// We always push one element in -- check if any other
// elements were pushed.
@@ -822,10 +914,10 @@ fn rss_config_text<'a>(
}
}
- if let Some(auth_key_id) = &p.auth_key_id {
+ if let Some(auth_key_id) = auth_key_id {
let mut auth_key_line =
- vec![Span::styled(" Auth key : ", label_style)];
- match sensitive.bgp_auth_keys.data.get(auth_key_id) {
+ vec![Span::styled(" Auth key : ", label_style)];
+ match bgp_auth_keys.data.get(auth_key_id) {
Some(BgpAuthKeyStatus::Unset) => {
auth_key_line.extend([
Span::styled(
@@ -861,7 +953,7 @@ fn rss_config_text<'a>(
bad_style,
),
Span::styled(
- "Unknown (internal error)",
+ "unknown (internal error)",
bad_style,
),
]);
@@ -870,6 +962,53 @@ fn rss_config_text<'a>(
lines.push(auth_key_line);
}
+ let import_export_policy_line =
+ |label: &'static str,
+ policy: &UserSpecifiedImportExportPolicy|
+ -> Option>> {
+ match policy {
+ UserSpecifiedImportExportPolicy::NoFiltering => {
+ None
+ }
+ UserSpecifiedImportExportPolicy::Allow(
+ prefixes,
+ ) => {
+ let mut line =
+ vec![Span::styled(label, label_style)];
+ if prefixes.is_empty() {
+ line.push(Span::styled(
+ "no prefixes allowed",
+ warn_style,
+ ));
+ } else {
+ line.push(Span::styled(
+ "allowed=",
+ label_style,
+ ));
+ line.push(Span::styled(
+ prefixes.iter().join(","),
+ ok_style,
+ ));
+ }
+
+ Some(line)
+ }
+ }
+ };
+
+ if let Some(allowed_import) = import_export_policy_line(
+ " Import policy : ",
+ allowed_import,
+ ) {
+ lines.push(allowed_import);
+ }
+ if let Some(allowed_export) = import_export_policy_line(
+ " Export policy : ",
+ allowed_export,
+ ) {
+ lines.push(allowed_export);
+ }
+
lines
});
@@ -879,33 +1018,51 @@ fn rss_config_text<'a>(
append_list(
&mut spans,
- Cow::from(format!("Port {}: ", i + 1)),
+ Cow::from(format!("Uplink {}: ", i + 1)),
items,
);
}
+
+ // Show BGP configuration.
+ for cfg in bgp {
+ let BgpConfig {
+ asn,
+ originate,
+ // The shaper and checker are not currently used.
+ shaper: _,
+ checker: _,
+ } = cfg;
+ let mut items = vec![
+ Span::styled(" • BGP config :", label_style),
+ Span::styled(" asn=", label_style),
+ Span::styled(asn.to_string(), ok_style),
+ Span::styled(" originate=", label_style),
+ ];
+ if originate.is_empty() {
+ items.push(Span::styled("None", warn_style));
+ } else {
+ items.push(Span::styled(originate.iter().join(","), ok_style));
+ }
+ spans.push(Line::from(items));
+ }
} else {
- append_list(&mut spans, "Ports: ".into(), vec![]);
+ append_list(&mut spans, "Uplinks: ".into(), vec![]);
}
append_list(
&mut spans,
"NTP servers: ".into(),
- insensitive.ntp_servers.iter().cloned().map(plain_list_item).collect(),
+ ntp_servers.iter().cloned().map(plain_list_item).collect(),
);
append_list(
&mut spans,
"DNS servers: ".into(),
- insensitive
- .dns_servers
- .iter()
- .map(|s| plain_list_item(s.to_string()))
- .collect(),
+ dns_servers.iter().map(|s| plain_list_item(s.to_string())).collect(),
);
append_list(
&mut spans,
"Internal services IP pool ranges: ".into(),
- insensitive
- .internal_services_ip_pool_ranges
+ internal_services_ip_pool_ranges
.iter()
.map(|r| {
let s = match r {
@@ -919,8 +1076,7 @@ fn rss_config_text<'a>(
append_list(
&mut spans,
"External DNS IPs: ".into(),
- insensitive
- .external_dns_ips
+ external_dns_ips
.iter()
.cloned()
.map(|ip| plain_list_item(ip.to_string()))
@@ -929,8 +1085,7 @@ fn rss_config_text<'a>(
append_list(
&mut spans,
"Sleds: ".into(),
- insensitive
- .bootstrap_sleds
+ bootstrap_sleds
.iter()
.map(|desc| {
let identifier = match &desc.baseboard {
diff --git a/wicket/tests/output/example_non_empty.toml b/wicket/tests/output/example_non_empty.toml
index 7288bd12c8..44c4c88ca6 100644
--- a/wicket/tests/output/example_non_empty.toml
+++ b/wicket/tests/output/example_non_empty.toml
@@ -72,6 +72,7 @@ delay_open = 0
connect_retry = 3
keepalive = 2
auth_key_id = "bgp-key-1"
+allowed_export = ["127.0.0.1/8"]
enforce_first_as = false
[[rack_network_config.switch0.port0.bgp_peers]]
@@ -88,6 +89,8 @@ min_ttl = 90
auth_key_id = "bgp-key-2"
multi_exit_discriminator = 100
communities = [60, 70]
+allowed_import = ["64:ff9b::/96", "255.255.0.0/16"]
+allowed_export = []
local_pref = 80
enforce_first_as = true
@@ -108,6 +111,7 @@ delay_open = 0
connect_retry = 3
keepalive = 2
auth_key_id = "bgp-key-1"
+allowed_import = ["224.0.0.0/4"]
enforce_first_as = false
[[rack_network_config.bgp]]
diff --git a/wicketd/src/rss_config.rs b/wicketd/src/rss_config.rs
index a0904705c3..4994d0f1ec 100644
--- a/wicketd/src/rss_config.rs
+++ b/wicketd/src/rss_config.rs
@@ -621,7 +621,7 @@ fn validate_rack_network_config(
// TODO this implies a single contiguous range for port IPs which is over
// constraining
// iterate through each port config
- for (_, _, port_config) in config.iter_ports() {
+ for (_, _, port_config) in config.iter_uplinks() {
for addr in &port_config.addresses {
// ... and check that it contains `uplink_ip`.
if addr.ip() < infra_ip_range.first
@@ -649,7 +649,7 @@ fn validate_rack_network_config(
infra_ip_first: config.infra_ip_first,
infra_ip_last: config.infra_ip_last,
ports: config
- .iter_ports()
+ .iter_uplinks()
.map(|(switch, port, config)| {
build_port_config(switch, port, config, bgp_auth_keys)
})
@@ -737,8 +737,8 @@ fn build_port_config(
min_ttl: p.min_ttl,
multi_exit_discriminator: p.multi_exit_discriminator,
remote_asn: p.remote_asn,
- allowed_export: p.allowed_export.clone(),
- allowed_import: p.allowed_import.clone(),
+ allowed_export: p.allowed_export.clone().into(),
+ allowed_import: p.allowed_import.clone().into(),
vlan_id: p.vlan_id,
}
})