class CommandAPI(BaseHandler): parser = BeerGardenSchemaParser() logger = logging.getLogger(__name__) @authenticated(permissions=[Permissions.COMMAND_READ]) def get(self, command_id): """ --- summary: Retrieve a specific Command parameters: - name: command_id in: path required: true description: The ID of the Command type: string responses: 200: description: Command with the given ID schema: $ref: '#/definitions/Command' 404: $ref: '#/definitions/404Error' 50x: $ref: '#/definitions/50xError' tags: - Commands """ self.logger.debug("Getting Command: %s", command_id) self.write( self.parser.serialize_command( Command.objects.get(id=str(command_id)), to_string=False))
class AdminAPI(BaseHandler): parser = BeerGardenSchemaParser() logger = logging.getLogger(__name__) @coroutine def patch(self): """ --- summary: Initiate a rescan of the plugin directory description: | The body of the request needs to contain a set of instructions detailing the operations to perform. Currently the only operation supported is `rescan`: ```JSON { "operations": [ { "operation": "rescan" } ] } ``` * Will remove from the registry and database any currently stopped plugins who's directory has been removed. * Will add and start any new plugin directories. parameters: - name: patch in: body required: true description: Instructions for operations schema: $ref: '#/definitions/Patch' responses: 204: description: Rescan successfully initiated 50x: $ref: '#/definitions/50xError' tags: - Admin """ operations = self.parser.parse_patch( self.request.decoded_body, many=True, from_string=True) for op in operations: if op.operation == 'rescan': check_permission(self.current_user, [Permissions.SYSTEM_CREATE]) with thrift_context() as client: yield client.rescanSystemDirectory() else: error_msg = "Unsupported operation '%s'" % op.operation self.logger.warning(error_msg) raise ModelValidationError(error_msg) self.set_status(204)
class EventSocket(AuthMixin, WebSocketHandler): logger = logging.getLogger(__name__) parser = BeerGardenSchemaParser() closing = False listeners = set() def __init__(self, *args, **kwargs): super(EventSocket, self).__init__(*args, **kwargs) self.auth_providers.append(query_token_auth) def check_origin(self, origin): return True def open(self): if EventSocket.closing: self.close(reason='Shutting down') return # We can't go though the 'normal' BaseHandler exception translation try: check_permission(self.current_user, [Permissions.EVENT_READ]) except (HTTPError, RequestForbidden) as ex: self.close(reason=str(ex)) return EventSocket.listeners.add(self) def on_close(self): EventSocket.listeners.discard(self) def on_message(self, message): pass @classmethod def publish(cls, message): # Don't bother if nobody is listening if not len(cls.listeners): return for listener in cls.listeners: listener.write_message(message) @classmethod def shutdown(cls): cls.logger.debug('Closing websocket connections') EventSocket.closing = True for listener in cls.listeners: listener.close(reason='Shutting down')
def test_get(self): response = self.app.get("/api/v1/requests/id") self.assertEqual(200, response.status_code) self.objects_mock.get.assert_called_with(id='id') self.objects_mock.assert_called_with(parent=self.default_request) response_request = BeerGardenSchemaParser().parse_request( response.data, from_string=True) self.assertEqual(self.default_request.system, response_request.system) self.assertEqual(self.default_request.command, response_request.command) self.assertDictEqual(self.default_request.parameters, response_request.parameters) self.assertEqual(self.default_request.output, response_request.output) self.assertEqual(self.default_request.status, response_request.status)
class CommandListAPI(BaseHandler): parser = BeerGardenSchemaParser() logger = logging.getLogger(__name__) @authenticated(permissions=[Permissions.COMMAND_READ]) def get(self): """ --- summary: Retrieve all Commands responses: 200: description: All Commands schema: type: array items: $ref: '#/definitions/Command' 50x: $ref: '#/definitions/50xError' tags: - Commands """ self.logger.debug("Getting Commands") self.set_header('Content-Type', 'application/json; charset=UTF-8') try: self.write( self.parser.serialize_command(Command.objects.all(), many=True, to_string=True)) except mongoengine.errors.DoesNotExist as ex: self.logger.error( "Got an error while attempting to serialize commands. " "This error usually indicates " "there are orphans in the database.") raise mongoengine.errors.InvalidDocumentError(ex)
class SystemListAPI(BaseHandler): parser = BeerGardenSchemaParser() logger = logging.getLogger(__name__) REQUEST_FIELDS = set(SystemSchema.get_attribute_names()) # Need to ensure that Systems are updated atomically system_lock = Lock() @authenticated(permissions=[Permissions.SYSTEM_READ]) def get(self): """ --- summary: Retrieve all Systems description: | This endpoint allows for querying Systems. There are several parameters that control which fields are returned and what information is available. Things to be aware of: * The `include_commands` parameter is __deprecated__. Don't use it. Use `exclude_fields=commands` instead. * It's possible to specify `include_fields` _and_ `exclude_fields`. This doesn't make a lot of sense, but you can do it. If the same field is in both `exclude_fields` takes priority (the field will NOT be included in the response). Systems matching specific criteria can be filtered using additional query parameters. This is a very basic capability: * ?name=foo&version=1.0.0 This will return the system named 'foo' with version '1.0.0' * ?name=foo&name=bar This will not do what you expect: only return the system named 'bar' will be returned. parameters: - name: include_fields in: query required: false description: Specify fields to include in the response. All other fields will be excluded. type: array collectionFormat: csv items: type: string - name: exclude_fields in: query required: false description: Specify fields to exclude from the response type: array collectionFormat: csv items: type: string - name: dereference_nested in: query required: false description: Commands and instances will be an object id type: boolean default: true - name: include_commands in: query required: false description: __DEPRECATED__ Include commands in the response. Use `exclude_fields=commands` instead. type: boolean default: true responses: 200: description: All Systems schema: type: array items: $ref: '#/definitions/System' 50x: $ref: '#/definitions/50xError' tags: - Systems """ query_set = System.objects.order_by( self.request.headers.get('order_by', 'name')) serialize_params = {'to_string': True, 'many': True} include_fields = self.get_query_argument('include_fields', None) exclude_fields = self.get_query_argument('exclude_fields', None) dereference_nested = self.get_query_argument('dereference_nested', None) include_commands = self.get_query_argument('include_commands', None) if include_fields: include_fields = set( include_fields.split(',')) & self.REQUEST_FIELDS query_set = query_set.only(*include_fields) serialize_params['only'] = include_fields if exclude_fields: exclude_fields = set( exclude_fields.split(',')) & self.REQUEST_FIELDS query_set = query_set.exclude(*exclude_fields) serialize_params['exclude'] = exclude_fields if include_commands and include_commands.lower() == 'false': query_set = query_set.exclude('commands') if 'exclude' not in serialize_params: serialize_params['exclude'] = set() serialize_params['exclude'].add('commands') if dereference_nested and dereference_nested.lower() == 'false': query_set = query_set.no_dereference() # TODO - Handle multiple query arguments with the same key # for example: (?name=foo&name=bar) ... what should that mean? filter_params = {} # Need to use self.request.query_arguments to get all the query args for key in self.request.query_arguments: if key in self.REQUEST_FIELDS: # Now use get_query_argument to get the decoded value filter_params[key] = self.get_query_argument(key) result_set = query_set.filter(**filter_params) self.set_header('Content-Type', 'application/json; charset=UTF-8') self.write(self.parser.serialize_system(result_set, **serialize_params)) @coroutine @authenticated(permissions=[Permissions.SYSTEM_CREATE]) def post(self): """ --- summary: Create a new System or update an existing System description: If the System does not exist it will be created. If the System already exists it will be updated (assuming it passes validation). parameters: - name: system in: body description: The System definition to create / update schema: $ref: '#/definitions/System' responses: 200: description: An existing System has been updated schema: $ref: '#/definitions/System' 201: description: A new System has been created schema: $ref: '#/definitions/System' 400: $ref: '#/definitions/400Error' 50x: $ref: '#/definitions/50xError' tags: - Systems """ self.request.event.name = Events.SYSTEM_CREATED.name system_model = self.parser.parse_system(self.request.decoded_body, from_string=True) with (yield self.system_lock.acquire()): # See if we already have a system with this name + version existing_system = System.find_unique(system_model.name, system_model.version) if not existing_system: self.logger.debug("Creating a new system: %s" % system_model.name) saved_system, status_code = self._create_new_system( system_model) else: self.logger.debug("System %s already exists. Updating it." % system_model.name) self.request.event.name = Events.SYSTEM_UPDATED.name saved_system, status_code = self._update_existing_system( existing_system, system_model) saved_system.deep_save() self.request.event_extras = {'system': saved_system} self.set_status(status_code) self.write( self.parser.serialize_system(saved_system, to_string=False, include_commands=True)) @staticmethod def _create_new_system(system_model): new_system = system_model # Assign a default 'main' instance if there aren't any instances and there can only be one if not new_system.instances or len(new_system.instances) == 0: if new_system.max_instances is None or new_system.max_instances == 1: new_system.instances = [Instance(name='default')] new_system.max_instances = 1 else: raise ModelValidationError( 'Could not create system %s-%s: Systems with ' 'max_instances > 1 must also define their instances' % (system_model.name, system_model.version)) else: if not new_system.max_instances: new_system.max_instances = len(new_system.instances) return new_system, 201 @staticmethod def _update_existing_system(existing_system, system_model): # Raise an exception if commands already exist for this system and they differ from what's # already in the database in a significant way if existing_system.commands and 'dev' not in existing_system.version and \ existing_system.has_different_commands(system_model.commands): raise ModelValidationError( 'System %s-%s already exists with different commands' % (system_model.name, system_model.version)) else: existing_system.upsert_commands(system_model.commands) # Update instances if not system_model.instances or len(system_model.instances) == 0: system_model.instances = [Instance(name='default')] for attr in ['description', 'icon_name', 'display_name']: value = getattr(system_model, attr) # If we set an attribute on the model as None, mongoengine marks the attribute for # deletion. We want to prevent this, so we set it to an emtpy string. if value is None: setattr(existing_system, attr, "") else: setattr(existing_system, attr, value) # Update metadata new_metadata = system_model.metadata or {} existing_system.metadata.update(new_metadata) old_instances = [ inst for inst in existing_system.instances if system_model.has_instance(inst.name) ] new_instances = [ inst for inst in system_model.instances if not existing_system.has_instance(inst.name) ] existing_system.instances = old_instances + new_instances return existing_system, 200
def setUpClass(cls): # brew_view.load_app(environment="test") cls.parser = BeerGardenSchemaParser()
class LoggingConfigAPI(BaseHandler): parser = BeerGardenSchemaParser() logger = logging.getLogger(__name__) def get(self): """ --- summary: Get the plugin logging configuration parameters: - name: system_name in: query required: false description: Specific system name to get logging configuration type: string responses: 200: description: Logging Configuration for system schema: $ref: '#/definitions/LoggingConfig' 50x: $ref: '#/definitions/50xError' tags: - Config """ system_name = self.get_query_argument('system_name', default=None) log_config = brew_view.plugin_logging_config.get_plugin_log_config( system_name=system_name) self.write( self.parser.serialize_logging_config(log_config, to_string=False)) @coroutine def patch(self): """ --- summary: Reload the plugin logging configuration description: | The body of the request needs to contain a set of instructions detailing the operation to make. Currently supported operations are below: ```JSON { "operations": [ { "operation": "reload" } ] } ``` parameters: - name: patch in: body required: true description: Operation to perform schema: $ref: '#/definitions/Patch' responses: 200: description: Updated plugin logging configuration schema: $ref: '#/definitions/LoggingConfig' 50x: $ref: '#/definitions/50xError' tags: - Config """ operations = self.parser.parse_patch(self.request.decoded_body, many=True, from_string=True) for op in operations: if op.operation == 'reload': brew_view.load_plugin_logging_config(brew_view.config) else: error_msg = "Unsupported operation '%s'" % op.operation self.logger.warning(error_msg) raise ModelValidationError('value', error_msg) self.set_status(200) self.write( self.parser.serialize_logging_config( brew_view.plugin_logging_config, to_string=False))
class RequestListAPI(BaseHandler): parser = BeerGardenSchemaParser() logger = logging.getLogger(__name__) @authenticated(permissions=[Permissions.REQUEST_READ]) def get(self): """ --- summary: Retrieve a page of all Requests description: | This endpoint queries multiple requests at once. Because it's intended to be used with Datatables the query parameters are ... complicated. Here are things to keep in mind: * With no query parameters this endpoint will return the first 100 non-child requests. This can be controlled by passing the `start` and `length` query parameters. * This endpoint does NOT return child request definitions. If you want to see child requests you must use the /api/v1/requests/{request_id} endpoint. * By default this endpoint also does not include child requests in the response. That is, if a request has a non-null `parent` field it will not be included in the response array. Use the `include_children` query parameter to change this behavior. To filter, search, and order you need to conform to how Datatables structures its query parameters. * To indicate fields that should be included in the response specify multiple `columns` query parameters: ```JSON { "data": "command", "name": "", "searchable": true, "orderable": true, "search": {"value":"","regex":false} } { "data": "system", "name": "", "searchable": true, "orderable": true, "search": {"value": "","regex": false} } ``` * To filter a specific field set the value in the `search` key of its `column` definition: ```JSON { "data": "status", "name": "", "searchable": true, "orderable": true, "search": {"value": "SUCCESS", "regex":false} } ``` * To sort by a field use the `order` parameter. The `column` value should be the index of the column to sort and the `dir` value can be either "asc" or "desc." ```JSON {"column": 3,"dir": "asc"} ``` * To perform a text-based search across all fields use the `search` parameter: ```JSON { "value": "SEARCH VALUE", "regex": false } ``` parameters: - name: include_children in: query required: false description: Flag indicating whether to include child requests in the response list type: boolean default: false - name: start in: query required: false description: The starting index for the page type: integer - name: length in: query required: false description: The maximum number of Requests to include in the page type: integer - name: draw in: query required: false description: Used by datatables, will be echoed in a response header type: integer - name: columns in: query required: false description: Datatables column definitions type: array collectionFormat: multi items: properties: data: type: string name: type: string searchable: type: boolean default: true orderable: type: boolean default: true search: properties: value: type: string regex: type: boolean default: false - name: search in: query required: false description: Datatables search object type: string - name: order in: query required: false description: Datatables order object type: string responses: 200: description: A page of Requests schema: type: array items: $ref: '#/definitions/Request' headers: start: type: integer description: Echo of 'start' query parameter or '0' length: type: integer description: Number of Requests in the response draw: type: integer description: Echo of the 'draw' query parameter recordsFiltered: type: integer description: The number of Requests that satisfied the search filters recordsTotal: type: integer description: The total number of Requests 50x: $ref: '#/definitions/50xError' tags: - Requests """ self.logger.debug("Getting Requests") # We need to do the slicing on the query. This greatly reduces load time. start = int(self.get_argument('start', default=0)) length = int(self.get_argument('length', default=100)) requests, filtered_count, requested_fields = self._get_requests( start, start + length) # Sweet, we have data. Now setup some headers for the response response_headers = { # These are a courtesy for non-datatables requests. We want people making a request # with no headers to realize they probably aren't getting the full dataset 'start': start, 'length': len(requests), # And these are required by datatables 'recordsFiltered': filtered_count, 'recordsTotal': Request.objects.count(), 'draw': self.get_argument('draw', '') } for key, value in response_headers.items(): self.add_header(key, value) self.add_header('Access-Control-Expose-Headers', key) self.set_header('Content-Type', 'application/json; charset=UTF-8') self.write( self.parser.serialize_request(requests, to_string=True, many=True, only=requested_fields)) @coroutine @authenticated(permissions=[Permissions.REQUEST_CREATE]) def post(self): """ --- summary: Create a new Request parameters: - name: request in: body description: The Request definition schema: $ref: '#/definitions/Request' - name: blocking in: query required: false description: Flag indicating whether to wait for request completion type: boolean default: false - name: timeout in: query required: false description: Maximum time (seconds) to wait for request completion type: integer default: None (Wait forever) consumes: - application/json - application/x-www-form-urlencoded responses: 201: description: A new Request has been created schema: $ref: '#/definitions/Request' headers: Instance-Status: type: string description: Current status of the Instance that will process the created Request 400: $ref: '#/definitions/400Error' 50x: $ref: '#/definitions/50xError' tags: - Requests """ self.request.event.name = Events.REQUEST_CREATED.name if self.request.mime_type == 'application/json': request_model = self.parser.parse_request( self.request.decoded_body, from_string=True) elif self.request.mime_type == 'application/x-www-form-urlencoded': args = {'parameters': {}} for key, value in self.request.body_arguments.items(): if key.startswith('parameters.'): args['parameters'][key.replace('parameters.', '')] = value[0].decode( self.request.charset) else: args[key] = value[0].decode(self.request.charset) request_model = Request(**args) else: raise ModelValidationError( 'Unsupported or missing content-type header') if request_model.parent: request_model.parent = Request.objects.get( id=str(request_model.parent.id)) request_model.has_parent = True else: request_model.has_parent = False if self.current_user: request_model.requester = self.current_user.username # Ok, ready to save request_model.save() request_id = str(request_model.id) # Set up the wait event BEFORE yielding the processRequest call blocking = self.get_argument('blocking', default='').lower() == 'true' if blocking: brew_view.request_map[request_id] = Event() with thrift_context() as client: try: yield client.processRequest(request_id) except bg_utils.bg_thrift.InvalidRequest as ex: request_model.delete() raise ModelValidationError(ex.message) except bg_utils.bg_thrift.PublishException as ex: request_model.delete() raise RequestPublishException(ex.message) except Exception: if request_model.id: request_model.delete() raise else: if blocking: timeout = self.get_argument('timeout', default=None) if timeout is None: timeout_delta = None else: timeout_delta = timedelta(seconds=int(timeout)) try: event = brew_view.request_map.get(request_id) yield event.wait(timeout_delta) except TimeoutError: raise TimeoutExceededError() finally: brew_view.request_map.pop(request_id, None) request_model.reload() # Query for request from body id req = Request.objects.get(id=request_id) # Now attempt to add the instance status as a header. # The Request is already created at this point so it's a best-effort thing self.set_header("Instance-Status", 'UNKNOWN') try: # Since request has system info we can query for a system object system = System.objects.get(name=req.system, version=req.system_version) # Loop through all instances in the system until we find the instance that matches # the request instance for instance in system.instances: if instance.name == req.instance_name: self.set_header("Instance-Status", instance.status) # The Request is already created at this point so adding the Instance status header is a # best-effort thing except Exception as ex: self.logger.exception( 'Unable to get Instance status for Request %s: %s', request_id, ex) self.request.event_extras = {'request': req} self.set_status(201) self.queued_request_gauge.labels( system=request_model.system, system_version=request_model.system_version, instance_name=request_model.instance_name, ).inc() self.request_counter_total.labels( system=request_model.system, system_version=request_model.system_version, instance_name=request_model.instance_name, command=request_model.command, ).inc() self.write( self.parser.serialize_request(request_model, to_string=False)) def _get_requests(self, start, end): """Get Requests matching the HTTP request query parameters. :return requests: The collection of requests :return requested_fields: The fields to be returned for each Request """ search_params = [] requested_fields = [] order_by = None overall_search = None query = Request.objects raw_columns = self.get_query_arguments('columns') if raw_columns: columns = [] for raw_column in raw_columns: column = json.loads(raw_column) columns.append(column) if column['data']: requested_fields.append(column['data']) if 'searchable' in column and column['searchable'] and column[ 'search']['value']: if column['data'] in ['created_at', 'updated_at']: search_dates = column['search']['value'].split('~') if search_dates[0]: search_params.append( Q(** {column['data'] + '__gte': search_dates[0]})) if search_dates[1]: search_params.append( Q(** {column['data'] + '__lte': search_dates[1]})) else: search_query = Q( **{ column['data'] + '__contains': column['search']['value'] }) search_params.append(search_query) raw_order = self.get_query_argument('order', default=None) if raw_order: order = json.loads(raw_order) order_by = columns[order.get('column')]['data'] if order.get('dir') == 'desc': order_by = '-' + order_by raw_search = self.get_query_argument('search', default=None) if raw_search: search = json.loads(raw_search) if search['value']: overall_search = '"' + search['value'] + '"' # Default to only top-level requests if self.get_query_argument('include_children', default='false').lower() != 'true': search_params.append(Q(has_parent=False)) # Now we can construct the actual query parameters query_params = reduce(lambda x, y: x & y, search_params, Q()) # Further modify the query itself if overall_search: query = query.search_text(overall_search) if order_by: query = query.order_by(order_by) # Marshmallow treats [] as 'serialize nothing' which is not what we # want, so translate to None if requested_fields: query = query.only(*requested_fields) else: requested_fields = None # Execute the query / count requests = query.filter(query_params) filtered_count = requests.count() # Only return the correct slice of the QuerySet return requests[start:end], filtered_count, requested_fields
class QueueListAPI(BaseHandler): parser = BeerGardenSchemaParser() logger = logging.getLogger(__name__) @coroutine @authenticated(permissions=[Permissions.QUEUE_READ]) def get(self): """ --- summary: Retrieve all queue information responses: 200: description: List of all queue information objects schema: type: array items: $ref: '#/definitions/Queue' 50x: $ref: '#/definitions/50xError' tags: - Queues """ self.logger.debug("Getting all queues") queues = [] systems = System.objects.all().select_related(max_depth=1) for system in systems: for instance in system.instances: queue = Queue(name='UNKNOWN', system=system.name, version=system.version, instance=instance.name, system_id=str(system.id), display=system.display_name, size=-1) with thrift_context() as client: try: queue_info = yield client.getQueueInfo( system.name, system.version, instance.name) queue.name = queue_info.name queue.size = queue_info.size except Exception: self.logger.error( "Error getting queue size for %s[%s]-%s" % (system.name, instance.name, system.version)) queues.append(queue) self.set_header('Content-Type', 'application/json; charset=UTF-8') self.write( self.parser.serialize_queue(queues, to_string=True, many=True)) @coroutine @authenticated(permissions=[Permissions.QUEUE_DELETE]) def delete(self): """ --- summary: Cancel and clear all requests in all queues responses: 204: description: All queues successfully cleared 50x: $ref: '#/definitions/50xError' tags: - Queues """ self.request.event.name = Events.ALL_QUEUES_CLEARED.name with thrift_context() as client: yield client.clearAllQueues() self.set_status(204)
class JobAPI(BaseHandler): logger = logging.getLogger(__name__) parser = BeerGardenSchemaParser() @authenticated(permissions=[Permissions.JOB_READ]) def get(self, job_id): """ --- summary: Retrieve a specific Job parameters: - name: job_id in: path required: true description: The ID of the Job type: string responses: 200: description: Job with the given ID schema: $ref: '#/definitions/Job' 404: $ref: '#/definitions/404Error' 50x: $ref: '#/definitions/50xError' tags: - Jobs """ document = Job.objects.get(id=job_id) self.set_header('Content-Type', 'application/json; charset=UTF-8') self.write(self.parser.serialize_job(document, to_string=False)) @authenticated(permissions=[Permissions.JOB_UPDATE]) def patch(self, job_id): """ --- summary: Pause/Resume a job description: | The body of the request needs to contain a set of instructions detailing the actions to take. Currently the only operation supported is `update` with `path` of `/status`. You can pause a job with: ```JSON { "operations": [ { "operation": "update", "path": "/status", "value": "PAUSED" } ] } ``` And resume it with: ```JSON { "operations": [ { "operation": "update", "path": "/status", "value": "RUNNING" } ] } ``` parameters: - name: job_id in: path required: true description: The ID of the Job type: string - name: patch in: body required: true description: Instructions for the actions to take schema: $ref: '#/definitions/Patch' responses: 200: description: Job with the given ID schema: $ref: '#/definitions/Job' 400: $ref: '#/definitions/400Error' 404: $ref: '#/definitions/404Error' 50x: $ref: '#/definitions/50xError' tags: - Jobs """ job = Job.objects.get(id=job_id) operations = self.parser.parse_patch( self.request.decoded_body, many=True, from_string=True ) for op in operations: if op.operation == 'update': if op.path == '/status': if str(op.value).upper() == 'PAUSED': brew_view.request_scheduler.pause_job( job_id, jobstore='beer_garden' ) job.status = 'PAUSED' elif str(op.value).upper() == 'RUNNING': brew_view.request_scheduler.resume_job( job_id, jobstore='beer_garden' ) job.status = 'RUNNING' else: error_msg = "Unsupported status value '%s'" % op.value self.logger.warning(error_msg) raise ModelValidationError(error_msg) else: error_msg = "Unsupported path value '%s'" % op.path self.logger.warning(error_msg) raise ModelValidationError(error_msg) else: error_msg = "Unsupported operation '%s'" % op.operation self.logger.warning(error_msg) raise ModelValidationError(error_msg) job.save() self.set_header('Content-Type', 'application/json; charset=UTF-8') self.write(self.parser.serialize_job(job, to_string=False)) @coroutine @authenticated(permissions=[Permissions.JOB_DELETE]) def delete(self, job_id): """ --- summary: Delete a specific Job. description: Will remove a specific job. No further executions will occur. parameters: - name: job_id in: path required: true description: The ID of the Job type: string responses: 204: description: Job has been successfully deleted. 404: $ref: '#/definitions/404Error' 50x: $ref: '#/definitions/50xError' tags: - Jobs """ brew_view.request_scheduler.remove_job(job_id, jobstore='beer_garden') self.set_status(204)
class InstanceAPI(BaseHandler): parser = BeerGardenSchemaParser() logger = logging.getLogger(__name__) event_dict = { 'initialize': Events.INSTANCE_INITIALIZED.name, 'start': Events.INSTANCE_STARTED.name, 'stop': Events.INSTANCE_STOPPED.name } @authenticated(permissions=[Permissions.INSTANCE_READ]) def get(self, instance_id): """ --- summary: Retrieve a specific Instance parameters: - name: instance_id in: path required: true description: The ID of the Instance type: string responses: 200: description: Instance with the given ID schema: $ref: '#/definitions/Instance' 404: $ref: '#/definitions/404Error' 50x: $ref: '#/definitions/50xError' tags: - Instances """ self.logger.debug("Getting Instance: %s", instance_id) self.write(self.parser.serialize_instance(Instance.objects.get(id=instance_id), to_string=False)) @authenticated(permissions=[Permissions.INSTANCE_DELETE]) def delete(self, instance_id): """ --- summary: Delete a specific Instance parameters: - name: instance_id in: path required: true description: The ID of the Instance type: string responses: 204: description: Instance has been successfully deleted 404: $ref: '#/definitions/404Error' 50x: $ref: '#/definitions/50xError' tags: - Instances """ self.logger.debug("Deleting Instance: %s", instance_id) Instance.objects.get(id=instance_id).delete() self.set_status(204) @coroutine @authenticated(permissions=[Permissions.INSTANCE_UPDATE]) def patch(self, instance_id): """ --- summary: Partially update an Instance description: | The body of the request needs to contain a set of instructions detailing the updates to apply. Currently the only operations are: * initialize * start * stop * heartbeat ```JSON { "operations": [ { "operation": "" } ] } ``` parameters: - name: instance_id in: path required: true description: The ID of the Instance type: string - name: patch in: body required: true description: Instructions for how to update the Instance schema: $ref: '#/definitions/Patch' responses: 200: description: Instance with the given ID schema: $ref: '#/definitions/Instance' 400: $ref: '#/definitions/400Error' 404: $ref: '#/definitions/404Error' 50x: $ref: '#/definitions/50xError' tags: - Instances """ response = {} instance = Instance.objects.get(id=instance_id) operations = self.parser.parse_patch(self.request.decoded_body, many=True, from_string=True) for op in operations: if op.operation.lower() in ('initialize', 'start', 'stop'): self.request.event.name = self.event_dict[op.operation.lower()] with thrift_context() as client: response = yield getattr(client, op.operation.lower()+'Instance')(instance_id) elif op.operation.lower() == 'heartbeat': instance.status_info.heartbeat = datetime.utcnow() instance.save() response = self.parser.serialize_instance(instance, to_string=False) elif op.operation.lower() == 'replace': if op.path.lower() == '/status': if op.value.upper() == 'INITIALIZING': self.request.event.name = Events.INSTANCE_INITIALIZED.name with thrift_context() as client: response = yield client.initializeInstance(instance_id) elif op.value.upper() == 'STOPPING': self.request.event.name = Events.INSTANCE_STOPPED.name with thrift_context() as client: response = yield client.stopInstance(instance_id) elif op.value.upper() == 'STARTING': self.request.event.name = Events.INSTANCE_STARTED.name with thrift_context() as client: response = yield client.startInstance(instance_id) elif op.value.upper() in ['RUNNING', 'STOPPED']: instance.status = op.value.upper() instance.save() response = self.parser.serialize_instance(instance, to_string=False) else: error_msg = "Unsupported status value '%s'" % op.value self.logger.warning(error_msg) raise ModelValidationError('value', error_msg) else: error_msg = "Unsupported path '%s'" % op.path self.logger.warning(error_msg) raise ModelValidationError('value', error_msg) else: error_msg = "Unsupported operation '%s'" % op.operation self.logger.warning(error_msg) raise ModelValidationError('value', error_msg) if self.request.event.name: self.request.event_extras = {'instance': instance} self.write(response)
class SystemAPI(BaseHandler): parser = BeerGardenSchemaParser() logger = logging.getLogger(__name__) @authenticated(permissions=[Permissions.SYSTEM_READ]) def get(self, system_id): """ --- summary: Retrieve a specific System parameters: - name: system_id in: path required: true description: The ID of the System type: string - name: include_commands in: query required: false description: Include the System's commands in the response type: boolean default: true responses: 200: description: System with the given ID schema: $ref: '#/definitions/System' 404: $ref: '#/definitions/404Error' 50x: $ref: '#/definitions/50xError' tags: - Systems """ self.logger.debug("Getting System: %s", system_id) include_commands = self.get_query_argument( 'include_commands', default='true').lower() != 'false' self.write(self.parser.serialize_system(System.objects.get(id=system_id), to_string=False, include_commands=include_commands)) @coroutine @authenticated(permissions=[Permissions.SYSTEM_DELETE]) def delete(self, system_id): """ Will give Bartender a chance to remove instances of this system from the registry but will always delete the system regardless of whether the Bartender operation succeeds. --- summary: Delete a specific System description: Will remove instances of local plugins from the registry, clear and remove message queues, and remove the system from the database. parameters: - name: system_id in: path required: true description: The ID of the System type: string responses: 204: description: System has been successfully deleted 404: $ref: '#/definitions/404Error' 50x: $ref: '#/definitions/50xError' tags: - Systems """ self.request.event.name = Events.SYSTEM_REMOVED.name self.request.event_extras = {'system': System.objects.get(id=system_id)} with thrift_context() as client: yield client.removeSystem(str(system_id)) self.set_status(204) @coroutine @authenticated(permissions=[Permissions.SYSTEM_UPDATE]) def patch(self, system_id): """ --- summary: Partially update a System description: | The body of the request needs to contain a set of instructions detailing the updates to apply. Currently supported operations are below: ```JSON { "operations": [ { "operation": "replace", "path": "/commands", "value": "" }, { "operation": "replace", "path": "/description", "value": "new description"}, { "operation": "replace", "path": "/display_name", "value": "new display name"}, { "operation": "replace", "path": "/icon_name", "value": "new icon name"}, { "operation": "update", "path": "/metadata", "value": {"foo": "bar"}} ] } ``` Where `value` is a list of new Commands. parameters: - name: system_id in: path required: true description: The ID of the System type: string - name: patch in: body required: true description: Instructions for how to update the System schema: $ref: '#/definitions/Patch' responses: 200: description: System with the given ID schema: $ref: '#/definitions/System' 400: $ref: '#/definitions/400Error' 404: $ref: '#/definitions/404Error' 50x: $ref: '#/definitions/50xError' tags: - Systems """ self.request.event.name = Events.SYSTEM_UPDATED.name system = System.objects.get(id=system_id) operations = self.parser.parse_patch(self.request.decoded_body, many=True, from_string=True) for op in operations: if op.operation == 'replace': if op.path == '/commands': new_commands = self.parser.parse_command(op.value, many=True) if system.has_different_commands(new_commands): if system.commands and 'dev' not in system.version: raise ModelValidationError('System %s-%s already exists with ' 'different commands' % (system.name, system.version)) else: system.upsert_commands(new_commands) elif op.path in ['/description', '/icon_name', '/display_name']: if op.value is None: # If we set an attribute to None, mongoengine marks that attribute # for deletion, so we don't do that. value = "" else: value = op.value attr = op.path.strip("/") self.logger.debug("Updating system %s" % attr) self.logger.debug("Old: %s" % getattr(system, attr)) setattr(system, attr, value) self.logger.debug("Updated: %s" % getattr(system, attr)) system.save() else: error_msg = "Unsupported path '%s'" % op.path self.logger.warning(error_msg) raise ModelValidationError('value', error_msg) elif op.operation == 'update': if op.path == '/metadata': self.logger.debug("Updating system metadata") self.logger.debug("Old: %s" % system.metadata) system.metadata.update(op.value) self.logger.debug("Updated: %s" % system.metadata) system.save() else: error_msg = "Unsupported path for update '%s'" % op.path self.logger.warning(error_msg) raise ModelValidationError('path', error_msg) elif op.operation == 'reload': with thrift_context() as client: yield client.reloadSystem(system_id) else: error_msg = "Unsupported operation '%s'" % op.operation self.logger.warning(error_msg) raise ModelValidationError('value', error_msg) system.reload() self.request.event_extras = {'system': system, 'patch': operations} self.write(self.parser.serialize_system(system, to_string=False))
class JobListAPI(BaseHandler): parser = BeerGardenSchemaParser() logger = logging.getLogger(__name__) @authenticated(permissions=[Permissions.JOB_READ]) def get(self): """ --- summary: Retrieve all Jobs. responses: 200: description: Successfully retrieved all systems. schema: type: array items: $ref: '#/definitions/Job' 50x: $ref: '#/definitions/50xError' tags: - Jobs """ filter_params = {} for key in self.request.arguments.keys(): if key in JobSchema.get_attribute_names(): filter_params[key] = self.get_query_argument(key) self.set_header('Content-Type', 'application/json; charset=UTF-8') self.write( self.parser.serialize_job(Job.objects.filter(**filter_params), to_string=True, many=True)) @coroutine @authenticated(permissions=[Permissions.JOB_CREATE]) def post(self): """ --- summary: Schedules a Job to be run. description: | Given a job, it will be scheduled to run on the interval set in the trigger argument. parameters: - name: job in: body description: The Job to create/schedule schema: $ref: '#/definitions/Job' responses: 201: description: A new job has been created schema: $ref: '#/definitions/Job' 400: $ref: '#/definitions/400Error' 50x: $ref: '#/definitions/50xError' tags: - Jobs """ document = self.parser.parse_job(self.request.decoded_body, from_string=True) # We have to save here, because we need an ID to pass # to the scheduler. document.save() try: brew_view.request_scheduler.add_job( run_job, None, kwargs={ 'request_template': document.request_template, 'job_id': str(document.id), }, name=document.name, misfire_grace_time=document.misfire_grace_time, coalesce=document.coalesce, max_instances=3, jobstore='beer_garden', replace_existing=False, id=str(document.id), ) except Exception: document.delete() raise self.set_status(201) self.set_header('Content-Type', 'application/json; charset=UTF-8') self.write(self.parser.serialize_job(document, to_string=False))
class RequestAPI(BaseHandler): parser = BeerGardenSchemaParser() logger = logging.getLogger(__name__) @authenticated(permissions=[Permissions.REQUEST_READ]) def get(self, request_id): """ --- summary: Retrieve a specific Request parameters: - name: request_id in: path required: true description: The ID of the Request type: string responses: 200: description: Request with the given ID schema: $ref: '#/definitions/Request' 404: $ref: '#/definitions/404Error' 50x: $ref: '#/definitions/50xError' tags: - Requests """ self.logger.debug("Getting Request: %s", request_id) req = Request.objects.get(id=str(request_id)) req.children = Request.objects(parent=req) self.write(self.parser.serialize_request(req, to_string=False)) @authenticated(permissions=[Permissions.REQUEST_UPDATE]) def patch(self, request_id): """ --- summary: Partially update a Request description: | The body of the request needs to contain a set of instructions detailing the updates to apply. Currently the only operation supported is `replace`, with paths `/status`, `/output`, and `/error_class`: ```JSON { "operations": [ { "operation": "replace", "path": "/status", "value": "" } ] } ``` parameters: - name: request_id in: path required: true description: The ID of the Request type: string - name: patch in: body required: true description: Instructions for how to update the Request schema: $ref: '#/definitions/Patch' responses: 200: description: Request with the given ID schema: $ref: '#/definitions/Request' 400: $ref: '#/definitions/400Error' 404: $ref: '#/definitions/404Error' 50x: $ref: '#/definitions/50xError' tags: - Requests """ req = Request.objects.get(id=request_id) operations = self.parser.parse_patch(self.request.decoded_body, many=True, from_string=True) wait_event = None # We note the status before the operations, because it is possible for the operations to # update the status of the request. In that case, because the updates are coming in in a # single request it is okay to update the output or error_class. Ideally this would be # handled correctly when we better integrate PatchOperations with their models. status_before = req.status for op in operations: if op.operation == 'replace': if op.path == '/status': if op.value.upper() in BrewtilsRequest.STATUS_LIST: req.status = op.value.upper() if op.value.upper() == 'IN_PROGRESS': self.request.event.name = Events.REQUEST_STARTED.name elif op.value.upper( ) in BrewtilsRequest.COMPLETED_STATUSES: self.request.event.name = Events.REQUEST_COMPLETED.name if request_id in brew_view.request_map: wait_event = brew_view.request_map[request_id] else: error_msg = "Unsupported status value '%s'" % op.value self.logger.warning(error_msg) raise ModelValidationError(error_msg) elif op.path == '/output': if req.output == op.value: continue if status_before in Request.COMPLETED_STATUSES: raise ModelValidationError( "Cannot update output for a request " "that is already completed") req.output = op.value elif op.path == '/error_class': if req.error_class == op.value: continue if status_before in Request.COMPLETED_STATUSES: raise ModelValidationError( "Cannot update error_class for a " "request that is already completed") req.error_class = op.value self.request.event.error = True else: error_msg = "Unsupported path '%s'" % op.path self.logger.warning(error_msg) raise ModelValidationError(error_msg) else: error_msg = "Unsupported operation '%s'" % op.operation self.logger.warning(error_msg) raise ModelValidationError(error_msg) req.save() self._update_metrics(req, status_before) self._update_job_numbers(req, status_before) if wait_event: wait_event.set() self.request.event_extras = {'request': req, 'patch': operations} self.write(self.parser.serialize_request(req, to_string=False)) def _update_job_numbers(self, request, status_before): if (not request.metadata.get('_bg_job_id') or status_before == request.status or request.status not in Request.COMPLETED_STATUSES): return try: job_id = request.metadata.get('_bg_job_id') document = Job.objects.get(id=job_id) if request.status == 'ERROR': document.error_count += 1 elif request.status == 'SUCCESS': document.success_count += 1 document.save() except Exception as exc: self.logger.warning('Could not update job counts.') self.logger.exception(exc) def _update_metrics(self, request, status_before): """Update metrics associated with this new request. We have already confirmed that this request has had a valid state transition because updating the status verifies that the updates are correct. In addition, this call is happening after the save to the database. Now we just need to update the metrics. """ if (status_before == request.status or status_before in BrewtilsRequest.COMPLETED_STATUSES): return labels = { 'system': request.system, 'system_version': request.system_version, 'instance_name': request.instance_name, } if status_before == 'CREATED': self.queued_request_gauge.labels(**labels).dec() elif status_before == 'IN_PROGRESS': self.in_progress_request_gauge.labels(**labels).dec() if request.status == 'IN_PROGRESS': self.in_progress_request_gauge.labels(**labels).inc() elif request.status in BrewtilsRequest.COMPLETED_STATUSES: # We don't use _measure_latency here because the request times are # stored in UTC and we need to make sure we're comparing apples to # apples. latency = (datetime.datetime.utcnow() - request.created_at).total_seconds() labels['command'] = request.command labels['status'] = request.status self.completed_request_counter.labels(**labels).inc() self.plugin_command_latency.labels(**labels).observe(latency)