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 //! MediaLabel/MediaSetLabel combination.
7 use std
::collections
::{HashMap, BTreeMap}
;
8 use std
::path
::{Path, PathBuf}
;
9 use std
::os
::unix
::io
::AsRawFd
;
11 use anyhow
::{bail, Error}
;
12 use serde
::{Serialize, Deserialize}
;
27 tools
::systemd
::time
::compute_next_event
,
41 changer
::OnlineStatusMap
,
45 /// Unique Media Identifier
47 /// This combines the label and media set label.
48 #[derive(Debug,Serialize,Deserialize,Clone)]
50 pub label
: MediaLabel
,
51 #[serde(skip_serializing_if="Option::is_none")]
52 pub media_set_label
: Option
<MediaSetLabel
>,
56 #[derive(Serialize,Deserialize)]
57 struct MediaStateEntry
{
59 #[serde(skip_serializing_if="Option::is_none")]
60 location
: Option
<MediaLocation
>,
61 #[serde(skip_serializing_if="Option::is_none")]
62 status
: Option
<MediaStatus
>,
66 pub struct Inventory
{
67 map
: BTreeMap
<Uuid
, MediaStateEntry
>,
69 inventory_path
: PathBuf
,
70 lockfile_path
: PathBuf
,
73 media_set_start_times
: HashMap
<Uuid
, i64>
78 pub const MEDIA_INVENTORY_FILENAME
: &'
static str = "inventory.json";
79 pub const MEDIA_INVENTORY_LOCKFILE
: &'
static str = ".inventory.lck";
81 fn new(base_path
: &Path
) -> Self {
83 let mut inventory_path
= base_path
.to_owned();
84 inventory_path
.push(Self::MEDIA_INVENTORY_FILENAME
);
86 let mut lockfile_path
= base_path
.to_owned();
87 lockfile_path
.push(Self::MEDIA_INVENTORY_LOCKFILE
);
91 media_set_start_times
: HashMap
::new(),
97 pub fn load(base_path
: &Path
) -> Result
<Self, Error
> {
98 let mut me
= Self::new(base_path
);
103 /// Reload the database
104 pub fn reload(&mut self) -> Result
<(), Error
> {
105 self.map
= Self::load_media_db(&self.inventory_path
)?
;
106 self.update_helpers();
110 fn update_helpers(&mut self) {
112 // recompute media_set_start_times
114 let mut set_start_times
= HashMap
::new();
116 for entry
in self.map
.values() {
117 let set
= match &entry
.id
.media_set_label
{
122 set_start_times
.insert(set
.uuid
.clone(), set
.ctime
);
126 self.media_set_start_times
= set_start_times
;
129 /// Lock the database
130 pub fn lock(&self) -> Result
<std
::fs
::File
, Error
> {
131 let file
= open_file_locked(&self.lockfile_path
, std
::time
::Duration
::new(10, 0), true)?
;
133 // We cannot use chown inside test environment (no permissions)
137 let backup_user
= crate::backup
::backup_user()?
;
138 fchown(file
.as_raw_fd(), Some(backup_user
.uid
), Some(backup_user
.gid
))?
;
143 fn load_media_db(path
: &Path
) -> Result
<BTreeMap
<Uuid
, MediaStateEntry
>, Error
> {
145 let data
= file_get_json(path
, Some(json
!([])))?
;
146 let media_list
: Vec
<MediaStateEntry
> = serde_json
::from_value(data
)?
;
148 let mut map
= BTreeMap
::new();
149 for entry
in media_list
.into_iter() {
150 map
.insert(entry
.id
.label
.uuid
.clone(), entry
);
156 fn replace_file(&self) -> Result
<(), Error
> {
157 let list
: Vec
<&MediaStateEntry
> = self.map
.values().collect();
158 let raw
= serde_json
::to_string_pretty(&serde_json
::to_value(list
)?
)?
;
160 let mode
= nix
::sys
::stat
::Mode
::from_bits_truncate(0o0640);
162 let options
= if cfg
!(test
) {
163 // We cannot use chown inside test environment (no permissions)
164 CreateOptions
::new().perm(mode
)
166 let backup_user
= crate::backup
::backup_user()?
;
169 .owner(backup_user
.uid
)
170 .group(backup_user
.gid
)
173 replace_file(&self.inventory_path
, raw
.as_bytes(), options
)?
;
178 /// Stores a single MediaID persistently
181 mut media_id
: MediaId
,
182 clear_media_status
: bool
,
183 ) -> Result
<(), Error
> {
184 let _lock
= self.lock()?
;
185 self.map
= Self::load_media_db(&self.inventory_path
)?
;
187 let uuid
= media_id
.label
.uuid
.clone();
189 if let Some(previous
) = self.map
.remove(&media_id
.label
.uuid
) {
190 // do not overwrite unsaved pool assignments
191 if media_id
.media_set_label
.is_none() {
192 if let Some(ref set
) = previous
.id
.media_set_label
{
193 if set
.uuid
.as_ref() == [0u8;16] {
194 media_id
.media_set_label
= Some(set
.clone());
198 let entry
= MediaStateEntry
{
200 location
: previous
.location
,
201 status
: if clear_media_status
{
207 self.map
.insert(uuid
, entry
);
209 let entry
= MediaStateEntry { id: media_id, location: None, status: None }
;
210 self.map
.insert(uuid
, entry
);
213 self.update_helpers();
214 self.replace_file()?
;
218 /// Remove a single media persistently
219 pub fn remove_media(&mut self, uuid
: &Uuid
) -> Result
<(), Error
> {
220 let _lock
= self.lock()?
;
221 self.map
= Self::load_media_db(&self.inventory_path
)?
;
222 self.map
.remove(uuid
);
223 self.update_helpers();
224 self.replace_file()?
;
229 pub fn lookup_media(&self, uuid
: &Uuid
) -> Option
<&MediaId
> {
230 self.map
.get(uuid
).map(|entry
| &entry
.id
)
233 /// List all media Uuids
234 pub fn media_list(&self) -> Vec
<&Uuid
> {
235 self.map
.keys().collect()
238 /// find media by label_text
239 pub fn find_media_by_label_text(&self, label_text
: &str) -> Option
<&MediaId
> {
240 self.map
.values().find_map(|entry
| {
241 if entry
.id
.label
.label_text
== label_text
{
249 /// Lookup media pool
251 /// Returns (pool, is_empty)
252 pub fn lookup_media_pool(&self, uuid
: &Uuid
) -> Option
<(&str, bool
)> {
253 match self.map
.get(uuid
) {
256 match entry
.id
.media_set_label
{
257 None
=> None
, // not assigned to any pool
259 let is_empty
= set
.uuid
.as_ref() == [0u8;16];
260 Some((&set
.pool
, is_empty
))
267 /// List all media assigned to the pool
268 pub fn list_pool_media(&self, pool
: &str) -> Vec
<MediaId
> {
269 let mut list
= Vec
::new();
271 for entry
in self.map
.values() {
272 match entry
.id
.media_set_label
{
273 None
=> continue, // not assigned to any pool
275 if set
.pool
!= pool
{
276 continue; // belong to another pool
279 if set
.uuid
.as_ref() == [0u8;16] { // should we do this??
281 label
: entry
.id
.label
.clone(),
282 media_set_label
: None
,
285 list
.push(entry
.id
.clone());
294 /// List all used media
295 pub fn list_used_media(&self) -> Vec
<MediaId
> {
296 let mut list
= Vec
::new();
298 for entry
in self.map
.values() {
299 match entry
.id
.media_set_label
{
300 None
=> continue, // not assigned to any pool
302 if set
.uuid
.as_ref() != [0u8;16] {
303 list
.push(entry
.id
.clone());
312 /// List media not assigned to any pool
313 pub fn list_unassigned_media(&self) -> Vec
<MediaId
> {
314 self.map
.values().filter_map(|entry
|
315 if entry
.id
.media_set_label
.is_none() {
316 Some(entry
.id
.clone())
323 pub fn media_set_start_time(&self, media_set_uuid
: &Uuid
) -> Option
<i64> {
324 self.media_set_start_times
.get(media_set_uuid
).copied()
327 /// Lookup media set pool
328 pub fn lookup_media_set_pool(&self, media_set_uuid
: &Uuid
) -> Result
<String
, Error
> {
330 let mut last_pool
= None
;
332 for entry
in self.map
.values() {
333 match entry
.id
.media_set_label
{
335 Some(MediaSetLabel { ref uuid, .. }
) => {
336 if uuid
!= media_set_uuid
{
339 if let Some((pool
, _
)) = self.lookup_media_pool(&entry
.id
.label
.uuid
) {
340 if let Some(last_pool
) = last_pool
{
341 if last_pool
!= pool
{
342 bail
!("detected media set with inconsistent pool assignment - internal error");
345 last_pool
= Some(pool
);
353 Some(pool
) => Ok(pool
.to_string()),
354 None
=> bail
!("media set {} is incomplete - unable to lookup pool", media_set_uuid
),
358 /// Compute a single media sets
359 pub fn compute_media_set_members(&self, media_set_uuid
: &Uuid
) -> Result
<MediaSet
, Error
> {
361 let mut set
= MediaSet
::with_data(media_set_uuid
.clone(), Vec
::new());
363 for entry
in self.map
.values() {
364 match entry
.id
.media_set_label
{
366 Some(MediaSetLabel { seq_nr, ref uuid, .. }
) => {
367 if uuid
!= media_set_uuid
{
370 set
.insert_media(entry
.id
.label
.uuid
.clone(), seq_nr
)?
;
378 /// Compute all media sets
379 pub fn compute_media_set_list(&self) -> Result
<HashMap
<Uuid
, MediaSet
>, Error
> {
381 let mut set_map
: HashMap
<Uuid
, MediaSet
> = HashMap
::new();
383 for entry
in self.map
.values() {
384 match entry
.id
.media_set_label
{
386 Some(MediaSetLabel { seq_nr, ref uuid, .. }
) => {
388 let set
= set_map
.entry(uuid
.clone()).or_insert_with(|| {
389 MediaSet
::with_data(uuid
.clone(), Vec
::new())
392 set
.insert_media(entry
.id
.label
.uuid
.clone(), seq_nr
)?
;
400 /// Returns the latest media set for a pool
401 pub fn latest_media_set(&self, pool
: &str) -> Option
<Uuid
> {
403 let mut last_set
: Option
<(Uuid
, i64)> = None
;
405 let set_list
= self.map
.values()
406 .filter_map(|entry
| entry
.id
.media_set_label
.as_ref())
407 .filter(|set
| set
.pool
== pool
&& set
.uuid
.as_ref() != [0u8;16]);
409 for set
in set_list
{
412 last_set
= Some((set
.uuid
.clone(), set
.ctime
));
414 Some((_
, last_ctime
)) => {
415 if set
.ctime
> last_ctime
{
416 last_set
= Some((set
.uuid
.clone(), set
.ctime
));
422 let (uuid
, ctime
) = match last_set
{
424 Some((uuid
, ctime
)) => (uuid
, ctime
),
427 // consistency check - must be the only set with that ctime
428 let set_list
= self.map
.values()
429 .filter_map(|entry
| entry
.id
.media_set_label
.as_ref())
430 .filter(|set
| set
.pool
== pool
&& set
.uuid
.as_ref() != [0u8;16]);
432 for set
in set_list
{
433 if set
.uuid
!= uuid
&& set
.ctime
>= ctime
{ // should not happen
434 eprintln
!("latest_media_set: found set with equal ctime ({}, {})", set
.uuid
, uuid
);
442 // Test if there is a media set (in the same pool) newer than this one.
443 // Return the ctime of the nearest media set
444 fn media_set_next_start_time(&self, media_set_uuid
: &Uuid
) -> Option
<i64> {
446 let (pool
, ctime
) = match self.map
.values()
447 .filter_map(|entry
| entry
.id
.media_set_label
.as_ref())
449 if &set
.uuid
== media_set_uuid
{
450 Some((set
.pool
.clone(), set
.ctime
))
455 Some((pool
, ctime
)) => (pool
, ctime
),
459 let set_list
= self.map
.values()
460 .filter_map(|entry
| entry
.id
.media_set_label
.as_ref())
461 .filter(|set
| (&set
.uuid
!= media_set_uuid
) && (set
.pool
== pool
));
463 let mut next_ctime
= None
;
465 for set
in set_list
{
466 if set
.ctime
> ctime
{
469 next_ctime
= Some(set
.ctime
);
471 Some(last_next_ctime
) => {
472 if set
.ctime
< last_next_ctime
{
473 next_ctime
= Some(set
.ctime
);
483 pub fn media_expire_time(
486 media_set_policy
: &MediaSetPolicy
,
487 retention_policy
: &RetentionPolicy
,
490 if let RetentionPolicy
::KeepForever
= retention_policy
{
494 let set
= match media
.media_set_label
{
495 None
=> return i64::MAX
,
496 Some(ref set
) => set
,
499 let set_start_time
= match self.media_set_start_time(&set
.uuid
) {
501 // missing information, use ctime from this
502 // set (always greater than ctime from seq_nr 0)
508 let max_use_time
= match media_set_policy
{
509 MediaSetPolicy
::ContinueCurrent
=> {
510 match self.media_set_next_start_time(&set
.uuid
) {
511 Some(next_start_time
) => next_start_time
,
512 None
=> return i64::MAX
,
515 MediaSetPolicy
::AlwaysCreate
=> {
518 MediaSetPolicy
::CreateAt(ref event
) => {
519 match compute_next_event(event
, set_start_time
, false) {
520 Ok(Some(next
)) => next
,
521 Ok(None
) | Err(_
) => return i64::MAX
,
526 match retention_policy
{
527 RetentionPolicy
::KeepForever
=> i64::MAX
,
528 RetentionPolicy
::OverwriteAlways
=> max_use_time
,
529 RetentionPolicy
::ProtectFor(time_span
) => {
530 let seconds
= f64::from(time_span
.clone()) as i64;
531 max_use_time
+ seconds
536 /// Generate a human readable name for the media set
538 /// The template can include strftime time format specifications.
539 pub fn generate_media_set_name(
541 media_set_uuid
: &Uuid
,
542 template
: Option
<String
>,
543 ) -> Result
<String
, Error
> {
545 if let Some(ctime
) = self.media_set_start_time(media_set_uuid
) {
546 let mut template
= template
.unwrap_or_else(|| String
::from("%c"));
547 template
= template
.replace("%id%", &media_set_uuid
.to_string());
548 proxmox
::tools
::time
::strftime_local(&template
, ctime
)
550 // We don't know the set start time, so we cannot use the template
551 Ok(media_set_uuid
.to_string())
555 // Helpers to simplify testing
557 /// Genreate and insert a new free tape (test helper)
558 pub fn generate_free_tape(&mut self, label_text
: &str, ctime
: i64) -> Uuid
{
560 let label
= MediaLabel
{
561 label_text
: label_text
.to_string(),
562 uuid
: Uuid
::generate(),
565 let uuid
= label
.uuid
.clone();
567 self.store(MediaId { label, media_set_label: None }
, false).unwrap();
572 /// Genreate and insert a new tape assigned to a specific pool
574 pub fn generate_assigned_tape(
581 let label
= MediaLabel
{
582 label_text
: label_text
.to_string(),
583 uuid
: Uuid
::generate(),
587 let uuid
= label
.uuid
.clone();
589 let set
= MediaSetLabel
::with_data(pool
, [0u8; 16].into(), 0, ctime
, None
);
591 self.store(MediaId { label, media_set_label: Some(set) }
, false).unwrap();
596 /// Genreate and insert a used tape (test helper)
597 pub fn generate_used_tape(
603 let label
= MediaLabel
{
604 label_text
: label_text
.to_string(),
605 uuid
: Uuid
::generate(),
608 let uuid
= label
.uuid
.clone();
610 self.store(MediaId { label, media_set_label: Some(set) }
, false).unwrap();
616 // Status/location handling
619 /// Returns status and location with reasonable defaults.
621 /// Default status is 'MediaStatus::Unknown'.
622 /// Default location is 'MediaLocation::Offline'.
623 pub fn status_and_location(&self, uuid
: &Uuid
) -> (MediaStatus
, MediaLocation
) {
625 match self.map
.get(uuid
) {
627 // no info stored - assume media is writable/offline
628 (MediaStatus
::Unknown
, MediaLocation
::Offline
)
631 let location
= entry
.location
.clone().unwrap_or(MediaLocation
::Offline
);
632 let status
= entry
.status
.unwrap_or(MediaStatus
::Unknown
);
638 // Lock database, reload database, set status, store database
639 fn set_media_status(&mut self, uuid
: &Uuid
, status
: Option
<MediaStatus
>) -> Result
<(), Error
> {
640 let _lock
= self.lock()?
;
641 self.map
= Self::load_media_db(&self.inventory_path
)?
;
642 if let Some(entry
) = self.map
.get_mut(uuid
) {
643 entry
.status
= status
;
644 self.update_helpers();
645 self.replace_file()?
;
648 bail
!("no such media '{}'", uuid
);
652 /// Lock database, reload database, set status to Full, store database
653 pub fn set_media_status_full(&mut self, uuid
: &Uuid
) -> Result
<(), Error
> {
654 self.set_media_status(uuid
, Some(MediaStatus
::Full
))
657 /// Lock database, reload database, set status to Damaged, store database
658 pub fn set_media_status_damaged(&mut self, uuid
: &Uuid
) -> Result
<(), Error
> {
659 self.set_media_status(uuid
, Some(MediaStatus
::Damaged
))
662 /// Lock database, reload database, set status to Retired, store database
663 pub fn set_media_status_retired(&mut self, uuid
: &Uuid
) -> Result
<(), Error
> {
664 self.set_media_status(uuid
, Some(MediaStatus
::Retired
))
667 /// Lock database, reload database, set status to None, store database
668 pub fn clear_media_status(&mut self, uuid
: &Uuid
) -> Result
<(), Error
> {
669 self.set_media_status(uuid
, None
)
672 // Lock database, reload database, set location, store database
673 fn set_media_location(&mut self, uuid
: &Uuid
, location
: Option
<MediaLocation
>) -> Result
<(), Error
> {
674 let _lock
= self.lock()?
;
675 self.map
= Self::load_media_db(&self.inventory_path
)?
;
676 if let Some(entry
) = self.map
.get_mut(uuid
) {
677 entry
.location
= location
;
678 self.update_helpers();
679 self.replace_file()?
;
682 bail
!("no such media '{}'", uuid
);
686 /// Lock database, reload database, set location to vault, store database
687 pub fn set_media_location_vault(&mut self, uuid
: &Uuid
, vault
: &str) -> Result
<(), Error
> {
688 self.set_media_location(uuid
, Some(MediaLocation
::Vault(vault
.to_string())))
691 /// Lock database, reload database, set location to offline, store database
692 pub fn set_media_location_offline(&mut self, uuid
: &Uuid
) -> Result
<(), Error
> {
693 self.set_media_location(uuid
, Some(MediaLocation
::Offline
))
696 /// Update online status
697 pub fn update_online_status(&mut self, online_map
: &OnlineStatusMap
) -> Result
<(), Error
> {
698 let _lock
= self.lock()?
;
699 self.map
= Self::load_media_db(&self.inventory_path
)?
;
701 for (uuid
, entry
) in self.map
.iter_mut() {
702 if let Some(changer_name
) = online_map
.lookup_changer(uuid
) {
703 entry
.location
= Some(MediaLocation
::Online(changer_name
.to_string()));
704 } else if let Some(MediaLocation
::Online(ref changer_name
)) = entry
.location
{
705 match online_map
.online_map(changer_name
) {
707 // no such changer device
708 entry
.location
= Some(MediaLocation
::Offline
);
711 // got no info - do nothing
714 // media changer changed
715 entry
.location
= Some(MediaLocation
::Offline
);
721 self.update_helpers();
722 self.replace_file()?
;
729 // shell completion helper
731 /// List of known media uuids
732 pub fn complete_media_uuid(
734 _param
: &HashMap
<String
, String
>,
737 let inventory
= match Inventory
::load(Path
::new(TAPE_STATUS_DIR
)) {
738 Ok(inventory
) => inventory
,
739 Err(_
) => return Vec
::new(),
742 inventory
.map
.keys().map(|uuid
| uuid
.to_string()).collect()
745 /// List of known media sets
746 pub fn complete_media_set_uuid(
748 _param
: &HashMap
<String
, String
>,
751 let inventory
= match Inventory
::load(Path
::new(TAPE_STATUS_DIR
)) {
752 Ok(inventory
) => inventory
,
753 Err(_
) => return Vec
::new(),
756 inventory
.map
.values()
757 .filter_map(|entry
| entry
.id
.media_set_label
.as_ref())
758 .map(|set
| set
.uuid
.to_string()).collect()
761 /// List of known media labels (barcodes)
762 pub fn complete_media_label_text(
764 _param
: &HashMap
<String
, String
>,
767 let inventory
= match Inventory
::load(Path
::new(TAPE_STATUS_DIR
)) {
768 Ok(inventory
) => inventory
,
769 Err(_
) => return Vec
::new(),
772 inventory
.map
.values().map(|entry
| entry
.id
.label
.label_text
.clone()).collect()