]> git.proxmox.com Git - proxmox-backup.git/blame - src/tape/changer/mod.rs
ui: add missing uri encoding in user edit and view
[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
04df41ce 13use anyhow::{bail, Error};
b107fdb9 14
697c41c5
DM
15use crate::api2::types::{
16 ScsiTapeChanger,
17 LinuxTapeDrive,
18};
19
20/// Changer element status.
21///
22/// Drive and slots may be `Empty`, or contain some media, either
23/// with knwon volume tag `VolumeTag(String)`, or without (`Full`).
24pub enum ElementStatus {
25 Empty,
26 Full,
27 VolumeTag(String),
28}
29
30/// Changer drive status.
31pub struct DriveStatus {
32 /// The slot the element was loaded from (if known).
33 pub loaded_slot: Option<u64>,
34 /// The status.
35 pub status: ElementStatus,
36 /// Drive Identifier (Serial number)
37 pub drive_serial_number: Option<String>,
38 /// Element Address
39 pub element_address: u16,
40}
41
42/// Storage element status.
43pub struct StorageElementStatus {
44 /// Flag for Import/Export slots
45 pub import_export: bool,
46 /// The status.
47 pub status: ElementStatus,
48 /// Element Address
49 pub element_address: u16,
50}
51
52/// Transport element status.
53pub struct TransportElementStatus {
54 /// The status.
55 pub status: ElementStatus,
56 /// Element Address
57 pub element_address: u16,
58}
59
60/// Changer status - show drive/slot usage
61pub struct MtxStatus {
62 /// List of known drives
63 pub drives: Vec<DriveStatus>,
64 /// List of known storage slots
65 pub slots: Vec<StorageElementStatus>,
66 /// Tranport elements
67 ///
68 /// Note: Some libraries do not report transport elements.
69 pub transports: Vec<TransportElementStatus>,
70}
71
72impl MtxStatus {
73
74 pub fn slot_address(&self, slot: u64) -> Result<u16, Error> {
75 if slot == 0 {
76 bail!("invalid slot number '{}' (slots numbers starts at 1)", slot);
77 }
78 if slot > (self.slots.len() as u64) {
79 bail!("invalid slot number '{}' (max {} slots)", slot, self.slots.len());
80 }
81
82 Ok(self.slots[(slot -1) as usize].element_address)
83 }
84
85 pub fn drive_address(&self, drivenum: u64) -> Result<u16, Error> {
86 if drivenum >= (self.drives.len() as u64) {
87 bail!("invalid drive number '{}'", drivenum);
88 }
89
90 Ok(self.drives[drivenum as usize].element_address)
91 }
92
93 pub fn transport_address(&self) -> u16 {
94 // simply use first transport
95 // (are there changers exposing more than one?)
96 // defaults to 0 for changer that do not report transports
97 self
98 .transports
99 .get(0)
100 .map(|t| t.element_address)
101 .unwrap_or(0u16)
102 }
103}
104
105/// Interface to SCSI changer devices
106pub trait ScsiMediaChange {
107
108 fn status(&mut self) -> Result<MtxStatus, Error>;
109
110 fn load_slot(&mut self, from_slot: u64, drivenum: u64) -> Result<(), Error>;
111
112 fn unload(&mut self, to_slot: u64, drivenum: u64) -> Result<(), Error>;
113
114 fn transfer(&mut self, from_slot: u64, to_slot: u64) -> Result<(), Error>;
115}
116
04df41ce 117/// Interface to the media changer device for a single drive
b107fdb9
DM
118pub trait MediaChange {
119
04df41ce
DM
120 /// Drive number inside changer
121 fn drive_number(&self) -> u64;
122
123 /// Drive name (used for debug messages)
124 fn drive_name(&self) -> &str;
125
46a1863f
DM
126 /// Returns the changer status
127 fn status(&mut self) -> Result<MtxStatus, Error>;
128
0057f0e5
DM
129 /// Transfer media from on slot to another (storage or import export slots)
130 ///
131 /// Target slot needs to be empty
c92e3832 132 fn transfer_media(&mut self, from: u64, to: u64) -> Result<(), Error>;
0057f0e5 133
46a1863f
DM
134 /// Load media from storage slot into drive
135 fn load_media_from_slot(&mut self, slot: u64) -> Result<(), Error>;
136
8446fbca 137 /// Load media by label-text into drive
b107fdb9
DM
138 ///
139 /// This unloads first if the drive is already loaded with another media.
46a1863f 140 ///
04df41ce
DM
141 /// Note: This refuses to load media inside import/export
142 /// slots. Also, you cannot load cleaning units with this
143 /// interface.
8446fbca 144 fn load_media(&mut self, label_text: &str) -> Result<(), Error> {
04df41ce 145
8446fbca
DM
146 if label_text.starts_with("CLN") {
147 bail!("unable to load media '{}' (seems top be a a cleaning units)", label_text);
04df41ce
DM
148 }
149
150 let mut status = self.status()?;
151
152 let mut unload_drive = false;
153
154 // already loaded?
155 for (i, drive_status) in status.drives.iter().enumerate() {
156 if let ElementStatus::VolumeTag(ref tag) = drive_status.status {
8446fbca 157 if *tag == label_text {
04df41ce
DM
158 if i as u64 != self.drive_number() {
159 bail!("unable to load media '{}' - media in wrong drive ({} != {})",
8446fbca 160 label_text, i, self.drive_number());
04df41ce
DM
161 }
162 return Ok(()) // already loaded
163 }
164 }
165 if i as u64 == self.drive_number() {
166 match drive_status.status {
167 ElementStatus::Empty => { /* OK */ },
168 _ => unload_drive = true,
169 }
170 }
171 }
172
173 if unload_drive {
174 self.unload_to_free_slot(status)?;
175 status = self.status()?;
176 }
177
178 let mut slot = None;
697c41c5
DM
179 for (i, slot_info) in status.slots.iter().enumerate() {
180 if let ElementStatus::VolumeTag(ref tag) = slot_info.status {
181 if tag == label_text {
182 if slot_info.import_export {
8446fbca 183 bail!("unable to load media '{}' - inside import/export slot", label_text);
04df41ce
DM
184 }
185 slot = Some(i+1);
186 break;
187 }
188 }
189 }
190
191 let slot = match slot {
8446fbca 192 None => bail!("unable to find media '{}' (offline?)", label_text),
04df41ce
DM
193 Some(slot) => slot,
194 };
195
196 self.load_media_from_slot(slot as u64)
197 }
b107fdb9 198
6638c034 199 /// Unload media from drive (eject media if necessary)
46a1863f 200 fn unload_media(&mut self, target_slot: Option<u64>) -> Result<(), Error>;
b107fdb9 201
8446fbca 202 /// List online media labels (label_text/barcodes)
46a1863f 203 ///
8446fbca 204 /// List acessible (online) label texts. This does not include
46a1863f 205 /// media inside import-export slots or cleaning media.
8446fbca 206 fn online_media_label_texts(&mut self) -> Result<Vec<String>, Error> {
46a1863f
DM
207 let status = self.status()?;
208
209 let mut list = Vec::new();
210
211 for drive_status in status.drives.iter() {
212 if let ElementStatus::VolumeTag(ref tag) = drive_status.status {
213 list.push(tag.clone());
214 }
215 }
216
697c41c5
DM
217 for slot_info in status.slots.iter() {
218 if slot_info.import_export { continue; }
219 if let ElementStatus::VolumeTag(ref tag) = slot_info.status {
25d39657 220 if tag.starts_with("CLN") { continue; }
46a1863f
DM
221 list.push(tag.clone());
222 }
223 }
224
225 Ok(list)
226 }
df69a4fc
DM
227
228 /// Load/Unload cleaning cartridge
229 ///
230 /// This fail if there is no cleaning cartridge online. Any media
231 /// inside the drive is automatically unloaded.
04df41ce
DM
232 fn clean_drive(&mut self) -> Result<(), Error> {
233 let status = self.status()?;
234
235 let mut cleaning_cartridge_slot = None;
236
697c41c5
DM
237 for (i, slot_info) in status.slots.iter().enumerate() {
238 if slot_info.import_export { continue; }
239 if let ElementStatus::VolumeTag(ref tag) = slot_info.status {
04df41ce
DM
240 if tag.starts_with("CLN") {
241 cleaning_cartridge_slot = Some(i + 1);
242 break;
243 }
244 }
245 }
246
247 let cleaning_cartridge_slot = match cleaning_cartridge_slot {
248 None => bail!("clean failed - unable to find cleaning cartridge"),
249 Some(cleaning_cartridge_slot) => cleaning_cartridge_slot as u64,
250 };
251
252 if let Some(drive_status) = status.drives.get(self.drive_number() as usize) {
253 match drive_status.status {
254 ElementStatus::Empty => { /* OK */ },
255 _ => self.unload_to_free_slot(status)?,
256 }
257 }
258
259 self.load_media_from_slot(cleaning_cartridge_slot)?;
260
261 self.unload_media(Some(cleaning_cartridge_slot))?;
262
263 Ok(())
264 }
0057f0e5
DM
265
266 /// Export media
267 ///
268 /// By moving the media to an empty import-export slot. Returns
269 /// Some(slot) if the media was exported. Returns None if the media is
270 /// not online (already exported).
8446fbca 271 fn export_media(&mut self, label_text: &str) -> Result<Option<u64>, Error> {
04df41ce
DM
272 let status = self.status()?;
273
274 let mut unload_from_drive = false;
275 if let Some(drive_status) = status.drives.get(self.drive_number() as usize) {
276 if let ElementStatus::VolumeTag(ref tag) = drive_status.status {
8446fbca 277 if tag == label_text {
04df41ce
DM
278 unload_from_drive = true;
279 }
280 }
281 }
282
283 let mut from = None;
284 let mut to = None;
285
697c41c5
DM
286 for (i, slot_info) in status.slots.iter().enumerate() {
287 if slot_info.import_export {
04df41ce 288 if to.is_some() { continue; }
697c41c5 289 if let ElementStatus::Empty = slot_info.status {
04df41ce
DM
290 to = Some(i as u64 + 1);
291 }
697c41c5 292 } else if let ElementStatus::VolumeTag(ref tag) = slot_info.status {
6334bdc1
FG
293 if tag == label_text {
294 from = Some(i as u64 + 1);
04df41ce
DM
295 }
296 }
297 }
298
299 if unload_from_drive {
300 match to {
301 Some(to) => {
302 self.unload_media(Some(to))?;
303 Ok(Some(to))
304 }
305 None => bail!("unable to find free export slot"),
306 }
307 } else {
308 match (from, to) {
309 (Some(from), Some(to)) => {
310 self.transfer_media(from, to)?;
311 Ok(Some(to))
312 }
313 (Some(_from), None) => bail!("unable to find free export slot"),
314 (None, _) => Ok(None), // not online
315 }
316 }
317 }
318
319 /// Unload media to a free storage slot
320 ///
321 /// If posible to the slot it was previously loaded from.
322 ///
323 /// Note: This method consumes status - so please read again afterward.
324 fn unload_to_free_slot(&mut self, status: MtxStatus) -> Result<(), Error> {
325
326 let drive_status = &status.drives[self.drive_number() as usize];
327 if let Some(slot) = drive_status.loaded_slot {
328 // check if original slot is empty/usable
329 if let Some(info) = status.slots.get(slot as usize - 1) {
697c41c5 330 if let ElementStatus::Empty = info.status {
04df41ce
DM
331 return self.unload_media(Some(slot));
332 }
333 }
334 }
335
336 let mut free_slot = None;
337 for i in 0..status.slots.len() {
697c41c5
DM
338 if status.slots[i].import_export { continue; } // skip import/export slots
339 if let ElementStatus::Empty = status.slots[i].status {
04df41ce
DM
340 free_slot = Some((i+1) as u64);
341 break;
342 }
343 }
344 if let Some(slot) = free_slot {
345 self.unload_media(Some(slot))
346 } else {
347 bail!("drive '{}' unload failure - no free slot", self.drive_name());
348 }
349 }
b107fdb9 350}
697c41c5
DM
351
352const USE_MTX: bool = false;
353
354impl ScsiMediaChange for ScsiTapeChanger {
355
356 fn status(&mut self) -> Result<MtxStatus, Error> {
357 if USE_MTX {
358 mtx::mtx_status(&self)
359 } else {
360 let mut file = sg_pt_changer::open(&self.path)?;
361 sg_pt_changer::read_element_status(&mut file)
362 }
363 }
364
365 fn load_slot(&mut self, from_slot: u64, drivenum: u64) -> Result<(), Error> {
366 if USE_MTX {
367 mtx::mtx_load(&self.path, from_slot, drivenum)
368 } else {
369 let mut file = sg_pt_changer::open(&self.path)?;
370 sg_pt_changer::load_slot(&mut file, from_slot, drivenum)
371 }
372 }
373
374 fn unload(&mut self, to_slot: u64, drivenum: u64) -> Result<(), Error> {
375 if USE_MTX {
376 mtx::mtx_unload(&self.path, to_slot, drivenum)
377 } else {
378 let mut file = sg_pt_changer::open(&self.path)?;
379 sg_pt_changer::unload(&mut file, to_slot, drivenum)
380 }
381 }
382
383 fn transfer(&mut self, from_slot: u64, to_slot: u64) -> Result<(), Error> {
384 if USE_MTX {
385 mtx::mtx_transfer(&self.path, from_slot, to_slot)
386 } else {
387 let mut file = sg_pt_changer::open(&self.path)?;
388 sg_pt_changer::transfer_medium(&mut file, from_slot, to_slot)
389 }
390 }
391}
392
393/// Implements MediaChange using 'mtx' linux cli tool
394pub struct MtxMediaChanger {
395 drive_name: String, // used for error messages
396 drive_number: u64,
397 config: ScsiTapeChanger,
398}
399
400impl MtxMediaChanger {
401
402 pub fn with_drive_config(drive_config: &LinuxTapeDrive) -> Result<Self, Error> {
403 let (config, _digest) = crate::config::drive::config()?;
404 let changer_config: ScsiTapeChanger = match drive_config.changer {
405 Some(ref changer) => config.lookup("changer", changer)?,
406 None => bail!("drive '{}' has no associated changer", drive_config.name),
407 };
408
409 Ok(Self {
410 drive_name: drive_config.name.clone(),
411 drive_number: drive_config.changer_drive_id.unwrap_or(0),
412 config: changer_config,
413 })
414 }
415}
416
417impl MediaChange for MtxMediaChanger {
418
419 fn drive_number(&self) -> u64 {
420 self.drive_number
421 }
422
423 fn drive_name(&self) -> &str {
424 &self.drive_name
425 }
426
427 fn status(&mut self) -> Result<MtxStatus, Error> {
428 self.config.status()
429 }
430
431 fn transfer_media(&mut self, from: u64, to: u64) -> Result<(), Error> {
432 self.config.transfer(from, to)
433 }
434
435 fn load_media_from_slot(&mut self, slot: u64) -> Result<(), Error> {
436 self.config.load_slot(slot, self.drive_number)
437 }
438
439 fn unload_media(&mut self, target_slot: Option<u64>) -> Result<(), Error> {
440 if let Some(target_slot) = target_slot {
441 self.config.unload(target_slot, self.drive_number)
442 } else {
443 let status = self.status()?;
444 self.unload_to_free_slot(status)
445 }
446 }
447}