summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDan Mick <dan.mick@inktank.com>2013-07-23 00:50:15 -0700
committerDan Mick <dan.mick@inktank.com>2013-07-26 16:11:03 -0700
commit7b42deef3810e207f0e044769e7c70175993954a (patch)
treec5846abe88bbb89bb9e2341b2dcfe923edd8dbb0
parentbcbb807c018f89d473a252d87e8d48b5220b3a61 (diff)
downloadceph-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.cc23
-rw-r--r--src/pybind/ceph_argparse.py3
-rwxr-xr-xsrc/pybind/ceph_rest_api.py296
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)