blob: b4d2fc0fa8a23bb2723947ee6f1bda6124c2978c [file] [log] [blame]
akutz77457a62018-08-22 16:07:21 -05001# vi: ts=4 expandtab
2#
akutzdd794a42018-09-18 10:04:21 -05003# Cloud-Init Datasource for VMware Guestinfo
4#
5# Copyright (c) 2018 VMware, Inc. All Rights Reserved.
6#
7# This product is licensed to you under the Apache 2.0 license (the "License").
8# You may not use this product except in compliance with the Apache 2.0 License.
9#
10# This product may include a number of subcomponents with separate copyright
11# notices and license terms. Your use of these subcomponents is subject to the
12# terms and conditions of the subcomponent's license, as noted in the LICENSE
13# file.
akutz77457a62018-08-22 16:07:21 -050014#
akutz6501f902018-08-24 12:19:05 -050015# Authors: Anish Swaminathan <anishs@vmware.com>
16# Andrew Kutz <akutz@vmware.com>
akutz77457a62018-08-22 16:07:21 -050017#
18import os
19import base64
akutz6501f902018-08-24 12:19:05 -050020import zlib
21import json
akutz77457a62018-08-22 16:07:21 -050022
23from cloudinit import log as logging
24from cloudinit import sources
25from cloudinit import util
akutz6501f902018-08-24 12:19:05 -050026from cloudinit import safeyaml
akutz77457a62018-08-22 16:07:21 -050027
28from distutils.spawn import find_executable
29
30LOG = logging.getLogger(__name__)
31
Andrew Kutz4f66b8b2018-09-16 18:28:59 -050032# This cloud-init datasource was designed for use with CentOS 7,
33# which uses cloud-init 0.7.9. However, this datasource should
akutz59d76d12019-05-29 13:38:28 -050034# work with any Linux distribution for which cloud-init is
Andrew Kutz4f66b8b2018-09-16 18:28:59 -050035# avaialble.
36#
akutz59d76d12019-05-29 13:38:28 -050037# The documentation for cloud-init 0.7.9's datasource is
Andrew Kutz4f66b8b2018-09-16 18:28:59 -050038# available at http://bit.ly/cloudinit-datasource-0-7-9. The
39# current documentation for cloud-init is found at
40# https://cloudinit.readthedocs.io/en/latest/.
akutz6501f902018-08-24 12:19:05 -050041#
42# Setting the hostname:
43# The hostname is set by way of the metadata key "local-hostname".
44#
45# Setting the instance ID:
46# The instance ID may be set by way of the metadata key "instance-id".
47# However, if this value is absent then then the instance ID is
48# read from the file /sys/class/dmi/id/product_uuid.
49#
50# Configuring the network:
51# The network is configured by setting the metadata key "network"
52# with a value consistent with Network Config Versions 1 or 2,
53# depending on the Linux distro's version of cloud-init:
54#
55# Network Config Version 1 - http://bit.ly/cloudinit-net-conf-v1
56# Network Config Version 2 - http://bit.ly/cloudinit-net-conf-v2
57#
58# For example, CentOS 7's official cloud-init package is version
59# 0.7.9 and does not support Network Config Version 2. However,
60# this datasource still supports supplying Network Config Version 2
61# data as long as the Linux distro's cloud-init package is new
62# enough to parse the data.
63#
64# The metadata key "network.encoding" may be used to indicate the
65# format of the metadata key "network". Valid encodings are base64
66# and gzip+base64.
Andrew Kutz4f66b8b2018-09-16 18:28:59 -050067class DataSourceVMwareGuestInfo(sources.DataSource):
akutz77457a62018-08-22 16:07:21 -050068 def __init__(self, sys_cfg, distro, paths, ud_proc=None):
69 sources.DataSource.__init__(self, sys_cfg, distro, paths, ud_proc)
akutz77457a62018-08-22 16:07:21 -050070 self.vmtoolsd = find_executable("vmtoolsd")
71 if not self.vmtoolsd:
72 LOG.error("Failed to find vmtoolsd")
73
74 def get_data(self):
75 if not self.vmtoolsd:
76 LOG.error("vmtoolsd is required to fetch guestinfo value")
77 return False
akutz6501f902018-08-24 12:19:05 -050078
79 # Get the JSON metadata. Can be plain-text, base64, or gzip+base64.
80 metadata = self._get_encoded_guestinfo_data('metadata')
81 if metadata:
akutz59d76d12019-05-29 13:38:28 -050082 try:
83 self.metadata = json.loads(metadata)
84 except:
85 self.metadata = safeyaml.load(metadata)
akutz6501f902018-08-24 12:19:05 -050086
87 # Get the YAML userdata. Can be plain-text, base64, or gzip+base64.
88 self.userdata_raw = self._get_encoded_guestinfo_data('userdata')
89
90 # Get the YAML vendordata. Can be plain-text, base64, or gzip+base64.
91 self.vendordata_raw = self._get_encoded_guestinfo_data('vendordata')
92
akutz77457a62018-08-22 16:07:21 -050093 return True
94
akutz6501f902018-08-24 12:19:05 -050095 @property
96 def network_config(self):
97 # Pull the network configuration out of the metadata.
98 if self.metadata and 'network' in self.metadata:
99 data = self._get_encoded_metadata('network')
100 if data:
101 # Load the YAML-formatted network data into an object
102 # and return it.
103 net_config = safeyaml.load(data)
104 LOG.debug("Loaded network config: %s", net_config)
105 return net_config
106 return None
akutz77457a62018-08-22 16:07:21 -0500107
108 def get_instance_id(self):
akutz6501f902018-08-24 12:19:05 -0500109 # Pull the instance ID out of the metadata if present. Otherwise
110 # read the file /sys/class/dmi/id/product_uuid for the instance ID.
111 if self.metadata and 'instance-id' in self.metadata:
112 return self.metadata['instance-id']
akutz77457a62018-08-22 16:07:21 -0500113 with open('/sys/class/dmi/id/product_uuid', 'r') as id_file:
114 return str(id_file.read()).rstrip()
115
akutz6501f902018-08-24 12:19:05 -0500116 def _get_encoded_guestinfo_data(self, key):
117 data = self._get_guestinfo_value(key)
118 if not data:
119 return None
120 enc_type = self._get_guestinfo_value(key + '.encoding')
121 return self._get_encoded_data('guestinfo.' + key, enc_type, data)
122
123 def _get_encoded_metadata(self, key):
124 if not self.metadata or not key in self.metadata:
125 return None
126 data = self.metadata[key]
127 enc_type = self.metadata.get(key + '.encoding')
128 return self._get_encoded_data('metadata.' + key, enc_type, data)
129
130 def _get_encoded_data(self, key, enc_type, data):
Sidharth Surana3a421682018-10-10 15:42:08 -0700131 '''
132 The _get_encoded_data would always return a str
133 ----
134 In py 2.7:
135 json.loads method takes string as input
136 zlib.decompress takes and returns a string
137 base64.b64decode takes and returns a string
138 -----
139 In py 3.6 and newer:
140 json.loads method takes bytes or string as input
141 zlib.decompress takes and returns a bytes
142 base64.b64decode takes bytes or string and returns bytes
143 -----
144 In py > 3, < 3.6:
145 json.loads method takes string as input
146 zlib.decompress takes and returns a bytes
147 base64.b64decode takes bytes or string and returns bytes
148 -----
149 Given the above conditions the output from zlib.decompress and
150 base64.b64decode would be bytes with newer python and str in older
151 version. Thus we would covert the output to str before returning
152 '''
153 rawdata = self._get_encoded_data_raw(key, enc_type, data)
154 if type(rawdata) == bytes:
155 return rawdata.decode('utf-8')
156 return rawdata
157
158 def _get_encoded_data_raw(self, key, enc_type, data):
akutz6501f902018-08-24 12:19:05 -0500159 LOG.debug("Getting encoded data for key=%s, enc=%s", key, enc_type)
160 if enc_type == "gzip+base64" or enc_type == "gz+b64":
161 LOG.debug("Decoding %s format %s", enc_type, key)
162 return zlib.decompress(base64.b64decode(data), zlib.MAX_WBITS | 16)
163 elif enc_type == "base64" or enc_type == "b64":
164 LOG.debug("Decoding %s format %s", enc_type, key)
165 return base64.b64decode(data)
166 else:
167 LOG.debug("Plain-text data %s", key)
168 return data
169
170 def _get_guestinfo_value(self, key):
171 NOVAL = "No value found"
172 LOG.debug("Getting guestinfo value for key %s", key)
173 try:
174 (stdout, stderr) = util.subp([self.vmtoolsd, "--cmd", "info-get guestinfo." + key])
175 if stderr == NOVAL:
176 LOG.debug("No value found for key %s", key)
177 elif not stdout:
178 LOG.error("Failed to get guestinfo value for key %s", key)
179 else:
180 return stdout.rstrip()
181 except util.ProcessExecutionError as error:
182 if error.stderr == NOVAL:
183 LOG.debug("No value found for key %s", key)
184 else:
185 util.logexc(LOG,"Failed to get guestinfo value for key %s: %s", key, error)
186 except Exception:
187 util.logexc(LOG,"Unexpected error while trying to get guestinfo value for key %s", key)
188 return None
189
akutz77457a62018-08-22 16:07:21 -0500190def get_datasource_list(depends):
191 """
192 Return a list of data sources that match this set of dependencies
193 """
Andrew Kutz4f66b8b2018-09-16 18:28:59 -0500194 return [DataSourceVMwareGuestInfo]