1 //! Backup media Inventory
3 //! The Inventory persistently stores the list of known backup
4 //! media. A backup media is identified by its 'MediaId', which is the
5 //! DriveLabel/MediaSetLabel combination.
7 use std
::collections
::{HashMap, BTreeMap}
;
8 use std
::path
::{Path, PathBuf}
;
10 use anyhow
::{bail, Error}
;
11 use serde
::{Serialize, Deserialize}
;
25 tools
::systemd
::time
::compute_next_event
,
40 /// Unique Media Identifier
42 /// This combines the label and media set label.
43 #[derive(Debug,Serialize,Deserialize,Clone)]
45 pub label
: DriveLabel
,
46 #[serde(skip_serializing_if="Option::is_none")]
47 pub media_set_label
: Option
<MediaSetLabel
>,
50 impl From
<MediaLabelInfo
> for MediaId
{
51 fn from(info
: MediaLabelInfo
) -> Self {
53 label
: info
.label
.clone(),
54 media_set_label
: info
.media_set_label
.map(|(l
, _
)| l
),
62 /// A List of backup media
63 #[derive(Debug, Serialize, Deserialize)]
65 /// Unique media set ID
67 /// List of BackupMedia
68 media_list
: Vec
<Option
<Uuid
>>,
73 pub const MEDIA_SET_MAX_SEQ_NR
: u64 = 100;
75 pub fn new() -> Self {
76 let uuid
= Uuid
::generate();
79 media_list
: Vec
::new(),
83 pub fn with_data(uuid
: Uuid
, media_list
: Vec
<Option
<Uuid
>>) -> Self {
84 Self { uuid, media_list }
87 pub fn uuid(&self) -> &Uuid
{
91 pub fn media_list(&self) -> &[Option
<Uuid
>] {
95 pub fn add_media(&mut self, uuid
: Uuid
) {
96 self.media_list
.push(Some(uuid
));
99 pub fn insert_media(&mut self, uuid
: Uuid
, seq_nr
: u64) -> Result
<(), Error
> {
100 if seq_nr
> Self::MEDIA_SET_MAX_SEQ_NR
{
101 bail
!("media set sequence number to large in media set {} ({} > {})",
102 self.uuid
.to_string(), seq_nr
, Self::MEDIA_SET_MAX_SEQ_NR
);
104 let seq_nr
= seq_nr
as usize;
105 if self.media_list
.len() > seq_nr
{
106 if self.media_list
[seq_nr
].is_some() {
107 bail
!("found duplicate squence number in media set '{}/{}'",
108 self.uuid
.to_string(), seq_nr
);
111 self.media_list
.resize(seq_nr
+ 1, None
);
113 self.media_list
[seq_nr
] = Some(uuid
);
117 pub fn last_media_uuid(&self) -> Option
<&Uuid
> {
118 match self.media_list
.last() {
121 Some(Some(ref last_uuid
)) => Some(last_uuid
),
125 pub fn is_last_media(&self, uuid
: &Uuid
) -> bool
{
126 match self.media_list
.last() {
129 Some(Some(last_uuid
)) => uuid
== last_uuid
,
135 pub struct Inventory
{
136 map
: BTreeMap
<Uuid
, MediaId
>,
138 inventory_path
: PathBuf
,
139 lockfile_path
: PathBuf
,
142 media_set_start_times
: HashMap
<Uuid
, i64>
147 pub const MEDIA_INVENTORY_FILENAME
: &'
static str = "inventory.json";
148 pub const MEDIA_INVENTORY_LOCKFILE
: &'
static str = ".inventory.lck";
150 fn new(base_path
: &Path
) -> Self {
152 let mut inventory_path
= base_path
.to_owned();
153 inventory_path
.push(Self::MEDIA_INVENTORY_FILENAME
);
155 let mut lockfile_path
= base_path
.to_owned();
156 lockfile_path
.push(Self::MEDIA_INVENTORY_LOCKFILE
);
159 map
: BTreeMap
::new(),
160 media_set_start_times
: HashMap
::new(),
166 pub fn load(base_path
: &Path
) -> Result
<Self, Error
> {
167 let mut me
= Self::new(base_path
);
172 /// Reload the database
173 pub fn reload(&mut self) -> Result
<(), Error
> {
174 self.map
= Self::load_media_db(&self.inventory_path
)?
;
175 self.update_helpers();
179 fn update_helpers(&mut self) {
181 // recompute media_set_start_times
183 let mut set_start_times
= HashMap
::new();
185 for media
in self.map
.values() {
186 let set
= match &media
.media_set_label
{
191 set_start_times
.insert(set
.uuid
.clone(), set
.ctime
);
195 self.media_set_start_times
= set_start_times
;
198 /// Lock the database
199 pub fn lock(&self) -> Result
<std
::fs
::File
, Error
> {
200 open_file_locked(&self.lockfile_path
, std
::time
::Duration
::new(10, 0), true)
203 fn load_media_db(path
: &Path
) -> Result
<BTreeMap
<Uuid
, MediaId
>, Error
> {
205 let data
= file_get_json(path
, Some(json
!([])))?
;
206 let media_list
: Vec
<MediaId
> = serde_json
::from_value(data
)?
;
208 let mut map
= BTreeMap
::new();
209 for item
in media_list
.into_iter() {
210 map
.insert(item
.label
.uuid
.clone(), item
);
216 fn replace_file(&self) -> Result
<(), Error
> {
217 let list
: Vec
<&MediaId
> = self.map
.values().collect();
218 let raw
= serde_json
::to_string_pretty(&serde_json
::to_value(list
)?
)?
;
220 let backup_user
= crate::backup
::backup_user()?
;
221 let mode
= nix
::sys
::stat
::Mode
::from_bits_truncate(0o0640);
222 let options
= CreateOptions
::new()
224 .owner(backup_user
.uid
)
225 .group(backup_user
.gid
);
227 replace_file(&self.inventory_path
, raw
.as_bytes(), options
)?
;
232 /// Stores a single MediaID persistently
233 pub fn store(&mut self, mut media_id
: MediaId
) -> Result
<(), Error
> {
234 let _lock
= self.lock()?
;
235 self.map
= Self::load_media_db(&self.inventory_path
)?
;
237 // do not overwrite unsaved pool assignments
238 if media_id
.media_set_label
.is_none() {
239 if let Some(previous
) = self.map
.get(&media_id
.label
.uuid
) {
240 if let Some(ref set
) = previous
.media_set_label
{
241 if set
.uuid
.as_ref() == [0u8;16] {
242 media_id
.media_set_label
= Some(set
.clone());
248 self.map
.insert(media_id
.label
.uuid
.clone(), media_id
);
249 self.update_helpers();
250 self.replace_file()?
;
254 /// Remove a single media persistently
255 pub fn remove_media(&mut self, uuid
: &Uuid
) -> Result
<(), Error
> {
256 let _lock
= self.lock()?
;
257 self.map
= Self::load_media_db(&self.inventory_path
)?
;
258 self.map
.remove(uuid
);
259 self.update_helpers();
260 self.replace_file()?
;
265 pub fn lookup_media(&self, uuid
: &Uuid
) -> Option
<&MediaId
> {
269 /// find media by changer_id
270 pub fn find_media_by_changer_id(&self, changer_id
: &str) -> Option
<&MediaId
> {
271 for (_uuid
, media_id
) in &self.map
{
272 if media_id
.label
.changer_id
== changer_id
{
273 return Some(media_id
);
279 /// Lookup media pool
281 /// Returns (pool, is_empty)
282 pub fn lookup_media_pool(&self, uuid
: &Uuid
) -> Option
<(&str, bool
)> {
283 match self.map
.get(uuid
) {
286 match media_id
.media_set_label
{
287 None
=> None
, // not assigned to any pool
289 let is_empty
= set
.uuid
.as_ref() == [0u8;16];
290 Some((&set
.pool
, is_empty
))
297 /// List all media assigned to the pool
298 pub fn list_pool_media(&self, pool
: &str) -> Vec
<MediaId
> {
299 let mut list
= Vec
::new();
301 for (_uuid
, media_id
) in &self.map
{
302 match media_id
.media_set_label
{
303 None
=> continue, // not assigned to any pool
305 if set
.pool
!= pool
{
306 continue; // belong to another pool
309 if set
.uuid
.as_ref() == [0u8;16] { // should we do this??
311 label
: media_id
.label
.clone(),
312 media_set_label
: None
,
315 list
.push(media_id
.clone());
325 /// List all used media
326 pub fn list_used_media(&self) -> Vec
<MediaId
> {
327 let mut list
= Vec
::new();
329 for (_uuid
, media_id
) in &self.map
{
330 match media_id
.media_set_label
{
331 None
=> continue, // not assigned to any pool
333 if set
.uuid
.as_ref() != [0u8;16] {
334 list
.push(media_id
.clone());
343 /// List media not assigned to any pool
344 pub fn list_unassigned_media(&self) -> Vec
<MediaId
> {
345 let mut list
= Vec
::new();
347 for (_uuid
, media_id
) in &self.map
{
348 if media_id
.media_set_label
.is_none() {
349 list
.push(media_id
.clone());
356 pub fn media_set_start_time(&self, media_set_uuid
: &Uuid
) -> Option
<i64> {
357 self.media_set_start_times
.get(media_set_uuid
).map(|t
| *t
)
360 /// Compute a single media sets
361 pub fn compute_media_set_members(&self, media_set_uuid
: &Uuid
) -> Result
<MediaSet
, Error
> {
363 let mut set
= MediaSet
::with_data(media_set_uuid
.clone(), Vec
::new());
365 for media
in self.map
.values() {
366 match media
.media_set_label
{
368 Some(MediaSetLabel { seq_nr, ref uuid, .. }
) => {
369 if uuid
!= media_set_uuid
{
372 set
.insert_media(media
.label
.uuid
.clone(), seq_nr
)?
;
380 /// Compute all media sets
381 pub fn compute_media_set_list(&self) -> Result
<HashMap
<Uuid
, MediaSet
>, Error
> {
383 let mut set_map
: HashMap
<Uuid
, MediaSet
> = HashMap
::new();
385 for media
in self.map
.values() {
386 match media
.media_set_label
{
388 Some(MediaSetLabel { seq_nr, ref uuid, .. }
) => {
390 let set
= set_map
.entry(uuid
.clone()).or_insert_with(|| {
391 MediaSet
::with_data(uuid
.clone(), Vec
::new())
394 set
.insert_media(media
.label
.uuid
.clone(), seq_nr
)?
;
402 /// Returns the latest media set for a pool
403 pub fn latest_media_set(&self, pool
: &str) -> Option
<Uuid
> {
405 let mut last_set
: Option
<(Uuid
, i64)> = None
;
407 let set_list
= self.map
.values()
408 .filter_map(|media
| media
.media_set_label
.as_ref())
409 .filter(|set
| &set
.pool
== &pool
&& set
.uuid
.as_ref() != [0u8;16]);
411 for set
in set_list
{
414 last_set
= Some((set
.uuid
.clone(), set
.ctime
));
416 Some((_
, last_ctime
)) => {
417 if set
.ctime
> last_ctime
{
418 last_set
= Some((set
.uuid
.clone(), set
.ctime
));
424 let (uuid
, ctime
) = match last_set
{
426 Some((uuid
, ctime
)) => (uuid
, ctime
),
429 // consistency check - must be the only set with that ctime
430 let set_list
= self.map
.values()
431 .filter_map(|media
| media
.media_set_label
.as_ref())
432 .filter(|set
| &set
.pool
== &pool
&& set
.uuid
.as_ref() != [0u8;16]);
434 for set
in set_list
{
435 if set
.uuid
!= uuid
&& set
.ctime
>= ctime
{ // should not happen
436 eprintln
!("latest_media_set: found set with equal ctime ({}, {})", set
.uuid
, uuid
);
444 // Test if there is a media set (in the same pool) newer than this one.
445 // Return the ctime of the nearest media set
446 fn media_set_next_start_time(&self, media_set_uuid
: &Uuid
) -> Option
<i64> {
448 let (pool
, ctime
) = match self.map
.values()
449 .filter_map(|media
| media
.media_set_label
.as_ref())
451 if &set
.uuid
== media_set_uuid
{
452 Some((set
.pool
.clone(), set
.ctime
))
457 Some((pool
, ctime
)) => (pool
, ctime
),
461 let set_list
= self.map
.values()
462 .filter_map(|media
| media
.media_set_label
.as_ref())
463 .filter(|set
| (&set
.uuid
!= media_set_uuid
) && (&set
.pool
== &pool
));
465 let mut next_ctime
= None
;
467 for set
in set_list
{
468 if set
.ctime
> ctime
{
471 next_ctime
= Some(set
.ctime
);
473 Some(last_next_ctime
) => {
474 if set
.ctime
< last_next_ctime
{
475 next_ctime
= Some(set
.ctime
);
485 pub fn media_expire_time(
488 media_set_policy
: &MediaSetPolicy
,
489 retention_policy
: &RetentionPolicy
,
492 if let RetentionPolicy
::KeepForever
= retention_policy
{
496 let set
= match media
.media_set_label
{
497 None
=> return i64::MAX
,
498 Some(ref set
) => set
,
501 let set_start_time
= match self.media_set_start_time(&set
.uuid
) {
503 // missing information, use ctime from this
504 // set (always greater than ctime from seq_nr 0)
510 let max_use_time
= match media_set_policy
{
511 MediaSetPolicy
::ContinueCurrent
=> {
512 match self.media_set_next_start_time(&set
.uuid
) {
513 Some(next_start_time
) => next_start_time
,
514 None
=> return i64::MAX
,
517 MediaSetPolicy
::AlwaysCreate
=> {
520 MediaSetPolicy
::CreateAt(ref event
) => {
521 match compute_next_event(event
, set_start_time
, false) {
522 Ok(Some(next
)) => next
,
523 Ok(None
) | Err(_
) => return i64::MAX
,
528 match retention_policy
{
529 RetentionPolicy
::KeepForever
=> i64::MAX
,
530 RetentionPolicy
::OverwriteAlways
=> max_use_time
,
531 RetentionPolicy
::ProtectFor(time_span
) => {
532 let seconds
= f64::from(time_span
.clone()) as i64;
533 max_use_time
+ seconds
538 /// Generate a human readable name for the media set
540 /// The template can include strftime time format specifications.
541 pub fn generate_media_set_name(
543 media_set_uuid
: &Uuid
,
544 template
: Option
<String
>,
545 ) -> Result
<String
, Error
> {
547 if let Some(ctime
) = self.media_set_start_time(media_set_uuid
) {
548 let mut template
= template
.unwrap_or(String
::from("%id%"));
549 template
= template
.replace("%id%", &media_set_uuid
.to_string());
550 proxmox
::tools
::time
::strftime_local(&template
, ctime
)
552 // We don't know the set start time, so we cannot use the template
553 Ok(media_set_uuid
.to_string())
557 // Helpers to simplify testing
559 /// Genreate and insert a new free tape (test helper)
560 pub fn generate_free_tape(&mut self, changer_id
: &str, ctime
: i64) -> Uuid
{
562 let label
= DriveLabel
{
563 changer_id
: changer_id
.to_string(),
564 uuid
: Uuid
::generate(),
567 let uuid
= label
.uuid
.clone();
569 self.store(MediaId { label, media_set_label: None }
).unwrap();
574 /// Genreate and insert a new tape assigned to a specific pool
576 pub fn generate_assigned_tape(
583 let label
= DriveLabel
{
584 changer_id
: changer_id
.to_string(),
585 uuid
: Uuid
::generate(),
589 let uuid
= label
.uuid
.clone();
591 let set
= MediaSetLabel
::with_data(pool
, [0u8; 16].into(), 0, ctime
);
593 self.store(MediaId { label, media_set_label: Some(set) }
).unwrap();
598 /// Genreate and insert a used tape (test helper)
599 pub fn generate_used_tape(
605 let label
= DriveLabel
{
606 changer_id
: changer_id
.to_string(),
607 uuid
: Uuid
::generate(),
610 let uuid
= label
.uuid
.clone();
612 self.store(MediaId { label, media_set_label: Some(set) }
).unwrap();
618 // shell completion helper
620 /// List of known media uuids
621 pub fn complete_media_uuid(
623 _param
: &HashMap
<String
, String
>,
626 let inventory
= match Inventory
::load(Path
::new(TAPE_STATUS_DIR
)) {
627 Ok(inventory
) => inventory
,
628 Err(_
) => return Vec
::new(),
631 inventory
.map
.keys().map(|uuid
| uuid
.to_string()).collect()
634 /// List of known media sets
635 pub fn complete_media_set_uuid(
637 _param
: &HashMap
<String
, String
>,
640 let inventory
= match Inventory
::load(Path
::new(TAPE_STATUS_DIR
)) {
641 Ok(inventory
) => inventory
,
642 Err(_
) => return Vec
::new(),
645 inventory
.map
.values()
646 .filter_map(|media
| media
.media_set_label
.as_ref())
647 .map(|set
| set
.uuid
.to_string()).collect()
650 /// List of known media labels (barcodes)
651 pub fn complete_media_changer_id(
653 _param
: &HashMap
<String
, String
>,
656 let inventory
= match Inventory
::load(Path
::new(TAPE_STATUS_DIR
)) {
657 Ok(inventory
) => inventory
,
658 Err(_
) => return Vec
::new(),
661 inventory
.map
.values().map(|media
| media
.label
.changer_id
.clone()).collect()