Skip to content

Commit a33943f

Browse files
authored
[nexus] Support Bundle HTTP endpoint implementations (#7187)
PR 5 / ??? Implements support bundle APIs for accessing storage bundles. Range request support is only partially implemented as-is -- follow-up support is described in #7356 Builds atop the API skeleton in: - #7008 Uses the support bundle datastore interfaces in: - #7021 Relies on the background task in: - #7063
1 parent be86cec commit a33943f

File tree

33 files changed

+1193
-146
lines changed

33 files changed

+1193
-146
lines changed

Cargo.lock

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

common/src/api/external/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -996,6 +996,7 @@ pub enum ResourceType {
996996
Instance,
997997
LoopbackAddress,
998998
SwitchPortSettings,
999+
SupportBundle,
9991000
IpPool,
10001001
IpPoolResource,
10011002
InstanceNetworkInterface,

dev-tools/omdb/tests/successes.out

+4-4
Original file line numberDiff line numberDiff line change
@@ -698,11 +698,11 @@ task: "service_zone_nat_tracker"
698698
last completion reported error: inventory collection is None
699699

700700
task: "support_bundle_collector"
701-
configured period: every <REDACTED_DURATION>s
701+
configured period: every <REDACTED_DURATION>days <REDACTED_DURATION>h <REDACTED_DURATION>m <REDACTED_DURATION>s
702702
currently executing: no
703703
last completed activation: <REDACTED ITERATIONS>, triggered by a periodic timer firing
704704
started at <REDACTED_TIMESTAMP> (<REDACTED DURATION>s ago) and ran for <REDACTED DURATION>ms
705-
last completion reported error: task disabled
705+
warning: unknown background task: "support_bundle_collector" (don't know how to interpret details: Object {"cleanup_err": Null, "cleanup_report": Object {"db_destroying_bundles_removed": Number(0), "db_failing_bundles_updated": Number(0), "sled_bundles_delete_failed": Number(0), "sled_bundles_deleted_not_found": Number(0), "sled_bundles_deleted_ok": Number(0)}, "collection_err": Null, "collection_report": Null})
706706

707707
task: "switch_port_config_manager"
708708
configured period: every <REDACTED_DURATION>s
@@ -1150,11 +1150,11 @@ task: "service_zone_nat_tracker"
11501150
last completion reported error: inventory collection is None
11511151

11521152
task: "support_bundle_collector"
1153-
configured period: every <REDACTED_DURATION>s
1153+
configured period: every <REDACTED_DURATION>days <REDACTED_DURATION>h <REDACTED_DURATION>m <REDACTED_DURATION>s
11541154
currently executing: no
11551155
last completed activation: <REDACTED ITERATIONS>, triggered by a periodic timer firing
11561156
started at <REDACTED_TIMESTAMP> (<REDACTED DURATION>s ago) and ran for <REDACTED DURATION>ms
1157-
last completion reported error: task disabled
1157+
warning: unknown background task: "support_bundle_collector" (don't know how to interpret details: Object {"cleanup_err": Null, "cleanup_report": Object {"db_destroying_bundles_removed": Number(0), "db_failing_bundles_updated": Number(0), "sled_bundles_delete_failed": Number(0), "sled_bundles_deleted_not_found": Number(0), "sled_bundles_deleted_ok": Number(0)}, "collection_err": Null, "collection_report": Null})
11581158

11591159
task: "switch_port_config_manager"
11601160
configured period: every <REDACTED_DURATION>s

docs/adding-an-endpoint.adoc

+1-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ this document should act as a jumping-off point.
5353
=== **Testing**
5454

5555
* Authorization
56-
** There exists a https://github.com/oxidecomputer/omicron/blob/main/nexus/src/authz/policy_test[policy test] which compares all Oso objects against an expected policy. New resources are usually added to https://github.com/oxidecomputer/omicron/blob/main/nexus/src/authz/policy_test/resources.rs[resources.rs] to get coverage.
56+
** There exists a https://github.com/oxidecomputer/omicron/blob/main/nexus/db-queries/src/policy_test[policy test] which compares all Oso objects against an expected policy. New resources are usually added to https://github.com/oxidecomputer/omicron/blob/main/nexus/db-queries/src/policy_test/resources.rs[resources.rs] to get coverage.
5757
* OpenAPI
5858
** Once you've added or changed endpoint definitions in `nexus-external-api` or `nexus-internal-api`, you'll need to update the corresponding OpenAPI documents (the JSON files in `openapi/`).
5959
** To update all OpenAPI documents, run `cargo xtask openapi generate`.

nexus/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ progenitor-client.workspace = true
7272
propolis-client.workspace = true
7373
qorb.workspace = true
7474
rand.workspace = true
75+
range-requests.workspace = true
7576
ref-cast.workspace = true
7677
reqwest = { workspace = true, features = ["json"] }
7778
ring.workspace = true

nexus/auth/src/authz/api_resources.rs

+8
Original file line numberDiff line numberDiff line change
@@ -994,6 +994,14 @@ authz_resource! {
994994
polar_snippet = FleetChild,
995995
}
996996

997+
authz_resource! {
998+
name = "SupportBundle",
999+
parent = "Fleet",
1000+
primary_key = { uuid_kind = SupportBundleKind },
1001+
roles_allowed = false,
1002+
polar_snippet = FleetChild,
1003+
}
1004+
9971005
authz_resource! {
9981006
name = "PhysicalDisk",
9991007
parent = "Fleet",

nexus/auth/src/authz/oso_generic.rs

+1
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ pub fn make_omicron_oso(log: &slog::Logger) -> Result<OsoInit, anyhow::Error> {
154154
Silo::init(),
155155
SiloUser::init(),
156156
SiloGroup::init(),
157+
SupportBundle::init(),
157158
IdentityProvider::init(),
158159
SamlIdentityProvider::init(),
159160
Sled::init(),

nexus/db-model/src/support_bundle.rs

+4
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,10 @@ impl SupportBundle {
118118
assigned_nexus: Some(nexus_id.into()),
119119
}
120120
}
121+
122+
pub fn id(&self) -> SupportBundleUuid {
123+
self.id.into()
124+
}
121125
}
122126

123127
impl From<SupportBundle> for SupportBundleView {

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

+41-26
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use crate::context::OpContext;
1010
use crate::db;
1111
use crate::db::error::public_error_from_diesel;
1212
use crate::db::error::ErrorHandler;
13+
use crate::db::lookup::LookupPath;
1314
use crate::db::model::Dataset;
1415
use crate::db::model::DatasetKind;
1516
use crate::db::model::SupportBundle;
@@ -163,16 +164,10 @@ impl DataStore {
163164
opctx: &OpContext,
164165
id: SupportBundleUuid,
165166
) -> LookupResult<SupportBundle> {
166-
opctx.authorize(authz::Action::Read, &authz::FLEET).await?;
167-
use db::schema::support_bundle::dsl;
167+
let (.., db_bundle) =
168+
LookupPath::new(opctx, self).support_bundle(id).fetch().await?;
168169

169-
let conn = self.pool_connection_authorized(opctx).await?;
170-
dsl::support_bundle
171-
.filter(dsl::id.eq(id.into_untyped_uuid()))
172-
.select(SupportBundle::as_select())
173-
.first_async::<SupportBundle>(&*conn)
174-
.await
175-
.map_err(|e| public_error_from_diesel(e, ErrorHandler::Server))
170+
Ok(db_bundle)
176171
}
177172

178173
/// Lists one page of support bundles
@@ -419,18 +414,20 @@ impl DataStore {
419414
pub async fn support_bundle_update(
420415
&self,
421416
opctx: &OpContext,
422-
id: SupportBundleUuid,
417+
authz_bundle: &authz::SupportBundle,
423418
state: SupportBundleState,
424419
) -> Result<(), Error> {
425-
opctx.authorize(authz::Action::Modify, &authz::FLEET).await?;
420+
opctx.authorize(authz::Action::Modify, authz_bundle).await?;
426421

427422
use db::schema::support_bundle::dsl;
423+
424+
let id = authz_bundle.id().into_untyped_uuid();
428425
let conn = self.pool_connection_authorized(opctx).await?;
429426
let result = diesel::update(dsl::support_bundle)
430-
.filter(dsl::id.eq(id.into_untyped_uuid()))
427+
.filter(dsl::id.eq(id))
431428
.filter(dsl::state.eq_any(state.valid_old_states()))
432429
.set(dsl::state.eq(state))
433-
.check_if_exists::<SupportBundle>(id.into_untyped_uuid())
430+
.check_if_exists::<SupportBundle>(id)
434431
.execute_and_check(&conn)
435432
.await
436433
.map_err(|e| public_error_from_diesel(e, ErrorHandler::Server))?;
@@ -453,20 +450,21 @@ impl DataStore {
453450
pub async fn support_bundle_delete(
454451
&self,
455452
opctx: &OpContext,
456-
id: SupportBundleUuid,
453+
authz_bundle: &authz::SupportBundle,
457454
) -> Result<(), Error> {
458-
opctx.authorize(authz::Action::Modify, &authz::FLEET).await?;
455+
opctx.authorize(authz::Action::Delete, authz_bundle).await?;
459456

460457
use db::schema::support_bundle::dsl;
461458

459+
let id = authz_bundle.id().into_untyped_uuid();
462460
let conn = self.pool_connection_authorized(opctx).await?;
463461
diesel::delete(dsl::support_bundle)
464462
.filter(
465463
dsl::state
466464
.eq(SupportBundleState::Destroying)
467465
.or(dsl::state.eq(SupportBundleState::Failed)),
468466
)
469-
.filter(dsl::id.eq(id.into_untyped_uuid()))
467+
.filter(dsl::id.eq(id))
470468
.execute_async(&*conn)
471469
.await
472470
.map(|_rows_modified| ())
@@ -494,6 +492,7 @@ mod test {
494492
use nexus_types::deployment::BlueprintZoneDisposition;
495493
use nexus_types::deployment::BlueprintZoneFilter;
496494
use nexus_types::deployment::BlueprintZoneType;
495+
use omicron_common::api::external::LookupType;
497496
use omicron_common::api::internal::shared::DatasetKind::Debug as DebugDatasetKind;
498497
use omicron_test_utils::dev;
499498
use omicron_uuid_kinds::BlueprintUuid;
@@ -502,6 +501,16 @@ mod test {
502501
use omicron_uuid_kinds::SledUuid;
503502
use rand::Rng;
504503

504+
fn authz_support_bundle_from_id(
505+
id: SupportBundleUuid,
506+
) -> authz::SupportBundle {
507+
authz::SupportBundle::new(
508+
authz::FLEET,
509+
id,
510+
LookupType::ById(id.into_untyped_uuid()),
511+
)
512+
}
513+
505514
// Pool/Dataset pairs, for debug datasets only.
506515
struct TestPool {
507516
pool: ZpoolUuid,
@@ -715,10 +724,11 @@ mod test {
715724

716725
// When we update the state of the bundles, the list results
717726
// should also be filtered.
727+
let authz_bundle = authz_support_bundle_from_id(bundle_a1.id.into());
718728
datastore
719729
.support_bundle_update(
720730
&opctx,
721-
bundle_a1.id.into(),
731+
&authz_bundle,
722732
SupportBundleState::Active,
723733
)
724734
.await
@@ -816,11 +826,11 @@ mod test {
816826
// database.
817827
//
818828
// We should still expect to hit capacity limits.
819-
829+
let authz_bundle = authz_support_bundle_from_id(bundles[0].id.into());
820830
datastore
821831
.support_bundle_update(
822832
&opctx,
823-
bundles[0].id.into(),
833+
&authz_bundle,
824834
SupportBundleState::Destroying,
825835
)
826836
.await
@@ -835,8 +845,9 @@ mod test {
835845
// If we delete a bundle, it should be gone. This means we can
836846
// re-allocate from that dataset which was just freed up.
837847

848+
let authz_bundle = authz_support_bundle_from_id(bundles[0].id.into());
838849
datastore
839-
.support_bundle_delete(&opctx, bundles[0].id.into())
850+
.support_bundle_delete(&opctx, &authz_bundle)
840851
.await
841852
.expect("Should be able to destroy this bundle");
842853
datastore
@@ -888,11 +899,11 @@ mod test {
888899
assert_eq!(bundle, observed_bundles[0]);
889900

890901
// Destroy the bundle, observe the new state
891-
902+
let authz_bundle = authz_support_bundle_from_id(bundle.id.into());
892903
datastore
893904
.support_bundle_update(
894905
&opctx,
895-
bundle.id.into(),
906+
&authz_bundle,
896907
SupportBundleState::Destroying,
897908
)
898909
.await
@@ -905,8 +916,9 @@ mod test {
905916

906917
// Delete the bundle, observe that it's gone
907918

919+
let authz_bundle = authz_support_bundle_from_id(bundle.id.into());
908920
datastore
909-
.support_bundle_delete(&opctx, bundle.id.into())
921+
.support_bundle_delete(&opctx, &authz_bundle)
910922
.await
911923
.expect("Should be able to destroy our bundle");
912924
let observed_bundles = datastore
@@ -1146,10 +1158,11 @@ mod test {
11461158
);
11471159

11481160
// Start the deletion of this bundle
1161+
let authz_bundle = authz_support_bundle_from_id(bundle.id.into());
11491162
datastore
11501163
.support_bundle_update(
11511164
&opctx,
1152-
bundle.id.into(),
1165+
&authz_bundle,
11531166
SupportBundleState::Destroying,
11541167
)
11551168
.await
@@ -1314,8 +1327,9 @@ mod test {
13141327
.unwrap()
13151328
.contains(FAILURE_REASON_NO_DATASET));
13161329

1330+
let authz_bundle = authz_support_bundle_from_id(bundle.id.into());
13171331
datastore
1318-
.support_bundle_delete(&opctx, bundle.id.into())
1332+
.support_bundle_delete(&opctx, &authz_bundle)
13191333
.await
13201334
.expect("Should have been able to delete support bundle");
13211335

@@ -1377,10 +1391,11 @@ mod test {
13771391
//
13781392
// This is what we would do when we finish collecting, and
13791393
// provisioned storage on a sled.
1394+
let authz_bundle = authz_support_bundle_from_id(bundle.id.into());
13801395
datastore
13811396
.support_bundle_update(
13821397
&opctx,
1383-
bundle.id.into(),
1398+
&authz_bundle,
13841399
SupportBundleState::Active,
13851400
)
13861401
.await

nexus/db-queries/src/db/lookup.rs

+15
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ use omicron_common::api::external::Error;
2222
use omicron_common::api::external::InternalContext;
2323
use omicron_common::api::external::{LookupResult, LookupType, ResourceType};
2424
use omicron_uuid_kinds::PhysicalDiskUuid;
25+
use omicron_uuid_kinds::SupportBundleUuid;
2526
use omicron_uuid_kinds::TufArtifactKind;
2627
use omicron_uuid_kinds::TufRepoKind;
2728
use omicron_uuid_kinds::TypedUuid;
@@ -391,6 +392,11 @@ impl<'a> LookupPath<'a> {
391392
PhysicalDisk::PrimaryKey(Root { lookup_root: self }, id)
392393
}
393394

395+
/// Select a resource of type SupportBundle, identified by its id
396+
pub fn support_bundle(self, id: SupportBundleUuid) -> SupportBundle<'a> {
397+
SupportBundle::PrimaryKey(Root { lookup_root: self }, id)
398+
}
399+
394400
pub fn silo_image_id(self, id: Uuid) -> SiloImage<'a> {
395401
SiloImage::PrimaryKey(Root { lookup_root: self }, id)
396402
}
@@ -872,6 +878,15 @@ lookup_resource! {
872878
primary_key_columns = [ { column_name = "id", uuid_kind = PhysicalDiskKind } ]
873879
}
874880

881+
lookup_resource! {
882+
name = "SupportBundle",
883+
ancestors = [],
884+
children = [],
885+
lookup_by_name = false,
886+
soft_deletes = false,
887+
primary_key_columns = [ { column_name = "id", uuid_kind = SupportBundleKind } ]
888+
}
889+
875890
lookup_resource! {
876891
name = "TufRepo",
877892
ancestors = [],

nexus/db-queries/src/policy_test/resource_builder.rs

+1
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,7 @@ impl_dyn_authorized_resource_for_resource!(authz::SiloUser);
271271
impl_dyn_authorized_resource_for_resource!(authz::Sled);
272272
impl_dyn_authorized_resource_for_resource!(authz::Snapshot);
273273
impl_dyn_authorized_resource_for_resource!(authz::SshKey);
274+
impl_dyn_authorized_resource_for_resource!(authz::SupportBundle);
274275
impl_dyn_authorized_resource_for_resource!(authz::TufArtifact);
275276
impl_dyn_authorized_resource_for_resource!(authz::TufRepo);
276277
impl_dyn_authorized_resource_for_resource!(authz::Vpc);

nexus/db-queries/src/policy_test/resources.rs

+9
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use nexus_auth::authz;
1010
use omicron_common::api::external::LookupType;
1111
use omicron_uuid_kinds::GenericUuid;
1212
use omicron_uuid_kinds::PhysicalDiskUuid;
13+
use omicron_uuid_kinds::SupportBundleUuid;
1314
use oso::PolarClass;
1415
use std::collections::BTreeSet;
1516
use uuid::Uuid;
@@ -109,6 +110,14 @@ pub async fn make_resources(
109110
LookupType::ById(physical_disk_id.into_untyped_uuid()),
110111
));
111112

113+
let support_bundle_id: SupportBundleUuid =
114+
"d9f923f6-caf3-4c83-96f9-8ffe8c627dd2".parse().unwrap();
115+
builder.new_resource(authz::SupportBundle::new(
116+
authz::FLEET,
117+
support_bundle_id,
118+
LookupType::ById(support_bundle_id.into_untyped_uuid()),
119+
));
120+
112121
let device_user_code = String::from("a-device-user-code");
113122
builder.new_resource(authz::DeviceAuthRequest::new(
114123
authz::FLEET,

nexus/db-queries/tests/output/authz-roles.out

+14
Original file line numberDiff line numberDiff line change
@@ -1034,6 +1034,20 @@ resource: PhysicalDisk id "c9f923f6-caf3-4c83-96f9-8ffe8c627dd2"
10341034
silo1-proj1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘
10351035
unauthenticated ! ! ! ! ! ! ! !
10361036

1037+
resource: SupportBundle id "d9f923f6-caf3-4c83-96f9-8ffe8c627dd2"
1038+
1039+
USER Q R LC RP M MP CC D
1040+
fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔
1041+
fleet-collaborator ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘
1042+
fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘
1043+
silo1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘
1044+
silo1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘
1045+
silo1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘
1046+
silo1-proj1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘
1047+
silo1-proj1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘
1048+
silo1-proj1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘
1049+
unauthenticated ! ! ! ! ! ! ! !
1050+
10371051
resource: DeviceAuthRequest "a-device-user-code"
10381052

10391053
USER Q R LC RP M MP CC D

nexus/src/app/background/mod.rs

+7
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,13 @@ pub use init::BackgroundTasksData;
140140
pub use init::BackgroundTasksInitializer;
141141
pub use tasks::saga_recovery::SagaRecoveryHelpers;
142142

143+
// Expose background task outputs to they can be deserialized and
144+
// observed.
145+
pub mod task_output {
146+
pub use super::tasks::support_bundle_collector::CleanupReport;
147+
pub use super::tasks::support_bundle_collector::CollectionReport;
148+
}
149+
143150
use futures::future::BoxFuture;
144151
use nexus_auth::context::OpContext;
145152

0 commit comments

Comments
 (0)