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