Skip to content

Commit 8934933

Browse files
committed
Separate transit IPs by version in the database
This is a follow-up to #9389 that I missed. We were already separating transit IPs by version in the code before and after the database. This pushes that separation all the way into the database, which will make it easier to implement updates to dual-stack NICs in the public API. Separating them lets clients update the transit IPs for only one version, without some janky read-modify-write code that ensures we don't clobber the transit IPs for the _other_ version.
1 parent 71d9385 commit 8934933

File tree

13 files changed

+217
-148
lines changed

13 files changed

+217
-148
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

nexus/db-model/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ diesel = { workspace = true, features = ["postgres", "r2d2", "chrono", "serde_js
2222
hex.workspace = true
2323
iddqd.workspace = true
2424
ipnetwork.workspace = true
25+
itertools.workspace = true
2526
macaddr.workspace = true
2627
newtype_derive.workspace = true
2728
omicron-cockroach-metrics.workspace = true

nexus/db-model/src/network_interface.rs

Lines changed: 83 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ use chrono::DateTime;
1212
use chrono::Utc;
1313
use db_macros::Resource;
1414
use diesel::AsChangeset;
15-
use ipnetwork::IpNetwork;
15+
use itertools::Either;
16+
use itertools::Itertools;
1617
use nexus_db_schema::schema::instance_network_interface;
1718
use nexus_db_schema::schema::network_interface;
1819
use nexus_db_schema::schema::service_network_interface;
@@ -98,9 +99,13 @@ pub struct NetworkInterface {
9899
/// True if this is the instance's primary interface.
99100
#[diesel(column_name = is_primary)]
100101
pub primary: bool,
101-
/// List of additional networks on which the instance is allowed to send /
102-
/// receive traffic.
103-
pub transit_ips: Vec<IpNetwork>,
102+
/// List of additional IPv4 networks on which the instance is allowed to
103+
/// send / receive traffic.
104+
#[diesel(column_name = transit_ips)]
105+
pub transit_ips_v4: Vec<crate::Ipv4Net>,
106+
/// List of additional IPv6 networks on which the instance is allowed to
107+
/// send / receive traffic.
108+
pub transit_ips_v6: Vec<crate::Ipv6Net>,
104109
}
105110

106111
impl NetworkInterface {
@@ -119,43 +124,25 @@ impl NetworkInterface {
119124
)));
120125
}
121126
(None, Some(ip)) => {
122-
// Check that all transit IPs are IPv6.
123127
let transit_ips = self
124-
.transit_ips
128+
.transit_ips_v6
125129
.iter()
126-
.map(|net| {
127-
let IpNetwork::V6(net) = net else {
128-
return Err(Error::internal_error(&format!(
129-
"NIC with ID '{}' is IPv6-only, but has \
130-
IPv4 transit IPs",
131-
self.id(),
132-
)));
133-
};
134-
Ok(Ipv6Net::from(*net))
135-
})
136-
.collect::<Result<_, _>>()?;
130+
.copied()
131+
.map(Into::into)
132+
.collect();
137133
PrivateIpConfig::V6(PrivateIpv6Config::new_with_transit_ips(
138134
*ip,
139135
ipv6_subnet,
140136
transit_ips,
141137
)?)
142138
}
143139
(Some(ip), None) => {
144-
// Check that all transit IPs are IPv4.
145140
let transit_ips = self
146-
.transit_ips
141+
.transit_ips_v4
147142
.iter()
148-
.map(|net| {
149-
let IpNetwork::V4(net) = net else {
150-
return Err(Error::internal_error(&format!(
151-
"NIC with ID '{}' is IPv4-only, but has \
152-
IPv6 transit IPs",
153-
self.id(),
154-
)));
155-
};
156-
Ok(Ipv4Net::from(*net))
157-
})
158-
.collect::<Result<_, _>>()?;
143+
.copied()
144+
.map(Into::into)
145+
.collect();
159146
PrivateIpConfig::V4(PrivateIpv4Config::new_with_transit_ips(
160147
*ip,
161148
ipv4_subnet,
@@ -164,20 +151,16 @@ impl NetworkInterface {
164151
}
165152
(Some(ipv4), Some(ipv6)) => {
166153
let ipv4_transit_ips = self
167-
.transit_ips
154+
.transit_ips_v4
168155
.iter()
169-
.filter_map(|net| match net {
170-
IpNetwork::V4(net) => Some(Ipv4Net::from(*net)),
171-
IpNetwork::V6(_) => None,
172-
})
156+
.copied()
157+
.map(Into::into)
173158
.collect();
174159
let ipv6_transit_ips = self
175-
.transit_ips
160+
.transit_ips_v6
176161
.iter()
177-
.filter_map(|net| match net {
178-
IpNetwork::V6(net) => Some(Ipv6Net::from(*net)),
179-
IpNetwork::V4(_) => None,
180-
})
162+
.copied()
163+
.map(Into::into)
181164
.collect();
182165
let v4 = PrivateIpv4Config::new_with_transit_ips(
183166
*ipv4,
@@ -241,7 +224,8 @@ pub struct InstanceNetworkInterface {
241224
pub slot: SqlU8,
242225
#[diesel(column_name = is_primary)]
243226
pub primary: bool,
244-
pub transit_ips: Vec<IpNetwork>,
227+
pub transit_ips_v4: Vec<crate::Ipv4Net>,
228+
pub transit_ips_v6: Vec<crate::Ipv6Net>,
245229
}
246230

247231
/// Service Network Interface DB model.
@@ -353,7 +337,8 @@ impl NetworkInterface {
353337
ipv6: self.ipv6,
354338
slot: self.slot,
355339
primary: self.primary,
356-
transit_ips: self.transit_ips,
340+
transit_ips_v4: self.transit_ips_v4,
341+
transit_ips_v6: self.transit_ips_v6,
357342
}
358343
}
359344

@@ -404,7 +389,8 @@ impl From<InstanceNetworkInterface> for NetworkInterface {
404389
ipv6: iface.ipv6,
405390
slot: iface.slot,
406391
primary: iface.primary,
407-
transit_ips: iface.transit_ips,
392+
transit_ips_v4: iface.transit_ips_v4,
393+
transit_ips_v6: iface.transit_ips_v6,
408394
}
409395
}
410396
}
@@ -429,7 +415,8 @@ impl From<ServiceNetworkInterface> for NetworkInterface {
429415
ipv6: iface.ipv6,
430416
slot: iface.slot,
431417
primary: iface.primary,
432-
transit_ips: vec![],
418+
transit_ips_v4: vec![],
419+
transit_ips_v6: vec![],
433420
}
434421
}
435422
}
@@ -513,15 +500,19 @@ impl IpConfig {
513500
IpConfig::V4(Ipv4Config::default())
514501
}
515502

516-
/// Return the IPv4 address assignment.
517-
pub fn ipv4_assignment(&self) -> Option<&Ipv4Assignment> {
503+
/// Return the IPv4 configuration, if it exists.
504+
pub fn ipv4_config(&self) -> Option<&Ipv4Config> {
518505
match self {
519-
IpConfig::V4(Ipv4Config { ip, .. }) => Some(ip),
506+
IpConfig::V4(v4) | IpConfig::DualStack { v4, .. } => Some(v4),
520507
IpConfig::V6(_) => None,
521-
IpConfig::DualStack { v4: Ipv4Config { ip, .. }, .. } => Some(ip),
522508
}
523509
}
524510

511+
/// Return the IPv4 address assignment.
512+
pub fn ipv4_assignment(&self) -> Option<&Ipv4Assignment> {
513+
self.ipv4_config().map(|v4| &v4.ip)
514+
}
515+
525516
/// Return the IPv4 address explicitly requested, if one exists.
526517
pub fn ipv4_addr(&self) -> Option<&std::net::Ipv4Addr> {
527518
self.ipv4_assignment().and_then(|assignment| match assignment {
@@ -530,6 +521,11 @@ impl IpConfig {
530521
})
531522
}
532523

524+
/// Return the IPv4 transit IPs, if any.
525+
pub fn ipv4_transit_ips(&self) -> Option<Vec<Ipv4Net>> {
526+
self.ipv4_config().map(|v4| v4.transit_ips.clone())
527+
}
528+
533529
/// Construct an IPv6 configuration with no transit IPs.
534530
pub fn from_ipv6(addr: std::net::Ipv6Addr) -> Self {
535531
IpConfig::V6(Ipv6Config {
@@ -543,15 +539,19 @@ impl IpConfig {
543539
IpConfig::V6(Ipv6Config::default())
544540
}
545541

546-
/// Return the IPv6 address assignment.
547-
pub fn ipv6_assignment(&self) -> Option<&Ipv6Assignment> {
542+
/// Return the IPv6 configuration, if it exists.
543+
pub fn ipv6_config(&self) -> Option<&Ipv6Config> {
548544
match self {
549-
IpConfig::V6(Ipv6Config { ip, .. }) => Some(ip),
545+
IpConfig::V6(v6) | IpConfig::DualStack { v6, .. } => Some(v6),
550546
IpConfig::V4(_) => None,
551-
IpConfig::DualStack { v6: Ipv6Config { ip, .. }, .. } => Some(ip),
552547
}
553548
}
554549

550+
/// Return the IPv6 address assignment.
551+
pub fn ipv6_assignment(&self) -> Option<&Ipv6Assignment> {
552+
self.ipv6_config().map(|v6| &v6.ip)
553+
}
554+
555555
/// Return the IPv6 address explicitly requested, if one exists.
556556
pub fn ipv6_addr(&self) -> Option<&std::net::Ipv6Addr> {
557557
self.ipv6_assignment().and_then(|assignment| match assignment {
@@ -560,25 +560,24 @@ impl IpConfig {
560560
})
561561
}
562562

563+
/// Return the IPv6 transit IPs, if any.
564+
pub fn ipv6_transit_ips(&self) -> Option<Vec<Ipv6Net>> {
565+
self.ipv6_config().map(|v6| v6.transit_ips.clone())
566+
}
567+
563568
/// Return the transit IPs requested in this configuration.
564569
pub fn transit_ips(&self) -> Vec<IpNet> {
565-
match self {
566-
IpConfig::V4(Ipv4Config { transit_ips, .. }) => {
567-
transit_ips.iter().copied().map(Into::into).collect()
568-
}
569-
IpConfig::V6(Ipv6Config { transit_ips, .. }) => {
570-
transit_ips.iter().copied().map(Into::into).collect()
571-
}
572-
IpConfig::DualStack {
573-
v4: Ipv4Config { transit_ips: ipv4_addrs, .. },
574-
v6: Ipv6Config { transit_ips: ipv6_addrs, .. },
575-
} => ipv4_addrs
576-
.iter()
577-
.copied()
578-
.map(Into::into)
579-
.chain(ipv6_addrs.iter().copied().map(Into::into))
580-
.collect(),
581-
}
570+
self.ipv4_transit_ips()
571+
.unwrap_or_default()
572+
.into_iter()
573+
.map(Into::into)
574+
.chain(
575+
self.ipv6_transit_ips()
576+
.unwrap_or_default()
577+
.into_iter()
578+
.map(Into::into),
579+
)
580+
.collect()
582581
}
583582

584583
/// Construct a dual-stack IP configuration with explicit IP addresses.
@@ -775,7 +774,9 @@ pub struct NetworkInterfaceUpdate {
775774
pub time_modified: DateTime<Utc>,
776775
#[diesel(column_name = is_primary)]
777776
pub primary: Option<bool>,
778-
pub transit_ips: Vec<IpNetwork>,
777+
#[diesel(column_name = transit_ips)]
778+
pub transit_ips_v4: Vec<crate::Ipv4Net>,
779+
pub transit_ips_v6: Vec<crate::Ipv6Net>,
779780
}
780781

781782
impl From<InstanceNetworkInterface> for external::InstanceNetworkInterface {
@@ -791,10 +792,13 @@ impl From<InstanceNetworkInterface> for external::InstanceNetworkInterface {
791792
ip,
792793
mac: *iface.mac,
793794
primary: iface.primary,
795+
// https://github.com/oxidecomputer/omicron/issues/9248 Separate
796+
// these into IP versions.
794797
transit_ips: iface
795-
.transit_ips
798+
.transit_ips_v4
796799
.into_iter()
797-
.map(Into::into)
800+
.map(|v4| v4.0.into())
801+
.chain(iface.transit_ips_v6.into_iter().map(|v6| v6.0.into()))
798802
.collect(),
799803
}
800804
}
@@ -803,16 +807,18 @@ impl From<InstanceNetworkInterface> for external::InstanceNetworkInterface {
803807
impl From<params::InstanceNetworkInterfaceUpdate> for NetworkInterfaceUpdate {
804808
fn from(params: params::InstanceNetworkInterfaceUpdate) -> Self {
805809
let primary = if params.primary { Some(true) } else { None };
810+
let (transit_ips_v4, transit_ips_v6): (Vec<_>, Vec<_>) =
811+
params.transit_ips.into_iter().partition_map(|net| match net {
812+
IpNet::V4(v4) => Either::Left(crate::Ipv4Net::from(v4)),
813+
IpNet::V6(v6) => Either::Right(crate::Ipv6Net::from(v6)),
814+
});
806815
Self {
807816
name: params.identity.name.map(|n| n.into()),
808817
description: params.identity.description,
809818
time_modified: Utc::now(),
810819
primary,
811-
transit_ips: params
812-
.transit_ips
813-
.into_iter()
814-
.map(Into::into)
815-
.collect(),
820+
transit_ips_v4,
821+
transit_ips_v6,
816822
}
817823
}
818824
}

nexus/db-model/src/schema_versions.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ use std::{collections::BTreeMap, sync::LazyLock};
1616
///
1717
/// This must be updated when you change the database schema. Refer to
1818
/// schema/crdb/README.adoc in the root of this repository for details.
19-
pub const SCHEMA_VERSION: Version = Version::new(213, 0, 0);
19+
pub const SCHEMA_VERSION: Version = Version::new(214, 0, 0);
2020

2121
/// List of all past database schema versions, in *reverse* order
2222
///
@@ -28,6 +28,7 @@ static KNOWN_VERSIONS: LazyLock<Vec<KnownVersion>> = LazyLock::new(|| {
2828
// | leaving the first copy as an example for the next person.
2929
// v
3030
// KnownVersion::new(next_int, "unique-dirname-with-the-sql-files"),
31+
KnownVersion::new(214, "separate-transit-ips-by-version"),
3132
KnownVersion::new(213, "fm-cases"),
3233
KnownVersion::new(212, "local-storage-disk-type"),
3334
KnownVersion::new(211, "blueprint-sled-config-subnet"),

0 commit comments

Comments
 (0)