~ubuntu-branches/ubuntu/precise/cloud-init/precise-updates

« back to all changes in this revision

Viewing changes to cloudinit/DataSourceMaaS.py

  • Committer: Package Import Robot
  • Author(s): Scott Moser
  • Date: 2012-03-09 16:37:01 UTC
  • mfrom: (174.1.9 precise)
  • Revision ID: package-import@ubuntu.com-20120309163701-w1ntss7kxz8jsy1p
Tags: 0.6.3~bzr539-0ubuntu1
* New upstream snapshot.
  * add ability to configure Acquire::http::Pipeline-Depth via
    cloud-config setting 'apt_pipelining' (LP: #942061)
  * if cloud-config settings removed default certificats
    (remove-defaults), then seed package ca-certificates to not
    install new ones on upgrade.
  * run-parts now uses internal implementation rather than
    separate command.
  * add MaaS datasource (LP: #942061)
* debian/cloud-init.postinst: address population of apt_pipeline 
  setting on installation.
* debian/cloud-init.postinst: support configuring cloud-init
  maas datasource via preseed values cloud-init/maas-metadata-url and
  cloud-init/maas-credentials. (LP: #942061)
* debian/cloud-init.postinst: support for (LP: #924375)

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# vi: ts=4 expandtab
 
2
#
 
3
#    Copyright (C) 2012 Canonical Ltd.
 
4
#
 
5
#    Author: Scott Moser <scott.moser@canonical.com>
 
6
#
 
7
#    This program is free software: you can redistribute it and/or modify
 
8
#    it under the terms of the GNU General Public License version 3, as
 
9
#    published by the Free Software Foundation.
 
10
#
 
11
#    This program is distributed in the hope that it will be useful,
 
12
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
 
13
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
14
#    GNU General Public License for more details.
 
15
#
 
16
#    You should have received a copy of the GNU General Public License
 
17
#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
18
 
 
19
import cloudinit.DataSource as DataSource
 
20
 
 
21
from cloudinit import seeddir as base_seeddir
 
22
from cloudinit import log
 
23
import cloudinit.util as util
 
24
import errno
 
25
import oauth.oauth as oauth
 
26
import os.path
 
27
import urllib2
 
28
import time
 
29
 
 
30
 
 
31
MD_VERSION = "2012-03-01"
 
32
 
 
33
 
 
34
class DataSourceMaaS(DataSource.DataSource):
 
35
    """
 
36
    DataSourceMaaS reads instance information from MaaS.
 
37
    Given a config metadata_url, and oauth tokens, it expects to find
 
38
    files under the root named:
 
39
      instance-id
 
40
      user-data
 
41
      hostname
 
42
    """
 
43
    seeddir = base_seeddir + '/maas'
 
44
    baseurl = None
 
45
 
 
46
    def __str__(self):
 
47
        return("DataSourceMaaS[%s]" % self.baseurl)
 
48
 
 
49
    def get_data(self):
 
50
        mcfg = self.ds_cfg
 
51
 
 
52
        try:
 
53
            (userdata, metadata) = read_maas_seed_dir(self.seeddir)
 
54
            self.userdata_raw = userdata
 
55
            self.metadata = metadata
 
56
            self.baseurl = self.seeddir
 
57
            return True
 
58
        except MaasSeedDirNone:
 
59
            pass
 
60
        except MaasSeedDirMalformed as exc:
 
61
            log.warn("%s was malformed: %s\n" % (self.seeddir, exc))
 
62
            raise
 
63
 
 
64
        try:
 
65
            # if there is no metadata_url, then we're not configured
 
66
            url = mcfg.get('metadata_url', None)
 
67
            if url == None:
 
68
                return False
 
69
 
 
70
            if not self.wait_for_metadata_service(url):
 
71
                return False
 
72
 
 
73
            self.baseurl = url
 
74
 
 
75
            (userdata, metadata) = read_maas_seed_url(self.baseurl,
 
76
                self.md_headers)
 
77
            self.userdata_raw = userdata
 
78
            self.metadata = metadata
 
79
            return True
 
80
        except Exception:
 
81
            util.logexc(log)
 
82
            return False
 
83
 
 
84
    def md_headers(self, url):
 
85
        mcfg = self.ds_cfg
 
86
 
 
87
        # if we are missing token_key, token_secret or consumer_key
 
88
        # then just do non-authed requests
 
89
        for required in ('token_key', 'token_secret', 'consumer_key'):
 
90
            if required not in mcfg:
 
91
                return({})
 
92
 
 
93
        consumer_secret = mcfg.get('consumer_secret', "")
 
94
 
 
95
        return(oauth_headers(url=url, consumer_key=mcfg['consumer_key'],
 
96
            token_key=mcfg['token_key'], token_secret=mcfg['token_secret'],
 
97
            consumer_secret=consumer_secret))
 
98
 
 
99
    def wait_for_metadata_service(self, url):
 
100
        mcfg = self.ds_cfg
 
101
 
 
102
        max_wait = 120
 
103
        try:
 
104
            max_wait = int(mcfg.get("max_wait", max_wait))
 
105
        except Exception:
 
106
            util.logexc(log)
 
107
            log.warn("Failed to get max wait. using %s" % max_wait)
 
108
 
 
109
        if max_wait == 0:
 
110
            return False
 
111
 
 
112
        timeout = 50
 
113
        try:
 
114
            timeout = int(mcfg.get("timeout", timeout))
 
115
        except Exception:
 
116
            util.logexc(log)
 
117
            log.warn("Failed to get timeout, using %s" % timeout)
 
118
 
 
119
        starttime = time.time()
 
120
        check_url = "%s/%s/meta-data/instance-id" % (url, MD_VERSION)
 
121
        url = util.wait_for_url(urls=[check_url], max_wait=max_wait,
 
122
            timeout=timeout, status_cb=log.warn,
 
123
            headers_cb=self.md_headers)
 
124
 
 
125
        if url:
 
126
            log.debug("Using metadata source: '%s'" % url)
 
127
        else:
 
128
            log.critical("giving up on md after %i seconds\n" %
 
129
                         int(time.time() - starttime))
 
130
 
 
131
        return (bool(url))
 
132
 
 
133
 
 
134
def read_maas_seed_dir(seed_d):
 
135
    """
 
136
    Return user-data and metadata for a maas seed dir in seed_d.
 
137
    Expected format of seed_d are the following files:
 
138
      * instance-id
 
139
      * local-hostname
 
140
      * user-data
 
141
    """
 
142
    files = ('local-hostname', 'instance-id', 'user-data')
 
143
    md = {}
 
144
 
 
145
    if not os.path.isdir(seed_d):
 
146
        raise MaasSeedDirNone("%s: not a directory")
 
147
 
 
148
    for fname in files:
 
149
        try:
 
150
            with open(os.path.join(seed_d, fname)) as fp:
 
151
                md[fname] = fp.read()
 
152
                fp.close()
 
153
        except IOError as e:
 
154
            if e.errno != errno.ENOENT:
 
155
                raise
 
156
 
 
157
    return(check_seed_contents(md, seed_d))
 
158
 
 
159
 
 
160
def read_maas_seed_url(seed_url, header_cb=None, timeout=None,
 
161
    version=MD_VERSION):
 
162
    """
 
163
    Read the maas datasource at seed_url.
 
164
    header_cb is a method that should return a headers dictionary that will
 
165
    be given to urllib2.Request()
 
166
 
 
167
    Expected format of seed_url is are the following files:
 
168
      * <seed_url>/<version>/instance-id
 
169
      * <seed_url>/<version>/local-hostname
 
170
      * <seed_url>/<version>/user-data
 
171
    """
 
172
    files = ('meta-data/local-hostname', 'meta-data/instance-id', 'user-data')
 
173
 
 
174
    base_url = "%s/%s" % (seed_url, version)
 
175
    md = {}
 
176
    for fname in files:
 
177
        url = "%s/%s" % (base_url, fname)
 
178
        if header_cb:
 
179
            headers = header_cb(url)
 
180
        else:
 
181
            headers = {}
 
182
 
 
183
        try:
 
184
            req = urllib2.Request(url, data=None, headers=headers)
 
185
            resp = urllib2.urlopen(req, timeout=timeout)
 
186
            md[os.path.basename(fname)] = resp.read()
 
187
        except urllib2.HTTPError as e:
 
188
            if e.code != 404:
 
189
                raise
 
190
 
 
191
    return(check_seed_contents(md, seed_url))
 
192
 
 
193
 
 
194
def check_seed_contents(content, seed):
 
195
    """Validate if content is Is the content a dict that is valid as a
 
196
       return for a datasource.
 
197
       Either return a (userdata, metadata) tuple or
 
198
       Raise MaasSeedDirMalformed or MaasSeedDirNone
 
199
    """
 
200
    md_required = ('instance-id', 'local-hostname')
 
201
    found = content.keys()
 
202
 
 
203
    if len(content) == 0:
 
204
        raise MaasSeedDirNone("%s: no data files found" % seed)
 
205
 
 
206
    missing = [k for k in md_required if k not in found]
 
207
    if len(missing):
 
208
        raise MaasSeedDirMalformed("%s: missing files %s" % (seed, missing))
 
209
 
 
210
    userdata = content.get('user-data', "")
 
211
    md = {}
 
212
    for (key, val) in content.iteritems():
 
213
        if key == 'user-data':
 
214
            continue
 
215
        md[key] = val
 
216
 
 
217
    return(userdata, md)
 
218
 
 
219
 
 
220
def oauth_headers(url, consumer_key, token_key, token_secret, consumer_secret):
 
221
    consumer = oauth.OAuthConsumer(consumer_key, consumer_secret)
 
222
    token = oauth.OAuthToken(token_key, token_secret)
 
223
    params = {
 
224
        'oauth_version': "1.0",
 
225
        'oauth_nonce': oauth.generate_nonce(),
 
226
        'oauth_timestamp': int(time.time()),
 
227
        'oauth_token': token.key,
 
228
        'oauth_consumer_key': consumer.key,
 
229
    }
 
230
    req = oauth.OAuthRequest(http_url=url, parameters=params)
 
231
    req.sign_request(oauth.OAuthSignatureMethod_PLAINTEXT(),
 
232
        consumer, token)
 
233
    return(req.to_header())
 
234
 
 
235
 
 
236
class MaasSeedDirNone(Exception):
 
237
    pass
 
238
 
 
239
 
 
240
class MaasSeedDirMalformed(Exception):
 
241
    pass
 
242
 
 
243
 
 
244
datasources = [
 
245
  (DataSourceMaaS, (DataSource.DEP_FILESYSTEM, DataSource.DEP_NETWORK)),
 
246
]
 
247
 
 
248
 
 
249
# return a list of data sources that match this set of dependencies
 
250
def get_datasource_list(depends):
 
251
    return(DataSource.list_from_depends(depends, datasources))
 
252
 
 
253
 
 
254
if __name__ == "__main__":
 
255
    def main():
 
256
        """
 
257
        Call with single argument of directory or http or https url.
 
258
        If url is given additional arguments are allowed, which will be
 
259
        interpreted as consumer_key, token_key, token_secret, consumer_secret
 
260
        """
 
261
        import argparse
 
262
        import pprint
 
263
 
 
264
        parser = argparse.ArgumentParser(description='Interact with Maas DS')
 
265
        parser.add_argument("--config", metavar="file",
 
266
            help="specify DS config file", default=None)
 
267
        parser.add_argument("--ckey", metavar="key",
 
268
            help="the consumer key to auth with", default=None)
 
269
        parser.add_argument("--tkey", metavar="key",
 
270
            help="the token key to auth with", default=None)
 
271
        parser.add_argument("--csec", metavar="secret",
 
272
            help="the consumer secret (likely '')", default="")
 
273
        parser.add_argument("--tsec", metavar="secret",
 
274
            help="the token secret to auth with", default=None)
 
275
        parser.add_argument("--apiver", metavar="version",
 
276
            help="the apiver to use ("" can be used)", default=MD_VERSION)
 
277
 
 
278
        subcmds = parser.add_subparsers(title="subcommands", dest="subcmd")
 
279
        subcmds.add_parser('crawl', help="crawl the datasource")
 
280
        subcmds.add_parser('get', help="do a single GET of provided url")
 
281
        subcmds.add_parser('check-seed', help="read andn verify seed at url")
 
282
 
 
283
        parser.add_argument("url", help="the data source to query")
 
284
 
 
285
        args = parser.parse_args()
 
286
 
 
287
        creds = {'consumer_key': args.ckey, 'token_key': args.tkey,
 
288
            'token_secret': args.tsec, 'consumer_secret': args.csec}
 
289
 
 
290
        if args.config:
 
291
            import yaml
 
292
            with open(args.config) as fp:
 
293
                cfg = yaml.load(fp)
 
294
            if 'datasource' in cfg:
 
295
                cfg = cfg['datasource']['MaaS']
 
296
            for key in creds.keys():
 
297
                if key in cfg and creds[key] == None:
 
298
                    creds[key] = cfg[key]
 
299
 
 
300
        def geturl(url, headers_cb):
 
301
            req = urllib2.Request(url, data=None, headers=headers_cb(url))
 
302
            return(urllib2.urlopen(req).read())
 
303
 
 
304
        def printurl(url, headers_cb):
 
305
            print "== %s ==\n%s\n" % (url, geturl(url, headers_cb))
 
306
 
 
307
        def crawl(url, headers_cb=None):
 
308
            if url.endswith("/"):
 
309
                for line in geturl(url, headers_cb).splitlines():
 
310
                    if line.endswith("/"):
 
311
                        crawl("%s%s" % (url, line), headers_cb)
 
312
                    else:
 
313
                        printurl("%s%s" % (url, line), headers_cb)
 
314
            else:
 
315
                printurl(url, headers_cb)
 
316
 
 
317
        def my_headers(url):
 
318
            headers = {}
 
319
            if creds.get('consumer_key', None) != None:
 
320
                headers = oauth_headers(url, **creds)
 
321
            return headers
 
322
 
 
323
        if args.subcmd == "check-seed":
 
324
            if args.url.startswith("http"):
 
325
                (userdata, metadata) = read_maas_seed_url(args.url,
 
326
                    header_cb=my_headers, version=args.apiver)
 
327
            else:
 
328
                (userdata, metadata) = read_maas_seed_url(args.url)
 
329
            print "=== userdata ==="
 
330
            print userdata
 
331
            print "=== metadata ==="
 
332
            pprint.pprint(metadata)
 
333
 
 
334
        elif args.subcmd == "get":
 
335
            printurl(args.url, my_headers)
 
336
 
 
337
        elif args.subcmd == "crawl":
 
338
            if not args.url.endswith("/"):
 
339
                args.url = "%s/" % args.url
 
340
            crawl(args.url, my_headers)
 
341
 
 
342
    main()