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