Skip to content

Commit 86a989f

Browse files
Enable wicket to configure the rack subnet (#9653)
To pave the way for a multi-rack world, we are replacing the static `fd00:1122:3344::01` prefix with a randomly generated one or a user specified one, that way racks can have unique addresses once they start talking to each other. - [x] verify the user provided address starts with `fd` - [x] verify that the user provided address is not longer that `/56` - [x] verify that the rack number (seventh octet) is not 0. - [x] generate an address that conforms to the above rules if user does not provide an address Related --- #9501 --------- Co-authored-by: John Gallagher <john@oxidecomputer.com>
1 parent 26b33a1 commit 86a989f

File tree

8 files changed

+99
-22
lines changed

8 files changed

+99
-22
lines changed

openapi/wicketd.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7784,6 +7784,11 @@
77847784
"type": "string",
77857785
"format": "ipv4"
77867786
},
7787+
"rack_subnet_address": {
7788+
"nullable": true,
7789+
"type": "string",
7790+
"format": "ipv6"
7791+
},
77877792
"switch0": {
77887793
"type": "object",
77897794
"additionalProperties": {

wicket-common/src/example.rs

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -196,13 +196,17 @@ impl ExampleRackSetupData {
196196
management_addrs: Some(vec!["172.32.0.4".parse().unwrap()]),
197197
});
198198

199+
let rack_subnet_address =
200+
Some(Ipv6Addr::new(0xfd00, 0x1122, 0x3344, 0x0100, 0, 0, 0, 0));
201+
199202
let rack_network_config = UserSpecifiedRackNetworkConfig {
203+
rack_subnet_address,
200204
infra_ip_first: "172.30.0.1".parse().unwrap(),
201205
infra_ip_last: "172.30.0.10".parse().unwrap(),
202206
#[rustfmt::skip]
203207
switch0: btreemap! {
204-
"port0".to_owned() => UserSpecifiedPortConfig {
205-
addresses: vec!["172.30.0.1/24".parse().unwrap()],
208+
"port0".to_owned() => UserSpecifiedPortConfig {
209+
addresses: vec!["172.30.0.1/24".parse().unwrap()],
206210
routes: vec![RouteConfig {
207211
destination: "0.0.0.0/0".parse().unwrap(),
208212
nexthop: "172.30.0.10".parse().unwrap(),
@@ -212,11 +216,11 @@ impl ExampleRackSetupData {
212216
bgp_peers: switch0_port0_bgp_peers,
213217
uplink_port_speed: PortSpeed::Speed400G,
214218
uplink_port_fec: Some(PortFec::Firecode),
215-
lldp: switch0_port0_lldp,
216-
tx_eq,
217-
autoneg: true,
218-
},
219-
},
219+
lldp: switch0_port0_lldp,
220+
tx_eq,
221+
autoneg: true,
222+
},
223+
},
220224
#[rustfmt::skip]
221225
switch1: btreemap! {
222226
// Use the same port name as in switch0 to test that it doesn't
@@ -233,7 +237,7 @@ impl ExampleRackSetupData {
233237
uplink_port_speed: PortSpeed::Speed400G,
234238
uplink_port_fec: None,
235239
lldp: switch1_port0_lldp,
236-
tx_eq,
240+
tx_eq,
237241
autoneg: true,
238242
},
239243
},

wicket-common/src/rack_setup.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ pub struct BootstrapSledDescription {
9999
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, JsonSchema)]
100100
#[serde(deny_unknown_fields)]
101101
pub struct UserSpecifiedRackNetworkConfig {
102+
pub rack_subnet_address: Option<Ipv6Addr>,
102103
pub infra_ip_first: Ipv4Addr,
103104
pub infra_ip_last: Ipv4Addr,
104105
// Map of switch -> port -> configuration, under the assumption that

wicket/src/cli/rack_setup/config_toml.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,19 @@ fn populate_network_table(
244244
return;
245245
};
246246

247+
if let Some(rack_subnet_address) = config.rack_subnet_address {
248+
let value =
249+
Value::String(Formatted::new(rack_subnet_address.to_string()));
250+
match table.entry("rack_subnet_address") {
251+
toml_edit::Entry::Occupied(mut entry) => {
252+
entry.insert(Item::Value(value));
253+
}
254+
toml_edit::Entry::Vacant(entry) => {
255+
entry.insert(Item::Value(value));
256+
}
257+
}
258+
}
259+
247260
for (property, value) in [
248261
("infra_ip_first", config.infra_ip_first.to_string()),
249262
("infra_ip_last", config.infra_ip_last.to_string()),

wicket/src/ui/panes/rack_setup.rs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -710,6 +710,19 @@ fn rss_config_text<'a>(
710710
"External DNS zone name: ",
711711
Cow::from(external_dns_zone_name.as_str()),
712712
),
713+
(
714+
"Rack subnet address (IPv6 /56): ",
715+
rack_network_config.as_ref().map_or(
716+
"(will be chosen randomly)".into(),
717+
|c| {
718+
match c.rack_subnet_address {
719+
Some(v) => v.to_string(),
720+
None => "(chosen randomly)".to_string(),
721+
}
722+
.into()
723+
},
724+
),
725+
),
713726
(
714727
"Infrastructure first IP: ",
715728
rack_network_config
@@ -755,7 +768,9 @@ fn rss_config_text<'a>(
755768
// This style ensures that if a new field is added to the struct, it
756769
// fails to compile.
757770
let UserSpecifiedRackNetworkConfig {
758-
// infra_ip_first and infra_ip_last have already been handled above.
771+
// rack_subnet_address, infra_ip_first, and infra_ip_last
772+
// have already been handled above.
773+
rack_subnet_address: _,
759774
infra_ip_first: _,
760775
infra_ip_last: _,
761776
// switch0 and switch1 re handled via the iter_uplinks iterator.

wicket/tests/output/example_non_empty.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ allow = "any"
7171
[rack_network_config]
7272
infra_ip_first = "172.30.0.1"
7373
infra_ip_last = "172.30.0.10"
74+
rack_subnet_address = "fd00:1122:3344:100::"
7475

7576
[rack_network_config.switch0.port0]
7677
routes = [{ nexthop = "172.30.0.10", destination = "0.0.0.0/0", vlan_id = 1 }]

wicketd/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ serde_json.workspace = true
4343
sha2.workspace = true
4444
slog-dtrace.workspace = true
4545
slog.workspace = true
46+
rand.workspace = true
4647
thiserror.workspace = true
4748
tufaceous-artifact.workspace = true
4849
tufaceous-lib.workspace = true
@@ -88,7 +89,6 @@ maplit.workspace = true
8889
omicron-test-utils.workspace = true
8990
openapi-lint.workspace = true
9091
openapiv3.workspace = true
91-
rand.workspace = true
9292
serde_json.workspace = true
9393
sled-agent-config-reconciler = { workspace = true, features = ["testing"] }
9494
sled-agent-types.workspace = true

wicketd/src/rss_config.rs

Lines changed: 50 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,9 @@ use display_error_chain::DisplayErrorChain;
2020
use omicron_certificates::CertificateError;
2121
use omicron_common::address;
2222
use omicron_common::address::Ipv4Range;
23-
use omicron_common::address::Ipv6Subnet;
24-
use omicron_common::address::RACK_PREFIX;
2523
use omicron_common::api::external::AllowedSourceIps;
2624
use omicron_common::api::external::SwitchLocation;
25+
use oxnet::Ipv6Net;
2726
use sled_hardware_types::Baseboard;
2827
use slog::debug;
2928
use slog::warn;
@@ -33,7 +32,6 @@ use std::collections::btree_map;
3332
use std::mem;
3433
use std::net::IpAddr;
3534
use std::net::Ipv6Addr;
36-
use std::sync::LazyLock;
3735
use thiserror::Error;
3836
use wicket_common::inventory::MgsV1Inventory;
3937
use wicket_common::inventory::SpType;
@@ -52,14 +50,6 @@ use wicketd_api::CurrentRssUserConfig;
5250
use wicketd_api::CurrentRssUserConfigSensitive;
5351
use wicketd_api::SetBgpAuthKeyStatus;
5452

55-
// TODO-correctness For now, we always use the same rack subnet when running
56-
// RSS. When we get to multirack, this will be wrong, but there are many other
57-
// RSS-related things that need to change then too.
58-
static RACK_SUBNET: LazyLock<Ipv6Subnet<RACK_PREFIX>> = LazyLock::new(|| {
59-
let ip = Ipv6Addr::new(0xfd00, 0x1122, 0x3344, 0x0100, 0, 0, 0, 0);
60-
Ipv6Subnet::new(ip)
61-
});
62-
6353
const RECOVERY_SILO_NAME: &str = "recovery";
6454
const RECOVERY_SILO_USERNAME: &str = "recovery";
6555

@@ -659,10 +649,15 @@ fn validate_rack_network_config(
659649
}
660650
}
661651

652+
let rack_subnet = match validate_rack_subnet(config.rack_subnet_address) {
653+
Ok(v) => v,
654+
Err(e) => bail!(e),
655+
};
656+
662657
// TODO Add more client side checks on `rack_network_config` contents?
663658

664659
Ok(bootstrap_agent_client::types::RackNetworkConfigV2 {
665-
rack_subnet: RACK_SUBNET.net(),
660+
rack_subnet,
666661
infra_ip_first: config.infra_ip_first,
667662
infra_ip_last: config.infra_ip_last,
668663
ports: config
@@ -686,6 +681,49 @@ fn validate_rack_network_config(
686681
})
687682
}
688683

684+
pub fn validate_rack_subnet(
685+
subnet_address: Option<Ipv6Addr>,
686+
) -> Result<Ipv6Net, String> {
687+
use rand::prelude::*;
688+
689+
let rack_subnet_address = match subnet_address {
690+
Some(addr) => addr,
691+
None => {
692+
let mut rng = rand::rng();
693+
let a: u16 = 0xfd00 + Into::<u16>::into(rng.random::<u8>());
694+
Ipv6Addr::new(
695+
a,
696+
rng.random::<u16>(),
697+
rng.random::<u16>(),
698+
0x0100,
699+
0,
700+
0,
701+
0,
702+
0,
703+
)
704+
}
705+
};
706+
707+
// first octet must be fd
708+
if rack_subnet_address.octets()[0] != 0xfd {
709+
return Err("rack subnet address must begin with 0xfd".into());
710+
};
711+
712+
// Do not allow rack0
713+
if rack_subnet_address.octets()[6] == 0x00 {
714+
return Err("rack number (seventh octet) cannot be 0".into());
715+
};
716+
717+
// Do not allow addresses more specific than /56
718+
if rack_subnet_address.octets()[7..].iter().any(|x| *x != 0x00) {
719+
return Err("rack subnet address is /56, \
720+
but a more specific prefix was provided"
721+
.into());
722+
};
723+
724+
Ipv6Net::new(rack_subnet_address, 56).map_err(|e| e.to_string())
725+
}
726+
689727
/// Builds a `BaPortConfigV2` from a `UserSpecifiedPortConfig`.
690728
///
691729
/// Assumes that all auth keys are present in `bgp_auth_keys`.

0 commit comments

Comments
 (0)