@@ -98,8 +98,7 @@ const MAX_PORT: i32 = u16::MAX as _;
9898/// FROM
9999/// ip_pool_range
100100/// WHERE
101- /// project_id = <project_id> OR
102- /// project_id IS NULL AND
101+ /// <pool restriction clause> AND
103102/// time_deleted IS NULL
104103/// )
105104/// CROSS JOIN
@@ -183,9 +182,29 @@ const MAX_PORT: i32 = u16::MAX as _;
183182/// (even though we send the start/stop to the database as `i32`s). We need to
184183/// cast it on the way out of the database, so that Diesel can correctly
185184/// deserialize it into an `i32`.
185+ ///
186+ /// Pool restriction
187+ /// ----------------
188+ ///
189+ /// Clients may optionally request an external address from a specific IP Pool.
190+ /// If they don't provide a pool, the query is restricted to all IP Pools
191+ /// available to their project (not reserved for a different project). In the
192+ /// first case, the restriction is:
193+ ///
194+ /// ```sql
195+ /// pool_id = <pool_id>
196+ /// ```
197+ ///
198+ /// In the latter:
199+ ///
200+ /// ```sql
201+ /// (project_id = <project_id> OR project_id IS NULL)
202+ /// ```
203+ ///
204+ /// I.e., the pool is reserved for _this instance's_ project, or not reserved at
205+ /// all.
186206#[ derive( Debug , Clone ) ]
187207pub struct NextExternalIp {
188- // TODO-completeness: Add `ip_pool_id`, and restrict search to it.
189208 ip : IncompleteInstanceExternalIp ,
190209 // Number of ports reserved per IP address. Only applicable if the IP kind
191210 // is snat.
@@ -390,7 +409,7 @@ impl NextExternalIp {
390409 // FROM
391410 // ip_pool_range
392411 // WHERE
393- // (project_id = <project_id> OR project_id IS NULL) AND
412+ // <pool_restriction> AND
394413 // time_deleted IS NULL
395414 // ```
396415 fn push_address_sequence_subquery < ' a > (
@@ -410,13 +429,21 @@ impl NextExternalIp {
410429 out. push_identifier ( dsl:: first_address:: NAME ) ?;
411430 out. push_sql ( ") AS candidate_ip FROM " ) ;
412431 IP_POOL_RANGE_FROM_CLAUSE . walk_ast ( out. reborrow ( ) ) ?;
413- out. push_sql ( " WHERE (" ) ;
414- out. push_identifier ( dsl:: project_id:: NAME ) ?;
415- out. push_sql ( " = " ) ;
416- out. push_bind_param :: < sql_types:: Uuid , Uuid > ( self . ip . project_id ( ) ) ?;
417- out. push_sql ( " OR " ) ;
418- out. push_identifier ( dsl:: project_id:: NAME ) ?;
419- out. push_sql ( " IS NULL) AND " ) ;
432+ out. push_sql ( " WHERE " ) ;
433+ if let Some ( ref pool_id) = self . ip . pool_id ( ) {
434+ out. push_identifier ( dsl:: ip_pool_id:: NAME ) ?;
435+ out. push_sql ( " = " ) ;
436+ out. push_bind_param :: < sql_types:: Uuid , Uuid > ( pool_id) ?;
437+ } else {
438+ out. push_sql ( "(" ) ;
439+ out. push_identifier ( dsl:: project_id:: NAME ) ?;
440+ out. push_sql ( " = " ) ;
441+ out. push_bind_param :: < sql_types:: Uuid , Uuid > ( self . ip . project_id ( ) ) ?;
442+ out. push_sql ( " OR " ) ;
443+ out. push_identifier ( dsl:: project_id:: NAME ) ?;
444+ out. push_sql ( " IS NULL)" ) ;
445+ }
446+ out. push_sql ( " AND " ) ;
420447 out. push_identifier ( dsl:: time_deleted:: NAME ) ?;
421448 out. push_sql ( " IS NULL" ) ;
422449 Ok ( ( ) )
@@ -581,6 +608,7 @@ mod tests {
581608 use crate :: db:: model:: IpKind ;
582609 use crate :: db:: model:: IpPool ;
583610 use crate :: db:: model:: IpPoolRange ;
611+ use crate :: db:: model:: Name ;
584612 use crate :: external_api:: shared:: IpRange ;
585613 use async_bb8_diesel:: AsyncRunQueryDsl ;
586614 use dropshot:: test_util:: LogContext ;
@@ -999,4 +1027,51 @@ mod tests {
9991027
10001028 context. success ( ) . await ;
10011029 }
1030+
1031+ #[ tokio:: test]
1032+ async fn test_next_external_ip_is_restricted_to_pools ( ) {
1033+ let context =
1034+ TestContext :: new ( "test_next_external_ip_is_restricted_to_pools" )
1035+ . await ;
1036+
1037+ // Create two pools, neither project-restricted.
1038+ let first_range = IpRange :: try_from ( (
1039+ Ipv4Addr :: new ( 10 , 0 , 0 , 1 ) ,
1040+ Ipv4Addr :: new ( 10 , 0 , 0 , 3 ) ,
1041+ ) )
1042+ . unwrap ( ) ;
1043+ context. create_ip_pool ( "p0" , first_range, None ) . await ;
1044+ let second_range = IpRange :: try_from ( (
1045+ Ipv4Addr :: new ( 10 , 0 , 0 , 4 ) ,
1046+ Ipv4Addr :: new ( 10 , 0 , 0 , 6 ) ,
1047+ ) )
1048+ . unwrap ( ) ;
1049+ context. create_ip_pool ( "p1" , second_range, None ) . await ;
1050+
1051+ // Allocating an address on an instance in the second pool should be
1052+ // respected, even though there are IPs available in the first.
1053+ let instance_id = Uuid :: new_v4 ( ) ;
1054+ let project_id = Uuid :: new_v4 ( ) ;
1055+ let id = Uuid :: new_v4 ( ) ;
1056+ let pool_name = Some ( Name ( "p1" . parse ( ) . unwrap ( ) ) ) ;
1057+
1058+ let ip = context
1059+ . db_datastore
1060+ . allocate_instance_ephemeral_ip (
1061+ & context. opctx ,
1062+ id,
1063+ project_id,
1064+ instance_id,
1065+ pool_name,
1066+ )
1067+ . await
1068+ . expect ( "Failed to allocate instance ephemeral IP address" ) ;
1069+ assert_eq ! ( ip. kind, IpKind :: Ephemeral ) ;
1070+ assert_eq ! ( ip. ip. ip( ) , second_range. first_address( ) ) ;
1071+ assert_eq ! ( ip. first_port. 0 , 0 ) ;
1072+ assert_eq ! ( ip. last_port. 0 , u16 :: MAX ) ;
1073+ assert_eq ! ( ip. project_id, project_id) ;
1074+
1075+ context. success ( ) . await ;
1076+ }
10021077}
0 commit comments