From 870e7e59c3d8c91d4b4a0ce5a7311e84a080f8fe Mon Sep 17 00:00:00 2001 From: "oxide-renovate[bot]" <146848827+oxide-renovate[bot]@users.noreply.github.com> Date: Thu, 14 Dec 2023 01:09:01 -0800 Subject: [PATCH 01/33] Update russh monorepo to 0.40.1 (#4686) --- Cargo.lock | 8 ++++---- end-to-end-tests/Cargo.toml | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4a1ac63b83..8bfd5110f7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6906,9 +6906,9 @@ dependencies = [ [[package]] name = "russh" -version = "0.40.0" +version = "0.40.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98bee7ebcce06bfc40a46b9d90205c6132d899bb9095c5ce9da3cdad8ec0833d" +checksum = "23955cec4c4186e8c36f42c5d4043f9fd6cab8702fd08ce1971d966b48ec832f" dependencies = [ "aes", "aes-gcm", @@ -6951,9 +6951,9 @@ dependencies = [ [[package]] name = "russh-keys" -version = "0.40.0" +version = "0.40.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b5d5a656fe1c3024d829d054cd8c0c78dc831e4b2d4b08360569c3b38f3017f" +checksum = "9d0de3cb3cbfa773b7f170b6830565fac207a0d630cc666a29f80097cc374dd8" dependencies = [ "aes", "async-trait", diff --git a/end-to-end-tests/Cargo.toml b/end-to-end-tests/Cargo.toml index 8a1f91eee8..9e38112c36 100644 --- a/end-to-end-tests/Cargo.toml +++ b/end-to-end-tests/Cargo.toml @@ -16,8 +16,8 @@ omicron-test-utils.workspace = true oxide-client.workspace = true rand.workspace = true reqwest.workspace = true -russh = "0.40.0" -russh-keys = "0.40.0" +russh = "0.40.1" +russh-keys = "0.40.1" serde.workspace = true serde_json.workspace = true tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } From 6999098a9cd8904c5f9f1b2215c0055d19959cf8 Mon Sep 17 00:00:00 2001 From: Andy Fiddaman Date: Thu, 14 Dec 2023 13:58:40 +0000 Subject: [PATCH 02/33] Update OPTE to 0.27.202 (#4693) Fix for https://github.com/oxidecomputer/opte/issues/428 --- Cargo.lock | 12 ++++++------ Cargo.toml | 4 ++-- tools/opte_version | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8bfd5110f7..e9e5c1594d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3189,7 +3189,7 @@ dependencies = [ [[package]] name = "illumos-sys-hdrs" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/opte?rev=01356ee8c5d876ce6614ea550e12114c10bcfb34#01356ee8c5d876ce6614ea550e12114c10bcfb34" +source = "git+https://github.com/oxidecomputer/opte?rev=4e6e6ab6379fa4bc40f5d0c7340b9f35c45ad6e4#4e6e6ab6379fa4bc40f5d0c7340b9f35c45ad6e4" [[package]] name = "illumos-utils" @@ -3596,7 +3596,7 @@ dependencies = [ [[package]] name = "kstat-macro" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/opte?rev=01356ee8c5d876ce6614ea550e12114c10bcfb34#01356ee8c5d876ce6614ea550e12114c10bcfb34" +source = "git+https://github.com/oxidecomputer/opte?rev=4e6e6ab6379fa4bc40f5d0c7340b9f35c45ad6e4#4e6e6ab6379fa4bc40f5d0c7340b9f35c45ad6e4" dependencies = [ "quote", "syn 2.0.32", @@ -5297,7 +5297,7 @@ dependencies = [ [[package]] name = "opte" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/opte?rev=01356ee8c5d876ce6614ea550e12114c10bcfb34#01356ee8c5d876ce6614ea550e12114c10bcfb34" +source = "git+https://github.com/oxidecomputer/opte?rev=4e6e6ab6379fa4bc40f5d0c7340b9f35c45ad6e4#4e6e6ab6379fa4bc40f5d0c7340b9f35c45ad6e4" dependencies = [ "cfg-if", "dyn-clone", @@ -5313,7 +5313,7 @@ dependencies = [ [[package]] name = "opte-api" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/opte?rev=01356ee8c5d876ce6614ea550e12114c10bcfb34#01356ee8c5d876ce6614ea550e12114c10bcfb34" +source = "git+https://github.com/oxidecomputer/opte?rev=4e6e6ab6379fa4bc40f5d0c7340b9f35c45ad6e4#4e6e6ab6379fa4bc40f5d0c7340b9f35c45ad6e4" dependencies = [ "illumos-sys-hdrs", "ipnetwork", @@ -5325,7 +5325,7 @@ dependencies = [ [[package]] name = "opte-ioctl" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/opte?rev=01356ee8c5d876ce6614ea550e12114c10bcfb34#01356ee8c5d876ce6614ea550e12114c10bcfb34" +source = "git+https://github.com/oxidecomputer/opte?rev=4e6e6ab6379fa4bc40f5d0c7340b9f35c45ad6e4#4e6e6ab6379fa4bc40f5d0c7340b9f35c45ad6e4" dependencies = [ "libc", "libnet", @@ -5399,7 +5399,7 @@ dependencies = [ [[package]] name = "oxide-vpc" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/opte?rev=01356ee8c5d876ce6614ea550e12114c10bcfb34#01356ee8c5d876ce6614ea550e12114c10bcfb34" +source = "git+https://github.com/oxidecomputer/opte?rev=4e6e6ab6379fa4bc40f5d0c7340b9f35c45ad6e4#4e6e6ab6379fa4bc40f5d0c7340b9f35c45ad6e4" dependencies = [ "illumos-sys-hdrs", "opte", diff --git a/Cargo.toml b/Cargo.toml index 7f85d9415b..841c7bb16b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -261,7 +261,7 @@ omicron-sled-agent = { path = "sled-agent" } omicron-test-utils = { path = "test-utils" } omicron-zone-package = "0.9.1" oxide-client = { path = "clients/oxide-client" } -oxide-vpc = { git = "https://github.com/oxidecomputer/opte", rev = "01356ee8c5d876ce6614ea550e12114c10bcfb34", features = [ "api", "std" ] } +oxide-vpc = { git = "https://github.com/oxidecomputer/opte", rev = "4e6e6ab6379fa4bc40f5d0c7340b9f35c45ad6e4", features = [ "api", "std" ] } once_cell = "1.19.0" openapi-lint = { git = "https://github.com/oxidecomputer/openapi-lint", branch = "main" } openapiv3 = "2.0.0" @@ -269,7 +269,7 @@ openapiv3 = "2.0.0" openssl = "0.10" openssl-sys = "0.9" openssl-probe = "0.1.5" -opte-ioctl = { git = "https://github.com/oxidecomputer/opte", rev = "01356ee8c5d876ce6614ea550e12114c10bcfb34" } +opte-ioctl = { git = "https://github.com/oxidecomputer/opte", rev = "4e6e6ab6379fa4bc40f5d0c7340b9f35c45ad6e4" } oso = "0.27" owo-colors = "3.5.0" oximeter = { path = "oximeter/oximeter" } diff --git a/tools/opte_version b/tools/opte_version index 77cae5bfa6..619a109b35 100644 --- a/tools/opte_version +++ b/tools/opte_version @@ -1 +1 @@ -0.27.201 +0.27.202 From c82d4fcbd8dc6336c8a61281485cd4ac3fb6698f Mon Sep 17 00:00:00 2001 From: "Andrew J. Stone" Date: Thu, 14 Dec 2023 13:45:07 -0500 Subject: [PATCH 03/33] [Trivial] Fix wording and comments (#4695) The comment about sled address allocation was removed because it was implemented in https://github.com/oxidecomputer/omicron/pull/4545 --- internal-dns/src/config.rs | 4 ++-- sled-agent/src/rack_setup/service.rs | 10 +++------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/internal-dns/src/config.rs b/internal-dns/src/config.rs index 86dd6e802e..92f37f6124 100644 --- a/internal-dns/src/config.rs +++ b/internal-dns/src/config.rs @@ -111,8 +111,8 @@ impl Host { /// /// `DnsConfigBuilder` provides a much simpler interface for constructing DNS /// zone data than using `DnsConfig` directly. That's because it makes a number -/// of assumptions that are true of the control plane DNS zone (all described in -/// RFD 248), but not true in general about DNS zones: +/// of assumptions that are true of the control plane DNS zones (all described +/// in RFD 248), but not true in general about DNS zones: /// /// - We assume that there are only two kinds of hosts: a "sled" (an illumos /// global zone) or a "zone" (an illumos non-global zone). (Both of these are diff --git a/sled-agent/src/rack_setup/service.rs b/sled-agent/src/rack_setup/service.rs index 8038658fb1..af81df52bb 100644 --- a/sled-agent/src/rack_setup/service.rs +++ b/sled-agent/src/rack_setup/service.rs @@ -191,11 +191,11 @@ impl RackSetupService { /// Arguments: /// - `log`: The logger. /// - `config`: The config file, which is used to setup the rack. - /// - `storage_resources`: All the disks and zpools managed by this sled + /// - `storage_manager`: A handle for interacting with the storage manager + /// task /// - `local_bootstrap_agent`: Communication channel by which we can send - /// commands to our local bootstrap-agent (e.g., to initialize sled + /// commands to our local bootstrap-agent (e.g., to start sled-agents) /// - `bootstore` - A handle to call bootstore APIs - /// agents). pub(crate) fn new( log: Logger, config: Config, @@ -1083,10 +1083,6 @@ impl ServiceInner { ) .await?; - // TODO Questions to consider: - // - What if a sled comes online *right after* this setup? How does - // it get a /64? - Ok(()) } } From b8c8658a8c5c0d9800848bc41fcc6b54e921f79b Mon Sep 17 00:00:00 2001 From: James MacMahon Date: Thu, 14 Dec 2023 14:39:28 -0500 Subject: [PATCH 04/33] Rename disks when un-deleting and faulting (#4681) When un-deleting phantom disks and setting them to faulted, use a new name that includes the disk's UUID: this ensures that even if a user created another disk with the same name in the project, the phantom disk can still be un-deleted and faulted, and eventually cleaned up. Fixes #4673 --- nexus/db-queries/src/db/datastore/disk.rs | 22 +++- nexus/tests/integration_tests/disks.rs | 141 +++++++++++++++++++++- 2 files changed, 158 insertions(+), 5 deletions(-) diff --git a/nexus/db-queries/src/db/datastore/disk.rs b/nexus/db-queries/src/db/datastore/disk.rs index 94d950f86a..2055287e62 100644 --- a/nexus/db-queries/src/db/datastore/disk.rs +++ b/nexus/db-queries/src/db/datastore/disk.rs @@ -657,7 +657,20 @@ impl DataStore { /// If the disk delete saga unwinds, then the disk should _not_ remain /// deleted: disk delete saga should be triggered again in order to fully /// complete, and the only way to do that is to un-delete the disk. Set it - /// to faulted to ensure that it won't be used. + /// to faulted to ensure that it won't be used. Use the disk's UUID as part + /// of its new name to ensure that even if a user created another disk that + /// shadows this "phantom" disk the original can still be un-deleted and + /// faulted. + /// + /// It's worth pointing out that it's possible that the user created a disk, + /// then used that disk's ID to make a new disk with the same name as this + /// function would have picked when undeleting the original disk. In the + /// event that the original disk's delete saga unwound, this would cause + /// that unwind to fail at this step, and would cause a stuck saga that + /// requires manual intervention. The fixes as part of addressing issue 3866 + /// should greatly reduce the number of disk delete sagas that unwind, but + /// this possibility does exist. To any customer reading this: please don't + /// name your disks `deleted-{another disk's id}` :) pub async fn project_undelete_disk_set_faulted_no_auth( &self, disk_id: &Uuid, @@ -667,12 +680,19 @@ impl DataStore { let faulted = api::external::DiskState::Faulted.label(); + // If only the UUID is used, you will hit "name cannot be a UUID to + // avoid ambiguity with IDs". Add a small prefix to avoid this, and use + // "deleted" to be unambigious to the user about what they should do + // with this disk. + let new_name = format!("deleted-{disk_id}"); + let result = diesel::update(dsl::disk) .filter(dsl::time_deleted.is_not_null()) .filter(dsl::id.eq(*disk_id)) .set(( dsl::time_deleted.eq(None::>), dsl::disk_state.eq(faulted), + dsl::name.eq(new_name), )) .check_if_exists::(*disk_id) .execute_and_check(&conn) diff --git a/nexus/tests/integration_tests/disks.rs b/nexus/tests/integration_tests/disks.rs index 807c054b64..a7c9c99509 100644 --- a/nexus/tests/integration_tests/disks.rs +++ b/nexus/tests/integration_tests/disks.rs @@ -1245,7 +1245,7 @@ async fn test_disk_virtual_provisioning_collection( async fn test_disk_virtual_provisioning_collection_failed_delete( cptestctx: &ControlPlaneTestContext, ) { - // Confirm that there's a panic deleting a project if a disk deletion fails + // Confirm that there's no panic deleting a project if a disk deletion fails let client = &cptestctx.external_client; let nexus = &cptestctx.server.apictx().nexus; let datastore = nexus.datastore(); @@ -1271,6 +1271,7 @@ async fn test_disk_virtual_provisioning_collection_failed_delete( }, size: disk_size, }; + NexusRequest::new( RequestBuilder::new(client, Method::POST, &disks_url) .body(Some(&disk_one)) @@ -1281,6 +1282,11 @@ async fn test_disk_virtual_provisioning_collection_failed_delete( .await .expect("unexpected failure creating 1 GiB disk"); + // Get the disk + let disk_url = format!("/v1/disks/{}?project={}", "disk-one", PROJECT_NAME); + let disk = disk_get(&client, &disk_url).await; + assert_eq!(disk.state, DiskState::Detached); + // Assert correct virtual provisioning collection numbers let virtual_provisioning_collection = datastore .virtual_provisioning_collection_get(&opctx, project_id1) @@ -1302,8 +1308,6 @@ async fn test_disk_virtual_provisioning_collection_failed_delete( .await; // Delete the disk - expect this to fail - let disk_url = format!("/v1/disks/{}?project={}", "disk-one", PROJECT_NAME); - NexusRequest::new( RequestBuilder::new(client, Method::DELETE, &disk_url) .expect_status(Some(StatusCode::INTERNAL_SERVER_ERROR)), @@ -1323,7 +1327,12 @@ async fn test_disk_virtual_provisioning_collection_failed_delete( disk_size ); - // And the disk is now faulted + // And the disk is now faulted. The name will have changed due to the + // "undelete and fault" function. + let disk_url = format!( + "/v1/disks/deleted-{}?project={}", + disk.identity.id, PROJECT_NAME + ); let disk = disk_get(&client, &disk_url).await; assert_eq!(disk.state, DiskState::Faulted); @@ -1373,6 +1382,130 @@ async fn test_disk_virtual_provisioning_collection_failed_delete( .expect("unexpected failure deleting project"); } +#[nexus_test] +async fn test_phantom_disk_rename(cptestctx: &ControlPlaneTestContext) { + // Confirm that phantom disks are renamed when they are un-deleted and + // faulted + + let client = &cptestctx.external_client; + let nexus = &cptestctx.server.apictx().nexus; + let datastore = nexus.datastore(); + + let _disk_test = DiskTest::new(&cptestctx).await; + + populate_ip_pool(&client, "default", None).await; + let _project_id1 = create_project(client, PROJECT_NAME).await.identity.id; + + // Create a 1 GB disk + let disk_size = ByteCount::from_gibibytes_u32(1); + let disks_url = get_disks_url(); + let disk_one = params::DiskCreate { + identity: IdentityMetadataCreateParams { + name: "disk-one".parse().unwrap(), + description: String::from("sells rainsticks"), + }, + disk_source: params::DiskSource::Blank { + block_size: params::BlockSize::try_from(512).unwrap(), + }, + size: disk_size, + }; + + NexusRequest::new( + RequestBuilder::new(client, Method::POST, &disks_url) + .body(Some(&disk_one)) + .expect_status(Some(StatusCode::CREATED)), + ) + .authn_as(AuthnMode::PrivilegedUser) + .execute() + .await + .expect("unexpected failure creating 1 GiB disk"); + + let disk_url = format!("/v1/disks/{}?project={}", "disk-one", PROJECT_NAME); + + // Confirm it's there + let disk = disk_get(&client, &disk_url).await; + assert_eq!(disk.state, DiskState::Detached); + + let original_disk_id = disk.identity.id; + + // Now, request disk delete + NexusRequest::new( + RequestBuilder::new(client, Method::DELETE, &disk_url) + .expect_status(Some(StatusCode::NO_CONTENT)), + ) + .authn_as(AuthnMode::PrivilegedUser) + .execute() + .await + .expect("unexpected failure deleting 1 GiB disk"); + + // It's gone! + NexusRequest::new( + RequestBuilder::new(client, Method::GET, &disk_url) + .expect_status(Some(StatusCode::NOT_FOUND)), + ) + .authn_as(AuthnMode::PrivilegedUser) + .execute() + .await + .expect("unexpected success finding 1 GiB disk"); + + // Create a new disk with the same name + NexusRequest::new( + RequestBuilder::new(client, Method::POST, &disks_url) + .body(Some(&disk_one)) + .expect_status(Some(StatusCode::CREATED)), + ) + .authn_as(AuthnMode::PrivilegedUser) + .execute() + .await + .expect("unexpected failure creating 1 GiB disk"); + + // Confirm it's there + let disk = disk_get(&client, &disk_url).await; + assert_eq!(disk.state, DiskState::Detached); + + // Confirm it's not the same disk + let new_disk_id = disk.identity.id; + assert_ne!(original_disk_id, new_disk_id); + + // Un-delete the original and set it to faulted + datastore + .project_undelete_disk_set_faulted_no_auth(&original_disk_id) + .await + .unwrap(); + + // The original disk is now faulted + let disk_url = format!( + "/v1/disks/deleted-{}?project={}", + original_disk_id, PROJECT_NAME + ); + let disk = disk_get(&client, &disk_url).await; + assert_eq!(disk.state, DiskState::Faulted); + + // Make sure original can still be deleted + NexusRequest::new( + RequestBuilder::new(client, Method::DELETE, &disk_url) + .expect_status(Some(StatusCode::NO_CONTENT)), + ) + .authn_as(AuthnMode::PrivilegedUser) + .execute() + .await + .expect("unexpected failure deleting 1 GiB disk"); + + // Make sure new can be deleted too + let disk_url = format!("/v1/disks/{}?project={}", "disk-one", PROJECT_NAME); + let disk = disk_get(&client, &disk_url).await; + assert_eq!(disk.state, DiskState::Detached); + + NexusRequest::new( + RequestBuilder::new(client, Method::DELETE, &disk_url) + .expect_status(Some(StatusCode::NO_CONTENT)), + ) + .authn_as(AuthnMode::PrivilegedUser) + .execute() + .await + .expect("unexpected failure deleting 1 GiB disk"); +} + // Test disk size accounting #[nexus_test] async fn test_disk_size_accounting(cptestctx: &ControlPlaneTestContext) { From d967e52bb59ae7741586ce47d0fb46c374b7d537 Mon Sep 17 00:00:00 2001 From: David Crespo Date: Thu, 14 Dec 2023 18:49:16 -0600 Subject: [PATCH 05/33] Bump web console (#4701) https://github.com/oxidecomputer/console/compare/1802c285...007cfe67 * [007cfe67](https://github.com/oxidecomputer/console/commit/007cfe67) oxidecomputer/console#1858 * [d536bd97](https://github.com/oxidecomputer/console/commit/d536bd97) oxidecomputer/console#1857 * [bfd59c0d](https://github.com/oxidecomputer/console/commit/bfd59c0d) oxidecomputer/console#1845 * [ba335d45](https://github.com/oxidecomputer/console/commit/ba335d45) oxidecomputer/console#1853 * [5556d881](https://github.com/oxidecomputer/console/commit/5556d881) update mockServiceWorker.js * [48da3e1c](https://github.com/oxidecomputer/console/commit/48da3e1c) oxidecomputer/console#1841 * [a7532d9a](https://github.com/oxidecomputer/console/commit/a7532d9a) oxidecomputer/console#1842 --- tools/console_version | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/console_version b/tools/console_version index 725bda0ee9..e62ecf2bf6 100644 --- a/tools/console_version +++ b/tools/console_version @@ -1,2 +1,2 @@ -COMMIT="1802c2859f40712017ab89e72740e39bfd59320b" -SHA2="34768a895f187a6ed263c0050c42084f3907c331b547362871c2ce330e9d08d1" +COMMIT="007cfe672aa7e7c791591be089bf2a2386d2c34f" +SHA2="a4f4264229724304ee383ba55d426acd1e5d713417cf1e77fed791c3a7162abf" From 582718839abbe61fb66968e90f93c426d2857e8c Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Thu, 14 Dec 2023 20:20:52 -0500 Subject: [PATCH 06/33] Capacity and utilization (#4696) This PR is a follow up to #4605 which adds views into capacity and utilization both at the silo and system level. API: |op|method|url| |--|--|--| |silo_utilization_list | GET | /v1/system/utilization/silos | |silo_utilization_view | GET | /v1/system/utilization/silos/{silo} | |utilization_view | GET | /v1/utilization | I'm not entirely satisfied w/ the silo utilization endpoints. They could be this instead: |op|method|url| |--|--|--| |silo_utilization_list | GET | /v1/system/silos-utilization | |silo_utilization_view | GET | /v1/system/silos/{silo}/utilization | Also take special note of the views ```rust // For the eyes of end users /// View of the current silo's resource utilization and capacity #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] pub struct Utilization { /// Accounts for resources allocated to running instances or storage allocated via disks or snapshots /// Note that CPU and memory resources associated with a stopped instances are not counted here /// whereas associated disks will still be counted pub provisioned: VirtualResourceCounts, /// The total amount of resources that can be provisioned in this silo /// Actions that would exceed this limit will fail pub capacity: VirtualResourceCounts, } // For the eyes of an operator /// View of a silo's resource utilization and capacity #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] pub struct SiloUtilization { pub silo_id: Uuid, pub silo_name: Name, /// Accounts for resources allocated by in silos like CPU or memory for running instances and storage for disks and snapshots /// Note that CPU and memory resources associated with a stopped instances are not counted here pub provisioned: VirtualResourceCounts, /// Accounts for the total amount of resources reserved for silos via their quotas pub allocated: VirtualResourceCounts, } ``` For users in the silo I use `provisioned` and `capacity` as the language. Their `capacity` is represented by the quota set by an operator. For the operator `provisioned` is the same but `allocated` is used to denote the amount of resources allotted via quotas. --- Note: I had planned to add a full system utilization endpoint to this PR but that would increase the scope. Instead will ship that API as a part of the next release. We can calculate some version of the full system utilization on the client by listing all the silos and their utilization. --------- Co-authored-by: Sean Klein --- common/src/api/external/http_pagination.rs | 11 +- common/src/api/external/mod.rs | 17 ++ nexus/db-model/src/lib.rs | 2 + nexus/db-model/src/quota.rs | 21 +- nexus/db-model/src/schema.rs | 15 +- nexus/db-model/src/utilization.rs | 56 ++++ nexus/db-queries/src/db/datastore/mod.rs | 1 + .../src/db/datastore/utilization.rs | 57 ++++ nexus/src/app/mod.rs | 1 + nexus/src/app/utilization.rs | 33 +++ nexus/src/external_api/http_entrypoints.rs | 100 ++++++- nexus/tests/integration_tests/endpoints.rs | 30 +++ nexus/tests/integration_tests/mod.rs | 1 + nexus/tests/integration_tests/quotas.rs | 6 +- nexus/tests/integration_tests/utilization.rs | 139 ++++++++++ nexus/tests/output/nexus_tags.txt | 3 + nexus/types/src/external_api/params.rs | 8 + nexus/types/src/external_api/views.rs | 58 +++- openapi/nexus.json | 254 +++++++++++++++++- schema/crdb/21.0.0/up01.sql | 17 ++ schema/crdb/dbinit.sql | 27 +- test-utils/src/dev/test_cmds.rs | 2 +- 22 files changed, 818 insertions(+), 41 deletions(-) create mode 100644 nexus/db-model/src/utilization.rs create mode 100644 nexus/db-queries/src/db/datastore/utilization.rs create mode 100644 nexus/src/app/utilization.rs create mode 100644 nexus/tests/integration_tests/utilization.rs create mode 100644 schema/crdb/21.0.0/up01.sql diff --git a/common/src/api/external/http_pagination.rs b/common/src/api/external/http_pagination.rs index 2bc78a54d6..65237f73c6 100644 --- a/common/src/api/external/http_pagination.rs +++ b/common/src/api/external/http_pagination.rs @@ -58,6 +58,8 @@ use std::fmt::Debug; use std::num::NonZeroU32; use uuid::Uuid; +use super::SimpleIdentity; + // General pagination infrastructure /// Specifies which page of results we're on @@ -147,15 +149,14 @@ pub fn marker_for_id(_: &S, t: &T) -> Uuid { /// /// This is intended for use with [`ScanByNameOrId::results_page`] with objects /// that impl [`ObjectIdentity`]. -pub fn marker_for_name_or_id( +pub fn marker_for_name_or_id( scan: &ScanByNameOrId, item: &T, ) -> NameOrId { - let identity = item.identity(); match scan.sort_by { - NameOrIdSortMode::NameAscending => identity.name.clone().into(), - NameOrIdSortMode::NameDescending => identity.name.clone().into(), - NameOrIdSortMode::IdAscending => identity.id.into(), + NameOrIdSortMode::NameAscending => item.name().clone().into(), + NameOrIdSortMode::NameDescending => item.name().clone().into(), + NameOrIdSortMode::IdAscending => item.id().into(), } } diff --git a/common/src/api/external/mod.rs b/common/src/api/external/mod.rs index 64a2e462ec..aa783ac9ca 100644 --- a/common/src/api/external/mod.rs +++ b/common/src/api/external/mod.rs @@ -71,6 +71,23 @@ pub trait ObjectIdentity { fn identity(&self) -> &IdentityMetadata; } +/// Exists for types that don't properly implement `ObjectIdentity` but +/// still need to be paginated by name or id. +pub trait SimpleIdentity { + fn id(&self) -> Uuid; + fn name(&self) -> &Name; +} + +impl SimpleIdentity for T { + fn id(&self) -> Uuid { + self.identity().id + } + + fn name(&self) -> &Name { + &self.identity().name + } +} + /// Parameters used to request a specific page of results when listing a /// collection of objects /// diff --git a/nexus/db-model/src/lib.rs b/nexus/db-model/src/lib.rs index 908f6f2368..2c3433b2d3 100644 --- a/nexus/db-model/src/lib.rs +++ b/nexus/db-model/src/lib.rs @@ -81,6 +81,7 @@ mod switch; mod unsigned; mod update_artifact; mod user_builtin; +mod utilization; mod virtual_provisioning_collection; mod virtual_provisioning_resource; mod vmm; @@ -167,6 +168,7 @@ pub use switch_port::*; pub use system_update::*; pub use update_artifact::*; pub use user_builtin::*; +pub use utilization::*; pub use virtual_provisioning_collection::*; pub use virtual_provisioning_resource::*; pub use vmm::*; diff --git a/nexus/db-model/src/quota.rs b/nexus/db-model/src/quota.rs index 70a8ffa1fd..ae88e12e66 100644 --- a/nexus/db-model/src/quota.rs +++ b/nexus/db-model/src/quota.rs @@ -65,22 +65,11 @@ impl From for views::SiloQuotas { fn from(silo_quotas: SiloQuotas) -> Self { Self { silo_id: silo_quotas.silo_id, - cpus: silo_quotas.cpus, - memory: silo_quotas.memory.into(), - storage: silo_quotas.storage.into(), - } - } -} - -impl From for SiloQuotas { - fn from(silo_quotas: views::SiloQuotas) -> Self { - Self { - silo_id: silo_quotas.silo_id, - time_created: Utc::now(), - time_modified: Utc::now(), - cpus: silo_quotas.cpus, - memory: silo_quotas.memory.into(), - storage: silo_quotas.storage.into(), + limits: views::VirtualResourceCounts { + cpus: silo_quotas.cpus, + memory: silo_quotas.memory.into(), + storage: silo_quotas.storage.into(), + }, } } } diff --git a/nexus/db-model/src/schema.rs b/nexus/db-model/src/schema.rs index 10fa8dcfac..6839af8a76 100644 --- a/nexus/db-model/src/schema.rs +++ b/nexus/db-model/src/schema.rs @@ -420,6 +420,19 @@ table! { } } +table! { + silo_utilization(silo_id) { + silo_id -> Uuid, + silo_name -> Text, + cpus_provisioned -> Int8, + memory_provisioned -> Int8, + storage_provisioned -> Int8, + cpus_allocated -> Int8, + memory_allocated -> Int8, + storage_allocated -> Int8, + } +} + table! { network_interface (id) { id -> Uuid, @@ -1333,7 +1346,7 @@ table! { /// /// This should be updated whenever the schema is changed. For more details, /// refer to: schema/crdb/README.adoc -pub const SCHEMA_VERSION: SemverVersion = SemverVersion::new(20, 0, 0); +pub const SCHEMA_VERSION: SemverVersion = SemverVersion::new(21, 0, 0); allow_tables_to_appear_in_same_query!( system_update, diff --git a/nexus/db-model/src/utilization.rs b/nexus/db-model/src/utilization.rs new file mode 100644 index 0000000000..9bef4f59c7 --- /dev/null +++ b/nexus/db-model/src/utilization.rs @@ -0,0 +1,56 @@ +use crate::ByteCount; +use crate::{schema::silo_utilization, Name}; +use nexus_types::external_api::views; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +#[derive(Queryable, Debug, Clone, Selectable, Serialize, Deserialize)] +#[diesel(table_name = silo_utilization)] +pub struct SiloUtilization { + pub silo_id: Uuid, + pub silo_name: Name, + + pub cpus_allocated: i64, + pub memory_allocated: ByteCount, + pub storage_allocated: ByteCount, + + pub cpus_provisioned: i64, + pub memory_provisioned: ByteCount, + pub storage_provisioned: ByteCount, +} + +impl From for views::SiloUtilization { + fn from(silo_utilization: SiloUtilization) -> Self { + Self { + silo_id: silo_utilization.silo_id, + silo_name: silo_utilization.silo_name.into(), + provisioned: views::VirtualResourceCounts { + cpus: silo_utilization.cpus_provisioned, + memory: silo_utilization.memory_provisioned.into(), + storage: silo_utilization.storage_provisioned.into(), + }, + allocated: views::VirtualResourceCounts { + cpus: silo_utilization.cpus_allocated, + memory: silo_utilization.memory_allocated.into(), + storage: silo_utilization.storage_allocated.into(), + }, + } + } +} + +impl From for views::Utilization { + fn from(silo_utilization: SiloUtilization) -> Self { + Self { + provisioned: views::VirtualResourceCounts { + cpus: silo_utilization.cpus_provisioned, + memory: silo_utilization.memory_provisioned.into(), + storage: silo_utilization.storage_provisioned.into(), + }, + capacity: views::VirtualResourceCounts { + cpus: silo_utilization.cpus_allocated, + memory: silo_utilization.memory_allocated.into(), + storage: silo_utilization.storage_allocated.into(), + }, + } + } +} diff --git a/nexus/db-queries/src/db/datastore/mod.rs b/nexus/db-queries/src/db/datastore/mod.rs index 1609fc7101..93486771b5 100644 --- a/nexus/db-queries/src/db/datastore/mod.rs +++ b/nexus/db-queries/src/db/datastore/mod.rs @@ -86,6 +86,7 @@ mod switch; mod switch_interface; mod switch_port; mod update; +mod utilization; mod virtual_provisioning_collection; mod vmm; mod volume; diff --git a/nexus/db-queries/src/db/datastore/utilization.rs b/nexus/db-queries/src/db/datastore/utilization.rs new file mode 100644 index 0000000000..4fbe215fe2 --- /dev/null +++ b/nexus/db-queries/src/db/datastore/utilization.rs @@ -0,0 +1,57 @@ +use super::DataStore; +use crate::authz; +use crate::context::OpContext; +use crate::db; +use crate::db::error::public_error_from_diesel; +use crate::db::error::ErrorHandler; +use crate::db::model::Name; +use crate::db::model::SiloUtilization; +use crate::db::pagination::paginated; +use async_bb8_diesel::AsyncRunQueryDsl; +use diesel::{ExpressionMethods, QueryDsl, SelectableHelper}; +use omicron_common::api::external::http_pagination::PaginatedBy; +use omicron_common::api::external::Error; +use omicron_common::api::external::ListResultVec; +use ref_cast::RefCast; + +impl DataStore { + pub async fn silo_utilization_view( + &self, + opctx: &OpContext, + authz_silo: &authz::Silo, + ) -> Result { + opctx.authorize(authz::Action::Read, authz_silo).await?; + let silo_id = authz_silo.id(); + + use db::schema::silo_utilization::dsl; + dsl::silo_utilization + .filter(dsl::silo_id.eq(silo_id)) + .select(SiloUtilization::as_select()) + .first_async(&*self.pool_connection_authorized(opctx).await?) + .await + .map_err(|e| public_error_from_diesel(e, ErrorHandler::Server)) + } + + pub async fn silo_utilization_list( + &self, + opctx: &OpContext, + pagparams: &PaginatedBy<'_>, + ) -> ListResultVec { + opctx.authorize(authz::Action::ListChildren, &authz::FLEET).await?; + use db::schema::silo_utilization::dsl; + match pagparams { + PaginatedBy::Id(pagparams) => { + paginated(dsl::silo_utilization, dsl::silo_id, pagparams) + } + PaginatedBy::Name(pagparams) => paginated( + dsl::silo_utilization, + dsl::silo_name, + &pagparams.map_name(|n| Name::ref_cast(n)), + ), + } + .select(SiloUtilization::as_select()) + .load_async(&*self.pool_connection_authorized(opctx).await?) + .await + .map_err(|e| public_error_from_diesel(e, ErrorHandler::Server)) + } +} diff --git a/nexus/src/app/mod.rs b/nexus/src/app/mod.rs index b92714a365..5af45985db 100644 --- a/nexus/src/app/mod.rs +++ b/nexus/src/app/mod.rs @@ -64,6 +64,7 @@ mod switch_interface; mod switch_port; pub mod test_interfaces; mod update; +mod utilization; mod volume; mod vpc; mod vpc_router; diff --git a/nexus/src/app/utilization.rs b/nexus/src/app/utilization.rs new file mode 100644 index 0000000000..526ebc9470 --- /dev/null +++ b/nexus/src/app/utilization.rs @@ -0,0 +1,33 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! Insights into capacity and utilization + +use nexus_db_queries::authz; +use nexus_db_queries::context::OpContext; +use nexus_db_queries::db; +use nexus_db_queries::db::lookup; +use omicron_common::api::external::http_pagination::PaginatedBy; +use omicron_common::api::external::Error; +use omicron_common::api::external::ListResultVec; + +impl super::Nexus { + pub async fn silo_utilization_view( + &self, + opctx: &OpContext, + silo_lookup: &lookup::Silo<'_>, + ) -> Result { + let (.., authz_silo) = + silo_lookup.lookup_for(authz::Action::Read).await?; + self.db_datastore.silo_utilization_view(opctx, &authz_silo).await + } + + pub async fn silo_utilization_list( + &self, + opctx: &OpContext, + pagparams: &PaginatedBy<'_>, + ) -> ListResultVec { + self.db_datastore.silo_utilization_list(opctx, pagparams).await + } +} diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index 6720f95c39..042ee294b7 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -6,11 +6,13 @@ use super::{ console_api, device_auth, params, + params::ProjectSelector, shared::UninitializedSled, views::{ self, Certificate, Group, IdentityProvider, Image, IpPool, IpPoolRange, - PhysicalDisk, Project, Rack, Role, Silo, Sled, Snapshot, SshKey, User, - UserBuiltin, Vpc, VpcRouter, VpcSubnet, + PhysicalDisk, Project, Rack, Role, Silo, SiloQuotas, SiloUtilization, + Sled, SledInstance, Snapshot, SshKey, Switch, User, UserBuiltin, Vpc, + VpcRouter, VpcSubnet, }, }; use crate::external_api::shared; @@ -38,6 +40,7 @@ use dropshot::{ use ipnetwork::IpNetwork; use nexus_db_queries::authz; use nexus_db_queries::db; +use nexus_db_queries::db::identity::AssetIdentityMetadata; use nexus_db_queries::db::identity::Resource; use nexus_db_queries::db::lookup::ImageLookup; use nexus_db_queries::db::lookup::ImageParentLookup; @@ -45,11 +48,7 @@ use nexus_db_queries::db::model::Name; use nexus_db_queries::{ authz::ApiResource, db::fixed_data::silo::INTERNAL_SILO_ID, }; -use nexus_types::external_api::{params::ProjectSelector, views::SiloQuotas}; -use nexus_types::{ - external_api::views::{SledInstance, Switch}, - identity::AssetIdentityMetadata, -}; +use nexus_types::external_api::views::Utilization; use omicron_common::api::external::http_pagination::data_page_params_for; use omicron_common::api::external::http_pagination::marker_for_name; use omicron_common::api::external::http_pagination::marker_for_name_or_id; @@ -272,6 +271,8 @@ pub(crate) fn external_api() -> NexusApiDescription { api.register(networking_bgp_announce_set_list)?; api.register(networking_bgp_announce_set_delete)?; + api.register(utilization_view)?; + // Fleet-wide API operations api.register(silo_list)?; api.register(silo_create)?; @@ -280,8 +281,10 @@ pub(crate) fn external_api() -> NexusApiDescription { api.register(silo_policy_view)?; api.register(silo_policy_update)?; - api.register(system_quotas_list)?; + api.register(silo_utilization_view)?; + api.register(silo_utilization_list)?; + api.register(system_quotas_list)?; api.register(silo_quotas_view)?; api.register(silo_quotas_update)?; @@ -515,6 +518,87 @@ async fn policy_update( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } +/// View the resource utilization of the user's current silo +#[endpoint { + method = GET, + path = "/v1/utilization", + tags = ["silos"], +}] +async fn utilization_view( + rqctx: RequestContext>, +) -> Result, HttpError> { + let apictx = rqctx.context(); + let handler = async { + let nexus = &apictx.nexus; + let opctx = crate::context::op_context_for_external_api(&rqctx).await?; + let silo_lookup = nexus.current_silo_lookup(&opctx)?; + let utilization = + nexus.silo_utilization_view(&opctx, &silo_lookup).await?; + + Ok(HttpResponseOk(utilization.into())) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + +/// View the current utilization of a given silo +#[endpoint { + method = GET, + path = "/v1/system/utilization/silos/{silo}", + tags = ["system/silos"], +}] +async fn silo_utilization_view( + rqctx: RequestContext>, + path_params: Path, +) -> Result, HttpError> { + let apictx = rqctx.context(); + let handler = async { + let nexus = &apictx.nexus; + + let opctx = crate::context::op_context_for_external_api(&rqctx).await?; + let silo_lookup = + nexus.silo_lookup(&opctx, path_params.into_inner().silo)?; + let quotas = nexus.silo_utilization_view(&opctx, &silo_lookup).await?; + + Ok(HttpResponseOk(quotas.into())) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} +/// List current utilization state for all silos +#[endpoint { + method = GET, + path = "/v1/system/utilization/silos", + tags = ["system/silos"], +}] +async fn silo_utilization_list( + rqctx: RequestContext>, + query_params: Query, +) -> Result>, HttpError> { + let apictx = rqctx.context(); + let handler = async { + let nexus = &apictx.nexus; + + let query = query_params.into_inner(); + let pagparams = data_page_params_for(&rqctx, &query)?; + let scan_params = ScanByNameOrId::from_query(&query)?; + let paginated_by = name_or_id_pagination(&pagparams, scan_params)?; + + let opctx = crate::context::op_context_for_external_api(&rqctx).await?; + let utilization = nexus + .silo_utilization_list(&opctx, &paginated_by) + .await? + .into_iter() + .map(|p| p.into()) + .collect(); + + Ok(HttpResponseOk(ScanByNameOrId::results_page( + &query, + utilization, + &marker_for_name_or_id, + )?)) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + /// Lists resource quotas for all silos #[endpoint { method = GET, diff --git a/nexus/tests/integration_tests/endpoints.rs b/nexus/tests/integration_tests/endpoints.rs index bd6df210c0..c41fcdbed9 100644 --- a/nexus/tests/integration_tests/endpoints.rs +++ b/nexus/tests/integration_tests/endpoints.rs @@ -100,6 +100,9 @@ lazy_static! { tls_certificates: vec![], mapped_fleet_roles: Default::default(), }; + + pub static ref DEMO_SILO_UTIL_URL: String = format!("/v1/system/utilization/silos/{}", *DEMO_SILO_NAME); + // Use the default Silo for testing the local IdP pub static ref DEMO_SILO_USERS_CREATE_URL: String = format!( "/v1/system/identity-providers/local/users?silo={}", @@ -121,6 +124,9 @@ lazy_static! { "/v1/system/identity-providers/local/users/{{id}}/set-password?silo={}", DEFAULT_SILO.identity().name, ); +} + +lazy_static! { // Project used for testing pub static ref DEMO_PROJECT_NAME: Name = "demo-project".parse().unwrap(); @@ -974,6 +980,30 @@ lazy_static! { AllowedMethod::Get ], }, + VerifyEndpoint { + url: "/v1/system/utilization/silos", + visibility: Visibility::Public, + unprivileged_access: UnprivilegedAccess::None, + allowed_methods: vec![ + AllowedMethod::Get + ] + }, + VerifyEndpoint { + url: &DEMO_SILO_UTIL_URL, + visibility: Visibility::Protected, + unprivileged_access: UnprivilegedAccess::None, + allowed_methods: vec![ + AllowedMethod::Get + ] + }, + VerifyEndpoint { + url: "/v1/utilization", + visibility: Visibility::Public, + unprivileged_access: UnprivilegedAccess::ReadOnly, + allowed_methods: vec![ + AllowedMethod::Get + ] + }, VerifyEndpoint { url: "/v1/policy", visibility: Visibility::Public, diff --git a/nexus/tests/integration_tests/mod.rs b/nexus/tests/integration_tests/mod.rs index 35c70bf874..6cb99b9e45 100644 --- a/nexus/tests/integration_tests/mod.rs +++ b/nexus/tests/integration_tests/mod.rs @@ -45,6 +45,7 @@ mod unauthorized; mod unauthorized_coverage; mod updates; mod users_builtin; +mod utilization; mod volume_management; mod vpc_firewall; mod vpc_routers; diff --git a/nexus/tests/integration_tests/quotas.rs b/nexus/tests/integration_tests/quotas.rs index 2fddf4e05c..0ad2419bee 100644 --- a/nexus/tests/integration_tests/quotas.rs +++ b/nexus/tests/integration_tests/quotas.rs @@ -269,9 +269,9 @@ async fn test_quotas(cptestctx: &ControlPlaneTestContext) { .expect("failed to set quotas"); let quotas = system.get_quotas(client).await; - assert_eq!(quotas.cpus, 4); - assert_eq!(quotas.memory, ByteCount::from_gibibytes_u32(15)); - assert_eq!(quotas.storage, ByteCount::from_gibibytes_u32(2)); + assert_eq!(quotas.limits.cpus, 4); + assert_eq!(quotas.limits.memory, ByteCount::from_gibibytes_u32(15)); + assert_eq!(quotas.limits.storage, ByteCount::from_gibibytes_u32(2)); // Ensure memory quota is enforced let err = system diff --git a/nexus/tests/integration_tests/utilization.rs b/nexus/tests/integration_tests/utilization.rs new file mode 100644 index 0000000000..5ebf56f35a --- /dev/null +++ b/nexus/tests/integration_tests/utilization.rs @@ -0,0 +1,139 @@ +use http::Method; +use http::StatusCode; +use nexus_test_utils::http_testing::AuthnMode; +use nexus_test_utils::http_testing::NexusRequest; +use nexus_test_utils::http_testing::RequestBuilder; +use nexus_test_utils::resource_helpers::create_instance; +use nexus_test_utils::resource_helpers::create_project; +use nexus_test_utils::resource_helpers::objects_list_page_authz; +use nexus_test_utils::resource_helpers::populate_ip_pool; +use nexus_test_utils::resource_helpers::DiskTest; +use nexus_test_utils_macros::nexus_test; +use nexus_types::external_api::params; +use nexus_types::external_api::params::SiloQuotasCreate; +use nexus_types::external_api::views::SiloUtilization; +use nexus_types::external_api::views::Utilization; +use nexus_types::external_api::views::VirtualResourceCounts; +use omicron_common::api::external::ByteCount; +use omicron_common::api::external::IdentityMetadataCreateParams; + +static PROJECT_NAME: &str = "utilization-test-project"; +static INSTANCE_NAME: &str = "utilization-test-instance"; + +type ControlPlaneTestContext = + nexus_test_utils::ControlPlaneTestContext; + +#[nexus_test] +async fn test_utilization(cptestctx: &ControlPlaneTestContext) { + let client = &cptestctx.external_client; + + populate_ip_pool(&client, "default", None).await; + + let current_util = objects_list_page_authz::( + client, + "/v1/system/utilization/silos", + ) + .await + .items; + + assert_eq!(current_util.len(), 2); + + assert_eq!(current_util[0].silo_name, "default-silo"); + assert_eq!(current_util[0].provisioned, SiloQuotasCreate::empty().into()); + assert_eq!( + current_util[0].allocated, + SiloQuotasCreate::arbitrarily_high_default().into() + ); + + assert_eq!(current_util[1].silo_name, "test-suite-silo"); + assert_eq!(current_util[1].provisioned, SiloQuotasCreate::empty().into()); + assert_eq!(current_util[1].allocated, SiloQuotasCreate::empty().into()); + + let _ = create_project(&client, &PROJECT_NAME).await; + let _ = create_instance(client, &PROJECT_NAME, &INSTANCE_NAME).await; + + // Start instance + NexusRequest::new( + RequestBuilder::new( + client, + Method::POST, + format!( + "/v1/instances/{}/start?project={}", + &INSTANCE_NAME, &PROJECT_NAME + ) + .as_str(), + ) + .body(None as Option<&serde_json::Value>) + .expect_status(Some(StatusCode::ACCEPTED)), + ) + .authn_as(AuthnMode::PrivilegedUser) + .execute() + .await + .expect("failed to start instance"); + + // get utilization for just the default silo + let silo_util = NexusRequest::object_get( + client, + "/v1/system/utilization/silos/default-silo", + ) + .authn_as(AuthnMode::PrivilegedUser) + .execute() + .await + .expect("failed to fetch silo utilization") + .parsed_body::() + .unwrap(); + + assert_eq!( + silo_util.provisioned, + VirtualResourceCounts { + cpus: 4, + memory: ByteCount::from_gibibytes_u32(1), + storage: ByteCount::from(0) + } + ); + + // Simulate space for disks + DiskTest::new(&cptestctx).await; + + // provision disk + NexusRequest::new( + RequestBuilder::new( + client, + Method::POST, + format!("/v1/disks?project={}", &PROJECT_NAME).as_str(), + ) + .body(Some(¶ms::DiskCreate { + identity: IdentityMetadataCreateParams { + name: "test-disk".parse().unwrap(), + description: "".into(), + }, + size: ByteCount::from_gibibytes_u32(2), + disk_source: params::DiskSource::Blank { + block_size: params::BlockSize::try_from(512).unwrap(), + }, + })) + .expect_status(Some(StatusCode::CREATED)), + ) + .authn_as(AuthnMode::PrivilegedUser) + .execute() + .await + .expect("disk failed to create"); + + // Get the silo but this time using the silo admin view + let silo_util = NexusRequest::object_get(client, "/v1/utilization") + .authn_as(AuthnMode::PrivilegedUser) + .execute() + .await + .expect("failed to fetch utilization for current (default) silo") + .parsed_body::() + .unwrap(); + + assert_eq!( + silo_util.provisioned, + VirtualResourceCounts { + cpus: 4, + memory: ByteCount::from_gibibytes_u32(1), + storage: ByteCount::from_gibibytes_u32(2) + } + ); +} diff --git a/nexus/tests/output/nexus_tags.txt b/nexus/tests/output/nexus_tags.txt index 3f77f4cb26..7e1dc306d5 100644 --- a/nexus/tests/output/nexus_tags.txt +++ b/nexus/tests/output/nexus_tags.txt @@ -106,6 +106,7 @@ group_view GET /v1/groups/{group_id} policy_update PUT /v1/policy policy_view GET /v1/policy user_list GET /v1/users +utilization_view GET /v1/utilization API operations found with tag "snapshots" OPERATION ID METHOD URL PATH @@ -187,6 +188,8 @@ silo_quotas_update PUT /v1/system/silos/{silo}/quotas silo_quotas_view GET /v1/system/silos/{silo}/quotas silo_user_list GET /v1/system/users silo_user_view GET /v1/system/users/{user_id} +silo_utilization_list GET /v1/system/utilization/silos +silo_utilization_view GET /v1/system/utilization/silos/{silo} silo_view GET /v1/system/silos/{silo} system_quotas_list GET /v1/system/silo-quotas user_builtin_list GET /v1/system/users-builtin diff --git a/nexus/types/src/external_api/params.rs b/nexus/types/src/external_api/params.rs index f27a6619e2..df399e310c 100644 --- a/nexus/types/src/external_api/params.rs +++ b/nexus/types/src/external_api/params.rs @@ -339,6 +339,14 @@ impl SiloQuotasCreate { } } +// This conversion is mostly just useful for tests such that we can reuse +// empty() and arbitrarily_high_default() when testing utilization +impl From for super::views::VirtualResourceCounts { + fn from(quota: SiloQuotasCreate) -> Self { + Self { cpus: quota.cpus, memory: quota.memory, storage: quota.storage } + } +} + /// Updateable properties of a Silo's resource limits. /// If a value is omitted it will not be updated. #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] diff --git a/nexus/types/src/external_api/views.rs b/nexus/types/src/external_api/views.rs index ecd459594a..46a8aa3d95 100644 --- a/nexus/types/src/external_api/views.rs +++ b/nexus/types/src/external_api/views.rs @@ -13,7 +13,7 @@ use chrono::DateTime; use chrono::Utc; use omicron_common::api::external::{ ByteCount, Digest, IdentityMetadata, InstanceState, Ipv4Net, Ipv6Net, Name, - ObjectIdentity, RoleName, SemverVersion, + ObjectIdentity, RoleName, SemverVersion, SimpleIdentity, }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -49,14 +49,64 @@ pub struct Silo { BTreeMap>, } -#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] -pub struct SiloQuotas { - pub silo_id: Uuid, +/// A collection of resource counts used to describe capacity and utilization +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, JsonSchema)] +pub struct VirtualResourceCounts { + /// Number of virtual CPUs pub cpus: i64, + /// Amount of memory in bytes pub memory: ByteCount, + /// Amount of disk storage in bytes pub storage: ByteCount, } +/// A collection of resource counts used to set the virtual capacity of a silo +#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] +pub struct SiloQuotas { + pub silo_id: Uuid, + #[serde(flatten)] + pub limits: VirtualResourceCounts, +} + +// For the eyes of end users +/// View of the current silo's resource utilization and capacity +#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] +pub struct Utilization { + /// Accounts for resources allocated to running instances or storage allocated via disks or snapshots + /// Note that CPU and memory resources associated with a stopped instances are not counted here + /// whereas associated disks will still be counted + pub provisioned: VirtualResourceCounts, + /// The total amount of resources that can be provisioned in this silo + /// Actions that would exceed this limit will fail + pub capacity: VirtualResourceCounts, +} + +// For the eyes of an operator +/// View of a silo's resource utilization and capacity +#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] +pub struct SiloUtilization { + pub silo_id: Uuid, + pub silo_name: Name, + /// Accounts for resources allocated by in silos like CPU or memory for running instances and storage for disks and snapshots + /// Note that CPU and memory resources associated with a stopped instances are not counted here + pub provisioned: VirtualResourceCounts, + /// Accounts for the total amount of resources reserved for silos via their quotas + pub allocated: VirtualResourceCounts, +} + +// We want to be able to paginate SiloUtilization by NameOrId +// but we can't derive ObjectIdentity because this isn't a typical asset. +// Instead we implement this new simple identity trait which is used under the +// hood by the pagination code. +impl SimpleIdentity for SiloUtilization { + fn id(&self) -> Uuid { + self.silo_id + } + fn name(&self) -> &Name { + &self.silo_name + } +} + // IDENTITY PROVIDER #[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, JsonSchema)] diff --git a/openapi/nexus.json b/openapi/nexus.json index 2ddd5f0e94..2a18934718 100644 --- a/openapi/nexus.json +++ b/openapi/nexus.json @@ -6815,6 +6815,103 @@ } } }, + "/v1/system/utilization/silos": { + "get": { + "tags": [ + "system/silos" + ], + "summary": "List current utilization state for all silos", + "operationId": "silo_utilization_list", + "parameters": [ + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SiloUtilizationResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + } + }, + "/v1/system/utilization/silos/{silo}": { + "get": { + "tags": [ + "system/silos" + ], + "summary": "View the current utilization of a given silo", + "operationId": "silo_utilization_view", + "parameters": [ + { + "in": "path", + "name": "silo", + "description": "Name or ID of the silo", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SiloUtilization" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, "/v1/users": { "get": { "tags": [ @@ -6883,6 +6980,33 @@ } } }, + "/v1/utilization": { + "get": { + "tags": [ + "silos" + ], + "summary": "View the resource utilization of the user's current silo", + "operationId": "utilization_view", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Utilization" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, "/v1/vpc-firewall-rules": { "get": { "tags": [ @@ -13395,21 +13519,33 @@ ] }, "SiloQuotas": { + "description": "A collection of resource counts used to set the virtual capacity of a silo", "type": "object", "properties": { "cpus": { + "description": "Number of virtual CPUs", "type": "integer", "format": "int64" }, "memory": { - "$ref": "#/components/schemas/ByteCount" + "description": "Amount of memory in bytes", + "allOf": [ + { + "$ref": "#/components/schemas/ByteCount" + } + ] }, "silo_id": { "type": "string", "format": "uuid" }, "storage": { - "$ref": "#/components/schemas/ByteCount" + "description": "Amount of disk storage in bytes", + "allOf": [ + { + "$ref": "#/components/schemas/ByteCount" + } + ] } }, "required": [ @@ -13568,6 +13704,62 @@ "role_name" ] }, + "SiloUtilization": { + "description": "View of a silo's resource utilization and capacity", + "type": "object", + "properties": { + "allocated": { + "description": "Accounts for the total amount of resources reserved for silos via their quotas", + "allOf": [ + { + "$ref": "#/components/schemas/VirtualResourceCounts" + } + ] + }, + "provisioned": { + "description": "Accounts for resources allocated by in silos like CPU or memory for running instances and storage for disks and snapshots Note that CPU and memory resources associated with a stopped instances are not counted here", + "allOf": [ + { + "$ref": "#/components/schemas/VirtualResourceCounts" + } + ] + }, + "silo_id": { + "type": "string", + "format": "uuid" + }, + "silo_name": { + "$ref": "#/components/schemas/Name" + } + }, + "required": [ + "allocated", + "provisioned", + "silo_id", + "silo_name" + ] + }, + "SiloUtilizationResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/SiloUtilization" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, "Sled": { "description": "An operator's view of a Sled.", "type": "object", @@ -14891,6 +15083,64 @@ "username" ] }, + "Utilization": { + "description": "View of the current silo's resource utilization and capacity", + "type": "object", + "properties": { + "capacity": { + "description": "The total amount of resources that can be provisioned in this silo Actions that would exceed this limit will fail", + "allOf": [ + { + "$ref": "#/components/schemas/VirtualResourceCounts" + } + ] + }, + "provisioned": { + "description": "Accounts for resources allocated to running instances or storage allocated via disks or snapshots Note that CPU and memory resources associated with a stopped instances are not counted here whereas associated disks will still be counted", + "allOf": [ + { + "$ref": "#/components/schemas/VirtualResourceCounts" + } + ] + } + }, + "required": [ + "capacity", + "provisioned" + ] + }, + "VirtualResourceCounts": { + "description": "A collection of resource counts used to describe capacity and utilization", + "type": "object", + "properties": { + "cpus": { + "description": "Number of virtual CPUs", + "type": "integer", + "format": "int64" + }, + "memory": { + "description": "Amount of memory in bytes", + "allOf": [ + { + "$ref": "#/components/schemas/ByteCount" + } + ] + }, + "storage": { + "description": "Amount of disk storage in bytes", + "allOf": [ + { + "$ref": "#/components/schemas/ByteCount" + } + ] + } + }, + "required": [ + "cpus", + "memory", + "storage" + ] + }, "Vpc": { "description": "View of a VPC", "type": "object", diff --git a/schema/crdb/21.0.0/up01.sql b/schema/crdb/21.0.0/up01.sql new file mode 100644 index 0000000000..0aaedc1862 --- /dev/null +++ b/schema/crdb/21.0.0/up01.sql @@ -0,0 +1,17 @@ +CREATE VIEW IF NOT EXISTS omicron.public.silo_utilization AS +SELECT + c.id AS silo_id, + s.name AS silo_name, + c.cpus_provisioned AS cpus_provisioned, + c.ram_provisioned AS memory_provisioned, + c.virtual_disk_bytes_provisioned AS storage_provisioned, + q.cpus AS cpus_allocated, + q.memory_bytes AS memory_allocated, + q.storage_bytes AS storage_allocated +FROM + omicron.public.virtual_provisioning_collection AS c + RIGHT JOIN omicron.public.silo_quotas AS q ON c.id = q.silo_id + INNER JOIN omicron.public.silo AS s ON c.id = s.id +WHERE + c.collection_type = 'Silo' + AND s.time_deleted IS NULL; \ No newline at end of file diff --git a/schema/crdb/dbinit.sql b/schema/crdb/dbinit.sql index be7291b4e4..cc61148048 100644 --- a/schema/crdb/dbinit.sql +++ b/schema/crdb/dbinit.sql @@ -836,6 +836,31 @@ CREATE TABLE IF NOT EXISTS omicron.public.silo_quotas ( storage_bytes INT8 NOT NULL ); +/** + * A view of the amount of provisioned and allocated (set by quotas) resources + * on a given silo. + */ +CREATE VIEW IF NOT EXISTS omicron.public.silo_utilization +AS SELECT + c.id AS silo_id, + s.name AS silo_name, + c.cpus_provisioned AS cpus_provisioned, + c.ram_provisioned AS memory_provisioned, + c.virtual_disk_bytes_provisioned AS storage_provisioned, + q.cpus AS cpus_allocated, + q.memory_bytes AS memory_allocated, + q.storage_bytes AS storage_allocated +FROM + omicron.public.virtual_provisioning_collection AS c + RIGHT JOIN omicron.public.silo_quotas AS q + ON c.id = q.silo_id + INNER JOIN omicron.public.silo AS s + ON c.id = s.id +WHERE + c.collection_type = 'Silo' +AND + s.time_deleted IS NULL; + /* * Projects */ @@ -3071,7 +3096,7 @@ INSERT INTO omicron.public.db_metadata ( version, target_version ) VALUES - ( TRUE, NOW(), NOW(), '20.0.0', NULL) + ( TRUE, NOW(), NOW(), '21.0.0', NULL) ON CONFLICT DO NOTHING; COMMIT; diff --git a/test-utils/src/dev/test_cmds.rs b/test-utils/src/dev/test_cmds.rs index f8fc6b1d27..15c94554c8 100644 --- a/test-utils/src/dev/test_cmds.rs +++ b/test-utils/src/dev/test_cmds.rs @@ -39,7 +39,7 @@ pub fn path_to_executable(cmd_name: &str) -> PathBuf { #[track_caller] pub fn assert_exit_code(exit_status: ExitStatus, code: u32, stderr_text: &str) { if let ExitStatus::Exited(exit_code) = exit_status { - assert_eq!(exit_code, code); + assert_eq!(exit_code, code, "stderr:\n{}", stderr_text); } else { panic!( "expected normal process exit with code {}, got {:?}\n\nprocess stderr:{}", From b1ebae8ab2cb7e636f04d847e0ac77aba891ff30 Mon Sep 17 00:00:00 2001 From: "oxide-reflector-bot[bot]" <130185838+oxide-reflector-bot[bot]@users.noreply.github.com> Date: Fri, 15 Dec 2023 01:23:48 +0000 Subject: [PATCH 07/33] Update dendrite to 1c2f91a (#4513) Updated dendrite to commit 1c2f91a. --------- Co-authored-by: reflector[bot] <130185838+reflector[bot]@users.noreply.github.com> --- package-manifest.toml | 12 ++++++------ tools/dendrite_openapi_version | 2 +- tools/dendrite_stub_checksums | 6 +++--- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/package-manifest.toml b/package-manifest.toml index 8516a50e65..1eca2004f8 100644 --- a/package-manifest.toml +++ b/package-manifest.toml @@ -476,8 +476,8 @@ only_for_targets.image = "standard" # 2. Copy dendrite.tar.gz from dendrite/out to omicron/out source.type = "prebuilt" source.repo = "dendrite" -source.commit = "45e05b2a90203d84510e0c8e902d9449b09ffd9b" -source.sha256 = "b14e73c8091a004472f9825b9b81b2c685bc5a48801704380a80481499060ad9" +source.commit = "1c2f91a493c8b3c5fb7b853c570b2901ac3c22a7" +source.sha256 = "052d97370515189465e4e835edb4a2d7e1e0b55ace0230ba18f045a03d975e80" output.type = "zone" output.intermediate_only = true @@ -501,8 +501,8 @@ only_for_targets.image = "standard" # 2. Copy the output zone image from dendrite/out to omicron/out source.type = "prebuilt" source.repo = "dendrite" -source.commit = "45e05b2a90203d84510e0c8e902d9449b09ffd9b" -source.sha256 = "06575bea6173d16f6d206b580956ae2cdc72c65df2eb2f40dac01468ab49e336" +source.commit = "1c2f91a493c8b3c5fb7b853c570b2901ac3c22a7" +source.sha256 = "3ebc1ee37c4d7a0657a78abbaad2fe81570da88128505bfdc4ea47e3e05c6277" output.type = "zone" output.intermediate_only = true @@ -519,8 +519,8 @@ only_for_targets.image = "standard" # 2. Copy dendrite.tar.gz from dendrite/out to omicron/out/dendrite-softnpu.tar.gz source.type = "prebuilt" source.repo = "dendrite" -source.commit = "45e05b2a90203d84510e0c8e902d9449b09ffd9b" -source.sha256 = "db2a398426fe59bd911eed91a3db7731a7a4d57e31dd357d89828d04b0891e2a" +source.commit = "1c2f91a493c8b3c5fb7b853c570b2901ac3c22a7" +source.sha256 = "18079b2ce1003facb476e28499f2e31ebe092510ecd6c685fa1a91f1a34f2dda" output.type = "zone" output.intermediate_only = true diff --git a/tools/dendrite_openapi_version b/tools/dendrite_openapi_version index c2afe5ca87..6bda68c69d 100644 --- a/tools/dendrite_openapi_version +++ b/tools/dendrite_openapi_version @@ -1,2 +1,2 @@ -COMMIT="45e05b2a90203d84510e0c8e902d9449b09ffd9b" +COMMIT="1c2f91a493c8b3c5fb7b853c570b2901ac3c22a7" SHA2="07d115bfa8498a8015ca2a8447efeeac32e24aeb25baf3d5e2313216e11293c0" diff --git a/tools/dendrite_stub_checksums b/tools/dendrite_stub_checksums index 2b4f0e7555..de183cb496 100644 --- a/tools/dendrite_stub_checksums +++ b/tools/dendrite_stub_checksums @@ -1,3 +1,3 @@ -CIDL_SHA256_ILLUMOS="b14e73c8091a004472f9825b9b81b2c685bc5a48801704380a80481499060ad9" -CIDL_SHA256_LINUX_DPD="a0d92b5007826b119c68fdaef753e33b125740ec7b3e771bfa6b3aa8d9fcb8cc" -CIDL_SHA256_LINUX_SWADM="13387460db5b57e6ffad6c0b8877af32cc6d53fecc4a1a0910143c0446d39a38" +CIDL_SHA256_ILLUMOS="052d97370515189465e4e835edb4a2d7e1e0b55ace0230ba18f045a03d975e80" +CIDL_SHA256_LINUX_DPD="5c8bc252818897bc552a039f2423eb668d99e19ef54374644412c7aca533f94e" +CIDL_SHA256_LINUX_SWADM="9d549fc3ebaf392961404b50e802ccb5e81e41e779ecc46166d49e5fb44b524f" From 137559c74ec190623c2f1d4c24e56f9735c98ee1 Mon Sep 17 00:00:00 2001 From: Kyle Simpson Date: Fri, 15 Dec 2023 20:55:13 +0000 Subject: [PATCH 08/33] Fix FIP creation being able to access IP Pools in other silos (#4705) As raised in Matrix, this backports a fix from #4261 where a new floating IP can be allocated using the name or ID of an IP pool which is bound to another silo. --------- Co-authored-by: David Crespo --- .../src/db/datastore/external_ip.rs | 42 +++++++---- nexus/src/app/sagas/disk_create.rs | 2 +- nexus/src/app/sagas/disk_delete.rs | 2 +- nexus/src/app/sagas/snapshot_create.rs | 2 +- nexus/test-utils/src/resource_helpers.rs | 3 +- nexus/tests/integration_tests/external_ips.rs | 71 +++++++++++++++++-- nexus/tests/integration_tests/instances.rs | 2 +- 7 files changed, 100 insertions(+), 24 deletions(-) diff --git a/nexus/db-queries/src/db/datastore/external_ip.rs b/nexus/db-queries/src/db/datastore/external_ip.rs index ddf396f871..2adeebd819 100644 --- a/nexus/db-queries/src/db/datastore/external_ip.rs +++ b/nexus/db-queries/src/db/datastore/external_ip.rs @@ -147,22 +147,34 @@ impl DataStore { ) -> CreateResult { let ip_id = Uuid::new_v4(); - let pool_id = match params.pool { - Some(NameOrId::Name(name)) => { - LookupPath::new(opctx, self) - .ip_pool_name(&Name(name)) - .fetch_for(authz::Action::Read) - .await? - .1 - } - Some(NameOrId::Id(id)) => { - LookupPath::new(opctx, self) - .ip_pool_id(id) - .fetch_for(authz::Action::Read) - .await? - .1 + // See `allocate_instance_ephemeral_ip`: we're replicating + // its strucutre to prevent cross-silo pool access. + let pool_id = if let Some(name_or_id) = params.pool { + let (.., authz_pool, pool) = match name_or_id { + NameOrId::Name(name) => { + LookupPath::new(opctx, self) + .ip_pool_name(&Name(name)) + .fetch_for(authz::Action::CreateChild) + .await? + } + NameOrId::Id(id) => { + LookupPath::new(opctx, self) + .ip_pool_id(id) + .fetch_for(authz::Action::CreateChild) + .await? + } + }; + + let authz_silo_id = opctx.authn.silo_required()?.id(); + if let Some(pool_silo_id) = pool.silo_id { + if pool_silo_id != authz_silo_id { + return Err(authz_pool.not_found()); + } } - None => self.ip_pools_fetch_default(opctx).await?, + + pool + } else { + self.ip_pools_fetch_default(opctx).await? } .id(); diff --git a/nexus/src/app/sagas/disk_create.rs b/nexus/src/app/sagas/disk_create.rs index 4883afaddc..ab62977746 100644 --- a/nexus/src/app/sagas/disk_create.rs +++ b/nexus/src/app/sagas/disk_create.rs @@ -854,7 +854,7 @@ pub(crate) mod test { const PROJECT_NAME: &str = "springfield-squidport"; async fn create_org_and_project(client: &ClientTestContext) -> Uuid { - create_ip_pool(&client, "p0", None).await; + create_ip_pool(&client, "p0", None, None).await; let project = create_project(client, PROJECT_NAME).await; project.identity.id } diff --git a/nexus/src/app/sagas/disk_delete.rs b/nexus/src/app/sagas/disk_delete.rs index 8f6d74da0a..f791d289db 100644 --- a/nexus/src/app/sagas/disk_delete.rs +++ b/nexus/src/app/sagas/disk_delete.rs @@ -202,7 +202,7 @@ pub(crate) mod test { const PROJECT_NAME: &str = "springfield-squidport"; async fn create_org_and_project(client: &ClientTestContext) -> Uuid { - create_ip_pool(&client, "p0", None).await; + create_ip_pool(&client, "p0", None, None).await; let project = create_project(client, PROJECT_NAME).await; project.identity.id } diff --git a/nexus/src/app/sagas/snapshot_create.rs b/nexus/src/app/sagas/snapshot_create.rs index 3b4dfc0043..c3fe6fc327 100644 --- a/nexus/src/app/sagas/snapshot_create.rs +++ b/nexus/src/app/sagas/snapshot_create.rs @@ -1786,7 +1786,7 @@ mod test { const INSTANCE_NAME: &str = "base-instance"; async fn create_org_project_and_disk(client: &ClientTestContext) -> Uuid { - create_ip_pool(&client, "p0", None).await; + create_ip_pool(&client, "p0", None, None).await; create_project(client, PROJECT_NAME).await; create_disk(client, PROJECT_NAME, DISK_NAME).await.identity.id } diff --git a/nexus/test-utils/src/resource_helpers.rs b/nexus/test-utils/src/resource_helpers.rs index 0527d99490..c72c7ad780 100644 --- a/nexus/test-utils/src/resource_helpers.rs +++ b/nexus/test-utils/src/resource_helpers.rs @@ -134,6 +134,7 @@ pub async fn create_ip_pool( client: &ClientTestContext, pool_name: &str, ip_range: Option, + silo: Option, ) -> (IpPool, IpPoolRange) { let pool = object_create( client, @@ -143,7 +144,7 @@ pub async fn create_ip_pool( name: pool_name.parse().unwrap(), description: String::from("an ip pool"), }, - silo: None, + silo: silo.map(|id| NameOrId::Id(id)), is_default: false, }, ) diff --git a/nexus/tests/integration_tests/external_ips.rs b/nexus/tests/integration_tests/external_ips.rs index f3161dea72..daec8e2064 100644 --- a/nexus/tests/integration_tests/external_ips.rs +++ b/nexus/tests/integration_tests/external_ips.rs @@ -19,14 +19,17 @@ use nexus_test_utils::resource_helpers::create_floating_ip; use nexus_test_utils::resource_helpers::create_instance_with; use nexus_test_utils::resource_helpers::create_ip_pool; use nexus_test_utils::resource_helpers::create_project; +use nexus_test_utils::resource_helpers::create_silo; use nexus_test_utils::resource_helpers::populate_ip_pool; use nexus_test_utils_macros::nexus_test; use nexus_types::external_api::params; +use nexus_types::external_api::shared; use nexus_types::external_api::views::FloatingIp; use omicron_common::address::IpRange; use omicron_common::address::Ipv4Range; use omicron_common::api::external::IdentityMetadataCreateParams; use omicron_common::api::external::Instance; +use omicron_common::api::external::NameOrId; use uuid::Uuid; type ControlPlaneTestContext = @@ -34,7 +37,8 @@ type ControlPlaneTestContext = const PROJECT_NAME: &str = "rootbeer-float"; -const FIP_NAMES: &[&str] = &["vanilla", "chocolate", "strawberry", "pistachio"]; +const FIP_NAMES: &[&str] = + &["vanilla", "chocolate", "strawberry", "pistachio", "caramel"]; pub fn get_floating_ips_url(project_name: &str) -> String { format!("/v1/floating-ips?project={project_name}") @@ -107,7 +111,7 @@ async fn test_floating_ip_create(cptestctx: &ControlPlaneTestContext) { Ipv4Range::new(Ipv4Addr::new(10, 1, 0, 1), Ipv4Addr::new(10, 1, 0, 5)) .unwrap(), ); - create_ip_pool(&client, "other-pool", Some(other_pool_range)).await; + create_ip_pool(&client, "other-pool", Some(other_pool_range), None).await; let project = create_project(client, PROJECT_NAME).await; @@ -142,7 +146,7 @@ async fn test_floating_ip_create(cptestctx: &ControlPlaneTestContext) { assert_eq!(fip.instance_id, None); assert_eq!(fip.ip, ip_addr); - // Create with no chosen IP from named pool. + // Create with no chosen IP from fleet-scoped named pool. let fip_name = FIP_NAMES[2]; let fip = create_floating_ip( client, @@ -157,7 +161,7 @@ async fn test_floating_ip_create(cptestctx: &ControlPlaneTestContext) { assert_eq!(fip.instance_id, None); assert_eq!(fip.ip, IpAddr::from(Ipv4Addr::new(10, 1, 0, 1))); - // Create with chosen IP from named pool. + // Create with chosen IP from fleet-scoped named pool. let fip_name = FIP_NAMES[3]; let ip_addr = "10.1.0.5".parse().unwrap(); let fip = create_floating_ip( @@ -174,6 +178,65 @@ async fn test_floating_ip_create(cptestctx: &ControlPlaneTestContext) { assert_eq!(fip.ip, ip_addr); } +#[nexus_test] +async fn test_floating_ip_create_fails_in_other_silo_pool( + cptestctx: &ControlPlaneTestContext, +) { + let client = &cptestctx.external_client; + + populate_ip_pool(&client, "default", None).await; + + let project = create_project(client, PROJECT_NAME).await; + + // Create other silo and pool linked to that silo + let other_silo = create_silo( + &client, + "not-my-silo", + true, + shared::SiloIdentityMode::SamlJit, + ) + .await; + let other_pool_range = IpRange::V4( + Ipv4Range::new(Ipv4Addr::new(10, 2, 0, 1), Ipv4Addr::new(10, 2, 0, 5)) + .unwrap(), + ); + create_ip_pool( + &client, + "external-silo-pool", + Some(other_pool_range), + Some(other_silo.identity.id), + ) + .await; + + let fip_name = FIP_NAMES[4]; + + // creating a floating IP should fail with a 404 as if the specified pool + // does not exist + let url = + format!("/v1/floating-ips?project={}", project.identity.name.as_str()); + let body = params::FloatingIpCreate { + identity: IdentityMetadataCreateParams { + name: fip_name.parse().unwrap(), + description: String::from("a floating ip"), + }, + address: None, + pool: Some(NameOrId::Name("external-silo-pool".parse().unwrap())), + }; + + let error = NexusRequest::new( + RequestBuilder::new(client, Method::POST, &url) + .body(Some(&body)) + .expect_status(Some(StatusCode::NOT_FOUND)), + ) + .authn_as(AuthnMode::PrivilegedUser) + .execute_and_parse_unwrap::() + .await; + assert_eq!( + error.message, + "not found: ip-pool with name \"external-silo-pool\"" + ); +} + #[nexus_test] async fn test_floating_ip_create_ip_in_use( cptestctx: &ControlPlaneTestContext, diff --git a/nexus/tests/integration_tests/instances.rs b/nexus/tests/integration_tests/instances.rs index 19b507f5bb..4acc918333 100644 --- a/nexus/tests/integration_tests/instances.rs +++ b/nexus/tests/integration_tests/instances.rs @@ -3563,7 +3563,7 @@ async fn test_instance_ephemeral_ip_from_correct_pool( .unwrap(), ); populate_ip_pool(&client, "default", Some(default_pool_range)).await; - create_ip_pool(&client, "other-pool", Some(other_pool_range)).await; + create_ip_pool(&client, "other-pool", Some(other_pool_range), None).await; // Create an instance with pool name blank, expect IP from default pool create_instance_with_pool(client, "default-pool-inst", None).await; From 2195b56ae6eb822e80d70a9b33cb946c1ea47063 Mon Sep 17 00:00:00 2001 From: David Crespo Date: Fri, 15 Dec 2023 14:55:56 -0600 Subject: [PATCH 09/33] Bump web console (#4706) https://github.com/oxidecomputer/console/compare/007cfe67...ad2ea54a * [ad2ea54a](https://github.com/oxidecomputer/console/commit/ad2ea54a) oxidecomputer/console#1844 --- tools/console_version | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/console_version b/tools/console_version index e62ecf2bf6..1b2d1b273a 100644 --- a/tools/console_version +++ b/tools/console_version @@ -1,2 +1,2 @@ -COMMIT="007cfe672aa7e7c791591be089bf2a2386d2c34f" -SHA2="a4f4264229724304ee383ba55d426acd1e5d713417cf1e77fed791c3a7162abf" +COMMIT="ad2ea54a27615e21a4993fbeff3fd83fbc2098a4" +SHA2="20c62ec121948fd0794b6e1f0326d3d8e701e4a3872b18e7d4752e92b614d185" From 83e01a51264497f780acc0f89fec65d70fc1aeca Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 15 Dec 2023 15:03:05 -0800 Subject: [PATCH 10/33] Bump zerocopy from 0.7.26 to 0.7.31 (#4702) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 16 ++++++++-------- workspace-hack/Cargo.toml | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e9e5c1594d..121e31550f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -62,7 +62,7 @@ dependencies = [ "getrandom 0.2.10", "once_cell", "version_check", - "zerocopy 0.7.26", + "zerocopy 0.7.31", ] [[package]] @@ -5178,7 +5178,7 @@ dependencies = [ "usdt", "uuid", "yasna", - "zerocopy 0.7.26", + "zerocopy 0.7.31", "zeroize", "zip", ] @@ -5405,7 +5405,7 @@ dependencies = [ "opte", "serde", "smoltcp 0.10.0", - "zerocopy 0.7.26", + "zerocopy 0.7.31", ] [[package]] @@ -10191,12 +10191,12 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.7.26" +version = "0.7.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e97e415490559a91254a2979b4829267a57d2fcd741a98eee8b722fb57289aa0" +checksum = "1c4061bedbb353041c12f413700357bec76df2c7e2ca8e4df8bac24c6bf68e3d" dependencies = [ "byteorder", - "zerocopy-derive 0.7.26", + "zerocopy-derive 0.7.31", ] [[package]] @@ -10223,9 +10223,9 @@ dependencies = [ [[package]] name = "zerocopy-derive" -version = "0.7.26" +version = "0.7.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd7e48ccf166952882ca8bd778a43502c64f33bf94c12ebe2a7f08e5a0f6689f" +checksum = "b3c129550b3e6de3fd0ba67ba5c81818f9805e58b8d7fee80a3a59d2c9fc601a" dependencies = [ "proc-macro2", "quote", diff --git a/workspace-hack/Cargo.toml b/workspace-hack/Cargo.toml index 01cd1bdb68..8998f7594b 100644 --- a/workspace-hack/Cargo.toml +++ b/workspace-hack/Cargo.toml @@ -112,7 +112,7 @@ unicode-normalization = { version = "0.1.22" } usdt = { version = "0.3.5" } uuid = { version = "1.6.1", features = ["serde", "v4"] } yasna = { version = "0.5.2", features = ["bit-vec", "num-bigint", "std", "time"] } -zerocopy = { version = "0.7.26", features = ["derive", "simd"] } +zerocopy = { version = "0.7.31", features = ["derive", "simd"] } zeroize = { version = "1.7.0", features = ["std", "zeroize_derive"] } zip = { version = "0.6.6", default-features = false, features = ["bzip2", "deflate"] } @@ -216,7 +216,7 @@ unicode-normalization = { version = "0.1.22" } usdt = { version = "0.3.5" } uuid = { version = "1.6.1", features = ["serde", "v4"] } yasna = { version = "0.5.2", features = ["bit-vec", "num-bigint", "std", "time"] } -zerocopy = { version = "0.7.26", features = ["derive", "simd"] } +zerocopy = { version = "0.7.31", features = ["derive", "simd"] } zeroize = { version = "1.7.0", features = ["std", "zeroize_derive"] } zip = { version = "0.6.6", default-features = false, features = ["bzip2", "deflate"] } From 4ae84726c2d2382fa643b29e04585727e204148f Mon Sep 17 00:00:00 2001 From: David Crespo Date: Sat, 16 Dec 2023 22:14:12 -0600 Subject: [PATCH 11/33] Bump web console (utilization rounding fix) (#4710) https://github.com/oxidecomputer/console/compare/ad2ea54a...02c6ce74 * [02c6ce74](https://github.com/oxidecomputer/console/commit/02c6ce74) oxidecomputer/console#1865 --- tools/console_version | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/console_version b/tools/console_version index 1b2d1b273a..785c535e8d 100644 --- a/tools/console_version +++ b/tools/console_version @@ -1,2 +1,2 @@ -COMMIT="ad2ea54a27615e21a4993fbeff3fd83fbc2098a4" -SHA2="20c62ec121948fd0794b6e1f0326d3d8e701e4a3872b18e7d4752e92b614d185" +COMMIT="02c6ce747fd5dd05e9d454ecb1bf70392c9d954e" +SHA2="39fd191993e147a569e28df86414e3d0f33963b7675474d7c522c3f685d4d4f0" From d5fb85eb605bc04244185eef93cc10f09b50e712 Mon Sep 17 00:00:00 2001 From: David Crespo Date: Mon, 18 Dec 2023 11:15:15 -0600 Subject: [PATCH 12/33] [trivial] Move `SCHEMA_VERSION` to the top of `schema.rs` (#4703) Digging for this in the middle of the file when I want to bump the version drives me nuts. Will obviously hold until after release. --- nexus/db-model/src/schema.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/nexus/db-model/src/schema.rs b/nexus/db-model/src/schema.rs index 6839af8a76..7f4bf51487 100644 --- a/nexus/db-model/src/schema.rs +++ b/nexus/db-model/src/schema.rs @@ -8,6 +8,13 @@ use omicron_common::api::external::SemverVersion; +/// The version of the database schema this particular version of Nexus was +/// built against. +/// +/// This should be updated whenever the schema is changed. For more details, +/// refer to: schema/crdb/README.adoc +pub const SCHEMA_VERSION: SemverVersion = SemverVersion::new(21, 0, 0); + table! { disk (id) { id -> Uuid, @@ -1341,13 +1348,6 @@ table! { } } -/// The version of the database schema this particular version of Nexus was -/// built against. -/// -/// This should be updated whenever the schema is changed. For more details, -/// refer to: schema/crdb/README.adoc -pub const SCHEMA_VERSION: SemverVersion = SemverVersion::new(21, 0, 0); - allow_tables_to_appear_in_same_query!( system_update, component_update, From 1032885c94d1b8581796b26cb4a254b84adaf5e1 Mon Sep 17 00:00:00 2001 From: Andy Fiddaman Date: Mon, 18 Dec 2023 18:16:27 +0000 Subject: [PATCH 13/33] Configure bash environment for NGZ root. (#4712) This removes the special case for the switch zone and deploys a consistent profile and bashrc to all non-global zones. While understanding this is subjective, improvements here are: * A switch zone prompt includes the local switch number (0/1) if it can be determined; * PATH is configured to include additional directories useful within the zone; * The hostname part of the prompt is truncated in zones which have UUIDs as part of their name; * Coloured prompt, as per the GZ. Pretty much everyone has their own preferred prompt format, so consensus is unlikely here, but this is a step forward in having consistency and a better PATH. In the limit, nobody will be logging into these zones outside of a development environment anyway. --- package-manifest.toml | 14 +++++++++- smf/profile/bashrc | 42 ++++++++++++++++++++++++++++++ smf/profile/profile | 24 +++++++++++++++++ smf/switch_zone_setup/root.profile | 3 --- 4 files changed, 79 insertions(+), 4 deletions(-) create mode 100644 smf/profile/bashrc create mode 100644 smf/profile/profile delete mode 100644 smf/switch_zone_setup/root.profile diff --git a/package-manifest.toml b/package-manifest.toml index 1eca2004f8..6bd40c320d 100644 --- a/package-manifest.toml +++ b/package-manifest.toml @@ -69,6 +69,7 @@ service_name = "overlay" source.type = "composite" source.packages = [ "logadm.tar.gz", + "profile.tar.gz", ] output.type = "zone" @@ -83,6 +84,18 @@ source.paths = [ ] output.type = "zone" output.intermediate_only = true +# +# The profile package is an overlay for all non-global zones to configure +# root's bash environment. +[package.profile] +service_name = "profile" +source.type = "local" +source.paths = [ + { from = "smf/profile/profile", to = "/root/.profile" }, + { from = "smf/profile/bashrc", to = "/root/.bashrc" }, +] +output.type = "zone" +output.intermediate_only = true [package.omicron-nexus] service_name = "nexus" @@ -335,7 +348,6 @@ source.paths = [ { from = "smf/switch_zone_setup/manifest.xml", to = "/var/svc/manifest/site/switch_zone_setup/manifest.xml" }, { from = "smf/switch_zone_setup/switch_zone_setup", to = "/opt/oxide/bin/switch_zone_setup" }, { from = "smf/switch_zone_setup/support_authorized_keys", to = "/opt/oxide/support/authorized_keys" }, - { from = "smf/switch_zone_setup/root.profile", to = "/root/.profile" }, ] output.type = "zone" output.intermediate_only = true diff --git a/smf/profile/bashrc b/smf/profile/bashrc new file mode 100644 index 0000000000..d19e41e5f7 --- /dev/null +++ b/smf/profile/bashrc @@ -0,0 +1,42 @@ + +C_RED='\[\033[01;31m\]' +C_GREEN='\[\033[01;32m\]' +C_CYAN='\[\033[01;36m\]' +C_BLD='\[\033[1m\]' +C_NUL='\[\033[00m\]' + +typeset _hst="$HOSTNAME" +typeset _hstc="$C_RED$HOSTNAME" +case "$_hst" in + oxz_switch) + # Try to determine which switch zone we are + _switchid=$(curl -s http://localhost:12225/local/switch-id \ + | /opt/ooce/bin/jq -r .slot) + if (( $? == 0 )) && [[ -n "$_switchid" ]]; then + _hst+="$_switchid" + _hstc+="$C_CYAN$_switchid" + fi + ;; + oxz_*-*) + # Shorten the hostname by truncating the UUID so that the prompt + # doesn't take up an excessive amount of width + _hst="${HOSTNAME%%-*}" + _hstc="$C_RED${HOSTNAME%%-*}" + ;; +esac + +if [[ -n $SSH_CLIENT ]]; then + echo -ne "\033]0;${_hst} \007" + export PROMPT_COMMAND='history -a' +fi + +case "$TERM" in +xterm*|rxvt*|screen*|sun-color) + PS1="$C_GREEN\\u$C_NUL@$_hstc$C_NUL:$C_RED\\w$C_NUL$C_BLD\\\$$C_NUL " + ;; +*) + PS1="\\u@$_hst:\\w\\$ " +esac + +export PS1 + diff --git a/smf/profile/profile b/smf/profile/profile new file mode 100644 index 0000000000..8f613d4d56 --- /dev/null +++ b/smf/profile/profile @@ -0,0 +1,24 @@ + +PATH+=:/opt/ooce/bin + +case "$HOSTNAME" in + oxz_switch) + # Add tools like xcvradm, swadm & ddmadm to the PATH by default + PATH+=:/opt/oxide/bin:/opt/oxide/dendrite/bin:/opt/oxide/mg-ddm/bin + ;; + oxz_cockroachdb*) + PATH+=:/opt/oxide/cockroachdb/bin + ;; + oxz_crucible*) + PATH+=:/opt/oxide/crucible/bin + ;; + oxz_clockhouse*) + PATH+=:/opt/oxide/clickhouse + ;; + oxz_external_dns*|oxz_internal_dns*) + PATH+=:/opt/oxide/dns-server/bin + ;; +esac + +[ -f ~/.bashrc ] && . ~/.bashrc + diff --git a/smf/switch_zone_setup/root.profile b/smf/switch_zone_setup/root.profile deleted file mode 100644 index b62b9e5403..0000000000 --- a/smf/switch_zone_setup/root.profile +++ /dev/null @@ -1,3 +0,0 @@ -# Add tools like xcvradm, swadm & ddmadm to the PATH by default -export PATH=$PATH:/opt/oxide/bin:/opt/oxide/dendrite/bin:/opt/oxide/mg-ddm/bin - From 6d3b8be167f2ef328e2976586de7006bb4ddbe03 Mon Sep 17 00:00:00 2001 From: Greg Colombo Date: Mon, 18 Dec 2023 11:28:07 -0800 Subject: [PATCH 14/33] Move instances to Failed if sled agent returns an "unhealthy" error type from calls to stop/reboot (#4711) Restore `instance_reboot` and `instance_stop` to their prior behavior: if these routines try to contact sled agent and get back a server error, mark the instance as unhealthy and move it to the Failed state. Also use `#[source]` instead of message interpolation in `InstanceStateChangeError::SledAgent`. This restores the status quo ante from #4682 in anticipation of reaching a better overall mechanism for dealing with failures to communicate about instances with sled agents. See #3206, #3238, and #4226 for more discussion. Tests: new integration test; stood up a dev cluster, started an instance, killed the zone with `zoneadm halt`, and verified that calls to reboot/stop the instance eventually marked it as Failed (due to a timeout attempting to contact the Propolis zone). Fixes #4709. --- nexus/src/app/instance.rs | 71 ++++++++++++++----- nexus/tests/integration_tests/instances.rs | 80 ++++++++++++++++++++++ 2 files changed, 132 insertions(+), 19 deletions(-) diff --git a/nexus/src/app/instance.rs b/nexus/src/app/instance.rs index 93386a66d0..4045269878 100644 --- a/nexus/src/app/instance.rs +++ b/nexus/src/app/instance.rs @@ -65,7 +65,7 @@ type SledAgentClientError = sled_agent_client::Error; // Newtype wrapper to avoid the orphan type rule. -#[derive(Debug)] +#[derive(Debug, thiserror::Error)] pub struct SledAgentInstancePutError(pub SledAgentClientError); impl std::fmt::Display for SledAgentInstancePutError { @@ -117,8 +117,8 @@ impl SledAgentInstancePutError { #[derive(Debug, thiserror::Error)] pub enum InstanceStateChangeError { /// Sled agent returned an error from one of its instance endpoints. - #[error("sled agent client error: {0}")] - SledAgent(SledAgentInstancePutError), + #[error("sled agent client error")] + SledAgent(#[source] SledAgentInstancePutError), /// Some other error occurred outside of the attempt to communicate with /// sled agent. @@ -624,14 +624,31 @@ impl super::Nexus { .instance_fetch_with_vmm(opctx, &authz_instance) .await?; - self.instance_request_state( - opctx, - &authz_instance, - state.instance(), - state.vmm(), - InstanceStateChangeRequest::Reboot, - ) - .await?; + if let Err(e) = self + .instance_request_state( + opctx, + &authz_instance, + state.instance(), + state.vmm(), + InstanceStateChangeRequest::Reboot, + ) + .await + { + if let InstanceStateChangeError::SledAgent(inner) = &e { + if inner.instance_unhealthy() { + let _ = self + .mark_instance_failed( + &authz_instance.id(), + state.instance().runtime(), + inner, + ) + .await; + } + } + + return Err(e.into()); + } + self.db_datastore.instance_fetch_with_vmm(opctx, &authz_instance).await } @@ -711,14 +728,30 @@ impl super::Nexus { .instance_fetch_with_vmm(opctx, &authz_instance) .await?; - self.instance_request_state( - opctx, - &authz_instance, - state.instance(), - state.vmm(), - InstanceStateChangeRequest::Stop, - ) - .await?; + if let Err(e) = self + .instance_request_state( + opctx, + &authz_instance, + state.instance(), + state.vmm(), + InstanceStateChangeRequest::Stop, + ) + .await + { + if let InstanceStateChangeError::SledAgent(inner) = &e { + if inner.instance_unhealthy() { + let _ = self + .mark_instance_failed( + &authz_instance.id(), + state.instance().runtime(), + inner, + ) + .await; + } + } + + return Err(e.into()); + } self.db_datastore.instance_fetch_with_vmm(opctx, &authz_instance).await } diff --git a/nexus/tests/integration_tests/instances.rs b/nexus/tests/integration_tests/instances.rs index 4acc918333..44b65fa67b 100644 --- a/nexus/tests/integration_tests/instances.rs +++ b/nexus/tests/integration_tests/instances.rs @@ -867,6 +867,86 @@ async fn test_instance_migrate_v2p(cptestctx: &ControlPlaneTestContext) { } } +// Verifies that if a request to reboot or stop an instance fails because of a +// 500-level error from sled agent, then the instance moves to the Failed state. +#[nexus_test] +async fn test_instance_failed_after_sled_agent_error( + cptestctx: &ControlPlaneTestContext, +) { + let client = &cptestctx.external_client; + let apictx = &cptestctx.server.apictx(); + let nexus = &apictx.nexus; + let instance_name = "losing-is-fun"; + + // Create and start the test instance. + create_org_and_project(&client).await; + let instance_url = get_instance_url(instance_name); + let instance = create_instance(client, PROJECT_NAME, instance_name).await; + instance_simulate(nexus, &instance.identity.id).await; + let instance_next = instance_get(&client, &instance_url).await; + assert_eq!(instance_next.runtime.run_state, InstanceState::Running); + + let sled_agent = &cptestctx.sled_agent.sled_agent; + sled_agent + .set_instance_ensure_state_error(Some( + omicron_common::api::external::Error::internal_error( + "injected by test_instance_failed_after_sled_agent_error", + ), + )) + .await; + + let url = get_instance_url(format!("{}/reboot", instance_name).as_str()); + NexusRequest::new( + RequestBuilder::new(client, Method::POST, &url) + .body(None as Option<&serde_json::Value>), + ) + .authn_as(AuthnMode::PrivilegedUser) + .execute() + .await + .unwrap() + .parsed_body::() + .expect_err("expected injected failure"); + + let instance_next = instance_get(&client, &instance_url).await; + assert_eq!(instance_next.runtime.run_state, InstanceState::Failed); + + NexusRequest::object_delete(client, &get_instance_url(instance_name)) + .authn_as(AuthnMode::PrivilegedUser) + .execute() + .await + .unwrap(); + + sled_agent.set_instance_ensure_state_error(None).await; + + let instance = create_instance(client, PROJECT_NAME, instance_name).await; + instance_simulate(nexus, &instance.identity.id).await; + let instance_next = instance_get(&client, &instance_url).await; + assert_eq!(instance_next.runtime.run_state, InstanceState::Running); + + sled_agent + .set_instance_ensure_state_error(Some( + omicron_common::api::external::Error::internal_error( + "injected by test_instance_failed_after_sled_agent_error", + ), + )) + .await; + + let url = get_instance_url(format!("{}/stop", instance_name).as_str()); + NexusRequest::new( + RequestBuilder::new(client, Method::POST, &url) + .body(None as Option<&serde_json::Value>), + ) + .authn_as(AuthnMode::PrivilegedUser) + .execute() + .await + .unwrap() + .parsed_body::() + .expect_err("expected injected failure"); + + let instance_next = instance_get(&client, &instance_url).await; + assert_eq!(instance_next.runtime.run_state, InstanceState::Failed); +} + /// Assert values for fleet, silo, and project using both system and silo /// metrics endpoints async fn assert_metrics( From 8ad838eac6b972134f3689c728126eb0f6311ffb Mon Sep 17 00:00:00 2001 From: "Andrew J. Stone" Date: Mon, 18 Dec 2023 16:20:31 -0500 Subject: [PATCH 15/33] Cleanup list uninitialized sled API (#4698) * Rename `uninitialized_sled_list` to `sled_list_uninitialized` * Rename endpoint from `v1/system/hardware/uninitialized-sleds` to `v1/system/hardware/sleds-uninitialized` * Add fake pagination to this API endpoint Fixes part of #4607 --- nexus/src/app/rack.rs | 2 +- nexus/src/external_api/http_entrypoints.rs | 12 ++-- nexus/tests/integration_tests/endpoints.rs | 2 +- nexus/tests/integration_tests/rack.rs | 15 ++-- nexus/tests/output/nexus_tags.txt | 2 +- openapi/nexus.json | 79 +++++++++++++--------- 6 files changed, 66 insertions(+), 46 deletions(-) diff --git a/nexus/src/app/rack.rs b/nexus/src/app/rack.rs index 168e9eeaa3..c0307e5b5b 100644 --- a/nexus/src/app/rack.rs +++ b/nexus/src/app/rack.rs @@ -866,7 +866,7 @@ impl super::Nexus { // // TODO-multirack: We currently limit sleds to a single rack and we also // retrieve the `rack_uuid` from the Nexus instance used. - pub(crate) async fn uninitialized_sled_list( + pub(crate) async fn sled_list_uninitialized( &self, opctx: &OpContext, ) -> ListResultVec { diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index 042ee294b7..8a4aeaeff5 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -227,7 +227,7 @@ pub(crate) fn external_api() -> NexusApiDescription { api.register(physical_disk_list)?; api.register(switch_list)?; api.register(switch_view)?; - api.register(uninitialized_sled_list)?; + api.register(sled_list_uninitialized)?; api.register(add_sled_to_initialized_rack)?; api.register(user_builtin_list)?; @@ -4654,18 +4654,18 @@ async fn rack_view( /// List uninitialized sleds in a given rack #[endpoint { method = GET, - path = "/v1/system/hardware/uninitialized-sleds", + path = "/v1/system/hardware/sleds-uninitialized", tags = ["system/hardware"] }] -async fn uninitialized_sled_list( +async fn sled_list_uninitialized( rqctx: RequestContext>, -) -> Result>, HttpError> { +) -> Result>, HttpError> { let apictx = rqctx.context(); let handler = async { let nexus = &apictx.nexus; let opctx = crate::context::op_context_for_external_api(&rqctx).await?; - let sleds = nexus.uninitialized_sled_list(&opctx).await?; - Ok(HttpResponseOk(sleds)) + let sleds = nexus.sled_list_uninitialized(&opctx).await?; + Ok(HttpResponseOk(ResultsPage { items: sleds, next_page: None })) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } diff --git a/nexus/tests/integration_tests/endpoints.rs b/nexus/tests/integration_tests/endpoints.rs index c41fcdbed9..545129d567 100644 --- a/nexus/tests/integration_tests/endpoints.rs +++ b/nexus/tests/integration_tests/endpoints.rs @@ -47,7 +47,7 @@ lazy_static! { pub static ref HARDWARE_RACK_URL: String = format!("/v1/system/hardware/racks/{}", RACK_UUID); pub static ref HARDWARE_UNINITIALIZED_SLEDS: String = - format!("/v1/system/hardware/uninitialized-sleds"); + format!("/v1/system/hardware/sleds-uninitialized"); pub static ref HARDWARE_SLED_URL: String = format!("/v1/system/hardware/sleds/{}", SLED_AGENT_UUID); pub static ref HARDWARE_SLED_PROVISION_STATE_URL: String = diff --git a/nexus/tests/integration_tests/rack.rs b/nexus/tests/integration_tests/rack.rs index 9f77223871..a6fc93e92a 100644 --- a/nexus/tests/integration_tests/rack.rs +++ b/nexus/tests/integration_tests/rack.rs @@ -2,6 +2,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. +use dropshot::ResultsPage; use http::Method; use http::StatusCode; use nexus_test_utils::http_testing::AuthnMode; @@ -85,18 +86,19 @@ async fn test_rack_initialization(cptestctx: &ControlPlaneTestContext) { } #[nexus_test] -async fn test_uninitialized_sled_list(cptestctx: &ControlPlaneTestContext) { +async fn test_sled_list_uninitialized(cptestctx: &ControlPlaneTestContext) { let internal_client = &cptestctx.internal_client; let external_client = &cptestctx.external_client; - let list_url = "/v1/system/hardware/uninitialized-sleds"; + let list_url = "/v1/system/hardware/sleds-uninitialized"; let mut uninitialized_sleds = NexusRequest::object_get(external_client, &list_url) .authn_as(AuthnMode::PrivilegedUser) .execute() .await .expect("failed to get uninitialized sleds") - .parsed_body::>() - .unwrap(); + .parsed_body::>() + .unwrap() + .items; debug!(cptestctx.logctx.log, "{:#?}", uninitialized_sleds); // There are currently two fake sim gimlets created in the latest inventory @@ -137,8 +139,9 @@ async fn test_uninitialized_sled_list(cptestctx: &ControlPlaneTestContext) { .execute() .await .expect("failed to get uninitialized sleds") - .parsed_body::>() - .unwrap(); + .parsed_body::>() + .unwrap() + .items; debug!(cptestctx.logctx.log, "{:#?}", uninitialized_sleds); assert_eq!(1, uninitialized_sleds_2.len()); assert_eq!(uninitialized_sleds, uninitialized_sleds_2); diff --git a/nexus/tests/output/nexus_tags.txt b/nexus/tests/output/nexus_tags.txt index 7e1dc306d5..10e7df7286 100644 --- a/nexus/tests/output/nexus_tags.txt +++ b/nexus/tests/output/nexus_tags.txt @@ -126,12 +126,12 @@ rack_list GET /v1/system/hardware/racks rack_view GET /v1/system/hardware/racks/{rack_id} sled_instance_list GET /v1/system/hardware/sleds/{sled_id}/instances sled_list GET /v1/system/hardware/sleds +sled_list_uninitialized GET /v1/system/hardware/sleds-uninitialized sled_physical_disk_list GET /v1/system/hardware/sleds/{sled_id}/disks sled_set_provision_state PUT /v1/system/hardware/sleds/{sled_id}/provision-state sled_view GET /v1/system/hardware/sleds/{sled_id} switch_list GET /v1/system/hardware/switches switch_view GET /v1/system/hardware/switches/{switch_id} -uninitialized_sled_list GET /v1/system/hardware/uninitialized-sleds API operations found with tag "system/metrics" OPERATION ID METHOD URL PATH diff --git a/openapi/nexus.json b/openapi/nexus.json index 2a18934718..4c89706a1c 100644 --- a/openapi/nexus.json +++ b/openapi/nexus.json @@ -4015,6 +4015,33 @@ } } }, + "/v1/system/hardware/sleds-uninitialized": { + "get": { + "tags": [ + "system/hardware" + ], + "summary": "List uninitialized sleds in a given rack", + "operationId": "sled_list_uninitialized", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UninitializedSledResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, "/v1/system/hardware/switch-port": { "get": { "tags": [ @@ -4290,37 +4317,6 @@ } } }, - "/v1/system/hardware/uninitialized-sleds": { - "get": { - "tags": [ - "system/hardware" - ], - "summary": "List uninitialized sleds in a given rack", - "operationId": "uninitialized_sled_list", - "responses": { - "200": { - "description": "successful operation", - "content": { - "application/json": { - "schema": { - "title": "Array_of_UninitializedSled", - "type": "array", - "items": { - "$ref": "#/components/schemas/UninitializedSled" - } - } - } - } - }, - "4XX": { - "$ref": "#/components/responses/Error" - }, - "5XX": { - "$ref": "#/components/responses/Error" - } - } - } - }, "/v1/system/identity-providers": { "get": { "tags": [ @@ -14888,6 +14884,27 @@ "rack_id" ] }, + "UninitializedSledResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/UninitializedSled" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, "User": { "description": "View of a User", "type": "object", From e4641722b3036e10e0528dcb1d208218ea337337 Mon Sep 17 00:00:00 2001 From: John Gallagher Date: Tue, 19 Dec 2023 09:50:41 -0800 Subject: [PATCH 16/33] [mgs] Use slog-error-chain to clean up error types and logging (#4717) These are all pretty mechanical changes: * Use `#[source]` and `#[from]` correctly * Add a bit of context to a few error cases (e.g., including the `SpIdentifier` when returning an `SpCommunicationFailed` error) * Use `InlineErrorChain` instead of `anyhow` to convert error chains into strings (avoiding the intermediate `anyhow::Error` heap allocation) * Switch to `Utf8PathBuf` for command line args and related errors --- Cargo.lock | 21 ++++ Cargo.toml | 1 + gateway/Cargo.toml | 2 + gateway/src/bin/mgs.rs | 8 +- gateway/src/config.rs | 53 +++------ gateway/src/error.rs | 148 ++++++++++++++---------- gateway/src/http_entrypoints.rs | 193 +++++++++++++++++++++---------- gateway/src/lib.rs | 12 +- gateway/src/management_switch.rs | 16 ++- gateway/src/serial_console.rs | 16 ++- 10 files changed, 291 insertions(+), 179 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 121e31550f..74c01d3411 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4698,6 +4698,7 @@ version = "0.1.0" dependencies = [ "anyhow", "base64", + "camino", "clap 4.4.3", "dropshot", "expectorate", @@ -4723,6 +4724,7 @@ dependencies = [ "signal-hook-tokio", "slog", "slog-dtrace", + "slog-error-chain", "sp-sim", "subprocess", "thiserror", @@ -7842,6 +7844,25 @@ dependencies = [ "slog-term", ] +[[package]] +name = "slog-error-chain" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/slog-error-chain?branch=main#15f69041f45774602108e47fb25e705dc23acfb2" +dependencies = [ + "slog", + "slog-error-chain-derive", +] + +[[package]] +name = "slog-error-chain-derive" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/slog-error-chain?branch=main#15f69041f45774602108e47fb25e705dc23acfb2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.32", +] + [[package]] name = "slog-json" version = "2.6.1" diff --git a/Cargo.toml b/Cargo.toml index 841c7bb16b..ca134536f5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -339,6 +339,7 @@ slog = { version = "2.7", features = [ "dynamic-keys", "max_level_trace", "relea slog-async = "2.8" slog-dtrace = "0.2" slog-envlogger = "2.2" +slog-error-chain = { git = "https://github.com/oxidecomputer/slog-error-chain", branch = "main", features = ["derive"] } slog-term = "2.9" smf = "0.2" snafu = "0.7" diff --git a/gateway/Cargo.toml b/gateway/Cargo.toml index 75c31e9977..f2e5f83a8a 100644 --- a/gateway/Cargo.toml +++ b/gateway/Cargo.toml @@ -7,6 +7,7 @@ license = "MPL-2.0" [dependencies] anyhow.workspace = true base64.workspace = true +camino.workspace = true clap.workspace = true dropshot.workspace = true futures.workspace = true @@ -25,6 +26,7 @@ signal-hook.workspace = true signal-hook-tokio.workspace = true slog.workspace = true slog-dtrace.workspace = true +slog-error-chain.workspace = true thiserror.workspace = true tokio = { workspace = true, features = ["full"] } tokio-stream.workspace = true diff --git a/gateway/src/bin/mgs.rs b/gateway/src/bin/mgs.rs index 6917d4f174..39810ea06a 100644 --- a/gateway/src/bin/mgs.rs +++ b/gateway/src/bin/mgs.rs @@ -5,6 +5,7 @@ //! Executable program to run gateway, the management gateway service use anyhow::{anyhow, Context}; +use camino::Utf8PathBuf; use clap::Parser; use futures::StreamExt; use omicron_common::cmd::{fatal, CmdError}; @@ -12,7 +13,6 @@ use omicron_gateway::{run_openapi, start_server, Config, MgsArguments}; use signal_hook::consts::signal; use signal_hook_tokio::Signals; use std::net::SocketAddrV6; -use std::path::PathBuf; use uuid::Uuid; #[derive(Debug, Parser)] @@ -24,7 +24,7 @@ enum Args { /// Start an MGS server Run { #[clap(name = "CONFIG_FILE_PATH", action)] - config_file_path: PathBuf, + config_file_path: Utf8PathBuf, /// Read server ID and address(es) for dropshot server from our SMF /// properties (only valid when running as a service on illumos) @@ -81,9 +81,7 @@ async fn do_run() -> Result<(), CmdError> { address, } => { let config = Config::from_file(&config_file_path) - .with_context(|| { - format!("failed to parse {}", config_file_path.display()) - }) + .map_err(anyhow::Error::new) .map_err(CmdError::Failure)?; let mut signals = Signals::new([signal::SIGUSR1]) diff --git a/gateway/src/config.rs b/gateway/src/config.rs index adbd16c6a1..afdb046881 100644 --- a/gateway/src/config.rs +++ b/gateway/src/config.rs @@ -6,10 +6,11 @@ //! configuration use crate::management_switch::SwitchConfig; +use camino::Utf8Path; +use camino::Utf8PathBuf; use dropshot::ConfigLogging; use serde::{Deserialize, Serialize}; -use std::path::Path; -use std::path::PathBuf; +use slog_error_chain::SlogInlineError; use thiserror::Error; /// Configuration for a gateway server @@ -30,13 +31,11 @@ impl Config { /// Load a `Config` from the given TOML file /// /// This config object can then be used to create a new gateway server. - // The format is described in the README. // TODO add a README - pub fn from_file>(path: P) -> Result { - let path = path.as_ref(); + pub fn from_file(path: &Utf8Path) -> Result { let file_contents = std::fs::read_to_string(path) - .map_err(|e| (path.to_path_buf(), e))?; + .map_err(|err| LoadError::Io { path: path.into(), err })?; let config_parsed: Config = toml::from_str(&file_contents) - .map_err(|e| (path.to_path_buf(), e))?; + .map_err(|err| LoadError::Parse { path: path.into(), err })?; Ok(config_parsed) } } @@ -46,32 +45,18 @@ pub struct PartialDropshotConfig { pub request_body_max_bytes: usize, } -#[derive(Debug, Error)] +#[derive(Debug, Error, SlogInlineError)] pub enum LoadError { - #[error("error reading \"{}\": {}", path.display(), err)] - Io { path: PathBuf, err: std::io::Error }, - #[error("error parsing \"{}\": {}", path.display(), err)] - Parse { path: PathBuf, err: toml::de::Error }, -} - -impl From<(PathBuf, std::io::Error)> for LoadError { - fn from((path, err): (PathBuf, std::io::Error)) -> Self { - LoadError::Io { path, err } - } -} - -impl From<(PathBuf, toml::de::Error)> for LoadError { - fn from((path, err): (PathBuf, toml::de::Error)) -> Self { - LoadError::Parse { path, err } - } -} - -impl std::cmp::PartialEq for LoadError { - fn eq(&self, other: &std::io::Error) -> bool { - if let LoadError::Io { err, .. } = self { - err.kind() == other.kind() - } else { - false - } - } + #[error("error reading \"{path}\"")] + Io { + path: Utf8PathBuf, + #[source] + err: std::io::Error, + }, + #[error("error parsing \"{path}\"")] + Parse { + path: Utf8PathBuf, + #[source] + err: toml::de::Error, + }, } diff --git a/gateway/src/error.rs b/gateway/src/error.rs index 6daf9312ba..5933daa340 100644 --- a/gateway/src/error.rs +++ b/gateway/src/error.rs @@ -5,16 +5,17 @@ //! Error handling facilities for the management gateway. use crate::management_switch::SpIdentifier; -use anyhow::anyhow; use dropshot::HttpError; use gateway_messages::SpError; pub use gateway_sp_comms::error::CommunicationError; use gateway_sp_comms::error::UpdateError; use gateway_sp_comms::BindError; +use slog_error_chain::InlineErrorChain; +use slog_error_chain::SlogInlineError; use std::time::Duration; use thiserror::Error; -#[derive(Debug, Error)] +#[derive(Debug, Error, SlogInlineError)] pub enum StartupError { #[error("invalid configuration file: {}", .reasons.join(", "))] InvalidConfig { reasons: Vec }, @@ -23,116 +24,137 @@ pub enum StartupError { BindError(#[from] BindError), } -#[derive(Debug, Error)] +#[derive(Debug, Error, SlogInlineError)] pub enum SpCommsError { #[error("discovery process not yet complete")] DiscoveryNotYetComplete, #[error("location discovery failed: {reason}")] DiscoveryFailed { reason: String }, - #[error("nonexistent SP (type {:?}, slot {})", .0.typ, .0.slot)] + #[error("nonexistent SP {0:?}")] SpDoesNotExist(SpIdentifier), - #[error( - "unknown socket address for SP (type {:?}, slot {})", - .0.typ, - .0.slot, - )] + #[error("unknown socket address for SP {0:?}")] SpAddressUnknown(SpIdentifier), #[error( "timeout ({timeout:?}) elapsed communicating with {sp:?} on port {port}" )] Timeout { timeout: Duration, port: usize, sp: Option }, - #[error("error communicating with SP: {0}")] - SpCommunicationFailed(#[from] CommunicationError), - #[error("updating SP failed: {0}")] - UpdateFailed(#[from] UpdateError), + #[error("error communicating with SP {sp:?}")] + SpCommunicationFailed { + sp: SpIdentifier, + #[source] + err: CommunicationError, + }, + #[error("updating SP {sp:?} failed")] + UpdateFailed { + sp: SpIdentifier, + #[source] + err: UpdateError, + }, } impl From for HttpError { - fn from(err: SpCommsError) -> Self { - match err { + fn from(error: SpCommsError) -> Self { + match error { SpCommsError::SpDoesNotExist(_) => HttpError::for_bad_request( Some("InvalidSp".to_string()), - format!("{:#}", anyhow!(err)), + InlineErrorChain::new(&error).to_string(), ), - SpCommsError::SpCommunicationFailed( - CommunicationError::SpError( - SpError::SerialConsoleAlreadyAttached, - ), - ) => HttpError::for_bad_request( + SpCommsError::SpCommunicationFailed { + err: + CommunicationError::SpError( + SpError::SerialConsoleAlreadyAttached, + ), + .. + } => HttpError::for_bad_request( Some("SerialConsoleAttached".to_string()), - format!("{:#}", anyhow!(err)), + InlineErrorChain::new(&error).to_string(), ), - SpCommsError::SpCommunicationFailed( - CommunicationError::SpError(SpError::RequestUnsupportedForSp), - ) => HttpError::for_bad_request( + SpCommsError::SpCommunicationFailed { + err: + CommunicationError::SpError(SpError::RequestUnsupportedForSp), + .. + } => HttpError::for_bad_request( Some("RequestUnsupportedForSp".to_string()), - format!("{:#}", anyhow!(err)), + InlineErrorChain::new(&error).to_string(), ), - SpCommsError::SpCommunicationFailed( - CommunicationError::SpError( - SpError::RequestUnsupportedForComponent, - ), - ) => HttpError::for_bad_request( + SpCommsError::SpCommunicationFailed { + err: + CommunicationError::SpError( + SpError::RequestUnsupportedForComponent, + ), + .. + } => HttpError::for_bad_request( Some("RequestUnsupportedForComponent".to_string()), - format!("{:#}", anyhow!(err)), + InlineErrorChain::new(&error).to_string(), ), - SpCommsError::SpCommunicationFailed( - CommunicationError::SpError(SpError::InvalidSlotForComponent), - ) => HttpError::for_bad_request( + SpCommsError::SpCommunicationFailed { + err: + CommunicationError::SpError(SpError::InvalidSlotForComponent), + .. + } => HttpError::for_bad_request( Some("InvalidSlotForComponent".to_string()), - format!("{:#}", anyhow!(err)), + InlineErrorChain::new(&error).to_string(), ), - SpCommsError::UpdateFailed(UpdateError::ImageTooLarge) => { - HttpError::for_bad_request( - Some("ImageTooLarge".to_string()), - format!("{:#}", anyhow!(err)), - ) - } - SpCommsError::UpdateFailed(UpdateError::Communication( - CommunicationError::SpError(SpError::UpdateSlotBusy), - )) => http_err_with_message( + SpCommsError::UpdateFailed { + err: UpdateError::ImageTooLarge, + .. + } => HttpError::for_bad_request( + Some("ImageTooLarge".to_string()), + InlineErrorChain::new(&error).to_string(), + ), + SpCommsError::UpdateFailed { + err: + UpdateError::Communication(CommunicationError::SpError( + SpError::UpdateSlotBusy, + )), + .. + } => http_err_with_message( http::StatusCode::SERVICE_UNAVAILABLE, "UpdateSlotBusy", - format!("{:#}", anyhow!(err)), + InlineErrorChain::new(&error).to_string(), ), - SpCommsError::UpdateFailed(UpdateError::Communication( - CommunicationError::SpError(SpError::UpdateInProgress { - .. - }), - )) => http_err_with_message( + SpCommsError::UpdateFailed { + err: + UpdateError::Communication(CommunicationError::SpError( + SpError::UpdateInProgress { .. }, + )), + .. + } => http_err_with_message( http::StatusCode::SERVICE_UNAVAILABLE, "UpdateInProgress", - format!("{:#}", anyhow!(err)), + InlineErrorChain::new(&error).to_string(), ), SpCommsError::DiscoveryNotYetComplete => http_err_with_message( http::StatusCode::SERVICE_UNAVAILABLE, "DiscoveryNotYetComplete", - format!("{:#}", anyhow!(err)), + InlineErrorChain::new(&error).to_string(), ), SpCommsError::SpAddressUnknown(_) => http_err_with_message( http::StatusCode::SERVICE_UNAVAILABLE, "SpAddressUnknown", - format!("{:#}", anyhow!(err)), + InlineErrorChain::new(&error).to_string(), ), SpCommsError::DiscoveryFailed { .. } => http_err_with_message( http::StatusCode::SERVICE_UNAVAILABLE, "DiscoveryFailed ", - format!("{:#}", anyhow!(err)), + InlineErrorChain::new(&error).to_string(), ), SpCommsError::Timeout { .. } => http_err_with_message( http::StatusCode::SERVICE_UNAVAILABLE, "Timeout ", - format!("{:#}", anyhow!(err)), + InlineErrorChain::new(&error).to_string(), ), - SpCommsError::SpCommunicationFailed(_) => http_err_with_message( - http::StatusCode::SERVICE_UNAVAILABLE, - "SpCommunicationFailed", - format!("{:#}", anyhow!(err)), - ), - SpCommsError::UpdateFailed(_) => http_err_with_message( + SpCommsError::SpCommunicationFailed { .. } => { + http_err_with_message( + http::StatusCode::SERVICE_UNAVAILABLE, + "SpCommunicationFailed", + InlineErrorChain::new(&error).to_string(), + ) + } + SpCommsError::UpdateFailed { .. } => http_err_with_message( http::StatusCode::SERVICE_UNAVAILABLE, "UpdateFailed", - format!("{:#}", anyhow!(err)), + InlineErrorChain::new(&error).to_string(), ), } } diff --git a/gateway/src/http_entrypoints.rs b/gateway/src/http_entrypoints.rs index 2db6121f1d..e33e8dd4a6 100644 --- a/gateway/src/http_entrypoints.rs +++ b/gateway/src/http_entrypoints.rs @@ -566,10 +566,12 @@ async fn sp_get( path: Path, ) -> Result, HttpError> { let apictx = rqctx.context(); - let sp_id = path.into_inner().sp; - let sp = apictx.mgmt_switch.sp(sp_id.into())?; + let sp_id = path.into_inner().sp.into(); + let sp = apictx.mgmt_switch.sp(sp_id)?; - let state = sp.state().await.map_err(SpCommsError::from)?; + let state = sp.state().await.map_err(|err| { + SpCommsError::SpCommunicationFailed { sp: sp_id, err } + })?; Ok(HttpResponseOk(state.into())) } @@ -588,9 +590,12 @@ async fn sp_startup_options_get( ) -> Result, HttpError> { let apictx = rqctx.context(); let mgmt_switch = &apictx.mgmt_switch; - let sp = mgmt_switch.sp(path.into_inner().sp.into())?; + let sp_id = path.into_inner().sp.into(); + let sp = mgmt_switch.sp(sp_id)?; - let options = sp.get_startup_options().await.map_err(SpCommsError::from)?; + let options = sp.get_startup_options().await.map_err(|err| { + SpCommsError::SpCommunicationFailed { sp: sp_id, err } + })?; Ok(HttpResponseOk(options.into())) } @@ -610,11 +615,12 @@ async fn sp_startup_options_set( ) -> Result { let apictx = rqctx.context(); let mgmt_switch = &apictx.mgmt_switch; - let sp = mgmt_switch.sp(path.into_inner().sp.into())?; + let sp_id = path.into_inner().sp.into(); + let sp = mgmt_switch.sp(sp_id)?; - sp.set_startup_options(body.into_inner().into()) - .await - .map_err(SpCommsError::from)?; + sp.set_startup_options(body.into_inner().into()).await.map_err(|err| { + SpCommsError::SpCommunicationFailed { sp: sp_id, err } + })?; Ok(HttpResponseUpdatedNoContent {}) } @@ -632,8 +638,11 @@ async fn sp_component_list( path: Path, ) -> Result, HttpError> { let apictx = rqctx.context(); - let sp = apictx.mgmt_switch.sp(path.into_inner().sp.into())?; - let inventory = sp.inventory().await.map_err(SpCommsError::from)?; + let sp_id = path.into_inner().sp.into(); + let sp = apictx.mgmt_switch.sp(sp_id)?; + let inventory = sp.inventory().await.map_err(|err| { + SpCommsError::SpCommunicationFailed { sp: sp_id, err } + })?; Ok(HttpResponseOk(inventory.into())) } @@ -653,11 +662,13 @@ async fn sp_component_get( ) -> Result>, HttpError> { let apictx = rqctx.context(); let PathSpComponent { sp, component } = path.into_inner(); - let sp = apictx.mgmt_switch.sp(sp.into())?; + let sp_id = sp.into(); + let sp = apictx.mgmt_switch.sp(sp_id)?; let component = component_from_str(&component)?; - let details = - sp.component_details(component).await.map_err(SpCommsError::from)?; + let details = sp.component_details(component).await.map_err(|err| { + SpCommsError::SpCommunicationFailed { sp: sp_id, err } + })?; Ok(HttpResponseOk(details.entries.into_iter().map(Into::into).collect())) } @@ -690,7 +701,8 @@ async fn sp_component_caboose_get( let apictx = rqctx.context(); let PathSpComponent { sp, component } = path.into_inner(); - let sp = apictx.mgmt_switch.sp(sp.into())?; + let sp_id = sp.into(); + let sp = apictx.mgmt_switch.sp(sp_id)?; let ComponentCabooseSlot { firmware_slot } = query_params.into_inner(); let component = component_from_str(&component)?; @@ -714,19 +726,31 @@ async fn sp_component_caboose_get( CABOOSE_KEY_GIT_COMMIT, ) .await - .map_err(SpCommsError::from)?; + .map_err(|err| SpCommsError::SpCommunicationFailed { + sp: sp_id, + err, + })?; let board = sp .read_component_caboose(component, firmware_slot, CABOOSE_KEY_BOARD) .await - .map_err(SpCommsError::from)?; + .map_err(|err| SpCommsError::SpCommunicationFailed { + sp: sp_id, + err, + })?; let name = sp .read_component_caboose(component, firmware_slot, CABOOSE_KEY_NAME) .await - .map_err(SpCommsError::from)?; + .map_err(|err| SpCommsError::SpCommunicationFailed { + sp: sp_id, + err, + })?; let version = sp .read_component_caboose(component, firmware_slot, CABOOSE_KEY_VERSION) .await - .map_err(SpCommsError::from)?; + .map_err(|err| SpCommsError::SpCommunicationFailed { + sp: sp_id, + err, + })?; let git_commit = from_utf8(&CABOOSE_KEY_GIT_COMMIT, git_commit)?; let board = from_utf8(&CABOOSE_KEY_BOARD, board)?; @@ -752,10 +776,13 @@ async fn sp_component_clear_status( ) -> Result { let apictx = rqctx.context(); let PathSpComponent { sp, component } = path.into_inner(); - let sp = apictx.mgmt_switch.sp(sp.into())?; + let sp_id = sp.into(); + let sp = apictx.mgmt_switch.sp(sp_id)?; let component = component_from_str(&component)?; - sp.component_clear_status(component).await.map_err(SpCommsError::from)?; + sp.component_clear_status(component).await.map_err(|err| { + SpCommsError::SpCommunicationFailed { sp: sp_id, err } + })?; Ok(HttpResponseUpdatedNoContent {}) } @@ -775,13 +802,13 @@ async fn sp_component_active_slot_get( ) -> Result, HttpError> { let apictx = rqctx.context(); let PathSpComponent { sp, component } = path.into_inner(); - let sp = apictx.mgmt_switch.sp(sp.into())?; + let sp_id = sp.into(); + let sp = apictx.mgmt_switch.sp(sp_id)?; let component = component_from_str(&component)?; - let slot = sp - .component_active_slot(component) - .await - .map_err(SpCommsError::from)?; + let slot = sp.component_active_slot(component).await.map_err(|err| { + SpCommsError::SpCommunicationFailed { sp: sp_id, err } + })?; Ok(HttpResponseOk(SpComponentFirmwareSlot { slot })) } @@ -809,14 +836,15 @@ async fn sp_component_active_slot_set( ) -> Result { let apictx = rqctx.context(); let PathSpComponent { sp, component } = path.into_inner(); - let sp = apictx.mgmt_switch.sp(sp.into())?; + let sp_id = sp.into(); + let sp = apictx.mgmt_switch.sp(sp_id)?; let component = component_from_str(&component)?; let slot = body.into_inner().slot; let persist = query_params.into_inner().persist; - sp.set_component_active_slot(component, slot, persist) - .await - .map_err(SpCommsError::from)?; + sp.set_component_active_slot(component, slot, persist).await.map_err( + |err| SpCommsError::SpCommunicationFailed { sp: sp_id, err }, + )?; Ok(HttpResponseUpdatedNoContent {}) } @@ -843,21 +871,27 @@ async fn sp_component_serial_console_attach( ) -> WebsocketEndpointResult { let apictx = rqctx.context(); let PathSpComponent { sp, component } = path.into_inner(); + let sp_id = sp.into(); let component = component_from_str(&component)?; // Ensure we can attach to this SP's serial console. let console = apictx .mgmt_switch - .sp(sp.into())? + .sp(sp_id)? .serial_console_attach(component) .await - .map_err(SpCommsError::from)?; + .map_err(|err| SpCommsError::SpCommunicationFailed { + sp: sp_id, + err, + })?; let log = apictx.log.new(slog::o!("sp" => format!("{sp:?}"))); // We've successfully attached to the SP's serial console: upgrade the // websocket and run our side of that connection. - websocket.handle(move |conn| crate::serial_console::run(console, conn, log)) + websocket.handle(move |conn| { + crate::serial_console::run(sp_id, console, conn, log) + }) } /// Detach the websocket connection attached to the given SP component's serial @@ -875,9 +909,12 @@ async fn sp_component_serial_console_detach( // TODO-cleanup: "component" support for the serial console is half baked; // we don't use it at all to detach. let PathSpComponent { sp, component: _ } = path.into_inner(); + let sp_id = sp.into(); - let sp = apictx.mgmt_switch.sp(sp.into())?; - sp.serial_console_detach().await.map_err(SpCommsError::from)?; + let sp = apictx.mgmt_switch.sp(sp_id)?; + sp.serial_console_detach().await.map_err(|err| { + SpCommsError::SpCommunicationFailed { sp: sp_id, err } + })?; Ok(HttpResponseUpdatedNoContent {}) } @@ -927,13 +964,17 @@ async fn sp_component_reset( ) -> Result { let apictx = rqctx.context(); let PathSpComponent { sp, component } = path.into_inner(); - let sp = apictx.mgmt_switch.sp(sp.into())?; + let sp_id = sp.into(); + let sp = apictx.mgmt_switch.sp(sp_id)?; let component = component_from_str(&component)?; sp.reset_component_prepare(component) .and_then(|()| sp.reset_component_trigger(component)) .await - .map_err(SpCommsError::from)?; + .map_err(|err| SpCommsError::SpCommunicationFailed { + sp: sp_id, + err, + })?; Ok(HttpResponseUpdatedNoContent {}) } @@ -964,7 +1005,8 @@ async fn sp_component_update( let apictx = rqctx.context(); let PathSpComponent { sp, component } = path.into_inner(); - let sp = apictx.mgmt_switch.sp(sp.into())?; + let sp_id = sp.into(); + let sp = apictx.mgmt_switch.sp(sp_id)?; let component = component_from_str(&component)?; let ComponentUpdateIdSlot { id, firmware_slot } = query_params.into_inner(); @@ -973,7 +1015,7 @@ async fn sp_component_update( sp.start_update(component, id, firmware_slot, image) .await - .map_err(SpCommsError::from)?; + .map_err(|err| SpCommsError::UpdateFailed { sp: sp_id, err })?; Ok(HttpResponseUpdatedNoContent {}) } @@ -993,11 +1035,13 @@ async fn sp_component_update_status( let apictx = rqctx.context(); let PathSpComponent { sp, component } = path.into_inner(); - let sp = apictx.mgmt_switch.sp(sp.into())?; + let sp_id = sp.into(); + let sp = apictx.mgmt_switch.sp(sp_id)?; let component = component_from_str(&component)?; - let status = - sp.update_status(component).await.map_err(SpCommsError::from)?; + let status = sp.update_status(component).await.map_err(|err| { + SpCommsError::SpCommunicationFailed { sp: sp_id, err } + })?; Ok(HttpResponseOk(status.into())) } @@ -1020,11 +1064,14 @@ async fn sp_component_update_abort( let apictx = rqctx.context(); let PathSpComponent { sp, component } = path.into_inner(); - let sp = apictx.mgmt_switch.sp(sp.into())?; + let sp_id = sp.into(); + let sp = apictx.mgmt_switch.sp(sp_id)?; let component = component_from_str(&component)?; let UpdateAbortBody { id } = body.into_inner(); - sp.update_abort(component, id).await.map_err(SpCommsError::from)?; + sp.update_abort(component, id).await.map_err(|err| { + SpCommsError::SpCommunicationFailed { sp: sp_id, err } + })?; Ok(HttpResponseUpdatedNoContent {}) } @@ -1043,6 +1090,7 @@ async fn sp_rot_cmpa_get( let apictx = rqctx.context(); let PathSpComponent { sp, component } = path.into_inner(); + let sp_id = sp.into(); // Ensure the caller knows they're asking for the RoT if component_from_str(&component)? != SpComponent::ROT { @@ -1052,8 +1100,10 @@ async fn sp_rot_cmpa_get( )); } - let sp = apictx.mgmt_switch.sp(sp.into())?; - let data = sp.read_rot_cmpa().await.map_err(SpCommsError::from)?; + let sp = apictx.mgmt_switch.sp(sp_id)?; + let data = sp.read_rot_cmpa().await.map_err(|err| { + SpCommsError::SpCommunicationFailed { sp: sp_id, err } + })?; let base64_data = base64::engine::general_purpose::STANDARD.encode(data); @@ -1076,6 +1126,7 @@ async fn sp_rot_cfpa_get( let PathSpComponent { sp, component } = path.into_inner(); let GetCfpaParams { slot } = params.into_inner(); + let sp_id = sp.into(); // Ensure the caller knows they're asking for the RoT if component_from_str(&component)? != SpComponent::ROT { @@ -1085,13 +1136,13 @@ async fn sp_rot_cfpa_get( )); } - let sp = apictx.mgmt_switch.sp(sp.into())?; + let sp = apictx.mgmt_switch.sp(sp_id)?; let data = match slot { RotCfpaSlot::Active => sp.read_rot_active_cfpa().await, RotCfpaSlot::Inactive => sp.read_rot_inactive_cfpa().await, RotCfpaSlot::Scratch => sp.read_rot_scratch_cfpa().await, } - .map_err(SpCommsError::from)?; + .map_err(|err| SpCommsError::SpCommunicationFailed { sp: sp_id, err })?; let base64_data = base64::engine::general_purpose::STANDARD.encode(data); @@ -1141,16 +1192,19 @@ async fn ignition_get( let apictx = rqctx.context(); let mgmt_switch = &apictx.mgmt_switch; - let sp = path.into_inner().sp; - let ignition_target = mgmt_switch.ignition_target(sp.into())?; + let sp_id = path.into_inner().sp.into(); + let ignition_target = mgmt_switch.ignition_target(sp_id)?; let state = mgmt_switch .ignition_controller() .ignition_state(ignition_target) .await - .map_err(SpCommsError::from)?; + .map_err(|err| SpCommsError::SpCommunicationFailed { + sp: sp_id, + err, + })?; - let info = SpIgnitionInfo { id: sp, details: state.into() }; + let info = SpIgnitionInfo { id: sp_id.into(), details: state.into() }; Ok(HttpResponseOk(info)) } @@ -1173,13 +1227,17 @@ async fn ignition_command( let apictx = rqctx.context(); let mgmt_switch = &apictx.mgmt_switch; let PathSpIgnitionCommand { sp, command } = path.into_inner(); - let ignition_target = mgmt_switch.ignition_target(sp.into())?; + let sp_id = sp.into(); + let ignition_target = mgmt_switch.ignition_target(sp_id)?; mgmt_switch .ignition_controller() .ignition_command(ignition_target, command.into()) .await - .map_err(SpCommsError::from)?; + .map_err(|err| SpCommsError::SpCommunicationFailed { + sp: sp_id, + err, + })?; Ok(HttpResponseUpdatedNoContent {}) } @@ -1197,9 +1255,12 @@ async fn sp_power_state_get( path: Path, ) -> Result, HttpError> { let apictx = rqctx.context(); - let sp = apictx.mgmt_switch.sp(path.into_inner().sp.into())?; + let sp_id = path.into_inner().sp.into(); + let sp = apictx.mgmt_switch.sp(sp_id)?; - let power_state = sp.power_state().await.map_err(SpCommsError::from)?; + let power_state = sp.power_state().await.map_err(|err| { + SpCommsError::SpCommunicationFailed { sp: sp_id, err } + })?; Ok(HttpResponseOk(power_state.into())) } @@ -1218,10 +1279,13 @@ async fn sp_power_state_set( body: TypedBody, ) -> Result { let apictx = rqctx.context(); - let sp = apictx.mgmt_switch.sp(path.into_inner().sp.into())?; + let sp_id = path.into_inner().sp.into(); + let sp = apictx.mgmt_switch.sp(sp_id)?; let power_state = body.into_inner(); - sp.set_power_state(power_state.into()).await.map_err(SpCommsError::from)?; + sp.set_power_state(power_state.into()).await.map_err(|err| { + SpCommsError::SpCommunicationFailed { sp: sp_id, err } + })?; Ok(HttpResponseUpdatedNoContent {}) } @@ -1241,7 +1305,8 @@ async fn sp_installinator_image_id_set( use ipcc_key_value::Key; let apictx = rqctx.context(); - let sp = apictx.mgmt_switch.sp(path.into_inner().sp.into())?; + let sp_id = path.into_inner().sp.into(); + let sp = apictx.mgmt_switch.sp(sp_id)?; let image_id = ipcc_key_value::InstallinatorImageId::from(body.into_inner()); @@ -1251,7 +1316,7 @@ async fn sp_installinator_image_id_set( image_id.serialize(), ) .await - .map_err(SpCommsError::from)?; + .map_err(|err| SpCommsError::SpCommunicationFailed { sp: sp_id, err })?; Ok(HttpResponseUpdatedNoContent {}) } @@ -1268,12 +1333,16 @@ async fn sp_installinator_image_id_delete( use ipcc_key_value::Key; let apictx = rqctx.context(); - let sp = apictx.mgmt_switch.sp(path.into_inner().sp.into())?; + let sp_id = path.into_inner().sp.into(); + let sp = apictx.mgmt_switch.sp(sp_id)?; // We clear the image ID by setting it to a 0-length vec. sp.set_ipcc_key_lookup_value(Key::InstallinatorImageId as u8, Vec::new()) .await - .map_err(SpCommsError::from)?; + .map_err(|err| SpCommsError::SpCommunicationFailed { + sp: sp_id, + err, + })?; Ok(HttpResponseUpdatedNoContent {}) } diff --git a/gateway/src/lib.rs b/gateway/src/lib.rs index 10fcf1539c..5aa833f6e2 100644 --- a/gateway/src/lib.rs +++ b/gateway/src/lib.rs @@ -35,6 +35,7 @@ use slog::info; use slog::o; use slog::warn; use slog::Logger; +use slog_error_chain::InlineErrorChain; use std::collections::hash_map::Entry; use std::collections::HashMap; use std::mem; @@ -138,7 +139,10 @@ impl Server { match gateway_sp_comms::register_probes() { Ok(_) => debug!(log, "successfully registered DTrace USDT probes"), Err(err) => { - warn!(log, "failed to register DTrace USDT probes: {}", err); + warn!( + log, "failed to register DTrace USDT probes"; + InlineErrorChain::new(&err), + ); } } @@ -328,9 +332,9 @@ pub async fn start_server( ); let log = slog::Logger::root(drain.fuse(), slog::o!(FileKv)); if let slog_dtrace::ProbeRegistration::Failed(e) = registration { - let msg = format!("failed to register DTrace probes: {}", e); - error!(log, "{}", msg); - return Err(msg); + let err = InlineErrorChain::new(&e); + error!(log, "failed to register DTrace probes"; &err); + return Err(format!("failed to register DTrace probes: {err}")); } else { debug!(log, "registered DTrace probes"); } diff --git a/gateway/src/management_switch.rs b/gateway/src/management_switch.rs index 03fdda2cca..0571dc051e 100644 --- a/gateway/src/management_switch.rs +++ b/gateway/src/management_switch.rs @@ -383,7 +383,14 @@ impl ManagementSwitch { > { let controller = self.ignition_controller(); let location_map = self.location_map()?; - let bulk_state = controller.bulk_ignition_state().await?; + let bulk_state = + controller.bulk_ignition_state().await.map_err(|err| { + SpCommsError::SpCommunicationFailed { + sp: location_map + .port_to_id(self.local_ignition_controller_port), + err, + } + })?; Ok(bulk_state.into_iter().enumerate().filter_map(|(target, state)| { // If the SP returns an ignition target we don't have a port @@ -402,11 +409,8 @@ impl ManagementSwitch { None => { warn!( self.log, - concat!( - "ignoring unknown ignition target {}", - " returned by ignition controller SP" - ), - target, + "ignoring unknown ignition target {target} \ + returned by ignition controller SP", ); None } diff --git a/gateway/src/serial_console.rs b/gateway/src/serial_console.rs index 3e49f8526a..49aa807e55 100644 --- a/gateway/src/serial_console.rs +++ b/gateway/src/serial_console.rs @@ -5,6 +5,7 @@ // Copyright 2022 Oxide Computer Company use crate::error::SpCommsError; +use crate::SpIdentifier; use dropshot::WebsocketChannelResult; use dropshot::WebsocketConnection; use futures::stream::SplitSink; @@ -19,6 +20,7 @@ use slog::error; use slog::info; use slog::warn; use slog::Logger; +use slog_error_chain::SlogInlineError; use std::borrow::Cow; use std::ops::Deref; use std::ops::DerefMut; @@ -34,7 +36,7 @@ use tokio_tungstenite::tungstenite::protocol::WebSocketConfig; use tokio_tungstenite::tungstenite::Message; use tokio_tungstenite::WebSocketStream; -#[derive(Debug, thiserror::Error)] +#[derive(Debug, thiserror::Error, SlogInlineError)] enum SerialTaskError { #[error(transparent)] SpCommsError(#[from] SpCommsError), @@ -43,6 +45,7 @@ enum SerialTaskError { } pub(crate) async fn run( + sp: SpIdentifier, console: AttachedSerialConsole, conn: WebsocketConnection, log: Logger, @@ -80,7 +83,7 @@ pub(crate) async fn run( let (console_tx, mut console_rx) = console.split(); let console_tx = DetachOnDrop::new(console_tx); let mut ws_recv_handle = - tokio::spawn(ws_recv_task(ws_stream, console_tx, log.clone())); + tokio::spawn(ws_recv_task(sp, ws_stream, console_tx, log.clone())); loop { tokio::select! { @@ -112,7 +115,9 @@ pub(crate) async fn run( Ok(()) => (), Err(TrySendError::Full(data)) => { warn!( - log, "channel full; discarding serial console data from SP"; + log, + "channel full; discarding serial \ + console data from SP"; "length" => data.len(), ); } @@ -160,6 +165,7 @@ async fn ws_sink_task( } async fn ws_recv_task( + sp: SpIdentifier, mut ws_stream: SplitStream>, mut console_tx: DetachOnDrop, log: Logger, @@ -175,7 +181,7 @@ async fn ws_recv_task( console_tx .write(data) .await - .map_err(SpCommsError::from)?; + .map_err(|err| SpCommsError::SpCommunicationFailed { sp, err })?; keepalive.reset(); } Some(Ok(Message::Close(_))) | None => { @@ -194,7 +200,7 @@ async fn ws_recv_task( } _= keepalive.tick() => { - console_tx.keepalive().await.map_err(SpCommsError::from)?; + console_tx.keepalive().await.map_err(|err| SpCommsError::SpCommunicationFailed { sp, err })?; } } } From 3382a33887a42db409bf3b8c780cd4125cc35f51 Mon Sep 17 00:00:00 2001 From: Adam Leventhal Date: Tue, 19 Dec 2023 12:34:20 -0800 Subject: [PATCH 17/33] fix Name regex in json schema (#4718) --- common/src/api/external/mod.rs | 2 +- common/tests/output/pagination-schema.txt | 4 ++-- openapi/bootstrap-agent.json | 2 +- openapi/nexus-internal.json | 4 ++-- openapi/nexus.json | 4 ++-- openapi/sled-agent.json | 2 +- schema/all-zone-requests.json | 2 +- schema/all-zones-requests.json | 2 +- schema/rss-service-plan-v2.json | 2 +- schema/rss-sled-plan.json | 2 +- 10 files changed, 13 insertions(+), 13 deletions(-) diff --git a/common/src/api/external/mod.rs b/common/src/api/external/mod.rs index aa783ac9ca..446152137a 100644 --- a/common/src/api/external/mod.rs +++ b/common/src/api/external/mod.rs @@ -316,7 +316,7 @@ impl JsonSchema for Name { r#"^"#, // Cannot match a UUID r#"(?![0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)"#, - r#"^[a-z][a-z0-9-]*[a-zA-Z0-9]*"#, + r#"^[a-z]([a-zA-Z0-9-]*[a-zA-Z0-9]+)?"#, r#"$"#, ) .to_string(), diff --git a/common/tests/output/pagination-schema.txt b/common/tests/output/pagination-schema.txt index 7cbaf439d6..436e614994 100644 --- a/common/tests/output/pagination-schema.txt +++ b/common/tests/output/pagination-schema.txt @@ -139,7 +139,7 @@ schema for pagination parameters: page selector, scan by name only "type": "string", "maxLength": 63, "minLength": 1, - "pattern": "^(?![0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)^[a-z][a-z0-9-]*[a-zA-Z0-9]*$" + "pattern": "^(?![0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)^[a-z]([a-zA-Z0-9-]*[a-zA-Z0-9]+)?$" }, "NameSortMode": { "description": "Supported set of sort modes for scanning by name only\n\nCurrently, we only support scanning in ascending order.", @@ -228,7 +228,7 @@ schema for pagination parameters: page selector, scan by name or id "type": "string", "maxLength": 63, "minLength": 1, - "pattern": "^(?![0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)^[a-z][a-z0-9-]*[a-zA-Z0-9]*$" + "pattern": "^(?![0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)^[a-z]([a-zA-Z0-9-]*[a-zA-Z0-9]+)?$" }, "NameOrId": { "oneOf": [ diff --git a/openapi/bootstrap-agent.json b/openapi/bootstrap-agent.json index 0c5bd15050..2a7ff43202 100644 --- a/openapi/bootstrap-agent.json +++ b/openapi/bootstrap-agent.json @@ -491,7 +491,7 @@ "title": "A name unique within the parent collection", "description": "Names must begin with a lower case ASCII letter, be composed exclusively of lowercase ASCII, uppercase ASCII, numbers, and '-', and may not end with a '-'. Names cannot be a UUID though they may contain a UUID.", "type": "string", - "pattern": "^(?![0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)^[a-z][a-z0-9-]*[a-zA-Z0-9]*$", + "pattern": "^(?![0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)^[a-z]([a-zA-Z0-9-]*[a-zA-Z0-9]+)?$", "minLength": 1, "maxLength": 63 }, diff --git a/openapi/nexus-internal.json b/openapi/nexus-internal.json index f909710ab4..a1d70d838b 100644 --- a/openapi/nexus-internal.json +++ b/openapi/nexus-internal.json @@ -4268,7 +4268,7 @@ "title": "A name unique within the parent collection", "description": "Names must begin with a lower case ASCII letter, be composed exclusively of lowercase ASCII, uppercase ASCII, numbers, and '-', and may not end with a '-'. Names cannot be a UUID though they may contain a UUID.", "type": "string", - "pattern": "^(?![0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)^[a-z][a-z0-9-]*[a-zA-Z0-9]*$", + "pattern": "^(?![0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)^[a-z]([a-zA-Z0-9-]*[a-zA-Z0-9]+)?$", "minLength": 1, "maxLength": 63 }, @@ -5578,7 +5578,7 @@ "title": "A name unique within the parent collection", "description": "Names must begin with a lower case ASCII letter, be composed exclusively of lowercase ASCII, uppercase ASCII, numbers, and '-', and may not end with a '-'. Names cannot be a UUID though they may contain a UUID.", "type": "string", - "pattern": "^(?![0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)^[a-z][a-z0-9-]*[a-zA-Z0-9]*$", + "pattern": "^(?![0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)^[a-z]([a-zA-Z0-9-]*[a-zA-Z0-9]+)?$", "minLength": 1, "maxLength": 63 }, diff --git a/openapi/nexus.json b/openapi/nexus.json index 4c89706a1c..35586375e8 100644 --- a/openapi/nexus.json +++ b/openapi/nexus.json @@ -12812,7 +12812,7 @@ "title": "A name unique within the parent collection", "description": "Names must begin with a lower case ASCII letter, be composed exclusively of lowercase ASCII, uppercase ASCII, numbers, and '-', and may not end with a '-'. Names cannot be a UUID though they may contain a UUID.", "type": "string", - "pattern": "^(?![0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)^[a-z][a-z0-9-]*[a-zA-Z0-9]*$", + "pattern": "^(?![0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)^[a-z]([a-zA-Z0-9-]*[a-zA-Z0-9]+)?$", "minLength": 1, "maxLength": 63 }, @@ -15020,7 +15020,7 @@ "title": "A name unique within the parent collection", "description": "Names must begin with a lower case ASCII letter, be composed exclusively of lowercase ASCII, uppercase ASCII, numbers, and '-', and may not end with a '-'. Names cannot be a UUID though they may contain a UUID.", "type": "string", - "pattern": "^(?![0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)^[a-z][a-z0-9-]*[a-zA-Z0-9]*$", + "pattern": "^(?![0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)^[a-z]([a-zA-Z0-9-]*[a-zA-Z0-9]+)?$", "minLength": 1, "maxLength": 63 }, diff --git a/openapi/sled-agent.json b/openapi/sled-agent.json index d71f8de644..6076df6dbb 100644 --- a/openapi/sled-agent.json +++ b/openapi/sled-agent.json @@ -5246,7 +5246,7 @@ "title": "A name unique within the parent collection", "description": "Names must begin with a lower case ASCII letter, be composed exclusively of lowercase ASCII, uppercase ASCII, numbers, and '-', and may not end with a '-'. Names cannot be a UUID though they may contain a UUID.", "type": "string", - "pattern": "^(?![0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)^[a-z][a-z0-9-]*[a-zA-Z0-9]*$", + "pattern": "^(?![0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)^[a-z]([a-zA-Z0-9-]*[a-zA-Z0-9]+)?$", "minLength": 1, "maxLength": 63 }, diff --git a/schema/all-zone-requests.json b/schema/all-zone-requests.json index 4eb56d379d..8c324a15bd 100644 --- a/schema/all-zone-requests.json +++ b/schema/all-zone-requests.json @@ -210,7 +210,7 @@ "type": "string", "maxLength": 63, "minLength": 1, - "pattern": "^(?![0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)^[a-z][a-z0-9-]*[a-zA-Z0-9]*$" + "pattern": "^(?![0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)^[a-z]([a-zA-Z0-9-]*[a-zA-Z0-9]+)?$" }, "NetworkInterface": { "description": "Information required to construct a virtual network interface", diff --git a/schema/all-zones-requests.json b/schema/all-zones-requests.json index 0e43e9ee21..7a07e2f9ae 100644 --- a/schema/all-zones-requests.json +++ b/schema/all-zones-requests.json @@ -94,7 +94,7 @@ "type": "string", "maxLength": 63, "minLength": 1, - "pattern": "^(?![0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)^[a-z][a-z0-9-]*[a-zA-Z0-9]*$" + "pattern": "^(?![0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)^[a-z]([a-zA-Z0-9-]*[a-zA-Z0-9]+)?$" }, "NetworkInterface": { "description": "Information required to construct a virtual network interface", diff --git a/schema/rss-service-plan-v2.json b/schema/rss-service-plan-v2.json index 0bcd27b9cc..62ce358938 100644 --- a/schema/rss-service-plan-v2.json +++ b/schema/rss-service-plan-v2.json @@ -179,7 +179,7 @@ "type": "string", "maxLength": 63, "minLength": 1, - "pattern": "^(?![0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)^[a-z][a-z0-9-]*[a-zA-Z0-9]*$" + "pattern": "^(?![0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)^[a-z]([a-zA-Z0-9-]*[a-zA-Z0-9]+)?$" }, "NetworkInterface": { "description": "Information required to construct a virtual network interface", diff --git a/schema/rss-sled-plan.json b/schema/rss-sled-plan.json index 2ef7a7b58a..0396ccc685 100644 --- a/schema/rss-sled-plan.json +++ b/schema/rss-sled-plan.json @@ -355,7 +355,7 @@ "type": "string", "maxLength": 63, "minLength": 1, - "pattern": "^(?![0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)^[a-z][a-z0-9-]*[a-zA-Z0-9]*$" + "pattern": "^(?![0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)^[a-z]([a-zA-Z0-9-]*[a-zA-Z0-9]+)?$" }, "NewPasswordHash": { "title": "A password hash in PHC string format", From 6783a5af9361a41840959fbb614d4bbd064b4f45 Mon Sep 17 00:00:00 2001 From: "Andrew J. Stone" Date: Tue, 19 Dec 2023 18:32:26 -0500 Subject: [PATCH 18/33] Fix fake pagination for sled_list_uninitialized (#4720) --- nexus/src/external_api/http_entrypoints.rs | 8 +++++++ openapi/nexus.json | 25 ++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index 8a4aeaeff5..3e38558760 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -4659,8 +4659,16 @@ async fn rack_view( }] async fn sled_list_uninitialized( rqctx: RequestContext>, + query: Query>, ) -> Result>, HttpError> { let apictx = rqctx.context(); + // We don't actually support real pagination + let pag_params = query.into_inner(); + if let dropshot::WhichPage::Next(last_seen) = &pag_params.page { + return Err( + Error::invalid_value(last_seen.clone(), "bad page token").into() + ); + } let handler = async { let nexus = &apictx.nexus; let opctx = crate::context::op_context_for_external_api(&rqctx).await?; diff --git a/openapi/nexus.json b/openapi/nexus.json index 35586375e8..4131460149 100644 --- a/openapi/nexus.json +++ b/openapi/nexus.json @@ -4022,6 +4022,28 @@ ], "summary": "List uninitialized sleds in a given rack", "operationId": "sled_list_uninitialized", + "parameters": [ + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + } + ], "responses": { "200": { "description": "successful operation", @@ -4039,6 +4061,9 @@ "5XX": { "$ref": "#/components/responses/Error" } + }, + "x-dropshot-pagination": { + "required": [] } } }, From f2fb5af6e3c86fc231768f6faf58281b912a33e4 Mon Sep 17 00:00:00 2001 From: James MacMahon Date: Tue, 19 Dec 2023 22:07:16 -0500 Subject: [PATCH 19/33] Deserialize pre-6.0.0 RegionSnapshot objects (#4721) Schema update 6.0.0 added the `deleted` column to the region_snapshot table and added the `deleted` field to the RegionSnapshot object. If an old RegionSnapshot was serialized before this schema update (as part of a volume delete) into the `resources_to_clean_up` column of the volume table, _and_ if that volume delete failed and unwound, Nexus will fail to deserialize that column after that schema update + model change if there is another request to delete that volume. Add `#[serde(default)]` to RegionSnapshot's deleting field so that Nexus can deserialize pre-6.0.0 RegionSnapshot objects. This will default to `false` which matches what the ALTER COLUMN's default setting was in the 6.0.0 schema upgrade. Fixes oxidecomputer/customer-support#72 --- nexus/db-model/src/region_snapshot.rs | 10 +- nexus/db-queries/src/db/datastore/volume.rs | 115 ++++++++++++++++++++ 2 files changed, 122 insertions(+), 3 deletions(-) diff --git a/nexus/db-model/src/region_snapshot.rs b/nexus/db-model/src/region_snapshot.rs index af1cf8b2b3..2ea59f99f0 100644 --- a/nexus/db-model/src/region_snapshot.rs +++ b/nexus/db-model/src/region_snapshot.rs @@ -27,12 +27,16 @@ pub struct RegionSnapshot { pub region_id: Uuid, pub snapshot_id: Uuid, - // used for identifying volumes that reference this + /// used for identifying volumes that reference this pub snapshot_addr: String, - // how many volumes reference this? + /// how many volumes reference this? pub volume_references: i64, - // true if part of a volume's `resources_to_clean_up` already + /// true if part of a volume's `resources_to_clean_up` already + // this column was added in `schema/crdb/6.0.0/up1.sql` with a default of + // false, so instruct serde to deserialize default as false if an old + // serialized version of RegionSnapshot is being deserialized. + #[serde(default)] pub deleting: bool, } diff --git a/nexus/db-queries/src/db/datastore/volume.rs b/nexus/db-queries/src/db/datastore/volume.rs index 4f31efd610..d0b093ff45 100644 --- a/nexus/db-queries/src/db/datastore/volume.rs +++ b/nexus/db-queries/src/db/datastore/volume.rs @@ -1059,3 +1059,118 @@ pub fn read_only_resources_associated_with_volume( } } } + +#[cfg(test)] +mod tests { + use super::*; + + use crate::db::datastore::datastore_test; + use nexus_test_utils::db::test_setup_database; + use omicron_test_utils::dev; + + // Assert that Nexus will not fail to deserialize an old version of + // CrucibleResources that was serialized before schema update 6.0.0. + #[tokio::test] + async fn test_deserialize_old_crucible_resources() { + let logctx = + dev::test_setup_log("test_deserialize_old_crucible_resources"); + let log = logctx.log.new(o!()); + let mut db = test_setup_database(&log).await; + let (_opctx, db_datastore) = datastore_test(&logctx, &db).await; + + // Start with a fake volume, doesn't matter if it's empty + + let volume_id = Uuid::new_v4(); + let _volume = db_datastore + .volume_create(nexus_db_model::Volume::new( + volume_id, + serde_json::to_string(&VolumeConstructionRequest::Volume { + id: volume_id, + block_size: 512, + sub_volumes: vec![], + read_only_parent: None, + }) + .unwrap(), + )) + .await + .unwrap(); + + // Add old CrucibleResources json in the `resources_to_clean_up` column - + // this was before the `deleting` column / field was added to + // ResourceSnapshot. + + { + use db::schema::volume::dsl; + + let conn = + db_datastore.pool_connection_unauthorized().await.unwrap(); + + let resources_to_clean_up = r#"{ + "V1": { + "datasets_and_regions": [], + "datasets_and_snapshots": [ + [ + { + "identity": { + "id": "844ee8d5-7641-4b04-bca8-7521e258028a", + "time_created": "2023-12-19T21:38:34.000000Z", + "time_modified": "2023-12-19T21:38:34.000000Z" + }, + "time_deleted": null, + "rcgen": 1, + "pool_id": "81a98506-4a97-4d92-8de5-c21f6fc71649", + "ip": "fd00:1122:3344:101::1", + "port": 32345, + "kind": "Crucible", + "size_used": 10737418240 + }, + { + "dataset_id": "b69edd77-1b3e-4f11-978c-194a0a0137d0", + "region_id": "8d668bf9-68cc-4387-8bc0-b4de7ef9744f", + "snapshot_id": "f548332c-6026-4eff-8c1c-ba202cd5c834", + "snapshot_addr": "[fd00:1122:3344:101::2]:19001", + "volume_references": 0 + } + ] + ] + } +} +"#; + + diesel::update(dsl::volume) + .filter(dsl::id.eq(volume_id)) + .set(dsl::resources_to_clean_up.eq(resources_to_clean_up)) + .execute_async(&*conn) + .await + .unwrap(); + } + + // Soft delete the volume, which runs the CTE + + let cr = db_datastore + .decrease_crucible_resource_count_and_soft_delete_volume(volume_id) + .await + .unwrap(); + + // Assert the contents of the returned CrucibleResources + + let datasets_and_regions = + db_datastore.regions_to_delete(&cr).await.unwrap(); + let datasets_and_snapshots = + db_datastore.snapshots_to_delete(&cr).await.unwrap(); + + assert!(datasets_and_regions.is_empty()); + assert_eq!(datasets_and_snapshots.len(), 1); + + let region_snapshot = &datasets_and_snapshots[0].1; + + assert_eq!( + region_snapshot.snapshot_id, + "f548332c-6026-4eff-8c1c-ba202cd5c834".parse().unwrap() + ); + assert_eq!(region_snapshot.deleting, false); + + db.cleanup().await.unwrap(); + logctx.cleanup_successful(); + } +} From 94944ccf5349479c9f0d1235fe504f2570253474 Mon Sep 17 00:00:00 2001 From: Sean Klein Date: Tue, 19 Dec 2023 22:56:26 -0800 Subject: [PATCH 20/33] Remove lazy_static in favor of once_cell (#4699) Fixes https://github.com/oxidecomputer/omicron/issues/4697 --- Cargo.lock | 7 +- Cargo.toml | 1 - common/Cargo.toml | 2 +- common/src/address.rs | 108 +- nexus/Cargo.toml | 1 - nexus/db-queries/Cargo.toml | 2 +- nexus/db-queries/src/authn/external/spoof.rs | 29 +- nexus/db-queries/src/authz/api_resources.rs | 7 +- .../db-queries/src/db/datastore/silo_user.rs | 10 +- nexus/db-queries/src/db/fixed_data/mod.rs | 15 +- nexus/db-queries/src/db/fixed_data/project.rs | 20 +- .../src/db/fixed_data/role_assignment.rs | 13 +- .../src/db/fixed_data/role_builtin.rs | 34 +- nexus/db-queries/src/db/fixed_data/silo.rs | 104 +- .../db-queries/src/db/fixed_data/silo_user.rs | 54 +- .../src/db/fixed_data/user_builtin.rs | 106 +- nexus/db-queries/src/db/fixed_data/vpc.rs | 42 +- .../src/db/fixed_data/vpc_firewall_rule.rs | 73 +- .../src/db/fixed_data/vpc_subnet.rs | 53 +- .../src/db/queries/network_interface.rs | 60 +- nexus/db-queries/src/db/saga_recovery.rs | 12 +- nexus/defaults/Cargo.toml | 2 +- nexus/defaults/src/lib.rs | 88 +- nexus/src/external_api/console_api.rs | 12 +- nexus/tests/integration_tests/endpoints.rs | 1256 +++++++++-------- nexus/tests/integration_tests/unauthorized.rs | 32 +- 26 files changed, 1155 insertions(+), 988 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 74c01d3411..962fe68e02 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4164,7 +4164,6 @@ dependencies = [ "internal-dns", "ipnetwork", "itertools 0.12.0", - "lazy_static", "macaddr", "newtype_derive", "nexus-db-model", @@ -4177,6 +4176,7 @@ dependencies = [ "omicron-sled-agent", "omicron-test-utils", "omicron-workspace-hack", + "once_cell", "openapiv3", "openssl", "oso", @@ -4213,9 +4213,9 @@ name = "nexus-defaults" version = "0.1.0" dependencies = [ "ipnetwork", - "lazy_static", "omicron-common", "omicron-workspace-hack", + "once_cell", "rand 0.8.5", "serde_json", ] @@ -4618,10 +4618,10 @@ dependencies = [ "hex", "http", "ipnetwork", - "lazy_static", "libc", "macaddr", "omicron-workspace-hack", + "once_cell", "parse-display", "progenitor", "proptest", @@ -4773,7 +4773,6 @@ dependencies = [ "internal-dns", "ipnetwork", "itertools 0.12.0", - "lazy_static", "macaddr", "mg-admin-client", "mime_guess", diff --git a/Cargo.toml b/Cargo.toml index ca134536f5..d651a13bf1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -229,7 +229,6 @@ ipnetwork = { version = "0.20", features = ["schemars"] } itertools = "0.12.0" key-manager = { path = "key-manager" } kstat-rs = "0.2.3" -lazy_static = "1.4.0" libc = "0.2.151" linear-map = "1.2.0" macaddr = { version = "1.0.1", features = ["serde_std"] } diff --git a/common/Cargo.toml b/common/Cargo.toml index 49997e619c..3941f5303e 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -17,7 +17,6 @@ hex.workspace = true http.workspace = true ipnetwork.workspace = true macaddr.workspace = true -lazy_static.workspace = true proptest = { workspace = true, optional = true } rand.workspace = true reqwest = { workspace = true, features = ["rustls-tls", "stream"] } @@ -38,6 +37,7 @@ uuid.workspace = true parse-display.workspace = true progenitor.workspace = true omicron-workspace-hack.workspace = true +once_cell.workspace = true [dev-dependencies] camino-tempfile.workspace = true diff --git a/common/src/address.rs b/common/src/address.rs index 992e8f0406..94361a2705 100644 --- a/common/src/address.rs +++ b/common/src/address.rs @@ -9,6 +9,7 @@ use crate::api::external::{self, Error, Ipv4Net, Ipv6Net}; use ipnetwork::{Ipv4Network, Ipv6Network}; +use once_cell::sync::Lazy; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddrV6}; @@ -76,65 +77,78 @@ pub const NTP_PORT: u16 = 123; // that situation (which may be as soon as allocating ephemeral IPs). pub const NUM_SOURCE_NAT_PORTS: u16 = 1 << 14; -lazy_static::lazy_static! { - // Services that require external connectivity are given an OPTE port - // with a "Service VNIC" record. Like a "Guest VNIC", a service is - // placed within a VPC (a built-in services VPC), along with a VPC subnet. - // But unlike guest instances which are created at runtime by Nexus, these - // services are created by RSS early on. So, we have some fixed values - // used to bootstrap service OPTE ports. Each service kind uses a distinct - // VPC subnet which RSS will allocate addresses from for those services. - // The specific values aren't deployment-specific as they are virtualized - // within OPTE. - - /// The IPv6 prefix assigned to the built-in services VPC. - // The specific prefix here was randomly chosen from the expected VPC - // prefix range (`fd00::/48`). See `random_vpc_ipv6_prefix`. - // Furthermore, all the below *_OPTE_IPV6_SUBNET constants are - // /64's within this prefix. - pub static ref SERVICE_VPC_IPV6_PREFIX: Ipv6Net = Ipv6Net( +// Services that require external connectivity are given an OPTE port +// with a "Service VNIC" record. Like a "Guest VNIC", a service is +// placed within a VPC (a built-in services VPC), along with a VPC subnet. +// But unlike guest instances which are created at runtime by Nexus, these +// services are created by RSS early on. So, we have some fixed values +// used to bootstrap service OPTE ports. Each service kind uses a distinct +// VPC subnet which RSS will allocate addresses from for those services. +// The specific values aren't deployment-specific as they are virtualized +// within OPTE. + +/// The IPv6 prefix assigned to the built-in services VPC. +// The specific prefix here was randomly chosen from the expected VPC +// prefix range (`fd00::/48`). See `random_vpc_ipv6_prefix`. +// Furthermore, all the below *_OPTE_IPV6_SUBNET constants are +// /64's within this prefix. +pub static SERVICE_VPC_IPV6_PREFIX: Lazy = Lazy::new(|| { + Ipv6Net( Ipv6Network::new( Ipv6Addr::new(0xfd77, 0xe9d2, 0x9cd9, 0, 0, 0, 0, 0), Ipv6Net::VPC_IPV6_PREFIX_LENGTH, - ).unwrap(), - ); - - /// The IPv4 subnet for External DNS OPTE ports. - pub static ref DNS_OPTE_IPV4_SUBNET: Ipv4Net = - Ipv4Net(Ipv4Network::new(Ipv4Addr::new(172, 30, 1, 0), 24).unwrap()); - - /// The IPv6 subnet for External DNS OPTE ports. - pub static ref DNS_OPTE_IPV6_SUBNET: Ipv6Net = Ipv6Net( + ) + .unwrap(), + ) +}); + +/// The IPv4 subnet for External DNS OPTE ports. +pub static DNS_OPTE_IPV4_SUBNET: Lazy = Lazy::new(|| { + Ipv4Net(Ipv4Network::new(Ipv4Addr::new(172, 30, 1, 0), 24).unwrap()) +}); + +/// The IPv6 subnet for External DNS OPTE ports. +pub static DNS_OPTE_IPV6_SUBNET: Lazy = Lazy::new(|| { + Ipv6Net( Ipv6Network::new( Ipv6Addr::new(0xfd77, 0xe9d2, 0x9cd9, 1, 0, 0, 0, 0), Ipv6Net::VPC_SUBNET_IPV6_PREFIX_LENGTH, - ).unwrap(), - ); - - /// The IPv4 subnet for Nexus OPTE ports. - pub static ref NEXUS_OPTE_IPV4_SUBNET: Ipv4Net = - Ipv4Net(Ipv4Network::new(Ipv4Addr::new(172, 30, 2, 0), 24).unwrap()); - - /// The IPv6 subnet for Nexus OPTE ports. - pub static ref NEXUS_OPTE_IPV6_SUBNET: Ipv6Net = Ipv6Net( + ) + .unwrap(), + ) +}); + +/// The IPv4 subnet for Nexus OPTE ports. +pub static NEXUS_OPTE_IPV4_SUBNET: Lazy = Lazy::new(|| { + Ipv4Net(Ipv4Network::new(Ipv4Addr::new(172, 30, 2, 0), 24).unwrap()) +}); + +/// The IPv6 subnet for Nexus OPTE ports. +pub static NEXUS_OPTE_IPV6_SUBNET: Lazy = Lazy::new(|| { + Ipv6Net( Ipv6Network::new( Ipv6Addr::new(0xfd77, 0xe9d2, 0x9cd9, 2, 0, 0, 0, 0), Ipv6Net::VPC_SUBNET_IPV6_PREFIX_LENGTH, - ).unwrap(), - ); - - /// The IPv4 subnet for Boundary NTP OPTE ports. - pub static ref NTP_OPTE_IPV4_SUBNET: Ipv4Net = - Ipv4Net(Ipv4Network::new(Ipv4Addr::new(172, 30, 3, 0), 24).unwrap()); - - /// The IPv6 subnet for Boundary NTP OPTE ports. - pub static ref NTP_OPTE_IPV6_SUBNET: Ipv6Net = Ipv6Net( + ) + .unwrap(), + ) +}); + +/// The IPv4 subnet for Boundary NTP OPTE ports. +pub static NTP_OPTE_IPV4_SUBNET: Lazy = Lazy::new(|| { + Ipv4Net(Ipv4Network::new(Ipv4Addr::new(172, 30, 3, 0), 24).unwrap()) +}); + +/// The IPv6 subnet for Boundary NTP OPTE ports. +pub static NTP_OPTE_IPV6_SUBNET: Lazy = Lazy::new(|| { + Ipv6Net( Ipv6Network::new( Ipv6Addr::new(0xfd77, 0xe9d2, 0x9cd9, 3, 0, 0, 0, 0), Ipv6Net::VPC_SUBNET_IPV6_PREFIX_LENGTH, - ).unwrap(), - ); -} + ) + .unwrap(), + ) +}); // Anycast is a mechanism in which a single IP address is shared by multiple // devices, and the destination is located based on routing distance. diff --git a/nexus/Cargo.toml b/nexus/Cargo.toml index 704a7ab7bd..25833ec104 100644 --- a/nexus/Cargo.toml +++ b/nexus/Cargo.toml @@ -32,7 +32,6 @@ http.workspace = true hyper.workspace = true internal-dns.workspace = true ipnetwork.workspace = true -lazy_static.workspace = true macaddr.workspace = true mime_guess.workspace = true # Not under "dev-dependencies"; these also need to be implemented for diff --git a/nexus/db-queries/Cargo.toml b/nexus/db-queries/Cargo.toml index 9d8afd1fea..d5320be733 100644 --- a/nexus/db-queries/Cargo.toml +++ b/nexus/db-queries/Cargo.toml @@ -24,9 +24,9 @@ headers.workspace = true http.workspace = true hyper.workspace = true ipnetwork.workspace = true -lazy_static.workspace = true macaddr.workspace = true newtype_derive.workspace = true +once_cell.workspace = true openssl.workspace = true oso.workspace = true paste.workspace = true diff --git a/nexus/db-queries/src/authn/external/spoof.rs b/nexus/db-queries/src/authn/external/spoof.rs index 0b5896a6f8..9b5ed94bde 100644 --- a/nexus/db-queries/src/authn/external/spoof.rs +++ b/nexus/db-queries/src/authn/external/spoof.rs @@ -16,7 +16,7 @@ use anyhow::Context; use async_trait::async_trait; use headers::authorization::{Authorization, Bearer}; use headers::HeaderMapExt; -use lazy_static::lazy_static; +use once_cell::sync::Lazy; use uuid::Uuid; // This scheme is intended for demos, development, and testing until we have a @@ -54,18 +54,21 @@ const SPOOF_RESERVED_BAD_CREDS: &str = "this-fake-ID-it-is-truly-excellent"; // subsets of the base64 character set, so we do not bother encoding them. const SPOOF_PREFIX: &str = "oxide-spoof-"; -lazy_static! { - /// Actor (id) used for the special "bad credentials" error - static ref SPOOF_RESERVED_BAD_CREDS_ACTOR: Actor = Actor::UserBuiltin { - user_builtin_id: "22222222-2222-2222-2222-222222222222".parse().unwrap(), - }; - /// Complete HTTP header value to trigger the "bad actor" error - pub static ref SPOOF_HEADER_BAD_ACTOR: Authorization = - make_header_value_str(SPOOF_RESERVED_BAD_ACTOR).unwrap(); - /// Complete HTTP header value to trigger the "bad creds" error - pub static ref SPOOF_HEADER_BAD_CREDS: Authorization = - make_header_value_str(SPOOF_RESERVED_BAD_CREDS).unwrap(); -} +/// Actor (id) used for the special "bad credentials" error +static SPOOF_RESERVED_BAD_CREDS_ACTOR: Lazy = + Lazy::new(|| Actor::UserBuiltin { + user_builtin_id: "22222222-2222-2222-2222-222222222222" + .parse() + .unwrap(), + }); + +/// Complete HTTP header value to trigger the "bad actor" error +pub static SPOOF_HEADER_BAD_ACTOR: Lazy> = + Lazy::new(|| make_header_value_str(SPOOF_RESERVED_BAD_ACTOR).unwrap()); + +/// Complete HTTP header value to trigger the "bad creds" error +pub static SPOOF_HEADER_BAD_CREDS: Lazy> = + Lazy::new(|| make_header_value_str(SPOOF_RESERVED_BAD_CREDS).unwrap()); /// Implements a (test-only) authentication scheme where the client simply /// provides the actor information in a custom bearer token and we always trust diff --git a/nexus/db-queries/src/authz/api_resources.rs b/nexus/db-queries/src/authz/api_resources.rs index 2dfe2f7174..8485b8f11f 100644 --- a/nexus/db-queries/src/authz/api_resources.rs +++ b/nexus/db-queries/src/authz/api_resources.rs @@ -42,9 +42,9 @@ use crate::db::DataStore; use authz_macros::authz_resource; use futures::future::BoxFuture; use futures::FutureExt; -use lazy_static::lazy_static; use nexus_types::external_api::shared::{FleetRole, ProjectRole, SiloRole}; use omicron_common::api::external::{Error, LookupType, ResourceType}; +use once_cell::sync::Lazy; use oso::PolarClass; use serde::{Deserialize, Serialize}; use uuid::Uuid; @@ -169,9 +169,8 @@ pub struct Fleet; /// Singleton representing the [`Fleet`] itself for authz purposes pub const FLEET: Fleet = Fleet; -lazy_static! { - pub static ref FLEET_LOOKUP: LookupType = LookupType::ById(*FLEET_ID); -} +pub static FLEET_LOOKUP: Lazy = + Lazy::new(|| LookupType::ById(*FLEET_ID)); impl Eq for Fleet {} impl PartialEq for Fleet { diff --git a/nexus/db-queries/src/db/datastore/silo_user.rs b/nexus/db-queries/src/db/datastore/silo_user.rs index 6084f8c2ab..59cb19a609 100644 --- a/nexus/db-queries/src/db/datastore/silo_user.rs +++ b/nexus/db-queries/src/db/datastore/silo_user.rs @@ -363,11 +363,11 @@ impl DataStore { let builtin_users = [ // Note: "db_init" is also a builtin user, but that one by necessity // is created with the database. - &*authn::USER_SERVICE_BALANCER, - &*authn::USER_INTERNAL_API, - &*authn::USER_INTERNAL_READ, - &*authn::USER_EXTERNAL_AUTHN, - &*authn::USER_SAGA_RECOVERY, + &authn::USER_SERVICE_BALANCER, + &authn::USER_INTERNAL_API, + &authn::USER_INTERNAL_READ, + &authn::USER_EXTERNAL_AUTHN, + &authn::USER_SAGA_RECOVERY, ] .iter() .map(|u| { diff --git a/nexus/db-queries/src/db/fixed_data/mod.rs b/nexus/db-queries/src/db/fixed_data/mod.rs index 5c91407134..4f896eb5d1 100644 --- a/nexus/db-queries/src/db/fixed_data/mod.rs +++ b/nexus/db-queries/src/db/fixed_data/mod.rs @@ -31,7 +31,7 @@ // 001de000-074c built-in services vpc // 001de000-c470 built-in services vpc subnets -use lazy_static::lazy_static; +use once_cell::sync::Lazy; pub mod project; pub mod role_assignment; @@ -43,13 +43,12 @@ pub mod vpc; pub mod vpc_firewall_rule; pub mod vpc_subnet; -lazy_static! { - /* See above for where this uuid comes from. */ - pub static ref FLEET_ID: uuid::Uuid = - "001de000-1334-4000-8000-000000000000" - .parse() - .expect("invalid uuid for builtin fleet id"); -} +/* See above for where this uuid comes from. */ +pub static FLEET_ID: Lazy = Lazy::new(|| { + "001de000-1334-4000-8000-000000000000" + .parse() + .expect("invalid uuid for builtin fleet id") +}); #[cfg(test)] fn assert_valid_uuid(id: &uuid::Uuid) { diff --git a/nexus/db-queries/src/db/fixed_data/project.rs b/nexus/db-queries/src/db/fixed_data/project.rs index 52450438c0..e240900e0c 100644 --- a/nexus/db-queries/src/db/fixed_data/project.rs +++ b/nexus/db-queries/src/db/fixed_data/project.rs @@ -4,18 +4,20 @@ use crate::db; use crate::db::datastore::SERVICES_DB_NAME; -use lazy_static::lazy_static; use nexus_types::external_api::params; use omicron_common::api::external::IdentityMetadataCreateParams; +use once_cell::sync::Lazy; -lazy_static! { - /// UUID of built-in project for internal services on the rack. - pub static ref SERVICES_PROJECT_ID: uuid::Uuid = "001de000-4401-4000-8000-000000000000" +/// UUID of built-in project for internal services on the rack. +pub static SERVICES_PROJECT_ID: Lazy = Lazy::new(|| { + "001de000-4401-4000-8000-000000000000" .parse() - .expect("invalid uuid for builtin services project id"); + .expect("invalid uuid for builtin services project id") +}); - /// Built-in Project for internal services on the rack. - pub static ref SERVICES_PROJECT: db::model::Project = db::model::Project::new_with_id( +/// Built-in Project for internal services on the rack. +pub static SERVICES_PROJECT: Lazy = Lazy::new(|| { + db::model::Project::new_with_id( *SERVICES_PROJECT_ID, *super::silo::INTERNAL_SILO_ID, params::ProjectCreate { @@ -24,5 +26,5 @@ lazy_static! { description: "Built-in project for Oxide Services".to_string(), }, }, - ); -} + ) +}); diff --git a/nexus/db-queries/src/db/fixed_data/role_assignment.rs b/nexus/db-queries/src/db/fixed_data/role_assignment.rs index 7d7ddffab6..d6c95d47b6 100644 --- a/nexus/db-queries/src/db/fixed_data/role_assignment.rs +++ b/nexus/db-queries/src/db/fixed_data/role_assignment.rs @@ -8,10 +8,10 @@ use super::user_builtin; use super::FLEET_ID; use crate::db::model::IdentityType; use crate::db::model::RoleAssignment; -use lazy_static::lazy_static; +use once_cell::sync::Lazy; -lazy_static! { - pub static ref BUILTIN_ROLE_ASSIGNMENTS: Vec = +pub static BUILTIN_ROLE_ASSIGNMENTS: Lazy> = + Lazy::new(|| { vec![ // The "internal-api" user gets the "admin" role on the sole Fleet. // This is a pretty elevated privilege. @@ -24,7 +24,6 @@ lazy_static! { *FLEET_ID, role_builtin::FLEET_ADMIN.role_name, ), - // The "USER_SERVICE_BALANCER" user gets the "admin" role on the // Fleet. // @@ -38,7 +37,6 @@ lazy_static! { *FLEET_ID, role_builtin::FLEET_ADMIN.role_name, ), - // The "internal-read" user gets the "viewer" role on the sole // Fleet. This will grant them the ability to read various control // plane data (like the list of sleds), which is in turn used to @@ -50,7 +48,6 @@ lazy_static! { *FLEET_ID, role_builtin::FLEET_VIEWER.role_name, ), - // The "external-authenticator" user gets the "authenticator" role // on the sole fleet. This grants them the ability to create // sessions. @@ -61,5 +58,5 @@ lazy_static! { *FLEET_ID, role_builtin::FLEET_AUTHENTICATOR.role_name, ), - ]; -} + ] + }); diff --git a/nexus/db-queries/src/db/fixed_data/role_builtin.rs b/nexus/db-queries/src/db/fixed_data/role_builtin.rs index 865f6328f4..f58077fc3f 100644 --- a/nexus/db-queries/src/db/fixed_data/role_builtin.rs +++ b/nexus/db-queries/src/db/fixed_data/role_builtin.rs @@ -3,8 +3,8 @@ // file, You can obtain one at https://mozilla.org/MPL/2.0/. //! Built-in roles -use lazy_static::lazy_static; use omicron_common::api; +use once_cell::sync::Lazy; #[derive(Clone, Debug)] pub struct RoleBuiltinConfig { @@ -13,28 +13,36 @@ pub struct RoleBuiltinConfig { pub description: &'static str, } -lazy_static! { - pub static ref FLEET_ADMIN: RoleBuiltinConfig = RoleBuiltinConfig { +pub static FLEET_ADMIN: Lazy = + Lazy::new(|| RoleBuiltinConfig { resource_type: api::external::ResourceType::Fleet, role_name: "admin", description: "Fleet Administrator", - }; - pub static ref FLEET_AUTHENTICATOR: RoleBuiltinConfig = RoleBuiltinConfig { + }); + +pub static FLEET_AUTHENTICATOR: Lazy = + Lazy::new(|| RoleBuiltinConfig { resource_type: api::external::ResourceType::Fleet, role_name: "external-authenticator", description: "Fleet External Authenticator", - }; - pub static ref FLEET_VIEWER: RoleBuiltinConfig = RoleBuiltinConfig { + }); + +pub static FLEET_VIEWER: Lazy = + Lazy::new(|| RoleBuiltinConfig { resource_type: api::external::ResourceType::Fleet, role_name: "viewer", description: "Fleet Viewer", - }; - pub static ref SILO_ADMIN: RoleBuiltinConfig = RoleBuiltinConfig { + }); + +pub static SILO_ADMIN: Lazy = + Lazy::new(|| RoleBuiltinConfig { resource_type: api::external::ResourceType::Silo, role_name: "admin", description: "Silo Administrator", - }; - pub static ref BUILTIN_ROLES: Vec = vec![ + }); + +pub static BUILTIN_ROLES: Lazy> = Lazy::new(|| { + vec![ FLEET_ADMIN.clone(), FLEET_AUTHENTICATOR.clone(), FLEET_VIEWER.clone(), @@ -69,8 +77,8 @@ lazy_static! { role_name: "viewer", description: "Project Viewer", }, - ]; -} + ] +}); #[cfg(test)] mod test { diff --git a/nexus/db-queries/src/db/fixed_data/silo.rs b/nexus/db-queries/src/db/fixed_data/silo.rs index 6eba849ee3..62bcc61c1e 100644 --- a/nexus/db-queries/src/db/fixed_data/silo.rs +++ b/nexus/db-queries/src/db/fixed_data/silo.rs @@ -3,62 +3,66 @@ // file, You can obtain one at https://mozilla.org/MPL/2.0/. use crate::db; -use lazy_static::lazy_static; use nexus_types::external_api::{params, shared}; use omicron_common::api::external::IdentityMetadataCreateParams; +use once_cell::sync::Lazy; -lazy_static! { - pub static ref SILO_ID: uuid::Uuid = "001de000-5110-4000-8000-000000000000" +pub static SILO_ID: Lazy = Lazy::new(|| { + "001de000-5110-4000-8000-000000000000" .parse() - .expect("invalid uuid for builtin silo id"); + .expect("invalid uuid for builtin silo id") +}); - /// "Default" Silo - /// - /// This was historically used for demos and the unit tests. The plan is to - /// remove it per omicron#2305. - pub static ref DEFAULT_SILO: db::model::Silo = - db::model::Silo::new_with_id( - *SILO_ID, - params::SiloCreate { - identity: IdentityMetadataCreateParams { - name: "default-silo".parse().unwrap(), - description: "default silo".to_string(), - }, - // This quota is actually _unused_ because the default silo - // isn't constructed in the same way a normal silo would be. - quotas: params::SiloQuotasCreate::empty(), - discoverable: false, - identity_mode: shared::SiloIdentityMode::LocalOnly, - admin_group_name: None, - tls_certificates: vec![], - mapped_fleet_roles: Default::default(), +/// "Default" Silo +/// +/// This was historically used for demos and the unit tests. The plan is to +/// remove it per omicron#2305. +pub static DEFAULT_SILO: Lazy = Lazy::new(|| { + db::model::Silo::new_with_id( + *SILO_ID, + params::SiloCreate { + identity: IdentityMetadataCreateParams { + name: "default-silo".parse().unwrap(), + description: "default silo".to_string(), }, - ) - .unwrap(); + // This quota is actually _unused_ because the default silo + // isn't constructed in the same way a normal silo would be. + quotas: params::SiloQuotasCreate::empty(), + discoverable: false, + identity_mode: shared::SiloIdentityMode::LocalOnly, + admin_group_name: None, + tls_certificates: vec![], + mapped_fleet_roles: Default::default(), + }, + ) + .unwrap() +}); - /// UUID of built-in internal silo. - pub static ref INTERNAL_SILO_ID: uuid::Uuid = - "001de000-5110-4000-8000-000000000001" - .parse() - .expect("invalid uuid for builtin silo id"); +/// UUID of built-in internal silo. +pub static INTERNAL_SILO_ID: Lazy = Lazy::new(|| { + "001de000-5110-4000-8000-000000000001" + .parse() + .expect("invalid uuid for builtin silo id") +}); - /// Built-in Silo to house internal resources. It contains no users and - /// can't be logged into. - pub static ref INTERNAL_SILO: db::model::Silo = - db::model::Silo::new_with_id( - *INTERNAL_SILO_ID, - params::SiloCreate { - identity: IdentityMetadataCreateParams { - name: "oxide-internal".parse().unwrap(), - description: "Built-in internal Silo.".to_string(), - }, - // The internal silo contains no virtual resources, so it has no allotted capacity. - quotas: params::SiloQuotasCreate::empty(), - discoverable: false, - identity_mode: shared::SiloIdentityMode::LocalOnly, - admin_group_name: None, - tls_certificates: vec![], - mapped_fleet_roles: Default::default(), +/// Built-in Silo to house internal resources. It contains no users and +/// can't be logged into. +pub static INTERNAL_SILO: Lazy = Lazy::new(|| { + db::model::Silo::new_with_id( + *INTERNAL_SILO_ID, + params::SiloCreate { + identity: IdentityMetadataCreateParams { + name: "oxide-internal".parse().unwrap(), + description: "Built-in internal Silo.".to_string(), }, - ).unwrap(); -} + // The internal silo contains no virtual resources, so it has no allotted capacity. + quotas: params::SiloQuotasCreate::empty(), + discoverable: false, + identity_mode: shared::SiloIdentityMode::LocalOnly, + admin_group_name: None, + tls_certificates: vec![], + mapped_fleet_roles: Default::default(), + }, + ) + .unwrap() +}); diff --git a/nexus/db-queries/src/db/fixed_data/silo_user.rs b/nexus/db-queries/src/db/fixed_data/silo_user.rs index d54bcfa59f..b5253b68e3 100644 --- a/nexus/db-queries/src/db/fixed_data/silo_user.rs +++ b/nexus/db-queries/src/db/fixed_data/silo_user.rs @@ -6,25 +6,26 @@ use super::role_builtin; use crate::db; use crate::db::identity::Asset; -use lazy_static::lazy_static; +use once_cell::sync::Lazy; -lazy_static! { - /// Test user that's granted all privileges, used for automated testing and - /// local development - // TODO-security Once we have a way to bootstrap the initial Silo with the - // initial privileged user, this user should be created in the test suite, - // not automatically at Nexus startup. See omicron#2305. - pub static ref USER_TEST_PRIVILEGED: db::model::SiloUser = - db::model::SiloUser::new( - *db::fixed_data::silo::SILO_ID, - // "4007" looks a bit like "root". - "001de000-05e4-4000-8000-000000004007".parse().unwrap(), - "privileged".into(), - ); +/// Test user that's granted all privileges, used for automated testing and +/// local development +// TODO-security Once we have a way to bootstrap the initial Silo with the +// initial privileged user, this user should be created in the test suite, +// not automatically at Nexus startup. See omicron#2305. +pub static USER_TEST_PRIVILEGED: Lazy = Lazy::new(|| { + db::model::SiloUser::new( + *db::fixed_data::silo::SILO_ID, + // "4007" looks a bit like "root". + "001de000-05e4-4000-8000-000000004007".parse().unwrap(), + "privileged".into(), + ) +}); - /// Role assignments needed for the privileged user - pub static ref ROLE_ASSIGNMENTS_PRIVILEGED: - Vec = vec![ +/// Role assignments needed for the privileged user +pub static ROLE_ASSIGNMENTS_PRIVILEGED: Lazy> = + Lazy::new(|| { + vec![ // The "test-privileged" user gets the "admin" role on the sole // Fleet as well as the default Silo. db::model::RoleAssignment::new( @@ -34,7 +35,6 @@ lazy_static! { *db::fixed_data::FLEET_ID, role_builtin::FLEET_ADMIN.role_name, ), - db::model::RoleAssignment::new( db::model::IdentityType::SiloUser, USER_TEST_PRIVILEGED.id(), @@ -42,20 +42,22 @@ lazy_static! { *db::fixed_data::silo::SILO_ID, role_builtin::SILO_ADMIN.role_name, ), - ]; + ] + }); - /// Test user that's granted no privileges, used for automated testing - // TODO-security Once we have a way to bootstrap the initial Silo with the - // initial privileged user, this user should be created in the test suite, - // not automatically at Nexus startup. See omicron#2305. - pub static ref USER_TEST_UNPRIVILEGED: db::model::SiloUser = +/// Test user that's granted no privileges, used for automated testing +// TODO-security Once we have a way to bootstrap the initial Silo with the +// initial privileged user, this user should be created in the test suite, +// not automatically at Nexus startup. See omicron#2305. +pub static USER_TEST_UNPRIVILEGED: Lazy = + Lazy::new(|| { db::model::SiloUser::new( *db::fixed_data::silo::SILO_ID, // 60001 is the decimal uid for "nobody" on Helios. "001de000-05e4-4000-8000-000000060001".parse().unwrap(), "unprivileged".into(), - ); -} + ) + }); #[cfg(test)] mod test { diff --git a/nexus/db-queries/src/db/fixed_data/user_builtin.rs b/nexus/db-queries/src/db/fixed_data/user_builtin.rs index 87f33fa355..1e96802683 100644 --- a/nexus/db-queries/src/db/fixed_data/user_builtin.rs +++ b/nexus/db-queries/src/db/fixed_data/user_builtin.rs @@ -3,8 +3,8 @@ // file, You can obtain one at https://mozilla.org/MPL/2.0/. //! Built-in users -use lazy_static::lazy_static; use omicron_common::api; +use once_cell::sync::Lazy; use uuid::Uuid; pub struct UserBuiltinConfig { @@ -27,61 +27,65 @@ impl UserBuiltinConfig { } } -lazy_static! { - /// Internal user used for seeding initial database data - // NOTE: This uuid and name are duplicated in dbinit.sql. - pub static ref USER_DB_INIT: UserBuiltinConfig = - UserBuiltinConfig::new_static( - // "0001" is the first possible user that wouldn't be confused with - // 0, or root. - "001de000-05e4-4000-8000-000000000001", - "db-init", - "used for seeding initial database data", - ); +/// Internal user used for seeding initial database data +// NOTE: This uuid and name are duplicated in dbinit.sql. +pub static USER_DB_INIT: Lazy = Lazy::new(|| { + UserBuiltinConfig::new_static( + // "0001" is the first possible user that wouldn't be confused with + // 0, or root. + "001de000-05e4-4000-8000-000000000001", + "db-init", + "used for seeding initial database data", + ) +}); - /// Internal user for performing operations to manage the - /// provisioning of services across the fleet. - pub static ref USER_SERVICE_BALANCER: UserBuiltinConfig = - UserBuiltinConfig::new_static( - "001de000-05e4-4000-8000-00000000bac3", - "service-balancer", - "used for Nexus-driven service balancing", - ); +/// Internal user for performing operations to manage the +/// provisioning of services across the fleet. +pub static USER_SERVICE_BALANCER: Lazy = Lazy::new(|| { + UserBuiltinConfig::new_static( + "001de000-05e4-4000-8000-00000000bac3", + "service-balancer", + "used for Nexus-driven service balancing", + ) +}); - /// Internal user used by Nexus when handling internal API requests - pub static ref USER_INTERNAL_API: UserBuiltinConfig = - UserBuiltinConfig::new_static( - "001de000-05e4-4000-8000-000000000002", - "internal-api", - "used by Nexus when handling internal API requests", - ); +/// Internal user used by Nexus when handling internal API requests +pub static USER_INTERNAL_API: Lazy = Lazy::new(|| { + UserBuiltinConfig::new_static( + "001de000-05e4-4000-8000-000000000002", + "internal-api", + "used by Nexus when handling internal API requests", + ) +}); - /// Internal user used by Nexus to read privileged control plane data - pub static ref USER_INTERNAL_READ: UserBuiltinConfig = - UserBuiltinConfig::new_static( - // "4ead" looks like "read" - "001de000-05e4-4000-8000-000000004ead", - "internal-read", - "used by Nexus to read privileged control plane data", - ); +/// Internal user used by Nexus to read privileged control plane data +pub static USER_INTERNAL_READ: Lazy = Lazy::new(|| { + UserBuiltinConfig::new_static( + // "4ead" looks like "read" + "001de000-05e4-4000-8000-000000004ead", + "internal-read", + "used by Nexus to read privileged control plane data", + ) +}); - /// Internal user used by Nexus when recovering sagas - pub static ref USER_SAGA_RECOVERY: UserBuiltinConfig = - UserBuiltinConfig::new_static( - // "3a8a" looks a bit like "saga". - "001de000-05e4-4000-8000-000000003a8a", - "saga-recovery", - "used by Nexus when recovering sagas", - ); +/// Internal user used by Nexus when recovering sagas +pub static USER_SAGA_RECOVERY: Lazy = Lazy::new(|| { + UserBuiltinConfig::new_static( + // "3a8a" looks a bit like "saga". + "001de000-05e4-4000-8000-000000003a8a", + "saga-recovery", + "used by Nexus when recovering sagas", + ) +}); - /// Internal user used by Nexus when authenticating external requests - pub static ref USER_EXTERNAL_AUTHN: UserBuiltinConfig = - UserBuiltinConfig::new_static( - "001de000-05e4-4000-8000-000000000003", - "external-authn", - "used by Nexus when authenticating external requests", - ); -} +/// Internal user used by Nexus when authenticating external requests +pub static USER_EXTERNAL_AUTHN: Lazy = Lazy::new(|| { + UserBuiltinConfig::new_static( + "001de000-05e4-4000-8000-000000000003", + "external-authn", + "used by Nexus when authenticating external requests", + ) +}); #[cfg(test)] mod test { diff --git a/nexus/db-queries/src/db/fixed_data/vpc.rs b/nexus/db-queries/src/db/fixed_data/vpc.rs index 6571e5c5f9..c71b655ddc 100644 --- a/nexus/db-queries/src/db/fixed_data/vpc.rs +++ b/nexus/db-queries/src/db/fixed_data/vpc.rs @@ -4,31 +4,35 @@ use crate::db; use crate::db::datastore::SERVICES_DB_NAME; -use lazy_static::lazy_static; use nexus_types::external_api::params; use omicron_common::address::SERVICE_VPC_IPV6_PREFIX; use omicron_common::api::external::IdentityMetadataCreateParams; +use once_cell::sync::Lazy; -lazy_static! { - /// UUID of built-in VPC for internal services on the rack. - pub static ref SERVICES_VPC_ID: uuid::Uuid = "001de000-074c-4000-8000-000000000000" +/// UUID of built-in VPC for internal services on the rack. +pub static SERVICES_VPC_ID: Lazy = Lazy::new(|| { + "001de000-074c-4000-8000-000000000000" .parse() - .expect("invalid uuid for builtin services vpc id"); + .expect("invalid uuid for builtin services vpc id") +}); - /// UUID of VpcRouter for built-in Services VPC. - pub static ref SERVICES_VPC_ROUTER_ID: uuid::Uuid = - "001de000-074c-4000-8000-000000000001" - .parse() - .expect("invalid uuid for builtin services vpc router id"); +/// UUID of VpcRouter for built-in Services VPC. +pub static SERVICES_VPC_ROUTER_ID: Lazy = Lazy::new(|| { + "001de000-074c-4000-8000-000000000001" + .parse() + .expect("invalid uuid for builtin services vpc router id") +}); - /// UUID of default route for built-in Services VPC. - pub static ref SERVICES_VPC_DEFAULT_ROUTE_ID: uuid::Uuid = - "001de000-074c-4000-8000-000000000002" - .parse() - .expect("invalid uuid for builtin services vpc default route id"); +/// UUID of default route for built-in Services VPC. +pub static SERVICES_VPC_DEFAULT_ROUTE_ID: Lazy = Lazy::new(|| { + "001de000-074c-4000-8000-000000000002" + .parse() + .expect("invalid uuid for builtin services vpc default route id") +}); - /// Built-in VPC for internal services on the rack. - pub static ref SERVICES_VPC: db::model::IncompleteVpc = db::model::IncompleteVpc::new( +/// Built-in VPC for internal services on the rack. +pub static SERVICES_VPC: Lazy = Lazy::new(|| { + db::model::IncompleteVpc::new( *SERVICES_VPC_ID, *super::project::SERVICES_PROJECT_ID, *SERVICES_VPC_ROUTER_ID, @@ -43,5 +47,5 @@ lazy_static! { ) // `IncompleteVpc::new` only fails if given an invalid `ipv6_prefix` // but we know `SERVICE_VPC_IPV6_PREFIX` is valid. - .unwrap(); -} + .unwrap() +}); diff --git a/nexus/db-queries/src/db/fixed_data/vpc_firewall_rule.rs b/nexus/db-queries/src/db/fixed_data/vpc_firewall_rule.rs index 3fae24abee..5062b1a11c 100644 --- a/nexus/db-queries/src/db/fixed_data/vpc_firewall_rule.rs +++ b/nexus/db-queries/src/db/fixed_data/vpc_firewall_rule.rs @@ -2,72 +2,63 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. -use lazy_static::lazy_static; use nexus_types::identity::Resource; use omicron_common::api::external::{ L4PortRange, VpcFirewallRuleAction, VpcFirewallRuleDirection, VpcFirewallRuleFilter, VpcFirewallRulePriority, VpcFirewallRuleProtocol, VpcFirewallRuleStatus, VpcFirewallRuleTarget, VpcFirewallRuleUpdate, }; +use once_cell::sync::Lazy; -lazy_static! { - /// Built-in VPC firewall rule for External DNS. - pub static ref DNS_VPC_FW_RULE: VpcFirewallRuleUpdate = VpcFirewallRuleUpdate { +/// Built-in VPC firewall rule for External DNS. +pub static DNS_VPC_FW_RULE: Lazy = + Lazy::new(|| VpcFirewallRuleUpdate { name: "external-dns-inbound".parse().unwrap(), description: "allow inbound connections for DNS from anywhere" .to_string(), status: VpcFirewallRuleStatus::Enabled, direction: VpcFirewallRuleDirection::Inbound, - targets: vec![ - VpcFirewallRuleTarget::Subnet( - super::vpc_subnet::DNS_VPC_SUBNET.name().clone(), - ), - ], + targets: vec![VpcFirewallRuleTarget::Subnet( + super::vpc_subnet::DNS_VPC_SUBNET.name().clone(), + )], filters: VpcFirewallRuleFilter { hosts: None, protocols: Some(vec![VpcFirewallRuleProtocol::Udp]), - ports: Some( - vec![ - L4PortRange { - first: 53.try_into().unwrap(), - last: 53.try_into().unwrap(), - }, - ], - ), + ports: Some(vec![L4PortRange { + first: 53.try_into().unwrap(), + last: 53.try_into().unwrap(), + }]), }, action: VpcFirewallRuleAction::Allow, priority: VpcFirewallRulePriority(65534), - }; + }); - /// Built-in VPC firewall rule for Nexus. - pub static ref NEXUS_VPC_FW_RULE: VpcFirewallRuleUpdate = VpcFirewallRuleUpdate { +/// Built-in VPC firewall rule for Nexus. +pub static NEXUS_VPC_FW_RULE: Lazy = + Lazy::new(|| VpcFirewallRuleUpdate { name: "nexus-inbound".parse().unwrap(), - description: "allow inbound connections for console & api from anywhere" - .to_string(), + description: + "allow inbound connections for console & api from anywhere" + .to_string(), status: VpcFirewallRuleStatus::Enabled, direction: VpcFirewallRuleDirection::Inbound, - targets: vec![ - VpcFirewallRuleTarget::Subnet( - super::vpc_subnet::NEXUS_VPC_SUBNET.name().clone(), - ), - ], + targets: vec![VpcFirewallRuleTarget::Subnet( + super::vpc_subnet::NEXUS_VPC_SUBNET.name().clone(), + )], filters: VpcFirewallRuleFilter { hosts: None, protocols: Some(vec![VpcFirewallRuleProtocol::Tcp]), - ports: Some( - vec![ - L4PortRange { - first: 80.try_into().unwrap(), - last: 80.try_into().unwrap(), - }, - L4PortRange { - first: 443.try_into().unwrap(), - last: 443.try_into().unwrap(), - }, - ], - ), + ports: Some(vec![ + L4PortRange { + first: 80.try_into().unwrap(), + last: 80.try_into().unwrap(), + }, + L4PortRange { + first: 443.try_into().unwrap(), + last: 443.try_into().unwrap(), + }, + ]), }, action: VpcFirewallRuleAction::Allow, priority: VpcFirewallRulePriority(65534), - }; -} + }); diff --git a/nexus/db-queries/src/db/fixed_data/vpc_subnet.rs b/nexus/db-queries/src/db/fixed_data/vpc_subnet.rs index 59bc87b34c..c42d4121c9 100644 --- a/nexus/db-queries/src/db/fixed_data/vpc_subnet.rs +++ b/nexus/db-queries/src/db/fixed_data/vpc_subnet.rs @@ -3,32 +3,37 @@ // file, You can obtain one at https://mozilla.org/MPL/2.0/. use crate::db::model::VpcSubnet; -use lazy_static::lazy_static; use omicron_common::address::{ DNS_OPTE_IPV4_SUBNET, DNS_OPTE_IPV6_SUBNET, NEXUS_OPTE_IPV4_SUBNET, NEXUS_OPTE_IPV6_SUBNET, NTP_OPTE_IPV4_SUBNET, NTP_OPTE_IPV6_SUBNET, }; use omicron_common::api::external::IdentityMetadataCreateParams; +use once_cell::sync::Lazy; -lazy_static! { - /// UUID of built-in VPC Subnet for External DNS. - pub static ref DNS_VPC_SUBNET_ID: uuid::Uuid = "001de000-c470-4000-8000-000000000001" +/// UUID of built-in VPC Subnet for External DNS. +pub static DNS_VPC_SUBNET_ID: Lazy = Lazy::new(|| { + "001de000-c470-4000-8000-000000000001" .parse() - .expect("invalid uuid for builtin external dns vpc subnet id"); + .expect("invalid uuid for builtin external dns vpc subnet id") +}); - /// UUID of built-in VPC Subnet for Nexus. - pub static ref NEXUS_VPC_SUBNET_ID: uuid::Uuid = "001de000-c470-4000-8000-000000000002" +/// UUID of built-in VPC Subnet for Nexus. +pub static NEXUS_VPC_SUBNET_ID: Lazy = Lazy::new(|| { + "001de000-c470-4000-8000-000000000002" .parse() - .expect("invalid uuid for builtin nexus vpc subnet id"); + .expect("invalid uuid for builtin nexus vpc subnet id") +}); - /// UUID of built-in VPC Subnet for Boundary NTP. - pub static ref NTP_VPC_SUBNET_ID: uuid::Uuid = "001de000-c470-4000-8000-000000000003" +/// UUID of built-in VPC Subnet for Boundary NTP. +pub static NTP_VPC_SUBNET_ID: Lazy = Lazy::new(|| { + "001de000-c470-4000-8000-000000000003" .parse() - .expect("invalid uuid for builtin boundary ntp vpc subnet id"); + .expect("invalid uuid for builtin boundary ntp vpc subnet id") +}); - - /// Built-in VPC Subnet for External DNS. - pub static ref DNS_VPC_SUBNET: VpcSubnet = VpcSubnet::new( +/// Built-in VPC Subnet for External DNS. +pub static DNS_VPC_SUBNET: Lazy = Lazy::new(|| { + VpcSubnet::new( *DNS_VPC_SUBNET_ID, *super::vpc::SERVICES_VPC_ID, IdentityMetadataCreateParams { @@ -38,10 +43,12 @@ lazy_static! { }, *DNS_OPTE_IPV4_SUBNET, *DNS_OPTE_IPV6_SUBNET, - ); + ) +}); - /// Built-in VPC Subnet for Nexus. - pub static ref NEXUS_VPC_SUBNET: VpcSubnet = VpcSubnet::new( +/// Built-in VPC Subnet for Nexus. +pub static NEXUS_VPC_SUBNET: Lazy = Lazy::new(|| { + VpcSubnet::new( *NEXUS_VPC_SUBNET_ID, *super::vpc::SERVICES_VPC_ID, IdentityMetadataCreateParams { @@ -51,10 +58,12 @@ lazy_static! { }, *NEXUS_OPTE_IPV4_SUBNET, *NEXUS_OPTE_IPV6_SUBNET, - ); + ) +}); - /// Built-in VPC Subnet for Boundary NTP. - pub static ref NTP_VPC_SUBNET: VpcSubnet = VpcSubnet::new( +/// Built-in VPC Subnet for Boundary NTP. +pub static NTP_VPC_SUBNET: Lazy = Lazy::new(|| { + VpcSubnet::new( *NTP_VPC_SUBNET_ID, *super::vpc::SERVICES_VPC_ID, IdentityMetadataCreateParams { @@ -64,5 +73,5 @@ lazy_static! { }, *NTP_OPTE_IPV4_SUBNET, *NTP_OPTE_IPV6_SUBNET, - ); -} + ) +}); diff --git a/nexus/db-queries/src/db/queries/network_interface.rs b/nexus/db-queries/src/db/queries/network_interface.rs index 1dbe57da6f..6d00b4bc29 100644 --- a/nexus/db-queries/src/db/queries/network_interface.rs +++ b/nexus/db-queries/src/db/queries/network_interface.rs @@ -30,6 +30,7 @@ use nexus_db_model::NetworkInterfaceKindEnum; use omicron_common::api::external; use omicron_common::api::external::MacAddr; use omicron_common::nexus_config::NUM_INITIAL_RESERVED_IP_ADDRESSES; +use once_cell::sync::Lazy; use std::net::IpAddr; use uuid::Uuid; @@ -42,36 +43,35 @@ pub(crate) const MAX_NICS: usize = 8; // These are sentinel values and other constants used to verify the state of the // system when operating on network interfaces -lazy_static::lazy_static! { - // States an instance must be in to operate on its network interfaces, in - // most situations. - static ref INSTANCE_STOPPED: db::model::InstanceState = - db::model::InstanceState(external::InstanceState::Stopped); - - static ref INSTANCE_FAILED: db::model::InstanceState = - db::model::InstanceState(external::InstanceState::Failed); - - // An instance can be in the creating state while we manipulate its - // interfaces. The intention is for this only to be the case during sagas. - static ref INSTANCE_CREATING: db::model::InstanceState = - db::model::InstanceState(external::InstanceState::Creating); - - // A sentinel value for the instance state when the instance actually does - // not exist. - static ref INSTANCE_DESTROYED: db::model::InstanceState = - db::model::InstanceState(external::InstanceState::Destroyed); - - // A sentinel value for the instance state when the instance has an active - // VMM, irrespective of that VMM's actual state. - static ref INSTANCE_RUNNING: db::model::InstanceState = - db::model::InstanceState(external::InstanceState::Running); - - static ref NO_INSTANCE_SENTINEL_STRING: String = - String::from(NO_INSTANCE_SENTINEL); - - static ref INSTANCE_BAD_STATE_SENTINEL_STRING: String = - String::from(INSTANCE_BAD_STATE_SENTINEL); -} + +// States an instance must be in to operate on its network interfaces, in +// most situations. +static INSTANCE_STOPPED: Lazy = + Lazy::new(|| db::model::InstanceState(external::InstanceState::Stopped)); + +static INSTANCE_FAILED: Lazy = + Lazy::new(|| db::model::InstanceState(external::InstanceState::Failed)); + +// An instance can be in the creating state while we manipulate its +// interfaces. The intention is for this only to be the case during sagas. +static INSTANCE_CREATING: Lazy = + Lazy::new(|| db::model::InstanceState(external::InstanceState::Creating)); + +// A sentinel value for the instance state when the instance actually does +// not exist. +static INSTANCE_DESTROYED: Lazy = + Lazy::new(|| db::model::InstanceState(external::InstanceState::Destroyed)); + +// A sentinel value for the instance state when the instance has an active +// VMM, irrespective of that VMM's actual state. +static INSTANCE_RUNNING: Lazy = + Lazy::new(|| db::model::InstanceState(external::InstanceState::Running)); + +static NO_INSTANCE_SENTINEL_STRING: Lazy = + Lazy::new(|| String::from(NO_INSTANCE_SENTINEL)); + +static INSTANCE_BAD_STATE_SENTINEL_STRING: Lazy = + Lazy::new(|| String::from(INSTANCE_BAD_STATE_SENTINEL)); // Uncastable sentinel used to detect when an instance exists, but is not // in the right state to have its network interfaces altered diff --git a/nexus/db-queries/src/db/saga_recovery.rs b/nexus/db-queries/src/db/saga_recovery.rs index 802093b889..55cda03c3c 100644 --- a/nexus/db-queries/src/db/saga_recovery.rs +++ b/nexus/db-queries/src/db/saga_recovery.rs @@ -305,9 +305,9 @@ mod test { use super::*; use crate::context::OpContext; use crate::db::test_utils::UnpluggableCockroachDbSecStore; - use lazy_static::lazy_static; use nexus_test_utils::db::test_setup_database; use omicron_test_utils::dev; + use once_cell::sync::Lazy; use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; use steno::{ new_action_noop_undo, Action, ActionContext, ActionError, @@ -376,12 +376,10 @@ mod test { type ExecContextType = TestContext; } - lazy_static! { - static ref ACTION_N1: Arc> = - new_action_noop_undo("n1_action", node_one); - static ref ACTION_N2: Arc> = - new_action_noop_undo("n2_action", node_two); - } + static ACTION_N1: Lazy>> = + Lazy::new(|| new_action_noop_undo("n1_action", node_one)); + static ACTION_N2: Lazy>> = + Lazy::new(|| new_action_noop_undo("n2_action", node_two)); fn registry_create() -> Arc> { let mut registry = ActionRegistry::new(); diff --git a/nexus/defaults/Cargo.toml b/nexus/defaults/Cargo.toml index 0724b5bf4d..535b78054b 100644 --- a/nexus/defaults/Cargo.toml +++ b/nexus/defaults/Cargo.toml @@ -6,7 +6,7 @@ license = "MPL-2.0" [dependencies] ipnetwork.workspace = true -lazy_static.workspace = true +once_cell.workspace = true rand.workspace = true serde_json.workspace = true diff --git a/nexus/defaults/src/lib.rs b/nexus/defaults/src/lib.rs index be1ce2193c..dd08b4e4ab 100644 --- a/nexus/defaults/src/lib.rs +++ b/nexus/defaults/src/lib.rs @@ -6,10 +6,10 @@ use ipnetwork::Ipv4Network; use ipnetwork::Ipv6Network; -use lazy_static::lazy_static; use omicron_common::api::external; use omicron_common::api::external::Ipv4Net; use omicron_common::api::external::Ipv6Net; +use once_cell::sync::Lazy; use std::net::Ipv4Addr; use std::net::Ipv6Addr; @@ -17,51 +17,51 @@ use std::net::Ipv6Addr; /// instance. pub const DEFAULT_PRIMARY_NIC_NAME: &str = "net0"; -lazy_static! { - /// The default IPv4 subnet range assigned to the default VPC Subnet, when - /// the VPC is created, if one is not provided in the request. See - /// for details. - pub static ref DEFAULT_VPC_SUBNET_IPV4_BLOCK: external::Ipv4Net = - Ipv4Net(Ipv4Network::new(Ipv4Addr::new(172, 30, 0, 0), 22).unwrap()); -} +/// The default IPv4 subnet range assigned to the default VPC Subnet, when +/// the VPC is created, if one is not provided in the request. See +/// for details. +pub static DEFAULT_VPC_SUBNET_IPV4_BLOCK: Lazy = + Lazy::new(|| { + Ipv4Net(Ipv4Network::new(Ipv4Addr::new(172, 30, 0, 0), 22).unwrap()) + }); -lazy_static! { - pub static ref DEFAULT_FIREWALL_RULES: external::VpcFirewallRuleUpdateParams = +pub static DEFAULT_FIREWALL_RULES: Lazy = + Lazy::new(|| { serde_json::from_str(r#"{ - "rules": [ - { - "name": "allow-internal-inbound", - "status": "enabled", - "direction": "inbound", - "targets": [ { "type": "vpc", "value": "default" } ], - "filters": { "hosts": [ { "type": "vpc", "value": "default" } ] }, - "action": "allow", - "priority": 65534, - "description": "allow inbound traffic to all instances within the VPC if originated within the VPC" - }, - { - "name": "allow-ssh", - "status": "enabled", - "direction": "inbound", - "targets": [ { "type": "vpc", "value": "default" } ], - "filters": { "ports": [ "22" ], "protocols": [ "TCP" ] }, - "action": "allow", - "priority": 65534, - "description": "allow inbound TCP connections on port 22 from anywhere" - }, - { - "name": "allow-icmp", - "status": "enabled", - "direction": "inbound", - "targets": [ { "type": "vpc", "value": "default" } ], - "filters": { "protocols": [ "ICMP" ] }, - "action": "allow", - "priority": 65534, - "description": "allow inbound ICMP traffic from anywhere" - } - ] - }"#).unwrap(); -} + "rules": [ + { + "name": "allow-internal-inbound", + "status": "enabled", + "direction": "inbound", + "targets": [ { "type": "vpc", "value": "default" } ], + "filters": { "hosts": [ { "type": "vpc", "value": "default" } ] }, + "action": "allow", + "priority": 65534, + "description": "allow inbound traffic to all instances within the VPC if originated within the VPC" + }, + { + "name": "allow-ssh", + "status": "enabled", + "direction": "inbound", + "targets": [ { "type": "vpc", "value": "default" } ], + "filters": { "ports": [ "22" ], "protocols": [ "TCP" ] }, + "action": "allow", + "priority": 65534, + "description": "allow inbound TCP connections on port 22 from anywhere" + }, + { + "name": "allow-icmp", + "status": "enabled", + "direction": "inbound", + "targets": [ { "type": "vpc", "value": "default" } ], + "filters": { "protocols": [ "ICMP" ] }, + "action": "allow", + "priority": 65534, + "description": "allow inbound ICMP traffic from anywhere" + } + ] + }"#).unwrap() + }); /// Generate a random VPC IPv6 prefix, in the range `fd00::/48`. pub fn random_vpc_ipv6_prefix() -> Result { diff --git a/nexus/src/external_api/console_api.rs b/nexus/src/external_api/console_api.rs index d779d34459..90450c3145 100644 --- a/nexus/src/external_api/console_api.rs +++ b/nexus/src/external_api/console_api.rs @@ -17,7 +17,6 @@ use dropshot::{ }; use http::{header, Response, StatusCode, Uri}; use hyper::Body; -use lazy_static::lazy_static; use mime_guess; use nexus_db_model::AuthenticationMode; use nexus_db_queries::authn::silos::IdentityProviderType; @@ -36,6 +35,7 @@ use nexus_types::external_api::params; use nexus_types::identity::Resource; use omicron_common::api::external::http_pagination::PaginatedBy; use omicron_common::api::external::{DataPageParams, Error, NameOrId}; +use once_cell::sync::Lazy; use parse_display::Display; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -810,15 +810,15 @@ fn not_found(internal_msg: &str) -> HttpError { HttpError::for_not_found(None, internal_msg.to_string()) } -lazy_static! { - static ref ALLOWED_EXTENSIONS: HashSet = HashSet::from( +static ALLOWED_EXTENSIONS: Lazy> = Lazy::new(|| { + HashSet::from( [ "js", "css", "html", "ico", "map", "otf", "png", "svg", "ttf", "txt", "webp", "woff", "woff2", ] - .map(|s| OsString::from(s)) - ); -} + .map(|s| OsString::from(s)), + ) +}); /// Starting from `root_dir`, follow the segments of `path` down the file tree /// until we find a file (or not). Do not follow symlinks. diff --git a/nexus/tests/integration_tests/endpoints.rs b/nexus/tests/integration_tests/endpoints.rs index 545129d567..be0ea2a3f5 100644 --- a/nexus/tests/integration_tests/endpoints.rs +++ b/nexus/tests/integration_tests/endpoints.rs @@ -11,7 +11,6 @@ use crate::integration_tests::unauthorized::HTTP_SERVER; use chrono::Utc; use http::method::Method; use internal_dns::names::DNS_ZONE_EXTERNAL_TESTING; -use lazy_static::lazy_static; use nexus_db_queries::authn; use nexus_db_queries::db::fixed_data::silo::DEFAULT_SILO; use nexus_db_queries::db::identity::Resource; @@ -38,225 +37,264 @@ use omicron_common::api::external::RouteTarget; use omicron_common::api::external::SemverVersion; use omicron_common::api::external::VpcFirewallRuleUpdateParams; use omicron_test_utils::certificates::CertificateChain; +use once_cell::sync::Lazy; use std::net::IpAddr; use std::net::Ipv4Addr; use std::str::FromStr; use uuid::Uuid; -lazy_static! { - pub static ref HARDWARE_RACK_URL: String = - format!("/v1/system/hardware/racks/{}", RACK_UUID); - pub static ref HARDWARE_UNINITIALIZED_SLEDS: String = - format!("/v1/system/hardware/sleds-uninitialized"); - pub static ref HARDWARE_SLED_URL: String = - format!("/v1/system/hardware/sleds/{}", SLED_AGENT_UUID); - pub static ref HARDWARE_SLED_PROVISION_STATE_URL: String = - format!("/v1/system/hardware/sleds/{}/provision-state", SLED_AGENT_UUID); - pub static ref DEMO_SLED_PROVISION_STATE: params::SledProvisionStateParams = +pub static HARDWARE_RACK_URL: Lazy = + Lazy::new(|| format!("/v1/system/hardware/racks/{}", RACK_UUID)); +pub const HARDWARE_UNINITIALIZED_SLEDS: &'static str = + "/v1/system/hardware/sleds-uninitialized"; +pub static HARDWARE_SLED_URL: Lazy = + Lazy::new(|| format!("/v1/system/hardware/sleds/{}", SLED_AGENT_UUID)); +pub static HARDWARE_SLED_PROVISION_STATE_URL: Lazy = Lazy::new(|| { + format!("/v1/system/hardware/sleds/{}/provision-state", SLED_AGENT_UUID) +}); +pub static DEMO_SLED_PROVISION_STATE: Lazy = + Lazy::new(|| { params::SledProvisionStateParams { state: nexus_types::external_api::views::SledProvisionState::NonProvisionable, - }; - pub static ref HARDWARE_SWITCH_URL: String = - format!("/v1/system/hardware/switches/{}", SWITCH_UUID); - pub static ref HARDWARE_DISK_URL: String = - format!("/v1/system/hardware/disks"); - pub static ref HARDWARE_SLED_DISK_URL: String = - format!("/v1/system/hardware/sleds/{}/disks", SLED_AGENT_UUID); - - pub static ref SLED_INSTANCES_URL: String = - format!("/v1/system/hardware/sleds/{}/instances", SLED_AGENT_UUID); - - pub static ref DEMO_UNINITIALIZED_SLED: UninitializedSled = UninitializedSled { + } + }); + +pub static HARDWARE_SWITCH_URL: Lazy = + Lazy::new(|| format!("/v1/system/hardware/switches/{}", SWITCH_UUID)); +pub const HARDWARE_DISK_URL: &'static str = "/v1/system/hardware/disks"; +pub static HARDWARE_SLED_DISK_URL: Lazy = Lazy::new(|| { + format!("/v1/system/hardware/sleds/{}/disks", SLED_AGENT_UUID) +}); + +pub static SLED_INSTANCES_URL: Lazy = Lazy::new(|| { + format!("/v1/system/hardware/sleds/{}/instances", SLED_AGENT_UUID) +}); +pub static DEMO_UNINITIALIZED_SLED: Lazy = + Lazy::new(|| UninitializedSled { baseboard: Baseboard { serial: "demo-serial".to_string(), part: "demo-part".to_string(), - revision: 6 + revision: 6, }, rack_id: Uuid::new_v4(), - cubby: 1 - }; - - // Global policy - pub static ref SYSTEM_POLICY_URL: &'static str = "/v1/system/policy"; - - // Silo used for testing - pub static ref DEMO_SILO_NAME: Name = "demo-silo".parse().unwrap(); - pub static ref DEMO_SILO_URL: String = - format!("/v1/system/silos/{}", *DEMO_SILO_NAME); - pub static ref DEMO_SILO_POLICY_URL: String = - format!("/v1/system/silos/{}/policy", *DEMO_SILO_NAME); - pub static ref DEMO_SILO_QUOTAS_URL: String = - format!("/v1/system/silos/{}/quotas", *DEMO_SILO_NAME); - pub static ref DEMO_SILO_CREATE: params::SiloCreate = - params::SiloCreate { - identity: IdentityMetadataCreateParams { - name: DEMO_SILO_NAME.clone(), - description: String::from(""), - }, - quotas: params::SiloQuotasCreate::arbitrarily_high_default(), - discoverable: true, - identity_mode: shared::SiloIdentityMode::SamlJit, - admin_group_name: None, - tls_certificates: vec![], - mapped_fleet_roles: Default::default(), - }; - - pub static ref DEMO_SILO_UTIL_URL: String = format!("/v1/system/utilization/silos/{}", *DEMO_SILO_NAME); - - // Use the default Silo for testing the local IdP - pub static ref DEMO_SILO_USERS_CREATE_URL: String = format!( + cubby: 1, + }); + +// Global policy +pub const SYSTEM_POLICY_URL: &'static str = "/v1/system/policy"; + +// Silo used for testing +pub static DEMO_SILO_NAME: Lazy = + Lazy::new(|| "demo-silo".parse().unwrap()); +pub static DEMO_SILO_URL: Lazy = + Lazy::new(|| format!("/v1/system/silos/{}", *DEMO_SILO_NAME)); +pub static DEMO_SILO_POLICY_URL: Lazy = + Lazy::new(|| format!("/v1/system/silos/{}/policy", *DEMO_SILO_NAME)); +pub static DEMO_SILO_QUOTAS_URL: Lazy = + Lazy::new(|| format!("/v1/system/silos/{}/quotas", *DEMO_SILO_NAME)); +pub static DEMO_SILO_CREATE: Lazy = + Lazy::new(|| params::SiloCreate { + identity: IdentityMetadataCreateParams { + name: DEMO_SILO_NAME.clone(), + description: String::from(""), + }, + quotas: params::SiloQuotasCreate::arbitrarily_high_default(), + discoverable: true, + identity_mode: shared::SiloIdentityMode::SamlJit, + admin_group_name: None, + tls_certificates: vec![], + mapped_fleet_roles: Default::default(), + }); + +pub static DEMO_SILO_UTIL_URL: Lazy = + Lazy::new(|| format!("/v1/system/utilization/silos/{}", *DEMO_SILO_NAME)); + +// Use the default Silo for testing the local IdP +pub static DEMO_SILO_USERS_CREATE_URL: Lazy = Lazy::new(|| { + format!( "/v1/system/identity-providers/local/users?silo={}", DEFAULT_SILO.identity().name, - ); - pub static ref DEMO_SILO_USERS_LIST_URL: String = format!( - "/v1/system/users?silo={}", - DEFAULT_SILO.identity().name, - ); - pub static ref DEMO_SILO_USER_ID_GET_URL: String = format!( - "/v1/system/users/{{id}}?silo={}", - DEFAULT_SILO.identity().name, - ); - pub static ref DEMO_SILO_USER_ID_DELETE_URL: String = format!( + ) +}); +pub static DEMO_SILO_USERS_LIST_URL: Lazy = Lazy::new(|| { + format!("/v1/system/users?silo={}", DEFAULT_SILO.identity().name,) +}); +pub static DEMO_SILO_USER_ID_GET_URL: Lazy = Lazy::new(|| { + format!("/v1/system/users/{{id}}?silo={}", DEFAULT_SILO.identity().name,) +}); +pub static DEMO_SILO_USER_ID_DELETE_URL: Lazy = Lazy::new(|| { + format!( "/v1/system/identity-providers/local/users/{{id}}?silo={}", DEFAULT_SILO.identity().name, - ); - pub static ref DEMO_SILO_USER_ID_SET_PASSWORD_URL: String = format!( + ) +}); +pub static DEMO_SILO_USER_ID_SET_PASSWORD_URL: Lazy = Lazy::new(|| { + format!( "/v1/system/identity-providers/local/users/{{id}}/set-password?silo={}", DEFAULT_SILO.identity().name, - ); -} - -lazy_static! { - - // Project used for testing - pub static ref DEMO_PROJECT_NAME: Name = "demo-project".parse().unwrap(); - pub static ref DEMO_PROJECT_URL: String = - format!("/v1/projects/{}", *DEMO_PROJECT_NAME); - pub static ref DEMO_PROJECT_SELECTOR: String = - format!("project={}", *DEMO_PROJECT_NAME); - pub static ref DEMO_PROJECT_POLICY_URL: String = - format!("/v1/projects/{}/policy", *DEMO_PROJECT_NAME); - pub static ref DEMO_PROJECT_URL_DISKS: String = - format!("/v1/disks?project={}", *DEMO_PROJECT_NAME); - pub static ref DEMO_PROJECT_URL_IMAGES: String = - format!("/v1/images?project={}", *DEMO_PROJECT_NAME); - pub static ref DEMO_PROJECT_URL_INSTANCES: String = format!("/v1/instances?project={}", *DEMO_PROJECT_NAME); - pub static ref DEMO_PROJECT_URL_SNAPSHOTS: String = format!("/v1/snapshots?project={}", *DEMO_PROJECT_NAME); - pub static ref DEMO_PROJECT_URL_VPCS: String = format!("/v1/vpcs?project={}", *DEMO_PROJECT_NAME); - pub static ref DEMO_PROJECT_URL_FIPS: String = format!("/v1/floating-ips?project={}", *DEMO_PROJECT_NAME); - pub static ref DEMO_PROJECT_CREATE: params::ProjectCreate = - params::ProjectCreate { - identity: IdentityMetadataCreateParams { - name: DEMO_PROJECT_NAME.clone(), - description: String::from(""), - }, - }; - - // VPC used for testing - pub static ref DEMO_VPC_NAME: Name = "demo-vpc".parse().unwrap(); - pub static ref DEMO_VPC_URL: String = - format!("/v1/vpcs/{}?{}", *DEMO_VPC_NAME, *DEMO_PROJECT_SELECTOR); - - pub static ref DEMO_VPC_SELECTOR: String = - format!("project={}&vpc={}", *DEMO_PROJECT_NAME, *DEMO_VPC_NAME); - pub static ref DEMO_VPC_URL_FIREWALL_RULES: String = - format!("/v1/vpc-firewall-rules?{}", *DEMO_VPC_SELECTOR); - pub static ref DEMO_VPC_URL_ROUTERS: String = - format!("/v1/vpc-routers?{}", *DEMO_VPC_SELECTOR); - pub static ref DEMO_VPC_URL_SUBNETS: String = - format!("/v1/vpc-subnets?{}", *DEMO_VPC_SELECTOR); - pub static ref DEMO_VPC_CREATE: params::VpcCreate = - params::VpcCreate { - identity: IdentityMetadataCreateParams { - name: DEMO_VPC_NAME.clone(), - description: String::from(""), - }, - ipv6_prefix: None, - dns_name: DEMO_VPC_NAME.clone(), - }; - - // VPC Subnet used for testing - pub static ref DEMO_VPC_SUBNET_NAME: Name = - "demo-vpc-subnet".parse().unwrap(); - pub static ref DEMO_VPC_SUBNET_URL: String = - format!("/v1/vpc-subnets/{}?{}", *DEMO_VPC_SUBNET_NAME, *DEMO_VPC_SELECTOR); - pub static ref DEMO_VPC_SUBNET_INTERFACES_URL: String = - format!("/v1/vpc-subnets/{}/network-interfaces?{}", *DEMO_VPC_SUBNET_NAME, *DEMO_VPC_SELECTOR); - pub static ref DEMO_VPC_SUBNET_CREATE: params::VpcSubnetCreate = - params::VpcSubnetCreate { - identity: IdentityMetadataCreateParams { - name: DEMO_VPC_SUBNET_NAME.clone(), - description: String::from(""), - }, - ipv4_block: Ipv4Net("10.1.2.3/8".parse().unwrap()), - ipv6_block: None, - }; - - // VPC Router used for testing - pub static ref DEMO_VPC_ROUTER_NAME: Name = - "demo-vpc-router".parse().unwrap(); - pub static ref DEMO_VPC_ROUTER_URL: String = - format!("/v1/vpc-routers/{}?project={}&vpc={}", *DEMO_VPC_ROUTER_NAME, *DEMO_PROJECT_NAME, *DEMO_VPC_NAME); - pub static ref DEMO_VPC_ROUTER_URL_ROUTES: String = - format!("/v1/vpc-router-routes?project={}&vpc={}&router={}", *DEMO_PROJECT_NAME, *DEMO_VPC_NAME, *DEMO_VPC_ROUTER_NAME); - pub static ref DEMO_VPC_ROUTER_CREATE: params::VpcRouterCreate = - params::VpcRouterCreate { - identity: IdentityMetadataCreateParams { - name: DEMO_VPC_ROUTER_NAME.clone(), - description: String::from(""), - }, - }; - - // Router Route used for testing - pub static ref DEMO_ROUTER_ROUTE_NAME: Name = - "demo-router-route".parse().unwrap(); - pub static ref DEMO_ROUTER_ROUTE_URL: String = - format!("/v1/vpc-router-routes/{}?project={}&vpc={}&router={}", *DEMO_ROUTER_ROUTE_NAME, *DEMO_PROJECT_NAME, *DEMO_VPC_NAME, *DEMO_VPC_ROUTER_NAME); - pub static ref DEMO_ROUTER_ROUTE_CREATE: params::RouterRouteCreate = - params::RouterRouteCreate { - identity: IdentityMetadataCreateParams { - name: DEMO_ROUTER_ROUTE_NAME.clone(), - description: String::from(""), - }, - target: RouteTarget::Ip(IpAddr::from(Ipv4Addr::new(127, 0, 0, 1))), - destination: RouteDestination::Subnet("loopback".parse().unwrap()), - }; - - // Disk used for testing - pub static ref DEMO_DISK_NAME: Name = "demo-disk".parse().unwrap(); - // TODO: Once we can test a URL multiple times we should also a case to exercise authz for disks filtered by instances - pub static ref DEMO_DISKS_URL: String = - format!("/v1/disks?{}", *DEMO_PROJECT_SELECTOR); - pub static ref DEMO_DISK_URL: String = - format!("/v1/disks/{}?{}", *DEMO_DISK_NAME, *DEMO_PROJECT_SELECTOR); - pub static ref DEMO_DISK_CREATE: params::DiskCreate = - params::DiskCreate { - identity: IdentityMetadataCreateParams { - name: DEMO_DISK_NAME.clone(), - description: "".parse().unwrap(), - }, - disk_source: params::DiskSource::Blank { - block_size: params::BlockSize::try_from(4096).unwrap(), - }, - size: ByteCount::from_gibibytes_u32( - // divide by at least two to leave space for snapshot blocks - DiskTest::DEFAULT_ZPOOL_SIZE_GIB / 5 - ), - }; - pub static ref DEMO_DISK_METRICS_URL: String = - format!( - "/v1/disks/{}/metrics/activated?start_time={:?}&end_time={:?}&{}", - *DEMO_DISK_NAME, - Utc::now(), - Utc::now(), - *DEMO_PROJECT_SELECTOR, - ); - - // Related to importing blocks from an external source - pub static ref DEMO_IMPORT_DISK_NAME: Name = "demo-import-disk".parse().unwrap(); - pub static ref DEMO_IMPORT_DISK_URL: String = - format!("/v1/disks/{}?{}", *DEMO_IMPORT_DISK_NAME, *DEMO_PROJECT_SELECTOR); - pub static ref DEMO_IMPORT_DISK_CREATE: params::DiskCreate = + ) +}); + +// Project used for testing +pub static DEMO_PROJECT_NAME: Lazy = + Lazy::new(|| "demo-project".parse().unwrap()); +pub static DEMO_PROJECT_URL: Lazy = + Lazy::new(|| format!("/v1/projects/{}", *DEMO_PROJECT_NAME)); +pub static DEMO_PROJECT_SELECTOR: Lazy = + Lazy::new(|| format!("project={}", *DEMO_PROJECT_NAME)); +pub static DEMO_PROJECT_POLICY_URL: Lazy = + Lazy::new(|| format!("/v1/projects/{}/policy", *DEMO_PROJECT_NAME)); +pub static DEMO_PROJECT_URL_IMAGES: Lazy = + Lazy::new(|| format!("/v1/images?project={}", *DEMO_PROJECT_NAME)); +pub static DEMO_PROJECT_URL_INSTANCES: Lazy = + Lazy::new(|| format!("/v1/instances?project={}", *DEMO_PROJECT_NAME)); +pub static DEMO_PROJECT_URL_SNAPSHOTS: Lazy = + Lazy::new(|| format!("/v1/snapshots?project={}", *DEMO_PROJECT_NAME)); +pub static DEMO_PROJECT_URL_VPCS: Lazy = + Lazy::new(|| format!("/v1/vpcs?project={}", *DEMO_PROJECT_NAME)); +pub static DEMO_PROJECT_URL_FIPS: Lazy = + Lazy::new(|| format!("/v1/floating-ips?project={}", *DEMO_PROJECT_NAME)); +pub static DEMO_PROJECT_CREATE: Lazy = + Lazy::new(|| params::ProjectCreate { + identity: IdentityMetadataCreateParams { + name: DEMO_PROJECT_NAME.clone(), + description: String::from(""), + }, + }); + +// VPC used for testing +pub static DEMO_VPC_NAME: Lazy = + Lazy::new(|| "demo-vpc".parse().unwrap()); +pub static DEMO_VPC_URL: Lazy = Lazy::new(|| { + format!("/v1/vpcs/{}?{}", *DEMO_VPC_NAME, *DEMO_PROJECT_SELECTOR) +}); +pub static DEMO_VPC_SELECTOR: Lazy = Lazy::new(|| { + format!("project={}&vpc={}", *DEMO_PROJECT_NAME, *DEMO_VPC_NAME) +}); +pub static DEMO_VPC_URL_FIREWALL_RULES: Lazy = + Lazy::new(|| format!("/v1/vpc-firewall-rules?{}", *DEMO_VPC_SELECTOR)); +pub static DEMO_VPC_URL_ROUTERS: Lazy = + Lazy::new(|| format!("/v1/vpc-routers?{}", *DEMO_VPC_SELECTOR)); +pub static DEMO_VPC_URL_SUBNETS: Lazy = + Lazy::new(|| format!("/v1/vpc-subnets?{}", *DEMO_VPC_SELECTOR)); +pub static DEMO_VPC_CREATE: Lazy = + Lazy::new(|| params::VpcCreate { + identity: IdentityMetadataCreateParams { + name: DEMO_VPC_NAME.clone(), + description: String::from(""), + }, + ipv6_prefix: None, + dns_name: DEMO_VPC_NAME.clone(), + }); + +// VPC Subnet used for testing +pub static DEMO_VPC_SUBNET_NAME: Lazy = + Lazy::new(|| "demo-vpc-subnet".parse().unwrap()); +pub static DEMO_VPC_SUBNET_URL: Lazy = Lazy::new(|| { + format!("/v1/vpc-subnets/{}?{}", *DEMO_VPC_SUBNET_NAME, *DEMO_VPC_SELECTOR) +}); +pub static DEMO_VPC_SUBNET_INTERFACES_URL: Lazy = Lazy::new(|| { + format!( + "/v1/vpc-subnets/{}/network-interfaces?{}", + *DEMO_VPC_SUBNET_NAME, *DEMO_VPC_SELECTOR + ) +}); +pub static DEMO_VPC_SUBNET_CREATE: Lazy = + Lazy::new(|| params::VpcSubnetCreate { + identity: IdentityMetadataCreateParams { + name: DEMO_VPC_SUBNET_NAME.clone(), + description: String::from(""), + }, + ipv4_block: Ipv4Net("10.1.2.3/8".parse().unwrap()), + ipv6_block: None, + }); + +// VPC Router used for testing +pub static DEMO_VPC_ROUTER_NAME: Lazy = + Lazy::new(|| "demo-vpc-router".parse().unwrap()); +pub static DEMO_VPC_ROUTER_URL: Lazy = Lazy::new(|| { + format!( + "/v1/vpc-routers/{}?project={}&vpc={}", + *DEMO_VPC_ROUTER_NAME, *DEMO_PROJECT_NAME, *DEMO_VPC_NAME + ) +}); +pub static DEMO_VPC_ROUTER_URL_ROUTES: Lazy = Lazy::new(|| { + format!( + "/v1/vpc-router-routes?project={}&vpc={}&router={}", + *DEMO_PROJECT_NAME, *DEMO_VPC_NAME, *DEMO_VPC_ROUTER_NAME + ) +}); +pub static DEMO_VPC_ROUTER_CREATE: Lazy = + Lazy::new(|| params::VpcRouterCreate { + identity: IdentityMetadataCreateParams { + name: DEMO_VPC_ROUTER_NAME.clone(), + description: String::from(""), + }, + }); + +// Router Route used for testing +pub static DEMO_ROUTER_ROUTE_NAME: Lazy = + Lazy::new(|| "demo-router-route".parse().unwrap()); +pub static DEMO_ROUTER_ROUTE_URL: Lazy = Lazy::new(|| { + format!( + "/v1/vpc-router-routes/{}?project={}&vpc={}&router={}", + *DEMO_ROUTER_ROUTE_NAME, + *DEMO_PROJECT_NAME, + *DEMO_VPC_NAME, + *DEMO_VPC_ROUTER_NAME + ) +}); +pub static DEMO_ROUTER_ROUTE_CREATE: Lazy = + Lazy::new(|| params::RouterRouteCreate { + identity: IdentityMetadataCreateParams { + name: DEMO_ROUTER_ROUTE_NAME.clone(), + description: String::from(""), + }, + target: RouteTarget::Ip(IpAddr::from(Ipv4Addr::new(127, 0, 0, 1))), + destination: RouteDestination::Subnet("loopback".parse().unwrap()), + }); + +// Disk used for testing +pub static DEMO_DISK_NAME: Lazy = + Lazy::new(|| "demo-disk".parse().unwrap()); + +// TODO: Once we can test a URL multiple times we should also a case to exercise +// authz for disks filtered by instances +pub static DEMO_DISKS_URL: Lazy = + Lazy::new(|| format!("/v1/disks?{}", *DEMO_PROJECT_SELECTOR)); +pub static DEMO_DISK_URL: Lazy = Lazy::new(|| { + format!("/v1/disks/{}?{}", *DEMO_DISK_NAME, *DEMO_PROJECT_SELECTOR) +}); +pub static DEMO_DISK_CREATE: Lazy = Lazy::new(|| { + params::DiskCreate { + identity: IdentityMetadataCreateParams { + name: DEMO_DISK_NAME.clone(), + description: "".parse().unwrap(), + }, + disk_source: params::DiskSource::Blank { + block_size: params::BlockSize::try_from(4096).unwrap(), + }, + size: ByteCount::from_gibibytes_u32( + // divide by at least two to leave space for snapshot blocks + DiskTest::DEFAULT_ZPOOL_SIZE_GIB / 5, + ), + } +}); +pub static DEMO_DISK_METRICS_URL: Lazy = Lazy::new(|| { + format!( + "/v1/disks/{}/metrics/activated?start_time={:?}&end_time={:?}&{}", + *DEMO_DISK_NAME, + Utc::now(), + Utc::now(), + *DEMO_PROJECT_SELECTOR, + ) +}); + +// Related to importing blocks from an external source +pub static DEMO_IMPORT_DISK_NAME: Lazy = + Lazy::new(|| "demo-import-disk".parse().unwrap()); +pub static DEMO_IMPORT_DISK_CREATE: Lazy = + Lazy::new(|| { params::DiskCreate { identity: IdentityMetadataCreateParams { name: DEMO_IMPORT_DISK_NAME.clone(), @@ -267,381 +305,486 @@ lazy_static! { }, size: ByteCount::from_gibibytes_u32( // divide by at least two to leave space for snapshot blocks - DiskTest::DEFAULT_ZPOOL_SIZE_GIB / 5 + DiskTest::DEFAULT_ZPOOL_SIZE_GIB / 5, ), - }; - - pub static ref DEMO_IMPORT_DISK_BULK_WRITE_START_URL: String = - format!("/v1/disks/{}/bulk-write-start?{}", *DEMO_IMPORT_DISK_NAME, *DEMO_PROJECT_SELECTOR); - pub static ref DEMO_IMPORT_DISK_BULK_WRITE_URL: String = - format!("/v1/disks/{}/bulk-write?{}", *DEMO_IMPORT_DISK_NAME, *DEMO_PROJECT_SELECTOR); - pub static ref DEMO_IMPORT_DISK_BULK_WRITE_STOP_URL: String = - format!("/v1/disks/{}/bulk-write-stop?{}", *DEMO_IMPORT_DISK_NAME, *DEMO_PROJECT_SELECTOR); - pub static ref DEMO_IMPORT_DISK_FINALIZE_URL: String = - format!("/v1/disks/{}/finalize?{}", *DEMO_IMPORT_DISK_NAME, *DEMO_PROJECT_SELECTOR); -} - -// Separate lazy_static! blocks to avoid hitting some recursion limit when -// compiling -lazy_static! { - // Instance used for testing - pub static ref DEMO_INSTANCE_NAME: Name = "demo-instance".parse().unwrap(); - pub static ref DEMO_INSTANCE_SELECTOR: String = format!("{}&instance={}", *DEMO_PROJECT_SELECTOR, *DEMO_INSTANCE_NAME); - pub static ref DEMO_INSTANCE_URL: String = - format!("/v1/instances/{}?{}", *DEMO_INSTANCE_NAME, *DEMO_PROJECT_SELECTOR); - pub static ref DEMO_INSTANCE_START_URL: String = - format!("/v1/instances/{}/start?{}", *DEMO_INSTANCE_NAME, *DEMO_PROJECT_SELECTOR); - pub static ref DEMO_INSTANCE_STOP_URL: String = - format!("/v1/instances/{}/stop?{}", *DEMO_INSTANCE_NAME, *DEMO_PROJECT_SELECTOR); - pub static ref DEMO_INSTANCE_REBOOT_URL: String = - format!("/v1/instances/{}/reboot?{}", *DEMO_INSTANCE_NAME, *DEMO_PROJECT_SELECTOR); - pub static ref DEMO_INSTANCE_MIGRATE_URL: String = - format!("/v1/instances/{}/migrate?{}", *DEMO_INSTANCE_NAME, *DEMO_PROJECT_SELECTOR); - pub static ref DEMO_INSTANCE_SERIAL_URL: String = - format!("/v1/instances/{}/serial-console?{}", *DEMO_INSTANCE_NAME, *DEMO_PROJECT_SELECTOR); - pub static ref DEMO_INSTANCE_SERIAL_STREAM_URL: String = - format!("/v1/instances/{}/serial-console/stream?{}", *DEMO_INSTANCE_NAME, *DEMO_PROJECT_SELECTOR); - - pub static ref DEMO_INSTANCE_DISKS_URL: String = - format!("/v1/instances/{}/disks?{}", *DEMO_INSTANCE_NAME, *DEMO_PROJECT_SELECTOR); - pub static ref DEMO_INSTANCE_DISKS_ATTACH_URL: String = - format!("/v1/instances/{}/disks/attach?{}", *DEMO_INSTANCE_NAME, *DEMO_PROJECT_SELECTOR); - pub static ref DEMO_INSTANCE_DISKS_DETACH_URL: String = - format!("/v1/instances/{}/disks/detach?{}", *DEMO_INSTANCE_NAME, *DEMO_PROJECT_SELECTOR); - - pub static ref DEMO_INSTANCE_NICS_URL: String = - format!("/v1/network-interfaces?project={}&instance={}", *DEMO_PROJECT_NAME, *DEMO_INSTANCE_NAME); - pub static ref DEMO_INSTANCE_EXTERNAL_IPS_URL: String = - format!("/v1/instances/{}/external-ips?{}", *DEMO_INSTANCE_NAME, *DEMO_PROJECT_SELECTOR); - pub static ref DEMO_INSTANCE_CREATE: params::InstanceCreate = - params::InstanceCreate { - identity: IdentityMetadataCreateParams { - name: DEMO_INSTANCE_NAME.clone(), - description: String::from(""), - }, - ncpus: InstanceCpuCount(1), - memory: ByteCount::from_gibibytes_u32(16), - hostname: String::from("demo-instance"), - user_data: vec![], - network_interfaces: - params::InstanceNetworkInterfaceAttachment::Default, - external_ips: vec![ - params::ExternalIpCreate::Ephemeral { pool_name: Some(DEMO_IP_POOL_NAME.clone()) } - ], - disks: vec![], - start: true, - }; - - // The instance needs a network interface, too. - pub static ref DEMO_INSTANCE_NIC_NAME: Name = - nexus_defaults::DEFAULT_PRIMARY_NIC_NAME.parse().unwrap(); - pub static ref DEMO_INSTANCE_NIC_URL: String = - format!("/v1/network-interfaces/{}?project={}&instance={}", *DEMO_INSTANCE_NIC_NAME, *DEMO_PROJECT_NAME, *DEMO_INSTANCE_NAME); - pub static ref DEMO_INSTANCE_NIC_CREATE: params::InstanceNetworkInterfaceCreate = - params::InstanceNetworkInterfaceCreate { - identity: IdentityMetadataCreateParams { - name: DEMO_INSTANCE_NIC_NAME.clone(), - description: String::from(""), - }, - vpc_name: DEMO_VPC_NAME.clone(), - subnet_name: DEMO_VPC_SUBNET_NAME.clone(), - ip: None, - }; - pub static ref DEMO_INSTANCE_NIC_PUT: params::InstanceNetworkInterfaceUpdate = { - params::InstanceNetworkInterfaceUpdate { - identity: IdentityMetadataUpdateParams { - name: None, - description: Some(String::from("an updated description")), - }, - primary: false, } - }; -} - -lazy_static! { - pub static ref DEMO_CERTIFICATE_NAME: Name = - "demo-certificate".parse().unwrap(); - pub static ref DEMO_CERTIFICATES_URL: String = format!("/v1/certificates"); - pub static ref DEMO_CERTIFICATE_URL: String = - format!("/v1/certificates/demo-certificate"); - pub static ref DEMO_CERTIFICATE: CertificateChain = - CertificateChain::new(format!("*.sys.{DNS_ZONE_EXTERNAL_TESTING}")); - pub static ref DEMO_CERTIFICATE_CREATE: params::CertificateCreate = - params::CertificateCreate { - identity: IdentityMetadataCreateParams { - name: DEMO_CERTIFICATE_NAME.clone(), - description: String::from(""), - }, - cert: DEMO_CERTIFICATE.cert_chain_as_pem(), - key: DEMO_CERTIFICATE.end_cert_private_key_as_pem(), - service: shared::ServiceUsingCertificate::ExternalApi, - }; -} - -lazy_static! { - pub static ref DEMO_SWITCH_PORT_URL: String = - format!("/v1/system/hardware/switch-port"); - - pub static ref DEMO_SWITCH_PORT_SETTINGS_APPLY_URL: String = + }); +pub static DEMO_IMPORT_DISK_BULK_WRITE_START_URL: Lazy = + Lazy::new(|| { format!( - "/v1/system/hardware/switch-port/qsfp7/settings?rack_id={}&switch_location={}", - uuid::Uuid::new_v4(), - "switch0", - ); - - pub static ref DEMO_SWITCH_PORT_SETTINGS: params::SwitchPortApplySettings = - params::SwitchPortApplySettings { - port_settings: NameOrId::Name("portofino".parse().unwrap()), - }; -} - -lazy_static! { - pub static ref DEMO_LOOPBACK_CREATE_URL: String = - "/v1/system/networking/loopback-address".into(); - pub static ref DEMO_LOOPBACK_URL: String = format!( + "/v1/disks/{}/bulk-write-start?{}", + *DEMO_IMPORT_DISK_NAME, *DEMO_PROJECT_SELECTOR + ) + }); +pub static DEMO_IMPORT_DISK_BULK_WRITE_URL: Lazy = Lazy::new(|| { + format!( + "/v1/disks/{}/bulk-write?{}", + *DEMO_IMPORT_DISK_NAME, *DEMO_PROJECT_SELECTOR + ) +}); +pub static DEMO_IMPORT_DISK_BULK_WRITE_STOP_URL: Lazy = + Lazy::new(|| { + format!( + "/v1/disks/{}/bulk-write-stop?{}", + *DEMO_IMPORT_DISK_NAME, *DEMO_PROJECT_SELECTOR + ) + }); +pub static DEMO_IMPORT_DISK_FINALIZE_URL: Lazy = Lazy::new(|| { + format!( + "/v1/disks/{}/finalize?{}", + *DEMO_IMPORT_DISK_NAME, *DEMO_PROJECT_SELECTOR + ) +}); + +// Instance used for testing +pub static DEMO_INSTANCE_NAME: Lazy = + Lazy::new(|| "demo-instance".parse().unwrap()); +pub static DEMO_INSTANCE_URL: Lazy = Lazy::new(|| { + format!("/v1/instances/{}?{}", *DEMO_INSTANCE_NAME, *DEMO_PROJECT_SELECTOR) +}); +pub static DEMO_INSTANCE_START_URL: Lazy = Lazy::new(|| { + format!( + "/v1/instances/{}/start?{}", + *DEMO_INSTANCE_NAME, *DEMO_PROJECT_SELECTOR + ) +}); +pub static DEMO_INSTANCE_STOP_URL: Lazy = Lazy::new(|| { + format!( + "/v1/instances/{}/stop?{}", + *DEMO_INSTANCE_NAME, *DEMO_PROJECT_SELECTOR + ) +}); +pub static DEMO_INSTANCE_REBOOT_URL: Lazy = Lazy::new(|| { + format!( + "/v1/instances/{}/reboot?{}", + *DEMO_INSTANCE_NAME, *DEMO_PROJECT_SELECTOR + ) +}); +pub static DEMO_INSTANCE_MIGRATE_URL: Lazy = Lazy::new(|| { + format!( + "/v1/instances/{}/migrate?{}", + *DEMO_INSTANCE_NAME, *DEMO_PROJECT_SELECTOR + ) +}); +pub static DEMO_INSTANCE_SERIAL_URL: Lazy = Lazy::new(|| { + format!( + "/v1/instances/{}/serial-console?{}", + *DEMO_INSTANCE_NAME, *DEMO_PROJECT_SELECTOR + ) +}); +pub static DEMO_INSTANCE_SERIAL_STREAM_URL: Lazy = Lazy::new(|| { + format!( + "/v1/instances/{}/serial-console/stream?{}", + *DEMO_INSTANCE_NAME, *DEMO_PROJECT_SELECTOR + ) +}); +pub static DEMO_INSTANCE_DISKS_URL: Lazy = Lazy::new(|| { + format!( + "/v1/instances/{}/disks?{}", + *DEMO_INSTANCE_NAME, *DEMO_PROJECT_SELECTOR + ) +}); +pub static DEMO_INSTANCE_DISKS_ATTACH_URL: Lazy = Lazy::new(|| { + format!( + "/v1/instances/{}/disks/attach?{}", + *DEMO_INSTANCE_NAME, *DEMO_PROJECT_SELECTOR + ) +}); +pub static DEMO_INSTANCE_DISKS_DETACH_URL: Lazy = Lazy::new(|| { + format!( + "/v1/instances/{}/disks/detach?{}", + *DEMO_INSTANCE_NAME, *DEMO_PROJECT_SELECTOR + ) +}); +pub static DEMO_INSTANCE_NICS_URL: Lazy = Lazy::new(|| { + format!( + "/v1/network-interfaces?project={}&instance={}", + *DEMO_PROJECT_NAME, *DEMO_INSTANCE_NAME + ) +}); +pub static DEMO_INSTANCE_EXTERNAL_IPS_URL: Lazy = Lazy::new(|| { + format!( + "/v1/instances/{}/external-ips?{}", + *DEMO_INSTANCE_NAME, *DEMO_PROJECT_SELECTOR + ) +}); +pub static DEMO_INSTANCE_CREATE: Lazy = + Lazy::new(|| params::InstanceCreate { + identity: IdentityMetadataCreateParams { + name: DEMO_INSTANCE_NAME.clone(), + description: String::from(""), + }, + ncpus: InstanceCpuCount(1), + memory: ByteCount::from_gibibytes_u32(16), + hostname: String::from("demo-instance"), + user_data: vec![], + network_interfaces: params::InstanceNetworkInterfaceAttachment::Default, + external_ips: vec![params::ExternalIpCreate::Ephemeral { + pool_name: Some(DEMO_IP_POOL_NAME.clone()), + }], + disks: vec![], + start: true, + }); + +// The instance needs a network interface, too. +pub static DEMO_INSTANCE_NIC_NAME: Lazy = + Lazy::new(|| nexus_defaults::DEFAULT_PRIMARY_NIC_NAME.parse().unwrap()); +pub static DEMO_INSTANCE_NIC_URL: Lazy = Lazy::new(|| { + format!( + "/v1/network-interfaces/{}?project={}&instance={}", + *DEMO_INSTANCE_NIC_NAME, *DEMO_PROJECT_NAME, *DEMO_INSTANCE_NAME + ) +}); +pub static DEMO_INSTANCE_NIC_CREATE: Lazy< + params::InstanceNetworkInterfaceCreate, +> = Lazy::new(|| params::InstanceNetworkInterfaceCreate { + identity: IdentityMetadataCreateParams { + name: DEMO_INSTANCE_NIC_NAME.clone(), + description: String::from(""), + }, + vpc_name: DEMO_VPC_NAME.clone(), + subnet_name: DEMO_VPC_SUBNET_NAME.clone(), + ip: None, +}); +pub static DEMO_INSTANCE_NIC_PUT: Lazy = + Lazy::new(|| params::InstanceNetworkInterfaceUpdate { + identity: IdentityMetadataUpdateParams { + name: None, + description: Some(String::from("an updated description")), + }, + primary: false, + }); + +pub static DEMO_CERTIFICATE_NAME: Lazy = + Lazy::new(|| "demo-certificate".parse().unwrap()); +pub const DEMO_CERTIFICATES_URL: &'static str = "/v1/certificates"; +pub const DEMO_CERTIFICATE_URL: &'static str = + "/v1/certificates/demo-certificate"; +pub static DEMO_CERTIFICATE: Lazy = Lazy::new(|| { + CertificateChain::new(format!("*.sys.{DNS_ZONE_EXTERNAL_TESTING}")) +}); +pub static DEMO_CERTIFICATE_CREATE: Lazy = + Lazy::new(|| params::CertificateCreate { + identity: IdentityMetadataCreateParams { + name: DEMO_CERTIFICATE_NAME.clone(), + description: String::from(""), + }, + cert: DEMO_CERTIFICATE.cert_chain_as_pem(), + key: DEMO_CERTIFICATE.end_cert_private_key_as_pem(), + service: shared::ServiceUsingCertificate::ExternalApi, + }); + +pub const DEMO_SWITCH_PORT_URL: &'static str = + "/v1/system/hardware/switch-port"; +pub static DEMO_SWITCH_PORT_SETTINGS_APPLY_URL: Lazy = Lazy::new( + || { + format!( + "/v1/system/hardware/switch-port/qsfp7/settings?rack_id={}&switch_location={}", + uuid::Uuid::new_v4(), + "switch0", + ) + }, +); +pub static DEMO_SWITCH_PORT_SETTINGS: Lazy = + Lazy::new(|| params::SwitchPortApplySettings { + port_settings: NameOrId::Name("portofino".parse().unwrap()), + }); + +pub static DEMO_LOOPBACK_CREATE_URL: Lazy = + Lazy::new(|| "/v1/system/networking/loopback-address".into()); +pub static DEMO_LOOPBACK_URL: Lazy = Lazy::new(|| { + format!( "/v1/system/networking/loopback-address/{}/{}/{}", uuid::Uuid::new_v4(), "switch0", "203.0.113.99/24", - ); - pub static ref DEMO_LOOPBACK_CREATE: params::LoopbackAddressCreate = - params::LoopbackAddressCreate { - address_lot: NameOrId::Name("parkinglot".parse().unwrap()), - rack_id: uuid::Uuid::new_v4(), - switch_location: "switch0".parse().unwrap(), - address: "203.0.113.99".parse().unwrap(), - mask: 24, - anycast: false, - }; -} - -lazy_static! { - pub static ref DEMO_SWITCH_PORT_SETTINGS_URL: String = format!( - "/v1/system/networking/switch-port-settings?port_settings=portofino" - ); - pub static ref DEMO_SWITCH_PORT_SETTINGS_INFO_URL: String = - format!("/v1/system/networking/switch-port-settings/protofino"); - pub static ref DEMO_SWITCH_PORT_SETTINGS_CREATE: params::SwitchPortSettingsCreate = - params::SwitchPortSettingsCreate::new(IdentityMetadataCreateParams { - name: "portofino".parse().unwrap(), - description: "just a port".into(), - }); -} - -lazy_static! { - pub static ref DEMO_ADDRESS_LOTS_URL: String = - format!("/v1/system/networking/address-lot"); - pub static ref DEMO_ADDRESS_LOT_URL: String = - format!("/v1/system/networking/address-lot/parkinglot"); - pub static ref DEMO_ADDRESS_LOT_BLOCKS_URL: String = - format!("/v1/system/networking/address-lot/parkinglot/blocks"); - pub static ref DEMO_ADDRESS_LOT_CREATE: params::AddressLotCreate = - params::AddressLotCreate { - identity: IdentityMetadataCreateParams { - name: "parkinglot".parse().unwrap(), - description: "an address parking lot".into(), - }, - kind: AddressLotKind::Infra, - blocks: vec![params::AddressLotBlockCreate { - first_address: "203.0.113.10".parse().unwrap(), - last_address: "203.0.113.20".parse().unwrap(), - }], - }; -} - -lazy_static! { - pub static ref DEMO_BGP_CONFIG_CREATE_URL: String = - format!("/v1/system/networking/bgp?name_or_id=as47"); - pub static ref DEMO_BGP_CONFIG: params::BgpConfigCreate = - params::BgpConfigCreate { - identity: IdentityMetadataCreateParams { - name: "as47".parse().unwrap(), - description: "BGP config for AS47".into(), - }, - bgp_announce_set_id: NameOrId::Name("instances".parse().unwrap()), - asn: 47, - vrf: None, - }; - pub static ref DEMO_BGP_ANNOUNCE_SET_URL: String = - format!("/v1/system/networking/bgp-announce?name_or_id=a-bag-of-addrs"); - pub static ref DEMO_BGP_ANNOUNCE: params::BgpAnnounceSetCreate = - params::BgpAnnounceSetCreate { - identity: IdentityMetadataCreateParams { - name: "a-bag-of-addrs".parse().unwrap(), - description: "a bag of addrs".into(), - }, - announcement: vec![params::BgpAnnouncementCreate { - address_lot_block: NameOrId::Name( - "some-block".parse().unwrap(), - ), - network: "10.0.0.0/16".parse().unwrap(), - }], - }; - pub static ref DEMO_BGP_STATUS_URL: String = - format!("/v1/system/networking/bgp-status"); - pub static ref DEMO_BGP_ROUTES_IPV4_URL: String = - format!("/v1/system/networking/bgp-routes-ipv4?asn=47"); -} + ) +}); +pub static DEMO_LOOPBACK_CREATE: Lazy = + Lazy::new(|| params::LoopbackAddressCreate { + address_lot: NameOrId::Name("parkinglot".parse().unwrap()), + rack_id: uuid::Uuid::new_v4(), + switch_location: "switch0".parse().unwrap(), + address: "203.0.113.99".parse().unwrap(), + mask: 24, + anycast: false, + }); + +pub const DEMO_SWITCH_PORT_SETTINGS_URL: &'static str = + "/v1/system/networking/switch-port-settings?port_settings=portofino"; +pub const DEMO_SWITCH_PORT_SETTINGS_INFO_URL: &'static str = + "/v1/system/networking/switch-port-settings/protofino"; +pub static DEMO_SWITCH_PORT_SETTINGS_CREATE: Lazy< + params::SwitchPortSettingsCreate, +> = Lazy::new(|| { + params::SwitchPortSettingsCreate::new(IdentityMetadataCreateParams { + name: "portofino".parse().unwrap(), + description: "just a port".into(), + }) +}); + +pub const DEMO_ADDRESS_LOTS_URL: &'static str = + "/v1/system/networking/address-lot"; +pub const DEMO_ADDRESS_LOT_URL: &'static str = + "/v1/system/networking/address-lot/parkinglot"; +pub const DEMO_ADDRESS_LOT_BLOCKS_URL: &'static str = + "/v1/system/networking/address-lot/parkinglot/blocks"; +pub static DEMO_ADDRESS_LOT_CREATE: Lazy = + Lazy::new(|| params::AddressLotCreate { + identity: IdentityMetadataCreateParams { + name: "parkinglot".parse().unwrap(), + description: "an address parking lot".into(), + }, + kind: AddressLotKind::Infra, + blocks: vec![params::AddressLotBlockCreate { + first_address: "203.0.113.10".parse().unwrap(), + last_address: "203.0.113.20".parse().unwrap(), + }], + }); + +pub const DEMO_BGP_CONFIG_CREATE_URL: &'static str = + "/v1/system/networking/bgp?name_or_id=as47"; +pub static DEMO_BGP_CONFIG: Lazy = + Lazy::new(|| params::BgpConfigCreate { + identity: IdentityMetadataCreateParams { + name: "as47".parse().unwrap(), + description: "BGP config for AS47".into(), + }, + bgp_announce_set_id: NameOrId::Name("instances".parse().unwrap()), + asn: 47, + vrf: None, + }); +pub const DEMO_BGP_ANNOUNCE_SET_URL: &'static str = + "/v1/system/networking/bgp-announce?name_or_id=a-bag-of-addrs"; +pub static DEMO_BGP_ANNOUNCE: Lazy = + Lazy::new(|| params::BgpAnnounceSetCreate { + identity: IdentityMetadataCreateParams { + name: "a-bag-of-addrs".parse().unwrap(), + description: "a bag of addrs".into(), + }, + announcement: vec![params::BgpAnnouncementCreate { + address_lot_block: NameOrId::Name("some-block".parse().unwrap()), + network: "10.0.0.0/16".parse().unwrap(), + }], + }); +pub const DEMO_BGP_STATUS_URL: &'static str = + "/v1/system/networking/bgp-status"; +pub const DEMO_BGP_ROUTES_IPV4_URL: &'static str = + "/v1/system/networking/bgp-routes-ipv4?asn=47"; + +// Project Images +pub static DEMO_IMAGE_NAME: Lazy = + Lazy::new(|| "demo-image".parse().unwrap()); +pub static DEMO_PROJECT_IMAGES_URL: Lazy = + Lazy::new(|| format!("/v1/images?project={}", *DEMO_PROJECT_NAME)); +pub static DEMO_PROJECT_IMAGE_URL: Lazy = Lazy::new(|| { + format!("/v1/images/{}?project={}", *DEMO_IMAGE_NAME, *DEMO_PROJECT_NAME) +}); +pub static DEMO_PROJECT_PROMOTE_IMAGE_URL: Lazy = Lazy::new(|| { + format!( + "/v1/images/{}/promote?project={}", + *DEMO_IMAGE_NAME, *DEMO_PROJECT_NAME + ) +}); + +pub static DEMO_SILO_DEMOTE_IMAGE_URL: Lazy = Lazy::new(|| { + format!( + "/v1/images/{}/demote?project={}", + *DEMO_IMAGE_NAME, *DEMO_PROJECT_NAME + ) +}); + +pub static DEMO_IMAGE_CREATE: Lazy = + Lazy::new(|| params::ImageCreate { + identity: IdentityMetadataCreateParams { + name: DEMO_IMAGE_NAME.clone(), + description: String::from(""), + }, + source: params::ImageSource::YouCanBootAnythingAsLongAsItsAlpine, + os: "fake-os".to_string(), + version: "1.0".to_string(), + }); + +// IP Pools +pub static DEMO_IP_POOLS_PROJ_URL: Lazy = + Lazy::new(|| format!("/v1/ip-pools?project={}", *DEMO_PROJECT_NAME)); +pub const DEMO_IP_POOLS_URL: &'static str = "/v1/system/ip-pools"; +pub static DEMO_IP_POOL_NAME: Lazy = + Lazy::new(|| "default".parse().unwrap()); +pub static DEMO_IP_POOL_CREATE: Lazy = + Lazy::new(|| params::IpPoolCreate { + identity: IdentityMetadataCreateParams { + name: DEMO_IP_POOL_NAME.clone(), + description: String::from("an IP pool"), + }, + silo: None, + is_default: true, + }); +pub static DEMO_IP_POOL_PROJ_URL: Lazy = Lazy::new(|| { + format!( + "/v1/ip-pools/{}?project={}", + *DEMO_IP_POOL_NAME, *DEMO_PROJECT_NAME + ) +}); +pub static DEMO_IP_POOL_URL: Lazy = + Lazy::new(|| format!("/v1/system/ip-pools/{}", *DEMO_IP_POOL_NAME)); +pub static DEMO_IP_POOL_UPDATE: Lazy = + Lazy::new(|| params::IpPoolUpdate { + identity: IdentityMetadataUpdateParams { + name: None, + description: Some(String::from("a new IP pool")), + }, + }); +pub static DEMO_IP_POOL_RANGE: Lazy = Lazy::new(|| { + IpRange::V4( + Ipv4Range::new( + std::net::Ipv4Addr::new(10, 0, 0, 0), + std::net::Ipv4Addr::new(10, 0, 0, 255), + ) + .unwrap(), + ) +}); +pub static DEMO_IP_POOL_RANGES_URL: Lazy = + Lazy::new(|| format!("{}/ranges", *DEMO_IP_POOL_URL)); +pub static DEMO_IP_POOL_RANGES_ADD_URL: Lazy = + Lazy::new(|| format!("{}/add", *DEMO_IP_POOL_RANGES_URL)); +pub static DEMO_IP_POOL_RANGES_DEL_URL: Lazy = + Lazy::new(|| format!("{}/remove", *DEMO_IP_POOL_RANGES_URL)); + +// IP Pools (Services) +pub const DEMO_IP_POOL_SERVICE_URL: &'static str = + "/v1/system/ip-pools-service"; +pub static DEMO_IP_POOL_SERVICE_RANGES_URL: Lazy = + Lazy::new(|| format!("{}/ranges", DEMO_IP_POOL_SERVICE_URL)); +pub static DEMO_IP_POOL_SERVICE_RANGES_ADD_URL: Lazy = + Lazy::new(|| format!("{}/add", *DEMO_IP_POOL_SERVICE_RANGES_URL)); +pub static DEMO_IP_POOL_SERVICE_RANGES_DEL_URL: Lazy = + Lazy::new(|| format!("{}/remove", *DEMO_IP_POOL_SERVICE_RANGES_URL)); + +// Snapshots +pub static DEMO_SNAPSHOT_NAME: Lazy = + Lazy::new(|| "demo-snapshot".parse().unwrap()); +pub static DEMO_SNAPSHOT_URL: Lazy = Lazy::new(|| { + format!( + "/v1/snapshots/{}?project={}", + *DEMO_SNAPSHOT_NAME, *DEMO_PROJECT_NAME + ) +}); +pub static DEMO_SNAPSHOT_CREATE: Lazy = + Lazy::new(|| params::SnapshotCreate { + identity: IdentityMetadataCreateParams { + name: DEMO_SNAPSHOT_NAME.clone(), + description: String::from(""), + }, + disk: DEMO_DISK_NAME.clone().into(), + }); -lazy_static! { - // Project Images - pub static ref DEMO_IMAGE_NAME: Name = "demo-image".parse().unwrap(); - pub static ref DEMO_PROJECT_IMAGES_URL: String = - format!("/v1/images?project={}", *DEMO_PROJECT_NAME); - pub static ref DEMO_PROJECT_IMAGE_URL: String = - format!("/v1/images/{}?project={}", *DEMO_IMAGE_NAME, *DEMO_PROJECT_NAME); - pub static ref DEMO_PROJECT_PROMOTE_IMAGE_URL: String = - format!("/v1/images/{}/promote?project={}", *DEMO_IMAGE_NAME, *DEMO_PROJECT_NAME); - pub static ref DEMO_SILO_DEMOTE_IMAGE_URL: String = - format!("/v1/images/{}/demote?project={}", *DEMO_IMAGE_NAME, *DEMO_PROJECT_NAME); - pub static ref DEMO_IMAGE_CREATE: params::ImageCreate = - params::ImageCreate { - identity: IdentityMetadataCreateParams { - name: DEMO_IMAGE_NAME.clone(), - description: String::from(""), - }, - source: params::ImageSource::YouCanBootAnythingAsLongAsItsAlpine, - os: "fake-os".to_string(), - version: "1.0".to_string() - }; - - // IP Pools - pub static ref DEMO_IP_POOLS_PROJ_URL: String = - format!("/v1/ip-pools?project={}", *DEMO_PROJECT_NAME); - pub static ref DEMO_IP_POOLS_URL: &'static str = "/v1/system/ip-pools"; - pub static ref DEMO_IP_POOL_NAME: Name = "default".parse().unwrap(); - pub static ref DEMO_IP_POOL_CREATE: params::IpPoolCreate = - params::IpPoolCreate { - identity: IdentityMetadataCreateParams { - name: DEMO_IP_POOL_NAME.clone(), - description: String::from("an IP pool"), - }, - silo: None, - is_default: true, - }; - pub static ref DEMO_IP_POOL_PROJ_URL: String = - format!("/v1/ip-pools/{}?project={}", *DEMO_IP_POOL_NAME, *DEMO_PROJECT_NAME); - pub static ref DEMO_IP_POOL_URL: String = format!("/v1/system/ip-pools/{}", *DEMO_IP_POOL_NAME); - pub static ref DEMO_IP_POOL_UPDATE: params::IpPoolUpdate = - params::IpPoolUpdate { - identity: IdentityMetadataUpdateParams { - name: None, - description: Some(String::from("a new IP pool")), - }, - }; - pub static ref DEMO_IP_POOL_RANGE: IpRange = IpRange::V4(Ipv4Range::new( - std::net::Ipv4Addr::new(10, 0, 0, 0), - std::net::Ipv4Addr::new(10, 0, 0, 255), - ).unwrap()); - pub static ref DEMO_IP_POOL_RANGES_URL: String = format!("{}/ranges", *DEMO_IP_POOL_URL); - pub static ref DEMO_IP_POOL_RANGES_ADD_URL: String = format!("{}/add", *DEMO_IP_POOL_RANGES_URL); - pub static ref DEMO_IP_POOL_RANGES_DEL_URL: String = format!("{}/remove", *DEMO_IP_POOL_RANGES_URL); - - // IP Pools (Services) - pub static ref DEMO_IP_POOL_SERVICE_URL: &'static str = "/v1/system/ip-pools-service"; - pub static ref DEMO_IP_POOL_SERVICE_RANGES_URL: String = format!("{}/ranges", *DEMO_IP_POOL_SERVICE_URL); - pub static ref DEMO_IP_POOL_SERVICE_RANGES_ADD_URL: String = format!("{}/add", *DEMO_IP_POOL_SERVICE_RANGES_URL); - pub static ref DEMO_IP_POOL_SERVICE_RANGES_DEL_URL: String = format!("{}/remove", *DEMO_IP_POOL_SERVICE_RANGES_URL); - - // Snapshots - pub static ref DEMO_SNAPSHOT_NAME: Name = "demo-snapshot".parse().unwrap(); - pub static ref DEMO_SNAPSHOT_URL: String = - format!("/v1/snapshots/{}?project={}", *DEMO_SNAPSHOT_NAME, *DEMO_PROJECT_NAME); - pub static ref DEMO_SNAPSHOT_CREATE: params::SnapshotCreate = - params::SnapshotCreate { - identity: IdentityMetadataCreateParams { - name: DEMO_SNAPSHOT_NAME.clone(), - description: String::from(""), - }, - disk: DEMO_DISK_NAME.clone().into(), - }; +// SSH keys +pub const DEMO_SSHKEYS_URL: &'static str = "/v1/me/ssh-keys"; +pub static DEMO_SSHKEY_NAME: Lazy = + Lazy::new(|| "aaaaa-ssh-key".parse().unwrap()); - // SSH keys - pub static ref DEMO_SSHKEYS_URL: &'static str = "/v1/me/ssh-keys"; - pub static ref DEMO_SSHKEY_NAME: Name = "aaaaa-ssh-key".parse().unwrap(); - pub static ref DEMO_SSHKEY_CREATE: params::SshKeyCreate = params::SshKeyCreate { +pub static DEMO_SSHKEY_CREATE: Lazy = + Lazy::new(|| params::SshKeyCreate { identity: IdentityMetadataCreateParams { name: DEMO_SSHKEY_NAME.clone(), description: "a demo key".to_string(), }, public_key: "AAAAAAAAAAAAAAA".to_string(), - }; + }); - pub static ref DEMO_SPECIFIC_SSHKEY_URL: String = - format!("{}/{}", *DEMO_SSHKEYS_URL, *DEMO_SSHKEY_NAME); +pub static DEMO_SPECIFIC_SSHKEY_URL: Lazy = + Lazy::new(|| format!("{}/{}", DEMO_SSHKEYS_URL, *DEMO_SSHKEY_NAME)); - // System update +// System update - pub static ref DEMO_SYSTEM_UPDATE_PARAMS: params::SystemUpdatePath = params::SystemUpdatePath { - version: SemverVersion::new(1,0,0), - }; -} +pub static DEMO_SYSTEM_UPDATE_PARAMS: Lazy = + Lazy::new(|| params::SystemUpdatePath { + version: SemverVersion::new(1, 0, 0), + }); -lazy_static! { - // Project Floating IPs - pub static ref DEMO_FLOAT_IP_NAME: Name = "float-ip".parse().unwrap(); - pub static ref DEMO_FLOAT_IP_URL: String = - format!("/v1/floating-ips/{}?project={}", *DEMO_FLOAT_IP_NAME, *DEMO_PROJECT_NAME); - pub static ref DEMO_FLOAT_IP_CREATE: params::FloatingIpCreate = - params::FloatingIpCreate { - identity: IdentityMetadataCreateParams { - name: DEMO_FLOAT_IP_NAME.clone(), - description: String::from("a new IP pool"), - }, - address: Some(std::net::Ipv4Addr::new(10, 0, 0, 141).into()), - pool: None, - }; -} +// Project Floating IPs +pub static DEMO_FLOAT_IP_NAME: Lazy = + Lazy::new(|| "float-ip".parse().unwrap()); -lazy_static! { - // Identity providers - pub static ref IDENTITY_PROVIDERS_URL: String = format!("/v1/system/identity-providers?silo=demo-silo"); - pub static ref SAML_IDENTITY_PROVIDERS_URL: String = format!("/v1/system/identity-providers/saml?silo=demo-silo"); +pub static DEMO_FLOAT_IP_URL: Lazy = Lazy::new(|| { + format!( + "/v1/floating-ips/{}?project={}", + *DEMO_FLOAT_IP_NAME, *DEMO_PROJECT_NAME + ) +}); - pub static ref DEMO_SAML_IDENTITY_PROVIDER_NAME: Name = "demo-saml-provider".parse().unwrap(); - pub static ref SPECIFIC_SAML_IDENTITY_PROVIDER_URL: String = format!("/v1/system/identity-providers/saml/{}?silo=demo-silo", *DEMO_SAML_IDENTITY_PROVIDER_NAME); +pub static DEMO_FLOAT_IP_CREATE: Lazy = + Lazy::new(|| params::FloatingIpCreate { + identity: IdentityMetadataCreateParams { + name: DEMO_FLOAT_IP_NAME.clone(), + description: String::from("a new IP pool"), + }, + address: Some(std::net::Ipv4Addr::new(10, 0, 0, 141).into()), + pool: None, + }); + +// Identity providers +pub const IDENTITY_PROVIDERS_URL: &'static str = + "/v1/system/identity-providers?silo=demo-silo"; +pub const SAML_IDENTITY_PROVIDERS_URL: &'static str = + "/v1/system/identity-providers/saml?silo=demo-silo"; +pub static DEMO_SAML_IDENTITY_PROVIDER_NAME: Lazy = + Lazy::new(|| "demo-saml-provider".parse().unwrap()); + +pub static SPECIFIC_SAML_IDENTITY_PROVIDER_URL: Lazy = + Lazy::new(|| { + format!( + "/v1/system/identity-providers/saml/{}?silo=demo-silo", + *DEMO_SAML_IDENTITY_PROVIDER_NAME + ) + }); - pub static ref SAML_IDENTITY_PROVIDER: params::SamlIdentityProviderCreate = - params::SamlIdentityProviderCreate { - identity: IdentityMetadataCreateParams { - name: DEMO_SAML_IDENTITY_PROVIDER_NAME.clone(), - description: "a demo provider".to_string(), - }, +pub static SAML_IDENTITY_PROVIDER: Lazy = + Lazy::new(|| params::SamlIdentityProviderCreate { + identity: IdentityMetadataCreateParams { + name: DEMO_SAML_IDENTITY_PROVIDER_NAME.clone(), + description: "a demo provider".to_string(), + }, - idp_metadata_source: params::IdpMetadataSource::Url { url: HTTP_SERVER.url("/descriptor").to_string() }, + idp_metadata_source: params::IdpMetadataSource::Url { + url: HTTP_SERVER.url("/descriptor").to_string(), + }, - idp_entity_id: "entity_id".to_string(), - sp_client_id: "client_id".to_string(), - acs_url: "http://acs".to_string(), - slo_url: "http://slo".to_string(), - technical_contact_email: "technical@fake".to_string(), + idp_entity_id: "entity_id".to_string(), + sp_client_id: "client_id".to_string(), + acs_url: "http://acs".to_string(), + slo_url: "http://slo".to_string(), + technical_contact_email: "technical@fake".to_string(), - signing_keypair: None, + signing_keypair: None, - group_attribute_name: None, - }; + group_attribute_name: None, + }); - pub static ref DEMO_SYSTEM_METRICS_URL: String = - format!( - "/v1/system/metrics/virtual_disk_space_provisioned?start_time={:?}&end_time={:?}", - Utc::now(), - Utc::now(), - ); +pub static DEMO_SYSTEM_METRICS_URL: Lazy = Lazy::new(|| { + format!( + "/v1/system/metrics/virtual_disk_space_provisioned?start_time={:?}&end_time={:?}", + Utc::now(), + Utc::now(), + ) +}); - pub static ref DEMO_SILO_METRICS_URL: String = - format!( - "/v1/metrics/virtual_disk_space_provisioned?start_time={:?}&end_time={:?}", - Utc::now(), - Utc::now(), - ); +pub static DEMO_SILO_METRICS_URL: Lazy = Lazy::new(|| { + format!( + "/v1/metrics/virtual_disk_space_provisioned?start_time={:?}&end_time={:?}", + Utc::now(), + Utc::now(), + ) +}); - // Users - pub static ref DEMO_USER_CREATE: params::UserCreate = params::UserCreate { +// Users +pub static DEMO_USER_CREATE: Lazy = + Lazy::new(|| params::UserCreate { external_id: params::UserId::from_str("dummy-user").unwrap(), password: params::UserPassword::LoginDisallowed, - }; -} + }); /// Describes an API endpoint to be verified by the "unauthorized" test /// @@ -779,12 +922,13 @@ impl AllowedMethod { } } -lazy_static! { - pub static ref URL_USERS_DB_INIT: String = - format!("/v1/system/users-builtin/{}", authn::USER_DB_INIT.name); +pub static URL_USERS_DB_INIT: Lazy = Lazy::new(|| { + format!("/v1/system/users-builtin/{}", authn::USER_DB_INIT.name) +}); - /// List of endpoints to be verified - pub static ref VERIFY_ENDPOINTS: Vec = vec![ +/// List of endpoints to be verified +pub static VERIFY_ENDPOINTS: Lazy> = Lazy::new(|| { + vec![ // Global IAM policy VerifyEndpoint { url: &SYSTEM_POLICY_URL, @@ -801,7 +945,6 @@ lazy_static! { ), ], }, - // IP Pools top-level endpoint VerifyEndpoint { url: &DEMO_IP_POOLS_URL, @@ -1558,7 +1701,6 @@ lazy_static! { AllowedMethod::GetWebsocket ], }, - /* Instance NICs */ VerifyEndpoint { url: &DEMO_INSTANCE_NICS_URL, @@ -1571,7 +1713,6 @@ lazy_static! { ), ], }, - VerifyEndpoint { url: &DEMO_INSTANCE_NIC_URL, visibility: Visibility::Protected, @@ -1839,7 +1980,6 @@ lazy_static! { unprivileged_access: UnprivilegedAccess::None, allowed_methods: vec![AllowedMethod::Get], }, - /* Misc */ VerifyEndpoint { @@ -2066,6 +2206,6 @@ lazy_static! { AllowedMethod::Get, AllowedMethod::Delete, ], - } - ]; -} + }, + ] +}); diff --git a/nexus/tests/integration_tests/unauthorized.rs b/nexus/tests/integration_tests/unauthorized.rs index 1cb2eaca3a..317a5a0576 100644 --- a/nexus/tests/integration_tests/unauthorized.rs +++ b/nexus/tests/integration_tests/unauthorized.rs @@ -13,7 +13,6 @@ use headers::authorization::Credentials; use http::method::Method; use http::StatusCode; use httptest::{matchers::*, responders::*, Expectation, ServerBuilder}; -use lazy_static::lazy_static; use nexus_db_queries::authn::external::spoof; use nexus_test_utils::http_testing::AuthnMode; use nexus_test_utils::http_testing::NexusRequest; @@ -21,6 +20,7 @@ use nexus_test_utils::http_testing::RequestBuilder; use nexus_test_utils::http_testing::TestResponse; use nexus_test_utils::resource_helpers::DiskTest; use nexus_test_utils_macros::nexus_test; +use once_cell::sync::Lazy; type ControlPlaneTestContext = nexus_test_utils::ControlPlaneTestContext; @@ -158,8 +158,8 @@ enum SetupReq { }, } -lazy_static! { - pub static ref HTTP_SERVER: httptest::Server = { +pub static HTTP_SERVER: Lazy = + Lazy::new(|| { // Run a httptest server let server = ServerBuilder::new().run().unwrap(); @@ -167,12 +167,10 @@ lazy_static! { server.expect( Expectation::matching(request::method_path("HEAD", "/image.raw")) .times(1..) - .respond_with( - status_code(200).append_header( - "Content-Length", - format!("{}", 4096 * 1000), - ), - ), + .respond_with(status_code(200).append_header( + "Content-Length", + format!("{}", 4096 * 1000), + )), ); server.expect( @@ -182,10 +180,11 @@ lazy_static! { ); server - }; + }); - /// List of requests to execute at setup time - static ref SETUP_REQUESTS: Vec = vec![ +/// List of requests to execute at setup time +static SETUP_REQUESTS: Lazy> = Lazy::new(|| { + vec![ // Create a separate Silo SetupReq::Post { url: "/v1/system/silos", @@ -203,10 +202,7 @@ lazy_static! { ], }, // Get the default IP pool - SetupReq::Get { - url: &DEMO_IP_POOL_URL, - id_routes: vec![], - }, + SetupReq::Get { url: &DEMO_IP_POOL_URL, id_routes: vec![] }, // Create an IP pool range SetupReq::Post { url: &DEMO_IP_POOL_RANGES_ADD_URL, @@ -302,8 +298,8 @@ lazy_static! { body: serde_json::to_value(&*DEMO_CERTIFICATE_CREATE).unwrap(), id_routes: vec![], }, - ]; -} + ] +}); /// Contents returned from an endpoint that creates a resource that has an id /// From de152304c3f551914db05bda1ddbc4b139e1f766 Mon Sep 17 00:00:00 2001 From: Laura Abbott Date: Wed, 20 Dec 2023 14:20:03 -0800 Subject: [PATCH 21/33] Bump SP versions to 1.0.4 (#4722) --- tools/hubris_checksums | 14 +++++++------- tools/hubris_version | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/tools/hubris_checksums b/tools/hubris_checksums index 1396af4d60..b63deb9687 100644 --- a/tools/hubris_checksums +++ b/tools/hubris_checksums @@ -1,7 +1,7 @@ -2df01d7dd17423588c99de4361694efdb6bd375e2f54db053320eeead3e07eda build-gimlet-c-image-default-v1.0.3.zip -8ac0eb6d7817825c6318feb8327f5758a33ccd2479512e3e2424f0eb8e290010 build-gimlet-d-image-default-v1.0.3.zip -eeeb72ec81a843fa1f5093096d1e4500aba6ce01c2d21040a2a10a092595d945 build-gimlet-e-image-default-v1.0.3.zip -de0d9028929322f6d5afc4cb52c198b3402c93a38aa15f9d378617ca1d1112c9 build-psc-b-image-default-v1.0.3.zip -11a6235d852bd75548f12d85b0913cb4ccb0aff3c38bf8a92510a2b9c14dad3c build-psc-c-image-default-v1.0.3.zip -3f863d46a462432f19d3fb5a293b8106da6e138de80271f869692bd29abd994b build-sidecar-b-image-default-v1.0.3.zip -2a9feac7f2da61b843d00edf2693c31c118f202c6cd889d1d1758ea1dd95dbca build-sidecar-c-image-default-v1.0.3.zip +a1a3abb29fb78330c682f8b4f58397f28e296463ac18659af82f762a714f3759 build-gimlet-c-image-default-v1.0.4.zip +f53bc6b8fa825fa1f49b5401b05a14bbd22516c16a8254ef5cd5f3b26b450098 build-gimlet-d-image-default-v1.0.4.zip +a91a1719a03531fdc62608a3b747962b3b7a6dc093ae3810ff35a353ef1e9bf7 build-gimlet-e-image-default-v1.0.4.zip +08ce2931d17d58cde8af49d99de425af4b15384923d2cf79d58000fd2ac5d88c build-psc-b-image-default-v1.0.4.zip +71167b0c889132c3584ba05ee1f7e5917092cd6d7fe8f50f04cdbf3f78321fdf build-psc-c-image-default-v1.0.4.zip +56a02e8620a8343282ee4f205dabcb4898a3acb0e50b6e6eca3919a33a159ee4 build-sidecar-b-image-default-v1.0.4.zip +54eb8d9e202cd69a8cdbdd505276c8c2c1d7f548e2b4234c01887209b190bc91 build-sidecar-c-image-default-v1.0.4.zip diff --git a/tools/hubris_version b/tools/hubris_version index b00c3286fe..0cce8d745a 100644 --- a/tools/hubris_version +++ b/tools/hubris_version @@ -1 +1 @@ -TAGS=(gimlet-v1.0.3 psc-v1.0.3 sidecar-v1.0.3) +TAGS=(gimlet-v1.0.4 psc-v1.0.4 sidecar-v1.0.4) From b114324f809a73a62d8cfb3e95ee1cd42e0794fc Mon Sep 17 00:00:00 2001 From: Laura Abbott Date: Wed, 20 Dec 2023 19:24:59 -0800 Subject: [PATCH 22/33] Correct hubris versions (#4723) The previous versions were tagged incorrectly in the caboose --- tools/hubris_checksums | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tools/hubris_checksums b/tools/hubris_checksums index b63deb9687..707c67fe0c 100644 --- a/tools/hubris_checksums +++ b/tools/hubris_checksums @@ -1,7 +1,7 @@ -a1a3abb29fb78330c682f8b4f58397f28e296463ac18659af82f762a714f3759 build-gimlet-c-image-default-v1.0.4.zip -f53bc6b8fa825fa1f49b5401b05a14bbd22516c16a8254ef5cd5f3b26b450098 build-gimlet-d-image-default-v1.0.4.zip -a91a1719a03531fdc62608a3b747962b3b7a6dc093ae3810ff35a353ef1e9bf7 build-gimlet-e-image-default-v1.0.4.zip -08ce2931d17d58cde8af49d99de425af4b15384923d2cf79d58000fd2ac5d88c build-psc-b-image-default-v1.0.4.zip -71167b0c889132c3584ba05ee1f7e5917092cd6d7fe8f50f04cdbf3f78321fdf build-psc-c-image-default-v1.0.4.zip -56a02e8620a8343282ee4f205dabcb4898a3acb0e50b6e6eca3919a33a159ee4 build-sidecar-b-image-default-v1.0.4.zip -54eb8d9e202cd69a8cdbdd505276c8c2c1d7f548e2b4234c01887209b190bc91 build-sidecar-c-image-default-v1.0.4.zip +09f0342eed777495ac0a852f219d2dec45fdc1b860f938f95736851b1627cad7 build-gimlet-c-image-default-v1.0.4.zip +aef9279ba6d1d0ffa64586d71cdf5933eddbe048ce1a10f5f611128a84b53642 build-gimlet-d-image-default-v1.0.4.zip +989f89f0060239b77d92fe068ceae1be406591c997224256c617d77b2ccbf1b0 build-gimlet-e-image-default-v1.0.4.zip +8e41a139bc62ff86b8343989889491739bb90eb46e1a02585252adf3ee540db9 build-psc-b-image-default-v1.0.4.zip +76e35e71714921a1ca5f7f8314fc596e3b5fe1dfd422c59fdc9a62c1ebfeec0e build-psc-c-image-default-v1.0.4.zip +a406045b1d545fd063bb989c84a774e4d09a445618d4a8889ce232a3b45884a7 build-sidecar-b-image-default-v1.0.4.zip +69ba3ac372388058f8a6e58230e7e2964990609f18c0960357d17bfc16f25bae build-sidecar-c-image-default-v1.0.4.zip From ae7c2ed727f04de778707af9efea55db9e4d1f5a Mon Sep 17 00:00:00 2001 From: Rain Date: Thu, 21 Dec 2023 15:08:29 -0800 Subject: [PATCH 23/33] [rust] update to Rust 1.74.1 (#4700) I'm hitting an ICE with 1.74.0 that appears to be fixed with 1.74.1. --- rust-toolchain.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 65ee8a9912..2e7a87b58b 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -4,5 +4,5 @@ # # We choose a specific toolchain (rather than "stable") for repeatability. The # intent is to keep this up-to-date with recently-released stable Rust. -channel = "1.74.0" +channel = "1.74.1" profile = "default" From b19c61a7137a909c58d8ae45fd81e266f9f98f73 Mon Sep 17 00:00:00 2001 From: Rain Date: Thu, 21 Dec 2023 15:09:06 -0800 Subject: [PATCH 24/33] [authz-macros] accept an optional input_key argument (#4707) In some cases including composite keys, it can be better to make the outside representation of the primary key some kind of struct, rather than passing around tuples of various types. Enable that in the `authz_resource` macro by allowing users to specify an optional `input_key` argument, which represents a better view into the primary key. I'm not entirely sure that the `From` trait is the right thing to use here, but it seems like a pretty low-cost decision to change in the future. As part of this PR I also switched to the prettyplease crate, which as the README explains is more suitable for generated code than rustfmt: https://crates.io/crates/prettyplease --- Cargo.lock | 7 +-- Cargo.toml | 1 + nexus/authz-macros/Cargo.toml | 3 ++ nexus/authz-macros/src/lib.rs | 96 ++++++++++++++++++++++++++++++----- nexus/db-macros/Cargo.toml | 2 +- nexus/db-macros/src/lookup.rs | 15 ++++-- 6 files changed, 103 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 962fe68e02..98275e144f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -354,6 +354,7 @@ version = "0.1.0" dependencies = [ "heck 0.4.1", "omicron-workspace-hack", + "prettyplease", "proc-macro2", "quote", "serde", @@ -1539,9 +1540,9 @@ version = "0.1.0" dependencies = [ "heck 0.4.1", "omicron-workspace-hack", + "prettyplease", "proc-macro2", "quote", - "rustfmt-wrapper", "serde", "serde_tokenstream 0.2.0", "syn 2.0.32", @@ -6147,9 +6148,9 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.12" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c64d9ba0963cdcea2e1b2230fbae2bab30eb25a174be395c41e764bfb65dd62" +checksum = "ae005bd773ab59b4725093fd7df83fd7892f7d8eafb48dbd7de6e024e4215f9d" dependencies = [ "proc-macro2", "syn 2.0.32", diff --git a/Cargo.toml b/Cargo.toml index d651a13bf1..d4f81b0310 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -289,6 +289,7 @@ postgres-protocol = "0.6.6" predicates = "3.0.4" pretty_assertions = "1.4.0" pretty-hex = "0.4.0" +prettyplease = "0.2.15" proc-macro2 = "1.0" progenitor = { git = "https://github.com/oxidecomputer/progenitor", branch = "main" } progenitor-client = { git = "https://github.com/oxidecomputer/progenitor", branch = "main" } diff --git a/nexus/authz-macros/Cargo.toml b/nexus/authz-macros/Cargo.toml index 15f18cb9c8..816100eb58 100644 --- a/nexus/authz-macros/Cargo.toml +++ b/nexus/authz-macros/Cargo.toml @@ -15,3 +15,6 @@ serde.workspace = true serde_tokenstream.workspace = true syn.workspace = true omicron-workspace-hack.workspace = true + +[dev-dependencies] +prettyplease.workspace = true diff --git a/nexus/authz-macros/src/lib.rs b/nexus/authz-macros/src/lib.rs index d85516f3ea..3d6f265fea 100644 --- a/nexus/authz-macros/src/lib.rs +++ b/nexus/authz-macros/src/lib.rs @@ -95,6 +95,33 @@ use serde_tokenstream::ParseWrapper; /// polar_snippet = FleetChild, /// } /// ``` +/// +/// In some cases, it may be more convenient to identify a composite key with a +/// struct rather than relying on tuples. This is supported too: +/// +/// ```ignore +/// struct SomeCompositeId { +/// foo: String, +/// bar: String, +/// } +/// +/// // There needs to be a `From` impl from the composite ID to the primary key. +/// impl From for (String, String) { +/// fn from(id: SomeCompositeId) -> Self { +/// (id.foo, id.bar) +/// } +/// } +/// +/// authz_resource! { +/// name = "MyResource", +/// parent = "Fleet", +/// primary_key = (String, String), +/// input_key = SomeCompositeId, +/// roles_allowed = false, +/// polar_snippet = FleetChild, +/// } +/// ``` + // Allow private intra-doc links. This is useful because the `Input` struct // cannot be exported (since we're a proc macro crate, and we can't expose // a struct), but its documentation is very useful. @@ -121,6 +148,12 @@ struct Input { parent: String, /// Rust type for the primary key for this resource primary_key: ParseWrapper, + /// Rust type for the input key for this resource (the key users specify + /// for this resource, convertible to `primary_key`). + /// + /// This is the same as primary_key if not specified. + #[serde(default)] + input_key: Option>, /// Whether roles may be attached directly to this resource roles_allowed: bool, /// How to generate the Polar snippet for this resource @@ -153,6 +186,9 @@ fn do_authz_resource( let parent_resource_name = format_ident!("{}", input.parent); let parent_as_snake = heck::AsSnakeCase(&input.parent).to_string(); let primary_key_type = &*input.primary_key; + let input_key_type = + &**input.input_key.as_ref().unwrap_or(&input.primary_key); + let (has_role_body, as_roles_body, api_resource_roles_trait) = if input.roles_allowed { ( @@ -334,6 +370,21 @@ fn do_authz_resource( /// `parent`, unique key `key`, looked up as described by /// `lookup_type` pub fn new( + parent: #parent_resource_name, + key: #input_key_type, + lookup_type: LookupType, + ) -> #resource_name { + #resource_name { + parent, + key: key.into(), + lookup_type, + } + } + + /// A version of `new` that takes the primary key type directly. + /// This is only different from [`Self::new`] if this resource + /// uses a different input key type. + pub fn with_primary_key( parent: #parent_resource_name, key: #primary_key_type, lookup_type: LookupType, @@ -346,7 +397,7 @@ fn do_authz_resource( } pub fn id(&self) -> #primary_key_type { - self.key.clone() + self.key.clone().into() } /// Describes how to register this type with Oso @@ -411,15 +462,36 @@ fn do_authz_resource( // See the test for lookup_resource. #[cfg(test)] -#[test] -fn test_authz_dump() { - let output = do_authz_resource(quote! { - name = "Organization", - parent = "Fleet", - primary_key = Uuid, - roles_allowed = false, - polar_snippet = Custom, - }) - .unwrap(); - println!("{}", output); +mod tests { + use super::*; + #[test] + fn test_authz_dump() { + let output = do_authz_resource(quote! { + name = "Organization", + parent = "Fleet", + primary_key = Uuid, + roles_allowed = false, + polar_snippet = Custom, + }) + .unwrap(); + println!("{}", pretty_format(output)); + + let output = do_authz_resource(quote! { + name = "Instance", + parent = "Project", + primary_key = (String, String), + // The SomeCompositeId type doesn't exist, but that's okay because + // this code is never compiled, just printed out. + input_key = SomeCompositeId, + roles_allowed = false, + polar_snippet = InProject, + }) + .unwrap(); + println!("{}", pretty_format(output)); + } + + fn pretty_format(input: TokenStream) -> String { + let parsed = syn::parse2(input).unwrap(); + prettyplease::unparse(&parsed) + } } diff --git a/nexus/db-macros/Cargo.toml b/nexus/db-macros/Cargo.toml index 053c381ac9..64398b266c 100644 --- a/nexus/db-macros/Cargo.toml +++ b/nexus/db-macros/Cargo.toml @@ -18,4 +18,4 @@ syn = { workspace = true, features = ["extra-traits"] } omicron-workspace-hack.workspace = true [dev-dependencies] -rustfmt-wrapper.workspace = true +prettyplease.workspace = true diff --git a/nexus/db-macros/src/lookup.rs b/nexus/db-macros/src/lookup.rs index f2362f5bc5..c7906c7bf0 100644 --- a/nexus/db-macros/src/lookup.rs +++ b/nexus/db-macros/src/lookup.rs @@ -406,7 +406,7 @@ fn generate_misc_helpers(config: &Config) -> TokenStream { db_row: &nexus_db_model::#resource_name, lookup_type: LookupType, ) -> authz::#resource_name { - authz::#resource_name::new( + authz::#resource_name::with_primary_key( authz_parent.clone(), db_row.id(), lookup_type @@ -923,8 +923,8 @@ fn generate_database_functions(config: &Config) -> TokenStream { #[cfg(test)] mod test { use super::lookup_resource; + use proc_macro2::TokenStream; use quote::quote; - use rustfmt_wrapper::rustfmt; #[test] #[ignore] @@ -938,7 +938,7 @@ mod test { primary_key_columns = [ { column_name = "id", rust_type = Uuid } ] }) .unwrap(); - println!("{}", rustfmt(output).unwrap()); + println!("{}", pretty_format(output)); let output = lookup_resource(quote! { name = "SiloUser", @@ -949,7 +949,7 @@ mod test { primary_key_columns = [ { column_name = "id", rust_type = Uuid } ] }) .unwrap(); - println!("{}", rustfmt(output).unwrap()); + println!("{}", pretty_format(output)); let output = lookup_resource(quote! { name = "UpdateArtifact", @@ -964,6 +964,11 @@ mod test { ] }) .unwrap(); - println!("{}", rustfmt(output).unwrap()); + println!("{}", pretty_format(output)); + } + + fn pretty_format(input: TokenStream) -> String { + let parsed = syn::parse2(input).unwrap(); + prettyplease::unparse(&parsed) } } From b16be3f68c420e223dccb0326deb327ae5be8e50 Mon Sep 17 00:00:00 2001 From: Kyle Simpson Date: Fri, 22 Dec 2023 08:13:03 +0000 Subject: [PATCH 25/33] Update Sidecar-lite, ignore ARP hostnames in SoftNPU init (#4725) Bumps to the most recent sidecar-lite, which includes a minor fix for manually added routes, and adds the `-n` flag when checking `arp` for the gateway's MAC address during SoftNPU init. This is really only needed if the gateway is also acting as a DNS server and names itself something cutesy, but it can come up: ``` kyle@farme:~/gits/omicron$ arp -a Net to Media Table: IPv4 Device IP Address Mask Flags Phys Addr ------ -------------------- --------------- -------- --------------- rge0 hub.home.arpa 255.255.255.255 b8:6a:f1:28:cd:00 ... kyle@farme:~/gits/omicron$ arp -an Net to Media Table: IPv4 Device IP Address Mask Flags Phys Addr ------ -------------------- --------------- -------- --------------- rge0 10.0.0.1 255.255.255.255 b8:6a:f1:28:cd:00 ``` --- tools/create_virtual_hardware.sh | 2 +- tools/scrimlet/softnpu-init.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/create_virtual_hardware.sh b/tools/create_virtual_hardware.sh index 7721fb1c0f..fa35bb24ab 100755 --- a/tools/create_virtual_hardware.sh +++ b/tools/create_virtual_hardware.sh @@ -63,7 +63,7 @@ function ensure_softnpu_zone { --omicron-zone \ --ports sc0_0,tfportrear0_0 \ --ports sc0_1,tfportqsfp0_0 \ - --sidecar-lite-commit 45ed98fea5824feb4d42f45bbf218e597dc9fc58 \ + --sidecar-lite-commit e3ea4b495ba0a71801ded0776ae4bbd31df57e26 \ --softnpu-commit dbab082dfa89da5db5ca2325c257089d2f130092 } "$SOURCE_DIR"/scrimlet/softnpu-init.sh diff --git a/tools/scrimlet/softnpu-init.sh b/tools/scrimlet/softnpu-init.sh index 6a2a9e10ce..59f8e83019 100755 --- a/tools/scrimlet/softnpu-init.sh +++ b/tools/scrimlet/softnpu-init.sh @@ -31,7 +31,7 @@ fi # Add an extrac space at the end of the search pattern passed to `grep`, so that # we can be sure we're matching the exact $GATEWAY_IP, and not something that # shares the same string prefix. -GATEWAY_MAC=${GATEWAY_MAC:=$(arp -a | grep "$GATEWAY_IP " | awk -F ' ' '{print $NF}')} +GATEWAY_MAC=${GATEWAY_MAC:=$(arp -an | grep "$GATEWAY_IP " | awk -F ' ' '{print $NF}')} # Check that the MAC appears to be exactly one MAC address. COUNT=$(grep -c -E '^([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}$' <(echo "$GATEWAY_MAC")) From 727bc961139e77f9e33d69bb6ebe8fce31bb117d Mon Sep 17 00:00:00 2001 From: Andy Fiddaman Date: Fri, 22 Dec 2023 13:54:54 +0000 Subject: [PATCH 26/33] Disable the SSH daemon in most non-global zones (#4716) This improves things by disabling the SSH daemon in self-assembling zones via the smf profile, and directly in non-self-assembling zones. The service remains enabled in the switch zone for wicket and support. --- sled-agent/src/profile.rs | 31 +++++++++++++++++++++++++++--- sled-agent/src/services.rs | 39 ++++++++++++++++++++++++-------------- 2 files changed, 53 insertions(+), 17 deletions(-) diff --git a/sled-agent/src/profile.rs b/sled-agent/src/profile.rs index 7c2d8d1738..1addbca4c9 100644 --- a/sled-agent/src/profile.rs +++ b/sled-agent/src/profile.rs @@ -116,12 +116,18 @@ impl Display for ServiceBuilder { pub struct ServiceInstanceBuilder { name: String, + enabled: bool, property_groups: Vec, } impl ServiceInstanceBuilder { pub fn new(name: &str) -> Self { - Self { name: name.to_string(), property_groups: vec![] } + Self { name: name.to_string(), enabled: true, property_groups: vec![] } + } + + pub fn disable(mut self) -> Self { + self.enabled = false; + self } pub fn add_property_group( @@ -137,9 +143,10 @@ impl Display for ServiceInstanceBuilder { fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { write!( f, - r#" + r#" "#, - name = self.name + name = self.name, + enabled = self.enabled )?; for property_group in &self.property_groups { @@ -315,6 +322,24 @@ mod tests { ); } + #[test] + fn test_disabled_instance() { + let builder = ProfileBuilder::new("myprofile") + .add_service(ServiceBuilder::new("myservice").add_instance( + ServiceInstanceBuilder::new("default").disable(), + )); + assert_eq!( + format!("{}", builder), + r#" + + + + + +"#, + ); + } + #[test] fn test_property_group() { let builder = ProfileBuilder::new("myprofile").add_service( diff --git a/sled-agent/src/services.rs b/sled-agent/src/services.rs index 837c2a05df..a9000a1c4b 100644 --- a/sled-agent/src/services.rs +++ b/sled-agent/src/services.rs @@ -1371,8 +1371,8 @@ impl ServiceManager { .add_property_group(dns_config_builder) // We do need to enable the default instance of the // dns/install service. It's enough to just mention it - // here, as the ServiceInstanceBuilder always enables the - // instance being added. + // here, as the ServiceInstanceBuilder enables the + // instance being added by default. .add_instance(ServiceInstanceBuilder::new("default"))) } @@ -1473,6 +1473,8 @@ impl ServiceManager { // // These zones are self-assembling -- after they boot, there should // be no "zlogin" necessary to initialize. + let disabled_ssh_service = ServiceBuilder::new("network/ssh") + .add_instance(ServiceInstanceBuilder::new("default").disable()); match &request { ZoneArgs::Omicron(OmicronZoneConfigLocal { zone: @@ -1507,6 +1509,7 @@ impl ServiceManager { ); let profile = ProfileBuilder::new("omicron") + .add_service(disabled_ssh_service) .add_service(clickhouse_service) .add_service(dns_service); profile @@ -1551,6 +1554,7 @@ impl ServiceManager { .add_property_group(config), ); let profile = ProfileBuilder::new("omicron") + .add_service(disabled_ssh_service) .add_service(clickhouse_keeper_service) .add_service(dns_service); profile @@ -1603,6 +1607,7 @@ impl ServiceManager { ); let profile = ProfileBuilder::new("omicron") + .add_service(disabled_ssh_service) .add_service(cockroachdb_service) .add_service(dns_service); profile @@ -1646,12 +1651,15 @@ impl ServiceManager { .add_property("uuid", "astring", uuid) .add_property("store", "astring", "/data"); - let profile = ProfileBuilder::new("omicron").add_service( - ServiceBuilder::new("oxide/crucible/agent").add_instance( - ServiceInstanceBuilder::new("default") - .add_property_group(config), - ), - ); + let profile = ProfileBuilder::new("omicron") + .add_service(disabled_ssh_service) + .add_service( + ServiceBuilder::new("oxide/crucible/agent") + .add_instance( + ServiceInstanceBuilder::new("default") + .add_property_group(config), + ), + ); profile .add_to_zone(&self.inner.log, &installed_zone) .await @@ -1685,12 +1693,15 @@ impl ServiceManager { .add_property("listen_addr", "astring", listen_addr) .add_property("listen_port", "astring", listen_port); - let profile = ProfileBuilder::new("omicron").add_service( - ServiceBuilder::new("oxide/crucible/pantry").add_instance( - ServiceInstanceBuilder::new("default") - .add_property_group(config), - ), - ); + let profile = ProfileBuilder::new("omicron") + .add_service(disabled_ssh_service) + .add_service( + ServiceBuilder::new("oxide/crucible/pantry") + .add_instance( + ServiceInstanceBuilder::new("default") + .add_property_group(config), + ), + ); profile .add_to_zone(&self.inner.log, &installed_zone) .await From a2cef18d7b735580bc8963103b2f0e4e30fd9885 Mon Sep 17 00:00:00 2001 From: "oxide-renovate[bot]" <146848827+oxide-renovate[bot]@users.noreply.github.com> Date: Fri, 22 Dec 2023 10:50:38 -0800 Subject: [PATCH 27/33] Update Rust crate russh to v0.40.2 [SECURITY] (#4714) --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 98275e144f..3cdf3dd678 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6908,9 +6908,9 @@ dependencies = [ [[package]] name = "russh" -version = "0.40.1" +version = "0.40.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23955cec4c4186e8c36f42c5d4043f9fd6cab8702fd08ce1971d966b48ec832f" +checksum = "93dab9e1c313d0d04a42e39c0995943fc38c037e2e3fa9c33685777a1aecdfb2" dependencies = [ "aes", "aes-gcm", From 709493b9adcec358fbdf9c3f5025698c589bd1df Mon Sep 17 00:00:00 2001 From: "Andrew J. Stone" Date: Fri, 22 Dec 2023 17:59:42 -0500 Subject: [PATCH 28/33] Rename API to sled_add and take UninitializedSledId as param (#4704) This is the second half of the fix for #4607 --- .../db-queries/src/db/datastore/inventory.rs | 4 +- nexus/src/app/rack.rs | 84 ++++++++++++------- nexus/src/external_api/http_entrypoints.rs | 10 +-- nexus/tests/integration_tests/endpoints.rs | 16 +--- nexus/tests/output/nexus_tags.txt | 2 +- nexus/types/src/external_api/params.rs | 17 ++++ nexus/types/src/inventory.rs | 7 ++ openapi/nexus.json | 20 ++++- openapi/sled-agent.json | 2 +- sled-agent/src/http_entrypoints.rs | 6 +- sled-agent/src/sled_agent.rs | 2 +- 11 files changed, 115 insertions(+), 55 deletions(-) diff --git a/nexus/db-queries/src/db/datastore/inventory.rs b/nexus/db-queries/src/db/datastore/inventory.rs index 31b24a7e75..7d880b4ec0 100644 --- a/nexus/db-queries/src/db/datastore/inventory.rs +++ b/nexus/db-queries/src/db/datastore/inventory.rs @@ -919,7 +919,7 @@ impl DataStore { pub async fn find_hw_baseboard_id( &self, opctx: &OpContext, - baseboard_id: BaseboardId, + baseboard_id: &BaseboardId, ) -> Result { opctx.authorize(authz::Action::Read, &authz::INVENTORY).await?; let conn = self.pool_connection_authorized(opctx).await?; @@ -1442,7 +1442,7 @@ mod test { part_number: "some-part".into(), }; let err = datastore - .find_hw_baseboard_id(&opctx, baseboard_id) + .find_hw_baseboard_id(&opctx, &baseboard_id) .await .unwrap_err(); assert!(matches!(err, Error::ObjectNotFound { .. })); diff --git a/nexus/src/app/rack.rs b/nexus/src/app/rack.rs index c0307e5b5b..a0dcb7fcb1 100644 --- a/nexus/src/app/rack.rs +++ b/nexus/src/app/rack.rs @@ -31,6 +31,7 @@ use nexus_types::external_api::params::LinkConfig; use nexus_types::external_api::params::LldpServiceConfig; use nexus_types::external_api::params::RouteConfig; use nexus_types::external_api::params::SwitchPortConfig; +use nexus_types::external_api::params::UninitializedSledId; use nexus_types::external_api::params::{ AddressLotCreate, BgpPeerConfig, LoopbackAddressCreate, Route, SiloCreate, SwitchPortSettingsCreate, @@ -51,6 +52,7 @@ use omicron_common::api::external::ListResultVec; use omicron_common::api::external::LookupResult; use omicron_common::api::external::Name; use omicron_common::api::external::NameOrId; +use omicron_common::api::external::ResourceType; use omicron_common::api::internal::shared::ExternalPortDiscovery; use sled_agent_client::types::AddSledRequest; use sled_agent_client::types::EarlyNetworkConfigBody; @@ -69,6 +71,19 @@ use std::num::NonZeroU32; use std::str::FromStr; use uuid::Uuid; +// A limit for querying the last inventory collection +// +// We set a limit of 200 here to give us some breathing room when +// querying for cabooses and RoT pages, each of which is "4 per SP/RoT", +// which in a single fully populated rack works out to (32 sleds + 2 +// switches + 1 psc) * 4 = 140. +// +// This feels bad and probably needs more thought; see +// https://github.com/oxidecomputer/omicron/issues/4621 where this limit +// being too low bit us, and it will link to a more general followup +// issue. +const INVENTORY_COLLECTION_LIMIT: u32 = 200; + impl super::Nexus { pub(crate) async fn racks_list( &self, @@ -872,17 +887,7 @@ impl super::Nexus { ) -> ListResultVec { debug!(self.log, "Getting latest collection"); // Grab the SPs from the last collection - // - // We set a limit of 200 here to give us some breathing room when - // querying for cabooses and RoT pages, each of which is "4 per SP/RoT", - // which in a single fully populated rack works out to (32 sleds + 2 - // switches + 1 psc) * 4 = 140. - // - // This feels bad and probably needs more thought; see - // https://github.com/oxidecomputer/omicron/issues/4621 where this limit - // being too low bit us, and it will link to a more general followup - // issue. - let limit = NonZeroU32::new(200).unwrap(); + let limit = NonZeroU32::new(INVENTORY_COLLECTION_LIMIT).unwrap(); let collection = self .db_datastore .inventory_get_latest_collection(opctx, limit) @@ -933,16 +938,18 @@ impl super::Nexus { } /// Add a sled to an intialized rack - pub(crate) async fn add_sled_to_initialized_rack( + pub(crate) async fn sled_add( &self, opctx: &OpContext, - sled: UninitializedSled, + sled: UninitializedSledId, ) -> Result<(), Error> { - let baseboard_id = sled.baseboard.clone().into(); - let hw_baseboard_id = - self.db_datastore.find_hw_baseboard_id(opctx, baseboard_id).await?; + let baseboard_id = sled.clone().into(); + let hw_baseboard_id = self + .db_datastore + .find_hw_baseboard_id(opctx, &baseboard_id) + .await?; - let subnet = self.db_datastore.rack_subnet(opctx, sled.rack_id).await?; + let subnet = self.db_datastore.rack_subnet(opctx, self.rack_id).await?; let rack_subnet = Ipv6Subnet::::from(rack_subnet(Some(subnet))?); @@ -950,16 +957,39 @@ impl super::Nexus { .db_datastore .allocate_sled_underlay_subnet_octets( opctx, - sled.rack_id, + self.rack_id, hw_baseboard_id, ) .await?; + // Grab the SPs from the last collection + let limit = NonZeroU32::new(INVENTORY_COLLECTION_LIMIT).unwrap(); + let collection = self + .db_datastore + .inventory_get_latest_collection(opctx, limit) + .await?; + + // If there isn't a collection, we don't know about the sled + let Some(collection) = collection else { + return Err(Error::unavail("no inventory data available")); + }; + + // Find the revision + let Some(sp) = collection.sps.get(&baseboard_id) else { + return Err(Error::ObjectNotFound { + type_name: ResourceType::Sled, + lookup_type: + omicron_common::api::external::LookupType::ByCompositeId( + format!("{sled:?}"), + ), + }); + }; + // Convert the baseboard as necessary let baseboard = sled_agent_client::types::Baseboard::Gimlet { - identifier: sled.baseboard.serial.clone(), - model: sled.baseboard.part.clone(), - revision: sled.baseboard.revision, + identifier: sled.serial.clone(), + model: sled.part.clone(), + revision: sp.baseboard_revision.into(), }; // Make the call to sled-agent @@ -985,13 +1015,11 @@ impl super::Nexus { }, }; let sa = self.get_any_sled_agent(opctx).await?; - sa.add_sled_to_initialized_rack(&req).await.map_err(|e| { - Error::InternalError { - internal_message: format!( - "failed to add sled with baseboard {:?} to rack {}: {e}", - sled.baseboard, allocation.rack_id - ), - } + sa.sled_add(&req).await.map_err(|e| Error::InternalError { + internal_message: format!( + "failed to add sled with baseboard {:?} to rack {}: {e}", + sled, allocation.rack_id + ), })?; Ok(()) diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index 3e38558760..dde641a4ad 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -6,7 +6,7 @@ use super::{ console_api, device_auth, params, - params::ProjectSelector, + params::{ProjectSelector, UninitializedSledId}, shared::UninitializedSled, views::{ self, Certificate, Group, IdentityProvider, Image, IpPool, IpPoolRange, @@ -228,7 +228,7 @@ pub(crate) fn external_api() -> NexusApiDescription { api.register(switch_list)?; api.register(switch_view)?; api.register(sled_list_uninitialized)?; - api.register(add_sled_to_initialized_rack)?; + api.register(sled_add)?; api.register(user_builtin_list)?; api.register(user_builtin_view)?; @@ -4689,15 +4689,15 @@ async fn sled_list_uninitialized( path = "/v1/system/hardware/sleds/", tags = ["system/hardware"] }] -async fn add_sled_to_initialized_rack( +async fn sled_add( rqctx: RequestContext>, - sled: TypedBody, + sled: TypedBody, ) -> Result { let apictx = rqctx.context(); let nexus = &apictx.nexus; let handler = async { let opctx = crate::context::op_context_for_external_api(&rqctx).await?; - nexus.add_sled_to_initialized_rack(&opctx, sled.into_inner()).await?; + nexus.sled_add(&opctx, sled.into_inner()).await?; Ok(HttpResponseUpdatedNoContent()) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await diff --git a/nexus/tests/integration_tests/endpoints.rs b/nexus/tests/integration_tests/endpoints.rs index be0ea2a3f5..8708083124 100644 --- a/nexus/tests/integration_tests/endpoints.rs +++ b/nexus/tests/integration_tests/endpoints.rs @@ -20,10 +20,8 @@ use nexus_test_utils::SLED_AGENT_UUID; use nexus_test_utils::SWITCH_UUID; use nexus_types::external_api::params; use nexus_types::external_api::shared; -use nexus_types::external_api::shared::Baseboard; use nexus_types::external_api::shared::IpRange; use nexus_types::external_api::shared::Ipv4Range; -use nexus_types::external_api::shared::UninitializedSled; use omicron_common::api::external::AddressLotKind; use omicron_common::api::external::ByteCount; use omicron_common::api::external::IdentityMetadataCreateParams; @@ -41,7 +39,6 @@ use once_cell::sync::Lazy; use std::net::IpAddr; use std::net::Ipv4Addr; use std::str::FromStr; -use uuid::Uuid; pub static HARDWARE_RACK_URL: Lazy = Lazy::new(|| format!("/v1/system/hardware/racks/{}", RACK_UUID)); @@ -69,15 +66,10 @@ pub static HARDWARE_SLED_DISK_URL: Lazy = Lazy::new(|| { pub static SLED_INSTANCES_URL: Lazy = Lazy::new(|| { format!("/v1/system/hardware/sleds/{}/instances", SLED_AGENT_UUID) }); -pub static DEMO_UNINITIALIZED_SLED: Lazy = - Lazy::new(|| UninitializedSled { - baseboard: Baseboard { - serial: "demo-serial".to_string(), - part: "demo-part".to_string(), - revision: 6, - }, - rack_id: Uuid::new_v4(), - cubby: 1, +pub static DEMO_UNINITIALIZED_SLED: Lazy = + Lazy::new(|| params::UninitializedSledId { + serial: "demo-serial".to_string(), + part: "demo-part".to_string(), }); // Global policy diff --git a/nexus/tests/output/nexus_tags.txt b/nexus/tests/output/nexus_tags.txt index 10e7df7286..b607bbf1f3 100644 --- a/nexus/tests/output/nexus_tags.txt +++ b/nexus/tests/output/nexus_tags.txt @@ -117,13 +117,13 @@ snapshot_view GET /v1/snapshots/{snapshot} API operations found with tag "system/hardware" OPERATION ID METHOD URL PATH -add_sled_to_initialized_rack POST /v1/system/hardware/sleds networking_switch_port_apply_settings POST /v1/system/hardware/switch-port/{port}/settings networking_switch_port_clear_settings DELETE /v1/system/hardware/switch-port/{port}/settings networking_switch_port_list GET /v1/system/hardware/switch-port physical_disk_list GET /v1/system/hardware/disks rack_list GET /v1/system/hardware/racks rack_view GET /v1/system/hardware/racks/{rack_id} +sled_add POST /v1/system/hardware/sleds sled_instance_list GET /v1/system/hardware/sleds/{sled_id}/instances sled_list GET /v1/system/hardware/sleds sled_list_uninitialized GET /v1/system/hardware/sleds-uninitialized diff --git a/nexus/types/src/external_api/params.rs b/nexus/types/src/external_api/params.rs index df399e310c..6749794a9a 100644 --- a/nexus/types/src/external_api/params.rs +++ b/nexus/types/src/external_api/params.rs @@ -47,6 +47,23 @@ macro_rules! id_path_param { }; } +/// The unique hardware ID for a sled +#[derive( + Clone, + Debug, + Serialize, + Deserialize, + JsonSchema, + PartialOrd, + Ord, + PartialEq, + Eq, +)] +pub struct UninitializedSledId { + pub serial: String, + pub part: String, +} + path_param!(ProjectPath, project, "project"); path_param!(InstancePath, instance, "instance"); path_param!(NetworkInterfacePath, interface, "network interface"); diff --git a/nexus/types/src/inventory.rs b/nexus/types/src/inventory.rs index 9401727162..77bc73306d 100644 --- a/nexus/types/src/inventory.rs +++ b/nexus/types/src/inventory.rs @@ -20,6 +20,7 @@ use std::sync::Arc; use strum::EnumIter; use uuid::Uuid; +use crate::external_api::params::UninitializedSledId; use crate::external_api::shared::Baseboard; /// Results of collecting hardware/software inventory from various Omicron @@ -139,6 +140,12 @@ impl From for BaseboardId { } } +impl From for BaseboardId { + fn from(value: UninitializedSledId) -> Self { + BaseboardId { part_number: value.part, serial_number: value.serial } + } +} + /// Caboose contents found during a collection /// /// These are normalized in the database. Each distinct `Caboose` is assigned a diff --git a/openapi/nexus.json b/openapi/nexus.json index 4131460149..815cc399ae 100644 --- a/openapi/nexus.json +++ b/openapi/nexus.json @@ -3765,12 +3765,12 @@ "system/hardware" ], "summary": "Add a sled to an initialized rack", - "operationId": "add_sled_to_initialized_rack", + "operationId": "sled_add", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UninitializedSled" + "$ref": "#/components/schemas/UninitializedSledId" } } }, @@ -14909,6 +14909,22 @@ "rack_id" ] }, + "UninitializedSledId": { + "description": "The unique hardware ID for a sled", + "type": "object", + "properties": { + "part": { + "type": "string" + }, + "serial": { + "type": "string" + } + }, + "required": [ + "part", + "serial" + ] + }, "UninitializedSledResultsPage": { "description": "A single page of results", "type": "object", diff --git a/openapi/sled-agent.json b/openapi/sled-agent.json index 6076df6dbb..467fd32cb8 100644 --- a/openapi/sled-agent.json +++ b/openapi/sled-agent.json @@ -576,7 +576,7 @@ "/sleds": { "put": { "summary": "Add a sled to a rack that was already initialized via RSS", - "operationId": "add_sled_to_initialized_rack", + "operationId": "sled_add", "requestBody": { "content": { "application/json": { diff --git a/sled-agent/src/http_entrypoints.rs b/sled-agent/src/http_entrypoints.rs index 8c8a5f2a03..26a0d2ddc2 100644 --- a/sled-agent/src/http_entrypoints.rs +++ b/sled-agent/src/http_entrypoints.rs @@ -77,7 +77,7 @@ pub fn api() -> SledApiDescription { api.register(uplink_ensure)?; api.register(read_network_bootstore_config_cache)?; api.register(write_network_bootstore_config)?; - api.register(add_sled_to_initialized_rack)?; + api.register(sled_add)?; api.register(metrics_collect)?; api.register(host_os_write_start)?; api.register(host_os_write_status_get)?; @@ -713,7 +713,7 @@ async fn write_network_bootstore_config( method = PUT, path = "/sleds" }] -async fn add_sled_to_initialized_rack( +async fn sled_add( rqctx: RequestContext, body: TypedBody, ) -> Result { @@ -731,7 +731,7 @@ async fn add_sled_to_initialized_rack( )); } - crate::sled_agent::add_sled_to_initialized_rack( + crate::sled_agent::sled_add( sa.logger().clone(), request.sled_id, request.start_request, diff --git a/sled-agent/src/sled_agent.rs b/sled-agent/src/sled_agent.rs index 5f278b7f38..621d003268 100644 --- a/sled-agent/src/sled_agent.rs +++ b/sled-agent/src/sled_agent.rs @@ -1105,7 +1105,7 @@ pub enum AddSledError { } /// Add a sled to an initialized rack. -pub async fn add_sled_to_initialized_rack( +pub async fn sled_add( log: Logger, sled_id: Baseboard, request: StartSledAgentRequest, From e760630902e3bd5e2e0a0a5e4518ffa3a0d05df3 Mon Sep 17 00:00:00 2001 From: Alan Hanson Date: Sat, 30 Dec 2023 12:46:21 -0800 Subject: [PATCH 29/33] Added dtrace_user and dtrace_proc permissions for oxz_ zones (#4736) This allows dtrace inside the oxz_ zones created by Omicron. Partial fix for https://github.com/oxidecomputer/omicron/issues/4731 Co-authored-by: Alan Hanson --- sled-agent/src/services.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/sled-agent/src/services.rs b/sled-agent/src/services.rs index a9000a1c4b..ddfea5d596 100644 --- a/sled-agent/src/services.rs +++ b/sled-agent/src/services.rs @@ -1260,11 +1260,14 @@ impl ServiceManager { // Check the services intended to run in the zone to determine whether any // additional privileges need to be enabled for the zone. fn privs_needed(zone_args: &ZoneArgs<'_>) -> Vec { - let mut needed = Vec::new(); + let mut needed = vec![ + "default".to_string(), + "dtrace_user".to_string(), + "dtrace_proc".to_string(), + ]; for svc_details in zone_args.sled_local_services() { match svc_details { SwitchService::Tfport { .. } => { - needed.push("default".to_string()); needed.push("sys_dl_config".to_string()); } _ => (), @@ -1275,7 +1278,6 @@ impl ServiceManager { match omicron_zone_type { OmicronZoneType::BoundaryNtp { .. } | OmicronZoneType::InternalNtp { .. } => { - needed.push("default".to_string()); needed.push("sys_time".to_string()); needed.push("proc_priocntl".to_string()); } From 44fea0d0acdb90f9fe55a8ae14294abe6890c0b7 Mon Sep 17 00:00:00 2001 From: Adam Leventhal Date: Sat, 30 Dec 2023 15:15:56 -0800 Subject: [PATCH 30/33] update schemars; address duplicate type names (#4737) --- Cargo.lock | 8 +- Cargo.toml | 2 +- .../src/db/datastore/switch_port.rs | 4 +- nexus/src/app/rack.rs | 12 +- nexus/tests/integration_tests/switch_port.rs | 12 +- nexus/types/src/external_api/params.rs | 18 +-- openapi/nexus.json | 149 +++++++++++++++++- workspace-hack/Cargo.toml | 4 +- 8 files changed, 174 insertions(+), 35 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3cdf3dd678..85e42458d4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7245,9 +7245,9 @@ dependencies = [ [[package]] name = "schemars" -version = "0.8.13" +version = "0.8.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "763f8cd0d4c71ed8389c90cb8100cba87e763bd01a8e614d4f0af97bcd50a161" +checksum = "45a28f4c49489add4ce10783f7911893516f15afe45d015608d41faca6bc4d29" dependencies = [ "bytes", "chrono", @@ -7260,9 +7260,9 @@ dependencies = [ [[package]] name = "schemars_derive" -version = "0.8.13" +version = "0.8.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0f696e21e10fa546b7ffb1c9672c6de8fbc7a81acf59524386d8639bf12737" +checksum = "c767fd6fa65d9ccf9cf026122c1b555f2ef9a4f0cea69da4d7dbc3e258d30967" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index d4f81b0310..f7256ce8b4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -314,7 +314,7 @@ rustfmt-wrapper = "0.2" rustls = "0.21.9" rustyline = "12.0.0" samael = { git = "https://github.com/njaremko/samael", features = ["xmlsec"], branch = "master" } -schemars = "0.8.12" +schemars = "0.8.16" secrecy = "0.8.0" semver = { version = "1.0.20", features = ["std", "serde"] } serde = { version = "1.0", default-features = false, features = [ "derive" ] } diff --git a/nexus/db-queries/src/db/datastore/switch_port.rs b/nexus/db-queries/src/db/datastore/switch_port.rs index 221feee23c..4771768e43 100644 --- a/nexus/db-queries/src/db/datastore/switch_port.rs +++ b/nexus/db-queries/src/db/datastore/switch_port.rs @@ -1196,7 +1196,7 @@ mod test { use nexus_test_utils::db::test_setup_database; use nexus_types::external_api::params::{ BgpAnnounceSetCreate, BgpConfigCreate, BgpPeer, BgpPeerConfig, - SwitchPortConfig, SwitchPortGeometry, SwitchPortSettingsCreate, + SwitchPortConfigCreate, SwitchPortGeometry, SwitchPortSettingsCreate, }; use omicron_common::api::external::{ IdentityMetadataCreateParams, Name, NameOrId, @@ -1250,7 +1250,7 @@ mod test { name: "test-settings".parse().unwrap(), description: "test settings".into(), }, - port_config: SwitchPortConfig { + port_config: SwitchPortConfigCreate { geometry: SwitchPortGeometry::Qsfp28x1, }, groups: Vec::new(), diff --git a/nexus/src/app/rack.rs b/nexus/src/app/rack.rs index a0dcb7fcb1..23ee39415f 100644 --- a/nexus/src/app/rack.rs +++ b/nexus/src/app/rack.rs @@ -27,10 +27,10 @@ use nexus_types::external_api::params::BgpAnnounceSetCreate; use nexus_types::external_api::params::BgpAnnouncementCreate; use nexus_types::external_api::params::BgpConfigCreate; use nexus_types::external_api::params::BgpPeer; -use nexus_types::external_api::params::LinkConfig; -use nexus_types::external_api::params::LldpServiceConfig; +use nexus_types::external_api::params::LinkConfigCreate; +use nexus_types::external_api::params::LldpServiceConfigCreate; use nexus_types::external_api::params::RouteConfig; -use nexus_types::external_api::params::SwitchPortConfig; +use nexus_types::external_api::params::SwitchPortConfigCreate; use nexus_types::external_api::params::UninitializedSledId; use nexus_types::external_api::params::{ AddressLotCreate, BgpPeerConfig, LoopbackAddressCreate, Route, SiloCreate, @@ -587,7 +587,7 @@ impl super::Nexus { description: "initial uplink configuration".to_string(), }; - let port_config = SwitchPortConfig { + let port_config = SwitchPortConfigCreate { geometry: nexus_types::external_api::params::SwitchPortGeometry::Qsfp28x1, }; @@ -653,9 +653,9 @@ impl super::Nexus { .bgp_peers .insert("phy0".to_string(), BgpPeerConfig { peers }); - let link = LinkConfig { + let link = LinkConfigCreate { mtu: 1500, //TODO https://github.com/oxidecomputer/omicron/issues/2274 - lldp: LldpServiceConfig { + lldp: LldpServiceConfigCreate { enabled: false, lldp_config: None, }, diff --git a/nexus/tests/integration_tests/switch_port.rs b/nexus/tests/integration_tests/switch_port.rs index df4d96c6d1..c6e774be09 100644 --- a/nexus/tests/integration_tests/switch_port.rs +++ b/nexus/tests/integration_tests/switch_port.rs @@ -11,9 +11,9 @@ use nexus_test_utils_macros::nexus_test; use nexus_types::external_api::params::{ Address, AddressConfig, AddressLotBlockCreate, AddressLotCreate, BgpAnnounceSetCreate, BgpAnnouncementCreate, BgpConfigCreate, BgpPeer, - BgpPeerConfig, LinkConfig, LinkFec, LinkSpeed, LldpServiceConfig, Route, - RouteConfig, SwitchInterfaceConfig, SwitchInterfaceKind, - SwitchPortApplySettings, SwitchPortSettingsCreate, + BgpPeerConfig, LinkConfigCreate, LinkFec, LinkSpeed, + LldpServiceConfigCreate, Route, RouteConfig, SwitchInterfaceConfigCreate, + SwitchInterfaceKind, SwitchPortApplySettings, SwitchPortSettingsCreate, }; use nexus_types::external_api::views::Rack; use omicron_common::api::external::{ @@ -113,9 +113,9 @@ async fn test_port_settings_basic_crud(ctx: &ControlPlaneTestContext) { // links settings.links.insert( "phy0".into(), - LinkConfig { + LinkConfigCreate { mtu: 4700, - lldp: LldpServiceConfig { enabled: false, lldp_config: None }, + lldp: LldpServiceConfigCreate { enabled: false, lldp_config: None }, fec: LinkFec::None, speed: LinkSpeed::Speed100G, autoneg: false, @@ -124,7 +124,7 @@ async fn test_port_settings_basic_crud(ctx: &ControlPlaneTestContext) { // interfaces settings.interfaces.insert( "phy0".into(), - SwitchInterfaceConfig { + SwitchInterfaceConfigCreate { v6_enabled: true, kind: SwitchInterfaceKind::Primary, }, diff --git a/nexus/types/src/external_api/params.rs b/nexus/types/src/external_api/params.rs index 6749794a9a..209d1f607c 100644 --- a/nexus/types/src/external_api/params.rs +++ b/nexus/types/src/external_api/params.rs @@ -1387,14 +1387,14 @@ pub struct SwtichPortSettingsGroupCreate { pub struct SwitchPortSettingsCreate { #[serde(flatten)] pub identity: IdentityMetadataCreateParams, - pub port_config: SwitchPortConfig, + pub port_config: SwitchPortConfigCreate, pub groups: Vec, /// Links indexed by phy name. On ports that are not broken out, this is /// always phy0. On a 2x breakout the options are phy0 and phy1, on 4x /// phy0-phy3, etc. - pub links: HashMap, + pub links: HashMap, /// Interfaces indexed by link name. - pub interfaces: HashMap, + pub interfaces: HashMap, /// Routes indexed by interface name. pub routes: HashMap, /// BGP peers indexed by interface name. @@ -1407,7 +1407,7 @@ impl SwitchPortSettingsCreate { pub fn new(identity: IdentityMetadataCreateParams) -> Self { Self { identity, - port_config: SwitchPortConfig { + port_config: SwitchPortConfigCreate { geometry: SwitchPortGeometry::Qsfp28x1, }, groups: Vec::new(), @@ -1423,7 +1423,7 @@ impl SwitchPortSettingsCreate { /// Physical switch port configuration. #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] #[serde(rename_all = "snake_case")] -pub struct SwitchPortConfig { +pub struct SwitchPortConfigCreate { /// Link geometry for the switch port. pub geometry: SwitchPortGeometry, } @@ -1526,12 +1526,12 @@ impl From for LinkSpeed { /// Switch link configuration. #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] -pub struct LinkConfig { +pub struct LinkConfigCreate { /// Maximum transmission unit for the link. pub mtu: u16, /// The link-layer discovery protocol (LLDP) configuration for the link. - pub lldp: LldpServiceConfig, + pub lldp: LldpServiceConfigCreate, /// The forward error correction mode of the link. pub fec: LinkFec, @@ -1546,7 +1546,7 @@ pub struct LinkConfig { /// The LLDP configuration associated with a port. LLDP may be either enabled or /// disabled, if enabled, an LLDP configuration must be provided by name or id. #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] -pub struct LldpServiceConfig { +pub struct LldpServiceConfigCreate { /// Whether or not LLDP is enabled. pub enabled: bool, @@ -1558,7 +1558,7 @@ pub struct LldpServiceConfig { /// A layer-3 switch interface configuration. When IPv6 is enabled, a link local /// address will be created for the interface. #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] -pub struct SwitchInterfaceConfig { +pub struct SwitchInterfaceConfigCreate { /// Whether or not IPv6 is enabled. pub v6_enabled: bool, diff --git a/openapi/nexus.json b/openapi/nexus.json index 815cc399ae..3b8b3525dc 100644 --- a/openapi/nexus.json +++ b/openapi/nexus.json @@ -12496,7 +12496,7 @@ "minLength": 1, "maxLength": 11 }, - "LinkConfig": { + "LinkConfigCreate": { "description": "Switch link configuration.", "type": "object", "properties": { @@ -12516,7 +12516,7 @@ "description": "The link-layer discovery protocol (LLDP) configuration for the link.", "allOf": [ { - "$ref": "#/components/schemas/LldpServiceConfig" + "$ref": "#/components/schemas/LldpServiceConfigCreate" } ] }, @@ -12638,6 +12638,31 @@ ] }, "LldpServiceConfig": { + "description": "A link layer discovery protocol (LLDP) service configuration.", + "type": "object", + "properties": { + "enabled": { + "description": "Whether or not the LLDP service is enabled.", + "type": "boolean" + }, + "id": { + "description": "The id of this LLDP service instance.", + "type": "string", + "format": "uuid" + }, + "lldp_config_id": { + "nullable": true, + "description": "The link-layer discovery protocol configuration for this service.", + "type": "string", + "format": "uuid" + } + }, + "required": [ + "enabled", + "id" + ] + }, + "LldpServiceConfigCreate": { "description": "The LLDP configuration associated with a port. LLDP may be either enabled or disabled, if enabled, an LLDP configuration must be provided by name or id.", "type": "object", "properties": { @@ -14251,6 +14276,45 @@ ] }, "SwitchInterfaceConfig": { + "description": "A switch port interface configuration for a port settings object.", + "type": "object", + "properties": { + "id": { + "description": "A unique identifier for this switch interface.", + "type": "string", + "format": "uuid" + }, + "interface_name": { + "description": "The name of this switch interface.", + "type": "string" + }, + "kind": { + "description": "The switch interface kind.", + "allOf": [ + { + "$ref": "#/components/schemas/SwitchInterfaceKind2" + } + ] + }, + "port_settings_id": { + "description": "The port settings object this switch interface configuration belongs to.", + "type": "string", + "format": "uuid" + }, + "v6_enabled": { + "description": "Whether or not IPv6 is enabled on this interface.", + "type": "boolean" + } + }, + "required": [ + "id", + "interface_name", + "kind", + "port_settings_id", + "v6_enabled" + ] + }, + "SwitchInterfaceConfigCreate": { "description": "A layer-3 switch interface configuration. When IPv6 is enabled, a link local address will be created for the interface.", "type": "object", "properties": { @@ -14329,6 +14393,32 @@ } ] }, + "SwitchInterfaceKind2": { + "description": "Describes the kind of an switch interface.", + "oneOf": [ + { + "description": "Primary interfaces are associated with physical links. There is exactly one primary interface per physical link.", + "type": "string", + "enum": [ + "primary" + ] + }, + { + "description": "VLAN interfaces allow physical interfaces to be multiplexed onto multiple logical links, each distinguished by a 12-bit 802.1Q Ethernet tag.", + "type": "string", + "enum": [ + "vlan" + ] + }, + { + "description": "Loopback interfaces are anchors for IP addresses that are not specific to any particular port.", + "type": "string", + "enum": [ + "loopback" + ] + } + ] + }, "SwitchLocation": { "description": "Identifies switch physical location", "oneOf": [ @@ -14467,6 +14557,29 @@ ] }, "SwitchPortConfig": { + "description": "A physical port configuration for a port settings object.", + "type": "object", + "properties": { + "geometry": { + "description": "The physical link geometry of the port.", + "allOf": [ + { + "$ref": "#/components/schemas/SwitchPortGeometry2" + } + ] + }, + "port_settings_id": { + "description": "The id of the port settings object this configuration belongs to.", + "type": "string", + "format": "uuid" + } + }, + "required": [ + "geometry", + "port_settings_id" + ] + }, + "SwitchPortConfigCreate": { "description": "Physical switch port configuration.", "type": "object", "properties": { @@ -14509,6 +14622,32 @@ } ] }, + "SwitchPortGeometry2": { + "description": "The link geometry associated with a switch port.", + "oneOf": [ + { + "description": "The port contains a single QSFP28 link with four lanes.", + "type": "string", + "enum": [ + "qsfp28x1" + ] + }, + { + "description": "The port contains two QSFP28 links each with two lanes.", + "type": "string", + "enum": [ + "qsfp28x2" + ] + }, + { + "description": "The port contains four SFP28 links each with one lane.", + "type": "string", + "enum": [ + "sfp28x4" + ] + } + ] + }, "SwitchPortLinkConfig": { "description": "A link configuration for a port settings object.", "type": "object", @@ -14677,21 +14816,21 @@ "description": "Interfaces indexed by link name.", "type": "object", "additionalProperties": { - "$ref": "#/components/schemas/SwitchInterfaceConfig" + "$ref": "#/components/schemas/SwitchInterfaceConfigCreate" } }, "links": { "description": "Links indexed by phy name. On ports that are not broken out, this is always phy0. On a 2x breakout the options are phy0 and phy1, on 4x phy0-phy3, etc.", "type": "object", "additionalProperties": { - "$ref": "#/components/schemas/LinkConfig" + "$ref": "#/components/schemas/LinkConfigCreate" } }, "name": { "$ref": "#/components/schemas/Name" }, "port_config": { - "$ref": "#/components/schemas/SwitchPortConfig" + "$ref": "#/components/schemas/SwitchPortConfigCreate" }, "routes": { "description": "Routes indexed by interface name.", diff --git a/workspace-hack/Cargo.toml b/workspace-hack/Cargo.toml index 8998f7594b..653e8b370a 100644 --- a/workspace-hack/Cargo.toml +++ b/workspace-hack/Cargo.toml @@ -85,7 +85,7 @@ regex-automata = { version = "0.4.3", default-features = false, features = ["dfa regex-syntax = { version = "0.8.2" } reqwest = { version = "0.11.22", features = ["blocking", "json", "rustls-tls", "stream"] } ring = { version = "0.17.7", features = ["std"] } -schemars = { version = "0.8.13", features = ["bytes", "chrono", "uuid1"] } +schemars = { version = "0.8.16", features = ["bytes", "chrono", "uuid1"] } semver = { version = "1.0.20", features = ["serde"] } serde = { version = "1.0.193", features = ["alloc", "derive", "rc"] } serde_json = { version = "1.0.108", features = ["raw_value", "unbounded_depth"] } @@ -188,7 +188,7 @@ regex-automata = { version = "0.4.3", default-features = false, features = ["dfa regex-syntax = { version = "0.8.2" } reqwest = { version = "0.11.22", features = ["blocking", "json", "rustls-tls", "stream"] } ring = { version = "0.17.7", features = ["std"] } -schemars = { version = "0.8.13", features = ["bytes", "chrono", "uuid1"] } +schemars = { version = "0.8.16", features = ["bytes", "chrono", "uuid1"] } semver = { version = "1.0.20", features = ["serde"] } serde = { version = "1.0.193", features = ["alloc", "derive", "rc"] } serde_json = { version = "1.0.108", features = ["raw_value", "unbounded_depth"] } From 9274beb0112e50014e8f14cba2f3fbec17a64421 Mon Sep 17 00:00:00 2001 From: "oxide-renovate[bot]" <146848827+oxide-renovate[bot]@users.noreply.github.com> Date: Sun, 31 Dec 2023 05:20:06 +0000 Subject: [PATCH 31/33] Update taiki-e/install-action digest to 56ab793 (#4738) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [taiki-e/install-action](https://togithub.com/taiki-e/install-action) | action | digest | [`0f94aa2` -> `56ab793`](https://togithub.com/taiki-e/install-action/compare/0f94aa2...56ab793) | --- ### Configuration 📅 **Schedule**: Branch creation - "after 8pm,before 6am" in timezone America/Los_Angeles, Automerge - "after 8pm,before 6am" in timezone America/Los_Angeles. 🚦 **Automerge**: Enabled. â™» **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Renovate Bot](https://togithub.com/renovatebot/renovate). Co-authored-by: oxide-renovate[bot] <146848827+oxide-renovate[bot]@users.noreply.github.com> --- .github/workflows/hakari.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/hakari.yml b/.github/workflows/hakari.yml index e70b959f8a..4d9812a44e 100644 --- a/.github/workflows/hakari.yml +++ b/.github/workflows/hakari.yml @@ -24,7 +24,7 @@ jobs: with: toolchain: stable - name: Install cargo-hakari - uses: taiki-e/install-action@0f94aa2032e24d01f7ae1cc63f71b13418365efd # v2 + uses: taiki-e/install-action@56ab7930c591507f833cbaed864d201386d518a8 # v2 with: tool: cargo-hakari - name: Check workspace-hack Cargo.toml is up-to-date From cfcf209c4f4b278bbfe27d19c115d657c63b5d76 Mon Sep 17 00:00:00 2001 From: Benjamin Leonard Date: Tue, 2 Jan 2024 15:59:33 +0000 Subject: [PATCH 32/33] Fix endpoint description casing (#4742) Noticed some inconsistencies with the endpoint descriptions. Fixes https://github.com/oxidecomputer/docs/issues/253 --- nexus/src/external_api/http_entrypoints.rs | 14 +++++++------- openapi/nexus.json | 14 +++++++------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index dde641a4ad..5ac782aee6 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -1294,7 +1294,7 @@ async fn project_policy_update( // IP Pools -/// List all IP Pools that can be used by a given project. +/// List all IP pools that can be used by a given project #[endpoint { method = GET, path = "/v1/ip-pools", @@ -1452,7 +1452,7 @@ async fn ip_pool_view( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } -/// Delete an IP Pool +/// Delete an IP pool #[endpoint { method = DELETE, path = "/v1/system/ip-pools/{pool}", @@ -1474,7 +1474,7 @@ async fn ip_pool_delete( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } -/// Update an IP Pool +/// Update an IP pool #[endpoint { method = PUT, path = "/v1/system/ip-pools/{pool}", @@ -1701,7 +1701,7 @@ async fn ip_pool_service_range_remove( // Floating IP Addresses -/// List all Floating IPs +/// List all floating IPs #[endpoint { method = GET, path = "/v1/floating-ips", @@ -1733,7 +1733,7 @@ async fn floating_ip_list( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } -/// Create a Floating IP +/// Create a floating IP #[endpoint { method = POST, path = "/v1/floating-ips", @@ -1759,7 +1759,7 @@ async fn floating_ip_create( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } -/// Delete a Floating IP +/// Delete a floating IP #[endpoint { method = DELETE, path = "/v1/floating-ips/{floating_ip}", @@ -4757,7 +4757,7 @@ async fn sled_view( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } -/// Set the sled's provision state. +/// Set the sled's provision state #[endpoint { method = PUT, path = "/v1/system/hardware/sleds/{sled_id}/provision-state", diff --git a/openapi/nexus.json b/openapi/nexus.json index 3b8b3525dc..f2433e5512 100644 --- a/openapi/nexus.json +++ b/openapi/nexus.json @@ -809,7 +809,7 @@ "tags": [ "floating-ips" ], - "summary": "List all Floating IPs", + "summary": "List all floating IPs", "operationId": "floating_ip_list", "parameters": [ { @@ -876,7 +876,7 @@ "tags": [ "floating-ips" ], - "summary": "Create a Floating IP", + "summary": "Create a floating IP", "operationId": "floating_ip_create", "parameters": [ { @@ -968,7 +968,7 @@ "tags": [ "floating-ips" ], - "summary": "Delete a Floating IP", + "summary": "Delete a floating IP", "operationId": "floating_ip_delete", "parameters": [ { @@ -2154,7 +2154,7 @@ "tags": [ "projects" ], - "summary": "List all IP Pools that can be used by a given project.", + "summary": "List all IP pools that can be used by a given project", "operationId": "project_ip_pool_list", "parameters": [ { @@ -3971,7 +3971,7 @@ "tags": [ "system/hardware" ], - "summary": "Set the sled's provision state.", + "summary": "Set the sled's provision state", "operationId": "sled_set_provision_state", "parameters": [ { @@ -4783,7 +4783,7 @@ "tags": [ "system/networking" ], - "summary": "Update an IP Pool", + "summary": "Update an IP pool", "operationId": "ip_pool_update", "parameters": [ { @@ -4829,7 +4829,7 @@ "tags": [ "system/networking" ], - "summary": "Delete an IP Pool", + "summary": "Delete an IP pool", "operationId": "ip_pool_delete", "parameters": [ { From b984facc5c83e8136a56d18dc2370ab7a4e360dd Mon Sep 17 00:00:00 2001 From: Sean Klein Date: Wed, 3 Jan 2024 10:37:25 -0800 Subject: [PATCH 33/33] Optimize OID lookup for user-defined types (#4735) # Background Looking up OIDs for ENUMs consumes a non-trival amount of time during initialization, since they are synchronously fetched one-by-one. Although this is amortized for long-lived connections, it incurs a cost on all tests using CRDB, and can be optimized. Additionally, these OID caches are per-connection, which incurs redundant work when we have a connection pool (and we do, with bb8). See #4733 for additional context. Feedback from the Diesel maintainers: > I think it would be reasonable to share this information at pool level... > > [OIDs are] currently queried by one type at the time as we argued that it won't matter for long running applications. There you do the overhead once per connection and then you just hit the cache. I don't think we did really consider short running applications (like tests) for that. # This PR 1. Performs a one-time-lookup of user-defined ENUM OIDs on the first connection, and shares it between subsequent connections. This pre-populates the cache used by Diesel. 2. Modifies all `SqlType` derives to ensure that the `schema` is specified as `public` (this ensures a cache hit) 3. Adds a test to compare the "list of user-defined enums to look up" with the current DB schema, and ensures they're in-sync. # Results I focused on optimizing `integration_tests::basic::test_ping`, as this is very close to a "no-op" version of the `#[nexus_test]`. I wrote a script to repeatedly execute this test in a loop (excluding the first result, as a "warm-up" run to populate CRDB). - Before: Average time over 10 runs: 10.42 seconds - After: Average time over 10 runs: 9.47 seconds That's a small difference, but hopefully it'll provide a dent over "all our tests", and avoid additional user-defined types from slowing down all tests. Part of https://github.com/oxidecomputer/omicron/issues/4733 --- Cargo.lock | 1 + nexus/db-model/src/address_lot.rs | 2 +- nexus/db-model/src/block_size.rs | 2 +- nexus/db-model/src/dataset_kind.rs | 2 +- nexus/db-model/src/dns.rs | 2 +- nexus/db-model/src/external_ip.rs | 2 +- nexus/db-model/src/identity_provider.rs | 2 +- nexus/db-model/src/instance_state.rs | 2 +- nexus/db-model/src/inventory.rs | 10 +- nexus/db-model/src/network_interface.rs | 2 +- nexus/db-model/src/physical_disk_kind.rs | 2 +- nexus/db-model/src/producer_endpoint.rs | 2 +- nexus/db-model/src/role_assignment.rs | 2 +- nexus/db-model/src/saga_types.rs | 2 +- nexus/db-model/src/service_kind.rs | 2 +- nexus/db-model/src/silo.rs | 4 +- nexus/db-model/src/sled_provision_state.rs | 2 +- nexus/db-model/src/sled_resource_kind.rs | 2 +- nexus/db-model/src/snapshot.rs | 2 +- nexus/db-model/src/switch_interface.rs | 2 +- nexus/db-model/src/switch_port.rs | 6 +- nexus/db-model/src/system_update.rs | 4 +- nexus/db-model/src/update_artifact.rs | 2 +- nexus/db-model/src/vpc_firewall_rule.rs | 8 +- nexus/db-model/src/vpc_route.rs | 2 +- nexus/db-model/src/vpc_router.rs | 2 +- nexus/db-queries/Cargo.toml | 1 + nexus/db-queries/src/db/mod.rs | 1 + nexus/db-queries/src/db/pool.rs | 31 +-- nexus/db-queries/src/db/pool_connection.rs | 303 +++++++++++++++++++++ 30 files changed, 346 insertions(+), 63 deletions(-) create mode 100644 nexus/db-queries/src/db/pool_connection.rs diff --git a/Cargo.lock b/Cargo.lock index 85e42458d4..440e2fe296 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4186,6 +4186,7 @@ dependencies = [ "pem", "petgraph", "pq-sys", + "pretty_assertions", "rand 0.8.5", "rcgen", "ref-cast", diff --git a/nexus/db-model/src/address_lot.rs b/nexus/db-model/src/address_lot.rs index de5a4654c5..4fef2466e6 100644 --- a/nexus/db-model/src/address_lot.rs +++ b/nexus/db-model/src/address_lot.rs @@ -13,7 +13,7 @@ use uuid::Uuid; impl_enum_type!( #[derive(SqlType, Debug, Clone, Copy)] - #[diesel(postgres_type(name = "address_lot_kind"))] + #[diesel(postgres_type(name = "address_lot_kind", schema = "public"))] pub struct AddressLotKindEnum; #[derive( diff --git a/nexus/db-model/src/block_size.rs b/nexus/db-model/src/block_size.rs index 1a090f1e44..c947f85388 100644 --- a/nexus/db-model/src/block_size.rs +++ b/nexus/db-model/src/block_size.rs @@ -9,7 +9,7 @@ use serde::{Deserialize, Serialize}; impl_enum_type!( #[derive(SqlType, Debug, QueryId)] - #[diesel(postgres_type(name = "block_size"))] + #[diesel(postgres_type(name = "block_size", schema = "public"))] pub struct BlockSizeEnum; #[derive(Copy, Clone, Debug, AsExpression, FromSqlRow, Serialize, Deserialize, PartialEq)] diff --git a/nexus/db-model/src/dataset_kind.rs b/nexus/db-model/src/dataset_kind.rs index d068f48fd3..00317592e8 100644 --- a/nexus/db-model/src/dataset_kind.rs +++ b/nexus/db-model/src/dataset_kind.rs @@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize}; impl_enum_type!( #[derive(SqlType, Debug, QueryId)] - #[diesel(postgres_type(name = "dataset_kind"))] + #[diesel(postgres_type(name = "dataset_kind", schema = "public"))] pub struct DatasetKindEnum; #[derive(Clone, Debug, AsExpression, FromSqlRow, Serialize, Deserialize, PartialEq)] diff --git a/nexus/db-model/src/dns.rs b/nexus/db-model/src/dns.rs index 6b37362c42..56dd1e0547 100644 --- a/nexus/db-model/src/dns.rs +++ b/nexus/db-model/src/dns.rs @@ -16,7 +16,7 @@ use uuid::Uuid; impl_enum_type!( #[derive(SqlType, Debug)] - #[diesel(postgres_type(name = "dns_group"))] + #[diesel(postgres_type(name = "dns_group", schema = "public"))] pub struct DnsGroupEnum; #[derive(Clone, Copy, Debug, AsExpression, FromSqlRow, PartialEq)] diff --git a/nexus/db-model/src/external_ip.rs b/nexus/db-model/src/external_ip.rs index 1a755f0396..6b3f8d5110 100644 --- a/nexus/db-model/src/external_ip.rs +++ b/nexus/db-model/src/external_ip.rs @@ -29,7 +29,7 @@ use uuid::Uuid; impl_enum_type!( #[derive(SqlType, Debug, Clone, Copy, QueryId)] - #[diesel(postgres_type(name = "ip_kind"))] + #[diesel(postgres_type(name = "ip_kind", schema = "public"))] pub struct IpKindEnum; #[derive(Clone, Copy, Debug, AsExpression, FromSqlRow, PartialEq)] diff --git a/nexus/db-model/src/identity_provider.rs b/nexus/db-model/src/identity_provider.rs index 6bc55b3220..869d64bc7e 100644 --- a/nexus/db-model/src/identity_provider.rs +++ b/nexus/db-model/src/identity_provider.rs @@ -13,7 +13,7 @@ use uuid::Uuid; impl_enum_type!( #[derive(SqlType, Debug, QueryId)] - #[diesel(postgres_type(name = "provider_type"))] + #[diesel(postgres_type(name = "provider_type", schema = "public"))] pub struct IdentityProviderTypeEnum; #[derive(Copy, Clone, Debug, AsExpression, FromSqlRow, Serialize, Deserialize, PartialEq)] diff --git a/nexus/db-model/src/instance_state.rs b/nexus/db-model/src/instance_state.rs index 6b4c71da79..7b98850b43 100644 --- a/nexus/db-model/src/instance_state.rs +++ b/nexus/db-model/src/instance_state.rs @@ -11,7 +11,7 @@ use std::io::Write; impl_enum_wrapper!( #[derive(SqlType, Debug)] - #[diesel(postgres_type(name = "instance_state"))] + #[diesel(postgres_type(name = "instance_state", schema = "public"))] pub struct InstanceStateEnum; #[derive(Clone, Debug, PartialEq, AsExpression, FromSqlRow, Serialize, Deserialize)] diff --git a/nexus/db-model/src/inventory.rs b/nexus/db-model/src/inventory.rs index d94334787d..72671fde98 100644 --- a/nexus/db-model/src/inventory.rs +++ b/nexus/db-model/src/inventory.rs @@ -26,7 +26,7 @@ use uuid::Uuid; // See [`nexus_types::inventory::PowerState`]. impl_enum_type!( #[derive(SqlType, Debug, QueryId)] - #[diesel(postgres_type(name = "hw_power_state"))] + #[diesel(postgres_type(name = "hw_power_state", schema = "public"))] pub struct HwPowerStateEnum; #[derive(Copy, Clone, Debug, AsExpression, FromSqlRow, PartialEq)] @@ -62,7 +62,7 @@ impl From for PowerState { // See [`nexus_types::inventory::RotSlot`]. impl_enum_type!( #[derive(SqlType, Debug, QueryId)] - #[diesel(postgres_type(name = "hw_rot_slot"))] + #[diesel(postgres_type(name = "hw_rot_slot", schema = "public"))] pub struct HwRotSlotEnum; #[derive(Copy, Clone, Debug, AsExpression, FromSqlRow, PartialEq)] @@ -95,7 +95,7 @@ impl From for RotSlot { // See [`nexus_types::inventory::CabooseWhich`]. impl_enum_type!( #[derive(SqlType, Debug, QueryId)] - #[diesel(postgres_type(name = "caboose_which"))] + #[diesel(postgres_type(name = "caboose_which", schema = "public"))] pub struct CabooseWhichEnum; #[derive(Copy, Clone, Debug, AsExpression, FromSqlRow, PartialEq)] @@ -136,7 +136,7 @@ impl From for nexus_types::inventory::CabooseWhich { // See [`nexus_types::inventory::RotPageWhich`]. impl_enum_type!( #[derive(SqlType, Debug, QueryId)] - #[diesel(postgres_type(name = "root_of_trust_page_which"))] + #[diesel(postgres_type(name = "root_of_trust_page_which", schema = "public"))] pub struct RotPageWhichEnum; #[derive(Copy, Clone, Debug, AsExpression, FromSqlRow, PartialEq)] @@ -189,7 +189,7 @@ impl From for nexus_types::inventory::RotPageWhich { // See [`nexus_types::inventory::SpType`]. impl_enum_type!( #[derive(SqlType, Debug, QueryId)] - #[diesel(postgres_type(name = "sp_type"))] + #[diesel(postgres_type(name = "sp_type", schema = "public"))] pub struct SpTypeEnum; #[derive( diff --git a/nexus/db-model/src/network_interface.rs b/nexus/db-model/src/network_interface.rs index ada2148516..3d3fabbe66 100644 --- a/nexus/db-model/src/network_interface.rs +++ b/nexus/db-model/src/network_interface.rs @@ -19,7 +19,7 @@ use uuid::Uuid; impl_enum_type! { #[derive(SqlType, QueryId, Debug, Clone, Copy)] - #[diesel(postgres_type(name = "network_interface_kind"))] + #[diesel(postgres_type(name = "network_interface_kind", schema = "public"))] pub struct NetworkInterfaceKindEnum; #[derive(Clone, Copy, Debug, AsExpression, FromSqlRow, PartialEq)] diff --git a/nexus/db-model/src/physical_disk_kind.rs b/nexus/db-model/src/physical_disk_kind.rs index a55d42beef..fe86c801d0 100644 --- a/nexus/db-model/src/physical_disk_kind.rs +++ b/nexus/db-model/src/physical_disk_kind.rs @@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize}; impl_enum_type!( #[derive(Clone, SqlType, Debug, QueryId)] - #[diesel(postgres_type(name = "physical_disk_kind"))] + #[diesel(postgres_type(name = "physical_disk_kind", schema = "public"))] pub struct PhysicalDiskKindEnum; #[derive(Clone, Copy, Debug, AsExpression, FromSqlRow, Serialize, Deserialize, PartialEq)] diff --git a/nexus/db-model/src/producer_endpoint.rs b/nexus/db-model/src/producer_endpoint.rs index f282f6f08f..55533690f1 100644 --- a/nexus/db-model/src/producer_endpoint.rs +++ b/nexus/db-model/src/producer_endpoint.rs @@ -12,7 +12,7 @@ use uuid::Uuid; impl_enum_type!( #[derive(SqlType, Copy, Clone, Debug, QueryId)] - #[diesel(postgres_type(name = "producer_kind"))] + #[diesel(postgres_type(name = "producer_kind", schema = "public"))] pub struct ProducerKindEnum; #[derive(AsExpression, Copy, Clone, Debug, FromSqlRow, PartialEq)] diff --git a/nexus/db-model/src/role_assignment.rs b/nexus/db-model/src/role_assignment.rs index 45b0c65e37..fbbe18579e 100644 --- a/nexus/db-model/src/role_assignment.rs +++ b/nexus/db-model/src/role_assignment.rs @@ -12,7 +12,7 @@ use uuid::Uuid; impl_enum_type!( #[derive(SqlType, Debug, QueryId)] - #[diesel(postgres_type(name = "identity_type"))] + #[diesel(postgres_type(name = "identity_type", schema = "public"))] pub struct IdentityTypeEnum; #[derive( diff --git a/nexus/db-model/src/saga_types.rs b/nexus/db-model/src/saga_types.rs index f2a8b57659..bb21e803bc 100644 --- a/nexus/db-model/src/saga_types.rs +++ b/nexus/db-model/src/saga_types.rs @@ -140,7 +140,7 @@ where } #[derive(Clone, Copy, Debug, PartialEq, SqlType)] -#[diesel(postgres_type(name = "saga_state"))] +#[diesel(postgres_type(name = "saga_state", schema = "public"))] pub struct SagaCachedStateEnum; /// Newtype wrapper around [`steno::SagaCachedState`] which implements diff --git a/nexus/db-model/src/service_kind.rs b/nexus/db-model/src/service_kind.rs index 4210c3ee20..016de9c44e 100644 --- a/nexus/db-model/src/service_kind.rs +++ b/nexus/db-model/src/service_kind.rs @@ -10,7 +10,7 @@ use strum::EnumIter; impl_enum_type!( #[derive(Clone, SqlType, Debug, QueryId)] - #[diesel(postgres_type(name = "service_kind"))] + #[diesel(postgres_type(name = "service_kind", schema = "public"))] pub struct ServiceKindEnum; #[derive(Clone, Copy, Debug, Eq, AsExpression, FromSqlRow, Serialize, Deserialize, PartialEq, EnumIter)] diff --git a/nexus/db-model/src/silo.rs b/nexus/db-model/src/silo.rs index 21d12cd7f1..66520fccb1 100644 --- a/nexus/db-model/src/silo.rs +++ b/nexus/db-model/src/silo.rs @@ -20,7 +20,7 @@ use uuid::Uuid; impl_enum_type!( #[derive(SqlType, Debug, QueryId)] - #[diesel(postgres_type(name = "authentication_mode"))] + #[diesel(postgres_type(name = "authentication_mode", schema = "public"))] pub struct AuthenticationModeEnum; #[derive(Copy, Clone, Debug, AsExpression, FromSqlRow, PartialEq, Eq)] @@ -52,7 +52,7 @@ impl From for shared::AuthenticationMode { impl_enum_type!( #[derive(SqlType, Debug, QueryId)] - #[diesel(postgres_type(name = "user_provision_type"))] + #[diesel(postgres_type(name = "user_provision_type", schema = "public"))] pub struct UserProvisionTypeEnum; #[derive(Copy, Clone, Debug, AsExpression, FromSqlRow, PartialEq, Eq)] diff --git a/nexus/db-model/src/sled_provision_state.rs b/nexus/db-model/src/sled_provision_state.rs index b2b1ee39dc..ada842a32f 100644 --- a/nexus/db-model/src/sled_provision_state.rs +++ b/nexus/db-model/src/sled_provision_state.rs @@ -9,7 +9,7 @@ use thiserror::Error; impl_enum_type!( #[derive(Clone, SqlType, Debug, QueryId)] - #[diesel(postgres_type(name = "sled_provision_state"))] + #[diesel(postgres_type(name = "sled_provision_state", schema = "public"))] pub struct SledProvisionStateEnum; #[derive(Clone, Copy, Debug, AsExpression, FromSqlRow, Serialize, Deserialize, PartialEq)] diff --git a/nexus/db-model/src/sled_resource_kind.rs b/nexus/db-model/src/sled_resource_kind.rs index 1c92431cfa..c17eb2e106 100644 --- a/nexus/db-model/src/sled_resource_kind.rs +++ b/nexus/db-model/src/sled_resource_kind.rs @@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize}; impl_enum_type!( #[derive(Clone, SqlType, Debug, QueryId)] - #[diesel(postgres_type(name = "sled_resource_kind"))] + #[diesel(postgres_type(name = "sled_resource_kind", schema = "public"))] pub struct SledResourceKindEnum; #[derive(Clone, Copy, Debug, AsExpression, FromSqlRow, Serialize, Deserialize, PartialEq)] diff --git a/nexus/db-model/src/snapshot.rs b/nexus/db-model/src/snapshot.rs index 2a93f03f69..6c160e5c6b 100644 --- a/nexus/db-model/src/snapshot.rs +++ b/nexus/db-model/src/snapshot.rs @@ -14,7 +14,7 @@ use uuid::Uuid; impl_enum_type!( #[derive(SqlType, Debug, QueryId)] - #[diesel(postgres_type(name = "snapshot_state"))] + #[diesel(postgres_type(name = "snapshot_state", schema = "public"))] pub struct SnapshotStateEnum; #[derive(Clone, Debug, AsExpression, FromSqlRow, Serialize, Deserialize, PartialEq)] diff --git a/nexus/db-model/src/switch_interface.rs b/nexus/db-model/src/switch_interface.rs index f0c4b91de6..71673354ea 100644 --- a/nexus/db-model/src/switch_interface.rs +++ b/nexus/db-model/src/switch_interface.rs @@ -14,7 +14,7 @@ use uuid::Uuid; impl_enum_type!( #[derive(SqlType, Debug, Clone, Copy)] - #[diesel(postgres_type(name = "switch_interface_kind"))] + #[diesel(postgres_type(name = "switch_interface_kind", schema = "public"))] pub struct DbSwitchInterfaceKindEnum; #[derive( diff --git a/nexus/db-model/src/switch_port.rs b/nexus/db-model/src/switch_port.rs index 6ff8612d2f..6ed918dae5 100644 --- a/nexus/db-model/src/switch_port.rs +++ b/nexus/db-model/src/switch_port.rs @@ -23,7 +23,7 @@ use uuid::Uuid; impl_enum_type!( #[derive(SqlType, Debug, Clone, Copy)] - #[diesel(postgres_type(name = "switch_port_geometry"))] + #[diesel(postgres_type(name = "switch_port_geometry", schema = "public"))] pub struct SwitchPortGeometryEnum; #[derive( @@ -46,7 +46,7 @@ impl_enum_type!( impl_enum_type!( #[derive(SqlType, Debug, Clone, Copy)] - #[diesel(postgres_type(name = "switch_link_fec"))] + #[diesel(postgres_type(name = "switch_link_fec", schema = "public"))] pub struct SwitchLinkFecEnum; #[derive( @@ -69,7 +69,7 @@ impl_enum_type!( impl_enum_type!( #[derive(SqlType, Debug, Clone, Copy)] - #[diesel(postgres_type(name = "switch_link_speed"))] + #[diesel(postgres_type(name = "switch_link_speed", schema = "public"))] pub struct SwitchLinkSpeedEnum; #[derive( diff --git a/nexus/db-model/src/system_update.rs b/nexus/db-model/src/system_update.rs index c8ae66648e..17421936b1 100644 --- a/nexus/db-model/src/system_update.rs +++ b/nexus/db-model/src/system_update.rs @@ -59,7 +59,7 @@ impl From for views::SystemUpdate { impl_enum_type!( #[derive(SqlType, Debug, QueryId)] - #[diesel(postgres_type(name = "update_status"))] + #[diesel(postgres_type(name = "update_status", schema = "public"))] pub struct UpdateStatusEnum; #[derive(Copy, Clone, Debug, AsExpression, FromSqlRow, Serialize, Deserialize, PartialEq)] @@ -81,7 +81,7 @@ impl From for views::UpdateStatus { impl_enum_type!( #[derive(SqlType, Debug, QueryId)] - #[diesel(postgres_type(name = "updateable_component_type"))] + #[diesel(postgres_type(name = "updateable_component_type", schema = "public"))] pub struct UpdateableComponentTypeEnum; #[derive(Copy, Clone, Debug, AsExpression, FromSqlRow, Serialize, Deserialize, PartialEq)] diff --git a/nexus/db-model/src/update_artifact.rs b/nexus/db-model/src/update_artifact.rs index 196dd6db4d..97c57b44cc 100644 --- a/nexus/db-model/src/update_artifact.rs +++ b/nexus/db-model/src/update_artifact.rs @@ -14,7 +14,7 @@ use std::io::Write; impl_enum_wrapper!( #[derive(SqlType, Debug, QueryId)] - #[diesel(postgres_type(name = "update_artifact_kind"))] + #[diesel(postgres_type(name = "update_artifact_kind", schema = "public"))] pub struct KnownArtifactKindEnum; #[derive(Clone, Copy, Debug, Display, AsExpression, FromSqlRow, PartialEq, Eq, Serialize, Deserialize)] diff --git a/nexus/db-model/src/vpc_firewall_rule.rs b/nexus/db-model/src/vpc_firewall_rule.rs index 6208d589ff..2d19796524 100644 --- a/nexus/db-model/src/vpc_firewall_rule.rs +++ b/nexus/db-model/src/vpc_firewall_rule.rs @@ -19,7 +19,7 @@ use uuid::Uuid; impl_enum_wrapper!( #[derive(SqlType, Debug)] - #[diesel(postgres_type(name = "vpc_firewall_rule_status"))] + #[diesel(postgres_type(name = "vpc_firewall_rule_status", schema = "public"))] pub struct VpcFirewallRuleStatusEnum; #[derive(Clone, Debug, AsExpression, FromSqlRow, Serialize, Deserialize)] @@ -34,7 +34,7 @@ NewtypeDeref! { () pub struct VpcFirewallRuleStatus(external::VpcFirewallRuleSta impl_enum_wrapper!( #[derive(SqlType, Debug)] - #[diesel(postgres_type(name = "vpc_firewall_rule_direction"))] + #[diesel(postgres_type(name = "vpc_firewall_rule_direction", schema = "public"))] pub struct VpcFirewallRuleDirectionEnum; #[derive(Clone, Debug, AsExpression, FromSqlRow, Serialize, Deserialize)] @@ -49,7 +49,7 @@ NewtypeDeref! { () pub struct VpcFirewallRuleDirection(external::VpcFirewallRule impl_enum_wrapper!( #[derive(SqlType, Debug)] - #[diesel(postgres_type(name = "vpc_firewall_rule_action"))] + #[diesel(postgres_type(name = "vpc_firewall_rule_action", schema = "public"))] pub struct VpcFirewallRuleActionEnum; #[derive(Clone, Debug, AsExpression, FromSqlRow, Serialize, Deserialize)] @@ -64,7 +64,7 @@ NewtypeDeref! { () pub struct VpcFirewallRuleAction(external::VpcFirewallRuleAct impl_enum_wrapper!( #[derive(SqlType, Debug)] - #[diesel(postgres_type(name = "vpc_firewall_rule_protocol"))] + #[diesel(postgres_type(name = "vpc_firewall_rule_protocol", schema = "public"))] pub struct VpcFirewallRuleProtocolEnum; #[derive(Clone, Debug, AsExpression, FromSqlRow, Serialize, Deserialize)] diff --git a/nexus/db-model/src/vpc_route.rs b/nexus/db-model/src/vpc_route.rs index 7f68f81254..168ed41cef 100644 --- a/nexus/db-model/src/vpc_route.rs +++ b/nexus/db-model/src/vpc_route.rs @@ -19,7 +19,7 @@ use uuid::Uuid; impl_enum_wrapper!( #[derive(SqlType, Debug)] - #[diesel(postgres_type(name = "router_route_kind"))] + #[diesel(postgres_type(name = "router_route_kind", schema = "public"))] pub struct RouterRouteKindEnum; #[derive(Clone, Debug, AsExpression, FromSqlRow)] diff --git a/nexus/db-model/src/vpc_router.rs b/nexus/db-model/src/vpc_router.rs index 676bc17ec4..71c753e6aa 100644 --- a/nexus/db-model/src/vpc_router.rs +++ b/nexus/db-model/src/vpc_router.rs @@ -14,7 +14,7 @@ use uuid::Uuid; impl_enum_type!( #[derive(SqlType, Debug)] - #[diesel(postgres_type(name = "vpc_router_kind"))] + #[diesel(postgres_type(name = "vpc_router_kind", schema = "public"))] pub struct VpcRouterKindEnum; #[derive(Clone, Copy, Debug, AsExpression, FromSqlRow, PartialEq)] diff --git a/nexus/db-queries/Cargo.toml b/nexus/db-queries/Cargo.toml index d5320be733..cae42a0944 100644 --- a/nexus/db-queries/Cargo.toml +++ b/nexus/db-queries/Cargo.toml @@ -72,6 +72,7 @@ omicron-test-utils.workspace = true openapiv3.workspace = true pem.workspace = true petgraph.workspace = true +pretty_assertions.workspace = true rcgen.workspace = true regex.workspace = true rustls.workspace = true diff --git a/nexus/db-queries/src/db/mod.rs b/nexus/db-queries/src/db/mod.rs index 924eab363f..e21ba2e3a8 100644 --- a/nexus/db-queries/src/db/mod.rs +++ b/nexus/db-queries/src/db/mod.rs @@ -24,6 +24,7 @@ pub mod lookup; // Public for doctests. pub mod pagination; mod pool; +mod pool_connection; // This is marked public because the error types are used elsewhere, e.g., in // sagas. pub mod queries; diff --git a/nexus/db-queries/src/db/pool.rs b/nexus/db-queries/src/db/pool.rs index 249852d832..497c8d97c5 100644 --- a/nexus/db-queries/src/db/pool.rs +++ b/nexus/db-queries/src/db/pool.rs @@ -25,16 +25,10 @@ // TODO-design Need TLS support (the types below hardcode NoTls). use super::Config as DbConfig; -use async_bb8_diesel::AsyncSimpleConnection; -use async_bb8_diesel::Connection; use async_bb8_diesel::ConnectionError; use async_bb8_diesel::ConnectionManager; -use async_trait::async_trait; -use bb8::CustomizeConnection; -use diesel::PgConnection; -use diesel_dtrace::DTraceConnection; -pub type DbConnection = DTraceConnection; +pub use super::pool_connection::DbConnection; /// Wrapper around a database connection pool. /// @@ -76,7 +70,9 @@ impl Pool { let error_sink = LoggingErrorSink::new(log); let manager = ConnectionManager::::new(url); let pool = builder - .connection_customizer(Box::new(DisallowFullTableScans {})) + .connection_customizer(Box::new( + super::pool_connection::ConnectionCustomizer::new(), + )) .error_sink(Box::new(error_sink)) .build_unchecked(manager); Pool { pool } @@ -88,25 +84,6 @@ impl Pool { } } -const DISALLOW_FULL_TABLE_SCAN_SQL: &str = - "set disallow_full_table_scans = on; set large_full_scan_rows = 0;"; - -#[derive(Debug)] -struct DisallowFullTableScans {} -#[async_trait] -impl CustomizeConnection, ConnectionError> - for DisallowFullTableScans -{ - async fn on_acquire( - &self, - conn: &mut Connection, - ) -> Result<(), ConnectionError> { - conn.batch_execute_async(DISALLOW_FULL_TABLE_SCAN_SQL) - .await - .map_err(|e| e.into()) - } -} - #[derive(Clone, Debug)] struct LoggingErrorSink { log: slog::Logger, diff --git a/nexus/db-queries/src/db/pool_connection.rs b/nexus/db-queries/src/db/pool_connection.rs new file mode 100644 index 0000000000..e96a15894d --- /dev/null +++ b/nexus/db-queries/src/db/pool_connection.rs @@ -0,0 +1,303 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! Customization that happens on each connection as they're acquired. + +use async_bb8_diesel::AsyncConnection; +use async_bb8_diesel::AsyncRunQueryDsl; +use async_bb8_diesel::AsyncSimpleConnection; +use async_bb8_diesel::Connection; +use async_bb8_diesel::ConnectionError; +use async_trait::async_trait; +use bb8::CustomizeConnection; +use diesel::pg::GetPgMetadataCache; +use diesel::pg::PgMetadataCacheKey; +use diesel::prelude::*; +use diesel::PgConnection; +use diesel_dtrace::DTraceConnection; +use std::collections::HashMap; +use tokio::sync::Mutex; + +pub type DbConnection = DTraceConnection; + +// This is a list of all user-defined types (ENUMS) in the current DB schema. +// +// Diesel looks up user-defined types as they are encountered, and loads +// them into a metadata cache. Although this cost is amortized over the lifetime +// of a connection, this can be slower than desired: +// - Diesel issues a round-trip database call on each user-defined type +// - The cache of OIDs for user-defined types is "per-connection", so when +// using a connection pool, we redo all these calls for new connections. +// +// To mitigate: We look up a list of user-defined types here on first access +// to the connection, and pre-populate the cache. Furthermore, we save this +// information and use it to populate other connections too, without incurring +// another database lookup. +// +// See https://github.com/oxidecomputer/omicron/issues/4733 for more context. +static CUSTOM_TYPE_KEYS: &'static [&'static str] = &[ + "address_lot_kind", + "authentication_mode", + "block_size", + "caboose_which", + "dataset_kind", + "dns_group", + "hw_power_state", + "hw_rot_slot", + "identity_type", + "instance_state", + "ip_kind", + "network_interface_kind", + "physical_disk_kind", + "producer_kind", + "provider_type", + "root_of_trust_page_which", + "router_route_kind", + "saga_state", + "service_kind", + "sled_provision_state", + "sled_resource_kind", + "snapshot_state", + "sp_type", + "switch_interface_kind", + "switch_link_fec", + "switch_link_speed", + "switch_port_geometry", + "update_artifact_kind", + "update_status", + "updateable_component_type", + "user_provision_type", + "vpc_firewall_rule_action", + "vpc_firewall_rule_direction", + "vpc_firewall_rule_protocol", + "vpc_firewall_rule_status", + "vpc_router_kind", +]; +const CUSTOM_TYPE_SCHEMA: &'static str = "public"; + +const DISALLOW_FULL_TABLE_SCAN_SQL: &str = + "set disallow_full_table_scans = on; set large_full_scan_rows = 0;"; + +#[derive(Debug)] +struct OIDCache(HashMap, (u32, u32)>); + +impl OIDCache { + // Populate a new OID cache by pre-filling values + async fn new( + conn: &mut Connection, + ) -> Result { + // Lookup all the OIDs for custom types. + // + // As a reminder, this is an optimization: + // - If we supply a value in CUSTOM_TYPE_KEYS that does not + // exist in the schema, the corresponding row won't be + // found, so the value will be ignored. + // - If we don't supply a value in CUSTOM_TYPE_KEYS, even + // though it DOES exist in the schema, it'll likewise not + // get pre-populated into the cache. Diesel would observe + // the cache miss, and perform the lookup later. + let results: Vec = pg_type::table + .select((pg_type::typname, pg_type::oid, pg_type::typarray)) + .inner_join( + pg_namespace::table + .on(pg_type::typnamespace.eq(pg_namespace::oid)), + ) + .filter(pg_type::typname.eq_any(CUSTOM_TYPE_KEYS)) + .filter(pg_namespace::nspname.eq(CUSTOM_TYPE_SCHEMA)) + .load_async(&*conn) + .await?; + + // Convert the OIDs into a ("Cache Key", "OID Tuple") pair, + // and store the result in a HashMap. + // + // We'll iterate over this HashMap to pre-populate the connection-local cache for all + // future connections, including this one. + Ok::<_, ConnectionError>(Self(HashMap::from_iter( + results.into_iter().map( + |PgTypeMetadata { typname, oid, array_oid }| { + ( + PgMetadataCacheKey::new( + Some(CUSTOM_TYPE_SCHEMA.into()), + std::borrow::Cow::Owned(typname), + ), + (oid, array_oid), + ) + }, + ), + ))) + } +} + +// String-based representation of the CockroachDB version. +// +// We currently do minimal parsing of this value, but it should +// be distinct between different revisions of CockroachDB. +// This version includes the semver version of the DB, but also +// build and target information. +#[derive(Debug, Eq, PartialEq, Hash)] +struct CockroachVersion(String); + +impl CockroachVersion { + async fn new( + conn: &Connection, + ) -> Result { + diesel::sql_function!(fn version() -> Text); + + let version = + diesel::select(version()).get_result_async::(conn).await?; + Ok(Self(version)) + } +} + +/// A customizer for all new connections made to CockroachDB, from Diesel. +#[derive(Debug)] +pub(crate) struct ConnectionCustomizer { + oid_caches: Mutex>, +} + +impl ConnectionCustomizer { + pub(crate) fn new() -> Self { + Self { oid_caches: Mutex::new(HashMap::new()) } + } + + async fn populate_metadata_cache( + &self, + conn: &mut Connection, + ) -> Result<(), ConnectionError> { + // Look up the CockroachDB version for new connections, to ensure + // that OID caches are distinct between different CRDB versions. + // + // This step is performed out of an abundance of caution: OIDs are not + // necessarily stable across major releases of CRDB, and this ensures + // that the OID lookups on custom types do not cross this version + // boundary. + let version = CockroachVersion::new(conn).await?; + + // Lookup the OID cache, or populate it if we haven't previously + // established a connection to this database version. + let mut oid_caches = self.oid_caches.lock().await; + let entry = oid_caches.entry(version); + use std::collections::hash_map::Entry::*; + let oid_cache = match entry { + Occupied(ref entry) => entry.get(), + Vacant(entry) => entry.insert(OIDCache::new(conn).await?), + }; + + // Copy the OID cache into this specific connection. + // + // NOTE: I don't love that this is blocking (due to "as_sync_conn"), but the + // "get_metadata_cache" method does not seem implemented for types that could have a + // non-Postgres backend. + let mut sync_conn = conn.as_sync_conn(); + let cache = sync_conn.get_metadata_cache(); + for (k, v) in &oid_cache.0 { + cache.store_type(k.clone(), *v); + } + Ok(()) + } + + async fn disallow_full_table_scans( + &self, + conn: &mut Connection, + ) -> Result<(), ConnectionError> { + conn.batch_execute_async(DISALLOW_FULL_TABLE_SCAN_SQL).await?; + Ok(()) + } +} + +#[async_trait] +impl CustomizeConnection, ConnectionError> + for ConnectionCustomizer +{ + async fn on_acquire( + &self, + conn: &mut Connection, + ) -> Result<(), ConnectionError> { + self.populate_metadata_cache(conn).await?; + self.disallow_full_table_scans(conn).await?; + Ok(()) + } +} + +#[derive(Debug, Clone, Hash, PartialEq, Eq, Queryable)] +pub struct PgTypeMetadata { + typname: String, + oid: u32, + array_oid: u32, +} + +table! { + pg_type (oid) { + oid -> Oid, + typname -> Text, + typarray -> Oid, + typnamespace -> Oid, + } +} + +table! { + pg_namespace (oid) { + oid -> Oid, + nspname -> Text, + } +} + +allow_tables_to_appear_in_same_query!(pg_type, pg_namespace); + +#[cfg(test)] +mod test { + use super::*; + use nexus_test_utils::db::test_setup_database; + use omicron_test_utils::dev; + + // Ensure that the "CUSTOM_TYPE_KEYS" values match the enums + // we find within the database. + // + // If the two are out-of-sync, identify the values causing problems. + #[tokio::test] + async fn all_enums_in_prepopulate_list() { + let logctx = dev::test_setup_log("test_project_creation"); + let mut crdb = test_setup_database(&logctx.log).await; + let client = crdb.connect().await.expect("Failed to connect to CRDB"); + + // https://www.cockroachlabs.com/docs/stable/show-enums + let rows = client + .query("SHOW ENUMS FROM omicron.public;", &[]) + .await + .unwrap_or_else(|_| panic!("failed to list enums")); + client.cleanup().await.expect("cleaning up after listing enums"); + + let mut observed_public_enums = rows + .into_iter() + .map(|row| -> String { + for i in 0..row.len() { + if row.columns()[i].name() == "name" { + return row.get(i); + } + } + panic!("Missing 'name' in row: {row:?}"); + }) + .collect::>(); + observed_public_enums.sort(); + + let mut expected_enums: Vec = + CUSTOM_TYPE_KEYS.into_iter().map(|s| s.to_string()).collect(); + expected_enums.sort(); + + pretty_assertions::assert_eq!( + observed_public_enums, + expected_enums, + "Enums did not match.\n\ + If the type is present on the left, but not the right:\n\ + \tThe enum is in the DB, but not in CUSTOM_TYPE_KEYS.\n\ + \tConsider adding it, so we can pre-populate the OID cache.\n\ + If the type is present on the right, but not the left:\n\ + \tThe enum is not the DB, but it is in CUSTOM_TYPE_KEYS.\n\ + \tConsider removing it, because the type no longer exists" + ); + + crdb.cleanup().await.unwrap(); + logctx.cleanup_successful(); + } +}