diff --git a/src/api2/tape/backup.rs b/src/api2/tape/backup.rs index 1462f200..589051bb 100644 --- a/src/api2/tape/backup.rs +++ b/src/api2/tape/backup.rs @@ -1,5 +1,5 @@ use std::path::Path; -use std::sync::{Mutex, Arc}; +use std::sync::{Arc, Mutex}; use anyhow::{bail, format_err, Error}; use serde_json::Value; @@ -10,43 +10,29 @@ use proxmox_schema::api; use proxmox_sys::{task_log, task_warn, WorkerTaskContext}; use pbs_api_types::{ - Authid, Userid, TapeBackupJobConfig, TapeBackupJobSetup, TapeBackupJobStatus, MediaPoolConfig, - UPID_SCHEMA, JOB_ID_SCHEMA, PRIV_DATASTORE_READ, PRIV_TAPE_AUDIT, PRIV_TAPE_WRITE, - GroupFilter, + Authid, GroupFilter, MediaPoolConfig, TapeBackupJobConfig, TapeBackupJobSetup, + TapeBackupJobStatus, Userid, JOB_ID_SCHEMA, PRIV_DATASTORE_READ, PRIV_TAPE_AUDIT, + PRIV_TAPE_WRITE, UPID_SCHEMA, }; -use pbs_datastore::{DataStore, StoreProgress, SnapshotReader}; -use pbs_datastore::backup_info::{BackupDir, BackupInfo, BackupGroup}; use pbs_config::CachedUserInfo; +use pbs_datastore::backup_info::{BackupDir, BackupGroup, BackupInfo}; +use pbs_datastore::{DataStore, SnapshotReader, StoreProgress}; use proxmox_rest_server::WorkerTask; use crate::{ server::{ - lookup_user_email, - TapeBackupJobSummary, - jobstate::{ - Job, - JobState, - compute_schedule_status, - }, + jobstate::{compute_schedule_status, Job, JobState}, + lookup_user_email, TapeBackupJobSummary, }, tape::{ - TAPE_STATUS_DIR, - Inventory, - PoolWriter, - MediaPool, - drive::{ - media_changer, - lock_tape_device, - TapeLockError, - set_tape_device_state, - }, changer::update_changer_online_status, + drive::{lock_tape_device, media_changer, set_tape_device_state, TapeLockError}, + Inventory, MediaPool, PoolWriter, TAPE_STATUS_DIR, }, }; -const TAPE_BACKUP_JOB_ROUTER: Router = Router::new() - .post(&API_METHOD_RUN_TAPE_BACKUP_JOB); +const TAPE_BACKUP_JOB_ROUTER: Router = Router::new().post(&API_METHOD_RUN_TAPE_BACKUP_JOB); pub const ROUTER: Router = Router::new() .get(&API_METHOD_LIST_TAPE_BACKUP_JOBS) @@ -59,7 +45,6 @@ fn check_backup_permission( pool: &str, drive: &str, ) -> Result<(), Error> { - let user_info = CachedUserInfo::new()?; let privs = user_info.lookup_privs(auth_id, &["datastore", store]); @@ -144,7 +129,11 @@ pub fn list_tape_backup_jobs( } } - list.push(TapeBackupJobStatus { config: job, status, next_media_label }); + list.push(TapeBackupJobStatus { + config: job, + status, + next_media_label, + }); } rpcenv["digest"] = hex::encode(&digest).into(); @@ -159,12 +148,13 @@ pub fn do_tape_backup_job( schedule: Option, to_stdout: bool, ) -> Result { - - let job_id = format!("{}:{}:{}:{}", - setup.store, - setup.pool, - setup.drive, - job.jobname()); + let job_id = format!( + "{}:{}:{}:{}", + setup.store, + setup.pool, + setup.drive, + job.jobname() + ); let worker_type = job.jobtype().to_string(); @@ -182,7 +172,10 @@ pub fn do_tape_backup_job( Some(lock_tape_device(&drive_config, &setup.drive)?) }; - let notify_user = setup.notify_user.as_ref().unwrap_or_else(|| Userid::root_userid()); + let notify_user = setup + .notify_user + .as_ref() + .unwrap_or_else(|| Userid::root_userid()); let email = lookup_user_email(notify_user); let upid_str = WorkerTask::new_thread( @@ -213,12 +206,11 @@ pub fn do_tape_backup_job( } set_tape_device_state(&setup.drive, &worker.upid().to_string())?; - task_log!(worker,"Starting tape backup job '{}'", job_id); + task_log!(worker, "Starting tape backup job '{}'", job_id); if let Some(event_str) = schedule { - task_log!(worker,"task triggered by schedule '{}'", event_str); + task_log!(worker, "task triggered by schedule '{}'", event_str); } - backup_worker( &worker, datastore, @@ -253,15 +245,11 @@ pub fn do_tape_backup_job( } if let Err(err) = set_tape_device_state(&setup.drive, "") { - eprintln!( - "could not unset drive state for {}: {}", - setup.drive, - err - ); + eprintln!("could not unset drive state for {}: {}", setup.drive, err); } job_result - } + }, )?; Ok(upid_str) @@ -283,10 +271,7 @@ pub fn do_tape_backup_job( }, )] /// Runs a tape backup job manually. -pub fn run_tape_backup_job( - id: String, - rpcenv: &mut dyn RpcEnvironment, -) -> Result { +pub fn run_tape_backup_job(id: String, rpcenv: &mut dyn RpcEnvironment) -> Result { let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?; let (config, _digest) = pbs_config::tape_job::config()?; @@ -339,15 +324,9 @@ pub fn backup( force_media_set: bool, rpcenv: &mut dyn RpcEnvironment, ) -> Result { - let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?; - check_backup_permission( - &auth_id, - &setup.store, - &setup.pool, - &setup.drive, - )?; + check_backup_permission(&auth_id, &setup.store, &setup.pool, &setup.drive)?; let datastore = DataStore::lookup_datastore(&setup.store)?; @@ -363,7 +342,10 @@ pub fn backup( let job_id = format!("{}:{}:{}", setup.store, setup.pool, setup.drive); - let notify_user = setup.notify_user.as_ref().unwrap_or_else(|| Userid::root_userid()); + let notify_user = setup + .notify_user + .as_ref() + .unwrap_or_else(|| Userid::root_userid()); let email = lookup_user_email(notify_user); let upid_str = WorkerTask::new_thread( @@ -401,7 +383,7 @@ pub fn backup( // ignore errors let _ = set_tape_device_state(&setup.drive, ""); job_result - } + }, )?; Ok(upid_str.into()) @@ -416,7 +398,6 @@ fn backup_worker( summary: &mut TapeBackupJobSummary, force_media_set: bool, ) -> Result<(), Error> { - let status_path = Path::new(TAPE_STATUS_DIR); let start = std::time::Instant::now(); @@ -425,13 +406,7 @@ fn backup_worker( let pool = MediaPool::with_config(status_path, pool_config, changer_name, false)?; - let mut pool_writer = PoolWriter::new( - pool, - &setup.drive, - worker, - email, - force_media_set - )?; + let mut pool_writer = PoolWriter::new(pool, &setup.drive, worker, email, force_media_set)?; let mut group_list = BackupInfo::list_backup_groups(&datastore.base_path())?; @@ -443,9 +418,17 @@ fn backup_worker( }; let group_count_full = group_list.len(); - let list: Vec = group_list.into_iter().filter(|group| filter_fn(group, group_filters)).collect(); + let list: Vec = group_list + .into_iter() + .filter(|group| filter_fn(group, group_filters)) + .collect(); let group_count = list.len(); - task_log!(worker, "found {} groups (out of {} total)", group_count, group_count_full); + task_log!( + worker, + "found {} groups (out of {} total)", + group_count, + group_count_full + ); (list, group_count) } else { let group_count = group_list.len(); @@ -458,7 +441,10 @@ fn backup_worker( let latest_only = setup.latest_only.unwrap_or(false); if latest_only { - task_log!(worker, "latest-only: true (only considering latest snapshots)"); + task_log!( + worker, + "latest-only: true (only considering latest snapshots)" + ); } let datastore_name = datastore.name(); @@ -504,11 +490,7 @@ fn backup_worker( summary.snapshot_list.push(snapshot_name); } progress.done_snapshots = 1; - task_log!( - worker, - "percentage done: {}", - progress - ); + task_log!(worker, "percentage done: {}", progress); } } else { progress.group_snapshots = snapshot_list.len() as u64; @@ -527,11 +509,7 @@ fn backup_worker( summary.snapshot_list.push(snapshot_name); } progress.done_snapshots = snapshot_number as u64 + 1; - task_log!( - worker, - "percentage done: {}", - progress - ); + task_log!(worker, "percentage done: {}", progress); } } } @@ -544,7 +522,10 @@ fn backup_worker( let uuid = pool_writer.load_writable_media(worker)?; let done = pool_writer.append_catalog_archive(worker)?; if !done { - task_log!(worker, "catalog does not fit on tape, writing to next volume"); + task_log!( + worker, + "catalog does not fit on tape, writing to next volume" + ); pool_writer.set_media_status_full(&uuid)?; pool_writer.load_writable_media(worker)?; let done = pool_writer.append_catalog_archive(worker)?; @@ -571,22 +552,15 @@ fn backup_worker( // Try to update the the media online status fn update_media_online_status(drive: &str) -> Result, Error> { - let (config, _digest) = pbs_config::drive::config()?; if let Ok(Some((mut changer, changer_name))) = media_changer(&config, drive) { - - let label_text_list = changer.online_media_label_texts()?; + let label_text_list = changer.online_media_label_texts()?; let status_path = Path::new(TAPE_STATUS_DIR); let mut inventory = Inventory::load(status_path)?; - update_changer_online_status( - &config, - &mut inventory, - &changer_name, - &label_text_list, - )?; + update_changer_online_status(&config, &mut inventory, &changer_name, &label_text_list)?; Ok(Some(changer_name)) } else { @@ -600,7 +574,6 @@ pub fn backup_snapshot( datastore: Arc, snapshot: BackupDir, ) -> Result { - task_log!(worker, "backup snapshot {}", snapshot); let snapshot_reader = match SnapshotReader::new(datastore.clone(), snapshot.clone()) { @@ -614,10 +587,8 @@ pub fn backup_snapshot( let snapshot_reader = Arc::new(Mutex::new(snapshot_reader)); - let (reader_thread, chunk_iter) = pool_writer.spawn_chunk_reader_thread( - datastore.clone(), - snapshot_reader.clone(), - )?; + let (reader_thread, chunk_iter) = + pool_writer.spawn_chunk_reader_thread(datastore.clone(), snapshot_reader.clone())?; let mut chunk_iter = chunk_iter.peekable(); @@ -627,7 +598,7 @@ pub fn backup_snapshot( // test is we have remaining chunks match chunk_iter.peek() { None => break, - Some(Ok(_)) => { /* Ok */ }, + Some(Ok(_)) => { /* Ok */ } Some(Err(err)) => bail!("{}", err), } @@ -635,7 +606,8 @@ pub fn backup_snapshot( worker.check_abort()?; - let (leom, _bytes) = pool_writer.append_chunk_archive(worker, &mut chunk_iter, datastore.name())?; + let (leom, _bytes) = + pool_writer.append_chunk_archive(worker, &mut chunk_iter, datastore.name())?; if leom { pool_writer.set_media_status_full(&uuid)?; diff --git a/src/api2/tape/changer.rs b/src/api2/tape/changer.rs index d2ac65f6..54fab87f 100644 --- a/src/api2/tape/changer.rs +++ b/src/api2/tape/changer.rs @@ -4,8 +4,8 @@ use std::path::Path; use anyhow::Error; use serde_json::Value; -use proxmox_schema::api; use proxmox_router::{list_subdirs_api_method, Permission, Router, RpcEnvironment, SubdirMap}; +use proxmox_schema::api; use pbs_api_types::{ Authid, ChangerListEntry, LtoTapeDrive, MtxEntryKind, MtxStatusEntry, ScsiTapeChanger, @@ -13,24 +13,16 @@ use pbs_api_types::{ }; use pbs_config::CachedUserInfo; use pbs_tape::{ + linux_list_drives::{linux_tape_changer_list, lookup_device_identification}, ElementStatus, - linux_list_drives::{lookup_device_identification, linux_tape_changer_list}, }; -use crate::{ - tape::{ - TAPE_STATUS_DIR, - Inventory, - changer::{ - OnlineStatusMap, - ScsiMediaChange, - mtx_status_to_online_set, - }, - drive::get_tape_device_state, - }, +use crate::tape::{ + changer::{mtx_status_to_online_set, OnlineStatusMap, ScsiMediaChange}, + drive::get_tape_device_state, + Inventory, TAPE_STATUS_DIR, }; - #[api( input: { properties: { @@ -56,18 +48,12 @@ use crate::{ }, )] /// Get tape changer status -pub async fn get_status( - name: String, - cache: bool, -) -> Result, Error> { - +pub async fn get_status(name: String, cache: bool) -> Result, Error> { let (config, _digest) = pbs_config::drive::config()?; let mut changer_config: ScsiTapeChanger = config.lookup("changer", &name)?; - let status = tokio::task::spawn_blocking(move || { - changer_config.status(cache) - }).await??; + let status = tokio::task::spawn_blocking(move || changer_config.status(cache)).await??; let state_path = Path::new(TAPE_STATUS_DIR); let mut inventory = Inventory::load(state_path)?; @@ -155,12 +141,7 @@ pub async fn get_status( }, )] /// Transfers media from one slot to another -pub async fn transfer( - name: String, - from: u64, - to: u64, -) -> Result<(), Error> { - +pub async fn transfer(name: String, from: u64, to: u64) -> Result<(), Error> { let (config, _digest) = pbs_config::drive::config()?; let mut changer_config: ScsiTapeChanger = config.lookup("changer", &name)?; @@ -168,7 +149,8 @@ pub async fn transfer( tokio::task::spawn_blocking(move || { changer_config.transfer(from, to)?; Ok(()) - }).await? + }) + .await? } #[api( @@ -210,23 +192,18 @@ pub fn list_changers( } let info = lookup_device_identification(&linux_changers, &changer.path); - let entry = ChangerListEntry { config: changer, info }; + let entry = ChangerListEntry { + config: changer, + info, + }; list.push(entry); } Ok(list) } const SUBDIRS: SubdirMap = &[ - ( - "status", - &Router::new() - .get(&API_METHOD_GET_STATUS) - ), - ( - "transfer", - &Router::new() - .post(&API_METHOD_TRANSFER) - ), + ("status", &Router::new().get(&API_METHOD_GET_STATUS)), + ("transfer", &Router::new().post(&API_METHOD_TRANSFER)), ]; const ITEM_ROUTER: Router = Router::new() diff --git a/src/api2/tape/drive.rs b/src/api2/tape/drive.rs index cc20fb16..70aaf761 100644 --- a/src/api2/tape/drive.rs +++ b/src/api2/tape/drive.rs @@ -1,67 +1,50 @@ +use std::collections::HashMap; use std::panic::UnwindSafe; use std::path::Path; use std::sync::Arc; -use std::collections::HashMap; use anyhow::{bail, format_err, Error}; use serde_json::Value; -use proxmox_sys::sortable; use proxmox_router::{ list_subdirs_api_method, Permission, Router, RpcEnvironment, RpcEnvironmentType, SubdirMap, }; use proxmox_schema::api; use proxmox_section_config::SectionConfigData; -use proxmox_uuid::Uuid; +use proxmox_sys::sortable; use proxmox_sys::{task_log, task_warn}; +use proxmox_uuid::Uuid; use pbs_api_types::{ - UPID_SCHEMA, CHANGER_NAME_SCHEMA, DRIVE_NAME_SCHEMA, MEDIA_LABEL_SCHEMA, MEDIA_POOL_NAME_SCHEMA, - Authid, DriveListEntry, LtoTapeDrive, MediaIdFlat, LabelUuidMap, MamAttribute, - LtoDriveAndMediaStatus, Lp17VolumeStatistics, + Authid, DriveListEntry, LabelUuidMap, Lp17VolumeStatistics, LtoDriveAndMediaStatus, + LtoTapeDrive, MamAttribute, MediaIdFlat, CHANGER_NAME_SCHEMA, DRIVE_NAME_SCHEMA, + MEDIA_LABEL_SCHEMA, MEDIA_POOL_NAME_SCHEMA, UPID_SCHEMA, }; - + use pbs_api_types::{PRIV_TAPE_AUDIT, PRIV_TAPE_READ, PRIV_TAPE_WRITE}; -use pbs_config::CachedUserInfo; use pbs_config::key_config::KeyConfig; use pbs_config::tape_encryption_keys::insert_key; +use pbs_config::CachedUserInfo; use pbs_tape::{ - BlockReadError, + linux_list_drives::{lookup_device_identification, lto_tape_device_list, open_lto_tape_device}, sg_tape::tape_alert_flags_critical, - linux_list_drives::{lto_tape_device_list, lookup_device_identification, open_lto_tape_device}, + BlockReadError, }; use proxmox_rest_server::WorkerTask; use crate::{ - api2::tape::restore::{ - fast_catalog_restore, - restore_media, - }, + api2::tape::restore::{fast_catalog_restore, restore_media}, tape::{ - TAPE_STATUS_DIR, - Inventory, - MediaCatalog, - MediaId, - lock_media_set, - lock_media_pool, - lock_unassigned_media_pool, - file_formats::{ - MediaLabel, - MediaSetLabel, - }, - drive::{ - TapeDriver, - LtoTapeHandle, - open_lto_tape_drive, - media_changer, - required_media_changer, - open_drive, - lock_tape_device, - set_tape_device_state, - get_tape_device_state, - }, changer::update_changer_online_status, + drive::{ + get_tape_device_state, lock_tape_device, media_changer, open_drive, + open_lto_tape_drive, required_media_changer, set_tape_device_state, LtoTapeHandle, + TapeDriver, + }, + file_formats::{MediaLabel, MediaSetLabel}, + lock_media_pool, lock_media_set, lock_unassigned_media_pool, Inventory, MediaCatalog, + MediaId, TAPE_STATUS_DIR, }, }; @@ -151,7 +134,12 @@ pub fn load_media( "load-media", Some(job_id), move |worker, config| { - task_log!(worker, "loading media '{}' into drive '{}'", label_text, drive); + task_log!( + worker, + "loading media '{}' into drive '{}'", + label_text, + drive + ); let (mut changer, _) = required_media_changer(&config, &drive)?; changer.load_media(&label_text)?; Ok(()) @@ -228,7 +216,7 @@ pub async fn export_media(drive: String, label_text: String) -> Result Result { +pub fn rewind(drive: String, rpcenv: &mut dyn RpcEnvironment) -> Result { let upid_str = run_drive_worker( rpcenv, drive.clone(), @@ -433,10 +422,7 @@ pub fn rewind( }, )] /// Eject/Unload drive media -pub fn eject_media( - drive: String, - rpcenv: &mut dyn RpcEnvironment, -) -> Result { +pub fn eject_media(drive: String, rpcenv: &mut dyn RpcEnvironment) -> Result { let upid_str = run_drive_worker( rpcenv, drive.clone(), @@ -509,8 +495,8 @@ pub fn label_media( match drive.read_next_file() { Ok(_reader) => bail!("media is not empty (format it first)"), - Err(BlockReadError::EndOfFile) => { /* EOF mark at BOT, assume tape is empty */ }, - Err(BlockReadError::EndOfStream) => { /* tape is empty */ }, + Err(BlockReadError::EndOfFile) => { /* EOF mark at BOT, assume tape is empty */ } + Err(BlockReadError::EndOfStream) => { /* tape is empty */ } Err(err) => { bail!("media read error - {}", err); } @@ -536,19 +522,26 @@ fn write_media_label( label: MediaLabel, pool: Option, ) -> Result<(), Error> { - drive.label_tape(&label)?; let status_path = Path::new(TAPE_STATUS_DIR); let media_id = if let Some(ref pool) = pool { // assign media to pool by writing special media set label - task_log!(worker, "Label media '{}' for pool '{}'", label.label_text, pool); + task_log!( + worker, + "Label media '{}' for pool '{}'", + label.label_text, + pool + ); let set = MediaSetLabel::with_data(pool, [0u8; 16].into(), 0, label.ctime, None); drive.write_media_set_label(&set, None)?; - let media_id = MediaId { label, media_set_label: Some(set) }; + let media_id = MediaId { + label, + media_set_label: Some(set), + }; // Create the media catalog MediaCatalog::overwrite(status_path, &media_id, false)?; @@ -558,9 +551,16 @@ fn write_media_label( media_id } else { - task_log!(worker, "Label media '{}' (no pool assignment)", label.label_text); + task_log!( + worker, + "Label media '{}' (no pool assignment)", + label.label_text + ); - let media_id = MediaId { label, media_set_label: None }; + let media_id = MediaId { + label, + media_set_label: None, + }; // Create the media catalog MediaCatalog::overwrite(status_path, &media_id, false)?; @@ -593,7 +593,7 @@ fn write_media_label( } } } - }, + } Ok((None, _)) => bail!("verify label failed (got empty media)"), Err(err) => bail!("verify label failed - {}", err), }; @@ -668,7 +668,7 @@ pub async fn restore_key( Ok(()) } - #[api( +#[api( input: { properties: { drive: { @@ -688,75 +688,63 @@ pub async fn restore_key( }, )] /// Read media label (optionally inventorize media) -pub async fn read_label( - drive: String, - inventorize: Option, -) -> Result { - run_drive_blocking_task( - drive.clone(), - "reading label".to_string(), - move |config| { - let mut drive = open_drive(&config, &drive)?; +pub async fn read_label(drive: String, inventorize: Option) -> Result { + run_drive_blocking_task(drive.clone(), "reading label".to_string(), move |config| { + let mut drive = open_drive(&config, &drive)?; - let (media_id, _key_config) = drive.read_label()?; + let (media_id, _key_config) = drive.read_label()?; + let media_id = media_id.ok_or(format_err!("Media is empty (no label)."))?; - let media_id = match media_id { - Some(media_id) => { - let mut flat = MediaIdFlat { - uuid: media_id.label.uuid.clone(), - label_text: media_id.label.label_text.clone(), - ctime: media_id.label.ctime, - media_set_ctime: None, - media_set_uuid: None, - encryption_key_fingerprint: None, - pool: None, - seq_nr: None, - }; - if let Some(ref set) = media_id.media_set_label { - flat.pool = Some(set.pool.clone()); - flat.seq_nr = Some(set.seq_nr); - flat.media_set_uuid = Some(set.uuid.clone()); - flat.media_set_ctime = Some(set.ctime); - flat.encryption_key_fingerprint = set - .encryption_key_fingerprint - .as_ref() - .map(|fp| fp.signature()); + let label = if let Some(ref set) = media_id.media_set_label { + let key = &set.encryption_key_fingerprint; - let encrypt_fingerprint = set.encryption_key_fingerprint.clone() - .map(|fp| (fp, set.uuid.clone())); + if let Err(err) = drive.set_encryption(key.clone().map(|fp| (fp, set.uuid.clone()))) { + eprintln!("unable to load encryption key: {}", err); // best-effort only + } + MediaIdFlat { + ctime: media_id.label.ctime, + encryption_key_fingerprint: key.as_ref().map(|fp| fp.signature()), + label_text: media_id.label.label_text.clone(), + media_set_ctime: Some(set.ctime), + media_set_uuid: Some(set.uuid.clone()), + pool: Some(set.pool.clone()), + seq_nr: Some(set.seq_nr), + uuid: media_id.label.uuid.clone(), + } + } else { + MediaIdFlat { + ctime: media_id.label.ctime, + encryption_key_fingerprint: None, + label_text: media_id.label.label_text.clone(), + media_set_ctime: None, + media_set_uuid: None, + pool: None, + seq_nr: None, + uuid: media_id.label.uuid.clone(), + } + }; - if let Err(err) = drive.set_encryption(encrypt_fingerprint) { - // try, but ignore errors. just log to stderr - eprintln!("unable to load encryption key: {}", err); - } - } + if let Some(true) = inventorize { + let state_path = Path::new(TAPE_STATUS_DIR); + let mut inventory = Inventory::new(state_path); - if let Some(true) = inventorize { - let state_path = Path::new(TAPE_STATUS_DIR); - let mut inventory = Inventory::new(state_path); - - if let Some(MediaSetLabel { ref pool, ref uuid, ..}) = media_id.media_set_label { - let _pool_lock = lock_media_pool(state_path, pool)?; - let _lock = lock_media_set(state_path, uuid, None)?; - MediaCatalog::destroy_unrelated_catalog(state_path, &media_id)?; - inventory.store(media_id, false)?; - } else { - let _lock = lock_unassigned_media_pool(state_path)?; - MediaCatalog::destroy(state_path, &media_id.label.uuid)?; - inventory.store(media_id, false)?; - }; - } - - flat - } - None => { - bail!("Media is empty (no label)."); - } + if let Some(MediaSetLabel { + ref pool, ref uuid, .. + }) = media_id.media_set_label + { + let _pool_lock = lock_media_pool(state_path, pool)?; + let _lock = lock_media_set(state_path, uuid, None)?; + MediaCatalog::destroy_unrelated_catalog(state_path, &media_id)?; + inventory.store(media_id, false)?; + } else { + let _lock = lock_unassigned_media_pool(state_path)?; + MediaCatalog::destroy(state_path, &media_id.label.uuid)?; + inventory.store(media_id, false)?; }; - - Ok(media_id) } - ) + + Ok(label) + }) .await } @@ -776,10 +764,7 @@ pub async fn read_label( }, )] /// Clean drive -pub fn clean_drive( - drive: String, - rpcenv: &mut dyn RpcEnvironment, -) -> Result { +pub fn clean_drive(drive: String, rpcenv: &mut dyn RpcEnvironment) -> Result { let upid_str = run_drive_worker( rpcenv, drive.clone(), @@ -792,27 +777,27 @@ pub fn clean_drive( changer.clean_drive()?; - if let Ok(drive_config) = config.lookup::("lto", &drive) { - // Note: clean_drive unloads the cleaning media, so we cannot use drive_config.open - let mut handle = LtoTapeHandle::new(open_lto_tape_device(&drive_config.path)?)?; + if let Ok(drive_config) = config.lookup::("lto", &drive) { + // Note: clean_drive unloads the cleaning media, so we cannot use drive_config.open + let mut handle = LtoTapeHandle::new(open_lto_tape_device(&drive_config.path)?)?; - // test for critical tape alert flags - if let Ok(alert_flags) = handle.tape_alert_flags() { - if !alert_flags.is_empty() { - task_log!(worker, "TapeAlertFlags: {:?}", alert_flags); - if tape_alert_flags_critical(alert_flags) { - bail!("found critical tape alert flags: {:?}", alert_flags); - } - } - } + // test for critical tape alert flags + if let Ok(alert_flags) = handle.tape_alert_flags() { + if !alert_flags.is_empty() { + task_log!(worker, "TapeAlertFlags: {:?}", alert_flags); + if tape_alert_flags_critical(alert_flags) { + bail!("found critical tape alert flags: {:?}", alert_flags); + } + } + } - // test wearout (max. 50 mounts) - if let Ok(volume_stats) = handle.volume_statistics() { - task_log!(worker, "Volume mounts: {}", volume_stats.volume_mounts); - let wearout = volume_stats.volume_mounts * 2; // (*100.0/50.0); - task_log!(worker, "Cleaning tape wearout: {}%", wearout); - } - } + // test wearout (max. 50 mounts) + if let Ok(volume_stats) = handle.volume_statistics() { + task_log!(worker, "Volume mounts: {}", volume_stats.volume_mounts); + let wearout = volume_stats.volume_mounts * 2; // (*100.0/50.0); + task_log!(worker, "Cleaning tape wearout: {}%", wearout); + } + } task_log!(worker, "Drive cleaned successfully"); @@ -849,48 +834,43 @@ pub fn clean_drive( /// This method queries the changer to get a list of media labels. /// /// Note: This updates the media online status. -pub async fn inventory( - drive: String, -) -> Result, Error> { - run_drive_blocking_task( - drive.clone(), - "inventorize".to_string(), - move |config| { - let (mut changer, changer_name) = required_media_changer(&config, &drive)?; +pub async fn inventory(drive: String) -> Result, Error> { + run_drive_blocking_task(drive.clone(), "inventorize".to_string(), move |config| { + let (mut changer, changer_name) = required_media_changer(&config, &drive)?; - let label_text_list = changer.online_media_label_texts()?; + let label_text_list = changer.online_media_label_texts()?; - let state_path = Path::new(TAPE_STATUS_DIR); + let state_path = Path::new(TAPE_STATUS_DIR); - let mut inventory = Inventory::load(state_path)?; + let mut inventory = Inventory::load(state_path)?; - update_changer_online_status( - &config, - &mut inventory, - &changer_name, - &label_text_list, - )?; + update_changer_online_status(&config, &mut inventory, &changer_name, &label_text_list)?; - let mut list = Vec::new(); + let mut list = Vec::new(); - for label_text in label_text_list.iter() { - if label_text.starts_with("CLN") { - // skip cleaning unit - continue; - } - - let label_text = label_text.to_string(); - - if let Some(media_id) = inventory.find_media_by_label_text(&label_text) { - list.push(LabelUuidMap { label_text, uuid: Some(media_id.label.uuid.clone()) }); - } else { - list.push(LabelUuidMap { label_text, uuid: None }); - } + for label_text in label_text_list.iter() { + if label_text.starts_with("CLN") { + // skip cleaning unit + continue; } - Ok(list) + let label_text = label_text.to_string(); + + if let Some(media_id) = inventory.find_media_by_label_text(&label_text) { + list.push(LabelUuidMap { + label_text, + uuid: Some(media_id.label.uuid.clone()), + }); + } else { + list.push(LabelUuidMap { + label_text, + uuid: None, + }); + } } - ) + + Ok(list) + }) .await } @@ -955,7 +935,9 @@ pub fn update_inventory( let label_text = label_text.to_string(); - if !read_all_labels.unwrap_or(false) && inventory.find_media_by_label_text(&label_text).is_some() { + if !read_all_labels.unwrap_or(false) + && inventory.find_media_by_label_text(&label_text).is_some() + { task_log!(worker, "media '{}' already inventoried", label_text); continue; } @@ -968,19 +950,37 @@ pub fn update_inventory( let mut drive = open_drive(&config, &drive)?; match drive.read_label() { Err(err) => { - task_warn!(worker, "unable to read label form media '{}' - {}", label_text, err); + task_warn!( + worker, + "unable to read label form media '{}' - {}", + label_text, + err + ); } Ok((None, _)) => { task_log!(worker, "media '{}' is empty", label_text); } Ok((Some(media_id), _key_config)) => { if label_text != media_id.label.label_text { - task_warn!(worker, "label text mismatch ({} != {})", label_text, media_id.label.label_text); + task_warn!( + worker, + "label text mismatch ({} != {})", + label_text, + media_id.label.label_text + ); continue; } - task_log!(worker, "inventorize media '{}' with uuid '{}'", label_text, media_id.label.uuid); + task_log!( + worker, + "inventorize media '{}' with uuid '{}'", + label_text, + media_id.label.uuid + ); - if let Some(MediaSetLabel { ref pool, ref uuid, ..}) = media_id.media_set_label { + if let Some(MediaSetLabel { + ref pool, ref uuid, .. + }) = media_id.media_set_label + { let _pool_lock = lock_media_pool(state_path, pool)?; let _lock = lock_media_set(state_path, uuid, None)?; MediaCatalog::destroy_unrelated_catalog(state_path, &media_id)?; @@ -1001,7 +1001,6 @@ pub fn update_inventory( Ok(upid_str.into()) } - #[api( input: { properties: { @@ -1063,18 +1062,29 @@ fn barcode_label_media_worker( let mut inventory = Inventory::load(state_path)?; - update_changer_online_status(drive_config, &mut inventory, &changer_name, &label_text_list)?; + update_changer_online_status( + drive_config, + &mut inventory, + &changer_name, + &label_text_list, + )?; if label_text_list.is_empty() { bail!("changer device does not list any media labels"); } for label_text in label_text_list { - if label_text.starts_with("CLN") { continue; } + if label_text.starts_with("CLN") { + continue; + } inventory.reload()?; if inventory.find_media_by_label_text(&label_text).is_some() { - task_log!(worker, "media '{}' already inventoried (already labeled)", label_text); + task_log!( + worker, + "media '{}' already inventoried (already labeled)", + label_text + ); continue; } @@ -1090,13 +1100,21 @@ fn barcode_label_media_worker( match drive.read_next_file() { Ok(_reader) => { - task_log!(worker, "media '{}' is not empty (format it first)", label_text); + task_log!( + worker, + "media '{}' is not empty (format it first)", + label_text + ); continue; } - Err(BlockReadError::EndOfFile) => { /* EOF mark at BOT, assume tape is empty */ }, - Err(BlockReadError::EndOfStream) => { /* tape is empty */ }, + Err(BlockReadError::EndOfFile) => { /* EOF mark at BOT, assume tape is empty */ } + Err(BlockReadError::EndOfStream) => { /* tape is empty */ } Err(_err) => { - task_warn!(worker, "media '{}' read error (maybe not empty - format it first)", label_text); + task_warn!( + worker, + "media '{}' read error (maybe not empty - format it first)", + label_text + ); continue; } } @@ -1143,7 +1161,7 @@ pub async fn cartridge_memory(drive: String) -> Result, Error> let mut handle = open_lto_tape_drive(&drive_config)?; handle.cartridge_memory() - } + }, ) .await } @@ -1173,7 +1191,7 @@ pub async fn volume_statistics(drive: String) -> Result Result { let mut handle = LtoTapeHandle::new(file)?; handle.get_drive_and_media_status() - } + }, ) .await } @@ -1279,7 +1297,7 @@ pub fn catalog_media( ); } media_id - }, + } (None, _) => bail!("media is empty (no media label found)"), }; @@ -1296,14 +1314,17 @@ pub fn catalog_media( return Ok(()); } Some(ref set) => { - if set.uuid.as_ref() == [0u8;16] { // media is empty + if set.uuid.as_ref() == [0u8; 16] { + // media is empty task_log!(worker, "media is empty"); let _lock = lock_unassigned_media_pool(status_path)?; MediaCatalog::destroy(status_path, &media_id.label.uuid)?; inventory.store(media_id.clone(), false)?; return Ok(()); } - let encrypt_fingerprint = set.encryption_key_fingerprint.clone() + let encrypt_fingerprint = set + .encryption_key_fingerprint + .clone() .map(|fp| (fp, set.uuid.clone())); drive.set_encryption(encrypt_fingerprint)?; @@ -1327,7 +1348,7 @@ pub fn catalog_media( let media_set = inventory.compute_media_set_members(media_set_uuid)?; if fast_catalog_restore(&worker, &mut drive, &media_set, &media_id.label.uuid)? { - return Ok(()) + return Ok(()); } task_log!(worker, "no catalog found"); @@ -1339,7 +1360,14 @@ pub fn catalog_media( drive.read_label()?; // skip over labels - we already read them above let mut checked_chunks = HashMap::new(); - restore_media(worker, &mut drive, &media_id, None, &mut checked_chunks, verbose)?; + restore_media( + worker, + &mut drive, + &media_id, + None, + &mut checked_chunks, + verbose, + )?; Ok(()) }, @@ -1398,7 +1426,11 @@ pub fn list_drives( let info = lookup_device_identification(<o_drives, &drive.path); let state = get_tape_device_state(&config, &drive.name)?; - let entry = DriveListEntry { config: drive, info, state }; + let entry = DriveListEntry { + config: drive, + info, + state, + }; list.push(entry); } @@ -1409,90 +1441,38 @@ pub fn list_drives( pub const SUBDIRS: SubdirMap = &sorted!([ ( "barcode-label-media", - &Router::new() - .post(&API_METHOD_BARCODE_LABEL_MEDIA) - ), - ( - "catalog", - &Router::new() - .post(&API_METHOD_CATALOG_MEDIA) - ), - ( - "clean", - &Router::new() - .put(&API_METHOD_CLEAN_DRIVE) - ), - ( - "eject-media", - &Router::new() - .post(&API_METHOD_EJECT_MEDIA) + &Router::new().post(&API_METHOD_BARCODE_LABEL_MEDIA) ), + ("catalog", &Router::new().post(&API_METHOD_CATALOG_MEDIA)), + ("clean", &Router::new().put(&API_METHOD_CLEAN_DRIVE)), + ("eject-media", &Router::new().post(&API_METHOD_EJECT_MEDIA)), ( "format-media", - &Router::new() - .post(&API_METHOD_FORMAT_MEDIA) - ), - ( - "export-media", - &Router::new() - .put(&API_METHOD_EXPORT_MEDIA) + &Router::new().post(&API_METHOD_FORMAT_MEDIA) ), + ("export-media", &Router::new().put(&API_METHOD_EXPORT_MEDIA)), ( "inventory", &Router::new() .get(&API_METHOD_INVENTORY) .put(&API_METHOD_UPDATE_INVENTORY) ), - ( - "label-media", - &Router::new() - .post(&API_METHOD_LABEL_MEDIA) - ), - ( - "load-media", - &Router::new() - .post(&API_METHOD_LOAD_MEDIA) - ), - ( - "load-slot", - &Router::new() - .post(&API_METHOD_LOAD_SLOT) - ), + ("label-media", &Router::new().post(&API_METHOD_LABEL_MEDIA)), + ("load-media", &Router::new().post(&API_METHOD_LOAD_MEDIA)), + ("load-slot", &Router::new().post(&API_METHOD_LOAD_SLOT)), ( "cartridge-memory", - &Router::new() - .get(&API_METHOD_CARTRIDGE_MEMORY) + &Router::new().get(&API_METHOD_CARTRIDGE_MEMORY) ), ( "volume-statistics", - &Router::new() - .get(&API_METHOD_VOLUME_STATISTICS) - ), - ( - "read-label", - &Router::new() - .get(&API_METHOD_READ_LABEL) - ), - ( - "restore-key", - &Router::new() - .post(&API_METHOD_RESTORE_KEY) - ), - ( - "rewind", - &Router::new() - .post(&API_METHOD_REWIND) - ), - ( - "status", - &Router::new() - .get(&API_METHOD_STATUS) - ), - ( - "unload", - &Router::new() - .post(&API_METHOD_UNLOAD) + &Router::new().get(&API_METHOD_VOLUME_STATISTICS) ), + ("read-label", &Router::new().get(&API_METHOD_READ_LABEL)), + ("restore-key", &Router::new().post(&API_METHOD_RESTORE_KEY)), + ("rewind", &Router::new().post(&API_METHOD_REWIND)), + ("status", &Router::new().get(&API_METHOD_STATUS)), + ("unload", &Router::new().post(&API_METHOD_UNLOAD)), ]); const ITEM_ROUTER: Router = Router::new() diff --git a/src/api2/tape/media.rs b/src/api2/tape/media.rs index ed99374d..ea27dd61 100644 --- a/src/api2/tape/media.rs +++ b/src/api2/tape/media.rs @@ -1,30 +1,23 @@ -use std::path::Path; use std::collections::HashSet; +use std::path::Path; use anyhow::{bail, format_err, Error}; -use proxmox_router::{list_subdirs_api_method, Router, SubdirMap, RpcEnvironment, Permission}; +use proxmox_router::{list_subdirs_api_method, Permission, Router, RpcEnvironment, SubdirMap}; use proxmox_schema::api; use proxmox_uuid::Uuid; -use pbs_datastore::backup_info::BackupDir; use pbs_api_types::{ - MEDIA_POOL_NAME_SCHEMA, MEDIA_LABEL_SCHEMA, MEDIA_UUID_SCHEMA, CHANGER_NAME_SCHEMA, - VAULT_NAME_SCHEMA, Authid, MediaPoolConfig, MediaListEntry, MediaSetListEntry, - MediaStatus, MediaContentEntry, MediaContentListFilter, - PRIV_TAPE_AUDIT, + Authid, MediaContentEntry, MediaContentListFilter, MediaListEntry, MediaPoolConfig, + MediaSetListEntry, MediaStatus, CHANGER_NAME_SCHEMA, MEDIA_LABEL_SCHEMA, + MEDIA_POOL_NAME_SCHEMA, MEDIA_UUID_SCHEMA, PRIV_TAPE_AUDIT, VAULT_NAME_SCHEMA, }; use pbs_config::CachedUserInfo; +use pbs_datastore::backup_info::BackupDir; -use crate::{ - tape::{ - TAPE_STATUS_DIR, - Inventory, - MediaPool, - MediaCatalog, - media_catalog_snapshot_list, - changer::update_online_status, - }, +use crate::tape::{ + changer::update_online_status, media_catalog_snapshot_list, Inventory, MediaCatalog, MediaPool, + TAPE_STATUS_DIR, }; #[api( @@ -61,7 +54,7 @@ pub async fn list_media_sets( }; let privs = user_info.lookup_privs(&auth_id, &["tape", "pool", pool_name]); - if (privs & PRIV_TAPE_AUDIT) == 0 { + if (privs & PRIV_TAPE_AUDIT) == 0 { continue; } @@ -150,7 +143,8 @@ pub async fn list_media( } // test what catalog files we have MediaCatalog::media_with_catalogs(status_path) - }).await??; + }) + .await??; let mut list = Vec::new(); @@ -166,7 +160,7 @@ pub async fn list_media( } let privs = user_info.lookup_privs(&auth_id, &["tape", "pool", pool_name]); - if (privs & PRIV_TAPE_AUDIT) == 0 { + if (privs & PRIV_TAPE_AUDIT) == 0 { continue; } @@ -185,17 +179,14 @@ pub async fn list_media( for media in pool.list_media() { let expired = pool.media_is_expired(&media, current_time); - let media_set_uuid = media.media_set_label() - .map(|set| set.uuid.clone()); + let media_set_uuid = media.media_set_label().map(|set| set.uuid.clone()); - let seq_nr = media.media_set_label() - .map(|set| set.seq_nr); + let seq_nr = media.media_set_label().map(|set| set.seq_nr); - let media_set_name = media.media_set_label() - .map(|set| { - pool.generate_media_set_name(&set.uuid, config.template.clone()) - .unwrap_or_else(|_| set.uuid.to_string()) - }); + let media_set_name = media.media_set_label().map(|set| { + pool.generate_media_set_name(&set.uuid, config.template.clone()) + .unwrap_or_else(|_| set.uuid.to_string()) + }); let catalog_ok = if media.media_set_label().is_none() { // Media is empty, we need no catalog @@ -224,11 +215,9 @@ pub async fn list_media( let inventory = Inventory::load(status_path)?; let privs = user_info.lookup_privs(&auth_id, &["tape", "pool"]); - if (privs & PRIV_TAPE_AUDIT) != 0 { + if (privs & PRIV_TAPE_AUDIT) != 0 { if pool.is_none() { - for media_id in inventory.list_unassigned_media() { - let (mut status, location) = inventory.status_and_location(&media_id.label.uuid); if status == MediaStatus::Unknown { @@ -267,7 +256,7 @@ pub async fn list_media( } let privs = user_info.lookup_privs(&auth_id, &["tape", "pool", &media_set_label.pool]); - if (privs & PRIV_TAPE_AUDIT) == 0 { + if (privs & PRIV_TAPE_AUDIT) == 0 { continue; } @@ -289,10 +278,8 @@ pub async fn list_media( media_set_name: Some(media_set_name), seq_nr: Some(media_set_label.seq_nr), }); - } - Ok(list) } @@ -310,15 +297,12 @@ pub async fn list_media( }, )] /// Change Tape location to vault (if given), or offline. -pub fn move_tape( - label_text: String, - vault_name: Option, -) -> Result<(), Error> { - +pub fn move_tape(label_text: String, vault_name: Option) -> Result<(), Error> { let status_path = Path::new(TAPE_STATUS_DIR); let mut inventory = Inventory::load(status_path)?; - let uuid = inventory.find_media_by_label_text(&label_text) + let uuid = inventory + .find_media_by_label_text(&label_text) .ok_or_else(|| format_err!("no such media '{}'", label_text))? .label .uuid @@ -348,21 +332,24 @@ pub fn move_tape( }, )] /// Destroy media (completely remove from database) -pub fn destroy_media(label_text: String, force: Option,) -> Result<(), Error> { - +pub fn destroy_media(label_text: String, force: Option) -> Result<(), Error> { let force = force.unwrap_or(false); let status_path = Path::new(TAPE_STATUS_DIR); let mut inventory = Inventory::load(status_path)?; - let media_id = inventory.find_media_by_label_text(&label_text) + let media_id = inventory + .find_media_by_label_text(&label_text) .ok_or_else(|| format_err!("no such media '{}'", label_text))?; if !force { if let Some(ref set) = media_id.media_set_label { - let is_empty = set.uuid.as_ref() == [0u8;16]; + let is_empty = set.uuid.as_ref() == [0u8; 16]; if !is_empty { - bail!("media '{}' contains data (please use 'force' flag to remove.", label_text); + bail!( + "media '{}' contains data (please use 'force' flag to remove.", + label_text + ); } } } @@ -414,24 +401,32 @@ pub fn list_content( let set = media_id.media_set_label.as_ref().unwrap(); if let Some(ref label_text) = filter.label_text { - if &media_id.label.label_text != label_text { continue; } + if &media_id.label.label_text != label_text { + continue; + } } if let Some(ref pool) = filter.pool { - if &set.pool != pool { continue; } + if &set.pool != pool { + continue; + } } let privs = user_info.lookup_privs(&auth_id, &["tape", "pool", &set.pool]); - if (privs & PRIV_TAPE_AUDIT) == 0 { + if (privs & PRIV_TAPE_AUDIT) == 0 { continue; } if let Some(ref media_uuid) = filter.media { - if &media_id.label.uuid != media_uuid { continue; } + if &media_id.label.uuid != media_uuid { + continue; + } } if let Some(ref media_set_uuid) = filter.media_set { - if &set.uuid != media_set_uuid { continue; } + if &set.uuid != media_set_uuid { + continue; + } } let template = match config.lookup::("pool", &set.pool) { @@ -447,10 +442,14 @@ pub fn list_content( let backup_dir: BackupDir = snapshot.parse()?; if let Some(ref backup_type) = filter.backup_type { - if backup_dir.group().backup_type() != backup_type { continue; } + if backup_dir.group().backup_type() != backup_type { + continue; + } } if let Some(ref backup_id) = filter.backup_id { - if backup_dir.group().backup_id() != backup_id { continue; } + if backup_dir.group().backup_id() != backup_id { + continue; + } } list.push(MediaContentEntry { @@ -482,7 +481,6 @@ pub fn list_content( )] /// Get current media status pub fn get_media_status(uuid: Uuid) -> Result { - let status_path = Path::new(TAPE_STATUS_DIR); let inventory = Inventory::load(status_path)?; @@ -509,7 +507,6 @@ pub fn get_media_status(uuid: Uuid) -> Result { /// It is not allowed to set status to 'writable' or 'unknown' (those /// are internally managed states). pub fn update_media_status(uuid: Uuid, status: Option) -> Result<(), Error> { - let status_path = Path::new(TAPE_STATUS_DIR); let mut inventory = Inventory::load(status_path)?; @@ -524,14 +521,12 @@ pub fn update_media_status(uuid: Uuid, status: Option) -> Result<() Ok(()) } -const MEDIA_SUBDIRS: SubdirMap = &[ - ( - "status", - &Router::new() - .get(&API_METHOD_GET_MEDIA_STATUS) - .post(&API_METHOD_UPDATE_MEDIA_STATUS) - ), -]; +const MEDIA_SUBDIRS: SubdirMap = &[( + "status", + &Router::new() + .get(&API_METHOD_GET_MEDIA_STATUS) + .post(&API_METHOD_UPDATE_MEDIA_STATUS), +)]; pub const MEDIA_ROUTER: Router = Router::new() .get(&list_subdirs_api_method!(MEDIA_SUBDIRS)) @@ -542,30 +537,16 @@ pub const MEDIA_LIST_ROUTER: Router = Router::new() .match_all("uuid", &MEDIA_ROUTER); const SUBDIRS: SubdirMap = &[ - ( - "content", - &Router::new() - .get(&API_METHOD_LIST_CONTENT) - ), - ( - "destroy", - &Router::new() - .get(&API_METHOD_DESTROY_MEDIA) - ), - ( "list", &MEDIA_LIST_ROUTER ), + ("content", &Router::new().get(&API_METHOD_LIST_CONTENT)), + ("destroy", &Router::new().get(&API_METHOD_DESTROY_MEDIA)), + ("list", &MEDIA_LIST_ROUTER), ( "media-sets", - &Router::new() - .get(&API_METHOD_LIST_MEDIA_SETS) - ), - ( - "move", - &Router::new() - .post(&API_METHOD_MOVE_TAPE) + &Router::new().get(&API_METHOD_LIST_MEDIA_SETS), ), + ("move", &Router::new().post(&API_METHOD_MOVE_TAPE)), ]; - pub const ROUTER: Router = Router::new() .get(&list_subdirs_api_method!(SUBDIRS)) .subdirs(SUBDIRS); diff --git a/src/api2/tape/mod.rs b/src/api2/tape/mod.rs index 9b569fd5..57700a8e 100644 --- a/src/api2/tape/mod.rs +++ b/src/api2/tape/mod.rs @@ -3,16 +3,16 @@ use anyhow::Error; use serde_json::Value; -use proxmox_schema::api; use proxmox_router::{list_subdirs_api_method, Router, SubdirMap}; +use proxmox_schema::api; use pbs_api_types::TapeDeviceInfo; -use pbs_tape::linux_list_drives::{lto_tape_device_list, linux_tape_changer_list}; +use pbs_tape::linux_list_drives::{linux_tape_changer_list, lto_tape_device_list}; -pub mod drive; -pub mod changer; -pub mod media; pub mod backup; +pub mod changer; +pub mod drive; +pub mod media; pub mod restore; #[api( @@ -29,7 +29,6 @@ pub mod restore; )] /// Scan tape drives pub fn scan_drives(_param: Value) -> Result, Error> { - let list = lto_tape_device_list(); Ok(list) @@ -49,7 +48,6 @@ pub fn scan_drives(_param: Value) -> Result, Error> { )] /// Scan for SCSI tape changers pub fn scan_changers(_param: Value) -> Result, Error> { - let list = linux_tape_changer_list(); Ok(list) @@ -63,14 +61,9 @@ const SUBDIRS: SubdirMap = &[ ("restore", &restore::ROUTER), ( "scan-changers", - &Router::new() - .get(&API_METHOD_SCAN_CHANGERS), - ), - ( - "scan-drives", - &Router::new() - .get(&API_METHOD_SCAN_DRIVES), + &Router::new().get(&API_METHOD_SCAN_CHANGERS), ), + ("scan-drives", &Router::new().get(&API_METHOD_SCAN_DRIVES)), ]; pub const ROUTER: Router = Router::new() diff --git a/src/api2/tape/restore.rs b/src/api2/tape/restore.rs index 2ce16c9d..3008aec8 100644 --- a/src/api2/tape/restore.rs +++ b/src/api2/tape/restore.rs @@ -1,71 +1,53 @@ -use std::path::{Path, PathBuf}; -use std::ffi::OsStr; -use std::collections::{HashMap, HashSet, BTreeMap}; +use std::collections::{BTreeMap, HashMap, HashSet}; use std::convert::TryFrom; +use std::ffi::OsStr; use std::io::{Seek, SeekFrom}; +use std::path::{Path, PathBuf}; use std::sync::Arc; use anyhow::{bail, format_err, Error}; use serde_json::Value; -use proxmox_sys::fs::{replace_file, CreateOptions}; use proxmox_io::ReadExt; use proxmox_router::{Permission, Router, RpcEnvironment, RpcEnvironmentType}; use proxmox_schema::api; use proxmox_section_config::SectionConfigData; -use proxmox_uuid::Uuid; +use proxmox_sys::fs::{replace_file, CreateOptions}; use proxmox_sys::{task_log, task_warn, WorkerTaskContext}; +use proxmox_uuid::Uuid; use pbs_api_types::{ - Authid, Userid, CryptMode, - DATASTORE_MAP_ARRAY_SCHEMA, DATASTORE_MAP_LIST_SCHEMA, DRIVE_NAME_SCHEMA, - UPID_SCHEMA, TAPE_RESTORE_SNAPSHOT_SCHEMA, - PRIV_DATASTORE_BACKUP, PRIV_DATASTORE_MODIFY, PRIV_TAPE_READ, + Authid, CryptMode, Userid, DATASTORE_MAP_ARRAY_SCHEMA, DATASTORE_MAP_LIST_SCHEMA, + DRIVE_NAME_SCHEMA, PRIV_DATASTORE_BACKUP, PRIV_DATASTORE_MODIFY, PRIV_TAPE_READ, + TAPE_RESTORE_SNAPSHOT_SCHEMA, UPID_SCHEMA, }; -use pbs_datastore::{DataStore, DataBlob}; +use pbs_config::CachedUserInfo; use pbs_datastore::backup_info::BackupDir; use pbs_datastore::dynamic_index::DynamicIndexReader; use pbs_datastore::fixed_index::FixedIndexReader; use pbs_datastore::index::IndexFile; use pbs_datastore::manifest::{archive_type, ArchiveType, BackupManifest, MANIFEST_BLOB_NAME}; -use pbs_config::CachedUserInfo; +use pbs_datastore::{DataBlob, DataStore}; use pbs_tape::{ - TapeRead, BlockReadError, MediaContentHeader, - PROXMOX_BACKUP_CONTENT_HEADER_MAGIC_1_0, + BlockReadError, MediaContentHeader, TapeRead, PROXMOX_BACKUP_CONTENT_HEADER_MAGIC_1_0, }; use proxmox_rest_server::WorkerTask; use crate::{ - tools::parallel_handler::ParallelHandler, server::lookup_user_email, tape::{ - TAPE_STATUS_DIR, - MediaId, - MediaSet, - MediaCatalog, - MediaSetCatalog, - Inventory, - lock_media_set, + drive::{lock_tape_device, request_and_load_media, set_tape_device_state, TapeDriver}, file_formats::{ - PROXMOX_BACKUP_MEDIA_LABEL_MAGIC_1_0, - PROXMOX_BACKUP_SNAPSHOT_ARCHIVE_MAGIC_1_0, + CatalogArchiveHeader, ChunkArchiveDecoder, ChunkArchiveHeader, SnapshotArchiveHeader, + PROXMOX_BACKUP_CATALOG_ARCHIVE_MAGIC_1_0, PROXMOX_BACKUP_CHUNK_ARCHIVE_MAGIC_1_0, + PROXMOX_BACKUP_CHUNK_ARCHIVE_MAGIC_1_1, PROXMOX_BACKUP_MEDIA_LABEL_MAGIC_1_0, + PROXMOX_BACKUP_MEDIA_SET_LABEL_MAGIC_1_0, PROXMOX_BACKUP_SNAPSHOT_ARCHIVE_MAGIC_1_0, PROXMOX_BACKUP_SNAPSHOT_ARCHIVE_MAGIC_1_1, - PROXMOX_BACKUP_MEDIA_SET_LABEL_MAGIC_1_0, - PROXMOX_BACKUP_CHUNK_ARCHIVE_MAGIC_1_0, - PROXMOX_BACKUP_CHUNK_ARCHIVE_MAGIC_1_1, - PROXMOX_BACKUP_CATALOG_ARCHIVE_MAGIC_1_0, - ChunkArchiveHeader, - ChunkArchiveDecoder, - SnapshotArchiveHeader, - CatalogArchiveHeader, - }, - drive::{ - TapeDriver, - request_and_load_media, - lock_tape_device, - set_tape_device_state, }, + lock_media_set, Inventory, MediaCatalog, MediaId, MediaSet, MediaSetCatalog, + TAPE_STATUS_DIR, }, + tools::parallel_handler::ParallelHandler, }; const RESTORE_TMP_DIR: &str = "/var/tmp/proxmox-backup"; @@ -306,16 +288,11 @@ pub fn restore( } if let Err(err) = set_tape_device_state(&drive, "") { - task_log!( - worker, - "could not unset drive state for {}: {}", - drive, - err - ); + task_log!(worker, "could not unset drive state for {}: {}", drive, err); } res - } + }, )?; Ok(upid_str.into()) @@ -342,12 +319,19 @@ fn restore_full_worker( for (seq_nr, media_uuid) in media_list.iter().enumerate() { match media_uuid { None => { - bail!("media set {} is incomplete (missing member {}).", media_set_uuid, seq_nr); + bail!( + "media set {} is incomplete (missing member {}).", + media_set_uuid, + seq_nr + ); } Some(media_uuid) => { let media_id = inventory.lookup_media(media_uuid).unwrap(); - if let Some(ref set) = media_id.media_set_label { // always true here - if encryption_key_fingerprint.is_none() && set.encryption_key_fingerprint.is_some() { + if let Some(ref set) = media_id.media_set_label { + // always true here + if encryption_key_fingerprint.is_none() + && set.encryption_key_fingerprint.is_some() + { encryption_key_fingerprint = set.encryption_key_fingerprint.clone(); } } @@ -364,21 +348,22 @@ fn restore_full_worker( worker, "Datastore(s): {}", store_map - .used_datastores() - .into_iter() - .map(String::from) - .collect::>() - .join(", "), + .used_datastores() + .into_iter() + .map(String::from) + .collect::>() + .join(", "), ); task_log!(worker, "Drive: {}", drive_name); task_log!( worker, "Required media list: {}", - media_id_list.iter() - .map(|media_id| media_id.label.label_text.as_str()) - .collect::>() - .join(";") + media_id_list + .iter() + .map(|media_id| media_id.label.label_text.as_str()) + .collect::>() + .join(";") ); let mut datastore_locks = Vec::new(); @@ -529,11 +514,13 @@ fn restore_list_worker( &info, &media_set_uuid, &mut datastore_chunk_map, - ).map_err(|err| format_err!("could not restore snapshots to tmpdir: {}", err))?; + ) + .map_err(|err| format_err!("could not restore snapshots to tmpdir: {}", err))?; } // sorted media_uuid => (sorted file_num => (set of digests))) - let mut media_file_chunk_map: BTreeMap>> = BTreeMap::new(); + let mut media_file_chunk_map: BTreeMap>> = + BTreeMap::new(); for (source_datastore, chunks) in datastore_chunk_map.into_iter() { let datastore = store_map.get_datastore(&source_datastore).ok_or_else(|| { @@ -546,7 +533,9 @@ fn restore_list_worker( // we only want to restore chunks that we do not have yet if !datastore.cond_touch_chunk(&digest, false)? { if let Some((uuid, nr)) = catalog.lookup_chunk(&source_datastore, &digest) { - let file = media_file_chunk_map.entry(uuid.clone()).or_insert_with(BTreeMap::new); + let file = media_file_chunk_map + .entry(uuid.clone()) + .or_insert_with(BTreeMap::new); let chunks = file.entry(nr).or_insert_with(HashSet::new); chunks.insert(digest); } @@ -590,9 +579,9 @@ fn restore_list_worker( .ok_or_else(|| format_err!("invalid snapshot:{}", store_snapshot))?; let backup_dir: BackupDir = snapshot.parse()?; - let datastore = store_map - .get_datastore(source_datastore) - .ok_or_else(|| format_err!("unexpected source datastore: {}", source_datastore))?; + let datastore = store_map.get_datastore(source_datastore).ok_or_else(|| { + format_err!("unexpected source datastore: {}", source_datastore) + })?; let mut tmp_path = base_path.clone(); tmp_path.push(&source_datastore); @@ -608,13 +597,17 @@ fn restore_list_worker( } task_log!(worker, "Restore snapshot '{}' done", snapshot); Ok(()) - }).map_err(|err: Error| format_err!("could not copy {}: {}", store_snapshot, err))?; + }) + .map_err(|err: Error| format_err!("could not copy {}: {}", store_snapshot, err))?; } Ok(()) }); if res.is_err() { - task_warn!(worker, "Error during restore, partially restored snapshots will NOT be cleaned up"); + task_warn!( + worker, + "Error during restore, partially restored snapshots will NOT be cleaned up" + ); } match std::fs::remove_dir_all(&base_path) { @@ -693,7 +686,12 @@ fn restore_snapshots_to_tmpdir( for file_num in file_list { let current_file_number = drive.current_file_number()?; if current_file_number != *file_num { - task_log!(worker, "was at file {}, moving to {}", current_file_number, file_num); + task_log!( + worker, + "was at file {}, moving to {}", + current_file_number, + file_num + ); drive.move_to_file(*file_num)?; let current_file_number = drive.current_file_number()?; task_log!(worker, "now at file {}", current_file_number); @@ -735,7 +733,8 @@ fn restore_snapshots_to_tmpdir( let chunks = chunks_list .entry(source_datastore) .or_insert_with(HashSet::new); - let manifest = try_restore_snapshot_archive(worker.clone(), &mut decoder, &tmp_path)?; + let manifest = + try_restore_snapshot_archive(worker.clone(), &mut decoder, &tmp_path)?; for item in manifest.files() { let mut archive_path = tmp_path.to_owned(); archive_path.push(&item.filename); @@ -744,9 +743,7 @@ fn restore_snapshots_to_tmpdir( ArchiveType::DynamicIndex => { Box::new(DynamicIndexReader::open(&archive_path)?) } - ArchiveType::FixedIndex => { - Box::new(FixedIndexReader::open(&archive_path)?) - } + ArchiveType::FixedIndex => Box::new(FixedIndexReader::open(&archive_path)?), ArchiveType::Blob => continue, }; for i in 0..index.index_count() { @@ -772,7 +769,12 @@ fn restore_file_chunk_map( for (nr, chunk_map) in file_chunk_map.iter_mut() { let current_file_number = drive.current_file_number()?; if current_file_number != *nr { - task_log!(worker, "was at file {}, moving to {}", current_file_number, nr); + task_log!( + worker, + "was at file {}, moving to {}", + current_file_number, + nr + ); drive.move_to_file(*nr)?; let current_file_number = drive.current_file_number()?; task_log!(worker, "now at file {}", current_file_number); @@ -803,7 +805,12 @@ fn restore_file_chunk_map( format_err!("unexpected chunk archive for store: {}", source_datastore) })?; - let count = restore_partial_chunk_archive(worker.clone(), reader, datastore.clone(), chunk_map)?; + let count = restore_partial_chunk_archive( + worker.clone(), + reader, + datastore.clone(), + chunk_map, + )?; task_log!(worker, "restored {} chunks", count); } _ => bail!("unexpected content magic {:?}", header.content_magic), @@ -882,7 +889,6 @@ fn restore_partial_chunk_archive<'a>( Ok(count) } - /// Request and restore complete media without using existing catalog (create catalog instead) pub fn request_and_restore_media( worker: Arc, @@ -890,7 +896,7 @@ pub fn request_and_restore_media( drive_config: &SectionConfigData, drive_name: &str, store_map: &DataStoreMap, - checked_chunks_map: &mut HashMap>, + checked_chunks_map: &mut HashMap>, restore_owner: &Authid, email: &Option, ) -> Result<(), Error> { @@ -899,20 +905,29 @@ pub fn request_and_restore_media( Some(ref set) => &set.uuid, }; - let (mut drive, info) = request_and_load_media(&worker, drive_config, drive_name, &media_id.label, email)?; + let (mut drive, info) = + request_and_load_media(&worker, drive_config, drive_name, &media_id.label, email)?; match info.media_set_label { None => { - bail!("missing media set label on media {} ({})", - media_id.label.label_text, media_id.label.uuid); + bail!( + "missing media set label on media {} ({})", + media_id.label.label_text, + media_id.label.uuid + ); } Some(ref set) => { if &set.uuid != media_set_uuid { - bail!("wrong media set label on media {} ({} != {})", - media_id.label.label_text, media_id.label.uuid, - media_set_uuid); + bail!( + "wrong media set label on media {} ({} != {})", + media_id.label.label_text, + media_id.label.uuid, + media_set_uuid + ); } - let encrypt_fingerprint = set.encryption_key_fingerprint.clone() + let encrypt_fingerprint = set + .encryption_key_fingerprint + .clone() .map(|fp| (fp, set.uuid.clone())); drive.set_encryption(encrypt_fingerprint)?; @@ -937,10 +952,9 @@ pub fn restore_media( drive: &mut Box, media_id: &MediaId, target: Option<(&DataStoreMap, &Authid)>, - checked_chunks_map: &mut HashMap>, + checked_chunks_map: &mut HashMap>, verbose: bool, -) -> Result<(), Error> { - +) -> Result<(), Error> { let status_path = Path::new(TAPE_STATUS_DIR); let mut catalog = MediaCatalog::create_temporary_database(status_path, media_id, false)?; @@ -948,7 +962,11 @@ pub fn restore_media( let current_file_number = drive.current_file_number()?; let reader = match drive.read_next_file() { Err(BlockReadError::EndOfFile) => { - task_log!(worker, "skip unexpected filemark at pos {}", current_file_number); + task_log!( + worker, + "skip unexpected filemark at pos {}", + current_file_number + ); continue; } Err(BlockReadError::EndOfStream) => { @@ -961,7 +979,15 @@ pub fn restore_media( Ok(reader) => reader, }; - restore_archive(worker.clone(), reader, current_file_number, target, &mut catalog, checked_chunks_map, verbose)?; + restore_archive( + worker.clone(), + reader, + current_file_number, + target, + &mut catalog, + checked_chunks_map, + verbose, + )?; } catalog.commit()?; @@ -977,7 +1003,7 @@ fn restore_archive<'a>( current_file_number: u64, target: Option<(&DataStoreMap, &Authid)>, catalog: &mut MediaCatalog, - checked_chunks_map: &mut HashMap>, + checked_chunks_map: &mut HashMap>, verbose: bool, ) -> Result<(), Error> { let header: MediaContentHeader = unsafe { reader.read_le_value()? }; @@ -1003,7 +1029,13 @@ fn restore_archive<'a>( let datastore_name = archive_header.store; let snapshot = archive_header.snapshot; - task_log!(worker, "File {}: snapshot archive {}:{}", current_file_number, datastore_name, snapshot); + task_log!( + worker, + "File {}: snapshot archive {}:{}", + current_file_number, + datastore_name, + snapshot + ); let backup_dir: BackupDir = snapshot.parse()?; @@ -1057,7 +1089,12 @@ fn restore_archive<'a>( reader.skip_data()?; // read all data if let Ok(false) = reader.is_incomplete() { - catalog.register_snapshot(Uuid::from(header.uuid), current_file_number, &datastore_name, &snapshot)?; + catalog.register_snapshot( + Uuid::from(header.uuid), + current_file_number, + &datastore_name, + &snapshot, + )?; catalog.commit_if_large()?; } } @@ -1072,18 +1109,35 @@ fn restore_archive<'a>( let source_datastore = archive_header.store; - task_log!(worker, "File {}: chunk archive for datastore '{}'", current_file_number, source_datastore); + task_log!( + worker, + "File {}: chunk archive for datastore '{}'", + current_file_number, + source_datastore + ); let datastore = target .as_ref() .and_then(|t| t.0.get_datastore(&source_datastore)); if datastore.is_some() || target.is_none() { let checked_chunks = checked_chunks_map - .entry(datastore.as_ref().map(|d| d.name()).unwrap_or("_unused_").to_string()) + .entry( + datastore + .as_ref() + .map(|d| d.name()) + .unwrap_or("_unused_") + .to_string(), + ) .or_insert(HashSet::new()); let chunks = if let Some(datastore) = datastore { - restore_chunk_archive(worker.clone(), reader, datastore, checked_chunks, verbose)? + restore_chunk_archive( + worker.clone(), + reader, + datastore, + checked_chunks, + verbose, + )? } else { scan_chunk_archive(worker.clone(), reader, verbose)? }; @@ -1111,11 +1165,16 @@ fn restore_archive<'a>( let archive_header: CatalogArchiveHeader = serde_json::from_slice(&header_data) .map_err(|err| format_err!("unable to parse catalog archive header - {}", err))?; - task_log!(worker, "File {}: skip catalog '{}'", current_file_number, archive_header.uuid); + task_log!( + worker, + "File {}: skip catalog '{}'", + current_file_number, + archive_header.uuid + ); reader.skip_data()?; // read all data } - _ => bail!("unknown content magic {:?}", header.content_magic), + _ => bail!("unknown content magic {:?}", header.content_magic), } Ok(()) @@ -1126,8 +1185,7 @@ fn scan_chunk_archive<'a>( worker: Arc, reader: Box, verbose: bool, -) -> Result>, Error> { - +) -> Result>, Error> { let mut chunks = Vec::new(); let mut decoder = ChunkArchiveDecoder::new(reader); @@ -1171,10 +1229,9 @@ fn restore_chunk_archive<'a>( worker: Arc, reader: Box, datastore: Arc, - checked_chunks: &mut HashSet<[u8;32]>, + checked_chunks: &mut HashSet<[u8; 32]>, verbose: bool, -) -> Result>, Error> { - +) -> Result>, Error> { let mut chunks = Vec::new(); let mut decoder = ChunkArchiveDecoder::new(reader); @@ -1211,7 +1268,6 @@ fn restore_chunk_archive<'a>( let verify_and_write_channel = writer_pool.channel(); - loop { let (digest, blob) = match decoder.next_chunk() { Ok(Some((digest, blob))) => (digest, blob), @@ -1267,7 +1323,6 @@ fn restore_snapshot_archive<'a>( reader: Box, snapshot_path: &Path, ) -> Result { - let mut decoder = pxar::decoder::sync::Decoder::from_std(reader)?; match try_restore_snapshot_archive(worker, &mut decoder, snapshot_path) { Ok(_) => Ok(true), @@ -1295,7 +1350,6 @@ fn try_restore_snapshot_archive( decoder: &mut pxar::decoder::sync::Decoder, snapshot_path: &Path, ) -> Result { - let _root = match decoder.next() { None => bail!("missing root entry"), Some(root) => { @@ -1348,12 +1402,12 @@ fn try_restore_snapshot_archive( tmp_path.set_extension("tmp"); if filename == manifest_file_name { - let blob = DataBlob::load_from_reader(&mut contents)?; let mut old_manifest = BackupManifest::try_from(blob)?; // Remove verify_state to indicate that this snapshot is not verified - old_manifest.unprotected + old_manifest + .unprotected .as_object_mut() .map(|m| m.remove("verify_state")); @@ -1394,7 +1448,11 @@ fn try_restore_snapshot_archive( tmp_manifest_path.set_extension("tmp"); if let Err(err) = std::fs::rename(&tmp_manifest_path, &manifest_path) { - bail!("Atomic rename manifest {:?} failed - {}", manifest_path, err); + bail!( + "Atomic rename manifest {:?} failed - {}", + manifest_path, + err + ); } Ok(manifest) @@ -1406,8 +1464,7 @@ pub fn fast_catalog_restore( drive: &mut Box, media_set: &MediaSet, uuid: &Uuid, // current media Uuid -) -> Result { - +) -> Result { let status_path = Path::new(TAPE_STATUS_DIR); let current_file_number = drive.current_file_number()?; @@ -1422,10 +1479,15 @@ pub fn fast_catalog_restore( loop { let current_file_number = drive.current_file_number()?; - { // limit reader scope + { + // limit reader scope let mut reader = match drive.read_next_file() { Err(BlockReadError::EndOfFile) => { - task_log!(worker, "skip unexpected filemark at pos {}", current_file_number); + task_log!( + worker, + "skip unexpected filemark at pos {}", + current_file_number + ); continue; } Err(BlockReadError::EndOfStream) => { @@ -1449,10 +1511,16 @@ pub fn fast_catalog_restore( let header_data = reader.read_exact_allocated(header.size as usize)?; let archive_header: CatalogArchiveHeader = serde_json::from_slice(&header_data) - .map_err(|err| format_err!("unable to parse catalog archive header - {}", err))?; + .map_err(|err| { + format_err!("unable to parse catalog archive header - {}", err) + })?; if &archive_header.media_set_uuid != media_set.uuid() { - task_log!(worker, "skipping unrelated catalog at pos {}", current_file_number); + task_log!( + worker, + "skipping unrelated catalog at pos {}", + current_file_number + ); reader.skip_data()?; // read all data continue; } @@ -1462,16 +1530,18 @@ pub fn fast_catalog_restore( let wanted = media_set .media_list() .iter() - .find(|e| { - match e { - None => false, - Some(uuid) => uuid == catalog_uuid, - } + .find(|e| match e { + None => false, + Some(uuid) => uuid == catalog_uuid, }) .is_some(); if !wanted { - task_log!(worker, "skip catalog because media '{}' not inventarized", catalog_uuid); + task_log!( + worker, + "skip catalog because media '{}' not inventarized", + catalog_uuid + ); reader.skip_data()?; // read all data continue; } @@ -1481,13 +1551,18 @@ pub fn fast_catalog_restore( } else { // only restore if catalog does not exist if MediaCatalog::exists(status_path, catalog_uuid) { - task_log!(worker, "catalog for media '{}' already exists", catalog_uuid); + task_log!( + worker, + "catalog for media '{}' already exists", + catalog_uuid + ); reader.skip_data()?; // read all data continue; } } - let mut file = MediaCatalog::create_temporary_database_file(status_path, catalog_uuid)?; + let mut file = + MediaCatalog::create_temporary_database_file(status_path, catalog_uuid)?; std::io::copy(&mut reader, &mut file)?; @@ -1496,11 +1571,19 @@ pub fn fast_catalog_restore( match MediaCatalog::parse_catalog_header(&mut file)? { (true, Some(media_uuid), Some(media_set_uuid)) => { if &media_uuid != catalog_uuid { - task_log!(worker, "catalog uuid missmatch at pos {}", current_file_number); + task_log!( + worker, + "catalog uuid missmatch at pos {}", + current_file_number + ); continue; } if media_set_uuid != archive_header.media_set_uuid { - task_log!(worker, "catalog media_set missmatch at pos {}", current_file_number); + task_log!( + worker, + "catalog media_set missmatch at pos {}", + current_file_number + ); continue; } @@ -1510,7 +1593,11 @@ pub fn fast_catalog_restore( task_log!(worker, "successfully restored catalog"); found_catalog = true } else { - task_log!(worker, "successfully restored related catalog {}", media_uuid); + task_log!( + worker, + "successfully restored related catalog {}", + media_uuid + ); } } _ => {