X-Git-Url: https://git.proxmox.com/?a=blobdiff_plain;f=proxinstall;h=65ae5a0948eeacbba407894f9752a4e347c7c354;hb=56207f2a241ddbc332992bb551b9d411c1126258;hp=8348c042f16989613626e2bf3d4089b258590f99;hpb=23c337f53ade857710e956430e2cce179f61f385;p=pve-installer.git diff --git a/proxinstall b/proxinstall index 8348c04..a1c3dc6 100755 --- a/proxinstall +++ b/proxinstall @@ -1,46 +1,198 @@ -#!/usr/bin/perl -w +#!/usr/bin/perl + +$ENV{DEBIAN_FRONTEND} = 'noninteractive'; +$ENV{LC_ALL} = 'C'; use strict; +use warnings; + use Getopt::Long; +use IPC::Open2; use IPC::Open3; use IO::File; -use IO::Dir; use IO::Select; use Cwd 'abs_path'; -use Gtk2 '-init'; -use Gtk2::Html2; -use Gtk2::Gdk::Keysyms; +use Gtk3 '-init'; +use Gtk3::WebKit; use Encode; +use String::ShellQuote; +use Data::Dumper; +use File::Basename; +use Time::HiRes; -my $release = '2.0'; +use ProxmoxInstallerSetup; -my $kapi = `uname -r`; -chomp $kapi; +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"; +} + if (!GetOptions ('testmode=s' => \$opt_testmode)) { die "usage error\n"; exit (-1); } -my $logfd = IO::File->new (">/tmp/install.log"); +my $zfstestpool = "test_rpool"; +my $zfspoolname = $opt_testmode ? $zfstestpool : 'rpool'; +my $zfsrootvolname = "$setup->{product}-1"; + +my $storage_cfg_zfs = <<__EOD__; +dir: local + path /var/lib/vz + content iso,vztmpl,backup + +zfspool: local-zfs + pool $zfspoolname/data + sparse + content images,rootdir +__EOD__ + +my $storage_cfg_btrfs = <<__EOD__; +dir: local + path /var/lib/vz + content iso,vztmpl,backup + disabled + +btrfs: local-btrfs + path /var/lib/pve/local-btrfs + content iso,vztmpl,backup,images,rootdir +__EOD__ + +my $storage_cfg_lvmthin = <<__EOD__; +dir: local + path /var/lib/vz + content iso,vztmpl,backup + +lvmthin: local-lvm + thinpool data + vgname pve + content rootdir,images +__EOD__ + + +sub file_read_firstline { + my ($filename) = @_; -my $proxmox_dir = $opt_testmode ? "." : "/var/lib/pve-installer"; + my $fh = IO::File->new ($filename, "r"); + return undef if !$fh; + my $res = <$fh>; + chomp $res if $res; + $fh->close; + return $res; +} -$ENV{DEBIAN_FRONTEND} = 'noninteractive'; -$ENV{LC_ALL} = 'C'; +my $logfd = IO::File->new (">/tmp/install.log"); -my ($window, $cmdbox, $inbox, $document, $htmlview); -my ($next, $next_fctn, $target_hd, $master_hd); +my $proxmox_libdir = $opt_testmode ? + Cwd::cwd() . "/testdir/var/lib/pve-installer" : "/var/lib/pve-installer"; +my $proxmox_cddir = $opt_testmode ? "../pve-cd-builder/tmp/data-gz/" : "/cdrom"; +my $proxmox_pkgdir = "${proxmox_cddir}/proxmox/packages/"; + +my $grub_plattform = "pc"; # pc, efi-amd64 or efi-ia32 + +$grub_plattform = "efi-amd64" if -d "/sys/firmware/efi"; + +my $IPV4OCTET = "(?:25[0-5]|(?:2[0-4]|1[0-9]|[1-9])?[0-9])"; +my $IPV4RE = "(?:(?:$IPV4OCTET\\.){3}$IPV4OCTET)"; +my $IPV6H16 = "(?:[0-9a-fA-F]{1,4})"; +my $IPV6LS32 = "(?:(?:$IPV4RE|$IPV6H16:$IPV6H16))"; + +my $IPV6RE = "(?:" . + "(?:(?:" . "(?:$IPV6H16:){6})$IPV6LS32)|" . + "(?:(?:" . "::(?:$IPV6H16:){5})$IPV6LS32)|" . + "(?:(?:(?:" . "$IPV6H16)?::(?:$IPV6H16:){4})$IPV6LS32)|" . + "(?:(?:(?:(?:$IPV6H16:){0,1}$IPV6H16)?::(?:$IPV6H16:){3})$IPV6LS32)|" . + "(?:(?:(?:(?:$IPV6H16:){0,2}$IPV6H16)?::(?:$IPV6H16:){2})$IPV6LS32)|" . + "(?:(?:(?:(?:$IPV6H16:){0,3}$IPV6H16)?::(?:$IPV6H16:){1})$IPV6LS32)|" . + "(?:(?:(?:(?:$IPV6H16:){0,4}$IPV6H16)?::" . ")$IPV6LS32)|" . + "(?:(?:(?:(?:$IPV6H16:){0,5}$IPV6H16)?::" . ")$IPV6H16)|" . + "(?:(?:(?:(?:$IPV6H16:){0,6}$IPV6H16)?::" . ")))"; + +my $IPRE = "(?:$IPV4RE|$IPV6RE)"; + + +my $ipv4_mask_hash = { + '128.0.0.0' => 1, + '192.0.0.0' => 2, + '224.0.0.0' => 3, + '240.0.0.0' => 4, + '248.0.0.0' => 5, + '252.0.0.0' => 6, + '254.0.0.0' => 7, + '255.0.0.0' => 8, + '255.128.0.0' => 9, + '255.192.0.0' => 10, + '255.224.0.0' => 11, + '255.240.0.0' => 12, + '255.248.0.0' => 13, + '255.252.0.0' => 14, + '255.254.0.0' => 15, + '255.255.0.0' => 16, + '255.255.128.0' => 17, + '255.255.192.0' => 18, + '255.255.224.0' => 19, + '255.255.240.0' => 20, + '255.255.248.0' => 21, + '255.255.252.0' => 22, + '255.255.254.0' => 23, + '255.255.255.0' => 24, + '255.255.255.128' => 25, + '255.255.255.192' => 26, + '255.255.255.224' => 27, + '255.255.255.240' => 28, + '255.255.255.248' => 29, + '255.255.255.252' => 30 +}; + +my $ipv4_reverse_mask = [ + '0.0.0.0', + '128.0.0.0', + '192.0.0.0', + '224.0.0.0', + '240.0.0.0', + '248.0.0.0', + '252.0.0.0', + '254.0.0.0', + '255.0.0.0', + '255.128.0.0', + '255.192.0.0', + '255.224.0.0', + '255.240.0.0', + '255.248.0.0', + '255.252.0.0', + '255.254.0.0', + '255.255.0.0', + '255.255.128.0', + '255.255.192.0', + '255.255.224.0', + '255.255.240.0', + '255.255.248.0', + '255.255.252.0', + '255.255.254.0', + '255.255.255.0', + '255.255.255.128', + '255.255.255.192', + '255.255.255.224', + '255.255.255.240', + '255.255.255.248', + '255.255.255.252', + '255.255.255.254', + '255.255.255.255', +]; + +my ($window, $cmdbox, $inbox, $htmlview); +my ($next, $next_fctn, $target_hd); my ($progress, $progress_status); -my ($ipaddress, $ip_1, $ip_2, $ip_3, $ip_4); -my ($netmask, $mask_1, $mask_2, $mask_3, $mask_4); -my ($gateway, $gw_1, $gw_2, $gw_3, $gw_4); -my ($dnsserver, $dns_1, $dns_2, $dns_3, $dns_4); +my ($ipversion, $ipaddress, $ipconf_entry_addr); +my ($netmask, $ipconf_entry_mask); +my ($gateway, $ipconf_entry_gw); +my ($dnsserver, $ipconf_entry_dns); my $hostname = 'proxmox'; my $domain = 'domain.tld'; -my $cmdline = `cat /proc/cmdline` || ''; +my $cmdline = file_read_firstline("/proc/cmdline"); my $ipconf; my $country; my $timezone = 'Europe/Vienna'; @@ -49,7 +201,35 @@ my $mailto; my $keymap = 'en-us'; my $cmap; -my $filesys = ($cmdline =~ m/\sext4(\s.*)$/) ? 'ext4' : 'ext3'; +# parse command line args + +my $config_options = {}; + +if ($cmdline =~ m/\s(ext3|ext4|xfs)(\s.*)?$/) { + $config_options->{filesys} = $1; +} else { + $config_options->{filesys} = 'ext4'; +} + +if ($cmdline =~ m/hdsize=(\d+(\.\d+)?)[\s\n]/i) { + $config_options->{hdsize} = $1; +} + +if ($cmdline =~ m/swapsize=(\d+(\.\d+)?)[\s\n]/i) { + $config_options->{swapsize} = $1; +} + +if ($cmdline =~ m/maxroot=(\d+(\.\d+)?)[\s\n]/i) { + $config_options->{maxroot} = $1; +} + +if ($cmdline =~ m/minfree=(\d+(\.\d+)?)[\s\n]/i) { + $config_options->{minfree} = $1; +} + +if ($cmdline =~ m/maxvz=(\d+(\.\d+)?)[\s\n]/i) { + $config_options->{maxvz} = $1; +} my $postfix_main_cf = <<_EOD; # See /usr/share/postfix/main.cf.dist for a commented, more complete version @@ -68,32 +248,68 @@ append_dot_mydomain = no alias_maps = hash:/etc/aliases alias_database = hash:/etc/aliases mydestination = \$myhostname, localhost.\$mydomain, localhost -relayhost = +relayhost = mynetworks = 127.0.0.0/8 inet_interfaces = loopback-only recipient_delimiter = + _EOD -sub syscmd { +sub shellquote { + my $str = shift; + + return String::ShellQuote::shell_quote($str); +} + +sub cmd2string { + my ($cmd) = @_; + + die "no arguments" if !$cmd; + + return $cmd if !ref($cmd); + + my @qa = (); + foreach my $arg (@$cmd) { push @qa, shellquote($arg); } + + return join (' ', @qa); +} + +sub syscmd { my ($cmd) = @_; return run_command ($cmd, undef, undef, 1); } - + sub run_command { my ($cmd, $func, $input, $noout) = @_; + my $cmdstr; + if (!ref($cmd)) { + $cmdstr = $cmd; + if ($cmd =~ m/|/) { + # see 'man bash' for option pipefail + $cmd = [ '/bin/bash', '-c', "set -o pipefail && $cmd" ]; + } else { + $cmd = [ $cmd ]; + } + } else { + $cmdstr = cmd2string($cmd); + } + my $cmdtxt; - if ($input && ($cmd !~ m/chpasswd/)) { - $cmdtxt = "# $cmd <flush(); + + if ($opt_testmode) { + print $cmdtxt; + STDOUT->flush(); + } + print $logfd $cmdtxt; my $reader = IO::File->new(); @@ -104,15 +320,15 @@ sub run_command { my $pid; eval { - $pid = open3 ($writer, $reader, $error, $cmd) || die $!; + $pid = open3 ($writer, $reader, $error, @$cmd) || die $!; }; my $err = $@; # catch exec errors if ($orig_pid != $$) { - POSIX::_exit (1); - kill ('KILL', $$); + POSIX::_exit (1); + kill ('KILL', $$); } die $err if $err; @@ -129,7 +345,7 @@ sub run_command { while ($select->count) { my @handles = $select->can_read (0.2); - Gtk2->main_iteration while Gtk2->events_pending; + Gtk3::main_iteration() while Gtk3::events_pending(); next if !scalar (@handles); # timeout @@ -166,10 +382,12 @@ sub run_command { return $? if $noout; # behave like standard system(); - my $ec = ($? >> 8); - - if ($ec) { - die "command '$cmd' failed with exit code $ec"; + if ($? == -1) { + die "command '$cmdstr' failed to execute\n"; + } elsif (my $sig = ($? & 127)) { + die "command '$cmdstr' failed - got signal $sig\n"; + } elsif (my $exitcode = ($? >> 8)) { + die "command '$cmdstr' failed with exit code $exitcode"; } return $ostream; @@ -178,8 +396,9 @@ sub run_command { sub detect_country { print "trying to detect country...\n"; - open (TMP, "traceroute -N 1 -q 1 -n www.debian.org|"); - + my $cpid = open2(\*TMP, undef, "traceroute -N 1 -q 1 -n 8.8.8.8"); + return undef if !$cpid; + my $country; my $previous_alarm = alarm (10); @@ -193,6 +412,7 @@ sub detect_country { print $logfd "DC GEOIP: $geoip"; if ($geoip =~ m/GeoIP Country Edition:\s*([A-Z]+),/) { $country = lc ($1); + print $logfd "DC FOUND: $country\n"; last; } } @@ -224,7 +444,7 @@ sub get_memtotal { while (my $line = ) { if ($line =~ m/^MemTotal:\s+(\d+)\s*kB/i) { $res = int ($1 / 1024); - } + } } close (MEMINFO); @@ -248,19 +468,11 @@ sub link_points_to { sub find_stable_path { my ($stabledir, $bdev) = @_; - my $dh = IO::Dir->new ($stabledir); - if ($dh) { - while (defined(my $tmp = $dh->read)) { - my $path = "$stabledir/$tmp"; - if (link_points_to ($path, $bdev)) { - return wantarray ? ($path, $tmp) : $path; - } + foreach my $path (<$stabledir/*>) { + if (link_points_to ($path, $bdev)) { + return wantarray ? ($path, basename($path)) : $path; } - - $dh->close; } - - return wantarray ? () : undef; } sub find_dev_by_uuid { @@ -277,6 +489,7 @@ sub hd_list { if ($opt_testmode) { push @$res, [-1, $opt_testmode, int((-s $opt_testmode)/512), "TESTDISK"]; + return $res; } my $count = 0; @@ -289,9 +502,9 @@ sub hd_list { next if $bd =~ m|^/sys/block/fd\d+$|; next if $bd =~ m|^/sys/block/sr\d+$|; - my $dev = `cat '$bd/dev'`; + my $dev = file_read_firstline("$bd/dev"); chomp $dev; - + next if !$dev; my $info = `udevadm info --path $bd --query all`; @@ -303,14 +516,14 @@ sub hd_list { my ($name) = $info =~ m/^N: (\S+)$/m; - if ($name) { + if ($name) { my $real_name = "/dev/$name"; - my $size = `cat '$bd/size'`; + my $size = file_read_firstline("$bd/size"); chomp $size; $size = undef if !($size && $size =~ m/^\d+$/); - my $model = `cat '$bd/device/model'`; + my $model = file_read_firstline("$bd/device/model") || ''; $model =~ s/^\s+//; $model =~ s/\s+$//; if (length ($model) > 30) { @@ -326,8 +539,7 @@ sub hd_list { } sub read_cmap { - my $countryfn = $opt_testmode ? "/usr/share/pve-manager/country.dat" : - "/proxmox/country.dat"; + my $countryfn = "${proxmox_libdir}/country.dat"; open (TMP, "<$countryfn") || die "unable to open '$countryfn' - $!\n"; my $line; my $country = {}; @@ -392,74 +604,52 @@ sub hd_size { foreach my $hd (@$hds) { my ($disk, $devname, $size, $model) = @$hd; + # size is always in 512B "sectors"! convert to KB return int($size/2) if $devname eq $dev; } - die "no such device '$dev'"; -} - -# find the master boot disk - return the first found scsi/ide disk -sub find_master { - my ($target_hd) = @_; - - foreach my $hd (sort { ${$a}[1] cmp ${$b}[1] } @$hds) { - my ($disk, $devname) = @$hd; - next if $disk < 0; - - if ($target_hd =~ m|^/dev/sd|) { - return $devname if $devname =~ m|^/dev/sd|; - } elsif ($target_hd =~ m|^/dev/hd|) { - return $devname if $devname =~ m|^/dev/hd|; - } elsif ($target_hd =~ m|^/dev/i2o/|) { - return $devname if $devname =~ m|^/dev/i2o/|; - } elsif ($target_hd =~ m|^/dev/ataraid/|) { - return $devname if $devname =~ m|^/dev/ataraid/|; - } elsif ($target_hd =~ m|^/dev/ida/|) { - return $devname if $devname =~ m|^/dev/ida/|; - } elsif ($target_hd =~ m|^/dev/cciss/|) { - return $devname if $devname =~ m|^/dev/cciss/|; - } elsif ($target_hd =~ m|^/dev/rd/|) { - return $devname if $devname =~ m|^/dev/rd/|; - } - } - - return $target_hd; + die "no such device '$dev'\n"; } sub get_partition_dev { - my ($target_hd, $partnum) = @_; - - if ($target_hd =~ m|^/dev/[hxsev]d[a-z]$|) { - return "${target_hd}$partnum"; - } elsif ($target_hd =~ m|^/dev/[^/]+/c\d+d\d+$|) { - return "${target_hd}p$partnum"; - } elsif ($target_hd =~ m|^/dev/[^/]+/d\d+$|) { - return "${target_hd}p$partnum"; - } elsif ($target_hd =~ m|^/dev/[^/]+/hd[a-z]$|) { - return "${target_hd}$partnum"; + my ($dev, $partnum) = @_; + + if ($dev =~ m|^/dev/[hxsev]d[a-z]$|) { + return "${dev}$partnum"; + } elsif ($dev =~ m|^/dev/[^/]+/c\d+d\d+$|) { + return "${dev}p$partnum"; + } elsif ($dev =~ m|^/dev/[^/]+/d\d+$|) { + return "${dev}p$partnum"; + } elsif ($dev =~ m|^/dev/[^/]+/hd[a-z]$|) { + return "${dev}$partnum"; + } elsif ($dev =~ m|^/dev/nvme\d+n\d+$|) { + return "${dev}p$partnum"; } else { - die "unable to get device for partition $partnum on device $target_hd\n"; + die "unable to get device for partition $partnum on device $dev\n"; } } -sub get_boot_part { - my ($target_hd, $gpt) = @_; +sub file_get_contents { + my ($filename, $max) = @_; - return get_partition_dev ($target_hd, $gpt ? 2 : 1); -} + my $fh = IO::File->new($filename, "r") || + die "can't open '$filename' - $!\n"; + + local $/; # slurp mode -sub get_lvm_part { - my ($target_hd, $gpt) = @_; + my $content = <$fh>; - return get_partition_dev ($target_hd, $gpt ? 3 : 2); + close $fh; + + return $content; } sub write_config { my ($text, $filename) = @_; my $fd = IO::File->new (">$filename") || - die "unable to open file '$filename' - $!"; + die "unable to open file '$filename' - $!\n"; print $fd $text; $fd->close(); } @@ -474,22 +664,44 @@ sub update_progress { $progress->set_text (sprintf ("%d%%", int ($res*100))); $progress_status->set_text ($text) if defined ($text); - Gtk2->main_iteration while Gtk2->events_pending; + Gtk3::main_iteration() while Gtk3::events_pending(); } -sub create_filesystem { - my ($dev, $name, $type, $start, $end, $fs, $fe, $opts) = @_; +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 => '', + mkfs_data_opt => '-m 0', + root_mountopt => 'errors=remount-ro', + }, + xfs => { + mkfs => 'mkfs.xfs -f', + mkfs_root_opt => '', + mkfs_data_opt => '', + root_mountopt => '', + }, +}; - $opts = '' if !$opts; +sub create_filesystem { + my ($dev, $name, $type, $start, $end, $fs, $fe) = @_; my $range = $end - $start; my $rs = $start + $range*$fs; my $re = $start + $range*$fe; my $max = 0; + my $fsdata = $fssetup->{$type} || die "internal error - unknown file system '$type'"; + my $opts = $name eq 'root' ? $fsdata->{mkfs_root_opt} : $fsdata->{mkfs_data_opt}; + update_progress (0, $rs, $re, "creating $name filesystem"); - run_command ("mkfs.$type $opts -F $dev", sub { + run_command ("$fsdata->{mkfs} $opts $dev", sub { my $line = shift; if ($line =~ m/Writing inode tables:\s+(\d+)\/(\d+)/) { @@ -500,7 +712,7 @@ sub create_filesystem { update_progress (0.95, $rs, $re); } elsif ($line =~ m/Writing superblocks and filesystem.*done/) { update_progress (1, $rs, $re); - } + } }); } @@ -509,8 +721,8 @@ sub debconfig_set { my $cfgfile = "/tmp/debconf.txt"; write_config ($dcdata, "$targetdir/$cfgfile"); - syscmd ("chroot $targetdir debconf-set-selections $cfgfile"); - unlink "$targetdir/$cfgfile"; + syscmd ("chroot $targetdir debconf-set-selections $cfgfile"); + unlink "$targetdir/$cfgfile"; } sub diversion_add { @@ -521,7 +733,7 @@ sub diversion_add { die "unable to exec dpkg-divert\n"; syscmd ("ln -sf ${new_cmd} $targetdir/$cmd") == 0 || - die "unable to link diversion to ${new_cmd}\n"; + die "unable to link diversion to ${new_cmd}\n"; } sub diversion_remove { @@ -529,186 +741,543 @@ sub diversion_remove { syscmd ("mv $targetdir/${cmd}.distrib $targetdir/${cmd};") == 0 || die "unable to remove $cmd diversion\n"; - + syscmd ("chroot $targetdir dpkg-divert --remove $cmd") == 0 || die "unable to remove $cmd diversion\n"; } +sub btrfs_create { + my ($partitions, $mode) = @_; + + die "unknown btrfs mode '$mode'" + if !($mode eq 'single' || $mode eq 'raid0' || + $mode eq 'raid1' || $mode eq 'raid10'); + + my $cmd = ['mkfs.btrfs', '-f']; + + push @$cmd, '-d', $mode, '-m', $mode; + + push @$cmd, @$partitions; + + syscmd($cmd); +} + +sub zfs_create_rpool { + my ($vdev) = @_; + + my $cmd = "zpool create -f -o cachefile=none"; + + $cmd .= " -o ashift=$config_options->{ashift}" + if defined($config_options->{ashift}); + + syscmd ("$cmd $zfspoolname $vdev") == 0 || + die "unable to create zfs root pool\n"; + + syscmd ("zfs create $zfspoolname/ROOT") == 0 || + die "unable to create zfs $zfspoolname/ROOT volume\n"; + + syscmd ("zfs create $zfspoolname/data") == 0 || + die "unable to create zfs $zfspoolname/data volume\n"; + + syscmd ("zfs create $zfspoolname/ROOT/$zfsrootvolname") == 0 || + die "unable to create zfs $zfspoolname/ROOT/$zfsrootvolname volume\n"; + + # disable atime during install + syscmd ("zfs set atime=off $zfspoolname") == 0 || + die "unable to set zfs properties\n"; + + my $value = $config_options->{compress}; + syscmd ("zfs set compression=$value $zfspoolname") + if defined($value) && $value ne 'off'; + + $value = $config_options->{checksum}; + syscmd ("zfs set checksum=$value $zfspoolname") + if defined($value) && $value ne 'on'; + + $value = $config_options->{copies}; + syscmd ("zfs set copies=$value $zfspoolname") + if defined($value) && $value != 1; +} + +sub zfs_create_swap { + my ($swapsize) = @_; + + my $cmd = "zfs create -V ${swapsize}K -b 4K"; + + $cmd .= " -o com.sun:auto-snapshot=false"; + + # copies for swap does not make sense + $cmd .= " -o copies=1"; + + # reduces memory pressure + $cmd .= " -o sync=always"; + + # cheapest compression to drop zero pages + $cmd .= " -o compression=zle"; + + # skip log devices + $cmd .= " -o logbias=throughput"; + # only cache metadata in RAM (caching swap content does not make sense) + $cmd .= " -o primarycache=metadata"; + # don't cache anything in L2ARC + $cmd .= " -o secondarycache=none"; + + $cmd .= " $zfspoolname/swap"; + syscmd ($cmd) == 0 || + die "unable to create zfs swap device\n"; + + return "/dev/zvol/$zfspoolname/swap"; +} + +my $udevadm_trigger_block = sub { + my ($nowait) = @_; + + sleep(1) if !$nowait; # give kernel time to reread part table + + # trigger udev to create /dev/disk/by-uuid + syscmd ("udevadm trigger --subsystem-match block"); + syscmd ("udevadm settle --timeout 10"); +}; + +my $clean_disk = sub { + my ($disk) = @_; + + my $partitions = `lsblk --output kname --noheadings --path --list $disk`; + foreach my $part (split "\n", $partitions) { + next if $part eq $disk; + next if $part !~ /^\Q$disk\E/; + eval { syscmd("pvremove -ff -y $part"); }; + eval { syscmd("dd if=/dev/zero of=$part bs=1M count=16"); }; + } +}; + +sub partition_bootable_disk { + my ($target_dev, $maxhdsize, $ptype) = @_; + + die "too dangerous" if $opt_testmode; + + die "unknown partition type '$ptype'" + if !($ptype eq '8E00' || $ptype eq '8300'); + + 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 + if ($maxhdsize && ($maxhdsize < $hdsize)) { + $hdsize = $maxhdsize; + $restricted_hdsize_mb = int($hdsize/1024) . 'M'; + } + + my $hdgb = int($hdsize/(1024*1024)); + die "hardisk '$target_dev' too small (${hdsize}GB)\n" if $hdgb < 8; + + # 1 - BIOS boot partition (Grub Stage2): first free 1M + # 2 - EFI ESP: next free 256M + # 3 - OS/Data partition: rest, up to $maxhdsize in MB + + my $grubbootdev = get_partition_dev($target_dev, 1); + my $efibootdev = get_partition_dev($target_dev, 2); + my $osdev = get_partition_dev ($target_dev, 3); + + my $pcmd = ['sgdisk']; + + my $pnum = 1; + push @$pcmd, "-n${pnum}:1M:+1M", "-t$pnum:EF02"; + + $pnum = 2; + push @$pcmd, "-n${pnum}:2M:+256M", "-t$pnum:EF00"; + + $pnum = 3; + push @$pcmd, "-n${pnum}:258M:${restricted_hdsize_mb}", "-t$pnum:$ptype"; + + push @$pcmd, $target_dev; + + my $os_size = $hdsize - 258*1024; # 256M + 1M + 1M alignment + + syscmd($pcmd) == 0 || + die "unable to partition harddisk '${target_dev}'\n"; + + &$udevadm_trigger_block(); + + foreach my $part ($efibootdev, $osdev) { + syscmd("dd if=/dev/zero of=$part bs=1M count=256") if -b $part; + } + + return ($os_size, $osdev, $efibootdev); +} + +# ZFS has this use_whole_disk concept, so we try to partition the same +# way as zfs does by default. There is room at start of disk to insert +# a grub boot partition. But adding a EFI ESP is not possible. +# +# Note: zfs people think this is just a waste of space an not +# required. Instead, you should put the ESP on another disk (log, +# ..). + +sub partition_bootable_zfs_disk { + my ($target_dev) = @_; + + die "too dangerous" if $opt_testmode; + + syscmd("sgdisk -Z ${target_dev}"); + my $hdsize = hd_size($target_dev); # size in blocks (1024 bytes) + + my $hdgb = int($hdsize/(1024*1024)); + die "hardisk '$target_dev' too small (${hdsize}GB)\n" if $hdgb < 8; + + # 1 - GRUB boot partition: 1M + # 2 - OS/Data partition + # 9 - ZFS reserved partition + + my $grubbootdev = get_partition_dev($target_dev, 1); + my $osdev = get_partition_dev ($target_dev, 2); + + my $pcmd = ['sgdisk', '-a1']; + + my $pnum = 1; + push @$pcmd, "-n$pnum:34:2047", "-t$pnum:EF02"; + + $pnum = 9; + push @$pcmd, "-n$pnum:-8M:0", "-t$pnum:BF07"; + + $pnum = 2; + push @$pcmd, "-n$pnum:2048:0", "-t$pnum:BF01", '-c', "$pnum:zfs"; + + push @$pcmd, $target_dev; + + my $os_size = $hdsize - 1024 - 1024*8; + + syscmd($pcmd) == 0 || + die "unable to partition harddisk '${target_dev}'\n"; + + &$udevadm_trigger_block(); + + syscmd("dd if=/dev/zero of=$osdev bs=1M count=16") if -b $osdev; + + return ($os_size, $osdev); +} + +sub create_lvm_volumes { + my ($lvmdev, $os_size, $swap_size) = @_; + + my $vgname = $setup->{product}; + + my $rootdev = "/dev/$vgname/root"; + my $datadev = "/dev/$vgname/data"; + my $swapfile = "/dev/$vgname/swap"; + + # we use --metadatasize 250k, which results in "pe_start = 512" + # so pe_start is aligned on a 128k boundary (advantage for SSDs) + syscmd ("/sbin/pvcreate --metadatasize 250k -y -ff $lvmdev") == 0 || + die "unable to initialize physical volume $lvmdev\n"; + syscmd ("/sbin/vgcreate $vgname $lvmdev") == 0 || + die "unable to create volume group '$vgname'\n"; + + my $hdgb = int($os_size/(1024*1024)); + my $space = (($hdgb > 128) ? 16 : ($hdgb/8))*1024*1024; + + my $maxroot; + if ($config_options->{maxroot}) { + $maxroot = $config_options->{maxroot}; + } else { + $maxroot = 96; + } + + my $rootsize = (($hdgb > ($maxroot*4)) ? $maxroot : $hdgb/4)*1024*1024; + + my $rest = $os_size - $swap_size - $rootsize; # in KB + + my $minfree; + if ($config_options->{minfree}) { + $minfree = (($config_options->{minfree}*1024*1024) >= $rest ) ? $space : + $config_options->{minfree}*1024*1024 ; + } else { + $minfree = $space; + } + + $rest = $rest - $minfree; + + if ($config_options->{maxvz}) { + $rest = (($config_options->{maxvz}*1024*1024) <= $rest) ? + $config_options->{maxvz}*1024*1024 : $rest; + } + + syscmd ("/sbin/lvcreate -L${swap_size}K -nswap $vgname") == 0 || + die "unable to create swap volume\n"; + + syscmd ("/sbin/lvcreate -L${rootsize}K -nroot $vgname") == 0 || + die "unable to create root volume\n"; + + syscmd ("/sbin/lvcreate -L${rest}K -ndata $vgname") == 0 || + die "unable to create data volume\n"; + + syscmd ("/sbin/lvconvert --yes --type thin-pool $vgname/data") == 0 || + die "unable to create data thin-pool\n"; + + syscmd ("/sbin/vgchange -a y $vgname") == 0 || + die "unable to activate volume group\n"; + + return ($rootdev, $datadev, $swapfile); +} + +sub compute_swapsize { + my ($hdsize) = @_; + + my $hdgb = int($hdsize/(1024*1024)); + + my $swapsize; + if ($config_options->{swapsize}) { + $swapsize = $config_options->{swapsize}*1024*1024; + } else { + my $ss = int ($total_memory / 1024); + $ss = 4 if $ss < 4; + $ss = ($hdgb/8) if $ss > ($hdgb/8); + $ss = 8 if $ss > 8; + $swapsize = $ss*1024*1024; + } + + return $swapsize; +} + + sub extract_data { - my ($tgzfile, $targetdir) = @_; + my ($basefile, $targetdir) = @_; die "target '$targetdir' does not exist\n" if ! -d $targetdir; - my $rootdev; - my $bootdev; + my $starttime = [Time::HiRes::gettimeofday]; + + my $bootdevinfo = []; + my $datadev; my $swapfile; + my $rootdev; + + my $use_zfs = 0; + my $use_btrfs = 0; + + my $filesys = $config_options->{filesys}; + + if ($filesys =~ m/zfs/) { + $target_hd = undef; # do not use this config + $use_zfs = 1; + $targetdir = "/$zfspoolname/ROOT/$zfsrootvolname"; + } elsif ($filesys =~ m/btrfs/) { + $target_hd = undef; # do not use this config + $use_btrfs = 1; + } + + if ($use_zfs) { + my $i; + for ($i = 5; $i > 0; $i--) { + syscmd("modprobe zfs"); + last if -c "/dev/zfs"; + sleep(1); + } - my $ptype = 'msdos'; + die "unable to load zfs kernel module\n" if !$i; + } eval { + my $maxper = 0.25; update_progress (0, 0, $maxper, "create partitions"); - if ( -b $target_hd) { - syscmd ("dd if=/dev/zero of=${target_hd} bs=512 count=256"); - my $hdsize = hd_size ($target_hd); # size in blocks (1024 bytes) + syscmd("vgchange -an") if !$opt_testmode; # deactivate all detected VGs - if ($hdsize >= 2*1024*1024*1024) { # MBR can only handle 2 TB - $ptype = 'gpt'; - } + if ($opt_testmode) { - my $bootsize_mb = 512; - my $bootsize = $bootsize_mb * 1024; - my $hdsize_mb = $hdsize/1024; + $rootdev = abs_path($opt_testmode); + syscmd("umount $rootdev"); - my $pcmd = "parted --align optimal ${target_hd}"; - $pcmd .= " unit MiB"; - $pcmd .= " mklabel $ptype"; + if ($use_btrfs) { - my $pnum = 1; + die "unsupported btrfs mode (for testing environment)\n" + if $filesys ne 'btrfs (RAID0)'; - if ($ptype eq 'gpt') { - $pcmd .= " mkpart primary 1 2"; - $pcmd .= " set $pnum bios_grub on"; - $pnum++; - }; + btrfs_create([$rootdev], 'single'); - $pcmd .= " mkpart primary ext2 $pnum ${bootsize_mb}"; - $pcmd .= " set $pnum boot on"; - $pnum++; + } elsif ($use_zfs) { + die "unsupported zfs mode (for testing environment)\n" + if $filesys ne 'zfs (RAID0)'; - $pcmd .= " mkpart primary ext2 ${bootsize_mb} ${hdsize_mb}"; - $pcmd .= " set $pnum lvm on"; + syscmd ("zpool destroy $zfstestpool"); - syscmd($pcmd) == 0 || - die "unable to partition harddisk '${target_hd}'\n"; + zfs_create_rpool($rootdev); + + } else { + + # nothing to do + } - sleep(1); # give kernel time to reread part table + } elsif ($use_btrfs) { + + my ($devlist, $btrfs_mode) = get_btrfs_raid_setup(); + my $btrfs_partitions = []; + my $disksize; + foreach my $hd (@$devlist) { + my $devname = @$hd[1]; + &$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 @$btrfs_partitions, $osdev; + $disksize = $size; + } - my $lvmdev = get_lvm_part($target_hd, $ptype eq 'gpt'); + &$udevadm_trigger_block(); - $rootdev = '/dev/pve/root'; - $datadev = '/dev/pve/data'; - $swapfile = '/dev/pve/swap'; + btrfs_create($btrfs_partitions, $btrfs_mode); - # we use --metadatasize 250k, which reseults in "pe_start = 512" - # so pe_start is aligned on a 128k boundary (advantage for SSDs) - syscmd ("/sbin/pvcreate --metadatasize 250k -y -ff $lvmdev") == 0 || - die "unable to initialize physical volume $lvmdev"; - syscmd ("/sbin/vgcreate pve $lvmdev") == 0 || - die "unable to create volume group"; + } elsif ($use_zfs) { - my $hdgb = int($hdsize/(1024*1024)); - die "hardisk too small (${hdgb}GB)" if $hdgb < 4; + my ($devlist, $bootdevlist, $vdev) = get_zfs_raid_setup(); - my $swapsize; - if ($cmdline =~ m/swapsize=(\d+)[\s\n]/i) { - $swapsize=$1*1024*1024; - } else { - my $ss = int ($total_memory / 1024); - $ss = 4 if $ss < 4; - $ss = ($hdgb/8) if $ss > ($hdgb/8); - $swapsize = $ss*1024*1024; + my $disksize; + foreach my $hd (@$devlist) { + &$clean_disk(@$hd[1]); + } + foreach my $hd (@$bootdevlist) { + my $devname = @$hd[1]; + my ($size, $osdev) = + partition_bootable_zfs_disk($devname); + zfs_mirror_size_check($disksize, $size) if $disksize; + push @$bootdevinfo, { devname => $devname, osdev => $osdev}; + $disksize = $size; } - my $space = (($hdgb > 128) ? 16 : ($hdgb/8))*1024*1024; + &$udevadm_trigger_block(); - my $maxroot; - if ($cmdline =~ m/maxroot=(\d+)[\s\n]/i) { - $maxroot = $1; - } else { - $maxroot = 96; + foreach my $di (@$bootdevinfo) { + my $devname = $di->{devname}; + $di->{by_id} = find_stable_path ("/dev/disk/by-id", $devname); + + # Note: using /dev/disk/by-id/ does not work for unknown reason, we get + # cannot create 'rpool': no such pool or dataset + #my $osdev = find_stable_path ("/dev/disk/by-id", $di->{osdev}) || $di->{osdev}; + + my $osdev = $di->{osdev}; + $vdev =~ s/ $devname/ $osdev/; } - my $rootsize = (($hdgb > ($maxroot*4)) ? $maxroot : $hdgb/4)*1024*1024; - my $rest = int($hdsize) - $bootsize - $swapsize - $space - $rootsize; # in KB - syscmd ("/sbin/lvcreate -L${swapsize}K -nswap pve") == 0 || - die "unable to create swap volume"; + zfs_create_rpool($vdev); + + my $swap_size = compute_swapsize($disksize); + $swapfile = zfs_create_swap($swap_size); + + } else { + + die "target '$target_hd' is not a valid block device\n" if ! -b $target_hd; - syscmd ("/sbin/lvcreate -L${rootsize}K -nroot pve") == 0 || - die "unable to create root volume"; + my $maxhdsize; + if ($config_options->{hdsize}) { + # max hdsize passed on cmdline (GB) + $maxhdsize = $config_options->{hdsize}*1024*1024; + } - syscmd ("/sbin/lvcreate -L${rest}K -ndata pve") == 0 || - die "unable to create data volume"; + &$clean_disk($target_hd); - syscmd ("/sbin/vgchange -a y pve") == 0 || - die "unable to activate volume group"; + my ($os_size, $osdev, $efidev); + ($os_size, $osdev, $efidev) = + partition_bootable_disk($target_hd, $maxhdsize, '8E00'); - } else { - $rootdev = $target_hd; - syscmd ("umount $rootdev"); + &$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 }; + + my $swap_size = compute_swapsize($os_size); + ($rootdev, $datadev, $swapfile) = + create_lvm_volumes($osdev, $os_size, $swap_size); + + # trigger udev to create /dev/disk/by-uuid + &$udevadm_trigger_block(1); + } + + if ($use_zfs) { + # to be fast during installation + syscmd ("zfs set sync=disabled $zfspoolname") == 0 || + die "unable to set zfs properties\n"; } update_progress (0.03, 0, $maxper, "create swap space"); if ($swapfile) { - syscmd ("mkswap $swapfile") == 0 || + syscmd ("mkswap -f $swapfile") == 0 || die "unable to create swap space\n"; } update_progress (0.05, 0, $maxper, "creating filesystems"); - if ( -b $target_hd) { - $bootdev = get_boot_part ($target_hd, $ptype eq 'gpt'); - create_filesystem ($bootdev, 'boot', $filesys, 0.05, $maxper, 0, 0.1); - create_filesystem ($rootdev, 'root', $filesys, 0.05, $maxper, 0.1, 0.5); - create_filesystem ($datadev, 'data', $filesys, 0.05, $maxper, 0.5, 1, '-m 0'); + foreach my $di (@$bootdevinfo) { + next if !$di->{esp}; + syscmd ("mkfs.vfat -F32 $di->{esp}") == 0 || + die "unable to initialize EFI ESP on device $di->{esp}\n"; + } + + if ($use_zfs) { + # do nothing + } elsif ($use_btrfs) { + # do nothing } else { create_filesystem ($rootdev, 'root', $filesys, 0.05, $maxper, 0, 1); } update_progress (1, 0.05, $maxper, "mounting target $rootdev"); - if ( -b $target_hd) { - - # trigger udev to create /dev/disk/by-uuid - syscmd ("udevadm trigger --subsystem-match block"); - syscmd ("udevadm settle --timeout 10"); + if ($use_zfs) { + # do nothing + } else { + my $mount_opts = 'noatime'; + $mount_opts .= ',nobarrier' + if $use_btrfs || $filesys =~ /^ext\d$/; - syscmd ("mount -n $rootdev -o noatime,barrier=0 $targetdir") == 0 || + syscmd("mount -n $rootdev -o $mount_opts $targetdir") == 0 || die "unable to mount $rootdev\n"; + } - mkdir "$targetdir/boot"; - syscmd ("mount -n $bootdev -o noatime,barrier=0 $targetdir/boot") == 0 || - die "unable to mount $bootdev\n"; + mkdir "$targetdir/boot"; + mkdir "$targetdir/boot/efi"; - mkdir "$targetdir/var"; - mkdir "$targetdir/var/lib"; + mkdir "$targetdir/var"; + mkdir "$targetdir/var/lib"; + + if ($setup->{product} eq 'pve') { mkdir "$targetdir/var/lib/vz"; - syscmd ("mount -n $datadev $targetdir/var/lib/vz") == 0 || - die "unable to mount $datadev\n"; + mkdir "$targetdir/var/lib/pve"; - } else { - syscmd ("mount $rootdev $targetdir -o loop,noatime,barrier=0") == 0 || - die "unable to mount $rootdev\n"; + if ($use_btrfs) { + syscmd("btrfs subvolume create $targetdir/var/lib/pve/local-btrfs") == 0 || + die "unable to create btrfs subvolume\n"; + } } display_html ("extract2-rulesystem.htm"); update_progress (1, 0.05, $maxper, "extracting base system"); - my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size) = stat ($tgzfile); - $ino || die "unable to open file '$tgzfile' - $!\n"; - - my $files; - if ($opt_testmode) { - $files = `cat /pve/$release/install/pve-base.cnt`; - } else { - $files = `cat /proxmox/pve-base.cnt`; - } + my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size) = stat ($basefile); + $ino || die "unable to open file '$basefile' - $!\n"; + + my $files = file_read_firstline("${proxmox_cddir}/proxmox/$setup->{product}-base.cnt") || + die "unable to read base file count\n"; my $per = 0; my $count = 0; - run_command ("tar xvf $tgzfile -C $targetdir", sub { + run_command ("unsquashfs -f -dest $targetdir -i $basefile", sub { my $line = shift; + return if $line !~ m/^$targetdir/; $count++; my $nper = int (($count *100)/$files); if ($nper != $per) { $per = $nper; - my $frac = $per > 100 ? 100 : $per/100; + my $frac = $per > 100 ? 1 : $per/100; update_progress ($frac, $maxper, 0.5); } }); @@ -725,9 +1294,9 @@ sub extract_data { # configure hosts - my $hosts = + my $hosts = "127.0.0.1 localhost.localdomain localhost\n" . - "$ipaddress $hostname.$domain $hostname pvelocalhost\n\n" . + "$ipaddress $hostname.$domain $hostname pvelocalhost\n\n" . "# The following lines are desirable for IPv6 capable hosts\n\n" . "::1 ip6-localhost ip6-loopback\n" . "fe00::0 ip6-localnet\n" . @@ -736,46 +1305,96 @@ sub extract_data { "ff02::2 ip6-allrouters\n" . "ff02::3 ip6-allhosts\n"; - write_config ($hosts, "$targetdir/etc/hosts"); + write_config ($hosts, "$targetdir/etc/hosts"); - write_config ("$hostname\n", "$targetdir/etc/hostname"); + write_config ("$hostname\n", "$targetdir/etc/hostname"); syscmd ("/bin/hostname $hostname") if !$opt_testmode; # configure interfaces - my $ifaces = - "auto lo\niface lo inet loopback\n\n" . - "auto vmbr0\niface vmbr0 inet static\n" . - "\taddress $ipaddress\n" . - "\tnetmask $netmask\n" . - "\tgateway $gateway\n" . - "\tbridge_ports eth0\n" . - "\tbridge_stp off\n" . - "\tbridge_fd 0\n"; + my $ifaces = "auto lo\niface lo inet loopback\n\n"; - write_config ($ifaces, "$targetdir/etc/network/interfaces"); + my $ntype = $ipversion == 4 ? 'inet' : 'inet6'; - # configure dns + my $ethdev = $ipconf->{ifaces}->{$ipconf->{selected}}->{name}; + + if ($setup->{bridged_network}) { + $ifaces .= "iface $ethdev $ntype manual\n"; - my $resolfconf = "search $domain\nnameserver $dnsserver\n"; - write_config ($resolfconf, "$targetdir/etc/resolv.conf"); + $ifaces .= + "\nauto vmbr0\niface vmbr0 $ntype static\n" . + "\taddress $ipaddress\n" . + "\tnetmask $netmask\n" . + "\tgateway $gateway\n" . + "\tbridge_ports $ethdev\n" . + "\tbridge_stp off\n" . + "\tbridge_fd 0\n"; + } else { + $ifaces .= "auto $ethdev\n" . + "iface $ethdev $ntype static\n" . + "\taddress $ipaddress\n" . + "\tnetmask $netmask\n" . + "\tgateway $gateway\n"; + } - # try to use UUID=XXX for boot device - my $boot_uuid = $bootdev; - if (my $uuid = find_dev_by_uuid ($bootdev)) { - $boot_uuid = "UUID=$uuid"; + foreach my $iface (sort keys %{$ipconf->{ifaces}}) { + my $name = $ipconf->{ifaces}->{$iface}->{name}; + next if $name eq $ethdev; + + $ifaces .= "\niface $name $ntype manual\n"; } + write_config ($ifaces, "$targetdir/etc/network/interfaces"); + + # configure dns + + my $resolvconf = "search $domain\nnameserver $dnsserver\n"; + write_config ($resolvconf, "$targetdir/etc/resolv.conf"); + # configure fstab - my $fstab = - "# \n" . - "$rootdev / $filesys errors=remount-ro 0 1\n"; + my $fstab = "# \n"; - $fstab .= "$datadev /var/lib/vz $filesys defaults 0 1\n" if $datadev; + if ($use_zfs) { + # do nothing + } elsif ($use_btrfs) { + my $fsuuid; + my $cmd = "blkid -u filesystem -t TYPE=btrfs -o export $rootdev"; + run_command($cmd, sub { + my $line = shift; + + if ($line =~ m/^UUID=([A-Fa-f0-9\-]+)$/) { + $fsuuid = $1; + } + }); + + die "unable to detect FS UUID" if !defined($fsuuid); + + $fstab .= "UUID=$fsuuid / btrfs defaults 0 1\n"; + } else { + my $root_mountopt = $fssetup->{$filesys}->{root_mountopt} || 'defaults'; + $fstab .= "$rootdev / $filesys ${root_mountopt} 0 1\n"; + } + + # mount /boot/efi + # Note: this is required by current grub, but really dangerous, because + # vfat does not have journaling, so it triggers manual fsck after each crash + # so we only mount /boot/efi if really required (efi systems). + if ($grub_plattform =~ m/^efi-/) { + if (scalar(@$bootdevinfo)) { + my $di = @$bootdevinfo[0]; # simply use first disk + if ($di->{esp}) { + my $efi_boot_uuid = $di->{esp}; + if (my $uuid = find_dev_by_uuid ($di->{esp})) { + $efi_boot_uuid = "UUID=$uuid"; + } + + $fstab .= "${efi_boot_uuid} /boot/efi vfat defaults 0 1\n"; + } + } + } - $fstab .= "${boot_uuid} /boot $filesys defaults 0 1\n" if $boot_uuid; $fstab .= "$swapfile none swap sw 0 0\n" if $swapfile; @@ -783,12 +1402,12 @@ sub extract_data { write_config ($fstab, "$targetdir/etc/fstab"); write_config ("", "$targetdir/etc/mtab"); - - syscmd ("cp ${proxmox_dir}/policy-disable-rc.d " . - "$targetdir/usr/sbin/policy-rc.d") == 0 || + + syscmd ("cp ${proxmox_libdir}/policy-disable-rc.d " . + "$targetdir/usr/sbin/policy-rc.d") == 0 || die "unable to copy policy-rc.d\n"; - syscmd ("cp ${proxmox_dir}/fake-start-stop-daemon " . - "$targetdir/sbin/") == 0 || + syscmd ("cp ${proxmox_libdir}/fake-start-stop-daemon " . + "$targetdir/sbin/") == 0 || die "unable to copy start-stop-daemon\n"; diversion_add ($targetdir, "/sbin/start-stop-daemon", "/sbin/fake-start-stop-daemon"); @@ -797,36 +1416,56 @@ sub extract_data { syscmd ("touch $targetdir/proxmox_install_mode"); + my $grub_install_devices_txt = ''; + foreach my $di (@$bootdevinfo) { + $grub_install_devices_txt .= ', ' if $grub_install_devices_txt; + $grub_install_devices_txt .= $di->{by_id} || $di->{devname}; + } + + # Note: keyboard-configuration/xbkb-keymap is used by console-setup + my $xkmap = $cmap->{kmap}->{$keymap}->{x11} // 'us'; + debconfig_set ($targetdir, <<_EOD); locales locales/default_environment_locale select en_US.UTF-8 locales locales/locales_to_be_generated select en_US.UTF-8 UTF-8 samba-common samba-common/dhcp boolean false samba-common samba-common/workgroup string WORKGROUP -postfix postfix/main_mailer_type select Local only -console-data console-data/keymap/policy select Don\'t touch keymap +postfix postfix/main_mailer_type select No configuration +keyboard-configuration keyboard-configuration/xkb-keymap select $xkmap +d-i debian-installer/locale select en_US.UTF-8 +grub-pc grub-pc/install_devices select $grub_install_devices_txt _EOD - my $pkgdir = $opt_testmode ? "packages" : "/proxmox/packages"; my $pkg_count = 0; - while (<$pkgdir/*.deb>) { $pkg_count++ }; + while (<${proxmox_pkgdir}/*.deb>) { $pkg_count++ }; + + # btrfs/dpkg is extremely slow without --force-unsafe-io + my $dpkg_opts = $use_btrfs ? "--force-unsafe-io" : ""; $count = 0; - while (<$pkgdir/*.deb>) { + while (<${proxmox_pkgdir}/*.deb>) { chomp; my $path = $_; - my ($deb) = $path =~ m/$pkgdir\/(.*\.deb)/; + my ($deb) = $path =~ m/${proxmox_pkgdir}\/(.*\.deb)/; +# if ($deb =~ m/^grub-efi-/ && $deb !~ m/^grub-${grub_plattform}/) { +# $count++; +# next; +# } update_progress ($count/$pkg_count, 0.5, 0.75, "extracting $deb"); print "extracting: $deb\n"; syscmd ("cp $path $targetdir/tmp/$deb") == 0 || die "installation of package $deb failed\n"; - syscmd ("chroot $targetdir dpkg --force-depends --no-triggers --unpack /tmp/$deb") == 0 || - die "installation of package $deb failed\n"; + syscmd ("chroot $targetdir dpkg $dpkg_opts --force-depends --no-triggers --unpack /tmp/$deb") == 0 || + die "installation of package $deb failed\n"; update_progress ((++$count)/$pkg_count, 0.5, 0.75); } display_html ("extract4-virus.htm"); - my $cmd = "chroot $targetdir dpkg --force-confold --configure -a"; + # needed for postfix postinst in case no other NIC is active + syscmd("chroot $targetdir ifup lo"); + + my $cmd = "chroot $targetdir dpkg $dpkg_opts --force-confold --configure -a"; $count = 0; run_command ($cmd, sub { my $line = shift; @@ -835,10 +1474,6 @@ _EOD "configuring $1"); } }); - - debconfig_set ($targetdir, <<_EOD); -postfix postfix/main_mailer_type select No configuration -_EOD unlink "$targetdir/etc/mailname"; $postfix_main_cf =~ s/__FQDN__/${hostname}.${domain}/; @@ -849,49 +1484,67 @@ _EOD # cleanup mail queue syscmd ("chroot $targetdir /usr/sbin/postsuper -d ALL"); - unlink "$targetdir/proxmox_install_mode"; + # enable NTP (timedatectl set-ntp true does not work without DBUS) + syscmd ("chroot $targetdir /bin/systemctl enable systemd-timesyncd.service"); - # disable bacula-fd - syscmd ("touch '$targetdir/etc/bacula/do_not_run'"); + unlink "$targetdir/proxmox_install_mode"; - # set timezone + # set timezone unlink ("$targetdir/etc/localtime"); symlink ("/usr/share/zoneinfo/$timezone", "$targetdir/etc/localtime"); write_config ("$timezone\n", "$targetdir/etc/timezone"); - # set console keymap - if (my $kmapfile = $cmap->{kmap}->{$keymap}->{console}) { - syscmd ("chroot $targetdir /usr/sbin/install-keymap '/usr/share/keymaps/i386/$kmapfile'"); - } - - # enable apache port redirect - syscmd ("chroot $targetdir a2ensite pve-redirect.conf"); - # set apt mirror if (my $mirror = $cmap->{country}->{$country}->{mirror}) { my $fn = "$targetdir/etc/apt/sources.list"; - syscmd ("sed -i 's/ftp\\.debian\\.org/$mirror/' '$fn'"); + syscmd ("sed -i 's/ftp\\.debian\\.org/$mirror/' '$fn'"); } # create extended_states for apt (avoid cron job warning if that # file does not exist) write_config ('', "$targetdir/var/lib/apt/extended_states"); - # save installer settings - my $ucc = uc ($country); - debconfig_set ($targetdir, <<_EOD); -pve-manager pve-manager/country string $ucc -_EOD + # allow ssh root login + syscmd ("sed -i 's/^PermitRootLogin.*/PermitRootLogin yes/' '$targetdir/etc/ssh/sshd_config'"); + + if ($setup->{product} eq 'pmg') { + # install initial clamav DB + my $srcdir = "${proxmox_cddir}/proxmox/clamav"; + foreach my $fn ("main.cvd", "bytecode.cvd", "daily.cvd", "safebrowsing.cvd") { + syscmd ("cp \"$srcdir/$fn\" \"$targetdir/var/lib/clamav\"") == 0 || + die "installation of clamav db file '$fn' failed\n"; + } + syscmd("chroot $targetdir /bin/chown clamav:clamav -R /var/lib/clamav") == 0 || + die "unable to set owner for clamav database files\n"; + } + + if ($setup->{product} eq 'pve') { + # save installer settings + my $ucc = uc ($country); + debconfig_set($targetdir, "pve-manager pve-manager/country string $ucc\n"); + } update_progress (0.8, 0.95, 1, "make system bootable"); - # update default grub settings - syscmd ("sed -e 's/^GRUB_DISTRIBUTOR.*/GRUB_DISTRIBUTOR=\"Proxmox Virtual Environment\"/' -e 's/#GRUB_DISABLE_LINUX_RECOVERY=.*/GRUB_DISABLE_LINUX_RECOVERY=\"true\"/' /etc/default/grub"); + if ($use_zfs) { + 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"; + + } diversion_remove ($targetdir, "/usr/sbin/update-grub"); diversion_remove ($targetdir, "/usr/sbin/update-initramfs"); - if (!$opt_testmode && -b $target_hd) { + my $kapi; + foreach my $fn (<$targetdir/lib/modules/*>) { + if ($fn =~ m!/(\d+\.\d+\.\d+-\d+-pve)$!) { + die "found multiple kernels\n" if defined($kapi); + $kapi = $1; + } + } + die "unable to detect kernel version\n" if !defined($kapi); + + if (!$opt_testmode) { unlink ("$targetdir/etc/mtab"); symlink ("/proc/mounts", "$targetdir/etc/mtab"); @@ -900,50 +1553,88 @@ _EOD syscmd ("chroot $targetdir /usr/sbin/update-initramfs -c -k $kapi") == 0 || die "unable to install initramfs\n"; - syscmd ("chroot $targetdir /usr/sbin/grub-install --no-floppy '(hd0)'") == 0 || - die "unable to install the boot loader\n"; + foreach my $di (@$bootdevinfo) { + my $dev = $di->{devname}; + 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"; + + if ($di->{esp}) { + syscmd ("mount -n $di->{esp} -t vfat $targetdir/boot/efi") == 0 || + die "unable to mount $di->{esp}\n"; + my $rc = syscmd ("chroot $targetdir /usr/sbin/grub-install --target x86_64-efi --no-floppy --bootloader-id='proxmox' $dev"); + if ($rc != 0) { + if (-d '/sys/firmware/efi') { + die "unable to install the EFI boot loader on '$dev'\n"; + } else { + warn "unable to install the EFI boot loader on '$dev', ignoring (not booted using UEFI)\n"; + } + } + # also install fallback boot file (OVMF does not boot without) + mkdir("$targetdir/boot/efi/EFI/BOOT"); + syscmd("cp $targetdir/boot/efi/EFI/proxmox/grubx64.efi $targetdir/boot/efi/EFI/BOOT/BOOTx64.EFI") == 0 || + die "unable to copy efi boot loader\n"; + + syscmd ("umount $targetdir/boot/efi") == 0 || + die "unable to umount $targetdir/boot/efi\n"; + } + } syscmd ("chroot $targetdir /usr/sbin/update-grub") == 0 || - die "unable to install the boot loader\n"; + die "unable to update boot loader config\n"; syscmd ("umount $targetdir/dev"); } + # cleanup - # cleanup - - # hack: remove dead.letter from sshd installation + # hack: remove dead.letter from sshd installation syscmd ("rm -rf $targetdir/dead.letter"); - unlink ("$targetdir/etc/mtab"); - syscmd ("touch $targetdir/etc/mtab"); - unlink "$targetdir/usr/sbin/policy-rc.d"; diversion_remove ($targetdir, "/sbin/start-stop-daemon"); # set root password - my $octets = encode("utf-8", $password); + my $octets = encode("utf-8", $password); run_command ("chroot $targetdir /usr/sbin/chpasswd", undef, "root:$octets\n"); - # create pmxcfs DB + if ($setup->{product} eq 'pmg') { + mkdir "/etc/pmg"; + # save admin email + write_config ("#Unix Superuser\nroot:1:0::root:${mailto}:::\n", + "/etc/pmg/user.conf"); - my $tmpdir = "$targetdir/tmp/pve"; - mkdir $tmpdir; + } elsif ($setup->{product} eq 'pve') { - # write vnc keymap to datacenter.cfg - my $vnckmap = $cmap->{kmap}->{$keymap}->{kvm} || 'en-us'; - write_config ("keyboard: $vnckmap\n", - "$tmpdir/datacenter.cfg"); + # create pmxcfs DB - # save admin email - write_config ("user:root\@pam:1:0:::${mailto}::\n", - "$tmpdir/user.cfg"); - - run_command("chroot $targetdir /usr/bin/create_pmxcfs_db /tmp/pve /var/lib/pve-cluster/config.db"); + my $tmpdir = "$targetdir/tmp/pve"; + mkdir $tmpdir; + + # write vnc keymap to datacenter.cfg + my $vnckmap = $cmap->{kmap}->{$keymap}->{kvm} || 'en-us'; + write_config ("keyboard: $vnckmap\n", + "$tmpdir/datacenter.cfg"); + + # save admin email + write_config ("user:root\@pam:1:0:::${mailto}::\n", + "$tmpdir/user.cfg"); + + # write storage.cfg + my $strorage_cfg_fn = "$tmpdir/storage.cfg"; + if ($use_zfs) { + write_config ($storage_cfg_zfs, $strorage_cfg_fn); + } elsif ($use_btrfs) { + write_config ($storage_cfg_btrfs, $strorage_cfg_fn); + } else { + write_config ($storage_cfg_lvmthin, $strorage_cfg_fn); + } + + run_command("chroot $targetdir /usr/bin/create_pmxcfs_db /tmp/pve /var/lib/pve-cluster/config.db"); - syscmd ("rm -rf $tmpdir"); + syscmd ("rm -rf $tmpdir"); + } }; my $err = $@; @@ -953,15 +1644,34 @@ _EOD print $err if $err; if ($opt_testmode) { - syscmd ("chroot $targetdir /usr/bin/dpkg-query -W --showformat='\${package}\n'> pve-final.pkglist"); + my $elapsed = Time::HiRes::tv_interval($starttime); + print "Elapsed extract time: $elapsed\n"; + + syscmd ("chroot $targetdir /usr/bin/dpkg-query -W --showformat='\${package}\n'> final.pkglist"); } - syscmd ("umount $targetdir/boot"); - syscmd ("umount $targetdir/var/lib/vz"); syscmd ("umount $targetdir/tmp"); syscmd ("umount $targetdir/proc"); syscmd ("umount $targetdir/sys"); - syscmd ("umount -d $targetdir"); + + if ($use_zfs) { + syscmd ("zfs umount -a") == 0 || + die "unable to unmount zfs\n"; + } else { + syscmd ("umount -d $targetdir"); + } + + if (!$err && $use_zfs) { + syscmd ("zfs set sync=standard $zfspoolname") == 0 || + die "unable to set zfs properties\n"; + + syscmd ("zfs set mountpoint=/ $zfspoolname/ROOT/$zfsrootvolname") == 0 || + die "zfs set mountpoint failed\n"; + + syscmd ("zpool set bootfs=$zfspoolname/ROOT/$zfsrootvolname $zfspoolname") == 0 || + die "zfs set bootfs failed\n"; + syscmd ("zpool export $zfspoolname"); + } die $err if $err; } @@ -969,19 +1679,20 @@ _EOD sub display_html { my ($filename) = @_; - $htmlview->set_document(undef); - $document->clear; - $htmlview->set_document($document); + my $path = "${proxmox_libdir}/html/$filename"; + + my $url = "file://$path"; - $document->open_stream ("text/html"); + my $data = file_get_contents($path); - my $fn = "${proxmox_dir}/html/$filename"; - open (HTML, $fn) || - die "unable to open file '$fn' - $!\n"; - while () { $document->write_stream ($_); } - close (HTML); + if ($filename eq 'license.htm') { + my $license = decode('utf8', file_get_contents("${proxmox_cddir}/EULA")); + my $title = "END USER LICENSE AGREEMENT (EULA)"; + $data =~ s/__LICENSE__/$license/; + $data =~ s/__LICENSE_TITLE__/$title/; + } - $document->close_stream; + $htmlview->load_html_string($data, $url); } sub set_next { @@ -990,90 +1701,62 @@ sub set_next { $next_fctn = $fctn; $text = "_Next" if !$text; $next->set_label ($text); - - $next->grab_focus (); -} - -sub url_requested { - my ($doc, $url, $stream) = @_; - - $stream->set_cancel_func (sub {}); # hack: avoid warning - my $path = "${proxmox_dir}/html/$url"; - - if (-f $path) { - open (HTMLTMP, $path) || - die "unable to open file '$path' - $! "; - my $buf; - while (my $i = read (HTMLTMP, $buf, 4096)) { - $stream->write ($buf); - Gtk2->main_iteration while Gtk2->events_pending; - } - close (HTMLTMP); - } - - #$stream->close(); # hack: dont close - avoid crash + $next->grab_focus (); } sub create_main_window { - $window = Gtk2::Window->new (); + $window = Gtk3::Window->new (); $window->set_default_size (1024, 768); + $window->set_has_resize_grip(0); $window->set_decorated (0) if !$opt_testmode; - my $vbox = Gtk2::VBox->new (0, 0); + my $vbox = Gtk3::VBox->new (0, 0); - my $image = Gtk2::Image->new_from_file ("${proxmox_dir}/proxlogo.xpm"); + my $image = Gtk3::Image->new_from_file ("${proxmox_libdir}/proxlogo.png"); $vbox->pack_start ($image, 0, 0, 0); - my $hbox = Gtk2::HBox->new (0, 0); + my $hbox = Gtk3::HBox->new (0, 0); $vbox->pack_start ($hbox, 1, 1, 0); - my $f1 = Gtk2::Frame->new (); - $f1->set_shadow_type ('in'); - $hbox->pack_start ($f1, 1, 1, 0); + # my $f1 = Gtk3::Frame->new ('test'); + # $f1->set_shadow_type ('none'); + # $hbox->pack_start ($f1, 1, 1, 0); - my $sep1 = Gtk2::HSeparator->new; + my $sep1 = Gtk3::HSeparator->new; $vbox->pack_start ($sep1, 0, 0, 0); - $cmdbox = Gtk2::HBox->new (); + $cmdbox = Gtk3::HBox->new (); $vbox->pack_start ($cmdbox, 0, 0, 10); - $next = Gtk2::Button->new ('_Next'); + $next = Gtk3::Button->new ('_Next'); $next->signal_connect (clicked => sub { &$next_fctn (); }); $cmdbox->pack_end ($next, 0, 0, 10); - my $abort = Gtk2::Button->new ('_Abort'); - $abort->can_focus (0); + my $abort = Gtk3::Button->new ('_Abort'); + $abort->set_can_focus (0); $cmdbox->pack_start ($abort, 0, 0, 10); $abort->signal_connect (clicked => sub { exit (-1); }); - my $vbox2 = Gtk2::VBox->new (0, 0); - $f1->add ($vbox2); - - $htmlview = new Gtk2::Html2::View; - # hack: create a separate style - else Gtk2::Html2 modifies - # main window style - my $rcs1 = Gtk2::RcStyle->new; - $htmlview->modify_style ($rcs1); - - $document = new Gtk2::Html2::Document; - $document->signal_connect (request_url => \&url_requested); - - $document->clear; - $htmlview->set_document ($document); + my $vbox2 = Gtk3::VBox->new (0, 0); + $hbox->add ($vbox2); - my $hbox2 = Gtk2::HBox->new (0, 0); - $hbox2->pack_start ($htmlview, 1, 1, 0); + $htmlview = Gtk3::WebKit::WebView->new(); + my $scrolls = Gtk3::ScrolledWindow->new(); + $scrolls->add($htmlview); + + my $hbox2 = Gtk3::HBox->new (0, 0); + $hbox2->pack_start ($scrolls, 1, 1, 0); $vbox2->pack_start ($hbox2, 1, 1, 0); - my $vbox3 = Gtk2::VBox->new (0, 0); + my $vbox3 = Gtk3::VBox->new (0, 0); $vbox2->pack_start ($vbox3, 0, 0, 0); - my $sep2 = Gtk2::HSeparator->new; + my $sep2 = Gtk3::HSeparator->new; $vbox3->pack_start ($sep2, 0, 0, 0); - $inbox = Gtk2::HBox->new (0, 0); + $inbox = Gtk3::HBox->new (0, 0); $vbox3->pack_start ($inbox, 0, 0, 0); $window->add ($vbox); @@ -1082,60 +1765,60 @@ sub create_main_window { $window->realize (); } -sub cleanup_view { - my $list = $inbox->get_children; - foreach my $c ($list) { - next if !defined ($c); - $inbox->remove ($c); - } +sub cleanup_view { + $inbox->foreach(sub { + my $child = shift; + $inbox->remove ($child); + }); } -sub check_num { - my ($entry, $event) = @_; +# fixme: newer GTK3 has special properties to handle numbers with Entry +# only allow floating point numbers with Gtk3::Entry - my $val = $event->keyval; +sub check_float { + my ($entry, $event) = @_; - if ($val == ord '.') { - $entry->parent->child_focus ('right'); - return 1; - } + return check_number($entry, $event, 1); +} - if ($val == $Gtk2::Gdk::Keysyms{ISO_Left_Tab} || - $val == $Gtk2::Gdk::Keysyms{Shift_L} || - $val == $Gtk2::Gdk::Keysyms{Tab} || - $val == $Gtk2::Gdk::Keysyms{BackSpace} || - $val == $Gtk2::Gdk::Keysyms{Delete} || - ($val >= ord '0' && $val <= ord '9') || - ($val >= $Gtk2::Gdk::Keysyms{KP_0} && - $val <= $Gtk2::Gdk::Keysyms{KP_9})) { - return undef; - } +sub check_int { + my ($entry, $event) = @_; - return 1; + return check_number($entry, $event, 0); } -sub check_range { - my ($entry, $event) = @_; +sub check_number { + my ($entry, $event, $float) = @_; + + my $val = $event->get_keyval; - my $text = $entry->get_text; - if (!defined($text) || ($text !~ m/^(\d+)$/) || ($1 > 255)) { - $entry->set_text ($entry->{default}); + if (($float && $val == ord '.') || + $val == Gtk3::Gdk::KEY_ISO_Left_Tab || + $val == Gtk3::Gdk::KEY_Shift_L || + $val == Gtk3::Gdk::KEY_Tab || + $val == Gtk3::Gdk::KEY_Left || + $val == Gtk3::Gdk::KEY_Right || + $val == Gtk3::Gdk::KEY_BackSpace || + $val == Gtk3::Gdk::KEY_Delete || + ($val >= ord '0' && $val <= ord '9') || + ($val >= Gtk3::Gdk::KEY_KP_0 && + $val <= Gtk3::Gdk::KEY_KP_9)) { + return undef; } - return undef; + return 1; } - -sub creat_text_input { +sub create_text_input { my ($default, $text) = @_; - my $hbox = Gtk2::HBox->new (0, 0); + my $hbox = Gtk3::HBox->new (0, 0); - my $label = Gtk2::Label->new ($text); + 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 = Gtk2::Entry->new (); + my $e1 = Gtk3::Entry->new (); $e1->set_width_chars (30); $hbox->pack_start ($e1, 0, 0, 0); $e1->set_text ($default); @@ -1143,88 +1826,73 @@ sub creat_text_input { return ($hbox, $e1); } -sub creat_ip_input { - my ($init, $default, $text) = @_; - - my (@ips) = split /\./, $init; - my (@defs) = split /\./, $default; - - my $hbox = Gtk2::HBox->new (0, 0); +sub get_ip_config { - my $label = Gtk2::Label->new ($text); - $label->set_size_request (150, -1); - $label->set_alignment (1, 0.5); - $hbox->pack_start ($label, 0, 0, 10); - - my $e1 = Gtk2::Entry->new_with_max_length (3); - $e1->{default} = $defs[0]; - $hbox->pack_start ($e1, 0, 0, 0); - $e1->set_width_chars (3); - $e1->set_text ($ips[0]); - $e1->signal_connect (key_press_event => \&check_num); - $e1->signal_connect (focus_out_event => \&check_range); - - my $l1 = Gtk2::Label->new ("."); - $hbox->pack_start ($l1, 0, 0, 2); - - my $e2 = Gtk2::Entry->new_with_max_length (3); - $e2->{default} = $defs[1]; - $hbox->pack_start ($e2, 0, 0, 0); - $e2->set_width_chars (3); - $e2->set_text ($ips[1]); - $e2->signal_connect (key_press_event => \&check_num); - $e2->signal_connect (focus_out_event => \&check_range); - - my $l2 = Gtk2::Label->new ("."); - $hbox->pack_start ($l2, 0, 0, 2); - - my $e3 = Gtk2::Entry->new_with_max_length (3); - $e3->{default} = $defs[2]; - $hbox->pack_start ($e3, 0, 0, 0); - $e3->set_width_chars (3); - $e3->set_text ($ips[2]); - $e3->signal_connect (key_press_event => \&check_num); - $e3->signal_connect (focus_out_event => \&check_range); - - my $l3 = Gtk2::Label->new ("."); - $hbox->pack_start ($l3, 0, 0, 2); - - my $e4 = Gtk2::Entry->new_with_max_length (3); - $e4->{default} = $defs[3]; - $hbox->pack_start ($e4, 0, 0, 0); - $e4->set_width_chars (3); - $e4->set_text ($ips[3]); - $e4->signal_connect (key_press_event => \&check_num); - $e4->signal_connect (focus_out_event => \&check_range); - - return ($hbox, $e1, $e2, $e3, $e4); -} + my $ifaces = {}; + my $default; + + my $links = `ip -o l`; + foreach my $l (split /\n/,$links) { + my ($index, $name, $flags, $state, $mac) = $l =~ m/^(\d+):\s+(\S+):\s+<(\S+)>.*\s+state\s+(\S+)\s+.*\s+link\/ether\s+(\S+)\s+/; + next if !$name || $name eq 'lo'; + + my $driver = readlink "/sys/class/net/$name/device/driver" || 'unknown'; + $driver =~ s!^.*/!!; + + $ifaces->{"$index"} = { + name => $name, + driver => $driver, + flags => $flags, + state => $state, + mac => $mac, + }; + + my $addresses = `ip -o a s $name`; + foreach my $a (split /\n/,$addresses) { + my ($family, $ip, $prefix) = $a =~ m/^\Q$index\E:\s+\Q$name\E\s+(inet|inet6)\s+($IPRE)\/(\d+)\s+/; + next if !$ip; + next if $a =~ /scope\s+link/; # ignore link local + + my $mask = $prefix; + + if ($family eq 'inet') { + next if !$ip =~ /$IPV4RE/; + next if $prefix < 8 || $prefix > 32; + $mask = @$ipv4_reverse_mask[$prefix]; + } else { + next if !$ip =~ /$IPV6RE/; + } -sub get_ip_config { + $default = $index if !$default; - my $ifconfig = `ifconfig eth0`; + $ifaces->{"$index"}->{"$family"} = { + mask => $mask, + addr => $ip, + }; + } + } - my ($addr) = $ifconfig =~ m/inet addr:(\S*)/m; - my ($mask) = $ifconfig =~ m/Mask:(\S*)/m; - my $route = `route -n`; - my ($gateway) = $route =~ m/^0\.0\.0\.0\s+(\d+\.\d+\.\d+\.\d+)\s+/m; + my $route = `ip route`; + my ($gateway) = $route =~ m/^default\s+via\s+(\S+)\s+/m; my $resolvconf = `cat /etc/resolv.conf`; my ($dnsserver) = $resolvconf =~ m/^nameserver\s+(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/m; + my ($domain) = $resolvconf =~ m/^domain\s+(\S+)$/m; return { - addr => $addr, - mask => $mask, + default => $default, + ifaces => $ifaces, gateway => $gateway, dnsserver => $dnsserver, + domain => $domain, } } sub display_message { my ($msg) = @_; - my $dialog = Gtk2::MessageDialog->new ($window, 'modal', + my $dialog = Gtk3::MessageDialog->new ($window, 'modal', 'info', 'ok', $msg); $dialog->run(); $dialog->destroy(); @@ -1233,73 +1901,195 @@ sub display_message { sub display_error { my ($msg) = @_; - my $dialog = Gtk2::MessageDialog->new ($window, 'modal', + my $dialog = Gtk3::MessageDialog->new ($window, 'modal', 'error', 'ok', $msg); $dialog->run(); $dialog->destroy(); } +my $ipconf_first_view = 1; + sub create_ipconf_view { cleanup_view (); display_html ("ipconf.htm"); - my $vbox = Gtk2::VBox->new (0, 0); + my $vbox = Gtk3::VBox->new (0, 0); $inbox->pack_start ($vbox, 1, 0, 0); - my $hbox = Gtk2::HBox->new (0, 0); - $vbox->pack_start ($hbox, 0, 0, 30); - my $vbox2 = Gtk2::VBox->new (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 $addr = $ipconf->{addr} || '192.168.100.2'; - my $mask = $ipconf->{mask} || '255.255.255.0'; - - my ($hostbox, $hostentry) = - creat_text_input ('proxmox.domain.tld', 'Hostname (FQDN):'); + my $ipbox; + ($ipbox, $ipconf_entry_addr) = + create_text_input ("192.168.100.2", 'IP Address:'); + + my $maskbox; + ($maskbox, $ipconf_entry_mask) = + create_text_input ("255.255.255.0", 'Netmask:'); + + my $device_cb = Gtk3::ComboBoxText->new(); + $device_cb->set_active(0); + $device_cb->set_visible(1); + + my $get_device_desc = sub { + my $iface = shift; + return "$iface->{name} - $iface->{mac} ($iface->{driver})"; + }; + + my $device_active_map = {}; + + my $device_change_handler = sub { + my $current = shift; + $ipconf->{selected} = $device_active_map->{$current->get_active()}; + my $iface = $ipconf->{ifaces}->{$ipconf->{selected}}; + $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}; + }; + + my $i = 0; + foreach my $index (sort keys %{$ipconf->{ifaces}}) { + $device_cb->append_text(&$get_device_desc($ipconf->{ifaces}->{$index})); + $device_active_map->{$i} = $index; + if ($ipconf_first_view && $index == $ipconf->{default}) { + $device_cb->set_active($i); + &$device_change_handler($device_cb); + $ipconf_first_view = 0; + } + $device_cb->signal_connect ('changed' => $device_change_handler); + $i++; + } + + $device_cb->set_active(0) + if !($ipconf->{selected}); + + my $devicebox = Gtk3::HBox->new (0, 0); + my $label = Gtk3::Label->new ("Management Interface:"); + $label->set_size_request (150, -1); + $label->set_alignment (1, 0.5); + $devicebox->pack_start ($label, 0, 0, 10); + $devicebox->pack_start ($device_cb, 0, 0, 0); + + $vbox2->pack_start ($devicebox, 0, 0, 2); + + my $hn = $ipconf->{domain} ? + "$setup->{product}.$ipconf->{domain}" : "$setup->{product}.example.invalid"; + + my ($hostbox, $hostentry) = + create_text_input ($hn, 'Hostname (FQDN):'); $vbox2->pack_start ($hostbox, 0, 0, 2); - my $ipbox; - ($ipbox, $ip_1, $ip_2, $ip_3, $ip_4) = - creat_ip_input ($addr, '0.0.0.0', 'IP Address:'); $vbox2->pack_start ($ipbox, 0, 0, 2); - my $maskbox; - ($maskbox, $mask_1, $mask_2, $mask_3, $mask_4) = - creat_ip_input ($mask, '255.255.255.0', 'Netmask:'); $vbox2->pack_start ($maskbox, 0, 0, 2); $gateway = $ipconf->{gateway} || '192.168.100.1'; my $gwbox; - ($gwbox, $gw_1, $gw_2, $gw_3, $gw_4) = - creat_ip_input ($gateway, '0.0.0.0', 'Gateway:'); + ($gwbox, $ipconf_entry_gw) = + create_text_input ($gateway, 'Gateway:'); - $vbox2->pack_start ($gwbox, 0, 0, 15); + $vbox2->pack_start ($gwbox, 0, 0, 2); $dnsserver = $ipconf->{dnsserver} || $gateway; my $dnsbox; - ($dnsbox, $dns_1, $dns_2, $dns_3, $dns_4) = - creat_ip_input ($dnsserver, '0.0.0.0', 'DNS Server:'); + ($dnsbox, $ipconf_entry_dns) = + create_text_input ($dnsserver, 'DNS Server:'); $vbox2->pack_start ($dnsbox, 0, 0, 0); $inbox->show_all; - set_next (undef, sub { - my $text = $hostentry->get_text(); + set_next (undef, sub { + + # verify hostname + my $text = $hostentry->get_text(); + $text =~ s/^\s+//; $text =~ s/\s+$//; - if ($text && $text =~ m/^[\w\-]+(\.[\w\-]+)+$/ && $text !~ m/.domain.tld$/ && + my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)"; + + # Debian does not support purely numeric hostnames + if ($text && $text =~ /^[0-9]+(?:\.|$)/) { + display_message("Purely numeric hostnames are not allowed."); + $hostentry->grab_focus(); + return; + } + + if ($text && $text =~ m/^(${namere}\.)*${namere}$/ && $text !~ m/.example.invalid$/ && $text =~ m/^([^\.]+)\.(\S+)$/) { $hostname = $1; $domain = $2; - create_extract_view (); + } else { + display_message ("Hostname does not look like a fully qualified domain name."); + $hostentry->grab_focus(); + return; + } + + # verify ip address + + $text = $ipconf_entry_addr->get_text(); + $text =~ s/^\s+//; + $text =~ s/\s+$//; + if ($text =~ m!^($IPV4RE)$!) { + $ipaddress = $text; + $ipversion = 4; + } elsif ($text =~ m!^($IPV6RE)$!) { + $ipaddress = $text; + $ipversion = 6; + } else { + display_message ("IP address is not valid."); + $ipconf_entry_addr->grab_focus(); + return; + } + + $text = $ipconf_entry_mask->get_text(); + $text =~ s/^\s+//; + $text =~ s/\s+$//; + if (($ipversion == 6) && ($text =~ m/^(\d+)$/) && ($1 >= 8) && ($1 <= 126)) { + $netmask = $text; + } elsif (($ipversion == 4) && defined($ipv4_mask_hash->{$text})) { + $netmask = $text; + } else { + display_message ("Netmask is not valid."); + $ipconf_entry_mask->grab_focus(); + return; + } + + $text = $ipconf_entry_gw->get_text(); + $text =~ s/^\s+//; + $text =~ s/\s+$//; + if (($ipversion == 4) && ($text =~ m!^($IPV4RE)$!)) { + $gateway = $text; + } elsif (($ipversion == 6) && ($text =~ m!^($IPV6RE)$!)) { + $gateway = $text; + } else { + display_message ("Gateway is not valid."); + $ipconf_entry_gw->grab_focus(); return; } - display_message ("Hostname does not look like a fully qualified domain name."); - $hostentry->grab_focus(); + + $text = $ipconf_entry_dns->get_text(); + $text =~ s/^\s+//; + $text =~ s/\s+$//; + if (($ipversion == 4) && ($text =~ m!^($IPV4RE)$!)) { + $dnsserver = $text; + } elsif (($ipversion == 6) && ($text =~ m!^($IPV6RE)$!)) { + $dnsserver = $text; + } else { + display_message ("DNS server is not valid."); + $ipconf_entry_dns->grab_focus(); + return; + } + + #print "TEST $ipaddress $netmask $gateway $dnsserver\n"; + + create_extract_view (); }); $hostentry->grab_focus(); @@ -1308,16 +2098,20 @@ sub create_ipconf_view { sub get_device_desc { my ($devname, $size, $model) = @_; - if ($size && ($size > 0) && $model) { - $size = int($size/2048); # size in MB + if ($size && ($size > 0)) { + $size = int($size/2048); # size in MB, from 512B "sectors" + my $text = "$devname ("; if ($size >= 1024) { $size = int($size/1024); # size in GB - return "$devname (${size}GB, $model)"; + $text .= "${size}GB"; } else { - return "$devname (${size}MB, $model)"; + $text .= "${size}MB"; } + $text .= ", $model" if $model; + $text .= ")"; + } else { return $devname; } @@ -1354,7 +2148,7 @@ sub update_zonelist { $sel = $timezone; # used once to select default } - my $cb = $lastzonecb = Gtk2::ComboBox->new_text (); + my $cb = $lastzonecb = Gtk3::ComboBoxText->new(); $cb->set_size_request (200, -1); $cb->signal_connect ('changed' => sub { @@ -1385,38 +2179,39 @@ sub create_password_view { cleanup_view (); - my $vbox2 = Gtk2::VBox->new (0, 0); + my $vbox2 = Gtk3::VBox->new (0, 0); $inbox->pack_start ($vbox2, 1, 0, 0); - my $vbox = Gtk2::VBox->new (0, 0); - $vbox2->pack_start ($vbox, 0, 0, 30); + my $vbox = Gtk3::VBox->new (0, 0); + $vbox2->pack_start ($vbox, 0, 0, 10); - my $hbox1 = Gtk2::HBox->new (0, 0); - my $label = Gtk2::Label->new ("Password"); + my $hbox1 = Gtk3::HBox->new (0, 0); + my $label = Gtk3::Label->new ("Password"); $label->set_size_request (150, -1); $label->set_alignment (1, 0.5); $hbox1->pack_start ($label, 0, 0, 10); - my $pwe1 = Gtk2::Entry->new (); + my $pwe1 = Gtk3::Entry->new (); $pwe1->set_visibility (0); $pwe1->set_size_request (200, -1); $hbox1->pack_start ($pwe1, 0, 0, 0); - my $hbox2 = Gtk2::HBox->new (0, 0); - $label = Gtk2::Label->new ("Confirm"); + my $hbox2 = Gtk3::HBox->new (0, 0); + $label = Gtk3::Label->new ("Confirm"); $label->set_size_request (150, -1); $label->set_alignment (1, 0.5); $hbox2->pack_start ($label, 0, 0, 10); - my $pwe2 = Gtk2::Entry->new (); + my $pwe2 = Gtk3::Entry->new (); $pwe2->set_visibility (0); $pwe2->set_size_request (200, -1); $hbox2->pack_start ($pwe2, 0, 0, 0); - my $hbox3 = Gtk2::HBox->new (0, 0); - $label = Gtk2::Label->new ("E-Mail"); + my $hbox3 = Gtk3::HBox->new (0, 0); + $label = Gtk3::Label->new ("E-Mail"); $label->set_size_request (150, -1); $label->set_alignment (1, 0.5); $hbox3->pack_start ($label, 0, 0, 10); - my $eme = Gtk2::Entry->new (); + my $eme = Gtk3::Entry->new (); $eme->set_size_request (200, -1); + $eme->set_text('mail@example.invalid'); $hbox3->pack_start ($eme, 0, 0, 0); @@ -1447,11 +2242,16 @@ sub create_password_view { my $t3 = $eme->get_text; if ($t3 !~ m/^\S+\@\S+\.\S+$/) { - display_message ("E-Mail does not look like a vaild address" . + display_message ("E-Mail 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"); + $eme->grab_focus(); + return; } $password = $t1; @@ -1471,33 +2271,34 @@ sub create_country_view { my $countryhash = $cmap->{countryhash}; my $ctr = $cmap->{country}; - my $vbox2 = Gtk2::VBox->new (0, 0); + my $vbox2 = Gtk3::VBox->new (0, 0); $inbox->pack_start ($vbox2, 1, 0, 0); - my $vbox = Gtk2::VBox->new (0, 0); - $vbox2->pack_start ($vbox, 0, 0, 30); + my $vbox = Gtk3::VBox->new (0, 0); + $vbox2->pack_start ($vbox, 0, 0, 10); - my $w = Gtk2::Entry->new (); + my $w = Gtk3::Entry->new (); $w->set_size_request (200, -1); - my $c = Gtk2::EntryCompletion->new (); + my $c = Gtk3::EntryCompletion->new (); $c->set_text_column (0); $c->set_minimum_key_length(0); $c->set_popup_set_width (1); + $c->set_inline_completion (1); - my $hbox2 = Gtk2::HBox->new (0, 0); - my $label = Gtk2::Label->new ("Time zone"); + my $hbox2 = Gtk3::HBox->new (0, 0); + my $label = Gtk3::Label->new ("Time zone"); $label->set_size_request (150, -1); $label->set_alignment (1, 0.5); $hbox2->pack_start ($label, 0, 0, 10); update_zonelist ($hbox2); - my $hbox3 = Gtk2::HBox->new (0, 0); - $label = Gtk2::Label->new ("Keyboard Layout"); + my $hbox3 = Gtk3::HBox->new (0, 0); + $label = Gtk3::Label->new ("Keyboard Layout"); $label->set_size_request (150, -1); $label->set_alignment (1, 0.5); $hbox3->pack_start ($label, 0, 0, 10); - my $kmapcb = Gtk2::ComboBox->new_text (); + my $kmapcb = Gtk3::ComboBoxText->new(); $kmapcb->set_size_request (200, -1); foreach my $layout (sort keys %{$cmap->{kmaphash}}) { $kmapcb->append_text ($layout); @@ -1531,46 +2332,60 @@ sub create_country_view { my ($entry, $event) = @_; my $text = $entry->get_text; - my $val = $event->keyval; - if ($val == $Gtk2::Gdk::Keysyms{Tab}) { + my $val = $event->get_keyval; + + if ($val == Gtk3::Gdk::KEY_Tab) { my $cc = $countryhash->{lc($text)}; - return undef if $cc; + my $found = 0; my $compl; - foreach my $cc (keys %$ctr) { - my $ct = $ctr->{$cc}->{name}; - if ($ct =~ m/^\Q$text\E.*$/i) { - $found++; - $compl = $ct; + + if ($cc) { + $found = 1; + $compl = $ctr->{$cc}->{name}; + } else { + foreach my $cc (keys %$ctr) { + my $ct = $ctr->{$cc}->{name}; + if ($ct =~ m/^\Q$text\E.*$/i) { + $found++; + $compl = $ct; + } + last if $found > 1; } - last if $found > 1; } + if ($found == 1) { - $entry->set_text ($compl); + $entry->set_text($compl); + $c->complete(); return undef; } else { - Gtk2::Gdk->beep(); + #Gtk3::Gdk::beep(); + print chr(7); # beep ? } - $w->insert_text('', -1); # popup selection + $c->complete(); + + my $buf = $w->get_buffer(); + $buf->insert_text(-1, '', -1); # popup selection + return 1; } return undef; }); - - my $ls = Gtk2::ListStore->new('Glib::String'); + + my $ls = Gtk3::ListStore->new('Glib::String'); foreach my $cc (sort {$ctr->{$a}->{name} cmp $ctr->{$b}->{name} } keys %$ctr) { my $iter = $ls->append(); $ls->set ($iter, 0, $ctr->{$cc}->{name}); } $c->set_model ($ls); - $w->set_completion ($c); + $w->set_completion ($c); - my $hbox = Gtk2::HBox->new (0, 0); + my $hbox = Gtk3::HBox->new (0, 0); - $label = Gtk2::Label->new ("Country"); + $label = Gtk3::Label->new ("Country"); $label->set_alignment (1, 0.5); $label->set_size_request (150, -1); $hbox->pack_start ($label, 0, 0, 10); @@ -1580,7 +2395,7 @@ sub create_country_view { $vbox->pack_start ($hbox2, 0, 0, 5); $vbox->pack_start ($hbox3, 0, 0, 5); - if ($country) { + if ($country && $ctr->{$country}) { $w->set_text ($ctr->{$country}->{name}); } @@ -1604,85 +2419,543 @@ sub create_country_view { $w->grab_focus(); } +my $target_hd_combo; +my $target_hd_label; + +my $hdopion_first_setup = 1; + +my $create_basic_grid = sub { + my $grid = Gtk3::Grid->new(); + $grid->set_visible(1); + $grid->set_column_spacing(10); + $grid->set_row_spacing(10); + $grid->set_hexpand(1); + + $grid->set_margin_start(5); + $grid->set_margin_end(5); + $grid->set_margin_top(5); + $grid->set_margin_bottom(5); + + return $grid; +}; + +my $create_label_widget_grid = sub { + my ($labeled_widgets) = @_; + + my $grid = &$create_basic_grid(); + my $row = 0; + + for (my $i = 0; $i < @$labeled_widgets; $i += 2) { + my $widget = @$labeled_widgets[$i+1]; + my $label = Gtk3::Label->new(@$labeled_widgets[$i]); + $label->set_visible(1); + $label->set_alignment (1, 0.5); + $grid->attach($label, 0, $row, 1, 1); + $widget->set_visible(1); + $grid->attach($widget, 1, $row, 1, 1); + $row++; + } + + return $grid; +}; + +my $create_raid_disk_grid = sub { + my $disk_labeled_widgets = []; + for (my $i = 0; $i < @$hds; $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; + $disk_selector->append_text(get_device_desc ($devname, $size, $model)); + $disk_selector->{pve_disk_id} = $i; + $disk_selector->signal_connect (changed => sub { + my $w = shift; + my $diskid = $w->{pve_disk_id}; + my $a = $w->get_active - 1; + $config_options->{"disksel${diskid}"} = ($a >= 0) ? $hds->[$a] : undef; + }); + } + + if ($hdopion_first_setup) { + $disk_selector->set_active ($i+1) if $hds->[$i]; + } else { + my $hdind = 0; + if (my $cur_hd = $config_options->{"disksel$i"}) { + foreach my $hd (@$hds) { + if (@$hd[1] eq @$cur_hd[1]) { + $disk_selector->set_active($hdind+1); + last; + } + $hdind++; + } + } + } + + push @$disk_labeled_widgets, "Harddisk $i", $disk_selector; + } + + 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_policy('never', 'automatic'); + + return $scrolled_window; +# &$create_label_widget_grid($disk_labeled_widgets) +}; + +my $create_raid_advanced_grid = sub { + my $labeled_widgets = []; + 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; + $config_options->{ashift} = $w->get_value_as_int(); + }); + $config_options->{ashift} = 12 if ! defined($config_options->{ashift}); + $spinbutton_ashift->set_value($config_options->{ashift}); + push @$labeled_widgets, "ashift"; + push @$labeled_widgets, $spinbutton_ashift; + + my $combo_compress = Gtk3::ComboBoxText->new(); + $combo_compress->set_tooltip_text("zfs compression algorithm for rpool dataset"); + # note: gzip / lze not allowed for bootfs vdevs + my $comp_opts = ["on","off","lzjb","lz4"]; + foreach my $opt (@$comp_opts) { + $combo_compress->append($opt, $opt); + } + $config_options->{compress} = "on" if !defined($config_options->{compress}); + $combo_compress->set_active_id($config_options->{compress}); + $combo_compress->signal_connect (changed => sub { + my $w = shift; + $config_options->{compress} = $w->get_active_text(); + }); + push @$labeled_widgets, "compress"; + push @$labeled_widgets, $combo_compress; + + my $combo_checksum = Gtk3::ComboBoxText->new(); + $combo_checksum->set_tooltip_text("zfs checksum algorithm for rpool dataset"); + my $csum_opts = ["on", "off","fletcher2", "fletcher4", "sha256"]; + foreach my $opt (@$csum_opts) { + $combo_checksum->append($opt, $opt); + } + $config_options->{checksum} = "on" if !($config_options->{checksum}); + $combo_checksum->set_active_id($config_options->{checksum}); + $combo_checksum->signal_connect (changed => sub { + my $w = shift; + $config_options->{checksum} = $w->get_active_text(); + }); + push @$labeled_widgets, "checksum"; + push @$labeled_widgets, $combo_checksum; + + my $spinbutton_copies = Gtk3::SpinButton->new_with_range(1,3,1); + $spinbutton_copies->set_tooltip_text("zfs copies property for rpool dataset (in addition to RAID redundancy!)"); + $spinbutton_copies->signal_connect ("value-changed" => sub { + my $w = shift; + $config_options->{copies} = $w->get_value_as_int(); + }); + $config_options->{copies} = 1 if !defined($config_options->{copies}); + $spinbutton_copies->set_value($config_options->{copies}); + push @$labeled_widgets, "copies", $spinbutton_copies; + + return &$create_label_widget_grid($labeled_widgets);; +}; + +sub create_hdoption_view { + + my $dialog = Gtk3::Dialog->new(); + + $dialog->set_title("Harddisk options"); + + $dialog->add_button("_OK", 1); + + my $contarea = $dialog->get_content_area(); + + my $hbox2 = Gtk3::Box->new('horizontal', 0); + $contarea->pack_start($hbox2, 1, 1, 10); + + my $grid = Gtk3::Grid->new(); + $grid->set_column_spacing(10); + $grid->set_row_spacing(10); + + $hbox2->pack_start($grid, 1, 0, 10); + + 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)']; + + 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; + $tcount++; + } + + $grid->attach($fstypecb, 1, $row, 1, 1); + + $hbox2->show_all(); + + $row++; + + my $sep = Gtk3::HSeparator->new(); + $sep->set_visible(1); + $grid->attach($sep, 0, $row, 2, 1); + $row++; + + my $hdsize_labeled_widgets = []; + + # size compute + my $hdsize = 0; + if ( -b $target_hd) { + $hdsize = int(hd_size ($target_hd) / (1024*1024.0)); # size in GB + } elsif ($target_hd) { + $hdsize = int((-s $target_hd) / (1024*1024*1024.0)); + } + + my $hdsize_size_adj = Gtk3::Adjustment->new($config_options->{hdsize} || $hdsize, 0, $hdsize+1, 1, 1, 1); + my $spinbutton_hdsize = Gtk3::SpinButton->new($hdsize_size_adj, 1, 1); + $spinbutton_hdsize->set_tooltip_text("only use specified size (GB) of the harddisk (rest left unpartitioned)"); + push @$hdsize_labeled_widgets, "hdsize", $spinbutton_hdsize; + + my $entry_swapsize = Gtk3::Entry->new(); + $entry_swapsize->set_tooltip_text("maximum SWAP size (GB)"); + $entry_swapsize->signal_connect (key_press_event => \&check_float); + $entry_swapsize->set_text($config_options->{swapsize}) if $config_options->{swapsize}; + push @$hdsize_labeled_widgets, "swapsize", $entry_swapsize; + + my $entry_maxroot = Gtk3::Entry->new(); + $entry_maxroot->set_tooltip_text("maximum size (GB) for LVM root volume"); + $entry_maxroot->signal_connect (key_press_event => \&check_float); + $entry_maxroot->set_text($config_options->{maxroot}) if $config_options->{maxroot}; + push @$hdsize_labeled_widgets, "maxroot", $entry_maxroot; + + my $entry_minfree = Gtk3::Entry->new(); + $entry_minfree->set_tooltip_text("minimum free LVM space (GB, required for LVM snapshots)"); + $entry_minfree->signal_connect (key_press_event => \&check_float); + $entry_minfree->set_text($config_options->{minfree}) if $config_options->{minfree}; + push @$hdsize_labeled_widgets, "minfree", $entry_minfree; + + my $entry_maxvz = Gtk3::Entry->new(); + $entry_maxvz->set_tooltip_text("maximum size (GB) for LVM data volume"); + $entry_maxvz->signal_connect (key_press_event => \&check_float); + $entry_maxvz->set_text($config_options->{maxvz}) if $config_options->{maxvz}; + push @$hdsize_labeled_widgets, "maxvz", $entry_maxvz; + + my $options_stack = Gtk3::Stack->new(); + $options_stack->set_visible(1); + $options_stack->set_hexpand(1); + $options_stack->set_vexpand(1); + $options_stack->add_titled(&$create_raid_disk_grid(), "raiddisk", "Disk Setup"); + $options_stack->add_titled(&$create_label_widget_grid($hdsize_labeled_widgets), "hdsize", "Size Options"); + $options_stack->add_titled(&$create_raid_advanced_grid("zfs"), "raidzfsadvanced", "Advanced Options"); + $options_stack->set_visible_child_name("raiddisk"); + my $options_stack_switcher = Gtk3::StackSwitcher->new(); + $options_stack_switcher->set_halign('center'); + $options_stack_switcher->set_stack($options_stack); + $grid->attach($options_stack_switcher, 0, $row, 2, 1); + $row++; + $grid->attach($options_stack, 0, $row, 2, 1); + $row++; + + $hdopion_first_setup = 0; + + my $switch_view = sub { + my $raid = $config_options->{filesys} =~ m/zfs|btrfs/; + my $enable_zfs_opts = $config_options->{filesys} =~ m/zfs/; + + $target_hd_combo->set_visible(!$raid); + $options_stack->get_child_by_name("hdsize")->set_visible(!$raid); + $options_stack->get_child_by_name("raiddisk")->set_visible($raid); + $options_stack_switcher->set_visible($enable_zfs_opts); + $options_stack->get_child_by_name("raidzfsadvanced")->set_visible($enable_zfs_opts); + if ($raid) { + $target_hd_label->set_text("Target: $config_options->{filesys} "); + $options_stack->set_visible_child_name("raiddisk"); + } else { + $target_hd_label->set_text("Target Harddisk: "); + } + my (undef, $pref_width) = $dialog->get_preferred_width(); + my (undef, $pref_height) = $dialog->get_preferred_height(); + $pref_height = 750 if $pref_height > 750; + $dialog->resize($pref_width, $pref_height); + }; + + &$switch_view(); + + $fstypecb->signal_connect (changed => sub { + $config_options->{filesys} = $fstypecb->get_active_text(); + &$switch_view(); + }); + + $dialog->show(); + + $dialog->run(); + + my $get_float = sub { + my ($entry) = @_; + + my $text = $entry->get_text(); + return undef if !defined($text); + + $text =~ s/^\s+//; + $text =~ s/\s+$//; + + return undef if $text !~ m/^\d+(\.\d+)?$/; + + return $text; + }; + + my $tmp; + + if (($tmp = &$get_float($spinbutton_hdsize)) && ($tmp != $hdsize)) { + $config_options->{hdsize} = $tmp; + } else { + delete $config_options->{hdsize}; + } + + if (defined($tmp = &$get_float($entry_swapsize))) { + $config_options->{swapsize} = $tmp; + } else { + delete $config_options->{swapsize}; + } + + if (defined($tmp = &$get_float($entry_maxroot))) { + $config_options->{maxroot} = $tmp; + } else { + delete $config_options->{maxroot}; + } + + if (defined($tmp = &$get_float($entry_minfree))) { + $config_options->{minfree} = $tmp; + } else { + delete $config_options->{minfree}; + } + + if (defined($tmp = &$get_float($entry_maxvz))) { + $config_options->{maxvz} = $tmp; + } else { + delete $config_options->{maxvz}; + } + + $dialog->destroy(); +} + +my $get_raid_devlist = sub { + + my $dev_name_hash = {}; + + my $devlist = []; + for (my $i = 0; $i < @$hds; $i++) { + if (my $hd = $config_options->{"disksel$i"}) { + my ($disk, $devname, $size, $model) = @$hd; + die "device '$devname' is used more than once\n" + if $dev_name_hash->{$devname}; + $dev_name_hash->{$devname} = $hd; + push @$devlist, $hd; + } + } + + return $devlist; +}; + +sub zfs_mirror_size_check { + my ($expected, $actual) = @_; + + die "mirrored disks must have same size\n" + if abs($expected - $actual) > $expected / 10; +} + +sub get_zfs_raid_setup { + + my $filesys = $config_options->{filesys}; + + my $devlist = &$get_raid_devlist(); + + 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) { + $cmd .= " @$hd[1]"; + } + } elsif ($filesys eq 'zfs (RAID1)') { + die "zfs (RAID1) needs at least 2 device\n" if $diskcount < 2; + $cmd .= ' mirror '; + my $hd = @$devlist[0]; + my $expected_size = @$hd[2]; # all disks need approximately same size + foreach $hd (@$devlist) { + zfs_mirror_size_check($expected_size, @$hd[2]); + $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 + $cmd .= ' mirror ' . @$hd1[1] . ' ' . @$hd2[1]; + } + + } elsif ($filesys =~ m/^zfs \(RAIDZ-([123])\)$/) { + my $level = $1; + my $mindisks = 2 + $level; + die "zfs (RAIDZ-$level) needs at least $mindisks devices\n" if scalar(@$devlist) < $mindisks; + my $hd = @$devlist[0]; + my $expected_size = @$hd[2]; # all disks need approximately same size + $cmd .= " raidz$level"; + foreach $hd (@$devlist) { + zfs_mirror_size_check($expected_size, @$hd[2]); + $cmd .= " @$hd[1]"; + push @$bootdevlist, $hd; + } + } else { + die "unknown zfs mode '$filesys'\n"; + } + + return ($devlist, $bootdevlist, $cmd); +} + +sub get_btrfs_raid_setup { + + my $filesys = $config_options->{filesys}; + + my $devlist = &$get_raid_devlist(); + + my $diskcount = scalar(@$devlist); + die "$filesys needs at least one device\n" if $diskcount < 1; + + my $mode; + + if ($diskcount == 1) { + $mode = 'single'; + } else { + if ($filesys eq 'btrfs (RAID0)') { + $mode = 'raid0'; + } elsif ($filesys eq 'btrfs (RAID1)') { + die "btrfs (RAID1) needs at least 2 device\n" if $diskcount < 2; + $mode = 'raid1'; + } elsif ($filesys eq 'btrfs (RAID10)') { + die "btrfs (RAID10) needs at least 4 device\n" if $diskcount < 4; + $mode = 'raid10'; + } else { + die "unknown btrfs mode '$filesys'\n"; + } + } + + return ($devlist, $mode); +} + sub create_hdsel_view { cleanup_view (); - my $vbox = Gtk2::VBox->new (0, 0); + my $vbox = Gtk3::VBox->new (0, 0); $inbox->pack_start ($vbox, 1, 0, 0); - my $hbox = Gtk2::HBox->new (0, 0); - $vbox->pack_start ($hbox, 0, 0, 30); - + my $hbox = Gtk3::HBox->new (0, 0); + $vbox->pack_start ($hbox, 0, 0, 10); + my ($disk, $devname, $size, $model) = @{@$hds[0]}; $target_hd = $devname; - $master_hd = find_master ($target_hd); - my $label; - if (scalar (@$hds) == 1) { - my $devdesc = get_device_desc ($devname, $size, $model); - $label = Gtk2::Label->new ("Target Harddisk: $devdesc"); - $hbox->pack_start ($label, 0, 0, 0); - } else { - $label = Gtk2::Label->new ("Target Harddisks: "); - $hbox->pack_start ($label, 0, 0, 0); + $target_hd_label = Gtk3::Label->new ("Target Harddisk: "); + $hbox->pack_start ($target_hd_label, 0, 0, 0); - my $combo = Gtk2::ComboBox->new_text (); + $target_hd_combo = Gtk3::ComboBoxText->new(); - foreach my $hd (@$hds) { - ($disk, $devname, $size, $model) = @$hd; - $combo->append_text (get_device_desc ($devname, $size, $model)); - } + foreach my $hd (@$hds) { + ($disk, $devname, $size, $model) = @$hd; + $target_hd_combo->append_text (get_device_desc ($devname, $size, $model)); + } - $combo->set_active (0); - $combo->signal_connect (changed => sub { - $a = shift->get_active; - my ($disk, $devname) = @{@$hds[$a]}; - $target_hd = $devname; - $master_hd = find_master ($target_hd); - }); + $target_hd_combo->set_active (0); + $target_hd_combo->signal_connect (changed => sub { + $a = shift->get_active; + my ($disk, $devname) = @{@$hds[$a]}; + $target_hd = $devname; + }); + + $hbox->pack_start ($target_hd_combo, 0, 0, 10); + + my $options = Gtk3::Button->new ('_Options'); + $options->signal_connect (clicked => \&create_hdoption_view); + $hbox->pack_start ($options, 0, 0, 0); - $hbox->pack_start ($combo, 0, 0, 0); - } $inbox->show_all; display_html ("page1.htm"); - set_next (undef, \&create_country_view); -} - -sub create_extract_view { - $ipaddress = $ip_1->get_text . "." . $ip_2->get_text . "." . - $ip_3->get_text . "." . $ip_4->get_text; - - $netmask = $mask_1->get_text . "." . $mask_2->get_text . "." . - $mask_3->get_text . "." . $mask_4->get_text; + set_next (undef, sub { - $gateway = $gw_1->get_text . "." . $gw_2->get_text . "." . - $gw_3->get_text . "." . $gw_4->get_text; + if ($config_options->{filesys} =~ m/zfs/) { + eval { get_zfs_raid_setup(); }; + if (my $err = $@) { + display_message ("Warning: $err\n" . + "Please fix ZFS setup first."); + } else { + create_country_view(); + } + } elsif ($config_options->{filesys} =~ m/btrfs/) { + eval { get_btrfs_raid_setup(); }; + if (my $err = $@) { + display_message ("Warning: $err\n" . + "Please fix BTRFS setup first."); + } else { + create_country_view(); + } + } else { + create_country_view(); + } + }); +} - $dnsserver = $dns_1->get_text . "." . $dns_2->get_text . "." . - $dns_3->get_text . "." . $dns_4->get_text; +sub create_extract_view { - # print "TEST $ipaddress $netmask $gateway $dnsserver\n"; cleanup_view (); display_html ("extract1-license.htm"); $next->set_sensitive (0); - my $vbox = Gtk2::VBox->new (0, 0); + my $vbox = Gtk3::VBox->new (0, 0); $inbox->pack_start ($vbox, 1, 0, 0); - my $hbox = Gtk2::HBox->new (0, 0); - $vbox->pack_start ($hbox, 0, 0, 30); + my $hbox = Gtk3::HBox->new (0, 0); + $vbox->pack_start ($hbox, 0, 0, 10); - my $vbox2 = Gtk2::VBox->new (0, 0); + my $vbox2 = Gtk3::VBox->new (0, 0); $hbox->pack_start ($vbox2, 0, 0, 0); - $progress_status = Gtk2::Label->new (); + $progress_status = Gtk3::Label->new (''); $vbox2->pack_start ($progress_status, 1, 1, 0); - - $progress = Gtk2::ProgressBar->new; - $progress->set_size_request (400, -1); + + $progress = Gtk3::ProgressBar->new; + $progress->set_show_text(1); + $progress->set_size_request (600, -1); $vbox2->pack_start ($progress, 0, 0, 0); @@ -1690,7 +2963,7 @@ sub create_extract_view { my $tdir = $opt_testmode ? "target" : "/target"; mkdir $tdir; - my $base = $opt_testmode ? "/pve/$release/install/pve-base.tar" : "/proxmox/pve-base.tar"; + my $base = "${proxmox_cddir}/$setup->{product}-base.squashfs"; eval { extract_data ($base, $tdir); }; my $err = $@; @@ -1699,21 +2972,29 @@ sub create_extract_view { set_next ("_Reboot", sub { exit (0); } ); - display_html ($err ? "fail.htm" : "success.htm"); - - display_error ($err) if $err; -} - -sub mupdate_progress { - my $per = shift; - print "GOT1: $per\n"; - + if ($err) { + display_html ("fail.htm"); + display_error ($err); + } else { + cleanup_view (); + display_html ("success.htm"); + } } sub create_intro_view { cleanup_view (); + if ($setup->{product} eq 'pve') { + eval { + my $cpuinfo = file_get_contents('/proc/cpuinfo'); + if ($cpuinfo && !($cpuinfo =~ /^flags\s*:.*(vmx|svm)/m)) { + display_error("No support for KVM virtualisation detected.\n\n" . + "Check BIOS settings for Intel VT / AMD-V / SVM.") + } + }; + } + display_html ("license.htm"); set_next ("I a_gree", \&create_hdsel_view); @@ -1721,28 +3002,42 @@ sub create_intro_view { $ipconf = get_ip_config (); -$country = detect_country () if $ipconf->{addr}; +$country = detect_country() if $ipconf->{default} || $opt_testmode; # read country, kmap and timezone infos $cmap = read_cmap (); +if (!defined($cmap->{country}->{$country})) { + print $logfd "ignoring detected country '$country', invalid or unknown\n"; + $country = undef; +} + create_main_window (); +my $initial_error = 0; + if (!defined ($hds) || (scalar (@$hds) <= 0)) { print "no hardisks found\n"; + $initial_error = 1; display_html ("nohds.htm"); set_next ("Reboot", sub { exit (0); } ); } else { - foreach my $hd (@$hds) { my ($disk, $devname) = @$hd; next if $devname =~ m|^/dev/md\d+$|; print "found Disk$disk N:$devname\n"; } +} - create_intro_view (); +if (!$initial_error && (scalar keys %{ $ipconf->{ifaces} } == 0)) { + print "no network interfaces found\n"; + $initial_error = 1; + display_html ("nonics.htm"); + set_next ("Reboot", sub { exit (0); } ); } -Gtk2->main; +create_intro_view () if !$initial_error; + +Gtk3->main; exit 0;