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