Revert "drop unused policy-disable-rc.d"
[pve-installer.git] / proxinstall
1 #!/usr/bin/perl
2
3 $ENV{DEBIAN_FRONTEND} = 'noninteractive';
4 $ENV{LC_ALL} = 'C';
5
6 use strict;
7 use warnings;
8
9 use Getopt::Long;
10 use IPC::Open2;
11 use IPC::Open3;
12 use IO::File;
13 use IO::Select;
14 use Cwd 'abs_path';
15 use Gtk3 '-init';
16 use Gtk3::WebKit2;
17 use Encode;
18 use String::ShellQuote;
19 use Data::Dumper;
20 use File::Basename;
21 use File::Path;
22 use Time::HiRes;
23
24 use ProxmoxInstallerSetup;
25
26 my $setup = ProxmoxInstallerSetup::setup();
27
28 my $opt_testmode;
29
30 if (!$ENV{G_SLICE} ||  $ENV{G_SLICE} ne "always-malloc") {
31     die "do not use slice allocator (run with 'G_SLICE=always-malloc ./proxinstall ...')\n";
32 }
33
34 if (!GetOptions('testmode=s' => \$opt_testmode)) {
35     die "usage error\n";
36     exit (-1);
37 }
38
39 my $zfstestpool = "test_rpool";
40 my $zfspoolname = $opt_testmode ? $zfstestpool : 'rpool';
41 my $zfsrootvolname = "$setup->{product}-1";
42
43 my $product_fullname = {
44     pve => 'Proxmox VE',
45     pmg => 'Proxmox MailGateway',
46 };
47
48 my $storage_cfg_zfs = <<__EOD__;
49 dir: local
50         path /var/lib/vz
51         content iso,vztmpl,backup
52
53 zfspool: local-zfs
54         pool $zfspoolname/data
55         sparse
56         content images,rootdir
57 __EOD__
58
59 my $storage_cfg_btrfs = <<__EOD__;
60 dir: local
61         path /var/lib/vz
62         content iso,vztmpl,backup
63         disabled
64
65 btrfs: local-btrfs
66         path /var/lib/pve/local-btrfs
67         content iso,vztmpl,backup,images,rootdir
68 __EOD__
69
70 my $storage_cfg_lvmthin = <<__EOD__;
71 dir: local
72         path /var/lib/vz
73         content iso,vztmpl,backup
74
75 lvmthin: local-lvm
76         thinpool data
77         vgname pve
78         content rootdir,images
79 __EOD__
80
81 my $storage_cfg_local = <<__EOD__;
82 dir: local
83         path /var/lib/vz
84         content iso,vztmpl,backup,rootdir,images
85 __EOD__
86
87 sub file_read_firstline {
88     my ($filename) = @_;
89
90     my $fh = IO::File->new ($filename, "r");
91     return undef if !$fh;
92     my $res = <$fh>;
93     chomp $res if $res;
94     $fh->close;
95     return $res;
96 }
97
98 my $logfd = IO::File->new(">/tmp/install.log");
99
100 my $proxmox_libdir = $opt_testmode ?
101     Cwd::cwd() . "/testdir/var/lib/pve-installer" : "/var/lib/pve-installer";
102 my $proxmox_cddir = $opt_testmode ? "../pve-cd-builder/tmp/data-gz/" : "/cdrom";
103 my $proxmox_pkgdir = "${proxmox_cddir}/proxmox/packages/";
104
105 my $boot_type = -d '/sys/firmware/efi' ? 'efi' : 'bios';
106
107 my $IPV4OCTET = "(?:25[0-5]|(?:2[0-4]|1[0-9]|[1-9])?[0-9])";
108 my $IPV4RE = "(?:(?:$IPV4OCTET\\.){3}$IPV4OCTET)";
109 my $IPV6H16 = "(?:[0-9a-fA-F]{1,4})";
110 my $IPV6LS32 = "(?:(?:$IPV4RE|$IPV6H16:$IPV6H16))";
111
112 my $IPV6RE = "(?:" .
113     "(?:(?:" .                             "(?:$IPV6H16:){6})$IPV6LS32)|" .
114     "(?:(?:" .                           "::(?:$IPV6H16:){5})$IPV6LS32)|" .
115     "(?:(?:(?:" .              "$IPV6H16)?::(?:$IPV6H16:){4})$IPV6LS32)|" .
116     "(?:(?:(?:(?:$IPV6H16:){0,1}$IPV6H16)?::(?:$IPV6H16:){3})$IPV6LS32)|" .
117     "(?:(?:(?:(?:$IPV6H16:){0,2}$IPV6H16)?::(?:$IPV6H16:){2})$IPV6LS32)|" .
118     "(?:(?:(?:(?:$IPV6H16:){0,3}$IPV6H16)?::(?:$IPV6H16:){1})$IPV6LS32)|" .
119     "(?:(?:(?:(?:$IPV6H16:){0,4}$IPV6H16)?::" .           ")$IPV6LS32)|" .
120     "(?:(?:(?:(?:$IPV6H16:){0,5}$IPV6H16)?::" .            ")$IPV6H16)|" .
121     "(?:(?:(?:(?:$IPV6H16:){0,6}$IPV6H16)?::" .                    ")))";
122
123 my $IPRE = "(?:$IPV4RE|$IPV6RE)";
124
125
126 my $ipv4_mask_hash = {
127     '128.0.0.0' => 1,
128     '192.0.0.0' => 2,
129     '224.0.0.0' => 3,
130     '240.0.0.0' => 4,
131     '248.0.0.0' => 5,
132     '252.0.0.0' => 6,
133     '254.0.0.0' => 7,
134     '255.0.0.0' => 8,
135     '255.128.0.0' => 9,
136     '255.192.0.0' => 10,
137     '255.224.0.0' => 11,
138     '255.240.0.0' => 12,
139     '255.248.0.0' => 13,
140     '255.252.0.0' => 14,
141     '255.254.0.0' => 15,
142     '255.255.0.0' => 16,
143     '255.255.128.0' => 17,
144     '255.255.192.0' => 18,
145     '255.255.224.0' => 19,
146     '255.255.240.0' => 20,
147     '255.255.248.0' => 21,
148     '255.255.252.0' => 22,
149     '255.255.254.0' => 23,
150     '255.255.255.0' => 24,
151     '255.255.255.128' => 25,
152     '255.255.255.192' => 26,
153     '255.255.255.224' => 27,
154     '255.255.255.240' => 28,
155     '255.255.255.248' => 29,
156     '255.255.255.252' => 30,
157     '255.255.255.254' => 31,
158     '255.255.255.255' => 32
159 };
160
161 my $ipv4_reverse_mask = [
162     '0.0.0.0',
163     '128.0.0.0',
164     '192.0.0.0',
165     '224.0.0.0',
166     '240.0.0.0',
167     '248.0.0.0',
168     '252.0.0.0',
169     '254.0.0.0',
170     '255.0.0.0',
171     '255.128.0.0',
172     '255.192.0.0',
173     '255.224.0.0',
174     '255.240.0.0',
175     '255.248.0.0',
176     '255.252.0.0',
177     '255.254.0.0',
178     '255.255.0.0',
179     '255.255.128.0',
180     '255.255.192.0',
181     '255.255.224.0',
182     '255.255.240.0',
183     '255.255.248.0',
184     '255.255.252.0',
185     '255.255.254.0',
186     '255.255.255.0',
187     '255.255.255.128',
188     '255.255.255.192',
189     '255.255.255.224',
190     '255.255.255.240',
191     '255.255.255.248',
192     '255.255.255.252',
193     '255.255.255.254',
194     '255.255.255.255',
195 ];
196
197 my $step_number = 0; # Init number for global function list
198
199 my @steps = (
200     {
201         step => 'intro',
202         html => 'license.htm',
203         next_button => 'I a_gree',
204         function => \&create_intro_view,
205     },
206     {
207         step => 'intro',
208         html => 'page1.htm',
209         function => \&create_hdsel_view,
210     },
211     {
212         step => 'country',
213         html => 'country.htm',
214         function => \&create_country_view,
215     },
216     {
217         step => 'password',
218         html => 'passwd.htm',
219         function => \&create_password_view,
220     },
221     {
222         step => 'ipconf',
223         html => 'ipconf.htm',
224         function => \&create_ipconf_view,
225     },
226     {
227         step => 'ack',
228         html => 'ack.htm',
229         next_button => '_Install',
230         function => \&create_ack_view,
231     },
232     {
233         step => 'extract',
234         next_button => '_Reboot',
235         function => \&create_extract_view,
236     },
237 );
238
239 # GUI global variables
240 my ($window, $cmdbox, $inbox, $htmlview);
241 my $prev_btn;
242 my ($next, $next_fctn, $target_hd);
243 my ($progress, $progress_status);
244
245 my ($ipversion, $ipaddress, $ipconf_entry_addr);
246 my ($netmask, $ipconf_entry_mask);
247 my ($gateway, $ipconf_entry_gw);
248 my ($dnsserver, $ipconf_entry_dns);
249 my $hostname = 'proxmox';
250 my $domain = 'domain.tld';
251 my $cmdline = file_read_firstline("/proc/cmdline");
252 my $ipconf;
253 my $country;
254 my $timezone = 'Europe/Vienna';
255 my $keymap = 'en-us';
256 my $password;
257 my $mailto = 'mail@example.invalid';
258 my $cmap;
259
260 my $config = {
261     # TODO: add all the user-provided options for previous button
262     country => $country,
263     timezone => $timezone,
264     keymap => $keymap,
265
266     password => $password,
267     mailto => $mailto,
268
269     mngmt_nic => undef,
270     hostname => $hostname,
271     fqdn => undef,
272     ipaddress => undef,
273     netmask => undef,
274     gateway => undef,
275 };
276
277 # parse command line args
278
279 my $config_options = {};
280
281 if ($cmdline =~ m/\s(ext3|ext4|xfs)(\s.*)?$/) {
282     $config_options->{filesys} = $1;
283 } else {
284     $config_options->{filesys} = 'ext4';
285 }
286
287 if ($cmdline =~ m/hdsize=(\d+(\.\d+)?)[\s\n]/i) {
288     $config_options->{hdsize} = $1;
289 }
290
291 if ($cmdline =~ m/swapsize=(\d+(\.\d+)?)[\s\n]/i) {
292     $config_options->{swapsize} = $1;
293 }
294
295 if ($cmdline =~ m/maxroot=(\d+(\.\d+)?)[\s\n]/i) {
296     $config_options->{maxroot} = $1;
297 }
298
299 if ($cmdline =~ m/minfree=(\d+(\.\d+)?)[\s\n]/i) {
300     $config_options->{minfree} = $1;
301 }
302
303 if ($setup->{product} eq 'pve') {
304     if ($cmdline =~ m/maxvz=(\d+(\.\d+)?)[\s\n]/i) {
305         $config_options->{maxvz} = $1;
306     }
307 }
308
309 my $postfix_main_cf = <<_EOD;
310 # See /usr/share/postfix/main.cf.dist for a commented, more complete version
311
312 myhostname=__FQDN__
313
314 smtpd_banner = \$myhostname ESMTP \$mail_name (Debian/GNU)
315 biff = no
316
317 # appending .domain is the MUA's job.
318 append_dot_mydomain = no
319
320 # Uncomment the next line to generate "delayed mail" warnings
321 #delay_warning_time = 4h
322
323 alias_maps = hash:/etc/aliases
324 alias_database = hash:/etc/aliases
325 mydestination = \$myhostname, localhost.\$mydomain, localhost
326 relayhost =
327 mynetworks = 127.0.0.0/8
328 inet_interfaces = loopback-only
329 recipient_delimiter = +
330
331 compatibility_level = 2
332
333 _EOD
334
335 sub shellquote {
336     my $str = shift;
337
338     return String::ShellQuote::shell_quote($str);
339 }
340
341 sub cmd2string {
342     my ($cmd) = @_;
343
344     die "no arguments" if !$cmd;
345
346     return $cmd if !ref($cmd);
347
348     my @qa = ();
349     foreach my $arg (@$cmd) { push @qa, shellquote($arg); }
350
351     return join (' ', @qa);
352 }
353
354 sub syscmd {
355     my ($cmd) = @_;
356
357     return run_command($cmd, undef, undef, 1);
358 }
359
360 sub run_command {
361     my ($cmd, $func, $input, $noout) = @_;
362
363     my $cmdstr;
364     if (!ref($cmd)) {
365         $cmdstr = $cmd;
366         if ($cmd =~ m/|/) {
367             # see 'man bash' for option pipefail
368             $cmd = [ '/bin/bash', '-c', "set -o pipefail && $cmd" ];
369         } else {
370             $cmd = [ $cmd ];
371         }
372     } else {
373         $cmdstr = cmd2string($cmd);
374     }
375
376     my $cmdtxt;
377     if ($input && ($cmdstr !~ m/chpasswd/)) {
378         $cmdtxt = "# $cmdstr <<EOD\n$input";
379         chomp $cmdtxt;
380         $cmdtxt .= "\nEOD\n";
381     } else {
382         $cmdtxt = "# $cmdstr\n";
383     }
384
385     if ($opt_testmode) {
386         print $cmdtxt;
387         STDOUT->flush();
388     }
389
390     print $logfd $cmdtxt;
391
392     my $reader = IO::File->new();
393     my $writer = IO::File->new();
394     my $error  = IO::File->new();
395
396     my $orig_pid = $$;
397
398     my $pid;
399     eval {
400         $pid = open3($writer, $reader, $error, @$cmd) || die $!;
401     };
402
403     my $err = $@;
404
405     # catch exec errors
406     if ($orig_pid != $$) {
407         POSIX::_exit (1);
408         kill ('KILL', $$);
409     }
410
411     die $err if $err;
412
413     print $writer $input if defined $input;
414     close $writer;
415
416     my $select = new IO::Select;
417     $select->add($reader);
418     $select->add($error);
419
420     my ($ostream, $logout) = ('', '', '');
421
422     while ($select->count) {
423         my @handles = $select->can_read (0.2);
424
425         Gtk3::main_iteration() while Gtk3::events_pending();
426
427         next if !scalar (@handles); # timeout
428
429         foreach my $h (@handles) {
430             my $buf = '';
431             my $count = sysread ($h, $buf, 4096);
432             if (!defined ($count)) {
433                 my $err = $!;
434                 kill (9, $pid);
435                 waitpid ($pid, 0);
436                 die "command '$cmd' failed: $err";
437             }
438             $select->remove($h) if !$count;
439             if ($h eq $reader) {
440                 $ostream .= $buf if !($noout || $func);
441                 $logout .= $buf;
442                 while ($logout =~ s/^([^\010\r\n]*)(\r|\n|(\010)+|\r\n)//s) {
443                     my $line = $1;
444                     &$func($line) if $func;
445                 }
446
447             } elsif ($h eq $error) {
448                 $ostream .= $buf if !($noout || $func);
449             }
450             print $buf;
451             STDOUT->flush();
452             print $logfd $buf;
453         }
454     }
455
456     &$func($logout) if $func;
457
458     my $rv = waitpid ($pid, 0);
459
460     return $? if $noout; # behave like standard system();
461
462     if ($? == -1) {
463         die "command '$cmdstr' failed to execute\n";
464     } elsif (my $sig = ($? & 127)) {
465         die "command '$cmdstr' failed - got signal $sig\n";
466     } elsif (my $exitcode = ($? >> 8)) {
467         die "command '$cmdstr' failed with exit code $exitcode";
468     }
469
470     return $ostream;
471 }
472
473 sub detect_country {
474
475     print "trying to detect country...\n";
476     my $cpid = open2(\*TMP, undef, "traceroute -N 1 -q 1 -n 8.8.8.8");
477     return undef if !$cpid;
478
479     my $country;
480
481     my $previous_alarm = alarm (10);
482     eval  {
483         local $SIG{ALRM} = sub { die "timed out!\n" };
484         my $line;
485         while (defined ($line = <TMP>)) {
486             print $logfd "DC TRACEROUTE: $line";
487             if ($line =~ m/\s*\d+\s+(\d+\.\d+\.\d+\.\d+)\s/) {
488                 my $geoip = `geoiplookup $1`;
489                 print $logfd "DC GEOIP: $geoip";
490                 if ($geoip =~ m/GeoIP Country Edition:\s*([A-Z]+),/) {
491                     $country = lc ($1);
492                     print $logfd "DC FOUND: $country\n";
493                     last;
494                 }
495             }
496         }
497     };
498
499     my $err = $@;
500
501     alarm ($previous_alarm);
502
503     close (TMP);
504
505     if ($err) {
506         print "unable to detect country - $err\n";
507     } elsif ($country) {
508         print "detected country: " . uc($country) . "\n";
509     } else {
510         print "unable to detect country\n";
511     }
512
513     return $country;
514 }
515
516 sub get_memtotal {
517
518     open (MEMINFO, "/proc/meminfo");
519
520     my $res = 512; # default to 512 if something goes wrong
521     while (my $line = <MEMINFO>) {
522         if ($line =~ m/^MemTotal:\s+(\d+)\s*kB/i) {
523             $res = int ($1 / 1024);
524         }
525     }
526
527     close (MEMINFO);
528
529     return $res;
530 }
531
532 my $total_memory = get_memtotal();
533
534 sub link_points_to {
535     my ($src, $dest) = @_;
536
537     my ($dev1,$ino1) = stat ($src);
538     my ($dev2,$ino2) = stat ($dest);
539
540     return 0 if !($dev1 && $dev2 && $ino1 && $ino2);
541
542     return $ino1 == $ino2 && $dev1 == $dev2;
543 }
544
545 sub find_stable_path {
546     my ($stabledir, $bdev) = @_;
547
548     foreach my $path (<$stabledir/*>) {
549         if (link_points_to($path, $bdev)) {
550             return wantarray ? ($path, basename($path)) : $path;
551         }
552     }
553 }
554
555 sub find_dev_by_uuid {
556     my $bdev = shift;
557
558     my ($full_path, $name) = find_stable_path("/dev/disk/by-uuid", $bdev);
559
560     return $name;
561 }
562
563 sub hd_list {
564
565     my $res = ();
566
567     if ($opt_testmode) {
568         my @disks = split /,/, $opt_testmode;
569
570         for my $disk (@disks) {
571             push @$res, [-1, $disk, int((-s $disk)/512), "TESTDISK", 512];
572         }
573         return $res;
574     }
575
576     my $count = 0;
577
578     foreach my $bd (</sys/block/*>) {
579         next if $bd =~ m|^/sys/block/ram\d+$|;
580         next if $bd =~ m|^/sys/block/loop\d+$|;
581         next if $bd =~ m|^/sys/block/md\d+$|;
582         next if $bd =~ m|^/sys/block/dm-.*$|;
583         next if $bd =~ m|^/sys/block/fd\d+$|;
584         next if $bd =~ m|^/sys/block/sr\d+$|;
585
586         my $dev = file_read_firstline("$bd/dev");
587         chomp $dev;
588
589         next if !$dev;
590
591         my $info = `udevadm info --path $bd --query all`;
592         next if !$info;
593
594         next if $info !~ m/^E: DEVTYPE=disk$/m;
595
596         next if $info =~ m/^E: ID_CDROM/m;
597         next if $info =~ m/^E: ID_FS_TYPE=iso9660/m;
598
599         my ($name) = $info =~ m/^N: (\S+)$/m;
600
601         if ($name) {
602             my $real_name = "/dev/$name";
603
604             my $size = file_read_firstline("$bd/size");
605             chomp $size;
606             $size = undef if !($size && $size =~ m/^\d+$/);
607
608             my $model = file_read_firstline("$bd/device/model") || '';
609             $model =~ s/^\s+//;
610             $model =~ s/\s+$//;
611             if (length ($model) > 30) {
612                 $model = substr ($model, 0, 30);
613             }
614
615             my $logical_bsize = file_read_firstline("$bd/queue/logical_block_size") // '';
616             chomp $logical_bsize;
617             $logical_bsize = undef if !($logical_bsize && $logical_bsize =~ m/^\d+$/);
618
619             push @$res, [$count++, $real_name, $size, $model, $logical_bsize] if $size;
620         } else {
621             print STDERR "ERROR: unable to map device $dev ($bd)\n";
622         }
623     }
624
625     return $res;
626 }
627
628 sub read_cmap {
629     my $countryfn = "${proxmox_libdir}/country.dat";
630     open (TMP, "<:encoding(utf8)", "$countryfn") || die "unable to open '$countryfn' - $!\n";
631     my $line;
632     my $country = {};
633     my $countryhash = {};
634     my $kmap = {};
635     my $kmaphash = {};
636     while (defined ($line = <TMP>)) {
637         if ($line =~ m|^map:([^\s:]+):([^:]+):([^:]+):([^:]+):([^:]+):([^:]*):$|) {
638             $kmap->{$1} = {
639                 name => $2,
640                 kvm => $3,
641                 console => $4,
642                 x11 => $5,
643                 x11var => $6,
644             };
645             $kmaphash->{$2} = $1;
646         } elsif ($line =~ m|^([a-z]{2}):([^:]+):([^:]*):([^:]*):$|) {
647             $country->{$1} = {
648                 name => $2,
649                 kmap => $3,
650                 mirror => $4,
651             };
652             $countryhash->{lc($2)} = $1;
653         } else {
654             warn "unable to parse 'country.dat' line: $line";
655         }
656     }
657     close (TMP);
658
659     my $zones = {};
660     my $cczones = {};
661     my $zonefn = "/usr/share/zoneinfo/zone.tab";
662     open (TMP, "<$zonefn") || die "unable to open '$zonefn' - $!\n";
663     while (defined ($line = <TMP>)) {
664         next if $line =~ m/^\#/;
665         next if $line =~ m/^\s*$/;
666         if ($line =~ m|^([A-Z][A-Z])\s+\S+\s+(([^/]+)/\S+)\s|) {
667             my $cc = lc($1);
668             $cczones->{$cc}->{$2} = 1;
669             $country->{$cc}->{zone} = $2 if !defined ($country->{$cc}->{zone});
670             $zones->{$2} = 1;
671
672         }
673     }
674     close (TMP);
675
676     return {
677         zones => $zones,
678         cczones => $cczones,
679         country => $country,
680         countryhash => $countryhash,
681         kmap => $kmap,
682         kmaphash => $kmaphash,
683     }
684 }
685
686 # search for Harddisks
687 my $hds = hd_list();
688
689 sub hd_size {
690     my ($dev) = @_;
691
692     foreach my $hd (@$hds) {
693         my ($disk, $devname, $size, $model, $logical_bsize) = @$hd;
694         # size is always (also for 4kn disks) in 512B "sectors"! convert to KB
695         return int($size/2) if $devname eq $dev;
696     }
697
698     die "no such device '$dev'\n";
699 }
700
701 sub logical_blocksize {
702     my ($dev) = @_;
703
704     foreach my $hd (@$hds) {
705         my ($disk, $devname, $size, $model, $logical_bsize) = @$hd;
706         return $logical_bsize if $devname eq $dev;
707     }
708
709     die "no such device '$dev'\n";
710 }
711
712 sub get_partition_dev {
713     my ($dev, $partnum) = @_;
714
715     if ($dev =~ m|^/dev/sd([a-h]?[a-z]\|i[a-v])$|) {
716         return "${dev}$partnum";
717     } elsif ($dev =~ m|^/dev/xvd[a-z]$|) {
718         # Citrix Hypervisor blockdev
719         return "${dev}$partnum";
720     } elsif ($dev =~ m|^/dev/[hxev]d[a-z]$|) {
721         return "${dev}$partnum";
722     } elsif ($dev =~ m|^/dev/[^/]+/c\d+d\d+$|) {
723         return "${dev}p$partnum";
724     } elsif ($dev =~ m|^/dev/[^/]+/d\d+$|) {
725         return "${dev}p$partnum";
726     } elsif ($dev =~ m|^/dev/[^/]+/hd[a-z]$|) {
727         return "${dev}$partnum";
728     } elsif ($dev =~ m|^/dev/nvme\d+n\d+$|) {
729         return "${dev}p$partnum";
730     } else {
731         die "unable to get device for partition $partnum on device $dev\n";
732     }
733
734 }
735
736 sub file_get_contents {
737     my ($filename, $max) = @_;
738
739     my $fh = IO::File->new($filename, "r") ||
740         die "can't open '$filename' - $!\n";
741
742     local $/; # slurp mode
743
744     my $content = <$fh>;
745
746     close $fh;
747
748     return $content;
749 }
750
751 sub write_config {
752     my ($text, $filename) = @_;
753
754     my $fd = IO::File->new(">$filename") ||
755         die "unable to open file '$filename' - $!\n";
756     print $fd $text;
757     $fd->close();
758 }
759
760 sub update_progress {
761     my ($frac, $start, $end, $text) = @_;
762
763     my $part = $end - $start;
764     my $res = $start + $frac*$part;
765
766     $progress->set_fraction ($res);
767     $progress->set_text (sprintf ("%d%%", int ($res*100)));
768     $progress_status->set_text ($text) if defined ($text);
769
770     display_info() if $res < 0.9;
771
772     Gtk3::main_iteration() while Gtk3::events_pending();
773 }
774
775 my $fssetup = {
776     ext3 => {
777         mkfs => 'mkfs.ext3 -F',
778         mkfs_root_opt => '',
779         mkfs_data_opt => '-m 0',
780         root_mountopt => 'errors=remount-ro',
781     },
782     ext4 => {
783         mkfs => 'mkfs.ext4 -F',
784         mkfs_root_opt => '',
785         mkfs_data_opt => '-m 0',
786         root_mountopt => 'errors=remount-ro',
787     },
788     xfs => {
789         mkfs => 'mkfs.xfs -f',
790         mkfs_root_opt => '',
791         mkfs_data_opt => '',
792         root_mountopt => '',
793     },
794 };
795
796 sub create_filesystem {
797     my ($dev, $name, $type, $start, $end, $fs, $fe) = @_;
798
799     my $range = $end - $start;
800     my $rs = $start + $range*$fs;
801     my $re = $start + $range*$fe;
802     my $max = 0;
803
804     my $fsdata = $fssetup->{$type} || die "internal error - unknown file system '$type'";
805     my $opts = $name eq 'root' ? $fsdata->{mkfs_root_opt} : $fsdata->{mkfs_data_opt};
806
807     update_progress(0, $rs, $re, "creating $name filesystem");
808
809     run_command("$fsdata->{mkfs} $opts $dev", sub {
810         my $line = shift;
811
812         if ($line =~ m/Writing inode tables:\s+(\d+)\/(\d+)/) {
813             $max = $2;
814         } elsif ($max && $line =~ m/(\d+)\/$max/) {
815             update_progress(($1/$max)*0.9, $rs, $re);
816         } elsif ($line =~ m/Creating journal.*done/) {
817             update_progress(0.95, $rs, $re);
818         } elsif ($line =~ m/Writing superblocks and filesystem.*done/) {
819             update_progress(1, $rs, $re);
820         }
821     });
822 }
823
824 sub debconfig_set {
825     my ($targetdir, $dcdata) = @_;
826
827     my $cfgfile = "/tmp/debconf.txt";
828     write_config($dcdata, "$targetdir/$cfgfile");
829     syscmd("chroot $targetdir debconf-set-selections $cfgfile");
830     unlink "$targetdir/$cfgfile";
831 }
832
833 sub diversion_add {
834     my ($targetdir, $cmd, $new_cmd) = @_;
835
836     syscmd("chroot $targetdir dpkg-divert --package proxmox " .
837            "--add --rename $cmd") == 0 ||
838            die "unable to exec dpkg-divert\n";
839
840     syscmd("ln -sf ${new_cmd} $targetdir/$cmd") == 0 ||
841         die "unable to link diversion to ${new_cmd}\n";
842 }
843
844 sub diversion_remove {
845     my  ($targetdir, $cmd) = @_;
846
847     syscmd("mv $targetdir/${cmd}.distrib $targetdir/${cmd};") == 0 ||
848         die "unable to remove $cmd diversion\n";
849
850     syscmd("chroot $targetdir dpkg-divert --remove $cmd") == 0 ||
851         die "unable to remove $cmd diversion\n";
852 }
853
854 sub btrfs_create {
855     my ($partitions, $mode) = @_;
856
857     die "unknown btrfs mode '$mode'"
858         if !($mode eq 'single' || $mode eq 'raid0' ||
859              $mode eq 'raid1' || $mode eq 'raid10');
860
861     my $cmd = ['mkfs.btrfs', '-f'];
862
863     push @$cmd, '-d', $mode, '-m', $mode;
864
865     push @$cmd, @$partitions;
866
867     syscmd($cmd);
868 }
869
870 sub zfs_create_rpool {
871     my ($vdev) = @_;
872
873     my $cmd = "zpool create -f -o cachefile=none";
874
875     $cmd .= " -o ashift=$config_options->{ashift}"
876         if defined($config_options->{ashift});
877
878     syscmd("$cmd $zfspoolname $vdev") == 0 ||
879         die "unable to create zfs root pool\n";
880
881     syscmd("zfs create $zfspoolname/ROOT")  == 0 ||
882         die "unable to create zfs $zfspoolname/ROOT volume\n";
883
884     if ($setup->{product} eq 'pve') {
885         syscmd("zfs create $zfspoolname/data")  == 0 ||
886             die "unable to create zfs $zfspoolname/data volume\n";
887     }
888
889     syscmd("zfs create $zfspoolname/ROOT/$zfsrootvolname")  == 0 ||
890         die "unable to create zfs $zfspoolname/ROOT/$zfsrootvolname volume\n";
891
892     # disable atime during install
893     syscmd("zfs set atime=off $zfspoolname") == 0 ||
894         die "unable to set zfs properties\n";
895
896     my $value = $config_options->{compress};
897     syscmd("zfs set compression=$value $zfspoolname")
898         if defined($value) && $value ne 'off';
899
900     $value = $config_options->{checksum};
901     syscmd("zfs set checksum=$value $zfspoolname")
902         if defined($value) && $value ne 'on';
903
904     $value = $config_options->{copies};
905     syscmd("zfs set copies=$value $zfspoolname")
906         if defined($value) && $value != 1;
907 }
908
909 my $udevadm_trigger_block = sub {
910     my ($nowait) = @_;
911
912     sleep(1) if !$nowait; # give kernel time to reread part table
913
914     # trigger udev to create /dev/disk/by-uuid
915     syscmd("udevadm trigger --subsystem-match block");
916     syscmd("udevadm settle --timeout 10");
917 };
918
919 my $clean_disk = sub {
920     my ($disk) = @_;
921
922     my $partitions = `lsblk --output kname --noheadings --path --list $disk`;
923     foreach my $part (split "\n", $partitions) {
924         next if $part eq $disk;
925         next if $part !~ /^\Q$disk\E/;
926         eval { syscmd("pvremove -ff -y $part"); };
927         eval { syscmd("zpool labelclear -f $part"); };
928         eval { syscmd("dd if=/dev/zero of=$part bs=1M count=16"); };
929     }
930 };
931
932 sub partition_bootable_disk {
933     my ($target_dev, $maxhdsizegb, $ptype) = @_;
934
935     die "too dangerous" if $opt_testmode;
936
937     die "unknown partition type '$ptype'"
938         if !($ptype eq '8E00' || $ptype eq '8300' || $ptype eq 'BF01');
939
940     my $hdsize = hd_size($target_dev); # size in KB (1024 bytes)
941
942     my $restricted_hdsize_mb = 0; # 0 ==> end of partition
943     if ($maxhdsizegb) {
944         my $maxhdsize = $maxhdsizegb * 1024 * 1024;
945         if ($maxhdsize < $hdsize) {
946             $hdsize = $maxhdsize;
947             $restricted_hdsize_mb = int($hdsize/1024) . 'M';
948         }
949     }
950
951     my $hdgb = int($hdsize/(1024*1024));
952     die "hardisk '$target_dev' too small (${hdgb}GB)\n" if $hdgb < 8;
953
954     syscmd("sgdisk -Z ${target_dev}");
955
956     # 1 - BIOS boot partition (Grub Stage2): first free 1M
957     # 2 - EFI ESP: next free 512M
958     # 3 - OS/Data partition: rest, up to $maxhdsize in MB
959
960     my $grubbootdev = get_partition_dev($target_dev, 1);
961     my $efibootdev = get_partition_dev($target_dev, 2);
962     my $osdev = get_partition_dev ($target_dev, 3);
963
964     my $pcmd = ['sgdisk'];
965
966     my $pnum = 2;
967     push @$pcmd, "-n${pnum}:1M:+512M", "-t$pnum:EF00";
968
969     $pnum = 3;
970     push @$pcmd, "-n${pnum}:513M:${restricted_hdsize_mb}", "-t$pnum:$ptype";
971
972     push @$pcmd, $target_dev;
973
974     my $os_size = $hdsize - 513*1024; # 512M efi + 1M bios_boot + 1M alignment
975
976     syscmd($pcmd) == 0 ||
977         die "unable to partition harddisk '${target_dev}'\n";
978
979     my $blocksize = logical_blocksize($target_dev);
980
981     if ($blocksize != 4096) {
982         $pnum = 1;
983         $pcmd = ['sgdisk', '-a1', "-n$pnum:34:2047", "-t$pnum:EF02" , $target_dev];
984
985         syscmd($pcmd) == 0 ||
986             die "unable to create bios_boot partition '${target_dev}'\n";
987     }
988
989     &$udevadm_trigger_block();
990
991     foreach my $part ($efibootdev, $osdev) {
992         syscmd("dd if=/dev/zero of=$part bs=1M count=256") if -b $part;
993     }
994
995     return ($os_size, $osdev, $efibootdev);
996 }
997
998 sub get_pv_list_from_vgname {
999     my ($vgname) = @_;
1000
1001     my $res;
1002
1003     my $parser = sub {
1004         my $line = shift;
1005         $line =~ s/^\s+//;
1006         $line =~ s/\s+$//;
1007         return if !$line;
1008         my ($pv, $vg_uuid) = split(/\s+/, $line);
1009
1010         if (!defined($res->{$vg_uuid}->{pvs})) {
1011             $res->{$vg_uuid}->{pvs} = "$pv";
1012         } else {
1013             $res->{$vg_uuid}->{pvs} .= ", $pv";
1014         }
1015     };
1016     run_command("pvs --noheadings -o pv_name,vg_uuid -S vg_name='$vgname'", $parser, undef, 1);
1017
1018     return $res;
1019 }
1020
1021 sub ask_existing_vg_rename_or_abort {
1022     my ($vgname) = @_;
1023
1024     # this normally only happens if one put a disk with a PVE installation in
1025     # this server and that disk is not the installation target.
1026     my $duplicate_vgs = get_pv_list_from_vgname($vgname);
1027     return if !$duplicate_vgs;
1028
1029     my $message = "Detected existing '$vgname' Volume Group(s)! Do you want to:\n";
1030
1031     for my $vg_uuid (keys %$duplicate_vgs) {
1032         my $vg = $duplicate_vgs->{$vg_uuid};
1033
1034         # no high randomnes properties, but this is only for the cases where
1035         # we either have multiple "$vgname" vgs from multiple old PVE disks, or
1036         # we have a disk with both a "$vgname" and "$vgname-old"...
1037         my $short_uid = sprintf "%08X", rand(0xffffffff);
1038         $vg->{new_vgname} = "$vgname-OLD-$short_uid";
1039
1040         $message .= "rename VG backed by PV '$vg->{pvs}' to '$vg->{new_vgname}'\n";
1041     }
1042     $message .= "or cancel the installation?";
1043
1044     my $dialog = Gtk3::MessageDialog->new($window, 'modal', 'question', 'ok-cancel', $message);
1045     my $response = $dialog->run();
1046     $dialog->destroy();
1047
1048     if ($response eq 'ok') {
1049         for my $vg_uuid (keys %$duplicate_vgs) {
1050             my $vg = $duplicate_vgs->{$vg_uuid};
1051             my $new_vgname = $vg->{new_vgname};
1052
1053             syscmd("vgrename $vg_uuid $new_vgname") == 0 ||
1054                 die "could not rename VG from '$vg->{pvs}' ($vg_uuid) to '$new_vgname'!\n";
1055         }
1056     } else {
1057         set_next("_Reboot", sub { exit (0); } );
1058         display_html("fail.htm");
1059         die "Cancled installation by user, due to already existing volume group '$vgname'\n";
1060     }
1061 }
1062
1063 sub create_lvm_volumes {
1064     my ($lvmdev, $os_size, $swap_size) = @_;
1065
1066     my $vgname = $setup->{product};
1067
1068     ask_existing_vg_rename_or_abort($vgname);
1069
1070     my $rootdev = "/dev/$vgname/root";
1071     my $datadev = "/dev/$vgname/data";
1072     my $swapfile;
1073
1074     # we use --metadatasize 250k, which results in "pe_start = 512"
1075     # so pe_start is aligned on a 128k boundary (advantage for SSDs)
1076     syscmd("/sbin/pvcreate --metadatasize 250k -y -ff $lvmdev") == 0 ||
1077         die "unable to initialize physical volume $lvmdev\n";
1078     syscmd("/sbin/vgcreate $vgname $lvmdev") == 0 ||
1079         die "unable to create volume group '$vgname'\n";
1080
1081     my $hdgb = int($os_size/(1024*1024));
1082     my $space = (($hdgb > 128) ? 16 : ($hdgb/8))*1024*1024;
1083
1084     my $rootsize;
1085     my $datasize;
1086
1087     if ($setup->{product} eq 'pve') {
1088
1089         my $maxroot;
1090         if ($config_options->{maxroot}) {
1091             $maxroot = $config_options->{maxroot};
1092         } else {
1093             $maxroot = 96;
1094         }
1095
1096         $rootsize = (($hdgb > ($maxroot*4)) ? $maxroot : $hdgb/4)*1024*1024;
1097
1098         my $rest = $os_size - $swap_size - $rootsize; # in KB
1099
1100         my $minfree;
1101         if (defined($config_options->{minfree})) {
1102             $minfree = (($config_options->{minfree}*1024*1024) >= $rest ) ? $space :
1103                 $config_options->{minfree}*1024*1024 ;
1104         } else {
1105             $minfree = $space;
1106         }
1107
1108         $rest = $rest - $minfree;
1109
1110         if (defined($config_options->{maxvz})) {
1111             $rest = (($config_options->{maxvz}*1024*1024) <= $rest) ?
1112                 $config_options->{maxvz}*1024*1024 : $rest;
1113         }
1114
1115         $datasize = $rest;
1116
1117     } else {
1118         my $minfree = defined($config_options->{minfree}) ? $config_options->{minfree}*1024*1024 : $space;
1119         $rootsize = $os_size - $minfree - $swap_size; # in KB
1120     }
1121
1122     if ($swap_size) {
1123         syscmd("/sbin/lvcreate -L${swap_size}K -nswap $vgname") == 0 ||
1124             die "unable to create swap volume\n";
1125
1126         $swapfile = "/dev/$vgname/swap";
1127     }
1128
1129     syscmd("/sbin/lvcreate -L${rootsize}K -nroot $vgname") == 0 ||
1130         die "unable to create root volume\n";
1131
1132     if ($datasize > 4*1024*1024) {
1133         my $metadatasize = $datasize/100; # default 1% of data
1134         $metadatasize = 1024*1024 if $metadatasize < 1024*1024; # but at least 1G
1135         $metadatasize = 16*1024*1024 if $metadatasize > 16*1024*1024; # but at most 16G
1136
1137         # otherwise the metadata is taken out of $minfree
1138         $datasize -= 2*$metadatasize;
1139
1140         # 1 4MB PE to allow for rounding
1141         $datasize -= 4*1024;
1142
1143         syscmd("/sbin/lvcreate -L${datasize}K -ndata $vgname") == 0 ||
1144             die "unable to create data volume\n";
1145
1146         syscmd("/sbin/lvconvert --yes --type thin-pool --poolmetadatasize ${metadatasize}K $vgname/data") == 0 ||
1147             die "unable to create data thin-pool\n";
1148     } else {
1149         $datadev = undef;
1150     }
1151
1152     syscmd("/sbin/vgchange -a y $vgname") == 0 ||
1153         die "unable to activate volume group\n";
1154
1155     return ($rootdev, $swapfile, $datadev);
1156 }
1157
1158 sub compute_swapsize {
1159     my ($hdsize) = @_;
1160
1161     my $hdgb = int($hdsize/(1024*1024));
1162
1163     my $swapsize;
1164     if (defined($config_options->{swapsize})) {
1165         $swapsize = $config_options->{swapsize}*1024*1024;
1166     } else {
1167         my $ss = int ($total_memory / 1024);
1168         $ss = 4 if $ss < 4;
1169         $ss = ($hdgb/8) if $ss > ($hdgb/8);
1170         $ss = 8 if $ss > 8;
1171         $swapsize = $ss*1024*1024;
1172     }
1173
1174     return $swapsize;
1175 }
1176
1177 sub prepare_systemd_boot_esp {
1178     my ($espdev, $targetdir) = @_;
1179
1180     syscmd("chroot $targetdir pve-efiboot-tool init $espdev") == 0 ||
1181         die "unable to init ESP and install systemd-boot loader on '$espdev'\n";
1182 }
1183
1184 sub prepare_grub_efi_boot_esp {
1185     my ($dev, $espdev, $targetdir) = @_;
1186
1187     syscmd("mount -n $espdev -t vfat $targetdir/boot/efi") == 0 ||
1188         die "unable to mount $espdev\n";
1189
1190     eval {
1191         my $rc = syscmd("chroot $targetdir /usr/sbin/grub-install --target x86_64-efi --no-floppy --bootloader-id='proxmox' $dev");
1192         if ($rc != 0) {
1193             if ($boot_type eq 'efi') {
1194                 die "unable to install the EFI boot loader on '$dev'\n";
1195             } else {
1196                 warn "unable to install the EFI boot loader on '$dev', ignoring (not booted using UEFI)\n";
1197             }
1198         }
1199         # also install fallback boot file (OVMF does not boot without)
1200         mkdir("$targetdir/boot/efi/EFI/BOOT");
1201         syscmd("cp $targetdir/boot/efi/EFI/proxmox/grubx64.efi $targetdir/boot/efi/EFI/BOOT/BOOTx64.EFI") == 0 ||
1202             die "unable to copy efi boot loader\n";
1203     };
1204     my $err = $@;
1205
1206     eval {
1207         syscmd("umount $targetdir/boot/efi") == 0 ||
1208             die "unable to umount $targetdir/boot/efi\n";
1209     };
1210     warn $@ if $@;
1211
1212     die "failed to prepare EFI boot using Grub on '$espdev': $err" if $err;
1213 }
1214
1215 sub extract_data {
1216     my ($basefile, $targetdir) = @_;
1217
1218     die "target '$targetdir' does not exist\n" if ! -d  $targetdir;
1219
1220     my $starttime = [Time::HiRes::gettimeofday];
1221
1222     my $bootdevinfo = [];
1223
1224     my $swapfile;
1225     my $rootdev;
1226     my $datadev;
1227
1228     my $use_zfs = 0;
1229     my $use_btrfs = 0;
1230
1231     my $filesys = $config_options->{filesys};
1232
1233     if ($filesys =~ m/zfs/) {
1234         $target_hd = undef; # do not use this config
1235         $use_zfs = 1;
1236         $targetdir = "/$zfspoolname/ROOT/$zfsrootvolname";
1237     } elsif ($filesys =~ m/btrfs/) {
1238         $target_hd = undef; # do not use this config
1239         $use_btrfs = 1;
1240     }
1241
1242     if ($use_zfs) {
1243         my $i;
1244         for ($i = 5; $i > 0; $i--) {
1245             syscmd("modprobe zfs");
1246             last if -c "/dev/zfs";
1247             sleep(1);
1248         }
1249
1250         die "unable to load zfs kernel module\n" if !$i;
1251     }
1252
1253     my $bootloader_err;
1254
1255     eval {
1256
1257
1258         my $maxper = 0.25;
1259
1260         update_progress(0, 0, $maxper, "create partitions");
1261
1262         syscmd("vgchange -an") if !$opt_testmode; # deactivate all detected VGs
1263
1264         if ($opt_testmode) {
1265
1266             $rootdev = abs_path($opt_testmode);
1267             syscmd("umount $rootdev");
1268
1269             if ($use_btrfs) {
1270
1271                 die "unsupported btrfs mode (for testing environment)\n"
1272                     if $filesys ne 'btrfs (RAID0)';
1273
1274                 btrfs_create([$rootdev], 'single');
1275
1276             } elsif ($use_zfs) {
1277
1278                 die "unsupported zfs mode (for testing environment)\n"
1279                     if $filesys ne 'zfs (RAID0)';
1280
1281                 syscmd("zpool destroy $zfstestpool");
1282
1283                 zfs_create_rpool($rootdev);
1284
1285             } else {
1286
1287                 # nothing to do
1288             }
1289
1290         } elsif ($use_btrfs) {
1291
1292             my ($devlist, $btrfs_mode) = get_btrfs_raid_setup();
1293             my $btrfs_partitions = [];
1294             my $disksize;
1295             foreach my $hd (@$devlist) {
1296                 my $devname = @$hd[1];
1297                 my $logical_bsize = @$hd[4];
1298
1299                 &$clean_disk($devname);
1300                 my ($size, $osdev, $efidev) =
1301                     partition_bootable_disk($devname, undef, '8300');
1302                 $rootdev = $osdev if !defined($rootdev); # simply point to first disk
1303                 my $by_id = find_stable_path("/dev/disk/by-id", $devname);
1304                 push @$bootdevinfo, {
1305                     esp => $efidev,
1306                     devname => $devname,
1307                     osdev => $osdev,
1308                     by_id => $by_id,
1309                     logical_bsize => $logical_bsize,
1310                 };
1311                 push @$btrfs_partitions, $osdev;
1312                 $disksize = $size;
1313             }
1314
1315             &$udevadm_trigger_block();
1316
1317             btrfs_create($btrfs_partitions, $btrfs_mode);
1318
1319         } elsif ($use_zfs) {
1320
1321             my ($devlist, $bootdevlist, $vdev) = get_zfs_raid_setup();
1322
1323             foreach my $hd (@$devlist) {
1324                 &$clean_disk(@$hd[1]);
1325             }
1326
1327             my $disksize;
1328             foreach my $hd (@$bootdevlist) {
1329                 my $devname = @$hd[1];
1330                 my $logical_bsize = @$hd[4];
1331
1332                 my ($size, $osdev, $efidev) =
1333                     partition_bootable_disk($devname, $config_options->{hdsize}, 'BF01');
1334
1335                 zfs_mirror_size_check($disksize, $size) if $disksize;
1336
1337                 push @$bootdevinfo, {
1338                     esp => $efidev,
1339                     devname => $devname,
1340                     osdev => $osdev,
1341                     logical_bsize => $logical_bsize,
1342                 };
1343                 $disksize = $size;
1344             }
1345
1346             &$udevadm_trigger_block();
1347
1348             foreach my $di (@$bootdevinfo) {
1349                 my $devname = $di->{devname};
1350                 $di->{by_id} = find_stable_path ("/dev/disk/by-id", $devname);
1351
1352                 my $osdev = find_stable_path ("/dev/disk/by-id", $di->{osdev}) || $di->{osdev};
1353
1354                 $vdev =~ s/ $devname/ $osdev/;
1355             }
1356
1357             foreach my $hd (@$devlist) {
1358                 my $devname = @$hd[1];
1359                 my $by_id = find_stable_path ("/dev/disk/by-id", $devname);
1360
1361                 $vdev =~ s/ $devname/ $by_id/;
1362             }
1363
1364             zfs_create_rpool($vdev);
1365
1366         } else {
1367
1368             die "target '$target_hd' is not a valid block device\n" if ! -b $target_hd;
1369
1370             &$clean_disk($target_hd);
1371
1372             my $logical_bsize = logical_blocksize($target_hd);
1373
1374             my ($os_size, $osdev, $efidev);
1375             ($os_size, $osdev, $efidev) =
1376                 partition_bootable_disk($target_hd, $config_options->{hdsize}, '8E00');
1377
1378             &$udevadm_trigger_block();
1379
1380             my $by_id = find_stable_path ("/dev/disk/by-id", $target_hd);
1381             push @$bootdevinfo, {
1382                 esp => $efidev,
1383                 devname => $target_hd,
1384                 osdev => $osdev,
1385                 by_id => $by_id,
1386                 logical_bsize => $logical_bsize,
1387             };
1388
1389             my $swap_size = compute_swapsize($os_size);
1390             ($rootdev, $swapfile, $datadev) =
1391                 create_lvm_volumes($osdev, $os_size, $swap_size);
1392
1393             # trigger udev to create /dev/disk/by-uuid
1394             &$udevadm_trigger_block(1);
1395         }
1396
1397         if ($use_zfs) {
1398             # to be fast during installation
1399             syscmd("zfs set sync=disabled $zfspoolname") == 0 ||
1400                 die "unable to set zfs properties\n";
1401         }
1402
1403         update_progress(0.03, 0, $maxper, "create swap space");
1404         if ($swapfile) {
1405             syscmd("mkswap -f $swapfile") == 0 ||
1406                 die "unable to create swap space\n";
1407         }
1408
1409         update_progress(0.05, 0, $maxper, "creating filesystems");
1410
1411         foreach my $di (@$bootdevinfo) {
1412             next if !$di->{esp};
1413             # FIXME remove '-s1' once https://github.com/dosfstools/dosfstools/issues/111 is fixed
1414             my $vfat_extra_opts = ($di->{logical_bsize} == 4096) ? '-s1' : '';
1415             syscmd("mkfs.vfat $vfat_extra_opts -F32 $di->{esp}") == 0 ||
1416                 die "unable to initialize EFI ESP on device $di->{esp}\n";
1417         }
1418
1419         if ($use_zfs) {
1420             # do nothing
1421         } elsif ($use_btrfs) {
1422             # do nothing
1423         } else {
1424             create_filesystem($rootdev, 'root', $filesys, 0.05, $maxper, 0, 1);
1425         }
1426
1427         update_progress(1, 0.05, $maxper, "mounting target $rootdev");
1428
1429         if ($use_zfs) {
1430             # do nothing
1431         } else {
1432             my $mount_opts = 'noatime';
1433             $mount_opts .= ',nobarrier'
1434                 if $use_btrfs || $filesys =~ /^ext\d$/;
1435
1436             syscmd("mount -n $rootdev -o $mount_opts $targetdir") == 0 ||
1437                 die "unable to mount $rootdev\n";
1438         }
1439
1440         mkdir "$targetdir/boot";
1441         mkdir "$targetdir/boot/efi";
1442
1443         mkdir "$targetdir/var";
1444         mkdir "$targetdir/var/lib";
1445
1446         if ($setup->{product} eq 'pve') {
1447             mkdir "$targetdir/var/lib/vz";
1448             mkdir "$targetdir/var/lib/pve";
1449
1450             if ($use_btrfs) {
1451                 syscmd("btrfs subvolume create $targetdir/var/lib/pve/local-btrfs") == 0 ||
1452                     die "unable to create btrfs subvolume\n";
1453             }
1454         }
1455
1456         mkdir "$targetdir/mnt";
1457         mkdir "$targetdir/mnt/hostrun";
1458         syscmd("mount --bind /run $targetdir/mnt/hostrun") == 0 ||
1459             die "unable to bindmount run on $targetdir/mnt/hostrun\n";
1460
1461         update_progress(1, 0.05, $maxper, "extracting base system");
1462
1463         my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size) = stat ($basefile);
1464         $ino || die "unable to open file '$basefile' - $!\n";
1465
1466         my $files = file_read_firstline("${proxmox_cddir}/proxmox/$setup->{product}-base.cnt") ||
1467             die "unable to read base file count\n";
1468
1469         my $per = 0;
1470         my $count = 0;
1471
1472         run_command("unsquashfs -f -dest $targetdir -i $basefile", sub {
1473             my $line = shift;
1474             return if $line !~ m/^$targetdir/;
1475             $count++;
1476             my $nper = int (($count *100)/$files);
1477             if ($nper != $per) {
1478                 $per = $nper;
1479                 my $frac = $per > 100 ? 1 : $per/100;
1480                 update_progress($frac, $maxper, 0.5);
1481             }
1482         });
1483
1484         syscmd("mount -n -t tmpfs tmpfs $targetdir/tmp") == 0 ||
1485             die "unable to mount tmpfs on $targetdir/tmp\n";
1486         syscmd("mount -n -t proc proc $targetdir/proc") == 0 ||
1487             die "unable to mount proc on $targetdir/proc\n";
1488         syscmd("mount -n -t sysfs sysfs $targetdir/sys") == 0 ||
1489             die "unable to mount sysfs on $targetdir/sys\n";
1490         if ($boot_type eq 'efi') {
1491             syscmd("mount -n -t efivarfs efivarfs $targetdir/sys/firmware/efi/efivars") == 0 ||
1492                 die "unable to mount efivarfs on $targetdir/sys/firmware/efi/efivars: $!\n";
1493         }
1494         syscmd("chroot $targetdir mount --bind /mnt/hostrun /run") == 0 ||
1495             die "unable to re-bindmount hostrun on /run in chroot\n";
1496
1497         update_progress(1, $maxper, 0.5, "configuring base system");
1498
1499         # configure hosts
1500
1501         my $hosts =
1502             "127.0.0.1 localhost.localdomain localhost\n" .
1503             "$ipaddress $hostname.$domain $hostname\n\n" .
1504             "# The following lines are desirable for IPv6 capable hosts\n\n" .
1505             "::1     ip6-localhost ip6-loopback\n" .
1506             "fe00::0 ip6-localnet\n" .
1507             "ff00::0 ip6-mcastprefix\n" .
1508             "ff02::1 ip6-allnodes\n" .
1509             "ff02::2 ip6-allrouters\n" .
1510             "ff02::3 ip6-allhosts\n";
1511
1512         write_config($hosts, "$targetdir/etc/hosts");
1513
1514         write_config("$hostname\n", "$targetdir/etc/hostname");
1515
1516         syscmd("/bin/hostname $hostname") if !$opt_testmode;
1517
1518         # configure interfaces
1519
1520         my $ifaces = "auto lo\niface lo inet loopback\n\n";
1521
1522         my $ntype = $ipversion == 4 ? 'inet' : 'inet6';
1523
1524         my $ethdev = $ipconf->{ifaces}->{$ipconf->{selected}}->{name};
1525
1526         if ($setup->{bridged_network}) {
1527             $ifaces .= "iface $ethdev $ntype manual\n";
1528
1529             $ifaces .=
1530                 "\nauto vmbr0\niface vmbr0 $ntype static\n" .
1531                 "\taddress $ipaddress\n" .
1532                 "\tnetmask $netmask\n" .
1533                 "\tgateway $gateway\n" .
1534                 "\tbridge_ports $ethdev\n" .
1535                 "\tbridge_stp off\n" .
1536                 "\tbridge_fd 0\n";
1537         } else {
1538             $ifaces .= "auto $ethdev\n" .
1539                 "iface $ethdev $ntype static\n" .
1540                 "\taddress $ipaddress\n" .
1541                 "\tnetmask $netmask\n" .
1542                 "\tgateway $gateway\n";
1543         }
1544
1545         foreach my $iface (sort keys %{$ipconf->{ifaces}}) {
1546             my $name = $ipconf->{ifaces}->{$iface}->{name};
1547             next if $name eq $ethdev;
1548
1549             $ifaces .= "\niface $name $ntype manual\n";
1550         }
1551
1552         write_config($ifaces, "$targetdir/etc/network/interfaces");
1553
1554         # configure dns
1555
1556         my $resolvconf = "search $domain\nnameserver $dnsserver\n";
1557         write_config($resolvconf, "$targetdir/etc/resolv.conf");
1558
1559         # configure fstab
1560
1561         my $fstab = "# <file system> <mount point> <type> <options> <dump> <pass>\n";
1562
1563         if ($use_zfs) {
1564             # do nothing
1565         } elsif ($use_btrfs) {
1566             my $fsuuid;
1567             my $cmd = "blkid -u filesystem -t TYPE=btrfs -o export $rootdev";
1568             run_command($cmd, sub {
1569                 my $line = shift;
1570
1571                 if ($line =~ m/^UUID=([A-Fa-f0-9\-]+)$/) {
1572                     $fsuuid = $1;
1573                 }
1574             });
1575
1576             die "unable to detect FS UUID" if !defined($fsuuid);
1577
1578             $fstab .= "UUID=$fsuuid / btrfs defaults 0 1\n";
1579         } else {
1580             my $root_mountopt = $fssetup->{$filesys}->{root_mountopt} || 'defaults';
1581             $fstab .= "$rootdev / $filesys ${root_mountopt} 0 1\n";
1582         }
1583
1584         # mount /boot/efi
1585         # Note: this is required by current grub, but really dangerous, because
1586         # vfat does not have journaling, so it triggers manual fsck after each crash
1587         # so we only mount /boot/efi if really required (efi systems).
1588         if ($boot_type eq 'efi' && !$use_zfs) {
1589             if (scalar(@$bootdevinfo)) {
1590                 my $di = @$bootdevinfo[0]; # simply use first disk
1591
1592                 if ($di->{esp}) {
1593                     my $efi_boot_uuid = $di->{esp};
1594                     if (my $uuid = find_dev_by_uuid ($di->{esp})) {
1595                         $efi_boot_uuid = "UUID=$uuid";
1596                     }
1597
1598                     $fstab .= "${efi_boot_uuid} /boot/efi vfat defaults 0 1\n";
1599                 }
1600             }
1601         }
1602
1603
1604         $fstab .= "$swapfile none swap sw 0 0\n" if $swapfile;
1605
1606         $fstab .= "proc /proc proc defaults 0 0\n";
1607
1608         write_config($fstab, "$targetdir/etc/fstab");
1609         write_config("", "$targetdir/etc/mtab");
1610
1611         syscmd("cp ${proxmox_libdir}/policy-disable-rc.d " .
1612                 "$targetdir/usr/sbin/policy-rc.d") == 0 ||
1613                 die "unable to copy policy-rc.d\n";
1614         syscmd("cp ${proxmox_libdir}/fake-start-stop-daemon " .
1615                 "$targetdir/sbin/") == 0 ||
1616                 die "unable to copy start-stop-daemon\n";
1617
1618         diversion_add($targetdir, "/sbin/start-stop-daemon", "/sbin/fake-start-stop-daemon");
1619         diversion_add($targetdir, "/usr/sbin/update-grub", "/bin/true");
1620         diversion_add($targetdir, "/usr/sbin/update-initramfs", "/bin/true");
1621
1622         my $machine_id = run_command("systemd-id128 new");
1623         die "unable to create a new machine-id\n" if ! $machine_id;
1624         write_config($machine_id, "$targetdir/etc/machine-id");
1625
1626         syscmd("cp /etc/hostid $targetdir/etc/") == 0 ||
1627             die "unable to copy hostid\n";
1628
1629         syscmd("touch  $targetdir/proxmox_install_mode");
1630
1631         my $grub_install_devices_txt = '';
1632         foreach my $di (@$bootdevinfo) {
1633             $grub_install_devices_txt .= ', ' if $grub_install_devices_txt;
1634             $grub_install_devices_txt .= $di->{by_id} || $di->{devname};
1635         }
1636
1637         # Note: keyboard-configuration/xbkb-keymap is used by console-setup
1638         my $xkmap = $cmap->{kmap}->{$keymap}->{x11} // 'us';
1639
1640         debconfig_set ($targetdir, <<_EOD);
1641 locales locales/default_environment_locale select en_US.UTF-8
1642 locales locales/locales_to_be_generated select en_US.UTF-8 UTF-8
1643 samba-common samba-common/dhcp boolean false
1644 samba-common samba-common/workgroup string WORKGROUP
1645 postfix postfix/main_mailer_type select No configuration
1646 keyboard-configuration keyboard-configuration/xkb-keymap select $xkmap
1647 d-i debian-installer/locale select en_US.UTF-8
1648 grub-pc grub-pc/install_devices select $grub_install_devices_txt
1649 _EOD
1650
1651         my $pkg_count = 0;
1652         while (<${proxmox_pkgdir}/*.deb>) { $pkg_count++ };
1653
1654         # btrfs/dpkg is extremely slow without --force-unsafe-io
1655         my $dpkg_opts = $use_btrfs ? "--force-unsafe-io" : "";
1656
1657         $count = 0;
1658         while (<${proxmox_pkgdir}/*.deb>) {
1659             chomp;
1660             my $path = $_;
1661             my ($deb) = $path =~ m/${proxmox_pkgdir}\/(.*\.deb)/;
1662             update_progress($count/$pkg_count, 0.5, 0.75, "extracting $deb");
1663             print "extracting: $deb\n";
1664             syscmd("cp $path $targetdir/tmp/$deb") == 0 ||
1665                 die "installation of package $deb failed\n";
1666             syscmd("chroot $targetdir dpkg $dpkg_opts --force-depends --no-triggers --unpack /tmp/$deb") == 0 ||
1667                 die "installation of package $deb failed\n";
1668             update_progress((++$count)/$pkg_count, 0.5, 0.75);
1669         }
1670
1671         # needed for postfix postinst in case no other NIC is active
1672         syscmd("chroot $targetdir ifup lo");
1673
1674         my $cmd = "chroot $targetdir dpkg $dpkg_opts --force-confold --configure -a";
1675         $count = 0;
1676         run_command($cmd, sub {
1677             my $line = shift;
1678             if ($line =~ m/Setting up\s+(\S+)/) {
1679                 update_progress((++$count)/$pkg_count, 0.75, 0.95,
1680                                 "configuring $1");
1681             }
1682         });
1683
1684         unlink "$targetdir/etc/mailname";
1685         $postfix_main_cf =~ s/__FQDN__/${hostname}.${domain}/;
1686         write_config($postfix_main_cf, "$targetdir/etc/postfix/main.cf");
1687
1688         # make sure we have all postfix directories
1689         syscmd("chroot $targetdir /usr/sbin/postfix check");
1690         # cleanup mail queue
1691         syscmd("chroot $targetdir /usr/sbin/postsuper -d ALL");
1692         # create /etc/aliases.db (/etc/aliases is shipped in the base squashfs)
1693         syscmd("chroot $targetdir /usr/bin/newaliases");
1694
1695         # enable NTP (timedatectl set-ntp true  does not work without DBUS)
1696         syscmd("chroot $targetdir /bin/systemctl enable systemd-timesyncd.service");
1697
1698         unlink  "$targetdir/proxmox_install_mode";
1699
1700         # set timezone
1701         unlink ("$targetdir/etc/localtime");
1702         symlink ("/usr/share/zoneinfo/$timezone", "$targetdir/etc/localtime");
1703         write_config("$timezone\n", "$targetdir/etc/timezone");
1704
1705         # set apt mirror
1706         if (my $mirror = $cmap->{country}->{$country}->{mirror}) {
1707             my $fn = "$targetdir/etc/apt/sources.list";
1708             syscmd("sed -i 's/ftp\\.debian\\.org/$mirror/' '$fn'");
1709         }
1710
1711         # create extended_states for apt (avoid cron job warning if that
1712         # file does not exist)
1713         write_config('', "$targetdir/var/lib/apt/extended_states");
1714
1715         # allow ssh root login
1716         syscmd(['sed', '-i', 's/^#\?PermitRootLogin.*/PermitRootLogin yes/', "$targetdir/etc/ssh/sshd_config"]);
1717
1718         if ($setup->{product} eq 'pmg') {
1719             # install initial clamav DB
1720             my $srcdir = "${proxmox_cddir}/proxmox/clamav";
1721             foreach my $fn ("main.cvd", "bytecode.cvd", "daily.cvd", "safebrowsing.cvd") {
1722                 syscmd("cp \"$srcdir/$fn\" \"$targetdir/var/lib/clamav\"") == 0 ||
1723                     die "installation of clamav db file '$fn' failed\n";
1724             }
1725             syscmd("chroot $targetdir /bin/chown clamav:clamav -R /var/lib/clamav") == 0 ||
1726                 die "unable to set owner for clamav database files\n";
1727         }
1728
1729         if ($setup->{product} eq 'pve') {
1730             # save installer settings
1731             my $ucc = uc ($country);
1732             debconfig_set($targetdir, "pve-manager pve-manager/country string $ucc\n");
1733         }
1734
1735         update_progress(0.8, 0.95, 1, "make system bootable");
1736
1737         if ($use_zfs) {
1738             syscmd("sed -i -e 's/^GRUB_CMDLINE_LINUX=.*/GRUB_CMDLINE_LINUX=\"root=ZFS=$zfspoolname\\/ROOT\\/$zfsrootvolname boot=zfs\"/' $targetdir/etc/default/grub") == 0 ||
1739                 die "unable to update /etc/default/grub\n";
1740
1741             if ($boot_type eq 'efi') {
1742                 write_config("root=ZFS=$zfspoolname/ROOT/$zfsrootvolname boot=zfs", "$targetdir/etc/kernel/cmdline");
1743             }
1744
1745         }
1746
1747         diversion_remove($targetdir, "/usr/sbin/update-grub");
1748         diversion_remove($targetdir, "/usr/sbin/update-initramfs");
1749
1750         my $kapi;
1751         foreach my $fn (<$targetdir/lib/modules/*>) {
1752             if ($fn =~ m!/(\d+\.\d+\.\d+-\d+-pve)$!) {
1753                 die "found multiple kernels\n" if defined($kapi);
1754                 $kapi = $1;
1755             }
1756         }
1757         die "unable to detect kernel version\n" if !defined($kapi);
1758
1759         if (!$opt_testmode) {
1760
1761             unlink ("$targetdir/etc/mtab");
1762             symlink ("/proc/mounts", "$targetdir/etc/mtab");
1763             syscmd("mount -n --bind /dev $targetdir/dev");
1764
1765             my $bootloader_err_list = [];
1766             eval {
1767                 syscmd("chroot $targetdir /usr/sbin/update-initramfs -c -k $kapi") == 0 ||
1768                     die "unable to install initramfs\n";
1769
1770                 my $native_4k_disk_bootable = 0;
1771                 foreach my $di (@$bootdevinfo) {
1772                     $native_4k_disk_bootable |= ($di->{logical_bsize} == 4096);
1773                 }
1774
1775                 foreach my $di (@$bootdevinfo) {
1776                     my $dev = $di->{devname};
1777                     if (!$native_4k_disk_bootable) {
1778                         eval {
1779                             syscmd("chroot $targetdir /usr/sbin/grub-install --target i386-pc --no-floppy --bootloader-id='proxmox' $dev") == 0 ||
1780                                     die "unable to install the i386-pc boot loader on '$dev'\n";
1781                         };
1782                         push @$bootloader_err_list, $@ if $@;
1783                     }
1784
1785                     eval {
1786                         if (my $esp = $di->{esp}) {
1787                             if ($use_zfs) {
1788                                 prepare_systemd_boot_esp($esp, $targetdir);
1789                             } else {
1790                                 prepare_grub_efi_boot_esp($dev, $esp, $targetdir);
1791                             }
1792                         }
1793                     };
1794                     push @$bootloader_err_list, $@ if $@;
1795                 }
1796
1797                 syscmd("chroot $targetdir /usr/sbin/update-grub") == 0 ||
1798                     die "unable to update boot loader config\n";
1799             };
1800             push @$bootloader_err_list, $@ if $@;
1801
1802             if (scalar(@$bootloader_err_list) > 0) {
1803                 $bootloader_err = "bootloader setup errors:\n";
1804                 map { $bootloader_err .= "- $_" } @$bootloader_err_list;
1805                 warn $bootloader_err;
1806             }
1807
1808             syscmd("umount $targetdir/dev");
1809         }
1810
1811         # cleanup
1812
1813         unlink "$targetdir/usr/sbin/policy-rc.d";
1814
1815         diversion_remove($targetdir, "/sbin/start-stop-daemon");
1816
1817         # set root password
1818         my $octets = encode("utf-8", $password);
1819         run_command("chroot $targetdir /usr/sbin/chpasswd", undef,
1820                     "root:$octets\n");
1821
1822         if ($setup->{product} eq 'pmg') {
1823             # save admin email
1824             write_config("section: admin\n\temail ${mailto}\n",
1825                          "$targetdir/etc/pmg/pmg.conf");
1826
1827         } elsif ($setup->{product} eq 'pve') {
1828
1829             # create pmxcfs DB
1830
1831             my $tmpdir = "$targetdir/tmp/pve";
1832             mkdir $tmpdir;
1833
1834             # write vnc keymap to datacenter.cfg
1835             my $vnckmap = $cmap->{kmap}->{$keymap}->{kvm} || 'en-us';
1836             write_config("keyboard: $vnckmap\n",
1837                          "$tmpdir/datacenter.cfg");
1838
1839             # save admin email
1840             write_config("user:root\@pam:1:0:::${mailto}::\n",
1841                          "$tmpdir/user.cfg");
1842
1843             # write storage.cfg
1844             my $storage_cfg_fn = "$tmpdir/storage.cfg";
1845             if ($use_zfs) {
1846                 write_config($storage_cfg_zfs, $storage_cfg_fn);
1847             } elsif ($use_btrfs) {
1848                 write_config($storage_cfg_btrfs, $storage_cfg_fn);
1849             } elsif ($datadev) {
1850                 write_config($storage_cfg_lvmthin, $storage_cfg_fn);
1851             } else {
1852                 write_config($storage_cfg_local, $storage_cfg_fn);
1853             }
1854
1855             run_command("chroot $targetdir /usr/bin/create_pmxcfs_db /tmp/pve /var/lib/pve-cluster/config.db");
1856
1857             syscmd("rm -rf $tmpdir");
1858         }
1859     };
1860
1861     my $err = $@;
1862
1863     update_progress(1, 0, 1, "");
1864
1865     print $err if $err;
1866
1867     if ($opt_testmode) {
1868         my $elapsed = Time::HiRes::tv_interval($starttime);
1869         print "Elapsed extract time: $elapsed\n";
1870
1871         syscmd("chroot $targetdir /usr/bin/dpkg-query -W --showformat='\${package}\n'> final.pkglist");
1872     }
1873
1874     syscmd("umount $targetdir/run");
1875     syscmd("umount $targetdir/mnt/hostrun");
1876     syscmd("umount $targetdir/tmp");
1877     syscmd("umount $targetdir/proc");
1878     syscmd("umount $targetdir/sys/firmware/efi/efivars");
1879     syscmd("umount $targetdir/sys");
1880
1881     if ($use_zfs) {
1882         syscmd("zfs umount -a") == 0 ||
1883             die "unable to unmount zfs\n";
1884     } else {
1885         syscmd("umount -d $targetdir");
1886     }
1887
1888     if (!$err && $use_zfs) {
1889         syscmd("zfs set sync=standard $zfspoolname") == 0 ||
1890             die "unable to set zfs properties\n";
1891
1892         syscmd("zfs set mountpoint=/ $zfspoolname/ROOT/$zfsrootvolname") == 0 ||
1893             die "zfs set mountpoint failed\n";
1894
1895         syscmd("zpool set bootfs=$zfspoolname/ROOT/$zfsrootvolname $zfspoolname")  == 0 ||
1896             die "zfs set bootfs failed\n";
1897         syscmd("zpool export $zfspoolname");
1898     }
1899
1900     if ($bootloader_err) {
1901         $err = $err ? "$err\n$bootloader_err" : $bootloader_err;
1902     }
1903
1904     die $err if $err;
1905 }
1906
1907 my $last_display_change = 0;
1908
1909 my $display_info_counter = 0;
1910
1911 my $display_info_items = [
1912     "extract1-license.htm",
1913     "extract2-rulesystem.htm",
1914     "extract3-spam.htm",
1915     "extract4-virus.htm",
1916     ];
1917
1918 sub display_info {
1919
1920     my $min_display_time = 15;
1921
1922     my $ctime = time();
1923
1924     return if ($ctime - $last_display_change) < $min_display_time;
1925
1926     my $page = $display_info_items->[$display_info_counter % scalar(@$display_info_items)];
1927
1928     $display_info_counter++;
1929
1930     display_html($page);
1931 }
1932
1933 sub display_html {
1934     my ($filename) = @_;
1935
1936     $filename = $steps[$step_number]->{html} if !$filename;
1937
1938     my $path = "${proxmox_libdir}/html/$filename";
1939
1940     my $url = "file://$path";
1941
1942     my $data = file_get_contents($path);
1943
1944     if ($filename eq 'license.htm') {
1945         my $license = eval { decode('utf8', file_get_contents("${proxmox_cddir}/EULA")) };
1946         if (my $err = $@) {
1947             die $err if !$opt_testmode;
1948             $license = "TESTMODE: Ignore non existent EULA...\n";
1949         }
1950         my $title = "END USER LICENSE AGREEMENT (EULA)";
1951         $data =~ s/__LICENSE__/$license/;
1952         $data =~ s/__LICENSE_TITLE__/$title/;
1953     } elsif ($filename eq 'success.htm') {
1954         my $addr = $ipversion == 6 ? "[${ipaddress}]" : "$ipaddress";
1955         $data =~ s/\@IPADDR\@/$addr/;
1956     }
1957
1958     $htmlview->load_html($data, $url);
1959
1960     $last_display_change = time();
1961 }
1962
1963 sub prev_function {
1964
1965     my ($text, $fctn) = @_;
1966
1967     $fctn = $step_number if !$fctn;
1968     $text = "_Previous" if !$text;
1969     $prev_btn->set_label ($text);
1970
1971     $step_number--;
1972     $steps[$step_number]->{function}();
1973
1974     $prev_btn->grab_focus();
1975 }
1976
1977 sub set_next {
1978     my ($text, $fctn) = @_;
1979
1980     $next_fctn = $fctn;
1981     my $step = $steps[$step_number];
1982     $text //= $steps[$step_number]->{next_button} // '_Next';
1983     $next->set_label($text);
1984
1985     $next->grab_focus();
1986 }
1987
1988 sub create_main_window {
1989
1990     $window = Gtk3::Window->new();
1991     $window->set_default_size(1024, 768);
1992     $window->set_has_resize_grip(0);
1993     $window->set_decorated(0) if !$opt_testmode;
1994
1995     my $vbox = Gtk3::VBox->new(0, 0);
1996
1997     my $logofn = "$setup->{product}-banner.png";
1998     my $image = Gtk3::Image->new_from_file("${proxmox_libdir}/$logofn");
1999     $vbox->pack_start($image, 0, 0, 0);
2000
2001     my $hbox = Gtk3::HBox->new(0, 0);
2002     $vbox->pack_start($hbox, 1, 1, 0);
2003
2004  #  my $f1 = Gtk3::Frame->new ('test');
2005  #  $f1->set_shadow_type ('none');
2006  #  $hbox->pack_start ($f1, 1, 1, 0);
2007
2008     my $sep1 = Gtk3::HSeparator->new();
2009     $vbox->pack_start($sep1, 0, 0, 0);
2010
2011     $cmdbox = Gtk3::HBox->new();
2012     $vbox->pack_start($cmdbox, 0, 0, 10);
2013
2014     $next = Gtk3::Button->new('_Next');
2015     $next->signal_connect(clicked => sub { $last_display_change = 0; &$next_fctn (); });
2016     $cmdbox->pack_end($next, 0, 0, 10);
2017
2018
2019     $prev_btn = Gtk3::Button->new('_Previous');
2020     $prev_btn->signal_connect(clicked => sub { $last_display_change = 0; &prev_function (); });
2021     $cmdbox->pack_end($prev_btn, 0, 0, 10);
2022
2023
2024     my $abort = Gtk3::Button->new('_Abort');
2025     $abort->set_can_focus(0);
2026     $cmdbox->pack_start($abort, 0, 0, 10);
2027     $abort->signal_connect(clicked => sub { exit (-1); });
2028
2029     my $vbox2 = Gtk3::VBox->new(0, 0);
2030     $hbox->add($vbox2);
2031
2032     $htmlview = Gtk3::WebKit2::WebView->new();
2033     my $scrolls = Gtk3::ScrolledWindow->new();
2034     $scrolls->add($htmlview);
2035
2036     my $hbox2 = Gtk3::HBox->new(0, 0);
2037     $hbox2->pack_start($scrolls, 1, 1, 0);
2038
2039     $vbox2->pack_start($hbox2, 1, 1, 0);
2040
2041     my $vbox3 = Gtk3::VBox->new(0, 0);
2042     $vbox2->pack_start($vbox3, 0, 0, 0);
2043
2044     my $sep2 = Gtk3::HSeparator->new;
2045     $vbox3->pack_start($sep2, 0, 0, 0);
2046
2047     $inbox = Gtk3::HBox->new(0, 0);
2048     $vbox3->pack_start($inbox, 0, 0, 0);
2049
2050     $window->add($vbox);
2051
2052     $window->show_all;
2053     $window->realize();
2054 }
2055
2056 sub cleanup_view {
2057     $inbox->foreach(sub {
2058         my $child = shift;
2059         $inbox->remove ($child);
2060     });
2061 }
2062
2063 # fixme: newer GTK3 has special properties to handle numbers with Entry
2064 # only allow floating point numbers with Gtk3::Entry
2065
2066 sub check_float {
2067     my ($entry, $event) = @_;
2068
2069     return check_number($entry, $event, 1);
2070 }
2071
2072 sub check_int {
2073     my ($entry, $event) = @_;
2074
2075     return check_number($entry, $event, 0);
2076 }
2077
2078 sub check_number {
2079     my ($entry, $event, $float) = @_;
2080
2081     my $val = $event->get_keyval;
2082
2083     if (($float && $val == ord '.') ||
2084         $val == Gtk3::Gdk::KEY_ISO_Left_Tab ||
2085         $val == Gtk3::Gdk::KEY_Shift_L ||
2086         $val == Gtk3::Gdk::KEY_Tab ||
2087         $val == Gtk3::Gdk::KEY_Left ||
2088         $val == Gtk3::Gdk::KEY_Right ||
2089         $val == Gtk3::Gdk::KEY_BackSpace ||
2090         $val == Gtk3::Gdk::KEY_Delete ||
2091         ($val >= ord '0' && $val <= ord '9') ||
2092         ($val >= Gtk3::Gdk::KEY_KP_0 &&
2093          $val <= Gtk3::Gdk::KEY_KP_9)) {
2094         return undef;
2095     }
2096
2097     return 1;
2098 }
2099
2100 sub create_text_input {
2101     my ($default, $text) = @_;
2102
2103     my $hbox = Gtk3::HBox->new(0, 0);
2104
2105     my $label = Gtk3::Label->new($text);
2106     $label->set_size_request(150, -1);
2107     $label->set_alignment(1, 0.5);
2108     $hbox->pack_start($label, 0, 0, 10);
2109     my $e1 = Gtk3::Entry->new();
2110     $e1->set_width_chars(30);
2111     $hbox->pack_start($e1, 0, 0, 0);
2112     $e1->set_text($default);
2113
2114     return ($hbox, $e1);
2115 }
2116
2117 sub get_ip_config {
2118
2119     my $ifaces = {};
2120     my $default;
2121
2122     my $links = `ip -o l`;
2123     foreach my $l (split /\n/,$links) {
2124         my ($index, $name, $flags, $state, $mac) = $l =~ m/^(\d+):\s+(\S+):\s+<(\S+)>.*\s+state\s+(\S+)\s+.*\s+link\/ether\s+(\S+)\s+/;
2125         next if !$name || $name eq 'lo';
2126
2127         my $driver = readlink "/sys/class/net/$name/device/driver" || 'unknown';
2128         $driver =~ s!^.*/!!;
2129
2130         $ifaces->{"$index"} = {
2131             name => $name,
2132             driver => $driver,
2133             flags => $flags,
2134             state => $state,
2135             mac => $mac,
2136         };
2137
2138         my $addresses = `ip -o a s $name`;
2139         foreach my $a (split /\n/,$addresses) {
2140             my ($family, $ip, $prefix) = $a =~ m/^\Q$index\E:\s+\Q$name\E\s+(inet|inet6)\s+($IPRE)\/(\d+)\s+/;
2141             next if !$ip;
2142             next if $a =~ /scope\s+link/; # ignore link local
2143
2144             my $mask = $prefix;
2145
2146             if ($family eq 'inet') {
2147                 next if !$ip =~ /$IPV4RE/;
2148                 next if $prefix < 8 || $prefix > 32;
2149                 $mask = @$ipv4_reverse_mask[$prefix];
2150             } else {
2151                 next if !$ip =~ /$IPV6RE/;
2152             }
2153
2154             $default = $index if !$default;
2155
2156             $ifaces->{"$index"}->{"$family"} = {
2157                 mask => $mask,
2158                 addr => $ip,
2159             };
2160         }
2161     }
2162
2163
2164     my $route = `ip route`;
2165     my ($gateway) = $route =~ m/^default\s+via\s+(\S+)\s+/m;
2166
2167     my $resolvconf = `cat /etc/resolv.conf`;
2168     my ($dnsserver) = $resolvconf =~ m/^nameserver\s+(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/m;
2169     my ($domain) = $resolvconf =~ m/^domain\s+(\S+)$/m;
2170
2171     return {
2172         default => $default,
2173         ifaces => $ifaces,
2174         gateway => $gateway,
2175         dnsserver => $dnsserver,
2176         domain => $domain,
2177     }
2178 }
2179
2180 sub display_message {
2181     my ($msg) = @_;
2182
2183     my $dialog = Gtk3::MessageDialog->new($window, 'modal',
2184                                           'info', 'ok', $msg);
2185     $dialog->run();
2186     $dialog->destroy();
2187 }
2188
2189 sub display_error {
2190     my ($msg) = @_;
2191
2192     my $dialog = Gtk3::MessageDialog->new($window, 'modal',
2193                                           'error', 'ok', $msg);
2194     $dialog->run();
2195     $dialog->destroy();
2196 }
2197
2198 my $ipconf_first_view = 1;
2199
2200 sub create_ipconf_view {
2201
2202     cleanup_view();
2203     display_html();
2204
2205     my $vbox =  Gtk3::VBox->new(0, 0);
2206     $inbox->pack_start($vbox, 1, 0, 0);
2207     my $hbox =  Gtk3::HBox->new(0, 0);
2208     $vbox->pack_start($hbox, 0, 0, 10);
2209     my $vbox2 =  Gtk3::VBox->new(0, 0);
2210     $hbox->add($vbox2);
2211
2212     my $ipaddr_text = $config->{ipaddress} // "192.168.100.2";
2213     my $ipbox;
2214     ($ipbox, $ipconf_entry_addr) =
2215         create_text_input($ipaddr_text, 'IP Address:');
2216
2217     my $netmask_text = $config->{netmask} // "255.255.255.0";
2218     my $maskbox;
2219     ($maskbox, $ipconf_entry_mask) =
2220         create_text_input($netmask_text, 'Netmask:');
2221
2222     my $device_cb = Gtk3::ComboBoxText->new();
2223     $device_cb->set_active(0);
2224     $device_cb->set_visible(1);
2225
2226     my $get_device_desc = sub {
2227         my $iface = shift;
2228         return "$iface->{name} - $iface->{mac} ($iface->{driver})";
2229     };
2230
2231     my $device_active_map = {};
2232     my $device_active_reverse_map = {};
2233
2234     my $device_change_handler = sub {
2235         my $current = shift;
2236
2237         my $new = $device_active_map->{$current->get_active()};
2238         return if defined($ipconf->{selected}) && $new eq $ipconf->{selected};
2239
2240         $ipconf->{selected} = $new;
2241         my $iface = $ipconf->{ifaces}->{$ipconf->{selected}};
2242         $config->{mngmt_nic} = $iface->{name};
2243         $ipconf_entry_addr->set_text($iface->{inet}->{addr} || $iface->{inet6}->{addr})
2244             if $iface->{inet}->{addr} || $iface->{inet6}->{addr};
2245         $ipconf_entry_mask->set_text($iface->{inet}->{mask} || $iface->{inet6}->{mask})
2246             if $iface->{inet}->{mask} || $iface->{inet6}->{mask};
2247     };
2248
2249     my $i = 0;
2250     foreach my $index (sort keys %{$ipconf->{ifaces}}) {
2251         $device_cb->append_text(&$get_device_desc($ipconf->{ifaces}->{$index}));
2252         $device_active_map->{$i} = $index;
2253         $device_active_reverse_map->{$ipconf->{ifaces}->{$index}->{name}} = $i;
2254         if ($ipconf_first_view && $index == $ipconf->{default}) {
2255             $device_cb->set_active($i);
2256             &$device_change_handler($device_cb);
2257             $ipconf_first_view = 0;
2258         }
2259         $device_cb->signal_connect('changed' => $device_change_handler);
2260         $i++;
2261     }
2262
2263     if (my $nic = $config->{mngmt_nic}) {
2264         $device_cb->set_active($device_active_reverse_map->{$nic} // 0);
2265     } else {
2266         $device_cb->set_active(0);
2267     }
2268
2269     my $devicebox = Gtk3::HBox->new(0, 0);
2270     my $label = Gtk3::Label->new("Management Interface:");
2271     $label->set_size_request(150, -1);
2272     $label->set_alignment(1, 0.5);
2273     $devicebox->pack_start($label, 0, 0, 10);
2274     $devicebox->pack_start($device_cb, 0, 0, 0);
2275
2276     $vbox2->pack_start($devicebox, 0, 0, 2);
2277
2278     my $hn = $config->{fqdn} //  "$setup->{product}." . ($ipconf->{domain} // "example.invalid");
2279
2280     my ($hostbox, $hostentry) =
2281         create_text_input($hn, 'Hostname (FQDN):');
2282     $vbox2->pack_start($hostbox, 0, 0, 2);
2283
2284     $vbox2->pack_start($ipbox, 0, 0, 2);
2285
2286     $vbox2->pack_start($maskbox, 0, 0, 2);
2287
2288     $gateway = $config->{gateway} // $ipconf->{gateway} || '192.168.100.1';
2289
2290     my $gwbox;
2291     ($gwbox, $ipconf_entry_gw) =
2292         create_text_input($gateway, 'Gateway:');
2293
2294     $vbox2->pack_start($gwbox, 0, 0, 2);
2295
2296     $dnsserver = $config->{dnsserver} // $ipconf->{dnsserver} || $gateway;
2297
2298     my $dnsbox;
2299     ($dnsbox, $ipconf_entry_dns) =
2300         create_text_input($dnsserver, 'DNS Server:');
2301
2302     $vbox2->pack_start($dnsbox, 0, 0, 0);
2303
2304     $inbox->show_all;
2305     set_next(undef, sub {
2306
2307         # verify hostname
2308
2309         my $text = $hostentry->get_text();
2310
2311         $text =~ s/^\s+//;
2312         $text =~ s/\s+$//;
2313
2314         $config->{fqdn} = $text;
2315
2316         my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
2317
2318         # Debian does not support purely numeric hostnames
2319         if ($text && $text =~ /^[0-9]+(?:\.|$)/) {
2320             display_message("Purely numeric hostnames are not allowed.");
2321             $hostentry->grab_focus();
2322             return;
2323         }
2324
2325         if ($text && $text =~ m/^(${namere}\.)*${namere}$/ && $text !~ m/.example.invalid$/ &&
2326             $text =~ m/^([^\.]+)\.(\S+)$/) {
2327             $hostname = $1;
2328             $domain = $2;
2329         } else {
2330             display_message("Hostname does not look like a fully qualified domain name.");
2331             $hostentry->grab_focus();
2332             return;
2333         }
2334
2335         # verify ip address
2336
2337         $text = $ipconf_entry_addr->get_text();
2338         $text =~ s/^\s+//;
2339         $text =~ s/\s+$//;
2340         if ($text =~ m!^($IPV4RE)$!) {
2341             $ipaddress = $text;
2342             $ipversion = 4;
2343         } elsif ($text =~ m!^($IPV6RE)$!) {
2344             $ipaddress = $text;
2345             $ipversion = 6;
2346         } else {
2347             display_message("IP address is not valid.");
2348             $ipconf_entry_addr->grab_focus();
2349             return;
2350         }
2351         $config->{ipaddress} = $ipaddress;
2352
2353         $text = $ipconf_entry_mask->get_text();
2354         $text =~ s/^\s+//;
2355         $text =~ s/\s+$//;
2356         if (($ipversion == 6) && ($text =~ m/^(\d+)$/) && ($1 >= 8) && ($1 <= 126)) {
2357             $netmask = $text;
2358         } elsif (($ipversion == 4) && defined($ipv4_mask_hash->{$text})) {
2359             $netmask = $text;
2360         } else {
2361             display_message("Netmask is not valid.");
2362             $ipconf_entry_mask->grab_focus();
2363             return;
2364         }
2365         $config->{netmask} = $netmask;
2366
2367         $text = $ipconf_entry_gw->get_text();
2368         $text =~ s/^\s+//;
2369         $text =~ s/\s+$//;
2370         if (($ipversion == 4) && ($text =~ m!^($IPV4RE)$!)) {
2371             $gateway = $text;
2372         } elsif (($ipversion == 6) && ($text =~ m!^($IPV6RE)$!)) {
2373             $gateway = $text;
2374         } else {
2375             display_message("Gateway is not valid.");
2376             $ipconf_entry_gw->grab_focus();
2377             return;
2378         }
2379         $config->{gateway} = $gateway;
2380
2381         $text = $ipconf_entry_dns->get_text();
2382         $text =~ s/^\s+//;
2383         $text =~ s/\s+$//;
2384         if (($ipversion == 4) && ($text =~ m!^($IPV4RE)$!)) {
2385             $dnsserver = $text;
2386         } elsif (($ipversion == 6) && ($text =~ m!^($IPV6RE)$!)) {
2387             $dnsserver = $text;
2388         } else {
2389             display_message("DNS server is not valid.");
2390             $ipconf_entry_dns->grab_focus();
2391             return;
2392         }
2393         $config->{dnsserver} = $dnsserver;
2394
2395         #print "TEST $ipaddress $netmask $gateway $dnsserver\n";
2396
2397         $step_number++;
2398         create_ack_view();
2399     });
2400
2401     $hostentry->grab_focus();
2402 }
2403
2404 sub create_ack_view {
2405
2406     cleanup_view();
2407
2408     my $ack_template = "${proxmox_libdir}/html/ack_template.htm";
2409     my $ack_html = "${proxmox_libdir}/html/$steps[$step_number]->{html}";
2410     my $html_data = file_get_contents($ack_template);
2411
2412     my %config_values = (
2413         __target_hd__ => join(' | ', @{$config_options->{target_hds}}),
2414         __target_fs__ => $config_options->{filesys},
2415         __country__ => $cmap->{country}->{$country}->{name},
2416         __timezone__ => $timezone,
2417         __keymap__ => $keymap,
2418         __mailto__ => $mailto,
2419         __interface__ => $ipconf->{ifaces}->{$ipconf->{selected}}->{name},
2420         __hostname__ => $hostname,
2421         __ip__ => $ipaddress,
2422         __netmask__ => $netmask,
2423         __gateway__ => $gateway,
2424         __dnsserver__ => $dnsserver,
2425     );
2426
2427     while ( my ($k, $v) = each %config_values) {
2428         $html_data =~ s/$k/$v/g;
2429     }
2430
2431     write_config($html_data, $ack_html);
2432
2433     display_html();
2434
2435     set_next(undef, sub {
2436         $step_number++;
2437         create_extract_view();
2438     });
2439 }
2440
2441 sub get_device_desc {
2442     my ($devname, $size, $model) = @_;
2443
2444     if ($size && ($size > 0)) {
2445         $size = int($size/2048); # size in MB, from 512B "sectors"
2446
2447         my $text = "$devname (";
2448         if ($size >= 1024) {
2449             $size = int($size/1024); # size in GB
2450             $text .= "${size}GB";
2451         } else {
2452             $text .= "${size}MB";
2453         }
2454
2455         $text .= ", $model" if $model;
2456         $text .= ")";
2457
2458     } else {
2459         return $devname;
2460     }
2461 }
2462
2463 sub update_layout {
2464     my ($cb, $kmap) = @_;
2465
2466     my $ind;
2467     my $def;
2468     my $i = 0;
2469     my $kmaphash = $cmap->{kmaphash};
2470     foreach my $layout (sort keys %$kmaphash) {
2471         $def = $i if $kmaphash->{$layout} eq 'en-us';
2472         $ind = $i if $kmap && $kmaphash->{$layout} eq $kmap;
2473         $i++;
2474     }
2475
2476     $cb->set_active($ind || $def || 0);
2477 }
2478
2479 my $lastzonecb;
2480 sub update_zonelist {
2481     my ($box, $cc) = @_;
2482
2483     my $cczones = $cmap->{cczones};
2484     my $zones = $cmap->{zones};
2485
2486     my $sel;
2487     if ($lastzonecb) {
2488         $sel = $lastzonecb->get_active_text();
2489         $box->remove ($lastzonecb);
2490     } else {
2491         $sel = $timezone; # used once to select default
2492     }
2493
2494     my $cb = $lastzonecb = Gtk3::ComboBoxText->new();
2495     $cb->set_size_request(200, -1);
2496
2497     $cb->signal_connect('changed' => sub {
2498         $timezone = $cb->get_active_text();
2499     });
2500
2501     my @za;
2502     if ($cc && defined ($cczones->{$cc})) {
2503         @za = keys %{$cczones->{$cc}};
2504     } else {
2505         @za = keys %$zones;
2506     }
2507     my $ind;
2508     my $i = 0;
2509     foreach my $zone (sort @za) {
2510         $ind = $i if $sel && $zone eq $sel;
2511         $cb->append_text($zone);
2512         $i++;
2513     }
2514
2515     $cb->set_active($ind || 0);
2516
2517     $cb->show;
2518     $box->pack_start($cb, 0, 0, 0);
2519 }
2520
2521 sub create_password_view {
2522
2523     cleanup_view();
2524
2525     my $vbox2 =  Gtk3::VBox->new(0, 0);
2526     $inbox->pack_start($vbox2, 1, 0, 0);
2527     my $vbox =  Gtk3::VBox->new(0, 0);
2528     $vbox2->pack_start($vbox, 0, 0, 10);
2529
2530     my $hbox1 = Gtk3::HBox->new(0, 0);
2531     my $label = Gtk3::Label->new("Password");
2532     $label->set_size_request(150, -1);
2533     $label->set_alignment(1, 0.5);
2534     $hbox1->pack_start($label, 0, 0, 10);
2535     my $pwe1 = Gtk3::Entry->new();
2536     $pwe1->set_visibility(0);
2537     $pwe1->set_text($password) if $password;
2538     $pwe1->set_size_request(200, -1);
2539     $hbox1->pack_start($pwe1, 0, 0, 0);
2540
2541     my $hbox2 = Gtk3::HBox->new(0, 0);
2542     $label = Gtk3::Label->new("Confirm");
2543     $label->set_size_request(150, -1);
2544     $label->set_alignment(1, 0.5);
2545     $hbox2->pack_start($label, 0, 0, 10);
2546     my $pwe2 = Gtk3::Entry->new();
2547     $pwe2->set_visibility(0);
2548     $pwe2->set_text($password) if $password;
2549     $pwe2->set_size_request(200, -1);
2550     $hbox2->pack_start($pwe2, 0, 0, 0);
2551
2552     my $hbox3 = Gtk3::HBox->new(0, 0);
2553     $label = Gtk3::Label->new("E-Mail");
2554     $label->set_size_request(150, -1);
2555     $label->set_alignment(1, 0.5);
2556     $hbox3->pack_start($label, 0, 0, 10);
2557     my $eme = Gtk3::Entry->new();
2558     $eme->set_size_request(200, -1);
2559     $eme->set_text($mailto);
2560     $hbox3->pack_start($eme, 0, 0, 0);
2561
2562
2563     $vbox->pack_start($hbox1, 0, 0, 5);
2564     $vbox->pack_start($hbox2, 0, 0, 5);
2565     $vbox->pack_start($hbox3, 0, 0, 15);
2566
2567     $inbox->show_all;
2568
2569     display_html();
2570
2571     set_next (undef,  sub {
2572
2573         my $t1 = $pwe1->get_text;
2574         my $t2 = $pwe2->get_text;
2575
2576         if (length ($t1) < 5) {
2577             display_message("Password is too short.");
2578             $pwe1->grab_focus();
2579             return;
2580         }
2581
2582         if ($t1 ne $t2) {
2583             display_message("Password does not match.");
2584             $pwe1->grab_focus();
2585             return;
2586         }
2587
2588         my $t3 = $eme->get_text;
2589         if ($t3 !~ m/^[\w\+\-\~]+(\.[\w\+\-\~]+)*@[a-zA-Z0-9\-]+(\.[a-zA-Z0-9\-]+)*$/) {
2590             display_message("E-Mail does not look like a valid address" .
2591                              " (user\@domain.tld)");
2592             $eme->grab_focus();
2593             return;
2594         }
2595
2596         if ($t3 eq 'mail@example.invalid') {
2597             display_message("Please enter a valid E-Mail address");
2598             $eme->grab_focus();
2599             return;
2600         }
2601
2602         $password = $t1;
2603         $mailto = $t3;
2604
2605         $step_number++;
2606         create_ipconf_view();
2607     });
2608
2609     $pwe1->grab_focus();
2610
2611 }
2612
2613 sub create_country_view {
2614
2615     cleanup_view();
2616
2617     my $countryhash = $cmap->{countryhash};
2618     my $ctr = $cmap->{country};
2619
2620     my $vbox2 =  Gtk3::VBox->new(0, 0);
2621     $inbox->pack_start($vbox2, 1, 0, 0);
2622     my $vbox =  Gtk3::VBox->new(0, 0);
2623     $vbox2->pack_start($vbox, 0, 0, 10);
2624
2625     my $w = Gtk3::Entry->new();
2626     $w->set_size_request(200, -1);
2627
2628     my $c = Gtk3::EntryCompletion->new();
2629     $c->set_text_column(0);
2630     $c->set_minimum_key_length(0);
2631     $c->set_popup_set_width(1);
2632     $c->set_inline_completion(1);
2633
2634     my $hbox2 = Gtk3::HBox->new(0, 0);
2635     my $label = Gtk3::Label->new("Time zone");
2636     $label->set_size_request(150, -1);
2637     $label->set_alignment(1, 0.5);
2638     $hbox2->pack_start($label, 0, 0, 10);
2639     update_zonelist ($hbox2);
2640
2641     my $hbox3 = Gtk3::HBox->new(0, 0);
2642     $label = Gtk3::Label->new("Keyboard Layout");
2643     $label->set_size_request(150, -1);
2644     $label->set_alignment(1, 0.5);
2645     $hbox3->pack_start($label, 0, 0, 10);
2646
2647     my $kmapcb = Gtk3::ComboBoxText->new();
2648     $kmapcb->set_size_request (200, -1);
2649     foreach my $layout (sort keys %{$cmap->{kmaphash}}) {
2650         $kmapcb->append_text ($layout);
2651     }
2652
2653     update_layout($kmapcb);
2654     $hbox3->pack_start ($kmapcb, 0, 0, 0);
2655
2656     $kmapcb->signal_connect ('changed' => sub {
2657         my $sel = $kmapcb->get_active_text();
2658         if (my $kmap = $cmap->{kmaphash}->{$sel}) {
2659             my $xkmap = $cmap->{kmap}->{$kmap}->{x11};
2660             my $xvar = $cmap->{kmap}->{$kmap}->{x11var};
2661             syscmd ("setxkbmap $xkmap $xvar") if !$opt_testmode;
2662             $keymap = $kmap;
2663         }
2664     });
2665
2666     $w->signal_connect ('changed' => sub {
2667         my ($entry, $event) = @_;
2668         my $text = $entry->get_text;
2669
2670         if (my $cc = $countryhash->{lc($text)}) {
2671             update_zonelist($hbox2, $cc);
2672             my $kmap = $ctr->{$cc}->{kmap} || 'en-us';
2673             update_layout($kmapcb, $kmap);
2674         }
2675     });
2676
2677     $w->signal_connect (key_press_event => sub {
2678         my ($entry, $event) = @_;
2679         my $text = $entry->get_text;
2680
2681         my $val = $event->get_keyval;
2682
2683         if ($val == Gtk3::Gdk::KEY_Tab) {
2684             my $cc = $countryhash->{lc($text)};
2685
2686             my $found = 0;
2687             my $compl;
2688
2689             if ($cc) {
2690                 $found = 1;
2691                 $compl = $ctr->{$cc}->{name};
2692             } else {
2693                 foreach my $cc (keys %$ctr) {
2694                     my $ct = $ctr->{$cc}->{name};
2695                     if ($ct =~ m/^\Q$text\E.*$/i) {
2696                         $found++;
2697                         $compl = $ct;
2698                     }
2699                     last if $found > 1;
2700                 }
2701             }
2702
2703             if ($found == 1) {
2704                 $entry->set_text($compl);
2705                 $c->complete();
2706                 return undef;
2707             } else {
2708                 #Gtk3::Gdk::beep();
2709                 print chr(7); # beep ?
2710             }
2711
2712             $c->complete();
2713
2714             my $buf = $w->get_buffer();
2715             $buf->insert_text(-1, '', -1); # popup selection
2716
2717             return 1;
2718         }
2719
2720         return undef;
2721     });
2722
2723     my $ls = Gtk3::ListStore->new('Glib::String');
2724     foreach my $cc (sort {$ctr->{$a}->{name} cmp $ctr->{$b}->{name} } keys %$ctr) {
2725         my $iter = $ls->append();
2726         $ls->set ($iter, 0, $ctr->{$cc}->{name});
2727     }
2728     $c->set_model ($ls);
2729
2730     $w->set_completion ($c);
2731
2732     my $hbox =  Gtk3::HBox->new(0, 0);
2733
2734     $label = Gtk3::Label->new("Country");
2735     $label->set_alignment(1, 0.5);
2736     $label->set_size_request(150, -1);
2737     $hbox->pack_start($label, 0, 0, 10);
2738     $hbox->pack_start($w, 0, 0, 0);
2739
2740     $vbox->pack_start($hbox, 0, 0, 5);
2741     $vbox->pack_start($hbox2, 0, 0, 5);
2742     $vbox->pack_start($hbox3, 0, 0, 5);
2743
2744     if ($country && $ctr->{$country}) {
2745         $w->set_text ($ctr->{$country}->{name});
2746     }
2747
2748     $inbox->show_all;
2749
2750     display_html();
2751     set_next (undef,  sub {
2752
2753         my $text = $w->get_text;
2754
2755         if (my $cc = $countryhash->{lc($text)}) {
2756             $country = $cc;
2757             $step_number++;
2758             create_password_view();
2759             return;
2760         } else {
2761             display_message("Please select a country first.");
2762             $w->grab_focus();
2763         }
2764     });
2765
2766     $w->grab_focus();
2767 }
2768
2769 my $target_hd_combo;
2770 my $target_hd_label;
2771
2772 my $hdoption_first_setup = 1;
2773
2774 my $create_basic_grid = sub {
2775     my $grid =  Gtk3::Grid->new();
2776     $grid->set_visible(1);
2777     $grid->set_column_spacing(10);
2778     $grid->set_row_spacing(10);
2779     $grid->set_hexpand(1);
2780
2781     $grid->set_margin_start(5);
2782     $grid->set_margin_end(5);
2783     $grid->set_margin_top(5);
2784     $grid->set_margin_bottom(5);
2785
2786     return $grid;
2787 };
2788
2789 my $create_label_widget_grid = sub {
2790     my ($labeled_widgets) = @_;
2791
2792     my $grid = &$create_basic_grid();
2793     my $row = 0;
2794
2795     for (my $i = 0; $i < @$labeled_widgets; $i += 2) {
2796         my $widget = @$labeled_widgets[$i+1];
2797         my $label = Gtk3::Label->new(@$labeled_widgets[$i]);
2798         $label->set_visible(1);
2799         $label->set_alignment (1, 0.5);
2800         $grid->attach($label, 0, $row, 1, 1);
2801         $widget->set_visible(1);
2802         $grid->attach($widget, 1, $row, 1, 1);
2803         $row++;
2804     }
2805
2806     return $grid;
2807 };
2808
2809 my $create_raid_disk_grid = sub {
2810     my $disk_labeled_widgets = [];
2811     for (my $i = 0; $i < @$hds; $i++) {
2812         my $disk_selector = Gtk3::ComboBoxText->new();
2813         $disk_selector->append_text("-- do not use --");
2814         $disk_selector->set_active(0);
2815         $disk_selector->set_visible(1);
2816         foreach my $hd (@$hds) {
2817             my ($disk, $devname, $size, $model, $logical_bsize) = @$hd;
2818             $disk_selector->append_text(get_device_desc ($devname, $size, $model));
2819             $disk_selector->{pve_disk_id} = $i;
2820             $disk_selector->signal_connect (changed => sub {
2821                 my $w = shift;
2822                 my $diskid = $w->{pve_disk_id};
2823                 my $a = $w->get_active - 1;
2824                 $config_options->{"disksel${diskid}"} = ($a >= 0) ? $hds->[$a] : undef;
2825             });
2826         }
2827
2828         if ($hdoption_first_setup) {
2829             $disk_selector->set_active ($i+1) if $hds->[$i];
2830         } else {
2831             my $hdind = 0;
2832             if (my $cur_hd = $config_options->{"disksel$i"}) {
2833                 foreach my $hd (@$hds) {
2834                     if (@$hd[1] eq @$cur_hd[1]) {
2835                         $disk_selector->set_active($hdind+1);
2836                         last;
2837                     }
2838                     $hdind++;
2839                 }
2840             }
2841         }
2842
2843         push @$disk_labeled_widgets, "Harddisk $i", $disk_selector;
2844     }
2845
2846     my $scrolled_window = Gtk3::ScrolledWindow->new();
2847     $scrolled_window->set_hexpand(1);
2848     $scrolled_window->set_propagate_natural_height(1) if @$hds > 4;
2849     $scrolled_window->add(&$create_label_widget_grid($disk_labeled_widgets));
2850     $scrolled_window->set_policy('never', 'automatic');
2851
2852     return $scrolled_window;
2853 #    &$create_label_widget_grid($disk_labeled_widgets)
2854 };
2855
2856 # shared between different ui parts (e.g., ZFS and "normal" single disk FS)
2857 my $hdsize_size_adj;
2858 my $hdsize_entry_buffer;
2859
2860 my $get_hdsize_spinbtn = sub {
2861     my $hdsize = shift;
2862
2863     $hdsize_entry_buffer //= Gtk3::EntryBuffer->new(undef, 1);
2864
2865     if (defined($hdsize)) {
2866         $hdsize_size_adj = Gtk3::Adjustment->new($config_options->{hdsize} || $hdsize, 0, $hdsize+1, 1, 1, 1);
2867     } else {
2868         die "called get_hdsize_spinbtn with \$hdsize_size_adj not defined but did not pass hdsize!\n"
2869             if !defined($hdsize_size_adj);
2870     }
2871
2872     my $spinbutton_hdsize = Gtk3::SpinButton->new($hdsize_size_adj, 1, 1);
2873     $spinbutton_hdsize->set_buffer($hdsize_entry_buffer);
2874     $spinbutton_hdsize->set_adjustment($hdsize_size_adj);
2875     $spinbutton_hdsize->set_tooltip_text("only use specified size (GB) of the harddisk (rest left unpartitioned)");
2876     return $spinbutton_hdsize;
2877 };
2878
2879 my $create_raid_advanced_grid = sub {
2880     my $labeled_widgets = [];
2881     my $spinbutton_ashift = Gtk3::SpinButton->new_with_range(9,13,1);
2882     $spinbutton_ashift->set_tooltip_text("zpool ashift property (pool sector size, default 2^12)");
2883     $spinbutton_ashift->signal_connect ("value-changed" => sub {
2884         my $w = shift;
2885         $config_options->{ashift} = $w->get_value_as_int();
2886     });
2887     $config_options->{ashift} = 12 if ! defined($config_options->{ashift});
2888     $spinbutton_ashift->set_value($config_options->{ashift});
2889     push @$labeled_widgets, "ashift";
2890     push @$labeled_widgets, $spinbutton_ashift;
2891
2892     my $combo_compress = Gtk3::ComboBoxText->new();
2893     $combo_compress->set_tooltip_text("zfs compression algorithm for rpool dataset");
2894     # note: gzip / lze not allowed for bootfs vdevs
2895     my $comp_opts = ["on","off","lzjb","lz4"];
2896     foreach my $opt (@$comp_opts) {
2897         $combo_compress->append($opt, $opt);
2898     }
2899     $config_options->{compress} = "on" if !defined($config_options->{compress});
2900     $combo_compress->set_active_id($config_options->{compress});
2901     $combo_compress->signal_connect (changed => sub {
2902         my $w = shift;
2903         $config_options->{compress} = $w->get_active_text();
2904     });
2905     push @$labeled_widgets, "compress";
2906     push @$labeled_widgets, $combo_compress;
2907
2908     my $combo_checksum = Gtk3::ComboBoxText->new();
2909     $combo_checksum->set_tooltip_text("zfs checksum algorithm for rpool dataset");
2910     my $csum_opts = ["on", "off","fletcher2", "fletcher4", "sha256"];
2911     foreach my $opt (@$csum_opts) {
2912         $combo_checksum->append($opt, $opt);
2913     }
2914     $config_options->{checksum} = "on" if !($config_options->{checksum});
2915     $combo_checksum->set_active_id($config_options->{checksum});
2916     $combo_checksum->signal_connect (changed => sub {
2917         my $w = shift;
2918         $config_options->{checksum} = $w->get_active_text();
2919     });
2920     push @$labeled_widgets, "checksum";
2921     push @$labeled_widgets, $combo_checksum;
2922
2923     my $spinbutton_copies = Gtk3::SpinButton->new_with_range(1,3,1);
2924     $spinbutton_copies->set_tooltip_text("zfs copies property for rpool dataset (in addition to RAID redundancy!)");
2925     $spinbutton_copies->signal_connect ("value-changed" => sub {
2926         my $w = shift;
2927         $config_options->{copies} = $w->get_value_as_int();
2928     });
2929     $config_options->{copies} = 1 if !defined($config_options->{copies});
2930     $spinbutton_copies->set_value($config_options->{copies});
2931     push @$labeled_widgets, "copies", $spinbutton_copies;
2932
2933     push @$labeled_widgets, "hdsize", $get_hdsize_spinbtn->();
2934     return &$create_label_widget_grid($labeled_widgets);;
2935 };
2936
2937 sub create_hdoption_view {
2938
2939     my $dialog = Gtk3::Dialog->new();
2940
2941     $dialog->set_title("Harddisk options");
2942
2943     $dialog->add_button("_OK", 1);
2944
2945     my $contarea = $dialog->get_content_area();
2946
2947     my $hbox2 =  Gtk3::Box->new('horizontal', 0);
2948     $contarea->pack_start($hbox2, 1, 1, 10);
2949
2950     my $grid =  Gtk3::Grid->new();
2951     $grid->set_column_spacing(10);
2952     $grid->set_row_spacing(10);
2953
2954     $hbox2->pack_start($grid, 1, 0, 10);
2955
2956     my $row = 0;
2957
2958     # Filesystem type
2959
2960     my $label0 = Gtk3::Label->new("Filesystem");
2961     $label0->set_alignment (1, 0.5);
2962     $grid->attach($label0, 0, $row, 1, 1);
2963
2964     my $fstypecb = Gtk3::ComboBoxText->new();
2965
2966     my $fstype = ['ext3', 'ext4', 'xfs',
2967                   'zfs (RAID0)', 'zfs (RAID1)',
2968                   'zfs (RAID10)', 'zfs (RAIDZ-1)',
2969                   'zfs (RAIDZ-2)', 'zfs (RAIDZ-3)'];
2970
2971     push @$fstype, 'btrfs (RAID0)', 'btrfs (RAID1)', 'btrfs (RAID10)'
2972         if $setup->{enable_btrfs};
2973
2974     my $tcount = 0;
2975     foreach my $tmp (@$fstype) {
2976         $fstypecb->append_text($tmp);
2977         $fstypecb->set_active ($tcount)
2978             if $config_options->{filesys} eq $tmp;
2979         $tcount++;
2980     }
2981
2982     $grid->attach($fstypecb, 1, $row, 1, 1);
2983
2984     $hbox2->show_all();
2985
2986     $row++;
2987
2988     my $sep = Gtk3::HSeparator->new();
2989     $sep->set_visible(1);
2990     $grid->attach($sep, 0, $row, 2, 1);
2991     $row++;
2992
2993     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.");
2994     $hw_raid_note->set_line_wrap(1);
2995     $hw_raid_note->set_max_width_chars(30);
2996     $hw_raid_note->set_visible(0);
2997     $grid->attach($hw_raid_note, 0, $row++, 2, 1);
2998
2999     my $hdsize_labeled_widgets = [];
3000
3001     # size compute
3002     my $hdsize = 0;
3003     if ( -b $target_hd) {
3004         $hdsize = int(hd_size ($target_hd) / (1024*1024.0)); # size in GB
3005     } elsif ($target_hd) {
3006         $hdsize = int((-s $target_hd) / (1024*1024*1024.0));
3007     }
3008
3009     my $spinbutton_hdsize = $get_hdsize_spinbtn->($hdsize);
3010     push @$hdsize_labeled_widgets, "hdsize", $spinbutton_hdsize;
3011
3012     my $entry_swapsize = Gtk3::Entry->new();
3013     $entry_swapsize->set_tooltip_text("maximum SWAP size (GB)");
3014     $entry_swapsize->signal_connect (key_press_event => \&check_float);
3015     $entry_swapsize->set_text($config_options->{swapsize}) if defined($config_options->{swapsize});
3016     push @$hdsize_labeled_widgets, "swapsize", $entry_swapsize;
3017
3018     my $entry_maxroot = Gtk3::Entry->new();
3019     if ($setup->{product} eq 'pve') {
3020         $entry_maxroot->set_tooltip_text("maximum size (GB) for LVM root volume");
3021         $entry_maxroot->signal_connect (key_press_event => \&check_float);
3022         $entry_maxroot->set_text($config_options->{maxroot}) if $config_options->{maxroot};
3023         push @$hdsize_labeled_widgets, "maxroot", $entry_maxroot;
3024     }
3025
3026     my $entry_minfree = Gtk3::Entry->new();
3027     $entry_minfree->set_tooltip_text("minimum free LVM space (GB, required for LVM snapshots)");
3028     $entry_minfree->signal_connect (key_press_event => \&check_float);
3029     $entry_minfree->set_text($config_options->{minfree}) if defined($config_options->{minfree});
3030     push @$hdsize_labeled_widgets, "minfree", $entry_minfree;
3031
3032     my $entry_maxvz;
3033     if ($setup->{product} eq 'pve') {
3034         $entry_maxvz = Gtk3::Entry->new();
3035         $entry_maxvz->set_tooltip_text("maximum size (GB) for LVM data volume");
3036         $entry_maxvz->signal_connect (key_press_event => \&check_float);
3037         $entry_maxvz->set_text($config_options->{maxvz}) if defined($config_options->{maxvz});
3038         push @$hdsize_labeled_widgets, "maxvz", $entry_maxvz;
3039     }
3040
3041     my $options_stack = Gtk3::Stack->new();
3042     $options_stack->set_visible(1);
3043     $options_stack->set_hexpand(1);
3044     $options_stack->set_vexpand(1);
3045     $options_stack->add_titled(&$create_raid_disk_grid(), "raiddisk", "Disk Setup");
3046     $options_stack->add_titled(&$create_label_widget_grid($hdsize_labeled_widgets), "hdsize", "Size Options");
3047     $options_stack->add_titled(&$create_raid_advanced_grid("zfs"), "raidzfsadvanced", "Advanced Options");
3048     $options_stack->set_visible_child_name("raiddisk");
3049     my $options_stack_switcher = Gtk3::StackSwitcher->new();
3050     $options_stack_switcher->set_halign('center');
3051     $options_stack_switcher->set_stack($options_stack);
3052     $grid->attach($options_stack_switcher, 0, $row, 2, 1);
3053     $row++;
3054     $grid->attach($options_stack, 0, $row, 2, 1);
3055     $row++;
3056
3057     $hdoption_first_setup = 0;
3058
3059     my $switch_view = sub {
3060         my $raid = $config_options->{filesys} =~ m/zfs|btrfs/;
3061         my $enable_zfs_opts = $config_options->{filesys} =~ m/zfs/;
3062
3063         $target_hd_combo->set_visible(!$raid);
3064         $options_stack->get_child_by_name("hdsize")->set_visible(!$raid);
3065         $options_stack->get_child_by_name("raiddisk")->set_visible($raid);
3066         $hw_raid_note->set_visible($raid);
3067         $options_stack_switcher->set_visible($enable_zfs_opts);
3068         $options_stack->get_child_by_name("raidzfsadvanced")->set_visible($enable_zfs_opts);
3069         if ($raid) {
3070             $target_hd_label->set_text("Target: $config_options->{filesys} ");
3071             $options_stack->set_visible_child_name("raiddisk");
3072         } else {
3073             $target_hd_label->set_text("Target Harddisk: ");
3074         }
3075         my (undef, $pref_width) = $dialog->get_preferred_width();
3076         my (undef, $pref_height) = $dialog->get_preferred_height();
3077         $pref_height = 750 if $pref_height > 750;
3078         $dialog->resize($pref_width, $pref_height);
3079     };
3080
3081     &$switch_view();
3082
3083     $fstypecb->signal_connect (changed => sub {
3084         $config_options->{filesys} = $fstypecb->get_active_text();
3085         &$switch_view();
3086     });
3087
3088     my $sep2 = Gtk3::HSeparator->new();
3089     $sep2->set_visible(1);
3090     $contarea->pack_end($sep2, 1, 1, 10);
3091
3092     $dialog->show();
3093
3094     $dialog->run();
3095
3096     my $get_float = sub {
3097         my ($entry) = @_;
3098
3099         my $text = $entry->get_text();
3100         return undef if !defined($text);
3101
3102         $text =~ s/^\s+//;
3103         $text =~ s/\s+$//;
3104
3105         return undef if $text !~ m/^\d+(\.\d+)?$/;
3106
3107         return $text;
3108     };
3109
3110     my $tmp;
3111
3112     if (($tmp = &$get_float($spinbutton_hdsize)) && ($tmp != $hdsize)) {
3113         $config_options->{hdsize} = $tmp;
3114     } else {
3115         delete $config_options->{hdsize};
3116     }
3117
3118     if (defined($tmp = &$get_float($entry_swapsize))) {
3119         $config_options->{swapsize} = $tmp;
3120     } else {
3121         delete $config_options->{swapsize};
3122     }
3123
3124     if (defined($tmp = &$get_float($entry_maxroot))) {
3125         $config_options->{maxroot} = $tmp;
3126     } else {
3127         delete $config_options->{maxroot};
3128     }
3129
3130     if (defined($tmp = &$get_float($entry_minfree))) {
3131         $config_options->{minfree} = $tmp;
3132     } else {
3133         delete $config_options->{minfree};
3134     }
3135
3136     if ($entry_maxvz && defined($tmp = &$get_float($entry_maxvz))) {
3137         $config_options->{maxvz} = $tmp;
3138     } else {
3139         delete $config_options->{maxvz};
3140     }
3141
3142     $dialog->destroy();
3143 }
3144
3145 my $get_raid_devlist = sub {
3146
3147     my $dev_name_hash = {};
3148
3149     my $devlist = [];
3150     for (my $i = 0; $i < @$hds; $i++) {
3151         if (my $hd = $config_options->{"disksel$i"}) {
3152             my ($disk, $devname, $size, $model, $logical_bsize) = @$hd;
3153             die "device '$devname' is used more than once\n"
3154                 if $dev_name_hash->{$devname};
3155             $dev_name_hash->{$devname} = $hd;
3156             push @$devlist, $hd;
3157         }
3158     }
3159
3160     return $devlist;
3161 };
3162
3163 sub zfs_mirror_size_check {
3164     my ($expected, $actual) = @_;
3165
3166     die "mirrored disks must have same size\n"
3167         if abs($expected - $actual) > $expected / 10;
3168 }
3169
3170 sub legacy_bios_4k_check {
3171     my ($lbs) = @_;
3172     die "Booting from 4kn drive in legacy BIOS mode is not supported.\n"
3173         if (($boot_type ne 'efi') && ($lbs == 4096));
3174 }
3175
3176 sub get_zfs_raid_setup {
3177
3178     my $filesys = $config_options->{filesys};
3179
3180     my $devlist = &$get_raid_devlist();
3181
3182     my $diskcount = scalar(@$devlist);
3183     die "$filesys needs at least one device\n" if $diskcount < 1;
3184
3185     my $bootdevlist = [];
3186
3187     my $cmd= '';
3188     if ($filesys eq 'zfs (RAID0)') {
3189         push @$bootdevlist, @$devlist[0];
3190         foreach my $hd (@$devlist) {
3191             legacy_bios_4k_check(@$hd[4]);
3192             $cmd .= " @$hd[1]";
3193         }
3194     } elsif ($filesys eq 'zfs (RAID1)') {
3195         die "zfs (RAID1) needs at least 2 device\n" if $diskcount < 2;
3196         $cmd .= ' mirror ';
3197         my $hd = @$devlist[0];
3198         my $expected_size = @$hd[2]; # all disks need approximately same size
3199         foreach $hd (@$devlist) {
3200             zfs_mirror_size_check($expected_size, @$hd[2]);
3201             legacy_bios_4k_check(@$hd[4]);
3202             $cmd .= " @$hd[1]";
3203             push @$bootdevlist, $hd;
3204         }
3205     } elsif ($filesys eq 'zfs (RAID10)') {
3206         die "zfs (RAID10) needs at least 4 device\n" if $diskcount < 4;
3207         die "zfs (RAID10) needs an even number of devices\n" if $diskcount & 1;
3208
3209         push @$bootdevlist, @$devlist[0], @$devlist[1];
3210
3211         for (my $i = 0; $i < $diskcount; $i+=2) {
3212             my $hd1 = @$devlist[$i];
3213             my $hd2 = @$devlist[$i+1];
3214             zfs_mirror_size_check(@$hd1[2], @$hd2[2]); # pairs need approximately same size
3215             legacy_bios_4k_check(@$hd1[4]);
3216             legacy_bios_4k_check(@$hd2[4]);
3217             $cmd .= ' mirror ' . @$hd1[1] . ' ' . @$hd2[1];
3218         }
3219
3220     } elsif ($filesys =~ m/^zfs \(RAIDZ-([123])\)$/) {
3221         my $level = $1;
3222         my $mindisks = 2 + $level;
3223         die "zfs (RAIDZ-$level) needs at least $mindisks devices\n" if scalar(@$devlist) < $mindisks;
3224         my $hd = @$devlist[0];
3225         my $expected_size = @$hd[2]; # all disks need approximately same size
3226         $cmd .= " raidz$level";
3227         foreach $hd (@$devlist) {
3228             zfs_mirror_size_check($expected_size, @$hd[2]);
3229             legacy_bios_4k_check(@$hd[4]);
3230             $cmd .= " @$hd[1]";
3231             push @$bootdevlist, $hd;
3232         }
3233     } else {
3234         die "unknown zfs mode '$filesys'\n";
3235     }
3236
3237     return ($devlist, $bootdevlist, $cmd);
3238 }
3239
3240 sub get_btrfs_raid_setup {
3241
3242     my $filesys = $config_options->{filesys};
3243
3244     my $devlist = &$get_raid_devlist();
3245
3246     my $diskcount = scalar(@$devlist);
3247     die "$filesys needs at least one device\n" if $diskcount < 1;
3248
3249     my $mode;
3250
3251     if ($diskcount == 1) {
3252         $mode = 'single';
3253     } else {
3254         if ($filesys eq 'btrfs (RAID0)') {
3255             $mode = 'raid0';
3256         } elsif ($filesys eq 'btrfs (RAID1)') {
3257             die "btrfs (RAID1) needs at least 2 device\n" if $diskcount < 2;
3258             $mode = 'raid1';
3259         } elsif ($filesys eq 'btrfs (RAID10)') {
3260             die "btrfs (RAID10) needs at least 4 device\n" if $diskcount < 4;
3261             $mode = 'raid10';
3262         } else {
3263             die "unknown btrfs mode '$filesys'\n";
3264         }
3265     }
3266
3267     return ($devlist, $mode);
3268 }
3269
3270 my $last_hd_selected = 0;
3271 sub create_hdsel_view {
3272
3273     $prev_btn->set_sensitive(1); # enable previous button at this point
3274
3275     cleanup_view();
3276
3277     my $vbox =  Gtk3::VBox->new(0, 0);
3278     $inbox->pack_start($vbox, 1, 0, 0);
3279     my $hbox =  Gtk3::HBox->new(0, 0);
3280     $vbox->pack_start($hbox, 0, 0, 10);
3281
3282     my ($disk, $devname, $size, $model, $logical_bsize) = @{@$hds[0]};
3283     $target_hd = $devname if !defined($target_hd);
3284
3285     $target_hd_label = Gtk3::Label->new("Target Harddisk: ");
3286     $hbox->pack_start($target_hd_label, 0, 0, 0);
3287
3288     $target_hd_combo = Gtk3::ComboBoxText->new();
3289
3290     foreach my $hd (@$hds) {
3291         ($disk, $devname, $size, $model, $logical_bsize) = @$hd;
3292         $target_hd_combo->append_text (get_device_desc($devname, $size, $model));
3293     }
3294
3295     my $raid = $config_options->{filesys} =~ m/zfs|btrfs/;
3296     if ($raid) {
3297         $target_hd_label->set_text("Target: $config_options->{filesys} ");
3298         $target_hd_combo->set_visible(0);
3299         $target_hd_combo->set_no_show_all(1);
3300     }
3301     $target_hd_combo->set_active($last_hd_selected);
3302     $target_hd_combo->signal_connect(changed => sub {
3303         $a = shift->get_active;
3304         my ($disk, $devname) = @{@$hds[$a]};
3305         $last_hd_selected = $a;
3306         $target_hd = $devname;
3307     });
3308
3309     $hbox->pack_start($target_hd_combo, 0, 0, 10);
3310
3311     my $options = Gtk3::Button->new('_Options');
3312     $options->signal_connect (clicked => \&create_hdoption_view);
3313     $hbox->pack_start ($options, 0, 0, 0);
3314
3315
3316     $inbox->show_all;
3317
3318     display_html();
3319
3320     set_next(undef, sub {
3321
3322         if ($config_options->{filesys} =~ m/zfs/) {
3323             my ($devlist) = eval { get_zfs_raid_setup() };
3324             if (my $err = $@) {
3325                 display_message("Warning: $err\nPlease fix ZFS setup first.");
3326                 return;
3327             }
3328             $config_options->{target_hds} = [ map { $_->[1] } @$devlist ];
3329         } elsif ($config_options->{filesys} =~ m/btrfs/) {
3330             my ($devlist) = eval { get_btrfs_raid_setup() };
3331             if (my $err = $@) {
3332                 display_message("Warning: $err\nPlease fix BTRFS setup first.");
3333                 return;
3334             }
3335             $config_options->{target_hds} = [ map { $_->[1] } @$devlist ];
3336         } else {
3337             eval { legacy_bios_4k_check(logical_blocksize($target_hd)) };
3338             if (my $err = $@) {
3339                 display_message("Warning: $err\n");
3340                 return;
3341             }
3342             $config_options->{target_hds} = [ $target_hd ];
3343         }
3344
3345         $step_number++;
3346         create_country_view();
3347     });
3348 }
3349
3350 sub create_extract_view {
3351
3352     cleanup_view();
3353
3354     display_info();
3355
3356     $next->set_sensitive(0);
3357     $prev_btn->set_sensitive(0);
3358     $prev_btn->hide();
3359
3360     my $vbox =  Gtk3::VBox->new(0, 0);
3361     $inbox->pack_start ($vbox, 1, 0, 0);
3362     my $hbox =  Gtk3::HBox->new(0, 0);
3363     $vbox->pack_start ($hbox, 0, 0, 10);
3364
3365     my $vbox2 =  Gtk3::VBox->new(0, 0);
3366     $hbox->pack_start ($vbox2, 0, 0, 0);
3367
3368     $progress_status = Gtk3::Label->new('');
3369     $vbox2->pack_start ($progress_status, 1, 1, 0);
3370
3371     $progress = Gtk3::ProgressBar->new;
3372     $progress->set_show_text(1);
3373     $progress->set_size_request (600, -1);
3374
3375     $vbox2->pack_start($progress, 0, 0, 0);
3376
3377     $inbox->show_all();
3378
3379     my $tdir = $opt_testmode ? "target" : "/target";
3380     mkdir $tdir;
3381     my $base = "${proxmox_cddir}/$setup->{product}-base.squashfs";
3382
3383     eval  { extract_data($base, $tdir); };
3384     my $err = $@;
3385
3386     $next->set_sensitive(1);
3387
3388     set_next("_Reboot", sub { exit (0); } );
3389
3390     if ($err) {
3391         display_html("fail.htm");
3392         display_error($err);
3393     } else {
3394         cleanup_view();
3395         display_html("success.htm");
3396     }
3397 }
3398
3399 sub create_intro_view {
3400
3401     $prev_btn->set_sensitive(0);
3402
3403     cleanup_view();
3404
3405     if (int($total_memory) < 1024) {
3406         my $fullname = $product_fullname->{$setup->{product}};
3407
3408         display_error("Less than 1 GiB of usable memory detected, installation will probably fail.\n\n".
3409             "See 'System Requirements' in the $fullname documentation.");
3410     }
3411
3412     if ($setup->{product} eq 'pve') {
3413         eval {
3414             my $cpuinfo = file_get_contents('/proc/cpuinfo');
3415             if ($cpuinfo && !($cpuinfo =~ /^flags\s*:.*(vmx|svm)/m)) {
3416                 display_error("No support for KVM virtualization detected.\n\n" .
3417                               "Check BIOS settings for Intel VT / AMD-V / SVM.")
3418             }
3419         };
3420     }
3421
3422     display_html();
3423
3424     $step_number++;
3425     set_next("I a_gree", \&create_hdsel_view);
3426 }
3427
3428 $ipconf = get_ip_config();
3429
3430 $country = detect_country() if $ipconf->{default} || $opt_testmode;
3431
3432 # read country, kmap and timezone infos
3433 $cmap = read_cmap();
3434
3435 if (!defined($cmap->{country}->{$country})) {
3436     print $logfd "ignoring detected country '$country', invalid or unknown\n";
3437     $country = undef;
3438 }
3439
3440 create_main_window ();
3441
3442 my $initial_error = 0;
3443
3444 if (!defined ($hds) || (scalar (@$hds) <= 0)) {
3445     print "no hardisks found\n";
3446     $initial_error = 1;
3447     display_html("nohds.htm");
3448     set_next("Reboot", sub { exit(0); } );
3449 } else {
3450     foreach my $hd (@$hds) {
3451         my ($disk, $devname) = @$hd;
3452         next if $devname =~ m|^/dev/md\d+$|;
3453         print "found Disk$disk N:$devname\n";
3454     }
3455 }
3456
3457 if (!$initial_error && (scalar keys %{ $ipconf->{ifaces} } == 0)) {
3458     print "no network interfaces found\n";
3459     $initial_error = 1;
3460     display_html("nonics.htm");
3461     set_next("Reboot", sub { exit(0); } );
3462 }
3463
3464 create_intro_view () if !$initial_error;
3465
3466 Gtk3->main;
3467
3468 exit 0;