mirror of
https://git.proxmox.com/git/vma-to-pbs
synced 2025-08-13 21:24:52 +00:00
add support for notes and logs
Allow the user to specify a notes file and a log file to associate with the backup Signed-off-by: Filip Schauer <f.schauer@proxmox.com>
This commit is contained in:
parent
2dd0dc023d
commit
737aed9eec
@ -7,14 +7,22 @@ edition = "2021"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
bincode = "1.3"
|
bincode = "1.3"
|
||||||
|
hyper = "0.14.5"
|
||||||
pico-args = "0.4"
|
pico-args = "0.4"
|
||||||
md5 = "0.7.0"
|
md5 = "0.7.0"
|
||||||
scopeguard = "1.1.0"
|
scopeguard = "1.1.0"
|
||||||
serde = "1.0"
|
serde = "1.0"
|
||||||
|
serde_json = "1.0"
|
||||||
serde-big-array = "0.4.1"
|
serde-big-array = "0.4.1"
|
||||||
|
|
||||||
|
proxmox-async = "0.4"
|
||||||
proxmox-io = "1.0.1"
|
proxmox-io = "1.0.1"
|
||||||
proxmox-sys = "0.5.0"
|
proxmox-sys = "0.5.0"
|
||||||
proxmox-time = "2"
|
proxmox-time = "2"
|
||||||
|
|
||||||
|
pbs-api-types = { path = "submodules/proxmox-backup-qemu/submodules/proxmox-backup/pbs-api-types" }
|
||||||
|
pbs-client = { path = "submodules/proxmox-backup-qemu/submodules/proxmox-backup/pbs-client" }
|
||||||
|
pbs-datastore = { path = "submodules/proxmox-backup-qemu/submodules/proxmox-backup/pbs-datastore" }
|
||||||
|
pbs-key-config = { path = "submodules/proxmox-backup-qemu/submodules/proxmox-backup/pbs-key-config" }
|
||||||
|
pbs-tools = { path = "submodules/proxmox-backup-qemu/submodules/proxmox-backup/pbs-tools" }
|
||||||
proxmox-backup-qemu = { path = "submodules/proxmox-backup-qemu" }
|
proxmox-backup-qemu = { path = "submodules/proxmox-backup-qemu" }
|
||||||
|
16
src/main.rs
16
src/main.rs
@ -37,6 +37,10 @@ Options:
|
|||||||
Password file
|
Password file
|
||||||
--key-password-file <KEY_PASSWORD_FILE>
|
--key-password-file <KEY_PASSWORD_FILE>
|
||||||
Key password file
|
Key password file
|
||||||
|
[--notes-file <NOTES_FILE>]
|
||||||
|
File containing a comment/notes
|
||||||
|
[--log-file <LOG_FILE>]
|
||||||
|
Log file
|
||||||
-h, --help
|
-h, --help
|
||||||
Print help
|
Print help
|
||||||
-V, --version
|
-V, --version
|
||||||
@ -93,6 +97,8 @@ fn parse_args() -> Result<BackupVmaToPbsArgs, Error> {
|
|||||||
let encrypt = args.contains(["-e", "--encrypt"]);
|
let encrypt = args.contains(["-e", "--encrypt"]);
|
||||||
let password_file: Option<OsString> = args.opt_value_from_str("--password-file")?;
|
let password_file: Option<OsString> = args.opt_value_from_str("--password-file")?;
|
||||||
let key_password_file: Option<OsString> = args.opt_value_from_str("--key-password-file")?;
|
let key_password_file: Option<OsString> = args.opt_value_from_str("--key-password-file")?;
|
||||||
|
let notes_file: Option<OsString> = args.opt_value_from_str("--notes-file")?;
|
||||||
|
let log_file_path: Option<OsString> = args.opt_value_from_str("--log-file")?;
|
||||||
|
|
||||||
match (encrypt, keyfile.is_some()) {
|
match (encrypt, keyfile.is_some()) {
|
||||||
(true, false) => bail!("--encrypt requires a --keyfile!"),
|
(true, false) => bail!("--encrypt requires a --keyfile!"),
|
||||||
@ -170,6 +176,14 @@ fn parse_args() -> Result<BackupVmaToPbsArgs, Error> {
|
|||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let notes = if let Some(notes_file) = notes_file {
|
||||||
|
let notes = std::fs::read_to_string(notes_file).context("Could not read notes file")?;
|
||||||
|
|
||||||
|
Some(notes)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
let options = BackupVmaToPbsArgs {
|
let options = BackupVmaToPbsArgs {
|
||||||
vma_file_path: vma_file_path.cloned(),
|
vma_file_path: vma_file_path.cloned(),
|
||||||
pbs_repository,
|
pbs_repository,
|
||||||
@ -183,6 +197,8 @@ fn parse_args() -> Result<BackupVmaToPbsArgs, Error> {
|
|||||||
fingerprint,
|
fingerprint,
|
||||||
compress,
|
compress,
|
||||||
encrypt,
|
encrypt,
|
||||||
|
notes,
|
||||||
|
log_file_path,
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(options)
|
Ok(options)
|
||||||
|
104
src/vma2pbs.rs
104
src/vma2pbs.rs
@ -8,6 +8,12 @@ use std::ptr;
|
|||||||
use std::time::SystemTime;
|
use std::time::SystemTime;
|
||||||
|
|
||||||
use anyhow::{anyhow, bail, Error};
|
use anyhow::{anyhow, bail, Error};
|
||||||
|
use pbs_api_types::{BackupDir, BackupNamespace, BackupType};
|
||||||
|
use pbs_client::{BackupRepository, HttpClient, HttpClientOptions};
|
||||||
|
use pbs_datastore::DataBlob;
|
||||||
|
use pbs_key_config::decrypt_key;
|
||||||
|
use pbs_tools::crypt_config::CryptConfig;
|
||||||
|
use proxmox_async::runtime::block_on;
|
||||||
use proxmox_backup_qemu::{
|
use proxmox_backup_qemu::{
|
||||||
capi_types::ProxmoxBackupHandle, proxmox_backup_add_config, proxmox_backup_close_image,
|
capi_types::ProxmoxBackupHandle, proxmox_backup_add_config, proxmox_backup_close_image,
|
||||||
proxmox_backup_connect, proxmox_backup_disconnect, proxmox_backup_finish,
|
proxmox_backup_connect, proxmox_backup_disconnect, proxmox_backup_finish,
|
||||||
@ -16,6 +22,7 @@ use proxmox_backup_qemu::{
|
|||||||
};
|
};
|
||||||
use proxmox_time::epoch_to_rfc3339;
|
use proxmox_time::epoch_to_rfc3339;
|
||||||
use scopeguard::defer;
|
use scopeguard::defer;
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
use crate::vma::VmaReader;
|
use crate::vma::VmaReader;
|
||||||
|
|
||||||
@ -34,6 +41,8 @@ pub struct BackupVmaToPbsArgs {
|
|||||||
pub fingerprint: String,
|
pub fingerprint: String,
|
||||||
pub compress: bool,
|
pub compress: bool,
|
||||||
pub encrypt: bool,
|
pub encrypt: bool,
|
||||||
|
pub notes: Option<String>,
|
||||||
|
pub log_file_path: Option<OsString>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone)]
|
#[derive(Copy, Clone)]
|
||||||
@ -352,6 +361,89 @@ where
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn pbs_client_setup(args: &BackupVmaToPbsArgs) -> Result<(HttpClient, String, Value), Error> {
|
||||||
|
let repo: BackupRepository = args.pbs_repository.parse()?;
|
||||||
|
let options = HttpClientOptions::new_interactive(
|
||||||
|
Some(args.pbs_password.clone()),
|
||||||
|
Some(args.fingerprint.clone()),
|
||||||
|
);
|
||||||
|
let client = HttpClient::new(repo.host(), repo.port(), repo.auth_id(), options)?;
|
||||||
|
|
||||||
|
let backup_dir = BackupDir::from((BackupType::Vm, args.backup_id.clone(), args.backup_time));
|
||||||
|
|
||||||
|
let namespace = match &args.namespace {
|
||||||
|
Some(namespace) => BackupNamespace::new(namespace)?,
|
||||||
|
None => BackupNamespace::root(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut request_args = serde_json::to_value(backup_dir)?;
|
||||||
|
if !namespace.is_root() {
|
||||||
|
request_args["ns"] = serde_json::to_value(namespace)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((client, repo.store().to_owned(), request_args))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn upload_log(
|
||||||
|
client: &HttpClient,
|
||||||
|
args: &BackupVmaToPbsArgs,
|
||||||
|
store: &str,
|
||||||
|
request_args: Value,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
if let Some(log_file_path) = &args.log_file_path {
|
||||||
|
let path = format!("api2/json/admin/datastore/{}/upload-backup-log", store);
|
||||||
|
let data = std::fs::read(log_file_path)?;
|
||||||
|
|
||||||
|
let blob = if args.encrypt {
|
||||||
|
let crypt_config = match &args.keyfile {
|
||||||
|
None => None,
|
||||||
|
Some(keyfile) => {
|
||||||
|
let key = std::fs::read(keyfile)?;
|
||||||
|
let (key, _created, _) = decrypt_key(&key, &|| -> Result<Vec<u8>, Error> {
|
||||||
|
match &args.key_password {
|
||||||
|
Some(key_password) => Ok(key_password.clone().into_bytes()),
|
||||||
|
None => bail!("no key password provided"),
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
let crypt_config = CryptConfig::new(key)?;
|
||||||
|
Some(crypt_config)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
DataBlob::encode(&data, crypt_config.as_ref(), args.compress)?
|
||||||
|
} else {
|
||||||
|
// fixme: howto sign log?
|
||||||
|
DataBlob::encode(&data, None, args.compress)?
|
||||||
|
};
|
||||||
|
|
||||||
|
let body = hyper::Body::from(blob.into_inner());
|
||||||
|
|
||||||
|
block_on(async {
|
||||||
|
client
|
||||||
|
.upload("application/octet-stream", body, &path, Some(request_args))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_notes(
|
||||||
|
client: &HttpClient,
|
||||||
|
notes: &str,
|
||||||
|
store: &str,
|
||||||
|
mut request_args: Value,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
request_args["notes"] = Value::from(notes);
|
||||||
|
let path = format!("api2/json/admin/datastore/{}/notes", store);
|
||||||
|
block_on(async {
|
||||||
|
client.put(&path, Some(request_args)).await.unwrap();
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn backup_vma_to_pbs(args: BackupVmaToPbsArgs) -> Result<(), Error> {
|
pub fn backup_vma_to_pbs(args: BackupVmaToPbsArgs) -> Result<(), Error> {
|
||||||
let vma_file: Box<dyn BufRead> = match &args.vma_file_path {
|
let vma_file: Box<dyn BufRead> = match &args.vma_file_path {
|
||||||
Some(vma_file_path) => match File::open(vma_file_path) {
|
Some(vma_file_path) => match File::open(vma_file_path) {
|
||||||
@ -386,6 +478,18 @@ pub fn backup_vma_to_pbs(args: BackupVmaToPbsArgs) -> Result<(), Error> {
|
|||||||
handle_pbs_error(pbs_err, "proxmox_backup_finish")?;
|
handle_pbs_error(pbs_err, "proxmox_backup_finish")?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if args.notes.is_some() || args.log_file_path.is_some() {
|
||||||
|
let (client, store, request_args) = pbs_client_setup(&args)?;
|
||||||
|
|
||||||
|
if args.log_file_path.is_some() {
|
||||||
|
upload_log(&client, &args, &store, request_args.clone())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(notes) = args.notes {
|
||||||
|
set_notes(&client, ¬es, &store, request_args)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let transfer_duration = SystemTime::now().duration_since(start_transfer_time)?;
|
let transfer_duration = SystemTime::now().duration_since(start_transfer_time)?;
|
||||||
let total_seconds = transfer_duration.as_secs();
|
let total_seconds = transfer_duration.as_secs();
|
||||||
let minutes = total_seconds / 60;
|
let minutes = total_seconds / 60;
|
||||||
|
Loading…
Reference in New Issue
Block a user