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