From a33389c391a2aa16307b74108bfd027628b23ab6 Mon Sep 17 00:00:00 2001 From: Dietmar Maurer Date: Tue, 29 Dec 2020 11:58:26 +0100 Subject: [PATCH] tape: implement media content list api --- src/api2/tape/media.rs | 140 ++++++++++++++++++++++++++++++++++ src/api2/types/tape/media.rs | 23 ++++++ src/bin/proxmox_tape/media.rs | 61 ++++++++++++++- 3 files changed, 223 insertions(+), 1 deletion(-) diff --git a/src/api2/tape/media.rs b/src/api2/tape/media.rs index e16477c5..cf8c8931 100644 --- a/src/api2/tape/media.rs +++ b/src/api2/tape/media.rs @@ -1,6 +1,7 @@ use std::path::Path; use anyhow::{bail, format_err, Error}; +use serde::{Serialize, Deserialize}; use proxmox::{ api::{api, Router, SubdirMap}, @@ -12,17 +13,24 @@ use crate::{ self, }, api2::types::{ + BACKUP_ID_SCHEMA, + BACKUP_TYPE_SCHEMA, MEDIA_POOL_NAME_SCHEMA, MEDIA_LABEL_SCHEMA, MediaPoolConfig, MediaListEntry, MediaStatus, + MediaContentEntry, + }, + backup::{ + BackupDir, }, tape::{ TAPE_STATUS_DIR, Inventory, MediaStateDatabase, MediaPool, + MediaCatalog, update_online_status, }, }; @@ -181,6 +189,133 @@ pub fn destroy_media(changer_id: String, force: Option,) -> Result<(), Err Ok(()) } +#[api( + properties: { + pool: { + schema: MEDIA_POOL_NAME_SCHEMA, + optional: true, + }, + "changer-id": { + schema: MEDIA_LABEL_SCHEMA, + optional: true, + }, + "media": { + description: "Filter by media UUID.", + type: String, + optional: true, + }, + "media-set": { + description: "Filter by media set UUID.", + type: String, + optional: true, + }, + "backup-type": { + schema: BACKUP_TYPE_SCHEMA, + optional: true, + }, + "backup-id": { + schema: BACKUP_ID_SCHEMA, + optional: true, + }, + }, +)] +#[derive(Serialize,Deserialize)] +#[serde(rename_all="kebab-case")] +/// Content list filter parameters +pub struct MediaContentListFilter { + pub pool: Option, + pub changer_id: Option, + pub media: Option, + pub media_set: Option, + pub backup_type: Option, + pub backup_id: Option, +} + +#[api( + input: { + properties: { + "filter": { + type: MediaContentListFilter, + flatten: true, + }, + }, + }, + returns: { + description: "Media content list.", + type: Array, + items: { + type: MediaContentEntry, + }, + }, +)] +/// List media content +pub fn list_content( + filter: MediaContentListFilter, +) -> Result, Error> { + + let (config, _digest) = config::media_pool::config()?; + + let status_path = Path::new(TAPE_STATUS_DIR); + let inventory = Inventory::load(status_path)?; + + let media_uuid = filter.media.and_then(|s| s.parse().ok()); + let media_set_uuid = filter.media_set.and_then(|s| s.parse().ok()); + + let mut list = Vec::new(); + + for media_id in inventory.list_used_media() { + let set = media_id.media_set_label.as_ref().unwrap(); + + if let Some(ref changer_id) = filter.changer_id { + if &media_id.label.changer_id != changer_id { continue; } + } + + if let Some(ref pool) = filter.pool { + if &set.pool != pool { continue; } + } + + if let Some(ref media_uuid) = media_uuid { + if &media_id.label.uuid != media_uuid { continue; } + } + + if let Some(ref media_set_uuid) = media_set_uuid { + if &set.uuid != media_set_uuid { continue; } + } + + let config: MediaPoolConfig = config.lookup("pool", &set.pool)?; + + let media_set_name = inventory + .generate_media_set_name(&set.uuid, config.template.clone()) + .unwrap_or_else(|_| set.uuid.to_string()); + + let catalog = MediaCatalog::open(status_path, &media_id.label.uuid, false, false)?; + + for snapshot in catalog.snapshot_index().keys() { + let backup_dir: BackupDir = snapshot.parse()?; + + if let Some(ref backup_type) = filter.backup_type { + if backup_dir.group().backup_type() != backup_type { continue; } + } + if let Some(ref backup_id) = filter.backup_id { + if backup_dir.group().backup_id() != backup_id { continue; } + } + + list.push(MediaContentEntry { + uuid: media_id.label.uuid.to_string(), + changer_id: media_id.label.changer_id.to_string(), + pool: set.pool.clone(), + media_set_name: media_set_name.clone(), + media_set_uuid: set.uuid.to_string(), + seq_nr: set.seq_nr, + snapshot: snapshot.to_owned(), + backup_time: backup_dir.backup_time(), + }); + } + } + + Ok(list) +} + const SUBDIRS: SubdirMap = &[ ( "destroy", @@ -192,6 +327,11 @@ const SUBDIRS: SubdirMap = &[ &Router::new() .get(&API_METHOD_LIST_MEDIA) ), + ( + "content", + &Router::new() + .get(&API_METHOD_LIST_CONTENT) + ), ]; diff --git a/src/api2/types/tape/media.rs b/src/api2/types/tape/media.rs index 956e0abc..6f0fa15c 100644 --- a/src/api2/types/tape/media.rs +++ b/src/api2/types/tape/media.rs @@ -79,3 +79,26 @@ pub struct LabelUuidMap { /// Associated Uuid (if any) pub uuid: Option, } + +#[api()] +#[derive(Serialize,Deserialize)] +#[serde(rename_all = "kebab-case")] +/// Media content list entry +pub struct MediaContentEntry { + /// Media changer ID + pub changer_id: String, + /// Media Uuid + pub uuid: String, + /// Media set name + pub media_set_name: String, + /// Media set uuid + pub media_set_uuid: String, + /// Media set seq_nr + pub seq_nr: u64, + /// Media Pool + pub pool: String, + /// Backup snapshot + pub snapshot: String, + /// Snapshot creation time (epoch) + pub backup_time: i64, +} diff --git a/src/bin/proxmox_tape/media.rs b/src/bin/proxmox_tape/media.rs index 0767bdb0..6f694b8c 100644 --- a/src/bin/proxmox_tape/media.rs +++ b/src/bin/proxmox_tape/media.rs @@ -18,8 +18,13 @@ use proxmox_backup::{ MediaStatus, MediaListEntry, }, + tape::media::MediaContentListFilter, + }, + tape::{ + complete_media_changer_id, + complete_media_uuid, + complete_media_set_uuid, }, - tape::complete_media_changer_id, config::{ media_pool::complete_pool_name, }, @@ -39,6 +44,14 @@ pub fn media_commands() -> CommandLineInterface { .arg_param(&["changer-id"]) .completion_cb("changer-id", complete_media_changer_id) ) + .insert( + "content", + CliCommand::new(&API_METHOD_LIST_CONTENT) + .completion_cb("pool", complete_pool_name) + .completion_cb("changer-id", complete_media_changer_id) + .completion_cb("media", complete_media_uuid) + .completion_cb("media-set", complete_media_set_uuid) + ) ; cmd_def.into() @@ -110,3 +123,49 @@ async fn list_media( Ok(()) } + +#[api( + input: { + properties: { + "filter": { + type: MediaContentListFilter, + flatten: true, + }, + "output-format": { + schema: OUTPUT_FORMAT, + optional: true, + }, + }, + }, +)] +/// List media content +fn list_content( + param: Value, + rpcenv: &mut dyn RpcEnvironment, +) -> Result<(), Error> { + + let output_format = get_output_format(¶m); + let info = &api2::tape::media::API_METHOD_LIST_CONTENT; + let mut data = match info.handler { + ApiHandler::Sync(handler) => (handler)(param, info, rpcenv)?, + _ => unreachable!(), + }; + + let options = default_table_format_options() + .sortby("media-set-uuid", false) + .sortby("seq-nr", false) + .sortby("snapshot", false) + .sortby("backup-time", false) + .column(ColumnConfig::new("changer-id")) + .column(ColumnConfig::new("pool")) + .column(ColumnConfig::new("media-set-name")) + .column(ColumnConfig::new("seq-nr")) + .column(ColumnConfig::new("snapshot")) + .column(ColumnConfig::new("media-set-uuid")) + ; + + format_and_print_result_full(&mut data, &info.returns, &output_format, &options); + + Ok(()) + +}