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