1 use std
::collections
::HashMap
;
2 use std
::panic
::UnwindSafe
;
5 use anyhow
::{bail, format_err, Error}
;
6 use pbs_tape
::sg_tape
::SgTape
;
8 use tracing
::{info, warn}
;
11 list_subdirs_api_method
, Permission
, Router
, RpcEnvironment
, RpcEnvironmentType
, SubdirMap
,
13 use proxmox_schema
::api
;
14 use proxmox_section_config
::SectionConfigData
;
15 use proxmox_sortable_macro
::sortable
;
16 use proxmox_uuid
::Uuid
;
19 Authid
, DriveListEntry
, LabelUuidMap
, Lp17VolumeStatistics
, LtoDriveAndMediaStatus
,
20 LtoTapeDrive
, MamAttribute
, MediaIdFlat
, TapeDensity
, CHANGER_NAME_SCHEMA
, DRIVE_NAME_SCHEMA
,
21 MEDIA_LABEL_SCHEMA
, MEDIA_POOL_NAME_SCHEMA
, UPID_SCHEMA
,
24 use pbs_api_types
::{PRIV_TAPE_AUDIT, PRIV_TAPE_READ, PRIV_TAPE_WRITE}
;
26 use pbs_config
::CachedUserInfo
;
28 linux_list_drives
::{lookup_device_identification, lto_tape_device_list, open_lto_tape_device}
,
29 sg_tape
::tape_alert_flags_critical
,
32 use proxmox_rest_server
::WorkerTask
;
35 api2
::tape
::restore
::{fast_catalog_restore, restore_media}
,
37 changer
::update_changer_online_status
,
39 get_tape_device_state
, lock_tape_device
, media_changer
, open_drive
,
40 required_media_changer
, set_tape_device_state
, LtoTapeHandle
, TapeDriver
,
42 encryption_keys
::insert_key
,
43 file_formats
::{MediaLabel, MediaSetLabel}
,
44 lock_media_pool
, lock_media_set
, lock_unassigned_media_pool
, Inventory
, MediaCatalog
,
45 MediaId
, TAPE_STATUS_DIR
,
49 fn run_drive_worker
<F
>(
50 rpcenv
: &dyn RpcEnvironment
,
53 job_id
: Option
<String
>,
55 ) -> Result
<String
, Error
>
60 + FnOnce(Arc
<WorkerTask
>, SectionConfigData
) -> Result
<(), Error
>,
62 // early check/lock before starting worker
63 let (config
, _digest
) = pbs_config
::drive
::config()?
;
64 let lock_guard
= lock_tape_device(&config
, &drive
)?
;
66 let auth_id
= rpcenv
.get_auth_id().unwrap();
67 let to_stdout
= rpcenv
.env_type() == RpcEnvironmentType
::CLI
;
69 WorkerTask
::new_thread(worker_type
, job_id
, auth_id
, to_stdout
, move |worker
| {
70 let _lock_guard
= lock_guard
;
71 set_tape_device_state(&drive
, &worker
.upid().to_string())
72 .map_err(|err
| format_err
!("could not set tape device state: {}", err
))?
;
74 let result
= f(worker
, config
);
75 set_tape_device_state(&drive
, "")
76 .map_err(|err
| format_err
!("could not unset tape device state: {}", err
))?
;
81 async
fn run_drive_blocking_task
<F
, R
>(drive
: String
, state
: String
, f
: F
) -> Result
<R
, Error
>
83 F
: Send
+ '
static + FnOnce(SectionConfigData
) -> Result
<R
, Error
>,
86 // early check/lock before starting worker
87 let (config
, _digest
) = pbs_config
::drive
::config()?
;
88 let lock_guard
= lock_tape_device(&config
, &drive
)?
;
89 tokio
::task
::spawn_blocking(move || {
90 let _lock_guard
= lock_guard
;
91 set_tape_device_state(&drive
, &state
)
92 .map_err(|err
| format_err
!("could not set tape device state: {}", err
))?
;
93 let result
= f(config
);
94 set_tape_device_state(&drive
, "")
95 .map_err(|err
| format_err
!("could not unset tape device state: {}", err
))?
;
105 schema
: DRIVE_NAME_SCHEMA
,
108 schema
: MEDIA_LABEL_SCHEMA
,
116 permission
: &Permission
::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_READ
, false),
119 /// Load media with specified label
121 /// Issue a media load request to the associated changer device.
125 rpcenv
: &mut dyn RpcEnvironment
,
126 ) -> Result
<Value
, Error
> {
127 let job_id
= format
!("{}:{}", drive
, label_text
);
129 let upid_str
= run_drive_worker(
134 move |_worker
, config
| {
135 info
!("loading media '{label_text}' into drive '{drive}'");
136 let (mut changer
, _
) = required_media_changer(&config
, &drive
)?
;
137 changer
.load_media(&label_text
)?
;
149 schema
: DRIVE_NAME_SCHEMA
,
152 description
: "Source slot number.",
158 permission
: &Permission
::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_READ
, false),
161 /// Load media from the specified slot
163 /// Issue a media load request to the associated changer device.
164 pub async
fn load_slot(drive
: String
, source_slot
: u64) -> Result
<(), Error
> {
165 run_drive_blocking_task(
167 format
!("load from slot {}", source_slot
),
169 let (mut changer
, _
) = required_media_changer(&config
, &drive
)?
;
170 changer
.load_media_from_slot(source_slot
)?
;
181 schema
: DRIVE_NAME_SCHEMA
,
184 schema
: MEDIA_LABEL_SCHEMA
,
189 description
: "The import-export slot number the media was transferred to.",
194 permission
: &Permission
::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_READ
, false),
197 /// Export media with specified label
198 pub async
fn export_media(drive
: String
, label_text
: String
) -> Result
<u64, Error
> {
199 run_drive_blocking_task(
201 format
!("export media {}", label_text
),
203 let (mut changer
, changer_name
) = required_media_changer(&config
, &drive
)?
;
204 match changer
.export_media(&label_text
)?
{
205 Some(slot
) => Ok(slot
),
207 "media '{}' is not online (via changer '{}')",
221 schema
: DRIVE_NAME_SCHEMA
,
224 description
: "Target slot number. If omitted, defaults to the slot that the drive was loaded from.",
234 permission
: &Permission
::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_READ
, false),
237 /// Unload media via changer
240 target_slot
: Option
<u64>,
241 rpcenv
: &mut dyn RpcEnvironment
,
242 ) -> Result
<Value
, Error
> {
243 let upid_str
= run_drive_worker(
248 move |_worker
, config
| {
249 info
!("unloading media from drive '{drive}'");
251 let (mut changer
, _
) = required_media_changer(&config
, &drive
)?
;
252 changer
.unload_media(target_slot
)?
;
264 schema
: DRIVE_NAME_SCHEMA
,
267 description
: "Use fast erase.",
273 schema
: MEDIA_LABEL_SCHEMA
,
282 permission
: &Permission
::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_WRITE
, false),
285 /// Format media. Check for label-text if given (cancels if wrong media).
289 label_text
: Option
<String
>,
290 rpcenv
: &mut dyn RpcEnvironment
,
291 ) -> Result
<Value
, Error
> {
292 let upid_str
= run_drive_worker(
297 move |_worker
, config
| {
298 if let Some(ref label
) = label_text
{
299 info
!("try to load media '{label}'");
300 if let Some((mut changer
, _
)) = media_changer(&config
, &drive
)?
{
301 changer
.load_media(label
)?
;
305 let mut handle
= open_drive(&config
, &drive
)?
;
307 if !fast
.unwrap_or(true) {
308 let drive_config
: LtoTapeDrive
= config
.lookup("lto", &drive
)?
;
309 let file
= open_lto_tape_device(&drive_config
.path
)?
;
310 let mut handle
= LtoTapeHandle
::new(file
)?
;
311 if let Ok(status
) = handle
.get_drive_and_media_status() {
312 if status
.density
>= TapeDensity
::LTO9
{
313 info
!("Slow formatting LTO9+ media.");
314 info
!("This can take a very long time due to media optimization.");
319 match handle
.read_label() {
321 if let Some(label
) = label_text
{
322 bail
!("expected label '{}', found unrelated data", label
);
324 /* assume drive contains no or unrelated data */
325 info
!("unable to read media label: {err}");
326 info
!("format anyways");
327 handle
.format_media(fast
.unwrap_or(true))?
;
330 if let Some(label
) = label_text
{
331 bail
!("expected label '{}', found empty tape", label
);
333 info
!("found empty media - format anyways");
334 handle
.format_media(fast
.unwrap_or(true))?
;
336 Ok((Some(media_id
), _key_config
)) => {
337 if let Some(label_text
) = label_text
{
338 if media_id
.label
.label_text
!= label_text
{
340 "expected label '{}', found '{}', aborting",
342 media_id
.label
.label_text
348 "found media '{}' with uuid '{}'",
349 media_id
.label
.label_text
, media_id
.label
.uuid
,
352 let mut inventory
= Inventory
::new(TAPE_STATUS_DIR
);
354 let _pool_lock
= if let Some(pool
) = media_id
.pool() {
355 lock_media_pool(TAPE_STATUS_DIR
, &pool
)?
357 lock_unassigned_media_pool(TAPE_STATUS_DIR
)?
360 let _media_set_lock
= match media_id
.media_set_label
{
361 Some(MediaSetLabel { ref uuid, .. }
) => {
362 Some(lock_media_set(TAPE_STATUS_DIR
, uuid
, None
)?
)
367 MediaCatalog
::destroy(TAPE_STATUS_DIR
, &media_id
.label
.uuid
)?
;
368 inventory
.remove_media(&media_id
.label
.uuid
)?
;
369 drop(_media_set_lock
);
372 handle
.format_media(fast
.unwrap_or(true))?
;
387 schema
: DRIVE_NAME_SCHEMA
,
395 permission
: &Permission
::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_READ
, false),
399 pub fn rewind(drive
: String
, rpcenv
: &mut dyn RpcEnvironment
) -> Result
<Value
, Error
> {
400 let upid_str
= run_drive_worker(
405 move |_worker
, config
| {
406 let mut drive
= open_drive(&config
, &drive
)?
;
419 schema
: DRIVE_NAME_SCHEMA
,
427 permission
: &Permission
::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_READ
, false),
430 /// Eject/Unload drive media
431 pub fn eject_media(drive
: String
, rpcenv
: &mut dyn RpcEnvironment
) -> Result
<Value
, Error
> {
432 let upid_str
= run_drive_worker(
437 move |_worker
, config
| {
438 if let Some((mut changer
, _
)) = media_changer(&config
, &drive
)?
{
439 changer
.unload_media(None
)?
;
441 let mut drive
= open_drive(&config
, &drive
)?
;
442 drive
.eject_media()?
;
455 schema
: DRIVE_NAME_SCHEMA
,
458 schema
: MEDIA_LABEL_SCHEMA
,
461 schema
: MEDIA_POOL_NAME_SCHEMA
,
470 permission
: &Permission
::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_WRITE
, false),
475 /// Write a new media label to the media in 'drive'. The media is
476 /// assigned to the specified 'pool', or else to the free media pool.
478 /// Note: The media need to be empty (you may want to format it first).
481 pool
: Option
<String
>,
483 rpcenv
: &mut dyn RpcEnvironment
,
484 ) -> Result
<Value
, Error
> {
485 if let Some(ref pool
) = pool
{
486 let (pool_config
, _digest
) = pbs_config
::media_pool
::config()?
;
488 if !pool_config
.sections
.contains_key(pool
) {
489 bail
!("no such pool ('{}')", pool
);
492 let upid_str
= run_drive_worker(
497 move |_worker
, config
| {
498 let mut drive
= open_drive(&config
, &drive
)?
;
502 match drive
.read_next_file() {
503 Ok(_reader
) => bail
!("media is not empty (format it first)"),
504 Err(BlockReadError
::EndOfFile
) => { /* EOF mark at BOT, assume tape is empty */ }
505 Err(BlockReadError
::EndOfStream
) => { /* tape is empty */ }
507 bail
!("media read error - {}", err
);
511 let ctime
= proxmox_time
::epoch_i64();
512 let label
= MediaLabel
{
513 label_text
: label_text
.to_string(),
514 uuid
: Uuid
::generate(),
519 write_media_label(&mut drive
, label
, pool
)
526 fn write_media_label(
527 drive
: &mut Box
<dyn TapeDriver
>,
529 pool
: Option
<String
>,
530 ) -> Result
<(), Error
> {
531 let mut inventory
= Inventory
::new(TAPE_STATUS_DIR
);
534 .find_media_by_label_text(&label
.label_text
)?
537 bail
!("Media with label '{}' already exists", label
.label_text
);
539 drive
.label_tape(&label
)?
;
540 if let Some(ref pool
) = pool
{
541 info
!("Label media '{}' for pool '{pool}'", label
.label_text
);
543 info
!("Label media '{}' (no pool assignment)", label
.label_text
);
546 let media_id
= MediaId
{
548 media_set_label
: None
,
551 // Create the media catalog
552 MediaCatalog
::overwrite(TAPE_STATUS_DIR
, &media_id
, false)?
;
553 inventory
.store(media_id
.clone(), false)?
;
557 match drive
.read_label() {
558 Ok((Some(info
), _
)) => {
559 if info
.label
.uuid
!= media_id
.label
.uuid
{
560 bail
!("verify label failed - got wrong label uuid");
562 if let Some(ref pool
) = pool
{
563 match (info
.label
.pool
, info
.media_set_label
) {
564 (None
, Some(set
)) => {
565 if !set
.unassigned() {
566 bail
!("verify media set label failed - got wrong set uuid");
568 if &set
.pool
!= pool
{
569 bail
!("verify media set label failed - got wrong pool");
572 (Some(initial_pool
), _
) => {
573 if initial_pool
!= *pool
{
574 bail
!("verify media label failed - got wrong pool");
578 bail
!("verify media set label failed (missing set label)");
583 Ok((None
, _
)) => bail
!("verify label failed (got empty media)"),
584 Err(err
) => bail
!("verify label failed - {}", err
),
589 drive
.write_additional_attributes(Some(media_id
.label
.label_text
), pool
);
599 schema
: DRIVE_NAME_SCHEMA
,
600 //description: "Restore the key from this drive the (encrypted) key was saved on.",
603 description
: "The password the key was encrypted with.",
608 permission
: &Permission
::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_READ
, false),
611 /// Try to restore a tape encryption key
612 pub async
fn restore_key(drive
: String
, password
: String
) -> Result
<(), Error
> {
613 run_drive_blocking_task(drive
.clone(), "restore key".to_string(), move |config
| {
614 let mut drive
= open_drive(&config
, &drive
)?
;
616 let (_media_id
, key_config
) = drive
.read_label_without_loading_key()?
;
618 if let Some(key_config
) = key_config
{
619 let password_fn
= || Ok(password
.as_bytes().to_vec());
620 let (key
, ..) = key_config
.decrypt(&password_fn
)?
;
621 insert_key(key
, key_config
, true)?
;
623 bail
!("media does not contain any encryption key configuration");
637 schema
: DRIVE_NAME_SCHEMA
,
640 description
: "Inventorize media",
649 permission
: &Permission
::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_READ
, false),
652 /// Read media label (optionally inventorize media)
653 pub async
fn read_label(drive
: String
, inventorize
: Option
<bool
>) -> Result
<MediaIdFlat
, Error
> {
654 run_drive_blocking_task(drive
.clone(), "reading label".to_string(), move |config
| {
655 let mut drive
= open_drive(&config
, &drive
)?
;
657 let (media_id
, _key_config
) = drive
.read_label()?
;
658 let media_id
= media_id
.ok_or_else(|| format_err
!("Media is empty (no label)."))?
;
660 let label
= if let Some(ref set
) = media_id
.media_set_label
{
661 let key
= &set
.encryption_key_fingerprint
;
664 ctime
: media_id
.label
.ctime
,
665 encryption_key_fingerprint
: key
.as_ref().map(|fp
| fp
.signature()),
666 label_text
: media_id
.label
.label_text
.clone(),
667 media_set_ctime
: Some(set
.ctime
),
668 media_set_uuid
: Some(set
.uuid
.clone()),
669 pool
: Some(set
.pool
.clone()),
670 seq_nr
: Some(set
.seq_nr
),
671 uuid
: media_id
.label
.uuid
.clone(),
675 ctime
: media_id
.label
.ctime
,
676 encryption_key_fingerprint
: None
,
677 label_text
: media_id
.label
.label_text
.clone(),
678 media_set_ctime
: None
,
679 media_set_uuid
: None
,
680 pool
: media_id
.label
.pool
.clone(),
682 uuid
: media_id
.label
.uuid
.clone(),
686 if let Some(true) = inventorize
{
687 let mut inventory
= Inventory
::new(TAPE_STATUS_DIR
);
689 let _pool_lock
= if let Some(pool
) = media_id
.pool() {
690 lock_media_pool(TAPE_STATUS_DIR
, &pool
)?
692 lock_unassigned_media_pool(TAPE_STATUS_DIR
)?
695 if let Some(MediaSetLabel { ref uuid, .. }
) = media_id
.media_set_label
{
696 let _lock
= lock_media_set(TAPE_STATUS_DIR
, uuid
, None
)?
;
697 MediaCatalog
::destroy_unrelated_catalog(TAPE_STATUS_DIR
, &media_id
)?
;
699 MediaCatalog
::destroy(TAPE_STATUS_DIR
, &media_id
.label
.uuid
)?
;
702 inventory
.store(media_id
, false)?
;
714 schema
: DRIVE_NAME_SCHEMA
,
722 permission
: &Permission
::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_READ
, false),
726 pub fn clean_drive(drive
: String
, rpcenv
: &mut dyn RpcEnvironment
) -> Result
<Value
, Error
> {
727 let upid_str
= run_drive_worker(
732 move |_worker
, config
| {
733 let (mut changer
, _changer_name
) = required_media_changer(&config
, &drive
)?
;
735 info
!("Starting drive clean");
737 changer
.clean_drive()?
;
739 if let Ok(drive_config
) = config
.lookup
::<LtoTapeDrive
>("lto", &drive
) {
740 // Note: clean_drive unloads the cleaning media, so we cannot use drive_config.open
741 let mut handle
= LtoTapeHandle
::new(open_lto_tape_device(&drive_config
.path
)?
)?
;
743 // test for critical tape alert flags
744 if let Ok(alert_flags
) = handle
.tape_alert_flags() {
745 if !alert_flags
.is_empty() {
746 info
!("TapeAlertFlags: {alert_flags:?}");
747 if tape_alert_flags_critical(alert_flags
) {
748 bail
!("found critical tape alert flags: {:?}", alert_flags
);
753 // test wearout (max. 50 mounts)
754 if let Ok(volume_stats
) = handle
.volume_statistics() {
755 info
!("Volume mounts: {}", volume_stats
.volume_mounts
);
756 let wearout
= volume_stats
.volume_mounts
* 2; // (*100.0/50.0);
757 info
!("Cleaning tape wearout: {wearout}%");
761 info
!("Drive cleaned successfully");
774 schema
: DRIVE_NAME_SCHEMA
,
779 description
: "The list of media labels with associated media Uuid (if any).",
786 permission
: &Permission
::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_READ
, false),
789 /// List known media labels (Changer Inventory)
791 /// Note: Only useful for drives with associated changer device.
793 /// This method queries the changer to get a list of media labels.
795 /// Note: This updates the media online status.
796 pub async
fn inventory(drive
: String
) -> Result
<Vec
<LabelUuidMap
>, Error
> {
797 run_drive_blocking_task(drive
.clone(), "inventorize".to_string(), move |config
| {
798 let (mut changer
, changer_name
) = required_media_changer(&config
, &drive
)?
;
800 let label_text_list
= changer
.online_media_label_texts()?
;
802 let mut inventory
= Inventory
::load(TAPE_STATUS_DIR
)?
;
804 update_changer_online_status(&config
, &mut inventory
, &changer_name
, &label_text_list
)?
;
806 let mut list
= Vec
::new();
808 for label_text
in label_text_list
.iter() {
809 if label_text
.starts_with("CLN") {
810 // skip cleaning unit
814 let label_text
= label_text
.to_string();
816 match inventory
.find_media_by_label_text(&label_text
) {
817 Ok(Some(media_id
)) => {
818 list
.push(LabelUuidMap
{
820 uuid
: Some(media_id
.label
.uuid
.clone()),
824 list
.push(LabelUuidMap
{
830 log
::warn
!("error getting unique media label: {err}");
831 list
.push(LabelUuidMap
{
848 schema
: DRIVE_NAME_SCHEMA
,
851 description
: "Load all tapes and try read labels (even if already inventoried)",
857 description
: "Restore the catalog from tape.",
868 permission
: &Permission
::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_READ
, false),
873 /// Note: Only useful for drives with associated changer device.
875 /// This method queries the changer to get a list of media labels. It
876 /// then loads any unknown media into the drive, reads the label, and
877 /// store the result to the media database.
879 /// If `catalog` is true, also tries to restore the catalog from tape.
881 /// Note: This updates the media online status.
882 pub fn update_inventory(
884 read_all_labels
: bool
,
886 rpcenv
: &mut dyn RpcEnvironment
,
887 ) -> Result
<Value
, Error
> {
888 let upid_str
= run_drive_worker(
893 move |_worker
, config
| {
894 let (mut changer
, changer_name
) = required_media_changer(&config
, &drive
)?
;
896 let label_text_list
= changer
.online_media_label_texts()?
;
897 if label_text_list
.is_empty() {
898 info
!("changer device does not list any media labels");
901 let mut inventory
= Inventory
::load(TAPE_STATUS_DIR
)?
;
903 update_changer_online_status(&config
, &mut inventory
, &changer_name
, &label_text_list
)?
;
905 for label_text
in label_text_list
.iter() {
906 if label_text
.starts_with("CLN") {
907 info
!("skip cleaning unit '{label_text}'");
911 let label_text
= label_text
.to_string();
913 if !read_all_labels
{
914 match inventory
.find_media_by_label_text(&label_text
) {
915 Ok(Some(media_id
)) => {
917 || MediaCatalog
::exists(TAPE_STATUS_DIR
, &media_id
.label
.uuid
)
919 info
!("media '{label_text}' already inventoried");
924 warn
!("error getting media by unique label: {err}");
925 // we can't be sure which uuid it is
928 Ok(None
) => {}
// ok to inventorize
932 if let Err(err
) = changer
.load_media(&label_text
) {
933 warn
!("unable to load media '{label_text}' - {err}");
937 let mut drive
= open_drive(&config
, &drive
)?
;
938 match drive
.read_label() {
940 warn
!("unable to read label form media '{label_text}' - {err}");
943 info
!("media '{label_text}' is empty");
945 Ok((Some(media_id
), _key_config
)) => {
946 if label_text
!= media_id
.label
.label_text
{
948 "label text mismatch ({label_text} != {})",
949 media_id
.label
.label_text
954 "inventorize media '{label_text}' with uuid '{}'",
958 let _pool_lock
= if let Some(pool
) = media_id
.pool() {
959 lock_media_pool(TAPE_STATUS_DIR
, &pool
)?
961 lock_unassigned_media_pool(TAPE_STATUS_DIR
)?
964 if let Some(ref set
) = media_id
.media_set_label
{
965 let _lock
= lock_media_set(TAPE_STATUS_DIR
, &set
.uuid
, None
)?
;
966 MediaCatalog
::destroy_unrelated_catalog(TAPE_STATUS_DIR
, &media_id
)?
;
967 inventory
.store(media_id
.clone(), false)?
;
969 if set
.unassigned() {
974 let media_set
= inventory
.compute_media_set_members(&set
.uuid
)?
;
975 if let Err(err
) = fast_catalog_restore(
978 &media_id
.label
.uuid
,
980 warn
!("could not restore catalog for {label_text}: {err}");
984 MediaCatalog
::destroy(TAPE_STATUS_DIR
, &media_id
.label
.uuid
)?
;
985 inventory
.store(media_id
, false)?
;
989 changer
.unload_media(None
)?
;
1002 schema
: DRIVE_NAME_SCHEMA
,
1005 schema
: MEDIA_POOL_NAME_SCHEMA
,
1011 schema
: UPID_SCHEMA
,
1014 permission
: &Permission
::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_WRITE
, false),
1017 /// Label media with barcodes from changer device
1018 pub fn barcode_label_media(
1020 pool
: Option
<String
>,
1021 rpcenv
: &mut dyn RpcEnvironment
,
1022 ) -> Result
<Value
, Error
> {
1023 if let Some(ref pool
) = pool
{
1024 let (pool_config
, _digest
) = pbs_config
::media_pool
::config()?
;
1026 if !pool_config
.sections
.contains_key(pool
) {
1027 bail
!("no such pool ('{}')", pool
);
1031 let upid_str
= run_drive_worker(
1034 "barcode-label-media",
1035 Some(drive
.clone()),
1036 move |_worker
, config
| barcode_label_media_worker(drive
, &config
, pool
),
1042 fn barcode_label_media_worker(
1044 drive_config
: &SectionConfigData
,
1045 pool
: Option
<String
>,
1046 ) -> Result
<(), Error
> {
1047 let (mut changer
, changer_name
) = required_media_changer(drive_config
, &drive
)?
;
1049 let mut label_text_list
= changer
.online_media_label_texts()?
;
1051 // make sure we label them in the right order
1052 label_text_list
.sort();
1054 let mut inventory
= Inventory
::load(TAPE_STATUS_DIR
)?
;
1056 update_changer_online_status(
1063 if label_text_list
.is_empty() {
1064 bail
!("changer device does not list any media labels");
1067 for label_text
in label_text_list
{
1068 if label_text
.starts_with("CLN") {
1072 inventory
.reload()?
;
1073 match inventory
.find_media_by_label_text(&label_text
) {
1075 info
!("media '{label_text}' already inventoried (already labeled)");
1079 warn
!("error getting media by unique label: {err}",);
1082 Ok(None
) => {}
// ok to label
1085 info
!("checking/loading media '{label_text}'");
1087 if let Err(err
) = changer
.load_media(&label_text
) {
1088 warn
!("unable to load media '{label_text}' - {err}");
1092 let mut drive
= open_drive(drive_config
, &drive
)?
;
1095 match drive
.read_next_file() {
1097 info
!("media '{label_text}' is not empty (format it first)");
1100 Err(BlockReadError
::EndOfFile
) => { /* EOF mark at BOT, assume tape is empty */ }
1101 Err(BlockReadError
::EndOfStream
) => { /* tape is empty */ }
1103 warn
!("media '{label_text}' read error (maybe not empty - format it first)");
1108 let ctime
= proxmox_time
::epoch_i64();
1109 let label
= MediaLabel
{
1110 label_text
: label_text
.to_string(),
1111 uuid
: Uuid
::generate(),
1116 write_media_label(&mut drive
, label
, pool
.clone())?
1126 schema
: DRIVE_NAME_SCHEMA
,
1131 description
: "A List of medium auxiliary memory attributes.",
1138 permission
: &Permission
::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_AUDIT
, false),
1141 /// Read Cartridge Memory (Medium auxiliary memory attributes)
1142 pub async
fn cartridge_memory(drive
: String
) -> Result
<Vec
<MamAttribute
>, Error
> {
1143 run_drive_blocking_task(
1145 "reading cartridge memory".to_string(),
1147 let drive_config
: LtoTapeDrive
= config
.lookup("lto", &drive
)?
;
1148 let mut handle
= LtoTapeHandle
::open_lto_drive(&drive_config
)?
;
1150 handle
.cartridge_memory()
1160 schema
: DRIVE_NAME_SCHEMA
,
1165 type: Lp17VolumeStatistics
,
1168 permission
: &Permission
::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_AUDIT
, false),
1171 /// Read Volume Statistics (SCSI log page 17h)
1172 pub async
fn volume_statistics(drive
: String
) -> Result
<Lp17VolumeStatistics
, Error
> {
1173 run_drive_blocking_task(
1175 "reading volume statistics".to_string(),
1177 let drive_config
: LtoTapeDrive
= config
.lookup("lto", &drive
)?
;
1178 let mut handle
= LtoTapeHandle
::open_lto_drive(&drive_config
)?
;
1180 handle
.volume_statistics()
1190 schema
: DRIVE_NAME_SCHEMA
,
1195 type: LtoDriveAndMediaStatus
,
1198 permission
: &Permission
::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_AUDIT
, false),
1201 /// Get drive/media status
1202 pub async
fn status(drive
: String
) -> Result
<LtoDriveAndMediaStatus
, Error
> {
1203 run_drive_blocking_task(
1205 "reading drive status".to_string(),
1207 let drive_config
: LtoTapeDrive
= config
.lookup("lto", &drive
)?
;
1209 // Note: use open_lto_tape_device, because this also works if no medium loaded
1210 let file
= open_lto_tape_device(&drive_config
.path
)?
;
1212 let mut handle
= LtoTapeHandle
::new(file
)?
;
1214 handle
.get_drive_and_media_status()
1224 schema
: DRIVE_NAME_SCHEMA
,
1227 description
: "Force overriding existing index.",
1232 description
: "Re-read the whole tape to reconstruct the catalog instead of restoring saved versions.",
1237 description
: "Verbose mode - log all found chunks.",
1244 schema
: UPID_SCHEMA
,
1247 permission
: &Permission
::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_READ
, false),
1250 /// Scan media and record content
1251 pub fn catalog_media(
1253 force
: Option
<bool
>,
1255 verbose
: Option
<bool
>,
1256 rpcenv
: &mut dyn RpcEnvironment
,
1257 ) -> Result
<Value
, Error
> {
1258 let verbose
= verbose
.unwrap_or(false);
1259 let force
= force
.unwrap_or(false);
1260 let scan
= scan
.unwrap_or(false);
1261 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
1263 let upid_str
= run_drive_worker(
1267 Some(drive
.clone()),
1268 move |worker
, config
| {
1269 let mut drive
= open_drive(&config
, &drive
)?
;
1273 let media_id
= match drive
.read_label()?
{
1274 (Some(media_id
), key_config
) => {
1276 "found media label: {}",
1277 serde_json
::to_string_pretty(&serde_json
::to_value(&media_id
)?
)?
1279 if key_config
.is_some() {
1281 "encryption key config: {}",
1282 serde_json
::to_string_pretty(&serde_json
::to_value(&key_config
)?
)?
1287 (None
, _
) => bail
!("media is empty (no media label found)"),
1290 let mut inventory
= Inventory
::new(TAPE_STATUS_DIR
);
1292 let (_media_set_lock
, media_set_uuid
) = match media_id
.media_set_label
{
1294 info
!("media is empty");
1295 let _pool_lock
= if let Some(pool
) = media_id
.pool() {
1296 lock_media_pool(TAPE_STATUS_DIR
, &pool
)?
1298 lock_unassigned_media_pool(TAPE_STATUS_DIR
)?
1300 MediaCatalog
::destroy(TAPE_STATUS_DIR
, &media_id
.label
.uuid
)?
;
1301 inventory
.store(media_id
.clone(), false)?
;
1305 if set
.unassigned() {
1307 info
!("media is empty");
1308 let _lock
= lock_unassigned_media_pool(TAPE_STATUS_DIR
)?
;
1309 MediaCatalog
::destroy(TAPE_STATUS_DIR
, &media_id
.label
.uuid
)?
;
1310 inventory
.store(media_id
.clone(), false)?
;
1314 let _pool_lock
= lock_media_pool(TAPE_STATUS_DIR
, &set
.pool
)?
;
1315 let media_set_lock
= lock_media_set(TAPE_STATUS_DIR
, &set
.uuid
, None
)?
;
1317 MediaCatalog
::destroy_unrelated_catalog(TAPE_STATUS_DIR
, &media_id
)?
;
1319 inventory
.store(media_id
.clone(), false)?
;
1321 (media_set_lock
, &set
.uuid
)
1325 if MediaCatalog
::exists(TAPE_STATUS_DIR
, &media_id
.label
.uuid
) && !force
{
1326 bail
!("media catalog exists (please use --force to overwrite)");
1330 let media_set
= inventory
.compute_media_set_members(media_set_uuid
)?
;
1332 if fast_catalog_restore(&mut drive
, &media_set
, &media_id
.label
.uuid
)?
{
1336 info
!("no catalog found");
1339 info
!("scanning entire media to reconstruct catalog");
1342 drive
.read_label()?
; // skip over labels - we already read them above
1344 let mut checked_chunks
= HashMap
::new();
1350 &mut checked_chunks
,
1366 schema
: CHANGER_NAME_SCHEMA
,
1371 description
: "If true, queries and returns the drive activity for each drive.",
1378 description
: "The list of configured drives with model information.",
1381 type: DriveListEntry
,
1385 description
: "List configured tape drives filtered by Tape.Audit privileges",
1386 permission
: &Permission
::Anybody
,
1391 changer
: Option
<String
>,
1392 query_activity
: bool
,
1394 rpcenv
: &mut dyn RpcEnvironment
,
1395 ) -> Result
<Vec
<DriveListEntry
>, Error
> {
1396 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
1397 let user_info
= CachedUserInfo
::new()?
;
1399 let (config
, _
) = pbs_config
::drive
::config()?
;
1401 let lto_drives
= lto_tape_device_list();
1403 let drive_list
: Vec
<LtoTapeDrive
> = config
.convert_to_typed_array("lto")?
;
1405 let mut list
= Vec
::new();
1407 for drive
in drive_list
{
1408 if changer
.is_some() && drive
.changer
!= changer
{
1412 let privs
= user_info
.lookup_privs(&auth_id
, &["tape", "drive", &drive
.name
]);
1413 if (privs
& PRIV_TAPE_AUDIT
) == 0 {
1417 let info
= lookup_device_identification(<o_drives
, &drive
.path
);
1418 let state
= get_tape_device_state(&config
, &drive
.name
)?
;
1419 let activity
= if query_activity
{
1420 SgTape
::device_activity(&drive
).ok()
1424 let entry
= DriveListEntry
{
1437 pub const SUBDIRS
: SubdirMap
= &sorted
!([
1439 "barcode-label-media",
1440 &Router
::new().post(&API_METHOD_BARCODE_LABEL_MEDIA
)
1442 ("catalog", &Router
::new().post(&API_METHOD_CATALOG_MEDIA
)),
1443 ("clean", &Router
::new().put(&API_METHOD_CLEAN_DRIVE
)),
1444 ("eject-media", &Router
::new().post(&API_METHOD_EJECT_MEDIA
)),
1447 &Router
::new().post(&API_METHOD_FORMAT_MEDIA
)
1449 ("export-media", &Router
::new().put(&API_METHOD_EXPORT_MEDIA
)),
1453 .get(&API_METHOD_INVENTORY
)
1454 .put(&API_METHOD_UPDATE_INVENTORY
)
1456 ("label-media", &Router
::new().post(&API_METHOD_LABEL_MEDIA
)),
1457 ("load-media", &Router
::new().post(&API_METHOD_LOAD_MEDIA
)),
1458 ("load-slot", &Router
::new().post(&API_METHOD_LOAD_SLOT
)),
1461 &Router
::new().get(&API_METHOD_CARTRIDGE_MEMORY
)
1464 "volume-statistics",
1465 &Router
::new().get(&API_METHOD_VOLUME_STATISTICS
)
1467 ("read-label", &Router
::new().get(&API_METHOD_READ_LABEL
)),
1468 ("restore-key", &Router
::new().post(&API_METHOD_RESTORE_KEY
)),
1469 ("rewind", &Router
::new().post(&API_METHOD_REWIND
)),
1470 ("status", &Router
::new().get(&API_METHOD_STATUS
)),
1471 ("unload", &Router
::new().post(&API_METHOD_UNLOAD
)),
1474 const ITEM_ROUTER
: Router
= Router
::new()
1475 .get(&list_subdirs_api_method
!(SUBDIRS
))
1478 pub const ROUTER
: Router
= Router
::new()
1479 .get(&API_METHOD_LIST_DRIVES
)
1480 .match_all("drive", &ITEM_ROUTER
);