forked from Proxmox/proxmox
notify: add PVE/PBS context
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>
This commit is contained in:
parent
5f7ac875f6
commit
c1a3505e51
@ -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"]
|
||||
|
@ -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")
|
||||
}
|
27
proxmox-notify/src/context/common.rs
Normal file
27
proxmox-notify/src/context/common.rs
Normal file
@ -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()),
|
||||
}
|
||||
}
|
36
proxmox-notify/src/context/mod.rs
Normal file
36
proxmox-notify/src/context/mod.rs
Normal file
@ -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")
|
||||
}
|
130
proxmox-notify/src/context/pbs.rs
Normal file
130
proxmox-notify/src/context/pbs.rs
Normal file
@ -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
|
||||
);
|
||||
}
|
||||
}
|
82
proxmox-notify/src/context/pve.rs
Normal file
82
proxmox-notify/src/context/pve.rs
Normal file
@ -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);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user