def get_users(self, user_ids):
        url = self.config['auth-url']

        header = {
            'Accept': 'application/json',
            'Authorization': self.token,
        }

        endpoint = url + '/api/V2/users/?list=' + ','.join(user_ids)

        response = requests.get(endpoint,
                                headers=header,
                                timeout=self.timeout / 1000)
        if response.status_code != 200:
            raise ServiceError(code=40000,
                               message='Error fetching users',
                               data={'user_id': user_ids})
        else:
            try:
                result = response.json()
                retval = dict()
                for username, realname in result.items():
                    retval[username] = {'realname': realname}
                return retval
            except Exception as err:
                raise ServiceError(code=40000,
                                   message='Bad response',
                                   data={
                                       'user_id': user_ids,
                                       'original_message': str(err)
                                   })
예제 #2
0
    def get_user(self):
        url = self.config['auth-url']

        header = {
            'Accept': 'application/json',
            'Authorization': self.token,
        }

        endpoint = url + '/api/V2/me'

        response = requests.get(endpoint, headers=header, timeout=10)
        if response.status_code != 200:
            raise ServiceError(code=40000, message='Error fetching me')
        else:
            try:
                result = response.json()
                return {
                    'username': result['user'],
                    'realname': result['display']
                }

            except Exception as err:
                raise ServiceError(code=40000,
                                   message='Bad response',
                                   data={'original_message': str(err)})
예제 #3
0
 def cancel_job(self, params):
     api = EE2Api(url=self.config['ee2-url'],
                  token=self.token,
                  timeout=self.timeout)
     admin = params.get('admin', False)
     if admin:
         as_admin = 1
         terminated_code = params.get('code', 1)
     else:
         as_admin = 0
         terminated_code = params.get('code', 0)
     try:
         api.cancel_job({
             'job_id': params['job_id'],
             'terminated_code': terminated_code,
             'as_admin': as_admin
         })
         return {'canceled': True}
     except ServiceError as se:
         if re.search(('A job with status .+ cannot be terminated. '
                       'It is already cancelled.'), se.message):
             return {'canceled': False}
         elif re.search('Cannot find job with ids:', se.message):
             raise ServiceError(
                 code=10,
                 message="The job specified for cancelation does not exist",
                 data={'job_id': params['job_id']})
         else:
             raise se
예제 #4
0
 def check_job_canceled(self, params):
     try:
         return self.rpc.call_func('check_job_canceled', params)
     except ServiceError:
         raise
     except Exception as err:
         raise ServiceError(code=1,
                            message='Unknown error',
                            data={'original_message': str(err)})
예제 #5
0
 def get_job_status(self, params):
     try:
         return self.rpc.call_func('get_job_status', params)
     except ServiceError:
         raise
     except Exception as err:
         raise ServiceError(code=1,
                            message='Unknown error',
                            data={'original_message': str(err)})
예제 #6
0
 def cancel_job(self, params):
     try:
         return self.rpc.call_func('cancel_job', params)
     except ServiceError:
         raise
     except Exception as ex:
         raise ServiceError(code=40000,
                            message='Unknown error',
                            data={'original_message': str(ex)})
예제 #7
0
 def status(self):
     try:
         return self.rpc.call_func('status')
     except ServiceError:
         raise
     except Exception as err:
         raise ServiceError(code=40000,
                            message='Unknown error',
                            data={'original_message': str(err)})
예제 #8
0
 def get_client_groups(self):
     try:
         return self.rpc.call_func('get_client_groups')
     except ServiceError:
         raise
     except Exception as err:
         raise ServiceError(code=1,
                            message='Unknown error',
                            data={'original_message': str(err)})
예제 #9
0
 def check_jobs_date_range_for_all(self, params):
     try:
         return self.rpc.call_func('check_jobs_date_range_for_all', params)
     except ServiceError:
         raise
     except Exception as ex:
         raise ServiceError(code=40000,
                            message='Unknown error',
                            data={'original_message': str(ex)})
예제 #10
0
 def is_admin(self):
     try:
         result = self.rpc.call_func('is_admin')
         return result
     except ServiceError:
         raise
     except Exception as ex:
         raise ServiceError(code=40000,
                            message='Unknown error',
                            data={'original_message': str(ex)})
    def get_job_log(self, job_id, offset, limit, search=None, level=None):
        # TODO: enforce perms

        # First, get the job and ensure the user has access to the logs.
        jobs_collection = self.db['jobs']
        raw_job = jobs_collection.find_one({'job_id': job_id}, {'_id': False})

        if raw_job is None:
            raise ServiceError(
                code=10,
                message='Job {} could not be found'.format(job_id),
                data={'job_id': job_id})

        job = json.loads(json_util.dumps(raw_job))
        if job['owner']['username'] != self.username:
            raise ServiceError(
                code=40,
                message='Access denied to job {}'.format(job_id),
                data={
                    'job_id': job_id,
                    'username': self.username
                })

        # Now get the logs
        logs_collection = self.db['job_logs']
        filters = []
        filters.append({'job_id': job_id})
        cursor = logs_collection.find({'$and': filters})
        total_count = cursor.count()
        cursor.sort([('id', 1)])
        cursor.skip(offset)
        cursor.limit(limit)

        log = json.loads(json_util.dumps(cursor))

        entries = [
            raw_log_entry_to_entry(line, index, offset)
            for index, line in enumerate(log)
        ]

        return {'job': job, 'log': entries, 'total_count': total_count}
    def get_job(self, job_id):
        # First, get the job and ensure the user has access to the logs.
        jobs_collection = self.db['jobs']
        raw_job = jobs_collection.find_one({'job_id': job_id}, {'_id': False})

        if raw_job is None:
            raise ServiceError(
                code=10,
                message='Job {} could not be found'.format(job_id),
                data={'job_id': job_id})

        job = json.loads(json_util.dumps(raw_job))
        if job['owner']['username'] != self.username:
            raise ServiceError(
                code=40,
                message='Access denied to job {}'.format(job_id),
                data={
                    'job_id': job_id,
                    'username': self.username
                })

        return job
예제 #13
0
    def ee2_get_jobs(self, params):
        api = EE2Api(url=self.config['ee2-url'],
                     token=self.token,
                     timeout=self.timeout)
        if params.get('admin', False):
            as_admin = 1
        else:
            as_admin = 0

        try:
            jobs = api.check_jobs({
                'job_ids': params['job_ids'],
                'as_admin': as_admin
            })
            return jobs['job_states']
        except ServiceError as se:
            if se.code == -32000:
                if re.search('Cannot find job with ids:', se.message):
                    # Hmm, wonder if the missing job ids are returned in the exception?
                    raise ServiceError(code=10,
                                       message='Job not found',
                                       data={'message': se.message})
                else:
                    raise ServiceError(code=1,
                                       message='Unknown error occurred',
                                       data={
                                           'upstream_error': {
                                               'code': se.code,
                                               'message': se.message,
                                               'data': se.data
                                           }
                                       })
            else:
                raise
        except Exception as ex:
            raise ServiceError(code=1,
                               message='Unknown error',
                               data={'original_message': str(ex)})
    def cancel_job(self, params):
        admin = params.get('admin', False)
        if admin:
            as_admin = 1
            terminated_code = params.get('code', 1)
        else:
            as_admin = 0
            terminated_code = params.get('code', 0)

        job_id = params['job_id']
        job = self.get_job(job_id)

        if job['state']['status'] not in ['create', 'queue', 'run']:
            return ServiceError(code=60,
                                message='Job is not cancelable',
                                data={
                                    'job_id': job_id,
                                    'job_status': job['state']['status']
                                })
    def get_jobs(self, params):
        jobs_collection = self.db['jobs']

        filters = []

        if params.get('admin', False):
            if not self.is_admin():
                raise ServiceError(
                    code=50,
                    message='Permission denied for this operation',
                    data={})
        else:
            filters.append({'owner.username': self.username})

        filters.append({'job_id': {'$in': params['job_ids']}})

        query = {'$and': filters}
        cursor = jobs_collection.find(query, {'_id': False})
        jobs = json.loads(json_util.dumps(cursor))
        return jobs
예제 #16
0
    def call_func(self, func_name, params=None, timeout=None):
        # Since this is designed to call KBase JSONRPC 1.1 services, we
        # follow the KBase conventions:
        # - params are always wrapped in an array; this emulates positional arguments
        # - a method with no arguments is called as either missing params in the call
        #   or the value None, and represented as an empty array in the service call
        # - a method with params is called with a single argument value which must be
        #   convertable to JSON; it is represented in the call to the service as an array
        #   wrapping that value.
        # Note that KBase methods can take multiple positional arguments (multiple elements
        # in the array), but by far most take just a single argument; this library makes that
        # simplifying assumption.
        if params is None:
            wrapped_params = []
        else:
            wrapped_params = [params]

        # The standard jsonrpc 1.1 calling object, with KBase conventions.
        # - id should be unique for this call (thus uuid), but is unused; it isn't
        #   really relevant for jsonrpc over http since each request always has a
        #   corresponding response; but it could aid in debugging since it connects
        #   the request and response.
        # - method is always the module name, a period, and the function name
        # - version is always 1.1 (even though there was no officially published jsonrpc 1.1)
        # - params as discussed above.
        call_params = {
            'id': str(uuid.uuid4()),
            'method': self.module + '.' + func_name,
            'version': '1.1',
            'params': wrapped_params
        }

        header = {
            'Content-Type': 'application/json'
        }

        # Calls may be authorized or not with a KBase token.
        if self.token:
            header['Authorization'] = self.token

        timeout = timeout or self.timeout

        # Note that timeout should be set here (except for type errors).
        # The constructor requires it, and it can be overridden in the call
        # to this method.
        try:
            response = requests.post(self.url, headers=header,
                                     data=json.dumps(call_params), timeout=timeout)
        except requests.exceptions.ReadTimeout as rte:
            # Note that ServiceError mirrors (and is eventually converted to)
            # a JSONRPC 2.0 error.
            raise ServiceError(
                code=100,
                message='Timeout calling service endpoint',
                data={
                    'url': self.url,
                    'headers': header,
                    'timeout': timeout,
                    'python_exception_string': str(rte)
                })
        except requests.exceptions.ConnectionError as rte:
            # Note that ServiceError mirrors (and is eventually converted to)
            # a JSONRPC 2.0 error.
            raise ServiceError(
                code=100,
                message='Timeout calling service endpoint',
                data={
                    'url': self.url,
                    'headers': header,
                    'timeout': timeout,
                    'python_exception_string': str(rte)
                })
        except Exception as ex:
            raise ServiceError(
                code=101,
                message='Error calling service endpoint: ' + ex.message,
                data={
                    'url': self.url,
                    'headers': header,
                })
        else:
            if response.headers.get('content-type', '').startswith('application/json'):
                try:
                    response_data = response.json()
                except json.decoder.JSONDecodeError as err:
                    raise ServiceError(
                        code=102,
                        message='Invalid response from upstream service - not json',
                        data={
                            'url': self.url,
                            'headers': header,
                            'decoding_error': str(err),
                            'response_text': response.content
                        })
                else:
                    error = response_data.get('error')
                    if error:
                        # Here we convert from the upstream KBase JSONRPC 1.1 error response
                        # to a JSONRPC 2.0 compatible exception. We pluck off commonly used
                        # error properties and put them into the data property. We do
                        # retain the error code (which should be JSONRPC 1.1 & 2.0 compatible).
                        error_data = {
                            'stack': error.get('error'),
                            'name': error.get('name')
                        }
                        raise ServiceError(
                            code=error.get('code'),
                            message=error.get('message', error.get('name')),
                            data=error_data)

                    result = response_data.get('result')

                    # The normal response has a result property which, like the params, is
                    # wrapped in an array. In this case the array emulates multiple value return.
                    # By far most KBase services keep that to a single array element, and provide
                    # multiple values if need be (most do) by usage of an object.
                    if isinstance(result, list):
                        if len(result) > 0:
                            return result[0]
                        else:
                            return None

                    # The one exception is when the result is just "null", which emulates a method
                    # with no, or void, result value.
                    elif result is None:
                        return result
                    else:
                        raise ServiceError(
                            code=103,
                            message=('Unexpected type in upstream service result; '
                                     'must be array or null'),
                            data={
                                'url': self.url,
                                'headers': header,
                                'result': result,
                                'result_type': type(result).__name__
                            })

                # Otherwise, if the service does not return json and has a 4xx or 5xx response,
                # raise an HTTPError from requests. This will be caught by the caller and
                # converted to a general purpose "unknown error" jsonrpc error response.
                response.raise_for_status()

                # If we get here, the response status is < 400 and not of type application/json,
                # thus an invalid response.
                raise ServiceError(
                    code=104,
                    message=('Invalid response from upstream service; '
                             'not application/json, not an error status'),
                    data={
                        'url': self.url,
                        'headers': header,
                        'response_status': response.status_code,
                        'response_content_type': response.headers.get('content-type', None),
                        'response_content': response.content
                    })
예제 #17
0
    def ee2_query_jobs(self,
                       offset,
                       limit,
                       time_span=None,
                       search=None,
                       filter=None,
                       sort=None,
                       admin=False):
        # TODO: timeout global or timeout per call?
        api = EE2Api(url=self.config['ee2-url'],
                     token=self.token,
                     timeout=self.timeout)

        params = {
            'start_time': time_span['from'],
            'end_time': time_span['to'],
            'offset': offset,
            'limit': limit
        }

        if filter is not None:
            raw_query = []

            status = filter.get('status', [])
            if len(status) > 0:
                status_transform = {
                    'create': 'created',
                    'queue': 'queued',
                    'run': 'running',
                    'complete': 'completed',
                    'error': 'error',
                    'terminate': 'terminated'
                }
                # value = list(map(lambda x: status_transform.get(x, x), value))
                status = [status_transform.get(x, x) for x in status]
                raw_query.append({'status': {'$in': status}})

            # parse and reformat the filters...
            workspace_id = filter.get('workspace_id', [])
            if len(workspace_id) > 0:
                raw_query.append({'wsid': {'$in': workspace_id}})

            user = filter.get('user', [])
            if len(user) > 0:
                raw_query.append({'user': {'$in': user}})

            client_group = filter.get('client_group', [])
            if len(client_group) > 0:
                raw_query.append({
                    'job_input.requirements.clientgroup': {
                        '$in': client_group
                    }
                })

            app_id = filter.get('app_id', [])
            if len(app_id) > 0:
                raw_query.append({'job_input.app_id': {'$in': app_id}})

            app_module = filter.get('app_module', [])
            if len(app_module) > 0:
                raw_query.append({
                    '$or': [{
                        'job_input.app_id': {
                            '$regex': f'^{module_name}/',
                            '$options': 'i'
                        }
                    } for module_name in app_module]
                })

            app_function = filter.get('app_function', [])
            if len(app_function) > 0:
                raw_query.append({
                    '$or': [{
                        'job_input.app_id': {
                            '$regex': f'/{function_name}$',
                            '$options': 'i'
                        }
                    } for function_name in app_function]
                })

            # wrap it in a raw query for mongoengine
            # TODO: upstream ee2 service should not be exposed like this!
            if len(raw_query) > 0:
                filter_query = {'__raw__': {'$and': raw_query}}

                params['filter'] = filter_query

        if sort is not None:
            # sort specs are not supported for ee2 (for now)
            # rather sorting is always by created timestamp
            # defaulting to ascending but reversable with the
            # ascending parameter set to 0.
            if len(sort) == 0:
                pass
            elif len(sort) > 1:
                raise ServiceError(code=40000,
                                   message="Only one sort spec supported",
                                   data={})
            elif sort[0].get('key') != 'created':
                raise ServiceError(
                    code=40000,
                    message="The sort spec must be for the 'created' key")

            if sort[0].get('direction') == 'ascending':
                ascending = 1
            else:
                ascending = 0
            params['ascending'] = ascending

        try:
            if admin:
                result = api.check_jobs_date_range_for_all(params)
            else:
                result = api.check_jobs_date_range_for_user(params)
            return result['jobs'], result['query_count']
        except ServiceError:
            raise
        except Exception as ex:
            raise ServiceError(code=40000,
                               message='Unknown error',
                               data={'original_message': str(ex)})
    def query_jobs(self, params):
        jobs_collection = self.db['jobs']

        is_admin = params.get('admin', False)

        filters = []

        # Enforce the single user model if not admin.
        if is_admin:
            if not self.is_admin():
                raise ServiceError(
                    code=50,
                    message='Permission denied for this operation',
                    data={})
        else:
            filters.append({'owner.username': self.username})

        # Get the total # jobs for this user, as the "total_count"
        if len(filters) > 0:
            cursor = jobs_collection.find({'$and': filters})
            total_count = cursor.count()
        else:
            total_count = jobs_collection.count()

        # Apply the time range.
        filters.append({
            '$and': [{
                'state.create_at': {
                    '$gte': params['time_span']['from']
                }
            }, {
                'state.create_at': {
                    '$lt': params['time_span']['to']
                }
            }]
        })

        # Handle field-specific filters
        if 'filter' in params:
            for key, value in params['filter'].items():
                if key == 'status':
                    filters.append({'state.status': value})
                elif key == 'app':
                    # filters.append({'$or': [
                    #     {'app.function_name': value},
                    #     {'app.module_name': value}
                    # ]})
                    filters.append({'app.id': value})
                elif key == 'workspace_id':
                    filters.append({'context.workspace.id': value})
                elif key == 'job_id':
                    filters.append({'job_id': value})
                elif key == 'client_group':
                    filters.append({'state.client_group': value})

        # Handle free-text search
        if 'search' in params:
            term_expressions = []
            for term in params['search']['terms']:
                term_expressions.append({
                    '$or': [{
                        'job_id': {
                            '$regex': term,
                            '$options': 'i'
                        }
                    }, {
                        'app.id': {
                            '$regex': term,
                            '$options': 'i'
                        }
                    }, {
                        'state.status': {
                            '$regex': term,
                            '$options': 'i'
                        }
                    }, {
                        'state.client_group': {
                            '$regex': term,
                            '$options': 'i'
                        }
                    }, {
                        'owner.username': {
                            '$regex': term,
                            '$options': 'i'
                        }
                    }, {
                        'msg': {
                            '$regex': term,
                            '$options': 'i'
                        }
                    }]
                })
            filters.append({'$and': term_expressions})

        # Put it all together and do it.
        query = {'$and': filters}
        cursor = jobs_collection.find(query, {'_id': False})
        found_count = cursor.count()

        # Handle sorting.
        if 'sort' in params:
            sort = []
            for sort_spec in params['sort']:
                key = sort_spec['key']
                direction = sort_spec['direction']

                if direction == 'ascending':
                    sort_direction = 1
                else:
                    sort_direction = -1

                if key == 'created':
                    sort_key = 'state.create_at'

                sort.append((sort_key, sort_direction))
            cursor.sort(sort)

        cursor.skip(params['offset'])
        cursor.limit(params['limit'])

        # TODO: filter on username and check if admin.
        jobs = json.loads(json_util.dumps(cursor))

        # Fix up workspace info relative to the current user.
        self.fix_workspaces(jobs)

        return jobs, found_count, total_count