]> git.proxmox.com Git - pve-installer.git/blob - proxinstall
disk selector: shorten ZFS/HW-RAID note
[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(10);
2874 $grid->set_margin_end(20);
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 $clear_all_button = Gtk3::Button->new('_Deselect All');
2940 if ($hd_count > 3) {
2941 $clear_all_button->signal_connect('clicked', sub {
2942 my $is_widget = 0;
2943 for my $disk_selector (@$disk_labeled_widgets) {
2944 $disk_selector->set_active(0) if $is_widget;
2945 $is_widget ^= 1;
2946 }
2947 });
2948 $clear_all_button->set_visible(1);
2949 }
2950
2951 my $scrolled_window = Gtk3::ScrolledWindow->new();
2952 $scrolled_window->set_hexpand(1);
2953 $scrolled_window->set_propagate_natural_height(1) if $hd_count > 4;
2954
2955 my $diskgrid = $create_label_widget_grid->($disk_labeled_widgets);
2956
2957 $scrolled_window->add($diskgrid);
2958 $scrolled_window->set_policy('never', 'automatic');
2959 $scrolled_window->set_visible(1);
2960 $scrolled_window->set_min_content_height(180);
2961
2962 my $vbox = Gtk3::Box->new('vertical', 0);
2963 $vbox->pack_start($scrolled_window, 1, 1, 10);
2964
2965 my $hbox = Gtk3::Box->new('horizontal', 0);
2966 $hbox->pack_end($clear_all_button, 0, 0, 20);
2967 $hbox->set_visible(1);
2968 $vbox->pack_end($hbox, 0, 0, 0);
2969
2970 return $vbox;
2971 };
2972
2973 # shared between different ui parts (e.g., ZFS and "normal" single disk FS)
2974 my $hdsize_size_adj;
2975 my $hdsize_entry_buffer;
2976
2977 my $get_hdsize_spinbtn = sub {
2978 my $hdsize = shift;
2979
2980 $hdsize_entry_buffer //= Gtk3::EntryBuffer->new(undef, 1);
2981
2982 if (defined($hdsize)) {
2983 $hdsize_size_adj = Gtk3::Adjustment->new($config_options->{hdsize} || $hdsize, 0, $hdsize+1, 1, 1, 1);
2984 } else {
2985 die "called get_hdsize_spinbtn with \$hdsize_size_adj not defined but did not pass hdsize!\n"
2986 if !defined($hdsize_size_adj);
2987 }
2988
2989 my $spinbutton_hdsize = Gtk3::SpinButton->new($hdsize_size_adj, 1, 1);
2990 $spinbutton_hdsize->set_buffer($hdsize_entry_buffer);
2991 $spinbutton_hdsize->set_adjustment($hdsize_size_adj);
2992 $spinbutton_hdsize->set_tooltip_text("only use specified size (GB) of the harddisk (rest left unpartitioned)");
2993 return $spinbutton_hdsize;
2994 };
2995
2996 my $create_raid_advanced_grid = sub {
2997 my $labeled_widgets = [];
2998 my $spinbutton_ashift = Gtk3::SpinButton->new_with_range(9,13,1);
2999 $spinbutton_ashift->set_tooltip_text("zpool ashift property (pool sector size, default 2^12)");
3000 $spinbutton_ashift->signal_connect ("value-changed" => sub {
3001 my $w = shift;
3002 $config_options->{ashift} = $w->get_value_as_int();
3003 });
3004 $config_options->{ashift} = 12 if ! defined($config_options->{ashift});
3005 $spinbutton_ashift->set_value($config_options->{ashift});
3006 push @$labeled_widgets, "ashift";
3007 push @$labeled_widgets, $spinbutton_ashift;
3008
3009 my $combo_compress = Gtk3::ComboBoxText->new();
3010 $combo_compress->set_tooltip_text("zfs compression algorithm for rpool dataset");
3011 # note: gzip / lze not allowed for bootfs vdevs
3012 my $comp_opts = ["on","off","lzjb","lz4"];
3013 foreach my $opt (@$comp_opts) {
3014 $combo_compress->append($opt, $opt);
3015 }
3016 $config_options->{compress} = "on" if !defined($config_options->{compress});
3017 $combo_compress->set_active_id($config_options->{compress});
3018 $combo_compress->signal_connect (changed => sub {
3019 my $w = shift;
3020 $config_options->{compress} = $w->get_active_text();
3021 });
3022 push @$labeled_widgets, "compress";
3023 push @$labeled_widgets, $combo_compress;
3024
3025 my $combo_checksum = Gtk3::ComboBoxText->new();
3026 $combo_checksum->set_tooltip_text("zfs checksum algorithm for rpool dataset");
3027 my $csum_opts = ["on", "off","fletcher2", "fletcher4", "sha256"];
3028 foreach my $opt (@$csum_opts) {
3029 $combo_checksum->append($opt, $opt);
3030 }
3031 $config_options->{checksum} = "on" if !($config_options->{checksum});
3032 $combo_checksum->set_active_id($config_options->{checksum});
3033 $combo_checksum->signal_connect (changed => sub {
3034 my $w = shift;
3035 $config_options->{checksum} = $w->get_active_text();
3036 });
3037 push @$labeled_widgets, "checksum";
3038 push @$labeled_widgets, $combo_checksum;
3039
3040 my $spinbutton_copies = Gtk3::SpinButton->new_with_range(1,3,1);
3041 $spinbutton_copies->set_tooltip_text("zfs copies property for rpool dataset (in addition to RAID redundancy!)");
3042 $spinbutton_copies->signal_connect ("value-changed" => sub {
3043 my $w = shift;
3044 $config_options->{copies} = $w->get_value_as_int();
3045 });
3046 $config_options->{copies} = 1 if !defined($config_options->{copies});
3047 $spinbutton_copies->set_value($config_options->{copies});
3048 push @$labeled_widgets, "copies", $spinbutton_copies;
3049
3050 push @$labeled_widgets, "hdsize", $get_hdsize_spinbtn->();
3051 return &$create_label_widget_grid($labeled_widgets);;
3052 };
3053
3054 sub create_hdoption_view {
3055
3056 my $dialog = Gtk3::Dialog->new();
3057
3058 $dialog->set_title("Harddisk options");
3059
3060 $dialog->add_button("_OK", 1);
3061
3062 my $contarea = $dialog->get_content_area();
3063
3064 my $hbox2 = Gtk3::Box->new('horizontal', 0);
3065 $contarea->pack_start($hbox2, 1, 1, 5);
3066
3067 my $grid = Gtk3::Grid->new();
3068 $grid->set_column_spacing(10);
3069 $grid->set_row_spacing(10);
3070
3071 $hbox2->pack_start($grid, 1, 0, 5);
3072
3073 my $row = 0;
3074
3075 # Filesystem type
3076
3077 my $label0 = Gtk3::Label->new("Filesystem");
3078 $label0->set_alignment (1, 0.5);
3079 $grid->attach($label0, 0, $row, 1, 1);
3080
3081 my $fstypecb = Gtk3::ComboBoxText->new();
3082
3083 my $fstype = ['ext4', 'xfs',
3084 'zfs (RAID0)', 'zfs (RAID1)',
3085 'zfs (RAID10)', 'zfs (RAIDZ-1)',
3086 'zfs (RAIDZ-2)', 'zfs (RAIDZ-3)'];
3087
3088 push @$fstype, 'btrfs (RAID0)', 'btrfs (RAID1)', 'btrfs (RAID10)'
3089 if $setup->{enable_btrfs};
3090
3091 my $tcount = 0;
3092 foreach my $tmp (@$fstype) {
3093 $fstypecb->append_text($tmp);
3094 $fstypecb->set_active ($tcount)
3095 if $config_options->{filesys} eq $tmp;
3096 $tcount++;
3097 }
3098
3099 $grid->attach($fstypecb, 1, $row, 1, 1);
3100
3101 $hbox2->show_all();
3102
3103 $row++;
3104
3105 my $sep = Gtk3::HSeparator->new();
3106 $sep->set_visible(1);
3107 $grid->attach($sep, 0, $row, 2, 1);
3108 $row++;
3109
3110 my $hw_raid_note = Gtk3::Label->new(
3111 "Note: ZFS is not compatible with hardware RAID controllers, for details see the documentation."
3112 );
3113 $hw_raid_note->set_line_wrap(1);
3114 $hw_raid_note->set_max_width_chars(30);
3115 $hw_raid_note->set_visible(0);
3116 $grid->attach($hw_raid_note, 0, $row++, 2, 1);
3117
3118 my $hdsize_labeled_widgets = [];
3119
3120 # size compute
3121 my $hdsize = 0;
3122 if ( -b $target_hd) {
3123 $hdsize = int(hd_size ($target_hd) / (1024*1024.0)); # size in GB
3124 } elsif ($target_hd) {
3125 $hdsize = int((-s $target_hd) / (1024*1024*1024.0));
3126 }
3127
3128 my $spinbutton_hdsize = $get_hdsize_spinbtn->($hdsize);
3129 push @$hdsize_labeled_widgets, "hdsize", $spinbutton_hdsize;
3130
3131 my $entry_swapsize = Gtk3::Entry->new();
3132 $entry_swapsize->set_tooltip_text("maximum SWAP size (GB)");
3133 $entry_swapsize->signal_connect (key_press_event => \&check_float);
3134 $entry_swapsize->set_text($config_options->{swapsize}) if defined($config_options->{swapsize});
3135 push @$hdsize_labeled_widgets, "swapsize", $entry_swapsize;
3136
3137 my $entry_maxroot = Gtk3::Entry->new();
3138 if ($setup->{product} eq 'pve') {
3139 $entry_maxroot->set_tooltip_text("maximum size (GB) for LVM root volume");
3140 $entry_maxroot->signal_connect (key_press_event => \&check_float);
3141 $entry_maxroot->set_text($config_options->{maxroot}) if $config_options->{maxroot};
3142 push @$hdsize_labeled_widgets, "maxroot", $entry_maxroot;
3143 }
3144
3145 my $entry_minfree = Gtk3::Entry->new();
3146 $entry_minfree->set_tooltip_text("minimum free LVM space (GB, required for LVM snapshots)");
3147 $entry_minfree->signal_connect (key_press_event => \&check_float);
3148 $entry_minfree->set_text($config_options->{minfree}) if defined($config_options->{minfree});
3149 push @$hdsize_labeled_widgets, "minfree", $entry_minfree;
3150
3151 my $entry_maxvz;
3152 if ($setup->{product} eq 'pve') {
3153 $entry_maxvz = Gtk3::Entry->new();
3154 $entry_maxvz->set_tooltip_text("maximum size (GB) for LVM data volume");
3155 $entry_maxvz->signal_connect (key_press_event => \&check_float);
3156 $entry_maxvz->set_text($config_options->{maxvz}) if defined($config_options->{maxvz});
3157 push @$hdsize_labeled_widgets, "maxvz", $entry_maxvz;
3158 }
3159
3160 my $options_stack = Gtk3::Stack->new();
3161 $options_stack->set_visible(1);
3162 $options_stack->set_hexpand(1);
3163 $options_stack->set_vexpand(1);
3164 $options_stack->add_titled(&$create_raid_disk_grid(), "raiddisk", "Disk Setup");
3165 $options_stack->add_titled(&$create_label_widget_grid($hdsize_labeled_widgets), "hdsize", "Size Options");
3166 $options_stack->add_titled(&$create_raid_advanced_grid("zfs"), "raidzfsadvanced", "Advanced Options");
3167 $options_stack->set_visible_child_name("raiddisk");
3168 my $options_stack_switcher = Gtk3::StackSwitcher->new();
3169 $options_stack_switcher->set_halign('center');
3170 $options_stack_switcher->set_stack($options_stack);
3171 $grid->attach($options_stack_switcher, 0, $row, 2, 1);
3172 $row++;
3173 $grid->attach($options_stack, 0, $row, 2, 1);
3174 $row++;
3175
3176 $hdoption_first_setup = 0;
3177
3178 my $switch_view = sub {
3179 my $raid = $config_options->{filesys} =~ m/zfs|btrfs/;
3180 my $enable_zfs_opts = $config_options->{filesys} =~ m/zfs/;
3181
3182 $target_hd_combo->set_visible(!$raid);
3183 $options_stack->get_child_by_name("hdsize")->set_visible(!$raid);
3184 $options_stack->get_child_by_name("raiddisk")->set_visible($raid);
3185 $hw_raid_note->set_visible($raid);
3186 $options_stack_switcher->set_visible($enable_zfs_opts);
3187 $options_stack->get_child_by_name("raidzfsadvanced")->set_visible($enable_zfs_opts);
3188 if ($raid) {
3189 $target_hd_label->set_text("Target: $config_options->{filesys} ");
3190 $options_stack->set_visible_child_name("raiddisk");
3191 } else {
3192 $target_hd_label->set_text("Target Harddisk: ");
3193 }
3194 my (undef, $pref_width) = $dialog->get_preferred_width();
3195 my (undef, $pref_height) = $dialog->get_preferred_height();
3196 $pref_height = 750 if $pref_height > 750;
3197 $dialog->resize($pref_width, $pref_height);
3198 };
3199
3200 &$switch_view();
3201
3202 $fstypecb->signal_connect (changed => sub {
3203 $config_options->{filesys} = $fstypecb->get_active_text();
3204 &$switch_view();
3205 });
3206
3207 my $sep2 = Gtk3::HSeparator->new();
3208 $sep2->set_visible(1);
3209 $contarea->pack_end($sep2, 1, 1, 10);
3210
3211 $dialog->show();
3212
3213 $dialog->run();
3214
3215 my $get_float = sub {
3216 my ($entry) = @_;
3217
3218 my $text = $entry->get_text();
3219 return undef if !defined($text);
3220
3221 $text =~ s/^\s+//;
3222 $text =~ s/\s+$//;
3223
3224 return undef if $text !~ m/^\d+(\.\d+)?$/;
3225
3226 return $text;
3227 };
3228
3229 my $tmp;
3230
3231 if (($tmp = &$get_float($spinbutton_hdsize)) && ($tmp != $hdsize)) {
3232 $config_options->{hdsize} = $tmp;
3233 } else {
3234 delete $config_options->{hdsize};
3235 }
3236
3237 if (defined($tmp = &$get_float($entry_swapsize))) {
3238 $config_options->{swapsize} = $tmp;
3239 } else {
3240 delete $config_options->{swapsize};
3241 }
3242
3243 if (defined($tmp = &$get_float($entry_maxroot))) {
3244 $config_options->{maxroot} = $tmp;
3245 } else {
3246 delete $config_options->{maxroot};
3247 }
3248
3249 if (defined($tmp = &$get_float($entry_minfree))) {
3250 $config_options->{minfree} = $tmp;
3251 } else {
3252 delete $config_options->{minfree};
3253 }
3254
3255 if ($entry_maxvz && defined($tmp = &$get_float($entry_maxvz))) {
3256 $config_options->{maxvz} = $tmp;
3257 } else {
3258 delete $config_options->{maxvz};
3259 }
3260
3261 $dialog->destroy();
3262 }
3263
3264 my $get_raid_devlist = sub {
3265
3266 my $dev_name_hash = {};
3267
3268 my $devlist = [];
3269 for (my $i = 0; $i < @$hds; $i++) {
3270 if (my $hd = $config_options->{"disksel$i"}) {
3271 my ($disk, $devname, $size, $model, $logical_bsize) = @$hd;
3272 die "device '$devname' is used more than once\n"
3273 if $dev_name_hash->{$devname};
3274 $dev_name_hash->{$devname} = $hd;
3275 push @$devlist, $hd;
3276 }
3277 }
3278
3279 return $devlist;
3280 };
3281
3282 sub zfs_mirror_size_check {
3283 my ($expected, $actual) = @_;
3284
3285 die "mirrored disks must have same size\n"
3286 if abs($expected - $actual) > $expected / 10;
3287 }
3288
3289 sub legacy_bios_4k_check {
3290 my ($lbs) = @_;
3291 die "Booting from 4kn drive in legacy BIOS mode is not supported.\n"
3292 if (($boot_type ne 'efi') && ($lbs == 4096));
3293 }
3294
3295 sub get_zfs_raid_setup {
3296
3297 my $filesys = $config_options->{filesys};
3298
3299 my $devlist = &$get_raid_devlist();
3300
3301 my $diskcount = scalar(@$devlist);
3302 die "$filesys needs at least one device\n" if $diskcount < 1;
3303
3304 my $bootdevlist = [];
3305
3306 my $cmd= '';
3307 if ($filesys eq 'zfs (RAID0)') {
3308 push @$bootdevlist, @$devlist[0];
3309 foreach my $hd (@$devlist) {
3310 legacy_bios_4k_check(@$hd[4]);
3311 $cmd .= " @$hd[1]";
3312 }
3313 } elsif ($filesys eq 'zfs (RAID1)') {
3314 die "zfs (RAID1) needs at least 2 device\n" if $diskcount < 2;
3315 $cmd .= ' mirror ';
3316 my $hd = @$devlist[0];
3317 my $expected_size = @$hd[2]; # all disks need approximately same size
3318 foreach my $hd (@$devlist) {
3319 zfs_mirror_size_check($expected_size, @$hd[2]);
3320 legacy_bios_4k_check(@$hd[4]);
3321 $cmd .= " @$hd[1]";
3322 push @$bootdevlist, $hd;
3323 }
3324 } elsif ($filesys eq 'zfs (RAID10)') {
3325 die "zfs (RAID10) needs at least 4 device\n" if $diskcount < 4;
3326 die "zfs (RAID10) needs an even number of devices\n" if $diskcount & 1;
3327
3328 push @$bootdevlist, @$devlist[0], @$devlist[1];
3329
3330 for (my $i = 0; $i < $diskcount; $i+=2) {
3331 my $hd1 = @$devlist[$i];
3332 my $hd2 = @$devlist[$i+1];
3333 zfs_mirror_size_check(@$hd1[2], @$hd2[2]); # pairs need approximately same size
3334 legacy_bios_4k_check(@$hd1[4]);
3335 legacy_bios_4k_check(@$hd2[4]);
3336 $cmd .= ' mirror ' . @$hd1[1] . ' ' . @$hd2[1];
3337 }
3338
3339 } elsif ($filesys =~ m/^zfs \(RAIDZ-([123])\)$/) {
3340 my $level = $1;
3341 my $mindisks = 2 + $level;
3342 die "zfs (RAIDZ-$level) needs at least $mindisks devices\n" if scalar(@$devlist) < $mindisks;
3343 my $hd = @$devlist[0];
3344 my $expected_size = @$hd[2]; # all disks need approximately same size
3345 $cmd .= " raidz$level";
3346 foreach my $hd (@$devlist) {
3347 zfs_mirror_size_check($expected_size, @$hd[2]);
3348 legacy_bios_4k_check(@$hd[4]);
3349 $cmd .= " @$hd[1]";
3350 push @$bootdevlist, $hd;
3351 }
3352 } else {
3353 die "unknown zfs mode '$filesys'\n";
3354 }
3355
3356 return ($devlist, $bootdevlist, $cmd);
3357 }
3358
3359 sub get_btrfs_raid_setup {
3360
3361 my $filesys = $config_options->{filesys};
3362
3363 my $devlist = &$get_raid_devlist();
3364
3365 my $diskcount = scalar(@$devlist);
3366 die "$filesys needs at least one device\n" if $diskcount < 1;
3367
3368 my $mode;
3369
3370 if ($diskcount == 1) {
3371 $mode = 'single';
3372 } else {
3373 if ($filesys eq 'btrfs (RAID0)') {
3374 $mode = 'raid0';
3375 } elsif ($filesys eq 'btrfs (RAID1)') {
3376 die "btrfs (RAID1) needs at least 2 device\n" if $diskcount < 2;
3377 $mode = 'raid1';
3378 } elsif ($filesys eq 'btrfs (RAID10)') {
3379 die "btrfs (RAID10) needs at least 4 device\n" if $diskcount < 4;
3380 $mode = 'raid10';
3381 } else {
3382 die "unknown btrfs mode '$filesys'\n";
3383 }
3384 }
3385
3386 return ($devlist, $mode);
3387 }
3388
3389 my $last_hd_selected = 0;
3390 sub create_hdsel_view {
3391
3392 $prev_btn->set_sensitive(1); # enable previous button at this point
3393
3394 cleanup_view();
3395
3396 my $vbox = Gtk3::VBox->new(0, 0);
3397 $inbox->pack_start($vbox, 1, 0, 0);
3398 my $hbox = Gtk3::HBox->new(0, 0);
3399 $vbox->pack_start($hbox, 0, 0, 10);
3400
3401 my ($disk, $devname, $size, $model, $logical_bsize) = @{@$hds[0]};
3402 $target_hd = $devname if !defined($target_hd);
3403
3404 $target_hd_label = Gtk3::Label->new("Target Harddisk: ");
3405 $hbox->pack_start($target_hd_label, 0, 0, 0);
3406
3407 $target_hd_combo = Gtk3::ComboBoxText->new();
3408
3409 foreach my $hd (@$hds) {
3410 ($disk, $devname, $size, $model, $logical_bsize) = @$hd;
3411 $target_hd_combo->append_text (get_device_desc($devname, $size, $model));
3412 }
3413
3414 my $raid = $config_options->{filesys} =~ m/zfs|btrfs/;
3415 if ($raid) {
3416 $target_hd_label->set_text("Target: $config_options->{filesys} ");
3417 $target_hd_combo->set_visible(0);
3418 $target_hd_combo->set_no_show_all(1);
3419 }
3420 $target_hd_combo->set_active($last_hd_selected);
3421 $target_hd_combo->signal_connect(changed => sub {
3422 $a = shift->get_active;
3423 my ($disk, $devname) = @{@$hds[$a]};
3424 $last_hd_selected = $a;
3425 $target_hd = $devname;
3426 });
3427
3428 $hbox->pack_start($target_hd_combo, 0, 0, 10);
3429
3430 my $options = Gtk3::Button->new('_Options');
3431 $options->signal_connect (clicked => \&create_hdoption_view);
3432 $hbox->pack_start ($options, 0, 0, 0);
3433
3434
3435 $inbox->show_all;
3436
3437 display_html();
3438
3439 set_next(undef, sub {
3440
3441 if ($config_options->{filesys} =~ m/zfs/) {
3442 my ($devlist) = eval { get_zfs_raid_setup() };
3443 if (my $err = $@) {
3444 display_message("Warning: $err\nPlease fix ZFS setup first.");
3445 return;
3446 }
3447 $config_options->{target_hds} = [ map { $_->[1] } @$devlist ];
3448 } elsif ($config_options->{filesys} =~ m/btrfs/) {
3449 my ($devlist) = eval { get_btrfs_raid_setup() };
3450 if (my $err = $@) {
3451 display_message("Warning: $err\nPlease fix BTRFS setup first.");
3452 return;
3453 }
3454 $config_options->{target_hds} = [ map { $_->[1] } @$devlist ];
3455 } else {
3456 eval { legacy_bios_4k_check(logical_blocksize($target_hd)) };
3457 if (my $err = $@) {
3458 display_message("Warning: $err\n");
3459 return;
3460 }
3461 $config_options->{target_hds} = [ $target_hd ];
3462 }
3463
3464 $step_number++;
3465 create_country_view();
3466 });
3467 }
3468
3469 sub create_extract_view {
3470
3471 cleanup_view();
3472
3473 display_info();
3474
3475 $next->set_sensitive(0);
3476 $prev_btn->set_sensitive(0);
3477 $prev_btn->hide();
3478
3479 my $vbox = Gtk3::VBox->new(0, 0);
3480 $inbox->pack_start ($vbox, 1, 0, 0);
3481 my $hbox = Gtk3::HBox->new(0, 0);
3482 $vbox->pack_start ($hbox, 0, 0, 10);
3483
3484 my $vbox2 = Gtk3::VBox->new(0, 0);
3485 $hbox->pack_start ($vbox2, 0, 0, 0);
3486
3487 $progress_status = Gtk3::Label->new('');
3488 $vbox2->pack_start ($progress_status, 1, 1, 0);
3489
3490 $progress = Gtk3::ProgressBar->new;
3491 $progress->set_show_text(1);
3492 $progress->set_size_request (600, -1);
3493
3494 $vbox2->pack_start($progress, 0, 0, 0);
3495
3496 $inbox->show_all();
3497
3498 my $tdir = $opt_testmode ? "target" : "/target";
3499 mkdir $tdir;
3500 my $base = "${proxmox_cddir}/$setup->{product}-base.squashfs";
3501
3502 eval { extract_data($base, $tdir); };
3503 my $err = $@;
3504
3505 $next->set_sensitive(1);
3506
3507 set_next("_Reboot", sub { exit (0); } );
3508
3509 if ($err) {
3510 display_html("fail.htm");
3511 display_error($err);
3512 } else {
3513 cleanup_view();
3514 display_html("success.htm");
3515
3516 if ($config_options->{autoreboot}) {
3517 Glib::Timeout->add(1000, sub {
3518 if ($autoreboot_seconds > 0) {
3519 $autoreboot_seconds--;
3520 display_html("success.htm");
3521 } else {
3522 exit(0);
3523 }
3524 });
3525 }
3526 }
3527 }
3528
3529 sub create_intro_view {
3530
3531 $prev_btn->set_sensitive(0);
3532
3533 cleanup_view();
3534
3535 if (int($total_memory) < 1024) {
3536 my $product = $product_cfg->{$setup->{product}};
3537
3538 display_error("Less than 1 GiB of usable memory detected, installation will probably fail.\n\n".
3539 "See 'System Requirements' in the $product->{fullname} documentation.");
3540 }
3541
3542 if ($setup->{product} eq 'pve') {
3543 eval {
3544 my $cpuinfo = file_get_contents('/proc/cpuinfo');
3545 if ($cpuinfo && !($cpuinfo =~ /^flags\s*:.*(vmx|svm)/m)) {
3546 display_error("No support for KVM virtualization detected.\n\n" .
3547 "Check BIOS settings for Intel VT / AMD-V / SVM.")
3548 }
3549 };
3550 }
3551
3552 display_html();
3553
3554 $step_number++;
3555 set_next("I a_gree", \&create_hdsel_view);
3556 }
3557
3558 $ipconf = get_ip_config();
3559
3560 $country = detect_country() if $ipconf->{default} || $opt_testmode;
3561
3562 # read country, kmap and timezone infos
3563 $cmap = read_cmap();
3564
3565 if (!defined($cmap->{country}->{$country})) {
3566 print $logfd "ignoring detected country '$country', invalid or unknown\n";
3567 $country = undef;
3568 }
3569
3570 create_main_window ();
3571
3572 my $initial_error = 0;
3573
3574 if (!defined ($hds) || (scalar (@$hds) <= 0)) {
3575 print "no hardisks found\n";
3576 $initial_error = 1;
3577 display_html("nohds.htm");
3578 set_next("Reboot", sub { exit(0); } );
3579 } else {
3580 foreach my $hd (@$hds) {
3581 my ($disk, $devname) = @$hd;
3582 next if $devname =~ m|^/dev/md\d+$|;
3583 print "found Disk$disk N:$devname\n";
3584 }
3585 }
3586
3587 if (!$initial_error && (scalar keys %{ $ipconf->{ifaces} } == 0)) {
3588 print "no network interfaces found\n";
3589 $initial_error = 1;
3590 display_html("nonics.htm");
3591 set_next("Reboot", sub { exit(0); } );
3592 }
3593
3594 create_intro_view () if !$initial_error;
3595
3596 Gtk3->main;
3597
3598 exit 0;