]> git.proxmox.com Git - proxmox-backup.git/blame - src/api2/admin/datastore.rs
src/client/backup_specification.rs: split code into extra file
[proxmox-backup.git] / src / api2 / admin / datastore.rs
CommitLineData
cad540e9 1use std::collections::{HashSet, HashMap};
09b1f7b2 2use std::convert::TryFrom;
cad540e9 3
aeeac29b 4use chrono::{TimeZone, Local};
f7d4e4b5 5use anyhow::{bail, Error};
9e47c0a5 6use futures::*;
cad540e9
WB
7use hyper::http::request::Parts;
8use hyper::{header, Body, Response, StatusCode};
15e9b4ed
DM
9use serde_json::{json, Value};
10
bb34b589
DM
11use proxmox::api::{
12 api, ApiResponseFuture, ApiHandler, ApiMethod, Router,
54552dda 13 RpcEnvironment, RpcEnvironmentType, Permission, UserInformation};
cad540e9
WB
14use proxmox::api::router::SubdirMap;
15use proxmox::api::schema::*;
feaa1ad3 16use proxmox::tools::fs::{file_get_contents, replace_file, CreateOptions};
9ea4bce4
WB
17use proxmox::try_block;
18use proxmox::{http_err, identity, list_subdirs_api_method, sortable};
e18a6c9e 19
cad540e9 20use crate::api2::types::*;
e5064ba6 21use crate::backup::*;
cad540e9 22use crate::config::datastore;
54552dda
DM
23use crate::config::cached_user_info::CachedUserInfo;
24
0f778e06 25use crate::server::WorkerTask;
cad540e9 26use crate::tools;
d00e1a21
DM
27use crate::config::acl::{
28 PRIV_DATASTORE_AUDIT,
54552dda 29 PRIV_DATASTORE_MODIFY,
d00e1a21
DM
30 PRIV_DATASTORE_READ,
31 PRIV_DATASTORE_PRUNE,
54552dda 32 PRIV_DATASTORE_BACKUP,
d00e1a21 33};
1629d2ad 34
54552dda
DM
35fn check_backup_owner(store: &DataStore, group: &BackupGroup, userid: &str) -> Result<(), Error> {
36 let owner = store.get_owner(group)?;
37 if &owner != userid {
38 bail!("backup owner check failed ({} != {})", userid, owner);
39 }
40 Ok(())
41}
42
09b1f7b2 43fn read_backup_index(store: &DataStore, backup_dir: &BackupDir) -> Result<Vec<BackupContent>, Error> {
8c70e3eb
DM
44
45 let mut path = store.base_path();
46 path.push(backup_dir.relative_path());
47 path.push("index.json.blob");
48
49 let raw_data = file_get_contents(&path)?;
09b1f7b2
DM
50 let index_size = raw_data.len() as u64;
51 let blob = DataBlob::from_raw(raw_data)?;
8c70e3eb 52
09b1f7b2 53 let manifest = BackupManifest::try_from(blob)?;
8c70e3eb 54
09b1f7b2
DM
55 let mut result = Vec::new();
56 for item in manifest.files() {
57 result.push(BackupContent {
58 filename: item.filename.clone(),
59 size: Some(item.size),
60 });
8c70e3eb
DM
61 }
62
09b1f7b2
DM
63 result.push(BackupContent {
64 filename: "index.json.blob".to_string(),
65 size: Some(index_size),
66 });
4f1e40a2 67
8c70e3eb
DM
68 Ok(result)
69}
70
8f579717
DM
71fn group_backups(backup_list: Vec<BackupInfo>) -> HashMap<String, Vec<BackupInfo>> {
72
73 let mut group_hash = HashMap::new();
74
75 for info in backup_list {
9b492eb2 76 let group_id = info.backup_dir.group().group_path().to_str().unwrap().to_owned();
8f579717
DM
77 let time_list = group_hash.entry(group_id).or_insert(vec![]);
78 time_list.push(info);
79 }
80
81 group_hash
82}
83
b31c8019
DM
84#[api(
85 input: {
86 properties: {
87 store: {
88 schema: DATASTORE_SCHEMA,
89 },
90 },
91 },
92 returns: {
93 type: Array,
94 description: "Returns the list of backup groups.",
95 items: {
96 type: GroupListItem,
97 }
98 },
bb34b589 99 access: {
54552dda
DM
100 permission: &Permission::Privilege(
101 &["datastore", "{store}"],
102 PRIV_DATASTORE_AUDIT | PRIV_DATASTORE_BACKUP,
103 true),
bb34b589 104 },
b31c8019
DM
105)]
106/// List backup groups.
ad20d198 107fn list_groups(
b31c8019 108 store: String,
54552dda 109 rpcenv: &mut dyn RpcEnvironment,
b31c8019 110) -> Result<Vec<GroupListItem>, Error> {
812c6f87 111
54552dda
DM
112 let username = rpcenv.get_user().unwrap();
113 let user_info = CachedUserInfo::new()?;
114 let user_privs = user_info.lookup_privs(&username, &["datastore", &store]);
115
b31c8019 116 let datastore = DataStore::lookup_datastore(&store)?;
812c6f87 117
c0977501 118 let backup_list = BackupInfo::list_backups(&datastore.base_path())?;
812c6f87
DM
119
120 let group_hash = group_backups(backup_list);
121
b31c8019 122 let mut groups = Vec::new();
812c6f87
DM
123
124 for (_group_id, mut list) in group_hash {
125
2b01a225 126 BackupInfo::sort_list(&mut list, false);
812c6f87
DM
127
128 let info = &list[0];
54552dda 129
9b492eb2 130 let group = info.backup_dir.group();
812c6f87 131
54552dda 132 let list_all = (user_privs & PRIV_DATASTORE_AUDIT) != 0;
04b0ca8b 133 let owner = datastore.get_owner(group)?;
54552dda 134 if !list_all {
54552dda
DM
135 if owner != username { continue; }
136 }
137
b31c8019
DM
138 let result_item = GroupListItem {
139 backup_type: group.backup_type().to_string(),
140 backup_id: group.backup_id().to_string(),
141 last_backup: info.backup_dir.backup_time().timestamp(),
142 backup_count: list.len() as u64,
143 files: info.files.clone(),
04b0ca8b 144 owner: Some(owner),
b31c8019
DM
145 };
146 groups.push(result_item);
812c6f87
DM
147 }
148
b31c8019 149 Ok(groups)
812c6f87 150}
8f579717 151
09b1f7b2
DM
152#[api(
153 input: {
154 properties: {
155 store: {
156 schema: DATASTORE_SCHEMA,
157 },
158 "backup-type": {
159 schema: BACKUP_TYPE_SCHEMA,
160 },
161 "backup-id": {
162 schema: BACKUP_ID_SCHEMA,
163 },
164 "backup-time": {
165 schema: BACKUP_TIME_SCHEMA,
166 },
167 },
168 },
169 returns: {
170 type: Array,
171 description: "Returns the list of archive files inside a backup snapshots.",
172 items: {
173 type: BackupContent,
174 }
175 },
bb34b589 176 access: {
54552dda
DM
177 permission: &Permission::Privilege(
178 &["datastore", "{store}"],
179 PRIV_DATASTORE_AUDIT | PRIV_DATASTORE_READ | PRIV_DATASTORE_BACKUP,
180 true),
bb34b589 181 },
09b1f7b2
DM
182)]
183/// List snapshot files.
ea5f547f 184pub fn list_snapshot_files(
09b1f7b2
DM
185 store: String,
186 backup_type: String,
187 backup_id: String,
188 backup_time: i64,
01a13423 189 _info: &ApiMethod,
54552dda 190 rpcenv: &mut dyn RpcEnvironment,
09b1f7b2 191) -> Result<Vec<BackupContent>, Error> {
01a13423 192
54552dda
DM
193 let username = rpcenv.get_user().unwrap();
194 let user_info = CachedUserInfo::new()?;
195 let user_privs = user_info.lookup_privs(&username, &["datastore", &store]);
196
09b1f7b2 197 let datastore = DataStore::lookup_datastore(&store)?;
54552dda 198
01a13423
DM
199 let snapshot = BackupDir::new(backup_type, backup_id, backup_time);
200
54552dda
DM
201 let allowed = (user_privs & (PRIV_DATASTORE_AUDIT | PRIV_DATASTORE_READ)) != 0;
202 if !allowed { check_backup_owner(&datastore, snapshot.group(), &username)?; }
203
d7c24397
DM
204 let mut files = read_backup_index(&datastore, &snapshot)?;
205
206 let info = BackupInfo::new(&datastore.base_path(), snapshot)?;
01a13423 207
09b1f7b2
DM
208 let file_set = files.iter().fold(HashSet::new(), |mut acc, item| {
209 acc.insert(item.filename.clone());
d7c24397
DM
210 acc
211 });
212
213 for file in info.files {
214 if file_set.contains(&file) { continue; }
09b1f7b2 215 files.push(BackupContent { filename: file, size: None });
d7c24397 216 }
01a13423 217
8c70e3eb 218 Ok(files)
01a13423
DM
219}
220
68a6a0ee
DM
221#[api(
222 input: {
223 properties: {
224 store: {
225 schema: DATASTORE_SCHEMA,
226 },
227 "backup-type": {
228 schema: BACKUP_TYPE_SCHEMA,
229 },
230 "backup-id": {
231 schema: BACKUP_ID_SCHEMA,
232 },
233 "backup-time": {
234 schema: BACKUP_TIME_SCHEMA,
235 },
236 },
237 },
bb34b589 238 access: {
54552dda
DM
239 permission: &Permission::Privilege(
240 &["datastore", "{store}"],
241 PRIV_DATASTORE_MODIFY| PRIV_DATASTORE_PRUNE,
242 true),
bb34b589 243 },
68a6a0ee
DM
244)]
245/// Delete backup snapshot.
246fn delete_snapshot(
247 store: String,
248 backup_type: String,
249 backup_id: String,
250 backup_time: i64,
6f62c924 251 _info: &ApiMethod,
54552dda 252 rpcenv: &mut dyn RpcEnvironment,
6f62c924
DM
253) -> Result<Value, Error> {
254
54552dda
DM
255 let username = rpcenv.get_user().unwrap();
256 let user_info = CachedUserInfo::new()?;
257 let user_privs = user_info.lookup_privs(&username, &["datastore", &store]);
258
391d3107 259 let snapshot = BackupDir::new(backup_type, backup_id, backup_time);
6f62c924 260
68a6a0ee 261 let datastore = DataStore::lookup_datastore(&store)?;
6f62c924 262
54552dda
DM
263 let allowed = (user_privs & PRIV_DATASTORE_MODIFY) != 0;
264 if !allowed { check_backup_owner(&datastore, snapshot.group(), &username)?; }
265
6f62c924
DM
266 datastore.remove_backup_dir(&snapshot)?;
267
268 Ok(Value::Null)
269}
270
fc189b19
DM
271#[api(
272 input: {
273 properties: {
274 store: {
275 schema: DATASTORE_SCHEMA,
276 },
277 "backup-type": {
278 optional: true,
279 schema: BACKUP_TYPE_SCHEMA,
280 },
281 "backup-id": {
282 optional: true,
283 schema: BACKUP_ID_SCHEMA,
284 },
285 },
286 },
287 returns: {
288 type: Array,
289 description: "Returns the list of snapshots.",
290 items: {
291 type: SnapshotListItem,
292 }
293 },
bb34b589 294 access: {
54552dda
DM
295 permission: &Permission::Privilege(
296 &["datastore", "{store}"],
297 PRIV_DATASTORE_AUDIT | PRIV_DATASTORE_BACKUP,
298 true),
bb34b589 299 },
fc189b19
DM
300)]
301/// List backup snapshots.
f24fc116 302pub fn list_snapshots (
54552dda
DM
303 store: String,
304 backup_type: Option<String>,
305 backup_id: Option<String>,
306 _param: Value,
184f17af 307 _info: &ApiMethod,
54552dda 308 rpcenv: &mut dyn RpcEnvironment,
fc189b19 309) -> Result<Vec<SnapshotListItem>, Error> {
184f17af 310
54552dda
DM
311 let username = rpcenv.get_user().unwrap();
312 let user_info = CachedUserInfo::new()?;
313 let user_privs = user_info.lookup_privs(&username, &["datastore", &store]);
184f17af 314
54552dda 315 let datastore = DataStore::lookup_datastore(&store)?;
184f17af 316
c0977501 317 let base_path = datastore.base_path();
184f17af 318
15c847f1 319 let backup_list = BackupInfo::list_backups(&base_path)?;
184f17af
DM
320
321 let mut snapshots = vec![];
322
c0977501 323 for info in backup_list {
15c847f1 324 let group = info.backup_dir.group();
54552dda 325 if let Some(ref backup_type) = backup_type {
15c847f1
DM
326 if backup_type != group.backup_type() { continue; }
327 }
54552dda 328 if let Some(ref backup_id) = backup_id {
15c847f1
DM
329 if backup_id != group.backup_id() { continue; }
330 }
a17a0e7a 331
54552dda 332 let list_all = (user_privs & PRIV_DATASTORE_AUDIT) != 0;
04b0ca8b
DC
333 let owner = datastore.get_owner(group)?;
334
54552dda 335 if !list_all {
54552dda
DM
336 if owner != username { continue; }
337 }
338
fc189b19
DM
339 let mut result_item = SnapshotListItem {
340 backup_type: group.backup_type().to_string(),
341 backup_id: group.backup_id().to_string(),
342 backup_time: info.backup_dir.backup_time().timestamp(),
343 files: info.files,
344 size: None,
04b0ca8b 345 owner: Some(owner),
fc189b19 346 };
a17a0e7a
DM
347
348 if let Ok(index) = read_backup_index(&datastore, &info.backup_dir) {
349 let mut backup_size = 0;
09b1f7b2
DM
350 for item in index.iter() {
351 if let Some(item_size) = item.size {
a17a0e7a
DM
352 backup_size += item_size;
353 }
354 }
fc189b19 355 result_item.size = Some(backup_size);
a17a0e7a
DM
356 }
357
358 snapshots.push(result_item);
184f17af
DM
359 }
360
fc189b19 361 Ok(snapshots)
184f17af
DM
362}
363
1dc117bb
DM
364#[api(
365 input: {
366 properties: {
367 store: {
368 schema: DATASTORE_SCHEMA,
369 },
370 },
371 },
372 returns: {
373 type: StorageStatus,
374 },
bb34b589 375 access: {
54552dda 376 permission: &Permission::Privilege(&["datastore", "{store}"], PRIV_DATASTORE_AUDIT | PRIV_DATASTORE_BACKUP, true),
bb34b589 377 },
1dc117bb
DM
378)]
379/// Get datastore status.
ea5f547f 380pub fn status(
1dc117bb 381 store: String,
0eecf38f
DM
382 _info: &ApiMethod,
383 _rpcenv: &mut dyn RpcEnvironment,
1dc117bb 384) -> Result<StorageStatus, Error> {
0eecf38f 385
1dc117bb 386 let datastore = DataStore::lookup_datastore(&store)?;
0eecf38f
DM
387
388 let base_path = datastore.base_path();
389
390 let mut stat: libc::statfs64 = unsafe { std::mem::zeroed() };
391
392 use nix::NixPath;
393
394 let res = base_path.with_nix_path(|cstr| unsafe { libc::statfs64(cstr.as_ptr(), &mut stat) })?;
395 nix::errno::Errno::result(res)?;
396
397 let bsize = stat.f_bsize as u64;
1dc117bb
DM
398
399 Ok(StorageStatus {
400 total: stat.f_blocks*bsize,
401 used: (stat.f_blocks-stat.f_bfree)*bsize,
402 avail: stat.f_bavail*bsize,
403 })
0eecf38f
DM
404}
405
255f378a
DM
406#[macro_export]
407macro_rules! add_common_prune_prameters {
552c2259
DM
408 ( [ $( $list1:tt )* ] ) => {
409 add_common_prune_prameters!([$( $list1 )* ] , [])
410 };
411 ( [ $( $list1:tt )* ] , [ $( $list2:tt )* ] ) => {
255f378a 412 [
552c2259 413 $( $list1 )*
255f378a 414 (
552c2259 415 "keep-daily",
255f378a 416 true,
49ff1092 417 &PRUNE_SCHEMA_KEEP_DAILY,
255f378a 418 ),
102d8d41
DM
419 (
420 "keep-hourly",
421 true,
49ff1092 422 &PRUNE_SCHEMA_KEEP_HOURLY,
102d8d41 423 ),
255f378a 424 (
552c2259 425 "keep-last",
255f378a 426 true,
49ff1092 427 &PRUNE_SCHEMA_KEEP_LAST,
255f378a
DM
428 ),
429 (
552c2259 430 "keep-monthly",
255f378a 431 true,
49ff1092 432 &PRUNE_SCHEMA_KEEP_MONTHLY,
255f378a
DM
433 ),
434 (
552c2259 435 "keep-weekly",
255f378a 436 true,
49ff1092 437 &PRUNE_SCHEMA_KEEP_WEEKLY,
255f378a
DM
438 ),
439 (
440 "keep-yearly",
441 true,
49ff1092 442 &PRUNE_SCHEMA_KEEP_YEARLY,
255f378a 443 ),
552c2259 444 $( $list2 )*
255f378a
DM
445 ]
446 }
0eecf38f
DM
447}
448
db1e061d
DM
449pub const API_RETURN_SCHEMA_PRUNE: Schema = ArraySchema::new(
450 "Returns the list of snapshots and a flag indicating if there are kept or removed.",
451 PruneListItem::API_SCHEMA
452).schema();
453
0ab08ac9
DM
454const API_METHOD_PRUNE: ApiMethod = ApiMethod::new(
455 &ApiHandler::Sync(&prune),
255f378a 456 &ObjectSchema::new(
0ab08ac9
DM
457 "Prune the datastore.",
458 &add_common_prune_prameters!([
459 ("backup-id", false, &BACKUP_ID_SCHEMA),
460 ("backup-type", false, &BACKUP_TYPE_SCHEMA),
3b03abfe
DM
461 ("dry-run", true, &BooleanSchema::new(
462 "Just show what prune would do, but do not delete anything.")
463 .schema()
464 ),
0ab08ac9 465 ],[
66c49c21 466 ("store", false, &DATASTORE_SCHEMA),
0ab08ac9 467 ])
db1e061d
DM
468 ))
469 .returns(&API_RETURN_SCHEMA_PRUNE)
470 .access(None, &Permission::Privilege(
54552dda
DM
471 &["datastore", "{store}"],
472 PRIV_DATASTORE_MODIFY | PRIV_DATASTORE_PRUNE,
473 true)
474);
255f378a 475
83b7db02
DM
476fn prune(
477 param: Value,
478 _info: &ApiMethod,
54552dda 479 rpcenv: &mut dyn RpcEnvironment,
83b7db02
DM
480) -> Result<Value, Error> {
481
54552dda 482 let store = tools::required_string_param(&param, "store")?;
9fdc3ef4
DM
483 let backup_type = tools::required_string_param(&param, "backup-type")?;
484 let backup_id = tools::required_string_param(&param, "backup-id")?;
485
54552dda
DM
486 let username = rpcenv.get_user().unwrap();
487 let user_info = CachedUserInfo::new()?;
488 let user_privs = user_info.lookup_privs(&username, &["datastore", &store]);
489
3b03abfe
DM
490 let dry_run = param["dry-run"].as_bool().unwrap_or(false);
491
9fdc3ef4
DM
492 let group = BackupGroup::new(backup_type, backup_id);
493
54552dda
DM
494 let datastore = DataStore::lookup_datastore(&store)?;
495
496 let allowed = (user_privs & PRIV_DATASTORE_MODIFY) != 0;
497 if !allowed { check_backup_owner(&datastore, &group, &username)?; }
83b7db02 498
9e3f0088
DM
499 let prune_options = PruneOptions {
500 keep_last: param["keep-last"].as_u64(),
102d8d41 501 keep_hourly: param["keep-hourly"].as_u64(),
9e3f0088
DM
502 keep_daily: param["keep-daily"].as_u64(),
503 keep_weekly: param["keep-weekly"].as_u64(),
504 keep_monthly: param["keep-monthly"].as_u64(),
505 keep_yearly: param["keep-yearly"].as_u64(),
506 };
8f579717 507
503995c7
DM
508 let worker_id = format!("{}_{}_{}", store, backup_type, backup_id);
509
dda70154
DM
510 let mut prune_result = Vec::new();
511
512 let list = group.list_backups(&datastore.base_path())?;
513
514 let mut prune_info = compute_prune_info(list, &prune_options)?;
515
516 prune_info.reverse(); // delete older snapshots first
517
518 let keep_all = !prune_options.keeps_something();
519
520 if dry_run {
521 for (info, mut keep) in prune_info {
522 if keep_all { keep = true; }
523
524 let backup_time = info.backup_dir.backup_time();
525 let group = info.backup_dir.group();
526
527 prune_result.push(json!({
528 "backup-type": group.backup_type(),
529 "backup-id": group.backup_id(),
530 "backup-time": backup_time.timestamp(),
531 "keep": keep,
532 }));
533 }
534 return Ok(json!(prune_result));
535 }
536
537
163e9bbe 538 // We use a WorkerTask just to have a task log, but run synchrounously
503995c7 539 let worker = WorkerTask::new("prune", Some(worker_id), "root@pam", true)?;
dda70154 540
dd8e744f 541 let result = try_block! {
dda70154 542 if keep_all {
9fdc3ef4 543 worker.log("No prune selection - keeping all files.");
dd8e744f 544 } else {
236a396a 545 worker.log(format!("retention options: {}", prune_options.cli_options_string()));
dda70154
DM
546 worker.log(format!("Starting prune on store \"{}\" group \"{}/{}\"",
547 store, backup_type, backup_id));
dd8e744f 548 }
8f579717 549
dda70154
DM
550 for (info, mut keep) in prune_info {
551 if keep_all { keep = true; }
dd8e744f 552
3b03abfe
DM
553 let backup_time = info.backup_dir.backup_time();
554 let timestamp = BackupDir::backup_time_to_string(backup_time);
555 let group = info.backup_dir.group();
556
dda70154 557
3b03abfe
DM
558 let msg = format!(
559 "{}/{}/{} {}",
560 group.backup_type(),
561 group.backup_id(),
562 timestamp,
563 if keep { "keep" } else { "remove" },
564 );
565
566 worker.log(msg);
567
dda70154
DM
568 prune_result.push(json!({
569 "backup-type": group.backup_type(),
570 "backup-id": group.backup_id(),
571 "backup-time": backup_time.timestamp(),
572 "keep": keep,
573 }));
574
3b03abfe 575 if !(dry_run || keep) {
8f0b4c1f
DM
576 datastore.remove_backup_dir(&info.backup_dir)?;
577 }
8f579717 578 }
dd8e744f
DM
579
580 Ok(())
581 };
582
583 worker.log_result(&result);
584
585 if let Err(err) = result {
586 bail!("prune failed - {}", err);
dda70154 587 };
83b7db02 588
dda70154 589 Ok(json!(prune_result))
83b7db02
DM
590}
591
dfc58d47
DM
592#[api(
593 input: {
594 properties: {
595 store: {
596 schema: DATASTORE_SCHEMA,
597 },
598 },
599 },
600 returns: {
601 schema: UPID_SCHEMA,
602 },
bb34b589 603 access: {
54552dda 604 permission: &Permission::Privilege(&["datastore", "{store}"], PRIV_DATASTORE_MODIFY, false),
bb34b589 605 },
dfc58d47
DM
606)]
607/// Start garbage collection.
6049b71f 608fn start_garbage_collection(
dfc58d47 609 store: String,
6049b71f 610 _info: &ApiMethod,
dd5495d6 611 rpcenv: &mut dyn RpcEnvironment,
6049b71f 612) -> Result<Value, Error> {
15e9b4ed 613
3e6a7dee 614 let datastore = DataStore::lookup_datastore(&store)?;
15e9b4ed 615
5a778d92 616 println!("Starting garbage collection on store {}", store);
15e9b4ed 617
0f778e06 618 let to_stdout = if rpcenv.env_type() == RpcEnvironmentType::CLI { true } else { false };
15e9b4ed 619
0f778e06
DM
620 let upid_str = WorkerTask::new_thread(
621 "garbage_collection", Some(store.clone()), "root@pam", to_stdout, move |worker|
622 {
623 worker.log(format!("starting garbage collection on store {}", store));
99641a6b 624 datastore.garbage_collection(&worker)
0f778e06
DM
625 })?;
626
627 Ok(json!(upid_str))
15e9b4ed
DM
628}
629
a92830dc
DM
630#[api(
631 input: {
632 properties: {
633 store: {
634 schema: DATASTORE_SCHEMA,
635 },
636 },
637 },
638 returns: {
639 type: GarbageCollectionStatus,
bb34b589
DM
640 },
641 access: {
642 permission: &Permission::Privilege(&["datastore", "{store}"], PRIV_DATASTORE_AUDIT, false),
643 },
a92830dc
DM
644)]
645/// Garbage collection status.
5eeea607 646pub fn garbage_collection_status(
a92830dc 647 store: String,
6049b71f 648 _info: &ApiMethod,
dd5495d6 649 _rpcenv: &mut dyn RpcEnvironment,
a92830dc 650) -> Result<GarbageCollectionStatus, Error> {
691c89a0 651
f2b99c34
DM
652 let datastore = DataStore::lookup_datastore(&store)?;
653
f2b99c34 654 let status = datastore.last_gc_status();
691c89a0 655
a92830dc 656 Ok(status)
691c89a0
DM
657}
658
bb34b589 659#[api(
30fb6025
DM
660 returns: {
661 description: "List the accessible datastores.",
662 type: Array,
663 items: {
664 description: "Datastore name and description.",
665 properties: {
666 store: {
667 schema: DATASTORE_SCHEMA,
668 },
669 comment: {
670 optional: true,
671 schema: SINGLE_LINE_COMMENT_SCHEMA,
672 },
673 },
674 },
675 },
bb34b589 676 access: {
54552dda 677 permission: &Permission::Anybody,
bb34b589
DM
678 },
679)]
680/// Datastore list
6049b71f
DM
681fn get_datastore_list(
682 _param: Value,
683 _info: &ApiMethod,
54552dda 684 rpcenv: &mut dyn RpcEnvironment,
6049b71f 685) -> Result<Value, Error> {
15e9b4ed 686
d0187a51 687 let (config, _digest) = datastore::config()?;
15e9b4ed 688
54552dda
DM
689 let username = rpcenv.get_user().unwrap();
690 let user_info = CachedUserInfo::new()?;
691
30fb6025 692 let mut list = Vec::new();
54552dda 693
30fb6025 694 for (store, (_, data)) in &config.sections {
54552dda
DM
695 let user_privs = user_info.lookup_privs(&username, &["datastore", &store]);
696 let allowed = (user_privs & (PRIV_DATASTORE_AUDIT| PRIV_DATASTORE_BACKUP)) != 0;
30fb6025
DM
697 if allowed {
698 let mut entry = json!({ "store": store });
699 if let Some(comment) = data["comment"].as_str() {
700 entry["comment"] = comment.into();
701 }
702 list.push(entry);
703 }
54552dda
DM
704 }
705
30fb6025 706 Ok(list.into())
15e9b4ed
DM
707}
708
0ab08ac9
DM
709#[sortable]
710pub const API_METHOD_DOWNLOAD_FILE: ApiMethod = ApiMethod::new(
711 &ApiHandler::AsyncHttp(&download_file),
712 &ObjectSchema::new(
713 "Download single raw file from backup snapshot.",
714 &sorted!([
66c49c21 715 ("store", false, &DATASTORE_SCHEMA),
0ab08ac9
DM
716 ("backup-type", false, &BACKUP_TYPE_SCHEMA),
717 ("backup-id", false, &BACKUP_ID_SCHEMA),
718 ("backup-time", false, &BACKUP_TIME_SCHEMA),
4191018c 719 ("file-name", false, &BACKUP_ARCHIVE_NAME_SCHEMA),
0ab08ac9
DM
720 ]),
721 )
54552dda
DM
722).access(None, &Permission::Privilege(
723 &["datastore", "{store}"],
724 PRIV_DATASTORE_READ | PRIV_DATASTORE_BACKUP,
725 true)
726);
691c89a0 727
9e47c0a5
DM
728fn download_file(
729 _parts: Parts,
730 _req_body: Body,
731 param: Value,
255f378a 732 _info: &ApiMethod,
54552dda 733 rpcenv: Box<dyn RpcEnvironment>,
bb084b9c 734) -> ApiResponseFuture {
9e47c0a5 735
ad51d02a
DM
736 async move {
737 let store = tools::required_string_param(&param, "store")?;
ad51d02a 738 let datastore = DataStore::lookup_datastore(store)?;
f14a8c9a 739
54552dda
DM
740 let username = rpcenv.get_user().unwrap();
741 let user_info = CachedUserInfo::new()?;
742 let user_privs = user_info.lookup_privs(&username, &["datastore", &store]);
743
ad51d02a 744 let file_name = tools::required_string_param(&param, "file-name")?.to_owned();
9e47c0a5 745
ad51d02a
DM
746 let backup_type = tools::required_string_param(&param, "backup-type")?;
747 let backup_id = tools::required_string_param(&param, "backup-id")?;
748 let backup_time = tools::required_integer_param(&param, "backup-time")?;
9e47c0a5 749
54552dda
DM
750 let backup_dir = BackupDir::new(backup_type, backup_id, backup_time);
751
752 let allowed = (user_privs & PRIV_DATASTORE_READ) != 0;
753 if !allowed { check_backup_owner(&datastore, backup_dir.group(), &username)?; }
754
ad51d02a
DM
755 println!("Download {} from {} ({}/{}/{}/{})", file_name, store,
756 backup_type, backup_id, Local.timestamp(backup_time, 0), file_name);
9e47c0a5 757
ad51d02a
DM
758 let mut path = datastore.base_path();
759 path.push(backup_dir.relative_path());
760 path.push(&file_name);
761
762 let file = tokio::fs::File::open(path)
763 .map_err(|err| http_err!(BAD_REQUEST, format!("File open failed: {}", err)))
764 .await?;
765
db0cb9ce
WB
766 let payload = tokio_util::codec::FramedRead::new(file, tokio_util::codec::BytesCodec::new())
767 .map_ok(|bytes| hyper::body::Bytes::from(bytes.freeze()));
ad51d02a 768 let body = Body::wrap_stream(payload);
9e47c0a5 769
ad51d02a
DM
770 // fixme: set other headers ?
771 Ok(Response::builder()
772 .status(StatusCode::OK)
773 .header(header::CONTENT_TYPE, "application/octet-stream")
774 .body(body)
775 .unwrap())
776 }.boxed()
9e47c0a5
DM
777}
778
552c2259 779#[sortable]
0ab08ac9
DM
780pub const API_METHOD_UPLOAD_BACKUP_LOG: ApiMethod = ApiMethod::new(
781 &ApiHandler::AsyncHttp(&upload_backup_log),
255f378a 782 &ObjectSchema::new(
54552dda 783 "Upload the client backup log file into a backup snapshot ('client.log.blob').",
552c2259 784 &sorted!([
66c49c21 785 ("store", false, &DATASTORE_SCHEMA),
255f378a 786 ("backup-type", false, &BACKUP_TYPE_SCHEMA),
0ab08ac9 787 ("backup-id", false, &BACKUP_ID_SCHEMA),
255f378a 788 ("backup-time", false, &BACKUP_TIME_SCHEMA),
552c2259 789 ]),
9e47c0a5 790 )
54552dda
DM
791).access(
792 Some("Only the backup creator/owner is allowed to do this."),
793 &Permission::Privilege(&["datastore", "{store}"], PRIV_DATASTORE_BACKUP, false)
794);
9e47c0a5 795
07ee2235
DM
796fn upload_backup_log(
797 _parts: Parts,
798 req_body: Body,
799 param: Value,
255f378a 800 _info: &ApiMethod,
54552dda 801 rpcenv: Box<dyn RpcEnvironment>,
bb084b9c 802) -> ApiResponseFuture {
07ee2235 803
ad51d02a
DM
804 async move {
805 let store = tools::required_string_param(&param, "store")?;
ad51d02a 806 let datastore = DataStore::lookup_datastore(store)?;
07ee2235 807
ad51d02a 808 let file_name = "client.log.blob";
07ee2235 809
ad51d02a
DM
810 let backup_type = tools::required_string_param(&param, "backup-type")?;
811 let backup_id = tools::required_string_param(&param, "backup-id")?;
812 let backup_time = tools::required_integer_param(&param, "backup-time")?;
07ee2235 813
ad51d02a 814 let backup_dir = BackupDir::new(backup_type, backup_id, backup_time);
07ee2235 815
54552dda
DM
816 let username = rpcenv.get_user().unwrap();
817 check_backup_owner(&datastore, backup_dir.group(), &username)?;
818
ad51d02a
DM
819 let mut path = datastore.base_path();
820 path.push(backup_dir.relative_path());
821 path.push(&file_name);
07ee2235 822
ad51d02a
DM
823 if path.exists() {
824 bail!("backup already contains a log.");
825 }
e128d4e8 826
ad51d02a
DM
827 println!("Upload backup log to {}/{}/{}/{}/{}", store,
828 backup_type, backup_id, BackupDir::backup_time_to_string(backup_dir.backup_time()), file_name);
829
830 let data = req_body
831 .map_err(Error::from)
832 .try_fold(Vec::new(), |mut acc, chunk| {
833 acc.extend_from_slice(&*chunk);
834 future::ok::<_, Error>(acc)
835 })
836 .await?;
837
838 let blob = DataBlob::from_raw(data)?;
839 // always verify CRC at server side
840 blob.verify_crc()?;
841 let raw_data = blob.raw_data();
feaa1ad3 842 replace_file(&path, raw_data, CreateOptions::new())?;
ad51d02a
DM
843
844 // fixme: use correct formatter
845 Ok(crate::server::formatter::json_response(Ok(Value::Null)))
846 }.boxed()
07ee2235
DM
847}
848
1a0d3d11
DM
849#[api(
850 input: {
851 properties: {
852 store: {
853 schema: DATASTORE_SCHEMA,
854 },
855 timeframe: {
856 type: RRDTimeFrameResolution,
857 },
858 cf: {
859 type: RRDMode,
860 },
861 },
862 },
863 access: {
864 permission: &Permission::Privilege(&["datastore", "{store}"], PRIV_DATASTORE_AUDIT | PRIV_DATASTORE_BACKUP, true),
865 },
866)]
867/// Read datastore stats
868fn get_rrd_stats(
869 store: String,
870 timeframe: RRDTimeFrameResolution,
871 cf: RRDMode,
872 _param: Value,
873) -> Result<Value, Error> {
874
875 let rrd_dir = format!("datastore/{}", store);
876
877 crate::rrd::extract_data(
878 &rrd_dir,
879 &[
880 "total", "used",
c94e1f65
DM
881 "read_ios", "read_bytes",
882 "write_ios", "write_bytes",
883 "io_ticks",
1a0d3d11
DM
884 ],
885 timeframe,
886 cf,
887 )
888}
889
552c2259 890#[sortable]
255f378a
DM
891const DATASTORE_INFO_SUBDIRS: SubdirMap = &[
892 (
893 "download",
894 &Router::new()
895 .download(&API_METHOD_DOWNLOAD_FILE)
896 ),
897 (
898 "files",
899 &Router::new()
09b1f7b2 900 .get(&API_METHOD_LIST_SNAPSHOT_FILES)
255f378a
DM
901 ),
902 (
903 "gc",
904 &Router::new()
905 .get(&API_METHOD_GARBAGE_COLLECTION_STATUS)
906 .post(&API_METHOD_START_GARBAGE_COLLECTION)
907 ),
908 (
909 "groups",
910 &Router::new()
b31c8019 911 .get(&API_METHOD_LIST_GROUPS)
255f378a
DM
912 ),
913 (
914 "prune",
915 &Router::new()
916 .post(&API_METHOD_PRUNE)
917 ),
1a0d3d11
DM
918 (
919 "rrd",
920 &Router::new()
921 .get(&API_METHOD_GET_RRD_STATS)
922 ),
255f378a
DM
923 (
924 "snapshots",
925 &Router::new()
fc189b19 926 .get(&API_METHOD_LIST_SNAPSHOTS)
68a6a0ee 927 .delete(&API_METHOD_DELETE_SNAPSHOT)
255f378a
DM
928 ),
929 (
930 "status",
931 &Router::new()
932 .get(&API_METHOD_STATUS)
933 ),
934 (
935 "upload-backup-log",
936 &Router::new()
937 .upload(&API_METHOD_UPLOAD_BACKUP_LOG)
938 ),
939];
940
ad51d02a 941const DATASTORE_INFO_ROUTER: Router = Router::new()
255f378a
DM
942 .get(&list_subdirs_api_method!(DATASTORE_INFO_SUBDIRS))
943 .subdirs(DATASTORE_INFO_SUBDIRS);
944
945
946pub const ROUTER: Router = Router::new()
bb34b589 947 .get(&API_METHOD_GET_DATASTORE_LIST)
255f378a 948 .match_all("store", &DATASTORE_INFO_ROUTER);