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