~oddbloke/cloud-init/lp1411582

« back to all changes in this revision

Viewing changes to cloudinit/sources/helpers/azure.py

  • Committer: Scott Moser
  • Date: 2015-05-15 20:16:14 UTC
  • mfrom: (1101.1.13 master2)
  • Revision ID: smoser@ubuntu.com-20150515201614-pz8p708eohad0gqj
Azure: remove dependency on walinux-agent

This takes away our dependency on walinux-agent, by providing a builtin
path for doing cloud-init had delegated to it.

Currently the default is to still use the old path, but adding this code
in will allow us to move to the new code path with more confidence.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
import logging
 
2
import os
 
3
import re
 
4
import socket
 
5
import struct
 
6
import tempfile
 
7
import time
 
8
from contextlib import contextmanager
 
9
from xml.etree import ElementTree
 
10
 
 
11
from cloudinit import util
 
12
 
 
13
 
 
14
LOG = logging.getLogger(__name__)
 
15
 
 
16
 
 
17
@contextmanager
 
18
def cd(newdir):
 
19
    prevdir = os.getcwd()
 
20
    os.chdir(os.path.expanduser(newdir))
 
21
    try:
 
22
        yield
 
23
    finally:
 
24
        os.chdir(prevdir)
 
25
 
 
26
 
 
27
class AzureEndpointHttpClient(object):
 
28
 
 
29
    headers = {
 
30
        'x-ms-agent-name': 'WALinuxAgent',
 
31
        'x-ms-version': '2012-11-30',
 
32
    }
 
33
 
 
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,
 
38
        }
 
39
 
 
40
    def get(self, url, secure=False):
 
41
        headers = self.headers
 
42
        if secure:
 
43
            headers = self.headers.copy()
 
44
            headers.update(self.extra_secure_headers)
 
45
        return util.read_file_or_url(url, headers=headers)
 
46
 
 
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)
 
53
 
 
54
 
 
55
class GoalState(object):
 
56
 
 
57
    def __init__(self, xml, http_client):
 
58
        self.http_client = http_client
 
59
        self.root = ElementTree.fromstring(xml)
 
60
        self._certificates_xml = None
 
61
 
 
62
    def _text_from_xpath(self, xpath):
 
63
        element = self.root.find(xpath)
 
64
        if element is not None:
 
65
            return element.text
 
66
        return None
 
67
 
 
68
    @property
 
69
    def container_id(self):
 
70
        return self._text_from_xpath('./Container/ContainerId')
 
71
 
 
72
    @property
 
73
    def incarnation(self):
 
74
        return self._text_from_xpath('./Incarnation')
 
75
 
 
76
    @property
 
77
    def instance_id(self):
 
78
        return self._text_from_xpath(
 
79
            './Container/RoleInstanceList/RoleInstance/InstanceId')
 
80
 
 
81
    @property
 
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
 
86
 
 
87
    @property
 
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')
 
93
            if url is not None:
 
94
                self._certificates_xml = self.http_client.get(
 
95
                    url, secure=True).contents
 
96
        return self._certificates_xml
 
97
 
 
98
 
 
99
class OpenSSLManager(object):
 
100
 
 
101
    certificate_names = {
 
102
        'private_key': 'TransportPrivate.pem',
 
103
        'certificate': 'TransportCert.pem',
 
104
    }
 
105
 
 
106
    def __init__(self):
 
107
        self.tmpdir = tempfile.mkdtemp()
 
108
        self.certificate = None
 
109
        self.generate_certificate()
 
110
 
 
111
    def clean_up(self):
 
112
        util.del_dir(self.tmpdir)
 
113
 
 
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.')
 
118
            return
 
119
        with cd(self.tmpdir):
 
120
            util.subp([
 
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'],
 
125
            ])
 
126
            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.')
 
132
 
 
133
    def parse_certificates(self, certificates_xml):
 
134
        tag = ElementTree.fromstring(certificates_xml).find(
 
135
            './/Data')
 
136
        certificates_content = tag.text
 
137
        lines = [
 
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',
 
142
            b'',
 
143
            certificates_content.encode('utf-8'),
 
144
        ]
 
145
        with cd(self.tmpdir):
 
146
            with open('Certificates.p7m', 'wb') as f:
 
147
                f.write(b'\n'.join(lines))
 
148
            out, _ = util.subp(
 
149
                'openssl cms -decrypt -in Certificates.p7m -inkey'
 
150
                ' {private_key} -recip {certificate} | openssl pkcs12 -nodes'
 
151
                ' -password pass:'.format(**self.certificate_names),
 
152
                shell=True)
 
153
        private_keys, certificates = [], []
 
154
        current = []
 
155
        for line in out.splitlines():
 
156
            current.append(line)
 
157
            if re.match(r'[-]+END .*?KEY[-]+$', line):
 
158
                private_keys.append('\n'.join(current))
 
159
                current = []
 
160
            elif re.match(r'[-]+END .*?CERTIFICATE[-]+$', line):
 
161
                certificates.append('\n'.join(current))
 
162
                current = []
 
163
        keys = []
 
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',
 
169
                    data=certificate,
 
170
                    shell=True)
 
171
            keys.append(public_key)
 
172
        return keys
 
173
 
 
174
 
 
175
def iid_from_shared_config_content(content):
 
176
    """
 
177
    find INSTANCE_ID in:
 
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}"/>
 
182
    """
 
183
    root = ElementTree.fromstring(content)
 
184
    depnode = root.find('Deployment')
 
185
    return depnode.get('name')
 
186
 
 
187
 
 
188
class WALinuxAgentShim(object):
 
189
 
 
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>',
 
195
        '  <Container>',
 
196
        '    <ContainerId>{container_id}</ContainerId>',
 
197
        '    <RoleInstanceList>',
 
198
        '      <Role>',
 
199
        '        <InstanceId>{instance_id}</InstanceId>',
 
200
        '        <Health>',
 
201
        '          <State>Ready</State>',
 
202
        '        </Health>',
 
203
        '      </Role>',
 
204
        '    </RoleInstanceList>',
 
205
        '  </Container>',
 
206
        '</Health>'])
 
207
 
 
208
    def __init__(self):
 
209
        LOG.debug('WALinuxAgentShim instantiated...')
 
210
        self.endpoint = self.find_endpoint()
 
211
        self.openssl_manager = None
 
212
        self.values = {}
 
213
 
 
214
    def clean_up(self):
 
215
        if self.openssl_manager is not None:
 
216
            self.openssl_manager.clean_up()
 
217
 
 
218
    @staticmethod
 
219
    def find_endpoint():
 
220
        LOG.debug('Finding Azure endpoint...')
 
221
        content = util.load_file('/var/lib/dhcp/dhclient.eth0.leases')
 
222
        value = None
 
223
        for line in content.splitlines():
 
224
            if 'unknown-245' in line:
 
225
                value = line.strip(' ').split(' ', 2)[-1].strip(';\n"')
 
226
        if value is None:
 
227
            raise Exception('No endpoint found in DHCP config.')
 
228
        if ':' in value:
 
229
            hex_string = ''
 
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))
 
235
        else:
 
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
 
240
 
 
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...')
 
245
        attempts = 0
 
246
        while True:
 
247
            try:
 
248
                response = http_client.get(
 
249
                    'http://{0}/machine/?comp=goalstate'.format(self.endpoint))
 
250
            except Exception:
 
251
                if attempts < 10:
 
252
                    time.sleep(attempts + 1)
 
253
                else:
 
254
                    raise
 
255
            else:
 
256
                break
 
257
            attempts += 1
 
258
        LOG.debug('Successfully fetched GoalState XML.')
 
259
        goal_state = GoalState(response.contents, http_client)
 
260
        public_keys = []
 
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)
 
265
        data = {
 
266
            'instance-id': iid_from_shared_config_content(
 
267
                goal_state.shared_config_xml),
 
268
            'public-keys': public_keys,
 
269
        }
 
270
        self._report_ready(goal_state, http_client)
 
271
        return data
 
272
 
 
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,
 
279
        )
 
280
        http_client.post(
 
281
            "http://{0}/machine?comp=health".format(self.endpoint),
 
282
            data=document,
 
283
            extra_headers={'Content-Type': 'text/xml; charset=utf-8'},
 
284
        )
 
285
        LOG.info('Reported ready to Azure fabric.')
 
286
 
 
287
 
 
288
def get_metadata_from_fabric():
 
289
    shim = WALinuxAgentShim()
 
290
    try:
 
291
        return shim.register_with_azure_and_fetch_data()
 
292
    finally:
 
293
        shim.clean_up()