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