underhill_config/schema/
v1.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! OpenHCL configuration schema V1
5//!
6//! The schema defined in this file is for VTL2 settings. This schema is a protocol between
7//! OpenHCL and client like Azure agent, which is opaque to WMI, so there is no corresponding
8//! definitions in MARS files.
9
10use super::ParseSchemaExt;
11use crate::Vtl2SettingsErrorCode;
12use crate::Vtl2SettingsErrorInfo;
13use crate::errors::ErrorContext;
14use crate::errors::ParseErrors;
15use crate::schema::ParseResultExt;
16use crate::schema::ParseSchema;
17use crate::schema::ParsingStopped;
18use guid::Guid;
19use physical_device::DeviceType;
20use std::error::Error as _;
21use std::fmt::Write;
22use storage_controller::StorageProtocol;
23use thiserror::Error;
24use vtl2_settings_proto::*;
25
26pub(crate) const NAMESPACE_BASE: &str = "Base";
27pub(crate) const NAMESPACE_NETWORK_DEVICE: &str = "NetworkDevice";
28pub(crate) const NAMESPACE_NETWORK_ACCELERATION: &str = "NetworkAcceleration";
29
30#[derive(Error, Debug)]
31pub(crate) enum Error<'a> {
32    #[error("unsupported schema version {0:#x}")]
33    UnsupportedSchemaVersion(u32),
34    #[error("unsupported schema namespace {0}")]
35    UnsupportedSchemaNamespace(&'a str),
36    #[error("empty namespace settings chunk {0}")]
37    EmptyNamespaceChunk(&'a str),
38    #[error("invalid instance ID '{0}'")]
39    InvalidInstanceId(&'a str, #[source] guid::ParseError),
40    #[error("invalid ntfs guid '{0}'")]
41    InvalidNtfsGuid(&'a str, #[source] guid::ParseError),
42    #[error("controller already exists")]
43    StorageControllerGuidAlreadyExists,
44    #[error("disk location exceeds limits {limits:?}")]
45    StorageLunLocationExceedsMaxLimits { limits: u32 },
46    #[error("exceeded 4 max SCSI controllers")]
47    StorageScsiControllerExceedsMaxLimits,
48    #[error("disk location is duplicated")]
49    StorageLunLocationDuplicated,
50    #[error("NVMe namespace id is invalid")]
51    StorageLunNvmeNsidInvalid,
52    #[error("NVMe namespace can't be a DVD drive")]
53    StorageLunNvmeDvdUnsupported,
54    #[error("build-to-build schema compat error: {0}")]
55    StorageSchemaVersionMismatch(&'a str),
56    #[error("invalid physical disk count: physical_disk_count = {physical_disk_count}")]
57    StorageInvalidPhysicalDiskCount { physical_disk_count: usize },
58    #[error("ide controller channel not provided")]
59    StorageIdeChannelNotProvided,
60    #[error("ide controller channel exceeds 2 max channels")]
61    StorageIdeChannelExceedsMaxLimits,
62    #[error("ide controller channel exceeds 1 max controller")]
63    StorageIdeControllerExceedsMaxLimits,
64    #[error("ide controller location exceeds 2 allowed drives per channel")]
65    StorageIdeLocationExceedsMaxLimits,
66    #[error("ide controller has invalid configuration")]
67    StorageIdeChannelInvalidConfiguration,
68    #[error("controller has unknown storage protocol")]
69    StorageProtocolUnknown,
70    #[error("invalid device type")]
71    StorageInvalidDeviceType,
72}
73
74impl Error<'_> {
75    fn code(&self) -> Vtl2SettingsErrorCode {
76        match self {
77            Error::UnsupportedSchemaVersion(_) => Vtl2SettingsErrorCode::UnsupportedSchemaVersion,
78            Error::UnsupportedSchemaNamespace(_) => {
79                Vtl2SettingsErrorCode::UnsupportedSchemaNamespace
80            }
81            Error::EmptyNamespaceChunk(_) => Vtl2SettingsErrorCode::EmptyNamespaceChunk,
82            Error::InvalidInstanceId { .. } => Vtl2SettingsErrorCode::InvalidInstanceId,
83            Error::InvalidNtfsGuid(_, _) => Vtl2SettingsErrorCode::StorageInvalidNtfsFormatGuid,
84            Error::StorageControllerGuidAlreadyExists => {
85                Vtl2SettingsErrorCode::StorageControllerGuidAlreadyExists
86            }
87            Error::StorageLunLocationExceedsMaxLimits { .. } => {
88                Vtl2SettingsErrorCode::StorageLunLocationExceedsMaxLimits
89            }
90            Error::StorageScsiControllerExceedsMaxLimits => {
91                Vtl2SettingsErrorCode::StorageScsiControllerExceedsMaxLimits
92            }
93            Error::StorageLunLocationDuplicated { .. } => {
94                Vtl2SettingsErrorCode::StorageLunLocationDuplicated
95            }
96            Error::StorageLunNvmeNsidInvalid { .. } => {
97                Vtl2SettingsErrorCode::StorageLunLocationExceedsMaxLimits
98            }
99            Error::StorageLunNvmeDvdUnsupported { .. } => {
100                Vtl2SettingsErrorCode::StorageUnsupportedDeviceType
101            }
102            Error::StorageSchemaVersionMismatch(_) => Vtl2SettingsErrorCode::JsonFormatError,
103            Error::StorageInvalidPhysicalDiskCount { .. } => {
104                Vtl2SettingsErrorCode::StorageInvalidPhysicalDiskCount
105            }
106            Error::StorageIdeChannelNotProvided => {
107                Vtl2SettingsErrorCode::StorageIdeChannelNotProvided
108            }
109            Error::StorageIdeChannelExceedsMaxLimits => {
110                Vtl2SettingsErrorCode::StorageIdeChannelExceedsMaxLimits
111            }
112            Error::StorageIdeControllerExceedsMaxLimits => {
113                Vtl2SettingsErrorCode::StorageIdeChannelExceedsMaxLimits
114            }
115            Error::StorageIdeLocationExceedsMaxLimits => {
116                Vtl2SettingsErrorCode::StorageIdeChannelExceedsMaxLimits
117            }
118            Error::StorageIdeChannelInvalidConfiguration => {
119                Vtl2SettingsErrorCode::StorageIdeChannelInvalidConfiguration
120            }
121            Error::StorageProtocolUnknown => Vtl2SettingsErrorCode::StorageInvalidControllerType,
122            Error::StorageInvalidDeviceType => Vtl2SettingsErrorCode::StorageUnsupportedDeviceType,
123        }
124    }
125}
126
127impl From<Error<'_>> for Vtl2SettingsErrorInfo {
128    #[track_caller]
129    fn from(e: Error<'_>) -> Vtl2SettingsErrorInfo {
130        // Format the message manually to get the full error string (including
131        // error sources).
132        let mut message = e.to_string();
133        let mut source = e.source();
134        while let Some(inner) = source {
135            write!(&mut message, ": {}", inner).unwrap();
136            source = inner.source();
137        }
138
139        Vtl2SettingsErrorInfo::new(e.code(), message)
140    }
141}
142
143pub(crate) fn validate_version(
144    version: i32,
145    errors: &mut ParseErrors<'_>,
146) -> Result<(), ParsingStopped> {
147    match vtl2_settings_base::Version::from_i32(version)
148        .unwrap_or(vtl2_settings_base::Version::Unknown)
149    {
150        vtl2_settings_base::Version::Unknown => {
151            errors.push(Error::UnsupportedSchemaVersion(version as u32));
152        }
153        vtl2_settings_base::Version::V1 => {}
154    }
155    Ok(())
156}
157
158fn parse_instance_id(instance_id: &str) -> Result<Guid, Error<'_>> {
159    instance_id
160        .parse()
161        .map_err(|err| Error::InvalidInstanceId(instance_id, err))
162}
163
164fn parse_ntfs_guid(ntfs_guid: Option<&str>) -> Result<Option<Guid>, Error<'_>> {
165    ntfs_guid
166        .map(|guid| {
167            guid.parse()
168                .map_err(|err| Error::InvalidNtfsGuid(guid, err))
169        })
170        .transpose()
171}
172
173fn check_dups(errors: &mut ParseErrors<'_>, iter: impl IntoIterator<Item = u32>) {
174    let mut v: Vec<_> = iter.into_iter().collect();
175    v.sort();
176    for (a, b) in v.iter().zip(v.iter().skip(1)) {
177        if a == b {
178            errors.push(Error::StorageLunLocationDuplicated);
179        }
180    }
181}
182
183impl ParseSchema<crate::DeviceType> for DeviceType {
184    fn parse_schema(
185        &self,
186        _errors: &mut ParseErrors<'_>,
187    ) -> Result<crate::DeviceType, ParsingStopped> {
188        match self {
189            DeviceType::Nvme => Ok(crate::DeviceType::NVMe),
190            DeviceType::Vscsi => Ok(crate::DeviceType::VScsi),
191            DeviceType::Unknown => Err(Error::StorageInvalidDeviceType.into()),
192        }
193    }
194}
195
196impl ParseSchema<crate::PhysicalDevice> for PhysicalDevice {
197    fn parse_schema(
198        &self,
199        errors: &mut ParseErrors<'_>,
200    ) -> Result<crate::PhysicalDevice, ParsingStopped> {
201        Ok(crate::PhysicalDevice {
202            device_type: self.device_type().parse(errors)?,
203            vmbus_instance_id: parse_instance_id(&self.device_path)?,
204            sub_device_path: self.sub_device_path,
205        })
206    }
207}
208
209impl ParseSchema<crate::DiskParameters> for Lun {
210    fn parse_schema(
211        &self,
212        _errors: &mut ParseErrors<'_>,
213    ) -> Result<crate::DiskParameters, ParsingStopped> {
214        Ok(crate::DiskParameters {
215            device_id: self.device_id.clone(),
216            vendor_id: self.vendor_id.clone(),
217            product_id: self.product_id.clone(),
218            product_revision_level: self.product_revision_level.clone(),
219            serial_number: self.serial_number.clone(),
220            model_number: self.model_number.clone(),
221            medium_rotation_rate: self.medium_rotation_rate.map(|x| x as u16).unwrap_or(0),
222            physical_sector_size: self.physical_sector_size,
223            fua: self.fua,
224            write_cache: self.write_cache,
225            scsi_disk_size_in_bytes: self.scsi_disk_size_in_bytes,
226            odx: self.odx,
227            unmap: self.disable_thin_provisioning.map(|disable| !disable),
228            max_transfer_length: self.max_transfer_length.map(|x| x as usize),
229        })
230    }
231}
232
233impl ParseSchema<crate::PhysicalDevices> for Lun {
234    fn parse_schema(
235        &self,
236        errors: &mut ParseErrors<'_>,
237    ) -> Result<crate::PhysicalDevices, ParsingStopped> {
238        #[expect(deprecated)]
239        if (self.is_dvd || self.physical_devices.is_some())
240            && (self.device_type.is_some()
241                || self.device_path.is_some()
242                || self.sub_device_path.is_some())
243        {
244            errors.push(Error::StorageSchemaVersionMismatch(
245                "cannot mix old/new physical device schema declarations",
246            ));
247        }
248
249        let v = if let Some(physical_devices) = &self.physical_devices {
250            let invalid_disk_count = || Error::StorageInvalidPhysicalDiskCount {
251                physical_disk_count: physical_devices.device.is_some() as usize
252                    + physical_devices.devices.len(),
253            };
254
255            match physical_devices.r#type() {
256                physical_devices::BackingType::Single => {
257                    let device = physical_devices
258                        .device
259                        .as_ref()
260                        .ok_or_else(invalid_disk_count)?;
261                    if !physical_devices.devices.is_empty() {
262                        errors.push(invalid_disk_count());
263                    }
264                    crate::PhysicalDevices::Single {
265                        device: device.parse(errors)?,
266                    }
267                }
268                physical_devices::BackingType::Striped => {
269                    if physical_devices.devices.len() < 2 || physical_devices.device.is_some() {
270                        errors.push(invalid_disk_count());
271                    }
272                    crate::PhysicalDevices::Striped {
273                        devices: physical_devices
274                            .devices
275                            .iter()
276                            .flat_map(|v| v.parse(errors).collect_error(errors))
277                            .collect(),
278                        chunk_size_in_kb: self.chunk_size_in_kb,
279                    }
280                }
281                physical_devices::BackingType::Unknown => {
282                    return Err(Error::StorageInvalidDeviceType.into());
283                }
284            }
285        } else if self.is_dvd {
286            crate::PhysicalDevices::EmptyDrive
287        } else {
288            // Legacy compat path.
289            #[expect(deprecated)]
290            let (device_path, sub_device_path) =
291                self.device_path.as_ref().zip(self.sub_device_path).ok_or(
292                    Error::StorageSchemaVersionMismatch("could not find any physical devices"),
293                )?;
294
295            let device_type = self.device_type().parse(errors)?;
296            let vmbus_instance_id = parse_instance_id(device_path)?;
297            crate::PhysicalDevices::Single {
298                device: crate::PhysicalDevice {
299                    device_type,
300                    vmbus_instance_id,
301                    sub_device_path,
302                },
303            }
304        };
305        Ok(v)
306    }
307}
308
309impl ParseSchema<crate::IdeDisk> for Lun {
310    fn parse_schema(&self, errors: &mut ParseErrors<'_>) -> Result<crate::IdeDisk, ParsingStopped> {
311        let channel = self.channel.ok_or(Error::StorageIdeChannelNotProvided)?;
312        errors.with_context(ErrorContext::Ide(channel, self.location), |errors| {
313            if channel >= crate::IDE_NUM_CHANNELS.into() {
314                return Err(Error::StorageIdeChannelExceedsMaxLimits.into());
315            }
316
317            if self.location >= crate::IDE_MAX_DRIVES_PER_CHANNEL.into() {
318                return Err(Error::StorageIdeLocationExceedsMaxLimits.into());
319            }
320
321            Ok(crate::IdeDisk {
322                channel: channel as u8,
323                location: self.location as u8,
324                disk_params: self.parse(errors)?,
325                physical_devices: self.parse(errors)?,
326                ntfs_guid: parse_ntfs_guid(self.ntfs_guid.as_deref())?,
327                is_dvd: self.is_dvd,
328            })
329        })
330    }
331}
332
333impl ParseSchema<crate::ScsiDisk> for Lun {
334    fn parse_schema(
335        &self,
336        errors: &mut ParseErrors<'_>,
337    ) -> Result<crate::ScsiDisk, ParsingStopped> {
338        errors.with_context(ErrorContext::Scsi(self.location), |errors| {
339            if self.location as usize >= crate::SCSI_LUN_NUM {
340                errors.push(Error::StorageLunLocationExceedsMaxLimits {
341                    limits: crate::SCSI_LUN_NUM as u32,
342                });
343            }
344            Ok(crate::ScsiDisk {
345                location: self.location as u8,
346                disk_params: self.parse(errors)?,
347                physical_devices: self.parse(errors)?,
348                ntfs_guid: parse_ntfs_guid(self.ntfs_guid.as_deref())?,
349                is_dvd: self.is_dvd,
350            })
351        })
352    }
353}
354
355impl ParseSchema<crate::NvmeNamespace> for Lun {
356    fn parse_schema(
357        &self,
358        errors: &mut ParseErrors<'_>,
359    ) -> Result<crate::NvmeNamespace, ParsingStopped> {
360        errors.with_context(ErrorContext::Nvme(self.location), |errors| {
361            if self.location == 0 || self.location == !0 {
362                errors.push(Error::StorageLunNvmeNsidInvalid);
363            }
364            if self.is_dvd {
365                errors.push(Error::StorageLunNvmeDvdUnsupported);
366            }
367
368            Ok(crate::NvmeNamespace {
369                nsid: self.location,
370                disk_params: self.parse(errors)?,
371                physical_devices: self.parse(errors)?,
372            })
373        })
374    }
375}
376
377impl ParseSchema<crate::IdeController> for StorageController {
378    fn parse_schema(
379        &self,
380        errors: &mut ParseErrors<'_>,
381    ) -> Result<crate::IdeController, ParsingStopped> {
382        let instance_id = parse_instance_id(&self.instance_id)?;
383
384        errors.with_context(ErrorContext::InstanceId(instance_id), |errors| {
385            let mut disks = self
386                .luns
387                .iter()
388                .flat_map(|lun| lun.parse(errors).collect_error(errors))
389                .collect::<Vec<crate::IdeDisk>>();
390
391            disks.sort_by(|a, b| (a.location).cmp(&b.location));
392
393            for (a, b) in disks.iter().zip(disks.iter().skip(1)) {
394                if a.channel == b.channel && a.location == b.location {
395                    // Only 1 disk can be attached to a single slot.
396                    errors.push_with_context(
397                        ErrorContext::Ide(a.channel.into(), a.location.into()),
398                        Error::StorageIdeChannelInvalidConfiguration,
399                    );
400                }
401            }
402
403            Ok(crate::IdeController {
404                instance_id,
405                disks,
406                io_queue_depth: self.io_queue_depth,
407            })
408        })
409    }
410}
411
412impl ParseSchema<crate::ScsiController> for StorageController {
413    fn parse_schema(
414        &self,
415        errors: &mut ParseErrors<'_>,
416    ) -> Result<crate::ScsiController, ParsingStopped> {
417        let instance_id = parse_instance_id(&self.instance_id)?;
418
419        errors.with_context(ErrorContext::InstanceId(instance_id), |errors| {
420            let disks = self
421                .luns
422                .iter()
423                .flat_map(|lun| lun.parse(errors).collect_error(errors))
424                .collect::<Vec<crate::ScsiDisk>>();
425
426            check_dups(errors, disks.iter().map(|disk| disk.location.into()));
427
428            Ok(crate::ScsiController {
429                instance_id,
430                disks,
431                io_queue_depth: self.io_queue_depth,
432            })
433        })
434    }
435}
436
437impl ParseSchema<crate::NvmeController> for StorageController {
438    fn parse_schema(
439        &self,
440        errors: &mut ParseErrors<'_>,
441    ) -> Result<crate::NvmeController, ParsingStopped> {
442        assert!(matches!(self.protocol(), StorageProtocol::Nvme));
443
444        let instance_id = parse_instance_id(&self.instance_id)?;
445
446        errors.with_context(ErrorContext::InstanceId(instance_id), |errors| {
447            let namespaces = self
448                .luns
449                .iter()
450                .flat_map(|lun| lun.parse(errors).collect_error(errors))
451                .collect::<Vec<crate::NvmeNamespace>>();
452
453            check_dups(errors, namespaces.iter().map(|ns| ns.nsid));
454
455            Ok(crate::NvmeController {
456                instance_id,
457                namespaces,
458            })
459        })
460    }
461}
462
463impl ParseSchema<crate::NicDevice> for NicDeviceLegacy {
464    fn parse_schema(
465        &self,
466        _errors: &mut ParseErrors<'_>,
467    ) -> Result<crate::NicDevice, ParsingStopped> {
468        let instance_id = parse_instance_id(&self.instance_id)?;
469
470        let mut subordinate_instance_id = self
471            .subordinate_instance_id
472            .as_ref()
473            .map(|id| parse_instance_id(id))
474            .transpose()?;
475
476        if subordinate_instance_id == Some(Guid::ZERO) {
477            subordinate_instance_id = None;
478        }
479
480        Ok(crate::NicDevice {
481            instance_id,
482            subordinate_instance_id,
483            max_sub_channels: self.max_sub_channels.map(|val| val as u16),
484        })
485    }
486}
487
488impl ParseSchema<crate::NicDevice> for NicDevice {
489    fn parse_schema(
490        &self,
491        _errors: &mut ParseErrors<'_>,
492    ) -> Result<crate::NicDevice, ParsingStopped> {
493        let instance_id = parse_instance_id(&self.instance_id)?;
494
495        Ok(crate::NicDevice {
496            instance_id,
497            subordinate_instance_id: None,
498            max_sub_channels: self.max_sub_channels.map(|val| val as u16),
499        })
500    }
501}
502
503impl ParseSchema<crate::NicDevice> for NicAcceleration {
504    fn parse_schema(
505        &self,
506        _errors: &mut ParseErrors<'_>,
507    ) -> Result<crate::NicDevice, ParsingStopped> {
508        let instance_id = parse_instance_id(&self.instance_id)?;
509
510        let subordinate_instance_id = parse_instance_id(&self.subordinate_instance_id)?;
511        let subordinate_instance_id =
512            (subordinate_instance_id != Guid::ZERO).then_some(subordinate_instance_id);
513
514        Ok(crate::NicDevice {
515            instance_id,
516            subordinate_instance_id,
517            max_sub_channels: None,
518        })
519    }
520}
521
522impl ParseSchema<crate::Vtl2SettingsFixed> for Vtl2SettingsFixed {
523    fn parse_schema(
524        &self,
525        _errors: &mut ParseErrors<'_>,
526    ) -> Result<crate::Vtl2SettingsFixed, ParsingStopped> {
527        Ok(crate::Vtl2SettingsFixed {
528            scsi_sub_channels: self.scsi_sub_channels.map_or(0, |x| x as u16),
529            io_ring_size: self.io_ring_size.unwrap_or(256),
530            max_bounce_buffer_pages: self.max_bounce_buffer_pages,
531        })
532    }
533}
534
535impl ParseSchema<crate::Vtl2SettingsDynamic> for Vtl2SettingsDynamic {
536    fn parse_schema(
537        &self,
538        errors: &mut ParseErrors<'_>,
539    ) -> Result<crate::Vtl2SettingsDynamic, ParsingStopped> {
540        let mut ide_controller = None;
541        let mut scsi_controllers = Vec::new();
542        let mut nvme_controllers = Vec::new();
543
544        for controller in &self.storage_controllers {
545            match controller.protocol() {
546                StorageProtocol::Ide => {
547                    if let Some(c) = controller
548                        .parse::<crate::IdeController>(errors)
549                        .collect_error(errors)
550                    {
551                        if ide_controller.is_some() {
552                            errors.push_with_context(
553                                ErrorContext::InstanceId(c.instance_id),
554                                Error::StorageIdeControllerExceedsMaxLimits,
555                            );
556                        }
557                        ide_controller = Some(c);
558                    }
559                }
560                StorageProtocol::Scsi => {
561                    if let Some(c) = controller
562                        .parse::<crate::ScsiController>(errors)
563                        .collect_error(errors)
564                    {
565                        if scsi_controllers.len() >= crate::SCSI_CONTROLLER_NUM {
566                            errors.push_with_context(
567                                ErrorContext::InstanceId(c.instance_id),
568                                Error::StorageScsiControllerExceedsMaxLimits,
569                            );
570                        }
571                        scsi_controllers.push(c);
572                    }
573                }
574                StorageProtocol::Nvme => {
575                    if let Some(c) = controller
576                        .parse::<crate::NvmeController>(errors)
577                        .collect_error(errors)
578                    {
579                        nvme_controllers.push(c);
580                    }
581                }
582                StorageProtocol::Unknown => {
583                    let instance_id = parse_instance_id(&controller.instance_id)?;
584                    errors.push_with_context(
585                        ErrorContext::InstanceId(instance_id),
586                        Error::StorageProtocolUnknown,
587                    );
588                }
589            }
590        }
591
592        let mut instance_ids = scsi_controllers
593            .iter()
594            .map(|c| c.instance_id)
595            .chain(nvme_controllers.iter().map(|c| c.instance_id))
596            .chain(ide_controller.iter().map(|c| c.instance_id))
597            .collect::<Vec<_>>();
598
599        instance_ids.sort();
600        for (a, b) in instance_ids.iter().zip(instance_ids.iter().skip(1)) {
601            if a == b {
602                errors.push_with_context(
603                    ErrorContext::InstanceId(*a),
604                    Error::StorageControllerGuidAlreadyExists,
605                );
606            }
607        }
608
609        let nic_devices = self
610            .nic_devices
611            .iter()
612            .flat_map(|nic| nic.parse(errors).collect_error(errors))
613            .collect();
614
615        Ok(crate::Vtl2SettingsDynamic {
616            ide_controller,
617            scsi_controllers,
618            nvme_controllers,
619            nic_devices,
620        })
621    }
622}