]> git.proxmox.com Git - proxmox-backup.git/blame - src/tape/changer/mod.rs
typo fixes all over the place
[proxmox-backup.git] / src / tape / changer / mod.rs
CommitLineData
37796ff7
DM
1//! Media changer implementation (SCSI media changer)
2
b107fdb9
DM
3mod email;
4pub use email::*;
5
697c41c5 6pub mod sg_pt_changer;
b107fdb9 7
697c41c5 8pub mod mtx;
b107fdb9 9
37796ff7
DM
10mod online_status_map;
11pub use online_status_map::*;
12
4be47366 13use std::collections::HashSet;
4188fd59 14use std::path::PathBuf;
4be47366 15
04df41ce 16use anyhow::{bail, Error};
c3747b93 17use serde::{Serialize, Deserialize};
4be47366
DC
18use serde_json::Value;
19
4188fd59
DM
20use proxmox::{
21 api::schema::parse_property_string,
22 tools::fs::{
23 CreateOptions,
24 replace_file,
25 file_read_optional_string,
26 },
27};
b107fdb9 28
697c41c5 29use crate::api2::types::{
4be47366 30 SLOT_ARRAY_SCHEMA,
697c41c5
DM
31 ScsiTapeChanger,
32 LinuxTapeDrive,
33};
34
35/// Changer element status.
36///
37/// Drive and slots may be `Empty`, or contain some media, either
d1d74c43 38/// with known volume tag `VolumeTag(String)`, or without (`Full`).
c3747b93 39#[derive(Serialize, Deserialize, Debug)]
697c41c5
DM
40pub enum ElementStatus {
41 Empty,
42 Full,
43 VolumeTag(String),
44}
45
46/// Changer drive status.
c3747b93 47#[derive(Serialize, Deserialize)]
697c41c5
DM
48pub struct DriveStatus {
49 /// The slot the element was loaded from (if known).
50 pub loaded_slot: Option<u64>,
51 /// The status.
52 pub status: ElementStatus,
53 /// Drive Identifier (Serial number)
54 pub drive_serial_number: Option<String>,
2da7aca8
DC
55 /// Drive Vendor
56 pub vendor: Option<String>,
57 /// Drive Model
58 pub model: Option<String>,
697c41c5
DM
59 /// Element Address
60 pub element_address: u16,
61}
62
63/// Storage element status.
c3747b93 64#[derive(Serialize, Deserialize)]
697c41c5
DM
65pub struct StorageElementStatus {
66 /// Flag for Import/Export slots
67 pub import_export: bool,
68 /// The status.
69 pub status: ElementStatus,
70 /// Element Address
71 pub element_address: u16,
72}
73
74/// Transport element status.
c3747b93 75#[derive(Serialize, Deserialize)]
697c41c5
DM
76pub struct TransportElementStatus {
77 /// The status.
78 pub status: ElementStatus,
79 /// Element Address
80 pub element_address: u16,
81}
82
83/// Changer status - show drive/slot usage
c3747b93 84#[derive(Serialize, Deserialize)]
697c41c5
DM
85pub struct MtxStatus {
86 /// List of known drives
87 pub drives: Vec<DriveStatus>,
88 /// List of known storage slots
89 pub slots: Vec<StorageElementStatus>,
d1d74c43 90 /// Transport elements
697c41c5
DM
91 ///
92 /// Note: Some libraries do not report transport elements.
93 pub transports: Vec<TransportElementStatus>,
94}
95
96impl MtxStatus {
97
98 pub fn slot_address(&self, slot: u64) -> Result<u16, Error> {
99 if slot == 0 {
100 bail!("invalid slot number '{}' (slots numbers starts at 1)", slot);
101 }
102 if slot > (self.slots.len() as u64) {
103 bail!("invalid slot number '{}' (max {} slots)", slot, self.slots.len());
104 }
105
106 Ok(self.slots[(slot -1) as usize].element_address)
107 }
108
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);
112 }
113
114 Ok(self.drives[drivenum as usize].element_address)
115 }
116
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
121 self
122 .transports
123 .get(0)
124 .map(|t| t.element_address)
125 .unwrap_or(0u16)
126 }
c3747b93
DM
127
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
133 }
134 if let ElementStatus::Empty = slot_info.status {
135 free_slot = Some((i+1) as u64);
136 break;
137 }
138 }
139 free_slot
140 }
4be47366
DC
141
142 pub fn mark_import_export_slots(&mut self, config: &ScsiTapeChanger) -> Result<(), Error>{
143 let mut export_slots: HashSet<u64> = HashSet::new();
144
145 if let Some(slots) = &config.export_slots {
146 let slots: Value = parse_property_string(&slots, &SLOT_ARRAY_SCHEMA)?;
147 export_slots = slots
148 .as_array()
149 .unwrap()
150 .iter()
151 .filter_map(|v| v.as_u64())
152 .collect();
153 }
154
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
159 }
160 }
161
162 Ok(())
163 }
697c41c5
DM
164}
165
166/// Interface to SCSI changer devices
167pub trait ScsiMediaChange {
168
4188fd59 169 fn status(&mut self, use_cache: bool) -> Result<MtxStatus, Error>;
697c41c5 170
3f16f1b0 171 fn load_slot(&mut self, from_slot: u64, drivenum: u64) -> Result<MtxStatus, Error>;
697c41c5 172
3f16f1b0 173 fn unload(&mut self, to_slot: u64, drivenum: u64) -> Result<MtxStatus, Error>;
697c41c5 174
3f16f1b0 175 fn transfer(&mut self, from_slot: u64, to_slot: u64) -> Result<MtxStatus, Error>;
697c41c5
DM
176}
177
04df41ce 178/// Interface to the media changer device for a single drive
b107fdb9
DM
179pub trait MediaChange {
180
04df41ce
DM
181 /// Drive number inside changer
182 fn drive_number(&self) -> u64;
183
184 /// Drive name (used for debug messages)
185 fn drive_name(&self) -> &str;
186
46a1863f
DM
187 /// Returns the changer status
188 fn status(&mut self) -> Result<MtxStatus, Error>;
189
0057f0e5
DM
190 /// Transfer media from on slot to another (storage or import export slots)
191 ///
192 /// Target slot needs to be empty
86d9f4e7 193 fn transfer_media(&mut self, from: u64, to: u64) -> Result<MtxStatus, Error>;
0057f0e5 194
46a1863f 195 /// Load media from storage slot into drive
86d9f4e7 196 fn load_media_from_slot(&mut self, slot: u64) -> Result<MtxStatus, Error>;
46a1863f 197
8446fbca 198 /// Load media by label-text into drive
b107fdb9
DM
199 ///
200 /// This unloads first if the drive is already loaded with another media.
46a1863f 201 ///
04df41ce
DM
202 /// Note: This refuses to load media inside import/export
203 /// slots. Also, you cannot load cleaning units with this
204 /// interface.
86d9f4e7 205 fn load_media(&mut self, label_text: &str) -> Result<MtxStatus, Error> {
04df41ce 206
8446fbca 207 if label_text.starts_with("CLN") {
bbf01b64 208 bail!("unable to load media '{}' (seems to be a cleaning unit)", label_text);
04df41ce
DM
209 }
210
211 let mut status = self.status()?;
212
213 let mut unload_drive = false;
214
215 // already loaded?
216 for (i, drive_status) in status.drives.iter().enumerate() {
217 if let ElementStatus::VolumeTag(ref tag) = drive_status.status {
8446fbca 218 if *tag == label_text {
04df41ce
DM
219 if i as u64 != self.drive_number() {
220 bail!("unable to load media '{}' - media in wrong drive ({} != {})",
8446fbca 221 label_text, i, self.drive_number());
04df41ce 222 }
86d9f4e7 223 return Ok(status) // already loaded
04df41ce
DM
224 }
225 }
226 if i as u64 == self.drive_number() {
227 match drive_status.status {
228 ElementStatus::Empty => { /* OK */ },
229 _ => unload_drive = true,
230 }
231 }
232 }
233
234 if unload_drive {
86d9f4e7 235 status = self.unload_to_free_slot(status)?;
04df41ce
DM
236 }
237
238 let mut slot = None;
697c41c5
DM
239 for (i, slot_info) in status.slots.iter().enumerate() {
240 if let ElementStatus::VolumeTag(ref tag) = slot_info.status {
241 if tag == label_text {
242 if slot_info.import_export {
8446fbca 243 bail!("unable to load media '{}' - inside import/export slot", label_text);
04df41ce
DM
244 }
245 slot = Some(i+1);
246 break;
247 }
248 }
249 }
250
251 let slot = match slot {
8446fbca 252 None => bail!("unable to find media '{}' (offline?)", label_text),
04df41ce
DM
253 Some(slot) => slot,
254 };
255
256 self.load_media_from_slot(slot as u64)
257 }
b107fdb9 258
6638c034 259 /// Unload media from drive (eject media if necessary)
86d9f4e7 260 fn unload_media(&mut self, target_slot: Option<u64>) -> Result<MtxStatus, Error>;
b107fdb9 261
8446fbca 262 /// List online media labels (label_text/barcodes)
46a1863f 263 ///
d1d74c43 264 /// List accessible (online) label texts. This does not include
46a1863f 265 /// media inside import-export slots or cleaning media.
8446fbca 266 fn online_media_label_texts(&mut self) -> Result<Vec<String>, Error> {
46a1863f
DM
267 let status = self.status()?;
268
269 let mut list = Vec::new();
270
271 for drive_status in status.drives.iter() {
272 if let ElementStatus::VolumeTag(ref tag) = drive_status.status {
273 list.push(tag.clone());
274 }
275 }
276
697c41c5
DM
277 for slot_info in status.slots.iter() {
278 if slot_info.import_export { continue; }
279 if let ElementStatus::VolumeTag(ref tag) = slot_info.status {
25d39657 280 if tag.starts_with("CLN") { continue; }
46a1863f
DM
281 list.push(tag.clone());
282 }
283 }
284
285 Ok(list)
286 }
df69a4fc
DM
287
288 /// Load/Unload cleaning cartridge
289 ///
290 /// This fail if there is no cleaning cartridge online. Any media
291 /// inside the drive is automatically unloaded.
86d9f4e7 292 fn clean_drive(&mut self) -> Result<MtxStatus, Error> {
9ce2481a
DM
293 let mut status = self.status()?;
294
295 // Unload drive first. Note: This also unloads a loaded cleaning tape
296 if let Some(drive_status) = status.drives.get(self.drive_number() as usize) {
297 match drive_status.status {
298 ElementStatus::Empty => { /* OK */ },
299 _ => { status = self.unload_to_free_slot(status)?; }
300 }
301 }
04df41ce
DM
302
303 let mut cleaning_cartridge_slot = None;
304
697c41c5
DM
305 for (i, slot_info) in status.slots.iter().enumerate() {
306 if slot_info.import_export { continue; }
307 if let ElementStatus::VolumeTag(ref tag) = slot_info.status {
04df41ce
DM
308 if tag.starts_with("CLN") {
309 cleaning_cartridge_slot = Some(i + 1);
310 break;
311 }
312 }
313 }
314
315 let cleaning_cartridge_slot = match cleaning_cartridge_slot {
316 None => bail!("clean failed - unable to find cleaning cartridge"),
317 Some(cleaning_cartridge_slot) => cleaning_cartridge_slot as u64,
318 };
319
04df41ce
DM
320
321 self.load_media_from_slot(cleaning_cartridge_slot)?;
322
86d9f4e7 323 self.unload_media(Some(cleaning_cartridge_slot))
04df41ce 324 }
0057f0e5
DM
325
326 /// Export media
327 ///
328 /// By moving the media to an empty import-export slot. Returns
329 /// Some(slot) if the media was exported. Returns None if the media is
330 /// not online (already exported).
8446fbca 331 fn export_media(&mut self, label_text: &str) -> Result<Option<u64>, Error> {
04df41ce
DM
332 let status = self.status()?;
333
334 let mut unload_from_drive = false;
335 if let Some(drive_status) = status.drives.get(self.drive_number() as usize) {
336 if let ElementStatus::VolumeTag(ref tag) = drive_status.status {
8446fbca 337 if tag == label_text {
04df41ce
DM
338 unload_from_drive = true;
339 }
340 }
341 }
342
343 let mut from = None;
344 let mut to = None;
345
697c41c5
DM
346 for (i, slot_info) in status.slots.iter().enumerate() {
347 if slot_info.import_export {
04df41ce 348 if to.is_some() { continue; }
697c41c5 349 if let ElementStatus::Empty = slot_info.status {
04df41ce
DM
350 to = Some(i as u64 + 1);
351 }
697c41c5 352 } else if let ElementStatus::VolumeTag(ref tag) = slot_info.status {
6334bdc1
FG
353 if tag == label_text {
354 from = Some(i as u64 + 1);
04df41ce
DM
355 }
356 }
357 }
358
359 if unload_from_drive {
360 match to {
361 Some(to) => {
362 self.unload_media(Some(to))?;
363 Ok(Some(to))
364 }
365 None => bail!("unable to find free export slot"),
366 }
367 } else {
368 match (from, to) {
369 (Some(from), Some(to)) => {
370 self.transfer_media(from, to)?;
371 Ok(Some(to))
372 }
373 (Some(_from), None) => bail!("unable to find free export slot"),
374 (None, _) => Ok(None), // not online
375 }
376 }
377 }
378
379 /// Unload media to a free storage slot
380 ///
d1d74c43 381 /// If possible to the slot it was previously loaded from.
04df41ce 382 ///
86d9f4e7
DM
383 /// Note: This method consumes status - so please use returned status afterward.
384 fn unload_to_free_slot(&mut self, status: MtxStatus) -> Result<MtxStatus, Error> {
04df41ce
DM
385
386 let drive_status = &status.drives[self.drive_number() as usize];
387 if let Some(slot) = drive_status.loaded_slot {
388 // check if original slot is empty/usable
389 if let Some(info) = status.slots.get(slot as usize - 1) {
697c41c5 390 if let ElementStatus::Empty = info.status {
04df41ce
DM
391 return self.unload_media(Some(slot));
392 }
393 }
394 }
395
c3747b93 396 if let Some(slot) = status.find_free_slot(false) {
04df41ce
DM
397 self.unload_media(Some(slot))
398 } else {
399 bail!("drive '{}' unload failure - no free slot", self.drive_name());
400 }
401 }
b107fdb9 402}
697c41c5
DM
403
404const USE_MTX: bool = false;
405
406impl ScsiMediaChange for ScsiTapeChanger {
407
4188fd59
DM
408 fn status(&mut self, use_cache: bool) -> Result<MtxStatus, Error> {
409 if use_cache {
410 if let Some(state) = load_changer_state_cache(&self.name)? {
411 return Ok(state);
412 }
413 }
414
415 let status = if USE_MTX {
697c41c5
DM
416 mtx::mtx_status(&self)
417 } else {
4be47366 418 sg_pt_changer::status(&self)
4188fd59
DM
419 };
420
421 match &status {
422 Ok(status) => {
423 save_changer_state_cache(&self.name, status)?;
424 }
425 Err(_) => {
426 delete_changer_state_cache(&self.name);
427 }
697c41c5 428 }
4188fd59
DM
429
430 status
697c41c5
DM
431 }
432
3f16f1b0
DM
433 fn load_slot(&mut self, from_slot: u64, drivenum: u64) -> Result<MtxStatus, Error> {
434 let result = if USE_MTX {
697c41c5
DM
435 mtx::mtx_load(&self.path, from_slot, drivenum)
436 } else {
437 let mut file = sg_pt_changer::open(&self.path)?;
438 sg_pt_changer::load_slot(&mut file, from_slot, drivenum)
3f16f1b0
DM
439 };
440
441 let status = self.status(false)?; // always update status
442
443 result?; // check load result
444
445 Ok(status)
697c41c5
DM
446 }
447
3f16f1b0
DM
448 fn unload(&mut self, to_slot: u64, drivenum: u64) -> Result<MtxStatus, Error> {
449 let result = if USE_MTX {
697c41c5
DM
450 mtx::mtx_unload(&self.path, to_slot, drivenum)
451 } else {
452 let mut file = sg_pt_changer::open(&self.path)?;
453 sg_pt_changer::unload(&mut file, to_slot, drivenum)
3f16f1b0
DM
454 };
455
456 let status = self.status(false)?; // always update status
457
458 result?; // check unload result
459
460 Ok(status)
697c41c5
DM
461 }
462
3f16f1b0
DM
463 fn transfer(&mut self, from_slot: u64, to_slot: u64) -> Result<MtxStatus, Error> {
464 let result = if USE_MTX {
697c41c5
DM
465 mtx::mtx_transfer(&self.path, from_slot, to_slot)
466 } else {
467 let mut file = sg_pt_changer::open(&self.path)?;
468 sg_pt_changer::transfer_medium(&mut file, from_slot, to_slot)
3f16f1b0
DM
469 };
470
471 let status = self.status(false)?; // always update status
472
473 result?; // check unload result
474
475 Ok(status)
697c41c5
DM
476 }
477}
478
4188fd59
DM
479fn save_changer_state_cache(
480 changer: &str,
481 state: &MtxStatus,
482) -> Result<(), Error> {
cd44fb8d
DM
483
484 let mut path = PathBuf::from(crate::tape::CHANGER_STATE_DIR);
4188fd59
DM
485 path.push(changer);
486
cd44fb8d
DM
487 let state = serde_json::to_string_pretty(state)?;
488
4188fd59
DM
489 let backup_user = crate::backup::backup_user()?;
490 let mode = nix::sys::stat::Mode::from_bits_truncate(0o0644);
491 let options = CreateOptions::new()
492 .perm(mode)
493 .owner(backup_user.uid)
494 .group(backup_user.gid);
495
4188fd59
DM
496 replace_file(path, state.as_bytes(), options)
497}
498
499fn delete_changer_state_cache(changer: &str) {
500 let mut path = PathBuf::from("/run/proxmox-backup/changer-state");
501 path.push(changer);
502
503 let _ = std::fs::remove_file(&path); // ignore errors
504}
505
506fn load_changer_state_cache(changer: &str) -> Result<Option<MtxStatus>, Error> {
507 let mut path = PathBuf::from("/run/proxmox-backup/changer-state");
508 path.push(changer);
509
510 let data = match file_read_optional_string(&path)? {
511 None => return Ok(None),
512 Some(data) => data,
513 };
514
515 let state = serde_json::from_str(&data)?;
516
517 Ok(Some(state))
518}
519
697c41c5
DM
520/// Implements MediaChange using 'mtx' linux cli tool
521pub struct MtxMediaChanger {
522 drive_name: String, // used for error messages
523 drive_number: u64,
524 config: ScsiTapeChanger,
525}
526
527impl MtxMediaChanger {
528
529 pub fn with_drive_config(drive_config: &LinuxTapeDrive) -> Result<Self, Error> {
530 let (config, _digest) = crate::config::drive::config()?;
531 let changer_config: ScsiTapeChanger = match drive_config.changer {
532 Some(ref changer) => config.lookup("changer", changer)?,
533 None => bail!("drive '{}' has no associated changer", drive_config.name),
534 };
535
536 Ok(Self {
537 drive_name: drive_config.name.clone(),
d737adc6 538 drive_number: drive_config.changer_drivenum.unwrap_or(0),
697c41c5
DM
539 config: changer_config,
540 })
541 }
542}
543
544impl MediaChange for MtxMediaChanger {
545
546 fn drive_number(&self) -> u64 {
547 self.drive_number
548 }
549
550 fn drive_name(&self) -> &str {
551 &self.drive_name
552 }
553
554 fn status(&mut self) -> Result<MtxStatus, Error> {
4188fd59 555 self.config.status(false)
697c41c5
DM
556 }
557
86d9f4e7
DM
558 fn transfer_media(&mut self, from: u64, to: u64) -> Result<MtxStatus, Error> {
559 self.config.transfer(from, to)
697c41c5
DM
560 }
561
86d9f4e7
DM
562 fn load_media_from_slot(&mut self, slot: u64) -> Result<MtxStatus, Error> {
563 self.config.load_slot(slot, self.drive_number)
697c41c5
DM
564 }
565
86d9f4e7 566 fn unload_media(&mut self, target_slot: Option<u64>) -> Result<MtxStatus, Error> {
697c41c5 567 if let Some(target_slot) = target_slot {
86d9f4e7 568 self.config.unload(target_slot, self.drive_number)
697c41c5
DM
569 } else {
570 let status = self.status()?;
86d9f4e7 571 self.unload_to_free_slot(status)
697c41c5
DM
572 }
573 }
574}