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