1 use anyhow
::{bail, format_err, Error}
;
3 use proxmox_router
::SubdirMap
;
4 use proxmox_router
::list_subdirs_api_method
;
6 use ::serde
::{Deserialize, Serialize}
;
8 use proxmox_router
::{http_err, ApiMethod, Router, RpcEnvironment, Permission}
;
9 use proxmox_schema
::api
;
11 use pbs_client
::{HttpClient, HttpClientOptions}
;
13 REMOTE_ID_SCHEMA
, REMOTE_PASSWORD_SCHEMA
, Remote
, RemoteConfig
, RemoteConfigUpdater
,
14 Authid
, PROXMOX_CONFIG_DIGEST_SCHEMA
, DATASTORE_SCHEMA
, GroupListItem
,
15 DataStoreListItem
, RateLimitConfig
, SyncJobConfig
, PRIV_REMOTE_AUDIT
, PRIV_REMOTE_MODIFY
,
19 use pbs_config
::CachedUserInfo
;
26 description
: "The list of configured remotes (with config digest).",
28 items
: { type: Remote }
,
31 description
: "List configured remotes filtered by Remote.Audit privileges",
32 permission
: &Permission
::Anybody
,
39 mut rpcenv
: &mut dyn RpcEnvironment
,
40 ) -> Result
<Vec
<Remote
>, Error
> {
41 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
42 let user_info
= CachedUserInfo
::new()?
;
44 let (config
, digest
) = pbs_config
::remote
::config()?
;
46 let mut list
: Vec
<Remote
> = config
.convert_to_typed_array("remote")?
;
47 // don't return password in api
48 for remote
in &mut list
{
49 remote
.password
= "".to_string();
55 let privs
= user_info
.lookup_privs(&auth_id
, &["remote", &remote
.name
]);
56 privs
& PRIV_REMOTE_AUDIT
!= 0
60 rpcenv
["digest"] = proxmox
::tools
::digest_to_hex(&digest
).into();
69 schema
: REMOTE_ID_SCHEMA
,
76 // We expect the plain password here (not base64 encoded)
77 schema
: REMOTE_PASSWORD_SCHEMA
,
82 permission
: &Permission
::Privilege(&["remote"], PRIV_REMOTE_MODIFY
, false),
85 /// Create new remote.
90 ) -> Result
<(), Error
> {
92 let _lock
= pbs_config
::remote
::lock_config()?
;
94 let (mut section_config
, _digest
) = pbs_config
::remote
::config()?
;
96 if section_config
.sections
.get(&name
).is_some() {
97 bail
!("remote '{}' already exists.", name
);
100 let remote
= Remote { name: name.clone(), config, password }
;
102 section_config
.set_data(&name
, "remote", &remote
)?
;
104 pbs_config
::remote
::save_config(§ion_config
)?
;
113 schema
: REMOTE_ID_SCHEMA
,
117 returns
: { type: Remote }
,
119 permission
: &Permission
::Privilege(&["remote", "{name}"], PRIV_REMOTE_AUDIT
, false),
122 /// Read remote configuration data.
126 mut rpcenv
: &mut dyn RpcEnvironment
,
127 ) -> Result
<Remote
, Error
> {
128 let (config
, digest
) = pbs_config
::remote
::config()?
;
129 let mut data
: Remote
= config
.lookup("remote", &name
)?
;
130 data
.password
= "".to_string(); // do not return password in api
131 rpcenv
["digest"] = proxmox
::tools
::digest_to_hex(&digest
).into();
136 #[derive(Serialize, Deserialize)]
137 #[allow(non_camel_case_types)]
138 /// Deletable property name
139 pub enum DeletableProperty
{
140 /// Delete the comment property.
142 /// Delete the fingerprint property.
144 /// Delete the port property.
153 schema
: REMOTE_ID_SCHEMA
,
156 type: RemoteConfigUpdater
,
160 // We expect the plain password here (not base64 encoded)
162 schema
: REMOTE_PASSWORD_SCHEMA
,
165 description
: "List of properties to delete.",
169 type: DeletableProperty
,
174 schema
: PROXMOX_CONFIG_DIGEST_SCHEMA
,
179 permission
: &Permission
::Privilege(&["remote", "{name}"], PRIV_REMOTE_MODIFY
, false),
182 /// Update remote configuration.
183 pub fn update_remote(
185 update
: RemoteConfigUpdater
,
186 password
: Option
<String
>,
187 delete
: Option
<Vec
<DeletableProperty
>>,
188 digest
: Option
<String
>,
189 ) -> Result
<(), Error
> {
191 let _lock
= pbs_config
::remote
::lock_config()?
;
193 let (mut config
, expected_digest
) = pbs_config
::remote
::config()?
;
195 if let Some(ref digest
) = digest
{
196 let digest
= proxmox
::tools
::hex_to_digest(digest
)?
;
197 crate::tools
::detect_modified_configuration_file(&digest
, &expected_digest
)?
;
200 let mut data
: Remote
= config
.lookup("remote", &name
)?
;
202 if let Some(delete
) = delete
{
203 for delete_prop
in delete
{
205 DeletableProperty
::comment
=> { data.config.comment = None; }
,
206 DeletableProperty
::fingerprint
=> { data.config.fingerprint = None; }
,
207 DeletableProperty
::port
=> { data.config.port = None; }
,
212 if let Some(comment
) = update
.comment
{
213 let comment
= comment
.trim().to_string();
214 if comment
.is_empty() {
215 data
.config
.comment
= None
;
217 data
.config
.comment
= Some(comment
);
220 if let Some(host
) = update
.host { data.config.host = host; }
221 if update
.port
.is_some() { data.config.port = update.port; }
222 if let Some(auth_id
) = update
.auth_id { data.config.auth_id = auth_id; }
223 if let Some(password
) = password { data.password = password; }
225 if update
.fingerprint
.is_some() { data.config.fingerprint = update.fingerprint; }
227 config
.set_data(&name
, "remote", &data
)?
;
229 pbs_config
::remote
::save_config(&config
)?
;
239 schema
: REMOTE_ID_SCHEMA
,
243 schema
: PROXMOX_CONFIG_DIGEST_SCHEMA
,
248 permission
: &Permission
::Privilege(&["remote", "{name}"], PRIV_REMOTE_MODIFY
, false),
251 /// Remove a remote from the configuration file.
252 pub fn delete_remote(name
: String
, digest
: Option
<String
>) -> Result
<(), Error
> {
254 let (sync_jobs
, _
) = sync
::config()?
;
256 let job_list
: Vec
<SyncJobConfig
> = sync_jobs
.convert_to_typed_array("sync")?
;
257 for job
in job_list
{
258 if job
.remote
== name
{
259 bail
!("remote '{}' is used by sync job '{}' (datastore '{}')", name
, job
.id
, job
.store
);
263 let _lock
= pbs_config
::remote
::lock_config()?
;
265 let (mut config
, expected_digest
) = pbs_config
::remote
::config()?
;
267 if let Some(ref digest
) = digest
{
268 let digest
= proxmox
::tools
::hex_to_digest(digest
)?
;
269 crate::tools
::detect_modified_configuration_file(&digest
, &expected_digest
)?
;
272 match config
.sections
.get(&name
) {
273 Some(_
) => { config.sections.remove(&name); }
,
274 None
=> bail
!("remote '{}' does not exist.", name
),
277 pbs_config
::remote
::save_config(&config
)?
;
282 /// Helper to get client for remote.cfg entry
283 pub async
fn remote_client(
285 limit
: Option
<RateLimitConfig
>,
286 ) -> Result
<HttpClient
, Error
> {
287 let mut options
= HttpClientOptions
::new_non_interactive(remote
.password
.clone(), remote
.config
.fingerprint
.clone());
289 if let Some(limit
) = limit
{
290 options
= options
.rate_limit(limit
);
293 let client
= HttpClient
::new(
295 remote
.config
.port
.unwrap_or(8007),
296 &remote
.config
.auth_id
,
298 let _auth_info
= client
.login() // make sure we can auth
300 .map_err(|err
| format_err
!("remote connection to '{}' failed - {}", remote
.config
.host
, err
))?
;
310 schema
: REMOTE_ID_SCHEMA
,
315 permission
: &Permission
::Privilege(&["remote", "{name}"], PRIV_REMOTE_AUDIT
, false),
318 description
: "List the accessible datastores.",
320 items
: { type: DataStoreListItem }
,
323 /// List datastores of a remote.cfg entry
324 pub async
fn scan_remote_datastores(name
: String
) -> Result
<Vec
<DataStoreListItem
>, Error
> {
325 let (remote_config
, _digest
) = pbs_config
::remote
::config()?
;
326 let remote
: Remote
= remote_config
.lookup("remote", &name
)?
;
328 let map_remote_err
= |api_err
| {
329 http_err
!(INTERNAL_SERVER_ERROR
,
330 "failed to scan remote '{}' - {}",
335 let client
= remote_client(&remote
, None
)
337 .map_err(map_remote_err
)?
;
339 .get("api2/json/admin/datastore", None
)
341 .map_err(map_remote_err
)?
;
342 let parse_res
= match api_res
.get("data") {
343 Some(data
) => serde_json
::from_value
::<Vec
<DataStoreListItem
>>(data
.to_owned()),
344 None
=> bail
!("remote {} did not return any datastore list data", &name
),
348 Ok(parsed
) => Ok(parsed
),
349 Err(_
) => bail
!("Failed to parse remote scan api result."),
357 schema
: REMOTE_ID_SCHEMA
,
360 schema
: DATASTORE_SCHEMA
,
365 permission
: &Permission
::Privilege(&["remote", "{name}"], PRIV_REMOTE_AUDIT
, false),
368 description
: "Lists the accessible backup groups in a remote datastore.",
370 items
: { type: GroupListItem }
,
373 /// List groups of a remote.cfg entry's datastore
374 pub async
fn scan_remote_groups(name
: String
, store
: String
) -> Result
<Vec
<GroupListItem
>, Error
> {
375 let (remote_config
, _digest
) = pbs_config
::remote
::config()?
;
376 let remote
: Remote
= remote_config
.lookup("remote", &name
)?
;
378 let map_remote_err
= |api_err
| {
379 http_err
!(INTERNAL_SERVER_ERROR
,
380 "failed to scan remote '{}' - {}",
385 let client
= remote_client(&remote
, None
)
387 .map_err(map_remote_err
)?
;
389 .get(&format
!("api2/json/admin/datastore/{}/groups", store
), None
)
391 .map_err(map_remote_err
)?
;
392 let parse_res
= match api_res
.get("data") {
393 Some(data
) => serde_json
::from_value
::<Vec
<GroupListItem
>>(data
.to_owned()),
394 None
=> bail
!("remote {} did not return any group list data", &name
),
398 Ok(parsed
) => Ok(parsed
),
399 Err(_
) => bail
!("Failed to parse remote scan api result."),
404 const DATASTORE_SCAN_SUBDIRS
: SubdirMap
= &[
408 .get(&API_METHOD_SCAN_REMOTE_GROUPS
)
412 const DATASTORE_SCAN_ROUTER
: Router
= Router
::new()
413 .get(&list_subdirs_api_method
!(DATASTORE_SCAN_SUBDIRS
))
414 .subdirs(DATASTORE_SCAN_SUBDIRS
);
416 const SCAN_ROUTER
: Router
= Router
::new()
417 .get(&API_METHOD_SCAN_REMOTE_DATASTORES
)
418 .match_all("store", &DATASTORE_SCAN_ROUTER
);
420 const ITEM_ROUTER
: Router
= Router
::new()
421 .get(&API_METHOD_READ_REMOTE
)
422 .put(&API_METHOD_UPDATE_REMOTE
)
423 .delete(&API_METHOD_DELETE_REMOTE
)
424 .subdirs(&[("scan", &SCAN_ROUTER
)]);
426 pub const ROUTER
: Router
= Router
::new()
427 .get(&API_METHOD_LIST_REMOTES
)
428 .post(&API_METHOD_CREATE_REMOTE
)
429 .match_all("name", &ITEM_ROUTER
);