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