From: Nathan Cutler Date: Wed, 6 Sep 2017 18:09:44 +0000 (+0200) Subject: tools: cleanup: rip out ceph-rest-api X-Git-Tag: v13.0.2~80^2~4 X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=495742f7dd635a240f39c708233d3bd84c56aa17;p=ceph.git tools: cleanup: rip out ceph-rest-api Obsoleted by the mgr REST API. Fixes: http://tracker.ceph.com/issues/21264 Signed-off-by: Nathan Cutler --- diff --git a/ceph.spec.in b/ceph.spec.in index 7560cdfc98f..5583e816844 100644 --- a/ceph.spec.in +++ b/ceph.spec.in @@ -333,13 +333,6 @@ Summary: Ceph Monitor Daemon Group: System/Filesystems %endif Requires: ceph-base = %{_epoch_prefix}%{version}-%{release} -# For ceph-rest-api -%if 0%{?fedora} || 0%{?rhel} -Requires: python%{_python_buildid}-flask -%endif -%if 0%{?suse_version} -Requires: python%{_python_buildid}-Flask -%endif %description mon ceph-mon is the cluster monitor daemon for the Ceph distributed file system. One or more instances of ceph-mon form a Paxos part-time @@ -1319,16 +1312,8 @@ fi %files mon %{_bindir}/ceph-mon -%{_bindir}/ceph-rest-api %{_bindir}/ceph-monstore-tool %{_mandir}/man8/ceph-mon.8* -%{_mandir}/man8/ceph-rest-api.8* -%if 0%{with python2} -%{python_sitelib}/ceph_rest_api.py* -%else -%{python3_sitelib}/ceph_rest_api.py -%{python3_sitelib}/__pycache__/ceph_rest_api.cpython*.py* -%endif %{_unitdir}/ceph-mon@.service %{_unitdir}/ceph-mon.target %attr(750,ceph,ceph) %dir %{_localstatedir}/lib/ceph/mon diff --git a/debian/ceph-mon.install b/debian/ceph-mon.install index a9785bddbb1..ff429f17a6a 100644 --- a/debian/ceph-mon.install +++ b/debian/ceph-mon.install @@ -1,7 +1,4 @@ lib/systemd/system/ceph-mon* usr/bin/ceph-mon usr/bin/ceph-monstore-tool -usr/bin/ceph-rest-api -usr/lib/python*/dist-packages/ceph_rest_api.py* usr/share/man/man8/ceph-mon.8 -usr/share/man/man8/ceph-rest-api.8 diff --git a/debian/control b/debian/control index 8422873d7d2..0c9ec39afe0 100644 --- a/debian/control +++ b/debian/control @@ -203,7 +203,6 @@ Description: debugging symbols for ceph-mgr Package: ceph-mon Architecture: linux-any Depends: ceph-base (= ${binary:Version}), - python-flask, ${misc:Depends}, ${shlibs:Depends}, Recommends: ceph-common, diff --git a/doc/man/8/CMakeLists.txt b/doc/man/8/CMakeLists.txt index 84e76409620..d559f4683b0 100644 --- a/doc/man/8/CMakeLists.txt +++ b/doc/man/8/CMakeLists.txt @@ -13,8 +13,7 @@ set(server_srcs crushtool.rst ceph-run.rst mount.ceph.rst - ceph-create-keys.rst - ceph-rest-api.rst) + ceph-create-keys.rst) if(WITH_TESTS) list(APPEND server_srcs ceph-debugpack.rst) diff --git a/doc/man/8/ceph-rest-api.rst b/doc/man/8/ceph-rest-api.rst deleted file mode 100644 index 9864a9b6f56..00000000000 --- a/doc/man/8/ceph-rest-api.rst +++ /dev/null @@ -1,150 +0,0 @@ -:orphan: - -===================================================== - ceph-rest-api -- ceph RESTlike administration server -===================================================== - -.. program:: ceph-rest-api - -Synopsis -======== - -| **ceph-rest-api** [ -c *conffile* ] [--cluster *clustername* ] [ -n *name* ] [-i *id* ] - - -Description -=========== - -**ceph-rest-api** is a WSGI application that can run as a -standalone web service or run under a web server that supports -WSGI. It provides much of the functionality of the **ceph** -command-line tool through an HTTP-accessible interface. - -Options -======= - -.. option:: -c/--conf conffile - - names the ceph.conf file to use for configuration. If -c is not - specified, the default depends on the state of the --cluster option - (default 'ceph'; see below). The configuration file is searched - for in this order: - - * $CEPH_CONF - * /etc/ceph/${cluster}.conf - * ~/.ceph/${cluster}.conf - * ${cluster}.conf (in the current directory) - - so you can also pass this option in the environment as CEPH_CONF. - -.. option:: --cluster clustername - - set *clustername* for use in the $cluster metavariable, for - locating the ceph.conf file. The default is 'ceph'. - -.. option:: -n/--name name - - specifies the client 'name', which is used to find the - client-specific configuration options in the config file, and - also is the name used for authentication when connecting - to the cluster (the entity name appearing in 'ceph auth ls' output, - for example). The default is 'client.restapi'. - -.. option:: -i/--id id - - specifies the client 'id', which will form the clientname - as 'client.' if clientname is not set. If -n/-name is - set, that takes precedence. - - Also, global Ceph options are supported. - - -Configuration parameters -======================== - -Supported configuration parameters include: - -* **keyring** the keyring file holding the key for 'clientname' -* **public addr** ip:port to listen on (default 0.0.0.0:5000) -* **log file** (usual Ceph default) -* **restapi base url** the base URL to answer requests on (default /api/v0.1) -* **restapi log level** critical, error, warning, info, debug (default warning) - -Configuration parameters are searched in the standard order: -first in the section named '', then 'client', then 'global'. - - is either supplied by -n/--name, "client." where - is supplied by -i/--id, or 'client.restapi' if neither option -is present. - -A single-threaded server will run on **public addr** if the ceph-rest-api -executed directly; otherwise, configuration is specified by the enclosing -WSGI web server. - -Commands -======== - -Commands are submitted with HTTP GET requests (for commands that -primarily return data) or PUT (for commands that affect cluster state). -HEAD and OPTIONS are also supported. Standard HTTP status codes -are returned. - -For commands that return bulk data, the request can include -Accept: application/json or Accept: application/xml to select the -desired structured output, or you may use a .json or .xml addition -to the requested PATH. Parameters are supplied as query parameters -in the request; for parameters that take more than one value, repeat -the key=val construct. For instance, to remove OSDs 2 and 3, -send a PUT request to ``osd/rm?ids=2&ids=3``. - -Discovery -========= - -Human-readable discovery of supported commands and parameters, along -with a small description of each command, is provided when the requested -path is incomplete/partially matching. Requesting / will redirect to -the value of **restapi base url**, and that path will give a full list -of all known commands. -For example, requesting ``api/vX.X/mon`` will return the list of API calls for -monitors - ``api/vX.X/osd`` will return the list of API calls for OSD and so on. - -The command set is very similar to the commands -supported by the **ceph** tool. One notable exception is that the -``ceph pg `` style of commands is supported here -as ``tell//command?args``. - -Deployment as WSGI application -============================== - -When deploying as WSGI application (say, with Apache/mod_wsgi, -or nginx/uwsgi, or gunicorn, etc.), use the ``ceph_rest_api.py`` module -(``ceph-rest-api`` is a thin layer around this module). The standalone web -server is of course not used, so address/port configuration is done in -the WSGI server. Use a python .wsgi module or the equivalent to call -``app = generate_app(conf, cluster, clientname, clientid, args)`` where: - -* conf is as -c/--conf above -* cluster is as --cluster above -* clientname, -n/--name -* clientid, -i/--id, and -* args are any other generic Ceph arguments - -When app is returned, it will have attributes 'ceph_addr' and 'ceph_port' -set to what the address and port are in the Ceph configuration; -those may be used for the server, or ignored. - -Any errors reading configuration or connecting to the cluster cause an -exception to be raised; see your WSGI server documentation for how to -see those messages in case of problem. - -Availability -============ - -**ceph-rest-api** is part of Ceph, a massively scalable, open-source, distributed storage system. Please refer to the Ceph documentation at -http://ceph.com/docs for more information. - - -See also -======== - -:doc:`ceph `\(8) diff --git a/doc/man_index.rst b/doc/man_index.rst index fbd0585cf82..c9fcdfe6027 100644 --- a/doc/man_index.rst +++ b/doc/man_index.rst @@ -22,7 +22,6 @@ man/8/ceph-osd man/8/ceph-post-file man/8/ceph-rbdnamer - man/8/ceph-rest-api man/8/ceph-run man/8/ceph-syn man/8/ceph diff --git a/doc/rados/man/index.rst b/doc/rados/man/index.rst index abeb88b1d38..8cca2cf565e 100644 --- a/doc/rados/man/index.rst +++ b/doc/rados/man/index.rst @@ -10,7 +10,6 @@ ../../man/8/ceph-volume-systemd.rst ../../man/8/ceph.rst ../../man/8/ceph-deploy.rst - ../../man/8/ceph-rest-api.rst ../../man/8/ceph-authtool.rst ../../man/8/ceph-clsinfo.rst ../../man/8/ceph-conf.rst diff --git a/qa/tasks/rest_api.py b/qa/tasks/rest_api.py deleted file mode 100644 index e86f77eb525..00000000000 --- a/qa/tasks/rest_api.py +++ /dev/null @@ -1,184 +0,0 @@ -""" -Rest Api -""" -import logging -import contextlib -import time - -from teuthology import misc as teuthology -from teuthology import contextutil -from teuthology.orchestra import run -from teuthology.orchestra.daemon import DaemonGroup - -log = logging.getLogger(__name__) - - -@contextlib.contextmanager -def run_rest_api_daemon(ctx, api_clients): - """ - Wrapper starts the rest api daemons - """ - if not hasattr(ctx, 'daemons'): - ctx.daemons = DaemonGroup() - remotes = ctx.cluster.only(teuthology.is_type('client')).remotes - for rems, roles in remotes.iteritems(): - for whole_id_ in roles: - if whole_id_ in api_clients: - id_ = whole_id_[len('clients'):] - run_cmd = [ - 'sudo', - 'daemon-helper', - 'kill', - 'ceph-rest-api', - '-n', - 'client.rest{id}'.format(id=id_), ] - cl_rest_id = 'client.rest{id}'.format(id=id_) - ctx.daemons.add_daemon(rems, 'restapi', - cl_rest_id, - args=run_cmd, - logger=log.getChild(cl_rest_id), - stdin=run.PIPE, - wait=False, - ) - for i in range(1, 12): - log.info('testing for ceph-rest-api try {0}'.format(i)) - run_cmd = [ - 'wget', - '-O', - '/dev/null', - '-q', - 'http://localhost:5000/api/v0.1/status' - ] - proc = rems.run( - args=run_cmd, - check_status=False - ) - if proc.exitstatus == 0: - break - time.sleep(5) - if proc.exitstatus != 0: - raise RuntimeError('Cannot contact ceph-rest-api') - try: - yield - - finally: - """ - TO DO: destroy daemons started -- modify iter_daemons_of_role - """ - teuthology.stop_daemons_of_type(ctx, 'restapi') - -@contextlib.contextmanager -def task(ctx, config): - """ - Start up rest-api. - - To start on on all clients:: - - tasks: - - ceph: - - rest-api: - - To only run on certain clients:: - - tasks: - - ceph: - - rest-api: [client.0, client.3] - - or - - tasks: - - ceph: - - rest-api: - client.0: - client.3: - - The general flow of things here is: - 1. Find clients on which rest-api is supposed to run (api_clients) - 2. Generate keyring values - 3. Start up ceph-rest-api daemons - On cleanup: - 4. Stop the daemons - 5. Delete keyring value files. - """ - api_clients = [] - remotes = ctx.cluster.only(teuthology.is_type('client')).remotes - log.info(remotes) - if config == None: - api_clients = ['client.{id}'.format(id=id_) - for id_ in teuthology.all_roles_of_type(ctx.cluster, 'client')] - else: - api_clients = config - log.info(api_clients) - testdir = teuthology.get_testdir(ctx) - coverage_dir = '{tdir}/archive/coverage'.format(tdir=testdir) - for rems, roles in remotes.iteritems(): - for whole_id_ in roles: - if whole_id_ in api_clients: - id_ = whole_id_[len('client.'):] - keyring = '/etc/ceph/ceph.client.rest{id}.keyring'.format( - id=id_) - rems.run( - args=[ - 'sudo', - 'adjust-ulimits', - 'ceph-coverage', - coverage_dir, - 'ceph-authtool', - '--create-keyring', - '--gen-key', - '--name=client.rest{id}'.format(id=id_), - '--set-uid=0', - '--cap', 'mon', 'allow *', - '--cap', 'osd', 'allow *', - '--cap', 'mds', 'allow', - keyring, - run.Raw('&&'), - 'sudo', - 'chmod', - '0644', - keyring, - ], - ) - rems.run( - args=[ - 'sudo', - 'sh', - '-c', - run.Raw("'"), - "echo", - '[client.rest{id}]'.format(id=id_), - run.Raw('>>'), - "/etc/ceph/ceph.conf", - run.Raw("'") - ] - ) - rems.run( - args=[ - 'sudo', - 'sh', - '-c', - run.Raw("'"), - 'echo', - 'restapi', - 'keyring', - '=', - '/etc/ceph/ceph.client.rest{id}.keyring'.format(id=id_), - run.Raw('>>'), - '/etc/ceph/ceph.conf', - run.Raw("'"), - ] - ) - rems.run( - args=[ - 'sudo', - 'ceph', - 'auth', - 'import', - '-i', - '/etc/ceph/ceph.client.rest{id}.keyring'.format(id=id_), - ] - ) - with contextutil.nested( - lambda: run_rest_api_daemon(ctx=ctx, api_clients=api_clients),): - yield - diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e399af3dce2..33a8de9e407 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -939,7 +939,6 @@ install(PROGRAMS ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/ceph ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/ceph-post-file ${CMAKE_SOURCE_DIR}/src/ceph-run - ${CMAKE_SOURCE_DIR}/src/ceph-rest-api ${CMAKE_SOURCE_DIR}/src/ceph-clsinfo DESTINATION bin) install(PROGRAMS diff --git a/src/ceph-rest-api b/src/ceph-rest-api deleted file mode 100755 index d185a8041ef..00000000000 --- a/src/ceph-rest-api +++ /dev/null @@ -1,55 +0,0 @@ -#!/usr/bin/env python -# vim: ts=4 sw=4 smarttab expandtab - -import argparse -import inspect -import os -import sys - -# Make life easier on developers - -MYPATH = os.path.abspath(__file__) -MYDIR = os.path.dirname(MYPATH) -DEVMODEMSG = '*** DEVELOPER MODE: setting PYTHONPATH and LD_LIBRARY_PATH' - -def parse_args(): - parser = argparse.ArgumentParser(description="Ceph REST API webapp") - parser.add_argument('-c', '--conf', help='Ceph configuration file', - default='/etc/ceph/ceph.conf') - parser.add_argument('--cluster', help='Ceph cluster name') - parser.add_argument('-n', '--name', help='Ceph client name') - parser.add_argument('-i', '--id', help='Ceph client id', default='admin') - - return parser.parse_known_args() - -# main - -parsed_args, rest = parse_args() - -# import now that env vars are available to imported module - -try: - import ceph_rest_api -except EnvironmentError as e: - print >> sys.stderr, "Error importing ceph_rest_api: ", str(e) - sys.exit(1) - -# let other exceptions generate traceback - -app = ceph_rest_api.generate_app( - parsed_args.conf, - parsed_args.cluster, - parsed_args.name, - parsed_args.id, - rest, -) - -files = [os.path.split(fr[1])[-1] for fr in inspect.stack()] -if 'pdb.py' in files: - app.run(host=app.ceph_addr, port=app.ceph_port, - debug=True, use_reloader=False, use_debugger=False) -else: - if __name__ == '__main__': - app.run(host=app.ceph_addr, port=app.ceph_port) - else: - application = app diff --git a/src/pybind/CMakeLists.txt b/src/pybind/CMakeLists.txt index 9a37726f996..f9679d8f795 100644 --- a/src/pybind/CMakeLists.txt +++ b/src/pybind/CMakeLists.txt @@ -61,10 +61,6 @@ foreach(python_version ${py_vers}) DESTINATION ${PYTHON${PYTHON_VERSION}_INSTDIR}) endforeach() -install(FILES - ${CMAKE_CURRENT_SOURCE_DIR}/ceph_rest_api.py - DESTINATION ${PYTHON${PYTHON_VERSION}_INSTDIR}) - if(WITH_MGR) if(NOT WITH_PYTHON2 AND MGR_PYTHON_VERSION_MAJOR EQUAL 2) message(FATAL_ERROR "mgr plugins require python2 binding") diff --git a/src/pybind/ceph_rest_api.py b/src/pybind/ceph_rest_api.py deleted file mode 100644 index 85895f1b968..00000000000 --- a/src/pybind/ceph_rest_api.py +++ /dev/null @@ -1,520 +0,0 @@ -# vim: ts=4 sw=4 smarttab expandtab - -import errno -import json -import logging -import logging.handlers -import os -import rados -import textwrap -import xml.etree.ElementTree -import xml.sax.saxutils - -import flask -from ceph_argparse import \ - ArgumentError, CephPgid, CephOsdName, CephChoices, CephPrefix, \ - concise_sig, descsort, parse_funcsig, parse_json_funcsigs, \ - validate, json_command - -# -# Globals and defaults -# - -DEFAULT_ADDR = '::' -DEFAULT_PORT = '5000' -DEFAULT_ID = 'restapi' - -DEFAULT_BASEURL = '/api/v0.1' -DEFAULT_LOG_LEVEL = 'warning' -DEFAULT_LOGDIR = '/var/log/ceph' -# default client name will be 'client.' - -# network failure could keep the underlying json_command() waiting forever, -# set a timeout, so it bails out on timeout. -DEFAULT_TIMEOUT = 20 -# and retry in that case. -DEFAULT_TRIES = 5 - -# 'app' must be global for decorators, etc. -APPNAME = '__main__' -app = flask.Flask(APPNAME) - -LOGLEVELS = { - 'critical': logging.CRITICAL, - 'error': logging.ERROR, - 'warning': logging.WARNING, - 'info': logging.INFO, - 'debug': logging.DEBUG, -} - - -def find_up_osd(app): - ''' - Find an up OSD. Return the last one that's up. - Returns id as an int. - ''' - ret, outbuf, outs = json_command(app.ceph_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: - return None - return int(osds[-1]) - - -METHOD_DICT = {'r': ['GET'], 'w': ['PUT', 'DELETE']} - - -def api_setup(app, conf, cluster, clientname, clientid, args): - ''' - This is done globally, and cluster connection kept open for - the lifetime of the daemon. librados should assure that even - if the cluster goes away and comes back, our connection remains. - - Initialize the running instance. Open the cluster, get the command - signatures, module, perms, and help; stuff them away in the app.ceph_urls - dict. Also save app.ceph_sigdict for help() handling. - ''' - def get_command_descriptions(cluster, target=('mon', '')): - ret, outbuf, outs = json_command(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 - - app.ceph_cluster = cluster or 'ceph' - app.ceph_urls = {} - app.ceph_sigdict = {} - app.ceph_baseurl = '' - - conf = conf or '' - cluster = cluster or 'ceph' - clientid = clientid or DEFAULT_ID - clientname = clientname or 'client.' + clientid - - app.ceph_cluster = rados.Rados(name=clientname, conffile=conf) - app.ceph_cluster.conf_parse_argv(args) - app.ceph_cluster.connect() - - app.ceph_baseurl = app.ceph_cluster.conf_get('restapi_base_url') \ - or DEFAULT_BASEURL - if app.ceph_baseurl.endswith('/'): - app.ceph_baseurl = app.ceph_baseurl[:-1] - addr = app.ceph_cluster.conf_get('public_addr') or DEFAULT_ADDR - - if addr == '-': - addr = DEFAULT_ADDR - port = int(DEFAULT_PORT) - else: - # remove the type prefix from the conf value if any - for t in ('legacy:', 'msgr2:'): - if addr.startswith(t): - addr = addr[len(t):] - break - # remove any nonce from the conf value - addr = addr.split('/')[0] - addr, port = addr.rsplit(':', 1) - port = int(port) or int(DEFAULT_PORT) - - loglevel = app.ceph_cluster.conf_get('restapi_log_level') \ - or DEFAULT_LOG_LEVEL - # ceph has a default log file for daemons only; clients (like this) - # default to "". Override that for this particular client. - logfile = app.ceph_cluster.conf_get('log_file') - if not logfile: - logfile = os.path.join( - DEFAULT_LOGDIR, - '{cluster}-{clientname}.{pid}.log'.format( - cluster=cluster, - clientname=clientname, - pid=os.getpid() - ) - ) - app.logger.addHandler(logging.handlers.WatchedFileHandler(logfile)) - app.logger.setLevel(LOGLEVELS[loglevel.lower()]) - for h in app.logger.handlers: - h.setFormatter(logging.Formatter( - '%(asctime)s %(name)s %(levelname)s: %(message)s')) - - app.ceph_sigdict = get_command_descriptions(app.ceph_cluster) - - osdid = find_up_osd(app) - if osdid is not None: - osd_sigdict = get_command_descriptions(app.ceph_cluster, - target=('osd', int(osdid))) - - # shift osd_sigdict keys up to fit at the end of the mon's app.ceph_sigdict - maxkey = sorted(app.ceph_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) - app.ceph_sigdict[globk] = newv - osdkey += 1 - - # app.ceph_sigdict maps "cmdNNN" to a dict containing: - # 'sig', an array of argdescs - # 'help', the helptext - # 'module', the Ceph module this command relates to - # 'perm', a 'rwx*' string representing required permissions, and also - # a hint as to whether this is a GET or POST/PUT operation - # 'avail', a comma-separated list of strings of consumers that should - # display this command (filtered by parse_json_funcsigs() above) - app.ceph_urls = {} - for cmdnum, cmddict in app.ceph_sigdict.iteritems(): - cmdsig = cmddict['sig'] - flavor = cmddict.get('flavor', 'mon') - url, params = generate_url_and_params(app, 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, } - - # app.ceph_urls contains a list of urldicts (usually only one long) - if url not in app.ceph_urls: - app.ceph_urls[url] = [urldict] - else: - # 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 app.ceph_urls[url]: - methodset |= set(old_urldict['methods']) - methods = list(methodset) - app.ceph_urls[url].append(urldict) - - # add, or re-add, rule with all methods and urldicts - app.add_url_rule(url, url, handler, methods=methods) - url += '.' - app.add_url_rule(url, url, handler, methods=methods) - - app.logger.debug("urls added: %d", len(app.ceph_urls)) - - app.add_url_rule('/', '/', - handler, methods=['GET', 'PUT']) - return addr, port - - -def generate_url_and_params(app, sig, flavor): - ''' - Digest command signature from cluster; generate an absolute - (including app.ceph_baseurl) endpoint from all the prefix words, - and a list of non-prefix param descs - ''' - - url = '' - params = [] - # the OSD command descriptors don't include the 'tell ', 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 - else: - # tell/ 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 += '/' - else: - params.append(desc) - - return app.ceph_baseurl + url, params - - -# -# end setup (import-time) functions, begin request-time functions -# -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//' - for d in sig: - if d.t == CephPrefix: - prefix.append(d.instance.prefix) - else: - args.append(d.name + '=' + str(d)) - ret += '/'.join(prefix) - if args: - ret += '?' + '&'.join(args) - return ret - - -def show_human_help(prefix): - ''' - Dump table showing commands matching prefix - ''' - # XXX There ought to be a better discovery mechanism than an HTML table - s = '' - - permmap = {'r': 'GET', 'rw': 'PUT', 'rx': 'GET', 'rwx': 'PUT'} - line = '' - for cmdsig in sorted(app.ceph_sigdict.itervalues(), cmp=descsort): - concise = concise_sig(cmdsig['sig']) - flavor = cmdsig.get('flavor', 'mon') - if flavor == 'tell': - concise = 'tell//' + concise - if concise.startswith(prefix): - line = ['\n') - s += ''.join(line) - - s += '
Possible commands:MethodDescription
'] - 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('') - line.append(permmap[cmdsig['perm']]) - line.append('') - line.append(flask.escape(cmdsig['help'])) - line.append('
' - if line: - return s - else: - return '' - - -@app.before_request -def log_request(): - ''' - For every request, log it. XXX Probably overkill for production - ''' - app.logger.info(flask.request.url + " from " + flask.request.remote_addr + " " + flask.request.user_agent.string) - app.logger.debug("Accept: %s", flask.request.accept_mimetypes.values()) - - -@app.route('/') -def root_redir(): - return flask.redirect(app.ceph_baseurl) - - -def make_response(fmt, output, statusmsg, errorcode): - ''' - If formatted output, cobble up a response object that contains the - output and status wrapped in enclosing objects; if nonformatted, just - use output+status. Return HTTP status errorcode in any event. - ''' - response = output - if fmt: - if 'json' in fmt: - try: - native_output = json.loads(output or '[]') - response = json.dumps({"output": native_output, - "status": statusmsg}) - except: - return flask.make_response("Error decoding JSON from " + - output, 500) - elif 'xml' in fmt: - # XXX - # one is tempted to do this with xml.etree, but figuring out how - # to 'un-XML' the XML-dumped output so it can be reassembled into - # a piece of the tree here is beyond me right now. - # ET = xml.etree.ElementTree - # resp_elem = ET.Element('response') - # o = ET.SubElement(resp_elem, 'output') - # o.text = output - # s = ET.SubElement(resp_elem, 'status') - # s.text = statusmsg - # response = ET.tostring(resp_elem) - response = ''' - - - {0} - - - {1} - -'''.format(response, xml.sax.saxutils.escape(statusmsg)) - else: - if not 200 <= errorcode < 300: - response = response + '\n' + statusmsg + '\n' - - return flask.make_response(response, errorcode) - - -def handler(catchall_path=None, fmt=None, target=None): - ''' - Main endpoint handler; generic for every endpoint, including catchall. - Handles the catchall, anything with <.fmt>, anything with embedded - . Partial match or ?help cause the HTML-table - "show_human_help" output. - ''' - - ep = catchall_path or flask.request.endpoint - ep = ep.replace('.', '') - - if ep[0] != '/': - ep = '/' + ep - - # demand that endpoint begin with app.ceph_baseurl - if not ep.startswith(app.ceph_baseurl): - return make_response(fmt, '', 'Page not found', 404) - - rel_ep = ep[len(app.ceph_baseurl) + 1:] - - # Extensions override Accept: headers override defaults - if not fmt: - if 'application/json' in flask.request.accept_mimetypes.values(): - fmt = 'json' - elif 'application/xml' in flask.request.accept_mimetypes.values(): - fmt = 'xml' - - prefix = '' - pgid = None - cmdtarget = 'mon', '' - - if target: - # got tell/; 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 - - # prefix does not include tell// - 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 ep not in app.ceph_urls: - helptext = show_human_help(prefix) - if helptext: - resp = flask.make_response(helptext, 400) - resp.headers['Content-Type'] = 'text/html' - return resp - else: - return make_response(fmt, '', 'Invalid endpoint ' + ep, 400) - - found = None - exc = '' - for urldict in app.ceph_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'] = 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) - - for _ in range(DEFAULT_TRIES): - ret, outbuf, outs = json_command(app.ceph_cluster, prefix=prefix, - target=cmdtarget, - inbuf=flask.request.data, - argdict=argdict, - timeout=DEFAULT_TIMEOUT) - if ret != -errno.EINTR: - break - else: - return make_response(fmt, '', - 'Timedout: {0} ({1})'.format(outs, ret), 504) - if ret: - return make_response(fmt, '', 'Error: {0} ({1})'.format(outs, ret), 400) - - response = make_response(fmt, outbuf, outs or 'OK', 200) - if fmt: - contenttype = 'application/' + fmt.replace('-pretty', '') - else: - contenttype = 'text/plain' - response.headers['Content-Type'] = contenttype - return response - - -# -# Main entry point from wrapper/WSGI server: call with cmdline args, -# get back the WSGI app entry point -# -def generate_app(conf, cluster, clientname, clientid, args): - addr, port = api_setup(app, conf, cluster, clientname, clientid, args) - app.ceph_addr = addr - app.ceph_port = port - return app