blob: b86938e452dbe649783f67b187b910e401002478 [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
Ivan Berezovskiy14759d42021-11-11 19:06:51 +040032# from cloud-init >= 20.3 subp is in its own module
33try:
34 from cloudinit.subp import subp, ProcessExecutionError
35except ImportError:
36 from cloudinit.util import subp, ProcessExecutionError
37
Andrew Kutz4f66b8b2018-09-16 18:28:59 -050038# This cloud-init datasource was designed for use with CentOS 7,
39# which uses cloud-init 0.7.9. However, this datasource should
akutz14fb50b2019-05-29 13:38:28 -050040# work with any Linux distribution for which cloud-init is
Andrew Kutz4f66b8b2018-09-16 18:28:59 -050041# avaialble.
42#
akutz14fb50b2019-05-29 13:38:28 -050043# The documentation for cloud-init 0.7.9's datasource is
Andrew Kutz4f66b8b2018-09-16 18:28:59 -050044# available at http://bit.ly/cloudinit-datasource-0-7-9. The
45# current documentation for cloud-init is found at
46# https://cloudinit.readthedocs.io/en/latest/.
akutz6501f902018-08-24 12:19:05 -050047#
48# Setting the hostname:
49# The hostname is set by way of the metadata key "local-hostname".
50#
51# Setting the instance ID:
52# The instance ID may be set by way of the metadata key "instance-id".
53# However, if this value is absent then then the instance ID is
54# read from the file /sys/class/dmi/id/product_uuid.
55#
56# Configuring the network:
57# The network is configured by setting the metadata key "network"
58# with a value consistent with Network Config Versions 1 or 2,
59# depending on the Linux distro's version of cloud-init:
60#
61# Network Config Version 1 - http://bit.ly/cloudinit-net-conf-v1
62# Network Config Version 2 - http://bit.ly/cloudinit-net-conf-v2
63#
64# For example, CentOS 7's official cloud-init package is version
65# 0.7.9 and does not support Network Config Version 2. However,
66# this datasource still supports supplying Network Config Version 2
67# data as long as the Linux distro's cloud-init package is new
68# enough to parse the data.
69#
70# The metadata key "network.encoding" may be used to indicate the
71# format of the metadata key "network". Valid encodings are base64
72# and gzip+base64.
Andrew Kutz4f66b8b2018-09-16 18:28:59 -050073class DataSourceVMwareGuestInfo(sources.DataSource):
akutz77457a62018-08-22 16:07:21 -050074 def __init__(self, sys_cfg, distro, paths, ud_proc=None):
75 sources.DataSource.__init__(self, sys_cfg, distro, paths, ud_proc)
akutz77457a62018-08-22 16:07:21 -050076 self.vmtoolsd = find_executable("vmtoolsd")
77 if not self.vmtoolsd:
78 LOG.error("Failed to find vmtoolsd")
79
80 def get_data(self):
81 if not self.vmtoolsd:
82 LOG.error("vmtoolsd is required to fetch guestinfo value")
83 return False
akutz6501f902018-08-24 12:19:05 -050084
85 # Get the JSON metadata. Can be plain-text, base64, or gzip+base64.
86 metadata = self._get_encoded_guestinfo_data('metadata')
87 if metadata:
akutz14fb50b2019-05-29 13:38:28 -050088 try:
89 self.metadata = json.loads(metadata)
90 except:
91 self.metadata = safeyaml.load(metadata)
akutz6501f902018-08-24 12:19:05 -050092
93 # Get the YAML userdata. Can be plain-text, base64, or gzip+base64.
94 self.userdata_raw = self._get_encoded_guestinfo_data('userdata')
95
96 # Get the YAML vendordata. Can be plain-text, base64, or gzip+base64.
97 self.vendordata_raw = self._get_encoded_guestinfo_data('vendordata')
98
akutz77457a62018-08-22 16:07:21 -050099 return True
100
akutz6501f902018-08-24 12:19:05 -0500101 @property
102 def network_config(self):
103 # Pull the network configuration out of the metadata.
104 if self.metadata and 'network' in self.metadata:
105 data = self._get_encoded_metadata('network')
106 if data:
107 # Load the YAML-formatted network data into an object
108 # and return it.
109 net_config = safeyaml.load(data)
110 LOG.debug("Loaded network config: %s", net_config)
111 return net_config
112 return None
akutz77457a62018-08-22 16:07:21 -0500113
114 def get_instance_id(self):
akutz6501f902018-08-24 12:19:05 -0500115 # Pull the instance ID out of the metadata if present. Otherwise
116 # read the file /sys/class/dmi/id/product_uuid for the instance ID.
117 if self.metadata and 'instance-id' in self.metadata:
118 return self.metadata['instance-id']
akutz77457a62018-08-22 16:07:21 -0500119 with open('/sys/class/dmi/id/product_uuid', 'r') as id_file:
120 return str(id_file.read()).rstrip()
121
akutz6501f902018-08-24 12:19:05 -0500122 def _get_encoded_guestinfo_data(self, key):
123 data = self._get_guestinfo_value(key)
124 if not data:
125 return None
126 enc_type = self._get_guestinfo_value(key + '.encoding')
127 return self._get_encoded_data('guestinfo.' + key, enc_type, data)
128
129 def _get_encoded_metadata(self, key):
130 if not self.metadata or not key in self.metadata:
131 return None
132 data = self.metadata[key]
133 enc_type = self.metadata.get(key + '.encoding')
134 return self._get_encoded_data('metadata.' + key, enc_type, data)
135
136 def _get_encoded_data(self, key, enc_type, data):
Sidharth Surana3a421682018-10-10 15:42:08 -0700137 '''
138 The _get_encoded_data would always return a str
139 ----
140 In py 2.7:
141 json.loads method takes string as input
142 zlib.decompress takes and returns a string
143 base64.b64decode takes and returns a string
144 -----
145 In py 3.6 and newer:
146 json.loads method takes bytes or string as input
147 zlib.decompress takes and returns a bytes
148 base64.b64decode takes bytes or string and returns bytes
149 -----
150 In py > 3, < 3.6:
151 json.loads method takes string as input
152 zlib.decompress takes and returns a bytes
153 base64.b64decode takes bytes or string and returns bytes
154 -----
155 Given the above conditions the output from zlib.decompress and
156 base64.b64decode would be bytes with newer python and str in older
157 version. Thus we would covert the output to str before returning
158 '''
159 rawdata = self._get_encoded_data_raw(key, enc_type, data)
160 if type(rawdata) == bytes:
161 return rawdata.decode('utf-8')
162 return rawdata
163
164 def _get_encoded_data_raw(self, key, enc_type, data):
akutz6501f902018-08-24 12:19:05 -0500165 LOG.debug("Getting encoded data for key=%s, enc=%s", key, enc_type)
166 if enc_type == "gzip+base64" or enc_type == "gz+b64":
167 LOG.debug("Decoding %s format %s", enc_type, key)
168 return zlib.decompress(base64.b64decode(data), zlib.MAX_WBITS | 16)
169 elif enc_type == "base64" or enc_type == "b64":
170 LOG.debug("Decoding %s format %s", enc_type, key)
171 return base64.b64decode(data)
172 else:
173 LOG.debug("Plain-text data %s", key)
174 return data
175
176 def _get_guestinfo_value(self, key):
177 NOVAL = "No value found"
178 LOG.debug("Getting guestinfo value for key %s", key)
179 try:
Ivan Berezovskiy14759d42021-11-11 19:06:51 +0400180 (stdout, stderr) = subp([self.vmtoolsd, "--cmd", "info-get guestinfo." + key])
akutz6501f902018-08-24 12:19:05 -0500181 if stderr == NOVAL:
182 LOG.debug("No value found for key %s", key)
183 elif not stdout:
184 LOG.error("Failed to get guestinfo value for key %s", key)
185 else:
186 return stdout.rstrip()
Ivan Berezovskiy14759d42021-11-11 19:06:51 +0400187 except ProcessExecutionError as error:
akutz6501f902018-08-24 12:19:05 -0500188 if error.stderr == NOVAL:
189 LOG.debug("No value found for key %s", key)
190 else:
191 util.logexc(LOG,"Failed to get guestinfo value for key %s: %s", key, error)
192 except Exception:
193 util.logexc(LOG,"Unexpected error while trying to get guestinfo value for key %s", key)
194 return None
195
akutz77457a62018-08-22 16:07:21 -0500196def get_datasource_list(depends):
197 """
198 Return a list of data sources that match this set of dependencies
199 """
Andrew Kutz4f66b8b2018-09-16 18:28:59 -0500200 return [DataSourceVMwareGuestInfo]