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