Compare commits
13 Commits
Author | SHA1 | Date | |
---|---|---|---|
96f35520a0 | |||
490560e0c6 | |||
52f53d8280 | |||
27b8a3f671 | |||
abf9b6da42 | |||
0c9209b04c | |||
edebd52374 | |||
61205f00fb | |||
a303e00289 | |||
af9f72e9d8 | |||
5176346b30 | |||
731eeef25b | |||
a65e3e4bc0 |
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "proxmox-backup"
|
||||
version = "1.0.3"
|
||||
version = "1.0.5"
|
||||
authors = [
|
||||
"Dietmar Maurer <dietmar@proxmox.com>",
|
||||
"Dominik Csapak <d.csapak@proxmox.com>",
|
||||
@ -48,7 +48,7 @@ percent-encoding = "2.1"
|
||||
pin-utils = "0.1.0"
|
||||
pin-project = "0.4"
|
||||
pathpatterns = "0.1.2"
|
||||
proxmox = { version = "0.7.1", features = [ "sortable-macro", "api-macro", "websocket" ] }
|
||||
proxmox = { version = "0.7.2", features = [ "sortable-macro", "api-macro", "websocket" ] }
|
||||
#proxmox = { git = "git://git.proxmox.com/git/proxmox", version = "0.1.2", features = [ "sortable-macro", "api-macro" ] }
|
||||
#proxmox = { path = "../proxmox/proxmox", features = [ "sortable-macro", "api-macro", "websocket" ] }
|
||||
proxmox-fuse = "0.1.0"
|
||||
|
16
debian/changelog
vendored
16
debian/changelog
vendored
@ -1,3 +1,19 @@
|
||||
rust-proxmox-backup (1.0.5-1) unstable; urgency=medium
|
||||
|
||||
* client: restore: print meta information exclusively to standard error
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Wed, 25 Nov 2020 15:29:58 +0100
|
||||
|
||||
rust-proxmox-backup (1.0.4-1) unstable; urgency=medium
|
||||
|
||||
* fingerprint: add bytes() accessor
|
||||
|
||||
* ui: fix broken gettext use
|
||||
|
||||
* cli: move more commands into "snapshot" sub-command
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Wed, 25 Nov 2020 06:37:41 +0100
|
||||
|
||||
rust-proxmox-backup (1.0.3-1) unstable; urgency=medium
|
||||
|
||||
* client: inform user when automatically using the default encryption key
|
||||
|
8
debian/control
vendored
8
debian/control
vendored
@ -35,10 +35,10 @@ Build-Depends: debhelper (>= 11),
|
||||
librust-percent-encoding-2+default-dev (>= 2.1-~~),
|
||||
librust-pin-project-0.4+default-dev,
|
||||
librust-pin-utils-0.1+default-dev,
|
||||
librust-proxmox-0.7+api-macro-dev (>= 0.7.1-~~),
|
||||
librust-proxmox-0.7+default-dev (>= 0.7.1-~~),
|
||||
librust-proxmox-0.7+sortable-macro-dev (>= 0.7.1-~~),
|
||||
librust-proxmox-0.7+websocket-dev (>= 0.7.1-~~),
|
||||
librust-proxmox-0.7+api-macro-dev (>= 0.7.2-~~),
|
||||
librust-proxmox-0.7+default-dev (>= 0.7.2-~~),
|
||||
librust-proxmox-0.7+sortable-macro-dev (>= 0.7.2-~~),
|
||||
librust-proxmox-0.7+websocket-dev (>= 0.7.2-~~),
|
||||
librust-proxmox-fuse-0.1+default-dev,
|
||||
librust-pxar-0.6+default-dev (>= 0.6.1-~~),
|
||||
librust-pxar-0.6+futures-io-dev (>= 0.6.1-~~),
|
||||
|
@ -392,11 +392,11 @@ periodic recovery tests to ensure that you can access the data in
|
||||
case of problems.
|
||||
|
||||
First, you need to find the snapshot which you want to restore. The snapshot
|
||||
command provides a list of all the snapshots on the server:
|
||||
list command provides a list of all the snapshots on the server:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
# proxmox-backup-client snapshots
|
||||
# proxmox-backup-client snapshot list
|
||||
┌────────────────────────────────┬─────────────┬────────────────────────────────────┐
|
||||
│ snapshot │ size │ files │
|
||||
╞════════════════════════════════╪═════════════╪════════════════════════════════════╡
|
||||
@ -581,7 +581,7 @@ command:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
# proxmox-backup-client forget <snapshot>
|
||||
# proxmox-backup-client snapshot forget <snapshot>
|
||||
|
||||
|
||||
.. caution:: This command removes all archives in this backup
|
||||
|
@ -47,6 +47,15 @@ pub struct Fingerprint {
|
||||
bytes: [u8; 32],
|
||||
}
|
||||
|
||||
impl Fingerprint {
|
||||
pub fn new(bytes: [u8; 32]) -> Self {
|
||||
Self { bytes }
|
||||
}
|
||||
pub fn bytes(&self) -> &[u8; 32] {
|
||||
&self.bytes
|
||||
}
|
||||
}
|
||||
|
||||
/// Display as short key ID
|
||||
impl Display for Fingerprint {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
@ -126,9 +135,7 @@ impl CryptConfig {
|
||||
}
|
||||
|
||||
pub fn fingerprint(&self) -> Fingerprint {
|
||||
Fingerprint {
|
||||
bytes: self.compute_digest(&FINGERPRINT_INPUT)
|
||||
}
|
||||
Fingerprint::new(self.compute_digest(&FINGERPRINT_INPUT))
|
||||
}
|
||||
|
||||
pub fn data_crypter(&self, iv: &[u8; 16], mode: Mode) -> Result<Crypter, Error> {
|
||||
|
@ -53,7 +53,6 @@ use proxmox_backup::backup::{
|
||||
ChunkStream,
|
||||
CryptConfig,
|
||||
CryptMode,
|
||||
DataBlob,
|
||||
DynamicIndexReader,
|
||||
FixedChunkStream,
|
||||
FixedIndexReader,
|
||||
@ -456,112 +455,6 @@ async fn change_backup_owner(group: String, mut param: Value) -> Result<(), Erro
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[api(
|
||||
input: {
|
||||
properties: {
|
||||
repository: {
|
||||
schema: REPO_URL_SCHEMA,
|
||||
optional: true,
|
||||
},
|
||||
group: {
|
||||
type: String,
|
||||
description: "Backup group.",
|
||||
optional: true,
|
||||
},
|
||||
"output-format": {
|
||||
schema: OUTPUT_FORMAT,
|
||||
optional: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
)]
|
||||
/// List backup snapshots.
|
||||
async fn list_snapshots(param: Value) -> Result<Value, Error> {
|
||||
|
||||
let repo = extract_repository_from_value(¶m)?;
|
||||
|
||||
let output_format = get_output_format(¶m);
|
||||
|
||||
let client = connect(&repo)?;
|
||||
|
||||
let group: Option<BackupGroup> = if let Some(path) = param["group"].as_str() {
|
||||
Some(path.parse()?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut data = api_datastore_list_snapshots(&client, repo.store(), group).await?;
|
||||
|
||||
record_repository(&repo);
|
||||
|
||||
let render_snapshot_path = |_v: &Value, record: &Value| -> Result<String, Error> {
|
||||
let item: SnapshotListItem = serde_json::from_value(record.to_owned())?;
|
||||
let snapshot = BackupDir::new(item.backup_type, item.backup_id, item.backup_time)?;
|
||||
Ok(snapshot.relative_path().to_str().unwrap().to_owned())
|
||||
};
|
||||
|
||||
let render_files = |_v: &Value, record: &Value| -> Result<String, Error> {
|
||||
let item: SnapshotListItem = serde_json::from_value(record.to_owned())?;
|
||||
let mut filenames = Vec::new();
|
||||
for file in &item.files {
|
||||
filenames.push(file.filename.to_string());
|
||||
}
|
||||
Ok(tools::format::render_backup_file_list(&filenames[..]))
|
||||
};
|
||||
|
||||
let options = default_table_format_options()
|
||||
.sortby("backup-type", false)
|
||||
.sortby("backup-id", false)
|
||||
.sortby("backup-time", false)
|
||||
.column(ColumnConfig::new("backup-id").renderer(render_snapshot_path).header("snapshot"))
|
||||
.column(ColumnConfig::new("size").renderer(tools::format::render_bytes_human_readable))
|
||||
.column(ColumnConfig::new("files").renderer(render_files))
|
||||
;
|
||||
|
||||
let info = &proxmox_backup::api2::admin::datastore::API_RETURN_SCHEMA_LIST_SNAPSHOTS;
|
||||
|
||||
format_and_print_result_full(&mut data, info, &output_format, &options);
|
||||
|
||||
Ok(Value::Null)
|
||||
}
|
||||
|
||||
#[api(
|
||||
input: {
|
||||
properties: {
|
||||
repository: {
|
||||
schema: REPO_URL_SCHEMA,
|
||||
optional: true,
|
||||
},
|
||||
snapshot: {
|
||||
type: String,
|
||||
description: "Snapshot path.",
|
||||
},
|
||||
}
|
||||
}
|
||||
)]
|
||||
/// Forget (remove) backup snapshots.
|
||||
async fn forget_snapshots(param: Value) -> Result<Value, Error> {
|
||||
|
||||
let repo = extract_repository_from_value(¶m)?;
|
||||
|
||||
let path = tools::required_string_param(¶m, "snapshot")?;
|
||||
let snapshot: BackupDir = path.parse()?;
|
||||
|
||||
let mut client = connect(&repo)?;
|
||||
|
||||
let path = format!("api2/json/admin/datastore/{}/snapshots", repo.store());
|
||||
|
||||
let result = client.delete(&path, Some(json!({
|
||||
"backup-type": snapshot.group().backup_type(),
|
||||
"backup-id": snapshot.group().backup_id(),
|
||||
"backup-time": snapshot.backup_time(),
|
||||
}))).await?;
|
||||
|
||||
record_repository(&repo);
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[api(
|
||||
input: {
|
||||
properties: {
|
||||
@ -655,58 +548,6 @@ async fn api_version(param: Value) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
#[api(
|
||||
input: {
|
||||
properties: {
|
||||
repository: {
|
||||
schema: REPO_URL_SCHEMA,
|
||||
optional: true,
|
||||
},
|
||||
snapshot: {
|
||||
type: String,
|
||||
description: "Snapshot path.",
|
||||
},
|
||||
"output-format": {
|
||||
schema: OUTPUT_FORMAT,
|
||||
optional: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
)]
|
||||
/// List snapshot files.
|
||||
async fn list_snapshot_files(param: Value) -> Result<Value, Error> {
|
||||
|
||||
let repo = extract_repository_from_value(¶m)?;
|
||||
|
||||
let path = tools::required_string_param(¶m, "snapshot")?;
|
||||
let snapshot: BackupDir = path.parse()?;
|
||||
|
||||
let output_format = get_output_format(¶m);
|
||||
|
||||
let client = connect(&repo)?;
|
||||
|
||||
let path = format!("api2/json/admin/datastore/{}/files", repo.store());
|
||||
|
||||
let mut result = client.get(&path, Some(json!({
|
||||
"backup-type": snapshot.group().backup_type(),
|
||||
"backup-id": snapshot.group().backup_id(),
|
||||
"backup-time": snapshot.backup_time(),
|
||||
}))).await?;
|
||||
|
||||
record_repository(&repo);
|
||||
|
||||
let info = &proxmox_backup::api2::admin::datastore::API_RETURN_SCHEMA_LIST_SNAPSHOT_FILES;
|
||||
|
||||
let mut data: Value = result["data"].take();
|
||||
|
||||
let options = default_table_format_options();
|
||||
|
||||
format_and_print_result_full(&mut data, info, &output_format, &options);
|
||||
|
||||
Ok(Value::Null)
|
||||
}
|
||||
|
||||
#[api(
|
||||
input: {
|
||||
properties: {
|
||||
@ -803,7 +644,7 @@ fn keyfile_parameters(param: &Value) -> Result<(Option<Vec<u8>>, CryptMode), Err
|
||||
(None, None) => None,
|
||||
(Some(_), Some(_)) => bail!("--keyfile and --keyfd are mutually exclusive"),
|
||||
(Some(keyfile), None) => {
|
||||
println!("Using encryption key file: {}", keyfile);
|
||||
eprintln!("Using encryption key file: {}", keyfile);
|
||||
Some(file_get_contents(keyfile)?)
|
||||
},
|
||||
(None, Some(fd)) => {
|
||||
@ -813,7 +654,7 @@ fn keyfile_parameters(param: &Value) -> Result<(Option<Vec<u8>>, CryptMode), Err
|
||||
.map_err(|err| {
|
||||
format_err!("error reading encryption key from fd {}: {}", fd, err)
|
||||
})?;
|
||||
println!("Using encryption key from file descriptor");
|
||||
eprintln!("Using encryption key from file descriptor");
|
||||
Some(data)
|
||||
}
|
||||
};
|
||||
@ -822,7 +663,7 @@ fn keyfile_parameters(param: &Value) -> Result<(Option<Vec<u8>>, CryptMode), Err
|
||||
// no parameters:
|
||||
(None, None) => match key::read_optional_default_encryption_key()? {
|
||||
Some(key) => {
|
||||
println!("Encrypting with default encryption key!");
|
||||
eprintln!("Encrypting with default encryption key!");
|
||||
(Some(key), CryptMode::Encrypt)
|
||||
},
|
||||
None => (None, CryptMode::None),
|
||||
@ -835,7 +676,7 @@ fn keyfile_parameters(param: &Value) -> Result<(Option<Vec<u8>>, CryptMode), Err
|
||||
(None, Some(crypt_mode)) => match key::read_optional_default_encryption_key()? {
|
||||
None => bail!("--crypt-mode without --keyfile and no default key file available"),
|
||||
Some(key) => {
|
||||
println!("Encrypting with default encryption key!");
|
||||
eprintln!("Encrypting with default encryption key!");
|
||||
(Some(key), crypt_mode)
|
||||
},
|
||||
}
|
||||
@ -1416,7 +1257,7 @@ async fn restore(param: Value) -> Result<Value, Error> {
|
||||
None => None,
|
||||
Some(key) => {
|
||||
let (key, _, fingerprint) = decrypt_key(&key, &key::get_encryption_key_password)?;
|
||||
println!("Encryption key fingerprint: '{}'", fingerprint);
|
||||
eprintln!("Encryption key fingerprint: '{}'", fingerprint);
|
||||
Some(Arc::new(CryptConfig::new(key)?))
|
||||
}
|
||||
};
|
||||
@ -1529,81 +1370,6 @@ async fn restore(param: Value) -> Result<Value, Error> {
|
||||
Ok(Value::Null)
|
||||
}
|
||||
|
||||
#[api(
|
||||
input: {
|
||||
properties: {
|
||||
repository: {
|
||||
schema: REPO_URL_SCHEMA,
|
||||
optional: true,
|
||||
},
|
||||
snapshot: {
|
||||
type: String,
|
||||
description: "Group/Snapshot path.",
|
||||
},
|
||||
logfile: {
|
||||
type: String,
|
||||
description: "The path to the log file you want to upload.",
|
||||
},
|
||||
keyfile: {
|
||||
schema: KEYFILE_SCHEMA,
|
||||
optional: true,
|
||||
},
|
||||
"keyfd": {
|
||||
schema: KEYFD_SCHEMA,
|
||||
optional: true,
|
||||
},
|
||||
"crypt-mode": {
|
||||
type: CryptMode,
|
||||
optional: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
)]
|
||||
/// Upload backup log file.
|
||||
async fn upload_log(param: Value) -> Result<Value, Error> {
|
||||
|
||||
let logfile = tools::required_string_param(¶m, "logfile")?;
|
||||
let repo = extract_repository_from_value(¶m)?;
|
||||
|
||||
let snapshot = tools::required_string_param(¶m, "snapshot")?;
|
||||
let snapshot: BackupDir = snapshot.parse()?;
|
||||
|
||||
let mut client = connect(&repo)?;
|
||||
|
||||
let (keydata, crypt_mode) = keyfile_parameters(¶m)?;
|
||||
|
||||
let crypt_config = match keydata {
|
||||
None => None,
|
||||
Some(key) => {
|
||||
let (key, _created, _) = decrypt_key(&key, &key::get_encryption_key_password)?;
|
||||
let crypt_config = CryptConfig::new(key)?;
|
||||
Some(Arc::new(crypt_config))
|
||||
}
|
||||
};
|
||||
|
||||
let data = file_get_contents(logfile)?;
|
||||
|
||||
// fixme: howto sign log?
|
||||
let blob = match crypt_mode {
|
||||
CryptMode::None | CryptMode::SignOnly => DataBlob::encode(&data, None, true)?,
|
||||
CryptMode::Encrypt => DataBlob::encode(&data, crypt_config.as_ref().map(Arc::as_ref), true)?,
|
||||
};
|
||||
|
||||
let raw_data = blob.into_inner();
|
||||
|
||||
let path = format!("api2/json/admin/datastore/{}/upload-backup-log", repo.store());
|
||||
|
||||
let args = json!({
|
||||
"backup-type": snapshot.group().backup_type(),
|
||||
"backup-id": snapshot.group().backup_id(),
|
||||
"backup-time": snapshot.backup_time(),
|
||||
});
|
||||
|
||||
let body = hyper::Body::from(raw_data);
|
||||
|
||||
client.upload("application/octet-stream", body, &path, Some(args)).await
|
||||
}
|
||||
|
||||
const API_METHOD_PRUNE: ApiMethod = ApiMethod::new(
|
||||
&ApiHandler::Async(&prune),
|
||||
&ObjectSchema::new(
|
||||
@ -2041,26 +1807,9 @@ fn main() {
|
||||
.completion_cb("repository", complete_repository)
|
||||
.completion_cb("keyfile", tools::complete_file_name);
|
||||
|
||||
let upload_log_cmd_def = CliCommand::new(&API_METHOD_UPLOAD_LOG)
|
||||
.arg_param(&["snapshot", "logfile"])
|
||||
.completion_cb("snapshot", complete_backup_snapshot)
|
||||
.completion_cb("logfile", tools::complete_file_name)
|
||||
.completion_cb("keyfile", tools::complete_file_name)
|
||||
.completion_cb("repository", complete_repository);
|
||||
|
||||
let list_cmd_def = CliCommand::new(&API_METHOD_LIST_BACKUP_GROUPS)
|
||||
.completion_cb("repository", complete_repository);
|
||||
|
||||
let snapshots_cmd_def = CliCommand::new(&API_METHOD_LIST_SNAPSHOTS)
|
||||
.arg_param(&["group"])
|
||||
.completion_cb("group", complete_backup_group)
|
||||
.completion_cb("repository", complete_repository);
|
||||
|
||||
let forget_cmd_def = CliCommand::new(&API_METHOD_FORGET_SNAPSHOTS)
|
||||
.arg_param(&["snapshot"])
|
||||
.completion_cb("repository", complete_repository)
|
||||
.completion_cb("snapshot", complete_backup_snapshot);
|
||||
|
||||
let garbage_collect_cmd_def = CliCommand::new(&API_METHOD_START_GARBAGE_COLLECTION)
|
||||
.completion_cb("repository", complete_repository);
|
||||
|
||||
@ -2071,11 +1820,6 @@ fn main() {
|
||||
.completion_cb("archive-name", complete_archive_name)
|
||||
.completion_cb("target", tools::complete_file_name);
|
||||
|
||||
let files_cmd_def = CliCommand::new(&API_METHOD_LIST_SNAPSHOT_FILES)
|
||||
.arg_param(&["snapshot"])
|
||||
.completion_cb("repository", complete_repository)
|
||||
.completion_cb("snapshot", complete_backup_snapshot);
|
||||
|
||||
let prune_cmd_def = CliCommand::new(&API_METHOD_PRUNE)
|
||||
.arg_param(&["group"])
|
||||
.completion_cb("group", complete_backup_group)
|
||||
@ -2101,16 +1845,13 @@ fn main() {
|
||||
|
||||
let cmd_def = CliCommandMap::new()
|
||||
.insert("backup", backup_cmd_def)
|
||||
.insert("upload-log", upload_log_cmd_def)
|
||||
.insert("forget", forget_cmd_def)
|
||||
.insert("garbage-collect", garbage_collect_cmd_def)
|
||||
.insert("list", list_cmd_def)
|
||||
.insert("login", login_cmd_def)
|
||||
.insert("logout", logout_cmd_def)
|
||||
.insert("prune", prune_cmd_def)
|
||||
.insert("restore", restore_cmd_def)
|
||||
.insert("snapshots", snapshots_cmd_def)
|
||||
.insert("files", files_cmd_def)
|
||||
.insert("snapshot", snapshot_mgtm_cli())
|
||||
.insert("status", status_cmd_def)
|
||||
.insert("key", key::cli())
|
||||
.insert("mount", mount_cmd_def())
|
||||
@ -2120,7 +1861,13 @@ fn main() {
|
||||
.insert("task", task_mgmt_cli())
|
||||
.insert("version", version_cmd_def)
|
||||
.insert("benchmark", benchmark_cmd_def)
|
||||
.insert("change-owner", change_owner_cmd_def);
|
||||
.insert("change-owner", change_owner_cmd_def)
|
||||
|
||||
.alias(&["files"], &["snapshot", "files"])
|
||||
.alias(&["forget"], &["snapshot", "forget"])
|
||||
.alias(&["upload-log"], &["snapshot", "upload-log"])
|
||||
.alias(&["snapshots"], &["snapshot", "list"])
|
||||
;
|
||||
|
||||
let rpcenv = CliEnvironment::new();
|
||||
run_cli_command(cmd_def, rpcenv, Some(|future| {
|
||||
|
@ -8,6 +8,8 @@ mod task;
|
||||
pub use task::*;
|
||||
mod catalog;
|
||||
pub use catalog::*;
|
||||
mod snapshot;
|
||||
pub use snapshot::*;
|
||||
|
||||
pub mod key;
|
||||
|
||||
|
416
src/bin/proxmox_backup_client/snapshot.rs
Normal file
416
src/bin/proxmox_backup_client/snapshot.rs
Normal file
@ -0,0 +1,416 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Error;
|
||||
use serde_json::{json, Value};
|
||||
|
||||
use proxmox::{
|
||||
api::{api, cli::*},
|
||||
tools::fs::file_get_contents,
|
||||
};
|
||||
|
||||
use proxmox_backup::{
|
||||
tools,
|
||||
api2::types::*,
|
||||
backup::{
|
||||
CryptMode,
|
||||
CryptConfig,
|
||||
DataBlob,
|
||||
BackupGroup,
|
||||
decrypt_key,
|
||||
}
|
||||
};
|
||||
|
||||
use crate::{
|
||||
REPO_URL_SCHEMA,
|
||||
KEYFILE_SCHEMA,
|
||||
KEYFD_SCHEMA,
|
||||
BackupDir,
|
||||
api_datastore_list_snapshots,
|
||||
complete_backup_snapshot,
|
||||
complete_backup_group,
|
||||
complete_repository,
|
||||
connect,
|
||||
extract_repository_from_value,
|
||||
record_repository,
|
||||
keyfile_parameters,
|
||||
};
|
||||
|
||||
#[api(
|
||||
input: {
|
||||
properties: {
|
||||
repository: {
|
||||
schema: REPO_URL_SCHEMA,
|
||||
optional: true,
|
||||
},
|
||||
group: {
|
||||
type: String,
|
||||
description: "Backup group.",
|
||||
optional: true,
|
||||
},
|
||||
"output-format": {
|
||||
schema: OUTPUT_FORMAT,
|
||||
optional: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
)]
|
||||
/// List backup snapshots.
|
||||
async fn list_snapshots(param: Value) -> Result<Value, Error> {
|
||||
|
||||
let repo = extract_repository_from_value(¶m)?;
|
||||
|
||||
let output_format = get_output_format(¶m);
|
||||
|
||||
let client = connect(&repo)?;
|
||||
|
||||
let group: Option<BackupGroup> = if let Some(path) = param["group"].as_str() {
|
||||
Some(path.parse()?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut data = api_datastore_list_snapshots(&client, repo.store(), group).await?;
|
||||
|
||||
record_repository(&repo);
|
||||
|
||||
let render_snapshot_path = |_v: &Value, record: &Value| -> Result<String, Error> {
|
||||
let item: SnapshotListItem = serde_json::from_value(record.to_owned())?;
|
||||
let snapshot = BackupDir::new(item.backup_type, item.backup_id, item.backup_time)?;
|
||||
Ok(snapshot.relative_path().to_str().unwrap().to_owned())
|
||||
};
|
||||
|
||||
let render_files = |_v: &Value, record: &Value| -> Result<String, Error> {
|
||||
let item: SnapshotListItem = serde_json::from_value(record.to_owned())?;
|
||||
let mut filenames = Vec::new();
|
||||
for file in &item.files {
|
||||
filenames.push(file.filename.to_string());
|
||||
}
|
||||
Ok(tools::format::render_backup_file_list(&filenames[..]))
|
||||
};
|
||||
|
||||
let options = default_table_format_options()
|
||||
.sortby("backup-type", false)
|
||||
.sortby("backup-id", false)
|
||||
.sortby("backup-time", false)
|
||||
.column(ColumnConfig::new("backup-id").renderer(render_snapshot_path).header("snapshot"))
|
||||
.column(ColumnConfig::new("size").renderer(tools::format::render_bytes_human_readable))
|
||||
.column(ColumnConfig::new("files").renderer(render_files))
|
||||
;
|
||||
|
||||
let info = &proxmox_backup::api2::admin::datastore::API_RETURN_SCHEMA_LIST_SNAPSHOTS;
|
||||
|
||||
format_and_print_result_full(&mut data, info, &output_format, &options);
|
||||
|
||||
Ok(Value::Null)
|
||||
}
|
||||
|
||||
#[api(
|
||||
input: {
|
||||
properties: {
|
||||
repository: {
|
||||
schema: REPO_URL_SCHEMA,
|
||||
optional: true,
|
||||
},
|
||||
snapshot: {
|
||||
type: String,
|
||||
description: "Snapshot path.",
|
||||
},
|
||||
"output-format": {
|
||||
schema: OUTPUT_FORMAT,
|
||||
optional: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
)]
|
||||
/// List snapshot files.
|
||||
async fn list_snapshot_files(param: Value) -> Result<Value, Error> {
|
||||
|
||||
let repo = extract_repository_from_value(¶m)?;
|
||||
|
||||
let path = tools::required_string_param(¶m, "snapshot")?;
|
||||
let snapshot: BackupDir = path.parse()?;
|
||||
|
||||
let output_format = get_output_format(¶m);
|
||||
|
||||
let client = connect(&repo)?;
|
||||
|
||||
let path = format!("api2/json/admin/datastore/{}/files", repo.store());
|
||||
|
||||
let mut result = client.get(&path, Some(json!({
|
||||
"backup-type": snapshot.group().backup_type(),
|
||||
"backup-id": snapshot.group().backup_id(),
|
||||
"backup-time": snapshot.backup_time(),
|
||||
}))).await?;
|
||||
|
||||
record_repository(&repo);
|
||||
|
||||
let info = &proxmox_backup::api2::admin::datastore::API_RETURN_SCHEMA_LIST_SNAPSHOT_FILES;
|
||||
|
||||
let mut data: Value = result["data"].take();
|
||||
|
||||
let options = default_table_format_options();
|
||||
|
||||
format_and_print_result_full(&mut data, info, &output_format, &options);
|
||||
|
||||
Ok(Value::Null)
|
||||
}
|
||||
|
||||
#[api(
|
||||
input: {
|
||||
properties: {
|
||||
repository: {
|
||||
schema: REPO_URL_SCHEMA,
|
||||
optional: true,
|
||||
},
|
||||
snapshot: {
|
||||
type: String,
|
||||
description: "Snapshot path.",
|
||||
},
|
||||
}
|
||||
}
|
||||
)]
|
||||
/// Forget (remove) backup snapshots.
|
||||
async fn forget_snapshots(param: Value) -> Result<Value, Error> {
|
||||
|
||||
let repo = extract_repository_from_value(¶m)?;
|
||||
|
||||
let path = tools::required_string_param(¶m, "snapshot")?;
|
||||
let snapshot: BackupDir = path.parse()?;
|
||||
|
||||
let mut client = connect(&repo)?;
|
||||
|
||||
let path = format!("api2/json/admin/datastore/{}/snapshots", repo.store());
|
||||
|
||||
let result = client.delete(&path, Some(json!({
|
||||
"backup-type": snapshot.group().backup_type(),
|
||||
"backup-id": snapshot.group().backup_id(),
|
||||
"backup-time": snapshot.backup_time(),
|
||||
}))).await?;
|
||||
|
||||
record_repository(&repo);
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[api(
|
||||
input: {
|
||||
properties: {
|
||||
repository: {
|
||||
schema: REPO_URL_SCHEMA,
|
||||
optional: true,
|
||||
},
|
||||
snapshot: {
|
||||
type: String,
|
||||
description: "Group/Snapshot path.",
|
||||
},
|
||||
logfile: {
|
||||
type: String,
|
||||
description: "The path to the log file you want to upload.",
|
||||
},
|
||||
keyfile: {
|
||||
schema: KEYFILE_SCHEMA,
|
||||
optional: true,
|
||||
},
|
||||
"keyfd": {
|
||||
schema: KEYFD_SCHEMA,
|
||||
optional: true,
|
||||
},
|
||||
"crypt-mode": {
|
||||
type: CryptMode,
|
||||
optional: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
)]
|
||||
/// Upload backup log file.
|
||||
async fn upload_log(param: Value) -> Result<Value, Error> {
|
||||
|
||||
let logfile = tools::required_string_param(¶m, "logfile")?;
|
||||
let repo = extract_repository_from_value(¶m)?;
|
||||
|
||||
let snapshot = tools::required_string_param(¶m, "snapshot")?;
|
||||
let snapshot: BackupDir = snapshot.parse()?;
|
||||
|
||||
let mut client = connect(&repo)?;
|
||||
|
||||
let (keydata, crypt_mode) = keyfile_parameters(¶m)?;
|
||||
|
||||
let crypt_config = match keydata {
|
||||
None => None,
|
||||
Some(key) => {
|
||||
let (key, _created, _) = decrypt_key(&key, &crate::key::get_encryption_key_password)?;
|
||||
let crypt_config = CryptConfig::new(key)?;
|
||||
Some(Arc::new(crypt_config))
|
||||
}
|
||||
};
|
||||
|
||||
let data = file_get_contents(logfile)?;
|
||||
|
||||
// fixme: howto sign log?
|
||||
let blob = match crypt_mode {
|
||||
CryptMode::None | CryptMode::SignOnly => DataBlob::encode(&data, None, true)?,
|
||||
CryptMode::Encrypt => DataBlob::encode(&data, crypt_config.as_ref().map(Arc::as_ref), true)?,
|
||||
};
|
||||
|
||||
let raw_data = blob.into_inner();
|
||||
|
||||
let path = format!("api2/json/admin/datastore/{}/upload-backup-log", repo.store());
|
||||
|
||||
let args = json!({
|
||||
"backup-type": snapshot.group().backup_type(),
|
||||
"backup-id": snapshot.group().backup_id(),
|
||||
"backup-time": snapshot.backup_time(),
|
||||
});
|
||||
|
||||
let body = hyper::Body::from(raw_data);
|
||||
|
||||
client.upload("application/octet-stream", body, &path, Some(args)).await
|
||||
}
|
||||
|
||||
#[api(
|
||||
input: {
|
||||
properties: {
|
||||
repository: {
|
||||
schema: REPO_URL_SCHEMA,
|
||||
optional: true,
|
||||
},
|
||||
snapshot: {
|
||||
type: String,
|
||||
description: "Snapshot path.",
|
||||
},
|
||||
"output-format": {
|
||||
schema: OUTPUT_FORMAT,
|
||||
optional: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
)]
|
||||
/// Show notes
|
||||
async fn show_notes(param: Value) -> Result<Value, Error> {
|
||||
let repo = extract_repository_from_value(¶m)?;
|
||||
let path = tools::required_string_param(¶m, "snapshot")?;
|
||||
|
||||
let snapshot: BackupDir = path.parse()?;
|
||||
let client = connect(&repo)?;
|
||||
|
||||
let path = format!("api2/json/admin/datastore/{}/notes", repo.store());
|
||||
|
||||
let args = json!({
|
||||
"backup-type": snapshot.group().backup_type(),
|
||||
"backup-id": snapshot.group().backup_id(),
|
||||
"backup-time": snapshot.backup_time(),
|
||||
});
|
||||
|
||||
let output_format = get_output_format(¶m);
|
||||
|
||||
let mut result = client.get(&path, Some(args)).await?;
|
||||
|
||||
let notes = result["data"].take();
|
||||
|
||||
if output_format == "text" {
|
||||
if let Some(notes) = notes.as_str() {
|
||||
println!("{}", notes);
|
||||
}
|
||||
} else {
|
||||
format_and_print_result(
|
||||
&json!({
|
||||
"notes": notes,
|
||||
}),
|
||||
&output_format,
|
||||
);
|
||||
}
|
||||
|
||||
Ok(Value::Null)
|
||||
}
|
||||
|
||||
#[api(
|
||||
input: {
|
||||
properties: {
|
||||
repository: {
|
||||
schema: REPO_URL_SCHEMA,
|
||||
optional: true,
|
||||
},
|
||||
snapshot: {
|
||||
type: String,
|
||||
description: "Snapshot path.",
|
||||
},
|
||||
notes: {
|
||||
type: String,
|
||||
description: "The Notes.",
|
||||
},
|
||||
}
|
||||
}
|
||||
)]
|
||||
/// Update Notes
|
||||
async fn update_notes(param: Value) -> Result<Value, Error> {
|
||||
let repo = extract_repository_from_value(¶m)?;
|
||||
let path = tools::required_string_param(¶m, "snapshot")?;
|
||||
let notes = tools::required_string_param(¶m, "notes")?;
|
||||
|
||||
let snapshot: BackupDir = path.parse()?;
|
||||
let mut client = connect(&repo)?;
|
||||
|
||||
let path = format!("api2/json/admin/datastore/{}/notes", repo.store());
|
||||
|
||||
let args = json!({
|
||||
"backup-type": snapshot.group().backup_type(),
|
||||
"backup-id": snapshot.group().backup_id(),
|
||||
"backup-time": snapshot.backup_time(),
|
||||
"notes": notes,
|
||||
});
|
||||
|
||||
client.put(&path, Some(args)).await?;
|
||||
|
||||
Ok(Value::Null)
|
||||
}
|
||||
|
||||
fn notes_cli() -> CliCommandMap {
|
||||
CliCommandMap::new()
|
||||
.insert(
|
||||
"show",
|
||||
CliCommand::new(&API_METHOD_SHOW_NOTES)
|
||||
.arg_param(&["snapshot"])
|
||||
.completion_cb("snapshot", complete_backup_snapshot),
|
||||
)
|
||||
.insert(
|
||||
"update",
|
||||
CliCommand::new(&API_METHOD_UPDATE_NOTES)
|
||||
.arg_param(&["snapshot", "notes"])
|
||||
.completion_cb("snapshot", complete_backup_snapshot),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn snapshot_mgtm_cli() -> CliCommandMap {
|
||||
CliCommandMap::new()
|
||||
.insert("notes", notes_cli())
|
||||
.insert(
|
||||
"list",
|
||||
CliCommand::new(&API_METHOD_LIST_SNAPSHOTS)
|
||||
.arg_param(&["group"])
|
||||
.completion_cb("group", complete_backup_group)
|
||||
.completion_cb("repository", complete_repository)
|
||||
)
|
||||
.insert(
|
||||
"files",
|
||||
CliCommand::new(&API_METHOD_LIST_SNAPSHOT_FILES)
|
||||
.arg_param(&["snapshot"])
|
||||
.completion_cb("repository", complete_repository)
|
||||
.completion_cb("snapshot", complete_backup_snapshot)
|
||||
)
|
||||
.insert(
|
||||
"forget",
|
||||
CliCommand::new(&API_METHOD_FORGET_SNAPSHOTS)
|
||||
.arg_param(&["snapshot"])
|
||||
.completion_cb("repository", complete_repository)
|
||||
.completion_cb("snapshot", complete_backup_snapshot)
|
||||
)
|
||||
.insert(
|
||||
"upload-log",
|
||||
CliCommand::new(&API_METHOD_UPLOAD_LOG)
|
||||
.arg_param(&["snapshot", "logfile"])
|
||||
.completion_cb("snapshot", complete_backup_snapshot)
|
||||
.completion_cb("logfile", tools::complete_file_name)
|
||||
.completion_cb("keyfile", tools::complete_file_name)
|
||||
.completion_cb("repository", complete_repository)
|
||||
)
|
||||
}
|
@ -395,7 +395,7 @@ Ext.define('PBS.dashboard.SubscriptionInfo', {
|
||||
break;
|
||||
case 0:
|
||||
icon = 'times-circle critical';
|
||||
message = gettext('<h1>No valid subscription</h1>' + PBS.Utils.noSubKeyHtml);
|
||||
message = `<h1>${gettext('No valid subscription')}</h1>${PBS.Utils.noSubKeyHtml}`;
|
||||
break;
|
||||
default:
|
||||
throw 'invalid subscription status';
|
||||
|
Reference in New Issue
Block a user