4 use anyhow
::{bail, Error}
;
10 list_subdirs_api_method
,
25 drive
::check_drive_exists
,
32 MEDIA_POOL_NAME_SCHEMA
,
39 LinuxDriveAndMediaStatus
,
41 tape
::restore
::restore_media
,
51 linux_tape_device_list
,
54 required_media_changer
,
55 update_changer_online_status
,
58 open_linux_tape_device
,
71 schema
: DRIVE_NAME_SCHEMA
,
74 schema
: MEDIA_LABEL_SCHEMA
,
79 /// Load media with specified label
81 /// Issue a media load request to the associated changer device.
82 pub async
fn load_media(drive
: String
, label_text
: String
) -> Result
<(), Error
> {
84 let (config
, _digest
) = config
::drive
::config()?
;
86 tokio
::task
::spawn_blocking(move || {
87 let (mut changer
, _
) = required_media_changer(&config
, &drive
)?
;
88 changer
.load_media(&label_text
)
96 schema
: DRIVE_NAME_SCHEMA
,
99 description
: "Source slot number.",
105 /// Load media from the specified slot
107 /// Issue a media load request to the associated changer device.
108 pub async
fn load_slot(drive
: String
, source_slot
: u64) -> Result
<(), Error
> {
110 let (config
, _digest
) = config
::drive
::config()?
;
112 tokio
::task
::spawn_blocking(move || {
113 let (mut changer
, _
) = required_media_changer(&config
, &drive
)?
;
114 changer
.load_media_from_slot(source_slot
)
122 schema
: DRIVE_NAME_SCHEMA
,
125 schema
: MEDIA_LABEL_SCHEMA
,
130 description
: "The import-export slot number the media was transfered to.",
135 /// Export media with specified label
136 pub async
fn export_media(drive
: String
, label_text
: String
) -> Result
<u64, Error
> {
138 let (config
, _digest
) = config
::drive
::config()?
;
140 tokio
::task
::spawn_blocking(move || {
141 let (mut changer
, changer_name
) = required_media_changer(&config
, &drive
)?
;
142 match changer
.export_media(&label_text
)?
{
143 Some(slot
) => Ok(slot
),
144 None
=> bail
!("media '{}' is not online (via changer '{}')", label_text
, changer_name
),
153 schema
: DRIVE_NAME_SCHEMA
,
156 description
: "Target slot number. If omitted, defaults to the slot that the drive was loaded from.",
163 /// Unload media via changer
166 target_slot
: Option
<u64>,
168 ) -> Result
<(), Error
> {
170 let (config
, _digest
) = config
::drive
::config()?
;
172 tokio
::task
::spawn_blocking(move || {
173 let (mut changer
, _
) = required_media_changer(&config
, &drive
)?
;
174 changer
.unload_media(target_slot
)
183 description
: "The list of autodetected tape drives.",
186 type: TapeDeviceInfo
,
191 pub fn scan_drives(_param
: Value
) -> Result
<Vec
<TapeDeviceInfo
>, Error
> {
193 let list
= linux_tape_device_list();
202 schema
: DRIVE_NAME_SCHEMA
,
205 description
: "Use fast erase.",
220 rpcenv
: &mut dyn RpcEnvironment
,
221 ) -> Result
<Value
, Error
> {
223 let (config
, _digest
) = config
::drive
::config()?
;
225 check_drive_exists(&config
, &drive
)?
; // early check before starting worker
227 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
229 let to_stdout
= if rpcenv
.env_type() == RpcEnvironmentType
::CLI { true }
else { false }
;
231 let upid_str
= WorkerTask
::new_thread(
237 let mut drive
= open_drive(&config
, &drive
)?
;
238 drive
.erase_media(fast
.unwrap_or(true))?
;
250 schema
: DRIVE_NAME_SCHEMA
,
261 rpcenv
: &mut dyn RpcEnvironment
,
262 ) -> Result
<Value
, Error
> {
264 let (config
, _digest
) = config
::drive
::config()?
;
266 check_drive_exists(&config
, &drive
)?
; // early check before starting worker
268 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
270 let to_stdout
= if rpcenv
.env_type() == RpcEnvironmentType
::CLI { true }
else { false }
;
272 let upid_str
= WorkerTask
::new_thread(
278 let mut drive
= open_drive(&config
, &drive
)?
;
291 schema
: DRIVE_NAME_SCHEMA
,
296 /// Eject/Unload drive media
297 pub async
fn eject_media(drive
: String
) -> Result
<(), Error
> {
299 let (config
, _digest
) = config
::drive
::config()?
;
301 tokio
::task
::spawn_blocking(move || {
302 if let Some((mut changer
, _
)) = media_changer(&config
, &drive
)?
{
303 changer
.unload_media(None
)?
;
305 let mut drive
= open_drive(&config
, &drive
)?
;
306 drive
.eject_media()?
;
316 schema
: DRIVE_NAME_SCHEMA
,
319 schema
: MEDIA_LABEL_SCHEMA
,
322 schema
: MEDIA_POOL_NAME_SCHEMA
,
333 /// Write a new media label to the media in 'drive'. The media is
334 /// assigned to the specified 'pool', or else to the free media pool.
336 /// Note: The media need to be empty (you may want to erase it first).
339 pool
: Option
<String
>,
341 rpcenv
: &mut dyn RpcEnvironment
,
342 ) -> Result
<Value
, Error
> {
344 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
346 if let Some(ref pool
) = pool
{
347 let (pool_config
, _digest
) = config
::media_pool
::config()?
;
349 if pool_config
.sections
.get(pool
).is_none() {
350 bail
!("no such pool ('{}')", pool
);
354 let (config
, _digest
) = config
::drive
::config()?
;
356 let to_stdout
= if rpcenv
.env_type() == RpcEnvironmentType
::CLI { true }
else { false }
;
358 let upid_str
= WorkerTask
::new_thread(
365 let mut drive
= open_drive(&config
, &drive
)?
;
369 match drive
.read_next_file() {
370 Ok(Some(_file
)) => bail
!("media is not empty (erase first)"),
371 Ok(None
) => { /* EOF mark at BOT, assume tape is empty */ }
,
373 if err
.is_errno(nix
::errno
::Errno
::ENOSPC
) || err
.is_errno(nix
::errno
::Errno
::EIO
) {
374 /* assume tape is empty */
376 bail
!("media read error - {}", err
);
381 let ctime
= proxmox
::tools
::time
::epoch_i64();
382 let label
= MediaLabel
{
383 label_text
: label_text
.to_string(),
384 uuid
: Uuid
::generate(),
388 write_media_label(worker
, &mut drive
, label
, pool
)
395 fn write_media_label(
396 worker
: Arc
<WorkerTask
>,
397 drive
: &mut Box
<dyn TapeDriver
>,
399 pool
: Option
<String
>,
400 ) -> Result
<(), Error
> {
402 drive
.label_tape(&label
)?
;
404 let mut media_set_label
= None
;
406 if let Some(ref pool
) = pool
{
407 // assign media to pool by writing special media set label
408 worker
.log(format
!("Label media '{}' for pool '{}'", label
.label_text
, pool
));
409 let set
= MediaSetLabel
::with_data(&pool
, [0u8; 16].into(), 0, label
.ctime
, None
);
411 drive
.write_media_set_label(&set
, None
)?
;
412 media_set_label
= Some(set
);
414 worker
.log(format
!("Label media '{}' (no pool assignment)", label
.label_text
));
417 let media_id
= MediaId { label, media_set_label }
;
419 let status_path
= Path
::new(TAPE_STATUS_DIR
);
421 // Create the media catalog
422 MediaCatalog
::overwrite(status_path
, &media_id
, false)?
;
424 let mut inventory
= Inventory
::load(status_path
)?
;
425 inventory
.store(media_id
.clone(), false)?
;
429 match drive
.read_label() {
430 Ok((Some(info
), _
)) => {
431 if info
.label
.uuid
!= media_id
.label
.uuid
{
432 bail
!("verify label failed - got wrong label uuid");
434 if let Some(ref pool
) = pool
{
435 match info
.media_set_label
{
437 if set
.uuid
!= [0u8; 16].into() {
438 bail
!("verify media set label failed - got wrong set uuid");
440 if &set
.pool
!= pool
{
441 bail
!("verify media set label failed - got wrong pool");
445 bail
!("verify media set label failed (missing set label)");
450 Ok((None
, _
)) => bail
!("verify label failed (got empty media)"),
451 Err(err
) => bail
!("verify label failed - {}", err
),
464 schema
: DRIVE_NAME_SCHEMA
,
467 description
: "Encryption key password.",
472 /// Try to restore a tape encryption key
473 pub async
fn restore_key(
476 ) -> Result
<(), Error
> {
478 let (config
, _digest
) = config
::drive
::config()?
;
480 tokio
::task
::spawn_blocking(move || {
481 let mut drive
= open_drive(&config
, &drive
)?
;
483 let (_media_id
, key_config
) = drive
.read_label()?
;
485 if let Some(key_config
) = key_config
{
486 let password_fn
= || { Ok(password.as_bytes().to_vec()) }
;
487 let key
= match key_config
.decrypt(&password_fn
) {
488 Ok((key
, ..)) => key
,
490 match key_config
.hint
{
492 bail
!("decrypt key failed (password hint: {})", hint
);
495 bail
!("decrypt key failed (wrong password)");
500 config
::tape_encryption_keys
::insert_key(key
, key_config
)?
;
502 bail
!("media does not contain any encryption key configuration");
513 schema
: DRIVE_NAME_SCHEMA
,
516 description
: "Inventorize media",
525 /// Read media label (optionally inventorize media)
526 pub async
fn read_label(
528 inventorize
: Option
<bool
>,
529 ) -> Result
<MediaIdFlat
, Error
> {
531 let (config
, _digest
) = config
::drive
::config()?
;
533 tokio
::task
::spawn_blocking(move || {
534 let mut drive
= open_drive(&config
, &drive
)?
;
536 let (media_id
, _key_config
) = drive
.read_label()?
;
538 let media_id
= match media_id
{
540 let mut flat
= MediaIdFlat
{
541 uuid
: media_id
.label
.uuid
.to_string(),
542 label_text
: media_id
.label
.label_text
.clone(),
543 ctime
: media_id
.label
.ctime
,
544 media_set_ctime
: None
,
545 media_set_uuid
: None
,
546 encryption_key_fingerprint
: None
,
550 if let Some(ref set
) = media_id
.media_set_label
{
551 flat
.pool
= Some(set
.pool
.clone());
552 flat
.seq_nr
= Some(set
.seq_nr
);
553 flat
.media_set_uuid
= Some(set
.uuid
.to_string());
554 flat
.media_set_ctime
= Some(set
.ctime
);
555 flat
.encryption_key_fingerprint
= set
556 .encryption_key_fingerprint
558 .map(|fp
| crate::tools
::format
::as_fingerprint(fp
.bytes()));
561 if let Some(true) = inventorize
{
562 let state_path
= Path
::new(TAPE_STATUS_DIR
);
563 let mut inventory
= Inventory
::load(state_path
)?
;
564 inventory
.store(media_id
, false)?
;
570 bail
!("Media is empty (no label).");
582 schema
: DRIVE_NAME_SCHEMA
,
593 rpcenv
: &mut dyn RpcEnvironment
,
594 ) -> Result
<Value
, Error
> {
596 let (config
, _digest
) = config
::drive
::config()?
;
598 check_drive_exists(&config
, &drive
)?
; // early check before starting worker
600 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
602 let to_stdout
= if rpcenv
.env_type() == RpcEnvironmentType
::CLI { true }
else { false }
;
604 let upid_str
= WorkerTask
::new_thread(
611 let (mut changer
, _changer_name
) = required_media_changer(&config
, &drive
)?
;
613 worker
.log("Starting drive clean");
615 changer
.clean_drive()?
;
617 worker
.log("Drive cleaned sucessfully");
629 schema
: DRIVE_NAME_SCHEMA
,
634 description
: "The list of media labels with associated media Uuid (if any).",
641 /// List known media labels (Changer Inventory)
643 /// Note: Only useful for drives with associated changer device.
645 /// This method queries the changer to get a list of media labels.
647 /// Note: This updates the media online status.
648 pub async
fn inventory(
650 ) -> Result
<Vec
<LabelUuidMap
>, Error
> {
652 let (config
, _digest
) = config
::drive
::config()?
;
654 tokio
::task
::spawn_blocking(move || {
655 let (mut changer
, changer_name
) = required_media_changer(&config
, &drive
)?
;
657 let label_text_list
= changer
.online_media_label_texts()?
;
659 let state_path
= Path
::new(TAPE_STATUS_DIR
);
661 let mut inventory
= Inventory
::load(state_path
)?
;
663 update_changer_online_status(
670 let mut list
= Vec
::new();
672 for label_text
in label_text_list
.iter() {
673 if label_text
.starts_with("CLN") {
674 // skip cleaning unit
678 let label_text
= label_text
.to_string();
680 if let Some(media_id
) = inventory
.find_media_by_label_text(&label_text
) {
681 list
.push(LabelUuidMap { label_text, uuid: Some(media_id.label.uuid.to_string()) }
);
683 list
.push(LabelUuidMap { label_text, uuid: None }
);
695 schema
: DRIVE_NAME_SCHEMA
,
698 description
: "Load all tapes and try read labels (even if already inventoried)",
710 /// Note: Only useful for drives with associated changer device.
712 /// This method queries the changer to get a list of media labels. It
713 /// then loads any unknown media into the drive, reads the label, and
714 /// store the result to the media database.
716 /// Note: This updates the media online status.
717 pub fn update_inventory(
719 read_all_labels
: Option
<bool
>,
720 rpcenv
: &mut dyn RpcEnvironment
,
721 ) -> Result
<Value
, Error
> {
723 let (config
, _digest
) = config
::drive
::config()?
;
725 check_drive_exists(&config
, &drive
)?
; // early check before starting worker
727 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
729 let to_stdout
= if rpcenv
.env_type() == RpcEnvironmentType
::CLI { true }
else { false }
;
731 let upid_str
= WorkerTask
::new_thread(
738 let (mut changer
, changer_name
) = required_media_changer(&config
, &drive
)?
;
740 let label_text_list
= changer
.online_media_label_texts()?
;
741 if label_text_list
.is_empty() {
742 worker
.log(format
!("changer device does not list any media labels"));
745 let state_path
= Path
::new(TAPE_STATUS_DIR
);
747 let mut inventory
= Inventory
::load(state_path
)?
;
749 update_changer_online_status(&config
, &mut inventory
, &changer_name
, &label_text_list
)?
;
751 for label_text
in label_text_list
.iter() {
752 if label_text
.starts_with("CLN") {
753 worker
.log(format
!("skip cleaning unit '{}'", label_text
));
757 let label_text
= label_text
.to_string();
759 if !read_all_labels
.unwrap_or(false) {
760 if let Some(_
) = inventory
.find_media_by_label_text(&label_text
) {
761 worker
.log(format
!("media '{}' already inventoried", label_text
));
766 if let Err(err
) = changer
.load_media(&label_text
) {
767 worker
.warn(format
!("unable to load media '{}' - {}", label_text
, err
));
771 let mut drive
= open_drive(&config
, &drive
)?
;
772 match drive
.read_label() {
774 worker
.warn(format
!("unable to read label form media '{}' - {}", label_text
, err
));
777 worker
.log(format
!("media '{}' is empty", label_text
));
779 Ok((Some(media_id
), _key_config
)) => {
780 if label_text
!= media_id
.label
.label_text
{
781 worker
.warn(format
!("label text missmatch ({} != {})", label_text
, media_id
.label
.label_text
));
784 worker
.log(format
!("inventorize media '{}' with uuid '{}'", label_text
, media_id
.label
.uuid
));
785 inventory
.store(media_id
, false)?
;
788 changer
.unload_media(None
)?
;
802 schema
: DRIVE_NAME_SCHEMA
,
805 schema
: MEDIA_POOL_NAME_SCHEMA
,
814 /// Label media with barcodes from changer device
815 pub fn barcode_label_media(
817 pool
: Option
<String
>,
818 rpcenv
: &mut dyn RpcEnvironment
,
819 ) -> Result
<Value
, Error
> {
821 if let Some(ref pool
) = pool
{
822 let (pool_config
, _digest
) = config
::media_pool
::config()?
;
824 if pool_config
.sections
.get(pool
).is_none() {
825 bail
!("no such pool ('{}')", pool
);
829 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
831 let to_stdout
= if rpcenv
.env_type() == RpcEnvironmentType
::CLI { true }
else { false }
;
833 let upid_str
= WorkerTask
::new_thread(
834 "barcode-label-media",
839 barcode_label_media_worker(worker
, drive
, pool
)
846 fn barcode_label_media_worker(
847 worker
: Arc
<WorkerTask
>,
849 pool
: Option
<String
>,
850 ) -> Result
<(), Error
> {
852 let (config
, _digest
) = config
::drive
::config()?
;
854 let (mut changer
, changer_name
) = required_media_changer(&config
, &drive
)?
;
856 let label_text_list
= changer
.online_media_label_texts()?
;
858 let state_path
= Path
::new(TAPE_STATUS_DIR
);
860 let mut inventory
= Inventory
::load(state_path
)?
;
862 update_changer_online_status(&config
, &mut inventory
, &changer_name
, &label_text_list
)?
;
864 if label_text_list
.is_empty() {
865 bail
!("changer device does not list any media labels");
868 for label_text
in label_text_list
{
869 if label_text
.starts_with("CLN") { continue; }
872 if inventory
.find_media_by_label_text(&label_text
).is_some() {
873 worker
.log(format
!("media '{}' already inventoried (already labeled)", label_text
));
877 worker
.log(format
!("checking/loading media '{}'", label_text
));
879 if let Err(err
) = changer
.load_media(&label_text
) {
880 worker
.warn(format
!("unable to load media '{}' - {}", label_text
, err
));
884 let mut drive
= open_drive(&config
, &drive
)?
;
887 match drive
.read_next_file() {
889 worker
.log(format
!("media '{}' is not empty (erase first)", label_text
));
892 Ok(None
) => { /* EOF mark at BOT, assume tape is empty */ }
,
894 if err
.is_errno(nix
::errno
::Errno
::ENOSPC
) || err
.is_errno(nix
::errno
::Errno
::EIO
) {
895 /* assume tape is empty */
897 worker
.warn(format
!("media '{}' read error (maybe not empty - erase first)", label_text
));
903 let ctime
= proxmox
::tools
::time
::epoch_i64();
904 let label
= MediaLabel
{
905 label_text
: label_text
.to_string(),
906 uuid
: Uuid
::generate(),
910 write_media_label(worker
.clone(), &mut drive
, label
, pool
.clone())?
920 schema
: DRIVE_NAME_SCHEMA
,
925 description
: "A List of medium auxiliary memory attributes.",
932 /// Read Cartridge Memory (Medium auxiliary memory attributes)
933 pub fn cartridge_memory(drive
: String
) -> Result
<Vec
<MamAttribute
>, Error
> {
935 let (config
, _digest
) = config
::drive
::config()?
;
937 let drive_config
: LinuxTapeDrive
= config
.lookup("linux", &drive
)?
;
938 let mut handle
= drive_config
.open()?
;
940 handle
.cartridge_memory()
947 schema
: DRIVE_NAME_SCHEMA
,
952 type: LinuxDriveAndMediaStatus
,
955 /// Get drive/media status
956 pub fn status(drive
: String
) -> Result
<LinuxDriveAndMediaStatus
, Error
> {
958 let (config
, _digest
) = config
::drive
::config()?
;
960 let drive_config
: LinuxTapeDrive
= config
.lookup("linux", &drive
)?
;
962 // Note: use open_linux_tape_device, because this also works if no medium loaded
963 let file
= open_linux_tape_device(&drive_config
.path
)?
;
965 let mut handle
= LinuxTapeHandle
::new(file
);
967 handle
.get_drive_and_media_status()
974 schema
: DRIVE_NAME_SCHEMA
,
977 description
: "Force overriding existing index.",
982 description
: "Verbose mode - log all found chunks.",
992 /// Scan media and record content
993 pub fn catalog_media(
996 verbose
: Option
<bool
>,
997 rpcenv
: &mut dyn RpcEnvironment
,
998 ) -> Result
<Value
, Error
> {
1000 let verbose
= verbose
.unwrap_or(false);
1001 let force
= force
.unwrap_or(false);
1003 let (config
, _digest
) = config
::drive
::config()?
;
1005 check_drive_exists(&config
, &drive
)?
; // early check before starting worker
1007 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
1009 let to_stdout
= if rpcenv
.env_type() == RpcEnvironmentType
::CLI { true }
else { false }
;
1011 let upid_str
= WorkerTask
::new_thread(
1013 Some(drive
.clone()),
1018 let mut drive
= open_drive(&config
, &drive
)?
;
1022 let media_id
= match drive
.read_label()?
{
1023 (Some(media_id
), key_config
) => {
1025 "found media label: {}",
1026 serde_json
::to_string_pretty(&serde_json
::to_value(&media_id
)?
)?
1028 if key_config
.is_some() {
1030 "encryption key config: {}",
1031 serde_json
::to_string_pretty(&serde_json
::to_value(&key_config
)?
)?
1036 (None
, _
) => bail
!("media is empty (no media label found)"),
1039 let status_path
= Path
::new(TAPE_STATUS_DIR
);
1041 let mut inventory
= Inventory
::load(status_path
)?
;
1042 inventory
.store(media_id
.clone(), false)?
;
1044 let pool
= match media_id
.media_set_label
{
1046 worker
.log("media is empty");
1047 MediaCatalog
::destroy(status_path
, &media_id
.label
.uuid
)?
;
1051 if set
.uuid
.as_ref() == [0u8;16] { // media is empty
1052 worker
.log("media is empty");
1053 MediaCatalog
::destroy(status_path
, &media_id
.label
.uuid
)?
;
1056 let encrypt_fingerprint
= set
.encryption_key_fingerprint
.clone()
1057 .map(|fp
| (fp
, set
.uuid
.clone()));
1059 drive
.set_encryption(encrypt_fingerprint
)?
;
1065 let _lock
= MediaPool
::lock(status_path
, &pool
)?
;
1067 if MediaCatalog
::exists(status_path
, &media_id
.label
.uuid
) {
1069 bail
!("media catalog exists (please use --force to overwrite)");
1073 restore_media(&worker
, &mut drive
, &media_id
, None
, verbose
)?
;
1084 pub const SUBDIRS
: SubdirMap
= &sorted
!([
1086 "barcode-label-media",
1088 .put(&API_METHOD_BARCODE_LABEL_MEDIA
)
1093 .put(&API_METHOD_CATALOG_MEDIA
)
1098 .put(&API_METHOD_CLEAN_DRIVE
)
1103 .put(&API_METHOD_EJECT_MEDIA
)
1108 .put(&API_METHOD_ERASE_MEDIA
)
1113 .get(&API_METHOD_INVENTORY
)
1114 .put(&API_METHOD_UPDATE_INVENTORY
)
1119 .put(&API_METHOD_LABEL_MEDIA
)
1124 .put(&API_METHOD_LOAD_SLOT
)
1129 .put(&API_METHOD_CARTRIDGE_MEMORY
)
1134 .get(&API_METHOD_READ_LABEL
)
1139 .put(&API_METHOD_REWIND
)
1144 .get(&API_METHOD_SCAN_DRIVES
)
1149 .get(&API_METHOD_STATUS
)
1154 .put(&API_METHOD_UNLOAD
)
1158 pub const ROUTER
: Router
= Router
::new()
1159 .get(&list_subdirs_api_method
!(SUBDIRS
))