@@ -27,6 +27,7 @@ use crate::db::queries::ip_pool::FilterOverlappingIpRanges;
27
27
use async_bb8_diesel:: AsyncRunQueryDsl ;
28
28
use chrono:: Utc ;
29
29
use diesel:: prelude:: * ;
30
+ use diesel:: result:: DatabaseErrorKind ;
30
31
use diesel:: result:: Error as DieselError ;
31
32
use ipnetwork:: IpNetwork ;
32
33
use nexus_db_errors:: ErrorHandler ;
@@ -86,6 +87,16 @@ impl ServiceIpPools {
86
87
}
87
88
}
88
89
90
+ // Constraint used to ensure we don't set a default IP Pool for the internal
91
+ // silo.
92
+ const INTERNAL_SILO_DEFAULT_CONSTRAINT : & ' static str =
93
+ "internal_silo_has_no_default_pool" ;
94
+
95
+ // Error message emitted when we attempt to set a default IP Pool for the
96
+ // internal silo.
97
+ const INTERNAL_SILO_DEFAULT_ERROR : & ' static str =
98
+ "The internal Silo cannot have a default IP Pool" ;
99
+
89
100
impl DataStore {
90
101
/// List IP Pools
91
102
pub async fn ip_pools_list (
@@ -588,22 +599,34 @@ impl DataStore {
588
599
let conn = self . pool_connection_authorized ( opctx) . await ?;
589
600
590
601
let result = diesel:: insert_into ( dsl:: ip_pool_resource)
591
- . values ( ip_pool_resource. clone ( ) )
602
+ . values ( ip_pool_resource)
592
603
. get_result_async ( & * conn)
593
604
. await
594
605
. map_err ( |e| {
595
- public_error_from_diesel (
596
- e,
597
- ErrorHandler :: Conflict (
598
- ResourceType :: IpPoolResource ,
599
- & format ! (
600
- "ip_pool_id: {:?}, resource_id: {:?}, resource_type: {:?}" ,
601
- ip_pool_resource. ip_pool_id,
602
- ip_pool_resource. resource_id,
603
- ip_pool_resource. resource_type,
606
+ match e {
607
+ // Catch the check constraint ensuring the internal silo has
608
+ // no default pool
609
+ DieselError :: DatabaseError ( DatabaseErrorKind :: CheckViolation , ref info)
610
+ if info. constraint_name ( ) == Some ( INTERNAL_SILO_DEFAULT_CONSTRAINT ) =>
611
+ {
612
+ Error :: invalid_request ( INTERNAL_SILO_DEFAULT_ERROR )
613
+ }
614
+ DieselError :: DatabaseError ( DatabaseErrorKind :: UniqueViolation , _) => {
615
+ public_error_from_diesel (
616
+ e,
617
+ ErrorHandler :: Conflict (
618
+ ResourceType :: IpPoolResource ,
619
+ & format ! (
620
+ "ip_pool_id: {}, resource_id: {}, resource_type: {:?}" ,
621
+ ip_pool_resource. ip_pool_id,
622
+ ip_pool_resource. resource_id,
623
+ ip_pool_resource. resource_type,
624
+ ) ,
625
+ )
604
626
)
605
- ) ,
606
- )
627
+ }
628
+ _ => public_error_from_diesel ( e, ErrorHandler :: Server ) ,
629
+ }
607
630
} ) ?;
608
631
609
632
if ip_pool_resource. is_default {
@@ -885,21 +908,47 @@ impl DataStore {
885
908
IpPoolResourceUpdateError :: FailedToUnsetDefault ( err) ,
886
909
) ) => public_error_from_diesel ( err, ErrorHandler :: Server ) ,
887
910
Some ( TxnError :: Database ( err) ) => {
888
- public_error_from_diesel ( err, ErrorHandler :: Server )
911
+ match err {
912
+ // Catch the check constraint ensuring the internal silo has
913
+ // no default pool
914
+ DieselError :: DatabaseError (
915
+ DatabaseErrorKind :: CheckViolation ,
916
+ ref info,
917
+ ) if info. constraint_name ( )
918
+ == Some ( INTERNAL_SILO_DEFAULT_CONSTRAINT ) =>
919
+ {
920
+ Error :: invalid_request ( INTERNAL_SILO_DEFAULT_ERROR )
921
+ }
922
+ _ => {
923
+ public_error_from_diesel ( err, ErrorHandler :: Server )
924
+ }
925
+ }
889
926
}
890
927
None => {
891
- public_error_from_diesel (
892
- e,
893
- ErrorHandler :: NotFoundByLookup (
894
- ResourceType :: IpPoolResource ,
895
- // TODO: would be nice to put the actual names and/or ids in
896
- // here but LookupType on each of the two silos doesn't have
897
- // a nice to_string yet or a way of composing them
898
- LookupType :: ByCompositeId (
899
- "(pool, silo)" . to_string ( ) ,
928
+ match e {
929
+ // Catch the check constraint ensuring the internal silo has
930
+ // no default pool
931
+ DieselError :: DatabaseError (
932
+ DatabaseErrorKind :: CheckViolation ,
933
+ ref info,
934
+ ) if info. constraint_name ( )
935
+ == Some ( INTERNAL_SILO_DEFAULT_CONSTRAINT ) =>
936
+ {
937
+ Error :: invalid_request ( INTERNAL_SILO_DEFAULT_ERROR )
938
+ }
939
+ _ => public_error_from_diesel (
940
+ e,
941
+ ErrorHandler :: NotFoundByLookup (
942
+ ResourceType :: IpPoolResource ,
943
+ // TODO: would be nice to put the actual names and/or ids in
944
+ // here but LookupType on each of the two silos doesn't have
945
+ // a nice to_string yet or a way of composing them
946
+ LookupType :: ByCompositeId (
947
+ "(pool, silo)" . to_string ( ) ,
948
+ ) ,
900
949
) ,
901
950
) ,
902
- )
951
+ }
903
952
}
904
953
} )
905
954
}
@@ -1269,12 +1318,13 @@ mod test {
1269
1318
use std:: num:: NonZeroU32 ;
1270
1319
1271
1320
use crate :: authz;
1321
+ use crate :: db:: datastore:: ip_pool:: INTERNAL_SILO_DEFAULT_ERROR ;
1272
1322
use crate :: db:: model:: {
1273
1323
IpPool , IpPoolResource , IpPoolResourceType , Project ,
1274
1324
} ;
1275
1325
use crate :: db:: pub_test_utils:: TestDatabase ;
1276
1326
use assert_matches:: assert_matches;
1277
- use nexus_db_model:: IpVersion ;
1327
+ use nexus_db_model:: { IpPoolIdentity , IpVersion } ;
1278
1328
use nexus_types:: external_api:: params;
1279
1329
use nexus_types:: identity:: Resource ;
1280
1330
use omicron_common:: address:: { IpRange , Ipv4Range , Ipv6Range } ;
@@ -1283,6 +1333,7 @@ mod test {
1283
1333
DataPageParams , Error , IdentityMetadataCreateParams , LookupType ,
1284
1334
} ;
1285
1335
use omicron_test_utils:: dev;
1336
+ use uuid:: Uuid ;
1286
1337
1287
1338
#[ tokio:: test]
1288
1339
async fn test_default_ip_pools ( ) {
@@ -1360,7 +1411,7 @@ mod test {
1360
1411
is_default : false ,
1361
1412
} ;
1362
1413
datastore
1363
- . ip_pool_link_silo ( & opctx, link_body. clone ( ) )
1414
+ . ip_pool_link_silo ( & opctx, link_body)
1364
1415
. await
1365
1416
. expect ( "Failed to associate IP pool with silo" ) ;
1366
1417
@@ -1489,9 +1540,6 @@ mod test {
1489
1540
assert_eq ! ( is_internal, Ok ( false ) ) ;
1490
1541
1491
1542
// now link it to the current silo, and it is still not internal.
1492
- //
1493
- // We're only making the IPv4 pool the default right now. See
1494
- // https://github.com/oxidecomputer/omicron/issues/8884 for more.
1495
1543
let silo_id = opctx. authn . silo_required ( ) . unwrap ( ) . id ( ) ;
1496
1544
let is_default = matches ! ( version, IpVersion :: V4 ) ;
1497
1545
let link = IpPoolResource {
@@ -1514,6 +1562,87 @@ mod test {
1514
1562
logctx. cleanup_successful ( ) ;
1515
1563
}
1516
1564
1565
+ #[ tokio:: test]
1566
+ async fn cannot_set_default_ip_pool_for_internal_silo ( ) {
1567
+ let logctx =
1568
+ dev:: test_setup_log ( "cannot_set_default_ip_pool_for_internal_silo" ) ;
1569
+ let db = TestDatabase :: new_with_datastore ( & logctx. log ) . await ;
1570
+ let ( opctx, datastore) = ( db. opctx ( ) , db. datastore ( ) ) ;
1571
+
1572
+ for ip_version in [ IpVersion :: V4 , IpVersion :: V6 ] {
1573
+ // Make some new pool.
1574
+ let params = IpPool {
1575
+ identity : IpPoolIdentity :: new (
1576
+ Uuid :: new_v4 ( ) ,
1577
+ IdentityMetadataCreateParams {
1578
+ name : format ! ( "test-pool-{}" , ip_version)
1579
+ . parse ( )
1580
+ . unwrap ( ) ,
1581
+ description : String :: new ( ) ,
1582
+ } ,
1583
+ ) ,
1584
+ ip_version,
1585
+ rcgen : 0 ,
1586
+ } ;
1587
+ let pool = datastore
1588
+ . ip_pool_create ( & opctx, params)
1589
+ . await
1590
+ . expect ( "Should be able to create pool" ) ;
1591
+ assert_eq ! ( pool. ip_version, ip_version) ;
1592
+ let authz_pool =
1593
+ nexus_db_lookup:: LookupPath :: new ( & opctx, datastore)
1594
+ . ip_pool_id ( pool. id ( ) )
1595
+ . lookup_for ( authz:: Action :: Read )
1596
+ . await
1597
+ . expect ( "Should be able to lookup new IP Pool" )
1598
+ . 0 ;
1599
+
1600
+ // Try to link it as the default.
1601
+ let ( authz_silo, ..) =
1602
+ nexus_db_lookup:: LookupPath :: new ( & opctx, datastore)
1603
+ . silo_id ( nexus_types:: silo:: INTERNAL_SILO_ID )
1604
+ . lookup_for ( authz:: Action :: Read )
1605
+ . await
1606
+ . expect ( "Should be able to lookup internal silo" ) ;
1607
+ let link = IpPoolResource {
1608
+ ip_pool_id : authz_pool. id ( ) ,
1609
+ resource_type : IpPoolResourceType :: Silo ,
1610
+ resource_id : authz_silo. id ( ) ,
1611
+ is_default : true ,
1612
+ } ;
1613
+ let Err ( e) = datastore. ip_pool_link_silo ( opctx, link) . await else {
1614
+ panic ! (
1615
+ "should have failed to link IP Pool to internal silo as a default"
1616
+ ) ;
1617
+ } ;
1618
+ let Error :: InvalidRequest { message } = & e else {
1619
+ panic ! ( "should have received an invalid request, got: {:?}" , e) ;
1620
+ } ;
1621
+ assert_eq ! ( message. external_message( ) , INTERNAL_SILO_DEFAULT_ERROR ) ;
1622
+
1623
+ // We can link it if it's not the default.
1624
+ let link = IpPoolResource { is_default : false , ..link } ;
1625
+ datastore. ip_pool_link_silo ( opctx, link) . await . expect (
1626
+ "Should be able to link non-default pool to internal silo" ,
1627
+ ) ;
1628
+
1629
+ // Try to set it to the default, and ensure that this also fails.
1630
+ let Err ( e) = datastore
1631
+ . ip_pool_set_default ( opctx, & authz_pool, & authz_silo, true )
1632
+ . await
1633
+ else {
1634
+ panic ! ( "should have failed to set internal pool to default" ) ;
1635
+ } ;
1636
+ let Error :: InvalidRequest { message } = & e else {
1637
+ panic ! ( "should have received an invalid request, got: {:?}" , e) ;
1638
+ } ;
1639
+ assert_eq ! ( message. external_message( ) , INTERNAL_SILO_DEFAULT_ERROR ) ;
1640
+ }
1641
+
1642
+ db. terminate ( ) . await ;
1643
+ logctx. cleanup_successful ( ) ;
1644
+ }
1645
+
1517
1646
// We're breaking out the utilization tests for IPv4 and IPv6 pools, since
1518
1647
// pools only contain one version now.
1519
1648
//
0 commit comments