diff --git a/src/api2/config/tape_backup_job.rs b/src/api2/config/tape_backup_job.rs index 7441ebde..718f19a1 100644 --- a/src/api2/config/tape_backup_job.rs +++ b/src/api2/config/tape_backup_job.rs @@ -74,6 +74,8 @@ pub fn create_tape_backup_job( config::tape_job::save_config(&config)?; + crate::server::jobstate::create_state_file("tape-backup-job", &job.id)?; + Ok(()) } @@ -112,6 +114,10 @@ pub enum DeletableProperty { comment, /// Delete the job schedule. schedule, + /// Delete the eject-media property + eject_media, + /// Delete the export-media-set property + export_media_set, } #[api( @@ -205,6 +211,8 @@ pub fn delete_tape_backup_job( config::tape_job::save_config(&config)?; + crate::server::jobstate::remove_state_file("tape-backup-job", &id)?; + Ok(()) } diff --git a/src/api2/tape/backup.rs b/src/api2/tape/backup.rs index 349ef304..c8b7a323 100644 --- a/src/api2/tape/backup.rs +++ b/src/api2/tape/backup.rs @@ -15,7 +15,11 @@ use proxmox::{ use crate::{ task_log, - config, + config::{ + self, + tape_job::TapeBackupJobConfig, + }, + server::jobstate::Job, backup::{ DataStore, BackupDir, @@ -45,6 +49,75 @@ use crate::{ }, }; +pub fn do_tape_backup_job( + mut job: Job, + tape_job: TapeBackupJobConfig, + auth_id: &Authid, + schedule: Option, +) -> Result { + + let job_id = format!("{}:{}:{}:{}", + tape_job.store, + tape_job.pool, + tape_job.drive, + job.jobname()); + + let worker_type = job.jobtype().to_string(); + + let datastore = DataStore::lookup_datastore(&tape_job.store)?; + + let (config, _digest) = config::media_pool::config()?; + let pool_config: MediaPoolConfig = config.lookup("pool", &tape_job.pool)?; + + let (drive_config, _digest) = config::drive::config()?; + + // early check/lock before starting worker + let drive_lock = lock_tape_device(&drive_config, &tape_job.drive)?; + + let upid_str = WorkerTask::new_thread( + &worker_type, + Some(job_id.clone()), + auth_id.clone(), + false, + move |worker| { + let _drive_lock = drive_lock; // keep lock guard + + job.start(&worker.upid().to_string())?; + + let eject_media = false; + let export_media_set = false; + + task_log!(worker,"Starting tape backup job '{}'", job_id); + if let Some(event_str) = schedule { + task_log!(worker,"task triggered by schedule '{}'", event_str); + } + + let job_result = backup_worker( + &worker, + datastore, + &tape_job.drive, + &pool_config, + eject_media, + export_media_set, + ); + + let status = worker.create_state(&job_result); + + if let Err(err) = job.finish(status) { + eprintln!( + "could not finish job state for {}: {}", + job.jobtype().to_string(), + err + ); + } + + job_result + } + )?; + + Ok(upid_str) +} + #[api( input: { properties: { @@ -100,9 +173,11 @@ pub fn backup( let eject_media = eject_media.unwrap_or(false); let export_media_set = export_media_set.unwrap_or(false); + let job_id = format!("{}:{}:{}", store, pool, drive); + let upid_str = WorkerTask::new_thread( "tape-backup", - Some(store), + Some(job_id), auth_id, to_stdout, move |worker| { diff --git a/src/bin/proxmox-backup-proxy.rs b/src/bin/proxmox-backup-proxy.rs index 0cfd56d5..987944da 100644 --- a/src/bin/proxmox-backup-proxy.rs +++ b/src/bin/proxmox-backup-proxy.rs @@ -49,6 +49,7 @@ use proxmox_backup::tools::{ }; use proxmox_backup::api2::pull::do_sync_job; +use proxmox_backup::api2::tape::backup::do_tape_backup_job; use proxmox_backup::server::do_verification_job; use proxmox_backup::server::do_prune_job; @@ -312,6 +313,7 @@ async fn schedule_tasks() -> Result<(), Error> { schedule_datastore_prune().await; schedule_datastore_sync_jobs().await; schedule_datastore_verify_jobs().await; + schedule_tape_backup_jobs().await; schedule_task_log_rotate().await; Ok(()) @@ -550,6 +552,48 @@ async fn schedule_datastore_verify_jobs() { } } +async fn schedule_tape_backup_jobs() { + + use proxmox_backup::config::tape_job::{ + self, + TapeBackupJobConfig, + }; + + let config = match tape_job::config() { + Err(err) => { + eprintln!("unable to read tape job config - {}", err); + return; + } + Ok((config, _digest)) => config, + }; + for (job_id, (_, job_config)) in config.sections { + let job_config: TapeBackupJobConfig = match serde_json::from_value(job_config) { + Ok(c) => c, + Err(err) => { + eprintln!("tape backup job config from_value failed - {}", err); + continue; + } + }; + let event_str = match job_config.schedule { + Some(ref event_str) => event_str.clone(), + None => continue, + }; + + let worker_type = "tape-backup-job"; + let auth_id = Authid::root_auth_id().clone(); + if check_schedule(worker_type, &event_str, &job_id) { + let job = match Job::new(&worker_type, &job_id) { + Ok(job) => job, + Err(_) => continue, // could not get lock + }; + if let Err(err) = do_tape_backup_job(job, job_config, &auth_id, Some(event_str)) { + eprintln!("unable to start tape bvackup job {} - {}", &job_id, err); + } + }; + } +} + + async fn schedule_task_log_rotate() { let worker_type = "logrotate"; diff --git a/src/config/tape_job.rs b/src/config/tape_job.rs index e623287a..970fb7fb 100644 --- a/src/config/tape_job.rs +++ b/src/config/tape_job.rs @@ -42,6 +42,16 @@ lazy_static! { drive: { schema: DRIVE_NAME_SCHEMA, }, + "eject-media": { + description: "Eject media upon job completion.", + type: bool, + optional: true, + }, + "export-media-set": { + description: "Export media set upon job completion.", + type: bool, + optional: true, + }, comment: { optional: true, schema: SINGLE_LINE_COMMENT_SCHEMA, @@ -62,6 +72,10 @@ pub struct TapeBackupJobConfig { pub pool: String, pub drive: String, #[serde(skip_serializing_if="Option::is_none")] + eject_media: Option, + #[serde(skip_serializing_if="Option::is_none")] + export_media_set: Option, + #[serde(skip_serializing_if="Option::is_none")] pub comment: Option, #[serde(skip_serializing_if="Option::is_none")] pub schedule: Option, diff --git a/www/Utils.js b/www/Utils.js index 3bb709e4..7c8f3fd3 100644 --- a/www/Utils.js +++ b/www/Utils.js @@ -161,6 +161,17 @@ Ext.define('PBS.Utils', { return `Datastore ${what} ${id}`; }, + render_tape_backup_id: function(id, what) { + const res = id.match(/^(\S+?):(\S+?):(\S+?)(:(.+))?$/); + if (res) { + let datastore = res[1]; + let pool = res[2]; + let drive = res[3]; + return `${what} ${datastore} (pool ${pool}, drive ${drive})`; + } + return `${what} ${id}`; + }, + // mimics Display trait in backend renderKeyID: function(fingerprint) { return fingerprint.substring(0, 23); @@ -295,7 +306,8 @@ Ext.define('PBS.Utils', { // do whatever you want here Proxmox.Utils.override_task_descriptions({ backup: (type, id) => PBS.Utils.render_datastore_worker_id(id, gettext('Backup')), - "tape-backup": ['Datastore', gettext('Tape Backup')], + "tape-backup": (type, id) => PBS.Utils.render_tape_backup_id(id, gettext('Tape Backup')), + "tape-backup-job": (type, id) => PBS.Utils.render_tape_backup_id(id, gettext('Tape Backup Job')), "tape-restore": ['Datastore', gettext('Tape Restore')], "barcode-label-media": [gettext('Drive'), gettext('Barcode label media')], dircreate: [gettext('Directory Storage'), gettext('Create')],