-use std::collections::{HashSet, HashMap};
-use std::convert::TryFrom;
+use std::collections::HashSet;
use std::io::{self, Read, Write, Seek, SeekFrom};
-use std::os::unix::io::{FromRawFd, RawFd};
use std::path::{Path, PathBuf};
use std::pin::Pin;
use std::sync::{Arc, Mutex};
use std::task::Context;
use anyhow::{bail, format_err, Error};
-use futures::future::FutureExt;
use futures::stream::{StreamExt, TryStreamExt};
use serde_json::{json, Value};
use tokio::sync::mpsc;
use proxmox::{
tools::{
time::{strftime_local, epoch_i64},
- fs::{file_get_contents, file_get_json, replace_file, CreateOptions, image_size},
+ fs::{file_get_json, replace_file, CreateOptions, image_size},
},
api::{
api,
- ApiHandler,
ApiMethod,
RpcEnvironment,
- schema::*,
cli::*,
},
};
use pxar::accessor::{MaybeReady, ReadAt, ReadAtOperation};
-use proxmox_backup::tools;
-use proxmox_backup::api2::access::user::UserWithTokens;
+use pbs_datastore::catalog::BackupCatalogWriter;
+use pbs_tools::sync::StdChannelWriter;
+use pbs_tools::tokio::TokioWriterAdapter;
+
use proxmox_backup::api2::types::*;
use proxmox_backup::api2::version;
use proxmox_backup::client::*;
-use proxmox_backup::pxar::catalog::*;
use proxmox_backup::backup::{
archive_type,
decrypt_key,
IndexFile,
MANIFEST_BLOB_NAME,
Shell,
+ PruneOptions,
};
+use proxmox_backup::tools;
mod proxmox_backup_client;
use proxmox_backup_client::*;
-const ENV_VAR_PBS_FINGERPRINT: &str = "PBS_FINGERPRINT";
-const ENV_VAR_PBS_PASSWORD: &str = "PBS_PASSWORD";
-
-
-pub const REPO_URL_SCHEMA: Schema = StringSchema::new("Repository URL.")
- .format(&BACKUP_REPO_URL)
- .max_length(256)
- .schema();
-
-pub const KEYFILE_SCHEMA: Schema =
- StringSchema::new("Path to encryption key. All data will be encrypted using this key.")
- .schema();
-
-pub const KEYFD_SCHEMA: Schema =
- IntegerSchema::new("Pass an encryption key via an already opened file descriptor.")
- .minimum(0)
- .schema();
-
-pub const MASTER_PUBKEY_FILE_SCHEMA: Schema = StringSchema::new(
- "Path to master public key. The encryption key used for a backup will be encrypted using this key and appended to the backup.")
- .schema();
-
-pub const MASTER_PUBKEY_FD_SCHEMA: Schema =
- IntegerSchema::new("Pass a master public key via an already opened file descriptor.")
- .minimum(0)
- .schema();
-
-const CHUNK_SIZE_SCHEMA: Schema = IntegerSchema::new(
- "Chunk size in KB. Must be a power of 2.")
- .minimum(64)
- .maximum(4096)
- .default(4096)
- .schema();
-
-fn get_default_repository() -> Option<String> {
- std::env::var("PBS_REPOSITORY").ok()
-}
-
-pub fn extract_repository_from_value(
- param: &Value,
-) -> Result<BackupRepository, Error> {
-
- let repo_url = param["repository"]
- .as_str()
- .map(String::from)
- .or_else(get_default_repository)
- .ok_or_else(|| format_err!("unable to get (default) repository"))?;
-
- let repo: BackupRepository = repo_url.parse()?;
-
- Ok(repo)
-}
-
-fn extract_repository_from_map(
- param: &HashMap<String, String>,
-) -> Option<BackupRepository> {
-
- param.get("repository")
- .map(String::from)
- .or_else(get_default_repository)
- .and_then(|repo_url| repo_url.parse::<BackupRepository>().ok())
-}
+pub mod proxmox_client_tools;
+use proxmox_client_tools::{
+ complete_archive_name, complete_auth_id, complete_backup_group, complete_backup_snapshot,
+ complete_backup_source, complete_chunk_size, complete_group_or_snapshot,
+ complete_img_archive_name, complete_pxar_archive_name, complete_repository, connect,
+ extract_repository_from_value,
+ key_source::{
+ crypto_parameters, format_key_source, get_encryption_key_password, KEYFD_SCHEMA,
+ KEYFILE_SCHEMA, MASTER_PUBKEY_FD_SCHEMA, MASTER_PUBKEY_FILE_SCHEMA,
+ },
+ CHUNK_SIZE_SCHEMA, REPO_URL_SCHEMA,
+};
fn record_repository(repo: &BackupRepository) {
let _ = replace_file(path, new_data.to_string().as_bytes(), CreateOptions::new());
}
-pub fn complete_repository(_arg: &str, _param: &HashMap<String, String>) -> Vec<String> {
-
- let mut result = vec![];
-
- let base = match BaseDirectories::with_prefix("proxmox-backup") {
- Ok(v) => v,
- _ => return result,
- };
-
- // usually $HOME/.cache/proxmox-backup/repo-list
- let path = match base.place_cache_file("repo-list") {
- Ok(v) => v,
- _ => return result,
- };
-
- let data = file_get_json(&path, None).unwrap_or_else(|_| json!({}));
-
- if let Some(map) = data.as_object() {
- for (repo, _count) in map {
- result.push(repo.to_owned());
- }
- }
-
- result
-}
-
-fn connect(repo: &BackupRepository) -> Result<HttpClient, Error> {
- connect_do(repo.host(), repo.port(), repo.auth_id())
- .map_err(|err| format_err!("error building client for repository {} - {}", repo, err))
-}
-
-fn connect_do(server: &str, port: u16, auth_id: &Authid) -> Result<HttpClient, Error> {
- let fingerprint = std::env::var(ENV_VAR_PBS_FINGERPRINT).ok();
-
- use std::env::VarError::*;
- let password = match std::env::var(ENV_VAR_PBS_PASSWORD) {
- Ok(p) => Some(p),
- Err(NotUnicode(_)) => bail!(format!("{} contains bad characters", ENV_VAR_PBS_PASSWORD)),
- Err(NotPresent) => None,
- };
-
- let options = HttpClientOptions::new_interactive(password, fingerprint);
-
- HttpClient::new(server, port, auth_id, options)
-}
-
async fn api_datastore_list_snapshots(
client: &HttpClient,
store: &str,
dir_path: P,
archive_name: &str,
chunk_size: Option<usize>,
- catalog: Arc<Mutex<CatalogWriter<crate::tools::StdChannelWriter>>>,
+ catalog: Arc<Mutex<CatalogWriter<TokioWriterAdapter<StdChannelWriter>>>>,
pxar_create_options: proxmox_backup::pxar::PxarCreateOptions,
upload_options: UploadOptions,
) -> Result<BackupStats, Error> {
let render_files = |_v: &Value, record: &Value| -> Result<String, Error> {
let item: GroupListItem = serde_json::from_value(record.to_owned())?;
- Ok(tools::format::render_backup_file_list(&item.files))
+ Ok(pbs_tools::format::render_backup_file_list(&item.files))
};
let options = default_table_format_options()
}
struct CatalogUploadResult {
- catalog_writer: Arc<Mutex<CatalogWriter<crate::tools::StdChannelWriter>>>,
+ catalog_writer: Arc<Mutex<CatalogWriter<TokioWriterAdapter<StdChannelWriter>>>>,
result: tokio::sync::oneshot::Receiver<Result<BackupStats, Error>>,
}
let catalog_chunk_size = 512*1024;
let catalog_chunk_stream = ChunkStream::new(catalog_stream, Some(catalog_chunk_size));
- let catalog_writer = Arc::new(Mutex::new(CatalogWriter::new(crate::tools::StdChannelWriter::new(catalog_tx))?));
+ let catalog_writer = Arc::new(Mutex::new(CatalogWriter::new(TokioWriterAdapter::new(StdChannelWriter::new(catalog_tx)))?));
let (catalog_result_tx, catalog_result_rx) = tokio::sync::oneshot::channel();
Ok(CatalogUploadResult { catalog_writer, result: catalog_result_rx })
}
-#[derive(Debug, Eq, PartialEq)]
-struct CryptoParams {
- mode: CryptMode,
- enc_key: Option<Vec<u8>>,
- // FIXME switch to openssl::rsa::rsa<openssl::pkey::Public> once that is Eq?
- master_pubkey: Option<Vec<u8>>,
-}
-
-fn crypto_parameters(param: &Value) -> Result<CryptoParams, Error> {
- let keyfile = match param.get("keyfile") {
- Some(Value::String(keyfile)) => Some(keyfile),
- Some(_) => bail!("bad --keyfile parameter type"),
- None => None,
- };
-
- let key_fd = match param.get("keyfd") {
- Some(Value::Number(key_fd)) => Some(
- RawFd::try_from(key_fd
- .as_i64()
- .ok_or_else(|| format_err!("bad key fd: {:?}", key_fd))?
- )
- .map_err(|err| format_err!("bad key fd: {:?}: {}", key_fd, err))?
- ),
- Some(_) => bail!("bad --keyfd parameter type"),
- None => None,
- };
-
- let master_pubkey_file = match param.get("master-pubkey-file") {
- Some(Value::String(keyfile)) => Some(keyfile),
- Some(_) => bail!("bad --master-pubkey-file parameter type"),
- None => None,
- };
-
- let master_pubkey_fd = match param.get("master-pubkey-fd") {
- Some(Value::Number(key_fd)) => Some(
- RawFd::try_from(key_fd
- .as_i64()
- .ok_or_else(|| format_err!("bad master public key fd: {:?}", key_fd))?
- )
- .map_err(|err| format_err!("bad public master key fd: {:?}: {}", key_fd, err))?
- ),
- Some(_) => bail!("bad --master-pubkey-fd parameter type"),
- None => None,
- };
-
- let mode: Option<CryptMode> = match param.get("crypt-mode") {
- Some(mode) => Some(serde_json::from_value(mode.clone())?),
- None => None,
- };
-
- let keydata = match (keyfile, key_fd) {
- (None, None) => None,
- (Some(_), Some(_)) => bail!("--keyfile and --keyfd are mutually exclusive"),
- (Some(keyfile), None) => {
- eprintln!("Using encryption key file: {}", keyfile);
- Some(file_get_contents(keyfile)?)
- },
- (None, Some(fd)) => {
- let input = unsafe { std::fs::File::from_raw_fd(fd) };
- let mut data = Vec::new();
- let _len: usize = { input }.read_to_end(&mut data)
- .map_err(|err| {
- format_err!("error reading encryption key from fd {}: {}", fd, err)
- })?;
- eprintln!("Using encryption key from file descriptor");
- Some(data)
- }
- };
-
- let master_pubkey_data = match (master_pubkey_file, master_pubkey_fd) {
- (None, None) => None,
- (Some(_), Some(_)) => bail!("--keyfile and --keyfd are mutually exclusive"),
- (Some(keyfile), None) => {
- eprintln!("Using master key from file: {}", keyfile);
- Some(file_get_contents(keyfile)?)
- },
- (None, Some(fd)) => {
- let input = unsafe { std::fs::File::from_raw_fd(fd) };
- let mut data = Vec::new();
- let _len: usize = { input }.read_to_end(&mut data)
- .map_err(|err| {
- format_err!("error reading master key from fd {}: {}", fd, err)
- })?;
- eprintln!("Using master key from file descriptor");
- Some(data)
- }
- };
-
- let res = match mode {
- // no crypt mode, enable encryption if keys are available
- None => match (keydata, master_pubkey_data) {
- // only default keys if available
- (None, None) => match key::read_optional_default_encryption_key()? {
- None => CryptoParams { mode: CryptMode::None, enc_key: None, master_pubkey: None },
- enc_key => {
- eprintln!("Encrypting with default encryption key!");
- let master_pubkey = key::read_optional_default_master_pubkey()?;
- CryptoParams {
- mode: CryptMode::Encrypt,
- enc_key,
- master_pubkey,
- }
- },
- },
-
- // explicit master key, default enc key needed
- (None, master_pubkey) => match key::read_optional_default_encryption_key()? {
- None => bail!("--master-pubkey-file/--master-pubkey-fd specified, but no key available"),
- enc_key => {
- eprintln!("Encrypting with default encryption key!");
- CryptoParams {
- mode: CryptMode::Encrypt,
- enc_key,
- master_pubkey,
- }
- },
- },
-
- // explicit keyfile, maybe default master key
- (enc_key, None) => CryptoParams { mode: CryptMode::Encrypt, enc_key, master_pubkey: key::read_optional_default_master_pubkey()? },
-
- // explicit keyfile and master key
- (enc_key, master_pubkey) => CryptoParams { mode: CryptMode::Encrypt, enc_key, master_pubkey },
- },
-
- // explicitly disabled encryption
- Some(CryptMode::None) => match (keydata, master_pubkey_data) {
- // no keys => OK, no encryption
- (None, None) => CryptoParams { mode: CryptMode::None, enc_key: None, master_pubkey: None },
-
- // --keyfile and --crypt-mode=none
- (Some(_), _) => bail!("--keyfile/--keyfd and --crypt-mode=none are mutually exclusive"),
-
- // --master-pubkey-file and --crypt-mode=none
- (_, Some(_)) => bail!("--master-pubkey-file/--master-pubkey-fd and --crypt-mode=none are mutually exclusive"),
- },
-
- // explicitly enabled encryption
- Some(mode) => match (keydata, master_pubkey_data) {
- // no key, maybe master key
- (None, master_pubkey) => match key::read_optional_default_encryption_key()? {
- None => bail!("--crypt-mode without --keyfile and no default key file available"),
- enc_key => {
- eprintln!("Encrypting with default encryption key!");
- let master_pubkey = match master_pubkey {
- None => key::read_optional_default_master_pubkey()?,
- master_pubkey => master_pubkey,
- };
-
- CryptoParams {
- mode,
- enc_key,
- master_pubkey,
- }
- },
- },
-
- // --keyfile and --crypt-mode other than none
- (enc_key, master_pubkey) => {
- let master_pubkey = match master_pubkey {
- None => key::read_optional_default_master_pubkey()?,
- master_pubkey => master_pubkey,
- };
-
- CryptoParams { mode, enc_key, master_pubkey }
- },
- },
- };
-
- Ok(res)
-}
-
-#[test]
-// WARNING: there must only be one test for crypto_parameters as the default key handling is not
-// safe w.r.t. concurrency
-fn test_crypto_parameters_handling() -> Result<(), Error> {
- let some_key = Some(vec![1;1]);
- let default_key = Some(vec![2;1]);
-
- let some_master_key = Some(vec![3;1]);
- let default_master_key = Some(vec![4;1]);
-
- let no_key_res = CryptoParams {
- enc_key: None,
- master_pubkey: None,
- mode: CryptMode::None,
- };
- let some_key_res = CryptoParams {
- enc_key: some_key.clone(),
- master_pubkey: None,
- mode: CryptMode::Encrypt,
- };
- let some_key_some_master_res = CryptoParams {
- enc_key: some_key.clone(),
- master_pubkey: some_master_key.clone(),
- mode: CryptMode::Encrypt,
- };
- let some_key_default_master_res = CryptoParams {
- enc_key: some_key.clone(),
- master_pubkey: default_master_key.clone(),
- mode: CryptMode::Encrypt,
- };
-
- let some_key_sign_res = CryptoParams {
- enc_key: some_key.clone(),
- master_pubkey: None,
- mode: CryptMode::SignOnly,
- };
- let default_key_res = CryptoParams {
- enc_key: default_key.clone(),
- master_pubkey: None,
- mode: CryptMode::Encrypt,
- };
- let default_key_sign_res = CryptoParams {
- enc_key: default_key.clone(),
- master_pubkey: None,
- mode: CryptMode::SignOnly,
- };
-
- let keypath = "./tests/keyfile.test";
- replace_file(&keypath, some_key.as_ref().unwrap(), CreateOptions::default())?;
- let master_keypath = "./tests/masterkeyfile.test";
- replace_file(&master_keypath, some_master_key.as_ref().unwrap(), CreateOptions::default())?;
- let invalid_keypath = "./tests/invalid_keyfile.test";
-
- // no params, no default key == no key
- let res = crypto_parameters(&json!({}));
- assert_eq!(res.unwrap(), no_key_res);
-
- // keyfile param == key from keyfile
- let res = crypto_parameters(&json!({"keyfile": keypath}));
- assert_eq!(res.unwrap(), some_key_res);
-
- // crypt mode none == no key
- let res = crypto_parameters(&json!({"crypt-mode": "none"}));
- assert_eq!(res.unwrap(), no_key_res);
-
- // crypt mode encrypt/sign-only, no keyfile, no default key == Error
- assert!(crypto_parameters(&json!({"crypt-mode": "sign-only"})).is_err());
- assert!(crypto_parameters(&json!({"crypt-mode": "encrypt"})).is_err());
-
- // crypt mode none with explicit key == Error
- assert!(crypto_parameters(&json!({"crypt-mode": "none", "keyfile": keypath})).is_err());
-
- // crypt mode sign-only/encrypt with keyfile == key from keyfile with correct mode
- let res = crypto_parameters(&json!({"crypt-mode": "sign-only", "keyfile": keypath}));
- assert_eq!(res.unwrap(), some_key_sign_res);
- let res = crypto_parameters(&json!({"crypt-mode": "encrypt", "keyfile": keypath}));
- assert_eq!(res.unwrap(), some_key_res);
-
- // invalid keyfile parameter always errors
- assert!(crypto_parameters(&json!({"keyfile": invalid_keypath})).is_err());
- assert!(crypto_parameters(&json!({"keyfile": invalid_keypath, "crypt-mode": "none"})).is_err());
- assert!(crypto_parameters(&json!({"keyfile": invalid_keypath, "crypt-mode": "sign-only"})).is_err());
- assert!(crypto_parameters(&json!({"keyfile": invalid_keypath, "crypt-mode": "encrypt"})).is_err());
-
- // now set a default key
- unsafe { key::set_test_encryption_key(Ok(default_key.clone())); }
-
- // and repeat
-
- // no params but default key == default key
- let res = crypto_parameters(&json!({}));
- assert_eq!(res.unwrap(), default_key_res);
-
- // keyfile param == key from keyfile
- let res = crypto_parameters(&json!({"keyfile": keypath}));
- assert_eq!(res.unwrap(), some_key_res);
-
- // crypt mode none == no key
- let res = crypto_parameters(&json!({"crypt-mode": "none"}));
- assert_eq!(res.unwrap(), no_key_res);
-
- // crypt mode encrypt/sign-only, no keyfile, default key == default key with correct mode
- let res = crypto_parameters(&json!({"crypt-mode": "sign-only"}));
- assert_eq!(res.unwrap(), default_key_sign_res);
- let res = crypto_parameters(&json!({"crypt-mode": "encrypt"}));
- assert_eq!(res.unwrap(), default_key_res);
-
- // crypt mode none with explicit key == Error
- assert!(crypto_parameters(&json!({"crypt-mode": "none", "keyfile": keypath})).is_err());
-
- // crypt mode sign-only/encrypt with keyfile == key from keyfile with correct mode
- let res = crypto_parameters(&json!({"crypt-mode": "sign-only", "keyfile": keypath}));
- assert_eq!(res.unwrap(), some_key_sign_res);
- let res = crypto_parameters(&json!({"crypt-mode": "encrypt", "keyfile": keypath}));
- assert_eq!(res.unwrap(), some_key_res);
-
- // invalid keyfile parameter always errors
- assert!(crypto_parameters(&json!({"keyfile": invalid_keypath})).is_err());
- assert!(crypto_parameters(&json!({"keyfile": invalid_keypath, "crypt-mode": "none"})).is_err());
- assert!(crypto_parameters(&json!({"keyfile": invalid_keypath, "crypt-mode": "sign-only"})).is_err());
- assert!(crypto_parameters(&json!({"keyfile": invalid_keypath, "crypt-mode": "encrypt"})).is_err());
-
- // now make default key retrieval error
- unsafe { key::set_test_encryption_key(Err(format_err!("test error"))); }
-
- // and repeat
-
- // no params, default key retrieval errors == Error
- assert!(crypto_parameters(&json!({})).is_err());
-
- // keyfile param == key from keyfile
- let res = crypto_parameters(&json!({"keyfile": keypath}));
- assert_eq!(res.unwrap(), some_key_res);
-
- // crypt mode none == no key
- let res = crypto_parameters(&json!({"crypt-mode": "none"}));
- assert_eq!(res.unwrap(), no_key_res);
-
- // crypt mode encrypt/sign-only, no keyfile, default key error == Error
- assert!(crypto_parameters(&json!({"crypt-mode": "sign-only"})).is_err());
- assert!(crypto_parameters(&json!({"crypt-mode": "encrypt"})).is_err());
-
- // crypt mode none with explicit key == Error
- assert!(crypto_parameters(&json!({"crypt-mode": "none", "keyfile": keypath})).is_err());
-
- // crypt mode sign-only/encrypt with keyfile == key from keyfile with correct mode
- let res = crypto_parameters(&json!({"crypt-mode": "sign-only", "keyfile": keypath}));
- assert_eq!(res.unwrap(), some_key_sign_res);
- let res = crypto_parameters(&json!({"crypt-mode": "encrypt", "keyfile": keypath}));
- assert_eq!(res.unwrap(), some_key_res);
-
- // invalid keyfile parameter always errors
- assert!(crypto_parameters(&json!({"keyfile": invalid_keypath})).is_err());
- assert!(crypto_parameters(&json!({"keyfile": invalid_keypath, "crypt-mode": "none"})).is_err());
- assert!(crypto_parameters(&json!({"keyfile": invalid_keypath, "crypt-mode": "sign-only"})).is_err());
- assert!(crypto_parameters(&json!({"keyfile": invalid_keypath, "crypt-mode": "encrypt"})).is_err());
-
- // now remove default key again
- unsafe { key::set_test_encryption_key(Ok(None)); }
- // set a default master key
- unsafe { key::set_test_default_master_pubkey(Ok(default_master_key.clone())); }
-
- // and use an explicit master key
- assert!(crypto_parameters(&json!({"master-pubkey-file": master_keypath})).is_err());
- // just a default == no key
- let res = crypto_parameters(&json!({}));
- assert_eq!(res.unwrap(), no_key_res);
-
- // keyfile param == key from keyfile
- let res = crypto_parameters(&json!({"keyfile": keypath, "master-pubkey-file": master_keypath}));
- assert_eq!(res.unwrap(), some_key_some_master_res);
- // same with fallback to default master key
- let res = crypto_parameters(&json!({"keyfile": keypath}));
- assert_eq!(res.unwrap(), some_key_default_master_res);
-
- // crypt mode none == error
- assert!(crypto_parameters(&json!({"crypt-mode": "none", "master-pubkey-file": master_keypath})).is_err());
- // with just default master key == no key
- let res = crypto_parameters(&json!({"crypt-mode": "none"}));
- assert_eq!(res.unwrap(), no_key_res);
-
- // crypt mode encrypt without enc key == error
- assert!(crypto_parameters(&json!({"crypt-mode": "encrypt", "master-pubkey-file": master_keypath})).is_err());
- assert!(crypto_parameters(&json!({"crypt-mode": "encrypt"})).is_err());
-
- // crypt mode none with explicit key == Error
- assert!(crypto_parameters(&json!({"crypt-mode": "none", "keyfile": keypath, "master-pubkey-file": master_keypath})).is_err());
- assert!(crypto_parameters(&json!({"crypt-mode": "none", "keyfile": keypath})).is_err());
-
- // crypt mode encrypt with keyfile == key from keyfile with correct mode
- let res = crypto_parameters(&json!({"crypt-mode": "encrypt", "keyfile": keypath, "master-pubkey-file": master_keypath}));
- assert_eq!(res.unwrap(), some_key_some_master_res);
- let res = crypto_parameters(&json!({"crypt-mode": "encrypt", "keyfile": keypath}));
- assert_eq!(res.unwrap(), some_key_default_master_res);
-
- // invalid master keyfile parameter always errors when a key is passed, even with a valid
- // default master key
- assert!(crypto_parameters(&json!({"keyfile": keypath, "master-pubkey-file": invalid_keypath})).is_err());
- assert!(crypto_parameters(&json!({"keyfile": keypath, "master-pubkey-file": invalid_keypath,"crypt-mode": "none"})).is_err());
- assert!(crypto_parameters(&json!({"keyfile": keypath, "master-pubkey-file": invalid_keypath,"crypt-mode": "sign-only"})).is_err());
- assert!(crypto_parameters(&json!({"keyfile": keypath, "master-pubkey-file": invalid_keypath,"crypt-mode": "encrypt"})).is_err());
-
- Ok(())
-}
-
#[api(
input: {
properties: {
let (crypt_config, rsa_encrypted_key) = match crypto.enc_key {
None => (None, None),
- Some(key) => {
- let (key, created, fingerprint) = decrypt_key(&key, &key::get_encryption_key_password)?;
+ Some(key_with_source) => {
+ println!(
+ "{}",
+ format_key_source(&key_with_source.source, "encryption")
+ );
+
+ let (key, created, fingerprint) =
+ decrypt_key(&key_with_source.key, &get_encryption_key_password)?;
println!("Encryption key fingerprint: {}", fingerprint);
let crypt_config = CryptConfig::new(key)?;
match crypto.master_pubkey {
- Some(pem_data) => {
- let rsa = openssl::rsa::Rsa::public_key_from_pem(&pem_data)?;
+ Some(pem_with_source) => {
+ println!("{}", format_key_source(&pem_with_source.source, "master"));
+
+ let rsa = openssl::rsa::Rsa::public_key_from_pem(&pem_with_source.key)?;
let mut key_config = KeyConfig::without_password(key)?;
key_config.created = created; // keep original value
let enc_key = rsa_encrypt_key_config(rsa, &key_config)?;
(Some(Arc::new(crypt_config)), Some(enc_key))
- }
+ },
_ => (Some(Arc::new(crypt_config)), None),
}
}
Ok(Value::Null)
}
-fn complete_backup_source(arg: &str, param: &HashMap<String, String>) -> Vec<String> {
-
- let mut result = vec![];
-
- let data: Vec<&str> = arg.splitn(2, ':').collect();
-
- if data.len() != 2 {
- result.push(String::from("root.pxar:/"));
- result.push(String::from("etc.pxar:/etc"));
- return result;
- }
-
- let files = tools::complete_file_name(data[1], param);
-
- for file in files {
- result.push(format!("{}:{}", data[0], file));
- }
-
- result
-}
-
async fn dump_image<W: Write>(
client: Arc<BackupReader>,
crypt_config: Option<Arc<CryptConfig>>,
type: String,
description: r###"Target directory path. Use '-' to write to standard output.
-We do not extraxt '.pxar' archives when writing to standard output.
+We do not extract '.pxar' archives when writing to standard output.
"###
},
let crypt_config = match crypto.enc_key {
None => None,
- Some(key) => {
- let (key, _, fingerprint) = decrypt_key(&key, &key::get_encryption_key_password)?;
- eprintln!("Encryption key fingerprint: '{}'", fingerprint);
+ Some(ref key) => {
+ let (key, _, _) =
+ decrypt_key(&key.key, &get_encryption_key_password).map_err(|err| {
+ eprintln!("{}", format_key_source(&key.source, "encryption"));
+ err
+ })?;
Some(Arc::new(CryptConfig::new(key)?))
}
};
if archive_name == ENCRYPTED_KEY_BLOB_NAME && crypt_config.is_none() {
eprintln!("Restoring encrypted key blob without original key - skipping manifest fingerprint check!")
} else {
+ if manifest.signature.is_some() {
+ if let Some(key) = &crypto.enc_key {
+ eprintln!("{}", format_key_source(&key.source, "encryption"));
+ }
+ if let Some(config) = &crypt_config {
+ eprintln!("Fingerprint: {}", config.fingerprint());
+ }
+ }
manifest.check_fingerprint(crypt_config.as_ref().map(Arc::as_ref))?;
}
Ok(Value::Null)
}
-const API_METHOD_PRUNE: ApiMethod = ApiMethod::new(
- &ApiHandler::Async(&prune),
- &ObjectSchema::new(
- "Prune a backup repository.",
- &proxmox_backup::add_common_prune_prameters!([
- ("dry-run", true, &BooleanSchema::new(
- "Just show what prune would do, but do not delete anything.")
- .schema()),
- ("group", false, &StringSchema::new("Backup group.").schema()),
- ], [
- ("output-format", true, &OUTPUT_FORMAT),
- (
- "quiet",
- true,
- &BooleanSchema::new("Minimal output - only show removals.")
- .schema()
- ),
- ("repository", true, &REPO_URL_SCHEMA),
- ])
- )
-);
-
-fn prune<'a>(
- param: Value,
- _info: &ApiMethod,
- _rpcenv: &'a mut dyn RpcEnvironment,
-) -> proxmox::api::ApiFuture<'a> {
- async move {
- prune_async(param).await
- }.boxed()
-}
-
-async fn prune_async(mut param: Value) -> Result<Value, Error> {
+#[api(
+ input: {
+ properties: {
+ "dry-run": {
+ type: bool,
+ optional: true,
+ description: "Just show what prune would do, but do not delete anything.",
+ },
+ group: {
+ type: String,
+ description: "Backup group",
+ },
+ "prune-options": {
+ type: PruneOptions,
+ flatten: true,
+ },
+ "output-format": {
+ schema: OUTPUT_FORMAT,
+ optional: true,
+ },
+ quiet: {
+ type: bool,
+ optional: true,
+ default: false,
+ description: "Minimal output - only show removals.",
+ },
+ repository: {
+ schema: REPO_URL_SCHEMA,
+ optional: true,
+ },
+ },
+ },
+)]
+/// Prune a backup repository.
+async fn prune(
+ dry_run: Option<bool>,
+ group: String,
+ prune_options: PruneOptions,
+ quiet: bool,
+ mut param: Value
+) -> Result<Value, Error> {
let repo = extract_repository_from_value(¶m)?;
let mut client = connect(&repo)?;
let path = format!("api2/json/admin/datastore/{}/prune", repo.store());
- let group = tools::required_string_param(¶m, "group")?;
let group: BackupGroup = group.parse()?;
- let output_format = get_output_format(¶m);
-
- let quiet = param["quiet"].as_bool().unwrap_or(false);
-
- param.as_object_mut().unwrap().remove("repository");
- param.as_object_mut().unwrap().remove("group");
- param.as_object_mut().unwrap().remove("output-format");
- param.as_object_mut().unwrap().remove("quiet");
+ let output_format = extract_output_format(&mut param);
- param["backup-type"] = group.backup_type().into();
- param["backup-id"] = group.backup_id().into();
+ let mut api_param = serde_json::to_value(prune_options)?;
+ if let Some(dry_run) = dry_run {
+ api_param["dry-run"] = dry_run.into();
+ }
+ api_param["backup-type"] = group.backup_type().into();
+ api_param["backup-id"] = group.backup_id().into();
- let mut result = client.post(&path, Some(param)).await?;
+ let mut result = client.post(&path, Some(api_param)).await?;
record_repository(&repo);
.sortby("backup-id", false)
.sortby("backup-time", false)
.column(ColumnConfig::new("backup-id").renderer(render_snapshot_path).header("snapshot"))
- .column(ColumnConfig::new("backup-time").renderer(tools::format::render_epoch).header("date"))
+ .column(ColumnConfig::new("backup-time").renderer(pbs_tools::format::render_epoch).header("date"))
.column(ColumnConfig::new("keep").renderer(render_prune_action).header("action"))
;
Ok(Value::Null)
}
-// like get, but simply ignore errors and return Null instead
-async fn try_get(repo: &BackupRepository, url: &str) -> Value {
-
- let fingerprint = std::env::var(ENV_VAR_PBS_FINGERPRINT).ok();
- let password = std::env::var(ENV_VAR_PBS_PASSWORD).ok();
-
- // ticket cache, but no questions asked
- let options = HttpClientOptions::new_interactive(password, fingerprint)
- .interactive(false);
-
- let client = match HttpClient::new(repo.host(), repo.port(), repo.auth_id(), options) {
- Ok(v) => v,
- _ => return Value::Null,
- };
-
- let mut resp = match client.get(url, None).await {
- Ok(v) => v,
- _ => return Value::Null,
- };
-
- if let Some(map) = resp.as_object_mut() {
- if let Some(data) = map.remove("data") {
- return data;
- }
- }
- Value::Null
-}
-
-fn complete_backup_group(_arg: &str, param: &HashMap<String, String>) -> Vec<String> {
- proxmox_backup::tools::runtime::main(async { complete_backup_group_do(param).await })
-}
-
-async fn complete_backup_group_do(param: &HashMap<String, String>) -> Vec<String> {
-
- let mut result = vec![];
-
- let repo = match extract_repository_from_map(param) {
- Some(v) => v,
- _ => return result,
- };
-
- let path = format!("api2/json/admin/datastore/{}/groups", repo.store());
-
- let data = try_get(&repo, &path).await;
-
- if let Some(list) = data.as_array() {
- for item in list {
- if let (Some(backup_id), Some(backup_type)) =
- (item["backup-id"].as_str(), item["backup-type"].as_str())
- {
- result.push(format!("{}/{}", backup_type, backup_id));
- }
- }
- }
-
- result
-}
-
-pub fn complete_group_or_snapshot(arg: &str, param: &HashMap<String, String>) -> Vec<String> {
- proxmox_backup::tools::runtime::main(async { complete_group_or_snapshot_do(arg, param).await })
-}
-
-async fn complete_group_or_snapshot_do(arg: &str, param: &HashMap<String, String>) -> Vec<String> {
-
- if arg.matches('/').count() < 2 {
- let groups = complete_backup_group_do(param).await;
- let mut result = vec![];
- for group in groups {
- result.push(group.to_string());
- result.push(format!("{}/", group));
- }
- return result;
- }
-
- complete_backup_snapshot_do(param).await
-}
-
-fn complete_backup_snapshot(_arg: &str, param: &HashMap<String, String>) -> Vec<String> {
- proxmox_backup::tools::runtime::main(async { complete_backup_snapshot_do(param).await })
-}
-
-async fn complete_backup_snapshot_do(param: &HashMap<String, String>) -> Vec<String> {
-
- let mut result = vec![];
-
- let repo = match extract_repository_from_map(param) {
- Some(v) => v,
- _ => return result,
- };
-
- let path = format!("api2/json/admin/datastore/{}/snapshots", repo.store());
-
- let data = try_get(&repo, &path).await;
-
- if let Some(list) = data.as_array() {
- for item in list {
- if let (Some(backup_id), Some(backup_type), Some(backup_time)) =
- (item["backup-id"].as_str(), item["backup-type"].as_str(), item["backup-time"].as_i64())
- {
- if let Ok(snapshot) = BackupDir::new(backup_type, backup_id, backup_time) {
- result.push(snapshot.relative_path().to_str().unwrap().to_owned());
- }
- }
- }
- }
-
- result
-}
-
-fn complete_server_file_name(_arg: &str, param: &HashMap<String, String>) -> Vec<String> {
- proxmox_backup::tools::runtime::main(async { complete_server_file_name_do(param).await })
-}
-
-async fn complete_server_file_name_do(param: &HashMap<String, String>) -> Vec<String> {
-
- let mut result = vec![];
-
- let repo = match extract_repository_from_map(param) {
- Some(v) => v,
- _ => return result,
- };
-
- let snapshot: BackupDir = match param.get("snapshot") {
- Some(path) => {
- match path.parse() {
- Ok(v) => v,
- _ => return result,
- }
- }
- _ => return result,
- };
-
- 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(),
- })).unwrap();
-
- let path = format!("api2/json/admin/datastore/{}/files?{}", repo.store(), query);
-
- let data = try_get(&repo, &path).await;
-
- if let Some(list) = data.as_array() {
- for item in list {
- if let Some(filename) = item["filename"].as_str() {
- result.push(filename.to_owned());
- }
- }
- }
-
- result
-}
-
-fn complete_archive_name(arg: &str, param: &HashMap<String, String>) -> Vec<String> {
- complete_server_file_name(arg, param)
- .iter()
- .map(|v| tools::format::strip_server_file_extension(&v))
- .collect()
-}
-
-pub fn complete_pxar_archive_name(arg: &str, param: &HashMap<String, String>) -> Vec<String> {
- complete_server_file_name(arg, param)
- .iter()
- .filter_map(|name| {
- if name.ends_with(".pxar.didx") {
- Some(tools::format::strip_server_file_extension(name))
- } else {
- None
- }
- })
- .collect()
-}
-
-pub fn complete_img_archive_name(arg: &str, param: &HashMap<String, String>) -> Vec<String> {
- complete_server_file_name(arg, param)
- .iter()
- .filter_map(|name| {
- if name.ends_with(".img.fidx") {
- Some(tools::format::strip_server_file_extension(name))
- } else {
- None
- }
- })
- .collect()
-}
-
-fn complete_chunk_size(_arg: &str, _param: &HashMap<String, String>) -> Vec<String> {
-
- let mut result = vec![];
-
- let mut size = 64;
- loop {
- result.push(size.to_string());
- size *= 2;
- if size > 4096 { break; }
- }
-
- result
-}
-
-fn complete_auth_id(_arg: &str, param: &HashMap<String, String>) -> Vec<String> {
- proxmox_backup::tools::runtime::main(async { complete_auth_id_do(param).await })
-}
-
-async fn complete_auth_id_do(param: &HashMap<String, String>) -> Vec<String> {
-
- let mut result = vec![];
-
- let repo = match extract_repository_from_map(param) {
- Some(v) => v,
- _ => return result,
- };
-
- let data = try_get(&repo, "api2/json/access/users?include_tokens=true").await;
-
- if let Ok(parsed) = serde_json::from_value::<Vec<UserWithTokens>>(data) {
- for user in parsed {
- result.push(user.userid.to_string());
- for token in user.tokens {
- result.push(token.tokenid.to_string());
- }
- }
- };
-
- result
-}
-
use proxmox_backup::client::RemoteChunkReader;
/// This is a workaround until we have cleaned up the chunk/reader/... infrastructure for better
/// async use!
let rpcenv = CliEnvironment::new();
run_cli_command(cmd_def, rpcenv, Some(|future| {
- proxmox_backup::tools::runtime::main(future)
+ pbs_runtime::main(future)
}));
}