]>
Commit | Line | Data |
---|---|---|
dc7a5b34 | 1 | use ::serde::{Deserialize, Serialize}; |
e0100d61 | 2 | use anyhow::{bail, format_err, Error}; |
dc7a5b34 | 3 | use hex::FromHex; |
1d9bc184 | 4 | use proxmox_router::list_subdirs_api_method; |
dc7a5b34 TL |
5 | use proxmox_router::SubdirMap; |
6 | use proxmox_sys::sortable; | |
141304d6 DM |
7 | use serde_json::Value; |
8 | ||
dc7a5b34 | 9 | use proxmox_router::{http_bail, http_err, ApiMethod, Permission, Router, RpcEnvironment}; |
8d6425aa | 10 | use proxmox_schema::{api, param_bail}; |
141304d6 | 11 | |
6afdda88 | 12 | use pbs_api_types::{ |
dc7a5b34 | 13 | Authid, DataStoreListItem, GroupListItem, RateLimitConfig, Remote, RemoteConfig, |
24cb5c7a DM |
14 | RemoteConfigUpdater, RemoteWithoutPassword, SyncJobConfig, DATASTORE_SCHEMA, PRIV_REMOTE_AUDIT, |
15 | PRIV_REMOTE_MODIFY, PROXMOX_CONFIG_DIGEST_SCHEMA, REMOTE_ID_SCHEMA, REMOTE_PASSWORD_SCHEMA, | |
6afdda88 | 16 | }; |
dc7a5b34 | 17 | use pbs_client::{HttpClient, HttpClientOptions}; |
a4e5a0fc | 18 | use pbs_config::sync; |
2b7f8dd5 | 19 | |
ba3d7e19 | 20 | use pbs_config::CachedUserInfo; |
141304d6 DM |
21 | |
22 | #[api( | |
23 | input: { | |
24 | properties: {}, | |
25 | }, | |
26 | returns: { | |
f3ec5dae | 27 | description: "The list of configured remotes (with config digest).", |
141304d6 | 28 | type: Array, |
24cb5c7a | 29 | items: { type: RemoteWithoutPassword }, |
141304d6 | 30 | }, |
70e5f246 | 31 | access: { |
59af9ca9 FG |
32 | description: "List configured remotes filtered by Remote.Audit privileges", |
33 | permission: &Permission::Anybody, | |
70e5f246 | 34 | }, |
141304d6 DM |
35 | )] |
36 | /// List all remotes | |
37 | pub fn list_remotes( | |
38 | _param: Value, | |
39 | _info: &ApiMethod, | |
41c1a179 | 40 | rpcenv: &mut dyn RpcEnvironment, |
24cb5c7a | 41 | ) -> Result<Vec<RemoteWithoutPassword>, Error> { |
59af9ca9 FG |
42 | let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?; |
43 | let user_info = CachedUserInfo::new()?; | |
141304d6 | 44 | |
6afdda88 | 45 | let (config, digest) = pbs_config::remote::config()?; |
141304d6 | 46 | |
24cb5c7a DM |
47 | // Note: This removes the password (we do not want to return the password). |
48 | let list: Vec<RemoteWithoutPassword> = config.convert_to_typed_array("remote")?; | |
83fd4b3b | 49 | |
59af9ca9 FG |
50 | let list = list |
51 | .into_iter() | |
52 | .filter(|remote| { | |
53 | let privs = user_info.lookup_privs(&auth_id, &["remote", &remote.name]); | |
54 | privs & PRIV_REMOTE_AUDIT != 0 | |
55 | }) | |
56 | .collect(); | |
57 | ||
25877d05 | 58 | rpcenv["digest"] = hex::encode(&digest).into(); |
83fd4b3b | 59 | Ok(list) |
141304d6 DM |
60 | } |
61 | ||
62 | #[api( | |
688fbe07 | 63 | protected: true, |
141304d6 DM |
64 | input: { |
65 | properties: { | |
66 | name: { | |
167971ed | 67 | schema: REMOTE_ID_SCHEMA, |
141304d6 | 68 | }, |
97dfc62f | 69 | config: { |
6afdda88 | 70 | type: RemoteConfig, |
97dfc62f | 71 | flatten: true, |
141304d6 DM |
72 | }, |
73 | password: { | |
97dfc62f | 74 | // We expect the plain password here (not base64 encoded) |
6afdda88 | 75 | schema: REMOTE_PASSWORD_SCHEMA, |
141304d6 DM |
76 | }, |
77 | }, | |
78 | }, | |
70e5f246 | 79 | access: { |
8247db5b | 80 | permission: &Permission::Privilege(&["remote"], PRIV_REMOTE_MODIFY, false), |
70e5f246 | 81 | }, |
141304d6 DM |
82 | )] |
83 | /// Create new remote. | |
dc7a5b34 | 84 | pub fn create_remote(name: String, config: RemoteConfig, password: String) -> Result<(), Error> { |
4beb7d2d | 85 | let _lock = pbs_config::remote::lock_config()?; |
141304d6 | 86 | |
6afdda88 | 87 | let (mut section_config, _digest) = pbs_config::remote::config()?; |
141304d6 | 88 | |
97dfc62f | 89 | if section_config.sections.get(&name).is_some() { |
8d6425aa | 90 | param_bail!("name", "remote '{}' already exists.", name); |
141304d6 DM |
91 | } |
92 | ||
dc7a5b34 TL |
93 | let remote = Remote { |
94 | name: name.clone(), | |
95 | config, | |
96 | password, | |
97 | }; | |
141304d6 | 98 | |
97dfc62f DM |
99 | section_config.set_data(&name, "remote", &remote)?; |
100 | ||
6afdda88 | 101 | pbs_config::remote::save_config(§ion_config)?; |
141304d6 DM |
102 | |
103 | Ok(()) | |
104 | } | |
105 | ||
08195ac8 DM |
106 | #[api( |
107 | input: { | |
108 | properties: { | |
109 | name: { | |
110 | schema: REMOTE_ID_SCHEMA, | |
111 | }, | |
112 | }, | |
113 | }, | |
2c88dc97 | 114 | returns: { type: RemoteWithoutPassword }, |
70e5f246 | 115 | access: { |
8247db5b | 116 | permission: &Permission::Privilege(&["remote", "{name}"], PRIV_REMOTE_AUDIT, false), |
70e5f246 | 117 | } |
08195ac8 DM |
118 | )] |
119 | /// Read remote configuration data. | |
4f966d05 DC |
120 | pub fn read_remote( |
121 | name: String, | |
122 | _info: &ApiMethod, | |
41c1a179 | 123 | rpcenv: &mut dyn RpcEnvironment, |
2c88dc97 | 124 | ) -> Result<RemoteWithoutPassword, Error> { |
6afdda88 | 125 | let (config, digest) = pbs_config::remote::config()?; |
2c88dc97 | 126 | let data: RemoteWithoutPassword = config.lookup("remote", &name)?; |
25877d05 | 127 | rpcenv["digest"] = hex::encode(&digest).into(); |
08195ac8 DM |
128 | Ok(data) |
129 | } | |
30003baa | 130 | |
5211705f DM |
131 | #[api()] |
132 | #[derive(Serialize, Deserialize)] | |
133 | #[allow(non_camel_case_types)] | |
134 | /// Deletable property name | |
135 | pub enum DeletableProperty { | |
136 | /// Delete the comment property. | |
137 | comment, | |
138 | /// Delete the fingerprint property. | |
139 | fingerprint, | |
ba20987a DC |
140 | /// Delete the port property. |
141 | port, | |
5211705f | 142 | } |
08195ac8 DM |
143 | |
144 | #[api( | |
145 | protected: true, | |
146 | input: { | |
147 | properties: { | |
148 | name: { | |
149 | schema: REMOTE_ID_SCHEMA, | |
150 | }, | |
97dfc62f | 151 | update: { |
6afdda88 | 152 | type: RemoteConfigUpdater, |
97dfc62f | 153 | flatten: true, |
08195ac8 DM |
154 | }, |
155 | password: { | |
97dfc62f | 156 | // We expect the plain password here (not base64 encoded) |
08195ac8 | 157 | optional: true, |
6afdda88 | 158 | schema: REMOTE_PASSWORD_SCHEMA, |
08195ac8 | 159 | }, |
5211705f DM |
160 | delete: { |
161 | description: "List of properties to delete.", | |
162 | type: Array, | |
163 | optional: true, | |
164 | items: { | |
165 | type: DeletableProperty, | |
166 | } | |
167 | }, | |
002a191a DM |
168 | digest: { |
169 | optional: true, | |
170 | schema: PROXMOX_CONFIG_DIGEST_SCHEMA, | |
171 | }, | |
08195ac8 DM |
172 | }, |
173 | }, | |
70e5f246 | 174 | access: { |
8247db5b | 175 | permission: &Permission::Privilege(&["remote", "{name}"], PRIV_REMOTE_MODIFY, false), |
70e5f246 | 176 | }, |
08195ac8 DM |
177 | )] |
178 | /// Update remote configuration. | |
179 | pub fn update_remote( | |
180 | name: String, | |
6afdda88 | 181 | update: RemoteConfigUpdater, |
08195ac8 | 182 | password: Option<String>, |
5211705f | 183 | delete: Option<Vec<DeletableProperty>>, |
002a191a | 184 | digest: Option<String>, |
08195ac8 | 185 | ) -> Result<(), Error> { |
4beb7d2d | 186 | let _lock = pbs_config::remote::lock_config()?; |
347834df | 187 | |
6afdda88 | 188 | let (mut config, expected_digest) = pbs_config::remote::config()?; |
002a191a DM |
189 | |
190 | if let Some(ref digest) = digest { | |
25877d05 | 191 | let digest = <[u8; 32]>::from_hex(digest)?; |
002a191a DM |
192 | crate::tools::detect_modified_configuration_file(&digest, &expected_digest)?; |
193 | } | |
08195ac8 | 194 | |
6afdda88 | 195 | let mut data: Remote = config.lookup("remote", &name)?; |
08195ac8 | 196 | |
5211705f DM |
197 | if let Some(delete) = delete { |
198 | for delete_prop in delete { | |
199 | match delete_prop { | |
dc7a5b34 TL |
200 | DeletableProperty::comment => { |
201 | data.config.comment = None; | |
202 | } | |
203 | DeletableProperty::fingerprint => { | |
204 | data.config.fingerprint = None; | |
205 | } | |
206 | DeletableProperty::port => { | |
207 | data.config.port = None; | |
208 | } | |
5211705f DM |
209 | } |
210 | } | |
211 | } | |
212 | ||
97dfc62f | 213 | if let Some(comment) = update.comment { |
08195ac8 DM |
214 | let comment = comment.trim().to_string(); |
215 | if comment.is_empty() { | |
97dfc62f | 216 | data.config.comment = None; |
08195ac8 | 217 | } else { |
97dfc62f | 218 | data.config.comment = Some(comment); |
08195ac8 DM |
219 | } |
220 | } | |
dc7a5b34 TL |
221 | if let Some(host) = update.host { |
222 | data.config.host = host; | |
223 | } | |
224 | if update.port.is_some() { | |
225 | data.config.port = update.port; | |
226 | } | |
227 | if let Some(auth_id) = update.auth_id { | |
228 | data.config.auth_id = auth_id; | |
229 | } | |
230 | if let Some(password) = password { | |
231 | data.password = password; | |
232 | } | |
08195ac8 | 233 | |
dc7a5b34 TL |
234 | if update.fingerprint.is_some() { |
235 | data.config.fingerprint = update.fingerprint; | |
236 | } | |
6afbe1d8 | 237 | |
08195ac8 DM |
238 | config.set_data(&name, "remote", &data)?; |
239 | ||
6afdda88 | 240 | pbs_config::remote::save_config(&config)?; |
08195ac8 DM |
241 | |
242 | Ok(()) | |
243 | } | |
244 | ||
141304d6 | 245 | #[api( |
688fbe07 | 246 | protected: true, |
141304d6 DM |
247 | input: { |
248 | properties: { | |
249 | name: { | |
167971ed | 250 | schema: REMOTE_ID_SCHEMA, |
141304d6 | 251 | }, |
99f443c6 DC |
252 | digest: { |
253 | optional: true, | |
254 | schema: PROXMOX_CONFIG_DIGEST_SCHEMA, | |
255 | }, | |
141304d6 DM |
256 | }, |
257 | }, | |
70e5f246 | 258 | access: { |
8247db5b | 259 | permission: &Permission::Privilege(&["remote", "{name}"], PRIV_REMOTE_MODIFY, false), |
70e5f246 | 260 | }, |
141304d6 DM |
261 | )] |
262 | /// Remove a remote from the configuration file. | |
99f443c6 | 263 | pub fn delete_remote(name: String, digest: Option<String>) -> Result<(), Error> { |
2791318f DM |
264 | let (sync_jobs, _) = sync::config()?; |
265 | ||
dc7a5b34 | 266 | let job_list: Vec<SyncJobConfig> = sync_jobs.convert_to_typed_array("sync")?; |
2791318f DM |
267 | for job in job_list { |
268 | if job.remote == name { | |
dc7a5b34 TL |
269 | param_bail!( |
270 | "name", | |
271 | "remote '{}' is used by sync job '{}' (datastore '{}')", | |
272 | name, | |
273 | job.id, | |
274 | job.store | |
275 | ); | |
2791318f DM |
276 | } |
277 | } | |
278 | ||
4beb7d2d | 279 | let _lock = pbs_config::remote::lock_config()?; |
141304d6 | 280 | |
6afdda88 | 281 | let (mut config, expected_digest) = pbs_config::remote::config()?; |
99f443c6 DC |
282 | |
283 | if let Some(ref digest) = digest { | |
25877d05 | 284 | let digest = <[u8; 32]>::from_hex(digest)?; |
99f443c6 DC |
285 | crate::tools::detect_modified_configuration_file(&digest, &expected_digest)?; |
286 | } | |
141304d6 DM |
287 | |
288 | match config.sections.get(&name) { | |
dc7a5b34 TL |
289 | Some(_) => { |
290 | config.sections.remove(&name); | |
291 | } | |
dcd1518e | 292 | None => http_bail!(NOT_FOUND, "remote '{}' does not exist.", name), |
141304d6 DM |
293 | } |
294 | ||
6afdda88 | 295 | pbs_config::remote::save_config(&config)?; |
98574722 | 296 | |
141304d6 DM |
297 | Ok(()) |
298 | } | |
299 | ||
e0100d61 | 300 | /// Helper to get client for remote.cfg entry |
2d5287fb DM |
301 | pub async fn remote_client( |
302 | remote: &Remote, | |
303 | limit: Option<RateLimitConfig>, | |
304 | ) -> Result<HttpClient, Error> { | |
dc7a5b34 TL |
305 | let mut options = HttpClientOptions::new_non_interactive( |
306 | remote.password.clone(), | |
307 | remote.config.fingerprint.clone(), | |
308 | ); | |
2d5287fb DM |
309 | |
310 | if let Some(limit) = limit { | |
311 | options = options.rate_limit(limit); | |
312 | } | |
e0100d61 FG |
313 | |
314 | let client = HttpClient::new( | |
97dfc62f DM |
315 | &remote.config.host, |
316 | remote.config.port.unwrap_or(8007), | |
317 | &remote.config.auth_id, | |
dc7a5b34 TL |
318 | options, |
319 | )?; | |
320 | let _auth_info = client | |
321 | .login() // make sure we can auth | |
e0100d61 | 322 | .await |
dc7a5b34 TL |
323 | .map_err(|err| { |
324 | format_err!( | |
325 | "remote connection to '{}' failed - {}", | |
326 | remote.config.host, | |
327 | err | |
328 | ) | |
329 | })?; | |
e0100d61 FG |
330 | |
331 | Ok(client) | |
332 | } | |
333 | ||
e0100d61 FG |
334 | #[api( |
335 | input: { | |
336 | properties: { | |
337 | name: { | |
338 | schema: REMOTE_ID_SCHEMA, | |
339 | }, | |
340 | }, | |
341 | }, | |
342 | access: { | |
343 | permission: &Permission::Privilege(&["remote", "{name}"], PRIV_REMOTE_AUDIT, false), | |
344 | }, | |
345 | returns: { | |
346 | description: "List the accessible datastores.", | |
347 | type: Array, | |
9b93c620 | 348 | items: { type: DataStoreListItem }, |
e0100d61 FG |
349 | }, |
350 | )] | |
351 | /// List datastores of a remote.cfg entry | |
352 | pub async fn scan_remote_datastores(name: String) -> Result<Vec<DataStoreListItem>, Error> { | |
6afdda88 DM |
353 | let (remote_config, _digest) = pbs_config::remote::config()?; |
354 | let remote: Remote = remote_config.lookup("remote", &name)?; | |
e0100d61 FG |
355 | |
356 | let map_remote_err = |api_err| { | |
dc7a5b34 TL |
357 | http_err!( |
358 | INTERNAL_SERVER_ERROR, | |
359 | "failed to scan remote '{}' - {}", | |
360 | &name, | |
361 | api_err | |
362 | ) | |
e0100d61 FG |
363 | }; |
364 | ||
dc7a5b34 | 365 | let client = remote_client(&remote, None).await.map_err(map_remote_err)?; |
e0100d61 FG |
366 | let api_res = client |
367 | .get("api2/json/admin/datastore", None) | |
368 | .await | |
369 | .map_err(map_remote_err)?; | |
370 | let parse_res = match api_res.get("data") { | |
371 | Some(data) => serde_json::from_value::<Vec<DataStoreListItem>>(data.to_owned()), | |
372 | None => bail!("remote {} did not return any datastore list data", &name), | |
373 | }; | |
374 | ||
375 | match parse_res { | |
376 | Ok(parsed) => Ok(parsed), | |
377 | Err(_) => bail!("Failed to parse remote scan api result."), | |
378 | } | |
379 | } | |
380 | ||
1d9bc184 FG |
381 | #[api( |
382 | input: { | |
383 | properties: { | |
384 | name: { | |
385 | schema: REMOTE_ID_SCHEMA, | |
386 | }, | |
387 | store: { | |
388 | schema: DATASTORE_SCHEMA, | |
389 | }, | |
390 | }, | |
391 | }, | |
392 | access: { | |
393 | permission: &Permission::Privilege(&["remote", "{name}"], PRIV_REMOTE_AUDIT, false), | |
394 | }, | |
395 | returns: { | |
396 | description: "Lists the accessible backup groups in a remote datastore.", | |
397 | type: Array, | |
398 | items: { type: GroupListItem }, | |
399 | }, | |
400 | )] | |
401 | /// List groups of a remote.cfg entry's datastore | |
402 | pub async fn scan_remote_groups(name: String, store: String) -> Result<Vec<GroupListItem>, Error> { | |
403 | let (remote_config, _digest) = pbs_config::remote::config()?; | |
404 | let remote: Remote = remote_config.lookup("remote", &name)?; | |
405 | ||
406 | let map_remote_err = |api_err| { | |
dc7a5b34 TL |
407 | http_err!( |
408 | INTERNAL_SERVER_ERROR, | |
409 | "failed to scan remote '{}' - {}", | |
410 | &name, | |
411 | api_err | |
412 | ) | |
1d9bc184 FG |
413 | }; |
414 | ||
dc7a5b34 | 415 | let client = remote_client(&remote, None).await.map_err(map_remote_err)?; |
1d9bc184 FG |
416 | let api_res = client |
417 | .get(&format!("api2/json/admin/datastore/{}/groups", store), None) | |
418 | .await | |
419 | .map_err(map_remote_err)?; | |
420 | let parse_res = match api_res.get("data") { | |
421 | Some(data) => serde_json::from_value::<Vec<GroupListItem>>(data.to_owned()), | |
422 | None => bail!("remote {} did not return any group list data", &name), | |
423 | }; | |
424 | ||
425 | match parse_res { | |
426 | Ok(parsed) => Ok(parsed), | |
427 | Err(_) => bail!("Failed to parse remote scan api result."), | |
428 | } | |
429 | } | |
430 | ||
431 | #[sortable] | |
dc7a5b34 TL |
432 | const DATASTORE_SCAN_SUBDIRS: SubdirMap = |
433 | &[("groups", &Router::new().get(&API_METHOD_SCAN_REMOTE_GROUPS))]; | |
1d9bc184 FG |
434 | |
435 | const DATASTORE_SCAN_ROUTER: Router = Router::new() | |
436 | .get(&list_subdirs_api_method!(DATASTORE_SCAN_SUBDIRS)) | |
437 | .subdirs(DATASTORE_SCAN_SUBDIRS); | |
438 | ||
e0100d61 | 439 | const SCAN_ROUTER: Router = Router::new() |
1d9bc184 FG |
440 | .get(&API_METHOD_SCAN_REMOTE_DATASTORES) |
441 | .match_all("store", &DATASTORE_SCAN_ROUTER); | |
e0100d61 | 442 | |
08195ac8 DM |
443 | const ITEM_ROUTER: Router = Router::new() |
444 | .get(&API_METHOD_READ_REMOTE) | |
445 | .put(&API_METHOD_UPDATE_REMOTE) | |
e0100d61 FG |
446 | .delete(&API_METHOD_DELETE_REMOTE) |
447 | .subdirs(&[("scan", &SCAN_ROUTER)]); | |
08195ac8 | 448 | |
141304d6 DM |
449 | pub const ROUTER: Router = Router::new() |
450 | .get(&API_METHOD_LIST_REMOTES) | |
451 | .post(&API_METHOD_CREATE_REMOTE) | |
08195ac8 | 452 | .match_all("name", &ITEM_ROUTER); |