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\n
JSON 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, } })