]>
Commit | Line | Data |
---|---|---|
2231d226 WB |
1 | package Proxmox::Install::RunEnv; |
2 | ||
3 | use strict; | |
4 | use warnings; | |
5 | ||
6 | use Carp; | |
7 | use JSON qw(from_json to_json); | |
2231d226 | 8 | |
0d27321b | 9 | use Proxmox::Log; |
8a1a7187 | 10 | use Proxmox::Sys::Command qw(run_command CMD_FINISHED); |
5364c0c1 | 11 | use Proxmox::Sys::File qw(file_read_all file_read_firstline); |
ceec3def | 12 | use Proxmox::Sys::Block; |
fe06d7e9 | 13 | use Proxmox::Sys::Net; |
2231d226 | 14 | |
e1b74696 TL |
15 | use Proxmox::Install::ISOEnv; |
16 | ||
2231d226 WB |
17 | my sub fromjs : prototype($) { |
18 | return from_json($_[0], { utf8 => 1 }); | |
19 | } | |
20 | ||
b91f9cad | 21 | my $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 |
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 | ||
4d8aec8a CH |
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 | ||
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 | # } | |
74 | my 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 | # } | |
128 | my 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 | # | |
166 | my 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. | |
188 | my 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 | # } |
247 | sub 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 | |
323 | our $ZFS_ARC_MIN_SIZE_MIB = 64; # MiB | |
324 | ||
325 | # See https://bugzilla.proxmox.com/show_bug.cgi?id=4829 | |
326 | our $ZFS_ARC_MAX_SIZE_MIB = 16 * 1024; # 16384 MiB = 16 GiB | |
327 | our $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. | |
331 | sub 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. | |
350 | sub 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 |
365 | my $_env = undef; |
366 | sub get { | |
367 | my ($k) = @_; | |
368 | $_env = query_installation_environment() if !defined($_env); | |
369 | return defined($k) ? $_env->{$k} : $_env; | |
370 | } | |
371 | ||
2231d226 | 372 | 1; |