1 //! Shared tools useful for common CLI clients.
2 use std
::collections
::HashMap
;
3 use std
::env
::VarError
::{NotPresent, NotUnicode}
;
5 use std
::io
::{BufRead, BufReader}
;
6 use std
::os
::unix
::io
::FromRawFd
;
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_router
::cli
::{complete_file_name, shellword_split}
;
14 use proxmox_schema
::*;
15 use proxmox_sys
::fs
::file_get_json
;
17 use pbs_api_types
::{Authid, RateLimitConfig, UserWithTokens, BACKUP_REPO_URL}
;
18 use pbs_tools
::json
::json_object_to_query
;
20 use crate::{BackupRepository, HttpClient, HttpClientOptions}
;
24 const ENV_VAR_PBS_FINGERPRINT
: &str = "PBS_FINGERPRINT";
25 const ENV_VAR_PBS_PASSWORD
: &str = "PBS_PASSWORD";
27 pub const REPO_URL_SCHEMA
: Schema
= StringSchema
::new("Repository URL.")
28 .format(&BACKUP_REPO_URL
)
32 pub const CHUNK_SIZE_SCHEMA
: Schema
= IntegerSchema
::new("Chunk size in KB. Must be a power of 2.")
38 /// Helper to read a secret through a environment variable (ENV).
40 /// Tries the following variable names in order and returns the value
41 /// it will resolve for the first defined one:
43 /// BASE_NAME => use value from ENV(BASE_NAME) directly as secret
44 /// BASE_NAME_FD => read the secret from the specified file descriptor
45 /// BASE_NAME_FILE => read the secret from the specified file name
46 /// BASE_NAME_CMD => read the secret from specified command first line of output on stdout
48 /// Only return the first line of data (without CRLF).
49 pub fn get_secret_from_env(base_name
: &str) -> Result
<Option
<String
>, Error
> {
50 let firstline
= |data
: String
| -> String
{
51 match data
.lines().next() {
52 Some(line
) => line
.to_string(),
53 None
=> String
::new(),
57 let firstline_file
= |file
: &mut File
| -> Result
<String
, Error
> {
58 let reader
= BufReader
::new(file
);
59 match reader
.lines().next() {
60 Some(Ok(line
)) => Ok(line
),
61 Some(Err(err
)) => Err(err
.into()),
62 None
=> Ok(String
::new()),
66 match std
::env
::var(base_name
) {
67 Ok(p
) => return Ok(Some(firstline(p
))),
68 Err(NotUnicode(_
)) => bail
!(format
!("{} contains bad characters", base_name
)),
72 let env_name
= format
!("{}_FD", base_name
);
73 match std
::env
::var(&env_name
) {
75 let fd
: i32 = fd_str
.parse().map_err(|err
| {
77 "unable to parse file descriptor in ENV({}): {}",
82 let mut file
= unsafe { File::from_raw_fd(fd) }
;
83 return Ok(Some(firstline_file(&mut file
)?
));
85 Err(NotUnicode(_
)) => bail
!(format
!("{} contains bad characters", env_name
)),
89 let env_name
= format
!("{}_FILE", base_name
);
90 match std
::env
::var(&env_name
) {
92 let mut file
= std
::fs
::File
::open(filename
)
93 .map_err(|err
| format_err
!("unable to open file in ENV({}): {}", env_name
, err
))?
;
94 return Ok(Some(firstline_file(&mut file
)?
));
96 Err(NotUnicode(_
)) => bail
!(format
!("{} contains bad characters", env_name
)),
100 let env_name
= format
!("{}_CMD", base_name
);
101 match std
::env
::var(&env_name
) {
103 let args
= shellword_split(command
)?
;
104 let mut command
= Command
::new(&args
[0]);
105 command
.args(&args
[1..]);
106 let output
= proxmox_sys
::command
::run_command(command
, None
)?
;
107 return Ok(Some(firstline(output
)));
109 Err(NotUnicode(_
)) => bail
!(format
!("{} contains bad characters", env_name
)),
110 Err(NotPresent
) => {}
116 pub fn get_default_repository() -> Option
<String
> {
117 std
::env
::var("PBS_REPOSITORY").ok()
120 pub fn extract_repository_from_value(param
: &Value
) -> Result
<BackupRepository
, Error
> {
121 let repo_url
= param
["repository"]
124 .or_else(get_default_repository
)
125 .ok_or_else(|| format_err
!("unable to get (default) repository"))?
;
127 let repo
: BackupRepository
= repo_url
.parse()?
;
132 pub fn extract_repository_from_map(param
: &HashMap
<String
, String
>) -> Option
<BackupRepository
> {
136 .or_else(get_default_repository
)
137 .and_then(|repo_url
| repo_url
.parse
::<BackupRepository
>().ok())
140 pub fn connect(repo
: &BackupRepository
) -> Result
<HttpClient
, Error
> {
141 let rate_limit
= RateLimitConfig
::default(); // unlimited
142 connect_do(repo
.host(), repo
.port(), repo
.auth_id(), rate_limit
)
143 .map_err(|err
| format_err
!("error building client for repository {} - {}", repo
, err
))
146 pub fn connect_rate_limited(
147 repo
: &BackupRepository
,
148 rate_limit
: RateLimitConfig
,
149 ) -> Result
<HttpClient
, Error
> {
150 connect_do(repo
.host(), repo
.port(), repo
.auth_id(), rate_limit
)
151 .map_err(|err
| format_err
!("error building client for repository {} - {}", repo
, err
))
158 rate_limit
: RateLimitConfig
,
159 ) -> Result
<HttpClient
, Error
> {
160 let fingerprint
= std
::env
::var(ENV_VAR_PBS_FINGERPRINT
).ok();
162 let password
= get_secret_from_env(ENV_VAR_PBS_PASSWORD
)?
;
163 let options
= HttpClientOptions
::new_interactive(password
, fingerprint
).rate_limit(rate_limit
);
165 HttpClient
::new(server
, port
, auth_id
, options
)
168 /// like get, but simply ignore errors and return Null instead
169 pub async
fn try_get(repo
: &BackupRepository
, url
: &str) -> Value
{
170 let fingerprint
= std
::env
::var(ENV_VAR_PBS_FINGERPRINT
).ok();
171 let password
= get_secret_from_env(ENV_VAR_PBS_PASSWORD
).unwrap_or(None
);
173 // ticket cache, but no questions asked
174 let options
= HttpClientOptions
::new_interactive(password
, fingerprint
).interactive(false);
176 let client
= match HttpClient
::new(repo
.host(), repo
.port(), repo
.auth_id(), options
) {
178 _
=> return Value
::Null
,
181 let mut resp
= match client
.get(url
, None
).await
{
183 _
=> return Value
::Null
,
186 if let Some(map
) = resp
.as_object_mut() {
187 if let Some(data
) = map
.remove("data") {
194 pub fn complete_backup_group(_arg
: &str, param
: &HashMap
<String
, String
>) -> Vec
<String
> {
195 proxmox_async
::runtime
::main(async { complete_backup_group_do(param).await }
)
198 pub async
fn complete_backup_group_do(param
: &HashMap
<String
, String
>) -> Vec
<String
> {
199 let mut result
= vec
![];
201 let repo
= match extract_repository_from_map(param
) {
206 let path
= format
!("api2/json/admin/datastore/{}/groups", repo
.store());
208 let data
= try_get(&repo
, &path
).await
;
210 if let Some(list
) = data
.as_array() {
212 if let (Some(backup_id
), Some(backup_type
)) =
213 (item
["backup-id"].as_str(), item
["backup-type"].as_str())
215 result
.push(format
!("{}/{}", backup_type
, backup_id
));
223 pub fn complete_group_or_snapshot(arg
: &str, param
: &HashMap
<String
, String
>) -> Vec
<String
> {
224 proxmox_async
::runtime
::main(async { complete_group_or_snapshot_do(arg, param).await }
)
227 pub async
fn complete_group_or_snapshot_do(
229 param
: &HashMap
<String
, String
>,
231 if arg
.matches('
/'
).count() < 2 {
232 let groups
= complete_backup_group_do(param
).await
;
233 let mut result
= vec
![];
234 for group
in groups
{
235 result
.push(group
.to_string());
236 result
.push(format
!("{}/", group
));
241 complete_backup_snapshot_do(param
).await
244 pub fn complete_backup_snapshot(_arg
: &str, param
: &HashMap
<String
, String
>) -> Vec
<String
> {
245 proxmox_async
::runtime
::main(async { complete_backup_snapshot_do(param).await }
)
248 pub async
fn complete_backup_snapshot_do(param
: &HashMap
<String
, String
>) -> Vec
<String
> {
249 let mut result
= vec
![];
251 let repo
= match extract_repository_from_map(param
) {
256 let path
= format
!("api2/json/admin/datastore/{}/snapshots", repo
.store());
258 let data
= try_get(&repo
, &path
).await
;
260 if let Value
::Array(list
) = data
{
262 match serde_json
::from_value
::<pbs_api_types
::BackupDir
>(item
) {
263 Ok(item
) => result
.push(item
.to_string()),
265 // FIXME: print error in completion?
275 pub fn complete_server_file_name(_arg
: &str, param
: &HashMap
<String
, String
>) -> Vec
<String
> {
276 proxmox_async
::runtime
::main(async { complete_server_file_name_do(param).await }
)
279 pub async
fn complete_server_file_name_do(param
: &HashMap
<String
, String
>) -> Vec
<String
> {
280 let mut result
= vec
![];
282 let repo
= match extract_repository_from_map(param
) {
287 let snapshot
: pbs_api_types
::BackupDir
= match param
.get("snapshot") {
288 Some(path
) => match path
.parse() {
295 let ns
: pbs_api_types
::BackupNamespace
= match param
.get("ns") {
296 Some(ns
) => match ns
.parse() {
303 let query
= json_object_to_query(json
!({
305 "backup-type": snapshot
.group
.ty
,
306 "backup-id": snapshot
.group
.id
,
307 "backup-time": snapshot
.time
,
311 let path
= format
!("api2/json/admin/datastore/{}/files?{}", repo
.store(), query
);
313 let data
= try_get(&repo
, &path
).await
;
315 if let Some(list
) = data
.as_array() {
317 if let Some(filename
) = item
["filename"].as_str() {
318 result
.push(filename
.to_owned());
326 pub fn complete_archive_name(arg
: &str, param
: &HashMap
<String
, String
>) -> Vec
<String
> {
327 complete_server_file_name(arg
, param
)
329 .map(|v
| pbs_tools
::format
::strip_server_file_extension(v
).to_owned())
333 pub fn complete_pxar_archive_name(arg
: &str, param
: &HashMap
<String
, String
>) -> Vec
<String
> {
334 complete_server_file_name(arg
, param
)
337 if name
.ends_with(".pxar.didx") {
338 Some(pbs_tools
::format
::strip_server_file_extension(name
).to_owned())
346 pub fn complete_img_archive_name(arg
: &str, param
: &HashMap
<String
, String
>) -> Vec
<String
> {
347 complete_server_file_name(arg
, param
)
350 if name
.ends_with(".img.fidx") {
351 Some(pbs_tools
::format
::strip_server_file_extension(name
).to_owned())
359 pub fn complete_chunk_size(_arg
: &str, _param
: &HashMap
<String
, String
>) -> Vec
<String
> {
360 let mut result
= vec
![];
364 result
.push(size
.to_string());
374 pub fn complete_auth_id(_arg
: &str, param
: &HashMap
<String
, String
>) -> Vec
<String
> {
375 proxmox_async
::runtime
::main(async { complete_auth_id_do(param).await }
)
378 pub async
fn complete_auth_id_do(param
: &HashMap
<String
, String
>) -> Vec
<String
> {
379 let mut result
= vec
![];
381 let repo
= match extract_repository_from_map(param
) {
386 let data
= try_get(&repo
, "api2/json/access/users?include_tokens=true").await
;
388 if let Ok(parsed
) = serde_json
::from_value
::<Vec
<UserWithTokens
>>(data
) {
390 result
.push(user
.userid
.to_string());
391 for token
in user
.tokens
{
392 result
.push(token
.tokenid
.to_string());
400 pub fn complete_repository(_arg
: &str, _param
: &HashMap
<String
, String
>) -> Vec
<String
> {
401 let mut result
= vec
![];
403 let base
= match BaseDirectories
::with_prefix("proxmox-backup") {
408 // usually $HOME/.cache/proxmox-backup/repo-list
409 let path
= match base
.place_cache_file("repo-list") {
414 let data
= file_get_json(&path
, None
).unwrap_or_else(|_
| json
!({}
));
416 if let Some(map
) = data
.as_object() {
417 for (repo
, _count
) in map
{
418 result
.push(repo
.to_owned());
425 pub fn complete_backup_source(arg
: &str, param
: &HashMap
<String
, String
>) -> Vec
<String
> {
426 let mut result
= vec
![];
428 let data
: Vec
<&str> = arg
.splitn(2, '
:'
).collect();
431 result
.push(String
::from("root.pxar:/"));
432 result
.push(String
::from("etc.pxar:/etc"));
436 let files
= complete_file_name(data
[1], param
);
439 result
.push(format
!("{}:{}", data
[0], file
));
445 pub fn base_directories() -> Result
<xdg
::BaseDirectories
, Error
> {
446 xdg
::BaseDirectories
::with_prefix("proxmox-backup").map_err(Error
::from
)
449 /// Convenience helper for better error messages:
450 pub fn find_xdg_file(
451 file_name
: impl AsRef
<std
::path
::Path
>,
452 description
: &'
static str,
453 ) -> Result
<Option
<std
::path
::PathBuf
>, Error
> {
454 let file_name
= file_name
.as_ref();
456 .map(|base
| base
.find_config_file(file_name
))
457 .with_context(|| format
!("error searching for {}", description
))
460 pub fn place_xdg_file(
461 file_name
: impl AsRef
<std
::path
::Path
>,
462 description
: &'
static str,
463 ) -> Result
<std
::path
::PathBuf
, Error
> {
464 let file_name
= file_name
.as_ref();
466 .and_then(|base
| base
.place_config_file(file_name
).map_err(Error
::from
))
467 .with_context(|| format
!("failed to place {} in xdg home", description
))