| # vi: ts=4 expandtab |
| # |
| # Cloud-Init Datasource for VMware Guestinfo |
| # |
| # Copyright (c) 2018 VMware, Inc. All Rights Reserved. |
| # |
| # This product is licensed to you under the Apache 2.0 license (the "License"). |
| # You may not use this product except in compliance with the Apache 2.0 License. |
| # |
| # This product may include a number of subcomponents with separate copyright |
| # notices and license terms. Your use of these subcomponents is subject to the |
| # terms and conditions of the subcomponent's license, as noted in the LICENSE |
| # file. |
| # |
| # 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__) |
| |
| # This cloud-init datasource was designed for use with CentOS 7, |
| # which uses cloud-init 0.7.9. However, this datasource should |
| # work with any Linux distribution for which cloud-init is |
| # avaialble. |
| # |
| # The documentation for cloud-init 0.7.9's datasource is |
| # available at http://bit.ly/cloudinit-datasource-0-7-9. The |
| # current documentation for cloud-init is found at |
| # https://cloudinit.readthedocs.io/en/latest/. |
| # |
| # 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 DataSourceVMwareGuestInfo(sources.DataSource): |
| def __init__(self, sys_cfg, distro, paths, ud_proc=None): |
| sources.DataSource.__init__(self, sys_cfg, distro, paths, ud_proc) |
| self.vmtoolsd = find_executable("vmtoolsd") |
| if not self.vmtoolsd: |
| LOG.error("Failed to find vmtoolsd") |
| |
| def get_data(self): |
| if not self.vmtoolsd: |
| LOG.error("vmtoolsd is required to fetch guestinfo value") |
| return False |
| |
| # Get the JSON metadata. Can be plain-text, base64, or gzip+base64. |
| metadata = self._get_encoded_guestinfo_data('metadata') |
| if metadata: |
| try: |
| self.metadata = json.loads(metadata) |
| except: |
| self.metadata = safeyaml.load(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 |
| |
| @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): |
| ''' |
| The _get_encoded_data would always return a str |
| ---- |
| In py 2.7: |
| json.loads method takes string as input |
| zlib.decompress takes and returns a string |
| base64.b64decode takes and returns a string |
| ----- |
| In py 3.6 and newer: |
| json.loads method takes bytes or string as input |
| zlib.decompress takes and returns a bytes |
| base64.b64decode takes bytes or string and returns bytes |
| ----- |
| In py > 3, < 3.6: |
| json.loads method takes string as input |
| zlib.decompress takes and returns a bytes |
| base64.b64decode takes bytes or string and returns bytes |
| ----- |
| Given the above conditions the output from zlib.decompress and |
| base64.b64decode would be bytes with newer python and str in older |
| version. Thus we would covert the output to str before returning |
| ''' |
| rawdata = self._get_encoded_data_raw(key, enc_type, data) |
| if type(rawdata) == bytes: |
| return rawdata.decode('utf-8') |
| return rawdata |
| |
| def _get_encoded_data_raw(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 [DataSourceVMwareGuestInfo] |