Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions common/src/address.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,18 @@ pub const AZ_PREFIX: u8 = 48;
pub const RACK_PREFIX: u8 = 56;
pub const SLED_PREFIX: u8 = 64;

// Multicast constants

/// IPv4 Source-Specific Multicast (SSM) subnet as defined in RFC 4607:
/// <https://tools.ietf.org/html/rfc4607>.
pub const IPV4_SSM_SUBNET: oxnet::Ipv4Net =
oxnet::Ipv4Net::new_unchecked(Ipv4Addr::new(232, 0, 0, 0), 8);

/// IPv6 Source-Specific Multicast (SSM) flag field value as defined in RFC 4607:
/// <https://tools.ietf.org/html/rfc4607>.
/// This is the flags nibble (high nibble of second byte) for FF3x::/32 addresses.
pub const IPV6_SSM_FLAG_FIELD: u8 = 3;

/// maximum possible value for a tcp or udp port
pub const MAX_PORT: u16 = u16::MAX;

Expand Down
22 changes: 21 additions & 1 deletion common/src/vlan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,18 @@
//! VLAN ID wrapper.

use crate::api::external::Error;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use std::fmt;
use std::str::FromStr;

/// The maximum VLAN value (inclusive), as specified by IEEE 802.1Q.
pub const VLAN_MAX: u16 = 4094;

/// Wrapper around a VLAN ID, ensuring it is valid.
#[derive(Debug, Deserialize, Clone, Copy)]
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone, Copy, JsonSchema)]
#[serde(rename = "VlanId")]
pub struct VlanID(u16);

impl VlanID {
Expand Down Expand Up @@ -44,3 +47,20 @@ impl FromStr for VlanID {
)
}
}

impl From<VlanID> for u16 {
fn from(vlan_id: VlanID) -> u16 {
vlan_id.0
}
}

impl slog::Value for VlanID {
fn serialize(
&self,
_record: &slog::Record,
key: slog::Key,
serializer: &mut dyn slog::Serializer,
) -> slog::Result {
serializer.emit_u16(key, self.0)
}
}
2 changes: 2 additions & 0 deletions docs/control-plane-architecture.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ NOTE: Much of this material originally came from <<rfd48>> and <<rfd61>>. This

NOTE: The RFD references in this documentation may be Oxide-internal. Where possible, we're trying to move relevant documentation from those RFDs into docs here.

See also: link:../notes/multicast-architecture.adoc[Multicast Architecture: VLAN Scope]

== What is the control plane

In software systems the terms **data plane** and **control plane** are often used to refer to the parts of the system that directly provide resources to users (the data plane) and the parts that support the configuration, control, monitoring, and operation of the system (the control plane). Within the Oxide system, we say that the data plane comprises those parts that provide CPU resources (including both the host CPU and hypervisor software), storage resources, and network resources. The control plane provides the APIs through which users provision, configure, and monitor these resources and the mechanisms through which these APIs are implemented. Also part of the control plane are the APIs and facilities through which operators manage the system itself, including fault management, alerting, software updates for various components of the system, and so on.
Expand Down
2 changes: 2 additions & 0 deletions docs/networking.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

This is a very rough introduction to how networking works within the Oxide system and particularly the control plane (Omicron). Much more information is available in various RFDs, particularly <<rfd63>>.

See also: link:../notes/multicast-architecture.adoc[Multicast Architecture: VLAN Scope]

== IPv6: the least you need to know

While IPv4 can be used for connectivity between Omicron and the outside world, everything else in the system uses IPv6. This section provides a _very_ cursory introduction to IPv6 for people only familiar with IPv4. You can skip this if you know IPv6. If you want slightly more detail than what's here, see https://www.roesen.org/files/ipv6_cheat_sheet.pdf[this cheat sheet].
Expand Down
5 changes: 3 additions & 2 deletions end-to-end-tests/src/bin/bootstrap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ use end_to_end_tests::helpers::{
use omicron_test_utils::dev::poll::{CondCheckError, wait_for_condition};
use oxide_client::types::{
ByteCount, DeviceAccessTokenRequest, DeviceAuthRequest, DeviceAuthVerify,
DiskCreate, DiskSource, IpPoolCreate, IpPoolLinkSilo, IpVersion, NameOrId,
SiloQuotasUpdate,
DiskCreate, DiskSource, IpPoolCreate, IpPoolLinkSilo, IpPoolType,
IpVersion, NameOrId, SiloQuotasUpdate,
};
use oxide_client::{
ClientConsoleAuthExt, ClientDisksExt, ClientProjectsExt,
Expand Down Expand Up @@ -53,6 +53,7 @@ async fn run_test() -> Result<()> {
name: pool_name.parse().unwrap(),
description: "Default IP pool".to_string(),
ip_version,
pool_type: IpPoolType::Unicast,
})
.send()
.await?;
Expand Down
5 changes: 3 additions & 2 deletions end-to-end-tests/src/bin/commtest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ use oxide_client::{
ClientSystemHardwareExt, ClientSystemIpPoolsExt, ClientSystemStatusExt,
ClientVpcsExt,
types::{
IpPoolCreate, IpPoolLinkSilo, IpRange, IpVersion, Name, NameOrId,
PingStatus, ProbeCreate, ProbeInfo, ProjectCreate,
IpPoolCreate, IpPoolLinkSilo, IpPoolType, IpRange, IpVersion, Name,
NameOrId, PingStatus, ProbeCreate, ProbeInfo, ProjectCreate,
UsernamePasswordCredentials,
},
};
Expand Down Expand Up @@ -295,6 +295,7 @@ async fn rack_prepare(
name: pool_name.parse().unwrap(),
description: "Default IP pool".to_string(),
ip_version,
pool_type: IpPoolType::Unicast,
})
.send()
.await?;
Expand Down
2 changes: 2 additions & 0 deletions nexus/db-model/src/generation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use diesel::pg::Pg;
use diesel::serialize::{self, ToSql};
use diesel::sql_types;
use omicron_common::api::external;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::convert::TryFrom;

Expand All @@ -23,6 +24,7 @@ use std::convert::TryFrom;
FromSqlRow,
Serialize,
Deserialize,
JsonSchema,
)]
#[diesel(sql_type = sql_types::BigInt)]
#[repr(transparent)]
Expand Down
84 changes: 74 additions & 10 deletions nexus/db-model/src/ip_pool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ use nexus_db_schema::schema::ip_pool_range;
use nexus_db_schema::schema::ip_pool_resource;
use nexus_types::external_api::params;
use nexus_types::external_api::shared;
use nexus_types::external_api::shared::IpRange;
use nexus_types::external_api::views;
use nexus_types::identity::Resource;
use omicron_common::api::external;
Expand Down Expand Up @@ -72,6 +71,24 @@ impl From<IpVersion> for shared::IpVersion {
}
}

impl From<shared::IpPoolType> for IpPoolType {
fn from(value: shared::IpPoolType) -> Self {
match value {
shared::IpPoolType::Unicast => Self::Unicast,
shared::IpPoolType::Multicast => Self::Multicast,
}
}
}

impl From<IpPoolType> for shared::IpPoolType {
fn from(value: IpPoolType) -> Self {
match value {
IpPoolType::Unicast => Self::Unicast,
IpPoolType::Multicast => Self::Multicast,
}
}
}

/// An IP Pool is a collection of IP addresses external to the rack.
///
/// IP pools can be external or internal. External IP pools can be associated
Expand All @@ -82,16 +99,17 @@ impl From<IpVersion> for shared::IpVersion {
pub struct IpPool {
#[diesel(embed)]
pub identity: IpPoolIdentity,

/// The IP version of the pool.
pub ip_version: IpVersion,

/// Pool type for unicast (default) vs multicast pools.
pub pool_type: IpPoolType,
/// Child resource generation number, for optimistic concurrency control of
/// the contained ranges.
pub rcgen: i64,
}

impl IpPool {
/// Creates a new unicast (default) IP pool.
pub fn new(
pool_identity: &external::IdentityMetadataCreateParams,
ip_version: IpVersion,
Expand All @@ -102,6 +120,23 @@ impl IpPool {
pool_identity.clone(),
),
ip_version,
pool_type: IpPoolType::Unicast,
rcgen: 0,
}
}

/// Creates a new multicast IP pool.
pub fn new_multicast(
pool_identity: &external::IdentityMetadataCreateParams,
ip_version: IpVersion,
) -> Self {
Self {
identity: IpPoolIdentity::new(
Uuid::new_v4(),
pool_identity.clone(),
),
ip_version,
pool_type: IpPoolType::Multicast,
rcgen: 0,
}
}
Expand All @@ -121,11 +156,21 @@ impl IpPool {

impl From<IpPool> for views::IpPool {
fn from(pool: IpPool) -> Self {
Self { identity: pool.identity(), ip_version: pool.ip_version.into() }
let identity = pool.identity();
let pool_type = pool.pool_type;

Self {
identity,
pool_type: pool_type.into(),
ip_version: pool.ip_version.into(),
}
}
}

/// A set of updates to an IP Pool
/// A set of updates to an IP Pool.
///
/// We do not modify the pool type after creation (e.g. unicast -> multicast or
/// vice versa), as that would require a migration of all associated resources.
#[derive(AsChangeset)]
#[diesel(table_name = ip_pool)]
pub struct IpPoolUpdate {
Expand Down Expand Up @@ -153,6 +198,25 @@ impl_enum_type!(
Silo => b"silo"
);

impl_enum_type!(
IpPoolTypeEnum:

#[derive(Clone, Copy, Debug, AsExpression, FromSqlRow, PartialEq)]
pub enum IpPoolType;

Unicast => b"unicast"
Multicast => b"multicast"
);

impl std::fmt::Display for IpPoolType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
IpPoolType::Unicast => write!(f, "unicast"),
IpPoolType::Multicast => write!(f, "multicast"),
}
}
}

#[derive(Queryable, Insertable, Selectable, Clone, Copy, Debug, PartialEq)]
#[diesel(table_name = ip_pool_resource)]
pub struct IpPoolResource {
Expand Down Expand Up @@ -192,7 +256,7 @@ pub struct IpPoolRange {
}

impl IpPoolRange {
pub fn new(range: &IpRange, ip_pool_id: Uuid) -> Self {
pub fn new(range: &shared::IpRange, ip_pool_id: Uuid) -> Self {
let now = Utc::now();
let first_address = range.first_address();
let last_address = range.last_address();
Expand Down Expand Up @@ -221,20 +285,20 @@ impl From<IpPoolRange> for views::IpPoolRange {
id: range.id,
ip_pool_id: range.ip_pool_id,
time_created: range.time_created,
range: IpRange::from(&range),
range: shared::IpRange::from(&range),
}
}
}

impl From<&IpPoolRange> for IpRange {
impl From<&IpPoolRange> for shared::IpRange {
fn from(range: &IpPoolRange) -> Self {
let maybe_range =
match (range.first_address.ip(), range.last_address.ip()) {
(IpAddr::V4(first), IpAddr::V4(last)) => {
IpRange::try_from((first, last))
shared::IpRange::try_from((first, last))
}
(IpAddr::V6(first), IpAddr::V6(last)) => {
IpRange::try_from((first, last))
shared::IpRange::try_from((first, last))
}
(first, last) => {
unreachable!(
Expand Down
3 changes: 2 additions & 1 deletion nexus/db-model/src/schema_versions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use std::{collections::BTreeMap, sync::LazyLock};
///
/// This must be updated when you change the database schema. Refer to
/// schema/crdb/README.adoc in the root of this repository for details.
pub const SCHEMA_VERSION: Version = Version::new(197, 0, 0);
pub const SCHEMA_VERSION: Version = Version::new(198, 0, 0);

/// List of all past database schema versions, in *reverse* order
///
Expand All @@ -28,6 +28,7 @@ static KNOWN_VERSIONS: LazyLock<Vec<KnownVersion>> = LazyLock::new(|| {
// | leaving the first copy as an example for the next person.
// v
// KnownVersion::new(next_int, "unique-dirname-with-the-sql-files"),
KnownVersion::new(198, "multicast-pool-support"),
KnownVersion::new(197, "scim-users-and-groups"),
KnownVersion::new(196, "user-provision-type-for-silo-user-and-group"),
KnownVersion::new(195, "tuf-pruned-index"),
Expand Down
41 changes: 10 additions & 31 deletions nexus/db-queries/src/db/datastore/external_ip.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
use super::DataStore;
use super::SQL_BATCH_SIZE;
use crate::authz;
use crate::authz::ApiResource;
use crate::context::OpContext;
use crate::db::collection_attach::AttachError;
use crate::db::collection_attach::DatastoreAttachTarget;
Expand All @@ -18,6 +17,7 @@ use crate::db::model::FloatingIp;
use crate::db::model::IncompleteExternalIp;
use crate::db::model::IpKind;
use crate::db::model::IpPool;
use crate::db::model::IpPoolType;
use crate::db::model::Name;
use crate::db::pagination::Paginator;
use crate::db::pagination::paginated;
Expand Down Expand Up @@ -87,7 +87,9 @@ impl DataStore {
probe_id: Uuid,
pool: Option<authz::IpPool>,
) -> CreateResult<ExternalIp> {
let authz_pool = self.resolve_pool_for_allocation(opctx, pool).await?;
let authz_pool = self
.resolve_pool_for_allocation(opctx, pool, IpPoolType::Unicast)
.await?;
let data = IncompleteExternalIp::for_ephemeral_probe(
ip_id,
probe_id,
Expand Down Expand Up @@ -123,7 +125,9 @@ impl DataStore {
// Naturally, we now *need* to destroy the ephemeral IP if the newly alloc'd
// IP was not attached, including on idempotent success.

let authz_pool = self.resolve_pool_for_allocation(opctx, pool).await?;
let authz_pool = self
.resolve_pool_for_allocation(opctx, pool, IpPoolType::Unicast)
.await?;
let data = IncompleteExternalIp::for_ephemeral(ip_id, authz_pool.id());

// We might not be able to acquire a new IP, but in the event of an
Expand Down Expand Up @@ -186,33 +190,6 @@ impl DataStore {
.map_err(|e| public_error_from_diesel(e, ErrorHandler::Server))
}

/// If a pool is specified, make sure it's linked to this silo. If a pool is
/// not specified, fetch the default pool for this silo. Once the pool is
/// resolved (by either method) do an auth check. Then return the pool.
async fn resolve_pool_for_allocation(
&self,
opctx: &OpContext,
pool: Option<authz::IpPool>,
) -> LookupResult<authz::IpPool> {
let authz_pool = match pool {
Some(authz_pool) => {
self.ip_pool_fetch_link(opctx, authz_pool.id())
.await
.map_err(|_| authz_pool.not_found())?;

authz_pool
}
// If no pool specified, use the default logic
None => {
let (authz_pool, ..) =
self.ip_pools_fetch_default(opctx).await?;
authz_pool
}
};
opctx.authorize(authz::Action::CreateChild, &authz_pool).await?;
Ok(authz_pool)
}

/// Allocates a floating IP address for instance usage.
pub async fn allocate_floating_ip(
&self,
Expand All @@ -224,7 +201,9 @@ impl DataStore {
) -> CreateResult<ExternalIp> {
let ip_id = Uuid::new_v4();

let authz_pool = self.resolve_pool_for_allocation(opctx, pool).await?;
let authz_pool = self
.resolve_pool_for_allocation(opctx, pool, IpPoolType::Unicast)
.await?;

let data = if let Some(ip) = ip {
IncompleteExternalIp::for_floating_explicit(
Expand Down
Loading
Loading