-
Notifications
You must be signed in to change notification settings - Fork 40
/
zone.rs
642 lines (583 loc) · 21.6 KB
/
zone.rs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
// 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/.
//! API for interacting with Zones running Propolis.
use anyhow::anyhow;
use ipnetwork::IpNetwork;
use slog::Logger;
use std::net::{IpAddr, Ipv6Addr};
use crate::illumos::addrobj::AddrObject;
use crate::illumos::dladm::{EtherstubVnic, VNIC_PREFIX_CONTROL};
use crate::illumos::zfs::ZONE_ZFS_DATASET_MOUNTPOINT;
use crate::illumos::{execute, PFEXEC};
use omicron_common::address::SLED_PREFIX;
const DLADM: &str = "/usr/sbin/dladm";
const IPADM: &str = "/usr/sbin/ipadm";
pub const SVCADM: &str = "/usr/sbin/svcadm";
pub const SVCCFG: &str = "/usr/sbin/svccfg";
pub const ZLOGIN: &str = "/usr/sbin/zlogin";
// TODO: These could become enums
pub const ZONE_PREFIX: &str = "oxz_";
pub const PROPOLIS_ZONE_PREFIX: &str = "oxz_propolis-server_";
#[derive(thiserror::Error, Debug)]
enum Error {
#[error("Zone execution error: {0}")]
Execution(#[from] crate::illumos::ExecutionError),
#[error(transparent)]
AddrObject(#[from] crate::illumos::addrobj::ParseError),
#[error("Address not found: {addrobj}")]
AddressNotFound { addrobj: AddrObject },
}
/// Operations issued via [`zone::Adm`].
#[derive(Debug, Clone)]
pub enum Operation {
Boot,
Configure,
Delete,
Halt,
Install,
List,
Uninstall,
}
/// Errors from issuing [`zone::Adm`] commands.
#[derive(thiserror::Error, Debug)]
#[error("Failed to execute zoneadm command '{op:?}' for zone '{zone}': {err}")]
pub struct AdmError {
op: Operation,
zone: String,
#[source]
err: zone::ZoneError,
}
/// Errors which may be encountered when deleting addresses.
#[derive(thiserror::Error, Debug)]
#[error("Failed to delete address '{addrobj}' in zone '{zone}': {err}")]
pub struct DeleteAddressError {
zone: String,
addrobj: AddrObject,
#[source]
err: crate::illumos::ExecutionError,
}
/// Errors from [`Zones::get_control_interface`].
/// Error which may be returned accessing the control interface of a zone.
#[derive(thiserror::Error, Debug)]
pub enum GetControlInterfaceError {
#[error("Failed to query zone '{zone}' for control interface: {err}")]
Execution {
zone: String,
#[source]
err: crate::illumos::ExecutionError,
},
#[error("VNIC starting with 'oxControl' not found in {zone}")]
NotFound { zone: String },
}
/// Errors which may be encountered ensuring addresses.
#[derive(thiserror::Error, Debug)]
#[error(
"Failed to create address {request:?} with name {name} in {zone}: {err}"
)]
pub struct EnsureAddressError {
zone: String,
request: AddressRequest,
name: AddrObject,
#[source]
err: anyhow::Error,
}
/// Errors from [`Zones::ensure_has_global_zone_v6_address`].
#[derive(thiserror::Error, Debug)]
#[error("Failed to create address {address} with name {name} in the GZ on {link:?}: {err}. Note to developers: {extra_note}")]
pub struct EnsureGzAddressError {
address: Ipv6Addr,
link: EtherstubVnic,
name: String,
#[source]
err: anyhow::Error,
extra_note: String,
}
/// Describes the type of addresses which may be requested from a zone.
#[derive(Copy, Clone, Debug)]
// TODO-cleanup: Remove, along with moving to IPv6 addressing everywhere.
// See https://github.com/oxidecomputer/omicron/issues/889.
#[allow(dead_code)]
pub enum AddressRequest {
Dhcp,
Static(IpNetwork),
}
impl AddressRequest {
/// Convenience function for creating an `AddressRequest` from a static IP.
pub fn new_static(ip: IpAddr, prefix: Option<u8>) -> Self {
let prefix = prefix.unwrap_or_else(|| match ip {
IpAddr::V4(_) => 24,
IpAddr::V6(_) => SLED_PREFIX,
});
let addr = IpNetwork::new(ip, prefix).unwrap();
AddressRequest::Static(addr)
}
}
/// Wraps commands for interacting with Zones.
pub struct Zones {}
#[cfg_attr(test, mockall::automock, allow(dead_code))]
impl Zones {
/// Ensures a zone is halted before both uninstalling and deleting it.
///
/// Returns the state the zone was in before it was removed, or None if the
/// zone did not exist.
pub fn halt_and_remove(
name: &str,
) -> Result<Option<zone::State>, AdmError> {
match Self::find(name)? {
None => Ok(None),
Some(zone) => {
let state = zone.state();
let (halt, uninstall) = match state {
// For states where we could be running, attempt to halt.
zone::State::Running | zone::State::Ready => (true, true),
// For zones where we never performed installation, simply
// delete the zone - uninstallation is invalid.
zone::State::Configured => (false, false),
// For most zone states, perform uninstallation.
_ => (false, true),
};
if halt {
zone::Adm::new(name).halt().map_err(|err| AdmError {
op: Operation::Halt,
zone: name.to_string(),
err,
})?;
}
if uninstall {
zone::Adm::new(name).uninstall(/* force= */ true).map_err(
|err| AdmError {
op: Operation::Uninstall,
zone: name.to_string(),
err,
},
)?;
}
zone::Config::new(name)
.delete(/* force= */ true)
.run()
.map_err(|err| AdmError {
op: Operation::Delete,
zone: name.to_string(),
err,
})?;
Ok(Some(state))
}
}
}
/// Halt and remove the zone, logging the state in which the zone was found.
pub fn halt_and_remove_logged(
log: &Logger,
name: &str,
) -> Result<(), AdmError> {
if let Some(state) = Self::halt_and_remove(name)? {
info!(
log,
"halt_and_remove_logged: Previous zone state: {:?}", state
);
}
Ok(())
}
pub fn install_omicron_zone(
log: &Logger,
zone_name: &str,
zone_image: &std::path::Path,
datasets: &[zone::Dataset],
devices: &[zone::Device],
vnics: Vec<String>,
) -> Result<(), AdmError> {
if let Some(zone) = Self::find(zone_name)? {
info!(
log,
"install_omicron_zone: Found zone: {} in state {:?}",
zone.name(),
zone.state()
);
if zone.state() == zone::State::Installed
|| zone.state() == zone::State::Running
{
// TODO: Admittedly, the zone still might be messed up. However,
// for now, we assume that "installed" means "good to go".
return Ok(());
} else {
info!(
log,
"Invalid state; uninstalling and deleting zone {}",
zone_name
);
Zones::halt_and_remove_logged(log, zone.name())?;
}
}
info!(log, "Configuring new Omicron zone: {}", zone_name);
let mut cfg = zone::Config::create(
zone_name,
// overwrite=
true,
zone::CreationOptions::Blank,
);
let path = format!("{}/{}", ZONE_ZFS_DATASET_MOUNTPOINT, zone_name);
cfg.get_global()
.set_brand("omicron1")
.set_path(&path)
.set_autoboot(false)
.set_ip_type(zone::IpType::Exclusive);
for dataset in datasets {
cfg.add_dataset(&dataset);
}
for device in devices {
cfg.add_device(device);
}
for vnic in &vnics {
cfg.add_net(&zone::Net {
physical: vnic.to_string(),
..Default::default()
});
}
cfg.run().map_err(|err| AdmError {
op: Operation::Configure,
zone: zone_name.to_string(),
err,
})?;
info!(log, "Installing Omicron zone: {}", zone_name);
zone::Adm::new(zone_name).install(&[zone_image.as_ref()]).map_err(
|err| AdmError {
op: Operation::Install,
zone: zone_name.to_string(),
err,
},
)?;
Ok(())
}
/// Boots a zone (named `name`).
pub fn boot(name: &str) -> Result<(), AdmError> {
zone::Adm::new(name).boot().map_err(|err| AdmError {
op: Operation::Boot,
zone: name.to_string(),
err,
})?;
Ok(())
}
/// Returns all zones that may be managed by the Sled Agent.
///
/// These zones must have names starting with [`ZONE_PREFIX`].
pub fn get() -> Result<Vec<zone::Zone>, AdmError> {
Ok(zone::Adm::list()
.map_err(|err| AdmError {
op: Operation::List,
zone: "<all>".to_string(),
err,
})?
.into_iter()
.filter(|z| z.name().starts_with(ZONE_PREFIX))
.collect())
}
/// Finds a zone with a specified name.
///
/// Can only return zones that start with [`ZONE_PREFIX`], as they
/// are managed by the Sled Agent.
pub fn find(name: &str) -> Result<Option<zone::Zone>, AdmError> {
Ok(Self::get()?.into_iter().find(|zone| zone.name() == name))
}
/// Returns the name of the VNIC used to communicate with the control plane.
pub fn get_control_interface(
zone: &str,
) -> Result<String, GetControlInterfaceError> {
let mut command = std::process::Command::new(PFEXEC);
let cmd = command.args(&[
ZLOGIN,
zone,
DLADM,
"show-vnic",
"-p",
"-o",
"LINK",
]);
let output = execute(cmd).map_err(|err| {
GetControlInterfaceError::Execution { zone: zone.to_string(), err }
})?;
String::from_utf8_lossy(&output.stdout)
.lines()
.find_map(|name| {
if name.starts_with(VNIC_PREFIX_CONTROL) {
Some(name.to_string())
} else {
None
}
})
.ok_or(GetControlInterfaceError::NotFound {
zone: zone.to_string(),
})
}
/// Ensures that an IP address on an interface matches the requested value.
///
/// - If the address exists, ensure it has the desired value.
/// - If the address does not exist, create it.
///
/// This address may be optionally within a zone `zone`.
/// If `None` is supplied, the address is queried from the Global Zone.
#[allow(clippy::needless_lifetimes)]
pub fn ensure_address<'a>(
zone: Option<&'a str>,
addrobj: &AddrObject,
addrtype: AddressRequest,
) -> Result<IpNetwork, EnsureAddressError> {
|zone, addrobj, addrtype| -> Result<IpNetwork, anyhow::Error> {
match Self::get_address(zone, addrobj) {
Ok(addr) => {
if let AddressRequest::Static(expected_addr) = addrtype {
// If the address is static, we need to validate that it
// matches the value we asked for.
if addr != expected_addr {
// If the address doesn't match, try removing the old
// value before using the new one.
Self::delete_address(zone, addrobj)
.map_err(|e| anyhow!(e))?;
return Self::create_address(
zone, addrobj, addrtype,
)
.map_err(|e| anyhow!(e));
}
}
Ok(addr)
}
Err(_) => Self::create_address(zone, addrobj, addrtype)
.map_err(|e| anyhow!(e)),
}
}(zone, addrobj, addrtype)
.map_err(|err| EnsureAddressError {
zone: zone.unwrap_or("global").to_string(),
request: addrtype,
name: addrobj.clone(),
err,
})
}
/// Gets the IP address of an interface.
///
/// This address may optionally be within a zone named `zone`.
/// If `None` is supplied, the address is queried from the Global Zone.
#[allow(clippy::needless_lifetimes)]
fn get_address<'a>(
zone: Option<&'a str>,
addrobj: &AddrObject,
) -> Result<IpNetwork, Error> {
let mut command = std::process::Command::new(PFEXEC);
let mut args = vec![];
if let Some(zone) = zone {
args.push(ZLOGIN);
args.push(zone);
};
let addrobj_str = addrobj.to_string();
args.extend(&[IPADM, "show-addr", "-p", "-o", "ADDR", &addrobj_str]);
let cmd = command.args(args);
let output = execute(cmd)?;
String::from_utf8_lossy(&output.stdout)
.lines()
.find_map(|s| s.parse().ok())
.ok_or(Error::AddressNotFound { addrobj: addrobj.clone() })
}
/// Returns Ok(()) if `addrobj` has a corresponding link-local IPv6 address.
///
/// Zone may either be `Some(zone)` for a non-global zone, or `None` to
/// run the command in the Global zone.
#[allow(clippy::needless_lifetimes)]
fn has_link_local_v6_address<'a>(
zone: Option<&'a str>,
addrobj: &AddrObject,
) -> Result<(), Error> {
let mut command = std::process::Command::new(PFEXEC);
let prefix =
if let Some(zone) = zone { vec![ZLOGIN, zone] } else { vec![] };
let interface = format!("{}/", addrobj.interface());
let show_addr_args =
&[IPADM, "show-addr", "-p", "-o", "TYPE", &interface];
let args = prefix.iter().chain(show_addr_args);
let cmd = command.args(args);
let output = execute(cmd)?;
if let Some(_) = String::from_utf8_lossy(&output.stdout)
.lines()
.find(|s| s.trim() == "addrconf")
{
return Ok(());
}
Err(Error::AddressNotFound { addrobj: addrobj.clone() })
}
// Attempts to create the requested address.
//
// Does NOT check if the address already exists.
#[allow(clippy::needless_lifetimes)]
fn create_address_internal<'a>(
zone: Option<&'a str>,
addrobj: &AddrObject,
addrtype: AddressRequest,
) -> Result<(), crate::illumos::ExecutionError> {
let mut command = std::process::Command::new(PFEXEC);
let mut args = vec![];
if let Some(zone) = zone {
args.push(ZLOGIN.to_string());
args.push(zone.to_string());
};
args.extend(
vec![IPADM, "create-addr", "-t", "-T"]
.into_iter()
.map(String::from),
);
match addrtype {
AddressRequest::Dhcp => {
args.push("dhcp".to_string());
}
AddressRequest::Static(addr) => {
args.push("static".to_string());
args.push("-a".to_string());
args.push(addr.to_string());
}
}
args.push(addrobj.to_string());
let cmd = command.args(args);
execute(cmd)?;
Ok(())
}
#[allow(clippy::needless_lifetimes)]
pub fn delete_address<'a>(
zone: Option<&'a str>,
addrobj: &AddrObject,
) -> Result<(), DeleteAddressError> {
let mut command = std::process::Command::new(PFEXEC);
let mut args = vec![];
if let Some(zone) = zone {
args.push(ZLOGIN.to_string());
args.push(zone.to_string());
};
args.push(IPADM.to_string());
args.push("delete-addr".to_string());
args.push(addrobj.to_string());
let cmd = command.args(args);
execute(cmd).map_err(|err| DeleteAddressError {
zone: zone.unwrap_or("global").to_string(),
addrobj: addrobj.clone(),
err,
})?;
Ok(())
}
/// Ensures a link-local IPv6 exists with the name provided in `addrobj`.
///
/// A link-local address is necessary for allocating a static address on an
/// interface on illumos.
///
/// For more context, see:
/// <https://ry.goodwu.net/tinkering/a-day-in-the-life-of-an-ipv6-address-on-illumos/>
#[allow(clippy::needless_lifetimes)]
pub fn ensure_has_link_local_v6_address<'a>(
zone: Option<&'a str>,
addrobj: &AddrObject,
) -> Result<(), crate::illumos::ExecutionError> {
if let Ok(()) = Self::has_link_local_v6_address(zone, &addrobj) {
return Ok(());
}
// No link-local address was found, attempt to make one.
let mut command = std::process::Command::new(PFEXEC);
let prefix =
if let Some(zone) = zone { vec![ZLOGIN, zone] } else { vec![] };
let create_addr_args = &[
IPADM,
"create-addr",
"-t",
"-T",
"addrconf",
&addrobj.to_string(),
];
let args = prefix.iter().chain(create_addr_args);
let cmd = command.args(args);
execute(cmd)?;
Ok(())
}
// TODO(https://github.com/oxidecomputer/omicron/issues/821): We
// should remove this function when Sled Agents are provided IPv6 addresses
// from RSS.
pub fn ensure_has_global_zone_v6_address(
link: EtherstubVnic,
address: Ipv6Addr,
name: &str,
) -> Result<(), EnsureGzAddressError> {
// Call the guts of this function within a closure to make it easier
// to wrap the error with appropriate context.
|link: EtherstubVnic, address, name| -> Result<(), anyhow::Error> {
let gz_link_local_addrobj = AddrObject::new(&link.0, "linklocal")
.map_err(|err| anyhow!(err))?;
Self::ensure_has_link_local_v6_address(
None,
&gz_link_local_addrobj,
)
.map_err(|err| anyhow!(err))?;
// Ensure that a static IPv6 address has been allocated
// to the Global Zone. Without this, we don't have a way
// to route to IP addresses that we want to create in
// the non-GZ. Note that we use a `/64` prefix, as all addresses
// allocated for services on this sled itself are within the underlay
// prefix. Anything else must be routed through Sidecar.
Self::ensure_address(
None,
&gz_link_local_addrobj
.on_same_interface(name)
.map_err(|err| anyhow!(err))?,
AddressRequest::new_static(
IpAddr::V6(address),
Some(omicron_common::address::SLED_PREFIX),
),
)
.map_err(|err| anyhow!(err))?;
Ok(())
}(link.clone(), address, name)
.map_err(|err| EnsureGzAddressError {
address,
link,
name: name.to_string(),
err,
extra_note:
r#"As of https://github.com/oxidecomputer/omicron/pull/1066, we are changing the
physical device on which Global Zone addresses are allocated.
Before this PR, we allocated addresses and VNICs directly on a physical link.
After this PR, we are allocating them on etherstubs.
As a result, however, if your machine previously ran Omicron, it
may have addresses on the physical link which we'd like to
allocate from the etherstub instead.
This can be fixed with the following commands:
$ pfexec ipadm delete-addr <your-link>/bootstrap6
$ pfexec ipadm delete-addr <your-link>/sled6
$ pfexec ipadm delete-addr <your-link>/internaldns"#.to_string()
})?;
Ok(())
}
// Creates an IP address within a Zone.
#[allow(clippy::needless_lifetimes)]
fn create_address<'a>(
zone: Option<&'a str>,
addrobj: &AddrObject,
addrtype: AddressRequest,
) -> Result<IpNetwork, Error> {
// Do any prep work before allocating the address.
//
// Currently, this only happens when allocating IPv6 addresses in the
// non-global zone - to access these addresses, we must first set up
// an arbitrary IPv6 address within the Global Zone.
if let Some(zone) = zone {
match addrtype {
AddressRequest::Dhcp => {}
AddressRequest::Static(addr) => {
if addr.is_ipv6() {
// Finally, actually ensure that the v6 address we want
// exists within the zone.
let link_local_addrobj =
addrobj.on_same_interface("linklocal")?;
Self::ensure_has_link_local_v6_address(
Some(zone),
&link_local_addrobj,
)?;
}
}
}
};
// Actually perform address allocation.
Self::create_address_internal(zone, addrobj, addrtype)?;
Self::get_address(zone, addrobj)
}
}