Skip to main content

memfaultd/config/
device_info.rs

1//
2// Copyright (c) Memfault, Inc.
3// See License.txt for details
4use std::fmt::{self, Display};
5use std::fs::read_to_string;
6use std::process::Command;
7
8use eyre::{eyre, Result};
9
10use crate::config::utils::{
11    device_id_is_valid, hardware_version_is_valid, software_type_is_valid,
12    software_version_is_valid,
13};
14use crate::util::etc_os_release::EtcOsRelease;
15
16const DEVICE_ID_PATH: &str = "/etc/machine-id";
17const HARDWARE_VERSION_COMMAND: &str = "uname";
18const HARDWARE_VERSION_ARGS: &[&str] = &["-n"];
19
20#[cfg_attr(test, mockall::automock)]
21/// Trait for providing default values for device info.
22///
23/// This is mostly a convenience for testing, as the default implementation
24/// reads the software version from /etc/os-release.
25pub trait DeviceInfoDefaults {
26    /// Get the software version from the system.
27    fn software_version(&self) -> Result<Option<String>>;
28
29    /// Get the device ID from the system.
30    fn device_id(&self) -> Result<String>;
31
32    /// Get the hardware version from the system.
33    fn hardware_version(&self) -> Result<String>;
34
35    /// Get the software type from the system.
36    fn software_type(&self) -> Result<Option<String>>;
37}
38
39/// Default implementation of DeviceInfoDefaults.
40pub struct DeviceInfoDefaultsImpl {
41    os_release: Option<EtcOsRelease>,
42}
43
44impl DeviceInfoDefaultsImpl {
45    fn new(os_release: Option<EtcOsRelease>) -> Self {
46        Self { os_release }
47    }
48}
49
50impl DeviceInfoDefaults for DeviceInfoDefaultsImpl {
51    fn software_version(&self) -> Result<Option<String>> {
52        Ok(self.os_release.as_ref().and_then(|os| os.version_id()))
53    }
54
55    fn device_id(&self) -> Result<String> {
56        let device_id = read_to_string(DEVICE_ID_PATH)?;
57        if device_id.is_empty() {
58            return Err(eyre!("Empty device id ({})", DEVICE_ID_PATH));
59        }
60
61        Ok(device_id)
62    }
63
64    fn hardware_version(&self) -> Result<String> {
65        let output = Command::new(HARDWARE_VERSION_COMMAND)
66            .args(HARDWARE_VERSION_ARGS)
67            .output()?;
68
69        Ok(String::from_utf8(output.stdout)?)
70    }
71
72    fn software_type(&self) -> Result<Option<String>> {
73        Ok(self.os_release.as_ref().and_then(|os| os.id()))
74    }
75}
76
77#[derive(Debug, PartialEq, Eq)]
78pub enum DeviceInfoValue {
79    Configured(String),
80    Default(String),
81}
82
83impl AsRef<str> for DeviceInfoValue {
84    fn as_ref(&self) -> &str {
85        match self {
86            DeviceInfoValue::Configured(s) => s.as_ref(),
87            DeviceInfoValue::Default(s) => s.as_ref(),
88        }
89    }
90}
91
92impl Display for DeviceInfoValue {
93    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
94        write!(f, "{}", self.as_ref())
95    }
96}
97
98#[derive(Debug)]
99pub struct DeviceInfo {
100    pub device_id: String,
101    pub hardware_version: String,
102    pub software_version: Option<DeviceInfoValue>,
103    pub software_type: Option<DeviceInfoValue>,
104}
105
106#[derive(PartialEq, Eq, Debug)]
107pub struct DeviceInfoWarning {
108    line: Option<String>,
109    message: String,
110}
111
112impl std::fmt::Display for DeviceInfoWarning {
113    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> fmt::Result {
114        match &self.line {
115            Some(line) => write!(f, "Skipped line: '{}' ({})", line, self.message),
116            None => write!(f, "{}", self.message),
117        }
118    }
119}
120
121impl DeviceInfo {
122    pub fn parse<T: DeviceInfoDefaults>(
123        output: Option<&[u8]>,
124        defaults: T,
125    ) -> Result<(DeviceInfo, Vec<DeviceInfoWarning>)> {
126        let mut warnings = vec![];
127
128        let mut device_id: Option<String> = None;
129        let mut hardware_version: Option<String> = None;
130        let mut software_version: Option<DeviceInfoValue> = None;
131        let mut software_type: Option<DeviceInfoValue> = None;
132
133        match output {
134            Some(output) => {
135                for line in std::str::from_utf8(output)?.lines() {
136                    if let Some((key, value)) = line.split_once('=') {
137                        match key {
138                            "MEMFAULT_DEVICE_ID" => device_id = Some(value.into()),
139                            "MEMFAULT_HARDWARE_VERSION" => hardware_version = Some(value.into()),
140                            "MEMFAULT_SOFTWARE_VERSION" => {
141                                software_version = Some(DeviceInfoValue::Configured(value.into()))
142                            }
143                            "MEMFAULT_SOFTWARE_TYPE" => {
144                                software_type = Some(DeviceInfoValue::Configured(value.into()))
145                            }
146                            _ => warnings.push(DeviceInfoWarning {
147                                line: Some(line.into()),
148                                message: "Unknown variable.".to_string(),
149                            }),
150                        }
151                    } else {
152                        warnings.push(DeviceInfoWarning {
153                            line: Some(line.into()),
154                            message: "Expect '=' separated key/value pairs.".to_string(),
155                        })
156                    }
157                }
158            }
159            None => {
160                warnings.push(DeviceInfoWarning {
161                    line: None,
162                    message: "No output from memfault-device-info.".to_string(),
163                });
164            }
165        }
166
167        // If we don't have device-info provided values, fall back to defaults
168        software_version = software_version.or_else(|| {
169            defaults
170                .software_version()
171                .unwrap_or_else(|_| {
172                    warnings.push(DeviceInfoWarning {
173                        line: None,
174                        message: "Failed to get default software version.".to_string(),
175                    });
176                    None
177                })
178                .map(DeviceInfoValue::Default)
179        });
180        device_id = device_id.or_else(|| {
181            defaults.device_id().map_or_else(
182                |_| {
183                    warnings.push(DeviceInfoWarning {
184                        line: None,
185                        message: format!("Failed to open {}", DEVICE_ID_PATH),
186                    });
187                    None
188                },
189                |id| Some(id.trim().to_string()),
190            )
191        });
192        hardware_version = hardware_version.or_else(|| {
193            defaults.hardware_version().map_or_else(
194                |_| {
195                    warnings.push(DeviceInfoWarning {
196                        line: None,
197                        message: format!(
198                            "Failed to to get hardware version from: '{}'",
199                            HARDWARE_VERSION_COMMAND
200                        ),
201                    });
202                    None
203                },
204                |hwv| Some(hwv.trim().to_string()),
205            )
206        });
207        software_type = software_type.or_else(|| {
208            defaults
209                .software_type()
210                .unwrap_or_else(|_| {
211                    warnings.push(DeviceInfoWarning {
212                        line: None,
213                        message: "Failed to get default software_type.".to_string(),
214                    });
215                    None
216                })
217                .map(DeviceInfoValue::Default)
218        });
219
220        let di = DeviceInfo {
221            device_id: device_id.ok_or(eyre!("No device id supplied"))?,
222            hardware_version: hardware_version.ok_or(eyre!("No hardware version supplied"))?,
223            software_version,
224            software_type,
225        };
226
227        // Create vector of keys whose values have invalid characters
228        let validation_errors: Vec<String> = [
229            (
230                "MEMFAULT_HARDWARE_VERSION",
231                hardware_version_is_valid(&di.hardware_version),
232            ),
233            (
234                "MEMFAULT_SOFTWARE_VERSION",
235                di.software_version
236                    .as_ref()
237                    .map_or(Ok(()), |swv| software_version_is_valid(swv.as_ref())),
238            ),
239            (
240                "MEMFAULT_SOFTWARE_TYPE",
241                di.software_type
242                    .as_ref()
243                    .map_or(Ok(()), |swt| software_type_is_valid(swt.as_ref())),
244            ),
245            ("MEMFAULT_DEVICE_ID", device_id_is_valid(&di.device_id)),
246        ]
247        .iter()
248        .filter_map(|(key, result)| match result {
249            Err(e) => Some(format!("  Invalid {}: {}", key, e)),
250            _ => None,
251        })
252        .collect();
253
254        match validation_errors.is_empty() {
255            true => Ok((di, warnings)),
256            false => Err(eyre::eyre!("\n{}", validation_errors.join("\n"))),
257        }
258    }
259
260    pub fn load() -> eyre::Result<(DeviceInfo, Vec<DeviceInfoWarning>)> {
261        let user_output = Command::new("memfault-device-info").output().ok();
262        let stdout = user_output.as_ref().map(|o| o.stdout.as_slice());
263
264        let os_release = EtcOsRelease::load().ok();
265        let di_defaults = DeviceInfoDefaultsImpl::new(os_release);
266        Self::parse(stdout, di_defaults)
267    }
268}
269
270#[cfg(test)]
271impl DeviceInfo {
272    pub fn test_fixture() -> Self {
273        DeviceInfo {
274            device_id: "001".to_owned(),
275            hardware_version: "DVT".to_owned(),
276            software_version: None,
277            software_type: None,
278        }
279    }
280
281    pub fn test_fixture_with_overrides(software_version: &str, software_type: &str) -> Self {
282        DeviceInfo {
283            device_id: "001".to_owned(),
284            hardware_version: "DVT".to_owned(),
285            software_version: Some(DeviceInfoValue::Configured(software_version.into())),
286            software_type: Some(DeviceInfoValue::Configured(software_type.into())),
287        }
288    }
289}
290
291#[cfg(test)]
292mod tests {
293    use super::*;
294    use rstest::rstest;
295
296    #[test]
297    fn test_empty() {
298        let mut di_defaults = MockDeviceInfoDefaults::new();
299        di_defaults
300            .expect_software_type()
301            .returning(|| Err(eyre!("")));
302        di_defaults.expect_software_version().returning(|| Ok(None));
303        di_defaults
304            .expect_hardware_version()
305            .returning(|| Err(eyre!("")));
306        di_defaults
307            .expect_device_id()
308            .returning(|| Ok("123ABC".into()));
309        let r = DeviceInfo::parse(Some(b""), di_defaults);
310        assert!(r.is_err())
311    }
312
313    #[test]
314    fn test_with_warnings() {
315        let mut di_defaults = MockDeviceInfoDefaults::new();
316        di_defaults.expect_software_type().returning(|| Ok(None));
317        di_defaults.expect_software_version().returning(|| Ok(None));
318        di_defaults
319            .expect_device_id()
320            .returning(|| Ok("123ABC".into()));
321        di_defaults
322            .expect_hardware_version()
323            .returning(|| Ok("Hardware".into()));
324        let r = DeviceInfo::parse(
325            Some(b"MEMFAULT_DEVICE_ID=X\nMEMFAULT_HARDWARE_VERSION=Y\nblahblahblah\n"),
326            di_defaults,
327        );
328        assert!(r.is_ok());
329
330        let (di, warnings) = r.unwrap();
331        assert_eq!(di.device_id, "X");
332        assert_eq!(di.hardware_version, "Y");
333        assert_eq!(warnings.len(), 1);
334        assert_eq!(
335            warnings[0],
336            DeviceInfoWarning {
337                line: Some("blahblahblah".into()),
338                message: "Expect '=' separated key/value pairs.".to_string()
339            }
340        );
341    }
342
343    #[rstest]
344    // Override software version
345    #[case(b"MEMFAULT_DEVICE_ID=123ABC\nMEMFAULT_HARDWARE_VERSION=1.0.0\nMEMFAULT_SOFTWARE_VERSION=1.2.3\n", Some("1.2.3".into()), None)]
346    // Override software type
347    #[case(b"MEMFAULT_DEVICE_ID=123ABC\nMEMFAULT_HARDWARE_VERSION=1.0.0\nMEMFAULT_SOFTWARE_TYPE=test\n", None, Some("test".into()))]
348    // Override both software version and type
349    #[case(b"MEMFAULT_DEVICE_ID=123ABC\nMEMFAULT_HARDWARE_VERSION=1.0.0\nMEMFAULT_SOFTWARE_VERSION=1.2.3\nMEMFAULT_SOFTWARE_TYPE=test\n", Some("1.2.3".into()), Some("test".into()))]
350    fn test_with_sw_version_and_type(
351        #[case] output: &[u8],
352        #[case] sw_version: Option<String>,
353        #[case] sw_type: Option<String>,
354    ) {
355        let mut di_defaults = MockDeviceInfoDefaults::new();
356        di_defaults.expect_software_type().returning(|| Ok(None));
357        di_defaults.expect_software_version().returning(|| Ok(None));
358        di_defaults
359            .expect_hardware_version()
360            .returning(|| Ok("Hardware".into()));
361        di_defaults
362            .expect_device_id()
363            .returning(|| Ok("123ABC".into()));
364        let r = DeviceInfo::parse(Some(output), di_defaults);
365        assert!(r.is_ok());
366
367        let (di, warnings) = r.unwrap();
368        assert_eq!(di.device_id, "123ABC");
369        assert_eq!(di.hardware_version, "1.0.0");
370        assert_eq!(
371            di.software_version,
372            sw_version.map(DeviceInfoValue::Configured)
373        );
374        assert_eq!(di.software_type, sw_type.map(DeviceInfoValue::Configured));
375
376        assert_eq!(warnings.len(), 0);
377    }
378
379    #[rstest]
380    #[case::default_with_no_response(
381        Some("1.2.3".to_string()),
382        Some(DeviceInfoValue::Default("1.2.3".to_string())),
383        b""
384    )]
385    #[case::default_with_response(
386        Some("1.2.3".to_string()),
387        Some(DeviceInfoValue::Configured("1.2.4".to_string())),
388        b"MEMFAULT_SOFTWARE_VERSION=1.2.4"
389    )]
390    #[case::no_default_with_response(
391        None,
392        Some(DeviceInfoValue::Configured("1.2.4".to_string())),
393        b"MEMFAULT_SOFTWARE_VERSION=1.2.4"
394    )]
395    #[case::no_default_no_response(None, None, b"")]
396    fn test_with_default_swv(
397        #[case] software_version_default: Option<String>,
398        #[case] expected: Option<DeviceInfoValue>,
399        #[case] output: &[u8],
400    ) {
401        // Required device info parameters that will cause a panic if not present
402        let mut output_required =
403            b"MEMFAULT_DEVICE_ID=DEVICE\nMEMFAULT_HARDWARE_VERSION=HARDWARE\n".to_vec();
404        output_required.extend(output);
405
406        let mut di_defaults = MockDeviceInfoDefaults::new();
407        di_defaults
408            .expect_software_type()
409            .returning(|| Err(eyre!("")));
410        di_defaults
411            .expect_software_version()
412            .returning(move || Ok(software_version_default.clone()));
413        di_defaults
414            .expect_hardware_version()
415            .returning(|| Err(eyre!("")));
416        di_defaults.expect_device_id().returning(|| Err(eyre!("")));
417
418        let (di, _warnings) = DeviceInfo::parse(Some(&output_required), di_defaults).unwrap();
419        assert_eq!(di.software_version, expected);
420    }
421
422    #[rstest]
423    #[case::default_with_no_response(Some("123ABC".to_string()), Some(DeviceInfoValue::Default("123ABC".to_string())), b"")]
424    #[case::default_with_response(Some("123ABC".to_string()), Some(DeviceInfoValue::Configured("main".to_string())), b"MEMFAULT_SOFTWARE_TYPE=main")]
425    #[case::no_default_with_response(None, Some(DeviceInfoValue::Configured("main".to_string())), b"MEMFAULT_SOFTWARE_TYPE=main")]
426    #[case::no_default_no_response(None, None, b"")]
427    fn test_with_default_sw_type(
428        #[case] software_type_default: Option<String>,
429        #[case] expected: Option<DeviceInfoValue>,
430        #[case] output: &[u8],
431    ) {
432        // Required device info parameters that will cause a panic if not present
433        let mut output_required =
434            b"MEMFAULT_DEVICE_ID=DEVICE\nMEMFAULT_HARDWARE_VERSION=HARDWARE\n".to_vec();
435        output_required.extend(output);
436
437        let mut di_defaults = MockDeviceInfoDefaults::new();
438        di_defaults
439            .expect_software_version()
440            .returning(|| Err(eyre!("")));
441        di_defaults
442            .expect_hardware_version()
443            .returning(|| Err(eyre!("")));
444        di_defaults.expect_device_id().returning(|| Err(eyre!("")));
445        di_defaults
446            .expect_software_type()
447            .returning(move || Ok(software_type_default.clone()));
448
449        let (di, _warnings) = DeviceInfo::parse(Some(&output_required), di_defaults).unwrap();
450        assert_eq!(di.software_type, expected);
451    }
452
453    #[rstest]
454    #[case::default_with_no_response(Some("123ABC".to_string()), Some("123ABC".to_string()), b"")]
455    #[case::default_with_whitespace(Some("123ABC\n".to_string()), Some("123ABC".to_string()), b"")]
456    #[case::default_with_response(Some("123ABC".to_string()), Some("DEVICE".to_string()), b"MEMFAULT_DEVICE_ID=DEVICE")]
457    #[case::no_default_with_response(None, Some("DEVICE".to_string()), b"MEMFAULT_DEVICE_ID=DEVICE")]
458    #[case::no_default_no_response(None, None, b"")]
459    fn test_with_default_device_id(
460        #[case] device_id_default: Option<String>,
461        #[case] expected: Option<String>,
462        #[case] output: &[u8],
463    ) {
464        // Required device info parameters that will cause a panic if not present
465        let mut output_required = b"MEMFAULT_HARDWARE_VERSION=HARDWARE\n".to_vec();
466        output_required.extend(output);
467
468        let mut di_defaults = MockDeviceInfoDefaults::new();
469        di_defaults
470            .expect_software_type()
471            .returning(|| Err(eyre!("")));
472        di_defaults
473            .expect_software_version()
474            .returning(|| Err(eyre!("")));
475        di_defaults
476            .expect_hardware_version()
477            .returning(|| Err(eyre!("")));
478        di_defaults
479            .expect_device_id()
480            .returning(move || device_id_default.clone().ok_or(eyre!("")));
481
482        let ret = DeviceInfo::parse(Some(&output_required), di_defaults);
483        if let Some(expected) = expected {
484            let (di, _warnings) = ret.unwrap();
485            assert_eq!(di.device_id, expected);
486        } else {
487            assert!(ret.is_err());
488        }
489    }
490
491    #[rstest]
492    #[case::default_with_no_response(Some("123ABC".to_string()), Some("123ABC".to_string()), b"")]
493    #[case::default_with_whitespace(Some("123ABC\n".to_string()), Some("123ABC".to_string()), b"")]
494    #[case::default_with_response(Some("123ABC".to_string()), Some("HARDWARE".to_string()), b"MEMFAULT_HARDWARE_VERSION=HARDWARE")]
495    #[case::no_default_with_response(None, Some("HARDWARE".to_string()), b"MEMFAULT_HARDWARE_VERSION=HARDWARE")]
496    #[case::no_default_no_response(None, None, b"")]
497    fn test_with_default_hardware_version(
498        #[case] hardware_version_default: Option<String>,
499        #[case] expected: Option<String>,
500        #[case] output: &[u8],
501    ) {
502        // Required device info parameters that will cause a panic if not present
503        let mut output_required = b"MEMFAULT_DEVICE_ID=DEVICE\n".to_vec();
504        output_required.extend(output);
505
506        let mut di_defaults = MockDeviceInfoDefaults::new();
507        di_defaults
508            .expect_software_type()
509            .returning(|| Err(eyre!("")));
510        di_defaults
511            .expect_software_version()
512            .returning(|| Err(eyre!("")));
513        di_defaults.expect_device_id().returning(|| Err(eyre!("")));
514        di_defaults
515            .expect_hardware_version()
516            .returning(move || hardware_version_default.clone().ok_or(eyre!("")));
517
518        let ret = DeviceInfo::parse(Some(&output_required), di_defaults);
519        if let Some(expected) = expected {
520            let (di, _warnings) = ret.unwrap();
521            assert_eq!(di.hardware_version, expected);
522        } else {
523            assert!(ret.is_err());
524        }
525    }
526
527    #[rstest]
528    fn test_with_no_device_info() {
529        let expected_software_type = "SOFTWARE_TYPE".to_string();
530        let expected_software_version = "SOFTWARE_VERSION".to_string();
531        let expected_hardware_version = "HARDWARE_VERSION".to_string();
532        let expected_device_id = "DEVICE_ID".to_string();
533
534        let mut di_defaults = MockDeviceInfoDefaults::new();
535        let default_software_type = expected_software_type.clone();
536        di_defaults
537            .expect_software_type()
538            .returning(move || Ok(Some(default_software_type.clone())));
539        let default_software_version = expected_software_version.clone();
540        di_defaults
541            .expect_software_version()
542            .returning(move || Ok(Some(default_software_version.clone())));
543        let default_hardware_version = expected_hardware_version.clone();
544        di_defaults
545            .expect_hardware_version()
546            .returning(move || Ok(default_hardware_version.clone()));
547        let default_device_id = expected_device_id.clone();
548        di_defaults
549            .expect_device_id()
550            .returning(move || Ok(default_device_id.clone()));
551
552        let r = DeviceInfo::parse(None, di_defaults).unwrap();
553
554        assert_eq!(
555            r.0.software_type,
556            Some(DeviceInfoValue::Default(expected_software_type))
557        );
558        assert_eq!(
559            r.0.software_version,
560            Some(DeviceInfoValue::Default(expected_software_version))
561        );
562        assert_eq!(r.0.hardware_version, expected_hardware_version);
563        assert_eq!(r.0.device_id, expected_device_id);
564    }
565
566    #[rstest]
567    fn test_no_default_calls_with_device_info() {
568        let expected_software_type = "SOFTWARE_TYPE".to_string();
569        let expected_software_version = "SOFTWARE_VERSION".to_string();
570        let expected_hardware_version = "HARDWARE_VERSION".to_string();
571        let expected_device_id = "DEVICE_ID".to_string();
572
573        let mut di_defaults = MockDeviceInfoDefaults::new();
574        di_defaults.expect_software_type().never();
575        di_defaults.expect_software_version().never();
576        di_defaults.expect_hardware_version().never();
577        di_defaults.expect_device_id().never();
578
579        let output = format!(
580            "MEMFAULT_DEVICE_ID={}\nMEMFAULT_HARDWARE_VERSION={}\nMEMFAULT_SOFTWARE_VERSION={}\nMEMFAULT_SOFTWARE_TYPE={}\n",
581            expected_device_id,
582            expected_hardware_version,
583            expected_software_version,
584            expected_software_type,
585        );
586
587        let r = DeviceInfo::parse(Some(output.as_bytes()), di_defaults).unwrap();
588
589        assert_eq!(
590            r.0.software_type,
591            Some(DeviceInfoValue::Configured(expected_software_type))
592        );
593        assert_eq!(
594            r.0.software_version,
595            Some(DeviceInfoValue::Configured(expected_software_version))
596        );
597        assert_eq!(r.0.hardware_version, expected_hardware_version);
598        assert_eq!(r.0.device_id, expected_device_id);
599    }
600}