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