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