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