]> git.proxmox.com Git - pve-installer.git/blob - Proxmox/Install/RunEnv.pm
run env: retrieve and store hostname from DHCP lease if available
[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::File qw(file_read_firstline);
11 use Proxmox::Sys::Block;
12 use Proxmox::Sys::Net;
13
14 use Proxmox::Install::ISOEnv;
15
16 my sub fromjs : prototype($) {
17 return from_json($_[0], { utf8 => 1 });
18 }
19
20 my $mem_total = undef;
21 sub query_total_memory : prototype() {
22 return $mem_total if defined($mem_total);
23
24 open (my $MEMINFO, '<', '/proc/meminfo');
25
26 my $res = 512; # default to 512 if something goes wrong
27 while (my $line = <$MEMINFO>) {
28 if ($line =~ m/^MemTotal:\s+(\d+)\s*kB/i) {
29 $res = int ($1 / 1024);
30 }
31 }
32 close($MEMINFO);
33
34 $mem_total = $res;
35 return $mem_total;
36 }
37
38 my $cpu_hvm_support = undef;
39 sub query_cpu_hvm_support : prototype() {
40 return $cpu_hvm_support if defined($cpu_hvm_support);
41
42 open (my $CPUINFO, '<', '/proc/cpuinfo');
43
44 my $res = 0;
45 while (my $line = <$CPUINFO>) {
46 if ($line =~ /^flags\s*:.*(vmx|svm)/m) {
47 $res = 1;
48 last;
49 }
50 }
51 close($CPUINFO);
52
53 $cpu_hvm_support = $res;
54 return $cpu_hvm_support;
55 }
56
57 # Returns a hash.
58 #
59 # {
60 # <ifname> => {
61 # mac => <mac address>,
62 # index => <index>,
63 # name => <ifname>,
64 # addresses => [
65 # family => <inet|inet6>,
66 # address => <mac address>,
67 # prefix => <length>,
68 # ],
69 # },
70 # }
71 my sub query_netdevs : prototype() {
72 my $ifs = {};
73 my $default;
74
75 # FIXME: not the same as the battle proven way we used in the installer for years?
76 my $interfaces = fromjs(qx/ip --json address show/);
77
78 for my $if (@$interfaces) {
79 my ($index, $name, $state, $mac, $addresses) =
80 $if->@{qw(ifindex ifname operstate address addr_info)};
81
82 next if !$name || $name eq 'lo'; # could also check flags for LOOPBACK..
83
84 my @valid_addrs;
85 if (uc($state) eq 'UP') {
86 for my $addr (@$addresses) {
87 next if $addr->{scope} eq 'link';
88
89 my ($family, $addr, $prefix) = $addr->@{qw(family local prefixlen)};
90
91 push @valid_addrs, {
92 family => $family,
93 address => $addr,
94 prefix => $prefix,
95 };
96 }
97 }
98
99 $ifs->{$name} = {
100 index => $index,
101 name => $name,
102 mac => $mac,
103 state => uc($state),
104 };
105 $ifs->{$name}->{addresses} = \@valid_addrs if @valid_addrs;
106 }
107
108 return $ifs;
109 }
110
111 # Returns a hash.
112 #
113 # {
114 # gateway4 => {
115 # dst => "default",
116 # gateway => <ipv4>,
117 # dev => <ifname>,
118 # },
119 # gateway6 => {
120 # dst => "default",
121 # gateway => <ipv6>,
122 # dev => <ifname>,
123 # },
124 # }
125 my sub query_routes : prototype() {
126 my ($gateway4, $gateway6);
127
128 log_info("query routes");
129 my $route4 = fromjs(qx/ip -4 --json route show/);
130 for my $route (@$route4) {
131 if ($route->{dst} eq 'default') {
132 $gateway4 = {
133 dev => $route->{dev},
134 gateway => $route->{gateway},
135 };
136 last;
137 }
138 }
139
140 my $route6 = fromjs(qx/ip -6 --json route show/);
141 for my $route (@$route6) {
142 if ($route->{dst} eq 'default') {
143 $gateway6 = {
144 dev => $route->{dev},
145 gateway => $route->{gateway},
146 };
147 last;
148 }
149 }
150
151 my $routes;
152 $routes->{gateway4} = $gateway4 if $gateway4;
153 $routes->{gateway6} = $gateway6 if $gateway6;
154
155 return $routes;
156 }
157
158 # If `/etc/resolv.conf` fails to open this returns nothing.
159 # Otherwise it returns a hash:
160 # {
161 # dns => <first dns entry>,
162 #
163 my sub query_dns : prototype() {
164 log_info("query DNS from resolv.conf (managed by DHCP client)");
165 open my $fh , '<', '/etc/resolv.conf' or return;
166
167 my @dns;
168 my $domain;
169 while (defined(my $line = <$fh>)) {
170 if ($line =~ /^nameserver\s+(\S+)/) {
171 push @dns, $1;
172 } elsif (!defined($domain) && $line =~ /^domain\s+(\S+)/) {
173 $domain = $1;
174 }
175 }
176
177 my $output = {
178 domain => $domain,
179 @dns ? (dns => \@dns) : (),
180 };
181 };
182
183 # Uses `traceroute` and `geoiplookup`/`geoiplookup6` to figure out the current country.
184 # Has a 10s timeout and uses the stops at the first entry found in the geoip database.
185 my sub detect_country_tracing_to : prototype($$) {
186 my ($ipver, $destination) = @_;
187
188 print STDERR "trying to detect country...\n";
189 open(my $TRACEROUTE_FH, '-|', 'traceroute', "-$ipver", '-N', '1', '-q', '1', '-n', $destination)
190 or return undef;
191
192 my $geoip_bin = ($ipver == 6) ? 'geoiplookup6' : 'geoiplookup';
193
194 my $country;
195
196 my $previous_alarm = alarm (10);
197 eval {
198 local $SIG{ALRM} = sub { die "timed out!\n" };
199 my $line;
200 while (defined ($line = <$TRACEROUTE_FH>)) {
201 log_debug("DC TRACEROUTE: $line");
202 if ($line =~ m/^\s*\d+\s+(\S+)\s/) {
203 my $geoip = qx/$geoip_bin $1/;
204 log_debug("DC GEOIP: $geoip");
205 if ($geoip =~ m/GeoIP Country Edition:\s*([A-Z]+),/) {
206 $country = lc ($1);
207 log_info("DC FOUND: $country\n");
208 last;
209 }
210 }
211 }
212 };
213 my $err = $@;
214 alarm ($previous_alarm);
215
216 close($TRACEROUTE_FH);
217
218 if ($err) {
219 die "unable to detect country - $err\n";
220 } elsif ($country) {
221 print STDERR "detected country: " . uc($country) . "\n";
222 }
223
224 return $country;
225 }
226
227 # Returns the entire environment as a hash.
228 # {
229 # country => <short country>,
230 # ipconf = <see Proxmox::Sys::Net::get_ip_config()>,
231 # kernel_cmdline = <contents of /proc/cmdline>,
232 # total_memory = <memory size in MiB>,
233 # hvm_supported = <1 if the CPU supports hardware-accelerated virtualization>,
234 # boot_type = <either 'efi' or 'bios'>,
235 # disks => <see Proxmox::Sys::Block::hd_list()>,
236 # network => {
237 # interfaces => <see query_netdevs()>,
238 # routes => <see query_routes()>,
239 # dns => <see query_dns()>,
240 # },
241 # }
242 sub query_installation_environment : prototype() {
243 # check first if somebody already cached this for us and re-use that
244 my $run_env_file = Proxmox::Install::ISOEnv::get('run-env-cache-file');
245 if (-f "$run_env_file") {
246 log_info("re-using cached runtime env from $run_env_file");
247 my $cached_env = eval {
248 my $run_env_raw = Proxmox::Sys::File::file_read_all($run_env_file);
249 return fromjs($run_env_raw); # returns from eval
250 };
251 log_error("failed to parse cached runtime env - $@") if $@;
252 return $cached_env if defined($cached_env) && scalar keys $cached_env->%*;
253 log_warn("cached runtime env seems empty, query everything (again)");
254 }
255 # else re-query everything
256 my $output = {};
257
258 my $routes = query_routes();
259
260 log_info("query block devices");
261 $output->{disks} = Proxmox::Sys::Block::get_cached_disks();
262 $output->{network} = {
263 interfaces => query_netdevs(),
264 routes => $routes,
265 dns => query_dns(),
266 };
267
268 # Cannot be put directly in the above hash as it might return undef ..
269 $output->{network}->{hostname} = Proxmox::Sys::Net::get_dhcp_hostname();
270
271 # FIXME: move whatever makes sense over to Proxmox::Sys::Net:: and keep that as single source,
272 # it can then use some different structure just fine (after adapting the GTK GUI to that) but
273 # **never** to (slightly different!) things for the same stuff...
274 $output->{ipconf} = Proxmox::Sys::Net::get_ip_config();
275
276 $output->{kernel_cmdline} = file_read_firstline("/proc/cmdline");
277 $output->{total_memory} = query_total_memory();
278 $output->{hvm_supported} = query_cpu_hvm_support();
279 $output->{boot_type} = -d '/sys/firmware/efi' ? 'efi' : 'bios';
280
281 my $err;
282 my $country;
283 if ($routes->{gateway4}) {
284 log_info("trace country via IPv4");
285 $country = eval { detect_country_tracing_to(4 => '8.8.8.8') };
286 $err = $@ if !$country;
287 }
288
289 if (!$country && $routes->{gateway6}) {
290 log_info("trace country via IPv6");
291 $country = eval { detect_country_tracing_to(6 => '2001:4860:4860::8888') };
292 $err = $@ if !$country;
293 }
294
295 if (defined($country)) {
296 $output->{country} = $country;
297 } else {
298 warn ($err // "unable to detect country\n");
299 }
300
301 return $output;
302 }
303
304 my $_env = undef;
305 sub get {
306 my ($k) = @_;
307 $_env = query_installation_environment() if !defined($_env);
308 return defined($k) ? $_env->{$k} : $_env;
309 }
310
311 1;