]>
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}; | |
988e8de1 | 9 | use std::os::unix::io::AsRawFd; |
7320e9ff DM |
10 | |
11 | use anyhow::{bail, Error}; | |
12 | use serde::{Serialize, Deserialize}; | |
13 | use serde_json::json; | |
14 | ||
15 | use 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 | ||
26 | use 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)] | |
49 | pub 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)] |
57 | struct 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 |
66 | pub 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 | ||
76 | impl 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 |
624 | impl 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 | |
739 | pub 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 | |
753 | pub 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 | 769 | pub 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 | } |