Skip to content

Commit 1c35870

Browse files
authored
Add IP version to IP Pool database objects (#8885)
- Add an `IpVersion` type and attach to all IP Pool objects - Schema and data migration to add the version to IP Pools in the database - Add a services IP Pool for IPv6 addresses - Ensure we can only add ranges to pools of the same version
1 parent e050434 commit 1c35870

File tree

28 files changed

+1023
-194
lines changed

28 files changed

+1023
-194
lines changed

common/src/address.rs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,29 @@ pub fn get_64_subnet(
368368
Ipv6Subnet::<SLED_PREFIX>::new(Ipv6Addr::from(rack_network))
369369
}
370370

371+
/// The IP address version.
372+
#[derive(Clone, Copy, Debug, Deserialize, JsonSchema, PartialEq, Serialize)]
373+
#[serde(rename_all = "snake_case")]
374+
pub enum IpVersion {
375+
V4,
376+
V6,
377+
}
378+
379+
impl IpVersion {
380+
pub const fn v4() -> IpVersion {
381+
IpVersion::V4
382+
}
383+
}
384+
385+
impl std::fmt::Display for IpVersion {
386+
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
387+
match self {
388+
Self::V4 => write!(f, "v4"),
389+
Self::V6 => write!(f, "v6"),
390+
}
391+
}
392+
}
393+
371394
/// An IP Range is a contiguous range of IP addresses, usually within an IP
372395
/// Pool.
373396
///
@@ -450,6 +473,24 @@ impl IpRange {
450473
IpRange::V6(ip6) => ip6.len(),
451474
}
452475
}
476+
477+
/// Return true if this is an IPv4 range, and false for IPv6.
478+
pub fn is_ipv4(&self) -> bool {
479+
matches!(self, IpRange::V4(_))
480+
}
481+
482+
/// Return true if this is an IPv6 range, and false for IPv4.
483+
pub fn is_ipv6(&self) -> bool {
484+
matches!(self, IpRange::V6(_))
485+
}
486+
487+
/// Return the IP version of this range.
488+
pub fn version(&self) -> IpVersion {
489+
match self {
490+
IpRange::V4(_) => IpVersion::V4,
491+
IpRange::V6(_) => IpVersion::V6,
492+
}
493+
}
453494
}
454495

455496
impl From<IpAddr> for IpRange {

common/src/api/external/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
1010
mod error;
1111
pub mod http_pagination;
12+
pub use crate::address::IpVersion;
1213
pub use crate::api::internal::shared::AllowedSourceIps;
1314
pub use crate::api::internal::shared::SwitchLocation;
1415
use crate::update::ArtifactId;

nexus/db-model/src/ip_pool.rs

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,62 @@ use nexus_db_schema::schema::ip_pool;
1616
use nexus_db_schema::schema::ip_pool_range;
1717
use nexus_db_schema::schema::ip_pool_resource;
1818
use nexus_types::external_api::params;
19+
use nexus_types::external_api::shared;
1920
use nexus_types::external_api::shared::IpRange;
2021
use nexus_types::external_api::views;
2122
use nexus_types::identity::Resource;
2223
use omicron_common::api::external;
2324
use std::net::IpAddr;
2425
use uuid::Uuid;
2526

27+
impl_enum_type!(
28+
IpVersionEnum:
29+
30+
#[derive(
31+
AsExpression,
32+
Clone,
33+
Copy,
34+
Debug,
35+
serde::Deserialize,
36+
Eq,
37+
FromSqlRow,
38+
schemars::JsonSchema,
39+
PartialEq,
40+
serde::Serialize,
41+
)]
42+
pub enum IpVersion;
43+
44+
V4 => b"v4"
45+
V6 => b"v6"
46+
);
47+
48+
impl ::std::fmt::Display for IpVersion {
49+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50+
f.write_str(match self {
51+
IpVersion::V4 => "v4",
52+
IpVersion::V6 => "v6",
53+
})
54+
}
55+
}
56+
57+
impl From<shared::IpVersion> for IpVersion {
58+
fn from(value: shared::IpVersion) -> Self {
59+
match value {
60+
shared::IpVersion::V4 => Self::V4,
61+
shared::IpVersion::V6 => Self::V6,
62+
}
63+
}
64+
}
65+
66+
impl From<IpVersion> for shared::IpVersion {
67+
fn from(value: IpVersion) -> Self {
68+
match value {
69+
IpVersion::V4 => Self::V4,
70+
IpVersion::V6 => Self::V6,
71+
}
72+
}
73+
}
74+
2675
/// An IP Pool is a collection of IP addresses external to the rack.
2776
///
2877
/// IP pools can be external or internal. External IP pools can be associated
@@ -34,21 +83,40 @@ pub struct IpPool {
3483
#[diesel(embed)]
3584
pub identity: IpPoolIdentity,
3685

86+
/// The IP version of the pool.
87+
pub ip_version: IpVersion,
88+
3789
/// Child resource generation number, for optimistic concurrency control of
3890
/// the contained ranges.
3991
pub rcgen: i64,
4092
}
4193

4294
impl IpPool {
43-
pub fn new(pool_identity: &external::IdentityMetadataCreateParams) -> Self {
95+
pub fn new(
96+
pool_identity: &external::IdentityMetadataCreateParams,
97+
ip_version: IpVersion,
98+
) -> Self {
4499
Self {
45100
identity: IpPoolIdentity::new(
46101
Uuid::new_v4(),
47102
pool_identity.clone(),
48103
),
104+
ip_version,
49105
rcgen: 0,
50106
}
51107
}
108+
109+
pub fn new_v4(
110+
pool_identity: &external::IdentityMetadataCreateParams,
111+
) -> Self {
112+
Self::new(pool_identity, IpVersion::V4)
113+
}
114+
115+
pub fn new_v6(
116+
pool_identity: &external::IdentityMetadataCreateParams,
117+
) -> Self {
118+
Self::new(pool_identity, IpVersion::V6)
119+
}
52120
}
53121

54122
impl From<IpPool> for views::IpPool {

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(182, 0, 0);
19+
pub const SCHEMA_VERSION: Version = Version::new(183, 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(183, "add-ip-version-to-pools"),
3132
KnownVersion::new(182, "add-tuf-artifact-board"),
3233
KnownVersion::new(181, "rename-nat-table"),
3334
KnownVersion::new(180, "sled-cpu-family"),

nexus/db-queries/src/db/datastore/deployment.rs

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2954,6 +2954,7 @@ mod tests {
29542954
use crate::db::pub_test_utils::TestDatabase;
29552955
use crate::db::raw_query_builder::QueryBuilder;
29562956
use gateway_types::rot::RotSlot;
2957+
use nexus_db_model::IpVersion;
29572958
use nexus_inventory::CollectionBuilder;
29582959
use nexus_inventory::now_db_precision;
29592960
use nexus_reconfigurator_planning::blueprint_builder::BlueprintBuilder;
@@ -4221,12 +4222,17 @@ mod tests {
42214222
Ipv4Addr::new(10, 0, 0, 10),
42224223
))
42234224
.unwrap();
4224-
let (service_ip_pool, _) = datastore
4225-
.ip_pools_service_lookup(&opctx)
4225+
let (service_authz_ip_pool, service_ip_pool) = datastore
4226+
.ip_pools_service_lookup(&opctx, IpVersion::V4)
42264227
.await
42274228
.expect("lookup service ip pool");
42284229
datastore
4229-
.ip_pool_add_range(&opctx, &service_ip_pool, &ip_range)
4230+
.ip_pool_add_range(
4231+
&opctx,
4232+
&service_authz_ip_pool,
4233+
&service_ip_pool,
4234+
&ip_range,
4235+
)
42304236
.await
42314237
.expect("add range to service ip pool");
42324238
let zone_id = OmicronZoneUuid::new_v4();
@@ -4353,13 +4359,14 @@ mod tests {
43534359
.map(|(ip, _nic)| ip.ip())
43544360
})
43554361
.expect("found external IP");
4356-
let (service_ip_pool, _) = datastore
4357-
.ip_pools_service_lookup(&opctx)
4362+
let (service_authz_ip_pool, service_ip_pool) = datastore
4363+
.ip_pools_service_lookup(&opctx, IpVersion::V4)
43584364
.await
43594365
.expect("lookup service ip pool");
43604366
datastore
43614367
.ip_pool_add_range(
43624368
&opctx,
4369+
&service_authz_ip_pool,
43634370
&service_ip_pool,
43644371
&IpRange::try_from((nexus_ip, nexus_ip))
43654372
.expect("valid IP range"),

nexus/db-queries/src/db/datastore/deployment/external_networking.rs

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ use nexus_types::deployment::BlueprintZoneConfig;
1818
use nexus_types::deployment::OmicronZoneExternalIp;
1919
use omicron_common::api::external::Error;
2020
use omicron_common::api::external::IdentityMetadataCreateParams;
21+
use omicron_common::api::external::IpVersion;
2122
use omicron_common::api::internal::shared::NetworkInterface;
2223
use omicron_common::api::internal::shared::NetworkInterfaceKind;
2324
use omicron_uuid_kinds::GenericUuid;
@@ -36,10 +37,11 @@ impl DataStore {
3637
opctx: &OpContext,
3738
zones_to_allocate: impl Iterator<Item = &BlueprintZoneConfig>,
3839
) -> Result<(), Error> {
39-
// Looking up the service pool ID requires an opctx; we'll do this once
40-
// up front and reuse the pool ID (which never changes) in the loop
41-
// below.
42-
let (_, pool) = self.ip_pools_service_lookup(opctx).await?;
40+
// Looking up the service pool IDs requires an opctx; we'll do this at
41+
// most once inside the loop below, when we first encounter an address
42+
// of the same IP version.
43+
let mut v4_pool = None;
44+
let mut v6_pool = None;
4345

4446
for z in zones_to_allocate {
4547
let Some((external_ip, nic)) = z.zone_type.external_networking()
@@ -55,10 +57,29 @@ impl DataStore {
5557
"nic" => format!("{nic:?}"),
5658
));
5759

60+
// Get existing pool or look it up and cache it.
61+
let version = external_ip.ip_version();
62+
let pool_ref = match version {
63+
IpVersion::V4 => &mut v4_pool,
64+
IpVersion::V6 => &mut v6_pool,
65+
};
66+
let pool = match pool_ref {
67+
Some(p) => p,
68+
None => {
69+
let new = self
70+
.ip_pools_service_lookup(opctx, version.into())
71+
.await?
72+
.1;
73+
*pool_ref = Some(new);
74+
pool_ref.as_ref().unwrap()
75+
}
76+
};
77+
78+
// Actually ensure the IP address.
5879
let kind = z.zone_type.kind();
5980
self.ensure_external_service_ip(
6081
conn,
61-
&pool,
82+
pool,
6283
kind,
6384
z.id,
6485
external_ip,
@@ -592,12 +613,17 @@ mod tests {
592613
opctx: &OpContext,
593614
datastore: &DataStore,
594615
) {
595-
let (ip_pool, _) = datastore
596-
.ip_pools_service_lookup(&opctx)
616+
let (ip_pool, db_pool) = datastore
617+
.ip_pools_service_lookup(&opctx, IpVersion::V4.into())
597618
.await
598619
.expect("failed to find service IP pool");
599620
datastore
600-
.ip_pool_add_range(&opctx, &ip_pool, &self.external_ips_range)
621+
.ip_pool_add_range(
622+
&opctx,
623+
&ip_pool,
624+
&db_pool,
625+
&self.external_ips_range,
626+
)
601627
.await
602628
.expect("failed to expand service IP pool");
603629
}

nexus/db-queries/src/db/datastore/external_ip.rs

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ use nexus_db_lookup::LookupPath;
3939
use nexus_db_model::FloatingIpUpdate;
4040
use nexus_db_model::Instance;
4141
use nexus_db_model::IpAttachState;
42+
use nexus_db_model::IpVersion;
4243
use nexus_sled_agent_shared::inventory::ZoneKind;
4344
use nexus_types::deployment::OmicronZoneExternalIp;
4445
use nexus_types::identity::Resource;
@@ -318,7 +319,9 @@ impl DataStore {
318319
zone_kind: ZoneKind,
319320
external_ip: OmicronZoneExternalIp,
320321
) -> CreateResult<ExternalIp> {
321-
let (authz_pool, pool) = self.ip_pools_service_lookup(opctx).await?;
322+
let version = IpVersion::from(external_ip.ip_version());
323+
let (authz_pool, pool) =
324+
self.ip_pools_service_lookup(opctx, version).await?;
322325
opctx.authorize(authz::Action::CreateChild, &authz_pool).await?;
323326
let data = IncompleteExternalIp::for_omicron_zone(
324327
pool.id(),
@@ -356,7 +359,12 @@ impl DataStore {
356359
) -> ListResultVec<ExternalIp> {
357360
use nexus_db_schema::schema::external_ip::dsl;
358361

359-
let (authz_pool, _pool) = self.ip_pools_service_lookup(opctx).await?;
362+
// Note the IP version used here isn't important. It's just for the
363+
// authz check to list children, and not used for the actual database
364+
// query below, which filters on is_service to get external IPs from
365+
// either pool.
366+
let (authz_pool, _pool) =
367+
self.ip_pools_service_lookup(opctx, IpVersion::V4).await?;
360368
opctx.authorize(authz::Action::ListChildren, &authz_pool).await?;
361369

362370
paginated(dsl::external_ip, dsl::id, pagparams)
@@ -1183,12 +1191,12 @@ mod tests {
11831191
Ipv4Addr::new(10, 0, 0, 10),
11841192
))
11851193
.unwrap();
1186-
let (service_ip_pool, _) = datastore
1187-
.ip_pools_service_lookup(opctx)
1194+
let (service_ip_pool, db_pool) = datastore
1195+
.ip_pools_service_lookup(opctx, IpVersion::V4)
11881196
.await
11891197
.expect("lookup service ip pool");
11901198
datastore
1191-
.ip_pool_add_range(opctx, &service_ip_pool, &ip_range)
1199+
.ip_pool_add_range(opctx, &service_ip_pool, &db_pool, &ip_range)
11921200
.await
11931201
.expect("add range to service ip pool");
11941202

0 commit comments

Comments
 (0)