Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[nexus] Populate IP pool, nexus service information, during rack setup #2358

Merged
merged 16 commits into from
Feb 21, 2023
Merged
8 changes: 8 additions & 0 deletions common/src/sql/dbinit.sql
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,14 @@ CREATE INDEX ON omicron.public.service (
sled_id
);

-- Extended information for services where "service.kind = nexus"
-- The UUID should match the "omicron.public.service" table exactly.
bnaecker marked this conversation as resolved.
Show resolved Hide resolved
CREATE TABLE omicron.public.nexus_service (
id UUID PRIMARY KEY,
-- The external IP address used for Nexus' external interface.
external_ip_id UUID NOT NULL
);

CREATE TYPE omicron.public.physical_disk_kind AS ENUM (
'm2',
'u2'
Expand Down
28 changes: 28 additions & 0 deletions nexus/db-model/src/external_ip.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ use nexus_types::external_api::shared;
use nexus_types::external_api::views;
use omicron_common::api::external::Error;
use std::convert::TryFrom;
use std::net::IpAddr;
use uuid::Uuid;

impl_enum_type!(
Expand Down Expand Up @@ -90,6 +91,8 @@ pub struct IncompleteExternalIp {
kind: IpKind,
instance_id: Option<Uuid>,
pool_id: Uuid,
// Optional address requesting that a specific IP address be allocated.
explicit_ip: Option<IpNetwork>,
}

impl IncompleteExternalIp {
Expand All @@ -106,6 +109,7 @@ impl IncompleteExternalIp {
kind: IpKind::SNat,
instance_id: Some(instance_id),
pool_id,
explicit_ip: None,
}
}

Expand All @@ -118,6 +122,7 @@ impl IncompleteExternalIp {
kind: IpKind::Ephemeral,
instance_id: Some(instance_id),
pool_id,
explicit_ip: None,
}
}

Expand All @@ -135,6 +140,24 @@ impl IncompleteExternalIp {
kind: IpKind::Floating,
instance_id: None,
pool_id,
explicit_ip: None,
}
}

pub fn for_service_explicit(
id: Uuid,
pool_id: Uuid,
address: IpAddr,
) -> Self {
Self {
id,
name: None,
description: None,
time_created: Utc::now(),
kind: IpKind::Service,
instance_id: None,
pool_id,
explicit_ip: Some(IpNetwork::from(address)),
}
}

Expand All @@ -147,6 +170,7 @@ impl IncompleteExternalIp {
kind: IpKind::Service,
instance_id: None,
pool_id,
explicit_ip: None,
}
}

Expand Down Expand Up @@ -177,6 +201,10 @@ impl IncompleteExternalIp {
pub fn pool_id(&self) -> &Uuid {
&self.pool_id
}

pub fn explicit_ip(&self) -> &Option<IpNetwork> {
&self.explicit_ip
}
}

impl TryFrom<IpKind> for shared::IpKind {
Expand Down
2 changes: 2 additions & 0 deletions nexus/db-model/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ mod l4_port_range;
mod macaddr;
mod name;
mod network_interface;
mod nexus_service;
mod organization;
mod oximeter_info;
mod physical_disk;
Expand Down Expand Up @@ -114,6 +115,7 @@ pub use ipv6net::*;
pub use l4_port_range::*;
pub use name::*;
pub use network_interface::*;
pub use nexus_service::*;
pub use organization::*;
pub use oximeter_info::*;
pub use physical_disk::*;
Expand Down
20 changes: 20 additions & 0 deletions nexus/db-model/src/nexus_service.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

use crate::schema::nexus_service;
use uuid::Uuid;

/// Nexus-specific extended service information.
#[derive(Queryable, Insertable, Debug, Clone, Selectable)]
#[diesel(table_name = nexus_service)]
pub struct NexusService {
pub id: Uuid,
pub external_ip_id: Uuid,
}

impl NexusService {
pub fn new(id: Uuid, external_ip_id: Uuid) -> Self {
Self { id, external_ip_id }
}
}
7 changes: 7 additions & 0 deletions nexus/db-model/src/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,13 @@ table! {
}
}

table! {
nexus_service (id) {
id -> Uuid,
external_ip_id -> Uuid,
}
}

table! {
physical_disk (id) {
id -> Uuid,
Expand Down
32 changes: 23 additions & 9 deletions nexus/src/app/rack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,22 +65,34 @@ impl super::Nexus {
) -> Result<(), Error> {
opctx.authorize(authz::Action::Modify, &authz::FLEET).await?;

// Convert from parameter -> DB type.
let services: Vec<_> = request
.services
.into_iter()
.iter()
.map(|svc| {
db::model::Service::new(
svc.service_id,
svc.sled_id,
svc.address,
svc.kind.into(),
// Pull the external IP address out of the service request so
// it can be passed to the rack initialization method.
let external_ip =
if let crate::internal_api::params::ServiceKind::Nexus {
external_address,
} = svc.kind
{
Some(external_address)
} else {
None
};

(
db::model::Service::new(
svc.service_id,
svc.sled_id,
svc.address,
svc.kind.into(),
),
external_ip,
)
})
.collect();

// TODO(https://github.com/oxidecomputer/omicron/issues/1958): If nexus, add a pool?

let datasets: Vec<_> = request
.datasets
.into_iter()
Expand All @@ -94,6 +106,7 @@ impl super::Nexus {
})
.collect();

let service_ip_pool_ranges = request.internal_services_ip_pool;
let certificates: Vec<_> = request
.certs
.into_iter()
Expand Down Expand Up @@ -128,6 +141,7 @@ impl super::Nexus {
rack_id,
services,
datasets,
service_ip_pool_ranges,
certificates,
)
.await?;
Expand Down
69 changes: 54 additions & 15 deletions nexus/src/db/datastore/external_ip.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,19 @@ use crate::db::model::ExternalIp;
use crate::db::model::IncompleteExternalIp;
use crate::db::model::IpKind;
use crate::db::model::Name;
use crate::db::pool::DbConnection;
use crate::db::queries::external_ip::NextExternalIp;
use crate::db::update_and_check::UpdateAndCheck;
use crate::db::update_and_check::UpdateStatus;
use async_bb8_diesel::AsyncRunQueryDsl;
use async_bb8_diesel::{AsyncRunQueryDsl, PoolError};
use chrono::Utc;
use diesel::prelude::*;
use nexus_types::identity::Resource;
use omicron_common::api::external::CreateResult;
use omicron_common::api::external::Error;
use omicron_common::api::external::LookupResult;
use omicron_common::api::external::Name as ExternalName;
use std::net::IpAddr;
use std::str::FromStr;
use uuid::Uuid;

Expand Down Expand Up @@ -83,28 +85,65 @@ impl DataStore {
opctx: &OpContext,
data: IncompleteExternalIp,
) -> CreateResult<ExternalIp> {
NextExternalIp::new(data)
.get_result_async(self.pool_authorized(opctx).await?)
.await
.map_err(|e| {
use async_bb8_diesel::ConnectionError::Query;
use async_bb8_diesel::PoolError::Connection;
use diesel::result::Error::NotFound;
match e {
Connection(Query(NotFound)) => Error::invalid_request(
"No external IP addresses available",
),
_ => public_error_from_diesel_pool(e, ErrorHandler::Server),
let conn = self.pool_authorized(opctx).await?;
Self::allocate_external_ip_on_connection(conn, data).await
}

/// Variant of [Self::allocate_external_ip] which may be called from a
/// transaction context.
pub(crate) async fn allocate_external_ip_on_connection<ConnErr>(
conn: &(impl async_bb8_diesel::AsyncConnection<DbConnection, ConnErr>
+ Sync),
data: IncompleteExternalIp,
) -> CreateResult<ExternalIp>
where
ConnErr: From<diesel::result::Error> + Send + 'static,
PoolError: From<ConnErr>,
{
let explicit_ip = data.explicit_ip().is_some();
NextExternalIp::new(data).get_result_async(conn).await.map_err(|e| {
use async_bb8_diesel::ConnectionError::Query;
use async_bb8_diesel::PoolError::Connection;
use diesel::result::Error::NotFound;
let e = PoolError::from(e);
match e {
Connection(Query(NotFound)) => {
if explicit_ip {
Error::invalid_request(
"Requested external IP address not available",
)
} else {
Error::invalid_request(
"No external IP addresses available",
)
}
}
})
_ => public_error_from_diesel_pool(e, ErrorHandler::Server),
}
})
}

/// Allocates an explicit IP address for an internal service.
///
/// Unlike the other IP allocation requests, this does not search for an
/// available IP address, it asks for one explicitly.
pub async fn allocate_explicit_service_ip(
&self,
opctx: &OpContext,
ip_id: Uuid,
ip: IpAddr,
) -> CreateResult<ExternalIp> {
let (.., pool) = self.ip_pools_service_lookup(opctx).await?;
let data =
IncompleteExternalIp::for_service_explicit(ip_id, pool.id(), ip);
self.allocate_external_ip(opctx, data).await
}

/// Deallocate the external IP address with the provided ID.
///
/// To support idempotency, such as in saga operations, this method returns
/// an extra boolean, rather than the usual `DeleteResult`. The meaning of
/// return values are:
///
/// - `Ok(true)`: The record was deleted during this call
/// - `Ok(false)`: The record was already deleted, such as by a previous
/// call
Expand Down
23 changes: 21 additions & 2 deletions nexus/src/db/datastore/ip_pool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,11 @@ use crate::db::model::IpPoolRange;
use crate::db::model::IpPoolUpdate;
use crate::db::model::Name;
use crate::db::pagination::paginated;
use crate::db::pool::DbConnection;
use crate::db::queries::ip_pool::FilterOverlappingIpRanges;
use crate::external_api::params;
use crate::external_api::shared::IpRange;
use async_bb8_diesel::AsyncRunQueryDsl;
use async_bb8_diesel::{AsyncRunQueryDsl, PoolError};
use chrono::Utc;
use diesel::prelude::*;
use ipnetwork::IpNetwork;
Expand Down Expand Up @@ -287,6 +288,24 @@ impl DataStore {
authz_pool: &authz::IpPool,
range: &IpRange,
) -> CreateResult<IpPoolRange> {
let conn = self.pool_authorized(opctx).await?;
Self::ip_pool_add_range_on_connection(conn, opctx, authz_pool, range)
.await
}

/// Variant of [Self::ip_pool_add_range] which may be called from a
/// transaction context.
pub(crate) async fn ip_pool_add_range_on_connection<ConnErr>(
conn: &(impl async_bb8_diesel::AsyncConnection<DbConnection, ConnErr>
+ Sync),
opctx: &OpContext,
authz_pool: &authz::IpPool,
range: &IpRange,
) -> CreateResult<IpPoolRange>
where
ConnErr: From<diesel::result::Error> + Send + 'static,
PoolError: From<ConnErr>,
{
use db::schema::ip_pool_range::dsl;
opctx.authorize(authz::Action::CreateChild, authz_pool).await?;
let pool_id = authz_pool.id();
Expand All @@ -295,7 +314,7 @@ impl DataStore {
let insert_query =
diesel::insert_into(dsl::ip_pool_range).values(filter_subquery);
IpPool::insert_resource(pool_id, insert_query)
.insert_and_get_result_async(self.pool_authorized(opctx).await?)
.insert_and_get_result_async(conn)
.await
.map_err(|e| {
use async_bb8_diesel::ConnectionError::Query;
Expand Down
Loading