]> git.proxmox.com Git - proxmox-backup.git/blame - src/api2/tape/media.rs
move acl to pbs_config workspaces, pbs_api_types cleanups
[proxmox-backup.git] / src / api2 / tape / media.rs
CommitLineData
fba0b774 1use std::path::Path;
03380db5 2use std::collections::HashSet;
fba0b774 3
fb657d8e 4use anyhow::{bail, format_err, Error};
fba0b774 5
fb657d8e 6use proxmox::{
8a76e711 7 api::{api, Router, SubdirMap, RpcEnvironment, Permission},
fb657d8e 8 list_subdirs_api_method,
f490dda0 9 tools::Uuid,
fb657d8e 10};
fba0b774 11
b2065dc7 12use pbs_datastore::backup_info::BackupDir;
1ce8e905
DM
13use pbs_api_types::{
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,
8cc3760e 17 PRIV_TAPE_AUDIT,
1ce8e905 18};
b2065dc7 19
fba0b774 20use crate::{
8cc3760e 21 config::cached_user_info::CachedUserInfo,
aad2d162 22 tape::{
fba0b774
DM
23 TAPE_STATUS_DIR,
24 Inventory,
fba0b774 25 MediaPool,
a33389c3 26 MediaCatalog,
855b55dc 27 media_catalog_snapshot_list,
37796ff7 28 changer::update_online_status,
fba0b774
DM
29 },
30};
31
03380db5
DC
32#[api(
33 returns: {
34 description: "List of media sets.",
35 type: Array,
36 items: {
37 type: MediaSetListEntry,
38 },
39 },
40 access: {
41 description: "List of media sets filtered by Tape.Audit privileges on pool",
42 permission: &Permission::Anybody,
43 },
44)]
45/// List Media sets
46pub 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()?;
51
aad2d162 52 let (config, _digest) = pbs_config::media_pool::config()?;
03380db5
DC
53
54 let status_path = Path::new(TAPE_STATUS_DIR);
55
56 let mut media_sets: HashSet<Uuid> = HashSet::new();
57 let mut list = Vec::new();
58
59 for (_section_type, data) in config.sections.values() {
60 let pool_name = match data["name"].as_str() {
61 None => continue,
62 Some(name) => name,
63 };
64
65 let privs = user_info.lookup_privs(&auth_id, &["tape", "pool", pool_name]);
66 if (privs & PRIV_TAPE_AUDIT) == 0 {
67 continue;
68 }
69
70 let config: MediaPoolConfig = config.lookup("pool", pool_name)?;
71
72 let changer_name = None; // assume standalone drive
73 let pool = MediaPool::with_config(status_path, &config, changer_name, true)?;
74
75 for media in pool.list_media() {
76 if let Some(label) = media.media_set_label() {
77 if media_sets.contains(&label.uuid) {
78 continue;
79 }
80
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());
86
87 media_sets.insert(media_set_uuid.clone());
88 list.push(MediaSetListEntry {
89 media_set_name,
90 media_set_uuid,
91 media_set_ctime,
92 pool: pool_name.to_string(),
93 });
94 }
95 }
96 }
97
98 Ok(list)
99}
fba0b774
DM
100#[api(
101 input: {
102 properties: {
103 pool: {
104 schema: MEDIA_POOL_NAME_SCHEMA,
105 optional: true,
106 },
159100b9
DM
107 "update-status": {
108 description: "Try to update tape library status (check what tapes are online).",
109 optional: true,
110 default: true,
111 },
9bbd83b1
DM
112 "update-status-changer": {
113 // only update status for a single changer
114 schema: CHANGER_NAME_SCHEMA,
115 optional: true,
116 },
fba0b774
DM
117 },
118 },
119 returns: {
120 description: "List of registered backup media.",
121 type: Array,
122 items: {
123 type: MediaListEntry,
124 },
125 },
8a76e711
DM
126 access: {
127 description: "List of registered backup media filtered by Tape.Audit privileges on pool",
128 permission: &Permission::Anybody,
129 },
fba0b774
DM
130)]
131/// List pool media
159100b9
DM
132pub async fn list_media(
133 pool: Option<String>,
134 update_status: bool,
9bbd83b1 135 update_status_changer: Option<String>,
8a76e711 136 rpcenv: &mut dyn RpcEnvironment,
159100b9 137) -> Result<Vec<MediaListEntry>, Error> {
8a76e711
DM
138 let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
139 let user_info = CachedUserInfo::new()?;
fba0b774 140
aad2d162 141 let (config, _digest) = pbs_config::media_pool::config()?;
fba0b774
DM
142
143 let status_path = Path::new(TAPE_STATUS_DIR);
144
0bf1c314 145 let catalogs = tokio::task::spawn_blocking(move || {
159100b9
DM
146 if update_status {
147 // update online media status
9bbd83b1 148 if let Err(err) = update_online_status(status_path, update_status_changer.as_deref()) {
159100b9
DM
149 eprintln!("{}", err);
150 eprintln!("update online media status failed - using old state");
151 }
fba0b774 152 }
0bf1c314
DM
153 // test what catalog files we have
154 MediaCatalog::media_with_catalogs(status_path)
155 }).await??;
fba0b774
DM
156
157 let mut list = Vec::new();
158
159 for (_section_type, data) in config.sections.values() {
160 let pool_name = match data["name"].as_str() {
161 None => continue,
162 Some(name) => name,
163 };
164 if let Some(ref name) = pool {
165 if name != pool_name {
166 continue;
167 }
168 }
169
8a76e711
DM
170 let privs = user_info.lookup_privs(&auth_id, &["tape", "pool", pool_name]);
171 if (privs & PRIV_TAPE_AUDIT) == 0 {
172 continue;
173 }
174
fba0b774
DM
175 let config: MediaPoolConfig = config.lookup("pool", pool_name)?;
176
ab77d660 177 let changer_name = None; // assume standalone drive
30316192 178 let mut pool = MediaPool::with_config(status_path, &config, changer_name, true)?;
fba0b774
DM
179
180 let current_time = proxmox::tools::time::epoch_i64();
181
ab77d660
DM
182 // Call start_write_session, so that we show the same status a
183 // backup job would see.
184 pool.force_media_availability();
e953029e 185 pool.start_write_session(current_time, false)?;
ab77d660 186
fba0b774 187 for media in pool.list_media() {
fba0b774
DM
188 let expired = pool.media_is_expired(&media, current_time);
189
6543214d 190 let media_set_uuid = media.media_set_label()
f490dda0 191 .map(|set| set.uuid.clone());
fba0b774 192
6543214d 193 let seq_nr = media.media_set_label()
fba0b774
DM
194 .map(|set| set.seq_nr);
195
6543214d 196 let media_set_name = media.media_set_label()
fba0b774
DM
197 .map(|set| {
198 pool.generate_media_set_name(&set.uuid, config.template.clone())
199 .unwrap_or_else(|_| set.uuid.to_string())
200 });
201
0bf1c314
DM
202 let catalog_ok = if media.media_set_label().is_none() {
203 // Media is empty, we need no catalog
204 true
205 } else {
206 catalogs.contains(media.uuid())
207 };
208
fba0b774 209 list.push(MediaListEntry {
f490dda0 210 uuid: media.uuid().clone(),
8446fbca 211 label_text: media.label_text().to_string(),
6543214d 212 ctime: media.ctime(),
fba0b774 213 pool: Some(pool_name.to_string()),
c1c2c8f6 214 location: media.location().clone(),
fba0b774 215 status: *media.status(),
0bf1c314 216 catalog: catalog_ok,
fba0b774 217 expired,
6543214d 218 media_set_ctime: media.media_set_label().map(|set| set.ctime),
fba0b774
DM
219 media_set_uuid,
220 media_set_name,
221 seq_nr,
222 });
223 }
224 }
225
955f4aef 226 let inventory = Inventory::load(status_path)?;
fba0b774 227
8a76e711
DM
228 let privs = user_info.lookup_privs(&auth_id, &["tape", "pool"]);
229 if (privs & PRIV_TAPE_AUDIT) != 0 {
230 if pool.is_none() {
231
232 for media_id in inventory.list_unassigned_media() {
233
234 let (mut status, location) = inventory.status_and_location(&media_id.label.uuid);
235
236 if status == MediaStatus::Unknown {
237 status = MediaStatus::Writable;
238 }
239
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(),
244 location,
245 status,
246 catalog: true, // empty, so we do not need a catalog
247 expired: false,
248 media_set_uuid: None,
249 media_set_name: None,
250 media_set_ctime: None,
251 seq_nr: None,
252 pool: None,
253 });
fba0b774 254 }
fba0b774
DM
255 }
256 }
257
955f4aef
DM
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,
264 None => continue,
265 };
266
267 if config.sections.get(&media_set_label.pool).is_some() {
268 continue;
269 }
270
8a76e711
DM
271 let privs = user_info.lookup_privs(&auth_id, &["tape", "pool", &media_set_label.pool]);
272 if (privs & PRIV_TAPE_AUDIT) == 0 {
273 continue;
274 }
275
955f4aef
DM
276 let (_status, location) = inventory.status_and_location(uuid);
277
278 let media_set_name = inventory.generate_media_set_name(&media_set_label.uuid, None)?;
279
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()),
285 location,
286 status: MediaStatus::Unknown,
287 catalog: catalogs.contains(uuid),
288 expired: false,
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),
293 });
294
295 }
296
297
fba0b774
DM
298 Ok(list)
299}
300
56d22c66
DC
301#[api(
302 input: {
303 properties: {
304 "label-text": {
305 schema: MEDIA_LABEL_SCHEMA,
306 },
307 "vault-name": {
308 schema: VAULT_NAME_SCHEMA,
309 optional: true,
310 },
311 },
312 },
313)]
314/// Change Tape location to vault (if given), or offline.
315pub fn move_tape(
316 label_text: String,
317 vault_name: Option<String>,
318) -> Result<(), Error> {
319
320 let status_path = Path::new(TAPE_STATUS_DIR);
321 let mut inventory = Inventory::load(status_path)?;
322
323 let uuid = inventory.find_media_by_label_text(&label_text)
324 .ok_or_else(|| format_err!("no such media '{}'", label_text))?
325 .label
326 .uuid
327 .clone();
328
329 if let Some(vault_name) = vault_name {
330 inventory.set_media_location_vault(&uuid, &vault_name)?;
331 } else {
332 inventory.set_media_location_offline(&uuid)?;
333 }
334
335 Ok(())
336}
337
fb657d8e
DM
338#[api(
339 input: {
340 properties: {
8446fbca 341 "label-text": {
fb657d8e
DM
342 schema: MEDIA_LABEL_SCHEMA,
343 },
344 force: {
345 description: "Force removal (even if media is used in a media set).",
346 type: bool,
347 optional: true,
348 },
349 },
350 },
351)]
352/// Destroy media (completely remove from database)
8446fbca 353pub fn destroy_media(label_text: String, force: Option<bool>,) -> Result<(), Error> {
fb657d8e
DM
354
355 let force = force.unwrap_or(false);
356
357 let status_path = Path::new(TAPE_STATUS_DIR);
358 let mut inventory = Inventory::load(status_path)?;
359
8446fbca
DM
360 let media_id = inventory.find_media_by_label_text(&label_text)
361 .ok_or_else(|| format_err!("no such media '{}'", label_text))?;
fb657d8e
DM
362
363 if !force {
364 if let Some(ref set) = media_id.media_set_label {
365 let is_empty = set.uuid.as_ref() == [0u8;16];
366 if !is_empty {
8446fbca 367 bail!("media '{}' contains data (please use 'force' flag to remove.", label_text);
fb657d8e
DM
368 }
369 }
370 }
371
372 let uuid = media_id.label.uuid.clone();
fb657d8e
DM
373
374 inventory.remove_media(&uuid)?;
375
fb657d8e
DM
376 Ok(())
377}
378
a33389c3
DM
379#[api(
380 input: {
381 properties: {
382 "filter": {
383 type: MediaContentListFilter,
384 flatten: true,
385 },
386 },
387 },
388 returns: {
389 description: "Media content list.",
390 type: Array,
391 items: {
392 type: MediaContentEntry,
393 },
394 },
8a76e711
DM
395 access: {
396 description: "List content filtered by Tape.Audit privilege on pool",
397 permission: &Permission::Anybody,
398 },
a33389c3
DM
399)]
400/// List media content
401pub fn list_content(
402 filter: MediaContentListFilter,
8a76e711 403 rpcenv: &mut dyn RpcEnvironment,
a33389c3 404) -> Result<Vec<MediaContentEntry>, Error> {
8a76e711
DM
405 let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
406 let user_info = CachedUserInfo::new()?;
a33389c3 407
aad2d162 408 let (config, _digest) = pbs_config::media_pool::config()?;
a33389c3
DM
409
410 let status_path = Path::new(TAPE_STATUS_DIR);
411 let inventory = Inventory::load(status_path)?;
412
a33389c3
DM
413 let mut list = Vec::new();
414
415 for media_id in inventory.list_used_media() {
416 let set = media_id.media_set_label.as_ref().unwrap();
417
8446fbca
DM
418 if let Some(ref label_text) = filter.label_text {
419 if &media_id.label.label_text != label_text { continue; }
a33389c3
DM
420 }
421
422 if let Some(ref pool) = filter.pool {
423 if &set.pool != pool { continue; }
424 }
425
8a76e711
DM
426 let privs = user_info.lookup_privs(&auth_id, &["tape", "pool", &set.pool]);
427 if (privs & PRIV_TAPE_AUDIT) == 0 {
428 continue;
429 }
430
f490dda0 431 if let Some(ref media_uuid) = filter.media {
a33389c3
DM
432 if &media_id.label.uuid != media_uuid { continue; }
433 }
434
f490dda0 435 if let Some(ref media_set_uuid) = filter.media_set {
a33389c3
DM
436 if &set.uuid != media_set_uuid { continue; }
437 }
438
955f4aef
DM
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
442 };
a33389c3
DM
443
444 let media_set_name = inventory
955f4aef 445 .generate_media_set_name(&set.uuid, template)
a33389c3
DM
446 .unwrap_or_else(|_| set.uuid.to_string());
447
855b55dc
DC
448 for (store, snapshot) in media_catalog_snapshot_list(status_path, &media_id)? {
449 let backup_dir: BackupDir = snapshot.parse()?;
a33389c3 450
855b55dc
DC
451 if let Some(ref backup_type) = filter.backup_type {
452 if backup_dir.group().backup_type() != backup_type { continue; }
453 }
454 if let Some(ref backup_id) = filter.backup_id {
455 if backup_dir.group().backup_id() != backup_id { continue; }
54722aca 456 }
855b55dc
DC
457
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,
465 seq_nr: set.seq_nr,
466 snapshot: snapshot.to_owned(),
467 store: store.to_owned(),
468 backup_time: backup_dir.backup_time(),
469 });
a33389c3
DM
470 }
471 }
472
473 Ok(list)
474}
475
08ec39be
DM
476#[api(
477 input: {
478 properties: {
479 uuid: {
480 schema: MEDIA_UUID_SCHEMA,
481 },
482 },
483 },
484)]
485/// Get current media status
486pub fn get_media_status(uuid: Uuid) -> Result<MediaStatus, Error> {
487
488 let status_path = Path::new(TAPE_STATUS_DIR);
489 let inventory = Inventory::load(status_path)?;
490
491 let (status, _location) = inventory.status_and_location(&uuid);
492
493 Ok(status)
494}
495
496#[api(
497 input: {
498 properties: {
499 uuid: {
500 schema: MEDIA_UUID_SCHEMA,
501 },
502 status: {
503 type: MediaStatus,
504 optional: true,
505 },
506 },
507 },
508)]
509/// Update media status (None, 'full', 'damaged' or 'retired')
510///
511/// It is not allowed to set status to 'writable' or 'unknown' (those
d1d74c43 512/// are internally managed states).
08ec39be
DM
513pub fn update_media_status(uuid: Uuid, status: Option<MediaStatus>) -> Result<(), Error> {
514
515 let status_path = Path::new(TAPE_STATUS_DIR);
516 let mut inventory = Inventory::load(status_path)?;
517
518 match status {
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),
524 }
525
526 Ok(())
527}
528
529const MEDIA_SUBDIRS: SubdirMap = &[
530 (
531 "status",
532 &Router::new()
533 .get(&API_METHOD_GET_MEDIA_STATUS)
534 .post(&API_METHOD_UPDATE_MEDIA_STATUS)
535 ),
536];
537
538pub const MEDIA_ROUTER: Router = Router::new()
539 .get(&list_subdirs_api_method!(MEDIA_SUBDIRS))
540 .subdirs(MEDIA_SUBDIRS);
541
542pub const MEDIA_LIST_ROUTER: Router = Router::new()
543 .get(&API_METHOD_LIST_MEDIA)
544 .match_all("uuid", &MEDIA_ROUTER);
545
fba0b774 546const SUBDIRS: SubdirMap = &[
250c29ed
DM
547 (
548 "content",
549 &Router::new()
550 .get(&API_METHOD_LIST_CONTENT)
551 ),
fb657d8e
DM
552 (
553 "destroy",
554 &Router::new()
555 .get(&API_METHOD_DESTROY_MEDIA)
556 ),
08ec39be 557 ( "list", &MEDIA_LIST_ROUTER ),
03380db5
DC
558 (
559 "media-sets",
560 &Router::new()
561 .get(&API_METHOD_LIST_MEDIA_SETS)
562 ),
56d22c66
DC
563 (
564 "move",
565 &Router::new()
566 .post(&API_METHOD_MOVE_TAPE)
567 ),
fba0b774
DM
568];
569
570
571pub const ROUTER: Router = Router::new()
572 .get(&list_subdirs_api_method!(SUBDIRS))
573 .subdirs(SUBDIRS);