blob: e320fd24e886e42ba312b0bd9e6000d46ee8b2aa [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
akutz77457a62018-08-22 16:07:21 -050042LOG = logging.getLogger(__name__)
akutz0d1fce52019-06-01 18:54:29 -050043NOVAL = "No value found"
yvespp8d58dfd2019-10-29 20:42:09 +010044VMWARE_RPCTOOL = find_executable("vmware-rpctool")
akutz10cd1402019-10-23 18:06:39 -050045VMX_GUESTINFO = "VMX_GUESTINFO"
akutzdaf81f12019-12-08 11:20:33 -060046GUESTINFO_EMPTY_YAML_VAL = "---"
akutz909cf9a2019-12-09 09:17:00 -060047LOCAL_IPV4 = 'local-ipv4'
48LOCAL_IPV6 = 'local-ipv6'
Yves Peter59c869d2019-12-11 13:09:14 +010049CLEANUP_GUESTINFO = 'cleanup-guestinfo'
akutz33dbfc22020-03-23 13:43:07 -050050WAIT_ON_NETWORK = 'wait-on-network'
51WAIT_ON_NETWORK_IPV4 = 'ipv4'
52WAIT_ON_NETWORK_IPV6 = 'ipv6'
akutz77457a62018-08-22 16:07:21 -050053
akutz0d1fce52019-06-01 18:54:29 -050054
55class NetworkConfigError(Exception):
56 '''
57 NetworkConfigError is raised when there is an issue getting or
58 applying network configuration.
59 '''
60 pass
61
62
Andrew Kutz4f66b8b2018-09-16 18:28:59 -050063class DataSourceVMwareGuestInfo(sources.DataSource):
akutz0d1fce52019-06-01 18:54:29 -050064 '''
65 This cloud-init datasource was designed for use with CentOS 7,
66 which uses cloud-init 0.7.9. However, this datasource should
67 work with any Linux distribution for which cloud-init is
68 avaialble.
69
70 The documentation for cloud-init 0.7.9's datasource is
71 available at http://bit.ly/cloudinit-datasource-0-7-9. The
72 current documentation for cloud-init is found at
73 https://cloudinit.readthedocs.io/en/latest/.
74
75 Setting the hostname:
76 The hostname is set by way of the metadata key "local-hostname".
77
78 Setting the instance ID:
79 The instance ID may be set by way of the metadata key "instance-id".
80 However, if this value is absent then then the instance ID is
81 read from the file /sys/class/dmi/id/product_uuid.
82
83 Configuring the network:
84 The network is configured by setting the metadata key "network"
85 with a value consistent with Network Config Versions 1 or 2,
86 depending on the Linux distro's version of cloud-init:
87
88 Network Config Version 1 - http://bit.ly/cloudinit-net-conf-v1
89 Network Config Version 2 - http://bit.ly/cloudinit-net-conf-v2
90
91 For example, CentOS 7's official cloud-init package is version
92 0.7.9 and does not support Network Config Version 2. However,
93 this datasource still supports supplying Network Config Version 2
94 data as long as the Linux distro's cloud-init package is new
95 enough to parse the data.
96
97 The metadata key "network.encoding" may be used to indicate the
98 format of the metadata key "network". Valid encodings are base64
99 and gzip+base64.
100 '''
101
102 dsname = 'VMwareGuestInfo'
103
akutz77457a62018-08-22 16:07:21 -0500104 def __init__(self, sys_cfg, distro, paths, ud_proc=None):
105 sources.DataSource.__init__(self, sys_cfg, distro, paths, ud_proc)
akutz10cd1402019-10-23 18:06:39 -0500106 if not get_data_access_method():
yvespp8d58dfd2019-10-29 20:42:09 +0100107 LOG.error("Failed to find vmware-rpctool")
akutz77457a62018-08-22 16:07:21 -0500108
109 def get_data(self):
akutz0d1fce52019-06-01 18:54:29 -0500110 """
111 This method should really be _get_data in accordance with the most
112 recent versions of cloud-init. However, because the datasource
113 supports as far back as cloud-init 0.7.9, get_data is still used.
114
115 Because of this the method attempts to do some of the same things
116 that the get_data functions in newer versions of cloud-init do,
117 such as calling persist_instance_data.
118 """
akutzdbce3d92019-12-08 00:09:11 -0600119 data_access_method = get_data_access_method()
120 if not data_access_method:
yvespp8d58dfd2019-10-29 20:42:09 +0100121 LOG.error("vmware-rpctool is required to fetch guestinfo value")
akutz77457a62018-08-22 16:07:21 -0500122 return False
akutz6501f902018-08-24 12:19:05 -0500123
akutz0d1fce52019-06-01 18:54:29 -0500124 # Get the metadata.
125 self.metadata = load_metadata()
akutz6501f902018-08-24 12:19:05 -0500126
akutz0d1fce52019-06-01 18:54:29 -0500127 # Get the user data.
128 self.userdata_raw = guestinfo('userdata')
akutz6501f902018-08-24 12:19:05 -0500129
akutz0d1fce52019-06-01 18:54:29 -0500130 # Get the vendor data.
131 self.vendordata_raw = guestinfo('vendordata')
akutz6501f902018-08-24 12:19:05 -0500132
akutzdbce3d92019-12-08 00:09:11 -0600133 # Check to see if any of the guestinfo data should be removed.
Yves Peter59c869d2019-12-11 13:09:14 +0100134 if data_access_method == VMWARE_RPCTOOL and CLEANUP_GUESTINFO in self.metadata:
135 clear_guestinfo_keys(self.metadata[CLEANUP_GUESTINFO])
akutzdbce3d92019-12-08 00:09:11 -0600136
Keerthana Kf0d27ea2019-09-05 21:46:31 +0530137 if self.metadata or self.userdata_raw or self.vendordata_raw:
138 return True
139 else:
140 return False
akutz77457a62018-08-22 16:07:21 -0500141
akutz0d1fce52019-06-01 18:54:29 -0500142 def setup(self, is_new_instance):
143 """setup(is_new_instance)
144
145 This is called before user-data and vendor-data have been processed.
146
147 Unless the datasource has set mode to 'local', then networking
148 per 'fallback' or per 'network_config' will have been written and
149 brought up the OS at this point.
150 """
151
akutz33dbfc22020-03-23 13:43:07 -0500152 host_info = wait_on_network(self.metadata)
akutz0d1fce52019-06-01 18:54:29 -0500153 LOG.info("got host-info: %s", host_info)
akutzffc4dd52019-06-02 11:34:55 -0500154
akutz909cf9a2019-12-09 09:17:00 -0600155 # Reflect any possible local IPv4 or IPv6 addresses in the guest
156 # info.
157 advertise_local_ip_addrs(host_info)
158
akutzffc4dd52019-06-02 11:34:55 -0500159 # Ensure the metadata gets updated with information about the
160 # host, including the network interfaces, default IP addresses,
161 # etc.
162 self.metadata = always_merger.merge(self.metadata, host_info)
akutz0d1fce52019-06-01 18:54:29 -0500163
164 # Persist the instance data for versions of cloud-init that support
165 # doing so. This occurs here rather than in the get_data call in
166 # order to ensure that the network interfaces are up and can be
167 # persisted with the metadata.
168 try:
169 self.persist_instance_data()
170 except AttributeError:
171 pass
172
akutz6501f902018-08-24 12:19:05 -0500173 @property
174 def network_config(self):
akutz0d1fce52019-06-01 18:54:29 -0500175 if 'network' in self.metadata:
176 LOG.debug("using metadata network config")
177 else:
178 LOG.debug("using fallback network config")
179 self.metadata['network'] = {
180 'config': self.distro.generate_fallback_config(),
181 }
182 return self.metadata['network']['config']
akutz77457a62018-08-22 16:07:21 -0500183
184 def get_instance_id(self):
akutz6501f902018-08-24 12:19:05 -0500185 # Pull the instance ID out of the metadata if present. Otherwise
186 # read the file /sys/class/dmi/id/product_uuid for the instance ID.
187 if self.metadata and 'instance-id' in self.metadata:
188 return self.metadata['instance-id']
akutz77457a62018-08-22 16:07:21 -0500189 with open('/sys/class/dmi/id/product_uuid', 'r') as id_file:
Andrey Klimentyeve1c5ed42019-10-09 12:32:44 +0300190 self.metadata['instance-id'] = str(id_file.read()).rstrip().lower()
akutz0d1fce52019-06-01 18:54:29 -0500191 return self.metadata['instance-id']
akutz77457a62018-08-22 16:07:21 -0500192
Andrey Klimentyeva23229d2019-10-17 15:30:00 +0300193 def get_public_ssh_keys(self):
194 public_keys_data = ""
195 if 'public-keys-data' in self.metadata:
196 public_keys_data = self.metadata['public-keys-data'].splitlines()
197
198 public_keys = []
199 if not public_keys_data:
200 return public_keys
201
202 for public_key in public_keys_data:
203 public_keys.append(public_key)
204
205 return public_keys
206
akutz6501f902018-08-24 12:19:05 -0500207
akutz0d1fce52019-06-01 18:54:29 -0500208def decode(key, enc_type, data):
209 '''
210 decode returns the decoded string value of data
211 key is a string used to identify the data being decoded in log messages
212 ----
213 In py 2.7:
214 json.loads method takes string as input
215 zlib.decompress takes and returns a string
216 base64.b64decode takes and returns a string
217 -----
218 In py 3.6 and newer:
219 json.loads method takes bytes or string as input
220 zlib.decompress takes and returns a bytes
221 base64.b64decode takes bytes or string and returns bytes
222 -----
223 In py > 3, < 3.6:
224 json.loads method takes string as input
225 zlib.decompress takes and returns a bytes
226 base64.b64decode takes bytes or string and returns bytes
227 -----
228 Given the above conditions the output from zlib.decompress and
229 base64.b64decode would be bytes with newer python and str in older
230 version. Thus we would covert the output to str before returning
231 '''
232 LOG.debug("Getting encoded data for key=%s, enc=%s", key, enc_type)
akutz6501f902018-08-24 12:19:05 -0500233
akutz0d1fce52019-06-01 18:54:29 -0500234 raw_data = None
235 if enc_type == "gzip+base64" or enc_type == "gz+b64":
236 LOG.debug("Decoding %s format %s", enc_type, key)
237 raw_data = zlib.decompress(base64.b64decode(data), zlib.MAX_WBITS | 16)
238 elif enc_type == "base64" or enc_type == "b64":
239 LOG.debug("Decoding %s format %s", enc_type, key)
240 raw_data = base64.b64decode(data)
241 else:
242 LOG.debug("Plain-text data %s", key)
243 raw_data = data
Sidharth Surana3a421682018-10-10 15:42:08 -0700244
akutz0d1fce52019-06-01 18:54:29 -0500245 if isinstance(raw_data, bytes):
246 return raw_data.decode('utf-8')
247 return raw_data
248
249
akutzdaf81f12019-12-08 11:20:33 -0600250def get_none_if_empty_val(val):
251 '''
252 get_none_if_empty_val returns None if the provided value, once stripped
253 of its trailing whitespace, is empty or equal to GUESTINFO_EMPTY_YAML_VAL.
254
255 The return value is always a string, regardless of whether the input is
256 a bytes class or a string.
257 '''
258
259 # If the provided value is a bytes class, convert it to a string to
260 # simplify the rest of this function's logic.
261 if isinstance(val, bytes):
262 val = val.decode()
263
264 val = val.rstrip()
265 if len(val) == 0 or val == GUESTINFO_EMPTY_YAML_VAL:
266 return None
267 return val
268
269
akutz909cf9a2019-12-09 09:17:00 -0600270def advertise_local_ip_addrs(host_info):
271 '''
272 advertise_local_ip_addrs gets the local IP address information from
273 the provided host_info map and sets the addresses in the guestinfo
274 namespace
275 '''
276 if not host_info:
277 return
278
279 # Reflect any possible local IPv4 or IPv6 addresses in the guest
280 # info.
Yves Peter9dcf3dc2019-12-11 13:12:34 +0100281 local_ipv4 = host_info.get(LOCAL_IPV4)
akutz909cf9a2019-12-09 09:17:00 -0600282 if local_ipv4:
283 set_guestinfo_value(LOCAL_IPV4, local_ipv4)
284 LOG.info("advertised local ipv4 address %s in guestinfo", local_ipv4)
285
Yves Peter9dcf3dc2019-12-11 13:12:34 +0100286 local_ipv6 = host_info.get(LOCAL_IPV6)
akutz909cf9a2019-12-09 09:17:00 -0600287 if local_ipv6:
288 set_guestinfo_value(LOCAL_IPV6, local_ipv6)
289 LOG.info("advertised local ipv6 address %s in guestinfo", local_ipv6)
290
291
akutzdaf81f12019-12-08 11:20:33 -0600292def handle_returned_guestinfo_val(key, val):
293 '''
294 handle_returned_guestinfo_val returns the provided value if it is
295 not empty or set to GUESTINFO_EMPTY_YAML_VAL, otherwise None is
296 returned
297 '''
298 val = get_none_if_empty_val(val)
299 if val:
300 return val
301 LOG.debug("No value found for key %s", key)
302 return None
303
304
akutz0d1fce52019-06-01 18:54:29 -0500305def get_guestinfo_value(key):
306 '''
307 Returns a guestinfo value for the specified key.
308 '''
309 LOG.debug("Getting guestinfo value for key %s", key)
akutz10cd1402019-10-23 18:06:39 -0500310
311 data_access_method = get_data_access_method()
312
313 if data_access_method == VMX_GUESTINFO:
314 env_key = ("vmx.guestinfo." + key).upper().replace(".", "_", -1)
akutzdaf81f12019-12-08 11:20:33 -0600315 return handle_returned_guestinfo_val(key, os.environ.get(env_key, ""))
akutz10cd1402019-10-23 18:06:39 -0500316
yvespp8d58dfd2019-10-29 20:42:09 +0100317 if data_access_method == VMWARE_RPCTOOL:
akutz10cd1402019-10-23 18:06:39 -0500318 try:
319 (stdout, stderr) = util.subp(
yvespp8d58dfd2019-10-29 20:42:09 +0100320 [VMWARE_RPCTOOL, "info-get guestinfo." + key])
akutz10cd1402019-10-23 18:06:39 -0500321 if stderr == NOVAL:
322 LOG.debug("No value found for key %s", key)
323 elif not stdout:
324 LOG.error("Failed to get guestinfo value for key %s", key)
325 else:
akutzdaf81f12019-12-08 11:20:33 -0600326 return handle_returned_guestinfo_val(key, stdout)
akutz10cd1402019-10-23 18:06:39 -0500327 except util.ProcessExecutionError as error:
328 if error.stderr == NOVAL:
329 LOG.debug("No value found for key %s", key)
330 else:
331 util.logexc(
332 LOG, "Failed to get guestinfo value for key %s: %s", key, error)
333 except Exception:
akutz0d1fce52019-06-01 18:54:29 -0500334 util.logexc(
akutz10cd1402019-10-23 18:06:39 -0500335 LOG, "Unexpected error while trying to get guestinfo value for key %s", key)
336
akutz0d1fce52019-06-01 18:54:29 -0500337 return None
akutz6501f902018-08-24 12:19:05 -0500338
akutz0d1fce52019-06-01 18:54:29 -0500339
akutzdbce3d92019-12-08 00:09:11 -0600340def set_guestinfo_value(key, value):
341 '''
342 Sets a guestinfo value for the specified key. Set value to an empty string
343 to clear an existing guestinfo key.
344 '''
345
346 # If value is an empty string then set it to a single space as it is not
347 # possible to set a guestinfo key to an empty string. Setting a guestinfo
348 # key to a single space is as close as it gets to clearing an existing
349 # guestinfo key.
350 if value == "":
351 value = " "
352
353 LOG.debug("Setting guestinfo key=%s to value=%s", key, value)
354
355 data_access_method = get_data_access_method()
356
357 if data_access_method == VMX_GUESTINFO:
358 return True
359
360 if data_access_method == VMWARE_RPCTOOL:
361 try:
362 util.subp(
363 [VMWARE_RPCTOOL, ("info-set guestinfo.%s %s" % (key, value))])
364 return True
365 except util.ProcessExecutionError as error:
366 util.logexc(
367 LOG, "Failed to set guestinfo key=%s to value=%s: %s", key, value, error)
368 except Exception:
369 util.logexc(
370 LOG, "Unexpected error while trying to set guestinfo key=%s to value=%s", key, value)
371
372 return None
373
374
375def clear_guestinfo_keys(keys):
376 '''
akutzdaf81f12019-12-08 11:20:33 -0600377 clear_guestinfo_keys clears guestinfo of all of the keys in the given list.
378 each key will have its value set to "---". Since the value is valid YAML,
379 cloud-init can still read it if it tries.
akutzdbce3d92019-12-08 00:09:11 -0600380 '''
381 if not keys:
382 return
akutzdaf81f12019-12-08 11:20:33 -0600383 if not type(keys) in (list, tuple):
384 keys = [keys]
akutzdbce3d92019-12-08 00:09:11 -0600385 for key in keys:
akutzdaf81f12019-12-08 11:20:33 -0600386 LOG.info("clearing guestinfo.%s", key)
387 if not set_guestinfo_value(key, GUESTINFO_EMPTY_YAML_VAL):
388 LOG.error("failed to clear guestinfo.%s", key)
389 LOG.info("clearing guestinfo.%s.encoding", key)
390 if not set_guestinfo_value(key + ".encoding", ""):
391 LOG.error("failed to clear guestinfo.%s.encoding", key)
akutzdbce3d92019-12-08 00:09:11 -0600392
393
akutz0d1fce52019-06-01 18:54:29 -0500394def guestinfo(key):
395 '''
396 guestinfo returns the guestinfo value for the provided key, decoding
397 the value when required
398 '''
399 data = get_guestinfo_value(key)
400 if not data:
akutz6501f902018-08-24 12:19:05 -0500401 return None
akutz0d1fce52019-06-01 18:54:29 -0500402 enc_type = get_guestinfo_value(key + '.encoding')
403 return decode('guestinfo.' + key, enc_type, data)
404
405
406def load(data):
407 '''
408 load first attempts to unmarshal the provided data as JSON, and if
409 that fails then attempts to unmarshal the data as YAML. If data is
410 None then a new dictionary is returned.
411 '''
412 if not data:
413 return {}
414 try:
415 return json.loads(data)
416 except:
417 return safeyaml.load(data)
418
419
420def load_metadata():
421 '''
422 load_metadata loads the metadata from the guestinfo data, optionally
423 decoding the network config when required
424 '''
425 data = load(guestinfo('metadata'))
akutz84389c82019-06-02 19:24:38 -0500426 LOG.debug('loaded metadata %s', data)
akutz0d1fce52019-06-01 18:54:29 -0500427
428 network = None
429 if 'network' in data:
430 network = data['network']
431 del data['network']
432
433 network_enc = None
434 if 'network.encoding' in data:
435 network_enc = data['network.encoding']
436 del data['network.encoding']
437
438 if network:
akutz84389c82019-06-02 19:24:38 -0500439 LOG.debug('network data found')
440 if isinstance(network, collections.Mapping):
441 LOG.debug("network data copied to 'config' key")
442 network = {
443 'config': copy.deepcopy(network)
444 }
445 else:
446 LOG.debug("network data to be decoded %s", network)
akutz0d1fce52019-06-01 18:54:29 -0500447 dec_net = decode('metadata.network', network_enc, network)
akutz84389c82019-06-02 19:24:38 -0500448 network = {
449 'config': load(dec_net),
450 }
451
452 LOG.debug('network data %s', network)
akutz0d1fce52019-06-01 18:54:29 -0500453 data['network'] = network
454
455 return data
456
akutz6501f902018-08-24 12:19:05 -0500457
akutz77457a62018-08-22 16:07:21 -0500458def get_datasource_list(depends):
akutz0d1fce52019-06-01 18:54:29 -0500459 '''
akutz77457a62018-08-22 16:07:21 -0500460 Return a list of data sources that match this set of dependencies
akutz0d1fce52019-06-01 18:54:29 -0500461 '''
Andrew Kutz4f66b8b2018-09-16 18:28:59 -0500462 return [DataSourceVMwareGuestInfo]
akutz0d1fce52019-06-01 18:54:29 -0500463
464
akutzffc4dd52019-06-02 11:34:55 -0500465def get_default_ip_addrs():
466 '''
467 Returns the default IPv4 and IPv6 addresses based on the device(s) used for
468 the default route. Please note that None may be returned for either address
469 family if that family has no default route or if there are multiple
470 addresses associated with the device used by the default route for a given
471 address.
472 '''
473 gateways = netifaces.gateways()
474 if 'default' not in gateways:
475 return None, None
476
477 default_gw = gateways['default']
478 if netifaces.AF_INET not in default_gw and netifaces.AF_INET6 not in default_gw:
479 return None, None
480
481 ipv4 = None
482 ipv6 = None
483
484 gw4 = default_gw.get(netifaces.AF_INET)
485 if gw4:
486 _, dev4 = gw4
akutz0b519f72019-06-02 14:58:57 -0500487 addr4_fams = netifaces.ifaddresses(dev4)
488 if addr4_fams:
489 af_inet4 = addr4_fams.get(netifaces.AF_INET)
490 if af_inet4:
491 if len(af_inet4) > 1:
akutzffc4dd52019-06-02 11:34:55 -0500492 LOG.warn(
akutz0b519f72019-06-02 14:58:57 -0500493 "device %s has more than one ipv4 address: %s", dev4, af_inet4)
494 elif 'addr' in af_inet4[0]:
495 ipv4 = af_inet4[0]['addr']
akutzffc4dd52019-06-02 11:34:55 -0500496
497 # Try to get the default IPv6 address by first seeing if there is a default
akutz0b519f72019-06-02 14:58:57 -0500498 # IPv6 route.
akutzffc4dd52019-06-02 11:34:55 -0500499 gw6 = default_gw.get(netifaces.AF_INET6)
500 if gw6:
501 _, dev6 = gw6
502 addr6_fams = netifaces.ifaddresses(dev6)
503 if addr6_fams:
504 af_inet6 = addr6_fams.get(netifaces.AF_INET6)
505 if af_inet6:
506 if len(af_inet6) > 1:
507 LOG.warn(
508 "device %s has more than one ipv6 address: %s", dev6, af_inet6)
509 elif 'addr' in af_inet6[0]:
510 ipv6 = af_inet6[0]['addr']
akutz0b519f72019-06-02 14:58:57 -0500511
512 # If there is a default IPv4 address but not IPv6, then see if there is a
513 # single IPv6 address associated with the same device associated with the
514 # default IPv4 address.
515 if ipv4 and not ipv6:
516 af_inet6 = addr4_fams.get(netifaces.AF_INET6)
akutzffc4dd52019-06-02 11:34:55 -0500517 if af_inet6:
518 if len(af_inet6) > 1:
519 LOG.warn(
520 "device %s has more than one ipv6 address: %s", dev4, af_inet6)
521 elif 'addr' in af_inet6[0]:
522 ipv6 = af_inet6[0]['addr']
523
akutz0b519f72019-06-02 14:58:57 -0500524 # If there is a default IPv6 address but not IPv4, then see if there is a
525 # single IPv4 address associated with the same device associated with the
526 # default IPv6 address.
527 if not ipv4 and ipv6:
Hieu47f5c7f2020-05-20 10:11:08 +0700528 af_inet4 = addr6_fams.get(netifaces.AF_INET)
akutz0b519f72019-06-02 14:58:57 -0500529 if af_inet4:
530 if len(af_inet4) > 1:
531 LOG.warn(
532 "device %s has more than one ipv4 address: %s", dev6, af_inet4)
533 elif 'addr' in af_inet4[0]:
534 ipv4 = af_inet4[0]['addr']
535
akutzffc4dd52019-06-02 11:34:55 -0500536 return ipv4, ipv6
537
Keerthana Kf0d27ea2019-09-05 21:46:31 +0530538# patched socket.getfqdn() - see https://bugs.python.org/issue5004
akutz10cd1402019-10-23 18:06:39 -0500539
540
Keerthana Kf0d27ea2019-09-05 21:46:31 +0530541def getfqdn(name=''):
542 """Get fully qualified domain name from name.
543 An empty argument is interpreted as meaning the local host.
544 """
545 name = name.strip()
546 if not name or name == '0.0.0.0':
547 name = socket.gethostname()
548 try:
akutz10cd1402019-10-23 18:06:39 -0500549 addrs = socket.getaddrinfo(
550 name, None, 0, socket.SOCK_DGRAM, 0, socket.AI_CANONNAME)
Keerthana Kf0d27ea2019-09-05 21:46:31 +0530551 except socket.error:
552 pass
553 else:
554 for addr in addrs:
555 if addr[3]:
556 name = addr[3]
557 break
558 return name
akutzffc4dd52019-06-02 11:34:55 -0500559
akutz10cd1402019-10-23 18:06:39 -0500560
akutz1cce7fa2020-03-24 11:01:45 -0500561def is_valid_ip_addr(val):
562 """
563 Returns false if the address is loopback, link local or unspecified;
564 otherwise true is returned.
565 """
566 addr = None
567 try:
568 addr = ipaddress.ip_address(val)
569 except:
570 return False
571 if addr.is_link_local or addr.is_loopback or addr.is_unspecified:
572 return False
573 return True
574
575
akutz0d1fce52019-06-01 18:54:29 -0500576def get_host_info():
577 '''
578 Returns host information such as the host name and network interfaces.
579 '''
akutz0d1fce52019-06-01 18:54:29 -0500580
581 host_info = {
582 'network': {
583 'interfaces': {
584 'by-mac': collections.OrderedDict(),
akutzffc4dd52019-06-02 11:34:55 -0500585 'by-ipv4': collections.OrderedDict(),
586 'by-ipv6': collections.OrderedDict(),
akutz0d1fce52019-06-01 18:54:29 -0500587 },
588 },
589 }
590
Keerthana Kf0d27ea2019-09-05 21:46:31 +0530591 hostname = getfqdn(socket.gethostname())
akutz0d1fce52019-06-01 18:54:29 -0500592 if hostname:
akutzffc4dd52019-06-02 11:34:55 -0500593 host_info['hostname'] = hostname
akutz0d1fce52019-06-01 18:54:29 -0500594 host_info['local-hostname'] = hostname
akutzb7a193d2019-12-09 11:36:32 -0600595 host_info['local_hostname'] = hostname
akutz0d1fce52019-06-01 18:54:29 -0500596
akutzffc4dd52019-06-02 11:34:55 -0500597 default_ipv4, default_ipv6 = get_default_ip_addrs()
598 if default_ipv4:
akutz909cf9a2019-12-09 09:17:00 -0600599 host_info[LOCAL_IPV4] = default_ipv4
akutzffc4dd52019-06-02 11:34:55 -0500600 if default_ipv6:
akutz909cf9a2019-12-09 09:17:00 -0600601 host_info[LOCAL_IPV6] = default_ipv6
akutzffc4dd52019-06-02 11:34:55 -0500602
akutz0d1fce52019-06-01 18:54:29 -0500603 by_mac = host_info['network']['interfaces']['by-mac']
akutzffc4dd52019-06-02 11:34:55 -0500604 by_ipv4 = host_info['network']['interfaces']['by-ipv4']
605 by_ipv6 = host_info['network']['interfaces']['by-ipv6']
akutz0d1fce52019-06-01 18:54:29 -0500606
607 ifaces = netifaces.interfaces()
608 for dev_name in ifaces:
609 addr_fams = netifaces.ifaddresses(dev_name)
610 af_link = addr_fams.get(netifaces.AF_LINK)
akutz0b519f72019-06-02 14:58:57 -0500611 af_inet4 = addr_fams.get(netifaces.AF_INET)
akutz0d1fce52019-06-01 18:54:29 -0500612 af_inet6 = addr_fams.get(netifaces.AF_INET6)
613
614 mac = None
615 if af_link and 'addr' in af_link[0]:
616 mac = af_link[0]['addr']
617
618 # Do not bother recording localhost
619 if mac == "00:00:00:00:00:00":
620 continue
621
akutz0b519f72019-06-02 14:58:57 -0500622 if mac and (af_inet4 or af_inet6):
akutz0d1fce52019-06-01 18:54:29 -0500623 key = mac
624 val = {}
akutz0b519f72019-06-02 14:58:57 -0500625 if af_inet4:
akutz1cce7fa2020-03-24 11:01:45 -0500626 af_inet4_vals = []
627 for ip_info in af_inet4:
628 if not is_valid_ip_addr(ip_info['addr']):
629 continue
630 af_inet4_vals.append(ip_info)
631 val["ipv4"] = af_inet4_vals
akutz0d1fce52019-06-01 18:54:29 -0500632 if af_inet6:
akutz1cce7fa2020-03-24 11:01:45 -0500633 af_inet6_vals = []
634 for ip_info in af_inet6:
635 if not is_valid_ip_addr(ip_info['addr']):
636 continue
637 af_inet6_vals.append(ip_info)
638 val["ipv6"] = af_inet6_vals
akutz0d1fce52019-06-01 18:54:29 -0500639 by_mac[key] = val
640
akutz0b519f72019-06-02 14:58:57 -0500641 if af_inet4:
642 for ip_info in af_inet4:
akutz0d1fce52019-06-01 18:54:29 -0500643 key = ip_info['addr']
akutz1cce7fa2020-03-24 11:01:45 -0500644 if not is_valid_ip_addr(key):
akutzffc4dd52019-06-02 11:34:55 -0500645 continue
akutz84389c82019-06-02 19:24:38 -0500646 val = copy.deepcopy(ip_info)
akutz0d1fce52019-06-01 18:54:29 -0500647 del val['addr']
648 if mac:
649 val['mac'] = mac
akutzffc4dd52019-06-02 11:34:55 -0500650 by_ipv4[key] = val
akutz0d1fce52019-06-01 18:54:29 -0500651
652 if af_inet6:
653 for ip_info in af_inet6:
654 key = ip_info['addr']
akutz1cce7fa2020-03-24 11:01:45 -0500655 if not is_valid_ip_addr(key):
akutzffc4dd52019-06-02 11:34:55 -0500656 continue
akutz84389c82019-06-02 19:24:38 -0500657 val = copy.deepcopy(ip_info)
akutz0d1fce52019-06-01 18:54:29 -0500658 del val['addr']
659 if mac:
660 val['mac'] = mac
akutzffc4dd52019-06-02 11:34:55 -0500661 by_ipv6[key] = val
akutz0d1fce52019-06-01 18:54:29 -0500662
663 return host_info
664
665
akutz33dbfc22020-03-23 13:43:07 -0500666def wait_on_network(metadata):
667 # Determine whether we need to wait on the network coming online.
668 wait_on_ipv4 = False
669 wait_on_ipv6 = False
670 if WAIT_ON_NETWORK in metadata:
671 wait_on_network = metadata[WAIT_ON_NETWORK]
672 if WAIT_ON_NETWORK_IPV4 in wait_on_network:
673 wait_on_ipv4_val = wait_on_network[WAIT_ON_NETWORK_IPV4]
674 if isinstance(wait_on_ipv4_val, bool):
675 wait_on_ipv4 = wait_on_ipv4_val
676 else:
677 wait_on_ipv4 = bool(strtobool(wait_on_ipv4_val))
678 if WAIT_ON_NETWORK_IPV6 in wait_on_network:
679 wait_on_ipv6_val = wait_on_network[WAIT_ON_NETWORK_IPV6]
680 if isinstance(wait_on_ipv6_val, bool):
681 wait_on_ipv6 = wait_on_ipv6_val
682 else:
683 wait_on_ipv6 = bool(strtobool(wait_on_ipv6_val))
684
685 # Get information about the host.
686 host_info = None
687 while host_info == None:
688 host_info = get_host_info()
akutze16f5e82020-03-24 11:11:07 -0500689 if wait_on_ipv4:
690 ipv4_ready = False
691 if 'network' in host_info:
692 if 'interfaces' in host_info['network']:
693 if 'by-ipv4' in host_info['network']['interfaces']:
694 if len(host_info['network']['interfaces']['by-ipv4']) > 0:
695 ipv4_ready = True
696 if not ipv4_ready:
697 LOG.info("ipv4 not ready")
698 host_info = None
699 if wait_on_ipv6:
700 ipv6_ready = False
701 if 'network' in host_info:
702 if 'interfaces' in host_info['network']:
703 if 'by-ipv6' in host_info['network']['interfaces']:
704 if len(host_info['network']['interfaces']['by-ipv6']) > 0:
705 ipv6_ready = True
706 if not ipv6_ready:
707 LOG.info("ipv6 not ready")
708 host_info = None
akutz33dbfc22020-03-23 13:43:07 -0500709 if host_info == None:
710 LOG.info("waiting on network")
711 time.sleep(1)
712
713 return host_info
714
715
akutz10cd1402019-10-23 18:06:39 -0500716def get_data_access_method():
717 if os.environ.get(VMX_GUESTINFO, ""):
718 return VMX_GUESTINFO
yvespp8d58dfd2019-10-29 20:42:09 +0100719 if VMWARE_RPCTOOL:
720 return VMWARE_RPCTOOL
akutz10cd1402019-10-23 18:06:39 -0500721 return None
722
723
akutzffc4dd52019-06-02 11:34:55 -0500724def main():
725 '''
726 Executed when this file is used as a program.
727 '''
akutz33dbfc22020-03-23 13:43:07 -0500728 metadata = {'wait-on-network': {'ipv4': True, 'ipv6': "false"},
729 'network': {'config': {'dhcp': True}}}
730 host_info = wait_on_network(metadata)
akutzffc4dd52019-06-02 11:34:55 -0500731 metadata = always_merger.merge(metadata, host_info)
732 print(util.json_dumps(metadata))
733
734
akutz0d1fce52019-06-01 18:54:29 -0500735if __name__ == "__main__":
akutzffc4dd52019-06-02 11:34:55 -0500736 main()
akutz0d1fce52019-06-01 18:54:29 -0500737
738# vi: ts=4 expandtab