]> git.proxmox.com Git - proxmox-backup.git/blob - src/tape/drive/virtual_tape.rs
cleanup proxmox_backup::backup module
[proxmox-backup.git] / src / tape / drive / virtual_tape.rs
1 // Note: This is only for test an debug
2
3 use std::fs::File;
4 use std::io;
5
6 use anyhow::{bail, format_err, Error};
7 use serde::{Serialize, Deserialize};
8
9 use proxmox::tools::{
10 fs::{replace_file, CreateOptions},
11 };
12
13 use pbs_datastore::key_derivation::KeyConfig;
14
15 use crate::{
16 tape::{
17 TapeWrite,
18 TapeRead,
19 BlockReadError,
20 changer::{
21 MediaChange,
22 MtxStatus,
23 DriveStatus,
24 ElementStatus,
25 StorageElementStatus,
26 },
27 drive::{
28 VirtualTapeDrive,
29 TapeDriver,
30 },
31 file_formats::{
32 MediaSetLabel,
33 MediaContentHeader,
34 PROXMOX_BACKUP_MEDIA_SET_LABEL_MAGIC_1_0,
35 BlockedReader,
36 BlockedWriter,
37 },
38 helpers::{
39 EmulateTapeReader,
40 EmulateTapeWriter,
41 },
42 },
43 };
44
45 impl VirtualTapeDrive {
46
47 /// This needs to lock the drive
48 pub fn open(&self) -> Result<VirtualTapeHandle, Error> {
49 proxmox::try_block!({
50 let mut lock_path = std::path::PathBuf::from(&self.path);
51 lock_path.push(".drive.lck");
52
53 let options = CreateOptions::new();
54 let timeout = std::time::Duration::new(10, 0);
55 let lock = proxmox::tools::fs::open_file_locked(&lock_path, timeout, true, options)?;
56
57 Ok(VirtualTapeHandle {
58 _lock: lock,
59 drive_name: self.name.clone(),
60 max_size: self.max_size.unwrap_or(64*1024*1024),
61 path: std::path::PathBuf::from(&self.path),
62 })
63 }).map_err(|err: Error| format_err!("open drive '{}' ({}) failed - {}", self.name, self.path, err))
64 }
65 }
66
67 #[derive(Serialize,Deserialize)]
68 struct VirtualTapeStatus {
69 name: String,
70 pos: usize,
71 }
72
73 #[derive(Serialize,Deserialize)]
74 struct VirtualDriveStatus {
75 current_tape: Option<VirtualTapeStatus>,
76 }
77
78 #[derive(Serialize,Deserialize)]
79 struct TapeIndex {
80 files: usize,
81 }
82
83 pub struct VirtualTapeHandle {
84 drive_name: String,
85 path: std::path::PathBuf,
86 max_size: usize,
87 _lock: File,
88 }
89
90 impl VirtualTapeHandle {
91
92 fn status_file_path(&self) -> std::path::PathBuf {
93 let mut path = self.path.clone();
94 path.push("drive-status.json");
95 path
96 }
97
98 fn tape_index_path(&self, tape_name: &str) -> std::path::PathBuf {
99 let mut path = self.path.clone();
100 path.push(format!("tape-{}.json", tape_name));
101 path
102 }
103
104 fn tape_file_path(&self, tape_name: &str, pos: usize) -> std::path::PathBuf {
105 let mut path = self.path.clone();
106 path.push(format!("tapefile-{}-{}.json", pos, tape_name));
107 path
108 }
109
110 fn load_tape_index(&self, tape_name: &str) -> Result<TapeIndex, Error> {
111 let path = self.tape_index_path(tape_name);
112 let raw = proxmox::tools::fs::file_get_contents(&path)?;
113 if raw.is_empty() {
114 return Ok(TapeIndex { files: 0 });
115 }
116 let data: TapeIndex = serde_json::from_slice(&raw)?;
117 Ok(data)
118 }
119
120 fn store_tape_index(&self, tape_name: &str, index: &TapeIndex) -> Result<(), Error> {
121 let path = self.tape_index_path(tape_name);
122 let raw = serde_json::to_string_pretty(&serde_json::to_value(index)?)?;
123
124 let options = CreateOptions::new();
125 replace_file(&path, raw.as_bytes(), options)?;
126 Ok(())
127 }
128
129 fn truncate_tape(&self, tape_name: &str, pos: usize) -> Result<usize, Error> {
130 let mut index = self.load_tape_index(tape_name)?;
131
132 if index.files <= pos {
133 return Ok(index.files)
134 }
135
136 for i in pos..index.files {
137 let path = self.tape_file_path(tape_name, i);
138 let _ = std::fs::remove_file(path);
139 }
140
141 index.files = pos;
142
143 self.store_tape_index(tape_name, &index)?;
144
145 Ok(index.files)
146 }
147
148 fn load_status(&self) -> Result<VirtualDriveStatus, Error> {
149 let path = self.status_file_path();
150
151 let default = serde_json::to_value(VirtualDriveStatus {
152 current_tape: None,
153 })?;
154
155 let data = proxmox::tools::fs::file_get_json(&path, Some(default))?;
156 let status: VirtualDriveStatus = serde_json::from_value(data)?;
157 Ok(status)
158 }
159
160 fn store_status(&self, status: &VirtualDriveStatus) -> Result<(), Error> {
161 let path = self.status_file_path();
162 let raw = serde_json::to_string_pretty(&serde_json::to_value(status)?)?;
163
164 let options = CreateOptions::new();
165 replace_file(&path, raw.as_bytes(), options)?;
166 Ok(())
167 }
168
169 fn online_media_label_texts(&self) -> Result<Vec<String>, Error> {
170 let mut list = Vec::new();
171 for entry in std::fs::read_dir(&self.path)? {
172 let entry = entry?;
173 let path = entry.path();
174 if path.is_file() && path.extension() == Some(std::ffi::OsStr::new("json")) {
175 if let Some(name) = path.file_stem() {
176 if let Some(name) = name.to_str() {
177 if let Some(label) = name.strip_prefix("tape-") {
178 list.push(label.to_string());
179 }
180 }
181 }
182 }
183 }
184 Ok(list)
185 }
186
187 #[allow(dead_code)]
188 fn forward_space_count_files(&mut self, count: usize) -> Result<(), Error> {
189 let mut status = self.load_status()?;
190 match status.current_tape {
191 Some(VirtualTapeStatus { ref name, ref mut pos }) => {
192
193 let index = self.load_tape_index(name)
194 .map_err(|err| io::Error::new(io::ErrorKind::Other, err.to_string()))?;
195
196 let new_pos = *pos + count;
197 if new_pos <= index.files {
198 *pos = new_pos;
199 } else {
200 bail!("forward_space_count_files failed: move beyond EOT");
201 }
202
203 self.store_status(&status)
204 .map_err(|err| io::Error::new(io::ErrorKind::Other, err.to_string()))?;
205
206 Ok(())
207 }
208 None => bail!("drive is empty (no tape loaded)."),
209 }
210 }
211
212 // Note: behavior differs from LTO, because we always position at
213 // EOT side.
214 fn backward_space_count_files(&mut self, count: usize) -> Result<(), Error> {
215 let mut status = self.load_status()?;
216 match status.current_tape {
217 Some(VirtualTapeStatus { ref mut pos, .. }) => {
218
219 if count <= *pos {
220 *pos = *pos - count;
221 } else {
222 bail!("backward_space_count_files failed: move before BOT");
223 }
224
225 self.store_status(&status)
226 .map_err(|err| io::Error::new(io::ErrorKind::Other, err.to_string()))?;
227
228 Ok(())
229 }
230 None => bail!("drive is empty (no tape loaded)."),
231 }
232 }
233
234 }
235
236 impl TapeDriver for VirtualTapeHandle {
237
238 fn sync(&mut self) -> Result<(), Error> {
239 Ok(()) // do nothing for now
240 }
241
242 fn current_file_number(&mut self) -> Result<u64, Error> {
243 let status = self.load_status()
244 .map_err(|err| format_err!("current_file_number failed: {}", err.to_string()))?;
245
246 match status.current_tape {
247 Some(VirtualTapeStatus { pos, .. }) => { Ok(pos as u64)},
248 None => bail!("current_file_number failed: drive is empty (no tape loaded)."),
249 }
250 }
251
252 /// Move to last file
253 fn move_to_last_file(&mut self) -> Result<(), Error> {
254
255 self.move_to_eom(false)?;
256
257 if self.current_file_number()? == 0 {
258 bail!("move_to_last_file failed - media contains no data");
259 }
260
261 self.backward_space_count_files(1)?;
262
263 Ok(())
264 }
265
266 fn move_to_file(&mut self, file: u64) -> Result<(), Error> {
267 let mut status = self.load_status()?;
268 match status.current_tape {
269 Some(VirtualTapeStatus { ref name, ref mut pos }) => {
270
271 let index = self.load_tape_index(name)
272 .map_err(|err| io::Error::new(io::ErrorKind::Other, err.to_string()))?;
273
274 if file as usize > index.files {
275 bail!("invalid file nr");
276 }
277
278 *pos = file as usize;
279
280 self.store_status(&status)
281 .map_err(|err| io::Error::new(io::ErrorKind::Other, err.to_string()))?;
282
283 Ok(())
284 }
285 None => bail!("drive is empty (no tape loaded)."),
286 }
287 }
288
289 fn read_next_file(&mut self) -> Result<Box<dyn TapeRead>, BlockReadError> {
290 let mut status = self.load_status()
291 .map_err(|err| BlockReadError::Error(io::Error::new(io::ErrorKind::Other, err.to_string())))?;
292
293 match status.current_tape {
294 Some(VirtualTapeStatus { ref name, ref mut pos }) => {
295
296 let index = self.load_tape_index(name)
297 .map_err(|err| BlockReadError::Error(io::Error::new(io::ErrorKind::Other, err.to_string())))?;
298
299 if *pos >= index.files {
300 return Err(BlockReadError::EndOfStream);
301 }
302
303 let path = self.tape_file_path(name, *pos);
304 let file = std::fs::OpenOptions::new()
305 .read(true)
306 .open(path)?;
307
308 *pos += 1;
309 self.store_status(&status)
310 .map_err(|err| BlockReadError::Error(io::Error::new(io::ErrorKind::Other, err.to_string())))?;
311
312 let reader = EmulateTapeReader::new(file);
313 let reader = BlockedReader::open(reader)?;
314 Ok(Box::new(reader))
315 }
316 None => {
317 return Err(BlockReadError::Error(proxmox::io_format_err!("drive is empty (no tape loaded).")));
318 }
319 }
320 }
321
322 fn write_file(&mut self) -> Result<Box<dyn TapeWrite>, io::Error> {
323 let mut status = self.load_status()
324 .map_err(|err| io::Error::new(io::ErrorKind::Other, err.to_string()))?;
325
326 match status.current_tape {
327 Some(VirtualTapeStatus { ref name, ref mut pos }) => {
328
329 let mut index = self.load_tape_index(name)
330 .map_err(|err| io::Error::new(io::ErrorKind::Other, err.to_string()))?;
331
332 for i in *pos..index.files {
333 let path = self.tape_file_path(name, i);
334 let _ = std::fs::remove_file(path);
335 }
336
337 let mut used_space = 0;
338 for i in 0..*pos {
339 let path = self.tape_file_path(name, i);
340 used_space += path.metadata()?.len() as usize;
341
342 }
343 index.files = *pos + 1;
344
345 self.store_tape_index(name, &index)
346 .map_err(|err| io::Error::new(io::ErrorKind::Other, err.to_string()))?;
347
348 let path = self.tape_file_path(name, *pos);
349 let file = std::fs::OpenOptions::new()
350 .write(true)
351 .create(true)
352 .truncate(true)
353 .open(path)?;
354
355 *pos = index.files;
356
357 self.store_status(&status)
358 .map_err(|err| io::Error::new(io::ErrorKind::Other, err.to_string()))?;
359
360 let mut free_space = 0;
361 if used_space < self.max_size {
362 free_space = self.max_size - used_space;
363 }
364
365 let writer = EmulateTapeWriter::new(file, free_space);
366 let writer = Box::new(BlockedWriter::new(writer));
367
368 Ok(writer)
369 }
370 None => proxmox::io_bail!("drive is empty (no tape loaded)."),
371 }
372 }
373
374 fn move_to_eom(&mut self, _write_missing_eof: bool) -> Result<(), Error> {
375 let mut status = self.load_status()?;
376 match status.current_tape {
377 Some(VirtualTapeStatus { ref name, ref mut pos }) => {
378
379 let index = self.load_tape_index(name)
380 .map_err(|err| io::Error::new(io::ErrorKind::Other, err.to_string()))?;
381
382 *pos = index.files;
383
384 self.store_status(&status)
385 .map_err(|err| io::Error::new(io::ErrorKind::Other, err.to_string()))?;
386
387 Ok(())
388 }
389 None => bail!("drive is empty (no tape loaded)."),
390 }
391 }
392
393 fn rewind(&mut self) -> Result<(), Error> {
394 let mut status = self.load_status()?;
395 match status.current_tape {
396 Some(ref mut tape_status) => {
397 tape_status.pos = 0;
398 self.store_status(&status)?;
399 Ok(())
400 }
401 None => bail!("drive is empty (no tape loaded)."),
402 }
403 }
404
405 fn format_media(&mut self, _fast: bool) -> Result<(), Error> {
406 let mut status = self.load_status()?;
407 match status.current_tape {
408 Some(VirtualTapeStatus { ref name, ref mut pos }) => {
409 *pos = self.truncate_tape(name, 0)?;
410 self.store_status(&status)?;
411 Ok(())
412 }
413 None => bail!("drive is empty (no tape loaded)."),
414 }
415 }
416
417 fn write_media_set_label(
418 &mut self,
419 media_set_label: &MediaSetLabel,
420 key_config: Option<&KeyConfig>,
421 ) -> Result<(), Error> {
422
423 self.set_encryption(None)?;
424
425 if key_config.is_some() {
426 bail!("encryption is not implemented - internal error");
427 }
428
429 let mut status = self.load_status()?;
430 match status.current_tape {
431 Some(VirtualTapeStatus { ref name, ref mut pos }) => {
432 *pos = self.truncate_tape(name, 1)?;
433 let pos = *pos;
434 self.store_status(&status)?;
435
436 if pos == 0 {
437 bail!("media is empty (no label).");
438 }
439 if pos != 1 {
440 bail!("write_media_set_label: truncate failed - got wrong pos '{}'", pos);
441 }
442
443 let raw = serde_json::to_string_pretty(&serde_json::to_value(media_set_label)?)?;
444 let header = MediaContentHeader::new(PROXMOX_BACKUP_MEDIA_SET_LABEL_MAGIC_1_0, raw.len() as u32);
445
446 {
447 let mut writer = self.write_file()?;
448 writer.write_header(&header, raw.as_bytes())?;
449 writer.finish(false)?;
450 }
451
452 Ok(())
453 }
454 None => bail!("drive is empty (no tape loaded)."),
455 }
456 }
457
458 fn eject_media(&mut self) -> Result<(), Error> {
459 let status = VirtualDriveStatus {
460 current_tape: None,
461 };
462 self.store_status(&status)
463 }
464 }
465
466 impl MediaChange for VirtualTapeHandle {
467
468 fn drive_number(&self) -> u64 {
469 0
470 }
471
472 fn drive_name(&self) -> &str {
473 &self.drive_name
474 }
475
476 fn status(&mut self) -> Result<MtxStatus, Error> {
477
478 let drive_status = self.load_status()?;
479
480 let mut drives = Vec::new();
481
482 if let Some(current_tape) = &drive_status.current_tape {
483 drives.push(DriveStatus {
484 loaded_slot: None,
485 status: ElementStatus::VolumeTag(current_tape.name.clone()),
486 drive_serial_number: None,
487 vendor: None,
488 model: None,
489 element_address: 0,
490 });
491 }
492
493 // This implementation is lame, because we do not have fixed
494 // slot-assignment here.
495
496 let mut slots = Vec::new();
497 let label_texts = self.online_media_label_texts()?;
498 let max_slots = ((label_texts.len() + 7)/8) * 8;
499
500 for i in 0..max_slots {
501 let status = if let Some(label_text) = label_texts.get(i) {
502 ElementStatus::VolumeTag(label_text.clone())
503 } else {
504 ElementStatus::Empty
505 };
506 slots.push(StorageElementStatus {
507 import_export: false,
508 status,
509 element_address: (i + 1) as u16,
510 });
511 }
512
513 Ok(MtxStatus { drives, slots, transports: Vec::new() })
514 }
515
516 fn transfer_media(&mut self, _from: u64, _to: u64) -> Result<MtxStatus, Error> {
517 bail!("media transfer is not implemented!");
518 }
519
520 fn export_media(&mut self, _label_text: &str) -> Result<Option<u64>, Error> {
521 bail!("media export is not implemented!");
522 }
523
524 fn load_media_from_slot(&mut self, slot: u64) -> Result<MtxStatus, Error> {
525 if slot < 1 {
526 bail!("invalid slot ID {}", slot);
527 }
528
529 let label_texts = self.online_media_label_texts()?;
530
531 if slot > label_texts.len() as u64 {
532 bail!("slot {} is empty", slot);
533 }
534
535 self.load_media(&label_texts[slot as usize - 1])
536 }
537
538 /// Try to load media
539 ///
540 /// We automatically create an empty virtual tape here (if it does
541 /// not exist already)
542 fn load_media(&mut self, label: &str) -> Result<MtxStatus, Error> {
543 let name = format!("tape-{}.json", label);
544 let mut path = self.path.clone();
545 path.push(&name);
546 if !path.exists() {
547 eprintln!("unable to find tape {} - creating file {:?}", label, path);
548 let index = TapeIndex { files: 0 };
549 self.store_tape_index(label, &index)?;
550 }
551
552 let status = VirtualDriveStatus {
553 current_tape: Some(VirtualTapeStatus {
554 name: label.to_string(),
555 pos: 0,
556 }),
557 };
558 self.store_status(&status)?;
559
560 self.status()
561 }
562
563 fn unload_media(&mut self, _target_slot: Option<u64>) -> Result<MtxStatus, Error> {
564 // Note: we currently simply ignore target_slot
565 self.eject_media()?;
566 self.status()
567 }
568
569 fn clean_drive(&mut self) -> Result<MtxStatus, Error> {
570 // do nothing
571 self.status()
572 }
573 }
574
575 impl MediaChange for VirtualTapeDrive {
576
577 fn drive_number(&self) -> u64 {
578 0
579 }
580
581 fn drive_name(&self) -> &str {
582 &self.name
583 }
584
585 fn status(&mut self) -> Result<MtxStatus, Error> {
586 let mut handle = self.open()?;
587 handle.status()
588 }
589
590 fn transfer_media(&mut self, from: u64, to: u64) -> Result<MtxStatus, Error> {
591 let mut handle = self.open()?;
592 handle.transfer_media(from, to)
593 }
594
595 fn export_media(&mut self, label_text: &str) -> Result<Option<u64>, Error> {
596 let mut handle = self.open()?;
597 handle.export_media(label_text)
598 }
599
600 fn load_media_from_slot(&mut self, slot: u64) -> Result<MtxStatus, Error> {
601 let mut handle = self.open()?;
602 handle.load_media_from_slot(slot)
603 }
604
605 fn load_media(&mut self, label_text: &str) -> Result<MtxStatus, Error> {
606 let mut handle = self.open()?;
607 handle.load_media(label_text)
608 }
609
610 fn unload_media(&mut self, target_slot: Option<u64>) -> Result<MtxStatus, Error> {
611 let mut handle = self.open()?;
612 handle.unload_media(target_slot)
613 }
614
615 fn online_media_label_texts(&mut self) -> Result<Vec<String>, Error> {
616 let handle = self.open()?;
617 handle.online_media_label_texts()
618 }
619
620 fn clean_drive(&mut self) -> Result<MtxStatus, Error> {
621 let mut handle = self.open()?;
622 handle.clean_drive()
623 }
624 }