1 use ::serde
::{Deserialize, Serialize}
;
2 use anyhow
::{bail, format_err, Error}
;
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
;
11 use proxmox_router
::{http_bail, http_err, ApiMethod, Permission, Router, RpcEnvironment}
;
12 use proxmox_schema
::{api, param_bail}
;
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
,
19 use pbs_client
::{HttpClient, HttpClientOptions}
;
22 use pbs_config
::CachedUserInfo
;
30 description
: "The list of configured remotes (with config digest).",
32 items
: { type: RemoteWithoutPassword }
,
35 description
: "List configured remotes filtered by Remote.Audit privileges",
36 permission
: &Permission
::Anybody
,
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()?
;
48 let (config
, digest
) = pbs_config
::remote
::config()?
;
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")?
;
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.
87 pub fn create_remote(name
: String
, config
: RemoteConfig
, password
: String
) -> Result
<(), Error
> {
88 let _lock
= pbs_config
::remote
::lock_config()?
;
90 let (mut section_config
, _digest
) = pbs_config
::remote
::config()?
;
92 if section_config
.sections
.get(&name
).is_some() {
93 param_bail
!("name", "remote '{}' already exists.", name
);
102 section_config
.set_data(&name
, "remote", &remote
)?
;
104 pbs_config
::remote
::save_config(§ion_config
)?
;
113 schema
: REMOTE_ID_SCHEMA
,
117 returns
: { type: RemoteWithoutPassword }
,
119 permission
: &Permission
::Privilege(&["remote", "{name}"], PRIV_REMOTE_AUDIT
, false),
122 /// Read remote configuration data.
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();
135 #[derive(Serialize, Deserialize)]
136 /// Deletable property name
137 pub enum DeletableProperty
{
138 /// Delete the comment property.
140 /// Delete the fingerprint property.
142 /// Delete the port property.
151 schema
: REMOTE_ID_SCHEMA
,
154 type: RemoteConfigUpdater
,
158 // We expect the plain password here (not base64 encoded)
160 schema
: REMOTE_PASSWORD_SCHEMA
,
163 description
: "List of properties to delete.",
167 type: DeletableProperty
,
172 schema
: PROXMOX_CONFIG_DIGEST_SCHEMA
,
177 permission
: &Permission
::Privilege(&["remote", "{name}"], PRIV_REMOTE_MODIFY
, false),
180 /// Update remote configuration.
181 pub fn update_remote(
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()?
;
190 let (mut config
, expected_digest
) = pbs_config
::remote
::config()?
;
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
)?
;
197 let mut data
: Remote
= config
.lookup("remote", &name
)?
;
199 if let Some(delete
) = delete
{
200 for delete_prop
in delete
{
202 DeletableProperty
::Comment
=> {
203 data
.config
.comment
= None
;
205 DeletableProperty
::Fingerprint
=> {
206 data
.config
.fingerprint
= None
;
208 DeletableProperty
::Port
=> {
209 data
.config
.port
= None
;
215 if let Some(comment
) = update
.comment
{
216 let comment
= comment
.trim().to_string();
217 if comment
.is_empty() {
218 data
.config
.comment
= None
;
220 data
.config
.comment
= Some(comment
);
223 if let Some(host
) = update
.host
{
224 data
.config
.host
= host
;
226 if update
.port
.is_some() {
227 data
.config
.port
= update
.port
;
229 if let Some(auth_id
) = update
.auth_id
{
230 data
.config
.auth_id
= auth_id
;
232 if let Some(password
) = password
{
233 data
.password
= password
;
236 if update
.fingerprint
.is_some() {
237 data
.config
.fingerprint
= update
.fingerprint
;
240 config
.set_data(&name
, "remote", &data
)?
;
242 pbs_config
::remote
::save_config(&config
)?
;
252 schema
: REMOTE_ID_SCHEMA
,
256 schema
: PROXMOX_CONFIG_DIGEST_SCHEMA
,
261 permission
: &Permission
::Privilege(&["remote", "{name}"], PRIV_REMOTE_MODIFY
, false),
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()?
;
268 let job_list
: Vec
<SyncJobConfig
> = sync_jobs
.convert_to_typed_array("sync")?
;
269 for job
in job_list
{
270 if job
.remote
== name
{
273 "remote '{}' is used by sync job '{}' (datastore '{}')",
281 let _lock
= pbs_config
::remote
::lock_config()?
;
283 let (mut config
, expected_digest
) = pbs_config
::remote
::config()?
;
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
)?
;
290 match config
.sections
.get(&name
) {
292 config
.sections
.remove(&name
);
294 None
=> http_bail
!(NOT_FOUND
, "remote '{}' does not exist.", name
),
297 pbs_config
::remote
::save_config(&config
)?
;
302 /// Helper to get client for remote.cfg entry
303 pub async
fn remote_client(
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(),
312 if let Some(limit
) = limit
{
313 options
= options
.rate_limit(limit
);
316 let client
= HttpClient
::new(
318 remote
.config
.port
.unwrap_or(8007),
319 &remote
.config
.auth_id
,
322 let _auth_info
= client
323 .login() // make sure we can auth
327 "remote connection to '{}' failed - {}",
340 schema
: REMOTE_ID_SCHEMA
,
345 permission
: &Permission
::Privilege(&["remote", "{name}"], PRIV_REMOTE_AUDIT
, false),
348 description
: "List the accessible datastores.",
350 items
: { type: DataStoreListItem }
,
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
)?
;
358 let map_remote_err
= |api_err
| {
360 INTERNAL_SERVER_ERROR
,
361 "failed to scan remote '{}' - {}",
367 let client
= remote_client(&remote
, None
).await
.map_err(map_remote_err
)?
;
369 .get("api2/json/admin/datastore", None
)
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
),
378 Ok(parsed
) => Ok(parsed
),
379 Err(_
) => bail
!("Failed to parse remote scan api result."),
387 schema
: REMOTE_ID_SCHEMA
,
390 schema
: DATASTORE_SCHEMA
,
395 permission
: &Permission
::Privilege(&["remote", "{name}"], PRIV_REMOTE_AUDIT
, false),
398 description
: "List the accessible namespaces of a remote datastore.",
400 items
: { type: NamespaceListItem }
,
403 /// List namespaces of a datastore of a remote.cfg entry
404 pub async
fn scan_remote_namespaces(
407 ) -> Result
<Vec
<NamespaceListItem
>, Error
> {
408 let (remote_config
, _digest
) = pbs_config
::remote
::config()?
;
409 let remote
: Remote
= remote_config
.lookup("remote", &name
)?
;
411 let map_remote_err
= |api_err
| {
413 INTERNAL_SERVER_ERROR
,
414 "failed to scan remote '{}' - {}",
420 let client
= remote_client(&remote
, None
).await
.map_err(map_remote_err
)?
;
423 &format
!("api2/json/admin/datastore/{}/namespace", store
),
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
),
434 Ok(parsed
) => Ok(parsed
),
435 Err(_
) => bail
!("Failed to parse remote scan api result."),
443 schema
: REMOTE_ID_SCHEMA
,
446 schema
: DATASTORE_SCHEMA
,
449 type: BackupNamespace
,
455 permission
: &Permission
::Privilege(&["remote", "{name}"], PRIV_REMOTE_AUDIT
, false),
458 description
: "Lists the accessible backup groups in a remote datastore.",
460 items
: { type: GroupListItem }
,
463 /// List groups of a remote.cfg entry's datastore
464 pub async
fn scan_remote_groups(
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
)?
;
472 let map_remote_err
= |api_err
| {
474 INTERNAL_SERVER_ERROR
,
475 "failed to scan remote '{}' - {}",
481 let client
= remote_client(&remote
, None
).await
.map_err(map_remote_err
)?
;
483 let args
= namespace
.map(|ns
| json
!({ "ns": ns }
));
486 .get(&format
!("api2/json/admin/datastore/{}/groups", store
), args
)
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
),
495 Ok(parsed
) => Ok(parsed
),
496 Err(_
) => bail
!("Failed to parse remote scan api result."),
501 const DATASTORE_SCAN_SUBDIRS
: SubdirMap
= &sorted
!([
502 ("groups", &Router
::new().get(&API_METHOD_SCAN_REMOTE_GROUPS
)),
505 &Router
::new().get(&API_METHOD_SCAN_REMOTE_NAMESPACES
),
509 const DATASTORE_SCAN_ROUTER
: Router
= Router
::new()
510 .get(&list_subdirs_api_method
!(DATASTORE_SCAN_SUBDIRS
))
511 .subdirs(DATASTORE_SCAN_SUBDIRS
);
513 const SCAN_ROUTER
: Router
= Router
::new()
514 .get(&API_METHOD_SCAN_REMOTE_DATASTORES
)
515 .match_all("store", &DATASTORE_SCAN_ROUTER
);
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
)]);
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
);