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