* [proxmox-datacenter-manager] Add notifications backend
@ 2026-04-08 9:40 Arthur Bied-Charreton
2026-04-09 5:00 ` superseded: " Arthur Bied-Charreton
0 siblings, 1 reply; 2+ messages in thread
From: Arthur Bied-Charreton @ 2026-04-08 9:40 UTC (permalink / raw)
To: pdm-devel
Ideally, the whole router should be in proxmox-notify. However, after
discussing this quite extensively off-list with Lukas, we came to the
conclusion that doing so would require a large refactor that is
especially tricky given the fact that PVE would still need a different
entry point into proxmox-notify than PBS and PDM, since it defines its
router on the Perl side.
For this reason, factoring the routing logic out into proxmox-notify is
offset to a future where PVE also has a Rust-based router, and for now
it is just copied over from PBS (commit: fbf8d1b2) with the adaptations
described below.
The `proxmox_notify::context::Context` trait implementation is added to
PDM directly instead of creating a pdm.rs module in proxmox-notify to
avoid adding even more product-specific logic to it. It uses
`proxmox-access-control`, which is pulled in by PDM anyway, for getting
the user config instead of reading the file directly.
The `matcher-fields` and `matcher-field-values` endpoints currently
return empty vecs, they will be updated in a future commit introducing
actual notifications. The same applies for the notification templates.
The endpoints are made unprotected and the configuration files
read/writable by `www-data`, like for `remotes.cfg`.
Signed-off-by: Arthur Bied-Charreton <a.bied-charreton@proxmox.com>
---
Cargo.toml | 1 +
Makefile | 3 +-
debian/proxmox-datacenter-manager.install | 2 +
defines.mk | 1 +
lib/pdm-config/Cargo.toml | 1 +
lib/pdm-config/src/lib.rs | 1 +
lib/pdm-config/src/notifications.rs | 39 ++++
server/Cargo.toml | 1 +
server/src/api/config/mod.rs | 2 +
server/src/api/config/notifications/gotify.rs | 185 +++++++++++++++++
.../src/api/config/notifications/matchers.rs | 165 ++++++++++++++++
server/src/api/config/notifications/mod.rs | 102 ++++++++++
.../src/api/config/notifications/sendmail.rs | 173 ++++++++++++++++
server/src/api/config/notifications/smtp.rs | 186 ++++++++++++++++++
.../src/api/config/notifications/targets.rs | 61 ++++++
.../src/api/config/notifications/webhook.rs | 170 ++++++++++++++++
server/src/context.rs | 2 +
server/src/lib.rs | 1 +
server/src/notifications/mod.rs | 115 +++++++++++
templates/Makefile | 14 ++
templates/default/test-body.txt.hbs | 1 +
templates/default/test-subject.txt.hbs | 1 +
22 files changed, 1226 insertions(+), 1 deletion(-)
create mode 100644 lib/pdm-config/src/notifications.rs
create mode 100644 server/src/api/config/notifications/gotify.rs
create mode 100644 server/src/api/config/notifications/matchers.rs
create mode 100644 server/src/api/config/notifications/mod.rs
create mode 100644 server/src/api/config/notifications/sendmail.rs
create mode 100644 server/src/api/config/notifications/smtp.rs
create mode 100644 server/src/api/config/notifications/targets.rs
create mode 100644 server/src/api/config/notifications/webhook.rs
create mode 100644 server/src/notifications/mod.rs
create mode 100644 templates/Makefile
create mode 100644 templates/default/test-body.txt.hbs
create mode 100644 templates/default/test-subject.txt.hbs
diff --git a/Cargo.toml b/Cargo.toml
index ec2aa3d..918e831 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -47,6 +47,7 @@ proxmox-ldap = { version = "1.1", features = ["sync"] }
proxmox-lang = "1.1"
proxmox-log = "1"
proxmox-login = "1.0.2"
+proxmox-notify = "1"
proxmox-rest-server = "1"
# some use "cli", some use "cli" and "server", pbs-config uses nothing
proxmox-router = { version = "3.0.0", default-features = false }
diff --git a/Makefile b/Makefile
index 8e1cd13..48327c9 100644
--- a/Makefile
+++ b/Makefile
@@ -79,6 +79,7 @@ install: $(COMPILED_BINS) $(SHELL_COMPLETION_FILES)
install -m644 $(COMPLETION_DIR)/$(i) $(DESTDIR)$(ZSHCOMPDIR)/ ;)
make -C services install
$(MAKE) -C docs install
+ $(MAKE) -C templates install
$(COMPILED_BINS) $(COMPILEDIR)/docgen &:
$(CARGO) build $(CARGO_BUILD_ARGS)
@@ -99,7 +100,7 @@ cargo-build:
$(BUILDDIR):
rm -rf $@ $@.tmp
mkdir $@.tmp
- cp -a debian/ server/ services/ cli/ lib/ docs/ ui/ defines.mk Makefile Cargo.toml $@.tmp
+ cp -a debian/ server/ services/ cli/ lib/ docs/ ui/ templates/ defines.mk Makefile Cargo.toml $@.tmp
echo "git clone git://git.proxmox.com/git/$(PACKAGE).git\\ngit checkout $$(git rev-parse HEAD)" \
> $@.tmp/debian/SOURCE
mv $@.tmp $@
diff --git a/debian/proxmox-datacenter-manager.install b/debian/proxmox-datacenter-manager.install
index fad3f4a..fb2b1f1 100644
--- a/debian/proxmox-datacenter-manager.install
+++ b/debian/proxmox-datacenter-manager.install
@@ -20,3 +20,5 @@ usr/share/man/man5/remotes.cfg.5
usr/share/man/man5/views.cfg.5
usr/share/zsh/vendor-completions/_pdmAtoB
usr/share/zsh/vendor-completions/_proxmox-datacenter-manager-admin
+usr/share/proxmox-datacenter-manager/templates/default/test-body.txt.hbs
+usr/share/proxmox-datacenter-manager/templates/default/test-subject.txt.hbs
diff --git a/defines.mk b/defines.mk
index 1b9c1a8..111923f 100644
--- a/defines.mk
+++ b/defines.mk
@@ -3,6 +3,7 @@ BINDIR = $(PREFIX)/bin
SBINDIR = $(PREFIX)/sbin
LIBDIR = $(PREFIX)/lib
LIBEXECDIR = $(PREFIX)/libexec
+DATAROOTDIR = $(PREFIX)/share
BASHCOMPDIR = $(PREFIX)/share/bash-completion/completions
ZSHCOMPDIR = $(PREFIX)/share/zsh/vendor-completions
MAN1DIR = $(PREFIX)/share/man/man1
diff --git a/lib/pdm-config/Cargo.toml b/lib/pdm-config/Cargo.toml
index d39c2ad..b6f3739 100644
--- a/lib/pdm-config/Cargo.toml
+++ b/lib/pdm-config/Cargo.toml
@@ -23,5 +23,6 @@ proxmox-shared-memory.workspace = true
proxmox-simple-config.workspace = true
proxmox-sys = { workspace = true, features = [ "acl", "crypt", "timer" ] }
proxmox-acme-api.workspace = true
+proxmox-notify.workspace = true
pdm-api-types.workspace = true
pdm-buildcfg.workspace = true
diff --git a/lib/pdm-config/src/lib.rs b/lib/pdm-config/src/lib.rs
index 4c49054..03dc247 100644
--- a/lib/pdm-config/src/lib.rs
+++ b/lib/pdm-config/src/lib.rs
@@ -5,6 +5,7 @@ pub use pdm_buildcfg::{BACKUP_GROUP_NAME, BACKUP_USER_NAME};
pub mod certificate_config;
pub mod domains;
pub mod node;
+pub mod notifications;
pub mod remotes;
pub mod setup;
pub mod views;
diff --git a/lib/pdm-config/src/notifications.rs b/lib/pdm-config/src/notifications.rs
new file mode 100644
index 0000000..fd6ec79
--- /dev/null
+++ b/lib/pdm-config/src/notifications.rs
@@ -0,0 +1,39 @@
+use anyhow::Error;
+
+use proxmox_notify::Config;
+
+use pdm_buildcfg::configdir;
+use proxmox_product_config::{open_api_lockfile, replace_config, ApiLockGuard};
+use proxmox_sys::fs::file_read_optional_string;
+
+/// Configuration file location for notification targets/matchers.
+pub const NOTIFICATION_CONFIG_PATH: &str = configdir!("/notifications.cfg");
+
+/// Private configuration file location for secrets - only readable by `root`.
+pub const NOTIFICATION_PRIV_CONFIG_PATH: &str = configdir!("/notifications-priv.cfg");
+
+/// Lockfile to prevent concurrent write access.
+pub const NOTIFICATION_LOCK_FILE: &str = configdir!("/.notifications.lck");
+
+/// Get exclusive lock for `notifications.cfg`.
+pub fn lock_config() -> Result<ApiLockGuard, Error> {
+ open_api_lockfile(NOTIFICATION_LOCK_FILE, None, true)
+}
+
+/// Load notification config.
+pub fn config() -> Result<Config, Error> {
+ let content = file_read_optional_string(NOTIFICATION_CONFIG_PATH)?.unwrap_or_default();
+
+ let priv_content =
+ file_read_optional_string(NOTIFICATION_PRIV_CONFIG_PATH)?.unwrap_or_default();
+
+ Ok(Config::new(&content, &priv_content)?)
+}
+
+/// Save notification config.
+pub fn save_config(config: Config) -> Result<(), Error> {
+ let (cfg, priv_cfg) = config.write()?;
+ replace_config(NOTIFICATION_CONFIG_PATH, cfg.as_bytes())?;
+ replace_config(NOTIFICATION_PRIV_CONFIG_PATH, priv_cfg.as_bytes())?;
+ Ok(())
+}
diff --git a/server/Cargo.toml b/server/Cargo.toml
index 6969549..a4f7bbd 100644
--- a/server/Cargo.toml
+++ b/server/Cargo.toml
@@ -46,6 +46,7 @@ proxmox-lang.workspace = true
proxmox-ldap.workspace = true
proxmox-log.workspace = true
proxmox-login.workspace = true
+proxmox-notify.workspace = true
proxmox-openid.workspace = true
proxmox-rest-server = { workspace = true, features = [ "templates" ] }
proxmox-router = { workspace = true, features = [ "cli", "server"] }
diff --git a/server/src/api/config/mod.rs b/server/src/api/config/mod.rs
index 8f646c1..a465219 100644
--- a/server/src/api/config/mod.rs
+++ b/server/src/api/config/mod.rs
@@ -6,6 +6,7 @@ pub mod access;
pub mod acme;
pub mod certificate;
pub mod notes;
+pub mod notifications;
pub mod views;
#[sortable]
@@ -14,6 +15,7 @@ const SUBDIRS: SubdirMap = &sorted!([
("acme", &acme::ROUTER),
("certificate", &certificate::ROUTER),
("notes", ¬es::ROUTER),
+ ("notifications", ¬ifications::ROUTER),
("views", &views::ROUTER)
]);
diff --git a/server/src/api/config/notifications/gotify.rs b/server/src/api/config/notifications/gotify.rs
new file mode 100644
index 0000000..a281245
--- /dev/null
+++ b/server/src/api/config/notifications/gotify.rs
@@ -0,0 +1,185 @@
+use anyhow::Error;
+use serde_json::Value;
+
+use proxmox_notify::endpoints::gotify::{
+ DeleteableGotifyProperty, GotifyConfig, GotifyConfigUpdater, GotifyPrivateConfig,
+ GotifyPrivateConfigUpdater,
+};
+use proxmox_notify::schema::ENTITY_NAME_SCHEMA;
+use proxmox_router::{Permission, Router, RpcEnvironment};
+use proxmox_schema::api;
+
+use pbs_api_types::{PRIV_SYS_AUDIT, PRIV_SYS_MODIFY, PROXMOX_CONFIG_DIGEST_SCHEMA};
+
+#[api(
+ input: {
+ properties: {},
+ },
+ returns: {
+ description: "List of gotify endpoints.",
+ type: Array,
+ items: { type: GotifyConfig },
+ },
+ access: {
+ permission: &Permission::Privilege(&["system", "notifications"], PRIV_SYS_AUDIT, false),
+ },
+)]
+/// List all gotify endpoints.
+pub fn list_endpoints(
+ _param: Value,
+ _rpcenv: &mut dyn RpcEnvironment,
+) -> Result<Vec<GotifyConfig>, Error> {
+ let config = pdm_config::notifications::config()?;
+
+ let endpoints = proxmox_notify::api::gotify::get_endpoints(&config)?;
+
+ Ok(endpoints)
+}
+
+#[api(
+ input: {
+ properties: {
+ name: {
+ schema: ENTITY_NAME_SCHEMA,
+ }
+ },
+ },
+ returns: { type: GotifyConfig },
+ access: {
+ permission: &Permission::Privilege(&["system", "notifications"], PRIV_SYS_AUDIT, false),
+ },
+)]
+/// Get a gotify endpoint.
+pub fn get_endpoint(name: String, rpcenv: &mut dyn RpcEnvironment) -> Result<GotifyConfig, Error> {
+ let config = pdm_config::notifications::config()?;
+ let endpoint = proxmox_notify::api::gotify::get_endpoint(&config, &name)?;
+
+ rpcenv["digest"] = hex::encode(config.digest()).into();
+
+ Ok(endpoint)
+}
+
+#[api(
+ input: {
+ properties: {
+ endpoint: {
+ type: GotifyConfig,
+ flatten: true,
+ },
+ token: {
+ description: "Authentication token",
+ }
+ },
+ },
+ access: {
+ permission: &Permission::Privilege(&["system", "notifications"], PRIV_SYS_MODIFY, false),
+ },
+)]
+/// Add a new gotify endpoint.
+pub fn add_endpoint(
+ endpoint: GotifyConfig,
+ token: String,
+ _rpcenv: &mut dyn RpcEnvironment,
+) -> Result<(), Error> {
+ let _lock = pdm_config::notifications::lock_config()?;
+ let mut config = pdm_config::notifications::config()?;
+ let private_endpoint_config = GotifyPrivateConfig {
+ name: endpoint.name.clone(),
+ token,
+ };
+
+ proxmox_notify::api::gotify::add_endpoint(&mut config, endpoint, private_endpoint_config)?;
+
+ pdm_config::notifications::save_config(config)?;
+ Ok(())
+}
+
+#[api(
+ input: {
+ properties: {
+ name: {
+ schema: ENTITY_NAME_SCHEMA,
+ },
+ updater: {
+ type: GotifyConfigUpdater,
+ flatten: true,
+ },
+ token: {
+ description: "Authentication token",
+ optional: true,
+ },
+ delete: {
+ description: "List of properties to delete.",
+ type: Array,
+ optional: true,
+ items: {
+ type: DeleteableGotifyProperty,
+ }
+ },
+ digest: {
+ optional: true,
+ schema: PROXMOX_CONFIG_DIGEST_SCHEMA,
+ },
+ },
+ },
+ access: {
+ permission: &Permission::Privilege(&["system", "notifications"], PRIV_SYS_MODIFY, false),
+ },
+)]
+/// Update gotify endpoint.
+pub fn update_endpoint(
+ name: String,
+ updater: GotifyConfigUpdater,
+ token: Option<String>,
+ delete: Option<Vec<DeleteableGotifyProperty>>,
+ digest: Option<String>,
+ _rpcenv: &mut dyn RpcEnvironment,
+) -> Result<(), Error> {
+ let _lock = pdm_config::notifications::lock_config()?;
+ let mut config = pdm_config::notifications::config()?;
+ let digest = digest.map(hex::decode).transpose()?;
+
+ proxmox_notify::api::gotify::update_endpoint(
+ &mut config,
+ &name,
+ updater,
+ GotifyPrivateConfigUpdater { token },
+ delete.as_deref(),
+ digest.as_deref(),
+ )?;
+
+ pdm_config::notifications::save_config(config)?;
+ Ok(())
+}
+
+#[api(
+ input: {
+ properties: {
+ name: {
+ schema: ENTITY_NAME_SCHEMA,
+ }
+ },
+ },
+ access: {
+ permission: &Permission::Privilege(&["system", "notifications"], PRIV_SYS_MODIFY, false),
+ },
+)]
+/// Delete gotify endpoint.
+pub fn delete_endpoint(name: String, _rpcenv: &mut dyn RpcEnvironment) -> Result<(), Error> {
+ let _lock = pdm_config::notifications::lock_config()?;
+ let mut config = pdm_config::notifications::config()?;
+ proxmox_notify::api::gotify::delete_gotify_endpoint(&mut config, &name)?;
+
+ pdm_config::notifications::save_config(config)?;
+ Ok(())
+}
+
+const ITEM_ROUTER: Router = Router::new()
+ .get(&API_METHOD_GET_ENDPOINT)
+ .put(&API_METHOD_UPDATE_ENDPOINT)
+ .delete(&API_METHOD_DELETE_ENDPOINT);
+
+pub const ROUTER: Router = Router::new()
+ .get(&API_METHOD_LIST_ENDPOINTS)
+ .post(&API_METHOD_ADD_ENDPOINT)
+ .match_all("name", &ITEM_ROUTER);
diff --git a/server/src/api/config/notifications/matchers.rs b/server/src/api/config/notifications/matchers.rs
new file mode 100644
index 0000000..9918305
--- /dev/null
+++ b/server/src/api/config/notifications/matchers.rs
@@ -0,0 +1,165 @@
+use anyhow::Error;
+use serde_json::Value;
+
+use proxmox_notify::matcher::{DeleteableMatcherProperty, MatcherConfig, MatcherConfigUpdater};
+use proxmox_notify::schema::ENTITY_NAME_SCHEMA;
+use proxmox_router::{Permission, Router, RpcEnvironment};
+use proxmox_schema::api;
+
+use pdm_api_types::{PRIV_SYS_AUDIT, PRIV_SYS_MODIFY, PROXMOX_CONFIG_DIGEST_SCHEMA};
+
+#[api(
+ input: {
+ properties: {},
+ },
+ returns: {
+ description: "List of matchers.",
+ type: Array,
+ items: { type: MatcherConfig },
+ },
+ access: {
+ permission: &Permission::Privilege(&["system", "notifications"], PRIV_SYS_AUDIT, false),
+ },
+)]
+/// List all notification matchers.
+pub fn list_matchers(
+ _param: Value,
+ _rpcenv: &mut dyn RpcEnvironment,
+) -> Result<Vec<MatcherConfig>, Error> {
+ let config = pdm_config::notifications::config()?;
+
+ let matchers = proxmox_notify::api::matcher::get_matchers(&config)?;
+
+ Ok(matchers)
+}
+
+#[api(
+ input: {
+ properties: {
+ name: {
+ schema: ENTITY_NAME_SCHEMA,
+ }
+ },
+ },
+ returns: { type: MatcherConfig },
+ access: {
+ permission: &Permission::Privilege(&["system", "notifications"], PRIV_SYS_AUDIT, false),
+ },
+)]
+/// Get a notification matcher.
+pub fn get_matcher(name: String, rpcenv: &mut dyn RpcEnvironment) -> Result<MatcherConfig, Error> {
+ let config = pdm_config::notifications::config()?;
+ let matcher = proxmox_notify::api::matcher::get_matcher(&config, &name)?;
+
+ rpcenv["digest"] = hex::encode(config.digest()).into();
+
+ Ok(matcher)
+}
+
+#[api(
+ input: {
+ properties: {
+ matcher: {
+ type: MatcherConfig,
+ flatten: true,
+ }
+ },
+ },
+ access: {
+ permission: &Permission::Privilege(&["system", "notifications"], PRIV_SYS_MODIFY, false),
+ },
+)]
+/// Add a new notification matcher.
+pub fn add_matcher(matcher: MatcherConfig, _rpcenv: &mut dyn RpcEnvironment) -> Result<(), Error> {
+ let _lock = pdm_config::notifications::lock_config()?;
+ let mut config = pdm_config::notifications::config()?;
+
+ proxmox_notify::api::matcher::add_matcher(&mut config, matcher)?;
+
+ pdm_config::notifications::save_config(config)?;
+ Ok(())
+}
+
+#[api(
+ input: {
+ properties: {
+ name: {
+ schema: ENTITY_NAME_SCHEMA,
+ },
+ updater: {
+ type: MatcherConfigUpdater,
+ flatten: true,
+ },
+ delete: {
+ description: "List of properties to delete.",
+ type: Array,
+ optional: true,
+ items: {
+ type: DeleteableMatcherProperty,
+ }
+ },
+ digest: {
+ optional: true,
+ schema: PROXMOX_CONFIG_DIGEST_SCHEMA,
+ },
+ },
+ },
+ access: {
+ permission: &Permission::Privilege(&["system", "notifications"], PRIV_SYS_MODIFY, false),
+ },
+)]
+/// Update notification matcher.
+pub fn update_matcher(
+ name: String,
+ updater: MatcherConfigUpdater,
+ delete: Option<Vec<DeleteableMatcherProperty>>,
+ digest: Option<String>,
+ _rpcenv: &mut dyn RpcEnvironment,
+) -> Result<(), Error> {
+ let _lock = pdm_config::notifications::lock_config()?;
+ let mut config = pdm_config::notifications::config()?;
+ let digest = digest.map(hex::decode).transpose()?;
+
+ proxmox_notify::api::matcher::update_matcher(
+ &mut config,
+ &name,
+ updater,
+ delete.as_deref(),
+ digest.as_deref(),
+ )?;
+
+ pdm_config::notifications::save_config(config)?;
+ Ok(())
+}
+
+#[api(
+ input: {
+ properties: {
+ name: {
+ schema: ENTITY_NAME_SCHEMA,
+ }
+ },
+ },
+ access: {
+ permission: &Permission::Privilege(&["system", "notifications"], PRIV_SYS_MODIFY, false),
+ },
+)]
+/// Delete notification matcher.
+pub fn delete_matcher(name: String, _rpcenv: &mut dyn RpcEnvironment) -> Result<(), Error> {
+ let _lock = pdm_config::notifications::lock_config()?;
+ let mut config = pdm_config::notifications::config()?;
+ proxmox_notify::api::matcher::delete_matcher(&mut config, &name)?;
+
+ pdm_config::notifications::save_config(config)?;
+ Ok(())
+}
+
+const ITEM_ROUTER: Router = Router::new()
+ .get(&API_METHOD_GET_MATCHER)
+ .put(&API_METHOD_UPDATE_MATCHER)
+ .delete(&API_METHOD_DELETE_MATCHER);
+
+pub const ROUTER: Router = Router::new()
+ .get(&API_METHOD_LIST_MATCHERS)
+ .post(&API_METHOD_ADD_MATCHER)
+ .match_all("name", &ITEM_ROUTER);
diff --git a/server/src/api/config/notifications/mod.rs b/server/src/api/config/notifications/mod.rs
new file mode 100644
index 0000000..5730116
--- /dev/null
+++ b/server/src/api/config/notifications/mod.rs
@@ -0,0 +1,102 @@
+use anyhow::Error;
+use pdm_api_types::PRIV_SYS_AUDIT;
+use proxmox_router::{
+ list_subdirs_api_method, ApiMethod, Permission, Router, RpcEnvironment, SubdirMap,
+};
+use proxmox_schema::api;
+use proxmox_sortable_macro::sortable;
+use serde::Serialize;
+use serde_json::Value;
+
+pub mod gotify;
+pub mod matchers;
+pub mod sendmail;
+pub mod smtp;
+pub mod targets;
+pub mod webhook;
+
+#[sortable]
+const SUBDIRS: SubdirMap = &sorted!([
+ ("endpoints", &ENDPOINT_ROUTER),
+ ("matcher-fields", &FIELD_ROUTER),
+ ("matcher-field-values", &VALUE_ROUTER),
+ ("targets", &targets::ROUTER),
+ ("matchers", &matchers::ROUTER),
+]);
+
+pub const ROUTER: Router = Router::new()
+ .get(&list_subdirs_api_method!(SUBDIRS))
+ .subdirs(SUBDIRS);
+
+#[sortable]
+const ENDPOINT_SUBDIRS: SubdirMap = &sorted!([
+ ("gotify", &gotify::ROUTER),
+ ("sendmail", &sendmail::ROUTER),
+ ("smtp", &smtp::ROUTER),
+ ("webhook", &webhook::ROUTER),
+]);
+
+const ENDPOINT_ROUTER: Router = Router::new()
+ .get(&list_subdirs_api_method!(ENDPOINT_SUBDIRS))
+ .subdirs(ENDPOINT_SUBDIRS);
+
+const FIELD_ROUTER: Router = Router::new().get(&API_METHOD_GET_FIELDS);
+const VALUE_ROUTER: Router = Router::new().get(&API_METHOD_GET_VALUES);
+
+#[api]
+#[derive(Serialize)]
+/// A matchable field
+pub struct MatchableField {
+ /// Name of the field
+ name: String,
+}
+
+#[api]
+#[derive(Serialize)]
+/// A matchable metadata field value
+pub struct MatchableValue {
+ /// Field this value belongs to.
+ field: String,
+ /// Notification metadata value known by the system.
+ value: String,
+ /// Additional comment for this value.
+ comment: Option<String>,
+}
+
+#[api(
+ input: {
+ properties: {}
+ },
+ returns: {
+ description: "List of known metadata fields.",
+ type: Array,
+ items: {type: MatchableField},
+ },
+ access: {permission: &Permission::Privilege(&["system", "notifications"], PRIV_SYS_AUDIT, false)},
+)]
+/// Get all known metadata fields.
+pub fn get_fields() -> Result<Vec<MatchableField>, Error> {
+ Ok(vec![])
+}
+
+#[api(
+ input: {
+ properties: {},
+ },
+ returns: {
+ description: "List of known metadata field values.",
+ type: Array,
+ items: { type: MatchableValue },
+ },
+ access: {
+ permission: &Permission::Privilege(&["system", "notifications"], PRIV_SYS_AUDIT, false),
+ },
+)]
+/// List all known, matchable metadata field values
+pub fn get_values(
+ _param: Value,
+ _info: &ApiMethod,
+ _rpcenv: &mut dyn RpcEnvironment,
+) -> Result<Vec<MatchableValue>, Error> {
+ Ok(vec![])
+}
diff --git a/server/src/api/config/notifications/sendmail.rs b/server/src/api/config/notifications/sendmail.rs
new file mode 100644
index 0000000..66c9656
--- /dev/null
+++ b/server/src/api/config/notifications/sendmail.rs
@@ -0,0 +1,173 @@
+use anyhow::Error;
+use serde_json::Value;
+
+use proxmox_notify::endpoints::sendmail::{
+ DeleteableSendmailProperty, SendmailConfig, SendmailConfigUpdater,
+};
+use proxmox_notify::schema::ENTITY_NAME_SCHEMA;
+use proxmox_router::{Permission, Router, RpcEnvironment};
+use proxmox_schema::api;
+
+use pdm_api_types::{PRIV_SYS_AUDIT, PRIV_SYS_MODIFY, PROXMOX_CONFIG_DIGEST_SCHEMA};
+
+#[api(
+ input: {
+ properties: {},
+ },
+ returns: {
+ description: "List of sendmail endpoints.",
+ type: Array,
+ items: { type: SendmailConfig },
+ },
+ access: {
+ permission: &Permission::Privilege(&["system", "notifications"], PRIV_SYS_AUDIT, false),
+ },
+)]
+/// List all sendmail endpoints.
+pub fn list_endpoints(
+ _param: Value,
+ _rpcenv: &mut dyn RpcEnvironment,
+) -> Result<Vec<SendmailConfig>, Error> {
+ let config = pdm_config::notifications::config()?;
+
+ let endpoints = proxmox_notify::api::sendmail::get_endpoints(&config)?;
+
+ Ok(endpoints)
+}
+
+#[api(
+ input: {
+ properties: {
+ name: {
+ schema: ENTITY_NAME_SCHEMA,
+ }
+ },
+ },
+ returns: { type: SendmailConfig },
+ access: {
+ permission: &Permission::Privilege(&["system", "notifications"], PRIV_SYS_AUDIT, false),
+ },
+)]
+/// Get a sendmail endpoint.
+pub fn get_endpoint(
+ name: String,
+ rpcenv: &mut dyn RpcEnvironment,
+) -> Result<SendmailConfig, Error> {
+ let config = pdm_config::notifications::config()?;
+ let endpoint = proxmox_notify::api::sendmail::get_endpoint(&config, &name)?;
+
+ rpcenv["digest"] = hex::encode(config.digest()).into();
+
+ Ok(endpoint)
+}
+
+#[api(
+ input: {
+ properties: {
+ endpoint: {
+ type: SendmailConfig,
+ flatten: true,
+ }
+ },
+ },
+ access: {
+ permission: &Permission::Privilege(&["system", "notifications"], PRIV_SYS_MODIFY, false),
+ },
+)]
+/// Add a new sendmail endpoint.
+pub fn add_endpoint(
+ endpoint: SendmailConfig,
+ _rpcenv: &mut dyn RpcEnvironment,
+) -> Result<(), Error> {
+ let _lock = pdm_config::notifications::lock_config()?;
+ let mut config = pdm_config::notifications::config()?;
+
+ proxmox_notify::api::sendmail::add_endpoint(&mut config, endpoint)?;
+
+ pdm_config::notifications::save_config(config)?;
+ Ok(())
+}
+
+#[api(
+ input: {
+ properties: {
+ name: {
+ schema: ENTITY_NAME_SCHEMA,
+ },
+ updater: {
+ type: SendmailConfigUpdater,
+ flatten: true,
+ },
+ delete: {
+ description: "List of properties to delete.",
+ type: Array,
+ optional: true,
+ items: {
+ type: DeleteableSendmailProperty,
+ }
+ },
+ digest: {
+ optional: true,
+ schema: PROXMOX_CONFIG_DIGEST_SCHEMA,
+ },
+ },
+ },
+ access: {
+ permission: &Permission::Privilege(&["system", "notifications"], PRIV_SYS_MODIFY, false),
+ },
+)]
+/// Update sendmail endpoint.
+pub fn update_endpoint(
+ name: String,
+ updater: SendmailConfigUpdater,
+ delete: Option<Vec<DeleteableSendmailProperty>>,
+ digest: Option<String>,
+ _rpcenv: &mut dyn RpcEnvironment,
+) -> Result<(), Error> {
+ let _lock = pdm_config::notifications::lock_config()?;
+ let mut config = pdm_config::notifications::config()?;
+ let digest = digest.map(hex::decode).transpose()?;
+
+ proxmox_notify::api::sendmail::update_endpoint(
+ &mut config,
+ &name,
+ updater,
+ delete.as_deref(),
+ digest.as_deref(),
+ )?;
+
+ pdm_config::notifications::save_config(config)?;
+ Ok(())
+}
+
+#[api(
+ input: {
+ properties: {
+ name: {
+ schema: ENTITY_NAME_SCHEMA,
+ }
+ },
+ },
+ access: {
+ permission: &Permission::Privilege(&["system", "notifications"], PRIV_SYS_MODIFY, false),
+ },
+)]
+/// Delete sendmail endpoint.
+pub fn delete_endpoint(name: String, _rpcenv: &mut dyn RpcEnvironment) -> Result<(), Error> {
+ let _lock = pdm_config::notifications::lock_config()?;
+ let mut config = pdm_config::notifications::config()?;
+ proxmox_notify::api::sendmail::delete_endpoint(&mut config, &name)?;
+
+ pdm_config::notifications::save_config(config)?;
+ Ok(())
+}
+
+const ITEM_ROUTER: Router = Router::new()
+ .get(&API_METHOD_GET_ENDPOINT)
+ .put(&API_METHOD_UPDATE_ENDPOINT)
+ .delete(&API_METHOD_DELETE_ENDPOINT);
+
+pub const ROUTER: Router = Router::new()
+ .get(&API_METHOD_LIST_ENDPOINTS)
+ .post(&API_METHOD_ADD_ENDPOINT)
+ .match_all("name", &ITEM_ROUTER);
diff --git a/server/src/api/config/notifications/smtp.rs b/server/src/api/config/notifications/smtp.rs
new file mode 100644
index 0000000..4a48256
--- /dev/null
+++ b/server/src/api/config/notifications/smtp.rs
@@ -0,0 +1,186 @@
+use anyhow::Error;
+use serde_json::Value;
+
+use proxmox_notify::endpoints::smtp::{
+ DeleteableSmtpProperty, SmtpConfig, SmtpConfigUpdater, SmtpPrivateConfig,
+ SmtpPrivateConfigUpdater,
+};
+use proxmox_notify::schema::ENTITY_NAME_SCHEMA;
+use proxmox_router::{Permission, Router, RpcEnvironment};
+use proxmox_schema::api;
+
+use pdm_api_types::{PRIV_SYS_AUDIT, PRIV_SYS_MODIFY, PROXMOX_CONFIG_DIGEST_SCHEMA};
+
+#[api(
+ input: {
+ properties: {},
+ },
+ returns: {
+ description: "List of smtp endpoints.",
+ type: Array,
+ items: { type: SmtpConfig },
+ },
+ access: {
+ permission: &Permission::Privilege(&["system", "notifications"], PRIV_SYS_AUDIT, false),
+ },
+)]
+/// List all smtp endpoints.
+pub fn list_endpoints(
+ _param: Value,
+ _rpcenv: &mut dyn RpcEnvironment,
+) -> Result<Vec<SmtpConfig>, Error> {
+ let config = pdm_config::notifications::config()?;
+
+ let endpoints = proxmox_notify::api::smtp::get_endpoints(&config)?;
+
+ Ok(endpoints)
+}
+
+#[api(
+ input: {
+ properties: {
+ name: {
+ schema: ENTITY_NAME_SCHEMA,
+ }
+ },
+ },
+ returns: { type: SmtpConfig },
+ access: {
+ permission: &Permission::Privilege(&["system", "notifications"], PRIV_SYS_AUDIT, false),
+ },
+)]
+/// Get a smtp endpoint.
+pub fn get_endpoint(name: String, rpcenv: &mut dyn RpcEnvironment) -> Result<SmtpConfig, Error> {
+ let config = pdm_config::notifications::config()?;
+ let endpoint = proxmox_notify::api::smtp::get_endpoint(&config, &name)?;
+
+ rpcenv["digest"] = hex::encode(config.digest()).into();
+
+ Ok(endpoint)
+}
+
+#[api(
+ input: {
+ properties: {
+ endpoint: {
+ type: SmtpConfig,
+ flatten: true,
+ },
+ password: {
+ optional: true,
+ description: "SMTP authentication password"
+ }
+ },
+ },
+ access: {
+ permission: &Permission::Privilege(&["system", "notifications"], PRIV_SYS_MODIFY, false),
+ },
+)]
+/// Add a new smtp endpoint.
+pub fn add_endpoint(
+ endpoint: SmtpConfig,
+ password: Option<String>,
+ _rpcenv: &mut dyn RpcEnvironment,
+) -> Result<(), Error> {
+ let _lock = pdm_config::notifications::lock_config()?;
+ let mut config = pdm_config::notifications::config()?;
+ let private_endpoint_config = SmtpPrivateConfig {
+ name: endpoint.name.clone(),
+ password,
+ };
+
+ proxmox_notify::api::smtp::add_endpoint(&mut config, endpoint, private_endpoint_config)?;
+
+ pdm_config::notifications::save_config(config)?;
+ Ok(())
+}
+
+#[api(
+ input: {
+ properties: {
+ name: {
+ schema: ENTITY_NAME_SCHEMA,
+ },
+ updater: {
+ type: SmtpConfigUpdater,
+ flatten: true,
+ },
+ password: {
+ description: "SMTP authentication password",
+ optional: true,
+ },
+ delete: {
+ description: "List of properties to delete.",
+ type: Array,
+ optional: true,
+ items: {
+ type: DeleteableSmtpProperty,
+ }
+ },
+ digest: {
+ optional: true,
+ schema: PROXMOX_CONFIG_DIGEST_SCHEMA,
+ },
+ },
+ },
+ access: {
+ permission: &Permission::Privilege(&["system", "notifications"], PRIV_SYS_MODIFY, false),
+ },
+)]
+/// Update smtp endpoint.
+pub fn update_endpoint(
+ name: String,
+ updater: SmtpConfigUpdater,
+ password: Option<String>,
+ delete: Option<Vec<DeleteableSmtpProperty>>,
+ digest: Option<String>,
+ _rpcenv: &mut dyn RpcEnvironment,
+) -> Result<(), Error> {
+ let _lock = pdm_config::notifications::lock_config()?;
+ let mut config = pdm_config::notifications::config()?;
+ let digest = digest.map(hex::decode).transpose()?;
+
+ proxmox_notify::api::smtp::update_endpoint(
+ &mut config,
+ &name,
+ updater,
+ SmtpPrivateConfigUpdater { password },
+ delete.as_deref(),
+ digest.as_deref(),
+ )?;
+
+ pdm_config::notifications::save_config(config)?;
+ Ok(())
+}
+
+#[api(
+ input: {
+ properties: {
+ name: {
+ schema: ENTITY_NAME_SCHEMA,
+ }
+ },
+ },
+ access: {
+ permission: &Permission::Privilege(&["system", "notifications"], PRIV_SYS_MODIFY, false),
+ },
+)]
+/// Delete smtp endpoint.
+pub fn delete_endpoint(name: String, _rpcenv: &mut dyn RpcEnvironment) -> Result<(), Error> {
+ let _lock = pdm_config::notifications::lock_config()?;
+ let mut config = pdm_config::notifications::config()?;
+ proxmox_notify::api::smtp::delete_endpoint(&mut config, &name)?;
+
+ pdm_config::notifications::save_config(config)?;
+ Ok(())
+}
+
+const ITEM_ROUTER: Router = Router::new()
+ .get(&API_METHOD_GET_ENDPOINT)
+ .put(&API_METHOD_UPDATE_ENDPOINT)
+ .delete(&API_METHOD_DELETE_ENDPOINT);
+
+pub const ROUTER: Router = Router::new()
+ .get(&API_METHOD_LIST_ENDPOINTS)
+ .post(&API_METHOD_ADD_ENDPOINT)
+ .match_all("name", &ITEM_ROUTER);
diff --git a/server/src/api/config/notifications/targets.rs b/server/src/api/config/notifications/targets.rs
new file mode 100644
index 0000000..b2655fb
--- /dev/null
+++ b/server/src/api/config/notifications/targets.rs
@@ -0,0 +1,61 @@
+use anyhow::Error;
+use serde_json::Value;
+
+use proxmox_notify::api::Target;
+use proxmox_notify::schema::ENTITY_NAME_SCHEMA;
+use proxmox_router::{list_subdirs_api_method, Permission, Router, RpcEnvironment, SubdirMap};
+use proxmox_schema::api;
+use proxmox_sortable_macro::sortable;
+
+use pdm_api_types::{PRIV_SYS_AUDIT, PRIV_SYS_MODIFY};
+
+#[api(
+ input: {
+ properties: {},
+ },
+ returns: {
+ description: "List of all entities which can be used as notification targets.",
+ type: Array,
+ items: { type: Target },
+ },
+ access: {
+ permission: &Permission::Privilege(&["system", "notifications"], PRIV_SYS_AUDIT, false),
+ },
+)]
+/// List all notification targets.
+pub fn list_targets(_param: Value, _rpcenv: &mut dyn RpcEnvironment) -> Result<Vec<Target>, Error> {
+ let config = pdm_config::notifications::config()?;
+ let targets = proxmox_notify::api::get_targets(&config)?;
+
+ Ok(targets)
+}
+
+#[api(
+ input: {
+ properties: {
+ name: {
+ schema: ENTITY_NAME_SCHEMA,
+ },
+ }
+ },
+ access: {
+ permission: &Permission::Privilege(&["system", "notifications"], PRIV_SYS_MODIFY, false),
+ },
+)]
+/// Test a given notification target.
+pub fn test_target(name: String, _rpcenv: &mut dyn RpcEnvironment) -> Result<(), Error> {
+ let config = pdm_config::notifications::config()?;
+ proxmox_notify::api::common::test_target(&config, &name)?;
+ Ok(())
+}
+
+#[sortable]
+const SUBDIRS: SubdirMap = &sorted!([("test", &TEST_ROUTER),]);
+const TEST_ROUTER: Router = Router::new().post(&API_METHOD_TEST_TARGET);
+const ITEM_ROUTER: Router = Router::new()
+ .get(&list_subdirs_api_method!(SUBDIRS))
+ .subdirs(SUBDIRS);
+
+pub const ROUTER: Router = Router::new()
+ .get(&API_METHOD_LIST_TARGETS)
+ .match_all("name", &ITEM_ROUTER);
diff --git a/server/src/api/config/notifications/webhook.rs b/server/src/api/config/notifications/webhook.rs
new file mode 100644
index 0000000..751086f
--- /dev/null
+++ b/server/src/api/config/notifications/webhook.rs
@@ -0,0 +1,170 @@
+use anyhow::Error;
+use serde_json::Value;
+
+use proxmox_notify::endpoints::webhook::{
+ DeleteableWebhookProperty, WebhookConfig, WebhookConfigUpdater,
+};
+use proxmox_notify::schema::ENTITY_NAME_SCHEMA;
+use proxmox_router::{Permission, Router, RpcEnvironment};
+use proxmox_schema::api;
+
+use pdm_api_types::{PRIV_SYS_AUDIT, PRIV_SYS_MODIFY, PROXMOX_CONFIG_DIGEST_SCHEMA};
+
+#[api(
+ input: {
+ properties: {},
+ },
+ returns: {
+ description: "List of webhook endpoints.",
+ type: Array,
+ items: { type: WebhookConfig },
+ },
+ access: {
+ permission: &Permission::Privilege(&["system", "notifications"], PRIV_SYS_AUDIT, false),
+ },
+)]
+/// List all webhook endpoints.
+pub fn list_endpoints(
+ _param: Value,
+ _rpcenv: &mut dyn RpcEnvironment,
+) -> Result<Vec<WebhookConfig>, Error> {
+ let config = pdm_config::notifications::config()?;
+
+ let endpoints = proxmox_notify::api::webhook::get_endpoints(&config)?;
+
+ Ok(endpoints)
+}
+
+#[api(
+ input: {
+ properties: {
+ name: {
+ schema: ENTITY_NAME_SCHEMA,
+ }
+ },
+ },
+ returns: { type: WebhookConfig },
+ access: {
+ permission: &Permission::Privilege(&["system", "notifications"], PRIV_SYS_AUDIT, false),
+ },
+)]
+/// Get a webhook endpoint.
+pub fn get_endpoint(name: String, rpcenv: &mut dyn RpcEnvironment) -> Result<WebhookConfig, Error> {
+ let config = pdm_config::notifications::config()?;
+ let endpoint = proxmox_notify::api::webhook::get_endpoint(&config, &name)?;
+
+ rpcenv["digest"] = hex::encode(config.digest()).into();
+
+ Ok(endpoint)
+}
+
+#[api(
+ input: {
+ properties: {
+ endpoint: {
+ type: WebhookConfig,
+ flatten: true,
+ },
+ },
+ },
+ access: {
+ permission: &Permission::Privilege(&["system", "notifications"], PRIV_SYS_MODIFY, false),
+ },
+)]
+/// Add a new webhook endpoint.
+pub fn add_endpoint(
+ endpoint: WebhookConfig,
+ _rpcenv: &mut dyn RpcEnvironment,
+) -> Result<(), Error> {
+ let _lock = pdm_config::notifications::lock_config()?;
+ let mut config = pdm_config::notifications::config()?;
+
+ proxmox_notify::api::webhook::add_endpoint(&mut config, endpoint)?;
+
+ pdm_config::notifications::save_config(config)?;
+ Ok(())
+}
+
+#[api(
+ input: {
+ properties: {
+ name: {
+ schema: ENTITY_NAME_SCHEMA,
+ },
+ updater: {
+ type: WebhookConfigUpdater,
+ flatten: true,
+ },
+ delete: {
+ description: "List of properties to delete.",
+ type: Array,
+ optional: true,
+ items: {
+ type: DeleteableWebhookProperty,
+ }
+ },
+ digest: {
+ optional: true,
+ schema: PROXMOX_CONFIG_DIGEST_SCHEMA,
+ },
+ },
+ },
+ access: {
+ permission: &Permission::Privilege(&["system", "notifications"], PRIV_SYS_MODIFY, false),
+ },
+)]
+/// Update webhook endpoint.
+pub fn update_endpoint(
+ name: String,
+ updater: WebhookConfigUpdater,
+ delete: Option<Vec<DeleteableWebhookProperty>>,
+ digest: Option<String>,
+ _rpcenv: &mut dyn RpcEnvironment,
+) -> Result<(), Error> {
+ let _lock = pdm_config::notifications::lock_config()?;
+ let mut config = pdm_config::notifications::config()?;
+ let digest = digest.map(hex::decode).transpose()?;
+
+ proxmox_notify::api::webhook::update_endpoint(
+ &mut config,
+ &name,
+ updater,
+ delete.as_deref(),
+ digest.as_deref(),
+ )?;
+
+ pdm_config::notifications::save_config(config)?;
+ Ok(())
+}
+
+#[api(
+ input: {
+ properties: {
+ name: {
+ schema: ENTITY_NAME_SCHEMA,
+ }
+ },
+ },
+ access: {
+ permission: &Permission::Privilege(&["system", "notifications"], PRIV_SYS_MODIFY, false),
+ },
+)]
+/// Delete webhook endpoint.
+pub fn delete_endpoint(name: String, _rpcenv: &mut dyn RpcEnvironment) -> Result<(), Error> {
+ let _lock = pdm_config::notifications::lock_config()?;
+ let mut config = pdm_config::notifications::config()?;
+ proxmox_notify::api::webhook::delete_endpoint(&mut config, &name)?;
+
+ pdm_config::notifications::save_config(config)?;
+ Ok(())
+}
+
+const ITEM_ROUTER: Router = Router::new()
+ .get(&API_METHOD_GET_ENDPOINT)
+ .put(&API_METHOD_UPDATE_ENDPOINT)
+ .delete(&API_METHOD_DELETE_ENDPOINT);
+
+pub const ROUTER: Router = Router::new()
+ .get(&API_METHOD_LIST_ENDPOINTS)
+ .post(&API_METHOD_ADD_ENDPOINT)
+ .match_all("name", &ITEM_ROUTER);
diff --git a/server/src/context.rs b/server/src/context.rs
index c5da0af..acff3a8 100644
--- a/server/src/context.rs
+++ b/server/src/context.rs
@@ -38,5 +38,7 @@ pub fn init() -> Result<(), Error> {
default_remote_setup();
}
+ crate::notifications::init();
+
Ok(())
}
diff --git a/server/src/lib.rs b/server/src/lib.rs
index 5ed10d6..9723bc4 100644
--- a/server/src/lib.rs
+++ b/server/src/lib.rs
@@ -7,6 +7,7 @@ pub mod context;
pub mod env;
pub mod jobstate;
pub mod metric_collection;
+pub mod notifications;
pub mod parallel_fetcher;
pub mod remote_cache;
pub mod remote_tasks;
diff --git a/server/src/notifications/mod.rs b/server/src/notifications/mod.rs
new file mode 100644
index 0000000..e38584b
--- /dev/null
+++ b/server/src/notifications/mod.rs
@@ -0,0 +1,115 @@
+use std::path::Path;
+
+use proxmox_access_control::types::User;
+use proxmox_notify::context::Context;
+use proxmox_notify::renderer::TemplateSource;
+use proxmox_notify::Error;
+use tracing::error;
+
+use pdm_buildcfg::configdir;
+
+const PDM_NODE_CFG_FILENAME: &str = configdir!("/node.cfg");
+
+const DEFAULT_CONFIG: &str = "\
+sendmail: mail-to-root
+ comment Send mails to root@pam's email address
+ mailto-user root@pam
+
+
+matcher: default-matcher
+ mode all
+ target mail-to-root
+ comment Route all notifications to mail-to-root
+";
+
+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) => {
+ error!("{err}");
+ None
+ }
+ }
+}
+
+fn lookup_datacenter_config_key(content: &str, key: &str) -> Option<String> {
+ let key_prefix = format!("{key}:");
+ let value = content
+ .lines()
+ .find_map(|line| line.strip_prefix(&key_prefix))?
+ .trim();
+
+ if value.is_empty() {
+ None
+ } else {
+ Some(value.to_string())
+ }
+}
+
+#[derive(Debug)]
+struct PDMContext;
+
+static PDM_CONTEXT: PDMContext = PDMContext;
+
+impl Context for PDMContext {
+ fn lookup_email_for_user(&self, user: &str) -> Option<String> {
+ let (config, _digest) = match proxmox_access_control::user::config() {
+ Ok(c) => c,
+ Err(err) => {
+ error!("failed to read user config: {err}");
+ return None;
+ }
+ };
+
+ match config.lookup::<User>("user", user) {
+ Ok(user) => user.email,
+ Err(_) => None,
+ }
+ }
+
+ fn default_sendmail_author(&self) -> String {
+ format!("Proxmox Datacenter Manager - {}", proxmox_sys::nodename())
+ }
+
+ fn default_sendmail_from(&self) -> String {
+ let content = attempt_file_read(PDM_NODE_CFG_FILENAME);
+ content
+ .and_then(|content| lookup_datacenter_config_key(&content, "email-from"))
+ .unwrap_or_else(|| String::from("root"))
+ }
+
+ fn http_proxy_config(&self) -> Option<String> {
+ let content = attempt_file_read(PDM_NODE_CFG_FILENAME);
+ content.and_then(|content| lookup_datacenter_config_key(&content, "http-proxy"))
+ }
+
+ fn default_config(&self) -> &'static str {
+ DEFAULT_CONFIG
+ }
+
+ fn lookup_template(
+ &self,
+ filename: &str,
+ namespace: Option<&str>,
+ source: TemplateSource,
+ ) -> Result<Option<String>, Error> {
+ let base = match source {
+ TemplateSource::Vendor => "/usr/share/proxmox-datacenter-manager/templates",
+ TemplateSource::Override => configdir!("/notification-templates"),
+ };
+
+ let path = Path::new(base)
+ .join(namespace.unwrap_or("default"))
+ .join(filename);
+
+ error!("{path:?}");
+
+ proxmox_sys::fs::file_read_optional_string(path)
+ .map_err(|err| Error::Generic(format!("could not load template: {err}")))
+ }
+}
+
+/// Initialize `proxmox-notify` by registering the PDM context.
+pub fn init() {
+ proxmox_notify::context::set_context(&PDM_CONTEXT);
+}
diff --git a/templates/Makefile b/templates/Makefile
new file mode 100644
index 0000000..294fa7b
--- /dev/null
+++ b/templates/Makefile
@@ -0,0 +1,14 @@
+include ../defines.mk
+
+NOTIFICATION_TEMPLATES= \
+ default/test-body.txt.hbs \
+ default/test-subject.txt.hbs \
+
+all:
+
+clean:
+
+install:
+ install -dm755 $(DESTDIR)$(DATAROOTDIR)/proxmox-datacenter-manager/templates/default
+ $(foreach i,$(NOTIFICATION_TEMPLATES), \
+ install -m644 $(i) $(DESTDIR)$(DATAROOTDIR)/proxmox-datacenter-manager/templates/$(i) ;)
diff --git a/templates/default/test-body.txt.hbs b/templates/default/test-body.txt.hbs
new file mode 100644
index 0000000..2445443
--- /dev/null
+++ b/templates/default/test-body.txt.hbs
@@ -0,0 +1 @@
+This is a test of the notification target '{{target}}'.
diff --git a/templates/default/test-subject.txt.hbs b/templates/default/test-subject.txt.hbs
new file mode 100644
index 0000000..cb8e132
--- /dev/null
+++ b/templates/default/test-subject.txt.hbs
@@ -0,0 +1 @@
+Test notification
--
2.47.3
^ permalink raw reply [flat|nested] 2+ messages in thread
* superseded: [proxmox-datacenter-manager] Add notifications backend
2026-04-08 9:40 [proxmox-datacenter-manager] Add notifications backend Arthur Bied-Charreton
@ 2026-04-09 5:00 ` Arthur Bied-Charreton
0 siblings, 0 replies; 2+ messages in thread
From: Arthur Bied-Charreton @ 2026-04-09 5:00 UTC (permalink / raw)
To: pdm-devel
Misconfigured the subject prefix in my git config, sorry about the noise
Superseded by:
https://lore.proxmox.com/pdm-devel/20260409045819.19858-1-a.bied-charreton@proxmox.com/T/#u
^ permalink raw reply [flat|nested] 2+ messages in thread
end of thread, other threads:[~2026-04-09 5:00 UTC | newest]
Thread overview: 2+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-04-08 9:40 [proxmox-datacenter-manager] Add notifications backend Arthur Bied-Charreton
2026-04-09 5:00 ` superseded: " Arthur Bied-Charreton
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.