From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) by lore.proxmox.com (Postfix) with ESMTPS id 89F611FF13F for ; Thu, 09 Apr 2026 11:19:54 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id E23F61C483; Thu, 9 Apr 2026 11:20:37 +0200 (CEST) Mime-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset=UTF-8 Date: Thu, 09 Apr 2026 11:20:22 +0200 Message-Id: Subject: Re: [PATCH datacenter-manager v2] Add notifications backend To: "Arthur Bied-Charreton" , X-Mailer: aerc 0.20.0 References: <20260409045819.19858-1-a.bied-charreton@proxmox.com> In-Reply-To: <20260409045819.19858-1-a.bied-charreton@proxmox.com> From: "Shannon Sterz" X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1775726354472 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.123 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment RCVD_IN_VALIDITY_CERTIFIED_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to Validity was blocked. See https://knowledge.validity.com/hc/en-us/articles/20961730681243 for more information. RCVD_IN_VALIDITY_RPBL_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to Validity was blocked. See https://knowledge.validity.com/hc/en-us/articles/20961730681243 for more information. RCVD_IN_VALIDITY_SAFE_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to Validity was blocked. See https://knowledge.validity.com/hc/en-us/articles/20961730681243 for more information. SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record URIBL_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to URIBL was blocked. See http://wiki.apache.org/spamassassin/DnsBlocklists#dnsbl-block for more information. [webhook.rs,matchers.rs,defines.mk,notifications.rs,smtp.rs,sendmail.rs,proxmox.com,gotify.rs,mod.rs,targets.rs,context.rs,pdm.rs,lib.rs] Message-ID-Hash: OQAUFEFRBERWBQHGWFT5CHYQNV63XCJI X-Message-ID-Hash: OQAUFEFRBERWBQHGWFT5CHYQNV63XCJI X-MailFrom: s.sterz@proxmox.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox Datacenter Manager development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: On Thu Apr 9, 2026 at 6:57 AM CEST, Arthur Bied-Charreton wrote: nit: imo a commit message like "server: add notification backend" would be more appropriate. usually it is recommended to try add a tag prefix of what your code touches [1]. [1]: https://pve.proxmox.com/wiki/Developer_Documentation#Commits_and_Commi= t_Messages > 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. nit: usually mentions of offline discussions would be more appropriate in the notes of a patch or a cover letter, than the commit message. generally, it may make sense to split this up a little and add a cover letter imo. > 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. any reason to not go ahead and add at least one notification in this series too? being able to test that notifications function, but not having anything to be notified about seems a little odd to me. one easy notification you could add is the `send_certificate_renewal_mail` in `api::nodes::certificates::spawn_certificate_worker()`. that should essentially be identical to pbs anyway. > The endpoints are made unprotected and the configuration files > read/writable by `www-data`, like for `remotes.cfg`. > > Signed-off-by: Arthur Bied-Charreton > --- > 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 =3D { version =3D "1.1", features =3D ["sy= nc"] } > proxmox-lang =3D "1.1" > proxmox-log =3D "1" > proxmox-login =3D "1.0.2" > +proxmox-notify =3D "1" > proxmox-rest-server =3D "1" > # some use "cli", some use "cli" and "server", pbs-config uses nothing > proxmox-router =3D { version =3D "3.0.0", default-features =3D 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-d= atacenter-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 =3D $(PREFIX)/bin > SBINDIR =3D $(PREFIX)/sbin > LIBDIR =3D $(PREFIX)/lib > LIBEXECDIR =3D $(PREFIX)/libexec > +DATAROOTDIR =3D $(PREFIX)/share > BASHCOMPDIR =3D $(PREFIX)/share/bash-completion/completions > ZSHCOMPDIR =3D $(PREFIX)/share/zsh/vendor-completions > MAN1DIR =3D $(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 =3D true > proxmox-simple-config.workspace =3D true > proxmox-sys =3D { workspace =3D true, features =3D [ "acl", "crypt", "ti= mer" ] } > proxmox-acme-api.workspace =3D true > +proxmox-notify.workspace =3D true > pdm-api-types.workspace =3D true > pdm-buildcfg.workspace =3D 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_NAM= E}; > 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/not= ifications.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, ApiLockG= uard}; > +use proxmox_sys::fs::file_read_optional_string; > + > +/// Configuration file location for notification targets/matchers. > +pub const NOTIFICATION_CONFIG_PATH: &str =3D configdir!("/notifications.= cfg"); > + > +/// Private configuration file location for secrets - only readable by `= root`. > +pub const NOTIFICATION_PRIV_CONFIG_PATH: &str =3D configdir!("/notificat= ions-priv.cfg"); > + > +/// Lockfile to prevent concurrent write access. > +pub const NOTIFICATION_LOCK_FILE: &str =3D configdir!("/.notifications.l= ck"); > + you might want to move these to their own "notifications" sub-folder or similar (maybe just "notify"?). in pdm we tend to put config files in topically fitting sub-folders (e.g. access, acme, auth) > +/// Get exclusive lock for `notifications.cfg`. > +pub fn lock_config() -> Result { > + open_api_lockfile(NOTIFICATION_LOCK_FILE, None, true) > +} > + > +/// Load notification config. > +pub fn config() -> Result { > + let content =3D file_read_optional_string(NOTIFICATION_CONFIG_PATH)?= .unwrap_or_default(); > + > + let priv_content =3D > + file_read_optional_string(NOTIFICATION_PRIV_CONFIG_PATH)?.unwrap= _or_default(); > + > + Ok(Config::new(&content, &priv_content)?) you might want to consider returning a `ConfigDigest` here too. returning that encourages validating the digest as its less easily forgotten. > +} > + > +/// Save notification config. > +pub fn save_config(config: Config) -> Result<(), Error> { > + let (cfg, priv_cfg) =3D config.write()?; > + replace_config(NOTIFICATION_CONFIG_PATH, cfg.as_bytes())?; > + replace_config(NOTIFICATION_PRIV_CONFIG_PATH, priv_cfg.as_bytes())?; the privileged config should probably be saved with higher permission than the general config. consider using `replace_secret_config` here. > + 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 =3D true > proxmox-ldap.workspace =3D true > proxmox-log.workspace =3D true > proxmox-login.workspace =3D true > +proxmox-notify.workspace =3D true > proxmox-openid.workspace =3D true > proxmox-rest-server =3D { workspace =3D true, features =3D [ "templates"= ] } > proxmox-router =3D { workspace =3D true, features =3D [ "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 =3D &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/a= pi/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, GotifyP= rivateConfig, > + 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_DIGE= ST_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, Error> { > + let config =3D pdm_config::notifications::config()?; > + > + let endpoints =3D 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) -> Re= sult { > + let config =3D pdm_config::notifications::config()?; > + let endpoint =3D proxmox_notify::api::gotify::get_endpoint(&config, = &name)?; > + > + rpcenv["digest"] =3D hex::encode(config.digest()).into(); if you return the digest as described above this could become: let (config, digest) =3D pdm_config::notifications::config()?; let endpoint =3D proxmox_notify::api::gotify::get_endpoint(&config, &na= me)?; rpcenv["digest"] =3D digest.to_hex().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 =3D pdm_config::notifications::lock_config()?; > + let mut config =3D pdm_config::notifications::config()?; this should probably verify the digest. if you return a config digest as described above this could become: let mut (config, expected_digest) =3D domains::config()?; expected_digest.detect_modification(digest.as_ref())?; assuming that you take the digest as an input parameter to this api call with: digest: { optional: true, type: ConfigDigest, } and: digest: Option > + let private_endpoint_config =3D GotifyPrivateConfig { > + name: endpoint.name.clone(), > + token, > + }; > + > + proxmox_notify::api::gotify::add_endpoint(&mut config, endpoint, pri= vate_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, > + delete: Option>, > + digest: Option, > + _rpcenv: &mut dyn RpcEnvironment, > +) -> Result<(), Error> { > + let _lock =3D pdm_config::notifications::lock_config()?; > + let mut config =3D pdm_config::notifications::config()?; > + let digest =3D digest.map(hex::decode).transpose()?; using the `ConfigDigest` trait instead of `String` above you could drop this line if im not mistaken. > + > + 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 =3D pdm_config::notifications::lock_config()?; > + let mut config =3D pdm_config::notifications::config()?; > + proxmox_notify::api::gotify::delete_gotify_endpoint(&mut config, &na= me)?; > + this would probably also benefit from checking the config digest. > + pdm_config::notifications::save_config(config)?; > + Ok(()) > +} > + > +const ITEM_ROUTER: Router =3D Router::new() > + .get(&API_METHOD_GET_ENDPOINT) > + .put(&API_METHOD_UPDATE_ENDPOINT) > + .delete(&API_METHOD_DELETE_ENDPOINT); > + > +pub const ROUTER: Router =3D 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_DIGE= ST_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, Error> { > + let config =3D pdm_config::notifications::config()?; > + > + let matchers =3D 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) -> Res= ult { > + let config =3D pdm_config::notifications::config()?; > + let matcher =3D proxmox_notify::api::matcher::get_matcher(&config, &= name)?; > + > + rpcenv["digest"] =3D hex::encode(config.digest()).into(); see above regarding `ConfigDigest` > + > + 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 RpcEnvironm= ent) -> Result<(), Error> { > + let _lock =3D pdm_config::notifications::lock_config()?; > + let mut config =3D pdm_config::notifications::config()?; see above regarding `ConfigDigest` > + > + 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>, > + digest: Option, > + _rpcenv: &mut dyn RpcEnvironment, > +) -> Result<(), Error> { > + let _lock =3D pdm_config::notifications::lock_config()?; > + let mut config =3D pdm_config::notifications::config()?; > + let digest =3D digest.map(hex::decode).transpose()?; see above regarding `ConfigDigest`. > + > + 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 =3D pdm_config::notifications::lock_config()?; > + let mut config =3D pdm_config::notifications::config()?; > + proxmox_notify::api::matcher::delete_matcher(&mut config, &name)?; > + see above regarding `ConfigDigest`. > + pdm_config::notifications::save_config(config)?; > + Ok(()) > +} > + > +const ITEM_ROUTER: Router =3D Router::new() > + .get(&API_METHOD_GET_MATCHER) > + .put(&API_METHOD_UPDATE_MATCHER) > + .delete(&API_METHOD_DELETE_MATCHER); > + > +pub const ROUTER: Router =3D 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, RpcEnvironme= nt, 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 =3D &sorted!([ > + ("endpoints", &ENDPOINT_ROUTER), > + ("matcher-fields", &FIELD_ROUTER), > + ("matcher-field-values", &VALUE_ROUTER), > + ("targets", &targets::ROUTER), > + ("matchers", &matchers::ROUTER), > +]); > + > +pub const ROUTER: Router =3D Router::new() > + .get(&list_subdirs_api_method!(SUBDIRS)) > + .subdirs(SUBDIRS); > + > +#[sortable] > +const ENDPOINT_SUBDIRS: SubdirMap =3D &sorted!([ > + ("gotify", &gotify::ROUTER), > + ("sendmail", &sendmail::ROUTER), > + ("smtp", &smtp::ROUTER), > + ("webhook", &webhook::ROUTER), > +]); > + > +const ENDPOINT_ROUTER: Router =3D Router::new() > + .get(&list_subdirs_api_method!(ENDPOINT_SUBDIRS)) > + .subdirs(ENDPOINT_SUBDIRS); > + > +const FIELD_ROUTER: Router =3D Router::new().get(&API_METHOD_GET_FIELDS)= ; > +const VALUE_ROUTER: Router =3D 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, > +} > + > +#[api( > + input: { > + properties: {} > + }, > + returns: { > + description: "List of known metadata fields.", > + type: Array, > + items: {type: MatchableField}, > + }, > + access: {permission: &Permission::Privilege(&["system", "notificatio= ns"], PRIV_SYS_AUDIT, false)}, > +)] > +/// Get all known metadata fields. > +pub fn get_fields() -> Result, Error> { > + Ok(vec![]) if you decide to add the acme notification here in a next version, you can add hostname here too imo. > +} > + > +#[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, 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_DIGE= ST_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, Error> { > + let config =3D pdm_config::notifications::config()?; > + > + let endpoints =3D proxmox_notify::api::sendmail::get_endpoints(&conf= ig)?; > + > + 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 { > + let config =3D pdm_config::notifications::config()?; > + let endpoint =3D proxmox_notify::api::sendmail::get_endpoint(&config= , &name)?; > + > + rpcenv["digest"] =3D hex::encode(config.digest()).into(); See above regarding `ConfigDigest`. > + > + 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 =3D pdm_config::notifications::lock_config()?; > + let mut config =3D pdm_config::notifications::config()?; > + See above regarding `ConfigDigest`. > + 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>, > + digest: Option, > + _rpcenv: &mut dyn RpcEnvironment, > +) -> Result<(), Error> { > + let _lock =3D pdm_config::notifications::lock_config()?; > + let mut config =3D pdm_config::notifications::config()?; > + let digest =3D digest.map(hex::decode).transpose()?; See above regarding `ConfigDigest`. > + > + 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 =3D pdm_config::notifications::lock_config()?; > + let mut config =3D pdm_config::notifications::config()?; > + proxmox_notify::api::sendmail::delete_endpoint(&mut config, &name)?; > + See above regarding `ConfigDigest`. > + pdm_config::notifications::save_config(config)?; > + Ok(()) > +} > + > +const ITEM_ROUTER: Router =3D Router::new() > + .get(&API_METHOD_GET_ENDPOINT) > + .put(&API_METHOD_UPDATE_ENDPOINT) > + .delete(&API_METHOD_DELETE_ENDPOINT); > + > +pub const ROUTER: Router =3D 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, SmtpPrivateCo= nfig, > + 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_DIGE= ST_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, Error> { > + let config =3D pdm_config::notifications::config()?; > + > + let endpoints =3D 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) -> Re= sult { > + let config =3D pdm_config::notifications::config()?; > + let endpoint =3D proxmox_notify::api::smtp::get_endpoint(&config, &n= ame)?; > + > + rpcenv["digest"] =3D hex::encode(config.digest()).into(); > + See above regarding `ConfigDigest`. > + 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, > + _rpcenv: &mut dyn RpcEnvironment, > +) -> Result<(), Error> { > + let _lock =3D pdm_config::notifications::lock_config()?; > + let mut config =3D pdm_config::notifications::config()?; > + let private_endpoint_config =3D SmtpPrivateConfig { > + name: endpoint.name.clone(), > + password, > + }; > + > + proxmox_notify::api::smtp::add_endpoint(&mut config, endpoint, priva= te_endpoint_config)?; > + See above regarding `ConfigDigest`. > + 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, > + delete: Option>, > + digest: Option, > + _rpcenv: &mut dyn RpcEnvironment, > +) -> Result<(), Error> { > + let _lock =3D pdm_config::notifications::lock_config()?; > + let mut config =3D pdm_config::notifications::config()?; > + let digest =3D digest.map(hex::decode).transpose()?; See above regarding `ConfigDigest`. > + > + 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 =3D pdm_config::notifications::lock_config()?; > + let mut config =3D pdm_config::notifications::config()?; > + proxmox_notify::api::smtp::delete_endpoint(&mut config, &name)?; See above regarding `ConfigDigest`. > + > + pdm_config::notifications::save_config(config)?; > + Ok(()) > +} > + > +const ITEM_ROUTER: Router =3D Router::new() > + .get(&API_METHOD_GET_ENDPOINT) > + .put(&API_METHOD_UPDATE_ENDPOINT) > + .delete(&API_METHOD_DELETE_ENDPOINT); > + > +pub const ROUTER: Router =3D 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, RpcEnv= ironment, 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 notifica= tion 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, Error> { > + let config =3D pdm_config::notifications::config()?; > + let targets =3D 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) -> Re= sult<(), Error> { > + let config =3D pdm_config::notifications::config()?; > + proxmox_notify::api::common::test_target(&config, &name)?; > + Ok(()) > +} > + > +#[sortable] > +const SUBDIRS: SubdirMap =3D &sorted!([("test", &TEST_ROUTER),]); > +const TEST_ROUTER: Router =3D Router::new().post(&API_METHOD_TEST_TARGET= ); > +const ITEM_ROUTER: Router =3D Router::new() > + .get(&list_subdirs_api_method!(SUBDIRS)) > + .subdirs(SUBDIRS); > + > +pub const ROUTER: Router =3D 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_DIGE= ST_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, Error> { > + let config =3D pdm_config::notifications::config()?; > + > + let endpoints =3D proxmox_notify::api::webhook::get_endpoints(&confi= g)?; > + > + 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) -> Re= sult { > + let config =3D pdm_config::notifications::config()?; > + let endpoint =3D proxmox_notify::api::webhook::get_endpoint(&config,= &name)?; > + > + rpcenv["digest"] =3D hex::encode(config.digest()).into(); See above regarding `ConfigDigest`. > + > + 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 =3D pdm_config::notifications::lock_config()?; > + let mut config =3D pdm_config::notifications::config()?; See above regarding `ConfigDigest`. > + > + 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>, > + digest: Option, > + _rpcenv: &mut dyn RpcEnvironment, > +) -> Result<(), Error> { > + let _lock =3D pdm_config::notifications::lock_config()?; > + let mut config =3D pdm_config::notifications::config()?; > + let digest =3D digest.map(hex::decode).transpose()?; See above regarding `ConfigDigest`. > + > + 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 =3D pdm_config::notifications::lock_config()?; > + let mut config =3D pdm_config::notifications::config()?; > + proxmox_notify::api::webhook::delete_endpoint(&mut config, &name)?; See above regarding `ConfigDigest`. > + > + pdm_config::notifications::save_config(config)?; > + Ok(()) > +} > + > +const ITEM_ROUTER: Router =3D Router::new() > + .get(&API_METHOD_GET_ENDPOINT) > + .put(&API_METHOD_UPDATE_ENDPOINT) > + .delete(&API_METHOD_DELETE_ENDPOINT); > + > +pub const ROUTER: Router =3D 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/m= od.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 =3D configdir!("/node.cfg"); > + > +const DEFAULT_CONFIG: &str =3D "\ > +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>(path: P) -> Option { > + match proxmox_sys::fs::file_read_optional_string(path) { > + Ok(contents) =3D> contents, > + Err(err) =3D> { > + error!("{err}"); > + None > + } > + } > +} > + > +fn lookup_datacenter_config_key(content: &str, key: &str) -> Option { > + let key_prefix =3D format!("{key}:"); > + let value =3D 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 =3D PDMContext; > + > +impl Context for PDMContext { > + fn lookup_email_for_user(&self, user: &str) -> Option { > + let (config, _digest) =3D match proxmox_access_control::user::co= nfig() { > + Ok(c) =3D> c, > + Err(err) =3D> { > + error!("failed to read user config: {err}"); > + return None; > + } > + }; > + > + match config.lookup::("user", user) { > + Ok(user) =3D> user.email, > + Err(_) =3D> None, > + } > + } > + > + fn default_sendmail_author(&self) -> String { > + format!("Proxmox Datacenter Manager - {}", proxmox_sys::nodename= ()) > + } > + > + fn default_sendmail_from(&self) -> String { > + let content =3D 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 { > + let content =3D 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, Error> { > + let base =3D match source { > + TemplateSource::Vendor =3D> "/usr/share/proxmox-datacenter-m= anager/templates", > + TemplateSource::Override =3D> configdir!("/notification-temp= lates"), if notification related config files are moved to their own sub-folder this should probably become `notifications/templates` or similar > + }; > + > + let path =3D Path::new(base) > + .join(namespace.unwrap_or("default")) > + .join(filename); > + > + error!("{path:?}"); is this intentional? i don't think unconditionally emitting an error event here makes sense. > + > + proxmox_sys::fs::file_read_optional_string(path) > + .map_err(|err| Error::Generic(format!("could not load templa= te: {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=3D \ > + default/test-body.txt.hbs \ > + default/test-subject.txt.hbs \ > + > +all: > + > +clean: > + > +install: > + install -dm755 $(DESTDIR)$(DATAROOTDIR)/proxmox-datacenter-manager/temp= lates/default > + $(foreach i,$(NOTIFICATION_TEMPLATES), \ > + install -m644 $(i) $(DESTDIR)$(DATAROOTDIR)/proxmox-datacenter-mana= ger/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/t= est-subject.txt.hbs > new file mode 100644 > index 0000000..cb8e132 > --- /dev/null > +++ b/templates/default/test-subject.txt.hbs > @@ -0,0 +1 @@ > +Test notification