Skip to content

Commit ffd479b

Browse files
committed
Respect IP Pool when allocating ephemeral addresses for guests
- Use IP Pool name provided by clients in the actual allocation query and calls using it - Add test verifying restriction of IPs to the requested pool, when an address from another pool would normally be preferred.
1 parent e9554ad commit ffd479b

File tree

2 files changed

+120
-12
lines changed

2 files changed

+120
-12
lines changed

nexus/src/db/datastore/instance_external_ip.rs

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ use crate::db::error::ErrorHandler;
1212
use crate::db::model::IncompleteInstanceExternalIp;
1313
use crate::db::model::InstanceExternalIp;
1414
use crate::db::model::IpKind;
15+
use crate::db::model::IpPool;
1516
use crate::db::model::Name;
1617
use crate::db::queries::external_ip::NextExternalIp;
1718
use crate::db::update_and_check::UpdateAndCheck;
@@ -22,6 +23,8 @@ use diesel::prelude::*;
2223
use omicron_common::api::external::CreateResult;
2324
use omicron_common::api::external::Error;
2425
use omicron_common::api::external::LookupResult;
26+
use omicron_common::api::external::LookupType;
27+
use omicron_common::api::external::ResourceType;
2528
use uuid::Uuid;
2629

2730
impl DataStore {
@@ -50,13 +53,42 @@ impl DataStore {
5053
ip_id: Uuid,
5154
project_id: Uuid,
5255
instance_id: Uuid,
53-
_pool_name: Option<Name>,
56+
pool_name: Option<Name>,
5457
) -> CreateResult<InstanceExternalIp> {
58+
let pool_id = if let Some(ref name) = pool_name {
59+
// We'd like to add authz checks here, and use the `LookupPath`
60+
// methods on the project-scoped view of this resource. It's not
61+
// entirely clear how that'll work in the API, so see RFD 288 and
62+
// https://github.com/oxidecomputer/omicron/issues/1470 for more
63+
// details.
64+
use db::schema::ip_pool::dsl;
65+
Some(
66+
dsl::ip_pool
67+
.filter(dsl::name.eq(name.clone()))
68+
.filter(dsl::time_deleted.is_null())
69+
.select(IpPool::as_select())
70+
.first_async::<IpPool>(self.pool_authorized(opctx).await?)
71+
.await
72+
.map_err(|e| {
73+
public_error_from_diesel_pool(
74+
e,
75+
ErrorHandler::NotFoundByLookup(
76+
ResourceType::IpPool,
77+
LookupType::ByName(name.to_string()),
78+
),
79+
)
80+
})?
81+
.identity
82+
.id,
83+
)
84+
} else {
85+
None
86+
};
5587
let data = IncompleteInstanceExternalIp::for_ephemeral(
5688
ip_id,
5789
project_id,
5890
instance_id,
59-
/* pool_id = */ None,
91+
pool_id,
6092
);
6193
self.allocate_instance_external_ip(opctx, data).await
6294
}

nexus/src/db/queries/external_ip.rs

Lines changed: 86 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -98,8 +98,7 @@ const MAX_PORT: i32 = u16::MAX as _;
9898
/// FROM
9999
/// ip_pool_range
100100
/// WHERE
101-
/// project_id = <project_id> OR
102-
/// project_id IS NULL AND
101+
/// <pool restriction clause> AND
103102
/// time_deleted IS NULL
104103
/// )
105104
/// CROSS JOIN
@@ -183,6 +182,27 @@ const MAX_PORT: i32 = u16::MAX as _;
183182
/// (even though we send the start/stop to the database as `i32`s). We need to
184183
/// cast it on the way out of the database, so that Diesel can correctly
185184
/// deserialize it into an `i32`.
185+
///
186+
/// Pool restriction
187+
/// ----------------
188+
///
189+
/// Clients may optionally request an external address from a specific IP Pool.
190+
/// If they don't provide a pool, the query is restricted to all IP Pools
191+
/// available to their project (not reserved for a different project). In the
192+
/// first case, the restriction is:
193+
///
194+
/// ```sql
195+
/// pool_id = <pool_id>
196+
/// ```
197+
///
198+
/// In the latter:
199+
///
200+
/// ```sql
201+
/// (project_id = <project_id> OR project_id IS NULL)
202+
/// ```
203+
///
204+
/// I.e., the pool is reserved for _this instance's_ project, or not reserved at
205+
/// all.
186206
#[derive(Debug, Clone)]
187207
pub struct NextExternalIp {
188208
// TODO-completeness: Add `ip_pool_id`, and restrict search to it.
@@ -390,7 +410,7 @@ impl NextExternalIp {
390410
// FROM
391411
// ip_pool_range
392412
// WHERE
393-
// (project_id = <project_id> OR project_id IS NULL) AND
413+
// <pool_restriction> AND
394414
// time_deleted IS NULL
395415
// ```
396416
fn push_address_sequence_subquery<'a>(
@@ -410,13 +430,21 @@ impl NextExternalIp {
410430
out.push_identifier(dsl::first_address::NAME)?;
411431
out.push_sql(") AS candidate_ip FROM ");
412432
IP_POOL_RANGE_FROM_CLAUSE.walk_ast(out.reborrow())?;
413-
out.push_sql(" WHERE (");
414-
out.push_identifier(dsl::project_id::NAME)?;
415-
out.push_sql(" = ");
416-
out.push_bind_param::<sql_types::Uuid, Uuid>(self.ip.project_id())?;
417-
out.push_sql(" OR ");
418-
out.push_identifier(dsl::project_id::NAME)?;
419-
out.push_sql(" IS NULL) AND ");
433+
out.push_sql(" WHERE ");
434+
if let Some(ref pool_id) = self.ip.pool_id() {
435+
out.push_identifier(dsl::ip_pool_id::NAME)?;
436+
out.push_sql(" = ");
437+
out.push_bind_param::<sql_types::Uuid, Uuid>(pool_id)?;
438+
} else {
439+
out.push_sql("(");
440+
out.push_identifier(dsl::project_id::NAME)?;
441+
out.push_sql(" = ");
442+
out.push_bind_param::<sql_types::Uuid, Uuid>(self.ip.project_id())?;
443+
out.push_sql(" OR ");
444+
out.push_identifier(dsl::project_id::NAME)?;
445+
out.push_sql(" IS NULL)");
446+
}
447+
out.push_sql(" AND ");
420448
out.push_identifier(dsl::time_deleted::NAME)?;
421449
out.push_sql(" IS NULL");
422450
Ok(())
@@ -581,6 +609,7 @@ mod tests {
581609
use crate::db::model::IpKind;
582610
use crate::db::model::IpPool;
583611
use crate::db::model::IpPoolRange;
612+
use crate::db::model::Name;
584613
use crate::external_api::shared::IpRange;
585614
use async_bb8_diesel::AsyncRunQueryDsl;
586615
use dropshot::test_util::LogContext;
@@ -999,4 +1028,51 @@ mod tests {
9991028

10001029
context.success().await;
10011030
}
1031+
1032+
#[tokio::test]
1033+
async fn test_next_external_ip_is_restricted_to_pools() {
1034+
let context =
1035+
TestContext::new("test_next_external_ip_is_restricted_to_pools")
1036+
.await;
1037+
1038+
// Create two pools, neither project-restricted.
1039+
let first_range = IpRange::try_from((
1040+
Ipv4Addr::new(10, 0, 0, 1),
1041+
Ipv4Addr::new(10, 0, 0, 3),
1042+
))
1043+
.unwrap();
1044+
context.create_ip_pool("p0", first_range, None).await;
1045+
let second_range = IpRange::try_from((
1046+
Ipv4Addr::new(10, 0, 0, 4),
1047+
Ipv4Addr::new(10, 0, 0, 6),
1048+
))
1049+
.unwrap();
1050+
context.create_ip_pool("p1", second_range, None).await;
1051+
1052+
// Allocating an address on an instance in the second pool should be
1053+
// respected, even though there are IPs available in the first.
1054+
let instance_id = Uuid::new_v4();
1055+
let project_id = Uuid::new_v4();
1056+
let id = Uuid::new_v4();
1057+
let pool_name = Some(Name("p1".parse().unwrap()));
1058+
1059+
let ip = context
1060+
.db_datastore
1061+
.allocate_instance_ephemeral_ip(
1062+
&context.opctx,
1063+
id,
1064+
project_id,
1065+
instance_id,
1066+
pool_name,
1067+
)
1068+
.await
1069+
.expect("Failed to allocate instance ephemeral IP address");
1070+
assert_eq!(ip.kind, IpKind::Ephemeral);
1071+
assert_eq!(ip.ip.ip(), second_range.first_address());
1072+
assert_eq!(ip.first_port.0, 0);
1073+
assert_eq!(ip.last_port.0, u16::MAX);
1074+
assert_eq!(ip.project_id, project_id);
1075+
1076+
context.success().await;
1077+
}
10021078
}

0 commit comments

Comments
 (0)