Skip to content

Add IP version to IP Pool database objects #8885

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
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
41 changes: 41 additions & 0 deletions common/src/address.rs
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,29 @@ pub fn get_64_subnet(
Ipv6Subnet::<SLED_PREFIX>::new(Ipv6Addr::from(rack_network))
}

/// The IP address version.
#[derive(Clone, Copy, Debug, Deserialize, JsonSchema, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum IpVersion {
V4,
V6,
}

impl IpVersion {
pub const fn v4() -> IpVersion {
IpVersion::V4
}
}

impl std::fmt::Display for IpVersion {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self::V4 => write!(f, "v4"),
Self::V6 => write!(f, "v6"),
}
}
}

/// An IP Range is a contiguous range of IP addresses, usually within an IP
/// Pool.
///
Expand Down Expand Up @@ -450,6 +473,24 @@ impl IpRange {
IpRange::V6(ip6) => ip6.len(),
}
}

/// Return true if this is an IPv4 range, and false for IPv6.
pub fn is_ipv4(&self) -> bool {
matches!(self, IpRange::V4(_))
}

/// Return true if this is an IPv6 range, and false for IPv4.
pub fn is_ipv6(&self) -> bool {
matches!(self, IpRange::V6(_))
}

/// Return the IP version of this range.
pub fn version(&self) -> IpVersion {
match self {
IpRange::V4(_) => IpVersion::V4,
IpRange::V6(_) => IpVersion::V6,
}
}
}

impl From<IpAddr> for IpRange {
Expand Down
1 change: 1 addition & 0 deletions common/src/api/external/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

mod error;
pub mod http_pagination;
pub use crate::address::IpVersion;
pub use crate::api::internal::shared::AllowedSourceIps;
pub use crate::api::internal::shared::SwitchLocation;
use crate::update::ArtifactId;
Expand Down
60 changes: 58 additions & 2 deletions nexus/db-model/src/ip_pool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,62 @@ use nexus_db_schema::schema::ip_pool;
use nexus_db_schema::schema::ip_pool_range;
use nexus_db_schema::schema::ip_pool_resource;
use nexus_types::external_api::params;
use nexus_types::external_api::shared;
use nexus_types::external_api::shared::IpRange;
use nexus_types::external_api::views;
use nexus_types::identity::Resource;
use omicron_common::api::external;
use std::net::IpAddr;
use uuid::Uuid;

impl_enum_type!(
IpVersionEnum:

#[derive(
AsExpression,
Clone,
Copy,
Debug,
serde::Deserialize,
Eq,
FromSqlRow,
schemars::JsonSchema,
PartialEq,
serde::Serialize,
)]
pub enum IpVersion;

V4 => b"v4"
V6 => b"v6"
);

impl ::std::fmt::Display for IpVersion {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
IpVersion::V4 => "v4",
IpVersion::V6 => "v6",
})
}
}

impl From<shared::IpVersion> for IpVersion {
fn from(value: shared::IpVersion) -> Self {
match value {
shared::IpVersion::V4 => Self::V4,
shared::IpVersion::V6 => Self::V6,
}
}
}

impl From<IpVersion> for shared::IpVersion {
fn from(value: IpVersion) -> Self {
match value {
IpVersion::V4 => Self::V4,
IpVersion::V6 => Self::V6,
}
}
}

/// An IP Pool is a collection of IP addresses external to the rack.
///
/// IP pools can be external or internal. External IP pools can be associated
Expand All @@ -34,26 +83,33 @@ pub struct IpPool {
#[diesel(embed)]
pub identity: IpPoolIdentity,

/// The IP version of the pool.
pub ip_version: IpVersion,

/// Child resource generation number, for optimistic concurrency control of
/// the contained ranges.
pub rcgen: i64,
}

impl IpPool {
pub fn new(pool_identity: &external::IdentityMetadataCreateParams) -> Self {
pub fn new(
pool_identity: &external::IdentityMetadataCreateParams,
ip_version: IpVersion,
) -> Self {
Self {
identity: IpPoolIdentity::new(
Uuid::new_v4(),
pool_identity.clone(),
),
ip_version,
rcgen: 0,
}
}
}

impl From<IpPool> for views::IpPool {
fn from(pool: IpPool) -> Self {
Self { identity: pool.identity() }
Self { identity: pool.identity(), ip_version: pool.ip_version.into() }
}
}

Expand Down
3 changes: 2 additions & 1 deletion nexus/db-model/src/schema_versions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use std::{collections::BTreeMap, sync::LazyLock};
///
/// This must be updated when you change the database schema. Refer to
/// schema/crdb/README.adoc in the root of this repository for details.
pub const SCHEMA_VERSION: Version = Version::new(181, 0, 0);
pub const SCHEMA_VERSION: Version = Version::new(182, 0, 0);

/// List of all past database schema versions, in *reverse* order
///
Expand All @@ -28,6 +28,7 @@ static KNOWN_VERSIONS: LazyLock<Vec<KnownVersion>> = LazyLock::new(|| {
// | leaving the first copy as an example for the next person.
// v
// KnownVersion::new(next_int, "unique-dirname-with-the-sql-files"),
KnownVersion::new(182, "add-ip-version-to-pools"),
KnownVersion::new(181, "rename-nat-table"),
KnownVersion::new(180, "sled-cpu-family"),
KnownVersion::new(179, "add-pending-mgs-updates-host-phase-1"),
Expand Down
17 changes: 12 additions & 5 deletions nexus/db-queries/src/db/datastore/deployment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2954,6 +2954,7 @@ mod tests {
use crate::db::pub_test_utils::TestDatabase;
use crate::db::raw_query_builder::QueryBuilder;
use gateway_types::rot::RotSlot;
use nexus_db_model::IpVersion;
use nexus_inventory::CollectionBuilder;
use nexus_inventory::now_db_precision;
use nexus_reconfigurator_planning::blueprint_builder::BlueprintBuilder;
Expand Down Expand Up @@ -4219,12 +4220,17 @@ mod tests {
Ipv4Addr::new(10, 0, 0, 10),
))
.unwrap();
let (service_ip_pool, _) = datastore
.ip_pools_service_lookup(&opctx)
let (service_authz_ip_pool, service_ip_pool) = datastore
.ip_pools_service_lookup(&opctx, IpVersion::V4)
.await
.expect("lookup service ip pool");
datastore
.ip_pool_add_range(&opctx, &service_ip_pool, &ip_range)
.ip_pool_add_range(
&opctx,
&service_authz_ip_pool,
&service_ip_pool,
&ip_range,
)
.await
.expect("add range to service ip pool");
let zone_id = OmicronZoneUuid::new_v4();
Expand Down Expand Up @@ -4351,13 +4357,14 @@ mod tests {
.map(|(ip, _nic)| ip.ip())
})
.expect("found external IP");
let (service_ip_pool, _) = datastore
.ip_pools_service_lookup(&opctx)
let (service_authz_ip_pool, service_ip_pool) = datastore
.ip_pools_service_lookup(&opctx, IpVersion::V4)
.await
.expect("lookup service ip pool");
datastore
.ip_pool_add_range(
&opctx,
&service_authz_ip_pool,
&service_ip_pool,
&IpRange::try_from((nexus_ip, nexus_ip))
.expect("valid IP range"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ use nexus_types::deployment::BlueprintZoneConfig;
use nexus_types::deployment::OmicronZoneExternalIp;
use omicron_common::api::external::Error;
use omicron_common::api::external::IdentityMetadataCreateParams;
use omicron_common::api::external::IpVersion;
use omicron_common::api::internal::shared::NetworkInterface;
use omicron_common::api::internal::shared::NetworkInterfaceKind;
use omicron_uuid_kinds::GenericUuid;
Expand All @@ -29,17 +30,58 @@ use slog::info;
use slog::warn;
use slog_error_chain::InlineErrorChain;

// Helper type to lookup service IP Pools only when needed, at most once.
//
// In `ensure_zone_external_networking_deallocated_on_connection`, we lookup
// pools only when we're sure we need them, based on the versions of the IP
// addresses we need to provide for each zone.
struct ServiceIpPools<'a> {
maybe_ipv4_pool: Option<IpPool>,
maybe_ipv6_pool: Option<IpPool>,
datastore: &'a DataStore,
opctx: &'a OpContext,
}

impl<'a> ServiceIpPools<'a> {
fn new(datastore: &'a DataStore, opctx: &'a OpContext) -> Self {
Self { maybe_ipv4_pool: None, maybe_ipv6_pool: None, datastore, opctx }
}

/// Get the service IP Pool for the given address.
///
/// This may need to call out to the database to fetch the pool.
async fn pool_for(
&mut self,
external_ip: &OmicronZoneExternalIp,
) -> Result<&IpPool, Error> {
let version = external_ip.ip_version();
let pool = match version {
IpVersion::V4 => &mut self.maybe_ipv4_pool,
IpVersion::V6 => &mut self.maybe_ipv6_pool,
};
if let Some(pool) = pool {
return Ok(&*pool);
}
let new_pool = self
.datastore
.ip_pools_service_lookup(self.opctx, version.into())
.await?
.1;
Ok(pool.insert(new_pool))
}
}

impl DataStore {
pub(super) async fn ensure_zone_external_networking_allocated_on_connection(
&self,
conn: &async_bb8_diesel::Connection<DbConnection>,
opctx: &OpContext,
zones_to_allocate: impl Iterator<Item = &BlueprintZoneConfig>,
) -> Result<(), Error> {
// Looking up the service pool ID requires an opctx; we'll do this once
// up front and reuse the pool ID (which never changes) in the loop
// Looking up the service pool IDs requires an opctx; we'll do this once
// up front and reuse the pool IDs (which never change) in the loop
// below.
let (_, pool) = self.ip_pools_service_lookup(opctx).await?;
let mut ip_pools = ServiceIpPools::new(self, opctx);

for z in zones_to_allocate {
let Some((external_ip, nic)) = z.zone_type.external_networking()
Expand All @@ -56,9 +98,10 @@ impl DataStore {
));

let kind = z.zone_type.kind();
let pool = ip_pools.pool_for(&external_ip).await?;
self.ensure_external_service_ip(
conn,
&pool,
pool,
kind,
z.id,
external_ip,
Expand Down Expand Up @@ -592,12 +635,17 @@ mod tests {
opctx: &OpContext,
datastore: &DataStore,
) {
let (ip_pool, _) = datastore
.ip_pools_service_lookup(&opctx)
let (ip_pool, db_pool) = datastore
.ip_pools_service_lookup(&opctx, IpVersion::V4.into())
.await
.expect("failed to find service IP pool");
datastore
.ip_pool_add_range(&opctx, &ip_pool, &self.external_ips_range)
.ip_pool_add_range(
&opctx,
&ip_pool,
&db_pool,
&self.external_ips_range,
)
.await
.expect("failed to expand service IP pool");
}
Expand Down
18 changes: 13 additions & 5 deletions nexus/db-queries/src/db/datastore/external_ip.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ use nexus_db_lookup::LookupPath;
use nexus_db_model::FloatingIpUpdate;
use nexus_db_model::Instance;
use nexus_db_model::IpAttachState;
use nexus_db_model::IpVersion;
use nexus_sled_agent_shared::inventory::ZoneKind;
use nexus_types::deployment::OmicronZoneExternalIp;
use nexus_types::identity::Resource;
Expand Down Expand Up @@ -318,7 +319,9 @@ impl DataStore {
zone_kind: ZoneKind,
external_ip: OmicronZoneExternalIp,
) -> CreateResult<ExternalIp> {
let (authz_pool, pool) = self.ip_pools_service_lookup(opctx).await?;
let version = IpVersion::from(external_ip.ip_version());
let (authz_pool, pool) =
self.ip_pools_service_lookup(opctx, version).await?;
opctx.authorize(authz::Action::CreateChild, &authz_pool).await?;
let data = IncompleteExternalIp::for_omicron_zone(
pool.id(),
Expand Down Expand Up @@ -356,7 +359,12 @@ impl DataStore {
) -> ListResultVec<ExternalIp> {
use nexus_db_schema::schema::external_ip::dsl;

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

paginated(dsl::external_ip, dsl::id, pagparams)
Expand Down Expand Up @@ -1183,12 +1191,12 @@ mod tests {
Ipv4Addr::new(10, 0, 0, 10),
))
.unwrap();
let (service_ip_pool, _) = datastore
.ip_pools_service_lookup(opctx)
let (service_ip_pool, db_pool) = datastore
.ip_pools_service_lookup(opctx, IpVersion::V4)
.await
.expect("lookup service ip pool");
datastore
.ip_pool_add_range(opctx, &service_ip_pool, &ip_range)
.ip_pool_add_range(opctx, &service_ip_pool, &db_pool, &ip_range)
.await
.expect("add range to service ip pool");

Expand Down
Loading
Loading