]> git.proxmox.com Git - pve-installer.git/blob - proxinstall
3891338cd8556f24ce25f3b8f9f5faf75aac9696
[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 mkdir "$targetdir/mnt";
1293 mkdir "$targetdir/mnt/hostrun";
1294 syscmd("mount --bind /run $targetdir/mnt/hostrun") == 0 ||
1295 die "unable to bindmount run on $targetdir/mnt/hostrun\n";
1296
1297 update_progress(1, 0.05, $maxper, "extracting base system");
1298
1299 my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size) = stat ($basefile);
1300 $ino || die "unable to open file '$basefile' - $!\n";
1301
1302 my $files = file_read_firstline("${proxmox_cddir}/proxmox/$setup->{product}-base.cnt") ||
1303 die "unable to read base file count\n";
1304
1305 my $per = 0;
1306 my $count = 0;
1307
1308 run_command("unsquashfs -f -dest $targetdir -i $basefile", sub {
1309 my $line = shift;
1310 return if $line !~ m/^$targetdir/;
1311 $count++;
1312 my $nper = int (($count *100)/$files);
1313 if ($nper != $per) {
1314 $per = $nper;
1315 my $frac = $per > 100 ? 1 : $per/100;
1316 update_progress($frac, $maxper, 0.5);
1317 }
1318 });
1319
1320 syscmd("mount -n -t tmpfs tmpfs $targetdir/tmp") == 0 ||
1321 die "unable to mount tmpfs on $targetdir/tmp\n";
1322 syscmd("mount -n -t proc proc $targetdir/proc") == 0 ||
1323 die "unable to mount proc on $targetdir/proc\n";
1324 syscmd("mount -n -t sysfs sysfs $targetdir/sys") == 0 ||
1325 die "unable to mount sysfs on $targetdir/sys\n";
1326 syscmd("chroot $targetdir mount --bind /mnt/hostrun /run") == 0 ||
1327 die "unable to re-bindmount hostrun on /run in chroot\n";
1328
1329 update_progress(1, $maxper, 0.5, "configuring base system");
1330
1331 # configure hosts
1332
1333 my $hosts =
1334 "127.0.0.1 localhost.localdomain localhost\n" .
1335 "$ipaddress $hostname.$domain $hostname\n\n" .
1336 "# The following lines are desirable for IPv6 capable hosts\n\n" .
1337 "::1 ip6-localhost ip6-loopback\n" .
1338 "fe00::0 ip6-localnet\n" .
1339 "ff00::0 ip6-mcastprefix\n" .
1340 "ff02::1 ip6-allnodes\n" .
1341 "ff02::2 ip6-allrouters\n" .
1342 "ff02::3 ip6-allhosts\n";
1343
1344 write_config($hosts, "$targetdir/etc/hosts");
1345
1346 write_config("$hostname\n", "$targetdir/etc/hostname");
1347
1348 syscmd("/bin/hostname $hostname") if !$opt_testmode;
1349
1350 # configure interfaces
1351
1352 my $ifaces = "auto lo\niface lo inet loopback\n\n";
1353
1354 my $ntype = $ipversion == 4 ? 'inet' : 'inet6';
1355
1356 my $ethdev = $ipconf->{ifaces}->{$ipconf->{selected}}->{name};
1357
1358 if ($setup->{bridged_network}) {
1359 $ifaces .= "iface $ethdev $ntype manual\n";
1360
1361 $ifaces .=
1362 "\nauto vmbr0\niface vmbr0 $ntype static\n" .
1363 "\taddress $ipaddress\n" .
1364 "\tnetmask $netmask\n" .
1365 "\tgateway $gateway\n" .
1366 "\tbridge_ports $ethdev\n" .
1367 "\tbridge_stp off\n" .
1368 "\tbridge_fd 0\n";
1369 } else {
1370 $ifaces .= "auto $ethdev\n" .
1371 "iface $ethdev $ntype static\n" .
1372 "\taddress $ipaddress\n" .
1373 "\tnetmask $netmask\n" .
1374 "\tgateway $gateway\n";
1375 }
1376
1377 foreach my $iface (sort keys %{$ipconf->{ifaces}}) {
1378 my $name = $ipconf->{ifaces}->{$iface}->{name};
1379 next if $name eq $ethdev;
1380
1381 $ifaces .= "\niface $name $ntype manual\n";
1382 }
1383
1384 write_config($ifaces, "$targetdir/etc/network/interfaces");
1385
1386 # configure dns
1387
1388 my $resolvconf = "search $domain\nnameserver $dnsserver\n";
1389 write_config($resolvconf, "$targetdir/etc/resolv.conf");
1390
1391 # configure fstab
1392
1393 my $fstab = "# <file system> <mount point> <type> <options> <dump> <pass>\n";
1394
1395 if ($use_zfs) {
1396 # do nothing
1397 } elsif ($use_btrfs) {
1398 my $fsuuid;
1399 my $cmd = "blkid -u filesystem -t TYPE=btrfs -o export $rootdev";
1400 run_command($cmd, sub {
1401 my $line = shift;
1402
1403 if ($line =~ m/^UUID=([A-Fa-f0-9\-]+)$/) {
1404 $fsuuid = $1;
1405 }
1406 });
1407
1408 die "unable to detect FS UUID" if !defined($fsuuid);
1409
1410 $fstab .= "UUID=$fsuuid / btrfs defaults 0 1\n";
1411 } else {
1412 my $root_mountopt = $fssetup->{$filesys}->{root_mountopt} || 'defaults';
1413 $fstab .= "$rootdev / $filesys ${root_mountopt} 0 1\n";
1414 }
1415
1416 # mount /boot/efi
1417 # Note: this is required by current grub, but really dangerous, because
1418 # vfat does not have journaling, so it triggers manual fsck after each crash
1419 # so we only mount /boot/efi if really required (efi systems).
1420 if ($grub_plattform =~ m/^efi-/) {
1421 if (scalar(@$bootdevinfo)) {
1422 my $di = @$bootdevinfo[0]; # simply use first disk
1423 if ($di->{esp}) {
1424 my $efi_boot_uuid = $di->{esp};
1425 if (my $uuid = find_dev_by_uuid ($di->{esp})) {
1426 $efi_boot_uuid = "UUID=$uuid";
1427 }
1428
1429 $fstab .= "${efi_boot_uuid} /boot/efi vfat defaults 0 1\n";
1430 }
1431 }
1432 }
1433
1434
1435 $fstab .= "$swapfile none swap sw 0 0\n" if $swapfile;
1436
1437 $fstab .= "proc /proc proc defaults 0 0\n";
1438
1439 write_config($fstab, "$targetdir/etc/fstab");
1440 write_config("", "$targetdir/etc/mtab");
1441
1442 syscmd("cp ${proxmox_libdir}/policy-disable-rc.d " .
1443 "$targetdir/usr/sbin/policy-rc.d") == 0 ||
1444 die "unable to copy policy-rc.d\n";
1445 syscmd("cp ${proxmox_libdir}/fake-start-stop-daemon " .
1446 "$targetdir/sbin/") == 0 ||
1447 die "unable to copy start-stop-daemon\n";
1448
1449 diversion_add($targetdir, "/sbin/start-stop-daemon", "/sbin/fake-start-stop-daemon");
1450 diversion_add($targetdir, "/usr/sbin/update-grub", "/bin/true");
1451 diversion_add($targetdir, "/usr/sbin/update-initramfs", "/bin/true");
1452
1453 syscmd("touch $targetdir/proxmox_install_mode");
1454
1455 my $grub_install_devices_txt = '';
1456 foreach my $di (@$bootdevinfo) {
1457 $grub_install_devices_txt .= ', ' if $grub_install_devices_txt;
1458 $grub_install_devices_txt .= $di->{by_id} || $di->{devname};
1459 }
1460
1461 # Note: keyboard-configuration/xbkb-keymap is used by console-setup
1462 my $xkmap = $cmap->{kmap}->{$keymap}->{x11} // 'us';
1463
1464 debconfig_set ($targetdir, <<_EOD);
1465 locales locales/default_environment_locale select en_US.UTF-8
1466 locales locales/locales_to_be_generated select en_US.UTF-8 UTF-8
1467 samba-common samba-common/dhcp boolean false
1468 samba-common samba-common/workgroup string WORKGROUP
1469 postfix postfix/main_mailer_type select No configuration
1470 keyboard-configuration keyboard-configuration/xkb-keymap select $xkmap
1471 d-i debian-installer/locale select en_US.UTF-8
1472 grub-pc grub-pc/install_devices select $grub_install_devices_txt
1473 _EOD
1474
1475 my $pkg_count = 0;
1476 while (<${proxmox_pkgdir}/*.deb>) { $pkg_count++ };
1477
1478 # btrfs/dpkg is extremely slow without --force-unsafe-io
1479 my $dpkg_opts = $use_btrfs ? "--force-unsafe-io" : "";
1480
1481 $count = 0;
1482 while (<${proxmox_pkgdir}/*.deb>) {
1483 chomp;
1484 my $path = $_;
1485 my ($deb) = $path =~ m/${proxmox_pkgdir}\/(.*\.deb)/;
1486 # if ($deb =~ m/^grub-efi-/ && $deb !~ m/^grub-${grub_plattform}/) {
1487 # $count++;
1488 # next;
1489 # }
1490 update_progress($count/$pkg_count, 0.5, 0.75, "extracting $deb");
1491 print "extracting: $deb\n";
1492 syscmd("cp $path $targetdir/tmp/$deb") == 0 ||
1493 die "installation of package $deb failed\n";
1494 syscmd("chroot $targetdir dpkg $dpkg_opts --force-depends --no-triggers --unpack /tmp/$deb") == 0 ||
1495 die "installation of package $deb failed\n";
1496 update_progress((++$count)/$pkg_count, 0.5, 0.75);
1497 }
1498
1499 # needed for postfix postinst in case no other NIC is active
1500 syscmd("chroot $targetdir ifup lo");
1501
1502 my $cmd = "chroot $targetdir dpkg $dpkg_opts --force-confold --configure -a";
1503 $count = 0;
1504 run_command($cmd, sub {
1505 my $line = shift;
1506 if ($line =~ m/Setting up\s+(\S+)/) {
1507 update_progress((++$count)/$pkg_count, 0.75, 0.95,
1508 "configuring $1");
1509 }
1510 });
1511
1512 unlink "$targetdir/etc/mailname";
1513 $postfix_main_cf =~ s/__FQDN__/${hostname}.${domain}/;
1514 write_config($postfix_main_cf, "$targetdir/etc/postfix/main.cf");
1515
1516 # make sure we have all postfix directories
1517 syscmd("chroot $targetdir /usr/sbin/postfix check");
1518 # cleanup mail queue
1519 syscmd("chroot $targetdir /usr/sbin/postsuper -d ALL");
1520
1521 # enable NTP (timedatectl set-ntp true does not work without DBUS)
1522 syscmd("chroot $targetdir /bin/systemctl enable systemd-timesyncd.service");
1523
1524 unlink "$targetdir/proxmox_install_mode";
1525
1526 # set timezone
1527 unlink ("$targetdir/etc/localtime");
1528 symlink ("/usr/share/zoneinfo/$timezone", "$targetdir/etc/localtime");
1529 write_config("$timezone\n", "$targetdir/etc/timezone");
1530
1531 # set apt mirror
1532 if (my $mirror = $cmap->{country}->{$country}->{mirror}) {
1533 my $fn = "$targetdir/etc/apt/sources.list";
1534 syscmd("sed -i 's/ftp\\.debian\\.org/$mirror/' '$fn'");
1535 }
1536
1537 # create extended_states for apt (avoid cron job warning if that
1538 # file does not exist)
1539 write_config('', "$targetdir/var/lib/apt/extended_states");
1540
1541 # allow ssh root login
1542 syscmd(['sed', '-i', 's/^#\?PermitRootLogin.*/PermitRootLogin yes/', "$targetdir/etc/ssh/sshd_config"]);
1543
1544 if ($setup->{product} eq 'pmg') {
1545 # install initial clamav DB
1546 my $srcdir = "${proxmox_cddir}/proxmox/clamav";
1547 foreach my $fn ("main.cvd", "bytecode.cvd", "daily.cvd", "safebrowsing.cvd") {
1548 syscmd("cp \"$srcdir/$fn\" \"$targetdir/var/lib/clamav\"") == 0 ||
1549 die "installation of clamav db file '$fn' failed\n";
1550 }
1551 syscmd("chroot $targetdir /bin/chown clamav:clamav -R /var/lib/clamav") == 0 ||
1552 die "unable to set owner for clamav database files\n";
1553 }
1554
1555 if ($setup->{product} eq 'pve') {
1556 # save installer settings
1557 my $ucc = uc ($country);
1558 debconfig_set($targetdir, "pve-manager pve-manager/country string $ucc\n");
1559 }
1560
1561 update_progress(0.8, 0.95, 1, "make system bootable");
1562
1563 if ($use_zfs) {
1564 syscmd("sed -i -e 's/^GRUB_CMDLINE_LINUX=.*/GRUB_CMDLINE_LINUX=\"root=ZFS=$zfspoolname\\/ROOT\\/$zfsrootvolname boot=zfs\"/' $targetdir/etc/default/grub") == 0 ||
1565 die "unable to update /etc/default/grub\n";
1566
1567 }
1568
1569 diversion_remove($targetdir, "/usr/sbin/update-grub");
1570 diversion_remove($targetdir, "/usr/sbin/update-initramfs");
1571
1572 my $kapi;
1573 foreach my $fn (<$targetdir/lib/modules/*>) {
1574 if ($fn =~ m!/(\d+\.\d+\.\d+-\d+-pve)$!) {
1575 die "found multiple kernels\n" if defined($kapi);
1576 $kapi = $1;
1577 }
1578 }
1579 die "unable to detect kernel version\n" if !defined($kapi);
1580
1581 if (!$opt_testmode) {
1582
1583 unlink ("$targetdir/etc/mtab");
1584 symlink ("/proc/mounts", "$targetdir/etc/mtab");
1585 syscmd("mount -n --bind /dev $targetdir/dev");
1586
1587 syscmd("chroot $targetdir /usr/sbin/update-initramfs -c -k $kapi") == 0 ||
1588 die "unable to install initramfs\n";
1589
1590 foreach my $di (@$bootdevinfo) {
1591 my $dev = $di->{devname};
1592 syscmd("chroot $targetdir /usr/sbin/grub-install --target i386-pc --no-floppy --bootloader-id='proxmox' $dev") == 0 ||
1593 die "unable to install the i386-pc boot loader on '$dev'\n";
1594
1595 if ($di->{esp}) {
1596 syscmd("mount -n $di->{esp} -t vfat $targetdir/boot/efi") == 0 ||
1597 die "unable to mount $di->{esp}\n";
1598 my $rc = syscmd("chroot $targetdir /usr/sbin/grub-install --target x86_64-efi --no-floppy --bootloader-id='proxmox' $dev");
1599 if ($rc != 0) {
1600 if (-d '/sys/firmware/efi') {
1601 die "unable to install the EFI boot loader on '$dev'\n";
1602 } else {
1603 warn "unable to install the EFI boot loader on '$dev', ignoring (not booted using UEFI)\n";
1604 }
1605 }
1606 # also install fallback boot file (OVMF does not boot without)
1607 mkdir("$targetdir/boot/efi/EFI/BOOT");
1608 syscmd("cp $targetdir/boot/efi/EFI/proxmox/grubx64.efi $targetdir/boot/efi/EFI/BOOT/BOOTx64.EFI") == 0 ||
1609 die "unable to copy efi boot loader\n";
1610
1611 syscmd("umount $targetdir/boot/efi") == 0 ||
1612 die "unable to umount $targetdir/boot/efi\n";
1613 }
1614 }
1615
1616 syscmd("chroot $targetdir /usr/sbin/update-grub") == 0 ||
1617 die "unable to update boot loader config\n";
1618
1619 syscmd("umount $targetdir/dev");
1620 }
1621
1622 # cleanup
1623
1624 unlink "$targetdir/usr/sbin/policy-rc.d";
1625
1626 diversion_remove($targetdir, "/sbin/start-stop-daemon");
1627
1628 # set root password
1629 my $octets = encode("utf-8", $password);
1630 run_command("chroot $targetdir /usr/sbin/chpasswd", undef,
1631 "root:$octets\n");
1632
1633 if ($setup->{product} eq 'pmg') {
1634 # save admin email
1635 write_config("section: admin\n\temail ${mailto}\n",
1636 "$targetdir/etc/pmg/pmg.conf");
1637
1638 } elsif ($setup->{product} eq 'pve') {
1639
1640 # create pmxcfs DB
1641
1642 my $tmpdir = "$targetdir/tmp/pve";
1643 mkdir $tmpdir;
1644
1645 # write vnc keymap to datacenter.cfg
1646 my $vnckmap = $cmap->{kmap}->{$keymap}->{kvm} || 'en-us';
1647 write_config("keyboard: $vnckmap\n",
1648 "$tmpdir/datacenter.cfg");
1649
1650 # save admin email
1651 write_config("user:root\@pam:1:0:::${mailto}::\n",
1652 "$tmpdir/user.cfg");
1653
1654 # write storage.cfg
1655 my $storage_cfg_fn = "$tmpdir/storage.cfg";
1656 if ($use_zfs) {
1657 write_config($storage_cfg_zfs, $storage_cfg_fn);
1658 } elsif ($use_btrfs) {
1659 write_config($storage_cfg_btrfs, $storage_cfg_fn);
1660 } elsif ($datadev) {
1661 write_config($storage_cfg_lvmthin, $storage_cfg_fn);
1662 } else {
1663 write_config($storage_cfg_local, $storage_cfg_fn);
1664 }
1665
1666 run_command("chroot $targetdir /usr/bin/create_pmxcfs_db /tmp/pve /var/lib/pve-cluster/config.db");
1667
1668 syscmd("rm -rf $tmpdir");
1669 }
1670 };
1671
1672 my $err = $@;
1673
1674 update_progress(1, 0, 1, "");
1675
1676 print $err if $err;
1677
1678 if ($opt_testmode) {
1679 my $elapsed = Time::HiRes::tv_interval($starttime);
1680 print "Elapsed extract time: $elapsed\n";
1681
1682 syscmd("chroot $targetdir /usr/bin/dpkg-query -W --showformat='\${package}\n'> final.pkglist");
1683 }
1684
1685 syscmd("umount $targetdir/run");
1686 syscmd("umount $targetdir/mnt/hostrun");
1687 syscmd("umount $targetdir/tmp");
1688 syscmd("umount $targetdir/proc");
1689 syscmd("umount $targetdir/sys");
1690
1691 if ($use_zfs) {
1692 syscmd("zfs umount -a") == 0 ||
1693 die "unable to unmount zfs\n";
1694 } else {
1695 syscmd("umount -d $targetdir");
1696 }
1697
1698 if (!$err && $use_zfs) {
1699 syscmd("zfs set sync=standard $zfspoolname") == 0 ||
1700 die "unable to set zfs properties\n";
1701
1702 syscmd("zfs set mountpoint=/ $zfspoolname/ROOT/$zfsrootvolname") == 0 ||
1703 die "zfs set mountpoint failed\n";
1704
1705 syscmd("zpool set bootfs=$zfspoolname/ROOT/$zfsrootvolname $zfspoolname") == 0 ||
1706 die "zfs set bootfs failed\n";
1707 syscmd("zpool export $zfspoolname");
1708 }
1709
1710 die $err if $err;
1711 }
1712
1713 my $last_display_change = 0;
1714
1715 my $display_info_counter = 0;
1716
1717 my $display_info_items = [
1718 "extract1-license.htm",
1719 "extract2-rulesystem.htm",
1720 "extract3-spam.htm",
1721 "extract4-virus.htm",
1722 ];
1723
1724 sub display_info {
1725
1726 my $min_display_time = 15;
1727
1728 my $ctime = time();
1729
1730 return if ($ctime - $last_display_change) < $min_display_time;
1731
1732 my $page = $display_info_items->[$display_info_counter % scalar(@$display_info_items)];
1733
1734 $display_info_counter++;
1735
1736 display_html($page);
1737 }
1738
1739 sub display_html {
1740 my ($filename) = @_;
1741
1742 $filename = $steps[$step_number]->{html} if !$filename;
1743
1744 my $path = "${proxmox_libdir}/html/$filename";
1745
1746 my $url = "file://$path";
1747
1748 my $data = file_get_contents($path);
1749
1750 if ($filename eq 'license.htm') {
1751 my $license = eval { decode('utf8', file_get_contents("${proxmox_cddir}/EULA")) };
1752 if (my $err = $@) {
1753 die $err if !$opt_testmode;
1754 $license = "TESTMODE: Ignore non existent EULA...\n";
1755 }
1756 my $title = "END USER LICENSE AGREEMENT (EULA)";
1757 $data =~ s/__LICENSE__/$license/;
1758 $data =~ s/__LICENSE_TITLE__/$title/;
1759 }
1760
1761 $htmlview->load_html($data, $url);
1762
1763 $last_display_change = time();
1764 }
1765
1766 sub prev_function {
1767
1768 my ($text, $fctn) = @_;
1769
1770 $fctn = $step_number if !$fctn;
1771 $text = "_Previous" if !$text;
1772 $prev_btn->set_label ($text);
1773
1774 $step_number--;
1775 $steps[$step_number]->{function}();
1776
1777 $prev_btn->grab_focus();
1778 }
1779
1780 sub set_next {
1781 my ($text, $fctn) = @_;
1782
1783 $next_fctn = $fctn;
1784 my $step = $steps[$step_number];
1785 $text //= $steps[$step_number]->{next_button} // '_Next';
1786 $next->set_label($text);
1787
1788 $next->grab_focus();
1789 }
1790
1791 sub create_main_window {
1792
1793 $window = Gtk3::Window->new();
1794 $window->set_default_size(1024, 768);
1795 $window->set_has_resize_grip(0);
1796 $window->set_decorated(0) if !$opt_testmode;
1797
1798 my $vbox = Gtk3::VBox->new(0, 0);
1799
1800 my $logofn = "$setup->{product}-banner.png";
1801 my $image = Gtk3::Image->new_from_file("${proxmox_libdir}/$logofn");
1802 $vbox->pack_start($image, 0, 0, 0);
1803
1804 my $hbox = Gtk3::HBox->new(0, 0);
1805 $vbox->pack_start($hbox, 1, 1, 0);
1806
1807 # my $f1 = Gtk3::Frame->new ('test');
1808 # $f1->set_shadow_type ('none');
1809 # $hbox->pack_start ($f1, 1, 1, 0);
1810
1811 my $sep1 = Gtk3::HSeparator->new();
1812 $vbox->pack_start($sep1, 0, 0, 0);
1813
1814 $cmdbox = Gtk3::HBox->new();
1815 $vbox->pack_start($cmdbox, 0, 0, 10);
1816
1817 $next = Gtk3::Button->new('_Next');
1818 $next->signal_connect(clicked => sub { $last_display_change = 0; &$next_fctn (); });
1819 $cmdbox->pack_end($next, 0, 0, 10);
1820
1821
1822 $prev_btn = Gtk3::Button->new('_Previous');
1823 $prev_btn->signal_connect(clicked => sub { $last_display_change = 0; &prev_function (); });
1824 $cmdbox->pack_end($prev_btn, 0, 0, 10);
1825
1826
1827 my $abort = Gtk3::Button->new('_Abort');
1828 $abort->set_can_focus(0);
1829 $cmdbox->pack_start($abort, 0, 0, 10);
1830 $abort->signal_connect(clicked => sub { exit (-1); });
1831
1832 my $vbox2 = Gtk3::VBox->new(0, 0);
1833 $hbox->add($vbox2);
1834
1835 $htmlview = Gtk3::WebKit2::WebView->new();
1836 my $scrolls = Gtk3::ScrolledWindow->new();
1837 $scrolls->add($htmlview);
1838
1839 my $hbox2 = Gtk3::HBox->new(0, 0);
1840 $hbox2->pack_start($scrolls, 1, 1, 0);
1841
1842 $vbox2->pack_start($hbox2, 1, 1, 0);
1843
1844 my $vbox3 = Gtk3::VBox->new(0, 0);
1845 $vbox2->pack_start($vbox3, 0, 0, 0);
1846
1847 my $sep2 = Gtk3::HSeparator->new;
1848 $vbox3->pack_start($sep2, 0, 0, 0);
1849
1850 $inbox = Gtk3::HBox->new(0, 0);
1851 $vbox3->pack_start($inbox, 0, 0, 0);
1852
1853 $window->add($vbox);
1854
1855 $window->show_all;
1856 $window->realize();
1857 }
1858
1859 sub cleanup_view {
1860 $inbox->foreach(sub {
1861 my $child = shift;
1862 $inbox->remove ($child);
1863 });
1864 }
1865
1866 # fixme: newer GTK3 has special properties to handle numbers with Entry
1867 # only allow floating point numbers with Gtk3::Entry
1868
1869 sub check_float {
1870 my ($entry, $event) = @_;
1871
1872 return check_number($entry, $event, 1);
1873 }
1874
1875 sub check_int {
1876 my ($entry, $event) = @_;
1877
1878 return check_number($entry, $event, 0);
1879 }
1880
1881 sub check_number {
1882 my ($entry, $event, $float) = @_;
1883
1884 my $val = $event->get_keyval;
1885
1886 if (($float && $val == ord '.') ||
1887 $val == Gtk3::Gdk::KEY_ISO_Left_Tab ||
1888 $val == Gtk3::Gdk::KEY_Shift_L ||
1889 $val == Gtk3::Gdk::KEY_Tab ||
1890 $val == Gtk3::Gdk::KEY_Left ||
1891 $val == Gtk3::Gdk::KEY_Right ||
1892 $val == Gtk3::Gdk::KEY_BackSpace ||
1893 $val == Gtk3::Gdk::KEY_Delete ||
1894 ($val >= ord '0' && $val <= ord '9') ||
1895 ($val >= Gtk3::Gdk::KEY_KP_0 &&
1896 $val <= Gtk3::Gdk::KEY_KP_9)) {
1897 return undef;
1898 }
1899
1900 return 1;
1901 }
1902
1903 sub create_text_input {
1904 my ($default, $text) = @_;
1905
1906 my $hbox = Gtk3::HBox->new(0, 0);
1907
1908 my $label = Gtk3::Label->new($text);
1909 $label->set_size_request(150, -1);
1910 $label->set_alignment(1, 0.5);
1911 $hbox->pack_start($label, 0, 0, 10);
1912 my $e1 = Gtk3::Entry->new();
1913 $e1->set_width_chars(30);
1914 $hbox->pack_start($e1, 0, 0, 0);
1915 $e1->set_text($default);
1916
1917 return ($hbox, $e1);
1918 }
1919
1920 sub get_ip_config {
1921
1922 my $ifaces = {};
1923 my $default;
1924
1925 my $links = `ip -o l`;
1926 foreach my $l (split /\n/,$links) {
1927 my ($index, $name, $flags, $state, $mac) = $l =~ m/^(\d+):\s+(\S+):\s+<(\S+)>.*\s+state\s+(\S+)\s+.*\s+link\/ether\s+(\S+)\s+/;
1928 next if !$name || $name eq 'lo';
1929
1930 my $driver = readlink "/sys/class/net/$name/device/driver" || 'unknown';
1931 $driver =~ s!^.*/!!;
1932
1933 $ifaces->{"$index"} = {
1934 name => $name,
1935 driver => $driver,
1936 flags => $flags,
1937 state => $state,
1938 mac => $mac,
1939 };
1940
1941 my $addresses = `ip -o a s $name`;
1942 foreach my $a (split /\n/,$addresses) {
1943 my ($family, $ip, $prefix) = $a =~ m/^\Q$index\E:\s+\Q$name\E\s+(inet|inet6)\s+($IPRE)\/(\d+)\s+/;
1944 next if !$ip;
1945 next if $a =~ /scope\s+link/; # ignore link local
1946
1947 my $mask = $prefix;
1948
1949 if ($family eq 'inet') {
1950 next if !$ip =~ /$IPV4RE/;
1951 next if $prefix < 8 || $prefix > 32;
1952 $mask = @$ipv4_reverse_mask[$prefix];
1953 } else {
1954 next if !$ip =~ /$IPV6RE/;
1955 }
1956
1957 $default = $index if !$default;
1958
1959 $ifaces->{"$index"}->{"$family"} = {
1960 mask => $mask,
1961 addr => $ip,
1962 };
1963 }
1964 }
1965
1966
1967 my $route = `ip route`;
1968 my ($gateway) = $route =~ m/^default\s+via\s+(\S+)\s+/m;
1969
1970 my $resolvconf = `cat /etc/resolv.conf`;
1971 my ($dnsserver) = $resolvconf =~ m/^nameserver\s+(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/m;
1972 my ($domain) = $resolvconf =~ m/^domain\s+(\S+)$/m;
1973
1974 return {
1975 default => $default,
1976 ifaces => $ifaces,
1977 gateway => $gateway,
1978 dnsserver => $dnsserver,
1979 domain => $domain,
1980 }
1981 }
1982
1983 sub display_message {
1984 my ($msg) = @_;
1985
1986 my $dialog = Gtk3::MessageDialog->new($window, 'modal',
1987 'info', 'ok', $msg);
1988 $dialog->run();
1989 $dialog->destroy();
1990 }
1991
1992 sub display_error {
1993 my ($msg) = @_;
1994
1995 my $dialog = Gtk3::MessageDialog->new($window, 'modal',
1996 'error', 'ok', $msg);
1997 $dialog->run();
1998 $dialog->destroy();
1999 }
2000
2001 my $ipconf_first_view = 1;
2002
2003 sub create_ipconf_view {
2004
2005 cleanup_view();
2006 display_html();
2007
2008 my $vbox = Gtk3::VBox->new(0, 0);
2009 $inbox->pack_start($vbox, 1, 0, 0);
2010 my $hbox = Gtk3::HBox->new(0, 0);
2011 $vbox->pack_start($hbox, 0, 0, 10);
2012 my $vbox2 = Gtk3::VBox->new(0, 0);
2013 $hbox->add($vbox2);
2014
2015 my $ipaddr_text = $config->{ipaddress} // "192.168.100.2";
2016 my $ipbox;
2017 ($ipbox, $ipconf_entry_addr) =
2018 create_text_input($ipaddr_text, 'IP Address:');
2019
2020 my $netmask_text = $config->{netmask} // "255.255.255.0";
2021 my $maskbox;
2022 ($maskbox, $ipconf_entry_mask) =
2023 create_text_input($netmask_text, 'Netmask:');
2024
2025 my $device_cb = Gtk3::ComboBoxText->new();
2026 $device_cb->set_active(0);
2027 $device_cb->set_visible(1);
2028
2029 my $get_device_desc = sub {
2030 my $iface = shift;
2031 return "$iface->{name} - $iface->{mac} ($iface->{driver})";
2032 };
2033
2034 my $device_active_map = {};
2035 my $device_active_reverse_map = {};
2036
2037 my $device_change_handler = sub {
2038 my $current = shift;
2039
2040 my $new = $device_active_map->{$current->get_active()};
2041 return if defined($ipconf->{selected}) && $new eq $ipconf->{selected};
2042
2043 $ipconf->{selected} = $new;
2044 my $iface = $ipconf->{ifaces}->{$ipconf->{selected}};
2045 $config->{mngmt_nic} = $iface->{name};
2046 $ipconf_entry_addr->set_text($iface->{inet}->{addr} || $iface->{inet6}->{addr})
2047 if $iface->{inet}->{addr} || $iface->{inet6}->{addr};
2048 $ipconf_entry_mask->set_text($iface->{inet}->{mask} || $iface->{inet6}->{mask})
2049 if $iface->{inet}->{mask} || $iface->{inet6}->{mask};
2050 };
2051
2052 my $i = 0;
2053 foreach my $index (sort keys %{$ipconf->{ifaces}}) {
2054 $device_cb->append_text(&$get_device_desc($ipconf->{ifaces}->{$index}));
2055 $device_active_map->{$i} = $index;
2056 $device_active_reverse_map->{$ipconf->{ifaces}->{$index}->{name}} = $i;
2057 if ($ipconf_first_view && $index == $ipconf->{default}) {
2058 $device_cb->set_active($i);
2059 &$device_change_handler($device_cb);
2060 $ipconf_first_view = 0;
2061 }
2062 $device_cb->signal_connect('changed' => $device_change_handler);
2063 $i++;
2064 }
2065
2066 if (my $nic = $config->{mngmt_nic}) {
2067 $device_cb->set_active($device_active_reverse_map->{$nic} // 0);
2068 } else {
2069 $device_cb->set_active(0);
2070 }
2071
2072 my $devicebox = Gtk3::HBox->new(0, 0);
2073 my $label = Gtk3::Label->new("Management Interface:");
2074 $label->set_size_request(150, -1);
2075 $label->set_alignment(1, 0.5);
2076 $devicebox->pack_start($label, 0, 0, 10);
2077 $devicebox->pack_start($device_cb, 0, 0, 0);
2078
2079 $vbox2->pack_start($devicebox, 0, 0, 2);
2080
2081 my $hn = $config->{fqdn} // "$setup->{product}." . ($ipconf->{domain} // "example.invalid");
2082
2083 my ($hostbox, $hostentry) =
2084 create_text_input($hn, 'Hostname (FQDN):');
2085 $vbox2->pack_start($hostbox, 0, 0, 2);
2086
2087 $vbox2->pack_start($ipbox, 0, 0, 2);
2088
2089 $vbox2->pack_start($maskbox, 0, 0, 2);
2090
2091 $gateway = $config->{gateway} // $ipconf->{gateway} || '192.168.100.1';
2092
2093 my $gwbox;
2094 ($gwbox, $ipconf_entry_gw) =
2095 create_text_input($gateway, 'Gateway:');
2096
2097 $vbox2->pack_start($gwbox, 0, 0, 2);
2098
2099 $dnsserver = $config->{dnsserver} // $ipconf->{dnsserver} || $gateway;
2100
2101 my $dnsbox;
2102 ($dnsbox, $ipconf_entry_dns) =
2103 create_text_input($dnsserver, 'DNS Server:');
2104
2105 $vbox2->pack_start($dnsbox, 0, 0, 0);
2106
2107 $inbox->show_all;
2108 set_next(undef, sub {
2109
2110 # verify hostname
2111
2112 my $text = $hostentry->get_text();
2113
2114 $text =~ s/^\s+//;
2115 $text =~ s/\s+$//;
2116
2117 $config->{fqdn} = $text;
2118
2119 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
2120
2121 # Debian does not support purely numeric hostnames
2122 if ($text && $text =~ /^[0-9]+(?:\.|$)/) {
2123 display_message("Purely numeric hostnames are not allowed.");
2124 $hostentry->grab_focus();
2125 return;
2126 }
2127
2128 if ($text && $text =~ m/^(${namere}\.)*${namere}$/ && $text !~ m/.example.invalid$/ &&
2129 $text =~ m/^([^\.]+)\.(\S+)$/) {
2130 $hostname = $1;
2131 $domain = $2;
2132 } else {
2133 display_message("Hostname does not look like a fully qualified domain name.");
2134 $hostentry->grab_focus();
2135 return;
2136 }
2137
2138 # verify ip address
2139
2140 $text = $ipconf_entry_addr->get_text();
2141 $text =~ s/^\s+//;
2142 $text =~ s/\s+$//;
2143 if ($text =~ m!^($IPV4RE)$!) {
2144 $ipaddress = $text;
2145 $ipversion = 4;
2146 } elsif ($text =~ m!^($IPV6RE)$!) {
2147 $ipaddress = $text;
2148 $ipversion = 6;
2149 } else {
2150 display_message("IP address is not valid.");
2151 $ipconf_entry_addr->grab_focus();
2152 return;
2153 }
2154 $config->{ipaddress} = $ipaddress;
2155
2156 $text = $ipconf_entry_mask->get_text();
2157 $text =~ s/^\s+//;
2158 $text =~ s/\s+$//;
2159 if (($ipversion == 6) && ($text =~ m/^(\d+)$/) && ($1 >= 8) && ($1 <= 126)) {
2160 $netmask = $text;
2161 } elsif (($ipversion == 4) && defined($ipv4_mask_hash->{$text})) {
2162 $netmask = $text;
2163 } else {
2164 display_message("Netmask is not valid.");
2165 $ipconf_entry_mask->grab_focus();
2166 return;
2167 }
2168 $config->{netmask} = $netmask;
2169
2170 $text = $ipconf_entry_gw->get_text();
2171 $text =~ s/^\s+//;
2172 $text =~ s/\s+$//;
2173 if (($ipversion == 4) && ($text =~ m!^($IPV4RE)$!)) {
2174 $gateway = $text;
2175 } elsif (($ipversion == 6) && ($text =~ m!^($IPV6RE)$!)) {
2176 $gateway = $text;
2177 } else {
2178 display_message("Gateway is not valid.");
2179 $ipconf_entry_gw->grab_focus();
2180 return;
2181 }
2182 $config->{gateway} = $gateway;
2183
2184 $text = $ipconf_entry_dns->get_text();
2185 $text =~ s/^\s+//;
2186 $text =~ s/\s+$//;
2187 if (($ipversion == 4) && ($text =~ m!^($IPV4RE)$!)) {
2188 $dnsserver = $text;
2189 } elsif (($ipversion == 6) && ($text =~ m!^($IPV6RE)$!)) {
2190 $dnsserver = $text;
2191 } else {
2192 display_message("DNS server is not valid.");
2193 $ipconf_entry_dns->grab_focus();
2194 return;
2195 }
2196 $config->{dnsserver} = $dnsserver;
2197
2198 #print "TEST $ipaddress $netmask $gateway $dnsserver\n";
2199
2200 $step_number++;
2201 create_ack_view();
2202 });
2203
2204 $hostentry->grab_focus();
2205 }
2206
2207 sub create_ack_view {
2208
2209 cleanup_view();
2210
2211 my $ack_template = "${proxmox_libdir}/html/ack_template.htm";
2212 my $ack_html = "${proxmox_libdir}/html/$steps[$step_number]->{html}";
2213 my $html_data = file_get_contents($ack_template);
2214
2215 my %config_values = (
2216 __target_hd__ => join(' | ', @{$config_options->{target_hds}}),
2217 __target_fs__ => $config_options->{filesys},
2218 __country__ => $cmap->{country}->{$country}->{name},
2219 __timezone__ => $timezone,
2220 __keymap__ => $keymap,
2221 __mailto__ => $mailto,
2222 __interface__ => $ipconf->{ifaces}->{$ipconf->{selected}}->{name},
2223 __hostname__ => $hostname,
2224 __ip__ => $ipaddress,
2225 __netmask__ => $netmask,
2226 __gateway__ => $gateway,
2227 __dnsserver__ => $dnsserver,
2228 );
2229
2230 while ( my ($k, $v) = each %config_values) {
2231 $html_data =~ s/$k/$v/g;
2232 }
2233
2234 write_config($html_data, $ack_html);
2235
2236 display_html();
2237
2238 set_next(undef, sub {
2239 $step_number++;
2240 create_extract_view();
2241 });
2242 }
2243
2244 sub get_device_desc {
2245 my ($devname, $size, $model) = @_;
2246
2247 if ($size && ($size > 0)) {
2248 $size = int($size/2048); # size in MB, from 512B "sectors"
2249
2250 my $text = "$devname (";
2251 if ($size >= 1024) {
2252 $size = int($size/1024); # size in GB
2253 $text .= "${size}GB";
2254 } else {
2255 $text .= "${size}MB";
2256 }
2257
2258 $text .= ", $model" if $model;
2259 $text .= ")";
2260
2261 } else {
2262 return $devname;
2263 }
2264 }
2265
2266 sub update_layout {
2267 my ($cb, $kmap) = @_;
2268
2269 my $ind;
2270 my $def;
2271 my $i = 0;
2272 my $kmaphash = $cmap->{kmaphash};
2273 foreach my $layout (sort keys %$kmaphash) {
2274 $def = $i if $kmaphash->{$layout} eq 'en-us';
2275 $ind = $i if $kmap && $kmaphash->{$layout} eq $kmap;
2276 $i++;
2277 }
2278
2279 $cb->set_active($ind || $def || 0);
2280 }
2281
2282 my $lastzonecb;
2283 sub update_zonelist {
2284 my ($box, $cc) = @_;
2285
2286 my $cczones = $cmap->{cczones};
2287 my $zones = $cmap->{zones};
2288
2289 my $sel;
2290 if ($lastzonecb) {
2291 $sel = $lastzonecb->get_active_text();
2292 $box->remove ($lastzonecb);
2293 } else {
2294 $sel = $timezone; # used once to select default
2295 }
2296
2297 my $cb = $lastzonecb = Gtk3::ComboBoxText->new();
2298 $cb->set_size_request(200, -1);
2299
2300 $cb->signal_connect('changed' => sub {
2301 $timezone = $cb->get_active_text();
2302 });
2303
2304 my @za;
2305 if ($cc && defined ($cczones->{$cc})) {
2306 @za = keys %{$cczones->{$cc}};
2307 } else {
2308 @za = keys %$zones;
2309 }
2310 my $ind;
2311 my $i = 0;
2312 foreach my $zone (sort @za) {
2313 $ind = $i if $sel && $zone eq $sel;
2314 $cb->append_text($zone);
2315 $i++;
2316 }
2317
2318 $cb->set_active($ind || 0);
2319
2320 $cb->show;
2321 $box->pack_start($cb, 0, 0, 0);
2322 }
2323
2324 sub create_password_view {
2325
2326 cleanup_view();
2327
2328 my $vbox2 = Gtk3::VBox->new(0, 0);
2329 $inbox->pack_start($vbox2, 1, 0, 0);
2330 my $vbox = Gtk3::VBox->new(0, 0);
2331 $vbox2->pack_start($vbox, 0, 0, 10);
2332
2333 my $hbox1 = Gtk3::HBox->new(0, 0);
2334 my $label = Gtk3::Label->new("Password");
2335 $label->set_size_request(150, -1);
2336 $label->set_alignment(1, 0.5);
2337 $hbox1->pack_start($label, 0, 0, 10);
2338 my $pwe1 = Gtk3::Entry->new();
2339 $pwe1->set_visibility(0);
2340 $pwe1->set_text($password) if $password;
2341 $pwe1->set_size_request(200, -1);
2342 $hbox1->pack_start($pwe1, 0, 0, 0);
2343
2344 my $hbox2 = Gtk3::HBox->new(0, 0);
2345 $label = Gtk3::Label->new("Confirm");
2346 $label->set_size_request(150, -1);
2347 $label->set_alignment(1, 0.5);
2348 $hbox2->pack_start($label, 0, 0, 10);
2349 my $pwe2 = Gtk3::Entry->new();
2350 $pwe2->set_visibility(0);
2351 $pwe2->set_text($password) if $password;
2352 $pwe2->set_size_request(200, -1);
2353 $hbox2->pack_start($pwe2, 0, 0, 0);
2354
2355 my $hbox3 = Gtk3::HBox->new(0, 0);
2356 $label = Gtk3::Label->new("E-Mail");
2357 $label->set_size_request(150, -1);
2358 $label->set_alignment(1, 0.5);
2359 $hbox3->pack_start($label, 0, 0, 10);
2360 my $eme = Gtk3::Entry->new();
2361 $eme->set_size_request(200, -1);
2362 $eme->set_text($mailto);
2363 $hbox3->pack_start($eme, 0, 0, 0);
2364
2365
2366 $vbox->pack_start($hbox1, 0, 0, 5);
2367 $vbox->pack_start($hbox2, 0, 0, 5);
2368 $vbox->pack_start($hbox3, 0, 0, 15);
2369
2370 $inbox->show_all;
2371
2372 display_html();
2373
2374 set_next (undef, sub {
2375
2376 my $t1 = $pwe1->get_text;
2377 my $t2 = $pwe2->get_text;
2378
2379 if (length ($t1) < 5) {
2380 display_message("Password is too short.");
2381 $pwe1->grab_focus();
2382 return;
2383 }
2384
2385 if ($t1 ne $t2) {
2386 display_message("Password does not match.");
2387 $pwe1->grab_focus();
2388 return;
2389 }
2390
2391 my $t3 = $eme->get_text;
2392 if ($t3 !~ m/^\S+\@\S+\.\S+$/) {
2393 display_message("E-Mail does not look like a valid address" .
2394 " (user\@domain.tld)");
2395 $eme->grab_focus();
2396 return;
2397 }
2398
2399 if ($t3 eq 'mail@example.invalid') {
2400 display_message("Please enter a valid E-Mail address");
2401 $eme->grab_focus();
2402 return;
2403 }
2404
2405 $password = $t1;
2406 $mailto = $t3;
2407
2408 $step_number++;
2409 create_ipconf_view();
2410 });
2411
2412 $pwe1->grab_focus();
2413
2414 }
2415
2416 sub create_country_view {
2417
2418 cleanup_view();
2419
2420 my $countryhash = $cmap->{countryhash};
2421 my $ctr = $cmap->{country};
2422
2423 my $vbox2 = Gtk3::VBox->new(0, 0);
2424 $inbox->pack_start($vbox2, 1, 0, 0);
2425 my $vbox = Gtk3::VBox->new(0, 0);
2426 $vbox2->pack_start($vbox, 0, 0, 10);
2427
2428 my $w = Gtk3::Entry->new();
2429 $w->set_size_request(200, -1);
2430
2431 my $c = Gtk3::EntryCompletion->new();
2432 $c->set_text_column(0);
2433 $c->set_minimum_key_length(0);
2434 $c->set_popup_set_width(1);
2435 $c->set_inline_completion(1);
2436
2437 my $hbox2 = Gtk3::HBox->new(0, 0);
2438 my $label = Gtk3::Label->new("Time zone");
2439 $label->set_size_request(150, -1);
2440 $label->set_alignment(1, 0.5);
2441 $hbox2->pack_start($label, 0, 0, 10);
2442 update_zonelist ($hbox2);
2443
2444 my $hbox3 = Gtk3::HBox->new(0, 0);
2445 $label = Gtk3::Label->new("Keyboard Layout");
2446 $label->set_size_request(150, -1);
2447 $label->set_alignment(1, 0.5);
2448 $hbox3->pack_start($label, 0, 0, 10);
2449
2450 my $kmapcb = Gtk3::ComboBoxText->new();
2451 $kmapcb->set_size_request (200, -1);
2452 foreach my $layout (sort keys %{$cmap->{kmaphash}}) {
2453 $kmapcb->append_text ($layout);
2454 }
2455
2456 update_layout($kmapcb);
2457 $hbox3->pack_start ($kmapcb, 0, 0, 0);
2458
2459 $kmapcb->signal_connect ('changed' => sub {
2460 my $sel = $kmapcb->get_active_text();
2461 if (my $kmap = $cmap->{kmaphash}->{$sel}) {
2462 my $xkmap = $cmap->{kmap}->{$kmap}->{x11};
2463 my $xvar = $cmap->{kmap}->{$kmap}->{x11var};
2464 syscmd ("setxkbmap $xkmap $xvar") if !$opt_testmode;
2465 $keymap = $kmap;
2466 }
2467 });
2468
2469 $w->signal_connect ('changed' => sub {
2470 my ($entry, $event) = @_;
2471 my $text = $entry->get_text;
2472
2473 if (my $cc = $countryhash->{lc($text)}) {
2474 update_zonelist($hbox2, $cc);
2475 my $kmap = $ctr->{$cc}->{kmap} || 'en-us';
2476 update_layout($kmapcb, $kmap);
2477 }
2478 });
2479
2480 $w->signal_connect (key_press_event => sub {
2481 my ($entry, $event) = @_;
2482 my $text = $entry->get_text;
2483
2484 my $val = $event->get_keyval;
2485
2486 if ($val == Gtk3::Gdk::KEY_Tab) {
2487 my $cc = $countryhash->{lc($text)};
2488
2489 my $found = 0;
2490 my $compl;
2491
2492 if ($cc) {
2493 $found = 1;
2494 $compl = $ctr->{$cc}->{name};
2495 } else {
2496 foreach my $cc (keys %$ctr) {
2497 my $ct = $ctr->{$cc}->{name};
2498 if ($ct =~ m/^\Q$text\E.*$/i) {
2499 $found++;
2500 $compl = $ct;
2501 }
2502 last if $found > 1;
2503 }
2504 }
2505
2506 if ($found == 1) {
2507 $entry->set_text($compl);
2508 $c->complete();
2509 return undef;
2510 } else {
2511 #Gtk3::Gdk::beep();
2512 print chr(7); # beep ?
2513 }
2514
2515 $c->complete();
2516
2517 my $buf = $w->get_buffer();
2518 $buf->insert_text(-1, '', -1); # popup selection
2519
2520 return 1;
2521 }
2522
2523 return undef;
2524 });
2525
2526 my $ls = Gtk3::ListStore->new('Glib::String');
2527 foreach my $cc (sort {$ctr->{$a}->{name} cmp $ctr->{$b}->{name} } keys %$ctr) {
2528 my $iter = $ls->append();
2529 $ls->set ($iter, 0, $ctr->{$cc}->{name});
2530 }
2531 $c->set_model ($ls);
2532
2533 $w->set_completion ($c);
2534
2535 my $hbox = Gtk3::HBox->new(0, 0);
2536
2537 $label = Gtk3::Label->new("Country");
2538 $label->set_alignment(1, 0.5);
2539 $label->set_size_request(150, -1);
2540 $hbox->pack_start($label, 0, 0, 10);
2541 $hbox->pack_start($w, 0, 0, 0);
2542
2543 $vbox->pack_start($hbox, 0, 0, 5);
2544 $vbox->pack_start($hbox2, 0, 0, 5);
2545 $vbox->pack_start($hbox3, 0, 0, 5);
2546
2547 if ($country && $ctr->{$country}) {
2548 $w->set_text ($ctr->{$country}->{name});
2549 }
2550
2551 $inbox->show_all;
2552
2553 display_html();
2554 set_next (undef, sub {
2555
2556 my $text = $w->get_text;
2557
2558 if (my $cc = $countryhash->{lc($text)}) {
2559 $country = $cc;
2560 $step_number++;
2561 create_password_view();
2562 return;
2563 } else {
2564 display_message("Please select a country first.");
2565 $w->grab_focus();
2566 }
2567 });
2568
2569 $w->grab_focus();
2570 }
2571
2572 my $target_hd_combo;
2573 my $target_hd_label;
2574
2575 my $hdoption_first_setup = 1;
2576
2577 my $create_basic_grid = sub {
2578 my $grid = Gtk3::Grid->new();
2579 $grid->set_visible(1);
2580 $grid->set_column_spacing(10);
2581 $grid->set_row_spacing(10);
2582 $grid->set_hexpand(1);
2583
2584 $grid->set_margin_start(5);
2585 $grid->set_margin_end(5);
2586 $grid->set_margin_top(5);
2587 $grid->set_margin_bottom(5);
2588
2589 return $grid;
2590 };
2591
2592 my $create_label_widget_grid = sub {
2593 my ($labeled_widgets) = @_;
2594
2595 my $grid = &$create_basic_grid();
2596 my $row = 0;
2597
2598 for (my $i = 0; $i < @$labeled_widgets; $i += 2) {
2599 my $widget = @$labeled_widgets[$i+1];
2600 my $label = Gtk3::Label->new(@$labeled_widgets[$i]);
2601 $label->set_visible(1);
2602 $label->set_alignment (1, 0.5);
2603 $grid->attach($label, 0, $row, 1, 1);
2604 $widget->set_visible(1);
2605 $grid->attach($widget, 1, $row, 1, 1);
2606 $row++;
2607 }
2608
2609 return $grid;
2610 };
2611
2612 my $create_raid_disk_grid = sub {
2613 my $disk_labeled_widgets = [];
2614 for (my $i = 0; $i < @$hds; $i++) {
2615 my $disk_selector = Gtk3::ComboBoxText->new();
2616 $disk_selector->append_text("-- do not use --");
2617 $disk_selector->set_active(0);
2618 $disk_selector->set_visible(1);
2619 foreach my $hd (@$hds) {
2620 my ($disk, $devname, $size, $model) = @$hd;
2621 $disk_selector->append_text(get_device_desc ($devname, $size, $model));
2622 $disk_selector->{pve_disk_id} = $i;
2623 $disk_selector->signal_connect (changed => sub {
2624 my $w = shift;
2625 my $diskid = $w->{pve_disk_id};
2626 my $a = $w->get_active - 1;
2627 $config_options->{"disksel${diskid}"} = ($a >= 0) ? $hds->[$a] : undef;
2628 });
2629 }
2630
2631 if ($hdoption_first_setup) {
2632 $disk_selector->set_active ($i+1) if $hds->[$i];
2633 } else {
2634 my $hdind = 0;
2635 if (my $cur_hd = $config_options->{"disksel$i"}) {
2636 foreach my $hd (@$hds) {
2637 if (@$hd[1] eq @$cur_hd[1]) {
2638 $disk_selector->set_active($hdind+1);
2639 last;
2640 }
2641 $hdind++;
2642 }
2643 }
2644 }
2645
2646 push @$disk_labeled_widgets, "Harddisk $i", $disk_selector;
2647 }
2648
2649 my $scrolled_window = Gtk3::ScrolledWindow->new();
2650 $scrolled_window->set_hexpand(1);
2651 $scrolled_window->set_propagate_natural_height(1) if @$hds > 4;
2652 $scrolled_window->add(&$create_label_widget_grid($disk_labeled_widgets));
2653 $scrolled_window->set_policy('never', 'automatic');
2654
2655 return $scrolled_window;
2656 # &$create_label_widget_grid($disk_labeled_widgets)
2657 };
2658
2659 # shared between different ui parts (e.g., ZFS and "normal" single disk FS)
2660 my $hdsize_size_adj;
2661 my $hdsize_entry_buffer;
2662
2663 my $get_hdsize_spinbtn = sub {
2664 my $hdsize = shift;
2665
2666 $hdsize_entry_buffer //= Gtk3::EntryBuffer->new(undef, 1);
2667
2668 if (defined($hdsize)) {
2669 $hdsize_size_adj = Gtk3::Adjustment->new($config_options->{hdsize} || $hdsize, 0, $hdsize+1, 1, 1, 1);
2670 } else {
2671 die "called get_hdsize_spinbtn with \$hdsize_size_adj not defined but did not pass hdsize!\n"
2672 if !defined($hdsize_size_adj);
2673 }
2674
2675 my $spinbutton_hdsize = Gtk3::SpinButton->new($hdsize_size_adj, 1, 1);
2676 $spinbutton_hdsize->set_buffer($hdsize_entry_buffer);
2677 $spinbutton_hdsize->set_adjustment($hdsize_size_adj);
2678 $spinbutton_hdsize->set_tooltip_text("only use specified size (GB) of the harddisk (rest left unpartitioned)");
2679 return $spinbutton_hdsize;
2680 };
2681
2682 my $create_raid_advanced_grid = sub {
2683 my $labeled_widgets = [];
2684 my $spinbutton_ashift = Gtk3::SpinButton->new_with_range(9,13,1);
2685 $spinbutton_ashift->set_tooltip_text("zpool ashift property (pool sector size, default 2^12)");
2686 $spinbutton_ashift->signal_connect ("value-changed" => sub {
2687 my $w = shift;
2688 $config_options->{ashift} = $w->get_value_as_int();
2689 });
2690 $config_options->{ashift} = 12 if ! defined($config_options->{ashift});
2691 $spinbutton_ashift->set_value($config_options->{ashift});
2692 push @$labeled_widgets, "ashift";
2693 push @$labeled_widgets, $spinbutton_ashift;
2694
2695 my $combo_compress = Gtk3::ComboBoxText->new();
2696 $combo_compress->set_tooltip_text("zfs compression algorithm for rpool dataset");
2697 # note: gzip / lze not allowed for bootfs vdevs
2698 my $comp_opts = ["on","off","lzjb","lz4"];
2699 foreach my $opt (@$comp_opts) {
2700 $combo_compress->append($opt, $opt);
2701 }
2702 $config_options->{compress} = "on" if !defined($config_options->{compress});
2703 $combo_compress->set_active_id($config_options->{compress});
2704 $combo_compress->signal_connect (changed => sub {
2705 my $w = shift;
2706 $config_options->{compress} = $w->get_active_text();
2707 });
2708 push @$labeled_widgets, "compress";
2709 push @$labeled_widgets, $combo_compress;
2710
2711 my $combo_checksum = Gtk3::ComboBoxText->new();
2712 $combo_checksum->set_tooltip_text("zfs checksum algorithm for rpool dataset");
2713 my $csum_opts = ["on", "off","fletcher2", "fletcher4", "sha256"];
2714 foreach my $opt (@$csum_opts) {
2715 $combo_checksum->append($opt, $opt);
2716 }
2717 $config_options->{checksum} = "on" if !($config_options->{checksum});
2718 $combo_checksum->set_active_id($config_options->{checksum});
2719 $combo_checksum->signal_connect (changed => sub {
2720 my $w = shift;
2721 $config_options->{checksum} = $w->get_active_text();
2722 });
2723 push @$labeled_widgets, "checksum";
2724 push @$labeled_widgets, $combo_checksum;
2725
2726 my $spinbutton_copies = Gtk3::SpinButton->new_with_range(1,3,1);
2727 $spinbutton_copies->set_tooltip_text("zfs copies property for rpool dataset (in addition to RAID redundancy!)");
2728 $spinbutton_copies->signal_connect ("value-changed" => sub {
2729 my $w = shift;
2730 $config_options->{copies} = $w->get_value_as_int();
2731 });
2732 $config_options->{copies} = 1 if !defined($config_options->{copies});
2733 $spinbutton_copies->set_value($config_options->{copies});
2734 push @$labeled_widgets, "copies", $spinbutton_copies;
2735
2736 push @$labeled_widgets, "hdsize", $get_hdsize_spinbtn->();
2737 return &$create_label_widget_grid($labeled_widgets);;
2738 };
2739
2740 sub create_hdoption_view {
2741
2742 my $dialog = Gtk3::Dialog->new();
2743
2744 $dialog->set_title("Harddisk options");
2745
2746 $dialog->add_button("_OK", 1);
2747
2748 my $contarea = $dialog->get_content_area();
2749
2750 my $hbox2 = Gtk3::Box->new('horizontal', 0);
2751 $contarea->pack_start($hbox2, 1, 1, 10);
2752
2753 my $grid = Gtk3::Grid->new();
2754 $grid->set_column_spacing(10);
2755 $grid->set_row_spacing(10);
2756
2757 $hbox2->pack_start($grid, 1, 0, 10);
2758
2759 my $row = 0;
2760
2761 # Filesystem type
2762
2763 my $label0 = Gtk3::Label->new("Filesystem");
2764 $label0->set_alignment (1, 0.5);
2765 $grid->attach($label0, 0, $row, 1, 1);
2766
2767 my $fstypecb = Gtk3::ComboBoxText->new();
2768
2769 my $fstype = ['ext3', 'ext4', 'xfs',
2770 'zfs (RAID0)', 'zfs (RAID1)',
2771 'zfs (RAID10)', 'zfs (RAIDZ-1)',
2772 'zfs (RAIDZ-2)', 'zfs (RAIDZ-3)'];
2773
2774 push @$fstype, 'btrfs (RAID0)', 'btrfs (RAID1)', 'btrfs (RAID10)'
2775 if $setup->{enable_btrfs};
2776
2777 my $tcount = 0;
2778 foreach my $tmp (@$fstype) {
2779 $fstypecb->append_text($tmp);
2780 $fstypecb->set_active ($tcount)
2781 if $config_options->{filesys} eq $tmp;
2782 $tcount++;
2783 }
2784
2785 $grid->attach($fstypecb, 1, $row, 1, 1);
2786
2787 $hbox2->show_all();
2788
2789 $row++;
2790
2791 my $sep = Gtk3::HSeparator->new();
2792 $sep->set_visible(1);
2793 $grid->attach($sep, 0, $row, 2, 1);
2794 $row++;
2795
2796 my $hw_raid_note = Gtk3::Label->new("Note: ZFS is not compatible with disks backed by a hardware RAID controller. For details see the reference documentation.");
2797 $hw_raid_note->set_line_wrap(1);
2798 $hw_raid_note->set_max_width_chars(30);
2799 #$hw_raid_note->set_size_request(150, -1);
2800 $hw_raid_note->set_visible(0);
2801 $grid->attach($hw_raid_note, 0, $row++, 2, 1);
2802
2803 my $hdsize_labeled_widgets = [];
2804
2805 # size compute
2806 my $hdsize = 0;
2807 if ( -b $target_hd) {
2808 $hdsize = int(hd_size ($target_hd) / (1024*1024.0)); # size in GB
2809 } elsif ($target_hd) {
2810 $hdsize = int((-s $target_hd) / (1024*1024*1024.0));
2811 }
2812
2813 my $spinbutton_hdsize = $get_hdsize_spinbtn->($hdsize);
2814 push @$hdsize_labeled_widgets, "hdsize", $spinbutton_hdsize;
2815
2816 my $entry_swapsize = Gtk3::Entry->new();
2817 $entry_swapsize->set_tooltip_text("maximum SWAP size (GB)");
2818 $entry_swapsize->signal_connect (key_press_event => \&check_float);
2819 $entry_swapsize->set_text($config_options->{swapsize}) if defined($config_options->{swapsize});
2820 push @$hdsize_labeled_widgets, "swapsize", $entry_swapsize;
2821
2822 my $entry_maxroot = Gtk3::Entry->new();
2823 if ($setup->{product} eq 'pve') {
2824 $entry_maxroot->set_tooltip_text("maximum size (GB) for LVM root volume");
2825 $entry_maxroot->signal_connect (key_press_event => \&check_float);
2826 $entry_maxroot->set_text($config_options->{maxroot}) if $config_options->{maxroot};
2827 push @$hdsize_labeled_widgets, "maxroot", $entry_maxroot;
2828 }
2829
2830 my $entry_minfree = Gtk3::Entry->new();
2831 $entry_minfree->set_tooltip_text("minimum free LVM space (GB, required for LVM snapshots)");
2832 $entry_minfree->signal_connect (key_press_event => \&check_float);
2833 $entry_minfree->set_text($config_options->{minfree}) if defined($config_options->{minfree});
2834 push @$hdsize_labeled_widgets, "minfree", $entry_minfree;
2835
2836 my $entry_maxvz;
2837 if ($setup->{product} eq 'pve') {
2838 $entry_maxvz = Gtk3::Entry->new();
2839 $entry_maxvz->set_tooltip_text("maximum size (GB) for LVM data volume");
2840 $entry_maxvz->signal_connect (key_press_event => \&check_float);
2841 $entry_maxvz->set_text($config_options->{maxvz}) if defined($config_options->{maxvz});
2842 push @$hdsize_labeled_widgets, "maxvz", $entry_maxvz;
2843 }
2844
2845 my $options_stack = Gtk3::Stack->new();
2846 $options_stack->set_visible(1);
2847 $options_stack->set_hexpand(1);
2848 $options_stack->set_vexpand(1);
2849 $options_stack->add_titled(&$create_raid_disk_grid(), "raiddisk", "Disk Setup");
2850 $options_stack->add_titled(&$create_label_widget_grid($hdsize_labeled_widgets), "hdsize", "Size Options");
2851 $options_stack->add_titled(&$create_raid_advanced_grid("zfs"), "raidzfsadvanced", "Advanced Options");
2852 $options_stack->set_visible_child_name("raiddisk");
2853 my $options_stack_switcher = Gtk3::StackSwitcher->new();
2854 $options_stack_switcher->set_halign('center');
2855 $options_stack_switcher->set_stack($options_stack);
2856 $grid->attach($options_stack_switcher, 0, $row, 2, 1);
2857 $row++;
2858 $grid->attach($options_stack, 0, $row, 2, 1);
2859 $row++;
2860
2861 $hdoption_first_setup = 0;
2862
2863 my $switch_view = sub {
2864 my $raid = $config_options->{filesys} =~ m/zfs|btrfs/;
2865 my $enable_zfs_opts = $config_options->{filesys} =~ m/zfs/;
2866
2867 $target_hd_combo->set_visible(!$raid);
2868 $options_stack->get_child_by_name("hdsize")->set_visible(!$raid);
2869 $options_stack->get_child_by_name("raiddisk")->set_visible($raid);
2870 $hw_raid_note->set_visible($raid);
2871 $options_stack_switcher->set_visible($enable_zfs_opts);
2872 $options_stack->get_child_by_name("raidzfsadvanced")->set_visible($enable_zfs_opts);
2873 if ($raid) {
2874 $target_hd_label->set_text("Target: $config_options->{filesys} ");
2875 $options_stack->set_visible_child_name("raiddisk");
2876 } else {
2877 $target_hd_label->set_text("Target Harddisk: ");
2878 }
2879 my (undef, $pref_width) = $dialog->get_preferred_width();
2880 my (undef, $pref_height) = $dialog->get_preferred_height();
2881 $pref_height = 750 if $pref_height > 750;
2882 $dialog->resize($pref_width, $pref_height);
2883 };
2884
2885 &$switch_view();
2886
2887 $fstypecb->signal_connect (changed => sub {
2888 $config_options->{filesys} = $fstypecb->get_active_text();
2889 &$switch_view();
2890 });
2891
2892 $dialog->show();
2893
2894 $dialog->run();
2895
2896 my $get_float = sub {
2897 my ($entry) = @_;
2898
2899 my $text = $entry->get_text();
2900 return undef if !defined($text);
2901
2902 $text =~ s/^\s+//;
2903 $text =~ s/\s+$//;
2904
2905 return undef if $text !~ m/^\d+(\.\d+)?$/;
2906
2907 return $text;
2908 };
2909
2910 my $tmp;
2911
2912 if (($tmp = &$get_float($spinbutton_hdsize)) && ($tmp != $hdsize)) {
2913 $config_options->{hdsize} = $tmp;
2914 } else {
2915 delete $config_options->{hdsize};
2916 }
2917
2918 if (defined($tmp = &$get_float($entry_swapsize))) {
2919 $config_options->{swapsize} = $tmp;
2920 } else {
2921 delete $config_options->{swapsize};
2922 }
2923
2924 if (defined($tmp = &$get_float($entry_maxroot))) {
2925 $config_options->{maxroot} = $tmp;
2926 } else {
2927 delete $config_options->{maxroot};
2928 }
2929
2930 if (defined($tmp = &$get_float($entry_minfree))) {
2931 $config_options->{minfree} = $tmp;
2932 } else {
2933 delete $config_options->{minfree};
2934 }
2935
2936 if ($entry_maxvz && defined($tmp = &$get_float($entry_maxvz))) {
2937 $config_options->{maxvz} = $tmp;
2938 } else {
2939 delete $config_options->{maxvz};
2940 }
2941
2942 $dialog->destroy();
2943 }
2944
2945 my $get_raid_devlist = sub {
2946
2947 my $dev_name_hash = {};
2948
2949 my $devlist = [];
2950 for (my $i = 0; $i < @$hds; $i++) {
2951 if (my $hd = $config_options->{"disksel$i"}) {
2952 my ($disk, $devname, $size, $model) = @$hd;
2953 die "device '$devname' is used more than once\n"
2954 if $dev_name_hash->{$devname};
2955 $dev_name_hash->{$devname} = $hd;
2956 push @$devlist, $hd;
2957 }
2958 }
2959
2960 return $devlist;
2961 };
2962
2963 sub zfs_mirror_size_check {
2964 my ($expected, $actual) = @_;
2965
2966 die "mirrored disks must have same size\n"
2967 if abs($expected - $actual) > $expected / 10;
2968 }
2969
2970 sub get_zfs_raid_setup {
2971
2972 my $filesys = $config_options->{filesys};
2973
2974 my $devlist = &$get_raid_devlist();
2975
2976 my $diskcount = scalar(@$devlist);
2977 die "$filesys needs at least one device\n" if $diskcount < 1;
2978
2979 my $bootdevlist = [];
2980
2981 my $cmd= '';
2982 if ($filesys eq 'zfs (RAID0)') {
2983 push @$bootdevlist, @$devlist[0];
2984 foreach my $hd (@$devlist) {
2985 $cmd .= " @$hd[1]";
2986 }
2987 } elsif ($filesys eq 'zfs (RAID1)') {
2988 die "zfs (RAID1) needs at least 2 device\n" if $diskcount < 2;
2989 $cmd .= ' mirror ';
2990 my $hd = @$devlist[0];
2991 my $expected_size = @$hd[2]; # all disks need approximately same size
2992 foreach $hd (@$devlist) {
2993 zfs_mirror_size_check($expected_size, @$hd[2]);
2994 $cmd .= " @$hd[1]";
2995 push @$bootdevlist, $hd;
2996 }
2997 } elsif ($filesys eq 'zfs (RAID10)') {
2998 die "zfs (RAID10) needs at least 4 device\n" if $diskcount < 4;
2999 die "zfs (RAID10) needs an even number of devices\n" if $diskcount & 1;
3000
3001 push @$bootdevlist, @$devlist[0], @$devlist[1];
3002
3003 for (my $i = 0; $i < $diskcount; $i+=2) {
3004 my $hd1 = @$devlist[$i];
3005 my $hd2 = @$devlist[$i+1];
3006 zfs_mirror_size_check(@$hd1[2], @$hd2[2]); # pairs need approximately same size
3007 $cmd .= ' mirror ' . @$hd1[1] . ' ' . @$hd2[1];
3008 }
3009
3010 } elsif ($filesys =~ m/^zfs \(RAIDZ-([123])\)$/) {
3011 my $level = $1;
3012 my $mindisks = 2 + $level;
3013 die "zfs (RAIDZ-$level) needs at least $mindisks devices\n" if scalar(@$devlist) < $mindisks;
3014 my $hd = @$devlist[0];
3015 my $expected_size = @$hd[2]; # all disks need approximately same size
3016 $cmd .= " raidz$level";
3017 foreach $hd (@$devlist) {
3018 zfs_mirror_size_check($expected_size, @$hd[2]);
3019 $cmd .= " @$hd[1]";
3020 push @$bootdevlist, $hd;
3021 }
3022 } else {
3023 die "unknown zfs mode '$filesys'\n";
3024 }
3025
3026 return ($devlist, $bootdevlist, $cmd);
3027 }
3028
3029 sub get_btrfs_raid_setup {
3030
3031 my $filesys = $config_options->{filesys};
3032
3033 my $devlist = &$get_raid_devlist();
3034
3035 my $diskcount = scalar(@$devlist);
3036 die "$filesys needs at least one device\n" if $diskcount < 1;
3037
3038 my $mode;
3039
3040 if ($diskcount == 1) {
3041 $mode = 'single';
3042 } else {
3043 if ($filesys eq 'btrfs (RAID0)') {
3044 $mode = 'raid0';
3045 } elsif ($filesys eq 'btrfs (RAID1)') {
3046 die "btrfs (RAID1) needs at least 2 device\n" if $diskcount < 2;
3047 $mode = 'raid1';
3048 } elsif ($filesys eq 'btrfs (RAID10)') {
3049 die "btrfs (RAID10) needs at least 4 device\n" if $diskcount < 4;
3050 $mode = 'raid10';
3051 } else {
3052 die "unknown btrfs mode '$filesys'\n";
3053 }
3054 }
3055
3056 return ($devlist, $mode);
3057 }
3058
3059 my $last_hd_selected = 0;
3060 sub create_hdsel_view {
3061
3062 $prev_btn->set_sensitive(1); # enable previous button at this point
3063
3064 cleanup_view();
3065
3066 my $vbox = Gtk3::VBox->new(0, 0);
3067 $inbox->pack_start($vbox, 1, 0, 0);
3068 my $hbox = Gtk3::HBox->new(0, 0);
3069 $vbox->pack_start($hbox, 0, 0, 10);
3070
3071 my ($disk, $devname, $size, $model) = @{@$hds[0]};
3072 $target_hd = $devname if !defined($target_hd);
3073
3074 $target_hd_label = Gtk3::Label->new("Target Harddisk: ");
3075 $hbox->pack_start($target_hd_label, 0, 0, 0);
3076
3077 $target_hd_combo = Gtk3::ComboBoxText->new();
3078
3079 foreach my $hd (@$hds) {
3080 ($disk, $devname, $size, $model) = @$hd;
3081 $target_hd_combo->append_text (get_device_desc($devname, $size, $model));
3082 }
3083
3084 my $raid = $config_options->{filesys} =~ m/zfs|btrfs/;
3085 if ($raid) {
3086 $target_hd_label->set_text("Target: $config_options->{filesys} ");
3087 $target_hd_combo->set_visible(0);
3088 $target_hd_combo->set_no_show_all(1);
3089 }
3090 $target_hd_combo->set_active($last_hd_selected);
3091 $target_hd_combo->signal_connect(changed => sub {
3092 $a = shift->get_active;
3093 my ($disk, $devname) = @{@$hds[$a]};
3094 $last_hd_selected = $a;
3095 $target_hd = $devname;
3096 });
3097
3098 $hbox->pack_start($target_hd_combo, 0, 0, 10);
3099
3100 my $options = Gtk3::Button->new('_Options');
3101 $options->signal_connect (clicked => \&create_hdoption_view);
3102 $hbox->pack_start ($options, 0, 0, 0);
3103
3104
3105 $inbox->show_all;
3106
3107 display_html();
3108
3109 set_next(undef, sub {
3110
3111 if ($config_options->{filesys} =~ m/zfs/) {
3112 my ($devlist) = eval { get_zfs_raid_setup() };
3113 if (my $err = $@) {
3114 display_message("Warning: $err\nPlease fix ZFS setup first.");
3115 return;
3116 }
3117 $config_options->{target_hds} = [ map { $_->[1] } @$devlist ];
3118 } elsif ($config_options->{filesys} =~ m/btrfs/) {
3119 my ($devlist) = eval { get_btrfs_raid_setup() };
3120 if (my $err = $@) {
3121 display_message("Warning: $err\nPlease fix BTRFS setup first.");
3122 return;
3123 }
3124 $config_options->{target_hds} = [ map { $_->[1] } @$devlist ];
3125 } else {
3126 $config_options->{target_hds} = [ $target_hd ];
3127 }
3128
3129 $step_number++;
3130 create_country_view();
3131 });
3132 }
3133
3134 sub create_extract_view {
3135
3136 cleanup_view();
3137
3138 display_info();
3139
3140 $next->set_sensitive(0);
3141 $prev_btn->set_sensitive(0);
3142 $prev_btn->hide();
3143
3144 my $vbox = Gtk3::VBox->new(0, 0);
3145 $inbox->pack_start ($vbox, 1, 0, 0);
3146 my $hbox = Gtk3::HBox->new(0, 0);
3147 $vbox->pack_start ($hbox, 0, 0, 10);
3148
3149 my $vbox2 = Gtk3::VBox->new(0, 0);
3150 $hbox->pack_start ($vbox2, 0, 0, 0);
3151
3152 $progress_status = Gtk3::Label->new('');
3153 $vbox2->pack_start ($progress_status, 1, 1, 0);
3154
3155 $progress = Gtk3::ProgressBar->new;
3156 $progress->set_show_text(1);
3157 $progress->set_size_request (600, -1);
3158
3159 $vbox2->pack_start($progress, 0, 0, 0);
3160
3161 $inbox->show_all();
3162
3163 my $tdir = $opt_testmode ? "target" : "/target";
3164 mkdir $tdir;
3165 my $base = "${proxmox_cddir}/$setup->{product}-base.squashfs";
3166
3167 eval { extract_data($base, $tdir); };
3168 my $err = $@;
3169
3170 $next->set_sensitive(1);
3171
3172 set_next("_Reboot", sub { exit (0); } );
3173
3174 if ($err) {
3175 display_html("fail.htm");
3176 display_error($err);
3177 } else {
3178 cleanup_view();
3179 display_html("success.htm");
3180 }
3181 }
3182
3183 sub create_intro_view {
3184
3185 $prev_btn->set_sensitive(0);
3186
3187 cleanup_view();
3188
3189 if ($setup->{product} eq 'pve') {
3190 eval {
3191 my $cpuinfo = file_get_contents('/proc/cpuinfo');
3192 if ($cpuinfo && !($cpuinfo =~ /^flags\s*:.*(vmx|svm)/m)) {
3193 display_error("No support for KVM virtualisation detected.\n\n" .
3194 "Check BIOS settings for Intel VT / AMD-V / SVM.")
3195 }
3196 };
3197 }
3198
3199 display_html();
3200
3201 $step_number++;
3202 set_next("I a_gree", \&create_hdsel_view);
3203 }
3204
3205 $ipconf = get_ip_config();
3206
3207 $country = detect_country() if $ipconf->{default} || $opt_testmode;
3208
3209 # read country, kmap and timezone infos
3210 $cmap = read_cmap();
3211
3212 if (!defined($cmap->{country}->{$country})) {
3213 print $logfd "ignoring detected country '$country', invalid or unknown\n";
3214 $country = undef;
3215 }
3216
3217 create_main_window ();
3218
3219 my $initial_error = 0;
3220
3221 if (!defined ($hds) || (scalar (@$hds) <= 0)) {
3222 print "no hardisks found\n";
3223 $initial_error = 1;
3224 display_html("nohds.htm");
3225 set_next("Reboot", sub { exit(0); } );
3226 } else {
3227 foreach my $hd (@$hds) {
3228 my ($disk, $devname) = @$hd;
3229 next if $devname =~ m|^/dev/md\d+$|;
3230 print "found Disk$disk N:$devname\n";
3231 }
3232 }
3233
3234 if (!$initial_error && (scalar keys %{ $ipconf->{ifaces} } == 0)) {
3235 print "no network interfaces found\n";
3236 $initial_error = 1;
3237 display_html("nonics.htm");
3238 set_next("Reboot", sub { exit(0); } );
3239 }
3240
3241 create_intro_view () if !$initial_error;
3242
3243 Gtk3->main;
3244
3245 exit 0;