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