Skip to content

Commit cdc8461

Browse files
committed
Add queries to allocate external IPv6 addresses
- Rewrite the `NextExternalIp` query to allow IPv6 address allocations. This uses queries more like the existing "next-item" queries based on a self-join, where we're joining the existing address with those addresses plus-one, and taking the first free one. - Handle a few more failure-cases from the new query to ensure we detect address exhaustion, reallocation, and so on. All the existing tests continue to pass. - Add expectorate / explain "tests" for the new queries - Add a few new tests, some specifically for IPv6 address allocations and the details / corner cases of the new query structure - Closes #9245
1 parent a65cda6 commit cdc8461

File tree

11 files changed

+1378
-502
lines changed

11 files changed

+1378
-502
lines changed

nexus/db-model/src/external_ip.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,7 @@ pub struct IncompleteExternalIp {
256256
project_id: Option<Uuid>,
257257
state: IpAttachState,
258258
// Optional address requesting that a specific IP address be allocated.
259+
// TODO(ben) Make this an `IpAssignment`
259260
explicit_ip: Option<IpNetwork>,
260261
// Optional range when requesting a specific SNAT range be allocated.
261262
explicit_port_range: Option<(i32, i32)>,
@@ -373,7 +374,7 @@ impl IncompleteExternalIp {
373374
pool_id,
374375
project_id: Some(project_id),
375376
explicit_ip: Some(explicit_ip.into()),
376-
explicit_port_range: None,
377+
explicit_port_range: Some((0, u16::MAX.into())),
377378
state: kind.initial_state(),
378379
}
379380
}
@@ -398,7 +399,7 @@ impl IncompleteExternalIp {
398399

399400
(
400401
IpKind::Floating,
401-
None,
402+
Some((0, u16::MAX.into())),
402403
Some(name),
403404
Some(zone_kind.report_str().to_string()),
404405
state,

nexus/db-model/src/schema_versions.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ use std::{collections::BTreeMap, sync::LazyLock};
1616
///
1717
/// This must be updated when you change the database schema. Refer to
1818
/// schema/crdb/README.adoc in the root of this repository for details.
19-
pub const SCHEMA_VERSION: Version = Version::new(201, 0, 0);
19+
pub const SCHEMA_VERSION: Version = Version::new(202, 0, 0);
2020

2121
/// List of all past database schema versions, in *reverse* order
2222
///
@@ -28,6 +28,7 @@ static KNOWN_VERSIONS: LazyLock<Vec<KnownVersion>> = LazyLock::new(|| {
2828
// | leaving the first copy as an example for the next person.
2929
// v
3030
// KnownVersion::new(next_int, "unique-dirname-with-the-sql-files"),
31+
KnownVersion::new(202, "add-ip-to-external-ip-index"),
3132
KnownVersion::new(201, "scim-client-bearer-token"),
3233
KnownVersion::new(200, "dual-stack-network-interfaces"),
3334
KnownVersion::new(199, "multicast-pool-support"),

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

Lines changed: 44 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -243,15 +243,32 @@ impl DataStore {
243243
conn: &async_bb8_diesel::Connection<DbConnection>,
244244
data: IncompleteExternalIp,
245245
) -> Result<ExternalIp, TransactionError<Error>> {
246-
use diesel::result::DatabaseErrorKind::UniqueViolation;
247246
// Name needs to be cloned out here (if present) to give users a
248247
// sensible error message on name collision.
249248
let name = data.name().clone();
250249
let explicit_ip = data.explicit_ip().is_some();
251250
NextExternalIp::new(data).get_result_async(conn).await.map_err(|e| {
251+
use diesel::result::DatabaseErrorKind::NotNullViolation;
252+
use diesel::result::DatabaseErrorKind::UniqueViolation;
252253
use diesel::result::Error::DatabaseError;
253254
use diesel::result::Error::NotFound;
254255
match e {
256+
DatabaseError(NotNullViolation, ref info)
257+
if info.message().contains("in column \"ip\"") =>
258+
{
259+
if explicit_ip {
260+
TransactionError::CustomError(Error::invalid_request(
261+
"Requested external IP address not available",
262+
))
263+
} else {
264+
TransactionError::CustomError(
265+
Error::insufficient_capacity(
266+
"No external IP addresses available",
267+
"NextExternalIp::new tried to insert NULL ip",
268+
),
269+
)
270+
}
271+
}
255272
NotFound => {
256273
if explicit_ip {
257274
TransactionError::CustomError(Error::invalid_request(
@@ -266,17 +283,32 @@ impl DataStore {
266283
)
267284
}
268285
}
269-
// Floating IP: name conflict
270-
DatabaseError(UniqueViolation, ..) if name.is_some() => {
271-
TransactionError::CustomError(public_error_from_diesel(
272-
e,
273-
ErrorHandler::Conflict(
274-
ResourceType::FloatingIp,
275-
name.as_ref()
276-
.map(|m| m.as_str())
277-
.unwrap_or_default(),
278-
),
279-
))
286+
DatabaseError(UniqueViolation, ref info) => {
287+
// Attempt to re-use same IP address.
288+
if info.constraint_name() == Some("external_ip_unique") {
289+
TransactionError::CustomError(Error::invalid_request(
290+
"Requested external IP address not available",
291+
))
292+
// Floating IP: name conflict
293+
} else if info
294+
.constraint_name()
295+
.map(|name| name.starts_with("lookup_floating_"))
296+
.unwrap_or(false)
297+
{
298+
TransactionError::CustomError(public_error_from_diesel(
299+
e,
300+
ErrorHandler::Conflict(
301+
ResourceType::FloatingIp,
302+
name.as_ref()
303+
.map(|m| m.as_str())
304+
.unwrap_or_default(),
305+
),
306+
))
307+
} else {
308+
TransactionError::CustomError(
309+
crate::db::queries::external_ip::from_diesel(e),
310+
)
311+
}
280312
}
281313
_ => {
282314
if retryable(&e) {

0 commit comments

Comments
 (0)