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