]>
Commit | Line | Data |
---|---|---|
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 | |
7 | use std::collections::{HashMap, BTreeMap}; | |
8 | use std::path::{Path, PathBuf}; | |
9 | ||
10 | use anyhow::{bail, Error}; | |
11 | use serde::{Serialize, Deserialize}; | |
12 | use serde_json::json; | |
13 | ||
14 | use proxmox::tools::{ | |
15 | Uuid, | |
16 | fs::{ | |
17 | open_file_locked, | |
18 | replace_file, | |
19 | file_get_json, | |
20 | CreateOptions, | |
21 | }, | |
22 | }; | |
23 | ||
24 | use crate::{ | |
25 | tools::systemd::time::compute_next_event, | |
26 | api2::types::{ | |
27 | MediaSetPolicy, | |
28 | RetentionPolicy, | |
29 | }, | |
30 | tape::{ | |
cafd51bf | 31 | TAPE_STATUS_DIR, |
7320e9ff | 32 | file_formats::{ |
a78348ac | 33 | MediaLabel, |
7320e9ff DM |
34 | MediaSetLabel, |
35 | }, | |
36 | }, | |
37 | }; | |
38 | ||
39 | /// Unique Media Identifier | |
40 | /// | |
41 | /// This combines the label and media set label. | |
42 | #[derive(Debug,Serialize,Deserialize,Clone)] | |
43 | pub struct MediaId { | |
a78348ac | 44 | pub label: MediaLabel, |
7320e9ff DM |
45 | #[serde(skip_serializing_if="Option::is_none")] |
46 | pub media_set_label: Option<MediaSetLabel>, | |
47 | } | |
48 | ||
49 | /// Media Set | |
50 | /// | |
51 | /// A List of backup media | |
52 | #[derive(Debug, Serialize, Deserialize)] | |
53 | pub struct MediaSet { | |
54 | /// Unique media set ID | |
55 | uuid: Uuid, | |
56 | /// List of BackupMedia | |
57 | media_list: Vec<Option<Uuid>>, | |
58 | } | |
59 | ||
60 | impl MediaSet { | |
61 | ||
62 | pub const MEDIA_SET_MAX_SEQ_NR: u64 = 100; | |
63 | ||
64 | pub fn new() -> Self { | |
65 | let uuid = Uuid::generate(); | |
66 | Self { | |
67 | uuid, | |
68 | media_list: Vec::new(), | |
69 | } | |
70 | } | |
71 | ||
72 | pub fn with_data(uuid: Uuid, media_list: Vec<Option<Uuid>>) -> Self { | |
73 | Self { uuid, media_list } | |
74 | } | |
75 | ||
76 | pub fn uuid(&self) -> &Uuid { | |
77 | &self.uuid | |
78 | } | |
79 | ||
80 | pub fn media_list(&self) -> &[Option<Uuid>] { | |
81 | &self.media_list | |
82 | } | |
83 | ||
84 | pub fn add_media(&mut self, uuid: Uuid) { | |
85 | self.media_list.push(Some(uuid)); | |
86 | } | |
87 | ||
88 | pub fn insert_media(&mut self, uuid: Uuid, seq_nr: u64) -> Result<(), Error> { | |
89 | if seq_nr > Self::MEDIA_SET_MAX_SEQ_NR { | |
90 | bail!("media set sequence number to large in media set {} ({} > {})", | |
91 | self.uuid.to_string(), seq_nr, Self::MEDIA_SET_MAX_SEQ_NR); | |
92 | } | |
93 | let seq_nr = seq_nr as usize; | |
94 | if self.media_list.len() > seq_nr { | |
95 | if self.media_list[seq_nr].is_some() { | |
96 | bail!("found duplicate squence number in media set '{}/{}'", | |
97 | self.uuid.to_string(), seq_nr); | |
98 | } | |
99 | } else { | |
100 | self.media_list.resize(seq_nr + 1, None); | |
101 | } | |
102 | self.media_list[seq_nr] = Some(uuid); | |
103 | Ok(()) | |
104 | } | |
105 | ||
106 | pub fn last_media_uuid(&self) -> Option<&Uuid> { | |
107 | match self.media_list.last() { | |
108 | None => None, | |
109 | Some(None) => None, | |
110 | Some(Some(ref last_uuid)) => Some(last_uuid), | |
111 | } | |
112 | } | |
113 | ||
114 | pub fn is_last_media(&self, uuid: &Uuid) -> bool { | |
115 | match self.media_list.last() { | |
116 | None => false, | |
117 | Some(None) => false, | |
118 | Some(Some(last_uuid)) => uuid == last_uuid, | |
119 | } | |
120 | } | |
121 | } | |
122 | ||
123 | /// Media Inventory | |
124 | pub struct Inventory { | |
125 | map: BTreeMap<Uuid, MediaId>, | |
126 | ||
127 | inventory_path: PathBuf, | |
128 | lockfile_path: PathBuf, | |
129 | ||
130 | // helpers | |
131 | media_set_start_times: HashMap<Uuid, i64> | |
132 | } | |
133 | ||
134 | impl Inventory { | |
135 | ||
136 | pub const MEDIA_INVENTORY_FILENAME: &'static str = "inventory.json"; | |
137 | pub const MEDIA_INVENTORY_LOCKFILE: &'static str = ".inventory.lck"; | |
138 | ||
139 | fn new(base_path: &Path) -> Self { | |
140 | ||
141 | let mut inventory_path = base_path.to_owned(); | |
142 | inventory_path.push(Self::MEDIA_INVENTORY_FILENAME); | |
143 | ||
144 | let mut lockfile_path = base_path.to_owned(); | |
145 | lockfile_path.push(Self::MEDIA_INVENTORY_LOCKFILE); | |
146 | ||
147 | Self { | |
148 | map: BTreeMap::new(), | |
149 | media_set_start_times: HashMap::new(), | |
150 | inventory_path, | |
151 | lockfile_path, | |
152 | } | |
153 | } | |
154 | ||
155 | pub fn load(base_path: &Path) -> Result<Self, Error> { | |
156 | let mut me = Self::new(base_path); | |
157 | me.reload()?; | |
158 | Ok(me) | |
159 | } | |
160 | ||
161 | /// Reload the database | |
162 | pub fn reload(&mut self) -> Result<(), Error> { | |
163 | self.map = Self::load_media_db(&self.inventory_path)?; | |
164 | self.update_helpers(); | |
165 | Ok(()) | |
166 | } | |
167 | ||
168 | fn update_helpers(&mut self) { | |
169 | ||
170 | // recompute media_set_start_times | |
171 | ||
172 | let mut set_start_times = HashMap::new(); | |
173 | ||
174 | for media in self.map.values() { | |
175 | let set = match &media.media_set_label { | |
176 | None => continue, | |
177 | Some(set) => set, | |
178 | }; | |
179 | if set.seq_nr == 0 { | |
180 | set_start_times.insert(set.uuid.clone(), set.ctime); | |
181 | } | |
182 | } | |
183 | ||
184 | self.media_set_start_times = set_start_times; | |
185 | } | |
186 | ||
187 | /// Lock the database | |
188 | pub fn lock(&self) -> Result<std::fs::File, Error> { | |
189 | open_file_locked(&self.lockfile_path, std::time::Duration::new(10, 0), true) | |
190 | } | |
191 | ||
192 | fn load_media_db(path: &Path) -> Result<BTreeMap<Uuid, MediaId>, Error> { | |
193 | ||
194 | let data = file_get_json(path, Some(json!([])))?; | |
195 | let media_list: Vec<MediaId> = serde_json::from_value(data)?; | |
196 | ||
197 | let mut map = BTreeMap::new(); | |
198 | for item in media_list.into_iter() { | |
199 | map.insert(item.label.uuid.clone(), item); | |
200 | } | |
201 | ||
202 | Ok(map) | |
203 | } | |
204 | ||
205 | fn replace_file(&self) -> Result<(), Error> { | |
206 | let list: Vec<&MediaId> = self.map.values().collect(); | |
207 | let raw = serde_json::to_string_pretty(&serde_json::to_value(list)?)?; | |
cafd51bf DM |
208 | |
209 | let backup_user = crate::backup::backup_user()?; | |
210 | let mode = nix::sys::stat::Mode::from_bits_truncate(0o0640); | |
211 | let options = CreateOptions::new() | |
212 | .perm(mode) | |
213 | .owner(backup_user.uid) | |
214 | .group(backup_user.gid); | |
215 | ||
7320e9ff | 216 | replace_file(&self.inventory_path, raw.as_bytes(), options)?; |
cafd51bf | 217 | |
7320e9ff DM |
218 | Ok(()) |
219 | } | |
220 | ||
221 | /// Stores a single MediaID persistently | |
222 | pub fn store(&mut self, mut media_id: MediaId) -> Result<(), Error> { | |
223 | let _lock = self.lock()?; | |
224 | self.map = Self::load_media_db(&self.inventory_path)?; | |
225 | ||
226 | // do not overwrite unsaved pool assignments | |
227 | if media_id.media_set_label.is_none() { | |
228 | if let Some(previous) = self.map.get(&media_id.label.uuid) { | |
229 | if let Some(ref set) = previous.media_set_label { | |
230 | if set.uuid.as_ref() == [0u8;16] { | |
231 | media_id.media_set_label = Some(set.clone()); | |
232 | } | |
233 | } | |
234 | } | |
235 | } | |
236 | ||
237 | self.map.insert(media_id.label.uuid.clone(), media_id); | |
238 | self.update_helpers(); | |
239 | self.replace_file()?; | |
240 | Ok(()) | |
241 | } | |
242 | ||
fb657d8e DM |
243 | /// Remove a single media persistently |
244 | pub fn remove_media(&mut self, uuid: &Uuid) -> Result<(), Error> { | |
245 | let _lock = self.lock()?; | |
246 | self.map = Self::load_media_db(&self.inventory_path)?; | |
247 | self.map.remove(uuid); | |
248 | self.update_helpers(); | |
249 | self.replace_file()?; | |
250 | Ok(()) | |
251 | } | |
252 | ||
7320e9ff DM |
253 | /// Lookup media |
254 | pub fn lookup_media(&self, uuid: &Uuid) -> Option<&MediaId> { | |
255 | self.map.get(uuid) | |
256 | } | |
257 | ||
258 | /// find media by changer_id | |
259 | pub fn find_media_by_changer_id(&self, changer_id: &str) -> Option<&MediaId> { | |
260 | for (_uuid, media_id) in &self.map { | |
261 | if media_id.label.changer_id == changer_id { | |
262 | return Some(media_id); | |
263 | } | |
264 | } | |
265 | None | |
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, | |
274 | Some(media_id) => { | |
275 | match media_id.media_set_label { | |
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 | ||
290 | for (_uuid, media_id) in &self.map { | |
291 | match media_id.media_set_label { | |
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 | ||
298 | if set.uuid.as_ref() == [0u8;16] { // should we do this?? | |
299 | list.push(MediaId { | |
300 | label: media_id.label.clone(), | |
301 | media_set_label: None, | |
302 | }) | |
303 | } else { | |
304 | list.push(media_id.clone()); | |
305 | } | |
306 | } | |
307 | } | |
308 | ||
309 | } | |
310 | ||
311 | list | |
312 | } | |
313 | ||
314 | /// List all used media | |
315 | pub fn list_used_media(&self) -> Vec<MediaId> { | |
316 | let mut list = Vec::new(); | |
317 | ||
318 | for (_uuid, media_id) in &self.map { | |
319 | match media_id.media_set_label { | |
320 | None => continue, // not assigned to any pool | |
321 | Some(ref set) => { | |
322 | if set.uuid.as_ref() != [0u8;16] { | |
323 | list.push(media_id.clone()); | |
324 | } | |
325 | } | |
326 | } | |
327 | } | |
328 | ||
329 | list | |
330 | } | |
331 | ||
332 | /// List media not assigned to any pool | |
333 | pub fn list_unassigned_media(&self) -> Vec<MediaId> { | |
334 | let mut list = Vec::new(); | |
335 | ||
336 | for (_uuid, media_id) in &self.map { | |
337 | if media_id.media_set_label.is_none() { | |
338 | list.push(media_id.clone()); | |
339 | } | |
340 | } | |
341 | ||
342 | list | |
343 | } | |
344 | ||
345 | pub fn media_set_start_time(&self, media_set_uuid: &Uuid) -> Option<i64> { | |
346 | self.media_set_start_times.get(media_set_uuid).map(|t| *t) | |
347 | } | |
348 | ||
349 | /// Compute a single media sets | |
350 | pub fn compute_media_set_members(&self, media_set_uuid: &Uuid) -> Result<MediaSet, Error> { | |
351 | ||
352 | let mut set = MediaSet::with_data(media_set_uuid.clone(), Vec::new()); | |
353 | ||
354 | for media in self.map.values() { | |
355 | match media.media_set_label { | |
356 | None => continue, | |
357 | Some(MediaSetLabel { seq_nr, ref uuid, .. }) => { | |
358 | if uuid != media_set_uuid { | |
359 | continue; | |
360 | } | |
361 | set.insert_media(media.label.uuid.clone(), seq_nr)?; | |
362 | } | |
363 | } | |
364 | } | |
365 | ||
366 | Ok(set) | |
367 | } | |
368 | ||
369 | /// Compute all media sets | |
370 | pub fn compute_media_set_list(&self) -> Result<HashMap<Uuid, MediaSet>, Error> { | |
371 | ||
372 | let mut set_map: HashMap<Uuid, MediaSet> = HashMap::new(); | |
373 | ||
374 | for media in self.map.values() { | |
375 | match media.media_set_label { | |
376 | None => continue, | |
377 | Some(MediaSetLabel { seq_nr, ref uuid, .. }) => { | |
378 | ||
379 | let set = set_map.entry(uuid.clone()).or_insert_with(|| { | |
380 | MediaSet::with_data(uuid.clone(), Vec::new()) | |
381 | }); | |
382 | ||
383 | set.insert_media(media.label.uuid.clone(), seq_nr)?; | |
384 | } | |
385 | } | |
386 | } | |
387 | ||
388 | Ok(set_map) | |
389 | } | |
390 | ||
391 | /// Returns the latest media set for a pool | |
392 | pub fn latest_media_set(&self, pool: &str) -> Option<Uuid> { | |
393 | ||
394 | let mut last_set: Option<(Uuid, i64)> = None; | |
395 | ||
396 | let set_list = self.map.values() | |
397 | .filter_map(|media| media.media_set_label.as_ref()) | |
398 | .filter(|set| &set.pool == &pool && set.uuid.as_ref() != [0u8;16]); | |
399 | ||
400 | for set in set_list { | |
401 | match last_set { | |
402 | None => { | |
403 | last_set = Some((set.uuid.clone(), set.ctime)); | |
404 | } | |
405 | Some((_, last_ctime)) => { | |
406 | if set.ctime > last_ctime { | |
407 | last_set = Some((set.uuid.clone(), set.ctime)); | |
408 | } | |
409 | } | |
410 | } | |
411 | } | |
412 | ||
413 | let (uuid, ctime) = match last_set { | |
414 | None => return None, | |
415 | Some((uuid, ctime)) => (uuid, ctime), | |
416 | }; | |
417 | ||
418 | // consistency check - must be the only set with that ctime | |
419 | let set_list = self.map.values() | |
420 | .filter_map(|media| media.media_set_label.as_ref()) | |
421 | .filter(|set| &set.pool == &pool && set.uuid.as_ref() != [0u8;16]); | |
422 | ||
423 | for set in set_list { | |
424 | if set.uuid != uuid && set.ctime >= ctime { // should not happen | |
425 | eprintln!("latest_media_set: found set with equal ctime ({}, {})", set.uuid, uuid); | |
426 | return None; | |
427 | } | |
428 | } | |
429 | ||
430 | Some(uuid) | |
431 | } | |
432 | ||
433 | // Test if there is a media set (in the same pool) newer than this one. | |
434 | // Return the ctime of the nearest media set | |
435 | fn media_set_next_start_time(&self, media_set_uuid: &Uuid) -> Option<i64> { | |
436 | ||
437 | let (pool, ctime) = match self.map.values() | |
438 | .filter_map(|media| media.media_set_label.as_ref()) | |
439 | .find_map(|set| { | |
440 | if &set.uuid == media_set_uuid { | |
441 | Some((set.pool.clone(), set.ctime)) | |
442 | } else { | |
443 | None | |
444 | } | |
445 | }) { | |
446 | Some((pool, ctime)) => (pool, ctime), | |
447 | None => return None, | |
448 | }; | |
449 | ||
450 | let set_list = self.map.values() | |
451 | .filter_map(|media| media.media_set_label.as_ref()) | |
452 | .filter(|set| (&set.uuid != media_set_uuid) && (&set.pool == &pool)); | |
453 | ||
454 | let mut next_ctime = None; | |
455 | ||
456 | for set in set_list { | |
457 | if set.ctime > ctime { | |
458 | match next_ctime { | |
459 | None => { | |
460 | next_ctime = Some(set.ctime); | |
461 | } | |
462 | Some(last_next_ctime) => { | |
463 | if set.ctime < last_next_ctime { | |
464 | next_ctime = Some(set.ctime); | |
465 | } | |
466 | } | |
467 | } | |
468 | } | |
469 | } | |
470 | ||
471 | next_ctime | |
472 | } | |
473 | ||
474 | pub fn media_expire_time( | |
475 | &self, | |
476 | media: &MediaId, | |
477 | media_set_policy: &MediaSetPolicy, | |
478 | retention_policy: &RetentionPolicy, | |
479 | ) -> i64 { | |
480 | ||
481 | if let RetentionPolicy::KeepForever = retention_policy { | |
482 | return i64::MAX; | |
483 | } | |
484 | ||
485 | let set = match media.media_set_label { | |
486 | None => return i64::MAX, | |
487 | Some(ref set) => set, | |
488 | }; | |
489 | ||
490 | let set_start_time = match self.media_set_start_time(&set.uuid) { | |
491 | None => { | |
492 | // missing information, use ctime from this | |
493 | // set (always greater than ctime from seq_nr 0) | |
494 | set.ctime | |
495 | } | |
496 | Some(time) => time, | |
497 | }; | |
498 | ||
499 | let max_use_time = match media_set_policy { | |
500 | MediaSetPolicy::ContinueCurrent => { | |
501 | match self.media_set_next_start_time(&set.uuid) { | |
502 | Some(next_start_time) => next_start_time, | |
503 | None => return i64::MAX, | |
504 | } | |
505 | } | |
506 | MediaSetPolicy::AlwaysCreate => { | |
507 | set_start_time + 1 | |
508 | } | |
509 | MediaSetPolicy::CreateAt(ref event) => { | |
510 | match compute_next_event(event, set_start_time, false) { | |
511 | Ok(Some(next)) => next, | |
512 | Ok(None) | Err(_) => return i64::MAX, | |
513 | } | |
514 | } | |
515 | }; | |
516 | ||
517 | match retention_policy { | |
518 | RetentionPolicy::KeepForever => i64::MAX, | |
519 | RetentionPolicy::OverwriteAlways => max_use_time, | |
520 | RetentionPolicy::ProtectFor(time_span) => { | |
521 | let seconds = f64::from(time_span.clone()) as i64; | |
522 | max_use_time + seconds | |
523 | } | |
524 | } | |
525 | } | |
526 | ||
527 | /// Generate a human readable name for the media set | |
528 | /// | |
529 | /// The template can include strftime time format specifications. | |
530 | pub fn generate_media_set_name( | |
531 | &self, | |
532 | media_set_uuid: &Uuid, | |
533 | template: Option<String>, | |
534 | ) -> Result<String, Error> { | |
535 | ||
536 | if let Some(ctime) = self.media_set_start_time(media_set_uuid) { | |
537 | let mut template = template.unwrap_or(String::from("%id%")); | |
538 | template = template.replace("%id%", &media_set_uuid.to_string()); | |
539 | proxmox::tools::time::strftime_local(&template, ctime) | |
540 | } else { | |
541 | // We don't know the set start time, so we cannot use the template | |
542 | Ok(media_set_uuid.to_string()) | |
543 | } | |
544 | } | |
545 | ||
546 | // Helpers to simplify testing | |
547 | ||
548 | /// Genreate and insert a new free tape (test helper) | |
549 | pub fn generate_free_tape(&mut self, changer_id: &str, ctime: i64) -> Uuid { | |
550 | ||
a78348ac | 551 | let label = MediaLabel { |
7320e9ff DM |
552 | changer_id: changer_id.to_string(), |
553 | uuid: Uuid::generate(), | |
554 | ctime, | |
555 | }; | |
556 | let uuid = label.uuid.clone(); | |
557 | ||
558 | self.store(MediaId { label, media_set_label: None }).unwrap(); | |
559 | ||
560 | uuid | |
561 | } | |
562 | ||
563 | /// Genreate and insert a new tape assigned to a specific pool | |
564 | /// (test helper) | |
565 | pub fn generate_assigned_tape( | |
566 | &mut self, | |
567 | changer_id: &str, | |
568 | pool: &str, | |
569 | ctime: i64, | |
570 | ) -> Uuid { | |
571 | ||
a78348ac | 572 | let label = MediaLabel { |
7320e9ff DM |
573 | changer_id: changer_id.to_string(), |
574 | uuid: Uuid::generate(), | |
575 | ctime, | |
576 | }; | |
577 | ||
578 | let uuid = label.uuid.clone(); | |
579 | ||
580 | let set = MediaSetLabel::with_data(pool, [0u8; 16].into(), 0, ctime); | |
581 | ||
582 | self.store(MediaId { label, media_set_label: Some(set) }).unwrap(); | |
583 | ||
584 | uuid | |
585 | } | |
586 | ||
587 | /// Genreate and insert a used tape (test helper) | |
588 | pub fn generate_used_tape( | |
589 | &mut self, | |
590 | changer_id: &str, | |
591 | set: MediaSetLabel, | |
592 | ctime: i64, | |
593 | ) -> Uuid { | |
a78348ac | 594 | let label = MediaLabel { |
7320e9ff DM |
595 | changer_id: changer_id.to_string(), |
596 | uuid: Uuid::generate(), | |
597 | ctime, | |
598 | }; | |
599 | let uuid = label.uuid.clone(); | |
600 | ||
601 | self.store(MediaId { label, media_set_label: Some(set) }).unwrap(); | |
602 | ||
603 | uuid | |
604 | } | |
605 | } | |
606 | ||
607 | // shell completion helper | |
608 | ||
609 | /// List of known media uuids | |
610 | pub fn complete_media_uuid( | |
611 | _arg: &str, | |
612 | _param: &HashMap<String, String>, | |
613 | ) -> Vec<String> { | |
614 | ||
cafd51bf | 615 | let inventory = match Inventory::load(Path::new(TAPE_STATUS_DIR)) { |
7320e9ff DM |
616 | Ok(inventory) => inventory, |
617 | Err(_) => return Vec::new(), | |
618 | }; | |
619 | ||
620 | inventory.map.keys().map(|uuid| uuid.to_string()).collect() | |
621 | } | |
622 | ||
623 | /// List of known media sets | |
624 | pub fn complete_media_set_uuid( | |
625 | _arg: &str, | |
626 | _param: &HashMap<String, String>, | |
627 | ) -> Vec<String> { | |
628 | ||
cafd51bf | 629 | let inventory = match Inventory::load(Path::new(TAPE_STATUS_DIR)) { |
7320e9ff DM |
630 | Ok(inventory) => inventory, |
631 | Err(_) => return Vec::new(), | |
632 | }; | |
633 | ||
634 | inventory.map.values() | |
635 | .filter_map(|media| media.media_set_label.as_ref()) | |
636 | .map(|set| set.uuid.to_string()).collect() | |
637 | } | |
638 | ||
639 | /// List of known media labels (barcodes) | |
640 | pub fn complete_media_changer_id( | |
641 | _arg: &str, | |
642 | _param: &HashMap<String, String>, | |
643 | ) -> Vec<String> { | |
644 | ||
cafd51bf | 645 | let inventory = match Inventory::load(Path::new(TAPE_STATUS_DIR)) { |
7320e9ff DM |
646 | Ok(inventory) => inventory, |
647 | Err(_) => return Vec::new(), | |
648 | }; | |
649 | ||
650 | inventory.map.values().map(|media| media.label.changer_id.clone()).collect() | |
651 | } |