3 //! A set of backup medias.
5 //! This struct manages backup media state during backup. The main
6 //! purpose is to allocate media sets and assign new tapes to it.
10 use std
::path
::{PathBuf, Path}
;
12 use anyhow
::{bail, Error}
;
13 use ::serde
::{Deserialize, Serialize}
;
15 use proxmox
::tools
::Uuid
;
18 backup
::{Fingerprint, BackupLockGuard}
,
26 tools
::systemd
::time
::compute_next_event
,
34 lock_unassigned_media_pool
,
43 pub struct MediaPool
{
48 media_set_policy
: MediaSetPolicy
,
49 retention
: RetentionPolicy
,
51 changer_name
: Option
<String
>,
52 force_media_availability
: bool
,
54 // Set this if you do not need to allocate writeable media - this
55 // is useful for list_media()
56 no_media_set_locking
: bool
,
58 encrypt_fingerprint
: Option
<Fingerprint
>,
62 current_media_set
: MediaSet
,
63 current_media_set_lock
: Option
<BackupLockGuard
>,
68 /// Creates a new instance
70 /// If you specify a `changer_name`, only media accessible via
71 /// that changer is considered available. If you pass `None` for
72 /// `changer`, all offline media is considered available (backups
73 /// to standalone drives may not use media from inside a tape
78 media_set_policy
: MediaSetPolicy
,
79 retention
: RetentionPolicy
,
80 changer_name
: Option
<String
>,
81 encrypt_fingerprint
: Option
<Fingerprint
>,
82 no_media_set_locking
: bool
, // for list_media()
83 ) -> Result
<Self, Error
> {
85 let _pool_lock
= if no_media_set_locking
{
88 Some(lock_media_pool(state_path
, name
)?
)
91 let inventory
= Inventory
::load(state_path
)?
;
93 let current_media_set
= match inventory
.latest_media_set(name
) {
94 Some(set_uuid
) => inventory
.compute_media_set_members(&set_uuid
)?
,
95 None
=> MediaSet
::new(),
98 let current_media_set_lock
= if no_media_set_locking
{
101 Some(lock_media_set(state_path
, current_media_set
.uuid(), None
)?
)
105 name
: String
::from(name
),
106 state_path
: state_path
.to_owned(),
112 current_media_set_lock
,
114 force_media_availability
: false,
115 no_media_set_locking
,
119 /// Pretend all Online(x) and Offline media is available
121 /// Only media in Vault(y) is considered unavailable.
122 pub fn force_media_availability(&mut self) {
123 self.force_media_availability
= true;
126 /// Returns the the current media set
127 pub fn current_media_set(&self) -> &MediaSet
{
128 &self.current_media_set
131 /// Creates a new instance using the media pool configuration
134 config
: &MediaPoolConfig
,
135 changer_name
: Option
<String
>,
136 no_media_set_locking
: bool
, // for list_media()
137 ) -> Result
<Self, Error
> {
139 let allocation
= config
.allocation
.clone().unwrap_or_else(|| String
::from("continue")).parse()?
;
141 let retention
= config
.retention
.clone().unwrap_or_else(|| String
::from("keep")).parse()?
;
143 let encrypt_fingerprint
= match config
.encrypt
{
144 Some(ref fingerprint
) => Some(fingerprint
.parse()?
),
155 no_media_set_locking
,
159 /// Returns the pool name
160 pub fn name(&self) -> &str {
164 /// Returns encryption settings
165 pub fn encrypt_fingerprint(&self) -> Option
<Fingerprint
> {
166 self.encrypt_fingerprint
.clone()
169 pub fn set_media_status_damaged(&mut self, uuid
: &Uuid
) -> Result
<(), Error
> {
170 self.inventory
.set_media_status_damaged(uuid
)
173 fn compute_media_state(&self, media_id
: &MediaId
) -> (MediaStatus
, MediaLocation
) {
175 let (status
, location
) = self.inventory
.status_and_location(&media_id
.label
.uuid
);
178 MediaStatus
::Full
| MediaStatus
::Damaged
| MediaStatus
::Retired
=> {
179 return (status
, location
);
181 MediaStatus
::Unknown
| MediaStatus
::Writable
=> {
182 /* possibly writable - fall through to check */
186 let set
= match media_id
.media_set_label
{
187 None
=> return (MediaStatus
::Writable
, location
), // not assigned to any pool
188 Some(ref set
) => set
,
191 if set
.pool
!= self.name
{ // should never trigger
192 return (MediaStatus
::Unknown
, location
); // belong to another pool
194 if set
.uuid
.as_ref() == [0u8;16] { // not assigned to any pool
195 return (MediaStatus
::Writable
, location
);
198 if &set
.uuid
!= self.current_media_set
.uuid() {
199 return (MediaStatus
::Full
, location
); // assume FULL
202 // media is member of current set
203 if self.current_media_set
.is_last_media(&media_id
.label
.uuid
) {
204 (MediaStatus
::Writable
, location
) // last set member is writable
206 (MediaStatus
::Full
, location
)
210 /// Returns the 'MediaId' with associated state
211 pub fn lookup_media(&self, uuid
: &Uuid
) -> Result
<BackupMedia
, Error
> {
212 let media_id
= match self.inventory
.lookup_media(uuid
) {
213 None
=> bail
!("unable to lookup media {}", uuid
),
214 Some(media_id
) => media_id
.clone(),
217 if let Some(ref set
) = media_id
.media_set_label
{
218 if set
.pool
!= self.name
{
219 bail
!("media does not belong to pool ({} != {})", set
.pool
, self.name
);
223 let (status
, location
) = self.compute_media_state(&media_id
);
225 Ok(BackupMedia
::with_media_id(
232 /// List all media associated with this pool
233 pub fn list_media(&self) -> Vec
<BackupMedia
> {
234 let media_id_list
= self.inventory
.list_pool_media(&self.name
);
236 media_id_list
.into_iter()
238 let (status
, location
) = self.compute_media_state(&media_id
);
239 BackupMedia
::with_media_id(
248 /// Set media status to FULL.
249 pub fn set_media_status_full(&mut self, uuid
: &Uuid
) -> Result
<(), Error
> {
250 let media
= self.lookup_media(uuid
)?
; // check if media belongs to this pool
251 if media
.status() != &MediaStatus
::Full
{
252 self.inventory
.set_media_status_full(uuid
)?
;
257 /// Make sure the current media set is usable for writing
259 /// If not, starts a new media set. Also creates a new
260 /// set if media_set_policy implies it, or if 'force' is true.
262 /// Note: We also call this in list_media to compute correct media
263 /// status, so this must not change persistent/saved state.
265 /// Returns the reason why we started a new media set (if we do)
266 pub fn start_write_session(
270 ) -> Result
<Option
<String
>, Error
> {
272 let _pool_lock
= if self.no_media_set_locking
{
275 Some(lock_media_pool(&self.state_path
, &self.name
)?
)
278 self.inventory
.reload()?
;
280 let mut create_new_set
= if force
{
281 Some(String
::from("forced"))
283 match self.current_set_usable() {
285 Some(err
.to_string())
291 if create_new_set
.is_none() {
292 match &self.media_set_policy
{
293 MediaSetPolicy
::AlwaysCreate
=> {
294 create_new_set
= Some(String
::from("policy is AlwaysCreate"));
296 MediaSetPolicy
::CreateAt(event
) => {
297 if let Some(set_start_time
) = self.inventory
.media_set_start_time(&self.current_media_set
.uuid()) {
298 if let Ok(Some(alloc_time
)) = compute_next_event(event
, set_start_time
as i64, false) {
299 if current_time
>= alloc_time
{
300 create_new_set
= Some(String
::from("policy CreateAt event triggered"));
305 MediaSetPolicy
::ContinueCurrent
=> { /* do nothing here */ }
309 if create_new_set
.is_some() {
310 let media_set
= MediaSet
::new();
312 let current_media_set_lock
= if self.no_media_set_locking
{
315 Some(lock_media_set(&self.state_path
, media_set
.uuid(), None
)?
)
318 self.current_media_set_lock
= current_media_set_lock
;
319 self.current_media_set
= media_set
;
325 /// List media in current media set
326 pub fn current_media_list(&self) -> Result
<Vec
<&Uuid
>, Error
> {
327 let mut list
= Vec
::new();
328 for opt_uuid
in self.current_media_set
.media_list().iter() {
330 Some(ref uuid
) => list
.push(uuid
),
331 None
=> bail
!("current_media_list failed - media set is incomplete"),
337 // tests if the media data is considered as expired at specified time
338 pub fn media_is_expired(&self, media
: &BackupMedia
, current_time
: i64) -> bool
{
339 if media
.status() != &MediaStatus
::Full
{
343 let expire_time
= self.inventory
.media_expire_time(
344 media
.id(), &self.media_set_policy
, &self.retention
);
346 current_time
>= expire_time
349 // check if a location is considered on site
350 pub fn location_is_available(&self, location
: &MediaLocation
) -> bool
{
352 MediaLocation
::Online(name
) => {
353 if self.force_media_availability
{
356 if let Some(ref changer_name
) = self.changer_name
{
359 // a standalone drive cannot use media currently inside a library
364 MediaLocation
::Offline
=> {
365 if self.force_media_availability
{
368 // consider available for standalone drives
369 self.changer_name
.is_none()
372 MediaLocation
::Vault(_
) => false,
376 fn add_media_to_current_set(&mut self, mut media_id
: MediaId
, current_time
: i64) -> Result
<(), Error
> {
378 if self.current_media_set_lock
.is_none() {
379 bail
!("add_media_to_current_set: media set is not locked - internal error");
382 let seq_nr
= self.current_media_set
.media_list().len() as u64;
384 let pool
= self.name
.clone();
386 let encrypt_fingerprint
= self.encrypt_fingerprint();
388 let set
= MediaSetLabel
::with_data(
390 self.current_media_set
.uuid().clone(),
396 media_id
.media_set_label
= Some(set
);
398 let uuid
= media_id
.label
.uuid
.clone();
400 MediaCatalog
::overwrite(&self.state_path
, &media_id
, false)?
; // overwite catalog
401 let clear_media_status
= true; // remove Full status
402 self.inventory
.store(media_id
, clear_media_status
)?
; // store persistently
404 self.current_media_set
.add_media(uuid
);
409 // Get next unassigned media (media not assigned to any pool)
410 pub fn next_unassigned_media(&self, media_list
: &[MediaId
]) -> Option
<MediaId
> {
411 let mut free_media
= Vec
::new();
413 for media_id
in media_list
{
415 let (status
, location
) = self.compute_media_state(&media_id
);
416 if media_id
.media_set_label
.is_some() { continue; }
// should not happen
418 if !self.location_is_available(&location
) {
422 // only consider writable media
423 if status
!= MediaStatus
::Writable { continue; }
425 free_media
.push(media_id
);
428 // sort free_media, newest first -> oldest last
429 free_media
.sort_unstable_by(|a
, b
| {
430 let mut res
= b
.label
.ctime
.cmp(&a
.label
.ctime
);
431 if res
== std
::cmp
::Ordering
::Equal
{
432 res
= b
.label
.label_text
.cmp(&a
.label
.label_text
);
437 free_media
.pop().map(|e
| e
.clone())
440 // Get next empty media
441 pub fn next_empty_media(&self, media_list
: &[BackupMedia
]) -> Option
<MediaId
> {
442 let mut empty_media
= Vec
::new();
444 for media
in media_list
.into_iter() {
445 if !self.location_is_available(media
.location()) {
448 // already part of a media set?
449 if media
.media_set_label().is_none() {
450 // only consider writable empty media
451 if media
.status() == &MediaStatus
::Writable
{
452 empty_media
.push(media
);
457 // sort empty_media, newest first -> oldest last
458 empty_media
.sort_unstable_by(|a
, b
| {
459 let mut res
= b
.label().ctime
.cmp(&a
.label().ctime
);
460 if res
== std
::cmp
::Ordering
::Equal
{
461 res
= b
.label().label_text
.cmp(&a
.label().label_text
);
466 empty_media
.pop().map(|e
| e
.clone().into_id())
469 // Get next expired media
470 pub fn next_expired_media(&self, current_time
: i64, media_list
: &[BackupMedia
]) -> Option
<MediaId
> {
471 let mut used_media
= Vec
::new();
473 for media
in media_list
.into_iter() {
474 if !self.location_is_available(media
.location()) {
477 // already part of a media set?
478 if media
.media_set_label().is_some() {
479 used_media
.push(media
);
483 let mut expired_media
= Vec
::new();
485 for media
in used_media
.into_iter() {
486 if let Some(set
) = media
.media_set_label() {
487 if &set
.uuid
== self.current_media_set
.uuid() {
494 if !self.media_is_expired(&media
, current_time
) {
498 expired_media
.push(media
);
501 // sort expired_media, newest first -> oldest last
502 expired_media
.sort_unstable_by(|a
, b
| {
503 let mut res
= b
.media_set_label().unwrap().ctime
.cmp(&a
.media_set_label().unwrap().ctime
);
504 if res
== std
::cmp
::Ordering
::Equal
{
505 res
= b
.label().label_text
.cmp(&a
.label().label_text
);
510 if self.no_media_set_locking
{
511 expired_media
.pop().map(|e
| e
.clone().into_id())
513 while let Some(media
) = expired_media
.pop() {
514 // check if we can modify the media-set (i.e. skip
515 // media used by a restore job)
516 if let Ok(_media_set_lock
) = lock_media_set(
518 &media
.media_set_label().unwrap().uuid
,
519 Some(std
::time
::Duration
::new(0, 0)), // do not wait
521 return Some(media
.clone().into_id());
528 /// Guess next writable media
530 /// Like alloc_writable_media(), but does not really allocate
531 /// anything (thus it does not need any locks)
532 // Note: Please keep in sync with alloc_writable_media()
533 pub fn guess_next_writable_media(&self, current_time
: i64) -> Result
<MediaId
, Error
> {
534 let last_is_writable
= self.current_set_usable()?
;
536 if last_is_writable
{
537 let last_uuid
= self.current_media_set
.last_media_uuid().unwrap();
538 let media
= self.lookup_media(last_uuid
)?
;
539 return Ok(media
.into_id());
542 let media_list
= self.list_media();
543 if let Some(media_id
) = self.next_empty_media(&media_list
) {
547 if let Some(media_id
) = self.next_expired_media(current_time
, &media_list
) {
551 let unassigned_list
= self.inventory
.list_unassigned_media();
553 if let Some(media_id
) = self.next_unassigned_media(&unassigned_list
) {
557 bail
!("guess_next_writable_media in pool '{}' failed: no usable media found", self.name());
560 /// Allocates a writable media to the current media set
561 // Note: Please keep in sync with guess_next_writable_media()
562 pub fn alloc_writable_media(&mut self, current_time
: i64) -> Result
<Uuid
, Error
> {
564 if self.current_media_set_lock
.is_none() {
565 bail
!("alloc_writable_media: media set is not locked - internal error");
568 let last_is_writable
= self.current_set_usable()?
;
570 if last_is_writable
{
571 let last_uuid
= self.current_media_set
.last_media_uuid().unwrap();
572 let media
= self.lookup_media(last_uuid
)?
;
573 return Ok(media
.uuid().clone());
576 { // limit pool lock scope
577 let _pool_lock
= lock_media_pool(&self.state_path
, &self.name
)?
;
579 self.inventory
.reload()?
;
581 let media_list
= self.list_media();
583 // try to find empty media in pool, add to media set
585 if let Some(media_id
) = self.next_empty_media(&media_list
) {
586 // found empty media, add to media set an use it
587 println
!("found empty media '{}'", media_id
.label
.label_text
);
588 let uuid
= media_id
.label
.uuid
.clone();
589 self.add_media_to_current_set(media_id
, current_time
)?
;
593 println
!("no empty media in pool, try to reuse expired media");
595 if let Some(media_id
) = self.next_expired_media(current_time
, &media_list
) {
596 // found expired media, add to media set an use it
597 println
!("reuse expired media '{}'", media_id
.label
.label_text
);
598 let uuid
= media_id
.label
.uuid
.clone();
599 self.add_media_to_current_set(media_id
, current_time
)?
;
604 println
!("no empty or expired media in pool, try to find unassigned/free media");
606 // try unassigned media
607 let _lock
= lock_unassigned_media_pool(&self.state_path
)?
;
609 self.inventory
.reload()?
;
611 let unassigned_list
= self.inventory
.list_unassigned_media();
613 if let Some(media_id
) = self.next_unassigned_media(&unassigned_list
) {
614 println
!("use free/unassigned media '{}'", media_id
.label
.label_text
);
615 let uuid
= media_id
.label
.uuid
.clone();
616 self.add_media_to_current_set(media_id
, current_time
)?
;
620 bail
!("alloc writable media in pool '{}' failed: no usable media found", self.name());
623 /// check if the current media set is usable for writing
625 /// This does several consistency checks, and return if
626 /// the last media in the current set is in writable state.
628 /// This return error when the media set must not be used any
629 /// longer because of consistency errors.
630 pub fn current_set_usable(&self) -> Result
<bool
, Error
> {
632 let media_list
= self.current_media_set
.media_list();
634 let media_count
= media_list
.len();
635 if media_count
== 0 {
639 let set_uuid
= self.current_media_set
.uuid();
640 let mut last_is_writable
= false;
642 let mut last_enc
: Option
<Option
<Fingerprint
>> = None
;
644 for (seq
, opt_uuid
) in media_list
.iter().enumerate() {
645 let uuid
= match opt_uuid
{
646 None
=> bail
!("media set is incomplete (missing media information)"),
649 let media
= self.lookup_media(uuid
)?
;
650 match media
.media_set_label() {
651 Some(MediaSetLabel { seq_nr, uuid, ..}
) if *seq_nr
== seq
as u64 && uuid
== set_uuid
=> { /* OK */ }
,
652 Some(MediaSetLabel { seq_nr, uuid, ..}
) if uuid
== set_uuid
=> {
653 bail
!("media sequence error ({} != {})", *seq_nr
, seq
);
655 Some(MediaSetLabel { uuid, ..}
) => bail
!("media owner error ({} != {}", uuid
, set_uuid
),
656 None
=> bail
!("media owner error (no owner)"),
659 if let Some(set
) = media
.media_set_label() { // always true here
660 if set
.encryption_key_fingerprint
!= self.encrypt_fingerprint
{
661 bail
!("pool encryption key changed");
665 last_enc
= Some(set
.encryption_key_fingerprint
.clone());
667 Some(ref last_enc
) => {
668 if last_enc
!= &set
.encryption_key_fingerprint
{
669 bail
!("inconsistent media encryption key");
675 match media
.status() {
676 MediaStatus
::Full
=> { /* OK */ }
,
677 MediaStatus
::Writable
if (seq
+ 1) == media_count
=> {
678 let media_location
= media
.location();
679 if self.location_is_available(media_location
) {
680 last_is_writable
= true;
682 if let MediaLocation
::Vault(vault
) = media_location
{
683 bail
!("writable media offsite in vault '{}'", vault
);
687 _
=> bail
!("unable to use media set - wrong media status {:?}", media
.status()),
694 /// Generate a human readable name for the media set
695 pub fn generate_media_set_name(
697 media_set_uuid
: &Uuid
,
698 template
: Option
<String
>,
699 ) -> Result
<String
, Error
> {
700 self.inventory
.generate_media_set_name(media_set_uuid
, template
)
707 /// Combines 'MediaId' with 'MediaLocation' and 'MediaStatus'
709 #[derive(Debug,Serialize,Deserialize,Clone)]
710 pub struct BackupMedia
{
714 location
: MediaLocation
,
721 /// Creates a new instance
722 pub fn with_media_id(
724 location
: MediaLocation
,
727 Self { id, location, status }
730 /// Returns the media location
731 pub fn location(&self) -> &MediaLocation
{
735 /// Returns the media status
736 pub fn status(&self) -> &MediaStatus
{
740 /// Returns the media uuid
741 pub fn uuid(&self) -> &Uuid
{
745 /// Returns the media set label
746 pub fn media_set_label(&self) -> Option
<&MediaSetLabel
> {
747 self.id
.media_set_label
.as_ref()
750 /// Returns the media creation time
751 pub fn ctime(&self) -> i64 {
755 /// Updates the media set label
756 pub fn set_media_set_label(&mut self, set_label
: MediaSetLabel
) {
757 self.id
.media_set_label
= Some(set_label
);
760 /// Returns the drive label
761 pub fn label(&self) -> &MediaLabel
{
765 /// Returns the media id (drive label + media set label)
766 pub fn id(&self) -> &MediaId
{
770 /// Returns the media id, consumes self)
771 pub fn into_id(self) -> MediaId
{
775 /// Returns the media label (Barcode)
776 pub fn label_text(&self) -> &str {
777 &self.id
.label
.label_text