]>
Commit | Line | Data |
---|---|---|
183e2a76 CH |
1 | #![forbid(unsafe_code)] |
2 | ||
b7828c87 | 3 | mod options; |
fc187196 | 4 | mod utils; |
83071f35 CH |
5 | mod views; |
6 | ||
b7828c87 | 7 | use crate::options::*; |
183e2a76 | 8 | use cursive::{ |
ccf3b075 | 9 | event::Event, |
af0dfe0e | 10 | view::{Nameable, Resizable, ViewWrapper}, |
183e2a76 | 11 | views::{ |
947fe360 CH |
12 | Button, Checkbox, Dialog, DummyView, EditView, LinearLayout, PaddedView, Panel, |
13 | ResizedView, ScrollView, SelectView, TextView, | |
183e2a76 CH |
14 | }, |
15 | Cursive, View, | |
16 | }; | |
af0dfe0e | 17 | use views::{ |
767843f9 CH |
18 | BootdiskOptionsView, CidrAddressEditView, FormView, FormInputView, FormInputViewGetValue, |
19 | TableView, TableViewItem, | |
af0dfe0e | 20 | }; |
183e2a76 CH |
21 | |
22 | // TextView::center() seems to garble the first two lines, so fix it manually here. | |
23 | const LOGO: &str = r#" | |
24 | ____ _ __ _____ | |
25 | / __ \_________ _ ______ ___ ____ _ __ | | / / ____/ | |
26 | / /_/ / ___/ __ \| |/_/ __ `__ \/ __ \| |/_/ | | / / __/ | |
27 | / ____/ / / /_/ /> </ / / / / / /_/ /> < | |/ / /___ | |
28 | /_/ /_/ \____/_/|_/_/ /_/ /_/\____/_/|_| |___/_____/ | |
29 | "#; | |
30 | ||
31 | const TITLE: &str = "Proxmox VE Installer"; | |
32 | ||
33 | struct InstallerView { | |
8b43c2d3 | 34 | view: ResizedView<LinearLayout>, |
183e2a76 CH |
35 | } |
36 | ||
37 | impl InstallerView { | |
f522afb1 | 38 | pub fn new<T: View>(view: T, next_cb: Box<dyn Fn(&mut Cursive)>) -> Self { |
8807d547 CH |
39 | let inner = LinearLayout::vertical() |
40 | .child(PaddedView::lrtb(0, 0, 1, 1, view)) | |
41 | .child(PaddedView::lrtb( | |
42 | 1, | |
43 | 1, | |
44 | 0, | |
45 | 0, | |
46 | LinearLayout::horizontal() | |
47 | .child(abort_install_button()) | |
48 | .child(DummyView.full_width()) | |
49 | .child(Button::new("Previous", switch_to_prev_screen)) | |
50 | .child(DummyView) | |
51 | .child(Button::new("Next", next_cb)), | |
52 | )); | |
f522afb1 CH |
53 | |
54 | Self::with_raw(inner) | |
55 | } | |
56 | ||
57 | pub fn with_raw<T: View>(view: T) -> Self { | |
8b43c2d3 CH |
58 | let inner = LinearLayout::vertical() |
59 | .child(PaddedView::lrtb(1, 1, 0, 1, TextView::new(LOGO).center())) | |
60 | .child(Dialog::around(view).title(TITLE)); | |
61 | ||
183e2a76 | 62 | Self { |
8b43c2d3 CH |
63 | // Limit the maximum to something reasonable, such that it won't get spread out much |
64 | // depending on the screen. | |
65 | view: ResizedView::with_max_size((120, 40), inner), | |
183e2a76 CH |
66 | } |
67 | } | |
68 | } | |
69 | ||
70 | impl ViewWrapper for InstallerView { | |
8b43c2d3 | 71 | cursive::wrap_impl!(self.view: ResizedView<LinearLayout>); |
183e2a76 CH |
72 | } |
73 | ||
ed191e60 CH |
74 | #[derive(Clone)] |
75 | struct InstallerData { | |
76 | options: InstallerOptions, | |
77 | available_disks: Vec<Disk>, | |
78 | } | |
79 | ||
183e2a76 CH |
80 | fn main() { |
81 | let mut siv = cursive::termion(); | |
82 | ||
ccf3b075 CH |
83 | siv.clear_global_callbacks(Event::CtrlChar('c')); |
84 | siv.set_on_pre_event(Event::CtrlChar('c'), trigger_abort_install_dialog); | |
85 | ||
ed191e60 CH |
86 | // TODO: retrieve actual disk info |
87 | let available_disks = vec![Disk { | |
e9557e62 CH |
88 | path: "/dev/vda".to_owned(), |
89 | size: 17179869184, | |
90 | }]; | |
ed191e60 CH |
91 | |
92 | siv.set_user_data(InstallerData { | |
93 | options: InstallerOptions { | |
94 | bootdisk: BootdiskOptions::defaults_from(&available_disks[0]), | |
95 | timezone: TimezoneOptions::default(), | |
96 | password: PasswordOptions::default(), | |
97 | network: NetworkOptions::default(), | |
e9557e62 | 98 | }, |
ed191e60 | 99 | available_disks, |
e9557e62 CH |
100 | }); |
101 | ||
5e73fcfe | 102 | add_next_screen(&mut siv, &license_dialog); |
183e2a76 CH |
103 | siv.run(); |
104 | } | |
105 | ||
5e73fcfe CH |
106 | fn add_next_screen(siv: &mut Cursive, constructor: &dyn Fn(&mut Cursive) -> InstallerView) { |
107 | let v = constructor(siv); | |
108 | siv.add_active_screen(); | |
109 | siv.screen_mut().add_layer(v); | |
183e2a76 CH |
110 | } |
111 | ||
64220ff1 CH |
112 | fn switch_to_prev_screen(siv: &mut Cursive) { |
113 | let id = siv.active_screen().saturating_sub(1); | |
114 | siv.set_screen(id); | |
115 | } | |
116 | ||
bb951500 | 117 | #[cfg(not(debug_assertions))] |
183e2a76 CH |
118 | fn yes_no_dialog( |
119 | siv: &mut Cursive, | |
120 | title: &str, | |
121 | text: &str, | |
122 | callback: &'static dyn Fn(&mut Cursive), | |
123 | ) { | |
124 | siv.add_layer( | |
125 | Dialog::around(TextView::new(text)) | |
126 | .title(title) | |
127 | .dismiss_button("No") | |
128 | .button("Yes", callback), | |
129 | ) | |
130 | } | |
131 | ||
ccf3b075 | 132 | fn trigger_abort_install_dialog(siv: &mut Cursive) { |
58869243 CH |
133 | #[cfg(debug_assertions)] |
134 | siv.quit(); | |
135 | ||
136 | #[cfg(not(debug_assertions))] | |
ccf3b075 CH |
137 | yes_no_dialog( |
138 | siv, | |
139 | "Abort installation?", | |
140 | "Are you sure you want to abort the installation?", | |
141 | &Cursive::quit, | |
142 | ) | |
143 | } | |
144 | ||
183e2a76 | 145 | fn abort_install_button() -> Button { |
ccf3b075 | 146 | Button::new("Abort", trigger_abort_install_dialog) |
183e2a76 CH |
147 | } |
148 | ||
149 | fn get_eula() -> String { | |
8f5fdd21 CH |
150 | // TODO: properly using info from Proxmox::Install::Env::setup() |
151 | std::fs::read_to_string("/cdrom/EULA") | |
152 | .unwrap_or_else(|_| "< Debug build - ignoring non-existing EULA >".to_owned()) | |
183e2a76 CH |
153 | } |
154 | ||
fbccf72c | 155 | fn license_dialog(_: &mut Cursive) -> InstallerView { |
183e2a76 CH |
156 | let inner = LinearLayout::vertical() |
157 | .child(PaddedView::lrtb( | |
158 | 0, | |
159 | 0, | |
160 | 1, | |
161 | 0, | |
162 | TextView::new("END USER LICENSE AGREEMENT (EULA)").center(), | |
163 | )) | |
8b43c2d3 CH |
164 | .child(Panel::new(ScrollView::new( |
165 | TextView::new(get_eula()).center(), | |
183e2a76 CH |
166 | ))) |
167 | .child(PaddedView::lrtb( | |
168 | 1, | |
169 | 1, | |
f522afb1 | 170 | 1, |
183e2a76 CH |
171 | 0, |
172 | LinearLayout::horizontal() | |
173 | .child(abort_install_button()) | |
174 | .child(DummyView.full_width()) | |
5e73fcfe CH |
175 | .child(Button::new("I agree", |siv| { |
176 | add_next_screen(siv, &bootdisk_dialog) | |
177 | })), | |
183e2a76 CH |
178 | )); |
179 | ||
f522afb1 | 180 | InstallerView::with_raw(inner) |
183e2a76 CH |
181 | } |
182 | ||
e70f1b2f | 183 | fn bootdisk_dialog(siv: &mut Cursive) -> InstallerView { |
ed191e60 | 184 | let data = siv.user_data::<InstallerData>().cloned().unwrap(); |
64220ff1 | 185 | |
f522afb1 | 186 | InstallerView::new( |
ed191e60 CH |
187 | BootdiskOptionsView::new(&data.available_disks, &data.options.bootdisk) |
188 | .with_name("bootdisk-options"), | |
f522afb1 CH |
189 | Box::new(|siv| { |
190 | let options = siv | |
ed191e60 | 191 | .call_on_name("bootdisk-options", BootdiskOptionsView::get_values) |
93ebe7bd | 192 | .flatten(); |
f522afb1 | 193 | |
93ebe7bd | 194 | if let Some(options) = options { |
ed191e60 CH |
195 | siv.with_user_data(|data: &mut InstallerData| { |
196 | data.options.bootdisk = options; | |
93ebe7bd | 197 | }); |
f522afb1 | 198 | |
5e73fcfe | 199 | add_next_screen(siv, &timezone_dialog); |
93ebe7bd CH |
200 | } else { |
201 | siv.add_layer(Dialog::info("Invalid values")); | |
202 | } | |
f522afb1 CH |
203 | }), |
204 | ) | |
64220ff1 CH |
205 | } |
206 | ||
a142f457 CH |
207 | fn timezone_dialog(siv: &mut Cursive) -> InstallerView { |
208 | let options = siv | |
ed191e60 CH |
209 | .user_data::<InstallerData>() |
210 | .map(|data| data.options.timezone.clone()) | |
a142f457 CH |
211 | .unwrap_or_default(); |
212 | ||
767843f9 CH |
213 | let inner = FormView::new() |
214 | .child("Country", EditView::new().content("Austria")) | |
215 | .child("Timezone", EditView::new().content(options.timezone)) | |
216 | .child( | |
a142f457 | 217 | "Keyboard layout", |
767843f9 CH |
218 | EditView::new().content(options.kb_layout), |
219 | ) | |
220 | .with_name("timezone-options"); | |
a142f457 CH |
221 | |
222 | InstallerView::new( | |
223 | inner, | |
224 | Box::new(|siv| { | |
767843f9 CH |
225 | let options: Option<Result<TimezoneOptions, String>> = |
226 | siv.call_on_name("timezone-options", |view: &mut FormView| { | |
227 | let timezone = view | |
228 | .get_value::<EditView, _>(1) | |
229 | .ok_or("failed to retrieve timezone")?; | |
a142f457 | 230 | |
767843f9 CH |
231 | let kb_layout = view |
232 | .get_value::<EditView, _>(2) | |
233 | .ok_or("failed to retrieve keyboard layout")?; | |
c2eee468 | 234 | |
767843f9 | 235 | Ok(TimezoneOptions { |
93ebe7bd CH |
236 | timezone, |
237 | kb_layout, | |
767843f9 | 238 | }) |
93ebe7bd CH |
239 | }); |
240 | ||
767843f9 CH |
241 | match options { |
242 | Some(Ok(options)) => { | |
243 | siv.with_user_data(|data: &mut InstallerData| { | |
244 | data.options.timezone = options; | |
245 | }); | |
246 | ||
247 | add_next_screen(siv, &password_dialog); | |
248 | } | |
249 | Some(Err(err)) => siv.add_layer(Dialog::info(format!("Invalid values: {err}"))), | |
250 | _ => siv.add_layer(Dialog::info("Invalid values")), | |
93ebe7bd | 251 | } |
a142f457 CH |
252 | }), |
253 | ) | |
183e2a76 | 254 | } |
c2eee468 CH |
255 | |
256 | fn password_dialog(siv: &mut Cursive) -> InstallerView { | |
257 | let options = siv | |
ed191e60 CH |
258 | .user_data::<InstallerData>() |
259 | .map(|data| data.options.password.clone()) | |
c2eee468 CH |
260 | .unwrap_or_default(); |
261 | ||
15832d18 CH |
262 | let inner = FormView::new() |
263 | .child("Root password", EditView::new().secret()) | |
264 | .child("Confirm root password", EditView::new().secret()) | |
265 | .child("Administator email", EditView::new().content(options.email)) | |
ef4957e8 | 266 | .with_name("password-options"); |
c2eee468 | 267 | |
ccd34284 CH |
268 | InstallerView::new( |
269 | inner, | |
270 | Box::new(|siv| { | |
15832d18 CH |
271 | let options = siv.call_on_name("password-options", |view: &mut FormView| { |
272 | let root_password = view | |
273 | .get_value::<EditView, _>(0) | |
274 | .ok_or("failed to retrieve password")?; | |
275 | ||
276 | let confirm_password = view | |
277 | .get_value::<EditView, _>(1) | |
278 | .ok_or("failed to retrieve password confirmation")?; | |
ef4957e8 | 279 | |
15832d18 CH |
280 | let email = view |
281 | .get_value::<EditView, _>(2) | |
282 | .ok_or("failed to retrieve email")?; | |
ef4957e8 CH |
283 | |
284 | // TODO: proper validation | |
285 | if root_password != confirm_password { | |
286 | Err("passwords do not match") | |
287 | } else { | |
288 | Ok(PasswordOptions { | |
289 | root_password, | |
15832d18 | 290 | email, |
ef4957e8 CH |
291 | }) |
292 | } | |
293 | }); | |
294 | ||
295 | match options { | |
296 | Some(Ok(options)) => { | |
297 | siv.with_user_data(|data: &mut InstallerData| { | |
298 | data.options.password = options; | |
299 | }); | |
300 | ||
301 | add_next_screen(siv, &network_dialog); | |
302 | } | |
303 | Some(Err(err)) => siv.add_layer(Dialog::info(format!("Invalid values: {err}"))), | |
304 | _ => siv.add_layer(Dialog::info("Invalid values")), | |
305 | } | |
ccd34284 CH |
306 | }), |
307 | ) | |
308 | } | |
309 | ||
310 | fn network_dialog(siv: &mut Cursive) -> InstallerView { | |
311 | let options = siv | |
ed191e60 CH |
312 | .user_data::<InstallerData>() |
313 | .map(|data| data.options.network.clone()) | |
ccd34284 CH |
314 | .unwrap_or_default(); |
315 | ||
316 | let inner = LinearLayout::vertical() | |
317 | .child(FormInputView::new( | |
318 | "Management interface", | |
319 | SelectView::new().popup().with_all_str(vec!["eth0"]), | |
320 | )) | |
321 | .child(FormInputView::new( | |
322 | "Hostname (FQDN)", | |
323 | EditView::new().content(options.fqdn), | |
324 | )) | |
325 | .child(FormInputView::new( | |
326 | "IP address (CIDR)", | |
fc187196 | 327 | CidrAddressEditView::new().content(options.address), |
ccd34284 CH |
328 | )) |
329 | .child(FormInputView::new( | |
330 | "Gateway address", | |
331 | EditView::new().content(options.gateway.to_string()), | |
332 | )) | |
333 | .child(FormInputView::new( | |
1397feec | 334 | "DNS server address", |
ccd34284 | 335 | EditView::new().content(options.dns_server.to_string()), |
cd383717 CH |
336 | )) |
337 | .with_name("network-options"); | |
ccd34284 | 338 | |
947fe360 CH |
339 | InstallerView::new( |
340 | inner, | |
341 | Box::new(|siv| { | |
cd383717 CH |
342 | let options = siv.call_on_name("network-options", |view: &mut LinearLayout| { |
343 | fn get_val<T, R>(view: &LinearLayout, index: usize) -> Option<R> | |
344 | where | |
345 | T: View, | |
346 | FormInputView<T>: FormInputViewGetValue<R>, | |
347 | { | |
348 | view.get_child(index)? | |
349 | .downcast_ref::<FormInputView<T>>()? | |
350 | .get_value() | |
351 | } | |
352 | ||
cd383717 CH |
353 | Some(NetworkOptions { |
354 | ifname: get_val::<SelectView, _>(view, 0)?, | |
355 | fqdn: get_val::<EditView, _>(view, 1)?, | |
fc187196 | 356 | address: get_val::<CidrAddressEditView, _>(view, 2)?, |
cd383717 CH |
357 | gateway: get_val::<EditView, _>(view, 3).and_then(|s| s.parse().ok())?, |
358 | dns_server: get_val::<EditView, _>(view, 3).and_then(|s| s.parse().ok())?, | |
359 | }) | |
360 | }); | |
361 | ||
362 | if let Some(options) = options.flatten() { | |
ed191e60 CH |
363 | siv.with_user_data(|data: &mut InstallerData| { |
364 | data.options.network = options; | |
cd383717 CH |
365 | }); |
366 | ||
5e73fcfe | 367 | add_next_screen(siv, &summary_dialog); |
e95f4f1a CH |
368 | } else { |
369 | siv.add_layer(Dialog::info("Invalid values")); | |
cd383717 | 370 | } |
947fe360 CH |
371 | }), |
372 | ) | |
373 | } | |
374 | ||
b7828c87 | 375 | pub struct SummaryOption { |
947fe360 CH |
376 | name: &'static str, |
377 | value: String, | |
378 | } | |
379 | ||
380 | impl SummaryOption { | |
381 | pub fn new<S: Into<String>>(name: &'static str, value: S) -> Self { | |
382 | Self { | |
383 | name, | |
384 | value: value.into(), | |
385 | } | |
386 | } | |
387 | } | |
388 | ||
389 | impl TableViewItem for SummaryOption { | |
390 | fn get_column(&self, name: &str) -> String { | |
391 | match name { | |
392 | "name" => self.name.to_owned(), | |
393 | "value" => self.value.clone(), | |
394 | _ => unreachable!(), | |
395 | } | |
396 | } | |
397 | } | |
398 | ||
399 | fn summary_dialog(siv: &mut Cursive) -> InstallerView { | |
400 | let options = siv | |
ed191e60 CH |
401 | .user_data::<InstallerData>() |
402 | .map(|d| d.options.clone()) | |
947fe360 CH |
403 | .unwrap(); |
404 | ||
405 | let inner = LinearLayout::vertical() | |
406 | .child(PaddedView::lrtb( | |
407 | 0, | |
408 | 0, | |
409 | 1, | |
410 | 2, | |
411 | TableView::new() | |
412 | .columns(&[ | |
413 | ("name".to_owned(), "Option".to_owned()), | |
414 | ("value".to_owned(), "Selected value".to_owned()), | |
415 | ]) | |
416 | .items(options.to_summary()), | |
417 | )) | |
418 | .child( | |
419 | LinearLayout::horizontal() | |
420 | .child(DummyView.full_width()) | |
421 | .child(Checkbox::new().with_name("reboot-after-install")) | |
422 | .child( | |
423 | TextView::new(" Automatically reboot after successful installation").no_wrap(), | |
424 | ) | |
425 | .child(DummyView.full_width()), | |
426 | ) | |
427 | .child(PaddedView::lrtb( | |
428 | 1, | |
429 | 1, | |
430 | 1, | |
431 | 0, | |
432 | LinearLayout::horizontal() | |
433 | .child(abort_install_button()) | |
434 | .child(DummyView.full_width()) | |
435 | .child(Button::new("Previous", switch_to_prev_screen)) | |
436 | .child(DummyView) | |
437 | .child(Button::new("Install", |_| {})), | |
438 | )); | |
439 | ||
440 | InstallerView::with_raw(inner) | |
c2eee468 | 441 | } |