]>
Commit | Line | Data |
---|---|---|
a2479cfa | 1 | use std::path::PathBuf; |
6ce50400 | 2 | |
f7d4e4b5 | 3 | use anyhow::{bail, Error}; |
5e62d19c | 4 | use serde_json::Value; |
0a00f6e0 | 5 | use ::serde::{Deserialize, Serialize}; |
6ce50400 | 6 | |
67f7ffd0 | 7 | use proxmox::api::{api, Router, RpcEnvironment, Permission}; |
c26c9390 | 8 | use proxmox::api::schema::parse_property_string; |
98c259b4 | 9 | use proxmox::tools::fs::open_file_locked; |
a2479cfa | 10 | |
66c49c21 | 11 | use crate::api2::types::*; |
a2479cfa | 12 | use crate::backup::*; |
b93bbab4 | 13 | use crate::config::cached_user_info::CachedUserInfo; |
67f7ffd0 | 14 | use crate::config::datastore::{self, DataStoreConfig, DIR_NAME_SCHEMA}; |
41bfd249 | 15 | use crate::config::acl::{PRIV_DATASTORE_ALLOCATE, PRIV_DATASTORE_AUDIT, PRIV_DATASTORE_MODIFY}; |
1298618a | 16 | use crate::server::jobstate; |
567713b4 | 17 | |
688fbe07 DM |
18 | #[api( |
19 | input: { | |
20 | properties: {}, | |
21 | }, | |
22 | returns: { | |
f3ec5dae | 23 | description: "List the configured datastores (with config digest).", |
688fbe07 | 24 | type: Array, |
67f7ffd0 | 25 | items: { type: datastore::DataStoreConfig }, |
688fbe07 | 26 | }, |
c0ef209a | 27 | access: { |
b93bbab4 | 28 | permission: &Permission::Anybody, |
c0ef209a | 29 | }, |
688fbe07 DM |
30 | )] |
31 | /// List all datastores | |
32 | pub fn list_datastores( | |
6049b71f | 33 | _param: Value, |
67f7ffd0 DM |
34 | mut rpcenv: &mut dyn RpcEnvironment, |
35 | ) -> Result<Vec<DataStoreConfig>, Error> { | |
567713b4 | 36 | |
d0187a51 | 37 | let (config, digest) = datastore::config()?; |
b65eaac6 | 38 | |
e6dc35ac | 39 | let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?; |
b93bbab4 | 40 | let user_info = CachedUserInfo::new()?; |
67f7ffd0 DM |
41 | |
42 | rpcenv["digest"] = proxmox::tools::digest_to_hex(&digest).into(); | |
43 | ||
b93bbab4 FG |
44 | let list:Vec<DataStoreConfig> = config.convert_to_typed_array("datastore")?; |
45 | let filter_by_privs = |store: &DataStoreConfig| { | |
e6dc35ac | 46 | let user_privs = user_info.lookup_privs(&auth_id, &["datastore", &store.name]); |
b93bbab4 FG |
47 | (user_privs & PRIV_DATASTORE_AUDIT) != 0 |
48 | }; | |
49 | ||
50 | Ok(list.into_iter().filter(filter_by_privs).collect()) | |
ea0b8b6e DM |
51 | } |
52 | ||
67f7ffd0 DM |
53 | |
54 | // fixme: impl. const fn get_object_schema(datastore::DataStoreConfig::API_SCHEMA), | |
55 | // but this need support for match inside const fn | |
56 | // see: https://github.com/rust-lang/rust/issues/49146 | |
57 | ||
688fbe07 DM |
58 | #[api( |
59 | protected: true, | |
60 | input: { | |
61 | properties: { | |
62 | name: { | |
63 | schema: DATASTORE_SCHEMA, | |
64 | }, | |
67f7ffd0 DM |
65 | path: { |
66 | schema: DIR_NAME_SCHEMA, | |
67 | }, | |
688fbe07 DM |
68 | comment: { |
69 | optional: true, | |
454c13ed | 70 | schema: SINGLE_LINE_COMMENT_SCHEMA, |
688fbe07 | 71 | }, |
6e545d00 DM |
72 | "notify-user": { |
73 | optional: true, | |
74 | type: Userid, | |
75 | }, | |
76 | "notify": { | |
77 | optional: true, | |
c26c9390 | 78 | schema: DATASTORE_NOTIFY_STRING_SCHEMA, |
6e545d00 | 79 | }, |
42fdbe51 DM |
80 | "gc-schedule": { |
81 | optional: true, | |
82 | schema: GC_SCHEDULE_SCHEMA, | |
83 | }, | |
67f7ffd0 DM |
84 | "prune-schedule": { |
85 | optional: true, | |
86 | schema: PRUNE_SCHEDULE_SCHEMA, | |
87 | }, | |
88 | "keep-last": { | |
89 | optional: true, | |
90 | schema: PRUNE_SCHEMA_KEEP_LAST, | |
91 | }, | |
92 | "keep-hourly": { | |
93 | optional: true, | |
94 | schema: PRUNE_SCHEMA_KEEP_HOURLY, | |
95 | }, | |
96 | "keep-daily": { | |
97 | optional: true, | |
98 | schema: PRUNE_SCHEMA_KEEP_DAILY, | |
99 | }, | |
100 | "keep-weekly": { | |
101 | optional: true, | |
102 | schema: PRUNE_SCHEMA_KEEP_WEEKLY, | |
103 | }, | |
104 | "keep-monthly": { | |
105 | optional: true, | |
106 | schema: PRUNE_SCHEMA_KEEP_MONTHLY, | |
107 | }, | |
108 | "keep-yearly": { | |
109 | optional: true, | |
110 | schema: PRUNE_SCHEMA_KEEP_YEARLY, | |
688fbe07 DM |
111 | }, |
112 | }, | |
113 | }, | |
c0ef209a | 114 | access: { |
41bfd249 | 115 | permission: &Permission::Privilege(&["datastore"], PRIV_DATASTORE_ALLOCATE, false), |
c0ef209a | 116 | }, |
688fbe07 DM |
117 | )] |
118 | /// Create new datastore config. | |
67f7ffd0 | 119 | pub fn create_datastore(param: Value) -> Result<(), Error> { |
ea0b8b6e | 120 | |
b56c111e | 121 | let _lock = open_file_locked(datastore::DATASTORE_CFG_LOCKFILE, std::time::Duration::new(10, 0), true)?; |
652c1190 | 122 | |
44288184 | 123 | let datastore: datastore::DataStoreConfig = serde_json::from_value(param)?; |
652c1190 | 124 | |
d0187a51 | 125 | let (mut config, _digest) = datastore::config()?; |
652c1190 | 126 | |
67f7ffd0 DM |
127 | if let Some(_) = config.sections.get(&datastore.name) { |
128 | bail!("datastore '{}' already exists.", datastore.name); | |
652c1190 DM |
129 | } |
130 | ||
688fbe07 DM |
131 | let path: PathBuf = datastore.path.clone().into(); |
132 | ||
133 | let backup_user = crate::backup::backup_user()?; | |
67f7ffd0 | 134 | let _store = ChunkStore::create(&datastore.name, path, backup_user.uid, backup_user.gid)?; |
688fbe07 | 135 | |
67f7ffd0 | 136 | config.set_data(&datastore.name, "datastore", &datastore)?; |
652c1190 DM |
137 | |
138 | datastore::save_config(&config)?; | |
139 | ||
1298618a DM |
140 | jobstate::create_state_file("prune", &datastore.name)?; |
141 | jobstate::create_state_file("garbage_collection", &datastore.name)?; | |
9866de5e | 142 | |
688fbe07 | 143 | Ok(()) |
6ce50400 DM |
144 | } |
145 | ||
c5799e40 DM |
146 | #[api( |
147 | input: { | |
148 | properties: { | |
149 | name: { | |
150 | schema: DATASTORE_SCHEMA, | |
151 | }, | |
152 | }, | |
153 | }, | |
9b93c620 | 154 | returns: { type: datastore::DataStoreConfig }, |
c0ef209a DM |
155 | access: { |
156 | permission: &Permission::Privilege(&["datastore", "{name}"], PRIV_DATASTORE_AUDIT, false), | |
157 | }, | |
c5799e40 DM |
158 | )] |
159 | /// Read a datastore configuration. | |
67f7ffd0 DM |
160 | pub fn read_datastore( |
161 | name: String, | |
162 | mut rpcenv: &mut dyn RpcEnvironment, | |
163 | ) -> Result<DataStoreConfig, Error> { | |
c5799e40 | 164 | let (config, digest) = datastore::config()?; |
67f7ffd0 DM |
165 | |
166 | let store_config = config.lookup("datastore", &name)?; | |
167 | rpcenv["digest"] = proxmox::tools::digest_to_hex(&digest).into(); | |
168 | ||
169 | Ok(store_config) | |
c5799e40 DM |
170 | } |
171 | ||
0a00f6e0 DM |
172 | #[api()] |
173 | #[derive(Serialize, Deserialize)] | |
42fdbe51 | 174 | #[serde(rename_all="kebab-case")] |
0a00f6e0 DM |
175 | #[allow(non_camel_case_types)] |
176 | /// Deletable property name | |
177 | pub enum DeletableProperty { | |
178 | /// Delete the comment property. | |
179 | comment, | |
42fdbe51 DM |
180 | /// Delete the garbage collection schedule. |
181 | gc_schedule, | |
67f7ffd0 DM |
182 | /// Delete the prune job schedule. |
183 | prune_schedule, | |
184 | /// Delete the keep-last property | |
185 | keep_last, | |
186 | /// Delete the keep-hourly property | |
187 | keep_hourly, | |
188 | /// Delete the keep-daily property | |
189 | keep_daily, | |
190 | /// Delete the keep-weekly property | |
191 | keep_weekly, | |
192 | /// Delete the keep-monthly property | |
193 | keep_monthly, | |
194 | /// Delete the keep-yearly property | |
195 | keep_yearly, | |
ad53c1d6 TL |
196 | /// Delete the verify-new property |
197 | verify_new, | |
6e545d00 DM |
198 | /// Delete the notify-user property |
199 | notify_user, | |
200 | /// Delete the notify property | |
201 | notify, | |
0a00f6e0 DM |
202 | } |
203 | ||
c5799e40 DM |
204 | #[api( |
205 | protected: true, | |
206 | input: { | |
207 | properties: { | |
208 | name: { | |
209 | schema: DATASTORE_SCHEMA, | |
210 | }, | |
211 | comment: { | |
212 | optional: true, | |
213 | schema: SINGLE_LINE_COMMENT_SCHEMA, | |
214 | }, | |
6e545d00 DM |
215 | "notify-user": { |
216 | optional: true, | |
217 | type: Userid, | |
218 | }, | |
219 | "notify": { | |
220 | optional: true, | |
c26c9390 | 221 | schema: DATASTORE_NOTIFY_STRING_SCHEMA, |
6e545d00 | 222 | }, |
42fdbe51 DM |
223 | "gc-schedule": { |
224 | optional: true, | |
225 | schema: GC_SCHEDULE_SCHEMA, | |
226 | }, | |
67f7ffd0 DM |
227 | "prune-schedule": { |
228 | optional: true, | |
229 | schema: PRUNE_SCHEDULE_SCHEMA, | |
230 | }, | |
231 | "keep-last": { | |
232 | optional: true, | |
233 | schema: PRUNE_SCHEMA_KEEP_LAST, | |
234 | }, | |
235 | "keep-hourly": { | |
236 | optional: true, | |
237 | schema: PRUNE_SCHEMA_KEEP_HOURLY, | |
238 | }, | |
239 | "keep-daily": { | |
240 | optional: true, | |
241 | schema: PRUNE_SCHEMA_KEEP_DAILY, | |
242 | }, | |
243 | "keep-weekly": { | |
244 | optional: true, | |
245 | schema: PRUNE_SCHEMA_KEEP_WEEKLY, | |
246 | }, | |
247 | "keep-monthly": { | |
248 | optional: true, | |
249 | schema: PRUNE_SCHEMA_KEEP_MONTHLY, | |
250 | }, | |
251 | "keep-yearly": { | |
252 | optional: true, | |
253 | schema: PRUNE_SCHEMA_KEEP_YEARLY, | |
254 | }, | |
ad53c1d6 TL |
255 | "verify-new": { |
256 | description: "If enabled, all new backups will be verified right after completion.", | |
257 | type: bool, | |
258 | optional: true, | |
259 | default: false, | |
260 | }, | |
0a00f6e0 DM |
261 | delete: { |
262 | description: "List of properties to delete.", | |
263 | type: Array, | |
264 | optional: true, | |
265 | items: { | |
266 | type: DeletableProperty, | |
267 | } | |
268 | }, | |
002a191a DM |
269 | digest: { |
270 | optional: true, | |
271 | schema: PROXMOX_CONFIG_DIGEST_SCHEMA, | |
272 | }, | |
c5799e40 DM |
273 | }, |
274 | }, | |
c0ef209a | 275 | access: { |
9c7fe29d | 276 | permission: &Permission::Privilege(&["datastore", "{name}"], PRIV_DATASTORE_MODIFY, false), |
c0ef209a | 277 | }, |
c5799e40 | 278 | )] |
2ea7bf1b | 279 | /// Update datastore config. |
c5799e40 DM |
280 | pub fn update_datastore( |
281 | name: String, | |
282 | comment: Option<String>, | |
42fdbe51 | 283 | gc_schedule: Option<String>, |
67f7ffd0 | 284 | prune_schedule: Option<String>, |
872062ee DM |
285 | keep_last: Option<u64>, |
286 | keep_hourly: Option<u64>, | |
287 | keep_daily: Option<u64>, | |
288 | keep_weekly: Option<u64>, | |
289 | keep_monthly: Option<u64>, | |
290 | keep_yearly: Option<u64>, | |
ad53c1d6 | 291 | verify_new: Option<bool>, |
c26c9390 | 292 | notify: Option<String>, |
6e545d00 | 293 | notify_user: Option<Userid>, |
0a00f6e0 | 294 | delete: Option<Vec<DeletableProperty>>, |
002a191a | 295 | digest: Option<String>, |
c5799e40 DM |
296 | ) -> Result<(), Error> { |
297 | ||
b56c111e | 298 | let _lock = open_file_locked(datastore::DATASTORE_CFG_LOCKFILE, std::time::Duration::new(10, 0), true)?; |
347834df | 299 | |
c5799e40 | 300 | // pass/compare digest |
002a191a DM |
301 | let (mut config, expected_digest) = datastore::config()?; |
302 | ||
303 | if let Some(ref digest) = digest { | |
304 | let digest = proxmox::tools::hex_to_digest(digest)?; | |
305 | crate::tools::detect_modified_configuration_file(&digest, &expected_digest)?; | |
306 | } | |
c5799e40 DM |
307 | |
308 | let mut data: datastore::DataStoreConfig = config.lookup("datastore", &name)?; | |
309 | ||
0a00f6e0 DM |
310 | if let Some(delete) = delete { |
311 | for delete_prop in delete { | |
312 | match delete_prop { | |
313 | DeletableProperty::comment => { data.comment = None; }, | |
42fdbe51 | 314 | DeletableProperty::gc_schedule => { data.gc_schedule = None; }, |
67f7ffd0 DM |
315 | DeletableProperty::prune_schedule => { data.prune_schedule = None; }, |
316 | DeletableProperty::keep_last => { data.keep_last = None; }, | |
317 | DeletableProperty::keep_hourly => { data.keep_hourly = None; }, | |
318 | DeletableProperty::keep_daily => { data.keep_daily = None; }, | |
319 | DeletableProperty::keep_weekly => { data.keep_weekly = None; }, | |
320 | DeletableProperty::keep_monthly => { data.keep_monthly = None; }, | |
321 | DeletableProperty::keep_yearly => { data.keep_yearly = None; }, | |
ad53c1d6 | 322 | DeletableProperty::verify_new => { data.verify_new = None; }, |
6e545d00 DM |
323 | DeletableProperty::notify => { data.notify = None; }, |
324 | DeletableProperty::notify_user => { data.notify_user = None; }, | |
0a00f6e0 DM |
325 | } |
326 | } | |
327 | } | |
328 | ||
c5799e40 DM |
329 | if let Some(comment) = comment { |
330 | let comment = comment.trim().to_string(); | |
331 | if comment.is_empty() { | |
332 | data.comment = None; | |
333 | } else { | |
334 | data.comment = Some(comment); | |
335 | } | |
336 | } | |
c5799e40 | 337 | |
d7a122a0 DC |
338 | let mut gc_schedule_changed = false; |
339 | if gc_schedule.is_some() { | |
340 | gc_schedule_changed = data.gc_schedule != gc_schedule; | |
341 | data.gc_schedule = gc_schedule; | |
342 | } | |
343 | ||
9866de5e DC |
344 | let mut prune_schedule_changed = false; |
345 | if prune_schedule.is_some() { | |
d7a122a0 | 346 | prune_schedule_changed = data.prune_schedule != prune_schedule; |
9866de5e DC |
347 | data.prune_schedule = prune_schedule; |
348 | } | |
d7a122a0 | 349 | |
67f7ffd0 DM |
350 | if keep_last.is_some() { data.keep_last = keep_last; } |
351 | if keep_hourly.is_some() { data.keep_hourly = keep_hourly; } | |
352 | if keep_daily.is_some() { data.keep_daily = keep_daily; } | |
353 | if keep_weekly.is_some() { data.keep_weekly = keep_weekly; } | |
354 | if keep_monthly.is_some() { data.keep_monthly = keep_monthly; } | |
355 | if keep_yearly.is_some() { data.keep_yearly = keep_yearly; } | |
42fdbe51 | 356 | |
c26c9390 DM |
357 | if let Some(notify_str) = notify { |
358 | let value = parse_property_string(¬ify_str, &DatastoreNotify::API_SCHEMA)?; | |
359 | let notify: DatastoreNotify = serde_json::from_value(value)?; | |
360 | if let DatastoreNotify { gc: None, verify: None, sync: None } = notify { | |
361 | data.notify = None; | |
362 | } else { | |
363 | data.notify = Some(notify_str); | |
364 | } | |
365 | } | |
ad53c1d6 TL |
366 | if verify_new.is_some() { data.verify_new = verify_new; } |
367 | ||
6e545d00 DM |
368 | if notify_user.is_some() { data.notify_user = notify_user; } |
369 | ||
c5799e40 DM |
370 | config.set_data(&name, "datastore", &data)?; |
371 | ||
372 | datastore::save_config(&config)?; | |
373 | ||
d7a122a0 | 374 | // we want to reset the statefiles, to avoid an immediate action in some cases |
9866de5e | 375 | // (e.g. going from monthly to weekly in the second week of the month) |
d7a122a0 | 376 | if gc_schedule_changed { |
1298618a | 377 | jobstate::create_state_file("garbage_collection", &name)?; |
d7a122a0 DC |
378 | } |
379 | ||
9866de5e | 380 | if prune_schedule_changed { |
1298618a | 381 | jobstate::create_state_file("prune", &name)?; |
9866de5e DC |
382 | } |
383 | ||
c5799e40 DM |
384 | Ok(()) |
385 | } | |
386 | ||
688fbe07 DM |
387 | #[api( |
388 | protected: true, | |
389 | input: { | |
390 | properties: { | |
391 | name: { | |
392 | schema: DATASTORE_SCHEMA, | |
393 | }, | |
c0ef209a DM |
394 | digest: { |
395 | optional: true, | |
396 | schema: PROXMOX_CONFIG_DIGEST_SCHEMA, | |
397 | }, | |
688fbe07 DM |
398 | }, |
399 | }, | |
c0ef209a | 400 | access: { |
92dd02aa | 401 | permission: &Permission::Privilege(&["datastore", "{name}"], PRIV_DATASTORE_ALLOCATE, false), |
c0ef209a | 402 | }, |
688fbe07 DM |
403 | )] |
404 | /// Remove a datastore configuration. | |
c0ef209a | 405 | pub fn delete_datastore(name: String, digest: Option<String>) -> Result<(), Error> { |
34d3ba52 | 406 | |
b56c111e | 407 | let _lock = open_file_locked(datastore::DATASTORE_CFG_LOCKFILE, std::time::Duration::new(10, 0), true)?; |
34d3ba52 | 408 | |
c0ef209a DM |
409 | let (mut config, expected_digest) = datastore::config()?; |
410 | ||
411 | if let Some(ref digest) = digest { | |
412 | let digest = proxmox::tools::hex_to_digest(digest)?; | |
413 | crate::tools::detect_modified_configuration_file(&digest, &expected_digest)?; | |
414 | } | |
34d3ba52 | 415 | |
688fbe07 DM |
416 | match config.sections.get(&name) { |
417 | Some(_) => { config.sections.remove(&name); }, | |
34d3ba52 DM |
418 | None => bail!("datastore '{}' does not exist.", name), |
419 | } | |
420 | ||
421 | datastore::save_config(&config)?; | |
422 | ||
d7a122a0 | 423 | // ignore errors |
1298618a DM |
424 | let _ = jobstate::remove_state_file("prune", &name); |
425 | let _ = jobstate::remove_state_file("garbage_collection", &name); | |
9866de5e | 426 | |
688fbe07 | 427 | Ok(()) |
34d3ba52 DM |
428 | } |
429 | ||
c5799e40 DM |
430 | const ITEM_ROUTER: Router = Router::new() |
431 | .get(&API_METHOD_READ_DATASTORE) | |
432 | .put(&API_METHOD_UPDATE_DATASTORE) | |
433 | .delete(&API_METHOD_DELETE_DATASTORE); | |
434 | ||
255f378a | 435 | pub const ROUTER: Router = Router::new() |
688fbe07 DM |
436 | .get(&API_METHOD_LIST_DATASTORES) |
437 | .post(&API_METHOD_CREATE_DATASTORE) | |
c5799e40 | 438 | .match_all("name", &ITEM_ROUTER); |