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}