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