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