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