]> git.proxmox.com Git - pve-installer.git/blob - Proxmox/Install/RunEnv.pm
25b6bb3db25a648328ac69f321e84c93ca895fd8
[pve-installer.git] / Proxmox / Install / RunEnv.pm
1 package Proxmox::Install::RunEnv;
2
3 use strict;
4 use warnings;
5
6 use Carp;
7 use JSON qw(from_json to_json);
8
9 use Proxmox::Log;
10 use Proxmox::Sys::Command qw(run_command CMD_FINISHED);
11 use Proxmox::Sys::File qw(file_read_firstline);
12 use Proxmox::Sys::Block;
13 use Proxmox::Sys::Net;
14
15 use Proxmox::Install::ISOEnv;
16
17 my sub fromjs : prototype($) {
18 return from_json($_[0], { utf8 => 1 });
19 }
20
21 my $mem_total = undef;
22 # Returns the system memory size in MiB, and falls back to 512 MiB if it
23 # could not be determined.
24 sub 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
41 my $cpu_hvm_support = undef;
42 sub 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
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 # }
74 my sub query_netdevs : prototype() {
75 my $ifs = {};
76 my $default;
77
78 # FIXME: not the same as the battle proven way we used in the installer for years?
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
85 next if !$name || $name eq 'lo'; # could also check flags for LOOPBACK..
86
87 my @valid_addrs;
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 }
100 }
101
102 $ifs->{$name} = {
103 index => $index,
104 name => $name,
105 mac => $mac,
106 state => uc($state),
107 };
108 $ifs->{$name}->{addresses} = \@valid_addrs if @valid_addrs;
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 # }
128 my sub query_routes : prototype() {
129 my ($gateway4, $gateway6);
130
131 log_info("query routes");
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 #
166 my sub query_dns : prototype() {
167 log_info("query DNS from resolv.conf (managed by DHCP client)");
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.
188 my sub detect_country_tracing_to : prototype($$) {
189 my ($ipver, $destination) = @_;
190
191 print STDERR "trying to detect country...\n";
192
193 my $traceroute_cmd = ['traceroute', "-$ipver", '-N', '1', '-q', '1', '-n', $destination];
194 my $geoip_bin = ($ipver == 6) ? 'geoiplookup6' : 'geoiplookup';
195
196 my $country;
197 my $previous_alarm;
198 eval {
199 local $SIG{ALRM} = sub { die "timed out!\n" };
200 $previous_alarm = alarm (10);
201
202 run_command($traceroute_cmd, sub {
203 my $line = shift;
204
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");
209
210 if ($geoip =~ m/GeoIP Country Edition:\s*([A-Z]+),/) {
211 $country = lc ($1);
212 log_info("DC FOUND: $country\n");
213 return CMD_FINISHED;
214 }
215 return;
216 }
217 }, undef, undef, 1);
218
219 };
220 my $err = $@;
221 alarm ($previous_alarm);
222
223 if ($err) {
224 die "unable to detect country - $err\n";
225 } elsif ($country) {
226 print STDERR "detected country: " . uc($country) . "\n";
227 }
228
229 return $country;
230 }
231
232 # Returns the entire environment as a hash.
233 # {
234 # country => <short country>,
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'>,
240 # disks => <see Proxmox::Sys::Block::hd_list()>,
241 # network => {
242 # interfaces => <see query_netdevs()>,
243 # routes => <see query_routes()>,
244 # dns => <see query_dns()>,
245 # },
246 # }
247 sub query_installation_environment : prototype() {
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
261 my $output = {};
262
263 my $routes = query_routes();
264
265 log_info("query block devices");
266 $output->{disks} = Proxmox::Sys::Block::get_cached_disks();
267 $output->{network} = {
268 interfaces => query_netdevs(),
269 routes => $routes,
270 dns => query_dns(),
271 };
272
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;
276 }
277
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();
282
283 $output->{kernel_cmdline} = file_read_firstline("/proc/cmdline");
284 $output->{total_memory} = query_total_memory();
285 $output->{hvm_supported} = query_cpu_hvm_support();
286 $output->{boot_type} = -d '/sys/firmware/efi' ? 'efi' : 'bios';
287
288 my $err;
289 my $country;
290 if ($routes->{gateway4}) {
291 log_info("trace country via IPv4");
292 $country = eval { detect_country_tracing_to(4 => '8.8.8.8') };
293 $err = $@ if !$country;
294 }
295
296 if (!$country && $routes->{gateway6}) {
297 log_info("trace country via IPv6");
298 $country = eval { detect_country_tracing_to(6 => '2001:4860:4860::8888') };
299 $err = $@ if !$country;
300 }
301
302 if (defined($country)) {
303 $output->{country} = $country;
304 } else {
305 warn ($err // "unable to detect country\n");
306 }
307
308 return $output;
309 }
310
311 # OpenZFS specifies 64 MiB as the absolute minimum:
312 # https://openzfs.github.io/openzfs-docs/Performance%20and%20Tuning/Module%20Parameters.html#zfs-arc-max
313 our $ZFS_ARC_MIN_SIZE_MIB = 64; # MiB
314
315 # See https://bugzilla.proxmox.com/show_bug.cgi?id=4829
316 our $ZFS_ARC_MAX_SIZE_MIB = 16 * 1024; # 16384 MiB = 16 GiB
317 our $ZFS_ARC_SYSMEM_PERCENTAGE = 0.1; # use 10% of available system memory by default
318
319 # Calculates the default upper limit for the ZFS ARC size.
320 # Returns the default ZFS maximum ARC size in MiB.
321 sub default_zfs_arc_max {
322 # Use ZFS default on non-PVE
323 return 0 if Proxmox::Install::ISOEnv::get('product') ne 'pve';
324
325 my $default_mib = get('total_memory') * $ZFS_ARC_SYSMEM_PERCENTAGE;
326 my $rounded_mib = int(sprintf('%.0f', $default_mib));
327
328 if ($rounded_mib > $ZFS_ARC_MAX_SIZE_MIB) {
329 return $ZFS_ARC_MAX_SIZE_MIB;
330 } elsif ($rounded_mib < $ZFS_ARC_MIN_SIZE_MIB) {
331 return $ZFS_ARC_MIN_SIZE_MIB;
332 }
333
334 return $rounded_mib;
335 }
336
337 # Clamps the provided ZFS arc_max value (in MiB) to the accepted bounds. The
338 # lower is specified by `$ZFS_ARC_MIN_SIZE_MIB`, the upper by the available system memory.
339 # Returns the clamped value in MiB.
340 sub clamp_zfs_arc_max {
341 my ($mib) = @_;
342
343 return $mib if $mib == 0;
344
345 my $total_mem_mib = get('total_memory');
346 if ($mib > $total_mem_mib) {
347 return $total_mem_mib;
348 } elsif ($mib < $ZFS_ARC_MIN_SIZE_MIB) {
349 return $ZFS_ARC_MIN_SIZE_MIB;
350 }
351
352 return $mib;
353 }
354
355 my $_env = undef;
356 sub get {
357 my ($k) = @_;
358 $_env = query_installation_environment() if !defined($_env);
359 return defined($k) ? $_env->{$k} : $_env;
360 }
361
362 1;