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