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