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