1use crate::VmResources;
7use crate::cli_args::DiskCliKind;
8use crate::cli_args::UnderhillDiskSource;
9use crate::disk_open;
10use anyhow::Context;
11use guid::Guid;
12use hvlite_defs::config::Config;
13use hvlite_defs::config::DeviceVtl;
14use hvlite_defs::config::LoadMode;
15use hvlite_defs::config::VpciDeviceConfig;
16use ide_resources::GuestMedia;
17use ide_resources::IdeDeviceConfig;
18use ide_resources::IdePath;
19use nvme_resources::NamespaceDefinition;
20use nvme_resources::NvmeControllerHandle;
21use scsidisk_resources::SimpleScsiDiskHandle;
22use scsidisk_resources::SimpleScsiDvdHandle;
23use storvsp_resources::ScsiControllerHandle;
24use storvsp_resources::ScsiDeviceAndPath;
25use storvsp_resources::ScsiPath;
26use vm_resource::IntoResource;
27use vtl2_settings_proto::Lun;
28use vtl2_settings_proto::StorageController;
29use vtl2_settings_proto::storage_controller;
30
31pub(super) struct StorageBuilder {
32 vtl0_ide_disks: Vec<IdeDeviceConfig>,
33 vtl0_scsi_devices: Vec<ScsiDeviceAndPath>,
34 vtl2_scsi_devices: Vec<ScsiDeviceAndPath>,
35 vtl0_nvme_namespaces: Vec<NamespaceDefinition>,
36 vtl2_nvme_namespaces: Vec<NamespaceDefinition>,
37 underhill_scsi_luns: Vec<Lun>,
38 underhill_nvme_luns: Vec<Lun>,
39 openhcl_vtl: Option<DeviceVtl>,
40}
41
42#[derive(Copy, Clone)]
43pub enum DiskLocation {
44 Ide(Option<u8>, Option<u8>),
45 Scsi(Option<u8>),
46 Nvme(Option<u32>),
47}
48
49impl From<UnderhillDiskSource> for DiskLocation {
50 fn from(value: UnderhillDiskSource) -> Self {
51 match value {
52 UnderhillDiskSource::Scsi => Self::Scsi(None),
53 UnderhillDiskSource::Nvme => Self::Nvme(None),
54 }
55 }
56}
57
58const NVME_VTL0_INSTANCE_ID: Guid = guid::guid!("008091f6-9688-497d-9091-af347dc9173c");
61const NVME_VTL2_INSTANCE_ID: Guid = guid::guid!("f9b90f6f-b129-4596-8171-a23481b8f718");
62const SCSI_VTL0_INSTANCE_ID: Guid = guid::guid!("ba6163d9-04a1-4d29-b605-72e2ffb1dc7f");
63const SCSI_VTL2_INSTANCE_ID: Guid = guid::guid!("73d3aa59-b82b-4fe7-9e15-e2b0b5575cf8");
64const UNDERHILL_VTL0_SCSI_INSTANCE: Guid = guid::guid!("e1c5bd94-d0d6-41d4-a2b0-88095a16ded7");
65const UNDERHILL_VTL0_NVME_INSTANCE: Guid = guid::guid!("09a59b81-2bf6-4164-81d7-3a0dc977ba65");
66
67impl StorageBuilder {
68 pub fn new(openhcl_vtl: Option<DeviceVtl>) -> Self {
69 Self {
70 vtl0_ide_disks: Vec::new(),
71 vtl0_scsi_devices: Vec::new(),
72 vtl2_scsi_devices: Vec::new(),
73 vtl0_nvme_namespaces: Vec::new(),
74 vtl2_nvme_namespaces: Vec::new(),
75 underhill_scsi_luns: Vec::new(),
76 underhill_nvme_luns: Vec::new(),
77 openhcl_vtl,
78 }
79 }
80
81 pub fn has_vtl0_nvme(&self) -> bool {
82 !self.vtl0_nvme_namespaces.is_empty() || !self.underhill_nvme_luns.is_empty()
83 }
84
85 pub fn add(
86 &mut self,
87 vtl: DeviceVtl,
88 underhill: Option<UnderhillDiskSource>,
89 target: DiskLocation,
90 kind: &DiskCliKind,
91 is_dvd: bool,
92 read_only: bool,
93 ) -> anyhow::Result<()> {
94 if let Some(source) = underhill {
95 if vtl != DeviceVtl::Vtl0 {
96 anyhow::bail!("underhill can only offer devices to vtl0");
97 }
98 self.add_underhill(source.into(), target, kind, is_dvd, read_only)?;
99 } else {
100 self.add_inner(vtl, target, kind, is_dvd, read_only)?;
101 }
102 Ok(())
103 }
104
105 fn add_inner(
108 &mut self,
109 vtl: DeviceVtl,
110 target: DiskLocation,
111 kind: &DiskCliKind,
112 is_dvd: bool,
113 read_only: bool,
114 ) -> anyhow::Result<Option<u32>> {
115 let disk = disk_open(kind, read_only || is_dvd)?;
116 let location = match target {
117 DiskLocation::Ide(channel, device) => {
118 let guest_media = if is_dvd {
119 GuestMedia::Dvd(
120 SimpleScsiDvdHandle {
121 media: Some(disk),
122 requests: None,
123 }
124 .into_resource(),
125 )
126 } else {
127 GuestMedia::Disk {
128 disk_type: disk,
129 read_only,
130 disk_parameters: None,
131 }
132 };
133
134 let check = |c: u8, d: u8| {
135 channel.unwrap_or(c) == c
136 && device.unwrap_or(d) == d
137 && !self
138 .vtl0_ide_disks
139 .iter()
140 .any(|cfg| cfg.path.channel == c && cfg.path.drive == d)
141 };
142
143 let (channel, device) = (0..=1)
144 .flat_map(|c| std::iter::repeat(c).zip(0..=1))
145 .find(|&(c, d)| check(c, d))
146 .context("no free ide slots")?;
147
148 if vtl != DeviceVtl::Vtl0 {
149 anyhow::bail!("ide only supported for VTL0");
150 }
151 self.vtl0_ide_disks.push(IdeDeviceConfig {
152 path: IdePath {
153 channel,
154 drive: device,
155 },
156 guest_media,
157 });
158 None
159 }
160 DiskLocation::Scsi(lun) => {
161 let device = if is_dvd {
162 SimpleScsiDvdHandle {
163 media: Some(disk),
164 requests: None,
165 }
166 .into_resource()
167 } else {
168 SimpleScsiDiskHandle {
169 disk,
170 read_only,
171 parameters: Default::default(),
172 }
173 .into_resource()
174 };
175 let devices = match vtl {
176 DeviceVtl::Vtl0 => &mut self.vtl0_scsi_devices,
177 DeviceVtl::Vtl1 => anyhow::bail!("vtl1 unsupported"),
178 DeviceVtl::Vtl2 => &mut self.vtl2_scsi_devices,
179 };
180 let lun = lun.unwrap_or(devices.len() as u8);
181 devices.push(ScsiDeviceAndPath {
182 path: ScsiPath {
183 path: 0,
184 target: 0,
185 lun,
186 },
187 device,
188 });
189 Some(lun.into())
190 }
191 DiskLocation::Nvme(nsid) => {
192 let namespaces = match vtl {
193 DeviceVtl::Vtl0 => &mut self.vtl0_nvme_namespaces,
194 DeviceVtl::Vtl1 => anyhow::bail!("vtl1 unsupported"),
195 DeviceVtl::Vtl2 => &mut self.vtl2_nvme_namespaces,
196 };
197 if is_dvd {
198 anyhow::bail!("dvd not supported with nvme");
199 }
200 let nsid = nsid.unwrap_or(namespaces.len() as u32 + 1);
201 namespaces.push(NamespaceDefinition {
202 nsid,
203 disk,
204 read_only,
205 });
206 Some(nsid)
207 }
208 };
209 Ok(location)
210 }
211
212 fn add_underhill(
213 &mut self,
214 source: DiskLocation,
215 target: DiskLocation,
216 kind: &DiskCliKind,
217 is_dvd: bool,
218 read_only: bool,
219 ) -> anyhow::Result<()> {
220 let vtl = self.openhcl_vtl.context("openhcl not configured")?;
221 let sub_device_path = self
222 .add_inner(vtl, source, kind, is_dvd, read_only)?
223 .context("source device not supported by underhill")?;
224
225 let (device_type, device_path) = match source {
226 DiskLocation::Ide(_, _) => anyhow::bail!("ide source not supported for Underhill"),
227 DiskLocation::Scsi(_) => (
228 vtl2_settings_proto::physical_device::DeviceType::Vscsi,
229 if vtl == DeviceVtl::Vtl2 {
230 SCSI_VTL2_INSTANCE_ID
231 } else {
232 SCSI_VTL0_INSTANCE_ID
233 },
234 ),
235 DiskLocation::Nvme(_) => (
236 vtl2_settings_proto::physical_device::DeviceType::Nvme,
237 if vtl == DeviceVtl::Vtl2 {
238 NVME_VTL2_INSTANCE_ID
239 } else {
240 NVME_VTL0_INSTANCE_ID
241 },
242 ),
243 };
244
245 let (luns, location) = match target {
246 DiskLocation::Ide(_, _) => {
248 anyhow::bail!("ide target currently not supported for Underhill (no PCAT support)")
249 }
250 DiskLocation::Scsi(lun) => {
251 let lun = lun.unwrap_or(self.underhill_scsi_luns.len() as u8);
252 (&mut self.underhill_scsi_luns, lun.into())
253 }
254 DiskLocation::Nvme(nsid) => {
255 let nsid = nsid.unwrap_or(self.underhill_nvme_luns.len() as u32 + 1);
256 (&mut self.underhill_nvme_luns, nsid)
257 }
258 };
259
260 luns.push(Lun {
261 location,
262 device_id: Guid::new_random().to_string(),
263 vendor_id: "OpenVMM".to_string(),
264 product_id: "Disk".to_string(),
265 product_revision_level: "1.0".to_string(),
266 serial_number: "0".to_string(),
267 model_number: "1".to_string(),
268 physical_devices: Some(vtl2_settings_proto::PhysicalDevices {
269 r#type: vtl2_settings_proto::physical_devices::BackingType::Single.into(),
270 device: Some(vtl2_settings_proto::PhysicalDevice {
271 device_type: device_type.into(),
272 device_path: device_path.to_string(),
273 sub_device_path,
274 }),
275 devices: Vec::new(),
276 }),
277 is_dvd,
278 ..Default::default()
279 });
280
281 Ok(())
282 }
283
284 pub fn build_config(
285 &mut self,
286 config: &mut Config,
287 resources: &mut VmResources,
288 scsi_sub_channels: u16,
289 ) -> anyhow::Result<()> {
290 config.ide_disks.append(&mut self.vtl0_ide_disks);
291
292 if !self.vtl0_scsi_devices.is_empty() || config.vmbus.is_some() {
294 let (send, recv) = mesh::channel();
295 config.vmbus_devices.push((
296 DeviceVtl::Vtl0,
297 ScsiControllerHandle {
298 instance_id: SCSI_VTL0_INSTANCE_ID,
299 max_sub_channel_count: scsi_sub_channels,
300 devices: std::mem::take(&mut self.vtl0_scsi_devices),
301 io_queue_depth: None,
302 requests: Some(recv),
303 }
304 .into_resource(),
305 ));
306 resources.scsi_rpc = Some(send);
307 }
308
309 if !self.vtl2_scsi_devices.is_empty() {
310 if config
311 .hypervisor
312 .with_vtl2
313 .as_ref()
314 .is_none_or(|c| c.vtl0_alias_map)
315 {
316 anyhow::bail!("must specify --vtl2 and --no-alias-map to offer disks to VTL2");
317 }
318 config.vmbus_devices.push((
319 DeviceVtl::Vtl2,
320 ScsiControllerHandle {
321 instance_id: SCSI_VTL2_INSTANCE_ID,
322 max_sub_channel_count: scsi_sub_channels,
323 devices: std::mem::take(&mut self.vtl2_scsi_devices),
324 io_queue_depth: None,
325 requests: None,
326 }
327 .into_resource(),
328 ));
329 }
330
331 if !self.vtl0_nvme_namespaces.is_empty() {
332 config.vpci_devices.push(VpciDeviceConfig {
333 vtl: DeviceVtl::Vtl0,
334 instance_id: NVME_VTL0_INSTANCE_ID,
335 resource: NvmeControllerHandle {
336 subsystem_id: NVME_VTL0_INSTANCE_ID,
337 namespaces: std::mem::take(&mut self.vtl0_nvme_namespaces),
338 max_io_queues: 64,
339 msix_count: 64,
340 }
341 .into_resource(),
342 });
343
344 if let LoadMode::Uefi {
347 enable_vpci_boot: vpci_boot,
348 ..
349 } = &mut config.load_mode
350 {
351 *vpci_boot = true;
352 }
353 }
354
355 if !self.vtl2_nvme_namespaces.is_empty() {
356 if config
357 .hypervisor
358 .with_vtl2
359 .as_ref()
360 .is_none_or(|c| c.vtl0_alias_map)
361 {
362 anyhow::bail!("must specify --vtl2 and --no-alias-map to offer disks to VTL2");
363 }
364 config.vpci_devices.push(VpciDeviceConfig {
365 vtl: DeviceVtl::Vtl2,
366 instance_id: NVME_VTL2_INSTANCE_ID,
367 resource: NvmeControllerHandle {
368 subsystem_id: NVME_VTL2_INSTANCE_ID,
369 namespaces: std::mem::take(&mut self.vtl2_nvme_namespaces),
370 max_io_queues: 64,
371 msix_count: 64,
372 }
373 .into_resource(),
374 });
375 }
376
377 Ok(())
378 }
379
380 pub fn build_underhill(&self) -> Vec<StorageController> {
381 let mut storage_controllers = Vec::new();
382 if !self.underhill_scsi_luns.is_empty() {
383 let controller = StorageController {
384 instance_id: UNDERHILL_VTL0_SCSI_INSTANCE.to_string(),
385 protocol: storage_controller::StorageProtocol::Scsi.into(),
386 luns: self.underhill_scsi_luns.clone(),
387 io_queue_depth: None,
388 };
389 storage_controllers.push(controller);
390 }
391
392 if !self.underhill_nvme_luns.is_empty() {
393 let controller = StorageController {
394 instance_id: UNDERHILL_VTL0_NVME_INSTANCE.to_string(),
395 protocol: storage_controller::StorageProtocol::Nvme.into(),
396 luns: self.underhill_nvme_luns.clone(),
397 io_queue_depth: None,
398 };
399 storage_controllers.push(controller);
400 }
401
402 storage_controllers
403 }
404}