1 #![forbid(unsafe_code)]
10 view
::{Nameable, Resizable, ViewWrapper}
,
12 Button
, Checkbox
, Dialog
, DummyView
, EditView
, LinearLayout
, PaddedView
, Panel
,
13 ResizedView
, ScrollView
, SelectView
, TextView
,
18 use views
::{BootdiskOptionsView, CidrAddressEditView, FormView, TableView, TableViewItem}
;
20 // TextView::center() seems to garble the first two lines, so fix it manually here.
21 const LOGO
: &str = r
#"
23 / __ \_________ _ ______ ___ ____ _ __ | | / / ____/
24 / /_/ / ___/ __ \| |/_/ __ `__ \/ __ \| |/_/ | | / / __/
25 / ____/ / / /_/ /> </ / / / / / /_/ /> < | |/ / /___
26 /_/ /_/ \____/_/|_/_/ /_/ /_/\____/_/|_| |___/_____/
29 const TITLE
: &str = "Proxmox VE Installer";
31 struct InstallerView
{
32 view
: ResizedView
<LinearLayout
>,
36 pub fn new
<T
: View
>(view
: T
, next_cb
: Box
<dyn Fn(&mut Cursive
)>) -> Self {
37 let inner
= LinearLayout
::vertical()
38 .child(PaddedView
::lrtb(0, 0, 1, 1, view
))
39 .child(PaddedView
::lrtb(
44 LinearLayout
::horizontal()
45 .child(abort_install_button())
46 .child(DummyView
.full_width())
47 .child(Button
::new("Previous", switch_to_prev_screen
))
49 .child(Button
::new("Next", next_cb
)),
55 pub fn with_raw
<T
: View
>(view
: T
) -> Self {
56 let inner
= LinearLayout
::vertical()
57 .child(PaddedView
::lrtb(1, 1, 0, 1, TextView
::new(LOGO
).center()))
58 .child(Dialog
::around(view
).title(TITLE
));
61 // Limit the maximum to something reasonable, such that it won't get spread out much
62 // depending on the screen.
63 view
: ResizedView
::with_max_size((120, 40), inner
),
68 impl ViewWrapper
for InstallerView
{
69 cursive
::wrap_impl
!(self.view
: ResizedView
<LinearLayout
>);
73 struct InstallerData
{
74 options
: InstallerOptions
,
75 available_disks
: Vec
<Disk
>,
79 let mut siv
= cursive
::termion();
81 siv
.clear_global_callbacks(Event
::CtrlChar('c'
));
82 siv
.set_on_pre_event(Event
::CtrlChar('c'
), trigger_abort_install_dialog
);
84 // TODO: retrieve actual disk info
85 let available_disks
= vec
![Disk
{
86 path
: "/dev/vda".to_owned(),
90 siv
.set_user_data(InstallerData
{
91 options
: InstallerOptions
{
92 bootdisk
: BootdiskOptions
::defaults_from(&available_disks
[0]),
93 timezone
: TimezoneOptions
::default(),
94 password
: PasswordOptions
::default(),
95 network
: NetworkOptions
::default(),
100 add_next_screen(&mut siv
, &license_dialog
);
104 fn add_next_screen(siv
: &mut Cursive
, constructor
: &dyn Fn(&mut Cursive
) -> InstallerView
) {
105 let v
= constructor(siv
);
106 siv
.add_active_screen();
107 siv
.screen_mut().add_layer(v
);
110 fn switch_to_prev_screen(siv
: &mut Cursive
) {
111 let id
= siv
.active_screen().saturating_sub(1);
115 #[cfg(not(debug_assertions))]
120 callback
: &'
static dyn Fn(&mut Cursive
),
123 Dialog
::around(TextView
::new(text
))
125 .dismiss_button("No")
126 .button("Yes", callback
),
130 fn trigger_abort_install_dialog(siv
: &mut Cursive
) {
131 #[cfg(debug_assertions)]
134 #[cfg(not(debug_assertions))]
137 "Abort installation?",
138 "Are you sure you want to abort the installation?",
143 fn abort_install_button() -> Button
{
144 Button
::new("Abort", trigger_abort_install_dialog
)
147 fn get_eula() -> String
{
148 // TODO: properly using info from Proxmox::Install::Env::setup()
149 std
::fs
::read_to_string("/cdrom/EULA")
150 .unwrap_or_else(|_
| "< Debug build - ignoring non-existing EULA >".to_owned())
153 fn license_dialog(_
: &mut Cursive
) -> InstallerView
{
154 let inner
= LinearLayout
::vertical()
155 .child(PaddedView
::lrtb(
160 TextView
::new("END USER LICENSE AGREEMENT (EULA)").center(),
162 .child(Panel
::new(ScrollView
::new(
163 TextView
::new(get_eula()).center(),
165 .child(PaddedView
::lrtb(
170 LinearLayout
::horizontal()
171 .child(abort_install_button())
172 .child(DummyView
.full_width())
173 .child(Button
::new("I agree", |siv
| {
174 add_next_screen(siv
, &bootdisk_dialog
)
178 InstallerView
::with_raw(inner
)
181 fn bootdisk_dialog(siv
: &mut Cursive
) -> InstallerView
{
182 let data
= siv
.user_data
::<InstallerData
>().cloned().unwrap();
185 BootdiskOptionsView
::new(&data
.available_disks
, &data
.options
.bootdisk
)
186 .with_name("bootdisk-options"),
189 .call_on_name("bootdisk-options", BootdiskOptionsView
::get_values
)
192 if let Some(options
) = options
{
193 siv
.with_user_data(|data
: &mut InstallerData
| {
194 data
.options
.bootdisk
= options
;
197 add_next_screen(siv
, &timezone_dialog
);
199 siv
.add_layer(Dialog
::info("Invalid values"));
205 fn timezone_dialog(siv
: &mut Cursive
) -> InstallerView
{
207 .user_data
::<InstallerData
>()
208 .map(|data
| data
.options
.timezone
.clone())
209 .unwrap_or_default();
211 let inner
= FormView
::new()
212 .child("Country", EditView
::new().content("Austria"))
213 .child("Timezone", EditView
::new().content(options
.timezone
))
216 EditView
::new().content(options
.kb_layout
),
218 .with_name("timezone-options");
223 let options
: Option
<Result
<TimezoneOptions
, String
>> =
224 siv
.call_on_name("timezone-options", |view
: &mut FormView
| {
226 .get_value
::<EditView
, _
>(1)
227 .ok_or("failed to retrieve timezone")?
;
230 .get_value
::<EditView
, _
>(2)
231 .ok_or("failed to retrieve keyboard layout")?
;
240 Some(Ok(options
)) => {
241 siv
.with_user_data(|data
: &mut InstallerData
| {
242 data
.options
.timezone
= options
;
245 add_next_screen(siv
, &password_dialog
);
247 Some(Err(err
)) => siv
.add_layer(Dialog
::info(format
!("Invalid values: {err}"))),
248 _
=> siv
.add_layer(Dialog
::info("Invalid values")),
254 fn password_dialog(siv
: &mut Cursive
) -> InstallerView
{
256 .user_data
::<InstallerData
>()
257 .map(|data
| data
.options
.password
.clone())
258 .unwrap_or_default();
260 let inner
= FormView
::new()
261 .child("Root password", EditView
::new().secret())
262 .child("Confirm root password", EditView
::new().secret())
263 .child("Administator email", EditView
::new().content(options
.email
))
264 .with_name("password-options");
269 let options
= siv
.call_on_name("password-options", |view
: &mut FormView
| {
270 let root_password
= view
271 .get_value
::<EditView
, _
>(0)
272 .ok_or("failed to retrieve password")?
;
274 let confirm_password
= view
275 .get_value
::<EditView
, _
>(1)
276 .ok_or("failed to retrieve password confirmation")?
;
279 .get_value
::<EditView
, _
>(2)
280 .ok_or("failed to retrieve email")?
;
282 if root_password
.len() < 5 {
283 Err("password too short")
284 } else if root_password
!= confirm_password
{
285 Err("passwords do not match")
286 } else if email
.ends_with(".invalid") {
287 Err("invalid email address")
297 Some(Ok(options
)) => {
298 siv
.with_user_data(|data
: &mut InstallerData
| {
299 data
.options
.password
= options
;
302 add_next_screen(siv
, &network_dialog
);
304 Some(Err(err
)) => siv
.add_layer(Dialog
::info(format
!("Invalid values: {err}"))),
305 _
=> siv
.add_layer(Dialog
::info("Invalid values")),
311 fn network_dialog(siv
: &mut Cursive
) -> InstallerView
{
313 .user_data
::<InstallerData
>()
314 .map(|data
| data
.options
.network
.clone())
315 .unwrap_or_default();
317 let inner
= FormView
::new()
319 "Management interface",
320 SelectView
::new().popup().with_all_str(vec
!["eth0"]),
322 .child("Hostname (FQDN)", EditView
::new().content(options
.fqdn
))
325 CidrAddressEditView
::new().content(options
.address
),
329 EditView
::new().content(options
.gateway
.to_string()),
332 "DNS server address",
333 EditView
::new().content(options
.dns_server
.to_string()),
335 .with_name("network-options");
340 let options
= siv
.call_on_name("network-options", |view
: &mut FormView
| {
342 .get_value
::<SelectView
, _
>(0)
343 .ok_or("failed to retrieve management interface name")?
;
346 .get_value
::<EditView
, _
>(1)
347 .ok_or("failed to retrieve host FQDN")?
;
350 .get_value
::<CidrAddressEditView
, _
>(2)
351 .ok_or("failed to retrieve host address")?
;
354 .get_value
::<EditView
, _
>(3)
355 .ok_or("failed to retrieve gateway address")?
357 .map_err(|err
| err
.to_string())?
;
359 let dns_server
= view
360 .get_value
::<EditView
, _
>(3)
361 .ok_or("failed to retrieve DNS server address")?
363 .map_err(|err
| err
.to_string())?
;
365 if address
.addr().is_ipv4() != gateway
.is_ipv4() {
366 Err("host and gateway IP address version must not differ".to_owned())
367 } else if address
.addr().is_ipv4() != dns_server
.is_ipv4() {
368 Err("host and DNS IP address version must not differ".to_owned())
369 } else if fqdn
.chars().all(|c
| c
.is_ascii_digit()) {
370 // Not supported/allowed on Debian
371 Err("hostname cannot be purely numeric".to_owned())
384 Some(Ok(options
)) => {
385 siv
.with_user_data(|data
: &mut InstallerData
| {
386 data
.options
.network
= options
;
389 add_next_screen(siv
, &summary_dialog
);
391 Some(Err(err
)) => siv
.add_layer(Dialog
::info(format
!("Invalid values: {err}"))),
392 _
=> siv
.add_layer(Dialog
::info("Invalid values")),
398 pub struct SummaryOption
{
404 pub fn new
<S
: Into
<String
>>(name
: &'
static str, value
: S
) -> Self {
412 impl TableViewItem
for SummaryOption
{
413 fn get_column(&self, name
: &str) -> String
{
415 "name" => self.name
.to_owned(),
416 "value" => self.value
.clone(),
422 fn summary_dialog(siv
: &mut Cursive
) -> InstallerView
{
424 .user_data
::<InstallerData
>()
425 .map(|d
| d
.options
.clone())
428 let inner
= LinearLayout
::vertical()
429 .child(PaddedView
::lrtb(
436 ("name".to_owned(), "Option".to_owned()),
437 ("value".to_owned(), "Selected value".to_owned()),
439 .items(options
.to_summary()),
442 LinearLayout
::horizontal()
443 .child(DummyView
.full_width())
444 .child(Checkbox
::new().with_name("reboot-after-install"))
446 TextView
::new(" Automatically reboot after successful installation").no_wrap(),
448 .child(DummyView
.full_width()),
450 .child(PaddedView
::lrtb(
455 LinearLayout
::horizontal()
456 .child(abort_install_button())
457 .child(DummyView
.full_width())
458 .child(Button
::new("Previous", switch_to_prev_screen
))
460 .child(Button
::new("Install", |_
| {}
)),
463 InstallerView
::with_raw(inner
)