1 //! Shared tools useful for common CLI clients.
2 use std
::collections
::HashMap
;
4 use std
::os
::unix
::io
::FromRawFd
;
5 use std
::env
::VarError
::{NotUnicode, NotPresent}
;
6 use std
::io
::{BufReader, BufRead}
;
7 use std
::process
::Command
;
9 use anyhow
::{bail, format_err, Context, Error}
;
10 use serde_json
::{json, Value}
;
11 use xdg
::BaseDirectories
;
13 use proxmox_schema
::*;
14 use proxmox_router
::cli
::shellword_split
;
15 use proxmox
::tools
::fs
::file_get_json
;
17 use pbs_api_types
::{BACKUP_REPO_URL, Authid, UserWithTokens}
;
18 use pbs_datastore
::BackupDir
;
19 use pbs_tools
::json
::json_object_to_query
;
21 use crate::{BackupRepository, HttpClient, HttpClientOptions}
;
25 const ENV_VAR_PBS_FINGERPRINT
: &str = "PBS_FINGERPRINT";
26 const ENV_VAR_PBS_PASSWORD
: &str = "PBS_PASSWORD";
28 pub const REPO_URL_SCHEMA
: Schema
= StringSchema
::new("Repository URL.")
29 .format(&BACKUP_REPO_URL
)
33 pub const CHUNK_SIZE_SCHEMA
: Schema
= IntegerSchema
::new("Chunk size in KB. Must be a power of 2.")
39 /// Helper to read a secret through a environment variable (ENV).
41 /// Tries the following variable names in order and returns the value
42 /// it will resolve for the first defined one:
44 /// BASE_NAME => use value from ENV(BASE_NAME) directly as secret
45 /// BASE_NAME_FD => read the secret from the specified file descriptor
46 /// BASE_NAME_FILE => read the secret from the specified file name
47 /// BASE_NAME_CMD => read the secret from specified command first line of output on stdout
49 /// Only return the first line of data (without CRLF).
50 pub fn get_secret_from_env(base_name
: &str) -> Result
<Option
<String
>, Error
> {
52 let firstline
= |data
: String
| -> String
{
53 match data
.lines().next() {
54 Some(line
) => line
.to_string(),
55 None
=> String
::new(),
59 let firstline_file
= |file
: &mut File
| -> Result
<String
, Error
> {
60 let reader
= BufReader
::new(file
);
61 match reader
.lines().next() {
62 Some(Ok(line
)) => Ok(line
),
63 Some(Err(err
)) => Err(err
.into()),
64 None
=> Ok(String
::new()),
68 match std
::env
::var(base_name
) {
69 Ok(p
) => return Ok(Some(firstline(p
))),
70 Err(NotUnicode(_
)) => bail
!(format
!("{} contains bad characters", base_name
)),
71 Err(NotPresent
) => {}
,
74 let env_name
= format
!("{}_FD", base_name
);
75 match std
::env
::var(&env_name
) {
77 let fd
: i32 = fd_str
.parse()
78 .map_err(|err
| format_err
!("unable to parse file descriptor in ENV({}): {}", env_name
, err
))?
;
79 let mut file
= unsafe { File::from_raw_fd(fd) }
;
80 return Ok(Some(firstline_file(&mut file
)?
));
82 Err(NotUnicode(_
)) => bail
!(format
!("{} contains bad characters", env_name
)),
83 Err(NotPresent
) => {}
,
86 let env_name
= format
!("{}_FILE", base_name
);
87 match std
::env
::var(&env_name
) {
89 let mut file
= std
::fs
::File
::open(filename
)
90 .map_err(|err
| format_err
!("unable to open file in ENV({}): {}", env_name
, err
))?
;
91 return Ok(Some(firstline_file(&mut file
)?
));
93 Err(NotUnicode(_
)) => bail
!(format
!("{} contains bad characters", env_name
)),
94 Err(NotPresent
) => {}
,
97 let env_name
= format
!("{}_CMD", base_name
);
98 match std
::env
::var(&env_name
) {
100 let args
= shellword_split(command
)?
;
101 let mut command
= Command
::new(&args
[0]);
102 command
.args(&args
[1..]);
103 let output
= pbs_tools
::run_command(command
, None
)?
;
104 return Ok(Some(firstline(output
)));
106 Err(NotUnicode(_
)) => bail
!(format
!("{} contains bad characters", env_name
)),
107 Err(NotPresent
) => {}
,
113 pub fn get_default_repository() -> Option
<String
> {
114 std
::env
::var("PBS_REPOSITORY").ok()
117 pub fn extract_repository_from_value(param
: &Value
) -> Result
<BackupRepository
, Error
> {
118 let repo_url
= param
["repository"]
121 .or_else(get_default_repository
)
122 .ok_or_else(|| format_err
!("unable to get (default) repository"))?
;
124 let repo
: BackupRepository
= repo_url
.parse()?
;
129 pub fn extract_repository_from_map(param
: &HashMap
<String
, String
>) -> Option
<BackupRepository
> {
133 .or_else(get_default_repository
)
134 .and_then(|repo_url
| repo_url
.parse
::<BackupRepository
>().ok())
137 pub fn connect(repo
: &BackupRepository
) -> Result
<HttpClient
, Error
> {
138 connect_do(repo
.host(), repo
.port(), repo
.auth_id())
139 .map_err(|err
| format_err
!("error building client for repository {} - {}", repo
, err
))
142 fn connect_do(server
: &str, port
: u16, auth_id
: &Authid
) -> Result
<HttpClient
, Error
> {
143 let fingerprint
= std
::env
::var(ENV_VAR_PBS_FINGERPRINT
).ok();
145 let password
= get_secret_from_env(ENV_VAR_PBS_PASSWORD
)?
;
146 let options
= HttpClientOptions
::new_interactive(password
, fingerprint
);
148 HttpClient
::new(server
, port
, auth_id
, options
)
151 /// like get, but simply ignore errors and return Null instead
152 pub async
fn try_get(repo
: &BackupRepository
, url
: &str) -> Value
{
154 let fingerprint
= std
::env
::var(ENV_VAR_PBS_FINGERPRINT
).ok();
155 let password
= get_secret_from_env(ENV_VAR_PBS_PASSWORD
).unwrap_or(None
);
157 // ticket cache, but no questions asked
158 let options
= HttpClientOptions
::new_interactive(password
, fingerprint
)
161 let client
= match HttpClient
::new(repo
.host(), repo
.port(), repo
.auth_id(), options
) {
163 _
=> return Value
::Null
,
166 let mut resp
= match client
.get(url
, None
).await
{
168 _
=> return Value
::Null
,
171 if let Some(map
) = resp
.as_object_mut() {
172 if let Some(data
) = map
.remove("data") {
179 pub fn complete_backup_group(_arg
: &str, param
: &HashMap
<String
, String
>) -> Vec
<String
> {
180 pbs_runtime
::main(async { complete_backup_group_do(param).await }
)
183 pub async
fn complete_backup_group_do(param
: &HashMap
<String
, String
>) -> Vec
<String
> {
185 let mut result
= vec
![];
187 let repo
= match extract_repository_from_map(param
) {
192 let path
= format
!("api2/json/admin/datastore/{}/groups", repo
.store());
194 let data
= try_get(&repo
, &path
).await
;
196 if let Some(list
) = data
.as_array() {
198 if let (Some(backup_id
), Some(backup_type
)) =
199 (item
["backup-id"].as_str(), item
["backup-type"].as_str())
201 result
.push(format
!("{}/{}", backup_type
, backup_id
));
209 pub fn complete_group_or_snapshot(arg
: &str, param
: &HashMap
<String
, String
>) -> Vec
<String
> {
210 pbs_runtime
::main(async { complete_group_or_snapshot_do(arg, param).await }
)
213 pub async
fn complete_group_or_snapshot_do(arg
: &str, param
: &HashMap
<String
, String
>) -> Vec
<String
> {
215 if arg
.matches('
/'
).count() < 2 {
216 let groups
= complete_backup_group_do(param
).await
;
217 let mut result
= vec
![];
218 for group
in groups
{
219 result
.push(group
.to_string());
220 result
.push(format
!("{}/", group
));
225 complete_backup_snapshot_do(param
).await
228 pub fn complete_backup_snapshot(_arg
: &str, param
: &HashMap
<String
, String
>) -> Vec
<String
> {
229 pbs_runtime
::main(async { complete_backup_snapshot_do(param).await }
)
232 pub async
fn complete_backup_snapshot_do(param
: &HashMap
<String
, String
>) -> Vec
<String
> {
234 let mut result
= vec
![];
236 let repo
= match extract_repository_from_map(param
) {
241 let path
= format
!("api2/json/admin/datastore/{}/snapshots", repo
.store());
243 let data
= try_get(&repo
, &path
).await
;
245 if let Some(list
) = data
.as_array() {
247 if let (Some(backup_id
), Some(backup_type
), Some(backup_time
)) =
248 (item
["backup-id"].as_str(), item
["backup-type"].as_str(), item
["backup-time"].as_i64())
250 if let Ok(snapshot
) = BackupDir
::new(backup_type
, backup_id
, backup_time
) {
251 result
.push(snapshot
.relative_path().to_str().unwrap().to_owned());
260 pub fn complete_server_file_name(_arg
: &str, param
: &HashMap
<String
, String
>) -> Vec
<String
> {
261 pbs_runtime
::main(async { complete_server_file_name_do(param).await }
)
264 pub async
fn complete_server_file_name_do(param
: &HashMap
<String
, String
>) -> Vec
<String
> {
266 let mut result
= vec
![];
268 let repo
= match extract_repository_from_map(param
) {
273 let snapshot
: BackupDir
= match param
.get("snapshot") {
283 let query
= json_object_to_query(json
!({
284 "backup-type": snapshot
.group().backup_type(),
285 "backup-id": snapshot
.group().backup_id(),
286 "backup-time": snapshot
.backup_time(),
289 let path
= format
!("api2/json/admin/datastore/{}/files?{}", repo
.store(), query
);
291 let data
= try_get(&repo
, &path
).await
;
293 if let Some(list
) = data
.as_array() {
295 if let Some(filename
) = item
["filename"].as_str() {
296 result
.push(filename
.to_owned());
304 pub fn complete_archive_name(arg
: &str, param
: &HashMap
<String
, String
>) -> Vec
<String
> {
305 complete_server_file_name(arg
, param
)
307 .map(|v
| pbs_tools
::format
::strip_server_file_extension(&v
).to_owned())
311 pub fn complete_pxar_archive_name(arg
: &str, param
: &HashMap
<String
, String
>) -> Vec
<String
> {
312 complete_server_file_name(arg
, param
)
315 if name
.ends_with(".pxar.didx") {
316 Some(pbs_tools
::format
::strip_server_file_extension(name
).to_owned())
324 pub fn complete_img_archive_name(arg
: &str, param
: &HashMap
<String
, String
>) -> Vec
<String
> {
325 complete_server_file_name(arg
, param
)
328 if name
.ends_with(".img.fidx") {
329 Some(pbs_tools
::format
::strip_server_file_extension(name
).to_owned())
337 pub fn complete_chunk_size(_arg
: &str, _param
: &HashMap
<String
, String
>) -> Vec
<String
> {
339 let mut result
= vec
![];
343 result
.push(size
.to_string());
345 if size
> 4096 { break; }
351 pub fn complete_auth_id(_arg
: &str, param
: &HashMap
<String
, String
>) -> Vec
<String
> {
352 pbs_runtime
::main(async { complete_auth_id_do(param).await }
)
355 pub async
fn complete_auth_id_do(param
: &HashMap
<String
, String
>) -> Vec
<String
> {
357 let mut result
= vec
![];
359 let repo
= match extract_repository_from_map(param
) {
364 let data
= try_get(&repo
, "api2/json/access/users?include_tokens=true").await
;
366 if let Ok(parsed
) = serde_json
::from_value
::<Vec
<UserWithTokens
>>(data
) {
368 result
.push(user
.userid
.to_string());
369 for token
in user
.tokens
{
370 result
.push(token
.tokenid
.to_string());
378 pub fn complete_repository(_arg
: &str, _param
: &HashMap
<String
, String
>) -> Vec
<String
> {
379 let mut result
= vec
![];
381 let base
= match BaseDirectories
::with_prefix("proxmox-backup") {
386 // usually $HOME/.cache/proxmox-backup/repo-list
387 let path
= match base
.place_cache_file("repo-list") {
392 let data
= file_get_json(&path
, None
).unwrap_or_else(|_
| json
!({}
));
394 if let Some(map
) = data
.as_object() {
395 for (repo
, _count
) in map
{
396 result
.push(repo
.to_owned());
403 pub fn complete_backup_source(arg
: &str, param
: &HashMap
<String
, String
>) -> Vec
<String
> {
404 let mut result
= vec
![];
406 let data
: Vec
<&str> = arg
.splitn(2, '
:'
).collect();
409 result
.push(String
::from("root.pxar:/"));
410 result
.push(String
::from("etc.pxar:/etc"));
414 let files
= pbs_tools
::fs
::complete_file_name(data
[1], param
);
417 result
.push(format
!("{}:{}", data
[0], file
));
423 pub fn base_directories() -> Result
<xdg
::BaseDirectories
, Error
> {
424 xdg
::BaseDirectories
::with_prefix("proxmox-backup").map_err(Error
::from
)
427 /// Convenience helper for better error messages:
428 pub fn find_xdg_file(
429 file_name
: impl AsRef
<std
::path
::Path
>,
430 description
: &'
static str,
431 ) -> Result
<Option
<std
::path
::PathBuf
>, Error
> {
432 let file_name
= file_name
.as_ref();
434 .map(|base
| base
.find_config_file(file_name
))
435 .with_context(|| format
!("error searching for {}", description
))
438 pub fn place_xdg_file(
439 file_name
: impl AsRef
<std
::path
::Path
>,
440 description
: &'
static str,
441 ) -> Result
<std
::path
::PathBuf
, Error
> {
442 let file_name
= file_name
.as_ref();
444 .and_then(|base
| base
.place_config_file(file_name
).map_err(Error
::from
))
445 .with_context(|| format
!("failed to place {} in xdg home", description
))