blob: e3136e5767b80ede257402eb5d5ae1b30d699592 [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'
Yves Peter59c869d2019-12-11 13:09:14 +010046CLEANUP_GUESTINFO = 'cleanup-guestinfo'
akutz77457a62018-08-22 16:07:21 -050047
akutz0d1fce52019-06-01 18:54:29 -050048
49class NetworkConfigError(Exception):
50 '''
51 NetworkConfigError is raised when there is an issue getting or
52 applying network configuration.
53 '''
54 pass
55
56
Andrew Kutz4f66b8b2018-09-16 18:28:59 -050057class DataSourceVMwareGuestInfo(sources.DataSource):
akutz0d1fce52019-06-01 18:54:29 -050058 '''
59 This cloud-init datasource was designed for use with CentOS 7,
60 which uses cloud-init 0.7.9. However, this datasource should
61 work with any Linux distribution for which cloud-init is
62 avaialble.
63
64 The documentation for cloud-init 0.7.9's datasource is
65 available at http://bit.ly/cloudinit-datasource-0-7-9. The
66 current documentation for cloud-init is found at
67 https://cloudinit.readthedocs.io/en/latest/.
68
69 Setting the hostname:
70 The hostname is set by way of the metadata key "local-hostname".
71
72 Setting the instance ID:
73 The instance ID may be set by way of the metadata key "instance-id".
74 However, if this value is absent then then the instance ID is
75 read from the file /sys/class/dmi/id/product_uuid.
76
77 Configuring the network:
78 The network is configured by setting the metadata key "network"
79 with a value consistent with Network Config Versions 1 or 2,
80 depending on the Linux distro's version of cloud-init:
81
82 Network Config Version 1 - http://bit.ly/cloudinit-net-conf-v1
83 Network Config Version 2 - http://bit.ly/cloudinit-net-conf-v2
84
85 For example, CentOS 7's official cloud-init package is version
86 0.7.9 and does not support Network Config Version 2. However,
87 this datasource still supports supplying Network Config Version 2
88 data as long as the Linux distro's cloud-init package is new
89 enough to parse the data.
90
91 The metadata key "network.encoding" may be used to indicate the
92 format of the metadata key "network". Valid encodings are base64
93 and gzip+base64.
94 '''
95
96 dsname = 'VMwareGuestInfo'
97
akutz77457a62018-08-22 16:07:21 -050098 def __init__(self, sys_cfg, distro, paths, ud_proc=None):
99 sources.DataSource.__init__(self, sys_cfg, distro, paths, ud_proc)
akutz10cd1402019-10-23 18:06:39 -0500100 if not get_data_access_method():
yvespp8d58dfd2019-10-29 20:42:09 +0100101 LOG.error("Failed to find vmware-rpctool")
akutz77457a62018-08-22 16:07:21 -0500102
103 def get_data(self):
akutz0d1fce52019-06-01 18:54:29 -0500104 """
105 This method should really be _get_data in accordance with the most
106 recent versions of cloud-init. However, because the datasource
107 supports as far back as cloud-init 0.7.9, get_data is still used.
108
109 Because of this the method attempts to do some of the same things
110 that the get_data functions in newer versions of cloud-init do,
111 such as calling persist_instance_data.
112 """
akutzdbce3d92019-12-08 00:09:11 -0600113 data_access_method = get_data_access_method()
114 if not data_access_method:
yvespp8d58dfd2019-10-29 20:42:09 +0100115 LOG.error("vmware-rpctool is required to fetch guestinfo value")
akutz77457a62018-08-22 16:07:21 -0500116 return False
akutz6501f902018-08-24 12:19:05 -0500117
akutz0d1fce52019-06-01 18:54:29 -0500118 # Get the metadata.
119 self.metadata = load_metadata()
akutz6501f902018-08-24 12:19:05 -0500120
akutz0d1fce52019-06-01 18:54:29 -0500121 # Get the user data.
122 self.userdata_raw = guestinfo('userdata')
akutz6501f902018-08-24 12:19:05 -0500123
akutz0d1fce52019-06-01 18:54:29 -0500124 # Get the vendor data.
125 self.vendordata_raw = guestinfo('vendordata')
akutz6501f902018-08-24 12:19:05 -0500126
akutzdbce3d92019-12-08 00:09:11 -0600127 # Check to see if any of the guestinfo data should be removed.
Yves Peter59c869d2019-12-11 13:09:14 +0100128 if data_access_method == VMWARE_RPCTOOL and CLEANUP_GUESTINFO in self.metadata:
129 clear_guestinfo_keys(self.metadata[CLEANUP_GUESTINFO])
akutzdbce3d92019-12-08 00:09:11 -0600130
Keerthana Kf0d27ea2019-09-05 21:46:31 +0530131 if self.metadata or self.userdata_raw or self.vendordata_raw:
132 return True
133 else:
134 return False
akutz77457a62018-08-22 16:07:21 -0500135
akutz0d1fce52019-06-01 18:54:29 -0500136 def setup(self, is_new_instance):
137 """setup(is_new_instance)
138
139 This is called before user-data and vendor-data have been processed.
140
141 Unless the datasource has set mode to 'local', then networking
142 per 'fallback' or per 'network_config' will have been written and
143 brought up the OS at this point.
144 """
145
akutzffc4dd52019-06-02 11:34:55 -0500146 # Get information about the host.
akutz0d1fce52019-06-01 18:54:29 -0500147 host_info = get_host_info()
148 LOG.info("got host-info: %s", host_info)
akutzffc4dd52019-06-02 11:34:55 -0500149
akutz909cf9a2019-12-09 09:17:00 -0600150 # Reflect any possible local IPv4 or IPv6 addresses in the guest
151 # info.
152 advertise_local_ip_addrs(host_info)
153
akutzffc4dd52019-06-02 11:34:55 -0500154 # Ensure the metadata gets updated with information about the
155 # host, including the network interfaces, default IP addresses,
156 # etc.
157 self.metadata = always_merger.merge(self.metadata, host_info)
akutz0d1fce52019-06-01 18:54:29 -0500158
159 # Persist the instance data for versions of cloud-init that support
160 # doing so. This occurs here rather than in the get_data call in
161 # order to ensure that the network interfaces are up and can be
162 # persisted with the metadata.
163 try:
164 self.persist_instance_data()
165 except AttributeError:
166 pass
167
akutz6501f902018-08-24 12:19:05 -0500168 @property
169 def network_config(self):
akutz0d1fce52019-06-01 18:54:29 -0500170 if 'network' in self.metadata:
171 LOG.debug("using metadata network config")
172 else:
173 LOG.debug("using fallback network config")
174 self.metadata['network'] = {
175 'config': self.distro.generate_fallback_config(),
176 }
177 return self.metadata['network']['config']
akutz77457a62018-08-22 16:07:21 -0500178
179 def get_instance_id(self):
akutz6501f902018-08-24 12:19:05 -0500180 # Pull the instance ID out of the metadata if present. Otherwise
181 # read the file /sys/class/dmi/id/product_uuid for the instance ID.
182 if self.metadata and 'instance-id' in self.metadata:
183 return self.metadata['instance-id']
akutz77457a62018-08-22 16:07:21 -0500184 with open('/sys/class/dmi/id/product_uuid', 'r') as id_file:
Andrey Klimentyeve1c5ed42019-10-09 12:32:44 +0300185 self.metadata['instance-id'] = str(id_file.read()).rstrip().lower()
akutz0d1fce52019-06-01 18:54:29 -0500186 return self.metadata['instance-id']
akutz77457a62018-08-22 16:07:21 -0500187
Andrey Klimentyeva23229d2019-10-17 15:30:00 +0300188 def get_public_ssh_keys(self):
189 public_keys_data = ""
190 if 'public-keys-data' in self.metadata:
191 public_keys_data = self.metadata['public-keys-data'].splitlines()
192
193 public_keys = []
194 if not public_keys_data:
195 return public_keys
196
197 for public_key in public_keys_data:
198 public_keys.append(public_key)
199
200 return public_keys
201
akutz6501f902018-08-24 12:19:05 -0500202
akutz0d1fce52019-06-01 18:54:29 -0500203def decode(key, enc_type, data):
204 '''
205 decode returns the decoded string value of data
206 key is a string used to identify the data being decoded in log messages
207 ----
208 In py 2.7:
209 json.loads method takes string as input
210 zlib.decompress takes and returns a string
211 base64.b64decode takes and returns a string
212 -----
213 In py 3.6 and newer:
214 json.loads method takes bytes or string as input
215 zlib.decompress takes and returns a bytes
216 base64.b64decode takes bytes or string and returns bytes
217 -----
218 In py > 3, < 3.6:
219 json.loads method takes string as input
220 zlib.decompress takes and returns a bytes
221 base64.b64decode takes bytes or string and returns bytes
222 -----
223 Given the above conditions the output from zlib.decompress and
224 base64.b64decode would be bytes with newer python and str in older
225 version. Thus we would covert the output to str before returning
226 '''
227 LOG.debug("Getting encoded data for key=%s, enc=%s", key, enc_type)
akutz6501f902018-08-24 12:19:05 -0500228
akutz0d1fce52019-06-01 18:54:29 -0500229 raw_data = None
230 if enc_type == "gzip+base64" or enc_type == "gz+b64":
231 LOG.debug("Decoding %s format %s", enc_type, key)
232 raw_data = zlib.decompress(base64.b64decode(data), zlib.MAX_WBITS | 16)
233 elif enc_type == "base64" or enc_type == "b64":
234 LOG.debug("Decoding %s format %s", enc_type, key)
235 raw_data = base64.b64decode(data)
236 else:
237 LOG.debug("Plain-text data %s", key)
238 raw_data = data
Sidharth Surana3a421682018-10-10 15:42:08 -0700239
akutz0d1fce52019-06-01 18:54:29 -0500240 if isinstance(raw_data, bytes):
241 return raw_data.decode('utf-8')
242 return raw_data
243
244
akutzdaf81f12019-12-08 11:20:33 -0600245def get_none_if_empty_val(val):
246 '''
247 get_none_if_empty_val returns None if the provided value, once stripped
248 of its trailing whitespace, is empty or equal to GUESTINFO_EMPTY_YAML_VAL.
249
250 The return value is always a string, regardless of whether the input is
251 a bytes class or a string.
252 '''
253
254 # If the provided value is a bytes class, convert it to a string to
255 # simplify the rest of this function's logic.
256 if isinstance(val, bytes):
257 val = val.decode()
258
259 val = val.rstrip()
260 if len(val) == 0 or val == GUESTINFO_EMPTY_YAML_VAL:
261 return None
262 return val
263
264
akutz909cf9a2019-12-09 09:17:00 -0600265def advertise_local_ip_addrs(host_info):
266 '''
267 advertise_local_ip_addrs gets the local IP address information from
268 the provided host_info map and sets the addresses in the guestinfo
269 namespace
270 '''
271 if not host_info:
272 return
273
274 # Reflect any possible local IPv4 or IPv6 addresses in the guest
275 # info.
276 local_ipv4 = host_info[LOCAL_IPV4]
277 if local_ipv4:
278 set_guestinfo_value(LOCAL_IPV4, local_ipv4)
279 LOG.info("advertised local ipv4 address %s in guestinfo", local_ipv4)
280
281 local_ipv6 = host_info[LOCAL_IPV6]
282 if local_ipv6:
283 set_guestinfo_value(LOCAL_IPV6, local_ipv6)
284 LOG.info("advertised local ipv6 address %s in guestinfo", local_ipv6)
285
286
akutzdaf81f12019-12-08 11:20:33 -0600287def handle_returned_guestinfo_val(key, val):
288 '''
289 handle_returned_guestinfo_val returns the provided value if it is
290 not empty or set to GUESTINFO_EMPTY_YAML_VAL, otherwise None is
291 returned
292 '''
293 val = get_none_if_empty_val(val)
294 if val:
295 return val
296 LOG.debug("No value found for key %s", key)
297 return None
298
299
akutz0d1fce52019-06-01 18:54:29 -0500300def get_guestinfo_value(key):
301 '''
302 Returns a guestinfo value for the specified key.
303 '''
304 LOG.debug("Getting guestinfo value for key %s", key)
akutz10cd1402019-10-23 18:06:39 -0500305
306 data_access_method = get_data_access_method()
307
308 if data_access_method == VMX_GUESTINFO:
309 env_key = ("vmx.guestinfo." + key).upper().replace(".", "_", -1)
akutzdaf81f12019-12-08 11:20:33 -0600310 return handle_returned_guestinfo_val(key, os.environ.get(env_key, ""))
akutz10cd1402019-10-23 18:06:39 -0500311
yvespp8d58dfd2019-10-29 20:42:09 +0100312 if data_access_method == VMWARE_RPCTOOL:
akutz10cd1402019-10-23 18:06:39 -0500313 try:
314 (stdout, stderr) = util.subp(
yvespp8d58dfd2019-10-29 20:42:09 +0100315 [VMWARE_RPCTOOL, "info-get guestinfo." + key])
akutz10cd1402019-10-23 18:06:39 -0500316 if stderr == NOVAL:
317 LOG.debug("No value found for key %s", key)
318 elif not stdout:
319 LOG.error("Failed to get guestinfo value for key %s", key)
320 else:
akutzdaf81f12019-12-08 11:20:33 -0600321 return handle_returned_guestinfo_val(key, stdout)
akutz10cd1402019-10-23 18:06:39 -0500322 except util.ProcessExecutionError as error:
323 if error.stderr == NOVAL:
324 LOG.debug("No value found for key %s", key)
325 else:
326 util.logexc(
327 LOG, "Failed to get guestinfo value for key %s: %s", key, error)
328 except Exception:
akutz0d1fce52019-06-01 18:54:29 -0500329 util.logexc(
akutz10cd1402019-10-23 18:06:39 -0500330 LOG, "Unexpected error while trying to get guestinfo value for key %s", key)
331
akutz0d1fce52019-06-01 18:54:29 -0500332 return None
akutz6501f902018-08-24 12:19:05 -0500333
akutz0d1fce52019-06-01 18:54:29 -0500334
akutzdbce3d92019-12-08 00:09:11 -0600335def set_guestinfo_value(key, value):
336 '''
337 Sets a guestinfo value for the specified key. Set value to an empty string
338 to clear an existing guestinfo key.
339 '''
340
341 # If value is an empty string then set it to a single space as it is not
342 # possible to set a guestinfo key to an empty string. Setting a guestinfo
343 # key to a single space is as close as it gets to clearing an existing
344 # guestinfo key.
345 if value == "":
346 value = " "
347
348 LOG.debug("Setting guestinfo key=%s to value=%s", key, value)
349
350 data_access_method = get_data_access_method()
351
352 if data_access_method == VMX_GUESTINFO:
353 return True
354
355 if data_access_method == VMWARE_RPCTOOL:
356 try:
357 util.subp(
358 [VMWARE_RPCTOOL, ("info-set guestinfo.%s %s" % (key, value))])
359 return True
360 except util.ProcessExecutionError as error:
361 util.logexc(
362 LOG, "Failed to set guestinfo key=%s to value=%s: %s", key, value, error)
363 except Exception:
364 util.logexc(
365 LOG, "Unexpected error while trying to set guestinfo key=%s to value=%s", key, value)
366
367 return None
368
369
370def clear_guestinfo_keys(keys):
371 '''
akutzdaf81f12019-12-08 11:20:33 -0600372 clear_guestinfo_keys clears guestinfo of all of the keys in the given list.
373 each key will have its value set to "---". Since the value is valid YAML,
374 cloud-init can still read it if it tries.
akutzdbce3d92019-12-08 00:09:11 -0600375 '''
376 if not keys:
377 return
akutzdaf81f12019-12-08 11:20:33 -0600378 if not type(keys) in (list, tuple):
379 keys = [keys]
akutzdbce3d92019-12-08 00:09:11 -0600380 for key in keys:
akutzdaf81f12019-12-08 11:20:33 -0600381 LOG.info("clearing guestinfo.%s", key)
382 if not set_guestinfo_value(key, GUESTINFO_EMPTY_YAML_VAL):
383 LOG.error("failed to clear guestinfo.%s", key)
384 LOG.info("clearing guestinfo.%s.encoding", key)
385 if not set_guestinfo_value(key + ".encoding", ""):
386 LOG.error("failed to clear guestinfo.%s.encoding", key)
akutzdbce3d92019-12-08 00:09:11 -0600387
388
akutz0d1fce52019-06-01 18:54:29 -0500389def guestinfo(key):
390 '''
391 guestinfo returns the guestinfo value for the provided key, decoding
392 the value when required
393 '''
394 data = get_guestinfo_value(key)
395 if not data:
akutz6501f902018-08-24 12:19:05 -0500396 return None
akutz0d1fce52019-06-01 18:54:29 -0500397 enc_type = get_guestinfo_value(key + '.encoding')
398 return decode('guestinfo.' + key, enc_type, data)
399
400
401def load(data):
402 '''
403 load first attempts to unmarshal the provided data as JSON, and if
404 that fails then attempts to unmarshal the data as YAML. If data is
405 None then a new dictionary is returned.
406 '''
407 if not data:
408 return {}
409 try:
410 return json.loads(data)
411 except:
412 return safeyaml.load(data)
413
414
415def load_metadata():
416 '''
417 load_metadata loads the metadata from the guestinfo data, optionally
418 decoding the network config when required
419 '''
420 data = load(guestinfo('metadata'))
akutz84389c82019-06-02 19:24:38 -0500421 LOG.debug('loaded metadata %s', data)
akutz0d1fce52019-06-01 18:54:29 -0500422
423 network = None
424 if 'network' in data:
425 network = data['network']
426 del data['network']
427
428 network_enc = None
429 if 'network.encoding' in data:
430 network_enc = data['network.encoding']
431 del data['network.encoding']
432
433 if network:
akutz84389c82019-06-02 19:24:38 -0500434 LOG.debug('network data found')
435 if isinstance(network, collections.Mapping):
436 LOG.debug("network data copied to 'config' key")
437 network = {
438 'config': copy.deepcopy(network)
439 }
440 else:
441 LOG.debug("network data to be decoded %s", network)
akutz0d1fce52019-06-01 18:54:29 -0500442 dec_net = decode('metadata.network', network_enc, network)
akutz84389c82019-06-02 19:24:38 -0500443 network = {
444 'config': load(dec_net),
445 }
446
447 LOG.debug('network data %s', network)
akutz0d1fce52019-06-01 18:54:29 -0500448 data['network'] = network
449
450 return data
451
akutz6501f902018-08-24 12:19:05 -0500452
akutz77457a62018-08-22 16:07:21 -0500453def get_datasource_list(depends):
akutz0d1fce52019-06-01 18:54:29 -0500454 '''
akutz77457a62018-08-22 16:07:21 -0500455 Return a list of data sources that match this set of dependencies
akutz0d1fce52019-06-01 18:54:29 -0500456 '''
Andrew Kutz4f66b8b2018-09-16 18:28:59 -0500457 return [DataSourceVMwareGuestInfo]
akutz0d1fce52019-06-01 18:54:29 -0500458
459
akutzffc4dd52019-06-02 11:34:55 -0500460def get_default_ip_addrs():
461 '''
462 Returns the default IPv4 and IPv6 addresses based on the device(s) used for
463 the default route. Please note that None may be returned for either address
464 family if that family has no default route or if there are multiple
465 addresses associated with the device used by the default route for a given
466 address.
467 '''
468 gateways = netifaces.gateways()
469 if 'default' not in gateways:
470 return None, None
471
472 default_gw = gateways['default']
473 if netifaces.AF_INET not in default_gw and netifaces.AF_INET6 not in default_gw:
474 return None, None
475
476 ipv4 = None
477 ipv6 = None
478
479 gw4 = default_gw.get(netifaces.AF_INET)
480 if gw4:
481 _, dev4 = gw4
akutz0b519f72019-06-02 14:58:57 -0500482 addr4_fams = netifaces.ifaddresses(dev4)
483 if addr4_fams:
484 af_inet4 = addr4_fams.get(netifaces.AF_INET)
485 if af_inet4:
486 if len(af_inet4) > 1:
akutzffc4dd52019-06-02 11:34:55 -0500487 LOG.warn(
akutz0b519f72019-06-02 14:58:57 -0500488 "device %s has more than one ipv4 address: %s", dev4, af_inet4)
489 elif 'addr' in af_inet4[0]:
490 ipv4 = af_inet4[0]['addr']
akutzffc4dd52019-06-02 11:34:55 -0500491
492 # Try to get the default IPv6 address by first seeing if there is a default
akutz0b519f72019-06-02 14:58:57 -0500493 # IPv6 route.
akutzffc4dd52019-06-02 11:34:55 -0500494 gw6 = default_gw.get(netifaces.AF_INET6)
495 if gw6:
496 _, dev6 = gw6
497 addr6_fams = netifaces.ifaddresses(dev6)
498 if addr6_fams:
499 af_inet6 = addr6_fams.get(netifaces.AF_INET6)
500 if af_inet6:
501 if len(af_inet6) > 1:
502 LOG.warn(
503 "device %s has more than one ipv6 address: %s", dev6, af_inet6)
504 elif 'addr' in af_inet6[0]:
505 ipv6 = af_inet6[0]['addr']
akutz0b519f72019-06-02 14:58:57 -0500506
507 # If there is a default IPv4 address but not IPv6, then see if there is a
508 # single IPv6 address associated with the same device associated with the
509 # default IPv4 address.
510 if ipv4 and not ipv6:
511 af_inet6 = addr4_fams.get(netifaces.AF_INET6)
akutzffc4dd52019-06-02 11:34:55 -0500512 if af_inet6:
513 if len(af_inet6) > 1:
514 LOG.warn(
515 "device %s has more than one ipv6 address: %s", dev4, af_inet6)
516 elif 'addr' in af_inet6[0]:
517 ipv6 = af_inet6[0]['addr']
518
akutz0b519f72019-06-02 14:58:57 -0500519 # If there is a default IPv6 address but not IPv4, then see if there is a
520 # single IPv4 address associated with the same device associated with the
521 # default IPv6 address.
522 if not ipv4 and ipv6:
523 af_inet4 = addr6_fams.get(netifaces.AF_INET4)
524 if af_inet4:
525 if len(af_inet4) > 1:
526 LOG.warn(
527 "device %s has more than one ipv4 address: %s", dev6, af_inet4)
528 elif 'addr' in af_inet4[0]:
529 ipv4 = af_inet4[0]['addr']
530
akutzffc4dd52019-06-02 11:34:55 -0500531 return ipv4, ipv6
532
Keerthana Kf0d27ea2019-09-05 21:46:31 +0530533# patched socket.getfqdn() - see https://bugs.python.org/issue5004
akutz10cd1402019-10-23 18:06:39 -0500534
535
Keerthana Kf0d27ea2019-09-05 21:46:31 +0530536def getfqdn(name=''):
537 """Get fully qualified domain name from name.
538 An empty argument is interpreted as meaning the local host.
539 """
540 name = name.strip()
541 if not name or name == '0.0.0.0':
542 name = socket.gethostname()
543 try:
akutz10cd1402019-10-23 18:06:39 -0500544 addrs = socket.getaddrinfo(
545 name, None, 0, socket.SOCK_DGRAM, 0, socket.AI_CANONNAME)
Keerthana Kf0d27ea2019-09-05 21:46:31 +0530546 except socket.error:
547 pass
548 else:
549 for addr in addrs:
550 if addr[3]:
551 name = addr[3]
552 break
553 return name
akutzffc4dd52019-06-02 11:34:55 -0500554
akutz10cd1402019-10-23 18:06:39 -0500555
akutz0d1fce52019-06-01 18:54:29 -0500556def get_host_info():
557 '''
558 Returns host information such as the host name and network interfaces.
559 '''
akutz0d1fce52019-06-01 18:54:29 -0500560
561 host_info = {
562 'network': {
563 'interfaces': {
564 'by-mac': collections.OrderedDict(),
akutzffc4dd52019-06-02 11:34:55 -0500565 'by-ipv4': collections.OrderedDict(),
566 'by-ipv6': collections.OrderedDict(),
akutz0d1fce52019-06-01 18:54:29 -0500567 },
568 },
569 }
570
Keerthana Kf0d27ea2019-09-05 21:46:31 +0530571 hostname = getfqdn(socket.gethostname())
akutz0d1fce52019-06-01 18:54:29 -0500572 if hostname:
akutzffc4dd52019-06-02 11:34:55 -0500573 host_info['hostname'] = hostname
akutz0d1fce52019-06-01 18:54:29 -0500574 host_info['local-hostname'] = hostname
akutzb7a193d2019-12-09 11:36:32 -0600575 host_info['local_hostname'] = hostname
akutz0d1fce52019-06-01 18:54:29 -0500576
akutzffc4dd52019-06-02 11:34:55 -0500577 default_ipv4, default_ipv6 = get_default_ip_addrs()
578 if default_ipv4:
akutz909cf9a2019-12-09 09:17:00 -0600579 host_info[LOCAL_IPV4] = default_ipv4
akutzffc4dd52019-06-02 11:34:55 -0500580 if default_ipv6:
akutz909cf9a2019-12-09 09:17:00 -0600581 host_info[LOCAL_IPV6] = default_ipv6
akutzffc4dd52019-06-02 11:34:55 -0500582
akutz0d1fce52019-06-01 18:54:29 -0500583 by_mac = host_info['network']['interfaces']['by-mac']
akutzffc4dd52019-06-02 11:34:55 -0500584 by_ipv4 = host_info['network']['interfaces']['by-ipv4']
585 by_ipv6 = host_info['network']['interfaces']['by-ipv6']
akutz0d1fce52019-06-01 18:54:29 -0500586
587 ifaces = netifaces.interfaces()
588 for dev_name in ifaces:
589 addr_fams = netifaces.ifaddresses(dev_name)
590 af_link = addr_fams.get(netifaces.AF_LINK)
akutz0b519f72019-06-02 14:58:57 -0500591 af_inet4 = addr_fams.get(netifaces.AF_INET)
akutz0d1fce52019-06-01 18:54:29 -0500592 af_inet6 = addr_fams.get(netifaces.AF_INET6)
593
594 mac = None
595 if af_link and 'addr' in af_link[0]:
596 mac = af_link[0]['addr']
597
598 # Do not bother recording localhost
599 if mac == "00:00:00:00:00:00":
600 continue
601
akutz0b519f72019-06-02 14:58:57 -0500602 if mac and (af_inet4 or af_inet6):
akutz0d1fce52019-06-01 18:54:29 -0500603 key = mac
604 val = {}
akutz0b519f72019-06-02 14:58:57 -0500605 if af_inet4:
606 val["ipv4"] = af_inet4
akutz0d1fce52019-06-01 18:54:29 -0500607 if af_inet6:
akutzffc4dd52019-06-02 11:34:55 -0500608 val["ipv6"] = af_inet6
akutz0d1fce52019-06-01 18:54:29 -0500609 by_mac[key] = val
610
akutz0b519f72019-06-02 14:58:57 -0500611 if af_inet4:
612 for ip_info in af_inet4:
akutz0d1fce52019-06-01 18:54:29 -0500613 key = ip_info['addr']
akutzffc4dd52019-06-02 11:34:55 -0500614 if key == '127.0.0.1':
615 continue
akutz84389c82019-06-02 19:24:38 -0500616 val = copy.deepcopy(ip_info)
akutz0d1fce52019-06-01 18:54:29 -0500617 del val['addr']
618 if mac:
619 val['mac'] = mac
akutzffc4dd52019-06-02 11:34:55 -0500620 by_ipv4[key] = val
akutz0d1fce52019-06-01 18:54:29 -0500621
622 if af_inet6:
623 for ip_info in af_inet6:
624 key = ip_info['addr']
akutzffc4dd52019-06-02 11:34:55 -0500625 if key == '::1':
626 continue
akutz84389c82019-06-02 19:24:38 -0500627 val = copy.deepcopy(ip_info)
akutz0d1fce52019-06-01 18:54:29 -0500628 del val['addr']
629 if mac:
630 val['mac'] = mac
akutzffc4dd52019-06-02 11:34:55 -0500631 by_ipv6[key] = val
akutz0d1fce52019-06-01 18:54:29 -0500632
633 return host_info
634
635
akutz10cd1402019-10-23 18:06:39 -0500636def get_data_access_method():
637 if os.environ.get(VMX_GUESTINFO, ""):
638 return VMX_GUESTINFO
yvespp8d58dfd2019-10-29 20:42:09 +0100639 if VMWARE_RPCTOOL:
640 return VMWARE_RPCTOOL
akutz10cd1402019-10-23 18:06:39 -0500641 return None
642
643
akutzffc4dd52019-06-02 11:34:55 -0500644def main():
645 '''
646 Executed when this file is used as a program.
647 '''
648 metadata = {'network': {'config': {'dhcp': True}}}
649 host_info = get_host_info()
650 metadata = always_merger.merge(metadata, host_info)
651 print(util.json_dumps(metadata))
652
653
akutz0d1fce52019-06-01 18:54:29 -0500654if __name__ == "__main__":
akutzffc4dd52019-06-02 11:34:55 -0500655 main()
akutz0d1fce52019-06-01 18:54:29 -0500656
657# vi: ts=4 expandtab