1 //! Media changer implementation (SCSI media changer)
6 pub use online_status_map
::*;
7 use proxmox_schema
::ApiType
;
9 use std
::path
::PathBuf
;
11 use anyhow
::{bail, Error}
;
13 use proxmox_sys
::fs
::{file_read_optional_string, replace_file, CreateOptions}
;
15 use pbs_api_types
::{ChangerOptions, LtoTapeDrive, ScsiTapeChanger}
;
17 use pbs_tape
::{linux_list_drives::open_lto_tape_device, sg_pt_changer, ElementStatus, MtxStatus}
;
19 use crate::tape
::drive
::{LtoTapeHandle, TapeDriver}
;
21 /// Interface to SCSI changer devices
22 pub trait ScsiMediaChange
{
23 fn status(&mut self, use_cache
: bool
) -> Result
<MtxStatus
, Error
>;
25 fn load_slot(&mut self, from_slot
: u64, drivenum
: u64) -> Result
<MtxStatus
, Error
>;
27 fn unload(&mut self, to_slot
: u64, drivenum
: u64) -> Result
<MtxStatus
, Error
>;
29 fn transfer(&mut self, from_slot
: u64, to_slot
: u64) -> Result
<MtxStatus
, Error
>;
32 /// Interface to the media changer device for a single drive
33 pub trait MediaChange
{
34 /// Drive number inside changer
35 fn drive_number(&self) -> u64;
37 /// Drive name (used for debug messages)
38 fn drive_name(&self) -> &str;
40 /// Returns the changer status
41 fn status(&mut self) -> Result
<MtxStatus
, Error
>;
43 /// Transfer media from on slot to another (storage or import export slots)
45 /// Target slot needs to be empty
46 fn transfer_media(&mut self, from
: u64, to
: u64) -> Result
<MtxStatus
, Error
>;
48 /// Load media from storage slot into drive
49 fn load_media_from_slot(&mut self, slot
: u64) -> Result
<MtxStatus
, Error
>;
51 /// Load media by label-text into drive
53 /// This unloads first if the drive is already loaded with another media.
55 /// Note: This refuses to load media inside import/export
56 /// slots. Also, you cannot load cleaning units with this
58 fn load_media(&mut self, label_text
: &str) -> Result
<MtxStatus
, Error
> {
59 if label_text
.starts_with("CLN") {
61 "unable to load media '{}' (seems to be a cleaning unit)",
66 let mut status
= self.status()?
;
68 let mut unload_drive
= false;
71 for (i
, drive_status
) in status
.drives
.iter().enumerate() {
72 if let ElementStatus
::VolumeTag(ref tag
) = drive_status
.status
{
73 if *tag
== label_text
{
74 if i
as u64 != self.drive_number() {
76 "unable to load media '{}' - media in wrong drive ({} != {})",
82 return Ok(status
); // already loaded
85 if i
as u64 == self.drive_number() {
86 match drive_status
.status
{
87 ElementStatus
::Empty
=> { /* OK */ }
88 _
=> unload_drive
= true,
94 status
= self.unload_to_free_slot(status
)?
;
98 for (i
, slot_info
) in status
.slots
.iter().enumerate() {
99 if let ElementStatus
::VolumeTag(ref tag
) = slot_info
.status
{
100 if tag
== label_text
{
101 if slot_info
.import_export
{
103 "unable to load media '{}' - inside import/export slot",
113 let slot
= match slot
{
114 None
=> bail
!("unable to find media '{}' (offline?)", label_text
),
118 self.load_media_from_slot(slot
as u64)
121 /// Unload media from drive (eject media if necessary)
122 fn unload_media(&mut self, target_slot
: Option
<u64>) -> Result
<MtxStatus
, Error
>;
124 /// List online media labels (label_text/barcodes)
126 /// List accessible (online) label texts. This does not include
127 /// media inside import-export slots or cleaning media.
128 fn online_media_label_texts(&mut self) -> Result
<Vec
<String
>, Error
> {
129 let status
= self.status()?
;
131 let mut list
= Vec
::new();
133 for drive_status
in status
.drives
.iter() {
134 if let ElementStatus
::VolumeTag(ref tag
) = drive_status
.status
{
135 list
.push(tag
.clone());
139 for slot_info
in status
.slots
.iter() {
140 if slot_info
.import_export
{
143 if let ElementStatus
::VolumeTag(ref tag
) = slot_info
.status
{
144 if tag
.starts_with("CLN") {
147 list
.push(tag
.clone());
154 /// Load/Unload cleaning cartridge
156 /// This fail if there is no cleaning cartridge online. Any media
157 /// inside the drive is automatically unloaded.
158 fn clean_drive(&mut self) -> Result
<MtxStatus
, Error
> {
159 let mut status
= self.status()?
;
161 // Unload drive first. Note: This also unloads a loaded cleaning tape
162 if let Some(drive_status
) = status
.drives
.get(self.drive_number() as usize) {
163 match drive_status
.status
{
164 ElementStatus
::Empty
=> { /* OK */ }
166 status
= self.unload_to_free_slot(status
)?
;
171 let mut cleaning_cartridge_slot
= None
;
173 for (i
, slot_info
) in status
.slots
.iter().enumerate() {
174 if slot_info
.import_export
{
177 if let ElementStatus
::VolumeTag(ref tag
) = slot_info
.status
{
178 if tag
.starts_with("CLN") {
179 cleaning_cartridge_slot
= Some(i
+ 1);
185 let cleaning_cartridge_slot
= match cleaning_cartridge_slot
{
186 None
=> bail
!("clean failed - unable to find cleaning cartridge"),
187 Some(cleaning_cartridge_slot
) => cleaning_cartridge_slot
as u64,
190 self.load_media_from_slot(cleaning_cartridge_slot
)?
;
192 self.unload_media(Some(cleaning_cartridge_slot
))
197 /// By moving the media to an empty import-export slot. Returns
198 /// Some(slot) if the media was exported. Returns None if the media is
199 /// not online (already exported).
200 fn export_media(&mut self, label_text
: &str) -> Result
<Option
<u64>, Error
> {
201 let status
= self.status()?
;
203 let mut unload_from_drive
= false;
204 if let Some(drive_status
) = status
.drives
.get(self.drive_number() as usize) {
205 if let ElementStatus
::VolumeTag(ref tag
) = drive_status
.status
{
206 if tag
== label_text
{
207 unload_from_drive
= true;
215 for (i
, slot_info
) in status
.slots
.iter().enumerate() {
216 if slot_info
.import_export
{
220 if let ElementStatus
::Empty
= slot_info
.status
{
221 to
= Some(i
as u64 + 1);
223 } else if let ElementStatus
::VolumeTag(ref tag
) = slot_info
.status
{
224 if tag
== label_text
{
225 from
= Some(i
as u64 + 1);
230 if unload_from_drive
{
233 self.unload_media(Some(to
))?
;
236 None
=> bail
!("unable to find free export slot"),
240 (Some(from
), Some(to
)) => {
241 self.transfer_media(from
, to
)?
;
244 (Some(_from
), None
) => bail
!("unable to find free export slot"),
245 (None
, _
) => Ok(None
), // not online
250 /// Unload media to a free storage slot
252 /// If possible to the slot it was previously loaded from.
254 /// Note: This method consumes status - so please use returned status afterward.
255 fn unload_to_free_slot(&mut self, status
: MtxStatus
) -> Result
<MtxStatus
, Error
> {
256 let drive_status
= &status
.drives
[self.drive_number() as usize];
257 if let Some(slot
) = drive_status
.loaded_slot
{
258 // check if original slot is empty/usable
259 if let Some(info
) = status
.slots
.get(slot
as usize - 1) {
260 if let ElementStatus
::Empty
= info
.status
{
261 return self.unload_media(Some(slot
));
266 if let Some(slot
) = status
.find_free_slot(false) {
267 self.unload_media(Some(slot
))
270 "drive '{}' unload failure - no free slot",
277 const USE_MTX
: bool
= false;
279 impl ScsiMediaChange
for ScsiTapeChanger
{
280 fn status(&mut self, use_cache
: bool
) -> Result
<MtxStatus
, Error
> {
282 if let Some(state
) = load_changer_state_cache(&self.name
)?
{
287 let status
= if USE_MTX
{
288 mtx
::mtx_status(self)
290 sg_pt_changer
::status(self)
295 save_changer_state_cache(&self.name
, status
)?
;
298 delete_changer_state_cache(&self.name
);
305 fn load_slot(&mut self, from_slot
: u64, drivenum
: u64) -> Result
<MtxStatus
, Error
> {
306 let result
= if USE_MTX
{
307 mtx
::mtx_load(&self.path
, from_slot
, drivenum
)
309 let mut file
= sg_pt_changer
::open(&self.path
)?
;
310 sg_pt_changer
::load_slot(&mut file
, from_slot
, drivenum
)
313 let status
= self.status(false)?
; // always update status
315 result?
; // check load result
320 fn unload(&mut self, to_slot
: u64, drivenum
: u64) -> Result
<MtxStatus
, Error
> {
321 let result
= if USE_MTX
{
322 mtx
::mtx_unload(&self.path
, to_slot
, drivenum
)
324 let mut file
= sg_pt_changer
::open(&self.path
)?
;
325 sg_pt_changer
::unload(&mut file
, to_slot
, drivenum
)
328 let status
= self.status(false)?
; // always update status
330 result?
; // check unload result
335 fn transfer(&mut self, from_slot
: u64, to_slot
: u64) -> Result
<MtxStatus
, Error
> {
336 let result
= if USE_MTX
{
337 mtx
::mtx_transfer(&self.path
, from_slot
, to_slot
)
339 let mut file
= sg_pt_changer
::open(&self.path
)?
;
340 sg_pt_changer
::transfer_medium(&mut file
, from_slot
, to_slot
)
343 let status
= self.status(false)?
; // always update status
345 result?
; // check unload result
351 fn save_changer_state_cache(changer
: &str, state
: &MtxStatus
) -> Result
<(), Error
> {
352 let mut path
= PathBuf
::from(crate::tape
::CHANGER_STATE_DIR
);
355 let state
= serde_json
::to_string_pretty(state
)?
;
357 let backup_user
= pbs_config
::backup_user()?
;
358 let mode
= nix
::sys
::stat
::Mode
::from_bits_truncate(0o0644);
359 let options
= CreateOptions
::new()
361 .owner(backup_user
.uid
)
362 .group(backup_user
.gid
);
364 replace_file(path
, state
.as_bytes(), options
, false)
367 fn delete_changer_state_cache(changer
: &str) {
368 let mut path
= PathBuf
::from("/run/proxmox-backup/changer-state");
371 let _
= std
::fs
::remove_file(&path
); // ignore errors
374 fn load_changer_state_cache(changer
: &str) -> Result
<Option
<MtxStatus
>, Error
> {
375 let mut path
= PathBuf
::from("/run/proxmox-backup/changer-state");
378 let data
= match file_read_optional_string(&path
)?
{
379 None
=> return Ok(None
),
383 let state
= serde_json
::from_str(&data
)?
;
388 /// Implements MediaChange using 'mtx' linux cli tool
389 pub struct MtxMediaChanger
{
391 config
: ScsiTapeChanger
,
394 impl MtxMediaChanger
{
395 pub fn with_drive_config(drive_config
: &LtoTapeDrive
) -> Result
<Self, Error
> {
396 let (config
, _digest
) = pbs_config
::drive
::config()?
;
397 let changer_config
: ScsiTapeChanger
= match drive_config
.changer
{
398 Some(ref changer
) => config
.lookup("changer", changer
)?
,
399 None
=> bail
!("drive '{}' has no associated changer", drive_config
.name
),
403 drive
: drive_config
.clone(),
404 config
: changer_config
,
409 impl MediaChange
for MtxMediaChanger
{
410 fn drive_number(&self) -> u64 {
411 self.drive
.changer_drivenum
.unwrap_or(0)
414 fn drive_name(&self) -> &str {
418 fn status(&mut self) -> Result
<MtxStatus
, Error
> {
419 self.config
.status(false)
422 fn transfer_media(&mut self, from
: u64, to
: u64) -> Result
<MtxStatus
, Error
> {
423 self.config
.transfer(from
, to
)
426 fn load_media_from_slot(&mut self, slot
: u64) -> Result
<MtxStatus
, Error
> {
427 self.config
.load_slot(slot
, self.drive_number())
430 fn unload_media(&mut self, target_slot
: Option
<u64>) -> Result
<MtxStatus
, Error
> {
431 let options
: ChangerOptions
= serde_json
::from_value(
432 ChangerOptions
::API_SCHEMA
433 .parse_property_string(self.config
.options
.as_deref().unwrap_or_default())?
,
436 if options
.eject_before_unload
{
437 let file
= open_lto_tape_device(&self.drive
.path
)?
;
438 let mut handle
= LtoTapeHandle
::new(file
)?
;
440 if handle
.medium_present() {
441 handle
.eject_media()?
;
445 if let Some(target_slot
) = target_slot
{
446 self.config
.unload(target_slot
, self.drive_number())
448 let status
= self.status()?
;
449 self.unload_to_free_slot(status
)