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