blob: 0e6e1438bc1b486d8670435bfa533c956d8a2a93 [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
akutz33dbfc22020-03-23 13:43:07 -050025from distutils.util import strtobool
akutz1cce7fa2020-03-24 11:01:45 -050026import ipaddress
akutzffc4dd52019-06-02 11:34:55 -050027import json
akutz10cd1402019-10-23 18:06:39 -050028import os
akutzffc4dd52019-06-02 11:34:55 -050029import socket
akutz10cd1402019-10-23 18:06:39 -050030import string
akutz33dbfc22020-03-23 13:43:07 -050031import time
akutzffc4dd52019-06-02 11:34:55 -050032import zlib
akutz77457a62018-08-22 16:07:21 -050033
34from cloudinit import log as logging
35from cloudinit import sources
36from cloudinit import util
akutz6501f902018-08-24 12:19:05 -050037from cloudinit import safeyaml
Shreenidhi Shedi5bb93fe2020-09-08 18:17:11 +053038from cloudinit import version as cl_ver
akutz77457a62018-08-22 16:07:21 -050039
akutzffc4dd52019-06-02 11:34:55 -050040from deepmerge import always_merger
41import netifaces
42
akutz77457a62018-08-22 16:07:21 -050043LOG = logging.getLogger(__name__)
akutz0d1fce52019-06-01 18:54:29 -050044NOVAL = "No value found"
yvespp8d58dfd2019-10-29 20:42:09 +010045VMWARE_RPCTOOL = find_executable("vmware-rpctool")
akutz10cd1402019-10-23 18:06:39 -050046VMX_GUESTINFO = "VMX_GUESTINFO"
akutzdaf81f12019-12-08 11:20:33 -060047GUESTINFO_EMPTY_YAML_VAL = "---"
akutz909cf9a2019-12-09 09:17:00 -060048LOCAL_IPV4 = 'local-ipv4'
49LOCAL_IPV6 = 'local-ipv6'
Yves Peter59c869d2019-12-11 13:09:14 +010050CLEANUP_GUESTINFO = 'cleanup-guestinfo'
akutz33dbfc22020-03-23 13:43:07 -050051WAIT_ON_NETWORK = 'wait-on-network'
52WAIT_ON_NETWORK_IPV4 = 'ipv4'
53WAIT_ON_NETWORK_IPV6 = 'ipv6'
Shreenidhi Shedi5bb93fe2020-09-08 18:17:11 +053054CLOUD_INIT_VERSION = cl_ver.version_string()
akutz0d1fce52019-06-01 18:54:29 -050055
56class NetworkConfigError(Exception):
57 '''
58 NetworkConfigError is raised when there is an issue getting or
59 applying network configuration.
60 '''
61 pass
62
63
Andrew Kutz4f66b8b2018-09-16 18:28:59 -050064class DataSourceVMwareGuestInfo(sources.DataSource):
akutz0d1fce52019-06-01 18:54:29 -050065 '''
66 This cloud-init datasource was designed for use with CentOS 7,
67 which uses cloud-init 0.7.9. However, this datasource should
68 work with any Linux distribution for which cloud-init is
69 avaialble.
70
71 The documentation for cloud-init 0.7.9's datasource is
72 available at http://bit.ly/cloudinit-datasource-0-7-9. The
73 current documentation for cloud-init is found at
74 https://cloudinit.readthedocs.io/en/latest/.
75
76 Setting the hostname:
77 The hostname is set by way of the metadata key "local-hostname".
78
79 Setting the instance ID:
80 The instance ID may be set by way of the metadata key "instance-id".
81 However, if this value is absent then then the instance ID is
82 read from the file /sys/class/dmi/id/product_uuid.
83
84 Configuring the network:
85 The network is configured by setting the metadata key "network"
86 with a value consistent with Network Config Versions 1 or 2,
87 depending on the Linux distro's version of cloud-init:
88
89 Network Config Version 1 - http://bit.ly/cloudinit-net-conf-v1
90 Network Config Version 2 - http://bit.ly/cloudinit-net-conf-v2
91
92 For example, CentOS 7's official cloud-init package is version
93 0.7.9 and does not support Network Config Version 2. However,
94 this datasource still supports supplying Network Config Version 2
95 data as long as the Linux distro's cloud-init package is new
96 enough to parse the data.
97
98 The metadata key "network.encoding" may be used to indicate the
99 format of the metadata key "network". Valid encodings are base64
100 and gzip+base64.
101 '''
102
103 dsname = 'VMwareGuestInfo'
104
akutz77457a62018-08-22 16:07:21 -0500105 def __init__(self, sys_cfg, distro, paths, ud_proc=None):
106 sources.DataSource.__init__(self, sys_cfg, distro, paths, ud_proc)
akutz10cd1402019-10-23 18:06:39 -0500107 if not get_data_access_method():
yvespp8d58dfd2019-10-29 20:42:09 +0100108 LOG.error("Failed to find vmware-rpctool")
akutz77457a62018-08-22 16:07:21 -0500109
110 def get_data(self):
akutz0d1fce52019-06-01 18:54:29 -0500111 """
112 This method should really be _get_data in accordance with the most
113 recent versions of cloud-init. However, because the datasource
114 supports as far back as cloud-init 0.7.9, get_data is still used.
115
116 Because of this the method attempts to do some of the same things
117 that the get_data functions in newer versions of cloud-init do,
118 such as calling persist_instance_data.
119 """
akutzdbce3d92019-12-08 00:09:11 -0600120 data_access_method = get_data_access_method()
121 if not data_access_method:
yvespp8d58dfd2019-10-29 20:42:09 +0100122 LOG.error("vmware-rpctool is required to fetch guestinfo value")
akutz77457a62018-08-22 16:07:21 -0500123 return False
akutz6501f902018-08-24 12:19:05 -0500124
akutz0d1fce52019-06-01 18:54:29 -0500125 # Get the metadata.
126 self.metadata = load_metadata()
akutz6501f902018-08-24 12:19:05 -0500127
akutz0d1fce52019-06-01 18:54:29 -0500128 # Get the user data.
129 self.userdata_raw = guestinfo('userdata')
akutz6501f902018-08-24 12:19:05 -0500130
akutz0d1fce52019-06-01 18:54:29 -0500131 # Get the vendor data.
132 self.vendordata_raw = guestinfo('vendordata')
akutz6501f902018-08-24 12:19:05 -0500133
akutzdbce3d92019-12-08 00:09:11 -0600134 # Check to see if any of the guestinfo data should be removed.
Yves Peter59c869d2019-12-11 13:09:14 +0100135 if data_access_method == VMWARE_RPCTOOL and CLEANUP_GUESTINFO in self.metadata:
136 clear_guestinfo_keys(self.metadata[CLEANUP_GUESTINFO])
akutzdbce3d92019-12-08 00:09:11 -0600137
Keerthana Kf0d27ea2019-09-05 21:46:31 +0530138 if self.metadata or self.userdata_raw or self.vendordata_raw:
139 return True
140 else:
141 return False
akutz77457a62018-08-22 16:07:21 -0500142
akutz0d1fce52019-06-01 18:54:29 -0500143 def setup(self, is_new_instance):
144 """setup(is_new_instance)
145
146 This is called before user-data and vendor-data have been processed.
147
148 Unless the datasource has set mode to 'local', then networking
149 per 'fallback' or per 'network_config' will have been written and
150 brought up the OS at this point.
151 """
152
akutz33dbfc22020-03-23 13:43:07 -0500153 host_info = wait_on_network(self.metadata)
akutz0d1fce52019-06-01 18:54:29 -0500154 LOG.info("got host-info: %s", host_info)
akutzffc4dd52019-06-02 11:34:55 -0500155
akutz909cf9a2019-12-09 09:17:00 -0600156 # Reflect any possible local IPv4 or IPv6 addresses in the guest
157 # info.
158 advertise_local_ip_addrs(host_info)
159
akutzffc4dd52019-06-02 11:34:55 -0500160 # Ensure the metadata gets updated with information about the
161 # host, including the network interfaces, default IP addresses,
162 # etc.
163 self.metadata = always_merger.merge(self.metadata, host_info)
akutz0d1fce52019-06-01 18:54:29 -0500164
165 # Persist the instance data for versions of cloud-init that support
166 # doing so. This occurs here rather than in the get_data call in
167 # order to ensure that the network interfaces are up and can be
168 # persisted with the metadata.
169 try:
170 self.persist_instance_data()
171 except AttributeError:
172 pass
173
akutz6501f902018-08-24 12:19:05 -0500174 @property
175 def network_config(self):
akutz0d1fce52019-06-01 18:54:29 -0500176 if 'network' in self.metadata:
177 LOG.debug("using metadata network config")
178 else:
179 LOG.debug("using fallback network config")
180 self.metadata['network'] = {
181 'config': self.distro.generate_fallback_config(),
182 }
183 return self.metadata['network']['config']
akutz77457a62018-08-22 16:07:21 -0500184
185 def get_instance_id(self):
akutz6501f902018-08-24 12:19:05 -0500186 # Pull the instance ID out of the metadata if present. Otherwise
187 # read the file /sys/class/dmi/id/product_uuid for the instance ID.
188 if self.metadata and 'instance-id' in self.metadata:
189 return self.metadata['instance-id']
akutz77457a62018-08-22 16:07:21 -0500190 with open('/sys/class/dmi/id/product_uuid', 'r') as id_file:
Andrey Klimentyeve1c5ed42019-10-09 12:32:44 +0300191 self.metadata['instance-id'] = str(id_file.read()).rstrip().lower()
akutz0d1fce52019-06-01 18:54:29 -0500192 return self.metadata['instance-id']
akutz77457a62018-08-22 16:07:21 -0500193
Andrey Klimentyeva23229d2019-10-17 15:30:00 +0300194 def get_public_ssh_keys(self):
195 public_keys_data = ""
196 if 'public-keys-data' in self.metadata:
197 public_keys_data = self.metadata['public-keys-data'].splitlines()
198
199 public_keys = []
200 if not public_keys_data:
201 return public_keys
202
203 for public_key in public_keys_data:
204 public_keys.append(public_key)
205
206 return public_keys
207
akutz6501f902018-08-24 12:19:05 -0500208
akutz0d1fce52019-06-01 18:54:29 -0500209def decode(key, enc_type, data):
210 '''
211 decode returns the decoded string value of data
212 key is a string used to identify the data being decoded in log messages
213 ----
214 In py 2.7:
215 json.loads method takes string as input
216 zlib.decompress takes and returns a string
217 base64.b64decode takes and returns a string
218 -----
219 In py 3.6 and newer:
220 json.loads method takes bytes or string as input
221 zlib.decompress takes and returns a bytes
222 base64.b64decode takes bytes or string and returns bytes
223 -----
224 In py > 3, < 3.6:
225 json.loads method takes string as input
226 zlib.decompress takes and returns a bytes
227 base64.b64decode takes bytes or string and returns bytes
228 -----
229 Given the above conditions the output from zlib.decompress and
230 base64.b64decode would be bytes with newer python and str in older
231 version. Thus we would covert the output to str before returning
232 '''
233 LOG.debug("Getting encoded data for key=%s, enc=%s", key, enc_type)
akutz6501f902018-08-24 12:19:05 -0500234
akutz0d1fce52019-06-01 18:54:29 -0500235 raw_data = None
236 if enc_type == "gzip+base64" or enc_type == "gz+b64":
237 LOG.debug("Decoding %s format %s", enc_type, key)
238 raw_data = zlib.decompress(base64.b64decode(data), zlib.MAX_WBITS | 16)
239 elif enc_type == "base64" or enc_type == "b64":
240 LOG.debug("Decoding %s format %s", enc_type, key)
241 raw_data = base64.b64decode(data)
242 else:
243 LOG.debug("Plain-text data %s", key)
244 raw_data = data
Sidharth Surana3a421682018-10-10 15:42:08 -0700245
akutz0d1fce52019-06-01 18:54:29 -0500246 if isinstance(raw_data, bytes):
247 return raw_data.decode('utf-8')
248 return raw_data
249
250
akutzdaf81f12019-12-08 11:20:33 -0600251def get_none_if_empty_val(val):
252 '''
253 get_none_if_empty_val returns None if the provided value, once stripped
254 of its trailing whitespace, is empty or equal to GUESTINFO_EMPTY_YAML_VAL.
255
256 The return value is always a string, regardless of whether the input is
257 a bytes class or a string.
258 '''
259
260 # If the provided value is a bytes class, convert it to a string to
261 # simplify the rest of this function's logic.
262 if isinstance(val, bytes):
263 val = val.decode()
264
265 val = val.rstrip()
266 if len(val) == 0 or val == GUESTINFO_EMPTY_YAML_VAL:
267 return None
268 return val
269
270
akutz909cf9a2019-12-09 09:17:00 -0600271def advertise_local_ip_addrs(host_info):
272 '''
273 advertise_local_ip_addrs gets the local IP address information from
274 the provided host_info map and sets the addresses in the guestinfo
275 namespace
276 '''
277 if not host_info:
278 return
279
280 # Reflect any possible local IPv4 or IPv6 addresses in the guest
281 # info.
Yves Peter9dcf3dc2019-12-11 13:12:34 +0100282 local_ipv4 = host_info.get(LOCAL_IPV4)
akutz909cf9a2019-12-09 09:17:00 -0600283 if local_ipv4:
284 set_guestinfo_value(LOCAL_IPV4, local_ipv4)
285 LOG.info("advertised local ipv4 address %s in guestinfo", local_ipv4)
286
Yves Peter9dcf3dc2019-12-11 13:12:34 +0100287 local_ipv6 = host_info.get(LOCAL_IPV6)
akutz909cf9a2019-12-09 09:17:00 -0600288 if local_ipv6:
289 set_guestinfo_value(LOCAL_IPV6, local_ipv6)
290 LOG.info("advertised local ipv6 address %s in guestinfo", local_ipv6)
291
292
akutzdaf81f12019-12-08 11:20:33 -0600293def handle_returned_guestinfo_val(key, val):
294 '''
295 handle_returned_guestinfo_val returns the provided value if it is
296 not empty or set to GUESTINFO_EMPTY_YAML_VAL, otherwise None is
297 returned
298 '''
299 val = get_none_if_empty_val(val)
300 if val:
301 return val
302 LOG.debug("No value found for key %s", key)
303 return None
304
Shreenidhi Shedi5bb93fe2020-09-08 18:17:11 +0530305def get_subp_obj():
306 '''
307 cloud-init 20.3 onwards, subp is a separate module
308 So, to keep things backward compatible this is needed
309 '''
310 subp_obj = None
311 if CLOUD_INIT_VERSION <= '20.2':
312 subp_obj = util
313 else:
314 subp_obj = util.subp
315
316 return subp_obj
317
akutzdaf81f12019-12-08 11:20:33 -0600318
akutz0d1fce52019-06-01 18:54:29 -0500319def get_guestinfo_value(key):
320 '''
321 Returns a guestinfo value for the specified key.
322 '''
323 LOG.debug("Getting guestinfo value for key %s", key)
akutz10cd1402019-10-23 18:06:39 -0500324
325 data_access_method = get_data_access_method()
326
327 if data_access_method == VMX_GUESTINFO:
328 env_key = ("vmx.guestinfo." + key).upper().replace(".", "_", -1)
akutzdaf81f12019-12-08 11:20:33 -0600329 return handle_returned_guestinfo_val(key, os.environ.get(env_key, ""))
akutz10cd1402019-10-23 18:06:39 -0500330
yvespp8d58dfd2019-10-29 20:42:09 +0100331 if data_access_method == VMWARE_RPCTOOL:
Shreenidhi Shedi5bb93fe2020-09-08 18:17:11 +0530332 subp_obj = get_subp_obj()
akutz10cd1402019-10-23 18:06:39 -0500333 try:
Shreenidhi Shedi5bb93fe2020-09-08 18:17:11 +0530334 (stdout, stderr) = subp_obj.subp(
yvespp8d58dfd2019-10-29 20:42:09 +0100335 [VMWARE_RPCTOOL, "info-get guestinfo." + key])
akutz10cd1402019-10-23 18:06:39 -0500336 if stderr == NOVAL:
337 LOG.debug("No value found for key %s", key)
338 elif not stdout:
339 LOG.error("Failed to get guestinfo value for key %s", key)
340 else:
akutzdaf81f12019-12-08 11:20:33 -0600341 return handle_returned_guestinfo_val(key, stdout)
Shreenidhi Shedi5bb93fe2020-09-08 18:17:11 +0530342 except subp_obj.ProcessExecutionError as error:
akutz10cd1402019-10-23 18:06:39 -0500343 if error.stderr == NOVAL:
344 LOG.debug("No value found for key %s", key)
345 else:
346 util.logexc(
347 LOG, "Failed to get guestinfo value for key %s: %s", key, error)
348 except Exception:
akutz0d1fce52019-06-01 18:54:29 -0500349 util.logexc(
akutz10cd1402019-10-23 18:06:39 -0500350 LOG, "Unexpected error while trying to get guestinfo value for key %s", key)
351
akutz0d1fce52019-06-01 18:54:29 -0500352 return None
akutz6501f902018-08-24 12:19:05 -0500353
akutz0d1fce52019-06-01 18:54:29 -0500354
akutzdbce3d92019-12-08 00:09:11 -0600355def set_guestinfo_value(key, value):
356 '''
357 Sets a guestinfo value for the specified key. Set value to an empty string
358 to clear an existing guestinfo key.
359 '''
360
361 # If value is an empty string then set it to a single space as it is not
362 # possible to set a guestinfo key to an empty string. Setting a guestinfo
363 # key to a single space is as close as it gets to clearing an existing
364 # guestinfo key.
365 if value == "":
366 value = " "
367
368 LOG.debug("Setting guestinfo key=%s to value=%s", key, value)
369
370 data_access_method = get_data_access_method()
371
372 if data_access_method == VMX_GUESTINFO:
373 return True
374
375 if data_access_method == VMWARE_RPCTOOL:
Shreenidhi Shedi5bb93fe2020-09-08 18:17:11 +0530376 subp_obj = get_subp_obj()
akutzdbce3d92019-12-08 00:09:11 -0600377 try:
Shreenidhi Shedi5bb93fe2020-09-08 18:17:11 +0530378 subp_obj.subp(
akutzdbce3d92019-12-08 00:09:11 -0600379 [VMWARE_RPCTOOL, ("info-set guestinfo.%s %s" % (key, value))])
380 return True
Shreenidhi Shedi5bb93fe2020-09-08 18:17:11 +0530381 except subp_obj.ProcessExecutionError as error:
akutzdbce3d92019-12-08 00:09:11 -0600382 util.logexc(
383 LOG, "Failed to set guestinfo key=%s to value=%s: %s", key, value, error)
384 except Exception:
385 util.logexc(
386 LOG, "Unexpected error while trying to set guestinfo key=%s to value=%s", key, value)
387
388 return None
389
390
391def clear_guestinfo_keys(keys):
392 '''
akutzdaf81f12019-12-08 11:20:33 -0600393 clear_guestinfo_keys clears guestinfo of all of the keys in the given list.
394 each key will have its value set to "---". Since the value is valid YAML,
395 cloud-init can still read it if it tries.
akutzdbce3d92019-12-08 00:09:11 -0600396 '''
397 if not keys:
398 return
akutzdaf81f12019-12-08 11:20:33 -0600399 if not type(keys) in (list, tuple):
400 keys = [keys]
akutzdbce3d92019-12-08 00:09:11 -0600401 for key in keys:
akutzdaf81f12019-12-08 11:20:33 -0600402 LOG.info("clearing guestinfo.%s", key)
403 if not set_guestinfo_value(key, GUESTINFO_EMPTY_YAML_VAL):
404 LOG.error("failed to clear guestinfo.%s", key)
405 LOG.info("clearing guestinfo.%s.encoding", key)
406 if not set_guestinfo_value(key + ".encoding", ""):
407 LOG.error("failed to clear guestinfo.%s.encoding", key)
akutzdbce3d92019-12-08 00:09:11 -0600408
409
akutz0d1fce52019-06-01 18:54:29 -0500410def guestinfo(key):
411 '''
412 guestinfo returns the guestinfo value for the provided key, decoding
413 the value when required
414 '''
415 data = get_guestinfo_value(key)
416 if not data:
akutz6501f902018-08-24 12:19:05 -0500417 return None
akutz0d1fce52019-06-01 18:54:29 -0500418 enc_type = get_guestinfo_value(key + '.encoding')
419 return decode('guestinfo.' + key, enc_type, data)
420
421
422def load(data):
423 '''
424 load first attempts to unmarshal the provided data as JSON, and if
425 that fails then attempts to unmarshal the data as YAML. If data is
426 None then a new dictionary is returned.
427 '''
428 if not data:
429 return {}
430 try:
431 return json.loads(data)
432 except:
433 return safeyaml.load(data)
434
435
436def load_metadata():
437 '''
438 load_metadata loads the metadata from the guestinfo data, optionally
439 decoding the network config when required
440 '''
441 data = load(guestinfo('metadata'))
akutz84389c82019-06-02 19:24:38 -0500442 LOG.debug('loaded metadata %s', data)
akutz0d1fce52019-06-01 18:54:29 -0500443
444 network = None
445 if 'network' in data:
446 network = data['network']
447 del data['network']
448
449 network_enc = None
450 if 'network.encoding' in data:
451 network_enc = data['network.encoding']
452 del data['network.encoding']
453
454 if network:
akutz84389c82019-06-02 19:24:38 -0500455 LOG.debug('network data found')
456 if isinstance(network, collections.Mapping):
457 LOG.debug("network data copied to 'config' key")
458 network = {
459 'config': copy.deepcopy(network)
460 }
461 else:
462 LOG.debug("network data to be decoded %s", network)
akutz0d1fce52019-06-01 18:54:29 -0500463 dec_net = decode('metadata.network', network_enc, network)
akutz84389c82019-06-02 19:24:38 -0500464 network = {
465 'config': load(dec_net),
466 }
467
468 LOG.debug('network data %s', network)
akutz0d1fce52019-06-01 18:54:29 -0500469 data['network'] = network
470
471 return data
472
akutz6501f902018-08-24 12:19:05 -0500473
akutz77457a62018-08-22 16:07:21 -0500474def get_datasource_list(depends):
akutz0d1fce52019-06-01 18:54:29 -0500475 '''
akutz77457a62018-08-22 16:07:21 -0500476 Return a list of data sources that match this set of dependencies
akutz0d1fce52019-06-01 18:54:29 -0500477 '''
Andrew Kutz4f66b8b2018-09-16 18:28:59 -0500478 return [DataSourceVMwareGuestInfo]
akutz0d1fce52019-06-01 18:54:29 -0500479
480
akutzffc4dd52019-06-02 11:34:55 -0500481def get_default_ip_addrs():
482 '''
483 Returns the default IPv4 and IPv6 addresses based on the device(s) used for
484 the default route. Please note that None may be returned for either address
485 family if that family has no default route or if there are multiple
486 addresses associated with the device used by the default route for a given
487 address.
488 '''
489 gateways = netifaces.gateways()
490 if 'default' not in gateways:
491 return None, None
492
493 default_gw = gateways['default']
494 if netifaces.AF_INET not in default_gw and netifaces.AF_INET6 not in default_gw:
495 return None, None
496
497 ipv4 = None
498 ipv6 = None
499
500 gw4 = default_gw.get(netifaces.AF_INET)
501 if gw4:
502 _, dev4 = gw4
akutz0b519f72019-06-02 14:58:57 -0500503 addr4_fams = netifaces.ifaddresses(dev4)
504 if addr4_fams:
505 af_inet4 = addr4_fams.get(netifaces.AF_INET)
506 if af_inet4:
507 if len(af_inet4) > 1:
akutzffc4dd52019-06-02 11:34:55 -0500508 LOG.warn(
akutz0b519f72019-06-02 14:58:57 -0500509 "device %s has more than one ipv4 address: %s", dev4, af_inet4)
510 elif 'addr' in af_inet4[0]:
511 ipv4 = af_inet4[0]['addr']
akutzffc4dd52019-06-02 11:34:55 -0500512
513 # Try to get the default IPv6 address by first seeing if there is a default
akutz0b519f72019-06-02 14:58:57 -0500514 # IPv6 route.
akutzffc4dd52019-06-02 11:34:55 -0500515 gw6 = default_gw.get(netifaces.AF_INET6)
516 if gw6:
517 _, dev6 = gw6
518 addr6_fams = netifaces.ifaddresses(dev6)
519 if addr6_fams:
520 af_inet6 = addr6_fams.get(netifaces.AF_INET6)
521 if af_inet6:
522 if len(af_inet6) > 1:
523 LOG.warn(
524 "device %s has more than one ipv6 address: %s", dev6, af_inet6)
525 elif 'addr' in af_inet6[0]:
526 ipv6 = af_inet6[0]['addr']
akutz0b519f72019-06-02 14:58:57 -0500527
528 # If there is a default IPv4 address but not IPv6, then see if there is a
529 # single IPv6 address associated with the same device associated with the
530 # default IPv4 address.
531 if ipv4 and not ipv6:
532 af_inet6 = addr4_fams.get(netifaces.AF_INET6)
akutzffc4dd52019-06-02 11:34:55 -0500533 if af_inet6:
534 if len(af_inet6) > 1:
535 LOG.warn(
536 "device %s has more than one ipv6 address: %s", dev4, af_inet6)
537 elif 'addr' in af_inet6[0]:
538 ipv6 = af_inet6[0]['addr']
539
akutz0b519f72019-06-02 14:58:57 -0500540 # If there is a default IPv6 address but not IPv4, then see if there is a
541 # single IPv4 address associated with the same device associated with the
542 # default IPv6 address.
543 if not ipv4 and ipv6:
Hieu47f5c7f2020-05-20 10:11:08 +0700544 af_inet4 = addr6_fams.get(netifaces.AF_INET)
akutz0b519f72019-06-02 14:58:57 -0500545 if af_inet4:
546 if len(af_inet4) > 1:
547 LOG.warn(
548 "device %s has more than one ipv4 address: %s", dev6, af_inet4)
549 elif 'addr' in af_inet4[0]:
550 ipv4 = af_inet4[0]['addr']
551
akutzffc4dd52019-06-02 11:34:55 -0500552 return ipv4, ipv6
553
Keerthana Kf0d27ea2019-09-05 21:46:31 +0530554# patched socket.getfqdn() - see https://bugs.python.org/issue5004
akutz10cd1402019-10-23 18:06:39 -0500555
556
Keerthana Kf0d27ea2019-09-05 21:46:31 +0530557def getfqdn(name=''):
558 """Get fully qualified domain name from name.
559 An empty argument is interpreted as meaning the local host.
560 """
561 name = name.strip()
562 if not name or name == '0.0.0.0':
563 name = socket.gethostname()
564 try:
akutz10cd1402019-10-23 18:06:39 -0500565 addrs = socket.getaddrinfo(
566 name, None, 0, socket.SOCK_DGRAM, 0, socket.AI_CANONNAME)
Keerthana Kf0d27ea2019-09-05 21:46:31 +0530567 except socket.error:
568 pass
569 else:
570 for addr in addrs:
571 if addr[3]:
572 name = addr[3]
573 break
574 return name
akutzffc4dd52019-06-02 11:34:55 -0500575
akutz10cd1402019-10-23 18:06:39 -0500576
akutz1cce7fa2020-03-24 11:01:45 -0500577def is_valid_ip_addr(val):
578 """
579 Returns false if the address is loopback, link local or unspecified;
580 otherwise true is returned.
581 """
582 addr = None
583 try:
584 addr = ipaddress.ip_address(val)
585 except:
586 return False
587 if addr.is_link_local or addr.is_loopback or addr.is_unspecified:
588 return False
589 return True
590
591
akutz0d1fce52019-06-01 18:54:29 -0500592def get_host_info():
593 '''
594 Returns host information such as the host name and network interfaces.
595 '''
akutz0d1fce52019-06-01 18:54:29 -0500596
597 host_info = {
598 'network': {
599 'interfaces': {
600 'by-mac': collections.OrderedDict(),
akutzffc4dd52019-06-02 11:34:55 -0500601 'by-ipv4': collections.OrderedDict(),
602 'by-ipv6': collections.OrderedDict(),
akutz0d1fce52019-06-01 18:54:29 -0500603 },
604 },
605 }
606
Keerthana Kf0d27ea2019-09-05 21:46:31 +0530607 hostname = getfqdn(socket.gethostname())
akutz0d1fce52019-06-01 18:54:29 -0500608 if hostname:
akutzffc4dd52019-06-02 11:34:55 -0500609 host_info['hostname'] = hostname
akutz0d1fce52019-06-01 18:54:29 -0500610 host_info['local-hostname'] = hostname
akutzb7a193d2019-12-09 11:36:32 -0600611 host_info['local_hostname'] = hostname
akutz0d1fce52019-06-01 18:54:29 -0500612
akutzffc4dd52019-06-02 11:34:55 -0500613 default_ipv4, default_ipv6 = get_default_ip_addrs()
614 if default_ipv4:
akutz909cf9a2019-12-09 09:17:00 -0600615 host_info[LOCAL_IPV4] = default_ipv4
akutzffc4dd52019-06-02 11:34:55 -0500616 if default_ipv6:
akutz909cf9a2019-12-09 09:17:00 -0600617 host_info[LOCAL_IPV6] = default_ipv6
akutzffc4dd52019-06-02 11:34:55 -0500618
akutz0d1fce52019-06-01 18:54:29 -0500619 by_mac = host_info['network']['interfaces']['by-mac']
akutzffc4dd52019-06-02 11:34:55 -0500620 by_ipv4 = host_info['network']['interfaces']['by-ipv4']
621 by_ipv6 = host_info['network']['interfaces']['by-ipv6']
akutz0d1fce52019-06-01 18:54:29 -0500622
623 ifaces = netifaces.interfaces()
624 for dev_name in ifaces:
625 addr_fams = netifaces.ifaddresses(dev_name)
626 af_link = addr_fams.get(netifaces.AF_LINK)
akutz0b519f72019-06-02 14:58:57 -0500627 af_inet4 = addr_fams.get(netifaces.AF_INET)
akutz0d1fce52019-06-01 18:54:29 -0500628 af_inet6 = addr_fams.get(netifaces.AF_INET6)
629
630 mac = None
631 if af_link and 'addr' in af_link[0]:
632 mac = af_link[0]['addr']
633
634 # Do not bother recording localhost
635 if mac == "00:00:00:00:00:00":
636 continue
637
akutz0b519f72019-06-02 14:58:57 -0500638 if mac and (af_inet4 or af_inet6):
akutz0d1fce52019-06-01 18:54:29 -0500639 key = mac
640 val = {}
akutz0b519f72019-06-02 14:58:57 -0500641 if af_inet4:
akutz1cce7fa2020-03-24 11:01:45 -0500642 af_inet4_vals = []
643 for ip_info in af_inet4:
644 if not is_valid_ip_addr(ip_info['addr']):
645 continue
646 af_inet4_vals.append(ip_info)
647 val["ipv4"] = af_inet4_vals
akutz0d1fce52019-06-01 18:54:29 -0500648 if af_inet6:
akutz1cce7fa2020-03-24 11:01:45 -0500649 af_inet6_vals = []
650 for ip_info in af_inet6:
651 if not is_valid_ip_addr(ip_info['addr']):
652 continue
653 af_inet6_vals.append(ip_info)
654 val["ipv6"] = af_inet6_vals
akutz0d1fce52019-06-01 18:54:29 -0500655 by_mac[key] = val
656
akutz0b519f72019-06-02 14:58:57 -0500657 if af_inet4:
658 for ip_info in af_inet4:
akutz0d1fce52019-06-01 18:54:29 -0500659 key = ip_info['addr']
akutz1cce7fa2020-03-24 11:01:45 -0500660 if not is_valid_ip_addr(key):
akutzffc4dd52019-06-02 11:34:55 -0500661 continue
akutz84389c82019-06-02 19:24:38 -0500662 val = copy.deepcopy(ip_info)
akutz0d1fce52019-06-01 18:54:29 -0500663 del val['addr']
664 if mac:
665 val['mac'] = mac
akutzffc4dd52019-06-02 11:34:55 -0500666 by_ipv4[key] = val
akutz0d1fce52019-06-01 18:54:29 -0500667
668 if af_inet6:
669 for ip_info in af_inet6:
670 key = ip_info['addr']
akutz1cce7fa2020-03-24 11:01:45 -0500671 if not is_valid_ip_addr(key):
akutzffc4dd52019-06-02 11:34:55 -0500672 continue
akutz84389c82019-06-02 19:24:38 -0500673 val = copy.deepcopy(ip_info)
akutz0d1fce52019-06-01 18:54:29 -0500674 del val['addr']
675 if mac:
676 val['mac'] = mac
akutzffc4dd52019-06-02 11:34:55 -0500677 by_ipv6[key] = val
akutz0d1fce52019-06-01 18:54:29 -0500678
679 return host_info
680
681
akutz33dbfc22020-03-23 13:43:07 -0500682def wait_on_network(metadata):
683 # Determine whether we need to wait on the network coming online.
684 wait_on_ipv4 = False
685 wait_on_ipv6 = False
686 if WAIT_ON_NETWORK in metadata:
687 wait_on_network = metadata[WAIT_ON_NETWORK]
688 if WAIT_ON_NETWORK_IPV4 in wait_on_network:
689 wait_on_ipv4_val = wait_on_network[WAIT_ON_NETWORK_IPV4]
690 if isinstance(wait_on_ipv4_val, bool):
691 wait_on_ipv4 = wait_on_ipv4_val
692 else:
693 wait_on_ipv4 = bool(strtobool(wait_on_ipv4_val))
694 if WAIT_ON_NETWORK_IPV6 in wait_on_network:
695 wait_on_ipv6_val = wait_on_network[WAIT_ON_NETWORK_IPV6]
696 if isinstance(wait_on_ipv6_val, bool):
697 wait_on_ipv6 = wait_on_ipv6_val
698 else:
699 wait_on_ipv6 = bool(strtobool(wait_on_ipv6_val))
700
701 # Get information about the host.
702 host_info = None
703 while host_info == None:
704 host_info = get_host_info()
akutze16f5e82020-03-24 11:11:07 -0500705 if wait_on_ipv4:
706 ipv4_ready = False
707 if 'network' in host_info:
708 if 'interfaces' in host_info['network']:
709 if 'by-ipv4' in host_info['network']['interfaces']:
710 if len(host_info['network']['interfaces']['by-ipv4']) > 0:
711 ipv4_ready = True
712 if not ipv4_ready:
713 LOG.info("ipv4 not ready")
714 host_info = None
715 if wait_on_ipv6:
716 ipv6_ready = False
717 if 'network' in host_info:
718 if 'interfaces' in host_info['network']:
719 if 'by-ipv6' in host_info['network']['interfaces']:
720 if len(host_info['network']['interfaces']['by-ipv6']) > 0:
721 ipv6_ready = True
722 if not ipv6_ready:
723 LOG.info("ipv6 not ready")
724 host_info = None
akutz33dbfc22020-03-23 13:43:07 -0500725 if host_info == None:
726 LOG.info("waiting on network")
727 time.sleep(1)
728
729 return host_info
730
731
akutz10cd1402019-10-23 18:06:39 -0500732def get_data_access_method():
733 if os.environ.get(VMX_GUESTINFO, ""):
734 return VMX_GUESTINFO
yvespp8d58dfd2019-10-29 20:42:09 +0100735 if VMWARE_RPCTOOL:
736 return VMWARE_RPCTOOL
akutz10cd1402019-10-23 18:06:39 -0500737 return None
738
739
akutzffc4dd52019-06-02 11:34:55 -0500740def main():
741 '''
742 Executed when this file is used as a program.
743 '''
akutz33dbfc22020-03-23 13:43:07 -0500744 metadata = {'wait-on-network': {'ipv4': True, 'ipv6': "false"},
745 'network': {'config': {'dhcp': True}}}
746 host_info = wait_on_network(metadata)
akutzffc4dd52019-06-02 11:34:55 -0500747 metadata = always_merger.merge(metadata, host_info)
748 print(util.json_dumps(metadata))
749
750
akutz0d1fce52019-06-01 18:54:29 -0500751if __name__ == "__main__":
akutzffc4dd52019-06-02 11:34:55 -0500752 main()
akutz0d1fce52019-06-01 18:54:29 -0500753
754# vi: ts=4 expandtab