Browse code

cloud-init: Support for additional features in VMGuestInfo datasource

Changes include:
1. Adding new code changes / features for guestinfo datasource support.
2. Datasource name is changed from vmxguestinfo to VMwareGuestInfo in the
datasource_list of cloud config file in the latest release.

Change-Id: I4107797fe50ae2e179d0f2d9ac37520817034f62
Reviewed-on: http://photon-jenkins.eng.vmware.com:8082/7679
Tested-by: gerrit-photon <photon-checkins@vmware.com>
Reviewed-by: Anish Swaminathan <anishs@vmware.com>

Keerthana K authored on 2019/07/23 19:50:47
Showing 6 changed files
1 1
new file mode 100644
... ...
@@ -0,0 +1,470 @@
0
+diff --git a/cloudinit/sources/DataSourceVMwareGuestInfo.py b/cloudinit/sources/DataSourceVMwareGuestInfo.py
1
+index e69de29..59dd685 100644
2
+--- a/cloudinit/sources/DataSourceVMwareGuestInfo.py
3
+@@ -0,0 +1,465 @@
4
++# Cloud-Init Datasource for VMware Guestinfo
5
++#
6
++# Copyright (c) 2018 VMware, Inc. All Rights Reserved.
7
++#
8
++# This product is licensed to you under the Apache 2.0 license (the "License").
9
++# You may not use this product except in compliance with the Apache 2.0 License.
10
++#
11
++# This product may include a number of subcomponents with separate copyright
12
++# notices and license terms. Your use of these subcomponents is subject to the
13
++# terms and conditions of the subcomponent's license, as noted in the LICENSE
14
++# file.
15
++#
16
++# Authors: Anish Swaminathan <anishs@vmware.com>
17
++#          Andrew Kutz <akutz@vmware.com>
18
++#
19
++
20
++'''
21
++A cloud init datasource for VMware GuestInfo.
22
++'''
23
++
24
++import base64
25
++import collections
26
++import copy
27
++from distutils.spawn import find_executable
28
++import json
29
++import socket
30
++import zlib
31
++
32
++from cloudinit import log as logging
33
++from cloudinit import sources
34
++from cloudinit import util
35
++from cloudinit import safeyaml
36
++
37
++from deepmerge import always_merger
38
++import netifaces
39
++
40
++LOG = logging.getLogger(__name__)
41
++NOVAL = "No value found"
42
++VMTOOLSD = find_executable("vmtoolsd")
43
++
44
++
45
++class NetworkConfigError(Exception):
46
++    '''
47
++    NetworkConfigError is raised when there is an issue getting or
48
++    applying network configuration.
49
++    '''
50
++    pass
51
++
52
++
53
++class DataSourceVMwareGuestInfo(sources.DataSource):
54
++    '''
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
++    The documentation for cloud-init 0.7.9's datasource is
60
++    available at http://bit.ly/cloudinit-datasource-0-7-9. The
61
++    current documentation for cloud-init is found at
62
++    https://cloudinit.readthedocs.io/en/latest/.
63
++    Setting the hostname:
64
++        The hostname is set by way of the metadata key "local-hostname".
65
++    Setting the instance ID:
66
++        The instance ID may be set by way of the metadata key "instance-id".
67
++        However, if this value is absent then then the instance ID is
68
++        read from the file /sys/class/dmi/id/product_uuid.
69
++    Configuring the network:
70
++        The network is configured by setting the metadata key "network"
71
++        with a value consistent with Network Config Versions 1 or 2,
72
++        depending on the Linux distro's version of cloud-init:
73
++            Network Config Version 1 - http://bit.ly/cloudinit-net-conf-v1
74
++            Network Config Version 2 - http://bit.ly/cloudinit-net-conf-v2
75
++        For example, CentOS 7's official cloud-init package is version
76
++        0.7.9 and does not support Network Config Version 2. However,
77
++        this datasource still supports supplying Network Config Version 2
78
++        data as long as the Linux distro's cloud-init package is new
79
++        enough to parse the data.
80
++        The metadata key "network.encoding" may be used to indicate the
81
++        format of the metadata key "network". Valid encodings are base64
82
++        and gzip+base64.
83
++    '''
84
++
85
++    dsname = 'VMwareGuestInfo'
86
++
87
++    def __init__(self, sys_cfg, distro, paths, ud_proc=None):
88
++        sources.DataSource.__init__(self, sys_cfg, distro, paths, ud_proc)
89
++        if not VMTOOLSD:
90
++            LOG.error("Failed to find vmtoolsd")
91
++
92
++    def get_data(self):
93
++        """
94
++        This method should really be _get_data in accordance with the most
95
++        recent versions of cloud-init. However, because the datasource
96
++        supports as far back as cloud-init 0.7.9, get_data is still used.
97
++        Because of this the method attempts to do some of the same things
98
++        that the get_data functions in newer versions of cloud-init do,
99
++        such as calling persist_instance_data.
100
++        """
101
++        if not VMTOOLSD:
102
++            LOG.error("vmtoolsd is required to fetch guestinfo value")
103
++            return False
104
++
105
++        # Get the metadata.
106
++        self.metadata = load_metadata()
107
++
108
++        # Get the user data.
109
++        self.userdata_raw = guestinfo('userdata')
110
++
111
++        # Get the vendor data.
112
++        self.vendordata_raw = guestinfo('vendordata')
113
++
114
++        return True
115
++
116
++    def setup(self, is_new_instance):
117
++        """setup(is_new_instance)
118
++        This is called before user-data and vendor-data have been processed.
119
++        Unless the datasource has set mode to 'local', then networking
120
++        per 'fallback' or per 'network_config' will have been written and
121
++        brought up the OS at this point.
122
++        """
123
++
124
++        # Get information about the host.
125
++        host_info = get_host_info()
126
++        LOG.info("got host-info: %s", host_info)
127
++
128
++        # Ensure the metadata gets updated with information about the
129
++        # host, including the network interfaces, default IP addresses,
130
++        # etc.
131
++        self.metadata = always_merger.merge(self.metadata, host_info)
132
++
133
++        # Persist the instance data for versions of cloud-init that support
134
++        # doing so. This occurs here rather than in the get_data call in
135
++        # order to ensure that the network interfaces are up and can be
136
++        # persisted with the metadata.
137
++        try:
138
++            self.persist_instance_data()
139
++        except AttributeError:
140
++            pass
141
++
142
++    @property
143
++    def network_config(self):
144
++        if 'network' in self.metadata:
145
++            LOG.debug("using metadata network config")
146
++        else:
147
++            LOG.debug("using fallback network config")
148
++            self.metadata['network'] = {
149
++                'config': self.distro.generate_fallback_config(),
150
++            }
151
++        return self.metadata['network']['config']
152
++
153
++    def get_instance_id(self):
154
++        # Pull the instance ID out of the metadata if present. Otherwise
155
++        # read the file /sys/class/dmi/id/product_uuid for the instance ID.
156
++        if self.metadata and 'instance-id' in self.metadata:
157
++            return self.metadata['instance-id']
158
++        with open('/sys/class/dmi/id/product_uuid', 'r') as id_file:
159
++            self.metadata['instance-id'] = str(id_file.read()).rstrip()
160
++            return self.metadata['instance-id']
161
++
162
++
163
++def decode(key, enc_type, data):
164
++    '''
165
++    decode returns the decoded string value of data
166
++    key is a string used to identify the data being decoded in log messages
167
++    ----
168
++    In py 2.7:
169
++    json.loads method takes string as input
170
++    zlib.decompress takes and returns a string
171
++    base64.b64decode takes and returns a string
172
++    -----
173
++    In py 3.6 and newer:
174
++    json.loads method takes bytes or string as input
175
++    zlib.decompress takes and returns a bytes
176
++    base64.b64decode takes bytes or string and returns bytes
177
++    -----
178
++    In py > 3, < 3.6:
179
++    json.loads method takes string as input
180
++    zlib.decompress takes and returns a bytes
181
++    base64.b64decode takes bytes or string and returns bytes
182
++    -----
183
++    Given the above conditions the output from zlib.decompress and
184
++    base64.b64decode would be bytes with newer python and str in older
185
++    version. Thus we would covert the output to str before returning
186
++    '''
187
++    LOG.debug("Getting encoded data for key=%s, enc=%s", key, enc_type)
188
++
189
++    raw_data = None
190
++    if enc_type == "gzip+base64" or enc_type == "gz+b64":
191
++        LOG.debug("Decoding %s format %s", enc_type, key)
192
++        raw_data = zlib.decompress(base64.b64decode(data), zlib.MAX_WBITS | 16)
193
++    elif enc_type == "base64" or enc_type == "b64":
194
++        LOG.debug("Decoding %s format %s", enc_type, key)
195
++        raw_data = base64.b64decode(data)
196
++    else:
197
++        LOG.debug("Plain-text data %s", key)
198
++        raw_data = data
199
++
200
++    if isinstance(raw_data, bytes):
201
++        return raw_data.decode('utf-8')
202
++    return raw_data
203
++
204
++
205
++def get_guestinfo_value(key):
206
++    '''
207
++    Returns a guestinfo value for the specified key.
208
++    '''
209
++    LOG.debug("Getting guestinfo value for key %s", key)
210
++    try:
211
++        (stdout, stderr) = util.subp(
212
++            [VMTOOLSD, "--cmd", "info-get guestinfo." + key])
213
++        if stderr == NOVAL:
214
++            LOG.debug("No value found for key %s", key)
215
++        elif not stdout:
216
++            LOG.error("Failed to get guestinfo value for key %s", key)
217
++        else:
218
++            return stdout.rstrip()
219
++    except util.ProcessExecutionError as error:
220
++        if error.stderr == NOVAL:
221
++            LOG.debug("No value found for key %s", key)
222
++        else:
223
++            util.logexc(
224
++                LOG, "Failed to get guestinfo value for key %s: %s", key, error)
225
++    except Exception:
226
++        util.logexc(
227
++            LOG, "Unexpected error while trying to get guestinfo value for key %s", key)
228
++    return None
229
++
230
++
231
++def guestinfo(key):
232
++    '''
233
++    guestinfo returns the guestinfo value for the provided key, decoding
234
++    the value when required
235
++    '''
236
++    data = get_guestinfo_value(key)
237
++    if not data:
238
++        return None
239
++    enc_type = get_guestinfo_value(key + '.encoding')
240
++    return decode('guestinfo.' + key, enc_type, data)
241
++
242
++
243
++def load(data):
244
++    '''
245
++    load first attempts to unmarshal the provided data as JSON, and if
246
++    that fails then attempts to unmarshal the data as YAML. If data is
247
++    None then a new dictionary is returned.
248
++    '''
249
++    if not data:
250
++        return {}
251
++    try:
252
++        return json.loads(data)
253
++    except:
254
++        return safeyaml.load(data)
255
++
256
++
257
++def load_metadata():
258
++    '''
259
++    load_metadata loads the metadata from the guestinfo data, optionally
260
++    decoding the network config when required
261
++    '''
262
++    data = load(guestinfo('metadata'))
263
++    LOG.debug('loaded metadata %s', data)
264
++
265
++    network = None
266
++    if 'network' in data:
267
++        network = data['network']
268
++        del data['network']
269
++
270
++    network_enc = None
271
++    if 'network.encoding' in data:
272
++        network_enc = data['network.encoding']
273
++        del data['network.encoding']
274
++
275
++    if network:
276
++        LOG.debug('network data found')
277
++        if isinstance(network, collections.Mapping):
278
++            LOG.debug("network data copied to 'config' key")
279
++            network = {
280
++                'config': copy.deepcopy(network)
281
++            }
282
++        else:
283
++            LOG.debug("network data to be decoded %s", network)
284
++            dec_net = decode('metadata.network', network_enc, network)
285
++            network = {
286
++                'config': load(dec_net),
287
++            }
288
++
289
++        LOG.debug('network data %s', network)
290
++        data['network'] = network
291
++
292
++    return data
293
++
294
++
295
++def get_datasource_list(depends):
296
++    '''
297
++    Return a list of data sources that match this set of dependencies
298
++    '''
299
++    return [DataSourceVMwareGuestInfo]
300
++
301
++
302
++def get_default_ip_addrs():
303
++    '''
304
++    Returns the default IPv4 and IPv6 addresses based on the device(s) used for
305
++    the default route. Please note that None may be returned for either address
306
++    family if that family has no default route or if there are multiple
307
++    addresses associated with the device used by the default route for a given
308
++    address.
309
++    '''
310
++    gateways = netifaces.gateways()
311
++    if 'default' not in gateways:
312
++        return None, None
313
++
314
++    default_gw = gateways['default']
315
++    if netifaces.AF_INET not in default_gw and netifaces.AF_INET6 not in default_gw:
316
++        return None, None
317
++
318
++    ipv4 = None
319
++    ipv6 = None
320
++
321
++    gw4 = default_gw.get(netifaces.AF_INET)
322
++    if gw4:
323
++        _, dev4 = gw4
324
++        addr4_fams = netifaces.ifaddresses(dev4)
325
++        if addr4_fams:
326
++            af_inet4 = addr4_fams.get(netifaces.AF_INET)
327
++            if af_inet4:
328
++                if len(af_inet4) > 1:
329
++                    LOG.warn(
330
++                        "device %s has more than one ipv4 address: %s", dev4, af_inet4)
331
++                elif 'addr' in af_inet4[0]:
332
++                    ipv4 = af_inet4[0]['addr']
333
++
334
++    # Try to get the default IPv6 address by first seeing if there is a default
335
++    # IPv6 route.
336
++    gw6 = default_gw.get(netifaces.AF_INET6)
337
++    if gw6:
338
++        _, dev6 = gw6
339
++        addr6_fams = netifaces.ifaddresses(dev6)
340
++        if addr6_fams:
341
++            af_inet6 = addr6_fams.get(netifaces.AF_INET6)
342
++            if af_inet6:
343
++                if len(af_inet6) > 1:
344
++                    LOG.warn(
345
++                        "device %s has more than one ipv6 address: %s", dev6, af_inet6)
346
++                elif 'addr' in af_inet6[0]:
347
++                    ipv6 = af_inet6[0]['addr']
348
++
349
++    # If there is a default IPv4 address but not IPv6, then see if there is a
350
++    # single IPv6 address associated with the same device associated with the
351
++    # default IPv4 address.
352
++    if ipv4 and not ipv6:
353
++        af_inet6 = addr4_fams.get(netifaces.AF_INET6)
354
++        if af_inet6:
355
++            if len(af_inet6) > 1:
356
++                LOG.warn(
357
++                    "device %s has more than one ipv6 address: %s", dev4, af_inet6)
358
++            elif 'addr' in af_inet6[0]:
359
++                ipv6 = af_inet6[0]['addr']
360
++
361
++    # If there is a default IPv6 address but not IPv4, then see if there is a
362
++    # single IPv4 address associated with the same device associated with the
363
++    # default IPv6 address.
364
++    if not ipv4 and ipv6:
365
++        af_inet4 = addr6_fams.get(netifaces.AF_INET4)
366
++        if af_inet4:
367
++            if len(af_inet4) > 1:
368
++                LOG.warn(
369
++                    "device %s has more than one ipv4 address: %s", dev6, af_inet4)
370
++            elif 'addr' in af_inet4[0]:
371
++                ipv4 = af_inet4[0]['addr']
372
++
373
++    return ipv4, ipv6
374
++
375
++
376
++def get_host_info():
377
++    '''
378
++    Returns host information such as the host name and network interfaces.
379
++    '''
380
++
381
++    host_info = {
382
++        'network': {
383
++            'interfaces': {
384
++                'by-mac': collections.OrderedDict(),
385
++                'by-ipv4': collections.OrderedDict(),
386
++                'by-ipv6': collections.OrderedDict(),
387
++            },
388
++        },
389
++    }
390
++
391
++    hostname = socket.getfqdn()
392
++    if hostname:
393
++        host_info['hostname'] = hostname
394
++        host_info['local-hostname'] = hostname
395
++
396
++    default_ipv4, default_ipv6 = get_default_ip_addrs()
397
++    if default_ipv4:
398
++        host_info['local-ipv4'] = default_ipv4
399
++    if default_ipv6:
400
++        host_info['local-ipv6'] = default_ipv6
401
++
402
++    by_mac = host_info['network']['interfaces']['by-mac']
403
++    by_ipv4 = host_info['network']['interfaces']['by-ipv4']
404
++    by_ipv6 = host_info['network']['interfaces']['by-ipv6']
405
++
406
++    ifaces = netifaces.interfaces()
407
++    for dev_name in ifaces:
408
++        addr_fams = netifaces.ifaddresses(dev_name)
409
++        af_link = addr_fams.get(netifaces.AF_LINK)
410
++        af_inet4 = addr_fams.get(netifaces.AF_INET)
411
++        af_inet6 = addr_fams.get(netifaces.AF_INET6)
412
++
413
++        mac = None
414
++        if af_link and 'addr' in af_link[0]:
415
++            mac = af_link[0]['addr']
416
++
417
++        # Do not bother recording localhost
418
++        if mac == "00:00:00:00:00:00":
419
++            continue
420
++
421
++        if mac and (af_inet4 or af_inet6):
422
++            key = mac
423
++            val = {}
424
++            if af_inet4:
425
++                val["ipv4"] = af_inet4
426
++            if af_inet6:
427
++                val["ipv6"] = af_inet6
428
++            by_mac[key] = val
429
++
430
++        if af_inet4:
431
++            for ip_info in af_inet4:
432
++                key = ip_info['addr']
433
++                if key == '127.0.0.1':
434
++                    continue
435
++                val = copy.deepcopy(ip_info)
436
++                del val['addr']
437
++                if mac:
438
++                    val['mac'] = mac
439
++                by_ipv4[key] = val
440
++
441
++        if af_inet6:
442
++            for ip_info in af_inet6:
443
++                key = ip_info['addr']
444
++                if key == '::1':
445
++                    continue
446
++                val = copy.deepcopy(ip_info)
447
++                del val['addr']
448
++                if mac:
449
++                    val['mac'] = mac
450
++                by_ipv6[key] = val
451
++
452
++    return host_info
453
++
454
++
455
++def main():
456
++    '''
457
++    Executed when this file is used as a program.
458
++    '''
459
++    metadata = {'network': {'config': {'dhcp': True}}}
460
++    host_info = get_host_info()
461
++    metadata = always_merger.merge(metadata, host_info)
462
++    print(util.json_dumps(metadata))
463
++
464
++
465
++if __name__ == "__main__":
466
++    main()
467
++
468
++# vi: ts=4 expandtab
... ...
@@ -2,7 +2,7 @@
2 2
 
3 3
 Name:           cloud-init
4 4
 Version:        19.1
5
-Release:        1%{?dist}
5
+Release:        2%{?dist}
6 6
 Summary:        Cloud instance init scripts
7 7
 Group:          System Environment/Base
8 8
 License:        GPLv3
... ...
@@ -13,16 +13,18 @@ Source0:        https://launchpad.net/cloud-init/trunk/%{version}/+download/%{na
13 13
 %define sha1 cloud-init=6de398dd755959dde47c8d6f6e255a0857017c44
14 14
 Source1:        cloud-photon.cfg
15 15
 Source2:        99-disable-networking-config.cfg
16
+Source3:        dscheck_VMwareGuestInfo
16 17
 
17 18
 Patch0:         photon-distro.patch
18 19
 Patch2:         vca-admin-pwd.patch
19 20
 Patch3:         photon-hosts-template.patch
20
-Patch5:         datasource-guestinfo.patch
21
+Patch5:         DataSourceVMwareGuestInfo.patch
21 22
 Patch6:         systemd-service-changes.patch
22 23
 Patch7:         makecheck.patch
23 24
 Patch8:         systemd-resolved-config.patch
24 25
 Patch9:         cloud-init-azureds.patch
25 26
 Patch10:        ds-identity.patch
27
+Patch11:        ds-guestinfo-photon.patch
26 28
 
27 29
 BuildRequires:  python3
28 30
 BuildRequires:  python3-libs
... ...
@@ -60,6 +62,8 @@ Requires:       python3-six
60 60
 Requires:       python3-setuptools
61 61
 Requires:       python3-xml
62 62
 Requires:       python3-jsonschema
63
+Requires:       python3-deepmerge
64
+Requires:       python3-netifaces
63 65
 BuildArch:      noarch
64 66
 
65 67
 %description
... ...
@@ -79,6 +83,7 @@ ssh keys and to let the user run various scripts.
79 79
 %patch8 -p1
80 80
 %patch9 -p1
81 81
 %patch10 -p1
82
+%patch11 -p1
82 83
 
83 84
 find systemd -name "cloud*.service*" | xargs sed -i s/StandardOutput=journal+console/StandardOutput=journal/g
84 85
 
... ...
@@ -97,6 +102,7 @@ cp -p %{SOURCE1} %{buildroot}/%{_sysconfdir}/cloud/cloud.cfg
97 97
 
98 98
 # Disable networking config by cloud-init
99 99
 cp -p %{SOURCE2} $RPM_BUILD_ROOT/%{_sysconfdir}/cloud/cloud.cfg.d/
100
+install -m 755 %{SOURCE3} $RPM_BUILD_ROOT/%{_bindir}/
100 101
 
101 102
 %check
102 103
 easy_install_3=$(ls /usr/bin |grep easy_install |grep 3)
... ...
@@ -143,10 +149,13 @@ rm -rf $RPM_BUILD_ROOT
143 143
 %{python3_sitelib}/*
144 144
 %{_bindir}/cloud-init*
145 145
 %{_bindir}/cloud-id
146
+%{_bindir}/dscheck_VMwareGuestInfo
146 147
 %{_datadir}/bash-completion/completions/cloud-init
147 148
 %dir /var/lib/cloud
148 149
 
149 150
 %changelog
151
+*   Tue Jul 23 2019 Keerthana K <keerthanak@vmware.com> 19.1-2
152
+-   support for additional features in VMGuestInfo Datasource.
150 153
 *   Tue Jun 25 2019 Keerthana K <keerthanak@vmware.com> 19.1-1
151 154
 -   Upgrade to version 19.1 and fix cloud-init GOS logic.
152 155
 *   Thu Jun 13 2019 Keerthana K <keerthanak@vmware.com> 18.3-4
... ...
@@ -32,7 +32,7 @@ datasource_list: [
32 32
 #                  CloudSigma, 
33 33
 #                  Ec2, 
34 34
 #                  CloudStack,
35
-#                  VmxGuestinfo, 
35
+                  VMwareGuestInfo,
36 36
                   None 
37 37
                  ]
38 38
 
39 39
deleted file mode 100644
... ...
@@ -1,149 +0,0 @@
1
-diff -rupN cloud-init-0.7.9/cloudinit/sources/DataSourceVmxGuestinfo.py cloud-init-0.7.9-new/cloudinit/sources/DataSourceVmxGuestinfo.py
2
-+++ cloud-init-0.7.9-new/cloudinit/sources/DataSourceVmxGuestinfo.py	2017-05-08 07:47:27.388662680 -0700
3
-@@ -0,0 +1,145 @@
4
-+# vi: ts=4 expandtab
5
-+#
6
-+# Copyright (C) 2017 VMware Inc.
7
-+#
8
-+# Author: Anish Swaminathan <anishs@vmware.com>
9
-+#
10
-+import os
11
-+import base64
12
-+
13
-+from cloudinit import log as logging
14
-+from cloudinit import sources
15
-+from cloudinit import util
16
-+
17
-+from distutils.spawn import find_executable
18
-+
19
-+LOG = logging.getLogger(__name__)
20
-+
21
-+class DataSourceVmxGuestinfo(sources.DataSource):
22
-+    def __init__(self, sys_cfg, distro, paths, ud_proc=None):
23
-+        sources.DataSource.__init__(self, sys_cfg, distro, paths, ud_proc)
24
-+        self.metadata = {}
25
-+        self.userdata_raw = ''
26
-+        self.vmtoolsd = find_executable("vmtoolsd")
27
-+        if not self.vmtoolsd:
28
-+            LOG.error("Failed to find vmtoolsd")
29
-+
30
-+    def get_data(self):
31
-+        if not self.vmtoolsd:
32
-+            LOG.error("vmtoolsd is required to fetch guestinfo value")
33
-+            return False
34
-+        hostname = self._get_guestinfo_value('hostname')
35
-+        if hostname:
36
-+            self.distro.set_hostname(hostname)
37
-+        ud = self._get_guestinfo_value('userdata')
38
-+        if ud:
39
-+            LOG.debug("Decoding base64 format guestinfo.userdata")
40
-+            self.userdata_raw = base64.b64decode(ud)
41
-+        found = True
42
-+        dev_index = 0
43
-+        network_settings = ''
44
-+        while found:
45
-+            key_begin = 'interface.' + str(dev_index)
46
-+            key_iname = key_begin + '.name'
47
-+            interface_name = self._get_guestinfo_value(key_iname)
48
-+            if interface_name:
49
-+                network_settings += 'auto ' + interface_name + '\n'
50
-+                network_settings += 'iface ' + interface_name
51
-+                key_proto = key_begin + '.dhcp'
52
-+                dhcp_enabled = self._get_guestinfo_value(key_proto)
53
-+                key_address = key_begin + '.address'
54
-+                address = self._get_guestinfo_value(key_address)
55
-+                bootproto = 'dhcp'
56
-+                if dhcp_enabled:
57
-+                    if dhcp_enabled == 'yes':
58
-+                        network_settings += ' dhcp\n'
59
-+                    elif dhcp_enabled == 'no':
60
-+                        network_settings += ' static\n'
61
-+                        bootproto = 'static'
62
-+                    else:
63
-+                        LOG.warning("Invalid value for yes/no parameter for %s, setting to dhcp", key_proto)
64
-+                elif address:
65
-+                    bootproto = 'static'
66
-+                    dhcp_enabled == 'no'
67
-+                    network_settings += ' static\n'
68
-+                else:
69
-+                    dhcp_enabled == 'yes'
70
-+                    network_settings += ' dhcp\n'
71
-+                    LOG.debug("Setting network bootproto to dhcp by default")
72
-+                key_mac = key_begin + '.mac'
73
-+                mac = self._get_guestinfo_value(key_mac)
74
-+                if address:
75
-+                    network_settings += 'address ' + address + '\n'
76
-+                if mac:
77
-+                    network_settings += 'hwaddress ' + mac + '\n'
78
-+                key_netmask = key_begin + '.netmask'
79
-+                netmask = self._get_guestinfo_value(key_netmask)
80
-+                if netmask:
81
-+                    network_settings += 'netmask ' + netmask + '\n'
82
-+                key_dnsserver = 'dns.servers'
83
-+                dnsserver = self._get_guestinfo_value(key_dnsserver)
84
-+                if dnsserver:
85
-+                    network_settings += 'dns-nameservers '
86
-+                    dnsserver = dnsserver.split(',')
87
-+                    for d in dnsserver:
88
-+                        network_settings += d + ' '
89
-+                    network_settings += '\n'
90
-+                key_dnsdomain = 'dns.domains'
91
-+                dnsdomain = self._get_guestinfo_value(key_dnsdomain)
92
-+                if dnsdomain:
93
-+                    network_settings += 'dns-search '
94
-+                    dnsdomain = dnsdomain.split(',')
95
-+                    for d in dnsdomain:
96
-+                        network_settings += d + ' '
97
-+                    network_settings += '\n'
98
-+                route_index = 0
99
-+                default_destination_set = False
100
-+                while True:
101
-+                    key_route = key_begin + '.route.' + str(route_index)
102
-+                    route = self._get_guestinfo_value(key_route)
103
-+                    if route:
104
-+                        network_settings += "routes.%s " % (route_index)
105
-+                        route = route.split(',')
106
-+                        if len(route) > 2:
107
-+                            LOG.debug("Route information for %s route in %s device incorrect - ", 
108
-+                                                "expected 2 values", route_index, dev_index)
109
-+                            continue
110
-+                        elif len(route) == 2:
111
-+                            network_settings += route[0] + ' ' + route[1] + '\n'# Gateway Destination
112
-+                        else: #length = 1
113
-+                            if not default_destination_set:
114
-+                                network_settings += route[0] + ' 0.0.0.0/0' + '\n'
115
-+                                default_destination_set = True
116
-+                            else:
117
-+                                LOG.debug("Default destination set previously, not setting route %s", route_index) 
118
-+                    else:
119
-+                        break
120
-+                    route_index += 1
121
-+            else:
122
-+                found = False
123
-+            dev_index += 1
124
-+        self.distro.apply_network(network_settings, False)
125
-+        return True
126
-+
127
-+    def _get_guestinfo_value(self, key):
128
-+        LOG.debug("Getting guestinfo value for key %s", key)
129
-+        value = ''
130
-+        try:
131
-+            (value, _err) = util.subp([self.vmtoolsd, "--cmd", "info-get guestinfo." + key])
132
-+            if _err:
133
-+                LOG.error("Failed to get guestinfo value for key %s", key)
134
-+        except util.ProcessExecutionError as error:
135
-+            util.logexc(LOG,"Failed to get guestinfo value for key %s: %s", key, error)
136
-+        except Exception:
137
-+            util.logexc(LOG,"Unexpected error while trying to get guestinfo value for key %s", key)
138
-+        return value.rstrip()
139
-+
140
-+    def get_instance_id(self):
141
-+        with open('/sys/class/dmi/id/product_uuid', 'r') as id_file:
142
-+            return str(id_file.read()).rstrip()
143
-+
144
-+def get_datasource_list(depends):
145
-+    """
146
-+    Return a list of data sources that match this set of dependencies
147
-+    """
148
-+    return [DataSourceVmxGuestinfo]
149 1
new file mode 100644
... ...
@@ -0,0 +1,99 @@
0
+diff --git a/cloudinit/sources/DataSourceVMwareGuestInfo.py b/cloudinit/sources/DataSourceVMwareGuestInfo.py
1
+index e69de29..59dd685 100644
2
+--- a/cloudinit/sources/DataSourceVMwareGuestInfo.py
3
+@@ -108,6 +108,94 @@
4
+         # Get the vendor data.
5
+         self.vendordata_raw = guestinfo('vendordata')
6
+ 
7
++        hostname = get_guestinfo_value('hostname')
8
++        if hostname:
9
++            self.distro.set_hostname(hostname)
10
++        found = True
11
++        dev_index = 0
12
++        network_settings = ''
13
++        while found:
14
++            key_begin = 'interface.' + str(dev_index)
15
++            key_iname = key_begin + '.name'
16
++            interface_name = get_guestinfo_value(key_iname)
17
++            if interface_name:
18
++                network_settings += 'auto ' + interface_name + '\n'
19
++                network_settings += 'iface ' + interface_name
20
++                key_proto = key_begin + '.dhcp'
21
++                dhcp_enabled = get_guestinfo_value(key_proto)
22
++                key_address = key_begin + '.address'
23
++                address = get_guestinfo_value(key_address)
24
++                bootproto = 'dhcp'
25
++                if dhcp_enabled:
26
++                    if dhcp_enabled == 'yes':
27
++                        network_settings += ' dhcp\n'
28
++                    elif dhcp_enabled == 'no':
29
++                        network_settings += ' static\n'
30
++                        bootproto = 'static'
31
++                    else:
32
++                        LOG.warning("Invalid value for yes/no parameter for %s, setting to dhcp", key_proto)
33
++                elif address:
34
++                    bootproto = 'static'
35
++                    dhcp_enabled == 'no'
36
++                    network_settings += ' static\n'
37
++                else:
38
++                    dhcp_enabled == 'yes'
39
++                    network_settings += ' dhcp\n'
40
++                    LOG.debug("Setting network bootproto to dhcp by default")
41
++                key_mac = key_begin + '.mac'
42
++                mac = get_guestinfo_value(key_mac)
43
++                if address:
44
++                    network_settings += 'address ' + address + '\n'
45
++                if mac:
46
++                    network_settings += 'hwaddress ' + mac + '\n'
47
++                key_netmask = key_begin + '.netmask'
48
++                netmask = get_guestinfo_value(key_netmask)
49
++                if netmask:
50
++                     network_settings += 'netmask ' + netmask + '\n'
51
++                key_dnsserver = 'dns.servers'
52
++                dnsserver = get_guestinfo_value(key_dnsserver)
53
++                if dnsserver:
54
++                    network_settings += 'dns-nameservers '
55
++                    dnsserver = dnsserver.split(',')
56
++                    for d in dnsserver:
57
++                        network_settings += d + ' '
58
++                    network_settings += '\n'
59
++                key_dnsdomain = 'dns.domains'
60
++                dnsdomain = get_guestinfo_value(key_dnsdomain)
61
++                if dnsdomain:
62
++                    network_settings += 'dns-search '
63
++                    dnsdomain = dnsdomain.split(',')
64
++                    for d in dnsdomain:
65
++                        network_settings += d + ' '
66
++                    network_settings += '\n'
67
++                route_index = 0
68
++                default_destination_set = False
69
++                while True:
70
++                    key_route = key_begin + '.route.' + str(route_index)
71
++                    route = get_guestinfo_value(key_route)
72
++                    if route:
73
++                        network_settings += "routes.%s " % (route_index)
74
++                        route = route.split(',')
75
++                        if len(route) > 2:
76
++                            LOG.debug("Route information for %s route in %s device incorrect - ",
77
++                                                "expected 2 values", route_index, dev_index)
78
++                            continue
79
++                        elif len(route) == 2:
80
++                            network_settings += route[0] + ' ' + route[1] + '\n'# Gateway Destination
81
++                        else: #length = 1
82
++                            if not default_destination_set:
83
++                                network_settings += route[0] + ' 0.0.0.0/0' + '\n'
84
++                                default_destination_set = True
85
++                            else:
86
++                                LOG.debug("Default destination set previously, not setting route %s", route_index)
87
++                    else:
88
++                        break
89
++                    route_index += 1
90
++            else:
91
++                found = False
92
++            dev_index += 1
93
++        self.distro.apply_network(network_settings, False)
94
++
95
+         return True
96
+ 
97
+     def setup(self, is_new_instance):
0 98
new file mode 100644
... ...
@@ -0,0 +1,46 @@
0
+#!/bin/sh
1
+
2
+# Cloud-Init Datasource for VMware Guestinfo
3
+#
4
+# Copyright (c) 2019 VMware, Inc. All Rights Reserved.
5
+#
6
+# This product is licensed to you under the Apache 2.0 license (the "License").
7
+# You may not use this product except in compliance with the Apache 2.0 License.
8
+#
9
+# This product may include a number of subcomponents with separate copyright
10
+# notices and license terms. Your use of these subcomponents is subject to the
11
+# terms and conditions of the subcomponent's license, as noted in the LICENSE
12
+# file.
13
+
14
+#
15
+# This file should be installed to /usr/bin/dscheck_VMwareGuestInfo
16
+# without the ".sh" extension. The extension only exists to make it easier
17
+# to identify the file during development.
18
+#
19
+# This file provides cloud-init's ds-identify program a shell type that
20
+# can be resolved with "type dscheck_VMwareGuestInfo" and used to validate
21
+# where a datasource is installed and useable.
22
+#
23
+# Cloud-init's ds-identify program in /usr/lib/cloud-init includes functions
24
+# to determine whether or not datasources can be used. Because the program
25
+# is a shell script and uses "type dscheck_DATASOURCE_NAME" to determine
26
+# if there is a matching bash type that can answer for the datasource,
27
+# it's possible to respond with an external script. While other datasources
28
+# have functions in ds-identify, the "type" command looks up types both
29
+# in Bash's function table as well as script in the PATH. Therefore the
30
+# ds-identify program, when looking up whether or not the datasource
31
+# VMwareGuestInfo can be used, will defer to this file when it is in the
32
+# PATH and named dscheck_VMwareGuestInfo.
33
+#
34
+
35
+if ! command -v vmtoolsd >/dev/null 2>&1; then
36
+  exit 1
37
+fi
38
+
39
+if { vmtoolsd --cmd "info-get guestinfo.metadata" || \
40
+     vmtoolsd --cmd "info-get guestinfo.userdata" || \
41
+     vmtoolsd --cmd "info-get guestinfo.vendordata"; } >/dev/null 2>&1; then
42
+   exit 0
43
+fi
44
+
45
+exit 1