]> git.proxmox.com Git - proxmox-backup.git/blame - src/api2/config/remote.rs
router change made one level of rpcenv mut superfluous
[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;
1d9bc184 4use proxmox_router::list_subdirs_api_method;
dc7a5b34
TL
5use proxmox_router::SubdirMap;
6use proxmox_sys::sortable;
141304d6
DM
7use serde_json::Value;
8
dc7a5b34 9use proxmox_router::{http_bail, http_err, ApiMethod, Permission, Router, RpcEnvironment};
8d6425aa 10use proxmox_schema::{api, param_bail};
141304d6 11
6afdda88 12use pbs_api_types::{
dc7a5b34 13 Authid, DataStoreListItem, GroupListItem, RateLimitConfig, Remote, RemoteConfig,
24cb5c7a
DM
14 RemoteConfigUpdater, RemoteWithoutPassword, SyncJobConfig, DATASTORE_SCHEMA, PRIV_REMOTE_AUDIT,
15 PRIV_REMOTE_MODIFY, PROXMOX_CONFIG_DIGEST_SCHEMA, REMOTE_ID_SCHEMA, REMOTE_PASSWORD_SCHEMA,
6afdda88 16};
dc7a5b34 17use pbs_client::{HttpClient, HttpClientOptions};
a4e5a0fc 18use pbs_config::sync;
2b7f8dd5 19
ba3d7e19 20use pbs_config::CachedUserInfo;
141304d6
DM
21
22#[api(
23 input: {
24 properties: {},
25 },
26 returns: {
f3ec5dae 27 description: "The list of configured remotes (with config digest).",
141304d6 28 type: Array,
24cb5c7a 29 items: { type: RemoteWithoutPassword },
141304d6 30 },
70e5f246 31 access: {
59af9ca9
FG
32 description: "List configured remotes filtered by Remote.Audit privileges",
33 permission: &Permission::Anybody,
70e5f246 34 },
141304d6
DM
35)]
36/// List all remotes
37pub fn list_remotes(
38 _param: Value,
39 _info: &ApiMethod,
41c1a179 40 rpcenv: &mut dyn RpcEnvironment,
24cb5c7a 41) -> Result<Vec<RemoteWithoutPassword>, Error> {
59af9ca9
FG
42 let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
43 let user_info = CachedUserInfo::new()?;
141304d6 44
6afdda88 45 let (config, digest) = pbs_config::remote::config()?;
141304d6 46
24cb5c7a
DM
47 // Note: This removes the password (we do not want to return the password).
48 let list: Vec<RemoteWithoutPassword> = config.convert_to_typed_array("remote")?;
83fd4b3b 49
59af9ca9
FG
50 let list = list
51 .into_iter()
52 .filter(|remote| {
53 let privs = user_info.lookup_privs(&auth_id, &["remote", &remote.name]);
54 privs & PRIV_REMOTE_AUDIT != 0
55 })
56 .collect();
57
25877d05 58 rpcenv["digest"] = hex::encode(&digest).into();
83fd4b3b 59 Ok(list)
141304d6
DM
60}
61
62#[api(
688fbe07 63 protected: true,
141304d6
DM
64 input: {
65 properties: {
66 name: {
167971ed 67 schema: REMOTE_ID_SCHEMA,
141304d6 68 },
97dfc62f 69 config: {
6afdda88 70 type: RemoteConfig,
97dfc62f 71 flatten: true,
141304d6
DM
72 },
73 password: {
97dfc62f 74 // We expect the plain password here (not base64 encoded)
6afdda88 75 schema: REMOTE_PASSWORD_SCHEMA,
141304d6
DM
76 },
77 },
78 },
70e5f246 79 access: {
8247db5b 80 permission: &Permission::Privilege(&["remote"], PRIV_REMOTE_MODIFY, false),
70e5f246 81 },
141304d6
DM
82)]
83/// Create new remote.
dc7a5b34 84pub fn create_remote(name: String, config: RemoteConfig, password: String) -> Result<(), Error> {
4beb7d2d 85 let _lock = pbs_config::remote::lock_config()?;
141304d6 86
6afdda88 87 let (mut section_config, _digest) = pbs_config::remote::config()?;
141304d6 88
97dfc62f 89 if section_config.sections.get(&name).is_some() {
8d6425aa 90 param_bail!("name", "remote '{}' already exists.", name);
141304d6
DM
91 }
92
dc7a5b34
TL
93 let remote = Remote {
94 name: name.clone(),
95 config,
96 password,
97 };
141304d6 98
97dfc62f
DM
99 section_config.set_data(&name, "remote", &remote)?;
100
6afdda88 101 pbs_config::remote::save_config(&section_config)?;
141304d6
DM
102
103 Ok(())
104}
105
08195ac8
DM
106#[api(
107 input: {
108 properties: {
109 name: {
110 schema: REMOTE_ID_SCHEMA,
111 },
112 },
113 },
2c88dc97 114 returns: { type: RemoteWithoutPassword },
70e5f246 115 access: {
8247db5b 116 permission: &Permission::Privilege(&["remote", "{name}"], PRIV_REMOTE_AUDIT, false),
70e5f246 117 }
08195ac8
DM
118)]
119/// Read remote configuration data.
4f966d05
DC
120pub fn read_remote(
121 name: String,
122 _info: &ApiMethod,
41c1a179 123 rpcenv: &mut dyn RpcEnvironment,
2c88dc97 124) -> Result<RemoteWithoutPassword, Error> {
6afdda88 125 let (config, digest) = pbs_config::remote::config()?;
2c88dc97 126 let data: RemoteWithoutPassword = config.lookup("remote", &name)?;
25877d05 127 rpcenv["digest"] = hex::encode(&digest).into();
08195ac8
DM
128 Ok(data)
129}
30003baa 130
5211705f
DM
131#[api()]
132#[derive(Serialize, Deserialize)]
133#[allow(non_camel_case_types)]
134/// Deletable property name
135pub enum DeletableProperty {
136 /// Delete the comment property.
137 comment,
138 /// Delete the fingerprint property.
139 fingerprint,
ba20987a
DC
140 /// Delete the port property.
141 port,
5211705f 142}
08195ac8
DM
143
144#[api(
145 protected: true,
146 input: {
147 properties: {
148 name: {
149 schema: REMOTE_ID_SCHEMA,
150 },
97dfc62f 151 update: {
6afdda88 152 type: RemoteConfigUpdater,
97dfc62f 153 flatten: true,
08195ac8
DM
154 },
155 password: {
97dfc62f 156 // We expect the plain password here (not base64 encoded)
08195ac8 157 optional: true,
6afdda88 158 schema: REMOTE_PASSWORD_SCHEMA,
08195ac8 159 },
5211705f
DM
160 delete: {
161 description: "List of properties to delete.",
162 type: Array,
163 optional: true,
164 items: {
165 type: DeletableProperty,
166 }
167 },
002a191a
DM
168 digest: {
169 optional: true,
170 schema: PROXMOX_CONFIG_DIGEST_SCHEMA,
171 },
08195ac8
DM
172 },
173 },
70e5f246 174 access: {
8247db5b 175 permission: &Permission::Privilege(&["remote", "{name}"], PRIV_REMOTE_MODIFY, false),
70e5f246 176 },
08195ac8
DM
177)]
178/// Update remote configuration.
179pub fn update_remote(
180 name: String,
6afdda88 181 update: RemoteConfigUpdater,
08195ac8 182 password: Option<String>,
5211705f 183 delete: Option<Vec<DeletableProperty>>,
002a191a 184 digest: Option<String>,
08195ac8 185) -> Result<(), Error> {
4beb7d2d 186 let _lock = pbs_config::remote::lock_config()?;
347834df 187
6afdda88 188 let (mut config, expected_digest) = pbs_config::remote::config()?;
002a191a
DM
189
190 if let Some(ref digest) = digest {
25877d05 191 let digest = <[u8; 32]>::from_hex(digest)?;
002a191a
DM
192 crate::tools::detect_modified_configuration_file(&digest, &expected_digest)?;
193 }
08195ac8 194
6afdda88 195 let mut data: Remote = config.lookup("remote", &name)?;
08195ac8 196
5211705f
DM
197 if let Some(delete) = delete {
198 for delete_prop in delete {
199 match delete_prop {
dc7a5b34
TL
200 DeletableProperty::comment => {
201 data.config.comment = None;
202 }
203 DeletableProperty::fingerprint => {
204 data.config.fingerprint = None;
205 }
206 DeletableProperty::port => {
207 data.config.port = None;
208 }
5211705f
DM
209 }
210 }
211 }
212
97dfc62f 213 if let Some(comment) = update.comment {
08195ac8
DM
214 let comment = comment.trim().to_string();
215 if comment.is_empty() {
97dfc62f 216 data.config.comment = None;
08195ac8 217 } else {
97dfc62f 218 data.config.comment = Some(comment);
08195ac8
DM
219 }
220 }
dc7a5b34
TL
221 if let Some(host) = update.host {
222 data.config.host = host;
223 }
224 if update.port.is_some() {
225 data.config.port = update.port;
226 }
227 if let Some(auth_id) = update.auth_id {
228 data.config.auth_id = auth_id;
229 }
230 if let Some(password) = password {
231 data.password = password;
232 }
08195ac8 233
dc7a5b34
TL
234 if update.fingerprint.is_some() {
235 data.config.fingerprint = update.fingerprint;
236 }
6afbe1d8 237
08195ac8
DM
238 config.set_data(&name, "remote", &data)?;
239
6afdda88 240 pbs_config::remote::save_config(&config)?;
08195ac8
DM
241
242 Ok(())
243}
244
141304d6 245#[api(
688fbe07 246 protected: true,
141304d6
DM
247 input: {
248 properties: {
249 name: {
167971ed 250 schema: REMOTE_ID_SCHEMA,
141304d6 251 },
99f443c6
DC
252 digest: {
253 optional: true,
254 schema: PROXMOX_CONFIG_DIGEST_SCHEMA,
255 },
141304d6
DM
256 },
257 },
70e5f246 258 access: {
8247db5b 259 permission: &Permission::Privilege(&["remote", "{name}"], PRIV_REMOTE_MODIFY, false),
70e5f246 260 },
141304d6
DM
261)]
262/// Remove a remote from the configuration file.
99f443c6 263pub fn delete_remote(name: String, digest: Option<String>) -> Result<(), Error> {
2791318f
DM
264 let (sync_jobs, _) = sync::config()?;
265
dc7a5b34 266 let job_list: Vec<SyncJobConfig> = sync_jobs.convert_to_typed_array("sync")?;
2791318f
DM
267 for job in job_list {
268 if job.remote == name {
dc7a5b34
TL
269 param_bail!(
270 "name",
271 "remote '{}' is used by sync job '{}' (datastore '{}')",
272 name,
273 job.id,
274 job.store
275 );
2791318f
DM
276 }
277 }
278
4beb7d2d 279 let _lock = pbs_config::remote::lock_config()?;
141304d6 280
6afdda88 281 let (mut config, expected_digest) = pbs_config::remote::config()?;
99f443c6
DC
282
283 if let Some(ref digest) = digest {
25877d05 284 let digest = <[u8; 32]>::from_hex(digest)?;
99f443c6
DC
285 crate::tools::detect_modified_configuration_file(&digest, &expected_digest)?;
286 }
141304d6
DM
287
288 match config.sections.get(&name) {
dc7a5b34
TL
289 Some(_) => {
290 config.sections.remove(&name);
291 }
dcd1518e 292 None => http_bail!(NOT_FOUND, "remote '{}' does not exist.", name),
141304d6
DM
293 }
294
6afdda88 295 pbs_config::remote::save_config(&config)?;
98574722 296
141304d6
DM
297 Ok(())
298}
299
e0100d61 300/// Helper to get client for remote.cfg entry
2d5287fb
DM
301pub async fn remote_client(
302 remote: &Remote,
303 limit: Option<RateLimitConfig>,
304) -> Result<HttpClient, Error> {
dc7a5b34
TL
305 let mut options = HttpClientOptions::new_non_interactive(
306 remote.password.clone(),
307 remote.config.fingerprint.clone(),
308 );
2d5287fb
DM
309
310 if let Some(limit) = limit {
311 options = options.rate_limit(limit);
312 }
e0100d61
FG
313
314 let client = HttpClient::new(
97dfc62f
DM
315 &remote.config.host,
316 remote.config.port.unwrap_or(8007),
317 &remote.config.auth_id,
dc7a5b34
TL
318 options,
319 )?;
320 let _auth_info = client
321 .login() // make sure we can auth
e0100d61 322 .await
dc7a5b34
TL
323 .map_err(|err| {
324 format_err!(
325 "remote connection to '{}' failed - {}",
326 remote.config.host,
327 err
328 )
329 })?;
e0100d61
FG
330
331 Ok(client)
332}
333
e0100d61
FG
334#[api(
335 input: {
336 properties: {
337 name: {
338 schema: REMOTE_ID_SCHEMA,
339 },
340 },
341 },
342 access: {
343 permission: &Permission::Privilege(&["remote", "{name}"], PRIV_REMOTE_AUDIT, false),
344 },
345 returns: {
346 description: "List the accessible datastores.",
347 type: Array,
9b93c620 348 items: { type: DataStoreListItem },
e0100d61
FG
349 },
350)]
351/// List datastores of a remote.cfg entry
352pub async fn scan_remote_datastores(name: String) -> Result<Vec<DataStoreListItem>, Error> {
6afdda88
DM
353 let (remote_config, _digest) = pbs_config::remote::config()?;
354 let remote: Remote = remote_config.lookup("remote", &name)?;
e0100d61
FG
355
356 let map_remote_err = |api_err| {
dc7a5b34
TL
357 http_err!(
358 INTERNAL_SERVER_ERROR,
359 "failed to scan remote '{}' - {}",
360 &name,
361 api_err
362 )
e0100d61
FG
363 };
364
dc7a5b34 365 let client = remote_client(&remote, None).await.map_err(map_remote_err)?;
e0100d61
FG
366 let api_res = client
367 .get("api2/json/admin/datastore", None)
368 .await
369 .map_err(map_remote_err)?;
370 let parse_res = match api_res.get("data") {
371 Some(data) => serde_json::from_value::<Vec<DataStoreListItem>>(data.to_owned()),
372 None => bail!("remote {} did not return any datastore list data", &name),
373 };
374
375 match parse_res {
376 Ok(parsed) => Ok(parsed),
377 Err(_) => bail!("Failed to parse remote scan api result."),
378 }
379}
380
1d9bc184
FG
381#[api(
382 input: {
383 properties: {
384 name: {
385 schema: REMOTE_ID_SCHEMA,
386 },
387 store: {
388 schema: DATASTORE_SCHEMA,
389 },
390 },
391 },
392 access: {
393 permission: &Permission::Privilege(&["remote", "{name}"], PRIV_REMOTE_AUDIT, false),
394 },
395 returns: {
396 description: "Lists the accessible backup groups in a remote datastore.",
397 type: Array,
398 items: { type: GroupListItem },
399 },
400)]
401/// List groups of a remote.cfg entry's datastore
402pub async fn scan_remote_groups(name: String, store: String) -> Result<Vec<GroupListItem>, Error> {
403 let (remote_config, _digest) = pbs_config::remote::config()?;
404 let remote: Remote = remote_config.lookup("remote", &name)?;
405
406 let map_remote_err = |api_err| {
dc7a5b34
TL
407 http_err!(
408 INTERNAL_SERVER_ERROR,
409 "failed to scan remote '{}' - {}",
410 &name,
411 api_err
412 )
1d9bc184
FG
413 };
414
dc7a5b34 415 let client = remote_client(&remote, None).await.map_err(map_remote_err)?;
1d9bc184
FG
416 let api_res = client
417 .get(&format!("api2/json/admin/datastore/{}/groups", store), None)
418 .await
419 .map_err(map_remote_err)?;
420 let parse_res = match api_res.get("data") {
421 Some(data) => serde_json::from_value::<Vec<GroupListItem>>(data.to_owned()),
422 None => bail!("remote {} did not return any group list data", &name),
423 };
424
425 match parse_res {
426 Ok(parsed) => Ok(parsed),
427 Err(_) => bail!("Failed to parse remote scan api result."),
428 }
429}
430
431#[sortable]
dc7a5b34
TL
432const DATASTORE_SCAN_SUBDIRS: SubdirMap =
433 &[("groups", &Router::new().get(&API_METHOD_SCAN_REMOTE_GROUPS))];
1d9bc184
FG
434
435const DATASTORE_SCAN_ROUTER: Router = Router::new()
436 .get(&list_subdirs_api_method!(DATASTORE_SCAN_SUBDIRS))
437 .subdirs(DATASTORE_SCAN_SUBDIRS);
438
e0100d61 439const SCAN_ROUTER: Router = Router::new()
1d9bc184
FG
440 .get(&API_METHOD_SCAN_REMOTE_DATASTORES)
441 .match_all("store", &DATASTORE_SCAN_ROUTER);
e0100d61 442
08195ac8
DM
443const ITEM_ROUTER: Router = Router::new()
444 .get(&API_METHOD_READ_REMOTE)
445 .put(&API_METHOD_UPDATE_REMOTE)
e0100d61
FG
446 .delete(&API_METHOD_DELETE_REMOTE)
447 .subdirs(&[("scan", &SCAN_ROUTER)]);
08195ac8 448
141304d6
DM
449pub const ROUTER: Router = Router::new()
450 .get(&API_METHOD_LIST_REMOTES)
451 .post(&API_METHOD_CREATE_REMOTE)
08195ac8 452 .match_all("name", &ITEM_ROUTER);