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