Skip to content

Commit 9c326f5

Browse files
committed
Add a unique VNI to each VPC
1 parent fb129b0 commit 9c326f5

File tree

14 files changed

+206
-118
lines changed

14 files changed

+206
-118
lines changed

Cargo.lock

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

common/src/api/external/mod.rs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1677,6 +1677,62 @@ impl JsonSchema for MacAddr {
16771677
}
16781678
}
16791679

1680+
/// A Geneve Virtual Network Identifier
1681+
#[derive(
1682+
Debug,
1683+
Clone,
1684+
Copy,
1685+
PartialEq,
1686+
Eq,
1687+
Hash,
1688+
PartialOrd,
1689+
Ord,
1690+
Deserialize,
1691+
Serialize,
1692+
JsonSchema,
1693+
)]
1694+
pub struct Vni(u32);
1695+
1696+
impl Vni {
1697+
const MAX_VNI: u32 = 1 << 24;
1698+
1699+
/// Create a new random VNI.
1700+
pub fn random() -> Self {
1701+
use rand::Rng;
1702+
Self(rand::thread_rng().gen_range(0..=Self::MAX_VNI))
1703+
}
1704+
}
1705+
1706+
impl From<Vni> for u32 {
1707+
fn from(vni: Vni) -> u32 {
1708+
vni.0
1709+
}
1710+
}
1711+
1712+
impl TryFrom<u32> for Vni {
1713+
type Error = Error;
1714+
1715+
fn try_from(x: u32) -> Result<Self, Error> {
1716+
if x <= Self::MAX_VNI {
1717+
Ok(Self(x))
1718+
} else {
1719+
Err(Error::internal_error(
1720+
format!("Invalid Geneve VNI: {}", x).as_str(),
1721+
))
1722+
}
1723+
}
1724+
}
1725+
1726+
impl TryFrom<i32> for Vni {
1727+
type Error = Error;
1728+
1729+
fn try_from(x: i32) -> Result<Self, Error> {
1730+
Self::try_from(u32::try_from(x).map_err(|_| {
1731+
Error::internal_error(format!("Invalid Geneve VNI: {}", x).as_str())
1732+
})?)
1733+
}
1734+
}
1735+
16801736
/// A `NetworkInterface` represents a virtual network interface device.
16811737
#[derive(ObjectIdentity, Clone, Debug, Deserialize, JsonSchema, Serialize)]
16821738
pub struct NetworkInterface {

common/src/sql/dbinit.sql

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -585,6 +585,13 @@ CREATE TABLE omicron.public.vpc (
585585
system_router_id UUID NOT NULL,
586586
dns_name STRING(63) NOT NULL,
587587

588+
/*
589+
* The Geneve Virtual Network Identifier for this VPC. Note that this is a
590+
* 24-bit unsigned value, properties which are checked in the application,
591+
* not the database.
592+
*/
593+
vni INT4 NOT NULL,
594+
588595
/* The IPv6 prefix allocated to subnets. */
589596
ipv6_prefix INET NOT NULL,
590597

@@ -599,6 +606,11 @@ CREATE UNIQUE INDEX ON omicron.public.vpc (
599606
) WHERE
600607
time_deleted IS NULL;
601608

609+
CREATE UNIQUE INDEX ON omicron.public.vpc (
610+
vni
611+
) WHERE
612+
time_deleted IS NULL;
613+
602614
CREATE TABLE omicron.public.vpc_subnet (
603615
/* Identity metadata (resource) */
604616
id UUID PRIMARY KEY,

nexus/src/db/datastore.rs

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1371,7 +1371,7 @@ impl DataStore {
13711371
}
13721372

13731373
/// Return the information about an instance's network interfaces required
1374-
/// for the sled agent to instantiate it via OPTE.
1374+
/// for the sled agent to instantiate them via OPTE.
13751375
///
13761376
/// OPTE requires information that's currently split across the network
13771377
/// interface and VPC subnet tables. This query just joins those for each
@@ -1384,6 +1384,7 @@ impl DataStore {
13841384
opctx.authorize(authz::Action::ListChildren, authz_instance).await?;
13851385

13861386
use db::schema::network_interface;
1387+
use db::schema::vpc;
13871388
use db::schema::vpc_subnet;
13881389

13891390
// The record type for the results of the below JOIN query
@@ -1394,7 +1395,7 @@ impl DataStore {
13941395
mac: db::model::MacAddr,
13951396
ipv4_block: db::model::Ipv4Net,
13961397
ipv6_block: db::model::Ipv6Net,
1397-
vpc_id: Uuid,
1398+
vni: db::model::Vni,
13981399
slot: i16,
13991400
}
14001401

@@ -1410,7 +1411,7 @@ impl DataStore {
14101411
ip: nic.ip.ip().to_string(),
14111412
mac: sled_client_types::MacAddr::from(nic.mac.0),
14121413
subnet: sled_client_types::IpNet::from(ip_subnet),
1413-
vpc_id: nic.vpc_id,
1414+
vni: sled_client_types::Vni::from(nic.vni.0),
14141415
slot: u8::try_from(nic.slot).unwrap(),
14151416
}
14161417
}
@@ -1423,24 +1424,26 @@ impl DataStore {
14231424
vpc_subnet::table
14241425
.on(network_interface::subnet_id.eq(vpc_subnet::id)),
14251426
)
1427+
.inner_join(vpc::table.on(vpc_subnet::vpc_id.eq(vpc::id)))
14261428
.order_by(network_interface::slot)
14271429
// TODO-cleanup: Having to specify each column again is less than
1428-
// ideal. However, this type doesn't appear to have the `Row`
1429-
// associated type in the documentation. DRY this out.
1430+
// ideal, but we can't derive `Selectable` since this is the result
1431+
// of a JOIN and not from a single table. DRY this out if possible.
14301432
.select((
14311433
network_interface::name,
14321434
network_interface::ip,
14331435
network_interface::mac,
14341436
vpc_subnet::ipv4_block,
14351437
vpc_subnet::ipv6_block,
1436-
network_interface::vpc_id,
1438+
vpc::vni,
14371439
network_interface::slot,
14381440
))
14391441
.get_results_async::<NicInfo>(self.pool_authorized(opctx).await?)
14401442
.await
14411443
.map_err(|e| {
14421444
public_error_from_diesel_pool(e, ErrorHandler::Server)
14431445
})?;
1446+
println!("{:#?}", rows);
14441447
Ok(rows
14451448
.into_iter()
14461449
.map(sled_client_types::NetworkInterface::from)

nexus/src/db/model.rs

Lines changed: 64 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -570,15 +570,25 @@ pub struct MacAddr(pub external::MacAddr);
570570
impl MacAddr {
571571
/// Generate a unique MAC address for an interface
572572
pub fn new() -> Result<Self, external::Error> {
573-
use rand::Fill;
574-
// Use the Oxide OUI A8 40 25
575-
let mut addr = [0xA8, 0x40, 0x25, 0x00, 0x00, 0x00];
576-
addr[3..].try_fill(&mut StdRng::from_entropy()).map_err(|_| {
577-
external::Error::internal_error("failed to generate MAC")
578-
})?;
579-
// From RFD 174, Oxide virtual MACs are constrained to have these bits
580-
// set.
581-
addr[3] |= 0xF0;
573+
// From RFD 174, the last three octets of Oxide virtual MACs are
574+
// constrained to be in the range F0:00:00 - FF:FF:FF. However, we're
575+
// further splitting this into a range for customer instances, F0:00:00
576+
// - EF:FF:FF, and VPC / system MAC addresses, FF:00:00 - FF:FF:FF.
577+
// See
578+
// https://github.com/oxidecomputer/omicron/pull/955#discussion_r856432498
579+
// for the initial discussion surrounding this split.
580+
//
581+
// TODO-completeness: Update this comment point to the relevant RFD(s)
582+
// once they're created.
583+
use rand::Rng;
584+
const MIN: u32 = 0x00_00_00;
585+
const MAX: u32 = 0x0E_FF_FF;
586+
let bytes = rand::thread_rng().gen_range(MIN..=MAX).to_be_bytes();
587+
588+
// Use the Oxide OUI A8 40 25, and set the first nibble of the third
589+
// byte, to ensure we're in the virtual address range.
590+
let addr = [0xA8, 0x40, 0x25, 0xF0 | bytes[1], bytes[2], bytes[3]];
591+
582592
// TODO-correctness: We should use an explicit allocator for the MACs
583593
// given the small address space. Right now creation requests may fail
584594
// due to MAC collision, especially given the 20-bit space.
@@ -1867,6 +1877,33 @@ impl OximeterInfo {
18671877
}
18681878
}
18691879

1880+
#[derive(Clone, Debug, Copy, AsExpression, FromSqlRow)]
1881+
#[sql_type = "sql_types::Int4"]
1882+
pub struct Vni(pub external::Vni);
1883+
1884+
impl<DB> ToSql<sql_types::Int4, DB> for Vni
1885+
where
1886+
DB: Backend,
1887+
i32: ToSql<sql_types::Int4, DB>,
1888+
{
1889+
fn to_sql<W: std::io::Write>(
1890+
&self,
1891+
out: &mut serialize::Output<W, DB>,
1892+
) -> serialize::Result {
1893+
i32::try_from(u32::from(self.0)).unwrap().to_sql(out)
1894+
}
1895+
}
1896+
1897+
impl<DB> FromSql<sql_types::Int4, DB> for Vni
1898+
where
1899+
DB: Backend,
1900+
i32: FromSql<sql_types::Int4, DB>,
1901+
{
1902+
fn from_sql(bytes: RawValue<DB>) -> deserialize::Result<Self> {
1903+
Ok(Vni(external::Vni::try_from(i32::from_sql(bytes)?)?))
1904+
}
1905+
}
1906+
18701907
#[derive(Queryable, Insertable, Clone, Debug, Selectable, Resource)]
18711908
#[table_name = "vpc"]
18721909
pub struct Vpc {
@@ -1875,6 +1912,7 @@ pub struct Vpc {
18751912

18761913
pub project_id: Uuid,
18771914
pub system_router_id: Uuid,
1915+
pub vni: Vni,
18781916
pub ipv6_prefix: Ipv6Net,
18791917
pub dns_name: Name,
18801918

@@ -1909,6 +1947,7 @@ impl Vpc {
19091947
identity,
19101948
project_id,
19111949
system_router_id,
1950+
vni: Vni(external::Vni::random()),
19121951
ipv6_prefix,
19131952
dns_name: params.dns_name.into(),
19141953
firewall_gen: Generation::new(),
@@ -2736,6 +2775,7 @@ impl UpdateAvailableArtifact {
27362775

27372776
#[cfg(test)]
27382777
mod tests {
2778+
use super::MacAddr;
27392779
use super::Uuid;
27402780
use super::VpcSubnet;
27412781
use ipnetwork::Ipv4Network;
@@ -2857,4 +2897,19 @@ mod tests {
28572897
assert!(!subnet.check_requestable_addr("fd00::1".parse().unwrap()));
28582898
assert!(subnet.check_requestable_addr("fd00::1:1".parse().unwrap()));
28592899
}
2900+
2901+
#[test]
2902+
fn test_random_mac_address() {
2903+
let mac = MacAddr::new().expect("Failed to create random MAC");
2904+
let bytes = mac.0.as_bytes();
2905+
assert_eq!(&bytes[..3], &[0xA8, 0x40, 0x25], "Invalid Oxide OUI");
2906+
assert_eq!(
2907+
bytes[3] & 0xF0,
2908+
0xF0,
2909+
concat!(
2910+
"Customer MAC addresses should be in the virtual range ",
2911+
"a8:40:25:f0:00:00 - a8:40:25:fe:ff:ff"
2912+
)
2913+
);
2914+
}
28602915
}

nexus/src/db/schema.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,7 @@ table! {
333333
time_deleted -> Nullable<Timestamptz>,
334334
project_id -> Uuid,
335335
system_router_id -> Uuid,
336+
vni -> Int4,
336337
ipv6_prefix -> Inet,
337338
dns_name -> Text,
338339
firewall_gen -> Int8,

nexus/src/nexus.rs

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1851,23 +1851,6 @@ impl Nexus {
18511851
.derive_guest_network_interface_info(&opctx, &authz_instance)
18521852
.await?;
18531853

1854-
/*
1855-
.instance_list_network_interfaces(
1856-
&opctx,
1857-
&authz_instance,
1858-
&DataPageParams {
1859-
marker: None,
1860-
direction: dropshot::PaginationOrder::Ascending,
1861-
limit: std::num::NonZeroU32::new(MAX_NICS_PER_INSTANCE)
1862-
.unwrap(),
1863-
},
1864-
)
1865-
.await?
1866-
.iter()
1867-
.map(|x| x.clone().into())
1868-
.collect();
1869-
*/
1870-
18711854
// Ask the sled agent to begin the state change. Then update the
18721855
// database to reflect the new intermediate state. If this update is
18731856
// not the newest one, that's fine. That might just mean the sled agent

openapi/sled-agent.json

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -915,7 +915,7 @@
915915
"maxLength": 63
916916
},
917917
"NetworkInterface": {
918-
"description": "Information required to construct virtual network interfaces for a guest",
918+
"description": "Information required to construct a virtual network interface for a guest",
919919
"type": "object",
920920
"properties": {
921921
"ip": {
@@ -936,9 +936,8 @@
936936
"subnet": {
937937
"$ref": "#/components/schemas/IpNet"
938938
},
939-
"vpc_id": {
940-
"type": "string",
941-
"format": "uuid"
939+
"vni": {
940+
"$ref": "#/components/schemas/Vni"
942941
}
943942
},
944943
"required": [
@@ -947,7 +946,7 @@
947946
"name",
948947
"slot",
949948
"subnet",
950-
"vpc_id"
949+
"vni"
951950
]
952951
},
953952
"ServiceEnsureBody": {
@@ -1017,6 +1016,12 @@
10171016
"zone"
10181017
]
10191018
},
1019+
"Vni": {
1020+
"description": "A Geneve Virtual Network Identifier",
1021+
"type": "integer",
1022+
"format": "uint32",
1023+
"minimum": 0
1024+
},
10201025
"VolumeConstructionRequest": {
10211026
"oneOf": [
10221027
{

sled-agent-client/src/lib.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,12 @@ impl From<&omicron_common::api::external::Name> for types::Name {
199199
}
200200
}
201201

202+
impl From<omicron_common::api::external::Vni> for types::Vni {
203+
fn from(v: omicron_common::api::external::Vni) -> Self {
204+
Self(u32::from(v))
205+
}
206+
}
207+
202208
impl From<omicron_common::api::external::MacAddr> for types::MacAddr {
203209
fn from(s: omicron_common::api::external::MacAddr) -> Self {
204210
Self(s.0.to_string())

0 commit comments

Comments
 (0)