1 use std
::collections
::HashSet
;
3 use std
::time
::SystemTime
;
5 use anyhow
::{bail, Error}
;
7 use proxmox
::tools
::Uuid
;
17 MAX_CHUNK_ARCHIVE_SIZE
,
21 SnapshotChunkIterator
,
29 tape_write_snapshot_archive
,
33 request_and_load_media
,
34 tape_alert_flags_critical
,
38 config
::tape_encryption_keys
::load_key_configs
,
42 struct PoolWriterState
{
43 drive
: Box
<dyn TapeDriver
>,
44 catalog
: MediaCatalog
,
45 // tell if we already moved to EOM
47 // bytes written after the last tape fush/sync
51 impl PoolWriterState
{
53 fn commit(&mut self) -> Result
<(), Error
> {
54 self.drive
.sync()?
; // sync all data to the tape
55 self.catalog
.commit()?
; // then commit the catalog
56 self.bytes_written
= 0;
61 /// Helper to manage a backup job, writing several tapes of a pool
62 pub struct PoolWriter
{
65 status
: Option
<PoolWriterState
>,
66 media_set_catalog
: MediaSetCatalog
,
71 pub fn new(mut pool
: MediaPool
, drive_name
: &str, worker
: &WorkerTask
) -> Result
<Self, Error
> {
73 let current_time
= proxmox
::tools
::time
::epoch_i64();
75 let new_media_set_reason
= pool
.start_write_session(current_time
)?
;
76 if let Some(reason
) = new_media_set_reason
{
79 "starting new media set - reason: {}",
84 task_log
!(worker
, "media set uuid: {}", pool
.current_media_set());
86 let mut media_set_catalog
= MediaSetCatalog
::new();
88 // load all catalogs read-only at start
89 for media_uuid
in pool
.current_media_list()?
{
90 let media_catalog
= MediaCatalog
::open(
91 Path
::new(TAPE_STATUS_DIR
),
96 media_set_catalog
.append_catalog(media_catalog
)?
;
101 drive_name
: drive_name
.to_string(),
107 pub fn pool(&mut self) -> &mut MediaPool
{
111 /// Set media status to FULL (persistent - stores pool status)
112 pub fn set_media_status_full(&mut self, uuid
: &Uuid
) -> Result
<(), Error
> {
113 self.pool
.set_media_status_full(&uuid
)?
;
117 pub fn contains_snapshot(&self, snapshot
: &str) -> bool
{
118 if let Some(PoolWriterState { ref catalog, .. }
) = self.status
{
119 if catalog
.contains_snapshot(snapshot
) {
123 self.media_set_catalog
.contains_snapshot(snapshot
)
126 /// Eject media and drop PoolWriterState (close drive)
127 pub fn eject_media(&mut self, worker
: &WorkerTask
) -> Result
<(), Error
> {
128 let mut status
= match self.status
.take() {
129 Some(status
) => status
,
130 None
=> return Ok(()), // no media loaded
133 let (drive_config
, _digest
) = crate::config
::drive
::config()?
;
135 if let Some((mut changer
, _
)) = media_changer(&drive_config
, &self.drive_name
)?
{
136 worker
.log("eject media");
137 status
.drive
.eject_media()?
; // rewind and eject early, so that unload_media is faster
138 drop(status
); // close drive
139 worker
.log("unload media");
140 changer
.unload_media(None
)?
; //eject and unload
142 worker
.log("standalone drive - ejecting media");
143 status
.drive
.eject_media()?
;
149 /// Export current media set and drop PoolWriterState (close drive)
150 pub fn export_media_set(&mut self, worker
: &WorkerTask
) -> Result
<(), Error
> {
151 let mut status
= self.status
.take();
153 let (drive_config
, _digest
) = crate::config
::drive
::config()?
;
155 if let Some((mut changer
, _
)) = media_changer(&drive_config
, &self.drive_name
)?
{
157 if let Some(ref mut status
) = status
{
158 worker
.log("eject media");
159 status
.drive
.eject_media()?
; // rewind and eject early, so that unload_media is faster
161 drop(status
); // close drive
163 worker
.log("unload media");
164 changer
.unload_media(None
)?
;
166 for media_uuid
in self.pool
.current_media_list()?
{
167 let media
= self.pool
.lookup_media(media_uuid
)?
;
168 let label_text
= media
.label_text();
169 if let Some(slot
) = changer
.export_media(label_text
)?
{
170 worker
.log(format
!("exported media '{}' to import/export slot {}", label_text
, slot
));
172 worker
.warn(format
!("export failed - media '{}' is not online", label_text
));
176 } else if let Some(mut status
) = status
{
177 worker
.log("standalone drive - ejecting media instead of export");
178 status
.drive
.eject_media()?
;
184 /// commit changes to tape and catalog
186 /// This is done automatically during a backupsession, but needs to
187 /// be called explicitly before dropping the PoolWriter
188 pub fn commit(&mut self) -> Result
<(), Error
> {
189 if let Some(ref mut status
) = self.status
{
195 /// Load a writable media into the drive
196 pub fn load_writable_media(&mut self, worker
: &WorkerTask
) -> Result
<Uuid
, Error
> {
197 let last_media_uuid
= match self.status
{
198 Some(PoolWriterState { ref catalog, .. }
) => Some(catalog
.uuid().clone()),
202 let current_time
= proxmox
::tools
::time
::epoch_i64();
203 let media_uuid
= self.pool
.alloc_writable_media(current_time
)?
;
205 let media
= self.pool
.lookup_media(&media_uuid
).unwrap();
207 let media_changed
= match last_media_uuid
{
208 Some(ref last_media_uuid
) => last_media_uuid
!= &media_uuid
,
213 return Ok(media_uuid
);
216 task_log
!(worker
, "allocated new writable media '{}'", media
.label_text());
218 // remove read-only catalog (we store a writable version in status)
219 self.media_set_catalog
.remove_catalog(&media_uuid
);
221 if let Some(PoolWriterState {mut drive, catalog, .. }
) = self.status
.take() {
222 self.media_set_catalog
.append_catalog(catalog
)?
;
223 task_log
!(worker
, "eject current media");
224 drive
.eject_media()?
;
227 let (drive_config
, _digest
) = crate::config
::drive
::config()?
;
229 let (mut drive
, old_media_id
) =
230 request_and_load_media(worker
, &drive_config
, &self.drive_name
, media
.label())?
;
232 // test for critical tape alert flags
233 if let Ok(alert_flags
) = drive
.tape_alert_flags() {
234 if !alert_flags
.is_empty() {
235 worker
.log(format
!("TapeAlertFlags: {:?}", alert_flags
));
236 if tape_alert_flags_critical(alert_flags
) {
237 self.pool
.set_media_status_damaged(&media_uuid
)?
;
238 bail
!("aborting due to critical tape alert flags: {:?}", alert_flags
);
243 let catalog
= update_media_set_label(
246 old_media_id
.media_set_label
,
250 let media_set
= media
.media_set_label().clone().unwrap();
252 let encrypt_fingerprint
= media_set
253 .encryption_key_fingerprint
255 .map(|fp
| (fp
, media_set
.uuid
.clone()));
257 drive
.set_encryption(encrypt_fingerprint
)?
;
259 self.status
= Some(PoolWriterState { drive, catalog, at_eom: false, bytes_written: 0 }
);
264 /// uuid of currently loaded BackupMedia
265 pub fn current_media_uuid(&self) -> Result
<&Uuid
, Error
> {
267 Some(PoolWriterState { ref catalog, ..}
) => Ok(catalog
.uuid()),
268 None
=> bail
!("PoolWriter - no media loaded"),
272 /// Move to EOM (if not aleady there), then creates a new snapshot
273 /// archive writing specified files (as .pxar) into it. On
274 /// success, this return 'Ok(true)' and the media catalog gets
277 /// Please note that this may fail when there is not enough space
278 /// on the media (return value 'Ok(false, _)'). In that case, the
279 /// archive is marked incomplete, and we do not use it. The caller
280 /// should mark the media as full and try again using another
282 pub fn append_snapshot_archive(
285 snapshot_reader
: &SnapshotReader
,
286 ) -> Result
<(bool
, usize), Error
> {
288 let status
= match self.status
{
289 Some(ref mut status
) => status
,
290 None
=> bail
!("PoolWriter - no media loaded"),
294 worker
.log(String
::from("moving to end of media"));
295 status
.drive
.move_to_eom()?
;
296 status
.at_eom
= true;
299 let current_file_number
= status
.drive
.current_file_number()?
;
300 if current_file_number
< 2 {
301 bail
!("got strange file position number from drive ({})", current_file_number
);
304 let (done
, bytes_written
) = {
305 let mut writer
: Box
<dyn TapeWrite
> = status
.drive
.write_file()?
;
307 match tape_write_snapshot_archive(writer
.as_mut(), snapshot_reader
)?
{
308 Some(content_uuid
) => {
309 status
.catalog
.register_snapshot(
312 &snapshot_reader
.snapshot().to_string(),
314 (true, writer
.bytes_written())
316 None
=> (false, writer
.bytes_written()),
320 status
.bytes_written
+= bytes_written
;
322 let request_sync
= status
.bytes_written
>= COMMIT_BLOCK_SIZE
;
324 if !done
|| request_sync
{
328 Ok((done
, bytes_written
))
331 /// Move to EOM (if not aleady there), then creates a new chunk
332 /// archive and writes chunks from 'chunk_iter'. This stops when
333 /// it detect LEOM or when we reach max archive size
334 /// (4GB). Written chunks are registered in the media catalog.
335 pub fn append_chunk_archive(
338 datastore
: &DataStore
,
339 chunk_iter
: &mut std
::iter
::Peekable
<SnapshotChunkIterator
>,
340 ) -> Result
<(bool
, usize), Error
> {
342 let status
= match self.status
{
343 Some(ref mut status
) => status
,
344 None
=> bail
!("PoolWriter - no media loaded"),
348 worker
.log(String
::from("moving to end of media"));
349 status
.drive
.move_to_eom()?
;
350 status
.at_eom
= true;
353 let current_file_number
= status
.drive
.current_file_number()?
;
354 if current_file_number
< 2 {
355 bail
!("got strange file position number from drive ({})", current_file_number
);
357 let writer
= status
.drive
.write_file()?
;
359 let start_time
= SystemTime
::now();
361 let (saved_chunks
, content_uuid
, leom
, bytes_written
) = write_chunk_archive(
366 &self.media_set_catalog
,
368 MAX_CHUNK_ARCHIVE_SIZE
,
371 status
.bytes_written
+= bytes_written
;
373 let elapsed
= start_time
.elapsed()?
.as_secs_f64();
375 "wrote {:.2} MB ({} MB/s)",
376 bytes_written
as f64 / (1024.0*1024.0),
377 (bytes_written
as f64)/(1024.0*1024.0*elapsed
),
380 let request_sync
= status
.bytes_written
>= COMMIT_BLOCK_SIZE
;
382 // register chunks in media_catalog
383 status
.catalog
.start_chunk_archive(content_uuid
, current_file_number
)?
;
384 for digest
in saved_chunks
{
385 status
.catalog
.register_chunk(&digest
)?
;
387 status
.catalog
.end_chunk_archive()?
;
389 if leom
|| request_sync
{
393 Ok((leom
, bytes_written
))
397 /// write up to <max_size> of chunks
398 fn write_chunk_archive
<'a
>(
400 writer
: Box
<dyn 'a
+ TapeWrite
>,
401 datastore
: &DataStore
,
402 chunk_iter
: &mut std
::iter
::Peekable
<SnapshotChunkIterator
>,
403 media_set_catalog
: &MediaSetCatalog
,
404 media_catalog
: &MediaCatalog
,
406 ) -> Result
<(Vec
<[u8;32]>, Uuid
, bool
, usize), Error
> {
408 let (mut writer
, content_uuid
) = ChunkArchiveWriter
::new(writer
, true)?
;
410 let mut chunk_index
: HashSet
<[u8;32]> = HashSet
::new();
412 // we want to get the chunk list in correct order
413 let mut chunk_list
: Vec
<[u8;32]> = Vec
::new();
415 let mut leom
= false;
418 let digest
= match chunk_iter
.next() {
420 Some(digest
) => digest?
,
422 if media_catalog
.contains_chunk(&digest
)
423 || chunk_index
.contains(&digest
)
424 || media_set_catalog
.contains_chunk(&digest
)
429 let blob
= datastore
.load_chunk(&digest
)?
;
430 //println!("CHUNK {} size {}", proxmox::tools::digest_to_hex(&digest), blob.raw_size());
432 match writer
.try_write_chunk(&digest
, &blob
) {
434 chunk_index
.insert(digest
);
435 chunk_list
.push(digest
);
441 Err(err
) => bail
!("write chunk failed - {}", err
),
444 if writer
.bytes_written() > max_size
{
445 worker
.log("Chunk Archive max size reached, closing archive".to_string());
452 Ok((chunk_list
, content_uuid
, leom
, writer
.bytes_written()))
455 // Compare the media set label. If the media is empty, or the existing
456 // set label does not match the expected media set, overwrite the
458 fn update_media_set_label(
460 drive
: &mut dyn TapeDriver
,
461 old_set
: Option
<MediaSetLabel
>,
463 ) -> Result
<MediaCatalog
, Error
> {
467 let new_set
= match media_id
.media_set_label
{
468 None
=> bail
!("got media without media set - internal error"),
469 Some(ref set
) => set
,
472 let key_config
= if let Some(ref fingerprint
) = new_set
.encryption_key_fingerprint
{
473 let (config_map
, _digest
) = load_key_configs()?
;
474 match config_map
.get(fingerprint
) {
475 Some(key_config
) => Some(key_config
.clone()),
477 bail
!("unable to find tape encryption key config '{}'", fingerprint
);
484 let status_path
= Path
::new(TAPE_STATUS_DIR
);
488 worker
.log("wrinting new media set label".to_string());
489 drive
.write_media_set_label(new_set
, key_config
.as_ref())?
;
490 media_catalog
= MediaCatalog
::overwrite(status_path
, media_id
, false)?
;
492 Some(media_set_label
) => {
493 if new_set
.uuid
== media_set_label
.uuid
{
494 if new_set
.seq_nr
!= media_set_label
.seq_nr
{
495 bail
!("got media with wrong media sequence number ({} != {}",
496 new_set
.seq_nr
,media_set_label
.seq_nr
);
498 if new_set
.encryption_key_fingerprint
!= media_set_label
.encryption_key_fingerprint
{
499 bail
!("detected changed encryption fingerprint - internal error");
501 media_catalog
= MediaCatalog
::open(status_path
, &media_id
.label
.uuid
, true, false)?
;
504 format
!("wrinting new media set label (overwrite '{}/{}')",
505 media_set_label
.uuid
.to_string(), media_set_label
.seq_nr
)
508 drive
.write_media_set_label(new_set
, key_config
.as_ref())?
;
509 media_catalog
= MediaCatalog
::overwrite(status_path
, media_id
, false)?
;
514 // todo: verify last content/media_catalog somehow?
515 drive
.move_to_eom()?
; // just to be sure