]> git.proxmox.com Git - proxmox-backup.git/blame - src/api2/config/remote.rs
move remote config into pbs-config workspace
[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;
141304d6 7
2b7f8dd5 8use pbs_client::{HttpClient, HttpClientOptions};
6afdda88
DM
9use pbs_api_types::{
10 REMOTE_ID_SCHEMA, REMOTE_PASSWORD_SCHEMA, Remote, RemoteConfig, RemoteConfigUpdater,
11 Authid, PROXMOX_CONFIG_DIGEST_SCHEMA, DataStoreListItem,
12};
2b7f8dd5 13
59af9ca9 14use crate::config::cached_user_info::CachedUserInfo;
8247db5b 15use 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
32pub 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
82pub 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(&section_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
119pub 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
135pub 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.
179pub 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 248pub 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 281pub 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
315pub 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
344const SCAN_ROUTER: Router = Router::new()
345 .get(&API_METHOD_SCAN_REMOTE_DATASTORES);
346
08195ac8
DM
347const 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
353pub const ROUTER: Router = Router::new()
354 .get(&API_METHOD_LIST_REMOTES)
355 .post(&API_METHOD_CREATE_REMOTE)
08195ac8 356 .match_all("name", &ITEM_ROUTER);