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