]>
Commit | Line | Data |
---|---|---|
2789d95b CH |
1 | #![forbid(unsafe_code)] |
2 | ||
dba905bf | 3 | use std::{collections::HashMap, env, net::IpAddr}; |
83071f35 | 4 | |
183e2a76 | 5 | use cursive::{ |
ccf3b075 | 6 | event::Event, |
08ad8ed6 CH |
7 | theme::{ColorStyle, Effect, PaletteColor, Style}, |
8 | view::{Nameable, Offset, Resizable, ViewWrapper}, | |
183e2a76 | 9 | views::{ |
08ad8ed6 | 10 | Button, Checkbox, Dialog, DummyView, EditView, Layer, LinearLayout, PaddedView, Panel, |
dba905bf | 11 | ResizedView, ScrollView, SelectView, StackView, TextView, ViewRef, |
183e2a76 | 12 | }, |
08ad8ed6 | 13 | Cursive, CursiveRunnable, ScreenId, View, XY, |
183e2a76 | 14 | }; |
32368ac3 | 15 | |
298a4de0 DC |
16 | use regex::Regex; |
17 | ||
32368ac3 | 18 | mod options; |
86c48f76 AL |
19 | use options::InstallerOptions; |
20 | ||
21 | use proxmox_installer_common::{ | |
22 | options::{BootdiskOptions, NetworkOptions, PasswordOptions, TimezoneOptions}, | |
9a848580 | 23 | setup::{installer_setup, LocaleInfo, ProxmoxProduct, RuntimeInfo, SetupInfo}, |
86c48f76 AL |
24 | utils::Fqdn, |
25 | }; | |
32368ac3 WB |
26 | |
27 | mod setup; | |
32368ac3 WB |
28 | |
29 | mod system; | |
30 | ||
32368ac3 | 31 | mod views; |
e1a11f79 | 32 | use views::{ |
dba905bf CH |
33 | BootdiskOptionsView, CidrAddressEditView, FormView, InstallProgressView, TableView, |
34 | TableViewItem, TimezoneOptionsView, | |
e1a11f79 | 35 | }; |
183e2a76 CH |
36 | |
37 | // TextView::center() seems to garble the first two lines, so fix it manually here. | |
08ad8ed6 | 38 | const PROXMOX_LOGO: &str = r#" |
e730b3fe CH |
39 | ____ |
40 | | _ \ _ __ _____ ___ __ ___ _____ __ | |
41 | | |_) | '__/ _ \ \/ / '_ ` _ \ / _ \ \/ / | |
42 | | __/| | | (_) > <| | | | | | (_) > < | |
43 | |_| |_| \___/_/\_\_| |_| |_|\___/_/\_\ "#; | |
183e2a76 CH |
44 | |
45 | struct InstallerView { | |
08ad8ed6 | 46 | view: ResizedView<Dialog>, |
183e2a76 CH |
47 | } |
48 | ||
49 | impl InstallerView { | |
90c0ea37 CH |
50 | pub fn new<T: View>( |
51 | state: &InstallerState, | |
52 | view: T, | |
53 | next_cb: Box<dyn Fn(&mut Cursive)>, | |
1d8fcd72 | 54 | focus_next: bool, |
90c0ea37 | 55 | ) -> Self { |
1d8fcd72 DC |
56 | let mut bbar = LinearLayout::horizontal() |
57 | .child(abort_install_button()) | |
58 | .child(DummyView.full_width()) | |
59 | .child(Button::new("Previous", switch_to_prev_screen)) | |
60 | .child(DummyView) | |
61 | .child(Button::new("Next", next_cb)); | |
62 | let _ = bbar.set_focus_index(4); // ignore errors | |
63 | let mut inner = LinearLayout::vertical() | |
8807d547 | 64 | .child(PaddedView::lrtb(0, 0, 1, 1, view)) |
1d8fcd72 DC |
65 | .child(PaddedView::lrtb(1, 1, 0, 0, bbar)); |
66 | if focus_next { | |
67 | let _ = inner.set_focus_index(1); // ignore errors | |
68 | } | |
f522afb1 | 69 | |
90c0ea37 | 70 | Self::with_raw(state, inner) |
f522afb1 CH |
71 | } |
72 | ||
90c0ea37 CH |
73 | pub fn with_raw(state: &InstallerState, view: impl View) -> Self { |
74 | let setup = &state.setup_info; | |
75 | ||
90c0ea37 CH |
76 | let title = format!( |
77 | "{} ({}-{}) Installer", | |
fbfd1838 | 78 | setup.config.fullname, setup.iso_info.release, setup.iso_info.isorelease |
90c0ea37 CH |
79 | ); |
80 | ||
08ad8ed6 | 81 | let inner = Dialog::around(view).title(title); |
8b43c2d3 | 82 | |
183e2a76 | 83 | Self { |
8b43c2d3 CH |
84 | // Limit the maximum to something reasonable, such that it won't get spread out much |
85 | // depending on the screen. | |
86 | view: ResizedView::with_max_size((120, 40), inner), | |
183e2a76 CH |
87 | } |
88 | } | |
89 | } | |
90 | ||
91 | impl ViewWrapper for InstallerView { | |
08ad8ed6 CH |
92 | cursive::wrap_impl!(self.view: ResizedView<Dialog>); |
93 | } | |
94 | ||
95 | struct InstallerBackgroundView { | |
96 | view: StackView, | |
97 | } | |
98 | ||
99 | impl InstallerBackgroundView { | |
1936156e | 100 | pub fn new() -> Self { |
08ad8ed6 CH |
101 | let style = Style { |
102 | effects: Effect::Bold.into(), | |
103 | color: ColorStyle::back(PaletteColor::View), | |
104 | }; | |
105 | ||
106 | let mut view = StackView::new(); | |
107 | view.add_fullscreen_layer(Layer::with_color( | |
108 | DummyView | |
109 | .full_width() | |
1936156e | 110 | .fixed_height(PROXMOX_LOGO.lines().count() + 1), |
08ad8ed6 CH |
111 | ColorStyle::back(PaletteColor::View), |
112 | )); | |
113 | view.add_transparent_layer_at( | |
114 | XY { | |
115 | x: Offset::Center, | |
116 | y: Offset::Absolute(0), | |
117 | }, | |
1936156e | 118 | TextView::new(PROXMOX_LOGO).style(style), |
08ad8ed6 CH |
119 | ); |
120 | ||
121 | Self { view } | |
122 | } | |
123 | } | |
124 | ||
125 | impl ViewWrapper for InstallerBackgroundView { | |
126 | cursive::wrap_impl!(self.view: StackView); | |
183e2a76 CH |
127 | } |
128 | ||
6ab6af4c CH |
129 | #[derive(Clone, Eq, Hash, PartialEq)] |
130 | enum InstallerStep { | |
131 | Licence, | |
132 | Bootdisk, | |
133 | Timezone, | |
134 | Password, | |
135 | Network, | |
136 | Summary, | |
81f9348c | 137 | Install, |
6ab6af4c CH |
138 | } |
139 | ||
ed191e60 | 140 | #[derive(Clone)] |
a6e00ea6 | 141 | struct InstallerState { |
ed191e60 | 142 | options: InstallerOptions, |
4296d200 | 143 | setup_info: SetupInfo, |
2473d5dc | 144 | runtime_info: RuntimeInfo, |
4296d200 | 145 | locales: LocaleInfo, |
6ab6af4c | 146 | steps: HashMap<InstallerStep, ScreenId>, |
cae15523 | 147 | in_test_mode: bool, |
ed191e60 CH |
148 | } |
149 | ||
183e2a76 CH |
150 | fn main() { |
151 | let mut siv = cursive::termion(); | |
152 | ||
cae15523 CH |
153 | let in_test_mode = match env::args().nth(1).as_deref() { |
154 | Some("-t") => true, | |
155 | ||
156 | // Always force the test directory in debug builds | |
c0706374 | 157 | _ => cfg!(debug_assertions), |
cae15523 CH |
158 | }; |
159 | ||
2f0b4f7c | 160 | let (setup_info, locales, runtime_info) = match installer_setup(in_test_mode) { |
4296d200 CH |
161 | Ok(result) => result, |
162 | Err(err) => initial_setup_error(&mut siv, &err), | |
163 | }; | |
130fb96a | 164 | |
ccf3b075 CH |
165 | siv.clear_global_callbacks(Event::CtrlChar('c')); |
166 | siv.set_on_pre_event(Event::CtrlChar('c'), trigger_abort_install_dialog); | |
167 | ||
a6e00ea6 | 168 | siv.set_user_data(InstallerState { |
ed191e60 | 169 | options: InstallerOptions { |
5dd6b6ed | 170 | bootdisk: BootdiskOptions::defaults_from(&runtime_info.disks[0]), |
0e42d278 | 171 | timezone: TimezoneOptions::defaults_from(&runtime_info, &locales), |
d09fe568 | 172 | password: Default::default(), |
2f0b4f7c | 173 | network: NetworkOptions::defaults_from(&setup_info, &runtime_info.network), |
f81ba3ed | 174 | autoreboot: false, |
e9557e62 | 175 | }, |
2f0b4f7c | 176 | setup_info, |
2473d5dc | 177 | runtime_info, |
4296d200 | 178 | locales, |
6ab6af4c | 179 | steps: HashMap::new(), |
cae15523 | 180 | in_test_mode, |
e9557e62 CH |
181 | }); |
182 | ||
6ab6af4c | 183 | switch_to_next_screen(&mut siv, InstallerStep::Licence, &license_dialog); |
183e2a76 CH |
184 | siv.run(); |
185 | } | |
186 | ||
79ae84de CH |
187 | /// Anything that can be done late in the setup and will not result in fatal errors. |
188 | fn installer_setup_late(siv: &mut Cursive) { | |
636da45e | 189 | let state = siv.user_data::<InstallerState>().cloned().unwrap(); |
79ae84de CH |
190 | |
191 | if !state.in_test_mode { | |
192 | let kmap_id = &state.options.timezone.kb_layout; | |
193 | if let Some(kmap) = state.locales.kmap.get(kmap_id) { | |
194 | if let Err(err) = system::set_keyboard_layout(kmap) { | |
082f3a70 | 195 | display_setup_warning(siv, &format!("Failed to apply keyboard layout: {err}")); |
79ae84de CH |
196 | } |
197 | } | |
198 | } | |
082f3a70 | 199 | |
f9960aa7 NU |
200 | if state.runtime_info.total_memory < 1024 { |
201 | display_setup_warning( | |
202 | siv, | |
203 | concat!( | |
204 | "Less than 1 GiB of usable memory detected, installation will probably fail.\n\n", | |
205 | "See 'System Requirements' in the documentation." | |
206 | ), | |
207 | ); | |
208 | } | |
209 | ||
636da45e CH |
210 | if state.setup_info.config.product == ProxmoxProduct::PVE && !state.runtime_info.hvm_supported { |
211 | display_setup_warning( | |
212 | siv, | |
213 | concat!( | |
214 | "No support for hardware-accelerated KVM virtualization detected.\n\n", | |
215 | "Check BIOS settings for Intel VT / AMD-V / SVM." | |
216 | ), | |
217 | ); | |
082f3a70 | 218 | } |
79ae84de CH |
219 | } |
220 | ||
4296d200 CH |
221 | fn initial_setup_error(siv: &mut CursiveRunnable, message: &str) -> ! { |
222 | siv.add_layer( | |
223 | Dialog::around(TextView::new(message)) | |
224 | .title("Installer setup error") | |
225 | .button("Ok", Cursive::quit), | |
226 | ); | |
227 | siv.run(); | |
228 | ||
229 | std::process::exit(1); | |
230 | } | |
231 | ||
082f3a70 CH |
232 | fn display_setup_warning(siv: &mut Cursive, message: &str) { |
233 | siv.add_layer(Dialog::info(message).title("Warning")); | |
234 | } | |
235 | ||
6ab6af4c CH |
236 | fn switch_to_next_screen( |
237 | siv: &mut Cursive, | |
238 | step: InstallerStep, | |
239 | constructor: &dyn Fn(&mut Cursive) -> InstallerView, | |
240 | ) { | |
08ad8ed6 | 241 | let state = siv.user_data::<InstallerState>().cloned().unwrap(); |
79ae84de | 242 | let is_first_screen = state.steps.is_empty(); |
08ad8ed6 | 243 | |
6ab6af4c | 244 | // Check if the screen already exists; if yes, then simply switch to it. |
08ad8ed6 CH |
245 | if let Some(screen_id) = state.steps.get(&step) { |
246 | siv.set_screen(*screen_id); | |
93f4fdfa CH |
247 | |
248 | // The summary view cannot be cached (otherwise it would display stale values). Thus | |
249 | // replace it if the screen is switched to. | |
250 | // TODO: Could be done by e.g. having all the main dialog views implement some sort of | |
251 | // .refresh(), which can be called if the view is switched to. | |
252 | if step == InstallerStep::Summary { | |
253 | let view = constructor(siv); | |
254 | siv.screen_mut().pop_layer(); | |
255 | siv.screen_mut().add_layer(view); | |
256 | } | |
257 | ||
08ad8ed6 | 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 | 264 | |
1936156e CH |
265 | siv.screen_mut().add_transparent_layer_at( |
266 | XY { | |
267 | x: Offset::Parent(0), | |
268 | y: Offset::Parent(0), | |
269 | }, | |
270 | InstallerBackgroundView::new(), | |
271 | ); | |
08ad8ed6 | 272 | |
5e73fcfe | 273 | siv.screen_mut().add_layer(v); |
79ae84de CH |
274 | |
275 | // If this is the first screen to be added, execute our late setup first. | |
276 | // Needs to be done here, at the end, to ensure that any potential layers get added to | |
277 | // the right screen and are on top. | |
278 | if is_first_screen { | |
279 | installer_setup_late(siv); | |
280 | } | |
183e2a76 CH |
281 | } |
282 | ||
64220ff1 CH |
283 | fn switch_to_prev_screen(siv: &mut Cursive) { |
284 | let id = siv.active_screen().saturating_sub(1); | |
285 | siv.set_screen(id); | |
286 | } | |
287 | ||
28a55aea | 288 | fn prompt_dialog( |
183e2a76 CH |
289 | siv: &mut Cursive, |
290 | title: &str, | |
291 | text: &str, | |
28a55aea | 292 | yes_text: &str, |
deebe07f | 293 | callback_yes: Box<dyn Fn(&mut Cursive)>, |
28a55aea | 294 | no_text: &str, |
deebe07f | 295 | callback_no: Box<dyn Fn(&mut Cursive)>, |
183e2a76 CH |
296 | ) { |
297 | siv.add_layer( | |
298 | Dialog::around(TextView::new(text)) | |
299 | .title(title) | |
28a55aea | 300 | .button(no_text, move |siv| { |
deebe07f CH |
301 | siv.pop_layer(); |
302 | callback_no(siv); | |
303 | }) | |
28a55aea | 304 | .button(yes_text, move |siv| { |
deebe07f CH |
305 | siv.pop_layer(); |
306 | callback_yes(siv); | |
307 | }), | |
183e2a76 CH |
308 | ) |
309 | } | |
310 | ||
ccf3b075 | 311 | fn trigger_abort_install_dialog(siv: &mut Cursive) { |
58869243 CH |
312 | #[cfg(debug_assertions)] |
313 | siv.quit(); | |
314 | ||
315 | #[cfg(not(debug_assertions))] | |
28a55aea | 316 | prompt_dialog( |
ccf3b075 CH |
317 | siv, |
318 | "Abort installation?", | |
319 | "Are you sure you want to abort the installation?", | |
28a55aea | 320 | "Yes", |
deebe07f | 321 | Box::new(Cursive::quit), |
28a55aea | 322 | "No", |
deebe07f | 323 | Box::new(|_| {}), |
ccf3b075 CH |
324 | ) |
325 | } | |
326 | ||
183e2a76 | 327 | fn abort_install_button() -> Button { |
ccf3b075 | 328 | Button::new("Abort", trigger_abort_install_dialog) |
183e2a76 CH |
329 | } |
330 | ||
8c9af1e7 CH |
331 | fn get_eula(setup: &SetupInfo) -> String { |
332 | let mut path = setup.locations.iso.clone(); | |
333 | path.push("EULA"); | |
334 | ||
335 | std::fs::read_to_string(path) | |
8f5fdd21 | 336 | .unwrap_or_else(|_| "< Debug build - ignoring non-existing EULA >".to_owned()) |
183e2a76 CH |
337 | } |
338 | ||
90c0ea37 CH |
339 | fn license_dialog(siv: &mut Cursive) -> InstallerView { |
340 | let state = siv.user_data::<InstallerState>().unwrap(); | |
341 | ||
1d8fcd72 DC |
342 | let mut bbar = LinearLayout::horizontal() |
343 | .child(abort_install_button()) | |
344 | .child(DummyView.full_width()) | |
345 | .child(Button::new("I agree", |siv| { | |
346 | switch_to_next_screen(siv, InstallerStep::Bootdisk, &bootdisk_dialog) | |
347 | })); | |
348 | let _ = bbar.set_focus_index(2); // ignore errors | |
349 | ||
350 | let mut inner = LinearLayout::vertical() | |
183e2a76 CH |
351 | .child(PaddedView::lrtb( |
352 | 0, | |
353 | 0, | |
354 | 1, | |
355 | 0, | |
356 | TextView::new("END USER LICENSE AGREEMENT (EULA)").center(), | |
357 | )) | |
8b43c2d3 | 358 | .child(Panel::new(ScrollView::new( |
8c9af1e7 | 359 | TextView::new(get_eula(&state.setup_info)).center(), |
183e2a76 | 360 | ))) |
1d8fcd72 DC |
361 | .child(PaddedView::lrtb(1, 1, 1, 0, bbar)); |
362 | ||
363 | let _ = inner.set_focus_index(2); // ignore errors | |
183e2a76 | 364 | |
90c0ea37 | 365 | InstallerView::with_raw(state, inner) |
183e2a76 CH |
366 | } |
367 | ||
e70f1b2f | 368 | fn bootdisk_dialog(siv: &mut Cursive) -> InstallerView { |
a6e00ea6 | 369 | let state = siv.user_data::<InstallerState>().cloned().unwrap(); |
64220ff1 | 370 | |
f522afb1 | 371 | InstallerView::new( |
90c0ea37 | 372 | &state, |
521dfff5 | 373 | BootdiskOptionsView::new(siv, &state.runtime_info, &state.options.bootdisk) |
ed191e60 | 374 | .with_name("bootdisk-options"), |
f522afb1 | 375 | Box::new(|siv| { |
994c4ff0 CH |
376 | let options = siv.call_on_name("bootdisk-options", BootdiskOptionsView::get_values); |
377 | ||
378 | match options { | |
379 | Some(Ok(options)) => { | |
380 | siv.with_user_data(|state: &mut InstallerState| { | |
381 | state.options.bootdisk = options; | |
382 | }); | |
383 | ||
384 | switch_to_next_screen(siv, InstallerStep::Timezone, &timezone_dialog); | |
385 | } | |
386 | ||
387 | Some(Err(err)) => siv.add_layer(Dialog::info(format!("Invalid values: {err}"))), | |
388 | _ => siv.add_layer(Dialog::info("Invalid values")), | |
93ebe7bd | 389 | } |
f522afb1 | 390 | }), |
1d8fcd72 | 391 | true, |
f522afb1 | 392 | ) |
64220ff1 CH |
393 | } |
394 | ||
a142f457 | 395 | fn timezone_dialog(siv: &mut Cursive) -> InstallerView { |
90c0ea37 CH |
396 | let state = siv.user_data::<InstallerState>().unwrap(); |
397 | let options = &state.options.timezone; | |
a142f457 | 398 | |
a142f457 | 399 | InstallerView::new( |
90c0ea37 | 400 | state, |
e1a11f79 | 401 | TimezoneOptionsView::new(&state.locales, options).with_name("timezone-options"), |
a142f457 | 402 | Box::new(|siv| { |
e1a11f79 | 403 | let options = siv.call_on_name("timezone-options", TimezoneOptionsView::get_values); |
93ebe7bd | 404 | |
767843f9 CH |
405 | match options { |
406 | Some(Ok(options)) => { | |
a6e00ea6 CH |
407 | siv.with_user_data(|state: &mut InstallerState| { |
408 | state.options.timezone = options; | |
767843f9 CH |
409 | }); |
410 | ||
6ab6af4c | 411 | switch_to_next_screen(siv, InstallerStep::Password, &password_dialog); |
767843f9 CH |
412 | } |
413 | Some(Err(err)) => siv.add_layer(Dialog::info(format!("Invalid values: {err}"))), | |
414 | _ => siv.add_layer(Dialog::info("Invalid values")), | |
93ebe7bd | 415 | } |
a142f457 | 416 | }), |
1d8fcd72 | 417 | true, |
a142f457 | 418 | ) |
183e2a76 | 419 | } |
c2eee468 CH |
420 | |
421 | fn password_dialog(siv: &mut Cursive) -> InstallerView { | |
90c0ea37 CH |
422 | let state = siv.user_data::<InstallerState>().unwrap(); |
423 | let options = &state.options.password; | |
c2eee468 | 424 | |
15832d18 CH |
425 | let inner = FormView::new() |
426 | .child("Root password", EditView::new().secret()) | |
427 | .child("Confirm root password", EditView::new().secret()) | |
90c0ea37 | 428 | .child( |
0f39ba93 | 429 | "Administrator email", |
90c0ea37 CH |
430 | EditView::new().content(&options.email), |
431 | ) | |
ef4957e8 | 432 | .with_name("password-options"); |
c2eee468 | 433 | |
ccd34284 | 434 | InstallerView::new( |
90c0ea37 | 435 | state, |
ccd34284 CH |
436 | inner, |
437 | Box::new(|siv| { | |
15832d18 CH |
438 | let options = siv.call_on_name("password-options", |view: &mut FormView| { |
439 | let root_password = view | |
440 | .get_value::<EditView, _>(0) | |
441 | .ok_or("failed to retrieve password")?; | |
442 | ||
443 | let confirm_password = view | |
444 | .get_value::<EditView, _>(1) | |
445 | .ok_or("failed to retrieve password confirmation")?; | |
ef4957e8 | 446 | |
15832d18 CH |
447 | let email = view |
448 | .get_value::<EditView, _>(2) | |
449 | .ok_or("failed to retrieve email")?; | |
ef4957e8 | 450 | |
298a4de0 DC |
451 | let email_regex = |
452 | Regex::new(r"^[\w\+\-\~]+(\.[\w\+\-\~]+)*@[a-zA-Z0-9\-]+(\.[a-zA-Z0-9\-]+)*$") | |
453 | .unwrap(); | |
454 | ||
b82fff5d | 455 | if root_password.len() < 5 { |
fe64752d | 456 | Err("password too short, must be at least 5 characters long") |
b82fff5d | 457 | } else if root_password != confirm_password { |
ef4957e8 | 458 | Err("passwords do not match") |
03888174 | 459 | } else if email == "mail@example.invalid" { |
b82fff5d | 460 | Err("invalid email address") |
298a4de0 DC |
461 | } else if !email_regex.is_match(&email) { |
462 | Err("Email does not look like a valid address (user@domain.tld)") | |
ef4957e8 CH |
463 | } else { |
464 | Ok(PasswordOptions { | |
465 | root_password, | |
15832d18 | 466 | email, |
ef4957e8 CH |
467 | }) |
468 | } | |
469 | }); | |
470 | ||
471 | match options { | |
472 | Some(Ok(options)) => { | |
a6e00ea6 CH |
473 | siv.with_user_data(|state: &mut InstallerState| { |
474 | state.options.password = options; | |
ef4957e8 CH |
475 | }); |
476 | ||
6ab6af4c | 477 | switch_to_next_screen(siv, InstallerStep::Network, &network_dialog); |
ef4957e8 CH |
478 | } |
479 | Some(Err(err)) => siv.add_layer(Dialog::info(format!("Invalid values: {err}"))), | |
480 | _ => siv.add_layer(Dialog::info("Invalid values")), | |
481 | } | |
ccd34284 | 482 | }), |
1d8fcd72 | 483 | false, |
ccd34284 CH |
484 | ) |
485 | } | |
486 | ||
487 | fn network_dialog(siv: &mut Cursive) -> InstallerView { | |
90c0ea37 CH |
488 | let state = siv.user_data::<InstallerState>().unwrap(); |
489 | let options = &state.options.network; | |
55e5c9cd TL |
490 | let ifaces = state.runtime_info.network.interfaces.values(); |
491 | let ifnames = ifaces | |
492 | .clone() | |
493 | .map(|iface| (iface.render(), iface.name.clone())); | |
89dfa5c9 TL |
494 | let mut ifaces_selection = SelectView::new().popup().with_all(ifnames.clone()); |
495 | ||
8f0e9ab0 | 496 | // sort first to always have stable view |
89dfa5c9 | 497 | ifaces_selection.sort(); |
8f0e9ab0 SI |
498 | let selected = ifaces_selection |
499 | .iter() | |
500 | .position(|(_label, iface)| *iface == options.ifname) | |
501 | .unwrap_or(ifaces.len() - 1); | |
502 | ||
503 | ifaces_selection.set_selection(selected); | |
ccd34284 | 504 | |
617f2534 | 505 | let inner = FormView::new() |
89dfa5c9 | 506 | .child("Management interface", ifaces_selection) |
95c49008 CH |
507 | .child( |
508 | "Hostname (FQDN)", | |
509 | EditView::new().content(options.fqdn.to_string()), | |
510 | ) | |
617f2534 | 511 | .child( |
ccd34284 | 512 | "IP address (CIDR)", |
90c0ea37 | 513 | CidrAddressEditView::new().content(options.address.clone()), |
617f2534 CH |
514 | ) |
515 | .child( | |
ccd34284 CH |
516 | "Gateway address", |
517 | EditView::new().content(options.gateway.to_string()), | |
617f2534 CH |
518 | ) |
519 | .child( | |
1397feec | 520 | "DNS server address", |
ccd34284 | 521 | EditView::new().content(options.dns_server.to_string()), |
617f2534 | 522 | ) |
cd383717 | 523 | .with_name("network-options"); |
ccd34284 | 524 | |
947fe360 | 525 | InstallerView::new( |
90c0ea37 | 526 | state, |
947fe360 CH |
527 | inner, |
528 | Box::new(|siv| { | |
617f2534 CH |
529 | let options = siv.call_on_name("network-options", |view: &mut FormView| { |
530 | let ifname = view | |
531 | .get_value::<SelectView, _>(0) | |
532 | .ok_or("failed to retrieve management interface name")?; | |
cd383717 | 533 | |
617f2534 CH |
534 | let fqdn = view |
535 | .get_value::<EditView, _>(1) | |
95c49008 CH |
536 | .ok_or("failed to retrieve host FQDN")? |
537 | .parse::<Fqdn>() | |
55dc67ca | 538 | .map_err(|err| format!("hostname does not look valid:\n\n{err}"))?; |
617f2534 CH |
539 | |
540 | let address = view | |
541 | .get_value::<CidrAddressEditView, _>(2) | |
542 | .ok_or("failed to retrieve host address")?; | |
543 | ||
544 | let gateway = view | |
545 | .get_value::<EditView, _>(3) | |
546 | .ok_or("failed to retrieve gateway address")? | |
547 | .parse::<IpAddr>() | |
548 | .map_err(|err| err.to_string())?; | |
549 | ||
550 | let dns_server = view | |
d2b13ba7 | 551 | .get_value::<EditView, _>(4) |
617f2534 CH |
552 | .ok_or("failed to retrieve DNS server address")? |
553 | .parse::<IpAddr>() | |
554 | .map_err(|err| err.to_string())?; | |
555 | ||
556 | if address.addr().is_ipv4() != gateway.is_ipv4() { | |
557 | Err("host and gateway IP address version must not differ".to_owned()) | |
558 | } else if address.addr().is_ipv4() != dns_server.is_ipv4() { | |
559 | Err("host and DNS IP address version must not differ".to_owned()) | |
95c49008 CH |
560 | } else if fqdn.to_string().ends_with(".invalid") { |
561 | Err("hostname does not look valid".to_owned()) | |
617f2534 CH |
562 | } else { |
563 | Ok(NetworkOptions { | |
564 | ifname, | |
565 | fqdn, | |
566 | address, | |
567 | gateway, | |
568 | dns_server, | |
569 | }) | |
570 | } | |
cd383717 CH |
571 | }); |
572 | ||
617f2534 CH |
573 | match options { |
574 | Some(Ok(options)) => { | |
a6e00ea6 CH |
575 | siv.with_user_data(|state: &mut InstallerState| { |
576 | state.options.network = options; | |
617f2534 | 577 | }); |
cd383717 | 578 | |
6ab6af4c | 579 | switch_to_next_screen(siv, InstallerStep::Summary, &summary_dialog); |
617f2534 CH |
580 | } |
581 | Some(Err(err)) => siv.add_layer(Dialog::info(format!("Invalid values: {err}"))), | |
582 | _ => siv.add_layer(Dialog::info("Invalid values")), | |
cd383717 | 583 | } |
947fe360 | 584 | }), |
1d8fcd72 | 585 | true, |
947fe360 CH |
586 | ) |
587 | } | |
588 | ||
b7828c87 | 589 | pub struct SummaryOption { |
947fe360 CH |
590 | name: &'static str, |
591 | value: String, | |
592 | } | |
593 | ||
594 | impl SummaryOption { | |
595 | pub fn new<S: Into<String>>(name: &'static str, value: S) -> Self { | |
596 | Self { | |
597 | name, | |
598 | value: value.into(), | |
599 | } | |
600 | } | |
601 | } | |
602 | ||
603 | impl TableViewItem for SummaryOption { | |
604 | fn get_column(&self, name: &str) -> String { | |
605 | match name { | |
606 | "name" => self.name.to_owned(), | |
607 | "value" => self.value.clone(), | |
608 | _ => unreachable!(), | |
609 | } | |
610 | } | |
611 | } | |
612 | ||
613 | fn summary_dialog(siv: &mut Cursive) -> InstallerView { | |
90c0ea37 | 614 | let state = siv.user_data::<InstallerState>().unwrap(); |
947fe360 | 615 | |
1d8fcd72 DC |
616 | let mut bbar = LinearLayout::horizontal() |
617 | .child(abort_install_button()) | |
618 | .child(DummyView.full_width()) | |
619 | .child(Button::new("Previous", switch_to_prev_screen)) | |
620 | .child(DummyView) | |
621 | .child(Button::new("Install", |siv| { | |
622 | let autoreboot = siv | |
623 | .find_name("reboot-after-install") | |
624 | .map(|v: ViewRef<Checkbox>| v.is_checked()) | |
625 | .unwrap_or_default(); | |
626 | ||
627 | siv.with_user_data(|state: &mut InstallerState| { | |
628 | state.options.autoreboot = autoreboot; | |
629 | }); | |
630 | ||
631 | switch_to_next_screen(siv, InstallerStep::Install, &install_progress_dialog); | |
632 | })); | |
633 | ||
fcb51d73 | 634 | let _ = bbar.set_focus_index(2); // ignore errors |
1d8fcd72 DC |
635 | |
636 | let mut inner = LinearLayout::vertical() | |
947fe360 CH |
637 | .child(PaddedView::lrtb( |
638 | 0, | |
639 | 0, | |
640 | 1, | |
641 | 2, | |
642 | TableView::new() | |
643 | .columns(&[ | |
644 | ("name".to_owned(), "Option".to_owned()), | |
645 | ("value".to_owned(), "Selected value".to_owned()), | |
646 | ]) | |
b107a892 | 647 | .items(state.options.to_summary(&state.locales)), |
947fe360 CH |
648 | )) |
649 | .child( | |
650 | LinearLayout::horizontal() | |
651 | .child(DummyView.full_width()) | |
a84f277b | 652 | .child(Checkbox::new().checked().with_name("reboot-after-install")) |
947fe360 CH |
653 | .child( |
654 | TextView::new(" Automatically reboot after successful installation").no_wrap(), | |
655 | ) | |
656 | .child(DummyView.full_width()), | |
657 | ) | |
1d8fcd72 | 658 | .child(PaddedView::lrtb(1, 1, 1, 0, bbar)); |
ea15ca43 | 659 | |
1d8fcd72 | 660 | let _ = inner.set_focus_index(2); // ignore errors |
947fe360 | 661 | |
90c0ea37 | 662 | InstallerView::with_raw(state, inner) |
c2eee468 | 663 | } |
81f9348c CH |
664 | |
665 | fn install_progress_dialog(siv: &mut Cursive) -> InstallerView { | |
666 | // Ensure the screen is updated independently of keyboard events and such | |
667 | siv.set_autorefresh(true); | |
668 | ||
dba905bf CH |
669 | let state = siv.user_data::<InstallerState>().cloned().unwrap(); |
670 | InstallerView::with_raw(&state, InstallProgressView::new(siv)) | |
3285f0ad | 671 | } |