Skip to content

Commit 205382f

Browse files
authored
[nexus] Endpoint to list IP pools for silo, add is_default to silo-scoped IP pools list (#4843)
Fixes #4752 Fixes #4763 The main trick here is introducing `views::SiloIpPool`, which is the same as `views::IpPool` except it also has `is_default` on it. It only makes sense in the context of a particular silo because `is_default` is only defined for a (pool, silo) pair, not for a pool alone. - [x] Add `GET /v1/system/silos/{silo}/ip-pools` - [x] `/v1/ip-pools` and `/v1/ip-pools/{pool}` should return `SiloIpPool` too - [x] Tests for `/v1/system/silos/{silo}/ip-pools` - [x] We can't have both `SiloIpPool` and `IpPoolSilo`, cleaned up by changing: - `views::IpPoolSilo` -> `views::SiloIpSiloLink` - `params::IpPoolSiloLink` -> `views::IpPoolLinkSilo`
1 parent d8bbf6d commit 205382f

File tree

13 files changed

+431
-137
lines changed

13 files changed

+431
-137
lines changed

end-to-end-tests/src/bin/bootstrap.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use end_to_end_tests::helpers::{generate_name, get_system_ip_pool};
44
use omicron_test_utils::dev::poll::{wait_for_condition, CondCheckError};
55
use oxide_client::types::{
66
ByteCount, DeviceAccessTokenRequest, DeviceAuthRequest, DeviceAuthVerify,
7-
DiskCreate, DiskSource, IpPoolCreate, IpPoolSiloLink, IpRange, Ipv4Range,
7+
DiskCreate, DiskSource, IpPoolCreate, IpPoolLinkSilo, IpRange, Ipv4Range,
88
NameOrId, SiloQuotasUpdate,
99
};
1010
use oxide_client::{
@@ -51,7 +51,7 @@ async fn main() -> Result<()> {
5151
client
5252
.ip_pool_silo_link()
5353
.pool(pool_name)
54-
.body(IpPoolSiloLink {
54+
.body(IpPoolLinkSilo {
5555
silo: NameOrId::Name(params.silo_name().parse().unwrap()),
5656
is_default: true,
5757
})

nexus/db-model/src/ip_pool.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ pub struct IpPoolResource {
9797
pub is_default: bool,
9898
}
9999

100-
impl From<IpPoolResource> for views::IpPoolSilo {
100+
impl From<IpPoolResource> for views::IpPoolSiloLink {
101101
fn from(assoc: IpPoolResource) -> Self {
102102
Self {
103103
ip_pool_id: assoc.ip_pool_id,

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

Lines changed: 40 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -79,47 +79,6 @@ impl DataStore {
7979
.map_err(|e| public_error_from_diesel(e, ErrorHandler::Server))
8080
}
8181

82-
/// List IP pools linked to the current silo
83-
pub async fn silo_ip_pools_list(
84-
&self,
85-
opctx: &OpContext,
86-
pagparams: &PaginatedBy<'_>,
87-
) -> ListResultVec<db::model::IpPool> {
88-
use db::schema::ip_pool;
89-
use db::schema::ip_pool_resource;
90-
91-
// From the developer user's point of view, we treat IP pools linked to
92-
// their silo as silo resources, so they can list them if they can list
93-
// silo children
94-
let authz_silo =
95-
opctx.authn.silo_required().internal_context("listing IP pools")?;
96-
opctx.authorize(authz::Action::ListChildren, &authz_silo).await?;
97-
98-
let silo_id = authz_silo.id();
99-
100-
match pagparams {
101-
PaginatedBy::Id(pagparams) => {
102-
paginated(ip_pool::table, ip_pool::id, pagparams)
103-
}
104-
PaginatedBy::Name(pagparams) => paginated(
105-
ip_pool::table,
106-
ip_pool::name,
107-
&pagparams.map_name(|n| Name::ref_cast(n)),
108-
),
109-
}
110-
.inner_join(ip_pool_resource::table)
111-
.filter(
112-
ip_pool_resource::resource_type
113-
.eq(IpPoolResourceType::Silo)
114-
.and(ip_pool_resource::resource_id.eq(silo_id)),
115-
)
116-
.filter(ip_pool::time_deleted.is_null())
117-
.select(db::model::IpPool::as_select())
118-
.get_results_async(&*self.pool_connection_authorized(opctx).await?)
119-
.await
120-
.map_err(|e| public_error_from_diesel(e, ErrorHandler::Server))
121-
}
122-
12382
/// Look up whether the given pool is available to users in the current
12483
/// silo, i.e., whether there is an entry in the association table linking
12584
/// the pool with that silo
@@ -400,6 +359,37 @@ impl DataStore {
400359
.map_err(|e| public_error_from_diesel(e, ErrorHandler::Server))
401360
}
402361

362+
/// Returns (IpPool, IpPoolResource) so we can know in the calling code
363+
/// whether the pool is default for the silo
364+
pub async fn silo_ip_pool_list(
365+
&self,
366+
opctx: &OpContext,
367+
authz_silo: &authz::Silo,
368+
pagparams: &PaginatedBy<'_>,
369+
) -> ListResultVec<(IpPool, IpPoolResource)> {
370+
use db::schema::ip_pool;
371+
use db::schema::ip_pool_resource;
372+
373+
match pagparams {
374+
PaginatedBy::Id(pagparams) => {
375+
paginated(ip_pool::table, ip_pool::id, pagparams)
376+
}
377+
PaginatedBy::Name(pagparams) => paginated(
378+
ip_pool::table,
379+
ip_pool::name,
380+
&pagparams.map_name(|n| Name::ref_cast(n)),
381+
),
382+
}
383+
.inner_join(ip_pool_resource::table)
384+
.filter(ip_pool_resource::resource_id.eq(authz_silo.id()))
385+
.filter(ip_pool_resource::resource_type.eq(IpPoolResourceType::Silo))
386+
.filter(ip_pool::time_deleted.is_null())
387+
.select(<(IpPool, IpPoolResource)>::as_select())
388+
.load_async(&*self.pool_connection_authorized(opctx).await?)
389+
.await
390+
.map_err(|e| public_error_from_diesel(e, ErrorHandler::Server))
391+
}
392+
403393
pub async fn ip_pool_link_silo(
404394
&self,
405395
opctx: &OpContext,
@@ -867,8 +857,11 @@ mod test {
867857
.await
868858
.expect("Should list IP pools");
869859
assert_eq!(all_pools.len(), 0);
860+
861+
let authz_silo = opctx.authn.silo_required().unwrap();
862+
870863
let silo_pools = datastore
871-
.silo_ip_pools_list(&opctx, &pagbyid)
864+
.silo_ip_pool_list(&opctx, &authz_silo, &pagbyid)
872865
.await
873866
.expect("Should list silo IP pools");
874867
assert_eq!(silo_pools.len(), 0);
@@ -893,7 +886,7 @@ mod test {
893886
.expect("Should list IP pools");
894887
assert_eq!(all_pools.len(), 1);
895888
let silo_pools = datastore
896-
.silo_ip_pools_list(&opctx, &pagbyid)
889+
.silo_ip_pool_list(&opctx, &authz_silo, &pagbyid)
897890
.await
898891
.expect("Should list silo IP pools");
899892
assert_eq!(silo_pools.len(), 0);
@@ -929,11 +922,12 @@ mod test {
929922

930923
// now it shows up in the silo list
931924
let silo_pools = datastore
932-
.silo_ip_pools_list(&opctx, &pagbyid)
925+
.silo_ip_pool_list(&opctx, &authz_silo, &pagbyid)
933926
.await
934927
.expect("Should list silo IP pools");
935928
assert_eq!(silo_pools.len(), 1);
936-
assert_eq!(silo_pools[0].id(), pool1_for_silo.id());
929+
assert_eq!(silo_pools[0].0.id(), pool1_for_silo.id());
930+
assert_eq!(silo_pools[0].1.is_default, false);
937931

938932
// linking an already linked silo errors due to PK conflict
939933
let err = datastore
@@ -998,7 +992,7 @@ mod test {
998992

999993
// and silo pools list is empty again
1000994
let silo_pools = datastore
1001-
.silo_ip_pools_list(&opctx, &pagbyid)
995+
.silo_ip_pool_list(&opctx, &authz_silo, &pagbyid)
1002996
.await
1003997
.expect("Should list silo IP pools");
1004998
assert_eq!(silo_pools.len(), 0);

nexus/src/app/ip_pool.rs

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ use omicron_common::api::external::CreateResult;
2020
use omicron_common::api::external::DataPageParams;
2121
use omicron_common::api::external::DeleteResult;
2222
use omicron_common::api::external::Error;
23+
use omicron_common::api::external::InternalContext;
2324
use omicron_common::api::external::ListResultVec;
2425
use omicron_common::api::external::LookupResult;
2526
use omicron_common::api::external::NameOrId;
@@ -74,12 +75,20 @@ impl super::Nexus {
7475
}
7576

7677
/// List IP pools in current silo
77-
pub(crate) async fn silo_ip_pools_list(
78+
pub(crate) async fn current_silo_ip_pool_list(
7879
&self,
7980
opctx: &OpContext,
8081
pagparams: &PaginatedBy<'_>,
81-
) -> ListResultVec<db::model::IpPool> {
82-
self.db_datastore.silo_ip_pools_list(opctx, pagparams).await
82+
) -> ListResultVec<(db::model::IpPool, db::model::IpPoolResource)> {
83+
let authz_silo =
84+
opctx.authn.silo_required().internal_context("listing IP pools")?;
85+
86+
// From the developer user's point of view, we treat IP pools linked to
87+
// their silo as silo resources, so they can list them if they can list
88+
// silo children
89+
opctx.authorize(authz::Action::ListChildren, &authz_silo).await?;
90+
91+
self.db_datastore.silo_ip_pool_list(opctx, &authz_silo, pagparams).await
8392
}
8493

8594
// Look up pool by name or ID, but only return it if it's linked to the
@@ -88,19 +97,19 @@ impl super::Nexus {
8897
&'a self,
8998
opctx: &'a OpContext,
9099
pool: &'a NameOrId,
91-
) -> LookupResult<db::model::IpPool> {
100+
) -> LookupResult<(db::model::IpPool, db::model::IpPoolResource)> {
92101
let (authz_pool, pool) =
93102
self.ip_pool_lookup(opctx, pool)?.fetch().await?;
94103

95104
// 404 if no link is found in the current silo
96105
let link = self.db_datastore.ip_pool_fetch_link(opctx, pool.id()).await;
97-
if link.is_err() {
98-
return Err(authz_pool.not_found());
106+
match link {
107+
Ok(link) => Ok((pool, link)),
108+
Err(_) => Err(authz_pool.not_found()),
99109
}
100-
101-
Ok(pool)
102110
}
103111

112+
/// List silos for a given pool
104113
pub(crate) async fn ip_pool_silo_list(
105114
&self,
106115
opctx: &OpContext,
@@ -109,14 +118,34 @@ impl super::Nexus {
109118
) -> ListResultVec<db::model::IpPoolResource> {
110119
let (.., authz_pool) =
111120
pool_lookup.lookup_for(authz::Action::ListChildren).await?;
121+
122+
// check ability to list silos in general
123+
opctx.authorize(authz::Action::ListChildren, &authz::FLEET).await?;
124+
112125
self.db_datastore.ip_pool_silo_list(opctx, &authz_pool, pagparams).await
113126
}
114127

128+
// List pools for a given silo
129+
pub(crate) async fn silo_ip_pool_list(
130+
&self,
131+
opctx: &OpContext,
132+
silo_lookup: &lookup::Silo<'_>,
133+
pagparams: &PaginatedBy<'_>,
134+
) -> ListResultVec<(db::model::IpPool, db::model::IpPoolResource)> {
135+
let (.., authz_silo) =
136+
silo_lookup.lookup_for(authz::Action::Read).await?;
137+
// check ability to list pools in general
138+
opctx
139+
.authorize(authz::Action::ListChildren, &authz::IP_POOL_LIST)
140+
.await?;
141+
self.db_datastore.silo_ip_pool_list(opctx, &authz_silo, pagparams).await
142+
}
143+
115144
pub(crate) async fn ip_pool_link_silo(
116145
&self,
117146
opctx: &OpContext,
118147
pool_lookup: &lookup::IpPool<'_>,
119-
silo_link: &params::IpPoolSiloLink,
148+
silo_link: &params::IpPoolLinkSilo,
120149
) -> CreateResult<db::model::IpPoolResource> {
121150
let (authz_pool,) =
122151
pool_lookup.lookup_for(authz::Action::Modify).await?;

0 commit comments

Comments
 (0)