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