blob: c5da8f2c233653a5ab44c2e5cfb7d9321cb9eaf4 [file] [log] [blame]
akutzdd794a42018-09-18 10:04:21 -05001# Cloud-Init Datasource for VMware Guestinfo
2#
3# Copyright (c) 2018 VMware, Inc. All Rights Reserved.
4#
5# This product is licensed to you under the Apache 2.0 license (the "License").
6# You may not use this product except in compliance with the Apache 2.0 License.
7#
8# This product may include a number of subcomponents with separate copyright
9# notices and license terms. Your use of these subcomponents is subject to the
10# terms and conditions of the subcomponent's license, as noted in the LICENSE
11# file.
akutz77457a62018-08-22 16:07:21 -050012#
akutz6501f902018-08-24 12:19:05 -050013# Authors: Anish Swaminathan <anishs@vmware.com>
14# Andrew Kutz <akutz@vmware.com>
akutz77457a62018-08-22 16:07:21 -050015#
akutz0d1fce52019-06-01 18:54:29 -050016
17'''
18A cloud init datasource for VMware GuestInfo.
19'''
20
akutz77457a62018-08-22 16:07:21 -050021import base64
akutzffc4dd52019-06-02 11:34:55 -050022import collections
akutz84389c82019-06-02 19:24:38 -050023import copy
akutz0d1fce52019-06-01 18:54:29 -050024from distutils.spawn import find_executable
akutzffc4dd52019-06-02 11:34:55 -050025import json
akutz10cd1402019-10-23 18:06:39 -050026import os
akutzffc4dd52019-06-02 11:34:55 -050027import socket
akutz10cd1402019-10-23 18:06:39 -050028import string
akutzffc4dd52019-06-02 11:34:55 -050029import zlib
akutz77457a62018-08-22 16:07:21 -050030
31from cloudinit import log as logging
32from cloudinit import sources
33from cloudinit import util
akutz6501f902018-08-24 12:19:05 -050034from cloudinit import safeyaml
akutz77457a62018-08-22 16:07:21 -050035
akutzffc4dd52019-06-02 11:34:55 -050036from deepmerge import always_merger
37import netifaces
38
akutz77457a62018-08-22 16:07:21 -050039LOG = logging.getLogger(__name__)
akutz0d1fce52019-06-01 18:54:29 -050040NOVAL = "No value found"
yvespp8d58dfd2019-10-29 20:42:09 +010041VMWARE_RPCTOOL = find_executable("vmware-rpctool")
akutz10cd1402019-10-23 18:06:39 -050042VMX_GUESTINFO = "VMX_GUESTINFO"
akutzdaf81f12019-12-08 11:20:33 -060043GUESTINFO_EMPTY_YAML_VAL = "---"
akutz909cf9a2019-12-09 09:17:00 -060044LOCAL_IPV4 = 'local-ipv4'
45LOCAL_IPV6 = 'local-ipv6'
akutz77457a62018-08-22 16:07:21 -050046
akutz0d1fce52019-06-01 18:54:29 -050047
48class NetworkConfigError(Exception):
49 '''
50 NetworkConfigError is raised when there is an issue getting or
51 applying network configuration.
52 '''
53 pass
54
55
Andrew Kutz4f66b8b2018-09-16 18:28:59 -050056class DataSourceVMwareGuestInfo(sources.DataSource):
akutz0d1fce52019-06-01 18:54:29 -050057 '''
58 This cloud-init datasource was designed for use with CentOS 7,
59 which uses cloud-init 0.7.9. However, this datasource should
60 work with any Linux distribution for which cloud-init is
61 avaialble.
62
63 The documentation for cloud-init 0.7.9's datasource is
64 available at http://bit.ly/cloudinit-datasource-0-7-9. The
65 current documentation for cloud-init is found at
66 https://cloudinit.readthedocs.io/en/latest/.
67
68 Setting the hostname:
69 The hostname is set by way of the metadata key "local-hostname".
70
71 Setting the instance ID:
72 The instance ID may be set by way of the metadata key "instance-id".
73 However, if this value is absent then then the instance ID is
74 read from the file /sys/class/dmi/id/product_uuid.
75
76 Configuring the network:
77 The network is configured by setting the metadata key "network"
78 with a value consistent with Network Config Versions 1 or 2,
79 depending on the Linux distro's version of cloud-init:
80
81 Network Config Version 1 - http://bit.ly/cloudinit-net-conf-v1
82 Network Config Version 2 - http://bit.ly/cloudinit-net-conf-v2
83
84 For example, CentOS 7's official cloud-init package is version
85 0.7.9 and does not support Network Config Version 2. However,
86 this datasource still supports supplying Network Config Version 2
87 data as long as the Linux distro's cloud-init package is new
88 enough to parse the data.
89
90 The metadata key "network.encoding" may be used to indicate the
91 format of the metadata key "network". Valid encodings are base64
92 and gzip+base64.
93 '''
94
95 dsname = 'VMwareGuestInfo'
96
akutz77457a62018-08-22 16:07:21 -050097 def __init__(self, sys_cfg, distro, paths, ud_proc=None):
98 sources.DataSource.__init__(self, sys_cfg, distro, paths, ud_proc)
akutz10cd1402019-10-23 18:06:39 -050099 if not get_data_access_method():
yvespp8d58dfd2019-10-29 20:42:09 +0100100 LOG.error("Failed to find vmware-rpctool")
akutz77457a62018-08-22 16:07:21 -0500101
102 def get_data(self):
akutz0d1fce52019-06-01 18:54:29 -0500103 """
104 This method should really be _get_data in accordance with the most
105 recent versions of cloud-init. However, because the datasource
106 supports as far back as cloud-init 0.7.9, get_data is still used.
107
108 Because of this the method attempts to do some of the same things
109 that the get_data functions in newer versions of cloud-init do,
110 such as calling persist_instance_data.
111 """
akutzdbce3d92019-12-08 00:09:11 -0600112 data_access_method = get_data_access_method()
113 if not data_access_method:
yvespp8d58dfd2019-10-29 20:42:09 +0100114 LOG.error("vmware-rpctool is required to fetch guestinfo value")
akutz77457a62018-08-22 16:07:21 -0500115 return False
akutz6501f902018-08-24 12:19:05 -0500116
akutz0d1fce52019-06-01 18:54:29 -0500117 # Get the metadata.
118 self.metadata = load_metadata()
akutz6501f902018-08-24 12:19:05 -0500119
akutz0d1fce52019-06-01 18:54:29 -0500120 # Get the user data.
121 self.userdata_raw = guestinfo('userdata')
akutz6501f902018-08-24 12:19:05 -0500122
akutz0d1fce52019-06-01 18:54:29 -0500123 # Get the vendor data.
124 self.vendordata_raw = guestinfo('vendordata')
akutz6501f902018-08-24 12:19:05 -0500125
akutzdbce3d92019-12-08 00:09:11 -0600126 # Check to see if any of the guestinfo data should be removed.
127 if data_access_method == VMWARE_RPCTOOL:
128 clear_guestinfo_keys(self.metadata['cleanup-guestinfo'])
129
Keerthana Kf0d27ea2019-09-05 21:46:31 +0530130 if self.metadata or self.userdata_raw or self.vendordata_raw:
131 return True
132 else:
133 return False
akutz77457a62018-08-22 16:07:21 -0500134
akutz0d1fce52019-06-01 18:54:29 -0500135 def setup(self, is_new_instance):
136 """setup(is_new_instance)
137
138 This is called before user-data and vendor-data have been processed.
139
140 Unless the datasource has set mode to 'local', then networking
141 per 'fallback' or per 'network_config' will have been written and
142 brought up the OS at this point.
143 """
144
akutzffc4dd52019-06-02 11:34:55 -0500145 # Get information about the host.
akutz0d1fce52019-06-01 18:54:29 -0500146 host_info = get_host_info()
147 LOG.info("got host-info: %s", host_info)
akutzffc4dd52019-06-02 11:34:55 -0500148
akutz909cf9a2019-12-09 09:17:00 -0600149 # Reflect any possible local IPv4 or IPv6 addresses in the guest
150 # info.
151 advertise_local_ip_addrs(host_info)
152
akutzffc4dd52019-06-02 11:34:55 -0500153 # Ensure the metadata gets updated with information about the
154 # host, including the network interfaces, default IP addresses,
155 # etc.
156 self.metadata = always_merger.merge(self.metadata, host_info)
akutz0d1fce52019-06-01 18:54:29 -0500157
158 # Persist the instance data for versions of cloud-init that support
159 # doing so. This occurs here rather than in the get_data call in
160 # order to ensure that the network interfaces are up and can be
161 # persisted with the metadata.
162 try:
163 self.persist_instance_data()
164 except AttributeError:
165 pass
166
akutz6501f902018-08-24 12:19:05 -0500167 @property
168 def network_config(self):
akutz0d1fce52019-06-01 18:54:29 -0500169 if 'network' in self.metadata:
170 LOG.debug("using metadata network config")
171 else:
172 LOG.debug("using fallback network config")
173 self.metadata['network'] = {
174 'config': self.distro.generate_fallback_config(),
175 }
176 return self.metadata['network']['config']
akutz77457a62018-08-22 16:07:21 -0500177
178 def get_instance_id(self):
akutz6501f902018-08-24 12:19:05 -0500179 # Pull the instance ID out of the metadata if present. Otherwise
180 # read the file /sys/class/dmi/id/product_uuid for the instance ID.
181 if self.metadata and 'instance-id' in self.metadata:
182 return self.metadata['instance-id']
akutz77457a62018-08-22 16:07:21 -0500183 with open('/sys/class/dmi/id/product_uuid', 'r') as id_file:
Andrey Klimentyeve1c5ed42019-10-09 12:32:44 +0300184 self.metadata['instance-id'] = str(id_file.read()).rstrip().lower()
akutz0d1fce52019-06-01 18:54:29 -0500185 return self.metadata['instance-id']
akutz77457a62018-08-22 16:07:21 -0500186
Andrey Klimentyeva23229d2019-10-17 15:30:00 +0300187 def get_public_ssh_keys(self):
188 public_keys_data = ""
189 if 'public-keys-data' in self.metadata:
190 public_keys_data = self.metadata['public-keys-data'].splitlines()
191
192 public_keys = []
193 if not public_keys_data:
194 return public_keys
195
196 for public_key in public_keys_data:
197 public_keys.append(public_key)
198
199 return public_keys
200
akutz6501f902018-08-24 12:19:05 -0500201
akutz0d1fce52019-06-01 18:54:29 -0500202def decode(key, enc_type, data):
203 '''
204 decode returns the decoded string value of data
205 key is a string used to identify the data being decoded in log messages
206 ----
207 In py 2.7:
208 json.loads method takes string as input
209 zlib.decompress takes and returns a string
210 base64.b64decode takes and returns a string
211 -----
212 In py 3.6 and newer:
213 json.loads method takes bytes or string as input
214 zlib.decompress takes and returns a bytes
215 base64.b64decode takes bytes or string and returns bytes
216 -----
217 In py > 3, < 3.6:
218 json.loads method takes string as input
219 zlib.decompress takes and returns a bytes
220 base64.b64decode takes bytes or string and returns bytes
221 -----
222 Given the above conditions the output from zlib.decompress and
223 base64.b64decode would be bytes with newer python and str in older
224 version. Thus we would covert the output to str before returning
225 '''
226 LOG.debug("Getting encoded data for key=%s, enc=%s", key, enc_type)
akutz6501f902018-08-24 12:19:05 -0500227
akutz0d1fce52019-06-01 18:54:29 -0500228 raw_data = None
229 if enc_type == "gzip+base64" or enc_type == "gz+b64":
230 LOG.debug("Decoding %s format %s", enc_type, key)
231 raw_data = zlib.decompress(base64.b64decode(data), zlib.MAX_WBITS | 16)
232 elif enc_type == "base64" or enc_type == "b64":
233 LOG.debug("Decoding %s format %s", enc_type, key)
234 raw_data = base64.b64decode(data)
235 else:
236 LOG.debug("Plain-text data %s", key)
237 raw_data = data
Sidharth Surana3a421682018-10-10 15:42:08 -0700238
akutz0d1fce52019-06-01 18:54:29 -0500239 if isinstance(raw_data, bytes):
240 return raw_data.decode('utf-8')
241 return raw_data
242
243
akutzdaf81f12019-12-08 11:20:33 -0600244def get_none_if_empty_val(val):
245 '''
246 get_none_if_empty_val returns None if the provided value, once stripped
247 of its trailing whitespace, is empty or equal to GUESTINFO_EMPTY_YAML_VAL.
248
249 The return value is always a string, regardless of whether the input is
250 a bytes class or a string.
251 '''
252
253 # If the provided value is a bytes class, convert it to a string to
254 # simplify the rest of this function's logic.
255 if isinstance(val, bytes):
256 val = val.decode()
257
258 val = val.rstrip()
259 if len(val) == 0 or val == GUESTINFO_EMPTY_YAML_VAL:
260 return None
261 return val
262
263
akutz909cf9a2019-12-09 09:17:00 -0600264def advertise_local_ip_addrs(host_info):
265 '''
266 advertise_local_ip_addrs gets the local IP address information from
267 the provided host_info map and sets the addresses in the guestinfo
268 namespace
269 '''
270 if not host_info:
271 return
272
273 # Reflect any possible local IPv4 or IPv6 addresses in the guest
274 # info.
275 local_ipv4 = host_info[LOCAL_IPV4]
276 if local_ipv4:
277 set_guestinfo_value(LOCAL_IPV4, local_ipv4)
278 LOG.info("advertised local ipv4 address %s in guestinfo", local_ipv4)
279
280 local_ipv6 = host_info[LOCAL_IPV6]
281 if local_ipv6:
282 set_guestinfo_value(LOCAL_IPV6, local_ipv6)
283 LOG.info("advertised local ipv6 address %s in guestinfo", local_ipv6)
284
285
akutzdaf81f12019-12-08 11:20:33 -0600286def handle_returned_guestinfo_val(key, val):
287 '''
288 handle_returned_guestinfo_val returns the provided value if it is
289 not empty or set to GUESTINFO_EMPTY_YAML_VAL, otherwise None is
290 returned
291 '''
292 val = get_none_if_empty_val(val)
293 if val:
294 return val
295 LOG.debug("No value found for key %s", key)
296 return None
297
298
akutz0d1fce52019-06-01 18:54:29 -0500299def get_guestinfo_value(key):
300 '''
301 Returns a guestinfo value for the specified key.
302 '''
303 LOG.debug("Getting guestinfo value for key %s", key)
akutz10cd1402019-10-23 18:06:39 -0500304
305 data_access_method = get_data_access_method()
306
307 if data_access_method == VMX_GUESTINFO:
308 env_key = ("vmx.guestinfo." + key).upper().replace(".", "_", -1)
akutzdaf81f12019-12-08 11:20:33 -0600309 return handle_returned_guestinfo_val(key, os.environ.get(env_key, ""))
akutz10cd1402019-10-23 18:06:39 -0500310
yvespp8d58dfd2019-10-29 20:42:09 +0100311 if data_access_method == VMWARE_RPCTOOL:
akutz10cd1402019-10-23 18:06:39 -0500312 try:
313 (stdout, stderr) = util.subp(
yvespp8d58dfd2019-10-29 20:42:09 +0100314 [VMWARE_RPCTOOL, "info-get guestinfo." + key])
akutz10cd1402019-10-23 18:06:39 -0500315 if stderr == NOVAL:
316 LOG.debug("No value found for key %s", key)
317 elif not stdout:
318 LOG.error("Failed to get guestinfo value for key %s", key)
319 else:
akutzdaf81f12019-12-08 11:20:33 -0600320 return handle_returned_guestinfo_val(key, stdout)
akutz10cd1402019-10-23 18:06:39 -0500321 except util.ProcessExecutionError as error:
322 if error.stderr == NOVAL:
323 LOG.debug("No value found for key %s", key)
324 else:
325 util.logexc(
326 LOG, "Failed to get guestinfo value for key %s: %s", key, error)
327 except Exception:
akutz0d1fce52019-06-01 18:54:29 -0500328 util.logexc(
akutz10cd1402019-10-23 18:06:39 -0500329 LOG, "Unexpected error while trying to get guestinfo value for key %s", key)
330
akutz0d1fce52019-06-01 18:54:29 -0500331 return None
akutz6501f902018-08-24 12:19:05 -0500332
akutz0d1fce52019-06-01 18:54:29 -0500333
akutzdbce3d92019-12-08 00:09:11 -0600334def set_guestinfo_value(key, value):
335 '''
336 Sets a guestinfo value for the specified key. Set value to an empty string
337 to clear an existing guestinfo key.
338 '''
339
340 # If value is an empty string then set it to a single space as it is not
341 # possible to set a guestinfo key to an empty string. Setting a guestinfo
342 # key to a single space is as close as it gets to clearing an existing
343 # guestinfo key.
344 if value == "":
345 value = " "
346
347 LOG.debug("Setting guestinfo key=%s to value=%s", key, value)
348
349 data_access_method = get_data_access_method()
350
351 if data_access_method == VMX_GUESTINFO:
352 return True
353
354 if data_access_method == VMWARE_RPCTOOL:
355 try:
356 util.subp(
357 [VMWARE_RPCTOOL, ("info-set guestinfo.%s %s" % (key, value))])
358 return True
359 except util.ProcessExecutionError as error:
360 util.logexc(
361 LOG, "Failed to set guestinfo key=%s to value=%s: %s", key, value, error)
362 except Exception:
363 util.logexc(
364 LOG, "Unexpected error while trying to set guestinfo key=%s to value=%s", key, value)
365
366 return None
367
368
369def clear_guestinfo_keys(keys):
370 '''
akutzdaf81f12019-12-08 11:20:33 -0600371 clear_guestinfo_keys clears guestinfo of all of the keys in the given list.
372 each key will have its value set to "---". Since the value is valid YAML,
373 cloud-init can still read it if it tries.
akutzdbce3d92019-12-08 00:09:11 -0600374 '''
375 if not keys:
376 return
akutzdaf81f12019-12-08 11:20:33 -0600377 if not type(keys) in (list, tuple):
378 keys = [keys]
akutzdbce3d92019-12-08 00:09:11 -0600379 for key in keys:
akutzdaf81f12019-12-08 11:20:33 -0600380 LOG.info("clearing guestinfo.%s", key)
381 if not set_guestinfo_value(key, GUESTINFO_EMPTY_YAML_VAL):
382 LOG.error("failed to clear guestinfo.%s", key)
383 LOG.info("clearing guestinfo.%s.encoding", key)
384 if not set_guestinfo_value(key + ".encoding", ""):
385 LOG.error("failed to clear guestinfo.%s.encoding", key)
akutzdbce3d92019-12-08 00:09:11 -0600386
387
akutz0d1fce52019-06-01 18:54:29 -0500388def guestinfo(key):
389 '''
390 guestinfo returns the guestinfo value for the provided key, decoding
391 the value when required
392 '''
393 data = get_guestinfo_value(key)
394 if not data:
akutz6501f902018-08-24 12:19:05 -0500395 return None
akutz0d1fce52019-06-01 18:54:29 -0500396 enc_type = get_guestinfo_value(key + '.encoding')
397 return decode('guestinfo.' + key, enc_type, data)
398
399
400def load(data):
401 '''
402 load first attempts to unmarshal the provided data as JSON, and if
403 that fails then attempts to unmarshal the data as YAML. If data is
404 None then a new dictionary is returned.
405 '''
406 if not data:
407 return {}
408 try:
409 return json.loads(data)
410 except:
411 return safeyaml.load(data)
412
413
414def load_metadata():
415 '''
416 load_metadata loads the metadata from the guestinfo data, optionally
417 decoding the network config when required
418 '''
419 data = load(guestinfo('metadata'))
akutz84389c82019-06-02 19:24:38 -0500420 LOG.debug('loaded metadata %s', data)
akutz0d1fce52019-06-01 18:54:29 -0500421
422 network = None
423 if 'network' in data:
424 network = data['network']
425 del data['network']
426
427 network_enc = None
428 if 'network.encoding' in data:
429 network_enc = data['network.encoding']
430 del data['network.encoding']
431
432 if network:
akutz84389c82019-06-02 19:24:38 -0500433 LOG.debug('network data found')
434 if isinstance(network, collections.Mapping):
435 LOG.debug("network data copied to 'config' key")
436 network = {
437 'config': copy.deepcopy(network)
438 }
439 else:
440 LOG.debug("network data to be decoded %s", network)
akutz0d1fce52019-06-01 18:54:29 -0500441 dec_net = decode('metadata.network', network_enc, network)
akutz84389c82019-06-02 19:24:38 -0500442 network = {
443 'config': load(dec_net),
444 }
445
446 LOG.debug('network data %s', network)
akutz0d1fce52019-06-01 18:54:29 -0500447 data['network'] = network
448
449 return data
450
akutz6501f902018-08-24 12:19:05 -0500451
akutz77457a62018-08-22 16:07:21 -0500452def get_datasource_list(depends):
akutz0d1fce52019-06-01 18:54:29 -0500453 '''
akutz77457a62018-08-22 16:07:21 -0500454 Return a list of data sources that match this set of dependencies
akutz0d1fce52019-06-01 18:54:29 -0500455 '''
Andrew Kutz4f66b8b2018-09-16 18:28:59 -0500456 return [DataSourceVMwareGuestInfo]
akutz0d1fce52019-06-01 18:54:29 -0500457
458
akutzffc4dd52019-06-02 11:34:55 -0500459def get_default_ip_addrs():
460 '''
461 Returns the default IPv4 and IPv6 addresses based on the device(s) used for
462 the default route. Please note that None may be returned for either address
463 family if that family has no default route or if there are multiple
464 addresses associated with the device used by the default route for a given
465 address.
466 '''
467 gateways = netifaces.gateways()
468 if 'default' not in gateways:
469 return None, None
470
471 default_gw = gateways['default']
472 if netifaces.AF_INET not in default_gw and netifaces.AF_INET6 not in default_gw:
473 return None, None
474
475 ipv4 = None
476 ipv6 = None
477
478 gw4 = default_gw.get(netifaces.AF_INET)
479 if gw4:
480 _, dev4 = gw4
akutz0b519f72019-06-02 14:58:57 -0500481 addr4_fams = netifaces.ifaddresses(dev4)
482 if addr4_fams:
483 af_inet4 = addr4_fams.get(netifaces.AF_INET)
484 if af_inet4:
485 if len(af_inet4) > 1:
akutzffc4dd52019-06-02 11:34:55 -0500486 LOG.warn(
akutz0b519f72019-06-02 14:58:57 -0500487 "device %s has more than one ipv4 address: %s", dev4, af_inet4)
488 elif 'addr' in af_inet4[0]:
489 ipv4 = af_inet4[0]['addr']
akutzffc4dd52019-06-02 11:34:55 -0500490
491 # Try to get the default IPv6 address by first seeing if there is a default
akutz0b519f72019-06-02 14:58:57 -0500492 # IPv6 route.
akutzffc4dd52019-06-02 11:34:55 -0500493 gw6 = default_gw.get(netifaces.AF_INET6)
494 if gw6:
495 _, dev6 = gw6
496 addr6_fams = netifaces.ifaddresses(dev6)
497 if addr6_fams:
498 af_inet6 = addr6_fams.get(netifaces.AF_INET6)
499 if af_inet6:
500 if len(af_inet6) > 1:
501 LOG.warn(
502 "device %s has more than one ipv6 address: %s", dev6, af_inet6)
503 elif 'addr' in af_inet6[0]:
504 ipv6 = af_inet6[0]['addr']
akutz0b519f72019-06-02 14:58:57 -0500505
506 # If there is a default IPv4 address but not IPv6, then see if there is a
507 # single IPv6 address associated with the same device associated with the
508 # default IPv4 address.
509 if ipv4 and not ipv6:
510 af_inet6 = addr4_fams.get(netifaces.AF_INET6)
akutzffc4dd52019-06-02 11:34:55 -0500511 if af_inet6:
512 if len(af_inet6) > 1:
513 LOG.warn(
514 "device %s has more than one ipv6 address: %s", dev4, af_inet6)
515 elif 'addr' in af_inet6[0]:
516 ipv6 = af_inet6[0]['addr']
517
akutz0b519f72019-06-02 14:58:57 -0500518 # If there is a default IPv6 address but not IPv4, then see if there is a
519 # single IPv4 address associated with the same device associated with the
520 # default IPv6 address.
521 if not ipv4 and ipv6:
522 af_inet4 = addr6_fams.get(netifaces.AF_INET4)
523 if af_inet4:
524 if len(af_inet4) > 1:
525 LOG.warn(
526 "device %s has more than one ipv4 address: %s", dev6, af_inet4)
527 elif 'addr' in af_inet4[0]:
528 ipv4 = af_inet4[0]['addr']
529
akutzffc4dd52019-06-02 11:34:55 -0500530 return ipv4, ipv6
531
Keerthana Kf0d27ea2019-09-05 21:46:31 +0530532# patched socket.getfqdn() - see https://bugs.python.org/issue5004
akutz10cd1402019-10-23 18:06:39 -0500533
534
Keerthana Kf0d27ea2019-09-05 21:46:31 +0530535def getfqdn(name=''):
536 """Get fully qualified domain name from name.
537 An empty argument is interpreted as meaning the local host.
538 """
539 name = name.strip()
540 if not name or name == '0.0.0.0':
541 name = socket.gethostname()
542 try:
akutz10cd1402019-10-23 18:06:39 -0500543 addrs = socket.getaddrinfo(
544 name, None, 0, socket.SOCK_DGRAM, 0, socket.AI_CANONNAME)
Keerthana Kf0d27ea2019-09-05 21:46:31 +0530545 except socket.error:
546 pass
547 else:
548 for addr in addrs:
549 if addr[3]:
550 name = addr[3]
551 break
552 return name
akutzffc4dd52019-06-02 11:34:55 -0500553
akutz10cd1402019-10-23 18:06:39 -0500554
akutz0d1fce52019-06-01 18:54:29 -0500555def get_host_info():
556 '''
557 Returns host information such as the host name and network interfaces.
558 '''
akutz0d1fce52019-06-01 18:54:29 -0500559
560 host_info = {
561 'network': {
562 'interfaces': {
563 'by-mac': collections.OrderedDict(),
akutzffc4dd52019-06-02 11:34:55 -0500564 'by-ipv4': collections.OrderedDict(),
565 'by-ipv6': collections.OrderedDict(),
akutz0d1fce52019-06-01 18:54:29 -0500566 },
567 },
568 }
569
Keerthana Kf0d27ea2019-09-05 21:46:31 +0530570 hostname = getfqdn(socket.gethostname())
akutz0d1fce52019-06-01 18:54:29 -0500571 if hostname:
akutzffc4dd52019-06-02 11:34:55 -0500572 host_info['hostname'] = hostname
akutz0d1fce52019-06-01 18:54:29 -0500573 host_info['local-hostname'] = hostname
akutzb7a193d2019-12-09 11:36:32 -0600574 host_info['local_hostname'] = hostname
akutz0d1fce52019-06-01 18:54:29 -0500575
akutzffc4dd52019-06-02 11:34:55 -0500576 default_ipv4, default_ipv6 = get_default_ip_addrs()
577 if default_ipv4:
akutz909cf9a2019-12-09 09:17:00 -0600578 host_info[LOCAL_IPV4] = default_ipv4
akutzffc4dd52019-06-02 11:34:55 -0500579 if default_ipv6:
akutz909cf9a2019-12-09 09:17:00 -0600580 host_info[LOCAL_IPV6] = default_ipv6
akutzffc4dd52019-06-02 11:34:55 -0500581
akutz0d1fce52019-06-01 18:54:29 -0500582 by_mac = host_info['network']['interfaces']['by-mac']
akutzffc4dd52019-06-02 11:34:55 -0500583 by_ipv4 = host_info['network']['interfaces']['by-ipv4']
584 by_ipv6 = host_info['network']['interfaces']['by-ipv6']
akutz0d1fce52019-06-01 18:54:29 -0500585
586 ifaces = netifaces.interfaces()
587 for dev_name in ifaces:
588 addr_fams = netifaces.ifaddresses(dev_name)
589 af_link = addr_fams.get(netifaces.AF_LINK)
akutz0b519f72019-06-02 14:58:57 -0500590 af_inet4 = addr_fams.get(netifaces.AF_INET)
akutz0d1fce52019-06-01 18:54:29 -0500591 af_inet6 = addr_fams.get(netifaces.AF_INET6)
592
593 mac = None
594 if af_link and 'addr' in af_link[0]:
595 mac = af_link[0]['addr']
596
597 # Do not bother recording localhost
598 if mac == "00:00:00:00:00:00":
599 continue
600
akutz0b519f72019-06-02 14:58:57 -0500601 if mac and (af_inet4 or af_inet6):
akutz0d1fce52019-06-01 18:54:29 -0500602 key = mac
603 val = {}
akutz0b519f72019-06-02 14:58:57 -0500604 if af_inet4:
605 val["ipv4"] = af_inet4
akutz0d1fce52019-06-01 18:54:29 -0500606 if af_inet6:
akutzffc4dd52019-06-02 11:34:55 -0500607 val["ipv6"] = af_inet6
akutz0d1fce52019-06-01 18:54:29 -0500608 by_mac[key] = val
609
akutz0b519f72019-06-02 14:58:57 -0500610 if af_inet4:
611 for ip_info in af_inet4:
akutz0d1fce52019-06-01 18:54:29 -0500612 key = ip_info['addr']
akutzffc4dd52019-06-02 11:34:55 -0500613 if key == '127.0.0.1':
614 continue
akutz84389c82019-06-02 19:24:38 -0500615 val = copy.deepcopy(ip_info)
akutz0d1fce52019-06-01 18:54:29 -0500616 del val['addr']
617 if mac:
618 val['mac'] = mac
akutzffc4dd52019-06-02 11:34:55 -0500619 by_ipv4[key] = val
akutz0d1fce52019-06-01 18:54:29 -0500620
621 if af_inet6:
622 for ip_info in af_inet6:
623 key = ip_info['addr']
akutzffc4dd52019-06-02 11:34:55 -0500624 if key == '::1':
625 continue
akutz84389c82019-06-02 19:24:38 -0500626 val = copy.deepcopy(ip_info)
akutz0d1fce52019-06-01 18:54:29 -0500627 del val['addr']
628 if mac:
629 val['mac'] = mac
akutzffc4dd52019-06-02 11:34:55 -0500630 by_ipv6[key] = val
akutz0d1fce52019-06-01 18:54:29 -0500631
632 return host_info
633
634
akutz10cd1402019-10-23 18:06:39 -0500635def get_data_access_method():
636 if os.environ.get(VMX_GUESTINFO, ""):
637 return VMX_GUESTINFO
yvespp8d58dfd2019-10-29 20:42:09 +0100638 if VMWARE_RPCTOOL:
639 return VMWARE_RPCTOOL
akutz10cd1402019-10-23 18:06:39 -0500640 return None
641
642
akutzffc4dd52019-06-02 11:34:55 -0500643def main():
644 '''
645 Executed when this file is used as a program.
646 '''
647 metadata = {'network': {'config': {'dhcp': True}}}
648 host_info = get_host_info()
649 metadata = always_merger.merge(metadata, host_info)
650 print(util.json_dumps(metadata))
651
652
akutz0d1fce52019-06-01 18:54:29 -0500653if __name__ == "__main__":
akutzffc4dd52019-06-02 11:34:55 -0500654 main()
akutz0d1fce52019-06-01 18:54:29 -0500655
656# vi: ts=4 expandtab