1 use anyhow
::{bail, format_err, Error}
;
3 use ::serde
::{Deserialize, Serialize}
;
5 use proxmox
::api
::{api, ApiMethod, Router, RpcEnvironment, Permission}
;
8 use pbs_client
::{HttpClient, HttpClientOptions}
;
10 REMOTE_ID_SCHEMA
, REMOTE_PASSWORD_SCHEMA
, Remote
, RemoteConfig
, RemoteConfigUpdater
,
11 Authid
, PROXMOX_CONFIG_DIGEST_SCHEMA
, DataStoreListItem
, SyncJobConfig
,
12 PRIV_REMOTE_AUDIT
, PRIV_REMOTE_MODIFY
,
16 use crate::config
::cached_user_info
::CachedUserInfo
;
23 description
: "The list of configured remotes (with config digest).",
25 items
: { type: Remote }
,
28 description
: "List configured remotes filtered by Remote.Audit privileges",
29 permission
: &Permission
::Anybody
,
36 mut rpcenv
: &mut dyn RpcEnvironment
,
37 ) -> Result
<Vec
<Remote
>, Error
> {
38 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
39 let user_info
= CachedUserInfo
::new()?
;
41 let (config
, digest
) = pbs_config
::remote
::config()?
;
43 let mut list
: Vec
<Remote
> = config
.convert_to_typed_array("remote")?
;
44 // don't return password in api
45 for remote
in &mut list
{
46 remote
.password
= "".to_string();
52 let privs
= user_info
.lookup_privs(&auth_id
, &["remote", &remote
.name
]);
53 privs
& PRIV_REMOTE_AUDIT
!= 0
57 rpcenv
["digest"] = proxmox
::tools
::digest_to_hex(&digest
).into();
66 schema
: REMOTE_ID_SCHEMA
,
73 // We expect the plain password here (not base64 encoded)
74 schema
: REMOTE_PASSWORD_SCHEMA
,
79 permission
: &Permission
::Privilege(&["remote"], PRIV_REMOTE_MODIFY
, false),
82 /// Create new remote.
87 ) -> Result
<(), Error
> {
89 let _lock
= pbs_config
::remote
::config()?
;
91 let (mut section_config
, _digest
) = pbs_config
::remote
::config()?
;
93 if section_config
.sections
.get(&name
).is_some() {
94 bail
!("remote '{}' already exists.", name
);
97 let remote
= Remote { name: name.clone(), config, password }
;
99 section_config
.set_data(&name
, "remote", &remote
)?
;
101 pbs_config
::remote
::save_config(§ion_config
)?
;
110 schema
: REMOTE_ID_SCHEMA
,
114 returns
: { type: Remote }
,
116 permission
: &Permission
::Privilege(&["remote", "{name}"], PRIV_REMOTE_AUDIT
, false),
119 /// Read remote configuration data.
123 mut rpcenv
: &mut dyn RpcEnvironment
,
124 ) -> Result
<Remote
, Error
> {
125 let (config
, digest
) = pbs_config
::remote
::config()?
;
126 let mut data
: Remote
= config
.lookup("remote", &name
)?
;
127 data
.password
= "".to_string(); // do not return password in api
128 rpcenv
["digest"] = proxmox
::tools
::digest_to_hex(&digest
).into();
133 #[derive(Serialize, Deserialize)]
134 #[allow(non_camel_case_types)]
135 /// Deletable property name
136 pub enum DeletableProperty
{
137 /// Delete the comment property.
139 /// Delete the fingerprint property.
141 /// Delete the port property.
150 schema
: REMOTE_ID_SCHEMA
,
153 type: RemoteConfigUpdater
,
157 // We expect the plain password here (not base64 encoded)
159 schema
: REMOTE_PASSWORD_SCHEMA
,
162 description
: "List of properties to delete.",
166 type: DeletableProperty
,
171 schema
: PROXMOX_CONFIG_DIGEST_SCHEMA
,
176 permission
: &Permission
::Privilege(&["remote", "{name}"], PRIV_REMOTE_MODIFY
, false),
179 /// Update remote configuration.
180 pub fn update_remote(
182 update
: RemoteConfigUpdater
,
183 password
: Option
<String
>,
184 delete
: Option
<Vec
<DeletableProperty
>>,
185 digest
: Option
<String
>,
186 ) -> Result
<(), Error
> {
188 let _lock
= pbs_config
::remote
::config()?
;
190 let (mut config
, expected_digest
) = pbs_config
::remote
::config()?
;
192 if let Some(ref digest
) = digest
{
193 let digest
= proxmox
::tools
::hex_to_digest(digest
)?
;
194 crate::tools
::detect_modified_configuration_file(&digest
, &expected_digest
)?
;
197 let mut data
: Remote
= config
.lookup("remote", &name
)?
;
199 if let Some(delete
) = delete
{
200 for delete_prop
in delete
{
202 DeletableProperty
::comment
=> { data.config.comment = None; }
,
203 DeletableProperty
::fingerprint
=> { data.config.fingerprint = None; }
,
204 DeletableProperty
::port
=> { data.config.port = None; }
,
209 if let Some(comment
) = update
.comment
{
210 let comment
= comment
.trim().to_string();
211 if comment
.is_empty() {
212 data
.config
.comment
= None
;
214 data
.config
.comment
= Some(comment
);
217 if let Some(host
) = update
.host { data.config.host = host; }
218 if update
.port
.is_some() { data.config.port = update.port; }
219 if let Some(auth_id
) = update
.auth_id { data.config.auth_id = auth_id; }
220 if let Some(password
) = password { data.password = password; }
222 if update
.fingerprint
.is_some() { data.config.fingerprint = update.fingerprint; }
224 config
.set_data(&name
, "remote", &data
)?
;
226 pbs_config
::remote
::save_config(&config
)?
;
236 schema
: REMOTE_ID_SCHEMA
,
240 schema
: PROXMOX_CONFIG_DIGEST_SCHEMA
,
245 permission
: &Permission
::Privilege(&["remote", "{name}"], PRIV_REMOTE_MODIFY
, false),
248 /// Remove a remote from the configuration file.
249 pub fn delete_remote(name
: String
, digest
: Option
<String
>) -> Result
<(), Error
> {
251 let (sync_jobs
, _
) = sync
::config()?
;
253 let job_list
: Vec
<SyncJobConfig
> = sync_jobs
.convert_to_typed_array("sync")?
;
254 for job
in job_list
{
255 if job
.remote
== name
{
256 bail
!("remote '{}' is used by sync job '{}' (datastore '{}')", name
, job
.id
, job
.store
);
260 let _lock
= pbs_config
::remote
::config()?
;
262 let (mut config
, expected_digest
) = pbs_config
::remote
::config()?
;
264 if let Some(ref digest
) = digest
{
265 let digest
= proxmox
::tools
::hex_to_digest(digest
)?
;
266 crate::tools
::detect_modified_configuration_file(&digest
, &expected_digest
)?
;
269 match config
.sections
.get(&name
) {
270 Some(_
) => { config.sections.remove(&name); }
,
271 None
=> bail
!("remote '{}' does not exist.", name
),
274 pbs_config
::remote
::save_config(&config
)?
;
279 /// Helper to get client for remote.cfg entry
280 pub async
fn remote_client(remote
: Remote
) -> Result
<HttpClient
, Error
> {
281 let options
= HttpClientOptions
::new_non_interactive(remote
.password
.clone(), remote
.config
.fingerprint
.clone());
283 let client
= HttpClient
::new(
285 remote
.config
.port
.unwrap_or(8007),
286 &remote
.config
.auth_id
,
288 let _auth_info
= client
.login() // make sure we can auth
290 .map_err(|err
| format_err
!("remote connection to '{}' failed - {}", remote
.config
.host
, err
))?
;
300 schema
: REMOTE_ID_SCHEMA
,
305 permission
: &Permission
::Privilege(&["remote", "{name}"], PRIV_REMOTE_AUDIT
, false),
308 description
: "List the accessible datastores.",
310 items
: { type: DataStoreListItem }
,
313 /// List datastores of a remote.cfg entry
314 pub async
fn scan_remote_datastores(name
: String
) -> Result
<Vec
<DataStoreListItem
>, Error
> {
315 let (remote_config
, _digest
) = pbs_config
::remote
::config()?
;
316 let remote
: Remote
= remote_config
.lookup("remote", &name
)?
;
318 let map_remote_err
= |api_err
| {
319 http_err
!(INTERNAL_SERVER_ERROR
,
320 "failed to scan remote '{}' - {}",
325 let client
= remote_client(remote
)
327 .map_err(map_remote_err
)?
;
329 .get("api2/json/admin/datastore", None
)
331 .map_err(map_remote_err
)?
;
332 let parse_res
= match api_res
.get("data") {
333 Some(data
) => serde_json
::from_value
::<Vec
<DataStoreListItem
>>(data
.to_owned()),
334 None
=> bail
!("remote {} did not return any datastore list data", &name
),
338 Ok(parsed
) => Ok(parsed
),
339 Err(_
) => bail
!("Failed to parse remote scan api result."),
343 const SCAN_ROUTER
: Router
= Router
::new()
344 .get(&API_METHOD_SCAN_REMOTE_DATASTORES
);
346 const ITEM_ROUTER
: Router
= Router
::new()
347 .get(&API_METHOD_READ_REMOTE
)
348 .put(&API_METHOD_UPDATE_REMOTE
)
349 .delete(&API_METHOD_DELETE_REMOTE
)
350 .subdirs(&[("scan", &SCAN_ROUTER
)]);
352 pub const ROUTER
: Router
= Router
::new()
353 .get(&API_METHOD_LIST_REMOTES
)
354 .post(&API_METHOD_CREATE_REMOTE
)
355 .match_all("name", &ITEM_ROUTER
);