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)
def delete(self, queue_name): """ --- summary: Clear a queue by canceling all requests parameters: - name: queue_name in: path required: true description: The name of the queue to clear type: string responses: 204: description: Queue successfully cleared 404: $ref: '#/definitions/404Error' 50x: $ref: '#/definitions/50xError' tags: - Queues """ self.request.event.name = Events.QUEUE_CLEARED.name self.request.event.payload = {'queue_name': queue_name} with thrift_context() as client: yield client.clearQueue(queue_name) self.set_status(204)
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))
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)
def get(self): try: with thrift_context() as client: bartender_version = yield client.getVersion() except Exception as ex: logger = logging.getLogger(__name__) logger.error("Could not get Bartender Version.") logger.exception(ex) bartender_version = "unknown" self.write({ "brew_view_version": brew_view.__version__, "bartender_version": bartender_version, "current_api_version": "v1", "supported_api_versions": ["v1"], })
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)
def post(self): """ --- summary: Initiate a rescan of the plugin directory deprecated: true description: | This endpoint is DEPRECATED - Use PATCH /api/v1/admin instead. Will initiate a rescan of the plugins directory. * 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. responses: 204: description: Rescan successfully initiated 50x: $ref: '#/definitions/50xError' tags: - Deprecated """ with thrift_context() as client: yield client.rescanSystemDirectory() self.set_status(204)
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.commands and "dev" not in system.version and system.has_different_commands(new_commands)): raise ModelValidationError( "System %s-%s already exists with different commands" % (system.name, system.version)) 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) elif op.operation == "add" and op.path == "/instance": add_instance = self.parser.parse_instance(op.value) # We also do these checks in mongo.models.System.clean # Unfortunately, they don't work very well if -1 < system.max_instances < len(system.instances) + 1: raise ModelValidationError( "Unable to add instance %s to %s - would exceed " "the system instance limit of %s" % (add_instance, system, system.max_instances)) if add_instance.name in system.instance_names: raise ModelValidationError( "Unable to add Instance %s to System %s: " "Duplicate instance names" % (add_instance, system)) else: system.instances.append(add_instance) system.deep_save() 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))
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)) if request_model.parent.status in Request.COMPLETED_STATUSES: raise ConflictError("Parent request has already completed") 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 # 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} # Metrics request_created(request_model) if blocking: # Publish metrics and event here here so they aren't skewed # See https://github.com/beer-garden/beer-garden/issues/190 self.request.publish_metrics = False http_api_latency_total.labels( method=self.request.method.upper(), route=self.prometheus_endpoint, status=self.get_status(), ).observe(request_latency(self.request.created_time)) self.request.publish_event = False brew_view.event_publishers.publish_event( self.request.event, **self.request.event_extras) try: timeout = self.get_argument("timeout", default=None) delta = timedelta(seconds=int(timeout)) if timeout else None event = brew_view.request_map.get(request_id) yield event.wait(delta) request_model.reload() except TimeoutError: raise TimeoutExceededError("Timeout exceeded for request %s" % request_id) finally: brew_view.request_map.pop(request_id, None) self.set_status(201) self.write( self.parser.serialize_request(request_model, to_string=False))
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 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)
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))