2 use std
::sync
::{Mutex, Arc}
;
4 use anyhow
::{bail, format_err, Error}
;
7 use proxmox_lang
::try_block
;
8 use proxmox_router
::{Permission, Router, RpcEnvironment, RpcEnvironmentType}
;
9 use proxmox_schema
::api
;
12 Authid
, Userid
, TapeBackupJobConfig
, TapeBackupJobSetup
, TapeBackupJobStatus
, MediaPoolConfig
,
13 UPID_SCHEMA
, JOB_ID_SCHEMA
, PRIV_DATASTORE_READ
, PRIV_TAPE_AUDIT
, PRIV_TAPE_WRITE
,
17 use pbs_datastore
::{DataStore, StoreProgress, SnapshotReader}
;
18 use pbs_datastore
::backup_info
::{BackupDir, BackupInfo, BackupGroup}
;
19 use pbs_tools
::{task_log, task_warn, task::WorkerTaskContext}
;
20 use pbs_config
::CachedUserInfo
;
21 use proxmox_rest_server
::WorkerTask
;
30 compute_schedule_status
,
42 set_tape_device_state
,
44 changer
::update_changer_online_status
,
48 const TAPE_BACKUP_JOB_ROUTER
: Router
= Router
::new()
49 .post(&API_METHOD_RUN_TAPE_BACKUP_JOB
);
51 pub const ROUTER
: Router
= Router
::new()
52 .get(&API_METHOD_LIST_TAPE_BACKUP_JOBS
)
53 .post(&API_METHOD_BACKUP
)
54 .match_all("id", &TAPE_BACKUP_JOB_ROUTER
);
56 fn check_backup_permission(
61 ) -> Result
<(), Error
> {
63 let user_info
= CachedUserInfo
::new()?
;
65 let privs
= user_info
.lookup_privs(auth_id
, &["datastore", store
]);
66 if (privs
& PRIV_DATASTORE_READ
) == 0 {
67 bail
!("no permissions on /datastore/{}", store
);
70 let privs
= user_info
.lookup_privs(auth_id
, &["tape", "drive", drive
]);
71 if (privs
& PRIV_TAPE_WRITE
) == 0 {
72 bail
!("no permissions on /tape/drive/{}", drive
);
75 let privs
= user_info
.lookup_privs(auth_id
, &["tape", "pool", pool
]);
76 if (privs
& PRIV_TAPE_WRITE
) == 0 {
77 bail
!("no permissions on /tape/pool/{}", pool
);
85 description
: "List configured thape backup jobs and their status",
87 items
: { type: TapeBackupJobStatus }
,
90 description
: "List configured tape jobs filtered by Tape.Audit privileges",
91 permission
: &Permission
::Anybody
,
94 /// List all tape backup jobs
95 pub fn list_tape_backup_jobs(
97 mut rpcenv
: &mut dyn RpcEnvironment
,
98 ) -> Result
<Vec
<TapeBackupJobStatus
>, Error
> {
99 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
100 let user_info
= CachedUserInfo
::new()?
;
102 let (job_config
, digest
) = pbs_config
::tape_job
::config()?
;
103 let (pool_config
, _pool_digest
) = pbs_config
::media_pool
::config()?
;
104 let (drive_config
, _digest
) = pbs_config
::drive
::config()?
;
106 let job_list_iter
= job_config
107 .convert_to_typed_array("backup")?
109 .filter(|_job
: &TapeBackupJobConfig
| {
110 // fixme: check access permission
114 let mut list
= Vec
::new();
115 let status_path
= Path
::new(TAPE_STATUS_DIR
);
116 let current_time
= proxmox_time
::epoch_i64();
118 for job
in job_list_iter
{
119 let privs
= user_info
.lookup_privs(&auth_id
, &["tape", "job", &job
.id
]);
120 if (privs
& PRIV_TAPE_AUDIT
) == 0 {
124 let last_state
= JobState
::load("tape-backup-job", &job
.id
)
125 .map_err(|err
| format_err
!("could not open statefile for {}: {}", &job
.id
, err
))?
;
127 let status
= compute_schedule_status(&last_state
, job
.schedule
.as_deref())?
;
129 let next_run
= status
.next_run
.unwrap_or(current_time
);
131 let mut next_media_label
= None
;
133 if let Ok(pool
) = pool_config
.lookup
::<MediaPoolConfig
>("pool", &job
.setup
.pool
) {
134 let mut changer_name
= None
;
135 if let Ok(Some((_
, name
))) = media_changer(&drive_config
, &job
.setup
.drive
) {
136 changer_name
= Some(name
);
138 if let Ok(mut pool
) = MediaPool
::with_config(status_path
, &pool
, changer_name
, true) {
139 if pool
.start_write_session(next_run
, false).is_ok() {
140 if let Ok(media_id
) = pool
.guess_next_writable_media(next_run
) {
141 next_media_label
= Some(media_id
.label
.label_text
);
147 list
.push(TapeBackupJobStatus { config: job, status, next_media_label }
);
150 rpcenv
["digest"] = proxmox
::tools
::digest_to_hex(&digest
).into();
155 pub fn do_tape_backup_job(
157 setup
: TapeBackupJobSetup
,
159 schedule
: Option
<String
>,
161 ) -> Result
<String
, Error
> {
163 let job_id
= format
!("{}:{}:{}:{}",
169 let worker_type
= job
.jobtype().to_string();
171 let datastore
= DataStore
::lookup_datastore(&setup
.store
)?
;
173 let (config
, _digest
) = pbs_config
::media_pool
::config()?
;
174 let pool_config
: MediaPoolConfig
= config
.lookup("pool", &setup
.pool
)?
;
176 let (drive_config
, _digest
) = pbs_config
::drive
::config()?
;
178 // for scheduled jobs we acquire the lock later in the worker
179 let drive_lock
= if schedule
.is_some() {
182 Some(lock_tape_device(&drive_config
, &setup
.drive
)?
)
185 let notify_user
= setup
.notify_user
.as_ref().unwrap_or_else(|| &Userid
::root_userid());
186 let email
= lookup_user_email(notify_user
);
188 let upid_str
= WorkerTask
::new_thread(
190 Some(job_id
.clone()),
194 job
.start(&worker
.upid().to_string())?
;
195 let mut drive_lock
= drive_lock
;
197 let mut summary
= Default
::default();
198 let job_result
= try_block
!({
199 if schedule
.is_some() {
200 // for scheduled tape backup jobs, we wait indefinitely for the lock
201 task_log
!(worker
, "waiting for drive lock...");
203 worker
.check_abort()?
;
204 match lock_tape_device(&drive_config
, &setup
.drive
) {
206 drive_lock
= Some(lock
);
209 Err(TapeLockError
::TimeOut
) => continue,
210 Err(TapeLockError
::Other(err
)) => return Err(err
),
214 set_tape_device_state(&setup
.drive
, &worker
.upid().to_string())?
;
216 task_log
!(worker
,"Starting tape backup job '{}'", job_id
);
217 if let Some(event_str
) = schedule
{
218 task_log
!(worker
,"task triggered by schedule '{}'", event_str
);
233 let status
= worker
.create_state(&job_result
);
235 if let Some(email
) = email
{
236 if let Err(err
) = crate::server
::send_tape_backup_status(
243 eprintln
!("send tape backup notification failed: {}", err
);
247 if let Err(err
) = job
.finish(status
) {
249 "could not finish job state for {}: {}",
250 job
.jobtype().to_string(),
255 if let Err(err
) = set_tape_device_state(&setup
.drive
, "") {
257 "could not unset drive state for {}: {}",
274 schema
: JOB_ID_SCHEMA
,
279 // Note: parameters are from job config, so we need to test inside function body
280 description
: "The user needs Tape.Write privilege on /tape/pool/{pool} \
281 and /tape/drive/{drive}, Datastore.Read privilege on /datastore/{store}.",
282 permission
: &Permission
::Anybody
,
285 /// Runs a tape backup job manually.
286 pub fn run_tape_backup_job(
288 rpcenv
: &mut dyn RpcEnvironment
,
289 ) -> Result
<String
, Error
> {
290 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
292 let (config
, _digest
) = pbs_config
::tape_job
::config()?
;
293 let backup_job
: TapeBackupJobConfig
= config
.lookup("backup", &id
)?
;
295 check_backup_permission(
297 &backup_job
.setup
.store
,
298 &backup_job
.setup
.pool
,
299 &backup_job
.setup
.drive
,
302 let job
= Job
::new("tape-backup-job", &id
)?
;
304 let to_stdout
= rpcenv
.env_type() == RpcEnvironmentType
::CLI
;
306 let upid_str
= do_tape_backup_job(job
, backup_job
.setup
, &auth_id
, None
, to_stdout
)?
;
315 type: TapeBackupJobSetup
,
319 description
: "Ignore the allocation policy and start a new media-set.",
330 // Note: parameters are no uri parameter, so we need to test inside function body
331 description
: "The user needs Tape.Write privilege on /tape/pool/{pool} \
332 and /tape/drive/{drive}, Datastore.Read privilege on /datastore/{store}.",
333 permission
: &Permission
::Anybody
,
336 /// Backup datastore to tape media pool
338 setup
: TapeBackupJobSetup
,
339 force_media_set
: bool
,
340 rpcenv
: &mut dyn RpcEnvironment
,
341 ) -> Result
<Value
, Error
> {
343 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
345 check_backup_permission(
352 let datastore
= DataStore
::lookup_datastore(&setup
.store
)?
;
354 let (config
, _digest
) = pbs_config
::media_pool
::config()?
;
355 let pool_config
: MediaPoolConfig
= config
.lookup("pool", &setup
.pool
)?
;
357 let (drive_config
, _digest
) = pbs_config
::drive
::config()?
;
359 // early check/lock before starting worker
360 let drive_lock
= lock_tape_device(&drive_config
, &setup
.drive
)?
;
362 let to_stdout
= rpcenv
.env_type() == RpcEnvironmentType
::CLI
;
364 let job_id
= format
!("{}:{}:{}", setup
.store
, setup
.pool
, setup
.drive
);
366 let notify_user
= setup
.notify_user
.as_ref().unwrap_or_else(|| &Userid
::root_userid());
367 let email
= lookup_user_email(notify_user
);
369 let upid_str
= WorkerTask
::new_thread(
375 let _drive_lock
= drive_lock
; // keep lock guard
376 set_tape_device_state(&setup
.drive
, &worker
.upid().to_string())?
;
378 let mut summary
= Default
::default();
379 let job_result
= backup_worker(
389 if let Some(email
) = email
{
390 if let Err(err
) = crate::server
::send_tape_backup_status(
397 eprintln
!("send tape backup notification failed: {}", err
);
402 let _
= set_tape_device_state(&setup
.drive
, "");
412 datastore
: Arc
<DataStore
>,
413 pool_config
: &MediaPoolConfig
,
414 setup
: &TapeBackupJobSetup
,
415 email
: Option
<String
>,
416 summary
: &mut TapeBackupJobSummary
,
417 force_media_set
: bool
,
418 ) -> Result
<(), Error
> {
420 let status_path
= Path
::new(TAPE_STATUS_DIR
);
421 let start
= std
::time
::Instant
::now();
423 task_log
!(worker
, "update media online status");
424 let changer_name
= update_media_online_status(&setup
.drive
)?
;
426 let pool
= MediaPool
::with_config(status_path
, &pool_config
, changer_name
, false)?
;
428 let mut pool_writer
= PoolWriter
::new(
436 let mut group_list
= BackupInfo
::list_backup_groups(&datastore
.base_path())?
;
438 group_list
.sort_unstable();
440 let (group_list
, group_count
) = if let Some(group_filters
) = &setup
.groups
{
441 let filter_fn
= |group
: &BackupGroup
, group_filters
: &[GroupFilter
]| {
442 group_filters
.iter().any(|filter
| group
.matches(filter
))
445 let group_count_full
= group_list
.len();
446 let list
: Vec
<BackupGroup
> = group_list
.into_iter().filter(|group
| filter_fn(group
, &group_filters
)).collect();
447 let group_count
= list
.len();
448 task_log
!(worker
, "found {} groups (out of {} total)", group_count
, group_count_full
);
451 let group_count
= group_list
.len();
452 task_log
!(worker
, "found {} groups", group_count
);
453 (group_list
, group_count
)
456 let mut progress
= StoreProgress
::new(group_count
as u64);
458 let latest_only
= setup
.latest_only
.unwrap_or(false);
461 task_log
!(worker
, "latest-only: true (only considering latest snapshots)");
464 let datastore_name
= datastore
.name();
466 let mut errors
= false;
468 let mut need_catalog
= false; // avoid writing catalog for empty jobs
470 for (group_number
, group
) in group_list
.into_iter().enumerate() {
471 progress
.done_groups
= group_number
as u64;
472 progress
.done_snapshots
= 0;
473 progress
.group_snapshots
= 0;
475 let snapshot_list
= group
.list_backups(&datastore
.base_path())?
;
477 // filter out unfinished backups
478 let mut snapshot_list
: Vec
<_
> = snapshot_list
480 .filter(|item
| item
.is_finished())
483 if snapshot_list
.is_empty() {
484 task_log
!(worker
, "group {} was empty", group
);
488 BackupInfo
::sort_list(&mut snapshot_list
, true); // oldest first
491 progress
.group_snapshots
= 1;
492 if let Some(info
) = snapshot_list
.pop() {
493 if pool_writer
.contains_snapshot(datastore_name
, &info
.backup_dir
.to_string()) {
494 task_log
!(worker
, "skip snapshot {}", info
.backup_dir
);
500 let snapshot_name
= info
.backup_dir
.to_string();
501 if !backup_snapshot(worker
, &mut pool_writer
, datastore
.clone(), info
.backup_dir
)?
{
504 summary
.snapshot_list
.push(snapshot_name
);
506 progress
.done_snapshots
= 1;
509 "percentage done: {}",
514 progress
.group_snapshots
= snapshot_list
.len() as u64;
515 for (snapshot_number
, info
) in snapshot_list
.into_iter().enumerate() {
516 if pool_writer
.contains_snapshot(datastore_name
, &info
.backup_dir
.to_string()) {
517 task_log
!(worker
, "skip snapshot {}", info
.backup_dir
);
523 let snapshot_name
= info
.backup_dir
.to_string();
524 if !backup_snapshot(worker
, &mut pool_writer
, datastore
.clone(), info
.backup_dir
)?
{
527 summary
.snapshot_list
.push(snapshot_name
);
529 progress
.done_snapshots
= snapshot_number
as u64 + 1;
532 "percentage done: {}",
539 pool_writer
.commit()?
;
542 task_log
!(worker
, "append media catalog");
544 let uuid
= pool_writer
.load_writable_media(worker
)?
;
545 let done
= pool_writer
.append_catalog_archive(worker
)?
;
547 task_log
!(worker
, "catalog does not fit on tape, writing to next volume");
548 pool_writer
.set_media_status_full(&uuid
)?
;
549 pool_writer
.load_writable_media(worker
)?
;
550 let done
= pool_writer
.append_catalog_archive(worker
)?
;
552 bail
!("write_catalog_archive failed on second media");
557 if setup
.export_media_set
.unwrap_or(false) {
558 pool_writer
.export_media_set(worker
)?
;
559 } else if setup
.eject_media
.unwrap_or(false) {
560 pool_writer
.eject_media(worker
)?
;
564 bail
!("Tape backup finished with some errors. Please check the task log.");
567 summary
.duration
= start
.elapsed();
572 // Try to update the the media online status
573 fn update_media_online_status(drive
: &str) -> Result
<Option
<String
>, Error
> {
575 let (config
, _digest
) = pbs_config
::drive
::config()?
;
577 if let Ok(Some((mut changer
, changer_name
))) = media_changer(&config
, drive
) {
579 let label_text_list
= changer
.online_media_label_texts()?
;
581 let status_path
= Path
::new(TAPE_STATUS_DIR
);
582 let mut inventory
= Inventory
::load(status_path
)?
;
584 update_changer_online_status(
591 Ok(Some(changer_name
))
597 pub fn backup_snapshot(
599 pool_writer
: &mut PoolWriter
,
600 datastore
: Arc
<DataStore
>,
602 ) -> Result
<bool
, Error
> {
604 task_log
!(worker
, "backup snapshot {}", snapshot
);
606 let snapshot_reader
= match SnapshotReader
::new(datastore
.clone(), snapshot
.clone()) {
607 Ok(reader
) => reader
,
609 // ignore missing snapshots and continue
610 task_warn
!(worker
, "failed opening snapshot '{}': {}", snapshot
, err
);
615 let snapshot_reader
= Arc
::new(Mutex
::new(snapshot_reader
));
617 let (reader_thread
, chunk_iter
) = pool_writer
.spawn_chunk_reader_thread(
619 snapshot_reader
.clone(),
622 let mut chunk_iter
= chunk_iter
.peekable();
625 worker
.check_abort()?
;
627 // test is we have remaining chunks
628 match chunk_iter
.peek() {
630 Some(Ok(_
)) => { /* Ok */ }
,
631 Some(Err(err
)) => bail
!("{}", err
),
634 let uuid
= pool_writer
.load_writable_media(worker
)?
;
636 worker
.check_abort()?
;
638 let (leom
, _bytes
) = pool_writer
.append_chunk_archive(worker
, &mut chunk_iter
, datastore
.name())?
;
641 pool_writer
.set_media_status_full(&uuid
)?
;
645 if let Err(_
) = reader_thread
.join() {
646 bail
!("chunk reader thread failed");
649 worker
.check_abort()?
;
651 let uuid
= pool_writer
.load_writable_media(worker
)?
;
653 worker
.check_abort()?
;
655 let snapshot_reader
= snapshot_reader
.lock().unwrap();
657 let (done
, _bytes
) = pool_writer
.append_snapshot_archive(worker
, &snapshot_reader
)?
;
660 // does not fit on tape, so we try on next volume
661 pool_writer
.set_media_status_full(&uuid
)?
;
663 worker
.check_abort()?
;
665 pool_writer
.load_writable_media(worker
)?
;
666 let (done
, _bytes
) = pool_writer
.append_snapshot_archive(worker
, &snapshot_reader
)?
;
669 bail
!("write_snapshot_archive failed on second media");
673 task_log
!(worker
, "end backup {}:{}", datastore
.name(), snapshot
);