From 6f62c9240af3a06e9fcdf3f8f13d2758abdc2a9b Mon Sep 17 00:00:00 2001 From: Dietmar Maurer Date: Sun, 3 Mar 2019 11:29:00 +0100 Subject: [PATCH] src/api2/admin/datastore.rs: imp delete_snapshot --- src/api2/admin/datastore.rs | 56 ++++++++++++++++++++++++++++---- src/backup/datastore.rs | 25 ++++++++++++-- src/bin/proxmox-backup-client.rs | 37 +++++++++++++++++++++ src/client/http_client.rs | 20 ++++++++++++ 4 files changed, 128 insertions(+), 10 deletions(-) diff --git a/src/api2/admin/datastore.rs b/src/api2/admin/datastore.rs index 0c07451a..4e079ea6 100644 --- a/src/api2/admin/datastore.rs +++ b/src/api2/admin/datastore.rs @@ -6,7 +6,7 @@ use crate::api_schema::router::*; //use crate::server::rest::*; use serde_json::{json, Value}; use std::collections::{HashSet, HashMap}; -use chrono::{DateTime, Datelike, Local}; +use chrono::{DateTime, Datelike, Local, TimeZone}; use std::path::PathBuf; use std::sync::Arc; @@ -87,6 +87,33 @@ fn list_groups( Ok(json!(groups)) } +fn delete_snapshots ( + param: Value, + _info: &ApiMethod, + _rpcenv: &mut RpcEnvironment, +) -> Result { + + let store = tools::required_string_param(¶m, "store")?; + let backup_type = tools::required_string_param(¶m, "backup-type")?; + let backup_id = tools::required_string_param(¶m, "backup-id")?; + let backup_time = tools::required_integer_param(¶m, "backup-time")?; + let backup_time = Local.timestamp(backup_time, 0); + + let snapshot = BackupDir { + group: BackupGroup { + backup_type: backup_type.to_owned(), + backup_id: backup_id.to_owned(), + }, + backup_time, + }; + + let datastore = DataStore::lookup_datastore(store)?; + + datastore.remove_backup_dir(&snapshot)?; + + Ok(Value::Null) +} + fn list_snapshots ( param: Value, _info: &ApiMethod, @@ -379,12 +406,27 @@ pub fn router() -> Router { .subdir( "snapshots", Router::new() - .get(ApiMethod::new( - list_snapshots, - ObjectSchema::new("List backup groups.") - .required("store", store_schema.clone()) - .required("backup-type", StringSchema::new("Backup type.")) - .required("backup-id", StringSchema::new("Backup ID."))))) + .get( + ApiMethod::new( + list_snapshots, + ObjectSchema::new("List backup groups.") + .required("store", store_schema.clone()) + .required("backup-type", StringSchema::new("Backup type.")) + .required("backup-id", StringSchema::new("Backup ID.")) + ) + ) + .delete( + ApiMethod::new( + delete_snapshots, + ObjectSchema::new("Delete backup snapshot.") + .required("store", store_schema.clone()) + .required("backup-type", StringSchema::new("Backup type.")) + .required("backup-id", StringSchema::new("Backup ID.")) + .required("backup-time", IntegerSchema::new("Backup time (Unix epoch.)") + .minimum(1547797308)) + ) + ) + ) .subdir( "prune", Router::new() diff --git a/src/backup/datastore.rs b/src/backup/datastore.rs index 5111be5a..48438915 100644 --- a/src/backup/datastore.rs +++ b/src/backup/datastore.rs @@ -72,6 +72,21 @@ pub struct BackupDir { impl BackupDir { + pub fn parse(path: &str) -> Result { + + let cap = SNAPSHOT_PATH_REGEX.captures(path) + .ok_or_else(|| format_err!("unable to parse backup snapshot path '{}'", path))?; + + + Ok(Self { + group: BackupGroup { + backup_type: cap.get(1).unwrap().as_str().to_owned(), + backup_id: cap.get(2).unwrap().as_str().to_owned(), + }, + backup_time: cap.get(3).unwrap().as_str().parse::>()?, + }) + } + fn backup_time_to_file_name(backup_time: DateTime) -> String { backup_time.to_rfc3339().to_string() } @@ -96,8 +111,9 @@ pub struct BackupInfo { } -macro_rules! BACKUP_ID_RE { () => ("[A-Za-z0-9][A-Za-z0-9_-]+") } -macro_rules! BACKUP_TYPE_RE { () => ("(?:host|vm|ct)") } +macro_rules! BACKUP_ID_RE { () => (r"[A-Za-z0-9][A-Za-z0-9_-]+") } +macro_rules! BACKUP_TYPE_RE { () => (r"(?:host|vm|ct)") } +macro_rules! BACKUP_TIME_RE { () => (r"[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\+[0-9]{2}:[0-9]{2}") } lazy_static!{ static ref datastore_map: Mutex>> = Mutex::new(HashMap::new()); @@ -112,11 +128,14 @@ lazy_static!{ concat!(r"^", BACKUP_ID_RE!(), r"$")).unwrap(); static ref BACKUP_DATE_REGEX: Regex = Regex::new( - r"^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\+[0-9]{2}:[0-9]{2}$").unwrap(); + concat!(r"^", BACKUP_TIME_RE!() ,r"$")).unwrap(); static ref GROUP_PATH_REGEX: Regex = Regex::new( concat!(r"(", BACKUP_TYPE_RE!(), ")/(", BACKUP_ID_RE!(), r")$")).unwrap(); + static ref SNAPSHOT_PATH_REGEX: Regex = Regex::new( + concat!(r"(", BACKUP_TYPE_RE!(), ")/(", BACKUP_ID_RE!(), ")/(", BACKUP_TIME_RE!(), r")$")).unwrap(); + } impl DataStore { diff --git a/src/bin/proxmox-backup-client.rs b/src/bin/proxmox-backup-client.rs index 9970ddc3..7448ce5b 100644 --- a/src/bin/proxmox-backup-client.rs +++ b/src/bin/proxmox-backup-client.rs @@ -235,6 +235,33 @@ fn list_snapshots( Ok(Value::Null) } +fn forget_snapshots( + param: Value, + _info: &ApiMethod, + _rpcenv: &mut RpcEnvironment, +) -> Result { + + let repo_url = tools::required_string_param(¶m, "repository")?; + let repo = BackupRepository::parse(repo_url)?; + + let path = tools::required_string_param(¶m, "snapshot")?; + let snapshot = BackupDir::parse(path)?; + + let query = tools::json_object_to_query(json!({ + "backup-type": &snapshot.group.backup_type, + "backup-id": &snapshot.group.backup_id, + "backup-time": snapshot.backup_time.timestamp(), + }))?; + + let mut client = HttpClient::new(&repo.host, &repo.user); + + let path = format!("api2/json/admin/datastore/{}/snapshots?{}", repo.store, query); + + let result = client.delete(&path)?; + + Ok(result) +} + fn start_garbage_collection( param: Value, _info: &ApiMethod, @@ -433,6 +460,15 @@ fn main() { )) .arg_param(vec!["repository", "group"]); + let forget_cmd_def = CliCommand::new( + ApiMethod::new( + forget_snapshots, + ObjectSchema::new("Forget (remove) backup snapshots.") + .required("repository", repo_url_schema.clone()) + .required("snapshot", StringSchema::new("Snapshot path.")) + )) + .arg_param(vec!["repository", "snapshot"]); + let garbage_collect_cmd_def = CliCommand::new( ApiMethod::new( start_garbage_collection, @@ -452,6 +488,7 @@ fn main() { .arg_param(vec!["repository"]); let cmd_def = CliCommandMap::new() .insert("create".to_owned(), create_cmd_def.into()) + .insert("forget".to_owned(), forget_cmd_def.into()) .insert("garbage-collect".to_owned(), garbage_collect_cmd_def.into()) .insert("list".to_owned(), list_cmd_def.into()) .insert("prune".to_owned(), prune_cmd_def.into()) diff --git a/src/client/http_client.rs b/src/client/http_client.rs index da902b15..740ac8bc 100644 --- a/src/client/http_client.rs +++ b/src/client/http_client.rs @@ -121,6 +121,26 @@ impl HttpClient { Self::run_request(request) } + pub fn delete(&mut self, path: &str) -> Result { + + let path = path.trim_matches('/'); + let url: Uri = format!("https://{}:8007/{}", self.server, path).parse()?; + + let (ticket, token) = self.login()?; + + let enc_ticket = percent_encode(ticket.as_bytes(), DEFAULT_ENCODE_SET).to_string(); + + let request = Request::builder() + .method("DELETE") + .uri(url) + .header("User-Agent", "proxmox-backup-client/1.0") + .header("Cookie", format!("PBSAuthCookie={}", enc_ticket)) + .header("CSRFPreventionToken", token) + .body(Body::empty())?; + + Self::run_request(request) + } + pub fn post(&mut self, path: &str) -> Result { let path = path.trim_matches('/');