X-Git-Url: https://git.proxmox.com/?a=blobdiff_plain;f=src%2Fapi2%2Ftape%2Fdrive.rs;h=5878ca445692d0af288e1ce3c3d0abdbd273cad2;hb=5839c469c16153034bf69ff435ef1e77d3c7f490;hp=6ec8fc84148950b22a6723e6605355abd8b2943c;hpb=5ae86dfaa158df4f19f17f2676122c150ee9a2bc;p=proxmox-backup.git diff --git a/src/api2/tape/drive.rs b/src/api2/tape/drive.rs index 6ec8fc84..5878ca44 100644 --- a/src/api2/tape/drive.rs +++ b/src/api2/tape/drive.rs @@ -1,5 +1,7 @@ +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; @@ -9,89 +11,176 @@ use proxmox::{ identity, list_subdirs_api_method, tools::Uuid, - sys::error::SysError, api::{ api, + section_config::SectionConfigData, RpcEnvironment, + RpcEnvironmentType, + Permission, Router, SubdirMap, }, }; +use pbs_datastore::task_log; + use crate::{ config::{ - self, - drive::check_drive_exists, - }, - api2::types::{ - UPID_SCHEMA, - DRIVE_NAME_SCHEMA, - MEDIA_LABEL_SCHEMA, - MEDIA_POOL_NAME_SCHEMA, - Authid, - LinuxTapeDrive, - ScsiTapeChanger, - TapeDeviceInfo, - MediaIdFlat, - LabelUuidMap, - MamAttribute, - LinuxDriveAndMediaStatus, + cached_user_info::CachedUserInfo, + acl::{ + PRIV_TAPE_AUDIT, + PRIV_TAPE_READ, + PRIV_TAPE_WRITE, + }, + }, + api2::{ + types::{ + UPID_SCHEMA, + CHANGER_NAME_SCHEMA, + DRIVE_NAME_SCHEMA, + MEDIA_LABEL_SCHEMA, + MEDIA_POOL_NAME_SCHEMA, + Authid, + DriveListEntry, + LtoTapeDrive, + MediaIdFlat, + LabelUuidMap, + MamAttribute, + LtoDriveAndMediaStatus, + Lp17VolumeStatistics, + }, + tape::restore::{ + fast_catalog_restore, + restore_media, + }, }, server::WorkerTask, tape::{ TAPE_STATUS_DIR, - TapeDriver, - MediaChange, Inventory, - MediaStateDatabase, + MediaCatalog, MediaId, - mtx_load, - mtx_unload, - linux_tape_device_list, - open_drive, - media_changer, - update_changer_online_status, - mam_extract_media_usage, + BlockReadError, + lock_media_set, + lock_media_pool, + lock_unassigned_media_pool, + lto_tape_device_list, + lookup_device_identification, file_formats::{ MediaLabel, MediaSetLabel, }, + drive::{ + TapeDriver, + LtoTapeHandle, + open_lto_tape_device, + open_lto_tape_drive, + media_changer, + required_media_changer, + open_drive, + lock_tape_device, + set_tape_device_state, + get_tape_device_state, + tape_alert_flags_critical, + }, + 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: Authid = rpcenv.get_auth_id().unwrap().parse()?; + 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, }, - slot: { - description: "Source slot number", - minimum: 1, + "label-text": { + schema: MEDIA_LABEL_SCHEMA, }, }, }, + returns: { + schema: UPID_SCHEMA, + }, + access: { + permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_READ, false), + }, )] -/// Load media via changer from slot -pub async fn load_slot( +/// Load media with specified label +/// +/// Issue a media load request to the associated changer device. +pub fn load_media( drive: String, - slot: u64, - _param: Value, -) -> Result<(), Error> { - - let (config, _digest) = config::drive::config()?; - - let drive_config: LinuxTapeDrive = config.lookup("linux", &drive)?; - - let changer: ScsiTapeChanger = match drive_config.changer { - Some(ref changer) => config.lookup("changer", changer)?, - None => bail!("drive '{}' has no associated changer", drive), - }; + 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(()) + }, + )?; - tokio::task::spawn_blocking(move || { - let drivenum = drive_config.changer_drive_id.unwrap_or(0); - mtx_load(&changer.path, slot, drivenum) - }).await? + Ok(upid_str.into()) } #[api( @@ -100,23 +189,30 @@ pub async fn load_slot( drive: { schema: DRIVE_NAME_SCHEMA, }, - "changer-id": { - schema: MEDIA_LABEL_SCHEMA, + "source-slot": { + description: "Source slot number.", + minimum: 1, }, }, }, + access: { + permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_READ, false), + }, )] -/// Load media with specified label +/// Load media from the specified slot /// /// Issue a media load request to the associated changer device. -pub async fn load_media(drive: String, changer_id: String) -> Result<(), Error> { - - let (config, _digest) = config::drive::config()?; - - tokio::task::spawn_blocking(move || { - let (mut changer, _) = media_changer(&config, &drive, false)?; - changer.load_media(&changer_id) - }).await? +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( @@ -125,59 +221,81 @@ pub async fn load_media(drive: String, changer_id: String) -> Result<(), Error> drive: { schema: DRIVE_NAME_SCHEMA, }, - slot: { - description: "Target slot number. If omitted, defaults to the slot that the drive was loaded from.", - minimum: 1, - optional: true, + "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), + }, )] -/// Unload media via changer -pub async fn unload( - drive: String, - slot: Option, - _param: Value, -) -> Result<(), Error> { - - let (config, _digest) = config::drive::config()?; - - let mut drive_config: LinuxTapeDrive = config.lookup("linux", &drive)?; - - let changer: ScsiTapeChanger = match drive_config.changer { - Some(ref changer) => config.lookup("changer", changer)?, - None => bail!("drive '{}' has no associated changer", drive), - }; - - let drivenum = drive_config.changer_drive_id.unwrap_or(0); - - tokio::task::spawn_blocking(move || { - if let Some(slot) = slot { - mtx_unload(&changer.path, slot, drivenum) - } else { - drive_config.unload_media() +/// 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? + ) + .await } #[api( input: { - properties: {}, + 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: { - description: "The list of autodetected tape drives.", - type: Array, - items: { - type: TapeDeviceInfo, - }, + schema: UPID_SCHEMA, + }, + access: { + permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_READ, false), }, )] -/// Scan tape drives -pub fn scan_drives(_param: Value) -> Result, Error> { +/// 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 list = linux_tape_device_list(); + let (mut changer, _) = required_media_changer(&config, &drive)?; + changer.unload_media(target_slot)?; + Ok(()) + }, + )?; - Ok(list) + Ok(upid_str.into()) } #[api( @@ -192,35 +310,95 @@ pub fn scan_drives(_param: Value) -> Result, Error> { 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), + }, )] -/// Erase media -pub fn erase_media( +/// 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 + ); + } + } - let (config, _digest) = config::drive::config()?; + task_log!( + worker, + "found media '{}' with uuid '{}'", + media_id.label.label_text, media_id.label.uuid, + ); - check_drive_exists(&config, &drive)?; // early check before starting worker + let status_path = Path::new(TAPE_STATUS_DIR); + let mut inventory = Inventory::new(status_path); - let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?; + 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))?; + } + } - let upid_str = WorkerTask::new_thread( - "erase-media", - Some(drive.clone()), - auth_id, - true, - move |_worker| { - let mut drive = open_drive(&config, &drive)?; - drive.erase_media(fast.unwrap_or(true))?; Ok(()) - } + }, )?; Ok(upid_str.into()) @@ -237,29 +415,25 @@ pub fn erase_media( 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 (config, _digest) = config::drive::config()?; - - check_drive_exists(&config, &drive)?; // early check before starting worker - - let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?; - - let upid_str = WorkerTask::new_thread( + let upid_str = run_drive_worker( + rpcenv, + drive.clone(), "rewind-media", Some(drive.clone()), - auth_id, - true, - move |_worker| { + move |_worker, config| { let mut drive = open_drive(&config, &drive)?; drive.rewind()?; Ok(()) - } + }, )?; Ok(upid_str.into()) @@ -273,22 +447,35 @@ pub fn rewind( }, }, }, + returns: { + schema: UPID_SCHEMA, + }, + access: { + permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_READ, false), + }, )] /// Eject/Unload drive media -pub async fn eject_media(drive: String) -> Result<(), Error> { - - let (config, _digest) = config::drive::config()?; - - tokio::task::spawn_blocking(move || { - let (mut changer, _) = media_changer(&config, &drive, false)?; - - if !changer.eject_on_unload() { - let mut drive = open_drive(&config, &drive)?; - drive.eject_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(()) + }, + )?; - changer.unload_media() - }).await? + Ok(upid_str.into()) } #[api( @@ -297,7 +484,7 @@ pub async fn eject_media(drive: String) -> Result<(), Error> { drive: { schema: DRIVE_NAME_SCHEMA, }, - "changer-id": { + "label-text": { schema: MEDIA_LABEL_SCHEMA, }, pool: { @@ -309,64 +496,57 @@ pub async fn eject_media(drive: String) -> Result<(), Error> { 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 erase it first). +/// Note: The media need to be empty (you may want to format it first). pub fn label_media( drive: String, pool: Option, - changer_id: String, + label_text: String, rpcenv: &mut dyn RpcEnvironment, ) -> Result { - - let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?; - if let Some(ref pool) = pool { - let (pool_config, _digest) = config::media_pool::config()?; + let (pool_config, _digest) = pbs_config::media_pool::config()?; if pool_config.sections.get(pool).is_none() { bail!("no such pool ('{}')", pool); } } - - let (config, _digest) = config::drive::config()?; - - let upid_str = WorkerTask::new_thread( + let upid_str = run_drive_worker( + rpcenv, + drive.clone(), "label-media", Some(drive.clone()), - auth_id, - true, - move |worker| { - + move |worker, config| { let mut drive = open_drive(&config, &drive)?; drive.rewind()?; match drive.read_next_file() { - Ok(Some(_file)) => bail!("media is not empty (erase first)"), - Ok(None) => { /* EOF mark at BOT, assume tape is empty */ }, + 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) => { - if err.is_errno(nix::errno::Errno::ENOSPC) || err.is_errno(nix::errno::Errno::EIO) { - /* assume tape is empty */ - } else { - bail!("media read error - {}", err); - } + bail!("media read error - {}", err); } } let ctime = proxmox::tools::time::epoch_i64(); let label = MediaLabel { - changer_id: changer_id.to_string(), + label_text: label_text.to_string(), uuid: Uuid::generate(), ctime, }; write_media_label(worker, &mut drive, label, pool) - } + }, )?; Ok(upid_str.into()) @@ -381,28 +561,42 @@ fn write_media_label( drive.label_tape(&label)?; - let mut media_set_label = None; + let status_path = Path::new(TAPE_STATUS_DIR); - if let Some(ref pool) = pool { + let media_id = if let Some(ref pool) = pool { // assign media to pool by writing special media set label - worker.log(format!("Label media '{}' for pool '{}'", label.changer_id, pool)); - let set = MediaSetLabel::with_data(&pool, [0u8; 16].into(), 0, label.ctime); + worker.log(format!("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)?; - drive.write_media_set_label(&set)?; - 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)?; + + let mut inventory = Inventory::new(status_path); + inventory.store(media_id.clone(), false)?; + + media_id } else { - worker.log(format!("Label media '{}' (no pool assignment)", label.changer_id)); - } + worker.log(format!("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 }; + // Create the media catalog + MediaCatalog::overwrite(status_path, &media_id, false)?; - let mut inventory = Inventory::load(Path::new(TAPE_STATUS_DIR))?; - inventory.store(media_id.clone())?; + let mut inventory = Inventory::new(status_path); + inventory.store(media_id.clone(), false)?; + + media_id + }; drive.rewind()?; match drive.read_label() { - Ok(Some(info)) => { + Ok((Some(info), _)) => { if info.label.uuid != media_id.label.uuid { bail!("verify label failed - got wrong label uuid"); } @@ -422,7 +616,7 @@ fn write_media_label( } } }, - Ok(None) => bail!("verify label failed (got empty media)"), + Ok((None, _)) => bail!("verify label failed (got empty media)"), Err(err) => bail!("verify label failed - {}", err), }; @@ -432,53 +626,201 @@ fn write_media_label( } #[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 -pub async fn read_label(drive: String) -> Result { +/// 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| pbs_tools::format::as_fingerprint(fp.bytes())); + + 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); + } + } - let (config, _digest) = config::drive::config()?; + 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)?; + }; + } - tokio::task::spawn_blocking(move || { - let mut drive = open_drive(&config, &drive)?; - - let media_id = drive.read_label()?; - - let media_id = match media_id { - Some(media_id) => { - let mut flat = MediaIdFlat { - uuid: media_id.label.uuid.to_string(), - changer_id: media_id.label.changer_id.clone(), - ctime: media_id.label.ctime, - media_set_ctime: None, - media_set_uuid: None, - pool: None, - seq_nr: None, - }; - if let Some(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.to_string()); - flat.media_set_ctime = Some(set.ctime); + flat } - flat - } - None => { - bail!("Media is empty (no label)."); - } - }; + 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)?; + + worker.log("Starting drive clean"); + + changer.clean_drive()?; - Ok(media_id) - }).await? + 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() { + worker.log(format!("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() { + worker.log(format!("Volume mounts: {}", volume_stats.volume_mounts)); + let wearout = volume_stats.volume_mounts * 2; // (*100.0/50.0); + worker.log(format!("Cleaning tape wearout: {}%", wearout)); + } + } + + worker.log("Drive cleaned successfully"); + + Ok(()) + }, + )?; + + Ok(upid_str.into()) } #[api( @@ -496,6 +838,9 @@ pub async fn read_label(drive: String) -> Result { type: LabelUuidMap, }, }, + access: { + permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_READ, false), + }, )] /// List known media labels (Changer Inventory) /// @@ -507,46 +852,46 @@ pub async fn read_label(drive: String) -> Result { 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 (config, _digest) = config::drive::config()?; + let label_text_list = changer.online_media_label_texts()?; - tokio::task::spawn_blocking(move || { - let (changer, changer_name) = media_changer(&config, &drive, false)?; + let state_path = Path::new(TAPE_STATUS_DIR); - let changer_id_list = changer.list_media_changer_ids()?; + let mut inventory = Inventory::load(state_path)?; - let state_path = Path::new(TAPE_STATUS_DIR); + update_changer_online_status( + &config, + &mut inventory, + &changer_name, + &label_text_list, + )?; - let mut inventory = Inventory::load(state_path)?; - let mut state_db = MediaStateDatabase::load(state_path)?; + let mut list = Vec::new(); - update_changer_online_status( - &config, - &mut inventory, - &mut state_db, - &changer_name, - &changer_id_list, - )?; + for label_text in label_text_list.iter() { + if label_text.starts_with("CLN") { + // skip cleaning unit + continue; + } - let mut list = Vec::new(); + let label_text = label_text.to_string(); - for changer_id in changer_id_list.iter() { - if changer_id.starts_with("CLN") { - // skip cleaning unit - continue; + 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 }); + } } - let changer_id = changer_id.to_string(); - - if let Some(media_id) = inventory.find_media_by_changer_id(&changer_id) { - list.push(LabelUuidMap { changer_id, uuid: Some(media_id.label.uuid.to_string()) }); - } else { - list.push(LabelUuidMap { changer_id, uuid: None }); - } + Ok(list) } - - Ok(list) - }).await? + ) + .await } #[api( @@ -565,6 +910,9 @@ pub async fn inventory( returns: { schema: UPID_SCHEMA, }, + access: { + permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_READ, false), + }, )] /// Update inventory /// @@ -580,74 +928,74 @@ pub fn update_inventory( read_all_labels: Option, rpcenv: &mut dyn RpcEnvironment, ) -> Result { - - let (config, _digest) = config::drive::config()?; - - check_drive_exists(&config, &drive)?; // early check before starting worker - - let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?; - - let upid_str = WorkerTask::new_thread( + let upid_str = run_drive_worker( + rpcenv, + drive.clone(), "inventory-update", Some(drive.clone()), - auth_id, - true, - move |worker| { - - let (mut changer, changer_name) = media_changer(&config, &drive, false)?; + move |worker, config| { + let (mut changer, changer_name) = required_media_changer(&config, &drive)?; - let changer_id_list = changer.list_media_changer_ids()?; - if changer_id_list.is_empty() { - worker.log(format!("changer device does not list any media labels")); + let label_text_list = changer.online_media_label_texts()?; + if label_text_list.is_empty() { + worker.log("changer device does not list any media labels".to_string()); } let state_path = Path::new(TAPE_STATUS_DIR); let mut inventory = Inventory::load(state_path)?; - let mut state_db = MediaStateDatabase::load(state_path)?; - update_changer_online_status(&config, &mut inventory, &mut state_db, &changer_name, &changer_id_list)?; + update_changer_online_status(&config, &mut inventory, &changer_name, &label_text_list)?; - for changer_id in changer_id_list.iter() { - if changer_id.starts_with("CLN") { - worker.log(format!("skip cleaning unit '{}'", changer_id)); + for label_text in label_text_list.iter() { + if label_text.starts_with("CLN") { + worker.log(format!("skip cleaning unit '{}'", label_text)); continue; } - let changer_id = changer_id.to_string(); + let label_text = label_text.to_string(); - if !read_all_labels.unwrap_or(false) { - if let Some(_) = inventory.find_media_by_changer_id(&changer_id) { - worker.log(format!("media '{}' already inventoried", changer_id)); - continue; - } + if !read_all_labels.unwrap_or(false) && inventory.find_media_by_label_text(&label_text).is_some() { + worker.log(format!("media '{}' already inventoried", label_text)); + continue; } - if let Err(err) = changer.load_media(&changer_id) { - worker.warn(format!("unable to load media '{}' - {}", changer_id, err)); + if let Err(err) = changer.load_media(&label_text) { + worker.warn(format!("unable to load media '{}' - {}", label_text, err)); continue; } let mut drive = open_drive(&config, &drive)?; match drive.read_label() { Err(err) => { - worker.warn(format!("unable to read label form media '{}' - {}", changer_id, err)); + worker.warn(format!("unable to read label form media '{}' - {}", label_text, err)); } - Ok(None) => { - worker.log(format!("media '{}' is empty", changer_id)); + Ok((None, _)) => { + worker.log(format!("media '{}' is empty", label_text)); } - Ok(Some(media_id)) => { - if changer_id != media_id.label.changer_id { - worker.warn(format!("label changer ID missmatch ({} != {})", changer_id, media_id.label.changer_id)); + Ok((Some(media_id), _key_config)) => { + if label_text != media_id.label.label_text { + worker.warn(format!("label text mismatch ({} != {})", label_text, media_id.label.label_text)); continue; } - worker.log(format!("inventorize media '{}' with uuid '{}'", changer_id, media_id.label.uuid)); - inventory.store(media_id)?; + worker.log(format!("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()) @@ -669,6 +1017,9 @@ pub fn update_inventory( 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( @@ -676,25 +1027,20 @@ pub fn barcode_label_media( pool: Option, rpcenv: &mut dyn RpcEnvironment, ) -> Result { - if let Some(ref pool) = pool { - let (pool_config, _digest) = config::media_pool::config()?; + let (pool_config, _digest) = pbs_config::media_pool::config()?; if pool_config.sections.get(pool).is_none() { bail!("no such pool ('{}')", pool); } } - let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?; - - let upid_str = WorkerTask::new_thread( + let upid_str = run_drive_worker( + rpcenv, + drive.clone(), "barcode-label-media", Some(drive.clone()), - auth_id, - true, - move |worker| { - barcode_label_media_worker(worker, drive, pool) - } + move |worker, config| barcode_label_media_worker(worker, drive, &config, pool), )?; Ok(upid_str.into()) @@ -703,64 +1049,61 @@ pub fn barcode_label_media( 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 (config, _digest) = config::drive::config()?; + let mut label_text_list = changer.online_media_label_texts()?; - let (mut changer, changer_name) = media_changer(&config, &drive, false)?; - - let changer_id_list = changer.list_media_changer_ids()?; + // 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)?; - let mut state_db = MediaStateDatabase::load(state_path)?; - update_changer_online_status(&config, &mut inventory, &mut state_db, &changer_name, &changer_id_list)?; + update_changer_online_status(drive_config, &mut inventory, &changer_name, &label_text_list)?; - if changer_id_list.is_empty() { + if label_text_list.is_empty() { bail!("changer device does not list any media labels"); } - for changer_id in changer_id_list { - if changer_id.starts_with("CLN") { continue; } + for label_text in label_text_list { + if label_text.starts_with("CLN") { continue; } inventory.reload()?; - if inventory.find_media_by_changer_id(&changer_id).is_some() { - worker.log(format!("media '{}' already inventoried (already labeled)", changer_id)); + if inventory.find_media_by_label_text(&label_text).is_some() { + worker.log(format!("media '{}' already inventoried (already labeled)", label_text)); continue; } - worker.log(format!("checking/loading media '{}'", changer_id)); + worker.log(format!("checking/loading media '{}'", label_text)); - if let Err(err) = changer.load_media(&changer_id) { - worker.warn(format!("unable to load media '{}' - {}", changer_id, err)); + if let Err(err) = changer.load_media(&label_text) { + worker.warn(format!("unable to load media '{}' - {}", label_text, err)); continue; } - let mut drive = open_drive(&config, &drive)?; + let mut drive = open_drive(drive_config, &drive)?; drive.rewind()?; match drive.read_next_file() { - Ok(Some(_file)) => { - worker.log(format!("media '{}' is not empty (erase first)", changer_id)); + Ok(_reader) => { + worker.log(format!("media '{}' is not empty (format it first)", label_text)); continue; } - Ok(None) => { /* EOF mark at BOT, assume tape is empty */ }, - Err(err) => { - if err.is_errno(nix::errno::Errno::ENOSPC) || err.is_errno(nix::errno::Errno::EIO) { - /* assume tape is empty */ - } else { - worker.warn(format!("media '{}' read error (maybe not empty - erase first)", changer_id)); - continue; - } + Err(BlockReadError::EndOfFile) => { /* EOF mark at BOT, assume tape is empty */ }, + Err(BlockReadError::EndOfStream) => { /* tape is empty */ }, + Err(_err) => { + worker.warn(format!("media '{}' read error (maybe not empty - format it first)", label_text)); + continue; } } let ctime = proxmox::tools::time::epoch_i64(); let label = MediaLabel { - changer_id: changer_id.to_string(), + label_text: label_text.to_string(), uuid: Uuid::generate(), ctime, }; @@ -786,17 +1129,87 @@ fn barcode_label_media_worker( type: MamAttribute, }, }, + access: { + permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_AUDIT, false), + }, )] /// Read Cartridge Memory (Medium auxiliary memory attributes) -pub fn cartridge_memory(drive: String) -> Result, Error> { +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)?; - let (config, _digest) = config::drive::config()?; + // Note: use open_lto_tape_device, because this also works if no medium loaded + let file = open_lto_tape_device(&drive_config.path)?; - let drive_config: LinuxTapeDrive = config.lookup("linux", &drive)?; - let mut handle = drive_config.open() - .map_err(|err| format_err!("open drive '{}' ({}) failed - {}", drive, drive_config.path, err))?; + let mut handle = LtoTapeHandle::new(file)?; - handle.cartridge_memory() + handle.get_drive_and_media_status() + } + ) + .await } #[api( @@ -805,40 +1218,189 @@ pub fn cartridge_memory(drive: String) -> Result, Error> { 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: { - type: LinuxDriveAndMediaStatus, + schema: UPID_SCHEMA, + }, + access: { + permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_READ, false), }, )] -/// Get drive/media status -pub fn status(drive: String) -> Result { +/// 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)?; - let (config, _digest) = config::drive::config()?; + drive.rewind()?; - let drive_config: LinuxTapeDrive = config.lookup("linux", &drive)?; + let media_id = match drive.read_label()? { + (Some(media_id), key_config) => { + worker.log(format!( + "found media label: {}", + serde_json::to_string_pretty(&serde_json::to_value(&media_id)?)? + )); + if key_config.is_some() { + worker.log(format!( + "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 mut handle = drive_config.open() - .map_err(|err| format_err!("open drive '{}' ({}) failed - {}", drive, drive_config.path, err))?; + let status_path = Path::new(TAPE_STATUS_DIR); - let drive_status = handle.get_drive_status()?; + let mut inventory = Inventory::new(status_path); - let mam = handle.cartridge_memory()?; + let (_media_set_lock, media_set_uuid) = match media_id.media_set_label { + None => { + worker.log("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 + worker.log("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())); - let usage = mam_extract_media_usage(&mam)?; + drive.set_encryption(encrypt_fingerprint)?; - let status = LinuxDriveAndMediaStatus { - blocksize: drive_status.blocksize, - density: drive_status.density, - status: format!("{:?}", drive_status.status), - file_number: drive_status.file_number, - block_number: drive_status.block_number, - manufactured: usage.manufactured, - bytes_read: usage.bytes_read, - bytes_written: usage.bytes_written, - }; + let _pool_lock = lock_media_pool(status_path, &set.pool)?; + let media_set_lock = lock_media_set(status_path, &set.uuid, None)?; - Ok(status) + 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] @@ -846,17 +1408,32 @@ pub const SUBDIRS: SubdirMap = &sorted!([ ( "barcode-label-media", &Router::new() - .put(&API_METHOD_BARCODE_LABEL_MEDIA) + .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() - .put(&API_METHOD_EJECT_MEDIA) + .post(&API_METHOD_EJECT_MEDIA) + ), + ( + "format-media", + &Router::new() + .post(&API_METHOD_FORMAT_MEDIA) ), ( - "erase-media", + "export-media", &Router::new() - .put(&API_METHOD_ERASE_MEDIA) + .put(&API_METHOD_EXPORT_MEDIA) ), ( "inventory", @@ -867,17 +1444,27 @@ pub const SUBDIRS: SubdirMap = &sorted!([ ( "label-media", &Router::new() - .put(&API_METHOD_LABEL_MEDIA) + .post(&API_METHOD_LABEL_MEDIA) + ), + ( + "load-media", + &Router::new() + .post(&API_METHOD_LOAD_MEDIA) ), ( "load-slot", &Router::new() - .put(&API_METHOD_LOAD_SLOT) + .post(&API_METHOD_LOAD_SLOT) ), ( "cartridge-memory", &Router::new() - .put(&API_METHOD_CARTRIDGE_MEMORY) + .get(&API_METHOD_CARTRIDGE_MEMORY) + ), + ( + "volume-statistics", + &Router::new() + .get(&API_METHOD_VOLUME_STATISTICS) ), ( "read-label", @@ -885,14 +1472,14 @@ pub const SUBDIRS: SubdirMap = &sorted!([ .get(&API_METHOD_READ_LABEL) ), ( - "rewind", + "restore-key", &Router::new() - .put(&API_METHOD_REWIND) + .post(&API_METHOD_RESTORE_KEY) ), ( - "scan", + "rewind", &Router::new() - .get(&API_METHOD_SCAN_DRIVES) + .post(&API_METHOD_REWIND) ), ( "status", @@ -902,10 +1489,14 @@ pub const SUBDIRS: SubdirMap = &sorted!([ ( "unload", &Router::new() - .put(&API_METHOD_UNLOAD) + .post(&API_METHOD_UNLOAD) ), ]); -pub const ROUTER: Router = Router::new() +const ITEM_ROUTER: Router = Router::new() .get(&list_subdirs_api_method!(SUBDIRS)) - .subdirs(SUBDIRS); + .subdirs(&SUBDIRS); + +pub const ROUTER: Router = Router::new() + .get(&API_METHOD_LIST_DRIVES) + .match_all("drive", &ITEM_ROUTER);