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