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::{sortable, identity}; 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::{task_log, task_warn}; 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, }; use pbs_api_types::{PRIV_TAPE_AUDIT, PRIV_TAPE_READ, PRIV_TAPE_WRITE}; use pbs_config::CachedUserInfo; use pbs_tape::{ BlockReadError, sg_tape::tape_alert_flags_critical, linux_list_drives::{lto_tape_device_list, lookup_device_identification, open_lto_tape_device}, }; use proxmox_rest_server::WorkerTask; use crate::{ 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, }, }; fn run_drive_worker( rpcenv: &dyn RpcEnvironment, drive: String, worker_type: &str, job_id: Option, f: F, ) -> Result where F: Send + UnwindSafe + 'static + FnOnce(Arc, SectionConfigData) -> Result<(), Error>, { // early check/lock before starting worker let (config, _digest) = pbs_config::drive::config()?; let lock_guard = lock_tape_device(&config, &drive)?; let auth_id = rpcenv.get_auth_id().unwrap(); let to_stdout = rpcenv.env_type() == RpcEnvironmentType::CLI; WorkerTask::new_thread(worker_type, job_id, auth_id, to_stdout, move |worker| { let _lock_guard = lock_guard; set_tape_device_state(&drive, &worker.upid().to_string()) .map_err(|err| format_err!("could not set tape device state: {}", err))?; let result = f(worker, config); set_tape_device_state(&drive, "") .map_err(|err| format_err!("could not unset tape device state: {}", err))?; result }) } async fn run_drive_blocking_task(drive: String, state: String, f: F) -> Result where F: Send + 'static + FnOnce(SectionConfigData) -> Result, R: Send + 'static, { // early check/lock before starting worker let (config, _digest) = pbs_config::drive::config()?; let lock_guard = lock_tape_device(&config, &drive)?; tokio::task::spawn_blocking(move || { let _lock_guard = lock_guard; set_tape_device_state(&drive, &state) .map_err(|err| format_err!("could not set tape device state: {}", err))?; let result = f(config); set_tape_device_state(&drive, "") .map_err(|err| format_err!("could not unset tape device state: {}", err))?; result }) .await? } #[api( input: { properties: { drive: { schema: DRIVE_NAME_SCHEMA, }, "label-text": { schema: MEDIA_LABEL_SCHEMA, }, }, }, returns: { schema: UPID_SCHEMA, }, access: { permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_READ, false), }, )] /// Load media with specified label /// /// Issue a media load request to the associated changer device. pub fn load_media( drive: String, label_text: String, rpcenv: &mut dyn RpcEnvironment, ) -> Result { let job_id = format!("{}:{}", drive, label_text); let upid_str = run_drive_worker( rpcenv, drive.clone(), "load-media", Some(job_id), move |worker, config| { task_log!(worker, "loading media '{}' into drive '{}'", label_text, drive); let (mut changer, _) = required_media_changer(&config, &drive)?; changer.load_media(&label_text)?; Ok(()) }, )?; Ok(upid_str.into()) } #[api( input: { properties: { drive: { schema: DRIVE_NAME_SCHEMA, }, "source-slot": { description: "Source slot number.", minimum: 1, }, }, }, access: { permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_READ, false), }, )] /// Load media from the specified slot /// /// Issue a media load request to the associated changer device. pub async fn load_slot(drive: String, source_slot: u64) -> Result<(), Error> { run_drive_blocking_task( drive.clone(), format!("load from slot {}", source_slot), move |config| { let (mut changer, _) = required_media_changer(&config, &drive)?; changer.load_media_from_slot(source_slot)?; Ok(()) }, ) .await } #[api( input: { properties: { drive: { schema: DRIVE_NAME_SCHEMA, }, "label-text": { schema: MEDIA_LABEL_SCHEMA, }, }, }, returns: { description: "The import-export slot number the media was transferred to.", type: u64, minimum: 1, }, access: { permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_READ, false), }, )] /// Export media with specified label pub async fn export_media(drive: String, label_text: String) -> Result { run_drive_blocking_task( drive.clone(), format!("export media {}", label_text), move |config| { let (mut changer, changer_name) = required_media_changer(&config, &drive)?; match changer.export_media(&label_text)? { Some(slot) => Ok(slot), None => bail!( "media '{}' is not online (via changer '{}')", label_text, changer_name ), } } ) .await } #[api( input: { properties: { drive: { schema: DRIVE_NAME_SCHEMA, }, "target-slot": { description: "Target slot number. If omitted, defaults to the slot that the drive was loaded from.", minimum: 1, optional: true, }, }, }, returns: { schema: UPID_SCHEMA, }, access: { permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_READ, false), }, )] /// Unload media via changer pub fn unload( drive: String, target_slot: Option, rpcenv: &mut dyn RpcEnvironment, ) -> Result { let upid_str = run_drive_worker( rpcenv, drive.clone(), "unload-media", Some(drive.clone()), move |worker, config| { task_log!(worker, "unloading media from drive '{}'", drive); let (mut changer, _) = required_media_changer(&config, &drive)?; changer.unload_media(target_slot)?; Ok(()) }, )?; Ok(upid_str.into()) } #[api( input: { properties: { drive: { schema: DRIVE_NAME_SCHEMA, }, fast: { description: "Use fast erase.", type: bool, optional: true, default: true, }, "label-text": { schema: MEDIA_LABEL_SCHEMA, optional: true, }, }, }, returns: { schema: UPID_SCHEMA, }, access: { permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_WRITE, false), }, )] /// Format media. Check for label-text if given (cancels if wrong media). pub fn format_media( drive: String, fast: Option, label_text: Option, rpcenv: &mut dyn RpcEnvironment, ) -> Result { let upid_str = run_drive_worker( rpcenv, drive.clone(), "format-media", Some(drive.clone()), move |worker, config| { if let Some(ref label) = label_text { task_log!(worker, "try to load media '{}'", label); if let Some((mut changer, _)) = media_changer(&config, &drive)? { changer.load_media(label)?; } } let mut handle = open_drive(&config, &drive)?; match handle.read_label() { Err(err) => { if let Some(label) = label_text { bail!("expected label '{}', found unrelated data", label); } /* assume drive contains no or unrelated data */ task_log!(worker, "unable to read media label: {}", err); task_log!(worker, "format anyways"); handle.format_media(fast.unwrap_or(true))?; } Ok((None, _)) => { if let Some(label) = label_text { bail!("expected label '{}', found empty tape", label); } task_log!(worker, "found empty media - format anyways"); handle.format_media(fast.unwrap_or(true))?; } Ok((Some(media_id), _key_config)) => { if let Some(label_text) = label_text { if media_id.label.label_text != label_text { bail!( "expected label '{}', found '{}', aborting", label_text, media_id.label.label_text ); } } task_log!( worker, "found media '{}' with uuid '{}'", media_id.label.label_text, media_id.label.uuid, ); let status_path = Path::new(TAPE_STATUS_DIR); let mut inventory = Inventory::new(status_path); if let Some(MediaSetLabel { ref pool, ref uuid, ..}) = media_id.media_set_label { let _pool_lock = lock_media_pool(status_path, pool)?; let _media_set_lock = lock_media_set(status_path, uuid, None)?; MediaCatalog::destroy(status_path, &media_id.label.uuid)?; inventory.remove_media(&media_id.label.uuid)?; } else { let _lock = lock_unassigned_media_pool(status_path)?; MediaCatalog::destroy(status_path, &media_id.label.uuid)?; inventory.remove_media(&media_id.label.uuid)?; }; handle.format_media(fast.unwrap_or(true))?; } } Ok(()) }, )?; Ok(upid_str.into()) } #[api( input: { properties: { drive: { schema: DRIVE_NAME_SCHEMA, }, }, }, returns: { schema: UPID_SCHEMA, }, access: { permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_READ, false), }, )] /// Rewind tape pub fn rewind( drive: String, rpcenv: &mut dyn RpcEnvironment, ) -> Result { let upid_str = run_drive_worker( rpcenv, drive.clone(), "rewind-media", Some(drive.clone()), move |_worker, config| { let mut drive = open_drive(&config, &drive)?; drive.rewind()?; Ok(()) }, )?; Ok(upid_str.into()) } #[api( input: { properties: { drive: { schema: DRIVE_NAME_SCHEMA, }, }, }, returns: { schema: UPID_SCHEMA, }, access: { permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_READ, false), }, )] /// Eject/Unload drive media pub fn eject_media( drive: String, rpcenv: &mut dyn RpcEnvironment, ) -> Result { let upid_str = run_drive_worker( rpcenv, drive.clone(), "eject-media", Some(drive.clone()), move |_worker, config| { if let Some((mut changer, _)) = media_changer(&config, &drive)? { changer.unload_media(None)?; } else { let mut drive = open_drive(&config, &drive)?; drive.eject_media()?; } Ok(()) }, )?; Ok(upid_str.into()) } #[api( input: { properties: { drive: { schema: DRIVE_NAME_SCHEMA, }, "label-text": { schema: MEDIA_LABEL_SCHEMA, }, pool: { schema: MEDIA_POOL_NAME_SCHEMA, optional: true, }, }, }, returns: { schema: UPID_SCHEMA, }, access: { permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_WRITE, false), }, )] /// Label media /// /// Write a new media label to the media in 'drive'. The media is /// assigned to the specified 'pool', or else to the free media pool. /// /// Note: The media need to be empty (you may want to format it first). pub fn label_media( drive: String, pool: Option, label_text: String, rpcenv: &mut dyn RpcEnvironment, ) -> Result { if let Some(ref pool) = pool { let (pool_config, _digest) = pbs_config::media_pool::config()?; if pool_config.sections.get(pool).is_none() { bail!("no such pool ('{}')", pool); } } let upid_str = run_drive_worker( rpcenv, drive.clone(), "label-media", Some(drive.clone()), move |worker, config| { let mut drive = open_drive(&config, &drive)?; drive.rewind()?; 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(err) => { bail!("media read error - {}", err); } } let ctime = proxmox_time::epoch_i64(); let label = MediaLabel { label_text: label_text.to_string(), uuid: Uuid::generate(), ctime, }; write_media_label(worker, &mut drive, label, pool) }, )?; Ok(upid_str.into()) } fn write_media_label( worker: Arc, drive: &mut Box, 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); 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) }; // Create the media catalog MediaCatalog::overwrite(status_path, &media_id, false)?; let mut inventory = Inventory::new(status_path); inventory.store(media_id.clone(), false)?; media_id } else { task_log!(worker, "Label media '{}' (no pool assignment)", label.label_text); let media_id = MediaId { label, media_set_label: None }; // Create the media catalog MediaCatalog::overwrite(status_path, &media_id, false)?; let mut inventory = Inventory::new(status_path); inventory.store(media_id.clone(), false)?; media_id }; drive.rewind()?; match drive.read_label() { Ok((Some(info), _)) => { if info.label.uuid != media_id.label.uuid { bail!("verify label failed - got wrong label uuid"); } if let Some(ref pool) = pool { match info.media_set_label { Some(set) => { if set.uuid != [0u8; 16].into() { bail!("verify media set label failed - got wrong set uuid"); } if &set.pool != pool { bail!("verify media set label failed - got wrong pool"); } } None => { bail!("verify media set label failed (missing set label)"); } } } }, Ok((None, _)) => bail!("verify label failed (got empty media)"), Err(err) => bail!("verify label failed - {}", err), }; drive.rewind()?; Ok(()) } #[api( protected: true, input: { properties: { drive: { schema: DRIVE_NAME_SCHEMA, }, password: { description: "Encryption key password.", }, }, }, access: { permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_READ, false), }, )] /// Try to restore a tape encryption key pub async fn restore_key( drive: String, password: String, ) -> Result<(), Error> { run_drive_blocking_task( drive.clone(), "restore key".to_string(), move |config| { let mut drive = open_drive(&config, &drive)?; let (_media_id, key_config) = drive.read_label()?; if let Some(key_config) = key_config { let password_fn = || { Ok(password.as_bytes().to_vec()) }; let (key, ..) = key_config.decrypt(&password_fn)?; pbs_config::tape_encryption_keys::insert_key(key, key_config, true)?; } else { bail!("media does not contain any encryption key configuration"); } Ok(()) } ) .await } #[api( input: { properties: { drive: { schema: DRIVE_NAME_SCHEMA, }, inventorize: { description: "Inventorize media", optional: true, }, }, }, returns: { type: MediaIdFlat, }, access: { permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_READ, false), }, )] /// 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)?; let (media_id, _key_config) = drive.read_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 encrypt_fingerprint = set.encryption_key_fingerprint.clone() .map(|fp| (fp, set.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(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)."); } }; Ok(media_id) } ) .await } #[api( input: { properties: { drive: { schema: DRIVE_NAME_SCHEMA, }, }, }, returns: { schema: UPID_SCHEMA, }, access: { permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_READ, false), }, )] /// Clean drive pub fn clean_drive( drive: String, rpcenv: &mut dyn RpcEnvironment, ) -> Result { let upid_str = run_drive_worker( rpcenv, drive.clone(), "clean-drive", Some(drive.clone()), move |worker, config| { let (mut changer, _changer_name) = required_media_changer(&config, &drive)?; task_log!(worker, "Starting drive clean"); 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)?)?; // 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); } } task_log!(worker, "Drive cleaned successfully"); Ok(()) }, )?; Ok(upid_str.into()) } #[api( input: { properties: { drive: { schema: DRIVE_NAME_SCHEMA, }, }, }, returns: { description: "The list of media labels with associated media Uuid (if any).", type: Array, items: { type: LabelUuidMap, }, }, access: { permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_READ, false), }, )] /// List known media labels (Changer Inventory) /// /// Note: Only useful for drives with associated changer device. /// /// 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)?; let label_text_list = changer.online_media_label_texts()?; let state_path = Path::new(TAPE_STATUS_DIR); let mut inventory = Inventory::load(state_path)?; update_changer_online_status( &config, &mut inventory, &changer_name, &label_text_list, )?; 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 }); } } Ok(list) } ) .await } #[api( input: { properties: { drive: { schema: DRIVE_NAME_SCHEMA, }, "read-all-labels": { description: "Load all tapes and try read labels (even if already inventoried)", type: bool, optional: true, }, }, }, returns: { schema: UPID_SCHEMA, }, access: { permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_READ, false), }, )] /// Update inventory /// /// Note: Only useful for drives with associated changer device. /// /// This method queries the changer to get a list of media labels. It /// then loads any unknown media into the drive, reads the label, and /// store the result to the media database. /// /// Note: This updates the media online status. pub fn update_inventory( drive: String, read_all_labels: Option, rpcenv: &mut dyn RpcEnvironment, ) -> Result { let upid_str = run_drive_worker( rpcenv, drive.clone(), "inventory-update", Some(drive.clone()), move |worker, config| { let (mut changer, changer_name) = required_media_changer(&config, &drive)?; let label_text_list = changer.online_media_label_texts()?; if label_text_list.is_empty() { task_log!(worker, "changer device does not list any media labels"); } let state_path = Path::new(TAPE_STATUS_DIR); let mut inventory = Inventory::load(state_path)?; update_changer_online_status(&config, &mut inventory, &changer_name, &label_text_list)?; for label_text in label_text_list.iter() { if label_text.starts_with("CLN") { task_log!(worker, "skip cleaning unit '{}'", label_text); continue; } let label_text = label_text.to_string(); 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; } if let Err(err) = changer.load_media(&label_text) { task_warn!(worker, "unable to load media '{}' - {}", label_text, err); continue; } 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); } 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); continue; } 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 { 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)?; }; } } changer.unload_media(None)?; } Ok(()) }, )?; Ok(upid_str.into()) } #[api( input: { properties: { drive: { schema: DRIVE_NAME_SCHEMA, }, pool: { schema: MEDIA_POOL_NAME_SCHEMA, optional: true, }, }, }, returns: { schema: UPID_SCHEMA, }, access: { permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_WRITE, false), }, )] /// Label media with barcodes from changer device pub fn barcode_label_media( drive: String, pool: Option, rpcenv: &mut dyn RpcEnvironment, ) -> Result { if let Some(ref pool) = pool { let (pool_config, _digest) = pbs_config::media_pool::config()?; if pool_config.sections.get(pool).is_none() { bail!("no such pool ('{}')", pool); } } let upid_str = run_drive_worker( rpcenv, drive.clone(), "barcode-label-media", Some(drive.clone()), move |worker, config| barcode_label_media_worker(worker, drive, &config, pool), )?; Ok(upid_str.into()) } fn barcode_label_media_worker( worker: Arc, drive: String, drive_config: &SectionConfigData, pool: Option, ) -> Result<(), Error> { let (mut changer, changer_name) = required_media_changer(drive_config, &drive)?; let mut label_text_list = changer.online_media_label_texts()?; // make sure we label them in the right order label_text_list.sort(); let state_path = Path::new(TAPE_STATUS_DIR); let mut inventory = Inventory::load(state_path)?; 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; } inventory.reload()?; if inventory.find_media_by_label_text(&label_text).is_some() { task_log!(worker, "media '{}' already inventoried (already labeled)", label_text); continue; } task_log!(worker, "checking/loading media '{}'", label_text); if let Err(err) = changer.load_media(&label_text) { task_warn!(worker, "unable to load media '{}' - {}", label_text, err); continue; } let mut drive = open_drive(drive_config, &drive)?; drive.rewind()?; match drive.read_next_file() { Ok(_reader) => { 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(_err) => { task_warn!(worker, "media '{}' read error (maybe not empty - format it first)", label_text); continue; } } let ctime = proxmox_time::epoch_i64(); let label = MediaLabel { label_text: label_text.to_string(), uuid: Uuid::generate(), ctime, }; write_media_label(worker.clone(), &mut drive, label, pool.clone())? } Ok(()) } #[api( input: { properties: { drive: { schema: DRIVE_NAME_SCHEMA, }, }, }, returns: { description: "A List of medium auxiliary memory attributes.", type: Array, items: { type: MamAttribute, }, }, access: { permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_AUDIT, false), }, )] /// Read Cartridge Memory (Medium auxiliary memory attributes) pub async fn cartridge_memory(drive: String) -> Result, Error> { run_drive_blocking_task( drive.clone(), "reading cartridge memory".to_string(), move |config| { let drive_config: LtoTapeDrive = config.lookup("lto", &drive)?; let mut handle = open_lto_tape_drive(&drive_config)?; handle.cartridge_memory() } ) .await } #[api( input: { properties: { drive: { schema: DRIVE_NAME_SCHEMA, }, }, }, returns: { type: Lp17VolumeStatistics, }, access: { permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_AUDIT, false), }, )] /// Read Volume Statistics (SCSI log page 17h) pub async fn volume_statistics(drive: String) -> Result { run_drive_blocking_task( drive.clone(), "reading volume statistics".to_string(), move |config| { let drive_config: LtoTapeDrive = config.lookup("lto", &drive)?; let mut handle = open_lto_tape_drive(&drive_config)?; handle.volume_statistics() } ) .await } #[api( input: { properties: { drive: { schema: DRIVE_NAME_SCHEMA, }, }, }, returns: { type: LtoDriveAndMediaStatus, }, access: { permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_AUDIT, false), }, )] /// Get drive/media status pub async fn status(drive: String) -> Result { run_drive_blocking_task( drive.clone(), "reading drive status".to_string(), move |config| { let drive_config: LtoTapeDrive = config.lookup("lto", &drive)?; // Note: use open_lto_tape_device, because this also works if no medium loaded let file = open_lto_tape_device(&drive_config.path)?; let mut handle = LtoTapeHandle::new(file)?; handle.get_drive_and_media_status() } ) .await } #[api( input: { properties: { drive: { schema: DRIVE_NAME_SCHEMA, }, force: { description: "Force overriding existing index.", type: bool, optional: true, }, scan: { description: "Re-read the whole tape to reconstruct the catalog instead of restoring saved versions.", type: bool, optional: true, }, verbose: { description: "Verbose mode - log all found chunks.", type: bool, optional: true, }, }, }, returns: { schema: UPID_SCHEMA, }, access: { permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_READ, false), }, )] /// Scan media and record content pub fn catalog_media( drive: String, force: Option, scan: Option, verbose: Option, rpcenv: &mut dyn RpcEnvironment, ) -> Result { let verbose = verbose.unwrap_or(false); let force = force.unwrap_or(false); let scan = scan.unwrap_or(false); let upid_str = run_drive_worker( rpcenv, drive.clone(), "catalog-media", Some(drive.clone()), move |worker, config| { let mut drive = open_drive(&config, &drive)?; drive.rewind()?; let media_id = match drive.read_label()? { (Some(media_id), key_config) => { task_log!( worker, "found media label: {}", serde_json::to_string_pretty(&serde_json::to_value(&media_id)?)? ); if key_config.is_some() { task_log!( worker, "encryption key config: {}", serde_json::to_string_pretty(&serde_json::to_value(&key_config)?)? ); } media_id }, (None, _) => bail!("media is empty (no media label found)"), }; let status_path = Path::new(TAPE_STATUS_DIR); let mut inventory = Inventory::new(status_path); let (_media_set_lock, media_set_uuid) = match media_id.media_set_label { None => { 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(()); } Some(ref set) => { 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() .map(|fp| (fp, set.uuid.clone())); drive.set_encryption(encrypt_fingerprint)?; let _pool_lock = lock_media_pool(status_path, &set.pool)?; let media_set_lock = lock_media_set(status_path, &set.uuid, None)?; MediaCatalog::destroy_unrelated_catalog(status_path, &media_id)?; inventory.store(media_id.clone(), false)?; (media_set_lock, &set.uuid) } }; if MediaCatalog::exists(status_path, &media_id.label.uuid) && !force { bail!("media catalog exists (please use --force to overwrite)"); } if !scan { 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(()) } task_log!(worker, "no catalog found"); } task_log!(worker, "scanning entire media to reconstruct catalog"); drive.rewind()?; 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)?; Ok(()) }, )?; Ok(upid_str.into()) } #[api( input: { properties: { changer: { schema: CHANGER_NAME_SCHEMA, optional: true, }, }, }, returns: { description: "The list of configured drives with model information.", type: Array, items: { type: DriveListEntry, }, }, access: { description: "List configured tape drives filtered by Tape.Audit privileges", permission: &Permission::Anybody, }, )] /// List drives pub fn list_drives( changer: Option, _param: Value, rpcenv: &mut dyn RpcEnvironment, ) -> Result, Error> { let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?; let user_info = CachedUserInfo::new()?; let (config, _) = pbs_config::drive::config()?; let lto_drives = lto_tape_device_list(); let drive_list: Vec = config.convert_to_typed_array("lto")?; let mut list = Vec::new(); for drive in drive_list { if changer.is_some() && drive.changer != changer { continue; } let privs = user_info.lookup_privs(&auth_id, &["tape", "drive", &drive.name]); if (privs & PRIV_TAPE_AUDIT) == 0 { continue; } 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 }; list.push(entry); } Ok(list) } #[sortable] 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) ), ( "format-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) ), ( "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) ), ]); const ITEM_ROUTER: Router = Router::new() .get(&list_subdirs_api_method!(SUBDIRS)) .subdirs(&SUBDIRS); pub const ROUTER: Router = Router::new() .get(&API_METHOD_LIST_DRIVES) .match_all("drive", &ITEM_ROUTER);