]> git.proxmox.com Git - proxmox-backup.git/blame - src/bin/proxmox-backup-client.rs
src/bin/proxmox-backup-client.rs - key API: pass kdf parameter
[proxmox-backup.git] / src / bin / proxmox-backup-client.rs
CommitLineData
826f309b 1//#[macro_use]
fe0e04c6 2extern crate proxmox_backup;
ff5d3707 3
4use failure::*;
728797d0 5//use std::os::unix::io::AsRawFd;
1c0472e8 6use chrono::{Local, TimeZone};
e9c9409a 7use std::path::{Path, PathBuf};
496a6784 8use std::collections::HashMap;
ff5d3707 9
fe0e04c6 10use proxmox_backup::tools;
4de0e142 11use proxmox_backup::cli::*;
ef2f2efb 12use proxmox_backup::api_schema::*;
dc9a007b 13use proxmox_backup::api_schema::router::*;
151c6ce2 14use proxmox_backup::client::*;
247cdbce 15use proxmox_backup::backup::*;
fe0e04c6
DM
16//use proxmox_backup::backup::image_index::*;
17//use proxmox_backup::config::datastore;
8968258b 18//use proxmox_backup::pxar::encoder::*;
728797d0 19//use proxmox_backup::backup::datastore::*;
23bb8780 20
f5f13ebc 21use serde_json::{json, Value};
1c0472e8 22//use hyper::Body;
33d64b81 23use std::sync::Arc;
ae0be2dd 24use regex::Regex;
d0a03d40 25use xdg::BaseDirectories;
ae0be2dd
DM
26
27use lazy_static::lazy_static;
5a2df000 28use futures::*;
c4ff3dce 29use tokio::sync::mpsc;
ae0be2dd
DM
30
31lazy_static! {
ec8a9bb9 32 static ref BACKUPSPEC_REGEX: Regex = Regex::new(r"^([a-zA-Z0-9_-]+\.(?:pxar|img|conf)):(.+)$").unwrap();
f2401311
DM
33
34 static ref REPO_URL_SCHEMA: Arc<Schema> = Arc::new(
35 StringSchema::new("Repository URL.")
36 .format(BACKUP_REPO_URL.clone())
37 .max_length(256)
38 .into()
39 );
ae0be2dd 40}
33d64b81 41
d0a03d40
DM
42
43fn record_repository(repo: &BackupRepository) {
44
45 let base = match BaseDirectories::with_prefix("proxmox-backup") {
46 Ok(v) => v,
47 _ => return,
48 };
49
50 // usually $HOME/.cache/proxmox-backup/repo-list
51 let path = match base.place_cache_file("repo-list") {
52 Ok(v) => v,
53 _ => return,
54 };
55
49cf9f3d 56 let mut data = tools::file_get_json(&path, None).unwrap_or(json!({}));
d0a03d40
DM
57
58 let repo = repo.to_string();
59
60 data[&repo] = json!{ data[&repo].as_i64().unwrap_or(0) + 1 };
61
62 let mut map = serde_json::map::Map::new();
63
64 loop {
65 let mut max_used = 0;
66 let mut max_repo = None;
67 for (repo, count) in data.as_object().unwrap() {
68 if map.contains_key(repo) { continue; }
69 if let Some(count) = count.as_i64() {
70 if count > max_used {
71 max_used = count;
72 max_repo = Some(repo);
73 }
74 }
75 }
76 if let Some(repo) = max_repo {
77 map.insert(repo.to_owned(), json!(max_used));
78 } else {
79 break;
80 }
81 if map.len() > 10 { // store max. 10 repos
82 break;
83 }
84 }
85
86 let new_data = json!(map);
87
88 let _ = tools::file_set_contents(path, new_data.to_string().as_bytes(), None);
89}
90
49811347 91fn complete_repository(_arg: &str, _param: &HashMap<String, String>) -> Vec<String> {
d0a03d40
DM
92
93 let mut result = vec![];
94
95 let base = match BaseDirectories::with_prefix("proxmox-backup") {
96 Ok(v) => v,
97 _ => return result,
98 };
99
100 // usually $HOME/.cache/proxmox-backup/repo-list
101 let path = match base.place_cache_file("repo-list") {
102 Ok(v) => v,
103 _ => return result,
104 };
105
49cf9f3d 106 let data = tools::file_get_json(&path, None).unwrap_or(json!({}));
d0a03d40
DM
107
108 if let Some(map) = data.as_object() {
49811347 109 for (repo, _count) in map {
d0a03d40
DM
110 result.push(repo.to_owned());
111 }
112 }
113
114 result
115}
116
17d6979a 117fn backup_directory<P: AsRef<Path>>(
c4ff3dce 118 client: &BackupClient,
17d6979a 119 dir_path: P,
247cdbce 120 archive_name: &str,
36898ffc 121 chunk_size: Option<usize>,
eed6db39 122 all_file_systems: bool,
219ef0e6 123 verbose: bool,
f98ac774 124 crypt_config: Option<Arc<CryptConfig>>,
247cdbce 125) -> Result<(), Error> {
33d64b81 126
c4ff3dce 127 let pxar_stream = PxarBackupStream::open(dir_path.as_ref(), all_file_systems, verbose)?;
36898ffc 128 let chunk_stream = ChunkStream::new(pxar_stream, chunk_size);
ff3d3100 129
c4ff3dce 130 let (tx, rx) = mpsc::channel(10); // allow to buffer 10 chunks
5e7a09be 131
c4ff3dce
DM
132 let stream = rx
133 .map_err(Error::from)
134 .and_then(|x| x); // flatten
17d6979a 135
c4ff3dce
DM
136 // spawn chunker inside a separate task so that it can run parallel
137 tokio::spawn(
138 tx.send_all(chunk_stream.then(|r| Ok(r)))
1c0472e8 139 .map_err(|_| {}).map(|_| ())
c4ff3dce 140 );
17d6979a 141
f98ac774 142 client.upload_stream(archive_name, stream, "dynamic", None, crypt_config).wait()?;
bcd879cf
DM
143
144 Ok(())
145}
146
6af905c1
DM
147fn backup_image<P: AsRef<Path>>(
148 client: &BackupClient,
149 image_path: P,
150 archive_name: &str,
151 image_size: u64,
36898ffc 152 chunk_size: Option<usize>,
1c0472e8 153 _verbose: bool,
f98ac774 154 crypt_config: Option<Arc<CryptConfig>>,
6af905c1
DM
155) -> Result<(), Error> {
156
6af905c1
DM
157 let path = image_path.as_ref().to_owned();
158
159 let file = tokio::fs::File::open(path).wait()?;
160
161 let stream = tokio::codec::FramedRead::new(file, tokio::codec::BytesCodec::new())
162 .map_err(Error::from);
163
36898ffc 164 let stream = FixedChunkStream::new(stream, chunk_size.unwrap_or(4*1024*1024));
6af905c1 165
f98ac774 166 client.upload_stream(archive_name, stream, "fixed", Some(image_size), crypt_config).wait()?;
6af905c1
DM
167
168 Ok(())
169}
170
8e39232a
DM
171fn strip_chunked_file_expenstions(list: Vec<String>) -> Vec<String> {
172
173 let mut result = vec![];
174
175 for file in list.into_iter() {
176 if file.ends_with(".didx") {
177 result.push(file[..file.len()-5].to_owned());
178 } else if file.ends_with(".fidx") {
179 result.push(file[..file.len()-5].to_owned());
180 } else {
181 result.push(file); // should not happen
182 }
183 }
184
185 result
186}
187
8968258b 188/* not used:
6049b71f
DM
189fn list_backups(
190 param: Value,
191 _info: &ApiMethod,
dd5495d6 192 _rpcenv: &mut dyn RpcEnvironment,
6049b71f 193) -> Result<Value, Error> {
41c039e1 194
33d64b81 195 let repo_url = tools::required_string_param(&param, "repository")?;
edd3c8c6 196 let repo: BackupRepository = repo_url.parse()?;
41c039e1 197
45cdce06 198 let mut client = HttpClient::new(repo.host(), repo.user())?;
41c039e1 199
d0a03d40 200 let path = format!("api2/json/admin/datastore/{}/backups", repo.store());
41c039e1 201
9e391bb7 202 let result = client.get(&path, None)?;
41c039e1 203
d0a03d40
DM
204 record_repository(&repo);
205
8c75372b
DM
206 // fixme: implement and use output formatter instead ..
207 let list = result["data"].as_array().unwrap();
208
209 for item in list {
210
49dc0740
DM
211 let id = item["backup-id"].as_str().unwrap();
212 let btype = item["backup-type"].as_str().unwrap();
213 let epoch = item["backup-time"].as_i64().unwrap();
e909522f 214
391d3107 215 let backup_dir = BackupDir::new(btype, id, epoch);
e909522f
DM
216
217 let files = item["files"].as_array().unwrap().iter().map(|v| v.as_str().unwrap().to_owned()).collect();
8e39232a 218 let files = strip_chunked_file_expenstions(files);
e909522f 219
8e39232a
DM
220 for filename in files {
221 let path = backup_dir.relative_path().to_str().unwrap().to_owned();
222 println!("{} | {}/{}", backup_dir.backup_time().format("%c"), path, filename);
8c75372b
DM
223 }
224 }
225
226 //Ok(result)
227 Ok(Value::Null)
41c039e1 228}
8968258b 229 */
41c039e1 230
812c6f87
DM
231fn list_backup_groups(
232 param: Value,
233 _info: &ApiMethod,
dd5495d6 234 _rpcenv: &mut dyn RpcEnvironment,
812c6f87
DM
235) -> Result<Value, Error> {
236
237 let repo_url = tools::required_string_param(&param, "repository")?;
edd3c8c6 238 let repo: BackupRepository = repo_url.parse()?;
812c6f87 239
45cdce06 240 let client = HttpClient::new(repo.host(), repo.user())?;
812c6f87 241
d0a03d40 242 let path = format!("api2/json/admin/datastore/{}/groups", repo.store());
812c6f87 243
9e391bb7 244 let mut result = client.get(&path, None).wait()?;
812c6f87 245
d0a03d40
DM
246 record_repository(&repo);
247
812c6f87 248 // fixme: implement and use output formatter instead ..
80822b95
DM
249 let list = result["data"].as_array_mut().unwrap();
250
251 list.sort_unstable_by(|a, b| {
252 let a_id = a["backup-id"].as_str().unwrap();
253 let a_backup_type = a["backup-type"].as_str().unwrap();
254 let b_id = b["backup-id"].as_str().unwrap();
255 let b_backup_type = b["backup-type"].as_str().unwrap();
256
257 let type_order = a_backup_type.cmp(b_backup_type);
258 if type_order == std::cmp::Ordering::Equal {
259 a_id.cmp(b_id)
260 } else {
261 type_order
262 }
263 });
812c6f87
DM
264
265 for item in list {
266
ad20d198
DM
267 let id = item["backup-id"].as_str().unwrap();
268 let btype = item["backup-type"].as_str().unwrap();
269 let epoch = item["last-backup"].as_i64().unwrap();
812c6f87 270 let last_backup = Local.timestamp(epoch, 0);
ad20d198 271 let backup_count = item["backup-count"].as_u64().unwrap();
812c6f87 272
1e9a94e5 273 let group = BackupGroup::new(btype, id);
812c6f87
DM
274
275 let path = group.group_path().to_str().unwrap().to_owned();
ad20d198 276
8e39232a
DM
277 let files = item["files"].as_array().unwrap().iter().map(|v| v.as_str().unwrap().to_owned()).collect();
278 let files = strip_chunked_file_expenstions(files);
ad20d198 279
80822b95 280 println!("{:20} | {} | {:5} | {}", path, last_backup.format("%c"),
ad20d198 281 backup_count, tools::join(&files, ' '));
812c6f87
DM
282 }
283
284 //Ok(result)
285 Ok(Value::Null)
286}
287
184f17af
DM
288fn list_snapshots(
289 param: Value,
290 _info: &ApiMethod,
dd5495d6 291 _rpcenv: &mut dyn RpcEnvironment,
184f17af
DM
292) -> Result<Value, Error> {
293
294 let repo_url = tools::required_string_param(&param, "repository")?;
edd3c8c6 295 let repo: BackupRepository = repo_url.parse()?;
184f17af
DM
296
297 let path = tools::required_string_param(&param, "group")?;
298 let group = BackupGroup::parse(path)?;
299
45cdce06 300 let client = HttpClient::new(repo.host(), repo.user())?;
184f17af 301
9e391bb7 302 let path = format!("api2/json/admin/datastore/{}/snapshots", repo.store());
184f17af 303
9e391bb7
DM
304 let result = client.get(&path, Some(json!({
305 "backup-type": group.backup_type(),
306 "backup-id": group.backup_id(),
307 }))).wait()?;
184f17af 308
d0a03d40
DM
309 record_repository(&repo);
310
184f17af
DM
311 // fixme: implement and use output formatter instead ..
312 let list = result["data"].as_array().unwrap();
313
314 for item in list {
315
316 let id = item["backup-id"].as_str().unwrap();
317 let btype = item["backup-type"].as_str().unwrap();
318 let epoch = item["backup-time"].as_i64().unwrap();
184f17af 319
391d3107 320 let snapshot = BackupDir::new(btype, id, epoch);
184f17af
DM
321
322 let path = snapshot.relative_path().to_str().unwrap().to_owned();
323
8e39232a
DM
324 let files = item["files"].as_array().unwrap().iter().map(|v| v.as_str().unwrap().to_owned()).collect();
325 let files = strip_chunked_file_expenstions(files);
184f17af 326
875fb1c0 327 println!("{} | {} | {}", path, snapshot.backup_time().format("%c"), tools::join(&files, ' '));
184f17af
DM
328 }
329
330 Ok(Value::Null)
331}
332
6f62c924
DM
333fn forget_snapshots(
334 param: Value,
335 _info: &ApiMethod,
dd5495d6 336 _rpcenv: &mut dyn RpcEnvironment,
6f62c924
DM
337) -> Result<Value, Error> {
338
339 let repo_url = tools::required_string_param(&param, "repository")?;
edd3c8c6 340 let repo: BackupRepository = repo_url.parse()?;
6f62c924
DM
341
342 let path = tools::required_string_param(&param, "snapshot")?;
343 let snapshot = BackupDir::parse(path)?;
344
45cdce06 345 let mut client = HttpClient::new(repo.host(), repo.user())?;
6f62c924 346
9e391bb7 347 let path = format!("api2/json/admin/datastore/{}/snapshots", repo.store());
6f62c924 348
9e391bb7
DM
349 let result = client.delete(&path, Some(json!({
350 "backup-type": snapshot.group().backup_type(),
351 "backup-id": snapshot.group().backup_id(),
352 "backup-time": snapshot.backup_time().timestamp(),
353 }))).wait()?;
6f62c924 354
d0a03d40
DM
355 record_repository(&repo);
356
6f62c924
DM
357 Ok(result)
358}
359
8cc0d6af
DM
360fn start_garbage_collection(
361 param: Value,
362 _info: &ApiMethod,
dd5495d6 363 _rpcenv: &mut dyn RpcEnvironment,
8cc0d6af
DM
364) -> Result<Value, Error> {
365
366 let repo_url = tools::required_string_param(&param, "repository")?;
edd3c8c6 367 let repo: BackupRepository = repo_url.parse()?;
8cc0d6af 368
45cdce06 369 let mut client = HttpClient::new(repo.host(), repo.user())?;
8cc0d6af 370
d0a03d40 371 let path = format!("api2/json/admin/datastore/{}/gc", repo.store());
8cc0d6af 372
5a2df000 373 let result = client.post(&path, None).wait()?;
8cc0d6af 374
d0a03d40
DM
375 record_repository(&repo);
376
8cc0d6af
DM
377 Ok(result)
378}
33d64b81 379
ae0be2dd
DM
380fn parse_backupspec(value: &str) -> Result<(&str, &str), Error> {
381
382 if let Some(caps) = BACKUPSPEC_REGEX.captures(value) {
383 return Ok((caps.get(1).unwrap().as_str(), caps.get(2).unwrap().as_str()));
384 }
385 bail!("unable to parse directory specification '{}'", value);
386}
387
6049b71f
DM
388fn create_backup(
389 param: Value,
390 _info: &ApiMethod,
dd5495d6 391 _rpcenv: &mut dyn RpcEnvironment,
6049b71f 392) -> Result<Value, Error> {
ff5d3707 393
33d64b81 394 let repo_url = tools::required_string_param(&param, "repository")?;
ae0be2dd
DM
395
396 let backupspec_list = tools::required_array_param(&param, "backupspec")?;
a914a774 397
edd3c8c6 398 let repo: BackupRepository = repo_url.parse()?;
33d64b81 399
eed6db39
DM
400 let all_file_systems = param["all-file-systems"].as_bool().unwrap_or(false);
401
219ef0e6
DM
402 let verbose = param["verbose"].as_bool().unwrap_or(false);
403
36898ffc 404 let chunk_size_opt = param["chunk-size"].as_u64().map(|v| (v*1024) as usize);
2d9d143a 405
247cdbce
DM
406 if let Some(size) = chunk_size_opt {
407 verify_chunk_size(size)?;
2d9d143a
DM
408 }
409
fba30411
DM
410 let backup_id = param["host-id"].as_str().unwrap_or(&tools::nodename());
411
ae0be2dd 412 let mut upload_list = vec![];
a914a774 413
ec8a9bb9 414 enum BackupType { PXAR, IMAGE, CONFIG };
6af905c1 415
ae0be2dd
DM
416 for backupspec in backupspec_list {
417 let (target, filename) = parse_backupspec(backupspec.as_str().unwrap())?;
bcd879cf 418
eb1804c5
DM
419 use std::os::unix::fs::FileTypeExt;
420
421 let metadata = match std::fs::metadata(filename) {
422 Ok(m) => m,
ae0be2dd
DM
423 Err(err) => bail!("unable to access '{}' - {}", filename, err),
424 };
eb1804c5 425 let file_type = metadata.file_type();
23bb8780 426
ec8a9bb9 427 let extension = Path::new(target).extension().map(|s| s.to_str().unwrap()).unwrap();
bcd879cf 428
ec8a9bb9
DM
429 match extension {
430 "pxar" => {
431 if !file_type.is_dir() {
432 bail!("got unexpected file type (expected directory)");
433 }
434 upload_list.push((BackupType::PXAR, filename.to_owned(), target.to_owned(), 0));
435 }
436 "img" => {
eb1804c5 437
ec8a9bb9
DM
438 if !(file_type.is_file() || file_type.is_block_device()) {
439 bail!("got unexpected file type (expected file or block device)");
440 }
eb1804c5 441
ec8a9bb9 442 let size = tools::image_size(&PathBuf::from(filename))?;
23bb8780 443
ec8a9bb9 444 if size == 0 { bail!("got zero-sized file '{}'", filename); }
ae0be2dd 445
ec8a9bb9
DM
446 upload_list.push((BackupType::IMAGE, filename.to_owned(), target.to_owned(), size));
447 }
448 "conf" => {
449 if !file_type.is_file() {
450 bail!("got unexpected file type (expected regular file)");
451 }
452 upload_list.push((BackupType::CONFIG, filename.to_owned(), target.to_owned(), metadata.len()));
453 }
454 _ => {
455 bail!("got unknown archive extension '{}'", extension);
456 }
ae0be2dd
DM
457 }
458 }
459
cdebd467 460 let backup_time = Local.timestamp(Local::now().timestamp(), 0);
ae0be2dd 461
c4ff3dce 462 let client = HttpClient::new(repo.host(), repo.user())?;
d0a03d40
DM
463 record_repository(&repo);
464
cdebd467
DM
465 println!("Starting backup");
466 println!("Client name: {}", tools::nodename());
467 println!("Start Time: {}", backup_time.to_rfc3339());
51144821 468
f98ac774
DM
469 let crypt_config = None;
470
39e60bd6 471 let client = client.start_backup(repo.store(), "host", &backup_id, verbose).wait()?;
c4ff3dce 472
6af905c1
DM
473 for (backup_type, filename, target, size) in upload_list {
474 match backup_type {
ec8a9bb9
DM
475 BackupType::CONFIG => {
476 println!("Upload config file '{}' to '{:?}' as {}", filename, repo, target);
477 client.upload_config(&filename, &target).wait()?;
478 }
6af905c1
DM
479 BackupType::PXAR => {
480 println!("Upload directory '{}' to '{:?}' as {}", filename, repo, target);
f98ac774
DM
481 backup_directory(
482 &client,
483 &filename,
484 &target,
485 chunk_size_opt,
486 all_file_systems,
487 verbose,
488 crypt_config.clone(),
489 )?;
6af905c1
DM
490 }
491 BackupType::IMAGE => {
492 println!("Upload image '{}' to '{:?}' as {}", filename, repo, target);
f98ac774
DM
493 backup_image(
494 &client,
495 &filename,
496 &target,
497 size,
498 chunk_size_opt,
499 verbose,
500 crypt_config.clone(),
501 )?;
6af905c1
DM
502 }
503 }
4818c8b6
DM
504 }
505
c4ff3dce
DM
506 client.finish().wait()?;
507
cdebd467 508 let end_time = Local.timestamp(Local::now().timestamp(), 0);
3ec3ec3f
DM
509 let elapsed = end_time.signed_duration_since(backup_time);
510 println!("Duration: {}", elapsed);
511
cdebd467 512 println!("End Time: {}", end_time.to_rfc3339());
3d5c11e5 513
ff5d3707 514 Ok(Value::Null)
f98ea63d
DM
515}
516
d0a03d40 517fn complete_backup_source(arg: &str, param: &HashMap<String, String>) -> Vec<String> {
f98ea63d
DM
518
519 let mut result = vec![];
520
521 let data: Vec<&str> = arg.splitn(2, ':').collect();
522
bff11030 523 if data.len() != 2 {
8968258b
DM
524 result.push(String::from("root.pxar:/"));
525 result.push(String::from("etc.pxar:/etc"));
bff11030
DM
526 return result;
527 }
f98ea63d 528
496a6784 529 let files = tools::complete_file_name(data[1], param);
f98ea63d
DM
530
531 for file in files {
532 result.push(format!("{}:{}", data[0], file));
533 }
534
535 result
ff5d3707 536}
537
9f912493
DM
538fn restore(
539 param: Value,
540 _info: &ApiMethod,
dd5495d6 541 _rpcenv: &mut dyn RpcEnvironment,
9f912493
DM
542) -> Result<Value, Error> {
543
544 let repo_url = tools::required_string_param(&param, "repository")?;
edd3c8c6 545 let repo: BackupRepository = repo_url.parse()?;
9f912493 546
d5c34d98
DM
547 let archive_name = tools::required_string_param(&param, "archive-name")?;
548
45cdce06 549 let mut client = HttpClient::new(repo.host(), repo.user())?;
d0a03d40 550
d0a03d40 551 record_repository(&repo);
d5c34d98 552
9f912493 553 let path = tools::required_string_param(&param, "snapshot")?;
9f912493 554
d5c34d98 555 let query;
9f912493 556
d5c34d98
DM
557 if path.matches('/').count() == 1 {
558 let group = BackupGroup::parse(path)?;
9f912493 559
9e391bb7
DM
560 let path = format!("api2/json/admin/datastore/{}/snapshots", repo.store());
561 let result = client.get(&path, Some(json!({
d5c34d98
DM
562 "backup-type": group.backup_type(),
563 "backup-id": group.backup_id(),
9e391bb7 564 }))).wait()?;
9f912493 565
d5c34d98
DM
566 let list = result["data"].as_array().unwrap();
567 if list.len() == 0 {
568 bail!("backup group '{}' does not contain any snapshots:", path);
569 }
9f912493 570
d5c34d98
DM
571 query = tools::json_object_to_query(json!({
572 "backup-type": group.backup_type(),
573 "backup-id": group.backup_id(),
574 "backup-time": list[0]["backup-time"].as_i64().unwrap(),
575 "archive-name": archive_name,
576 }))?;
577 } else {
578 let snapshot = BackupDir::parse(path)?;
9f912493 579
d5c34d98 580 query = tools::json_object_to_query(json!({
9f912493
DM
581 "backup-type": snapshot.group().backup_type(),
582 "backup-id": snapshot.group().backup_id(),
583 "backup-time": snapshot.backup_time().timestamp(),
d5c34d98 584 "archive-name": archive_name,
9f912493 585 }))?;
d5c34d98 586 }
9f912493 587
d5c34d98 588 let target = tools::required_string_param(&param, "target")?;
2ae7d196 589
8968258b
DM
590 if archive_name.ends_with(".pxar") {
591 let path = format!("api2/json/admin/datastore/{}/pxar?{}", repo.store(), query);
2ae7d196 592
d5c34d98
DM
593 println!("DOWNLOAD FILE {} to {}", path, target);
594
595 let target = PathBuf::from(target);
5defa71b 596 let writer = PxarDecodeWriter::new(&target, true)?;
5a2df000 597 client.download(&path, Box::new(writer)).wait()?;
d5c34d98
DM
598 } else {
599 bail!("unknown file extensions - unable to download '{}'", archive_name);
9f912493
DM
600 }
601
602 Ok(Value::Null)
603}
604
83b7db02
DM
605fn prune(
606 mut param: Value,
607 _info: &ApiMethod,
dd5495d6 608 _rpcenv: &mut dyn RpcEnvironment,
83b7db02
DM
609) -> Result<Value, Error> {
610
611 let repo_url = tools::required_string_param(&param, "repository")?;
edd3c8c6 612 let repo: BackupRepository = repo_url.parse()?;
83b7db02 613
45cdce06 614 let mut client = HttpClient::new(repo.host(), repo.user())?;
83b7db02 615
d0a03d40 616 let path = format!("api2/json/admin/datastore/{}/prune", repo.store());
83b7db02
DM
617
618 param.as_object_mut().unwrap().remove("repository");
619
5a2df000 620 let result = client.post(&path, Some(param)).wait()?;
83b7db02 621
d0a03d40
DM
622 record_repository(&repo);
623
83b7db02
DM
624 Ok(result)
625}
626
5a2df000 627// like get, but simply ignore errors and return Null instead
b2388518 628fn try_get(repo: &BackupRepository, url: &str) -> Value {
024f11bb 629
45cdce06
DM
630 let client = match HttpClient::new(repo.host(), repo.user()) {
631 Ok(v) => v,
632 _ => return Value::Null,
633 };
b2388518 634
9e391bb7 635 let mut resp = match client.get(url, None).wait() {
b2388518
DM
636 Ok(v) => v,
637 _ => return Value::Null,
638 };
639
640 if let Some(map) = resp.as_object_mut() {
641 if let Some(data) = map.remove("data") {
642 return data;
643 }
644 }
645 Value::Null
646}
647
648fn extract_repo(param: &HashMap<String, String>) -> Option<BackupRepository> {
024f11bb
DM
649
650 let repo_url = match param.get("repository") {
651 Some(v) => v,
b2388518 652 _ => return None,
024f11bb
DM
653 };
654
655 let repo: BackupRepository = match repo_url.parse() {
656 Ok(v) => v,
b2388518 657 _ => return None,
024f11bb
DM
658 };
659
b2388518
DM
660 Some(repo)
661}
024f11bb 662
b2388518 663fn complete_backup_group(_arg: &str, param: &HashMap<String, String>) -> Vec<String> {
024f11bb 664
b2388518
DM
665 let mut result = vec![];
666
667 let repo = match extract_repo(param) {
668 Some(v) => v,
024f11bb
DM
669 _ => return result,
670 };
671
b2388518
DM
672 let path = format!("api2/json/admin/datastore/{}/groups", repo.store());
673
674 let data = try_get(&repo, &path);
675
676 if let Some(list) = data.as_array() {
024f11bb 677 for item in list {
98f0b972
DM
678 if let (Some(backup_id), Some(backup_type)) =
679 (item["backup-id"].as_str(), item["backup-type"].as_str())
680 {
681 result.push(format!("{}/{}", backup_type, backup_id));
024f11bb
DM
682 }
683 }
684 }
685
686 result
687}
688
b2388518
DM
689fn complete_group_or_snapshot(arg: &str, param: &HashMap<String, String>) -> Vec<String> {
690
691 let mut result = vec![];
692
693 let repo = match extract_repo(param) {
694 Some(v) => v,
695 _ => return result,
696 };
697
698 if arg.matches('/').count() < 2 {
699 let groups = complete_backup_group(arg, param);
700 for group in groups {
701 result.push(group.to_string());
702 result.push(format!("{}/", group));
703 }
704 return result;
705 }
706
707 let mut parts = arg.split('/');
708 let query = tools::json_object_to_query(json!({
709 "backup-type": parts.next().unwrap(),
710 "backup-id": parts.next().unwrap(),
711 })).unwrap();
712
713 let path = format!("api2/json/admin/datastore/{}/snapshots?{}", repo.store(), query);
714
715 let data = try_get(&repo, &path);
716
717 if let Some(list) = data.as_array() {
718 for item in list {
719 if let (Some(backup_id), Some(backup_type), Some(backup_time)) =
720 (item["backup-id"].as_str(), item["backup-type"].as_str(), item["backup-time"].as_i64())
721 {
722 let snapshot = BackupDir::new(backup_type, backup_id, backup_time);
723 result.push(snapshot.relative_path().to_str().unwrap().to_owned());
724 }
725 }
726 }
727
728 result
729}
730
08dc340a
DM
731fn complete_archive_name(_arg: &str, param: &HashMap<String, String>) -> Vec<String> {
732
733 let mut result = vec![];
734
735 let repo = match extract_repo(param) {
736 Some(v) => v,
737 _ => return result,
738 };
739
740 let snapshot = match param.get("snapshot") {
741 Some(path) => {
742 match BackupDir::parse(path) {
743 Ok(v) => v,
744 _ => return result,
745 }
746 }
747 _ => return result,
748 };
749
750 let query = tools::json_object_to_query(json!({
751 "backup-type": snapshot.group().backup_type(),
752 "backup-id": snapshot.group().backup_id(),
753 "backup-time": snapshot.backup_time().timestamp(),
754 })).unwrap();
755
756 let path = format!("api2/json/admin/datastore/{}/files?{}", repo.store(), query);
757
758 let data = try_get(&repo, &path);
759
760 if let Some(list) = data.as_array() {
761 for item in list {
762 if let Some(filename) = item.as_str() {
763 result.push(filename.to_owned());
764 }
765 }
766 }
767
768 strip_chunked_file_expenstions(result)
769}
770
49811347
DM
771fn complete_chunk_size(_arg: &str, _param: &HashMap<String, String>) -> Vec<String> {
772
773 let mut result = vec![];
774
775 let mut size = 64;
776 loop {
777 result.push(size.to_string());
778 size = size * 2;
779 if size > 4096 { break; }
780 }
781
782 result
783}
784
826f309b 785fn get_encryption_key_password() -> Result<Vec<u8>, Error> {
ff5d3707 786
f2401311
DM
787 // fixme: implement other input methods
788
789 use std::env::VarError::*;
790 match std::env::var("PBS_ENCRYPTION_PASSWORD") {
826f309b 791 Ok(p) => return Ok(p.as_bytes().to_vec()),
f2401311
DM
792 Err(NotUnicode(_)) => bail!("PBS_ENCRYPTION_PASSWORD contains bad characters"),
793 Err(NotPresent) => {
794 // Try another method
795 }
796 }
797
798 // If we're on a TTY, query the user for a password
799 if crate::tools::tty::stdin_isatty() {
826f309b 800 return Ok(crate::tools::tty::read_password("Encryption Key Password: ")?);
f2401311
DM
801 }
802
803 bail!("no password input mechanism available");
804}
805
ac716234
DM
806fn key_create(
807 param: Value,
808 _info: &ApiMethod,
809 _rpcenv: &mut dyn RpcEnvironment,
810) -> Result<Value, Error> {
811
9b06db45
DM
812 let path = tools::required_string_param(&param, "path")?;
813 let path = PathBuf::from(path);
ac716234 814
181f097a 815 let kdf = param["kdf"].as_str().unwrap_or("scrypt");
ac716234
DM
816
817 let key = proxmox::sys::linux::random_data(32)?;
818
181f097a
DM
819 if kdf == "scrypt" {
820 // always read passphrase from tty
821 if !crate::tools::tty::stdin_isatty() {
822 bail!("unable to read passphrase - no tty");
823 }
ac716234 824
181f097a
DM
825 let password = crate::tools::tty::read_password("Encryption Key Password: ")?;
826
827 store_key_with_passphrase(&path, &key, &password, false)?;
828
829 Ok(Value::Null)
830 } else if kdf == "none" {
831 let created = Local.timestamp(Local::now().timestamp(), 0);
832
833 store_key_config(&path, false, KeyConfig {
834 kdf: None,
835 created,
836 data: key,
837 })?;
838
839 Ok(Value::Null)
840 } else {
841 unreachable!();
842 }
ac716234
DM
843}
844
ac716234
DM
845
846fn key_change_passphrase(
847 param: Value,
848 _info: &ApiMethod,
849 _rpcenv: &mut dyn RpcEnvironment,
850) -> Result<Value, Error> {
851
9b06db45
DM
852 let path = tools::required_string_param(&param, "path")?;
853 let path = PathBuf::from(path);
ac716234 854
181f097a
DM
855 let kdf = param["kdf"].as_str().unwrap_or("scrypt");
856
ac716234
DM
857 // we need a TTY to query the new password
858 if !crate::tools::tty::stdin_isatty() {
859 bail!("unable to change passphrase - no tty");
860 }
861
826f309b 862 let key = load_and_decrtypt_key(&path, get_encryption_key_password)?;
ac716234 863
181f097a 864 if kdf == "scrypt" {
ac716234 865
181f097a
DM
866 let new_pw = String::from_utf8(crate::tools::tty::read_password("New Password: ")?)?;
867 let verify_pw = String::from_utf8(crate::tools::tty::read_password("Verify Password: ")?)?;
ac716234 868
181f097a
DM
869 if new_pw != verify_pw {
870 bail!("Password verification fail!");
871 }
872
873 if new_pw.len() < 5 {
874 bail!("Password is too short!");
875 }
ac716234 876
181f097a 877 store_key_with_passphrase(&path, &key, new_pw.as_bytes(), true)?;
ac716234 878
181f097a
DM
879 Ok(Value::Null)
880 } else if kdf == "none" {
881 // fixme: keep original creation time, add modified timestamp ??
882 let created = Local.timestamp(Local::now().timestamp(), 0);
883
884 store_key_config(&path, true, KeyConfig {
885 kdf: None,
886 created,
887 data: key,
888 })?;
889
890 Ok(Value::Null)
891 } else {
892 unreachable!();
893 }
f2401311
DM
894}
895
896fn key_mgmt_cli() -> CliCommandMap {
897
181f097a
DM
898 let kdf_schema: Arc<Schema> = Arc::new(
899 StringSchema::new("Key derivation function. Choose 'none' to store the key unecrypted.")
900 .format(Arc::new(ApiStringFormat::Enum(&["scrypt", "none"])))
901 .default("scrypt")
902 .into()
903 );
904
f2401311
DM
905 // fixme: change-passphrase, import, export, list
906 let key_create_cmd_def = CliCommand::new(
907 ApiMethod::new(
908 key_create,
909 ObjectSchema::new("Create a new encryption key.")
9b06db45 910 .required("path", StringSchema::new("File system path."))
181f097a 911 .optional("kdf", kdf_schema.clone())
f2401311 912 ))
9b06db45
DM
913 .arg_param(vec!["path"])
914 .completion_cb("path", tools::complete_file_name);
f2401311 915
ac716234
DM
916 let key_change_passphrase_cmd_def = CliCommand::new(
917 ApiMethod::new(
918 key_change_passphrase,
919 ObjectSchema::new("Change the passphrase required to decrypt the key.")
9b06db45 920 .required("path", StringSchema::new("File system path."))
181f097a 921 .optional("kdf", kdf_schema.clone())
9b06db45
DM
922 ))
923 .arg_param(vec!["path"])
924 .completion_cb("path", tools::complete_file_name);
ac716234 925
f2401311 926 let cmd_def = CliCommandMap::new()
ac716234
DM
927 .insert("create".to_owned(), key_create_cmd_def.into())
928 .insert("change-passphrase".to_owned(), key_change_passphrase_cmd_def.into());
f2401311
DM
929
930 cmd_def
931}
932
933
934fn main() {
33d64b81 935
25f1650b
DM
936 let backup_source_schema: Arc<Schema> = Arc::new(
937 StringSchema::new("Backup source specification ([<label>:<path>]).")
938 .format(Arc::new(ApiStringFormat::Pattern(&BACKUPSPEC_REGEX)))
939 .into()
940 );
941
597a9203 942 let backup_cmd_def = CliCommand::new(
ff5d3707 943 ApiMethod::new(
bcd879cf 944 create_backup,
597a9203 945 ObjectSchema::new("Create (host) backup.")
f2401311 946 .required("repository", REPO_URL_SCHEMA.clone())
ae0be2dd
DM
947 .required(
948 "backupspec",
949 ArraySchema::new(
74cdb521 950 "List of backup source specifications ([<label.ext>:<path>] ...)",
25f1650b 951 backup_source_schema,
ae0be2dd
DM
952 ).min_length(1)
953 )
219ef0e6
DM
954 .optional(
955 "verbose",
956 BooleanSchema::new("Verbose output.").default(false))
fba30411
DM
957 .optional(
958 "host-id",
959 StringSchema::new("Use specified ID for the backup group name ('host/<id>'). The default is the system hostname."))
2d9d143a
DM
960 .optional(
961 "chunk-size",
962 IntegerSchema::new("Chunk size in KB. Must be a power of 2.")
963 .minimum(64)
964 .maximum(4096)
965 .default(4096)
966 )
ff5d3707 967 ))
ae0be2dd 968 .arg_param(vec!["repository", "backupspec"])
d0a03d40 969 .completion_cb("repository", complete_repository)
49811347
DM
970 .completion_cb("backupspec", complete_backup_source)
971 .completion_cb("chunk-size", complete_chunk_size);
f8838fe9 972
41c039e1
DM
973 let list_cmd_def = CliCommand::new(
974 ApiMethod::new(
812c6f87
DM
975 list_backup_groups,
976 ObjectSchema::new("List backup groups.")
f2401311 977 .required("repository", REPO_URL_SCHEMA.clone())
41c039e1 978 ))
d0a03d40
DM
979 .arg_param(vec!["repository"])
980 .completion_cb("repository", complete_repository);
41c039e1 981
184f17af
DM
982 let snapshots_cmd_def = CliCommand::new(
983 ApiMethod::new(
984 list_snapshots,
985 ObjectSchema::new("List backup snapshots.")
f2401311 986 .required("repository", REPO_URL_SCHEMA.clone())
184f17af
DM
987 .required("group", StringSchema::new("Backup group."))
988 ))
d0a03d40 989 .arg_param(vec!["repository", "group"])
024f11bb 990 .completion_cb("group", complete_backup_group)
d0a03d40 991 .completion_cb("repository", complete_repository);
184f17af 992
6f62c924
DM
993 let forget_cmd_def = CliCommand::new(
994 ApiMethod::new(
995 forget_snapshots,
996 ObjectSchema::new("Forget (remove) backup snapshots.")
f2401311 997 .required("repository", REPO_URL_SCHEMA.clone())
6f62c924
DM
998 .required("snapshot", StringSchema::new("Snapshot path."))
999 ))
d0a03d40 1000 .arg_param(vec!["repository", "snapshot"])
b2388518
DM
1001 .completion_cb("repository", complete_repository)
1002 .completion_cb("snapshot", complete_group_or_snapshot);
6f62c924 1003
8cc0d6af
DM
1004 let garbage_collect_cmd_def = CliCommand::new(
1005 ApiMethod::new(
1006 start_garbage_collection,
1007 ObjectSchema::new("Start garbage collection for a specific repository.")
f2401311 1008 .required("repository", REPO_URL_SCHEMA.clone())
8cc0d6af 1009 ))
d0a03d40
DM
1010 .arg_param(vec!["repository"])
1011 .completion_cb("repository", complete_repository);
8cc0d6af 1012
9f912493
DM
1013 let restore_cmd_def = CliCommand::new(
1014 ApiMethod::new(
1015 restore,
1016 ObjectSchema::new("Restore backup repository.")
f2401311 1017 .required("repository", REPO_URL_SCHEMA.clone())
d5c34d98
DM
1018 .required("snapshot", StringSchema::new("Group/Snapshot path."))
1019 .required("archive-name", StringSchema::new("Backup archive name."))
9f912493
DM
1020 .required("target", StringSchema::new("Target directory path."))
1021 ))
d0a03d40 1022 .arg_param(vec!["repository", "snapshot", "archive-name", "target"])
b2388518 1023 .completion_cb("repository", complete_repository)
08dc340a
DM
1024 .completion_cb("snapshot", complete_group_or_snapshot)
1025 .completion_cb("archive-name", complete_archive_name)
1026 .completion_cb("target", tools::complete_file_name);
9f912493 1027
83b7db02
DM
1028 let prune_cmd_def = CliCommand::new(
1029 ApiMethod::new(
1030 prune,
1031 proxmox_backup::api2::admin::datastore::add_common_prune_prameters(
1032 ObjectSchema::new("Prune backup repository.")
f2401311 1033 .required("repository", REPO_URL_SCHEMA.clone())
83b7db02
DM
1034 )
1035 ))
d0a03d40
DM
1036 .arg_param(vec!["repository"])
1037 .completion_cb("repository", complete_repository);
9f912493 1038
41c039e1 1039 let cmd_def = CliCommandMap::new()
597a9203 1040 .insert("backup".to_owned(), backup_cmd_def.into())
6f62c924 1041 .insert("forget".to_owned(), forget_cmd_def.into())
8cc0d6af 1042 .insert("garbage-collect".to_owned(), garbage_collect_cmd_def.into())
83b7db02 1043 .insert("list".to_owned(), list_cmd_def.into())
184f17af 1044 .insert("prune".to_owned(), prune_cmd_def.into())
9f912493 1045 .insert("restore".to_owned(), restore_cmd_def.into())
f2401311
DM
1046 .insert("snapshots".to_owned(), snapshots_cmd_def.into())
1047 .insert("key".to_owned(), key_mgmt_cli().into());
a914a774 1048
5a2df000
DM
1049 hyper::rt::run(futures::future::lazy(move || {
1050 run_cli_command(cmd_def.into());
1051 Ok(())
1052 }));
496a6784 1053
ff5d3707 1054}