2 use const_format
::concatcp
;
4 use std
::collections
::HashMap
;
6 use std
::time
::{Duration, Instant}
;
8 use handlebars
::{Handlebars, TemplateError}
;
11 use proxmox_lang
::try_block
;
12 use proxmox_notify
::context
::pbs
::PBS_CONTEXT
;
13 use proxmox_schema
::ApiType
;
14 use proxmox_sys
::email
::sendmail
;
15 use proxmox_sys
::fs
::{create_path, CreateOptions}
;
18 APTUpdateInfo
, DataStoreConfig
, DatastoreNotify
, GarbageCollectionStatus
, NotificationMode
,
19 Notify
, SyncJobConfig
, TapeBackupJobSetup
, User
, Userid
, VerificationJobConfig
,
21 use proxmox_notify
::endpoints
::sendmail
::{SendmailConfig, SendmailEndpoint}
;
22 use proxmox_notify
::{Endpoint, Notification, Severity}
;
24 const SPOOL_DIR
: &str = concatcp
!(pbs_buildcfg
::PROXMOX_BACKUP_STATE_DIR
, "/notifications");
26 const TAPE_BACKUP_OK_TEMPLATE
: &str = r
###"
31 Datastore: {{job.store}}
32 Tape Pool: {{job.pool}}
33 Tape Drive: {{job.drive}}
35 {{#if snapshot-list ~}}
38 {{#each snapshot-list~}}
42 Duration: {{duration}}
49 Tape Backup successful.
52 Please visit the web interface for further details:
54 <https://{{fqdn}}:{{port}}/#DataStore-{{job.store}}>
58 const TAPE_BACKUP_ERR_TEMPLATE
: &str = r
###"
63 Datastore: {{job.store}}
64 Tape Pool: {{job.pool}}
65 Tape Drive: {{job.drive}}
67 {{#if snapshot-list ~}}
70 {{#each snapshot-list~}}
80 Tape Backup failed: {{error}}
83 Please visit the web interface for further details:
85 <https://{{fqdn}}:{{port}}/#pbsServerAdministration:tasks>
89 lazy_static
::lazy_static
! {
91 static ref HANDLEBARS
: Handlebars
<'
static> = {
92 let mut hb
= Handlebars
::new();
93 let result
: Result
<(), TemplateError
> = try_block
!({
95 hb
.set_strict_mode(true);
96 hb
.register_escape_fn(handlebars
::no_escape
);
98 hb
.register_template_string("tape_backup_ok_template", TAPE_BACKUP_OK_TEMPLATE
)?
;
99 hb
.register_template_string("tape_backup_err_template", TAPE_BACKUP_ERR_TEMPLATE
)?
;
104 if let Err(err
) = result
{
105 eprintln
!("error during template registration: {err}");
112 /// Initialize the notification system by setting context in proxmox_notify
113 pub fn init() -> Result
<(), Error
> {
114 proxmox_notify
::context
::set_context(&PBS_CONTEXT
);
118 /// Create the directory which will be used to temporarily store notifications
119 /// which were sent from an unprivileged process.
120 pub fn create_spool_dir() -> Result
<(), Error
> {
121 let backup_user
= pbs_config
::backup_user()?
;
122 let opts
= CreateOptions
::new()
123 .owner(backup_user
.uid
)
124 .group(backup_user
.gid
);
126 create_path(SPOOL_DIR
, None
, Some(opts
))?
;
130 async
fn send_queued_notifications() -> Result
<(), Error
> {
131 let mut read_dir
= tokio
::fs
::read_dir(SPOOL_DIR
).await?
;
133 let mut notifications
= Vec
::new();
135 while let Some(entry
) = read_dir
.next_entry().await?
{
136 let path
= entry
.path();
138 if let Some(ext
) = path
.extension() {
140 let p
= path
.clone();
142 let bytes
= tokio
::fs
::read(p
).await?
;
143 let notification
: Notification
= serde_json
::from_slice(&bytes
)?
;
144 notifications
.push(notification
);
146 // Currently, there is no retry-mechanism in case of failure...
147 // For retries, we'd have to keep track of which targets succeeded/failed
148 // to send, so we do not retry notifying a target which succeeded before.
149 tokio
::fs
::remove_file(path
).await?
;
154 // Make sure that we send the oldest notification first
155 notifications
.sort_unstable_by_key(|n
| n
.timestamp());
157 let res
= tokio
::task
::spawn_blocking(move || {
158 let config
= pbs_config
::notifications
::config()?
;
159 for notification
in notifications
{
160 if let Err(err
) = proxmox_notify
::api
::common
::send(&config
, ¬ification
) {
161 log
::error
!("failed to send notification: {err}");
169 if let Err(e
) = res
{
170 log
::error
!("could not read notification config: {e}");
176 /// Worker task to periodically send any queued notifications.
177 pub async
fn notification_worker() {
179 let delay_target
= Instant
::now() + Duration
::from_secs(5);
181 if let Err(err
) = send_queued_notifications().await
{
182 log
::error
!("notification worker task error: {err}");
185 tokio
::time
::sleep_until(tokio
::time
::Instant
::from_std(delay_target
)).await
;
189 fn send_notification(notification
: Notification
) -> Result
<(), Error
> {
190 if nix
::unistd
::ROOT
== Uid
::current() {
191 let config
= pbs_config
::notifications
::config()?
;
192 proxmox_notify
::api
::common
::send(&config
, ¬ification
)?
;
194 let ser
= serde_json
::to_vec(¬ification
)?
;
195 let path
= Path
::new(SPOOL_DIR
).join(format
!("{id}.json", id
= notification
.id()));
197 let backup_user
= pbs_config
::backup_user()?
;
198 let opts
= CreateOptions
::new()
199 .owner(backup_user
.uid
)
200 .group(backup_user
.gid
);
201 proxmox_sys
::fs
::replace_file(path
, &ser
, opts
, true)?
;
202 log
::info
!("queued notification (id={id})", id
= notification
.id())
208 fn send_sendmail_legacy_notification(notification
: Notification
, email
: &str) -> Result
<(), Error
> {
209 let endpoint
= SendmailEndpoint
{
210 config
: SendmailConfig
{
211 mailto
: vec
![email
.into()],
216 endpoint
.send(¬ification
)?
;
221 /// Summary of a successful Tape Job
223 pub struct TapeBackupJobSummary
{
224 /// The list of snaphots backed up
225 pub snapshot_list
: Vec
<String
>,
226 /// The total time of the backup job
227 pub duration
: std
::time
::Duration
,
228 /// The labels of the used tapes of the backup job
229 pub used_tapes
: Option
<Vec
<String
>>,
232 fn send_job_status_mail(email
: &str, subject
: &str, text
: &str) -> Result
<(), Error
> {
233 let (config
, _
) = crate::config
::node
::config()?
;
234 let from
= config
.email_from
;
236 // NOTE: some (web)mailers have big problems displaying text mails, so include html as well
237 let escaped_text
= handlebars
::html_escape(text
);
238 let html
= format
!("<html><body><pre>\n{escaped_text}\n<pre>");
240 let nodename
= proxmox_sys
::nodename();
242 let author
= format
!("Proxmox Backup Server - {nodename}");
256 pub fn send_gc_status(
258 status
: &GarbageCollectionStatus
,
259 result
: &Result
<(), Error
>,
260 ) -> Result
<(), Error
> {
261 let (fqdn
, port
) = get_server_url();
262 let mut data
= json
!({
263 "datastore": datastore
,
268 let (severity
, template
) = match result
{
270 let deduplication_factor
= if status
.disk_bytes
> 0 {
271 (status
.index_data_bytes
as f64) / (status
.disk_bytes
as f64)
276 data
["status"] = json
!(status
);
277 data
["deduplication-factor"] = format
!("{:.2}", deduplication_factor
).into();
279 (Severity
::Info
, "gc-ok")
282 data
["error"] = err
.to_string().into();
283 (Severity
::Error
, "gc-err")
286 let metadata
= HashMap
::from([
287 ("datastore".into(), datastore
.into()),
288 ("hostname".into(), proxmox_sys
::nodename().into()),
289 ("type".into(), "gc".into()),
292 let notification
= Notification
::from_template(severity
, template
, data
, metadata
);
294 let (email
, notify
, mode
) = lookup_datastore_notify_settings(datastore
);
296 NotificationMode
::LegacySendmail
=> {
297 let notify
= notify
.gc
.unwrap_or(Notify
::Always
);
299 if notify
== Notify
::Never
|| (result
.is_ok() && notify
== Notify
::Error
) {
303 if let Some(email
) = email
{
304 send_sendmail_legacy_notification(notification
, &email
)?
;
307 NotificationMode
::NotificationSystem
=> {
308 send_notification(notification
)?
;
315 pub fn send_verify_status(
316 job
: VerificationJobConfig
,
317 result
: &Result
<Vec
<String
>, Error
>,
318 ) -> Result
<(), Error
> {
319 let (fqdn
, port
) = get_server_url();
320 let mut data
= json
!({
326 let (template
, severity
) = match result
{
327 Ok(errors
) if errors
.is_empty() => ("verify-ok", Severity
::Info
),
329 data
["errors"] = json
!(errors
);
330 ("verify-err", Severity
::Error
)
333 // aborted job - do not send any notification
338 let metadata
= HashMap
::from([
339 ("job-id".into(), job
.id
.clone()),
340 ("datastore".into(), job
.store
.clone()),
341 ("hostname".into(), proxmox_sys
::nodename().into()),
342 ("type".into(), "verify".into()),
345 let notification
= Notification
::from_template(severity
, template
, data
, metadata
);
347 let (email
, notify
, mode
) = lookup_datastore_notify_settings(&job
.store
);
349 NotificationMode
::LegacySendmail
=> {
350 let notify
= notify
.verify
.unwrap_or(Notify
::Always
);
352 if notify
== Notify
::Never
|| (result
.is_ok() && notify
== Notify
::Error
) {
356 if let Some(email
) = email
{
357 send_sendmail_legacy_notification(notification
, &email
)?
;
360 NotificationMode
::NotificationSystem
=> {
361 send_notification(notification
)?
;
368 pub fn send_prune_status(
371 result
: &Result
<(), Error
>,
372 ) -> Result
<(), Error
> {
373 let (fqdn
, port
) = get_server_url();
374 let mut data
= json
!({
381 let (template
, severity
) = match result
{
382 Ok(()) => ("prune-ok", Severity
::Info
),
384 data
["error"] = err
.to_string().into();
385 ("prune-err", Severity
::Error
)
389 let metadata
= HashMap
::from([
390 ("job-id".into(), jobname
.to_string()),
391 ("datastore".into(), store
.into()),
392 ("hostname".into(), proxmox_sys
::nodename().into()),
393 ("type".into(), "prune".into()),
396 let notification
= Notification
::from_template(severity
, template
, data
, metadata
);
398 let (email
, notify
, mode
) = lookup_datastore_notify_settings(store
);
400 NotificationMode
::LegacySendmail
=> {
401 let notify
= notify
.prune
.unwrap_or(Notify
::Error
);
403 if notify
== Notify
::Never
|| (result
.is_ok() && notify
== Notify
::Error
) {
407 if let Some(email
) = email
{
408 send_sendmail_legacy_notification(notification
, &email
)?
;
411 NotificationMode
::NotificationSystem
=> {
412 send_notification(notification
)?
;
419 pub fn send_sync_status(job
: &SyncJobConfig
, result
: &Result
<(), Error
>) -> Result
<(), Error
> {
420 let (fqdn
, port
) = get_server_url();
421 let mut data
= json
!({
427 let (template
, severity
) = match result
{
428 Ok(()) => ("sync-ok", Severity
::Info
),
430 data
["error"] = err
.to_string().into();
431 ("sync-err", Severity
::Error
)
435 let metadata
= HashMap
::from([
436 ("job-id".into(), job
.id
.clone()),
437 ("datastore".into(), job
.store
.clone()),
438 ("hostname".into(), proxmox_sys
::nodename().into()),
439 ("type".into(), "sync".into()),
442 let notification
= Notification
::from_template(severity
, template
, data
, metadata
);
444 let (email
, notify
, mode
) = lookup_datastore_notify_settings(&job
.store
);
446 NotificationMode
::LegacySendmail
=> {
447 let notify
= notify
.prune
.unwrap_or(Notify
::Error
);
449 if notify
== Notify
::Never
|| (result
.is_ok() && notify
== Notify
::Error
) {
453 if let Some(email
) = email
{
454 send_sendmail_legacy_notification(notification
, &email
)?
;
457 NotificationMode
::NotificationSystem
=> {
458 send_notification(notification
)?
;
465 pub fn send_tape_backup_status(
468 job
: &TapeBackupJobSetup
,
469 result
: &Result
<(), Error
>,
470 summary
: TapeBackupJobSummary
,
471 ) -> Result
<(), Error
> {
472 let (fqdn
, port
) = get_server_url();
473 let duration
: proxmox_time
::TimeSpan
= summary
.duration
.into();
474 let mut data
= json
!({
479 "snapshot-list": summary
.snapshot_list
,
480 "used-tapes": summary
.used_tapes
,
481 "duration": duration
.to_string(),
484 let text
= match result
{
485 Ok(()) => HANDLEBARS
.render("tape_backup_ok_template", &data
)?
,
487 data
["error"] = err
.to_string().into();
488 HANDLEBARS
.render("tape_backup_err_template", &data
)?
492 let subject
= match (result
, id
) {
493 (Ok(()), Some(id
)) => format
!("Tape Backup '{id}' datastore '{}' successful", job
.store
,),
494 (Ok(()), None
) => format
!("Tape Backup datastore '{}' successful", job
.store
,),
495 (Err(_
), Some(id
)) => format
!("Tape Backup '{id}' datastore '{}' failed", job
.store
,),
496 (Err(_
), None
) => format
!("Tape Backup datastore '{}' failed", job
.store
,),
499 send_job_status_mail(email
, &subject
, &text
)?
;
504 /// Send email to a person to request a manual media change
505 pub fn send_load_media_email(
510 reason
: Option
<String
>,
511 ) -> Result
<(), Error
> {
512 use std
::fmt
::Write
as _
;
514 let device_type
= if changer { "changer" }
else { "drive" }
;
516 let subject
= format
!("Load Media '{label_text}' request for {device_type} '{device}'");
518 let mut text
= String
::new();
520 if let Some(reason
) = reason
{
523 "The {device_type} has the wrong or no tape(s) inserted. Error:\n{reason}\n\n"
528 text
.push_str("Please insert the requested media into the changer.\n\n");
529 let _
= writeln
!(text
, "Changer: {device}");
531 text
.push_str("Please insert the requested media into the backup drive.\n\n");
532 let _
= writeln
!(text
, "Drive: {device}");
534 let _
= writeln
!(text
, "Media: {label_text}");
536 send_job_status_mail(to
, &subject
, &text
)
539 fn get_server_url() -> (String
, usize) {
540 // user will surely request that they can change this
542 let nodename
= proxmox_sys
::nodename();
543 let mut fqdn
= nodename
.to_owned();
545 if let Ok(resolv_conf
) = crate::api2
::node
::dns
::read_etc_resolv_conf() {
546 if let Some(search
) = resolv_conf
["search"].as_str() {
548 fqdn
.push_str(search
);
557 pub fn send_updates_available(updates
: &[&APTUpdateInfo
]) -> Result
<(), Error
> {
558 let (fqdn
, port
) = get_server_url();
559 let hostname
= proxmox_sys
::nodename().to_string();
563 "hostname": &hostname
,
568 let metadata
= HashMap
::from([
569 ("hostname".into(), hostname
),
570 ("type".into(), "package-updates".into()),
574 Notification
::from_template(Severity
::Info
, "package-updates", data
, metadata
);
576 send_notification(notification
)?
;
580 /// send email on certificate renewal failure.
581 pub fn send_certificate_renewal_mail(result
: &Result
<(), Error
>) -> Result
<(), Error
> {
582 let error
: String
= match result
{
583 Err(e
) => e
.to_string(),
587 let (fqdn
, port
) = get_server_url();
595 let metadata
= HashMap
::from([
596 ("hostname".into(), proxmox_sys
::nodename().into()),
597 ("type".into(), "acme".into()),
600 let notification
= Notification
::from_template(Severity
::Info
, "acme-err", data
, metadata
);
602 send_notification(notification
)?
;
606 /// Lookup users email address
607 pub fn lookup_user_email(userid
: &Userid
) -> Option
<String
> {
608 if let Ok(user_config
) = pbs_config
::user
::cached_config() {
609 if let Ok(user
) = user_config
.lookup
::<User
>("user", userid
.as_str()) {
617 /// Lookup Datastore notify settings
618 pub fn lookup_datastore_notify_settings(
620 ) -> (Option
<String
>, DatastoreNotify
, NotificationMode
) {
621 let mut email
= None
;
623 let notify
= DatastoreNotify
{
630 let (config
, _digest
) = match pbs_config
::datastore
::config() {
631 Ok(result
) => result
,
632 Err(_
) => return (email
, notify
, NotificationMode
::default()),
635 let config
: DataStoreConfig
= match config
.lookup("datastore", store
) {
636 Ok(result
) => result
,
637 Err(_
) => return (email
, notify
, NotificationMode
::default()),
640 email
= match config
.notify_user
{
641 Some(ref userid
) => lookup_user_email(userid
),
642 None
=> lookup_user_email(Userid
::root_userid()),
645 let notification_mode
= config
.notification_mode
.unwrap_or_default();
646 let notify_str
= config
.notify
.unwrap_or_default();
648 if let Ok(value
) = DatastoreNotify
::API_SCHEMA
.parse_property_string(¬ify_str
) {
649 if let Ok(notify
) = serde_json
::from_value(value
) {
650 return (email
, notify
, notification_mode
);
654 (email
, notify
, notification_mode
)
658 fn test_template_register() {
659 assert
!(HANDLEBARS
.has_template("tape_backup_ok_template"));
660 assert
!(HANDLEBARS
.has_template("tape_backup_err_template"));