]>
Commit | Line | Data |
---|---|---|
183e2a76 CH |
1 | #![forbid(unsafe_code)] |
2 | ||
3285f0ad CH |
3 | use std::{ |
4 | collections::HashMap, | |
5 | env, | |
6 | io::{BufRead, BufReader, Write}, | |
7 | net::IpAddr, | |
8 | path::PathBuf, | |
9 | str::FromStr, | |
10 | sync::{Arc, Mutex}, | |
11 | }; | |
83071f35 | 12 | |
183e2a76 | 13 | use cursive::{ |
ccf3b075 | 14 | event::Event, |
08ad8ed6 | 15 | theme::{ColorStyle, Effect, PaletteColor, Style}, |
3285f0ad | 16 | utils::Counter, |
08ad8ed6 | 17 | view::{Nameable, Offset, Resizable, ViewWrapper}, |
183e2a76 | 18 | views::{ |
08ad8ed6 CH |
19 | Button, Checkbox, Dialog, DummyView, EditView, Layer, LinearLayout, PaddedView, Panel, |
20 | ProgressBar, ResizedView, ScrollView, SelectView, StackView, TextContent, TextView, | |
21 | ViewRef, | |
183e2a76 | 22 | }, |
08ad8ed6 | 23 | Cursive, CursiveRunnable, ScreenId, View, XY, |
183e2a76 | 24 | }; |
32368ac3 WB |
25 | |
26 | mod options; | |
27 | use options::*; | |
28 | ||
29 | mod setup; | |
3285f0ad | 30 | use setup::{InstallConfig, LocaleInfo, RuntimeInfo, SetupInfo}; |
32368ac3 WB |
31 | |
32 | mod system; | |
33 | ||
34 | mod utils; | |
95c49008 | 35 | use utils::Fqdn; |
32368ac3 WB |
36 | |
37 | mod views; | |
e1a11f79 CH |
38 | use views::{ |
39 | BootdiskOptionsView, CidrAddressEditView, FormView, TableView, TableViewItem, | |
40 | TimezoneOptionsView, | |
41 | }; | |
183e2a76 CH |
42 | |
43 | // TextView::center() seems to garble the first two lines, so fix it manually here. | |
08ad8ed6 CH |
44 | const PROXMOX_LOGO: &str = r#" |
45 | ____ | |
46 | / __ \_________ _ ______ ___ ____ _ __ | |
47 | / /_/ / ___/ __ \| |/_/ __ `__ \/ __ \| |/_/ | |
48 | / ____/ / / /_/ /> </ / / / / / /_/ /> < | |
49 | /_/ /_/ \____/_/|_/_/ /_/ /_/\____/_/|_| "#; | |
183e2a76 CH |
50 | |
51 | struct InstallerView { | |
08ad8ed6 | 52 | view: ResizedView<Dialog>, |
183e2a76 CH |
53 | } |
54 | ||
55 | impl InstallerView { | |
90c0ea37 CH |
56 | pub fn new<T: View>( |
57 | state: &InstallerState, | |
58 | view: T, | |
59 | next_cb: Box<dyn Fn(&mut Cursive)>, | |
60 | ) -> Self { | |
8807d547 CH |
61 | let inner = LinearLayout::vertical() |
62 | .child(PaddedView::lrtb(0, 0, 1, 1, view)) | |
63 | .child(PaddedView::lrtb( | |
64 | 1, | |
65 | 1, | |
66 | 0, | |
67 | 0, | |
68 | LinearLayout::horizontal() | |
69 | .child(abort_install_button()) | |
70 | .child(DummyView.full_width()) | |
71 | .child(Button::new("Previous", switch_to_prev_screen)) | |
72 | .child(DummyView) | |
73 | .child(Button::new("Next", next_cb)), | |
74 | )); | |
f522afb1 | 75 | |
90c0ea37 | 76 | Self::with_raw(state, inner) |
f522afb1 CH |
77 | } |
78 | ||
90c0ea37 CH |
79 | pub fn with_raw(state: &InstallerState, view: impl View) -> Self { |
80 | let setup = &state.setup_info; | |
81 | ||
90c0ea37 CH |
82 | let title = format!( |
83 | "{} ({}-{}) Installer", | |
fbfd1838 | 84 | setup.config.fullname, setup.iso_info.release, setup.iso_info.isorelease |
90c0ea37 CH |
85 | ); |
86 | ||
08ad8ed6 | 87 | let inner = Dialog::around(view).title(title); |
8b43c2d3 | 88 | |
183e2a76 | 89 | Self { |
8b43c2d3 CH |
90 | // Limit the maximum to something reasonable, such that it won't get spread out much |
91 | // depending on the screen. | |
92 | view: ResizedView::with_max_size((120, 40), inner), | |
183e2a76 CH |
93 | } |
94 | } | |
95 | } | |
96 | ||
97 | impl ViewWrapper for InstallerView { | |
08ad8ed6 CH |
98 | cursive::wrap_impl!(self.view: ResizedView<Dialog>); |
99 | } | |
100 | ||
101 | struct InstallerBackgroundView { | |
102 | view: StackView, | |
103 | } | |
104 | ||
105 | impl InstallerBackgroundView { | |
5ad2f28c TL |
106 | pub fn new(_state: &InstallerState) -> Self { |
107 | let logo = format!("{PROXMOX_LOGO}"); | |
08ad8ed6 CH |
108 | let style = Style { |
109 | effects: Effect::Bold.into(), | |
110 | color: ColorStyle::back(PaletteColor::View), | |
111 | }; | |
112 | ||
113 | let mut view = StackView::new(); | |
114 | view.add_fullscreen_layer(Layer::with_color( | |
115 | DummyView | |
116 | .full_width() | |
117 | .fixed_height(logo.lines().count() + 1), | |
118 | ColorStyle::back(PaletteColor::View), | |
119 | )); | |
120 | view.add_transparent_layer_at( | |
121 | XY { | |
122 | x: Offset::Center, | |
123 | y: Offset::Absolute(0), | |
124 | }, | |
125 | TextView::new(logo).style(style), | |
126 | ); | |
127 | ||
128 | Self { view } | |
129 | } | |
130 | } | |
131 | ||
132 | impl ViewWrapper for InstallerBackgroundView { | |
133 | cursive::wrap_impl!(self.view: StackView); | |
183e2a76 CH |
134 | } |
135 | ||
6ab6af4c CH |
136 | #[derive(Clone, Eq, Hash, PartialEq)] |
137 | enum InstallerStep { | |
138 | Licence, | |
139 | Bootdisk, | |
140 | Timezone, | |
141 | Password, | |
142 | Network, | |
143 | Summary, | |
81f9348c | 144 | Install, |
6ab6af4c CH |
145 | } |
146 | ||
ed191e60 | 147 | #[derive(Clone)] |
a6e00ea6 | 148 | struct InstallerState { |
ed191e60 | 149 | options: InstallerOptions, |
4296d200 | 150 | setup_info: SetupInfo, |
2473d5dc | 151 | runtime_info: RuntimeInfo, |
4296d200 | 152 | locales: LocaleInfo, |
6ab6af4c | 153 | steps: HashMap<InstallerStep, ScreenId>, |
cae15523 | 154 | in_test_mode: bool, |
ed191e60 CH |
155 | } |
156 | ||
183e2a76 CH |
157 | fn main() { |
158 | let mut siv = cursive::termion(); | |
159 | ||
cae15523 CH |
160 | let in_test_mode = match env::args().nth(1).as_deref() { |
161 | Some("-t") => true, | |
162 | ||
163 | // Always force the test directory in debug builds | |
164 | #[cfg(debug_assertions)] | |
165 | _ => true, | |
166 | ||
167 | #[cfg(not(debug_assertions))] | |
168 | _ => false, | |
169 | }; | |
170 | ||
2473d5dc | 171 | let (setup_info, locales, runtime_info) = match installer_setup(in_test_mode) { |
4296d200 CH |
172 | Ok(result) => result, |
173 | Err(err) => initial_setup_error(&mut siv, &err), | |
174 | }; | |
130fb96a | 175 | |
ccf3b075 CH |
176 | siv.clear_global_callbacks(Event::CtrlChar('c')); |
177 | siv.set_on_pre_event(Event::CtrlChar('c'), trigger_abort_install_dialog); | |
178 | ||
a6e00ea6 | 179 | siv.set_user_data(InstallerState { |
ed191e60 | 180 | options: InstallerOptions { |
5dd6b6ed | 181 | bootdisk: BootdiskOptions::defaults_from(&runtime_info.disks[0]), |
0e42d278 | 182 | timezone: TimezoneOptions::defaults_from(&runtime_info, &locales), |
c7d3d928 | 183 | password: PasswordOptions::defaults_from(&runtime_info), |
b5aa2ddf | 184 | network: NetworkOptions::from(&runtime_info.network), |
ea15ca43 | 185 | reboot: false, |
e9557e62 | 186 | }, |
4296d200 | 187 | setup_info, |
2473d5dc | 188 | runtime_info, |
4296d200 | 189 | locales, |
6ab6af4c | 190 | steps: HashMap::new(), |
cae15523 | 191 | in_test_mode, |
e9557e62 CH |
192 | }); |
193 | ||
6ab6af4c | 194 | switch_to_next_screen(&mut siv, InstallerStep::Licence, &license_dialog); |
183e2a76 CH |
195 | siv.run(); |
196 | } | |
197 | ||
2473d5dc | 198 | fn installer_setup(in_test_mode: bool) -> Result<(SetupInfo, LocaleInfo, RuntimeInfo), String> { |
4f2295a7 CH |
199 | let base_path = if in_test_mode { "./testdir" } else { "/" }; |
200 | let mut path = PathBuf::from(base_path); | |
4296d200 CH |
201 | |
202 | path.push("run"); | |
203 | path.push("proxmox-installer"); | |
204 | ||
205 | let installer_info = { | |
206 | let mut path = path.clone(); | |
207 | path.push("iso-info.json"); | |
208 | ||
209 | setup::read_json(&path).map_err(|err| format!("Failed to retrieve setup info: {err}"))? | |
210 | }; | |
211 | ||
212 | let locale_info = { | |
213 | let mut path = path.clone(); | |
214 | path.push("locales.json"); | |
215 | ||
216 | setup::read_json(&path).map_err(|err| format!("Failed to retrieve locale info: {err}"))? | |
217 | }; | |
218 | ||
c97b9971 | 219 | let mut runtime_info: RuntimeInfo = { |
2473d5dc WB |
220 | let mut path = path.clone(); |
221 | path.push("run-env-info.json"); | |
222 | ||
223 | setup::read_json(&path) | |
224 | .map_err(|err| format!("Failed to retrieve runtime environment info: {err}"))? | |
225 | }; | |
226 | ||
7e68e30e CH |
227 | system::has_min_requirements(&runtime_info)?; |
228 | ||
c97b9971 | 229 | runtime_info.disks.sort(); |
d4d0f2bf CH |
230 | if runtime_info.disks.is_empty() { |
231 | Err("The installer could not find any supported hard disks.".to_owned()) | |
232 | } else { | |
233 | Ok((installer_info, locale_info, runtime_info)) | |
234 | } | |
4296d200 CH |
235 | } |
236 | ||
237 | fn initial_setup_error(siv: &mut CursiveRunnable, message: &str) -> ! { | |
238 | siv.add_layer( | |
239 | Dialog::around(TextView::new(message)) | |
240 | .title("Installer setup error") | |
241 | .button("Ok", Cursive::quit), | |
242 | ); | |
243 | siv.run(); | |
244 | ||
245 | std::process::exit(1); | |
246 | } | |
247 | ||
6ab6af4c CH |
248 | fn switch_to_next_screen( |
249 | siv: &mut Cursive, | |
250 | step: InstallerStep, | |
251 | constructor: &dyn Fn(&mut Cursive) -> InstallerView, | |
252 | ) { | |
08ad8ed6 CH |
253 | let state = siv.user_data::<InstallerState>().cloned().unwrap(); |
254 | ||
6ab6af4c | 255 | // Check if the screen already exists; if yes, then simply switch to it. |
08ad8ed6 CH |
256 | if let Some(screen_id) = state.steps.get(&step) { |
257 | siv.set_screen(*screen_id); | |
258 | return; | |
6ab6af4c CH |
259 | } |
260 | ||
5e73fcfe | 261 | let v = constructor(siv); |
6ab6af4c CH |
262 | let screen = siv.add_active_screen(); |
263 | siv.with_user_data(|state: &mut InstallerState| state.steps.insert(step, screen)); | |
08ad8ed6 CH |
264 | |
265 | if let Some(state) = siv.user_data::<InstallerState>().cloned() { | |
266 | siv.screen_mut().add_transparent_layer_at( | |
267 | XY { | |
268 | x: Offset::Parent(0), | |
269 | y: Offset::Parent(0), | |
270 | }, | |
271 | InstallerBackgroundView::new(&state), | |
272 | ); | |
273 | } | |
274 | ||
5e73fcfe | 275 | siv.screen_mut().add_layer(v); |
183e2a76 CH |
276 | } |
277 | ||
64220ff1 CH |
278 | fn switch_to_prev_screen(siv: &mut Cursive) { |
279 | let id = siv.active_screen().saturating_sub(1); | |
280 | siv.set_screen(id); | |
281 | } | |
282 | ||
183e2a76 CH |
283 | fn yes_no_dialog( |
284 | siv: &mut Cursive, | |
285 | title: &str, | |
286 | text: &str, | |
deebe07f CH |
287 | // callback_yes: &'static dyn Fn(&mut Cursive), |
288 | // callback_no: &'static dyn Fn(&mut Cursive), | |
289 | callback_yes: Box<dyn Fn(&mut Cursive)>, | |
290 | callback_no: Box<dyn Fn(&mut Cursive)>, | |
183e2a76 CH |
291 | ) { |
292 | siv.add_layer( | |
293 | Dialog::around(TextView::new(text)) | |
294 | .title(title) | |
deebe07f CH |
295 | .button("No", move |siv| { |
296 | siv.pop_layer(); | |
297 | callback_no(siv); | |
298 | }) | |
299 | .button("Yes", move |siv| { | |
300 | siv.pop_layer(); | |
301 | callback_yes(siv); | |
302 | }), | |
183e2a76 CH |
303 | ) |
304 | } | |
305 | ||
ccf3b075 | 306 | fn trigger_abort_install_dialog(siv: &mut Cursive) { |
58869243 CH |
307 | #[cfg(debug_assertions)] |
308 | siv.quit(); | |
309 | ||
310 | #[cfg(not(debug_assertions))] | |
ccf3b075 CH |
311 | yes_no_dialog( |
312 | siv, | |
313 | "Abort installation?", | |
314 | "Are you sure you want to abort the installation?", | |
deebe07f CH |
315 | Box::new(Cursive::quit), |
316 | Box::new(|_| {}), | |
ccf3b075 CH |
317 | ) |
318 | } | |
319 | ||
183e2a76 | 320 | fn abort_install_button() -> Button { |
ccf3b075 | 321 | Button::new("Abort", trigger_abort_install_dialog) |
183e2a76 CH |
322 | } |
323 | ||
324 | fn get_eula() -> String { | |
8f5fdd21 CH |
325 | // TODO: properly using info from Proxmox::Install::Env::setup() |
326 | std::fs::read_to_string("/cdrom/EULA") | |
327 | .unwrap_or_else(|_| "< Debug build - ignoring non-existing EULA >".to_owned()) | |
183e2a76 CH |
328 | } |
329 | ||
90c0ea37 CH |
330 | fn license_dialog(siv: &mut Cursive) -> InstallerView { |
331 | let state = siv.user_data::<InstallerState>().unwrap(); | |
332 | ||
183e2a76 CH |
333 | let inner = LinearLayout::vertical() |
334 | .child(PaddedView::lrtb( | |
335 | 0, | |
336 | 0, | |
337 | 1, | |
338 | 0, | |
339 | TextView::new("END USER LICENSE AGREEMENT (EULA)").center(), | |
340 | )) | |
8b43c2d3 CH |
341 | .child(Panel::new(ScrollView::new( |
342 | TextView::new(get_eula()).center(), | |
183e2a76 CH |
343 | ))) |
344 | .child(PaddedView::lrtb( | |
345 | 1, | |
346 | 1, | |
f522afb1 | 347 | 1, |
183e2a76 CH |
348 | 0, |
349 | LinearLayout::horizontal() | |
350 | .child(abort_install_button()) | |
351 | .child(DummyView.full_width()) | |
5e73fcfe | 352 | .child(Button::new("I agree", |siv| { |
6ab6af4c | 353 | switch_to_next_screen(siv, InstallerStep::Bootdisk, &bootdisk_dialog) |
5e73fcfe | 354 | })), |
183e2a76 CH |
355 | )); |
356 | ||
90c0ea37 | 357 | InstallerView::with_raw(state, inner) |
183e2a76 CH |
358 | } |
359 | ||
e70f1b2f | 360 | fn bootdisk_dialog(siv: &mut Cursive) -> InstallerView { |
a6e00ea6 | 361 | let state = siv.user_data::<InstallerState>().cloned().unwrap(); |
64220ff1 | 362 | |
f522afb1 | 363 | InstallerView::new( |
90c0ea37 | 364 | &state, |
5dd6b6ed | 365 | BootdiskOptionsView::new(&state.runtime_info.disks, &state.options.bootdisk) |
ed191e60 | 366 | .with_name("bootdisk-options"), |
f522afb1 | 367 | Box::new(|siv| { |
994c4ff0 CH |
368 | let options = siv.call_on_name("bootdisk-options", BootdiskOptionsView::get_values); |
369 | ||
370 | match options { | |
371 | Some(Ok(options)) => { | |
372 | siv.with_user_data(|state: &mut InstallerState| { | |
373 | state.options.bootdisk = options; | |
374 | }); | |
375 | ||
376 | switch_to_next_screen(siv, InstallerStep::Timezone, &timezone_dialog); | |
377 | } | |
378 | ||
379 | Some(Err(err)) => siv.add_layer(Dialog::info(format!("Invalid values: {err}"))), | |
380 | _ => siv.add_layer(Dialog::info("Invalid values")), | |
93ebe7bd | 381 | } |
f522afb1 CH |
382 | }), |
383 | ) | |
64220ff1 CH |
384 | } |
385 | ||
a142f457 | 386 | fn timezone_dialog(siv: &mut Cursive) -> InstallerView { |
90c0ea37 CH |
387 | let state = siv.user_data::<InstallerState>().unwrap(); |
388 | let options = &state.options.timezone; | |
a142f457 | 389 | |
a142f457 | 390 | InstallerView::new( |
90c0ea37 | 391 | state, |
e1a11f79 | 392 | TimezoneOptionsView::new(&state.locales, options).with_name("timezone-options"), |
a142f457 | 393 | Box::new(|siv| { |
e1a11f79 | 394 | let options = siv.call_on_name("timezone-options", TimezoneOptionsView::get_values); |
93ebe7bd | 395 | |
767843f9 CH |
396 | match options { |
397 | Some(Ok(options)) => { | |
a6e00ea6 CH |
398 | siv.with_user_data(|state: &mut InstallerState| { |
399 | state.options.timezone = options; | |
767843f9 CH |
400 | }); |
401 | ||
6ab6af4c | 402 | switch_to_next_screen(siv, InstallerStep::Password, &password_dialog); |
767843f9 CH |
403 | } |
404 | Some(Err(err)) => siv.add_layer(Dialog::info(format!("Invalid values: {err}"))), | |
405 | _ => siv.add_layer(Dialog::info("Invalid values")), | |
93ebe7bd | 406 | } |
a142f457 CH |
407 | }), |
408 | ) | |
183e2a76 | 409 | } |
c2eee468 CH |
410 | |
411 | fn password_dialog(siv: &mut Cursive) -> InstallerView { | |
90c0ea37 CH |
412 | let state = siv.user_data::<InstallerState>().unwrap(); |
413 | let options = &state.options.password; | |
c2eee468 | 414 | |
15832d18 CH |
415 | let inner = FormView::new() |
416 | .child("Root password", EditView::new().secret()) | |
417 | .child("Confirm root password", EditView::new().secret()) | |
90c0ea37 CH |
418 | .child( |
419 | "Administator email", | |
420 | EditView::new().content(&options.email), | |
421 | ) | |
ef4957e8 | 422 | .with_name("password-options"); |
c2eee468 | 423 | |
ccd34284 | 424 | InstallerView::new( |
90c0ea37 | 425 | state, |
ccd34284 CH |
426 | inner, |
427 | Box::new(|siv| { | |
15832d18 CH |
428 | let options = siv.call_on_name("password-options", |view: &mut FormView| { |
429 | let root_password = view | |
430 | .get_value::<EditView, _>(0) | |
431 | .ok_or("failed to retrieve password")?; | |
432 | ||
433 | let confirm_password = view | |
434 | .get_value::<EditView, _>(1) | |
435 | .ok_or("failed to retrieve password confirmation")?; | |
ef4957e8 | 436 | |
15832d18 CH |
437 | let email = view |
438 | .get_value::<EditView, _>(2) | |
439 | .ok_or("failed to retrieve email")?; | |
ef4957e8 | 440 | |
b82fff5d CH |
441 | if root_password.len() < 5 { |
442 | Err("password too short") | |
443 | } else if root_password != confirm_password { | |
ef4957e8 | 444 | Err("passwords do not match") |
03888174 | 445 | } else if email == "mail@example.invalid" { |
b82fff5d | 446 | Err("invalid email address") |
ef4957e8 CH |
447 | } else { |
448 | Ok(PasswordOptions { | |
449 | root_password, | |
15832d18 | 450 | email, |
ef4957e8 CH |
451 | }) |
452 | } | |
453 | }); | |
454 | ||
455 | match options { | |
456 | Some(Ok(options)) => { | |
a6e00ea6 CH |
457 | siv.with_user_data(|state: &mut InstallerState| { |
458 | state.options.password = options; | |
ef4957e8 CH |
459 | }); |
460 | ||
6ab6af4c | 461 | switch_to_next_screen(siv, InstallerStep::Network, &network_dialog); |
ef4957e8 CH |
462 | } |
463 | Some(Err(err)) => siv.add_layer(Dialog::info(format!("Invalid values: {err}"))), | |
464 | _ => siv.add_layer(Dialog::info("Invalid values")), | |
465 | } | |
ccd34284 CH |
466 | }), |
467 | ) | |
468 | } | |
469 | ||
470 | fn network_dialog(siv: &mut Cursive) -> InstallerView { | |
90c0ea37 CH |
471 | let state = siv.user_data::<InstallerState>().unwrap(); |
472 | let options = &state.options.network; | |
ccd34284 | 473 | |
617f2534 CH |
474 | let inner = FormView::new() |
475 | .child( | |
ccd34284 | 476 | "Management interface", |
efdd5d6f CH |
477 | SelectView::new() |
478 | .popup() | |
479 | .with_all_str(state.runtime_info.network.interfaces.keys()), | |
617f2534 | 480 | ) |
95c49008 CH |
481 | .child( |
482 | "Hostname (FQDN)", | |
483 | EditView::new().content(options.fqdn.to_string()), | |
484 | ) | |
617f2534 | 485 | .child( |
ccd34284 | 486 | "IP address (CIDR)", |
90c0ea37 | 487 | CidrAddressEditView::new().content(options.address.clone()), |
617f2534 CH |
488 | ) |
489 | .child( | |
ccd34284 CH |
490 | "Gateway address", |
491 | EditView::new().content(options.gateway.to_string()), | |
617f2534 CH |
492 | ) |
493 | .child( | |
1397feec | 494 | "DNS server address", |
ccd34284 | 495 | EditView::new().content(options.dns_server.to_string()), |
617f2534 | 496 | ) |
cd383717 | 497 | .with_name("network-options"); |
ccd34284 | 498 | |
947fe360 | 499 | InstallerView::new( |
90c0ea37 | 500 | state, |
947fe360 CH |
501 | inner, |
502 | Box::new(|siv| { | |
617f2534 CH |
503 | let options = siv.call_on_name("network-options", |view: &mut FormView| { |
504 | let ifname = view | |
505 | .get_value::<SelectView, _>(0) | |
506 | .ok_or("failed to retrieve management interface name")?; | |
cd383717 | 507 | |
617f2534 CH |
508 | let fqdn = view |
509 | .get_value::<EditView, _>(1) | |
95c49008 CH |
510 | .ok_or("failed to retrieve host FQDN")? |
511 | .parse::<Fqdn>() | |
512 | .map_err(|_| "failed to parse hostname".to_owned())?; | |
617f2534 CH |
513 | |
514 | let address = view | |
515 | .get_value::<CidrAddressEditView, _>(2) | |
516 | .ok_or("failed to retrieve host address")?; | |
517 | ||
518 | let gateway = view | |
519 | .get_value::<EditView, _>(3) | |
520 | .ok_or("failed to retrieve gateway address")? | |
521 | .parse::<IpAddr>() | |
522 | .map_err(|err| err.to_string())?; | |
523 | ||
524 | let dns_server = view | |
525 | .get_value::<EditView, _>(3) | |
526 | .ok_or("failed to retrieve DNS server address")? | |
527 | .parse::<IpAddr>() | |
528 | .map_err(|err| err.to_string())?; | |
529 | ||
530 | if address.addr().is_ipv4() != gateway.is_ipv4() { | |
531 | Err("host and gateway IP address version must not differ".to_owned()) | |
532 | } else if address.addr().is_ipv4() != dns_server.is_ipv4() { | |
533 | Err("host and DNS IP address version must not differ".to_owned()) | |
95c49008 | 534 | } else if fqdn.to_string().chars().all(|c| c.is_ascii_digit()) { |
b82fff5d CH |
535 | // Not supported/allowed on Debian |
536 | Err("hostname cannot be purely numeric".to_owned()) | |
95c49008 CH |
537 | } else if fqdn.to_string().ends_with(".invalid") { |
538 | Err("hostname does not look valid".to_owned()) | |
617f2534 CH |
539 | } else { |
540 | Ok(NetworkOptions { | |
541 | ifname, | |
542 | fqdn, | |
543 | address, | |
544 | gateway, | |
545 | dns_server, | |
546 | }) | |
547 | } | |
cd383717 CH |
548 | }); |
549 | ||
617f2534 CH |
550 | match options { |
551 | Some(Ok(options)) => { | |
a6e00ea6 CH |
552 | siv.with_user_data(|state: &mut InstallerState| { |
553 | state.options.network = options; | |
617f2534 | 554 | }); |
cd383717 | 555 | |
6ab6af4c | 556 | switch_to_next_screen(siv, InstallerStep::Summary, &summary_dialog); |
617f2534 CH |
557 | } |
558 | Some(Err(err)) => siv.add_layer(Dialog::info(format!("Invalid values: {err}"))), | |
559 | _ => siv.add_layer(Dialog::info("Invalid values")), | |
cd383717 | 560 | } |
947fe360 CH |
561 | }), |
562 | ) | |
563 | } | |
564 | ||
b7828c87 | 565 | pub struct SummaryOption { |
947fe360 CH |
566 | name: &'static str, |
567 | value: String, | |
568 | } | |
569 | ||
570 | impl SummaryOption { | |
571 | pub fn new<S: Into<String>>(name: &'static str, value: S) -> Self { | |
572 | Self { | |
573 | name, | |
574 | value: value.into(), | |
575 | } | |
576 | } | |
577 | } | |
578 | ||
579 | impl TableViewItem for SummaryOption { | |
580 | fn get_column(&self, name: &str) -> String { | |
581 | match name { | |
582 | "name" => self.name.to_owned(), | |
583 | "value" => self.value.clone(), | |
584 | _ => unreachable!(), | |
585 | } | |
586 | } | |
587 | } | |
588 | ||
589 | fn summary_dialog(siv: &mut Cursive) -> InstallerView { | |
90c0ea37 | 590 | let state = siv.user_data::<InstallerState>().unwrap(); |
947fe360 CH |
591 | |
592 | let inner = LinearLayout::vertical() | |
593 | .child(PaddedView::lrtb( | |
594 | 0, | |
595 | 0, | |
596 | 1, | |
597 | 2, | |
598 | TableView::new() | |
599 | .columns(&[ | |
600 | ("name".to_owned(), "Option".to_owned()), | |
601 | ("value".to_owned(), "Selected value".to_owned()), | |
602 | ]) | |
b107a892 | 603 | .items(state.options.to_summary(&state.locales)), |
947fe360 CH |
604 | )) |
605 | .child( | |
606 | LinearLayout::horizontal() | |
607 | .child(DummyView.full_width()) | |
608 | .child(Checkbox::new().with_name("reboot-after-install")) | |
609 | .child( | |
610 | TextView::new(" Automatically reboot after successful installation").no_wrap(), | |
611 | ) | |
612 | .child(DummyView.full_width()), | |
613 | ) | |
614 | .child(PaddedView::lrtb( | |
615 | 1, | |
616 | 1, | |
617 | 1, | |
618 | 0, | |
619 | LinearLayout::horizontal() | |
620 | .child(abort_install_button()) | |
621 | .child(DummyView.full_width()) | |
622 | .child(Button::new("Previous", switch_to_prev_screen)) | |
623 | .child(DummyView) | |
81f9348c | 624 | .child(Button::new("Install", |siv| { |
ea15ca43 CH |
625 | let reboot = siv |
626 | .find_name("reboot-after-install") | |
627 | .map(|v: ViewRef<Checkbox>| v.is_checked()) | |
628 | .unwrap_or_default(); | |
629 | ||
630 | siv.with_user_data(|state: &mut InstallerState| { | |
631 | state.options.reboot = reboot; | |
632 | }); | |
633 | ||
81f9348c CH |
634 | switch_to_next_screen(siv, InstallerStep::Install, &install_progress_dialog); |
635 | })), | |
947fe360 CH |
636 | )); |
637 | ||
90c0ea37 | 638 | InstallerView::with_raw(state, inner) |
c2eee468 | 639 | } |
81f9348c CH |
640 | |
641 | fn install_progress_dialog(siv: &mut Cursive) -> InstallerView { | |
642 | // Ensure the screen is updated independently of keyboard events and such | |
643 | siv.set_autorefresh(true); | |
644 | ||
3285f0ad | 645 | let cb_sink = siv.cb_sink().clone(); |
81f9348c | 646 | let state = siv.user_data::<InstallerState>().unwrap(); |
3285f0ad CH |
647 | let progress_text = TextContent::new("starting the installation .."); |
648 | ||
649 | let progress_task = { | |
650 | let progress_text = progress_text.clone(); | |
651 | let options = state.options.clone(); | |
652 | move |counter: Counter| { | |
653 | let child = { | |
654 | use std::process::{Command, Stdio}; | |
655 | ||
656 | #[cfg(not(debug_assertions))] | |
657 | let (path, args, envs): (&str, [&str; 1], [(&str, &str); 0]) = | |
658 | ("proxmox-low-level-installer", ["start-session"], []); | |
659 | ||
660 | #[cfg(debug_assertions)] | |
661 | let (path, args, envs) = ( | |
662 | PathBuf::from("../proxmox-low-level-installer") | |
663 | .canonicalize() | |
664 | .unwrap(), | |
665 | ["-t", "start-session"], | |
666 | [("PERL5LIB", PathBuf::from("..").canonicalize().unwrap())], | |
667 | ); | |
668 | ||
669 | Command::new(path) | |
670 | .args(args) | |
671 | .envs(envs) | |
672 | .stdin(Stdio::piped()) | |
673 | .stdout(Stdio::piped()) | |
674 | .spawn() | |
675 | }; | |
676 | ||
677 | let mut child = match child { | |
678 | Ok(child) => child, | |
679 | Err(err) => { | |
680 | let _ = cb_sink.send(Box::new(move |siv| { | |
681 | siv.add_layer( | |
682 | Dialog::text(err.to_string()) | |
683 | .title("Error") | |
684 | .button("Ok", Cursive::quit), | |
685 | ); | |
686 | })); | |
687 | return; | |
688 | } | |
689 | }; | |
690 | ||
691 | let inner = || { | |
692 | let reader = child.stdout.take().map(BufReader::new)?; | |
693 | let mut writer = child.stdin.take()?; | |
694 | ||
695 | serde_json::to_writer(&mut writer, &InstallConfig::from(options)).unwrap(); | |
696 | writeln!(writer).unwrap(); | |
697 | ||
698 | let writer = Arc::new(Mutex::new(writer)); | |
699 | ||
700 | for line in reader.lines() { | |
701 | let line = match line { | |
702 | Ok(line) => line, | |
703 | Err(_) => break, | |
704 | }; | |
705 | ||
706 | let msg = match line.parse::<UiMessage>() { | |
707 | Ok(msg) => msg, | |
708 | Err(_stray) => { | |
709 | // eprintln!("low-level installer: {stray}"); | |
710 | continue; | |
711 | } | |
712 | }; | |
713 | ||
714 | match msg { | |
715 | UiMessage::Info(s) => cb_sink.send(Box::new(|siv| { | |
716 | siv.add_layer(Dialog::info(s).title("Information")); | |
717 | })), | |
718 | UiMessage::Error(s) => cb_sink.send(Box::new(|siv| { | |
719 | siv.add_layer(Dialog::info(s).title("Error")); | |
720 | })), | |
721 | UiMessage::Prompt(s) => cb_sink.send({ | |
722 | let writer = writer.clone(); | |
723 | Box::new(move |siv| { | |
724 | yes_no_dialog( | |
725 | siv, | |
726 | "Prompt", | |
727 | &s, | |
728 | Box::new({ | |
729 | let writer = writer.clone(); | |
730 | move |_| { | |
731 | if let Ok(mut writer) = writer.lock() { | |
732 | let _ = writeln!(writer, "ok"); | |
733 | } | |
734 | } | |
735 | }), | |
736 | Box::new(move |_| { | |
737 | if let Ok(mut writer) = writer.lock() { | |
738 | let _ = writeln!(writer); | |
739 | } | |
740 | }), | |
741 | ); | |
742 | }) | |
743 | }), | |
744 | UiMessage::Progress(ratio, s) => { | |
745 | counter.set(ratio); | |
746 | progress_text.set_content(s); | |
747 | Ok(()) | |
748 | } | |
a8fbe0ff TL |
749 | UiMessage::Finished(success, msg) => { |
750 | counter.set(100); | |
751 | progress_text.set_content(msg.to_owned()); | |
752 | cb_sink.send(Box::new(move |siv| { | |
753 | let title = if success { "Success" } else { "Failure" }; | |
754 | siv.add_layer( | |
755 | Dialog::text(msg).title(title).button("Reboot", |s| s.quit()), | |
756 | ); | |
757 | })) | |
758 | } | |
3285f0ad CH |
759 | } |
760 | .unwrap(); | |
81f9348c | 761 | } |
3285f0ad | 762 | |
3285f0ad CH |
763 | Some(()) |
764 | }; | |
765 | ||
766 | if inner().is_none() { | |
767 | cb_sink | |
768 | .send(Box::new(|siv| { | |
769 | siv.add_layer( | |
770 | Dialog::text("low-level installer exited early") | |
771 | .title("Error") | |
772 | .button("Exit", Cursive::quit), | |
773 | ); | |
774 | })) | |
775 | .unwrap(); | |
81f9348c | 776 | } |
3285f0ad CH |
777 | } |
778 | }; | |
81f9348c | 779 | |
3285f0ad | 780 | let progress_bar = ProgressBar::new().with_task(progress_task).full_width(); |
81f9348c CH |
781 | let inner = PaddedView::lrtb( |
782 | 1, | |
783 | 1, | |
784 | 1, | |
785 | 1, | |
786 | LinearLayout::vertical() | |
787 | .child(PaddedView::lrtb(1, 1, 0, 0, progress_bar)) | |
788 | .child(DummyView) | |
e2a1e0c2 CH |
789 | .child(TextView::new_with_content(progress_text).center()) |
790 | .child(PaddedView::lrtb( | |
791 | 1, | |
792 | 1, | |
793 | 1, | |
794 | 0, | |
795 | LinearLayout::horizontal().child(abort_install_button()), | |
796 | )), | |
81f9348c CH |
797 | ); |
798 | ||
799 | InstallerView::with_raw(state, inner) | |
800 | } | |
3285f0ad CH |
801 | |
802 | enum UiMessage { | |
803 | Info(String), | |
804 | Error(String), | |
805 | Prompt(String), | |
a8fbe0ff | 806 | Finished(bool, String), |
3285f0ad CH |
807 | Progress(usize, String), |
808 | } | |
809 | ||
810 | impl FromStr for UiMessage { | |
811 | type Err = String; | |
812 | ||
813 | fn from_str(s: &str) -> Result<Self, Self::Err> { | |
814 | let (ty, rest) = s.split_once(": ").ok_or("invalid message: no type")?; | |
815 | ||
816 | match ty { | |
817 | "message" => Ok(UiMessage::Info(rest.to_owned())), | |
818 | "error" => Ok(UiMessage::Error(rest.to_owned())), | |
819 | "prompt" => Ok(UiMessage::Prompt(rest.to_owned())), | |
a8fbe0ff TL |
820 | "finished" => { |
821 | let (state, rest) = rest.split_once(", ").ok_or("invalid message: no state")?; | |
822 | Ok(UiMessage::Finished(state == "ok", rest.to_owned())) | |
823 | } | |
3285f0ad CH |
824 | "progress" => { |
825 | let (percent, rest) = rest.split_once(' ').ok_or("invalid progress message")?; | |
826 | Ok(UiMessage::Progress( | |
827 | percent | |
828 | .parse::<f64>() | |
a8fbe0ff | 829 | .map(|v| (v * 100.).floor() as usize) |
3285f0ad CH |
830 | .map_err(|err| err.to_string())?, |
831 | rest.to_owned(), | |
832 | )) | |
833 | } | |
834 | _ => Err("invalid message type".to_owned()), | |
835 | } | |
836 | } | |
837 | } |