1# Copyright (C) 2012 Yahoo! Inc. 2# Copyright (C) 2014 Amazon.com, Inc. or its affiliates. 3# 4# Author: Joshua Harlow <harlowja@yahoo-inc.com> 5# Author: Andrew Jorgensen <ajorgens@amazon.com> 6# 7# This file is part of cloud-init. See LICENSE file for license information. 8 9import functools 10import json 11 12from cloudinit import log as logging 13from cloudinit import url_helper 14from cloudinit import util 15 16LOG = logging.getLogger(__name__) 17SKIP_USERDATA_CODES = frozenset([url_helper.NOT_FOUND]) 18 19 20class MetadataLeafDecoder(object): 21 """Decodes a leaf blob into something meaningful.""" 22 23 def _maybe_json_object(self, text): 24 if not text: 25 return False 26 text = text.strip() 27 if text.startswith("{") and text.endswith("}"): 28 return True 29 return False 30 31 def __call__(self, field, blob): 32 if not blob: 33 return '' 34 try: 35 blob = util.decode_binary(blob) 36 except UnicodeDecodeError: 37 return blob 38 if self._maybe_json_object(blob): 39 try: 40 # Assume it's json, unless it fails parsing... 41 return json.loads(blob) 42 except (ValueError, TypeError) as e: 43 LOG.warning("Field %s looked like a json object, but it" 44 " was not: %s", field, e) 45 if blob.find("\n") != -1: 46 return blob.splitlines() 47 return blob 48 49 50# See: http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ 51# ec2-instance-metadata.html 52class MetadataMaterializer(object): 53 def __init__(self, blob, base_url, caller, leaf_decoder=None): 54 self._blob = blob 55 self._md = None 56 self._base_url = base_url 57 self._caller = caller 58 if leaf_decoder is None: 59 self._leaf_decoder = MetadataLeafDecoder() 60 else: 61 self._leaf_decoder = leaf_decoder 62 63 def _parse(self, blob): 64 leaves = {} 65 children = [] 66 blob = util.decode_binary(blob) 67 68 if not blob: 69 return (leaves, children) 70 71 def has_children(item): 72 if item.endswith("/"): 73 return True 74 else: 75 return False 76 77 def get_name(item): 78 if item.endswith("/"): 79 return item.rstrip("/") 80 return item 81 82 for field in blob.splitlines(): 83 field = field.strip() 84 field_name = get_name(field) 85 if not field or not field_name: 86 continue 87 # Don't materialize credentials 88 if field_name == 'security-credentials': 89 continue 90 if has_children(field): 91 if field_name not in children: 92 children.append(field_name) 93 else: 94 contents = field.split("=", 1) 95 resource = field_name 96 if len(contents) > 1: 97 # What a PITA... 98 (ident, sub_contents) = contents 99 ident = util.safe_int(ident) 100 if ident is not None: 101 resource = "%s/openssh-key" % (ident) 102 field_name = sub_contents 103 leaves[field_name] = resource 104 return (leaves, children) 105 106 def materialize(self): 107 if self._md is not None: 108 return self._md 109 self._md = self._materialize(self._blob, self._base_url) 110 return self._md 111 112 def _materialize(self, blob, base_url): 113 (leaves, children) = self._parse(blob) 114 child_contents = {} 115 for c in children: 116 child_url = url_helper.combine_url(base_url, c) 117 if not child_url.endswith("/"): 118 child_url += "/" 119 child_blob = self._caller(child_url) 120 child_contents[c] = self._materialize(child_blob, child_url) 121 leaf_contents = {} 122 for (field, resource) in leaves.items(): 123 leaf_url = url_helper.combine_url(base_url, resource) 124 leaf_blob = self._caller(leaf_url) 125 leaf_contents[field] = self._leaf_decoder(field, leaf_blob) 126 joined = {} 127 joined.update(child_contents) 128 for field in leaf_contents.keys(): 129 if field in joined: 130 LOG.warning("Duplicate key found in results from %s", 131 base_url) 132 else: 133 joined[field] = leaf_contents[field] 134 return joined 135 136 137def skip_retry_on_codes(status_codes, _request_args, cause): 138 """Returns False if cause.code is in status_codes.""" 139 return cause.code not in status_codes 140 141 142def get_instance_userdata(api_version='latest', 143 metadata_address='http://169.254.169.254', 144 ssl_details=None, timeout=5, retries=5, 145 headers_cb=None, headers_redact=None, 146 exception_cb=None): 147 ud_url = url_helper.combine_url(metadata_address, api_version) 148 ud_url = url_helper.combine_url(ud_url, 'user-data') 149 user_data = '' 150 try: 151 if not exception_cb: 152 # It is ok for userdata to not exist (thats why we are stopping if 153 # NOT_FOUND occurs) and just in that case returning an empty 154 # string. 155 exception_cb = functools.partial(skip_retry_on_codes, 156 SKIP_USERDATA_CODES) 157 response = url_helper.read_file_or_url( 158 ud_url, ssl_details=ssl_details, timeout=timeout, 159 retries=retries, exception_cb=exception_cb, headers_cb=headers_cb, 160 headers_redact=headers_redact) 161 user_data = response.contents 162 except url_helper.UrlError as e: 163 if e.code not in SKIP_USERDATA_CODES: 164 util.logexc(LOG, "Failed fetching userdata from url %s", ud_url) 165 except Exception: 166 util.logexc(LOG, "Failed fetching userdata from url %s", ud_url) 167 return user_data 168 169 170def _get_instance_metadata(tree, api_version='latest', 171 metadata_address='http://169.254.169.254', 172 ssl_details=None, timeout=5, retries=5, 173 leaf_decoder=None, headers_cb=None, 174 headers_redact=None, 175 exception_cb=None): 176 md_url = url_helper.combine_url(metadata_address, api_version, tree) 177 caller = functools.partial( 178 url_helper.read_file_or_url, ssl_details=ssl_details, 179 timeout=timeout, retries=retries, headers_cb=headers_cb, 180 headers_redact=headers_redact, 181 exception_cb=exception_cb) 182 183 def mcaller(url): 184 return caller(url).contents 185 186 try: 187 response = caller(md_url) 188 materializer = MetadataMaterializer(response.contents, 189 md_url, mcaller, 190 leaf_decoder=leaf_decoder) 191 md = materializer.materialize() 192 if not isinstance(md, (dict)): 193 md = {} 194 return md 195 except Exception: 196 util.logexc(LOG, "Failed fetching %s from url %s", tree, md_url) 197 return {} 198 199 200def get_instance_metadata(api_version='latest', 201 metadata_address='http://169.254.169.254', 202 ssl_details=None, timeout=5, retries=5, 203 leaf_decoder=None, headers_cb=None, 204 headers_redact=None, 205 exception_cb=None): 206 # Note, 'meta-data' explicitly has trailing /. 207 # this is required for CloudStack (LP: #1356855) 208 return _get_instance_metadata(tree='meta-data/', api_version=api_version, 209 metadata_address=metadata_address, 210 ssl_details=ssl_details, timeout=timeout, 211 retries=retries, leaf_decoder=leaf_decoder, 212 headers_redact=headers_redact, 213 headers_cb=headers_cb, 214 exception_cb=exception_cb) 215 216 217def get_instance_identity(api_version='latest', 218 metadata_address='http://169.254.169.254', 219 ssl_details=None, timeout=5, retries=5, 220 leaf_decoder=None, headers_cb=None, 221 headers_redact=None, 222 exception_cb=None): 223 return _get_instance_metadata(tree='dynamic/instance-identity', 224 api_version=api_version, 225 metadata_address=metadata_address, 226 ssl_details=ssl_details, timeout=timeout, 227 retries=retries, leaf_decoder=leaf_decoder, 228 headers_redact=headers_redact, 229 headers_cb=headers_cb, 230 exception_cb=exception_cb) 231# vi: ts=4 expandtab 232