]> git.proxmox.com Git - proxmox-backup.git/blob - src/api2/tape/media.rs
move acl to pbs_config workspaces, pbs_api_types cleanups
[proxmox-backup.git] / src / api2 / tape / media.rs
1 use std::path::Path;
2 use std::collections::HashSet;
3
4 use anyhow::{bail, format_err, Error};
5
6 use proxmox::{
7 api::{api, Router, SubdirMap, RpcEnvironment, Permission},
8 list_subdirs_api_method,
9 tools::Uuid,
10 };
11
12 use pbs_datastore::backup_info::BackupDir;
13 use 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,
17 PRIV_TAPE_AUDIT,
18 };
19
20 use crate::{
21 config::cached_user_info::CachedUserInfo,
22 tape::{
23 TAPE_STATUS_DIR,
24 Inventory,
25 MediaPool,
26 MediaCatalog,
27 media_catalog_snapshot_list,
28 changer::update_online_status,
29 },
30 };
31
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
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()?;
51
52 let (config, _digest) = pbs_config::media_pool::config()?;
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 }
100 #[api(
101 input: {
102 properties: {
103 pool: {
104 schema: MEDIA_POOL_NAME_SCHEMA,
105 optional: true,
106 },
107 "update-status": {
108 description: "Try to update tape library status (check what tapes are online).",
109 optional: true,
110 default: true,
111 },
112 "update-status-changer": {
113 // only update status for a single changer
114 schema: CHANGER_NAME_SCHEMA,
115 optional: true,
116 },
117 },
118 },
119 returns: {
120 description: "List of registered backup media.",
121 type: Array,
122 items: {
123 type: MediaListEntry,
124 },
125 },
126 access: {
127 description: "List of registered backup media filtered by Tape.Audit privileges on pool",
128 permission: &Permission::Anybody,
129 },
130 )]
131 /// List pool media
132 pub async fn list_media(
133 pool: Option<String>,
134 update_status: bool,
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()?;
140
141 let (config, _digest) = pbs_config::media_pool::config()?;
142
143 let status_path = Path::new(TAPE_STATUS_DIR);
144
145 let catalogs = tokio::task::spawn_blocking(move || {
146 if update_status {
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");
151 }
152 }
153 // test what catalog files we have
154 MediaCatalog::media_with_catalogs(status_path)
155 }).await??;
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
170 let privs = user_info.lookup_privs(&auth_id, &["tape", "pool", pool_name]);
171 if (privs & PRIV_TAPE_AUDIT) == 0 {
172 continue;
173 }
174
175 let config: MediaPoolConfig = config.lookup("pool", pool_name)?;
176
177 let changer_name = None; // assume standalone drive
178 let mut pool = MediaPool::with_config(status_path, &config, changer_name, true)?;
179
180 let current_time = proxmox::tools::time::epoch_i64();
181
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)?;
186
187 for media in pool.list_media() {
188 let expired = pool.media_is_expired(&media, current_time);
189
190 let media_set_uuid = media.media_set_label()
191 .map(|set| set.uuid.clone());
192
193 let seq_nr = media.media_set_label()
194 .map(|set| set.seq_nr);
195
196 let media_set_name = media.media_set_label()
197 .map(|set| {
198 pool.generate_media_set_name(&set.uuid, config.template.clone())
199 .unwrap_or_else(|_| set.uuid.to_string())
200 });
201
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
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(),
216 catalog: catalog_ok,
217 expired,
218 media_set_ctime: media.media_set_label().map(|set| set.ctime),
219 media_set_uuid,
220 media_set_name,
221 seq_nr,
222 });
223 }
224 }
225
226 let inventory = Inventory::load(status_path)?;
227
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 });
254 }
255 }
256 }
257
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
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
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
298 Ok(list)
299 }
300
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.
315 pub 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
338 #[api(
339 input: {
340 properties: {
341 "label-text": {
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)
353 pub fn destroy_media(label_text: String, force: Option<bool>,) -> Result<(), Error> {
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
360 let media_id = inventory.find_media_by_label_text(&label_text)
361 .ok_or_else(|| format_err!("no such media '{}'", label_text))?;
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 {
367 bail!("media '{}' contains data (please use 'force' flag to remove.", label_text);
368 }
369 }
370 }
371
372 let uuid = media_id.label.uuid.clone();
373
374 inventory.remove_media(&uuid)?;
375
376 Ok(())
377 }
378
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 },
395 access: {
396 description: "List content filtered by Tape.Audit privilege on pool",
397 permission: &Permission::Anybody,
398 },
399 )]
400 /// List media content
401 pub fn list_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()?;
407
408 let (config, _digest) = pbs_config::media_pool::config()?;
409
410 let status_path = Path::new(TAPE_STATUS_DIR);
411 let inventory = Inventory::load(status_path)?;
412
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
418 if let Some(ref label_text) = filter.label_text {
419 if &media_id.label.label_text != label_text { continue; }
420 }
421
422 if let Some(ref pool) = filter.pool {
423 if &set.pool != pool { continue; }
424 }
425
426 let privs = user_info.lookup_privs(&auth_id, &["tape", "pool", &set.pool]);
427 if (privs & PRIV_TAPE_AUDIT) == 0 {
428 continue;
429 }
430
431 if let Some(ref media_uuid) = filter.media {
432 if &media_id.label.uuid != media_uuid { continue; }
433 }
434
435 if let Some(ref media_set_uuid) = filter.media_set {
436 if &set.uuid != media_set_uuid { continue; }
437 }
438
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 };
443
444 let media_set_name = inventory
445 .generate_media_set_name(&set.uuid, template)
446 .unwrap_or_else(|_| set.uuid.to_string());
447
448 for (store, snapshot) in media_catalog_snapshot_list(status_path, &media_id)? {
449 let backup_dir: BackupDir = snapshot.parse()?;
450
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; }
456 }
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 });
470 }
471 }
472
473 Ok(list)
474 }
475
476 #[api(
477 input: {
478 properties: {
479 uuid: {
480 schema: MEDIA_UUID_SCHEMA,
481 },
482 },
483 },
484 )]
485 /// Get current media status
486 pub 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
512 /// are internally managed states).
513 pub 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
529 const 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
538 pub const MEDIA_ROUTER: Router = Router::new()
539 .get(&list_subdirs_api_method!(MEDIA_SUBDIRS))
540 .subdirs(MEDIA_SUBDIRS);
541
542 pub const MEDIA_LIST_ROUTER: Router = Router::new()
543 .get(&API_METHOD_LIST_MEDIA)
544 .match_all("uuid", &MEDIA_ROUTER);
545
546 const SUBDIRS: SubdirMap = &[
547 (
548 "content",
549 &Router::new()
550 .get(&API_METHOD_LIST_CONTENT)
551 ),
552 (
553 "destroy",
554 &Router::new()
555 .get(&API_METHOD_DESTROY_MEDIA)
556 ),
557 ( "list", &MEDIA_LIST_ROUTER ),
558 (
559 "media-sets",
560 &Router::new()
561 .get(&API_METHOD_LIST_MEDIA_SETS)
562 ),
563 (
564 "move",
565 &Router::new()
566 .post(&API_METHOD_MOVE_TAPE)
567 ),
568 ];
569
570
571 pub const ROUTER: Router = Router::new()
572 .get(&list_subdirs_api_method!(SUBDIRS))
573 .subdirs(SUBDIRS);