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