]> git.proxmox.com Git - pve-installer.git/blob - proxinstall
5f4544a0ff7a598bd367cbb3e7e65977409208c3
[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 # Note: using /dev/disk/by-id/ does not work for unknown reason, we get
1254 # cannot create 'rpool': no such pool or dataset
1255 #my $osdev = find_stable_path ("/dev/disk/by-id", $di->{osdev}) || $di->{osdev};
1256
1257 my $osdev = $di->{osdev};
1258 $vdev =~ s/ $devname/ $osdev/;
1259 }
1260
1261 zfs_create_rpool($vdev);
1262
1263 } else {
1264
1265 die "target '$target_hd' is not a valid block device\n" if ! -b $target_hd;
1266
1267 &$clean_disk($target_hd);
1268
1269 my ($os_size, $osdev, $efidev);
1270 ($os_size, $osdev, $efidev) =
1271 partition_bootable_disk($target_hd, $config_options->{hdsize}, '8E00');
1272
1273 &$udevadm_trigger_block();
1274
1275 my $by_id = find_stable_path ("/dev/disk/by-id", $target_hd);
1276 push @$bootdevinfo, { esp => $efidev, devname => $target_hd,
1277 osdev => $osdev, by_id => $by_id };
1278
1279 my $swap_size = compute_swapsize($os_size);
1280 ($rootdev, $swapfile, $datadev) =
1281 create_lvm_volumes($osdev, $os_size, $swap_size);
1282
1283 # trigger udev to create /dev/disk/by-uuid
1284 &$udevadm_trigger_block(1);
1285 }
1286
1287 if ($use_zfs) {
1288 # to be fast during installation
1289 syscmd("zfs set sync=disabled $zfspoolname") == 0 ||
1290 die "unable to set zfs properties\n";
1291 }
1292
1293 update_progress(0.03, 0, $maxper, "create swap space");
1294 if ($swapfile) {
1295 syscmd("mkswap -f $swapfile") == 0 ||
1296 die "unable to create swap space\n";
1297 }
1298
1299 update_progress(0.05, 0, $maxper, "creating filesystems");
1300
1301 foreach my $di (@$bootdevinfo) {
1302 next if !$di->{esp};
1303 syscmd("mkfs.vfat -F32 $di->{esp}") == 0 ||
1304 die "unable to initialize EFI ESP on device $di->{esp}\n";
1305 }
1306
1307 if ($use_zfs) {
1308 # do nothing
1309 } elsif ($use_btrfs) {
1310 # do nothing
1311 } else {
1312 create_filesystem($rootdev, 'root', $filesys, 0.05, $maxper, 0, 1);
1313 }
1314
1315 update_progress(1, 0.05, $maxper, "mounting target $rootdev");
1316
1317 if ($use_zfs) {
1318 # do nothing
1319 } else {
1320 my $mount_opts = 'noatime';
1321 $mount_opts .= ',nobarrier'
1322 if $use_btrfs || $filesys =~ /^ext\d$/;
1323
1324 syscmd("mount -n $rootdev -o $mount_opts $targetdir") == 0 ||
1325 die "unable to mount $rootdev\n";
1326 }
1327
1328 mkdir "$targetdir/boot";
1329 mkdir "$targetdir/boot/efi";
1330
1331 mkdir "$targetdir/var";
1332 mkdir "$targetdir/var/lib";
1333
1334 if ($setup->{product} eq 'pve') {
1335 mkdir "$targetdir/var/lib/vz";
1336 mkdir "$targetdir/var/lib/pve";
1337
1338 if ($use_btrfs) {
1339 syscmd("btrfs subvolume create $targetdir/var/lib/pve/local-btrfs") == 0 ||
1340 die "unable to create btrfs subvolume\n";
1341 }
1342 }
1343
1344 mkdir "$targetdir/mnt";
1345 mkdir "$targetdir/mnt/hostrun";
1346 syscmd("mount --bind /run $targetdir/mnt/hostrun") == 0 ||
1347 die "unable to bindmount run on $targetdir/mnt/hostrun\n";
1348
1349 update_progress(1, 0.05, $maxper, "extracting base system");
1350
1351 my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size) = stat ($basefile);
1352 $ino || die "unable to open file '$basefile' - $!\n";
1353
1354 my $files = file_read_firstline("${proxmox_cddir}/proxmox/$setup->{product}-base.cnt") ||
1355 die "unable to read base file count\n";
1356
1357 my $per = 0;
1358 my $count = 0;
1359
1360 run_command("unsquashfs -f -dest $targetdir -i $basefile", sub {
1361 my $line = shift;
1362 return if $line !~ m/^$targetdir/;
1363 $count++;
1364 my $nper = int (($count *100)/$files);
1365 if ($nper != $per) {
1366 $per = $nper;
1367 my $frac = $per > 100 ? 1 : $per/100;
1368 update_progress($frac, $maxper, 0.5);
1369 }
1370 });
1371
1372 syscmd("mount -n -t tmpfs tmpfs $targetdir/tmp") == 0 ||
1373 die "unable to mount tmpfs on $targetdir/tmp\n";
1374 syscmd("mount -n -t proc proc $targetdir/proc") == 0 ||
1375 die "unable to mount proc on $targetdir/proc\n";
1376 syscmd("mount -n -t sysfs sysfs $targetdir/sys") == 0 ||
1377 die "unable to mount sysfs on $targetdir/sys\n";
1378 syscmd("chroot $targetdir mount --bind /mnt/hostrun /run") == 0 ||
1379 die "unable to re-bindmount hostrun on /run in chroot\n";
1380
1381 update_progress(1, $maxper, 0.5, "configuring base system");
1382
1383 # configure hosts
1384
1385 my $hosts =
1386 "127.0.0.1 localhost.localdomain localhost\n" .
1387 "$ipaddress $hostname.$domain $hostname\n\n" .
1388 "# The following lines are desirable for IPv6 capable hosts\n\n" .
1389 "::1 ip6-localhost ip6-loopback\n" .
1390 "fe00::0 ip6-localnet\n" .
1391 "ff00::0 ip6-mcastprefix\n" .
1392 "ff02::1 ip6-allnodes\n" .
1393 "ff02::2 ip6-allrouters\n" .
1394 "ff02::3 ip6-allhosts\n";
1395
1396 write_config($hosts, "$targetdir/etc/hosts");
1397
1398 write_config("$hostname\n", "$targetdir/etc/hostname");
1399
1400 syscmd("/bin/hostname $hostname") if !$opt_testmode;
1401
1402 # configure interfaces
1403
1404 my $ifaces = "auto lo\niface lo inet loopback\n\n";
1405
1406 my $ntype = $ipversion == 4 ? 'inet' : 'inet6';
1407
1408 my $ethdev = $ipconf->{ifaces}->{$ipconf->{selected}}->{name};
1409
1410 if ($setup->{bridged_network}) {
1411 $ifaces .= "iface $ethdev $ntype manual\n";
1412
1413 $ifaces .=
1414 "\nauto vmbr0\niface vmbr0 $ntype static\n" .
1415 "\taddress $ipaddress\n" .
1416 "\tnetmask $netmask\n" .
1417 "\tgateway $gateway\n" .
1418 "\tbridge_ports $ethdev\n" .
1419 "\tbridge_stp off\n" .
1420 "\tbridge_fd 0\n";
1421 } else {
1422 $ifaces .= "auto $ethdev\n" .
1423 "iface $ethdev $ntype static\n" .
1424 "\taddress $ipaddress\n" .
1425 "\tnetmask $netmask\n" .
1426 "\tgateway $gateway\n";
1427 }
1428
1429 foreach my $iface (sort keys %{$ipconf->{ifaces}}) {
1430 my $name = $ipconf->{ifaces}->{$iface}->{name};
1431 next if $name eq $ethdev;
1432
1433 $ifaces .= "\niface $name $ntype manual\n";
1434 }
1435
1436 write_config($ifaces, "$targetdir/etc/network/interfaces");
1437
1438 # configure dns
1439
1440 my $resolvconf = "search $domain\nnameserver $dnsserver\n";
1441 write_config($resolvconf, "$targetdir/etc/resolv.conf");
1442
1443 # configure fstab
1444
1445 my $fstab = "# <file system> <mount point> <type> <options> <dump> <pass>\n";
1446
1447 if ($use_zfs) {
1448 # do nothing
1449 } elsif ($use_btrfs) {
1450 my $fsuuid;
1451 my $cmd = "blkid -u filesystem -t TYPE=btrfs -o export $rootdev";
1452 run_command($cmd, sub {
1453 my $line = shift;
1454
1455 if ($line =~ m/^UUID=([A-Fa-f0-9\-]+)$/) {
1456 $fsuuid = $1;
1457 }
1458 });
1459
1460 die "unable to detect FS UUID" if !defined($fsuuid);
1461
1462 $fstab .= "UUID=$fsuuid / btrfs defaults 0 1\n";
1463 } else {
1464 my $root_mountopt = $fssetup->{$filesys}->{root_mountopt} || 'defaults';
1465 $fstab .= "$rootdev / $filesys ${root_mountopt} 0 1\n";
1466 }
1467
1468 # mount /boot/efi
1469 # Note: this is required by current grub, but really dangerous, because
1470 # vfat does not have journaling, so it triggers manual fsck after each crash
1471 # so we only mount /boot/efi if really required (efi systems).
1472 if ($boot_type eq 'efi' && !$use_zfs) {
1473 if (scalar(@$bootdevinfo)) {
1474 my $di = @$bootdevinfo[0]; # simply use first disk
1475
1476 if ($di->{esp}) {
1477 my $efi_boot_uuid = $di->{esp};
1478 if (my $uuid = find_dev_by_uuid ($di->{esp})) {
1479 $efi_boot_uuid = "UUID=$uuid";
1480 }
1481
1482 $fstab .= "${efi_boot_uuid} /boot/efi vfat defaults 0 1\n";
1483 }
1484 }
1485 }
1486
1487
1488 $fstab .= "$swapfile none swap sw 0 0\n" if $swapfile;
1489
1490 $fstab .= "proc /proc proc defaults 0 0\n";
1491
1492 write_config($fstab, "$targetdir/etc/fstab");
1493 write_config("", "$targetdir/etc/mtab");
1494
1495 syscmd("cp ${proxmox_libdir}/policy-disable-rc.d " .
1496 "$targetdir/usr/sbin/policy-rc.d") == 0 ||
1497 die "unable to copy policy-rc.d\n";
1498 syscmd("cp ${proxmox_libdir}/fake-start-stop-daemon " .
1499 "$targetdir/sbin/") == 0 ||
1500 die "unable to copy start-stop-daemon\n";
1501
1502 diversion_add($targetdir, "/sbin/start-stop-daemon", "/sbin/fake-start-stop-daemon");
1503 diversion_add($targetdir, "/usr/sbin/update-grub", "/bin/true");
1504 diversion_add($targetdir, "/usr/sbin/update-initramfs", "/bin/true");
1505
1506 syscmd("touch $targetdir/proxmox_install_mode");
1507
1508 my $grub_install_devices_txt = '';
1509 foreach my $di (@$bootdevinfo) {
1510 $grub_install_devices_txt .= ', ' if $grub_install_devices_txt;
1511 $grub_install_devices_txt .= $di->{by_id} || $di->{devname};
1512 }
1513
1514 # Note: keyboard-configuration/xbkb-keymap is used by console-setup
1515 my $xkmap = $cmap->{kmap}->{$keymap}->{x11} // 'us';
1516
1517 debconfig_set ($targetdir, <<_EOD);
1518 locales locales/default_environment_locale select en_US.UTF-8
1519 locales locales/locales_to_be_generated select en_US.UTF-8 UTF-8
1520 samba-common samba-common/dhcp boolean false
1521 samba-common samba-common/workgroup string WORKGROUP
1522 postfix postfix/main_mailer_type select No configuration
1523 keyboard-configuration keyboard-configuration/xkb-keymap select $xkmap
1524 d-i debian-installer/locale select en_US.UTF-8
1525 grub-pc grub-pc/install_devices select $grub_install_devices_txt
1526 _EOD
1527
1528 my $pkg_count = 0;
1529 while (<${proxmox_pkgdir}/*.deb>) { $pkg_count++ };
1530
1531 # btrfs/dpkg is extremely slow without --force-unsafe-io
1532 my $dpkg_opts = $use_btrfs ? "--force-unsafe-io" : "";
1533
1534 $count = 0;
1535 while (<${proxmox_pkgdir}/*.deb>) {
1536 chomp;
1537 my $path = $_;
1538 my ($deb) = $path =~ m/${proxmox_pkgdir}\/(.*\.deb)/;
1539 update_progress($count/$pkg_count, 0.5, 0.75, "extracting $deb");
1540 print "extracting: $deb\n";
1541 syscmd("cp $path $targetdir/tmp/$deb") == 0 ||
1542 die "installation of package $deb failed\n";
1543 syscmd("chroot $targetdir dpkg $dpkg_opts --force-depends --no-triggers --unpack /tmp/$deb") == 0 ||
1544 die "installation of package $deb failed\n";
1545 update_progress((++$count)/$pkg_count, 0.5, 0.75);
1546 }
1547
1548 # needed for postfix postinst in case no other NIC is active
1549 syscmd("chroot $targetdir ifup lo");
1550
1551 my $cmd = "chroot $targetdir dpkg $dpkg_opts --force-confold --configure -a";
1552 $count = 0;
1553 run_command($cmd, sub {
1554 my $line = shift;
1555 if ($line =~ m/Setting up\s+(\S+)/) {
1556 update_progress((++$count)/$pkg_count, 0.75, 0.95,
1557 "configuring $1");
1558 }
1559 });
1560
1561 unlink "$targetdir/etc/mailname";
1562 $postfix_main_cf =~ s/__FQDN__/${hostname}.${domain}/;
1563 write_config($postfix_main_cf, "$targetdir/etc/postfix/main.cf");
1564
1565 # make sure we have all postfix directories
1566 syscmd("chroot $targetdir /usr/sbin/postfix check");
1567 # cleanup mail queue
1568 syscmd("chroot $targetdir /usr/sbin/postsuper -d ALL");
1569
1570 # enable NTP (timedatectl set-ntp true does not work without DBUS)
1571 syscmd("chroot $targetdir /bin/systemctl enable systemd-timesyncd.service");
1572
1573 unlink "$targetdir/proxmox_install_mode";
1574
1575 # set timezone
1576 unlink ("$targetdir/etc/localtime");
1577 symlink ("/usr/share/zoneinfo/$timezone", "$targetdir/etc/localtime");
1578 write_config("$timezone\n", "$targetdir/etc/timezone");
1579
1580 # set apt mirror
1581 if (my $mirror = $cmap->{country}->{$country}->{mirror}) {
1582 my $fn = "$targetdir/etc/apt/sources.list";
1583 syscmd("sed -i 's/ftp\\.debian\\.org/$mirror/' '$fn'");
1584 }
1585
1586 # create extended_states for apt (avoid cron job warning if that
1587 # file does not exist)
1588 write_config('', "$targetdir/var/lib/apt/extended_states");
1589
1590 # allow ssh root login
1591 syscmd(['sed', '-i', 's/^#\?PermitRootLogin.*/PermitRootLogin yes/', "$targetdir/etc/ssh/sshd_config"]);
1592
1593 if ($setup->{product} eq 'pmg') {
1594 # install initial clamav DB
1595 my $srcdir = "${proxmox_cddir}/proxmox/clamav";
1596 foreach my $fn ("main.cvd", "bytecode.cvd", "daily.cvd", "safebrowsing.cvd") {
1597 syscmd("cp \"$srcdir/$fn\" \"$targetdir/var/lib/clamav\"") == 0 ||
1598 die "installation of clamav db file '$fn' failed\n";
1599 }
1600 syscmd("chroot $targetdir /bin/chown clamav:clamav -R /var/lib/clamav") == 0 ||
1601 die "unable to set owner for clamav database files\n";
1602 }
1603
1604 if ($setup->{product} eq 'pve') {
1605 # save installer settings
1606 my $ucc = uc ($country);
1607 debconfig_set($targetdir, "pve-manager pve-manager/country string $ucc\n");
1608 }
1609
1610 update_progress(0.8, 0.95, 1, "make system bootable");
1611
1612 if ($use_zfs) {
1613 syscmd("sed -i -e 's/^GRUB_CMDLINE_LINUX=.*/GRUB_CMDLINE_LINUX=\"root=ZFS=$zfspoolname\\/ROOT\\/$zfsrootvolname boot=zfs\"/' $targetdir/etc/default/grub") == 0 ||
1614 die "unable to update /etc/default/grub\n";
1615
1616 if ($boot_type eq 'efi') {
1617 write_config("root=ZFS=$zfspoolname/ROOT/$zfsrootvolname boot=zfs", "$targetdir/etc/kernel/cmdline");
1618 }
1619
1620 }
1621
1622 diversion_remove($targetdir, "/usr/sbin/update-grub");
1623 diversion_remove($targetdir, "/usr/sbin/update-initramfs");
1624
1625 my $kapi;
1626 foreach my $fn (<$targetdir/lib/modules/*>) {
1627 if ($fn =~ m!/(\d+\.\d+\.\d+-\d+-pve)$!) {
1628 die "found multiple kernels\n" if defined($kapi);
1629 $kapi = $1;
1630 }
1631 }
1632 die "unable to detect kernel version\n" if !defined($kapi);
1633
1634 if (!$opt_testmode) {
1635
1636 unlink ("$targetdir/etc/mtab");
1637 symlink ("/proc/mounts", "$targetdir/etc/mtab");
1638 syscmd("mount -n --bind /dev $targetdir/dev");
1639
1640 syscmd("chroot $targetdir /usr/sbin/update-initramfs -c -k $kapi") == 0 ||
1641 die "unable to install initramfs\n";
1642
1643 foreach my $di (@$bootdevinfo) {
1644 my $dev = $di->{devname};
1645 syscmd("chroot $targetdir /usr/sbin/grub-install --target i386-pc --no-floppy --bootloader-id='proxmox' $dev") == 0 ||
1646 die "unable to install the i386-pc boot loader on '$dev'\n";
1647
1648 if (my $esp = $di->{esp}) {
1649 if ($use_zfs) {
1650 prepare_systemd_boot_esp($esp, $targetdir);
1651 } else {
1652 prepare_grub_efi_boot_esp($dev, $esp, $targetdir);
1653 }
1654 }
1655 }
1656
1657 syscmd("chroot $targetdir /usr/sbin/update-grub") == 0 ||
1658 die "unable to update boot loader config\n";
1659
1660 if ($use_zfs && $boot_type eq 'efi') {
1661 syscmd("chroot $targetdir /etc/kernel/postinst.d/zz-pve-efiboot") == 0 ||
1662 die "unable to generate systemd-boot config\n";
1663 }
1664
1665 syscmd("umount $targetdir/dev");
1666 }
1667
1668 # cleanup
1669
1670 unlink "$targetdir/usr/sbin/policy-rc.d";
1671
1672 diversion_remove($targetdir, "/sbin/start-stop-daemon");
1673
1674 # set root password
1675 my $octets = encode("utf-8", $password);
1676 run_command("chroot $targetdir /usr/sbin/chpasswd", undef,
1677 "root:$octets\n");
1678
1679 if ($setup->{product} eq 'pmg') {
1680 # save admin email
1681 write_config("section: admin\n\temail ${mailto}\n",
1682 "$targetdir/etc/pmg/pmg.conf");
1683
1684 } elsif ($setup->{product} eq 'pve') {
1685
1686 # create pmxcfs DB
1687
1688 my $tmpdir = "$targetdir/tmp/pve";
1689 mkdir $tmpdir;
1690
1691 # write vnc keymap to datacenter.cfg
1692 my $vnckmap = $cmap->{kmap}->{$keymap}->{kvm} || 'en-us';
1693 write_config("keyboard: $vnckmap\n",
1694 "$tmpdir/datacenter.cfg");
1695
1696 # save admin email
1697 write_config("user:root\@pam:1:0:::${mailto}::\n",
1698 "$tmpdir/user.cfg");
1699
1700 # write storage.cfg
1701 my $storage_cfg_fn = "$tmpdir/storage.cfg";
1702 if ($use_zfs) {
1703 write_config($storage_cfg_zfs, $storage_cfg_fn);
1704 } elsif ($use_btrfs) {
1705 write_config($storage_cfg_btrfs, $storage_cfg_fn);
1706 } elsif ($datadev) {
1707 write_config($storage_cfg_lvmthin, $storage_cfg_fn);
1708 } else {
1709 write_config($storage_cfg_local, $storage_cfg_fn);
1710 }
1711
1712 run_command("chroot $targetdir /usr/bin/create_pmxcfs_db /tmp/pve /var/lib/pve-cluster/config.db");
1713
1714 syscmd("rm -rf $tmpdir");
1715 }
1716 };
1717
1718 my $err = $@;
1719
1720 update_progress(1, 0, 1, "");
1721
1722 print $err if $err;
1723
1724 if ($opt_testmode) {
1725 my $elapsed = Time::HiRes::tv_interval($starttime);
1726 print "Elapsed extract time: $elapsed\n";
1727
1728 syscmd("chroot $targetdir /usr/bin/dpkg-query -W --showformat='\${package}\n'> final.pkglist");
1729 }
1730
1731 syscmd("umount $targetdir/run");
1732 syscmd("umount $targetdir/mnt/hostrun");
1733 syscmd("umount $targetdir/tmp");
1734 syscmd("umount $targetdir/proc");
1735 syscmd("umount $targetdir/sys");
1736
1737 if ($use_zfs) {
1738 syscmd("zfs umount -a") == 0 ||
1739 die "unable to unmount zfs\n";
1740 } else {
1741 syscmd("umount -d $targetdir");
1742 }
1743
1744 if (!$err && $use_zfs) {
1745 syscmd("zfs set sync=standard $zfspoolname") == 0 ||
1746 die "unable to set zfs properties\n";
1747
1748 syscmd("zfs set mountpoint=/ $zfspoolname/ROOT/$zfsrootvolname") == 0 ||
1749 die "zfs set mountpoint failed\n";
1750
1751 syscmd("zpool set bootfs=$zfspoolname/ROOT/$zfsrootvolname $zfspoolname") == 0 ||
1752 die "zfs set bootfs failed\n";
1753 syscmd("zpool export $zfspoolname");
1754 }
1755
1756 die $err if $err;
1757 }
1758
1759 my $last_display_change = 0;
1760
1761 my $display_info_counter = 0;
1762
1763 my $display_info_items = [
1764 "extract1-license.htm",
1765 "extract2-rulesystem.htm",
1766 "extract3-spam.htm",
1767 "extract4-virus.htm",
1768 ];
1769
1770 sub display_info {
1771
1772 my $min_display_time = 15;
1773
1774 my $ctime = time();
1775
1776 return if ($ctime - $last_display_change) < $min_display_time;
1777
1778 my $page = $display_info_items->[$display_info_counter % scalar(@$display_info_items)];
1779
1780 $display_info_counter++;
1781
1782 display_html($page);
1783 }
1784
1785 sub display_html {
1786 my ($filename) = @_;
1787
1788 $filename = $steps[$step_number]->{html} if !$filename;
1789
1790 my $path = "${proxmox_libdir}/html/$filename";
1791
1792 my $url = "file://$path";
1793
1794 my $data = file_get_contents($path);
1795
1796 if ($filename eq 'license.htm') {
1797 my $license = eval { decode('utf8', file_get_contents("${proxmox_cddir}/EULA")) };
1798 if (my $err = $@) {
1799 die $err if !$opt_testmode;
1800 $license = "TESTMODE: Ignore non existent EULA...\n";
1801 }
1802 my $title = "END USER LICENSE AGREEMENT (EULA)";
1803 $data =~ s/__LICENSE__/$license/;
1804 $data =~ s/__LICENSE_TITLE__/$title/;
1805 }
1806
1807 $htmlview->load_html($data, $url);
1808
1809 $last_display_change = time();
1810 }
1811
1812 sub prev_function {
1813
1814 my ($text, $fctn) = @_;
1815
1816 $fctn = $step_number if !$fctn;
1817 $text = "_Previous" if !$text;
1818 $prev_btn->set_label ($text);
1819
1820 $step_number--;
1821 $steps[$step_number]->{function}();
1822
1823 $prev_btn->grab_focus();
1824 }
1825
1826 sub set_next {
1827 my ($text, $fctn) = @_;
1828
1829 $next_fctn = $fctn;
1830 my $step = $steps[$step_number];
1831 $text //= $steps[$step_number]->{next_button} // '_Next';
1832 $next->set_label($text);
1833
1834 $next->grab_focus();
1835 }
1836
1837 sub create_main_window {
1838
1839 $window = Gtk3::Window->new();
1840 $window->set_default_size(1024, 768);
1841 $window->set_has_resize_grip(0);
1842 $window->set_decorated(0) if !$opt_testmode;
1843
1844 my $vbox = Gtk3::VBox->new(0, 0);
1845
1846 my $logofn = "$setup->{product}-banner.png";
1847 my $image = Gtk3::Image->new_from_file("${proxmox_libdir}/$logofn");
1848 $vbox->pack_start($image, 0, 0, 0);
1849
1850 my $hbox = Gtk3::HBox->new(0, 0);
1851 $vbox->pack_start($hbox, 1, 1, 0);
1852
1853 # my $f1 = Gtk3::Frame->new ('test');
1854 # $f1->set_shadow_type ('none');
1855 # $hbox->pack_start ($f1, 1, 1, 0);
1856
1857 my $sep1 = Gtk3::HSeparator->new();
1858 $vbox->pack_start($sep1, 0, 0, 0);
1859
1860 $cmdbox = Gtk3::HBox->new();
1861 $vbox->pack_start($cmdbox, 0, 0, 10);
1862
1863 $next = Gtk3::Button->new('_Next');
1864 $next->signal_connect(clicked => sub { $last_display_change = 0; &$next_fctn (); });
1865 $cmdbox->pack_end($next, 0, 0, 10);
1866
1867
1868 $prev_btn = Gtk3::Button->new('_Previous');
1869 $prev_btn->signal_connect(clicked => sub { $last_display_change = 0; &prev_function (); });
1870 $cmdbox->pack_end($prev_btn, 0, 0, 10);
1871
1872
1873 my $abort = Gtk3::Button->new('_Abort');
1874 $abort->set_can_focus(0);
1875 $cmdbox->pack_start($abort, 0, 0, 10);
1876 $abort->signal_connect(clicked => sub { exit (-1); });
1877
1878 my $vbox2 = Gtk3::VBox->new(0, 0);
1879 $hbox->add($vbox2);
1880
1881 $htmlview = Gtk3::WebKit2::WebView->new();
1882 my $scrolls = Gtk3::ScrolledWindow->new();
1883 $scrolls->add($htmlview);
1884
1885 my $hbox2 = Gtk3::HBox->new(0, 0);
1886 $hbox2->pack_start($scrolls, 1, 1, 0);
1887
1888 $vbox2->pack_start($hbox2, 1, 1, 0);
1889
1890 my $vbox3 = Gtk3::VBox->new(0, 0);
1891 $vbox2->pack_start($vbox3, 0, 0, 0);
1892
1893 my $sep2 = Gtk3::HSeparator->new;
1894 $vbox3->pack_start($sep2, 0, 0, 0);
1895
1896 $inbox = Gtk3::HBox->new(0, 0);
1897 $vbox3->pack_start($inbox, 0, 0, 0);
1898
1899 $window->add($vbox);
1900
1901 $window->show_all;
1902 $window->realize();
1903 }
1904
1905 sub cleanup_view {
1906 $inbox->foreach(sub {
1907 my $child = shift;
1908 $inbox->remove ($child);
1909 });
1910 }
1911
1912 # fixme: newer GTK3 has special properties to handle numbers with Entry
1913 # only allow floating point numbers with Gtk3::Entry
1914
1915 sub check_float {
1916 my ($entry, $event) = @_;
1917
1918 return check_number($entry, $event, 1);
1919 }
1920
1921 sub check_int {
1922 my ($entry, $event) = @_;
1923
1924 return check_number($entry, $event, 0);
1925 }
1926
1927 sub check_number {
1928 my ($entry, $event, $float) = @_;
1929
1930 my $val = $event->get_keyval;
1931
1932 if (($float && $val == ord '.') ||
1933 $val == Gtk3::Gdk::KEY_ISO_Left_Tab ||
1934 $val == Gtk3::Gdk::KEY_Shift_L ||
1935 $val == Gtk3::Gdk::KEY_Tab ||
1936 $val == Gtk3::Gdk::KEY_Left ||
1937 $val == Gtk3::Gdk::KEY_Right ||
1938 $val == Gtk3::Gdk::KEY_BackSpace ||
1939 $val == Gtk3::Gdk::KEY_Delete ||
1940 ($val >= ord '0' && $val <= ord '9') ||
1941 ($val >= Gtk3::Gdk::KEY_KP_0 &&
1942 $val <= Gtk3::Gdk::KEY_KP_9)) {
1943 return undef;
1944 }
1945
1946 return 1;
1947 }
1948
1949 sub create_text_input {
1950 my ($default, $text) = @_;
1951
1952 my $hbox = Gtk3::HBox->new(0, 0);
1953
1954 my $label = Gtk3::Label->new($text);
1955 $label->set_size_request(150, -1);
1956 $label->set_alignment(1, 0.5);
1957 $hbox->pack_start($label, 0, 0, 10);
1958 my $e1 = Gtk3::Entry->new();
1959 $e1->set_width_chars(30);
1960 $hbox->pack_start($e1, 0, 0, 0);
1961 $e1->set_text($default);
1962
1963 return ($hbox, $e1);
1964 }
1965
1966 sub get_ip_config {
1967
1968 my $ifaces = {};
1969 my $default;
1970
1971 my $links = `ip -o l`;
1972 foreach my $l (split /\n/,$links) {
1973 my ($index, $name, $flags, $state, $mac) = $l =~ m/^(\d+):\s+(\S+):\s+<(\S+)>.*\s+state\s+(\S+)\s+.*\s+link\/ether\s+(\S+)\s+/;
1974 next if !$name || $name eq 'lo';
1975
1976 my $driver = readlink "/sys/class/net/$name/device/driver" || 'unknown';
1977 $driver =~ s!^.*/!!;
1978
1979 $ifaces->{"$index"} = {
1980 name => $name,
1981 driver => $driver,
1982 flags => $flags,
1983 state => $state,
1984 mac => $mac,
1985 };
1986
1987 my $addresses = `ip -o a s $name`;
1988 foreach my $a (split /\n/,$addresses) {
1989 my ($family, $ip, $prefix) = $a =~ m/^\Q$index\E:\s+\Q$name\E\s+(inet|inet6)\s+($IPRE)\/(\d+)\s+/;
1990 next if !$ip;
1991 next if $a =~ /scope\s+link/; # ignore link local
1992
1993 my $mask = $prefix;
1994
1995 if ($family eq 'inet') {
1996 next if !$ip =~ /$IPV4RE/;
1997 next if $prefix < 8 || $prefix > 32;
1998 $mask = @$ipv4_reverse_mask[$prefix];
1999 } else {
2000 next if !$ip =~ /$IPV6RE/;
2001 }
2002
2003 $default = $index if !$default;
2004
2005 $ifaces->{"$index"}->{"$family"} = {
2006 mask => $mask,
2007 addr => $ip,
2008 };
2009 }
2010 }
2011
2012
2013 my $route = `ip route`;
2014 my ($gateway) = $route =~ m/^default\s+via\s+(\S+)\s+/m;
2015
2016 my $resolvconf = `cat /etc/resolv.conf`;
2017 my ($dnsserver) = $resolvconf =~ m/^nameserver\s+(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/m;
2018 my ($domain) = $resolvconf =~ m/^domain\s+(\S+)$/m;
2019
2020 return {
2021 default => $default,
2022 ifaces => $ifaces,
2023 gateway => $gateway,
2024 dnsserver => $dnsserver,
2025 domain => $domain,
2026 }
2027 }
2028
2029 sub display_message {
2030 my ($msg) = @_;
2031
2032 my $dialog = Gtk3::MessageDialog->new($window, 'modal',
2033 'info', 'ok', $msg);
2034 $dialog->run();
2035 $dialog->destroy();
2036 }
2037
2038 sub display_error {
2039 my ($msg) = @_;
2040
2041 my $dialog = Gtk3::MessageDialog->new($window, 'modal',
2042 'error', 'ok', $msg);
2043 $dialog->run();
2044 $dialog->destroy();
2045 }
2046
2047 my $ipconf_first_view = 1;
2048
2049 sub create_ipconf_view {
2050
2051 cleanup_view();
2052 display_html();
2053
2054 my $vbox = Gtk3::VBox->new(0, 0);
2055 $inbox->pack_start($vbox, 1, 0, 0);
2056 my $hbox = Gtk3::HBox->new(0, 0);
2057 $vbox->pack_start($hbox, 0, 0, 10);
2058 my $vbox2 = Gtk3::VBox->new(0, 0);
2059 $hbox->add($vbox2);
2060
2061 my $ipaddr_text = $config->{ipaddress} // "192.168.100.2";
2062 my $ipbox;
2063 ($ipbox, $ipconf_entry_addr) =
2064 create_text_input($ipaddr_text, 'IP Address:');
2065
2066 my $netmask_text = $config->{netmask} // "255.255.255.0";
2067 my $maskbox;
2068 ($maskbox, $ipconf_entry_mask) =
2069 create_text_input($netmask_text, 'Netmask:');
2070
2071 my $device_cb = Gtk3::ComboBoxText->new();
2072 $device_cb->set_active(0);
2073 $device_cb->set_visible(1);
2074
2075 my $get_device_desc = sub {
2076 my $iface = shift;
2077 return "$iface->{name} - $iface->{mac} ($iface->{driver})";
2078 };
2079
2080 my $device_active_map = {};
2081 my $device_active_reverse_map = {};
2082
2083 my $device_change_handler = sub {
2084 my $current = shift;
2085
2086 my $new = $device_active_map->{$current->get_active()};
2087 return if defined($ipconf->{selected}) && $new eq $ipconf->{selected};
2088
2089 $ipconf->{selected} = $new;
2090 my $iface = $ipconf->{ifaces}->{$ipconf->{selected}};
2091 $config->{mngmt_nic} = $iface->{name};
2092 $ipconf_entry_addr->set_text($iface->{inet}->{addr} || $iface->{inet6}->{addr})
2093 if $iface->{inet}->{addr} || $iface->{inet6}->{addr};
2094 $ipconf_entry_mask->set_text($iface->{inet}->{mask} || $iface->{inet6}->{mask})
2095 if $iface->{inet}->{mask} || $iface->{inet6}->{mask};
2096 };
2097
2098 my $i = 0;
2099 foreach my $index (sort keys %{$ipconf->{ifaces}}) {
2100 $device_cb->append_text(&$get_device_desc($ipconf->{ifaces}->{$index}));
2101 $device_active_map->{$i} = $index;
2102 $device_active_reverse_map->{$ipconf->{ifaces}->{$index}->{name}} = $i;
2103 if ($ipconf_first_view && $index == $ipconf->{default}) {
2104 $device_cb->set_active($i);
2105 &$device_change_handler($device_cb);
2106 $ipconf_first_view = 0;
2107 }
2108 $device_cb->signal_connect('changed' => $device_change_handler);
2109 $i++;
2110 }
2111
2112 if (my $nic = $config->{mngmt_nic}) {
2113 $device_cb->set_active($device_active_reverse_map->{$nic} // 0);
2114 } else {
2115 $device_cb->set_active(0);
2116 }
2117
2118 my $devicebox = Gtk3::HBox->new(0, 0);
2119 my $label = Gtk3::Label->new("Management Interface:");
2120 $label->set_size_request(150, -1);
2121 $label->set_alignment(1, 0.5);
2122 $devicebox->pack_start($label, 0, 0, 10);
2123 $devicebox->pack_start($device_cb, 0, 0, 0);
2124
2125 $vbox2->pack_start($devicebox, 0, 0, 2);
2126
2127 my $hn = $config->{fqdn} // "$setup->{product}." . ($ipconf->{domain} // "example.invalid");
2128
2129 my ($hostbox, $hostentry) =
2130 create_text_input($hn, 'Hostname (FQDN):');
2131 $vbox2->pack_start($hostbox, 0, 0, 2);
2132
2133 $vbox2->pack_start($ipbox, 0, 0, 2);
2134
2135 $vbox2->pack_start($maskbox, 0, 0, 2);
2136
2137 $gateway = $config->{gateway} // $ipconf->{gateway} || '192.168.100.1';
2138
2139 my $gwbox;
2140 ($gwbox, $ipconf_entry_gw) =
2141 create_text_input($gateway, 'Gateway:');
2142
2143 $vbox2->pack_start($gwbox, 0, 0, 2);
2144
2145 $dnsserver = $config->{dnsserver} // $ipconf->{dnsserver} || $gateway;
2146
2147 my $dnsbox;
2148 ($dnsbox, $ipconf_entry_dns) =
2149 create_text_input($dnsserver, 'DNS Server:');
2150
2151 $vbox2->pack_start($dnsbox, 0, 0, 0);
2152
2153 $inbox->show_all;
2154 set_next(undef, sub {
2155
2156 # verify hostname
2157
2158 my $text = $hostentry->get_text();
2159
2160 $text =~ s/^\s+//;
2161 $text =~ s/\s+$//;
2162
2163 $config->{fqdn} = $text;
2164
2165 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
2166
2167 # Debian does not support purely numeric hostnames
2168 if ($text && $text =~ /^[0-9]+(?:\.|$)/) {
2169 display_message("Purely numeric hostnames are not allowed.");
2170 $hostentry->grab_focus();
2171 return;
2172 }
2173
2174 if ($text && $text =~ m/^(${namere}\.)*${namere}$/ && $text !~ m/.example.invalid$/ &&
2175 $text =~ m/^([^\.]+)\.(\S+)$/) {
2176 $hostname = $1;
2177 $domain = $2;
2178 } else {
2179 display_message("Hostname does not look like a fully qualified domain name.");
2180 $hostentry->grab_focus();
2181 return;
2182 }
2183
2184 # verify ip address
2185
2186 $text = $ipconf_entry_addr->get_text();
2187 $text =~ s/^\s+//;
2188 $text =~ s/\s+$//;
2189 if ($text =~ m!^($IPV4RE)$!) {
2190 $ipaddress = $text;
2191 $ipversion = 4;
2192 } elsif ($text =~ m!^($IPV6RE)$!) {
2193 $ipaddress = $text;
2194 $ipversion = 6;
2195 } else {
2196 display_message("IP address is not valid.");
2197 $ipconf_entry_addr->grab_focus();
2198 return;
2199 }
2200 $config->{ipaddress} = $ipaddress;
2201
2202 $text = $ipconf_entry_mask->get_text();
2203 $text =~ s/^\s+//;
2204 $text =~ s/\s+$//;
2205 if (($ipversion == 6) && ($text =~ m/^(\d+)$/) && ($1 >= 8) && ($1 <= 126)) {
2206 $netmask = $text;
2207 } elsif (($ipversion == 4) && defined($ipv4_mask_hash->{$text})) {
2208 $netmask = $text;
2209 } else {
2210 display_message("Netmask is not valid.");
2211 $ipconf_entry_mask->grab_focus();
2212 return;
2213 }
2214 $config->{netmask} = $netmask;
2215
2216 $text = $ipconf_entry_gw->get_text();
2217 $text =~ s/^\s+//;
2218 $text =~ s/\s+$//;
2219 if (($ipversion == 4) && ($text =~ m!^($IPV4RE)$!)) {
2220 $gateway = $text;
2221 } elsif (($ipversion == 6) && ($text =~ m!^($IPV6RE)$!)) {
2222 $gateway = $text;
2223 } else {
2224 display_message("Gateway is not valid.");
2225 $ipconf_entry_gw->grab_focus();
2226 return;
2227 }
2228 $config->{gateway} = $gateway;
2229
2230 $text = $ipconf_entry_dns->get_text();
2231 $text =~ s/^\s+//;
2232 $text =~ s/\s+$//;
2233 if (($ipversion == 4) && ($text =~ m!^($IPV4RE)$!)) {
2234 $dnsserver = $text;
2235 } elsif (($ipversion == 6) && ($text =~ m!^($IPV6RE)$!)) {
2236 $dnsserver = $text;
2237 } else {
2238 display_message("DNS server is not valid.");
2239 $ipconf_entry_dns->grab_focus();
2240 return;
2241 }
2242 $config->{dnsserver} = $dnsserver;
2243
2244 #print "TEST $ipaddress $netmask $gateway $dnsserver\n";
2245
2246 $step_number++;
2247 create_ack_view();
2248 });
2249
2250 $hostentry->grab_focus();
2251 }
2252
2253 sub create_ack_view {
2254
2255 cleanup_view();
2256
2257 my $ack_template = "${proxmox_libdir}/html/ack_template.htm";
2258 my $ack_html = "${proxmox_libdir}/html/$steps[$step_number]->{html}";
2259 my $html_data = file_get_contents($ack_template);
2260
2261 my %config_values = (
2262 __target_hd__ => join(' | ', @{$config_options->{target_hds}}),
2263 __target_fs__ => $config_options->{filesys},
2264 __country__ => $cmap->{country}->{$country}->{name},
2265 __timezone__ => $timezone,
2266 __keymap__ => $keymap,
2267 __mailto__ => $mailto,
2268 __interface__ => $ipconf->{ifaces}->{$ipconf->{selected}}->{name},
2269 __hostname__ => $hostname,
2270 __ip__ => $ipaddress,
2271 __netmask__ => $netmask,
2272 __gateway__ => $gateway,
2273 __dnsserver__ => $dnsserver,
2274 );
2275
2276 while ( my ($k, $v) = each %config_values) {
2277 $html_data =~ s/$k/$v/g;
2278 }
2279
2280 write_config($html_data, $ack_html);
2281
2282 display_html();
2283
2284 set_next(undef, sub {
2285 $step_number++;
2286 create_extract_view();
2287 });
2288 }
2289
2290 sub get_device_desc {
2291 my ($devname, $size, $model) = @_;
2292
2293 if ($size && ($size > 0)) {
2294 $size = int($size/2048); # size in MB, from 512B "sectors"
2295
2296 my $text = "$devname (";
2297 if ($size >= 1024) {
2298 $size = int($size/1024); # size in GB
2299 $text .= "${size}GB";
2300 } else {
2301 $text .= "${size}MB";
2302 }
2303
2304 $text .= ", $model" if $model;
2305 $text .= ")";
2306
2307 } else {
2308 return $devname;
2309 }
2310 }
2311
2312 sub update_layout {
2313 my ($cb, $kmap) = @_;
2314
2315 my $ind;
2316 my $def;
2317 my $i = 0;
2318 my $kmaphash = $cmap->{kmaphash};
2319 foreach my $layout (sort keys %$kmaphash) {
2320 $def = $i if $kmaphash->{$layout} eq 'en-us';
2321 $ind = $i if $kmap && $kmaphash->{$layout} eq $kmap;
2322 $i++;
2323 }
2324
2325 $cb->set_active($ind || $def || 0);
2326 }
2327
2328 my $lastzonecb;
2329 sub update_zonelist {
2330 my ($box, $cc) = @_;
2331
2332 my $cczones = $cmap->{cczones};
2333 my $zones = $cmap->{zones};
2334
2335 my $sel;
2336 if ($lastzonecb) {
2337 $sel = $lastzonecb->get_active_text();
2338 $box->remove ($lastzonecb);
2339 } else {
2340 $sel = $timezone; # used once to select default
2341 }
2342
2343 my $cb = $lastzonecb = Gtk3::ComboBoxText->new();
2344 $cb->set_size_request(200, -1);
2345
2346 $cb->signal_connect('changed' => sub {
2347 $timezone = $cb->get_active_text();
2348 });
2349
2350 my @za;
2351 if ($cc && defined ($cczones->{$cc})) {
2352 @za = keys %{$cczones->{$cc}};
2353 } else {
2354 @za = keys %$zones;
2355 }
2356 my $ind;
2357 my $i = 0;
2358 foreach my $zone (sort @za) {
2359 $ind = $i if $sel && $zone eq $sel;
2360 $cb->append_text($zone);
2361 $i++;
2362 }
2363
2364 $cb->set_active($ind || 0);
2365
2366 $cb->show;
2367 $box->pack_start($cb, 0, 0, 0);
2368 }
2369
2370 sub create_password_view {
2371
2372 cleanup_view();
2373
2374 my $vbox2 = Gtk3::VBox->new(0, 0);
2375 $inbox->pack_start($vbox2, 1, 0, 0);
2376 my $vbox = Gtk3::VBox->new(0, 0);
2377 $vbox2->pack_start($vbox, 0, 0, 10);
2378
2379 my $hbox1 = Gtk3::HBox->new(0, 0);
2380 my $label = Gtk3::Label->new("Password");
2381 $label->set_size_request(150, -1);
2382 $label->set_alignment(1, 0.5);
2383 $hbox1->pack_start($label, 0, 0, 10);
2384 my $pwe1 = Gtk3::Entry->new();
2385 $pwe1->set_visibility(0);
2386 $pwe1->set_text($password) if $password;
2387 $pwe1->set_size_request(200, -1);
2388 $hbox1->pack_start($pwe1, 0, 0, 0);
2389
2390 my $hbox2 = Gtk3::HBox->new(0, 0);
2391 $label = Gtk3::Label->new("Confirm");
2392 $label->set_size_request(150, -1);
2393 $label->set_alignment(1, 0.5);
2394 $hbox2->pack_start($label, 0, 0, 10);
2395 my $pwe2 = Gtk3::Entry->new();
2396 $pwe2->set_visibility(0);
2397 $pwe2->set_text($password) if $password;
2398 $pwe2->set_size_request(200, -1);
2399 $hbox2->pack_start($pwe2, 0, 0, 0);
2400
2401 my $hbox3 = Gtk3::HBox->new(0, 0);
2402 $label = Gtk3::Label->new("E-Mail");
2403 $label->set_size_request(150, -1);
2404 $label->set_alignment(1, 0.5);
2405 $hbox3->pack_start($label, 0, 0, 10);
2406 my $eme = Gtk3::Entry->new();
2407 $eme->set_size_request(200, -1);
2408 $eme->set_text($mailto);
2409 $hbox3->pack_start($eme, 0, 0, 0);
2410
2411
2412 $vbox->pack_start($hbox1, 0, 0, 5);
2413 $vbox->pack_start($hbox2, 0, 0, 5);
2414 $vbox->pack_start($hbox3, 0, 0, 15);
2415
2416 $inbox->show_all;
2417
2418 display_html();
2419
2420 set_next (undef, sub {
2421
2422 my $t1 = $pwe1->get_text;
2423 my $t2 = $pwe2->get_text;
2424
2425 if (length ($t1) < 5) {
2426 display_message("Password is too short.");
2427 $pwe1->grab_focus();
2428 return;
2429 }
2430
2431 if ($t1 ne $t2) {
2432 display_message("Password does not match.");
2433 $pwe1->grab_focus();
2434 return;
2435 }
2436
2437 my $t3 = $eme->get_text;
2438 if ($t3 !~ m/^\S+\@\S+\.\S+$/) {
2439 display_message("E-Mail does not look like a valid address" .
2440 " (user\@domain.tld)");
2441 $eme->grab_focus();
2442 return;
2443 }
2444
2445 if ($t3 eq 'mail@example.invalid') {
2446 display_message("Please enter a valid E-Mail address");
2447 $eme->grab_focus();
2448 return;
2449 }
2450
2451 $password = $t1;
2452 $mailto = $t3;
2453
2454 $step_number++;
2455 create_ipconf_view();
2456 });
2457
2458 $pwe1->grab_focus();
2459
2460 }
2461
2462 sub create_country_view {
2463
2464 cleanup_view();
2465
2466 my $countryhash = $cmap->{countryhash};
2467 my $ctr = $cmap->{country};
2468
2469 my $vbox2 = Gtk3::VBox->new(0, 0);
2470 $inbox->pack_start($vbox2, 1, 0, 0);
2471 my $vbox = Gtk3::VBox->new(0, 0);
2472 $vbox2->pack_start($vbox, 0, 0, 10);
2473
2474 my $w = Gtk3::Entry->new();
2475 $w->set_size_request(200, -1);
2476
2477 my $c = Gtk3::EntryCompletion->new();
2478 $c->set_text_column(0);
2479 $c->set_minimum_key_length(0);
2480 $c->set_popup_set_width(1);
2481 $c->set_inline_completion(1);
2482
2483 my $hbox2 = Gtk3::HBox->new(0, 0);
2484 my $label = Gtk3::Label->new("Time zone");
2485 $label->set_size_request(150, -1);
2486 $label->set_alignment(1, 0.5);
2487 $hbox2->pack_start($label, 0, 0, 10);
2488 update_zonelist ($hbox2);
2489
2490 my $hbox3 = Gtk3::HBox->new(0, 0);
2491 $label = Gtk3::Label->new("Keyboard Layout");
2492 $label->set_size_request(150, -1);
2493 $label->set_alignment(1, 0.5);
2494 $hbox3->pack_start($label, 0, 0, 10);
2495
2496 my $kmapcb = Gtk3::ComboBoxText->new();
2497 $kmapcb->set_size_request (200, -1);
2498 foreach my $layout (sort keys %{$cmap->{kmaphash}}) {
2499 $kmapcb->append_text ($layout);
2500 }
2501
2502 update_layout($kmapcb);
2503 $hbox3->pack_start ($kmapcb, 0, 0, 0);
2504
2505 $kmapcb->signal_connect ('changed' => sub {
2506 my $sel = $kmapcb->get_active_text();
2507 if (my $kmap = $cmap->{kmaphash}->{$sel}) {
2508 my $xkmap = $cmap->{kmap}->{$kmap}->{x11};
2509 my $xvar = $cmap->{kmap}->{$kmap}->{x11var};
2510 syscmd ("setxkbmap $xkmap $xvar") if !$opt_testmode;
2511 $keymap = $kmap;
2512 }
2513 });
2514
2515 $w->signal_connect ('changed' => sub {
2516 my ($entry, $event) = @_;
2517 my $text = $entry->get_text;
2518
2519 if (my $cc = $countryhash->{lc($text)}) {
2520 update_zonelist($hbox2, $cc);
2521 my $kmap = $ctr->{$cc}->{kmap} || 'en-us';
2522 update_layout($kmapcb, $kmap);
2523 }
2524 });
2525
2526 $w->signal_connect (key_press_event => sub {
2527 my ($entry, $event) = @_;
2528 my $text = $entry->get_text;
2529
2530 my $val = $event->get_keyval;
2531
2532 if ($val == Gtk3::Gdk::KEY_Tab) {
2533 my $cc = $countryhash->{lc($text)};
2534
2535 my $found = 0;
2536 my $compl;
2537
2538 if ($cc) {
2539 $found = 1;
2540 $compl = $ctr->{$cc}->{name};
2541 } else {
2542 foreach my $cc (keys %$ctr) {
2543 my $ct = $ctr->{$cc}->{name};
2544 if ($ct =~ m/^\Q$text\E.*$/i) {
2545 $found++;
2546 $compl = $ct;
2547 }
2548 last if $found > 1;
2549 }
2550 }
2551
2552 if ($found == 1) {
2553 $entry->set_text($compl);
2554 $c->complete();
2555 return undef;
2556 } else {
2557 #Gtk3::Gdk::beep();
2558 print chr(7); # beep ?
2559 }
2560
2561 $c->complete();
2562
2563 my $buf = $w->get_buffer();
2564 $buf->insert_text(-1, '', -1); # popup selection
2565
2566 return 1;
2567 }
2568
2569 return undef;
2570 });
2571
2572 my $ls = Gtk3::ListStore->new('Glib::String');
2573 foreach my $cc (sort {$ctr->{$a}->{name} cmp $ctr->{$b}->{name} } keys %$ctr) {
2574 my $iter = $ls->append();
2575 $ls->set ($iter, 0, $ctr->{$cc}->{name});
2576 }
2577 $c->set_model ($ls);
2578
2579 $w->set_completion ($c);
2580
2581 my $hbox = Gtk3::HBox->new(0, 0);
2582
2583 $label = Gtk3::Label->new("Country");
2584 $label->set_alignment(1, 0.5);
2585 $label->set_size_request(150, -1);
2586 $hbox->pack_start($label, 0, 0, 10);
2587 $hbox->pack_start($w, 0, 0, 0);
2588
2589 $vbox->pack_start($hbox, 0, 0, 5);
2590 $vbox->pack_start($hbox2, 0, 0, 5);
2591 $vbox->pack_start($hbox3, 0, 0, 5);
2592
2593 if ($country && $ctr->{$country}) {
2594 $w->set_text ($ctr->{$country}->{name});
2595 }
2596
2597 $inbox->show_all;
2598
2599 display_html();
2600 set_next (undef, sub {
2601
2602 my $text = $w->get_text;
2603
2604 if (my $cc = $countryhash->{lc($text)}) {
2605 $country = $cc;
2606 $step_number++;
2607 create_password_view();
2608 return;
2609 } else {
2610 display_message("Please select a country first.");
2611 $w->grab_focus();
2612 }
2613 });
2614
2615 $w->grab_focus();
2616 }
2617
2618 my $target_hd_combo;
2619 my $target_hd_label;
2620
2621 my $hdoption_first_setup = 1;
2622
2623 my $create_basic_grid = sub {
2624 my $grid = Gtk3::Grid->new();
2625 $grid->set_visible(1);
2626 $grid->set_column_spacing(10);
2627 $grid->set_row_spacing(10);
2628 $grid->set_hexpand(1);
2629
2630 $grid->set_margin_start(5);
2631 $grid->set_margin_end(5);
2632 $grid->set_margin_top(5);
2633 $grid->set_margin_bottom(5);
2634
2635 return $grid;
2636 };
2637
2638 my $create_label_widget_grid = sub {
2639 my ($labeled_widgets) = @_;
2640
2641 my $grid = &$create_basic_grid();
2642 my $row = 0;
2643
2644 for (my $i = 0; $i < @$labeled_widgets; $i += 2) {
2645 my $widget = @$labeled_widgets[$i+1];
2646 my $label = Gtk3::Label->new(@$labeled_widgets[$i]);
2647 $label->set_visible(1);
2648 $label->set_alignment (1, 0.5);
2649 $grid->attach($label, 0, $row, 1, 1);
2650 $widget->set_visible(1);
2651 $grid->attach($widget, 1, $row, 1, 1);
2652 $row++;
2653 }
2654
2655 return $grid;
2656 };
2657
2658 my $create_raid_disk_grid = sub {
2659 my $disk_labeled_widgets = [];
2660 for (my $i = 0; $i < @$hds; $i++) {
2661 my $disk_selector = Gtk3::ComboBoxText->new();
2662 $disk_selector->append_text("-- do not use --");
2663 $disk_selector->set_active(0);
2664 $disk_selector->set_visible(1);
2665 foreach my $hd (@$hds) {
2666 my ($disk, $devname, $size, $model) = @$hd;
2667 $disk_selector->append_text(get_device_desc ($devname, $size, $model));
2668 $disk_selector->{pve_disk_id} = $i;
2669 $disk_selector->signal_connect (changed => sub {
2670 my $w = shift;
2671 my $diskid = $w->{pve_disk_id};
2672 my $a = $w->get_active - 1;
2673 $config_options->{"disksel${diskid}"} = ($a >= 0) ? $hds->[$a] : undef;
2674 });
2675 }
2676
2677 if ($hdoption_first_setup) {
2678 $disk_selector->set_active ($i+1) if $hds->[$i];
2679 } else {
2680 my $hdind = 0;
2681 if (my $cur_hd = $config_options->{"disksel$i"}) {
2682 foreach my $hd (@$hds) {
2683 if (@$hd[1] eq @$cur_hd[1]) {
2684 $disk_selector->set_active($hdind+1);
2685 last;
2686 }
2687 $hdind++;
2688 }
2689 }
2690 }
2691
2692 push @$disk_labeled_widgets, "Harddisk $i", $disk_selector;
2693 }
2694
2695 my $scrolled_window = Gtk3::ScrolledWindow->new();
2696 $scrolled_window->set_hexpand(1);
2697 $scrolled_window->set_propagate_natural_height(1) if @$hds > 4;
2698 $scrolled_window->add(&$create_label_widget_grid($disk_labeled_widgets));
2699 $scrolled_window->set_policy('never', 'automatic');
2700
2701 return $scrolled_window;
2702 # &$create_label_widget_grid($disk_labeled_widgets)
2703 };
2704
2705 # shared between different ui parts (e.g., ZFS and "normal" single disk FS)
2706 my $hdsize_size_adj;
2707 my $hdsize_entry_buffer;
2708
2709 my $get_hdsize_spinbtn = sub {
2710 my $hdsize = shift;
2711
2712 $hdsize_entry_buffer //= Gtk3::EntryBuffer->new(undef, 1);
2713
2714 if (defined($hdsize)) {
2715 $hdsize_size_adj = Gtk3::Adjustment->new($config_options->{hdsize} || $hdsize, 0, $hdsize+1, 1, 1, 1);
2716 } else {
2717 die "called get_hdsize_spinbtn with \$hdsize_size_adj not defined but did not pass hdsize!\n"
2718 if !defined($hdsize_size_adj);
2719 }
2720
2721 my $spinbutton_hdsize = Gtk3::SpinButton->new($hdsize_size_adj, 1, 1);
2722 $spinbutton_hdsize->set_buffer($hdsize_entry_buffer);
2723 $spinbutton_hdsize->set_adjustment($hdsize_size_adj);
2724 $spinbutton_hdsize->set_tooltip_text("only use specified size (GB) of the harddisk (rest left unpartitioned)");
2725 return $spinbutton_hdsize;
2726 };
2727
2728 my $create_raid_advanced_grid = sub {
2729 my $labeled_widgets = [];
2730 my $spinbutton_ashift = Gtk3::SpinButton->new_with_range(9,13,1);
2731 $spinbutton_ashift->set_tooltip_text("zpool ashift property (pool sector size, default 2^12)");
2732 $spinbutton_ashift->signal_connect ("value-changed" => sub {
2733 my $w = shift;
2734 $config_options->{ashift} = $w->get_value_as_int();
2735 });
2736 $config_options->{ashift} = 12 if ! defined($config_options->{ashift});
2737 $spinbutton_ashift->set_value($config_options->{ashift});
2738 push @$labeled_widgets, "ashift";
2739 push @$labeled_widgets, $spinbutton_ashift;
2740
2741 my $combo_compress = Gtk3::ComboBoxText->new();
2742 $combo_compress->set_tooltip_text("zfs compression algorithm for rpool dataset");
2743 # note: gzip / lze not allowed for bootfs vdevs
2744 my $comp_opts = ["on","off","lzjb","lz4"];
2745 foreach my $opt (@$comp_opts) {
2746 $combo_compress->append($opt, $opt);
2747 }
2748 $config_options->{compress} = "on" if !defined($config_options->{compress});
2749 $combo_compress->set_active_id($config_options->{compress});
2750 $combo_compress->signal_connect (changed => sub {
2751 my $w = shift;
2752 $config_options->{compress} = $w->get_active_text();
2753 });
2754 push @$labeled_widgets, "compress";
2755 push @$labeled_widgets, $combo_compress;
2756
2757 my $combo_checksum = Gtk3::ComboBoxText->new();
2758 $combo_checksum->set_tooltip_text("zfs checksum algorithm for rpool dataset");
2759 my $csum_opts = ["on", "off","fletcher2", "fletcher4", "sha256"];
2760 foreach my $opt (@$csum_opts) {
2761 $combo_checksum->append($opt, $opt);
2762 }
2763 $config_options->{checksum} = "on" if !($config_options->{checksum});
2764 $combo_checksum->set_active_id($config_options->{checksum});
2765 $combo_checksum->signal_connect (changed => sub {
2766 my $w = shift;
2767 $config_options->{checksum} = $w->get_active_text();
2768 });
2769 push @$labeled_widgets, "checksum";
2770 push @$labeled_widgets, $combo_checksum;
2771
2772 my $spinbutton_copies = Gtk3::SpinButton->new_with_range(1,3,1);
2773 $spinbutton_copies->set_tooltip_text("zfs copies property for rpool dataset (in addition to RAID redundancy!)");
2774 $spinbutton_copies->signal_connect ("value-changed" => sub {
2775 my $w = shift;
2776 $config_options->{copies} = $w->get_value_as_int();
2777 });
2778 $config_options->{copies} = 1 if !defined($config_options->{copies});
2779 $spinbutton_copies->set_value($config_options->{copies});
2780 push @$labeled_widgets, "copies", $spinbutton_copies;
2781
2782 push @$labeled_widgets, "hdsize", $get_hdsize_spinbtn->();
2783 return &$create_label_widget_grid($labeled_widgets);;
2784 };
2785
2786 sub create_hdoption_view {
2787
2788 my $dialog = Gtk3::Dialog->new();
2789
2790 $dialog->set_title("Harddisk options");
2791
2792 $dialog->add_button("_OK", 1);
2793
2794 my $contarea = $dialog->get_content_area();
2795
2796 my $hbox2 = Gtk3::Box->new('horizontal', 0);
2797 $contarea->pack_start($hbox2, 1, 1, 10);
2798
2799 my $grid = Gtk3::Grid->new();
2800 $grid->set_column_spacing(10);
2801 $grid->set_row_spacing(10);
2802
2803 $hbox2->pack_start($grid, 1, 0, 10);
2804
2805 my $row = 0;
2806
2807 # Filesystem type
2808
2809 my $label0 = Gtk3::Label->new("Filesystem");
2810 $label0->set_alignment (1, 0.5);
2811 $grid->attach($label0, 0, $row, 1, 1);
2812
2813 my $fstypecb = Gtk3::ComboBoxText->new();
2814
2815 my $fstype = ['ext3', 'ext4', 'xfs',
2816 'zfs (RAID0)', 'zfs (RAID1)',
2817 'zfs (RAID10)', 'zfs (RAIDZ-1)',
2818 'zfs (RAIDZ-2)', 'zfs (RAIDZ-3)'];
2819
2820 push @$fstype, 'btrfs (RAID0)', 'btrfs (RAID1)', 'btrfs (RAID10)'
2821 if $setup->{enable_btrfs};
2822
2823 my $tcount = 0;
2824 foreach my $tmp (@$fstype) {
2825 $fstypecb->append_text($tmp);
2826 $fstypecb->set_active ($tcount)
2827 if $config_options->{filesys} eq $tmp;
2828 $tcount++;
2829 }
2830
2831 $grid->attach($fstypecb, 1, $row, 1, 1);
2832
2833 $hbox2->show_all();
2834
2835 $row++;
2836
2837 my $sep = Gtk3::HSeparator->new();
2838 $sep->set_visible(1);
2839 $grid->attach($sep, 0, $row, 2, 1);
2840 $row++;
2841
2842 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.");
2843 $hw_raid_note->set_line_wrap(1);
2844 $hw_raid_note->set_max_width_chars(30);
2845 $hw_raid_note->set_visible(0);
2846 $grid->attach($hw_raid_note, 0, $row++, 2, 1);
2847
2848 my $hdsize_labeled_widgets = [];
2849
2850 # size compute
2851 my $hdsize = 0;
2852 if ( -b $target_hd) {
2853 $hdsize = int(hd_size ($target_hd) / (1024*1024.0)); # size in GB
2854 } elsif ($target_hd) {
2855 $hdsize = int((-s $target_hd) / (1024*1024*1024.0));
2856 }
2857
2858 my $spinbutton_hdsize = $get_hdsize_spinbtn->($hdsize);
2859 push @$hdsize_labeled_widgets, "hdsize", $spinbutton_hdsize;
2860
2861 my $entry_swapsize = Gtk3::Entry->new();
2862 $entry_swapsize->set_tooltip_text("maximum SWAP size (GB)");
2863 $entry_swapsize->signal_connect (key_press_event => \&check_float);
2864 $entry_swapsize->set_text($config_options->{swapsize}) if defined($config_options->{swapsize});
2865 push @$hdsize_labeled_widgets, "swapsize", $entry_swapsize;
2866
2867 my $entry_maxroot = Gtk3::Entry->new();
2868 if ($setup->{product} eq 'pve') {
2869 $entry_maxroot->set_tooltip_text("maximum size (GB) for LVM root volume");
2870 $entry_maxroot->signal_connect (key_press_event => \&check_float);
2871 $entry_maxroot->set_text($config_options->{maxroot}) if $config_options->{maxroot};
2872 push @$hdsize_labeled_widgets, "maxroot", $entry_maxroot;
2873 }
2874
2875 my $entry_minfree = Gtk3::Entry->new();
2876 $entry_minfree->set_tooltip_text("minimum free LVM space (GB, required for LVM snapshots)");
2877 $entry_minfree->signal_connect (key_press_event => \&check_float);
2878 $entry_minfree->set_text($config_options->{minfree}) if defined($config_options->{minfree});
2879 push @$hdsize_labeled_widgets, "minfree", $entry_minfree;
2880
2881 my $entry_maxvz;
2882 if ($setup->{product} eq 'pve') {
2883 $entry_maxvz = Gtk3::Entry->new();
2884 $entry_maxvz->set_tooltip_text("maximum size (GB) for LVM data volume");
2885 $entry_maxvz->signal_connect (key_press_event => \&check_float);
2886 $entry_maxvz->set_text($config_options->{maxvz}) if defined($config_options->{maxvz});
2887 push @$hdsize_labeled_widgets, "maxvz", $entry_maxvz;
2888 }
2889
2890 my $options_stack = Gtk3::Stack->new();
2891 $options_stack->set_visible(1);
2892 $options_stack->set_hexpand(1);
2893 $options_stack->set_vexpand(1);
2894 $options_stack->add_titled(&$create_raid_disk_grid(), "raiddisk", "Disk Setup");
2895 $options_stack->add_titled(&$create_label_widget_grid($hdsize_labeled_widgets), "hdsize", "Size Options");
2896 $options_stack->add_titled(&$create_raid_advanced_grid("zfs"), "raidzfsadvanced", "Advanced Options");
2897 $options_stack->set_visible_child_name("raiddisk");
2898 my $options_stack_switcher = Gtk3::StackSwitcher->new();
2899 $options_stack_switcher->set_halign('center');
2900 $options_stack_switcher->set_stack($options_stack);
2901 $grid->attach($options_stack_switcher, 0, $row, 2, 1);
2902 $row++;
2903 $grid->attach($options_stack, 0, $row, 2, 1);
2904 $row++;
2905
2906 $hdoption_first_setup = 0;
2907
2908 my $switch_view = sub {
2909 my $raid = $config_options->{filesys} =~ m/zfs|btrfs/;
2910 my $enable_zfs_opts = $config_options->{filesys} =~ m/zfs/;
2911
2912 $target_hd_combo->set_visible(!$raid);
2913 $options_stack->get_child_by_name("hdsize")->set_visible(!$raid);
2914 $options_stack->get_child_by_name("raiddisk")->set_visible($raid);
2915 $hw_raid_note->set_visible($raid);
2916 $options_stack_switcher->set_visible($enable_zfs_opts);
2917 $options_stack->get_child_by_name("raidzfsadvanced")->set_visible($enable_zfs_opts);
2918 if ($raid) {
2919 $target_hd_label->set_text("Target: $config_options->{filesys} ");
2920 $options_stack->set_visible_child_name("raiddisk");
2921 } else {
2922 $target_hd_label->set_text("Target Harddisk: ");
2923 }
2924 my (undef, $pref_width) = $dialog->get_preferred_width();
2925 my (undef, $pref_height) = $dialog->get_preferred_height();
2926 $pref_height = 750 if $pref_height > 750;
2927 $dialog->resize($pref_width, $pref_height);
2928 };
2929
2930 &$switch_view();
2931
2932 $fstypecb->signal_connect (changed => sub {
2933 $config_options->{filesys} = $fstypecb->get_active_text();
2934 &$switch_view();
2935 });
2936
2937 my $sep2 = Gtk3::HSeparator->new();
2938 $sep2->set_visible(1);
2939 $contarea->pack_end($sep2, 1, 1, 10);
2940
2941 $dialog->show();
2942
2943 $dialog->run();
2944
2945 my $get_float = sub {
2946 my ($entry) = @_;
2947
2948 my $text = $entry->get_text();
2949 return undef if !defined($text);
2950
2951 $text =~ s/^\s+//;
2952 $text =~ s/\s+$//;
2953
2954 return undef if $text !~ m/^\d+(\.\d+)?$/;
2955
2956 return $text;
2957 };
2958
2959 my $tmp;
2960
2961 if (($tmp = &$get_float($spinbutton_hdsize)) && ($tmp != $hdsize)) {
2962 $config_options->{hdsize} = $tmp;
2963 } else {
2964 delete $config_options->{hdsize};
2965 }
2966
2967 if (defined($tmp = &$get_float($entry_swapsize))) {
2968 $config_options->{swapsize} = $tmp;
2969 } else {
2970 delete $config_options->{swapsize};
2971 }
2972
2973 if (defined($tmp = &$get_float($entry_maxroot))) {
2974 $config_options->{maxroot} = $tmp;
2975 } else {
2976 delete $config_options->{maxroot};
2977 }
2978
2979 if (defined($tmp = &$get_float($entry_minfree))) {
2980 $config_options->{minfree} = $tmp;
2981 } else {
2982 delete $config_options->{minfree};
2983 }
2984
2985 if ($entry_maxvz && defined($tmp = &$get_float($entry_maxvz))) {
2986 $config_options->{maxvz} = $tmp;
2987 } else {
2988 delete $config_options->{maxvz};
2989 }
2990
2991 $dialog->destroy();
2992 }
2993
2994 my $get_raid_devlist = sub {
2995
2996 my $dev_name_hash = {};
2997
2998 my $devlist = [];
2999 for (my $i = 0; $i < @$hds; $i++) {
3000 if (my $hd = $config_options->{"disksel$i"}) {
3001 my ($disk, $devname, $size, $model) = @$hd;
3002 die "device '$devname' is used more than once\n"
3003 if $dev_name_hash->{$devname};
3004 $dev_name_hash->{$devname} = $hd;
3005 push @$devlist, $hd;
3006 }
3007 }
3008
3009 return $devlist;
3010 };
3011
3012 sub zfs_mirror_size_check {
3013 my ($expected, $actual) = @_;
3014
3015 die "mirrored disks must have same size\n"
3016 if abs($expected - $actual) > $expected / 10;
3017 }
3018
3019 sub get_zfs_raid_setup {
3020
3021 my $filesys = $config_options->{filesys};
3022
3023 my $devlist = &$get_raid_devlist();
3024
3025 my $diskcount = scalar(@$devlist);
3026 die "$filesys needs at least one device\n" if $diskcount < 1;
3027
3028 my $bootdevlist = [];
3029
3030 my $cmd= '';
3031 if ($filesys eq 'zfs (RAID0)') {
3032 push @$bootdevlist, @$devlist[0];
3033 foreach my $hd (@$devlist) {
3034 $cmd .= " @$hd[1]";
3035 }
3036 } elsif ($filesys eq 'zfs (RAID1)') {
3037 die "zfs (RAID1) needs at least 2 device\n" if $diskcount < 2;
3038 $cmd .= ' mirror ';
3039 my $hd = @$devlist[0];
3040 my $expected_size = @$hd[2]; # all disks need approximately same size
3041 foreach $hd (@$devlist) {
3042 zfs_mirror_size_check($expected_size, @$hd[2]);
3043 $cmd .= " @$hd[1]";
3044 push @$bootdevlist, $hd;
3045 }
3046 } elsif ($filesys eq 'zfs (RAID10)') {
3047 die "zfs (RAID10) needs at least 4 device\n" if $diskcount < 4;
3048 die "zfs (RAID10) needs an even number of devices\n" if $diskcount & 1;
3049
3050 push @$bootdevlist, @$devlist[0], @$devlist[1];
3051
3052 for (my $i = 0; $i < $diskcount; $i+=2) {
3053 my $hd1 = @$devlist[$i];
3054 my $hd2 = @$devlist[$i+1];
3055 zfs_mirror_size_check(@$hd1[2], @$hd2[2]); # pairs need approximately same size
3056 $cmd .= ' mirror ' . @$hd1[1] . ' ' . @$hd2[1];
3057 }
3058
3059 } elsif ($filesys =~ m/^zfs \(RAIDZ-([123])\)$/) {
3060 my $level = $1;
3061 my $mindisks = 2 + $level;
3062 die "zfs (RAIDZ-$level) needs at least $mindisks devices\n" if scalar(@$devlist) < $mindisks;
3063 my $hd = @$devlist[0];
3064 my $expected_size = @$hd[2]; # all disks need approximately same size
3065 $cmd .= " raidz$level";
3066 foreach $hd (@$devlist) {
3067 zfs_mirror_size_check($expected_size, @$hd[2]);
3068 $cmd .= " @$hd[1]";
3069 push @$bootdevlist, $hd;
3070 }
3071 } else {
3072 die "unknown zfs mode '$filesys'\n";
3073 }
3074
3075 return ($devlist, $bootdevlist, $cmd);
3076 }
3077
3078 sub get_btrfs_raid_setup {
3079
3080 my $filesys = $config_options->{filesys};
3081
3082 my $devlist = &$get_raid_devlist();
3083
3084 my $diskcount = scalar(@$devlist);
3085 die "$filesys needs at least one device\n" if $diskcount < 1;
3086
3087 my $mode;
3088
3089 if ($diskcount == 1) {
3090 $mode = 'single';
3091 } else {
3092 if ($filesys eq 'btrfs (RAID0)') {
3093 $mode = 'raid0';
3094 } elsif ($filesys eq 'btrfs (RAID1)') {
3095 die "btrfs (RAID1) needs at least 2 device\n" if $diskcount < 2;
3096 $mode = 'raid1';
3097 } elsif ($filesys eq 'btrfs (RAID10)') {
3098 die "btrfs (RAID10) needs at least 4 device\n" if $diskcount < 4;
3099 $mode = 'raid10';
3100 } else {
3101 die "unknown btrfs mode '$filesys'\n";
3102 }
3103 }
3104
3105 return ($devlist, $mode);
3106 }
3107
3108 my $last_hd_selected = 0;
3109 sub create_hdsel_view {
3110
3111 $prev_btn->set_sensitive(1); # enable previous button at this point
3112
3113 cleanup_view();
3114
3115 my $vbox = Gtk3::VBox->new(0, 0);
3116 $inbox->pack_start($vbox, 1, 0, 0);
3117 my $hbox = Gtk3::HBox->new(0, 0);
3118 $vbox->pack_start($hbox, 0, 0, 10);
3119
3120 my ($disk, $devname, $size, $model) = @{@$hds[0]};
3121 $target_hd = $devname if !defined($target_hd);
3122
3123 $target_hd_label = Gtk3::Label->new("Target Harddisk: ");
3124 $hbox->pack_start($target_hd_label, 0, 0, 0);
3125
3126 $target_hd_combo = Gtk3::ComboBoxText->new();
3127
3128 foreach my $hd (@$hds) {
3129 ($disk, $devname, $size, $model) = @$hd;
3130 $target_hd_combo->append_text (get_device_desc($devname, $size, $model));
3131 }
3132
3133 my $raid = $config_options->{filesys} =~ m/zfs|btrfs/;
3134 if ($raid) {
3135 $target_hd_label->set_text("Target: $config_options->{filesys} ");
3136 $target_hd_combo->set_visible(0);
3137 $target_hd_combo->set_no_show_all(1);
3138 }
3139 $target_hd_combo->set_active($last_hd_selected);
3140 $target_hd_combo->signal_connect(changed => sub {
3141 $a = shift->get_active;
3142 my ($disk, $devname) = @{@$hds[$a]};
3143 $last_hd_selected = $a;
3144 $target_hd = $devname;
3145 });
3146
3147 $hbox->pack_start($target_hd_combo, 0, 0, 10);
3148
3149 my $options = Gtk3::Button->new('_Options');
3150 $options->signal_connect (clicked => \&create_hdoption_view);
3151 $hbox->pack_start ($options, 0, 0, 0);
3152
3153
3154 $inbox->show_all;
3155
3156 display_html();
3157
3158 set_next(undef, sub {
3159
3160 if ($config_options->{filesys} =~ m/zfs/) {
3161 my ($devlist) = eval { get_zfs_raid_setup() };
3162 if (my $err = $@) {
3163 display_message("Warning: $err\nPlease fix ZFS setup first.");
3164 return;
3165 }
3166 $config_options->{target_hds} = [ map { $_->[1] } @$devlist ];
3167 } elsif ($config_options->{filesys} =~ m/btrfs/) {
3168 my ($devlist) = eval { get_btrfs_raid_setup() };
3169 if (my $err = $@) {
3170 display_message("Warning: $err\nPlease fix BTRFS setup first.");
3171 return;
3172 }
3173 $config_options->{target_hds} = [ map { $_->[1] } @$devlist ];
3174 } else {
3175 $config_options->{target_hds} = [ $target_hd ];
3176 }
3177
3178 $step_number++;
3179 create_country_view();
3180 });
3181 }
3182
3183 sub create_extract_view {
3184
3185 cleanup_view();
3186
3187 display_info();
3188
3189 $next->set_sensitive(0);
3190 $prev_btn->set_sensitive(0);
3191 $prev_btn->hide();
3192
3193 my $vbox = Gtk3::VBox->new(0, 0);
3194 $inbox->pack_start ($vbox, 1, 0, 0);
3195 my $hbox = Gtk3::HBox->new(0, 0);
3196 $vbox->pack_start ($hbox, 0, 0, 10);
3197
3198 my $vbox2 = Gtk3::VBox->new(0, 0);
3199 $hbox->pack_start ($vbox2, 0, 0, 0);
3200
3201 $progress_status = Gtk3::Label->new('');
3202 $vbox2->pack_start ($progress_status, 1, 1, 0);
3203
3204 $progress = Gtk3::ProgressBar->new;
3205 $progress->set_show_text(1);
3206 $progress->set_size_request (600, -1);
3207
3208 $vbox2->pack_start($progress, 0, 0, 0);
3209
3210 $inbox->show_all();
3211
3212 my $tdir = $opt_testmode ? "target" : "/target";
3213 mkdir $tdir;
3214 my $base = "${proxmox_cddir}/$setup->{product}-base.squashfs";
3215
3216 eval { extract_data($base, $tdir); };
3217 my $err = $@;
3218
3219 $next->set_sensitive(1);
3220
3221 set_next("_Reboot", sub { exit (0); } );
3222
3223 if ($err) {
3224 display_html("fail.htm");
3225 display_error($err);
3226 } else {
3227 cleanup_view();
3228 display_html("success.htm");
3229 }
3230 }
3231
3232 sub create_intro_view {
3233
3234 $prev_btn->set_sensitive(0);
3235
3236 cleanup_view();
3237
3238 if ($setup->{product} eq 'pve') {
3239 eval {
3240 my $cpuinfo = file_get_contents('/proc/cpuinfo');
3241 if ($cpuinfo && !($cpuinfo =~ /^flags\s*:.*(vmx|svm)/m)) {
3242 display_error("No support for KVM virtualisation detected.\n\n" .
3243 "Check BIOS settings for Intel VT / AMD-V / SVM.")
3244 }
3245 };
3246 }
3247
3248 display_html();
3249
3250 $step_number++;
3251 set_next("I a_gree", \&create_hdsel_view);
3252 }
3253
3254 $ipconf = get_ip_config();
3255
3256 $country = detect_country() if $ipconf->{default} || $opt_testmode;
3257
3258 # read country, kmap and timezone infos
3259 $cmap = read_cmap();
3260
3261 if (!defined($cmap->{country}->{$country})) {
3262 print $logfd "ignoring detected country '$country', invalid or unknown\n";
3263 $country = undef;
3264 }
3265
3266 create_main_window ();
3267
3268 my $initial_error = 0;
3269
3270 if (!defined ($hds) || (scalar (@$hds) <= 0)) {
3271 print "no hardisks found\n";
3272 $initial_error = 1;
3273 display_html("nohds.htm");
3274 set_next("Reboot", sub { exit(0); } );
3275 } else {
3276 foreach my $hd (@$hds) {
3277 my ($disk, $devname) = @$hd;
3278 next if $devname =~ m|^/dev/md\d+$|;
3279 print "found Disk$disk N:$devname\n";
3280 }
3281 }
3282
3283 if (!$initial_error && (scalar keys %{ $ipconf->{ifaces} } == 0)) {
3284 print "no network interfaces found\n";
3285 $initial_error = 1;
3286 display_html("nonics.htm");
3287 set_next("Reboot", sub { exit(0); } );
3288 }
3289
3290 create_intro_view () if !$initial_error;
3291
3292 Gtk3->main;
3293
3294 exit 0;