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