diff --git a/src/api2/tape/drive.rs b/src/api2/tape/drive.rs index 7ab4f484..12c79dd6 100644 --- a/src/api2/tape/drive.rs +++ b/src/api2/tape/drive.rs @@ -406,7 +406,7 @@ fn write_media_label( if let Some(ref pool) = pool { // assign media to pool by writing special media set label worker.log(format!("Label media '{}' for pool '{}'", label.label_text, pool)); - let set = MediaSetLabel::with_data(&pool, [0u8; 16].into(), 0, label.ctime); + let set = MediaSetLabel::with_data(&pool, [0u8; 16].into(), 0, label.ctime, None); drive.write_media_set_label(&set)?; media_set_label = Some(set); @@ -487,12 +487,13 @@ pub async fn read_label( let media_id = match media_id { Some(media_id) => { - let mut flat = MediaIdFlat { + let mut flat = MediaIdFlat { uuid: media_id.label.uuid.to_string(), label_text: media_id.label.label_text.clone(), ctime: media_id.label.ctime, media_set_ctime: None, media_set_uuid: None, + encryption_key_fingerprint: None, pool: None, seq_nr: None, }; @@ -501,6 +502,10 @@ pub async fn read_label( flat.seq_nr = Some(set.seq_nr); flat.media_set_uuid = Some(set.uuid.to_string()); flat.media_set_ctime = Some(set.ctime); + flat.encryption_key_fingerprint = set + .encryption_key_fingerprint + .as_ref() + .map(|fp| crate::tools::format::as_fingerprint(fp.bytes())); } if let Some(true) = inventorize { @@ -992,6 +997,9 @@ pub fn catalog_media( MediaCatalog::destroy(status_path, &media_id.label.uuid)?; return Ok(()); } + let encrypt_fingerprint = set.encryption_key_fingerprint.clone(); + drive.set_encryption(encrypt_fingerprint)?; + set.pool.clone() } }; diff --git a/src/api2/types/tape/media.rs b/src/api2/types/tape/media.rs index 3318e3a6..62f1a2b1 100644 --- a/src/api2/types/tape/media.rs +++ b/src/api2/types/tape/media.rs @@ -74,6 +74,9 @@ pub struct MediaIdFlat { /// MediaSet Creation time stamp #[serde(skip_serializing_if="Option::is_none")] pub media_set_ctime: Option, + /// Encryption key fingerprint + #[serde(skip_serializing_if="Option::is_none")] + pub encryption_key_fingerprint: Option, } #[api()] diff --git a/src/bin/proxmox-tape.rs b/src/bin/proxmox-tape.rs index 3ac1337e..09dd25b1 100644 --- a/src/bin/proxmox-tape.rs +++ b/src/bin/proxmox-tape.rs @@ -422,6 +422,7 @@ async fn read_label( .column(ColumnConfig::new("pool")) .column(ColumnConfig::new("media-set-uuid")) .column(ColumnConfig::new("media-set-ctime").renderer(render_epoch)) + .column(ColumnConfig::new("encryption-key-fingerprint")) ; format_and_print_result_full(&mut data, &info.returns, &output_format, &options); diff --git a/src/bin/sg-tape-cmd.rs b/src/bin/sg-tape-cmd.rs index 4ced4194..d63f595f 100644 --- a/src/bin/sg-tape-cmd.rs +++ b/src/bin/sg-tape-cmd.rs @@ -8,7 +8,7 @@ use std::fs::File; use std::os::unix::io::{AsRawFd, FromRawFd}; -use anyhow::{bail, format_err, Error}; +use anyhow::{bail, Error}; use serde_json::Value; use proxmox::{ diff --git a/src/tape/file_formats.rs b/src/tape/file_formats.rs index 38a86dab..4391ebca 100644 --- a/src/tape/file_formats.rs +++ b/src/tape/file_formats.rs @@ -7,6 +7,8 @@ use bitflags::bitflags; use proxmox::tools::Uuid; +use crate::backup::Fingerprint; + /// We use 256KB blocksize (always) pub const PROXMOX_TAPE_BLOCK_SIZE: usize = 256*1024; @@ -185,16 +187,26 @@ pub struct MediaSetLabel { pub seq_nr: u64, /// Creation time stamp pub ctime: i64, + /// Encryption key finkerprint (if encryped) + #[serde(skip_serializing_if="Option::is_none")] + pub encryption_key_fingerprint: Option, } impl MediaSetLabel { - pub fn with_data(pool: &str, uuid: Uuid, seq_nr: u64, ctime: i64) -> Self { + pub fn with_data( + pool: &str, + uuid: Uuid, + seq_nr: u64, + ctime: i64, + encryption_key_fingerprint: Option, + ) -> Self { Self { pool: pool.to_string(), uuid, seq_nr, ctime, + encryption_key_fingerprint, } } } diff --git a/src/tape/inventory.rs b/src/tape/inventory.rs index fce27f83..21b72e0a 100644 --- a/src/tape/inventory.rs +++ b/src/tape/inventory.rs @@ -565,7 +565,7 @@ impl Inventory { let uuid = label.uuid.clone(); - let set = MediaSetLabel::with_data(pool, [0u8; 16].into(), 0, ctime); + let set = MediaSetLabel::with_data(pool, [0u8; 16].into(), 0, ctime, None); self.store(MediaId { label, media_set_label: Some(set) }, false).unwrap(); diff --git a/src/tape/media_pool.rs b/src/tape/media_pool.rs index c888eda4..08e2a16b 100644 --- a/src/tape/media_pool.rs +++ b/src/tape/media_pool.rs @@ -14,6 +14,7 @@ use ::serde::{Deserialize, Serialize}; use proxmox::tools::Uuid; use crate::{ + backup::Fingerprint, api2::types::{ MediaStatus, MediaLocation, @@ -44,6 +45,7 @@ pub struct MediaPool { media_set_policy: MediaSetPolicy, retention: RetentionPolicy, use_offline_media: bool, + encrypt_fingerprint: Option, inventory: Inventory, @@ -59,6 +61,7 @@ impl MediaPool { media_set_policy: MediaSetPolicy, retention: RetentionPolicy, use_offline_media: bool, + encrypt_fingerprint: Option, ) -> Result { let inventory = Inventory::load(state_path)?; @@ -75,6 +78,7 @@ impl MediaPool { use_offline_media, inventory, current_media_set, + encrypt_fingerprint, }) } @@ -89,7 +93,19 @@ impl MediaPool { let retention = config.retention.clone().unwrap_or(String::from("keep")).parse()?; - MediaPool::new(&config.name, state_path, allocation, retention, use_offline_media) + let encrypt_fingerprint = match config.encrypt { + Some(ref fingerprint) => Some(fingerprint.parse()?), + None => None, + }; + + MediaPool::new( + &config.name, + state_path, + allocation, + retention, + use_offline_media, + encrypt_fingerprint, + ) } /// Returns the pool name @@ -97,6 +113,12 @@ impl MediaPool { &self.name } + /// Retruns encryption settings + pub fn encrypt_fingerprint(&self) -> Option { + self.encrypt_fingerprint.clone() + } + + fn compute_media_state(&self, media_id: &MediaId) -> (MediaStatus, MediaLocation) { let (status, location) = self.inventory.status_and_location(&media_id.label.uuid); @@ -247,13 +269,49 @@ impl MediaPool { current_time > expire_time } + // check if a location is considered on site + pub fn location_is_available(&self, location: &MediaLocation) -> bool { + match location { + MediaLocation::Online(_) => true, + MediaLocation::Offline => self.use_offline_media, + MediaLocation::Vault(_) => false, + } + } + + fn add_media_to_current_set(&mut self, mut media_id: MediaId, current_time: i64) -> Result<(), Error> { + + let seq_nr = self.current_media_set.media_list().len() as u64; + + let pool = self.name.clone(); + + let encrypt_fingerprint = self.encrypt_fingerprint(); + + let set = MediaSetLabel::with_data( + &pool, + self.current_media_set.uuid().clone(), + seq_nr, + current_time, + encrypt_fingerprint, + ); + + media_id.media_set_label = Some(set); + + let uuid = media_id.label.uuid.clone(); + + let clear_media_status = true; // remove Full status + self.inventory.store(media_id, clear_media_status)?; // store persistently + + self.current_media_set.add_media(uuid); + + Ok(()) + } + + /// Allocates a writable media to the current media set pub fn alloc_writable_media(&mut self, current_time: i64) -> Result { let last_is_writable = self.current_set_usable()?; - let pool = self.name.clone(); - if last_is_writable { let last_uuid = self.current_media_set.last_media_uuid().unwrap(); let media = self.lookup_media(last_uuid)?; @@ -262,88 +320,65 @@ impl MediaPool { // try to find empty media in pool, add to media set - let mut media_list = self.list_media(); + let media_list = self.list_media(); let mut empty_media = Vec::new(); - for media in media_list.iter_mut() { - // already part of a media set? - if media.media_set_label().is_some() { continue; } + let mut used_media = Vec::new(); - // check if media is on site - match media.location() { - MediaLocation::Online(_) => { /* OK */ }, - MediaLocation::Offline => { - if self.use_offline_media { - /* OK */ - } else { - continue; - } - }, - MediaLocation::Vault(_) => continue, + for media in media_list.into_iter() { + if !self.location_is_available(media.location()) { + continue; + } + // already part of a media set? + if media.media_set_label().is_some() { + used_media.push(media); + } else { + // only consider writable empty media + if media.status() == &MediaStatus::Writable { + empty_media.push(media); + } } - - // only consider writable media - if media.status() != &MediaStatus::Writable { continue; } - - empty_media.push(media); } - // sort empty_media, oldest media first - empty_media.sort_unstable_by_key(|media| media.label().ctime); + // sort empty_media, newest first -> oldest last + empty_media.sort_unstable_by(|a, b| b.label().ctime.cmp(&a.label().ctime)); - if let Some(media) = empty_media.first_mut() { + if let Some(media) = empty_media.pop() { // found empty media, add to media set an use it - let seq_nr = self.current_media_set.media_list().len() as u64; - - let set = MediaSetLabel::with_data(&pool, self.current_media_set.uuid().clone(), seq_nr, current_time); - - media.set_media_set_label(set); - - self.inventory.store(media.id().clone(), true)?; // store persistently - - self.current_media_set.add_media(media.uuid().clone()); - - return Ok(media.uuid().clone()); + let uuid = media.uuid().clone(); + self.add_media_to_current_set(media.into_id(), current_time)?; + return Ok(uuid); } println!("no empty media in pool, try to reuse expired media"); let mut expired_media = Vec::new(); - for media in media_list.into_iter() { + for media in used_media.into_iter() { if let Some(set) = media.media_set_label() { if &set.uuid == self.current_media_set.uuid() { continue; } + } else { + continue; } + if self.media_is_expired(&media, current_time) { println!("found expired media on media '{}'", media.label_text()); expired_media.push(media); } } - // sort, oldest media first - expired_media.sort_unstable_by_key(|media| { - match media.media_set_label() { - None => 0, // should not happen here - Some(set) => set.ctime, - } + // sort expired_media, newest first -> oldest last + expired_media.sort_unstable_by(|a, b| { + b.media_set_label().unwrap().ctime.cmp(&a.media_set_label().unwrap().ctime) }); - if let Some(media) = expired_media.first_mut() { + if let Some(media) = expired_media.pop() { println!("reuse expired media '{}'", media.label_text()); - - let seq_nr = self.current_media_set.media_list().len() as u64; - let set = MediaSetLabel::with_data(&pool, self.current_media_set.uuid().clone(), seq_nr, current_time); - - media.set_media_set_label(set); - - let clear_media_status = true; // remove Full status - self.inventory.store(media.id().clone(), clear_media_status)?; // store persistently - - self.current_media_set.add_media(media.uuid().clone()); - - return Ok(media.uuid().clone()); + let uuid = media.uuid().clone(); + self.add_media_to_current_set(media.into_id(), current_time)?; + return Ok(uuid); } println!("no expired media in pool, try to find unassigned/free media"); @@ -357,17 +392,8 @@ impl MediaPool { let (status, location) = self.compute_media_state(&media_id); if media_id.media_set_label.is_some() { continue; } // should not happen - // check if media is on site - match location { - MediaLocation::Online(_) => { /* OK */ }, - MediaLocation::Offline => { - if self.use_offline_media { - /* OK */ - } else { - continue; - } - }, - MediaLocation::Vault(_) => continue, + if !self.location_is_available(&location) { + continue; } // only consider writable media @@ -376,23 +402,13 @@ impl MediaPool { free_media.push(media_id); } - if let Some(media) = free_media.first_mut() { - println!("use free media '{}'", media.label.label_text); - - let seq_nr = self.current_media_set.media_list().len() as u64; - let set = MediaSetLabel::with_data(&pool, self.current_media_set.uuid().clone(), seq_nr, current_time); - - media.media_set_label = Some(set); - - let clear_media_status = true; // remove Full status - self.inventory.store(media.clone(), clear_media_status)?; // store persistently - - self.current_media_set.add_media(media.label.uuid.clone()); - - return Ok(media.label.uuid.clone()); + if let Some(media_id) = free_media.pop() { + println!("use free media '{}'", media_id.label.label_text); + let uuid = media_id.label.uuid.clone(); + self.add_media_to_current_set(media_id, current_time)?; + return Ok(uuid); } - bail!("alloc writable media in pool '{}' failed: no usable media found", self.name()); } @@ -537,6 +553,11 @@ impl BackupMedia { &self.id } + /// Returns the media id, consumes self) + pub fn into_id(self) -> MediaId { + self.id + } + /// Returns the media label (Barcode) pub fn label_text(&self) -> &str { &self.id.label.label_text diff --git a/src/tape/pool_writer.rs b/src/tape/pool_writer.rs index 95574499..f017497e 100644 --- a/src/tape/pool_writer.rs +++ b/src/tape/pool_writer.rs @@ -230,6 +230,15 @@ impl PoolWriter { media.id(), )?; + let encrypt_fingerprint = media + .media_set_label() + .as_ref() + .unwrap() + .encryption_key_fingerprint + .clone(); + + drive.set_encryption(encrypt_fingerprint)?; + self.status = Some(PoolWriterState { drive, catalog, at_eom: false, bytes_written: 0 }); Ok(media_uuid) @@ -457,6 +466,9 @@ fn update_media_set_label( bail!("got media with wrong media sequence number ({} != {}", new_set.seq_nr,media_set_label.seq_nr); } + if new_set.encryption_key_fingerprint != media_set_label.encryption_key_fingerprint { + bail!("detected changed encryption fingerprint - internal error"); + } media_catalog = MediaCatalog::open(status_path, &media_id.label.uuid, true, false)?; } else { worker.log(