]> git.proxmox.com Git - proxmox-backup.git/blob - src/api2/config/remote.rs
fix non-camel-case enums
[proxmox-backup.git] / src / api2 / config / remote.rs
1 use ::serde::{Deserialize, Serialize};
2 use anyhow::{bail, format_err, Error};
3 use hex::FromHex;
4 use pbs_api_types::BackupNamespace;
5 use pbs_api_types::NamespaceListItem;
6 use proxmox_router::list_subdirs_api_method;
7 use proxmox_router::SubdirMap;
8 use proxmox_sys::sortable;
9 use serde_json::Value;
10
11 use proxmox_router::{http_bail, http_err, ApiMethod, Permission, Router, RpcEnvironment};
12 use proxmox_schema::{api, param_bail};
13
14 use pbs_api_types::{
15 Authid, DataStoreListItem, GroupListItem, RateLimitConfig, Remote, RemoteConfig,
16 RemoteConfigUpdater, RemoteWithoutPassword, SyncJobConfig, DATASTORE_SCHEMA, PRIV_REMOTE_AUDIT,
17 PRIV_REMOTE_MODIFY, PROXMOX_CONFIG_DIGEST_SCHEMA, REMOTE_ID_SCHEMA, REMOTE_PASSWORD_SCHEMA,
18 };
19 use pbs_client::{HttpClient, HttpClientOptions};
20 use pbs_config::sync;
21
22 use pbs_config::CachedUserInfo;
23 use serde_json::json;
24
25 #[api(
26 input: {
27 properties: {},
28 },
29 returns: {
30 description: "The list of configured remotes (with config digest).",
31 type: Array,
32 items: { type: RemoteWithoutPassword },
33 },
34 access: {
35 description: "List configured remotes filtered by Remote.Audit privileges",
36 permission: &Permission::Anybody,
37 },
38 )]
39 /// List all remotes
40 pub fn list_remotes(
41 _param: Value,
42 _info: &ApiMethod,
43 rpcenv: &mut dyn RpcEnvironment,
44 ) -> Result<Vec<RemoteWithoutPassword>, Error> {
45 let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
46 let user_info = CachedUserInfo::new()?;
47
48 let (config, digest) = pbs_config::remote::config()?;
49
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")?;
52
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
61 rpcenv["digest"] = hex::encode(digest).into();
62 Ok(list)
63 }
64
65 #[api(
66 protected: true,
67 input: {
68 properties: {
69 name: {
70 schema: REMOTE_ID_SCHEMA,
71 },
72 config: {
73 type: RemoteConfig,
74 flatten: true,
75 },
76 password: {
77 // We expect the plain password here (not base64 encoded)
78 schema: REMOTE_PASSWORD_SCHEMA,
79 },
80 },
81 },
82 access: {
83 permission: &Permission::Privilege(&["remote"], PRIV_REMOTE_MODIFY, false),
84 },
85 )]
86 /// Create new remote.
87 pub fn create_remote(name: String, config: RemoteConfig, password: String) -> Result<(), Error> {
88 let _lock = pbs_config::remote::lock_config()?;
89
90 let (mut section_config, _digest) = pbs_config::remote::config()?;
91
92 if section_config.sections.get(&name).is_some() {
93 param_bail!("name", "remote '{}' already exists.", name);
94 }
95
96 let remote = Remote {
97 name: name.clone(),
98 config,
99 password,
100 };
101
102 section_config.set_data(&name, "remote", &remote)?;
103
104 pbs_config::remote::save_config(&section_config)?;
105
106 Ok(())
107 }
108
109 #[api(
110 input: {
111 properties: {
112 name: {
113 schema: REMOTE_ID_SCHEMA,
114 },
115 },
116 },
117 returns: { type: RemoteWithoutPassword },
118 access: {
119 permission: &Permission::Privilege(&["remote", "{name}"], PRIV_REMOTE_AUDIT, false),
120 }
121 )]
122 /// Read remote configuration data.
123 pub fn read_remote(
124 name: String,
125 _info: &ApiMethod,
126 rpcenv: &mut dyn RpcEnvironment,
127 ) -> Result<RemoteWithoutPassword, Error> {
128 let (config, digest) = pbs_config::remote::config()?;
129 let data: RemoteWithoutPassword = config.lookup("remote", &name)?;
130 rpcenv["digest"] = hex::encode(digest).into();
131 Ok(data)
132 }
133
134 #[api()]
135 #[derive(Serialize, Deserialize)]
136 /// Deletable property name
137 pub enum DeletableProperty {
138 /// Delete the comment property.
139 Comment,
140 /// Delete the fingerprint property.
141 Fingerprint,
142 /// Delete the port property.
143 Port,
144 }
145
146 #[api(
147 protected: true,
148 input: {
149 properties: {
150 name: {
151 schema: REMOTE_ID_SCHEMA,
152 },
153 update: {
154 type: RemoteConfigUpdater,
155 flatten: true,
156 },
157 password: {
158 // We expect the plain password here (not base64 encoded)
159 optional: true,
160 schema: REMOTE_PASSWORD_SCHEMA,
161 },
162 delete: {
163 description: "List of properties to delete.",
164 type: Array,
165 optional: true,
166 items: {
167 type: DeletableProperty,
168 }
169 },
170 digest: {
171 optional: true,
172 schema: PROXMOX_CONFIG_DIGEST_SCHEMA,
173 },
174 },
175 },
176 access: {
177 permission: &Permission::Privilege(&["remote", "{name}"], PRIV_REMOTE_MODIFY, false),
178 },
179 )]
180 /// Update remote configuration.
181 pub fn update_remote(
182 name: String,
183 update: RemoteConfigUpdater,
184 password: Option<String>,
185 delete: Option<Vec<DeletableProperty>>,
186 digest: Option<String>,
187 ) -> Result<(), Error> {
188 let _lock = pbs_config::remote::lock_config()?;
189
190 let (mut config, expected_digest) = pbs_config::remote::config()?;
191
192 if let Some(ref digest) = digest {
193 let digest = <[u8; 32]>::from_hex(digest)?;
194 crate::tools::detect_modified_configuration_file(&digest, &expected_digest)?;
195 }
196
197 let mut data: Remote = config.lookup("remote", &name)?;
198
199 if let Some(delete) = delete {
200 for delete_prop in delete {
201 match delete_prop {
202 DeletableProperty::Comment => {
203 data.config.comment = None;
204 }
205 DeletableProperty::Fingerprint => {
206 data.config.fingerprint = None;
207 }
208 DeletableProperty::Port => {
209 data.config.port = None;
210 }
211 }
212 }
213 }
214
215 if let Some(comment) = update.comment {
216 let comment = comment.trim().to_string();
217 if comment.is_empty() {
218 data.config.comment = None;
219 } else {
220 data.config.comment = Some(comment);
221 }
222 }
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 }
235
236 if update.fingerprint.is_some() {
237 data.config.fingerprint = update.fingerprint;
238 }
239
240 config.set_data(&name, "remote", &data)?;
241
242 pbs_config::remote::save_config(&config)?;
243
244 Ok(())
245 }
246
247 #[api(
248 protected: true,
249 input: {
250 properties: {
251 name: {
252 schema: REMOTE_ID_SCHEMA,
253 },
254 digest: {
255 optional: true,
256 schema: PROXMOX_CONFIG_DIGEST_SCHEMA,
257 },
258 },
259 },
260 access: {
261 permission: &Permission::Privilege(&["remote", "{name}"], PRIV_REMOTE_MODIFY, false),
262 },
263 )]
264 /// Remove a remote from the configuration file.
265 pub fn delete_remote(name: String, digest: Option<String>) -> Result<(), Error> {
266 let (sync_jobs, _) = sync::config()?;
267
268 let job_list: Vec<SyncJobConfig> = sync_jobs.convert_to_typed_array("sync")?;
269 for job in job_list {
270 if job.remote == name {
271 param_bail!(
272 "name",
273 "remote '{}' is used by sync job '{}' (datastore '{}')",
274 name,
275 job.id,
276 job.store
277 );
278 }
279 }
280
281 let _lock = pbs_config::remote::lock_config()?;
282
283 let (mut config, expected_digest) = pbs_config::remote::config()?;
284
285 if let Some(ref digest) = digest {
286 let digest = <[u8; 32]>::from_hex(digest)?;
287 crate::tools::detect_modified_configuration_file(&digest, &expected_digest)?;
288 }
289
290 match config.sections.get(&name) {
291 Some(_) => {
292 config.sections.remove(&name);
293 }
294 None => http_bail!(NOT_FOUND, "remote '{}' does not exist.", name),
295 }
296
297 pbs_config::remote::save_config(&config)?;
298
299 Ok(())
300 }
301
302 /// Helper to get client for remote.cfg entry
303 pub async fn remote_client(
304 remote: &Remote,
305 limit: Option<RateLimitConfig>,
306 ) -> Result<HttpClient, Error> {
307 let mut options = HttpClientOptions::new_non_interactive(
308 remote.password.clone(),
309 remote.config.fingerprint.clone(),
310 );
311
312 if let Some(limit) = limit {
313 options = options.rate_limit(limit);
314 }
315
316 let client = HttpClient::new(
317 &remote.config.host,
318 remote.config.port.unwrap_or(8007),
319 &remote.config.auth_id,
320 options,
321 )?;
322 let _auth_info = client
323 .login() // make sure we can auth
324 .await
325 .map_err(|err| {
326 format_err!(
327 "remote connection to '{}' failed - {}",
328 remote.config.host,
329 err
330 )
331 })?;
332
333 Ok(client)
334 }
335
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,
350 items: { type: DataStoreListItem },
351 },
352 )]
353 /// List datastores of a remote.cfg entry
354 pub async fn scan_remote_datastores(name: String) -> Result<Vec<DataStoreListItem>, Error> {
355 let (remote_config, _digest) = pbs_config::remote::config()?;
356 let remote: Remote = remote_config.lookup("remote", &name)?;
357
358 let map_remote_err = |api_err| {
359 http_err!(
360 INTERNAL_SERVER_ERROR,
361 "failed to scan remote '{}' - {}",
362 &name,
363 api_err
364 )
365 };
366
367 let client = remote_client(&remote, None).await.map_err(map_remote_err)?;
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
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 },
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 },
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
464 pub async fn scan_remote_groups(
465 name: String,
466 store: String,
467 namespace: Option<BackupNamespace>,
468 ) -> Result<Vec<GroupListItem>, Error> {
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| {
473 http_err!(
474 INTERNAL_SERVER_ERROR,
475 "failed to scan remote '{}' - {}",
476 &name,
477 api_err
478 )
479 };
480
481 let client = remote_client(&remote, None).await.map_err(map_remote_err)?;
482
483 let args = namespace.map(|ns| json!({ "ns": ns }));
484
485 let api_res = client
486 .get(&format!("api2/json/admin/datastore/{}/groups", store), args)
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]
501 const DATASTORE_SCAN_SUBDIRS: SubdirMap = &sorted!([
502 ("groups", &Router::new().get(&API_METHOD_SCAN_REMOTE_GROUPS)),
503 (
504 "namespaces",
505 &Router::new().get(&API_METHOD_SCAN_REMOTE_NAMESPACES),
506 ),
507 ]);
508
509 const DATASTORE_SCAN_ROUTER: Router = Router::new()
510 .get(&list_subdirs_api_method!(DATASTORE_SCAN_SUBDIRS))
511 .subdirs(DATASTORE_SCAN_SUBDIRS);
512
513 const SCAN_ROUTER: Router = Router::new()
514 .get(&API_METHOD_SCAN_REMOTE_DATASTORES)
515 .match_all("store", &DATASTORE_SCAN_ROUTER);
516
517 const ITEM_ROUTER: Router = Router::new()
518 .get(&API_METHOD_READ_REMOTE)
519 .put(&API_METHOD_UPDATE_REMOTE)
520 .delete(&API_METHOD_DELETE_REMOTE)
521 .subdirs(&[("scan", &SCAN_ROUTER)]);
522
523 pub const ROUTER: Router = Router::new()
524 .get(&API_METHOD_LIST_REMOTES)
525 .post(&API_METHOD_CREATE_REMOTE)
526 .match_all("name", &ITEM_ROUTER);