underhill_config/schema/
mod.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Underhill configuration schema
5//!
6//! Generic schema structs and functions
7
8use self::v1::NAMESPACE_BASE;
9use self::v1::NAMESPACE_NETWORK_ACCELERATION;
10use self::v1::NAMESPACE_NETWORK_DEVICE;
11use crate::Vtl2SettingsErrorInfoVec;
12use crate::errors::ParseErrors;
13use crate::errors::ParseErrorsBase;
14use crate::errors::ParseResultExt;
15use crate::errors::ParsingStopped;
16use thiserror::Error;
17use vtl2_settings_proto::*;
18
19mod v1;
20
21#[derive(Debug, Error)]
22pub enum ParseError {
23    #[error("json parsing failed")]
24    Json(#[source] serde_json::Error),
25    #[error("protobuf parsing failed")]
26    Protobuf(#[source] prost::DecodeError),
27    #[error("validation failed")]
28    Validation(#[source] Vtl2SettingsErrorInfoVec),
29}
30
31enum ParseErrorInner {
32    Parse(ParseError),
33    Validation(ParsingStopped),
34}
35
36impl From<ParseError> for ParseErrorInner {
37    fn from(value: ParseError) -> Self {
38        ParseErrorInner::Parse(value)
39    }
40}
41
42impl From<ParsingStopped> for ParseErrorInner {
43    fn from(value: ParsingStopped) -> Self {
44        ParseErrorInner::Validation(value)
45    }
46}
47
48impl crate::Vtl2Settings {
49    /// Reads the settings from either a JSON- or protobuf-encoded schema.
50    pub fn read_from(
51        data: &[u8],
52        old_settings: crate::Vtl2Settings,
53    ) -> Result<crate::Vtl2Settings, ParseError> {
54        let mut base = ParseErrorsBase::new();
55        let mut errors = base.root();
56        match Self::read_from_inner(data, old_settings, &mut errors) {
57            Ok(v) => {
58                base.result().map_err(ParseError::Validation)?;
59                Ok(v)
60            }
61            Err(ParseErrorInner::Parse(err)) => Err(err),
62            Err(ParseErrorInner::Validation(err)) => {
63                Err::<(), _>(err).collect_error(&mut errors);
64                Err(ParseError::Validation(base.result().unwrap_err()))
65            }
66        }
67    }
68
69    fn read_from_inner(
70        data: &[u8],
71        old_settings: crate::Vtl2Settings,
72        errors: &mut ParseErrors<'_>,
73    ) -> Result<crate::Vtl2Settings, ParseErrorInner> {
74        let mut old_settings = old_settings;
75
76        let decoded: Vtl2Settings = Self::read(data)?;
77
78        let mut has_base: bool = decoded.fixed.is_some() || decoded.dynamic.is_some();
79
80        // backwards compatibility
81        if has_base {
82            v1::validate_version(decoded.version, errors)?;
83        }
84
85        let mut fixed = decoded.fixed.unwrap_or_default().parse(errors)?;
86        let mut dynamic = decoded.dynamic.unwrap_or_default().parse(errors)?;
87
88        let mut nic_devices: Option<Vec<crate::NicDevice>> = None;
89        let mut nic_acceleration: Option<Vec<crate::NicDevice>> = None;
90
91        for chunk in &decoded.namespace_settings {
92            if chunk.settings.is_empty() {
93                errors.push(v1::Error::EmptyNamespaceChunk(chunk.namespace.as_ref()));
94            }
95            match chunk.namespace.as_str() {
96                NAMESPACE_BASE => {
97                    has_base = true;
98                    let base: Vtl2SettingsBase = Self::read(&chunk.settings)?;
99                    v1::validate_version(base.version, errors)?;
100                    fixed = base.fixed.unwrap_or_default().parse(errors)?;
101                    dynamic = base.dynamic.unwrap_or_default().parse(errors)?;
102                }
103                NAMESPACE_NETWORK_DEVICE => {
104                    let settings: Vtl2SettingsNetworkDevice = Self::read(&chunk.settings)?;
105                    nic_devices = Some(
106                        settings
107                            .nic_devices
108                            .iter()
109                            .flat_map(|v| v.parse(errors).collect_error(errors))
110                            .collect(),
111                    );
112                }
113                NAMESPACE_NETWORK_ACCELERATION => {
114                    let settings: Vtl2SettingsNetworkAcceleration = Self::read(&chunk.settings)?;
115                    nic_acceleration = Some(
116                        settings
117                            .nic_acceleration
118                            .iter()
119                            .flat_map(|v| v.parse(errors).collect_error(errors))
120                            .collect(),
121                    );
122                }
123                _ => {
124                    errors.push(v1::Error::UnsupportedSchemaNamespace(
125                        chunk.namespace.as_ref(),
126                    ));
127                }
128            }
129        }
130
131        // NAMESPACE_BASE
132        if has_base {
133            old_settings.fixed = fixed;
134            let old_nic_devices = std::mem::take(&mut old_settings.dynamic.nic_devices);
135            old_settings.dynamic = dynamic;
136            // If new network information is not present, do nothing. This handles the
137            // case where the base namespace is modified for non-networking reasons
138            // (e.g. storage), without adding current network information.
139            if old_settings.dynamic.nic_devices.is_empty() && nic_devices.is_none() {
140                old_settings.dynamic.nic_devices = old_nic_devices;
141            }
142        }
143
144        // NAMESPACE_NETWORK_DEVICE
145        if let Some(nic_devices) = nic_devices {
146            old_settings.dynamic.nic_devices = nic_devices;
147        }
148
149        // NAMESPACE_NETWORK_ACCELERATION
150        if let Some(nic_acceleration) = nic_acceleration {
151            // From the nic acceleration namespace, only process those instances which were
152            // originally specified.
153            for acc in nic_acceleration.iter() {
154                for nic in old_settings.dynamic.nic_devices.iter_mut() {
155                    if nic.instance_id == acc.instance_id {
156                        nic.subordinate_instance_id = acc.subordinate_instance_id;
157                        break;
158                    }
159                }
160            }
161        }
162
163        Ok(old_settings)
164    }
165
166    fn read<'a, T>(data: &'a [u8]) -> Result<T, ParseError>
167    where
168        T: Default,
169        T: prost::Message,
170        T: serde::Deserialize<'a>,
171    {
172        // Detect JSON vs. protobuf by looking for an opening
173        // brace by skipping whitespaces. This is mostly safe* (see below) because
174        // we reserve the protobuf field numbers that would conflict with this detection.
175        let idx = data.iter().position(|&b| !b.is_ascii_whitespace());
176        let is_json = match idx {
177            Some(idx) => data[idx] == b'{',
178            None => false,
179        };
180        let decoded: T = if is_json {
181            // *: in very rare cases, the message might be protobuf but
182            // LEN-encoded with a length of exactly 0x7b aka '{' which
183            // will take this branch and cause a failure. To address this,
184            // if the JSON parse fails attempt a protobuf parse before failing.
185            // Preserve the original JSON parse error to return in case the
186            // protobuf parse fails.
187            match serde_json::from_slice(data).map_err(ParseError::Json) {
188                Ok(json) => json,
189                Err(json_parse_err) => {
190                    match prost::Message::decode(data).map_err(ParseError::Protobuf) {
191                        Ok(protobuf) => protobuf,
192                        Err(_) => return Err(json_parse_err),
193                    }
194                }
195            }
196        } else {
197            prost::Message::decode(data).map_err(ParseError::Protobuf)?
198        };
199
200        Ok(decoded)
201    }
202}
203
204impl Default for crate::Vtl2SettingsFixed {
205    fn default() -> Self {
206        Vtl2SettingsFixed::default()
207            .parse(&mut ParseErrorsBase::new().root())
208            .unwrap()
209    }
210}
211
212impl Default for crate::Vtl2SettingsDynamic {
213    fn default() -> Self {
214        Vtl2SettingsDynamic::default()
215            .parse(&mut ParseErrorsBase::new().root())
216            .unwrap()
217    }
218}
219
220/// Convert scheme structs to config structs.
221pub(crate) trait ParseSchema<T>: Sized {
222    /// Parse the schema into a config struct.
223    ///
224    /// If possible, the parser should try to continue parsing after
225    /// encountering an error, pushing errors into `errors`. If the parser
226    /// cannot continue parsing, it should return a [`ParsingStopped`] error.
227    fn parse_schema(&self, errors: &mut ParseErrors<'_>) -> Result<T, ParsingStopped>;
228}
229
230/// Extension trait on schema types to parse them into config types.
231///
232/// This is useful over `ParseSchema<T>` so that you can use turbo-fish syntax
233/// to specify the type to parse into.
234pub(crate) trait ParseSchemaExt {
235    /// Parse the schema into a config struct.
236    fn parse<T>(&self, errors: &mut ParseErrors<'_>) -> Result<T, ParsingStopped>
237    where
238        Self: ParseSchema<T>;
239}
240
241impl<T> ParseSchemaExt for T {
242    fn parse<U>(&self, errors: &mut ParseErrors<'_>) -> Result<U, ParsingStopped>
243    where
244        T: ParseSchema<U>,
245    {
246        self.parse_schema(errors)
247    }
248}
249
250#[cfg(test)]
251mod test {
252    use super::*;
253    use crate::Vtl2SettingsErrorCode;
254    use crate::Vtl2SettingsErrorInfo;
255    use guid::Guid;
256    use prost::Message;
257
258    #[test]
259    fn smoke_test_sample() {
260        // { "version": "V1" }
261        let json = b"{ \"version\": \"V1\" }";
262        crate::Vtl2Settings::read_from(json, Default::default()).unwrap();
263    }
264
265    #[test]
266    fn smoke_test_namespace() {
267        let json = include_bytes!("vtl2s_test_namespace.json");
268        crate::Vtl2Settings::read_from(json, Default::default()).unwrap();
269    }
270
271    #[test]
272    fn smoke_test_namespace_mix_protobuf_json() {
273        let json = include_bytes!("vtl2s_test_namespace.json");
274        let settings: Vtl2Settings = crate::Vtl2Settings::read(json).unwrap();
275        // read only decode the top level payload, namespace settings chunk keeps its encoding
276        let base_json: &[u8] = &settings.namespace_settings[0].settings;
277        assert_eq!(base_json, include_bytes!("vtl2s_test_json.json"));
278        let mut buf = Vec::new();
279        settings.encode(&mut buf).unwrap();
280        crate::Vtl2Settings::read_from(&buf, Default::default()).unwrap();
281    }
282
283    #[test]
284    fn smoke_test_compat() {
285        crate::Vtl2Settings::read_from(
286            include_bytes!("vtl2s_test_compat.json"),
287            Default::default(),
288        )
289        .unwrap();
290    }
291
292    #[test]
293    fn validation_test_max_storage_controllers() {
294        crate::Vtl2Settings::read_from(
295            include_bytes!("vtl2s_test_max_storage_controllers.json"),
296            Default::default(),
297        )
298        .unwrap();
299    }
300
301    fn stable_error_json(err: &Vtl2SettingsErrorInfo) -> String {
302        let mut value = serde_json::to_value(err).unwrap();
303        let obj = value.as_object_mut().unwrap();
304        obj.remove("file_name").unwrap();
305        obj.remove("line").unwrap();
306        serde_json::to_string(&value).unwrap()
307    }
308
309    #[test]
310    fn validation_test_storage_controllers_exceeds_limits() {
311        let err = crate::Vtl2Settings::read_from(
312            include_bytes!("vtl2s_test_storage_controllers_exceeds_limits.json"),
313            Default::default(),
314        )
315        .unwrap_err();
316
317        let ParseError::Validation(err) = err else {
318            panic!("wrong error {err:?}")
319        };
320        let [err] = err.errors.try_into().unwrap();
321        assert_eq!(
322            err.code(),
323            Vtl2SettingsErrorCode::StorageScsiControllerExceedsMaxLimits
324        );
325        let expected = r#"{"error_id":"Configuration.StorageScsiControllerExceedsMaxLimits","message":"exceeded 4 max SCSI controllers, instance ID: 0bf355d5-0cae-411e-9662-86c3035556ae"}"#;
326        assert_eq!(stable_error_json(&err).as_str(), expected);
327    }
328
329    #[test]
330    fn namespace_test_nic_namespaces() {
331        /*
332        NetworkDevice
333        {
334            "nic_devices": [
335                {
336                    "instance_id": "9e14fd10-19cb-4da5-b667-e8e38a436cb8"
337                },
338                {
339                    "instance_id": "9e14fd11-19cb-4da5-b667-e8e38a436cb8"
340                },
341                {
342                    "instance_id": "9e14fd12-19cb-4da5-b667-e8e38a436cb8"
343                },
344            ]
345        }
346        NetworkAcceleration
347        {
348            "nic_acceleration": [
349                {
350                    "instance_id": "9e14fd11-19cb-4da5-b667-e8e38a436cb8",
351                    "subordinate_instance_id": "12345678-19cb-4da5-b667-e8e38a436cb8"
352                },
353                {
354                    "instance_id": "9e14fd12-19cb-4da5-b667-e8e38a436cb8",
355                    "subordinate_instance_id": "00000000-0000-0000-0000-000000000000"
356                }
357            ]
358        }
359        */
360
361        let settings = crate::Vtl2Settings::read_from(
362            include_bytes!("vtl2s_test_nic_namespaces.json"),
363            Default::default(),
364        )
365        .unwrap();
366
367        assert_eq!(3, settings.dynamic.nic_devices.len());
368
369        let nic0 = settings
370            .dynamic
371            .nic_devices
372            .iter()
373            .find(|nic| {
374                nic.instance_id
375                    == "9e14fd10-19cb-4da5-b667-e8e38a436cb8"
376                        .parse::<Guid>()
377                        .unwrap()
378            })
379            .unwrap();
380        assert_eq!(true, nic0.subordinate_instance_id.is_none());
381
382        let nic1 = settings
383            .dynamic
384            .nic_devices
385            .iter()
386            .find(|nic| {
387                nic.instance_id
388                    == "9e14fd11-19cb-4da5-b667-e8e38a436cb8"
389                        .parse::<Guid>()
390                        .unwrap()
391            })
392            .unwrap();
393        assert_eq!(
394            "12345678-19cb-4da5-b667-e8e38a436cb8"
395                .parse::<Guid>()
396                .unwrap(),
397            nic1.subordinate_instance_id.unwrap()
398        );
399
400        let nic2 = settings
401            .dynamic
402            .nic_devices
403            .iter()
404            .find(|nic| {
405                nic.instance_id
406                    == "9e14fd12-19cb-4da5-b667-e8e38a436cb8"
407                        .parse::<Guid>()
408                        .unwrap()
409            })
410            .unwrap();
411        assert_eq!(true, nic2.subordinate_instance_id.is_none());
412    }
413
414    #[test]
415    fn namespace_test_empty_nic_devices_ignored_json() {
416        let old_settings = crate::Vtl2Settings::read_from(
417            include_bytes!("vtl2s_test_nic_namespaces.json"),
418            Default::default(),
419        )
420        .unwrap();
421        assert_eq!(3, old_settings.dynamic.nic_devices.len());
422
423        let no_nic_settings = crate::Vtl2Settings::read_from(
424            include_bytes!("vtl2s_test_json_no_nic.json"),
425            Default::default(),
426        )
427        .unwrap();
428        assert_eq!(0, no_nic_settings.dynamic.nic_devices.len());
429
430        let settings = crate::Vtl2Settings::read_from(
431            include_bytes!("vtl2s_test_json_no_nic.json"),
432            old_settings.clone(),
433        )
434        .unwrap();
435        assert_eq!(3, settings.dynamic.nic_devices.len());
436    }
437
438    #[test]
439    fn namespace_test_adding_nic_devices_json() {
440        let old_settings = crate::Vtl2Settings::read_from(
441            include_bytes!("vtl2s_test_json_no_nic.json"),
442            Default::default(),
443        )
444        .unwrap();
445        assert_eq!(0, old_settings.dynamic.nic_devices.len());
446
447        let nic_settings = crate::Vtl2Settings::read_from(
448            include_bytes!("vtl2s_test_nic_namespaces.json"),
449            Default::default(),
450        )
451        .unwrap();
452        assert_eq!(3, nic_settings.dynamic.nic_devices.len());
453
454        let settings = crate::Vtl2Settings::read_from(
455            include_bytes!("vtl2s_test_nic_namespaces.json"),
456            old_settings.clone(),
457        )
458        .unwrap();
459        assert_eq!(3, settings.dynamic.nic_devices.len());
460    }
461
462    #[test]
463    fn namespace_test_adding_nic_devices_protobuf() {
464        let json = include_bytes!("vtl2s_test_nic_namespaces.json");
465        let settings: Vtl2Settings = crate::Vtl2Settings::read(json).unwrap();
466        assert_eq!("NetworkDevice", settings.namespace_settings[0].namespace);
467        let mut buf = Vec::new();
468        settings.encode(&mut buf).unwrap();
469        let settings = crate::Vtl2Settings::read_from(&buf, Default::default()).unwrap();
470        assert_eq!(3, settings.dynamic.nic_devices.len());
471    }
472
473    #[test]
474    fn namespace_test_empty_nic_devices_protobuf() {
475        let old_settings = crate::Vtl2Settings::read_from(
476            include_bytes!("vtl2s_test_nic_namespaces.json"),
477            Default::default(),
478        )
479        .unwrap();
480        assert_eq!(3, old_settings.dynamic.nic_devices.len());
481
482        // Create protobuff of empty nic_devices[]
483        let json = include_bytes!("vtl2s_test_json_no_nic.json");
484        let empty_json_settings: Vtl2Settings = crate::Vtl2Settings::read(json).unwrap();
485        let mut empty_protobuf = Vec::new();
486        empty_json_settings.encode(&mut empty_protobuf).unwrap();
487
488        // Create Vtl2Settings with NetworkDevices and no nic_devices
489        let mut empty_vtl2_settings: Vtl2Settings =
490            crate::Vtl2Settings::read(include_bytes!("vtl2s_test_nic_namespaces.json")).unwrap();
491        empty_vtl2_settings.namespace_settings.pop(); // Get rid of NetworkAcceleration
492        empty_vtl2_settings.namespace_settings[0].settings = empty_protobuf; // Empty nic_devices[]
493
494        let mut buf = Vec::new();
495        empty_vtl2_settings.encode(&mut buf).unwrap();
496        let settings = crate::Vtl2Settings::read_from(&buf, old_settings).unwrap();
497        assert_eq!(0, settings.dynamic.nic_devices.len());
498    }
499}