]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
ceph-rest-api: make main program be "shell" around WSGI guts
authorDan Mick <dan.mick@inktank.com>
Fri, 12 Jul 2013 00:17:39 +0000 (17:17 -0700)
committerDan Mick <dan.mick@inktank.com>
Fri, 12 Jul 2013 00:17:39 +0000 (17:17 -0700)
Move WSGI guts to pybind/ceph_rest_api.py for importability
into WSGI server frameworks.

Signed-off-by: Dan Mick <dan.mick@inktank.com>
src/ceph-rest-api
src/pybind/ceph_rest_api.py [new file with mode: 0755]

index 7a4db38915d1742c866c4ea1c85b4936458d5468..712286cd09b123950d897f51c6ff0bc6f8a1c881 100755 (executable)
@@ -25,423 +25,7 @@ if MYDIR.endswith('src') and \
         os.execvp('python', ['python'] + sys.argv)
     sys.path.insert(0, os.path.join(MYDIR, 'pybind'))
 
-import argparse
-import collections
-import ConfigParser
-import errno
-import json
-import logging
-import logging.handlers
-import rados
-import textwrap
-import xml.etree.ElementTree
-import xml.sax.saxutils
-
-import flask
-from ceph_argparse import *
-
-#
-# Globals
-#
-
-APPNAME = '__main__'
-DEFAULT_BASEURL = '/api/v0.1'
-DEFAULT_ADDR = '0.0.0.0:5000'
-DEFAULT_LOG_LEVEL = 'warning'
-DEFAULT_CLIENTNAME = 'client.restapi'
-DEFAULT_LOG_FILE = '/var/log/ceph/' + DEFAULT_CLIENTNAME + '.log'
-
-app = flask.Flask(APPNAME)
-
-LOGLEVELS = {
-    'critical':logging.CRITICAL,
-    'error':logging.ERROR,
-    'warning':logging.WARNING,
-    'info':logging.INFO,
-    'debug':logging.DEBUG,
-}
-
-
-# my globals, in a named tuple for usage clarity
-
-glob = collections.namedtuple('gvars',
-    'args cluster urls sigdict baseurl clientname')
-glob.args = None
-glob.cluster = None
-glob.urls = {}
-glob.sigdict = {}
-glob.baseurl = ''
-glob.clientname = ''
-
-def parse_args():
-    parser = argparse.ArgumentParser(description="Ceph REST API webapp")
-    parser.add_argument('-c', '--conf', help='Ceph configuration file')
-    parser.add_argument('-n', '--name', help='Ceph client config/key name')
-
-    return parser.parse_args()
-
-def load_conf(conffile=None):
-    import contextlib
-
-    class _TrimIndentFile(object):
-        def __init__(self, fp):
-            self.fp = fp
-
-        def readline(self):
-            line = self.fp.readline()
-            return line.lstrip(' \t')
-
-
-    def _optionxform(s):
-        s = s.replace('_', ' ')
-        s = '_'.join(s.split())
-        return s
-
-
-    def parse(fp):
-        cfg = ConfigParser.RawConfigParser()
-        cfg.optionxform = _optionxform
-        ifp = _TrimIndentFile(fp)
-        cfg.readfp(ifp)
-        return cfg
-
-
-    def load(path):
-        f = file(path)
-        with contextlib.closing(f):
-            return parse(f)
-
-    # XXX this should probably use cluster name
-    if conffile:
-        return load(conffile)
-    elif 'CEPH_CONF' in os.environ:
-        conffile = os.environ['CEPH_CONF']
-    elif os.path.exists('/etc/ceph/ceph.conf'):
-        conffile = '/etc/ceph/ceph.conf'
-    elif os.path.exists(os.path.expanduser('~/.ceph/ceph.conf')):
-        conffile = os.path.expanduser('~/.ceph/ceph.conf')
-    elif os.path.exists('ceph.conf'):
-        conffile = 'ceph.conf'
-    else:
-        return None
-
-    return load(conffile)
-
-def get_conf(cfg, key):
-    try:
-        return cfg.get(glob.clientname, 'restapi_' + key)
-    except ConfigParser.NoOptionError:
-        return None
-
-
-# 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
-
-def api_setup():
-    """
-    Initialize the running instance.  Open the cluster, get the command
-    signatures, module,, perms, and help; stuff them away in the glob.urls
-    dict.
-    """
-
-    glob.args = parse_args()
-
-    conffile = glob.args.conf or ''
-    if glob.args.name:
-        glob.clientname = glob.args.name
-        glob.logfile = '/var/log/ceph' + glob.clientname + '.log'
-
-    glob.clientname = glob.args.name or DEFAULT_CLIENTNAME
-    glob.cluster = rados.Rados(name=glob.clientname, conffile=conffile)
-    glob.cluster.connect()
-
-    cfg = load_conf(conffile)
-    glob.baseurl = get_conf(cfg, 'base_url') or DEFAULT_BASEURL
-    if glob.baseurl.endswith('/'):
-        glob.baseurl
-    addr = get_conf(cfg, 'public_addr') or DEFAULT_ADDR
-    addrport = addr.rsplit(':', 1)
-    addr = addrport[0]
-    if len(addrport) > 1:
-        port = addrport[1]
-    else:
-        port = DEFAULT_ADDR.rsplit(':', 1)
-    port = int(port)
-
-    loglevel = get_conf(cfg, 'log_level') or 'warning'
-    logfile = get_conf(cfg, 'log_file') or glob.logfile
-    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'))
-
-    ret, outbuf, outs = json_command(glob.cluster,
-                                     prefix='get_command_descriptions')
-    if ret:
-        app.logger.error('Can\'t contact cluster for command descriptions: %s',
-                         outs)
-        sys.exit(1)
-
-    try:
-        glob.sigdict = parse_json_funcsigs(outbuf, 'rest')
-    except Exception as e:
-        app.logger.error('Can\'t parse command descriptions: %s', e)
-        sys.exit(1)
-
-    # glob.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)
-    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
-        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
-    app.logger.debug("urls added: %d", len(glob.urls))
-
-    app.add_url_rule('/<path:catchall_path>', '/<path:catchall_path>',
-                     handler, methods=['GET', 'PUT'])
-    return addr, port
-
-
-def generate_url_and_params(sig):
-    """
-    Digest command signature from cluster; generate an absolute
-    (including glob.baseurl) endpoint from all the prefix words,
-    and a dictionary of non-prefix parameters
-    """
-
-    url = ''
-    params = []
-    for desc in sig:
-        if desc.t == CephPrefix:
-            url += '/' + desc.instance.prefix
-        elif desc.t == CephChoices and \
-             len(desc.instance.strings) == 1 and \
-             desc.req and \
-             not str(desc.instance).startswith('--'):
-                url += '/' + str(desc.instance)
-        else:
-            params.append(desc)
-    return glob.baseurl + url, params
-
-
-def concise_sig_for_uri(sig):
-    """
-    Return a generic description of how one would send a REST request for sig
-    """
-    prefix = []
-    args = []
-    for d in sig:
-        if d.t == CephPrefix:
-            prefix.append(d.instance.prefix)
-        else:
-            args.append(d.name + '=' + str(d))
-    sig = '/'.join(prefix)
-    if args:
-        sig += '?' + '&'.join(args)
-    return sig
-
-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.
-    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'])
-        if concise.startswith(prefix):
-            line = ['<tr><td>']
-            wrapped_sig = textwrap.wrap(concise_sig_for_uri(cmdsig['sig']), 40)
-            for sigline in wrapped_sig:
-                line.append(flask.escape(sigline) + '\n')
-            line.append('</td><td>')
-            line.append(permmap[cmdsig['perm']])
-            line.append('</td><td>')
-            line.append(flask.escape(cmdsig['help']))
-            line.append('</td></tr>\n')
-            s += ''.join(line)
-
-    s += '</table></body></html>'
-    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(glob.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.  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:
-            # 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 = '''
-<response>
-  <output>
-    {0}
-  </output>
-  <status>
-    {1}
-  </status>
-</response>'''.format(response, xml.sax.saxutils.escape(statusmsg))
-
-    return flask.make_response(response, errorcode)
-
-def handler(catchall_path=None, fmt=None):
-    """
-    Main endpoint handler; generic for every endpoint
-    """
-
-    if (catchall_path):
-        ep = catchall_path.replace('.<fmt>', '')
-    else:
-        ep = flask.request.endpoint.replace('.<fmt>', '')
-
-    if ep[0] != '/':
-        ep = '/' + ep
-
-    # 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'
-
-    # demand that endpoint begin with glob.baseurl
-    if not ep.startswith(glob.baseurl):
-        return make_response(fmt, '', 'Page not found', 404)
-
-    relative_endpoint = ep[len(glob.baseurl)+1:]
-    prefix = ' '.join(relative_endpoint.split('/')).strip()
-
-    # show "match as much as you gave me" help for unknown endpoints
-    if not ep in glob.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)
-
-    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 = {}
-
-
-    argdict['format'] = fmt or 'plain'
-    argdict['module'] = urldict['module']
-    argdict['perm'] = urldict['perm']
-
-    app.logger.debug('sending command prefix %s argdict %s', prefix, argdict)
-    ret, outbuf, outs = json_command(glob.cluster, prefix=prefix,
-                                     inbuf=flask.request.data, argdict=argdict)
-    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
-#
+from ceph_rest_api import api_setup, app
 
 addr, port = api_setup()
 
diff --git a/src/pybind/ceph_rest_api.py b/src/pybind/ceph_rest_api.py
new file mode 100755 (executable)
index 0000000..755ba97
--- /dev/null
@@ -0,0 +1,418 @@
+#!/usr/bin/python
+# vim: ts=4 sw=4 smarttab expandtab
+
+import os
+import sys
+import argparse
+import collections
+import ConfigParser
+import errno
+import json
+import logging
+import logging.handlers
+import rados
+import textwrap
+import xml.etree.ElementTree
+import xml.sax.saxutils
+
+import flask
+from ceph_argparse import *
+
+#
+# Globals
+#
+
+APPNAME = '__main__'
+DEFAULT_BASEURL = '/api/v0.1'
+DEFAULT_ADDR = '0.0.0.0:5000'
+DEFAULT_LOG_LEVEL = 'warning'
+DEFAULT_CLIENTNAME = 'client.restapi'
+DEFAULT_LOG_FILE = '/var/log/ceph/' + DEFAULT_CLIENTNAME + '.log'
+
+app = flask.Flask(APPNAME)
+
+LOGLEVELS = {
+    'critical':logging.CRITICAL,
+    'error':logging.ERROR,
+    'warning':logging.WARNING,
+    'info':logging.INFO,
+    'debug':logging.DEBUG,
+}
+
+
+# my globals, in a named tuple for usage clarity
+
+glob = collections.namedtuple('gvars',
+    'args cluster urls sigdict baseurl clientname')
+glob.args = None
+glob.cluster = None
+glob.urls = {}
+glob.sigdict = {}
+glob.baseurl = ''
+glob.clientname = ''
+
+def parse_args():
+    parser = argparse.ArgumentParser(description="Ceph REST API webapp")
+    parser.add_argument('-c', '--conf', help='Ceph configuration file')
+    parser.add_argument('-n', '--name', help='Ceph client config/key name')
+
+    return parser.parse_args()
+
+def load_conf(conffile=None):
+    import contextlib
+
+    class _TrimIndentFile(object):
+        def __init__(self, fp):
+            self.fp = fp
+
+        def readline(self):
+            line = self.fp.readline()
+            return line.lstrip(' \t')
+
+
+    def _optionxform(s):
+        s = s.replace('_', ' ')
+        s = '_'.join(s.split())
+        return s
+
+
+    def parse(fp):
+        cfg = ConfigParser.RawConfigParser()
+        cfg.optionxform = _optionxform
+        ifp = _TrimIndentFile(fp)
+        cfg.readfp(ifp)
+        return cfg
+
+
+    def load(path):
+        f = file(path)
+        with contextlib.closing(f):
+            return parse(f)
+
+    # XXX this should probably use cluster name
+    if conffile:
+        return load(conffile)
+    elif 'CEPH_CONF' in os.environ:
+        conffile = os.environ['CEPH_CONF']
+    elif os.path.exists('/etc/ceph/ceph.conf'):
+        conffile = '/etc/ceph/ceph.conf'
+    elif os.path.exists(os.path.expanduser('~/.ceph/ceph.conf')):
+        conffile = os.path.expanduser('~/.ceph/ceph.conf')
+    elif os.path.exists('ceph.conf'):
+        conffile = 'ceph.conf'
+    else:
+        return None
+
+    return load(conffile)
+
+def get_conf(cfg, key):
+    try:
+        return cfg.get(glob.clientname, 'restapi_' + key)
+    except ConfigParser.NoOptionError:
+        return None
+
+
+# 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
+
+def api_setup():
+    """
+    Initialize the running instance.  Open the cluster, get the command
+    signatures, module,, perms, and help; stuff them away in the glob.urls
+    dict.
+    """
+
+    glob.args = parse_args()
+
+    conffile = glob.args.conf or ''
+    if glob.args.name:
+        glob.clientname = glob.args.name
+        glob.logfile = '/var/log/ceph' + glob.clientname + '.log'
+
+    glob.clientname = glob.args.name or DEFAULT_CLIENTNAME
+    glob.cluster = rados.Rados(name=glob.clientname, conffile=conffile)
+    glob.cluster.connect()
+
+    cfg = load_conf(conffile)
+    glob.baseurl = get_conf(cfg, 'base_url') or DEFAULT_BASEURL
+    if glob.baseurl.endswith('/'):
+        glob.baseurl
+    addr = get_conf(cfg, 'public_addr') or DEFAULT_ADDR
+    addrport = addr.rsplit(':', 1)
+    addr = addrport[0]
+    if len(addrport) > 1:
+        port = addrport[1]
+    else:
+        port = DEFAULT_ADDR.rsplit(':', 1)
+    port = int(port)
+
+    loglevel = get_conf(cfg, 'log_level') or 'warning'
+    logfile = get_conf(cfg, 'log_file') or glob.logfile
+    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'))
+
+    ret, outbuf, outs = json_command(glob.cluster,
+                                     prefix='get_command_descriptions')
+    if ret:
+        app.logger.error('Can\'t contact cluster for command descriptions: %s',
+                         outs)
+        sys.exit(1)
+
+    try:
+        glob.sigdict = parse_json_funcsigs(outbuf, 'rest')
+    except Exception as e:
+        app.logger.error('Can\'t parse command descriptions: %s', e)
+        sys.exit(1)
+
+    # glob.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)
+    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
+        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
+    app.logger.debug("urls added: %d", len(glob.urls))
+
+    app.add_url_rule('/<path:catchall_path>', '/<path:catchall_path>',
+                     handler, methods=['GET', 'PUT'])
+    return addr, port
+
+
+def generate_url_and_params(sig):
+    """
+    Digest command signature from cluster; generate an absolute
+    (including glob.baseurl) endpoint from all the prefix words,
+    and a dictionary of non-prefix parameters
+    """
+
+    url = ''
+    params = []
+    for desc in sig:
+        if desc.t == CephPrefix:
+            url += '/' + desc.instance.prefix
+        elif desc.t == CephChoices and \
+             len(desc.instance.strings) == 1 and \
+             desc.req and \
+             not str(desc.instance).startswith('--'):
+                url += '/' + str(desc.instance)
+        else:
+            params.append(desc)
+    return glob.baseurl + url, params
+
+
+def concise_sig_for_uri(sig):
+    """
+    Return a generic description of how one would send a REST request for sig
+    """
+    prefix = []
+    args = []
+    for d in sig:
+        if d.t == CephPrefix:
+            prefix.append(d.instance.prefix)
+        else:
+            args.append(d.name + '=' + str(d))
+    sig = '/'.join(prefix)
+    if args:
+        sig += '?' + '&'.join(args)
+    return sig
+
+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.
+    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'])
+        if concise.startswith(prefix):
+            line = ['<tr><td>']
+            wrapped_sig = textwrap.wrap(concise_sig_for_uri(cmdsig['sig']), 40)
+            for sigline in wrapped_sig:
+                line.append(flask.escape(sigline) + '\n')
+            line.append('</td><td>')
+            line.append(permmap[cmdsig['perm']])
+            line.append('</td><td>')
+            line.append(flask.escape(cmdsig['help']))
+            line.append('</td></tr>\n')
+            s += ''.join(line)
+
+    s += '</table></body></html>'
+    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(glob.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.  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:
+            # 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 = '''
+<response>
+  <output>
+    {0}
+  </output>
+  <status>
+    {1}
+  </status>
+</response>'''.format(response, xml.sax.saxutils.escape(statusmsg))
+
+    return flask.make_response(response, errorcode)
+
+def handler(catchall_path=None, fmt=None):
+    """
+    Main endpoint handler; generic for every endpoint
+    """
+
+    if (catchall_path):
+        ep = catchall_path.replace('.<fmt>', '')
+    else:
+        ep = flask.request.endpoint.replace('.<fmt>', '')
+
+    if ep[0] != '/':
+        ep = '/' + ep
+
+    # 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'
+
+    # demand that endpoint begin with glob.baseurl
+    if not ep.startswith(glob.baseurl):
+        return make_response(fmt, '', 'Page not found', 404)
+
+    relative_endpoint = ep[len(glob.baseurl)+1:]
+    prefix = ' '.join(relative_endpoint.split('/')).strip()
+
+    # show "match as much as you gave me" help for unknown endpoints
+    if not ep in glob.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)
+
+    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 = {}
+
+
+    argdict['format'] = fmt or 'plain'
+    argdict['module'] = urldict['module']
+    argdict['perm'] = urldict['perm']
+
+    app.logger.debug('sending command prefix %s argdict %s', prefix, argdict)
+    ret, outbuf, outs = json_command(glob.cluster, prefix=prefix,
+                                     inbuf=flask.request.data, argdict=argdict)
+    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