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