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