]> git.proxmox.com Git - pve-installer.git/blob - proxinstall
fix #3905: allow lze/gzip/zstd compression for ZFS
[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 zfs_mirror_size_check($disksize, $size) if $disksize;
1384
1385 push @$bootdevinfo, {
1386 esp => $efidev,
1387 devname => $devname,
1388 osdev => $osdev,
1389 logical_bsize => $logical_bsize,
1390 };
1391 $disksize = $size;
1392 }
1393
1394 $udevadm_trigger_block->();
1395
1396 foreach my $di (@$bootdevinfo) {
1397 my $devname = $di->{devname};
1398 $di->{by_id} = find_stable_path ("/dev/disk/by-id", $devname);
1399
1400 my $osdev = find_stable_path ("/dev/disk/by-id", $di->{osdev}) || $di->{osdev};
1401
1402 $vdev =~ s/ $devname/ $osdev/;
1403 }
1404
1405 foreach my $hd (@$devlist) {
1406 my $devname = @$hd[1];
1407 my $by_id = find_stable_path ("/dev/disk/by-id", $devname);
1408
1409 $vdev =~ s/ $devname/ $by_id/ if $by_id;
1410 }
1411
1412 update_progress(0, 0.03, $maxper, "create rpool");
1413
1414 zfs_create_rpool($vdev);
1415
1416 } else {
1417
1418 die "target '$target_hd' is not a valid block device\n" if ! -b $target_hd;
1419
1420 $clean_disk->($target_hd);
1421
1422 update_progress(0, 0.02, $maxper, "create partitions");
1423
1424 my $logical_bsize = logical_blocksize($target_hd);
1425
1426 my ($os_size, $osdev, $efidev);
1427 ($os_size, $osdev, $efidev) =
1428 partition_bootable_disk($target_hd, $config_options->{hdsize}, '8E00');
1429
1430 &$udevadm_trigger_block();
1431
1432 my $by_id = find_stable_path ("/dev/disk/by-id", $target_hd);
1433 push @$bootdevinfo, {
1434 esp => $efidev,
1435 devname => $target_hd,
1436 osdev => $osdev,
1437 by_id => $by_id,
1438 logical_bsize => $logical_bsize,
1439 };
1440
1441 update_progress(0, 0.03, $maxper, "create LVs");
1442
1443 my $swap_size = compute_swapsize($os_size);
1444 ($rootdev, $swapfile, $datadev) =
1445 create_lvm_volumes($osdev, $os_size, $swap_size);
1446
1447 # trigger udev to create /dev/disk/by-uuid
1448 &$udevadm_trigger_block(1);
1449 }
1450
1451 if ($use_zfs) {
1452 # to be fast during installation
1453 syscmd("zfs set sync=disabled $zfspoolname") == 0 ||
1454 die "unable to set zfs properties\n";
1455 }
1456
1457 update_progress(0.04, 0, $maxper, "create swap space");
1458 if ($swapfile) {
1459 syscmd("mkswap -f $swapfile") == 0 ||
1460 die "unable to create swap space\n";
1461 }
1462
1463 update_progress(0.045, 0, $maxper, "creating root filesystems");
1464
1465 foreach my $di (@$bootdevinfo) {
1466 next if !$di->{esp};
1467 # FIXME remove '-s1' once https://github.com/dosfstools/dosfstools/issues/111 is fixed
1468 my $vfat_extra_opts = ($di->{logical_bsize} == 4096) ? '-s1' : '';
1469 syscmd("mkfs.vfat $vfat_extra_opts -F32 $di->{esp}") == 0 ||
1470 die "unable to initialize EFI ESP on device $di->{esp}\n";
1471 }
1472
1473 if ($use_zfs) {
1474 # do nothing
1475 } elsif ($use_btrfs) {
1476 # do nothing
1477 } else {
1478 create_filesystem($rootdev, 'root', $filesys, 0.05, $maxper, 0, 1);
1479 }
1480
1481 update_progress(1, 0.05, $maxper, "mounting target $rootdev");
1482
1483 if ($use_zfs) {
1484 # do nothing
1485 } else {
1486 my $mount_opts = 'noatime';
1487 $mount_opts .= ',nobarrier'
1488 if $use_btrfs || $filesys =~ /^ext\d$/;
1489
1490 syscmd("mount -n $rootdev -o $mount_opts $targetdir") == 0 ||
1491 die "unable to mount $rootdev\n";
1492 }
1493
1494 mkdir "$targetdir/boot";
1495 mkdir "$targetdir/boot/efi";
1496
1497 mkdir "$targetdir/var";
1498 mkdir "$targetdir/var/lib";
1499
1500 if ($setup->{product} eq 'pve') {
1501 mkdir "$targetdir/var/lib/vz";
1502 mkdir "$targetdir/var/lib/pve";
1503
1504 if ($use_btrfs) {
1505 syscmd("btrfs subvolume create $targetdir/var/lib/pve/local-btrfs") == 0 ||
1506 die "unable to create btrfs subvolume\n";
1507 }
1508 }
1509
1510 mkdir "$targetdir/mnt";
1511 mkdir "$targetdir/mnt/hostrun";
1512 syscmd("mount --bind /run $targetdir/mnt/hostrun") == 0 ||
1513 die "unable to bindmount run on $targetdir/mnt/hostrun\n";
1514
1515 update_progress(1, 0.05, $maxper, "extracting base system");
1516
1517 my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size) = stat ($basefile);
1518 $ino || die "unable to open file '$basefile' - $!\n";
1519
1520 my $files = file_read_firstline("${proxmox_cddir}/proxmox/$setup->{product}-base.cnt") ||
1521 die "unable to read base file count\n";
1522
1523 my $per = 0;
1524 my $count = 0;
1525
1526 run_command("unsquashfs -f -dest $targetdir -i $basefile", sub {
1527 my $line = shift;
1528 return if $line !~ m/^$targetdir/;
1529 $count++;
1530 my $nper = int (($count *100)/$files);
1531 if ($nper != $per) {
1532 $per = $nper;
1533 my $frac = $per > 100 ? 1 : $per/100;
1534 update_progress($frac, $maxper, 0.5);
1535 }
1536 });
1537
1538 syscmd("mount -n -t tmpfs tmpfs $targetdir/tmp") == 0 || die "unable to mount tmpfs on $targetdir/tmp\n";
1539
1540 mkdir "$targetdir/tmp/pkg";
1541 syscmd("mount -n --bind '$proxmox_pkgdir' '$targetdir/tmp/pkg'") == 0
1542 || die "unable to bind-mount packages on $targetdir/tmp/pkg\n";
1543 syscmd("mount -n -t proc proc $targetdir/proc") == 0 || die "unable to mount proc on $targetdir/proc\n";
1544 syscmd("mount -n -t sysfs sysfs $targetdir/sys") == 0 || die "unable to mount sysfs on $targetdir/sys\n";
1545 if ($boot_type eq 'efi') {
1546 syscmd("mount -n -t efivarfs efivarfs $targetdir/sys/firmware/efi/efivars") == 0 ||
1547 die "unable to mount efivarfs on $targetdir/sys/firmware/efi/efivars: $!\n";
1548 }
1549 syscmd("chroot $targetdir mount --bind /mnt/hostrun /run") == 0 ||
1550 die "unable to re-bindmount hostrun on /run in chroot\n";
1551
1552 update_progress(1, $maxper, 0.5, "configuring base system");
1553
1554 # configure hosts
1555
1556 my $hosts =
1557 "127.0.0.1 localhost.localdomain localhost\n" .
1558 "$ipaddress $hostname.$domain $hostname\n\n" .
1559 "# The following lines are desirable for IPv6 capable hosts\n\n" .
1560 "::1 ip6-localhost ip6-loopback\n" .
1561 "fe00::0 ip6-localnet\n" .
1562 "ff00::0 ip6-mcastprefix\n" .
1563 "ff02::1 ip6-allnodes\n" .
1564 "ff02::2 ip6-allrouters\n" .
1565 "ff02::3 ip6-allhosts\n";
1566
1567 write_config($hosts, "$targetdir/etc/hosts");
1568
1569 write_config("$hostname\n", "$targetdir/etc/hostname");
1570
1571 syscmd("/bin/hostname $hostname") if !$opt_testmode;
1572
1573 # configure interfaces
1574
1575 my $ifaces = "auto lo\niface lo inet loopback\n\n";
1576
1577 my $ntype = $ipversion == 4 ? 'inet' : 'inet6';
1578
1579 my $ethdev = $ipconf->{ifaces}->{$ipconf->{selected}}->{name};
1580
1581 if ($setup->{bridged_network}) {
1582 $ifaces .= "iface $ethdev $ntype manual\n";
1583
1584 $ifaces .=
1585 "\nauto vmbr0\niface vmbr0 $ntype static\n" .
1586 "\taddress $cidr\n" .
1587 "\tgateway $gateway\n" .
1588 "\tbridge-ports $ethdev\n" .
1589 "\tbridge-stp off\n" .
1590 "\tbridge-fd 0\n";
1591 } else {
1592 $ifaces .= "auto $ethdev\n" .
1593 "iface $ethdev $ntype static\n" .
1594 "\taddress $cidr\n" .
1595 "\tgateway $gateway\n";
1596 }
1597
1598 foreach my $iface (sort keys %{$ipconf->{ifaces}}) {
1599 my $name = $ipconf->{ifaces}->{$iface}->{name};
1600 next if $name eq $ethdev;
1601
1602 $ifaces .= "\niface $name $ntype manual\n";
1603 }
1604
1605 write_config($ifaces, "$targetdir/etc/network/interfaces");
1606
1607 # configure dns
1608
1609 my $resolvconf = "search $domain\nnameserver $dnsserver\n";
1610 write_config($resolvconf, "$targetdir/etc/resolv.conf");
1611
1612 # configure fstab
1613
1614 my $fstab = "# <file system> <mount point> <type> <options> <dump> <pass>\n";
1615
1616 if ($use_zfs) {
1617 # do nothing
1618 } elsif ($use_btrfs) {
1619 my $fsuuid;
1620 my $cmd = "blkid -u filesystem -t TYPE=btrfs -o export $rootdev";
1621 run_command($cmd, sub {
1622 my $line = shift;
1623
1624 if ($line =~ m/^UUID=([A-Fa-f0-9\-]+)$/) {
1625 $fsuuid = $1;
1626 }
1627 });
1628
1629 die "unable to detect FS UUID" if !defined($fsuuid);
1630
1631 $fstab .= "UUID=$fsuuid / btrfs defaults 0 1\n";
1632 } else {
1633 my $root_mountopt = $fssetup->{$filesys}->{root_mountopt} || 'defaults';
1634 $fstab .= "$rootdev / $filesys ${root_mountopt} 0 1\n";
1635 }
1636
1637 # mount /boot/efi
1638 # Note: this is required by current grub, but really dangerous, because
1639 # vfat does not have journaling, so it triggers manual fsck after each crash
1640 # so we only mount /boot/efi if really required (efi systems).
1641 if ($boot_type eq 'efi' && !$use_zfs) {
1642 if (scalar(@$bootdevinfo)) {
1643 my $di = @$bootdevinfo[0]; # simply use first disk
1644
1645 if ($di->{esp}) {
1646 my $efi_boot_uuid = $di->{esp};
1647 if (my $uuid = find_dev_by_uuid ($di->{esp})) {
1648 $efi_boot_uuid = "UUID=$uuid";
1649 }
1650
1651 $fstab .= "${efi_boot_uuid} /boot/efi vfat defaults 0 1\n";
1652 }
1653 }
1654 }
1655
1656
1657 $fstab .= "$swapfile none swap sw 0 0\n" if $swapfile;
1658
1659 $fstab .= "proc /proc proc defaults 0 0\n";
1660
1661 write_config($fstab, "$targetdir/etc/fstab");
1662 write_config("", "$targetdir/etc/mtab");
1663
1664 syscmd("cp ${proxmox_libdir}/policy-disable-rc.d " .
1665 "$targetdir/usr/sbin/policy-rc.d") == 0 ||
1666 die "unable to copy policy-rc.d\n";
1667 syscmd("cp ${proxmox_libdir}/fake-start-stop-daemon " .
1668 "$targetdir/sbin/") == 0 ||
1669 die "unable to copy start-stop-daemon\n";
1670
1671 diversion_add($targetdir, "/sbin/start-stop-daemon", "/sbin/fake-start-stop-daemon");
1672 diversion_add($targetdir, "/usr/sbin/update-grub", "/bin/true");
1673 diversion_add($targetdir, "/usr/sbin/update-initramfs", "/bin/true");
1674
1675 my $machine_id = run_command("systemd-id128 new");
1676 die "unable to create a new machine-id\n" if ! $machine_id;
1677 write_config($machine_id, "$targetdir/etc/machine-id");
1678
1679 syscmd("cp /etc/hostid $targetdir/etc/") == 0 ||
1680 die "unable to copy hostid\n";
1681
1682 syscmd("touch $targetdir/proxmox_install_mode");
1683
1684 my $grub_install_devices_txt = '';
1685 foreach my $di (@$bootdevinfo) {
1686 $grub_install_devices_txt .= ', ' if $grub_install_devices_txt;
1687 $grub_install_devices_txt .= $di->{by_id} || $di->{devname};
1688 }
1689
1690 # Note: keyboard-configuration/xbkb-keymap is used by console-setup
1691 my $xkmap = $cmap->{kmap}->{$keymap}->{x11} // 'us';
1692
1693 debconfig_set ($targetdir, <<_EOD);
1694 locales locales/default_environment_locale select en_US.UTF-8
1695 locales locales/locales_to_be_generated select en_US.UTF-8 UTF-8
1696 samba-common samba-common/dhcp boolean false
1697 samba-common samba-common/workgroup string WORKGROUP
1698 postfix postfix/main_mailer_type select No configuration
1699 keyboard-configuration keyboard-configuration/xkb-keymap select $xkmap
1700 d-i debian-installer/locale select en_US.UTF-8
1701 grub-pc grub-pc/install_devices select $grub_install_devices_txt
1702 _EOD
1703
1704 my $pkg_count = 0;
1705 while (<${proxmox_pkgdir}/*.deb>) { $pkg_count++ };
1706
1707 # btrfs/dpkg is extremely slow without --force-unsafe-io
1708 my $dpkg_opts = $use_btrfs ? "--force-unsafe-io" : "";
1709
1710 $count = 0;
1711 while (<${proxmox_pkgdir}/*.deb>) {
1712 chomp;
1713 my $path = $_;
1714 my ($deb) = $path =~ m/${proxmox_pkgdir}\/(.*\.deb)/;
1715 update_progress($count/$pkg_count, 0.5, 0.75, "extracting $deb");
1716 print "extracting: $deb\n";
1717 syscmd("chroot $targetdir dpkg $dpkg_opts --force-depends --no-triggers --unpack /tmp/pkg/$deb") == 0
1718 || die "installation of package $deb failed\n";
1719 update_progress((++$count)/$pkg_count, 0.5, 0.75);
1720 }
1721
1722 # needed for postfix postinst in case no other NIC is active
1723 syscmd("chroot $targetdir ifup lo");
1724
1725 my $cmd = "chroot $targetdir dpkg $dpkg_opts --force-confold --configure -a";
1726 $count = 0;
1727 run_command($cmd, sub {
1728 my $line = shift;
1729 if ($line =~ m/Setting up\s+(\S+)/) {
1730 update_progress((++$count)/$pkg_count, 0.75, 0.95, "configuring $1");
1731 }
1732 });
1733
1734 unlink "$targetdir/etc/mailname";
1735 $postfix_main_cf =~ s/__FQDN__/${hostname}.${domain}/;
1736 write_config($postfix_main_cf, "$targetdir/etc/postfix/main.cf");
1737
1738 # make sure we have all postfix directories
1739 syscmd("chroot $targetdir /usr/sbin/postfix check");
1740 # cleanup mail queue
1741 syscmd("chroot $targetdir /usr/sbin/postsuper -d ALL");
1742 # create /etc/aliases.db (/etc/aliases is shipped in the base squashfs)
1743 syscmd("chroot $targetdir /usr/bin/newaliases");
1744
1745 # enable NTP (timedatectl set-ntp true does not work without DBUS)
1746 syscmd("chroot $targetdir /bin/systemctl enable systemd-timesyncd.service");
1747
1748 unlink "$targetdir/proxmox_install_mode";
1749
1750 # set timezone
1751 unlink ("$targetdir/etc/localtime");
1752 symlink ("/usr/share/zoneinfo/$timezone", "$targetdir/etc/localtime");
1753 write_config("$timezone\n", "$targetdir/etc/timezone");
1754
1755 # set apt mirror
1756 if (my $mirror = $cmap->{country}->{$country}->{mirror}) {
1757 my $fn = "$targetdir/etc/apt/sources.list";
1758 syscmd("sed -i 's/ftp\\.debian\\.org/$mirror/' '$fn'");
1759 }
1760
1761 # create extended_states for apt (avoid cron job warning if that
1762 # file does not exist)
1763 write_config('', "$targetdir/var/lib/apt/extended_states");
1764
1765 # allow ssh root login
1766 syscmd(['sed', '-i', 's/^#\?PermitRootLogin.*/PermitRootLogin yes/', "$targetdir/etc/ssh/sshd_config"]);
1767
1768 if ($setup->{product} eq 'pmg') {
1769 # install initial clamav DB
1770 my $srcdir = "${proxmox_cddir}/proxmox/clamav";
1771 foreach my $fn ("main.cvd", "bytecode.cvd", "daily.cvd", "safebrowsing.cvd") {
1772 syscmd("cp \"$srcdir/$fn\" \"$targetdir/var/lib/clamav\"") == 0 ||
1773 die "installation of clamav db file '$fn' failed\n";
1774 }
1775 syscmd("chroot $targetdir /bin/chown clamav:clamav -R /var/lib/clamav") == 0 ||
1776 die "unable to set owner for clamav database files\n";
1777 }
1778
1779 if ($setup->{product} eq 'pve') {
1780 # save installer settings
1781 my $ucc = uc ($country);
1782 debconfig_set($targetdir, "pve-manager pve-manager/country string $ucc\n");
1783 }
1784
1785 update_progress(0.8, 0.95, 1, "make system bootable");
1786
1787 if ($use_zfs) {
1788 # add ZFS options while preserving existing kernel cmdline
1789 my $zfs_snippet = "GRUB_CMDLINE_LINUX=\"\$GRUB_CMDLINE_LINUX root=ZFS=$zfspoolname/ROOT/$zfsrootvolname boot=zfs\"";
1790 write_config($zfs_snippet, "$targetdir/etc/default/grub.d/zfs.cfg");
1791
1792 write_config("root=ZFS=$zfspoolname/ROOT/$zfsrootvolname boot=zfs\n", "$targetdir/etc/kernel/cmdline");
1793
1794 }
1795
1796 diversion_remove($targetdir, "/usr/sbin/update-grub");
1797 diversion_remove($targetdir, "/usr/sbin/update-initramfs");
1798
1799 my $kapi;
1800 foreach my $fn (<$targetdir/lib/modules/*>) {
1801 if ($fn =~ m!/(\d+\.\d+\.\d+-\d+-pve)$!) {
1802 die "found multiple kernels\n" if defined($kapi);
1803 $kapi = $1;
1804 }
1805 }
1806 die "unable to detect kernel version\n" if !defined($kapi);
1807
1808 if (!$opt_testmode) {
1809
1810 unlink ("$targetdir/etc/mtab");
1811 symlink ("/proc/mounts", "$targetdir/etc/mtab");
1812 syscmd("mount -n --bind /dev $targetdir/dev");
1813
1814 my $bootloader_err_list = [];
1815 eval {
1816 syscmd("chroot $targetdir /usr/sbin/update-initramfs -c -k $kapi") == 0 ||
1817 die "unable to install initramfs\n";
1818
1819 my $native_4k_disk_bootable = 0;
1820 foreach my $di (@$bootdevinfo) {
1821 $native_4k_disk_bootable |= ($di->{logical_bsize} == 4096);
1822 }
1823
1824 foreach my $di (@$bootdevinfo) {
1825 my $dev = $di->{devname};
1826 if ($use_zfs) {
1827 prepare_proxmox_boot_esp($di->{esp}, $targetdir);
1828 } else {
1829 if (!$native_4k_disk_bootable) {
1830 eval {
1831 syscmd("chroot $targetdir /usr/sbin/grub-install --target i386-pc --no-floppy --bootloader-id='proxmox' $dev") == 0 ||
1832 die "unable to install the i386-pc boot loader on '$dev'\n";
1833 };
1834 push @$bootloader_err_list, $@ if $@;
1835 }
1836
1837 eval {
1838 if (my $esp = $di->{esp}) {
1839 prepare_grub_efi_boot_esp($dev, $esp, $targetdir);
1840 }
1841 }
1842 };
1843 push @$bootloader_err_list, $@ if $@;
1844 }
1845
1846 syscmd("chroot $targetdir /usr/sbin/update-grub") == 0 ||
1847 die "unable to update boot loader config\n";
1848 };
1849 push @$bootloader_err_list, $@ if $@;
1850
1851 if (scalar(@$bootloader_err_list) > 0) {
1852 $bootloader_err = "bootloader setup errors:\n";
1853 map { $bootloader_err .= "- $_" } @$bootloader_err_list;
1854 warn $bootloader_err;
1855 }
1856
1857 syscmd("umount $targetdir/dev");
1858 }
1859
1860 # cleanup
1861
1862 unlink "$targetdir/usr/sbin/policy-rc.d";
1863
1864 diversion_remove($targetdir, "/sbin/start-stop-daemon");
1865
1866 # set root password
1867 my $octets = encode("utf-8", $password);
1868 run_command("chroot $targetdir /usr/sbin/chpasswd", undef,
1869 "root:$octets\n");
1870
1871 if ($setup->{product} eq 'pmg') {
1872 # save admin email
1873 write_config("section: admin\n\temail ${mailto}\n",
1874 "$targetdir/etc/pmg/pmg.conf");
1875
1876 } elsif ($setup->{product} eq 'pve') {
1877
1878 # create pmxcfs DB
1879
1880 my $tmpdir = "$targetdir/tmp/pve";
1881 mkdir $tmpdir;
1882
1883 # write vnc keymap to datacenter.cfg
1884 my $vnckmap = $cmap->{kmap}->{$keymap}->{kvm} || 'en-us';
1885 write_config("keyboard: $vnckmap\n",
1886 "$tmpdir/datacenter.cfg");
1887
1888 # save admin email
1889 write_config("user:root\@pam:1:0:::${mailto}::\n", "$tmpdir/user.cfg");
1890
1891 # write storage.cfg
1892 my $storage_cfg_fn = "$tmpdir/storage.cfg";
1893 if ($use_zfs) {
1894 write_config($storage_cfg_zfs, $storage_cfg_fn);
1895 } elsif ($use_btrfs) {
1896 write_config($storage_cfg_btrfs, $storage_cfg_fn);
1897 } elsif ($datadev) {
1898 write_config($storage_cfg_lvmthin, $storage_cfg_fn);
1899 } else {
1900 write_config($storage_cfg_local, $storage_cfg_fn);
1901 }
1902
1903 run_command("chroot $targetdir /usr/bin/create_pmxcfs_db /tmp/pve /var/lib/pve-cluster/config.db");
1904
1905 syscmd("rm -rf $tmpdir");
1906 } elsif ($setup->{product} eq 'pbs') {
1907 my $base_cfg_path = "/etc/proxmox-backup";
1908 mkdir "$targetdir/$base_cfg_path";
1909
1910 chroot_chown($targetdir, $base_cfg_path, user => 'backup', recursive => 1);
1911 chroot_chmod($targetdir, $base_cfg_path, mode => '0700');
1912
1913 my $user_cfg_fn = "$base_cfg_path/user.cfg";
1914 write_config("user: root\@pam\n\temail ${mailto}\n", "$targetdir/$user_cfg_fn");
1915 chroot_chown($targetdir, $user_cfg_fn, user => 'root', group => 'backup');
1916 chroot_chmod($targetdir, $user_cfg_fn, mode => '0640');
1917 }
1918 };
1919
1920 my $err = $@;
1921
1922 update_progress(1, 0, 1, "");
1923
1924 print $err if $err;
1925
1926 if ($opt_testmode) {
1927 my $elapsed = Time::HiRes::tv_interval($starttime);
1928 print "Elapsed extract time: $elapsed\n";
1929
1930 syscmd("chroot $targetdir /usr/bin/dpkg-query -W --showformat='\${package}\n'> final.pkglist");
1931 }
1932
1933 syscmd("umount $targetdir/run");
1934 syscmd("umount $targetdir/mnt/hostrun");
1935 syscmd("umount $targetdir/tmp/pkg");
1936 syscmd("umount $targetdir/tmp");
1937 syscmd("umount $targetdir/proc");
1938 syscmd("umount $targetdir/sys/firmware/efi/efivars");
1939 syscmd("umount $targetdir/sys");
1940
1941 if ($use_zfs) {
1942 syscmd("zfs umount -a") == 0 ||
1943 die "unable to unmount zfs\n";
1944 } else {
1945 syscmd("umount -d $targetdir");
1946 }
1947
1948 if (!$err && $use_zfs) {
1949 syscmd("zfs set sync=standard $zfspoolname") == 0 ||
1950 die "unable to set zfs properties\n";
1951
1952 syscmd("zfs set mountpoint=/ $zfspoolname/ROOT/$zfsrootvolname") == 0 ||
1953 die "zfs set mountpoint failed\n";
1954
1955 syscmd("zpool set bootfs=$zfspoolname/ROOT/$zfsrootvolname $zfspoolname") == 0 ||
1956 die "zfs set bootfs failed\n";
1957 syscmd("zpool export $zfspoolname");
1958 }
1959
1960 if ($bootloader_err) {
1961 $err = $err ? "$err\n$bootloader_err" : $bootloader_err;
1962 }
1963
1964 die $err if $err;
1965 }
1966
1967 my $last_display_change = 0;
1968
1969 my $display_info_counter = 0;
1970
1971 my $display_info_items = [
1972 "extract1-license.htm",
1973 "extract2-rulesystem.htm",
1974 "extract3-spam.htm",
1975 "extract4-virus.htm",
1976 ];
1977
1978 sub display_info {
1979
1980 my $min_display_time = 15;
1981
1982 my $ctime = time();
1983
1984 return if ($ctime - $last_display_change) < $min_display_time;
1985
1986 my $page = $display_info_items->[$display_info_counter % scalar(@$display_info_items)];
1987
1988 $display_info_counter++;
1989
1990 display_html($page);
1991 }
1992
1993 sub display_html {
1994 my ($filename) = @_;
1995
1996 $filename = $steps[$step_number]->{html} if !$filename;
1997
1998 my $htmldir = "${proxmox_libdir}/html";
1999 my $path;
2000 if (-f "$htmldir/$setup->{product}/$filename") {
2001 $path = "$htmldir/$setup->{product}/$filename";
2002 } else {
2003 $path = "$htmldir/$filename";
2004 }
2005
2006 my $data = file_get_contents($path);
2007
2008 if ($filename eq 'license.htm') {
2009 my $license = eval { decode('utf8', file_get_contents("${proxmox_cddir}/EULA")) };
2010 if (my $err = $@) {
2011 die $err if !$opt_testmode;
2012 $license = "TESTMODE: Ignore non existent EULA...\n";
2013 }
2014 my $title = "END USER LICENSE AGREEMENT (EULA)";
2015 $data =~ s/__LICENSE__/$license/;
2016 $data =~ s/__LICENSE_TITLE__/$title/;
2017 } elsif ($filename eq 'success.htm') {
2018 my $addr = $ipversion == 6 ? "[${ipaddress}]" : "$ipaddress";
2019 $data =~ s/__IPADDR__/$addr/g;
2020 $data =~ s/__PORT__/$setup->{port}/g;
2021
2022 my $autoreboot_msg = $config_options->{autoreboot}
2023 ? "Automatic reboot scheduled in $autoreboot_seconds seconds."
2024 : '';
2025 $data =~ s/__AUTOREBOOT_MSG__/$autoreboot_msg/;
2026 }
2027 $data =~ s/__FULL_PRODUCT_NAME__/$setup->{fullname}/g;
2028
2029 # always set base-path to common path, all resources are accesible from there.
2030 $htmlview->load_html($data, "file://$htmldir/");
2031
2032 $last_display_change = time();
2033 }
2034
2035 sub prev_function {
2036
2037 my ($text, $fctn) = @_;
2038
2039 $fctn = $step_number if !$fctn;
2040 $text = "_Previous" if !$text;
2041 $prev_btn->set_label ($text);
2042
2043 $step_number--;
2044 $steps[$step_number]->{function}();
2045
2046 $prev_btn->grab_focus();
2047 }
2048
2049 sub set_next {
2050 my ($text, $fctn) = @_;
2051
2052 $next_fctn = $fctn;
2053 my $step = $steps[$step_number];
2054 $text //= $steps[$step_number]->{next_button} // '_Next';
2055 $next->set_label($text);
2056
2057 $next->grab_focus();
2058 }
2059
2060 sub create_main_window {
2061
2062 $window = Gtk3::Window->new();
2063 $window->set_default_size(1024, 768);
2064 $window->set_has_resize_grip(0);
2065 $window->fullscreen() if !$opt_testmode;
2066 $window->set_decorated(0) if !$opt_testmode;
2067
2068 my $vbox = Gtk3::VBox->new(0, 0);
2069
2070 my $logofn = "$setup->{product}-banner.png";
2071 my $image = Gtk3::Image->new_from_file("${proxmox_libdir}/$logofn");
2072 $vbox->pack_start($image, 0, 0, 0);
2073
2074 my $hbox = Gtk3::HBox->new(0, 0);
2075 $vbox->pack_start($hbox, 1, 1, 0);
2076
2077 # my $f1 = Gtk3::Frame->new ('test');
2078 # $f1->set_shadow_type ('none');
2079 # $hbox->pack_start ($f1, 1, 1, 0);
2080
2081 my $sep1 = Gtk3::HSeparator->new();
2082 $vbox->pack_start($sep1, 0, 0, 0);
2083
2084 $cmdbox = Gtk3::HBox->new();
2085 $vbox->pack_start($cmdbox, 0, 0, 10);
2086
2087 $next = Gtk3::Button->new('_Next');
2088 $next->signal_connect(clicked => sub { $last_display_change = 0; &$next_fctn (); });
2089 $cmdbox->pack_end($next, 0, 0, 10);
2090
2091
2092 $prev_btn = Gtk3::Button->new('_Previous');
2093 $prev_btn->signal_connect(clicked => sub { $last_display_change = 0; &prev_function (); });
2094 $cmdbox->pack_end($prev_btn, 0, 0, 10);
2095
2096
2097 my $abort = Gtk3::Button->new('_Abort');
2098 $abort->set_can_focus(0);
2099 $cmdbox->pack_start($abort, 0, 0, 10);
2100 $abort->signal_connect(clicked => sub { exit (-1); });
2101
2102 my $vbox2 = Gtk3::VBox->new(0, 0);
2103 $hbox->add($vbox2);
2104
2105 $htmlview = Gtk3::WebKit2::WebView->new();
2106 my $scrolls = Gtk3::ScrolledWindow->new();
2107 $scrolls->add($htmlview);
2108
2109 my $hbox2 = Gtk3::HBox->new(0, 0);
2110 $hbox2->pack_start($scrolls, 1, 1, 0);
2111
2112 $vbox2->pack_start($hbox2, 1, 1, 0);
2113
2114 my $vbox3 = Gtk3::VBox->new(0, 0);
2115 $vbox2->pack_start($vbox3, 0, 0, 0);
2116
2117 my $sep2 = Gtk3::HSeparator->new;
2118 $vbox3->pack_start($sep2, 0, 0, 0);
2119
2120 $inbox = Gtk3::HBox->new(0, 0);
2121 $vbox3->pack_start($inbox, 0, 0, 0);
2122
2123 $window->add($vbox);
2124
2125 $window->show_all;
2126 $window->realize();
2127 }
2128
2129 sub cleanup_view {
2130 $inbox->foreach(sub {
2131 my $child = shift;
2132 $inbox->remove ($child);
2133 });
2134 }
2135
2136 # fixme: newer GTK3 has special properties to handle numbers with Entry
2137 # only allow floating point numbers with Gtk3::Entry
2138
2139 sub check_float {
2140 my ($entry, $event) = @_;
2141
2142 return check_number($entry, $event, 1);
2143 }
2144
2145 sub check_int {
2146 my ($entry, $event) = @_;
2147
2148 return check_number($entry, $event, 0);
2149 }
2150
2151 sub check_number {
2152 my ($entry, $event, $float) = @_;
2153
2154 my $val = $event->get_keyval;
2155
2156 if (($float && $val == ord '.') ||
2157 $val == Gtk3::Gdk::KEY_ISO_Left_Tab ||
2158 $val == Gtk3::Gdk::KEY_Shift_L ||
2159 $val == Gtk3::Gdk::KEY_Tab ||
2160 $val == Gtk3::Gdk::KEY_Left ||
2161 $val == Gtk3::Gdk::KEY_Right ||
2162 $val == Gtk3::Gdk::KEY_BackSpace ||
2163 $val == Gtk3::Gdk::KEY_Delete ||
2164 ($val >= ord '0' && $val <= ord '9') ||
2165 ($val >= Gtk3::Gdk::KEY_KP_0 &&
2166 $val <= Gtk3::Gdk::KEY_KP_9)) {
2167 return undef;
2168 }
2169
2170 return 1;
2171 }
2172
2173 sub create_text_input {
2174 my ($default, $text) = @_;
2175
2176 my $hbox = Gtk3::Box->new('horizontal', 0);
2177
2178 my $label = Gtk3::Label->new($text);
2179 $label->set_size_request(150, -1);
2180 $label->set_alignment(1, 0.5);
2181 $hbox->pack_start($label, 0, 0, 10);
2182 my $e1 = Gtk3::Entry->new();
2183 $e1->set_width_chars(35);
2184 $hbox->pack_start($e1, 0, 0, 0);
2185 $e1->set_text($default);
2186
2187 return ($hbox, $e1);
2188 }
2189 sub create_cidr_inputs {
2190 my ($default_ip, $default_mask) = @_;
2191
2192 my $hbox = Gtk3::Box->new('horizontal', 0);
2193
2194 my $label = Gtk3::Label->new('IP Address (CIDR)');
2195 $label->set_size_request(150, -1);
2196 $label->set_alignment(1, 0.5);
2197 $hbox->pack_start($label, 0, 0, 10);
2198
2199 my $ip_el = Gtk3::Entry->new();
2200 $ip_el->set_width_chars(28);
2201 $hbox->pack_start($ip_el, 0, 0, 0);
2202 $ip_el->set_text($default_ip);
2203
2204 $label = Gtk3::Label->new('/');
2205 $label->set_size_request(10, -1);
2206 $label->set_alignment(0.5, 0.5);
2207 $hbox->pack_start($label, 0, 0, 2);
2208
2209 my $cidr_el = Gtk3::Entry->new();
2210 $cidr_el->set_width_chars(3);
2211 $hbox->pack_start($cidr_el, 0, 0, 0);
2212 $cidr_el->set_text($default_mask);
2213
2214 return ($hbox, $ip_el, $cidr_el);
2215 }
2216
2217 sub get_ip_config {
2218
2219 my $ifaces = {};
2220 my $default;
2221
2222 my $links = `ip -o l`;
2223 foreach my $l (split /\n/,$links) {
2224 my ($index, $name, $flags, $state, $mac) = $l =~ m/^(\d+):\s+(\S+):\s+<(\S+)>.*\s+state\s+(\S+)\s+.*\s+link\/ether\s+(\S+)\s+/;
2225 next if !$name || $name eq 'lo';
2226
2227 my $driver = readlink "/sys/class/net/$name/device/driver" || 'unknown';
2228 $driver =~ s!^.*/!!;
2229
2230 $ifaces->{"$index"} = {
2231 name => $name,
2232 driver => $driver,
2233 flags => $flags,
2234 state => $state,
2235 mac => $mac,
2236 };
2237
2238 my $addresses = `ip -o a s $name`;
2239 foreach my $a (split /\n/,$addresses) {
2240 my ($family, $ip, $prefix) = $a =~ m/^\Q$index\E:\s+\Q$name\E\s+(inet|inet6)\s+($IPRE)\/(\d+)\s+/;
2241 next if !$ip;
2242 next if $a =~ /scope\s+link/; # ignore link local
2243
2244 my $mask = $prefix;
2245
2246 if ($family eq 'inet') {
2247 next if !$ip =~ /$IPV4RE/;
2248 next if $prefix < 8 || $prefix > 32;
2249 $mask = @$ipv4_reverse_mask[$prefix];
2250 } else {
2251 next if !$ip =~ /$IPV6RE/;
2252 }
2253
2254 $default = $index if !$default;
2255
2256 $ifaces->{"$index"}->{"$family"} = {
2257 prefix => $prefix,
2258 mask => $mask,
2259 addr => $ip,
2260 };
2261 }
2262 }
2263
2264
2265 my $route = `ip route`;
2266 my ($gateway) = $route =~ m/^default\s+via\s+(\S+)\s+/m;
2267
2268 my $resolvconf = `cat /etc/resolv.conf`;
2269 my ($dnsserver) = $resolvconf =~ m/^nameserver\s+(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/m;
2270 my ($domain) = $resolvconf =~ m/^domain\s+(\S+)$/m;
2271
2272 return {
2273 default => $default,
2274 ifaces => $ifaces,
2275 gateway => $gateway,
2276 dnsserver => $dnsserver,
2277 domain => $domain,
2278 }
2279 }
2280
2281 sub display_message {
2282 my ($msg) = @_;
2283
2284 my $dialog = Gtk3::MessageDialog->new($window, 'modal', 'info', 'ok', $msg);
2285 $dialog->run();
2286 $dialog->destroy();
2287 }
2288
2289 sub display_error {
2290 my ($msg) = @_;
2291
2292 my $dialog = Gtk3::MessageDialog->new($window, 'modal', 'error', 'ok', $msg);
2293 $dialog->run();
2294 $dialog->destroy();
2295 }
2296
2297 my $ipconf_first_view = 1;
2298
2299 sub create_ipconf_view {
2300
2301 cleanup_view();
2302 display_html();
2303
2304 my $vcontainer = Gtk3::Box->new('vertical', 0);
2305 $inbox->pack_start($vcontainer, 1, 0, 0);
2306 my $hcontainer = Gtk3::Box->new('horizontal', 0);
2307 $vcontainer->pack_start($hcontainer, 0, 0, 10);
2308 my $vbox = Gtk3::Box->new('vertical', 0);
2309 $hcontainer->add($vbox);
2310
2311 my $ipaddr_text = $config->{ipaddress} // "192.168.100.2";
2312 my $netmask_text = $config->{netmask} // "24";
2313 my $cidr_box;
2314 ($cidr_box, $ipconf_entry_addr, $ipconf_entry_mask) =
2315 create_cidr_inputs($ipaddr_text, $netmask_text);
2316
2317 my $device_cb = Gtk3::ComboBoxText->new();
2318 $device_cb->set_active(0);
2319 $device_cb->set_visible(1);
2320
2321 my $get_device_desc = sub {
2322 my $iface = shift;
2323 return "$iface->{name} - $iface->{mac} ($iface->{driver})";
2324 };
2325
2326 my $device_active_map = {};
2327 my $device_active_reverse_map = {};
2328
2329 my $device_change_handler = sub {
2330 my $current = shift;
2331
2332 my $new = $device_active_map->{$current->get_active()};
2333 return if defined($ipconf->{selected}) && $new eq $ipconf->{selected};
2334
2335 $ipconf->{selected} = $new;
2336 my $iface = $ipconf->{ifaces}->{$ipconf->{selected}};
2337 $config->{mngmt_nic} = $iface->{name};
2338 $ipconf_entry_addr->set_text($iface->{inet}->{addr} || $iface->{inet6}->{addr})
2339 if $iface->{inet}->{addr} || $iface->{inet6}->{addr};
2340 $ipconf_entry_mask->set_text($iface->{inet}->{prefix} || $iface->{inet6}->{prefix})
2341 if $iface->{inet}->{prefix} || $iface->{inet6}->{prefix};
2342 };
2343
2344 my $i = 0;
2345 foreach my $index (sort keys %{$ipconf->{ifaces}}) {
2346 $device_cb->append_text(&$get_device_desc($ipconf->{ifaces}->{$index}));
2347 $device_active_map->{$i} = $index;
2348 $device_active_reverse_map->{$ipconf->{ifaces}->{$index}->{name}} = $i;
2349 if ($ipconf_first_view && $index == $ipconf->{default}) {
2350 $device_cb->set_active($i);
2351 &$device_change_handler($device_cb);
2352 $ipconf_first_view = 0;
2353 }
2354 $device_cb->signal_connect('changed' => $device_change_handler);
2355 $i++;
2356 }
2357
2358 if (my $nic = $config->{mngmt_nic}) {
2359 $device_cb->set_active($device_active_reverse_map->{$nic} // 0);
2360 } else {
2361 $device_cb->set_active(0);
2362 }
2363
2364 my $devicebox = Gtk3::HBox->new(0, 0);
2365 my $label = Gtk3::Label->new("Management Interface:");
2366 $label->set_size_request(150, -1);
2367 $label->set_alignment(1, 0.5);
2368 $devicebox->pack_start($label, 0, 0, 10);
2369 $devicebox->pack_start($device_cb, 0, 0, 0);
2370
2371 $vbox->pack_start($devicebox, 0, 0, 2);
2372
2373 my $hn = $config->{fqdn} // "$setup->{product}." . ($ipconf->{domain} // "example.invalid");
2374
2375 my ($hostbox, $hostentry) = create_text_input($hn, 'Hostname (FQDN):');
2376 $vbox->pack_start($hostbox, 0, 0, 2);
2377
2378 $vbox->pack_start($cidr_box, 0, 0, 2);
2379
2380 $gateway = $config->{gateway} // $ipconf->{gateway} || '192.168.100.1';
2381
2382 my $gwbox;
2383 ($gwbox, $ipconf_entry_gw) =
2384 create_text_input($gateway, 'Gateway:');
2385
2386 $vbox->pack_start($gwbox, 0, 0, 2);
2387
2388 $dnsserver = $config->{dnsserver} // $ipconf->{dnsserver} || $gateway;
2389
2390 my $dnsbox;
2391 ($dnsbox, $ipconf_entry_dns) =
2392 create_text_input($dnsserver, 'DNS Server:');
2393
2394 $vbox->pack_start($dnsbox, 0, 0, 0);
2395
2396 $inbox->show_all;
2397 set_next(undef, sub {
2398
2399 # verify hostname
2400
2401 my $text = $hostentry->get_text();
2402
2403 $text =~ s/^\s+//;
2404 $text =~ s/\s+$//;
2405
2406 $config->{fqdn} = $text;
2407
2408 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
2409
2410 # Debian does not support purely numeric hostnames
2411 if ($text && $text =~ /^[0-9]+(?:\.|$)/) {
2412 display_message("Purely numeric hostnames are not allowed.");
2413 $hostentry->grab_focus();
2414 return;
2415 }
2416
2417 if ($text && $text =~ m/^(${namere}\.)*${namere}$/ && $text !~ m/.example.invalid$/ &&
2418 $text =~ m/^([^\.]+)\.(\S+)$/) {
2419 $hostname = $1;
2420 $domain = $2;
2421 } else {
2422 display_message("Hostname does not look like a fully qualified domain name.");
2423 $hostentry->grab_focus();
2424 return;
2425 }
2426
2427 # verify ip address
2428
2429 $text = $ipconf_entry_addr->get_text();
2430 $text =~ s/^\s+//;
2431 $text =~ s/\s+$//;
2432 if ($text =~ m!^($IPV4RE)$!) {
2433 $ipaddress = $text;
2434 $ipversion = 4;
2435 } elsif ($text =~ m!^($IPV6RE)$!) {
2436 $ipaddress = $text;
2437 $ipversion = 6;
2438 } else {
2439 display_message("IP address is not valid.");
2440 $ipconf_entry_addr->grab_focus();
2441 return;
2442 }
2443 $config->{ipaddress} = $ipaddress;
2444
2445 $text = $ipconf_entry_mask->get_text();
2446 $text =~ s/^\s+//;
2447 $text =~ s/\s+$//;
2448 if ($ipversion == 6 && ($text =~ m/^(\d+)$/) && $1 >= 8 && $1 <= 126) {
2449 $netmask = $text;
2450 } elsif ($ipversion == 4 && ($text =~ m/^(\d+)$/) && $1 >= 8 && $1 <= 32) {
2451 $netmask = $text;
2452 } elsif ($ipversion == 4 && defined($ipv4_mask_hash->{$text})) {
2453 # costs nothing to handle 255.x.y.z style masks, so continue to allow it
2454 $netmask = $ipv4_mask_hash->{$text};
2455 } else {
2456 display_message("Netmask is not valid.");
2457 $ipconf_entry_mask->grab_focus();
2458 return;
2459 }
2460 $cidr = "$ipaddress/$netmask";
2461 $config->{netmask} = $netmask;
2462
2463 $text = $ipconf_entry_gw->get_text();
2464 $text =~ s/^\s+//;
2465 $text =~ s/\s+$//;
2466 if (($ipversion == 4) && ($text =~ m!^($IPV4RE)$!)) {
2467 $gateway = $text;
2468 } elsif (($ipversion == 6) && ($text =~ m!^($IPV6RE)$!)) {
2469 $gateway = $text;
2470 } else {
2471 display_message("Gateway is not valid.");
2472 $ipconf_entry_gw->grab_focus();
2473 return;
2474 }
2475 $config->{gateway} = $gateway;
2476
2477 $text = $ipconf_entry_dns->get_text();
2478 $text =~ s/^\s+//;
2479 $text =~ s/\s+$//;
2480 if (($ipversion == 4) && ($text =~ m!^($IPV4RE)$!)) {
2481 $dnsserver = $text;
2482 } elsif (($ipversion == 6) && ($text =~ m!^($IPV6RE)$!)) {
2483 $dnsserver = $text;
2484 } else {
2485 display_message("DNS server is not valid.");
2486 $ipconf_entry_dns->grab_focus();
2487 return;
2488 }
2489 $config->{dnsserver} = $dnsserver;
2490
2491 #print "TEST $ipaddress $netmask $gateway $dnsserver\n";
2492
2493 $step_number++;
2494 create_ack_view();
2495 });
2496
2497 $hostentry->grab_focus();
2498 }
2499
2500 sub create_ack_view {
2501
2502 cleanup_view();
2503
2504 my $vbox = Gtk3::VBox->new(0, 0);
2505 $inbox->pack_start($vbox, 1, 0, 0);
2506
2507 my $reboot_checkbox = Gtk3::CheckButton->new('Automatically reboot after successful installation');
2508 $reboot_checkbox->set_active(1);
2509 $reboot_checkbox->signal_connect ("toggled" => sub {
2510 my $cb = shift;
2511 $config_options->{autoreboot} = $cb->get_active();
2512 });
2513 $vbox->pack_start($reboot_checkbox, 0, 0, 2);
2514
2515 my $ack_template = "${proxmox_libdir}/html/ack_template.htm";
2516 my $ack_html = "${proxmox_libdir}/html/$setup->{product}/$steps[$step_number]->{html}";
2517 my $html_data = file_get_contents($ack_template);
2518
2519 my %config_values = (
2520 __target_hd__ => join(' | ', @{$config_options->{target_hds}}),
2521 __target_fs__ => $config_options->{filesys},
2522 __country__ => $cmap->{country}->{$country}->{name},
2523 __timezone__ => $timezone,
2524 __keymap__ => $keymap,
2525 __mailto__ => $mailto,
2526 __interface__ => $ipconf->{ifaces}->{$ipconf->{selected}}->{name},
2527 __hostname__ => $hostname,
2528 __ip__ => $ipaddress,
2529 __cidr__ => $cidr,
2530 __netmask__ => $netmask,
2531 __gateway__ => $gateway,
2532 __dnsserver__ => $dnsserver,
2533 );
2534
2535 while (my ($k, $v) = each %config_values) {
2536 $html_data =~ s/$k/$v/g;
2537 }
2538
2539 write_config($html_data, $ack_html);
2540
2541 display_html();
2542
2543 $inbox->show_all;
2544
2545 set_next(undef, sub {
2546 $step_number++;
2547 create_extract_view();
2548 });
2549 }
2550
2551 sub get_device_desc {
2552 my ($devname, $size, $model) = @_;
2553
2554 if ($size && ($size > 0)) {
2555 $size = int($size/2048); # size in MiB, from 512B "sectors"
2556
2557 my $text = "$devname (";
2558 if ($size >= 1024) {
2559 $size = $size/1024; # size in GiB
2560 if ($size >= 1024) {
2561 $size = $size/1024; # size in TiB
2562 $text .= sprintf("%.2f", $size) . "TiB";
2563 } else {
2564 $text .= sprintf("%.2f", $size) . "GiB";
2565 }
2566 } else {
2567 $text .= "${size}MiB";
2568 }
2569
2570 $text .= ", $model" if $model;
2571 $text .= ")";
2572 return $text;
2573
2574 } else {
2575 return $devname;
2576 }
2577 }
2578
2579 my $last_layout;
2580 my $country_layout;
2581 sub update_layout {
2582 my ($cb, $kmap) = @_;
2583
2584 my $ind;
2585 my $def;
2586 my $i = 0;
2587 my $kmaphash = $cmap->{kmaphash};
2588 foreach my $layout (sort keys %$kmaphash) {
2589 $def = $i if $kmaphash->{$layout} eq 'en-us';
2590 $ind = $i if $kmap && $kmaphash->{$layout} eq $kmap;
2591 $i++;
2592 }
2593
2594 my $val = $ind || $def || 0;
2595
2596 if (!defined($kmap)) {
2597 $last_layout //= $val;
2598 } elsif (!defined($country_layout) || $country_layout != $val) {
2599 $last_layout = $country_layout = $val;
2600 }
2601 $cb->set_active($last_layout);
2602 }
2603
2604 my $lastzonecb;
2605 sub update_zonelist {
2606 my ($box, $cc) = @_;
2607
2608 my $cczones = $cmap->{cczones};
2609 my $zones = $cmap->{zones};
2610
2611 my $sel;
2612 if ($lastzonecb) {
2613 $sel = $lastzonecb->get_active_text();
2614 $box->remove ($lastzonecb);
2615 } else {
2616 $sel = $timezone; # used once to select default
2617 }
2618
2619 my $cb = $lastzonecb = Gtk3::ComboBoxText->new();
2620 $cb->set_size_request(200, -1);
2621
2622 $cb->signal_connect('changed' => sub {
2623 $timezone = $cb->get_active_text();
2624 });
2625
2626 my @za;
2627 if ($cc && defined ($cczones->{$cc})) {
2628 @za = keys %{$cczones->{$cc}};
2629 } else {
2630 @za = keys %$zones;
2631 }
2632 my $ind;
2633 my $i = 0;
2634 foreach my $zone (sort @za) {
2635 $ind = $i if $sel && $zone eq $sel;
2636 $cb->append_text($zone);
2637 $i++;
2638 }
2639
2640 $cb->set_active($ind || 0);
2641
2642 $cb->show;
2643 $box->pack_start($cb, 0, 0, 0);
2644 }
2645
2646 sub create_password_view {
2647
2648 cleanup_view();
2649
2650 my $vbox2 = Gtk3::VBox->new(0, 0);
2651 $inbox->pack_start($vbox2, 1, 0, 0);
2652 my $vbox = Gtk3::VBox->new(0, 0);
2653 $vbox2->pack_start($vbox, 0, 0, 10);
2654
2655 my $hbox1 = Gtk3::HBox->new(0, 0);
2656 my $label = Gtk3::Label->new("Password");
2657 $label->set_size_request(150, -1);
2658 $label->set_alignment(1, 0.5);
2659 $hbox1->pack_start($label, 0, 0, 10);
2660 my $pwe1 = Gtk3::Entry->new();
2661 $pwe1->set_visibility(0);
2662 $pwe1->set_text($password) if $password;
2663 $pwe1->set_size_request(200, -1);
2664 $hbox1->pack_start($pwe1, 0, 0, 0);
2665
2666 my $hbox2 = Gtk3::HBox->new(0, 0);
2667 $label = Gtk3::Label->new("Confirm");
2668 $label->set_size_request(150, -1);
2669 $label->set_alignment(1, 0.5);
2670 $hbox2->pack_start($label, 0, 0, 10);
2671 my $pwe2 = Gtk3::Entry->new();
2672 $pwe2->set_visibility(0);
2673 $pwe2->set_text($password) if $password;
2674 $pwe2->set_size_request(200, -1);
2675 $hbox2->pack_start($pwe2, 0, 0, 0);
2676
2677 my $hbox3 = Gtk3::HBox->new(0, 0);
2678 $label = Gtk3::Label->new("Email");
2679 $label->set_size_request(150, -1);
2680 $label->set_alignment(1, 0.5);
2681 $hbox3->pack_start($label, 0, 0, 10);
2682 my $eme = Gtk3::Entry->new();
2683 $eme->set_size_request(200, -1);
2684 $eme->set_text($mailto);
2685 $hbox3->pack_start($eme, 0, 0, 0);
2686
2687
2688 $vbox->pack_start($hbox1, 0, 0, 5);
2689 $vbox->pack_start($hbox2, 0, 0, 5);
2690 $vbox->pack_start($hbox3, 0, 0, 15);
2691
2692 $inbox->show_all;
2693
2694 display_html();
2695
2696 set_next (undef, sub {
2697
2698 my $t1 = $pwe1->get_text;
2699 my $t2 = $pwe2->get_text;
2700
2701 if (length ($t1) < 5) {
2702 display_message("Password is too short.");
2703 $pwe1->grab_focus();
2704 return;
2705 }
2706
2707 if ($t1 ne $t2) {
2708 display_message("Password does not match.");
2709 $pwe1->grab_focus();
2710 return;
2711 }
2712
2713 my $t3 = $eme->get_text;
2714 if ($t3 !~ m/^[\w\+\-\~]+(\.[\w\+\-\~]+)*@[a-zA-Z0-9\-]+(\.[a-zA-Z0-9\-]+)*$/) {
2715 display_message("Email does not look like a valid address" .
2716 " (user\@domain.tld)");
2717 $eme->grab_focus();
2718 return;
2719 }
2720
2721 if ($t3 eq 'mail@example.invalid') {
2722 display_message("Please enter a valid Email address");
2723 $eme->grab_focus();
2724 return;
2725 }
2726
2727 $password = $t1;
2728 $mailto = $t3;
2729
2730 $step_number++;
2731 create_ipconf_view();
2732 });
2733
2734 $pwe1->grab_focus();
2735
2736 }
2737
2738 my $installer_kmap;
2739 sub create_country_view {
2740
2741 cleanup_view();
2742
2743 my $countryhash = $cmap->{countryhash};
2744 my $ctr = $cmap->{country};
2745
2746 my $vbox2 = Gtk3::VBox->new(0, 0);
2747 $inbox->pack_start($vbox2, 1, 0, 0);
2748 my $vbox = Gtk3::VBox->new(0, 0);
2749 $vbox2->pack_start($vbox, 0, 0, 10);
2750
2751 my $w = Gtk3::Entry->new();
2752 $w->set_size_request(200, -1);
2753
2754 my $c = Gtk3::EntryCompletion->new();
2755 $c->set_text_column(0);
2756 $c->set_minimum_key_length(0);
2757 $c->set_popup_set_width(1);
2758 $c->set_inline_completion(1);
2759
2760 my $hbox2 = Gtk3::HBox->new(0, 0);
2761 my $label = Gtk3::Label->new("Time zone");
2762 $label->set_size_request(150, -1);
2763 $label->set_alignment(1, 0.5);
2764 $hbox2->pack_start($label, 0, 0, 10);
2765 update_zonelist ($hbox2);
2766
2767 my $hbox3 = Gtk3::HBox->new(0, 0);
2768 $label = Gtk3::Label->new("Keyboard Layout");
2769 $label->set_size_request(150, -1);
2770 $label->set_alignment(1, 0.5);
2771 $hbox3->pack_start($label, 0, 0, 10);
2772
2773 my $kmapcb = Gtk3::ComboBoxText->new();
2774 $kmapcb->set_size_request (200, -1);
2775 foreach my $layout (sort keys %{$cmap->{kmaphash}}) {
2776 $kmapcb->append_text ($layout);
2777 }
2778
2779 update_layout($kmapcb);
2780 $hbox3->pack_start ($kmapcb, 0, 0, 0);
2781
2782 $kmapcb->signal_connect ('changed' => sub {
2783 my $sel = $kmapcb->get_active_text();
2784 $last_layout = $kmapcb->get_active();
2785 if (my $kmap = $cmap->{kmaphash}->{$sel}) {
2786 my $xkmap = $cmap->{kmap}->{$kmap}->{x11};
2787 my $xvar = $cmap->{kmap}->{$kmap}->{x11var};
2788 $keymap = $kmap;
2789
2790 return if (defined($installer_kmap) && $installer_kmap eq $kmap);
2791
2792 $installer_kmap = $keymap;
2793
2794 if (! $opt_testmode) {
2795 syscmd ("setxkbmap $xkmap $xvar");
2796
2797 my $kbd_config = qq{
2798 XKBLAYOUT="$xkmap"
2799 XKBVARIANT="$xvar"
2800 BACKSPACE="guess"
2801 };
2802 $kbd_config =~ s/^\s+//gm;
2803
2804 run_in_background( sub {
2805 write_config($kbd_config, '/etc/default/keyboard');
2806 system("setupcon");
2807 });
2808 }
2809 }
2810 });
2811
2812 $w->signal_connect ('changed' => sub {
2813 my ($entry, $event) = @_;
2814 my $text = $entry->get_text;
2815
2816 if (my $cc = $countryhash->{lc($text)}) {
2817 update_zonelist($hbox2, $cc);
2818 my $kmap = $ctr->{$cc}->{kmap} || 'en-us';
2819 update_layout($kmapcb, $kmap);
2820 }
2821 });
2822
2823 $w->signal_connect (key_press_event => sub {
2824 my ($entry, $event) = @_;
2825 my $text = $entry->get_text;
2826
2827 my $val = $event->get_keyval;
2828
2829 if ($val == Gtk3::Gdk::KEY_Tab) {
2830 my $cc = $countryhash->{lc($text)};
2831
2832 my $found = 0;
2833 my $compl;
2834
2835 if ($cc) {
2836 $found = 1;
2837 $compl = $ctr->{$cc}->{name};
2838 } else {
2839 foreach my $cc (keys %$ctr) {
2840 my $ct = $ctr->{$cc}->{name};
2841 if ($ct =~ m/^\Q$text\E.*$/i) {
2842 $found++;
2843 $compl = $ct;
2844 }
2845 last if $found > 1;
2846 }
2847 }
2848
2849 if ($found == 1) {
2850 $entry->set_text($compl);
2851 $c->complete();
2852 return undef;
2853 } else {
2854 #Gtk3::Gdk::beep();
2855 print chr(7); # beep ?
2856 }
2857
2858 $c->complete();
2859
2860 my $buf = $w->get_buffer();
2861 $buf->insert_text(-1, '', -1); # popup selection
2862
2863 return 1;
2864 }
2865
2866 return undef;
2867 });
2868
2869 my $ls = Gtk3::ListStore->new('Glib::String');
2870 foreach my $cc (sort {$ctr->{$a}->{name} cmp $ctr->{$b}->{name} } keys %$ctr) {
2871 my $iter = $ls->append();
2872 $ls->set ($iter, 0, $ctr->{$cc}->{name});
2873 }
2874 $c->set_model ($ls);
2875
2876 $w->set_completion ($c);
2877
2878 my $hbox = Gtk3::HBox->new(0, 0);
2879
2880 $label = Gtk3::Label->new("Country");
2881 $label->set_alignment(1, 0.5);
2882 $label->set_size_request(150, -1);
2883 $hbox->pack_start($label, 0, 0, 10);
2884 $hbox->pack_start($w, 0, 0, 0);
2885
2886 $vbox->pack_start($hbox, 0, 0, 5);
2887 $vbox->pack_start($hbox2, 0, 0, 5);
2888 $vbox->pack_start($hbox3, 0, 0, 5);
2889
2890 if ($country && $ctr->{$country}) {
2891 $w->set_text ($ctr->{$country}->{name});
2892 }
2893
2894 $inbox->show_all;
2895
2896 display_html();
2897 set_next (undef, sub {
2898
2899 my $text = $w->get_text;
2900
2901 if (my $cc = $countryhash->{lc($text)}) {
2902 $country = $cc;
2903 $step_number++;
2904 create_password_view();
2905 return;
2906 } else {
2907 display_message("Please select a country first.");
2908 $w->grab_focus();
2909 }
2910 });
2911
2912 $w->grab_focus();
2913 }
2914
2915 my $target_hd_combo;
2916 my $target_hd_label;
2917
2918 my $hdoption_first_setup = 1;
2919
2920 my $create_basic_grid = sub {
2921 my $grid = Gtk3::Grid->new();
2922 $grid->set_visible(1);
2923 $grid->set_column_spacing(10);
2924 $grid->set_row_spacing(10);
2925 $grid->set_hexpand(1);
2926
2927 $grid->set_margin_start(10);
2928 $grid->set_margin_end(20);
2929 $grid->set_margin_top(5);
2930 $grid->set_margin_bottom(5);
2931
2932 return $grid;
2933 };
2934
2935 my $create_label_widget_grid = sub {
2936 my ($labeled_widgets) = @_;
2937
2938 my $grid = &$create_basic_grid();
2939 my $row = 0;
2940
2941 for (my $i = 0; $i < @$labeled_widgets; $i += 2) {
2942 my $widget = @$labeled_widgets[$i+1];
2943 my $label = Gtk3::Label->new(@$labeled_widgets[$i]);
2944 $label->set_visible(1);
2945 $label->set_alignment (1, 0.5);
2946 $grid->attach($label, 0, $row, 1, 1);
2947 $widget->set_visible(1);
2948 $grid->attach($widget, 1, $row, 1, 1);
2949 $row++;
2950 }
2951
2952 return $grid;
2953 };
2954
2955 my $create_raid_disk_grid = sub {
2956 my $hd_count = scalar(@$hds);
2957 my $disk_labeled_widgets = [];
2958 for (my $i = 0; $i < $hd_count; $i++) {
2959 my $disk_selector = Gtk3::ComboBoxText->new();
2960 $disk_selector->append_text("-- do not use --");
2961 $disk_selector->set_active(0);
2962 $disk_selector->set_visible(1);
2963 foreach my $hd (@$hds) {
2964 my ($disk, $devname, $size, $model, $logical_bsize) = @$hd;
2965 $disk_selector->append_text(get_device_desc ($devname, $size, $model));
2966 $disk_selector->{pve_disk_id} = $i;
2967 $disk_selector->signal_connect (changed => sub {
2968 my $w = shift;
2969 my $diskid = $w->{pve_disk_id};
2970 my $a = $w->get_active - 1;
2971 $config_options->{"disksel${diskid}"} = ($a >= 0) ? $hds->[$a] : undef;
2972 });
2973 }
2974
2975 if ($hdoption_first_setup) {
2976 $disk_selector->set_active ($i+1) if $hds->[$i];
2977 } else {
2978 my $hdind = 0;
2979 if (my $cur_hd = $config_options->{"disksel$i"}) {
2980 foreach my $hd (@$hds) {
2981 if (@$hd[1] eq @$cur_hd[1]) {
2982 $disk_selector->set_active($hdind+1);
2983 last;
2984 }
2985 $hdind++;
2986 }
2987 }
2988 }
2989
2990 push @$disk_labeled_widgets, "Harddisk $i", $disk_selector;
2991 }
2992
2993 my $clear_all_button = Gtk3::Button->new('_Deselect All');
2994 if ($hd_count > 3) {
2995 $clear_all_button->signal_connect('clicked', sub {
2996 my $is_widget = 0;
2997 for my $disk_selector (@$disk_labeled_widgets) {
2998 $disk_selector->set_active(0) if $is_widget;
2999 $is_widget ^= 1;
3000 }
3001 });
3002 $clear_all_button->set_visible(1);
3003 }
3004
3005 my $scrolled_window = Gtk3::ScrolledWindow->new();
3006 $scrolled_window->set_hexpand(1);
3007 $scrolled_window->set_propagate_natural_height(1) if $hd_count > 4;
3008
3009 my $diskgrid = $create_label_widget_grid->($disk_labeled_widgets);
3010
3011 $scrolled_window->add($diskgrid);
3012 $scrolled_window->set_policy('never', 'automatic');
3013 $scrolled_window->set_visible(1);
3014 $scrolled_window->set_min_content_height(190);
3015
3016 my $vbox = Gtk3::Box->new('vertical', 0);
3017 $vbox->pack_start($scrolled_window, 1, 1, 10);
3018
3019 my $hbox = Gtk3::Box->new('horizontal', 0);
3020 $hbox->pack_end($clear_all_button, 0, 0, 20);
3021 $hbox->set_visible(1);
3022 $vbox->pack_end($hbox, 0, 0, 0);
3023
3024 return $vbox;
3025 };
3026
3027 # shared between different ui parts (e.g., ZFS and "normal" single disk FS)
3028 my $hdsize_size_adj;
3029 my $hdsize_entry_buffer;
3030
3031 my $get_hdsize_spinbtn = sub {
3032 my $hdsize = shift;
3033
3034 $hdsize_entry_buffer //= Gtk3::EntryBuffer->new(undef, 1);
3035
3036 if (defined($hdsize)) {
3037 $hdsize_size_adj = Gtk3::Adjustment->new($config_options->{hdsize} || $hdsize, 0, $hdsize+1, 1, 1, 1);
3038 } else {
3039 die "called get_hdsize_spinbtn with \$hdsize_size_adj not defined but did not pass hdsize!\n"
3040 if !defined($hdsize_size_adj);
3041 }
3042
3043 my $spinbutton_hdsize = Gtk3::SpinButton->new($hdsize_size_adj, 1, 1);
3044 $spinbutton_hdsize->set_buffer($hdsize_entry_buffer);
3045 $spinbutton_hdsize->set_adjustment($hdsize_size_adj);
3046 $spinbutton_hdsize->set_tooltip_text("only use specified size (GB) of the harddisk (rest left unpartitioned)");
3047 return $spinbutton_hdsize;
3048 };
3049
3050 my $create_raid_advanced_grid = sub {
3051 my $labeled_widgets = [];
3052 my $spinbutton_ashift = Gtk3::SpinButton->new_with_range(9, 13, 1);
3053 $spinbutton_ashift->set_tooltip_text("zpool ashift property (pool sector size, default 2^12)");
3054 $spinbutton_ashift->signal_connect ("value-changed" => sub {
3055 my $w = shift;
3056 $config_options->{ashift} = $w->get_value_as_int();
3057 });
3058 $config_options->{ashift} = 12 if ! defined($config_options->{ashift});
3059 $spinbutton_ashift->set_value($config_options->{ashift});
3060 push @$labeled_widgets, "ashift";
3061 push @$labeled_widgets, $spinbutton_ashift;
3062
3063 my $combo_compress = Gtk3::ComboBoxText->new();
3064 $combo_compress->set_tooltip_text("zfs compression algorithm for rpool dataset");
3065 my $comp_opts = ["on","off","lzjb","lz4", "lze", "gzip", "zstd"];
3066 foreach my $opt (@$comp_opts) {
3067 $combo_compress->append($opt, $opt);
3068 }
3069 $config_options->{compress} = "on" if !defined($config_options->{compress});
3070 $combo_compress->set_active_id($config_options->{compress});
3071 $combo_compress->signal_connect (changed => sub {
3072 my $w = shift;
3073 $config_options->{compress} = $w->get_active_text();
3074 });
3075 push @$labeled_widgets, "compress";
3076 push @$labeled_widgets, $combo_compress;
3077
3078 my $combo_checksum = Gtk3::ComboBoxText->new();
3079 $combo_checksum->set_tooltip_text("zfs checksum algorithm for rpool dataset");
3080 my $csum_opts = ["on", "off","fletcher2", "fletcher4", "sha256"];
3081 foreach my $opt (@$csum_opts) {
3082 $combo_checksum->append($opt, $opt);
3083 }
3084 $config_options->{checksum} = "on" if !($config_options->{checksum});
3085 $combo_checksum->set_active_id($config_options->{checksum});
3086 $combo_checksum->signal_connect (changed => sub {
3087 my $w = shift;
3088 $config_options->{checksum} = $w->get_active_text();
3089 });
3090 push @$labeled_widgets, "checksum";
3091 push @$labeled_widgets, $combo_checksum;
3092
3093 my $spinbutton_copies = Gtk3::SpinButton->new_with_range(1,3,1);
3094 $spinbutton_copies->set_tooltip_text("zfs copies property for rpool dataset (in addition to RAID redundancy!)");
3095 $spinbutton_copies->signal_connect ("value-changed" => sub {
3096 my $w = shift;
3097 $config_options->{copies} = $w->get_value_as_int();
3098 });
3099 $config_options->{copies} = 1 if !defined($config_options->{copies});
3100 $spinbutton_copies->set_value($config_options->{copies});
3101 push @$labeled_widgets, "copies", $spinbutton_copies;
3102
3103 push @$labeled_widgets, "hdsize", $get_hdsize_spinbtn->();
3104 return $create_label_widget_grid->($labeled_widgets);;
3105 };
3106
3107 sub create_hdoption_view {
3108
3109 my $dialog = Gtk3::Dialog->new();
3110
3111 $dialog->set_title("Harddisk options");
3112
3113 $dialog->add_button("_OK", 1);
3114
3115 my $contarea = $dialog->get_content_area();
3116
3117 my $hbox2 = Gtk3::Box->new('horizontal', 0);
3118 $contarea->pack_start($hbox2, 1, 1, 5);
3119
3120 my $grid = Gtk3::Grid->new();
3121 $grid->set_column_spacing(10);
3122 $grid->set_row_spacing(10);
3123
3124 $hbox2->pack_start($grid, 1, 0, 5);
3125
3126 my $row = 0;
3127
3128 # Filesystem type
3129 my $label0 = Gtk3::Label->new("Filesystem");
3130 $label0->set_alignment (1, 0.5);
3131 $grid->attach($label0, 0, $row, 1, 1);
3132
3133 my $fstypecb = Gtk3::ComboBoxText->new();
3134 my $fstype = [
3135 'ext4',
3136 'xfs',
3137 'zfs (RAID0)',
3138 'zfs (RAID1)',
3139 'zfs (RAID10)',
3140 'zfs (RAIDZ-1)',
3141 'zfs (RAIDZ-2)',
3142 'zfs (RAIDZ-3)',
3143 ];
3144 push @$fstype, 'btrfs (RAID0)', 'btrfs (RAID1)', 'btrfs (RAID10)'
3145 if $setup->{enable_btrfs};
3146
3147 my $tcount = 0;
3148 foreach my $tmp (@$fstype) {
3149 $fstypecb->append_text($tmp);
3150 $fstypecb->set_active ($tcount) if $config_options->{filesys} eq $tmp;
3151 $tcount++;
3152 }
3153
3154 $grid->attach($fstypecb, 1, $row, 1, 1);
3155
3156 $hbox2->show_all();
3157
3158 $row++;
3159
3160 my $sep = Gtk3::HSeparator->new();
3161 $sep->set_visible(1);
3162 $grid->attach($sep, 0, $row, 2, 1);
3163 $row++;
3164
3165 my $hw_raid_note = Gtk3::Label->new(""); # text will be set below, before making it visible
3166 $hw_raid_note->set_line_wrap(1);
3167 $hw_raid_note->set_max_width_chars(30);
3168 $hw_raid_note->set_visible(0);
3169 $grid->attach($hw_raid_note, 0, $row++, 2, 1);
3170
3171 my $hdsize_labeled_widgets = [];
3172
3173 # size compute
3174 my $hdsize = 0;
3175 if ( -b $target_hd) {
3176 $hdsize = int(hd_size ($target_hd) / (1024*1024.0)); # size in GB
3177 } elsif ($target_hd) {
3178 $hdsize = int((-s $target_hd) / (1024*1024*1024.0));
3179 }
3180
3181 my $spinbutton_hdsize = $get_hdsize_spinbtn->($hdsize);
3182 push @$hdsize_labeled_widgets, "hdsize", $spinbutton_hdsize;
3183
3184 my $entry_swapsize = Gtk3::Entry->new();
3185 $entry_swapsize->set_tooltip_text("maximum SWAP size (GB)");
3186 $entry_swapsize->signal_connect (key_press_event => \&check_float);
3187 $entry_swapsize->set_text($config_options->{swapsize}) if defined($config_options->{swapsize});
3188 push @$hdsize_labeled_widgets, "swapsize", $entry_swapsize;
3189
3190 my $entry_maxroot = Gtk3::Entry->new();
3191 if ($setup->{product} eq 'pve') {
3192 $entry_maxroot->set_tooltip_text("maximum size (GB) for LVM root volume");
3193 $entry_maxroot->signal_connect (key_press_event => \&check_float);
3194 $entry_maxroot->set_text($config_options->{maxroot}) if $config_options->{maxroot};
3195 push @$hdsize_labeled_widgets, "maxroot", $entry_maxroot;
3196 }
3197
3198 my $entry_minfree = Gtk3::Entry->new();
3199 $entry_minfree->set_tooltip_text("minimum free LVM space (GB, required for LVM snapshots)");
3200 $entry_minfree->signal_connect (key_press_event => \&check_float);
3201 $entry_minfree->set_text($config_options->{minfree}) if defined($config_options->{minfree});
3202 push @$hdsize_labeled_widgets, "minfree", $entry_minfree;
3203
3204 my $entry_maxvz;
3205 if ($setup->{product} eq 'pve') {
3206 $entry_maxvz = Gtk3::Entry->new();
3207 $entry_maxvz->set_tooltip_text("maximum size (GB) for LVM data volume");
3208 $entry_maxvz->signal_connect (key_press_event => \&check_float);
3209 $entry_maxvz->set_text($config_options->{maxvz}) if defined($config_options->{maxvz});
3210 push @$hdsize_labeled_widgets, "maxvz", $entry_maxvz;
3211 }
3212
3213 my $options_stack = Gtk3::Stack->new();
3214 $options_stack->set_visible(1);
3215 $options_stack->set_hexpand(1);
3216 $options_stack->set_vexpand(1);
3217 $options_stack->add_titled(&$create_raid_disk_grid(), "raiddisk", "Disk Setup");
3218 $options_stack->add_titled(&$create_label_widget_grid($hdsize_labeled_widgets), "hdsize", "Size Options");
3219 $options_stack->add_titled(&$create_raid_advanced_grid("zfs"), "raidzfsadvanced", "Advanced Options");
3220 $options_stack->set_visible_child_name("raiddisk");
3221 my $options_stack_switcher = Gtk3::StackSwitcher->new();
3222 $options_stack_switcher->set_halign('center');
3223 $options_stack_switcher->set_stack($options_stack);
3224 $grid->attach($options_stack_switcher, 0, $row, 2, 1);
3225 $row++;
3226 $grid->attach($options_stack, 0, $row, 2, 1);
3227 $row++;
3228
3229 $hdoption_first_setup = 0;
3230
3231 my $switch_view = sub {
3232 my $raid = $config_options->{filesys} =~ m/zfs|btrfs/;
3233 my $is_zfs = $config_options->{filesys} =~ m/zfs/;
3234
3235 $target_hd_combo->set_visible(!$raid);
3236 $options_stack->get_child_by_name("hdsize")->set_visible(!$raid);
3237 $options_stack->get_child_by_name("raiddisk")->set_visible($raid);
3238
3239 if ($raid) {
3240 my $msg = "<b>Note</b>: " . ($is_zfs
3241 ? "ZFS is not compatible with hardware RAID controllers, for details see the documentation."
3242 : "BTRFS integration in $setup->{fullname} is a technology preview!"
3243 );
3244 $hw_raid_note->set_markup($msg);
3245 }
3246 $hw_raid_note->set_visible($raid);
3247 $options_stack_switcher->set_visible($is_zfs);
3248 $options_stack->get_child_by_name("raidzfsadvanced")->set_visible($is_zfs);
3249 if ($raid) {
3250 $target_hd_label->set_text("Target: $config_options->{filesys} ");
3251 $options_stack->set_visible_child_name("raiddisk");
3252 } else {
3253 $target_hd_label->set_text("Target Harddisk: ");
3254 }
3255 my (undef, $pref_width) = $dialog->get_preferred_width();
3256 my (undef, $pref_height) = $dialog->get_preferred_height();
3257 $pref_height = 750 if $pref_height > 750;
3258 $dialog->resize($pref_width, $pref_height);
3259 };
3260
3261 &$switch_view();
3262
3263 $fstypecb->signal_connect (changed => sub {
3264 $config_options->{filesys} = $fstypecb->get_active_text();
3265 &$switch_view();
3266 });
3267
3268 my $sep2 = Gtk3::HSeparator->new();
3269 $sep2->set_visible(1);
3270 $contarea->pack_end($sep2, 1, 1, 10);
3271
3272 $dialog->show();
3273
3274 $dialog->run();
3275
3276 my $get_float = sub {
3277 my ($entry) = @_;
3278
3279 my $text = $entry->get_text();
3280 return undef if !defined($text);
3281
3282 $text =~ s/^\s+//;
3283 $text =~ s/\s+$//;
3284
3285 return undef if $text !~ m/^\d+(\.\d+)?$/;
3286
3287 return $text;
3288 };
3289
3290 my $tmp;
3291
3292 if (($tmp = &$get_float($spinbutton_hdsize)) && ($tmp != $hdsize)) {
3293 $config_options->{hdsize} = $tmp;
3294 } else {
3295 delete $config_options->{hdsize};
3296 }
3297
3298 if (defined($tmp = &$get_float($entry_swapsize))) {
3299 $config_options->{swapsize} = $tmp;
3300 } else {
3301 delete $config_options->{swapsize};
3302 }
3303
3304 if (defined($tmp = &$get_float($entry_maxroot))) {
3305 $config_options->{maxroot} = $tmp;
3306 } else {
3307 delete $config_options->{maxroot};
3308 }
3309
3310 if (defined($tmp = &$get_float($entry_minfree))) {
3311 $config_options->{minfree} = $tmp;
3312 } else {
3313 delete $config_options->{minfree};
3314 }
3315
3316 if ($entry_maxvz && defined($tmp = &$get_float($entry_maxvz))) {
3317 $config_options->{maxvz} = $tmp;
3318 } else {
3319 delete $config_options->{maxvz};
3320 }
3321
3322 $dialog->destroy();
3323 }
3324
3325 my $get_raid_devlist = sub {
3326
3327 my $dev_name_hash = {};
3328
3329 my $devlist = [];
3330 for (my $i = 0; $i < @$hds; $i++) {
3331 if (my $hd = $config_options->{"disksel$i"}) {
3332 my ($disk, $devname, $size, $model, $logical_bsize) = @$hd;
3333 die "device '$devname' is used more than once\n"
3334 if $dev_name_hash->{$devname};
3335 $dev_name_hash->{$devname} = $hd;
3336 push @$devlist, $hd;
3337 }
3338 }
3339
3340 return $devlist;
3341 };
3342
3343 sub zfs_mirror_size_check {
3344 my ($expected, $actual) = @_;
3345
3346 die "mirrored disks must have same size\n"
3347 if abs($expected - $actual) > $expected / 10;
3348 }
3349
3350 sub legacy_bios_4k_check {
3351 my ($lbs) = @_;
3352 die "Booting from 4kn drive in legacy BIOS mode is not supported.\n"
3353 if (($boot_type ne 'efi') && ($lbs == 4096));
3354 }
3355
3356 sub get_zfs_raid_setup {
3357 my $filesys = $config_options->{filesys};
3358
3359 my $devlist = &$get_raid_devlist();
3360
3361 my $diskcount = scalar(@$devlist);
3362 die "$filesys needs at least one device\n" if $diskcount < 1;
3363
3364 my $cmd= '';
3365 if ($filesys eq 'zfs (RAID0)') {
3366 foreach my $hd (@$devlist) {
3367 legacy_bios_4k_check(@$hd[4]);
3368 $cmd .= " @$hd[1]";
3369 }
3370 } elsif ($filesys eq 'zfs (RAID1)') {
3371 die "zfs (RAID1) needs at least 2 device\n" if $diskcount < 2;
3372 $cmd .= ' mirror ';
3373 my $hd = @$devlist[0];
3374 my $expected_size = @$hd[2]; # all disks need approximately same size
3375 foreach my $hd (@$devlist) {
3376 zfs_mirror_size_check($expected_size, @$hd[2]);
3377 legacy_bios_4k_check(@$hd[4]);
3378 $cmd .= " @$hd[1]";
3379 }
3380 } elsif ($filesys eq 'zfs (RAID10)') {
3381 die "zfs (RAID10) needs at least 4 device\n" if $diskcount < 4;
3382 die "zfs (RAID10) needs an even number of devices\n" if $diskcount & 1;
3383
3384 for (my $i = 0; $i < $diskcount; $i+=2) {
3385 my $hd1 = @$devlist[$i];
3386 my $hd2 = @$devlist[$i+1];
3387 zfs_mirror_size_check(@$hd1[2], @$hd2[2]); # pairs need approximately same size
3388 legacy_bios_4k_check(@$hd1[4]);
3389 legacy_bios_4k_check(@$hd2[4]);
3390 $cmd .= ' mirror ' . @$hd1[1] . ' ' . @$hd2[1];
3391 }
3392
3393 } elsif ($filesys =~ m/^zfs \(RAIDZ-([123])\)$/) {
3394 my $level = $1;
3395 my $mindisks = 2 + $level;
3396 die "zfs (RAIDZ-$level) needs at least $mindisks devices\n" if scalar(@$devlist) < $mindisks;
3397 my $hd = @$devlist[0];
3398 my $expected_size = @$hd[2]; # all disks need approximately same size
3399 $cmd .= " raidz$level";
3400 foreach my $hd (@$devlist) {
3401 zfs_mirror_size_check($expected_size, @$hd[2]);
3402 legacy_bios_4k_check(@$hd[4]);
3403 $cmd .= " @$hd[1]";
3404 }
3405 } else {
3406 die "unknown zfs mode '$filesys'\n";
3407 }
3408
3409 return ($devlist, $cmd);
3410 }
3411
3412 sub get_btrfs_raid_setup {
3413
3414 my $filesys = $config_options->{filesys};
3415
3416 my $devlist = &$get_raid_devlist();
3417
3418 my $diskcount = scalar(@$devlist);
3419 die "$filesys needs at least one device\n" if $diskcount < 1;
3420
3421 my $mode;
3422
3423 if ($diskcount == 1) {
3424 $mode = 'single';
3425 } else {
3426 if ($filesys eq 'btrfs (RAID0)') {
3427 $mode = 'raid0';
3428 } elsif ($filesys eq 'btrfs (RAID1)') {
3429 die "btrfs (RAID1) needs at least 2 device\n" if $diskcount < 2;
3430 $mode = 'raid1';
3431 } elsif ($filesys eq 'btrfs (RAID10)') {
3432 die "btrfs (RAID10) needs at least 4 device\n" if $diskcount < 4;
3433 $mode = 'raid10';
3434 } else {
3435 die "unknown btrfs mode '$filesys'\n";
3436 }
3437 }
3438
3439 return ($devlist, $mode);
3440 }
3441
3442 my $last_hd_selected = 0;
3443 sub create_hdsel_view {
3444
3445 $prev_btn->set_sensitive(1); # enable previous button at this point
3446
3447 cleanup_view();
3448
3449 my $vbox = Gtk3::VBox->new(0, 0);
3450 $inbox->pack_start($vbox, 1, 0, 0);
3451 my $hbox = Gtk3::HBox->new(0, 0);
3452 $vbox->pack_start($hbox, 0, 0, 10);
3453
3454 my ($disk, $devname, $size, $model, $logical_bsize) = @{@$hds[0]};
3455 $target_hd = $devname if !defined($target_hd);
3456
3457 $target_hd_label = Gtk3::Label->new("Target Harddisk: ");
3458 $hbox->pack_start($target_hd_label, 0, 0, 0);
3459
3460 $target_hd_combo = Gtk3::ComboBoxText->new();
3461
3462 foreach my $hd (@$hds) {
3463 ($disk, $devname, $size, $model, $logical_bsize) = @$hd;
3464 $target_hd_combo->append_text (get_device_desc($devname, $size, $model));
3465 }
3466
3467 my $raid = $config_options->{filesys} =~ m/zfs|btrfs/;
3468 if ($raid) {
3469 $target_hd_label->set_text("Target: $config_options->{filesys} ");
3470 $target_hd_combo->set_visible(0);
3471 $target_hd_combo->set_no_show_all(1);
3472 }
3473 $target_hd_combo->set_active($last_hd_selected);
3474 $target_hd_combo->signal_connect(changed => sub {
3475 $a = shift->get_active;
3476 my ($disk, $devname) = @{@$hds[$a]};
3477 $last_hd_selected = $a;
3478 $target_hd = $devname;
3479 });
3480
3481 $hbox->pack_start($target_hd_combo, 0, 0, 10);
3482
3483 my $options = Gtk3::Button->new('_Options');
3484 $options->signal_connect (clicked => \&create_hdoption_view);
3485 $hbox->pack_start ($options, 0, 0, 0);
3486
3487
3488 $inbox->show_all;
3489
3490 display_html();
3491
3492 set_next(undef, sub {
3493
3494 if ($config_options->{filesys} =~ m/zfs/) {
3495 my ($devlist) = eval { get_zfs_raid_setup() };
3496 if (my $err = $@) {
3497 display_message("Warning: $err\nPlease fix ZFS setup first.");
3498 return;
3499 }
3500 $config_options->{target_hds} = [ map { $_->[1] } @$devlist ];
3501 } elsif ($config_options->{filesys} =~ m/btrfs/) {
3502 my ($devlist) = eval { get_btrfs_raid_setup() };
3503 if (my $err = $@) {
3504 display_message("Warning: $err\nPlease fix BTRFS setup first.");
3505 return;
3506 }
3507 $config_options->{target_hds} = [ map { $_->[1] } @$devlist ];
3508 } else {
3509 eval { legacy_bios_4k_check(logical_blocksize($target_hd)) };
3510 if (my $err = $@) {
3511 display_message("Warning: $err\n");
3512 return;
3513 }
3514 $config_options->{target_hds} = [ $target_hd ];
3515 }
3516
3517 $step_number++;
3518 create_country_view();
3519 });
3520 }
3521
3522 sub create_extract_view {
3523
3524 cleanup_view();
3525
3526 display_info();
3527
3528 $next->set_sensitive(0);
3529 $prev_btn->set_sensitive(0);
3530 $prev_btn->hide();
3531
3532 my $vbox = Gtk3::VBox->new(0, 0);
3533 $inbox->pack_start ($vbox, 1, 0, 0);
3534 my $hbox = Gtk3::HBox->new(0, 0);
3535 $vbox->pack_start ($hbox, 0, 0, 10);
3536
3537 my $vbox2 = Gtk3::VBox->new(0, 0);
3538 $hbox->pack_start ($vbox2, 0, 0, 0);
3539
3540 $progress_status = Gtk3::Label->new('');
3541 $vbox2->pack_start ($progress_status, 1, 1, 0);
3542
3543 $progress = Gtk3::ProgressBar->new;
3544 $progress->set_show_text(1);
3545 $progress->set_size_request (600, -1);
3546
3547 $vbox2->pack_start($progress, 0, 0, 0);
3548
3549 $inbox->show_all();
3550
3551 my $tdir = $opt_testmode ? "target" : "/target";
3552 mkdir $tdir;
3553 my $base = "${proxmox_cddir}/$setup->{product}-base.squashfs";
3554
3555 eval { extract_data($base, $tdir); };
3556 my $err = $@;
3557
3558 $next->set_sensitive(1);
3559
3560 set_next("_Reboot", sub { exit (0); } );
3561
3562 if ($err) {
3563 display_html("fail.htm");
3564 display_error($err);
3565 } else {
3566 cleanup_view();
3567 display_html("success.htm");
3568
3569 if ($config_options->{autoreboot}) {
3570 Glib::Timeout->add(1000, sub {
3571 if ($autoreboot_seconds > 0) {
3572 $autoreboot_seconds--;
3573 display_html("success.htm");
3574 } else {
3575 exit(0);
3576 }
3577 });
3578 }
3579 }
3580 }
3581
3582 sub create_intro_view {
3583
3584 $prev_btn->set_sensitive(0);
3585
3586 cleanup_view();
3587
3588 if (int($total_memory) < 1024) {
3589 display_error("Less than 1 GiB of usable memory detected, installation will probably fail.\n\n".
3590 "See 'System Requirements' in the $setup->{fullname} documentation.");
3591 }
3592
3593 if ($setup->{product} eq 'pve') {
3594 eval {
3595 my $cpuinfo = file_get_contents('/proc/cpuinfo');
3596 if ($cpuinfo && !($cpuinfo =~ /^flags\s*:.*(vmx|svm)/m)) {
3597 display_error("No support for KVM virtualization detected.\n\n" .
3598 "Check BIOS settings for Intel VT / AMD-V / SVM.")
3599 }
3600 };
3601 }
3602
3603 display_html();
3604
3605 $step_number++;
3606 set_next("I a_gree", \&create_hdsel_view);
3607 }
3608
3609 $ipconf = get_ip_config();
3610
3611 $country = detect_country() if $ipconf->{default} || $opt_testmode;
3612
3613 # read country, kmap and timezone infos
3614 $cmap = read_cmap();
3615
3616 if (!defined($cmap->{country}->{$country})) {
3617 print $logfd "ignoring detected country '$country', invalid or unknown\n";
3618 $country = undef;
3619 }
3620
3621 create_main_window ();
3622
3623 my $initial_error = 0;
3624
3625 if (!defined ($hds) || (scalar (@$hds) <= 0)) {
3626 print "no hardisks found\n";
3627 $initial_error = 1;
3628 display_html("nohds.htm");
3629 set_next("Reboot", sub { exit(0); } );
3630 } else {
3631 foreach my $hd (@$hds) {
3632 my ($disk, $devname) = @$hd;
3633 next if $devname =~ m|^/dev/md\d+$|;
3634 print "found Disk$disk N:$devname\n";
3635 }
3636 }
3637
3638 if (!$initial_error && (scalar keys %{ $ipconf->{ifaces} } == 0)) {
3639 print "no network interfaces found\n";
3640 $initial_error = 1;
3641 display_html("nonics.htm");
3642 set_next("Reboot", sub { exit(0); } );
3643 }
3644
3645 create_intro_view () if !$initial_error;
3646
3647 Gtk3->main;
3648
3649 # reap left over zombie processes
3650 while ((my $child = waitpid(-1, POSIX::WNOHANG)) > 0) {
3651 print "reaped child $child\n";
3652 }
3653
3654 exit 0;