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