]>
Commit | Line | Data |
---|---|---|
e0100d61 | 1 | use anyhow::{bail, format_err, Error}; |
141304d6 | 2 | use serde_json::Value; |
5211705f | 3 | use ::serde::{Deserialize, Serialize}; |
141304d6 | 4 | |
70e5f246 | 5 | use proxmox::api::{api, ApiMethod, Router, RpcEnvironment, Permission}; |
e0100d61 | 6 | use proxmox::http_err; |
98c259b4 | 7 | use proxmox::tools::fs::open_file_locked; |
141304d6 | 8 | |
167971ed | 9 | use crate::api2::types::*; |
e0100d61 | 10 | use crate::client::{HttpClient, HttpClientOptions}; |
59af9ca9 | 11 | use crate::config::cached_user_info::CachedUserInfo; |
f357390c | 12 | use crate::config::remote; |
8247db5b | 13 | use crate::config::acl::{PRIV_REMOTE_AUDIT, PRIV_REMOTE_MODIFY}; |
141304d6 DM |
14 | |
15 | #[api( | |
16 | input: { | |
17 | properties: {}, | |
18 | }, | |
19 | returns: { | |
f3ec5dae | 20 | description: "The list of configured remotes (with config digest).", |
141304d6 | 21 | type: Array, |
9b93c620 | 22 | items: { type: remote::Remote }, |
141304d6 | 23 | }, |
70e5f246 | 24 | access: { |
59af9ca9 FG |
25 | description: "List configured remotes filtered by Remote.Audit privileges", |
26 | permission: &Permission::Anybody, | |
70e5f246 | 27 | }, |
141304d6 DM |
28 | )] |
29 | /// List all remotes | |
30 | pub fn list_remotes( | |
31 | _param: Value, | |
32 | _info: &ApiMethod, | |
83fd4b3b DC |
33 | mut rpcenv: &mut dyn RpcEnvironment, |
34 | ) -> Result<Vec<remote::Remote>, Error> { | |
59af9ca9 FG |
35 | let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?; |
36 | let user_info = CachedUserInfo::new()?; | |
141304d6 | 37 | |
f357390c | 38 | let (config, digest) = remote::config()?; |
141304d6 | 39 | |
83fd4b3b | 40 | let mut list: Vec<remote::Remote> = config.convert_to_typed_array("remote")?; |
83fd4b3b DC |
41 | // don't return password in api |
42 | for remote in &mut list { | |
43 | remote.password = "".to_string(); | |
44 | } | |
45 | ||
59af9ca9 FG |
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 | ||
83fd4b3b DC |
54 | rpcenv["digest"] = proxmox::tools::digest_to_hex(&digest).into(); |
55 | Ok(list) | |
141304d6 DM |
56 | } |
57 | ||
58 | #[api( | |
688fbe07 | 59 | protected: true, |
141304d6 DM |
60 | input: { |
61 | properties: { | |
62 | name: { | |
167971ed | 63 | schema: REMOTE_ID_SCHEMA, |
141304d6 DM |
64 | }, |
65 | comment: { | |
66 | optional: true, | |
454c13ed | 67 | schema: SINGLE_LINE_COMMENT_SCHEMA, |
141304d6 DM |
68 | }, |
69 | host: { | |
163dc16c | 70 | schema: DNS_NAME_OR_IP_SCHEMA, |
141304d6 | 71 | }, |
ba20987a DC |
72 | port: { |
73 | description: "The (optional) port.", | |
74 | type: u16, | |
75 | optional: true, | |
76 | default: 8007, | |
77 | }, | |
10052ea6 | 78 | "auth-id": { |
e6dc35ac | 79 | type: Authid, |
141304d6 DM |
80 | }, |
81 | password: { | |
f357390c | 82 | schema: remote::REMOTE_PASSWORD_SCHEMA, |
141304d6 | 83 | }, |
6afbe1d8 DM |
84 | fingerprint: { |
85 | optional: true, | |
86 | schema: CERT_FINGERPRINT_SHA256_SCHEMA, | |
87 | }, | |
141304d6 DM |
88 | }, |
89 | }, | |
70e5f246 | 90 | access: { |
8247db5b | 91 | permission: &Permission::Privilege(&["remote"], PRIV_REMOTE_MODIFY, false), |
70e5f246 | 92 | }, |
141304d6 DM |
93 | )] |
94 | /// Create new remote. | |
de4db62c | 95 | pub fn create_remote(password: String, param: Value) -> Result<(), Error> { |
141304d6 | 96 | |
b56c111e | 97 | let _lock = open_file_locked(remote::REMOTE_CFG_LOCKFILE, std::time::Duration::new(10, 0), true)?; |
141304d6 | 98 | |
44288184 | 99 | let mut data = param; |
de4db62c DC |
100 | data["password"] = Value::from(base64::encode(password.as_bytes())); |
101 | let remote: remote::Remote = serde_json::from_value(data)?; | |
141304d6 | 102 | |
f357390c | 103 | let (mut config, _digest) = remote::config()?; |
141304d6 | 104 | |
3984a5fd | 105 | if config.sections.get(&remote.name).is_some() { |
db0c2287 | 106 | bail!("remote '{}' already exists.", remote.name); |
141304d6 DM |
107 | } |
108 | ||
db0c2287 | 109 | config.set_data(&remote.name, "remote", &remote)?; |
141304d6 | 110 | |
f357390c | 111 | remote::save_config(&config)?; |
141304d6 DM |
112 | |
113 | Ok(()) | |
114 | } | |
115 | ||
08195ac8 DM |
116 | #[api( |
117 | input: { | |
118 | properties: { | |
119 | name: { | |
120 | schema: REMOTE_ID_SCHEMA, | |
121 | }, | |
122 | }, | |
123 | }, | |
9b93c620 | 124 | returns: { type: remote::Remote }, |
70e5f246 | 125 | access: { |
8247db5b | 126 | permission: &Permission::Privilege(&["remote", "{name}"], PRIV_REMOTE_AUDIT, false), |
70e5f246 | 127 | } |
08195ac8 DM |
128 | )] |
129 | /// Read remote configuration data. | |
4f966d05 DC |
130 | pub fn read_remote( |
131 | name: String, | |
132 | _info: &ApiMethod, | |
133 | mut rpcenv: &mut dyn RpcEnvironment, | |
83fd4b3b | 134 | ) -> Result<remote::Remote, Error> { |
f357390c | 135 | let (config, digest) = remote::config()?; |
83fd4b3b DC |
136 | let mut data: remote::Remote = config.lookup("remote", &name)?; |
137 | data.password = "".to_string(); // do not return password in api | |
4f966d05 | 138 | rpcenv["digest"] = proxmox::tools::digest_to_hex(&digest).into(); |
08195ac8 DM |
139 | Ok(data) |
140 | } | |
30003baa | 141 | |
5211705f DM |
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, | |
ba20987a DC |
151 | /// Delete the port property. |
152 | port, | |
5211705f | 153 | } |
08195ac8 DM |
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 | }, | |
ba20987a DC |
170 | port: { |
171 | description: "The (optional) port.", | |
172 | type: u16, | |
173 | optional: true, | |
174 | }, | |
10052ea6 | 175 | "auth-id": { |
08195ac8 | 176 | optional: true, |
e6dc35ac | 177 | type: Authid, |
08195ac8 DM |
178 | }, |
179 | password: { | |
180 | optional: true, | |
f357390c | 181 | schema: remote::REMOTE_PASSWORD_SCHEMA, |
08195ac8 | 182 | }, |
6afbe1d8 DM |
183 | fingerprint: { |
184 | optional: true, | |
185 | schema: CERT_FINGERPRINT_SHA256_SCHEMA, | |
186 | }, | |
5211705f DM |
187 | delete: { |
188 | description: "List of properties to delete.", | |
189 | type: Array, | |
190 | optional: true, | |
191 | items: { | |
192 | type: DeletableProperty, | |
193 | } | |
194 | }, | |
002a191a DM |
195 | digest: { |
196 | optional: true, | |
197 | schema: PROXMOX_CONFIG_DIGEST_SCHEMA, | |
198 | }, | |
08195ac8 DM |
199 | }, |
200 | }, | |
70e5f246 | 201 | access: { |
8247db5b | 202 | permission: &Permission::Privilege(&["remote", "{name}"], PRIV_REMOTE_MODIFY, false), |
70e5f246 | 203 | }, |
08195ac8 DM |
204 | )] |
205 | /// Update remote configuration. | |
206 | pub fn update_remote( | |
207 | name: String, | |
208 | comment: Option<String>, | |
209 | host: Option<String>, | |
ba20987a | 210 | port: Option<u16>, |
10052ea6 | 211 | auth_id: Option<Authid>, |
08195ac8 | 212 | password: Option<String>, |
6afbe1d8 | 213 | fingerprint: Option<String>, |
5211705f | 214 | delete: Option<Vec<DeletableProperty>>, |
002a191a | 215 | digest: Option<String>, |
08195ac8 DM |
216 | ) -> Result<(), Error> { |
217 | ||
b56c111e | 218 | let _lock = open_file_locked(remote::REMOTE_CFG_LOCKFILE, std::time::Duration::new(10, 0), true)?; |
347834df | 219 | |
f357390c | 220 | let (mut config, expected_digest) = remote::config()?; |
002a191a DM |
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 | } | |
08195ac8 | 226 | |
f357390c | 227 | let mut data: remote::Remote = config.lookup("remote", &name)?; |
08195ac8 | 228 | |
5211705f DM |
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; }, | |
ba20987a | 234 | DeletableProperty::port => { data.port = None; }, |
5211705f DM |
235 | } |
236 | } | |
237 | } | |
238 | ||
08195ac8 DM |
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; } | |
ba20987a | 248 | if port.is_some() { data.port = port; } |
10052ea6 | 249 | if let Some(auth_id) = auth_id { data.auth_id = auth_id; } |
08195ac8 DM |
250 | if let Some(password) = password { data.password = password; } |
251 | ||
6afbe1d8 DM |
252 | if let Some(fingerprint) = fingerprint { data.fingerprint = Some(fingerprint); } |
253 | ||
08195ac8 DM |
254 | config.set_data(&name, "remote", &data)?; |
255 | ||
f357390c | 256 | remote::save_config(&config)?; |
08195ac8 DM |
257 | |
258 | Ok(()) | |
259 | } | |
260 | ||
141304d6 | 261 | #[api( |
688fbe07 | 262 | protected: true, |
141304d6 DM |
263 | input: { |
264 | properties: { | |
265 | name: { | |
167971ed | 266 | schema: REMOTE_ID_SCHEMA, |
141304d6 | 267 | }, |
99f443c6 DC |
268 | digest: { |
269 | optional: true, | |
270 | schema: PROXMOX_CONFIG_DIGEST_SCHEMA, | |
271 | }, | |
141304d6 DM |
272 | }, |
273 | }, | |
70e5f246 | 274 | access: { |
8247db5b | 275 | permission: &Permission::Privilege(&["remote", "{name}"], PRIV_REMOTE_MODIFY, false), |
70e5f246 | 276 | }, |
141304d6 DM |
277 | )] |
278 | /// Remove a remote from the configuration file. | |
99f443c6 | 279 | pub fn delete_remote(name: String, digest: Option<String>) -> Result<(), Error> { |
141304d6 | 280 | |
2791318f DM |
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 | ||
b56c111e | 292 | let _lock = open_file_locked(remote::REMOTE_CFG_LOCKFILE, std::time::Duration::new(10, 0), true)?; |
141304d6 | 293 | |
99f443c6 DC |
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 | } | |
141304d6 DM |
300 | |
301 | match config.sections.get(&name) { | |
302 | Some(_) => { config.sections.remove(&name); }, | |
303 | None => bail!("remote '{}' does not exist.", name), | |
304 | } | |
305 | ||
98574722 DC |
306 | remote::save_config(&config)?; |
307 | ||
141304d6 DM |
308 | Ok(()) |
309 | } | |
310 | ||
e0100d61 FG |
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), | |
10052ea6 | 320 | &remote.auth_id, |
e0100d61 FG |
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, | |
9b93c620 | 344 | items: { type: DataStoreListItem }, |
e0100d61 FG |
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 | ||
08195ac8 DM |
380 | const ITEM_ROUTER: Router = Router::new() |
381 | .get(&API_METHOD_READ_REMOTE) | |
382 | .put(&API_METHOD_UPDATE_REMOTE) | |
e0100d61 FG |
383 | .delete(&API_METHOD_DELETE_REMOTE) |
384 | .subdirs(&[("scan", &SCAN_ROUTER)]); | |
08195ac8 | 385 | |
141304d6 DM |
386 | pub const ROUTER: Router = Router::new() |
387 | .get(&API_METHOD_LIST_REMOTES) | |
388 | .post(&API_METHOD_CREATE_REMOTE) | |
08195ac8 | 389 | .match_all("name", &ITEM_ROUTER); |