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