]> git.proxmox.com Git - proxmox-backup.git/blame - pbs-client/src/tools/mod.rs
api-types: introduce BackupType enum and Group/Dir api types
[proxmox-backup.git] / pbs-client / src / tools / mod.rs
CommitLineData
f1a83e97 1//! Shared tools useful for common CLI clients.
f1a83e97 2use std::collections::HashMap;
bdfa6370 3use std::env::VarError::{NotPresent, NotUnicode};
16a01c19 4use std::fs::File;
bdfa6370 5use std::io::{BufRead, BufReader};
16a01c19 6use std::os::unix::io::FromRawFd;
16a01c19 7use std::process::Command;
f1a83e97 8
ff8945fd 9use anyhow::{bail, format_err, Context, Error};
f1a83e97
SR
10use serde_json::{json, Value};
11use xdg::BaseDirectories;
12
b3f279e2 13use proxmox_router::cli::{complete_file_name, shellword_split};
bdfa6370 14use proxmox_schema::*;
25877d05 15use proxmox_sys::fs::file_get_json;
f1a83e97 16
bdfa6370 17use pbs_api_types::{Authid, RateLimitConfig, UserWithTokens, BACKUP_REPO_URL};
9eb78407
WB
18use pbs_datastore::BackupDir;
19use pbs_tools::json::json_object_to_query;
af06decd 20
2b7f8dd5 21use crate::{BackupRepository, HttpClient, HttpClientOptions};
f1a83e97 22
ff8945fd
SR
23pub mod key_source;
24
f1a83e97
SR
25const ENV_VAR_PBS_FINGERPRINT: &str = "PBS_FINGERPRINT";
26const ENV_VAR_PBS_PASSWORD: &str = "PBS_PASSWORD";
27
28pub const REPO_URL_SCHEMA: Schema = StringSchema::new("Repository URL.")
29 .format(&BACKUP_REPO_URL)
30 .max_length(256)
31 .schema();
32
f1a83e97
SR
33pub const CHUNK_SIZE_SCHEMA: Schema = IntegerSchema::new("Chunk size in KB. Must be a power of 2.")
34 .minimum(64)
35 .maximum(4096)
36 .default(4096)
37 .schema();
38
16a01c19
DM
39/// Helper to read a secret through a environment variable (ENV).
40///
41/// Tries the following variable names in order and returns the value
42/// it will resolve for the first defined one:
43///
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
48///
49/// Only return the first line of data (without CRLF).
50pub fn get_secret_from_env(base_name: &str) -> Result<Option<String>, Error> {
16a01c19
DM
51 let firstline = |data: String| -> String {
52 match data.lines().next() {
53 Some(line) => line.to_string(),
54 None => String::new(),
55 }
56 };
57
58 let firstline_file = |file: &mut File| -> Result<String, Error> {
59 let reader = BufReader::new(file);
60 match reader.lines().next() {
61 Some(Ok(line)) => Ok(line),
62 Some(Err(err)) => Err(err.into()),
63 None => Ok(String::new()),
64 }
65 };
66
67 match std::env::var(base_name) {
68 Ok(p) => return Ok(Some(firstline(p))),
69 Err(NotUnicode(_)) => bail!(format!("{} contains bad characters", base_name)),
bdfa6370 70 Err(NotPresent) => {}
16a01c19
DM
71 };
72
73 let env_name = format!("{}_FD", base_name);
74 match std::env::var(&env_name) {
75 Ok(fd_str) => {
bdfa6370
TL
76 let fd: i32 = fd_str.parse().map_err(|err| {
77 format_err!(
78 "unable to parse file descriptor in ENV({}): {}",
79 env_name,
80 err
81 )
82 })?;
16a01c19
DM
83 let mut file = unsafe { File::from_raw_fd(fd) };
84 return Ok(Some(firstline_file(&mut file)?));
85 }
86 Err(NotUnicode(_)) => bail!(format!("{} contains bad characters", env_name)),
bdfa6370 87 Err(NotPresent) => {}
16a01c19
DM
88 }
89
90 let env_name = format!("{}_FILE", base_name);
91 match std::env::var(&env_name) {
92 Ok(filename) => {
93 let mut file = std::fs::File::open(filename)
94 .map_err(|err| format_err!("unable to open file in ENV({}): {}", env_name, err))?;
95 return Ok(Some(firstline_file(&mut file)?));
96 }
97 Err(NotUnicode(_)) => bail!(format!("{} contains bad characters", env_name)),
bdfa6370 98 Err(NotPresent) => {}
16a01c19
DM
99 }
100
101 let env_name = format!("{}_CMD", base_name);
102 match std::env::var(&env_name) {
103 Ok(ref command) => {
104 let args = shellword_split(command)?;
105 let mut command = Command::new(&args[0]);
106 command.args(&args[1..]);
25877d05 107 let output = proxmox_sys::command::run_command(command, None)?;
16a01c19
DM
108 return Ok(Some(firstline(output)));
109 }
110 Err(NotUnicode(_)) => bail!(format!("{} contains bad characters", env_name)),
bdfa6370 111 Err(NotPresent) => {}
16a01c19
DM
112 }
113
114 Ok(None)
115}
116
f1a83e97
SR
117pub fn get_default_repository() -> Option<String> {
118 std::env::var("PBS_REPOSITORY").ok()
119}
120
121pub fn extract_repository_from_value(param: &Value) -> Result<BackupRepository, Error> {
122 let repo_url = param["repository"]
123 .as_str()
124 .map(String::from)
125 .or_else(get_default_repository)
126 .ok_or_else(|| format_err!("unable to get (default) repository"))?;
127
128 let repo: BackupRepository = repo_url.parse()?;
129
130 Ok(repo)
131}
132
133pub fn extract_repository_from_map(param: &HashMap<String, String>) -> Option<BackupRepository> {
134 param
135 .get("repository")
136 .map(String::from)
137 .or_else(get_default_repository)
138 .and_then(|repo_url| repo_url.parse::<BackupRepository>().ok())
139}
140
141pub fn connect(repo: &BackupRepository) -> Result<HttpClient, Error> {
2d5287fb
DM
142 let rate_limit = RateLimitConfig::default(); // unlimited
143 connect_do(repo.host(), repo.port(), repo.auth_id(), rate_limit)
f1a83e97
SR
144 .map_err(|err| format_err!("error building client for repository {} - {}", repo, err))
145}
146
2419dc0d
DM
147pub fn connect_rate_limited(
148 repo: &BackupRepository,
2d5287fb 149 rate_limit: RateLimitConfig,
2419dc0d 150) -> Result<HttpClient, Error> {
2d5287fb 151 connect_do(repo.host(), repo.port(), repo.auth_id(), rate_limit)
2419dc0d
DM
152 .map_err(|err| format_err!("error building client for repository {} - {}", repo, err))
153}
154
155fn connect_do(
156 server: &str,
157 port: u16,
158 auth_id: &Authid,
2d5287fb 159 rate_limit: RateLimitConfig,
2419dc0d 160) -> Result<HttpClient, Error> {
f1a83e97
SR
161 let fingerprint = std::env::var(ENV_VAR_PBS_FINGERPRINT).ok();
162
16a01c19 163 let password = get_secret_from_env(ENV_VAR_PBS_PASSWORD)?;
bdfa6370 164 let options = HttpClientOptions::new_interactive(password, fingerprint).rate_limit(rate_limit);
f1a83e97
SR
165
166 HttpClient::new(server, port, auth_id, options)
167}
168
169/// like get, but simply ignore errors and return Null instead
170pub async fn try_get(repo: &BackupRepository, url: &str) -> Value {
f1a83e97 171 let fingerprint = std::env::var(ENV_VAR_PBS_FINGERPRINT).ok();
16a01c19 172 let password = get_secret_from_env(ENV_VAR_PBS_PASSWORD).unwrap_or(None);
f1a83e97
SR
173
174 // ticket cache, but no questions asked
bdfa6370 175 let options = HttpClientOptions::new_interactive(password, fingerprint).interactive(false);
f1a83e97
SR
176
177 let client = match HttpClient::new(repo.host(), repo.port(), repo.auth_id(), options) {
178 Ok(v) => v,
179 _ => return Value::Null,
180 };
181
182 let mut resp = match client.get(url, None).await {
183 Ok(v) => v,
184 _ => return Value::Null,
185 };
186
187 if let Some(map) = resp.as_object_mut() {
188 if let Some(data) = map.remove("data") {
189 return data;
190 }
191 }
192 Value::Null
193}
194
195pub fn complete_backup_group(_arg: &str, param: &HashMap<String, String>) -> Vec<String> {
9a1b24b6 196 proxmox_async::runtime::main(async { complete_backup_group_do(param).await })
f1a83e97
SR
197}
198
199pub async fn complete_backup_group_do(param: &HashMap<String, String>) -> Vec<String> {
f1a83e97
SR
200 let mut result = vec![];
201
202 let repo = match extract_repository_from_map(param) {
203 Some(v) => v,
204 _ => return result,
205 };
206
207 let path = format!("api2/json/admin/datastore/{}/groups", repo.store());
208
209 let data = try_get(&repo, &path).await;
210
211 if let Some(list) = data.as_array() {
212 for item in list {
213 if let (Some(backup_id), Some(backup_type)) =
214 (item["backup-id"].as_str(), item["backup-type"].as_str())
215 {
216 result.push(format!("{}/{}", backup_type, backup_id));
217 }
218 }
219 }
220
221 result
222}
223
224pub fn complete_group_or_snapshot(arg: &str, param: &HashMap<String, String>) -> Vec<String> {
9a1b24b6 225 proxmox_async::runtime::main(async { complete_group_or_snapshot_do(arg, param).await })
f1a83e97
SR
226}
227
bdfa6370
TL
228pub async fn complete_group_or_snapshot_do(
229 arg: &str,
230 param: &HashMap<String, String>,
231) -> Vec<String> {
f1a83e97
SR
232 if arg.matches('/').count() < 2 {
233 let groups = complete_backup_group_do(param).await;
234 let mut result = vec![];
235 for group in groups {
236 result.push(group.to_string());
237 result.push(format!("{}/", group));
238 }
239 return result;
240 }
241
242 complete_backup_snapshot_do(param).await
243}
244
245pub fn complete_backup_snapshot(_arg: &str, param: &HashMap<String, String>) -> Vec<String> {
9a1b24b6 246 proxmox_async::runtime::main(async { complete_backup_snapshot_do(param).await })
f1a83e97
SR
247}
248
249pub async fn complete_backup_snapshot_do(param: &HashMap<String, String>) -> Vec<String> {
f1a83e97
SR
250 let mut result = vec![];
251
252 let repo = match extract_repository_from_map(param) {
253 Some(v) => v,
254 _ => return result,
255 };
256
257 let path = format!("api2/json/admin/datastore/{}/snapshots", repo.store());
258
259 let data = try_get(&repo, &path).await;
260
261 if let Some(list) = data.as_array() {
262 for item in list {
bdfa6370
TL
263 if let (Some(backup_id), Some(backup_type), Some(backup_time)) = (
264 item["backup-id"].as_str(),
265 item["backup-type"].as_str(),
266 item["backup-time"].as_i64(),
267 ) {
988d575d
WB
268 let backup_type = match backup_type.parse() {
269 Ok(ty) => ty,
270 Err(_) => {
271 // FIXME: print error in completion?
272 continue;
273 }
274 };
f1a83e97
SR
275 if let Ok(snapshot) = BackupDir::new(backup_type, backup_id, backup_time) {
276 result.push(snapshot.relative_path().to_str().unwrap().to_owned());
277 }
278 }
279 }
280 }
281
282 result
283}
284
285pub fn complete_server_file_name(_arg: &str, param: &HashMap<String, String>) -> Vec<String> {
9a1b24b6 286 proxmox_async::runtime::main(async { complete_server_file_name_do(param).await })
f1a83e97
SR
287}
288
289pub async fn complete_server_file_name_do(param: &HashMap<String, String>) -> Vec<String> {
f1a83e97
SR
290 let mut result = vec![];
291
292 let repo = match extract_repository_from_map(param) {
293 Some(v) => v,
294 _ => return result,
295 };
296
297 let snapshot: BackupDir = match param.get("snapshot") {
bdfa6370
TL
298 Some(path) => match path.parse() {
299 Ok(v) => v,
300 _ => return result,
301 },
f1a83e97
SR
302 _ => return result,
303 };
304
9eb78407 305 let query = json_object_to_query(json!({
f1a83e97
SR
306 "backup-type": snapshot.group().backup_type(),
307 "backup-id": snapshot.group().backup_id(),
308 "backup-time": snapshot.backup_time(),
bdfa6370
TL
309 }))
310 .unwrap();
f1a83e97
SR
311
312 let path = format!("api2/json/admin/datastore/{}/files?{}", repo.store(), query);
313
314 let data = try_get(&repo, &path).await;
315
316 if let Some(list) = data.as_array() {
317 for item in list {
318 if let Some(filename) = item["filename"].as_str() {
319 result.push(filename.to_owned());
320 }
321 }
322 }
323
324 result
325}
326
327pub fn complete_archive_name(arg: &str, param: &HashMap<String, String>) -> Vec<String> {
328 complete_server_file_name(arg, param)
329 .iter()
9a37bd6c 330 .map(|v| pbs_tools::format::strip_server_file_extension(v).to_owned())
f1a83e97
SR
331 .collect()
332}
333
334pub fn complete_pxar_archive_name(arg: &str, param: &HashMap<String, String>) -> Vec<String> {
335 complete_server_file_name(arg, param)
336 .iter()
337 .filter_map(|name| {
338 if name.ends_with(".pxar.didx") {
d7eedbd2 339 Some(pbs_tools::format::strip_server_file_extension(name).to_owned())
f1a83e97
SR
340 } else {
341 None
342 }
343 })
344 .collect()
345}
346
347pub fn complete_img_archive_name(arg: &str, param: &HashMap<String, String>) -> Vec<String> {
348 complete_server_file_name(arg, param)
349 .iter()
350 .filter_map(|name| {
351 if name.ends_with(".img.fidx") {
d7eedbd2 352 Some(pbs_tools::format::strip_server_file_extension(name).to_owned())
f1a83e97
SR
353 } else {
354 None
355 }
356 })
357 .collect()
358}
359
360pub fn complete_chunk_size(_arg: &str, _param: &HashMap<String, String>) -> Vec<String> {
f1a83e97
SR
361 let mut result = vec![];
362
363 let mut size = 64;
364 loop {
365 result.push(size.to_string());
366 size *= 2;
bdfa6370
TL
367 if size > 4096 {
368 break;
369 }
f1a83e97
SR
370 }
371
372 result
373}
374
375pub fn complete_auth_id(_arg: &str, param: &HashMap<String, String>) -> Vec<String> {
9a1b24b6 376 proxmox_async::runtime::main(async { complete_auth_id_do(param).await })
f1a83e97
SR
377}
378
379pub async fn complete_auth_id_do(param: &HashMap<String, String>) -> Vec<String> {
f1a83e97
SR
380 let mut result = vec![];
381
382 let repo = match extract_repository_from_map(param) {
383 Some(v) => v,
384 _ => return result,
385 };
386
387 let data = try_get(&repo, "api2/json/access/users?include_tokens=true").await;
388
389 if let Ok(parsed) = serde_json::from_value::<Vec<UserWithTokens>>(data) {
390 for user in parsed {
391 result.push(user.userid.to_string());
392 for token in user.tokens {
393 result.push(token.tokenid.to_string());
394 }
395 }
396 };
397
398 result
399}
400
401pub fn complete_repository(_arg: &str, _param: &HashMap<String, String>) -> Vec<String> {
402 let mut result = vec![];
403
404 let base = match BaseDirectories::with_prefix("proxmox-backup") {
405 Ok(v) => v,
406 _ => return result,
407 };
408
409 // usually $HOME/.cache/proxmox-backup/repo-list
410 let path = match base.place_cache_file("repo-list") {
411 Ok(v) => v,
412 _ => return result,
413 };
414
415 let data = file_get_json(&path, None).unwrap_or_else(|_| json!({}));
416
417 if let Some(map) = data.as_object() {
418 for (repo, _count) in map {
419 result.push(repo.to_owned());
420 }
421 }
422
423 result
424}
425
426pub fn complete_backup_source(arg: &str, param: &HashMap<String, String>) -> Vec<String> {
427 let mut result = vec![];
428
429 let data: Vec<&str> = arg.splitn(2, ':').collect();
430
431 if data.len() != 2 {
432 result.push(String::from("root.pxar:/"));
433 result.push(String::from("etc.pxar:/etc"));
434 return result;
435 }
436
b3f279e2 437 let files = complete_file_name(data[1], param);
f1a83e97
SR
438
439 for file in files {
440 result.push(format!("{}:{}", data[0], file));
441 }
442
443 result
444}
ff8945fd
SR
445
446pub fn base_directories() -> Result<xdg::BaseDirectories, Error> {
447 xdg::BaseDirectories::with_prefix("proxmox-backup").map_err(Error::from)
448}
449
450/// Convenience helper for better error messages:
451pub fn find_xdg_file(
452 file_name: impl AsRef<std::path::Path>,
453 description: &'static str,
454) -> Result<Option<std::path::PathBuf>, Error> {
455 let file_name = file_name.as_ref();
456 base_directories()
457 .map(|base| base.find_config_file(file_name))
458 .with_context(|| format!("error searching for {}", description))
459}
460
461pub fn place_xdg_file(
462 file_name: impl AsRef<std::path::Path>,
463 description: &'static str,
464) -> Result<std::path::PathBuf, Error> {
465 let file_name = file_name.as_ref();
466 base_directories()
467 .and_then(|base| base.place_config_file(file_name).map_err(Error::from))
468 .with_context(|| format!("failed to place {} in xdg home", description))
469}