Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions openapi/wicketd.json
Original file line number Diff line number Diff line change
Expand Up @@ -7784,6 +7784,11 @@
"type": "string",
"format": "ipv4"
},
"rack_subnet_address": {
"nullable": true,
"type": "string",
"format": "ipv6"
},
"switch0": {
"type": "object",
"additionalProperties": {
Expand Down
20 changes: 12 additions & 8 deletions wicket-common/src/example.rs
Original file line number Diff line number Diff line change
Expand Up @@ -196,13 +196,17 @@ impl ExampleRackSetupData {
management_addrs: Some(vec!["172.32.0.4".parse().unwrap()]),
});

let rack_subnet_address =
Some(Ipv6Addr::new(0xfd00, 0x1122, 0x3344, 0x0100, 0, 0, 0, 0));

let rack_network_config = UserSpecifiedRackNetworkConfig {
rack_subnet_address,
infra_ip_first: "172.30.0.1".parse().unwrap(),
infra_ip_last: "172.30.0.10".parse().unwrap(),
#[rustfmt::skip]
switch0: btreemap! {
"port0".to_owned() => UserSpecifiedPortConfig {
addresses: vec!["172.30.0.1/24".parse().unwrap()],
"port0".to_owned() => UserSpecifiedPortConfig {
addresses: vec!["172.30.0.1/24".parse().unwrap()],
routes: vec![RouteConfig {
destination: "0.0.0.0/0".parse().unwrap(),
nexthop: "172.30.0.10".parse().unwrap(),
Expand All @@ -212,11 +216,11 @@ impl ExampleRackSetupData {
bgp_peers: switch0_port0_bgp_peers,
uplink_port_speed: PortSpeed::Speed400G,
uplink_port_fec: Some(PortFec::Firecode),
lldp: switch0_port0_lldp,
tx_eq,
autoneg: true,
},
},
lldp: switch0_port0_lldp,
tx_eq,
autoneg: true,
},
},
#[rustfmt::skip]
switch1: btreemap! {
// Use the same port name as in switch0 to test that it doesn't
Expand All @@ -233,7 +237,7 @@ impl ExampleRackSetupData {
uplink_port_speed: PortSpeed::Speed400G,
uplink_port_fec: None,
lldp: switch1_port0_lldp,
tx_eq,
tx_eq,
autoneg: true,
},
},
Expand Down
1 change: 1 addition & 0 deletions wicket-common/src/rack_setup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ pub struct BootstrapSledDescription {
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct UserSpecifiedRackNetworkConfig {
pub rack_subnet_address: Option<Ipv6Addr>,
pub infra_ip_first: Ipv4Addr,
pub infra_ip_last: Ipv4Addr,
// Map of switch -> port -> configuration, under the assumption that
Expand Down
13 changes: 13 additions & 0 deletions wicket/src/cli/rack_setup/config_toml.rs
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,19 @@ fn populate_network_table(
return;
};

if let Some(rack_subnet_address) = config.rack_subnet_address {
let value =
Value::String(Formatted::new(rack_subnet_address.to_string()));
match table.entry("rack_subnet_address") {
toml_edit::Entry::Occupied(mut entry) => {
entry.insert(Item::Value(value));
}
toml_edit::Entry::Vacant(entry) => {
entry.insert(Item::Value(value));
}
}
}

for (property, value) in [
("infra_ip_first", config.infra_ip_first.to_string()),
("infra_ip_last", config.infra_ip_last.to_string()),
Expand Down
17 changes: 16 additions & 1 deletion wicket/src/ui/panes/rack_setup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -710,6 +710,19 @@ fn rss_config_text<'a>(
"External DNS zone name: ",
Cow::from(external_dns_zone_name.as_str()),
),
(
"Rack subnet address (IPv6 /56): ",
rack_network_config.as_ref().map_or(
"(will be chosen randomly)".into(),
|c| {
match c.rack_subnet_address {
Some(v) => v.to_string(),
None => "(chosen randomly)".to_string(),
Copy link
Contributor

Choose a reason for hiding this comment

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

Tiny nit - why is this string different from the one a few lines up?

Copy link
Contributor Author

@internet-diglett internet-diglett Jan 20, 2026

Choose a reason for hiding this comment

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

I figured we wanted it to say "will be chosen randomly" if the user hasn't provided a ipv6 subnet address and we are still uninitialzed, but it should say "chosen randomly` once we are initialized.

This is how it looks when None maps to "will be chosen randomly"
image

It seems like it could be a little confusing to users if we left it that way.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ahh, got it. Yeah that works for me 👍

Copy link
Contributor Author

@internet-diglett internet-diglett Jan 20, 2026

Choose a reason for hiding this comment

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

Here's how it looks with the two different strings:

Before Rack Init

Screenshot 2026-01-20 at 1 29 20 PM

After Rack Init

Screenshot 2026-01-20 at 1 20 06 PM

With User Specified Address

Screenshot 2026-01-19 at 6 53 18 PM

}
.into()
},
),
),
(
"Infrastructure first IP: ",
rack_network_config
Expand Down Expand Up @@ -755,7 +768,9 @@ fn rss_config_text<'a>(
// 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.
// rack_subnet_address, infra_ip_first, and infra_ip_last
// have already been handled above.
rack_subnet_address: _,
infra_ip_first: _,
infra_ip_last: _,
// switch0 and switch1 re handled via the iter_uplinks iterator.
Expand Down
1 change: 1 addition & 0 deletions wicket/tests/output/example_non_empty.toml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ allow = "any"
[rack_network_config]
infra_ip_first = "172.30.0.1"
infra_ip_last = "172.30.0.10"
rack_subnet_address = "fd00:1122:3344:100::"

[rack_network_config.switch0.port0]
routes = [{ nexthop = "172.30.0.10", destination = "0.0.0.0/0", vlan_id = 1 }]
Expand Down
2 changes: 1 addition & 1 deletion wicketd/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ serde_json.workspace = true
sha2.workspace = true
slog-dtrace.workspace = true
slog.workspace = true
rand.workspace = true
thiserror.workspace = true
tufaceous-artifact.workspace = true
tufaceous-lib.workspace = true
Expand Down Expand Up @@ -88,7 +89,6 @@ maplit.workspace = true
omicron-test-utils.workspace = true
openapi-lint.workspace = true
openapiv3.workspace = true
rand.workspace = true
serde_json.workspace = true
sled-agent-config-reconciler = { workspace = true, features = ["testing"] }
sled-agent-types.workspace = true
Expand Down
62 changes: 50 additions & 12 deletions wicketd/src/rss_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,9 @@ use display_error_chain::DisplayErrorChain;
use omicron_certificates::CertificateError;
use omicron_common::address;
use omicron_common::address::Ipv4Range;
use omicron_common::address::Ipv6Subnet;
use omicron_common::address::RACK_PREFIX;
use omicron_common::api::external::AllowedSourceIps;
use omicron_common::api::external::SwitchLocation;
use oxnet::Ipv6Net;
use sled_hardware_types::Baseboard;
use slog::debug;
use slog::warn;
Expand All @@ -33,7 +32,6 @@ use std::collections::btree_map;
use std::mem;
use std::net::IpAddr;
use std::net::Ipv6Addr;
use std::sync::LazyLock;
use thiserror::Error;
use wicket_common::inventory::MgsV1Inventory;
use wicket_common::inventory::SpType;
Expand All @@ -52,14 +50,6 @@ use wicketd_api::CurrentRssUserConfig;
use wicketd_api::CurrentRssUserConfigSensitive;
use wicketd_api::SetBgpAuthKeyStatus;

// TODO-correctness For now, we always use the same rack subnet when running
// RSS. When we get to multirack, this will be wrong, but there are many other
// RSS-related things that need to change then too.
static RACK_SUBNET: LazyLock<Ipv6Subnet<RACK_PREFIX>> = LazyLock::new(|| {
let ip = Ipv6Addr::new(0xfd00, 0x1122, 0x3344, 0x0100, 0, 0, 0, 0);
Ipv6Subnet::new(ip)
});

const RECOVERY_SILO_NAME: &str = "recovery";
const RECOVERY_SILO_USERNAME: &str = "recovery";

Expand Down Expand Up @@ -659,10 +649,15 @@ fn validate_rack_network_config(
}
}

let rack_subnet = match validate_rack_subnet(config.rack_subnet_address) {
Ok(v) => v,
Err(e) => bail!(e),
};

// TODO Add more client side checks on `rack_network_config` contents?

Ok(bootstrap_agent_client::types::RackNetworkConfigV2 {
rack_subnet: RACK_SUBNET.net(),
rack_subnet,
infra_ip_first: config.infra_ip_first,
infra_ip_last: config.infra_ip_last,
ports: config
Expand All @@ -686,6 +681,49 @@ fn validate_rack_network_config(
})
}

pub fn validate_rack_subnet(
subnet_address: Option<Ipv6Addr>,
) -> Result<Ipv6Net, String> {
use rand::prelude::*;

let rack_subnet_address = match subnet_address {
Some(addr) => addr,
None => {
let mut rng = rand::rng();
let a: u16 = 0xfd00 + Into::<u16>::into(rng.random::<u8>());
Ipv6Addr::new(
a,
rng.random::<u16>(),
rng.random::<u16>(),
0x0100,
0,
0,
0,
0,
)
}
};

// first octet must be fd
if rack_subnet_address.octets()[0] != 0xfd {
return Err("rack subnet address must begin with 0xfd".into());
};

// Do not allow rack0
if rack_subnet_address.octets()[6] == 0x00 {
return Err("rack number (seventh octet) cannot be 0".into());
};

Copy link
Contributor

Choose a reason for hiding this comment

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

Do we want to check that the low 72 bits are all 0? That's guaranteed for our random ones, and converting the subnet address into an Ipv6Net<56> will do that, but it may indicate some confusion if an operator asks for it?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think that's a reasonable check to add, as it does make things a bit clearer in terms of what we expect (and what the system will do if you set the lower bits of the address)

// Do not allow addresses more specific than /56
if rack_subnet_address.octets()[7..].iter().any(|x| *x != 0x00) {
return Err("rack subnet address is /56, \
but a more specific prefix was provided"
.into());
};

Ipv6Net::new(rack_subnet_address, 56).map_err(|e| e.to_string())
}

/// Builds a `BaPortConfigV2` from a `UserSpecifiedPortConfig`.
///
/// Assumes that all auth keys are present in `bgp_auth_keys`.
Expand Down
Loading