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