]> git.proxmox.com Git - pve-installer.git/blobdiff - proxinstall
merge installer into single package
[pve-installer.git] / proxinstall
index 963152aaa8df5bcfed4a1ddbf742057a223905cc..7bd4892adf5f0821d47b878ab82c324eeeefe0b8 100755 (executable)
@@ -1,17 +1,18 @@
 #!/usr/bin/perl
 
-$ENV{DEBIAN_FRONTEND} = 'noninteractive';
-$ENV{LC_ALL} = 'C';
-
 use strict;
 use warnings;
 
+$ENV{DEBIAN_FRONTEND} = 'noninteractive';
+$ENV{LC_ALL} = 'C';
+
 use Getopt::Long;
 use IPC::Open2;
 use IPC::Open3;
 use IO::File;
 use IO::Select;
 use Cwd 'abs_path';
+use Glib;
 use Gtk3 '-init';
 use Gtk3::WebKit2;
 use Encode;
@@ -20,31 +21,26 @@ use Data::Dumper;
 use File::Basename;
 use File::Path;
 use Time::HiRes;
+use POSIX ":sys_wait_h";
 
 use ProxmoxInstallerSetup;
 
-my $setup = ProxmoxInstallerSetup::setup();
-
-my $opt_testmode;
-
 if (!$ENV{G_SLICE} ||  $ENV{G_SLICE} ne "always-malloc") {
     die "do not use slice allocator (run with 'G_SLICE=always-malloc ./proxinstall ...')\n";
 }
 
+my $opt_testmode;
 if (!GetOptions('testmode=s' => \$opt_testmode)) {
     die "usage error\n";
     exit (-1);
 }
 
+my ($setup, $cd_info) = ProxmoxInstallerSetup::setup();
+
 my $zfstestpool = "test_rpool";
 my $zfspoolname = $opt_testmode ? $zfstestpool : 'rpool';
 my $zfsrootvolname = "$setup->{product}-1";
 
-my $product_fullname = {
-    pve => 'Proxmox VE',
-    pmg => 'Proxmox MailGateway',
-};
-
 my $storage_cfg_zfs = <<__EOD__;
 dir: local
        path /var/lib/vz
@@ -97,8 +93,10 @@ sub file_read_firstline {
 
 my $logfd = IO::File->new(">/tmp/install.log");
 
-my $proxmox_libdir = $opt_testmode ?
-    Cwd::cwd() . "/testdir/var/lib/pve-installer" : "/var/lib/pve-installer";
+my $proxmox_libdir = $opt_testmode
+    ? Cwd::cwd() . "/testdir/var/lib/proxmox-installer"
+    : "/var/lib/proxmox-installer"
+    ;
 my $proxmox_cddir = $opt_testmode ? "../pve-cd-builder/tmp/data-gz/" : "/cdrom";
 my $proxmox_pkgdir = "${proxmox_cddir}/proxmox/packages/";
 
@@ -242,7 +240,7 @@ my $prev_btn;
 my ($next, $next_fctn, $target_hd);
 my ($progress, $progress_status);
 
-my ($ipversion, $ipaddress, $ipconf_entry_addr);
+my ($ipversion, $ipaddress, $cidr, $ipconf_entry_addr);
 my ($netmask, $ipconf_entry_mask);
 my ($gateway, $ipconf_entry_gw);
 my ($dnsserver, $ipconf_entry_dns);
@@ -256,6 +254,7 @@ my $keymap = 'en-us';
 my $password;
 my $mailto = 'mail@example.invalid';
 my $cmap;
+my $autoreboot_seconds = 5;
 
 my $config = {
     # TODO: add all the user-provided options for previous button
@@ -276,9 +275,11 @@ my $config = {
 
 # parse command line args
 
-my $config_options = {};
+my $config_options = {
+    autoreboot => 1,
+};
 
-if ($cmdline =~ m/\s(ext3|ext4|xfs)(\s.*)?$/) {
+if ($cmdline =~ m/\s(ext4|xfs)(\s.*)?$/) {
     $config_options->{filesys} = $1;
 } else {
     $config_options->{filesys} = 'ext4';
@@ -328,6 +329,8 @@ mynetworks = 127.0.0.0/8
 inet_interfaces = loopback-only
 recipient_delimiter = +
 
+compatibility_level = 2
+
 _EOD
 
 sub shellquote {
@@ -411,7 +414,7 @@ sub run_command {
     print $writer $input if defined $input;
     close $writer;
 
-    my $select = new IO::Select;
+    my $select = IO::Select->new();
     $select->add($reader);
     $select->add($error);
 
@@ -468,6 +471,23 @@ sub run_command {
     return $ostream;
 }
 
+# forks and runs the provided coderef in the child
+# do not use syscmd or run_command as both confuse the GTK mainloop if
+# run from a child process
+sub run_in_background {
+    my ($cmd) = @_;
+
+    my $pid = fork() // die "fork failed: $!\n";
+    if (!$pid) {
+       eval { $cmd->(); };
+       if (my $err = $@) {
+           warn "run_in_background error: $err\n";
+           POSIX::_exit(1);
+       }
+       POSIX::_exit(0);
+    }
+}
+
 sub detect_country {
 
     print "trying to detect country...\n";
@@ -513,16 +533,16 @@ sub detect_country {
 
 sub get_memtotal {
 
-    open (MEMINFO, "/proc/meminfo");
+    open (my $MEMINFO, '<', '/proc/meminfo');
 
     my $res = 512; # default to 512 if something goes wrong
-    while (my $line = <MEMINFO>) {
+    while (my $line = <$MEMINFO>) {
        if ($line =~ m/^MemTotal:\s+(\d+)\s*kB/i) {
            $res = int ($1 / 1024);
        }
     }
 
-    close (MEMINFO);
+    close($MEMINFO);
 
     return $res;
 }
@@ -566,7 +586,7 @@ sub hd_list {
        my @disks = split /,/, $opt_testmode;
 
        for my $disk (@disks) {
-           push @$res, [-1, $disk, int((-s $disk)/512), "TESTDISK"];
+           push @$res, [-1, $disk, int((-s $disk)/512), "TESTDISK", 512];
        }
        return $res;
     }
@@ -609,7 +629,12 @@ sub hd_list {
            if (length ($model) > 30) {
                $model = substr ($model, 0, 30);
            }
-           push @$res, [$count++, $real_name, $size, $model] if $size;
+
+           my $logical_bsize = file_read_firstline("$bd/queue/logical_block_size") // '';
+           chomp $logical_bsize;
+           $logical_bsize = undef if !($logical_bsize && $logical_bsize =~ m/^\d+$/);
+
+           push @$res, [$count++, $real_name, $size, $model, $logical_bsize] if $size;
        } else {
            print STDERR "ERROR: unable to map device $dev ($bd)\n";
        }
@@ -620,13 +645,13 @@ sub hd_list {
 
 sub read_cmap {
     my $countryfn = "${proxmox_libdir}/country.dat";
-    open (TMP, "<$countryfn") || die "unable to open '$countryfn' - $!\n";
+    open (my $TMP, "<:encoding(utf8)", "$countryfn") || die "unable to open '$countryfn' - $!\n";
     my $line;
     my $country = {};
     my $countryhash = {};
     my $kmap = {};
     my $kmaphash = {};
-    while (defined ($line = <TMP>)) {
+    while (defined ($line = <$TMP>)) {
        if ($line =~ m|^map:([^\s:]+):([^:]+):([^:]+):([^:]+):([^:]+):([^:]*):$|) {
            $kmap->{$1} = {
                name => $2,
@@ -647,13 +672,14 @@ sub read_cmap {
            warn "unable to parse 'country.dat' line: $line";
        }
     }
-    close (TMP);
+    close ($TMP);
+    $TMP = undef;
 
     my $zones = {};
     my $cczones = {};
     my $zonefn = "/usr/share/zoneinfo/zone.tab";
-    open (TMP, "<$zonefn") || die "unable to open '$zonefn' - $!\n";
-    while (defined ($line = <TMP>)) {
+    open ($TMP, '<', "$zonefn") || die "unable to open '$zonefn' - $!\n";
+    while (defined ($line = <$TMP>)) {
        next if $line =~ m/^\#/;
        next if $line =~ m/^\s*$/;
        if ($line =~ m|^([A-Z][A-Z])\s+\S+\s+(([^/]+)/\S+)\s|) {
@@ -664,7 +690,7 @@ sub read_cmap {
 
        }
     }
-    close (TMP);
+    close ($TMP);
 
     return {
        zones => $zones,
@@ -683,14 +709,25 @@ sub hd_size {
     my ($dev) = @_;
 
     foreach my $hd (@$hds) {
-       my ($disk, $devname, $size, $model) = @$hd;
-       # size is always in 512B "sectors"! convert to KB
+       my ($disk, $devname, $size, $model, $logical_bsize) = @$hd;
+       # size is always (also for 4kn disks) in 512B "sectors"! convert to KB
        return int($size/2) if $devname eq $dev;
     }
 
     die "no such device '$dev'\n";
 }
 
+sub logical_blocksize {
+    my ($dev) = @_;
+
+    foreach my $hd (@$hds) {
+       my ($disk, $devname, $size, $model, $logical_bsize) = @$hd;
+       return $logical_bsize if $devname eq $dev;
+    }
+
+    die "no such device '$dev'\n";
+}
+
 sub get_partition_dev {
     my ($dev, $partnum) = @_;
 
@@ -755,12 +792,6 @@ sub update_progress {
 }
 
 my $fssetup = {
-    ext3 => {
-       mkfs => 'mkfs.ext3 -F',
-       mkfs_root_opt => '',
-       mkfs_data_opt => '-m 0',
-       root_mountopt => 'errors=remount-ro',
-    },
     ext4 => {
        mkfs => 'mkfs.ext4 -F',
        mkfs_root_opt => '',
@@ -919,7 +950,6 @@ sub partition_bootable_disk {
     die "unknown partition type '$ptype'"
        if !($ptype eq '8E00' || $ptype eq '8300' || $ptype eq 'BF01');
 
-    syscmd("sgdisk -Z ${target_dev}");
     my $hdsize = hd_size($target_dev); # size in KB (1024 bytes)
 
     my $restricted_hdsize_mb = 0; # 0 ==> end of partition
@@ -934,6 +964,8 @@ sub partition_bootable_disk {
     my $hdgb = int($hdsize/(1024*1024));
     die "hardisk '$target_dev' too small (${hdgb}GB)\n" if $hdgb < 8;
 
+    syscmd("sgdisk -Z ${target_dev}");
+
     # 1 - BIOS boot partition (Grub Stage2): first free 1M
     # 2 - EFI ESP: next free 512M
     # 3 - OS/Data partition: rest, up to $maxhdsize in MB
@@ -957,11 +989,15 @@ sub partition_bootable_disk {
     syscmd($pcmd) == 0 ||
        die "unable to partition harddisk '${target_dev}'\n";
 
-    $pnum = 1;
-    $pcmd = ['sgdisk', '-a1', "-n$pnum:34:2047", "-t$pnum:EF02" , $target_dev];
+    my $blocksize = logical_blocksize($target_dev);
 
-    syscmd($pcmd) == 0 ||
-       die "unable to create bios_boot partition '${target_dev}'\n";
+    if ($blocksize != 4096) {
+       $pnum = 1;
+       $pcmd = ['sgdisk', '-a1', "-n$pnum:34:2047", "-t$pnum:EF02" , $target_dev];
+
+       syscmd($pcmd) == 0 ||
+           die "unable to create bios_boot partition '${target_dev}'\n";
+    }
 
     &$udevadm_trigger_block();
 
@@ -1151,11 +1187,34 @@ sub compute_swapsize {
     return $swapsize;
 }
 
-sub prepare_systemd_boot_esp {
+my sub chroot_chown {
+    my ($root, $path, %param) = @_;
+
+    my $recursive = $param{recursive} ? ' -R' : '';
+    my $user = $param{user};
+    die "can not chown without user parameter\n" if !defined($user);
+    my $group = $param{group} // $user;
+
+    syscmd("chroot $root /bin/chown $user:$group $recursive $path") == 0 ||
+       die "chroot: unable to change owner for '$path'\n";
+}
+
+my sub chroot_chmod {
+    my ($root, $path, %param) = @_;
+
+    my $recursive = $param{recursive} ? ' -R' : '';
+    my $mode = $param{mode};
+    die "can not chmod without mode parameter\n" if !defined($mode);
+
+    syscmd("chroot $root /bin/chmod $mode $recursive $path") == 0 ||
+       die "chroot: unable to change permission mode for '$path'\n";
+}
+
+sub prepare_proxmox_boot_esp {
     my ($espdev, $targetdir) = @_;
 
-    syscmd("chroot $targetdir pve-efiboot-tool init $espdev") == 0 ||
-       die "unable to init ESP and install systemd-boot loader on '$espdev'\n";
+    syscmd("chroot $targetdir proxmox-boot-tool init $espdev") == 0 ||
+       die "unable to init ESP and install proxmox-boot loader on '$espdev'\n";
 }
 
 sub prepare_grub_efi_boot_esp {
@@ -1271,32 +1330,41 @@ sub extract_data {
            my $disksize;
            foreach my $hd (@$devlist) {
                my $devname = @$hd[1];
+               my $logical_bsize = @$hd[4];
+
                &$clean_disk($devname);
                my ($size, $osdev, $efidev) =
                    partition_bootable_disk($devname, undef, '8300');
                $rootdev = $osdev if !defined($rootdev); # simply point to first disk
                my $by_id = find_stable_path("/dev/disk/by-id", $devname);
-               push @$bootdevinfo, { esp => $efidev, devname => $devname,
-                                     osdev => $osdev, by_id => $by_id };
+               push @$bootdevinfo, {
+                   esp => $efidev,
+                   devname => $devname,
+                   osdev => $osdev,
+                   by_id => $by_id,
+                   logical_bsize => $logical_bsize,
+               };
                push @$btrfs_partitions, $osdev;
                $disksize = $size;
            }
 
-           &$udevadm_trigger_block();
+           $udevadm_trigger_block->();
 
            btrfs_create($btrfs_partitions, $btrfs_mode);
 
        } elsif ($use_zfs) {
 
-           my ($devlist, $bootdevlist, $vdev) = get_zfs_raid_setup();
+           my ($devlist, $vdev) = get_zfs_raid_setup();
 
            foreach my $hd (@$devlist) {
-               &$clean_disk(@$hd[1]);
+               $clean_disk->(@$hd[1]);
            }
 
+           # install esp/boot part on all, we can only win!
            my $disksize;
-           foreach my $hd (@$bootdevlist) {
+           for my $hd (@$devlist) {
                my $devname = @$hd[1];
+               my $logical_bsize = @$hd[4];
 
                my ($size, $osdev, $efidev) =
                    partition_bootable_disk($devname, $config_options->{hdsize}, 'BF01');
@@ -1306,12 +1374,13 @@ sub extract_data {
                push @$bootdevinfo, {
                    esp => $efidev,
                    devname => $devname,
-                   osdev => $osdev
+                   osdev => $osdev,
+                   logical_bsize => $logical_bsize,
                };
                $disksize = $size;
            }
 
-           &$udevadm_trigger_block();
+           $udevadm_trigger_block->();
 
            foreach my $di (@$bootdevinfo) {
                my $devname = $di->{devname};
@@ -1322,6 +1391,13 @@ sub extract_data {
                $vdev =~ s/ $devname/ $osdev/;
            }
 
+           foreach my $hd (@$devlist) {
+               my $devname = @$hd[1];
+               my $by_id = find_stable_path ("/dev/disk/by-id", $devname);
+
+               $vdev =~ s/ $devname/ $by_id/ if $by_id;
+           }
+
            zfs_create_rpool($vdev);
 
        } else {
@@ -1330,6 +1406,8 @@ sub extract_data {
 
            &$clean_disk($target_hd);
 
+           my $logical_bsize = logical_blocksize($target_hd);
+
            my ($os_size, $osdev, $efidev);
            ($os_size, $osdev, $efidev) =
                partition_bootable_disk($target_hd, $config_options->{hdsize}, '8E00');
@@ -1337,8 +1415,13 @@ sub extract_data {
            &$udevadm_trigger_block();
 
            my $by_id = find_stable_path ("/dev/disk/by-id", $target_hd);
-           push @$bootdevinfo, { esp => $efidev, devname => $target_hd,
-                                 osdev => $osdev, by_id => $by_id };
+           push @$bootdevinfo, {
+               esp => $efidev,
+               devname => $target_hd,
+               osdev => $osdev,
+               by_id => $by_id,
+               logical_bsize => $logical_bsize,
+           };
 
            my $swap_size = compute_swapsize($os_size);
            ($rootdev, $swapfile, $datadev) =
@@ -1364,7 +1447,9 @@ sub extract_data {
 
        foreach my $di (@$bootdevinfo) {
            next if !$di->{esp};
-           syscmd("mkfs.vfat -F32 $di->{esp}") == 0 ||
+           # FIXME remove '-s1' once https://github.com/dosfstools/dosfstools/issues/111 is fixed
+           my $vfat_extra_opts = ($di->{logical_bsize} == 4096) ? '-s1' : '';
+           syscmd("mkfs.vfat $vfat_extra_opts -F32 $di->{esp}") == 0 ||
                die "unable to initialize EFI ESP on device $di->{esp}\n";
        }
 
@@ -1480,8 +1565,7 @@ sub extract_data {
 
            $ifaces .=
                "\nauto vmbr0\niface vmbr0 $ntype static\n" .
-               "\taddress $ipaddress\n" .
-               "\tnetmask $netmask\n" .
+               "\taddress $cidr\n" .
                "\tgateway $gateway\n" .
                "\tbridge_ports $ethdev\n" .
                "\tbridge_stp off\n" .
@@ -1489,8 +1573,7 @@ sub extract_data {
        } else {
            $ifaces .= "auto $ethdev\n" .
                "iface $ethdev $ntype static\n" .
-               "\taddress $ipaddress\n" .
-               "\tnetmask $netmask\n" .
+               "\taddress $cidr\n" .
                "\tgateway $gateway\n";
        }
 
@@ -1641,6 +1724,8 @@ _EOD
        syscmd("chroot $targetdir /usr/sbin/postfix check");
        # cleanup mail queue
        syscmd("chroot $targetdir /usr/sbin/postsuper -d ALL");
+       # create /etc/aliases.db (/etc/aliases is shipped in the base squashfs)
+       syscmd("chroot $targetdir /usr/bin/newaliases");
 
        # enable NTP (timedatectl set-ntp true  does not work without DBUS)
        syscmd("chroot $targetdir /bin/systemctl enable systemd-timesyncd.service");
@@ -1688,9 +1773,7 @@ _EOD
            syscmd("sed -i -e 's/^GRUB_CMDLINE_LINUX=.*/GRUB_CMDLINE_LINUX=\"root=ZFS=$zfspoolname\\/ROOT\\/$zfsrootvolname boot=zfs\"/' $targetdir/etc/default/grub") == 0 ||
                die "unable to update /etc/default/grub\n";
 
-           if ($boot_type eq 'efi') {
-               write_config("root=ZFS=$zfspoolname/ROOT/$zfsrootvolname boot=zfs", "$targetdir/etc/kernel/cmdline");
-           }
+           write_config("root=ZFS=$zfspoolname/ROOT/$zfsrootvolname boot=zfs", "$targetdir/etc/kernel/cmdline");
 
        }
 
@@ -1717,19 +1800,26 @@ _EOD
                syscmd("chroot $targetdir /usr/sbin/update-initramfs -c -k $kapi") == 0 ||
                    die "unable to install initramfs\n";
 
+               my $native_4k_disk_bootable = 0;
+               foreach my $di (@$bootdevinfo) {
+                   $native_4k_disk_bootable |= ($di->{logical_bsize} == 4096);
+               }
+
                foreach my $di (@$bootdevinfo) {
                    my $dev = $di->{devname};
-                   eval {
-                       syscmd("chroot $targetdir /usr/sbin/grub-install --target i386-pc --no-floppy --bootloader-id='proxmox' $dev") == 0 ||
-                               die "unable to install the i386-pc boot loader on '$dev'\n";
-                   };
-                   push @$bootloader_err_list, $@ if $@;
+                   if ($use_zfs) {
+                       prepare_proxmox_boot_esp($di->{esp}, $targetdir);
+                   } else {
+                       if (!$native_4k_disk_bootable) {
+                           eval {
+                               syscmd("chroot $targetdir /usr/sbin/grub-install --target i386-pc --no-floppy --bootloader-id='proxmox' $dev") == 0 ||
+                                       die "unable to install the i386-pc boot loader on '$dev'\n";
+                           };
+                           push @$bootloader_err_list, $@ if $@;
+                       }
 
                    eval {
                        if (my $esp = $di->{esp}) {
-                           if ($use_zfs) {
-                               prepare_systemd_boot_esp($esp, $targetdir);
-                           } else {
                                prepare_grub_efi_boot_esp($dev, $esp, $targetdir);
                            }
                        }
@@ -1780,8 +1870,7 @@ _EOD
                         "$tmpdir/datacenter.cfg");
 
            # save admin email
-           write_config("user:root\@pam:1:0:::${mailto}::\n",
-                        "$tmpdir/user.cfg");
+           write_config("user:root\@pam:1:0:::${mailto}::\n", "$tmpdir/user.cfg");
 
            # write storage.cfg
            my $storage_cfg_fn = "$tmpdir/storage.cfg";
@@ -1798,6 +1887,17 @@ _EOD
            run_command("chroot $targetdir /usr/bin/create_pmxcfs_db /tmp/pve /var/lib/pve-cluster/config.db");
 
            syscmd("rm -rf $tmpdir");
+       } elsif ($setup->{product} eq 'pbs') {
+           my $base_cfg_path = "/etc/proxmox-backup";
+           mkdir "$targetdir/$base_cfg_path";
+
+           chroot_chown($targetdir, $base_cfg_path, user => 'backup', recursive => 1);
+           chroot_chmod($targetdir, $base_cfg_path, mode => '0700');
+
+           my $user_cfg_fn = "$base_cfg_path/user.cfg";
+           write_config("user: root\@pam\n\temail ${mailto}\n", "$targetdir/$user_cfg_fn");
+           chroot_chown($targetdir, $user_cfg_fn, user => 'root', group => 'backup');
+           chroot_chmod($targetdir, $user_cfg_fn, mode => '0640');
        }
     };
 
@@ -1878,9 +1978,15 @@ sub display_html {
 
     $filename = $steps[$step_number]->{html} if !$filename;
 
-    my $path = "${proxmox_libdir}/html/$filename";
-
-    my $url = "file://$path";
+    my $htmldir = "${proxmox_libdir}/html";
+    my $path;
+    if (-f "$htmldir/$setup->{product}/$filename") {
+       $path = "$htmldir/$setup->{product}/$filename";
+    } elsif (-f "$htmldir/common/$filename") {
+       $path = "$htmldir/common/$filename";
+    } else {
+       # FIXME: die now already?
+    }
 
     my $data = file_get_contents($path);
 
@@ -1895,10 +2001,19 @@ sub display_html {
        $data =~ s/__LICENSE_TITLE__/$title/;
     } elsif ($filename eq 'success.htm') {
        my $addr = $ipversion == 6 ? "[${ipaddress}]" : "$ipaddress";
-       $data =~ s/\@IPADDR\@/$addr/;
+       $data =~ s/__IPADDR__/$addr/g;
+       $data =~ s/__PORT__/$setup->{port}/g;
+
+       my $autoreboot_msg = $config_options->{autoreboot}
+           ? "Automatic reboot scheduled in $autoreboot_seconds seconds."
+           : '';
+       $data =~ s/__AUTOREBOOT_MSG__/$autoreboot_msg/;
     }
+    $data =~ s/__FULL_PRODUCT_NAME__/$setup->{fullname}/g;
 
-    $htmlview->load_html($data, $url);
+    # HACK: always set base-path to common path, all resources are there.
+    # NOTE: we could also use an overlayfs with lower=common upper=$product & work=/run/$tmp
+    $htmlview->load_html($data,  "file://$htmldir/common/");
 
     $last_display_change = time();
 }
@@ -1933,6 +2048,7 @@ sub create_main_window {
     $window = Gtk3::Window->new();
     $window->set_default_size(1024, 768);
     $window->set_has_resize_grip(0);
+    $window->fullscreen() if !$opt_testmode;
     $window->set_decorated(0) if !$opt_testmode;
 
     my $vbox = Gtk3::VBox->new(0, 0);
@@ -2043,19 +2159,46 @@ sub check_number {
 sub create_text_input {
     my ($default, $text) = @_;
 
-    my $hbox = Gtk3::HBox->new(0, 0);
+    my $hbox = Gtk3::Box->new('horizontal', 0);
 
     my $label = Gtk3::Label->new($text);
     $label->set_size_request(150, -1);
     $label->set_alignment(1, 0.5);
     $hbox->pack_start($label, 0, 0, 10);
     my $e1 = Gtk3::Entry->new();
-    $e1->set_width_chars(30);
+    $e1->set_width_chars(35);
     $hbox->pack_start($e1, 0, 0, 0);
     $e1->set_text($default);
 
     return ($hbox, $e1);
 }
+sub create_cidr_inputs {
+    my ($default_ip, $default_mask) = @_;
+
+    my $hbox = Gtk3::Box->new('horizontal', 0);
+
+    my $label = Gtk3::Label->new('IP Address (CIDR)');
+    $label->set_size_request(150, -1);
+    $label->set_alignment(1, 0.5);
+    $hbox->pack_start($label, 0, 0, 10);
+
+    my $ip_el = Gtk3::Entry->new();
+    $ip_el->set_width_chars(28);
+    $hbox->pack_start($ip_el, 0, 0, 0);
+    $ip_el->set_text($default_ip);
+
+    $label = Gtk3::Label->new('/');
+    $label->set_size_request(10, -1);
+    $label->set_alignment(0.5, 0.5);
+    $hbox->pack_start($label, 0, 0, 2);
+
+    my $cidr_el = Gtk3::Entry->new();
+    $cidr_el->set_width_chars(3);
+    $hbox->pack_start($cidr_el, 0, 0, 0);
+    $cidr_el->set_text($default_mask);
+
+    return ($hbox, $ip_el, $cidr_el);
+}
 
 sub get_ip_config {
 
@@ -2097,6 +2240,7 @@ sub get_ip_config {
            $default = $index if !$default;
 
            $ifaces->{"$index"}->{"$family"} = {
+               prefix => $prefix,
                mask => $mask,
                addr => $ip,
            };
@@ -2145,22 +2289,18 @@ sub create_ipconf_view {
     cleanup_view();
     display_html();
 
-    my $vbox =  Gtk3::VBox->new(0, 0);
-    $inbox->pack_start($vbox, 1, 0, 0);
-    my $hbox =  Gtk3::HBox->new(0, 0);
-    $vbox->pack_start($hbox, 0, 0, 10);
-    my $vbox2 =  Gtk3::VBox->new(0, 0);
-    $hbox->add($vbox2);
+    my $vcontainer = Gtk3::Box->new('vertical', 0);
+    $inbox->pack_start($vcontainer, 1, 0, 0);
+    my $hcontainer =  Gtk3::Box->new('horizontal', 0);
+    $vcontainer->pack_start($hcontainer, 0, 0, 10);
+    my $vbox =  Gtk3::Box->new('vertical', 0);
+    $hcontainer->add($vbox);
 
     my $ipaddr_text = $config->{ipaddress} // "192.168.100.2";
-    my $ipbox;
-    ($ipbox, $ipconf_entry_addr) =
-       create_text_input($ipaddr_text, 'IP Address:');
-
-    my $netmask_text = $config->{netmask} // "255.255.255.0";
-    my $maskbox;
-    ($maskbox, $ipconf_entry_mask) =
-       create_text_input($netmask_text, 'Netmask:');
+    my $netmask_text = $config->{netmask} // "24";
+    my $cidr_box;
+    ($cidr_box, $ipconf_entry_addr, $ipconf_entry_mask) =
+       create_cidr_inputs($ipaddr_text, $netmask_text);
 
     my $device_cb = Gtk3::ComboBoxText->new();
     $device_cb->set_active(0);
@@ -2185,8 +2325,8 @@ sub create_ipconf_view {
        $config->{mngmt_nic} = $iface->{name};
        $ipconf_entry_addr->set_text($iface->{inet}->{addr} || $iface->{inet6}->{addr})
            if $iface->{inet}->{addr} || $iface->{inet6}->{addr};
-       $ipconf_entry_mask->set_text($iface->{inet}->{mask} || $iface->{inet6}->{mask})
-           if $iface->{inet}->{mask} || $iface->{inet6}->{mask};
+       $ipconf_entry_mask->set_text($iface->{inet}->{prefix} || $iface->{inet6}->{prefix})
+           if $iface->{inet}->{prefix} || $iface->{inet6}->{prefix};
     };
 
     my $i = 0;
@@ -2216,17 +2356,14 @@ sub create_ipconf_view {
     $devicebox->pack_start($label, 0, 0, 10);
     $devicebox->pack_start($device_cb, 0, 0, 0);
 
-    $vbox2->pack_start($devicebox, 0, 0, 2);
+    $vbox->pack_start($devicebox, 0, 0, 2);
 
     my $hn = $config->{fqdn} //  "$setup->{product}." . ($ipconf->{domain} // "example.invalid");
 
-    my ($hostbox, $hostentry) =
-       create_text_input($hn, 'Hostname (FQDN):');
-    $vbox2->pack_start($hostbox, 0, 0, 2);
-
-    $vbox2->pack_start($ipbox, 0, 0, 2);
+    my ($hostbox, $hostentry) = create_text_input($hn, 'Hostname (FQDN):');
+    $vbox->pack_start($hostbox, 0, 0, 2);
 
-    $vbox2->pack_start($maskbox, 0, 0, 2);
+    $vbox->pack_start($cidr_box, 0, 0, 2);
 
     $gateway = $config->{gateway} // $ipconf->{gateway} || '192.168.100.1';
 
@@ -2234,7 +2371,7 @@ sub create_ipconf_view {
     ($gwbox, $ipconf_entry_gw) =
        create_text_input($gateway, 'Gateway:');
 
-    $vbox2->pack_start($gwbox, 0, 0, 2);
+    $vbox->pack_start($gwbox, 0, 0, 2);
 
     $dnsserver = $config->{dnsserver} // $ipconf->{dnsserver} || $gateway;
 
@@ -2242,7 +2379,7 @@ sub create_ipconf_view {
     ($dnsbox, $ipconf_entry_dns) =
        create_text_input($dnsserver, 'DNS Server:');
 
-    $vbox2->pack_start($dnsbox, 0, 0, 0);
+    $vbox->pack_start($dnsbox, 0, 0, 0);
 
     $inbox->show_all;
     set_next(undef, sub {
@@ -2296,15 +2433,19 @@ sub create_ipconf_view {
        $text = $ipconf_entry_mask->get_text();
        $text =~ s/^\s+//;
        $text =~ s/\s+$//;
-       if (($ipversion == 6) && ($text =~ m/^(\d+)$/) && ($1 >= 8) && ($1 <= 126)) {
+       if ($ipversion == 6 && ($text =~ m/^(\d+)$/) && $1 >= 8 && $1 <= 126) {
            $netmask = $text;
-       } elsif (($ipversion == 4) && defined($ipv4_mask_hash->{$text})) {
+       } elsif ($ipversion == 4 && ($text =~ m/^(\d+)$/) && $1 >= 8 && $1 <= 32) {
            $netmask = $text;
+       } elsif ($ipversion == 4 && defined($ipv4_mask_hash->{$text})) {
+           # costs nothing to handle 255.x.y.z style masks, so continue to allow it
+           $netmask = $ipv4_mask_hash->{$text};
        } else {
            display_message("Netmask is not valid.");
            $ipconf_entry_mask->grab_focus();
            return;
        }
+       $cidr = "$ipaddress/$netmask";
        $config->{netmask} = $netmask;
 
        $text = $ipconf_entry_gw->get_text();
@@ -2348,8 +2489,21 @@ sub create_ack_view {
 
     cleanup_view();
 
-    my $ack_template = "${proxmox_libdir}/html/ack_template.htm";
-    my $ack_html = "${proxmox_libdir}/html/$steps[$step_number]->{html}";
+    my $vbox =  Gtk3::VBox->new(0, 0);
+    $inbox->pack_start($vbox, 1, 0, 0);
+    #my $hbox =  Gtk3::HBox->new(0, 0);
+    #$vbox->pack_start($hbox, 0, 0, 10);
+
+    my $reboot_checkbox = Gtk3::CheckButton->new('Automatically reboot after successful installation');
+    $reboot_checkbox->set_active(1);
+    $reboot_checkbox->signal_connect ("toggled" => sub {
+       my $cb = shift;
+       $config_options->{autoreboot} = $cb->get_active();
+    });
+    $vbox->pack_start($reboot_checkbox, 0, 0, 2);
+
+    my $ack_template = "${proxmox_libdir}/html/common/ack_template.htm";
+    my $ack_html = "${proxmox_libdir}/html/$setup->{product}/$steps[$step_number]->{html}";
     my $html_data = file_get_contents($ack_template);
 
     my %config_values = (
@@ -2362,6 +2516,7 @@ sub create_ack_view {
        __interface__ => $ipconf->{ifaces}->{$ipconf->{selected}}->{name},
        __hostname__ => $hostname,
        __ip__ => $ipaddress,
+       __cidr__ => $cidr,
        __netmask__ => $netmask,
        __gateway__ => $gateway,
        __dnsserver__ => $dnsserver,
@@ -2375,6 +2530,8 @@ sub create_ack_view {
 
     display_html();
 
+    $inbox->show_all;
+
     set_next(undef, sub {
        $step_number++;
        create_extract_view();
@@ -2390,9 +2547,14 @@ sub get_device_desc {
        my $text = "$devname (";
        if ($size >= 1024) {
            $size = int($size/1024); # size in GB
-           $text .= "${size}GB";
+           if ($size >= 1024) {
+               $size = int($size/1024); # size in GB
+               $text .= "${size}TiB";
+           } else {
+               $text .= "${size}GiB";
+           }
        } else {
-           $text .= "${size}MB";
+           $text .= "${size}MiB";
        }
 
        $text .= ", $model" if $model;
@@ -2403,6 +2565,8 @@ sub get_device_desc {
     }
 }
 
+my $last_layout;
+my $country_layout;
 sub update_layout {
     my ($cb, $kmap) = @_;
 
@@ -2416,7 +2580,14 @@ sub update_layout {
        $i++;
     }
 
-    $cb->set_active($ind || $def || 0);
+    my $val = $ind || $def || 0;
+
+    if (!defined($kmap)) {
+       $last_layout //= $val;
+    } elsif (!defined($country_layout) || $country_layout != $val) {
+       $last_layout = $country_layout = $val;
+    }
+    $cb->set_active($last_layout);
 }
 
 my $lastzonecb;
@@ -2493,7 +2664,7 @@ sub create_password_view {
     $hbox2->pack_start($pwe2, 0, 0, 0);
 
     my $hbox3 = Gtk3::HBox->new(0, 0);
-    $label = Gtk3::Label->new("E-Mail");
+    $label = Gtk3::Label->new("Email");
     $label->set_size_request(150, -1);
     $label->set_alignment(1, 0.5);
     $hbox3->pack_start($label, 0, 0, 10);
@@ -2529,15 +2700,15 @@ sub create_password_view {
        }
 
        my $t3 = $eme->get_text;
-       if ($t3 !~ m/^\S+\@\S+\.\S+$/) {
-           display_message("E-Mail does not look like a valid address" .
+       if ($t3 !~ m/^[\w\+\-\~]+(\.[\w\+\-\~]+)*@[a-zA-Z0-9\-]+(\.[a-zA-Z0-9\-]+)*$/) {
+           display_message("Email does not look like a valid address" .
                             " (user\@domain.tld)");
            $eme->grab_focus();
            return;
        }
 
        if ($t3 eq 'mail@example.invalid') {
-           display_message("Please enter a valid E-Mail address");
+           display_message("Please enter a valid Email address");
            $eme->grab_focus();
            return;
        }
@@ -2553,6 +2724,7 @@ sub create_password_view {
 
 }
 
+my $installer_kmap;
 sub create_country_view {
 
     cleanup_view();
@@ -2598,11 +2770,31 @@ sub create_country_view {
 
     $kmapcb->signal_connect ('changed' => sub {
        my $sel = $kmapcb->get_active_text();
+       $last_layout = $kmapcb->get_active();
        if (my $kmap = $cmap->{kmaphash}->{$sel}) {
            my $xkmap = $cmap->{kmap}->{$kmap}->{x11};
            my $xvar = $cmap->{kmap}->{$kmap}->{x11var};
-           syscmd ("setxkbmap $xkmap $xvar") if !$opt_testmode;
            $keymap = $kmap;
+
+           return if (defined($installer_kmap) && $installer_kmap eq $kmap);
+
+           $installer_kmap = $keymap;
+
+           if (! $opt_testmode) {
+               syscmd ("setxkbmap $xkmap $xvar");
+
+               my $kbd_config = qq{
+                   XKBLAYOUT="$xkmap"
+                   XKBVARIANT="$xvar"
+                   BACKSPACE="guess"
+               };
+               $kbd_config =~ s/^\s+//gm;
+
+               run_in_background( sub {
+                   write_config($kbd_config, '/etc/default/keyboard');
+                   system("setupcon");
+               });
+           }
        }
     });
 
@@ -2721,8 +2913,8 @@ my $create_basic_grid = sub {
     $grid->set_row_spacing(10);
     $grid->set_hexpand(1);
 
-    $grid->set_margin_start(5);
-    $grid->set_margin_end(5);
+    $grid->set_margin_start(10);
+    $grid->set_margin_end(20);
     $grid->set_margin_top(5);
     $grid->set_margin_bottom(5);
 
@@ -2750,14 +2942,15 @@ my $create_label_widget_grid = sub {
 };
 
 my $create_raid_disk_grid = sub {
+    my $hd_count = scalar(@$hds);
     my $disk_labeled_widgets = [];
-    for (my $i = 0; $i < @$hds; $i++) {
+    for (my $i = 0; $i < $hd_count; $i++) {
        my $disk_selector = Gtk3::ComboBoxText->new();
        $disk_selector->append_text("-- do not use --");
        $disk_selector->set_active(0);
        $disk_selector->set_visible(1);
        foreach my $hd (@$hds) {
-           my ($disk, $devname, $size, $model) = @$hd;
+           my ($disk, $devname, $size, $model, $logical_bsize) = @$hd;
            $disk_selector->append_text(get_device_desc ($devname, $size, $model));
            $disk_selector->{pve_disk_id} = $i;
            $disk_selector->signal_connect (changed => sub {
@@ -2786,14 +2979,38 @@ my $create_raid_disk_grid = sub {
        push @$disk_labeled_widgets, "Harddisk $i", $disk_selector;
     }
 
+    my $clear_all_button = Gtk3::Button->new('_Deselect All');
+    if ($hd_count > 3) {
+       $clear_all_button->signal_connect('clicked', sub {
+           my $is_widget = 0;
+           for my $disk_selector (@$disk_labeled_widgets) {
+               $disk_selector->set_active(0) if $is_widget;
+               $is_widget ^= 1;
+           }
+       });
+       $clear_all_button->set_visible(1);
+    }
+
     my $scrolled_window = Gtk3::ScrolledWindow->new();
     $scrolled_window->set_hexpand(1);
-    $scrolled_window->set_propagate_natural_height(1) if @$hds > 4;
-    $scrolled_window->add(&$create_label_widget_grid($disk_labeled_widgets));
+    $scrolled_window->set_propagate_natural_height(1) if $hd_count > 4;
+
+    my $diskgrid = $create_label_widget_grid->($disk_labeled_widgets);
+
+    $scrolled_window->add($diskgrid);
     $scrolled_window->set_policy('never', 'automatic');
+    $scrolled_window->set_visible(1);
+    $scrolled_window->set_min_content_height(190);
+
+    my $vbox = Gtk3::Box->new('vertical', 0);
+    $vbox->pack_start($scrolled_window, 1, 1, 10);
+
+    my $hbox = Gtk3::Box->new('horizontal', 0);
+    $hbox->pack_end($clear_all_button, 0, 0, 20);
+    $hbox->set_visible(1);
+    $vbox->pack_end($hbox, 0, 0, 0);
 
-    return $scrolled_window;
-#    &$create_label_widget_grid($disk_labeled_widgets)
+    return $vbox;
 };
 
 # shared between different ui parts (e.g., ZFS and "normal" single disk FS)
@@ -2821,7 +3038,7 @@ my $get_hdsize_spinbtn = sub {
 
 my $create_raid_advanced_grid = sub {
     my $labeled_widgets = [];
-    my $spinbutton_ashift = Gtk3::SpinButton->new_with_range(9,13,1);
+    my $spinbutton_ashift = Gtk3::SpinButton->new_with_range(9, 13, 1);
     $spinbutton_ashift->set_tooltip_text("zpool ashift property (pool sector size, default 2^12)");
     $spinbutton_ashift->signal_connect ("value-changed" => sub {
        my $w = shift;
@@ -2874,7 +3091,7 @@ my $create_raid_advanced_grid = sub {
     push @$labeled_widgets, "copies", $spinbutton_copies;
 
     push @$labeled_widgets, "hdsize", $get_hdsize_spinbtn->();
-    return &$create_label_widget_grid($labeled_widgets);;
+    return $create_label_widget_grid->($labeled_widgets);;
 };
 
 sub create_hdoption_view {
@@ -2888,37 +3105,39 @@ sub create_hdoption_view {
     my $contarea = $dialog->get_content_area();
 
     my $hbox2 =  Gtk3::Box->new('horizontal', 0);
-    $contarea->pack_start($hbox2, 1, 1, 10);
+    $contarea->pack_start($hbox2, 1, 1, 5);
 
     my $grid =  Gtk3::Grid->new();
     $grid->set_column_spacing(10);
     $grid->set_row_spacing(10);
 
-    $hbox2->pack_start($grid, 1, 0, 10);
+    $hbox2->pack_start($grid, 1, 0, 5);
 
     my $row = 0;
 
     # Filesystem type
-
     my $label0 = Gtk3::Label->new("Filesystem");
     $label0->set_alignment (1, 0.5);
     $grid->attach($label0, 0, $row, 1, 1);
 
     my $fstypecb = Gtk3::ComboBoxText->new();
-
-    my $fstype = ['ext3', 'ext4', 'xfs',
-                 'zfs (RAID0)', 'zfs (RAID1)',
-                 'zfs (RAID10)', 'zfs (RAIDZ-1)',
-                 'zfs (RAIDZ-2)', 'zfs (RAIDZ-3)'];
-
+    my $fstype = [
+       'ext4',
+       'xfs',
+       'zfs (RAID0)',
+       'zfs (RAID1)',
+       'zfs (RAID10)',
+       'zfs (RAIDZ-1)',
+       'zfs (RAIDZ-2)',
+       'zfs (RAIDZ-3)',
+    ];
     push @$fstype, 'btrfs (RAID0)', 'btrfs (RAID1)', 'btrfs (RAID10)'
        if $setup->{enable_btrfs};
 
     my $tcount = 0;
     foreach my $tmp (@$fstype) {
        $fstypecb->append_text($tmp);
-       $fstypecb->set_active ($tcount)
-           if $config_options->{filesys} eq $tmp;
+       $fstypecb->set_active ($tcount) if $config_options->{filesys} eq $tmp;
        $tcount++;
     }
 
@@ -2933,7 +3152,9 @@ sub create_hdoption_view {
     $grid->attach($sep, 0, $row, 2, 1);
     $row++;
 
-    my $hw_raid_note = Gtk3::Label->new("Note: ZFS is not compatible with disks backed by a hardware RAID controller. For details see the reference documentation.");
+    my $hw_raid_note = Gtk3::Label->new(
+        "Note: ZFS is not compatible with hardware RAID controllers, for details see the documentation."
+    );
     $hw_raid_note->set_line_wrap(1);
     $hw_raid_note->set_max_width_chars(30);
     $hw_raid_note->set_visible(0);
@@ -3092,7 +3313,7 @@ my $get_raid_devlist = sub {
     my $devlist = [];
     for (my $i = 0; $i < @$hds; $i++) {
        if (my $hd = $config_options->{"disksel$i"}) {
-           my ($disk, $devname, $size, $model) = @$hd;
+           my ($disk, $devname, $size, $model, $logical_bsize) = @$hd;
            die "device '$devname' is used more than once\n"
                if $dev_name_hash->{$devname};
            $dev_name_hash->{$devname} = $hd;
@@ -3110,8 +3331,13 @@ sub zfs_mirror_size_check {
        if abs($expected - $actual) > $expected / 10;
 }
 
-sub get_zfs_raid_setup {
+sub legacy_bios_4k_check {
+    my ($lbs) = @_;
+    die "Booting from 4kn drive in legacy BIOS mode is not supported.\n"
+       if (($boot_type ne 'efi') && ($lbs == 4096));
+}
 
+sub get_zfs_raid_setup {
     my $filesys = $config_options->{filesys};
 
     my $devlist = &$get_raid_devlist();
@@ -3119,12 +3345,10 @@ sub get_zfs_raid_setup {
     my $diskcount = scalar(@$devlist);
     die "$filesys needs at least one device\n" if $diskcount < 1;
 
-    my $bootdevlist = [];
-
     my $cmd= '';
     if ($filesys eq 'zfs (RAID0)') {
-       push @$bootdevlist, @$devlist[0];
        foreach my $hd (@$devlist) {
+           legacy_bios_4k_check(@$hd[4]);
            $cmd .= " @$hd[1]";
        }
     } elsif ($filesys eq 'zfs (RAID1)') {
@@ -3132,21 +3356,21 @@ sub get_zfs_raid_setup {
        $cmd .= ' mirror ';
        my $hd = @$devlist[0];
        my $expected_size = @$hd[2]; # all disks need approximately same size
-       foreach $hd (@$devlist) {
+       foreach my $hd (@$devlist) {
            zfs_mirror_size_check($expected_size, @$hd[2]);
+           legacy_bios_4k_check(@$hd[4]);
            $cmd .= " @$hd[1]";
-           push @$bootdevlist, $hd;
        }
     } elsif ($filesys eq 'zfs (RAID10)') {
        die "zfs (RAID10) needs at least 4 device\n" if $diskcount < 4;
        die "zfs (RAID10) needs an even number of devices\n" if $diskcount & 1;
 
-       push @$bootdevlist, @$devlist[0], @$devlist[1];
-
        for (my $i = 0; $i < $diskcount; $i+=2) {
            my $hd1 = @$devlist[$i];
            my $hd2 = @$devlist[$i+1];
            zfs_mirror_size_check(@$hd1[2], @$hd2[2]); # pairs need approximately same size
+           legacy_bios_4k_check(@$hd1[4]);
+           legacy_bios_4k_check(@$hd2[4]);
            $cmd .= ' mirror ' . @$hd1[1] . ' ' . @$hd2[1];
        }
 
@@ -3157,16 +3381,16 @@ sub get_zfs_raid_setup {
        my $hd = @$devlist[0];
        my $expected_size = @$hd[2]; # all disks need approximately same size
        $cmd .= " raidz$level";
-       foreach $hd (@$devlist) {
+       foreach my $hd (@$devlist) {
            zfs_mirror_size_check($expected_size, @$hd[2]);
+           legacy_bios_4k_check(@$hd[4]);
            $cmd .= " @$hd[1]";
-           push @$bootdevlist, $hd;
        }
     } else {
        die "unknown zfs mode '$filesys'\n";
     }
 
-    return ($devlist, $bootdevlist, $cmd);
+    return ($devlist, $cmd);
 }
 
 sub get_btrfs_raid_setup {
@@ -3211,7 +3435,7 @@ sub create_hdsel_view {
     my $hbox =  Gtk3::HBox->new(0, 0);
     $vbox->pack_start($hbox, 0, 0, 10);
 
-    my ($disk, $devname, $size, $model) = @{@$hds[0]};
+    my ($disk, $devname, $size, $model, $logical_bsize) = @{@$hds[0]};
     $target_hd = $devname if !defined($target_hd);
 
     $target_hd_label = Gtk3::Label->new("Target Harddisk: ");
@@ -3220,7 +3444,7 @@ sub create_hdsel_view {
     $target_hd_combo = Gtk3::ComboBoxText->new();
 
     foreach my $hd (@$hds) {
-       ($disk, $devname, $size, $model) = @$hd;
+       ($disk, $devname, $size, $model, $logical_bsize) = @$hd;
        $target_hd_combo->append_text (get_device_desc($devname, $size, $model));
     }
 
@@ -3266,6 +3490,11 @@ sub create_hdsel_view {
            }
            $config_options->{target_hds} = [ map { $_->[1] } @$devlist ];
        } else {
+           eval { legacy_bios_4k_check(logical_blocksize($target_hd)) };
+           if (my $err = $@) {
+               display_message("Warning: $err\n");
+               return;
+           }
            $config_options->{target_hds} = [ $target_hd ];
        }
 
@@ -3320,6 +3549,17 @@ sub create_extract_view {
     } else {
        cleanup_view();
        display_html("success.htm");
+
+        if ($config_options->{autoreboot}) {
+           Glib::Timeout->add(1000, sub {
+               if ($autoreboot_seconds > 0) {
+                   $autoreboot_seconds--;
+                   display_html("success.htm");
+               } else {
+                   exit(0);
+               }
+           });
+       }
     }
 }
 
@@ -3330,10 +3570,8 @@ sub create_intro_view {
     cleanup_view();
 
     if (int($total_memory) < 1024) {
-       my $fullname = $product_fullname->{$setup->{product}};
-
        display_error("Less than 1 GiB of usable memory detected, installation will probably fail.\n\n".
-           "See 'System Requirements' in the $fullname documentation.");
+           "See 'System Requirements' in the $setup->{fullname} documentation.");
     }
 
     if ($setup->{product} eq 'pve') {
@@ -3392,4 +3630,9 @@ create_intro_view () if !$initial_error;
 
 Gtk3->main;
 
+# reap left over zombie processes
+while ((my $child = waitpid(-1, POSIX::WNOHANG)) > 0) {
+    print "reaped child $child\n";
+}
+
 exit 0;