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