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