underhill_config/
lib.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Underhill configuration lib
5//!
6//! The structs and functions for Underhill configuration
7
8#![expect(missing_docs)]
9#![forbid(unsafe_code)]
10
11use guid::Guid;
12use inspect::Inspect;
13use mesh::MeshPayload;
14use serde::Serialize;
15
16mod errors;
17pub mod schema;
18
19// IDE constants
20const IDE_NUM_CHANNELS: u8 = 2;
21const IDE_MAX_DRIVES_PER_CHANNEL: u8 = 2;
22
23// SCSI constants
24const SCSI_CONTROLLER_NUM: usize = 4;
25pub const SCSI_LUN_NUM: usize = 64;
26
27#[derive(Debug, Copy, Clone, Eq, PartialEq, MeshPayload, Inspect)]
28pub enum DeviceType {
29    NVMe,
30    VScsi,
31}
32
33#[derive(Debug, Clone, Eq, PartialEq, MeshPayload, Inspect)]
34pub struct PhysicalDevice {
35    pub device_type: DeviceType,
36    // The associated vmbus device's instance ID, from which Underhill can find
37    // the hardware PCI path.
38    pub vmbus_instance_id: Guid,
39    // The additional sub device path. It's the namespace ID for NVMe devices.
40    pub sub_device_path: u32,
41}
42
43#[derive(Debug, Clone, Eq, PartialEq, MeshPayload, Inspect)]
44#[inspect(external_tag)]
45pub enum PhysicalDevices {
46    EmptyDrive,
47    Single {
48        device: PhysicalDevice,
49    },
50    Striped {
51        #[inspect(iter_by_index)]
52        devices: Vec<PhysicalDevice>,
53        chunk_size_in_kb: u32,
54    },
55}
56
57impl PhysicalDevices {
58    pub fn is_striping(&self) -> bool {
59        matches!(self, PhysicalDevices::Striped { .. })
60    }
61
62    pub fn is_empty(&self) -> bool {
63        matches!(self, PhysicalDevices::EmptyDrive)
64    }
65}
66
67#[derive(Debug, Clone, Eq, PartialEq, MeshPayload, Inspect)]
68pub enum GuestMediaType {
69    Hdd,
70    DvdLoaded,
71    DvdUnloaded,
72}
73
74#[derive(Debug, Clone, Eq, PartialEq, MeshPayload, Inspect)]
75pub struct DiskParameters {
76    pub device_id: String,
77    pub vendor_id: String,
78    pub product_id: String,
79    pub product_revision_level: String,
80    pub serial_number: String,
81    pub model_number: String,
82    pub medium_rotation_rate: u16,
83    pub physical_sector_size: Option<u32>,
84    pub fua: Option<bool>,
85    pub write_cache: Option<bool>,
86    pub scsi_disk_size_in_bytes: Option<u64>,
87    pub odx: Option<bool>,
88    pub unmap: Option<bool>,
89    pub max_transfer_length: Option<usize>,
90}
91
92#[derive(Debug)]
93pub enum StorageDisk {
94    Ide(IdeDisk),
95    Scsi(ScsiDisk),
96}
97
98impl StorageDisk {
99    pub fn physical_devices(&self) -> &PhysicalDevices {
100        match self {
101            StorageDisk::Ide(ide_disk) => &ide_disk.physical_devices,
102            StorageDisk::Scsi(scsi_disk) => &scsi_disk.physical_devices,
103        }
104    }
105
106    pub fn is_dvd(&self) -> bool {
107        match self {
108            StorageDisk::Ide(ide_disk) => ide_disk.is_dvd,
109            StorageDisk::Scsi(scsi_disk) => scsi_disk.is_dvd,
110        }
111    }
112    pub fn ntfs_guid(&self) -> Option<Guid> {
113        match self {
114            StorageDisk::Ide(ide_disk) => ide_disk.ntfs_guid,
115            StorageDisk::Scsi(scsi_disk) => scsi_disk.ntfs_guid,
116        }
117    }
118}
119
120#[derive(Debug, Clone, Eq, PartialEq, MeshPayload, Inspect)]
121pub struct IdeDisk {
122    pub channel: u8,
123    pub location: u8,
124    pub disk_params: DiskParameters,
125    pub physical_devices: PhysicalDevices,
126    pub ntfs_guid: Option<Guid>,
127    pub is_dvd: bool,
128}
129
130#[derive(Debug, Clone, Eq, PartialEq, MeshPayload, Inspect)]
131pub struct IdeController {
132    pub instance_id: Guid,
133    #[inspect(iter_by_index)]
134    pub disks: Vec<IdeDisk>,
135    pub io_queue_depth: Option<u32>,
136}
137
138#[derive(Debug, Clone, Eq, PartialEq, MeshPayload, Inspect)]
139pub struct ScsiDisk {
140    pub location: u8,
141    pub disk_params: DiskParameters,
142    pub physical_devices: PhysicalDevices,
143    pub ntfs_guid: Option<Guid>,
144    pub is_dvd: bool,
145}
146
147#[derive(Debug, Clone, Eq, PartialEq, MeshPayload, Inspect)]
148pub struct ScsiController {
149    pub instance_id: Guid,
150    #[inspect(iter_by_index)]
151    pub disks: Vec<ScsiDisk>,
152    pub io_queue_depth: Option<u32>,
153}
154
155#[derive(Debug, Clone, Eq, PartialEq, MeshPayload, Inspect)]
156pub struct NvmeNamespace {
157    pub nsid: u32,
158    pub disk_params: DiskParameters,
159    pub physical_devices: PhysicalDevices,
160}
161
162#[derive(Debug, Clone, Eq, PartialEq, MeshPayload, Inspect)]
163pub struct NvmeController {
164    pub instance_id: Guid,
165    #[inspect(iter_by_index)]
166    pub namespaces: Vec<NvmeNamespace>,
167}
168
169#[derive(Debug, Clone, MeshPayload, Inspect)]
170pub struct NicDevice {
171    pub instance_id: Guid,
172    pub subordinate_instance_id: Option<Guid>,
173    pub max_sub_channels: Option<u16>,
174}
175
176#[derive(Debug, Clone, MeshPayload, Inspect)]
177pub struct Vtl2SettingsFixed {
178    /// number of sub-channels for the SCSI controller
179    pub scsi_sub_channels: u16,
180    /// size of the io-uring submission queues
181    pub io_ring_size: u32,
182    /// Max bounce buffer pages active per cpu
183    pub max_bounce_buffer_pages: Option<u32>,
184}
185
186#[derive(Debug, Clone, MeshPayload, Inspect)]
187pub struct Vtl2SettingsDynamic {
188    /// Primary IDE controller
189    pub ide_controller: Option<IdeController>,
190    /// SCSI controllers
191    #[inspect(iter_by_index)]
192    pub scsi_controllers: Vec<ScsiController>,
193    /// NIC devices
194    #[inspect(iter_by_index)]
195    pub nic_devices: Vec<NicDevice>,
196    /// NVMe controllers
197    #[inspect(iter_by_index)]
198    pub nvme_controllers: Vec<NvmeController>,
199}
200
201#[derive(Debug, Default, Clone, MeshPayload, Inspect)]
202pub struct Vtl2Settings {
203    /// Static settings which cannot be updated during runtime
204    pub fixed: Vtl2SettingsFixed,
205    /// Dynamic settings
206    pub dynamic: Vtl2SettingsDynamic,
207}
208
209enum Component {
210    Underhill,
211    Storage,
212    Network,
213}
214
215enum EscalationCategory {
216    Underhill,
217    VmCreation,
218    Configuration,
219}
220
221macro_rules! error_codes {
222    {
223        $(#[$enum_attr:meta])*
224        $vis:vis enum $enum_name:ident {
225            $(
226                $(#[$attr:meta])*
227                $name:ident => ($component:tt, $category:tt),
228            )*
229        }
230    } => {
231        $(#[$enum_attr])*
232        $vis enum $enum_name {
233            $(
234                $(#[$attr])*
235                $name,
236            )*
237        }
238
239        impl $enum_name {
240            /// Returns the string representation of the error code for sending
241            /// ot the host.
242            fn name(&self) -> &'static str {
243                match self {
244                    $(
245                        $enum_name::$name => {
246                            // Validate the component and category names.
247                            let _ = Component::$component;
248                            let _ = EscalationCategory::$category;
249                            concat!(stringify!($category), ".", stringify!($name))
250                        }
251                    )*
252                }
253            }
254        }
255    };
256}
257
258error_codes! {
259/// The error codes used for failures when parsing or acting on a VTL2 settings
260/// document from the host.
261///
262/// These are used to provide an identifier that the host can match on, as well
263/// as some error categorization. Additional (string) error context can be
264/// provided in [`Vtl2SettingsErrorInfo`].
265///
266/// This table is in the form `name => (component, category)`. `component` and
267/// `category` must be elements in the `Component` and `EscalationCategory`
268/// enums, respectively.
269///
270/// Note that the values of name, component, and category are encoded into a
271/// form sent to the host, and so changing them for an error may be a breaking
272/// change.
273///
274/// Specifically, they are encoded into a string as `category.name`. The astute
275/// reader will note that the `component` is not actually used in the error
276/// message, or anywhere. FUTURE: remove the component, or include it as a
277/// separate field in the JSON.
278///
279/// In any case, the Rust-assigned discriminant values are not used in error
280/// messages and do not need to be stable.
281#[derive(Clone, Copy, Debug, PartialEq)]
282pub enum Vtl2SettingsErrorCode {
283    /// Underhill internal failure
284    InternalFailure => (Underhill, Underhill),
285    /// Invalid JSON format
286    JsonFormatError => (Underhill, Configuration),
287    /// VM Bus server is not configured
288    NoVmbusServer => (Underhill, VmCreation),
289    /// Unsupported schema version
290    UnsupportedSchemaVersion => (Underhill, Configuration),
291    /// Invalid vmbus instance ID
292    InvalidInstanceId => (Underhill, Configuration),
293    /// Invalid protobuf format
294    ProtobufFormatError => (Underhill, Configuration),
295    /// Unsupported schema namespace
296    UnsupportedSchemaNamespace => (Underhill, Configuration),
297    /// Empty namespace chunk
298    EmptyNamespaceChunk => (Underhill, Configuration),
299    /// Change storage controller at runtime
300    StorageCannotAddRemoveControllerAtRuntime => (Storage, Configuration),
301    /// SCSI LUN exceeds max limits (64)
302    StorageLunLocationExceedsMaxLimits => (Storage, Configuration),
303    /// SCSI LUN location duplicated in configuration
304    StorageLunLocationDuplicated => (Storage, Configuration),
305    /// Unsupported device type in configuration
306    StorageUnsupportedDeviceType => (Storage, Configuration),
307    /// Cannot find NVMe device namespace /dev/nvme*n*
308    StorageCannotFindVtl2Device => (Storage, Configuration),
309    /// Hard drive cannot be empty
310    EmptyDriveNotAllowed => (Storage, Configuration),
311
312    /// Cannot open VTL2 block device
313    StorageCannotOpenVtl2Device => (Storage, Underhill),
314    /// Cannot find a given SCSI controller
315    StorageScsiControllerNotFound => (Storage, Underhill),
316    /// Failed to attack a disk to a controller
317    StorageAttachDiskFailed => (Storage, Underhill),
318    /// Failed to remove a disk from a controller
319    StorageRmDiskFailed => (Storage, Underhill),
320    /// Storage controller already exists
321    StorageControllerGuidAlreadyExists => (Storage, Configuration),
322    /// SCSI controller exceeds max limits (4)
323    StorageScsiControllerExceedsMaxLimits => (Storage, Configuration),
324    /// Invalid vendor ID
325    StorageInvalidVendorId => (Storage, Configuration),
326    /// Invalid product ID
327    StorageInvalidProductId => (Storage, Configuration),
328    /// Invalid product revision level
329    StorageInvalidProductRevisionLevel => (Storage, Configuration),
330    /// IDE channel is not provided
331    StorageIdeChannelNotProvided => (Storage, Configuration),
332    /// IDE channel exceeds max limits (0 or 1)
333    StorageIdeChannelExceedsMaxLimits => (Storage, Configuration),
334    /// IDE location exceeds max limits (0 or 1)
335    StorageIdeLocationExceedsMaxLimits => (Storage, Configuration),
336    /// IDE configuration is invalid
337    StorageIdeChannelInvalidConfiguration => (Storage, Configuration),
338    /// Cannot change storage controller with striped devices at runtime
339    StripedStorageCannotChangeControllerAtRuntime => (Storage, Configuration),
340    /// Invalid physical disk count
341    StorageInvalidPhysicalDiskCount => (Storage, Configuration),
342    /// Cannot modify IDE devices at runtime
343    StorageCannotModifyIdeAtRuntime => (Storage, Configuration),
344    /// Invalid controller type
345    StorageInvalidControllerType => (Storage, Configuration),
346    /// Invalid vendor ID
347    StorageInvalidDeviceId => (Storage, Configuration),
348    /// Failed to change media on a controller
349    StorageChangeMediaFailed => (Storage, Underhill),
350    /// Invalid NTFS format guid
351    StorageInvalidNtfsFormatGuid => (Storage, Configuration),
352
353    /// Failed to modify NIC
354    NetworkingModifyNicFailed => (Network, Configuration),
355    /// Failed to add NIC
356    NetworkingAddNicFailed => (Network, Configuration),
357    /// Failed to remove NIC
358    NetworkingRemoveNicFailed => (Network, Configuration),
359}
360}
361
362impl Serialize for Vtl2SettingsErrorCode {
363    fn serialize<S: serde::Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
364        ser.serialize_str(self.name())
365    }
366}
367
368#[derive(Debug, Serialize)]
369pub struct Vtl2SettingsErrorInfo {
370    error_id: Vtl2SettingsErrorCode,
371    message: String,
372    file_name: &'static str,
373    line: u32,
374}
375
376impl Vtl2SettingsErrorInfo {
377    #[track_caller]
378    pub fn new(code: Vtl2SettingsErrorCode, message: String) -> Self {
379        let caller = std::panic::Location::caller();
380        Vtl2SettingsErrorInfo {
381            error_id: code,
382            message,
383            file_name: caller.file(),
384            line: caller.line(),
385        }
386    }
387
388    pub fn code(&self) -> Vtl2SettingsErrorCode {
389        self.error_id
390    }
391}
392
393impl std::fmt::Display for Vtl2SettingsErrorInfo {
394    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
395        // TODO: use a more standard `Display` impl and make sure anyone who
396        // wants JSON requests it explicitly via `serde_json`.
397        let json = serde_json::to_string(self).map_err(|_| std::fmt::Error)?;
398        write!(f, "{}", json)
399    }
400}
401
402impl std::error::Error for Vtl2SettingsErrorInfo {}
403
404#[derive(Debug)]
405pub struct Vtl2SettingsErrorInfoVec {
406    pub errors: Vec<Vtl2SettingsErrorInfo>,
407}
408
409impl std::fmt::Display for Vtl2SettingsErrorInfoVec {
410    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
411        for e in &self.errors {
412            writeln!(f, "{}", e)?;
413        }
414        Ok(())
415    }
416}
417
418impl std::error::Error for Vtl2SettingsErrorInfoVec {}