@@ -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 ;
@@ -587,23 +588,40 @@ impl DataStore {
587
588
588
589
let conn = self . pool_connection_authorized ( opctx) . await ?;
589
590
591
+ // We always insert and update the record on conflicts.
592
+ //
593
+ // This lets us use the database constraints for a few checks, such as
594
+ // assigning more than one default pool for a silo, and ensuring that
595
+ // there is no default at all for the internal silo.
590
596
let result = diesel:: insert_into ( dsl:: ip_pool_resource)
591
- . values ( ip_pool_resource. clone ( ) )
597
+ . values ( ip_pool_resource)
598
+ . on_conflict ( ( dsl:: ip_pool_id, dsl:: resource_id, dsl:: resource_type) )
599
+ . do_update ( )
600
+ . set ( ip_pool_resource)
592
601
. get_result_async ( & * conn)
593
602
. await
594
603
. 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,
604
+ match e {
605
+ // Specifically catch conflicts on the unique index which
606
+ // ensures at most one default IP Pool per silo.
607
+ DieselError :: DatabaseError ( DatabaseErrorKind :: UniqueViolation , ref info)
608
+ if info. constraint_name ( ) == Some ( "one_default_ip_pool_per_resource" ) =>
609
+ {
610
+ public_error_from_diesel (
611
+ e,
612
+ ErrorHandler :: Conflict (
613
+ ResourceType :: IpPoolResource ,
614
+ & format ! (
615
+ "ip_pool_id: {}, resource_id: {}, resource_type: {:?}" ,
616
+ ip_pool_resource. ip_pool_id,
617
+ ip_pool_resource. resource_id,
618
+ ip_pool_resource. resource_type,
619
+ ) ,
620
+ )
604
621
)
605
- ) ,
606
- )
622
+ }
623
+ _ => public_error_from_diesel ( e, ErrorHandler :: Server ) ,
624
+ }
607
625
} ) ?;
608
626
609
627
if ip_pool_resource. is_default {
@@ -1360,7 +1378,7 @@ mod test {
1360
1378
is_default : false ,
1361
1379
} ;
1362
1380
datastore
1363
- . ip_pool_link_silo ( & opctx, link_body. clone ( ) )
1381
+ . ip_pool_link_silo ( & opctx, link_body)
1364
1382
. await
1365
1383
. expect ( "Failed to associate IP pool with silo" ) ;
1366
1384
@@ -1378,12 +1396,12 @@ mod test {
1378
1396
assert_eq ! ( silo_pools[ 0 ] . 0 . id( ) , pool1_for_silo. id( ) ) ;
1379
1397
assert_eq ! ( silo_pools[ 0 ] . 1 . is_default, false ) ;
1380
1398
1381
- // linking an already linked silo errors due to PK conflict
1382
- let err = datastore
1399
+ // linking an already linked silo is fine
1400
+ let new = datastore
1383
1401
. ip_pool_link_silo ( & opctx, link_body)
1384
1402
. await
1385
- . expect_err ( "Creating the same link again should conflict" ) ;
1386
- assert_matches ! ( err , Error :: ObjectAlreadyExists { .. } ) ;
1403
+ . expect ( "Creating the same link again should not conflict" ) ;
1404
+ assert_eq ! ( new , link_body ) ;
1387
1405
1388
1406
// now make it default
1389
1407
datastore
@@ -1489,9 +1507,6 @@ mod test {
1489
1507
assert_eq ! ( is_internal, Ok ( false ) ) ;
1490
1508
1491
1509
// 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
1510
let silo_id = opctx. authn . silo_required ( ) . unwrap ( ) . id ( ) ;
1496
1511
let is_default = matches ! ( version, IpVersion :: V4 ) ;
1497
1512
let link = IpPoolResource {
@@ -1514,6 +1529,79 @@ mod test {
1514
1529
logctx. cleanup_successful ( ) ;
1515
1530
}
1516
1531
1532
+ #[ tokio:: test]
1533
+ async fn cannot_set_default_ip_pool_for_internal_silo ( ) {
1534
+ let logctx =
1535
+ dev:: test_setup_log ( "cannot_set_default_ip_pool_for_internal_silo" ) ;
1536
+ let db = TestDatabase :: new_with_datastore ( & logctx. log ) . await ;
1537
+ let ( opctx, datastore) = ( db. opctx ( ) , db. datastore ( ) ) ;
1538
+
1539
+ for version in [ IpVersion :: V4 , IpVersion :: V6 ] {
1540
+ // confirm internal pools appear as internal
1541
+ let ( authz_pool, pool) = datastore
1542
+ . ip_pools_service_lookup ( & opctx, version)
1543
+ . await
1544
+ . unwrap ( ) ;
1545
+ assert_eq ! ( pool. ip_version, version) ;
1546
+
1547
+ let is_internal =
1548
+ datastore. ip_pool_is_internal ( & opctx, & authz_pool) . await ;
1549
+ assert_eq ! ( is_internal, Ok ( true ) ) ;
1550
+
1551
+ // Try to link it as the default.
1552
+ let ( authz_silo, ..) =
1553
+ nexus_db_lookup:: LookupPath :: new ( & opctx, datastore)
1554
+ . silo_id ( nexus_types:: silo:: INTERNAL_SILO_ID )
1555
+ . lookup_for ( authz:: Action :: Read )
1556
+ . await
1557
+ . expect ( "Should be able to lookup internal silo" ) ;
1558
+ let link = IpPoolResource {
1559
+ ip_pool_id : authz_pool. id ( ) ,
1560
+ resource_type : IpPoolResourceType :: Silo ,
1561
+ resource_id : authz_silo. id ( ) ,
1562
+ is_default : true ,
1563
+ } ;
1564
+ let Err ( e) = datastore. ip_pool_link_silo ( opctx, link) . await else {
1565
+ panic ! (
1566
+ "should have failed to link IP Pool to internal silo as a default"
1567
+ ) ;
1568
+ } ;
1569
+ let Error :: InternalError { internal_message } = & e else {
1570
+ panic ! ( "should have received an internal error" ) ;
1571
+ } ;
1572
+ assert ! (
1573
+ internal_message. contains( "failed to satisfy CHECK constraint" ) ,
1574
+ "Expected a CHECK constraint violation, found: {}" ,
1575
+ internal_message,
1576
+ ) ;
1577
+
1578
+ // We can link it if it's not the default.
1579
+ let link = IpPoolResource { is_default : false , ..link } ;
1580
+ datastore. ip_pool_link_silo ( opctx, link) . await . expect (
1581
+ "Should be able to link non-default pool to internal silo" ,
1582
+ ) ;
1583
+
1584
+ // Try to set it to the default, and ensure that this also fails.
1585
+ let Err ( e) = datastore
1586
+ . ip_pool_set_default ( opctx, & authz_pool, & authz_silo, true )
1587
+ . await
1588
+ else {
1589
+ panic ! ( "should have failed to set internal pool to default" ) ;
1590
+ } ;
1591
+ let Error :: InternalError { internal_message } = & e else {
1592
+ panic ! ( "should have received an internal error" ) ;
1593
+ } ;
1594
+ assert ! (
1595
+ internal_message. contains( "failed to satisfy CHECK constraint" ) ,
1596
+ "Expected a CHECK constraint violation, found: {}" ,
1597
+ internal_message,
1598
+ ) ;
1599
+ }
1600
+
1601
+ db. terminate ( ) . await ;
1602
+ logctx. cleanup_successful ( ) ;
1603
+ }
1604
+
1517
1605
// We're breaking out the utilization tests for IPv4 and IPv6 pools, since
1518
1606
// pools only contain one version now.
1519
1607
//
0 commit comments