4 use anyhow
::{bail, Error}
;
10 list_subdirs_api_method
,
25 drive
::check_drive_exists
,
27 backup
::decrypt_key_config
,
33 MEDIA_POOL_NAME_SCHEMA
,
40 LinuxDriveAndMediaStatus
,
42 tape
::restore
::restore_media
,
52 linux_tape_device_list
,
55 required_media_changer
,
56 update_changer_online_status
,
59 open_linux_tape_device
,
72 schema
: DRIVE_NAME_SCHEMA
,
75 schema
: MEDIA_LABEL_SCHEMA
,
80 /// Load media with specified label
82 /// Issue a media load request to the associated changer device.
83 pub async
fn load_media(drive
: String
, label_text
: String
) -> Result
<(), Error
> {
85 let (config
, _digest
) = config
::drive
::config()?
;
87 tokio
::task
::spawn_blocking(move || {
88 let (mut changer
, _
) = required_media_changer(&config
, &drive
)?
;
89 changer
.load_media(&label_text
)
97 schema
: DRIVE_NAME_SCHEMA
,
100 description
: "Source slot number.",
106 /// Load media from the specified slot
108 /// Issue a media load request to the associated changer device.
109 pub async
fn load_slot(drive
: String
, source_slot
: u64) -> Result
<(), Error
> {
111 let (config
, _digest
) = config
::drive
::config()?
;
113 tokio
::task
::spawn_blocking(move || {
114 let (mut changer
, _
) = required_media_changer(&config
, &drive
)?
;
115 changer
.load_media_from_slot(source_slot
)
123 schema
: DRIVE_NAME_SCHEMA
,
126 schema
: MEDIA_LABEL_SCHEMA
,
131 description
: "The import-export slot number the media was transfered to.",
136 /// Export media with specified label
137 pub async
fn export_media(drive
: String
, label_text
: String
) -> Result
<u64, Error
> {
139 let (config
, _digest
) = config
::drive
::config()?
;
141 tokio
::task
::spawn_blocking(move || {
142 let (mut changer
, changer_name
) = required_media_changer(&config
, &drive
)?
;
143 match changer
.export_media(&label_text
)?
{
144 Some(slot
) => Ok(slot
),
145 None
=> bail
!("media '{}' is not online (via changer '{}')", label_text
, changer_name
),
154 schema
: DRIVE_NAME_SCHEMA
,
157 description
: "Target slot number. If omitted, defaults to the slot that the drive was loaded from.",
164 /// Unload media via changer
167 target_slot
: Option
<u64>,
169 ) -> Result
<(), Error
> {
171 let (config
, _digest
) = config
::drive
::config()?
;
173 tokio
::task
::spawn_blocking(move || {
174 let (mut changer
, _
) = required_media_changer(&config
, &drive
)?
;
175 changer
.unload_media(target_slot
)
184 description
: "The list of autodetected tape drives.",
187 type: TapeDeviceInfo
,
192 pub fn scan_drives(_param
: Value
) -> Result
<Vec
<TapeDeviceInfo
>, Error
> {
194 let list
= linux_tape_device_list();
203 schema
: DRIVE_NAME_SCHEMA
,
206 description
: "Use fast erase.",
221 rpcenv
: &mut dyn RpcEnvironment
,
222 ) -> Result
<Value
, Error
> {
224 let (config
, _digest
) = config
::drive
::config()?
;
226 check_drive_exists(&config
, &drive
)?
; // early check before starting worker
228 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
230 let to_stdout
= if rpcenv
.env_type() == RpcEnvironmentType
::CLI { true }
else { false }
;
232 let upid_str
= WorkerTask
::new_thread(
238 let mut drive
= open_drive(&config
, &drive
)?
;
239 drive
.erase_media(fast
.unwrap_or(true))?
;
251 schema
: DRIVE_NAME_SCHEMA
,
262 rpcenv
: &mut dyn RpcEnvironment
,
263 ) -> Result
<Value
, Error
> {
265 let (config
, _digest
) = config
::drive
::config()?
;
267 check_drive_exists(&config
, &drive
)?
; // early check before starting worker
269 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
271 let to_stdout
= if rpcenv
.env_type() == RpcEnvironmentType
::CLI { true }
else { false }
;
273 let upid_str
= WorkerTask
::new_thread(
279 let mut drive
= open_drive(&config
, &drive
)?
;
292 schema
: DRIVE_NAME_SCHEMA
,
297 /// Eject/Unload drive media
298 pub async
fn eject_media(drive
: String
) -> Result
<(), Error
> {
300 let (config
, _digest
) = config
::drive
::config()?
;
302 tokio
::task
::spawn_blocking(move || {
303 if let Some((mut changer
, _
)) = media_changer(&config
, &drive
)?
{
304 changer
.unload_media(None
)?
;
306 let mut drive
= open_drive(&config
, &drive
)?
;
307 drive
.eject_media()?
;
317 schema
: DRIVE_NAME_SCHEMA
,
320 schema
: MEDIA_LABEL_SCHEMA
,
323 schema
: MEDIA_POOL_NAME_SCHEMA
,
334 /// Write a new media label to the media in 'drive'. The media is
335 /// assigned to the specified 'pool', or else to the free media pool.
337 /// Note: The media need to be empty (you may want to erase it first).
340 pool
: Option
<String
>,
342 rpcenv
: &mut dyn RpcEnvironment
,
343 ) -> Result
<Value
, Error
> {
345 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
347 if let Some(ref pool
) = pool
{
348 let (pool_config
, _digest
) = config
::media_pool
::config()?
;
350 if pool_config
.sections
.get(pool
).is_none() {
351 bail
!("no such pool ('{}')", pool
);
355 let (config
, _digest
) = config
::drive
::config()?
;
357 let to_stdout
= if rpcenv
.env_type() == RpcEnvironmentType
::CLI { true }
else { false }
;
359 let upid_str
= WorkerTask
::new_thread(
366 let mut drive
= open_drive(&config
, &drive
)?
;
370 match drive
.read_next_file() {
371 Ok(Some(_file
)) => bail
!("media is not empty (erase first)"),
372 Ok(None
) => { /* EOF mark at BOT, assume tape is empty */ }
,
374 if err
.is_errno(nix
::errno
::Errno
::ENOSPC
) || err
.is_errno(nix
::errno
::Errno
::EIO
) {
375 /* assume tape is empty */
377 bail
!("media read error - {}", err
);
382 let ctime
= proxmox
::tools
::time
::epoch_i64();
383 let label
= MediaLabel
{
384 label_text
: label_text
.to_string(),
385 uuid
: Uuid
::generate(),
389 write_media_label(worker
, &mut drive
, label
, pool
)
396 fn write_media_label(
397 worker
: Arc
<WorkerTask
>,
398 drive
: &mut Box
<dyn TapeDriver
>,
400 pool
: Option
<String
>,
401 ) -> Result
<(), Error
> {
403 drive
.label_tape(&label
)?
;
405 let mut media_set_label
= None
;
407 if let Some(ref pool
) = pool
{
408 // assign media to pool by writing special media set label
409 worker
.log(format
!("Label media '{}' for pool '{}'", label
.label_text
, pool
));
410 let set
= MediaSetLabel
::with_data(&pool
, [0u8; 16].into(), 0, label
.ctime
, None
);
412 drive
.write_media_set_label(&set
, None
)?
;
413 media_set_label
= Some(set
);
415 worker
.log(format
!("Label media '{}' (no pool assignment)", label
.label_text
));
418 let media_id
= MediaId { label, media_set_label }
;
420 let status_path
= Path
::new(TAPE_STATUS_DIR
);
422 // Create the media catalog
423 MediaCatalog
::overwrite(status_path
, &media_id
, false)?
;
425 let mut inventory
= Inventory
::load(status_path
)?
;
426 inventory
.store(media_id
.clone(), false)?
;
430 match drive
.read_label() {
431 Ok((Some(info
), _
)) => {
432 if info
.label
.uuid
!= media_id
.label
.uuid
{
433 bail
!("verify label failed - got wrong label uuid");
435 if let Some(ref pool
) = pool
{
436 match info
.media_set_label
{
438 if set
.uuid
!= [0u8; 16].into() {
439 bail
!("verify media set label failed - got wrong set uuid");
441 if &set
.pool
!= pool
{
442 bail
!("verify media set label failed - got wrong pool");
446 bail
!("verify media set label failed (missing set label)");
451 Ok((None
, _
)) => bail
!("verify label failed (got empty media)"),
452 Err(err
) => bail
!("verify label failed - {}", err
),
465 schema
: DRIVE_NAME_SCHEMA
,
468 description
: "Encryption key password.",
473 /// Try to restore a tape encryption key
474 pub async
fn restore_key(
477 ) -> Result
<(), Error
> {
479 let (config
, _digest
) = config
::drive
::config()?
;
481 tokio
::task
::spawn_blocking(move || {
482 let mut drive
= open_drive(&config
, &drive
)?
;
484 let (_media_id
, key_config
) = drive
.read_label()?
;
486 if let Some(key_config
) = key_config
{
487 let hint
= String
::from("fixme: add hint");
488 // fixme: howto show restore hint
489 let password_fn
= || { Ok(password.as_bytes().to_vec()) }
;
490 let (key
, ..) = decrypt_key_config(&key_config
, &password_fn
)?
;
491 config
::tape_encryption_keys
::insert_key(key
, key_config
, hint
)?
;
493 bail
!("media does not contain any encryption key configuration");
504 schema
: DRIVE_NAME_SCHEMA
,
507 description
: "Inventorize media",
516 /// Read media label (optionally inventorize media)
517 pub async
fn read_label(
519 inventorize
: Option
<bool
>,
520 ) -> Result
<MediaIdFlat
, Error
> {
522 let (config
, _digest
) = config
::drive
::config()?
;
524 tokio
::task
::spawn_blocking(move || {
525 let mut drive
= open_drive(&config
, &drive
)?
;
527 let (media_id
, _key_config
) = drive
.read_label()?
;
529 let media_id
= match media_id
{
531 let mut flat
= MediaIdFlat
{
532 uuid
: media_id
.label
.uuid
.to_string(),
533 label_text
: media_id
.label
.label_text
.clone(),
534 ctime
: media_id
.label
.ctime
,
535 media_set_ctime
: None
,
536 media_set_uuid
: None
,
537 encryption_key_fingerprint
: None
,
541 if let Some(ref set
) = media_id
.media_set_label
{
542 flat
.pool
= Some(set
.pool
.clone());
543 flat
.seq_nr
= Some(set
.seq_nr
);
544 flat
.media_set_uuid
= Some(set
.uuid
.to_string());
545 flat
.media_set_ctime
= Some(set
.ctime
);
546 flat
.encryption_key_fingerprint
= set
547 .encryption_key_fingerprint
549 .map(|fp
| crate::tools
::format
::as_fingerprint(fp
.bytes()));
552 if let Some(true) = inventorize
{
553 let state_path
= Path
::new(TAPE_STATUS_DIR
);
554 let mut inventory
= Inventory
::load(state_path
)?
;
555 inventory
.store(media_id
, false)?
;
561 bail
!("Media is empty (no label).");
573 schema
: DRIVE_NAME_SCHEMA
,
584 rpcenv
: &mut dyn RpcEnvironment
,
585 ) -> Result
<Value
, Error
> {
587 let (config
, _digest
) = config
::drive
::config()?
;
589 check_drive_exists(&config
, &drive
)?
; // early check before starting worker
591 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
593 let to_stdout
= if rpcenv
.env_type() == RpcEnvironmentType
::CLI { true }
else { false }
;
595 let upid_str
= WorkerTask
::new_thread(
602 let (mut changer
, _changer_name
) = required_media_changer(&config
, &drive
)?
;
604 worker
.log("Starting drive clean");
606 changer
.clean_drive()?
;
608 worker
.log("Drive cleaned sucessfully");
620 schema
: DRIVE_NAME_SCHEMA
,
625 description
: "The list of media labels with associated media Uuid (if any).",
632 /// List known media labels (Changer Inventory)
634 /// Note: Only useful for drives with associated changer device.
636 /// This method queries the changer to get a list of media labels.
638 /// Note: This updates the media online status.
639 pub async
fn inventory(
641 ) -> Result
<Vec
<LabelUuidMap
>, Error
> {
643 let (config
, _digest
) = config
::drive
::config()?
;
645 tokio
::task
::spawn_blocking(move || {
646 let (mut changer
, changer_name
) = required_media_changer(&config
, &drive
)?
;
648 let label_text_list
= changer
.online_media_label_texts()?
;
650 let state_path
= Path
::new(TAPE_STATUS_DIR
);
652 let mut inventory
= Inventory
::load(state_path
)?
;
654 update_changer_online_status(
661 let mut list
= Vec
::new();
663 for label_text
in label_text_list
.iter() {
664 if label_text
.starts_with("CLN") {
665 // skip cleaning unit
669 let label_text
= label_text
.to_string();
671 if let Some(media_id
) = inventory
.find_media_by_label_text(&label_text
) {
672 list
.push(LabelUuidMap { label_text, uuid: Some(media_id.label.uuid.to_string()) }
);
674 list
.push(LabelUuidMap { label_text, uuid: None }
);
686 schema
: DRIVE_NAME_SCHEMA
,
689 description
: "Load all tapes and try read labels (even if already inventoried)",
701 /// Note: Only useful for drives with associated changer device.
703 /// This method queries the changer to get a list of media labels. It
704 /// then loads any unknown media into the drive, reads the label, and
705 /// store the result to the media database.
707 /// Note: This updates the media online status.
708 pub fn update_inventory(
710 read_all_labels
: Option
<bool
>,
711 rpcenv
: &mut dyn RpcEnvironment
,
712 ) -> Result
<Value
, Error
> {
714 let (config
, _digest
) = config
::drive
::config()?
;
716 check_drive_exists(&config
, &drive
)?
; // early check before starting worker
718 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
720 let to_stdout
= if rpcenv
.env_type() == RpcEnvironmentType
::CLI { true }
else { false }
;
722 let upid_str
= WorkerTask
::new_thread(
729 let (mut changer
, changer_name
) = required_media_changer(&config
, &drive
)?
;
731 let label_text_list
= changer
.online_media_label_texts()?
;
732 if label_text_list
.is_empty() {
733 worker
.log(format
!("changer device does not list any media labels"));
736 let state_path
= Path
::new(TAPE_STATUS_DIR
);
738 let mut inventory
= Inventory
::load(state_path
)?
;
740 update_changer_online_status(&config
, &mut inventory
, &changer_name
, &label_text_list
)?
;
742 for label_text
in label_text_list
.iter() {
743 if label_text
.starts_with("CLN") {
744 worker
.log(format
!("skip cleaning unit '{}'", label_text
));
748 let label_text
= label_text
.to_string();
750 if !read_all_labels
.unwrap_or(false) {
751 if let Some(_
) = inventory
.find_media_by_label_text(&label_text
) {
752 worker
.log(format
!("media '{}' already inventoried", label_text
));
757 if let Err(err
) = changer
.load_media(&label_text
) {
758 worker
.warn(format
!("unable to load media '{}' - {}", label_text
, err
));
762 let mut drive
= open_drive(&config
, &drive
)?
;
763 match drive
.read_label() {
765 worker
.warn(format
!("unable to read label form media '{}' - {}", label_text
, err
));
768 worker
.log(format
!("media '{}' is empty", label_text
));
770 Ok((Some(media_id
), _key_config
)) => {
771 if label_text
!= media_id
.label
.label_text
{
772 worker
.warn(format
!("label text missmatch ({} != {})", label_text
, media_id
.label
.label_text
));
775 worker
.log(format
!("inventorize media '{}' with uuid '{}'", label_text
, media_id
.label
.uuid
));
776 inventory
.store(media_id
, false)?
;
779 changer
.unload_media(None
)?
;
793 schema
: DRIVE_NAME_SCHEMA
,
796 schema
: MEDIA_POOL_NAME_SCHEMA
,
805 /// Label media with barcodes from changer device
806 pub fn barcode_label_media(
808 pool
: Option
<String
>,
809 rpcenv
: &mut dyn RpcEnvironment
,
810 ) -> Result
<Value
, Error
> {
812 if let Some(ref pool
) = pool
{
813 let (pool_config
, _digest
) = config
::media_pool
::config()?
;
815 if pool_config
.sections
.get(pool
).is_none() {
816 bail
!("no such pool ('{}')", pool
);
820 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
822 let to_stdout
= if rpcenv
.env_type() == RpcEnvironmentType
::CLI { true }
else { false }
;
824 let upid_str
= WorkerTask
::new_thread(
825 "barcode-label-media",
830 barcode_label_media_worker(worker
, drive
, pool
)
837 fn barcode_label_media_worker(
838 worker
: Arc
<WorkerTask
>,
840 pool
: Option
<String
>,
841 ) -> Result
<(), Error
> {
843 let (config
, _digest
) = config
::drive
::config()?
;
845 let (mut changer
, changer_name
) = required_media_changer(&config
, &drive
)?
;
847 let label_text_list
= changer
.online_media_label_texts()?
;
849 let state_path
= Path
::new(TAPE_STATUS_DIR
);
851 let mut inventory
= Inventory
::load(state_path
)?
;
853 update_changer_online_status(&config
, &mut inventory
, &changer_name
, &label_text_list
)?
;
855 if label_text_list
.is_empty() {
856 bail
!("changer device does not list any media labels");
859 for label_text
in label_text_list
{
860 if label_text
.starts_with("CLN") { continue; }
863 if inventory
.find_media_by_label_text(&label_text
).is_some() {
864 worker
.log(format
!("media '{}' already inventoried (already labeled)", label_text
));
868 worker
.log(format
!("checking/loading media '{}'", label_text
));
870 if let Err(err
) = changer
.load_media(&label_text
) {
871 worker
.warn(format
!("unable to load media '{}' - {}", label_text
, err
));
875 let mut drive
= open_drive(&config
, &drive
)?
;
878 match drive
.read_next_file() {
880 worker
.log(format
!("media '{}' is not empty (erase first)", label_text
));
883 Ok(None
) => { /* EOF mark at BOT, assume tape is empty */ }
,
885 if err
.is_errno(nix
::errno
::Errno
::ENOSPC
) || err
.is_errno(nix
::errno
::Errno
::EIO
) {
886 /* assume tape is empty */
888 worker
.warn(format
!("media '{}' read error (maybe not empty - erase first)", label_text
));
894 let ctime
= proxmox
::tools
::time
::epoch_i64();
895 let label
= MediaLabel
{
896 label_text
: label_text
.to_string(),
897 uuid
: Uuid
::generate(),
901 write_media_label(worker
.clone(), &mut drive
, label
, pool
.clone())?
911 schema
: DRIVE_NAME_SCHEMA
,
916 description
: "A List of medium auxiliary memory attributes.",
923 /// Read Cartridge Memory (Medium auxiliary memory attributes)
924 pub fn cartridge_memory(drive
: String
) -> Result
<Vec
<MamAttribute
>, Error
> {
926 let (config
, _digest
) = config
::drive
::config()?
;
928 let drive_config
: LinuxTapeDrive
= config
.lookup("linux", &drive
)?
;
929 let mut handle
= drive_config
.open()?
;
931 handle
.cartridge_memory()
938 schema
: DRIVE_NAME_SCHEMA
,
943 type: LinuxDriveAndMediaStatus
,
946 /// Get drive/media status
947 pub fn status(drive
: String
) -> Result
<LinuxDriveAndMediaStatus
, Error
> {
949 let (config
, _digest
) = config
::drive
::config()?
;
951 let drive_config
: LinuxTapeDrive
= config
.lookup("linux", &drive
)?
;
953 // Note: use open_linux_tape_device, because this also works if no medium loaded
954 let file
= open_linux_tape_device(&drive_config
.path
)?
;
956 let mut handle
= LinuxTapeHandle
::new(file
);
958 handle
.get_drive_and_media_status()
965 schema
: DRIVE_NAME_SCHEMA
,
968 description
: "Force overriding existing index.",
973 description
: "Verbose mode - log all found chunks.",
983 /// Scan media and record content
984 pub fn catalog_media(
987 verbose
: Option
<bool
>,
988 rpcenv
: &mut dyn RpcEnvironment
,
989 ) -> Result
<Value
, Error
> {
991 let verbose
= verbose
.unwrap_or(false);
992 let force
= force
.unwrap_or(false);
994 let (config
, _digest
) = config
::drive
::config()?
;
996 check_drive_exists(&config
, &drive
)?
; // early check before starting worker
998 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
1000 let to_stdout
= if rpcenv
.env_type() == RpcEnvironmentType
::CLI { true }
else { false }
;
1002 let upid_str
= WorkerTask
::new_thread(
1004 Some(drive
.clone()),
1009 let mut drive
= open_drive(&config
, &drive
)?
;
1013 let media_id
= match drive
.read_label()?
{
1014 (Some(media_id
), key_config
) => {
1016 "found media label: {}",
1017 serde_json
::to_string_pretty(&serde_json
::to_value(&media_id
)?
)?
1019 if key_config
.is_some() {
1021 "encryption key config: {}",
1022 serde_json
::to_string_pretty(&serde_json
::to_value(&key_config
)?
)?
1027 (None
, _
) => bail
!("media is empty (no media label found)"),
1030 let status_path
= Path
::new(TAPE_STATUS_DIR
);
1032 let mut inventory
= Inventory
::load(status_path
)?
;
1033 inventory
.store(media_id
.clone(), false)?
;
1035 let pool
= match media_id
.media_set_label
{
1037 worker
.log("media is empty");
1038 MediaCatalog
::destroy(status_path
, &media_id
.label
.uuid
)?
;
1042 if set
.uuid
.as_ref() == [0u8;16] { // media is empty
1043 worker
.log("media is empty");
1044 MediaCatalog
::destroy(status_path
, &media_id
.label
.uuid
)?
;
1047 let encrypt_fingerprint
= set
.encryption_key_fingerprint
.clone();
1048 drive
.set_encryption(encrypt_fingerprint
)?
;
1054 let _lock
= MediaPool
::lock(status_path
, &pool
)?
;
1056 if MediaCatalog
::exists(status_path
, &media_id
.label
.uuid
) {
1058 bail
!("media catalog exists (please use --force to overwrite)");
1062 restore_media(&worker
, &mut drive
, &media_id
, None
, verbose
)?
;
1073 pub const SUBDIRS
: SubdirMap
= &sorted
!([
1075 "barcode-label-media",
1077 .put(&API_METHOD_BARCODE_LABEL_MEDIA
)
1082 .put(&API_METHOD_CATALOG_MEDIA
)
1087 .put(&API_METHOD_CLEAN_DRIVE
)
1092 .put(&API_METHOD_EJECT_MEDIA
)
1097 .put(&API_METHOD_ERASE_MEDIA
)
1102 .get(&API_METHOD_INVENTORY
)
1103 .put(&API_METHOD_UPDATE_INVENTORY
)
1108 .put(&API_METHOD_LABEL_MEDIA
)
1113 .put(&API_METHOD_LOAD_SLOT
)
1118 .put(&API_METHOD_CARTRIDGE_MEMORY
)
1123 .get(&API_METHOD_READ_LABEL
)
1128 .put(&API_METHOD_REWIND
)
1133 .get(&API_METHOD_SCAN_DRIVES
)
1138 .get(&API_METHOD_STATUS
)
1143 .put(&API_METHOD_UNLOAD
)
1147 pub const ROUTER
: Router
= Router
::new()
1148 .get(&list_subdirs_api_method
!(SUBDIRS
))