openvmm_entry/
storage_builder.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Code to build storage configuration from command line arguments.
5
6use 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
58// Arbitrary but constant instance IDs to maintain the same device IDs
59// across reboots.
60const 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    /// Returns the "sub device path" for assigning this into Underhill, or
106    /// `None` if Underhill can't use this device as a source.
107    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            // TODO: once hvlite supports VTL2 with PCAT VTL0, remove this restriction.
247            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        // Add an empty VTL0 SCSI controller even if there are no configured disks.
293        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            // Tell UEFI to try to enumerate VPCI devices since there might be
345            // an NVMe namespace to boot from.
346            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}