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