]>
Commit | Line | Data |
---|---|---|
f1a83e97 | 1 | //! Shared tools useful for common CLI clients. |
f1a83e97 SR |
2 | use std::collections::HashMap; |
3 | ||
ff8945fd | 4 | use anyhow::{bail, format_err, Context, Error}; |
f1a83e97 SR |
5 | use serde_json::{json, Value}; |
6 | use xdg::BaseDirectories; | |
7 | ||
8 | use proxmox::{ | |
9 | api::schema::*, | |
10 | tools::fs::file_get_json, | |
11 | }; | |
12 | ||
75f83c6a | 13 | use pbs_api_types::{BACKUP_REPO_URL, Authid}; |
af06decd | 14 | use pbs_buildcfg; |
9eb78407 WB |
15 | use pbs_datastore::BackupDir; |
16 | use pbs_tools::json::json_object_to_query; | |
af06decd | 17 | |
f1a83e97 | 18 | use proxmox_backup::api2::access::user::UserWithTokens; |
75f83c6a | 19 | use proxmox_backup::client::{BackupRepository, HttpClient, HttpClientOptions}; |
f1a83e97 SR |
20 | use proxmox_backup::tools; |
21 | ||
ff8945fd SR |
22 | pub mod key_source; |
23 | ||
f1a83e97 SR |
24 | const ENV_VAR_PBS_FINGERPRINT: &str = "PBS_FINGERPRINT"; |
25 | const ENV_VAR_PBS_PASSWORD: &str = "PBS_PASSWORD"; | |
26 | ||
27 | pub const REPO_URL_SCHEMA: Schema = StringSchema::new("Repository URL.") | |
28 | .format(&BACKUP_REPO_URL) | |
29 | .max_length(256) | |
30 | .schema(); | |
31 | ||
f1a83e97 SR |
32 | pub 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 | ||
38 | pub fn get_default_repository() -> Option<String> { | |
39 | std::env::var("PBS_REPOSITORY").ok() | |
40 | } | |
41 | ||
42 | pub 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 | ||
54 | pub 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 | ||
62 | pub 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 | ||
67 | fn 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 | |
83 | pub 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 | ||
110 | pub 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 | ||
114 | pub 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 | ||
140 | pub 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 | ||
144 | pub 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 | ||
159 | pub 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 | ||
163 | pub 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 | ||
191 | pub 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 | ||
195 | pub 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 | ||
235 | pub 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 | ||
242 | pub 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 | ||
255 | pub 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 | ||
268 | pub 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 | ||
282 | pub 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 | ||
286 | pub 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 | ||
309 | pub 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 | ||
334 | pub 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 | |
354 | pub 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: | |
359 | pub 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 | ||
369 | pub 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. | |
382 | pub 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 | } |