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