]> git.proxmox.com Git - proxmox-backup.git/blob - src/api2/config/datastore.rs
fix non-camel-case enums
[proxmox-backup.git] / src / api2 / config / datastore.rs
1 use std::path::PathBuf;
2
3 use ::serde::{Deserialize, Serialize};
4 use anyhow::Error;
5 use hex::FromHex;
6 use serde_json::Value;
7
8 use proxmox_router::{http_bail, Permission, Router, RpcEnvironment, RpcEnvironmentType};
9 use proxmox_schema::{api, param_bail, ApiType};
10 use proxmox_section_config::SectionConfigData;
11 use proxmox_sys::{task_warn, WorkerTaskContext};
12
13 use pbs_api_types::{
14 Authid, DataStoreConfig, DataStoreConfigUpdater, DatastoreNotify, DatastoreTuning,
15 DATASTORE_SCHEMA, PRIV_DATASTORE_ALLOCATE, PRIV_DATASTORE_AUDIT, PRIV_DATASTORE_MODIFY,
16 PROXMOX_CONFIG_DIGEST_SCHEMA, UPID_SCHEMA,
17 };
18 use pbs_config::BackupLockGuard;
19 use pbs_datastore::chunk_store::ChunkStore;
20
21 use crate::api2::admin::{
22 prune::list_prune_jobs, sync::list_sync_jobs, verify::list_verification_jobs,
23 };
24 use crate::api2::config::prune::delete_prune_job;
25 use crate::api2::config::sync::delete_sync_job;
26 use crate::api2::config::tape_backup_job::{delete_tape_backup_job, list_tape_backup_jobs};
27 use crate::api2::config::verify::delete_verification_job;
28 use pbs_config::CachedUserInfo;
29
30 use proxmox_rest_server::WorkerTask;
31
32 use crate::server::jobstate;
33
34 #[api(
35 input: {
36 properties: {},
37 },
38 returns: {
39 description: "List the configured datastores (with config digest).",
40 type: Array,
41 items: { type: DataStoreConfig },
42 },
43 access: {
44 permission: &Permission::Anybody,
45 },
46 )]
47 /// List all datastores
48 pub fn list_datastores(
49 _param: Value,
50 rpcenv: &mut dyn RpcEnvironment,
51 ) -> Result<Vec<DataStoreConfig>, Error> {
52 let (config, digest) = pbs_config::datastore::config()?;
53
54 let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
55 let user_info = CachedUserInfo::new()?;
56
57 rpcenv["digest"] = hex::encode(digest).into();
58
59 let list: Vec<DataStoreConfig> = config.convert_to_typed_array("datastore")?;
60 let filter_by_privs = |store: &DataStoreConfig| {
61 let user_privs = user_info.lookup_privs(&auth_id, &["datastore", &store.name]);
62 (user_privs & PRIV_DATASTORE_AUDIT) != 0
63 };
64
65 Ok(list.into_iter().filter(filter_by_privs).collect())
66 }
67
68 pub(crate) fn do_create_datastore(
69 _lock: BackupLockGuard,
70 mut config: SectionConfigData,
71 datastore: DataStoreConfig,
72 worker: Option<&dyn WorkerTaskContext>,
73 ) -> Result<(), Error> {
74 let path: PathBuf = datastore.path.clone().into();
75
76 let tuning: DatastoreTuning = serde_json::from_value(
77 DatastoreTuning::API_SCHEMA
78 .parse_property_string(datastore.tuning.as_deref().unwrap_or(""))?,
79 )?;
80 let backup_user = pbs_config::backup_user()?;
81 let _store = ChunkStore::create(
82 &datastore.name,
83 path,
84 backup_user.uid,
85 backup_user.gid,
86 worker,
87 tuning.sync_level.unwrap_or_default(),
88 )?;
89
90 config.set_data(&datastore.name, "datastore", &datastore)?;
91
92 pbs_config::datastore::save_config(&config)?;
93
94 jobstate::create_state_file("prune", &datastore.name)?;
95 jobstate::create_state_file("garbage_collection", &datastore.name)?;
96
97 Ok(())
98 }
99
100 #[api(
101 protected: true,
102 input: {
103 properties: {
104 config: {
105 type: DataStoreConfig,
106 flatten: true,
107 },
108 },
109 },
110 access: {
111 permission: &Permission::Privilege(&["datastore"], PRIV_DATASTORE_ALLOCATE, false),
112 },
113 )]
114 /// Create new datastore config.
115 pub fn create_datastore(
116 config: DataStoreConfig,
117 rpcenv: &mut dyn RpcEnvironment,
118 ) -> Result<String, Error> {
119 let lock = pbs_config::datastore::lock_config()?;
120
121 let (section_config, _digest) = pbs_config::datastore::config()?;
122
123 if section_config.sections.get(&config.name).is_some() {
124 param_bail!("name", "datastore '{}' already exists.", config.name);
125 }
126
127 let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
128 let to_stdout = rpcenv.env_type() == RpcEnvironmentType::CLI;
129
130 WorkerTask::new_thread(
131 "create-datastore",
132 Some(config.name.to_string()),
133 auth_id.to_string(),
134 to_stdout,
135 move |worker| do_create_datastore(lock, section_config, config, Some(&worker)),
136 )
137 }
138
139 #[api(
140 input: {
141 properties: {
142 name: {
143 schema: DATASTORE_SCHEMA,
144 },
145 },
146 },
147 returns: { type: DataStoreConfig },
148 access: {
149 permission: &Permission::Privilege(&["datastore", "{name}"], PRIV_DATASTORE_AUDIT, false),
150 },
151 )]
152 /// Read a datastore configuration.
153 pub fn read_datastore(
154 name: String,
155 rpcenv: &mut dyn RpcEnvironment,
156 ) -> Result<DataStoreConfig, Error> {
157 let (config, digest) = pbs_config::datastore::config()?;
158
159 let store_config = config.lookup("datastore", &name)?;
160 rpcenv["digest"] = hex::encode(digest).into();
161
162 Ok(store_config)
163 }
164
165 #[api()]
166 #[derive(Serialize, Deserialize)]
167 #[serde(rename_all = "kebab-case")]
168 /// Deletable property name
169 pub enum DeletableProperty {
170 /// Delete the comment property.
171 Comment,
172 /// Delete the garbage collection schedule.
173 GcSchedule,
174 /// Delete the prune job schedule.
175 PruneSchedule,
176 /// Delete the keep-last property
177 KeepLast,
178 /// Delete the keep-hourly property
179 KeepHourly,
180 /// Delete the keep-daily property
181 KeepDaily,
182 /// Delete the keep-weekly property
183 KeepWeekly,
184 /// Delete the keep-monthly property
185 KeepMonthly,
186 /// Delete the keep-yearly property
187 KeepYearly,
188 /// Delete the verify-new property
189 VerifyNew,
190 /// Delete the notify-user property
191 NotifyUser,
192 /// Delete the notify property
193 Notify,
194 /// Delete the tuning property
195 Tuning,
196 /// Delete the maintenance-mode property
197 MaintenanceMode,
198 }
199
200 #[api(
201 protected: true,
202 input: {
203 properties: {
204 name: {
205 schema: DATASTORE_SCHEMA,
206 },
207 update: {
208 type: DataStoreConfigUpdater,
209 flatten: true,
210 },
211 delete: {
212 description: "List of properties to delete.",
213 type: Array,
214 optional: true,
215 items: {
216 type: DeletableProperty,
217 }
218 },
219 digest: {
220 optional: true,
221 schema: PROXMOX_CONFIG_DIGEST_SCHEMA,
222 },
223 },
224 },
225 access: {
226 permission: &Permission::Privilege(&["datastore", "{name}"], PRIV_DATASTORE_MODIFY, false),
227 },
228 )]
229 /// Update datastore config.
230 pub fn update_datastore(
231 update: DataStoreConfigUpdater,
232 name: String,
233 delete: Option<Vec<DeletableProperty>>,
234 digest: Option<String>,
235 ) -> Result<(), Error> {
236 let _lock = pbs_config::datastore::lock_config()?;
237
238 // pass/compare digest
239 let (mut config, expected_digest) = pbs_config::datastore::config()?;
240
241 if let Some(ref digest) = digest {
242 let digest = <[u8; 32]>::from_hex(digest)?;
243 crate::tools::detect_modified_configuration_file(&digest, &expected_digest)?;
244 }
245
246 let mut data: DataStoreConfig = config.lookup("datastore", &name)?;
247
248 if let Some(delete) = delete {
249 for delete_prop in delete {
250 match delete_prop {
251 DeletableProperty::Comment => {
252 data.comment = None;
253 }
254 DeletableProperty::GcSchedule => {
255 data.gc_schedule = None;
256 }
257 DeletableProperty::PruneSchedule => {
258 data.prune_schedule = None;
259 }
260 DeletableProperty::KeepLast => {
261 data.keep.keep_last = None;
262 }
263 DeletableProperty::KeepHourly => {
264 data.keep.keep_hourly = None;
265 }
266 DeletableProperty::KeepDaily => {
267 data.keep.keep_daily = None;
268 }
269 DeletableProperty::KeepWeekly => {
270 data.keep.keep_weekly = None;
271 }
272 DeletableProperty::KeepMonthly => {
273 data.keep.keep_monthly = None;
274 }
275 DeletableProperty::KeepYearly => {
276 data.keep.keep_yearly = None;
277 }
278 DeletableProperty::VerifyNew => {
279 data.verify_new = None;
280 }
281 DeletableProperty::Notify => {
282 data.notify = None;
283 }
284 DeletableProperty::NotifyUser => {
285 data.notify_user = None;
286 }
287 DeletableProperty::Tuning => {
288 data.tuning = None;
289 }
290 DeletableProperty::MaintenanceMode => {
291 data.maintenance_mode = None;
292 }
293 }
294 }
295 }
296
297 if let Some(comment) = update.comment {
298 let comment = comment.trim().to_string();
299 if comment.is_empty() {
300 data.comment = None;
301 } else {
302 data.comment = Some(comment);
303 }
304 }
305
306 let mut gc_schedule_changed = false;
307 if update.gc_schedule.is_some() {
308 gc_schedule_changed = data.gc_schedule != update.gc_schedule;
309 data.gc_schedule = update.gc_schedule;
310 }
311
312 macro_rules! prune_disabled {
313 ($(($param:literal, $($member:tt)+)),+) => {
314 $(
315 if update.$($member)+.is_some() {
316 param_bail!(
317 $param,
318 "datastore prune settings have been replaced by prune jobs",
319 );
320 }
321 )+
322 };
323 }
324 prune_disabled! {
325 ("keep-last", keep.keep_last),
326 ("keep-hourly", keep.keep_hourly),
327 ("keep-daily", keep.keep_daily),
328 ("keep-weekly", keep.keep_weekly),
329 ("keep-monthly", keep.keep_monthly),
330 ("keep-yearly", keep.keep_yearly),
331 ("prune-schedule", prune_schedule)
332 }
333
334 if let Some(notify_str) = update.notify {
335 let value = DatastoreNotify::API_SCHEMA.parse_property_string(&notify_str)?;
336 let notify: DatastoreNotify = serde_json::from_value(value)?;
337 if let DatastoreNotify {
338 gc: None,
339 verify: None,
340 sync: None,
341 prune: None,
342 } = notify
343 {
344 data.notify = None;
345 } else {
346 data.notify = Some(notify_str);
347 }
348 }
349 if update.verify_new.is_some() {
350 data.verify_new = update.verify_new;
351 }
352
353 if update.notify_user.is_some() {
354 data.notify_user = update.notify_user;
355 }
356
357 if update.tuning.is_some() {
358 data.tuning = update.tuning;
359 }
360
361 if update.maintenance_mode.is_some() {
362 data.maintenance_mode = update.maintenance_mode;
363 }
364
365 config.set_data(&name, "datastore", &data)?;
366
367 pbs_config::datastore::save_config(&config)?;
368
369 // we want to reset the statefiles, to avoid an immediate action in some cases
370 // (e.g. going from monthly to weekly in the second week of the month)
371 if gc_schedule_changed {
372 jobstate::update_job_last_run_time("garbage_collection", &name)?;
373 }
374
375 Ok(())
376 }
377
378 #[api(
379 protected: true,
380 input: {
381 properties: {
382 name: {
383 schema: DATASTORE_SCHEMA,
384 },
385 "keep-job-configs": {
386 description: "If enabled, the job configurations related to this datastore will be kept.",
387 type: bool,
388 optional: true,
389 default: false,
390 },
391 "destroy-data": {
392 description: "Delete the datastore's underlying contents",
393 optional: true,
394 type: bool,
395 default: false,
396 },
397 digest: {
398 optional: true,
399 schema: PROXMOX_CONFIG_DIGEST_SCHEMA,
400 },
401 },
402 },
403 access: {
404 permission: &Permission::Privilege(&["datastore", "{name}"], PRIV_DATASTORE_ALLOCATE, false),
405 },
406 returns: {
407 schema: UPID_SCHEMA,
408 },
409 )]
410 /// Remove a datastore configuration and optionally delete all its contents.
411 pub async fn delete_datastore(
412 name: String,
413 keep_job_configs: bool,
414 destroy_data: bool,
415 digest: Option<String>,
416 rpcenv: &mut dyn RpcEnvironment,
417 ) -> Result<String, Error> {
418 let _lock = pbs_config::datastore::lock_config()?;
419
420 let (config, expected_digest) = pbs_config::datastore::config()?;
421
422 if let Some(ref digest) = digest {
423 let digest = <[u8; 32]>::from_hex(digest)?;
424 crate::tools::detect_modified_configuration_file(&digest, &expected_digest)?;
425 }
426
427 if !config.sections.contains_key(&name) {
428 http_bail!(NOT_FOUND, "datastore '{}' does not exist.", name);
429 }
430
431 if !keep_job_configs {
432 for job in list_verification_jobs(Some(name.clone()), Value::Null, rpcenv)? {
433 delete_verification_job(job.config.id, None, rpcenv)?
434 }
435 for job in list_sync_jobs(Some(name.clone()), Value::Null, rpcenv)? {
436 delete_sync_job(job.config.id, None, rpcenv)?
437 }
438 for job in list_prune_jobs(Some(name.clone()), Value::Null, rpcenv)? {
439 delete_prune_job(job.config.id, None, rpcenv)?
440 }
441
442 let (mut tree, _digest) = pbs_config::acl::config()?;
443 tree.delete_node(&format!("/datastore/{}", name));
444 pbs_config::acl::save_config(&tree)?;
445
446 let tape_jobs = list_tape_backup_jobs(Value::Null, rpcenv)?;
447 for job_config in tape_jobs
448 .into_iter()
449 .filter(|config| config.setup.store == name)
450 {
451 delete_tape_backup_job(job_config.id, None, rpcenv)?;
452 }
453 }
454
455 let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
456 let to_stdout = rpcenv.env_type() == RpcEnvironmentType::CLI;
457
458 let upid = WorkerTask::new_thread(
459 "delete-datastore",
460 Some(name.clone()),
461 auth_id.to_string(),
462 to_stdout,
463 move |worker| {
464 pbs_datastore::DataStore::destroy(&name, destroy_data, &worker)?;
465
466 // ignore errors
467 let _ = jobstate::remove_state_file("prune", &name);
468 let _ = jobstate::remove_state_file("garbage_collection", &name);
469
470 if let Err(err) =
471 proxmox_async::runtime::block_on(crate::server::notify_datastore_removed())
472 {
473 task_warn!(worker, "failed to notify after datastore removal: {err}");
474 }
475
476 Ok(())
477 },
478 )?;
479
480 Ok(upid)
481 }
482
483 const ITEM_ROUTER: Router = Router::new()
484 .get(&API_METHOD_READ_DATASTORE)
485 .put(&API_METHOD_UPDATE_DATASTORE)
486 .delete(&API_METHOD_DELETE_DATASTORE);
487
488 pub const ROUTER: Router = Router::new()
489 .get(&API_METHOD_LIST_DATASTORES)
490 .post(&API_METHOD_CREATE_DATASTORE)
491 .match_all("name", &ITEM_ROUTER);