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