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