]> git.proxmox.com Git - proxmox-backup.git/blame - src/tape/inventory.rs
tree-wide: fix needless borrows
[proxmox-backup.git] / src / tape / inventory.rs
CommitLineData
7320e9ff
DM
1//! Backup media Inventory
2//!
3//! The Inventory persistently stores the list of known backup
4//! media. A backup media is identified by its 'MediaId', which is the
a78348ac 5//! MediaLabel/MediaSetLabel combination.
30316192
DM
6//!
7//! Inventory Locking
8//!
9//! The inventory itself has several methods to update single entries,
10//! but all of them can be considered atomic.
11//!
12//! Pool Locking
13//!
14//! To add/modify media assigned to a pool, we always do
15//! lock_media_pool(). For unassigned media, we call
16//! lock_unassigned_media_pool().
17//!
18//! MediaSet Locking
19//!
20//! To add/remove media from a media set, or to modify catalogs we
21//! always do lock_media_set(). Also, we aquire this lock during
22//! restore, to make sure it is not reused for backups.
23//!
7320e9ff
DM
24
25use std::collections::{HashMap, BTreeMap};
26use std::path::{Path, PathBuf};
30316192 27use std::time::Duration;
7320e9ff
DM
28
29use anyhow::{bail, Error};
30use serde::{Serialize, Deserialize};
31use serde_json::json;
32
25877d05 33use proxmox_sys::fs::{replace_file, file_get_json, CreateOptions};
6ef1b649 34use proxmox_uuid::Uuid;
7320e9ff 35
f4680641 36use pbs_config::BackupLockGuard;
6227654a 37use pbs_api_types::{MediaSetPolicy, RetentionPolicy, MediaStatus, MediaLocation};
f4680641
DC
38
39#[cfg(not(test))]
40use pbs_config::open_backup_lockfile;
41
42#[cfg(test)]
43fn open_backup_lockfile<P: AsRef<std::path::Path>>(
44 _path: P,
45 _timeout: Option<std::time::Duration>,
46 _exclusive: bool,
47) -> Result<pbs_config::BackupLockGuard, anyhow::Error> {
48 Ok(unsafe { pbs_config::create_mocked_lock() })
49}
50
dd2162f6 51
7320e9ff 52use crate::{
7320e9ff 53 tape::{
cafd51bf 54 TAPE_STATUS_DIR,
c7926d8e 55 MediaSet,
13f435ca 56 MediaCatalog,
7320e9ff 57 file_formats::{
a78348ac 58 MediaLabel,
7320e9ff
DM
59 MediaSetLabel,
60 },
37796ff7 61 changer::OnlineStatusMap,
7320e9ff
DM
62 },
63};
64
65/// Unique Media Identifier
66///
67/// This combines the label and media set label.
68#[derive(Debug,Serialize,Deserialize,Clone)]
69pub struct MediaId {
a78348ac 70 pub label: MediaLabel,
7320e9ff
DM
71 #[serde(skip_serializing_if="Option::is_none")]
72 pub media_set_label: Option<MediaSetLabel>,
73}
74
7320e9ff 75
cfae8f06
DM
76#[derive(Serialize,Deserialize)]
77struct MediaStateEntry {
78 id: MediaId,
79 #[serde(skip_serializing_if="Option::is_none")]
80 location: Option<MediaLocation>,
81 #[serde(skip_serializing_if="Option::is_none")]
82 status: Option<MediaStatus>,
83}
84
7320e9ff
DM
85/// Media Inventory
86pub struct Inventory {
cfae8f06 87 map: BTreeMap<Uuid, MediaStateEntry>,
7320e9ff
DM
88
89 inventory_path: PathBuf,
90 lockfile_path: PathBuf,
91
92 // helpers
93 media_set_start_times: HashMap<Uuid, i64>
94}
95
96impl Inventory {
97
98 pub const MEDIA_INVENTORY_FILENAME: &'static str = "inventory.json";
99 pub const MEDIA_INVENTORY_LOCKFILE: &'static str = ".inventory.lck";
100
30316192
DM
101 /// Create empty instance, no data loaded
102 pub fn new(base_path: &Path) -> Self {
7320e9ff
DM
103
104 let mut inventory_path = base_path.to_owned();
105 inventory_path.push(Self::MEDIA_INVENTORY_FILENAME);
106
107 let mut lockfile_path = base_path.to_owned();
108 lockfile_path.push(Self::MEDIA_INVENTORY_LOCKFILE);
109
110 Self {
111 map: BTreeMap::new(),
112 media_set_start_times: HashMap::new(),
113 inventory_path,
114 lockfile_path,
115 }
116 }
117
118 pub fn load(base_path: &Path) -> Result<Self, Error> {
119 let mut me = Self::new(base_path);
120 me.reload()?;
121 Ok(me)
122 }
123
124 /// Reload the database
125 pub fn reload(&mut self) -> Result<(), Error> {
126 self.map = Self::load_media_db(&self.inventory_path)?;
127 self.update_helpers();
128 Ok(())
129 }
130
131 fn update_helpers(&mut self) {
132
133 // recompute media_set_start_times
134
135 let mut set_start_times = HashMap::new();
136
cfae8f06
DM
137 for entry in self.map.values() {
138 let set = match &entry.id.media_set_label {
7320e9ff
DM
139 None => continue,
140 Some(set) => set,
141 };
142 if set.seq_nr == 0 {
143 set_start_times.insert(set.uuid.clone(), set.ctime);
144 }
145 }
146
147 self.media_set_start_times = set_start_times;
148 }
149
150 /// Lock the database
7526d864
DM
151 fn lock(&self) -> Result<BackupLockGuard, Error> {
152 open_backup_lockfile(&self.lockfile_path, None, true)
7320e9ff
DM
153 }
154
cfae8f06 155 fn load_media_db(path: &Path) -> Result<BTreeMap<Uuid, MediaStateEntry>, Error> {
7320e9ff
DM
156
157 let data = file_get_json(path, Some(json!([])))?;
cfae8f06 158 let media_list: Vec<MediaStateEntry> = serde_json::from_value(data)?;
7320e9ff
DM
159
160 let mut map = BTreeMap::new();
cfae8f06
DM
161 for entry in media_list.into_iter() {
162 map.insert(entry.id.label.uuid.clone(), entry);
7320e9ff
DM
163 }
164
165 Ok(map)
166 }
167
168 fn replace_file(&self) -> Result<(), Error> {
cfae8f06 169 let list: Vec<&MediaStateEntry> = self.map.values().collect();
7320e9ff 170 let raw = serde_json::to_string_pretty(&serde_json::to_value(list)?)?;
cafd51bf 171
cafd51bf 172 let mode = nix::sys::stat::Mode::from_bits_truncate(0o0640);
2f8809c6
DM
173
174 let options = if cfg!(test) {
175 // We cannot use chown inside test environment (no permissions)
176 CreateOptions::new().perm(mode)
177 } else {
21211748 178 let backup_user = pbs_config::backup_user()?;
2f8809c6
DM
179 CreateOptions::new()
180 .perm(mode)
181 .owner(backup_user.uid)
182 .group(backup_user.gid)
183 };
cafd51bf 184
e0a19d33 185 replace_file(&self.inventory_path, raw.as_bytes(), options, true)?;
cafd51bf 186
7320e9ff
DM
187 Ok(())
188 }
189
190 /// Stores a single MediaID persistently
cfae8f06
DM
191 pub fn store(
192 &mut self,
193 mut media_id: MediaId,
194 clear_media_status: bool,
195 ) -> Result<(), Error> {
7320e9ff
DM
196 let _lock = self.lock()?;
197 self.map = Self::load_media_db(&self.inventory_path)?;
198
cfae8f06
DM
199 let uuid = media_id.label.uuid.clone();
200
201 if let Some(previous) = self.map.remove(&media_id.label.uuid) {
202 // do not overwrite unsaved pool assignments
203 if media_id.media_set_label.is_none() {
204 if let Some(ref set) = previous.id.media_set_label {
7320e9ff
DM
205 if set.uuid.as_ref() == [0u8;16] {
206 media_id.media_set_label = Some(set.clone());
207 }
208 }
209 }
cfae8f06
DM
210 let entry = MediaStateEntry {
211 id: media_id,
212 location: previous.location,
213 status: if clear_media_status {
214 None
215 } else {
216 previous.status
217 },
218 };
219 self.map.insert(uuid, entry);
220 } else {
221 let entry = MediaStateEntry { id: media_id, location: None, status: None };
222 self.map.insert(uuid, entry);
7320e9ff
DM
223 }
224
7320e9ff
DM
225 self.update_helpers();
226 self.replace_file()?;
227 Ok(())
228 }
229
fb657d8e
DM
230 /// Remove a single media persistently
231 pub fn remove_media(&mut self, uuid: &Uuid) -> Result<(), Error> {
232 let _lock = self.lock()?;
233 self.map = Self::load_media_db(&self.inventory_path)?;
234 self.map.remove(uuid);
235 self.update_helpers();
236 self.replace_file()?;
237 Ok(())
238 }
239
7320e9ff
DM
240 /// Lookup media
241 pub fn lookup_media(&self, uuid: &Uuid) -> Option<&MediaId> {
cfae8f06 242 self.map.get(uuid).map(|entry| &entry.id)
7320e9ff
DM
243 }
244
d984a9ac
DM
245 /// List all media Uuids
246 pub fn media_list(&self) -> Vec<&Uuid> {
247 self.map.keys().collect()
248 }
249
8446fbca
DM
250 /// find media by label_text
251 pub fn find_media_by_label_text(&self, label_text: &str) -> Option<&MediaId> {
f2f81791 252 self.map.values().find_map(|entry| {
8446fbca 253 if entry.id.label.label_text == label_text {
f2f81791
FG
254 Some(&entry.id)
255 } else {
256 None
7320e9ff 257 }
f2f81791 258 })
7320e9ff
DM
259 }
260
261 /// Lookup media pool
262 ///
263 /// Returns (pool, is_empty)
264 pub fn lookup_media_pool(&self, uuid: &Uuid) -> Option<(&str, bool)> {
265 match self.map.get(uuid) {
266 None => None,
cfae8f06
DM
267 Some(entry) => {
268 match entry.id.media_set_label {
7320e9ff
DM
269 None => None, // not assigned to any pool
270 Some(ref set) => {
271 let is_empty = set.uuid.as_ref() == [0u8;16];
272 Some((&set.pool, is_empty))
273 }
274 }
275 }
276 }
277 }
278
279 /// List all media assigned to the pool
280 pub fn list_pool_media(&self, pool: &str) -> Vec<MediaId> {
281 let mut list = Vec::new();
282
f2f81791 283 for entry in self.map.values() {
cfae8f06 284 match entry.id.media_set_label {
7320e9ff
DM
285 None => continue, // not assigned to any pool
286 Some(ref set) => {
287 if set.pool != pool {
288 continue; // belong to another pool
289 }
290
af762341 291 if set.uuid.as_ref() == [0u8;16] {
7320e9ff 292 list.push(MediaId {
cfae8f06 293 label: entry.id.label.clone(),
7320e9ff
DM
294 media_set_label: None,
295 })
296 } else {
cfae8f06 297 list.push(entry.id.clone());
7320e9ff
DM
298 }
299 }
300 }
7320e9ff
DM
301 }
302
303 list
304 }
305
306 /// List all used media
307 pub fn list_used_media(&self) -> Vec<MediaId> {
308 let mut list = Vec::new();
309
f2f81791 310 for entry in self.map.values() {
cfae8f06 311 match entry.id.media_set_label {
7320e9ff
DM
312 None => continue, // not assigned to any pool
313 Some(ref set) => {
314 if set.uuid.as_ref() != [0u8;16] {
cfae8f06 315 list.push(entry.id.clone());
7320e9ff
DM
316 }
317 }
318 }
319 }
320
321 list
322 }
323
324 /// List media not assigned to any pool
325 pub fn list_unassigned_media(&self) -> Vec<MediaId> {
f2f81791 326 self.map.values().filter_map(|entry|
cfae8f06 327 if entry.id.media_set_label.is_none() {
f2f81791
FG
328 Some(entry.id.clone())
329 } else {
330 None
7320e9ff 331 }
f2f81791 332 ).collect()
7320e9ff
DM
333 }
334
335 pub fn media_set_start_time(&self, media_set_uuid: &Uuid) -> Option<i64> {
a375df6f 336 self.media_set_start_times.get(media_set_uuid).copied()
7320e9ff
DM
337 }
338
b4772d1c
DM
339 /// Lookup media set pool
340 pub fn lookup_media_set_pool(&self, media_set_uuid: &Uuid) -> Result<String, Error> {
341
342 let mut last_pool = None;
343
cfae8f06
DM
344 for entry in self.map.values() {
345 match entry.id.media_set_label {
b4772d1c
DM
346 None => continue,
347 Some(MediaSetLabel { ref uuid, .. }) => {
348 if uuid != media_set_uuid {
349 continue;
350 }
cfae8f06 351 if let Some((pool, _)) = self.lookup_media_pool(&entry.id.label.uuid) {
b4772d1c
DM
352 if let Some(last_pool) = last_pool {
353 if last_pool != pool {
354 bail!("detected media set with inconsistent pool assignment - internal error");
355 }
356 } else {
357 last_pool = Some(pool);
358 }
359 }
360 }
361 }
362 }
363
364 match last_pool {
365 Some(pool) => Ok(pool.to_string()),
f197c286 366 None => bail!("media set {} is incomplete - unable to lookup pool", media_set_uuid),
b4772d1c
DM
367 }
368 }
369
7320e9ff
DM
370 /// Compute a single media sets
371 pub fn compute_media_set_members(&self, media_set_uuid: &Uuid) -> Result<MediaSet, Error> {
372
373 let mut set = MediaSet::with_data(media_set_uuid.clone(), Vec::new());
374
cfae8f06
DM
375 for entry in self.map.values() {
376 match entry.id.media_set_label {
7320e9ff
DM
377 None => continue,
378 Some(MediaSetLabel { seq_nr, ref uuid, .. }) => {
379 if uuid != media_set_uuid {
380 continue;
381 }
cfae8f06 382 set.insert_media(entry.id.label.uuid.clone(), seq_nr)?;
7320e9ff
DM
383 }
384 }
385 }
386
387 Ok(set)
388 }
389
390 /// Compute all media sets
391 pub fn compute_media_set_list(&self) -> Result<HashMap<Uuid, MediaSet>, Error> {
392
393 let mut set_map: HashMap<Uuid, MediaSet> = HashMap::new();
394
cfae8f06
DM
395 for entry in self.map.values() {
396 match entry.id.media_set_label {
7320e9ff
DM
397 None => continue,
398 Some(MediaSetLabel { seq_nr, ref uuid, .. }) => {
399
400 let set = set_map.entry(uuid.clone()).or_insert_with(|| {
401 MediaSet::with_data(uuid.clone(), Vec::new())
402 });
403
cfae8f06 404 set.insert_media(entry.id.label.uuid.clone(), seq_nr)?;
7320e9ff
DM
405 }
406 }
407 }
408
409 Ok(set_map)
410 }
411
412 /// Returns the latest media set for a pool
413 pub fn latest_media_set(&self, pool: &str) -> Option<Uuid> {
414
415 let mut last_set: Option<(Uuid, i64)> = None;
416
417 let set_list = self.map.values()
cfae8f06 418 .filter_map(|entry| entry.id.media_set_label.as_ref())
1d928b25 419 .filter(|set| set.pool == pool && set.uuid.as_ref() != [0u8;16]);
7320e9ff
DM
420
421 for set in set_list {
422 match last_set {
423 None => {
424 last_set = Some((set.uuid.clone(), set.ctime));
425 }
426 Some((_, last_ctime)) => {
427 if set.ctime > last_ctime {
428 last_set = Some((set.uuid.clone(), set.ctime));
429 }
430 }
431 }
432 }
433
434 let (uuid, ctime) = match last_set {
435 None => return None,
436 Some((uuid, ctime)) => (uuid, ctime),
437 };
438
439 // consistency check - must be the only set with that ctime
440 let set_list = self.map.values()
cfae8f06 441 .filter_map(|entry| entry.id.media_set_label.as_ref())
1d928b25 442 .filter(|set| set.pool == pool && set.uuid.as_ref() != [0u8;16]);
7320e9ff
DM
443
444 for set in set_list {
445 if set.uuid != uuid && set.ctime >= ctime { // should not happen
446 eprintln!("latest_media_set: found set with equal ctime ({}, {})", set.uuid, uuid);
447 return None;
448 }
449 }
450
451 Some(uuid)
452 }
453
454 // Test if there is a media set (in the same pool) newer than this one.
455 // Return the ctime of the nearest media set
456 fn media_set_next_start_time(&self, media_set_uuid: &Uuid) -> Option<i64> {
457
458 let (pool, ctime) = match self.map.values()
cfae8f06 459 .filter_map(|entry| entry.id.media_set_label.as_ref())
7320e9ff
DM
460 .find_map(|set| {
461 if &set.uuid == media_set_uuid {
462 Some((set.pool.clone(), set.ctime))
463 } else {
464 None
465 }
466 }) {
467 Some((pool, ctime)) => (pool, ctime),
468 None => return None,
469 };
470
471 let set_list = self.map.values()
cfae8f06 472 .filter_map(|entry| entry.id.media_set_label.as_ref())
1d928b25 473 .filter(|set| (&set.uuid != media_set_uuid) && (set.pool == pool));
7320e9ff
DM
474
475 let mut next_ctime = None;
476
477 for set in set_list {
478 if set.ctime > ctime {
479 match next_ctime {
480 None => {
481 next_ctime = Some(set.ctime);
482 }
483 Some(last_next_ctime) => {
484 if set.ctime < last_next_ctime {
485 next_ctime = Some(set.ctime);
486 }
487 }
488 }
489 }
490 }
491
492 next_ctime
493 }
494
495 pub fn media_expire_time(
496 &self,
497 media: &MediaId,
498 media_set_policy: &MediaSetPolicy,
499 retention_policy: &RetentionPolicy,
500 ) -> i64 {
501
502 if let RetentionPolicy::KeepForever = retention_policy {
503 return i64::MAX;
504 }
505
506 let set = match media.media_set_label {
507 None => return i64::MAX,
508 Some(ref set) => set,
509 };
510
511 let set_start_time = match self.media_set_start_time(&set.uuid) {
512 None => {
513 // missing information, use ctime from this
514 // set (always greater than ctime from seq_nr 0)
515 set.ctime
516 }
517 Some(time) => time,
518 };
519
cd5d6103
DM
520 let max_use_time = match self.media_set_next_start_time(&set.uuid) {
521 Some(next_start_time) => {
522 match media_set_policy {
523 MediaSetPolicy::AlwaysCreate => set_start_time,
524 _ => next_start_time,
525 }
7320e9ff 526 }
cd5d6103
DM
527 None => {
528 match media_set_policy {
529 MediaSetPolicy::ContinueCurrent => {
530 return i64::MAX;
531 }
532 MediaSetPolicy::AlwaysCreate => {
533 set_start_time
534 }
535 MediaSetPolicy::CreateAt(ref event) => {
7549114c 536 match event.compute_next_event(set_start_time) {
cd5d6103
DM
537 Ok(Some(next)) => next,
538 Ok(None) | Err(_) => return i64::MAX,
539 }
540 }
7320e9ff
DM
541 }
542 }
543 };
544
545 match retention_policy {
546 RetentionPolicy::KeepForever => i64::MAX,
547 RetentionPolicy::OverwriteAlways => max_use_time,
548 RetentionPolicy::ProtectFor(time_span) => {
549 let seconds = f64::from(time_span.clone()) as i64;
550 max_use_time + seconds
551 }
552 }
553 }
554
555 /// Generate a human readable name for the media set
556 ///
557 /// The template can include strftime time format specifications.
558 pub fn generate_media_set_name(
559 &self,
560 media_set_uuid: &Uuid,
561 template: Option<String>,
562 ) -> Result<String, Error> {
563
564 if let Some(ctime) = self.media_set_start_time(media_set_uuid) {
e062ebbc 565 let mut template = template.unwrap_or_else(|| String::from("%c"));
7320e9ff 566 template = template.replace("%id%", &media_set_uuid.to_string());
6ef1b649 567 Ok(proxmox_time::strftime_local(&template, ctime)?)
7320e9ff
DM
568 } else {
569 // We don't know the set start time, so we cannot use the template
570 Ok(media_set_uuid.to_string())
571 }
572 }
573
574 // Helpers to simplify testing
575
d1d74c43 576 /// Generate and insert a new free tape (test helper)
8446fbca 577 pub fn generate_free_tape(&mut self, label_text: &str, ctime: i64) -> Uuid {
7320e9ff 578
a78348ac 579 let label = MediaLabel {
8446fbca 580 label_text: label_text.to_string(),
7320e9ff
DM
581 uuid: Uuid::generate(),
582 ctime,
583 };
584 let uuid = label.uuid.clone();
585
cfae8f06 586 self.store(MediaId { label, media_set_label: None }, false).unwrap();
7320e9ff
DM
587
588 uuid
589 }
590
d1d74c43 591 /// Generate and insert a new tape assigned to a specific pool
7320e9ff
DM
592 /// (test helper)
593 pub fn generate_assigned_tape(
594 &mut self,
8446fbca 595 label_text: &str,
7320e9ff
DM
596 pool: &str,
597 ctime: i64,
598 ) -> Uuid {
599
a78348ac 600 let label = MediaLabel {
8446fbca 601 label_text: label_text.to_string(),
7320e9ff
DM
602 uuid: Uuid::generate(),
603 ctime,
604 };
605
606 let uuid = label.uuid.clone();
607
8a0046f5 608 let set = MediaSetLabel::with_data(pool, [0u8; 16].into(), 0, ctime, None);
7320e9ff 609
cfae8f06 610 self.store(MediaId { label, media_set_label: Some(set) }, false).unwrap();
7320e9ff
DM
611
612 uuid
613 }
614
d1d74c43 615 /// Generate and insert a used tape (test helper)
7320e9ff
DM
616 pub fn generate_used_tape(
617 &mut self,
8446fbca 618 label_text: &str,
7320e9ff
DM
619 set: MediaSetLabel,
620 ctime: i64,
621 ) -> Uuid {
a78348ac 622 let label = MediaLabel {
8446fbca 623 label_text: label_text.to_string(),
7320e9ff
DM
624 uuid: Uuid::generate(),
625 ctime,
626 };
627 let uuid = label.uuid.clone();
628
cfae8f06 629 self.store(MediaId { label, media_set_label: Some(set) }, false).unwrap();
7320e9ff
DM
630
631 uuid
632 }
633}
634
cfae8f06
DM
635// Status/location handling
636impl Inventory {
637
638 /// Returns status and location with reasonable defaults.
639 ///
640 /// Default status is 'MediaStatus::Unknown'.
641 /// Default location is 'MediaLocation::Offline'.
642 pub fn status_and_location(&self, uuid: &Uuid) -> (MediaStatus, MediaLocation) {
643
644 match self.map.get(uuid) {
645 None => {
646 // no info stored - assume media is writable/offline
647 (MediaStatus::Unknown, MediaLocation::Offline)
648 }
649 Some(entry) => {
650 let location = entry.location.clone().unwrap_or(MediaLocation::Offline);
651 let status = entry.status.unwrap_or(MediaStatus::Unknown);
652 (status, location)
653 }
654 }
655 }
656
657 // Lock database, reload database, set status, store database
658 fn set_media_status(&mut self, uuid: &Uuid, status: Option<MediaStatus>) -> Result<(), Error> {
659 let _lock = self.lock()?;
660 self.map = Self::load_media_db(&self.inventory_path)?;
661 if let Some(entry) = self.map.get_mut(uuid) {
662 entry.status = status;
663 self.update_helpers();
664 self.replace_file()?;
665 Ok(())
666 } else {
667 bail!("no such media '{}'", uuid);
668 }
669 }
670
671 /// Lock database, reload database, set status to Full, store database
672 pub fn set_media_status_full(&mut self, uuid: &Uuid) -> Result<(), Error> {
673 self.set_media_status(uuid, Some(MediaStatus::Full))
674 }
675
676 /// Lock database, reload database, set status to Damaged, store database
677 pub fn set_media_status_damaged(&mut self, uuid: &Uuid) -> Result<(), Error> {
678 self.set_media_status(uuid, Some(MediaStatus::Damaged))
679 }
680
08ec39be
DM
681 /// Lock database, reload database, set status to Retired, store database
682 pub fn set_media_status_retired(&mut self, uuid: &Uuid) -> Result<(), Error> {
683 self.set_media_status(uuid, Some(MediaStatus::Retired))
684 }
685
cfae8f06
DM
686 /// Lock database, reload database, set status to None, store database
687 pub fn clear_media_status(&mut self, uuid: &Uuid) -> Result<(), Error> {
688 self.set_media_status(uuid, None)
689 }
690
691 // Lock database, reload database, set location, store database
692 fn set_media_location(&mut self, uuid: &Uuid, location: Option<MediaLocation>) -> Result<(), Error> {
693 let _lock = self.lock()?;
694 self.map = Self::load_media_db(&self.inventory_path)?;
695 if let Some(entry) = self.map.get_mut(uuid) {
696 entry.location = location;
697 self.update_helpers();
698 self.replace_file()?;
699 Ok(())
700 } else {
701 bail!("no such media '{}'", uuid);
702 }
703 }
704
705 /// Lock database, reload database, set location to vault, store database
706 pub fn set_media_location_vault(&mut self, uuid: &Uuid, vault: &str) -> Result<(), Error> {
707 self.set_media_location(uuid, Some(MediaLocation::Vault(vault.to_string())))
708 }
709
710 /// Lock database, reload database, set location to offline, store database
711 pub fn set_media_location_offline(&mut self, uuid: &Uuid) -> Result<(), Error> {
712 self.set_media_location(uuid, Some(MediaLocation::Offline))
713 }
714
715 /// Update online status
716 pub fn update_online_status(&mut self, online_map: &OnlineStatusMap) -> Result<(), Error> {
717 let _lock = self.lock()?;
718 self.map = Self::load_media_db(&self.inventory_path)?;
719
720 for (uuid, entry) in self.map.iter_mut() {
721 if let Some(changer_name) = online_map.lookup_changer(uuid) {
722 entry.location = Some(MediaLocation::Online(changer_name.to_string()));
6334bdc1
FG
723 } else if let Some(MediaLocation::Online(ref changer_name)) = entry.location {
724 match online_map.online_map(changer_name) {
725 None => {
726 // no such changer device
727 entry.location = Some(MediaLocation::Offline);
728 }
729 Some(None) => {
730 // got no info - do nothing
731 }
732 Some(Some(_)) => {
733 // media changer changed
734 entry.location = Some(MediaLocation::Offline);
cfae8f06
DM
735 }
736 }
737 }
738 }
739
740 self.update_helpers();
741 self.replace_file()?;
742
743 Ok(())
744 }
745
746}
747
30316192 748/// Lock a media pool
7526d864 749pub fn lock_media_pool(base_path: &Path, name: &str) -> Result<BackupLockGuard, Error> {
30316192
DM
750 let mut path = base_path.to_owned();
751 path.push(format!(".pool-{}", name));
752 path.set_extension("lck");
753
7526d864 754 open_backup_lockfile(&path, None, true)
30316192
DM
755}
756
757/// Lock for media not assigned to any pool
7526d864 758pub fn lock_unassigned_media_pool(base_path: &Path) -> Result<BackupLockGuard, Error> {
30316192
DM
759 // lock artificial "__UNASSIGNED__" pool to avoid races
760 lock_media_pool(base_path, "__UNASSIGNED__")
761}
762
763/// Lock a media set
764///
765/// Timeout is 10 seconds by default
766pub fn lock_media_set(
767 base_path: &Path,
768 media_set_uuid: &Uuid,
769 timeout: Option<Duration>,
7526d864 770) -> Result<BackupLockGuard, Error> {
30316192
DM
771 let mut path = base_path.to_owned();
772 path.push(format!(".media-set-{}", media_set_uuid));
773 path.set_extension("lck");
774
7526d864 775 open_backup_lockfile(&path, timeout, true)
30316192
DM
776}
777
7320e9ff
DM
778// shell completion helper
779
780/// List of known media uuids
781pub fn complete_media_uuid(
782 _arg: &str,
783 _param: &HashMap<String, String>,
784) -> Vec<String> {
785
cafd51bf 786 let inventory = match Inventory::load(Path::new(TAPE_STATUS_DIR)) {
7320e9ff
DM
787 Ok(inventory) => inventory,
788 Err(_) => return Vec::new(),
789 };
790
791 inventory.map.keys().map(|uuid| uuid.to_string()).collect()
792}
793
794/// List of known media sets
795pub fn complete_media_set_uuid(
796 _arg: &str,
797 _param: &HashMap<String, String>,
798) -> Vec<String> {
799
cafd51bf 800 let inventory = match Inventory::load(Path::new(TAPE_STATUS_DIR)) {
7320e9ff
DM
801 Ok(inventory) => inventory,
802 Err(_) => return Vec::new(),
803 };
804
805 inventory.map.values()
cfae8f06 806 .filter_map(|entry| entry.id.media_set_label.as_ref())
7320e9ff
DM
807 .map(|set| set.uuid.to_string()).collect()
808}
809
810/// List of known media labels (barcodes)
8446fbca 811pub fn complete_media_label_text(
7320e9ff
DM
812 _arg: &str,
813 _param: &HashMap<String, String>,
814) -> Vec<String> {
815
cafd51bf 816 let inventory = match Inventory::load(Path::new(TAPE_STATUS_DIR)) {
7320e9ff
DM
817 Ok(inventory) => inventory,
818 Err(_) => return Vec::new(),
819 };
820
8446fbca 821 inventory.map.values().map(|entry| entry.id.label.label_text.clone()).collect()
7320e9ff 822}
13f435ca
DC
823
824pub fn complete_media_set_snapshots(_arg: &str, param: &HashMap<String, String>) -> Vec<String> {
825 let media_set_uuid: Uuid = match param.get("media-set").and_then(|s| s.parse().ok()) {
826 Some(uuid) => uuid,
827 None => return Vec::new(),
828 };
829 let status_path = Path::new(TAPE_STATUS_DIR);
9a37bd6c 830 let inventory = match Inventory::load(status_path) {
13f435ca
DC
831 Ok(inventory) => inventory,
832 Err(_) => return Vec::new(),
833 };
834
835 let mut res = Vec::new();
836 let media_ids = inventory.list_used_media().into_iter().filter(|media| {
837 match &media.media_set_label {
838 Some(label) => label.uuid == media_set_uuid,
839 None => false,
840 }
841 });
842
843 for media_id in media_ids {
844 let catalog = match MediaCatalog::open(status_path, &media_id, false, false) {
845 Ok(catalog) => catalog,
846 Err(_) => continue,
847 };
848
849 for (store, content) in catalog.content() {
850 for snapshot in content.snapshot_index.keys() {
851 res.push(format!("{}:{}", store, snapshot));
852 }
853 }
854 }
855
856 res
857}