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