4 use anyhow
::{bail, Error}
;
10 list_subdirs_api_method
,
24 drive
::check_drive_exists
,
30 MEDIA_POOL_NAME_SCHEMA
,
48 linux_tape_device_list
,
51 update_changer_online_status
,
63 schema
: DRIVE_NAME_SCHEMA
,
66 description
: "Source slot number",
72 /// Load media via changer from slot
73 pub async
fn load_slot(
77 ) -> Result
<(), Error
> {
79 let (config
, _digest
) = config
::drive
::config()?
;
81 let drive_config
: LinuxTapeDrive
= config
.lookup("linux", &drive
)?
;
83 let changer
: ScsiTapeChanger
= match drive_config
.changer
{
84 Some(ref changer
) => config
.lookup("changer", changer
)?
,
85 None
=> bail
!("drive '{}' has no associated changer", drive
),
88 tokio
::task
::spawn_blocking(move || {
89 let drivenum
= drive_config
.changer_drive_id
.unwrap_or(0);
90 mtx_load(&changer
.path
, slot
, drivenum
)
98 schema
: DRIVE_NAME_SCHEMA
,
101 schema
: MEDIA_LABEL_SCHEMA
,
106 /// Load media with specified label
108 /// Issue a media load request to the associated changer device.
109 pub async
fn load_media(drive
: String
, changer_id
: String
) -> Result
<(), Error
> {
111 let (config
, _digest
) = config
::drive
::config()?
;
113 tokio
::task
::spawn_blocking(move || {
114 let (mut changer
, _
) = media_changer(&config
, &drive
, false)?
;
115 changer
.load_media(&changer_id
)
123 schema
: DRIVE_NAME_SCHEMA
,
126 description
: "Target slot number. If omitted, defaults to the slot that the drive was loaded from.",
133 /// Unload media via changer
138 ) -> Result
<(), Error
> {
140 let (config
, _digest
) = config
::drive
::config()?
;
142 let mut drive_config
: LinuxTapeDrive
= config
.lookup("linux", &drive
)?
;
144 let changer
: ScsiTapeChanger
= match drive_config
.changer
{
145 Some(ref changer
) => config
.lookup("changer", changer
)?
,
146 None
=> bail
!("drive '{}' has no associated changer", drive
),
149 let drivenum
= drive_config
.changer_drive_id
.unwrap_or(0);
151 tokio
::task
::spawn_blocking(move || {
152 if let Some(slot
) = slot
{
153 mtx_unload(&changer
.path
, slot
, drivenum
)
155 drive_config
.unload_media()
165 description
: "The list of autodetected tape drives.",
168 type: TapeDeviceInfo
,
173 pub fn scan_drives(_param
: Value
) -> Result
<Vec
<TapeDeviceInfo
>, Error
> {
175 let list
= linux_tape_device_list();
184 schema
: DRIVE_NAME_SCHEMA
,
187 description
: "Use fast erase.",
202 rpcenv
: &mut dyn RpcEnvironment
,
203 ) -> Result
<Value
, Error
> {
205 let (config
, _digest
) = config
::drive
::config()?
;
207 check_drive_exists(&config
, &drive
)?
; // early check before starting worker
209 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
211 let upid_str
= WorkerTask
::new_thread(
217 let mut drive
= open_drive(&config
, &drive
)?
;
218 drive
.erase_media(fast
.unwrap_or(true))?
;
230 schema
: DRIVE_NAME_SCHEMA
,
241 rpcenv
: &mut dyn RpcEnvironment
,
242 ) -> Result
<Value
, Error
> {
244 let (config
, _digest
) = config
::drive
::config()?
;
246 check_drive_exists(&config
, &drive
)?
; // early check before starting worker
248 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
250 let upid_str
= WorkerTask
::new_thread(
256 let mut drive
= open_drive(&config
, &drive
)?
;
269 schema
: DRIVE_NAME_SCHEMA
,
274 /// Eject/Unload drive media
275 pub async
fn eject_media(drive
: String
) -> Result
<(), Error
> {
277 let (config
, _digest
) = config
::drive
::config()?
;
279 tokio
::task
::spawn_blocking(move || {
280 let (mut changer
, _
) = media_changer(&config
, &drive
, false)?
;
282 if !changer
.eject_on_unload() {
283 let mut drive
= open_drive(&config
, &drive
)?
;
284 drive
.eject_media()?
;
287 changer
.unload_media()
295 schema
: DRIVE_NAME_SCHEMA
,
298 schema
: MEDIA_LABEL_SCHEMA
,
301 schema
: MEDIA_POOL_NAME_SCHEMA
,
312 /// Write a new media label to the media in 'drive'. The media is
313 /// assigned to the specified 'pool', or else to the free media pool.
315 /// Note: The media need to be empty (you may want to erase it first).
318 pool
: Option
<String
>,
320 rpcenv
: &mut dyn RpcEnvironment
,
321 ) -> Result
<Value
, Error
> {
323 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
325 if let Some(ref pool
) = pool
{
326 let (pool_config
, _digest
) = config
::media_pool
::config()?
;
328 if pool_config
.sections
.get(pool
).is_none() {
329 bail
!("no such pool ('{}')", pool
);
333 let (config
, _digest
) = config
::drive
::config()?
;
335 let upid_str
= WorkerTask
::new_thread(
342 let mut drive
= open_drive(&config
, &drive
)?
;
346 match drive
.read_next_file() {
347 Ok(Some(_file
)) => bail
!("media is not empty (erase first)"),
348 Ok(None
) => { /* EOF mark at BOT, assume tape is empty */ }
,
350 if err
.is_errno(nix
::errno
::Errno
::ENOSPC
) || err
.is_errno(nix
::errno
::Errno
::EIO
) {
351 /* assume tape is empty */
353 bail
!("media read error - {}", err
);
358 let ctime
= proxmox
::tools
::time
::epoch_i64();
359 let label
= MediaLabel
{
360 changer_id
: changer_id
.to_string(),
361 uuid
: Uuid
::generate(),
365 write_media_label(worker
, &mut drive
, label
, pool
)
372 fn write_media_label(
373 worker
: Arc
<WorkerTask
>,
374 drive
: &mut Box
<dyn TapeDriver
>,
376 pool
: Option
<String
>,
377 ) -> Result
<(), Error
> {
379 drive
.label_tape(&label
)?
;
381 let mut media_set_label
= None
;
383 if let Some(ref pool
) = pool
{
384 // assign media to pool by writing special media set label
385 worker
.log(format
!("Label media '{}' for pool '{}'", label
.changer_id
, pool
));
386 let set
= MediaSetLabel
::with_data(&pool
, [0u8; 16].into(), 0, label
.ctime
);
388 drive
.write_media_set_label(&set
)?
;
389 media_set_label
= Some(set
);
391 worker
.log(format
!("Label media '{}' (no pool assignment)", label
.changer_id
));
394 let media_id
= MediaId { label, media_set_label }
;
396 let mut inventory
= Inventory
::load(Path
::new(TAPE_STATUS_DIR
))?
;
397 inventory
.store(media_id
.clone())?
;
401 match drive
.read_label() {
403 if info
.label
.uuid
!= media_id
.label
.uuid
{
404 bail
!("verify label failed - got wrong label uuid");
406 if let Some(ref pool
) = pool
{
407 match info
.media_set_label
{
409 if set
.uuid
!= [0u8; 16].into() {
410 bail
!("verify media set label failed - got wrong set uuid");
412 if &set
.pool
!= pool
{
413 bail
!("verify media set label failed - got wrong pool");
417 bail
!("verify media set label failed (missing set label)");
422 Ok(None
) => bail
!("verify label failed (got empty media)"),
423 Err(err
) => bail
!("verify label failed - {}", err
),
435 schema
: DRIVE_NAME_SCHEMA
,
444 pub async
fn read_label(drive
: String
) -> Result
<MediaIdFlat
, Error
> {
446 let (config
, _digest
) = config
::drive
::config()?
;
448 tokio
::task
::spawn_blocking(move || {
449 let mut drive
= open_drive(&config
, &drive
)?
;
451 let media_id
= drive
.read_label()?
;
453 let media_id
= match media_id
{
455 let mut flat
= MediaIdFlat
{
456 uuid
: media_id
.label
.uuid
.to_string(),
457 changer_id
: media_id
.label
.changer_id
.clone(),
458 ctime
: media_id
.label
.ctime
,
459 media_set_ctime
: None
,
460 media_set_uuid
: None
,
464 if let Some(set
) = media_id
.media_set_label
{
465 flat
.pool
= Some(set
.pool
.clone());
466 flat
.seq_nr
= Some(set
.seq_nr
);
467 flat
.media_set_uuid
= Some(set
.uuid
.to_string());
468 flat
.media_set_ctime
= Some(set
.ctime
);
473 bail
!("Media is empty (no label).");
485 schema
: DRIVE_NAME_SCHEMA
,
490 description
: "The list of media labels with associated media Uuid (if any).",
497 /// List known media labels (Changer Inventory)
499 /// Note: Only useful for drives with associated changer device.
501 /// This method queries the changer to get a list of media labels.
503 /// Note: This updates the media online status.
504 pub async
fn inventory(
506 ) -> Result
<Vec
<LabelUuidMap
>, Error
> {
508 let (config
, _digest
) = config
::drive
::config()?
;
510 tokio
::task
::spawn_blocking(move || {
511 let (changer
, changer_name
) = media_changer(&config
, &drive
, false)?
;
513 let changer_id_list
= changer
.list_media_changer_ids()?
;
515 let state_path
= Path
::new(TAPE_STATUS_DIR
);
517 let mut inventory
= Inventory
::load(state_path
)?
;
518 let mut state_db
= MediaStateDatabase
::load(state_path
)?
;
520 update_changer_online_status(
528 let mut list
= Vec
::new();
530 for changer_id
in changer_id_list
.iter() {
531 if changer_id
.starts_with("CLN") {
532 // skip cleaning unit
536 let changer_id
= changer_id
.to_string();
538 if let Some(media_id
) = inventory
.find_media_by_changer_id(&changer_id
) {
539 list
.push(LabelUuidMap { changer_id, uuid: Some(media_id.label.uuid.to_string()) }
);
541 list
.push(LabelUuidMap { changer_id, uuid: None }
);
553 schema
: DRIVE_NAME_SCHEMA
,
556 description
: "Load all tapes and try read labels (even if already inventoried)",
568 /// Note: Only useful for drives with associated changer device.
570 /// This method queries the changer to get a list of media labels. It
571 /// then loads any unknown media into the drive, reads the label, and
572 /// store the result to the media database.
574 /// Note: This updates the media online status.
575 pub fn update_inventory(
577 read_all_labels
: Option
<bool
>,
578 rpcenv
: &mut dyn RpcEnvironment
,
579 ) -> Result
<Value
, Error
> {
581 let (config
, _digest
) = config
::drive
::config()?
;
583 check_drive_exists(&config
, &drive
)?
; // early check before starting worker
585 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
587 let upid_str
= WorkerTask
::new_thread(
594 let (mut changer
, changer_name
) = media_changer(&config
, &drive
, false)?
;
596 let changer_id_list
= changer
.list_media_changer_ids()?
;
597 if changer_id_list
.is_empty() {
598 worker
.log(format
!("changer device does not list any media labels"));
601 let state_path
= Path
::new(TAPE_STATUS_DIR
);
603 let mut inventory
= Inventory
::load(state_path
)?
;
604 let mut state_db
= MediaStateDatabase
::load(state_path
)?
;
606 update_changer_online_status(&config
, &mut inventory
, &mut state_db
, &changer_name
, &changer_id_list
)?
;
608 for changer_id
in changer_id_list
.iter() {
609 if changer_id
.starts_with("CLN") {
610 worker
.log(format
!("skip cleaning unit '{}'", changer_id
));
614 let changer_id
= changer_id
.to_string();
616 if !read_all_labels
.unwrap_or(false) {
617 if let Some(_
) = inventory
.find_media_by_changer_id(&changer_id
) {
618 worker
.log(format
!("media '{}' already inventoried", changer_id
));
623 if let Err(err
) = changer
.load_media(&changer_id
) {
624 worker
.warn(format
!("unable to load media '{}' - {}", changer_id
, err
));
628 let mut drive
= open_drive(&config
, &drive
)?
;
629 match drive
.read_label() {
631 worker
.warn(format
!("unable to read label form media '{}' - {}", changer_id
, err
));
634 worker
.log(format
!("media '{}' is empty", changer_id
));
636 Ok(Some(media_id
)) => {
637 if changer_id
!= media_id
.label
.changer_id
{
638 worker
.warn(format
!("label changer ID missmatch ({} != {})", changer_id
, media_id
.label
.changer_id
));
641 worker
.log(format
!("inventorize media '{}' with uuid '{}'", changer_id
, media_id
.label
.uuid
));
642 inventory
.store(media_id
)?
;
658 schema
: DRIVE_NAME_SCHEMA
,
661 schema
: MEDIA_POOL_NAME_SCHEMA
,
670 /// Label media with barcodes from changer device
671 pub fn barcode_label_media(
673 pool
: Option
<String
>,
674 rpcenv
: &mut dyn RpcEnvironment
,
675 ) -> Result
<Value
, Error
> {
677 if let Some(ref pool
) = pool
{
678 let (pool_config
, _digest
) = config
::media_pool
::config()?
;
680 if pool_config
.sections
.get(pool
).is_none() {
681 bail
!("no such pool ('{}')", pool
);
685 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
687 let upid_str
= WorkerTask
::new_thread(
688 "barcode-label-media",
693 barcode_label_media_worker(worker
, drive
, pool
)
700 fn barcode_label_media_worker(
701 worker
: Arc
<WorkerTask
>,
703 pool
: Option
<String
>,
704 ) -> Result
<(), Error
> {
706 let (config
, _digest
) = config
::drive
::config()?
;
708 let (mut changer
, changer_name
) = media_changer(&config
, &drive
, false)?
;
710 let changer_id_list
= changer
.list_media_changer_ids()?
;
712 let state_path
= Path
::new(TAPE_STATUS_DIR
);
714 let mut inventory
= Inventory
::load(state_path
)?
;
715 let mut state_db
= MediaStateDatabase
::load(state_path
)?
;
717 update_changer_online_status(&config
, &mut inventory
, &mut state_db
, &changer_name
, &changer_id_list
)?
;
719 if changer_id_list
.is_empty() {
720 bail
!("changer device does not list any media labels");
723 for changer_id
in changer_id_list
{
724 if changer_id
.starts_with("CLN") { continue; }
727 if inventory
.find_media_by_changer_id(&changer_id
).is_some() {
728 worker
.log(format
!("media '{}' already inventoried (already labeled)", changer_id
));
732 worker
.log(format
!("checking/loading media '{}'", changer_id
));
734 if let Err(err
) = changer
.load_media(&changer_id
) {
735 worker
.warn(format
!("unable to load media '{}' - {}", changer_id
, err
));
739 let mut drive
= open_drive(&config
, &drive
)?
;
742 match drive
.read_next_file() {
744 worker
.log(format
!("media '{}' is not empty (erase first)", changer_id
));
747 Ok(None
) => { /* EOF mark at BOT, assume tape is empty */ }
,
749 if err
.is_errno(nix
::errno
::Errno
::ENOSPC
) || err
.is_errno(nix
::errno
::Errno
::EIO
) {
750 /* assume tape is empty */
752 worker
.warn(format
!("media '{}' read error (maybe not empty - erase first)", changer_id
));
758 let ctime
= proxmox
::tools
::time
::epoch_i64();
759 let label
= MediaLabel
{
760 changer_id
: changer_id
.to_string(),
761 uuid
: Uuid
::generate(),
765 write_media_label(worker
.clone(), &mut drive
, label
, pool
.clone())?
772 pub const SUBDIRS
: SubdirMap
= &sorted
!([
774 "barcode-label-media",
776 .put(&API_METHOD_BARCODE_LABEL_MEDIA
)
781 .put(&API_METHOD_EJECT_MEDIA
)
786 .put(&API_METHOD_ERASE_MEDIA
)
791 .get(&API_METHOD_INVENTORY
)
792 .put(&API_METHOD_UPDATE_INVENTORY
)
797 .put(&API_METHOD_LABEL_MEDIA
)
802 .put(&API_METHOD_LOAD_SLOT
)
807 .get(&API_METHOD_READ_LABEL
)
812 .put(&API_METHOD_REWIND
)
817 .get(&API_METHOD_SCAN_DRIVES
)
822 .put(&API_METHOD_UNLOAD
)
826 pub const ROUTER
: Router
= Router
::new()
827 .get(&list_subdirs_api_method
!(SUBDIRS
))