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