]>
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 | ||
183e2a76 CH |
288 | fn yes_no_dialog( |
289 | siv: &mut Cursive, | |
290 | title: &str, | |
291 | text: &str, | |
deebe07f CH |
292 | callback_yes: Box<dyn Fn(&mut Cursive)>, |
293 | callback_no: Box<dyn Fn(&mut Cursive)>, | |
183e2a76 CH |
294 | ) { |
295 | siv.add_layer( | |
296 | Dialog::around(TextView::new(text)) | |
297 | .title(title) | |
deebe07f CH |
298 | .button("No", move |siv| { |
299 | siv.pop_layer(); | |
300 | callback_no(siv); | |
301 | }) | |
302 | .button("Yes", move |siv| { | |
303 | siv.pop_layer(); | |
304 | callback_yes(siv); | |
305 | }), | |
183e2a76 CH |
306 | ) |
307 | } | |
308 | ||
ccf3b075 | 309 | fn trigger_abort_install_dialog(siv: &mut Cursive) { |
58869243 CH |
310 | #[cfg(debug_assertions)] |
311 | siv.quit(); | |
312 | ||
313 | #[cfg(not(debug_assertions))] | |
ccf3b075 CH |
314 | yes_no_dialog( |
315 | siv, | |
316 | "Abort installation?", | |
317 | "Are you sure you want to abort the installation?", | |
deebe07f CH |
318 | Box::new(Cursive::quit), |
319 | Box::new(|_| {}), | |
ccf3b075 CH |
320 | ) |
321 | } | |
322 | ||
183e2a76 | 323 | fn abort_install_button() -> Button { |
ccf3b075 | 324 | Button::new("Abort", trigger_abort_install_dialog) |
183e2a76 CH |
325 | } |
326 | ||
8c9af1e7 CH |
327 | fn get_eula(setup: &SetupInfo) -> String { |
328 | let mut path = setup.locations.iso.clone(); | |
329 | path.push("EULA"); | |
330 | ||
331 | std::fs::read_to_string(path) | |
8f5fdd21 | 332 | .unwrap_or_else(|_| "< Debug build - ignoring non-existing EULA >".to_owned()) |
183e2a76 CH |
333 | } |
334 | ||
90c0ea37 CH |
335 | fn license_dialog(siv: &mut Cursive) -> InstallerView { |
336 | let state = siv.user_data::<InstallerState>().unwrap(); | |
337 | ||
1d8fcd72 DC |
338 | let mut bbar = LinearLayout::horizontal() |
339 | .child(abort_install_button()) | |
340 | .child(DummyView.full_width()) | |
341 | .child(Button::new("I agree", |siv| { | |
342 | switch_to_next_screen(siv, InstallerStep::Bootdisk, &bootdisk_dialog) | |
343 | })); | |
344 | let _ = bbar.set_focus_index(2); // ignore errors | |
345 | ||
346 | let mut inner = LinearLayout::vertical() | |
183e2a76 CH |
347 | .child(PaddedView::lrtb( |
348 | 0, | |
349 | 0, | |
350 | 1, | |
351 | 0, | |
352 | TextView::new("END USER LICENSE AGREEMENT (EULA)").center(), | |
353 | )) | |
8b43c2d3 | 354 | .child(Panel::new(ScrollView::new( |
8c9af1e7 | 355 | TextView::new(get_eula(&state.setup_info)).center(), |
183e2a76 | 356 | ))) |
1d8fcd72 DC |
357 | .child(PaddedView::lrtb(1, 1, 1, 0, bbar)); |
358 | ||
359 | let _ = inner.set_focus_index(2); // ignore errors | |
183e2a76 | 360 | |
90c0ea37 | 361 | InstallerView::with_raw(state, inner) |
183e2a76 CH |
362 | } |
363 | ||
e70f1b2f | 364 | fn bootdisk_dialog(siv: &mut Cursive) -> InstallerView { |
a6e00ea6 | 365 | let state = siv.user_data::<InstallerState>().cloned().unwrap(); |
64220ff1 | 366 | |
f522afb1 | 367 | InstallerView::new( |
90c0ea37 | 368 | &state, |
521dfff5 | 369 | BootdiskOptionsView::new(siv, &state.runtime_info, &state.options.bootdisk) |
ed191e60 | 370 | .with_name("bootdisk-options"), |
f522afb1 | 371 | Box::new(|siv| { |
994c4ff0 CH |
372 | let options = siv.call_on_name("bootdisk-options", BootdiskOptionsView::get_values); |
373 | ||
374 | match options { | |
375 | Some(Ok(options)) => { | |
376 | siv.with_user_data(|state: &mut InstallerState| { | |
377 | state.options.bootdisk = options; | |
378 | }); | |
379 | ||
380 | switch_to_next_screen(siv, InstallerStep::Timezone, &timezone_dialog); | |
381 | } | |
382 | ||
383 | Some(Err(err)) => siv.add_layer(Dialog::info(format!("Invalid values: {err}"))), | |
384 | _ => siv.add_layer(Dialog::info("Invalid values")), | |
93ebe7bd | 385 | } |
f522afb1 | 386 | }), |
1d8fcd72 | 387 | true, |
f522afb1 | 388 | ) |
64220ff1 CH |
389 | } |
390 | ||
a142f457 | 391 | fn timezone_dialog(siv: &mut Cursive) -> InstallerView { |
90c0ea37 CH |
392 | let state = siv.user_data::<InstallerState>().unwrap(); |
393 | let options = &state.options.timezone; | |
a142f457 | 394 | |
a142f457 | 395 | InstallerView::new( |
90c0ea37 | 396 | state, |
e1a11f79 | 397 | TimezoneOptionsView::new(&state.locales, options).with_name("timezone-options"), |
a142f457 | 398 | Box::new(|siv| { |
e1a11f79 | 399 | let options = siv.call_on_name("timezone-options", TimezoneOptionsView::get_values); |
93ebe7bd | 400 | |
767843f9 CH |
401 | match options { |
402 | Some(Ok(options)) => { | |
a6e00ea6 CH |
403 | siv.with_user_data(|state: &mut InstallerState| { |
404 | state.options.timezone = options; | |
767843f9 CH |
405 | }); |
406 | ||
6ab6af4c | 407 | switch_to_next_screen(siv, InstallerStep::Password, &password_dialog); |
767843f9 CH |
408 | } |
409 | Some(Err(err)) => siv.add_layer(Dialog::info(format!("Invalid values: {err}"))), | |
410 | _ => siv.add_layer(Dialog::info("Invalid values")), | |
93ebe7bd | 411 | } |
a142f457 | 412 | }), |
1d8fcd72 | 413 | true, |
a142f457 | 414 | ) |
183e2a76 | 415 | } |
c2eee468 CH |
416 | |
417 | fn password_dialog(siv: &mut Cursive) -> InstallerView { | |
90c0ea37 CH |
418 | let state = siv.user_data::<InstallerState>().unwrap(); |
419 | let options = &state.options.password; | |
c2eee468 | 420 | |
15832d18 CH |
421 | let inner = FormView::new() |
422 | .child("Root password", EditView::new().secret()) | |
423 | .child("Confirm root password", EditView::new().secret()) | |
90c0ea37 | 424 | .child( |
0f39ba93 | 425 | "Administrator email", |
90c0ea37 CH |
426 | EditView::new().content(&options.email), |
427 | ) | |
ef4957e8 | 428 | .with_name("password-options"); |
c2eee468 | 429 | |
ccd34284 | 430 | InstallerView::new( |
90c0ea37 | 431 | state, |
ccd34284 CH |
432 | inner, |
433 | Box::new(|siv| { | |
15832d18 CH |
434 | let options = siv.call_on_name("password-options", |view: &mut FormView| { |
435 | let root_password = view | |
436 | .get_value::<EditView, _>(0) | |
437 | .ok_or("failed to retrieve password")?; | |
438 | ||
439 | let confirm_password = view | |
440 | .get_value::<EditView, _>(1) | |
441 | .ok_or("failed to retrieve password confirmation")?; | |
ef4957e8 | 442 | |
15832d18 CH |
443 | let email = view |
444 | .get_value::<EditView, _>(2) | |
445 | .ok_or("failed to retrieve email")?; | |
ef4957e8 | 446 | |
298a4de0 DC |
447 | let email_regex = |
448 | Regex::new(r"^[\w\+\-\~]+(\.[\w\+\-\~]+)*@[a-zA-Z0-9\-]+(\.[a-zA-Z0-9\-]+)*$") | |
449 | .unwrap(); | |
450 | ||
b82fff5d | 451 | if root_password.len() < 5 { |
fe64752d | 452 | Err("password too short, must be at least 5 characters long") |
b82fff5d | 453 | } else if root_password != confirm_password { |
ef4957e8 | 454 | Err("passwords do not match") |
03888174 | 455 | } else if email == "mail@example.invalid" { |
b82fff5d | 456 | Err("invalid email address") |
298a4de0 DC |
457 | } else if !email_regex.is_match(&email) { |
458 | Err("Email does not look like a valid address (user@domain.tld)") | |
ef4957e8 CH |
459 | } else { |
460 | Ok(PasswordOptions { | |
461 | root_password, | |
15832d18 | 462 | email, |
ef4957e8 CH |
463 | }) |
464 | } | |
465 | }); | |
466 | ||
467 | match options { | |
468 | Some(Ok(options)) => { | |
a6e00ea6 CH |
469 | siv.with_user_data(|state: &mut InstallerState| { |
470 | state.options.password = options; | |
ef4957e8 CH |
471 | }); |
472 | ||
6ab6af4c | 473 | switch_to_next_screen(siv, InstallerStep::Network, &network_dialog); |
ef4957e8 CH |
474 | } |
475 | Some(Err(err)) => siv.add_layer(Dialog::info(format!("Invalid values: {err}"))), | |
476 | _ => siv.add_layer(Dialog::info("Invalid values")), | |
477 | } | |
ccd34284 | 478 | }), |
1d8fcd72 | 479 | false, |
ccd34284 CH |
480 | ) |
481 | } | |
482 | ||
483 | fn network_dialog(siv: &mut Cursive) -> InstallerView { | |
90c0ea37 CH |
484 | let state = siv.user_data::<InstallerState>().unwrap(); |
485 | let options = &state.options.network; | |
55e5c9cd TL |
486 | let ifaces = state.runtime_info.network.interfaces.values(); |
487 | let ifnames = ifaces | |
488 | .clone() | |
489 | .map(|iface| (iface.render(), iface.name.clone())); | |
89dfa5c9 TL |
490 | let mut ifaces_selection = SelectView::new().popup().with_all(ifnames.clone()); |
491 | ||
492 | ifaces_selection.sort(); | |
493 | ifaces_selection.set_selection( | |
494 | ifnames | |
495 | .clone() | |
57161139 | 496 | .position(|iface| iface.1 == options.ifname) |
89dfa5c9 TL |
497 | .unwrap_or(ifaces.len() - 1), |
498 | ); | |
ccd34284 | 499 | |
617f2534 | 500 | let inner = FormView::new() |
89dfa5c9 | 501 | .child("Management interface", ifaces_selection) |
95c49008 CH |
502 | .child( |
503 | "Hostname (FQDN)", | |
504 | EditView::new().content(options.fqdn.to_string()), | |
505 | ) | |
617f2534 | 506 | .child( |
ccd34284 | 507 | "IP address (CIDR)", |
90c0ea37 | 508 | CidrAddressEditView::new().content(options.address.clone()), |
617f2534 CH |
509 | ) |
510 | .child( | |
ccd34284 CH |
511 | "Gateway address", |
512 | EditView::new().content(options.gateway.to_string()), | |
617f2534 CH |
513 | ) |
514 | .child( | |
1397feec | 515 | "DNS server address", |
ccd34284 | 516 | EditView::new().content(options.dns_server.to_string()), |
617f2534 | 517 | ) |
cd383717 | 518 | .with_name("network-options"); |
ccd34284 | 519 | |
947fe360 | 520 | InstallerView::new( |
90c0ea37 | 521 | state, |
947fe360 CH |
522 | inner, |
523 | Box::new(|siv| { | |
617f2534 CH |
524 | let options = siv.call_on_name("network-options", |view: &mut FormView| { |
525 | let ifname = view | |
526 | .get_value::<SelectView, _>(0) | |
527 | .ok_or("failed to retrieve management interface name")?; | |
cd383717 | 528 | |
617f2534 CH |
529 | let fqdn = view |
530 | .get_value::<EditView, _>(1) | |
95c49008 CH |
531 | .ok_or("failed to retrieve host FQDN")? |
532 | .parse::<Fqdn>() | |
55dc67ca | 533 | .map_err(|err| format!("hostname does not look valid:\n\n{err}"))?; |
617f2534 CH |
534 | |
535 | let address = view | |
536 | .get_value::<CidrAddressEditView, _>(2) | |
537 | .ok_or("failed to retrieve host address")?; | |
538 | ||
539 | let gateway = view | |
540 | .get_value::<EditView, _>(3) | |
541 | .ok_or("failed to retrieve gateway address")? | |
542 | .parse::<IpAddr>() | |
543 | .map_err(|err| err.to_string())?; | |
544 | ||
545 | let dns_server = view | |
d2b13ba7 | 546 | .get_value::<EditView, _>(4) |
617f2534 CH |
547 | .ok_or("failed to retrieve DNS server address")? |
548 | .parse::<IpAddr>() | |
549 | .map_err(|err| err.to_string())?; | |
550 | ||
551 | if address.addr().is_ipv4() != gateway.is_ipv4() { | |
552 | Err("host and gateway IP address version must not differ".to_owned()) | |
553 | } else if address.addr().is_ipv4() != dns_server.is_ipv4() { | |
554 | Err("host and DNS IP address version must not differ".to_owned()) | |
95c49008 CH |
555 | } else if fqdn.to_string().ends_with(".invalid") { |
556 | Err("hostname does not look valid".to_owned()) | |
617f2534 CH |
557 | } else { |
558 | Ok(NetworkOptions { | |
559 | ifname, | |
560 | fqdn, | |
561 | address, | |
562 | gateway, | |
563 | dns_server, | |
564 | }) | |
565 | } | |
cd383717 CH |
566 | }); |
567 | ||
617f2534 CH |
568 | match options { |
569 | Some(Ok(options)) => { | |
a6e00ea6 CH |
570 | siv.with_user_data(|state: &mut InstallerState| { |
571 | state.options.network = options; | |
617f2534 | 572 | }); |
cd383717 | 573 | |
6ab6af4c | 574 | switch_to_next_screen(siv, InstallerStep::Summary, &summary_dialog); |
617f2534 CH |
575 | } |
576 | Some(Err(err)) => siv.add_layer(Dialog::info(format!("Invalid values: {err}"))), | |
577 | _ => siv.add_layer(Dialog::info("Invalid values")), | |
cd383717 | 578 | } |
947fe360 | 579 | }), |
1d8fcd72 | 580 | true, |
947fe360 CH |
581 | ) |
582 | } | |
583 | ||
b7828c87 | 584 | pub struct SummaryOption { |
947fe360 CH |
585 | name: &'static str, |
586 | value: String, | |
587 | } | |
588 | ||
589 | impl SummaryOption { | |
590 | pub fn new<S: Into<String>>(name: &'static str, value: S) -> Self { | |
591 | Self { | |
592 | name, | |
593 | value: value.into(), | |
594 | } | |
595 | } | |
596 | } | |
597 | ||
598 | impl TableViewItem for SummaryOption { | |
599 | fn get_column(&self, name: &str) -> String { | |
600 | match name { | |
601 | "name" => self.name.to_owned(), | |
602 | "value" => self.value.clone(), | |
603 | _ => unreachable!(), | |
604 | } | |
605 | } | |
606 | } | |
607 | ||
608 | fn summary_dialog(siv: &mut Cursive) -> InstallerView { | |
90c0ea37 | 609 | let state = siv.user_data::<InstallerState>().unwrap(); |
947fe360 | 610 | |
1d8fcd72 DC |
611 | let mut bbar = LinearLayout::horizontal() |
612 | .child(abort_install_button()) | |
613 | .child(DummyView.full_width()) | |
614 | .child(Button::new("Previous", switch_to_prev_screen)) | |
615 | .child(DummyView) | |
616 | .child(Button::new("Install", |siv| { | |
617 | let autoreboot = siv | |
618 | .find_name("reboot-after-install") | |
619 | .map(|v: ViewRef<Checkbox>| v.is_checked()) | |
620 | .unwrap_or_default(); | |
621 | ||
622 | siv.with_user_data(|state: &mut InstallerState| { | |
623 | state.options.autoreboot = autoreboot; | |
624 | }); | |
625 | ||
626 | switch_to_next_screen(siv, InstallerStep::Install, &install_progress_dialog); | |
627 | })); | |
628 | ||
fcb51d73 | 629 | let _ = bbar.set_focus_index(2); // ignore errors |
1d8fcd72 DC |
630 | |
631 | let mut inner = LinearLayout::vertical() | |
947fe360 CH |
632 | .child(PaddedView::lrtb( |
633 | 0, | |
634 | 0, | |
635 | 1, | |
636 | 2, | |
637 | TableView::new() | |
638 | .columns(&[ | |
639 | ("name".to_owned(), "Option".to_owned()), | |
640 | ("value".to_owned(), "Selected value".to_owned()), | |
641 | ]) | |
b107a892 | 642 | .items(state.options.to_summary(&state.locales)), |
947fe360 CH |
643 | )) |
644 | .child( | |
645 | LinearLayout::horizontal() | |
646 | .child(DummyView.full_width()) | |
a84f277b | 647 | .child(Checkbox::new().checked().with_name("reboot-after-install")) |
947fe360 CH |
648 | .child( |
649 | TextView::new(" Automatically reboot after successful installation").no_wrap(), | |
650 | ) | |
651 | .child(DummyView.full_width()), | |
652 | ) | |
1d8fcd72 | 653 | .child(PaddedView::lrtb(1, 1, 1, 0, bbar)); |
ea15ca43 | 654 | |
1d8fcd72 | 655 | let _ = inner.set_focus_index(2); // ignore errors |
947fe360 | 656 | |
90c0ea37 | 657 | InstallerView::with_raw(state, inner) |
c2eee468 | 658 | } |
81f9348c CH |
659 | |
660 | fn install_progress_dialog(siv: &mut Cursive) -> InstallerView { | |
661 | // Ensure the screen is updated independently of keyboard events and such | |
662 | siv.set_autorefresh(true); | |
663 | ||
dba905bf CH |
664 | let state = siv.user_data::<InstallerState>().cloned().unwrap(); |
665 | InstallerView::with_raw(&state, InstallProgressView::new(siv)) | |
3285f0ad | 666 | } |