]> git.proxmox.com Git - proxmox-backup.git/blame - src/tape/media_pool.rs
bump version to 1.0.9-1
[proxmox-backup.git] / src / tape / media_pool.rs
CommitLineData
c4d8542e
DM
1//! Media Pool
2//!
3//! A set of backup medias.
4//!
5//! This struct manages backup media state during backup. The main
7bb720cb 6//! purpose is to allocate media sets and assing new tapes to it.
c4d8542e
DM
7//!
8//!
9
10use std::path::Path;
11use anyhow::{bail, Error};
12use ::serde::{Deserialize, Serialize};
13
14use proxmox::tools::Uuid;
15
16use crate::{
8a0046f5 17 backup::Fingerprint,
c4d8542e
DM
18 api2::types::{
19 MediaStatus,
c1c2c8f6 20 MediaLocation,
c4d8542e
DM
21 MediaSetPolicy,
22 RetentionPolicy,
23 MediaPoolConfig,
24 },
25 tools::systemd::time::compute_next_event,
26 tape::{
27 MediaId,
28 MediaSet,
c4d8542e 29 Inventory,
c4d8542e 30 file_formats::{
a78348ac 31 MediaLabel,
c4d8542e
DM
32 MediaSetLabel,
33 },
34 }
35};
36
9839d3f7 37/// Media Pool lock guard
c4d8542e
DM
38pub struct MediaPoolLockGuard(std::fs::File);
39
40/// Media Pool
41pub struct MediaPool {
42
43 name: String,
44
45 media_set_policy: MediaSetPolicy,
46 retention: RetentionPolicy,
cdf39e62
DM
47
48 changer_name: Option<String>,
ab77d660 49 force_media_availability: bool,
cdf39e62 50
8a0046f5 51 encrypt_fingerprint: Option<Fingerprint>,
c4d8542e
DM
52
53 inventory: Inventory,
c4d8542e
DM
54
55 current_media_set: MediaSet,
56}
57
58impl MediaPool {
59
60 /// Creates a new instance
cdf39e62
DM
61 ///
62 /// If you specify a `changer_name`, only media accessible via
63 /// that changer is considered available. If you pass `None` for
64 /// `changer`, all offline media is considered available (backups
65 /// to standalone drives may not use media from inside a tape
66 /// library).
c4d8542e
DM
67 pub fn new(
68 name: &str,
69 state_path: &Path,
70 media_set_policy: MediaSetPolicy,
71 retention: RetentionPolicy,
cdf39e62 72 changer_name: Option<String>,
8a0046f5 73 encrypt_fingerprint: Option<Fingerprint>,
c4d8542e
DM
74 ) -> Result<Self, Error> {
75
76 let inventory = Inventory::load(state_path)?;
77
78 let current_media_set = match inventory.latest_media_set(name) {
79 Some(set_uuid) => inventory.compute_media_set_members(&set_uuid)?,
80 None => MediaSet::new(),
81 };
82
c4d8542e
DM
83 Ok(MediaPool {
84 name: String::from(name),
85 media_set_policy,
86 retention,
cdf39e62 87 changer_name,
c4d8542e 88 inventory,
c4d8542e 89 current_media_set,
8a0046f5 90 encrypt_fingerprint,
ab77d660 91 force_media_availability: false,
c4d8542e
DM
92 })
93 }
94
ab77d660
DM
95 /// Pretend all Online(x) and Offline media is available
96 ///
97 /// Only media in Vault(y) is considered unavailable.
98 pub fn force_media_availability(&mut self) {
99 self.force_media_availability = true;
100 }
101
90e16be3
DM
102 /// Returns the Uuid of the current media set
103 pub fn current_media_set(&self) -> &Uuid {
104 self.current_media_set.uuid()
105 }
106
c4d8542e
DM
107 /// Creates a new instance using the media pool configuration
108 pub fn with_config(
c4d8542e
DM
109 state_path: &Path,
110 config: &MediaPoolConfig,
cdf39e62 111 changer_name: Option<String>,
c4d8542e
DM
112 ) -> Result<Self, Error> {
113
e062ebbc 114 let allocation = config.allocation.clone().unwrap_or_else(|| String::from("continue")).parse()?;
c4d8542e 115
e062ebbc 116 let retention = config.retention.clone().unwrap_or_else(|| String::from("keep")).parse()?;
c4d8542e 117
8a0046f5
DM
118 let encrypt_fingerprint = match config.encrypt {
119 Some(ref fingerprint) => Some(fingerprint.parse()?),
120 None => None,
121 };
122
123 MediaPool::new(
124 &config.name,
125 state_path,
126 allocation,
127 retention,
cdf39e62 128 changer_name,
8a0046f5
DM
129 encrypt_fingerprint,
130 )
c4d8542e
DM
131 }
132
133 /// Returns the pool name
134 pub fn name(&self) -> &str {
135 &self.name
136 }
137
8a0046f5
DM
138 /// Retruns encryption settings
139 pub fn encrypt_fingerprint(&self) -> Option<Fingerprint> {
140 self.encrypt_fingerprint.clone()
141 }
142
25350f33
DM
143 pub fn set_media_status_damaged(&mut self, uuid: &Uuid) -> Result<(), Error> {
144 self.inventory.set_media_status_damaged(uuid)
145 }
8a0046f5 146
c4d8542e
DM
147 fn compute_media_state(&self, media_id: &MediaId) -> (MediaStatus, MediaLocation) {
148
cfae8f06 149 let (status, location) = self.inventory.status_and_location(&media_id.label.uuid);
c4d8542e
DM
150
151 match status {
152 MediaStatus::Full | MediaStatus::Damaged | MediaStatus::Retired => {
153 return (status, location);
154 }
155 MediaStatus::Unknown | MediaStatus::Writable => {
156 /* possibly writable - fall through to check */
157 }
158 }
159
160 let set = match media_id.media_set_label {
161 None => return (MediaStatus::Writable, location), // not assigned to any pool
162 Some(ref set) => set,
163 };
164
165 if set.pool != self.name { // should never trigger
166 return (MediaStatus::Unknown, location); // belong to another pool
167 }
168 if set.uuid.as_ref() == [0u8;16] { // not assigned to any pool
169 return (MediaStatus::Writable, location);
170 }
171
172 if &set.uuid != self.current_media_set.uuid() {
173 return (MediaStatus::Full, location); // assume FULL
174 }
175
176 // media is member of current set
177 if self.current_media_set.is_last_media(&media_id.label.uuid) {
178 (MediaStatus::Writable, location) // last set member is writable
179 } else {
180 (MediaStatus::Full, location)
181 }
182 }
183
184 /// Returns the 'MediaId' with associated state
185 pub fn lookup_media(&self, uuid: &Uuid) -> Result<BackupMedia, Error> {
186 let media_id = match self.inventory.lookup_media(uuid) {
187 None => bail!("unable to lookup media {}", uuid),
188 Some(media_id) => media_id.clone(),
189 };
190
191 if let Some(ref set) = media_id.media_set_label {
192 if set.pool != self.name {
193 bail!("media does not belong to pool ({} != {})", set.pool, self.name);
194 }
195 }
196
197 let (status, location) = self.compute_media_state(&media_id);
198
199 Ok(BackupMedia::with_media_id(
200 media_id,
201 location,
202 status,
203 ))
204 }
205
206 /// List all media associated with this pool
207 pub fn list_media(&self) -> Vec<BackupMedia> {
208 let media_id_list = self.inventory.list_pool_media(&self.name);
209
210 media_id_list.into_iter()
211 .map(|media_id| {
212 let (status, location) = self.compute_media_state(&media_id);
213 BackupMedia::with_media_id(
214 media_id,
215 location,
216 status,
217 )
218 })
219 .collect()
220 }
221
222 /// Set media status to FULL.
223 pub fn set_media_status_full(&mut self, uuid: &Uuid) -> Result<(), Error> {
224 let media = self.lookup_media(uuid)?; // check if media belongs to this pool
225 if media.status() != &MediaStatus::Full {
cfae8f06 226 self.inventory.set_media_status_full(uuid)?;
c4d8542e
DM
227 }
228 Ok(())
229 }
230
231 /// Make sure the current media set is usable for writing
232 ///
233 /// If not, starts a new media set. Also creates a new
234 /// set if media_set_policy implies it.
ab77d660
DM
235 ///
236 /// Note: We also call this in list_media to compute correct media
237 /// status, so this must not change persistent/saved state.
90e16be3
DM
238 ///
239 /// Returns the reason why we started a new media set (if we do)
240 pub fn start_write_session(&mut self, current_time: i64) -> Result<Option<String>, Error> {
241
242 let mut create_new_set = match self.current_set_usable() {
243 Err(err) => {
244 Some(err.to_string())
245 }
246 Ok(_) => None,
c4d8542e
DM
247 };
248
90e16be3 249 if create_new_set.is_none() {
c4d8542e
DM
250 match &self.media_set_policy {
251 MediaSetPolicy::AlwaysCreate => {
90e16be3 252 create_new_set = Some(String::from("policy is AlwaysCreate"));
c4d8542e
DM
253 }
254 MediaSetPolicy::CreateAt(event) => {
255 if let Some(set_start_time) = self.inventory.media_set_start_time(&self.current_media_set.uuid()) {
256 if let Ok(Some(alloc_time)) = compute_next_event(event, set_start_time as i64, false) {
90e16be3
DM
257 if current_time >= alloc_time {
258 create_new_set = Some(String::from("policy CreateAt event triggered"));
c4d8542e
DM
259 }
260 }
261 }
262 }
263 MediaSetPolicy::ContinueCurrent => { /* do nothing here */ }
264 }
265 }
266
90e16be3 267 if create_new_set.is_some() {
c4d8542e 268 let media_set = MediaSet::new();
c4d8542e
DM
269 self.current_media_set = media_set;
270 }
271
90e16be3 272 Ok(create_new_set)
c4d8542e
DM
273 }
274
275 /// List media in current media set
276 pub fn current_media_list(&self) -> Result<Vec<&Uuid>, Error> {
277 let mut list = Vec::new();
278 for opt_uuid in self.current_media_set.media_list().iter() {
279 match opt_uuid {
280 Some(ref uuid) => list.push(uuid),
281 None => bail!("current_media_list failed - media set is incomplete"),
282 }
283 }
284 Ok(list)
285 }
286
287 // tests if the media data is considered as expired at sepcified time
288 pub fn media_is_expired(&self, media: &BackupMedia, current_time: i64) -> bool {
289 if media.status() != &MediaStatus::Full {
290 return false;
291 }
292
293 let expire_time = self.inventory.media_expire_time(
294 media.id(), &self.media_set_policy, &self.retention);
295
1bed3aed 296 current_time >= expire_time
c4d8542e
DM
297 }
298
8a0046f5
DM
299 // check if a location is considered on site
300 pub fn location_is_available(&self, location: &MediaLocation) -> bool {
301 match location {
cdf39e62 302 MediaLocation::Online(name) => {
ab77d660
DM
303 if self.force_media_availability {
304 true
cdf39e62 305 } else {
ab77d660
DM
306 if let Some(ref changer_name) = self.changer_name {
307 name == changer_name
308 } else {
309 // a standalone drive cannot use media currently inside a library
310 false
311 }
cdf39e62
DM
312 }
313 }
314 MediaLocation::Offline => {
ab77d660
DM
315 if self.force_media_availability {
316 true
317 } else {
318 // consider available for standalone drives
319 self.changer_name.is_none()
320 }
cdf39e62 321 }
8a0046f5
DM
322 MediaLocation::Vault(_) => false,
323 }
324 }
325
326 fn add_media_to_current_set(&mut self, mut media_id: MediaId, current_time: i64) -> Result<(), Error> {
327
328 let seq_nr = self.current_media_set.media_list().len() as u64;
329
330 let pool = self.name.clone();
331
332 let encrypt_fingerprint = self.encrypt_fingerprint();
333
334 let set = MediaSetLabel::with_data(
335 &pool,
336 self.current_media_set.uuid().clone(),
337 seq_nr,
338 current_time,
339 encrypt_fingerprint,
340 );
341
342 media_id.media_set_label = Some(set);
343
344 let uuid = media_id.label.uuid.clone();
345
346 let clear_media_status = true; // remove Full status
347 self.inventory.store(media_id, clear_media_status)?; // store persistently
348
349 self.current_media_set.add_media(uuid);
350
351 Ok(())
352 }
353
354
c4d8542e
DM
355 /// Allocates a writable media to the current media set
356 pub fn alloc_writable_media(&mut self, current_time: i64) -> Result<Uuid, Error> {
357
358 let last_is_writable = self.current_set_usable()?;
359
c4d8542e
DM
360 if last_is_writable {
361 let last_uuid = self.current_media_set.last_media_uuid().unwrap();
362 let media = self.lookup_media(last_uuid)?;
363 return Ok(media.uuid().clone());
364 }
365
366 // try to find empty media in pool, add to media set
367
8a0046f5 368 let media_list = self.list_media();
c4d8542e
DM
369
370 let mut empty_media = Vec::new();
8a0046f5
DM
371 let mut used_media = Vec::new();
372
373 for media in media_list.into_iter() {
374 if !self.location_is_available(media.location()) {
375 continue;
376 }
c4d8542e 377 // already part of a media set?
8a0046f5
DM
378 if media.media_set_label().is_some() {
379 used_media.push(media);
380 } else {
381 // only consider writable empty media
382 if media.status() == &MediaStatus::Writable {
383 empty_media.push(media);
384 }
c4d8542e 385 }
c4d8542e
DM
386 }
387
8a0046f5
DM
388 // sort empty_media, newest first -> oldest last
389 empty_media.sort_unstable_by(|a, b| b.label().ctime.cmp(&a.label().ctime));
c4d8542e 390
8a0046f5 391 if let Some(media) = empty_media.pop() {
c4d8542e 392 // found empty media, add to media set an use it
8a0046f5
DM
393 let uuid = media.uuid().clone();
394 self.add_media_to_current_set(media.into_id(), current_time)?;
395 return Ok(uuid);
c4d8542e
DM
396 }
397
398 println!("no empty media in pool, try to reuse expired media");
399
400 let mut expired_media = Vec::new();
401
8a0046f5 402 for media in used_media.into_iter() {
c4d8542e
DM
403 if let Some(set) = media.media_set_label() {
404 if &set.uuid == self.current_media_set.uuid() {
405 continue;
406 }
8a0046f5
DM
407 } else {
408 continue;
c4d8542e 409 }
8a0046f5 410
c4d8542e 411 if self.media_is_expired(&media, current_time) {
8446fbca 412 println!("found expired media on media '{}'", media.label_text());
c4d8542e
DM
413 expired_media.push(media);
414 }
415 }
416
8a0046f5
DM
417 // sort expired_media, newest first -> oldest last
418 expired_media.sort_unstable_by(|a, b| {
419 b.media_set_label().unwrap().ctime.cmp(&a.media_set_label().unwrap().ctime)
c4d8542e
DM
420 });
421
8a0046f5 422 if let Some(media) = expired_media.pop() {
25e464c5 423 println!("reuse expired media '{}'", media.label_text());
8a0046f5
DM
424 let uuid = media.uuid().clone();
425 self.add_media_to_current_set(media.into_id(), current_time)?;
426 return Ok(uuid);
25e464c5 427 }
c4d8542e 428
25e464c5 429 println!("no expired media in pool, try to find unassigned/free media");
c4d8542e 430
25e464c5
DM
431 // try unassigned media
432 // fixme: lock free media pool to avoid races
433 let mut free_media = Vec::new();
434
435 for media_id in self.inventory.list_unassigned_media() {
436
437 let (status, location) = self.compute_media_state(&media_id);
438 if media_id.media_set_label.is_some() { continue; } // should not happen
439
8a0046f5
DM
440 if !self.location_is_available(&location) {
441 continue;
c4d8542e 442 }
25e464c5
DM
443
444 // only consider writable media
445 if status != MediaStatus::Writable { continue; }
446
447 free_media.push(media_id);
448 }
449
8a0046f5
DM
450 if let Some(media_id) = free_media.pop() {
451 println!("use free media '{}'", media_id.label.label_text);
452 let uuid = media_id.label.uuid.clone();
453 self.add_media_to_current_set(media_id, current_time)?;
454 return Ok(uuid);
c4d8542e 455 }
25e464c5 456
25e464c5 457 bail!("alloc writable media in pool '{}' failed: no usable media found", self.name());
c4d8542e
DM
458 }
459
460 /// check if the current media set is usable for writing
461 ///
462 /// This does several consistency checks, and return if
463 /// the last media in the current set is in writable state.
464 ///
465 /// This return error when the media set must not be used any
466 /// longer because of consistency errors.
467 pub fn current_set_usable(&self) -> Result<bool, Error> {
468
6dd05135
DM
469 let media_list = self.current_media_set.media_list();
470
471 let media_count = media_list.len();
c4d8542e
DM
472 if media_count == 0 {
473 return Ok(false);
474 }
475
476 let set_uuid = self.current_media_set.uuid();
477 let mut last_is_writable = false;
478
6dd05135
DM
479 let mut last_enc: Option<Option<Fingerprint>> = None;
480
481 for (seq, opt_uuid) in media_list.iter().enumerate() {
c4d8542e
DM
482 let uuid = match opt_uuid {
483 None => bail!("media set is incomplete (missing media information)"),
484 Some(uuid) => uuid,
485 };
486 let media = self.lookup_media(uuid)?;
487 match media.media_set_label() {
488 Some(MediaSetLabel { seq_nr, uuid, ..}) if *seq_nr == seq as u64 && uuid == set_uuid => { /* OK */ },
489 Some(MediaSetLabel { seq_nr, uuid, ..}) if uuid == set_uuid => {
490 bail!("media sequence error ({} != {})", *seq_nr, seq);
491 },
492 Some(MediaSetLabel { uuid, ..}) => bail!("media owner error ({} != {}", uuid, set_uuid),
493 None => bail!("media owner error (no owner)"),
494 }
6dd05135
DM
495
496 if let Some(set) = media.media_set_label() { // always true here
497 if set.encryption_key_fingerprint != self.encrypt_fingerprint {
498 bail!("pool encryption key changed");
499 }
500 match last_enc {
501 None => {
502 last_enc = Some(set.encryption_key_fingerprint.clone());
503 }
504 Some(ref last_enc) => {
505 if last_enc != &set.encryption_key_fingerprint {
506 bail!("inconsistent media encryption key");
507 }
508 }
509 }
510 }
511
c4d8542e
DM
512 match media.status() {
513 MediaStatus::Full => { /* OK */ },
514 MediaStatus::Writable if (seq + 1) == media_count => {
b81e37f6
DM
515 let media_location = media.location();
516 if self.location_is_available(media_location) {
517 last_is_writable = true;
518 } else {
519 if let MediaLocation::Vault(vault) = media_location {
c4d8542e
DM
520 bail!("writable media offsite in vault '{}'", vault);
521 }
522 }
523 },
524 _ => bail!("unable to use media set - wrong media status {:?}", media.status()),
525 }
526 }
ab77d660 527
c4d8542e
DM
528 Ok(last_is_writable)
529 }
530
531 /// Generate a human readable name for the media set
532 pub fn generate_media_set_name(
533 &self,
534 media_set_uuid: &Uuid,
535 template: Option<String>,
536 ) -> Result<String, Error> {
537 self.inventory.generate_media_set_name(media_set_uuid, template)
538 }
539
540 /// Lock the pool
541 pub fn lock(base_path: &Path, name: &str) -> Result<MediaPoolLockGuard, Error> {
542 let mut path = base_path.to_owned();
543 path.push(format!(".{}", name));
544 path.set_extension("lck");
545
546 let timeout = std::time::Duration::new(10, 0);
547 let lock = proxmox::tools::fs::open_file_locked(&path, timeout, true)?;
548
549 Ok(MediaPoolLockGuard(lock))
550 }
551}
552
553/// Backup media
554///
555/// Combines 'MediaId' with 'MediaLocation' and 'MediaStatus'
556/// information.
557#[derive(Debug,Serialize,Deserialize,Clone)]
558pub struct BackupMedia {
559 /// Media ID
560 id: MediaId,
561 /// Media location
562 location: MediaLocation,
563 /// Media status
564 status: MediaStatus,
565}
566
567impl BackupMedia {
568
569 /// Creates a new instance
570 pub fn with_media_id(
571 id: MediaId,
572 location: MediaLocation,
573 status: MediaStatus,
574 ) -> Self {
575 Self { id, location, status }
576 }
577
578 /// Returns the media location
579 pub fn location(&self) -> &MediaLocation {
580 &self.location
581 }
582
583 /// Returns the media status
584 pub fn status(&self) -> &MediaStatus {
585 &self.status
586 }
587
588 /// Returns the media uuid
589 pub fn uuid(&self) -> &Uuid {
590 &self.id.label.uuid
591 }
592
593 /// Returns the media set label
6543214d
DM
594 pub fn media_set_label(&self) -> Option<&MediaSetLabel> {
595 self.id.media_set_label.as_ref()
596 }
597
598 /// Returns the media creation time
599 pub fn ctime(&self) -> i64 {
600 self.id.label.ctime
c4d8542e
DM
601 }
602
603 /// Updates the media set label
604 pub fn set_media_set_label(&mut self, set_label: MediaSetLabel) {
605 self.id.media_set_label = Some(set_label);
606 }
54f4ecd4 607
c4d8542e 608 /// Returns the drive label
a78348ac 609 pub fn label(&self) -> &MediaLabel {
c4d8542e
DM
610 &self.id.label
611 }
612
613 /// Returns the media id (drive label + media set label)
614 pub fn id(&self) -> &MediaId {
615 &self.id
616 }
617
8a0046f5
DM
618 /// Returns the media id, consumes self)
619 pub fn into_id(self) -> MediaId {
620 self.id
621 }
622
c4d8542e 623 /// Returns the media label (Barcode)
8446fbca
DM
624 pub fn label_text(&self) -> &str {
625 &self.id.label.label_text
c4d8542e
DM
626 }
627}