]> git.proxmox.com Git - pve-installer.git/blob - proxinstall
fixup comment indentation
[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 $ipconf->{selected} = $device_active_map->{$current->get_active()};
2030 my $iface = $ipconf->{ifaces}->{$ipconf->{selected}};
2031 $config->{mngmt_nic} = $iface->{name};
2032 $ipconf_entry_addr->set_text($iface->{inet}->{addr} || $iface->{inet6}->{addr})
2033 if $iface->{inet}->{addr} || $iface->{inet6}->{addr};
2034 $ipconf_entry_mask->set_text($iface->{inet}->{mask} || $iface->{inet6}->{mask})
2035 if $iface->{inet}->{mask} || $iface->{inet6}->{mask};
2036 };
2037
2038 my $i = 0;
2039 foreach my $index (sort keys %{$ipconf->{ifaces}}) {
2040 $device_cb->append_text(&$get_device_desc($ipconf->{ifaces}->{$index}));
2041 $device_active_map->{$i} = $index;
2042 $device_active_reverse_map->{$ipconf->{ifaces}->{$index}->{name}} = $i;
2043 if ($ipconf_first_view && $index == $ipconf->{default}) {
2044 $device_cb->set_active($i);
2045 &$device_change_handler($device_cb);
2046 $ipconf_first_view = 0;
2047 }
2048 $device_cb->signal_connect('changed' => $device_change_handler);
2049 $i++;
2050 }
2051
2052 if (my $nic = $config->{mngmt_nic}) {
2053 $device_cb->set_active($device_active_reverse_map->{$nic} // 0);
2054 } else {
2055 $device_cb->set_active(0);
2056 }
2057
2058 my $devicebox = Gtk3::HBox->new(0, 0);
2059 my $label = Gtk3::Label->new("Management Interface:");
2060 $label->set_size_request(150, -1);
2061 $label->set_alignment(1, 0.5);
2062 $devicebox->pack_start($label, 0, 0, 10);
2063 $devicebox->pack_start($device_cb, 0, 0, 0);
2064
2065 $vbox2->pack_start($devicebox, 0, 0, 2);
2066
2067 my $hn = $config->{fqdn} // "$setup->{product}." . ($ipconf->{domain} // "example.invalid");
2068
2069 my ($hostbox, $hostentry) =
2070 create_text_input($hn, 'Hostname (FQDN):');
2071 $vbox2->pack_start($hostbox, 0, 0, 2);
2072
2073 $vbox2->pack_start($ipbox, 0, 0, 2);
2074
2075 $vbox2->pack_start($maskbox, 0, 0, 2);
2076
2077 $gateway = $config->{gateway} // $ipconf->{gateway} || '192.168.100.1';
2078
2079 my $gwbox;
2080 ($gwbox, $ipconf_entry_gw) =
2081 create_text_input($gateway, 'Gateway:');
2082
2083 $vbox2->pack_start($gwbox, 0, 0, 2);
2084
2085 $dnsserver = $config->{dnsserver} // $ipconf->{dnsserver} || $gateway;
2086
2087 my $dnsbox;
2088 ($dnsbox, $ipconf_entry_dns) =
2089 create_text_input($dnsserver, 'DNS Server:');
2090
2091 $vbox2->pack_start($dnsbox, 0, 0, 0);
2092
2093 $inbox->show_all;
2094 set_next(undef, sub {
2095
2096 # verify hostname
2097
2098 my $text = $hostentry->get_text();
2099
2100 $text =~ s/^\s+//;
2101 $text =~ s/\s+$//;
2102
2103 $config->{fqdn} = $text;
2104
2105 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
2106
2107 # Debian does not support purely numeric hostnames
2108 if ($text && $text =~ /^[0-9]+(?:\.|$)/) {
2109 display_message("Purely numeric hostnames are not allowed.");
2110 $hostentry->grab_focus();
2111 return;
2112 }
2113
2114 if ($text && $text =~ m/^(${namere}\.)*${namere}$/ && $text !~ m/.example.invalid$/ &&
2115 $text =~ m/^([^\.]+)\.(\S+)$/) {
2116 $hostname = $1;
2117 $domain = $2;
2118 } else {
2119 display_message("Hostname does not look like a fully qualified domain name.");
2120 $hostentry->grab_focus();
2121 return;
2122 }
2123
2124 # verify ip address
2125
2126 $text = $ipconf_entry_addr->get_text();
2127 $text =~ s/^\s+//;
2128 $text =~ s/\s+$//;
2129 if ($text =~ m!^($IPV4RE)$!) {
2130 $ipaddress = $text;
2131 $ipversion = 4;
2132 } elsif ($text =~ m!^($IPV6RE)$!) {
2133 $ipaddress = $text;
2134 $ipversion = 6;
2135 } else {
2136 display_message("IP address is not valid.");
2137 $ipconf_entry_addr->grab_focus();
2138 return;
2139 }
2140 $config->{ipaddress} = $ipaddress;
2141
2142 $text = $ipconf_entry_mask->get_text();
2143 $text =~ s/^\s+//;
2144 $text =~ s/\s+$//;
2145 if (($ipversion == 6) && ($text =~ m/^(\d+)$/) && ($1 >= 8) && ($1 <= 126)) {
2146 $netmask = $text;
2147 } elsif (($ipversion == 4) && defined($ipv4_mask_hash->{$text})) {
2148 $netmask = $text;
2149 } else {
2150 display_message("Netmask is not valid.");
2151 $ipconf_entry_mask->grab_focus();
2152 return;
2153 }
2154 $config->{netmask} = $netmask;
2155
2156 $text = $ipconf_entry_gw->get_text();
2157 $text =~ s/^\s+//;
2158 $text =~ s/\s+$//;
2159 if (($ipversion == 4) && ($text =~ m!^($IPV4RE)$!)) {
2160 $gateway = $text;
2161 } elsif (($ipversion == 6) && ($text =~ m!^($IPV6RE)$!)) {
2162 $gateway = $text;
2163 } else {
2164 display_message("Gateway is not valid.");
2165 $ipconf_entry_gw->grab_focus();
2166 return;
2167 }
2168 $config->{gateway} = $gateway;
2169
2170 $text = $ipconf_entry_dns->get_text();
2171 $text =~ s/^\s+//;
2172 $text =~ s/\s+$//;
2173 if (($ipversion == 4) && ($text =~ m!^($IPV4RE)$!)) {
2174 $dnsserver = $text;
2175 } elsif (($ipversion == 6) && ($text =~ m!^($IPV6RE)$!)) {
2176 $dnsserver = $text;
2177 } else {
2178 display_message("DNS server is not valid.");
2179 $ipconf_entry_dns->grab_focus();
2180 return;
2181 }
2182 $config->{dnsserver} = $dnsserver;
2183
2184 #print "TEST $ipaddress $netmask $gateway $dnsserver\n";
2185
2186 $step_number++;
2187 create_ack_view();
2188 });
2189
2190 $hostentry->grab_focus();
2191 }
2192
2193 sub create_ack_view {
2194
2195 cleanup_view();
2196
2197 my $ack_template = "${proxmox_libdir}/html/ack_template.htm";
2198 my $ack_html = "${proxmox_libdir}/html/$steps[$step_number]->{html}";
2199 my $html_data = file_get_contents($ack_template);
2200
2201 my %config_values = (
2202 __target_hd__ => join(' | ', @{$config_options->{target_hds}}),
2203 __target_fs__ => $config_options->{filesys},
2204 __country__ => $cmap->{country}->{$country}->{name},
2205 __timezone__ => $timezone,
2206 __keymap__ => $keymap,
2207 __mailto__ => $mailto,
2208 __interface__ => $ipconf->{ifaces}->{$ipconf->{selected}}->{name},
2209 __hostname__ => $hostname,
2210 __ip__ => $ipaddress,
2211 __netmask__ => $netmask,
2212 __gateway__ => $gateway,
2213 __dnsserver__ => $dnsserver,
2214 );
2215
2216 while ( my ($k, $v) = each %config_values) {
2217 $html_data =~ s/$k/$v/g;
2218 }
2219
2220 write_config($html_data, $ack_html);
2221
2222 display_html();
2223
2224 set_next(undef, sub {
2225 $step_number++;
2226 create_extract_view();
2227 });
2228 }
2229
2230 sub get_device_desc {
2231 my ($devname, $size, $model) = @_;
2232
2233 if ($size && ($size > 0)) {
2234 $size = int($size/2048); # size in MB, from 512B "sectors"
2235
2236 my $text = "$devname (";
2237 if ($size >= 1024) {
2238 $size = int($size/1024); # size in GB
2239 $text .= "${size}GB";
2240 } else {
2241 $text .= "${size}MB";
2242 }
2243
2244 $text .= ", $model" if $model;
2245 $text .= ")";
2246
2247 } else {
2248 return $devname;
2249 }
2250 }
2251
2252 sub update_layout {
2253 my ($cb, $kmap) = @_;
2254
2255 my $ind;
2256 my $def;
2257 my $i = 0;
2258 my $kmaphash = $cmap->{kmaphash};
2259 foreach my $layout (sort keys %$kmaphash) {
2260 $def = $i if $kmaphash->{$layout} eq 'en-us';
2261 $ind = $i if $kmap && $kmaphash->{$layout} eq $kmap;
2262 $i++;
2263 }
2264
2265 $cb->set_active($ind || $def || 0);
2266 }
2267
2268 my $lastzonecb;
2269 sub update_zonelist {
2270 my ($box, $cc) = @_;
2271
2272 my $cczones = $cmap->{cczones};
2273 my $zones = $cmap->{zones};
2274
2275 my $sel;
2276 if ($lastzonecb) {
2277 $sel = $lastzonecb->get_active_text();
2278 $box->remove ($lastzonecb);
2279 } else {
2280 $sel = $timezone; # used once to select default
2281 }
2282
2283 my $cb = $lastzonecb = Gtk3::ComboBoxText->new();
2284 $cb->set_size_request(200, -1);
2285
2286 $cb->signal_connect('changed' => sub {
2287 $timezone = $cb->get_active_text();
2288 });
2289
2290 my @za;
2291 if ($cc && defined ($cczones->{$cc})) {
2292 @za = keys %{$cczones->{$cc}};
2293 } else {
2294 @za = keys %$zones;
2295 }
2296 my $ind;
2297 my $i = 0;
2298 foreach my $zone (sort @za) {
2299 $ind = $i if $sel && $zone eq $sel;
2300 $cb->append_text($zone);
2301 $i++;
2302 }
2303
2304 $cb->set_active($ind || 0);
2305
2306 $cb->show;
2307 $box->pack_start($cb, 0, 0, 0);
2308 }
2309
2310 sub create_password_view {
2311
2312 cleanup_view();
2313
2314 my $vbox2 = Gtk3::VBox->new(0, 0);
2315 $inbox->pack_start($vbox2, 1, 0, 0);
2316 my $vbox = Gtk3::VBox->new(0, 0);
2317 $vbox2->pack_start($vbox, 0, 0, 10);
2318
2319 my $hbox1 = Gtk3::HBox->new(0, 0);
2320 my $label = Gtk3::Label->new("Password");
2321 $label->set_size_request(150, -1);
2322 $label->set_alignment(1, 0.5);
2323 $hbox1->pack_start($label, 0, 0, 10);
2324 my $pwe1 = Gtk3::Entry->new();
2325 $pwe1->set_visibility(0);
2326 $pwe1->set_text($password) if $password;
2327 $pwe1->set_size_request(200, -1);
2328 $hbox1->pack_start($pwe1, 0, 0, 0);
2329
2330 my $hbox2 = Gtk3::HBox->new(0, 0);
2331 $label = Gtk3::Label->new("Confirm");
2332 $label->set_size_request(150, -1);
2333 $label->set_alignment(1, 0.5);
2334 $hbox2->pack_start($label, 0, 0, 10);
2335 my $pwe2 = Gtk3::Entry->new();
2336 $pwe2->set_visibility(0);
2337 $pwe2->set_text($password) if $password;
2338 $pwe2->set_size_request(200, -1);
2339 $hbox2->pack_start($pwe2, 0, 0, 0);
2340
2341 my $hbox3 = Gtk3::HBox->new(0, 0);
2342 $label = Gtk3::Label->new("E-Mail");
2343 $label->set_size_request(150, -1);
2344 $label->set_alignment(1, 0.5);
2345 $hbox3->pack_start($label, 0, 0, 10);
2346 my $eme = Gtk3::Entry->new();
2347 $eme->set_size_request(200, -1);
2348 $eme->set_text($mailto);
2349 $hbox3->pack_start($eme, 0, 0, 0);
2350
2351
2352 $vbox->pack_start($hbox1, 0, 0, 5);
2353 $vbox->pack_start($hbox2, 0, 0, 5);
2354 $vbox->pack_start($hbox3, 0, 0, 15);
2355
2356 $inbox->show_all;
2357
2358 display_html();
2359
2360 set_next (undef, sub {
2361
2362 my $t1 = $pwe1->get_text;
2363 my $t2 = $pwe2->get_text;
2364
2365 if (length ($t1) < 5) {
2366 display_message("Password is too short.");
2367 $pwe1->grab_focus();
2368 return;
2369 }
2370
2371 if ($t1 ne $t2) {
2372 display_message("Password does not match.");
2373 $pwe1->grab_focus();
2374 return;
2375 }
2376
2377 my $t3 = $eme->get_text;
2378 if ($t3 !~ m/^\S+\@\S+\.\S+$/) {
2379 display_message("E-Mail does not look like a valid address" .
2380 " (user\@domain.tld)");
2381 $eme->grab_focus();
2382 return;
2383 }
2384
2385 if ($t3 eq 'mail@example.invalid') {
2386 display_message("Please enter a valid E-Mail address");
2387 $eme->grab_focus();
2388 return;
2389 }
2390
2391 $password = $t1;
2392 $mailto = $t3;
2393
2394 $step_number++;
2395 create_ipconf_view();
2396 });
2397
2398 $pwe1->grab_focus();
2399
2400 }
2401
2402 sub create_country_view {
2403
2404 cleanup_view();
2405
2406 my $countryhash = $cmap->{countryhash};
2407 my $ctr = $cmap->{country};
2408
2409 my $vbox2 = Gtk3::VBox->new(0, 0);
2410 $inbox->pack_start($vbox2, 1, 0, 0);
2411 my $vbox = Gtk3::VBox->new(0, 0);
2412 $vbox2->pack_start($vbox, 0, 0, 10);
2413
2414 my $w = Gtk3::Entry->new();
2415 $w->set_size_request(200, -1);
2416
2417 my $c = Gtk3::EntryCompletion->new();
2418 $c->set_text_column(0);
2419 $c->set_minimum_key_length(0);
2420 $c->set_popup_set_width(1);
2421 $c->set_inline_completion(1);
2422
2423 my $hbox2 = Gtk3::HBox->new(0, 0);
2424 my $label = Gtk3::Label->new("Time zone");
2425 $label->set_size_request(150, -1);
2426 $label->set_alignment(1, 0.5);
2427 $hbox2->pack_start($label, 0, 0, 10);
2428 update_zonelist ($hbox2);
2429
2430 my $hbox3 = Gtk3::HBox->new(0, 0);
2431 $label = Gtk3::Label->new("Keyboard Layout");
2432 $label->set_size_request(150, -1);
2433 $label->set_alignment(1, 0.5);
2434 $hbox3->pack_start($label, 0, 0, 10);
2435
2436 my $kmapcb = Gtk3::ComboBoxText->new();
2437 $kmapcb->set_size_request (200, -1);
2438 foreach my $layout (sort keys %{$cmap->{kmaphash}}) {
2439 $kmapcb->append_text ($layout);
2440 }
2441
2442 update_layout($kmapcb);
2443 $hbox3->pack_start ($kmapcb, 0, 0, 0);
2444
2445 $kmapcb->signal_connect ('changed' => sub {
2446 my $sel = $kmapcb->get_active_text();
2447 if (my $kmap = $cmap->{kmaphash}->{$sel}) {
2448 my $xkmap = $cmap->{kmap}->{$kmap}->{x11};
2449 my $xvar = $cmap->{kmap}->{$kmap}->{x11var};
2450 syscmd ("setxkbmap $xkmap $xvar") if !$opt_testmode;
2451 $keymap = $kmap;
2452 }
2453 });
2454
2455 $w->signal_connect ('changed' => sub {
2456 my ($entry, $event) = @_;
2457 my $text = $entry->get_text;
2458
2459 if (my $cc = $countryhash->{lc($text)}) {
2460 update_zonelist($hbox2, $cc);
2461 my $kmap = $ctr->{$cc}->{kmap} || 'en-us';
2462 update_layout($kmapcb, $kmap);
2463 }
2464 });
2465
2466 $w->signal_connect (key_press_event => sub {
2467 my ($entry, $event) = @_;
2468 my $text = $entry->get_text;
2469
2470 my $val = $event->get_keyval;
2471
2472 if ($val == Gtk3::Gdk::KEY_Tab) {
2473 my $cc = $countryhash->{lc($text)};
2474
2475 my $found = 0;
2476 my $compl;
2477
2478 if ($cc) {
2479 $found = 1;
2480 $compl = $ctr->{$cc}->{name};
2481 } else {
2482 foreach my $cc (keys %$ctr) {
2483 my $ct = $ctr->{$cc}->{name};
2484 if ($ct =~ m/^\Q$text\E.*$/i) {
2485 $found++;
2486 $compl = $ct;
2487 }
2488 last if $found > 1;
2489 }
2490 }
2491
2492 if ($found == 1) {
2493 $entry->set_text($compl);
2494 $c->complete();
2495 return undef;
2496 } else {
2497 #Gtk3::Gdk::beep();
2498 print chr(7); # beep ?
2499 }
2500
2501 $c->complete();
2502
2503 my $buf = $w->get_buffer();
2504 $buf->insert_text(-1, '', -1); # popup selection
2505
2506 return 1;
2507 }
2508
2509 return undef;
2510 });
2511
2512 my $ls = Gtk3::ListStore->new('Glib::String');
2513 foreach my $cc (sort {$ctr->{$a}->{name} cmp $ctr->{$b}->{name} } keys %$ctr) {
2514 my $iter = $ls->append();
2515 $ls->set ($iter, 0, $ctr->{$cc}->{name});
2516 }
2517 $c->set_model ($ls);
2518
2519 $w->set_completion ($c);
2520
2521 my $hbox = Gtk3::HBox->new(0, 0);
2522
2523 $label = Gtk3::Label->new("Country");
2524 $label->set_alignment(1, 0.5);
2525 $label->set_size_request(150, -1);
2526 $hbox->pack_start($label, 0, 0, 10);
2527 $hbox->pack_start($w, 0, 0, 0);
2528
2529 $vbox->pack_start($hbox, 0, 0, 5);
2530 $vbox->pack_start($hbox2, 0, 0, 5);
2531 $vbox->pack_start($hbox3, 0, 0, 5);
2532
2533 if ($country && $ctr->{$country}) {
2534 $w->set_text ($ctr->{$country}->{name});
2535 }
2536
2537 $inbox->show_all;
2538
2539 display_html();
2540 set_next (undef, sub {
2541
2542 my $text = $w->get_text;
2543
2544 if (my $cc = $countryhash->{lc($text)}) {
2545 $country = $cc;
2546 $step_number++;
2547 create_password_view();
2548 return;
2549 } else {
2550 display_message("Please select a country first.");
2551 $w->grab_focus();
2552 }
2553 });
2554
2555 $w->grab_focus();
2556 }
2557
2558 my $target_hd_combo;
2559 my $target_hd_label;
2560
2561 my $hdoption_first_setup = 1;
2562
2563 my $create_basic_grid = sub {
2564 my $grid = Gtk3::Grid->new();
2565 $grid->set_visible(1);
2566 $grid->set_column_spacing(10);
2567 $grid->set_row_spacing(10);
2568 $grid->set_hexpand(1);
2569
2570 $grid->set_margin_start(5);
2571 $grid->set_margin_end(5);
2572 $grid->set_margin_top(5);
2573 $grid->set_margin_bottom(5);
2574
2575 return $grid;
2576 };
2577
2578 my $create_label_widget_grid = sub {
2579 my ($labeled_widgets) = @_;
2580
2581 my $grid = &$create_basic_grid();
2582 my $row = 0;
2583
2584 for (my $i = 0; $i < @$labeled_widgets; $i += 2) {
2585 my $widget = @$labeled_widgets[$i+1];
2586 my $label = Gtk3::Label->new(@$labeled_widgets[$i]);
2587 $label->set_visible(1);
2588 $label->set_alignment (1, 0.5);
2589 $grid->attach($label, 0, $row, 1, 1);
2590 $widget->set_visible(1);
2591 $grid->attach($widget, 1, $row, 1, 1);
2592 $row++;
2593 }
2594
2595 return $grid;
2596 };
2597
2598 my $create_raid_disk_grid = sub {
2599 my $disk_labeled_widgets = [];
2600 for (my $i = 0; $i < @$hds; $i++) {
2601 my $disk_selector = Gtk3::ComboBoxText->new();
2602 $disk_selector->append_text("-- do not use --");
2603 $disk_selector->set_active(0);
2604 $disk_selector->set_visible(1);
2605 foreach my $hd (@$hds) {
2606 my ($disk, $devname, $size, $model) = @$hd;
2607 $disk_selector->append_text(get_device_desc ($devname, $size, $model));
2608 $disk_selector->{pve_disk_id} = $i;
2609 $disk_selector->signal_connect (changed => sub {
2610 my $w = shift;
2611 my $diskid = $w->{pve_disk_id};
2612 my $a = $w->get_active - 1;
2613 $config_options->{"disksel${diskid}"} = ($a >= 0) ? $hds->[$a] : undef;
2614 });
2615 }
2616
2617 if ($hdoption_first_setup) {
2618 $disk_selector->set_active ($i+1) if $hds->[$i];
2619 } else {
2620 my $hdind = 0;
2621 if (my $cur_hd = $config_options->{"disksel$i"}) {
2622 foreach my $hd (@$hds) {
2623 if (@$hd[1] eq @$cur_hd[1]) {
2624 $disk_selector->set_active($hdind+1);
2625 last;
2626 }
2627 $hdind++;
2628 }
2629 }
2630 }
2631
2632 push @$disk_labeled_widgets, "Harddisk $i", $disk_selector;
2633 }
2634
2635 my $scrolled_window = Gtk3::ScrolledWindow->new();
2636 $scrolled_window->set_hexpand(1);
2637 $scrolled_window->set_propagate_natural_height(1) if @$hds > 4;
2638 $scrolled_window->add(&$create_label_widget_grid($disk_labeled_widgets));
2639 $scrolled_window->set_policy('never', 'automatic');
2640
2641 return $scrolled_window;
2642 # &$create_label_widget_grid($disk_labeled_widgets)
2643 };
2644
2645 # shared between different ui parts (e.g., ZFS and "normal" single disk FS)
2646 my $hdsize_size_adj;
2647 my $hdsize_entry_buffer;
2648
2649 my $get_hdsize_spinbtn = sub {
2650 my $hdsize = shift;
2651
2652 $hdsize_entry_buffer //= Gtk3::EntryBuffer->new(undef, 1);
2653
2654 if (defined($hdsize)) {
2655 $hdsize_size_adj = Gtk3::Adjustment->new($config_options->{hdsize} || $hdsize, 0, $hdsize+1, 1, 1, 1);
2656 } else {
2657 die "called get_hdsize_spinbtn with \$hdsize_size_adj not defined but did not pass hdsize!\n"
2658 if !defined($hdsize_size_adj);
2659 }
2660
2661 my $spinbutton_hdsize = Gtk3::SpinButton->new($hdsize_size_adj, 1, 1);
2662 $spinbutton_hdsize->set_buffer($hdsize_entry_buffer);
2663 $spinbutton_hdsize->set_adjustment($hdsize_size_adj);
2664 $spinbutton_hdsize->set_tooltip_text("only use specified size (GB) of the harddisk (rest left unpartitioned)");
2665 return $spinbutton_hdsize;
2666 };
2667
2668 my $create_raid_advanced_grid = sub {
2669 my $labeled_widgets = [];
2670 my $spinbutton_ashift = Gtk3::SpinButton->new_with_range(9,13,1);
2671 $spinbutton_ashift->set_tooltip_text("zpool ashift property (pool sector size, default 2^12)");
2672 $spinbutton_ashift->signal_connect ("value-changed" => sub {
2673 my $w = shift;
2674 $config_options->{ashift} = $w->get_value_as_int();
2675 });
2676 $config_options->{ashift} = 12 if ! defined($config_options->{ashift});
2677 $spinbutton_ashift->set_value($config_options->{ashift});
2678 push @$labeled_widgets, "ashift";
2679 push @$labeled_widgets, $spinbutton_ashift;
2680
2681 my $combo_compress = Gtk3::ComboBoxText->new();
2682 $combo_compress->set_tooltip_text("zfs compression algorithm for rpool dataset");
2683 # note: gzip / lze not allowed for bootfs vdevs
2684 my $comp_opts = ["on","off","lzjb","lz4"];
2685 foreach my $opt (@$comp_opts) {
2686 $combo_compress->append($opt, $opt);
2687 }
2688 $config_options->{compress} = "on" if !defined($config_options->{compress});
2689 $combo_compress->set_active_id($config_options->{compress});
2690 $combo_compress->signal_connect (changed => sub {
2691 my $w = shift;
2692 $config_options->{compress} = $w->get_active_text();
2693 });
2694 push @$labeled_widgets, "compress";
2695 push @$labeled_widgets, $combo_compress;
2696
2697 my $combo_checksum = Gtk3::ComboBoxText->new();
2698 $combo_checksum->set_tooltip_text("zfs checksum algorithm for rpool dataset");
2699 my $csum_opts = ["on", "off","fletcher2", "fletcher4", "sha256"];
2700 foreach my $opt (@$csum_opts) {
2701 $combo_checksum->append($opt, $opt);
2702 }
2703 $config_options->{checksum} = "on" if !($config_options->{checksum});
2704 $combo_checksum->set_active_id($config_options->{checksum});
2705 $combo_checksum->signal_connect (changed => sub {
2706 my $w = shift;
2707 $config_options->{checksum} = $w->get_active_text();
2708 });
2709 push @$labeled_widgets, "checksum";
2710 push @$labeled_widgets, $combo_checksum;
2711
2712 my $spinbutton_copies = Gtk3::SpinButton->new_with_range(1,3,1);
2713 $spinbutton_copies->set_tooltip_text("zfs copies property for rpool dataset (in addition to RAID redundancy!)");
2714 $spinbutton_copies->signal_connect ("value-changed" => sub {
2715 my $w = shift;
2716 $config_options->{copies} = $w->get_value_as_int();
2717 });
2718 $config_options->{copies} = 1 if !defined($config_options->{copies});
2719 $spinbutton_copies->set_value($config_options->{copies});
2720 push @$labeled_widgets, "copies", $spinbutton_copies;
2721
2722 push @$labeled_widgets, "hdsize", $get_hdsize_spinbtn->();
2723 return &$create_label_widget_grid($labeled_widgets);;
2724 };
2725
2726 sub create_hdoption_view {
2727
2728 my $dialog = Gtk3::Dialog->new();
2729
2730 $dialog->set_title("Harddisk options");
2731
2732 $dialog->add_button("_OK", 1);
2733
2734 my $contarea = $dialog->get_content_area();
2735
2736 my $hbox2 = Gtk3::Box->new('horizontal', 0);
2737 $contarea->pack_start($hbox2, 1, 1, 10);
2738
2739 my $grid = Gtk3::Grid->new();
2740 $grid->set_column_spacing(10);
2741 $grid->set_row_spacing(10);
2742
2743 $hbox2->pack_start($grid, 1, 0, 10);
2744
2745 my $row = 0;
2746
2747 # Filesystem type
2748
2749 my $label0 = Gtk3::Label->new("Filesystem");
2750 $label0->set_alignment (1, 0.5);
2751 $grid->attach($label0, 0, $row, 1, 1);
2752
2753 my $fstypecb = Gtk3::ComboBoxText->new();
2754
2755 my $fstype = ['ext3', 'ext4', 'xfs',
2756 'zfs (RAID0)', 'zfs (RAID1)',
2757 'zfs (RAID10)', 'zfs (RAIDZ-1)',
2758 'zfs (RAIDZ-2)', 'zfs (RAIDZ-3)'];
2759
2760 push @$fstype, 'btrfs (RAID0)', 'btrfs (RAID1)', 'btrfs (RAID10)'
2761 if $setup->{enable_btrfs};
2762
2763 my $tcount = 0;
2764 foreach my $tmp (@$fstype) {
2765 $fstypecb->append_text($tmp);
2766 $fstypecb->set_active ($tcount)
2767 if $config_options->{filesys} eq $tmp;
2768 $tcount++;
2769 }
2770
2771 $grid->attach($fstypecb, 1, $row, 1, 1);
2772
2773 $hbox2->show_all();
2774
2775 $row++;
2776
2777 my $sep = Gtk3::HSeparator->new();
2778 $sep->set_visible(1);
2779 $grid->attach($sep, 0, $row, 2, 1);
2780 $row++;
2781
2782 my $hdsize_labeled_widgets = [];
2783
2784 # size compute
2785 my $hdsize = 0;
2786 if ( -b $target_hd) {
2787 $hdsize = int(hd_size ($target_hd) / (1024*1024.0)); # size in GB
2788 } elsif ($target_hd) {
2789 $hdsize = int((-s $target_hd) / (1024*1024*1024.0));
2790 }
2791
2792 my $spinbutton_hdsize = $get_hdsize_spinbtn->($hdsize);
2793 push @$hdsize_labeled_widgets, "hdsize", $spinbutton_hdsize;
2794
2795 my $entry_swapsize = Gtk3::Entry->new();
2796 $entry_swapsize->set_tooltip_text("maximum SWAP size (GB)");
2797 $entry_swapsize->signal_connect (key_press_event => \&check_float);
2798 $entry_swapsize->set_text($config_options->{swapsize}) if defined($config_options->{swapsize});
2799 push @$hdsize_labeled_widgets, "swapsize", $entry_swapsize;
2800
2801 my $entry_maxroot = Gtk3::Entry->new();
2802 if ($setup->{product} eq 'pve') {
2803 $entry_maxroot->set_tooltip_text("maximum size (GB) for LVM root volume");
2804 $entry_maxroot->signal_connect (key_press_event => \&check_float);
2805 $entry_maxroot->set_text($config_options->{maxroot}) if $config_options->{maxroot};
2806 push @$hdsize_labeled_widgets, "maxroot", $entry_maxroot;
2807 }
2808
2809 my $entry_minfree = Gtk3::Entry->new();
2810 $entry_minfree->set_tooltip_text("minimum free LVM space (GB, required for LVM snapshots)");
2811 $entry_minfree->signal_connect (key_press_event => \&check_float);
2812 $entry_minfree->set_text($config_options->{minfree}) if defined($config_options->{minfree});
2813 push @$hdsize_labeled_widgets, "minfree", $entry_minfree;
2814
2815 my $entry_maxvz;
2816 if ($setup->{product} eq 'pve') {
2817 $entry_maxvz = Gtk3::Entry->new();
2818 $entry_maxvz->set_tooltip_text("maximum size (GB) for LVM data volume");
2819 $entry_maxvz->signal_connect (key_press_event => \&check_float);
2820 $entry_maxvz->set_text($config_options->{maxvz}) if defined($config_options->{maxvz});
2821 push @$hdsize_labeled_widgets, "maxvz", $entry_maxvz;
2822 }
2823
2824 my $options_stack = Gtk3::Stack->new();
2825 $options_stack->set_visible(1);
2826 $options_stack->set_hexpand(1);
2827 $options_stack->set_vexpand(1);
2828 $options_stack->add_titled(&$create_raid_disk_grid(), "raiddisk", "Disk Setup");
2829 $options_stack->add_titled(&$create_label_widget_grid($hdsize_labeled_widgets), "hdsize", "Size Options");
2830 $options_stack->add_titled(&$create_raid_advanced_grid("zfs"), "raidzfsadvanced", "Advanced Options");
2831 $options_stack->set_visible_child_name("raiddisk");
2832 my $options_stack_switcher = Gtk3::StackSwitcher->new();
2833 $options_stack_switcher->set_halign('center');
2834 $options_stack_switcher->set_stack($options_stack);
2835 $grid->attach($options_stack_switcher, 0, $row, 2, 1);
2836 $row++;
2837 $grid->attach($options_stack, 0, $row, 2, 1);
2838 $row++;
2839
2840 $hdoption_first_setup = 0;
2841
2842 my $switch_view = sub {
2843 my $raid = $config_options->{filesys} =~ m/zfs|btrfs/;
2844 my $enable_zfs_opts = $config_options->{filesys} =~ m/zfs/;
2845
2846 $target_hd_combo->set_visible(!$raid);
2847 $options_stack->get_child_by_name("hdsize")->set_visible(!$raid);
2848 $options_stack->get_child_by_name("raiddisk")->set_visible($raid);
2849 $options_stack_switcher->set_visible($enable_zfs_opts);
2850 $options_stack->get_child_by_name("raidzfsadvanced")->set_visible($enable_zfs_opts);
2851 if ($raid) {
2852 $target_hd_label->set_text("Target: $config_options->{filesys} ");
2853 $options_stack->set_visible_child_name("raiddisk");
2854 } else {
2855 $target_hd_label->set_text("Target Harddisk: ");
2856 }
2857 my (undef, $pref_width) = $dialog->get_preferred_width();
2858 my (undef, $pref_height) = $dialog->get_preferred_height();
2859 $pref_height = 750 if $pref_height > 750;
2860 $dialog->resize($pref_width, $pref_height);
2861 };
2862
2863 &$switch_view();
2864
2865 $fstypecb->signal_connect (changed => sub {
2866 $config_options->{filesys} = $fstypecb->get_active_text();
2867 &$switch_view();
2868 });
2869
2870 $dialog->show();
2871
2872 $dialog->run();
2873
2874 my $get_float = sub {
2875 my ($entry) = @_;
2876
2877 my $text = $entry->get_text();
2878 return undef if !defined($text);
2879
2880 $text =~ s/^\s+//;
2881 $text =~ s/\s+$//;
2882
2883 return undef if $text !~ m/^\d+(\.\d+)?$/;
2884
2885 return $text;
2886 };
2887
2888 my $tmp;
2889
2890 if (($tmp = &$get_float($spinbutton_hdsize)) && ($tmp != $hdsize)) {
2891 $config_options->{hdsize} = $tmp;
2892 } else {
2893 delete $config_options->{hdsize};
2894 }
2895
2896 if (defined($tmp = &$get_float($entry_swapsize))) {
2897 $config_options->{swapsize} = $tmp;
2898 } else {
2899 delete $config_options->{swapsize};
2900 }
2901
2902 if (defined($tmp = &$get_float($entry_maxroot))) {
2903 $config_options->{maxroot} = $tmp;
2904 } else {
2905 delete $config_options->{maxroot};
2906 }
2907
2908 if (defined($tmp = &$get_float($entry_minfree))) {
2909 $config_options->{minfree} = $tmp;
2910 } else {
2911 delete $config_options->{minfree};
2912 }
2913
2914 if ($entry_maxvz && defined($tmp = &$get_float($entry_maxvz))) {
2915 $config_options->{maxvz} = $tmp;
2916 } else {
2917 delete $config_options->{maxvz};
2918 }
2919
2920 $dialog->destroy();
2921 }
2922
2923 my $get_raid_devlist = sub {
2924
2925 my $dev_name_hash = {};
2926
2927 my $devlist = [];
2928 for (my $i = 0; $i < @$hds; $i++) {
2929 if (my $hd = $config_options->{"disksel$i"}) {
2930 my ($disk, $devname, $size, $model) = @$hd;
2931 die "device '$devname' is used more than once\n"
2932 if $dev_name_hash->{$devname};
2933 $dev_name_hash->{$devname} = $hd;
2934 push @$devlist, $hd;
2935 }
2936 }
2937
2938 return $devlist;
2939 };
2940
2941 sub zfs_mirror_size_check {
2942 my ($expected, $actual) = @_;
2943
2944 die "mirrored disks must have same size\n"
2945 if abs($expected - $actual) > $expected / 10;
2946 }
2947
2948 sub get_zfs_raid_setup {
2949
2950 my $filesys = $config_options->{filesys};
2951
2952 my $devlist = &$get_raid_devlist();
2953
2954 my $diskcount = scalar(@$devlist);
2955 die "$filesys needs at least one device\n" if $diskcount < 1;
2956
2957 my $bootdevlist = [];
2958
2959 my $cmd= '';
2960 if ($filesys eq 'zfs (RAID0)') {
2961 push @$bootdevlist, @$devlist[0];
2962 foreach my $hd (@$devlist) {
2963 $cmd .= " @$hd[1]";
2964 }
2965 } elsif ($filesys eq 'zfs (RAID1)') {
2966 die "zfs (RAID1) needs at least 2 device\n" if $diskcount < 2;
2967 $cmd .= ' mirror ';
2968 my $hd = @$devlist[0];
2969 my $expected_size = @$hd[2]; # all disks need approximately same size
2970 foreach $hd (@$devlist) {
2971 zfs_mirror_size_check($expected_size, @$hd[2]);
2972 $cmd .= " @$hd[1]";
2973 push @$bootdevlist, $hd;
2974 }
2975 } elsif ($filesys eq 'zfs (RAID10)') {
2976 die "zfs (RAID10) needs at least 4 device\n" if $diskcount < 4;
2977 die "zfs (RAID10) needs an even number of devices\n" if $diskcount & 1;
2978
2979 push @$bootdevlist, @$devlist[0], @$devlist[1];
2980
2981 for (my $i = 0; $i < $diskcount; $i+=2) {
2982 my $hd1 = @$devlist[$i];
2983 my $hd2 = @$devlist[$i+1];
2984 zfs_mirror_size_check(@$hd1[2], @$hd2[2]); # pairs need approximately same size
2985 $cmd .= ' mirror ' . @$hd1[1] . ' ' . @$hd2[1];
2986 }
2987
2988 } elsif ($filesys =~ m/^zfs \(RAIDZ-([123])\)$/) {
2989 my $level = $1;
2990 my $mindisks = 2 + $level;
2991 die "zfs (RAIDZ-$level) needs at least $mindisks devices\n" if scalar(@$devlist) < $mindisks;
2992 my $hd = @$devlist[0];
2993 my $expected_size = @$hd[2]; # all disks need approximately same size
2994 $cmd .= " raidz$level";
2995 foreach $hd (@$devlist) {
2996 zfs_mirror_size_check($expected_size, @$hd[2]);
2997 $cmd .= " @$hd[1]";
2998 push @$bootdevlist, $hd;
2999 }
3000 } else {
3001 die "unknown zfs mode '$filesys'\n";
3002 }
3003
3004 return ($devlist, $bootdevlist, $cmd);
3005 }
3006
3007 sub get_btrfs_raid_setup {
3008
3009 my $filesys = $config_options->{filesys};
3010
3011 my $devlist = &$get_raid_devlist();
3012
3013 my $diskcount = scalar(@$devlist);
3014 die "$filesys needs at least one device\n" if $diskcount < 1;
3015
3016 my $mode;
3017
3018 if ($diskcount == 1) {
3019 $mode = 'single';
3020 } else {
3021 if ($filesys eq 'btrfs (RAID0)') {
3022 $mode = 'raid0';
3023 } elsif ($filesys eq 'btrfs (RAID1)') {
3024 die "btrfs (RAID1) needs at least 2 device\n" if $diskcount < 2;
3025 $mode = 'raid1';
3026 } elsif ($filesys eq 'btrfs (RAID10)') {
3027 die "btrfs (RAID10) needs at least 4 device\n" if $diskcount < 4;
3028 $mode = 'raid10';
3029 } else {
3030 die "unknown btrfs mode '$filesys'\n";
3031 }
3032 }
3033
3034 return ($devlist, $mode);
3035 }
3036
3037 my $last_hd_selected = 0;
3038 sub create_hdsel_view {
3039
3040 $prev_btn->set_sensitive(1); # enable previous button at this point
3041
3042 cleanup_view();
3043
3044 my $vbox = Gtk3::VBox->new(0, 0);
3045 $inbox->pack_start($vbox, 1, 0, 0);
3046 my $hbox = Gtk3::HBox->new(0, 0);
3047 $vbox->pack_start($hbox, 0, 0, 10);
3048
3049 my ($disk, $devname, $size, $model) = @{@$hds[0]};
3050 $target_hd = $devname if !defined($target_hd);
3051
3052 $target_hd_label = Gtk3::Label->new("Target Harddisk: ");
3053 $hbox->pack_start($target_hd_label, 0, 0, 0);
3054
3055 $target_hd_combo = Gtk3::ComboBoxText->new();
3056
3057 foreach my $hd (@$hds) {
3058 ($disk, $devname, $size, $model) = @$hd;
3059 $target_hd_combo->append_text (get_device_desc($devname, $size, $model));
3060 }
3061
3062 my $raid = $config_options->{filesys} =~ m/zfs|btrfs/;
3063 if ($raid) {
3064 $target_hd_label->set_text("Target: $config_options->{filesys} ");
3065 $target_hd_combo->set_visible(0);
3066 $target_hd_combo->set_no_show_all(1);
3067 }
3068 $target_hd_combo->set_active($last_hd_selected);
3069 $target_hd_combo->signal_connect(changed => sub {
3070 $a = shift->get_active;
3071 my ($disk, $devname) = @{@$hds[$a]};
3072 $last_hd_selected = $a;
3073 $target_hd = $devname;
3074 });
3075
3076 $hbox->pack_start($target_hd_combo, 0, 0, 10);
3077
3078 my $options = Gtk3::Button->new('_Options');
3079 $options->signal_connect (clicked => \&create_hdoption_view);
3080 $hbox->pack_start ($options, 0, 0, 0);
3081
3082
3083 $inbox->show_all;
3084
3085 display_html();
3086
3087 set_next(undef, sub {
3088
3089 if ($config_options->{filesys} =~ m/zfs/) {
3090 my ($devlist) = eval { get_zfs_raid_setup() };
3091 if (my $err = $@) {
3092 display_message("Warning: $err\nPlease fix ZFS setup first.");
3093 return;
3094 }
3095 $config_options->{target_hds} = [ map { $_->[1] } @$devlist ];
3096 } elsif ($config_options->{filesys} =~ m/btrfs/) {
3097 my ($devlist) = eval { get_btrfs_raid_setup() };
3098 if (my $err = $@) {
3099 display_message("Warning: $err\nPlease fix BTRFS setup first.");
3100 return;
3101 }
3102 $config_options->{target_hds} = [ map { $_->[1] } @$devlist ];
3103 } else {
3104 $config_options->{target_hds} = [ $target_hd ];
3105 }
3106
3107 $step_number++;
3108 create_country_view();
3109 });
3110 }
3111
3112 sub create_extract_view {
3113
3114 cleanup_view();
3115
3116 display_info();
3117
3118 $next->set_sensitive(0);
3119 $prev_btn->set_sensitive(0);
3120 $prev_btn->hide();
3121
3122 my $vbox = Gtk3::VBox->new(0, 0);
3123 $inbox->pack_start ($vbox, 1, 0, 0);
3124 my $hbox = Gtk3::HBox->new(0, 0);
3125 $vbox->pack_start ($hbox, 0, 0, 10);
3126
3127 my $vbox2 = Gtk3::VBox->new(0, 0);
3128 $hbox->pack_start ($vbox2, 0, 0, 0);
3129
3130 $progress_status = Gtk3::Label->new('');
3131 $vbox2->pack_start ($progress_status, 1, 1, 0);
3132
3133 $progress = Gtk3::ProgressBar->new;
3134 $progress->set_show_text(1);
3135 $progress->set_size_request (600, -1);
3136
3137 $vbox2->pack_start($progress, 0, 0, 0);
3138
3139 $inbox->show_all();
3140
3141 my $tdir = $opt_testmode ? "target" : "/target";
3142 mkdir $tdir;
3143 my $base = "${proxmox_cddir}/$setup->{product}-base.squashfs";
3144
3145 eval { extract_data($base, $tdir); };
3146 my $err = $@;
3147
3148 $next->set_sensitive(1);
3149
3150 set_next("_Reboot", sub { exit (0); } );
3151
3152 if ($err) {
3153 display_html("fail.htm");
3154 display_error($err);
3155 } else {
3156 cleanup_view();
3157 display_html("success.htm");
3158 }
3159 }
3160
3161 sub create_intro_view {
3162
3163 $prev_btn->set_sensitive(0);
3164
3165 cleanup_view();
3166
3167 if ($setup->{product} eq 'pve') {
3168 eval {
3169 my $cpuinfo = file_get_contents('/proc/cpuinfo');
3170 if ($cpuinfo && !($cpuinfo =~ /^flags\s*:.*(vmx|svm)/m)) {
3171 display_error("No support for KVM virtualisation detected.\n\n" .
3172 "Check BIOS settings for Intel VT / AMD-V / SVM.")
3173 }
3174 };
3175 }
3176
3177 display_html();
3178
3179 $step_number++;
3180 set_next("I a_gree", \&create_hdsel_view);
3181 }
3182
3183 $ipconf = get_ip_config();
3184
3185 $country = detect_country() if $ipconf->{default} || $opt_testmode;
3186
3187 # read country, kmap and timezone infos
3188 $cmap = read_cmap();
3189
3190 if (!defined($cmap->{country}->{$country})) {
3191 print $logfd "ignoring detected country '$country', invalid or unknown\n";
3192 $country = undef;
3193 }
3194
3195 create_main_window ();
3196
3197 my $initial_error = 0;
3198
3199 if (!defined ($hds) || (scalar (@$hds) <= 0)) {
3200 print "no hardisks found\n";
3201 $initial_error = 1;
3202 display_html("nohds.htm");
3203 set_next("Reboot", sub { exit(0); } );
3204 } else {
3205 foreach my $hd (@$hds) {
3206 my ($disk, $devname) = @$hd;
3207 next if $devname =~ m|^/dev/md\d+$|;
3208 print "found Disk$disk N:$devname\n";
3209 }
3210 }
3211
3212 if (!$initial_error && (scalar keys %{ $ipconf->{ifaces} } == 0)) {
3213 print "no network interfaces found\n";
3214 $initial_error = 1;
3215 display_html("nonics.htm");
3216 set_next("Reboot", sub { exit(0); } );
3217 }
3218
3219 create_intro_view () if !$initial_error;
3220
3221 Gtk3->main;
3222
3223 exit 0;