]>
Commit | Line | Data |
---|---|---|
769f8c99 | 1 | use failure::*; |
47d47121 | 2 | use serde_json::{json, Value}; |
550e0d88 DM |
3 | use std::path::PathBuf; |
4 | use nix::sys::stat::Mode; | |
ea0b8b6e | 5 | |
550e0d88 | 6 | use proxmox::tools::fs::{CreateOptions, replace_file}; |
769f8c99 DM |
7 | use proxmox::api::{api, cli::*}; |
8 | ||
550e0d88 | 9 | use proxmox_backup::configdir; |
769f8c99 DM |
10 | use proxmox_backup::tools; |
11 | use proxmox_backup::config; | |
550e0d88 | 12 | use proxmox_backup::backup::*; |
769f8c99 DM |
13 | use proxmox_backup::api2::types::*; |
14 | use proxmox_backup::client::*; | |
15 | use proxmox_backup::tools::ticket::*; | |
16 | use proxmox_backup::auth_helpers::*; | |
17 | ||
18 | ||
19 | async fn view_task_result( | |
20 | client: HttpClient, | |
21 | result: Value, | |
22 | output_format: &str, | |
23 | ) -> Result<(), Error> { | |
24 | let data = &result["data"]; | |
25 | if output_format == "text" { | |
26 | if let Some(upid) = data.as_str() { | |
27 | display_task_log(client, upid, true).await?; | |
28 | } | |
29 | } else { | |
30 | format_and_print_result(&data, &output_format); | |
31 | } | |
32 | ||
33 | Ok(()) | |
34 | } | |
211fabd7 | 35 | |
47d47121 DM |
36 | fn connect() -> Result<HttpClient, Error> { |
37 | ||
38 | let uid = nix::unistd::Uid::current(); | |
39 | ||
40 | let client = if uid.is_root() { | |
41 | let ticket = assemble_rsa_ticket(private_auth_key(), "PBS", Some("root@pam"), None)?; | |
42 | HttpClient::new("localhost", "root@pam", Some(ticket))? | |
43 | } else { | |
44 | HttpClient::new("localhost", "root@pam", None)? | |
45 | }; | |
46 | ||
47 | Ok(client) | |
48 | } | |
49 | ||
9f6ab1fc | 50 | fn datastore_commands() -> CommandLineInterface { |
ea0b8b6e | 51 | |
576e3bf2 | 52 | use proxmox_backup::api2; |
bf7f1039 | 53 | |
6460764d | 54 | let cmd_def = CliCommandMap::new() |
48ef3c33 | 55 | .insert("list", CliCommand::new(&api2::config::datastore::GET)) |
6460764d | 56 | .insert("create", |
255f378a | 57 | CliCommand::new(&api2::config::datastore::POST) |
49fddd98 | 58 | .arg_param(&["name", "path"]) |
48ef3c33 | 59 | ) |
6460764d | 60 | .insert("remove", |
255f378a | 61 | CliCommand::new(&api2::config::datastore::DELETE) |
49fddd98 | 62 | .arg_param(&["name"]) |
07b4694a | 63 | .completion_cb("name", config::datastore::complete_datastore_name) |
48ef3c33 | 64 | ); |
211fabd7 | 65 | |
8f62336b | 66 | cmd_def.into() |
211fabd7 DM |
67 | } |
68 | ||
691c89a0 | 69 | |
769f8c99 DM |
70 | #[api( |
71 | input: { | |
72 | properties: { | |
73 | store: { | |
74 | schema: DATASTORE_SCHEMA, | |
75 | }, | |
76 | "output-format": { | |
77 | schema: OUTPUT_FORMAT, | |
78 | optional: true, | |
79 | }, | |
80 | } | |
81 | } | |
82 | )] | |
83 | /// Start garbage collection for a specific datastore. | |
84 | async fn start_garbage_collection(param: Value) -> Result<Value, Error> { | |
691c89a0 | 85 | |
769f8c99 | 86 | let output_format = param["output-format"].as_str().unwrap_or("text").to_owned(); |
691c89a0 | 87 | |
769f8c99 DM |
88 | let store = tools::required_string_param(¶m, "store")?; |
89 | ||
47d47121 | 90 | let mut client = connect()?; |
769f8c99 DM |
91 | |
92 | let path = format!("api2/json/admin/datastore/{}/gc", store); | |
93 | ||
94 | let result = client.post(&path, None).await?; | |
95 | ||
96 | view_task_result(client, result, &output_format).await?; | |
97 | ||
98 | Ok(Value::Null) | |
99 | } | |
100 | ||
101 | #[api( | |
102 | input: { | |
103 | properties: { | |
104 | store: { | |
105 | schema: DATASTORE_SCHEMA, | |
106 | }, | |
107 | "output-format": { | |
108 | schema: OUTPUT_FORMAT, | |
109 | optional: true, | |
110 | }, | |
111 | } | |
112 | } | |
113 | )] | |
114 | /// Show garbage collection status for a specific datastore. | |
115 | async fn garbage_collection_status(param: Value) -> Result<Value, Error> { | |
116 | ||
117 | let output_format = param["output-format"].as_str().unwrap_or("text").to_owned(); | |
118 | ||
119 | let store = tools::required_string_param(¶m, "store")?; | |
120 | ||
47d47121 | 121 | let client = connect()?; |
769f8c99 DM |
122 | |
123 | let path = format!("api2/json/admin/datastore/{}/gc", store); | |
124 | ||
125 | let result = client.get(&path, None).await?; | |
126 | let data = &result["data"]; | |
127 | if output_format == "text" { | |
128 | format_and_print_result(&data, "json-pretty"); | |
129 | } else { | |
130 | format_and_print_result(&data, &output_format); | |
131 | } | |
132 | ||
133 | Ok(Value::Null) | |
134 | } | |
135 | ||
136 | fn garbage_collection_commands() -> CommandLineInterface { | |
691c89a0 DM |
137 | |
138 | let cmd_def = CliCommandMap::new() | |
139 | .insert("status", | |
769f8c99 | 140 | CliCommand::new(&API_METHOD_GARBAGE_COLLECTION_STATUS) |
49fddd98 | 141 | .arg_param(&["store"]) |
9ac1045c | 142 | .completion_cb("store", config::datastore::complete_datastore_name) |
48ef3c33 | 143 | ) |
691c89a0 | 144 | .insert("start", |
769f8c99 | 145 | CliCommand::new(&API_METHOD_START_GARBAGE_COLLECTION) |
49fddd98 | 146 | .arg_param(&["store"]) |
9ac1045c | 147 | .completion_cb("store", config::datastore::complete_datastore_name) |
48ef3c33 | 148 | ); |
691c89a0 DM |
149 | |
150 | cmd_def.into() | |
151 | } | |
152 | ||
47d47121 DM |
153 | #[api( |
154 | input: { | |
155 | properties: { | |
156 | limit: { | |
157 | description: "The maximal number of tasks to list.", | |
158 | type: Integer, | |
159 | optional: true, | |
160 | minimum: 1, | |
161 | maximum: 1000, | |
162 | default: 50, | |
163 | }, | |
164 | "output-format": { | |
165 | schema: OUTPUT_FORMAT, | |
166 | optional: true, | |
167 | }, | |
168 | all: { | |
169 | type: Boolean, | |
170 | description: "Also list stopped tasks.", | |
171 | optional: true, | |
172 | } | |
173 | } | |
174 | } | |
175 | )] | |
176 | /// List running server tasks. | |
177 | async fn task_list(param: Value) -> Result<Value, Error> { | |
178 | ||
179 | let output_format = param["output-format"].as_str().unwrap_or("text").to_owned(); | |
180 | ||
181 | let client = connect()?; | |
182 | ||
183 | let limit = param["limit"].as_u64().unwrap_or(50) as usize; | |
184 | let running = !param["all"].as_bool().unwrap_or(false); | |
185 | let args = json!({ | |
186 | "running": running, | |
187 | "start": 0, | |
188 | "limit": limit, | |
189 | }); | |
190 | let result = client.get("api2/json/nodes/localhost/tasks", Some(args)).await?; | |
191 | ||
192 | let data = &result["data"]; | |
193 | ||
194 | if output_format == "text" { | |
195 | for item in data.as_array().unwrap() { | |
196 | println!( | |
197 | "{} {}", | |
198 | item["upid"].as_str().unwrap(), | |
199 | item["status"].as_str().unwrap_or("running"), | |
200 | ); | |
201 | } | |
202 | } else { | |
203 | format_and_print_result(data, &output_format); | |
204 | } | |
205 | ||
206 | Ok(Value::Null) | |
207 | } | |
208 | ||
209 | #[api( | |
210 | input: { | |
211 | properties: { | |
212 | upid: { | |
213 | schema: UPID_SCHEMA, | |
214 | }, | |
215 | } | |
216 | } | |
217 | )] | |
218 | /// Display the task log. | |
219 | async fn task_log(param: Value) -> Result<Value, Error> { | |
220 | ||
221 | let upid = tools::required_string_param(¶m, "upid")?; | |
222 | ||
223 | let client = connect()?; | |
224 | ||
225 | display_task_log(client, upid, true).await?; | |
226 | ||
227 | Ok(Value::Null) | |
228 | } | |
229 | ||
230 | #[api( | |
231 | input: { | |
232 | properties: { | |
233 | upid: { | |
234 | schema: UPID_SCHEMA, | |
235 | }, | |
236 | } | |
237 | } | |
238 | )] | |
239 | /// Try to stop a specific task. | |
240 | async fn task_stop(param: Value) -> Result<Value, Error> { | |
241 | ||
242 | let upid_str = tools::required_string_param(¶m, "upid")?; | |
243 | ||
244 | let mut client = connect()?; | |
245 | ||
246 | let path = format!("api2/json/nodes/localhost/tasks/{}", upid_str); | |
247 | let _ = client.delete(&path, None).await?; | |
248 | ||
249 | Ok(Value::Null) | |
250 | } | |
251 | ||
252 | fn task_mgmt_cli() -> CommandLineInterface { | |
253 | ||
254 | let task_log_cmd_def = CliCommand::new(&API_METHOD_TASK_LOG) | |
255 | .arg_param(&["upid"]); | |
256 | ||
257 | let task_stop_cmd_def = CliCommand::new(&API_METHOD_TASK_STOP) | |
258 | .arg_param(&["upid"]); | |
259 | ||
260 | let cmd_def = CliCommandMap::new() | |
261 | .insert("list", CliCommand::new(&API_METHOD_TASK_LIST)) | |
262 | .insert("log", task_log_cmd_def) | |
263 | .insert("stop", task_stop_cmd_def); | |
264 | ||
265 | cmd_def.into() | |
266 | } | |
267 | ||
e739a8d8 DM |
268 | fn x509name_to_string(name: &openssl::x509::X509NameRef) -> Result<String, Error> { |
269 | let mut parts = Vec::new(); | |
270 | for entry in name.entries() { | |
271 | parts.push(format!("{} = {}", entry.object().nid().short_name()?, entry.data().as_utf8()?)); | |
272 | } | |
273 | Ok(parts.join(", ")) | |
274 | } | |
275 | ||
276 | #[api] | |
277 | /// Diplay node certificate information. | |
278 | fn cert_info() -> Result<(), Error> { | |
279 | ||
280 | let cert_path = PathBuf::from(configdir!("/proxy.pem")); | |
281 | ||
282 | let cert_pem = proxmox::tools::fs::file_get_contents(&cert_path)?; | |
283 | ||
284 | let cert = openssl::x509::X509::from_pem(&cert_pem)?; | |
285 | ||
286 | println!("Subject: {}", x509name_to_string(cert.subject_name())?); | |
287 | ||
288 | if let Some(san) = cert.subject_alt_names() { | |
289 | for name in san.iter() { | |
290 | if let Some(v) = name.dnsname() { | |
291 | println!(" DNS:{}", v); | |
292 | } else if let Some(v) = name.ipaddress() { | |
293 | println!(" IP:{:?}", v); | |
294 | } else if let Some(v) = name.email() { | |
295 | println!(" EMAIL:{}", v); | |
296 | } else if let Some(v) = name.uri() { | |
297 | println!(" URI:{}", v); | |
298 | } | |
299 | } | |
300 | } | |
301 | ||
302 | println!("Issuer: {}", x509name_to_string(cert.issuer_name())?); | |
303 | println!("Validity:"); | |
304 | println!(" Not Before: {}", cert.not_before()); | |
305 | println!(" Not After : {}", cert.not_after()); | |
306 | ||
307 | let fp = cert.digest(openssl::hash::MessageDigest::sha256())?; | |
308 | let fp_string = proxmox::tools::digest_to_hex(&fp); | |
309 | let fp_string = fp_string.as_bytes().chunks(2).map(|v| std::str::from_utf8(v).unwrap()) | |
310 | .collect::<Vec<&str>>().join(":"); | |
311 | ||
312 | println!("Fingerprint (sha256): {}", fp_string); | |
313 | ||
314 | let pubkey = cert.public_key()?; | |
315 | println!("Public key type: {}", openssl::nid::Nid::from_raw(pubkey.id().as_raw()).long_name()?); | |
316 | println!("Public key bits: {}", pubkey.bits()); | |
317 | ||
318 | Ok(()) | |
319 | } | |
320 | ||
550e0d88 DM |
321 | #[api( |
322 | input: { | |
323 | properties: { | |
324 | force: { | |
325 | description: "Force generation of new SSL certifate.", | |
326 | type: Boolean, | |
327 | optional:true, | |
328 | }, | |
329 | } | |
330 | }, | |
331 | )] | |
332 | /// Update node certificates and generate all needed files/directories. | |
333 | fn update_certs(force: Option<bool>) -> Result<(), Error> { | |
334 | ||
335 | let backup_user = backup_user()?; | |
336 | ||
337 | config::create_configdir()?; | |
338 | ||
339 | if let Err(err) = generate_auth_key() { | |
340 | bail!("unable to generate auth key - {}", err); | |
341 | } | |
342 | ||
343 | if let Err(err) = generate_csrf_key() { | |
344 | bail!("unable to generate csrf key - {}", err); | |
345 | } | |
346 | ||
347 | //openssl req -x509 -newkey rsa:4096 -keyout /etc/proxmox-backup/proxy.key -out /etc/proxmox-backup/proxy.pem -nodes | |
348 | let key_path = PathBuf::from(configdir!("/proxy.key")); | |
349 | let cert_path = PathBuf::from(configdir!("/proxy.pem")); | |
350 | ||
351 | if key_path.exists() && cert_path.exists() && !force.unwrap_or(false) { return Ok(()); } | |
352 | ||
353 | use openssl::rsa::{Rsa}; | |
354 | use openssl::x509::{X509Builder}; | |
355 | use openssl::pkey::PKey; | |
356 | ||
357 | let rsa = Rsa::generate(4096).unwrap(); | |
358 | ||
359 | let priv_pem = rsa.private_key_to_pem()?; | |
360 | ||
361 | replace_file( | |
362 | &key_path, | |
363 | &priv_pem, | |
364 | CreateOptions::new() | |
365 | .perm(Mode::from_bits_truncate(0o0640)) | |
366 | .owner(nix::unistd::ROOT) | |
367 | .group(backup_user.gid), | |
368 | )?; | |
369 | ||
370 | let mut x509 = X509Builder::new()?; | |
371 | ||
372 | x509.set_version(2)?; | |
373 | ||
374 | let today = openssl::asn1::Asn1Time::days_from_now(0)?; | |
375 | x509.set_not_before(&today)?; | |
376 | let expire = openssl::asn1::Asn1Time::days_from_now(365*1000)?; | |
377 | x509.set_not_after(&expire)?; | |
378 | ||
379 | let nodename = proxmox::tools::nodename(); | |
380 | let mut fqdn = nodename.to_owned(); | |
381 | ||
382 | let resolv_conf = proxmox_backup::api2::node::dns::read_etc_resolv_conf()?; | |
383 | if let Some(search) = resolv_conf["search"].as_str() { | |
384 | fqdn.push('.'); | |
385 | fqdn.push_str(search); | |
386 | } | |
387 | ||
388 | // we try to generate an unique 'subject' to avoid browser problems | |
389 | //(reused serial numbers, ..) | |
390 | let uuid = proxmox::tools::uuid::Uuid::generate(); | |
391 | ||
392 | let mut subject_name = openssl::x509::X509NameBuilder::new()?; | |
393 | subject_name.append_entry_by_text("O", "Proxmox Backup Server")?; | |
394 | subject_name.append_entry_by_text("OU", &format!("{:X}", uuid))?; | |
395 | subject_name.append_entry_by_text("CN", &fqdn)?; | |
396 | let subject_name = subject_name.build(); | |
397 | ||
398 | x509.set_subject_name(&subject_name)?; | |
399 | x509.set_issuer_name(&subject_name)?; | |
400 | ||
401 | let bc = openssl::x509::extension::BasicConstraints::new(); // CA = false | |
402 | let bc = bc.build()?; | |
403 | x509.append_extension(bc)?; | |
404 | ||
405 | let usage = openssl::x509::extension::ExtendedKeyUsage::new() | |
406 | .server_auth() | |
407 | .build()?; | |
408 | x509.append_extension(usage)?; | |
409 | ||
410 | let context = x509.x509v3_context(None, None); | |
411 | ||
412 | let mut alt_names = openssl::x509::extension::SubjectAlternativeName::new(); | |
413 | ||
414 | alt_names.ip("127.0.0.1"); | |
415 | alt_names.ip("::1"); | |
416 | ||
417 | // fixme: add local node IPs | |
418 | ||
419 | alt_names.dns("localhost"); | |
420 | ||
421 | if nodename != "localhost" { alt_names.dns(nodename); } | |
422 | if nodename != fqdn { alt_names.dns(&fqdn); } | |
423 | ||
424 | let alt_names = alt_names.build(&context)?; | |
425 | ||
426 | x509.append_extension(alt_names)?; | |
427 | ||
428 | let pub_pem = rsa.public_key_to_pem()?; | |
429 | let pubkey = PKey::public_key_from_pem(&pub_pem)?; | |
430 | ||
431 | x509.set_pubkey(&pubkey)?; | |
432 | ||
433 | let context = x509.x509v3_context(None, None); | |
434 | let ext = openssl::x509::extension::SubjectKeyIdentifier::new().build(&context)?; | |
435 | x509.append_extension(ext)?; | |
436 | ||
437 | let context = x509.x509v3_context(None, None); | |
438 | let ext = openssl::x509::extension::AuthorityKeyIdentifier::new() | |
439 | .keyid(true) | |
440 | .build(&context)?; | |
441 | x509.append_extension(ext)?; | |
442 | ||
443 | let privkey = PKey::from_rsa(rsa)?; | |
444 | ||
445 | x509.sign(&privkey, openssl::hash::MessageDigest::sha256())?; | |
446 | ||
447 | let x509 = x509.build(); | |
448 | let cert_pem = x509.to_pem()?; | |
449 | ||
450 | replace_file( | |
451 | &cert_path, | |
452 | &cert_pem, | |
453 | CreateOptions::new() | |
454 | .perm(Mode::from_bits_truncate(0o0640)) | |
455 | .owner(nix::unistd::ROOT) | |
456 | .group(backup_user.gid), | |
457 | )?; | |
458 | ||
459 | Ok(()) | |
460 | } | |
461 | ||
462 | fn cert_mgmt_cli() -> CommandLineInterface { | |
463 | ||
550e0d88 | 464 | let cmd_def = CliCommandMap::new() |
e739a8d8 DM |
465 | .insert("info", CliCommand::new(&API_METHOD_CERT_INFO)) |
466 | .insert("update", CliCommand::new(&API_METHOD_UPDATE_CERTS)); | |
550e0d88 DM |
467 | |
468 | cmd_def.into() | |
469 | } | |
470 | ||
211fabd7 DM |
471 | fn main() { |
472 | ||
6460764d | 473 | let cmd_def = CliCommandMap::new() |
48ef3c33 | 474 | .insert("datastore", datastore_commands()) |
47d47121 | 475 | .insert("garbage-collection", garbage_collection_commands()) |
550e0d88 | 476 | .insert("cert", cert_mgmt_cli()) |
47d47121 | 477 | .insert("task", task_mgmt_cli()); |
34d3ba52 | 478 | |
48ef3c33 | 479 | run_cli_command(cmd_def); |
ea0b8b6e | 480 | } |