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