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