Refactored data source to cloud-init interface
This patch heavily refactors the data source so it follows the
cloud-init datasource interface standard. The hostname, network
configuration, and instance ID are all set via the "guestinfo.metadata"
property. Please see README.md for more information.
diff --git a/DataSourceVmxGuestinfo.py b/DataSourceVmxGuestinfo.py
index c5d7998..60b68dd 100644
--- a/DataSourceVmxGuestinfo.py
+++ b/DataSourceVmxGuestinfo.py
@@ -2,24 +2,54 @@
#
# Copyright (C) 2017 VMware Inc.
#
-# Author: Anish Swaminathan <anishs@vmware.com>
+# Authors: Anish Swaminathan <anishs@vmware.com>
+# Andrew Kutz <akutz@vmware.com>
#
import os
import base64
+import zlib
+import json
from cloudinit import log as logging
from cloudinit import sources
from cloudinit import util
+from cloudinit import safeyaml
from distutils.spawn import find_executable
LOG = logging.getLogger(__name__)
+# Used with CentOS 7 which installs cloud-init 0.7.9. The URL for
+# the DataSource class is http://bit.ly/cloudinit-datasource-0-7-9.
+#
+# Setting the hostname:
+# The hostname is set by way of the metadata key "local-hostname".
+#
+# Setting the instance ID:
+# The instance ID may be set by way of the metadata key "instance-id".
+# However, if this value is absent then then the instance ID is
+# read from the file /sys/class/dmi/id/product_uuid.
+#
+# Configuring the network:
+# The network is configured by setting the metadata key "network"
+# with a value consistent with Network Config Versions 1 or 2,
+# depending on the Linux distro's version of cloud-init:
+#
+# Network Config Version 1 - http://bit.ly/cloudinit-net-conf-v1
+# Network Config Version 2 - http://bit.ly/cloudinit-net-conf-v2
+#
+# For example, CentOS 7's official cloud-init package is version
+# 0.7.9 and does not support Network Config Version 2. However,
+# this datasource still supports supplying Network Config Version 2
+# data as long as the Linux distro's cloud-init package is new
+# enough to parse the data.
+#
+# The metadata key "network.encoding" may be used to indicate the
+# format of the metadata key "network". Valid encodings are base64
+# and gzip+base64.
class DataSourceVmxGuestinfo(sources.DataSource):
def __init__(self, sys_cfg, distro, paths, ud_proc=None):
sources.DataSource.__init__(self, sys_cfg, distro, paths, ud_proc)
- self.metadata = {}
- self.userdata_raw = ''
self.vmtoolsd = find_executable("vmtoolsd")
if not self.vmtoolsd:
LOG.error("Failed to find vmtoolsd")
@@ -28,118 +58,89 @@
if not self.vmtoolsd:
LOG.error("vmtoolsd is required to fetch guestinfo value")
return False
- hostname = self._get_guestinfo_value('hostname')
- if hostname:
- self.distro.set_hostname(hostname)
- ud = self._get_guestinfo_value('userdata')
- if ud:
- LOG.debug("Decoding base64 format guestinfo.userdata")
- self.userdata_raw = base64.b64decode(ud)
- found = True
- dev_index = 0
- network_settings = ''
- while found:
- key_begin = 'interface.' + str(dev_index)
- key_iname = key_begin + '.name'
- interface_name = self._get_guestinfo_value(key_iname)
- if interface_name:
- network_settings += 'auto ' + interface_name + '\n'
- network_settings += 'iface ' + interface_name
- key_proto = key_begin + '.dhcp'
- dhcp_enabled = self._get_guestinfo_value(key_proto)
- key_address = key_begin + '.address'
- address = self._get_guestinfo_value(key_address)
- bootproto = 'dhcp'
- if dhcp_enabled:
- if dhcp_enabled == 'yes':
- network_settings += ' dhcp\n'
- elif dhcp_enabled == 'no':
- network_settings += ' static\n'
- bootproto = 'static'
- else:
- LOG.warning("Invalid value for yes/no parameter for %s, setting to dhcp", key_proto)
- elif address:
- bootproto = 'static'
- dhcp_enabled == 'no'
- network_settings += ' static\n'
- else:
- dhcp_enabled == 'yes'
- network_settings += ' dhcp\n'
- LOG.debug("Setting network bootproto to dhcp by default")
- key_mac = key_begin + '.mac'
- mac = self._get_guestinfo_value(key_mac)
- if address:
- network_settings += 'address ' + address + '\n'
- if mac:
- network_settings += 'hwaddress ' + mac + '\n'
- key_netmask = key_begin + '.netmask'
- netmask = self._get_guestinfo_value(key_netmask)
- if netmask:
- network_settings += 'netmask ' + netmask + '\n'
- key_dnsserver = 'dns.servers'
- dnsserver = self._get_guestinfo_value(key_dnsserver)
- if dnsserver:
- network_settings += 'dns-nameservers '
- dnsserver = dnsserver.split(',')
- for d in dnsserver:
- network_settings += d + ' '
- network_settings += '\n'
- key_dnsdomain = 'dns.domains'
- dnsdomain = self._get_guestinfo_value(key_dnsdomain)
- if dnsdomain:
- network_settings += 'dns-search '
- dnsdomain = dnsdomain.split(',')
- for d in dnsdomain:
- network_settings += d + ' '
- network_settings += '\n'
- route_index = 0
- default_destination_set = False
- while True:
- key_route = key_begin + '.route.' + str(route_index)
- route = self._get_guestinfo_value(key_route)
- if route:
- network_settings += "routes.%s " % (route_index)
- route = route.split(',')
- if len(route) > 2:
- LOG.debug("Route information for %s route in %s device incorrect - ",
- "expected 2 values", route_index, dev_index)
- continue
- elif len(route) == 2:
- network_settings += route[0] + ' ' + route[1] + '\n'# Gateway Destination
- else: #length = 1
- if not default_destination_set:
- network_settings += route[0] + ' 0.0.0.0/0' + '\n'
- default_destination_set = True
- else:
- LOG.debug("Default destination set previously, not setting route %s", route_index)
- else:
- break
- route_index += 1
- else:
- found = False
- dev_index += 1
- self.distro.apply_network(network_settings, False)
+
+ # Get the JSON metadata. Can be plain-text, base64, or gzip+base64.
+ metadata = self._get_encoded_guestinfo_data('metadata')
+ if metadata:
+ self.metadata = json.loads(metadata)
+
+ # Get the YAML userdata. Can be plain-text, base64, or gzip+base64.
+ self.userdata_raw = self._get_encoded_guestinfo_data('userdata')
+
+ # Get the YAML vendordata. Can be plain-text, base64, or gzip+base64.
+ self.vendordata_raw = self._get_encoded_guestinfo_data('vendordata')
+
return True
- def _get_guestinfo_value(self, key):
- LOG.debug("Getting guestinfo value for key %s", key)
- value = ''
- try:
- (value, _err) = util.subp([self.vmtoolsd, "--cmd", "info-get guestinfo." + key])
- if _err:
- LOG.error("Failed to get guestinfo value for key %s", key)
- except util.ProcessExecutionError as error:
- util.logexc(LOG,"Failed to get guestinfo value for key %s: %s", key, error)
- except Exception:
- util.logexc(LOG,"Unexpected error while trying to get guestinfo value for key %s", key)
- return value.rstrip()
+ @property
+ def network_config(self):
+ # Pull the network configuration out of the metadata.
+ if self.metadata and 'network' in self.metadata:
+ data = self._get_encoded_metadata('network')
+ if data:
+ # Load the YAML-formatted network data into an object
+ # and return it.
+ net_config = safeyaml.load(data)
+ LOG.debug("Loaded network config: %s", net_config)
+ return net_config
+ return None
def get_instance_id(self):
+ # Pull the instance ID out of the metadata if present. Otherwise
+ # read the file /sys/class/dmi/id/product_uuid for the instance ID.
+ if self.metadata and 'instance-id' in self.metadata:
+ return self.metadata['instance-id']
with open('/sys/class/dmi/id/product_uuid', 'r') as id_file:
return str(id_file.read()).rstrip()
+ def _get_encoded_guestinfo_data(self, key):
+ data = self._get_guestinfo_value(key)
+ if not data:
+ return None
+ enc_type = self._get_guestinfo_value(key + '.encoding')
+ return self._get_encoded_data('guestinfo.' + key, enc_type, data)
+
+ def _get_encoded_metadata(self, key):
+ if not self.metadata or not key in self.metadata:
+ return None
+ data = self.metadata[key]
+ enc_type = self.metadata.get(key + '.encoding')
+ return self._get_encoded_data('metadata.' + key, enc_type, data)
+
+ def _get_encoded_data(self, key, enc_type, data):
+ LOG.debug("Getting encoded data for key=%s, enc=%s", key, enc_type)
+ if enc_type == "gzip+base64" or enc_type == "gz+b64":
+ LOG.debug("Decoding %s format %s", enc_type, key)
+ return zlib.decompress(base64.b64decode(data), zlib.MAX_WBITS | 16)
+ elif enc_type == "base64" or enc_type == "b64":
+ LOG.debug("Decoding %s format %s", enc_type, key)
+ return base64.b64decode(data)
+ else:
+ LOG.debug("Plain-text data %s", key)
+ return data
+
+ def _get_guestinfo_value(self, key):
+ NOVAL = "No value found"
+ LOG.debug("Getting guestinfo value for key %s", key)
+ try:
+ (stdout, stderr) = util.subp([self.vmtoolsd, "--cmd", "info-get guestinfo." + key])
+ if stderr == NOVAL:
+ LOG.debug("No value found for key %s", key)
+ elif not stdout:
+ LOG.error("Failed to get guestinfo value for key %s", key)
+ else:
+ return stdout.rstrip()
+ except util.ProcessExecutionError as error:
+ if error.stderr == NOVAL:
+ LOG.debug("No value found for key %s", key)
+ else:
+ util.logexc(LOG,"Failed to get guestinfo value for key %s: %s", key, error)
+ except Exception:
+ util.logexc(LOG,"Unexpected error while trying to get guestinfo value for key %s", key)
+ return None
+
def get_datasource_list(depends):
"""
Return a list of data sources that match this set of dependencies
"""
- return [DataSourceVmxGuestinfo]
\ No newline at end of file
+ return [DataSourceVmxGuestinfo]