]>
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 | ||
4be47366 | 13 | use std::collections::HashSet; |
4188fd59 | 14 | use std::path::PathBuf; |
4be47366 | 15 | |
04df41ce | 16 | use anyhow::{bail, Error}; |
c3747b93 | 17 | use serde::{Serialize, Deserialize}; |
4be47366 DC |
18 | use serde_json::Value; |
19 | ||
4188fd59 DM |
20 | use 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 | 29 | use 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 |
40 | pub enum ElementStatus { |
41 | Empty, | |
42 | Full, | |
43 | VolumeTag(String), | |
44 | } | |
45 | ||
46 | /// Changer drive status. | |
c3747b93 | 47 | #[derive(Serialize, Deserialize)] |
697c41c5 DM |
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>, | |
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 |
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. | |
c3747b93 | 75 | #[derive(Serialize, Deserialize)] |
697c41c5 DM |
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 | |
c3747b93 | 84 | #[derive(Serialize, Deserialize)] |
697c41c5 DM |
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>, | |
d1d74c43 | 90 | /// Transport elements |
697c41c5 DM |
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 | } | |
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 | |
167 | pub 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 |
179 | pub 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 | |
404 | const USE_MTX: bool = false; | |
405 | ||
406 | impl 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 |
479 | fn 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 | ||
499 | fn 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 | ||
506 | fn 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 |
521 | pub struct MtxMediaChanger { | |
522 | drive_name: String, // used for error messages | |
523 | drive_number: u64, | |
524 | config: ScsiTapeChanger, | |
525 | } | |
526 | ||
527 | impl 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 | ||
544 | impl 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 | } |