Skip to content

Commit e5704d7

Browse files
authored
Adds external IPs for instance source NAT (#1298)
* Adds external IPs for instance source NAT - Adds the `instance_external_ip` table for tracking external IP addresses and port ranges for instance source NAT, allocated out of an IP pool / range. - Adds child-generation counter and tracking to the `ip_pool_range` table, and checks for allocated addresses when deleting a range. - Adds query to insert next available address from any range, updating the parent rcgen. Adds tests for query behavior, including testing exhaustion. - Add test helper to create an IP pool and range, since all instance-creation tests now need to have a pool available. - Forwards external IP configuration to sled agent and passes to OPTE. * Review feedback - Deallocation of external IPs is idempotent - Cleanup and comments
1 parent 49bf1b0 commit e5704d7

28 files changed

+1416
-40
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.

common/src/sql/dbinit.sql

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -983,7 +983,9 @@ CREATE TABLE omicron.public.ip_pool_range (
983983
first_address INET NOT NULL,
984984
/* The range is inclusive of the last address. */
985985
last_address INET NOT NULL,
986-
ip_pool_id UUID NOT NULL
986+
ip_pool_id UUID NOT NULL,
987+
/* Tracks child resources, IP addresses allocated out of this range. */
988+
rcgen INT8 NOT NULL
987989
);
988990

989991
/*
@@ -1002,6 +1004,66 @@ CREATE UNIQUE INDEX ON omicron.public.ip_pool_range (
10021004
STORING (first_address)
10031005
WHERE time_deleted IS NULL;
10041006

1007+
/*
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.
1013+
*/
1014+
CREATE TABLE omicron.public.instance_external_ip (
1015+
id UUID PRIMARY KEY,
1016+
time_created TIMESTAMPTZ NOT NULL,
1017+
time_modified TIMESTAMPTZ NOT NULL,
1018+
time_deleted TIMESTAMPTZ,
1019+
1020+
/* FK to the `ip_pool` table. */
1021+
ip_pool_id UUID NOT NULL,
1022+
1023+
/* FK to the `ip_pool_range` table. */
1024+
ip_pool_range_id UUID NOT NULL,
1025+
1026+
/* FK to the `instance` table. */
1027+
instance_id UUID NOT NULL,
1028+
1029+
/* The actual external IP address. */
1030+
ip INET NOT NULL,
1031+
1032+
/* The first port in the allowed range, inclusive. */
1033+
first_port INT4 NOT NULL,
1034+
1035+
/* The last port in the allowed range, also inclusive. */
1036+
last_port INT4 NOT NULL
1037+
);
1038+
1039+
/*
1040+
* Index used to support quickly looking up children of the IP Pool range table,
1041+
* when checking for allocated addresses during deletion.
1042+
*/
1043+
CREATE INDEX ON omicron.public.instance_external_ip (
1044+
ip_pool_id,
1045+
ip_pool_range_id
1046+
)
1047+
WHERE time_deleted IS NULL;
1048+
1049+
/*
1050+
* Index used to enforce uniqueness of external IPs
1051+
*
1052+
* NOTE: This relies on the uniqueness constraint of IP addresses across all
1053+
* pools, _and_ on the fact that the number of ports assigned to each instance
1054+
* is fixed at compile time.
1055+
*/
1056+
CREATE UNIQUE INDEX ON omicron.public.instance_external_ip (
1057+
ip,
1058+
first_port
1059+
)
1060+
WHERE time_deleted IS NULL;
1061+
1062+
CREATE INDEX ON omicron.public.instance_external_ip (
1063+
instance_id
1064+
)
1065+
WHERE time_deleted IS NULL;
1066+
10051067
/*******************************************************************/
10061068

10071069
/*

docs/how-to-run.adoc

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,24 @@ command line interface. Note that the `jq` command is required. In addition, th
144144
oxide org create myorg
145145
oxide project create -o myorg myproj
146146

147-
2. Define a global image that will be used as initial disk contents.
147+
2. Create an IP Pool, for providing external connectivity to the instance later.
148+
We need to create an IP Pool itself, and a range of IP addresses in that pool.
149+
150+
oxide api /ip-pools --method POST --input - <<EOF
151+
{
152+
"name": "mypool",
153+
"description": "an IP pool"
154+
}
155+
EOF
156+
157+
oxide api /ip-pools/mypool/ranges/add --method POST --input - <<EOF
158+
{
159+
"first": "10.0.0.1",
160+
"last": "10.0.0.255"
161+
}
162+
EOF
163+
164+
3. Define a global image that will be used as initial disk contents.
148165

149166
a. This can be the alpine.iso image that ships with propolis:
150167

@@ -181,7 +198,7 @@ command line interface. Note that the `jq` command is required. In addition, th
181198
}
182199
EOF
183200

184-
3. Create a disk from that global image (note that disk size must be greater than or equal to image size and a 1GiB multiple!). The example below creates a disk using the image made from the alpine ISO that ships with propolis, and sets the size to the next 1GiB multiple of the original alpine source:
201+
4. Create a disk from that global image (note that disk size must be greater than or equal to image size and a 1GiB multiple!). The example below creates a disk using the image made from the alpine ISO that ships with propolis, and sets the size to the next 1GiB multiple of the original alpine source:
185202

186203
oxide api /organizations/myorg/projects/myproj/disks/ --method POST --input - <<EOF
187204
{
@@ -196,7 +213,7 @@ command line interface. Note that the `jq` command is required. In addition, th
196213
}
197214
EOF
198215

199-
4. Create an instance, attaching the alpine disk created above:
216+
5. Create an instance, attaching the alpine disk created above:
200217

201218
oxide api /organizations/myorg/projects/myproj/instances --method POST --input - <<EOF
202219
{
@@ -214,7 +231,7 @@ command line interface. Note that the `jq` command is required. In addition, th
214231
}
215232
EOF
216233

217-
5. Optionally, attach to the propolis server serial console, though the serial console is under active development and these commands are subject to change:
234+
6. Optionally, attach to the propolis server serial console, though the serial console is under active development and these commands are subject to change:
218235

219236
a. find the zone launched for the instance: `zoneadm list -c | grep oxz_propolis-server`
220237
b. get the instance uuid from the zone name. if the zone's name is `oxz_propolis-server_3b03ad43-4e9b-4f3a-866c-238d9ec4ac45`, then the uuid is `3b03ad43-4e9b-4f3a-866c-238d9ec4ac45`

nexus/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ features = [ "serde", "v4" ]
118118
[dev-dependencies]
119119
criterion = { version = "0.3", features = [ "async_tokio" ] }
120120
expectorate = "1.0.5"
121+
itertools = "0.10.3"
121122
nexus-test-utils-macros = { path = "test-utils-macros" }
122123
nexus-test-utils = { path = "test-utils" }
123124
omicron-test-utils = { path = "../test-utils" }

nexus/src/app/instance.rs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ use omicron_common::api::external::ListResultVec;
2626
use omicron_common::api::external::LookupResult;
2727
use omicron_common::api::external::UpdateResult;
2828
use omicron_common::api::internal::nexus;
29+
use sled_agent_client::types::ExternalIp;
2930
use sled_agent_client::types::InstanceRuntimeStateMigrateParams;
3031
use sled_agent_client::types::InstanceRuntimeStateRequested;
3132
use sled_agent_client::types::InstanceStateRequested;
@@ -199,7 +200,15 @@ impl super::Nexus {
199200
.fetch()
200201
.await?;
201202

202-
self.db_datastore.project_delete_instance(opctx, &authz_instance).await
203+
self.db_datastore
204+
.project_delete_instance(opctx, &authz_instance)
205+
.await?;
206+
self.db_datastore
207+
.deallocate_instance_external_ip_by_instance_id(
208+
opctx,
209+
authz_instance.id(),
210+
)
211+
.await
203212
}
204213

205214
pub async fn project_instance_migrate(
@@ -466,6 +475,12 @@ impl super::Nexus {
466475
.derive_guest_network_interface_info(&opctx, &authz_instance)
467476
.await?;
468477

478+
let external_ip = self
479+
.db_datastore
480+
.instance_lookup_external_ip(&opctx, authz_instance.id())
481+
.await
482+
.map(ExternalIp::from)?;
483+
469484
// Gather the SSH public keys of the actor make the request so
470485
// that they may be injected into the new image via cloud-init.
471486
// TODO-security: this should be replaced with a lookup based on
@@ -502,6 +517,7 @@ impl super::Nexus {
502517
db_instance.runtime().clone(),
503518
),
504519
nics,
520+
external_ip,
505521
disks: disk_reqs,
506522
cloud_init_bytes: Some(base64::encode(
507523
db_instance.generate_cidata(&public_keys)?,

nexus/src/app/sagas/instance_create.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,17 @@ fn saga_instance_create() -> SagaTemplate<SagaInstanceCreate> {
141141
new_action_noop_undo(sic_create_network_interfaces),
142142
);
143143

144+
// Grab an external IP address and port range for the guest's Internet
145+
// Gateway, allowing external connectivity.
146+
template_builder.append(
147+
"external_ip",
148+
"ExternalIp",
149+
ActionFunc::new_action(
150+
sic_allocate_external_ip,
151+
sic_allocate_external_ip_undo,
152+
),
153+
);
154+
144155
// Saga actions must be atomic - they have to fully complete or fully abort.
145156
// This is because Steno assumes that the saga actions are atomic and
146157
// therefore undo actions are *not* run for the failing node.
@@ -470,6 +481,40 @@ async fn sic_create_network_interfaces_undo(
470481
Ok(())
471482
}
472483

484+
/// Create an external IP address for the instance.
485+
async fn sic_allocate_external_ip(
486+
sagactx: ActionContext<SagaInstanceCreate>,
487+
) -> Result<Uuid, ActionError> {
488+
let osagactx = sagactx.user_data();
489+
let datastore = osagactx.datastore();
490+
let saga_params = sagactx.saga_params();
491+
let opctx =
492+
OpContext::for_saga_action(&sagactx, &saga_params.serialized_authn);
493+
let instance_id = sagactx.lookup::<Uuid>("instance_id")?;
494+
let external_ip = datastore
495+
.allocate_instance_external_ip(&opctx, instance_id)
496+
.await
497+
.map_err(ActionError::action_failed)?;
498+
Ok(external_ip.id)
499+
}
500+
501+
/// Destroy / release an external IP address allocated for the instance.
502+
async fn sic_allocate_external_ip_undo(
503+
sagactx: ActionContext<SagaInstanceCreate>,
504+
) -> Result<(), anyhow::Error> {
505+
let osagactx = sagactx.user_data();
506+
let datastore = osagactx.datastore();
507+
let saga_params = sagactx.saga_params();
508+
let opctx =
509+
OpContext::for_saga_action(&sagactx, &saga_params.serialized_authn);
510+
let ip_id = sagactx.lookup::<Uuid>("external_ip")?;
511+
datastore
512+
.deallocate_instance_external_ip(&opctx, ip_id)
513+
.await
514+
.map_err(ActionError::action_failed)?;
515+
Ok(())
516+
}
517+
473518
/// Create disks during instance creation, and return a list of disk names
474519
// TODO implement
475520
async fn sic_create_disks_for_instance(

nexus/src/app/sagas/instance_migrate.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use omicron_common::api::external::Error;
1414
use omicron_common::api::internal::nexus::InstanceRuntimeState;
1515
use serde::Deserialize;
1616
use serde::Serialize;
17+
use sled_agent_client::types::ExternalIp;
1718
use sled_agent_client::types::InstanceEnsureBody;
1819
use sled_agent_client::types::InstanceHardware;
1920
use sled_agent_client::types::InstanceMigrateParams;
@@ -162,10 +163,17 @@ async fn sim_instance_migrate(
162163
)),
163164
..old_runtime
164165
};
166+
let external_ip = osagactx
167+
.datastore()
168+
.instance_lookup_external_ip(&opctx, instance_id)
169+
.await
170+
.map_err(ActionError::action_failed)
171+
.map(ExternalIp::from)?;
165172
let instance_hardware = InstanceHardware {
166173
runtime: runtime.into(),
167174
// TODO: populate NICs
168175
nics: vec![],
176+
external_ip,
169177
// TODO: populate disks
170178
disks: vec![],
171179
// TODO: populate cloud init bytes

0 commit comments

Comments
 (0)