@@ -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}
0 commit comments