]> git.proxmox.com Git - proxmox-backup.git/blame - src/api2/config/remote.rs
clippy: is_some/none/ok/err/empty
[proxmox-backup.git] / src / api2 / config / remote.rs
CommitLineData
e0100d61 1use anyhow::{bail, format_err, Error};
141304d6 2use serde_json::Value;
5211705f 3use ::serde::{Deserialize, Serialize};
141304d6 4
70e5f246 5use proxmox::api::{api, ApiMethod, Router, RpcEnvironment, Permission};
e0100d61 6use proxmox::http_err;
98c259b4 7use proxmox::tools::fs::open_file_locked;
141304d6 8
167971ed 9use crate::api2::types::*;
e0100d61 10use crate::client::{HttpClient, HttpClientOptions};
59af9ca9 11use crate::config::cached_user_info::CachedUserInfo;
f357390c 12use crate::config::remote;
8247db5b 13use 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
30pub 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 95pub 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
130pub 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
146pub 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.
206pub 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 279pub 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
312pub 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
348pub 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
377const SCAN_ROUTER: Router = Router::new()
378 .get(&API_METHOD_SCAN_REMOTE_DATASTORES);
379
08195ac8
DM
380const 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
386pub const ROUTER: Router = Router::new()
387 .get(&API_METHOD_LIST_REMOTES)
388 .post(&API_METHOD_CREATE_REMOTE)
08195ac8 389 .match_all("name", &ITEM_ROUTER);