all lists on lists.proxmox.com
 help / color / mirror / Atom feed
From: Lukas Wagner <l.wagner@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [pve-devel] [PATCH v3 proxmox 1/5] notify: add PVE/PBS context
Date: Fri, 10 Nov 2023 15:57:49 +0100	[thread overview]
Message-ID: <20231110145753.809938-2-l.wagner@proxmox.com> (raw)
In-Reply-To: <20231110145753.809938-1-l.wagner@proxmox.com>

This commit moves PVEContext from `proxmox-perl-rs` into the
`proxmox-notify` crate, since we now also need to access it from
`promxox-mail-forward`. The context is now hidden behind a feature
flag `pve-context`, ensuring that we only compile it when needed.

This commit adds PBSContext, since we now require it for
`proxmox-mail-forward`. Some of the code for PBSContext comes
from `proxmox-mail-forward`.

This commit also changes the global context from being stored in a
`once_cell` to a regular `Mutex`, since we now need to set/reset
the context in `proxmox-mail-forward`.

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---

Notes:
    Changes v2 -> v3:
      - no changes

 proxmox-notify/Cargo.toml            |   3 +-
 proxmox-notify/src/context.rs        |  21 -----
 proxmox-notify/src/context/common.rs |  27 ++++++
 proxmox-notify/src/context/mod.rs    |  36 ++++++++
 proxmox-notify/src/context/pbs.rs    | 130 +++++++++++++++++++++++++++
 proxmox-notify/src/context/pve.rs    |  82 +++++++++++++++++
 6 files changed, 277 insertions(+), 22 deletions(-)
 delete mode 100644 proxmox-notify/src/context.rs
 create mode 100644 proxmox-notify/src/context/common.rs
 create mode 100644 proxmox-notify/src/context/mod.rs
 create mode 100644 proxmox-notify/src/context/pbs.rs
 create mode 100644 proxmox-notify/src/context/pve.rs

diff --git a/proxmox-notify/Cargo.toml b/proxmox-notify/Cargo.toml
index f2b4db5..7a3d434 100644
--- a/proxmox-notify/Cargo.toml
+++ b/proxmox-notify/Cargo.toml
@@ -13,7 +13,6 @@ handlebars = { workspace = true }
 lazy_static.workspace = true
 log.workspace = true
 mail-parser = { workspace = true, optional = true }
-once_cell.workspace = true
 openssl.workspace = true
 proxmox-http = { workspace = true, features = ["client-sync"], optional = true }
 proxmox-http-error.workspace = true
@@ -32,3 +31,5 @@ default = ["sendmail", "gotify"]
 mail-forwarder = ["dep:mail-parser"]
 sendmail = ["dep:proxmox-sys"]
 gotify = ["dep:proxmox-http"]
+pve-context = ["dep:proxmox-sys"]
+pbs-context = ["dep:proxmox-sys"]
diff --git a/proxmox-notify/src/context.rs b/proxmox-notify/src/context.rs
deleted file mode 100644
index 370c7ee..0000000
--- a/proxmox-notify/src/context.rs
+++ /dev/null
@@ -1,21 +0,0 @@
-use std::fmt::Debug;
-
-use once_cell::sync::OnceCell;
-
-pub trait Context: Send + Sync + Debug {
-    fn lookup_email_for_user(&self, user: &str) -> Option<String>;
-    fn default_sendmail_author(&self) -> String;
-    fn default_sendmail_from(&self) -> String;
-    fn http_proxy_config(&self) -> Option<String>;
-}
-
-static CONTEXT: OnceCell<&'static dyn Context> = OnceCell::new();
-
-pub fn set_context(context: &'static dyn Context) {
-    CONTEXT.set(context).expect("context has already been set");
-}
-
-#[allow(unused)] // context is not used if all endpoint features are disabled
-pub(crate) fn context() -> &'static dyn Context {
-    *CONTEXT.get().expect("context has not been yet")
-}
diff --git a/proxmox-notify/src/context/common.rs b/proxmox-notify/src/context/common.rs
new file mode 100644
index 0000000..7580bd1
--- /dev/null
+++ b/proxmox-notify/src/context/common.rs
@@ -0,0 +1,27 @@
+use std::path::Path;
+
+pub(crate) fn attempt_file_read<P: AsRef<Path>>(path: P) -> Option<String> {
+    match proxmox_sys::fs::file_read_optional_string(path) {
+        Ok(contents) => contents,
+        Err(err) => {
+            log::error!("{err}");
+            None
+        }
+    }
+}
+
+pub(crate) fn lookup_datacenter_config_key(content: &str, key: &str) -> Option<String> {
+    let key_prefix = format!("{key}:");
+    normalize_for_return(
+        content
+            .lines()
+            .find_map(|line| line.strip_prefix(&key_prefix)),
+    )
+}
+
+pub(crate) fn normalize_for_return(s: Option<&str>) -> Option<String> {
+    match s?.trim() {
+        "" => None,
+        s => Some(s.to_string()),
+    }
+}
diff --git a/proxmox-notify/src/context/mod.rs b/proxmox-notify/src/context/mod.rs
new file mode 100644
index 0000000..99d86de
--- /dev/null
+++ b/proxmox-notify/src/context/mod.rs
@@ -0,0 +1,36 @@
+use std::fmt::Debug;
+use std::sync::Mutex;
+
+#[cfg(any(feature = "pve-context", feature = "pbs-context"))]
+pub mod common;
+#[cfg(feature = "pbs-context")]
+pub mod pbs;
+#[cfg(feature = "pve-context")]
+pub mod pve;
+
+/// Product-specific context
+pub trait Context: Send + Sync + Debug {
+    /// Look up a user's email address from users.cfg
+    fn lookup_email_for_user(&self, user: &str) -> Option<String>;
+    /// Default mail author for mail-based targets
+    fn default_sendmail_author(&self) -> String;
+    /// Default from address for sendmail-based targets
+    fn default_sendmail_from(&self) -> String;
+    /// Proxy configuration for the current node
+    fn http_proxy_config(&self) -> Option<String>;
+}
+
+static CONTEXT: Mutex<Option<&'static dyn Context>> = Mutex::new(None);
+
+/// Set the product-specific context
+pub fn set_context(context: &'static dyn Context) {
+    *CONTEXT.lock().unwrap() = Some(context);
+}
+
+/// Get product-specific context.
+///
+/// Panics if the context has not been set yet.
+#[allow(unused)] // context is not used if all endpoint features are disabled
+pub(crate) fn context() -> &'static dyn Context {
+    (*CONTEXT.lock().unwrap()).expect("context for proxmox-notify has not been set yet")
+}
diff --git a/proxmox-notify/src/context/pbs.rs b/proxmox-notify/src/context/pbs.rs
new file mode 100644
index 0000000..b5d3168
--- /dev/null
+++ b/proxmox-notify/src/context/pbs.rs
@@ -0,0 +1,130 @@
+use serde::Deserialize;
+
+use proxmox_schema::{ObjectSchema, Schema, StringSchema};
+use proxmox_section_config::{SectionConfig, SectionConfigPlugin};
+
+use crate::context::{common, Context};
+
+const PBS_USER_CFG_FILENAME: &str = "/etc/proxmox-backup/user.cfg";
+const PBS_NODE_CFG_FILENAME: &str = "/etc/proxmox-backup/node.cfg";
+
+// FIXME: Switch to the actual schema when possible in terms of dependency.
+// It's safe to assume that the config was written with the actual schema restrictions, so parsing
+// it with the less restrictive schema should be enough for the purpose of getting the mail address.
+const DUMMY_ID_SCHEMA: Schema = StringSchema::new("dummy ID").min_length(3).schema();
+const DUMMY_EMAIL_SCHEMA: Schema = StringSchema::new("dummy email").schema();
+const DUMMY_USER_SCHEMA: ObjectSchema = ObjectSchema {
+    description: "minimal PBS user",
+    properties: &[
+        ("userid", false, &DUMMY_ID_SCHEMA),
+        ("email", true, &DUMMY_EMAIL_SCHEMA),
+    ],
+    additional_properties: true,
+    default_key: None,
+};
+
+#[derive(Deserialize)]
+struct DummyPbsUser {
+    pub email: Option<String>,
+}
+
+/// Extract the root user's email address from the PBS user config.
+fn lookup_mail_address(content: &str, username: &str) -> Option<String> {
+    let mut config = SectionConfig::new(&DUMMY_ID_SCHEMA).allow_unknown_sections(true);
+    let user_plugin = SectionConfigPlugin::new(
+        "user".to_string(),
+        Some("userid".to_string()),
+        &DUMMY_USER_SCHEMA,
+    );
+    config.register_plugin(user_plugin);
+
+    match config.parse(PBS_USER_CFG_FILENAME, content) {
+        Ok(parsed) => {
+            parsed.sections.get(username)?;
+            match parsed.lookup::<DummyPbsUser>("user", username) {
+                Ok(user) => common::normalize_for_return(user.email.as_deref()),
+                Err(err) => {
+                    log::error!("unable to parse {PBS_USER_CFG_FILENAME}: {err}");
+                    None
+                }
+            }
+        }
+        Err(err) => {
+            log::error!("unable to parse {PBS_USER_CFG_FILENAME}: {err}");
+            None
+        }
+    }
+}
+
+#[derive(Debug)]
+pub struct PBSContext;
+
+pub static PBS_CONTEXT: PBSContext = PBSContext;
+
+impl Context for PBSContext {
+    fn lookup_email_for_user(&self, user: &str) -> Option<String> {
+        let content = common::attempt_file_read(PBS_USER_CFG_FILENAME);
+        content.and_then(|content| lookup_mail_address(&content, user))
+    }
+
+    fn default_sendmail_author(&self) -> String {
+        "Proxmox Backup Server".into()
+    }
+
+    fn default_sendmail_from(&self) -> String {
+        let content = common::attempt_file_read(PBS_NODE_CFG_FILENAME);
+        content
+            .and_then(|content| common::lookup_datacenter_config_key(&content, "email-from"))
+            .unwrap_or_else(|| String::from("root"))
+    }
+
+    fn http_proxy_config(&self) -> Option<String> {
+        let content = common::attempt_file_read(PBS_NODE_CFG_FILENAME);
+        content.and_then(|content| common::lookup_datacenter_config_key(&content, "http-proxy"))
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    const USER_CONFIG: &str = "
+user: root@pam
+	email root@example.com
+
+user: test@pbs
+	enable true
+	expire 0
+    ";
+
+    #[test]
+    fn test_parse_mail() {
+        assert_eq!(
+            lookup_mail_address(USER_CONFIG, "root@pam"),
+            Some("root@example.com".to_string())
+        );
+        assert_eq!(lookup_mail_address(USER_CONFIG, "test@pbs"), None);
+    }
+
+    const NODE_CONFIG: &str = "
+default-lang: de
+email-from: root@example.com
+http-proxy: http://localhost:1234
+    ";
+
+    #[test]
+    fn test_parse_node_config() {
+        assert_eq!(
+            common::lookup_datacenter_config_key(NODE_CONFIG, "email-from"),
+            Some("root@example.com".to_string())
+        );
+        assert_eq!(
+            common::lookup_datacenter_config_key(NODE_CONFIG, "http-proxy"),
+            Some("http://localhost:1234".to_string())
+        );
+        assert_eq!(
+            common::lookup_datacenter_config_key(NODE_CONFIG, "foo"),
+            None
+        );
+    }
+}
diff --git a/proxmox-notify/src/context/pve.rs b/proxmox-notify/src/context/pve.rs
new file mode 100644
index 0000000..f263c95
--- /dev/null
+++ b/proxmox-notify/src/context/pve.rs
@@ -0,0 +1,82 @@
+use crate::context::{common, Context};
+
+fn lookup_mail_address(content: &str, user: &str) -> Option<String> {
+    common::normalize_for_return(content.lines().find_map(|line| {
+        let fields: Vec<&str> = line.split(':').collect();
+        #[allow(clippy::get_first)] // to keep expression style consistent
+        match fields.get(0)?.trim() == "user" && fields.get(1)?.trim() == user {
+            true => fields.get(6).copied(),
+            false => None,
+        }
+    }))
+}
+
+#[derive(Debug)]
+pub struct PVEContext;
+
+impl Context for PVEContext {
+    fn lookup_email_for_user(&self, user: &str) -> Option<String> {
+        let content = common::attempt_file_read("/etc/pve/user.cfg");
+        content.and_then(|content| lookup_mail_address(&content, user))
+    }
+
+    fn default_sendmail_author(&self) -> String {
+        "Proxmox VE".into()
+    }
+
+    fn default_sendmail_from(&self) -> String {
+        let content = common::attempt_file_read("/etc/pve/datacenter.cfg");
+        content
+            .and_then(|content| common::lookup_datacenter_config_key(&content, "email_from"))
+            .unwrap_or_else(|| String::from("root"))
+    }
+
+    fn http_proxy_config(&self) -> Option<String> {
+        let content = common::attempt_file_read("/etc/pve/datacenter.cfg");
+        content.and_then(|content| common::lookup_datacenter_config_key(&content, "http_proxy"))
+    }
+}
+
+pub static PVE_CONTEXT: PVEContext = PVEContext;
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    const USER_CONFIG: &str = "
+user:root@pam:1:0:::root@example.com:::
+user:test@pve:1:0:::test@example.com:::
+user:no-mail@pve:1:0::::::
+    ";
+
+    #[test]
+    fn test_parse_mail() {
+        assert_eq!(
+            lookup_mail_address(USER_CONFIG, "root@pam"),
+            Some("root@example.com".to_string())
+        );
+        assert_eq!(
+            lookup_mail_address(USER_CONFIG, "test@pve"),
+            Some("test@example.com".to_string())
+        );
+        assert_eq!(lookup_mail_address(USER_CONFIG, "no-mail@pve"), None);
+    }
+
+    const DC_CONFIG: &str = "
+email_from: user@example.com
+http_proxy: http://localhost:1234
+keyboard: en-us
+";
+    #[test]
+    fn test_parse_dc_config() {
+        assert_eq!(
+            common::lookup_datacenter_config_key(DC_CONFIG, "email_from"),
+            Some("user@example.com".to_string())
+        );
+        assert_eq!(
+            common::lookup_datacenter_config_key(DC_CONFIG, "http_proxy"),
+            Some("http://localhost:1234".to_string())
+        );
+        assert_eq!(common::lookup_datacenter_config_key(DC_CONFIG, "foo"), None);
+    }
+}
-- 
2.39.2





  reply	other threads:[~2023-11-10 14:58 UTC|newest]

Thread overview: 6+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2023-11-10 14:57 [pve-devel] [PATCH v3 many 0/5] notifications: feed system mails into proxmox_notify Lukas Wagner
2023-11-10 14:57 ` Lukas Wagner [this message]
2023-11-10 14:57 ` [pve-devel] [PATCH v3 proxmox-perl-rs 2/5] pve-rs: notify: remove notify_context for PVE Lukas Wagner
2023-11-10 14:57 ` [pve-devel] [PATCH v3 proxmox-mail-forward 3/5] feed forwarded mails into proxmox_notify Lukas Wagner
2023-11-10 14:57 ` [pve-devel] [PATCH v3 proxmox-mail-forward 4/5] update d/control Lukas Wagner
2023-11-10 14:57 ` [pve-devel] [PATCH v3 pve-docs 5/5] notifications: add documentation for system mail forwarding Lukas Wagner

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20231110145753.809938-2-l.wagner@proxmox.com \
    --to=l.wagner@proxmox.com \
    --cc=pve-devel@lists.proxmox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal