diff --git a/src/api2/node/apt.rs b/src/api2/node/apt.rs index f720dee6..9beaf024 100644 --- a/src/api2/node/apt.rs +++ b/src/api2/node/apt.rs @@ -85,6 +85,13 @@ fn do_apt_update(worker: &WorkerTask, quiet: bool) -> Result<(), Error> { node: { schema: NODE_SCHEMA, }, + notify: { + type: bool, + description: r#"Send notification mail about new package updates availanle to the + email address configured for 'root@pam')."#, + optional: true, + default: false, + }, quiet: { description: "Only produces output suitable for logging, omitting progress indicators.", type: bool, @@ -102,16 +109,46 @@ fn do_apt_update(worker: &WorkerTask, quiet: bool) -> Result<(), Error> { )] /// Update the APT database pub fn apt_update_database( + notify: Option, quiet: Option, rpcenv: &mut dyn RpcEnvironment, ) -> Result { let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?; let to_stdout = if rpcenv.env_type() == RpcEnvironmentType::CLI { true } else { false }; + // FIXME: change to non-option in signature and drop below once we have proxmox-api-macro 0.2.3 let quiet = quiet.unwrap_or(API_METHOD_APT_UPDATE_DATABASE_PARAM_DEFAULT_QUIET); + let notify = notify.unwrap_or(API_METHOD_APT_UPDATE_DATABASE_PARAM_DEFAULT_NOTIFY); let upid_str = WorkerTask::new_thread("aptupdate", None, auth_id, to_stdout, move |worker| { do_apt_update(&worker, quiet)?; + + let mut cache = apt::update_cache()?; + + if notify { + let mut notified = match cache.notified { + Some(notified) => notified, + None => std::collections::HashMap::new(), + }; + let mut to_notify: Vec<&APTUpdateInfo> = Vec::new(); + + for pkg in &cache.package_status { + match notified.insert(pkg.package.to_owned(), pkg.version.to_owned()) { + Some(notified_version) => { + if notified_version != pkg.version { + to_notify.push(pkg); + } + }, + None => to_notify.push(pkg), + } + } + if !to_notify.is_empty() { + crate::server::send_updates_available(&to_notify)?; + } + cache.notified = Some(notified); + apt::write_pkg_cache(&cache)?; + } + Ok(()) })?; diff --git a/src/server/email_notifications.rs b/src/server/email_notifications.rs index dd632625..4a26535b 100644 --- a/src/server/email_notifications.rs +++ b/src/server/email_notifications.rs @@ -9,8 +9,9 @@ use crate::{ config::verify::VerificationJobConfig, config::sync::SyncJobConfig, api2::types::{ - Userid, + APTUpdateInfo, GarbageCollectionStatus, + Userid, }, tools::format::HumanByte, }; @@ -91,6 +92,16 @@ Synchronization failed: {{error}} "###; +const PACKAGE_UPDATES_TEMPLATE: &str = r###" +Proxmox Backup Server has the following updates available: +{{#each updates }} + {{Package}}: {{OldVersion}} -> {{Version~}} +{{/each }} + +To upgrade visit the webinderface: +"###; + + lazy_static::lazy_static!{ static ref HANDLEBARS: Handlebars<'static> = { @@ -110,6 +121,8 @@ lazy_static::lazy_static!{ hb.register_template_string("sync_ok_template", SYNC_OK_TEMPLATE).unwrap(); hb.register_template_string("sync_err_template", SYNC_ERR_TEMPLATE).unwrap(); + hb.register_template_string("package_update_template", PACKAGE_UPDATES_TEMPLATE).unwrap(); + hb }; } @@ -261,6 +274,25 @@ pub fn send_sync_status( Ok(()) } +pub fn send_updates_available( + updates: &Vec<&APTUpdateInfo>, +) -> Result<(), Error> { + // update mails always go to the root@pam configured email.. + if let Some(email) = lookup_user_email(Userid::root_userid()) { + let nodename = proxmox::tools::nodename(); + let subject = format!("New software packages available ({})", nodename); + + let text = HANDLEBARS.render("package_update_template", &json!({ + "fqdn": nix::sys::utsname::uname().nodename(), // FIXME: add get_fqdn helper like PVE? + "port": 8007, // user will surely request that they can change this + "updates": updates, + }))?; + + send_job_status_mail(&email, &subject, &text)?; + } + Ok(()) +} + /// Lookup users email address /// /// For "backup@pam", this returns the address from "root@pam". diff --git a/src/tools/apt.rs b/src/tools/apt.rs index 4c314f90..5800e0a2 100644 --- a/src/tools/apt.rs +++ b/src/tools/apt.rs @@ -1,4 +1,5 @@ use std::collections::HashSet; +use std::collections::HashMap; use anyhow::{Error, bail, format_err}; use apt_pkg_native::Cache; @@ -11,8 +12,11 @@ use crate::api2::types::APTUpdateInfo; const APT_PKG_STATE_FN: &str = "/var/lib/proxmox-backup/pkg-state.json"; #[derive(Debug, serde::Serialize, serde::Deserialize)] -/// Some information we cache about the package (update) state +/// Some information we cache about the package (update) state, like what pending update version +/// we already notfied an user about pub struct PkgState { + /// simple map from package name to most recently notified (emailed) version + pub notified: Option>, /// A list of pending updates pub package_status: Vec, } @@ -64,6 +68,7 @@ pub fn update_cache() -> Result { cache }, _ => PkgState { + notified: None, package_status: all_upgradeable, }, };