diff options
author | Dan Mick <dan.mick@inktank.com> | 2013-07-23 00:50:15 -0700 |
---|---|---|
committer | Dan Mick <dan.mick@inktank.com> | 2013-07-26 16:11:03 -0700 |
commit | 7b42deef3810e207f0e044769e7c70175993954a (patch) | |
tree | c5846abe88bbb89bb9e2341b2dcfe923edd8dbb0 | |
parent | bcbb807c018f89d473a252d87e8d48b5220b3a61 (diff) | |
download | ceph-7b42deef3810e207f0e044769e7c70175993954a.tar.gz |
ceph_rest_api.py: obtain and handle tell <osd-or-pgid> commands
Contact an OSD that's up to get a list of the commands, and use
them to add to the URL map.
Special treatment throughout for these commands:
* hack the help signature dump
* keep a 'flavor' per command to allow special handler() processing
* strip off 'tell/<target>' when constructing command
* allow multiple dicts with the same url
(the parameters and get/put methods can change)
* because of above, method must be validated in handler()
* validate the given OSD
* calculate target for command (mon, osd, pg)
Unrelated: make method_dict into global METHOD_DICT
Signed-off-by: Dan Mick <dan.mick@inktank.com>
-rw-r--r-- | src/osd/OSD.cc | 23 | ||||
-rw-r--r-- | src/pybind/ceph_argparse.py | 3 | ||||
-rwxr-xr-x | src/pybind/ceph_rest_api.py | 296 |
3 files changed, 229 insertions, 93 deletions
diff --git a/src/osd/OSD.cc b/src/osd/OSD.cc index 76b11672b81..d611afdd08f 100644 --- a/src/osd/OSD.cc +++ b/src/osd/OSD.cc @@ -3822,24 +3822,41 @@ struct OSDCommand { {parsesig, helptext, module, perm, availability}, // yes, these are really pg commands, but there's a limit to how -// much work it's worth. The OSD returns all of them. +// much work it's worth. The OSD returns all of them. Make this +// form (pg <pgid> <cmd>) valid only for the cli. +// Rest uses "tell <pgid> <cmd>" COMMAND("pg " \ "name=pgid,type=CephPgid " \ "name=cmd,type=CephChoices,strings=query", \ - "show details of a specific pg", "osd", "r", "cli,rest") + "show details of a specific pg", "osd", "r", "cli") COMMAND("pg " \ "name=pgid,type=CephPgid " \ "name=cmd,type=CephChoices,strings=mark_unfound_lost " \ "name=mulcmd,type=CephChoices,strings=revert", \ "mark all unfound objects in this pg as lost, either removing or reverting to a prior version if one is available", - "osd", "rw", "cli,rest") + "osd", "rw", "cli") COMMAND("pg " \ "name=pgid,type=CephPgid " \ "name=cmd,type=CephChoices,strings=list_missing " \ "name=offset,type=CephString,req=false", "list missing objects on this pg, perhaps starting at an offset given in JSON", + "osd", "r", "cli") + +// new form: tell <pgid> <cmd> for both cli and rest + +COMMAND("query", + "show details of a specific pg", "osd", "r", "cli,rest") +COMMAND("mark_unfound_lost " \ + "name=mulcmd,type=CephChoices,strings=revert", \ + "mark all unfound objects in this pg as lost, either removing or reverting to a prior version if one is available", "osd", "rw", "cli,rest") +COMMAND("list_missing " \ + "name=offset,type=CephString,req=false", + "list missing objects on this pg, perhaps starting at an offset given in JSON", + "osd", "r", "cli,rest") + +// tell <osd.n> commands. Validation of osd.n must be special-cased in client COMMAND("version", "report version of OSD", "osd", "r", "cli,rest") COMMAND("injectargs " \ diff --git a/src/pybind/ceph_argparse.py b/src/pybind/ceph_argparse.py index b014d7d626c..855e21c2508 100644 --- a/src/pybind/ceph_argparse.py +++ b/src/pybind/ceph_argparse.py @@ -263,6 +263,8 @@ class CephIPAddr(CephArgtype): if p is not None and long(p) > 65535: raise ArgumentValid("{0} not a valid port number".format(p)) self.val = s + self.addr = a + self.port = p def __str__(self): return '<IPaddr[:port]>' @@ -274,6 +276,7 @@ class CephEntityAddr(CephIPAddr): def valid(self, s, partial=False): ip, nonce = s.split('/') super(self.__class__, self).valid(ip) + self.nonce = nonce self.val = s def __str__(self): diff --git a/src/pybind/ceph_rest_api.py b/src/pybind/ceph_rest_api.py index 28a0419c33c..f8d9b92129e 100755 --- a/src/pybind/ceph_rest_api.py +++ b/src/pybind/ceph_rest_api.py @@ -1,13 +1,15 @@ #!/usr/bin/python # vim: ts=4 sw=4 smarttab expandtab -import os import collections import ConfigParser +import errno import json import logging import logging.handlers +import os import rados +import socket import textwrap import xml.etree.ElementTree import xml.sax.saxutils @@ -99,6 +101,26 @@ def get_conf(cfg, clientname, key): pass return None +METHOD_DICT = {'r':['GET'], 'w':['PUT', 'DELETE']} + +def find_up_osd(): + ''' + Find an up OSD. Return the last one that's up. + Returns id as an int. + ''' + ret, outbuf, outs = json_command(glob.cluster, prefix="osd dump", + argdict=dict(format='json')) + if ret: + raise EnvironmentError(ret, 'Can\'t get osd dump output') + try: + osddump = json.loads(outbuf) + except: + raise EnvironmentError(errno.EINVAL, 'Invalid JSON back from osd dump') + osds = [osd['osd'] for osd in osddump['osds'] if osd['up']] + if not osds: + raise EnvironmentError(errno.ENOENT, 'No up OSDs found') + return int(osds[-1]) + # XXX this is done globally, and cluster connection kept open; there # are facilities to pass around global info to requests and to # tear down connections between requests if it becomes important @@ -109,6 +131,22 @@ def api_setup(): signatures, module, perms, and help; stuff them away in the glob.urls dict. """ + def get_command_descriptions(target=('mon','')): + ret, outbuf, outs = json_command(glob.cluster, target, + prefix='get_command_descriptions', + timeout=30) + if ret: + err = "Can't get command descriptions: {0}".format(outs) + app.logger.error(err) + raise EnvironmentError(ret, err) + + try: + sigdict = parse_json_funcsigs(outbuf, 'rest') + except Exception as e: + err = "Can't parse command descriptions: {}".format(e) + app.logger.error(err) + raise EnvironmentError(err) + return sigdict conffile = os.environ.get('CEPH_CONF', '') clustername = os.environ.get('CEPH_CLUSTER_NAME', 'ceph') @@ -148,19 +186,22 @@ def api_setup(): h.setFormatter(logging.Formatter( '%(asctime)s %(name)s %(levelname)s: %(message)s')) - ret, outbuf, outs = json_command(glob.cluster, - prefix='get_command_descriptions') - if ret: - err = "Can't contact cluster for command descriptions: {0}".format(outs) - app.logger.error(err) - raise EnvironmentError(ret, err) + glob.sigdict = get_command_descriptions() - try: - glob.sigdict = parse_json_funcsigs(outbuf, 'rest') - except Exception as e: - err = "Can't parse command descriptions: {}".format(e) - app.logger.error(err) - raise EnvironmentError(err) + osdid = find_up_osd() + if osdid: + osd_sigdict = get_command_descriptions(target=('osd', int(osdid))) + + # shift osd_sigdict keys up to fit at the end of the mon's glob.sigdict + maxkey = sorted(glob.sigdict.keys())[-1] + maxkey = int(maxkey.replace('cmd', '')) + osdkey = maxkey + 1 + for k, v in osd_sigdict.iteritems(): + newv = v + newv['flavor'] = 'tell' + globk = 'cmd' + str(osdkey) + glob.sigdict[globk] = newv + osdkey += 1 # glob.sigdict maps "cmdNNN" to a dict containing: # 'sig', an array of argdescs @@ -173,27 +214,37 @@ def api_setup(): glob.urls = {} for cmdnum, cmddict in glob.sigdict.iteritems(): cmdsig = cmddict['sig'] - url, params = generate_url_and_params(cmdsig) - if url in glob.urls: - continue + flavor = cmddict.get('flavor', 'mon') + url, params = generate_url_and_params(cmdsig, flavor) + perm = cmddict['perm'] + for k in METHOD_DICT.iterkeys(): + if k in perm: + methods = METHOD_DICT[k] + urldict = {'paramsig':params, + 'help':cmddict['help'], + 'module':cmddict['module'], + 'perm':perm, + 'flavor':flavor, + 'methods':methods, + } + + # glob.urls contains a list of urldicts (usually only one long) + if url not in glob.urls: + glob.urls[url] = [urldict] else: - perm = cmddict['perm'] - urldict = {'paramsig':params, - 'help':cmddict['help'], - 'module':cmddict['module'], - 'perm':perm, - } - method_dict = {'r':['GET'], - 'w':['PUT', 'DELETE']} - for k in method_dict.iterkeys(): - if k in perm: - methods = method_dict[k] - app.add_url_rule(url, url, handler, methods=methods) - glob.urls[url] = urldict - - url += '.<fmt>' - app.add_url_rule(url, url, handler, methods=methods) - glob.urls[url] = urldict + # If more than one, need to make union of methods of all. + # Method must be checked in handler + methodset = set(methods) + for old_urldict in glob.urls[url]: + methodset |= set(old_urldict['methods']) + methods = list(methodset) + glob.urls[url].append(urldict) + + # add, or re-add, rule with all methods and urldicts + app.add_url_rule(url, url, handler, methods=methods) + url += '.<fmt>' + app.add_url_rule(url, url, handler, methods=methods) + app.logger.debug("urls added: %d", len(glob.urls)) app.add_url_rule('/<path:catchall_path>', '/<path:catchall_path>', @@ -201,63 +252,84 @@ def api_setup(): return addr, port -def generate_url_and_params(sig): +def generate_url_and_params(sig, flavor): """ Digest command signature from cluster; generate an absolute (including glob.baseurl) endpoint from all the prefix words, - and a dictionary of non-prefix parameters + and a list of non-prefix param descs """ url = '' params = [] + # the OSD command descriptors don't include the 'tell <target>', so + # tack it onto the front of sig + if flavor == 'tell': + tellsig = parse_funcsig(['tell', + {'name':'target', 'type':'CephOsdName'}]) + sig = tellsig + sig + for desc in sig: + # prefixes go in the URL path if desc.t == CephPrefix: url += '/' + desc.instance.prefix + # CephChoices with 1 required string (not --) do too, unless + # we've already started collecting params, in which case they + # too are params elif desc.t == CephChoices and \ len(desc.instance.strings) == 1 and \ desc.req and \ - not str(desc.instance).startswith('--'): + not str(desc.instance).startswith('--') and \ + not params: url += '/' + str(desc.instance) else: - params.append(desc) - return glob.baseurl + url, params + # tell/<target> is a weird case; the URL includes what + # would everywhere else be a parameter + if flavor == 'tell' and \ + (desc.t, desc.name) == (CephOsdName, 'target'): + url += '/<target>' + else: + params.append(desc) + return glob.baseurl + url, params -def concise_sig_for_uri(sig): +def concise_sig_for_uri(sig, flavor): """ Return a generic description of how one would send a REST request for sig """ prefix = [] args = [] + ret = '' + if flavor == 'tell': + ret = 'tell/<osdid-or-pgid>/' for d in sig: if d.t == CephPrefix: prefix.append(d.instance.prefix) else: args.append(d.name + '=' + str(d)) - sig = '/'.join(prefix) + ret += '/'.join(prefix) if args: - sig += '?' + '&'.join(args) - return sig + ret += '?' + '&'.join(args) + return ret def show_human_help(prefix): """ Dump table showing commands matching prefix """ - # XXX this really needs to be a template - #s = '<html><body><style>.colhalf { width: 50%;} body{word-wrap:break-word;}</style>' - #s += '<table border=1><col class=colhalf /><col class=colhalf />' - #s += '<th>Possible commands:</th>' - # XXX the above mucking with css doesn't cause sensible columns. + # XXX There ought to be a better discovery mechanism than an HTML table s = '<html><body><table border=1><th>Possible commands:</th><th>Method</th><th>Description</th>' - possible = [] permmap = {'r':'GET', 'rw':'PUT'} line = '' for cmdsig in sorted(glob.sigdict.itervalues(), cmp=descsort): concise = concise_sig(cmdsig['sig']) + flavor = cmdsig.get('flavor', 'mon') + if flavor == 'tell': + concise = 'tell/<target>/' + concise if concise.startswith(prefix): line = ['<tr><td>'] - wrapped_sig = textwrap.wrap(concise_sig_for_uri(cmdsig['sig']), 40) + wrapped_sig = textwrap.wrap( + concise_sig_for_uri(cmdsig['sig'], flavor), 40 + ) for sigline in wrapped_sig: line.append(flask.escape(sigline) + '\n') line.append('</td><td>') @@ -328,19 +400,23 @@ def make_response(fmt, output, statusmsg, errorcode): return flask.make_response(response, errorcode) -def handler(catchall_path=None, fmt=None): +def handler(catchall_path=None, fmt=None, target=None): """ Main endpoint handler; generic for every endpoint """ - if (catchall_path): - ep = catchall_path.replace('.<fmt>', '') - else: - ep = flask.request.endpoint.replace('.<fmt>', '') + ep = catchall_path or flask.request.endpoint + ep = ep.replace('.<fmt>', '') if ep[0] != '/': ep = '/' + ep + # demand that endpoint begin with glob.baseurl + if not ep.startswith(glob.baseurl): + return make_response(fmt, '', 'Page not found', 404) + + rel_ep = ep[len(glob.baseurl)+1:] + # Extensions override Accept: headers override defaults if not fmt: if 'application/json' in flask.request.accept_mimetypes.values(): @@ -348,12 +424,36 @@ def handler(catchall_path=None, fmt=None): elif 'application/xml' in flask.request.accept_mimetypes.values(): fmt = 'xml' - # demand that endpoint begin with glob.baseurl - if not ep.startswith(glob.baseurl): - return make_response(fmt, '', 'Page not found', 404) + valid = True + prefix = '' + pgid = None + cmdtarget = 'mon', '' + + if target: + # got tell/<target>; validate osdid or pgid + name = CephOsdName() + pgidobj = CephPgid() + try: + name.valid(target) + except ArgumentError: + # try pgid + try: + pgidobj.valid(target) + except ArgumentError: + return flask.make_response("invalid osdid or pgid", 400) + else: + # it's a pgid + pgid = pgidobj.val + cmdtarget = 'pg', pgid + else: + # it's an osd + cmdtarget = name.nametype, name.nameid - relative_endpoint = ep[len(glob.baseurl)+1:] - prefix = ' '.join(relative_endpoint.split('/')).strip() + # prefix does not include tell/<target>/ + prefix = ' '.join(rel_ep.split('/')[2:]).strip() + else: + # non-target command: prefix is entire path + prefix = ' '.join(rel_ep.split('/')).strip() # show "match as much as you gave me" help for unknown endpoints if not ep in glob.urls: @@ -365,43 +465,59 @@ def handler(catchall_path=None, fmt=None): else: return make_response(fmt, '', 'Invalid endpoint ' + ep, 400) - urldict = glob.urls[ep] - paramsig = urldict['paramsig'] - - # allow '?help' for any specifically-known endpoint - if 'help' in flask.request.args: - response = flask.make_response('{0}: {1}'.\ - format(prefix + concise_sig(paramsig), urldict['help'])) - response.headers['Content-Type'] = 'text/plain' - return response - - # if there are parameters for this endpoint, process them - if paramsig: - args = {} - for k, l in flask.request.args.iterlists(): - if len(l) == 1: - args[k] = l[0] - else: - args[k] = l - - # is this a valid set of params? - try: - argdict = validate(args, paramsig) - except Exception as e: - return make_response(fmt, '', str(e) + '\n', 400) - else: - # no parameters for this endpoint; complain if args are supplied - if flask.request.args: - return make_response(fmt, '', ep + 'takes no params', 400) - argdict = {} + found = None + exc = '' + for urldict in glob.urls[ep]: + if flask.request.method not in urldict['methods']: + continue + paramsig = urldict['paramsig'] + + # allow '?help' for any specifically-known endpoint + if 'help' in flask.request.args: + response = flask.make_response('{0}: {1}'.\ + format(prefix + concise_sig(paramsig), urldict['help'])) + response.headers['Content-Type'] = 'text/plain' + return response + + # if there are parameters for this endpoint, process them + if paramsig: + args = {} + for k, l in flask.request.args.iterlists(): + if len(l) == 1: + args[k] = l[0] + else: + args[k] = l + + # is this a valid set of params? + try: + argdict = validate(args, paramsig) + found = urldict + break + except Exception as e: + exc += str(e) + continue + else: + if flask.request.args: + continue + found = urldict + argdict = {} + break + if not found: + return make_response(fmt, '', exc + '\n', 400) argdict['format'] = fmt or 'plain' - argdict['module'] = urldict['module'] - argdict['perm'] = urldict['perm'] + argdict['module'] = found['module'] + argdict['perm'] = found['perm'] + if pgid: + argdict['pgid'] = pgid + + if not cmdtarget: + cmdtarget = ('mon', '') app.logger.debug('sending command prefix %s argdict %s', prefix, argdict) ret, outbuf, outs = json_command(glob.cluster, prefix=prefix, + target=cmdtarget, inbuf=flask.request.data, argdict=argdict) if ret: return make_response(fmt, '', 'Error: {0} ({1})'.format(outs, ret), 400) |