]>
Commit | Line | Data |
---|---|---|
37796ff7 DM |
1 | //! Media changer implementation (SCSI media changer) |
2 | ||
b107fdb9 DM |
3 | mod email; |
4 | pub use email::*; | |
5 | ||
697c41c5 | 6 | pub mod sg_pt_changer; |
b107fdb9 | 7 | |
697c41c5 | 8 | pub mod mtx; |
b107fdb9 | 9 | |
37796ff7 DM |
10 | mod online_status_map; |
11 | pub use online_status_map::*; | |
12 | ||
04df41ce | 13 | use anyhow::{bail, Error}; |
b107fdb9 | 14 | |
697c41c5 DM |
15 | use 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`). | |
24 | pub enum ElementStatus { | |
25 | Empty, | |
26 | Full, | |
27 | VolumeTag(String), | |
28 | } | |
29 | ||
30 | /// Changer drive status. | |
31 | pub 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. | |
43 | pub 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. | |
53 | pub 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 | |
61 | pub 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 | ||
72 | impl 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 | |
106 | pub 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 |
118 | pub 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 | |
352 | const USE_MTX: bool = false; | |
353 | ||
354 | impl 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 | |
394 | pub struct MtxMediaChanger { | |
395 | drive_name: String, // used for error messages | |
396 | drive_number: u64, | |
397 | config: ScsiTapeChanger, | |
398 | } | |
399 | ||
400 | impl 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 | ||
417 | impl 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 | } |