Skip to content

Commit 813a859

Browse files
authored
[sled-agent] Allocate VNICs over etherstubs, fix inter-zone routing (#1066)
- Switches all VNIC allocation to occur over an "etherstub" device, called stub0. - Allocate all GZ addresses (bootstrap, Sled, addrconf) over an "etherstub"-allocated VNIC, called underlay0.
1 parent 4dfb749 commit 813a859

File tree

15 files changed

+298
-99
lines changed

15 files changed

+298
-99
lines changed

sled-agent/src/bootstrap/agent.rs

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -154,19 +154,23 @@ impl Agent {
154154
err,
155155
})?;
156156

157-
let data_link = if let Some(link) = sled_config.data_link.clone() {
158-
link
159-
} else {
160-
Dladm::find_physical().map_err(|err| {
157+
let etherstub = Dladm::create_etherstub().map_err(|e| {
158+
BootstrapError::SledError(format!(
159+
"Can't access etherstub device: {}",
160+
e
161+
))
162+
})?;
163+
164+
let etherstub_vnic =
165+
Dladm::create_etherstub_vnic(&etherstub).map_err(|e| {
161166
BootstrapError::SledError(format!(
162-
"Can't access physical link, and none in config: {}",
163-
err
167+
"Can't access etherstub VNIC device: {}",
168+
e
164169
))
165-
})?
166-
};
170+
})?;
167171

168172
Zones::ensure_has_global_zone_v6_address(
169-
data_link,
173+
etherstub_vnic,
170174
address,
171175
"bootstrap6",
172176
)

sled-agent/src/config.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ pub struct Config {
2727
/// Optional list of zpools to be used as "discovered disks".
2828
pub zpools: Option<Vec<ZpoolName>>,
2929

30-
/// The data link on which to allocate VNICs.
30+
/// The data link on which we infer the bootstrap address.
3131
///
3232
/// If unsupplied, we default to the first physical device.
3333
pub data_link: Option<PhysicalLink>,

sled-agent/src/illumos/dladm.rs

Lines changed: 78 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,15 @@ pub const VNIC_PREFIX_CONTROL: &str = "oxControl";
1919
// Viona, and thus plumbed directly to guests.
2020
pub const VNIC_PREFIX_GUEST: &str = "vopte";
2121

22+
/// Path to the DLADM command.
2223
pub const DLADM: &str = "/usr/sbin/dladm";
2324

25+
/// The name of the etherstub to be created for the underlay.
26+
pub const ETHERSTUB_NAME: &str = "stub0";
27+
28+
/// The name of the etherstub VNIC to be created in the global zone.
29+
pub const ETHERSTUB_VNIC_NAME: &str = "underlay0";
30+
2431
/// Errors returned from [`Dladm::find_physical`].
2532
#[derive(thiserror::Error, Debug)]
2633
pub enum FindPhysicalLinkError {
@@ -49,7 +56,7 @@ pub enum GetMacError {
4956
#[error("Failed to create VNIC {name} on link {link:?}: {err}")]
5057
pub struct CreateVnicError {
5158
name: String,
52-
link: PhysicalLink,
59+
link: String,
5360
#[source]
5461
err: ExecutionError,
5562
}
@@ -75,11 +82,77 @@ pub struct DeleteVnicError {
7582
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
7683
pub struct PhysicalLink(pub String);
7784

85+
/// The name of an etherstub
86+
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
87+
pub struct Etherstub(pub String);
88+
89+
/// The name of an etherstub's underlay VNIC
90+
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
91+
pub struct EtherstubVnic(pub String);
92+
93+
/// Identifies that an object may be used to create a VNIC.
94+
pub trait VnicSource {
95+
fn name(&self) -> &str;
96+
}
97+
98+
impl VnicSource for Etherstub {
99+
fn name(&self) -> &str {
100+
&self.0
101+
}
102+
}
103+
104+
impl VnicSource for PhysicalLink {
105+
fn name(&self) -> &str {
106+
&self.0
107+
}
108+
}
109+
78110
/// Wraps commands for interacting with data links.
79111
pub struct Dladm {}
80112

81113
#[cfg_attr(test, mockall::automock, allow(dead_code))]
82114
impl Dladm {
115+
/// Creates an etherstub, or returns one which already exists.
116+
pub fn create_etherstub() -> Result<Etherstub, ExecutionError> {
117+
if let Ok(stub) = Self::get_etherstub() {
118+
return Ok(stub);
119+
}
120+
let mut command = std::process::Command::new(PFEXEC);
121+
let cmd =
122+
command.args(&[DLADM, "create-etherstub", "-t", ETHERSTUB_NAME]);
123+
execute(cmd)?;
124+
Ok(Etherstub(ETHERSTUB_NAME.to_string()))
125+
}
126+
127+
/// Finds an etherstub.
128+
fn get_etherstub() -> Result<Etherstub, ExecutionError> {
129+
let mut command = std::process::Command::new(PFEXEC);
130+
let cmd = command.args(&[DLADM, "show-etherstub", ETHERSTUB_NAME]);
131+
execute(cmd)?;
132+
Ok(Etherstub(ETHERSTUB_NAME.to_string()))
133+
}
134+
135+
/// Creates a VNIC on top of the etherstub.
136+
///
137+
/// This VNIC is not tracked like [`crate::illumos::vnic::Vnic`], because
138+
/// it is expected to exist for the lifetime of the sled.
139+
pub fn create_etherstub_vnic(
140+
source: &Etherstub,
141+
) -> Result<EtherstubVnic, CreateVnicError> {
142+
if let Ok(vnic) = Self::get_etherstub_vnic() {
143+
return Ok(vnic);
144+
}
145+
Self::create_vnic(source, ETHERSTUB_VNIC_NAME, None, None)?;
146+
Ok(EtherstubVnic(ETHERSTUB_VNIC_NAME.to_string()))
147+
}
148+
149+
fn get_etherstub_vnic() -> Result<EtherstubVnic, ExecutionError> {
150+
let mut command = std::process::Command::new(PFEXEC);
151+
let cmd = command.args(&[DLADM, "show-vnic", ETHERSTUB_VNIC_NAME]);
152+
execute(cmd)?;
153+
Ok(EtherstubVnic(ETHERSTUB_VNIC_NAME.to_string()))
154+
}
155+
83156
/// Returns the name of the first observed physical data link.
84157
pub fn find_physical() -> Result<PhysicalLink, FindPhysicalLinkError> {
85158
let mut command = std::process::Command::new(PFEXEC);
@@ -135,8 +208,8 @@ impl Dladm {
135208
/// * `vnic_name`: Exact name of the VNIC to be created.
136209
/// * `mac`: An optional unicast MAC address for the newly created NIC.
137210
/// * `vlan`: An optional VLAN ID for VLAN tagging.
138-
pub fn create_vnic(
139-
physical: &PhysicalLink,
211+
pub fn create_vnic<T: VnicSource + 'static>(
212+
source: &T,
140213
vnic_name: &str,
141214
mac: Option<MacAddr>,
142215
vlan: Option<VlanID>,
@@ -147,7 +220,7 @@ impl Dladm {
147220
"create-vnic".to_string(),
148221
"-t".to_string(),
149222
"-l".to_string(),
150-
physical.0.to_string(),
223+
source.name().to_string(),
151224
];
152225

153226
if let Some(mac) = mac {
@@ -164,7 +237,7 @@ impl Dladm {
164237
let cmd = command.args(&args);
165238
execute(cmd).map_err(|err| CreateVnicError {
166239
name: vnic_name.to_string(),
167-
link: physical.clone(),
240+
link: source.name().to_string(),
168241
err,
169242
})?;
170243
Ok(())

sled-agent/src/illumos/mod.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,20 @@ pub mod zfs;
1515
pub mod zone;
1616
pub mod zpool;
1717

18-
const PFEXEC: &str = "/usr/bin/pfexec";
18+
pub const PFEXEC: &str = "/usr/bin/pfexec";
1919

2020
#[derive(thiserror::Error, Debug)]
2121
pub enum ExecutionError {
2222
#[error("Failed to start execution of [{command}]: {err}")]
2323
ExecutionStart { command: String, err: std::io::Error },
2424

2525
#[error(
26-
"Command [{command}] executed and failed with status: {status}. Output: {stderr}"
26+
"Command [{command}] executed and failed with status: {status}. Stdout: {stdout}, Stderr: {stderr}"
2727
)]
2828
CommandFailure {
2929
command: String,
3030
status: std::process::ExitStatus,
31+
stdout: String,
3132
stderr: String,
3233
},
3334
}
@@ -63,6 +64,7 @@ mod inner {
6364
.collect::<Vec<String>>()
6465
.join(" "),
6566
status: output.status,
67+
stdout: String::from_utf8_lossy(&output.stdout).to_string(),
6668
stderr: String::from_utf8_lossy(&output.stderr).to_string(),
6769
});
6870
}

sled-agent/src/illumos/running_zone.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,21 @@ impl RunningZone {
171171
Ok(network)
172172
}
173173

174+
pub async fn add_route(
175+
&self,
176+
destination: ipnetwork::Ipv6Network,
177+
) -> Result<(), RunCommandError> {
178+
self.run_cmd(&[
179+
"/usr/sbin/route",
180+
"add",
181+
"-inet6",
182+
&format!("{}/{}", destination.network(), destination.prefix()),
183+
"-inet6",
184+
&destination.ip().to_string(),
185+
])?;
186+
Ok(())
187+
}
188+
174189
/// Looks up a running zone based on the `zone_prefix`, if one already exists.
175190
///
176191
/// - If the zone was found, is running, and has a network interface, it is

sled-agent/src/illumos/vnic.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
//! API for controlling a single instance.
66
77
use crate::illumos::dladm::{
8-
CreateVnicError, DeleteVnicError, PhysicalLink, VNIC_PREFIX,
8+
CreateVnicError, DeleteVnicError, Etherstub, VNIC_PREFIX,
99
VNIC_PREFIX_CONTROL, VNIC_PREFIX_GUEST,
1010
};
1111
use omicron_common::api::external::MacAddr;
@@ -26,7 +26,7 @@ use crate::illumos::dladm::MockDladm as Dladm;
2626
pub struct VnicAllocator {
2727
value: Arc<AtomicU64>,
2828
scope: String,
29-
data_link: PhysicalLink,
29+
data_link: Etherstub,
3030
}
3131

3232
impl VnicAllocator {
@@ -41,11 +41,11 @@ impl VnicAllocator {
4141
///
4242
/// VnicAllocator::new("Storage") produces
4343
/// - oxControlStorage[NNN]
44-
pub fn new<S: AsRef<str>>(scope: S, physical_link: PhysicalLink) -> Self {
44+
pub fn new<S: AsRef<str>>(scope: S, etherstub: Etherstub) -> Self {
4545
Self {
4646
value: Arc::new(AtomicU64::new(0)),
4747
scope: scope.as_ref().to_string(),
48-
data_link: physical_link,
48+
data_link: etherstub,
4949
}
5050
}
5151

@@ -171,7 +171,7 @@ mod test {
171171
#[test]
172172
fn test_allocate() {
173173
let allocator =
174-
VnicAllocator::new("Foo", PhysicalLink("mylink".to_string()));
174+
VnicAllocator::new("Foo", Etherstub("mystub".to_string()));
175175
assert_eq!("oxFoo0", allocator.next());
176176
assert_eq!("oxFoo1", allocator.next());
177177
assert_eq!("oxFoo2", allocator.next());
@@ -180,7 +180,7 @@ mod test {
180180
#[test]
181181
fn test_allocate_within_scopes() {
182182
let allocator =
183-
VnicAllocator::new("Foo", PhysicalLink("mylink".to_string()));
183+
VnicAllocator::new("Foo", Etherstub("mystub".to_string()));
184184
assert_eq!("oxFoo0", allocator.next());
185185
let allocator = allocator.new_superscope("Baz");
186186
assert_eq!("oxBazFoo1", allocator.next());

sled-agent/src/illumos/zone.rs

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@ use slog::Logger;
1010
use std::net::{IpAddr, Ipv6Addr};
1111

1212
use crate::illumos::addrobj::AddrObject;
13-
use crate::illumos::dladm::{PhysicalLink, VNIC_PREFIX_CONTROL};
13+
use crate::illumos::dladm::{EtherstubVnic, VNIC_PREFIX_CONTROL};
1414
use crate::illumos::zfs::ZONE_ZFS_DATASET_MOUNTPOINT;
1515
use crate::illumos::{execute, PFEXEC};
16+
use omicron_common::address::SLED_PREFIX;
1617

1718
const DLADM: &str = "/usr/sbin/dladm";
1819
const IPADM: &str = "/usr/sbin/ipadm";
@@ -98,13 +99,14 @@ pub struct EnsureAddressError {
9899

99100
/// Errors from [`Zones::ensure_has_global_zone_v6_address`].
100101
#[derive(thiserror::Error, Debug)]
101-
#[error("Failed to create address {address} with name {name} in the GZ on {link:?}: {err}")]
102+
#[error("Failed to create address {address} with name {name} in the GZ on {link:?}: {err}. Note to developers: {extra_note}")]
102103
pub struct EnsureGzAddressError {
103104
address: Ipv6Addr,
104-
link: PhysicalLink,
105+
link: EtherstubVnic,
105106
name: String,
106107
#[source]
107108
err: anyhow::Error,
109+
extra_note: String,
108110
}
109111

110112
/// Describes the type of addresses which may be requested from a zone.
@@ -122,7 +124,7 @@ impl AddressRequest {
122124
pub fn new_static(ip: IpAddr, prefix: Option<u8>) -> Self {
123125
let prefix = prefix.unwrap_or_else(|| match ip {
124126
IpAddr::V4(_) => 24,
125-
IpAddr::V6(_) => 64,
127+
IpAddr::V6(_) => SLED_PREFIX,
126128
});
127129
let addr = IpNetwork::new(ip, prefix).unwrap();
128130
AddressRequest::Static(addr)
@@ -543,13 +545,13 @@ impl Zones {
543545
// should remove this function when Sled Agents are provided IPv6 addresses
544546
// from RSS.
545547
pub fn ensure_has_global_zone_v6_address(
546-
link: PhysicalLink,
548+
link: EtherstubVnic,
547549
address: Ipv6Addr,
548550
name: &str,
549551
) -> Result<(), EnsureGzAddressError> {
550552
// Call the guts of this function within a closure to make it easier
551553
// to wrap the error with appropriate context.
552-
|link: PhysicalLink, address, name| -> Result<(), anyhow::Error> {
554+
|link: EtherstubVnic, address, name| -> Result<(), anyhow::Error> {
553555
let gz_link_local_addrobj = AddrObject::new(&link.0, "linklocal")
554556
.map_err(|err| anyhow!(err))?;
555557
Self::ensure_has_link_local_v6_address(
@@ -582,6 +584,22 @@ impl Zones {
582584
link,
583585
name: name.to_string(),
584586
err,
587+
extra_note:
588+
r#"As of https://github.com/oxidecomputer/omicron/pull/1066, we are changing the
589+
physical device on which Global Zone addresses are allocated.
590+
591+
Before this PR, we allocated addresses and VNICs directly on a physical link.
592+
After this PR, we are allocating them on etherstubs.
593+
594+
As a result, however, if your machine previously ran Omicron, it
595+
may have addresses on the physical link which we'd like to
596+
allocate from the etherstub instead.
597+
598+
This can be fixed with the following commands:
599+
600+
$ pfexec ipadm delete-addr <your-link>/bootstrap6
601+
$ pfexec ipadm delete-addr <your-link>/sled6
602+
$ pfexec ipadm delete-addr <your-link>/internaldns"#.to_string()
585603
})?;
586604
Ok(())
587605
}

sled-agent/src/instance.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -715,7 +715,7 @@ impl Instance {
715715
#[cfg(test)]
716716
mod test {
717717
use super::*;
718-
use crate::illumos::dladm::PhysicalLink;
718+
use crate::illumos::dladm::Etherstub;
719719
use crate::mocks::MockNexusClient;
720720
use crate::opte::OptePortAllocator;
721721
use crate::params::InstanceStateRequested;
@@ -786,7 +786,7 @@ mod test {
786786
let log = logger();
787787
let vnic_allocator = VnicAllocator::new(
788788
"Test".to_string(),
789-
PhysicalLink("mylink".to_string()),
789+
Etherstub("mylink".to_string()),
790790
);
791791
let port_allocator = OptePortAllocator::new();
792792
let nexus_client = MockNexusClient::default();

0 commit comments

Comments
 (0)