1 //! Media changer implementation (SCSI media changer)
10 mod online_status_map
;
11 pub use online_status_map
::*;
13 use std
::collections
::HashSet
;
14 use std
::path
::PathBuf
;
16 use anyhow
::{bail, Error}
;
17 use serde
::{Serialize, Deserialize}
;
18 use serde_json
::Value
;
21 api
::schema
::parse_property_string
,
25 file_read_optional_string
,
29 use crate::api2
::types
::{
35 /// Changer element status.
37 /// Drive and slots may be `Empty`, or contain some media, either
38 /// with knwon volume tag `VolumeTag(String)`, or without (`Full`).
39 #[derive(Serialize, Deserialize, Debug)]
40 pub enum ElementStatus
{
46 /// Changer drive status.
47 #[derive(Serialize, Deserialize)]
48 pub struct DriveStatus
{
49 /// The slot the element was loaded from (if known).
50 pub loaded_slot
: Option
<u64>,
52 pub status
: ElementStatus
,
53 /// Drive Identifier (Serial number)
54 pub drive_serial_number
: Option
<String
>,
56 pub vendor
: Option
<String
>,
58 pub model
: Option
<String
>,
60 pub element_address
: u16,
63 /// Storage element status.
64 #[derive(Serialize, Deserialize)]
65 pub struct StorageElementStatus
{
66 /// Flag for Import/Export slots
67 pub import_export
: bool
,
69 pub status
: ElementStatus
,
71 pub element_address
: u16,
74 /// Transport element status.
75 #[derive(Serialize, Deserialize)]
76 pub struct TransportElementStatus
{
78 pub status
: ElementStatus
,
80 pub element_address
: u16,
83 /// Changer status - show drive/slot usage
84 #[derive(Serialize, Deserialize)]
85 pub struct MtxStatus
{
86 /// List of known drives
87 pub drives
: Vec
<DriveStatus
>,
88 /// List of known storage slots
89 pub slots
: Vec
<StorageElementStatus
>,
92 /// Note: Some libraries do not report transport elements.
93 pub transports
: Vec
<TransportElementStatus
>,
98 pub fn slot_address(&self, slot
: u64) -> Result
<u16, Error
> {
100 bail
!("invalid slot number '{}' (slots numbers starts at 1)", slot
);
102 if slot
> (self.slots
.len() as u64) {
103 bail
!("invalid slot number '{}' (max {} slots)", slot
, self.slots
.len());
106 Ok(self.slots
[(slot
-1) as usize].element_address
)
109 pub fn drive_address(&self, drivenum
: u64) -> Result
<u16, Error
> {
110 if drivenum
>= (self.drives
.len() as u64) {
111 bail
!("invalid drive number '{}'", drivenum
);
114 Ok(self.drives
[drivenum
as usize].element_address
)
117 pub fn transport_address(&self) -> u16 {
118 // simply use first transport
119 // (are there changers exposing more than one?)
120 // defaults to 0 for changer that do not report transports
124 .map(|t
| t
.element_address
)
128 pub fn find_free_slot(&self, import_export
: bool
) -> Option
<u64> {
129 let mut free_slot
= None
;
130 for (i
, slot_info
) in self.slots
.iter().enumerate() {
131 if slot_info
.import_export
!= import_export
{
132 continue; // skip slots of wrong type
134 if let ElementStatus
::Empty
= slot_info
.status
{
135 free_slot
= Some((i
+1) as u64);
142 pub fn mark_import_export_slots(&mut self, config
: &ScsiTapeChanger
) -> Result
<(), Error
>{
143 let mut export_slots
: HashSet
<u64> = HashSet
::new();
145 if let Some(slots
) = &config
.export_slots
{
146 let slots
: Value
= parse_property_string(&slots
, &SLOT_ARRAY_SCHEMA
)?
;
151 .filter_map(|v
| v
.as_u64())
155 for (i
, entry
) in self.slots
.iter_mut().enumerate() {
156 let slot
= i
as u64 + 1;
157 if export_slots
.contains(&slot
) {
158 entry
.import_export
= true; // mark as IMPORT/EXPORT
166 /// Interface to SCSI changer devices
167 pub trait ScsiMediaChange
{
169 fn status(&mut self, use_cache
: bool
) -> Result
<MtxStatus
, Error
>;
171 fn load_slot(&mut self, from_slot
: u64, drivenum
: u64) -> Result
<MtxStatus
, Error
>;
173 fn unload(&mut self, to_slot
: u64, drivenum
: u64) -> Result
<MtxStatus
, Error
>;
175 fn transfer(&mut self, from_slot
: u64, to_slot
: u64) -> Result
<MtxStatus
, Error
>;
178 /// Interface to the media changer device for a single drive
179 pub trait MediaChange
{
181 /// Drive number inside changer
182 fn drive_number(&self) -> u64;
184 /// Drive name (used for debug messages)
185 fn drive_name(&self) -> &str;
187 /// Returns the changer status
188 fn status(&mut self) -> Result
<MtxStatus
, Error
>;
190 /// Transfer media from on slot to another (storage or import export slots)
192 /// Target slot needs to be empty
193 fn transfer_media(&mut self, from
: u64, to
: u64) -> Result
<(), Error
>;
195 /// Load media from storage slot into drive
196 fn load_media_from_slot(&mut self, slot
: u64) -> Result
<(), Error
>;
198 /// Load media by label-text into drive
200 /// This unloads first if the drive is already loaded with another media.
202 /// Note: This refuses to load media inside import/export
203 /// slots. Also, you cannot load cleaning units with this
205 fn load_media(&mut self, label_text
: &str) -> Result
<(), Error
> {
207 if label_text
.starts_with("CLN") {
208 bail
!("unable to load media '{}' (seems to be a cleaning unit)", label_text
);
211 let mut status
= self.status()?
;
213 let mut unload_drive
= false;
216 for (i
, drive_status
) in status
.drives
.iter().enumerate() {
217 if let ElementStatus
::VolumeTag(ref tag
) = drive_status
.status
{
218 if *tag
== label_text
{
219 if i
as u64 != self.drive_number() {
220 bail
!("unable to load media '{}' - media in wrong drive ({} != {})",
221 label_text
, i
, self.drive_number());
223 return Ok(()) // already loaded
226 if i
as u64 == self.drive_number() {
227 match drive_status
.status
{
228 ElementStatus
::Empty
=> { /* OK */ }
,
229 _
=> unload_drive
= true,
235 self.unload_to_free_slot(status
)?
;
236 status
= self.status()?
;
240 for (i
, slot_info
) in status
.slots
.iter().enumerate() {
241 if let ElementStatus
::VolumeTag(ref tag
) = slot_info
.status
{
242 if tag
== label_text
{
243 if slot_info
.import_export
{
244 bail
!("unable to load media '{}' - inside import/export slot", label_text
);
252 let slot
= match slot
{
253 None
=> bail
!("unable to find media '{}' (offline?)", label_text
),
257 self.load_media_from_slot(slot
as u64)
260 /// Unload media from drive (eject media if necessary)
261 fn unload_media(&mut self, target_slot
: Option
<u64>) -> Result
<(), Error
>;
263 /// List online media labels (label_text/barcodes)
265 /// List acessible (online) label texts. This does not include
266 /// media inside import-export slots or cleaning media.
267 fn online_media_label_texts(&mut self) -> Result
<Vec
<String
>, Error
> {
268 let status
= self.status()?
;
270 let mut list
= Vec
::new();
272 for drive_status
in status
.drives
.iter() {
273 if let ElementStatus
::VolumeTag(ref tag
) = drive_status
.status
{
274 list
.push(tag
.clone());
278 for slot_info
in status
.slots
.iter() {
279 if slot_info
.import_export { continue; }
280 if let ElementStatus
::VolumeTag(ref tag
) = slot_info
.status
{
281 if tag
.starts_with("CLN") { continue; }
282 list
.push(tag
.clone());
289 /// Load/Unload cleaning cartridge
291 /// This fail if there is no cleaning cartridge online. Any media
292 /// inside the drive is automatically unloaded.
293 fn clean_drive(&mut self) -> Result
<(), Error
> {
294 let status
= self.status()?
;
296 let mut cleaning_cartridge_slot
= None
;
298 for (i
, slot_info
) in status
.slots
.iter().enumerate() {
299 if slot_info
.import_export { continue; }
300 if let ElementStatus
::VolumeTag(ref tag
) = slot_info
.status
{
301 if tag
.starts_with("CLN") {
302 cleaning_cartridge_slot
= Some(i
+ 1);
308 let cleaning_cartridge_slot
= match cleaning_cartridge_slot
{
309 None
=> bail
!("clean failed - unable to find cleaning cartridge"),
310 Some(cleaning_cartridge_slot
) => cleaning_cartridge_slot
as u64,
313 if let Some(drive_status
) = status
.drives
.get(self.drive_number() as usize) {
314 match drive_status
.status
{
315 ElementStatus
::Empty
=> { /* OK */ }
,
316 _
=> self.unload_to_free_slot(status
)?
,
320 self.load_media_from_slot(cleaning_cartridge_slot
)?
;
322 self.unload_media(Some(cleaning_cartridge_slot
))?
;
329 /// By moving the media to an empty import-export slot. Returns
330 /// Some(slot) if the media was exported. Returns None if the media is
331 /// not online (already exported).
332 fn export_media(&mut self, label_text
: &str) -> Result
<Option
<u64>, Error
> {
333 let status
= self.status()?
;
335 let mut unload_from_drive
= false;
336 if let Some(drive_status
) = status
.drives
.get(self.drive_number() as usize) {
337 if let ElementStatus
::VolumeTag(ref tag
) = drive_status
.status
{
338 if tag
== label_text
{
339 unload_from_drive
= true;
347 for (i
, slot_info
) in status
.slots
.iter().enumerate() {
348 if slot_info
.import_export
{
349 if to
.is_some() { continue; }
350 if let ElementStatus
::Empty
= slot_info
.status
{
351 to
= Some(i
as u64 + 1);
353 } else if let ElementStatus
::VolumeTag(ref tag
) = slot_info
.status
{
354 if tag
== label_text
{
355 from
= Some(i
as u64 + 1);
360 if unload_from_drive
{
363 self.unload_media(Some(to
))?
;
366 None
=> bail
!("unable to find free export slot"),
370 (Some(from
), Some(to
)) => {
371 self.transfer_media(from
, to
)?
;
374 (Some(_from
), None
) => bail
!("unable to find free export slot"),
375 (None
, _
) => Ok(None
), // not online
380 /// Unload media to a free storage slot
382 /// If posible to the slot it was previously loaded from.
384 /// Note: This method consumes status - so please read again afterward.
385 fn unload_to_free_slot(&mut self, status
: MtxStatus
) -> Result
<(), Error
> {
387 let drive_status
= &status
.drives
[self.drive_number() as usize];
388 if let Some(slot
) = drive_status
.loaded_slot
{
389 // check if original slot is empty/usable
390 if let Some(info
) = status
.slots
.get(slot
as usize - 1) {
391 if let ElementStatus
::Empty
= info
.status
{
392 return self.unload_media(Some(slot
));
397 if let Some(slot
) = status
.find_free_slot(false) {
398 self.unload_media(Some(slot
))
400 bail
!("drive '{}' unload failure - no free slot", self.drive_name());
405 const USE_MTX
: bool
= false;
407 impl ScsiMediaChange
for ScsiTapeChanger
{
409 fn status(&mut self, use_cache
: bool
) -> Result
<MtxStatus
, Error
> {
411 if let Some(state
) = load_changer_state_cache(&self.name
)?
{
416 let status
= if USE_MTX
{
417 mtx
::mtx_status(&self)
419 sg_pt_changer
::status(&self)
424 save_changer_state_cache(&self.name
, status
)?
;
427 delete_changer_state_cache(&self.name
);
434 fn load_slot(&mut self, from_slot
: u64, drivenum
: u64) -> Result
<MtxStatus
, Error
> {
435 let result
= if USE_MTX
{
436 mtx
::mtx_load(&self.path
, from_slot
, drivenum
)
438 let mut file
= sg_pt_changer
::open(&self.path
)?
;
439 sg_pt_changer
::load_slot(&mut file
, from_slot
, drivenum
)
442 let status
= self.status(false)?
; // always update status
444 result?
; // check load result
449 fn unload(&mut self, to_slot
: u64, drivenum
: u64) -> Result
<MtxStatus
, Error
> {
450 let result
= if USE_MTX
{
451 mtx
::mtx_unload(&self.path
, to_slot
, drivenum
)
453 let mut file
= sg_pt_changer
::open(&self.path
)?
;
454 sg_pt_changer
::unload(&mut file
, to_slot
, drivenum
)
457 let status
= self.status(false)?
; // always update status
459 result?
; // check unload result
464 fn transfer(&mut self, from_slot
: u64, to_slot
: u64) -> Result
<MtxStatus
, Error
> {
465 let result
= if USE_MTX
{
466 mtx
::mtx_transfer(&self.path
, from_slot
, to_slot
)
468 let mut file
= sg_pt_changer
::open(&self.path
)?
;
469 sg_pt_changer
::transfer_medium(&mut file
, from_slot
, to_slot
)
472 let status
= self.status(false)?
; // always update status
474 result?
; // check unload result
480 fn save_changer_state_cache(
483 ) -> Result
<(), Error
> {
485 let mut path
= PathBuf
::from(crate::tape
::CHANGER_STATE_DIR
);
488 let state
= serde_json
::to_string_pretty(state
)?
;
490 let backup_user
= crate::backup
::backup_user()?
;
491 let mode
= nix
::sys
::stat
::Mode
::from_bits_truncate(0o0644);
492 let options
= CreateOptions
::new()
494 .owner(backup_user
.uid
)
495 .group(backup_user
.gid
);
497 replace_file(path
, state
.as_bytes(), options
)
500 fn delete_changer_state_cache(changer
: &str) {
501 let mut path
= PathBuf
::from("/run/proxmox-backup/changer-state");
504 let _
= std
::fs
::remove_file(&path
); // ignore errors
507 fn load_changer_state_cache(changer
: &str) -> Result
<Option
<MtxStatus
>, Error
> {
508 let mut path
= PathBuf
::from("/run/proxmox-backup/changer-state");
511 let data
= match file_read_optional_string(&path
)?
{
512 None
=> return Ok(None
),
516 let state
= serde_json
::from_str(&data
)?
;
521 /// Implements MediaChange using 'mtx' linux cli tool
522 pub struct MtxMediaChanger
{
523 drive_name
: String
, // used for error messages
525 config
: ScsiTapeChanger
,
528 impl MtxMediaChanger
{
530 pub fn with_drive_config(drive_config
: &LinuxTapeDrive
) -> Result
<Self, Error
> {
531 let (config
, _digest
) = crate::config
::drive
::config()?
;
532 let changer_config
: ScsiTapeChanger
= match drive_config
.changer
{
533 Some(ref changer
) => config
.lookup("changer", changer
)?
,
534 None
=> bail
!("drive '{}' has no associated changer", drive_config
.name
),
538 drive_name
: drive_config
.name
.clone(),
539 drive_number
: drive_config
.changer_drivenum
.unwrap_or(0),
540 config
: changer_config
,
545 impl MediaChange
for MtxMediaChanger
{
547 fn drive_number(&self) -> u64 {
551 fn drive_name(&self) -> &str {
555 fn status(&mut self) -> Result
<MtxStatus
, Error
> {
556 self.config
.status(false)
559 fn transfer_media(&mut self, from
: u64, to
: u64) -> Result
<(), Error
> {
560 self.config
.transfer(from
, to
)?
;
564 fn load_media_from_slot(&mut self, slot
: u64) -> Result
<(), Error
> {
565 self.config
.load_slot(slot
, self.drive_number
)?
;
569 fn unload_media(&mut self, target_slot
: Option
<u64>) -> Result
<(), Error
> {
570 if let Some(target_slot
) = target_slot
{
571 self.config
.unload(target_slot
, self.drive_number
)?
;
573 let status
= self.status()?
;
574 self.unload_to_free_slot(status
)?
;