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