Skip to content

Commit 36eb2df

Browse files
committed
Support multiple external IPs in blueprint zone type
- Add support for multiple EIPs to the blueprint zone types for Nexus, External DNS, and Boundary NTP zones. - Add some newtypes and wrappers to support lists of these or up to 2 of them for the SNAT case of Boundary NTP. - Add a test that the full blueprint with multiple addresses round-trips through the database. - Planner still emits exactly one address in all these cases, this is only the structural change to support multiple addresses. - Update lockstep OpenAPI docs - Closes #9288
1 parent d0d875b commit 36eb2df

29 files changed

Lines changed: 1681 additions & 591 deletions

File tree

‎clients/nexus-lockstep-client/src/lib.rs‎

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,10 @@ progenitor::generate_api!(
6666
NetworkInterface = sled_agent_types::inventory::NetworkInterface,
6767
NetworkInterfaceKind = sled_agent_types::inventory::NetworkInterfaceKind,
6868
NewPasswordHash = omicron_passwords::NewPasswordHash,
69+
OmicronZoneExternalFloatingAddr =
70+
nexus_types::deployment::OmicronZoneExternalFloatingAddr,
71+
OmicronZoneExternalFloatingIp =
72+
nexus_types::deployment::OmicronZoneExternalFloatingIp,
6973
OximeterReadMode = nexus_types::deployment::OximeterReadMode,
7074
OximeterReadPolicy = nexus_types::deployment::OximeterReadPolicy,
7175
PendingMgsUpdate = nexus_types::deployment::PendingMgsUpdate,
@@ -151,15 +155,6 @@ impl From<omicron_common::address::Ipv6Range> for types::Ipv6Range {
151155
}
152156
}
153157

154-
impl From<&sled_agent_types::inventory::SourceNatConfigGeneric>
155-
for types::SourceNatConfigGeneric
156-
{
157-
fn from(r: &sled_agent_types::inventory::SourceNatConfigGeneric) -> Self {
158-
let (first_port, last_port) = r.port_range_raw();
159-
Self { ip: r.ip, first_port, last_port }
160-
}
161-
}
162-
163158
impl From<&omicron_common::api::external::AllowedSourceIps>
164159
for types::AllowedSourceIps
165160
{

‎dev-tools/reconfigurator-cli/src/lib.rs‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -157,12 +157,14 @@ impl ReconfiguratorSim {
157157

158158
// Handle zone networking setup first
159159
for (_, zone) in parent_blueprint.in_service_zones() {
160-
if let Some((external_ip, nic)) =
160+
if let Some((external_ips, nic)) =
161161
zone.zone_type.external_networking()
162162
{
163-
builder
164-
.add_omicron_zone_external_ip(zone.id, external_ip)
165-
.context("adding omicron zone external IP")?;
163+
for external_ip in external_ips {
164+
builder
165+
.add_omicron_zone_external_ip(zone.id, external_ip)
166+
.context("adding omicron zone external IP")?;
167+
}
166168
let nic = OmicronZoneNic {
167169
// TODO-cleanup use `TypedUuid` everywhere
168170
id: VnicUuid::from_untyped_uuid(nic.id),

‎live-tests/tests/test_nexus_handoff.rs‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -565,9 +565,11 @@ async fn check_external_dns(
565565
// what's in-service in the blueprint.
566566
let expected_nexus_addrs = blueprint
567567
.in_service_nexus_zones()
568-
.filter_map(|(_sled_id, _zone_cfg, nexus_config)| {
569-
(nexus_config.nexus_generation == active_generation)
570-
.then_some(nexus_config.external_ip.ip)
568+
.filter(|(_sled_id, _zone_cfg, nexus_config)| {
569+
nexus_config.nexus_generation == active_generation
570+
})
571+
.flat_map(|(_sled_id, _zone_cfg, nexus_config)| {
572+
nexus_config.external_ips.iter().map(|e| e.ip)
571573
})
572574
.collect::<BTreeSet<_>>();
573575

‎nexus/db-model/src/deployment.rs‎

Lines changed: 120 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,9 @@ use nexus_types::deployment::{
6060
use nexus_types::deployment::{BlueprintPhysicalDiskConfig, BlueprintSource};
6161
use nexus_types::deployment::{BlueprintZoneImageSource, blueprint_zone_type};
6262
use nexus_types::deployment::{
63-
OmicronZoneExternalFloatingAddr, OmicronZoneExternalFloatingIp,
64-
OmicronZoneExternalSnatIp,
63+
OmicronZoneExternalFloatingAddr, OmicronZoneExternalFloatingAddrs,
64+
OmicronZoneExternalFloatingIp, OmicronZoneExternalFloatingIps,
65+
OmicronZoneExternalSnat, OmicronZoneExternalSnatIp,
6566
};
6667
use omicron_common::address::Ipv6Subnet;
6768
use omicron_common::address::SLED_PREFIX_LENGTH;
@@ -855,7 +856,7 @@ impl BpOmicronZone {
855856
http_address,
856857
// The external DNS address is stored in the
857858
// `bp_omicron_zone_external_ip` table, not here.
858-
dns_address: _,
859+
dns_addresses: _,
859860
nic,
860861
},
861862
) => {
@@ -901,7 +902,7 @@ impl BpOmicronZone {
901902
lockstep_port,
902903
// The external IP is stored in the
903904
// `bp_omicron_zone_external_ip` table, not here.
904-
external_ip: _,
905+
external_ips: _,
905906
nic,
906907
external_tls,
907908
external_dns_servers,
@@ -978,18 +979,7 @@ impl BpOmicronZone {
978979
nic_row.map(Into::into),
979980
)?;
980981

981-
// The external IP(s) for this zone live in the
982-
// `bp_omicron_zone_external_ip` table. Until `BlueprintZoneType` can
983-
// handle multiple IPs (#9288), we need zero or exactly one row here,
984-
// for the zone types that have external networking.
985-
//
986-
// NOTE: This returns an error if `external_ip_rows` is empty. That's
987-
// fine if the zone doesn't need external networking, so we only
988-
// ?-propagate this inside the zone-specific code below.
989-
let external_ip = BpOmicronZoneExternalIp::into_single(
990-
external_ip_rows,
991-
self.id.into(),
992-
);
982+
let zone_id = self.id.into();
993983

994984
// NOTE: this is the *internal* DNS underlay address, held in
995985
// `second_service_ip` / `second_service_port`. External DNS's external
@@ -1011,19 +1001,19 @@ impl BpOmicronZone {
10111001

10121002
let zone_type = match self.zone_type {
10131003
ZoneType::BoundaryNtp => {
1014-
let external_ip = external_ip?;
1015-
let snat_cfg = external_ip.to_snat_config()?;
1004+
let external_ip =
1005+
BpOmicronZoneExternalIp::into_boundary_ntp_snat(
1006+
external_ip_rows,
1007+
zone_id,
1008+
)?;
10161009
BlueprintZoneType::BoundaryNtp(
10171010
blueprint_zone_type::BoundaryNtp {
10181011
address: primary_address,
10191012
ntp_servers: ntp_servers?,
10201013
dns_servers: ntp_dns_servers?,
10211014
domain: self.ntp_domain,
10221015
nic: nic?,
1023-
external_ip: OmicronZoneExternalSnatIp {
1024-
id: external_ip.external_ip_id.into(),
1025-
snat_cfg,
1026-
},
1016+
external_ip,
10271017
},
10281018
)
10291019
}
@@ -1064,15 +1054,16 @@ impl BpOmicronZone {
10641054
},
10651055
),
10661056
ZoneType::ExternalDns => {
1067-
let external_ip = external_ip?;
1057+
let dns_addresses =
1058+
BpOmicronZoneExternalIp::into_external_dns_addrs(
1059+
external_ip_rows,
1060+
zone_id,
1061+
)?;
10681062
BlueprintZoneType::ExternalDns(
10691063
blueprint_zone_type::ExternalDns {
10701064
dataset: dataset?,
10711065
http_address: primary_address,
1072-
dns_address: OmicronZoneExternalFloatingAddr {
1073-
id: external_ip.external_ip_id.into(),
1074-
addr: external_ip.to_floating_addr()?,
1075-
},
1066+
dns_addresses,
10761067
nic: nic?,
10771068
},
10781069
)
@@ -1099,16 +1090,17 @@ impl BpOmicronZone {
10991090
blueprint_zone_type::InternalNtp { address: primary_address },
11001091
),
11011092
ZoneType::Nexus => {
1102-
let external_ip = external_ip?;
1093+
let external_ips =
1094+
BpOmicronZoneExternalIp::into_nexus_external_ips(
1095+
external_ip_rows,
1096+
zone_id,
1097+
)?;
11031098
BlueprintZoneType::Nexus(blueprint_zone_type::Nexus {
11041099
internal_address: primary_address,
11051100
lockstep_port: *self.nexus_lockstep_port.ok_or_else(
11061101
|| anyhow!("expected 'nexus_lockstep_port'"),
11071102
)?,
1108-
external_ip: OmicronZoneExternalFloatingIp {
1109-
id: external_ip.external_ip_id.into(),
1110-
ip: external_ip.ip.ip(),
1111-
},
1103+
external_ips,
11121104
nic: nic?,
11131105
external_tls: self
11141106
.nexus_external_tls
@@ -1186,10 +1178,9 @@ pub struct BpOmicronZoneExternalIp {
11861178
impl BpOmicronZoneExternalIp {
11871179
/// Build the external IP child rows for a blueprint zone.
11881180
///
1189-
/// Returns one row per external IP. Today the in-memory `BlueprintZoneType`
1190-
/// only ever has at most one external IP per zone, so this returns at most
1191-
/// one row. In general though, the `bp_omicron_zone_external_ip` table can
1192-
/// store any number of rows per zone, so we're returning an array.
1181+
/// Returns one row per external IP: Nexus and external DNS may each have
1182+
/// several, and boundary NTP may have a source-NAT address per IP family
1183+
/// (with at least one address).
11931184
pub fn for_zone(
11941185
blueprint_id: BlueprintUuid,
11951186
blueprint_zone: &BlueprintZoneConfig,
@@ -1198,43 +1189,52 @@ impl BpOmicronZoneExternalIp {
11981189
let zone_id = blueprint_zone.id.into();
11991190
match &blueprint_zone.zone_type {
12001191
BlueprintZoneType::Nexus(blueprint_zone_type::Nexus {
1201-
external_ip,
1192+
external_ips,
12021193
..
1203-
}) => vec![Self {
1204-
blueprint_id,
1205-
zone_id,
1206-
external_ip_id: external_ip.id.into(),
1207-
ip: IpNetwork::from(external_ip.ip),
1208-
port: None,
1209-
snat_first_port: None,
1210-
snat_last_port: None,
1211-
}],
1212-
BlueprintZoneType::ExternalDns(
1213-
blueprint_zone_type::ExternalDns { dns_address, .. },
1214-
) => vec![Self {
1215-
blueprint_id,
1216-
zone_id,
1217-
external_ip_id: dns_address.id.into(),
1218-
ip: IpNetwork::from(dns_address.addr.ip()),
1219-
port: Some(SqlU16::from(dns_address.addr.port())),
1220-
snat_first_port: None,
1221-
snat_last_port: None,
1222-
}],
1223-
BlueprintZoneType::BoundaryNtp(
1224-
blueprint_zone_type::BoundaryNtp { external_ip, .. },
1225-
) => {
1226-
let (first_port, last_port) =
1227-
external_ip.snat_cfg.port_range_raw();
1228-
vec![Self {
1194+
}) => external_ips
1195+
.iter()
1196+
.map(|external_ip| Self {
12291197
blueprint_id,
12301198
zone_id,
12311199
external_ip_id: external_ip.id.into(),
1232-
ip: IpNetwork::from(external_ip.snat_cfg.ip),
1200+
ip: IpNetwork::from(external_ip.ip),
12331201
port: None,
1234-
snat_first_port: Some(SqlU16::from(first_port)),
1235-
snat_last_port: Some(SqlU16::from(last_port)),
1236-
}]
1237-
}
1202+
snat_first_port: None,
1203+
snat_last_port: None,
1204+
})
1205+
.collect(),
1206+
BlueprintZoneType::ExternalDns(
1207+
blueprint_zone_type::ExternalDns { dns_addresses, .. },
1208+
) => dns_addresses
1209+
.iter()
1210+
.map(|dns_address| Self {
1211+
blueprint_id,
1212+
zone_id,
1213+
external_ip_id: dns_address.id.into(),
1214+
ip: IpNetwork::from(dns_address.addr.ip()),
1215+
port: Some(SqlU16::from(dns_address.addr.port())),
1216+
snat_first_port: None,
1217+
snat_last_port: None,
1218+
})
1219+
.collect(),
1220+
BlueprintZoneType::BoundaryNtp(
1221+
blueprint_zone_type::BoundaryNtp { external_ip, .. },
1222+
) => external_ip
1223+
.iter()
1224+
.map(|snat| {
1225+
let (first_port, last_port) =
1226+
snat.snat_cfg.port_range_raw();
1227+
Self {
1228+
blueprint_id,
1229+
zone_id,
1230+
external_ip_id: snat.id.into(),
1231+
ip: IpNetwork::from(snat.snat_cfg.ip),
1232+
port: None,
1233+
snat_first_port: Some(SqlU16::from(first_port)),
1234+
snat_last_port: Some(SqlU16::from(last_port)),
1235+
}
1236+
})
1237+
.collect(),
12381238
BlueprintZoneType::Clickhouse(_)
12391239
| BlueprintZoneType::ClickhouseKeeper(_)
12401240
| BlueprintZoneType::ClickhouseServer(_)
@@ -1247,28 +1247,59 @@ impl BpOmicronZoneExternalIp {
12471247
}
12481248
}
12491249

1250-
/// Collapse the external IP rows for a zone into exactly one.
1251-
///
1252-
/// NOTE: This is a temporary method until the `BlueprintZoneType` variants
1253-
/// with external addresses can handle more than one EIP. Until then, these
1254-
/// zones must have exactly one external IP. This returns that single
1255-
/// address, or an error if there is any other number of rows.
1256-
fn into_single(
1257-
mut rows: Vec<Self>,
1250+
/// Reconstruct a Nexus zone's external IPs from its child rows.
1251+
fn into_nexus_external_ips(
1252+
rows: Vec<Self>,
12581253
zone_id: OmicronZoneUuid,
1259-
) -> anyhow::Result<Self> {
1260-
match rows.len() {
1261-
1 => Ok(rows.pop().expect("length checked to be 1")),
1262-
0 => bail!(
1263-
"zone {zone_id} has no external IP, \
1264-
but its type requires one"
1265-
),
1266-
n => bail!(
1267-
"zone {zone_id} has {n} external IPs, but only one is \
1268-
supported until the in-memory blueprint type is widened \
1269-
(#9288)"
1270-
),
1271-
}
1254+
) -> anyhow::Result<OmicronZoneExternalFloatingIps> {
1255+
let ips = rows
1256+
.into_iter()
1257+
.map(|row| OmicronZoneExternalFloatingIp {
1258+
id: row.external_ip_id.into(),
1259+
ip: row.ip.ip(),
1260+
})
1261+
.collect();
1262+
OmicronZoneExternalFloatingIps::new(ips).with_context(|| {
1263+
format!("zone {zone_id} has invalid Nexus external IPs")
1264+
})
1265+
}
1266+
1267+
/// Reconstruct an external DNS zone's addresses from its child rows.
1268+
fn into_external_dns_addrs(
1269+
rows: Vec<Self>,
1270+
zone_id: OmicronZoneUuid,
1271+
) -> anyhow::Result<OmicronZoneExternalFloatingAddrs> {
1272+
let addrs = rows
1273+
.into_iter()
1274+
.map(|row| {
1275+
Ok(OmicronZoneExternalFloatingAddr {
1276+
id: row.external_ip_id.into(),
1277+
addr: row.to_floating_addr()?,
1278+
})
1279+
})
1280+
.collect::<anyhow::Result<Vec<_>>>()?;
1281+
OmicronZoneExternalFloatingAddrs::new(addrs).with_context(|| {
1282+
format!("zone {zone_id} has invalid external DNS addresses")
1283+
})
1284+
}
1285+
1286+
/// Reconstruct a boundary NTP zone's SNAT configuration from its child rows.
1287+
fn into_boundary_ntp_snat(
1288+
rows: Vec<Self>,
1289+
zone_id: OmicronZoneUuid,
1290+
) -> anyhow::Result<OmicronZoneExternalSnat> {
1291+
let snat_ips = rows
1292+
.into_iter()
1293+
.map(|row| {
1294+
Ok(OmicronZoneExternalSnatIp {
1295+
id: row.external_ip_id.into(),
1296+
snat_cfg: row.to_snat_config()?,
1297+
})
1298+
})
1299+
.collect::<anyhow::Result<Vec<_>>>()?;
1300+
OmicronZoneExternalSnat::from_ips(snat_ips).with_context(|| {
1301+
format!("zone {zone_id} has invalid boundary NTP SNAT config")
1302+
})
12721303
}
12731304

12741305
/// Interpret this row as a boundary NTP source-NAT configuration.

0 commit comments

Comments
 (0)