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