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