blob: 19d20a2d6d4cbad28c9c1630cec5cfc4f41483e2 [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 -050039import netifaces
40
Shreenidhi Shedi15160772020-09-11 16:25:05 +053041# from cloud-init >= 20.3 subp is in its own module
Yves Peter444519c2020-09-10 17:19:46 +020042try:
Shreenidhi Shedi15160772020-09-11 16:25:05 +053043 from cloudinit.subp import subp, ProcessExecutionError
Yves Peter444519c2020-09-10 17:19:46 +020044except ImportError:
Shreenidhi Shedi15160772020-09-11 16:25:05 +053045 from cloudinit.util import subp, ProcessExecutionError
46
Yves Peter444519c2020-09-10 17:19:46 +020047
akutz77457a62018-08-22 16:07:21 -050048LOG = logging.getLogger(__name__)
akutz0d1fce52019-06-01 18:54:29 -050049NOVAL = "No value found"
yvespp8d58dfd2019-10-29 20:42:09 +010050VMWARE_RPCTOOL = find_executable("vmware-rpctool")
akutz10cd1402019-10-23 18:06:39 -050051VMX_GUESTINFO = "VMX_GUESTINFO"
akutzdaf81f12019-12-08 11:20:33 -060052GUESTINFO_EMPTY_YAML_VAL = "---"
akutz909cf9a2019-12-09 09:17:00 -060053LOCAL_IPV4 = 'local-ipv4'
54LOCAL_IPV6 = 'local-ipv6'
Yves Peter59c869d2019-12-11 13:09:14 +010055CLEANUP_GUESTINFO = 'cleanup-guestinfo'
akutz33dbfc22020-03-23 13:43:07 -050056WAIT_ON_NETWORK = 'wait-on-network'
57WAIT_ON_NETWORK_IPV4 = 'ipv4'
58WAIT_ON_NETWORK_IPV6 = 'ipv6'
akutz0d1fce52019-06-01 18:54:29 -050059
akutz45f316d2020-09-22 13:04:05 -050060
akutz0d1fce52019-06-01 18:54:29 -050061class NetworkConfigError(Exception):
62 '''
63 NetworkConfigError is raised when there is an issue getting or
64 applying network configuration.
65 '''
66 pass
67
68
Andrew Kutz4f66b8b2018-09-16 18:28:59 -050069class DataSourceVMwareGuestInfo(sources.DataSource):
akutz0d1fce52019-06-01 18:54:29 -050070 '''
71 This cloud-init datasource was designed for use with CentOS 7,
72 which uses cloud-init 0.7.9. However, this datasource should
73 work with any Linux distribution for which cloud-init is
74 avaialble.
75
76 The documentation for cloud-init 0.7.9's datasource is
77 available at http://bit.ly/cloudinit-datasource-0-7-9. The
78 current documentation for cloud-init is found at
79 https://cloudinit.readthedocs.io/en/latest/.
80
81 Setting the hostname:
82 The hostname is set by way of the metadata key "local-hostname".
83
84 Setting the instance ID:
85 The instance ID may be set by way of the metadata key "instance-id".
86 However, if this value is absent then then the instance ID is
87 read from the file /sys/class/dmi/id/product_uuid.
88
89 Configuring the network:
90 The network is configured by setting the metadata key "network"
91 with a value consistent with Network Config Versions 1 or 2,
92 depending on the Linux distro's version of cloud-init:
93
94 Network Config Version 1 - http://bit.ly/cloudinit-net-conf-v1
95 Network Config Version 2 - http://bit.ly/cloudinit-net-conf-v2
96
97 For example, CentOS 7's official cloud-init package is version
98 0.7.9 and does not support Network Config Version 2. However,
99 this datasource still supports supplying Network Config Version 2
100 data as long as the Linux distro's cloud-init package is new
101 enough to parse the data.
102
103 The metadata key "network.encoding" may be used to indicate the
104 format of the metadata key "network". Valid encodings are base64
105 and gzip+base64.
106 '''
107
108 dsname = 'VMwareGuestInfo'
109
akutz77457a62018-08-22 16:07:21 -0500110 def __init__(self, sys_cfg, distro, paths, ud_proc=None):
111 sources.DataSource.__init__(self, sys_cfg, distro, paths, ud_proc)
akutz10cd1402019-10-23 18:06:39 -0500112 if not get_data_access_method():
yvespp8d58dfd2019-10-29 20:42:09 +0100113 LOG.error("Failed to find vmware-rpctool")
akutz77457a62018-08-22 16:07:21 -0500114
115 def get_data(self):
akutz0d1fce52019-06-01 18:54:29 -0500116 """
117 This method should really be _get_data in accordance with the most
118 recent versions of cloud-init. However, because the datasource
119 supports as far back as cloud-init 0.7.9, get_data is still used.
120
121 Because of this the method attempts to do some of the same things
122 that the get_data functions in newer versions of cloud-init do,
123 such as calling persist_instance_data.
124 """
akutzdbce3d92019-12-08 00:09:11 -0600125 data_access_method = get_data_access_method()
126 if not data_access_method:
yvespp8d58dfd2019-10-29 20:42:09 +0100127 LOG.error("vmware-rpctool is required to fetch guestinfo value")
akutz77457a62018-08-22 16:07:21 -0500128 return False
akutz6501f902018-08-24 12:19:05 -0500129
akutz0d1fce52019-06-01 18:54:29 -0500130 # Get the metadata.
131 self.metadata = load_metadata()
akutz6501f902018-08-24 12:19:05 -0500132
akutz0d1fce52019-06-01 18:54:29 -0500133 # Get the user data.
134 self.userdata_raw = guestinfo('userdata')
akutz6501f902018-08-24 12:19:05 -0500135
akutz0d1fce52019-06-01 18:54:29 -0500136 # Get the vendor data.
137 self.vendordata_raw = guestinfo('vendordata')
akutz6501f902018-08-24 12:19:05 -0500138
akutzdbce3d92019-12-08 00:09:11 -0600139 # Check to see if any of the guestinfo data should be removed.
Yves Peter59c869d2019-12-11 13:09:14 +0100140 if data_access_method == VMWARE_RPCTOOL and CLEANUP_GUESTINFO in self.metadata:
141 clear_guestinfo_keys(self.metadata[CLEANUP_GUESTINFO])
akutzdbce3d92019-12-08 00:09:11 -0600142
Keerthana Kf0d27ea2019-09-05 21:46:31 +0530143 if self.metadata or self.userdata_raw or self.vendordata_raw:
144 return True
145 else:
146 return False
akutz77457a62018-08-22 16:07:21 -0500147
akutz0d1fce52019-06-01 18:54:29 -0500148 def setup(self, is_new_instance):
149 """setup(is_new_instance)
150
151 This is called before user-data and vendor-data have been processed.
152
153 Unless the datasource has set mode to 'local', then networking
154 per 'fallback' or per 'network_config' will have been written and
155 brought up the OS at this point.
156 """
157
akutz33dbfc22020-03-23 13:43:07 -0500158 host_info = wait_on_network(self.metadata)
akutz0d1fce52019-06-01 18:54:29 -0500159 LOG.info("got host-info: %s", host_info)
akutzffc4dd52019-06-02 11:34:55 -0500160
akutz909cf9a2019-12-09 09:17:00 -0600161 # Reflect any possible local IPv4 or IPv6 addresses in the guest
162 # info.
163 advertise_local_ip_addrs(host_info)
164
akutzffc4dd52019-06-02 11:34:55 -0500165 # Ensure the metadata gets updated with information about the
166 # host, including the network interfaces, default IP addresses,
167 # etc.
akutz45f316d2020-09-22 13:04:05 -0500168 self.metadata = merge_dicts(self.metadata, host_info)
akutz0d1fce52019-06-01 18:54:29 -0500169
170 # Persist the instance data for versions of cloud-init that support
171 # doing so. This occurs here rather than in the get_data call in
172 # order to ensure that the network interfaces are up and can be
173 # persisted with the metadata.
174 try:
175 self.persist_instance_data()
176 except AttributeError:
177 pass
178
akutz6501f902018-08-24 12:19:05 -0500179 @property
180 def network_config(self):
akutz0d1fce52019-06-01 18:54:29 -0500181 if 'network' in self.metadata:
182 LOG.debug("using metadata network config")
183 else:
184 LOG.debug("using fallback network config")
185 self.metadata['network'] = {
186 'config': self.distro.generate_fallback_config(),
187 }
188 return self.metadata['network']['config']
akutz77457a62018-08-22 16:07:21 -0500189
190 def get_instance_id(self):
akutz6501f902018-08-24 12:19:05 -0500191 # Pull the instance ID out of the metadata if present. Otherwise
192 # read the file /sys/class/dmi/id/product_uuid for the instance ID.
193 if self.metadata and 'instance-id' in self.metadata:
194 return self.metadata['instance-id']
akutz77457a62018-08-22 16:07:21 -0500195 with open('/sys/class/dmi/id/product_uuid', 'r') as id_file:
Andrey Klimentyeve1c5ed42019-10-09 12:32:44 +0300196 self.metadata['instance-id'] = str(id_file.read()).rstrip().lower()
akutz0d1fce52019-06-01 18:54:29 -0500197 return self.metadata['instance-id']
akutz77457a62018-08-22 16:07:21 -0500198
Andrey Klimentyeva23229d2019-10-17 15:30:00 +0300199 def get_public_ssh_keys(self):
200 public_keys_data = ""
201 if 'public-keys-data' in self.metadata:
202 public_keys_data = self.metadata['public-keys-data'].splitlines()
203
204 public_keys = []
205 if not public_keys_data:
206 return public_keys
207
208 for public_key in public_keys_data:
209 public_keys.append(public_key)
210
211 return public_keys
212
akutz6501f902018-08-24 12:19:05 -0500213
akutz0d1fce52019-06-01 18:54:29 -0500214def decode(key, enc_type, data):
215 '''
216 decode returns the decoded string value of data
217 key is a string used to identify the data being decoded in log messages
218 ----
219 In py 2.7:
220 json.loads method takes string as input
221 zlib.decompress takes and returns a string
222 base64.b64decode takes and returns a string
223 -----
224 In py 3.6 and newer:
225 json.loads method takes bytes or string as input
226 zlib.decompress takes and returns a bytes
227 base64.b64decode takes bytes or string and returns bytes
228 -----
229 In py > 3, < 3.6:
230 json.loads method takes string as input
231 zlib.decompress takes and returns a bytes
232 base64.b64decode takes bytes or string and returns bytes
233 -----
234 Given the above conditions the output from zlib.decompress and
235 base64.b64decode would be bytes with newer python and str in older
236 version. Thus we would covert the output to str before returning
237 '''
238 LOG.debug("Getting encoded data for key=%s, enc=%s", key, enc_type)
akutz6501f902018-08-24 12:19:05 -0500239
akutz0d1fce52019-06-01 18:54:29 -0500240 raw_data = None
241 if enc_type == "gzip+base64" or enc_type == "gz+b64":
242 LOG.debug("Decoding %s format %s", enc_type, key)
243 raw_data = zlib.decompress(base64.b64decode(data), zlib.MAX_WBITS | 16)
244 elif enc_type == "base64" or enc_type == "b64":
245 LOG.debug("Decoding %s format %s", enc_type, key)
246 raw_data = base64.b64decode(data)
247 else:
248 LOG.debug("Plain-text data %s", key)
249 raw_data = data
Sidharth Surana3a421682018-10-10 15:42:08 -0700250
akutz0d1fce52019-06-01 18:54:29 -0500251 if isinstance(raw_data, bytes):
252 return raw_data.decode('utf-8')
253 return raw_data
254
255
akutzdaf81f12019-12-08 11:20:33 -0600256def get_none_if_empty_val(val):
257 '''
258 get_none_if_empty_val returns None if the provided value, once stripped
259 of its trailing whitespace, is empty or equal to GUESTINFO_EMPTY_YAML_VAL.
260
261 The return value is always a string, regardless of whether the input is
262 a bytes class or a string.
263 '''
264
265 # If the provided value is a bytes class, convert it to a string to
266 # simplify the rest of this function's logic.
267 if isinstance(val, bytes):
268 val = val.decode()
269
270 val = val.rstrip()
271 if len(val) == 0 or val == GUESTINFO_EMPTY_YAML_VAL:
272 return None
273 return val
274
275
akutz909cf9a2019-12-09 09:17:00 -0600276def advertise_local_ip_addrs(host_info):
277 '''
278 advertise_local_ip_addrs gets the local IP address information from
279 the provided host_info map and sets the addresses in the guestinfo
280 namespace
281 '''
282 if not host_info:
283 return
284
285 # Reflect any possible local IPv4 or IPv6 addresses in the guest
286 # info.
Yves Peter9dcf3dc2019-12-11 13:12:34 +0100287 local_ipv4 = host_info.get(LOCAL_IPV4)
akutz909cf9a2019-12-09 09:17:00 -0600288 if local_ipv4:
289 set_guestinfo_value(LOCAL_IPV4, local_ipv4)
290 LOG.info("advertised local ipv4 address %s in guestinfo", local_ipv4)
291
Yves Peter9dcf3dc2019-12-11 13:12:34 +0100292 local_ipv6 = host_info.get(LOCAL_IPV6)
akutz909cf9a2019-12-09 09:17:00 -0600293 if local_ipv6:
294 set_guestinfo_value(LOCAL_IPV6, local_ipv6)
295 LOG.info("advertised local ipv6 address %s in guestinfo", local_ipv6)
296
297
akutzdaf81f12019-12-08 11:20:33 -0600298def handle_returned_guestinfo_val(key, val):
299 '''
300 handle_returned_guestinfo_val returns the provided value if it is
301 not empty or set to GUESTINFO_EMPTY_YAML_VAL, otherwise None is
302 returned
303 '''
304 val = get_none_if_empty_val(val)
305 if val:
306 return val
307 LOG.debug("No value found for key %s", key)
308 return None
309
akutz45f316d2020-09-22 13:04:05 -0500310
akutz0d1fce52019-06-01 18:54:29 -0500311def get_guestinfo_value(key):
312 '''
313 Returns a guestinfo value for the specified key.
314 '''
315 LOG.debug("Getting guestinfo value for key %s", key)
akutz10cd1402019-10-23 18:06:39 -0500316
317 data_access_method = get_data_access_method()
318
319 if data_access_method == VMX_GUESTINFO:
320 env_key = ("vmx.guestinfo." + key).upper().replace(".", "_", -1)
akutzdaf81f12019-12-08 11:20:33 -0600321 return handle_returned_guestinfo_val(key, os.environ.get(env_key, ""))
akutz10cd1402019-10-23 18:06:39 -0500322
yvespp8d58dfd2019-10-29 20:42:09 +0100323 if data_access_method == VMWARE_RPCTOOL:
akutz10cd1402019-10-23 18:06:39 -0500324 try:
Shreenidhi Shedi15160772020-09-11 16:25:05 +0530325 (stdout, stderr) = subp(
yvespp8d58dfd2019-10-29 20:42:09 +0100326 [VMWARE_RPCTOOL, "info-get guestinfo." + key])
akutz10cd1402019-10-23 18:06:39 -0500327 if stderr == NOVAL:
328 LOG.debug("No value found for key %s", key)
329 elif not stdout:
330 LOG.error("Failed to get guestinfo value for key %s", key)
331 else:
akutzdaf81f12019-12-08 11:20:33 -0600332 return handle_returned_guestinfo_val(key, stdout)
Shreenidhi Shedi15160772020-09-11 16:25:05 +0530333 except ProcessExecutionError as error:
akutz10cd1402019-10-23 18:06:39 -0500334 if error.stderr == NOVAL:
335 LOG.debug("No value found for key %s", key)
336 else:
337 util.logexc(
338 LOG, "Failed to get guestinfo value for key %s: %s", key, error)
339 except Exception:
akutz0d1fce52019-06-01 18:54:29 -0500340 util.logexc(
akutz10cd1402019-10-23 18:06:39 -0500341 LOG, "Unexpected error while trying to get guestinfo value for key %s", key)
342
akutz0d1fce52019-06-01 18:54:29 -0500343 return None
akutz6501f902018-08-24 12:19:05 -0500344
akutz0d1fce52019-06-01 18:54:29 -0500345
akutzdbce3d92019-12-08 00:09:11 -0600346def set_guestinfo_value(key, value):
347 '''
348 Sets a guestinfo value for the specified key. Set value to an empty string
349 to clear an existing guestinfo key.
350 '''
351
352 # If value is an empty string then set it to a single space as it is not
353 # possible to set a guestinfo key to an empty string. Setting a guestinfo
354 # key to a single space is as close as it gets to clearing an existing
355 # guestinfo key.
356 if value == "":
357 value = " "
358
359 LOG.debug("Setting guestinfo key=%s to value=%s", key, value)
360
361 data_access_method = get_data_access_method()
362
363 if data_access_method == VMX_GUESTINFO:
364 return True
365
366 if data_access_method == VMWARE_RPCTOOL:
367 try:
Shreenidhi Shedi15160772020-09-11 16:25:05 +0530368 subp([VMWARE_RPCTOOL, ("info-set guestinfo.%s %s" % (key, value))])
akutzdbce3d92019-12-08 00:09:11 -0600369 return True
Shreenidhi Shedi15160772020-09-11 16:25:05 +0530370 except ProcessExecutionError as error:
akutzdbce3d92019-12-08 00:09:11 -0600371 util.logexc(
372 LOG, "Failed to set guestinfo key=%s to value=%s: %s", key, value, error)
373 except Exception:
374 util.logexc(
375 LOG, "Unexpected error while trying to set guestinfo key=%s to value=%s", key, value)
376
377 return None
378
379
380def clear_guestinfo_keys(keys):
381 '''
akutzdaf81f12019-12-08 11:20:33 -0600382 clear_guestinfo_keys clears guestinfo of all of the keys in the given list.
383 each key will have its value set to "---". Since the value is valid YAML,
384 cloud-init can still read it if it tries.
akutzdbce3d92019-12-08 00:09:11 -0600385 '''
386 if not keys:
387 return
akutzdaf81f12019-12-08 11:20:33 -0600388 if not type(keys) in (list, tuple):
389 keys = [keys]
akutzdbce3d92019-12-08 00:09:11 -0600390 for key in keys:
akutzdaf81f12019-12-08 11:20:33 -0600391 LOG.info("clearing guestinfo.%s", key)
392 if not set_guestinfo_value(key, GUESTINFO_EMPTY_YAML_VAL):
393 LOG.error("failed to clear guestinfo.%s", key)
394 LOG.info("clearing guestinfo.%s.encoding", key)
395 if not set_guestinfo_value(key + ".encoding", ""):
396 LOG.error("failed to clear guestinfo.%s.encoding", key)
akutzdbce3d92019-12-08 00:09:11 -0600397
398
akutz0d1fce52019-06-01 18:54:29 -0500399def guestinfo(key):
400 '''
401 guestinfo returns the guestinfo value for the provided key, decoding
402 the value when required
403 '''
404 data = get_guestinfo_value(key)
405 if not data:
akutz6501f902018-08-24 12:19:05 -0500406 return None
akutz0d1fce52019-06-01 18:54:29 -0500407 enc_type = get_guestinfo_value(key + '.encoding')
408 return decode('guestinfo.' + key, enc_type, data)
409
410
411def load(data):
412 '''
413 load first attempts to unmarshal the provided data as JSON, and if
414 that fails then attempts to unmarshal the data as YAML. If data is
415 None then a new dictionary is returned.
416 '''
417 if not data:
418 return {}
419 try:
420 return json.loads(data)
421 except:
422 return safeyaml.load(data)
423
424
425def load_metadata():
426 '''
427 load_metadata loads the metadata from the guestinfo data, optionally
428 decoding the network config when required
429 '''
430 data = load(guestinfo('metadata'))
akutz84389c82019-06-02 19:24:38 -0500431 LOG.debug('loaded metadata %s', data)
akutz0d1fce52019-06-01 18:54:29 -0500432
433 network = None
434 if 'network' in data:
435 network = data['network']
436 del data['network']
437
438 network_enc = None
439 if 'network.encoding' in data:
440 network_enc = data['network.encoding']
441 del data['network.encoding']
442
443 if network:
akutz84389c82019-06-02 19:24:38 -0500444 LOG.debug('network data found')
445 if isinstance(network, collections.Mapping):
446 LOG.debug("network data copied to 'config' key")
447 network = {
448 'config': copy.deepcopy(network)
449 }
450 else:
451 LOG.debug("network data to be decoded %s", network)
akutz0d1fce52019-06-01 18:54:29 -0500452 dec_net = decode('metadata.network', network_enc, network)
akutz84389c82019-06-02 19:24:38 -0500453 network = {
454 'config': load(dec_net),
455 }
456
457 LOG.debug('network data %s', network)
akutz0d1fce52019-06-01 18:54:29 -0500458 data['network'] = network
459
460 return data
461
akutz6501f902018-08-24 12:19:05 -0500462
akutz77457a62018-08-22 16:07:21 -0500463def get_datasource_list(depends):
akutz0d1fce52019-06-01 18:54:29 -0500464 '''
akutz77457a62018-08-22 16:07:21 -0500465 Return a list of data sources that match this set of dependencies
akutz0d1fce52019-06-01 18:54:29 -0500466 '''
Andrew Kutz4f66b8b2018-09-16 18:28:59 -0500467 return [DataSourceVMwareGuestInfo]
akutz0d1fce52019-06-01 18:54:29 -0500468
469
akutzffc4dd52019-06-02 11:34:55 -0500470def get_default_ip_addrs():
471 '''
472 Returns the default IPv4 and IPv6 addresses based on the device(s) used for
473 the default route. Please note that None may be returned for either address
474 family if that family has no default route or if there are multiple
475 addresses associated with the device used by the default route for a given
476 address.
477 '''
478 gateways = netifaces.gateways()
479 if 'default' not in gateways:
480 return None, None
481
482 default_gw = gateways['default']
483 if netifaces.AF_INET not in default_gw and netifaces.AF_INET6 not in default_gw:
484 return None, None
485
486 ipv4 = None
487 ipv6 = None
488
489 gw4 = default_gw.get(netifaces.AF_INET)
490 if gw4:
491 _, dev4 = gw4
akutz0b519f72019-06-02 14:58:57 -0500492 addr4_fams = netifaces.ifaddresses(dev4)
493 if addr4_fams:
494 af_inet4 = addr4_fams.get(netifaces.AF_INET)
495 if af_inet4:
496 if len(af_inet4) > 1:
akutzffc4dd52019-06-02 11:34:55 -0500497 LOG.warn(
akutz0b519f72019-06-02 14:58:57 -0500498 "device %s has more than one ipv4 address: %s", dev4, af_inet4)
499 elif 'addr' in af_inet4[0]:
500 ipv4 = af_inet4[0]['addr']
akutzffc4dd52019-06-02 11:34:55 -0500501
502 # Try to get the default IPv6 address by first seeing if there is a default
akutz0b519f72019-06-02 14:58:57 -0500503 # IPv6 route.
akutzffc4dd52019-06-02 11:34:55 -0500504 gw6 = default_gw.get(netifaces.AF_INET6)
505 if gw6:
506 _, dev6 = gw6
507 addr6_fams = netifaces.ifaddresses(dev6)
508 if addr6_fams:
509 af_inet6 = addr6_fams.get(netifaces.AF_INET6)
510 if af_inet6:
511 if len(af_inet6) > 1:
512 LOG.warn(
513 "device %s has more than one ipv6 address: %s", dev6, af_inet6)
514 elif 'addr' in af_inet6[0]:
515 ipv6 = af_inet6[0]['addr']
akutz0b519f72019-06-02 14:58:57 -0500516
517 # If there is a default IPv4 address but not IPv6, then see if there is a
518 # single IPv6 address associated with the same device associated with the
519 # default IPv4 address.
520 if ipv4 and not ipv6:
521 af_inet6 = addr4_fams.get(netifaces.AF_INET6)
akutzffc4dd52019-06-02 11:34:55 -0500522 if af_inet6:
523 if len(af_inet6) > 1:
524 LOG.warn(
525 "device %s has more than one ipv6 address: %s", dev4, af_inet6)
526 elif 'addr' in af_inet6[0]:
527 ipv6 = af_inet6[0]['addr']
528
akutz0b519f72019-06-02 14:58:57 -0500529 # If there is a default IPv6 address but not IPv4, then see if there is a
530 # single IPv4 address associated with the same device associated with the
531 # default IPv6 address.
532 if not ipv4 and ipv6:
Hieu47f5c7f2020-05-20 10:11:08 +0700533 af_inet4 = addr6_fams.get(netifaces.AF_INET)
akutz0b519f72019-06-02 14:58:57 -0500534 if af_inet4:
535 if len(af_inet4) > 1:
536 LOG.warn(
537 "device %s has more than one ipv4 address: %s", dev6, af_inet4)
538 elif 'addr' in af_inet4[0]:
539 ipv4 = af_inet4[0]['addr']
540
akutzffc4dd52019-06-02 11:34:55 -0500541 return ipv4, ipv6
542
Keerthana Kf0d27ea2019-09-05 21:46:31 +0530543# patched socket.getfqdn() - see https://bugs.python.org/issue5004
akutz10cd1402019-10-23 18:06:39 -0500544
545
Keerthana Kf0d27ea2019-09-05 21:46:31 +0530546def getfqdn(name=''):
547 """Get fully qualified domain name from name.
548 An empty argument is interpreted as meaning the local host.
549 """
550 name = name.strip()
551 if not name or name == '0.0.0.0':
552 name = socket.gethostname()
553 try:
akutz10cd1402019-10-23 18:06:39 -0500554 addrs = socket.getaddrinfo(
555 name, None, 0, socket.SOCK_DGRAM, 0, socket.AI_CANONNAME)
Keerthana Kf0d27ea2019-09-05 21:46:31 +0530556 except socket.error:
557 pass
558 else:
559 for addr in addrs:
560 if addr[3]:
561 name = addr[3]
562 break
563 return name
akutzffc4dd52019-06-02 11:34:55 -0500564
akutz10cd1402019-10-23 18:06:39 -0500565
akutz1cce7fa2020-03-24 11:01:45 -0500566def is_valid_ip_addr(val):
567 """
568 Returns false if the address is loopback, link local or unspecified;
569 otherwise true is returned.
570 """
571 addr = None
572 try:
Promaethius8626bab2020-09-22 11:04:48 -0600573 try:
574 addr = ipaddress.ip_address(val)
575 except ipaddress.AddressValueError:
Marcus Portmann2a3c8d82020-10-05 16:19:00 +0200576 addr = ipaddress.ip_address(unicode(val))
akutz1cce7fa2020-03-24 11:01:45 -0500577 except:
578 return False
579 if addr.is_link_local or addr.is_loopback or addr.is_unspecified:
580 return False
581 return True
582
583
akutz0d1fce52019-06-01 18:54:29 -0500584def get_host_info():
585 '''
586 Returns host information such as the host name and network interfaces.
587 '''
akutz0d1fce52019-06-01 18:54:29 -0500588
589 host_info = {
590 'network': {
591 'interfaces': {
592 'by-mac': collections.OrderedDict(),
akutzffc4dd52019-06-02 11:34:55 -0500593 'by-ipv4': collections.OrderedDict(),
594 'by-ipv6': collections.OrderedDict(),
akutz0d1fce52019-06-01 18:54:29 -0500595 },
596 },
597 }
598
Keerthana Kf0d27ea2019-09-05 21:46:31 +0530599 hostname = getfqdn(socket.gethostname())
akutz0d1fce52019-06-01 18:54:29 -0500600 if hostname:
akutzffc4dd52019-06-02 11:34:55 -0500601 host_info['hostname'] = hostname
akutz0d1fce52019-06-01 18:54:29 -0500602 host_info['local-hostname'] = hostname
akutzb7a193d2019-12-09 11:36:32 -0600603 host_info['local_hostname'] = hostname
akutz0d1fce52019-06-01 18:54:29 -0500604
akutzffc4dd52019-06-02 11:34:55 -0500605 default_ipv4, default_ipv6 = get_default_ip_addrs()
606 if default_ipv4:
akutz909cf9a2019-12-09 09:17:00 -0600607 host_info[LOCAL_IPV4] = default_ipv4
akutzffc4dd52019-06-02 11:34:55 -0500608 if default_ipv6:
akutz909cf9a2019-12-09 09:17:00 -0600609 host_info[LOCAL_IPV6] = default_ipv6
akutzffc4dd52019-06-02 11:34:55 -0500610
akutz0d1fce52019-06-01 18:54:29 -0500611 by_mac = host_info['network']['interfaces']['by-mac']
akutzffc4dd52019-06-02 11:34:55 -0500612 by_ipv4 = host_info['network']['interfaces']['by-ipv4']
613 by_ipv6 = host_info['network']['interfaces']['by-ipv6']
akutz0d1fce52019-06-01 18:54:29 -0500614
615 ifaces = netifaces.interfaces()
616 for dev_name in ifaces:
617 addr_fams = netifaces.ifaddresses(dev_name)
618 af_link = addr_fams.get(netifaces.AF_LINK)
akutz0b519f72019-06-02 14:58:57 -0500619 af_inet4 = addr_fams.get(netifaces.AF_INET)
akutz0d1fce52019-06-01 18:54:29 -0500620 af_inet6 = addr_fams.get(netifaces.AF_INET6)
621
622 mac = None
623 if af_link and 'addr' in af_link[0]:
624 mac = af_link[0]['addr']
625
626 # Do not bother recording localhost
627 if mac == "00:00:00:00:00:00":
628 continue
629
akutz0b519f72019-06-02 14:58:57 -0500630 if mac and (af_inet4 or af_inet6):
akutz0d1fce52019-06-01 18:54:29 -0500631 key = mac
632 val = {}
akutz0b519f72019-06-02 14:58:57 -0500633 if af_inet4:
akutz1cce7fa2020-03-24 11:01:45 -0500634 af_inet4_vals = []
635 for ip_info in af_inet4:
636 if not is_valid_ip_addr(ip_info['addr']):
637 continue
638 af_inet4_vals.append(ip_info)
639 val["ipv4"] = af_inet4_vals
akutz0d1fce52019-06-01 18:54:29 -0500640 if af_inet6:
akutz1cce7fa2020-03-24 11:01:45 -0500641 af_inet6_vals = []
642 for ip_info in af_inet6:
643 if not is_valid_ip_addr(ip_info['addr']):
644 continue
645 af_inet6_vals.append(ip_info)
646 val["ipv6"] = af_inet6_vals
akutz0d1fce52019-06-01 18:54:29 -0500647 by_mac[key] = val
648
akutz0b519f72019-06-02 14:58:57 -0500649 if af_inet4:
650 for ip_info in af_inet4:
akutz0d1fce52019-06-01 18:54:29 -0500651 key = ip_info['addr']
akutz1cce7fa2020-03-24 11:01:45 -0500652 if not is_valid_ip_addr(key):
akutzffc4dd52019-06-02 11:34:55 -0500653 continue
akutz84389c82019-06-02 19:24:38 -0500654 val = copy.deepcopy(ip_info)
akutz0d1fce52019-06-01 18:54:29 -0500655 del val['addr']
656 if mac:
657 val['mac'] = mac
akutzffc4dd52019-06-02 11:34:55 -0500658 by_ipv4[key] = val
akutz0d1fce52019-06-01 18:54:29 -0500659
660 if af_inet6:
661 for ip_info in af_inet6:
662 key = ip_info['addr']
akutz1cce7fa2020-03-24 11:01:45 -0500663 if not is_valid_ip_addr(key):
akutzffc4dd52019-06-02 11:34:55 -0500664 continue
akutz84389c82019-06-02 19:24:38 -0500665 val = copy.deepcopy(ip_info)
akutz0d1fce52019-06-01 18:54:29 -0500666 del val['addr']
667 if mac:
668 val['mac'] = mac
akutzffc4dd52019-06-02 11:34:55 -0500669 by_ipv6[key] = val
akutz0d1fce52019-06-01 18:54:29 -0500670
671 return host_info
672
673
akutz33dbfc22020-03-23 13:43:07 -0500674def wait_on_network(metadata):
675 # Determine whether we need to wait on the network coming online.
676 wait_on_ipv4 = False
677 wait_on_ipv6 = False
678 if WAIT_ON_NETWORK in metadata:
679 wait_on_network = metadata[WAIT_ON_NETWORK]
680 if WAIT_ON_NETWORK_IPV4 in wait_on_network:
681 wait_on_ipv4_val = wait_on_network[WAIT_ON_NETWORK_IPV4]
682 if isinstance(wait_on_ipv4_val, bool):
683 wait_on_ipv4 = wait_on_ipv4_val
684 else:
685 wait_on_ipv4 = bool(strtobool(wait_on_ipv4_val))
686 if WAIT_ON_NETWORK_IPV6 in wait_on_network:
687 wait_on_ipv6_val = wait_on_network[WAIT_ON_NETWORK_IPV6]
688 if isinstance(wait_on_ipv6_val, bool):
689 wait_on_ipv6 = wait_on_ipv6_val
690 else:
691 wait_on_ipv6 = bool(strtobool(wait_on_ipv6_val))
692
693 # Get information about the host.
694 host_info = None
695 while host_info == None:
696 host_info = get_host_info()
akutze16f5e82020-03-24 11:11:07 -0500697 if wait_on_ipv4:
698 ipv4_ready = False
699 if 'network' in host_info:
700 if 'interfaces' in host_info['network']:
701 if 'by-ipv4' in host_info['network']['interfaces']:
702 if len(host_info['network']['interfaces']['by-ipv4']) > 0:
703 ipv4_ready = True
704 if not ipv4_ready:
705 LOG.info("ipv4 not ready")
706 host_info = None
707 if wait_on_ipv6:
708 ipv6_ready = False
709 if 'network' in host_info:
710 if 'interfaces' in host_info['network']:
711 if 'by-ipv6' in host_info['network']['interfaces']:
712 if len(host_info['network']['interfaces']['by-ipv6']) > 0:
713 ipv6_ready = True
714 if not ipv6_ready:
715 LOG.info("ipv6 not ready")
716 host_info = None
akutz33dbfc22020-03-23 13:43:07 -0500717 if host_info == None:
718 LOG.info("waiting on network")
719 time.sleep(1)
720
721 return host_info
722
723
akutz10cd1402019-10-23 18:06:39 -0500724def get_data_access_method():
725 if os.environ.get(VMX_GUESTINFO, ""):
726 return VMX_GUESTINFO
yvespp8d58dfd2019-10-29 20:42:09 +0100727 if VMWARE_RPCTOOL:
728 return VMWARE_RPCTOOL
akutz10cd1402019-10-23 18:06:39 -0500729 return None
730
731
akutz45f316d2020-09-22 13:04:05 -0500732_MERGE_STRATEGY_ENV_VAR = 'CLOUD_INIT_VMWARE_GUEST_INFO_MERGE_STRATEGY'
733_MERGE_STRATEGY_DEEPMERGE = 'deepmerge'
734
735
736def merge_dicts(a, b):
737 merge_strategy = os.getenv(_MERGE_STRATEGY_ENV_VAR)
738 if merge_strategy == _MERGE_STRATEGY_DEEPMERGE:
739 try:
740 LOG.info('merging dictionaries with deepmerge strategy')
741 return merge_dicts_with_deep_merge(a, b)
742 except Exception as err:
743 LOG.error("deep merge failed: %s" % err)
744 LOG.info('merging dictionaries with stdlib strategy')
745 return merge_dicts_with_stdlib(a, b)
746
747
748def merge_dicts_with_deep_merge(a, b):
749 from deepmerge import always_merger
750 return always_merger.merge(a, b)
751
752
753def merge_dicts_with_stdlib(a, b):
754 for key, value in a.items():
755 if isinstance(value, dict):
756 node = b.setdefault(key, {})
757 merge_dicts_with_stdlib(value, node)
758 else:
759 b[key] = value
760 return b
761
762
akutzffc4dd52019-06-02 11:34:55 -0500763def main():
764 '''
765 Executed when this file is used as a program.
766 '''
akutz45f316d2020-09-22 13:04:05 -0500767 try:
768 logging.setupBasicLogging()
769 except Exception:
770 pass
akutz33dbfc22020-03-23 13:43:07 -0500771 metadata = {'wait-on-network': {'ipv4': True, 'ipv6': "false"},
772 'network': {'config': {'dhcp': True}}}
773 host_info = wait_on_network(metadata)
akutz45f316d2020-09-22 13:04:05 -0500774 metadata = merge_dicts(metadata, host_info)
akutzffc4dd52019-06-02 11:34:55 -0500775 print(util.json_dumps(metadata))
776
777
akutz0d1fce52019-06-01 18:54:29 -0500778if __name__ == "__main__":
akutzffc4dd52019-06-02 11:34:55 -0500779 main()
akutz0d1fce52019-06-01 18:54:29 -0500780
781# vi: ts=4 expandtab