1 use anyhow
::{bail, format_err, Error}
;
2 use proxmox_sys
::sortable
;
3 use proxmox_router
::SubdirMap
;
4 use proxmox_router
::list_subdirs_api_method
;
6 use ::serde
::{Deserialize, Serialize}
;
9 use proxmox_router
::{http_err, ApiMethod, Router, RpcEnvironment, Permission}
;
10 use proxmox_schema
::{api, param_bail}
;
12 use pbs_client
::{HttpClient, HttpClientOptions}
;
14 REMOTE_ID_SCHEMA
, REMOTE_PASSWORD_SCHEMA
, Remote
, RemoteConfig
, RemoteConfigUpdater
,
15 Authid
, PROXMOX_CONFIG_DIGEST_SCHEMA
, DATASTORE_SCHEMA
, GroupListItem
,
16 DataStoreListItem
, RateLimitConfig
, SyncJobConfig
, PRIV_REMOTE_AUDIT
, PRIV_REMOTE_MODIFY
,
20 use pbs_config
::CachedUserInfo
;
27 description
: "The list of configured remotes (with config digest).",
29 items
: { type: Remote }
,
32 description
: "List configured remotes filtered by Remote.Audit privileges",
33 permission
: &Permission
::Anybody
,
40 mut rpcenv
: &mut dyn RpcEnvironment
,
41 ) -> Result
<Vec
<Remote
>, Error
> {
42 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
43 let user_info
= CachedUserInfo
::new()?
;
45 let (config
, digest
) = pbs_config
::remote
::config()?
;
47 let mut list
: Vec
<Remote
> = config
.convert_to_typed_array("remote")?
;
48 // don't return password in api
49 for remote
in &mut list
{
50 remote
.password
= "".to_string();
56 let privs
= user_info
.lookup_privs(&auth_id
, &["remote", &remote
.name
]);
57 privs
& PRIV_REMOTE_AUDIT
!= 0
61 rpcenv
["digest"] = hex
::encode(&digest
).into();
70 schema
: REMOTE_ID_SCHEMA
,
77 // We expect the plain password here (not base64 encoded)
78 schema
: REMOTE_PASSWORD_SCHEMA
,
83 permission
: &Permission
::Privilege(&["remote"], PRIV_REMOTE_MODIFY
, false),
86 /// Create new remote.
91 ) -> Result
<(), Error
> {
93 let _lock
= pbs_config
::remote
::lock_config()?
;
95 let (mut section_config
, _digest
) = pbs_config
::remote
::config()?
;
97 if section_config
.sections
.get(&name
).is_some() {
98 param_bail
!("name", "remote '{}' already exists.", name
);
101 let remote
= Remote { name: name.clone(), config, password }
;
103 section_config
.set_data(&name
, "remote", &remote
)?
;
105 pbs_config
::remote
::save_config(§ion_config
)?
;
114 schema
: REMOTE_ID_SCHEMA
,
118 returns
: { type: Remote }
,
120 permission
: &Permission
::Privilege(&["remote", "{name}"], PRIV_REMOTE_AUDIT
, false),
123 /// Read remote configuration data.
127 mut rpcenv
: &mut dyn RpcEnvironment
,
128 ) -> Result
<Remote
, Error
> {
129 let (config
, digest
) = pbs_config
::remote
::config()?
;
130 let mut data
: Remote
= config
.lookup("remote", &name
)?
;
131 data
.password
= "".to_string(); // do not return password in api
132 rpcenv
["digest"] = hex
::encode(&digest
).into();
137 #[derive(Serialize, Deserialize)]
138 #[allow(non_camel_case_types)]
139 /// Deletable property name
140 pub enum DeletableProperty
{
141 /// Delete the comment property.
143 /// Delete the fingerprint property.
145 /// Delete the port property.
154 schema
: REMOTE_ID_SCHEMA
,
157 type: RemoteConfigUpdater
,
161 // We expect the plain password here (not base64 encoded)
163 schema
: REMOTE_PASSWORD_SCHEMA
,
166 description
: "List of properties to delete.",
170 type: DeletableProperty
,
175 schema
: PROXMOX_CONFIG_DIGEST_SCHEMA
,
180 permission
: &Permission
::Privilege(&["remote", "{name}"], PRIV_REMOTE_MODIFY
, false),
183 /// Update remote configuration.
184 pub fn update_remote(
186 update
: RemoteConfigUpdater
,
187 password
: Option
<String
>,
188 delete
: Option
<Vec
<DeletableProperty
>>,
189 digest
: Option
<String
>,
190 ) -> Result
<(), Error
> {
192 let _lock
= pbs_config
::remote
::lock_config()?
;
194 let (mut config
, expected_digest
) = pbs_config
::remote
::config()?
;
196 if let Some(ref digest
) = digest
{
197 let digest
= <[u8; 32]>::from_hex(digest
)?
;
198 crate::tools
::detect_modified_configuration_file(&digest
, &expected_digest
)?
;
201 let mut data
: Remote
= config
.lookup("remote", &name
)?
;
203 if let Some(delete
) = delete
{
204 for delete_prop
in delete
{
206 DeletableProperty
::comment
=> { data.config.comment = None; }
,
207 DeletableProperty
::fingerprint
=> { data.config.fingerprint = None; }
,
208 DeletableProperty
::port
=> { data.config.port = None; }
,
213 if let Some(comment
) = update
.comment
{
214 let comment
= comment
.trim().to_string();
215 if comment
.is_empty() {
216 data
.config
.comment
= None
;
218 data
.config
.comment
= Some(comment
);
221 if let Some(host
) = update
.host { data.config.host = host; }
222 if update
.port
.is_some() { data.config.port = update.port; }
223 if let Some(auth_id
) = update
.auth_id { data.config.auth_id = auth_id; }
224 if let Some(password
) = password { data.password = password; }
226 if update
.fingerprint
.is_some() { data.config.fingerprint = update.fingerprint; }
228 config
.set_data(&name
, "remote", &data
)?
;
230 pbs_config
::remote
::save_config(&config
)?
;
240 schema
: REMOTE_ID_SCHEMA
,
244 schema
: PROXMOX_CONFIG_DIGEST_SCHEMA
,
249 permission
: &Permission
::Privilege(&["remote", "{name}"], PRIV_REMOTE_MODIFY
, false),
252 /// Remove a remote from the configuration file.
253 pub fn delete_remote(name
: String
, digest
: Option
<String
>) -> Result
<(), Error
> {
255 let (sync_jobs
, _
) = sync
::config()?
;
257 let job_list
: Vec
<SyncJobConfig
> = sync_jobs
.convert_to_typed_array("sync")?
;
258 for job
in job_list
{
259 if job
.remote
== name
{
260 param_bail
!("name", "remote '{}' is used by sync job '{}' (datastore '{}')", name
, job
.id
, job
.store
);
264 let _lock
= pbs_config
::remote
::lock_config()?
;
266 let (mut config
, expected_digest
) = pbs_config
::remote
::config()?
;
268 if let Some(ref digest
) = digest
{
269 let digest
= <[u8; 32]>::from_hex(digest
)?
;
270 crate::tools
::detect_modified_configuration_file(&digest
, &expected_digest
)?
;
273 match config
.sections
.get(&name
) {
274 Some(_
) => { config.sections.remove(&name); }
,
275 None
=> bail
!("remote '{}' does not exist.", name
),
278 pbs_config
::remote
::save_config(&config
)?
;
283 /// Helper to get client for remote.cfg entry
284 pub async
fn remote_client(
286 limit
: Option
<RateLimitConfig
>,
287 ) -> Result
<HttpClient
, Error
> {
288 let mut options
= HttpClientOptions
::new_non_interactive(remote
.password
.clone(), remote
.config
.fingerprint
.clone());
290 if let Some(limit
) = limit
{
291 options
= options
.rate_limit(limit
);
294 let client
= HttpClient
::new(
296 remote
.config
.port
.unwrap_or(8007),
297 &remote
.config
.auth_id
,
299 let _auth_info
= client
.login() // make sure we can auth
301 .map_err(|err
| format_err
!("remote connection to '{}' failed - {}", remote
.config
.host
, err
))?
;
311 schema
: REMOTE_ID_SCHEMA
,
316 permission
: &Permission
::Privilege(&["remote", "{name}"], PRIV_REMOTE_AUDIT
, false),
319 description
: "List the accessible datastores.",
321 items
: { type: DataStoreListItem }
,
324 /// List datastores of a remote.cfg entry
325 pub async
fn scan_remote_datastores(name
: String
) -> Result
<Vec
<DataStoreListItem
>, Error
> {
326 let (remote_config
, _digest
) = pbs_config
::remote
::config()?
;
327 let remote
: Remote
= remote_config
.lookup("remote", &name
)?
;
329 let map_remote_err
= |api_err
| {
330 http_err
!(INTERNAL_SERVER_ERROR
,
331 "failed to scan remote '{}' - {}",
336 let client
= remote_client(&remote
, None
)
338 .map_err(map_remote_err
)?
;
340 .get("api2/json/admin/datastore", None
)
342 .map_err(map_remote_err
)?
;
343 let parse_res
= match api_res
.get("data") {
344 Some(data
) => serde_json
::from_value
::<Vec
<DataStoreListItem
>>(data
.to_owned()),
345 None
=> bail
!("remote {} did not return any datastore list data", &name
),
349 Ok(parsed
) => Ok(parsed
),
350 Err(_
) => bail
!("Failed to parse remote scan api result."),
358 schema
: REMOTE_ID_SCHEMA
,
361 schema
: DATASTORE_SCHEMA
,
366 permission
: &Permission
::Privilege(&["remote", "{name}"], PRIV_REMOTE_AUDIT
, false),
369 description
: "Lists the accessible backup groups in a remote datastore.",
371 items
: { type: GroupListItem }
,
374 /// List groups of a remote.cfg entry's datastore
375 pub async
fn scan_remote_groups(name
: String
, store
: String
) -> Result
<Vec
<GroupListItem
>, Error
> {
376 let (remote_config
, _digest
) = pbs_config
::remote
::config()?
;
377 let remote
: Remote
= remote_config
.lookup("remote", &name
)?
;
379 let map_remote_err
= |api_err
| {
380 http_err
!(INTERNAL_SERVER_ERROR
,
381 "failed to scan remote '{}' - {}",
386 let client
= remote_client(&remote
, None
)
388 .map_err(map_remote_err
)?
;
390 .get(&format
!("api2/json/admin/datastore/{}/groups", store
), None
)
392 .map_err(map_remote_err
)?
;
393 let parse_res
= match api_res
.get("data") {
394 Some(data
) => serde_json
::from_value
::<Vec
<GroupListItem
>>(data
.to_owned()),
395 None
=> bail
!("remote {} did not return any group list data", &name
),
399 Ok(parsed
) => Ok(parsed
),
400 Err(_
) => bail
!("Failed to parse remote scan api result."),
405 const DATASTORE_SCAN_SUBDIRS
: SubdirMap
= &[
409 .get(&API_METHOD_SCAN_REMOTE_GROUPS
)
413 const DATASTORE_SCAN_ROUTER
: Router
= Router
::new()
414 .get(&list_subdirs_api_method
!(DATASTORE_SCAN_SUBDIRS
))
415 .subdirs(DATASTORE_SCAN_SUBDIRS
);
417 const SCAN_ROUTER
: Router
= Router
::new()
418 .get(&API_METHOD_SCAN_REMOTE_DATASTORES
)
419 .match_all("store", &DATASTORE_SCAN_ROUTER
);
421 const ITEM_ROUTER
: Router
= Router
::new()
422 .get(&API_METHOD_READ_REMOTE
)
423 .put(&API_METHOD_UPDATE_REMOTE
)
424 .delete(&API_METHOD_DELETE_REMOTE
)
425 .subdirs(&[("scan", &SCAN_ROUTER
)]);
427 pub const ROUTER
: Router
= Router
::new()
428 .get(&API_METHOD_LIST_REMOTES
)
429 .post(&API_METHOD_CREATE_REMOTE
)
430 .match_all("name", &ITEM_ROUTER
);