Skip to content

Commit afb56f3

Browse files
committed
Initial schema updates disallowing default IP Pool for the internal silo
- Add database constraints that ensure that we can't set a default pool for the Oxide internal silo. Add tests for this specifically. - This is part of #8948, but does not resolve it.
1 parent cd38f25 commit afb56f3

File tree

7 files changed

+119
-20
lines changed

7 files changed

+119
-20
lines changed

nexus/db-model/src/schema_versions.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ use std::{collections::BTreeMap, sync::LazyLock};
1616
///
1717
/// This must be updated when you change the database schema. Refer to
1818
/// schema/crdb/README.adoc in the root of this repository for details.
19-
pub const SCHEMA_VERSION: Version = Version::new(186, 0, 0);
19+
pub const SCHEMA_VERSION: Version = Version::new(187, 0, 0);
2020

2121
/// List of all past database schema versions, in *reverse* order
2222
///
@@ -28,6 +28,7 @@ static KNOWN_VERSIONS: LazyLock<Vec<KnownVersion>> = LazyLock::new(|| {
2828
// | leaving the first copy as an example for the next person.
2929
// v
3030
// KnownVersion::new(next_int, "unique-dirname-with-the-sql-files"),
31+
KnownVersion::new(187, "no-default-pool-for-internal-silo"),
3132
KnownVersion::new(186, "nexus-generation"),
3233
KnownVersion::new(185, "populate-db-metadata-nexus"),
3334
KnownVersion::new(184, "store-silo-admin-group-name"),

nexus/db-queries/src/db/datastore/ip_pool.rs

Lines changed: 70 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1489,9 +1489,6 @@ mod test {
14891489
assert_eq!(is_internal, Ok(false));
14901490

14911491
// 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.
14951492
let silo_id = opctx.authn.silo_required().unwrap().id();
14961493
let is_default = matches!(version, IpVersion::V4);
14971494
let link = IpPoolResource {
@@ -1514,6 +1511,76 @@ mod test {
15141511
logctx.cleanup_successful();
15151512
}
15161513

1514+
#[tokio::test]
1515+
async fn cannot_set_default_ip_pool_for_internal_silo() {
1516+
let logctx =
1517+
dev::test_setup_log("cannot_set_default_ip_pool_for_internal_silo");
1518+
let db = TestDatabase::new_with_datastore(&logctx.log).await;
1519+
let (opctx, datastore) = (db.opctx(), db.datastore());
1520+
1521+
for version in [IpVersion::V4, IpVersion::V6] {
1522+
// confirm internal pools appear as internal
1523+
let (authz_pool, pool) = datastore
1524+
.ip_pools_service_lookup(&opctx, version)
1525+
.await
1526+
.unwrap();
1527+
assert_eq!(pool.ip_version, version);
1528+
1529+
let is_internal =
1530+
datastore.ip_pool_is_internal(&opctx, &authz_pool).await;
1531+
assert_eq!(is_internal, Ok(true));
1532+
1533+
// Try to link it as the default.
1534+
let (authz_silo, ..) =
1535+
nexus_db_lookup::LookupPath::new(&opctx, datastore)
1536+
.silo_id(nexus_types::silo::INTERNAL_SILO_ID)
1537+
.lookup_for(authz::Action::Read)
1538+
.await
1539+
.expect("Should be able to lookup internal silo");
1540+
let link = IpPoolResource {
1541+
ip_pool_id: authz_pool.id(),
1542+
resource_type: IpPoolResourceType::Silo,
1543+
resource_id: authz_silo.id(),
1544+
is_default: true,
1545+
};
1546+
let Err(e) = datastore.ip_pool_link_silo(opctx, link.clone()).await
1547+
else {
1548+
panic!(
1549+
"should have failed to link IP Pool to internal silo as a default"
1550+
);
1551+
};
1552+
let Error::InternalError { internal_message } = &e else {
1553+
panic!("should have received an internal error");
1554+
};
1555+
assert!(
1556+
internal_message.contains("failed to satisfy CHECK constraint")
1557+
);
1558+
1559+
// We can link it if it's not the default.
1560+
let link = IpPoolResource { is_default: false, ..link };
1561+
datastore.ip_pool_link_silo(opctx, link).await.expect(
1562+
"Should be able to link non-default pool to internal silo",
1563+
);
1564+
1565+
// Try to set it to the default, and ensure that this also fails.
1566+
let Err(e) = datastore
1567+
.ip_pool_set_default(opctx, &authz_pool, &authz_silo, true)
1568+
.await
1569+
else {
1570+
panic!("should have failed to set internal pool to default");
1571+
};
1572+
let Error::InternalError { internal_message } = &e else {
1573+
panic!("should have received an internal error");
1574+
};
1575+
assert!(
1576+
internal_message.contains("failed to satisfy CHECK constraint")
1577+
);
1578+
}
1579+
1580+
db.terminate().await;
1581+
logctx.cleanup_successful();
1582+
}
1583+
15171584
// We're breaking out the utilization tests for IPv4 and IPv6 pools, since
15181585
// pools only contain one version now.
15191586
//

nexus/db-queries/src/db/datastore/rack.rs

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -991,9 +991,9 @@ impl DataStore {
991991
},
992992
version,
993993
);
994-
994+
// Create the pool, and link it to the internal silo if needed. But
995+
// we cannot set a default.
995996
let internal_pool_id = internal_pool.id();
996-
997997
let internal_created = self
998998
.ip_pool_create(opctx, internal_pool)
999999
.await
@@ -1002,25 +1002,14 @@ impl DataStore {
10021002
Error::ObjectAlreadyExists { .. } => Ok(false),
10031003
_ => Err(e),
10041004
})?;
1005-
1006-
// make default for the internal silo. only need to do this if
1007-
// the create went through, i.e., if it wasn't already there
1008-
//
1009-
// TODO-completeness: We're linking both IP pools here, but only the
1010-
// IPv4 pool is set as a default. We need a way for the operator to
1011-
// control this, either at RSS or through the API. An alternative is
1012-
// to not set a default at all, even though both are linked.
1013-
//
1014-
// See https://github.com/oxidecomputer/omicron/issues/8884
10151005
if internal_created {
1016-
let is_default = matches!(version, IpVersion::V4);
10171006
self.ip_pool_link_silo(
10181007
opctx,
10191008
db::model::IpPoolResource {
10201009
ip_pool_id: internal_pool_id,
10211010
resource_type: db::model::IpPoolResourceType::Silo,
10221011
resource_id: INTERNAL_SILO_ID,
1023-
is_default,
1012+
is_default: false,
10241013
},
10251014
)
10261015
.await?;

nexus/src/app/silo.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ impl super::Nexus {
5050
let silo = self.silo_lookup(opctx, NameOrId::Id(silo.id()))?;
5151
Ok(silo)
5252
}
53+
5354
pub fn silo_lookup<'a>(
5455
&'a self,
5556
opctx: &'a OpContext,

schema/crdb/dbinit.sql

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2110,7 +2110,20 @@ CREATE TABLE IF NOT EXISTS omicron.public.ip_pool_resource (
21102110

21112111
-- resource_type is redundant because resource IDs are globally unique, but
21122112
-- logically it belongs here
2113-
PRIMARY KEY (ip_pool_id, resource_type, resource_id)
2113+
PRIMARY KEY (ip_pool_id, resource_type, resource_id),
2114+
2115+
-- Check that there are no default pools for the internal silo
2116+
CONSTRAINT internal_silo_has_no_default_pool CHECK (
2117+
-- A = linked to internal, B = (not default)
2118+
-- A -> B iff ¬A v B
2119+
-- ¬(linked to internal silo) OR (not default)
2120+
NOT (
2121+
resource_type = 'silo' AND
2122+
resource_id = '001de000-5110-4000-8000-000000000001'
2123+
)
2124+
OR NOT is_default
2125+
)
2126+
21142127
);
21152128

21162129
-- a given resource can only have one default ip pool
@@ -2123,6 +2136,7 @@ CREATE UNIQUE INDEX IF NOT EXISTS one_default_ip_pool_per_resource ON omicron.pu
21232136
CREATE INDEX IF NOT EXISTS ip_pool_resource_id ON omicron.public.ip_pool_resource (
21242137
resource_id
21252138
);
2139+
21262140
CREATE INDEX IF NOT EXISTS ip_pool_resource_ip_pool_id ON omicron.public.ip_pool_resource (
21272141
ip_pool_id
21282142
);
@@ -6602,7 +6616,7 @@ INSERT INTO omicron.public.db_metadata (
66026616
version,
66036617
target_version
66046618
) VALUES
6605-
(TRUE, NOW(), NOW(), '186.0.0', NULL)
6619+
(TRUE, NOW(), NOW(), '187.0.0', NULL)
66066620
ON CONFLICT DO NOTHING;
66076621

66086622
COMMIT;
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/*
2+
* Ensure we have no default pool for the internal silo.
3+
*/
4+
SET LOCAL disallow_full_table_scans = 'off';
5+
UPDATE omicron.public.ip_pool_resource
6+
SET is_default = FALSE
7+
WHERE
8+
resource_type = 'silo' AND
9+
resource_id = '001de000-5110-4000-8000-000000000001';
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/*
2+
* Add check constraint ensuring that, if the pool is linked
3+
* to the internal Oxide services silo, it's not marked as
4+
* a default pool.
5+
*/
6+
ALTER TABLE IF EXISTS
7+
omicron.public.ip_pool_resource
8+
ADD CONSTRAINT IF NOT EXISTS
9+
internal_silo_has_no_default_pool CHECK (
10+
-- A = linked to internal, B = (not default)
11+
-- A -> B iff ¬A v B
12+
-- ¬(linked to internal silo) OR (not default)
13+
NOT (
14+
resource_type = 'silo' AND
15+
resource_id = '001de000-5110-4000-8000-000000000001'
16+
)
17+
OR NOT is_default
18+
);

0 commit comments

Comments
 (0)