]> git.proxmox.com Git - proxmox-backup.git/blame - src/bin/proxmox_client_tools/mod.rs
move more helpers to pbs-tools
[proxmox-backup.git] / src / bin / proxmox_client_tools / mod.rs
CommitLineData
f1a83e97 1//! Shared tools useful for common CLI clients.
f1a83e97
SR
2use std::collections::HashMap;
3
ff8945fd 4use anyhow::{bail, format_err, Context, Error};
f1a83e97
SR
5use serde_json::{json, Value};
6use xdg::BaseDirectories;
7
8use proxmox::{
9 api::schema::*,
10 tools::fs::file_get_json,
11};
12
75f83c6a 13use pbs_api_types::{BACKUP_REPO_URL, Authid};
af06decd 14use pbs_buildcfg;
9eb78407
WB
15use pbs_datastore::BackupDir;
16use pbs_tools::json::json_object_to_query;
af06decd 17
f1a83e97 18use proxmox_backup::api2::access::user::UserWithTokens;
75f83c6a 19use proxmox_backup::client::{BackupRepository, HttpClient, HttpClientOptions};
f1a83e97
SR
20use proxmox_backup::tools;
21
ff8945fd
SR
22pub mod key_source;
23
f1a83e97
SR
24const ENV_VAR_PBS_FINGERPRINT: &str = "PBS_FINGERPRINT";
25const ENV_VAR_PBS_PASSWORD: &str = "PBS_PASSWORD";
26
27pub const REPO_URL_SCHEMA: Schema = StringSchema::new("Repository URL.")
28 .format(&BACKUP_REPO_URL)
29 .max_length(256)
30 .schema();
31
f1a83e97
SR
32pub const CHUNK_SIZE_SCHEMA: Schema = IntegerSchema::new("Chunk size in KB. Must be a power of 2.")
33 .minimum(64)
34 .maximum(4096)
35 .default(4096)
36 .schema();
37
38pub fn get_default_repository() -> Option<String> {
39 std::env::var("PBS_REPOSITORY").ok()
40}
41
42pub fn extract_repository_from_value(param: &Value) -> Result<BackupRepository, Error> {
43 let repo_url = param["repository"]
44 .as_str()
45 .map(String::from)
46 .or_else(get_default_repository)
47 .ok_or_else(|| format_err!("unable to get (default) repository"))?;
48
49 let repo: BackupRepository = repo_url.parse()?;
50
51 Ok(repo)
52}
53
54pub fn extract_repository_from_map(param: &HashMap<String, String>) -> Option<BackupRepository> {
55 param
56 .get("repository")
57 .map(String::from)
58 .or_else(get_default_repository)
59 .and_then(|repo_url| repo_url.parse::<BackupRepository>().ok())
60}
61
62pub fn connect(repo: &BackupRepository) -> Result<HttpClient, Error> {
63 connect_do(repo.host(), repo.port(), repo.auth_id())
64 .map_err(|err| format_err!("error building client for repository {} - {}", repo, err))
65}
66
67fn connect_do(server: &str, port: u16, auth_id: &Authid) -> Result<HttpClient, Error> {
68 let fingerprint = std::env::var(ENV_VAR_PBS_FINGERPRINT).ok();
69
70 use std::env::VarError::*;
71 let password = match std::env::var(ENV_VAR_PBS_PASSWORD) {
72 Ok(p) => Some(p),
73 Err(NotUnicode(_)) => bail!(format!("{} contains bad characters", ENV_VAR_PBS_PASSWORD)),
74 Err(NotPresent) => None,
75 };
76
77 let options = HttpClientOptions::new_interactive(password, fingerprint);
78
79 HttpClient::new(server, port, auth_id, options)
80}
81
82/// like get, but simply ignore errors and return Null instead
83pub async fn try_get(repo: &BackupRepository, url: &str) -> Value {
84
85 let fingerprint = std::env::var(ENV_VAR_PBS_FINGERPRINT).ok();
86 let password = std::env::var(ENV_VAR_PBS_PASSWORD).ok();
87
88 // ticket cache, but no questions asked
89 let options = HttpClientOptions::new_interactive(password, fingerprint)
90 .interactive(false);
91
92 let client = match HttpClient::new(repo.host(), repo.port(), repo.auth_id(), options) {
93 Ok(v) => v,
94 _ => return Value::Null,
95 };
96
97 let mut resp = match client.get(url, None).await {
98 Ok(v) => v,
99 _ => return Value::Null,
100 };
101
102 if let Some(map) = resp.as_object_mut() {
103 if let Some(data) = map.remove("data") {
104 return data;
105 }
106 }
107 Value::Null
108}
109
110pub fn complete_backup_group(_arg: &str, param: &HashMap<String, String>) -> Vec<String> {
d420962f 111 pbs_runtime::main(async { complete_backup_group_do(param).await })
f1a83e97
SR
112}
113
114pub async fn complete_backup_group_do(param: &HashMap<String, String>) -> Vec<String> {
115
116 let mut result = vec![];
117
118 let repo = match extract_repository_from_map(param) {
119 Some(v) => v,
120 _ => return result,
121 };
122
123 let path = format!("api2/json/admin/datastore/{}/groups", repo.store());
124
125 let data = try_get(&repo, &path).await;
126
127 if let Some(list) = data.as_array() {
128 for item in list {
129 if let (Some(backup_id), Some(backup_type)) =
130 (item["backup-id"].as_str(), item["backup-type"].as_str())
131 {
132 result.push(format!("{}/{}", backup_type, backup_id));
133 }
134 }
135 }
136
137 result
138}
139
140pub fn complete_group_or_snapshot(arg: &str, param: &HashMap<String, String>) -> Vec<String> {
d420962f 141 pbs_runtime::main(async { complete_group_or_snapshot_do(arg, param).await })
f1a83e97
SR
142}
143
144pub async fn complete_group_or_snapshot_do(arg: &str, param: &HashMap<String, String>) -> Vec<String> {
145
146 if arg.matches('/').count() < 2 {
147 let groups = complete_backup_group_do(param).await;
148 let mut result = vec![];
149 for group in groups {
150 result.push(group.to_string());
151 result.push(format!("{}/", group));
152 }
153 return result;
154 }
155
156 complete_backup_snapshot_do(param).await
157}
158
159pub fn complete_backup_snapshot(_arg: &str, param: &HashMap<String, String>) -> Vec<String> {
d420962f 160 pbs_runtime::main(async { complete_backup_snapshot_do(param).await })
f1a83e97
SR
161}
162
163pub async fn complete_backup_snapshot_do(param: &HashMap<String, String>) -> Vec<String> {
164
165 let mut result = vec![];
166
167 let repo = match extract_repository_from_map(param) {
168 Some(v) => v,
169 _ => return result,
170 };
171
172 let path = format!("api2/json/admin/datastore/{}/snapshots", repo.store());
173
174 let data = try_get(&repo, &path).await;
175
176 if let Some(list) = data.as_array() {
177 for item in list {
178 if let (Some(backup_id), Some(backup_type), Some(backup_time)) =
179 (item["backup-id"].as_str(), item["backup-type"].as_str(), item["backup-time"].as_i64())
180 {
181 if let Ok(snapshot) = BackupDir::new(backup_type, backup_id, backup_time) {
182 result.push(snapshot.relative_path().to_str().unwrap().to_owned());
183 }
184 }
185 }
186 }
187
188 result
189}
190
191pub fn complete_server_file_name(_arg: &str, param: &HashMap<String, String>) -> Vec<String> {
d420962f 192 pbs_runtime::main(async { complete_server_file_name_do(param).await })
f1a83e97
SR
193}
194
195pub async fn complete_server_file_name_do(param: &HashMap<String, String>) -> Vec<String> {
196
197 let mut result = vec![];
198
199 let repo = match extract_repository_from_map(param) {
200 Some(v) => v,
201 _ => return result,
202 };
203
204 let snapshot: BackupDir = match param.get("snapshot") {
205 Some(path) => {
206 match path.parse() {
207 Ok(v) => v,
208 _ => return result,
209 }
210 }
211 _ => return result,
212 };
213
9eb78407 214 let query = json_object_to_query(json!({
f1a83e97
SR
215 "backup-type": snapshot.group().backup_type(),
216 "backup-id": snapshot.group().backup_id(),
217 "backup-time": snapshot.backup_time(),
218 })).unwrap();
219
220 let path = format!("api2/json/admin/datastore/{}/files?{}", repo.store(), query);
221
222 let data = try_get(&repo, &path).await;
223
224 if let Some(list) = data.as_array() {
225 for item in list {
226 if let Some(filename) = item["filename"].as_str() {
227 result.push(filename.to_owned());
228 }
229 }
230 }
231
232 result
233}
234
235pub fn complete_archive_name(arg: &str, param: &HashMap<String, String>) -> Vec<String> {
236 complete_server_file_name(arg, param)
237 .iter()
770a36e5 238 .map(|v| pbs_tools::format::strip_server_file_extension(&v))
f1a83e97
SR
239 .collect()
240}
241
242pub fn complete_pxar_archive_name(arg: &str, param: &HashMap<String, String>) -> Vec<String> {
243 complete_server_file_name(arg, param)
244 .iter()
245 .filter_map(|name| {
246 if name.ends_with(".pxar.didx") {
770a36e5 247 Some(pbs_tools::format::strip_server_file_extension(name))
f1a83e97
SR
248 } else {
249 None
250 }
251 })
252 .collect()
253}
254
255pub fn complete_img_archive_name(arg: &str, param: &HashMap<String, String>) -> Vec<String> {
256 complete_server_file_name(arg, param)
257 .iter()
258 .filter_map(|name| {
259 if name.ends_with(".img.fidx") {
770a36e5 260 Some(pbs_tools::format::strip_server_file_extension(name))
f1a83e97
SR
261 } else {
262 None
263 }
264 })
265 .collect()
266}
267
268pub fn complete_chunk_size(_arg: &str, _param: &HashMap<String, String>) -> Vec<String> {
269
270 let mut result = vec![];
271
272 let mut size = 64;
273 loop {
274 result.push(size.to_string());
275 size *= 2;
276 if size > 4096 { break; }
277 }
278
279 result
280}
281
282pub fn complete_auth_id(_arg: &str, param: &HashMap<String, String>) -> Vec<String> {
d420962f 283 pbs_runtime::main(async { complete_auth_id_do(param).await })
f1a83e97
SR
284}
285
286pub async fn complete_auth_id_do(param: &HashMap<String, String>) -> Vec<String> {
287
288 let mut result = vec![];
289
290 let repo = match extract_repository_from_map(param) {
291 Some(v) => v,
292 _ => return result,
293 };
294
295 let data = try_get(&repo, "api2/json/access/users?include_tokens=true").await;
296
297 if let Ok(parsed) = serde_json::from_value::<Vec<UserWithTokens>>(data) {
298 for user in parsed {
299 result.push(user.userid.to_string());
300 for token in user.tokens {
301 result.push(token.tokenid.to_string());
302 }
303 }
304 };
305
306 result
307}
308
309pub fn complete_repository(_arg: &str, _param: &HashMap<String, String>) -> Vec<String> {
310 let mut result = vec![];
311
312 let base = match BaseDirectories::with_prefix("proxmox-backup") {
313 Ok(v) => v,
314 _ => return result,
315 };
316
317 // usually $HOME/.cache/proxmox-backup/repo-list
318 let path = match base.place_cache_file("repo-list") {
319 Ok(v) => v,
320 _ => return result,
321 };
322
323 let data = file_get_json(&path, None).unwrap_or_else(|_| json!({}));
324
325 if let Some(map) = data.as_object() {
326 for (repo, _count) in map {
327 result.push(repo.to_owned());
328 }
329 }
330
331 result
332}
333
334pub fn complete_backup_source(arg: &str, param: &HashMap<String, String>) -> Vec<String> {
335 let mut result = vec![];
336
337 let data: Vec<&str> = arg.splitn(2, ':').collect();
338
339 if data.len() != 2 {
340 result.push(String::from("root.pxar:/"));
341 result.push(String::from("etc.pxar:/etc"));
342 return result;
343 }
344
345 let files = tools::complete_file_name(data[1], param);
346
347 for file in files {
348 result.push(format!("{}:{}", data[0], file));
349 }
350
351 result
352}
ff8945fd
SR
353
354pub fn base_directories() -> Result<xdg::BaseDirectories, Error> {
355 xdg::BaseDirectories::with_prefix("proxmox-backup").map_err(Error::from)
356}
357
358/// Convenience helper for better error messages:
359pub fn find_xdg_file(
360 file_name: impl AsRef<std::path::Path>,
361 description: &'static str,
362) -> Result<Option<std::path::PathBuf>, Error> {
363 let file_name = file_name.as_ref();
364 base_directories()
365 .map(|base| base.find_config_file(file_name))
366 .with_context(|| format!("error searching for {}", description))
367}
368
369pub fn place_xdg_file(
370 file_name: impl AsRef<std::path::Path>,
371 description: &'static str,
372) -> Result<std::path::PathBuf, Error> {
373 let file_name = file_name.as_ref();
374 base_directories()
375 .and_then(|base| base.place_config_file(file_name).map_err(Error::from))
376 .with_context(|| format!("failed to place {} in xdg home", description))
377}
58421ec1
SR
378
379/// Returns a runtime dir owned by the current user.
380/// Note that XDG_RUNTIME_DIR is not always available, especially for non-login users like
381/// "www-data", so we use a custom one in /run/proxmox-backup/<uid> instead.
382pub fn get_user_run_dir() -> Result<std::path::PathBuf, Error> {
383 let uid = nix::unistd::Uid::current();
af06decd 384 let mut path: std::path::PathBuf = pbs_buildcfg::PROXMOX_BACKUP_RUN_DIR.into();
58421ec1
SR
385 path.push(uid.to_string());
386 tools::create_run_dir()?;
387 std::fs::create_dir_all(&path)?;
388 Ok(path)
389}