Skip to content
Open
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
27 changes: 20 additions & 7 deletions nexus/src/app/sagas/instance_create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -269,13 +269,16 @@ impl NexusSaga for SagaInstanceCreate {
)?;
}

// Allocate an external IP address for the default outbound connectivity
builder.append(Node::action(
"snat_ip_id",
"CreateSnatIpId",
ACTION_GENERATE_ID.as_ref(),
));
builder.append(create_snat_ip_action());
// Allocate an external IP address for the default outbound
// connectivity, if there are no explicit IP addresses.
if need_snat_ip(params) {
builder.append(Node::action(
"snat_ip_id",
"CreateSnatIpId",
ACTION_GENERATE_ID.as_ref(),
));
builder.append(create_snat_ip_action());
}

// See the comment above where we add nodes for creating NICs. We use
// the same pattern here.
Expand Down Expand Up @@ -734,7 +737,17 @@ async fn create_default_primary_network_interface(
Ok(())
}

// Return `true` if we need to allocate an SNAT IP for this instance.
//
// This is currently done only if there is no other external IP address for the
// instance.
fn need_snat_ip(params: &Params) -> bool {
params.create_params.external_ips.is_empty()
}

/// Create an external IP address for instance source NAT.
///
/// Note that we only do this if there is no other outbound connectivity.
async fn sic_allocate_instance_snat_ip(
sagactx: NexusActionContext,
) -> Result<(), ActionError> {
Expand Down
61 changes: 61 additions & 0 deletions nexus/tests/integration_tests/external_ips.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1326,6 +1326,67 @@ async fn can_list_instance_snat_ip(cptestctx: &ControlPlaneTestContext) {
assert_eq!(*last_port, NUM_SOURCE_NAT_PORTS - 1);
}

#[nexus_test]
async fn do_not_create_snat_ip_with_ephemeral(
cptestctx: &ControlPlaneTestContext,
) {
let client = &cptestctx.external_client;

let pool = create_default_ip_pool(&client).await;
let _project = create_project(client, PROJECT_NAME).await;

// Get the first address in the pool.
let range = NexusRequest::object_get(
client,
&format!("/v1/system/ip-pools/{}/ranges", pool.identity.id),
)
.authn_as(AuthnMode::PrivilegedUser)
.execute()
.await
.unwrap_or_else(|e| panic!("failed to get IP pool range: {e}"))
.parsed_body::<IpPoolRangeResultsPage>()
.unwrap_or_else(|e| panic!("failed to parse IP pool range: {e}"));
assert_eq!(range.items.len(), 1, "Should have 1 range in the pool");
let oxide_client::types::IpRange::V4(oxide_client::types::Ipv4Range {
first,
..
}) = &range.items[0].range
else {
panic!("Expected IPv4 range, found {:?}", &range.items[0]);
};
let expected_ip = IpAddr::V4(*first);

// Create a running instance with an Ephemeral IP, which means the instance
// should not have an SNAT IP as well.
let instance_name = INSTANCE_NAMES[0];
let instance =
instance_for_external_ips(client, instance_name, true, true, &[]).await;
let url = format!("/v1/instances/{}/external-ips", instance.identity.id);
let page = NexusRequest::object_get(client, &url)
.authn_as(AuthnMode::PrivilegedUser)
.execute()
.await
.unwrap_or_else(|e| {
panic!("failed to make \"get\" request to {url}: {e}")
})
.parsed_body::<ExternalIpResultsPage>()
.unwrap_or_else(|e| {
panic!("failed to make \"get\" request to {url}: {e}")
});
let ips = page.items;
assert_eq!(
ips.len(),
1,
"Instance should have been created with exactly 1 IP"
);
let oxide_client::types::ExternalIp::Ephemeral { ip, ip_pool_id } = &ips[0]
else {
panic!("Expected an SNAT external IP, found {:?}", &ips[0]);
};
assert_eq!(ip_pool_id, &pool.identity.id);
assert_eq!(ip, &expected_ip);
}

pub async fn floating_ip_get(
client: &ClientTestContext,
fip_url: &str,
Expand Down
Loading