Skip to content

Commit f5437f6

Browse files
committed
wip: teach propolis-server to understand configurable boot order
1 parent b278193 commit f5437f6

File tree

11 files changed

+316
-42
lines changed

11 files changed

+316
-42
lines changed

bin/propolis-server/src/lib/initializer.rs

Lines changed: 135 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,18 @@ use propolis::hw::ibmpc;
2828
use propolis::hw::pci;
2929
use propolis::hw::ps2::ctrl::PS2Ctrl;
3030
use propolis::hw::qemu::pvpanic::QemuPvpanic;
31-
use propolis::hw::qemu::{debug::QemuDebugPort, fwcfg, ramfb};
31+
use propolis::hw::qemu::{
32+
debug::QemuDebugPort,
33+
fwcfg::{self, Entry},
34+
ramfb,
35+
};
3236
use propolis::hw::uart::LpcUart;
3337
use propolis::hw::{nvme, virtio};
3438
use propolis::intr_pins;
3539
use propolis::vmm::{self, Builder, Machine};
36-
use propolis_api_types::instance_spec::{self, v0::InstanceSpecV0};
40+
use propolis_api_types::instance_spec::{
41+
self, v0::BootDeclaration, v0::InstanceSpecV0,
42+
};
3743
use propolis_api_types::InstanceProperties;
3844
use slog::info;
3945

@@ -117,6 +123,7 @@ pub struct MachineInitializer<'a> {
117123
pub(crate) properties: &'a InstanceProperties,
118124
pub(crate) toml_config: &'a crate::server::VmTomlConfig,
119125
pub(crate) producer_registry: Option<ProducerRegistry>,
126+
pub(crate) boot_order: Option<Vec<BootDeclaration>>,
120127
pub(crate) state: MachineInitializerState,
121128
}
122129

@@ -994,6 +1001,121 @@ impl<'a> MachineInitializer<'a> {
9941001
smb_tables.commit()
9951002
}
9961003

1004+
fn generate_bootorder(&self) -> Result<Option<Entry>, Error> {
1005+
eprintln!(
1006+
"generating bootorder with order: {:?}",
1007+
self.boot_order.as_ref()
1008+
);
1009+
let Some(boot_names) = self.boot_order.as_ref() else {
1010+
return Ok(None);
1011+
};
1012+
1013+
let mut order = fwcfg::formats::BootOrder::new();
1014+
1015+
let parse_bdf =
1016+
|pci_path: &propolis_api_types::instance_spec::PciPath| {
1017+
let bdf: Result<pci::Bdf, Error> =
1018+
pci_path.to_owned().try_into().map_err(|e| {
1019+
Error::new(
1020+
ErrorKind::InvalidInput,
1021+
format!(
1022+
"Couldn't get PCI BDF for {}: {}",
1023+
pci_path, e
1024+
),
1025+
)
1026+
});
1027+
1028+
bdf
1029+
};
1030+
1031+
for boot_entry in boot_names.iter() {
1032+
if boot_entry.first_boot_only {
1033+
continue;
1034+
}
1035+
// names may refer to a storage device or network device.
1036+
//
1037+
// realistically we won't be booting from network devices, but leave that as a question
1038+
// for plumbing on the next layer up.
1039+
1040+
let storage_backend = |spec| {
1041+
let spec: &instance_spec::v0::StorageDeviceV0 = spec;
1042+
match spec {
1043+
instance_spec::v0::StorageDeviceV0::VirtioDisk(disk) => {
1044+
&disk.backend_name
1045+
}
1046+
instance_spec::v0::StorageDeviceV0::NvmeDisk(disk) => {
1047+
&disk.backend_name
1048+
}
1049+
}
1050+
};
1051+
1052+
let network_backend = |spec| {
1053+
let spec: &instance_spec::v0::NetworkDeviceV0 = spec;
1054+
let instance_spec::v0::NetworkDeviceV0::VirtioNic(vnic_spec) =
1055+
spec;
1056+
1057+
&vnic_spec.backend_name
1058+
};
1059+
1060+
if let Some(device_spec) =
1061+
self.spec.devices.storage_devices.iter().find_map(
1062+
|(_name, spec)| {
1063+
if storage_backend(spec) == &boot_entry.name {
1064+
Some(spec)
1065+
} else {
1066+
None
1067+
}
1068+
},
1069+
)
1070+
{
1071+
match device_spec {
1072+
instance_spec::v0::StorageDeviceV0::VirtioDisk(disk) => {
1073+
let bdf = parse_bdf(&disk.pci_path)?;
1074+
// TODO: check that bus is 0. only support boot devices
1075+
// directly attached to the root bus for now.
1076+
order.add_disk(bdf.location);
1077+
}
1078+
instance_spec::v0::StorageDeviceV0::NvmeDisk(disk) => {
1079+
let bdf = parse_bdf(&disk.pci_path)?;
1080+
// TODO: check that bus is 0. only support boot devices
1081+
// directly attached to the root bus for now.
1082+
//
1083+
// TODO: separately, propolis-standalone passes an eui64
1084+
// of 0, so do that here too. is that.. ok?
1085+
order.add_nvme(bdf.location, 0);
1086+
}
1087+
};
1088+
} else if let Some(vnic_spec) =
1089+
self.spec.devices.network_devices.iter().find_map(
1090+
|(name, spec)| {
1091+
if network_backend(spec) == &boot_entry.name {
1092+
Some(spec)
1093+
} else {
1094+
None
1095+
}
1096+
},
1097+
)
1098+
{
1099+
let instance_spec::v0::NetworkDeviceV0::VirtioNic(vnic_spec) =
1100+
vnic_spec;
1101+
1102+
let bdf = parse_bdf(&vnic_spec.pci_path)?;
1103+
1104+
order.add_pci(bdf.location, "ethernet");
1105+
} else {
1106+
eprintln!(
1107+
"could not find {:?} in {:?} or {:?}",
1108+
boot_entry,
1109+
self.spec.devices.storage_devices,
1110+
self.spec.devices.network_devices
1111+
);
1112+
panic!("TODO: return an error; the device name doesn't exist?");
1113+
}
1114+
}
1115+
1116+
Ok(Some(order.finish()))
1117+
}
1118+
9971119
/// Initialize qemu `fw_cfg` device, and populate it with data including CPU
9981120
/// count, SMBIOS tables, and attached RAM-FB device.
9991121
///
@@ -1010,6 +1132,13 @@ impl<'a> MachineInitializer<'a> {
10101132
)
10111133
.unwrap();
10121134

1135+
// TODO(ixi): extract `generate_smbios` and expect `smbios::TableBytes` as an argument to
1136+
// initialize_fwcfg. make `generate_smbios` accept rom size as an explicit parameter. this
1137+
// avoids the implicit panic if these are called out of order (and makes it harder to call
1138+
// out of order).
1139+
//
1140+
// one step towards sharing smbios init code between propolis-standalone and
1141+
// propolis-server.
10131142
let smbios::TableBytes { entry_point, structure_table } =
10141143
self.generate_smbios();
10151144
fwcfg
@@ -1025,6 +1154,10 @@ impl<'a> MachineInitializer<'a> {
10251154
)
10261155
.unwrap();
10271156

1157+
if let Some(boot_order) = self.generate_bootorder()? {
1158+
fwcfg.insert_named("bootorder", boot_order).unwrap();
1159+
}
1160+
10281161
let ramfb = ramfb::RamFb::create(
10291162
self.log.new(slog::o!("component" => "ramfb")),
10301163
);

bin/propolis-server/src/lib/vm/ensure.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ impl<'a> VmEnsureNotStarted<'a> {
177177
properties,
178178
toml_config: &options.toml_config,
179179
producer_registry: options.oximeter_registry.clone(),
180+
boot_order: v0_spec.devices.boot_order.clone(),
180181
state: MachineInitializerState::default(),
181182
};
182183

bin/propolis-standalone/src/config.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,9 @@ pub fn block_backend(
136136
log: &slog::Logger,
137137
) -> (Arc<dyn block::Backend>, String) {
138138
let backend_name = dev.options.get("block_dev").unwrap().as_str().unwrap();
139-
let be = config.block_devs.get(backend_name).unwrap();
139+
let Some(be) = config.block_devs.get(backend_name) else {
140+
panic!("no configured block device named \"{}\"", backend_name);
141+
};
140142
let opts = block::BackendOpts {
141143
block_size: be.block_opts.block_size,
142144
read_only: be.block_opts.read_only,

bin/propolis-standalone/src/main.rs

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -959,8 +959,13 @@ fn generate_smbios(params: SmbiosParams) -> anyhow::Result<smbios::TableBytes> {
959959
Ok(smb_tables.commit())
960960
}
961961

962-
fn generate_bootorder(config: &config::Config) -> anyhow::Result<fwcfg::Entry> {
963-
let names = config.main.boot_order.as_ref().unwrap();
962+
fn generate_bootorder(
963+
config: &config::Config,
964+
) -> anyhow::Result<Option<fwcfg::Entry>> {
965+
eprintln!("bootorder declared as {:?}", config.main.boot_order.as_ref());
966+
let Some(names) = config.main.boot_order.as_ref() else {
967+
return Ok(None);
968+
};
964969

965970
let mut order = fwcfg::formats::BootOrder::new();
966971
for name in names.iter() {
@@ -994,7 +999,7 @@ fn generate_bootorder(config: &config::Config) -> anyhow::Result<fwcfg::Entry> {
994999
}
9951000
}
9961001
}
997-
Ok(order.finish())
1002+
Ok(Some(order.finish()))
9981003
}
9991004

10001005
fn setup_instance(
@@ -1306,14 +1311,10 @@ fn setup_instance(
13061311

13071312
// It is "safe" to generate bootorder (if requested) now, given that PCI
13081313
// device configuration has been validated by preceding logic
1309-
if config.main.boot_order.is_some() {
1310-
fwcfg
1311-
.insert_named(
1312-
"bootorder",
1313-
generate_bootorder(&config)
1314-
.context("Failed to generate boot order")?,
1315-
)
1316-
.unwrap();
1314+
if let Some(boot_config) =
1315+
generate_bootorder(&config).context("Failed to generate boot order")?
1316+
{
1317+
fwcfg.insert_named("bootorder", boot_config).unwrap();
13171318
}
13181319

13191320
fwcfg.attach(pio, &machine.acc_mem);

crates/propolis-api-types/src/instance_spec/v0/mod.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,11 @@ pub struct DeviceSpecV0 {
109109
#[serde(skip_serializing_if = "Option::is_none")]
110110
pub qemu_pvpanic: Option<components::devices::QemuPvpanic>,
111111

112+
// same as above
113+
#[serde(default)]
114+
#[serde(skip_serializing_if = "Option::is_none")]
115+
pub boot_order: Option<Vec<BootDeclaration>>,
116+
112117
#[cfg(feature = "falcon")]
113118
pub softnpu_pci_port: Option<components::devices::SoftNpuPciPort>,
114119
#[cfg(feature = "falcon")]
@@ -177,6 +182,12 @@ impl DeviceSpecV0 {
177182
}
178183
}
179184

185+
#[derive(Clone, Deserialize, Serialize, Debug, JsonSchema)]
186+
pub struct BootDeclaration {
187+
pub name: SpecKey,
188+
pub first_boot_only: bool,
189+
}
190+
180191
#[derive(Clone, Deserialize, Serialize, Debug, JsonSchema)]
181192
#[serde(deny_unknown_fields, tag = "type", content = "component")]
182193
pub enum StorageBackendV0 {

lib/propolis-client/src/instance_spec.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,35 @@ impl SpecBuilderV0 {
184184
}
185185
}
186186

187+
/// Sets a boot order. Names here refer to devices included in this spec.
188+
///
189+
/// Permissible to not this if the implicit boot order is desired, but the implicit boot order
190+
/// may be unstable across device addition and removal.
191+
///
192+
/// XXX: talk about what happens if names are included that do not name real devices..?
193+
///
194+
/// XXX: this should certainly return `&mut Self` - all the builders here should. check if any
195+
/// of these are chained..?
196+
pub fn set_boot_order(
197+
&mut self,
198+
boot_order: Vec<String>,
199+
) -> Result<&Self, SpecBuilderError> {
200+
let boot_declarations = boot_order
201+
.into_iter()
202+
.map(|name| crate::types::BootDeclaration {
203+
name,
204+
first_boot_only: false,
205+
})
206+
.collect();
207+
eprintln!("setting boot order to {:?}", boot_declarations);
208+
self.spec.devices.boot_order = Some(boot_declarations);
209+
210+
// TODO: would be nice to warn if any of the devices named here are not devices in the spec
211+
// though.
212+
213+
Ok(self)
214+
}
215+
187216
/// Yields the completed spec, consuming the builder.
188217
pub fn finish(self) -> InstanceSpecV0 {
189218
self.spec

openapi/propolis-server.json

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -662,6 +662,13 @@
662662
"additionalProperties": {
663663
"$ref": "#/components/schemas/StorageDeviceV0"
664664
}
665+
},
666+
"boot_order": {
667+
"nullable": true,
668+
"type": "array",
669+
"items": {
670+
"$ref": "#/components/schemas/BootDeclaration"
671+
}
665672
}
666673
},
667674
"required": [
@@ -1463,6 +1470,24 @@
14631470
"vcr_matches"
14641471
]
14651472
},
1473+
"BootDeclaration": {
1474+
"description": "A boot preference for some device.",
1475+
"type": "object",
1476+
"properties": {
1477+
"name": {
1478+
"description": "The name of the device to attempt booting from.",
1479+
"type": "string"
1480+
},
1481+
"first_boot_only": {
1482+
"description": "Should we include this device in the boot order for reboots of a started instance?",
1483+
"type": "boolean"
1484+
}
1485+
},
1486+
"required": [
1487+
"name",
1488+
"first_boot_only"
1489+
]
1490+
},
14661491
"SerialPort": {
14671492
"description": "A serial port device.",
14681493
"type": "object",

0 commit comments

Comments
 (0)