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