1 use std
::panic
::UnwindSafe
;
4 use std
::collections
::HashMap
;
6 use anyhow
::{bail, format_err, Error}
;
9 use proxmox
::{sortable, identity}
;
11 list_subdirs_api_method
, Permission
, Router
, RpcEnvironment
, RpcEnvironmentType
, SubdirMap
,
13 use proxmox_schema
::api
;
14 use proxmox_section_config
::SectionConfigData
;
15 use proxmox_uuid
::Uuid
;
16 use proxmox_sys
::{task_log, task_warn}
;
19 UPID_SCHEMA
, CHANGER_NAME_SCHEMA
, DRIVE_NAME_SCHEMA
, MEDIA_LABEL_SCHEMA
, MEDIA_POOL_NAME_SCHEMA
,
20 Authid
, DriveListEntry
, LtoTapeDrive
, MediaIdFlat
, LabelUuidMap
, MamAttribute
,
21 LtoDriveAndMediaStatus
, Lp17VolumeStatistics
,
24 use pbs_api_types
::{PRIV_TAPE_AUDIT, PRIV_TAPE_READ, PRIV_TAPE_WRITE}
;
26 use pbs_config
::CachedUserInfo
;
29 sg_tape
::tape_alert_flags_critical
,
30 linux_list_drives
::{lto_tape_device_list, lookup_device_identification, open_lto_tape_device}
,
32 use proxmox_rest_server
::WorkerTask
;
35 api2
::tape
::restore
::{
46 lock_unassigned_media_pool
,
56 required_media_changer
,
59 set_tape_device_state
,
60 get_tape_device_state
,
62 changer
::update_changer_online_status
,
66 fn run_drive_worker
<F
>(
67 rpcenv
: &dyn RpcEnvironment
,
70 job_id
: Option
<String
>,
72 ) -> Result
<String
, Error
>
77 + FnOnce(Arc
<WorkerTask
>, SectionConfigData
) -> Result
<(), Error
>,
79 // early check/lock before starting worker
80 let (config
, _digest
) = pbs_config
::drive
::config()?
;
81 let lock_guard
= lock_tape_device(&config
, &drive
)?
;
83 let auth_id
= rpcenv
.get_auth_id().unwrap();
84 let to_stdout
= rpcenv
.env_type() == RpcEnvironmentType
::CLI
;
86 WorkerTask
::new_thread(worker_type
, job_id
, auth_id
, to_stdout
, move |worker
| {
87 let _lock_guard
= lock_guard
;
88 set_tape_device_state(&drive
, &worker
.upid().to_string())
89 .map_err(|err
| format_err
!("could not set tape device state: {}", err
))?
;
91 let result
= f(worker
, config
);
92 set_tape_device_state(&drive
, "")
93 .map_err(|err
| format_err
!("could not unset tape device state: {}", err
))?
;
98 async
fn run_drive_blocking_task
<F
, R
>(drive
: String
, state
: String
, f
: F
) -> Result
<R
, Error
>
100 F
: Send
+ '
static + FnOnce(SectionConfigData
) -> Result
<R
, Error
>,
103 // early check/lock before starting worker
104 let (config
, _digest
) = pbs_config
::drive
::config()?
;
105 let lock_guard
= lock_tape_device(&config
, &drive
)?
;
106 tokio
::task
::spawn_blocking(move || {
107 let _lock_guard
= lock_guard
;
108 set_tape_device_state(&drive
, &state
)
109 .map_err(|err
| format_err
!("could not set tape device state: {}", err
))?
;
110 let result
= f(config
);
111 set_tape_device_state(&drive
, "")
112 .map_err(|err
| format_err
!("could not unset tape device state: {}", err
))?
;
122 schema
: DRIVE_NAME_SCHEMA
,
125 schema
: MEDIA_LABEL_SCHEMA
,
133 permission
: &Permission
::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_READ
, false),
136 /// Load media with specified label
138 /// Issue a media load request to the associated changer device.
142 rpcenv
: &mut dyn RpcEnvironment
,
143 ) -> Result
<Value
, Error
> {
144 let job_id
= format
!("{}:{}", drive
, label_text
);
146 let upid_str
= run_drive_worker(
151 move |worker
, config
| {
152 task_log
!(worker
, "loading media '{}' into drive '{}'", label_text
, drive
);
153 let (mut changer
, _
) = required_media_changer(&config
, &drive
)?
;
154 changer
.load_media(&label_text
)?
;
166 schema
: DRIVE_NAME_SCHEMA
,
169 description
: "Source slot number.",
175 permission
: &Permission
::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_READ
, false),
178 /// Load media from the specified slot
180 /// Issue a media load request to the associated changer device.
181 pub async
fn load_slot(drive
: String
, source_slot
: u64) -> Result
<(), Error
> {
182 run_drive_blocking_task(
184 format
!("load from slot {}", source_slot
),
186 let (mut changer
, _
) = required_media_changer(&config
, &drive
)?
;
187 changer
.load_media_from_slot(source_slot
)?
;
198 schema
: DRIVE_NAME_SCHEMA
,
201 schema
: MEDIA_LABEL_SCHEMA
,
206 description
: "The import-export slot number the media was transferred to.",
211 permission
: &Permission
::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_READ
, false),
214 /// Export media with specified label
215 pub async
fn export_media(drive
: String
, label_text
: String
) -> Result
<u64, Error
> {
216 run_drive_blocking_task(
218 format
!("export media {}", label_text
),
220 let (mut changer
, changer_name
) = required_media_changer(&config
, &drive
)?
;
221 match changer
.export_media(&label_text
)?
{
222 Some(slot
) => Ok(slot
),
224 "media '{}' is not online (via changer '{}')",
238 schema
: DRIVE_NAME_SCHEMA
,
241 description
: "Target slot number. If omitted, defaults to the slot that the drive was loaded from.",
251 permission
: &Permission
::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_READ
, false),
254 /// Unload media via changer
257 target_slot
: Option
<u64>,
258 rpcenv
: &mut dyn RpcEnvironment
,
259 ) -> Result
<Value
, Error
> {
260 let upid_str
= run_drive_worker(
265 move |worker
, config
| {
266 task_log
!(worker
, "unloading media from drive '{}'", drive
);
268 let (mut changer
, _
) = required_media_changer(&config
, &drive
)?
;
269 changer
.unload_media(target_slot
)?
;
281 schema
: DRIVE_NAME_SCHEMA
,
284 description
: "Use fast erase.",
290 schema
: MEDIA_LABEL_SCHEMA
,
299 permission
: &Permission
::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_WRITE
, false),
302 /// Format media. Check for label-text if given (cancels if wrong media).
306 label_text
: Option
<String
>,
307 rpcenv
: &mut dyn RpcEnvironment
,
308 ) -> Result
<Value
, Error
> {
309 let upid_str
= run_drive_worker(
314 move |worker
, config
| {
315 if let Some(ref label
) = label_text
{
316 task_log
!(worker
, "try to load media '{}'", label
);
317 if let Some((mut changer
, _
)) = media_changer(&config
, &drive
)?
{
318 changer
.load_media(label
)?
;
322 let mut handle
= open_drive(&config
, &drive
)?
;
324 match handle
.read_label() {
326 if let Some(label
) = label_text
{
327 bail
!("expected label '{}', found unrelated data", label
);
329 /* assume drive contains no or unrelated data */
330 task_log
!(worker
, "unable to read media label: {}", err
);
331 task_log
!(worker
, "format anyways");
332 handle
.format_media(fast
.unwrap_or(true))?
;
335 if let Some(label
) = label_text
{
336 bail
!("expected label '{}', found empty tape", label
);
338 task_log
!(worker
, "found empty media - format anyways");
339 handle
.format_media(fast
.unwrap_or(true))?
;
341 Ok((Some(media_id
), _key_config
)) => {
342 if let Some(label_text
) = label_text
{
343 if media_id
.label
.label_text
!= label_text
{
345 "expected label '{}', found '{}', aborting",
347 media_id
.label
.label_text
354 "found media '{}' with uuid '{}'",
355 media_id
.label
.label_text
, media_id
.label
.uuid
,
358 let status_path
= Path
::new(TAPE_STATUS_DIR
);
359 let mut inventory
= Inventory
::new(status_path
);
361 if let Some(MediaSetLabel { ref pool, ref uuid, ..}
) = media_id
.media_set_label
{
362 let _pool_lock
= lock_media_pool(status_path
, pool
)?
;
363 let _media_set_lock
= lock_media_set(status_path
, uuid
, None
)?
;
364 MediaCatalog
::destroy(status_path
, &media_id
.label
.uuid
)?
;
365 inventory
.remove_media(&media_id
.label
.uuid
)?
;
367 let _lock
= lock_unassigned_media_pool(status_path
)?
;
368 MediaCatalog
::destroy(status_path
, &media_id
.label
.uuid
)?
;
369 inventory
.remove_media(&media_id
.label
.uuid
)?
;
372 handle
.format_media(fast
.unwrap_or(true))?
;
387 schema
: DRIVE_NAME_SCHEMA
,
395 permission
: &Permission
::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_READ
, false),
401 rpcenv
: &mut dyn RpcEnvironment
,
402 ) -> Result
<Value
, Error
> {
403 let upid_str
= run_drive_worker(
408 move |_worker
, config
| {
409 let mut drive
= open_drive(&config
, &drive
)?
;
422 schema
: DRIVE_NAME_SCHEMA
,
430 permission
: &Permission
::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_READ
, false),
433 /// Eject/Unload drive media
436 rpcenv
: &mut dyn RpcEnvironment
,
437 ) -> Result
<Value
, Error
> {
438 let upid_str
= run_drive_worker(
443 move |_worker
, config
| {
444 if let Some((mut changer
, _
)) = media_changer(&config
, &drive
)?
{
445 changer
.unload_media(None
)?
;
447 let mut drive
= open_drive(&config
, &drive
)?
;
448 drive
.eject_media()?
;
461 schema
: DRIVE_NAME_SCHEMA
,
464 schema
: MEDIA_LABEL_SCHEMA
,
467 schema
: MEDIA_POOL_NAME_SCHEMA
,
476 permission
: &Permission
::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_WRITE
, false),
481 /// Write a new media label to the media in 'drive'. The media is
482 /// assigned to the specified 'pool', or else to the free media pool.
484 /// Note: The media need to be empty (you may want to format it first).
487 pool
: Option
<String
>,
489 rpcenv
: &mut dyn RpcEnvironment
,
490 ) -> Result
<Value
, Error
> {
491 if let Some(ref pool
) = pool
{
492 let (pool_config
, _digest
) = pbs_config
::media_pool
::config()?
;
494 if pool_config
.sections
.get(pool
).is_none() {
495 bail
!("no such pool ('{}')", pool
);
498 let upid_str
= run_drive_worker(
503 move |worker
, config
| {
504 let mut drive
= open_drive(&config
, &drive
)?
;
508 match drive
.read_next_file() {
509 Ok(_reader
) => bail
!("media is not empty (format it first)"),
510 Err(BlockReadError
::EndOfFile
) => { /* EOF mark at BOT, assume tape is empty */ }
,
511 Err(BlockReadError
::EndOfStream
) => { /* tape is empty */ }
,
513 bail
!("media read error - {}", err
);
517 let ctime
= proxmox_time
::epoch_i64();
518 let label
= MediaLabel
{
519 label_text
: label_text
.to_string(),
520 uuid
: Uuid
::generate(),
524 write_media_label(worker
, &mut drive
, label
, pool
)
531 fn write_media_label(
532 worker
: Arc
<WorkerTask
>,
533 drive
: &mut Box
<dyn TapeDriver
>,
535 pool
: Option
<String
>,
536 ) -> Result
<(), Error
> {
538 drive
.label_tape(&label
)?
;
540 let status_path
= Path
::new(TAPE_STATUS_DIR
);
542 let media_id
= if let Some(ref pool
) = pool
{
543 // assign media to pool by writing special media set label
544 task_log
!(worker
, "Label media '{}' for pool '{}'", label
.label_text
, pool
);
545 let set
= MediaSetLabel
::with_data(&pool
, [0u8; 16].into(), 0, label
.ctime
, None
);
547 drive
.write_media_set_label(&set
, None
)?
;
549 let media_id
= MediaId { label, media_set_label: Some(set) }
;
551 // Create the media catalog
552 MediaCatalog
::overwrite(status_path
, &media_id
, false)?
;
554 let mut inventory
= Inventory
::new(status_path
);
555 inventory
.store(media_id
.clone(), false)?
;
559 task_log
!(worker
, "Label media '{}' (no pool assignment)", label
.label_text
);
561 let media_id
= MediaId { label, media_set_label: None }
;
563 // Create the media catalog
564 MediaCatalog
::overwrite(status_path
, &media_id
, false)?
;
566 let mut inventory
= Inventory
::new(status_path
);
567 inventory
.store(media_id
.clone(), false)?
;
574 match drive
.read_label() {
575 Ok((Some(info
), _
)) => {
576 if info
.label
.uuid
!= media_id
.label
.uuid
{
577 bail
!("verify label failed - got wrong label uuid");
579 if let Some(ref pool
) = pool
{
580 match info
.media_set_label
{
582 if set
.uuid
!= [0u8; 16].into() {
583 bail
!("verify media set label failed - got wrong set uuid");
585 if &set
.pool
!= pool
{
586 bail
!("verify media set label failed - got wrong pool");
590 bail
!("verify media set label failed (missing set label)");
595 Ok((None
, _
)) => bail
!("verify label failed (got empty media)"),
596 Err(err
) => bail
!("verify label failed - {}", err
),
609 schema
: DRIVE_NAME_SCHEMA
,
612 description
: "Encryption key password.",
617 permission
: &Permission
::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_READ
, false),
620 /// Try to restore a tape encryption key
621 pub async
fn restore_key(
624 ) -> Result
<(), Error
> {
625 run_drive_blocking_task(
627 "restore key".to_string(),
629 let mut drive
= open_drive(&config
, &drive
)?
;
631 let (_media_id
, key_config
) = drive
.read_label()?
;
633 if let Some(key_config
) = key_config
{
634 let password_fn
= || { Ok(password.as_bytes().to_vec()) }
;
635 let (key
, ..) = key_config
.decrypt(&password_fn
)?
;
636 pbs_config
::tape_encryption_keys
::insert_key(key
, key_config
, true)?
;
638 bail
!("media does not contain any encryption key configuration");
651 schema
: DRIVE_NAME_SCHEMA
,
654 description
: "Inventorize media",
663 permission
: &Permission
::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_READ
, false),
666 /// Read media label (optionally inventorize media)
667 pub async
fn read_label(
669 inventorize
: Option
<bool
>,
670 ) -> Result
<MediaIdFlat
, Error
> {
671 run_drive_blocking_task(
673 "reading label".to_string(),
675 let mut drive
= open_drive(&config
, &drive
)?
;
677 let (media_id
, _key_config
) = drive
.read_label()?
;
679 let media_id
= match media_id
{
681 let mut flat
= MediaIdFlat
{
682 uuid
: media_id
.label
.uuid
.clone(),
683 label_text
: media_id
.label
.label_text
.clone(),
684 ctime
: media_id
.label
.ctime
,
685 media_set_ctime
: None
,
686 media_set_uuid
: None
,
687 encryption_key_fingerprint
: None
,
691 if let Some(ref set
) = media_id
.media_set_label
{
692 flat
.pool
= Some(set
.pool
.clone());
693 flat
.seq_nr
= Some(set
.seq_nr
);
694 flat
.media_set_uuid
= Some(set
.uuid
.clone());
695 flat
.media_set_ctime
= Some(set
.ctime
);
696 flat
.encryption_key_fingerprint
= set
697 .encryption_key_fingerprint
699 .map(|fp
| fp
.to_string());
701 let encrypt_fingerprint
= set
.encryption_key_fingerprint
.clone()
702 .map(|fp
| (fp
, set
.uuid
.clone()));
704 if let Err(err
) = drive
.set_encryption(encrypt_fingerprint
) {
705 // try, but ignore errors. just log to stderr
706 eprintln
!("unable to load encryption key: {}", err
);
710 if let Some(true) = inventorize
{
711 let state_path
= Path
::new(TAPE_STATUS_DIR
);
712 let mut inventory
= Inventory
::new(state_path
);
714 if let Some(MediaSetLabel { ref pool, ref uuid, ..}
) = media_id
.media_set_label
{
715 let _pool_lock
= lock_media_pool(state_path
, pool
)?
;
716 let _lock
= lock_media_set(state_path
, uuid
, None
)?
;
717 MediaCatalog
::destroy_unrelated_catalog(state_path
, &media_id
)?
;
718 inventory
.store(media_id
, false)?
;
720 let _lock
= lock_unassigned_media_pool(state_path
)?
;
721 MediaCatalog
::destroy(state_path
, &media_id
.label
.uuid
)?
;
722 inventory
.store(media_id
, false)?
;
729 bail
!("Media is empty (no label).");
743 schema
: DRIVE_NAME_SCHEMA
,
751 permission
: &Permission
::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_READ
, false),
757 rpcenv
: &mut dyn RpcEnvironment
,
758 ) -> Result
<Value
, Error
> {
759 let upid_str
= run_drive_worker(
764 move |worker
, config
| {
765 let (mut changer
, _changer_name
) = required_media_changer(&config
, &drive
)?
;
767 task_log
!(worker
, "Starting drive clean");
769 changer
.clean_drive()?
;
771 if let Ok(drive_config
) = config
.lookup
::<LtoTapeDrive
>("lto", &drive
) {
772 // Note: clean_drive unloads the cleaning media, so we cannot use drive_config.open
773 let mut handle
= LtoTapeHandle
::new(open_lto_tape_device(&drive_config
.path
)?
)?
;
775 // test for critical tape alert flags
776 if let Ok(alert_flags
) = handle
.tape_alert_flags() {
777 if !alert_flags
.is_empty() {
778 task_log
!(worker
, "TapeAlertFlags: {:?}", alert_flags
);
779 if tape_alert_flags_critical(alert_flags
) {
780 bail
!("found critical tape alert flags: {:?}", alert_flags
);
785 // test wearout (max. 50 mounts)
786 if let Ok(volume_stats
) = handle
.volume_statistics() {
787 task_log
!(worker
, "Volume mounts: {}", volume_stats
.volume_mounts
);
788 let wearout
= volume_stats
.volume_mounts
* 2; // (*100.0/50.0);
789 task_log
!(worker
, "Cleaning tape wearout: {}%", wearout
);
793 task_log
!(worker
, "Drive cleaned successfully");
806 schema
: DRIVE_NAME_SCHEMA
,
811 description
: "The list of media labels with associated media Uuid (if any).",
818 permission
: &Permission
::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_READ
, false),
821 /// List known media labels (Changer Inventory)
823 /// Note: Only useful for drives with associated changer device.
825 /// This method queries the changer to get a list of media labels.
827 /// Note: This updates the media online status.
828 pub async
fn inventory(
830 ) -> Result
<Vec
<LabelUuidMap
>, Error
> {
831 run_drive_blocking_task(
833 "inventorize".to_string(),
835 let (mut changer
, changer_name
) = required_media_changer(&config
, &drive
)?
;
837 let label_text_list
= changer
.online_media_label_texts()?
;
839 let state_path
= Path
::new(TAPE_STATUS_DIR
);
841 let mut inventory
= Inventory
::load(state_path
)?
;
843 update_changer_online_status(
850 let mut list
= Vec
::new();
852 for label_text
in label_text_list
.iter() {
853 if label_text
.starts_with("CLN") {
854 // skip cleaning unit
858 let label_text
= label_text
.to_string();
860 if let Some(media_id
) = inventory
.find_media_by_label_text(&label_text
) {
861 list
.push(LabelUuidMap { label_text, uuid: Some(media_id.label.uuid.clone()) }
);
863 list
.push(LabelUuidMap { label_text, uuid: None }
);
877 schema
: DRIVE_NAME_SCHEMA
,
880 description
: "Load all tapes and try read labels (even if already inventoried)",
890 permission
: &Permission
::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_READ
, false),
895 /// Note: Only useful for drives with associated changer device.
897 /// This method queries the changer to get a list of media labels. It
898 /// then loads any unknown media into the drive, reads the label, and
899 /// store the result to the media database.
901 /// Note: This updates the media online status.
902 pub fn update_inventory(
904 read_all_labels
: Option
<bool
>,
905 rpcenv
: &mut dyn RpcEnvironment
,
906 ) -> Result
<Value
, Error
> {
907 let upid_str
= run_drive_worker(
912 move |worker
, config
| {
913 let (mut changer
, changer_name
) = required_media_changer(&config
, &drive
)?
;
915 let label_text_list
= changer
.online_media_label_texts()?
;
916 if label_text_list
.is_empty() {
917 task_log
!(worker
, "changer device does not list any media labels");
920 let state_path
= Path
::new(TAPE_STATUS_DIR
);
922 let mut inventory
= Inventory
::load(state_path
)?
;
924 update_changer_online_status(&config
, &mut inventory
, &changer_name
, &label_text_list
)?
;
926 for label_text
in label_text_list
.iter() {
927 if label_text
.starts_with("CLN") {
928 task_log
!(worker
, "skip cleaning unit '{}'", label_text
);
932 let label_text
= label_text
.to_string();
934 if !read_all_labels
.unwrap_or(false) && inventory
.find_media_by_label_text(&label_text
).is_some() {
935 task_log
!(worker
, "media '{}' already inventoried", label_text
);
939 if let Err(err
) = changer
.load_media(&label_text
) {
940 task_warn
!(worker
, "unable to load media '{}' - {}", label_text
, err
);
944 let mut drive
= open_drive(&config
, &drive
)?
;
945 match drive
.read_label() {
947 task_warn
!(worker
, "unable to read label form media '{}' - {}", label_text
, err
);
950 task_log
!(worker
, "media '{}' is empty", label_text
);
952 Ok((Some(media_id
), _key_config
)) => {
953 if label_text
!= media_id
.label
.label_text
{
954 task_warn
!(worker
, "label text mismatch ({} != {})", label_text
, media_id
.label
.label_text
);
957 task_log
!(worker
, "inventorize media '{}' with uuid '{}'", label_text
, media_id
.label
.uuid
);
959 if let Some(MediaSetLabel { ref pool, ref uuid, ..}
) = media_id
.media_set_label
{
960 let _pool_lock
= lock_media_pool(state_path
, pool
)?
;
961 let _lock
= lock_media_set(state_path
, uuid
, None
)?
;
962 MediaCatalog
::destroy_unrelated_catalog(state_path
, &media_id
)?
;
963 inventory
.store(media_id
, false)?
;
965 let _lock
= lock_unassigned_media_pool(state_path
)?
;
966 MediaCatalog
::destroy(state_path
, &media_id
.label
.uuid
)?
;
967 inventory
.store(media_id
, false)?
;
971 changer
.unload_media(None
)?
;
985 schema
: DRIVE_NAME_SCHEMA
,
988 schema
: MEDIA_POOL_NAME_SCHEMA
,
997 permission
: &Permission
::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_WRITE
, false),
1000 /// Label media with barcodes from changer device
1001 pub fn barcode_label_media(
1003 pool
: Option
<String
>,
1004 rpcenv
: &mut dyn RpcEnvironment
,
1005 ) -> Result
<Value
, Error
> {
1006 if let Some(ref pool
) = pool
{
1007 let (pool_config
, _digest
) = pbs_config
::media_pool
::config()?
;
1009 if pool_config
.sections
.get(pool
).is_none() {
1010 bail
!("no such pool ('{}')", pool
);
1014 let upid_str
= run_drive_worker(
1017 "barcode-label-media",
1018 Some(drive
.clone()),
1019 move |worker
, config
| barcode_label_media_worker(worker
, drive
, &config
, pool
),
1025 fn barcode_label_media_worker(
1026 worker
: Arc
<WorkerTask
>,
1028 drive_config
: &SectionConfigData
,
1029 pool
: Option
<String
>,
1030 ) -> Result
<(), Error
> {
1031 let (mut changer
, changer_name
) = required_media_changer(drive_config
, &drive
)?
;
1033 let mut label_text_list
= changer
.online_media_label_texts()?
;
1035 // make sure we label them in the right order
1036 label_text_list
.sort();
1038 let state_path
= Path
::new(TAPE_STATUS_DIR
);
1040 let mut inventory
= Inventory
::load(state_path
)?
;
1042 update_changer_online_status(drive_config
, &mut inventory
, &changer_name
, &label_text_list
)?
;
1044 if label_text_list
.is_empty() {
1045 bail
!("changer device does not list any media labels");
1048 for label_text
in label_text_list
{
1049 if label_text
.starts_with("CLN") { continue; }
1051 inventory
.reload()?
;
1052 if inventory
.find_media_by_label_text(&label_text
).is_some() {
1053 task_log
!(worker
, "media '{}' already inventoried (already labeled)", label_text
);
1057 task_log
!(worker
, "checking/loading media '{}'", label_text
);
1059 if let Err(err
) = changer
.load_media(&label_text
) {
1060 task_warn
!(worker
, "unable to load media '{}' - {}", label_text
, err
);
1064 let mut drive
= open_drive(drive_config
, &drive
)?
;
1067 match drive
.read_next_file() {
1069 task_log
!(worker
, "media '{}' is not empty (format it first)", label_text
);
1072 Err(BlockReadError
::EndOfFile
) => { /* EOF mark at BOT, assume tape is empty */ }
,
1073 Err(BlockReadError
::EndOfStream
) => { /* tape is empty */ }
,
1075 task_warn
!(worker
, "media '{}' read error (maybe not empty - format it first)", label_text
);
1080 let ctime
= proxmox_time
::epoch_i64();
1081 let label
= MediaLabel
{
1082 label_text
: label_text
.to_string(),
1083 uuid
: Uuid
::generate(),
1087 write_media_label(worker
.clone(), &mut drive
, label
, pool
.clone())?
1097 schema
: DRIVE_NAME_SCHEMA
,
1102 description
: "A List of medium auxiliary memory attributes.",
1109 permission
: &Permission
::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_AUDIT
, false),
1112 /// Read Cartridge Memory (Medium auxiliary memory attributes)
1113 pub async
fn cartridge_memory(drive
: String
) -> Result
<Vec
<MamAttribute
>, Error
> {
1114 run_drive_blocking_task(
1116 "reading cartridge memory".to_string(),
1118 let drive_config
: LtoTapeDrive
= config
.lookup("lto", &drive
)?
;
1119 let mut handle
= open_lto_tape_drive(&drive_config
)?
;
1121 handle
.cartridge_memory()
1131 schema
: DRIVE_NAME_SCHEMA
,
1136 type: Lp17VolumeStatistics
,
1139 permission
: &Permission
::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_AUDIT
, false),
1142 /// Read Volume Statistics (SCSI log page 17h)
1143 pub async
fn volume_statistics(drive
: String
) -> Result
<Lp17VolumeStatistics
, Error
> {
1144 run_drive_blocking_task(
1146 "reading volume statistics".to_string(),
1148 let drive_config
: LtoTapeDrive
= config
.lookup("lto", &drive
)?
;
1149 let mut handle
= open_lto_tape_drive(&drive_config
)?
;
1151 handle
.volume_statistics()
1161 schema
: DRIVE_NAME_SCHEMA
,
1166 type: LtoDriveAndMediaStatus
,
1169 permission
: &Permission
::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_AUDIT
, false),
1172 /// Get drive/media status
1173 pub async
fn status(drive
: String
) -> Result
<LtoDriveAndMediaStatus
, Error
> {
1174 run_drive_blocking_task(
1176 "reading drive status".to_string(),
1178 let drive_config
: LtoTapeDrive
= config
.lookup("lto", &drive
)?
;
1180 // Note: use open_lto_tape_device, because this also works if no medium loaded
1181 let file
= open_lto_tape_device(&drive_config
.path
)?
;
1183 let mut handle
= LtoTapeHandle
::new(file
)?
;
1185 handle
.get_drive_and_media_status()
1195 schema
: DRIVE_NAME_SCHEMA
,
1198 description
: "Force overriding existing index.",
1203 description
: "Re-read the whole tape to reconstruct the catalog instead of restoring saved versions.",
1208 description
: "Verbose mode - log all found chunks.",
1215 schema
: UPID_SCHEMA
,
1218 permission
: &Permission
::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_READ
, false),
1221 /// Scan media and record content
1222 pub fn catalog_media(
1224 force
: Option
<bool
>,
1226 verbose
: Option
<bool
>,
1227 rpcenv
: &mut dyn RpcEnvironment
,
1228 ) -> Result
<Value
, Error
> {
1229 let verbose
= verbose
.unwrap_or(false);
1230 let force
= force
.unwrap_or(false);
1231 let scan
= scan
.unwrap_or(false);
1233 let upid_str
= run_drive_worker(
1237 Some(drive
.clone()),
1238 move |worker
, config
| {
1239 let mut drive
= open_drive(&config
, &drive
)?
;
1243 let media_id
= match drive
.read_label()?
{
1244 (Some(media_id
), key_config
) => {
1247 "found media label: {}",
1248 serde_json
::to_string_pretty(&serde_json
::to_value(&media_id
)?
)?
1250 if key_config
.is_some() {
1253 "encryption key config: {}",
1254 serde_json
::to_string_pretty(&serde_json
::to_value(&key_config
)?
)?
1259 (None
, _
) => bail
!("media is empty (no media label found)"),
1262 let status_path
= Path
::new(TAPE_STATUS_DIR
);
1264 let mut inventory
= Inventory
::new(status_path
);
1266 let (_media_set_lock
, media_set_uuid
) = match media_id
.media_set_label
{
1268 task_log
!(worker
, "media is empty");
1269 let _lock
= lock_unassigned_media_pool(status_path
)?
;
1270 MediaCatalog
::destroy(status_path
, &media_id
.label
.uuid
)?
;
1271 inventory
.store(media_id
.clone(), false)?
;
1275 if set
.uuid
.as_ref() == [0u8;16] { // media is empty
1276 task_log
!(worker
, "media is empty");
1277 let _lock
= lock_unassigned_media_pool(status_path
)?
;
1278 MediaCatalog
::destroy(status_path
, &media_id
.label
.uuid
)?
;
1279 inventory
.store(media_id
.clone(), false)?
;
1282 let encrypt_fingerprint
= set
.encryption_key_fingerprint
.clone()
1283 .map(|fp
| (fp
, set
.uuid
.clone()));
1285 drive
.set_encryption(encrypt_fingerprint
)?
;
1287 let _pool_lock
= lock_media_pool(status_path
, &set
.pool
)?
;
1288 let media_set_lock
= lock_media_set(status_path
, &set
.uuid
, None
)?
;
1290 MediaCatalog
::destroy_unrelated_catalog(status_path
, &media_id
)?
;
1292 inventory
.store(media_id
.clone(), false)?
;
1294 (media_set_lock
, &set
.uuid
)
1298 if MediaCatalog
::exists(status_path
, &media_id
.label
.uuid
) && !force
{
1299 bail
!("media catalog exists (please use --force to overwrite)");
1303 let media_set
= inventory
.compute_media_set_members(media_set_uuid
)?
;
1305 if fast_catalog_restore(&worker
, &mut drive
, &media_set
, &media_id
.label
.uuid
)?
{
1309 task_log
!(worker
, "no catalog found");
1312 task_log
!(worker
, "scanning entire media to reconstruct catalog");
1315 drive
.read_label()?
; // skip over labels - we already read them above
1317 let mut checked_chunks
= HashMap
::new();
1318 restore_media(worker
, &mut drive
, &media_id
, None
, &mut checked_chunks
, verbose
)?
;
1331 schema
: CHANGER_NAME_SCHEMA
,
1337 description
: "The list of configured drives with model information.",
1340 type: DriveListEntry
,
1344 description
: "List configured tape drives filtered by Tape.Audit privileges",
1345 permission
: &Permission
::Anybody
,
1350 changer
: Option
<String
>,
1352 rpcenv
: &mut dyn RpcEnvironment
,
1353 ) -> Result
<Vec
<DriveListEntry
>, Error
> {
1354 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
1355 let user_info
= CachedUserInfo
::new()?
;
1357 let (config
, _
) = pbs_config
::drive
::config()?
;
1359 let lto_drives
= lto_tape_device_list();
1361 let drive_list
: Vec
<LtoTapeDrive
> = config
.convert_to_typed_array("lto")?
;
1363 let mut list
= Vec
::new();
1365 for drive
in drive_list
{
1366 if changer
.is_some() && drive
.changer
!= changer
{
1370 let privs
= user_info
.lookup_privs(&auth_id
, &["tape", "drive", &drive
.name
]);
1371 if (privs
& PRIV_TAPE_AUDIT
) == 0 {
1375 let info
= lookup_device_identification(<o_drives
, &drive
.path
);
1376 let state
= get_tape_device_state(&config
, &drive
.name
)?
;
1377 let entry
= DriveListEntry { config: drive, info, state }
;
1385 pub const SUBDIRS
: SubdirMap
= &sorted
!([
1387 "barcode-label-media",
1389 .post(&API_METHOD_BARCODE_LABEL_MEDIA
)
1394 .post(&API_METHOD_CATALOG_MEDIA
)
1399 .put(&API_METHOD_CLEAN_DRIVE
)
1404 .post(&API_METHOD_EJECT_MEDIA
)
1409 .post(&API_METHOD_FORMAT_MEDIA
)
1414 .put(&API_METHOD_EXPORT_MEDIA
)
1419 .get(&API_METHOD_INVENTORY
)
1420 .put(&API_METHOD_UPDATE_INVENTORY
)
1425 .post(&API_METHOD_LABEL_MEDIA
)
1430 .post(&API_METHOD_LOAD_MEDIA
)
1435 .post(&API_METHOD_LOAD_SLOT
)
1440 .get(&API_METHOD_CARTRIDGE_MEMORY
)
1443 "volume-statistics",
1445 .get(&API_METHOD_VOLUME_STATISTICS
)
1450 .get(&API_METHOD_READ_LABEL
)
1455 .post(&API_METHOD_RESTORE_KEY
)
1460 .post(&API_METHOD_REWIND
)
1465 .get(&API_METHOD_STATUS
)
1470 .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
);