]>
Commit | Line | Data |
---|---|---|
183e2a76 CH |
1 | #![forbid(unsafe_code)] |
2 | ||
83071f35 CH |
3 | mod views; |
4 | ||
52e6b337 | 5 | use crate::views::DiskSizeFormInputView; |
183e2a76 | 6 | use cursive::{ |
ccf3b075 | 7 | event::Event, |
64220ff1 | 8 | view::{Finder, Nameable, Resizable, ViewWrapper}, |
183e2a76 | 9 | views::{ |
a142f457 CH |
10 | Button, Dialog, DummyView, EditView, LinearLayout, PaddedView, Panel, ResizedView, |
11 | ScrollView, SelectView, TextView, | |
183e2a76 CH |
12 | }, |
13 | Cursive, View, | |
14 | }; | |
e9557e62 | 15 | use std::fmt; |
52e6b337 | 16 | use views::FormInputView; |
183e2a76 CH |
17 | |
18 | // TextView::center() seems to garble the first two lines, so fix it manually here. | |
19 | const LOGO: &str = r#" | |
20 | ____ _ __ _____ | |
21 | / __ \_________ _ ______ ___ ____ _ __ | | / / ____/ | |
22 | / /_/ / ___/ __ \| |/_/ __ `__ \/ __ \| |/_/ | | / / __/ | |
23 | / ____/ / / /_/ /> </ / / / / / /_/ /> < | |/ / /___ | |
24 | /_/ /_/ \____/_/|_/_/ /_/ /_/\____/_/|_| |___/_____/ | |
25 | "#; | |
26 | ||
27 | const TITLE: &str = "Proxmox VE Installer"; | |
28 | ||
29 | struct InstallerView { | |
8b43c2d3 | 30 | view: ResizedView<LinearLayout>, |
183e2a76 CH |
31 | } |
32 | ||
33 | impl InstallerView { | |
f522afb1 CH |
34 | pub fn new<T: View>(view: T, next_cb: Box<dyn Fn(&mut Cursive)>) -> Self { |
35 | let inner = LinearLayout::vertical().child(view).child(PaddedView::lrtb( | |
36 | 1, | |
37 | 1, | |
38 | 1, | |
39 | 0, | |
40 | LinearLayout::horizontal() | |
41 | .child(abort_install_button()) | |
42 | .child(DummyView.full_width()) | |
43 | .child(Button::new("Previous", switch_to_prev_screen)) | |
44 | .child(DummyView) | |
45 | .child(Button::new("Next", next_cb)), | |
46 | )); | |
47 | ||
48 | Self::with_raw(inner) | |
49 | } | |
50 | ||
51 | pub fn with_raw<T: View>(view: T) -> Self { | |
8b43c2d3 CH |
52 | let inner = LinearLayout::vertical() |
53 | .child(PaddedView::lrtb(1, 1, 0, 1, TextView::new(LOGO).center())) | |
54 | .child(Dialog::around(view).title(TITLE)); | |
55 | ||
183e2a76 | 56 | Self { |
8b43c2d3 CH |
57 | // Limit the maximum to something reasonable, such that it won't get spread out much |
58 | // depending on the screen. | |
59 | view: ResizedView::with_max_size((120, 40), inner), | |
183e2a76 CH |
60 | } |
61 | } | |
62 | } | |
63 | ||
64 | impl ViewWrapper for InstallerView { | |
8b43c2d3 | 65 | cursive::wrap_impl!(self.view: ResizedView<LinearLayout>); |
183e2a76 CH |
66 | } |
67 | ||
e9557e62 CH |
68 | #[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] |
69 | enum FsType { | |
70 | #[default] | |
71 | Ext4, | |
72 | Xfs, | |
73 | } | |
74 | ||
75 | impl fmt::Display for FsType { | |
76 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { | |
77 | let s = match self { | |
78 | FsType::Ext4 => "ext4", | |
79 | FsType::Xfs => "XFS", | |
80 | }; | |
81 | write!(f, "{s}") | |
82 | } | |
83 | } | |
84 | ||
48ce057c CH |
85 | const FS_TYPES: &[FsType] = &[FsType::Ext4, FsType::Xfs]; |
86 | ||
e9557e62 CH |
87 | #[derive(Clone, Debug)] |
88 | struct LvmBootdiskOptions { | |
89 | disk: Disk, | |
90 | total_size: u64, | |
91 | swap_size: u64, | |
92 | max_root_size: u64, | |
93 | max_data_size: u64, | |
94 | min_lvm_free: u64, | |
95 | } | |
96 | ||
97 | impl LvmBootdiskOptions { | |
98 | fn defaults_from(disk: &Disk) -> Self { | |
99 | let min_lvm_free = if disk.size > 128 * 1024 * 1024 { | |
100 | 16 * 1024 * 1024 | |
101 | } else { | |
102 | disk.size / 8 | |
103 | }; | |
104 | ||
105 | Self { | |
106 | disk: disk.clone(), | |
107 | total_size: disk.size, | |
108 | swap_size: 4 * 1024 * 1024, // TODO: value from installed memory | |
109 | max_root_size: 0, | |
110 | max_data_size: 0, | |
111 | min_lvm_free, | |
112 | } | |
113 | } | |
114 | } | |
115 | ||
116 | #[derive(Clone, Debug)] | |
117 | enum AdvancedBootdiskOptions { | |
118 | Lvm(LvmBootdiskOptions), | |
119 | } | |
120 | ||
121 | #[derive(Clone, Debug)] | |
122 | struct Disk { | |
123 | path: String, | |
124 | size: u64, | |
125 | } | |
126 | ||
127 | impl fmt::Display for Disk { | |
128 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | |
129 | // TODO: Format sizes properly with `proxmox-human-byte` once merged | |
130 | // https://lists.proxmox.com/pipermail/pbs-devel/2023-May/006125.html | |
131 | write!(f, "{} ({} B)", self.path, self.size) | |
132 | } | |
133 | } | |
134 | ||
135 | #[derive(Clone, Debug)] | |
136 | struct BootdiskOptions { | |
137 | disks: Vec<Disk>, | |
138 | fstype: FsType, | |
139 | advanced: AdvancedBootdiskOptions, | |
140 | } | |
141 | ||
a142f457 CH |
142 | #[derive(Clone, Debug)] |
143 | struct TimezoneOptions { | |
144 | timezone: String, | |
145 | kb_layout: String, | |
146 | } | |
147 | ||
148 | impl Default for TimezoneOptions { | |
149 | fn default() -> Self { | |
150 | Self { | |
151 | timezone: "Europe/Vienna".to_owned(), | |
152 | kb_layout: "en_US".to_owned(), | |
153 | } | |
154 | } | |
155 | } | |
156 | ||
c2eee468 CH |
157 | #[derive(Clone, Debug)] |
158 | struct PasswordOptions { | |
159 | email: String, | |
160 | root_password: String, | |
161 | } | |
162 | ||
163 | impl Default for PasswordOptions { | |
164 | fn default() -> Self { | |
165 | Self { | |
166 | email: "mail@example.invalid".to_owned(), | |
167 | root_password: String::new(), | |
168 | } | |
169 | } | |
170 | } | |
171 | ||
e9557e62 CH |
172 | #[derive(Clone, Debug)] |
173 | struct InstallerOptions { | |
174 | bootdisk: BootdiskOptions, | |
a142f457 | 175 | timezone: TimezoneOptions, |
c2eee468 | 176 | password: PasswordOptions, |
e9557e62 CH |
177 | } |
178 | ||
183e2a76 CH |
179 | fn main() { |
180 | let mut siv = cursive::termion(); | |
181 | ||
ccf3b075 CH |
182 | siv.clear_global_callbacks(Event::CtrlChar('c')); |
183 | siv.set_on_pre_event(Event::CtrlChar('c'), trigger_abort_install_dialog); | |
184 | ||
e9557e62 CH |
185 | let disks = vec![Disk { |
186 | path: "/dev/vda".to_owned(), | |
187 | size: 17179869184, | |
188 | }]; | |
189 | siv.set_user_data(InstallerOptions { | |
190 | bootdisk: BootdiskOptions { | |
191 | disks: disks.clone(), | |
192 | fstype: FsType::default(), | |
193 | advanced: AdvancedBootdiskOptions::Lvm(LvmBootdiskOptions::defaults_from(&disks[0])), | |
194 | }, | |
a142f457 | 195 | timezone: TimezoneOptions::default(), |
c2eee468 | 196 | password: PasswordOptions::default(), |
e9557e62 CH |
197 | }); |
198 | ||
183e2a76 CH |
199 | siv.add_active_screen(); |
200 | siv.screen_mut().add_layer(license_dialog()); | |
201 | siv.run(); | |
202 | } | |
203 | ||
e70f1b2f CH |
204 | fn add_next_screen( |
205 | constructor: &dyn Fn(&mut Cursive) -> InstallerView, | |
206 | ) -> Box<dyn Fn(&mut Cursive) + '_> { | |
183e2a76 | 207 | Box::new(|siv: &mut Cursive| { |
e70f1b2f | 208 | let v = constructor(siv); |
183e2a76 | 209 | siv.add_active_screen(); |
e70f1b2f | 210 | siv.screen_mut().add_layer(v); |
183e2a76 CH |
211 | }) |
212 | } | |
213 | ||
64220ff1 CH |
214 | fn switch_to_prev_screen(siv: &mut Cursive) { |
215 | let id = siv.active_screen().saturating_sub(1); | |
216 | siv.set_screen(id); | |
217 | } | |
218 | ||
183e2a76 CH |
219 | fn yes_no_dialog( |
220 | siv: &mut Cursive, | |
221 | title: &str, | |
222 | text: &str, | |
223 | callback: &'static dyn Fn(&mut Cursive), | |
224 | ) { | |
225 | siv.add_layer( | |
226 | Dialog::around(TextView::new(text)) | |
227 | .title(title) | |
228 | .dismiss_button("No") | |
229 | .button("Yes", callback), | |
230 | ) | |
231 | } | |
232 | ||
ccf3b075 | 233 | fn trigger_abort_install_dialog(siv: &mut Cursive) { |
58869243 CH |
234 | #[cfg(debug_assertions)] |
235 | siv.quit(); | |
236 | ||
237 | #[cfg(not(debug_assertions))] | |
ccf3b075 CH |
238 | yes_no_dialog( |
239 | siv, | |
240 | "Abort installation?", | |
241 | "Are you sure you want to abort the installation?", | |
242 | &Cursive::quit, | |
243 | ) | |
244 | } | |
245 | ||
183e2a76 | 246 | fn abort_install_button() -> Button { |
ccf3b075 | 247 | Button::new("Abort", trigger_abort_install_dialog) |
183e2a76 CH |
248 | } |
249 | ||
250 | fn get_eula() -> String { | |
8f5fdd21 CH |
251 | // TODO: properly using info from Proxmox::Install::Env::setup() |
252 | std::fs::read_to_string("/cdrom/EULA") | |
253 | .unwrap_or_else(|_| "< Debug build - ignoring non-existing EULA >".to_owned()) | |
183e2a76 CH |
254 | } |
255 | ||
256 | fn license_dialog() -> InstallerView { | |
257 | let inner = LinearLayout::vertical() | |
258 | .child(PaddedView::lrtb( | |
259 | 0, | |
260 | 0, | |
261 | 1, | |
262 | 0, | |
263 | TextView::new("END USER LICENSE AGREEMENT (EULA)").center(), | |
264 | )) | |
8b43c2d3 CH |
265 | .child(Panel::new(ScrollView::new( |
266 | TextView::new(get_eula()).center(), | |
183e2a76 CH |
267 | ))) |
268 | .child(PaddedView::lrtb( | |
269 | 1, | |
270 | 1, | |
f522afb1 | 271 | 1, |
183e2a76 CH |
272 | 0, |
273 | LinearLayout::horizontal() | |
274 | .child(abort_install_button()) | |
275 | .child(DummyView.full_width()) | |
276 | .child(Button::new("I agree", add_next_screen(&bootdisk_dialog))), | |
277 | )); | |
278 | ||
f522afb1 | 279 | InstallerView::with_raw(inner) |
183e2a76 CH |
280 | } |
281 | ||
e70f1b2f | 282 | fn bootdisk_dialog(siv: &mut Cursive) -> InstallerView { |
64220ff1 CH |
283 | let options = siv |
284 | .user_data::<InstallerOptions>() | |
285 | .map(|o| o.clone()) | |
286 | .unwrap() | |
287 | .bootdisk; | |
288 | ||
289 | let AdvancedBootdiskOptions::Lvm(advanced) = options.advanced; | |
290 | ||
291 | let fstype_select = LinearLayout::horizontal() | |
292 | .child(TextView::new("Filesystem: ")) | |
293 | .child(DummyView.full_width()) | |
294 | .child( | |
295 | SelectView::new() | |
296 | .popup() | |
297 | .with_all(FS_TYPES.iter().map(|t| (t.to_string(), t))) | |
298 | .selected( | |
299 | FS_TYPES | |
300 | .iter() | |
301 | .position(|t| *t == options.fstype) | |
302 | .unwrap_or_default(), | |
303 | ) | |
304 | .on_submit({ | |
305 | let disks = options.disks.clone(); | |
306 | let advanced = advanced.clone(); | |
307 | move |siv, fstype: &FsType| { | |
308 | let view = match fstype { | |
309 | FsType::Ext4 | FsType::Xfs => { | |
310 | LvmBootdiskOptionsView::new(&disks, &advanced) | |
311 | } | |
312 | }; | |
313 | ||
314 | siv.call_on_name("bootdisk-options", |v: &mut LinearLayout| { | |
315 | v.clear(); | |
316 | v.add_child(view); | |
317 | }); | |
318 | } | |
319 | }) | |
320 | .with_name("fstype") | |
321 | .full_width(), | |
322 | ); | |
323 | ||
324 | let inner = LinearLayout::vertical() | |
325 | .child(fstype_select) | |
326 | .child(DummyView) | |
327 | .child( | |
328 | LinearLayout::horizontal() | |
329 | .child(LvmBootdiskOptionsView::new(&options.disks, &advanced)) | |
330 | .with_name("bootdisk-options"), | |
f522afb1 | 331 | ); |
64220ff1 | 332 | |
f522afb1 CH |
333 | InstallerView::new( |
334 | inner, | |
335 | Box::new(|siv| { | |
336 | let options = siv | |
337 | .call_on_name("bootdisk-options", |v: &mut LinearLayout| { | |
338 | v.get_child_mut(0)? | |
339 | .downcast_mut::<LvmBootdiskOptionsView>()? | |
340 | .get_values() | |
341 | .map(AdvancedBootdiskOptions::Lvm) | |
342 | }) | |
343 | .flatten() | |
344 | .unwrap(); | |
345 | ||
346 | siv.with_user_data(|opts: &mut InstallerOptions| { | |
347 | opts.bootdisk.advanced = options; | |
348 | }); | |
349 | ||
a142f457 | 350 | add_next_screen(&timezone_dialog)(siv) |
f522afb1 CH |
351 | }), |
352 | ) | |
64220ff1 CH |
353 | } |
354 | ||
355 | struct LvmBootdiskOptionsView { | |
356 | view: LinearLayout, | |
357 | } | |
358 | ||
359 | impl LvmBootdiskOptionsView { | |
360 | fn new(disks: &[Disk], options: &LvmBootdiskOptions) -> Self { | |
361 | let view = LinearLayout::vertical() | |
52e6b337 CH |
362 | .child(FormInputView::new( |
363 | "Target harddisk", | |
364 | SelectView::new() | |
365 | .popup() | |
366 | .with_all(disks.iter().map(|d| (d.to_string(), d.clone()))) | |
367 | .with_name("bootdisk-disk"), | |
368 | )) | |
369 | .child(DiskSizeFormInputView::new("Total size").content(options.total_size)) | |
370 | .child(DiskSizeFormInputView::new("Swap size").content(options.swap_size)) | |
64220ff1 | 371 | .child( |
52e6b337 CH |
372 | DiskSizeFormInputView::new("Maximum root volume size") |
373 | .content(options.max_root_size), | |
64220ff1 | 374 | ) |
64220ff1 | 375 | .child( |
52e6b337 CH |
376 | DiskSizeFormInputView::new("Maximum data volume size") |
377 | .content(options.max_data_size), | |
64220ff1 CH |
378 | ) |
379 | .child( | |
52e6b337 CH |
380 | DiskSizeFormInputView::new("Minimum free LVM space").content(options.min_lvm_free), |
381 | ); | |
64220ff1 CH |
382 | |
383 | Self { view } | |
384 | } | |
385 | ||
386 | fn get_values(&mut self) -> Option<LvmBootdiskOptions> { | |
387 | let disk = self | |
388 | .view | |
389 | .call_on_name("bootdisk-disk", |view: &mut SelectView<Disk>| { | |
390 | view.selection() | |
391 | })? | |
392 | .map(|d| (*d).clone())?; | |
393 | ||
394 | let mut get_disksize_value = |i| { | |
395 | self.view | |
396 | .get_child_mut(i)? | |
52e6b337 | 397 | .downcast_mut::<DiskSizeFormInputView>()? |
64220ff1 CH |
398 | .get_content() |
399 | }; | |
400 | ||
401 | Some(LvmBootdiskOptions { | |
402 | disk, | |
403 | total_size: get_disksize_value(1)?, | |
404 | swap_size: get_disksize_value(2)?, | |
405 | max_root_size: get_disksize_value(3)?, | |
406 | max_data_size: get_disksize_value(4)?, | |
407 | min_lvm_free: get_disksize_value(5)?, | |
408 | }) | |
409 | } | |
410 | } | |
411 | ||
412 | impl ViewWrapper for LvmBootdiskOptionsView { | |
413 | cursive::wrap_impl!(self.view: LinearLayout); | |
414 | } | |
415 | ||
a142f457 CH |
416 | fn timezone_dialog(siv: &mut Cursive) -> InstallerView { |
417 | let options = siv | |
418 | .user_data::<InstallerOptions>() | |
419 | .map(|o| o.timezone.clone()) | |
420 | .unwrap_or_default(); | |
421 | ||
422 | let inner = LinearLayout::vertical() | |
423 | .child(FormInputView::new( | |
424 | "Country", | |
425 | EditView::new().content("Austria"), | |
426 | )) | |
427 | .child(FormInputView::new( | |
428 | "Timezone", | |
429 | EditView::new() | |
430 | .content(options.timezone) | |
431 | .with_name("timezone-tzname"), | |
432 | )) | |
433 | .child(FormInputView::new( | |
434 | "Keyboard layout", | |
435 | EditView::new() | |
436 | .content(options.kb_layout) | |
437 | .with_name("timezone-kblayout"), | |
438 | )); | |
439 | ||
440 | InstallerView::new( | |
441 | inner, | |
442 | Box::new(|siv| { | |
443 | let timezone = siv | |
444 | .call_on_name("timezone-tzname", |v: &mut EditView| { | |
445 | (*v.get_content()).clone() | |
446 | }) | |
447 | .unwrap(); | |
448 | ||
449 | let kb_layout = siv | |
450 | .call_on_name("timezone-kblayout", |v: &mut EditView| { | |
451 | (*v.get_content()).clone() | |
452 | }) | |
453 | .unwrap(); | |
454 | ||
455 | siv.with_user_data(|opts: &mut InstallerOptions| { | |
456 | opts.timezone = TimezoneOptions { | |
457 | timezone, | |
458 | kb_layout, | |
459 | }; | |
a142f457 | 460 | }); |
c2eee468 CH |
461 | |
462 | add_next_screen(&password_dialog)(siv); | |
a142f457 CH |
463 | }), |
464 | ) | |
183e2a76 | 465 | } |
c2eee468 CH |
466 | |
467 | fn password_dialog(siv: &mut Cursive) -> InstallerView { | |
468 | let options = siv | |
469 | .user_data::<InstallerOptions>() | |
470 | .map(|o| o.password.clone()) | |
471 | .unwrap_or_default(); | |
472 | ||
473 | let inner = LinearLayout::vertical() | |
474 | .child(FormInputView::new( | |
475 | "Root password", | |
476 | EditView::new() | |
477 | .secret() | |
478 | .with_name("password-dialog-root-pw"), | |
479 | )) | |
480 | .child(FormInputView::new( | |
481 | "Confirm root password", | |
482 | EditView::new() | |
483 | .secret() | |
484 | .with_name("password-dialog-root-pw-confirm"), | |
485 | )) | |
486 | .child(FormInputView::new( | |
487 | "Administator email", | |
488 | EditView::new() | |
489 | .content(options.email) | |
490 | .with_name("password-dialog-email"), | |
491 | )); | |
492 | ||
493 | InstallerView::new(inner, Box::new(|_| {})) | |
494 | } |