1 #![forbid(unsafe_code)]
12 view
::{Nameable, Resizable, ViewWrapper}
,
14 Button
, Checkbox
, Dialog
, DummyView
, EditView
, LinearLayout
, PaddedView
, Panel
,
15 ResizedView
, ScrollView
, SelectView
, TextView
,
17 Cursive
, CursiveRunnable
, View
,
19 use setup
::{LocaleInfo, SetupInfo}
;
20 use std
::{env, net::IpAddr}
;
22 use views
::{BootdiskOptionsView, CidrAddressEditView, FormView, TableView, TableViewItem}
;
24 // TextView::center() seems to garble the first two lines, so fix it manually here.
25 const LOGO
: &str = r
#"
27 / __ \_________ _ ______ ___ ____ _ __ | | / / ____/
28 / /_/ / ___/ __ \| |/_/ __ `__ \/ __ \| |/_/ | | / / __/
29 / ____/ / / /_/ /> </ / / / / / /_/ /> < | |/ / /___
30 /_/ /_/ \____/_/|_/_/ /_/ /_/\____/_/|_| |___/_____/
33 const TITLE
: &str = "Proxmox VE Installer";
35 struct InstallerView
{
36 view
: ResizedView
<LinearLayout
>,
40 pub fn new
<T
: View
>(view
: T
, next_cb
: Box
<dyn Fn(&mut Cursive
)>) -> Self {
41 let inner
= LinearLayout
::vertical()
42 .child(PaddedView
::lrtb(0, 0, 1, 1, view
))
43 .child(PaddedView
::lrtb(
48 LinearLayout
::horizontal()
49 .child(abort_install_button())
50 .child(DummyView
.full_width())
51 .child(Button
::new("Previous", switch_to_prev_screen
))
53 .child(Button
::new("Next", next_cb
)),
59 pub fn with_raw
<T
: View
>(view
: T
) -> Self {
60 let inner
= LinearLayout
::vertical()
61 .child(PaddedView
::lrtb(1, 1, 0, 1, TextView
::new(LOGO
).center()))
62 .child(Dialog
::around(view
).title(TITLE
));
65 // Limit the maximum to something reasonable, such that it won't get spread out much
66 // depending on the screen.
67 view
: ResizedView
::with_max_size((120, 40), inner
),
72 impl ViewWrapper
for InstallerView
{
73 cursive
::wrap_impl
!(self.view
: ResizedView
<LinearLayout
>);
77 struct InstallerData
{
78 options
: InstallerOptions
,
79 available_disks
: Vec
<Disk
>,
80 setup_info
: SetupInfo
,
85 let mut siv
= cursive
::termion();
87 let (setup_info
, locales
) = match installer_setup() {
89 Err(err
) => initial_setup_error(&mut siv
, &err
),
92 siv
.clear_global_callbacks(Event
::CtrlChar('c'
));
93 siv
.set_on_pre_event(Event
::CtrlChar('c'
), trigger_abort_install_dialog
);
95 // TODO: retrieve actual disk info
96 let available_disks
= vec
![Disk
{
97 path
: "/dev/vda".to_owned(),
101 siv
.set_user_data(InstallerData
{
102 options
: InstallerOptions
{
103 bootdisk
: BootdiskOptions
::defaults_from(&available_disks
[0]),
104 timezone
: TimezoneOptions
::default(),
105 password
: PasswordOptions
::default(),
106 network
: NetworkOptions
::default(),
113 add_next_screen(&mut siv
, &license_dialog
);
117 fn installer_setup() -> Result
<(SetupInfo
, LocaleInfo
), String
> {
118 system
::has_min_requirements()?
;
126 .map_err(|err
| err
.to_string())
129 let mut path
= match env
::args().nth(1).as_deref() {
130 Some("-t") => testdir(),
132 #[cfg(debug_assertions)]
135 #[cfg(not(debug_assertions))]
136 _
=> Ok(std
::path
::PathBuf
::from("/run")),
140 path
.push("proxmox-installer");
142 let installer_info
= {
143 let mut path
= path
.clone();
144 path
.push("iso-info.json");
146 setup
::read_json(&path
).map_err(|err
| format
!("Failed to retrieve setup info: {err}"))?
150 let mut path
= path
.clone();
151 path
.push("locales.json");
153 setup
::read_json(&path
).map_err(|err
| format
!("Failed to retrieve locale info: {err}"))?
156 Ok((installer_info
, locale_info
))
159 fn initial_setup_error(siv
: &mut CursiveRunnable
, message
: &str) -> ! {
161 Dialog
::around(TextView
::new(message
))
162 .title("Installer setup error")
163 .button("Ok", Cursive
::quit
),
167 std
::process
::exit(1);
170 fn add_next_screen(siv
: &mut Cursive
, constructor
: &dyn Fn(&mut Cursive
) -> InstallerView
) {
171 let v
= constructor(siv
);
172 siv
.add_active_screen();
173 siv
.screen_mut().add_layer(v
);
176 fn switch_to_prev_screen(siv
: &mut Cursive
) {
177 let id
= siv
.active_screen().saturating_sub(1);
181 #[cfg(not(debug_assertions))]
186 callback
: &'
static dyn Fn(&mut Cursive
),
189 Dialog
::around(TextView
::new(text
))
191 .dismiss_button("No")
192 .button("Yes", callback
),
196 fn trigger_abort_install_dialog(siv
: &mut Cursive
) {
197 #[cfg(debug_assertions)]
200 #[cfg(not(debug_assertions))]
203 "Abort installation?",
204 "Are you sure you want to abort the installation?",
209 fn abort_install_button() -> Button
{
210 Button
::new("Abort", trigger_abort_install_dialog
)
213 fn get_eula() -> String
{
214 // TODO: properly using info from Proxmox::Install::Env::setup()
215 std
::fs
::read_to_string("/cdrom/EULA")
216 .unwrap_or_else(|_
| "< Debug build - ignoring non-existing EULA >".to_owned())
219 fn license_dialog(_
: &mut Cursive
) -> InstallerView
{
220 let inner
= LinearLayout
::vertical()
221 .child(PaddedView
::lrtb(
226 TextView
::new("END USER LICENSE AGREEMENT (EULA)").center(),
228 .child(Panel
::new(ScrollView
::new(
229 TextView
::new(get_eula()).center(),
231 .child(PaddedView
::lrtb(
236 LinearLayout
::horizontal()
237 .child(abort_install_button())
238 .child(DummyView
.full_width())
239 .child(Button
::new("I agree", |siv
| {
240 add_next_screen(siv
, &bootdisk_dialog
)
244 InstallerView
::with_raw(inner
)
247 fn bootdisk_dialog(siv
: &mut Cursive
) -> InstallerView
{
248 let data
= siv
.user_data
::<InstallerData
>().cloned().unwrap();
251 BootdiskOptionsView
::new(&data
.available_disks
, &data
.options
.bootdisk
)
252 .with_name("bootdisk-options"),
255 .call_on_name("bootdisk-options", BootdiskOptionsView
::get_values
)
258 if let Some(options
) = options
{
259 siv
.with_user_data(|data
: &mut InstallerData
| {
260 data
.options
.bootdisk
= options
;
263 add_next_screen(siv
, &timezone_dialog
);
265 siv
.add_layer(Dialog
::info("Invalid values"));
271 fn timezone_dialog(siv
: &mut Cursive
) -> InstallerView
{
273 .user_data
::<InstallerData
>()
274 .map(|data
| data
.options
.timezone
.clone())
275 .unwrap_or_default();
277 let inner
= FormView
::new()
278 .child("Country", EditView
::new().content("Austria"))
279 .child("Timezone", EditView
::new().content(options
.timezone
))
282 EditView
::new().content(options
.kb_layout
),
284 .with_name("timezone-options");
289 let options
: Option
<Result
<TimezoneOptions
, String
>> =
290 siv
.call_on_name("timezone-options", |view
: &mut FormView
| {
292 .get_value
::<EditView
, _
>(1)
293 .ok_or("failed to retrieve timezone")?
;
296 .get_value
::<EditView
, _
>(2)
297 .ok_or("failed to retrieve keyboard layout")?
;
306 Some(Ok(options
)) => {
307 siv
.with_user_data(|data
: &mut InstallerData
| {
308 data
.options
.timezone
= options
;
311 add_next_screen(siv
, &password_dialog
);
313 Some(Err(err
)) => siv
.add_layer(Dialog
::info(format
!("Invalid values: {err}"))),
314 _
=> siv
.add_layer(Dialog
::info("Invalid values")),
320 fn password_dialog(siv
: &mut Cursive
) -> InstallerView
{
322 .user_data
::<InstallerData
>()
323 .map(|data
| data
.options
.password
.clone())
324 .unwrap_or_default();
326 let inner
= FormView
::new()
327 .child("Root password", EditView
::new().secret())
328 .child("Confirm root password", EditView
::new().secret())
329 .child("Administator email", EditView
::new().content(options
.email
))
330 .with_name("password-options");
335 let options
= siv
.call_on_name("password-options", |view
: &mut FormView
| {
336 let root_password
= view
337 .get_value
::<EditView
, _
>(0)
338 .ok_or("failed to retrieve password")?
;
340 let confirm_password
= view
341 .get_value
::<EditView
, _
>(1)
342 .ok_or("failed to retrieve password confirmation")?
;
345 .get_value
::<EditView
, _
>(2)
346 .ok_or("failed to retrieve email")?
;
348 if root_password
.len() < 5 {
349 Err("password too short")
350 } else if root_password
!= confirm_password
{
351 Err("passwords do not match")
352 } else if email
.ends_with(".invalid") {
353 Err("invalid email address")
363 Some(Ok(options
)) => {
364 siv
.with_user_data(|data
: &mut InstallerData
| {
365 data
.options
.password
= options
;
368 add_next_screen(siv
, &network_dialog
);
370 Some(Err(err
)) => siv
.add_layer(Dialog
::info(format
!("Invalid values: {err}"))),
371 _
=> siv
.add_layer(Dialog
::info("Invalid values")),
377 fn network_dialog(siv
: &mut Cursive
) -> InstallerView
{
379 .user_data
::<InstallerData
>()
380 .map(|data
| data
.options
.network
.clone())
381 .unwrap_or_default();
383 let inner
= FormView
::new()
385 "Management interface",
386 SelectView
::new().popup().with_all_str(vec
!["eth0"]),
390 EditView
::new().content(options
.fqdn
.to_string()),
394 CidrAddressEditView
::new().content(options
.address
),
398 EditView
::new().content(options
.gateway
.to_string()),
401 "DNS server address",
402 EditView
::new().content(options
.dns_server
.to_string()),
404 .with_name("network-options");
409 let options
= siv
.call_on_name("network-options", |view
: &mut FormView
| {
411 .get_value
::<SelectView
, _
>(0)
412 .ok_or("failed to retrieve management interface name")?
;
415 .get_value
::<EditView
, _
>(1)
416 .ok_or("failed to retrieve host FQDN")?
418 .map_err(|_
| "failed to parse hostname".to_owned())?
;
421 .get_value
::<CidrAddressEditView
, _
>(2)
422 .ok_or("failed to retrieve host address")?
;
425 .get_value
::<EditView
, _
>(3)
426 .ok_or("failed to retrieve gateway address")?
428 .map_err(|err
| err
.to_string())?
;
430 let dns_server
= view
431 .get_value
::<EditView
, _
>(3)
432 .ok_or("failed to retrieve DNS server address")?
434 .map_err(|err
| err
.to_string())?
;
436 if address
.addr().is_ipv4() != gateway
.is_ipv4() {
437 Err("host and gateway IP address version must not differ".to_owned())
438 } else if address
.addr().is_ipv4() != dns_server
.is_ipv4() {
439 Err("host and DNS IP address version must not differ".to_owned())
440 } else if fqdn
.to_string().chars().all(|c
| c
.is_ascii_digit()) {
441 // Not supported/allowed on Debian
442 Err("hostname cannot be purely numeric".to_owned())
443 } else if fqdn
.to_string().ends_with(".invalid") {
444 Err("hostname does not look valid".to_owned())
457 Some(Ok(options
)) => {
458 siv
.with_user_data(|data
: &mut InstallerData
| {
459 data
.options
.network
= options
;
462 add_next_screen(siv
, &summary_dialog
);
464 Some(Err(err
)) => siv
.add_layer(Dialog
::info(format
!("Invalid values: {err}"))),
465 _
=> siv
.add_layer(Dialog
::info("Invalid values")),
471 pub struct SummaryOption
{
477 pub fn new
<S
: Into
<String
>>(name
: &'
static str, value
: S
) -> Self {
485 impl TableViewItem
for SummaryOption
{
486 fn get_column(&self, name
: &str) -> String
{
488 "name" => self.name
.to_owned(),
489 "value" => self.value
.clone(),
495 fn summary_dialog(siv
: &mut Cursive
) -> InstallerView
{
497 .user_data
::<InstallerData
>()
498 .map(|d
| d
.options
.clone())
501 let inner
= LinearLayout
::vertical()
502 .child(PaddedView
::lrtb(
509 ("name".to_owned(), "Option".to_owned()),
510 ("value".to_owned(), "Selected value".to_owned()),
512 .items(options
.to_summary()),
515 LinearLayout
::horizontal()
516 .child(DummyView
.full_width())
517 .child(Checkbox
::new().with_name("reboot-after-install"))
519 TextView
::new(" Automatically reboot after successful installation").no_wrap(),
521 .child(DummyView
.full_width()),
523 .child(PaddedView
::lrtb(
528 LinearLayout
::horizontal()
529 .child(abort_install_button())
530 .child(DummyView
.full_width())
531 .child(Button
::new("Previous", switch_to_prev_screen
))
533 .child(Button
::new("Install", |_
| {}
)),
536 InstallerView
::with_raw(inner
)