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

Add IP Pools and contained IP ranges #1253

Merged
merged 6 commits into from
Jun 24, 2022
Merged
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
99 changes: 98 additions & 1 deletion common/src/api/external/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,7 @@ pub enum ResourceType {
Disk,
Image,
Instance,
IpPool,
NetworkInterface,
Rack,
Service,
Expand Down Expand Up @@ -1123,6 +1124,51 @@ pub enum IpNet {
V6(Ipv6Net),
}

impl IpNet {
/// Return the first address in this subnet
pub fn first_address(&self) -> IpAddr {
match self {
IpNet::V4(inner) => IpAddr::from(inner.iter().next().unwrap()),
IpNet::V6(inner) => IpAddr::from(inner.iter().next().unwrap()),
}
}

/// Return the last address in this subnet.
///
/// For a subnet of size 1, e.g., a /32, this is the same as the first
/// address.
// NOTE: This is a workaround for the fact that the `ipnetwork` crate's
// iterator provides only the `Iterator::next()` method. That means that
// finding the last address is linear in the size of the subnet, which is
// completely untenable and totally avoidable with some addition. In the
// long term, we should either put up a patch to the `ipnetwork` crate or
// move the `ipnet` crate, which does provide an efficient iterator
// implementation.
pub fn last_address(&self) -> IpAddr {
match self {
IpNet::V4(inner) => {
let base: u32 = inner.network().into();
let size = inner.size() - 1;
std::net::IpAddr::V4(std::net::Ipv4Addr::from(base + size))
}
IpNet::V6(inner) => {
let base: u128 = inner.network().into();
let size = inner.size() - 1;
std::net::IpAddr::V6(std::net::Ipv6Addr::from(base + size))
}
}
}
}

impl From<ipnetwork::IpNetwork> for IpNet {
fn from(n: ipnetwork::IpNetwork) -> Self {
match n {
ipnetwork::IpNetwork::V4(v4) => IpNet::V4(Ipv4Net(v4)),
ipnetwork::IpNetwork::V6(v6) => IpNet::V6(Ipv6Net(v6)),
}
}
}

impl From<Ipv4Net> for IpNet {
fn from(n: Ipv4Net) -> IpNet {
IpNet::V4(n)
Expand Down Expand Up @@ -1234,7 +1280,10 @@ impl JsonSchema for IpNet {
/// Insert another level of schema indirection in order to provide an
/// additional title for a subschema. This allows generators to infer a better
/// variant name for an "untagged" enum.
fn label_schema(
// TODO-cleanup: We should move IpNet and this to
// `omicron_nexus::external_api::shared`. It's public now because `IpRange`,
// which is defined there, uses it.
pub fn label_schema(
label: &str,
schema: schemars::schema::Schema,
) -> schemars::schema::Schema {
Expand Down Expand Up @@ -2449,4 +2498,52 @@ mod test {
let net_des = serde_json::from_str::<IpNet>(&ser).unwrap();
assert_eq!(net, net_des);
}

#[test]
fn test_ipnet_first_last_address() {
use std::net::IpAddr;
use std::net::Ipv4Addr;
use std::net::Ipv6Addr;
let net: IpNet = "fd00::/128".parse().unwrap();
assert_eq!(
net.first_address(),
IpAddr::from(Ipv6Addr::new(0xfd00, 0, 0, 0, 0, 0, 0, 0)),
);
assert_eq!(
net.last_address(),
IpAddr::from(Ipv6Addr::new(0xfd00, 0, 0, 0, 0, 0, 0, 0)),
);

let net: IpNet = "fd00::/64".parse().unwrap();
assert_eq!(
net.first_address(),
IpAddr::from(Ipv6Addr::new(0xfd00, 0, 0, 0, 0, 0, 0, 0)),
);
assert_eq!(
net.last_address(),
IpAddr::from(Ipv6Addr::new(
0xfd00, 0, 0, 0, 0xffff, 0xffff, 0xffff, 0xffff
)),
);

let net: IpNet = "10.0.0.0/16".parse().unwrap();
assert_eq!(
net.first_address(),
IpAddr::from(Ipv4Addr::new(10, 0, 0, 0)),
);
assert_eq!(
net.last_address(),
IpAddr::from(Ipv4Addr::new(10, 0, 255, 255)),
);

let net: IpNet = "10.0.0.0/32".parse().unwrap();
assert_eq!(
net.first_address(),
IpAddr::from(Ipv4Addr::new(10, 0, 0, 0)),
);
assert_eq!(
net.last_address(),
IpAddr::from(Ipv4Addr::new(10, 0, 0, 0)),
);
}
bnaecker marked this conversation as resolved.
Show resolved Hide resolved
}
107 changes: 81 additions & 26 deletions common/src/sql/dbinit.sql
Original file line number Diff line number Diff line change
Expand Up @@ -827,32 +827,6 @@ STORING (vpc_id, subnet_id, is_primary)
WHERE
time_deleted IS NULL;


CREATE TYPE omicron.public.vpc_router_kind AS ENUM (
'system',
'custom'
);

CREATE TABLE omicron.public.vpc_router (
/* Identity metadata (resource) */
id UUID PRIMARY KEY,
name STRING(63) NOT NULL,
description STRING(512) NOT NULL,
time_created TIMESTAMPTZ NOT NULL,
time_modified TIMESTAMPTZ NOT NULL,
/* Indicates that the object has been deleted */
time_deleted TIMESTAMPTZ,
kind omicron.public.vpc_router_kind NOT NULL,
vpc_id UUID NOT NULL,
rcgen INT NOT NULL
);

CREATE UNIQUE INDEX ON omicron.public.vpc_router (
vpc_id,
name
) WHERE
time_deleted IS NULL;

CREATE TYPE omicron.public.vpc_firewall_rule_status AS ENUM (
'disabled',
'enabled'
Expand Down Expand Up @@ -904,6 +878,31 @@ CREATE UNIQUE INDEX ON omicron.public.vpc_firewall_rule (
) WHERE
time_deleted IS NULL;

CREATE TYPE omicron.public.vpc_router_kind AS ENUM (
'system',
'custom'
);

CREATE TABLE omicron.public.vpc_router (
/* Identity metadata (resource) */
id UUID PRIMARY KEY,
name STRING(63) NOT NULL,
description STRING(512) NOT NULL,
time_created TIMESTAMPTZ NOT NULL,
time_modified TIMESTAMPTZ NOT NULL,
/* Indicates that the object has been deleted */
time_deleted TIMESTAMPTZ,
kind omicron.public.vpc_router_kind NOT NULL,
vpc_id UUID NOT NULL,
rcgen INT NOT NULL
);

CREATE UNIQUE INDEX ON omicron.public.vpc_router (
vpc_id,
name
) WHERE
time_deleted IS NULL;

CREATE TYPE omicron.public.router_route_kind AS ENUM (
'default',
'vpc_subnet',
Expand Down Expand Up @@ -933,6 +932,62 @@ CREATE UNIQUE INDEX ON omicron.public.router_route (
) WHERE
time_deleted IS NULL;

/*
* An IP Pool, a collection of zero or more IP ranges for external IPs.
*/
CREATE TABLE omicron.public.ip_pool (
/* Resource identity metadata */
id UUID PRIMARY KEY,
name STRING(63) NOT NULL,
description STRING(512) NOT NULL,
time_created TIMESTAMPTZ NOT NULL,
time_modified TIMESTAMPTZ NOT NULL,
time_deleted TIMESTAMPTZ,

/* The collection's child-resource generation number */
rcgen INT8 NOT NULL
);

/*
* Index ensuring uniqueness of IP Pool names, globally.
*/
CREATE UNIQUE INDEX ON omicron.public.ip_pool (
name
) WHERE
time_deleted IS NULL;

/*
* IP Pools are made up of a set of IP ranges, which are start/stop addresses.
* Note that these need not be CIDR blocks or well-behaved subnets with a
* specific netmask.
*/
CREATE TABLE omicron.public.ip_pool_range (
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When it comes time to associate external IPs with instances (or services, like Nexus), do we expect to just use this table as-is, or create a new representation of "assigned" IP addresses?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not entirely sure what that'll look like, but I've had some thoughts. TL;DR: I'm expecting to add the used IPs into things like the network_interface table.

So the IP Pools that we've implemented here are a bit of a conflation of two things: IP Pools as defined by RFD 21 and "address sets" defined by RFD 267. The former are addresses used for instance NAT. The latter are more general -- they are a superset of IP Pools, but also used for things like services (e.g, Nexus, DNS) and customer network integration (such as prefixes advertised in some routing protocol like BGP). The work here unfortunately merges these, because we need to get something and it's not entirely clear what's required for address sets yet.

That said, I'd expect that the address set table(s) will track what each set is for, and which will direct us to another table, say the ip_pool table, for finding the ranges within that set. Specifically for IP Pools in the sense of instance NAT, I do expect that we'll update the network_interface table (or add a new table) to include the IPs from this range that are currently in use. It all gets pretty hairy here in terms of the database operations, since now there are at least 3 tables to consider for certain operations, such as deleting an IP Pool resource.

id UUID PRIMARY KEY,
time_created TIMESTAMPTZ NOT NULL,
time_modified TIMESTAMPTZ NOT NULL,
time_deleted TIMESTAMPTZ,
first_address INET NOT NULL,
/* The range is inclusive of the last address. */
last_address INET NOT NULL,
bnaecker marked this conversation as resolved.
Show resolved Hide resolved
ip_pool_id UUID NOT NULL
);

/*
* These help Nexus enforce that the ranges within an IP Pool do not overlap
* with any other ranges. See `nexus/src/db/queries/ip_pool.rs` for the actual
* query which does that.
*/
CREATE UNIQUE INDEX ON omicron.public.ip_pool_range (
first_address
)
STORING (last_address)
WHERE time_deleted IS NULL;
CREATE UNIQUE INDEX ON omicron.public.ip_pool_range (
last_address
)
STORING (first_address)
WHERE time_deleted IS NULL;

/*******************************************************************/

/*
Expand Down
128 changes: 128 additions & 0 deletions nexus/src/app/ip_pool.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// 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/.

//! IP Pools, collections of external IP addresses for guest instances

use crate::authz;
use crate::context::OpContext;
use crate::db;
use crate::db::lookup::LookupPath;
use crate::db::model::Name;
use crate::external_api::params;
use crate::external_api::shared::IpRange;
use ipnetwork::IpNetwork;
use omicron_common::api::external::CreateResult;
use omicron_common::api::external::DataPageParams;
use omicron_common::api::external::DeleteResult;
use omicron_common::api::external::ListResultVec;
use omicron_common::api::external::LookupResult;
use omicron_common::api::external::UpdateResult;
use uuid::Uuid;

impl super::Nexus {
pub async fn ip_pool_create(
&self,
opctx: &OpContext,
new_pool: &params::IpPoolCreate,
) -> CreateResult<db::model::IpPool> {
self.db_datastore.ip_pool_create(opctx, new_pool).await
}

pub async fn ip_pools_list_by_name(
&self,
opctx: &OpContext,
pagparams: &DataPageParams<'_, Name>,
) -> ListResultVec<db::model::IpPool> {
self.db_datastore.ip_pools_list_by_name(opctx, pagparams).await
}

pub async fn ip_pools_list_by_id(
&self,
opctx: &OpContext,
pagparams: &DataPageParams<'_, Uuid>,
) -> ListResultVec<db::model::IpPool> {
self.db_datastore.ip_pools_list_by_id(opctx, pagparams).await
}

pub async fn ip_pool_fetch(
&self,
opctx: &OpContext,
pool_name: &Name,
) -> LookupResult<db::model::IpPool> {
let (.., db_pool) = LookupPath::new(opctx, &self.db_datastore)
.ip_pool_name(pool_name)
.fetch()
.await?;
Ok(db_pool)
}

pub async fn ip_pool_delete(
&self,
opctx: &OpContext,
pool_name: &Name,
) -> DeleteResult {
let (.., authz_pool, db_pool) =
LookupPath::new(opctx, &self.db_datastore)
.ip_pool_name(pool_name)
.fetch_for(authz::Action::Delete)
.await?;
self.db_datastore.ip_pool_delete(opctx, &authz_pool, &db_pool).await
}

pub async fn ip_pool_update(
&self,
opctx: &OpContext,
pool_name: &Name,
updates: &params::IpPoolUpdate,
) -> UpdateResult<db::model::IpPool> {
let (.., authz_pool) = LookupPath::new(opctx, &self.db_datastore)
.ip_pool_name(pool_name)
.lookup_for(authz::Action::Modify)
.await?;
self.db_datastore
.ip_pool_update(opctx, &authz_pool, updates.clone().into())
.await
}

pub async fn ip_pool_list_ranges(
&self,
opctx: &OpContext,
pool_name: &Name,
pagparams: &DataPageParams<'_, IpNetwork>,
) -> ListResultVec<db::model::IpPoolRange> {
let (.., authz_pool) = LookupPath::new(opctx, &self.db_datastore)
.ip_pool_name(pool_name)
.lookup_for(authz::Action::ListChildren)
.await?;
self.db_datastore
.ip_pool_list_ranges(opctx, &authz_pool, pagparams)
.await
}

pub async fn ip_pool_add_range(
&self,
opctx: &OpContext,
pool_name: &Name,
range: &IpRange,
) -> UpdateResult<db::model::IpPoolRange> {
let (.., authz_pool) = LookupPath::new(opctx, &self.db_datastore)
.ip_pool_name(pool_name)
.lookup_for(authz::Action::Modify)
.await?;
self.db_datastore.ip_pool_add_range(opctx, &authz_pool, range).await
}

pub async fn ip_pool_delete_range(
&self,
opctx: &OpContext,
pool_name: &Name,
range: &IpRange,
) -> DeleteResult {
let (.., authz_pool) = LookupPath::new(opctx, &self.db_datastore)
.ip_pool_name(pool_name)
.lookup_for(authz::Action::Modify)
.await?;
self.db_datastore.ip_pool_delete_range(opctx, &authz_pool, range).await
}
}
1 change: 1 addition & 0 deletions nexus/src/app/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ mod disk;
mod iam;
mod image;
mod instance;
mod ip_pool;
mod organization;
mod oximeter;
mod project;
Expand Down
Loading