]> git.proxmox.com Git - proxmox-backup.git/blob - src/tape/drive/virtual_tape.rs
tape: add tape device driver
[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 Uuid,
11 fs::{replace_file, CreateOptions},
12 };
13
14 use crate::{
15 tape::{
16 TapeWrite,
17 TapeRead,
18 changer::MediaChange,
19 drive::{
20 VirtualTapeDrive,
21 TapeDriver,
22 },
23 file_formats::{
24 MediaSetLabel,
25 MediaContentHeader,
26 PROXMOX_BACKUP_MEDIA_SET_LABEL_MAGIC_1_0,
27 },
28 helpers::{
29 EmulateTapeReader,
30 EmulateTapeWriter,
31 BlockedReader,
32 BlockedWriter,
33 },
34 },
35 };
36
37 impl VirtualTapeDrive {
38
39 /// This needs to lock the drive
40 pub fn open(&self) -> Result<VirtualTapeHandle, Error> {
41 let mut lock_path = std::path::PathBuf::from(&self.path);
42 lock_path.push(".drive.lck");
43
44 let timeout = std::time::Duration::new(10, 0);
45 let lock = proxmox::tools::fs::open_file_locked(&lock_path, timeout, true)?;
46
47 Ok(VirtualTapeHandle {
48 _lock: lock,
49 max_size: self.max_size.unwrap_or(64*1024*1024),
50 path: std::path::PathBuf::from(&self.path),
51 })
52 }
53 }
54
55 #[derive(Serialize,Deserialize)]
56 struct VirtualTapeStatus {
57 name: String,
58 pos: usize,
59 }
60
61 #[derive(Serialize,Deserialize)]
62 struct VirtualDriveStatus {
63 current_tape: Option<VirtualTapeStatus>,
64 }
65
66 #[derive(Serialize,Deserialize)]
67 struct TapeIndex {
68 files: usize,
69 }
70
71 pub struct VirtualTapeHandle {
72 path: std::path::PathBuf,
73 max_size: usize,
74 _lock: File,
75 }
76
77 impl VirtualTapeHandle {
78
79 pub fn insert_tape(&self, _tape_filename: &str) {
80 unimplemented!();
81 }
82
83 pub fn eject_tape(&self) {
84 unimplemented!();
85 }
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)?;
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)?;
161 Ok(())
162 }
163 }
164
165 impl TapeDriver for VirtualTapeHandle {
166
167 fn sync(&mut self) -> Result<(), Error> {
168 Ok(()) // do nothing for now
169 }
170
171 fn current_file_number(&mut self) -> Result<usize, Error> {
172 let status = self.load_status()
173 .map_err(|err| format_err!("current_file_number failed: {}", err.to_string()))?;
174
175 match status.current_tape {
176 Some(VirtualTapeStatus { pos, .. }) => { Ok(pos)},
177 None => bail!("current_file_number failed: drive is empty (no tape loaded)."),
178 }
179 }
180
181 fn read_next_file(&mut self) -> Result<Option<Box<dyn TapeRead>>, io::Error> {
182 let mut status = self.load_status()
183 .map_err(|err| io::Error::new(io::ErrorKind::Other, err.to_string()))?;
184
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 if *pos >= index.files {
192 return Ok(None); // EOM
193 }
194
195 let path = self.tape_file_path(name, *pos);
196 let file = std::fs::OpenOptions::new()
197 .read(true)
198 .open(path)?;
199
200 *pos += 1;
201 self.store_status(&status)
202 .map_err(|err| io::Error::new(io::ErrorKind::Other, err.to_string()))?;
203
204 let reader = Box::new(file);
205 let reader = Box::new(EmulateTapeReader::new(reader));
206
207 match BlockedReader::open(reader)? {
208 Some(reader) => Ok(Some(Box::new(reader))),
209 None => Ok(None),
210 }
211 }
212 None => proxmox::io_bail!("drive is empty (no tape loaded)."),
213 }
214 }
215
216 fn write_file(&mut self) -> Result<Box<dyn TapeWrite>, io::Error> {
217 let mut status = self.load_status()
218 .map_err(|err| io::Error::new(io::ErrorKind::Other, err.to_string()))?;
219
220 match status.current_tape {
221 Some(VirtualTapeStatus { ref name, ref mut pos }) => {
222
223 let mut index = self.load_tape_index(name)
224 .map_err(|err| io::Error::new(io::ErrorKind::Other, err.to_string()))?;
225
226 for i in *pos..index.files {
227 let path = self.tape_file_path(name, i);
228 let _ = std::fs::remove_file(path);
229 }
230
231 let mut used_space = 0;
232 for i in 0..*pos {
233 let path = self.tape_file_path(name, i);
234 used_space += path.metadata()?.len() as usize;
235
236 }
237 index.files = *pos + 1;
238
239 self.store_tape_index(name, &index)
240 .map_err(|err| io::Error::new(io::ErrorKind::Other, err.to_string()))?;
241
242 let path = self.tape_file_path(name, *pos);
243 let file = std::fs::OpenOptions::new()
244 .write(true)
245 .create(true)
246 .truncate(true)
247 .open(path)?;
248
249 *pos = index.files;
250
251 self.store_status(&status)
252 .map_err(|err| io::Error::new(io::ErrorKind::Other, err.to_string()))?;
253
254 let mut free_space = 0;
255 if used_space < self.max_size {
256 free_space = self.max_size - used_space;
257 }
258
259 let writer = Box::new(file);
260 let writer = Box::new(EmulateTapeWriter::new(writer, free_space));
261 let writer = Box::new(BlockedWriter::new(writer));
262
263 Ok(writer)
264 }
265 None => proxmox::io_bail!("drive is empty (no tape loaded)."),
266 }
267 }
268
269 fn move_to_eom(&mut self) -> Result<(), Error> {
270 let mut status = self.load_status()?;
271 match status.current_tape {
272 Some(VirtualTapeStatus { ref name, ref mut pos }) => {
273
274 let index = self.load_tape_index(name)
275 .map_err(|err| io::Error::new(io::ErrorKind::Other, err.to_string()))?;
276
277 *pos = index.files;
278 self.store_status(&status)
279 .map_err(|err| io::Error::new(io::ErrorKind::Other, err.to_string()))?;
280
281 Ok(())
282 }
283 None => bail!("drive is empty (no tape loaded)."),
284 }
285 }
286
287 fn rewind(&mut self) -> Result<(), Error> {
288 let mut status = self.load_status()?;
289 match status.current_tape {
290 Some(ref mut tape_status) => {
291 tape_status.pos = 0;
292 self.store_status(&status)?;
293 Ok(())
294 }
295 None => bail!("drive is empty (no tape loaded)."),
296 }
297 }
298
299 fn erase_media(&mut self, _fast: bool) -> Result<(), Error> {
300 let mut status = self.load_status()?;
301 match status.current_tape {
302 Some(VirtualTapeStatus { ref name, ref mut pos }) => {
303 *pos = self.truncate_tape(name, 0)?;
304 self.store_status(&status)?;
305 Ok(())
306 }
307 None => bail!("drive is empty (no tape loaded)."),
308 }
309 }
310
311 fn write_media_set_label(&mut self, media_set_label: &MediaSetLabel) -> Result<Uuid, Error> {
312
313 let mut status = self.load_status()?;
314 match status.current_tape {
315 Some(VirtualTapeStatus { ref name, ref mut pos }) => {
316 *pos = self.truncate_tape(name, 1)?;
317 let pos = *pos;
318 self.store_status(&status)?;
319
320 if pos == 0 {
321 bail!("media is empty (no label).");
322 }
323 if pos != 1 {
324 bail!("write_media_set_label: truncate failed - got wrong pos '{}'", pos);
325 }
326
327 let raw = serde_json::to_string_pretty(&serde_json::to_value(media_set_label)?)?;
328 let header = MediaContentHeader::new(PROXMOX_BACKUP_MEDIA_SET_LABEL_MAGIC_1_0, raw.len() as u32);
329
330 {
331 let mut writer = self.write_file()?;
332 writer.write_header(&header, raw.as_bytes())?;
333 writer.finish(false)?;
334 }
335
336 Ok(Uuid::from(header.uuid))
337 }
338 None => bail!("drive is empty (no tape loaded)."),
339 }
340 }
341
342 fn eject_media(&mut self) -> Result<(), Error> {
343 let status = VirtualDriveStatus {
344 current_tape: None,
345 };
346 self.store_status(&status)
347 }
348 }
349
350 impl MediaChange for VirtualTapeHandle {
351
352 /// Try to load media
353 ///
354 /// We automatically create an empty virtual tape here (if it does
355 /// not exist already)
356 fn load_media(&mut self, label: &str) -> Result<(), Error> {
357 let name = format!("tape-{}.json", label);
358 let mut path = self.path.clone();
359 path.push(&name);
360 if !path.exists() {
361 eprintln!("unable to find tape {} - creating file {:?}", label, path);
362 let index = TapeIndex { files: 0 };
363 self.store_tape_index(label, &index)?;
364 }
365
366 let status = VirtualDriveStatus {
367 current_tape: Some(VirtualTapeStatus {
368 name: label.to_string(),
369 pos: 0,
370 }),
371 };
372 self.store_status(&status)
373 }
374
375 fn unload_media(&mut self) -> Result<(), Error> {
376 self.eject_media()?;
377 Ok(())
378 }
379
380 fn eject_on_unload(&self) -> bool {
381 true
382 }
383
384 fn list_media_changer_ids(&self) -> Result<Vec<String>, Error> {
385 let mut list = Vec::new();
386 for entry in std::fs::read_dir(&self.path)? {
387 let entry = entry?;
388 let path = entry.path();
389 if path.is_file() && path.extension() == Some(std::ffi::OsStr::new("json")) {
390 if let Some(name) = path.file_stem() {
391 if let Some(name) = name.to_str() {
392 if name.starts_with("tape-") {
393 list.push(name[5..].to_string());
394 }
395 }
396 }
397 }
398 }
399 Ok(list)
400 }
401 }
402
403 impl MediaChange for VirtualTapeDrive {
404
405 fn load_media(&mut self, changer_id: &str) -> Result<(), Error> {
406 let mut handle = self.open()?;
407 handle.load_media(changer_id)
408 }
409
410 fn unload_media(&mut self) -> Result<(), Error> {
411 let mut handle = self.open()?;
412 handle.eject_media()?;
413 Ok(())
414 }
415
416 fn eject_on_unload(&self) -> bool {
417 true
418 }
419
420 fn list_media_changer_ids(&self) -> Result<Vec<String>, Error> {
421 let handle = self.open()?;
422 handle.list_media_changer_ids()
423 }
424 }