]> git.proxmox.com Git - pve-installer.git/blame - Proxmox/Install/RunEnv.pm
install module: getters: correctly use plural in error messages
[pve-installer.git] / Proxmox / Install / RunEnv.pm
CommitLineData
2231d226
WB
1package Proxmox::Install::RunEnv;
2
3use strict;
4use warnings;
5
6use Carp;
7use JSON qw(from_json to_json);
2231d226 8
0d27321b 9use Proxmox::Log;
8a1a7187 10use Proxmox::Sys::Command qw(run_command CMD_FINISHED);
5364c0c1 11use Proxmox::Sys::File qw(file_read_all file_read_firstline);
ceec3def 12use Proxmox::Sys::Block;
fe06d7e9 13use Proxmox::Sys::Net;
2231d226 14
e1b74696
TL
15use Proxmox::Install::ISOEnv;
16
2231d226
WB
17my sub fromjs : prototype($) {
18 return from_json($_[0], { utf8 => 1 });
19}
20
b91f9cad 21my $mem_total = undef;
ed2b90a4
CH
22# Returns the system memory size in MiB, and falls back to 512 MiB if it
23# could not be determined.
b91f9cad
TL
24sub query_total_memory : prototype() {
25 return $mem_total if defined($mem_total);
26
27 open (my $MEMINFO, '<', '/proc/meminfo');
28
29 my $res = 512; # default to 512 if something goes wrong
30 while (my $line = <$MEMINFO>) {
31 if ($line =~ m/^MemTotal:\s+(\d+)\s*kB/i) {
32 $res = int ($1 / 1024);
33 }
34 }
35 close($MEMINFO);
36
37 $mem_total = $res;
38 return $mem_total;
39}
40
4d8aec8a
CH
41my $cpu_hvm_support = undef;
42sub query_cpu_hvm_support : prototype() {
43 return $cpu_hvm_support if defined($cpu_hvm_support);
44
45 open (my $CPUINFO, '<', '/proc/cpuinfo');
46
47 my $res = 0;
48 while (my $line = <$CPUINFO>) {
49 if ($line =~ /^flags\s*:.*(vmx|svm)/m) {
50 $res = 1;
51 last;
52 }
53 }
54 close($CPUINFO);
55
56 $cpu_hvm_support = $res;
57 return $cpu_hvm_support;
58}
59
2231d226
WB
60# Returns a hash.
61#
62# {
63# <ifname> => {
64# mac => <mac address>,
65# index => <index>,
66# name => <ifname>,
67# addresses => [
68# family => <inet|inet6>,
69# address => <mac address>,
70# prefix => <length>,
71# ],
72# },
73# }
74my sub query_netdevs : prototype() {
75 my $ifs = {};
76 my $default;
77
88adf315 78 # FIXME: not the same as the battle proven way we used in the installer for years?
2231d226
WB
79 my $interfaces = fromjs(qx/ip --json address show/);
80
81 for my $if (@$interfaces) {
82 my ($index, $name, $state, $mac, $addresses) =
83 $if->@{qw(ifindex ifname operstate address addr_info)};
84
5ea718f6 85 next if !$name || $name eq 'lo'; # could also check flags for LOOPBACK..
2231d226
WB
86
87 my @valid_addrs;
5ea718f6
TL
88 if (uc($state) eq 'UP') {
89 for my $addr (@$addresses) {
90 next if $addr->{scope} eq 'link';
91
92 my ($family, $addr, $prefix) = $addr->@{qw(family local prefixlen)};
93
94 push @valid_addrs, {
95 family => $family,
96 address => $addr,
97 prefix => $prefix,
98 };
99 }
2231d226
WB
100 }
101
5ea718f6
TL
102 $ifs->{$name} = {
103 index => $index,
104 name => $name,
105 mac => $mac,
5c450165 106 state => uc($state),
5ea718f6
TL
107 };
108 $ifs->{$name}->{addresses} = \@valid_addrs if @valid_addrs;
2231d226
WB
109 }
110
111 return $ifs;
112}
113
114# Returns a hash.
115#
116# {
117# gateway4 => {
118# dst => "default",
119# gateway => <ipv4>,
120# dev => <ifname>,
121# },
122# gateway6 => {
123# dst => "default",
124# gateway => <ipv6>,
125# dev => <ifname>,
126# },
127# }
128my sub query_routes : prototype() {
129 my ($gateway4, $gateway6);
130
a467ec47 131 log_info("query routes");
2231d226
WB
132 my $route4 = fromjs(qx/ip -4 --json route show/);
133 for my $route (@$route4) {
134 if ($route->{dst} eq 'default') {
135 $gateway4 = {
136 dev => $route->{dev},
137 gateway => $route->{gateway},
138 };
139 last;
140 }
141 }
142
143 my $route6 = fromjs(qx/ip -6 --json route show/);
144 for my $route (@$route6) {
145 if ($route->{dst} eq 'default') {
146 $gateway6 = {
147 dev => $route->{dev},
148 gateway => $route->{gateway},
149 };
150 last;
151 }
152 }
153
154 my $routes;
155 $routes->{gateway4} = $gateway4 if $gateway4;
156 $routes->{gateway6} = $gateway6 if $gateway6;
157
158 return $routes;
159}
160
161# If `/etc/resolv.conf` fails to open this returns nothing.
162# Otherwise it returns a hash:
163# {
164# dns => <first dns entry>,
165#
166my sub query_dns : prototype() {
a467ec47 167 log_info("query DNS from resolv.conf (managed by DHCP client)");
2231d226
WB
168 open my $fh , '<', '/etc/resolv.conf' or return;
169
170 my @dns;
171 my $domain;
172 while (defined(my $line = <$fh>)) {
173 if ($line =~ /^nameserver\s+(\S+)/) {
174 push @dns, $1;
175 } elsif (!defined($domain) && $line =~ /^domain\s+(\S+)/) {
176 $domain = $1;
177 }
178 }
179
180 my $output = {
181 domain => $domain,
182 @dns ? (dns => \@dns) : (),
183 };
184};
185
186# Uses `traceroute` and `geoiplookup`/`geoiplookup6` to figure out the current country.
187# Has a 10s timeout and uses the stops at the first entry found in the geoip database.
188my sub detect_country_tracing_to : prototype($$) {
189 my ($ipver, $destination) = @_;
190
a467ec47 191 print STDERR "trying to detect country...\n";
2231d226 192
8a1a7187 193 my $traceroute_cmd = ['traceroute', "-$ipver", '-N', '1', '-q', '1', '-n', $destination];
2231d226
WB
194 my $geoip_bin = ($ipver == 6) ? 'geoiplookup6' : 'geoiplookup';
195
196 my $country;
8a1a7187 197 my $previous_alarm;
2231d226
WB
198 eval {
199 local $SIG{ALRM} = sub { die "timed out!\n" };
8a1a7187
CH
200 $previous_alarm = alarm (10);
201
202 run_command($traceroute_cmd, sub {
203 my $line = shift;
204
2231d226
WB
205 log_debug("DC TRACEROUTE: $line");
206 if ($line =~ m/^\s*\d+\s+(\S+)\s/) {
207 my $geoip = qx/$geoip_bin $1/;
208 log_debug("DC GEOIP: $geoip");
8a1a7187 209
2231d226
WB
210 if ($geoip =~ m/GeoIP Country Edition:\s*([A-Z]+),/) {
211 $country = lc ($1);
212 log_info("DC FOUND: $country\n");
8a1a7187 213 return CMD_FINISHED;
2231d226 214 }
41c686d4 215 return;
2231d226 216 }
8a1a7187
CH
217 }, undef, undef, 1);
218
2231d226
WB
219 };
220 my $err = $@;
221 alarm ($previous_alarm);
222
2231d226
WB
223 if ($err) {
224 die "unable to detect country - $err\n";
225 } elsif ($country) {
a467ec47 226 print STDERR "detected country: " . uc($country) . "\n";
2231d226
WB
227 }
228
229 return $country;
230}
231
232# Returns the entire environment as a hash.
233# {
234# country => <short country>,
4d8aec8a
CH
235# ipconf = <see Proxmox::Sys::Net::get_ip_config()>,
236# kernel_cmdline = <contents of /proc/cmdline>,
237# total_memory = <memory size in MiB>,
238# hvm_supported = <1 if the CPU supports hardware-accelerated virtualization>,
239# boot_type = <either 'efi' or 'bios'>,
ceec3def 240# disks => <see Proxmox::Sys::Block::hd_list()>,
2231d226
WB
241# network => {
242# interfaces => <see query_netdevs()>,
243# routes => <see query_routes()>,
1c5e103a 244# dns => <see query_dns()>,
2231d226 245# },
2231d226
WB
246# }
247sub query_installation_environment : prototype() {
e1b74696
TL
248 # check first if somebody already cached this for us and re-use that
249 my $run_env_file = Proxmox::Install::ISOEnv::get('run-env-cache-file');
250 if (-f "$run_env_file") {
251 log_info("re-using cached runtime env from $run_env_file");
252 my $cached_env = eval {
253 my $run_env_raw = Proxmox::Sys::File::file_read_all($run_env_file);
254 return fromjs($run_env_raw); # returns from eval
255 };
256 log_error("failed to parse cached runtime env - $@") if $@;
257 return $cached_env if defined($cached_env) && scalar keys $cached_env->%*;
258 log_warn("cached runtime env seems empty, query everything (again)");
259 }
260 # else re-query everything
2231d226
WB
261 my $output = {};
262
263 my $routes = query_routes();
264
a467ec47 265 log_info("query block devices");
ceec3def 266 $output->{disks} = Proxmox::Sys::Block::get_cached_disks();
2231d226
WB
267 $output->{network} = {
268 interfaces => query_netdevs(),
269 routes => $routes,
1c5e103a 270 dns => query_dns(),
2231d226 271 };
bda1cdf6 272
5130de4d
TL
273 # avoid serializing out null or an empty string, that can trip up the UIs
274 if (my $fqdn = Proxmox::Sys::Net::get_dhcp_fqdn()) {
275 $output->{network}->{hostname} = $fqdn;
a8054233 276 }
bda1cdf6 277
fe06d7e9
TL
278 # FIXME: move whatever makes sense over to Proxmox::Sys::Net:: and keep that as single source,
279 # it can then use some different structure just fine (after adapting the GTK GUI to that) but
280 # **never** to (slightly different!) things for the same stuff...
281 $output->{ipconf} = Proxmox::Sys::Net::get_ip_config();
2231d226 282
88adf315 283 $output->{kernel_cmdline} = file_read_firstline("/proc/cmdline");
b91f9cad 284 $output->{total_memory} = query_total_memory();
4d8aec8a 285 $output->{hvm_supported} = query_cpu_hvm_support();
71583761 286 $output->{boot_type} = -d '/sys/firmware/efi' ? 'efi' : 'bios';
b91f9cad 287
5364c0c1
FG
288 if ($output->{boot_type} eq 'efi') {
289 my $content = eval { file_read_all("/sys/firmware/efi/efivars/SecureBoot-8be4df61-93ca-11d2-aa0d-00e098032b8c") };
290 if ($@) {
291 log_warn("Failed to read secure boot state: $@\n");
292 } else {
293 my @secureboot = unpack("CCCCC", $content);
294 $output->{secure_boot} = $secureboot[4] == 1;
295 }
296 }
297
2231d226
WB
298 my $err;
299 my $country;
300 if ($routes->{gateway4}) {
a467ec47 301 log_info("trace country via IPv4");
2231d226
WB
302 $country = eval { detect_country_tracing_to(4 => '8.8.8.8') };
303 $err = $@ if !$country;
304 }
305
306 if (!$country && $routes->{gateway6}) {
a467ec47 307 log_info("trace country via IPv6");
2231d226
WB
308 $country = eval { detect_country_tracing_to(6 => '2001:4860:4860::8888') };
309 $err = $@ if !$country;
310 }
311
312 if (defined($country)) {
313 $output->{country} = $country;
314 } else {
176b7be6 315 warn ($err || "unable to detect country\n");
2231d226
WB
316 }
317
318 return $output;
319}
320
42aa2fa7
CH
321# OpenZFS specifies 64 MiB as the absolute minimum:
322# https://openzfs.github.io/openzfs-docs/Performance%20and%20Tuning/Module%20Parameters.html#zfs-arc-max
323our $ZFS_ARC_MIN_SIZE_MIB = 64; # MiB
324
325# See https://bugzilla.proxmox.com/show_bug.cgi?id=4829
326our $ZFS_ARC_MAX_SIZE_MIB = 16 * 1024; # 16384 MiB = 16 GiB
327our $ZFS_ARC_SYSMEM_PERCENTAGE = 0.1; # use 10% of available system memory by default
328
329# Calculates the default upper limit for the ZFS ARC size.
330# Returns the default ZFS maximum ARC size in MiB.
331sub default_zfs_arc_max {
332 # Use ZFS default on non-PVE
333 return 0 if Proxmox::Install::ISOEnv::get('product') ne 'pve';
334
335 my $default_mib = get('total_memory') * $ZFS_ARC_SYSMEM_PERCENTAGE;
336 my $rounded_mib = int(sprintf('%.0f', $default_mib));
42aa2fa7
CH
337
338 if ($rounded_mib > $ZFS_ARC_MAX_SIZE_MIB) {
339 return $ZFS_ARC_MAX_SIZE_MIB;
340 } elsif ($rounded_mib < $ZFS_ARC_MIN_SIZE_MIB) {
341 return $ZFS_ARC_MIN_SIZE_MIB;
342 }
343
344 return $rounded_mib;
345}
346
347# Clamps the provided ZFS arc_max value (in MiB) to the accepted bounds. The
348# lower is specified by `$ZFS_ARC_MIN_SIZE_MIB`, the upper by the available system memory.
349# Returns the clamped value in MiB.
350sub clamp_zfs_arc_max {
351 my ($mib) = @_;
352
353 return $mib if $mib == 0;
354
355 my $total_mem_mib = get('total_memory');
356 if ($mib > $total_mem_mib) {
357 return $total_mem_mib;
358 } elsif ($mib < $ZFS_ARC_MIN_SIZE_MIB) {
359 return $ZFS_ARC_MIN_SIZE_MIB;
360 }
361
362 return $mib;
363}
364
ea3b7cae
TL
365my $_env = undef;
366sub get {
367 my ($k) = @_;
368 $_env = query_installation_environment() if !defined($_env);
369 return defined($k) ? $_env->{$k} : $_env;
370}
371
2231d226 3721;