blob: b7cf780fefef52aba15c3a486cee566d3884565e [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
akutz77457a62018-08-22 16:07:21 -050038
akutzffc4dd52019-06-02 11:34:55 -050039from deepmerge import always_merger
40import netifaces
41
Yves Peter444519c2020-09-10 17:19:46 +020042# in cloud init >= 20.3 subp is in its own module
43try:
44 import subp
45except ImportError:
46 subp_module = util
47else:
48 # early versions of the subp module don't have the subp function
49 if hasattr(subp, 'subp'):
50 subp_module = subp
51 else:
52 subp_module = util
53
akutz77457a62018-08-22 16:07:21 -050054LOG = logging.getLogger(__name__)
akutz0d1fce52019-06-01 18:54:29 -050055NOVAL = "No value found"
yvespp8d58dfd2019-10-29 20:42:09 +010056VMWARE_RPCTOOL = find_executable("vmware-rpctool")
akutz10cd1402019-10-23 18:06:39 -050057VMX_GUESTINFO = "VMX_GUESTINFO"
akutzdaf81f12019-12-08 11:20:33 -060058GUESTINFO_EMPTY_YAML_VAL = "---"
akutz909cf9a2019-12-09 09:17:00 -060059LOCAL_IPV4 = 'local-ipv4'
60LOCAL_IPV6 = 'local-ipv6'
Yves Peter59c869d2019-12-11 13:09:14 +010061CLEANUP_GUESTINFO = 'cleanup-guestinfo'
akutz33dbfc22020-03-23 13:43:07 -050062WAIT_ON_NETWORK = 'wait-on-network'
63WAIT_ON_NETWORK_IPV4 = 'ipv4'
64WAIT_ON_NETWORK_IPV6 = 'ipv6'
akutz0d1fce52019-06-01 18:54:29 -050065
66class NetworkConfigError(Exception):
67 '''
68 NetworkConfigError is raised when there is an issue getting or
69 applying network configuration.
70 '''
71 pass
72
73
Andrew Kutz4f66b8b2018-09-16 18:28:59 -050074class DataSourceVMwareGuestInfo(sources.DataSource):
akutz0d1fce52019-06-01 18:54:29 -050075 '''
76 This cloud-init datasource was designed for use with CentOS 7,
77 which uses cloud-init 0.7.9. However, this datasource should
78 work with any Linux distribution for which cloud-init is
79 avaialble.
80
81 The documentation for cloud-init 0.7.9's datasource is
82 available at http://bit.ly/cloudinit-datasource-0-7-9. The
83 current documentation for cloud-init is found at
84 https://cloudinit.readthedocs.io/en/latest/.
85
86 Setting the hostname:
87 The hostname is set by way of the metadata key "local-hostname".
88
89 Setting the instance ID:
90 The instance ID may be set by way of the metadata key "instance-id".
91 However, if this value is absent then then the instance ID is
92 read from the file /sys/class/dmi/id/product_uuid.
93
94 Configuring the network:
95 The network is configured by setting the metadata key "network"
96 with a value consistent with Network Config Versions 1 or 2,
97 depending on the Linux distro's version of cloud-init:
98
99 Network Config Version 1 - http://bit.ly/cloudinit-net-conf-v1
100 Network Config Version 2 - http://bit.ly/cloudinit-net-conf-v2
101
102 For example, CentOS 7's official cloud-init package is version
103 0.7.9 and does not support Network Config Version 2. However,
104 this datasource still supports supplying Network Config Version 2
105 data as long as the Linux distro's cloud-init package is new
106 enough to parse the data.
107
108 The metadata key "network.encoding" may be used to indicate the
109 format of the metadata key "network". Valid encodings are base64
110 and gzip+base64.
111 '''
112
113 dsname = 'VMwareGuestInfo'
114
akutz77457a62018-08-22 16:07:21 -0500115 def __init__(self, sys_cfg, distro, paths, ud_proc=None):
116 sources.DataSource.__init__(self, sys_cfg, distro, paths, ud_proc)
akutz10cd1402019-10-23 18:06:39 -0500117 if not get_data_access_method():
yvespp8d58dfd2019-10-29 20:42:09 +0100118 LOG.error("Failed to find vmware-rpctool")
akutz77457a62018-08-22 16:07:21 -0500119
120 def get_data(self):
akutz0d1fce52019-06-01 18:54:29 -0500121 """
122 This method should really be _get_data in accordance with the most
123 recent versions of cloud-init. However, because the datasource
124 supports as far back as cloud-init 0.7.9, get_data is still used.
125
126 Because of this the method attempts to do some of the same things
127 that the get_data functions in newer versions of cloud-init do,
128 such as calling persist_instance_data.
129 """
akutzdbce3d92019-12-08 00:09:11 -0600130 data_access_method = get_data_access_method()
131 if not data_access_method:
yvespp8d58dfd2019-10-29 20:42:09 +0100132 LOG.error("vmware-rpctool is required to fetch guestinfo value")
akutz77457a62018-08-22 16:07:21 -0500133 return False
akutz6501f902018-08-24 12:19:05 -0500134
akutz0d1fce52019-06-01 18:54:29 -0500135 # Get the metadata.
136 self.metadata = load_metadata()
akutz6501f902018-08-24 12:19:05 -0500137
akutz0d1fce52019-06-01 18:54:29 -0500138 # Get the user data.
139 self.userdata_raw = guestinfo('userdata')
akutz6501f902018-08-24 12:19:05 -0500140
akutz0d1fce52019-06-01 18:54:29 -0500141 # Get the vendor data.
142 self.vendordata_raw = guestinfo('vendordata')
akutz6501f902018-08-24 12:19:05 -0500143
akutzdbce3d92019-12-08 00:09:11 -0600144 # Check to see if any of the guestinfo data should be removed.
Yves Peter59c869d2019-12-11 13:09:14 +0100145 if data_access_method == VMWARE_RPCTOOL and CLEANUP_GUESTINFO in self.metadata:
146 clear_guestinfo_keys(self.metadata[CLEANUP_GUESTINFO])
akutzdbce3d92019-12-08 00:09:11 -0600147
Keerthana Kf0d27ea2019-09-05 21:46:31 +0530148 if self.metadata or self.userdata_raw or self.vendordata_raw:
149 return True
150 else:
151 return False
akutz77457a62018-08-22 16:07:21 -0500152
akutz0d1fce52019-06-01 18:54:29 -0500153 def setup(self, is_new_instance):
154 """setup(is_new_instance)
155
156 This is called before user-data and vendor-data have been processed.
157
158 Unless the datasource has set mode to 'local', then networking
159 per 'fallback' or per 'network_config' will have been written and
160 brought up the OS at this point.
161 """
162
akutz33dbfc22020-03-23 13:43:07 -0500163 host_info = wait_on_network(self.metadata)
akutz0d1fce52019-06-01 18:54:29 -0500164 LOG.info("got host-info: %s", host_info)
akutzffc4dd52019-06-02 11:34:55 -0500165
akutz909cf9a2019-12-09 09:17:00 -0600166 # Reflect any possible local IPv4 or IPv6 addresses in the guest
167 # info.
168 advertise_local_ip_addrs(host_info)
169
akutzffc4dd52019-06-02 11:34:55 -0500170 # Ensure the metadata gets updated with information about the
171 # host, including the network interfaces, default IP addresses,
172 # etc.
173 self.metadata = always_merger.merge(self.metadata, host_info)
akutz0d1fce52019-06-01 18:54:29 -0500174
175 # Persist the instance data for versions of cloud-init that support
176 # doing so. This occurs here rather than in the get_data call in
177 # order to ensure that the network interfaces are up and can be
178 # persisted with the metadata.
179 try:
180 self.persist_instance_data()
181 except AttributeError:
182 pass
183
akutz6501f902018-08-24 12:19:05 -0500184 @property
185 def network_config(self):
akutz0d1fce52019-06-01 18:54:29 -0500186 if 'network' in self.metadata:
187 LOG.debug("using metadata network config")
188 else:
189 LOG.debug("using fallback network config")
190 self.metadata['network'] = {
191 'config': self.distro.generate_fallback_config(),
192 }
193 return self.metadata['network']['config']
akutz77457a62018-08-22 16:07:21 -0500194
195 def get_instance_id(self):
akutz6501f902018-08-24 12:19:05 -0500196 # Pull the instance ID out of the metadata if present. Otherwise
197 # read the file /sys/class/dmi/id/product_uuid for the instance ID.
198 if self.metadata and 'instance-id' in self.metadata:
199 return self.metadata['instance-id']
akutz77457a62018-08-22 16:07:21 -0500200 with open('/sys/class/dmi/id/product_uuid', 'r') as id_file:
Andrey Klimentyeve1c5ed42019-10-09 12:32:44 +0300201 self.metadata['instance-id'] = str(id_file.read()).rstrip().lower()
akutz0d1fce52019-06-01 18:54:29 -0500202 return self.metadata['instance-id']
akutz77457a62018-08-22 16:07:21 -0500203
Andrey Klimentyeva23229d2019-10-17 15:30:00 +0300204 def get_public_ssh_keys(self):
205 public_keys_data = ""
206 if 'public-keys-data' in self.metadata:
207 public_keys_data = self.metadata['public-keys-data'].splitlines()
208
209 public_keys = []
210 if not public_keys_data:
211 return public_keys
212
213 for public_key in public_keys_data:
214 public_keys.append(public_key)
215
216 return public_keys
217
akutz6501f902018-08-24 12:19:05 -0500218
akutz0d1fce52019-06-01 18:54:29 -0500219def decode(key, enc_type, data):
220 '''
221 decode returns the decoded string value of data
222 key is a string used to identify the data being decoded in log messages
223 ----
224 In py 2.7:
225 json.loads method takes string as input
226 zlib.decompress takes and returns a string
227 base64.b64decode takes and returns a string
228 -----
229 In py 3.6 and newer:
230 json.loads method takes bytes or string as input
231 zlib.decompress takes and returns a bytes
232 base64.b64decode takes bytes or string and returns bytes
233 -----
234 In py > 3, < 3.6:
235 json.loads method takes string as input
236 zlib.decompress takes and returns a bytes
237 base64.b64decode takes bytes or string and returns bytes
238 -----
239 Given the above conditions the output from zlib.decompress and
240 base64.b64decode would be bytes with newer python and str in older
241 version. Thus we would covert the output to str before returning
242 '''
243 LOG.debug("Getting encoded data for key=%s, enc=%s", key, enc_type)
akutz6501f902018-08-24 12:19:05 -0500244
akutz0d1fce52019-06-01 18:54:29 -0500245 raw_data = None
246 if enc_type == "gzip+base64" or enc_type == "gz+b64":
247 LOG.debug("Decoding %s format %s", enc_type, key)
248 raw_data = zlib.decompress(base64.b64decode(data), zlib.MAX_WBITS | 16)
249 elif enc_type == "base64" or enc_type == "b64":
250 LOG.debug("Decoding %s format %s", enc_type, key)
251 raw_data = base64.b64decode(data)
252 else:
253 LOG.debug("Plain-text data %s", key)
254 raw_data = data
Sidharth Surana3a421682018-10-10 15:42:08 -0700255
akutz0d1fce52019-06-01 18:54:29 -0500256 if isinstance(raw_data, bytes):
257 return raw_data.decode('utf-8')
258 return raw_data
259
260
akutzdaf81f12019-12-08 11:20:33 -0600261def get_none_if_empty_val(val):
262 '''
263 get_none_if_empty_val returns None if the provided value, once stripped
264 of its trailing whitespace, is empty or equal to GUESTINFO_EMPTY_YAML_VAL.
265
266 The return value is always a string, regardless of whether the input is
267 a bytes class or a string.
268 '''
269
270 # If the provided value is a bytes class, convert it to a string to
271 # simplify the rest of this function's logic.
272 if isinstance(val, bytes):
273 val = val.decode()
274
275 val = val.rstrip()
276 if len(val) == 0 or val == GUESTINFO_EMPTY_YAML_VAL:
277 return None
278 return val
279
280
akutz909cf9a2019-12-09 09:17:00 -0600281def advertise_local_ip_addrs(host_info):
282 '''
283 advertise_local_ip_addrs gets the local IP address information from
284 the provided host_info map and sets the addresses in the guestinfo
285 namespace
286 '''
287 if not host_info:
288 return
289
290 # Reflect any possible local IPv4 or IPv6 addresses in the guest
291 # info.
Yves Peter9dcf3dc2019-12-11 13:12:34 +0100292 local_ipv4 = host_info.get(LOCAL_IPV4)
akutz909cf9a2019-12-09 09:17:00 -0600293 if local_ipv4:
294 set_guestinfo_value(LOCAL_IPV4, local_ipv4)
295 LOG.info("advertised local ipv4 address %s in guestinfo", local_ipv4)
296
Yves Peter9dcf3dc2019-12-11 13:12:34 +0100297 local_ipv6 = host_info.get(LOCAL_IPV6)
akutz909cf9a2019-12-09 09:17:00 -0600298 if local_ipv6:
299 set_guestinfo_value(LOCAL_IPV6, local_ipv6)
300 LOG.info("advertised local ipv6 address %s in guestinfo", local_ipv6)
301
302
akutzdaf81f12019-12-08 11:20:33 -0600303def handle_returned_guestinfo_val(key, val):
304 '''
305 handle_returned_guestinfo_val returns the provided value if it is
306 not empty or set to GUESTINFO_EMPTY_YAML_VAL, otherwise None is
307 returned
308 '''
309 val = get_none_if_empty_val(val)
310 if val:
311 return val
312 LOG.debug("No value found for key %s", key)
313 return None
314
akutz0d1fce52019-06-01 18:54:29 -0500315def get_guestinfo_value(key):
316 '''
317 Returns a guestinfo value for the specified key.
318 '''
319 LOG.debug("Getting guestinfo value for key %s", key)
akutz10cd1402019-10-23 18:06:39 -0500320
321 data_access_method = get_data_access_method()
322
323 if data_access_method == VMX_GUESTINFO:
324 env_key = ("vmx.guestinfo." + key).upper().replace(".", "_", -1)
akutzdaf81f12019-12-08 11:20:33 -0600325 return handle_returned_guestinfo_val(key, os.environ.get(env_key, ""))
akutz10cd1402019-10-23 18:06:39 -0500326
yvespp8d58dfd2019-10-29 20:42:09 +0100327 if data_access_method == VMWARE_RPCTOOL:
akutz10cd1402019-10-23 18:06:39 -0500328 try:
Yves Peter444519c2020-09-10 17:19:46 +0200329 (stdout, stderr) = subp_module.subp(
yvespp8d58dfd2019-10-29 20:42:09 +0100330 [VMWARE_RPCTOOL, "info-get guestinfo." + key])
akutz10cd1402019-10-23 18:06:39 -0500331 if stderr == NOVAL:
332 LOG.debug("No value found for key %s", key)
333 elif not stdout:
334 LOG.error("Failed to get guestinfo value for key %s", key)
335 else:
akutzdaf81f12019-12-08 11:20:33 -0600336 return handle_returned_guestinfo_val(key, stdout)
Yves Peter444519c2020-09-10 17:19:46 +0200337 except subp_module.ProcessExecutionError as error:
akutz10cd1402019-10-23 18:06:39 -0500338 if error.stderr == NOVAL:
339 LOG.debug("No value found for key %s", key)
340 else:
341 util.logexc(
342 LOG, "Failed to get guestinfo value for key %s: %s", key, error)
343 except Exception:
akutz0d1fce52019-06-01 18:54:29 -0500344 util.logexc(
akutz10cd1402019-10-23 18:06:39 -0500345 LOG, "Unexpected error while trying to get guestinfo value for key %s", key)
346
akutz0d1fce52019-06-01 18:54:29 -0500347 return None
akutz6501f902018-08-24 12:19:05 -0500348
akutz0d1fce52019-06-01 18:54:29 -0500349
akutzdbce3d92019-12-08 00:09:11 -0600350def set_guestinfo_value(key, value):
351 '''
352 Sets a guestinfo value for the specified key. Set value to an empty string
353 to clear an existing guestinfo key.
354 '''
355
356 # If value is an empty string then set it to a single space as it is not
357 # possible to set a guestinfo key to an empty string. Setting a guestinfo
358 # key to a single space is as close as it gets to clearing an existing
359 # guestinfo key.
360 if value == "":
361 value = " "
362
363 LOG.debug("Setting guestinfo key=%s to value=%s", key, value)
364
365 data_access_method = get_data_access_method()
366
367 if data_access_method == VMX_GUESTINFO:
368 return True
369
370 if data_access_method == VMWARE_RPCTOOL:
371 try:
Yves Peter444519c2020-09-10 17:19:46 +0200372 subp_module.subp(
akutzdbce3d92019-12-08 00:09:11 -0600373 [VMWARE_RPCTOOL, ("info-set guestinfo.%s %s" % (key, value))])
374 return True
Yves Peter444519c2020-09-10 17:19:46 +0200375 except subp_module.ProcessExecutionError as error:
akutzdbce3d92019-12-08 00:09:11 -0600376 util.logexc(
377 LOG, "Failed to set guestinfo key=%s to value=%s: %s", key, value, error)
378 except Exception:
379 util.logexc(
380 LOG, "Unexpected error while trying to set guestinfo key=%s to value=%s", key, value)
381
382 return None
383
384
385def clear_guestinfo_keys(keys):
386 '''
akutzdaf81f12019-12-08 11:20:33 -0600387 clear_guestinfo_keys clears guestinfo of all of the keys in the given list.
388 each key will have its value set to "---". Since the value is valid YAML,
389 cloud-init can still read it if it tries.
akutzdbce3d92019-12-08 00:09:11 -0600390 '''
391 if not keys:
392 return
akutzdaf81f12019-12-08 11:20:33 -0600393 if not type(keys) in (list, tuple):
394 keys = [keys]
akutzdbce3d92019-12-08 00:09:11 -0600395 for key in keys:
akutzdaf81f12019-12-08 11:20:33 -0600396 LOG.info("clearing guestinfo.%s", key)
397 if not set_guestinfo_value(key, GUESTINFO_EMPTY_YAML_VAL):
398 LOG.error("failed to clear guestinfo.%s", key)
399 LOG.info("clearing guestinfo.%s.encoding", key)
400 if not set_guestinfo_value(key + ".encoding", ""):
401 LOG.error("failed to clear guestinfo.%s.encoding", key)
akutzdbce3d92019-12-08 00:09:11 -0600402
403
akutz0d1fce52019-06-01 18:54:29 -0500404def guestinfo(key):
405 '''
406 guestinfo returns the guestinfo value for the provided key, decoding
407 the value when required
408 '''
409 data = get_guestinfo_value(key)
410 if not data:
akutz6501f902018-08-24 12:19:05 -0500411 return None
akutz0d1fce52019-06-01 18:54:29 -0500412 enc_type = get_guestinfo_value(key + '.encoding')
413 return decode('guestinfo.' + key, enc_type, data)
414
415
416def load(data):
417 '''
418 load first attempts to unmarshal the provided data as JSON, and if
419 that fails then attempts to unmarshal the data as YAML. If data is
420 None then a new dictionary is returned.
421 '''
422 if not data:
423 return {}
424 try:
425 return json.loads(data)
426 except:
427 return safeyaml.load(data)
428
429
430def load_metadata():
431 '''
432 load_metadata loads the metadata from the guestinfo data, optionally
433 decoding the network config when required
434 '''
435 data = load(guestinfo('metadata'))
akutz84389c82019-06-02 19:24:38 -0500436 LOG.debug('loaded metadata %s', data)
akutz0d1fce52019-06-01 18:54:29 -0500437
438 network = None
439 if 'network' in data:
440 network = data['network']
441 del data['network']
442
443 network_enc = None
444 if 'network.encoding' in data:
445 network_enc = data['network.encoding']
446 del data['network.encoding']
447
448 if network:
akutz84389c82019-06-02 19:24:38 -0500449 LOG.debug('network data found')
450 if isinstance(network, collections.Mapping):
451 LOG.debug("network data copied to 'config' key")
452 network = {
453 'config': copy.deepcopy(network)
454 }
455 else:
456 LOG.debug("network data to be decoded %s", network)
akutz0d1fce52019-06-01 18:54:29 -0500457 dec_net = decode('metadata.network', network_enc, network)
akutz84389c82019-06-02 19:24:38 -0500458 network = {
459 'config': load(dec_net),
460 }
461
462 LOG.debug('network data %s', network)
akutz0d1fce52019-06-01 18:54:29 -0500463 data['network'] = network
464
465 return data
466
akutz6501f902018-08-24 12:19:05 -0500467
akutz77457a62018-08-22 16:07:21 -0500468def get_datasource_list(depends):
akutz0d1fce52019-06-01 18:54:29 -0500469 '''
akutz77457a62018-08-22 16:07:21 -0500470 Return a list of data sources that match this set of dependencies
akutz0d1fce52019-06-01 18:54:29 -0500471 '''
Andrew Kutz4f66b8b2018-09-16 18:28:59 -0500472 return [DataSourceVMwareGuestInfo]
akutz0d1fce52019-06-01 18:54:29 -0500473
474
akutzffc4dd52019-06-02 11:34:55 -0500475def get_default_ip_addrs():
476 '''
477 Returns the default IPv4 and IPv6 addresses based on the device(s) used for
478 the default route. Please note that None may be returned for either address
479 family if that family has no default route or if there are multiple
480 addresses associated with the device used by the default route for a given
481 address.
482 '''
483 gateways = netifaces.gateways()
484 if 'default' not in gateways:
485 return None, None
486
487 default_gw = gateways['default']
488 if netifaces.AF_INET not in default_gw and netifaces.AF_INET6 not in default_gw:
489 return None, None
490
491 ipv4 = None
492 ipv6 = None
493
494 gw4 = default_gw.get(netifaces.AF_INET)
495 if gw4:
496 _, dev4 = gw4
akutz0b519f72019-06-02 14:58:57 -0500497 addr4_fams = netifaces.ifaddresses(dev4)
498 if addr4_fams:
499 af_inet4 = addr4_fams.get(netifaces.AF_INET)
500 if af_inet4:
501 if len(af_inet4) > 1:
akutzffc4dd52019-06-02 11:34:55 -0500502 LOG.warn(
akutz0b519f72019-06-02 14:58:57 -0500503 "device %s has more than one ipv4 address: %s", dev4, af_inet4)
504 elif 'addr' in af_inet4[0]:
505 ipv4 = af_inet4[0]['addr']
akutzffc4dd52019-06-02 11:34:55 -0500506
507 # Try to get the default IPv6 address by first seeing if there is a default
akutz0b519f72019-06-02 14:58:57 -0500508 # IPv6 route.
akutzffc4dd52019-06-02 11:34:55 -0500509 gw6 = default_gw.get(netifaces.AF_INET6)
510 if gw6:
511 _, dev6 = gw6
512 addr6_fams = netifaces.ifaddresses(dev6)
513 if addr6_fams:
514 af_inet6 = addr6_fams.get(netifaces.AF_INET6)
515 if af_inet6:
516 if len(af_inet6) > 1:
517 LOG.warn(
518 "device %s has more than one ipv6 address: %s", dev6, af_inet6)
519 elif 'addr' in af_inet6[0]:
520 ipv6 = af_inet6[0]['addr']
akutz0b519f72019-06-02 14:58:57 -0500521
522 # If there is a default IPv4 address but not IPv6, then see if there is a
523 # single IPv6 address associated with the same device associated with the
524 # default IPv4 address.
525 if ipv4 and not ipv6:
526 af_inet6 = addr4_fams.get(netifaces.AF_INET6)
akutzffc4dd52019-06-02 11:34:55 -0500527 if af_inet6:
528 if len(af_inet6) > 1:
529 LOG.warn(
530 "device %s has more than one ipv6 address: %s", dev4, af_inet6)
531 elif 'addr' in af_inet6[0]:
532 ipv6 = af_inet6[0]['addr']
533
akutz0b519f72019-06-02 14:58:57 -0500534 # If there is a default IPv6 address but not IPv4, then see if there is a
535 # single IPv4 address associated with the same device associated with the
536 # default IPv6 address.
537 if not ipv4 and ipv6:
Hieu47f5c7f2020-05-20 10:11:08 +0700538 af_inet4 = addr6_fams.get(netifaces.AF_INET)
akutz0b519f72019-06-02 14:58:57 -0500539 if af_inet4:
540 if len(af_inet4) > 1:
541 LOG.warn(
542 "device %s has more than one ipv4 address: %s", dev6, af_inet4)
543 elif 'addr' in af_inet4[0]:
544 ipv4 = af_inet4[0]['addr']
545
akutzffc4dd52019-06-02 11:34:55 -0500546 return ipv4, ipv6
547
Keerthana Kf0d27ea2019-09-05 21:46:31 +0530548# patched socket.getfqdn() - see https://bugs.python.org/issue5004
akutz10cd1402019-10-23 18:06:39 -0500549
550
Keerthana Kf0d27ea2019-09-05 21:46:31 +0530551def getfqdn(name=''):
552 """Get fully qualified domain name from name.
553 An empty argument is interpreted as meaning the local host.
554 """
555 name = name.strip()
556 if not name or name == '0.0.0.0':
557 name = socket.gethostname()
558 try:
akutz10cd1402019-10-23 18:06:39 -0500559 addrs = socket.getaddrinfo(
560 name, None, 0, socket.SOCK_DGRAM, 0, socket.AI_CANONNAME)
Keerthana Kf0d27ea2019-09-05 21:46:31 +0530561 except socket.error:
562 pass
563 else:
564 for addr in addrs:
565 if addr[3]:
566 name = addr[3]
567 break
568 return name
akutzffc4dd52019-06-02 11:34:55 -0500569
akutz10cd1402019-10-23 18:06:39 -0500570
akutz1cce7fa2020-03-24 11:01:45 -0500571def is_valid_ip_addr(val):
572 """
573 Returns false if the address is loopback, link local or unspecified;
574 otherwise true is returned.
575 """
576 addr = None
577 try:
Promaethius8626bab2020-09-22 11:04:48 -0600578 try:
579 addr = ipaddress.ip_address(val)
580 except ipaddress.AddressValueError:
581 addr = ipaddress.ip_address(val.encode('utf-8'))
akutz1cce7fa2020-03-24 11:01:45 -0500582 except:
583 return False
584 if addr.is_link_local or addr.is_loopback or addr.is_unspecified:
585 return False
586 return True
587
588
akutz0d1fce52019-06-01 18:54:29 -0500589def get_host_info():
590 '''
591 Returns host information such as the host name and network interfaces.
592 '''
akutz0d1fce52019-06-01 18:54:29 -0500593
594 host_info = {
595 'network': {
596 'interfaces': {
597 'by-mac': collections.OrderedDict(),
akutzffc4dd52019-06-02 11:34:55 -0500598 'by-ipv4': collections.OrderedDict(),
599 'by-ipv6': collections.OrderedDict(),
akutz0d1fce52019-06-01 18:54:29 -0500600 },
601 },
602 }
603
Keerthana Kf0d27ea2019-09-05 21:46:31 +0530604 hostname = getfqdn(socket.gethostname())
akutz0d1fce52019-06-01 18:54:29 -0500605 if hostname:
akutzffc4dd52019-06-02 11:34:55 -0500606 host_info['hostname'] = hostname
akutz0d1fce52019-06-01 18:54:29 -0500607 host_info['local-hostname'] = hostname
akutzb7a193d2019-12-09 11:36:32 -0600608 host_info['local_hostname'] = hostname
akutz0d1fce52019-06-01 18:54:29 -0500609
akutzffc4dd52019-06-02 11:34:55 -0500610 default_ipv4, default_ipv6 = get_default_ip_addrs()
611 if default_ipv4:
akutz909cf9a2019-12-09 09:17:00 -0600612 host_info[LOCAL_IPV4] = default_ipv4
akutzffc4dd52019-06-02 11:34:55 -0500613 if default_ipv6:
akutz909cf9a2019-12-09 09:17:00 -0600614 host_info[LOCAL_IPV6] = default_ipv6
akutzffc4dd52019-06-02 11:34:55 -0500615
akutz0d1fce52019-06-01 18:54:29 -0500616 by_mac = host_info['network']['interfaces']['by-mac']
akutzffc4dd52019-06-02 11:34:55 -0500617 by_ipv4 = host_info['network']['interfaces']['by-ipv4']
618 by_ipv6 = host_info['network']['interfaces']['by-ipv6']
akutz0d1fce52019-06-01 18:54:29 -0500619
620 ifaces = netifaces.interfaces()
621 for dev_name in ifaces:
622 addr_fams = netifaces.ifaddresses(dev_name)
623 af_link = addr_fams.get(netifaces.AF_LINK)
akutz0b519f72019-06-02 14:58:57 -0500624 af_inet4 = addr_fams.get(netifaces.AF_INET)
akutz0d1fce52019-06-01 18:54:29 -0500625 af_inet6 = addr_fams.get(netifaces.AF_INET6)
626
627 mac = None
628 if af_link and 'addr' in af_link[0]:
629 mac = af_link[0]['addr']
630
631 # Do not bother recording localhost
632 if mac == "00:00:00:00:00:00":
633 continue
634
akutz0b519f72019-06-02 14:58:57 -0500635 if mac and (af_inet4 or af_inet6):
akutz0d1fce52019-06-01 18:54:29 -0500636 key = mac
637 val = {}
akutz0b519f72019-06-02 14:58:57 -0500638 if af_inet4:
akutz1cce7fa2020-03-24 11:01:45 -0500639 af_inet4_vals = []
640 for ip_info in af_inet4:
641 if not is_valid_ip_addr(ip_info['addr']):
642 continue
643 af_inet4_vals.append(ip_info)
644 val["ipv4"] = af_inet4_vals
akutz0d1fce52019-06-01 18:54:29 -0500645 if af_inet6:
akutz1cce7fa2020-03-24 11:01:45 -0500646 af_inet6_vals = []
647 for ip_info in af_inet6:
648 if not is_valid_ip_addr(ip_info['addr']):
649 continue
650 af_inet6_vals.append(ip_info)
651 val["ipv6"] = af_inet6_vals
akutz0d1fce52019-06-01 18:54:29 -0500652 by_mac[key] = val
653
akutz0b519f72019-06-02 14:58:57 -0500654 if af_inet4:
655 for ip_info in af_inet4:
akutz0d1fce52019-06-01 18:54:29 -0500656 key = ip_info['addr']
akutz1cce7fa2020-03-24 11:01:45 -0500657 if not is_valid_ip_addr(key):
akutzffc4dd52019-06-02 11:34:55 -0500658 continue
akutz84389c82019-06-02 19:24:38 -0500659 val = copy.deepcopy(ip_info)
akutz0d1fce52019-06-01 18:54:29 -0500660 del val['addr']
661 if mac:
662 val['mac'] = mac
akutzffc4dd52019-06-02 11:34:55 -0500663 by_ipv4[key] = val
akutz0d1fce52019-06-01 18:54:29 -0500664
665 if af_inet6:
666 for ip_info in af_inet6:
667 key = ip_info['addr']
akutz1cce7fa2020-03-24 11:01:45 -0500668 if not is_valid_ip_addr(key):
akutzffc4dd52019-06-02 11:34:55 -0500669 continue
akutz84389c82019-06-02 19:24:38 -0500670 val = copy.deepcopy(ip_info)
akutz0d1fce52019-06-01 18:54:29 -0500671 del val['addr']
672 if mac:
673 val['mac'] = mac
akutzffc4dd52019-06-02 11:34:55 -0500674 by_ipv6[key] = val
akutz0d1fce52019-06-01 18:54:29 -0500675
676 return host_info
677
678
akutz33dbfc22020-03-23 13:43:07 -0500679def wait_on_network(metadata):
680 # Determine whether we need to wait on the network coming online.
681 wait_on_ipv4 = False
682 wait_on_ipv6 = False
683 if WAIT_ON_NETWORK in metadata:
684 wait_on_network = metadata[WAIT_ON_NETWORK]
685 if WAIT_ON_NETWORK_IPV4 in wait_on_network:
686 wait_on_ipv4_val = wait_on_network[WAIT_ON_NETWORK_IPV4]
687 if isinstance(wait_on_ipv4_val, bool):
688 wait_on_ipv4 = wait_on_ipv4_val
689 else:
690 wait_on_ipv4 = bool(strtobool(wait_on_ipv4_val))
691 if WAIT_ON_NETWORK_IPV6 in wait_on_network:
692 wait_on_ipv6_val = wait_on_network[WAIT_ON_NETWORK_IPV6]
693 if isinstance(wait_on_ipv6_val, bool):
694 wait_on_ipv6 = wait_on_ipv6_val
695 else:
696 wait_on_ipv6 = bool(strtobool(wait_on_ipv6_val))
697
698 # Get information about the host.
699 host_info = None
700 while host_info == None:
701 host_info = get_host_info()
akutze16f5e82020-03-24 11:11:07 -0500702 if wait_on_ipv4:
703 ipv4_ready = False
704 if 'network' in host_info:
705 if 'interfaces' in host_info['network']:
706 if 'by-ipv4' in host_info['network']['interfaces']:
707 if len(host_info['network']['interfaces']['by-ipv4']) > 0:
708 ipv4_ready = True
709 if not ipv4_ready:
710 LOG.info("ipv4 not ready")
711 host_info = None
712 if wait_on_ipv6:
713 ipv6_ready = False
714 if 'network' in host_info:
715 if 'interfaces' in host_info['network']:
716 if 'by-ipv6' in host_info['network']['interfaces']:
717 if len(host_info['network']['interfaces']['by-ipv6']) > 0:
718 ipv6_ready = True
719 if not ipv6_ready:
720 LOG.info("ipv6 not ready")
721 host_info = None
akutz33dbfc22020-03-23 13:43:07 -0500722 if host_info == None:
723 LOG.info("waiting on network")
724 time.sleep(1)
725
726 return host_info
727
728
akutz10cd1402019-10-23 18:06:39 -0500729def get_data_access_method():
730 if os.environ.get(VMX_GUESTINFO, ""):
731 return VMX_GUESTINFO
yvespp8d58dfd2019-10-29 20:42:09 +0100732 if VMWARE_RPCTOOL:
733 return VMWARE_RPCTOOL
akutz10cd1402019-10-23 18:06:39 -0500734 return None
735
736
akutzffc4dd52019-06-02 11:34:55 -0500737def main():
738 '''
739 Executed when this file is used as a program.
740 '''
akutz33dbfc22020-03-23 13:43:07 -0500741 metadata = {'wait-on-network': {'ipv4': True, 'ipv6': "false"},
742 'network': {'config': {'dhcp': True}}}
743 host_info = wait_on_network(metadata)
akutzffc4dd52019-06-02 11:34:55 -0500744 metadata = always_merger.merge(metadata, host_info)
745 print(util.json_dumps(metadata))
746
747
akutz0d1fce52019-06-01 18:54:29 -0500748if __name__ == "__main__":
akutzffc4dd52019-06-02 11:34:55 -0500749 main()
akutz0d1fce52019-06-01 18:54:29 -0500750
751# vi: ts=4 expandtab