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