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