blob: 2cb589a660dbc55007fbb850258037c1fc4ab814 [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
akutzffc4dd52019-06-02 11:34:55 -050026import json
akutz10cd1402019-10-23 18:06:39 -050027import os
akutzffc4dd52019-06-02 11:34:55 -050028import socket
akutz10cd1402019-10-23 18:06:39 -050029import string
akutz33dbfc22020-03-23 13:43:07 -050030import time
akutzffc4dd52019-06-02 11:34:55 -050031import zlib
akutz77457a62018-08-22 16:07:21 -050032
33from cloudinit import log as logging
34from cloudinit import sources
35from cloudinit import util
akutz6501f902018-08-24 12:19:05 -050036from cloudinit import safeyaml
akutz77457a62018-08-22 16:07:21 -050037
akutzffc4dd52019-06-02 11:34:55 -050038from deepmerge import always_merger
39import netifaces
40
akutz77457a62018-08-22 16:07:21 -050041LOG = logging.getLogger(__name__)
akutz0d1fce52019-06-01 18:54:29 -050042NOVAL = "No value found"
yvespp8d58dfd2019-10-29 20:42:09 +010043VMWARE_RPCTOOL = find_executable("vmware-rpctool")
akutz10cd1402019-10-23 18:06:39 -050044VMX_GUESTINFO = "VMX_GUESTINFO"
akutzdaf81f12019-12-08 11:20:33 -060045GUESTINFO_EMPTY_YAML_VAL = "---"
akutz909cf9a2019-12-09 09:17:00 -060046LOCAL_IPV4 = 'local-ipv4'
47LOCAL_IPV6 = 'local-ipv6'
Yves Peter59c869d2019-12-11 13:09:14 +010048CLEANUP_GUESTINFO = 'cleanup-guestinfo'
akutz33dbfc22020-03-23 13:43:07 -050049WAIT_ON_NETWORK = 'wait-on-network'
50WAIT_ON_NETWORK_IPV4 = 'ipv4'
51WAIT_ON_NETWORK_IPV6 = 'ipv6'
akutz77457a62018-08-22 16:07:21 -050052
akutz0d1fce52019-06-01 18:54:29 -050053
54class NetworkConfigError(Exception):
55 '''
56 NetworkConfigError is raised when there is an issue getting or
57 applying network configuration.
58 '''
59 pass
60
61
Andrew Kutz4f66b8b2018-09-16 18:28:59 -050062class DataSourceVMwareGuestInfo(sources.DataSource):
akutz0d1fce52019-06-01 18:54:29 -050063 '''
64 This cloud-init datasource was designed for use with CentOS 7,
65 which uses cloud-init 0.7.9. However, this datasource should
66 work with any Linux distribution for which cloud-init is
67 avaialble.
68
69 The documentation for cloud-init 0.7.9's datasource is
70 available at http://bit.ly/cloudinit-datasource-0-7-9. The
71 current documentation for cloud-init is found at
72 https://cloudinit.readthedocs.io/en/latest/.
73
74 Setting the hostname:
75 The hostname is set by way of the metadata key "local-hostname".
76
77 Setting the instance ID:
78 The instance ID may be set by way of the metadata key "instance-id".
79 However, if this value is absent then then the instance ID is
80 read from the file /sys/class/dmi/id/product_uuid.
81
82 Configuring the network:
83 The network is configured by setting the metadata key "network"
84 with a value consistent with Network Config Versions 1 or 2,
85 depending on the Linux distro's version of cloud-init:
86
87 Network Config Version 1 - http://bit.ly/cloudinit-net-conf-v1
88 Network Config Version 2 - http://bit.ly/cloudinit-net-conf-v2
89
90 For example, CentOS 7's official cloud-init package is version
91 0.7.9 and does not support Network Config Version 2. However,
92 this datasource still supports supplying Network Config Version 2
93 data as long as the Linux distro's cloud-init package is new
94 enough to parse the data.
95
96 The metadata key "network.encoding" may be used to indicate the
97 format of the metadata key "network". Valid encodings are base64
98 and gzip+base64.
99 '''
100
101 dsname = 'VMwareGuestInfo'
102
akutz77457a62018-08-22 16:07:21 -0500103 def __init__(self, sys_cfg, distro, paths, ud_proc=None):
104 sources.DataSource.__init__(self, sys_cfg, distro, paths, ud_proc)
akutz10cd1402019-10-23 18:06:39 -0500105 if not get_data_access_method():
yvespp8d58dfd2019-10-29 20:42:09 +0100106 LOG.error("Failed to find vmware-rpctool")
akutz77457a62018-08-22 16:07:21 -0500107
108 def get_data(self):
akutz0d1fce52019-06-01 18:54:29 -0500109 """
110 This method should really be _get_data in accordance with the most
111 recent versions of cloud-init. However, because the datasource
112 supports as far back as cloud-init 0.7.9, get_data is still used.
113
114 Because of this the method attempts to do some of the same things
115 that the get_data functions in newer versions of cloud-init do,
116 such as calling persist_instance_data.
117 """
akutzdbce3d92019-12-08 00:09:11 -0600118 data_access_method = get_data_access_method()
119 if not data_access_method:
yvespp8d58dfd2019-10-29 20:42:09 +0100120 LOG.error("vmware-rpctool is required to fetch guestinfo value")
akutz77457a62018-08-22 16:07:21 -0500121 return False
akutz6501f902018-08-24 12:19:05 -0500122
akutz0d1fce52019-06-01 18:54:29 -0500123 # Get the metadata.
124 self.metadata = load_metadata()
akutz6501f902018-08-24 12:19:05 -0500125
akutz0d1fce52019-06-01 18:54:29 -0500126 # Get the user data.
127 self.userdata_raw = guestinfo('userdata')
akutz6501f902018-08-24 12:19:05 -0500128
akutz0d1fce52019-06-01 18:54:29 -0500129 # Get the vendor data.
130 self.vendordata_raw = guestinfo('vendordata')
akutz6501f902018-08-24 12:19:05 -0500131
akutzdbce3d92019-12-08 00:09:11 -0600132 # Check to see if any of the guestinfo data should be removed.
Yves Peter59c869d2019-12-11 13:09:14 +0100133 if data_access_method == VMWARE_RPCTOOL and CLEANUP_GUESTINFO in self.metadata:
134 clear_guestinfo_keys(self.metadata[CLEANUP_GUESTINFO])
akutzdbce3d92019-12-08 00:09:11 -0600135
Keerthana Kf0d27ea2019-09-05 21:46:31 +0530136 if self.metadata or self.userdata_raw or self.vendordata_raw:
137 return True
138 else:
139 return False
akutz77457a62018-08-22 16:07:21 -0500140
akutz0d1fce52019-06-01 18:54:29 -0500141 def setup(self, is_new_instance):
142 """setup(is_new_instance)
143
144 This is called before user-data and vendor-data have been processed.
145
146 Unless the datasource has set mode to 'local', then networking
147 per 'fallback' or per 'network_config' will have been written and
148 brought up the OS at this point.
149 """
150
akutz33dbfc22020-03-23 13:43:07 -0500151 host_info = wait_on_network(self.metadata)
akutz0d1fce52019-06-01 18:54:29 -0500152 LOG.info("got host-info: %s", host_info)
akutzffc4dd52019-06-02 11:34:55 -0500153
akutz909cf9a2019-12-09 09:17:00 -0600154 # Reflect any possible local IPv4 or IPv6 addresses in the guest
155 # info.
156 advertise_local_ip_addrs(host_info)
157
akutzffc4dd52019-06-02 11:34:55 -0500158 # Ensure the metadata gets updated with information about the
159 # host, including the network interfaces, default IP addresses,
160 # etc.
161 self.metadata = always_merger.merge(self.metadata, host_info)
akutz0d1fce52019-06-01 18:54:29 -0500162
163 # Persist the instance data for versions of cloud-init that support
164 # doing so. This occurs here rather than in the get_data call in
165 # order to ensure that the network interfaces are up and can be
166 # persisted with the metadata.
167 try:
168 self.persist_instance_data()
169 except AttributeError:
170 pass
171
akutz6501f902018-08-24 12:19:05 -0500172 @property
173 def network_config(self):
akutz0d1fce52019-06-01 18:54:29 -0500174 if 'network' in self.metadata:
175 LOG.debug("using metadata network config")
176 else:
177 LOG.debug("using fallback network config")
178 self.metadata['network'] = {
179 'config': self.distro.generate_fallback_config(),
180 }
181 return self.metadata['network']['config']
akutz77457a62018-08-22 16:07:21 -0500182
183 def get_instance_id(self):
akutz6501f902018-08-24 12:19:05 -0500184 # Pull the instance ID out of the metadata if present. Otherwise
185 # read the file /sys/class/dmi/id/product_uuid for the instance ID.
186 if self.metadata and 'instance-id' in self.metadata:
187 return self.metadata['instance-id']
akutz77457a62018-08-22 16:07:21 -0500188 with open('/sys/class/dmi/id/product_uuid', 'r') as id_file:
Andrey Klimentyeve1c5ed42019-10-09 12:32:44 +0300189 self.metadata['instance-id'] = str(id_file.read()).rstrip().lower()
akutz0d1fce52019-06-01 18:54:29 -0500190 return self.metadata['instance-id']
akutz77457a62018-08-22 16:07:21 -0500191
Andrey Klimentyeva23229d2019-10-17 15:30:00 +0300192 def get_public_ssh_keys(self):
193 public_keys_data = ""
194 if 'public-keys-data' in self.metadata:
195 public_keys_data = self.metadata['public-keys-data'].splitlines()
196
197 public_keys = []
198 if not public_keys_data:
199 return public_keys
200
201 for public_key in public_keys_data:
202 public_keys.append(public_key)
203
204 return public_keys
205
akutz6501f902018-08-24 12:19:05 -0500206
akutz0d1fce52019-06-01 18:54:29 -0500207def decode(key, enc_type, data):
208 '''
209 decode returns the decoded string value of data
210 key is a string used to identify the data being decoded in log messages
211 ----
212 In py 2.7:
213 json.loads method takes string as input
214 zlib.decompress takes and returns a string
215 base64.b64decode takes and returns a string
216 -----
217 In py 3.6 and newer:
218 json.loads method takes bytes or string as input
219 zlib.decompress takes and returns a bytes
220 base64.b64decode takes bytes or string and returns bytes
221 -----
222 In py > 3, < 3.6:
223 json.loads method takes string as input
224 zlib.decompress takes and returns a bytes
225 base64.b64decode takes bytes or string and returns bytes
226 -----
227 Given the above conditions the output from zlib.decompress and
228 base64.b64decode would be bytes with newer python and str in older
229 version. Thus we would covert the output to str before returning
230 '''
231 LOG.debug("Getting encoded data for key=%s, enc=%s", key, enc_type)
akutz6501f902018-08-24 12:19:05 -0500232
akutz0d1fce52019-06-01 18:54:29 -0500233 raw_data = None
234 if enc_type == "gzip+base64" or enc_type == "gz+b64":
235 LOG.debug("Decoding %s format %s", enc_type, key)
236 raw_data = zlib.decompress(base64.b64decode(data), zlib.MAX_WBITS | 16)
237 elif enc_type == "base64" or enc_type == "b64":
238 LOG.debug("Decoding %s format %s", enc_type, key)
239 raw_data = base64.b64decode(data)
240 else:
241 LOG.debug("Plain-text data %s", key)
242 raw_data = data
Sidharth Surana3a421682018-10-10 15:42:08 -0700243
akutz0d1fce52019-06-01 18:54:29 -0500244 if isinstance(raw_data, bytes):
245 return raw_data.decode('utf-8')
246 return raw_data
247
248
akutzdaf81f12019-12-08 11:20:33 -0600249def get_none_if_empty_val(val):
250 '''
251 get_none_if_empty_val returns None if the provided value, once stripped
252 of its trailing whitespace, is empty or equal to GUESTINFO_EMPTY_YAML_VAL.
253
254 The return value is always a string, regardless of whether the input is
255 a bytes class or a string.
256 '''
257
258 # If the provided value is a bytes class, convert it to a string to
259 # simplify the rest of this function's logic.
260 if isinstance(val, bytes):
261 val = val.decode()
262
263 val = val.rstrip()
264 if len(val) == 0 or val == GUESTINFO_EMPTY_YAML_VAL:
265 return None
266 return val
267
268
akutz909cf9a2019-12-09 09:17:00 -0600269def advertise_local_ip_addrs(host_info):
270 '''
271 advertise_local_ip_addrs gets the local IP address information from
272 the provided host_info map and sets the addresses in the guestinfo
273 namespace
274 '''
275 if not host_info:
276 return
277
278 # Reflect any possible local IPv4 or IPv6 addresses in the guest
279 # info.
Yves Peter9dcf3dc2019-12-11 13:12:34 +0100280 local_ipv4 = host_info.get(LOCAL_IPV4)
akutz909cf9a2019-12-09 09:17:00 -0600281 if local_ipv4:
282 set_guestinfo_value(LOCAL_IPV4, local_ipv4)
283 LOG.info("advertised local ipv4 address %s in guestinfo", local_ipv4)
284
Yves Peter9dcf3dc2019-12-11 13:12:34 +0100285 local_ipv6 = host_info.get(LOCAL_IPV6)
akutz909cf9a2019-12-09 09:17:00 -0600286 if local_ipv6:
287 set_guestinfo_value(LOCAL_IPV6, local_ipv6)
288 LOG.info("advertised local ipv6 address %s in guestinfo", local_ipv6)
289
290
akutzdaf81f12019-12-08 11:20:33 -0600291def handle_returned_guestinfo_val(key, val):
292 '''
293 handle_returned_guestinfo_val returns the provided value if it is
294 not empty or set to GUESTINFO_EMPTY_YAML_VAL, otherwise None is
295 returned
296 '''
297 val = get_none_if_empty_val(val)
298 if val:
299 return val
300 LOG.debug("No value found for key %s", key)
301 return None
302
303
akutz0d1fce52019-06-01 18:54:29 -0500304def get_guestinfo_value(key):
305 '''
306 Returns a guestinfo value for the specified key.
307 '''
308 LOG.debug("Getting guestinfo value for key %s", key)
akutz10cd1402019-10-23 18:06:39 -0500309
310 data_access_method = get_data_access_method()
311
312 if data_access_method == VMX_GUESTINFO:
313 env_key = ("vmx.guestinfo." + key).upper().replace(".", "_", -1)
akutzdaf81f12019-12-08 11:20:33 -0600314 return handle_returned_guestinfo_val(key, os.environ.get(env_key, ""))
akutz10cd1402019-10-23 18:06:39 -0500315
yvespp8d58dfd2019-10-29 20:42:09 +0100316 if data_access_method == VMWARE_RPCTOOL:
akutz10cd1402019-10-23 18:06:39 -0500317 try:
318 (stdout, stderr) = util.subp(
yvespp8d58dfd2019-10-29 20:42:09 +0100319 [VMWARE_RPCTOOL, "info-get guestinfo." + key])
akutz10cd1402019-10-23 18:06:39 -0500320 if stderr == NOVAL:
321 LOG.debug("No value found for key %s", key)
322 elif not stdout:
323 LOG.error("Failed to get guestinfo value for key %s", key)
324 else:
akutzdaf81f12019-12-08 11:20:33 -0600325 return handle_returned_guestinfo_val(key, stdout)
akutz10cd1402019-10-23 18:06:39 -0500326 except util.ProcessExecutionError as error:
327 if error.stderr == NOVAL:
328 LOG.debug("No value found for key %s", key)
329 else:
330 util.logexc(
331 LOG, "Failed to get guestinfo value for key %s: %s", key, error)
332 except Exception:
akutz0d1fce52019-06-01 18:54:29 -0500333 util.logexc(
akutz10cd1402019-10-23 18:06:39 -0500334 LOG, "Unexpected error while trying to get guestinfo value for key %s", key)
335
akutz0d1fce52019-06-01 18:54:29 -0500336 return None
akutz6501f902018-08-24 12:19:05 -0500337
akutz0d1fce52019-06-01 18:54:29 -0500338
akutzdbce3d92019-12-08 00:09:11 -0600339def set_guestinfo_value(key, value):
340 '''
341 Sets a guestinfo value for the specified key. Set value to an empty string
342 to clear an existing guestinfo key.
343 '''
344
345 # If value is an empty string then set it to a single space as it is not
346 # possible to set a guestinfo key to an empty string. Setting a guestinfo
347 # key to a single space is as close as it gets to clearing an existing
348 # guestinfo key.
349 if value == "":
350 value = " "
351
352 LOG.debug("Setting guestinfo key=%s to value=%s", key, value)
353
354 data_access_method = get_data_access_method()
355
356 if data_access_method == VMX_GUESTINFO:
357 return True
358
359 if data_access_method == VMWARE_RPCTOOL:
360 try:
361 util.subp(
362 [VMWARE_RPCTOOL, ("info-set guestinfo.%s %s" % (key, value))])
363 return True
364 except util.ProcessExecutionError as error:
365 util.logexc(
366 LOG, "Failed to set guestinfo key=%s to value=%s: %s", key, value, error)
367 except Exception:
368 util.logexc(
369 LOG, "Unexpected error while trying to set guestinfo key=%s to value=%s", key, value)
370
371 return None
372
373
374def clear_guestinfo_keys(keys):
375 '''
akutzdaf81f12019-12-08 11:20:33 -0600376 clear_guestinfo_keys clears guestinfo of all of the keys in the given list.
377 each key will have its value set to "---". Since the value is valid YAML,
378 cloud-init can still read it if it tries.
akutzdbce3d92019-12-08 00:09:11 -0600379 '''
380 if not keys:
381 return
akutzdaf81f12019-12-08 11:20:33 -0600382 if not type(keys) in (list, tuple):
383 keys = [keys]
akutzdbce3d92019-12-08 00:09:11 -0600384 for key in keys:
akutzdaf81f12019-12-08 11:20:33 -0600385 LOG.info("clearing guestinfo.%s", key)
386 if not set_guestinfo_value(key, GUESTINFO_EMPTY_YAML_VAL):
387 LOG.error("failed to clear guestinfo.%s", key)
388 LOG.info("clearing guestinfo.%s.encoding", key)
389 if not set_guestinfo_value(key + ".encoding", ""):
390 LOG.error("failed to clear guestinfo.%s.encoding", key)
akutzdbce3d92019-12-08 00:09:11 -0600391
392
akutz0d1fce52019-06-01 18:54:29 -0500393def guestinfo(key):
394 '''
395 guestinfo returns the guestinfo value for the provided key, decoding
396 the value when required
397 '''
398 data = get_guestinfo_value(key)
399 if not data:
akutz6501f902018-08-24 12:19:05 -0500400 return None
akutz0d1fce52019-06-01 18:54:29 -0500401 enc_type = get_guestinfo_value(key + '.encoding')
402 return decode('guestinfo.' + key, enc_type, data)
403
404
405def load(data):
406 '''
407 load first attempts to unmarshal the provided data as JSON, and if
408 that fails then attempts to unmarshal the data as YAML. If data is
409 None then a new dictionary is returned.
410 '''
411 if not data:
412 return {}
413 try:
414 return json.loads(data)
415 except:
416 return safeyaml.load(data)
417
418
419def load_metadata():
420 '''
421 load_metadata loads the metadata from the guestinfo data, optionally
422 decoding the network config when required
423 '''
424 data = load(guestinfo('metadata'))
akutz84389c82019-06-02 19:24:38 -0500425 LOG.debug('loaded metadata %s', data)
akutz0d1fce52019-06-01 18:54:29 -0500426
427 network = None
428 if 'network' in data:
429 network = data['network']
430 del data['network']
431
432 network_enc = None
433 if 'network.encoding' in data:
434 network_enc = data['network.encoding']
435 del data['network.encoding']
436
437 if network:
akutz84389c82019-06-02 19:24:38 -0500438 LOG.debug('network data found')
439 if isinstance(network, collections.Mapping):
440 LOG.debug("network data copied to 'config' key")
441 network = {
442 'config': copy.deepcopy(network)
443 }
444 else:
445 LOG.debug("network data to be decoded %s", network)
akutz0d1fce52019-06-01 18:54:29 -0500446 dec_net = decode('metadata.network', network_enc, network)
akutz84389c82019-06-02 19:24:38 -0500447 network = {
448 'config': load(dec_net),
449 }
450
451 LOG.debug('network data %s', network)
akutz0d1fce52019-06-01 18:54:29 -0500452 data['network'] = network
453
454 return data
455
akutz6501f902018-08-24 12:19:05 -0500456
akutz77457a62018-08-22 16:07:21 -0500457def get_datasource_list(depends):
akutz0d1fce52019-06-01 18:54:29 -0500458 '''
akutz77457a62018-08-22 16:07:21 -0500459 Return a list of data sources that match this set of dependencies
akutz0d1fce52019-06-01 18:54:29 -0500460 '''
Andrew Kutz4f66b8b2018-09-16 18:28:59 -0500461 return [DataSourceVMwareGuestInfo]
akutz0d1fce52019-06-01 18:54:29 -0500462
463
akutzffc4dd52019-06-02 11:34:55 -0500464def get_default_ip_addrs():
465 '''
466 Returns the default IPv4 and IPv6 addresses based on the device(s) used for
467 the default route. Please note that None may be returned for either address
468 family if that family has no default route or if there are multiple
469 addresses associated with the device used by the default route for a given
470 address.
471 '''
472 gateways = netifaces.gateways()
473 if 'default' not in gateways:
474 return None, None
475
476 default_gw = gateways['default']
477 if netifaces.AF_INET not in default_gw and netifaces.AF_INET6 not in default_gw:
478 return None, None
479
480 ipv4 = None
481 ipv6 = None
482
483 gw4 = default_gw.get(netifaces.AF_INET)
484 if gw4:
485 _, dev4 = gw4
akutz0b519f72019-06-02 14:58:57 -0500486 addr4_fams = netifaces.ifaddresses(dev4)
487 if addr4_fams:
488 af_inet4 = addr4_fams.get(netifaces.AF_INET)
489 if af_inet4:
490 if len(af_inet4) > 1:
akutzffc4dd52019-06-02 11:34:55 -0500491 LOG.warn(
akutz0b519f72019-06-02 14:58:57 -0500492 "device %s has more than one ipv4 address: %s", dev4, af_inet4)
493 elif 'addr' in af_inet4[0]:
494 ipv4 = af_inet4[0]['addr']
akutzffc4dd52019-06-02 11:34:55 -0500495
496 # Try to get the default IPv6 address by first seeing if there is a default
akutz0b519f72019-06-02 14:58:57 -0500497 # IPv6 route.
akutzffc4dd52019-06-02 11:34:55 -0500498 gw6 = default_gw.get(netifaces.AF_INET6)
499 if gw6:
500 _, dev6 = gw6
501 addr6_fams = netifaces.ifaddresses(dev6)
502 if addr6_fams:
503 af_inet6 = addr6_fams.get(netifaces.AF_INET6)
504 if af_inet6:
505 if len(af_inet6) > 1:
506 LOG.warn(
507 "device %s has more than one ipv6 address: %s", dev6, af_inet6)
508 elif 'addr' in af_inet6[0]:
509 ipv6 = af_inet6[0]['addr']
akutz0b519f72019-06-02 14:58:57 -0500510
511 # If there is a default IPv4 address but not IPv6, then see if there is a
512 # single IPv6 address associated with the same device associated with the
513 # default IPv4 address.
514 if ipv4 and not ipv6:
515 af_inet6 = addr4_fams.get(netifaces.AF_INET6)
akutzffc4dd52019-06-02 11:34:55 -0500516 if af_inet6:
517 if len(af_inet6) > 1:
518 LOG.warn(
519 "device %s has more than one ipv6 address: %s", dev4, af_inet6)
520 elif 'addr' in af_inet6[0]:
521 ipv6 = af_inet6[0]['addr']
522
akutz0b519f72019-06-02 14:58:57 -0500523 # If there is a default IPv6 address but not IPv4, then see if there is a
524 # single IPv4 address associated with the same device associated with the
525 # default IPv6 address.
526 if not ipv4 and ipv6:
527 af_inet4 = addr6_fams.get(netifaces.AF_INET4)
528 if af_inet4:
529 if len(af_inet4) > 1:
530 LOG.warn(
531 "device %s has more than one ipv4 address: %s", dev6, af_inet4)
532 elif 'addr' in af_inet4[0]:
533 ipv4 = af_inet4[0]['addr']
534
akutzffc4dd52019-06-02 11:34:55 -0500535 return ipv4, ipv6
536
Keerthana Kf0d27ea2019-09-05 21:46:31 +0530537# patched socket.getfqdn() - see https://bugs.python.org/issue5004
akutz10cd1402019-10-23 18:06:39 -0500538
539
Keerthana Kf0d27ea2019-09-05 21:46:31 +0530540def getfqdn(name=''):
541 """Get fully qualified domain name from name.
542 An empty argument is interpreted as meaning the local host.
543 """
544 name = name.strip()
545 if not name or name == '0.0.0.0':
546 name = socket.gethostname()
547 try:
akutz10cd1402019-10-23 18:06:39 -0500548 addrs = socket.getaddrinfo(
549 name, None, 0, socket.SOCK_DGRAM, 0, socket.AI_CANONNAME)
Keerthana Kf0d27ea2019-09-05 21:46:31 +0530550 except socket.error:
551 pass
552 else:
553 for addr in addrs:
554 if addr[3]:
555 name = addr[3]
556 break
557 return name
akutzffc4dd52019-06-02 11:34:55 -0500558
akutz10cd1402019-10-23 18:06:39 -0500559
akutz0d1fce52019-06-01 18:54:29 -0500560def get_host_info():
561 '''
562 Returns host information such as the host name and network interfaces.
563 '''
akutz0d1fce52019-06-01 18:54:29 -0500564
565 host_info = {
566 'network': {
567 'interfaces': {
568 'by-mac': collections.OrderedDict(),
akutzffc4dd52019-06-02 11:34:55 -0500569 'by-ipv4': collections.OrderedDict(),
570 'by-ipv6': collections.OrderedDict(),
akutz0d1fce52019-06-01 18:54:29 -0500571 },
572 },
573 }
574
Keerthana Kf0d27ea2019-09-05 21:46:31 +0530575 hostname = getfqdn(socket.gethostname())
akutz0d1fce52019-06-01 18:54:29 -0500576 if hostname:
akutzffc4dd52019-06-02 11:34:55 -0500577 host_info['hostname'] = hostname
akutz0d1fce52019-06-01 18:54:29 -0500578 host_info['local-hostname'] = hostname
akutzb7a193d2019-12-09 11:36:32 -0600579 host_info['local_hostname'] = hostname
akutz0d1fce52019-06-01 18:54:29 -0500580
akutzffc4dd52019-06-02 11:34:55 -0500581 default_ipv4, default_ipv6 = get_default_ip_addrs()
582 if default_ipv4:
akutz909cf9a2019-12-09 09:17:00 -0600583 host_info[LOCAL_IPV4] = default_ipv4
akutzffc4dd52019-06-02 11:34:55 -0500584 if default_ipv6:
akutz909cf9a2019-12-09 09:17:00 -0600585 host_info[LOCAL_IPV6] = default_ipv6
akutzffc4dd52019-06-02 11:34:55 -0500586
akutz0d1fce52019-06-01 18:54:29 -0500587 by_mac = host_info['network']['interfaces']['by-mac']
akutzffc4dd52019-06-02 11:34:55 -0500588 by_ipv4 = host_info['network']['interfaces']['by-ipv4']
589 by_ipv6 = host_info['network']['interfaces']['by-ipv6']
akutz0d1fce52019-06-01 18:54:29 -0500590
591 ifaces = netifaces.interfaces()
592 for dev_name in ifaces:
593 addr_fams = netifaces.ifaddresses(dev_name)
594 af_link = addr_fams.get(netifaces.AF_LINK)
akutz0b519f72019-06-02 14:58:57 -0500595 af_inet4 = addr_fams.get(netifaces.AF_INET)
akutz0d1fce52019-06-01 18:54:29 -0500596 af_inet6 = addr_fams.get(netifaces.AF_INET6)
597
598 mac = None
599 if af_link and 'addr' in af_link[0]:
600 mac = af_link[0]['addr']
601
602 # Do not bother recording localhost
603 if mac == "00:00:00:00:00:00":
604 continue
605
akutz0b519f72019-06-02 14:58:57 -0500606 if mac and (af_inet4 or af_inet6):
akutz0d1fce52019-06-01 18:54:29 -0500607 key = mac
608 val = {}
akutz0b519f72019-06-02 14:58:57 -0500609 if af_inet4:
610 val["ipv4"] = af_inet4
akutz0d1fce52019-06-01 18:54:29 -0500611 if af_inet6:
akutzffc4dd52019-06-02 11:34:55 -0500612 val["ipv6"] = af_inet6
akutz0d1fce52019-06-01 18:54:29 -0500613 by_mac[key] = val
614
akutz0b519f72019-06-02 14:58:57 -0500615 if af_inet4:
616 for ip_info in af_inet4:
akutz0d1fce52019-06-01 18:54:29 -0500617 key = ip_info['addr']
akutzffc4dd52019-06-02 11:34:55 -0500618 if key == '127.0.0.1':
619 continue
akutz84389c82019-06-02 19:24:38 -0500620 val = copy.deepcopy(ip_info)
akutz0d1fce52019-06-01 18:54:29 -0500621 del val['addr']
622 if mac:
623 val['mac'] = mac
akutzffc4dd52019-06-02 11:34:55 -0500624 by_ipv4[key] = val
akutz0d1fce52019-06-01 18:54:29 -0500625
626 if af_inet6:
627 for ip_info in af_inet6:
628 key = ip_info['addr']
akutzffc4dd52019-06-02 11:34:55 -0500629 if key == '::1':
630 continue
akutz84389c82019-06-02 19:24:38 -0500631 val = copy.deepcopy(ip_info)
akutz0d1fce52019-06-01 18:54:29 -0500632 del val['addr']
633 if mac:
634 val['mac'] = mac
akutzffc4dd52019-06-02 11:34:55 -0500635 by_ipv6[key] = val
akutz0d1fce52019-06-01 18:54:29 -0500636
637 return host_info
638
639
akutz33dbfc22020-03-23 13:43:07 -0500640def wait_on_network(metadata):
641 # Determine whether we need to wait on the network coming online.
642 wait_on_ipv4 = False
643 wait_on_ipv6 = False
644 if WAIT_ON_NETWORK in metadata:
645 wait_on_network = metadata[WAIT_ON_NETWORK]
646 if WAIT_ON_NETWORK_IPV4 in wait_on_network:
647 wait_on_ipv4_val = wait_on_network[WAIT_ON_NETWORK_IPV4]
648 if isinstance(wait_on_ipv4_val, bool):
649 wait_on_ipv4 = wait_on_ipv4_val
650 else:
651 wait_on_ipv4 = bool(strtobool(wait_on_ipv4_val))
652 if WAIT_ON_NETWORK_IPV6 in wait_on_network:
653 wait_on_ipv6_val = wait_on_network[WAIT_ON_NETWORK_IPV6]
654 if isinstance(wait_on_ipv6_val, bool):
655 wait_on_ipv6 = wait_on_ipv6_val
656 else:
657 wait_on_ipv6 = bool(strtobool(wait_on_ipv6_val))
658
659 # Get information about the host.
660 host_info = None
661 while host_info == None:
662 host_info = get_host_info()
663 if wait_on_ipv4 and LOCAL_IPV4 not in host_info:
664 LOG.info("ipv4 not ready")
665 host_info = None
666 if wait_on_ipv6 and LOCAL_IPV6 not in host_info:
667 LOG.info("ipv6 not ready")
668 host_info = None
669 if host_info == None:
670 LOG.info("waiting on network")
671 time.sleep(1)
672
673 return host_info
674
675
akutz10cd1402019-10-23 18:06:39 -0500676def get_data_access_method():
677 if os.environ.get(VMX_GUESTINFO, ""):
678 return VMX_GUESTINFO
yvespp8d58dfd2019-10-29 20:42:09 +0100679 if VMWARE_RPCTOOL:
680 return VMWARE_RPCTOOL
akutz10cd1402019-10-23 18:06:39 -0500681 return None
682
683
akutzffc4dd52019-06-02 11:34:55 -0500684def main():
685 '''
686 Executed when this file is used as a program.
687 '''
akutz33dbfc22020-03-23 13:43:07 -0500688 metadata = {'wait-on-network': {'ipv4': True, 'ipv6': "false"},
689 'network': {'config': {'dhcp': True}}}
690 host_info = wait_on_network(metadata)
akutzffc4dd52019-06-02 11:34:55 -0500691 metadata = always_merger.merge(metadata, host_info)
692 print(util.json_dumps(metadata))
693
694
akutz0d1fce52019-06-01 18:54:29 -0500695if __name__ == "__main__":
akutzffc4dd52019-06-02 11:34:55 -0500696 main()
akutz0d1fce52019-06-01 18:54:29 -0500697
698# vi: ts=4 expandtab