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