blob: 3cf3450780e53bc2e1e0d5589ad391632c887ad5 [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
akutzffc4dd52019-06-02 11:34:55 -050025import json
akutz10cd1402019-10-23 18:06:39 -050026import os
akutzffc4dd52019-06-02 11:34:55 -050027import socket
akutz10cd1402019-10-23 18:06:39 -050028import string
akutzffc4dd52019-06-02 11:34:55 -050029import zlib
akutz77457a62018-08-22 16:07:21 -050030
31from cloudinit import log as logging
32from cloudinit import sources
33from cloudinit import util
akutz6501f902018-08-24 12:19:05 -050034from cloudinit import safeyaml
akutz77457a62018-08-22 16:07:21 -050035
akutzffc4dd52019-06-02 11:34:55 -050036from deepmerge import always_merger
37import netifaces
38
akutz77457a62018-08-22 16:07:21 -050039LOG = logging.getLogger(__name__)
akutz0d1fce52019-06-01 18:54:29 -050040NOVAL = "No value found"
yvespp8d58dfd2019-10-29 20:42:09 +010041VMWARE_RPCTOOL = find_executable("vmware-rpctool")
akutz10cd1402019-10-23 18:06:39 -050042VMX_GUESTINFO = "VMX_GUESTINFO"
akutz77457a62018-08-22 16:07:21 -050043
akutz0d1fce52019-06-01 18:54:29 -050044
45class NetworkConfigError(Exception):
46 '''
47 NetworkConfigError is raised when there is an issue getting or
48 applying network configuration.
49 '''
50 pass
51
52
Andrew Kutz4f66b8b2018-09-16 18:28:59 -050053class DataSourceVMwareGuestInfo(sources.DataSource):
akutz0d1fce52019-06-01 18:54:29 -050054 '''
55 This cloud-init datasource was designed for use with CentOS 7,
56 which uses cloud-init 0.7.9. However, this datasource should
57 work with any Linux distribution for which cloud-init is
58 avaialble.
59
60 The documentation for cloud-init 0.7.9's datasource is
61 available at http://bit.ly/cloudinit-datasource-0-7-9. The
62 current documentation for cloud-init is found at
63 https://cloudinit.readthedocs.io/en/latest/.
64
65 Setting the hostname:
66 The hostname is set by way of the metadata key "local-hostname".
67
68 Setting the instance ID:
69 The instance ID may be set by way of the metadata key "instance-id".
70 However, if this value is absent then then the instance ID is
71 read from the file /sys/class/dmi/id/product_uuid.
72
73 Configuring the network:
74 The network is configured by setting the metadata key "network"
75 with a value consistent with Network Config Versions 1 or 2,
76 depending on the Linux distro's version of cloud-init:
77
78 Network Config Version 1 - http://bit.ly/cloudinit-net-conf-v1
79 Network Config Version 2 - http://bit.ly/cloudinit-net-conf-v2
80
81 For example, CentOS 7's official cloud-init package is version
82 0.7.9 and does not support Network Config Version 2. However,
83 this datasource still supports supplying Network Config Version 2
84 data as long as the Linux distro's cloud-init package is new
85 enough to parse the data.
86
87 The metadata key "network.encoding" may be used to indicate the
88 format of the metadata key "network". Valid encodings are base64
89 and gzip+base64.
90 '''
91
92 dsname = 'VMwareGuestInfo'
93
akutz77457a62018-08-22 16:07:21 -050094 def __init__(self, sys_cfg, distro, paths, ud_proc=None):
95 sources.DataSource.__init__(self, sys_cfg, distro, paths, ud_proc)
akutz10cd1402019-10-23 18:06:39 -050096 if not get_data_access_method():
yvespp8d58dfd2019-10-29 20:42:09 +010097 LOG.error("Failed to find vmware-rpctool")
akutz77457a62018-08-22 16:07:21 -050098
99 def get_data(self):
akutz0d1fce52019-06-01 18:54:29 -0500100 """
101 This method should really be _get_data in accordance with the most
102 recent versions of cloud-init. However, because the datasource
103 supports as far back as cloud-init 0.7.9, get_data is still used.
104
105 Because of this the method attempts to do some of the same things
106 that the get_data functions in newer versions of cloud-init do,
107 such as calling persist_instance_data.
108 """
akutz10cd1402019-10-23 18:06:39 -0500109 if not get_data_access_method():
yvespp8d58dfd2019-10-29 20:42:09 +0100110 LOG.error("vmware-rpctool is required to fetch guestinfo value")
akutz77457a62018-08-22 16:07:21 -0500111 return False
akutz6501f902018-08-24 12:19:05 -0500112
akutz0d1fce52019-06-01 18:54:29 -0500113 # Get the metadata.
114 self.metadata = load_metadata()
akutz6501f902018-08-24 12:19:05 -0500115
akutz0d1fce52019-06-01 18:54:29 -0500116 # Get the user data.
117 self.userdata_raw = guestinfo('userdata')
akutz6501f902018-08-24 12:19:05 -0500118
akutz0d1fce52019-06-01 18:54:29 -0500119 # Get the vendor data.
120 self.vendordata_raw = guestinfo('vendordata')
akutz6501f902018-08-24 12:19:05 -0500121
Keerthana Kf0d27ea2019-09-05 21:46:31 +0530122 if self.metadata or self.userdata_raw or self.vendordata_raw:
123 return True
124 else:
125 return False
akutz77457a62018-08-22 16:07:21 -0500126
akutz0d1fce52019-06-01 18:54:29 -0500127 def setup(self, is_new_instance):
128 """setup(is_new_instance)
129
130 This is called before user-data and vendor-data have been processed.
131
132 Unless the datasource has set mode to 'local', then networking
133 per 'fallback' or per 'network_config' will have been written and
134 brought up the OS at this point.
135 """
136
akutzffc4dd52019-06-02 11:34:55 -0500137 # Get information about the host.
akutz0d1fce52019-06-01 18:54:29 -0500138 host_info = get_host_info()
139 LOG.info("got host-info: %s", host_info)
akutzffc4dd52019-06-02 11:34:55 -0500140
141 # Ensure the metadata gets updated with information about the
142 # host, including the network interfaces, default IP addresses,
143 # etc.
144 self.metadata = always_merger.merge(self.metadata, host_info)
akutz0d1fce52019-06-01 18:54:29 -0500145
146 # Persist the instance data for versions of cloud-init that support
147 # doing so. This occurs here rather than in the get_data call in
148 # order to ensure that the network interfaces are up and can be
149 # persisted with the metadata.
150 try:
151 self.persist_instance_data()
152 except AttributeError:
153 pass
154
akutz6501f902018-08-24 12:19:05 -0500155 @property
156 def network_config(self):
akutz0d1fce52019-06-01 18:54:29 -0500157 if 'network' in self.metadata:
158 LOG.debug("using metadata network config")
159 else:
160 LOG.debug("using fallback network config")
161 self.metadata['network'] = {
162 'config': self.distro.generate_fallback_config(),
163 }
164 return self.metadata['network']['config']
akutz77457a62018-08-22 16:07:21 -0500165
166 def get_instance_id(self):
akutz6501f902018-08-24 12:19:05 -0500167 # Pull the instance ID out of the metadata if present. Otherwise
168 # read the file /sys/class/dmi/id/product_uuid for the instance ID.
169 if self.metadata and 'instance-id' in self.metadata:
170 return self.metadata['instance-id']
akutz77457a62018-08-22 16:07:21 -0500171 with open('/sys/class/dmi/id/product_uuid', 'r') as id_file:
Andrey Klimentyeve1c5ed42019-10-09 12:32:44 +0300172 self.metadata['instance-id'] = str(id_file.read()).rstrip().lower()
akutz0d1fce52019-06-01 18:54:29 -0500173 return self.metadata['instance-id']
akutz77457a62018-08-22 16:07:21 -0500174
Andrey Klimentyeva23229d2019-10-17 15:30:00 +0300175 def get_public_ssh_keys(self):
176 public_keys_data = ""
177 if 'public-keys-data' in self.metadata:
178 public_keys_data = self.metadata['public-keys-data'].splitlines()
179
180 public_keys = []
181 if not public_keys_data:
182 return public_keys
183
184 for public_key in public_keys_data:
185 public_keys.append(public_key)
186
187 return public_keys
188
akutz6501f902018-08-24 12:19:05 -0500189
akutz0d1fce52019-06-01 18:54:29 -0500190def decode(key, enc_type, data):
191 '''
192 decode returns the decoded string value of data
193 key is a string used to identify the data being decoded in log messages
194 ----
195 In py 2.7:
196 json.loads method takes string as input
197 zlib.decompress takes and returns a string
198 base64.b64decode takes and returns a string
199 -----
200 In py 3.6 and newer:
201 json.loads method takes bytes or string as input
202 zlib.decompress takes and returns a bytes
203 base64.b64decode takes bytes or string and returns bytes
204 -----
205 In py > 3, < 3.6:
206 json.loads method takes string as input
207 zlib.decompress takes and returns a bytes
208 base64.b64decode takes bytes or string and returns bytes
209 -----
210 Given the above conditions the output from zlib.decompress and
211 base64.b64decode would be bytes with newer python and str in older
212 version. Thus we would covert the output to str before returning
213 '''
214 LOG.debug("Getting encoded data for key=%s, enc=%s", key, enc_type)
akutz6501f902018-08-24 12:19:05 -0500215
akutz0d1fce52019-06-01 18:54:29 -0500216 raw_data = None
217 if enc_type == "gzip+base64" or enc_type == "gz+b64":
218 LOG.debug("Decoding %s format %s", enc_type, key)
219 raw_data = zlib.decompress(base64.b64decode(data), zlib.MAX_WBITS | 16)
220 elif enc_type == "base64" or enc_type == "b64":
221 LOG.debug("Decoding %s format %s", enc_type, key)
222 raw_data = base64.b64decode(data)
223 else:
224 LOG.debug("Plain-text data %s", key)
225 raw_data = data
Sidharth Surana3a421682018-10-10 15:42:08 -0700226
akutz0d1fce52019-06-01 18:54:29 -0500227 if isinstance(raw_data, bytes):
228 return raw_data.decode('utf-8')
229 return raw_data
230
231
232def get_guestinfo_value(key):
233 '''
234 Returns a guestinfo value for the specified key.
235 '''
236 LOG.debug("Getting guestinfo value for key %s", key)
akutz10cd1402019-10-23 18:06:39 -0500237
238 data_access_method = get_data_access_method()
239
240 if data_access_method == VMX_GUESTINFO:
241 env_key = ("vmx.guestinfo." + key).upper().replace(".", "_", -1)
242 val = os.environ.get(env_key, "")
243 if val == "":
akutz0d1fce52019-06-01 18:54:29 -0500244 LOG.debug("No value found for key %s", key)
245 else:
akutz10cd1402019-10-23 18:06:39 -0500246 return val
247
yvespp8d58dfd2019-10-29 20:42:09 +0100248 if data_access_method == VMWARE_RPCTOOL:
akutz10cd1402019-10-23 18:06:39 -0500249 try:
250 (stdout, stderr) = util.subp(
yvespp8d58dfd2019-10-29 20:42:09 +0100251 [VMWARE_RPCTOOL, "info-get guestinfo." + key])
akutz10cd1402019-10-23 18:06:39 -0500252 if stderr == NOVAL:
253 LOG.debug("No value found for key %s", key)
254 elif not stdout:
255 LOG.error("Failed to get guestinfo value for key %s", key)
256 else:
257 return stdout.rstrip()
258 except util.ProcessExecutionError as error:
259 if error.stderr == NOVAL:
260 LOG.debug("No value found for key %s", key)
261 else:
262 util.logexc(
263 LOG, "Failed to get guestinfo value for key %s: %s", key, error)
264 except Exception:
akutz0d1fce52019-06-01 18:54:29 -0500265 util.logexc(
akutz10cd1402019-10-23 18:06:39 -0500266 LOG, "Unexpected error while trying to get guestinfo value for key %s", key)
267
akutz0d1fce52019-06-01 18:54:29 -0500268 return None
akutz6501f902018-08-24 12:19:05 -0500269
akutz0d1fce52019-06-01 18:54:29 -0500270
271def guestinfo(key):
272 '''
273 guestinfo returns the guestinfo value for the provided key, decoding
274 the value when required
275 '''
276 data = get_guestinfo_value(key)
277 if not data:
akutz6501f902018-08-24 12:19:05 -0500278 return None
akutz0d1fce52019-06-01 18:54:29 -0500279 enc_type = get_guestinfo_value(key + '.encoding')
280 return decode('guestinfo.' + key, enc_type, data)
281
282
283def load(data):
284 '''
285 load first attempts to unmarshal the provided data as JSON, and if
286 that fails then attempts to unmarshal the data as YAML. If data is
287 None then a new dictionary is returned.
288 '''
289 if not data:
290 return {}
291 try:
292 return json.loads(data)
293 except:
294 return safeyaml.load(data)
295
296
297def load_metadata():
298 '''
299 load_metadata loads the metadata from the guestinfo data, optionally
300 decoding the network config when required
301 '''
302 data = load(guestinfo('metadata'))
akutz84389c82019-06-02 19:24:38 -0500303 LOG.debug('loaded metadata %s', data)
akutz0d1fce52019-06-01 18:54:29 -0500304
305 network = None
306 if 'network' in data:
307 network = data['network']
308 del data['network']
309
310 network_enc = None
311 if 'network.encoding' in data:
312 network_enc = data['network.encoding']
313 del data['network.encoding']
314
315 if network:
akutz84389c82019-06-02 19:24:38 -0500316 LOG.debug('network data found')
317 if isinstance(network, collections.Mapping):
318 LOG.debug("network data copied to 'config' key")
319 network = {
320 'config': copy.deepcopy(network)
321 }
322 else:
323 LOG.debug("network data to be decoded %s", network)
akutz0d1fce52019-06-01 18:54:29 -0500324 dec_net = decode('metadata.network', network_enc, network)
akutz84389c82019-06-02 19:24:38 -0500325 network = {
326 'config': load(dec_net),
327 }
328
329 LOG.debug('network data %s', network)
akutz0d1fce52019-06-01 18:54:29 -0500330 data['network'] = network
331
332 return data
333
akutz6501f902018-08-24 12:19:05 -0500334
akutz77457a62018-08-22 16:07:21 -0500335def get_datasource_list(depends):
akutz0d1fce52019-06-01 18:54:29 -0500336 '''
akutz77457a62018-08-22 16:07:21 -0500337 Return a list of data sources that match this set of dependencies
akutz0d1fce52019-06-01 18:54:29 -0500338 '''
Andrew Kutz4f66b8b2018-09-16 18:28:59 -0500339 return [DataSourceVMwareGuestInfo]
akutz0d1fce52019-06-01 18:54:29 -0500340
341
akutzffc4dd52019-06-02 11:34:55 -0500342def get_default_ip_addrs():
343 '''
344 Returns the default IPv4 and IPv6 addresses based on the device(s) used for
345 the default route. Please note that None may be returned for either address
346 family if that family has no default route or if there are multiple
347 addresses associated with the device used by the default route for a given
348 address.
349 '''
350 gateways = netifaces.gateways()
351 if 'default' not in gateways:
352 return None, None
353
354 default_gw = gateways['default']
355 if netifaces.AF_INET not in default_gw and netifaces.AF_INET6 not in default_gw:
356 return None, None
357
358 ipv4 = None
359 ipv6 = None
360
361 gw4 = default_gw.get(netifaces.AF_INET)
362 if gw4:
363 _, dev4 = gw4
akutz0b519f72019-06-02 14:58:57 -0500364 addr4_fams = netifaces.ifaddresses(dev4)
365 if addr4_fams:
366 af_inet4 = addr4_fams.get(netifaces.AF_INET)
367 if af_inet4:
368 if len(af_inet4) > 1:
akutzffc4dd52019-06-02 11:34:55 -0500369 LOG.warn(
akutz0b519f72019-06-02 14:58:57 -0500370 "device %s has more than one ipv4 address: %s", dev4, af_inet4)
371 elif 'addr' in af_inet4[0]:
372 ipv4 = af_inet4[0]['addr']
akutzffc4dd52019-06-02 11:34:55 -0500373
374 # Try to get the default IPv6 address by first seeing if there is a default
akutz0b519f72019-06-02 14:58:57 -0500375 # IPv6 route.
akutzffc4dd52019-06-02 11:34:55 -0500376 gw6 = default_gw.get(netifaces.AF_INET6)
377 if gw6:
378 _, dev6 = gw6
379 addr6_fams = netifaces.ifaddresses(dev6)
380 if addr6_fams:
381 af_inet6 = addr6_fams.get(netifaces.AF_INET6)
382 if af_inet6:
383 if len(af_inet6) > 1:
384 LOG.warn(
385 "device %s has more than one ipv6 address: %s", dev6, af_inet6)
386 elif 'addr' in af_inet6[0]:
387 ipv6 = af_inet6[0]['addr']
akutz0b519f72019-06-02 14:58:57 -0500388
389 # If there is a default IPv4 address but not IPv6, then see if there is a
390 # single IPv6 address associated with the same device associated with the
391 # default IPv4 address.
392 if ipv4 and not ipv6:
393 af_inet6 = addr4_fams.get(netifaces.AF_INET6)
akutzffc4dd52019-06-02 11:34:55 -0500394 if af_inet6:
395 if len(af_inet6) > 1:
396 LOG.warn(
397 "device %s has more than one ipv6 address: %s", dev4, af_inet6)
398 elif 'addr' in af_inet6[0]:
399 ipv6 = af_inet6[0]['addr']
400
akutz0b519f72019-06-02 14:58:57 -0500401 # If there is a default IPv6 address but not IPv4, then see if there is a
402 # single IPv4 address associated with the same device associated with the
403 # default IPv6 address.
404 if not ipv4 and ipv6:
405 af_inet4 = addr6_fams.get(netifaces.AF_INET4)
406 if af_inet4:
407 if len(af_inet4) > 1:
408 LOG.warn(
409 "device %s has more than one ipv4 address: %s", dev6, af_inet4)
410 elif 'addr' in af_inet4[0]:
411 ipv4 = af_inet4[0]['addr']
412
akutzffc4dd52019-06-02 11:34:55 -0500413 return ipv4, ipv6
414
Keerthana Kf0d27ea2019-09-05 21:46:31 +0530415# patched socket.getfqdn() - see https://bugs.python.org/issue5004
akutz10cd1402019-10-23 18:06:39 -0500416
417
Keerthana Kf0d27ea2019-09-05 21:46:31 +0530418def getfqdn(name=''):
419 """Get fully qualified domain name from name.
420 An empty argument is interpreted as meaning the local host.
421 """
422 name = name.strip()
423 if not name or name == '0.0.0.0':
424 name = socket.gethostname()
425 try:
akutz10cd1402019-10-23 18:06:39 -0500426 addrs = socket.getaddrinfo(
427 name, None, 0, socket.SOCK_DGRAM, 0, socket.AI_CANONNAME)
Keerthana Kf0d27ea2019-09-05 21:46:31 +0530428 except socket.error:
429 pass
430 else:
431 for addr in addrs:
432 if addr[3]:
433 name = addr[3]
434 break
435 return name
akutzffc4dd52019-06-02 11:34:55 -0500436
akutz10cd1402019-10-23 18:06:39 -0500437
akutz0d1fce52019-06-01 18:54:29 -0500438def get_host_info():
439 '''
440 Returns host information such as the host name and network interfaces.
441 '''
akutz0d1fce52019-06-01 18:54:29 -0500442
443 host_info = {
444 'network': {
445 'interfaces': {
446 'by-mac': collections.OrderedDict(),
akutzffc4dd52019-06-02 11:34:55 -0500447 'by-ipv4': collections.OrderedDict(),
448 'by-ipv6': collections.OrderedDict(),
akutz0d1fce52019-06-01 18:54:29 -0500449 },
450 },
451 }
452
Keerthana Kf0d27ea2019-09-05 21:46:31 +0530453 hostname = getfqdn(socket.gethostname())
akutz0d1fce52019-06-01 18:54:29 -0500454 if hostname:
akutzffc4dd52019-06-02 11:34:55 -0500455 host_info['hostname'] = hostname
akutz0d1fce52019-06-01 18:54:29 -0500456 host_info['local-hostname'] = hostname
457
akutzffc4dd52019-06-02 11:34:55 -0500458 default_ipv4, default_ipv6 = get_default_ip_addrs()
459 if default_ipv4:
460 host_info['local-ipv4'] = default_ipv4
461 if default_ipv6:
462 host_info['local-ipv6'] = default_ipv6
463
akutz0d1fce52019-06-01 18:54:29 -0500464 by_mac = host_info['network']['interfaces']['by-mac']
akutzffc4dd52019-06-02 11:34:55 -0500465 by_ipv4 = host_info['network']['interfaces']['by-ipv4']
466 by_ipv6 = host_info['network']['interfaces']['by-ipv6']
akutz0d1fce52019-06-01 18:54:29 -0500467
468 ifaces = netifaces.interfaces()
469 for dev_name in ifaces:
470 addr_fams = netifaces.ifaddresses(dev_name)
471 af_link = addr_fams.get(netifaces.AF_LINK)
akutz0b519f72019-06-02 14:58:57 -0500472 af_inet4 = addr_fams.get(netifaces.AF_INET)
akutz0d1fce52019-06-01 18:54:29 -0500473 af_inet6 = addr_fams.get(netifaces.AF_INET6)
474
475 mac = None
476 if af_link and 'addr' in af_link[0]:
477 mac = af_link[0]['addr']
478
479 # Do not bother recording localhost
480 if mac == "00:00:00:00:00:00":
481 continue
482
akutz0b519f72019-06-02 14:58:57 -0500483 if mac and (af_inet4 or af_inet6):
akutz0d1fce52019-06-01 18:54:29 -0500484 key = mac
485 val = {}
akutz0b519f72019-06-02 14:58:57 -0500486 if af_inet4:
487 val["ipv4"] = af_inet4
akutz0d1fce52019-06-01 18:54:29 -0500488 if af_inet6:
akutzffc4dd52019-06-02 11:34:55 -0500489 val["ipv6"] = af_inet6
akutz0d1fce52019-06-01 18:54:29 -0500490 by_mac[key] = val
491
akutz0b519f72019-06-02 14:58:57 -0500492 if af_inet4:
493 for ip_info in af_inet4:
akutz0d1fce52019-06-01 18:54:29 -0500494 key = ip_info['addr']
akutzffc4dd52019-06-02 11:34:55 -0500495 if key == '127.0.0.1':
496 continue
akutz84389c82019-06-02 19:24:38 -0500497 val = copy.deepcopy(ip_info)
akutz0d1fce52019-06-01 18:54:29 -0500498 del val['addr']
499 if mac:
500 val['mac'] = mac
akutzffc4dd52019-06-02 11:34:55 -0500501 by_ipv4[key] = val
akutz0d1fce52019-06-01 18:54:29 -0500502
503 if af_inet6:
504 for ip_info in af_inet6:
505 key = ip_info['addr']
akutzffc4dd52019-06-02 11:34:55 -0500506 if key == '::1':
507 continue
akutz84389c82019-06-02 19:24:38 -0500508 val = copy.deepcopy(ip_info)
akutz0d1fce52019-06-01 18:54:29 -0500509 del val['addr']
510 if mac:
511 val['mac'] = mac
akutzffc4dd52019-06-02 11:34:55 -0500512 by_ipv6[key] = val
akutz0d1fce52019-06-01 18:54:29 -0500513
514 return host_info
515
516
akutz10cd1402019-10-23 18:06:39 -0500517def get_data_access_method():
518 if os.environ.get(VMX_GUESTINFO, ""):
519 return VMX_GUESTINFO
yvespp8d58dfd2019-10-29 20:42:09 +0100520 if VMWARE_RPCTOOL:
521 return VMWARE_RPCTOOL
akutz10cd1402019-10-23 18:06:39 -0500522 return None
523
524
akutzffc4dd52019-06-02 11:34:55 -0500525def main():
526 '''
527 Executed when this file is used as a program.
528 '''
529 metadata = {'network': {'config': {'dhcp': True}}}
530 host_info = get_host_info()
531 metadata = always_merger.merge(metadata, host_info)
532 print(util.json_dumps(metadata))
533
534
akutz0d1fce52019-06-01 18:54:29 -0500535if __name__ == "__main__":
akutzffc4dd52019-06-02 11:34:55 -0500536 main()
akutz0d1fce52019-06-01 18:54:29 -0500537
538# vi: ts=4 expandtab