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 1 commit
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
204 changes: 204 additions & 0 deletions 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 @@ -1258,6 +1304,116 @@ fn label_schema(
.into()
}

/// An IP Range is a contiguous range of IP addresses, usually within an IP
/// Pool.
#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
pub enum IpRange {
bnaecker marked this conversation as resolved.
Show resolved Hide resolved
V4(Ipv4Range),
V6(Ipv6Range),
}

impl JsonSchema for IpRange {
bnaecker marked this conversation as resolved.
Show resolved Hide resolved
fn schema_name() -> String {
"IpRange".to_string()
}

fn json_schema(
gen: &mut schemars::gen::SchemaGenerator,
) -> schemars::schema::Schema {
schemars::schema::SchemaObject {
metadata: Some(
schemars::schema::Metadata { ..Default::default() }.into(),
),
subschemas: Some(
schemars::schema::SubschemaValidation {
one_of: Some(vec![
label_schema("v4", gen.subschema_for::<Ipv4Range>()),
label_schema("v6", gen.subschema_for::<Ipv6Range>()),
]),
..Default::default()
}
.into(),
),
..Default::default()
}
.into()
}
}

impl IpRange {
pub fn first_address(&self) -> IpAddr {
match self {
IpRange::V4(inner) => IpAddr::from(inner.first),
IpRange::V6(inner) => IpAddr::from(inner.first),
}
}

pub fn last_address(&self) -> IpAddr {
match self {
IpRange::V4(inner) => IpAddr::from(inner.last),
IpRange::V6(inner) => IpAddr::from(inner.last),
}
}
}

/// A non-decreasing IPv4 address range, inclusive of both ends.
///
/// The first address must be less than or equal to the last address.
#[derive(Clone, Copy, Debug, Deserialize, Serialize, JsonSchema)]
#[serde(try_from = "AnyIpv4Range")]
pub struct Ipv4Range {
pub first: Ipv4Addr,
pub last: Ipv4Addr,
}

#[derive(Clone, Copy, Debug, Deserialize)]
struct AnyIpv4Range {
first: Ipv4Addr,
last: Ipv4Addr,
}
bnaecker marked this conversation as resolved.
Show resolved Hide resolved

impl TryFrom<AnyIpv4Range> for Ipv4Range {
type Error = Error;
fn try_from(r: AnyIpv4Range) -> Result<Self, Self::Error> {
if r.first <= r.last {
Ok(Self { first: r.first, last: r.last })
} else {
Err(Error::invalid_request(
"IP address ranges must be non-decreasing",
))
}
}
}

/// A non-decreasing IPv6 address range, inclusive of both ends.
///
/// The first address must be less than or equal to the last address.
#[derive(Clone, Copy, Debug, Deserialize, Serialize, JsonSchema)]
#[serde(try_from = "AnyIpv6Range")]
pub struct Ipv6Range {
pub first: Ipv6Addr,
bnaecker marked this conversation as resolved.
Show resolved Hide resolved
pub last: Ipv6Addr,
}

#[derive(Clone, Copy, Debug, Deserialize)]
struct AnyIpv6Range {
first: Ipv6Addr,
last: Ipv6Addr,
}

impl TryFrom<AnyIpv6Range> for Ipv6Range {
type Error = Error;
fn try_from(r: AnyIpv6Range) -> Result<Self, Self::Error> {
if r.first <= r.last {
Ok(Self { first: r.first, last: r.last })
} else {
Err(Error::invalid_request(
"IP address ranges must be non-decreasing",
))
}
}
}

/// A `RouteTarget` describes the possible locations that traffic matching a
/// route destination can be sent.
#[derive(
Expand Down Expand Up @@ -2449,4 +2605,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
}
113 changes: 87 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,68 @@ 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

/*
* TODO-completeness: These will need some enum describing what the pool can
bnaecker marked this conversation as resolved.
Show resolved Hide resolved
* be used for. That can currently be:
* - Routing
* - Oxide public use, e.g., Nexus's public API or the console
* - Virtual machine instance NAT
*/
);

/*
* 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,
last_address INET NOT NULL,
bnaecker marked this conversation as resolved.
Show resolved Hide resolved
ip_pool_id UUID NOT NULL
);

/*
* These indexes are currently used to enforce that the ranges within an IP Pool
* do not overlap with any other ranges.
bnaecker marked this conversation as resolved.
Show resolved Hide resolved
*/
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
Loading