diff --git a/.github/buildomat/jobs/deploy.sh b/.github/buildomat/jobs/deploy.sh index a2aac86aec..2dde4286dc 100755 --- a/.github/buildomat/jobs/deploy.sh +++ b/.github/buildomat/jobs/deploy.sh @@ -238,7 +238,7 @@ infra_ip_last = \"$UPLINK_IP\" /^routes/c\\ routes = \\[{nexthop = \"$GATEWAY_IP\", destination = \"0.0.0.0/0\"}\\] /^addresses/c\\ -addresses = \\[\"$UPLINK_IP/24\"\\] +addresses = \\[{address = \"$UPLINK_IP/24\"} \\] } " pkg/config-rss.toml diff -u pkg/config-rss.toml{~,} || true diff --git a/clients/sled-agent-client/src/lib.rs b/clients/sled-agent-client/src/lib.rs index 862ae00cc9..1edd464cd5 100644 --- a/clients/sled-agent-client/src/lib.rs +++ b/clients/sled-agent-client/src/lib.rs @@ -33,8 +33,9 @@ progenitor::generate_api!( BgpConfig = { derives = [Eq, Hash] }, BgpPeerConfig = { derives = [Eq, Hash] }, OmicronPhysicalDiskConfig = { derives = [Eq, Hash, PartialOrd, Ord] }, - PortConfigV1 = { derives = [Eq, Hash] }, + PortConfigV2 = { derives = [Eq, Hash] }, RouteConfig = { derives = [Eq, Hash] }, + UplinkAddressConfig = { derives = [Eq, Hash] }, VirtualNetworkInterfaceHost = { derives = [Eq, Hash] }, }, crates = { diff --git a/clients/wicketd-client/src/lib.rs b/clients/wicketd-client/src/lib.rs index 8edb797b20..6198c6cf9e 100644 --- a/clients/wicketd-client/src/lib.rs +++ b/clients/wicketd-client/src/lib.rs @@ -24,7 +24,7 @@ progenitor::generate_api!( GetLocationResponse = { derives = [PartialEq, Eq, PartialOrd, Ord] }, ImageVersion = { derives = [PartialEq, Eq, PartialOrd, Ord]}, RackInitId = { derives = [PartialEq, Eq, PartialOrd, Ord] }, - RackNetworkConfigV1 = { derives = [PartialEq, Eq, PartialOrd, Ord] }, + RackNetworkConfigV2 = { derives = [PartialEq, Eq, PartialOrd, Ord] }, RackOperationStatus = { derives = [PartialEq, Eq, PartialOrd, Ord] }, RackResetId = { derives = [PartialEq, Eq, PartialOrd, Ord] }, RackV1Inventory = { derives = [PartialEq, Eq, PartialOrd, Ord]}, @@ -62,7 +62,7 @@ progenitor::generate_api!( Ipv4Range = omicron_common::address::Ipv4Range, Ipv6Range = omicron_common::address::Ipv6Range, M2Slot = installinator_common::M2Slot, - PortConfigV1 = omicron_common::api::internal::shared::PortConfigV1, + PortConfigV2 = omicron_common::api::internal::shared::PortConfigV2, PortFec = omicron_common::api::internal::shared::PortFec, PortSpeed = omicron_common::api::internal::shared::PortSpeed, ProgressEventForGenericSpec = update_engine::events::ProgressEvent, diff --git a/common/src/api/external/mod.rs b/common/src/api/external/mod.rs index 6b171b59fe..0af437bd99 100644 --- a/common/src/api/external/mod.rs +++ b/common/src/api/external/mod.rs @@ -2553,6 +2553,9 @@ pub struct SwitchPortAddressConfig { /// The IP address and prefix. pub address: oxnet::IpNet, + /// An optional VLAN ID + pub vlan_id: Option, + /// The interface name this address belongs to. // TODO: https://github.com/oxidecomputer/omicron/issues/3050 // Use `Name` instead of `String` for `interface_name` type diff --git a/common/src/api/internal/shared.rs b/common/src/api/internal/shared.rs index 9e3e1a71f5..3d710fc952 100644 --- a/common/src/api/internal/shared.rs +++ b/common/src/api/internal/shared.rs @@ -152,13 +152,17 @@ pub enum SourceNatConfigError { UnalignedPortPair { first_port: u16, last_port: u16 }, } +// We alias [`PortConfig`] to the current version of the protocol, so +// that we can convert between versions as necessary. +pub type PortConfig = PortConfigV2; + // We alias [`RackNetworkConfig`] to the current version of the protocol, so // that we can convert between versions as necessary. -pub type RackNetworkConfig = RackNetworkConfigV1; +pub type RackNetworkConfig = RackNetworkConfigV2; /// Initial network configuration #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, JsonSchema)] -pub struct RackNetworkConfigV1 { +pub struct RackNetworkConfigV2 { pub rack_subnet: Ipv6Net, // TODO: #3591 Consider making infra-ip ranges implicit for uplinks /// First ip address to be used for configuring network infrastructure @@ -166,7 +170,7 @@ pub struct RackNetworkConfigV1 { /// Last ip address to be used for configuring network infrastructure pub infra_ip_last: Ipv4Addr, /// Uplinks for connecting the rack to external networks - pub ports: Vec, + pub ports: Vec, /// BGP configurations for connecting the rack to external networks pub bgp: Vec, /// BFD configuration for connecting the rack to external networks @@ -299,12 +303,81 @@ pub struct RouteConfig { pub vlan_id: Option, } +#[derive( + Clone, Debug, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Hash, +)] +pub struct UplinkAddressConfig { + pub address: IpNet, + /// The VLAN id (if any) associated with this address. + #[serde(default)] + pub vlan_id: Option, +} + +impl UplinkAddressConfig { + pub fn addr(&self) -> IpAddr { + self.address.addr() + } +} + +impl std::fmt::Display for UplinkAddressConfig { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.vlan_id { + None => write!(f, "{}", self.address), + Some(v) => write!(f, "{};{}", self.address, v), + } + } +} + +#[derive(Debug, PartialEq, Eq, Deserialize, Serialize)] +pub struct UplinkAddressConfigError(String); + +impl std::fmt::Display for UplinkAddressConfigError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "parse switch location error: {}", self.0) + } +} + +/// Convert a string into an UplinkAddressConfig. +/// 192.168.1.1/24 => UplinkAddressConfig { 192.168.1.1/24, None } +/// 192.168.1.1/24;200 => UplinkAddressConfig { 192.168.1.1/24, Some(200) } +impl FromStr for UplinkAddressConfig { + type Err = UplinkAddressConfigError; + + fn from_str(s: &str) -> Result { + let fields: Vec<&str> = s.split(';').collect(); + let (address, vlan_id) = match fields.len() { + 1 => Ok((fields[0], None)), + 2 => Ok((fields[0], Some(fields[1]))), + _ => Err(UplinkAddressConfigError(format!( + "not a valid uplink address: {s}" + ))), + }?; + let address = address.parse().map_err(|_| { + UplinkAddressConfigError(format!( + "not a valid ip address: {address}" + )) + })?; + let vlan_id = match vlan_id { + None => Ok(None), + Some(v) => match v.parse() { + Err(_) => Err(format!("invalid vlan id: {v}")), + Ok(vlan_id) if vlan_id > 1 && vlan_id < 4096 => { + Ok(Some(vlan_id)) + } + Ok(vlan_id) => Err(format!("vlan id out of range: {vlan_id}")), + }, + } + .map_err(|e| UplinkAddressConfigError(e))?; + Ok(UplinkAddressConfig { address, vlan_id }) + } +} + #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, JsonSchema)] -pub struct PortConfigV1 { +pub struct PortConfigV2 { /// The set of routes associated with this port. pub routes: Vec, - /// This port's addresses. - pub addresses: Vec, + /// This port's addresses and optional vlan IDs + pub addresses: Vec, /// Switch the port belongs to. pub switch: SwitchLocation, /// Nmae of the port this config applies to. @@ -320,46 +393,6 @@ pub struct PortConfigV1 { pub autoneg: bool, } -impl From for PortConfigV1 { - fn from(value: UplinkConfig) -> Self { - PortConfigV1 { - routes: vec![RouteConfig { - destination: "0.0.0.0/0".parse().unwrap(), - nexthop: value.gateway_ip.into(), - vlan_id: None, - }], - addresses: vec![value.uplink_cidr.into()], - switch: value.switch, - port: value.uplink_port, - uplink_port_speed: value.uplink_port_speed, - uplink_port_fec: value.uplink_port_fec, - bgp_peers: vec![], - autoneg: false, - } - } -} - -/// Deprecated, use PortConfigV1 instead. Cannot actually deprecate due to -/// -#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, JsonSchema)] -pub struct UplinkConfig { - /// Gateway address - pub gateway_ip: Ipv4Addr, - /// Switch to use for uplink - pub switch: SwitchLocation, - /// Switchport to use for external connectivity - pub uplink_port: String, - /// Speed for the Switchport - pub uplink_port_speed: PortSpeed, - /// Forward Error Correction setting for the uplink port - pub uplink_port_fec: PortFec, - /// IP Address and prefix (e.g., `192.168.0.1/16`) to apply to switchport - /// (must be in infra_ip pool) - pub uplink_cidr: Ipv4Net, - /// VLAN id to use for uplink - pub uplink_vid: Option, -} - /// A set of switch uplinks. #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] pub struct SwitchPorts { @@ -372,12 +405,12 @@ pub struct HostPortConfig { pub port: String, /// IP Address and prefix (e.g., `192.168.0.1/16`) to apply to switchport - /// (must be in infra_ip pool) - pub addrs: Vec, + /// (must be in infra_ip pool). May also include an optional VLAN ID. + pub addrs: Vec, } -impl From for HostPortConfig { - fn from(x: PortConfigV1) -> Self { +impl From for HostPortConfig { + fn from(x: PortConfigV2) -> Self { Self { port: x.port, addrs: x.addresses } } } diff --git a/dev-tools/xtask/src/virtual_hardware.rs b/dev-tools/xtask/src/virtual_hardware.rs index 0ec9f91492..5384433f55 100644 --- a/dev-tools/xtask/src/virtual_hardware.rs +++ b/dev-tools/xtask/src/virtual_hardware.rs @@ -114,7 +114,7 @@ const ZPOOL: &'static str = "/usr/sbin/zpool"; const ZONEADM: &'static str = "/usr/sbin/zoneadm"; const SIDECAR_LITE_COMMIT: &'static str = - "960f11afe859e0316088e04578aedb700fba6159"; + "de6fab7885a6bbc5327accffd2a872a31e2f1cb6"; const SOFTNPU_COMMIT: &'static str = "3203c51cf4473d30991b522062ac0df2e045c2f2"; const PXA_MAC_DEFAULT: &'static str = "a8:e1:de:01:70:1d"; diff --git a/docs/how-to-run.adoc b/docs/how-to-run.adoc index 50c4b4e174..097467ef04 100644 --- a/docs/how-to-run.adoc +++ b/docs/how-to-run.adoc @@ -292,7 +292,7 @@ routes = [{nexthop = "192.168.1.199", destination = "0.0.0.0/0"}] # Addresses associated with this port. # For softnpu, an address within the "infra" block above that will be used for # the softnpu uplink port. You can just pick the first address in that pool. -addresses = ["192.168.1.30/24"] +addresses = [{address = "192.168.1.30/24"}] # Name of the uplink port. This should always be "qsfp0" when using softnpu. port = "qsfp0" # The speed of this port. diff --git a/nexus/db-model/src/schema.rs b/nexus/db-model/src/schema.rs index dedb0efc62..b91fa72aff 100644 --- a/nexus/db-model/src/schema.rs +++ b/nexus/db-model/src/schema.rs @@ -322,6 +322,7 @@ table! { rsvd_address_lot_block_id -> Uuid, address -> Inet, interface_name -> Text, + vlan_id -> Nullable, } } diff --git a/nexus/db-model/src/schema_versions.rs b/nexus/db-model/src/schema_versions.rs index 4e0e9cb233..b1b2697bb3 100644 --- a/nexus/db-model/src/schema_versions.rs +++ b/nexus/db-model/src/schema_versions.rs @@ -17,7 +17,7 @@ use std::collections::BTreeMap; /// /// This must be updated when you change the database schema. Refer to /// schema/crdb/README.adoc in the root of this repository for details. -pub const SCHEMA_VERSION: SemverVersion = SemverVersion::new(72, 0, 0); +pub const SCHEMA_VERSION: SemverVersion = SemverVersion::new(73, 0, 0); /// List of all past database schema versions, in *reverse* order /// @@ -29,6 +29,7 @@ static KNOWN_VERSIONS: Lazy> = Lazy::new(|| { // | leaving the first copy as an example for the next person. // v // KnownVersion::new(next_int, "unique-dirname-with-the-sql-files"), + KnownVersion::new(73, "add-vlan-to-uplink"), KnownVersion::new(72, "fix-provisioning-counters"), KnownVersion::new(71, "add-saga-unwound-vmm-state"), KnownVersion::new(70, "separate-instance-and-vmm-states"), diff --git a/nexus/db-model/src/switch_port.rs b/nexus/db-model/src/switch_port.rs index b10f6ba679..48afd7b52a 100644 --- a/nexus/db-model/src/switch_port.rs +++ b/nexus/db-model/src/switch_port.rs @@ -722,6 +722,7 @@ pub struct SwitchPortAddressConfig { pub rsvd_address_lot_block_id: Uuid, pub address: IpNetwork, pub interface_name: String, + pub vlan_id: Option, } impl SwitchPortAddressConfig { @@ -731,6 +732,7 @@ impl SwitchPortAddressConfig { rsvd_address_lot_block_id: Uuid, address: IpNetwork, interface_name: String, + vlan_id: Option, ) -> Self { Self { port_settings_id, @@ -738,6 +740,7 @@ impl SwitchPortAddressConfig { rsvd_address_lot_block_id, address, interface_name, + vlan_id: vlan_id.map(|x| x.into()), } } } @@ -749,6 +752,7 @@ impl Into for SwitchPortAddressConfig { address_lot_block_id: self.address_lot_block_id, address: self.address.into(), interface_name: self.interface_name, + vlan_id: self.vlan_id.map(|x| x.into()), } } } diff --git a/nexus/db-queries/src/db/datastore/switch_port.rs b/nexus/db-queries/src/db/datastore/switch_port.rs index edb16e95ac..d3cc6f56ab 100644 --- a/nexus/db-queries/src/db/datastore/switch_port.rs +++ b/nexus/db-queries/src/db/datastore/switch_port.rs @@ -549,6 +549,7 @@ impl DataStore { rsvd_block.id, address.address.into(), interface_name.clone(), + address.vlan_id )); } diff --git a/nexus/src/app/background/sync_switch_configuration.rs b/nexus/src/app/background/sync_switch_configuration.rs index 54fc5b8be0..8552d62988 100644 --- a/nexus/src/app/background/sync_switch_configuration.rs +++ b/nexus/src/app/background/sync_switch_configuration.rs @@ -51,8 +51,8 @@ use omicron_common::{ use serde_json::json; use sled_agent_client::types::{ BgpConfig as SledBgpConfig, BgpPeerConfig as SledBgpPeerConfig, - EarlyNetworkConfig, EarlyNetworkConfigBody, HostPortConfig, PortConfigV1, - RackNetworkConfigV1, RouteConfig as SledRouteConfig, + EarlyNetworkConfig, EarlyNetworkConfigBody, HostPortConfig, PortConfigV2, + RackNetworkConfigV2, RouteConfig as SledRouteConfig, UplinkAddressConfig, }; use std::{ collections::{hash_map::Entry, HashMap, HashSet}, @@ -901,7 +901,7 @@ impl BackgroundTask for SwitchPortSettingsManager { bgp.dedup(); - let mut ports: Vec = vec![]; + let mut ports: Vec = vec![]; for (location, port, change) in &changes { let PortSettingsChange::Apply(info) = change else { @@ -922,8 +922,12 @@ impl BackgroundTask for SwitchPortSettingsManager { }, }; - let mut port_config = PortConfigV1 { - addresses: info.addresses.iter().map(|a| a.address.into()).collect(), + let mut port_config = PortConfigV2 { + addresses: info.addresses.iter().map(|a| + UplinkAddressConfig { + address: a.address.into(), + vlan_id: a.vlan_id.map(|v| v.into()) + }).collect(), autoneg: info .links .get(0) //TODO breakout support @@ -1096,10 +1100,10 @@ impl BackgroundTask for SwitchPortSettingsManager { let mut desired_config = EarlyNetworkConfig { generation: 0, - schema_version: 1, + schema_version: 2, body: EarlyNetworkConfigBody { ntp_servers, - rack_network_config: Some(RackNetworkConfigV1 { + rack_network_config: Some(RackNetworkConfigV2 { rack_subnet: subnet, infra_ip_first, infra_ip_last, @@ -1401,7 +1405,14 @@ fn uplinks( }; let config = HostPortConfig { port: port.port_name.clone(), - addrs: config.addresses.iter().map(|a| a.address.into()).collect(), + addrs: config + .addresses + .iter() + .map(|a| UplinkAddressConfig { + address: a.address.into(), + vlan_id: a.vlan_id.map(|v| v.into()), + }) + .collect(), }; match uplinks.entry(*location) { diff --git a/nexus/src/app/rack.rs b/nexus/src/app/rack.rs index da97c77c04..780cb85f3f 100644 --- a/nexus/src/app/rack.rs +++ b/nexus/src/app/rack.rs @@ -554,7 +554,8 @@ impl super::Nexus { .iter() .map(|a| Address { address_lot: NameOrId::Name(address_lot_name.clone()), - address: (*a), + address: a.address, + vlan_id: a.vlan_id, }) .collect(); @@ -565,7 +566,11 @@ impl super::Nexus { let routes: Vec = uplink_config .routes .iter() - .map(|r| Route { dst: r.destination, gw: r.nexthop, vid: None }) + .map(|r| Route { + dst: r.destination, + gw: r.nexthop, + vid: r.vlan_id, + }) .collect(); port_settings_params diff --git a/nexus/test-utils/src/lib.rs b/nexus/test-utils/src/lib.rs index deb43c42b6..de00290616 100644 --- a/nexus/test-utils/src/lib.rs +++ b/nexus/test-utils/src/lib.rs @@ -65,7 +65,7 @@ use oximeter_producer::LogConfig; use oximeter_producer::Server as ProducerServer; use sled_agent_client::types::EarlyNetworkConfig; use sled_agent_client::types::EarlyNetworkConfigBody; -use sled_agent_client::types::RackNetworkConfigV1; +use sled_agent_client::types::RackNetworkConfigV2; use slog::{debug, error, o, Logger}; use std::collections::BTreeMap; use std::collections::HashMap; @@ -939,7 +939,7 @@ impl<'a, N: NexusServer> ControlPlaneTestContextBuilder<'a, N> { .write_network_bootstore_config(&EarlyNetworkConfig { body: EarlyNetworkConfigBody { ntp_servers: Vec::new(), - rack_network_config: Some(RackNetworkConfigV1 { + rack_network_config: Some(RackNetworkConfigV2 { bfd: Vec::new(), bgp: Vec::new(), infra_ip_first: "192.0.2.10".parse().unwrap(), @@ -951,7 +951,7 @@ impl<'a, N: NexusServer> ControlPlaneTestContextBuilder<'a, N> { }), }, generation: 1, - schema_version: 1, + schema_version: 2, }) .await .expect("Failed to write early networking config to bootstore"); diff --git a/nexus/tests/integration_tests/switch_port.rs b/nexus/tests/integration_tests/switch_port.rs index 41542d8554..0b71ddb2cf 100644 --- a/nexus/tests/integration_tests/switch_port.rs +++ b/nexus/tests/integration_tests/switch_port.rs @@ -149,6 +149,7 @@ async fn test_port_settings_basic_crud(ctx: &ControlPlaneTestContext) { AddressConfig { addresses: vec![Address { address: "203.0.113.10/24".parse().unwrap(), + vlan_id: None, address_lot: NameOrId::Name("parkinglot".parse().unwrap()), }], }, diff --git a/nexus/types/src/external_api/params.rs b/nexus/types/src/external_api/params.rs index 3f53503cc2..ac169a35ee 100644 --- a/nexus/types/src/external_api/params.rs +++ b/nexus/types/src/external_api/params.rs @@ -1753,6 +1753,9 @@ pub struct Address { /// The address and prefix length of this address. pub address: IpNet, + + /// Optional VLAN ID for this address + pub vlan_id: Option, } /// Select a port settings object by an optional name or id. diff --git a/openapi/bootstrap-agent.json b/openapi/bootstrap-agent.json index b09f34ea9e..5d175e7b09 100644 --- a/openapi/bootstrap-agent.json +++ b/openapi/bootstrap-agent.json @@ -737,14 +737,14 @@ "description": "Password hashes must be in PHC (Password Hashing Competition) string format. Passwords must be hashed with Argon2id. Password hashes may be rejected if the parameters appear not to be secure enough.", "type": "string" }, - "PortConfigV1": { + "PortConfigV2": { "type": "object", "properties": { "addresses": { - "description": "This port's addresses.", + "description": "This port's addresses and optional vlan IDs", "type": "array", "items": { - "$ref": "#/components/schemas/IpNet" + "$ref": "#/components/schemas/UplinkAddressConfig" } }, "autoneg": { @@ -901,7 +901,7 @@ "description": "Initial rack network configuration", "allOf": [ { - "$ref": "#/components/schemas/RackNetworkConfigV1" + "$ref": "#/components/schemas/RackNetworkConfigV2" } ] }, @@ -934,7 +934,7 @@ "recovery_silo" ] }, - "RackNetworkConfigV1": { + "RackNetworkConfigV2": { "description": "Initial network configuration", "type": "object", "properties": { @@ -967,7 +967,7 @@ "description": "Uplinks for connecting the rack to external networks", "type": "array", "items": { - "$ref": "#/components/schemas/PortConfigV1" + "$ref": "#/components/schemas/PortConfigV2" } }, "rack_subnet": { @@ -1225,6 +1225,24 @@ } ] }, + "UplinkAddressConfig": { + "type": "object", + "properties": { + "address": { + "$ref": "#/components/schemas/IpNet" + }, + "vlan_id": { + "nullable": true, + "description": "The VLAN id (if any) associated with this address.", + "type": "integer", + "format": "uint16", + "minimum": 0 + } + }, + "required": [ + "address" + ] + }, "UserId": { "description": "Names must begin with a lower case ASCII letter, be composed exclusively of lowercase ASCII, uppercase ASCII, numbers, and '-', and may not end with a '-'. Names cannot be a UUID though they may contain a UUID.\n\n
JSON schema\n\n```json { \"title\": \"A name unique within the parent collection\", \"description\": \"Names must begin with a lower case ASCII letter, be composed exclusively of lowercase ASCII, uppercase ASCII, numbers, and '-', and may not end with a '-'. Names cannot be a UUID though they may contain a UUID.\", \"type\": \"string\", \"maxLength\": 63, \"minLength\": 1, \"pattern\": \"^(?![0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)^[a-z]([a-zA-Z0-9-]*[a-zA-Z0-9]+)?$\" } ```
", "type": "string" diff --git a/openapi/nexus-internal.json b/openapi/nexus-internal.json index 637334483d..17330b0974 100644 --- a/openapi/nexus-internal.json +++ b/openapi/nexus-internal.json @@ -3724,14 +3724,14 @@ "vendor" ] }, - "PortConfigV1": { + "PortConfigV2": { "type": "object", "properties": { "addresses": { - "description": "This port's addresses.", + "description": "This port's addresses and optional vlan IDs", "type": "array", "items": { - "$ref": "#/components/schemas/IpNet" + "$ref": "#/components/schemas/UplinkAddressConfig" } }, "autoneg": { @@ -4048,7 +4048,7 @@ "description": "Initial rack network configuration", "allOf": [ { - "$ref": "#/components/schemas/RackNetworkConfigV1" + "$ref": "#/components/schemas/RackNetworkConfigV2" } ] }, @@ -4083,7 +4083,7 @@ "zpools" ] }, - "RackNetworkConfigV1": { + "RackNetworkConfigV2": { "description": "Initial network configuration", "type": "object", "properties": { @@ -4116,7 +4116,7 @@ "description": "Uplinks for connecting the rack to external networks", "type": "array", "items": { - "$ref": "#/components/schemas/PortConfigV1" + "$ref": "#/components/schemas/PortConfigV2" } }, "rack_subnet": { @@ -4890,6 +4890,24 @@ "items" ] }, + "UplinkAddressConfig": { + "type": "object", + "properties": { + "address": { + "$ref": "#/components/schemas/IpNet" + }, + "vlan_id": { + "nullable": true, + "description": "The VLAN id (if any) associated with this address.", + "type": "integer", + "format": "uint16", + "minimum": 0 + } + }, + "required": [ + "address" + ] + }, "UpstairsRepairType": { "type": "string", "enum": [ diff --git a/openapi/nexus.json b/openapi/nexus.json index a0789aecde..01ec9aeb56 100644 --- a/openapi/nexus.json +++ b/openapi/nexus.json @@ -8996,6 +8996,13 @@ "$ref": "#/components/schemas/NameOrId" } ] + }, + "vlan_id": { + "nullable": true, + "description": "Optional VLAN ID for this address", + "type": "integer", + "format": "uint16", + "minimum": 0 } }, "required": [ @@ -17282,6 +17289,13 @@ "description": "The port settings object this address configuration belongs to.", "type": "string", "format": "uuid" + }, + "vlan_id": { + "nullable": true, + "description": "An optional VLAN ID", + "type": "integer", + "format": "uint16", + "minimum": 0 } }, "required": [ diff --git a/openapi/sled-agent.json b/openapi/sled-agent.json index 68513345e2..79e3bac727 100644 --- a/openapi/sled-agent.json +++ b/openapi/sled-agent.json @@ -2613,7 +2613,7 @@ "nullable": true, "allOf": [ { - "$ref": "#/components/schemas/RackNetworkConfigV1" + "$ref": "#/components/schemas/RackNetworkConfigV2" } ] } @@ -2707,10 +2707,10 @@ "type": "object", "properties": { "addrs": { - "description": "IP Address and prefix (e.g., `192.168.0.1/16`) to apply to switchport (must be in infra_ip pool)", + "description": "IP Address and prefix (e.g., `192.168.0.1/16`) to apply to switchport (must be in infra_ip pool). May also include an optional VLAN ID.", "type": "array", "items": { - "$ref": "#/components/schemas/IpNet" + "$ref": "#/components/schemas/UplinkAddressConfig" } }, "port": { @@ -3974,14 +3974,14 @@ "zones" ] }, - "PortConfigV1": { + "PortConfigV2": { "type": "object", "properties": { "addresses": { - "description": "This port's addresses.", + "description": "This port's addresses and optional vlan IDs", "type": "array", "items": { - "$ref": "#/components/schemas/IpNet" + "$ref": "#/components/schemas/UplinkAddressConfig" } }, "autoneg": { @@ -4094,7 +4094,7 @@ "minItems": 2, "maxItems": 2 }, - "RackNetworkConfigV1": { + "RackNetworkConfigV2": { "description": "Initial network configuration", "type": "object", "properties": { @@ -4127,7 +4127,7 @@ "description": "Uplinks for connecting the rack to external networks", "type": "array", "items": { - "$ref": "#/components/schemas/PortConfigV1" + "$ref": "#/components/schemas/PortConfigV2" } }, "rack_subnet": { @@ -4443,6 +4443,24 @@ "version" ] }, + "UplinkAddressConfig": { + "type": "object", + "properties": { + "address": { + "$ref": "#/components/schemas/IpNet" + }, + "vlan_id": { + "nullable": true, + "description": "The VLAN id (if any) associated with this address.", + "type": "integer", + "format": "uint16", + "minimum": 0 + } + }, + "required": [ + "address" + ] + }, "VirtualNetworkInterfaceHost": { "description": "A mapping from a virtual NIC to a physical host", "type": "object", diff --git a/openapi/wicketd.json b/openapi/wicketd.json index edef5b9813..df13340334 100644 --- a/openapi/wicketd.json +++ b/openapi/wicketd.json @@ -4999,6 +4999,24 @@ } ] }, + "UplinkAddressConfig": { + "type": "object", + "properties": { + "address": { + "$ref": "#/components/schemas/IpNet" + }, + "vlan_id": { + "nullable": true, + "description": "The VLAN id (if any) associated with this address.", + "type": "integer", + "format": "uint16", + "minimum": 0 + } + }, + "required": [ + "address" + ] + }, "UserSpecifiedBgpPeerConfig": { "description": "User-specified version of [`BgpPeerConfig`].\n\nThis is similar to [`BgpPeerConfig`], except it doesn't have the sensitive `md5_auth_key` parameter, instead requiring that the user provide the key separately.\n\n[`BgpPeerConfig`]: omicron_common::api::internal::shared::BgpPeerConfig", "type": "object", @@ -5144,13 +5162,13 @@ } }, "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", + "description": "User-specified version of [`PortConfigV2`].\n\nAll of [`PortConfigV2`] is user-specified. But we expect the port name to be a key, rather than a field as in [`PortConfigV2`]. So this has all of the fields other than the port name.\n\n[`PortConfigV2`]: omicron_common::api::internal::shared::PortConfigV2", "type": "object", "properties": { "addresses": { "type": "array", "items": { - "$ref": "#/components/schemas/IpNet" + "$ref": "#/components/schemas/UplinkAddressConfig" } }, "autoneg": { diff --git a/package-manifest.toml b/package-manifest.toml index e1dfd1f4d6..8e27588be3 100644 --- a/package-manifest.toml +++ b/package-manifest.toml @@ -548,10 +548,10 @@ source.repo = "maghemite" # `tools/maghemite_openapi_version`. Failing to do so will cause a failure when # building `ddm-admin-client` (which will instruct you to update # `tools/maghemite_openapi_version`). -source.commit = "5630887d0373857f77cb264f84aa19bdec720ce3" +source.commit = "c67f6ab49e0e8a49bcf84542500fceb6b9417ca4" # The SHA256 digest is automatically posted to: # https://buildomat.eng.oxide.computer/public/file/oxidecomputer/maghemite/image//mg-ddm-gz.sha256.txt -source.sha256 = "28965f303a64f49cf5b83322babe1e0ceb4cfe33fb2df8c8d452d8c3ec02d933" +source.sha256 = "33e3b09408551be860debac08de50a840909d4e6c6bed9aecaef63fe8bef2d69" output.type = "tarball" [package.mg-ddm] @@ -564,10 +564,10 @@ source.repo = "maghemite" # `tools/maghemite_openapi_version`. Failing to do so will cause a failure when # building `ddm-admin-client` (which will instruct you to update # `tools/maghemite_openapi_version`). -source.commit = "5630887d0373857f77cb264f84aa19bdec720ce3" +source.commit = "c67f6ab49e0e8a49bcf84542500fceb6b9417ca4" # The SHA256 digest is automatically posted to: # https://buildomat.eng.oxide.computer/public/file/oxidecomputer/maghemite/image//mg-ddm.sha256.txt -source.sha256 = "6fa53be6fc5ad6273e0ca5e969c882ea40c473722415b060dfea420e962d4f8e" +source.sha256 = "81674afa17873f84bb49a800c8511938d1c2e871026cbb17e5eed2b645b1eb55" output.type = "zone" output.intermediate_only = true @@ -579,10 +579,10 @@ source.repo = "maghemite" # `tools/maghemite_openapi_version`. Failing to do so will cause a failure when # building `ddm-admin-client` (which will instruct you to update # `tools/maghemite_openapi_version`). -source.commit = "5630887d0373857f77cb264f84aa19bdec720ce3" +source.commit = "c67f6ab49e0e8a49bcf84542500fceb6b9417ca4" # The SHA256 digest is automatically posted to: -# https://buildomat.eng.oxide.computer/public/file/oxidecomputer/maghemite/image//mgd.sha256.txt -source.sha256 = "6ae4bc3b332e91706c1c6633a7fc218aac65b7feff5643ee2dbbe79b841e0df3" +# https://buildomat.eng.oxide.computer/public/file/oxidecomputer/maghemite/image//mg-ddm.sha256.txt +source.sha256 = "5e8bdd6774ef6041189621306577d0e0d174d596d216e53740ce6f035316c5af" output.type = "zone" output.intermediate_only = true @@ -628,8 +628,8 @@ only_for_targets.image = "standard" # 2. Copy dendrite.tar.gz from dendrite/out to omicron/out source.type = "prebuilt" source.repo = "dendrite" -source.commit = "6334bf74fa21790c15f1c4e494ea2ec0edd1c83c" -source.sha256 = "5929f9abf0daf4bbf17d835e5d69fc842b9617b312fb5644fa99daf785203700" +source.commit = "861c00bacbdf7a6e22471f0dabd8f926409b5292" +source.sha256 = "1db849892c60b22f600fb081d4b0145d8ecd98acce9fad3094499a5d2159d001" output.type = "zone" output.intermediate_only = true @@ -653,8 +653,8 @@ only_for_targets.image = "standard" # 2. Copy the output zone image from dendrite/out to omicron/out source.type = "prebuilt" source.repo = "dendrite" -source.commit = "6334bf74fa21790c15f1c4e494ea2ec0edd1c83c" -source.sha256 = "0294a1911212c4764d1034b5e0ca00cc9dfc51df482a9f6e5547b191b4481ad8" +source.commit = "861c00bacbdf7a6e22471f0dabd8f926409b5292" +source.sha256 = "00b2b9372145bc8974f3c75ba7a59d8f2a8178c67cc1869086d29c7f3a2deb36" output.type = "zone" output.intermediate_only = true @@ -671,8 +671,8 @@ only_for_targets.image = "standard" # 2. Copy dendrite.tar.gz from dendrite/out to omicron/out/dendrite-softnpu.tar.gz source.type = "prebuilt" source.repo = "dendrite" -source.commit = "6334bf74fa21790c15f1c4e494ea2ec0edd1c83c" -source.sha256 = "1a188da01dccf565058145b43573a549a2eb4d71fe8800170152b823af27a010" +source.commit = "861c00bacbdf7a6e22471f0dabd8f926409b5292" +source.sha256 = "b0b62b22c0e781edb0790b8730b99bb6e635c95ad3e83c2afbb2b15956153d66" output.type = "zone" output.intermediate_only = true diff --git a/schema/crdb/add-vlan-to-uplink/up.sql b/schema/crdb/add-vlan-to-uplink/up.sql new file mode 100644 index 0000000000..9ffa043d32 --- /dev/null +++ b/schema/crdb/add-vlan-to-uplink/up.sql @@ -0,0 +1 @@ + ALTER TABLE omicron.public.switch_port_settings_address_config ADD COLUMN IF NOT EXISTS vlan_id INT4 DEFAULT NULL; diff --git a/schema/crdb/dbinit.sql b/schema/crdb/dbinit.sql index b0774817f0..ddbe0dcd44 100644 --- a/schema/crdb/dbinit.sql +++ b/schema/crdb/dbinit.sql @@ -2763,6 +2763,7 @@ CREATE TABLE IF NOT EXISTS omicron.public.switch_port_settings_address_config ( rsvd_address_lot_block_id UUID NOT NULL, address INET, interface_name TEXT, + vlan_id INT4, /* TODO https://github.com/oxidecomputer/omicron/issues/3013 */ PRIMARY KEY (port_settings_id, address, interface_name) @@ -4076,7 +4077,7 @@ INSERT INTO omicron.public.db_metadata ( version, target_version ) VALUES - (TRUE, NOW(), NOW(), '72.0.0', NULL) + (TRUE, NOW(), NOW(), '73.0.0', NULL) ON CONFLICT DO NOTHING; COMMIT; diff --git a/schema/rss-sled-plan.json b/schema/rss-sled-plan.json index 5971235634..04ba5d8d31 100644 --- a/schema/rss-sled-plan.json +++ b/schema/rss-sled-plan.json @@ -609,7 +609,7 @@ "description": "Password hashes must be in PHC (Password Hashing Competition) string format. Passwords must be hashed with Argon2id. Password hashes may be rejected if the parameters appear not to be secure enough.", "type": "string" }, - "PortConfigV1": { + "PortConfigV2": { "type": "object", "required": [ "addresses", @@ -622,10 +622,10 @@ ], "properties": { "addresses": { - "description": "This port's addresses.", + "description": "This port's addresses and optional vlan IDs", "type": "array", "items": { - "$ref": "#/definitions/IpNet" + "$ref": "#/definitions/UplinkAddressConfig" } }, "autoneg": { @@ -780,7 +780,7 @@ "description": "Initial rack network configuration", "allOf": [ { - "$ref": "#/definitions/RackNetworkConfigV1" + "$ref": "#/definitions/RackNetworkConfigV2" } ] }, @@ -804,7 +804,7 @@ } } }, - "RackNetworkConfigV1": { + "RackNetworkConfigV2": { "description": "Initial network configuration", "type": "object", "required": [ @@ -844,7 +844,7 @@ "description": "Uplinks for connecting the rack to external networks", "type": "array", "items": { - "$ref": "#/definitions/PortConfigV1" + "$ref": "#/definitions/PortConfigV2" } }, "rack_subnet": { @@ -986,6 +986,26 @@ } ] }, + "UplinkAddressConfig": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "$ref": "#/definitions/IpNet" + }, + "vlan_id": { + "description": "The VLAN id (if any) associated with this address.", + "type": [ + "integer", + "null" + ], + "format": "uint16", + "minimum": 0.0 + } + } + }, "UserId": { "description": "Names must begin with a lower case ASCII letter, be composed exclusively of lowercase ASCII, uppercase ASCII, numbers, and '-', and may not end with a '-'. Names cannot be a UUID though they may contain a UUID.\n\n
JSON schema\n\n```json { \"title\": \"A name unique within the parent collection\", \"description\": \"Names must begin with a lower case ASCII letter, be composed exclusively of lowercase ASCII, uppercase ASCII, numbers, and '-', and may not end with a '-'. Names cannot be a UUID though they may contain a UUID.\", \"type\": \"string\", \"maxLength\": 63, \"minLength\": 1, \"pattern\": \"^(?![0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)^[a-z]([a-zA-Z0-9-]*[a-zA-Z0-9]+)?$\" } ```
", "type": "string" diff --git a/sled-agent/src/bootstrap/early_networking.rs b/sled-agent/src/bootstrap/early_networking.rs index bd12bb745a..61cfd8485d 100644 --- a/sled-agent/src/bootstrap/early_networking.rs +++ b/sled-agent/src/bootstrap/early_networking.rs @@ -24,8 +24,8 @@ use omicron_common::address::DENDRITE_PORT; use omicron_common::address::{MGD_PORT, MGS_PORT}; use omicron_common::api::external::{BfdMode, ImportExportPolicy}; use omicron_common::api::internal::shared::{ - BgpConfig, PortConfigV1, PortFec, PortSpeed, RackNetworkConfig, - RackNetworkConfigV1, SwitchLocation, UplinkConfig, + BgpConfig, PortConfig, PortConfigV2, PortFec, PortSpeed, RackNetworkConfig, + RackNetworkConfigV2, RouteConfig, SwitchLocation, UplinkAddressConfig, }; use omicron_common::backoff::{ retry_notify, retry_policy_local, BackoffError, ExponentialBackoff, @@ -33,7 +33,7 @@ use omicron_common::backoff::{ }; use omicron_common::OMICRON_DPD_TAG; use omicron_ddm_admin_client::DdmError; -use oxnet::{IpNet, Ipv6Net}; +use oxnet::{IpNet, Ipv4Net, Ipv6Net}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use slog::Logger; @@ -360,7 +360,7 @@ impl<'a> EarlyNetworkSetup<'a> { &mut self, rack_network_config: &RackNetworkConfig, switch_zone_underlay_ip: Ipv6Addr, - ) -> Result, EarlyNetworkSetupError> { + ) -> Result, EarlyNetworkSetupError> { // First, we have to know which switch we are: ask MGS. info!( self.log, @@ -646,7 +646,7 @@ impl<'a> EarlyNetworkSetup<'a> { fn build_port_config( &self, - port_config: &PortConfigV1, + port_config: &PortConfig, ) -> Result<(PortSettings, PortId), EarlyNetworkSetupError> { info!(self.log, "Building Port Configuration"); let mut dpd_port_settings = PortSettings { links: HashMap::new() }; @@ -726,7 +726,7 @@ fn retry_policy_switch_mapping() -> ExponentialBackoff { // The first production version of the `EarlyNetworkConfig`. // // If this version is in the bootstore than we need to convert it to -// `EarlyNetworkConfigV1`. +// `EarlyNetworkConfigV2`. // // Once we do this for all customers that have initialized racks with the // old version we can go ahead and remove this type and its conversion code @@ -748,6 +748,29 @@ struct EarlyNetworkConfigV0 { pub rack_network_config: Option, } +// The second production version of the `EarlyNetworkConfig`. +// +// If this version is in the bootstore than we need to convert it to +// `EarlyNetworkConfigV2`. +// +// Once we do this for all customers that have initialized racks with the +// old version we can go ahead and remove this type and its conversion code +// altogether. +#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] +struct EarlyNetworkConfigV1 { + // The current generation number of data as stored in CRDB. + // The initial generation is set during RSS time and then only mutated + // by Nexus. + pub generation: u64, + + // Which version of the data structure do we have. This is to help with + // deserialization and conversion in future updates. + pub schema_version: u32, + + // The actual configuration details + pub body: EarlyNetworkConfigBodyV1, +} + /// Network configuration required to bring up the control plane /// /// The fields in this structure are those from @@ -776,9 +799,9 @@ impl EarlyNetworkConfig { log: &Logger, config: &bootstore::NetworkConfig, ) -> Result { - // Try to deserialize the latest version of the data structure (v1). If + // Try to deserialize the latest version of the data structure (v2). If // that succeeds we are done. - let v1_error = + let v2_error = match serde_json::from_slice::(&config.blob) { Ok(val) => return Ok(val), Err(error) => { @@ -787,24 +810,49 @@ impl EarlyNetworkConfig { warn!( log, "Failed to deserialize EarlyNetworkConfig \ - as v1, trying next as v0: {}", + as v2, trying next as v1: {}", error, ); error } }; + match serde_json::from_slice::(&config.blob) { + Ok(val) => { + // Convert from v1 to v2 + return Ok(EarlyNetworkConfig { + generation: val.generation, + schema_version: 2, + body: EarlyNetworkConfigBody { + ntp_servers: val.body.ntp_servers, + rack_network_config: val.body.rack_network_config.map( + |v1_config| RackNetworkConfigV1::to_v2(v1_config), + ), + }, + }); + } + Err(error) => { + // Log this error. + warn!( + log, + "Failed to deserialize EarlyNetworkConfig \ + as v1, trying next as v0: {}", + error + ); + } + }; + match serde_json::from_slice::(&config.blob) { Ok(val) => { - // Convert from v0 to v1 + // Convert from v0 to v2 return Ok(EarlyNetworkConfig { generation: val.generation, - schema_version: 1, + schema_version: 2, body: EarlyNetworkConfigBody { ntp_servers: val.ntp_servers, rack_network_config: val.rack_network_config.map( |v0_config| { - RackNetworkConfigV0::to_v1( + RackNetworkConfigV0::to_v2( val.rack_subnet, v0_config, ) @@ -822,9 +870,9 @@ impl EarlyNetworkConfig { } }; - // Return the v1 error preferentially over the v0 error as it's more - // likely to be useful. - Err(v1_error) + // Return the v2 error preferentially over subsequent errors as it's + // more likely to be useful. + Err(v2_error) } } @@ -857,13 +905,22 @@ impl From for bootstore::NetworkConfig { } } +#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] +struct EarlyNetworkConfigBodyV1 { + /// The external NTP server addresses. + pub ntp_servers: Vec, + + // Rack network configuration as delivered from RSS or Nexus + pub rack_network_config: Option, +} + /// Deprecated, use `RackNetworkConfig` instead. Cannot actually deprecate due to /// /// /// Our first version of `RackNetworkConfig`. If this exists in the bootstore, we /// upgrade out of it into `RackNetworkConfigV1` or later versions if possible. #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, JsonSchema)] -pub struct RackNetworkConfigV0 { +struct RackNetworkConfigV0 { // TODO: #3591 Consider making infra-ip ranges implicit for uplinks /// First ip address to be used for configuring network infrastructure pub infra_ip_first: Ipv4Addr, @@ -876,22 +933,22 @@ pub struct RackNetworkConfigV0 { impl RackNetworkConfigV0 { /// Convert from `RackNetworkConfigV0` to `RackNetworkConfigV1` /// - /// We cannot use `From for `RackNetworkConfigV1` + /// We cannot use `From for `RackNetworkConfigV2` /// because the `rack_subnet` field does not exist in `RackNetworkConfigV0` /// and must be passed in from the `EarlyNetworkConfigV0` struct which - /// contains the `RackNetworkConfivV0` struct. - pub fn to_v1( + /// contains the `RackNetworkConfigV0` struct. + pub fn to_v2( rack_subnet: Ipv6Addr, v0: RackNetworkConfigV0, - ) -> RackNetworkConfigV1 { - RackNetworkConfigV1 { + ) -> RackNetworkConfigV2 { + RackNetworkConfigV2 { rack_subnet: Ipv6Net::new(rack_subnet, 56).unwrap(), infra_ip_first: v0.infra_ip_first, infra_ip_last: v0.infra_ip_last, ports: v0 .uplinks .into_iter() - .map(|uplink| PortConfigV1::from(uplink)) + .map(|uplink| PortConfigV2::from(uplink)) .collect(), bgp: vec![], bfd: vec![], @@ -899,6 +956,137 @@ impl RackNetworkConfigV0 { } } +/// Deprecated, use PortConfigV2 instead. Cannot actually deprecate due to +/// +#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] +struct PortConfigV1 { + /// The set of routes associated with this port. + pub routes: Vec, + /// This port's addresses and optional vlan IDs + pub addresses: Vec, + /// Switch the port belongs to. + pub switch: SwitchLocation, + /// Nmae of the port this config applies to. + pub port: String, + /// Port speed. + pub uplink_port_speed: PortSpeed, + /// Port forward error correction type. + pub uplink_port_fec: PortFec, + /// BGP peers on this port + pub bgp_peers: Vec, + /// Whether or not to set autonegotiation + #[serde(default)] + pub autoneg: bool, +} + +impl From for PortConfigV2 { + fn from(value: PortConfigV1) -> Self { + PortConfigV2 { + routes: value.routes.clone(), + addresses: value + .addresses + .iter() + .map(|a| UplinkAddressConfig { address: *a, vlan_id: None }) + .collect(), + switch: value.switch, + port: value.port, + uplink_port_speed: value.uplink_port_speed, + uplink_port_fec: value.uplink_port_fec, + bgp_peers: vec![], + autoneg: false, + } + } +} + +/// Deprecated, use PortConfigV2 instead. Cannot actually deprecate due to +/// +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, JsonSchema)] +struct UplinkConfig { + /// Gateway address + pub gateway_ip: Ipv4Addr, + /// Switch to use for uplink + pub switch: SwitchLocation, + /// Switchport to use for external connectivity + pub uplink_port: String, + /// Speed for the Switchport + pub uplink_port_speed: PortSpeed, + /// Forward Error Correction setting for the uplink port + pub uplink_port_fec: PortFec, + /// IP Address and prefix (e.g., `192.168.0.1/16`) to apply to switchport + /// (must be in infra_ip pool) + pub uplink_cidr: Ipv4Net, + /// VLAN id to use for uplink + pub uplink_vid: Option, +} + +impl From for PortConfigV2 { + fn from(value: UplinkConfig) -> Self { + PortConfigV2 { + routes: vec![RouteConfig { + destination: "0.0.0.0/0".parse().unwrap(), + nexthop: value.gateway_ip.into(), + vlan_id: value.uplink_vid, + }], + addresses: vec![UplinkAddressConfig { + address: value.uplink_cidr.into(), + vlan_id: value.uplink_vid, + }], + switch: value.switch, + port: value.uplink_port, + uplink_port_speed: value.uplink_port_speed, + uplink_port_fec: value.uplink_port_fec, + bgp_peers: vec![], + autoneg: false, + } + } +} + +/// Deprecated, use `RackNetworkConfig` instead. Cannot actually deprecate due to +/// +/// +/// Our second version of `RackNetworkConfig`. If this exists in the bootstore, +/// we upgrade out of it into `RackNetworkConfigV1` or later versions if +/// possible. +#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] +struct RackNetworkConfigV1 { + pub rack_subnet: Ipv6Net, + // TODO: #3591 Consider making infra-ip ranges implicit for uplinks + /// First ip address to be used for configuring network infrastructure + pub infra_ip_first: Ipv4Addr, + /// Last ip address to be used for configuring network infrastructure + pub infra_ip_last: Ipv4Addr, + /// Uplinks for connecting the rack to external networks + pub ports: Vec, + /// BGP configurations for connecting the rack to external networks + pub bgp: Vec, + /// BFD configuration for connecting the rack to external networks + #[serde(default)] + pub bfd: Vec, +} + +impl RackNetworkConfigV1 { + /// Convert from `RackNetworkConfigV1` to `RackNetworkConfigV2` + /// + /// We cannot use `From for `RackNetworkConfigV1` + /// because the `rack_subnet` field does not exist in `RackNetworkConfigV0` + /// and must be passed in from the `EarlyNetworkConfigV0` struct which + /// contains the `RackNetworkConfivV0` struct. + pub fn to_v2(v1: RackNetworkConfigV1) -> RackNetworkConfigV2 { + RackNetworkConfigV2 { + rack_subnet: v1.rack_subnet, + infra_ip_first: v1.infra_ip_first, + infra_ip_last: v1.infra_ip_last, + ports: v1 + .ports + .into_iter() + .map(|ports| PortConfigV2::from(ports)) + .collect(), + bgp: v1.bgp.clone(), + bfd: v1.bfd.clone(), + } + } +} + // The following two conversion functions translate the speed and fec types used // in the internal API to the types used in the dpd-client API. The conversion // is done here, rather than with "impl From" at the definition, to avoid a @@ -929,12 +1117,13 @@ fn convert_fec(fec: &PortFec) -> dpd_client::types::PortFec { mod tests { use super::*; use omicron_common::api::internal::shared::RouteConfig; + use omicron_common::api::internal::shared::UplinkAddressConfig; use omicron_test_utils::dev::test_setup_log; #[test] - fn serialized_early_network_config_v0_to_v1_conversion() { + fn serialized_early_network_config_v0_to_v2_conversion() { let logctx = test_setup_log( - "serialized_early_network_config_v0_to_v1_conversion", + "serialized_early_network_config_v0_to_v2_conversion", ); let v0 = EarlyNetworkConfigV0 { generation: 1, @@ -959,7 +1148,7 @@ mod tests { let bootstore_conf = bootstore::NetworkConfig { generation: 1, blob: v0_serialized }; - let v1 = EarlyNetworkConfig::deserialize_bootstore_config( + let v2 = EarlyNetworkConfig::deserialize_bootstore_config( &logctx.log, &bootstore_conf, ) @@ -968,20 +1157,23 @@ mod tests { let uplink = v0_rack_network_config.uplinks[0].clone(); let expected = EarlyNetworkConfig { generation: 1, - schema_version: 1, + schema_version: 2, body: EarlyNetworkConfigBody { ntp_servers: v0.ntp_servers.clone(), - rack_network_config: Some(RackNetworkConfigV1 { + rack_network_config: Some(RackNetworkConfigV2 { rack_subnet: Ipv6Net::new(v0.rack_subnet, 56).unwrap(), infra_ip_first: v0_rack_network_config.infra_ip_first, infra_ip_last: v0_rack_network_config.infra_ip_last, - ports: vec![PortConfigV1 { + ports: vec![PortConfigV2 { routes: vec![RouteConfig { destination: "0.0.0.0/0".parse().unwrap(), nexthop: uplink.gateway_ip.into(), vlan_id: None, }], - addresses: vec![uplink.uplink_cidr.into()], + addresses: vec![UplinkAddressConfig { + address: uplink.uplink_cidr.into(), + vlan_id: None, + }], switch: uplink.switch, port: uplink.uplink_port, uplink_port_speed: uplink.uplink_port_speed, @@ -995,7 +1187,87 @@ mod tests { }, }; - assert_eq!(expected, v1); + assert_eq!(expected, v2); + + logctx.cleanup_successful(); + } + + #[test] + fn serialized_early_network_config_v1_to_v2_conversion() { + let logctx = test_setup_log( + "serialized_early_network_config_v1_to_v2_conversion", + ); + + let v1 = EarlyNetworkConfigV1 { + generation: 1, + schema_version: 1, + body: EarlyNetworkConfigBodyV1 { + ntp_servers: Vec::new(), + rack_network_config: Some(RackNetworkConfigV1 { + rack_subnet: Ipv6Net::new(Ipv6Addr::UNSPECIFIED, 56) + .unwrap(), + infra_ip_first: Ipv4Addr::UNSPECIFIED, + infra_ip_last: Ipv4Addr::UNSPECIFIED, + ports: vec![PortConfigV1 { + routes: vec![RouteConfig { + destination: "0.0.0.0/0".parse().unwrap(), + nexthop: "192.168.0.2".parse().unwrap(), + vlan_id: None, + }], + addresses: vec!["192.168.0.1/16".parse().unwrap()], + switch: SwitchLocation::Switch0, + port: "Port0".to_string(), + uplink_port_speed: PortSpeed::Speed100G, + uplink_port_fec: PortFec::None, + bgp_peers: Vec::new(), + autoneg: false, + }], + bgp: Vec::new(), + bfd: Vec::new(), + }), + }, + }; + + let v1_serialized = serde_json::to_vec(&v1).unwrap(); + let bootstore_conf = + bootstore::NetworkConfig { generation: 1, blob: v1_serialized }; + + let v2 = EarlyNetworkConfig::deserialize_bootstore_config( + &logctx.log, + &bootstore_conf, + ) + .unwrap(); + let v1_rack_network_config = v1.body.rack_network_config.unwrap(); + let port = v1_rack_network_config.ports[0].clone(); + let expected = EarlyNetworkConfig { + generation: 1, + schema_version: 2, + body: EarlyNetworkConfigBody { + ntp_servers: v1.body.ntp_servers.clone(), + rack_network_config: Some(RackNetworkConfigV2 { + rack_subnet: v1_rack_network_config.rack_subnet, + infra_ip_first: v1_rack_network_config.infra_ip_first, + infra_ip_last: v1_rack_network_config.infra_ip_last, + ports: vec![PortConfigV2 { + routes: port.routes.clone(), + addresses: vec![UplinkAddressConfig { + address: port.addresses[0], + vlan_id: None, + }], + switch: port.switch, + port: port.port, + uplink_port_speed: port.uplink_port_speed, + uplink_port_fec: port.uplink_port_fec, + autoneg: false, + bgp_peers: vec![], + }], + bgp: vec![], + bfd: vec![], + }), + }, + }; + + assert_eq!(expected, v2); logctx.cleanup_successful(); } diff --git a/sled-agent/src/rack_setup/service.rs b/sled-agent/src/rack_setup/service.rs index 1d8b3e7ad3..2d7a355440 100644 --- a/sled-agent/src/rack_setup/service.rs +++ b/sled-agent/src/rack_setup/service.rs @@ -734,16 +734,16 @@ impl ServiceInner { let rack_network_config = { let config = &config.rack_network_config; - NexusTypes::RackNetworkConfigV1 { + NexusTypes::RackNetworkConfigV2 { rack_subnet: config.rack_subnet, infra_ip_first: config.infra_ip_first, infra_ip_last: config.infra_ip_last, ports: config .ports .iter() - .map(|config| NexusTypes::PortConfigV1 { + .map(|config| NexusTypes::PortConfigV2 { port: config.port.clone(), - routes: config + routes: config .routes .iter() .map(|r| NexusTypes::RouteConfig { @@ -752,7 +752,14 @@ impl ServiceInner { vlan_id: r.vlan_id, }) .collect(), - addresses: config.addresses.iter().cloned().map(Into::into).collect(), + addresses: config + .addresses + .iter() + .map(|a| NexusTypes::UplinkAddressConfig { + address: a.address, + vlan_id: a.vlan_id + }) + .collect(), switch: config.switch.into(), uplink_port_speed: config.uplink_port_speed.into(), uplink_port_fec: config.uplink_port_fec.into(), @@ -1129,7 +1136,7 @@ impl ServiceInner { // from the bootstore". let early_network_config = EarlyNetworkConfig { generation: 1, - schema_version: 1, + schema_version: 2, body: EarlyNetworkConfigBody { ntp_servers: config.ntp_servers.clone(), rack_network_config: Some(config.rack_network_config.clone()), diff --git a/sled-agent/src/sim/server.rs b/sled-agent/src/sim/server.rs index ae7f40f5f3..7ce34473e7 100644 --- a/sled-agent/src/sim/server.rs +++ b/sled-agent/src/sim/server.rs @@ -527,7 +527,7 @@ pub async fn run_standalone_server( external_port_count: NexusTypes::ExternalPortDiscovery::Static( HashMap::new(), ), - rack_network_config: NexusTypes::RackNetworkConfigV1 { + rack_network_config: NexusTypes::RackNetworkConfigV2 { rack_subnet: Ipv6Net::host_net(Ipv6Addr::LOCALHOST), infra_ip_first: Ipv4Addr::LOCALHOST, infra_ip_last: Ipv4Addr::LOCALHOST, diff --git a/sled-agent/tests/data/early_network_blobs.txt b/sled-agent/tests/data/early_network_blobs.txt index c968d4010b..e9b9927e86 100644 --- a/sled-agent/tests/data/early_network_blobs.txt +++ b/sled-agent/tests/data/early_network_blobs.txt @@ -1,2 +1,2 @@ -2023-11-30 mupdate failing blob,{"generation":15,"schema_version":1,"body":{"ntp_servers":[],"rack_network_config":{"rack_subnet":"fd00:1122:3344:100::/56","infra_ip_first":"0.0.0.0","infra_ip_last":"0.0.0.0","ports":[{"routes":[],"addresses":[],"switch":"switch1","port":"qsfp0","uplink_port_speed":"speed100_g","uplink_port_fec":"none","bgp_peers":[]},{"routes":[],"addresses":["172.20.15.53/29"],"switch":"switch1","port":"qsfp18","uplink_port_speed":"speed100_g","uplink_port_fec":"rs","bgp_peers":[{"asn":65002,"port":"qsfp18","addr":"172.20.15.51","hold_time":6,"idle_hold_time":6,"delay_open":0,"connect_retry":3,"keepalive":2}]},{"routes":[],"addresses":["172.20.15.45/29"],"switch":"switch0","port":"qsfp18","uplink_port_speed":"speed100_g","uplink_port_fec":"rs","bgp_peers":[{"asn":65002,"port":"qsfp18","addr":"172.20.15.43","hold_time":6,"idle_hold_time":6,"delay_open":0,"connect_retry":3,"keepalive":2}]},{"routes":[],"addresses":[],"switch":"switch0","port":"qsfp0","uplink_port_speed":"speed100_g","uplink_port_fec":"none","bgp_peers":[]}],"bgp":[{"asn":65002,"originate":["172.20.26.0/24"]},{"asn":65002,"originate":["172.20.26.0/24"]}]}}} -2023-12-06 config,{"generation":20,"schema_version":1,"body":{"ntp_servers":["ntp.example.com"],"rack_network_config":{"rack_subnet":"ff01::/32","infra_ip_first":"127.0.0.1","infra_ip_last":"127.1.0.1","ports":[{"routes":[{"destination":"10.1.9.32/16","nexthop":"10.1.9.32"}],"addresses":["2001:db8::/96"],"switch":"switch0","port":"foo","uplink_port_speed":"speed200_g","uplink_port_fec":"firecode","bgp_peers":[{"asn":65000,"port":"bar","addr":"1.2.3.4","hold_time":20,"idle_hold_time":50,"delay_open":null,"connect_retry":30,"keepalive":10}],"autoneg":true}],"bgp":[{"asn":20000,"originate":["192.168.0.0/24"]}]}}} +2023-11-30 mupdate failing blob,{"generation":15,"schema_version":1,"body":{"ntp_servers":[],"rack_network_config":{"rack_subnet":"fd00:1122:3344:100::/56","infra_ip_first":"0.0.0.0","infra_ip_last":"0.0.0.0","ports":[{"routes":[],"addresses":[],"switch":"switch1","port":"qsfp0","uplink_port_speed":"speed100_g","uplink_port_fec":"none","bgp_peers":[]},{"routes":[],"addresses":[{"address":"172.20.15.53/29"}],"switch":"switch1","port":"qsfp18","uplink_port_speed":"speed100_g","uplink_port_fec":"rs","bgp_peers":[{"asn":65002,"port":"qsfp18","addr":"172.20.15.51","hold_time":6,"idle_hold_time":6,"delay_open":0,"connect_retry":3,"keepalive":2}]},{"routes":[],"addresses":[{"address":"172.20.15.45/29"}],"switch":"switch0","port":"qsfp18","uplink_port_speed":"speed100_g","uplink_port_fec":"rs","bgp_peers":[{"asn":65002,"port":"qsfp18","addr":"172.20.15.43","hold_time":6,"idle_hold_time":6,"delay_open":0,"connect_retry":3,"keepalive":2}]},{"routes":[],"addresses":[],"switch":"switch0","port":"qsfp0","uplink_port_speed":"speed100_g","uplink_port_fec":"none","bgp_peers":[]}],"bgp":[{"asn":65002,"originate":["172.20.26.0/24"]},{"asn":65002,"originate":["172.20.26.0/24"]}]}}} +2023-12-06 config,{"generation":20,"schema_version":1,"body":{"ntp_servers":["ntp.example.com"],"rack_network_config":{"rack_subnet":"ff01::/32","infra_ip_first":"127.0.0.1","infra_ip_last":"127.1.0.1","ports":[{"routes":[{"destination":"10.1.9.32/16","nexthop":"10.1.9.32"}],"addresses":[{"address":"2001:db8::/96"}],"switch":"switch0","port":"foo","uplink_port_speed":"speed200_g","uplink_port_fec":"firecode","bgp_peers":[{"asn":65000,"port":"bar","addr":"1.2.3.4","hold_time":20,"idle_hold_time":50,"delay_open":null,"connect_retry":30,"keepalive":10}],"autoneg":true}],"bgp":[{"asn":20000,"originate":["192.168.0.0/24"]}]}}} diff --git a/sled-agent/tests/integration_tests/early_network.rs b/sled-agent/tests/integration_tests/early_network.rs index 4421e76e8b..b7cab53a51 100644 --- a/sled-agent/tests/integration_tests/early_network.rs +++ b/sled-agent/tests/integration_tests/early_network.rs @@ -10,7 +10,7 @@ use bootstore::schemes::v0 as bootstore; use omicron_common::api::{ external::{ImportExportPolicy, SwitchLocation}, internal::shared::{ - BgpConfig, BgpPeerConfig, PortConfigV1, PortFec, PortSpeed, + BgpConfig, BgpPeerConfig, PortConfigV2, PortFec, PortSpeed, RackNetworkConfig, RouteConfig, }, }; @@ -120,7 +120,7 @@ fn current_config_example() -> (&'static str, EarlyNetworkConfig) { rack_subnet: "ff01::0/32".parse().unwrap(), infra_ip_first: Ipv4Addr::new(127, 0, 0, 1), infra_ip_last: Ipv4Addr::new(127, 1, 0, 1), - ports: vec![PortConfigV1 { + ports: vec![PortConfigV2 { routes: vec![RouteConfig { destination: "10.1.9.32/16".parse().unwrap(), nexthop: "10.1.9.32".parse().unwrap(), diff --git a/sled-agent/tests/old-rss-sled-plans/madrid-rss-sled-plan.json b/sled-agent/tests/old-rss-sled-plans/madrid-rss-sled-plan.json index 5512247ee8..683e8fb833 100644 --- a/sled-agent/tests/old-rss-sled-plans/madrid-rss-sled-plan.json +++ b/sled-agent/tests/old-rss-sled-plans/madrid-rss-sled-plan.json @@ -1 +1 @@ -{"rack_id":"ed6bcf59-9620-491d-8ebd-4a4eebf2e136","sleds":{"[fdb0:a840:2504:396::1]:12346":{"generation":0,"schema_version":1,"body":{"id":"b3e78a88-0f2e-476e-a8a9-2d8c90a169d6","rack_id":"ed6bcf59-9620-491d-8ebd-4a4eebf2e136","use_trust_quorum":true,"is_lrtq_learner":false,"subnet":{"net":"fd00:1122:3344:103::/64"}}},"[fdb0:a840:2504:157::1]:12346":{"generation":0,"schema_version":1,"body":{"id":"168e1ad6-1e4b-4f7a-b894-157974bd8bb8","rack_id":"ed6bcf59-9620-491d-8ebd-4a4eebf2e136","use_trust_quorum":true,"is_lrtq_learner":false,"subnet":{"net":"fd00:1122:3344:104::/64"}}},"[fdb0:a840:2504:355::1]:12346":{"generation":0,"schema_version":1,"body":{"id":"b9877212-212b-4588-b818-9c7b53c5b143","rack_id":"ed6bcf59-9620-491d-8ebd-4a4eebf2e136","use_trust_quorum":true,"is_lrtq_learner":false,"subnet":{"net":"fd00:1122:3344:102::/64"}}},"[fdb0:a840:2504:3d2::1]:12346":{"generation":0,"schema_version":1,"body":{"id":"c3a0f8be-5b05-4ee8-8c4e-2514de6501b6","rack_id":"ed6bcf59-9620-491d-8ebd-4a4eebf2e136","use_trust_quorum":true,"is_lrtq_learner":false,"subnet":{"net":"fd00:1122:3344:101::/64"}}}},"config":{"rack_subnet":"fd00:1122:3344:100::","trust_quorum_peers":[{"type":"gimlet","identifier":"BRM42220081","model":"913-0000019","revision":6},{"type":"gimlet","identifier":"BRM42220046","model":"913-0000019","revision":6},{"type":"gimlet","identifier":"BRM44220001","model":"913-0000019","revision":6},{"type":"gimlet","identifier":"BRM42220004","model":"913-0000019","revision":6}],"bootstrap_discovery":{"type":"only_these","addrs":["fdb0:a840:2504:3d2::1","fdb0:a840:2504:355::1","fdb0:a840:2504:396::1","fdb0:a840:2504:157::1"]},"ntp_servers":["ntp.eng.oxide.computer"],"dns_servers":["1.1.1.1","9.9.9.9"],"internal_services_ip_pool_ranges":[{"first":"172.20.28.1","last":"172.20.28.10"}],"external_dns_ips":["172.20.28.1"],"external_dns_zone_name":"madrid.eng.oxide.computer","external_certificates":[{"cert":"","key":""}],"recovery_silo":{"silo_name":"recovery","user_name":"recovery","user_password_hash":"$argon2id$v=19$m=98304,t=13,p=1$RUlWc0ZxaHo0WFdrN0N6ZQ$S8p52j85GPvMhR/ek3GL0el/oProgTwWpHJZ8lsQQoY"},"rack_network_config":{"rack_subnet":"fd00:1122:3344:1::/56","infra_ip_first":"172.20.15.37","infra_ip_last":"172.20.15.38","ports":[{"routes":[{"destination":"0.0.0.0/0","nexthop":"172.20.15.33"}],"addresses":["172.20.15.38/29"],"switch":"switch0","port":"qsfp0","uplink_port_speed":"speed40_g","uplink_port_fec":"none","bgp_peers":[],"autoneg":false},{"routes":[{"destination":"0.0.0.0/0","nexthop":"172.20.15.33"}],"addresses":["172.20.15.37/29"],"switch":"switch1","port":"qsfp0","uplink_port_speed":"speed40_g","uplink_port_fec":"none","bgp_peers":[],"autoneg":false}],"bgp":[]}}} +{"rack_id":"ed6bcf59-9620-491d-8ebd-4a4eebf2e136","sleds":{"[fdb0:a840:2504:396::1]:12346":{"generation":0,"schema_version":1,"body":{"id":"b3e78a88-0f2e-476e-a8a9-2d8c90a169d6","rack_id":"ed6bcf59-9620-491d-8ebd-4a4eebf2e136","use_trust_quorum":true,"is_lrtq_learner":false,"subnet":{"net":"fd00:1122:3344:103::/64"}}},"[fdb0:a840:2504:157::1]:12346":{"generation":0,"schema_version":1,"body":{"id":"168e1ad6-1e4b-4f7a-b894-157974bd8bb8","rack_id":"ed6bcf59-9620-491d-8ebd-4a4eebf2e136","use_trust_quorum":true,"is_lrtq_learner":false,"subnet":{"net":"fd00:1122:3344:104::/64"}}},"[fdb0:a840:2504:355::1]:12346":{"generation":0,"schema_version":1,"body":{"id":"b9877212-212b-4588-b818-9c7b53c5b143","rack_id":"ed6bcf59-9620-491d-8ebd-4a4eebf2e136","use_trust_quorum":true,"is_lrtq_learner":false,"subnet":{"net":"fd00:1122:3344:102::/64"}}},"[fdb0:a840:2504:3d2::1]:12346":{"generation":0,"schema_version":1,"body":{"id":"c3a0f8be-5b05-4ee8-8c4e-2514de6501b6","rack_id":"ed6bcf59-9620-491d-8ebd-4a4eebf2e136","use_trust_quorum":true,"is_lrtq_learner":false,"subnet":{"net":"fd00:1122:3344:101::/64"}}}},"config":{"rack_subnet":"fd00:1122:3344:100::","trust_quorum_peers":[{"type":"gimlet","identifier":"BRM42220081","model":"913-0000019","revision":6},{"type":"gimlet","identifier":"BRM42220046","model":"913-0000019","revision":6},{"type":"gimlet","identifier":"BRM44220001","model":"913-0000019","revision":6},{"type":"gimlet","identifier":"BRM42220004","model":"913-0000019","revision":6}],"bootstrap_discovery":{"type":"only_these","addrs":["fdb0:a840:2504:3d2::1","fdb0:a840:2504:355::1","fdb0:a840:2504:396::1","fdb0:a840:2504:157::1"]},"ntp_servers":["ntp.eng.oxide.computer"],"dns_servers":["1.1.1.1","9.9.9.9"],"internal_services_ip_pool_ranges":[{"first":"172.20.28.1","last":"172.20.28.10"}],"external_dns_ips":["172.20.28.1"],"external_dns_zone_name":"madrid.eng.oxide.computer","external_certificates":[{"cert":"","key":""}],"recovery_silo":{"silo_name":"recovery","user_name":"recovery","user_password_hash":"$argon2id$v=19$m=98304,t=13,p=1$RUlWc0ZxaHo0WFdrN0N6ZQ$S8p52j85GPvMhR/ek3GL0el/oProgTwWpHJZ8lsQQoY"},"rack_network_config":{"rack_subnet":"fd00:1122:3344:1::/56","infra_ip_first":"172.20.15.37","infra_ip_last":"172.20.15.38","ports":[{"routes":[{"destination":"0.0.0.0/0","nexthop":"172.20.15.33"}],"addresses":[{"address":"172.20.15.38/29"}],"switch":"switch0","port":"qsfp0","uplink_port_speed":"speed40_g","uplink_port_fec":"none","bgp_peers":[],"autoneg":false},{"routes":[{"destination":"0.0.0.0/0","nexthop":"172.20.15.33"}],"addresses":[{"address":"172.20.15.37/29"}],"switch":"switch1","port":"qsfp0","uplink_port_speed":"speed40_g","uplink_port_fec":"none","bgp_peers":[],"autoneg":false}],"bgp":[]}}} diff --git a/sled-agent/tests/output/new-rss-sled-plans/madrid-rss-sled-plan.json b/sled-agent/tests/output/new-rss-sled-plans/madrid-rss-sled-plan.json index 108914a26f..efd1a3c167 100644 --- a/sled-agent/tests/output/new-rss-sled-plans/madrid-rss-sled-plan.json +++ b/sled-agent/tests/output/new-rss-sled-plans/madrid-rss-sled-plan.json @@ -132,7 +132,10 @@ } ], "addresses": [ - "172.20.15.38/29" + { + "address": "172.20.15.38/29", + "vlan_id": null + } ], "switch": "switch0", "port": "qsfp0", @@ -150,7 +153,10 @@ } ], "addresses": [ - "172.20.15.37/29" + { + "address": "172.20.15.37/29", + "vlan_id": null + } ], "switch": "switch1", "port": "qsfp0", diff --git a/smf/sled-agent/gimlet-standalone/config-rss.toml b/smf/sled-agent/gimlet-standalone/config-rss.toml index 616d8d496b..f1b5da6f24 100644 --- a/smf/sled-agent/gimlet-standalone/config-rss.toml +++ b/smf/sled-agent/gimlet-standalone/config-rss.toml @@ -102,7 +102,7 @@ bgp = [] # Routes associated with this port. routes = [{nexthop = "192.168.1.199", destination = "0.0.0.0/0"}] # Addresses associated with this port. -addresses = ["192.168.1.30/32"] +addresses = [{address = "192.168.1.30/32"}] # Name of the uplink port. This should always be "qsfp0" when using softnpu. port = "qsfp0" # The speed of this port. diff --git a/smf/sled-agent/non-gimlet/config-rss.toml b/smf/sled-agent/non-gimlet/config-rss.toml index d897f7ba4b..90f5339e84 100644 --- a/smf/sled-agent/non-gimlet/config-rss.toml +++ b/smf/sled-agent/non-gimlet/config-rss.toml @@ -102,7 +102,7 @@ bgp = [] # Routes associated with this port. routes = [{nexthop = "192.168.1.199", destination = "0.0.0.0/0"}] # Addresses associated with this port. -addresses = ["192.168.1.30/24"] +addresses = [{address = "192.168.1.30/24"}] # Name of the uplink port. This should always be "qsfp0" when using softnpu. port = "qsfp0" # The speed of this port. diff --git a/tools/dendrite_openapi_version b/tools/dendrite_openapi_version index 6d71042250..0d6d6f810e 100755 --- a/tools/dendrite_openapi_version +++ b/tools/dendrite_openapi_version @@ -1,2 +1,2 @@ -COMMIT="6334bf74fa21790c15f1c4e494ea2ec0edd1c83c" -SHA2="213031aa058f0aa355964e4a5ca350db30110454bad5c77cbc94ab77fdcbe013" +COMMIT="861c00bacbdf7a6e22471f0dabd8f926409b5292" +SHA2="12dc61e7c62b2e1ee1cf3c2bf7cdda6bee6ec96925d2fc1c021c6c1a8fdd56cd" diff --git a/tools/dendrite_stub_checksums b/tools/dendrite_stub_checksums index 3f4d9854c4..75c76f3585 100644 --- a/tools/dendrite_stub_checksums +++ b/tools/dendrite_stub_checksums @@ -1,3 +1,3 @@ -CIDL_SHA256_ILLUMOS="5929f9abf0daf4bbf17d835e5d69fc842b9617b312fb5644fa99daf785203700" -CIDL_SHA256_LINUX_DPD="fa38138db9ce1c2cababd11dd9ef1289295e4a8185c78372f6ff1a090c75a05b" -CIDL_SHA256_LINUX_SWADM="ebda6c0a8e29f40c389337fe2e37c1191eeeb34d729de7724b6d707bb6c9a882" +CIDL_SHA256_ILLUMOS="1db849892c60b22f600fb081d4b0145d8ecd98acce9fad3094499a5d2159d001" +CIDL_SHA256_LINUX_DPD="4022e8c0de268c4bc38046b29a48d021b3204e6c2dc8371f2de67f42019720c0" +CIDL_SHA256_LINUX_SWADM="a1308303fd0d8f8ac272288e801beb913f695dcf820dd53f5c03871e6b8674f7" diff --git a/wicket-common/src/rack_setup.rs b/wicket-common/src/rack_setup.rs index ba88c258a5..33fbcb65b3 100644 --- a/wicket-common/src/rack_setup.rs +++ b/wicket-common/src/rack_setup.rs @@ -16,6 +16,7 @@ use omicron_common::api::internal::shared::BgpPeerConfig; use omicron_common::api::internal::shared::PortFec; use omicron_common::api::internal::shared::PortSpeed; use omicron_common::api::internal::shared::RouteConfig; +use omicron_common::api::internal::shared::UplinkAddressConfig; use omicron_common::update::ArtifactHash; use owo_colors::OwoColorize; use owo_colors::Style; @@ -170,18 +171,18 @@ impl UserSpecifiedRackNetworkConfig { } } -/// User-specified version of [`PortConfigV1`]. +/// User-specified version of [`PortConfigV2`]. /// -/// All 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 +/// All of [`PortConfigV2`] is user-specified. But we expect the port name to +/// be a key, rather than a field as in [`PortConfigV2`]. So this has all of /// the fields other than the port name. /// -/// [`PortConfigV1`]: omicron_common::api::internal::shared::PortConfigV1 +/// [`PortConfigV2`]: omicron_common::api::internal::shared::PortConfigV2 #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, JsonSchema)] #[serde(deny_unknown_fields)] pub struct UserSpecifiedPortConfig { pub routes: Vec, - pub addresses: Vec, + pub addresses: Vec, pub uplink_port_speed: PortSpeed, pub uplink_port_fec: PortFec, pub autoneg: bool, diff --git a/wicket/src/cli/rack_setup/config_toml.rs b/wicket/src/cli/rack_setup/config_toml.rs index 164ecf16b7..cef3746ff9 100644 --- a/wicket/src/cli/rack_setup/config_toml.rs +++ b/wicket/src/cli/rack_setup/config_toml.rs @@ -9,6 +9,7 @@ use omicron_common::address::IpRange; use omicron_common::api::external::AllowedSourceIps; use omicron_common::api::internal::shared::BgpConfig; use omicron_common::api::internal::shared::RouteConfig; +use omicron_common::api::internal::shared::UplinkAddressConfig; use serde::Serialize; use sled_hardware_types::Baseboard; use std::borrow::Cow; @@ -340,7 +341,13 @@ fn populate_uplink_table(cfg: &UserSpecifiedPortConfig) -> Table { // addresses = [] let mut addresses_out = Array::new(); for a in addresses { - addresses_out.push(string_value(a)); + let UplinkAddressConfig { address, vlan_id } = a; + let mut x = InlineTable::new(); + x.insert("address", string_value(address)); + if let Some(vlan_id) = vlan_id { + x.insert("vlan_id", i64_value(i64::from(*vlan_id))); + } + addresses_out.push(Value::InlineTable(x)); } uplink.insert("addresses", Item::Value(Value::Array(addresses_out))); diff --git a/wicket/src/ui/panes/rack_setup.rs b/wicket/src/ui/panes/rack_setup.rs index 941f5f7dc1..b4fa9de6f0 100644 --- a/wicket/src/ui/panes/rack_setup.rs +++ b/wicket/src/ui/panes/rack_setup.rs @@ -792,10 +792,19 @@ fn rss_config_text<'a>( }); let addresses = addresses.iter().map(|a| { - vec![ + let mut items = vec![ Span::styled(" • Address : ", label_style), - Span::styled(a.to_string(), ok_style), - ] + Span::styled(a.address.to_string(), ok_style), + ]; + if let Some(vlan_id) = a.vlan_id { + items.extend([ + Span::styled(" (vlan_id=", label_style), + Span::styled(vlan_id.to_string(), ok_style), + Span::styled(")", label_style), + ]); + } + + items }); let peers = bgp_peers.iter().flat_map(|p| { diff --git a/wicket/tests/output/example_non_empty.toml b/wicket/tests/output/example_non_empty.toml index 8785d158de..717e940ca5 100644 --- a/wicket/tests/output/example_non_empty.toml +++ b/wicket/tests/output/example_non_empty.toml @@ -74,7 +74,7 @@ infra_ip_last = "172.30.0.10" [rack_network_config.switch0.port0] routes = [{ nexthop = "172.30.0.10", destination = "0.0.0.0/0", vlan_id = 1 }] -addresses = ["172.30.0.1/24"] +addresses = [{ address = "172.30.0.1/24" }] uplink_port_speed = "speed400_g" uplink_port_fec = "firecode" autoneg = true @@ -113,7 +113,7 @@ enforce_first_as = true [rack_network_config.switch1.port0] routes = [{ nexthop = "172.33.0.10", destination = "0.0.0.0/0", vlan_id = 1 }] -addresses = ["172.32.0.1/24"] +addresses = [{ address = "172.32.0.1/24" }] uplink_port_speed = "speed400_g" uplink_port_fec = "firecode" autoneg = true diff --git a/wicketd/src/preflight_check/uplink.rs b/wicketd/src/preflight_check/uplink.rs index 3a70823b5b..395fb8c795 100644 --- a/wicketd/src/preflight_check/uplink.rs +++ b/wicketd/src/preflight_check/uplink.rs @@ -301,7 +301,10 @@ fn add_steps_for_single_local_uplink_preflight_check<'a>( UplinkProperty(format!("uplinks/{}_0", port)); for addr in &uplink.addresses { - let uplink_cidr = addr.to_string(); + // This includes the CIDR only + let uplink_cidr = addr.address.to_string(); + // This includes the VLAN ID, if any + let uplink_cfg = addr.to_string(); if let Err(err) = execute_command(&[ SVCCFG, "-s", @@ -309,7 +312,7 @@ fn add_steps_for_single_local_uplink_preflight_check<'a>( "addpropvalue", &uplink_property.0, "astring:", - &uplink_cidr, + &uplink_cfg, ]) .await { diff --git a/wicketd/src/rss_config.rs b/wicketd/src/rss_config.rs index 77e107a129..dde6d35da5 100644 --- a/wicketd/src/rss_config.rs +++ b/wicketd/src/rss_config.rs @@ -17,7 +17,7 @@ use anyhow::Result; use bootstrap_agent_client::types::BootstrapAddressDiscovery; use bootstrap_agent_client::types::Certificate; use bootstrap_agent_client::types::Name; -use bootstrap_agent_client::types::PortConfigV1 as BaPortConfigV1; +use bootstrap_agent_client::types::PortConfigV2 as BaPortConfigV2; use bootstrap_agent_client::types::RackInitializeRequest; use bootstrap_agent_client::types::RecoverySiloConfig; use bootstrap_agent_client::types::UserId; @@ -609,7 +609,7 @@ pub(crate) enum BgpAuthKeyError { fn validate_rack_network_config( config: &UserSpecifiedRackNetworkConfig, bgp_auth_keys: &BTreeMap>, -) -> Result { +) -> Result { use bootstrap_agent_client::types::BgpConfig as BaBgpConfig; // Ensure that there is at least one uplink @@ -651,7 +651,7 @@ fn validate_rack_network_config( // TODO Add more client side checks on `rack_network_config` contents? - Ok(bootstrap_agent_client::types::RackNetworkConfigV1 { + Ok(bootstrap_agent_client::types::RackNetworkConfigV2 { rack_subnet: RACK_SUBNET.net(), infra_ip_first: config.infra_ip_first, infra_ip_last: config.infra_ip_last, @@ -676,7 +676,7 @@ fn validate_rack_network_config( }) } -/// Builds a `BaPortConfigV1` from a `UserSpecifiedPortConfig`. +/// Builds a `BaPortConfigV2` from a `UserSpecifiedPortConfig`. /// /// Assumes that all auth keys are present in `bgp_auth_keys`. fn build_port_config( @@ -684,16 +684,17 @@ fn build_port_config( port: &str, config: &UserSpecifiedPortConfig, bgp_auth_keys: &BTreeMap>, -) -> BaPortConfigV1 { +) -> BaPortConfigV2 { use bootstrap_agent_client::types::BgpPeerConfig as BaBgpPeerConfig; use bootstrap_agent_client::types::PortFec as BaPortFec; use bootstrap_agent_client::types::PortSpeed as BaPortSpeed; use bootstrap_agent_client::types::RouteConfig as BaRouteConfig; use bootstrap_agent_client::types::SwitchLocation as BaSwitchLocation; + use bootstrap_agent_client::types::UplinkAddressConfig as BaUplinkAddressConfig; use omicron_common::api::internal::shared::PortFec; use omicron_common::api::internal::shared::PortSpeed; - BaPortConfigV1 { + BaPortConfigV2 { port: port.to_owned(), routes: config .routes @@ -704,7 +705,14 @@ fn build_port_config( vlan_id: r.vlan_id, }) .collect(), - addresses: config.addresses.iter().cloned().map(Into::into).collect(), + addresses: config + .addresses + .iter() + .map(|a| BaUplinkAddressConfig { + address: a.address, + vlan_id: a.vlan_id, + }) + .collect(), bgp_peers: config .bgp_peers .iter()