blob: 8fea17d8c0b8721da5a97089ff25fc6594e288c6 [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
Shreenidhi Shedi15160772020-09-11 16:25:05 +053042# from cloud-init >= 20.3 subp is in its own module
Yves Peter444519c2020-09-10 17:19:46 +020043try:
Shreenidhi Shedi15160772020-09-11 16:25:05 +053044 from cloudinit.subp import subp, ProcessExecutionError
Yves Peter444519c2020-09-10 17:19:46 +020045except ImportError:
Shreenidhi Shedi15160772020-09-11 16:25:05 +053046 from cloudinit.util import subp, ProcessExecutionError
47
Yves Peter444519c2020-09-10 17:19:46 +020048
akutz77457a62018-08-22 16:07:21 -050049LOG = logging.getLogger(__name__)
akutz0d1fce52019-06-01 18:54:29 -050050NOVAL = "No value found"
yvespp8d58dfd2019-10-29 20:42:09 +010051VMWARE_RPCTOOL = find_executable("vmware-rpctool")
akutz10cd1402019-10-23 18:06:39 -050052VMX_GUESTINFO = "VMX_GUESTINFO"
akutzdaf81f12019-12-08 11:20:33 -060053GUESTINFO_EMPTY_YAML_VAL = "---"
akutz909cf9a2019-12-09 09:17:00 -060054LOCAL_IPV4 = 'local-ipv4'
55LOCAL_IPV6 = 'local-ipv6'
Yves Peter59c869d2019-12-11 13:09:14 +010056CLEANUP_GUESTINFO = 'cleanup-guestinfo'
akutz33dbfc22020-03-23 13:43:07 -050057WAIT_ON_NETWORK = 'wait-on-network'
58WAIT_ON_NETWORK_IPV4 = 'ipv4'
59WAIT_ON_NETWORK_IPV6 = 'ipv6'
akutz0d1fce52019-06-01 18:54:29 -050060
61class 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.
168 self.metadata = always_merger.merge(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
akutz0d1fce52019-06-01 18:54:29 -0500310def get_guestinfo_value(key):
311 '''
312 Returns a guestinfo value for the specified key.
313 '''
314 LOG.debug("Getting guestinfo value for key %s", key)
akutz10cd1402019-10-23 18:06:39 -0500315
316 data_access_method = get_data_access_method()
317
318 if data_access_method == VMX_GUESTINFO:
319 env_key = ("vmx.guestinfo." + key).upper().replace(".", "_", -1)
akutzdaf81f12019-12-08 11:20:33 -0600320 return handle_returned_guestinfo_val(key, os.environ.get(env_key, ""))
akutz10cd1402019-10-23 18:06:39 -0500321
yvespp8d58dfd2019-10-29 20:42:09 +0100322 if data_access_method == VMWARE_RPCTOOL:
akutz10cd1402019-10-23 18:06:39 -0500323 try:
Shreenidhi Shedi15160772020-09-11 16:25:05 +0530324 (stdout, stderr) = subp(
yvespp8d58dfd2019-10-29 20:42:09 +0100325 [VMWARE_RPCTOOL, "info-get guestinfo." + key])
akutz10cd1402019-10-23 18:06:39 -0500326 if stderr == NOVAL:
327 LOG.debug("No value found for key %s", key)
328 elif not stdout:
329 LOG.error("Failed to get guestinfo value for key %s", key)
330 else:
akutzdaf81f12019-12-08 11:20:33 -0600331 return handle_returned_guestinfo_val(key, stdout)
Shreenidhi Shedi15160772020-09-11 16:25:05 +0530332 except ProcessExecutionError as error:
akutz10cd1402019-10-23 18:06:39 -0500333 if error.stderr == NOVAL:
334 LOG.debug("No value found for key %s", key)
335 else:
336 util.logexc(
337 LOG, "Failed to get guestinfo value for key %s: %s", key, error)
338 except Exception:
akutz0d1fce52019-06-01 18:54:29 -0500339 util.logexc(
akutz10cd1402019-10-23 18:06:39 -0500340 LOG, "Unexpected error while trying to get guestinfo value for key %s", key)
341
akutz0d1fce52019-06-01 18:54:29 -0500342 return None
akutz6501f902018-08-24 12:19:05 -0500343
akutz0d1fce52019-06-01 18:54:29 -0500344
akutzdbce3d92019-12-08 00:09:11 -0600345def set_guestinfo_value(key, value):
346 '''
347 Sets a guestinfo value for the specified key. Set value to an empty string
348 to clear an existing guestinfo key.
349 '''
350
351 # If value is an empty string then set it to a single space as it is not
352 # possible to set a guestinfo key to an empty string. Setting a guestinfo
353 # key to a single space is as close as it gets to clearing an existing
354 # guestinfo key.
355 if value == "":
356 value = " "
357
358 LOG.debug("Setting guestinfo key=%s to value=%s", key, value)
359
360 data_access_method = get_data_access_method()
361
362 if data_access_method == VMX_GUESTINFO:
363 return True
364
365 if data_access_method == VMWARE_RPCTOOL:
366 try:
Shreenidhi Shedi15160772020-09-11 16:25:05 +0530367 subp([VMWARE_RPCTOOL, ("info-set guestinfo.%s %s" % (key, value))])
akutzdbce3d92019-12-08 00:09:11 -0600368 return True
Shreenidhi Shedi15160772020-09-11 16:25:05 +0530369 except ProcessExecutionError as error:
akutzdbce3d92019-12-08 00:09:11 -0600370 util.logexc(
371 LOG, "Failed to set guestinfo key=%s to value=%s: %s", key, value, error)
372 except Exception:
373 util.logexc(
374 LOG, "Unexpected error while trying to set guestinfo key=%s to value=%s", key, value)
375
376 return None
377
378
379def clear_guestinfo_keys(keys):
380 '''
akutzdaf81f12019-12-08 11:20:33 -0600381 clear_guestinfo_keys clears guestinfo of all of the keys in the given list.
382 each key will have its value set to "---". Since the value is valid YAML,
383 cloud-init can still read it if it tries.
akutzdbce3d92019-12-08 00:09:11 -0600384 '''
385 if not keys:
386 return
akutzdaf81f12019-12-08 11:20:33 -0600387 if not type(keys) in (list, tuple):
388 keys = [keys]
akutzdbce3d92019-12-08 00:09:11 -0600389 for key in keys:
akutzdaf81f12019-12-08 11:20:33 -0600390 LOG.info("clearing guestinfo.%s", key)
391 if not set_guestinfo_value(key, GUESTINFO_EMPTY_YAML_VAL):
392 LOG.error("failed to clear guestinfo.%s", key)
393 LOG.info("clearing guestinfo.%s.encoding", key)
394 if not set_guestinfo_value(key + ".encoding", ""):
395 LOG.error("failed to clear guestinfo.%s.encoding", key)
akutzdbce3d92019-12-08 00:09:11 -0600396
397
akutz0d1fce52019-06-01 18:54:29 -0500398def guestinfo(key):
399 '''
400 guestinfo returns the guestinfo value for the provided key, decoding
401 the value when required
402 '''
403 data = get_guestinfo_value(key)
404 if not data:
akutz6501f902018-08-24 12:19:05 -0500405 return None
akutz0d1fce52019-06-01 18:54:29 -0500406 enc_type = get_guestinfo_value(key + '.encoding')
407 return decode('guestinfo.' + key, enc_type, data)
408
409
410def load(data):
411 '''
412 load first attempts to unmarshal the provided data as JSON, and if
413 that fails then attempts to unmarshal the data as YAML. If data is
414 None then a new dictionary is returned.
415 '''
416 if not data:
417 return {}
418 try:
419 return json.loads(data)
420 except:
421 return safeyaml.load(data)
422
423
424def load_metadata():
425 '''
426 load_metadata loads the metadata from the guestinfo data, optionally
427 decoding the network config when required
428 '''
429 data = load(guestinfo('metadata'))
akutz84389c82019-06-02 19:24:38 -0500430 LOG.debug('loaded metadata %s', data)
akutz0d1fce52019-06-01 18:54:29 -0500431
432 network = None
433 if 'network' in data:
434 network = data['network']
435 del data['network']
436
437 network_enc = None
438 if 'network.encoding' in data:
439 network_enc = data['network.encoding']
440 del data['network.encoding']
441
442 if network:
akutz84389c82019-06-02 19:24:38 -0500443 LOG.debug('network data found')
444 if isinstance(network, collections.Mapping):
445 LOG.debug("network data copied to 'config' key")
446 network = {
447 'config': copy.deepcopy(network)
448 }
449 else:
450 LOG.debug("network data to be decoded %s", network)
akutz0d1fce52019-06-01 18:54:29 -0500451 dec_net = decode('metadata.network', network_enc, network)
akutz84389c82019-06-02 19:24:38 -0500452 network = {
453 'config': load(dec_net),
454 }
455
456 LOG.debug('network data %s', network)
akutz0d1fce52019-06-01 18:54:29 -0500457 data['network'] = network
458
459 return data
460
akutz6501f902018-08-24 12:19:05 -0500461
akutz77457a62018-08-22 16:07:21 -0500462def get_datasource_list(depends):
akutz0d1fce52019-06-01 18:54:29 -0500463 '''
akutz77457a62018-08-22 16:07:21 -0500464 Return a list of data sources that match this set of dependencies
akutz0d1fce52019-06-01 18:54:29 -0500465 '''
Andrew Kutz4f66b8b2018-09-16 18:28:59 -0500466 return [DataSourceVMwareGuestInfo]
akutz0d1fce52019-06-01 18:54:29 -0500467
468
akutzffc4dd52019-06-02 11:34:55 -0500469def get_default_ip_addrs():
470 '''
471 Returns the default IPv4 and IPv6 addresses based on the device(s) used for
472 the default route. Please note that None may be returned for either address
473 family if that family has no default route or if there are multiple
474 addresses associated with the device used by the default route for a given
475 address.
476 '''
477 gateways = netifaces.gateways()
478 if 'default' not in gateways:
479 return None, None
480
481 default_gw = gateways['default']
482 if netifaces.AF_INET not in default_gw and netifaces.AF_INET6 not in default_gw:
483 return None, None
484
485 ipv4 = None
486 ipv6 = None
487
488 gw4 = default_gw.get(netifaces.AF_INET)
489 if gw4:
490 _, dev4 = gw4
akutz0b519f72019-06-02 14:58:57 -0500491 addr4_fams = netifaces.ifaddresses(dev4)
492 if addr4_fams:
493 af_inet4 = addr4_fams.get(netifaces.AF_INET)
494 if af_inet4:
495 if len(af_inet4) > 1:
akutzffc4dd52019-06-02 11:34:55 -0500496 LOG.warn(
akutz0b519f72019-06-02 14:58:57 -0500497 "device %s has more than one ipv4 address: %s", dev4, af_inet4)
498 elif 'addr' in af_inet4[0]:
499 ipv4 = af_inet4[0]['addr']
akutzffc4dd52019-06-02 11:34:55 -0500500
501 # Try to get the default IPv6 address by first seeing if there is a default
akutz0b519f72019-06-02 14:58:57 -0500502 # IPv6 route.
akutzffc4dd52019-06-02 11:34:55 -0500503 gw6 = default_gw.get(netifaces.AF_INET6)
504 if gw6:
505 _, dev6 = gw6
506 addr6_fams = netifaces.ifaddresses(dev6)
507 if addr6_fams:
508 af_inet6 = addr6_fams.get(netifaces.AF_INET6)
509 if af_inet6:
510 if len(af_inet6) > 1:
511 LOG.warn(
512 "device %s has more than one ipv6 address: %s", dev6, af_inet6)
513 elif 'addr' in af_inet6[0]:
514 ipv6 = af_inet6[0]['addr']
akutz0b519f72019-06-02 14:58:57 -0500515
516 # If there is a default IPv4 address but not IPv6, then see if there is a
517 # single IPv6 address associated with the same device associated with the
518 # default IPv4 address.
519 if ipv4 and not ipv6:
520 af_inet6 = addr4_fams.get(netifaces.AF_INET6)
akutzffc4dd52019-06-02 11:34:55 -0500521 if af_inet6:
522 if len(af_inet6) > 1:
523 LOG.warn(
524 "device %s has more than one ipv6 address: %s", dev4, af_inet6)
525 elif 'addr' in af_inet6[0]:
526 ipv6 = af_inet6[0]['addr']
527
akutz0b519f72019-06-02 14:58:57 -0500528 # If there is a default IPv6 address but not IPv4, then see if there is a
529 # single IPv4 address associated with the same device associated with the
530 # default IPv6 address.
531 if not ipv4 and ipv6:
Hieu47f5c7f2020-05-20 10:11:08 +0700532 af_inet4 = addr6_fams.get(netifaces.AF_INET)
akutz0b519f72019-06-02 14:58:57 -0500533 if af_inet4:
534 if len(af_inet4) > 1:
535 LOG.warn(
536 "device %s has more than one ipv4 address: %s", dev6, af_inet4)
537 elif 'addr' in af_inet4[0]:
538 ipv4 = af_inet4[0]['addr']
539
akutzffc4dd52019-06-02 11:34:55 -0500540 return ipv4, ipv6
541
Keerthana Kf0d27ea2019-09-05 21:46:31 +0530542# patched socket.getfqdn() - see https://bugs.python.org/issue5004
akutz10cd1402019-10-23 18:06:39 -0500543
544
Keerthana Kf0d27ea2019-09-05 21:46:31 +0530545def getfqdn(name=''):
546 """Get fully qualified domain name from name.
547 An empty argument is interpreted as meaning the local host.
548 """
549 name = name.strip()
550 if not name or name == '0.0.0.0':
551 name = socket.gethostname()
552 try:
akutz10cd1402019-10-23 18:06:39 -0500553 addrs = socket.getaddrinfo(
554 name, None, 0, socket.SOCK_DGRAM, 0, socket.AI_CANONNAME)
Keerthana Kf0d27ea2019-09-05 21:46:31 +0530555 except socket.error:
556 pass
557 else:
558 for addr in addrs:
559 if addr[3]:
560 name = addr[3]
561 break
562 return name
akutzffc4dd52019-06-02 11:34:55 -0500563
akutz10cd1402019-10-23 18:06:39 -0500564
akutz1cce7fa2020-03-24 11:01:45 -0500565def is_valid_ip_addr(val):
566 """
567 Returns false if the address is loopback, link local or unspecified;
568 otherwise true is returned.
569 """
570 addr = None
571 try:
572 addr = ipaddress.ip_address(val)
573 except:
574 return False
575 if addr.is_link_local or addr.is_loopback or addr.is_unspecified:
576 return False
577 return True
578
579
akutz0d1fce52019-06-01 18:54:29 -0500580def get_host_info():
581 '''
582 Returns host information such as the host name and network interfaces.
583 '''
akutz0d1fce52019-06-01 18:54:29 -0500584
585 host_info = {
586 'network': {
587 'interfaces': {
588 'by-mac': collections.OrderedDict(),
akutzffc4dd52019-06-02 11:34:55 -0500589 'by-ipv4': collections.OrderedDict(),
590 'by-ipv6': collections.OrderedDict(),
akutz0d1fce52019-06-01 18:54:29 -0500591 },
592 },
593 }
594
Keerthana Kf0d27ea2019-09-05 21:46:31 +0530595 hostname = getfqdn(socket.gethostname())
akutz0d1fce52019-06-01 18:54:29 -0500596 if hostname:
akutzffc4dd52019-06-02 11:34:55 -0500597 host_info['hostname'] = hostname
akutz0d1fce52019-06-01 18:54:29 -0500598 host_info['local-hostname'] = hostname
akutzb7a193d2019-12-09 11:36:32 -0600599 host_info['local_hostname'] = hostname
akutz0d1fce52019-06-01 18:54:29 -0500600
akutzffc4dd52019-06-02 11:34:55 -0500601 default_ipv4, default_ipv6 = get_default_ip_addrs()
602 if default_ipv4:
akutz909cf9a2019-12-09 09:17:00 -0600603 host_info[LOCAL_IPV4] = default_ipv4
akutzffc4dd52019-06-02 11:34:55 -0500604 if default_ipv6:
akutz909cf9a2019-12-09 09:17:00 -0600605 host_info[LOCAL_IPV6] = default_ipv6
akutzffc4dd52019-06-02 11:34:55 -0500606
akutz0d1fce52019-06-01 18:54:29 -0500607 by_mac = host_info['network']['interfaces']['by-mac']
akutzffc4dd52019-06-02 11:34:55 -0500608 by_ipv4 = host_info['network']['interfaces']['by-ipv4']
609 by_ipv6 = host_info['network']['interfaces']['by-ipv6']
akutz0d1fce52019-06-01 18:54:29 -0500610
611 ifaces = netifaces.interfaces()
612 for dev_name in ifaces:
613 addr_fams = netifaces.ifaddresses(dev_name)
614 af_link = addr_fams.get(netifaces.AF_LINK)
akutz0b519f72019-06-02 14:58:57 -0500615 af_inet4 = addr_fams.get(netifaces.AF_INET)
akutz0d1fce52019-06-01 18:54:29 -0500616 af_inet6 = addr_fams.get(netifaces.AF_INET6)
617
618 mac = None
619 if af_link and 'addr' in af_link[0]:
620 mac = af_link[0]['addr']
621
622 # Do not bother recording localhost
623 if mac == "00:00:00:00:00:00":
624 continue
625
akutz0b519f72019-06-02 14:58:57 -0500626 if mac and (af_inet4 or af_inet6):
akutz0d1fce52019-06-01 18:54:29 -0500627 key = mac
628 val = {}
akutz0b519f72019-06-02 14:58:57 -0500629 if af_inet4:
akutz1cce7fa2020-03-24 11:01:45 -0500630 af_inet4_vals = []
631 for ip_info in af_inet4:
632 if not is_valid_ip_addr(ip_info['addr']):
633 continue
634 af_inet4_vals.append(ip_info)
635 val["ipv4"] = af_inet4_vals
akutz0d1fce52019-06-01 18:54:29 -0500636 if af_inet6:
akutz1cce7fa2020-03-24 11:01:45 -0500637 af_inet6_vals = []
638 for ip_info in af_inet6:
639 if not is_valid_ip_addr(ip_info['addr']):
640 continue
641 af_inet6_vals.append(ip_info)
642 val["ipv6"] = af_inet6_vals
akutz0d1fce52019-06-01 18:54:29 -0500643 by_mac[key] = val
644
akutz0b519f72019-06-02 14:58:57 -0500645 if af_inet4:
646 for ip_info in af_inet4:
akutz0d1fce52019-06-01 18:54:29 -0500647 key = ip_info['addr']
akutz1cce7fa2020-03-24 11:01:45 -0500648 if not is_valid_ip_addr(key):
akutzffc4dd52019-06-02 11:34:55 -0500649 continue
akutz84389c82019-06-02 19:24:38 -0500650 val = copy.deepcopy(ip_info)
akutz0d1fce52019-06-01 18:54:29 -0500651 del val['addr']
652 if mac:
653 val['mac'] = mac
akutzffc4dd52019-06-02 11:34:55 -0500654 by_ipv4[key] = val
akutz0d1fce52019-06-01 18:54:29 -0500655
656 if af_inet6:
657 for ip_info in af_inet6:
658 key = ip_info['addr']
akutz1cce7fa2020-03-24 11:01:45 -0500659 if not is_valid_ip_addr(key):
akutzffc4dd52019-06-02 11:34:55 -0500660 continue
akutz84389c82019-06-02 19:24:38 -0500661 val = copy.deepcopy(ip_info)
akutz0d1fce52019-06-01 18:54:29 -0500662 del val['addr']
663 if mac:
664 val['mac'] = mac
akutzffc4dd52019-06-02 11:34:55 -0500665 by_ipv6[key] = val
akutz0d1fce52019-06-01 18:54:29 -0500666
667 return host_info
668
669
akutz33dbfc22020-03-23 13:43:07 -0500670def wait_on_network(metadata):
671 # Determine whether we need to wait on the network coming online.
672 wait_on_ipv4 = False
673 wait_on_ipv6 = False
674 if WAIT_ON_NETWORK in metadata:
675 wait_on_network = metadata[WAIT_ON_NETWORK]
676 if WAIT_ON_NETWORK_IPV4 in wait_on_network:
677 wait_on_ipv4_val = wait_on_network[WAIT_ON_NETWORK_IPV4]
678 if isinstance(wait_on_ipv4_val, bool):
679 wait_on_ipv4 = wait_on_ipv4_val
680 else:
681 wait_on_ipv4 = bool(strtobool(wait_on_ipv4_val))
682 if WAIT_ON_NETWORK_IPV6 in wait_on_network:
683 wait_on_ipv6_val = wait_on_network[WAIT_ON_NETWORK_IPV6]
684 if isinstance(wait_on_ipv6_val, bool):
685 wait_on_ipv6 = wait_on_ipv6_val
686 else:
687 wait_on_ipv6 = bool(strtobool(wait_on_ipv6_val))
688
689 # Get information about the host.
690 host_info = None
691 while host_info == None:
692 host_info = get_host_info()
akutze16f5e82020-03-24 11:11:07 -0500693 if wait_on_ipv4:
694 ipv4_ready = False
695 if 'network' in host_info:
696 if 'interfaces' in host_info['network']:
697 if 'by-ipv4' in host_info['network']['interfaces']:
698 if len(host_info['network']['interfaces']['by-ipv4']) > 0:
699 ipv4_ready = True
700 if not ipv4_ready:
701 LOG.info("ipv4 not ready")
702 host_info = None
703 if wait_on_ipv6:
704 ipv6_ready = False
705 if 'network' in host_info:
706 if 'interfaces' in host_info['network']:
707 if 'by-ipv6' in host_info['network']['interfaces']:
708 if len(host_info['network']['interfaces']['by-ipv6']) > 0:
709 ipv6_ready = True
710 if not ipv6_ready:
711 LOG.info("ipv6 not ready")
712 host_info = None
akutz33dbfc22020-03-23 13:43:07 -0500713 if host_info == None:
714 LOG.info("waiting on network")
715 time.sleep(1)
716
717 return host_info
718
719
akutz10cd1402019-10-23 18:06:39 -0500720def get_data_access_method():
721 if os.environ.get(VMX_GUESTINFO, ""):
722 return VMX_GUESTINFO
yvespp8d58dfd2019-10-29 20:42:09 +0100723 if VMWARE_RPCTOOL:
724 return VMWARE_RPCTOOL
akutz10cd1402019-10-23 18:06:39 -0500725 return None
726
727
akutzffc4dd52019-06-02 11:34:55 -0500728def main():
729 '''
730 Executed when this file is used as a program.
731 '''
akutz33dbfc22020-03-23 13:43:07 -0500732 metadata = {'wait-on-network': {'ipv4': True, 'ipv6': "false"},
733 'network': {'config': {'dhcp': True}}}
734 host_info = wait_on_network(metadata)
akutzffc4dd52019-06-02 11:34:55 -0500735 metadata = always_merger.merge(metadata, host_info)
736 print(util.json_dumps(metadata))
737
738
akutz0d1fce52019-06-01 18:54:29 -0500739if __name__ == "__main__":
akutzffc4dd52019-06-02 11:34:55 -0500740 main()
akutz0d1fce52019-06-01 18:54:29 -0500741
742# vi: ts=4 expandtab