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