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