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