Skip to content

Commit e673367

Browse files
authored
Fix ipv6 nic creation when only ipv4 addresses exist. (#9880)
Fix a bug in nic creation after adding ipv6 support. Previously, the `ip` column was `NOT NULL`, but ipv6 support made both the existing `ip` and new `ipv6` columns nullable. This revealed a bug in the next item helper, which didn't expect the item column to be null. As a result, creating an ipv6 address in a subnet that only previously had ipv4 addresses would silently fail: for example, creating a dual-stack instance in a subnet with only ipv4 addresses actually produced an ipv4-only instance. This patch fixes the bug by checking that the item column is not null, and adds a regression test. cc @lgfa29. This fixes at least one category of terraform test flake, but there may be other related flakes as well.
1 parent f7a9801 commit e673367

2 files changed

Lines changed: 81 additions & 0 deletions

File tree

nexus/db-queries/src/db/queries/network_interface.rs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3515,4 +3515,82 @@ mod tests {
35153515
}
35163516
context.success().await;
35173517
}
3518+
3519+
// Regression: ignore rows where item column is null. After adding
3520+
// ipv6 support, the `ip` and `ipv6` columns became nullable: only
3521+
// one should be set. But the logic for choosing the next ipv6
3522+
// address didn't expect nulls on the item column: the min_item
3523+
// query sees that the subnet isn't empty (because ipv4 addresses
3524+
// already exist), and the gap queries don't produce candidate
3525+
// addresses because the existing rows have null ipv6 addresses.
3526+
// So creating a dual-stack instance using a subnet with only ipv4
3527+
// addresses actually produced an ipv4-only instance. This
3528+
// regression test creates an instance with an ipv4 instance in
3529+
// an empty subnet, then a dual-stack instance, then asserts that
3530+
// the dual-stack instance has both addresses.
3531+
#[tokio::test]
3532+
async fn test_dual_stack_after_ipv4_only() {
3533+
let context =
3534+
TestContext::new("test_dual_stack_after_ipv4_only", 2).await;
3535+
let subnet = &context.net1.subnets[0];
3536+
3537+
// Create an instance with an IPv4-only NIC using the empty subnet.
3538+
let instance1 = context.create_stopped_instance().await;
3539+
let instance1_id = InstanceUuid::from_untyped_uuid(instance1.id());
3540+
let nic1 = IncompleteNetworkInterface::new_instance(
3541+
Uuid::new_v4(),
3542+
instance1_id,
3543+
subnet.clone(),
3544+
IdentityMetadataCreateParams {
3545+
name: "nic-v4-only".parse().unwrap(),
3546+
description: String::from("IPv4-only NIC"),
3547+
},
3548+
PrivateIpStackCreate::auto_ipv4(),
3549+
)
3550+
.unwrap();
3551+
let inserted1 = context
3552+
.datastore()
3553+
.instance_create_network_interface_raw(context.opctx(), nic1)
3554+
.await
3555+
.expect("Failed to insert IPv4-only NIC");
3556+
assert!(
3557+
inserted1.ipv4.is_some(),
3558+
"IPv4-only NIC should have an IPv4 address"
3559+
);
3560+
assert!(
3561+
inserted1.ipv6.is_none(),
3562+
"IPv4-only NIC should not have an IPv6 address"
3563+
);
3564+
3565+
// Now create a second instance with a dual-stack NIC on the same
3566+
// subnet. This should succeed and allocate both IPv4 and IPv6.
3567+
let instance2 = context.create_stopped_instance().await;
3568+
let instance2_id = InstanceUuid::from_untyped_uuid(instance2.id());
3569+
let nic2 = IncompleteNetworkInterface::new_instance(
3570+
Uuid::new_v4(),
3571+
instance2_id,
3572+
subnet.clone(),
3573+
IdentityMetadataCreateParams {
3574+
name: "nic-dual-stack".parse().unwrap(),
3575+
description: String::from("dual-stack NIC"),
3576+
},
3577+
PrivateIpStackCreate::auto_dual_stack(),
3578+
)
3579+
.unwrap();
3580+
let inserted2 = context
3581+
.datastore()
3582+
.instance_create_network_interface_raw(context.opctx(), nic2)
3583+
.await
3584+
.expect("Failed to insert dual-stack NIC");
3585+
assert!(
3586+
inserted2.ipv4.is_some(),
3587+
"Dual-stack NIC should have an IPv4 address"
3588+
);
3589+
assert!(
3590+
inserted2.ipv6.is_some(),
3591+
"Dual-stack NIC should have an IPv6 address"
3592+
);
3593+
3594+
context.success().await;
3595+
}
35183596
}

nexus/db-queries/src/db/queries/next_item.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -787,6 +787,9 @@ where
787787
&self.scope_key,
788788
)?;
789789
out.push_sql(" AND ");
790+
out.push_identifier(ItemColumn::NAME)?;
791+
out.push_sql(" IS NOT NULL ");
792+
out.push_sql(" AND ");
790793
out.push_identifier(TIME_DELETED_COLUMN_IDENT)?;
791794
out.push_sql(" IS NULL LIMIT 1)");
792795
Ok(())

0 commit comments

Comments
 (0)