]> git.proxmox.com Git - proxmox-backup.git/blame - src/api2/config/remote.rs
pxar: log when skipping mount points
[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
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
33pub 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 98pub 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
136pub 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
152pub 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.
212pub 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 285pub 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
307pub 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
346pub 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
375const SCAN_ROUTER: Router = Router::new()
376 .get(&API_METHOD_SCAN_REMOTE_DATASTORES);
377
08195ac8
DM
378const 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
384pub const ROUTER: Router = Router::new()
385 .get(&API_METHOD_LIST_REMOTES)
386 .post(&API_METHOD_CREATE_REMOTE)
08195ac8 387 .match_all("name", &ITEM_ROUTER);