Skip to content

[nexus] Support allocating external IP addresses for internal services #1611

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

Merged
merged 6 commits into from
Sep 6, 2022
Merged
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
26 changes: 21 additions & 5 deletions common/src/sql/dbinit.sql
Original file line number Diff line number Diff line change
Expand Up @@ -1133,7 +1133,13 @@ CREATE TYPE omicron.public.ip_kind AS ENUM (
* known address that can be moved between instances. Its lifetime is not
* fixed to any instance.
*/
'floating'
'floating',

/*
* A service IP is an IP address not attached to a project nor an instance.
* It's intended to be used for internal services.
*/
'service'
);

/*
Expand All @@ -1160,7 +1166,7 @@ CREATE TABLE omicron.public.instance_external_ip (
ip_pool_range_id UUID NOT NULL,

/* FK to the `project` table. */
project_id UUID NOT NULL,
project_id UUID,

/* FK to the `instance` table. See the constraints below. */
instance_id UUID,
Expand All @@ -1177,6 +1183,15 @@ CREATE TABLE omicron.public.instance_external_ip (
/* The last port in the allowed range, also inclusive. */
last_port INT4 NOT NULL,

/*
* The project can only be NULL for service IPs.
* Additionally, the project MUST be NULL for service IPs.
*/
CONSTRAINT null_project CHECK(
(kind != 'service' AND project_id IS NOT NULL) OR
(kind = 'service' AND project_id IS NULL)
),

/* The name must be non-NULL iff this is a floating IP. */
CONSTRAINT null_fip_name CHECK (
(kind != 'floating' AND name IS NULL) OR
Expand All @@ -1190,12 +1205,13 @@ CREATE TABLE omicron.public.instance_external_ip (
),

/*
* Only nullable if this is a floating IP, which may exist not attached
* to any instance.
* Only nullable if this is a floating/service IP, which may exist not
* attached to any instance.
*/
CONSTRAINT null_non_fip_instance_id CHECK (
(kind != 'floating' AND instance_id IS NOT NULL) OR
(kind = 'floating')
(kind = 'floating') OR
(kind = 'service')
)
);

Expand Down
47 changes: 32 additions & 15 deletions nexus/db-model/src/external_ip.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ impl_enum_type!(
SNat => b"snat"
Ephemeral => b"ephemeral"
Floating => b"floating"
Service => b"service"
);

/// The main model type for external IP addresses for instances.
Expand All @@ -56,7 +57,7 @@ pub struct InstanceExternalIp {
pub time_deleted: Option<DateTime<Utc>>,
pub ip_pool_id: Uuid,
pub ip_pool_range_id: Uuid,
pub project_id: Uuid,
pub project_id: Option<Uuid>,
// This is Some(_) for:
// - all instance SNAT IPs
// - all ephemeral IPs
Expand All @@ -78,6 +79,18 @@ impl From<InstanceExternalIp> for sled_agent_client::types::SourceNatConfig {
}
}

/// Describes where the IP candidates for allocation come from: either
/// from an IP pool, or from a project.
///
/// This ensures that a source is always specified, and a caller cannot
/// request an external IP allocation without providing at least one of
/// these options.
#[derive(Debug, Clone, Copy)]
pub enum IpSource {
Instance { project_id: Uuid, pool_id: Option<Uuid> },
Service { pool_id: Uuid },
}

/// An incomplete external IP, used to store state required for issuing the
/// database query that selects an available IP and stores the resulting record.
#[derive(Debug, Clone)]
Expand All @@ -87,9 +100,8 @@ pub struct IncompleteInstanceExternalIp {
description: Option<String>,
time_created: DateTime<Utc>,
kind: IpKind,
project_id: Uuid,
instance_id: Option<Uuid>,
pool_id: Option<Uuid>,
source: IpSource,
}

impl IncompleteInstanceExternalIp {
Expand All @@ -105,9 +117,8 @@ impl IncompleteInstanceExternalIp {
description: None,
time_created: Utc::now(),
kind: IpKind::SNat,
project_id,
instance_id: Some(instance_id),
pool_id,
source: IpSource::Instance { project_id, pool_id },
}
}

Expand All @@ -123,9 +134,8 @@ impl IncompleteInstanceExternalIp {
description: None,
time_created: Utc::now(),
kind: IpKind::Ephemeral,
project_id,
instance_id: Some(instance_id),
pool_id,
source: IpSource::Instance { project_id, pool_id },
}
}

Expand All @@ -142,9 +152,20 @@ impl IncompleteInstanceExternalIp {
description: Some(description.to_string()),
time_created: Utc::now(),
kind: IpKind::Floating,
project_id,
instance_id: None,
pool_id,
source: IpSource::Instance { project_id, pool_id },
}
}

pub fn for_service(id: Uuid, pool_id: Uuid) -> Self {
Self {
id,
name: None,
description: None,
time_created: Utc::now(),
kind: IpKind::Service,
instance_id: None,
source: IpSource::Service { pool_id },
}
}

Expand All @@ -168,16 +189,12 @@ impl IncompleteInstanceExternalIp {
&self.kind
}

pub fn project_id(&self) -> &Uuid {
&self.project_id
}

pub fn instance_id(&self) -> &Option<Uuid> {
&self.instance_id
}

pub fn pool_id(&self) -> &Option<Uuid> {
&self.pool_id
pub fn source(&self) -> &IpSource {
&self.source
}
}

Expand Down
2 changes: 1 addition & 1 deletion nexus/db-model/src/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ table! {
time_deleted -> Nullable<Timestamptz>,
ip_pool_id -> Uuid,
ip_pool_range_id -> Uuid,
project_id -> Uuid,
project_id -> Nullable<Uuid>,
instance_id -> Nullable<Uuid>,
kind -> crate::IpKindEnum,
ip -> Inet,
Expand Down
16 changes: 15 additions & 1 deletion nexus/src/db/datastore/instance_external_ip.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ use crate::db::update_and_check::UpdateStatus;
use async_bb8_diesel::AsyncRunQueryDsl;
use chrono::Utc;
use diesel::prelude::*;
use nexus_types::identity::Resource;
use omicron_common::api::external::CreateResult;
use omicron_common::api::external::Error;
use omicron_common::api::external::LookupResult;
Expand All @@ -29,7 +30,6 @@ use uuid::Uuid;

impl DataStore {
/// Create an external IP address for source NAT for an instance.
// TODO-correctness: This should be made idempotent.
pub async fn allocate_instance_snat_ip(
&self,
opctx: &OpContext,
Expand Down Expand Up @@ -101,6 +101,20 @@ impl DataStore {
self.allocate_instance_external_ip(opctx, data).await
}

/// Allocates an IP address for internal service usage.
pub async fn allocate_service_ip(
&self,
opctx: &OpContext,
ip_id: Uuid,
rack_id: Uuid,
) -> CreateResult<InstanceExternalIp> {
let (.., pool) =
self.ip_pools_lookup_by_rack_id(opctx, rack_id).await?;

let data = IncompleteInstanceExternalIp::for_service(ip_id, pool.id());
self.allocate_instance_external_ip(opctx, data).await
}

async fn allocate_instance_external_ip(
&self,
opctx: &OpContext,
Expand Down
6 changes: 3 additions & 3 deletions nexus/src/db/datastore/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1034,7 +1034,7 @@ mod test {
time_deleted: None,
ip_pool_id: Uuid::new_v4(),
ip_pool_range_id: Uuid::new_v4(),
project_id: Uuid::new_v4(),
project_id: Some(Uuid::new_v4()),
instance_id: Some(instance_id),
kind: IpKind::Ephemeral,
ip: ipnetwork::IpNetwork::from(IpAddr::from(Ipv4Addr::new(
Expand Down Expand Up @@ -1094,7 +1094,7 @@ mod test {
time_deleted: None,
ip_pool_id: Uuid::new_v4(),
ip_pool_range_id: Uuid::new_v4(),
project_id: Uuid::new_v4(),
project_id: Some(Uuid::new_v4()),
instance_id: Some(Uuid::new_v4()),
kind: IpKind::SNat,
ip: ipnetwork::IpNetwork::from(IpAddr::from(Ipv4Addr::new(
Expand Down Expand Up @@ -1169,7 +1169,7 @@ mod test {
time_deleted: None,
ip_pool_id: Uuid::new_v4(),
ip_pool_range_id: Uuid::new_v4(),
project_id: Uuid::new_v4(),
project_id: Some(Uuid::new_v4()),
instance_id: Some(Uuid::new_v4()),
kind: IpKind::Floating,
ip: addresses.next().unwrap().into(),
Expand Down
Loading