Skip to content

Commit 92669b0

Browse files
authored
Search all ranges in a pool for the next external IP (#9343)
- Add a where-clause to ensure we skip IP Pool Ranges which have been exhausted when searching for the next external IP address. - Add regressions - Fixes #9342
1 parent bd1f13b commit 92669b0

File tree

2 files changed

+168
-15
lines changed

2 files changed

+168
-15
lines changed

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

Lines changed: 166 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -458,7 +458,12 @@ impl NextExternalIp {
458458
UNION ALL ",
459459
);
460460
self.push_automatic_full_ip_subquery_body(out.reborrow())?;
461-
out.push_sql(") AS all_candidates ORDER BY candidate_ip LIMIT 1 ");
461+
out.push_sql(
462+
") AS all_candidates \
463+
WHERE candidate_ip IS NOT NULL \
464+
ORDER BY candidate_ip \
465+
LIMIT 1 ",
466+
);
462467
Ok(())
463468
}
464469

@@ -857,7 +862,7 @@ mod tests {
857862
name: &str,
858863
range: IpRange,
859864
is_default: bool,
860-
) -> authz::IpPool {
865+
) -> (authz::IpPool, IpPool) {
861866
let pool = IpPool::new(
862867
&IdentityMetadataCreateParams {
863868
name: name.parse().unwrap(),
@@ -867,7 +872,8 @@ mod tests {
867872
IpPoolReservationType::ExternalSilos,
868873
);
869874

870-
self.db
875+
let db_pool = self
876+
.db
871877
.datastore()
872878
.ip_pool_create(self.db.opctx(), pool.clone())
873879
.await
@@ -888,12 +894,14 @@ mod tests {
888894

889895
self.initialize_ip_pool(name, range).await;
890896

891-
LookupPath::new(self.db.opctx(), self.db.datastore())
892-
.ip_pool_id(pool.id())
893-
.lookup_for(authz::Action::Read)
894-
.await
895-
.unwrap()
896-
.0
897+
let authz_pool =
898+
LookupPath::new(self.db.opctx(), self.db.datastore())
899+
.ip_pool_id(pool.id())
900+
.lookup_for(authz::Action::Read)
901+
.await
902+
.unwrap()
903+
.0;
904+
(authz_pool, db_pool)
897905
}
898906

899907
async fn initialize_ip_pool(&self, name: &str, range: IpRange) {
@@ -1122,7 +1130,7 @@ mod tests {
11221130
res.unwrap_err(),
11231131
Error::insufficient_capacity(
11241132
"No external IP addresses available",
1125-
"NextExternalIp::new tried to insert NULL ip",
1133+
"NextExternalIp::new returned NotFound",
11261134
),
11271135
);
11281136
context.success().await;
@@ -1603,7 +1611,7 @@ mod tests {
16031611
Ipv4Addr::new(10, 0, 0, 6),
16041612
))
16051613
.unwrap();
1606-
let p1 = context.create_ip_pool("p1", second_range, false).await;
1614+
let (p1, ..) = context.create_ip_pool("p1", second_range, false).await;
16071615

16081616
// Allocating an address on an instance in the second pool should be
16091617
// respected, even though there are IPs available in the first.
@@ -1648,7 +1656,7 @@ mod tests {
16481656
let last_address = Ipv4Addr::new(10, 0, 0, 6);
16491657
let second_range =
16501658
IpRange::try_from((first_address, last_address)).unwrap();
1651-
let p1 = context.create_ip_pool("p1", second_range, false).await;
1659+
let (p1, ..) = context.create_ip_pool("p1", second_range, false).await;
16521660

16531661
// Allocate all available addresses in the second pool.
16541662
let first_octet = first_address.octets()[3];
@@ -1810,7 +1818,7 @@ mod tests {
18101818
let first_address = Ipv4Addr::new(10, 0, 0, 1);
18111819
let last_address = Ipv4Addr::new(10, 0, 0, 3);
18121820
let range = IpRange::try_from((first_address, last_address)).unwrap();
1813-
let p1 = context.create_ip_pool("default", range, true).await;
1821+
let (p1, ..) = context.create_ip_pool("default", range, true).await;
18141822

18151823
let mut ips = Vec::with_capacity(range.len() as _);
18161824
let mut instance_id = None;
@@ -1870,7 +1878,7 @@ mod tests {
18701878
let first_address = Ipv4Addr::new(10, 0, 0, 1);
18711879
let last_address = Ipv4Addr::new(10, 0, 0, 3);
18721880
let range = IpRange::try_from((first_address, last_address)).unwrap();
1873-
let p1 = context.create_ip_pool("default", range, true).await;
1881+
let (p1, ..) = context.create_ip_pool("default", range, true).await;
18741882

18751883
let mut ips = Vec::with_capacity(range.len() as usize * 4);
18761884
let mut instance_id = None;
@@ -1937,7 +1945,7 @@ mod tests {
19371945
"fd00::ffff".parse::<Ipv6Addr>().unwrap(),
19381946
))
19391947
.unwrap();
1940-
let pool = context.create_ip_pool("default", range, true).await;
1948+
let (pool, ..) = context.create_ip_pool("default", range, true).await;
19411949

19421950
let start = std::time::Instant::now();
19431951
for (i, expected_addr) in range.iter().enumerate() {
@@ -2076,4 +2084,147 @@ mod tests {
20762084

20772085
context.success().await;
20782086
}
2087+
2088+
#[tokio::test]
2089+
async fn can_allocate_ephemeral_ips_from_all_ranges_in_a_pool() {
2090+
let context = TestContext::new(
2091+
"can_allocate_ephemeral_ips_from_all_ranges_in_a_pool",
2092+
)
2093+
.await;
2094+
2095+
// Create two ranges in the same pool. Each range will have one address
2096+
// for simplicity.
2097+
let addrs = [Ipv4Addr::new(10, 0, 0, 1), Ipv4Addr::new(10, 0, 0, 2)];
2098+
let range1 = IpRange::try_from((addrs[0], addrs[0])).unwrap();
2099+
let range2 = IpRange::try_from((addrs[1], addrs[1])).unwrap();
2100+
let (authz_pool, db_pool) =
2101+
context.create_ip_pool("default", range1, true).await;
2102+
let _ = context
2103+
.db
2104+
.datastore()
2105+
.ip_pool_add_range(
2106+
context.db.opctx(),
2107+
&authz_pool,
2108+
&db_pool,
2109+
&range2,
2110+
)
2111+
.await
2112+
.expect("able to add a second range to the pool");
2113+
2114+
// Allocate an instance and address, which should take the first address
2115+
// (which is the whole range).
2116+
let iid = context.create_instance("inst1").await;
2117+
let ip = context
2118+
.db
2119+
.datastore()
2120+
.allocate_instance_ephemeral_ip(
2121+
context.db.opctx(),
2122+
Uuid::new_v4(),
2123+
iid,
2124+
Some(authz_pool.clone()),
2125+
true,
2126+
)
2127+
.await
2128+
.expect("Failed to allocate instance ephemeral IP address")
2129+
.0;
2130+
if let IpAddr::V4(addr) = ip.ip.ip() {
2131+
assert_eq!(addr, addrs[0]);
2132+
} else {
2133+
panic!("Expected an IPv4 address");
2134+
}
2135+
2136+
// Allocate another one, which should take the second address, which is
2137+
// "all" of the second range.
2138+
let iid = context.create_instance("inst2").await;
2139+
let ip = context
2140+
.db
2141+
.datastore()
2142+
.allocate_instance_ephemeral_ip(
2143+
context.db.opctx(),
2144+
Uuid::new_v4(),
2145+
iid,
2146+
Some(authz_pool.clone()),
2147+
true,
2148+
)
2149+
.await
2150+
.expect("Failed to allocate instance ephemeral IP address")
2151+
.0;
2152+
if let IpAddr::V4(addr) = ip.ip.ip() {
2153+
assert_eq!(addr, addrs[1]);
2154+
} else {
2155+
panic!("Expected an IPv4 address");
2156+
}
2157+
2158+
context.success().await;
2159+
}
2160+
2161+
#[tokio::test]
2162+
async fn can_allocate_snat_ips_from_all_ranges_in_a_pool() {
2163+
let context =
2164+
TestContext::new("can_allocate_snat_ips_from_all_ranges_in_a_pool")
2165+
.await;
2166+
2167+
// Create two ranges in the same pool. Each range will have one address
2168+
// for simplicity.
2169+
let addrs = [Ipv4Addr::new(10, 0, 0, 1), Ipv4Addr::new(10, 0, 0, 2)];
2170+
let range1 = IpRange::try_from((addrs[0], addrs[0])).unwrap();
2171+
let range2 = IpRange::try_from((addrs[1], addrs[1])).unwrap();
2172+
let (authz_pool, db_pool) =
2173+
context.create_ip_pool("default", range1, true).await;
2174+
let _ = context
2175+
.db
2176+
.datastore()
2177+
.ip_pool_add_range(
2178+
context.db.opctx(),
2179+
&authz_pool,
2180+
&db_pool,
2181+
&range2,
2182+
)
2183+
.await
2184+
.expect("able to add a second range to the pool");
2185+
2186+
// Allocate 4 instances, to take the whole address that constitutes the
2187+
// first range.
2188+
for i in 0..4 {
2189+
let iid = context.create_instance(&format!("inst{i}")).await;
2190+
let ip = context
2191+
.db
2192+
.datastore()
2193+
.allocate_instance_snat_ip(
2194+
context.db.opctx(),
2195+
Uuid::new_v4(),
2196+
iid,
2197+
db_pool.id(),
2198+
)
2199+
.await
2200+
.expect("Failed to allocate instance SNAT IP address");
2201+
if let IpAddr::V4(addr) = ip.ip.ip() {
2202+
assert_eq!(addr, addrs[0]);
2203+
} else {
2204+
panic!("Expected an IPv4 address");
2205+
}
2206+
}
2207+
2208+
// Allocate another one, which should take the second address, the first
2209+
// port block in the second range.
2210+
let iid = context.create_instance("last").await;
2211+
let ip = context
2212+
.db
2213+
.datastore()
2214+
.allocate_instance_snat_ip(
2215+
context.db.opctx(),
2216+
Uuid::new_v4(),
2217+
iid,
2218+
db_pool.id(),
2219+
)
2220+
.await
2221+
.expect("Failed to allocate instance SNAT IP address");
2222+
if let IpAddr::V4(addr) = ip.ip.ip() {
2223+
assert_eq!(addr, addrs[1]);
2224+
} else {
2225+
panic!("Expected an IPv4 address");
2226+
}
2227+
2228+
context.success().await;
2229+
}
20792230
}

nexus/db-queries/tests/output/next_automatic_floating_ip.sql

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,8 @@ WITH
9999
r.ip_pool_id = $19 AND r.time_deleted IS NULL
100100
)
101101
AS all_candidates
102+
WHERE
103+
candidate_ip IS NOT NULL
102104
ORDER BY
103105
candidate_ip
104106
LIMIT

0 commit comments

Comments
 (0)