1 #![forbid(unsafe_code)]
5 use crate::views
::DiskSizeFormInputView
;
8 view
::{Finder, Nameable, Resizable, ViewWrapper}
,
10 Button
, Checkbox
, Dialog
, DummyView
, EditView
, LinearLayout
, PaddedView
, Panel
,
11 ResizedView
, ScrollView
, SelectView
, TextView
,
17 net
::{IpAddr, Ipv4Addr}
,
19 use views
::{CidrAddressEditView, FormInputView, TableView, TableViewItem}
;
21 // TextView::center() seems to garble the first two lines, so fix it manually here.
22 const LOGO
: &str = r
#"
24 / __ \_________ _ ______ ___ ____ _ __ | | / / ____/
25 / /_/ / ___/ __ \| |/_/ __ `__ \/ __ \| |/_/ | | / / __/
26 / ____/ / / /_/ /> </ / / / / / /_/ /> < | |/ / /___
27 /_/ /_/ \____/_/|_/_/ /_/ /_/\____/_/|_| |___/_____/
30 const TITLE
: &str = "Proxmox VE Installer";
32 struct InstallerView
{
33 view
: ResizedView
<LinearLayout
>,
37 pub fn new
<T
: View
>(view
: T
, next_cb
: Box
<dyn Fn(&mut Cursive
)>) -> Self {
38 let inner
= LinearLayout
::vertical().child(view
).child(PaddedView
::lrtb(
43 LinearLayout
::horizontal()
44 .child(abort_install_button())
45 .child(DummyView
.full_width())
46 .child(Button
::new("Previous", switch_to_prev_screen
))
48 .child(Button
::new("Next", next_cb
)),
54 pub fn with_raw
<T
: View
>(view
: T
) -> Self {
55 let inner
= LinearLayout
::vertical()
56 .child(PaddedView
::lrtb(1, 1, 0, 1, TextView
::new(LOGO
).center()))
57 .child(Dialog
::around(view
).title(TITLE
));
60 // Limit the maximum to something reasonable, such that it won't get spread out much
61 // depending on the screen.
62 view
: ResizedView
::with_max_size((120, 40), inner
),
67 impl ViewWrapper
for InstallerView
{
68 cursive
::wrap_impl
!(self.view
: ResizedView
<LinearLayout
>);
71 #[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
78 impl fmt
::Display
for FsType
{
79 fn fmt(&self, f
: &mut fmt
::Formatter
) -> fmt
::Result
{
81 FsType
::Ext4
=> "ext4",
88 const FS_TYPES
: &[FsType
] = &[FsType
::Ext4
, FsType
::Xfs
];
90 #[derive(Clone, Debug)]
91 struct LvmBootdiskOptions
{
100 impl LvmBootdiskOptions
{
101 fn defaults_from(disk
: &Disk
) -> Self {
102 let min_lvm_free
= if disk
.size
> 128 * 1024 * 1024 {
110 total_size
: disk
.size
,
111 swap_size
: 4 * 1024 * 1024, // TODO: value from installed memory
119 #[derive(Clone, Debug)]
120 enum AdvancedBootdiskOptions
{
121 Lvm(LvmBootdiskOptions
),
124 impl AdvancedBootdiskOptions
{
125 fn selected_disks(&self) -> impl Iterator
<Item
= &Disk
> {
127 AdvancedBootdiskOptions
::Lvm(LvmBootdiskOptions { disk, .. }
) => iter
::once(disk
),
132 #[derive(Clone, Debug)]
138 impl fmt
::Display
for Disk
{
139 fn fmt(&self, f
: &mut fmt
::Formatter
<'_
>) -> fmt
::Result
{
140 // TODO: Format sizes properly with `proxmox-human-byte` once merged
141 // https://lists.proxmox.com/pipermail/pbs-devel/2023-May/006125.html
142 write
!(f
, "{} ({} B)", self.path
, self.size
)
146 #[derive(Clone, Debug)]
147 struct BootdiskOptions
{
150 advanced
: AdvancedBootdiskOptions
,
153 #[derive(Clone, Debug)]
154 struct TimezoneOptions
{
159 impl Default
for TimezoneOptions
{
160 fn default() -> Self {
162 timezone
: "Europe/Vienna".to_owned(),
163 kb_layout
: "en_US".to_owned(),
168 #[derive(Clone, Debug)]
169 struct PasswordOptions
{
171 root_password
: String
,
174 impl Default
for PasswordOptions
{
175 fn default() -> Self {
177 email
: "mail@example.invalid".to_owned(),
178 root_password
: String
::new(),
183 #[derive(Clone, Debug)]
184 struct NetworkOptions
{
193 impl Default
for NetworkOptions
{
194 fn default() -> Self {
195 // TODO: Retrieve automatically
197 ifname
: String
::new(),
198 fqdn
: "pve.example.invalid".to_owned(),
199 ip_addr
: IpAddr
::V4(Ipv4Addr
::UNSPECIFIED
),
201 gateway
: IpAddr
::V4(Ipv4Addr
::UNSPECIFIED
),
202 dns_server
: IpAddr
::V4(Ipv4Addr
::UNSPECIFIED
),
207 #[derive(Clone, Debug)]
208 struct InstallerOptions
{
209 bootdisk
: BootdiskOptions
,
210 timezone
: TimezoneOptions
,
211 password
: PasswordOptions
,
212 network
: NetworkOptions
,
215 impl InstallerOptions
{
216 fn to_summary(&self) -> Vec
<SummaryOption
> {
218 SummaryOption
::new("Bootdisk filesystem", self.bootdisk
.fstype
.to_string()),
224 .map(|d
| d
.path
.as_str())
225 .collect
::<Vec
<&str>>()
228 SummaryOption
::new("Timezone", &self.timezone
.timezone
),
229 SummaryOption
::new("Keyboard layout", &self.timezone
.kb_layout
),
230 SummaryOption
::new("Administator email:", &self.password
.email
),
231 SummaryOption
::new("Management interface:", &self.network
.ifname
),
232 SummaryOption
::new("Hostname:", &self.network
.fqdn
),
235 format
!("{}/{}", self.network
.ip_addr
, self.network
.cidr_mask
),
237 SummaryOption
::new("Gateway", self.network
.gateway
.to_string()),
238 SummaryOption
::new("DNS:", self.network
.dns_server
.to_string()),
244 let mut siv
= cursive
::termion();
246 siv
.clear_global_callbacks(Event
::CtrlChar('c'
));
247 siv
.set_on_pre_event(Event
::CtrlChar('c'
), trigger_abort_install_dialog
);
249 let disks
= vec
![Disk
{
250 path
: "/dev/vda".to_owned(),
253 siv
.set_user_data(InstallerOptions
{
254 bootdisk
: BootdiskOptions
{
255 disks
: disks
.clone(),
256 fstype
: FsType
::default(),
257 advanced
: AdvancedBootdiskOptions
::Lvm(LvmBootdiskOptions
::defaults_from(&disks
[0])),
259 timezone
: TimezoneOptions
::default(),
260 password
: PasswordOptions
::default(),
261 network
: NetworkOptions
::default(),
264 add_next_screen(&license_dialog
)(&mut siv
);
269 constructor
: &dyn Fn(&mut Cursive
) -> InstallerView
,
270 ) -> Box
<dyn Fn(&mut Cursive
) + '_
> {
271 Box
::new(|siv
: &mut Cursive
| {
272 let v
= constructor(siv
);
273 siv
.add_active_screen();
274 siv
.screen_mut().add_layer(v
);
278 fn switch_to_prev_screen(siv
: &mut Cursive
) {
279 let id
= siv
.active_screen().saturating_sub(1);
287 callback
: &'
static dyn Fn(&mut Cursive
),
290 Dialog
::around(TextView
::new(text
))
292 .dismiss_button("No")
293 .button("Yes", callback
),
297 fn trigger_abort_install_dialog(siv
: &mut Cursive
) {
298 #[cfg(debug_assertions)]
301 #[cfg(not(debug_assertions))]
304 "Abort installation?",
305 "Are you sure you want to abort the installation?",
310 fn abort_install_button() -> Button
{
311 Button
::new("Abort", trigger_abort_install_dialog
)
314 fn get_eula() -> String
{
315 // TODO: properly using info from Proxmox::Install::Env::setup()
316 std
::fs
::read_to_string("/cdrom/EULA")
317 .unwrap_or_else(|_
| "< Debug build - ignoring non-existing EULA >".to_owned())
320 fn license_dialog(_
: &mut Cursive
) -> InstallerView
{
321 let inner
= LinearLayout
::vertical()
322 .child(PaddedView
::lrtb(
327 TextView
::new("END USER LICENSE AGREEMENT (EULA)").center(),
329 .child(Panel
::new(ScrollView
::new(
330 TextView
::new(get_eula()).center(),
332 .child(PaddedView
::lrtb(
337 LinearLayout
::horizontal()
338 .child(abort_install_button())
339 .child(DummyView
.full_width())
340 .child(Button
::new("I agree", add_next_screen(&bootdisk_dialog
))),
343 InstallerView
::with_raw(inner
)
346 fn bootdisk_dialog(siv
: &mut Cursive
) -> InstallerView
{
348 .user_data
::<InstallerOptions
>()
353 let AdvancedBootdiskOptions
::Lvm(advanced
) = options
.advanced
;
355 let fstype_select
= LinearLayout
::horizontal()
356 .child(TextView
::new("Filesystem: "))
357 .child(DummyView
.full_width())
361 .with_all(FS_TYPES
.iter().map(|t
| (t
.to_string(), t
)))
365 .position(|t
| *t
== options
.fstype
)
366 .unwrap_or_default(),
369 let disks
= options
.disks
.clone();
370 let advanced
= advanced
.clone();
371 move |siv
, fstype
: &FsType
| {
372 let view
= match fstype
{
373 FsType
::Ext4
| FsType
::Xfs
=> {
374 LvmBootdiskOptionsView
::new(&disks
, &advanced
)
378 siv
.call_on_name("bootdisk-options", |v
: &mut LinearLayout
| {
388 let inner
= LinearLayout
::vertical()
389 .child(fstype_select
)
392 LinearLayout
::horizontal()
393 .child(LvmBootdiskOptionsView
::new(&options
.disks
, &advanced
))
394 .with_name("bootdisk-options"),
401 .call_on_name("bootdisk-options", |v
: &mut LinearLayout
| {
403 .downcast_mut
::<LvmBootdiskOptionsView
>()?
405 .map(AdvancedBootdiskOptions
::Lvm
)
409 if let Some(options
) = options
{
410 siv
.with_user_data(|opts
: &mut InstallerOptions
| {
411 opts
.bootdisk
.advanced
= options
;
414 add_next_screen(&timezone_dialog
)(siv
)
416 siv
.add_layer(Dialog
::info("Invalid values"));
422 struct LvmBootdiskOptionsView
{
426 impl LvmBootdiskOptionsView
{
427 fn new(disks
: &[Disk
], options
: &LvmBootdiskOptions
) -> Self {
428 let view
= LinearLayout
::vertical()
429 .child(FormInputView
::new(
433 .with_all(disks
.iter().map(|d
| (d
.to_string(), d
.clone())))
434 .with_name("bootdisk-disk"),
436 .child(DiskSizeFormInputView
::new("Total size").content(options
.total_size
))
437 .child(DiskSizeFormInputView
::new("Swap size").content(options
.swap_size
))
439 DiskSizeFormInputView
::new("Maximum root volume size")
440 .content(options
.max_root_size
),
443 DiskSizeFormInputView
::new("Maximum data volume size")
444 .content(options
.max_data_size
),
447 DiskSizeFormInputView
::new("Minimum free LVM space").content(options
.min_lvm_free
),
453 fn get_values(&mut self) -> Option
<LvmBootdiskOptions
> {
456 .call_on_name("bootdisk-disk", |view
: &mut SelectView
<Disk
>| {
459 .map(|d
| (*d
).clone())?
;
461 let mut get_disksize_value
= |i
| {
464 .downcast_mut
::<DiskSizeFormInputView
>()?
468 Some(LvmBootdiskOptions
{
470 total_size
: get_disksize_value(1)?
,
471 swap_size
: get_disksize_value(2)?
,
472 max_root_size
: get_disksize_value(3)?
,
473 max_data_size
: get_disksize_value(4)?
,
474 min_lvm_free
: get_disksize_value(5)?
,
479 impl ViewWrapper
for LvmBootdiskOptionsView
{
480 cursive
::wrap_impl
!(self.view
: LinearLayout
);
483 fn timezone_dialog(siv
: &mut Cursive
) -> InstallerView
{
485 .user_data
::<InstallerOptions
>()
486 .map(|o
| o
.timezone
.clone())
487 .unwrap_or_default();
489 let inner
= LinearLayout
::vertical()
490 .child(FormInputView
::new(
492 EditView
::new().content("Austria"),
494 .child(FormInputView
::new(
497 .content(options
.timezone
)
498 .with_name("timezone-tzname"),
500 .child(FormInputView
::new(
503 .content(options
.kb_layout
)
504 .with_name("timezone-kblayout"),
510 let timezone
= siv
.call_on_name("timezone-tzname", |v
: &mut EditView
| {
511 (*v
.get_content()).clone()
514 let kb_layout
= siv
.call_on_name("timezone-kblayout", |v
: &mut EditView
| {
515 (*v
.get_content()).clone()
518 if let (Some(timezone
), Some(kb_layout
)) = (timezone
, kb_layout
) {
519 siv
.with_user_data(|opts
: &mut InstallerOptions
| {
520 opts
.timezone
= TimezoneOptions
{
526 add_next_screen(&password_dialog
)(siv
);
528 siv
.add_layer(Dialog
::info("Invalid values"));
534 fn password_dialog(siv
: &mut Cursive
) -> InstallerView
{
536 .user_data
::<InstallerOptions
>()
537 .map(|o
| o
.password
.clone())
538 .unwrap_or_default();
540 let inner
= LinearLayout
::vertical()
541 .child(FormInputView
::new(
545 .with_name("password-dialog-root-pw"),
547 .child(FormInputView
::new(
548 "Confirm root password",
551 .with_name("password-dialog-root-pw-confirm"),
553 .child(FormInputView
::new(
554 "Administator email",
556 .content(options
.email
)
557 .with_name("password-dialog-email"),
563 // TODO: password validation
564 add_next_screen(&network_dialog
)(siv
)
569 fn network_dialog(siv
: &mut Cursive
) -> InstallerView
{
571 .user_data
::<InstallerOptions
>()
572 .map(|o
| o
.network
.clone())
573 .unwrap_or_default();
575 let inner
= LinearLayout
::vertical()
576 .child(FormInputView
::new(
577 "Management interface",
578 SelectView
::new().popup().with_all_str(vec
!["eth0"]),
580 .child(FormInputView
::new(
582 EditView
::new().content(options
.fqdn
),
584 .child(FormInputView
::new(
586 CidrAddressEditView
::new().content(options
.ip_addr
, options
.cidr_mask
),
588 .child(FormInputView
::new(
590 EditView
::new().content(options
.gateway
.to_string()),
592 .child(FormInputView
::new(
593 "DNS server address",
594 EditView
::new().content(options
.dns_server
.to_string()),
600 add_next_screen(&summary_dialog
)(siv
);
605 struct SummaryOption
{
611 pub fn new
<S
: Into
<String
>>(name
: &'
static str, value
: S
) -> Self {
619 impl TableViewItem
for SummaryOption
{
620 fn get_column(&self, name
: &str) -> String
{
622 "name" => self.name
.to_owned(),
623 "value" => self.value
.clone(),
629 fn summary_dialog(siv
: &mut Cursive
) -> InstallerView
{
631 .user_data
::<InstallerOptions
>()
635 let inner
= LinearLayout
::vertical()
636 .child(PaddedView
::lrtb(
643 ("name".to_owned(), "Option".to_owned()),
644 ("value".to_owned(), "Selected value".to_owned()),
646 .items(options
.to_summary()),
649 LinearLayout
::horizontal()
650 .child(DummyView
.full_width())
651 .child(Checkbox
::new().with_name("reboot-after-install"))
653 TextView
::new(" Automatically reboot after successful installation").no_wrap(),
655 .child(DummyView
.full_width()),
657 .child(PaddedView
::lrtb(
662 LinearLayout
::horizontal()
663 .child(abort_install_button())
664 .child(DummyView
.full_width())
665 .child(Button
::new("Previous", switch_to_prev_screen
))
667 .child(Button
::new("Install", |_
| {}
)),
670 InstallerView
::with_raw(inner
)