kumo_server_common/
nodeid.rs

1use std::path::PathBuf;
2use std::sync::LazyLock;
3use uuid::Uuid;
4
5static NODEID: LazyLock<NodeId> = LazyLock::new(NodeId::new);
6const DEFAULT_NODE_ID_PATH: &str = "/opt/kumomta/etc/.nodeid";
7
8/// The NodeId is intended to identify a specific instance of KumoMTA
9/// within your own local cluster.
10/// It is a uuid that will be generated and persisted when the node
11/// starts up.
12///
13/// If persisting the id isn't possible, we fall back to generating
14/// a "stable" v1 uuid from the mac address or deriving a fake mac address
15/// from the hostid of the system. Those aren't great when running in
16/// some virtualization environments, so it is recommended to resolve
17/// any issues with persisting the id there. There are some environment
18/// variables that can be used to influence that if the default filesystem
19/// path is not suitable for whatever reason.
20///
21/// The intended use of the nodeid is disambiguation during reporting,
22/// and also for future configuration management/provisioning related
23/// functionality.
24#[derive(Debug, Clone)]
25pub struct NodeId {
26    /// Unique node id in the cluster
27    pub uuid: Uuid,
28
29    /// Captures any write error we may have experienced while generating
30    /// the uuid. This is surfaced by the `check` method.
31    write_error: Option<String>,
32}
33
34impl std::fmt::Display for NodeId {
35    fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
36        self.uuid.fmt(fmt)
37    }
38}
39
40impl Default for NodeId {
41    fn default() -> Self {
42        Self::new()
43    }
44}
45
46impl NodeId {
47    /// Get the NodeId
48    pub fn get() -> Self {
49        (*NODEID).clone()
50    }
51
52    /// Retrieve just the uuid
53    pub fn get_uuid() -> Uuid {
54        NODEID.uuid
55    }
56
57    /// Raises an error if we don't have a persistent unique node id
58    pub fn check() -> anyhow::Result<()> {
59        let nodeid = Self::get();
60        if let Some(err) = &nodeid.write_error {
61            anyhow::bail!(
62                "Unable to determine the KUMO_NODE_ID. \
63                 Refusing to operate as part of a cluster. {err}"
64            );
65        }
66        Ok(())
67    }
68
69    pub fn new() -> Self {
70        let mut write_error = None;
71
72        let uuid = match std::env::var_os("KUMO_NODE_ID") {
73            Some(id_os) => match id_os.to_str() {
74                Some(id) => match Uuid::parse_str(id) {
75                    Ok(uuid) => uuid,
76                    Err(err) => {
77                        panic!("Env var KUMO_NODE_ID (`{id}`) is not a valid UUID: {err:#}")
78                    }
79                },
80                None => panic!("Env var KUMO_NODE_ID (`{id_os:?}`) is not valid UTF-8"),
81            },
82            None => {
83                let uuid_path: PathBuf = match std::env::var_os("KUMO_NODE_ID_PATH") {
84                    Some(node_path) => node_path.into(),
85                    None => DEFAULT_NODE_ID_PATH.into(),
86                };
87
88                match std::fs::read_to_string(&uuid_path) {
89                    Ok(id) => match Uuid::parse_str(&id) {
90                        Ok(uuid) => uuid,
91                        Err(err) => {
92                            panic!("File {uuid_path:?} content `{id}` is not a valid UUID: {err:#}")
93                        }
94                    },
95                    Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
96                        let uuid = Uuid::new_v4();
97
98                        match std::fs::write(&uuid_path, format!("{uuid}")) {
99                            Ok(_) => uuid,
100                            Err(err)
101                                if err.kind() == std::io::ErrorKind::PermissionDenied
102                                    || err.kind() == std::io::ErrorKind::NotFound =>
103                            {
104                                let err =
105                                    format!("Failed to write node id to {uuid_path:?}: {err:#}");
106                                tracing::debug!(
107                                    "{err}. Proceeding on the assumption that \
108                                     we're not in a cluster and switching to a \
109                                     stable v1 uuid based on the mac address"
110                                );
111                                write_error.replace(err);
112
113                                // Switch to a mac address based v1 uuid, with a fixed
114                                // timestamp. It looks like:
115                                // 00000000-0000-1000-8000-XXXXXXXXXXXX
116                                // where the X's are the hex digits from the mac address
117                                uuid_helper::new_v1(uuid::Timestamp::from_gregorian(0, 0))
118                            }
119                            Err(err) => panic!("Failed to write node id to {uuid_path:?}: {err:#}"),
120                        }
121                    }
122                    Err(err) => panic!("File {uuid_path:?} could not be read: {err:#}"),
123                }
124            }
125        };
126
127        Self { uuid, write_error }
128    }
129}