Skip to content

Commit b151e08

Browse files
committed
new default IP pool logic with fallback to higher levels
1 parent 049fc0f commit b151e08

File tree

13 files changed

+169
-14
lines changed

13 files changed

+169
-14
lines changed

nexus/db-model/src/ip_pool.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,15 @@ pub struct IpPool {
4848

4949
/// Project, if IP pool is associated with a particular project
5050
pub project_id: Option<Uuid>,
51+
52+
pub default: bool,
5153
}
5254

5355
impl IpPool {
5456
pub fn new(
5557
pool_identity: &external::IdentityMetadataCreateParams,
5658
silo_id: Option<Uuid>,
59+
default: bool,
5760
) -> Self {
5861
Self {
5962
identity: IpPoolIdentity::new(
@@ -63,6 +66,7 @@ impl IpPool {
6366
rcgen: 0,
6467
silo_id,
6568
project_id: None,
69+
default,
6670
}
6771
}
6872
}

nexus/db-model/src/schema.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,7 @@ table! {
455455
rcgen -> Int8,
456456
silo_id -> Nullable<Uuid>,
457457
project_id -> Nullable<Uuid>,
458+
default -> Bool,
458459
}
459460
}
460461

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,13 @@ impl DataStore {
5858
let name = pool_name.unwrap_or_else(|| {
5959
Name(ExternalName::from_str("default").unwrap())
6060
});
61+
62+
// TODO: this is the whole thing -- update this call to EITHER look up
63+
// the pool by name (only allowing access to pools matching the scope of
64+
// the current project, i.e., you can pick a pool by name only if it's
65+
// scoped to your project, silo, or the whole fleet) or use the default
66+
// logic in ip_pool_fetch_default_for
67+
6168
let (.., pool) = self
6269
.ip_pools_fetch_for(opctx, authz::Action::CreateChild, &name)
6370
.await?;

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

Lines changed: 123 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,9 @@ use omicron_common::api::external::Error;
3737
use omicron_common::api::external::ListResultVec;
3838
use omicron_common::api::external::LookupResult;
3939
use omicron_common::api::external::LookupType;
40-
use omicron_common::api::external::Name as ExternalName;
4140
use omicron_common::api::external::ResourceType;
4241
use omicron_common::api::external::UpdateResult;
4342
use ref_cast::RefCast;
44-
use std::str::FromStr;
4543
use uuid::Uuid;
4644

4745
impl DataStore {
@@ -74,18 +72,42 @@ impl DataStore {
7472
.map_err(|e| public_error_from_diesel_pool(e, ErrorHandler::Server))
7573
}
7674

77-
/// Looks up the default IP pool by name.
75+
/// Looks up the default IP pool for a given scope, i.e., a given
76+
/// combination of silo and project ID (or none). If there is no default at
77+
/// a given scope, fall back up a level. There should always be a default at
78+
/// fleet level, though this query can theoretically fail.
7879
pub async fn ip_pools_fetch_default_for(
7980
&self,
8081
opctx: &OpContext,
8182
action: authz::Action,
82-
) -> LookupResult<(authz::IpPool, IpPool)> {
83-
self.ip_pools_fetch_for(
84-
opctx,
85-
action,
86-
&Name(ExternalName::from_str("default").unwrap()),
87-
)
88-
.await
83+
silo_id: Option<Uuid>,
84+
project_id: Option<Uuid>,
85+
) -> LookupResult<IpPool> {
86+
use db::schema::ip_pool::dsl;
87+
opctx.authorize(action, &authz::IP_POOL_LIST).await?;
88+
89+
dsl::ip_pool
90+
.filter(dsl::silo_id.eq(silo_id).or(dsl::silo_id.is_null()))
91+
.filter(
92+
dsl::project_id.eq(project_id).or(dsl::project_id.is_null()),
93+
)
94+
.filter(dsl::default.eq(true))
95+
.filter(dsl::time_deleted.is_null())
96+
// this will sort by most specific first, i.e.,
97+
//
98+
// (silo, project)
99+
// (silo, null)
100+
// (null, null)
101+
//
102+
// then by only taking the first result, we get the most specific one
103+
.order((
104+
dsl::project_id.asc().nulls_last(),
105+
dsl::silo_id.asc().nulls_last(),
106+
))
107+
.select(IpPool::as_select())
108+
.first_async::<IpPool>(self.pool_authorized(opctx).await?)
109+
.await
110+
.map_err(|e| public_error_from_diesel_pool(e, ErrorHandler::Server))
89111
}
90112

91113
/// Looks up an IP pool by name.
@@ -429,3 +451,94 @@ impl DataStore {
429451
}
430452
}
431453
}
454+
455+
#[cfg(test)]
456+
mod test {
457+
use crate::authz;
458+
use crate::db::datastore::datastore_test;
459+
use crate::db::model::IpPool;
460+
use nexus_test_utils::db::test_setup_database;
461+
use nexus_types::identity::Resource;
462+
use omicron_common::api::external::IdentityMetadataCreateParams;
463+
use omicron_test_utils::dev;
464+
465+
#[tokio::test]
466+
async fn test_default_ip_pools() {
467+
let logctx = dev::test_setup_log("test_default_ip_pools");
468+
let mut db = test_setup_database(&logctx.log).await;
469+
let (opctx, datastore) = datastore_test(&logctx, &db).await;
470+
471+
let action = authz::Action::ListChildren;
472+
473+
// we start out with the default fleet-level pool already created,
474+
// so when we ask for the fleet default (no silo or project) we get it back
475+
let fleet_default_pool = datastore
476+
.ip_pools_fetch_default_for(&opctx, action, None, None)
477+
.await
478+
.unwrap();
479+
480+
assert_eq!(fleet_default_pool.identity.name.as_str(), "default");
481+
assert!(fleet_default_pool.default);
482+
assert_eq!(fleet_default_pool.silo_id, None);
483+
assert_eq!(fleet_default_pool.project_id, None);
484+
485+
// now the interesting thing is that when we fetch the default pool for
486+
// a particular silo or a particular project, if those scopes do not
487+
// have a default IP pool, we will still get back the fleet default
488+
489+
// default for "current" silo is still the fleet default one because it
490+
// has no default of its own
491+
let silo_id = opctx.authn.silo_required().unwrap().id();
492+
let ip_pool = datastore
493+
.ip_pools_fetch_default_for(&opctx, action, Some(silo_id), None)
494+
.await
495+
.expect("Failed to get silo's default IP pool");
496+
assert_eq!(ip_pool.id(), fleet_default_pool.id());
497+
498+
// create a non-default pool for the silo
499+
let identity = IdentityMetadataCreateParams {
500+
name: "non-default-for-silo".parse().unwrap(),
501+
description: "".to_string(),
502+
};
503+
let _ = datastore
504+
.ip_pool_create(
505+
&opctx,
506+
IpPool::new(&identity, Some(silo_id), /*default= */ false),
507+
)
508+
.await;
509+
510+
// because that one was not a default, when we ask for silo default
511+
// pool, we still get the fleet default
512+
let ip_pool = datastore
513+
.ip_pools_fetch_default_for(&opctx, action, Some(silo_id), None)
514+
.await
515+
.expect("Failed to get fleet default IP pool");
516+
assert_eq!(ip_pool.id(), fleet_default_pool.id());
517+
518+
// now create a default pool for the silo
519+
let identity = IdentityMetadataCreateParams {
520+
name: "default-for-silo".parse().unwrap(),
521+
description: "".to_string(),
522+
};
523+
let _ = datastore
524+
.ip_pool_create(&opctx, IpPool::new(&identity, Some(silo_id), true))
525+
.await;
526+
527+
// now when we ask for the silo default pool, we get the one we just made
528+
let ip_pool = datastore
529+
.ip_pools_fetch_default_for(&opctx, action, Some(silo_id), None)
530+
.await
531+
.expect("Failed to get silo's default IP pool");
532+
assert_eq!(ip_pool.name().as_str(), "default-for-silo");
533+
534+
// and of course, if we ask for the fleet default again we still get that one
535+
let ip_pool = datastore
536+
.ip_pools_fetch_default_for(&opctx, action, None, None)
537+
.await
538+
.expect("Failed to get fleet default IP pool");
539+
assert_eq!(ip_pool.id(), fleet_default_pool.id());
540+
541+
db.cleanup().await.unwrap();
542+
logctx.cleanup_successful();
543+
}
544+
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -563,6 +563,7 @@ impl DataStore {
563563
description: String::from("IP Pool for Oxide Services"),
564564
},
565565
Some(*INTERNAL_SILO_ID),
566+
true, // default for internal silo
566567
);
567568

568569
self.ip_pool_create(opctx, internal_pool).await.map(|_| ()).or_else(
@@ -578,6 +579,7 @@ impl DataStore {
578579
description: String::from("default IP pool"),
579580
},
580581
None, // no silo ID, fleet scoped
582+
true, // default for fleet
581583
);
582584
self.ip_pool_create(opctx, default_pool).await.map(|_| ()).or_else(
583585
|e| match e {

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -869,6 +869,7 @@ mod tests {
869869
description: format!("ip pool {}", name),
870870
},
871871
Some(*INTERNAL_SILO_ID),
872+
true,
872873
);
873874

874875
use crate::db::schema::ip_pool::dsl as ip_pool_dsl;
@@ -916,11 +917,13 @@ mod tests {
916917
}
917918

918919
async fn default_pool_id(&self) -> Uuid {
919-
let (.., pool) = self
920+
let pool = self
920921
.db_datastore
921922
.ip_pools_fetch_default_for(
922923
&self.opctx,
923924
crate::authz::Action::ListChildren,
925+
None,
926+
None,
924927
)
925928
.await
926929
.expect("Failed to lookup default ip pool");

nexus/src/app/ip_pool.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,11 @@ impl super::Nexus {
6666
}
6767
_ => None,
6868
};
69-
let pool = db::model::IpPool::new(&pool_params.identity, silo_id);
69+
let pool = db::model::IpPool::new(
70+
&pool_params.identity,
71+
silo_id,
72+
pool_params.default,
73+
);
7074
self.db_datastore.ip_pool_create(opctx, pool).await
7175
}
7276

nexus/src/app/sagas/instance_create.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -826,8 +826,14 @@ async fn sic_allocate_instance_snat_ip(
826826
let instance_id = sagactx.lookup::<Uuid>("instance_id")?;
827827
let ip_id = sagactx.lookup::<Uuid>("snat_ip_id")?;
828828

829-
let (.., pool) = datastore
830-
.ip_pools_fetch_default_for(&opctx, authz::Action::CreateChild)
829+
let pool = datastore
830+
.ip_pools_fetch_default_for(
831+
&opctx,
832+
authz::Action::CreateChild,
833+
// TODO: get these values right
834+
None,
835+
None,
836+
)
831837
.await
832838
.map_err(ActionError::action_failed)?;
833839
let pool_id = pool.identity.id;

nexus/test-utils/src/resource_helpers.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ pub async fn create_ip_pool(
141141
description: String::from("an ip pool"),
142142
},
143143
silo: None,
144+
default: false,
144145
},
145146
)
146147
.await;

nexus/tests/integration_tests/endpoints.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,8 @@ lazy_static! {
455455
description: String::from("an IP pool"),
456456
},
457457
silo: None,
458+
// TODO: this might error due to the unique index
459+
default: true,
458460
};
459461
pub static ref DEMO_IP_POOL_PROJ_URL: String =
460462
format!("/v1/ip-pools/{}?project={}", *DEMO_IP_POOL_NAME, *DEMO_PROJECT_NAME);

0 commit comments

Comments
 (0)