]> git.proxmox.com Git - proxmox-backup.git/blame - src/api2/admin/datastore.rs
src/cli/command.rs: set exit status on error
[proxmox-backup.git] / src / api2 / admin / datastore.rs
CommitLineData
15e9b4ed 1use failure::*;
9e47c0a5 2use futures::*;
15e9b4ed 3
184f17af 4use crate::tools;
ef2f2efb 5use crate::api_schema::*;
dc9a007b 6use crate::api_schema::router::*;
2085142e 7//use crate::server::rest::*;
15e9b4ed 8use serde_json::{json, Value};
8f579717 9use std::collections::{HashSet, HashMap};
9e47c0a5 10use chrono::{DateTime, Datelike, TimeZone, Local};
38b0dfa5 11use std::path::PathBuf;
812c6f87 12use std::sync::Arc;
15e9b4ed
DM
13
14use crate::config::datastore;
15
e5064ba6 16use crate::backup::*;
0f778e06 17use crate::server::WorkerTask;
15e9b4ed 18
9e47c0a5
DM
19use hyper::{header, Body, Response, StatusCode};
20use hyper::http::request::Parts;
1629d2ad 21
8f579717
DM
22fn group_backups(backup_list: Vec<BackupInfo>) -> HashMap<String, Vec<BackupInfo>> {
23
24 let mut group_hash = HashMap::new();
25
26 for info in backup_list {
9b492eb2 27 let group_id = info.backup_dir.group().group_path().to_str().unwrap().to_owned();
8f579717
DM
28 let time_list = group_hash.entry(group_id).or_insert(vec![]);
29 time_list.push(info);
30 }
31
32 group_hash
33}
34
35fn mark_selections<F: Fn(DateTime<Local>, &BackupInfo) -> String> (
38b0dfa5 36 mark: &mut HashSet<PathBuf>,
8f579717
DM
37 list: &Vec<BackupInfo>,
38 keep: usize,
39 select_id: F,
40){
41 let mut hash = HashSet::new();
42 for info in list {
9b492eb2 43 let local_time = info.backup_dir.backup_time().with_timezone(&Local);
8f579717 44 if hash.len() >= keep as usize { break; }
38b0dfa5 45 let backup_id = info.backup_dir.relative_path();
8f579717
DM
46 let sel_id: String = select_id(local_time, &info);
47 if !hash.contains(&sel_id) {
48 hash.insert(sel_id);
49 //println!(" KEEP ID {} {}", backup_id, local_time.format("%c"));
50 mark.insert(backup_id);
51 }
52 }
53}
54
ad20d198 55fn list_groups(
812c6f87
DM
56 param: Value,
57 _info: &ApiMethod,
dd5495d6 58 _rpcenv: &mut dyn RpcEnvironment,
812c6f87
DM
59) -> Result<Value, Error> {
60
61 let store = param["store"].as_str().unwrap();
62
63 let datastore = DataStore::lookup_datastore(store)?;
64
c0977501 65 let backup_list = BackupInfo::list_backups(&datastore.base_path())?;
812c6f87
DM
66
67 let group_hash = group_backups(backup_list);
68
69 let mut groups = vec![];
70
71 for (_group_id, mut list) in group_hash {
72
2b01a225 73 BackupInfo::sort_list(&mut list, false);
812c6f87
DM
74
75 let info = &list[0];
9b492eb2 76 let group = info.backup_dir.group();
812c6f87
DM
77
78 groups.push(json!({
1e9a94e5
DM
79 "backup-type": group.backup_type(),
80 "backup-id": group.backup_id(),
9b492eb2 81 "last-backup": info.backup_dir.backup_time().timestamp(),
ad20d198
DM
82 "backup-count": list.len() as u64,
83 "files": info.files,
812c6f87
DM
84 }));
85 }
86
87 Ok(json!(groups))
88}
8f579717 89
01a13423
DM
90fn list_snapshot_files (
91 param: Value,
92 _info: &ApiMethod,
dd5495d6 93 _rpcenv: &mut dyn RpcEnvironment,
01a13423
DM
94) -> Result<Value, Error> {
95
96 let store = tools::required_string_param(&param, "store")?;
97 let backup_type = tools::required_string_param(&param, "backup-type")?;
98 let backup_id = tools::required_string_param(&param, "backup-id")?;
99 let backup_time = tools::required_integer_param(&param, "backup-time")?;
100
101 let snapshot = BackupDir::new(backup_type, backup_id, backup_time);
102
103 let datastore = DataStore::lookup_datastore(store)?;
104
c0977501
DM
105 let path = datastore.base_path();
106 let files = BackupInfo::list_files(&path, &snapshot)?;
01a13423
DM
107
108 Ok(json!(files))
109}
110
6f62c924
DM
111fn delete_snapshots (
112 param: Value,
113 _info: &ApiMethod,
dd5495d6 114 _rpcenv: &mut dyn RpcEnvironment,
6f62c924
DM
115) -> Result<Value, Error> {
116
117 let store = tools::required_string_param(&param, "store")?;
118 let backup_type = tools::required_string_param(&param, "backup-type")?;
119 let backup_id = tools::required_string_param(&param, "backup-id")?;
120 let backup_time = tools::required_integer_param(&param, "backup-time")?;
6f62c924 121
391d3107 122 let snapshot = BackupDir::new(backup_type, backup_id, backup_time);
6f62c924
DM
123
124 let datastore = DataStore::lookup_datastore(store)?;
125
126 datastore.remove_backup_dir(&snapshot)?;
127
128 Ok(Value::Null)
129}
130
184f17af
DM
131fn list_snapshots (
132 param: Value,
133 _info: &ApiMethod,
dd5495d6 134 _rpcenv: &mut dyn RpcEnvironment,
184f17af
DM
135) -> Result<Value, Error> {
136
137 let store = tools::required_string_param(&param, "store")?;
138 let backup_type = tools::required_string_param(&param, "backup-type")?;
139 let backup_id = tools::required_string_param(&param, "backup-id")?;
140
1e9a94e5 141 let group = BackupGroup::new(backup_type, backup_id);
184f17af
DM
142
143 let datastore = DataStore::lookup_datastore(store)?;
144
c0977501 145 let base_path = datastore.base_path();
184f17af 146
c0977501 147 let backup_list = group.list_backups(&base_path)?;
184f17af
DM
148
149 let mut snapshots = vec![];
150
c0977501 151 for info in backup_list {
184f17af 152 snapshots.push(json!({
1e9a94e5
DM
153 "backup-type": group.backup_type(),
154 "backup-id": group.backup_id(),
9b492eb2 155 "backup-time": info.backup_dir.backup_time().timestamp(),
184f17af
DM
156 "files": info.files,
157 }));
158 }
159
160 Ok(json!(snapshots))
161}
162
0eecf38f
DM
163fn status(
164 param: Value,
165 _info: &ApiMethod,
166 _rpcenv: &mut dyn RpcEnvironment,
167) -> Result<Value, Error> {
168
169 let store = param["store"].as_str().unwrap();
170
171 let datastore = DataStore::lookup_datastore(store)?;
172
173 let base_path = datastore.base_path();
174
175 let mut stat: libc::statfs64 = unsafe { std::mem::zeroed() };
176
177 use nix::NixPath;
178
179 let res = base_path.with_nix_path(|cstr| unsafe { libc::statfs64(cstr.as_ptr(), &mut stat) })?;
180 nix::errno::Errno::result(res)?;
181
182 let bsize = stat.f_bsize as u64;
183 Ok(json!({
184 "total": stat.f_blocks*bsize,
185 "used": (stat.f_blocks-stat.f_bfree)*bsize,
186 "avail": stat.f_bavail*bsize,
187 }))
188}
189
190fn api_method_status() -> ApiMethod {
191 ApiMethod::new(
192 status,
193 add_common_prune_prameters(
194 ObjectSchema::new("Get datastore status.")
195 .required(
196 "store",
197 StringSchema::new("Datastore name.")
198 )
199 )
200 )
201}
202
83b7db02
DM
203fn prune(
204 param: Value,
205 _info: &ApiMethod,
dd5495d6 206 _rpcenv: &mut dyn RpcEnvironment,
83b7db02
DM
207) -> Result<Value, Error> {
208
209 let store = param["store"].as_str().unwrap();
210
211 let datastore = DataStore::lookup_datastore(store)?;
212
dd8e744f 213 let mut keep_all = true;
83b7db02 214
dd8e744f
DM
215 for opt in &["keep-last", "keep-daily", "keep-weekly", "keep-weekly", "keep-yearly"] {
216 if !param[opt].is_null() {
217 keep_all = false;
218 break;
219 }
220 }
8f579717 221
dd8e744f
DM
222 let worker = WorkerTask::new("prune", Some(store.to_owned()), "root@pam", true)?;
223 let result = try_block! {
224 if keep_all {
225 worker.log("No selection - keeping all files.");
226 return Ok(());
227 } else {
228 worker.log(format!("Starting prune on store {}", store));
229 }
8f579717 230
dd8e744f 231 let backup_list = BackupInfo::list_backups(&datastore.base_path())?;
8f579717 232
dd8e744f 233 let group_hash = group_backups(backup_list);
8f579717 234
dd8e744f 235 for (_group_id, mut list) in group_hash {
8f579717 236
dd8e744f 237 let mut mark = HashSet::new();
8f579717 238
dd8e744f 239 BackupInfo::sort_list(&mut list, false);
83b7db02 240
dd8e744f
DM
241 if let Some(keep_last) = param["keep-last"].as_u64() {
242 list.iter().take(keep_last as usize).for_each(|info| {
243 mark.insert(info.backup_dir.relative_path());
244 });
245 }
8f579717 246
dd8e744f
DM
247 if let Some(keep_daily) = param["keep-daily"].as_u64() {
248 mark_selections(&mut mark, &list, keep_daily as usize, |local_time, _info| {
249 format!("{}/{}/{}", local_time.year(), local_time.month(), local_time.day())
250 });
251 }
8f579717 252
dd8e744f
DM
253 if let Some(keep_weekly) = param["keep-weekly"].as_u64() {
254 mark_selections(&mut mark, &list, keep_weekly as usize, |local_time, _info| {
255 format!("{}/{}", local_time.year(), local_time.iso_week().week())
256 });
257 }
258
259 if let Some(keep_monthly) = param["keep-monthly"].as_u64() {
260 mark_selections(&mut mark, &list, keep_monthly as usize, |local_time, _info| {
261 format!("{}/{}", local_time.year(), local_time.month())
262 });
263 }
8f579717 264
dd8e744f
DM
265 if let Some(keep_yearly) = param["keep-yearly"].as_u64() {
266 mark_selections(&mut mark, &list, keep_yearly as usize, |local_time, _info| {
267 format!("{}/{}", local_time.year(), local_time.year())
268 });
269 }
8f579717 270
dd8e744f
DM
271 let mut remove_list: Vec<BackupInfo> = list.into_iter()
272 .filter(|info| !mark.contains(&info.backup_dir.relative_path())).collect();
8f579717 273
dd8e744f
DM
274 BackupInfo::sort_list(&mut remove_list, true);
275
276 for info in remove_list {
277 worker.log(format!("remove {:?}", info.backup_dir));
278 datastore.remove_backup_dir(&info.backup_dir)?;
279 }
8f579717 280 }
dd8e744f
DM
281
282 Ok(())
283 };
284
285 worker.log_result(&result);
286
287 if let Err(err) = result {
288 bail!("prune failed - {}", err);
8f579717 289 }
83b7db02
DM
290
291 Ok(json!(null))
292}
293
294pub fn add_common_prune_prameters(schema: ObjectSchema) -> ObjectSchema {
295
296 schema
8f579717
DM
297 .optional(
298 "keep-last",
299 IntegerSchema::new("Number of backups to keep.")
300 .minimum(1)
301 )
83b7db02
DM
302 .optional(
303 "keep-daily",
8f579717
DM
304 IntegerSchema::new("Number of daily backups to keep.")
305 .minimum(1)
306 )
307 .optional(
308 "keep-weekly",
309 IntegerSchema::new("Number of weekly backups to keep.")
310 .minimum(1)
311 )
312 .optional(
313 "keep-monthly",
314 IntegerSchema::new("Number of monthly backups to keep.")
315 .minimum(1)
316 )
317 .optional(
318 "keep-yearly",
319 IntegerSchema::new("Number of yearly backups to keep.")
320 .minimum(1)
83b7db02
DM
321 )
322}
323
324fn api_method_prune() -> ApiMethod {
325 ApiMethod::new(
326 prune,
327 add_common_prune_prameters(
328 ObjectSchema::new("Prune the datastore.")
329 .required(
330 "store",
331 StringSchema::new("Datastore name.")
332 )
333 )
334 )
335}
336
6049b71f
DM
337fn start_garbage_collection(
338 param: Value,
339 _info: &ApiMethod,
dd5495d6 340 rpcenv: &mut dyn RpcEnvironment,
6049b71f 341) -> Result<Value, Error> {
15e9b4ed 342
3e6a7dee 343 let store = param["store"].as_str().unwrap().to_string();
15e9b4ed 344
3e6a7dee 345 let datastore = DataStore::lookup_datastore(&store)?;
15e9b4ed 346
5a778d92 347 println!("Starting garbage collection on store {}", store);
15e9b4ed 348
0f778e06 349 let to_stdout = if rpcenv.env_type() == RpcEnvironmentType::CLI { true } else { false };
15e9b4ed 350
0f778e06
DM
351 let upid_str = WorkerTask::new_thread(
352 "garbage_collection", Some(store.clone()), "root@pam", to_stdout, move |worker|
353 {
354 worker.log(format!("starting garbage collection on store {}", store));
d4b59ae0 355 datastore.garbage_collection(worker)
0f778e06
DM
356 })?;
357
358 Ok(json!(upid_str))
15e9b4ed
DM
359}
360
691c89a0
DM
361pub fn api_method_start_garbage_collection() -> ApiMethod {
362 ApiMethod::new(
363 start_garbage_collection,
364 ObjectSchema::new("Start garbage collection.")
5a778d92 365 .required("store", StringSchema::new("Datastore name."))
691c89a0
DM
366 )
367}
368
6049b71f
DM
369fn garbage_collection_status(
370 param: Value,
371 _info: &ApiMethod,
dd5495d6 372 _rpcenv: &mut dyn RpcEnvironment,
6049b71f 373) -> Result<Value, Error> {
691c89a0 374
5a778d92 375 let store = param["store"].as_str().unwrap();
691c89a0 376
f2b99c34
DM
377 let datastore = DataStore::lookup_datastore(&store)?;
378
5a778d92 379 println!("Garbage collection status on store {}", store);
691c89a0 380
f2b99c34 381 let status = datastore.last_gc_status();
691c89a0 382
f2b99c34 383 Ok(serde_json::to_value(&status)?)
691c89a0
DM
384}
385
386pub fn api_method_garbage_collection_status() -> ApiMethod {
387 ApiMethod::new(
388 garbage_collection_status,
389 ObjectSchema::new("Garbage collection status.")
5a778d92 390 .required("store", StringSchema::new("Datastore name."))
691c89a0
DM
391 )
392}
393
6049b71f
DM
394fn get_backup_list(
395 param: Value,
396 _info: &ApiMethod,
dd5495d6 397 _rpcenv: &mut dyn RpcEnvironment,
6049b71f 398) -> Result<Value, Error> {
83dbd80b 399
9f49fe1d 400 //let config = datastore::config()?;
83dbd80b
DM
401
402 let store = param["store"].as_str().unwrap();
403
404 let datastore = DataStore::lookup_datastore(store)?;
405
406 let mut list = vec![];
407
c0977501
DM
408 let backup_list = BackupInfo::list_backups(&datastore.base_path())?;
409
410 for info in backup_list {
83dbd80b 411 list.push(json!({
9b492eb2
DM
412 "backup-type": info.backup_dir.group().backup_type(),
413 "backup-id": info.backup_dir.group().backup_id(),
414 "backup-time": info.backup_dir.backup_time().timestamp(),
8c75372b 415 "files": info.files,
83dbd80b
DM
416 }));
417 }
418
419 let result = json!(list);
420
421 Ok(result)
422}
7e21da6e 423
6049b71f
DM
424fn get_datastore_list(
425 _param: Value,
426 _info: &ApiMethod,
dd5495d6 427 _rpcenv: &mut dyn RpcEnvironment,
6049b71f 428) -> Result<Value, Error> {
15e9b4ed
DM
429
430 let config = datastore::config()?;
431
5a778d92 432 Ok(config.convert_to_array("store"))
15e9b4ed
DM
433}
434
691c89a0 435
9e47c0a5
DM
436fn download_file(
437 _parts: Parts,
438 _req_body: Body,
439 param: Value,
440 _info: &ApiAsyncMethod,
441 _rpcenv: Box<dyn RpcEnvironment>,
442) -> Result<BoxFut, Error> {
443
444 let store = tools::required_string_param(&param, "store")?;
f14a8c9a
DM
445
446 let datastore = DataStore::lookup_datastore(store)?;
447
9e47c0a5
DM
448 let file_name = tools::required_string_param(&param, "file-name")?.to_owned();
449
450 let backup_type = tools::required_string_param(&param, "backup-type")?;
451 let backup_id = tools::required_string_param(&param, "backup-id")?;
452 let backup_time = tools::required_integer_param(&param, "backup-time")?;
453
454 println!("Download {} from {} ({}/{}/{}/{})", file_name, store,
455 backup_type, backup_id, Local.timestamp(backup_time, 0), file_name);
456
457 let backup_dir = BackupDir::new(backup_type, backup_id, backup_time);
458
f14a8c9a
DM
459 let mut path = datastore.base_path();
460 path.push(backup_dir.relative_path());
9e47c0a5
DM
461 path.push(&file_name);
462
f14a8c9a 463 let response_future = tokio::fs::File::open(path)
9e47c0a5
DM
464 .map_err(|err| http_err!(BAD_REQUEST, format!("File open failed: {}", err)))
465 .and_then(move |file| {
466 let payload = tokio::codec::FramedRead::new(file, tokio::codec::BytesCodec::new()).
fcfb84fe 467 map(|bytes| hyper::Chunk::from(bytes.freeze()));
9e47c0a5
DM
468 let body = Body::wrap_stream(payload);
469
470 // fixme: set other headers ?
471 Ok(Response::builder()
472 .status(StatusCode::OK)
473 .header(header::CONTENT_TYPE, "application/octet-stream")
474 .body(body)
475 .unwrap())
476 });
477
478 Ok(Box::new(response_future))
479}
480
481pub fn api_method_download_file() -> ApiAsyncMethod {
482 ApiAsyncMethod::new(
483 download_file,
484 ObjectSchema::new("Download single raw file from backup snapshot.")
485 .required("store", StringSchema::new("Datastore name."))
486 .required("backup-type", StringSchema::new("Backup type.")
487 .format(Arc::new(ApiStringFormat::Enum(&["ct", "host"]))))
488 .required("backup-id", StringSchema::new("Backup ID."))
489 .required("backup-time", IntegerSchema::new("Backup time (Unix epoch.)")
490 .minimum(1547797308))
491 .required("file-name", StringSchema::new("Raw file name."))
492 )
493}
494
15e9b4ed
DM
495pub fn router() -> Router {
496
812c6f87
DM
497 let store_schema: Arc<Schema> = Arc::new(
498 StringSchema::new("Datastore name.").into()
499 );
500
15e9b4ed 501 let datastore_info = Router::new()
83dbd80b
DM
502 .subdir(
503 "backups",
504 Router::new()
505 .get(ApiMethod::new(
506 get_backup_list,
507 ObjectSchema::new("List backups.")
812c6f87 508 .required("store", store_schema.clone()))))
264f52cf 509 .subdir(
9e47c0a5 510 "download",
264f52cf 511 Router::new()
9e47c0a5 512 .download(api_method_download_file())
da7d6721 513 )
15e9b4ed
DM
514 .subdir(
515 "gc",
516 Router::new()
691c89a0 517 .get(api_method_garbage_collection_status())
83b7db02 518 .post(api_method_start_garbage_collection()))
01a13423
DM
519 .subdir(
520 "files",
521 Router::new()
522 .get(
523 ApiMethod::new(
524 list_snapshot_files,
525 ObjectSchema::new("List snapshot files.")
526 .required("store", store_schema.clone())
527 .required("backup-type", StringSchema::new("Backup type."))
528 .required("backup-id", StringSchema::new("Backup ID."))
529 .required("backup-time", IntegerSchema::new("Backup time (Unix epoch.)")
530 .minimum(1547797308))
531 )
532 )
533 )
812c6f87
DM
534 .subdir(
535 "groups",
536 Router::new()
537 .get(ApiMethod::new(
ad20d198 538 list_groups,
812c6f87
DM
539 ObjectSchema::new("List backup groups.")
540 .required("store", store_schema.clone()))))
184f17af
DM
541 .subdir(
542 "snapshots",
543 Router::new()
6f62c924
DM
544 .get(
545 ApiMethod::new(
546 list_snapshots,
547 ObjectSchema::new("List backup groups.")
548 .required("store", store_schema.clone())
549 .required("backup-type", StringSchema::new("Backup type."))
550 .required("backup-id", StringSchema::new("Backup ID."))
551 )
552 )
553 .delete(
554 ApiMethod::new(
555 delete_snapshots,
556 ObjectSchema::new("Delete backup snapshot.")
557 .required("store", store_schema.clone())
558 .required("backup-type", StringSchema::new("Backup type."))
559 .required("backup-id", StringSchema::new("Backup ID."))
560 .required("backup-time", IntegerSchema::new("Backup time (Unix epoch.)")
561 .minimum(1547797308))
562 )
563 )
564 )
83b7db02
DM
565 .subdir(
566 "prune",
567 Router::new()
13f1cc17
DM
568 .post(api_method_prune())
569 )
0eecf38f
DM
570 .subdir(
571 "status",
572 Router::new()
573 .get(api_method_status())
574 )
13f1cc17 575 .list_subdirs();
7e21da6e 576
15e9b4ed
DM
577
578
579 let route = Router::new()
580 .get(ApiMethod::new(
581 get_datastore_list,
582 ObjectSchema::new("Directory index.")))
5a778d92 583 .match_all("store", datastore_info);
15e9b4ed
DM
584
585
586
587 route
588}