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