Skip to content
Snippets Groups Projects
api.py 13.8 KiB
Newer Older
import sys
import traceback
import webapp2
import webapp2_extras.routes

Nathaniel Kofalt's avatar
Nathaniel Kofalt committed
from . import encoder
Renzo Frigato's avatar
Renzo Frigato committed
from . import root
from . import util
from . import config
from .request import SciTranRequest
Nathaniel Kofalt's avatar
Nathaniel Kofalt committed
from .centralclient               import CentralClient
from .download                    import Download
from .handlers.collectionshandler import CollectionsHandler
from .handlers.confighandler      import Config, Version
from .handlers.containerhandler   import ContainerHandler
from .handlers.devicehandler      import DeviceHandler
from .handlers.grouphandler       import GroupHandler
from .handlers.listhandler        import AnalysesHandler, ListHandler, FileListHandler, NotesListHandler, PermissionsListHandler, TagsListHandler
from .handlers.reporthandler      import ReportHandler
from .handlers.resolvehandler     import ResolveHandler
from .handlers.schemahandler      import SchemaHandler
from .handlers.searchhandler      import SearchHandler
from .handlers.userhandler        import UserHandler
from .jobs.handlers               import JobsHandler, JobHandler, GearsHandler, GearHandler, RulesHandler
from .upload                      import Upload

Gunnar Schaefer's avatar
Gunnar Schaefer committed
log = config.log

try:
    import uwsgi
except ImportError:
    uwsgi = None

Nathaniel Kofalt's avatar
Nathaniel Kofalt committed
routing_regexes = {
Renzo Frigato's avatar
Renzo Frigato committed

Nathaniel Kofalt's avatar
Nathaniel Kofalt committed
    # Group ID: 2-32 characters of form [0-9a-z.@_-]. Start and ends with alphanum.
    'gid': '[0-9a-z][0-9a-z.@_-]{0,30}[0-9a-z]',
Renzo Frigato's avatar
Renzo Frigato committed

Nathaniel Kofalt's avatar
Nathaniel Kofalt committed
    # Container ID: 24-character hex
    'cid': '[0-9a-f]{24}',
Renzo Frigato's avatar
Renzo Frigato committed

Nathaniel Kofalt's avatar
Nathaniel Kofalt committed
    # Site ID: <= 24-character alphanum
    'sid': '[0-9a-z_]{0,24}',
Nathaniel Kofalt's avatar
Nathaniel Kofalt committed
    # User ID: any length, [0-9a-z.@_-]
    'uid': '[0-9a-z.@_-]*',
Nathaniel Kofalt's avatar
Nathaniel Kofalt committed
    # Container name
    'cname': 'projects|sessions|acquisitions|collections',

    # Tag name
    'tag': '[^/]{1,32}',

    # Filename
    'fname': '[^/]+',

    # Note ID
    'nid': '[0-9a-f]{24}',

    # Schema path
    'schema': r'[^/.]{3,60}/[^/.]{3,60}\.json'
Nathaniel Kofalt's avatar
Nathaniel Kofalt committed
def route(path, target, h=None, m=None, name=None):

    # https://webapp2.readthedocs.io/en/latest/api/webapp2.html#webapp2.Route
    return webapp2.Route(
        # re.compile(path)
        path.format(**routing_regexes),
        target,
        handler_method=h,
        methods=m,
        name=name
    )

def prefix(path, routes):

    # https://webapp2.readthedocs.io/en/latest/api/webapp2_extras/routes.html#webapp2_extras.routes.PathPrefixRoute
    return webapp2_extras.routes.PathPrefixRoute(
        path.format(**routing_regexes),
        routes
    )

endpoints = [
    route('/api',                  root.Root),
    prefix('/api', [

        # System configuration

        route('/config',           Config,              m=['GET']),
        route('/config.js',        Config,  h='get_js', m=['GET']),
        route('/version',          Version,             m=['GET']),


        # General-purpose upload & download

        route('/download',                              Download, h='download',              m=['GET', 'POST']),
        route('/upload/<strategy:label|uid|uid-match>', Upload,   h='upload',                m=['POST']),
        route('/clean-packfiles',                       Upload,   h='clean_packfile_tokens', m=['POST']),
        route('/engine',                                Upload,   h='engine',                m=['POST']),


        # Top-level endpoints

        route('/resolve',                           ResolveHandler, h='resolve', m=['POST']),
        route('/schemas/<schema:{schema}>',         SchemaHandler,               m=['GET']),
        route('/report/<report_type:site|project>', ReportHandler,               m=['GET']),


        # Search

        route('/search',                     SearchHandler, h='advanced_search', m=['POST']),
        route('/search/files',               SearchHandler, h='get_datatree',    m=['GET']),
        route('/search/<cont_name:{cname}>', SearchHandler,                      m=['GET']),


        # Users

        route( '/users',                   UserHandler, 'get_all', m=['GET']),
        route( '/users',                   UserHandler,            m=['POST']),
        prefix('/users', [
            route('/self',                 UserHandler, h='self',            m=['GET']),
            route('/self/avatar',          UserHandler, h='self_avatar',     m=['GET']),
            route('/self/key',             UserHandler, h='generate_api_key',m=['POST']),

            route('/<_id:{uid}>',          UserHandler),
            route('/<uid:{uid}>/groups',   GroupHandler,                h='get_all',          m=['GET']),
            route('/<uid:{uid}>/avatar',   UserHandler,                 h='avatar',           m=['GET']),
            route('/<uid:{uid}>/<cont_name:{cname}>', ContainerHandler, h='get_all_for_user', m=['GET']),

        ]),


        # Jobs & gears

        route( '/jobs',                    JobsHandler),
        prefix('/jobs', [
            route('/next',                 JobsHandler, h='next',       m=['GET']),
            route('/stats',                JobsHandler, h='stats',      m=['GET']),
            route('/reap',                 JobsHandler, h='reap_stale', m=['POST']),
            route('/add',                  JobsHandler, h='add',        m=['POST']),
            route('/<:[^/]+>',             JobHandler),
            route('/<:[^/]+>/config.json', JobHandler,  h='get_config'),
            route('/<:[^/]+>/retry',       JobHandler,  h='retry',      m=['POST']),
        ]),

        route('/gears',                                  GearsHandler),
        prefix('/gears', [
            route('/<:[^/]+>',                           GearHandler),
            route('/<:[^/]+>/invocation',                GearHandler, h='get_invocation'),
            route('/<:[^/]+>/suggest/<:[^/]+>/<:[^/]+>', GearHandler, h='suggest'),
        ]),

        route('/rules', RulesHandler),


        # Devices

        route( '/devices',              DeviceHandler, h='get_all',    m=['GET']),
        route( '/devices',              DeviceHandler,                 m=['POST']),
        prefix('/devices', [
            route('/status',            DeviceHandler, h='get_status', m=['GET']),
            route('/self',              DeviceHandler, h='get_self',   m=['GET']),
            route('/<device_id:[^/]+>', DeviceHandler,                 m=['GET']),
        ]),


        # Groups

        route('/groups',             GroupHandler, h='get_all', m=['GET']),
        route('/groups',             GroupHandler,              m=['POST']),
        route('/groups/<_id:{gid}>', GroupHandler,              m=['GET', 'DELETE', 'PUT']),

        prefix('/<cont_name:groups>', [
            route('/<cid:{gid}>/<list_name:roles>',                          ListHandler,     m=['POST']),
            route('/<cid:{gid}>/<list_name:roles>/<site:{sid}>/<_id:{uid}>', ListHandler,     m=['GET', 'PUT', 'DELETE']),

            route('/<cid:{gid}>/<list_name:tags>',                           TagsListHandler, m=['POST']),
            route('/<cid:{gid}>/<list_name:tags>/<value:{tag}>',             TagsListHandler, m=['GET', 'PUT', 'DELETE']),
        ]),


        # Projects

        prefix('/projects', [
            route('/groups',               ContainerHandler, h='get_groups_with_project',      m=['GET']),
            route('/recalc',               ContainerHandler, h='calculate_project_compliance', m=['POST']),
            route('/<cid:{cid}>/template', ContainerHandler, h='set_project_template',         m=['POST']),
            route('/<cid:{cid}>/template', ContainerHandler, h='delete_project_template',      m=['DELETE']),
            route('/<cid:{cid}>/recalc',   ContainerHandler, h='calculate_project_compliance', m=['POST']),
        ]),


        # Sessions

        route('/sessions/<cid:{cid}>/jobs',    ContainerHandler, h='get_jobs', m=['GET']),


        # Collections

        route( '/collections',                 CollectionsHandler, h='get_all',                    m=['GET']),
        route( '/collections',                 CollectionsHandler,                                 m=['POST']),
        prefix('/collections', [
            route('/curators',                 CollectionsHandler, h='curators',                   m=['GET']),
            route('/<cid:{cid}>',              CollectionsHandler,                                 m=['GET', 'PUT', 'DELETE']),
            route('/<cid:{cid}>/sessions',     CollectionsHandler, h='get_sessions',               m=['GET']),
            route('/<cid:{cid}>/acquisitions', CollectionsHandler, h='get_acquisitions',           m=['GET']),
        ]),


        # Collections / Projects

        prefix('/<cont_name:collections|projects>', [
            prefix('/<cid:{cid}>', [
                route('/<list_name:permissions>',                          PermissionsListHandler, m=['POST']),
                route('/<list_name:permissions>/<site:{sid}>/<_id:{uid}>', PermissionsListHandler, m=['GET', 'PUT', 'DELETE']),
            ]),
        ]),


        # Containers

        route('/<cont_name:{cname}>', ContainerHandler, name='cont_list', h='get_all', m=['GET']),
        route('/<cont_name:{cname}>', ContainerHandler, m=['POST']),

        prefix('/<cont_name:{cname}>', [
            route('/<cid:{cid}>',                          ContainerHandler,                    m=['GET','PUT','DELETE']),

            prefix('/<cid:{cid}>', [

                route('/<list_name:tags>',                 TagsListHandler,                     m=['POST']),
                route('/<list_name:tags>/<value:{tag}>',   TagsListHandler,                     m=['GET', 'PUT', 'DELETE']),

                route('/packfile-start',                   FileListHandler, h='packfile_start', m=['POST']),
                route('/packfile',                         FileListHandler, h='packfile',       m=['POST']),
                route('/packfile-end',                     FileListHandler, h='packfile_end'),
                route('/<list_name:files>',                FileListHandler,                     m=['POST']),
                route('/<list_name:files>/<name:{fname}>', FileListHandler,                     m=['GET', 'DELETE']),


                route('/<list_name:analyses>', AnalysesHandler, m=['POST']),
                # Could be in a prefix. Had weird syntax highlighting issues so leaving for another day
                route('/<list_name:analyses>/<_id:{cid}>',                       AnalysesHandler,                  m=['GET', 'DELETE']),
                route('/<list_name:analyses>/<_id:{cid}>/files',                 AnalysesHandler, h='download',    m=['GET']),
                route('/<list_name:analyses>/<_id:{cid}>/files/<name:{fname}>',  AnalysesHandler, h='download',    m=['GET']),
                route('/<list_name:analyses>/<_id:{cid}>/notes',                 AnalysesHandler, h='add_note',    m=['POST']),
                route('/<list_name:analyses>/<_id:{cid}>/notes/<note_id:{cid}>', AnalysesHandler, h='delete_note', m=['DELETE']),
                route('/<list_name:notes>',                                      NotesListHandler,                 m=['POST']),
                route('/<list_name:notes>/<_id:{nid}>',                          NotesListHandler, name='notes',   m=['GET', 'PUT', 'DELETE']),
            ])
        ]),


        # Misc (to be cleaned up later)

        route('/<par_cont_name:groups>/<par_id:{gid}>/<cont_name:projects>', ContainerHandler, h='get_all', m=['GET']),
        route('/<par_cont_name:{cname}>/<par_id:{cid}>/<cont_name:{cname}>', ContainerHandler, h='get_all', m=['GET']),


        # Multi - site
        route('/sites',    CentralClient, h='sites',    m=['GET']),
        route('/register', CentralClient, h='register', m=['POST']),

def dispatcher(router, request, response):
    try:
        if uwsgi is not None:
            uwsgi.set_logvar('request_id', request.id)
    except: # pylint: disable=bare-except
        request.logger.error("Error setting request_id log var", exc_info=True)

    try:
        rv = router.default_dispatcher(request, response)
        if rv is not None:
Nathaniel Kofalt's avatar
Nathaniel Kofalt committed
            response.write(json.dumps(rv, default=encoder.custom_json_serializer))
            response.headers['Content-Type'] = 'application/json; charset=utf-8'
    except webapp2.HTTPException as e:
        util.send_json_http_exception(response, str(e), e.code)
    except Exception as e: # pylint: disable=broad-except
        request.logger.error("Error dispatching request", exc_info=True)
        if config.get_item('core', 'debug'):
            message = traceback.format_exc()
        else:
            message = 'Internal Server Error'
        util.send_json_http_exception(response, message, 500)
def app_factory(*_, **__):
    # pylint: disable=protected-access,unused-argument

    # don't use config.get_item() as we don't want to require the database at startup
Nathaniel Kofalt's avatar
Nathaniel Kofalt committed
    application = webapp2.WSGIApplication(endpoints, debug=config.__config['core']['debug'])
    application.router.set_dispatcher(dispatcher)
    application.request_class = SciTranRequest
    if os.environ.get("SCITRAN_RUNTIME_COVERAGE") == "true":
        start_coverage()
    # configure new relic
    if config.__config['core']['newrelic']:
        try:
            import newrelic.agent, newrelic.api.exceptions
            newrelic.agent.initialize(config.__config['core']['newrelic'])
            application = newrelic.agent.WSGIApplicationWrapper(application)
            log.info('New Relic detected and loaded. Monitoring enabled.')
        except ImportError:
            log.critical('New Relic libraries not found.')
            sys.exit(1)
        except newrelic.api.exceptions.ConfigurationError:
            log.critical('New Relic detected, but configuration invalid.')
            sys.exit(1)

    return application

# Functions to enable code coverage when API is started for testing
def start_coverage():
    import coverage
    config.log.info("Enabling code coverage")
    cov = coverage.coverage(source=["api"], data_suffix="integration-tests")
    cov.start()
    atexit.register(save_coverage, cov)

def save_coverage(cov):
    config.log.info("Saving coverage")
    cov.stop()
    cov.save()