4 use anyhow
::{bail, format_err, Error}
;
10 list_subdirs_api_method
,
24 drive
::check_drive_exists
,
30 MEDIA_POOL_NAME_SCHEMA
,
50 linux_tape_device_list
,
54 update_changer_online_status
,
66 schema
: DRIVE_NAME_SCHEMA
,
69 description
: "Source slot number",
75 /// Load media via changer from slot
76 pub async
fn load_slot(
80 ) -> Result
<(), Error
> {
82 let (config
, _digest
) = config
::drive
::config()?
;
84 let drive_config
: LinuxTapeDrive
= config
.lookup("linux", &drive
)?
;
86 let changer
: ScsiTapeChanger
= match drive_config
.changer
{
87 Some(ref changer
) => config
.lookup("changer", changer
)?
,
88 None
=> bail
!("drive '{}' has no associated changer", drive
),
91 tokio
::task
::spawn_blocking(move || {
92 let drivenum
= drive_config
.changer_drive_id
.unwrap_or(0);
93 mtx_load(&changer
.path
, slot
, drivenum
)
101 schema
: DRIVE_NAME_SCHEMA
,
104 schema
: MEDIA_LABEL_SCHEMA
,
109 /// Load media with specified label
111 /// Issue a media load request to the associated changer device.
112 pub async
fn load_media(drive
: String
, changer_id
: String
) -> Result
<(), Error
> {
114 let (config
, _digest
) = config
::drive
::config()?
;
116 tokio
::task
::spawn_blocking(move || {
117 let (mut changer
, _
) = media_changer(&config
, &drive
, false)?
;
118 changer
.load_media(&changer_id
)
126 schema
: DRIVE_NAME_SCHEMA
,
129 description
: "Target slot number. If omitted, defaults to the slot that the drive was loaded from.",
136 /// Unload media via changer
141 ) -> Result
<(), Error
> {
143 let (config
, _digest
) = config
::drive
::config()?
;
145 let mut drive_config
: LinuxTapeDrive
= config
.lookup("linux", &drive
)?
;
147 let changer
: ScsiTapeChanger
= match drive_config
.changer
{
148 Some(ref changer
) => config
.lookup("changer", changer
)?
,
149 None
=> bail
!("drive '{}' has no associated changer", drive
),
152 let drivenum
= drive_config
.changer_drive_id
.unwrap_or(0);
154 tokio
::task
::spawn_blocking(move || {
155 if let Some(slot
) = slot
{
156 mtx_unload(&changer
.path
, slot
, drivenum
)
158 drive_config
.unload_media()
168 description
: "The list of autodetected tape drives.",
171 type: TapeDeviceInfo
,
176 pub fn scan_drives(_param
: Value
) -> Result
<Vec
<TapeDeviceInfo
>, Error
> {
178 let list
= linux_tape_device_list();
187 schema
: DRIVE_NAME_SCHEMA
,
190 description
: "Use fast erase.",
205 rpcenv
: &mut dyn RpcEnvironment
,
206 ) -> Result
<Value
, Error
> {
208 let (config
, _digest
) = config
::drive
::config()?
;
210 check_drive_exists(&config
, &drive
)?
; // early check before starting worker
212 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
214 let upid_str
= WorkerTask
::new_thread(
220 let mut drive
= open_drive(&config
, &drive
)?
;
221 drive
.erase_media(fast
.unwrap_or(true))?
;
233 schema
: DRIVE_NAME_SCHEMA
,
244 rpcenv
: &mut dyn RpcEnvironment
,
245 ) -> Result
<Value
, Error
> {
247 let (config
, _digest
) = config
::drive
::config()?
;
249 check_drive_exists(&config
, &drive
)?
; // early check before starting worker
251 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
253 let upid_str
= WorkerTask
::new_thread(
259 let mut drive
= open_drive(&config
, &drive
)?
;
272 schema
: DRIVE_NAME_SCHEMA
,
277 /// Eject/Unload drive media
278 pub async
fn eject_media(drive
: String
) -> Result
<(), Error
> {
280 let (config
, _digest
) = config
::drive
::config()?
;
282 tokio
::task
::spawn_blocking(move || {
283 let (mut changer
, _
) = media_changer(&config
, &drive
, false)?
;
285 if !changer
.eject_on_unload() {
286 let mut drive
= open_drive(&config
, &drive
)?
;
287 drive
.eject_media()?
;
290 changer
.unload_media()
298 schema
: DRIVE_NAME_SCHEMA
,
301 schema
: MEDIA_LABEL_SCHEMA
,
304 schema
: MEDIA_POOL_NAME_SCHEMA
,
315 /// Write a new media label to the media in 'drive'. The media is
316 /// assigned to the specified 'pool', or else to the free media pool.
318 /// Note: The media need to be empty (you may want to erase it first).
321 pool
: Option
<String
>,
323 rpcenv
: &mut dyn RpcEnvironment
,
324 ) -> Result
<Value
, Error
> {
326 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
328 if let Some(ref pool
) = pool
{
329 let (pool_config
, _digest
) = config
::media_pool
::config()?
;
331 if pool_config
.sections
.get(pool
).is_none() {
332 bail
!("no such pool ('{}')", pool
);
336 let (config
, _digest
) = config
::drive
::config()?
;
338 let upid_str
= WorkerTask
::new_thread(
345 let mut drive
= open_drive(&config
, &drive
)?
;
349 match drive
.read_next_file() {
350 Ok(Some(_file
)) => bail
!("media is not empty (erase first)"),
351 Ok(None
) => { /* EOF mark at BOT, assume tape is empty */ }
,
353 if err
.is_errno(nix
::errno
::Errno
::ENOSPC
) || err
.is_errno(nix
::errno
::Errno
::EIO
) {
354 /* assume tape is empty */
356 bail
!("media read error - {}", err
);
361 let ctime
= proxmox
::tools
::time
::epoch_i64();
362 let label
= MediaLabel
{
363 changer_id
: changer_id
.to_string(),
364 uuid
: Uuid
::generate(),
368 write_media_label(worker
, &mut drive
, label
, pool
)
375 fn write_media_label(
376 worker
: Arc
<WorkerTask
>,
377 drive
: &mut Box
<dyn TapeDriver
>,
379 pool
: Option
<String
>,
380 ) -> Result
<(), Error
> {
382 drive
.label_tape(&label
)?
;
384 let mut media_set_label
= None
;
386 if let Some(ref pool
) = pool
{
387 // assign media to pool by writing special media set label
388 worker
.log(format
!("Label media '{}' for pool '{}'", label
.changer_id
, pool
));
389 let set
= MediaSetLabel
::with_data(&pool
, [0u8; 16].into(), 0, label
.ctime
);
391 drive
.write_media_set_label(&set
)?
;
392 media_set_label
= Some(set
);
394 worker
.log(format
!("Label media '{}' (no pool assignment)", label
.changer_id
));
397 let media_id
= MediaId { label, media_set_label }
;
399 let mut inventory
= Inventory
::load(Path
::new(TAPE_STATUS_DIR
))?
;
400 inventory
.store(media_id
.clone())?
;
404 match drive
.read_label() {
406 if info
.label
.uuid
!= media_id
.label
.uuid
{
407 bail
!("verify label failed - got wrong label uuid");
409 if let Some(ref pool
) = pool
{
410 match info
.media_set_label
{
412 if set
.uuid
!= [0u8; 16].into() {
413 bail
!("verify media set label failed - got wrong set uuid");
415 if &set
.pool
!= pool
{
416 bail
!("verify media set label failed - got wrong pool");
420 bail
!("verify media set label failed (missing set label)");
425 Ok(None
) => bail
!("verify label failed (got empty media)"),
426 Err(err
) => bail
!("verify label failed - {}", err
),
438 schema
: DRIVE_NAME_SCHEMA
,
447 pub async
fn read_label(drive
: String
) -> Result
<MediaIdFlat
, Error
> {
449 let (config
, _digest
) = config
::drive
::config()?
;
451 tokio
::task
::spawn_blocking(move || {
452 let mut drive
= open_drive(&config
, &drive
)?
;
454 let media_id
= drive
.read_label()?
;
456 let media_id
= match media_id
{
458 let mut flat
= MediaIdFlat
{
459 uuid
: media_id
.label
.uuid
.to_string(),
460 changer_id
: media_id
.label
.changer_id
.clone(),
461 ctime
: media_id
.label
.ctime
,
462 media_set_ctime
: None
,
463 media_set_uuid
: None
,
467 if let Some(set
) = media_id
.media_set_label
{
468 flat
.pool
= Some(set
.pool
.clone());
469 flat
.seq_nr
= Some(set
.seq_nr
);
470 flat
.media_set_uuid
= Some(set
.uuid
.to_string());
471 flat
.media_set_ctime
= Some(set
.ctime
);
476 bail
!("Media is empty (no label).");
488 schema
: DRIVE_NAME_SCHEMA
,
493 description
: "The list of media labels with associated media Uuid (if any).",
500 /// List known media labels (Changer Inventory)
502 /// Note: Only useful for drives with associated changer device.
504 /// This method queries the changer to get a list of media labels.
506 /// Note: This updates the media online status.
507 pub async
fn inventory(
509 ) -> Result
<Vec
<LabelUuidMap
>, Error
> {
511 let (config
, _digest
) = config
::drive
::config()?
;
513 tokio
::task
::spawn_blocking(move || {
514 let (changer
, changer_name
) = media_changer(&config
, &drive
, false)?
;
516 let changer_id_list
= changer
.list_media_changer_ids()?
;
518 let state_path
= Path
::new(TAPE_STATUS_DIR
);
520 let mut inventory
= Inventory
::load(state_path
)?
;
521 let mut state_db
= MediaStateDatabase
::load(state_path
)?
;
523 update_changer_online_status(
531 let mut list
= Vec
::new();
533 for changer_id
in changer_id_list
.iter() {
534 if changer_id
.starts_with("CLN") {
535 // skip cleaning unit
539 let changer_id
= changer_id
.to_string();
541 if let Some(media_id
) = inventory
.find_media_by_changer_id(&changer_id
) {
542 list
.push(LabelUuidMap { changer_id, uuid: Some(media_id.label.uuid.to_string()) }
);
544 list
.push(LabelUuidMap { changer_id, uuid: None }
);
556 schema
: DRIVE_NAME_SCHEMA
,
559 description
: "Load all tapes and try read labels (even if already inventoried)",
571 /// Note: Only useful for drives with associated changer device.
573 /// This method queries the changer to get a list of media labels. It
574 /// then loads any unknown media into the drive, reads the label, and
575 /// store the result to the media database.
577 /// Note: This updates the media online status.
578 pub fn update_inventory(
580 read_all_labels
: Option
<bool
>,
581 rpcenv
: &mut dyn RpcEnvironment
,
582 ) -> Result
<Value
, Error
> {
584 let (config
, _digest
) = config
::drive
::config()?
;
586 check_drive_exists(&config
, &drive
)?
; // early check before starting worker
588 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
590 let upid_str
= WorkerTask
::new_thread(
597 let (mut changer
, changer_name
) = media_changer(&config
, &drive
, false)?
;
599 let changer_id_list
= changer
.list_media_changer_ids()?
;
600 if changer_id_list
.is_empty() {
601 worker
.log(format
!("changer device does not list any media labels"));
604 let state_path
= Path
::new(TAPE_STATUS_DIR
);
606 let mut inventory
= Inventory
::load(state_path
)?
;
607 let mut state_db
= MediaStateDatabase
::load(state_path
)?
;
609 update_changer_online_status(&config
, &mut inventory
, &mut state_db
, &changer_name
, &changer_id_list
)?
;
611 for changer_id
in changer_id_list
.iter() {
612 if changer_id
.starts_with("CLN") {
613 worker
.log(format
!("skip cleaning unit '{}'", changer_id
));
617 let changer_id
= changer_id
.to_string();
619 if !read_all_labels
.unwrap_or(false) {
620 if let Some(_
) = inventory
.find_media_by_changer_id(&changer_id
) {
621 worker
.log(format
!("media '{}' already inventoried", changer_id
));
626 if let Err(err
) = changer
.load_media(&changer_id
) {
627 worker
.warn(format
!("unable to load media '{}' - {}", changer_id
, err
));
631 let mut drive
= open_drive(&config
, &drive
)?
;
632 match drive
.read_label() {
634 worker
.warn(format
!("unable to read label form media '{}' - {}", changer_id
, err
));
637 worker
.log(format
!("media '{}' is empty", changer_id
));
639 Ok(Some(media_id
)) => {
640 if changer_id
!= media_id
.label
.changer_id
{
641 worker
.warn(format
!("label changer ID missmatch ({} != {})", changer_id
, media_id
.label
.changer_id
));
644 worker
.log(format
!("inventorize media '{}' with uuid '{}'", changer_id
, media_id
.label
.uuid
));
645 inventory
.store(media_id
)?
;
661 schema
: DRIVE_NAME_SCHEMA
,
664 schema
: MEDIA_POOL_NAME_SCHEMA
,
673 /// Label media with barcodes from changer device
674 pub fn barcode_label_media(
676 pool
: Option
<String
>,
677 rpcenv
: &mut dyn RpcEnvironment
,
678 ) -> Result
<Value
, Error
> {
680 if let Some(ref pool
) = pool
{
681 let (pool_config
, _digest
) = config
::media_pool
::config()?
;
683 if pool_config
.sections
.get(pool
).is_none() {
684 bail
!("no such pool ('{}')", pool
);
688 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
690 let upid_str
= WorkerTask
::new_thread(
691 "barcode-label-media",
696 barcode_label_media_worker(worker
, drive
, pool
)
703 fn barcode_label_media_worker(
704 worker
: Arc
<WorkerTask
>,
706 pool
: Option
<String
>,
707 ) -> Result
<(), Error
> {
709 let (config
, _digest
) = config
::drive
::config()?
;
711 let (mut changer
, changer_name
) = media_changer(&config
, &drive
, false)?
;
713 let changer_id_list
= changer
.list_media_changer_ids()?
;
715 let state_path
= Path
::new(TAPE_STATUS_DIR
);
717 let mut inventory
= Inventory
::load(state_path
)?
;
718 let mut state_db
= MediaStateDatabase
::load(state_path
)?
;
720 update_changer_online_status(&config
, &mut inventory
, &mut state_db
, &changer_name
, &changer_id_list
)?
;
722 if changer_id_list
.is_empty() {
723 bail
!("changer device does not list any media labels");
726 for changer_id
in changer_id_list
{
727 if changer_id
.starts_with("CLN") { continue; }
730 if inventory
.find_media_by_changer_id(&changer_id
).is_some() {
731 worker
.log(format
!("media '{}' already inventoried (already labeled)", changer_id
));
735 worker
.log(format
!("checking/loading media '{}'", changer_id
));
737 if let Err(err
) = changer
.load_media(&changer_id
) {
738 worker
.warn(format
!("unable to load media '{}' - {}", changer_id
, err
));
742 let mut drive
= open_drive(&config
, &drive
)?
;
745 match drive
.read_next_file() {
747 worker
.log(format
!("media '{}' is not empty (erase first)", changer_id
));
750 Ok(None
) => { /* EOF mark at BOT, assume tape is empty */ }
,
752 if err
.is_errno(nix
::errno
::Errno
::ENOSPC
) || err
.is_errno(nix
::errno
::Errno
::EIO
) {
753 /* assume tape is empty */
755 worker
.warn(format
!("media '{}' read error (maybe not empty - erase first)", changer_id
));
761 let ctime
= proxmox
::tools
::time
::epoch_i64();
762 let label
= MediaLabel
{
763 changer_id
: changer_id
.to_string(),
764 uuid
: Uuid
::generate(),
768 write_media_label(worker
.clone(), &mut drive
, label
, pool
.clone())?
778 schema
: DRIVE_NAME_SCHEMA
,
783 description
: "A List of medium auxiliary memory attributes.",
790 /// Read Cartridge Memory (Medium auxiliary memory attributes)
791 pub fn cartridge_memory(drive
: String
) -> Result
<Vec
<MamAttribute
>, Error
> {
793 let (config
, _digest
) = config
::drive
::config()?
;
795 let drive_config
: LinuxTapeDrive
= config
.lookup("linux", &drive
)?
;
797 read_mam_attributes(&drive_config
.path
)
804 schema
: DRIVE_NAME_SCHEMA
,
809 type: LinuxDriveStatusFlat
,
813 pub fn status(drive
: String
) -> Result
<LinuxDriveStatusFlat
, Error
> {
815 let (config
, _digest
) = config
::drive
::config()?
;
817 let drive_config
: LinuxTapeDrive
= config
.lookup("linux", &drive
)?
;
819 let handle
= drive_config
.open()
820 .map_err(|err
| format_err
!("open drive '{}' ({}) failed - {}", drive
, drive_config
.path
, err
))?
;
822 let drive_status
= handle
.get_drive_status()?
;
824 Ok(drive_status
.into())
828 pub const SUBDIRS
: SubdirMap
= &sorted
!([
830 "barcode-label-media",
832 .put(&API_METHOD_BARCODE_LABEL_MEDIA
)
837 .put(&API_METHOD_EJECT_MEDIA
)
842 .put(&API_METHOD_ERASE_MEDIA
)
847 .get(&API_METHOD_INVENTORY
)
848 .put(&API_METHOD_UPDATE_INVENTORY
)
853 .put(&API_METHOD_LABEL_MEDIA
)
858 .put(&API_METHOD_LOAD_SLOT
)
863 .put(&API_METHOD_CARTRIDGE_MEMORY
)
868 .get(&API_METHOD_READ_LABEL
)
873 .put(&API_METHOD_REWIND
)
878 .get(&API_METHOD_SCAN_DRIVES
)
883 .get(&API_METHOD_STATUS
)
888 .put(&API_METHOD_UNLOAD
)
892 pub const ROUTER
: Router
= Router
::new()
893 .get(&list_subdirs_api_method
!(SUBDIRS
))