Skip to content

Commit 5260a19

Browse files
authored
Exclude default silo from utilization response (#8461)
Closes #5731
1 parent 165b854 commit 5260a19

File tree

2 files changed

+165
-90
lines changed

2 files changed

+165
-90
lines changed

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use diesel::BoolExpressionMethods;
99
use diesel::{ExpressionMethods, QueryDsl, SelectableHelper};
1010
use nexus_db_errors::ErrorHandler;
1111
use nexus_db_errors::public_error_from_diesel;
12+
use nexus_types::silo::DEFAULT_SILO_ID;
1213
use omicron_common::api::external::Error;
1314
use omicron_common::api::external::ListResultVec;
1415
use omicron_common::api::external::http_pagination::PaginatedBy;
@@ -57,6 +58,12 @@ impl DataStore {
5758
.or(dsl::memory_allocated.gt(0))
5859
.or(dsl::storage_allocated.gt(0)),
5960
)
61+
// Filter out default silo from utilization response by its well-known
62+
// ID because it has gigantic quotas that confuse everyone. The proper
63+
// solution will be to eliminate the default silo altogether, but this
64+
// is dramatically easier.
65+
// See https://github.com/oxidecomputer/omicron/issues/5731
66+
.filter(dsl::silo_id.ne(DEFAULT_SILO_ID))
6067
.load_async(&*self.pool_connection_authorized(opctx).await?)
6168
.await
6269
.map_err(|e| public_error_from_diesel(e, ErrorHandler::Server))
Lines changed: 158 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use dropshot::test_util::ClientTestContext;
12
use http::Method;
23
use http::StatusCode;
34
use nexus_test_utils::http_testing::AuthnMode;
@@ -6,16 +7,26 @@ use nexus_test_utils::http_testing::RequestBuilder;
67
use nexus_test_utils::resource_helpers::DiskTest;
78
use nexus_test_utils::resource_helpers::create_default_ip_pool;
89
use nexus_test_utils::resource_helpers::create_instance;
10+
use nexus_test_utils::resource_helpers::create_local_user;
911
use nexus_test_utils::resource_helpers::create_project;
12+
use nexus_test_utils::resource_helpers::grant_iam;
13+
use nexus_test_utils::resource_helpers::link_ip_pool;
14+
use nexus_test_utils::resource_helpers::object_get;
15+
use nexus_test_utils::resource_helpers::object_put;
1016
use nexus_test_utils::resource_helpers::objects_list_page_authz;
17+
use nexus_test_utils::resource_helpers::test_params;
1118
use nexus_test_utils_macros::nexus_test;
1219
use nexus_types::external_api::params;
1320
use nexus_types::external_api::params::SiloQuotasCreate;
21+
use nexus_types::external_api::views::Silo;
22+
use nexus_types::external_api::views::SiloQuotas;
1423
use nexus_types::external_api::views::SiloUtilization;
1524
use nexus_types::external_api::views::Utilization;
1625
use nexus_types::external_api::views::VirtualResourceCounts;
1726
use omicron_common::api::external::ByteCount;
1827
use omicron_common::api::external::IdentityMetadataCreateParams;
28+
use omicron_common::api::external::InstanceCpuCount;
29+
use oxide_client::types::SiloRole;
1930

2031
static PROJECT_NAME: &str = "utilization-test-project";
2132
static INSTANCE_NAME: &str = "utilization-test-instance";
@@ -24,108 +35,97 @@ type ControlPlaneTestContext =
2435
nexus_test_utils::ControlPlaneTestContext<omicron_nexus::Server>;
2536

2637
#[nexus_test]
27-
async fn test_utilization(cptestctx: &ControlPlaneTestContext) {
38+
async fn test_utilization_list(cptestctx: &ControlPlaneTestContext) {
2839
let client = &cptestctx.external_client;
2940

3041
create_default_ip_pool(&client).await;
3142

32-
// set high quota for test silo
33-
let _ = NexusRequest::object_put(
34-
client,
35-
"/v1/system/silos/test-suite-silo/quotas",
36-
Some(&params::SiloQuotasCreate::arbitrarily_high_default()),
37-
)
38-
.authn_as(AuthnMode::PrivilegedUser)
39-
.execute()
40-
.await;
43+
// default-silo has quotas, but is explicitly filtered out by ID in the
44+
// DB query to avoid user confusion. test-suite-silo also exists, but is
45+
// filtered out because it has no quotas, so list is empty
46+
assert!(util_list(client).await.is_empty());
4147

42-
let current_util = objects_list_page_authz::<SiloUtilization>(
48+
// setting quotas will make test-suite-silo show up in the list
49+
let quotas_url = "/v1/system/silos/test-suite-silo/quotas";
50+
let _: SiloQuotas = object_put(
4351
client,
44-
"/v1/system/utilization/silos",
52+
quotas_url,
53+
&params::SiloQuotasCreate::arbitrarily_high_default(),
4554
)
46-
.await
47-
.items;
48-
49-
// `default-silo` should be the only silo that shows up because
50-
// it has a default quota set
51-
assert_eq!(current_util.len(), 2);
55+
.await;
5256

53-
assert_eq!(current_util[0].silo_name, "default-silo");
57+
// now test-suite-silo shows up in the list
58+
let current_util = util_list(client).await;
59+
assert_eq!(current_util.len(), 1);
60+
assert_eq!(current_util[0].silo_name, "test-suite-silo");
61+
// it's empty because it has no resources
5462
assert_eq!(current_util[0].provisioned, SiloQuotasCreate::empty().into());
5563
assert_eq!(
5664
current_util[0].allocated,
5765
SiloQuotasCreate::arbitrarily_high_default().into()
5866
);
5967

60-
assert_eq!(current_util[1].silo_name, "test-suite-silo");
61-
assert_eq!(current_util[1].provisioned, SiloQuotasCreate::empty().into());
68+
// create the resources that should change the utilization
69+
create_resources_in_test_suite_silo(client).await;
70+
71+
// list response shows provisioned resources
72+
let current_util = util_list(client).await;
73+
assert_eq!(current_util.len(), 1);
74+
assert_eq!(current_util[0].silo_name, "test-suite-silo");
75+
assert_eq!(
76+
current_util[0].provisioned,
77+
VirtualResourceCounts {
78+
cpus: 2,
79+
memory: ByteCount::from_gibibytes_u32(4),
80+
storage: ByteCount::from(0)
81+
}
82+
);
6283
assert_eq!(
63-
current_util[1].allocated,
84+
current_util[0].allocated,
6485
SiloQuotasCreate::arbitrarily_high_default().into()
6586
);
6687

67-
let _ = NexusRequest::object_put(
68-
client,
69-
"/v1/system/silos/test-suite-silo/quotas",
70-
Some(&params::SiloQuotasCreate::empty()),
71-
)
72-
.authn_as(AuthnMode::PrivilegedUser)
73-
.execute()
74-
.await;
88+
// now we take the quota back off of test-suite-silo and end up empty again
89+
let _: SiloQuotas =
90+
object_put(client, quotas_url, &params::SiloQuotasCreate::empty())
91+
.await;
7592

76-
let current_util = objects_list_page_authz::<SiloUtilization>(
77-
client,
78-
"/v1/system/utilization/silos",
79-
)
80-
.await
81-
.items;
93+
assert!(util_list(client).await.is_empty());
94+
}
8295

83-
// Now that default-silo is the only one with a quota, it should be the only result
84-
assert_eq!(current_util.len(), 1);
96+
// Even though default silo is filtered out of the list view, you can still
97+
// fetch utilization for it individiually, so we test that here
98+
#[nexus_test]
99+
async fn test_utilization_view(cptestctx: &ControlPlaneTestContext) {
100+
let client = &cptestctx.external_client;
85101

86-
assert_eq!(current_util[0].silo_name, "default-silo");
87-
assert_eq!(current_util[0].provisioned, SiloQuotasCreate::empty().into());
88-
assert_eq!(
89-
current_util[0].allocated,
90-
SiloQuotasCreate::arbitrarily_high_default().into()
91-
);
102+
create_default_ip_pool(&client).await;
92103

93104
let _ = create_project(&client, &PROJECT_NAME).await;
94105
let _ = create_instance(client, &PROJECT_NAME, &INSTANCE_NAME).await;
95106

107+
let instance_start_url = format!(
108+
"/v1/instances/{}/start?project={}",
109+
&INSTANCE_NAME, &PROJECT_NAME
110+
);
111+
96112
// Start instance
97113
NexusRequest::new(
98-
RequestBuilder::new(
99-
client,
100-
Method::POST,
101-
format!(
102-
"/v1/instances/{}/start?project={}",
103-
&INSTANCE_NAME, &PROJECT_NAME
104-
)
105-
.as_str(),
106-
)
107-
.body(None as Option<&serde_json::Value>)
108-
.expect_status(Some(StatusCode::ACCEPTED)),
114+
RequestBuilder::new(client, Method::POST, &instance_start_url)
115+
.body(None as Option<&serde_json::Value>)
116+
.expect_status(Some(StatusCode::ACCEPTED)),
109117
)
110118
.authn_as(AuthnMode::PrivilegedUser)
111119
.execute()
112120
.await
113121
.expect("failed to start instance");
114122

115123
// get utilization for just the default silo
116-
let silo_util = NexusRequest::object_get(
117-
client,
118-
"/v1/system/utilization/silos/default-silo",
119-
)
120-
.authn_as(AuthnMode::PrivilegedUser)
121-
.execute()
122-
.await
123-
.expect("failed to fetch silo utilization")
124-
.parsed_body::<SiloUtilization>()
125-
.unwrap();
124+
let default_silo_util: SiloUtilization =
125+
object_get(client, "/v1/system/utilization/silos/default-silo").await;
126126

127127
assert_eq!(
128-
silo_util.provisioned,
128+
default_silo_util.provisioned,
129129
VirtualResourceCounts {
130130
cpus: 4,
131131
memory: ByteCount::from_gibibytes_u32(1),
@@ -136,45 +136,113 @@ async fn test_utilization(cptestctx: &ControlPlaneTestContext) {
136136
// Simulate space for disks
137137
DiskTest::new(&cptestctx).await;
138138

139+
let disk_url = format!("/v1/disks?project={}", &PROJECT_NAME);
139140
// provision disk
140141
NexusRequest::new(
141-
RequestBuilder::new(
142-
client,
143-
Method::POST,
144-
format!("/v1/disks?project={}", &PROJECT_NAME).as_str(),
145-
)
146-
.body(Some(&params::DiskCreate {
147-
identity: IdentityMetadataCreateParams {
148-
name: "test-disk".parse().unwrap(),
149-
description: "".into(),
150-
},
151-
size: ByteCount::from_gibibytes_u32(2),
152-
disk_source: params::DiskSource::Blank {
153-
block_size: params::BlockSize::try_from(512).unwrap(),
154-
},
155-
}))
156-
.expect_status(Some(StatusCode::CREATED)),
142+
RequestBuilder::new(client, Method::POST, &disk_url)
143+
.body(Some(&params::DiskCreate {
144+
identity: IdentityMetadataCreateParams {
145+
name: "test-disk".parse().unwrap(),
146+
description: "".into(),
147+
},
148+
size: ByteCount::from_gibibytes_u32(2),
149+
disk_source: params::DiskSource::Blank {
150+
block_size: params::BlockSize::try_from(512).unwrap(),
151+
},
152+
}))
153+
.expect_status(Some(StatusCode::CREATED)),
157154
)
158155
.authn_as(AuthnMode::PrivilegedUser)
159156
.execute()
160157
.await
161158
.expect("disk failed to create");
162159

163160
// Get the silo but this time using the silo admin view
164-
let silo_util = NexusRequest::object_get(client, "/v1/utilization")
165-
.authn_as(AuthnMode::PrivilegedUser)
166-
.execute()
167-
.await
168-
.expect("failed to fetch utilization for current (default) silo")
169-
.parsed_body::<Utilization>()
170-
.unwrap();
161+
let default_silo_util: Utilization =
162+
object_get(client, "/v1/utilization").await;
171163

172164
assert_eq!(
173-
silo_util.provisioned,
165+
default_silo_util.provisioned,
174166
VirtualResourceCounts {
175167
cpus: 4,
176168
memory: ByteCount::from_gibibytes_u32(1),
177169
storage: ByteCount::from_gibibytes_u32(2)
178170
}
179171
);
180172
}
173+
174+
async fn util_list(client: &ClientTestContext) -> Vec<SiloUtilization> {
175+
objects_list_page_authz(client, "/v1/system/utilization/silos").await.items
176+
}
177+
178+
/// Could be inlined, but pulling it out makes the test much clearer
179+
async fn create_resources_in_test_suite_silo(client: &ClientTestContext) {
180+
// in order to create resources in test-suite-silo, we have to create a user
181+
// with the right perms so we have a user ID on hand to use in the authn_as
182+
let silo_url = "/v1/system/silos/test-suite-silo";
183+
let test_suite_silo: Silo = object_get(client, silo_url).await;
184+
link_ip_pool(client, "default", &test_suite_silo.identity.id, true).await;
185+
let user1 = create_local_user(
186+
client,
187+
&test_suite_silo,
188+
&"user1".parse().unwrap(),
189+
test_params::UserPassword::LoginDisallowed,
190+
)
191+
.await;
192+
grant_iam(
193+
client,
194+
silo_url,
195+
SiloRole::Collaborator,
196+
user1.id,
197+
AuthnMode::PrivilegedUser,
198+
)
199+
.await;
200+
201+
let test_project_name = "test-suite-project";
202+
203+
// Create project in test-suite-silo as the test user
204+
NexusRequest::objects_post(
205+
client,
206+
"/v1/projects",
207+
&params::ProjectCreate {
208+
identity: IdentityMetadataCreateParams {
209+
name: test_project_name.parse().unwrap(),
210+
description: String::new(),
211+
},
212+
},
213+
)
214+
.authn_as(AuthnMode::SiloUser(user1.id))
215+
.execute()
216+
.await
217+
.expect("failed to create project in test-suite-silo");
218+
219+
// Create instance in test-suite-silo as the test user
220+
let instance_params = params::InstanceCreate {
221+
identity: IdentityMetadataCreateParams {
222+
name: "test-inst".parse().unwrap(),
223+
description: "test instance in test-suite-silo".to_string(),
224+
},
225+
ncpus: InstanceCpuCount::try_from(2).unwrap(),
226+
memory: ByteCount::from_gibibytes_u32(4),
227+
hostname: "test-inst".parse().unwrap(),
228+
user_data: vec![],
229+
ssh_public_keys: None,
230+
network_interfaces: params::InstanceNetworkInterfaceAttachment::Default,
231+
external_ips: vec![],
232+
disks: vec![],
233+
boot_disk: None,
234+
start: true,
235+
auto_restart_policy: Default::default(),
236+
anti_affinity_groups: Vec::new(),
237+
};
238+
239+
NexusRequest::objects_post(
240+
client,
241+
&format!("/v1/instances?project={}", test_project_name),
242+
&instance_params,
243+
)
244+
.authn_as(AuthnMode::SiloUser(user1.id))
245+
.execute()
246+
.await
247+
.expect("failed to create instance in test-suite-silo");
248+
}

0 commit comments

Comments
 (0)