]> git.proxmox.com Git - proxmox-backup.git/blob - src/api2/config/remote.rs
446a260413769e3db2e434a31471764efd2d4035
[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 config.sections.get(&remote.name).is_some() {
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 #[allow(clippy::too_many_arguments)]
207 pub fn update_remote(
208 name: String,
209 comment: Option<String>,
210 host: Option<String>,
211 port: Option<u16>,
212 auth_id: Option<Authid>,
213 password: Option<String>,
214 fingerprint: Option<String>,
215 delete: Option<Vec<DeletableProperty>>,
216 digest: Option<String>,
217 ) -> Result<(), Error> {
218
219 let _lock = open_file_locked(remote::REMOTE_CFG_LOCKFILE, std::time::Duration::new(10, 0), true)?;
220
221 let (mut config, expected_digest) = remote::config()?;
222
223 if let Some(ref digest) = digest {
224 let digest = proxmox::tools::hex_to_digest(digest)?;
225 crate::tools::detect_modified_configuration_file(&digest, &expected_digest)?;
226 }
227
228 let mut data: remote::Remote = config.lookup("remote", &name)?;
229
230 if let Some(delete) = delete {
231 for delete_prop in delete {
232 match delete_prop {
233 DeletableProperty::comment => { data.comment = None; },
234 DeletableProperty::fingerprint => { data.fingerprint = None; },
235 DeletableProperty::port => { data.port = None; },
236 }
237 }
238 }
239
240 if let Some(comment) = comment {
241 let comment = comment.trim().to_string();
242 if comment.is_empty() {
243 data.comment = None;
244 } else {
245 data.comment = Some(comment);
246 }
247 }
248 if let Some(host) = host { data.host = host; }
249 if port.is_some() { data.port = port; }
250 if let Some(auth_id) = auth_id { data.auth_id = auth_id; }
251 if let Some(password) = password { data.password = password; }
252
253 if let Some(fingerprint) = fingerprint { data.fingerprint = Some(fingerprint); }
254
255 config.set_data(&name, "remote", &data)?;
256
257 remote::save_config(&config)?;
258
259 Ok(())
260 }
261
262 #[api(
263 protected: true,
264 input: {
265 properties: {
266 name: {
267 schema: REMOTE_ID_SCHEMA,
268 },
269 digest: {
270 optional: true,
271 schema: PROXMOX_CONFIG_DIGEST_SCHEMA,
272 },
273 },
274 },
275 access: {
276 permission: &Permission::Privilege(&["remote", "{name}"], PRIV_REMOTE_MODIFY, false),
277 },
278 )]
279 /// Remove a remote from the configuration file.
280 pub fn delete_remote(name: String, digest: Option<String>) -> Result<(), Error> {
281
282 use crate::config::sync::{self, SyncJobConfig};
283
284 let (sync_jobs, _) = sync::config()?;
285
286 let job_list: Vec<SyncJobConfig> = sync_jobs.convert_to_typed_array("sync")?;
287 for job in job_list {
288 if job.remote == name {
289 bail!("remote '{}' is used by sync job '{}' (datastore '{}')", name, job.id, job.store);
290 }
291 }
292
293 let _lock = open_file_locked(remote::REMOTE_CFG_LOCKFILE, std::time::Duration::new(10, 0), true)?;
294
295 let (mut config, expected_digest) = remote::config()?;
296
297 if let Some(ref digest) = digest {
298 let digest = proxmox::tools::hex_to_digest(digest)?;
299 crate::tools::detect_modified_configuration_file(&digest, &expected_digest)?;
300 }
301
302 match config.sections.get(&name) {
303 Some(_) => { config.sections.remove(&name); },
304 None => bail!("remote '{}' does not exist.", name),
305 }
306
307 remote::save_config(&config)?;
308
309 Ok(())
310 }
311
312 /// Helper to get client for remote.cfg entry
313 pub async fn remote_client(remote: remote::Remote) -> Result<HttpClient, Error> {
314 let options = HttpClientOptions::new_non_interactive(remote.password.clone(), remote.fingerprint.clone());
315
316 let client = HttpClient::new(
317 &remote.host,
318 remote.port.unwrap_or(8007),
319 &remote.auth_id,
320 options)?;
321 let _auth_info = client.login() // make sure we can auth
322 .await
323 .map_err(|err| format_err!("remote connection to '{}' failed - {}", remote.host, err))?;
324
325 Ok(client)
326 }
327
328
329 #[api(
330 input: {
331 properties: {
332 name: {
333 schema: REMOTE_ID_SCHEMA,
334 },
335 },
336 },
337 access: {
338 permission: &Permission::Privilege(&["remote", "{name}"], PRIV_REMOTE_AUDIT, false),
339 },
340 returns: {
341 description: "List the accessible datastores.",
342 type: Array,
343 items: { type: DataStoreListItem },
344 },
345 )]
346 /// List datastores of a remote.cfg entry
347 pub async fn scan_remote_datastores(name: String) -> Result<Vec<DataStoreListItem>, Error> {
348 let (remote_config, _digest) = remote::config()?;
349 let remote: remote::Remote = remote_config.lookup("remote", &name)?;
350
351 let map_remote_err = |api_err| {
352 http_err!(INTERNAL_SERVER_ERROR,
353 "failed to scan remote '{}' - {}",
354 &name,
355 api_err)
356 };
357
358 let client = remote_client(remote)
359 .await
360 .map_err(map_remote_err)?;
361 let api_res = client
362 .get("api2/json/admin/datastore", None)
363 .await
364 .map_err(map_remote_err)?;
365 let parse_res = match api_res.get("data") {
366 Some(data) => serde_json::from_value::<Vec<DataStoreListItem>>(data.to_owned()),
367 None => bail!("remote {} did not return any datastore list data", &name),
368 };
369
370 match parse_res {
371 Ok(parsed) => Ok(parsed),
372 Err(_) => bail!("Failed to parse remote scan api result."),
373 }
374 }
375
376 const SCAN_ROUTER: Router = Router::new()
377 .get(&API_METHOD_SCAN_REMOTE_DATASTORES);
378
379 const ITEM_ROUTER: Router = Router::new()
380 .get(&API_METHOD_READ_REMOTE)
381 .put(&API_METHOD_UPDATE_REMOTE)
382 .delete(&API_METHOD_DELETE_REMOTE)
383 .subdirs(&[("scan", &SCAN_ROUTER)]);
384
385 pub const ROUTER: Router = Router::new()
386 .get(&API_METHOD_LIST_REMOTES)
387 .post(&API_METHOD_CREATE_REMOTE)
388 .match_all("name", &ITEM_ROUTER);