This repository has been archived by the owner on Mar 24, 2022. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 171
/
Copy pathec2_info.py
197 lines (164 loc) · 5.9 KB
/
ec2_info.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
# -*- coding: utf-8 -*-
"""
Get some grains information that is only available in Amazon AWS
Author: Erik Günther, J C Lawrence <[email protected]>, Mark McGuire
"""
from __future__ import absolute_import
import logging
import socket
import json
try:
from http.client import HTTPConnection, BadStatusLine
except ImportError:
from salt.ext.six.moves.http_client import HTTPConnection, BadStatusLine
# Set up logging
LOG = logging.getLogger(__name__)
def _call_aws(url):
"""
Call AWS via httplib. Require correct path.
Host: 169.254.169.254
"""
conn = HTTPConnection("169.254.169.254", 80, timeout=1)
conn.request('GET', url)
return conn.getresponse()
def _get_ec2_hostinfo(path=""):
"""
Recursive function that walks the EC2 metadata available to each minion.
:param path: URI fragment to append to /latest/meta-data/
Returns a nested dictionary containing all the EC2 metadata. All keys
are converted from dash case to snake case.
"""
resp = _call_aws("/latest/meta-data/{0}".format(path))
resp_data = resp.read().decode('utf-8').strip()
d = {}
for line in resp_data.split("\n"):
if path == "public-keys/":
line = line.split("=")[0] + "/"
if path == "instance-id/":
return {'instance-id': line}
if line[-1] != "/":
call_response = _call_aws("/latest/meta-data/{0}".format(path + line))
call_response_data = call_response.read().decode('utf-8')
# avoid setting empty grain
if call_response_data == '':
d[line] = None
elif call_response_data is not None:
line = _dash_to_snake_case(line)
try:
data = json.loads(call_response_data)
if isinstance(data, dict):
data = _snake_caseify_dict(data)
d[line] = data
except ValueError:
if "\n" in call_response_data:
d[line] = []
for dline in call_response_data.split("\n"):
d[line].append(dline)
else:
d[line] = call_response_data
else:
return line
else:
d[_dash_to_snake_case(line[:-1])] = _get_ec2_hostinfo(path + line)
return d
def _camel_to_snake_case(s):
return s[0].lower() + "".join((("_" + x.lower()) if x.isupper() else x) for x in s[1:])
def _dash_to_snake_case(s):
return s.replace("-", "_")
def _snake_caseify_dict(d):
nd = {}
for k, v in d.items():
nd[_camel_to_snake_case(k)] = v
return nd
def _get_ec2_additional():
"""
Recursive call in _get_ec2_hostinfo() does not retrieve some of
the hosts information like region, availability zone or
architecture.
"""
response = _call_aws("/latest/dynamic/instance-identity/document")
# _call_aws returns None for all non '200' reponses,
# catching that here would rule out AWS resource
if response.status == 200:
response_data = response.read()
try:
data = json.loads(response_data.decode('utf-8'))
except ValueError as e:
data = {}
data = _snake_caseify_dict(data)
data.update({'instance_identity': {'document': response_data}})
return data
else:
raise BadStatusLine("Could not read EC2 metadata")
def _get_ec2_user_data():
"""
Recursive call in _get_ec2_hostinfo() does not retrieve user-data.
"""
response = _call_aws("/latest/user-data")
# _call_aws returns None for all non '200' reponses,
# catching that here would rule out AWS resource
if response.status == 200:
response_data = response.read().decode('utf-8')
try:
return json.loads(response_data)
except ValueError:
return response_data
elif response.status == 404:
return ''
else:
raise BadStatusLine("Could not read EC2 user-data")
def _get_instance_identity():
"""
Fill in the details from the instance identity info.
"""
result = {}
response = _call_aws('/latest/dynamic/instance-identity/')
data = response.read().decode('utf-8')
for i in data.split('\n'):
if not i or i == 'document': # document saved in _get_ec2_additional
continue
response = _call_aws('/latest/dynamic/instance-identity/{0}'.format(i))
result[i] = response.read()
return result
def ec2_info():
"""
Collect all ec2 grains into the 'ec2' key.
"""
try:
grains = _get_ec2_additional()
grains.update({'user-data': _get_ec2_user_data()})
grains.update(_get_ec2_hostinfo())
grains['instance_identity'].update(_get_instance_identity())
return {'ec2': grains}
except BadStatusLine as error:
LOG.debug(error)
return {}
except socket.timeout as serr:
LOG.info("Could not read EC2 data (timeout): {0}".format(serr))
return {}
except socket.error as serr:
LOG.info("Could not read EC2 data (error): {0}".format(serr))
return {}
except IOError as serr:
LOG.info("Could not read EC2 data (IOError): {0}".format(serr))
return {}
def ec2_instance_id():
"""
Set the top-level grain 'instance-id' per the grain expected
by pillar-ec2.
"""
try:
instance_id = list(_get_ec2_hostinfo("instance-id/").values())[0]
return {'instance-id': instance_id}
except BadStatusLine as error:
LOG.debug(error)
return {}
except socket.timeout as serr:
LOG.info("Could not read EC2 data (timeout): {0}".format(serr))
return {}
except socket.error as serr:
LOG.info("Could not read EC2 data (error): {0}".format(serr))
return {}
except IOError as serr:
LOG.info("Could not read EC2 data (IOError): {0}".format(serr))
return {}