]> git.proxmox.com Git - proxmox-backup.git/blob - src/tape/changer/mod.rs
e1dda621f5e10a272963d27357fa4b1ab5f70283
[proxmox-backup.git] / src / tape / changer / mod.rs
1 //! Media changer implementation (SCSI media changer)
2
3 mod email;
4 pub use email::*;
5
6 pub mod sg_pt_changer;
7
8 pub mod mtx;
9
10 mod online_status_map;
11 pub use online_status_map::*;
12
13 use std::collections::HashSet;
14 use std::path::PathBuf;
15
16 use anyhow::{bail, Error};
17 use serde::{Serialize, Deserialize};
18 use serde_json::Value;
19
20 use proxmox::{
21 api::schema::parse_property_string,
22 tools::fs::{
23 CreateOptions,
24 replace_file,
25 file_read_optional_string,
26 },
27 };
28
29 use crate::api2::types::{
30 SLOT_ARRAY_SCHEMA,
31 ScsiTapeChanger,
32 LinuxTapeDrive,
33 };
34
35 /// Changer element status.
36 ///
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 {
41 Empty,
42 Full,
43 VolumeTag(String),
44 }
45
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>,
51 /// The status.
52 pub status: ElementStatus,
53 /// Drive Identifier (Serial number)
54 pub drive_serial_number: Option<String>,
55 /// Drive Vendor
56 pub vendor: Option<String>,
57 /// Drive Model
58 pub model: Option<String>,
59 /// Element Address
60 pub element_address: u16,
61 }
62
63 /// Storage element status.
64 #[derive(Serialize, Deserialize)]
65 pub 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.
75 #[derive(Serialize, Deserialize)]
76 pub 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
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>,
90 /// Tranport elements
91 ///
92 /// Note: Some libraries do not report transport elements.
93 pub transports: Vec<TransportElementStatus>,
94 }
95
96 impl 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 }
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 }
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 }
164 }
165
166 /// Interface to SCSI changer devices
167 pub trait ScsiMediaChange {
168
169 fn status(&mut self, use_cache: bool) -> Result<MtxStatus, Error>;
170
171 fn load_slot(&mut self, from_slot: u64, drivenum: u64) -> Result<(), Error>;
172
173 fn unload(&mut self, to_slot: u64, drivenum: u64) -> Result<(), Error>;
174
175 fn transfer(&mut self, from_slot: u64, to_slot: u64) -> Result<(), Error>;
176 }
177
178 /// Interface to the media changer device for a single drive
179 pub trait MediaChange {
180
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
187 /// Returns the changer status
188 fn status(&mut self) -> Result<MtxStatus, Error>;
189
190 /// Transfer media from on slot to another (storage or import export slots)
191 ///
192 /// Target slot needs to be empty
193 fn transfer_media(&mut self, from: u64, to: u64) -> Result<(), Error>;
194
195 /// Load media from storage slot into drive
196 fn load_media_from_slot(&mut self, slot: u64) -> Result<(), Error>;
197
198 /// Load media by label-text into drive
199 ///
200 /// This unloads first if the drive is already loaded with another media.
201 ///
202 /// Note: This refuses to load media inside import/export
203 /// slots. Also, you cannot load cleaning units with this
204 /// interface.
205 fn load_media(&mut self, label_text: &str) -> Result<(), Error> {
206
207 if label_text.starts_with("CLN") {
208 bail!("unable to load media '{}' (seems to be a cleaning unit)", label_text);
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 {
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());
222 }
223 return Ok(()) // already loaded
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 {
235 self.unload_to_free_slot(status)?;
236 status = self.status()?;
237 }
238
239 let mut slot = None;
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);
245 }
246 slot = Some(i+1);
247 break;
248 }
249 }
250 }
251
252 let slot = match slot {
253 None => bail!("unable to find media '{}' (offline?)", label_text),
254 Some(slot) => slot,
255 };
256
257 self.load_media_from_slot(slot as u64)
258 }
259
260 /// Unload media from drive (eject media if necessary)
261 fn unload_media(&mut self, target_slot: Option<u64>) -> Result<(), Error>;
262
263 /// List online media labels (label_text/barcodes)
264 ///
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()?;
269
270 let mut list = Vec::new();
271
272 for drive_status in status.drives.iter() {
273 if let ElementStatus::VolumeTag(ref tag) = drive_status.status {
274 list.push(tag.clone());
275 }
276 }
277
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());
283 }
284 }
285
286 Ok(list)
287 }
288
289 /// Load/Unload cleaning cartridge
290 ///
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()?;
295
296 let mut cleaning_cartridge_slot = None;
297
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);
303 break;
304 }
305 }
306 }
307
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,
311 };
312
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)?,
317 }
318 }
319
320 self.load_media_from_slot(cleaning_cartridge_slot)?;
321
322 self.unload_media(Some(cleaning_cartridge_slot))?;
323
324 Ok(())
325 }
326
327 /// Export media
328 ///
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()?;
334
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;
340 }
341 }
342 }
343
344 let mut from = None;
345 let mut to = None;
346
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);
352 }
353 } else if let ElementStatus::VolumeTag(ref tag) = slot_info.status {
354 if tag == label_text {
355 from = Some(i as u64 + 1);
356 }
357 }
358 }
359
360 if unload_from_drive {
361 match to {
362 Some(to) => {
363 self.unload_media(Some(to))?;
364 Ok(Some(to))
365 }
366 None => bail!("unable to find free export slot"),
367 }
368 } else {
369 match (from, to) {
370 (Some(from), Some(to)) => {
371 self.transfer_media(from, to)?;
372 Ok(Some(to))
373 }
374 (Some(_from), None) => bail!("unable to find free export slot"),
375 (None, _) => Ok(None), // not online
376 }
377 }
378 }
379
380 /// Unload media to a free storage slot
381 ///
382 /// If posible to the slot it was previously loaded from.
383 ///
384 /// Note: This method consumes status - so please read again afterward.
385 fn unload_to_free_slot(&mut self, status: MtxStatus) -> Result<(), Error> {
386
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));
393 }
394 }
395 }
396
397 if let Some(slot) = status.find_free_slot(false) {
398 self.unload_media(Some(slot))
399 } else {
400 bail!("drive '{}' unload failure - no free slot", self.drive_name());
401 }
402 }
403 }
404
405 const USE_MTX: bool = false;
406
407 impl ScsiMediaChange for ScsiTapeChanger {
408
409 fn status(&mut self, use_cache: bool) -> Result<MtxStatus, Error> {
410 if use_cache {
411 if let Some(state) = load_changer_state_cache(&self.name)? {
412 return Ok(state);
413 }
414 }
415
416 let status = if USE_MTX {
417 mtx::mtx_status(&self)
418 } else {
419 sg_pt_changer::status(&self)
420 };
421
422 match &status {
423 Ok(status) => {
424 save_changer_state_cache(&self.name, status)?;
425 }
426 Err(_) => {
427 delete_changer_state_cache(&self.name);
428 }
429 }
430
431 status
432 }
433
434 fn load_slot(&mut self, from_slot: u64, drivenum: u64) -> Result<(), Error> {
435 if USE_MTX {
436 mtx::mtx_load(&self.path, from_slot, drivenum)
437 } else {
438 let mut file = sg_pt_changer::open(&self.path)?;
439 sg_pt_changer::load_slot(&mut file, from_slot, drivenum)
440 }
441 }
442
443 fn unload(&mut self, to_slot: u64, drivenum: u64) -> Result<(), Error> {
444 if USE_MTX {
445 mtx::mtx_unload(&self.path, to_slot, drivenum)
446 } else {
447 let mut file = sg_pt_changer::open(&self.path)?;
448 sg_pt_changer::unload(&mut file, to_slot, drivenum)
449 }
450 }
451
452 fn transfer(&mut self, from_slot: u64, to_slot: u64) -> Result<(), Error> {
453 if USE_MTX {
454 mtx::mtx_transfer(&self.path, from_slot, to_slot)
455 } else {
456 let mut file = sg_pt_changer::open(&self.path)?;
457 sg_pt_changer::transfer_medium(&mut file, from_slot, to_slot)
458 }
459 }
460 }
461
462 fn save_changer_state_cache(
463 changer: &str,
464 state: &MtxStatus,
465 ) -> Result<(), Error> {
466 let mut path = PathBuf::from("/run/proxmox-backup/changer-state");
467 std::fs::create_dir_all(&path)?;
468 path.push(changer);
469
470 let backup_user = crate::backup::backup_user()?;
471 let mode = nix::sys::stat::Mode::from_bits_truncate(0o0644);
472 let options = CreateOptions::new()
473 .perm(mode)
474 .owner(backup_user.uid)
475 .group(backup_user.gid);
476
477 let state = serde_json::to_string_pretty(state)?;
478
479 replace_file(path, state.as_bytes(), options)
480 }
481
482 fn delete_changer_state_cache(changer: &str) {
483 let mut path = PathBuf::from("/run/proxmox-backup/changer-state");
484 path.push(changer);
485
486 let _ = std::fs::remove_file(&path); // ignore errors
487 }
488
489 fn load_changer_state_cache(changer: &str) -> Result<Option<MtxStatus>, Error> {
490 let mut path = PathBuf::from("/run/proxmox-backup/changer-state");
491 path.push(changer);
492
493 let data = match file_read_optional_string(&path)? {
494 None => return Ok(None),
495 Some(data) => data,
496 };
497
498 let state = serde_json::from_str(&data)?;
499
500 Ok(Some(state))
501 }
502
503 /// Implements MediaChange using 'mtx' linux cli tool
504 pub struct MtxMediaChanger {
505 drive_name: String, // used for error messages
506 drive_number: u64,
507 config: ScsiTapeChanger,
508 }
509
510 impl MtxMediaChanger {
511
512 pub fn with_drive_config(drive_config: &LinuxTapeDrive) -> Result<Self, Error> {
513 let (config, _digest) = crate::config::drive::config()?;
514 let changer_config: ScsiTapeChanger = match drive_config.changer {
515 Some(ref changer) => config.lookup("changer", changer)?,
516 None => bail!("drive '{}' has no associated changer", drive_config.name),
517 };
518
519 Ok(Self {
520 drive_name: drive_config.name.clone(),
521 drive_number: drive_config.changer_drivenum.unwrap_or(0),
522 config: changer_config,
523 })
524 }
525 }
526
527 impl MediaChange for MtxMediaChanger {
528
529 fn drive_number(&self) -> u64 {
530 self.drive_number
531 }
532
533 fn drive_name(&self) -> &str {
534 &self.drive_name
535 }
536
537 fn status(&mut self) -> Result<MtxStatus, Error> {
538 self.config.status(false)
539 }
540
541 fn transfer_media(&mut self, from: u64, to: u64) -> Result<(), Error> {
542 self.config.transfer(from, to)
543 }
544
545 fn load_media_from_slot(&mut self, slot: u64) -> Result<(), Error> {
546 self.config.load_slot(slot, self.drive_number)
547 }
548
549 fn unload_media(&mut self, target_slot: Option<u64>) -> Result<(), Error> {
550 if let Some(target_slot) = target_slot {
551 self.config.unload(target_slot, self.drive_number)
552 } else {
553 let status = self.status()?;
554 self.unload_to_free_slot(status)
555 }
556 }
557 }