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