| akutz | dd794a4 | 2018-09-18 10:04:21 -0500 | [diff] [blame] | 1 | # Cloud-Init Datasource for VMware Guestinfo | 
 | 2 | # | 
 | 3 | # Copyright (c) 2018 VMware, Inc. All Rights Reserved. | 
 | 4 | # | 
 | 5 | # This product is licensed to you under the Apache 2.0 license (the "License"). | 
 | 6 | # You may not use this product except in compliance with the Apache 2.0 License. | 
 | 7 | # | 
 | 8 | # This product may include a number of subcomponents with separate copyright | 
 | 9 | # notices and license terms. Your use of these subcomponents is subject to the | 
 | 10 | # terms and conditions of the subcomponent's license, as noted in the LICENSE | 
 | 11 | # file. | 
| akutz | 77457a6 | 2018-08-22 16:07:21 -0500 | [diff] [blame] | 12 | # | 
| akutz | 6501f90 | 2018-08-24 12:19:05 -0500 | [diff] [blame] | 13 | # Authors: Anish Swaminathan <anishs@vmware.com> | 
 | 14 | #          Andrew Kutz <akutz@vmware.com> | 
| akutz | 77457a6 | 2018-08-22 16:07:21 -0500 | [diff] [blame] | 15 | # | 
| akutz | 0d1fce5 | 2019-06-01 18:54:29 -0500 | [diff] [blame] | 16 |  | 
 | 17 | ''' | 
 | 18 | A cloud init datasource for VMware GuestInfo. | 
 | 19 | ''' | 
 | 20 |  | 
| akutz | 77457a6 | 2018-08-22 16:07:21 -0500 | [diff] [blame] | 21 | import base64 | 
| akutz | ffc4dd5 | 2019-06-02 11:34:55 -0500 | [diff] [blame] | 22 | import collections | 
| akutz | 84389c8 | 2019-06-02 19:24:38 -0500 | [diff] [blame] | 23 | import copy | 
| akutz | 0d1fce5 | 2019-06-01 18:54:29 -0500 | [diff] [blame] | 24 | from distutils.spawn import find_executable | 
| akutz | ffc4dd5 | 2019-06-02 11:34:55 -0500 | [diff] [blame] | 25 | import json | 
| akutz | 10cd140 | 2019-10-23 18:06:39 -0500 | [diff] [blame] | 26 | import os | 
| akutz | ffc4dd5 | 2019-06-02 11:34:55 -0500 | [diff] [blame] | 27 | import socket | 
| akutz | 10cd140 | 2019-10-23 18:06:39 -0500 | [diff] [blame] | 28 | import string | 
| akutz | ffc4dd5 | 2019-06-02 11:34:55 -0500 | [diff] [blame] | 29 | import zlib | 
| akutz | 77457a6 | 2018-08-22 16:07:21 -0500 | [diff] [blame] | 30 |  | 
 | 31 | from cloudinit import log as logging | 
 | 32 | from cloudinit import sources | 
 | 33 | from cloudinit import util | 
| akutz | 6501f90 | 2018-08-24 12:19:05 -0500 | [diff] [blame] | 34 | from cloudinit import safeyaml | 
| akutz | 77457a6 | 2018-08-22 16:07:21 -0500 | [diff] [blame] | 35 |  | 
| akutz | ffc4dd5 | 2019-06-02 11:34:55 -0500 | [diff] [blame] | 36 | from deepmerge import always_merger | 
 | 37 | import netifaces | 
 | 38 |  | 
| akutz | 77457a6 | 2018-08-22 16:07:21 -0500 | [diff] [blame] | 39 | LOG = logging.getLogger(__name__) | 
| akutz | 0d1fce5 | 2019-06-01 18:54:29 -0500 | [diff] [blame] | 40 | NOVAL = "No value found" | 
| yvespp | 8d58dfd | 2019-10-29 20:42:09 +0100 | [diff] [blame] | 41 | VMWARE_RPCTOOL = find_executable("vmware-rpctool") | 
| akutz | 10cd140 | 2019-10-23 18:06:39 -0500 | [diff] [blame] | 42 | VMX_GUESTINFO = "VMX_GUESTINFO" | 
| akutz | daf81f1 | 2019-12-08 11:20:33 -0600 | [diff] [blame^] | 43 | GUESTINFO_EMPTY_YAML_VAL = "---" | 
| akutz | 77457a6 | 2018-08-22 16:07:21 -0500 | [diff] [blame] | 44 |  | 
| akutz | 0d1fce5 | 2019-06-01 18:54:29 -0500 | [diff] [blame] | 45 |  | 
 | 46 | class NetworkConfigError(Exception): | 
 | 47 |     ''' | 
 | 48 |     NetworkConfigError is raised when there is an issue getting or | 
 | 49 |     applying network configuration. | 
 | 50 |     ''' | 
 | 51 |     pass | 
 | 52 |  | 
 | 53 |  | 
| Andrew Kutz | 4f66b8b | 2018-09-16 18:28:59 -0500 | [diff] [blame] | 54 | class DataSourceVMwareGuestInfo(sources.DataSource): | 
| akutz | 0d1fce5 | 2019-06-01 18:54:29 -0500 | [diff] [blame] | 55 |     ''' | 
 | 56 |     This cloud-init datasource was designed for use with CentOS 7, | 
 | 57 |     which uses cloud-init 0.7.9. However, this datasource should | 
 | 58 |     work with any Linux distribution for which cloud-init is | 
 | 59 |     avaialble. | 
 | 60 |  | 
 | 61 |     The documentation for cloud-init 0.7.9's datasource is | 
 | 62 |     available at http://bit.ly/cloudinit-datasource-0-7-9. The | 
 | 63 |     current documentation for cloud-init is found at | 
 | 64 |     https://cloudinit.readthedocs.io/en/latest/. | 
 | 65 |  | 
 | 66 |     Setting the hostname: | 
 | 67 |         The hostname is set by way of the metadata key "local-hostname". | 
 | 68 |  | 
 | 69 |     Setting the instance ID: | 
 | 70 |         The instance ID may be set by way of the metadata key "instance-id". | 
 | 71 |         However, if this value is absent then then the instance ID is | 
 | 72 |         read from the file /sys/class/dmi/id/product_uuid. | 
 | 73 |  | 
 | 74 |     Configuring the network: | 
 | 75 |         The network is configured by setting the metadata key "network" | 
 | 76 |         with a value consistent with Network Config Versions 1 or 2, | 
 | 77 |         depending on the Linux distro's version of cloud-init: | 
 | 78 |  | 
 | 79 |             Network Config Version 1 - http://bit.ly/cloudinit-net-conf-v1 | 
 | 80 |             Network Config Version 2 - http://bit.ly/cloudinit-net-conf-v2 | 
 | 81 |  | 
 | 82 |         For example, CentOS 7's official cloud-init package is version | 
 | 83 |         0.7.9 and does not support Network Config Version 2. However, | 
 | 84 |         this datasource still supports supplying Network Config Version 2 | 
 | 85 |         data as long as the Linux distro's cloud-init package is new | 
 | 86 |         enough to parse the data. | 
 | 87 |  | 
 | 88 |         The metadata key "network.encoding" may be used to indicate the | 
 | 89 |         format of the metadata key "network". Valid encodings are base64 | 
 | 90 |         and gzip+base64. | 
 | 91 |     ''' | 
 | 92 |  | 
 | 93 |     dsname = 'VMwareGuestInfo' | 
 | 94 |  | 
| akutz | 77457a6 | 2018-08-22 16:07:21 -0500 | [diff] [blame] | 95 |     def __init__(self, sys_cfg, distro, paths, ud_proc=None): | 
 | 96 |         sources.DataSource.__init__(self, sys_cfg, distro, paths, ud_proc) | 
| akutz | 10cd140 | 2019-10-23 18:06:39 -0500 | [diff] [blame] | 97 |         if not get_data_access_method(): | 
| yvespp | 8d58dfd | 2019-10-29 20:42:09 +0100 | [diff] [blame] | 98 |             LOG.error("Failed to find vmware-rpctool") | 
| akutz | 77457a6 | 2018-08-22 16:07:21 -0500 | [diff] [blame] | 99 |  | 
 | 100 |     def get_data(self): | 
| akutz | 0d1fce5 | 2019-06-01 18:54:29 -0500 | [diff] [blame] | 101 |         """ | 
 | 102 |         This method should really be _get_data in accordance with the most | 
 | 103 |         recent versions of cloud-init. However, because the datasource | 
 | 104 |         supports as far back as cloud-init 0.7.9, get_data is still used. | 
 | 105 |  | 
 | 106 |         Because of this the method attempts to do some of the same things | 
 | 107 |         that the get_data functions in newer versions of cloud-init do, | 
 | 108 |         such as calling persist_instance_data. | 
 | 109 |         """ | 
| akutz | dbce3d9 | 2019-12-08 00:09:11 -0600 | [diff] [blame] | 110 |         data_access_method = get_data_access_method() | 
 | 111 |         if not data_access_method: | 
| yvespp | 8d58dfd | 2019-10-29 20:42:09 +0100 | [diff] [blame] | 112 |             LOG.error("vmware-rpctool is required to fetch guestinfo value") | 
| akutz | 77457a6 | 2018-08-22 16:07:21 -0500 | [diff] [blame] | 113 |             return False | 
| akutz | 6501f90 | 2018-08-24 12:19:05 -0500 | [diff] [blame] | 114 |  | 
| akutz | 0d1fce5 | 2019-06-01 18:54:29 -0500 | [diff] [blame] | 115 |         # Get the metadata. | 
 | 116 |         self.metadata = load_metadata() | 
| akutz | 6501f90 | 2018-08-24 12:19:05 -0500 | [diff] [blame] | 117 |  | 
| akutz | 0d1fce5 | 2019-06-01 18:54:29 -0500 | [diff] [blame] | 118 |         # Get the user data. | 
 | 119 |         self.userdata_raw = guestinfo('userdata') | 
| akutz | 6501f90 | 2018-08-24 12:19:05 -0500 | [diff] [blame] | 120 |  | 
| akutz | 0d1fce5 | 2019-06-01 18:54:29 -0500 | [diff] [blame] | 121 |         # Get the vendor data. | 
 | 122 |         self.vendordata_raw = guestinfo('vendordata') | 
| akutz | 6501f90 | 2018-08-24 12:19:05 -0500 | [diff] [blame] | 123 |  | 
| akutz | dbce3d9 | 2019-12-08 00:09:11 -0600 | [diff] [blame] | 124 |         # Check to see if any of the guestinfo data should be removed. | 
 | 125 |         if data_access_method == VMWARE_RPCTOOL: | 
 | 126 |             clear_guestinfo_keys(self.metadata['cleanup-guestinfo']) | 
 | 127 |  | 
| Keerthana K | f0d27ea | 2019-09-05 21:46:31 +0530 | [diff] [blame] | 128 |         if self.metadata or self.userdata_raw or self.vendordata_raw: | 
 | 129 |             return True | 
 | 130 |         else: | 
 | 131 |             return False | 
| akutz | 77457a6 | 2018-08-22 16:07:21 -0500 | [diff] [blame] | 132 |  | 
| akutz | 0d1fce5 | 2019-06-01 18:54:29 -0500 | [diff] [blame] | 133 |     def setup(self, is_new_instance): | 
 | 134 |         """setup(is_new_instance) | 
 | 135 |  | 
 | 136 |         This is called before user-data and vendor-data have been processed. | 
 | 137 |  | 
 | 138 |         Unless the datasource has set mode to 'local', then networking | 
 | 139 |         per 'fallback' or per 'network_config' will have been written and | 
 | 140 |         brought up the OS at this point. | 
 | 141 |         """ | 
 | 142 |  | 
| akutz | ffc4dd5 | 2019-06-02 11:34:55 -0500 | [diff] [blame] | 143 |         # Get information about the host. | 
| akutz | 0d1fce5 | 2019-06-01 18:54:29 -0500 | [diff] [blame] | 144 |         host_info = get_host_info() | 
 | 145 |         LOG.info("got host-info: %s", host_info) | 
| akutz | ffc4dd5 | 2019-06-02 11:34:55 -0500 | [diff] [blame] | 146 |  | 
 | 147 |         # Ensure the metadata gets updated with information about the | 
 | 148 |         # host, including the network interfaces, default IP addresses, | 
 | 149 |         # etc. | 
 | 150 |         self.metadata = always_merger.merge(self.metadata, host_info) | 
| akutz | 0d1fce5 | 2019-06-01 18:54:29 -0500 | [diff] [blame] | 151 |  | 
 | 152 |         # Persist the instance data for versions of cloud-init that support | 
 | 153 |         # doing so. This occurs here rather than in the get_data call in | 
 | 154 |         # order to ensure that the network interfaces are up and can be | 
 | 155 |         # persisted with the metadata. | 
 | 156 |         try: | 
 | 157 |             self.persist_instance_data() | 
 | 158 |         except AttributeError: | 
 | 159 |             pass | 
 | 160 |  | 
| akutz | 6501f90 | 2018-08-24 12:19:05 -0500 | [diff] [blame] | 161 |     @property | 
 | 162 |     def network_config(self): | 
| akutz | 0d1fce5 | 2019-06-01 18:54:29 -0500 | [diff] [blame] | 163 |         if 'network' in self.metadata: | 
 | 164 |             LOG.debug("using metadata network config") | 
 | 165 |         else: | 
 | 166 |             LOG.debug("using fallback network config") | 
 | 167 |             self.metadata['network'] = { | 
 | 168 |                 'config': self.distro.generate_fallback_config(), | 
 | 169 |             } | 
 | 170 |         return self.metadata['network']['config'] | 
| akutz | 77457a6 | 2018-08-22 16:07:21 -0500 | [diff] [blame] | 171 |  | 
 | 172 |     def get_instance_id(self): | 
| akutz | 6501f90 | 2018-08-24 12:19:05 -0500 | [diff] [blame] | 173 |         # Pull the instance ID out of the metadata if present. Otherwise | 
 | 174 |         # read the file /sys/class/dmi/id/product_uuid for the instance ID. | 
 | 175 |         if self.metadata and 'instance-id' in self.metadata: | 
 | 176 |             return self.metadata['instance-id'] | 
| akutz | 77457a6 | 2018-08-22 16:07:21 -0500 | [diff] [blame] | 177 |         with open('/sys/class/dmi/id/product_uuid', 'r') as id_file: | 
| Andrey Klimentyev | e1c5ed4 | 2019-10-09 12:32:44 +0300 | [diff] [blame] | 178 |             self.metadata['instance-id'] = str(id_file.read()).rstrip().lower() | 
| akutz | 0d1fce5 | 2019-06-01 18:54:29 -0500 | [diff] [blame] | 179 |             return self.metadata['instance-id'] | 
| akutz | 77457a6 | 2018-08-22 16:07:21 -0500 | [diff] [blame] | 180 |  | 
| Andrey Klimentyev | a23229d | 2019-10-17 15:30:00 +0300 | [diff] [blame] | 181 |     def get_public_ssh_keys(self): | 
 | 182 |         public_keys_data = "" | 
 | 183 |         if 'public-keys-data' in self.metadata: | 
 | 184 |             public_keys_data = self.metadata['public-keys-data'].splitlines() | 
 | 185 |  | 
 | 186 |         public_keys = [] | 
 | 187 |         if not public_keys_data: | 
 | 188 |             return public_keys | 
 | 189 |  | 
 | 190 |         for public_key in public_keys_data: | 
 | 191 |             public_keys.append(public_key) | 
 | 192 |  | 
 | 193 |         return public_keys | 
 | 194 |  | 
| akutz | 6501f90 | 2018-08-24 12:19:05 -0500 | [diff] [blame] | 195 |  | 
| akutz | 0d1fce5 | 2019-06-01 18:54:29 -0500 | [diff] [blame] | 196 | def decode(key, enc_type, data): | 
 | 197 |     ''' | 
 | 198 |     decode returns the decoded string value of data | 
 | 199 |     key is a string used to identify the data being decoded in log messages | 
 | 200 |     ---- | 
 | 201 |     In py 2.7: | 
 | 202 |     json.loads method takes string as input | 
 | 203 |     zlib.decompress takes and returns a string | 
 | 204 |     base64.b64decode takes and returns a string | 
 | 205 |     ----- | 
 | 206 |     In py 3.6 and newer: | 
 | 207 |     json.loads method takes bytes or string as input | 
 | 208 |     zlib.decompress takes and returns a bytes | 
 | 209 |     base64.b64decode takes bytes or string and returns bytes | 
 | 210 |     ----- | 
 | 211 |     In py > 3, < 3.6: | 
 | 212 |     json.loads method takes string as input | 
 | 213 |     zlib.decompress takes and returns a bytes | 
 | 214 |     base64.b64decode takes bytes or string and returns bytes | 
 | 215 |     ----- | 
 | 216 |     Given the above conditions the output from zlib.decompress and | 
 | 217 |     base64.b64decode would be bytes with newer python and str in older | 
 | 218 |     version. Thus we would covert the output to str before returning | 
 | 219 |     ''' | 
 | 220 |     LOG.debug("Getting encoded data for key=%s, enc=%s", key, enc_type) | 
| akutz | 6501f90 | 2018-08-24 12:19:05 -0500 | [diff] [blame] | 221 |  | 
| akutz | 0d1fce5 | 2019-06-01 18:54:29 -0500 | [diff] [blame] | 222 |     raw_data = None | 
 | 223 |     if enc_type == "gzip+base64" or enc_type == "gz+b64": | 
 | 224 |         LOG.debug("Decoding %s format %s", enc_type, key) | 
 | 225 |         raw_data = zlib.decompress(base64.b64decode(data), zlib.MAX_WBITS | 16) | 
 | 226 |     elif enc_type == "base64" or enc_type == "b64": | 
 | 227 |         LOG.debug("Decoding %s format %s", enc_type, key) | 
 | 228 |         raw_data = base64.b64decode(data) | 
 | 229 |     else: | 
 | 230 |         LOG.debug("Plain-text data %s", key) | 
 | 231 |         raw_data = data | 
| Sidharth Surana | 3a42168 | 2018-10-10 15:42:08 -0700 | [diff] [blame] | 232 |  | 
| akutz | 0d1fce5 | 2019-06-01 18:54:29 -0500 | [diff] [blame] | 233 |     if isinstance(raw_data, bytes): | 
 | 234 |         return raw_data.decode('utf-8') | 
 | 235 |     return raw_data | 
 | 236 |  | 
 | 237 |  | 
| akutz | daf81f1 | 2019-12-08 11:20:33 -0600 | [diff] [blame^] | 238 | def get_none_if_empty_val(val): | 
 | 239 |     ''' | 
 | 240 |     get_none_if_empty_val returns None if the provided value, once stripped | 
 | 241 |     of its trailing whitespace, is empty or equal to GUESTINFO_EMPTY_YAML_VAL. | 
 | 242 |  | 
 | 243 |     The return value is always a string, regardless of whether the input is | 
 | 244 |     a bytes class or a string. | 
 | 245 |     ''' | 
 | 246 |  | 
 | 247 |     # If the provided value is a bytes class, convert it to a string to | 
 | 248 |     # simplify the rest of this function's logic. | 
 | 249 |     if isinstance(val, bytes): | 
 | 250 |         val = val.decode() | 
 | 251 |  | 
 | 252 |     val = val.rstrip() | 
 | 253 |     if len(val) == 0 or val == GUESTINFO_EMPTY_YAML_VAL: | 
 | 254 |         return None | 
 | 255 |     return val | 
 | 256 |  | 
 | 257 |  | 
 | 258 | def handle_returned_guestinfo_val(key, val): | 
 | 259 |     ''' | 
 | 260 |     handle_returned_guestinfo_val returns the provided value if it is | 
 | 261 |     not empty or set to GUESTINFO_EMPTY_YAML_VAL, otherwise None is | 
 | 262 |     returned | 
 | 263 |     ''' | 
 | 264 |     val = get_none_if_empty_val(val) | 
 | 265 |     if val: | 
 | 266 |         return val | 
 | 267 |     LOG.debug("No value found for key %s", key) | 
 | 268 |     return None | 
 | 269 |  | 
 | 270 |  | 
| akutz | 0d1fce5 | 2019-06-01 18:54:29 -0500 | [diff] [blame] | 271 | def get_guestinfo_value(key): | 
 | 272 |     ''' | 
 | 273 |     Returns a guestinfo value for the specified key. | 
 | 274 |     ''' | 
 | 275 |     LOG.debug("Getting guestinfo value for key %s", key) | 
| akutz | 10cd140 | 2019-10-23 18:06:39 -0500 | [diff] [blame] | 276 |  | 
 | 277 |     data_access_method = get_data_access_method() | 
 | 278 |  | 
 | 279 |     if data_access_method == VMX_GUESTINFO: | 
 | 280 |         env_key = ("vmx.guestinfo." + key).upper().replace(".", "_", -1) | 
| akutz | daf81f1 | 2019-12-08 11:20:33 -0600 | [diff] [blame^] | 281 |         return handle_returned_guestinfo_val(key, os.environ.get(env_key, "")) | 
| akutz | 10cd140 | 2019-10-23 18:06:39 -0500 | [diff] [blame] | 282 |  | 
| yvespp | 8d58dfd | 2019-10-29 20:42:09 +0100 | [diff] [blame] | 283 |     if data_access_method == VMWARE_RPCTOOL: | 
| akutz | 10cd140 | 2019-10-23 18:06:39 -0500 | [diff] [blame] | 284 |         try: | 
 | 285 |             (stdout, stderr) = util.subp( | 
| yvespp | 8d58dfd | 2019-10-29 20:42:09 +0100 | [diff] [blame] | 286 |                 [VMWARE_RPCTOOL, "info-get guestinfo." + key]) | 
| akutz | 10cd140 | 2019-10-23 18:06:39 -0500 | [diff] [blame] | 287 |             if stderr == NOVAL: | 
 | 288 |                 LOG.debug("No value found for key %s", key) | 
 | 289 |             elif not stdout: | 
 | 290 |                 LOG.error("Failed to get guestinfo value for key %s", key) | 
 | 291 |             else: | 
| akutz | daf81f1 | 2019-12-08 11:20:33 -0600 | [diff] [blame^] | 292 |                 return handle_returned_guestinfo_val(key, stdout) | 
| akutz | 10cd140 | 2019-10-23 18:06:39 -0500 | [diff] [blame] | 293 |         except util.ProcessExecutionError as error: | 
 | 294 |             if error.stderr == NOVAL: | 
 | 295 |                 LOG.debug("No value found for key %s", key) | 
 | 296 |             else: | 
 | 297 |                 util.logexc( | 
 | 298 |                     LOG, "Failed to get guestinfo value for key %s: %s", key, error) | 
 | 299 |         except Exception: | 
| akutz | 0d1fce5 | 2019-06-01 18:54:29 -0500 | [diff] [blame] | 300 |             util.logexc( | 
| akutz | 10cd140 | 2019-10-23 18:06:39 -0500 | [diff] [blame] | 301 |                 LOG, "Unexpected error while trying to get guestinfo value for key %s", key) | 
 | 302 |  | 
| akutz | 0d1fce5 | 2019-06-01 18:54:29 -0500 | [diff] [blame] | 303 |     return None | 
| akutz | 6501f90 | 2018-08-24 12:19:05 -0500 | [diff] [blame] | 304 |  | 
| akutz | 0d1fce5 | 2019-06-01 18:54:29 -0500 | [diff] [blame] | 305 |  | 
| akutz | dbce3d9 | 2019-12-08 00:09:11 -0600 | [diff] [blame] | 306 | def set_guestinfo_value(key, value): | 
 | 307 |     ''' | 
 | 308 |     Sets a guestinfo value for the specified key. Set value to an empty string | 
 | 309 |     to clear an existing guestinfo key. | 
 | 310 |     ''' | 
 | 311 |  | 
 | 312 |     # If value is an empty string then set it to a single space as it is not | 
 | 313 |     # possible to set a guestinfo key to an empty string. Setting a guestinfo | 
 | 314 |     # key to a single space is as close as it gets to clearing an existing | 
 | 315 |     # guestinfo key. | 
 | 316 |     if value == "": | 
 | 317 |         value = " " | 
 | 318 |  | 
 | 319 |     LOG.debug("Setting guestinfo key=%s to value=%s", key, value) | 
 | 320 |  | 
 | 321 |     data_access_method = get_data_access_method() | 
 | 322 |  | 
 | 323 |     if data_access_method == VMX_GUESTINFO: | 
 | 324 |         return True | 
 | 325 |  | 
 | 326 |     if data_access_method == VMWARE_RPCTOOL: | 
 | 327 |         try: | 
 | 328 |             util.subp( | 
 | 329 |                 [VMWARE_RPCTOOL, ("info-set guestinfo.%s %s" % (key, value))]) | 
 | 330 |             return True | 
 | 331 |         except util.ProcessExecutionError as error: | 
 | 332 |             util.logexc( | 
 | 333 |                 LOG, "Failed to set guestinfo key=%s to value=%s: %s", key, value, error) | 
 | 334 |         except Exception: | 
 | 335 |             util.logexc( | 
 | 336 |                 LOG, "Unexpected error while trying to set guestinfo key=%s to value=%s", key, value) | 
 | 337 |  | 
 | 338 |     return None | 
 | 339 |  | 
 | 340 |  | 
 | 341 | def clear_guestinfo_keys(keys): | 
 | 342 |     ''' | 
| akutz | daf81f1 | 2019-12-08 11:20:33 -0600 | [diff] [blame^] | 343 |     clear_guestinfo_keys clears guestinfo of all of the keys in the given list. | 
 | 344 |     each key will have its value set to "---". Since the value is valid YAML, | 
 | 345 |     cloud-init can still read it if it tries. | 
| akutz | dbce3d9 | 2019-12-08 00:09:11 -0600 | [diff] [blame] | 346 |     ''' | 
 | 347 |     if not keys: | 
 | 348 |         return | 
| akutz | daf81f1 | 2019-12-08 11:20:33 -0600 | [diff] [blame^] | 349 |     if not type(keys) in (list, tuple): | 
 | 350 |         keys = [keys] | 
| akutz | dbce3d9 | 2019-12-08 00:09:11 -0600 | [diff] [blame] | 351 |     for key in keys: | 
| akutz | daf81f1 | 2019-12-08 11:20:33 -0600 | [diff] [blame^] | 352 |         LOG.info("clearing guestinfo.%s", key) | 
 | 353 |         if not set_guestinfo_value(key, GUESTINFO_EMPTY_YAML_VAL): | 
 | 354 |             LOG.error("failed to clear guestinfo.%s", key) | 
 | 355 |         LOG.info("clearing guestinfo.%s.encoding", key) | 
 | 356 |         if not set_guestinfo_value(key + ".encoding", ""): | 
 | 357 |             LOG.error("failed to clear guestinfo.%s.encoding", key) | 
| akutz | dbce3d9 | 2019-12-08 00:09:11 -0600 | [diff] [blame] | 358 |  | 
 | 359 |  | 
| akutz | 0d1fce5 | 2019-06-01 18:54:29 -0500 | [diff] [blame] | 360 | def guestinfo(key): | 
 | 361 |     ''' | 
 | 362 |     guestinfo returns the guestinfo value for the provided key, decoding | 
 | 363 |     the value when required | 
 | 364 |     ''' | 
 | 365 |     data = get_guestinfo_value(key) | 
 | 366 |     if not data: | 
| akutz | 6501f90 | 2018-08-24 12:19:05 -0500 | [diff] [blame] | 367 |         return None | 
| akutz | 0d1fce5 | 2019-06-01 18:54:29 -0500 | [diff] [blame] | 368 |     enc_type = get_guestinfo_value(key + '.encoding') | 
 | 369 |     return decode('guestinfo.' + key, enc_type, data) | 
 | 370 |  | 
 | 371 |  | 
 | 372 | def load(data): | 
 | 373 |     ''' | 
 | 374 |     load first attempts to unmarshal the provided data as JSON, and if | 
 | 375 |     that fails then attempts to unmarshal the data as YAML. If data is | 
 | 376 |     None then a new dictionary is returned. | 
 | 377 |     ''' | 
 | 378 |     if not data: | 
 | 379 |         return {} | 
 | 380 |     try: | 
 | 381 |         return json.loads(data) | 
 | 382 |     except: | 
 | 383 |         return safeyaml.load(data) | 
 | 384 |  | 
 | 385 |  | 
 | 386 | def load_metadata(): | 
 | 387 |     ''' | 
 | 388 |     load_metadata loads the metadata from the guestinfo data, optionally | 
 | 389 |     decoding the network config when required | 
 | 390 |     ''' | 
 | 391 |     data = load(guestinfo('metadata')) | 
| akutz | 84389c8 | 2019-06-02 19:24:38 -0500 | [diff] [blame] | 392 |     LOG.debug('loaded metadata %s', data) | 
| akutz | 0d1fce5 | 2019-06-01 18:54:29 -0500 | [diff] [blame] | 393 |  | 
 | 394 |     network = None | 
 | 395 |     if 'network' in data: | 
 | 396 |         network = data['network'] | 
 | 397 |         del data['network'] | 
 | 398 |  | 
 | 399 |     network_enc = None | 
 | 400 |     if 'network.encoding' in data: | 
 | 401 |         network_enc = data['network.encoding'] | 
 | 402 |         del data['network.encoding'] | 
 | 403 |  | 
 | 404 |     if network: | 
| akutz | 84389c8 | 2019-06-02 19:24:38 -0500 | [diff] [blame] | 405 |         LOG.debug('network data found') | 
 | 406 |         if isinstance(network, collections.Mapping): | 
 | 407 |             LOG.debug("network data copied to 'config' key") | 
 | 408 |             network = { | 
 | 409 |                 'config': copy.deepcopy(network) | 
 | 410 |             } | 
 | 411 |         else: | 
 | 412 |             LOG.debug("network data to be decoded %s", network) | 
| akutz | 0d1fce5 | 2019-06-01 18:54:29 -0500 | [diff] [blame] | 413 |             dec_net = decode('metadata.network', network_enc, network) | 
| akutz | 84389c8 | 2019-06-02 19:24:38 -0500 | [diff] [blame] | 414 |             network = { | 
 | 415 |                 'config': load(dec_net), | 
 | 416 |             } | 
 | 417 |  | 
 | 418 |         LOG.debug('network data %s', network) | 
| akutz | 0d1fce5 | 2019-06-01 18:54:29 -0500 | [diff] [blame] | 419 |         data['network'] = network | 
 | 420 |  | 
 | 421 |     return data | 
 | 422 |  | 
| akutz | 6501f90 | 2018-08-24 12:19:05 -0500 | [diff] [blame] | 423 |  | 
| akutz | 77457a6 | 2018-08-22 16:07:21 -0500 | [diff] [blame] | 424 | def get_datasource_list(depends): | 
| akutz | 0d1fce5 | 2019-06-01 18:54:29 -0500 | [diff] [blame] | 425 |     ''' | 
| akutz | 77457a6 | 2018-08-22 16:07:21 -0500 | [diff] [blame] | 426 |     Return a list of data sources that match this set of dependencies | 
| akutz | 0d1fce5 | 2019-06-01 18:54:29 -0500 | [diff] [blame] | 427 |     ''' | 
| Andrew Kutz | 4f66b8b | 2018-09-16 18:28:59 -0500 | [diff] [blame] | 428 |     return [DataSourceVMwareGuestInfo] | 
| akutz | 0d1fce5 | 2019-06-01 18:54:29 -0500 | [diff] [blame] | 429 |  | 
 | 430 |  | 
| akutz | ffc4dd5 | 2019-06-02 11:34:55 -0500 | [diff] [blame] | 431 | def get_default_ip_addrs(): | 
 | 432 |     ''' | 
 | 433 |     Returns the default IPv4 and IPv6 addresses based on the device(s) used for | 
 | 434 |     the default route. Please note that None may be returned for either address | 
 | 435 |     family if that family has no default route or if there are multiple | 
 | 436 |     addresses associated with the device used by the default route for a given | 
 | 437 |     address. | 
 | 438 |     ''' | 
 | 439 |     gateways = netifaces.gateways() | 
 | 440 |     if 'default' not in gateways: | 
 | 441 |         return None, None | 
 | 442 |  | 
 | 443 |     default_gw = gateways['default'] | 
 | 444 |     if netifaces.AF_INET not in default_gw and netifaces.AF_INET6 not in default_gw: | 
 | 445 |         return None, None | 
 | 446 |  | 
 | 447 |     ipv4 = None | 
 | 448 |     ipv6 = None | 
 | 449 |  | 
 | 450 |     gw4 = default_gw.get(netifaces.AF_INET) | 
 | 451 |     if gw4: | 
 | 452 |         _, dev4 = gw4 | 
| akutz | 0b519f7 | 2019-06-02 14:58:57 -0500 | [diff] [blame] | 453 |         addr4_fams = netifaces.ifaddresses(dev4) | 
 | 454 |         if addr4_fams: | 
 | 455 |             af_inet4 = addr4_fams.get(netifaces.AF_INET) | 
 | 456 |             if af_inet4: | 
 | 457 |                 if len(af_inet4) > 1: | 
| akutz | ffc4dd5 | 2019-06-02 11:34:55 -0500 | [diff] [blame] | 458 |                     LOG.warn( | 
| akutz | 0b519f7 | 2019-06-02 14:58:57 -0500 | [diff] [blame] | 459 |                         "device %s has more than one ipv4 address: %s", dev4, af_inet4) | 
 | 460 |                 elif 'addr' in af_inet4[0]: | 
 | 461 |                     ipv4 = af_inet4[0]['addr'] | 
| akutz | ffc4dd5 | 2019-06-02 11:34:55 -0500 | [diff] [blame] | 462 |  | 
 | 463 |     # Try to get the default IPv6 address by first seeing if there is a default | 
| akutz | 0b519f7 | 2019-06-02 14:58:57 -0500 | [diff] [blame] | 464 |     # IPv6 route. | 
| akutz | ffc4dd5 | 2019-06-02 11:34:55 -0500 | [diff] [blame] | 465 |     gw6 = default_gw.get(netifaces.AF_INET6) | 
 | 466 |     if gw6: | 
 | 467 |         _, dev6 = gw6 | 
 | 468 |         addr6_fams = netifaces.ifaddresses(dev6) | 
 | 469 |         if addr6_fams: | 
 | 470 |             af_inet6 = addr6_fams.get(netifaces.AF_INET6) | 
 | 471 |             if af_inet6: | 
 | 472 |                 if len(af_inet6) > 1: | 
 | 473 |                     LOG.warn( | 
 | 474 |                         "device %s has more than one ipv6 address: %s", dev6, af_inet6) | 
 | 475 |                 elif 'addr' in af_inet6[0]: | 
 | 476 |                     ipv6 = af_inet6[0]['addr'] | 
| akutz | 0b519f7 | 2019-06-02 14:58:57 -0500 | [diff] [blame] | 477 |  | 
 | 478 |     # If there is a default IPv4 address but not IPv6, then see if there is a | 
 | 479 |     # single IPv6 address associated with the same device associated with the | 
 | 480 |     # default IPv4 address. | 
 | 481 |     if ipv4 and not ipv6: | 
 | 482 |         af_inet6 = addr4_fams.get(netifaces.AF_INET6) | 
| akutz | ffc4dd5 | 2019-06-02 11:34:55 -0500 | [diff] [blame] | 483 |         if af_inet6: | 
 | 484 |             if len(af_inet6) > 1: | 
 | 485 |                 LOG.warn( | 
 | 486 |                     "device %s has more than one ipv6 address: %s", dev4, af_inet6) | 
 | 487 |             elif 'addr' in af_inet6[0]: | 
 | 488 |                 ipv6 = af_inet6[0]['addr'] | 
 | 489 |  | 
| akutz | 0b519f7 | 2019-06-02 14:58:57 -0500 | [diff] [blame] | 490 |     # If there is a default IPv6 address but not IPv4, then see if there is a | 
 | 491 |     # single IPv4 address associated with the same device associated with the | 
 | 492 |     # default IPv6 address. | 
 | 493 |     if not ipv4 and ipv6: | 
 | 494 |         af_inet4 = addr6_fams.get(netifaces.AF_INET4) | 
 | 495 |         if af_inet4: | 
 | 496 |             if len(af_inet4) > 1: | 
 | 497 |                 LOG.warn( | 
 | 498 |                     "device %s has more than one ipv4 address: %s", dev6, af_inet4) | 
 | 499 |             elif 'addr' in af_inet4[0]: | 
 | 500 |                 ipv4 = af_inet4[0]['addr'] | 
 | 501 |  | 
| akutz | ffc4dd5 | 2019-06-02 11:34:55 -0500 | [diff] [blame] | 502 |     return ipv4, ipv6 | 
 | 503 |  | 
| Keerthana K | f0d27ea | 2019-09-05 21:46:31 +0530 | [diff] [blame] | 504 | # patched socket.getfqdn() - see https://bugs.python.org/issue5004 | 
| akutz | 10cd140 | 2019-10-23 18:06:39 -0500 | [diff] [blame] | 505 |  | 
 | 506 |  | 
| Keerthana K | f0d27ea | 2019-09-05 21:46:31 +0530 | [diff] [blame] | 507 | def getfqdn(name=''): | 
 | 508 |     """Get fully qualified domain name from name. | 
 | 509 |      An empty argument is interpreted as meaning the local host. | 
 | 510 |     """ | 
 | 511 |     name = name.strip() | 
 | 512 |     if not name or name == '0.0.0.0': | 
 | 513 |         name = socket.gethostname() | 
 | 514 |     try: | 
| akutz | 10cd140 | 2019-10-23 18:06:39 -0500 | [diff] [blame] | 515 |         addrs = socket.getaddrinfo( | 
 | 516 |             name, None, 0, socket.SOCK_DGRAM, 0, socket.AI_CANONNAME) | 
| Keerthana K | f0d27ea | 2019-09-05 21:46:31 +0530 | [diff] [blame] | 517 |     except socket.error: | 
 | 518 |         pass | 
 | 519 |     else: | 
 | 520 |         for addr in addrs: | 
 | 521 |             if addr[3]: | 
 | 522 |                 name = addr[3] | 
 | 523 |                 break | 
 | 524 |     return name | 
| akutz | ffc4dd5 | 2019-06-02 11:34:55 -0500 | [diff] [blame] | 525 |  | 
| akutz | 10cd140 | 2019-10-23 18:06:39 -0500 | [diff] [blame] | 526 |  | 
| akutz | 0d1fce5 | 2019-06-01 18:54:29 -0500 | [diff] [blame] | 527 | def get_host_info(): | 
 | 528 |     ''' | 
 | 529 |     Returns host information such as the host name and network interfaces. | 
 | 530 |     ''' | 
| akutz | 0d1fce5 | 2019-06-01 18:54:29 -0500 | [diff] [blame] | 531 |  | 
 | 532 |     host_info = { | 
 | 533 |         'network': { | 
 | 534 |             'interfaces': { | 
 | 535 |                 'by-mac': collections.OrderedDict(), | 
| akutz | ffc4dd5 | 2019-06-02 11:34:55 -0500 | [diff] [blame] | 536 |                 'by-ipv4': collections.OrderedDict(), | 
 | 537 |                 'by-ipv6': collections.OrderedDict(), | 
| akutz | 0d1fce5 | 2019-06-01 18:54:29 -0500 | [diff] [blame] | 538 |             }, | 
 | 539 |         }, | 
 | 540 |     } | 
 | 541 |  | 
| Keerthana K | f0d27ea | 2019-09-05 21:46:31 +0530 | [diff] [blame] | 542 |     hostname = getfqdn(socket.gethostname()) | 
| akutz | 0d1fce5 | 2019-06-01 18:54:29 -0500 | [diff] [blame] | 543 |     if hostname: | 
| akutz | ffc4dd5 | 2019-06-02 11:34:55 -0500 | [diff] [blame] | 544 |         host_info['hostname'] = hostname | 
| akutz | 0d1fce5 | 2019-06-01 18:54:29 -0500 | [diff] [blame] | 545 |         host_info['local-hostname'] = hostname | 
 | 546 |  | 
| akutz | ffc4dd5 | 2019-06-02 11:34:55 -0500 | [diff] [blame] | 547 |     default_ipv4, default_ipv6 = get_default_ip_addrs() | 
 | 548 |     if default_ipv4: | 
 | 549 |         host_info['local-ipv4'] = default_ipv4 | 
 | 550 |     if default_ipv6: | 
 | 551 |         host_info['local-ipv6'] = default_ipv6 | 
 | 552 |  | 
| akutz | 0d1fce5 | 2019-06-01 18:54:29 -0500 | [diff] [blame] | 553 |     by_mac = host_info['network']['interfaces']['by-mac'] | 
| akutz | ffc4dd5 | 2019-06-02 11:34:55 -0500 | [diff] [blame] | 554 |     by_ipv4 = host_info['network']['interfaces']['by-ipv4'] | 
 | 555 |     by_ipv6 = host_info['network']['interfaces']['by-ipv6'] | 
| akutz | 0d1fce5 | 2019-06-01 18:54:29 -0500 | [diff] [blame] | 556 |  | 
 | 557 |     ifaces = netifaces.interfaces() | 
 | 558 |     for dev_name in ifaces: | 
 | 559 |         addr_fams = netifaces.ifaddresses(dev_name) | 
 | 560 |         af_link = addr_fams.get(netifaces.AF_LINK) | 
| akutz | 0b519f7 | 2019-06-02 14:58:57 -0500 | [diff] [blame] | 561 |         af_inet4 = addr_fams.get(netifaces.AF_INET) | 
| akutz | 0d1fce5 | 2019-06-01 18:54:29 -0500 | [diff] [blame] | 562 |         af_inet6 = addr_fams.get(netifaces.AF_INET6) | 
 | 563 |  | 
 | 564 |         mac = None | 
 | 565 |         if af_link and 'addr' in af_link[0]: | 
 | 566 |             mac = af_link[0]['addr'] | 
 | 567 |  | 
 | 568 |         # Do not bother recording localhost | 
 | 569 |         if mac == "00:00:00:00:00:00": | 
 | 570 |             continue | 
 | 571 |  | 
| akutz | 0b519f7 | 2019-06-02 14:58:57 -0500 | [diff] [blame] | 572 |         if mac and (af_inet4 or af_inet6): | 
| akutz | 0d1fce5 | 2019-06-01 18:54:29 -0500 | [diff] [blame] | 573 |             key = mac | 
 | 574 |             val = {} | 
| akutz | 0b519f7 | 2019-06-02 14:58:57 -0500 | [diff] [blame] | 575 |             if af_inet4: | 
 | 576 |                 val["ipv4"] = af_inet4 | 
| akutz | 0d1fce5 | 2019-06-01 18:54:29 -0500 | [diff] [blame] | 577 |             if af_inet6: | 
| akutz | ffc4dd5 | 2019-06-02 11:34:55 -0500 | [diff] [blame] | 578 |                 val["ipv6"] = af_inet6 | 
| akutz | 0d1fce5 | 2019-06-01 18:54:29 -0500 | [diff] [blame] | 579 |             by_mac[key] = val | 
 | 580 |  | 
| akutz | 0b519f7 | 2019-06-02 14:58:57 -0500 | [diff] [blame] | 581 |         if af_inet4: | 
 | 582 |             for ip_info in af_inet4: | 
| akutz | 0d1fce5 | 2019-06-01 18:54:29 -0500 | [diff] [blame] | 583 |                 key = ip_info['addr'] | 
| akutz | ffc4dd5 | 2019-06-02 11:34:55 -0500 | [diff] [blame] | 584 |                 if key == '127.0.0.1': | 
 | 585 |                     continue | 
| akutz | 84389c8 | 2019-06-02 19:24:38 -0500 | [diff] [blame] | 586 |                 val = copy.deepcopy(ip_info) | 
| akutz | 0d1fce5 | 2019-06-01 18:54:29 -0500 | [diff] [blame] | 587 |                 del val['addr'] | 
 | 588 |                 if mac: | 
 | 589 |                     val['mac'] = mac | 
| akutz | ffc4dd5 | 2019-06-02 11:34:55 -0500 | [diff] [blame] | 590 |                 by_ipv4[key] = val | 
| akutz | 0d1fce5 | 2019-06-01 18:54:29 -0500 | [diff] [blame] | 591 |  | 
 | 592 |         if af_inet6: | 
 | 593 |             for ip_info in af_inet6: | 
 | 594 |                 key = ip_info['addr'] | 
| akutz | ffc4dd5 | 2019-06-02 11:34:55 -0500 | [diff] [blame] | 595 |                 if key == '::1': | 
 | 596 |                     continue | 
| akutz | 84389c8 | 2019-06-02 19:24:38 -0500 | [diff] [blame] | 597 |                 val = copy.deepcopy(ip_info) | 
| akutz | 0d1fce5 | 2019-06-01 18:54:29 -0500 | [diff] [blame] | 598 |                 del val['addr'] | 
 | 599 |                 if mac: | 
 | 600 |                     val['mac'] = mac | 
| akutz | ffc4dd5 | 2019-06-02 11:34:55 -0500 | [diff] [blame] | 601 |                 by_ipv6[key] = val | 
| akutz | 0d1fce5 | 2019-06-01 18:54:29 -0500 | [diff] [blame] | 602 |  | 
 | 603 |     return host_info | 
 | 604 |  | 
 | 605 |  | 
| akutz | 10cd140 | 2019-10-23 18:06:39 -0500 | [diff] [blame] | 606 | def get_data_access_method(): | 
 | 607 |     if os.environ.get(VMX_GUESTINFO, ""): | 
 | 608 |         return VMX_GUESTINFO | 
| yvespp | 8d58dfd | 2019-10-29 20:42:09 +0100 | [diff] [blame] | 609 |     if VMWARE_RPCTOOL: | 
 | 610 |         return VMWARE_RPCTOOL | 
| akutz | 10cd140 | 2019-10-23 18:06:39 -0500 | [diff] [blame] | 611 |     return None | 
 | 612 |  | 
 | 613 |  | 
| akutz | ffc4dd5 | 2019-06-02 11:34:55 -0500 | [diff] [blame] | 614 | def main(): | 
 | 615 |     ''' | 
 | 616 |     Executed when this file is used as a program. | 
 | 617 |     ''' | 
 | 618 |     metadata = {'network': {'config': {'dhcp': True}}} | 
 | 619 |     host_info = get_host_info() | 
 | 620 |     metadata = always_merger.merge(metadata, host_info) | 
 | 621 |     print(util.json_dumps(metadata)) | 
 | 622 |  | 
 | 623 |  | 
| akutz | 0d1fce5 | 2019-06-01 18:54:29 -0500 | [diff] [blame] | 624 | if __name__ == "__main__": | 
| akutz | ffc4dd5 | 2019-06-02 11:34:55 -0500 | [diff] [blame] | 625 |     main() | 
| akutz | 0d1fce5 | 2019-06-01 18:54:29 -0500 | [diff] [blame] | 626 |  | 
 | 627 | # vi: ts=4 expandtab |