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