Skip to content

Commit 5b515c9

Browse files
authored
Add db structures for Oxide service IP pools (#1531)
Necessary for #1530 - Adds endpoints for "IP Pools used by Oxide services". These use the same "IP Pool" database implementation internally, but expose a distinct endpoint through the HTTP API. - Adds a step to the "populate" process for creating the Oxide-owned IP pool. This is where IPs provisioned for usage by Nexus will be stored.
1 parent 356c449 commit 5b515c9

File tree

13 files changed

+691
-10
lines changed

13 files changed

+691
-10
lines changed

common/src/sql/dbinit.sql

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -969,6 +969,14 @@ CREATE TABLE omicron.public.ip_pool (
969969
/* Optional ID of the project for which this pool is reserved. */
970970
project_id UUID,
971971

972+
/*
973+
* Optional rack ID, indicating this is a reserved pool for internal
974+
* services on a specific rack.
975+
* TODO(https://github.com/oxidecomputer/omicron/issues/1276): This
976+
* should probably point to an AZ or fleet, not a rack.
977+
*/
978+
rack_id UUID,
979+
972980
/* The collection's child-resource generation number */
973981
rcgen INT8 NOT NULL
974982
);
@@ -981,6 +989,15 @@ CREATE UNIQUE INDEX ON omicron.public.ip_pool (
981989
) WHERE
982990
time_deleted IS NULL;
983991

992+
/*
993+
* Index ensuring uniqueness of IP pools by rack ID
994+
*/
995+
CREATE UNIQUE INDEX ON omicron.public.ip_pool (
996+
rack_id
997+
) WHERE
998+
rack_id IS NOT NULL AND
999+
time_deleted IS NULL;
1000+
9841001
/*
9851002
* IP Pools are made up of a set of IP ranges, which are start/stop addresses.
9861003
* Note that these need not be CIDR blocks or well-behaved subnets with a

nexus/db-model/src/ip_pool.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ pub struct IpPool {
3131
/// An optional ID of the project for which this pool is reserved.
3232
pub project_id: Option<Uuid>,
3333

34+
/// An optional ID of the rack for which this pool is reserved.
35+
// TODO(https://github.com/oxidecomputer/omicron/issues/1276): This
36+
// should probably point to an AZ or fleet, not a rack.
37+
pub rack_id: Option<Uuid>,
38+
3439
/// Child resource generation number, for optimistic concurrency control of
3540
/// the contained ranges.
3641
pub rcgen: i64,
@@ -40,13 +45,15 @@ impl IpPool {
4045
pub fn new(
4146
pool_identity: &external::IdentityMetadataCreateParams,
4247
project_id: Option<Uuid>,
48+
rack_id: Option<Uuid>,
4349
) -> Self {
4450
Self {
4551
identity: IpPoolIdentity::new(
4652
Uuid::new_v4(),
4753
pool_identity.clone(),
4854
),
4955
project_id,
56+
rack_id,
5057
rcgen: 0,
5158
}
5259
}

nexus/db-model/src/schema.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ table! {
144144
time_modified -> Timestamptz,
145145
time_deleted -> Nullable<Timestamptz>,
146146
project_id -> Nullable<Uuid>,
147+
rack_id -> Nullable<Uuid>,
147148
rcgen -> Int8,
148149
}
149150
}

nexus/src/app/ip_pool.rs

Lines changed: 106 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@ use ipnetwork::IpNetwork;
1515
use omicron_common::api::external::CreateResult;
1616
use omicron_common::api::external::DataPageParams;
1717
use omicron_common::api::external::DeleteResult;
18+
use omicron_common::api::external::Error;
1819
use omicron_common::api::external::ListResultVec;
1920
use omicron_common::api::external::LookupResult;
21+
use omicron_common::api::external::ResourceType;
2022
use omicron_common::api::external::UpdateResult;
2123
use uuid::Uuid;
2224

@@ -26,7 +28,16 @@ impl super::Nexus {
2628
opctx: &OpContext,
2729
new_pool: &params::IpPoolCreate,
2830
) -> CreateResult<db::model::IpPool> {
29-
self.db_datastore.ip_pool_create(opctx, new_pool).await
31+
self.db_datastore.ip_pool_create(opctx, new_pool, None).await
32+
}
33+
34+
pub async fn ip_pool_services_create(
35+
&self,
36+
opctx: &OpContext,
37+
new_pool: &params::IpPoolCreate,
38+
rack_id: Uuid,
39+
) -> CreateResult<db::model::IpPool> {
40+
self.db_datastore.ip_pool_create(opctx, new_pool, Some(rack_id)).await
3041
}
3142

3243
pub async fn ip_pools_list_by_name(
@@ -91,10 +102,18 @@ impl super::Nexus {
91102
pool_name: &Name,
92103
pagparams: &DataPageParams<'_, IpNetwork>,
93104
) -> ListResultVec<db::model::IpPoolRange> {
94-
let (.., authz_pool) = LookupPath::new(opctx, &self.db_datastore)
95-
.ip_pool_name(pool_name)
96-
.lookup_for(authz::Action::ListChildren)
97-
.await?;
105+
let (.., authz_pool, db_pool) =
106+
LookupPath::new(opctx, &self.db_datastore)
107+
.ip_pool_name(pool_name)
108+
.fetch_for(authz::Action::ListChildren)
109+
.await?;
110+
if db_pool.rack_id.is_some() {
111+
return Err(Error::not_found_by_name(
112+
ResourceType::IpPool,
113+
pool_name,
114+
));
115+
}
116+
98117
self.db_datastore
99118
.ip_pool_list_ranges(opctx, &authz_pool, pagparams)
100119
.await
@@ -111,6 +130,12 @@ impl super::Nexus {
111130
.ip_pool_name(pool_name)
112131
.fetch_for(authz::Action::Modify)
113132
.await?;
133+
if db_pool.rack_id.is_some() {
134+
return Err(Error::not_found_by_name(
135+
ResourceType::IpPool,
136+
pool_name,
137+
));
138+
}
114139
self.db_datastore
115140
.ip_pool_add_range(opctx, &authz_pool, &db_pool, range)
116141
.await
@@ -122,10 +147,83 @@ impl super::Nexus {
122147
pool_name: &Name,
123148
range: &IpRange,
124149
) -> DeleteResult {
125-
let (.., authz_pool) = LookupPath::new(opctx, &self.db_datastore)
126-
.ip_pool_name(pool_name)
127-
.lookup_for(authz::Action::Modify)
150+
let (.., authz_pool, db_pool) =
151+
LookupPath::new(opctx, &self.db_datastore)
152+
.ip_pool_name(pool_name)
153+
.fetch_for(authz::Action::Modify)
154+
.await?;
155+
if db_pool.rack_id.is_some() {
156+
return Err(Error::not_found_by_name(
157+
ResourceType::IpPool,
158+
pool_name,
159+
));
160+
}
161+
self.db_datastore.ip_pool_delete_range(opctx, &authz_pool, range).await
162+
}
163+
164+
// The "ip_pool_service_..." functions look up IP pools for Oxide service usage,
165+
// rather than for VMs. As such, they're identified by rack UUID, not
166+
// by pool names.
167+
//
168+
// TODO(https://github.com/oxidecomputer/omicron/issues/1276): Should be
169+
// AZ UUID, probably.
170+
171+
pub async fn ip_pool_service_fetch(
172+
&self,
173+
opctx: &OpContext,
174+
rack_id: Uuid,
175+
) -> LookupResult<db::model::IpPool> {
176+
let (authz_pool, db_pool) = self
177+
.db_datastore
178+
.ip_pools_lookup_by_rack_id(opctx, rack_id)
179+
.await?;
180+
opctx.authorize(authz::Action::Read, &authz_pool).await?;
181+
Ok(db_pool)
182+
}
183+
184+
pub async fn ip_pool_service_list_ranges(
185+
&self,
186+
opctx: &OpContext,
187+
rack_id: Uuid,
188+
pagparams: &DataPageParams<'_, IpNetwork>,
189+
) -> ListResultVec<db::model::IpPoolRange> {
190+
let (authz_pool, ..) = self
191+
.db_datastore
192+
.ip_pools_lookup_by_rack_id(opctx, rack_id)
193+
.await?;
194+
opctx.authorize(authz::Action::Read, &authz_pool).await?;
195+
self.db_datastore
196+
.ip_pool_list_ranges(opctx, &authz_pool, pagparams)
197+
.await
198+
}
199+
200+
pub async fn ip_pool_service_add_range(
201+
&self,
202+
opctx: &OpContext,
203+
rack_id: Uuid,
204+
range: &IpRange,
205+
) -> UpdateResult<db::model::IpPoolRange> {
206+
let (authz_pool, db_pool) = self
207+
.db_datastore
208+
.ip_pools_lookup_by_rack_id(opctx, rack_id)
209+
.await?;
210+
opctx.authorize(authz::Action::Modify, &authz_pool).await?;
211+
self.db_datastore
212+
.ip_pool_add_range(opctx, &authz_pool, &db_pool, range)
213+
.await
214+
}
215+
216+
pub async fn ip_pool_service_delete_range(
217+
&self,
218+
opctx: &OpContext,
219+
rack_id: Uuid,
220+
range: &IpRange,
221+
) -> DeleteResult {
222+
let (authz_pool, ..) = self
223+
.db_datastore
224+
.ip_pools_lookup_by_rack_id(opctx, rack_id)
128225
.await?;
226+
opctx.authorize(authz::Action::Modify, &authz_pool).await?;
129227
self.db_datastore.ip_pool_delete_range(opctx, &authz_pool, range).await
130228
}
131229
}

nexus/src/authz/omicron.polar

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,3 +436,5 @@ has_permission(_actor: AuthenticatedActor, "query", _resource: Database);
436436
# The "db-init" user is the only one with the "init" role.
437437
has_permission(actor: AuthenticatedActor, "modify", _resource: Database)
438438
if actor = USER_DB_INIT;
439+
has_permission(actor: AuthenticatedActor, "create_child", _resource: IpPoolList)
440+
if actor = USER_DB_INIT;

nexus/src/db/datastore/ip_pool.rs

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ use omicron_common::api::external::DataPageParams;
3232
use omicron_common::api::external::DeleteResult;
3333
use omicron_common::api::external::Error;
3434
use omicron_common::api::external::ListResultVec;
35+
use omicron_common::api::external::LookupResult;
3536
use omicron_common::api::external::LookupType;
3637
use omicron_common::api::external::ResourceType;
3738
use omicron_common::api::external::UpdateResult;
@@ -49,6 +50,7 @@ impl DataStore {
4950
.authorize(authz::Action::ListChildren, &authz::IP_POOL_LIST)
5051
.await?;
5152
paginated(dsl::ip_pool, dsl::name, pagparams)
53+
.filter(dsl::rack_id.is_null())
5254
.filter(dsl::time_deleted.is_null())
5355
.select(db::model::IpPool::as_select())
5456
.get_results_async(self.pool_authorized(opctx).await?)
@@ -67,17 +69,71 @@ impl DataStore {
6769
.authorize(authz::Action::ListChildren, &authz::IP_POOL_LIST)
6870
.await?;
6971
paginated(dsl::ip_pool, dsl::id, pagparams)
72+
.filter(dsl::rack_id.is_null())
7073
.filter(dsl::time_deleted.is_null())
7174
.select(db::model::IpPool::as_select())
7275
.get_results_async(self.pool_authorized(opctx).await?)
7376
.await
7477
.map_err(|e| public_error_from_diesel_pool(e, ErrorHandler::Server))
7578
}
7679

80+
/// Looks up an IP pool by a particular Rack ID.
81+
///
82+
/// An index exists to look up pools by rack ID, but it is not a primary
83+
/// key, which requires this lookup function to be used instead of the
84+
/// [`LookupPath`] utility.
85+
pub async fn ip_pools_lookup_by_rack_id(
86+
&self,
87+
opctx: &OpContext,
88+
rack_id: Uuid,
89+
) -> LookupResult<(authz::IpPool, IpPool)> {
90+
use db::schema::ip_pool::dsl;
91+
92+
// Ensure the caller has the ability to look up these IP pools.
93+
// If they don't, return "not found" instead of "forbidden".
94+
opctx
95+
.authorize(authz::Action::ListChildren, &authz::IP_POOL_LIST)
96+
.await
97+
.map_err(|e| match e {
98+
Error::Forbidden => {
99+
LookupType::ByCompositeId(format!("Rack ID: {rack_id}"))
100+
.into_not_found(ResourceType::IpPool)
101+
}
102+
_ => e,
103+
})?;
104+
105+
// Look up this IP pool by rack ID.
106+
let (authz_pool, pool) = dsl::ip_pool
107+
.filter(dsl::rack_id.eq(Some(rack_id)))
108+
.filter(dsl::time_deleted.is_null())
109+
.select(IpPool::as_select())
110+
.get_result_async(self.pool_authorized(opctx).await?)
111+
.await
112+
.map_err(|e| public_error_from_diesel_pool(e, ErrorHandler::Server))
113+
.map(|ip_pool| {
114+
(
115+
authz::IpPool::new(
116+
authz::FLEET,
117+
ip_pool.id(),
118+
LookupType::ByCompositeId(format!(
119+
"Rack ID: {rack_id}"
120+
)),
121+
),
122+
ip_pool,
123+
)
124+
})?;
125+
Ok((authz_pool, pool))
126+
}
127+
128+
/// Creates a new IP pool.
129+
///
130+
/// - If `rack_id` is provided, this IP pool is used for Oxide
131+
/// services.
77132
pub async fn ip_pool_create(
78133
&self,
79134
opctx: &OpContext,
80135
new_pool: &params::IpPoolCreate,
136+
rack_id: Option<Uuid>,
81137
) -> CreateResult<IpPool> {
82138
use db::schema::ip_pool::dsl;
83139
opctx
@@ -86,6 +142,12 @@ impl DataStore {
86142
let project_id = match new_pool.project.clone() {
87143
None => None,
88144
Some(project) => {
145+
if let Some(_) = &rack_id {
146+
return Err(Error::invalid_request(
147+
"Internal Service IP pools cannot be project-scoped",
148+
));
149+
}
150+
89151
let (.., authz_project) = LookupPath::new(opctx, self)
90152
.organization_name(&Name(project.organization))
91153
.project_name(&Name(project.project))
@@ -94,7 +156,7 @@ impl DataStore {
94156
Some(authz_project.id())
95157
}
96158
};
97-
let pool = IpPool::new(&new_pool.identity, project_id);
159+
let pool = IpPool::new(&new_pool.identity, project_id, rack_id);
98160
let pool_name = pool.name().as_str().to_string();
99161
diesel::insert_into(dsl::ip_pool)
100162
.values(pool)
@@ -143,6 +205,7 @@ impl DataStore {
143205
// in between the above check for children and this query.
144206
let now = Utc::now();
145207
let updated_rows = diesel::update(dsl::ip_pool)
208+
.filter(dsl::rack_id.is_null())
146209
.filter(dsl::time_deleted.is_null())
147210
.filter(dsl::id.eq(authz_pool.id()))
148211
.filter(dsl::rcgen.eq(db_pool.rcgen))
@@ -174,6 +237,7 @@ impl DataStore {
174237
use db::schema::ip_pool::dsl;
175238
opctx.authorize(authz::Action::Modify, authz_pool).await?;
176239
diesel::update(dsl::ip_pool)
240+
.filter(dsl::rack_id.is_null())
177241
.filter(dsl::id.eq(authz_pool.id()))
178242
.filter(dsl::time_deleted.is_null())
179243
.set(updates)

nexus/src/db/queries/external_ip.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -662,7 +662,8 @@ mod tests {
662662
name: String::from(name).parse().unwrap(),
663663
description: format!("ip pool {}", name),
664664
},
665-
None,
665+
/* project_id= */ None,
666+
/* rack_id= */ None,
666667
);
667668
pool.project_id = project_id;
668669

0 commit comments

Comments
 (0)