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

teach propolis-server to understand configurable boot order #756

Merged
merged 45 commits into from
Sep 28, 2024
Merged
Show file tree
Hide file tree
Changes from 43 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
f5437f6
wip: teach propolis-server to understand configurable boot order
iximeow Sep 4, 2024
2032c1b
Merge branch 'master' into ixi/propolis-bootorder
iximeow Sep 5, 2024
ca97278
log plumbing
iximeow Sep 6, 2024
d5e43c4
shelve the first_boot_only attribute for a moment
iximeow Sep 7, 2024
3f921f0
rustfmt and log tweaks
iximeow Sep 10, 2024
e0c7c71
include actually-usable tests
iximeow Sep 10, 2024
54bda84
actually include guest OS definitions for 24.04 (shouldn't land)
iximeow Sep 10, 2024
cc485b0
move all the EFI variable stuff to a module
iximeow Sep 12, 2024
7ddf735
lose dependency on specific test images
iximeow Sep 12, 2024
b112aa6
fill out other half of test "boot_order_source_priority"
iximeow Sep 13, 2024
f0a09db
use the implicit boot-disk entry like every other test
iximeow Sep 13, 2024
ed4a3e8
use empty FAT filesystems for unbootable disks
iximeow Sep 13, 2024
7484eb4
log formatting consistency
iximeow Sep 13, 2024
ed455ea
review feedback: stray eprintln, include new boot_order in api-types
iximeow Sep 17, 2024
ee46c91
Merge remote-tracking branch 'github/master' into ixi/propolis-bootorder
iximeow Sep 17, 2024
09583fb
extracted that into another PR
iximeow Sep 17, 2024
7d595a3
cleanup on aisle SpecBuilder
iximeow Sep 17, 2024
f53ba2e
fix phd-tests also
iximeow Sep 17, 2024
16138d1
clippy lints
iximeow Sep 17, 2024
92e9859
include new boot order item in propolis-server-falcon.json too
iximeow Sep 17, 2024
2183773
fix formatting in propolis-server.json
iximeow Sep 17, 2024
05495d5
fix bad merge resolution
iximeow Sep 18, 2024
6a386b0
move to more forward-compatible boot settings approach
iximeow Sep 24, 2024
bcb6824
check if guest environment supports efivar writes, bail early if not
iximeow Sep 24, 2024
3200708
update falcon openapi document too
iximeow Sep 24, 2024
d4d9b07
last eprintln: should be trace tbh
iximeow Sep 24, 2024
fda55f8
Merge remote-tracking branch 'github/master' into ixi/propolis-bootorder
iximeow Sep 24, 2024
3ad7383
keep some notes about where backend_name went from
iximeow Sep 24, 2024
2e01169
resolve merge conflict with refactors
iximeow Sep 25, 2024
9af0380
one less todo
iximeow Sep 25, 2024
617dd0c
settle on an actual approach for the test "boot_disk" helper fn
iximeow Sep 25, 2024
fcfa0a2
ok, keep all tests passing with framework changes
iximeow Sep 25, 2024
247f1e3
rustfmt
iximeow Sep 25, 2024
52aa0fe
speed limit 80
iximeow Sep 25, 2024
c9a522b
dont want to wrap those lines, avoid the problem
iximeow Sep 25, 2024
bbeafcf
rustfmt
iximeow Sep 25, 2024
a636216
rustfmt
iximeow Sep 25, 2024
ab9743e
remove unneeded (and not-generally-correct) ubuntu 24.04 adapter
iximeow Sep 25, 2024
458b740
cargo clippy --workspace...
iximeow Sep 25, 2024
6d0077e
remove unnecessary warning about EFI partition
iximeow Sep 25, 2024
cee2adc
fix migration tests for this non-API-breaking change
iximeow Sep 25, 2024
93e33e4
Update phd-tests/framework/src/test_vm/config.rs
iximeow Sep 25, 2024
ba45929
make PHD disk name/backend name derivation match propolis-server API
iximeow Sep 27, 2024
22b5a08
review feedback:
iximeow Sep 27, 2024
3a3e753
use newtypes to delineate DeviceName/BackendName in PHD
iximeow Sep 27, 2024
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
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions bin/propolis-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,7 @@ async fn new_instance(
// TODO: Allow specifying NICs
nics: vec![],
disks,
boot_settings: None,
migrate: None,
cloud_init_bytes,
};
Expand Down Expand Up @@ -517,6 +518,9 @@ async fn migrate_instance(
// TODO: Handle migrating NICs
nics: vec![],
disks,
// TODO: Handle retaining boot settings? Or extant boot settings
// forwarded along outside InstanceEnsure anyway.
hawkw marked this conversation as resolved.
Show resolved Hide resolved
boot_settings: None,
migrate: Some(InstanceMigrateInitiateRequest {
migration_id: Uuid::new_v4(),
src_addr: src_addr.to_string(),
Expand Down
74 changes: 72 additions & 2 deletions bin/propolis-server/src/lib/initializer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};

use crate::serial::Serial;
use crate::spec::{self, Spec, StorageBackend};
use crate::spec::{self, Spec, StorageBackend, StorageDevice};
use crate::stats::{
track_network_interface_kstats, track_vcpu_kstats, VirtualDiskProducer,
VirtualMachine,
Expand All @@ -34,7 +34,11 @@ use propolis::hw::ibmpc;
use propolis::hw::pci;
use propolis::hw::ps2::ctrl::PS2Ctrl;
use propolis::hw::qemu::pvpanic::QemuPvpanic;
use propolis::hw::qemu::{debug::QemuDebugPort, fwcfg, ramfb};
use propolis::hw::qemu::{
debug::QemuDebugPort,
fwcfg::{self, Entry},
ramfb,
};
use propolis::hw::uart::LpcUart;
use propolis::hw::{nvme, virtio};
use propolis::intr_pins;
Expand Down Expand Up @@ -1001,6 +1005,68 @@ impl<'a> MachineInitializer<'a> {
smb_tables.commit()
}

fn generate_bootorder(&self) -> Result<Option<Entry>, Error> {
iximeow marked this conversation as resolved.
Show resolved Hide resolved
info!(
self.log,
"Generating bootorder with order: {:?}",
self.spec.boot_order.as_ref()
);
let Some(boot_names) = self.spec.boot_order.as_ref() else {
return Ok(None);
};

let mut order = fwcfg::formats::BootOrder::new();

let parse_bdf =
iximeow marked this conversation as resolved.
Show resolved Hide resolved
|pci_path: &propolis_api_types::instance_spec::PciPath| {
let bdf: Result<pci::Bdf, Error> =
pci_path.to_owned().try_into().map_err(|e| {
Error::new(
ErrorKind::InvalidInput,
format!(
"Couldn't get PCI BDF for {}: {}",
pci_path, e
),
)
});

bdf
};

for boot_entry in boot_names.iter() {
iximeow marked this conversation as resolved.
Show resolved Hide resolved
// Theoretically we could support booting from network devices by
// matching them here and adding their PCI paths, but exactly what
// would happen is ill-understood. So, only check disks here.
if let Some(spec) = self.spec.disks.get(boot_entry.name.as_str()) {
match &spec.device_spec {
StorageDevice::Virtio(disk) => {
let bdf = parse_bdf(&disk.pci_path)?;
// TODO: check that bus is 0. only support boot devices
iximeow marked this conversation as resolved.
Show resolved Hide resolved
// directly attached to the root bus for now.
order.add_disk(bdf.location);
}
StorageDevice::Nvme(disk) => {
let bdf = parse_bdf(&disk.pci_path)?;
// TODO: check that bus is 0. only support boot devices
// directly attached to the root bus for now.
//
// TODO: separately, propolis-standalone passes an eui64
// of 0, so do that here too. is that.. ok?
order.add_nvme(bdf.location, 0);
}
};
} else {
let message = format!(
"Instance spec included boot entry which does not refer to an existing disk: `{}`",
boot_entry.name.as_str(),
);
return Err(Error::new(ErrorKind::Other, message));
iximeow marked this conversation as resolved.
Show resolved Hide resolved
}
}

Ok(Some(order.finish()))
}

/// Initialize qemu `fw_cfg` device, and populate it with data including CPU
/// count, SMBIOS tables, and attached RAM-FB device.
///
Expand Down Expand Up @@ -1032,6 +1098,10 @@ impl<'a> MachineInitializer<'a> {
)
.unwrap();

if let Some(boot_order) = self.generate_bootorder()? {
fwcfg.insert_named("bootorder", boot_order).unwrap();
}

let ramfb = ramfb::RamFb::create(
self.log.new(slog::o!("component" => "ramfb")),
);
Expand Down
6 changes: 6 additions & 0 deletions bin/propolis-server/src/lib/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,12 @@ fn instance_spec_from_request(
spec_builder.add_disk_from_request(disk)?;
}

if let Some(boot_settings) = request.boot_settings.as_ref() {
for item in boot_settings.order.iter() {
spec_builder.add_boot_option(item)?;
}
}

if let Some(base64) = &request.cloud_init_bytes {
spec_builder.add_cloud_init_from_request(base64.clone())?;
}
Expand Down
6 changes: 6 additions & 0 deletions bin/propolis-server/src/lib/spec/api_spec_v0.rs
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,12 @@ impl TryFrom<InstanceSpecV0> for Spec {
return Err(ApiSpecError::BackendNotUsed(backend.to_owned()));
}

if let Some(boot_settings) = value.devices.boot_settings.as_ref() {
for item in boot_settings.order.iter() {
builder.add_boot_option(item)?;
}
}

for (name, serial_port) in value.devices.serial_ports {
builder.add_serial_port(name, serial_port.num)?;
}
Expand Down
22 changes: 21 additions & 1 deletion bin/propolis-server/src/lib/spec/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use propolis_api_types::{
},
PciPath,
},
DiskRequest, InstanceProperties, NetworkInterfaceRequest,
BootOrderEntry, DiskRequest, InstanceProperties, NetworkInterfaceRequest,
};
use thiserror::Error;

Expand Down Expand Up @@ -57,6 +57,9 @@ pub(crate) enum SpecBuilderError {

#[error("pvpanic device already specified")]
PvpanicInUse,

#[error("Boot option {0} is not an attached device")]
BootOptionMissing(String),
}

#[derive(Debug, Default)]
Expand Down Expand Up @@ -110,6 +113,23 @@ impl SpecBuilder {
Ok(())
}

/// Add a boot option to the boot option list of the spec under construction.
pub fn add_boot_option(
&mut self,
item: &BootOrderEntry,
) -> Result<(), SpecBuilderError> {
if !self.spec.disks.contains_key(item.name.as_str()) {
return Err(SpecBuilderError::BootOptionMissing(item.name.clone()));
}

let boot_order = self.spec.boot_order.get_or_insert(Vec::new());

boot_order
.push(crate::spec::BootOrderEntry { name: item.name.clone() });

Ok(())
}

/// Converts an HTTP API request to add a cloud-init disk to an instance
/// into device/backend entries in the spec under construction.
pub fn add_cloud_init_from_request(
Expand Down
6 changes: 6 additions & 0 deletions bin/propolis-server/src/lib/spec/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ pub(crate) struct Spec {
pub board: Board,
pub disks: HashMap<String, Disk>,
pub nics: HashMap<String, Nic>,
pub boot_order: Option<Vec<BootOrderEntry>>,
Copy link
Member

Choose a reason for hiding this comment

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

nitpicky: maybe worth a comment explaining the semantic difference between None and Some([])?


pub serial: HashMap<String, SerialPort>,

Expand All @@ -67,6 +68,11 @@ pub(crate) struct Spec {
pub softnpu: SoftNpu,
}

#[derive(Clone, Debug, Default)]
pub(crate) struct BootOrderEntry {
pub name: String,
}

/// Describes the device half of a [`Disk`].
#[derive(Clone, Debug)]
pub enum StorageDevice {
Expand Down
29 changes: 18 additions & 11 deletions bin/propolis-standalone/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -959,8 +959,19 @@ fn generate_smbios(params: SmbiosParams) -> anyhow::Result<smbios::TableBytes> {
Ok(smb_tables.commit())
}

fn generate_bootorder(config: &config::Config) -> anyhow::Result<fwcfg::Entry> {
let names = config.main.boot_order.as_ref().unwrap();
fn generate_bootorder(
config: &config::Config,
log: &slog::Logger,
) -> anyhow::Result<Option<fwcfg::Entry>> {
let Some(names) = config.main.boot_order.as_ref() else {
return Ok(None);
};

slog::info!(
log,
"Bootorder declared as {:?}",
config.main.boot_order.as_ref()
);

let mut order = fwcfg::formats::BootOrder::new();
for name in names.iter() {
Expand Down Expand Up @@ -994,7 +1005,7 @@ fn generate_bootorder(config: &config::Config) -> anyhow::Result<fwcfg::Entry> {
}
}
}
Ok(order.finish())
Ok(Some(order.finish()))
}

fn setup_instance(
Expand Down Expand Up @@ -1306,14 +1317,10 @@ fn setup_instance(

// It is "safe" to generate bootorder (if requested) now, given that PCI
// device configuration has been validated by preceding logic
if config.main.boot_order.is_some() {
fwcfg
.insert_named(
"bootorder",
generate_bootorder(&config)
.context("Failed to generate boot order")?,
)
.unwrap();
if let Some(boot_config) = generate_bootorder(&config, log)
.context("Failed to generate boot order")?
{
fwcfg.insert_named("bootorder", boot_config).unwrap();
}

fwcfg.attach(pio, &machine.acc_mem);
Expand Down
5 changes: 5 additions & 0 deletions crates/propolis-api-types/src/instance_spec/v0.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ pub struct DeviceSpecV0 {
#[serde(skip_serializing_if = "Option::is_none")]
pub qemu_pvpanic: Option<components::devices::QemuPvpanic>,

// Same backwards compatibility reasoning as above.
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub boot_settings: Option<crate::BootSettings>,

#[cfg(feature = "falcon")]
pub softnpu_pci_port: Option<components::devices::SoftNpuPciPort>,
#[cfg(feature = "falcon")]
Expand Down
15 changes: 15 additions & 0 deletions crates/propolis-api-types/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ pub struct InstanceEnsureRequest {
#[serde(default)]
pub disks: Vec<DiskRequest>,

#[serde(default)]
pub boot_settings: Option<BootSettings>,

pub migrate: Option<InstanceMigrateInitiateRequest>,

// base64 encoded cloud-init ISO
Expand Down Expand Up @@ -385,6 +388,18 @@ pub struct DiskAttachment {
pub state: DiskAttachmentState,
}

#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)]
pub struct BootSettings {
pub order: Vec<BootOrderEntry>,
}

/// An entry in a list of boot options.
#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)]
pub struct BootOrderEntry {
/// The name of the device to attempt booting from.
pub name: String,
}

/// A stable index which is translated by Propolis
/// into a PCI BDF, visible to the guest.
#[derive(Copy, Clone, Debug, Deserialize, Serialize, JsonSchema)]
Expand Down
26 changes: 26 additions & 0 deletions lib/propolis-client/src/instance_spec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,32 @@ impl SpecBuilderV0 {
}
}

/// Sets a boot order. Names here refer to devices included in this spec.
///
/// Permissible to not this if the implicit boot order is desired, but the implicit boot order
iximeow marked this conversation as resolved.
Show resolved Hide resolved
/// may be unstable across device addition and removal.
///
/// If any devices named in this order are not actually present in the constructed spec,
/// Propolis will return an error when the spec is provided.
///
/// XXX: this should certainly return `&mut Self` - all the builders here should. check if any
/// of these are chained..?
pub fn set_boot_order(
&mut self,
boot_order: Vec<String>,
) -> Result<&Self, SpecBuilderError> {
let boot_order = boot_order
.into_iter()
.map(|name| crate::types::BootOrderEntry { name })
.collect();

let settings = crate::types::BootSettings { order: boot_order };

self.spec.devices.boot_settings = Some(settings);

Ok(self)
}

/// Yields the completed spec, consuming the builder.
pub fn finish(self) -> InstanceSpecV0 {
self.spec
Expand Down
2 changes: 2 additions & 0 deletions lib/propolis-client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ progenitor::generate_api!(

// Some Crucible-related bits are re-exported through simulated
// sled-agent and thus require JsonSchema
BootOrderEntry = { derives = [schemars::JsonSchema] },
BootSettings = { derives = [schemars::JsonSchema] },
DiskRequest = { derives = [schemars::JsonSchema] },
VolumeConstructionRequest = { derives = [schemars::JsonSchema] },
CrucibleOpts = { derives = [schemars::JsonSchema] },
Expand Down
Loading
Loading