2 use std
::collections
::HashSet
;
4 use anyhow
::{bail, format_err, Error}
;
7 api
::{api, Router, SubdirMap, RpcEnvironment, Permission}
,
8 list_subdirs_api_method
,
12 use pbs_datastore
::backup_info
::BackupDir
;
14 MEDIA_POOL_NAME_SCHEMA
, MEDIA_LABEL_SCHEMA
, MEDIA_UUID_SCHEMA
, CHANGER_NAME_SCHEMA
,
15 VAULT_NAME_SCHEMA
, Authid
, MediaPoolConfig
, MediaListEntry
, MediaSetListEntry
,
16 MediaStatus
, MediaContentEntry
, MediaContentListFilter
,
21 config
::cached_user_info
::CachedUserInfo
,
27 media_catalog_snapshot_list
,
28 changer
::update_online_status
,
34 description
: "List of media sets.",
37 type: MediaSetListEntry
,
41 description
: "List of media sets filtered by Tape.Audit privileges on pool",
42 permission
: &Permission
::Anybody
,
46 pub async
fn list_media_sets(
47 rpcenv
: &mut dyn RpcEnvironment
,
48 ) -> Result
<Vec
<MediaSetListEntry
>, Error
> {
49 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
50 let user_info
= CachedUserInfo
::new()?
;
52 let (config
, _digest
) = pbs_config
::media_pool
::config()?
;
54 let status_path
= Path
::new(TAPE_STATUS_DIR
);
56 let mut media_sets
: HashSet
<Uuid
> = HashSet
::new();
57 let mut list
= Vec
::new();
59 for (_section_type
, data
) in config
.sections
.values() {
60 let pool_name
= match data
["name"].as_str() {
65 let privs
= user_info
.lookup_privs(&auth_id
, &["tape", "pool", pool_name
]);
66 if (privs
& PRIV_TAPE_AUDIT
) == 0 {
70 let config
: MediaPoolConfig
= config
.lookup("pool", pool_name
)?
;
72 let changer_name
= None
; // assume standalone drive
73 let pool
= MediaPool
::with_config(status_path
, &config
, changer_name
, true)?
;
75 for media
in pool
.list_media() {
76 if let Some(label
) = media
.media_set_label() {
77 if media_sets
.contains(&label
.uuid
) {
81 let media_set_uuid
= label
.uuid
.clone();
82 let media_set_ctime
= label
.ctime
;
83 let media_set_name
= pool
84 .generate_media_set_name(&media_set_uuid
, config
.template
.clone())
85 .unwrap_or_else(|_
| media_set_uuid
.to_string());
87 media_sets
.insert(media_set_uuid
.clone());
88 list
.push(MediaSetListEntry
{
92 pool
: pool_name
.to_string(),
104 schema
: MEDIA_POOL_NAME_SCHEMA
,
108 description
: "Try to update tape library status (check what tapes are online).",
112 "update-status-changer": {
113 // only update status for a single changer
114 schema
: CHANGER_NAME_SCHEMA
,
120 description
: "List of registered backup media.",
123 type: MediaListEntry
,
127 description
: "List of registered backup media filtered by Tape.Audit privileges on pool",
128 permission
: &Permission
::Anybody
,
132 pub async
fn list_media(
133 pool
: Option
<String
>,
135 update_status_changer
: Option
<String
>,
136 rpcenv
: &mut dyn RpcEnvironment
,
137 ) -> Result
<Vec
<MediaListEntry
>, Error
> {
138 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
139 let user_info
= CachedUserInfo
::new()?
;
141 let (config
, _digest
) = pbs_config
::media_pool
::config()?
;
143 let status_path
= Path
::new(TAPE_STATUS_DIR
);
145 let catalogs
= tokio
::task
::spawn_blocking(move || {
147 // update online media status
148 if let Err(err
) = update_online_status(status_path
, update_status_changer
.as_deref()) {
149 eprintln
!("{}", err
);
150 eprintln
!("update online media status failed - using old state");
153 // test what catalog files we have
154 MediaCatalog
::media_with_catalogs(status_path
)
157 let mut list
= Vec
::new();
159 for (_section_type
, data
) in config
.sections
.values() {
160 let pool_name
= match data
["name"].as_str() {
164 if let Some(ref name
) = pool
{
165 if name
!= pool_name
{
170 let privs
= user_info
.lookup_privs(&auth_id
, &["tape", "pool", pool_name
]);
171 if (privs
& PRIV_TAPE_AUDIT
) == 0 {
175 let config
: MediaPoolConfig
= config
.lookup("pool", pool_name
)?
;
177 let changer_name
= None
; // assume standalone drive
178 let mut pool
= MediaPool
::with_config(status_path
, &config
, changer_name
, true)?
;
180 let current_time
= proxmox
::tools
::time
::epoch_i64();
182 // Call start_write_session, so that we show the same status a
183 // backup job would see.
184 pool
.force_media_availability();
185 pool
.start_write_session(current_time
, false)?
;
187 for media
in pool
.list_media() {
188 let expired
= pool
.media_is_expired(&media
, current_time
);
190 let media_set_uuid
= media
.media_set_label()
191 .map(|set
| set
.uuid
.clone());
193 let seq_nr
= media
.media_set_label()
194 .map(|set
| set
.seq_nr
);
196 let media_set_name
= media
.media_set_label()
198 pool
.generate_media_set_name(&set
.uuid
, config
.template
.clone())
199 .unwrap_or_else(|_
| set
.uuid
.to_string())
202 let catalog_ok
= if media
.media_set_label().is_none() {
203 // Media is empty, we need no catalog
206 catalogs
.contains(media
.uuid())
209 list
.push(MediaListEntry
{
210 uuid
: media
.uuid().clone(),
211 label_text
: media
.label_text().to_string(),
212 ctime
: media
.ctime(),
213 pool
: Some(pool_name
.to_string()),
214 location
: media
.location().clone(),
215 status
: *media
.status(),
218 media_set_ctime
: media
.media_set_label().map(|set
| set
.ctime
),
226 let inventory
= Inventory
::load(status_path
)?
;
228 let privs
= user_info
.lookup_privs(&auth_id
, &["tape", "pool"]);
229 if (privs
& PRIV_TAPE_AUDIT
) != 0 {
232 for media_id
in inventory
.list_unassigned_media() {
234 let (mut status
, location
) = inventory
.status_and_location(&media_id
.label
.uuid
);
236 if status
== MediaStatus
::Unknown
{
237 status
= MediaStatus
::Writable
;
240 list
.push(MediaListEntry
{
241 uuid
: media_id
.label
.uuid
.clone(),
242 ctime
: media_id
.label
.ctime
,
243 label_text
: media_id
.label
.label_text
.to_string(),
246 catalog
: true, // empty, so we do not need a catalog
248 media_set_uuid
: None
,
249 media_set_name
: None
,
250 media_set_ctime
: None
,
258 // add media with missing pool configuration
259 // set status to MediaStatus::Unknown
260 for uuid
in inventory
.media_list() {
261 let media_id
= inventory
.lookup_media(uuid
).unwrap();
262 let media_set_label
= match media_id
.media_set_label
{
263 Some(ref set
) => set
,
267 if config
.sections
.get(&media_set_label
.pool
).is_some() {
271 let privs
= user_info
.lookup_privs(&auth_id
, &["tape", "pool", &media_set_label
.pool
]);
272 if (privs
& PRIV_TAPE_AUDIT
) == 0 {
276 let (_status
, location
) = inventory
.status_and_location(uuid
);
278 let media_set_name
= inventory
.generate_media_set_name(&media_set_label
.uuid
, None
)?
;
280 list
.push(MediaListEntry
{
281 uuid
: media_id
.label
.uuid
.clone(),
282 label_text
: media_id
.label
.label_text
.clone(),
283 ctime
: media_id
.label
.ctime
,
284 pool
: Some(media_set_label
.pool
.clone()),
286 status
: MediaStatus
::Unknown
,
287 catalog
: catalogs
.contains(uuid
),
289 media_set_ctime
: Some(media_set_label
.ctime
),
290 media_set_uuid
: Some(media_set_label
.uuid
.clone()),
291 media_set_name
: Some(media_set_name
),
292 seq_nr
: Some(media_set_label
.seq_nr
),
305 schema
: MEDIA_LABEL_SCHEMA
,
308 schema
: VAULT_NAME_SCHEMA
,
314 /// Change Tape location to vault (if given), or offline.
317 vault_name
: Option
<String
>,
318 ) -> Result
<(), Error
> {
320 let status_path
= Path
::new(TAPE_STATUS_DIR
);
321 let mut inventory
= Inventory
::load(status_path
)?
;
323 let uuid
= inventory
.find_media_by_label_text(&label_text
)
324 .ok_or_else(|| format_err
!("no such media '{}'", label_text
))?
329 if let Some(vault_name
) = vault_name
{
330 inventory
.set_media_location_vault(&uuid
, &vault_name
)?
;
332 inventory
.set_media_location_offline(&uuid
)?
;
342 schema
: MEDIA_LABEL_SCHEMA
,
345 description
: "Force removal (even if media is used in a media set).",
352 /// Destroy media (completely remove from database)
353 pub fn destroy_media(label_text
: String
, force
: Option
<bool
>,) -> Result
<(), Error
> {
355 let force
= force
.unwrap_or(false);
357 let status_path
= Path
::new(TAPE_STATUS_DIR
);
358 let mut inventory
= Inventory
::load(status_path
)?
;
360 let media_id
= inventory
.find_media_by_label_text(&label_text
)
361 .ok_or_else(|| format_err
!("no such media '{}'", label_text
))?
;
364 if let Some(ref set
) = media_id
.media_set_label
{
365 let is_empty
= set
.uuid
.as_ref() == [0u8;16];
367 bail
!("media '{}' contains data (please use 'force' flag to remove.", label_text
);
372 let uuid
= media_id
.label
.uuid
.clone();
374 inventory
.remove_media(&uuid
)?
;
383 type: MediaContentListFilter
,
389 description
: "Media content list.",
392 type: MediaContentEntry
,
396 description
: "List content filtered by Tape.Audit privilege on pool",
397 permission
: &Permission
::Anybody
,
400 /// List media content
402 filter
: MediaContentListFilter
,
403 rpcenv
: &mut dyn RpcEnvironment
,
404 ) -> Result
<Vec
<MediaContentEntry
>, Error
> {
405 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
406 let user_info
= CachedUserInfo
::new()?
;
408 let (config
, _digest
) = pbs_config
::media_pool
::config()?
;
410 let status_path
= Path
::new(TAPE_STATUS_DIR
);
411 let inventory
= Inventory
::load(status_path
)?
;
413 let mut list
= Vec
::new();
415 for media_id
in inventory
.list_used_media() {
416 let set
= media_id
.media_set_label
.as_ref().unwrap();
418 if let Some(ref label_text
) = filter
.label_text
{
419 if &media_id
.label
.label_text
!= label_text { continue; }
422 if let Some(ref pool
) = filter
.pool
{
423 if &set
.pool
!= pool { continue; }
426 let privs
= user_info
.lookup_privs(&auth_id
, &["tape", "pool", &set
.pool
]);
427 if (privs
& PRIV_TAPE_AUDIT
) == 0 {
431 if let Some(ref media_uuid
) = filter
.media
{
432 if &media_id
.label
.uuid
!= media_uuid { continue; }
435 if let Some(ref media_set_uuid
) = filter
.media_set
{
436 if &set
.uuid
!= media_set_uuid { continue; }
439 let template
= match config
.lookup
::<MediaPoolConfig
>("pool", &set
.pool
) {
440 Ok(pool_config
) => pool_config
.template
.clone(),
441 _
=> None
, // simply use default if there is no pool config
444 let media_set_name
= inventory
445 .generate_media_set_name(&set
.uuid
, template
)
446 .unwrap_or_else(|_
| set
.uuid
.to_string());
448 for (store
, snapshot
) in media_catalog_snapshot_list(status_path
, &media_id
)?
{
449 let backup_dir
: BackupDir
= snapshot
.parse()?
;
451 if let Some(ref backup_type
) = filter
.backup_type
{
452 if backup_dir
.group().backup_type() != backup_type { continue; }
454 if let Some(ref backup_id
) = filter
.backup_id
{
455 if backup_dir
.group().backup_id() != backup_id { continue; }
458 list
.push(MediaContentEntry
{
459 uuid
: media_id
.label
.uuid
.clone(),
460 label_text
: media_id
.label
.label_text
.to_string(),
461 pool
: set
.pool
.clone(),
462 media_set_name
: media_set_name
.clone(),
463 media_set_uuid
: set
.uuid
.clone(),
464 media_set_ctime
: set
.ctime
,
466 snapshot
: snapshot
.to_owned(),
467 store
: store
.to_owned(),
468 backup_time
: backup_dir
.backup_time(),
480 schema
: MEDIA_UUID_SCHEMA
,
485 /// Get current media status
486 pub fn get_media_status(uuid
: Uuid
) -> Result
<MediaStatus
, Error
> {
488 let status_path
= Path
::new(TAPE_STATUS_DIR
);
489 let inventory
= Inventory
::load(status_path
)?
;
491 let (status
, _location
) = inventory
.status_and_location(&uuid
);
500 schema
: MEDIA_UUID_SCHEMA
,
509 /// Update media status (None, 'full', 'damaged' or 'retired')
511 /// It is not allowed to set status to 'writable' or 'unknown' (those
512 /// are internally managed states).
513 pub fn update_media_status(uuid
: Uuid
, status
: Option
<MediaStatus
>) -> Result
<(), Error
> {
515 let status_path
= Path
::new(TAPE_STATUS_DIR
);
516 let mut inventory
= Inventory
::load(status_path
)?
;
519 None
=> inventory
.clear_media_status(&uuid
)?
,
520 Some(MediaStatus
::Retired
) => inventory
.set_media_status_retired(&uuid
)?
,
521 Some(MediaStatus
::Damaged
) => inventory
.set_media_status_damaged(&uuid
)?
,
522 Some(MediaStatus
::Full
) => inventory
.set_media_status_full(&uuid
)?
,
523 Some(status
) => bail
!("setting media status '{:?}' is not allowed", status
),
529 const MEDIA_SUBDIRS
: SubdirMap
= &[
533 .get(&API_METHOD_GET_MEDIA_STATUS
)
534 .post(&API_METHOD_UPDATE_MEDIA_STATUS
)
538 pub const MEDIA_ROUTER
: Router
= Router
::new()
539 .get(&list_subdirs_api_method
!(MEDIA_SUBDIRS
))
540 .subdirs(MEDIA_SUBDIRS
);
542 pub const MEDIA_LIST_ROUTER
: Router
= Router
::new()
543 .get(&API_METHOD_LIST_MEDIA
)
544 .match_all("uuid", &MEDIA_ROUTER
);
546 const SUBDIRS
: SubdirMap
= &[
550 .get(&API_METHOD_LIST_CONTENT
)
555 .get(&API_METHOD_DESTROY_MEDIA
)
557 ( "list", &MEDIA_LIST_ROUTER
),
561 .get(&API_METHOD_LIST_MEDIA_SETS
)
566 .post(&API_METHOD_MOVE_TAPE
)
571 pub const ROUTER
: Router
= Router
::new()
572 .get(&list_subdirs_api_method
!(SUBDIRS
))