]> git.proxmox.com Git - proxmox-backup.git/blob - src/tape/changer/mod.rs
df63f6f8d2926e3f01126bd7dab0b2db46b893b3
[proxmox-backup.git] / src / tape / changer / mod.rs
1 //! Media changer implementation (SCSI media changer)
2
3 pub mod mtx;
4
5 mod online_status_map;
6 pub use online_status_map::*;
7 use proxmox_schema::ApiType;
8
9 use std::path::PathBuf;
10
11 use anyhow::{bail, Error};
12
13 use proxmox_sys::fs::{file_read_optional_string, replace_file, CreateOptions};
14
15 use pbs_api_types::{ChangerOptions, LtoTapeDrive, ScsiTapeChanger};
16
17 use pbs_tape::{linux_list_drives::open_lto_tape_device, sg_pt_changer, ElementStatus, MtxStatus};
18
19 use crate::tape::drive::{LtoTapeHandle, TapeDriver};
20
21 /// Interface to SCSI changer devices
22 pub trait ScsiMediaChange {
23 fn status(&mut self, use_cache: bool) -> Result<MtxStatus, Error>;
24
25 fn load_slot(&mut self, from_slot: u64, drivenum: u64) -> Result<MtxStatus, Error>;
26
27 fn unload(&mut self, to_slot: u64, drivenum: u64) -> Result<MtxStatus, Error>;
28
29 fn transfer(&mut self, from_slot: u64, to_slot: u64) -> Result<MtxStatus, Error>;
30 }
31
32 /// Interface to the media changer device for a single drive
33 pub trait MediaChange {
34 /// Drive number inside changer
35 fn drive_number(&self) -> u64;
36
37 /// Drive name (used for debug messages)
38 fn drive_name(&self) -> &str;
39
40 /// Returns the changer status
41 fn status(&mut self) -> Result<MtxStatus, Error>;
42
43 /// Transfer media from on slot to another (storage or import export slots)
44 ///
45 /// Target slot needs to be empty
46 fn transfer_media(&mut self, from: u64, to: u64) -> Result<MtxStatus, Error>;
47
48 /// Load media from storage slot into drive
49 fn load_media_from_slot(&mut self, slot: u64) -> Result<MtxStatus, Error>;
50
51 /// Load media by label-text into drive
52 ///
53 /// This unloads first if the drive is already loaded with another media.
54 ///
55 /// Note: This refuses to load media inside import/export
56 /// slots. Also, you cannot load cleaning units with this
57 /// interface.
58 fn load_media(&mut self, label_text: &str) -> Result<MtxStatus, Error> {
59 if label_text.starts_with("CLN") {
60 bail!(
61 "unable to load media '{}' (seems to be a cleaning unit)",
62 label_text
63 );
64 }
65
66 let mut status = self.status()?;
67
68 let mut unload_drive = false;
69
70 // already loaded?
71 for (i, drive_status) in status.drives.iter().enumerate() {
72 if let ElementStatus::VolumeTag(ref tag) = drive_status.status {
73 if *tag == label_text {
74 if i as u64 != self.drive_number() {
75 bail!(
76 "unable to load media '{}' - media in wrong drive ({} != {})",
77 label_text,
78 i,
79 self.drive_number()
80 );
81 }
82 return Ok(status); // already loaded
83 }
84 }
85 if i as u64 == self.drive_number() {
86 match drive_status.status {
87 ElementStatus::Empty => { /* OK */ }
88 _ => unload_drive = true,
89 }
90 }
91 }
92
93 if unload_drive {
94 status = self.unload_to_free_slot(status)?;
95 }
96
97 let mut slot = None;
98 for (i, slot_info) in status.slots.iter().enumerate() {
99 if let ElementStatus::VolumeTag(ref tag) = slot_info.status {
100 if tag == label_text {
101 if slot_info.import_export {
102 bail!(
103 "unable to load media '{}' - inside import/export slot",
104 label_text
105 );
106 }
107 slot = Some(i + 1);
108 break;
109 }
110 }
111 }
112
113 let slot = match slot {
114 None => bail!("unable to find media '{}' (offline?)", label_text),
115 Some(slot) => slot,
116 };
117
118 self.load_media_from_slot(slot as u64)
119 }
120
121 /// Unload media from drive (eject media if necessary)
122 fn unload_media(&mut self, target_slot: Option<u64>) -> Result<MtxStatus, Error>;
123
124 /// List online media labels (label_text/barcodes)
125 ///
126 /// List accessible (online) label texts. This does not include
127 /// media inside import-export slots or cleaning media.
128 fn online_media_label_texts(&mut self) -> Result<Vec<String>, Error> {
129 let status = self.status()?;
130
131 let mut list = Vec::new();
132
133 for drive_status in status.drives.iter() {
134 if let ElementStatus::VolumeTag(ref tag) = drive_status.status {
135 list.push(tag.clone());
136 }
137 }
138
139 for slot_info in status.slots.iter() {
140 if slot_info.import_export {
141 continue;
142 }
143 if let ElementStatus::VolumeTag(ref tag) = slot_info.status {
144 if tag.starts_with("CLN") {
145 continue;
146 }
147 list.push(tag.clone());
148 }
149 }
150
151 Ok(list)
152 }
153
154 /// Load/Unload cleaning cartridge
155 ///
156 /// This fail if there is no cleaning cartridge online. Any media
157 /// inside the drive is automatically unloaded.
158 fn clean_drive(&mut self) -> Result<MtxStatus, Error> {
159 let mut status = self.status()?;
160
161 // Unload drive first. Note: This also unloads a loaded cleaning tape
162 if let Some(drive_status) = status.drives.get(self.drive_number() as usize) {
163 match drive_status.status {
164 ElementStatus::Empty => { /* OK */ }
165 _ => {
166 status = self.unload_to_free_slot(status)?;
167 }
168 }
169 }
170
171 let mut cleaning_cartridge_slot = None;
172
173 for (i, slot_info) in status.slots.iter().enumerate() {
174 if slot_info.import_export {
175 continue;
176 }
177 if let ElementStatus::VolumeTag(ref tag) = slot_info.status {
178 if tag.starts_with("CLN") {
179 cleaning_cartridge_slot = Some(i + 1);
180 break;
181 }
182 }
183 }
184
185 let cleaning_cartridge_slot = match cleaning_cartridge_slot {
186 None => bail!("clean failed - unable to find cleaning cartridge"),
187 Some(cleaning_cartridge_slot) => cleaning_cartridge_slot as u64,
188 };
189
190 self.load_media_from_slot(cleaning_cartridge_slot)?;
191
192 self.unload_media(Some(cleaning_cartridge_slot))
193 }
194
195 /// Export media
196 ///
197 /// By moving the media to an empty import-export slot. Returns
198 /// Some(slot) if the media was exported. Returns None if the media is
199 /// not online (already exported).
200 fn export_media(&mut self, label_text: &str) -> Result<Option<u64>, Error> {
201 let status = self.status()?;
202
203 let mut unload_from_drive = false;
204 if let Some(drive_status) = status.drives.get(self.drive_number() as usize) {
205 if let ElementStatus::VolumeTag(ref tag) = drive_status.status {
206 if tag == label_text {
207 unload_from_drive = true;
208 }
209 }
210 }
211
212 let mut from = None;
213 let mut to = None;
214
215 for (i, slot_info) in status.slots.iter().enumerate() {
216 if slot_info.import_export {
217 if to.is_some() {
218 continue;
219 }
220 if let ElementStatus::Empty = slot_info.status {
221 to = Some(i as u64 + 1);
222 }
223 } else if let ElementStatus::VolumeTag(ref tag) = slot_info.status {
224 if tag == label_text {
225 from = Some(i as u64 + 1);
226 }
227 }
228 }
229
230 if unload_from_drive {
231 match to {
232 Some(to) => {
233 self.unload_media(Some(to))?;
234 Ok(Some(to))
235 }
236 None => bail!("unable to find free export slot"),
237 }
238 } else {
239 match (from, to) {
240 (Some(from), Some(to)) => {
241 self.transfer_media(from, to)?;
242 Ok(Some(to))
243 }
244 (Some(_from), None) => bail!("unable to find free export slot"),
245 (None, _) => Ok(None), // not online
246 }
247 }
248 }
249
250 /// Unload media to a free storage slot
251 ///
252 /// If possible to the slot it was previously loaded from.
253 ///
254 /// Note: This method consumes status - so please use returned status afterward.
255 fn unload_to_free_slot(&mut self, status: MtxStatus) -> Result<MtxStatus, Error> {
256 let drive_status = &status.drives[self.drive_number() as usize];
257 if let Some(slot) = drive_status.loaded_slot {
258 // check if original slot is empty/usable
259 if let Some(info) = status.slots.get(slot as usize - 1) {
260 if let ElementStatus::Empty = info.status {
261 return self.unload_media(Some(slot));
262 }
263 }
264 }
265
266 if let Some(slot) = status.find_free_slot(false) {
267 self.unload_media(Some(slot))
268 } else {
269 bail!(
270 "drive '{}' unload failure - no free slot",
271 self.drive_name()
272 );
273 }
274 }
275 }
276
277 const USE_MTX: bool = false;
278
279 impl ScsiMediaChange for ScsiTapeChanger {
280 fn status(&mut self, use_cache: bool) -> Result<MtxStatus, Error> {
281 if use_cache {
282 if let Some(state) = load_changer_state_cache(&self.name)? {
283 return Ok(state);
284 }
285 }
286
287 let status = if USE_MTX {
288 mtx::mtx_status(self)
289 } else {
290 sg_pt_changer::status(self)
291 };
292
293 match &status {
294 Ok(status) => {
295 save_changer_state_cache(&self.name, status)?;
296 }
297 Err(_) => {
298 delete_changer_state_cache(&self.name);
299 }
300 }
301
302 status
303 }
304
305 fn load_slot(&mut self, from_slot: u64, drivenum: u64) -> Result<MtxStatus, Error> {
306 let result = if USE_MTX {
307 mtx::mtx_load(&self.path, from_slot, drivenum)
308 } else {
309 let mut file = sg_pt_changer::open(&self.path)?;
310 sg_pt_changer::load_slot(&mut file, from_slot, drivenum)
311 };
312
313 let status = self.status(false)?; // always update status
314
315 result?; // check load result
316
317 Ok(status)
318 }
319
320 fn unload(&mut self, to_slot: u64, drivenum: u64) -> Result<MtxStatus, Error> {
321 let result = if USE_MTX {
322 mtx::mtx_unload(&self.path, to_slot, drivenum)
323 } else {
324 let mut file = sg_pt_changer::open(&self.path)?;
325 sg_pt_changer::unload(&mut file, to_slot, drivenum)
326 };
327
328 let status = self.status(false)?; // always update status
329
330 result?; // check unload result
331
332 Ok(status)
333 }
334
335 fn transfer(&mut self, from_slot: u64, to_slot: u64) -> Result<MtxStatus, Error> {
336 let result = if USE_MTX {
337 mtx::mtx_transfer(&self.path, from_slot, to_slot)
338 } else {
339 let mut file = sg_pt_changer::open(&self.path)?;
340 sg_pt_changer::transfer_medium(&mut file, from_slot, to_slot)
341 };
342
343 let status = self.status(false)?; // always update status
344
345 result?; // check unload result
346
347 Ok(status)
348 }
349 }
350
351 fn save_changer_state_cache(changer: &str, state: &MtxStatus) -> Result<(), Error> {
352 let mut path = PathBuf::from(crate::tape::CHANGER_STATE_DIR);
353 path.push(changer);
354
355 let state = serde_json::to_string_pretty(state)?;
356
357 let backup_user = pbs_config::backup_user()?;
358 let mode = nix::sys::stat::Mode::from_bits_truncate(0o0644);
359 let options = CreateOptions::new()
360 .perm(mode)
361 .owner(backup_user.uid)
362 .group(backup_user.gid);
363
364 replace_file(path, state.as_bytes(), options, false)
365 }
366
367 fn delete_changer_state_cache(changer: &str) {
368 let mut path = PathBuf::from("/run/proxmox-backup/changer-state");
369 path.push(changer);
370
371 let _ = std::fs::remove_file(&path); // ignore errors
372 }
373
374 fn load_changer_state_cache(changer: &str) -> Result<Option<MtxStatus>, Error> {
375 let mut path = PathBuf::from("/run/proxmox-backup/changer-state");
376 path.push(changer);
377
378 let data = match file_read_optional_string(&path)? {
379 None => return Ok(None),
380 Some(data) => data,
381 };
382
383 let state = serde_json::from_str(&data)?;
384
385 Ok(Some(state))
386 }
387
388 /// Implements MediaChange using 'mtx' linux cli tool
389 pub struct MtxMediaChanger {
390 drive: LtoTapeDrive,
391 config: ScsiTapeChanger,
392 }
393
394 impl MtxMediaChanger {
395 pub fn with_drive_config(drive_config: &LtoTapeDrive) -> Result<Self, Error> {
396 let (config, _digest) = pbs_config::drive::config()?;
397 let changer_config: ScsiTapeChanger = match drive_config.changer {
398 Some(ref changer) => config.lookup("changer", changer)?,
399 None => bail!("drive '{}' has no associated changer", drive_config.name),
400 };
401
402 Ok(Self {
403 drive: drive_config.clone(),
404 config: changer_config,
405 })
406 }
407 }
408
409 impl MediaChange for MtxMediaChanger {
410 fn drive_number(&self) -> u64 {
411 self.drive.changer_drivenum.unwrap_or(0)
412 }
413
414 fn drive_name(&self) -> &str {
415 &self.drive.name
416 }
417
418 fn status(&mut self) -> Result<MtxStatus, Error> {
419 self.config.status(false)
420 }
421
422 fn transfer_media(&mut self, from: u64, to: u64) -> Result<MtxStatus, Error> {
423 self.config.transfer(from, to)
424 }
425
426 fn load_media_from_slot(&mut self, slot: u64) -> Result<MtxStatus, Error> {
427 self.config.load_slot(slot, self.drive_number())
428 }
429
430 fn unload_media(&mut self, target_slot: Option<u64>) -> Result<MtxStatus, Error> {
431 let options: ChangerOptions = serde_json::from_value(
432 ChangerOptions::API_SCHEMA
433 .parse_property_string(self.config.options.as_deref().unwrap_or_default())?,
434 )?;
435
436 if options.eject_before_unload {
437 let file = open_lto_tape_device(&self.drive.path)?;
438 let mut handle = LtoTapeHandle::new(file)?;
439
440 if handle.medium_present() {
441 handle.eject_media()?;
442 }
443 }
444
445 if let Some(target_slot) = target_slot {
446 self.config.unload(target_slot, self.drive_number())
447 } else {
448 let status = self.status()?;
449 self.unload_to_free_slot(status)
450 }
451 }
452 }