Skip to content

Commit d023a6d

Browse files
authored
Implement ephemeral IPs (#1458)
* Implement ephemeral IPs - Updates the current external IP allocation query to handle both floating and ephemeral IPs, by assuming that the whole port range is already reserved for any existing IP address. - Add public datastore methods for creating SNAT and Ephemeral IPs, delegating to private method for the actual query running/handling - Updates sagas to include UUID generation for external IPs as separate steps, for idempotency, and to create Ephemeral IPs if they're requested. Also rework instance creation/migration sagas to select the Ephemeral IP address, if one was requested, or the SNAT if not. - Adds optional restriction of IP Pools to a project. This adds the project ID or name in a bunch of places, and updates the external IP allocation query to only consider pools which are unrestricted, or whose project ID matches the one of the instance we're allocating an IP for. This relies on a new index on the `instance_external_ip` table, which induces an undesirable sorting (by project, not IP), so we add a new sorting criterion to the query. - Adds tests, especially for the external IP table's check constraints which verify integrity of the name / description / instance ID for different kinds of addresses, and for restriction of an IP pool to a project. - Plumb the external IPs up to Nexus's public API, including instance creation and an endpoint for listing external IPs for an instance. - Adds integration tests for assignment of Ephemeral IPs and authz tests for the endpoint(s) * remove unused wrapper types around external IP model type * Review feedback - More comments and links to issues - Better handling of external IP vs SNAT IPs during instance provision/migrate - Revert bad MAC address
1 parent 7ed4f67 commit d023a6d

36 files changed

+1511
-161
lines changed

common/src/sql/dbinit.sql

+80-9
Original file line numberDiff line numberDiff line change
@@ -958,6 +958,9 @@ CREATE TABLE omicron.public.ip_pool (
958958
time_modified TIMESTAMPTZ NOT NULL,
959959
time_deleted TIMESTAMPTZ,
960960

961+
/* Optional ID of the project for which this pool is reserved. */
962+
project_id UUID,
963+
961964
/* The collection's child-resource generation number */
962965
rcgen INT8 NOT NULL
963966
);
@@ -984,6 +987,15 @@ CREATE TABLE omicron.public.ip_pool_range (
984987
/* The range is inclusive of the last address. */
985988
last_address INET NOT NULL,
986989
ip_pool_id UUID NOT NULL,
990+
/* Optional ID of the project for which this range is reserved.
991+
*
992+
* NOTE: This denormalizes the tables a bit, since the project_id is
993+
* duplicated here and in the parent `ip_pool` table. We're allowing this
994+
* for now, since it reduces the complexity of the already-bad IP allocation
995+
* query, but we may want to revisit that, and JOIN with the parent table
996+
* instead.
997+
*/
998+
project_id UUID,
987999
/* Tracks child resources, IP addresses allocated out of this range. */
9881000
rcgen INT8 NOT NULL
9891001
);
@@ -1005,14 +1017,46 @@ STORING (first_address)
10051017
WHERE time_deleted IS NULL;
10061018

10071019
/*
1008-
* External IP addresses used for instance source NAT.
1009-
*
1010-
* NOTE: This currently stores only address and port information for the
1011-
* automatic source NAT supplied for all guest instances. It does not currently
1012-
* store information about ephemeral or floating IPs.
1020+
* Index supporting allocation of IPs out of a Pool reserved for a project.
1021+
*/
1022+
CREATE INDEX ON omicron.public.ip_pool_range (
1023+
project_id
1024+
) WHERE
1025+
time_deleted IS NULL;
1026+
1027+
1028+
/* The kind of external IP address. */
1029+
CREATE TYPE omicron.public.ip_kind AS ENUM (
1030+
/* Automatic source NAT provided to all guests by default */
1031+
'snat',
1032+
1033+
/*
1034+
* An ephemeral IP is a fixed, known address whose lifetime is the same as
1035+
* the instance to which it is attached.
1036+
*/
1037+
'ephemeral',
1038+
1039+
/*
1040+
* A floating IP is an independent, named API resource. It is a fixed,
1041+
* known address that can be moved between instances. Its lifetime is not
1042+
* fixed to any instance.
1043+
*/
1044+
'floating'
1045+
);
1046+
1047+
/*
1048+
* External IP addresses used for guest instances
10131049
*/
10141050
CREATE TABLE omicron.public.instance_external_ip (
1051+
/* Identity metadata */
10151052
id UUID PRIMARY KEY,
1053+
1054+
/* Name for floating IPs. See the constraints below. */
1055+
name STRING(128),
1056+
1057+
/* Description for floating IPs. See the constraints below. */
1058+
description STRING(512),
1059+
10161060
time_created TIMESTAMPTZ NOT NULL,
10171061
time_modified TIMESTAMPTZ NOT NULL,
10181062
time_deleted TIMESTAMPTZ,
@@ -1023,8 +1067,14 @@ CREATE TABLE omicron.public.instance_external_ip (
10231067
/* FK to the `ip_pool_range` table. */
10241068
ip_pool_range_id UUID NOT NULL,
10251069

1026-
/* FK to the `instance` table. */
1027-
instance_id UUID NOT NULL,
1070+
/* FK to the `project` table. */
1071+
project_id UUID NOT NULL,
1072+
1073+
/* FK to the `instance` table. See the constraints below. */
1074+
instance_id UUID,
1075+
1076+
/* The kind of external address, e.g., ephemeral. */
1077+
kind omicron.public.ip_kind NOT NULL,
10281078

10291079
/* The actual external IP address. */
10301080
ip INET NOT NULL,
@@ -1033,7 +1083,28 @@ CREATE TABLE omicron.public.instance_external_ip (
10331083
first_port INT4 NOT NULL,
10341084

10351085
/* The last port in the allowed range, also inclusive. */
1036-
last_port INT4 NOT NULL
1086+
last_port INT4 NOT NULL,
1087+
1088+
/* The name must be non-NULL iff this is a floating IP. */
1089+
CONSTRAINT null_fip_name CHECK (
1090+
(kind != 'floating' AND name IS NULL) OR
1091+
(kind = 'floating' AND name IS NOT NULL)
1092+
),
1093+
1094+
/* The description must be non-NULL iff this is a floating IP. */
1095+
CONSTRAINT null_fip_description CHECK (
1096+
(kind != 'floating' AND description IS NULL) OR
1097+
(kind = 'floating' AND description IS NOT NULL)
1098+
),
1099+
1100+
/*
1101+
* Only nullable if this is a floating IP, which may exist not attached
1102+
* to any instance.
1103+
*/
1104+
CONSTRAINT null_non_fip_instance_id CHECK (
1105+
(kind != 'floating' AND instance_id IS NOT NULL) OR
1106+
(kind = 'floating')
1107+
)
10371108
);
10381109

10391110
/*
@@ -1062,7 +1133,7 @@ CREATE UNIQUE INDEX ON omicron.public.instance_external_ip (
10621133
CREATE INDEX ON omicron.public.instance_external_ip (
10631134
instance_id
10641135
)
1065-
WHERE time_deleted IS NULL;
1136+
WHERE instance_id IS NOT NULL AND time_deleted IS NULL;
10661137

10671138
/*******************************************************************/
10681139

nexus/src/app/external_ip.rs

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// This Source Code Form is subject to the terms of the Mozilla Public
2+
// License, v. 2.0. If a copy of the MPL was not distributed with this
3+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4+
5+
//! External IP addresses for instances
6+
7+
use crate::authz;
8+
use crate::context::OpContext;
9+
use crate::db::lookup::LookupPath;
10+
use crate::db::model::IpKind;
11+
use crate::db::model::Name;
12+
use crate::external_api::views::ExternalIp;
13+
use omicron_common::api::external::ListResultVec;
14+
15+
impl super::Nexus {
16+
pub async fn instance_list_external_ips(
17+
&self,
18+
opctx: &OpContext,
19+
organization_name: &Name,
20+
project_name: &Name,
21+
instance_name: &Name,
22+
) -> ListResultVec<ExternalIp> {
23+
let (.., authz_instance) = LookupPath::new(opctx, &self.db_datastore)
24+
.organization_name(organization_name)
25+
.project_name(project_name)
26+
.instance_name(instance_name)
27+
.lookup_for(authz::Action::Read)
28+
.await?;
29+
Ok(self
30+
.db_datastore
31+
.instance_lookup_external_ips(opctx, authz_instance.id())
32+
.await?
33+
.into_iter()
34+
.filter_map(|ip| {
35+
if ip.kind == IpKind::SNat {
36+
None
37+
} else {
38+
Some(ip.try_into().unwrap())
39+
}
40+
})
41+
.collect::<Vec<_>>())
42+
}
43+
}

nexus/src/app/instance.rs

+37-5
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use crate::context::OpContext;
1313
use crate::db;
1414
use crate::db::identity::Resource;
1515
use crate::db::lookup::LookupPath;
16+
use crate::db::model::IpKind;
1617
use crate::db::model::Name;
1718
use crate::db::queries::network_interface;
1819
use crate::external_api::params;
@@ -216,12 +217,14 @@ impl super::Nexus {
216217
self.db_datastore
217218
.project_delete_instance(opctx, &authz_instance)
218219
.await?;
220+
// Ignore the count of addresses deleted
219221
self.db_datastore
220222
.deallocate_instance_external_ip_by_instance_id(
221223
opctx,
222224
authz_instance.id(),
223225
)
224-
.await
226+
.await?;
227+
Ok(())
225228
}
226229

227230
pub async fn project_instance_migrate(
@@ -488,11 +491,40 @@ impl super::Nexus {
488491
.derive_guest_network_interface_info(&opctx, &authz_instance)
489492
.await?;
490493

491-
let external_ip = self
494+
// Collect the external IPs for the instance.
495+
// TODO-correctness: Handle Floating IPs, see
496+
// https://github.com/oxidecomputer/omicron/issues/1334
497+
let (snat_ip, external_ips): (Vec<_>, Vec<_>) = self
492498
.db_datastore
493-
.instance_lookup_external_ip(&opctx, authz_instance.id())
494-
.await
495-
.map(ExternalIp::from)?;
499+
.instance_lookup_external_ips(&opctx, authz_instance.id())
500+
.await?
501+
.into_iter()
502+
.partition(|ip| ip.kind == IpKind::SNat);
503+
504+
// Sanity checks on the number and kind of each IP address.
505+
if external_ips.len() > crate::app::MAX_EPHEMERAL_IPS_PER_INSTANCE {
506+
return Err(Error::internal_error(
507+
format!(
508+
"Expected the number of external IPs to be limited to \
509+
{}, but found {}",
510+
crate::app::MAX_EPHEMERAL_IPS_PER_INSTANCE,
511+
external_ips.len(),
512+
)
513+
.as_str(),
514+
));
515+
}
516+
if snat_ip.len() != 1 {
517+
return Err(Error::internal_error(
518+
"Expected exactly one SNAT IP address for an instance",
519+
));
520+
}
521+
522+
// For now, we take the Ephemeral IP, if it exists, or the SNAT IP if not.
523+
// TODO-correctness: Handle multiple IP addresses, see
524+
// https://github.com/oxidecomputer/omicron/issues/1467
525+
let external_ip = ExternalIp::from(
526+
external_ips.into_iter().chain(snat_ip).next().unwrap(),
527+
);
496528

497529
// Gather the SSH public keys of the actor make the request so
498530
// that they may be injected into the new image via cloud-init.

nexus/src/app/ip_pool.rs

+8-5
Original file line numberDiff line numberDiff line change
@@ -106,11 +106,14 @@ impl super::Nexus {
106106
pool_name: &Name,
107107
range: &IpRange,
108108
) -> UpdateResult<db::model::IpPoolRange> {
109-
let (.., authz_pool) = LookupPath::new(opctx, &self.db_datastore)
110-
.ip_pool_name(pool_name)
111-
.lookup_for(authz::Action::Modify)
112-
.await?;
113-
self.db_datastore.ip_pool_add_range(opctx, &authz_pool, range).await
109+
let (.., authz_pool, db_pool) =
110+
LookupPath::new(opctx, &self.db_datastore)
111+
.ip_pool_name(pool_name)
112+
.fetch_for(authz::Action::Modify)
113+
.await?;
114+
self.db_datastore
115+
.ip_pool_add_range(opctx, &authz_pool, &db_pool, range)
116+
.await
114117
}
115118

116119
pub async fn ip_pool_delete_range(

nexus/src/app/mod.rs

+4
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ use uuid::Uuid;
2424
// by resource.
2525
mod device_auth;
2626
mod disk;
27+
mod external_ip;
2728
mod iam;
2829
mod image;
2930
mod instance;
@@ -53,6 +54,9 @@ pub(crate) const MAX_DISKS_PER_INSTANCE: u32 = 8;
5354

5455
pub(crate) const MAX_NICS_PER_INSTANCE: u32 = 8;
5556

57+
// TODO-completness: Support multiple Ephemeral IPs
58+
pub(crate) const MAX_EPHEMERAL_IPS_PER_INSTANCE: usize = 1;
59+
5660
/// Manages an Oxide fleet -- the heart of the control plane
5761
pub struct Nexus {
5862
/// uuid for this nexus instance.

0 commit comments

Comments
 (0)