~ubuntu-branches/debian/jessie/ceph/jessie

« back to all changes in this revision

Viewing changes to .pc/firefly-post-release.patch/src/pybind/ceph_rest_api.py

  • Committer: Package Import Robot
  • Author(s): Dmitry Smirnov
  • Date: 2014-07-18 02:33:39 UTC
  • mfrom: (1.2.17)
  • Revision ID: package-import@ubuntu.com-20140718023339-03jdyr6dxl2hx00y
Tags: 0.80.4-1
* New upstream release [July 2014].
* New patches:
  + rbdmap1-mount.patch
  + rbdmap2-hooks.patch
  + rbdmap3-lazyumount.patch
  + bug-8821.patch
* radosgw: removed unused lintian overrides.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# vim: ts=4 sw=4 smarttab expandtab
2
 
 
3
 
import errno
4
 
import json
5
 
import logging
6
 
import logging.handlers
7
 
import os
8
 
import rados
9
 
import textwrap
10
 
import xml.etree.ElementTree
11
 
import xml.sax.saxutils
12
 
 
13
 
import flask
14
 
from ceph_argparse import \
15
 
    ArgumentError, CephPgid, CephOsdName, CephChoices, CephPrefix, \
16
 
    concise_sig, descsort, parse_funcsig, parse_json_funcsigs, \
17
 
    validate, json_command
18
 
 
19
 
#
20
 
# Globals and defaults
21
 
#
22
 
 
23
 
DEFAULT_ADDR = '0.0.0.0'
24
 
DEFAULT_PORT = '5000'
25
 
DEFAULT_ID = 'restapi'
26
 
 
27
 
DEFAULT_BASEURL = '/api/v0.1'
28
 
DEFAULT_LOG_LEVEL = 'warning'
29
 
DEFAULT_LOGDIR = '/var/log/ceph'
30
 
# default client name will be 'client.<DEFAULT_ID>'
31
 
 
32
 
# 'app' must be global for decorators, etc.
33
 
APPNAME = '__main__'
34
 
app = flask.Flask(APPNAME)
35
 
 
36
 
LOGLEVELS = {
37
 
    'critical':logging.CRITICAL,
38
 
    'error':logging.ERROR,
39
 
    'warning':logging.WARNING,
40
 
    'info':logging.INFO,
41
 
    'debug':logging.DEBUG,
42
 
}
43
 
 
44
 
def find_up_osd(app):
45
 
    '''
46
 
    Find an up OSD.  Return the last one that's up.
47
 
    Returns id as an int.
48
 
    '''
49
 
    ret, outbuf, outs = json_command(app.ceph_cluster, prefix="osd dump",
50
 
                                     argdict=dict(format='json'))
51
 
    if ret:
52
 
        raise EnvironmentError(ret, 'Can\'t get osd dump output')
53
 
    try:
54
 
        osddump = json.loads(outbuf)
55
 
    except:
56
 
        raise EnvironmentError(errno.EINVAL, 'Invalid JSON back from osd dump')
57
 
    osds = [osd['osd'] for osd in osddump['osds'] if osd['up']]
58
 
    if not osds:
59
 
        return None
60
 
    return int(osds[-1])
61
 
 
62
 
 
63
 
METHOD_DICT = {'r':['GET'], 'w':['PUT', 'DELETE']}
64
 
 
65
 
def api_setup(app, conf, cluster, clientname, clientid, args):
66
 
    '''
67
 
    This is done globally, and cluster connection kept open for
68
 
    the lifetime of the daemon.  librados should assure that even
69
 
    if the cluster goes away and comes back, our connection remains.
70
 
 
71
 
    Initialize the running instance.  Open the cluster, get the command
72
 
    signatures, module, perms, and help; stuff them away in the app.ceph_urls
73
 
    dict.  Also save app.ceph_sigdict for help() handling.
74
 
    '''
75
 
    def get_command_descriptions(cluster, target=('mon','')):
76
 
        ret, outbuf, outs = json_command(cluster, target,
77
 
                                         prefix='get_command_descriptions',
78
 
                                         timeout=30)
79
 
        if ret:
80
 
            err = "Can't get command descriptions: {0}".format(outs)
81
 
            app.logger.error(err)
82
 
            raise EnvironmentError(ret, err)
83
 
 
84
 
        try:
85
 
            sigdict = parse_json_funcsigs(outbuf, 'rest')
86
 
        except Exception as e:
87
 
            err = "Can't parse command descriptions: {}".format(e)
88
 
            app.logger.error(err)
89
 
            raise EnvironmentError(err)
90
 
        return sigdict
91
 
 
92
 
    app.ceph_cluster = cluster or 'ceph'
93
 
    app.ceph_urls = {}
94
 
    app.ceph_sigdict = {}
95
 
    app.ceph_baseurl = ''
96
 
 
97
 
    conf = conf or ''
98
 
    cluster = cluster or 'ceph'
99
 
    clientid = clientid or DEFAULT_ID
100
 
    clientname = clientname or 'client.' + clientid
101
 
 
102
 
    app.ceph_cluster = rados.Rados(name=clientname, conffile=conf)
103
 
    app.ceph_cluster.conf_parse_argv(args)
104
 
    app.ceph_cluster.connect()
105
 
 
106
 
    app.ceph_baseurl = app.ceph_cluster.conf_get('restapi_base_url') \
107
 
         or DEFAULT_BASEURL
108
 
    if app.ceph_baseurl.endswith('/'):
109
 
        app.ceph_baseurl = app.ceph_baseurl[:-1]
110
 
    addr = app.ceph_cluster.conf_get('public_addr') or DEFAULT_ADDR
111
 
 
112
 
    # remove any nonce from the conf value
113
 
    addr = addr.split('/')[0]
114
 
    addr, port = addr.rsplit(':', 1)
115
 
    addr = addr or DEFAULT_ADDR
116
 
    port = port or DEFAULT_PORT
117
 
    port = int(port)
118
 
 
119
 
    loglevel = app.ceph_cluster.conf_get('restapi_log_level') \
120
 
        or DEFAULT_LOG_LEVEL
121
 
    # ceph has a default log file for daemons only; clients (like this)
122
 
    # default to "".  Override that for this particular client.
123
 
    logfile = app.ceph_cluster.conf_get('log_file')
124
 
    if not logfile:
125
 
        logfile = os.path.join(
126
 
            DEFAULT_LOGDIR,
127
 
            '{cluster}-{clientname}.{pid}.log'.format(
128
 
                cluster=cluster,
129
 
                clientname=clientname,
130
 
                pid=os.getpid()
131
 
            )
132
 
        )
133
 
    app.logger.addHandler(logging.handlers.WatchedFileHandler(logfile))
134
 
    app.logger.setLevel(LOGLEVELS[loglevel.lower()])
135
 
    for h in app.logger.handlers:
136
 
        h.setFormatter(logging.Formatter(
137
 
            '%(asctime)s %(name)s %(levelname)s: %(message)s'))
138
 
 
139
 
    app.ceph_sigdict = get_command_descriptions(app.ceph_cluster)
140
 
 
141
 
    osdid = find_up_osd(app)
142
 
    if osdid is not None:
143
 
        osd_sigdict = get_command_descriptions(app.ceph_cluster,
144
 
                                               target=('osd', int(osdid)))
145
 
 
146
 
        # shift osd_sigdict keys up to fit at the end of the mon's app.ceph_sigdict
147
 
        maxkey = sorted(app.ceph_sigdict.keys())[-1]
148
 
        maxkey = int(maxkey.replace('cmd', ''))
149
 
        osdkey = maxkey + 1
150
 
        for k, v in osd_sigdict.iteritems():
151
 
            newv = v
152
 
            newv['flavor'] = 'tell'
153
 
            globk = 'cmd' + str(osdkey)
154
 
            app.ceph_sigdict[globk] = newv
155
 
            osdkey += 1
156
 
 
157
 
    # app.ceph_sigdict maps "cmdNNN" to a dict containing:
158
 
    # 'sig', an array of argdescs
159
 
    # 'help', the helptext
160
 
    # 'module', the Ceph module this command relates to
161
 
    # 'perm', a 'rwx*' string representing required permissions, and also
162
 
    #    a hint as to whether this is a GET or POST/PUT operation
163
 
    # 'avail', a comma-separated list of strings of consumers that should
164
 
    #    display this command (filtered by parse_json_funcsigs() above)
165
 
    app.ceph_urls = {}
166
 
    for cmdnum, cmddict in app.ceph_sigdict.iteritems():
167
 
        cmdsig = cmddict['sig']
168
 
        flavor = cmddict.get('flavor', 'mon')
169
 
        url, params = generate_url_and_params(app, cmdsig, flavor)
170
 
        perm = cmddict['perm']
171
 
        for k in METHOD_DICT.iterkeys():
172
 
            if k in perm:
173
 
                methods = METHOD_DICT[k]
174
 
        urldict = {'paramsig':params,
175
 
                   'help':cmddict['help'],
176
 
                   'module':cmddict['module'],
177
 
                   'perm':perm,
178
 
                   'flavor':flavor,
179
 
                   'methods':methods,
180
 
                  }
181
 
 
182
 
        # app.ceph_urls contains a list of urldicts (usually only one long)
183
 
        if url not in app.ceph_urls:
184
 
            app.ceph_urls[url] = [urldict]
185
 
        else:
186
 
            # If more than one, need to make union of methods of all.
187
 
            # Method must be checked in handler
188
 
            methodset = set(methods)
189
 
            for old_urldict in app.ceph_urls[url]:
190
 
                methodset |= set(old_urldict['methods'])
191
 
            methods = list(methodset)
192
 
            app.ceph_urls[url].append(urldict)
193
 
 
194
 
        # add, or re-add, rule with all methods and urldicts
195
 
        app.add_url_rule(url, url, handler, methods=methods)
196
 
        url += '.<fmt>'
197
 
        app.add_url_rule(url, url, handler, methods=methods)
198
 
 
199
 
    app.logger.debug("urls added: %d", len(app.ceph_urls))
200
 
 
201
 
    app.add_url_rule('/<path:catchall_path>', '/<path:catchall_path>',
202
 
                     handler, methods=['GET', 'PUT'])
203
 
    return addr, port
204
 
 
205
 
 
206
 
def generate_url_and_params(app, sig, flavor):
207
 
    '''
208
 
    Digest command signature from cluster; generate an absolute
209
 
    (including app.ceph_baseurl) endpoint from all the prefix words,
210
 
    and a list of non-prefix param descs
211
 
    '''
212
 
 
213
 
    url = ''
214
 
    params = []
215
 
    # the OSD command descriptors don't include the 'tell <target>', so
216
 
    # tack it onto the front of sig
217
 
    if flavor == 'tell':
218
 
        tellsig = parse_funcsig(['tell',
219
 
                                {'name':'target', 'type':'CephOsdName'}])
220
 
        sig = tellsig + sig
221
 
 
222
 
    for desc in sig:
223
 
        # prefixes go in the URL path
224
 
        if desc.t == CephPrefix:
225
 
            url += '/' + desc.instance.prefix
226
 
        # CephChoices with 1 required string (not --) do too, unless
227
 
        # we've already started collecting params, in which case they
228
 
        # too are params
229
 
        elif desc.t == CephChoices and \
230
 
             len(desc.instance.strings) == 1 and \
231
 
             desc.req and \
232
 
             not str(desc.instance).startswith('--') and \
233
 
             not params:
234
 
            url += '/' + str(desc.instance)
235
 
        else:
236
 
            # tell/<target> is a weird case; the URL includes what
237
 
            # would everywhere else be a parameter
238
 
            if flavor == 'tell' and  \
239
 
              (desc.t, desc.name) == (CephOsdName, 'target'):
240
 
                url += '/<target>'
241
 
            else:
242
 
                params.append(desc)
243
 
 
244
 
    return app.ceph_baseurl + url, params
245
 
 
246
 
 
247
 
#
248
 
# end setup (import-time) functions, begin request-time functions
249
 
#
250
 
 
251
 
def concise_sig_for_uri(sig, flavor):
252
 
    '''
253
 
    Return a generic description of how one would send a REST request for sig
254
 
    '''
255
 
    prefix = []
256
 
    args = []
257
 
    ret = ''
258
 
    if flavor == 'tell':
259
 
        ret = 'tell/<osdid-or-pgid>/'
260
 
    for d in sig:
261
 
        if d.t == CephPrefix:
262
 
            prefix.append(d.instance.prefix)
263
 
        else:
264
 
            args.append(d.name + '=' + str(d))
265
 
    ret += '/'.join(prefix)
266
 
    if args:
267
 
        ret += '?' + '&'.join(args)
268
 
    return ret
269
 
 
270
 
def show_human_help(prefix):
271
 
    '''
272
 
    Dump table showing commands matching prefix
273
 
    '''
274
 
    # XXX There ought to be a better discovery mechanism than an HTML table
275
 
    s = '<html><body><table border=1><th>Possible commands:</th><th>Method</th><th>Description</th>'
276
 
 
277
 
    permmap = {'r':'GET', 'rw':'PUT'}
278
 
    line = ''
279
 
    for cmdsig in sorted(app.ceph_sigdict.itervalues(), cmp=descsort):
280
 
        concise = concise_sig(cmdsig['sig'])
281
 
        flavor = cmdsig.get('flavor', 'mon')
282
 
        if flavor == 'tell':
283
 
            concise = 'tell/<target>/' + concise
284
 
        if concise.startswith(prefix):
285
 
            line = ['<tr><td>']
286
 
            wrapped_sig = textwrap.wrap(
287
 
                concise_sig_for_uri(cmdsig['sig'], flavor), 40
288
 
            )
289
 
            for sigline in wrapped_sig:
290
 
                line.append(flask.escape(sigline) + '\n')
291
 
            line.append('</td><td>')
292
 
            line.append(permmap[cmdsig['perm']])
293
 
            line.append('</td><td>')
294
 
            line.append(flask.escape(cmdsig['help']))
295
 
            line.append('</td></tr>\n')
296
 
            s += ''.join(line)
297
 
 
298
 
    s += '</table></body></html>'
299
 
    if line:
300
 
        return s
301
 
    else:
302
 
        return ''
303
 
 
304
 
@app.before_request
305
 
def log_request():
306
 
    '''
307
 
    For every request, log it.  XXX Probably overkill for production
308
 
    '''
309
 
    app.logger.info(flask.request.url + " from " + flask.request.remote_addr + " " + flask.request.user_agent.string)
310
 
    app.logger.debug("Accept: %s", flask.request.accept_mimetypes.values())
311
 
 
312
 
@app.route('/')
313
 
def root_redir():
314
 
    return flask.redirect(app.ceph_baseurl)
315
 
 
316
 
def make_response(fmt, output, statusmsg, errorcode):
317
 
    '''
318
 
    If formatted output, cobble up a response object that contains the
319
 
    output and status wrapped in enclosing objects; if nonformatted, just
320
 
    use output+status.  Return HTTP status errorcode in any event.
321
 
    '''
322
 
    response = output
323
 
    if fmt:
324
 
        if 'json' in fmt:
325
 
            try:
326
 
                native_output = json.loads(output or '[]')
327
 
                response = json.dumps({"output":native_output,
328
 
                                       "status":statusmsg})
329
 
            except:
330
 
                return flask.make_response("Error decoding JSON from " +
331
 
                                           output, 500)
332
 
        elif 'xml' in fmt:
333
 
            # XXX
334
 
            # one is tempted to do this with xml.etree, but figuring out how
335
 
            # to 'un-XML' the XML-dumped output so it can be reassembled into
336
 
            # a piece of the tree here is beyond me right now.
337
 
            #ET = xml.etree.ElementTree
338
 
            #resp_elem = ET.Element('response')
339
 
            #o = ET.SubElement(resp_elem, 'output')
340
 
            #o.text = output
341
 
            #s = ET.SubElement(resp_elem, 'status')
342
 
            #s.text = statusmsg
343
 
            #response = ET.tostring(resp_elem)
344
 
            response = '''
345
 
<response>
346
 
  <output>
347
 
    {0}
348
 
  </output>
349
 
  <status>
350
 
    {1}
351
 
  </status>
352
 
</response>'''.format(response, xml.sax.saxutils.escape(statusmsg))
353
 
    else:
354
 
        if not 200 <= errorcode < 300:
355
 
            response = response + '\n' + statusmsg + '\n'
356
 
 
357
 
    return flask.make_response(response, errorcode)
358
 
 
359
 
def handler(catchall_path=None, fmt=None, target=None):
360
 
    '''
361
 
    Main endpoint handler; generic for every endpoint, including catchall.
362
 
    Handles the catchall, anything with <.fmt>, anything with embedded
363
 
    <target>.  Partial match or ?help cause the HTML-table
364
 
    "show_human_help" output.  
365
 
    '''
366
 
 
367
 
    ep = catchall_path or flask.request.endpoint
368
 
    ep = ep.replace('.<fmt>', '')
369
 
 
370
 
    if ep[0] != '/':
371
 
        ep = '/' + ep
372
 
 
373
 
    # demand that endpoint begin with app.ceph_baseurl
374
 
    if not ep.startswith(app.ceph_baseurl):
375
 
        return make_response(fmt, '', 'Page not found', 404)
376
 
 
377
 
    rel_ep = ep[len(app.ceph_baseurl)+1:]
378
 
 
379
 
    # Extensions override Accept: headers override defaults
380
 
    if not fmt:
381
 
        if 'application/json' in flask.request.accept_mimetypes.values():
382
 
            fmt = 'json'
383
 
        elif 'application/xml' in flask.request.accept_mimetypes.values():
384
 
            fmt = 'xml'
385
 
 
386
 
    prefix = ''
387
 
    pgid = None
388
 
    cmdtarget = 'mon', ''
389
 
 
390
 
    if target:
391
 
        # got tell/<target>; validate osdid or pgid
392
 
        name = CephOsdName()
393
 
        pgidobj = CephPgid()
394
 
        try:
395
 
            name.valid(target)
396
 
        except ArgumentError:
397
 
            # try pgid
398
 
            try:
399
 
                pgidobj.valid(target)
400
 
            except ArgumentError:
401
 
                return flask.make_response("invalid osdid or pgid", 400)
402
 
            else:
403
 
                # it's a pgid
404
 
                pgid = pgidobj.val
405
 
                cmdtarget = 'pg', pgid
406
 
        else:
407
 
            # it's an osd
408
 
            cmdtarget = name.nametype, name.nameid
409
 
 
410
 
        # prefix does not include tell/<target>/
411
 
        prefix = ' '.join(rel_ep.split('/')[2:]).strip()
412
 
    else:
413
 
        # non-target command: prefix is entire path
414
 
        prefix = ' '.join(rel_ep.split('/')).strip()
415
 
 
416
 
    # show "match as much as you gave me" help for unknown endpoints
417
 
    if not ep in app.ceph_urls:
418
 
        helptext = show_human_help(prefix)
419
 
        if helptext:
420
 
            resp = flask.make_response(helptext, 400)
421
 
            resp.headers['Content-Type'] = 'text/html'
422
 
            return resp
423
 
        else:
424
 
            return make_response(fmt, '', 'Invalid endpoint ' + ep, 400)
425
 
 
426
 
    found = None
427
 
    exc = ''
428
 
    for urldict in app.ceph_urls[ep]:
429
 
        if flask.request.method not in urldict['methods']:
430
 
            continue
431
 
        paramsig = urldict['paramsig']
432
 
 
433
 
        # allow '?help' for any specifically-known endpoint
434
 
        if 'help' in flask.request.args:
435
 
            response = flask.make_response('{0}: {1}'.\
436
 
                format(prefix + concise_sig(paramsig), urldict['help']))
437
 
            response.headers['Content-Type'] = 'text/plain'
438
 
            return response
439
 
 
440
 
        # if there are parameters for this endpoint, process them
441
 
        if paramsig:
442
 
            args = {}
443
 
            for k, l in flask.request.args.iterlists():
444
 
                if len(l) == 1:
445
 
                    args[k] = l[0]
446
 
                else:
447
 
                    args[k] = l
448
 
 
449
 
            # is this a valid set of params?
450
 
            try:
451
 
                argdict = validate(args, paramsig)
452
 
                found = urldict
453
 
                break
454
 
            except Exception as e:
455
 
                exc += str(e)
456
 
                continue
457
 
        else:
458
 
            if flask.request.args:
459
 
                continue
460
 
            found = urldict
461
 
            argdict = {}
462
 
            break
463
 
 
464
 
    if not found:
465
 
        return make_response(fmt, '', exc + '\n', 400)
466
 
 
467
 
    argdict['format'] = fmt or 'plain'
468
 
    argdict['module'] = found['module']
469
 
    argdict['perm'] = found['perm']
470
 
    if pgid:
471
 
        argdict['pgid'] = pgid
472
 
 
473
 
    if not cmdtarget:
474
 
        cmdtarget = ('mon', '')
475
 
 
476
 
    app.logger.debug('sending command prefix %s argdict %s', prefix, argdict)
477
 
    ret, outbuf, outs = json_command(app.ceph_cluster, prefix=prefix,
478
 
                                     target=cmdtarget,
479
 
                                     inbuf=flask.request.data, argdict=argdict)
480
 
    if ret:
481
 
        return make_response(fmt, '', 'Error: {0} ({1})'.format(outs, ret), 400)
482
 
 
483
 
    response = make_response(fmt, outbuf, outs or 'OK', 200)
484
 
    if fmt:
485
 
        contenttype = 'application/' + fmt.replace('-pretty','')
486
 
    else:
487
 
        contenttype = 'text/plain'
488
 
    response.headers['Content-Type'] = contenttype
489
 
    return response
490
 
 
491
 
#
492
 
# Main entry point from wrapper/WSGI server: call with cmdline args,
493
 
# get back the WSGI app entry point
494
 
#
495
 
def generate_app(conf, cluster, clientname, clientid, args):
496
 
    addr, port = api_setup(app, conf, cluster, clientname, clientid, args)
497
 
    app.ceph_addr = addr
498
 
    app.ceph_port = port
499
 
    return app