verneuil/
instance_id.rs

1//! Describes the current incarnation of the running image textually.
2//! The description should be constant until the next reboot, and
3//! change after each reboot.
4use std::boxed::Box;
5use std::fs::File;
6use std::io::BufRead;
7use std::io::Error;
8use std::io::ErrorKind;
9use std::io::Result;
10
11const DEFAULT_HOSTNAME: &str = "no.hostname.verneuil";
12
13fn compute_boot_time_slow() -> Result<u64> {
14    let file = File::open("/proc/stat")?;
15
16    // Look for a line that looks like
17    // `btime 1623415789` in `/proc/stat`.
18    for line_or in std::io::BufReader::new(file).lines() {
19        let line = line_or?;
20        if let Some(suffix) = line.strip_prefix("btime ") {
21            return suffix
22                .parse::<u64>()
23                .map_err(|_| Error::new(ErrorKind::Other, "failed to parse btime"));
24        }
25    }
26
27    Err(Error::new(ErrorKind::Other, "btime not in `/proc/stat`"))
28}
29
30/// Returns the Unix timestamp at which the machine booted up.  This
31/// value is only advisory, and useful to improve debuggability, but
32/// not for correctness.
33pub(crate) fn boot_timestamp() -> u64 {
34    lazy_static::lazy_static! {
35        static ref TIMESTAMP: u64 = compute_boot_time_slow().unwrap_or(0);
36    }
37
38    *TIMESTAMP
39}
40
41fn find_boot_id() -> Result<&'static str> {
42    let file = File::open("/proc/sys/kernel/random/boot_id")?;
43
44    match std::io::BufReader::new(file).lines().next() {
45        None => Err(Error::new(ErrorKind::Other, "boot_id is empty")),
46        Some(Err(e)) => Err(e),
47        Some(Ok(line)) => Ok(Box::leak(line.into_boxed_str())),
48    }
49}
50
51/// Returns the randomly generated UUID for this boot.
52pub(crate) fn boot_id() -> &'static str {
53    lazy_static::lazy_static! {
54        static ref ID: &'static str = find_boot_id().expect("`/proc/sys/kernel/random/boot_id` should be set");
55    }
56
57    &ID
58}
59
60fn find_hostname() -> Result<&'static str> {
61    let file = File::open("/etc/hostname")?;
62
63    match std::io::BufReader::new(file).lines().next() {
64        None => Err(Error::new(ErrorKind::Other, "hostname is empty")),
65        Some(Err(e)) => Err(e),
66        Some(Ok(line)) => Ok(Box::leak(line.into_boxed_str())),
67    }
68}
69
70/// Returns the machine's hostname, or a default placeholder if none.
71pub fn hostname() -> &'static str {
72    lazy_static::lazy_static! {
73        static ref NAME: &'static str = find_hostname().unwrap_or(DEFAULT_HOSTNAME);
74    }
75
76    &NAME
77}
78
79/// Returns a high-entropy short string hash of the hostname.
80pub(crate) fn hostname_hash(hostname: &str) -> String {
81    lazy_static::lazy_static! {
82        static ref PARAMS: umash::Params = umash::Params::derive(0, b"verneuil hostname params");
83    }
84
85    let hash = PARAMS.hasher(0).write(hostname.as_bytes()).digest();
86    format!("{:04x}", hash % (1 << (4 * 4)))
87}
88
89/// Returns the verneuil instance id for this incarnation of the
90/// current machine: the first component is the boot timestamp, to
91/// help operations, and the second is the boot UUID, which is
92/// expected to always change between reboots.
93pub(crate) fn instance_id() -> &'static str {
94    lazy_static::lazy_static! {
95        static ref INSTANCE: &'static str = Box::leak(format!("{}.{}", boot_timestamp(), boot_id()).into_boxed_str());
96    }
97
98    &INSTANCE
99}
100
101/// Returns a list of instance ids within `range` seconds of our
102/// `boot_timestamp()`, from most to least similar to `instance_id()`.
103/// The `boot_id()` suffices for correctness, while the
104/// `boot_timestamp()` mostly exists to help operators, so it can make
105/// sense to probe for `boot_timestamp()`.
106///
107/// The `boot_timestamp()` is subject to time adjustments, so we may
108/// want to probe for instance ids similar to the one we think we
109/// have.  If we ever allow configuration without `boot_id()`, this
110/// function should probably detect that situation and inconditionally
111/// return an empty list.
112pub(crate) fn likely_instance_ids(range: u64) -> Vec<String> {
113    let base_ts = boot_timestamp();
114    let boot_id = boot_id();
115
116    let mut ret = vec![instance_id().to_string()];
117
118    for delta in 1..=range {
119        if let Some(ts) = base_ts.checked_sub(delta) {
120            ret.push(format!("{}.{}", ts, boot_id));
121        }
122
123        if let Some(ts) = base_ts.checked_add(delta) {
124            ret.push(format!("{}.{}", ts, boot_id));
125        }
126    }
127
128    ret
129}
130
131#[test]
132fn print_boot_time() {
133    assert_ne!(boot_timestamp(), 0);
134    println!("Boot time = {}", boot_timestamp());
135}
136
137#[test]
138fn print_boot_id() {
139    assert_ne!(boot_id(), "");
140    println!("Boot id = {}", boot_id());
141}
142
143#[test]
144fn print_instance_id() {
145    println!("instance id = '{}'", instance_id());
146}
147
148#[test]
149fn print_hostname() {
150    assert_ne!(hostname(), DEFAULT_HOSTNAME);
151    println!(
152        "hostname = '{}', hash = '{}'",
153        hostname(),
154        hostname_hash(hostname())
155    );
156}
157
158/// Changing the hostname hash function is a backward incompatible
159/// change in storage format.  Test against that.
160#[test]
161fn test_hostname_hash() {
162    assert_eq!(hostname_hash("example.com"), "7010");
163}