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