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