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