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