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