8
from contextlib import contextmanager
9
from xml.etree import ElementTree
11
from cloudinit import util
14
LOG = logging.getLogger(__name__)
20
os.chdir(os.path.expanduser(newdir))
27
class AzureEndpointHttpClient(object):
30
'x-ms-agent-name': 'WALinuxAgent',
31
'x-ms-version': '2012-11-30',
34
def __init__(self, certificate):
35
self.extra_secure_headers = {
36
"x-ms-cipher-name": "DES_EDE3_CBC",
37
"x-ms-guest-agent-public-x509-cert": certificate,
40
def get(self, url, secure=False):
41
headers = self.headers
43
headers = self.headers.copy()
44
headers.update(self.extra_secure_headers)
45
return util.read_file_or_url(url, headers=headers)
47
def post(self, url, data=None, extra_headers=None):
48
headers = self.headers
49
if extra_headers is not None:
50
headers = self.headers.copy()
51
headers.update(extra_headers)
52
return util.read_file_or_url(url, data=data, headers=headers)
55
class GoalState(object):
57
def __init__(self, xml, http_client):
58
self.http_client = http_client
59
self.root = ElementTree.fromstring(xml)
60
self._certificates_xml = None
62
def _text_from_xpath(self, xpath):
63
element = self.root.find(xpath)
64
if element is not None:
69
def container_id(self):
70
return self._text_from_xpath('./Container/ContainerId')
73
def incarnation(self):
74
return self._text_from_xpath('./Incarnation')
77
def instance_id(self):
78
return self._text_from_xpath(
79
'./Container/RoleInstanceList/RoleInstance/InstanceId')
82
def shared_config_xml(self):
83
url = self._text_from_xpath('./Container/RoleInstanceList/RoleInstance'
84
'/Configuration/SharedConfig')
85
return self.http_client.get(url).contents
88
def certificates_xml(self):
89
if self._certificates_xml is None:
90
url = self._text_from_xpath(
91
'./Container/RoleInstanceList/RoleInstance'
92
'/Configuration/Certificates')
94
self._certificates_xml = self.http_client.get(
95
url, secure=True).contents
96
return self._certificates_xml
99
class OpenSSLManager(object):
101
certificate_names = {
102
'private_key': 'TransportPrivate.pem',
103
'certificate': 'TransportCert.pem',
107
self.tmpdir = tempfile.mkdtemp()
108
self.certificate = None
109
self.generate_certificate()
112
util.del_dir(self.tmpdir)
114
def generate_certificate(self):
115
LOG.debug('Generating certificate for communication with fabric...')
116
if self.certificate is not None:
117
LOG.debug('Certificate already generated.')
119
with cd(self.tmpdir):
121
'openssl', 'req', '-x509', '-nodes', '-subj',
122
'/CN=LinuxTransport', '-days', '32768', '-newkey', 'rsa:2048',
123
'-keyout', self.certificate_names['private_key'],
124
'-out', self.certificate_names['certificate'],
127
for line in open(self.certificate_names['certificate']):
128
if "CERTIFICATE" not in line:
129
certificate += line.rstrip()
130
self.certificate = certificate
131
LOG.debug('New certificate generated.')
133
def parse_certificates(self, certificates_xml):
134
tag = ElementTree.fromstring(certificates_xml).find(
136
certificates_content = tag.text
138
b'MIME-Version: 1.0',
139
b'Content-Disposition: attachment; filename="Certificates.p7m"',
140
b'Content-Type: application/x-pkcs7-mime; name="Certificates.p7m"',
141
b'Content-Transfer-Encoding: base64',
143
certificates_content.encode('utf-8'),
145
with cd(self.tmpdir):
146
with open('Certificates.p7m', 'wb') as f:
147
f.write(b'\n'.join(lines))
149
'openssl cms -decrypt -in Certificates.p7m -inkey'
150
' {private_key} -recip {certificate} | openssl pkcs12 -nodes'
151
' -password pass:'.format(**self.certificate_names),
153
private_keys, certificates = [], []
155
for line in out.splitlines():
157
if re.match(r'[-]+END .*?KEY[-]+$', line):
158
private_keys.append('\n'.join(current))
160
elif re.match(r'[-]+END .*?CERTIFICATE[-]+$', line):
161
certificates.append('\n'.join(current))
164
for certificate in certificates:
165
with cd(self.tmpdir):
166
public_key, _ = util.subp(
167
'openssl x509 -noout -pubkey |'
168
'ssh-keygen -i -m PKCS8 -f /dev/stdin',
171
keys.append(public_key)
175
def iid_from_shared_config_content(content):
178
<?xml version="1.0" encoding="utf-8"?>
179
<SharedConfig version="1.0.0.0" goalStateIncarnation="1">
180
<Deployment name="INSTANCE_ID" guid="{...}" incarnation="0">
181
<Service name="..." guid="{00000000-0000-0000-0000-000000000000}"/>
183
root = ElementTree.fromstring(content)
184
depnode = root.find('Deployment')
185
return depnode.get('name')
188
class WALinuxAgentShim(object):
190
REPORT_READY_XML_TEMPLATE = '\n'.join([
191
'<?xml version="1.0" encoding="utf-8"?>',
192
'<Health xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
193
' xmlns:xsd="http://www.w3.org/2001/XMLSchema">',
194
' <GoalStateIncarnation>{incarnation}</GoalStateIncarnation>',
196
' <ContainerId>{container_id}</ContainerId>',
197
' <RoleInstanceList>',
199
' <InstanceId>{instance_id}</InstanceId>',
201
' <State>Ready</State>',
204
' </RoleInstanceList>',
209
LOG.debug('WALinuxAgentShim instantiated...')
210
self.endpoint = self.find_endpoint()
211
self.openssl_manager = None
215
if self.openssl_manager is not None:
216
self.openssl_manager.clean_up()
220
LOG.debug('Finding Azure endpoint...')
221
content = util.load_file('/var/lib/dhcp/dhclient.eth0.leases')
223
for line in content.splitlines():
224
if 'unknown-245' in line:
225
value = line.strip(' ').split(' ', 2)[-1].strip(';\n"')
227
raise Exception('No endpoint found in DHCP config.')
230
for hex_pair in value.split(':'):
231
if len(hex_pair) == 1:
232
hex_pair = '0' + hex_pair
233
hex_string += hex_pair
234
value = struct.pack('>L', int(hex_string.replace(':', ''), 16))
236
value = value.encode('utf-8')
237
endpoint_ip_address = socket.inet_ntoa(value)
238
LOG.debug('Azure endpoint found at %s', endpoint_ip_address)
239
return endpoint_ip_address
241
def register_with_azure_and_fetch_data(self):
242
self.openssl_manager = OpenSSLManager()
243
http_client = AzureEndpointHttpClient(self.openssl_manager.certificate)
244
LOG.info('Registering with Azure...')
248
response = http_client.get(
249
'http://{0}/machine/?comp=goalstate'.format(self.endpoint))
252
time.sleep(attempts + 1)
258
LOG.debug('Successfully fetched GoalState XML.')
259
goal_state = GoalState(response.contents, http_client)
261
if goal_state.certificates_xml is not None:
262
LOG.debug('Certificate XML found; parsing out public keys.')
263
public_keys = self.openssl_manager.parse_certificates(
264
goal_state.certificates_xml)
266
'instance-id': iid_from_shared_config_content(
267
goal_state.shared_config_xml),
268
'public-keys': public_keys,
270
self._report_ready(goal_state, http_client)
273
def _report_ready(self, goal_state, http_client):
274
LOG.debug('Reporting ready to Azure fabric.')
275
document = self.REPORT_READY_XML_TEMPLATE.format(
276
incarnation=goal_state.incarnation,
277
container_id=goal_state.container_id,
278
instance_id=goal_state.instance_id,
281
"http://{0}/machine?comp=health".format(self.endpoint),
283
extra_headers={'Content-Type': 'text/xml; charset=utf-8'},
285
LOG.info('Reported ready to Azure fabric.')
288
def get_metadata_from_fabric():
289
shim = WALinuxAgentShim()
291
return shim.register_with_azure_and_fetch_data()