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

(Multiple) Floating IP Support #4559

Merged
merged 32 commits into from
Dec 6, 2023
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
9bb158c
Base compatability with new OPTE ioctl API
FelixMcFelix Nov 21, 2023
a1aa492
Initial implementation of add/list floating IP endpoint
FelixMcFelix Nov 22, 2023
07e0187
Add incremental schema updates
FelixMcFelix Nov 22, 2023
b430952
`cargo fmt`, some implementation post-it notes
FelixMcFelix Nov 22, 2023
4db9048
Fix schema migration and definitions
FelixMcFelix Nov 22, 2023
ba0f7a3
Working addition/listing of floating IPs, correct DB constraints
FelixMcFelix Nov 23, 2023
afb7f93
Sled-agent + sled-agent-client plumbing of extIPs
FelixMcFelix Nov 23, 2023
73d9f0d
Better view type for floating IPs
FelixMcFelix Nov 23, 2023
c826bb3
Add FloatingIp view, resource handling + authz
FelixMcFelix Nov 24, 2023
dd37e41
Create/Delete-time Floating IP management
FelixMcFelix Nov 24, 2023
649cce5
Appeasement for Clippy
FelixMcFelix Nov 24, 2023
57620d8
Cargo hakari + OPTE false version
FelixMcFelix Nov 24, 2023
0cdbc1c
Apply missed schema version bump
FelixMcFelix Nov 25, 2023
520e589
Fix External IP dup-name error checking, constraints test
FelixMcFelix Nov 27, 2023
037f7b6
Fixup iam_roles_test with FloatingIp Resource
FelixMcFelix Nov 27, 2023
9edb4ea
Fix up authz integration tests, add ephemeral limit
FelixMcFelix Nov 27, 2023
d95a288
Merge branch 'main' into felixmcfelix/floating-ip
FelixMcFelix Nov 27, 2023
4b6029e
Move new changes to Schema v15.0
FelixMcFelix Nov 27, 2023
2b76bd5
Merge branch 'main' into felixmcfelix/floating-ip
FelixMcFelix Nov 28, 2023
e6cc0fb
Add unit tests for Floating API endpoint behaviour
FelixMcFelix Nov 28, 2023
e037e3e
Add multi-external-IP test for instances
FelixMcFelix Nov 28, 2023
5a5a02a
Re-read complete changelog, tidying up.
FelixMcFelix Nov 28, 2023
649eba7
Accidentally a cargo fmt
FelixMcFelix Nov 28, 2023
b29188e
Correct doc comment for openapi test.
FelixMcFelix Nov 28, 2023
d41e1fb
Merge branch 'main' into felixmcfelix/floating-ip
FelixMcFelix Dec 1, 2023
9680ac8
Use published OPTE.
FelixMcFelix Dec 1, 2023
7fb49b8
Missed a merge conflict.
FelixMcFelix Dec 1, 2023
43ef99d
Fix: detach floating IPs during create saga undo
FelixMcFelix Dec 4, 2023
52b9157
Merge branch 'main' into felixmcfelix/floating-ip
FelixMcFelix Dec 5, 2023
deaf98b
Merge branch 'main' into felixmcfelix/floating-ip
FelixMcFelix Dec 6, 2023
49bd634
Minor fmt.
FelixMcFelix Dec 6, 2023
112e9f2
Review feedback: comment expansion.
FelixMcFelix Dec 6, 2023
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
220 changes: 137 additions & 83 deletions Cargo.lock

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -260,15 +260,15 @@ omicron-sled-agent = { path = "sled-agent" }
omicron-test-utils = { path = "test-utils" }
omicron-zone-package = "0.9.1"
oxide-client = { path = "clients/oxide-client" }
oxide-vpc = { git = "https://github.com/oxidecomputer/opte", rev = "258a8b59902dd36fc7ee5425e6b1fb5fc80d4649", features = [ "api", "std" ] }
oxide-vpc = { git = "https://github.com/oxidecomputer/opte", rev = "d508dcd12cbfeb8b948c0cffe800899046322233", features = [ "api", "std" ] }
once_cell = "1.18.0"
openapi-lint = { git = "https://github.com/oxidecomputer/openapi-lint", branch = "main" }
openapiv3 = "1.0"
# must match samael's crate!
openssl = "0.10"
openssl-sys = "0.9"
openssl-probe = "0.1.5"
opte-ioctl = { git = "https://github.com/oxidecomputer/opte", rev = "258a8b59902dd36fc7ee5425e6b1fb5fc80d4649" }
opte-ioctl = { git = "https://github.com/oxidecomputer/opte", rev = "d508dcd12cbfeb8b948c0cffe800899046322233" }
oso = "0.27"
owo-colors = "3.5.0"
oximeter = { path = "oximeter/oximeter" }
Expand Down
1 change: 1 addition & 0 deletions common/src/api/external/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -751,6 +751,7 @@ pub enum ResourceType {
Zpool,
Vmm,
Ipv4NatEntry,
FloatingIp,
}

// IDENTITY METADATA
Expand Down
41 changes: 27 additions & 14 deletions illumos-utils/src/opte/port_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ use omicron_common::api::internal::shared::NetworkInterfaceKind;
use omicron_common::api::internal::shared::SourceNatConfig;
use oxide_vpc::api::AddRouterEntryReq;
use oxide_vpc::api::DhcpCfg;
use oxide_vpc::api::ExternalIpCfg;
use oxide_vpc::api::IpCfg;
use oxide_vpc::api::IpCidr;
use oxide_vpc::api::Ipv4Cfg;
Expand Down Expand Up @@ -99,7 +100,8 @@ impl PortManager {
&self,
nic: &NetworkInterface,
source_nat: Option<SourceNatConfig>,
external_ips: &[IpAddr],
ephemeral_ip: Option<IpAddr>,
floating_ips: &[IpAddr],
firewall_rules: &[VpcFirewallRule],
dhcp_config: DhcpCfg,
) -> Result<(Port, PortTicket), Error> {
Expand All @@ -111,13 +113,6 @@ impl PortManager {
let boundary_services = default_boundary_services();

// Describe the external IP addresses for this port.
//
// Note that we're currently only taking the first address, which is all
// that OPTE supports. The array is guaranteed to be limited by Nexus.
// See https://github.com/oxidecomputer/omicron/issues/1467
// See https://github.com/oxidecomputer/opte/issues/196
let external_ip = external_ips.get(0);

macro_rules! ip_cfg {
($ip:expr, $log_prefix:literal, $ip_t:path, $cidr_t:path,
$ipcfg_e:path, $ipcfg_t:ident, $snat_t:ident) => {{
Expand Down Expand Up @@ -152,25 +147,43 @@ impl PortManager {
}
None => None,
};
let external_ip = match external_ip {
Some($ip_t(ip)) => Some((*ip).into()),
let ephemeral_ip = match ephemeral_ip {
Some($ip_t(ip)) => Some(ip.into()),
Some(_) => {
error!(
self.inner.log,
concat!($log_prefix, " external IP");
"external_ip" => ?external_ip,
concat!($log_prefix, " ephemeral IP");
"ephemeral_ip" => ?ephemeral_ip,
);
return Err(Error::InvalidPortIpConfig);
}
None => None,
};
let floating_ips: Vec<_> = floating_ips
.iter()
.copied()
.map(|ip| match ip {
$ip_t(ip) => Ok(ip.into()),
_ => {
error!(
self.inner.log,
concat!($log_prefix, " ephemeral IP");
"ephemeral_ip" => ?ephemeral_ip,
);
Err(Error::InvalidPortIpConfig)
}
})
.collect::<Result<Vec<_>, _>>()?;

$ipcfg_e($ipcfg_t {
vpc_subnet,
private_ip: $ip.into(),
gateway_ip: gateway_ip.into(),
snat,
external_ips: external_ip,
external_ips: ExternalIpCfg {
ephemeral_ip,
snat,
floating_ips,
},
})
}}
}
Expand Down
140 changes: 139 additions & 1 deletion nexus/db-model/src/external_ip.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,21 @@
//! services.

use crate::impl_enum_type;
use crate::schema::external_ip;
use crate::schema::{external_ip, floating_ip};
use crate::Name;
use crate::SqlU16;
use chrono::DateTime;
use chrono::Utc;
use db_macros::Resource;
use diesel::Queryable;
use diesel::Selectable;
use ipnetwork::IpNetwork;
use nexus_types::external_api::shared;
use nexus_types::external_api::views;
use omicron_common::address::NUM_SOURCE_NAT_PORTS;
use omicron_common::api::external::Error;
use omicron_common::api::external::IdentityMetadata;
use serde::{Deserialize, Serialize};
use std::convert::TryFrom;
use std::net::IpAddr;
use uuid::Uuid;
Expand Down Expand Up @@ -69,6 +72,30 @@ pub struct ExternalIp {
pub ip: IpNetwork,
pub first_port: SqlU16,
pub last_port: SqlU16,
// Only Some(_) for instance Floating IPs
pub project_id: Option<Uuid>,
}

/// A view type constructed from `ExternalIp` used to represent Floating IP
/// objects in user-facing APIs.
///
/// This View type fills a similar niche to `ProjectImage` etc.: we need to
/// represent identity as non-nullable (ditto for parent project) so as to
/// play nicely with authz and resource APIs.
#[derive(
Queryable, Selectable, Clone, Debug, Resource, Serialize, Deserialize,
)]
#[diesel(table_name = floating_ip)]
pub struct FloatingIp {
#[diesel(embed)]
pub identity: FloatingIpIdentity,

pub ip_pool_id: Uuid,
pub ip_pool_range_id: Uuid,
pub is_service: bool,
pub parent_id: Option<Uuid>,
pub ip: IpNetwork,
pub project_id: Uuid,
}

impl From<ExternalIp> for sled_agent_client::types::SourceNatConfig {
Expand All @@ -93,6 +120,7 @@ pub struct IncompleteExternalIp {
is_service: bool,
parent_id: Option<Uuid>,
pool_id: Uuid,
project_id: Option<Uuid>,
// Optional address requesting that a specific IP address be allocated.
explicit_ip: Option<IpNetwork>,
// Optional range when requesting a specific SNAT range be allocated.
Expand All @@ -114,6 +142,7 @@ impl IncompleteExternalIp {
is_service: false,
parent_id: Some(instance_id),
pool_id,
project_id: None,
explicit_ip: None,
explicit_port_range: None,
}
Expand All @@ -129,6 +158,7 @@ impl IncompleteExternalIp {
is_service: false,
parent_id: Some(instance_id),
pool_id,
project_id: None,
explicit_ip: None,
explicit_port_range: None,
}
Expand All @@ -138,6 +168,7 @@ impl IncompleteExternalIp {
id: Uuid,
name: &Name,
description: &str,
project_id: Uuid,
pool_id: Uuid,
) -> Self {
Self {
Expand All @@ -149,11 +180,35 @@ impl IncompleteExternalIp {
is_service: false,
parent_id: None,
pool_id,
project_id: Some(project_id),
explicit_ip: None,
explicit_port_range: None,
}
}

pub fn for_floating_explicit(
id: Uuid,
name: &Name,
description: &str,
project_id: Uuid,
explicit_ip: IpAddr,
pool_id: Uuid,
) -> Self {
Self {
id,
name: Some(name.clone()),
description: Some(description.to_string()),
time_created: Utc::now(),
kind: IpKind::Floating,
is_service: false,
parent_id: None,
pool_id,
project_id: Some(project_id),
explicit_ip: Some(explicit_ip.into()),
explicit_port_range: None,
}
}

pub fn for_service_explicit(
id: Uuid,
name: &Name,
Expand All @@ -171,6 +226,7 @@ impl IncompleteExternalIp {
is_service: true,
parent_id: Some(service_id),
pool_id,
project_id: None,
explicit_ip: Some(IpNetwork::from(address)),
explicit_port_range: None,
}
Expand Down Expand Up @@ -199,6 +255,7 @@ impl IncompleteExternalIp {
is_service: true,
parent_id: Some(service_id),
pool_id,
project_id: None,
explicit_ip: Some(IpNetwork::from(address)),
explicit_port_range,
}
Expand All @@ -220,6 +277,7 @@ impl IncompleteExternalIp {
is_service: true,
parent_id: Some(service_id),
pool_id,
project_id: None,
explicit_ip: None,
explicit_port_range: None,
}
Expand All @@ -235,6 +293,7 @@ impl IncompleteExternalIp {
is_service: true,
parent_id: Some(service_id),
pool_id,
project_id: None,
explicit_ip: None,
explicit_port_range: None,
}
Expand Down Expand Up @@ -272,6 +331,10 @@ impl IncompleteExternalIp {
&self.pool_id
}

pub fn project_id(&self) -> &Option<Uuid> {
&self.project_id
}

pub fn explicit_ip(&self) -> &Option<IpNetwork> {
&self.explicit_ip
}
Expand Down Expand Up @@ -308,3 +371,78 @@ impl TryFrom<ExternalIp> for views::ExternalIp {
Ok(views::ExternalIp { kind, ip: ip.ip.ip() })
}
}

impl TryFrom<ExternalIp> for FloatingIp {
type Error = Error;

fn try_from(ip: ExternalIp) -> Result<Self, Self::Error> {
if ip.kind != IpKind::Floating {
return Err(Error::internal_error(
"attempted to convert non-floating external IP to floating",
));
}
if ip.is_service {
return Err(Error::internal_error(
"Service IPs should not be exposed in the API",
));
}

let project_id = ip.project_id.ok_or(Error::internal_error(
"database schema guarantees parent project for non-service FIP",
))?;

let name = ip.name.ok_or(Error::internal_error(
"database schema guarantees ID metadata for non-service FIP",
))?;

let description = ip.description.ok_or(Error::internal_error(
"database schema guarantees ID metadata for non-service FIP",
))?;

let identity = FloatingIpIdentity {
id: ip.id,
name,
description,
time_created: ip.time_created,
time_modified: ip.time_modified,
time_deleted: ip.time_deleted,
};

Ok(FloatingIp {
ip: ip.ip,
identity,
project_id,
ip_pool_id: ip.ip_pool_id,
ip_pool_range_id: ip.ip_pool_range_id,
is_service: ip.is_service,
parent_id: ip.parent_id,
})
}
}

impl TryFrom<ExternalIp> for views::FloatingIp {
type Error = Error;

fn try_from(ip: ExternalIp) -> Result<Self, Self::Error> {
FloatingIp::try_from(ip).map(Into::into)
}
}

impl From<FloatingIp> for views::FloatingIp {
fn from(ip: FloatingIp) -> Self {
let identity = IdentityMetadata {
id: ip.identity.id,
name: ip.identity.name.into(),
description: ip.identity.description,
time_created: ip.identity.time_created,
time_modified: ip.identity.time_modified,
};

views::FloatingIp {
ip: ip.ip.ip(),
identity,
project_id: ip.project_id,
instance_id: ip.parent_id,
}
}
}
23 changes: 22 additions & 1 deletion nexus/db-model/src/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,7 @@ table! {
time_created -> Timestamptz,
time_modified -> Timestamptz,
time_deleted -> Nullable<Timestamptz>,

ip_pool_id -> Uuid,
ip_pool_range_id -> Uuid,
is_service -> Bool,
Expand All @@ -532,6 +533,26 @@ table! {
ip -> Inet,
first_port -> Int4,
last_port -> Int4,

project_id -> Nullable<Uuid>,
}
}

table! {
floating_ip (id) {
id -> Uuid,
name -> Text,
description -> Text,
time_created -> Timestamptz,
time_modified -> Timestamptz,
time_deleted -> Nullable<Timestamptz>,

ip_pool_id -> Uuid,
ip_pool_range_id -> Uuid,
is_service -> Bool,
parent_id -> Nullable<Uuid>,
ip -> Inet,
project_id -> Uuid,
}
}

Expand Down Expand Up @@ -1299,7 +1320,7 @@ table! {
///
/// This should be updated whenever the schema is changed. For more details,
/// refer to: schema/crdb/README.adoc
pub const SCHEMA_VERSION: SemverVersion = SemverVersion::new(14, 0, 0);
pub const SCHEMA_VERSION: SemverVersion = SemverVersion::new(15, 0, 0);

allow_tables_to_appear_in_same_query!(
system_update,
Expand Down
Loading
Loading