def post(self): """ --- summary: Create a new Role parameters: - name: role in: body description: The Role definition schema: $ref: '#/definitions/Role' consumes: - application/json responses: 201: description: A new Role has been created schema: $ref: '#/definitions/Role' 400: $ref: '#/definitions/400Error' 50x: $ref: '#/definitions/50xError' tags: - Roles """ role = MongoParser.parse_role(self.request.decoded_body, from_string=True) # Make sure all new permissions are real if not set(role.permissions).issubset(Permissions.values): invalid = set(role.permissions).difference(Permissions.values) raise ModelValidationError("Permissions %s do not exist" % invalid) # And the same for nested roles nested_roles = [] for nested_role in role.roles: try: db_role = Role.objects.get(name=nested_role.name) # There shouldn't be any way to construct a cycle with a new # role, but check just to be sure ensure_no_cycles(role, db_role) nested_roles.append(db_role) except DoesNotExist: raise ModelValidationError("Role '%s' does not exist" % nested_role.name) role.roles = nested_roles role.save() self.set_status(201) self.write(MongoParser.serialize_role(role, to_string=False))
def get(self): """ --- summary: Retrieve all Users responses: 200: description: All Users schema: type: array items: $ref: '#/definitions/User' 50x: $ref: '#/definitions/50xError' tags: - Users """ principals = Principal.objects.all().select_related(max_depth=1) for principal in principals: principal.permissions = coalesce_permissions(principal.roles)[1] self.set_header("Content-Type", "application/json; charset=UTF-8") self.write( MongoParser.serialize_principal(principals, to_string=True, many=True))
class CommandAPI(BaseHandler): parser = MongoParser() 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))
def get(self, role_id): """ --- summary: Retrieve all specific Role parameters: - name: role_id in: path required: true description: The ID of the Role type: string responses: 200: description: Role with the given ID schema: $ref: '#/definitions/Role' 404: $ref: '#/definitions/404Error' 50x: $ref: '#/definitions/50xError' tags: - Roles """ self.write( MongoParser.serialize_role(Role.objects.get(id=str(role_id)), to_string=False))
class AdminAPI(BaseHandler): parser = MongoParser() 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 = MongoParser() 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 post(self): """ --- summary: Create a new User parameters: - name: user in: body description: The user schema: type: object properties: username: type: string description: the name password: type: string description: the password required: - username - password consumes: - application/json responses: 201: description: A new User has been created schema: $ref: '#/definitions/User' 400: $ref: '#/definitions/400Error' 50x: $ref: '#/definitions/50xError' tags: - Users """ parsed = json.loads(self.request.decoded_body) user = Principal( username=parsed["username"], hash=custom_app_context.hash(parsed["password"]), ) if "roles" in parsed: user.roles = [ Role.objects.get(name=name) for name in parsed["roles"] ] user.save() user.permissions = coalesce_permissions(user.roles)[1] self.set_status(201) self.write(MongoParser.serialize_principal(user, to_string=False))
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 = MongoParser().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)
def get(self, user_identifier): """ --- summary: Retrieve a specific User parameters: - name: user_identifier in: path required: true description: The ID or name of the User type: string responses: 200: description: User with the given ID schema: $ref: '#/definitions/User' 404: $ref: '#/definitions/404Error' 50x: $ref: '#/definitions/50xError' tags: - Users """ if user_identifier == "anonymous": principal = brew_view.anonymous_principal else: # Need fine-grained access control here if user_identifier not in [ str(self.current_user.id), self.current_user.username, ]: check_permission(self.current_user, [Permissions.USER_READ]) try: principal = Principal.objects.get(id=str(user_identifier)) except (DoesNotExist, ValidationError): principal = Principal.objects.get( username=str(user_identifier)) principal.permissions = coalesce_permissions(principal.roles)[1] self.write(MongoParser.serialize_principal(principal, to_string=False))
def get(self): """ --- summary: Retrieve all Roles responses: 200: description: All Roles schema: type: array items: $ref: '#/definitions/Role' 50x: $ref: '#/definitions/50xError' tags: - Roles """ self.set_header("Content-Type", "application/json; charset=UTF-8") self.write( MongoParser.serialize_role(Role.objects.all(), many=True, to_string=True))
class CommandListAPI(BaseHandler): parser = MongoParser() 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)
def clear_queue(self, queue_name): """Remove all messages in a queue. :param queue_name: The name of the queue :return:None """ self.logger.info("Clearing Queue: %s", queue_name) queue_dictionary = self._client.get_queue(self._virtual_host, queue_name) number_of_messages = queue_dictionary.get("messages_ready", 0) while number_of_messages > 0: self.logger.debug("Getting the Next Message") messages = self._client.get_messages(self._virtual_host, queue_name, count=1, requeue=False) if messages and len(messages) > 0: message = messages[0] try: request = MongoParser.parse_request(message["payload"], from_string=True) self.logger.debug("Canceling Request: %s", request.id) bartender.bv_client.update_request(request.id, status="CANCELED") except Exception as ex: self.logger.error("Error removing message:") self.logger.exception(ex) else: self.logger.debug( "Race condition: The while loop thought there were " "more messages to ingest but no more messages could " "be received.") break number_of_messages -= 1
def _event_serialize(self, event, **kwargs): return MongoParser.parse_event( MongoParser.serialize_event(event, to_string=False))
class JobAPI(BaseHandler): logger = logging.getLogger(__name__) parser = MongoParser() @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 TokenListAPI(BaseHandler): parser = MongoParser() logger = logging.getLogger(__name__) def __init__(self, *args, **kwargs): super(TokenListAPI, self).__init__(*args, **kwargs) self.executor = ProcessPoolExecutor() def get(self): """ --- summary: Use a refresh token to retrieve a new access token description: | Your refresh token can either be set in a cookie (which we set on your session when you logged in) or you can include the refresh ID as a header named "X-BG-RefreshID" responses: 200: description: New Auth Token schema: $ref: '#/definitions/RefreshToken' 404: $ref: '#/definitions/404Error' 50x: $ref: '#/definitions/50xError' tags: - Tokens """ self.write(json.dumps(self._refresh_token())) def patch(self): """ --- summary: Refresh an auth token. description: | The body of the request needs to contain a set of instructions. Currently the only operation supported is `refresh`, with path `/payload`: ```JSON { "operations": [ { "operation": "refresh", "path": "/payload", "value": "REFRESH_ID" } ] } ``` If you do not know your REFRESH_ID, it should be set in a cookie by the server. If you leave `value` as `null` and include this cookie, then we will automatically refresh. Also, if you are using a cookie, you should really consider just using a GET on /api/v1/tokens as it has the same effect. parameters: - name: patch in: body required: true description: Instructions for what to do schema: $ref: '#/definitions/Patch' responses: 200: description: New Auth token schema: $ref: '#/definitions/RefreshToken' 400: $ref: '#/definitions/400Error' 404: $ref: '#/definitions/404Error' 50x: $ref: '#/definitions/50xError' tags: - Tokens """ operations = self.parser.parse_patch(self.request.decoded_body, many=True, from_string=True) token = None for op in operations: if op.operation == "refresh": if op.path == "/payload": token = self._refresh_token(op.value) else: raise ModelValidationError("Unsupported path '%s'" % op.path) else: raise ModelValidationError("Unsupported operation '%s'" % op.operation) self.write(json.dumps(token)) def delete(self): """ --- summary: Remove a refresh token description: | Your refresh token can either be set in a cookie (which we set on your session when you logged in) or you can include the refresh ID as a header named "X-BG-RefreshID" responses: 204: description: Token has been successfully deleted 404: $ref: '#/definitions/404Error' 50x: $ref: '#/definitions/50xError' tags: - Tokens """ token = self._get_refresh_token() if token: token.delete() self.clear_cookie(self.REFRESH_COOKIE_NAME) self.set_status(204) return raise HTTPError(status_code=403, log_message="Bad credentials") @coroutine def post(self): """ --- summary: Use credentials to generate access and refresh tokens responses: 200: description: All Tokens schema: type: array items: $ref: '#/definitions/Command' 50x: $ref: '#/definitions/50xError' tags: - Tokens """ parsed_body = json.loads(self.request.decoded_body) try: principal = Principal.objects.get(username=parsed_body["username"]) if (brew_view.config.auth.guest_login_enabled and principal.username == brew_view.anonymous_principal.username): verified = True else: verified = yield self.executor.submit( verify, str(parsed_body["password"]), str(principal.hash)) if verified: tokens = generate_tokens(principal, self.REFRESH_COOKIE_EXP) # This is a semi-done solution. To really do this, we cannot give them # a token, instead we should return an error, indicating they need to # update their password, and then login again. In the short term, this # will be enough. This is really meant only to work for our UI so # backwards compatibility is not a concern. if principal.metadata.get( "auto_change" ) and not principal.metadata.get("changed"): self.set_header("change_password_required", "true") if parsed_body.get("remember_me", False): self.set_secure_cookie( self.REFRESH_COOKIE_NAME, tokens["refresh"], expires_days=self.REFRESH_COOKIE_EXP, ) self.write(json.dumps(tokens)) return except DoesNotExist: # Still attempt to verify something so the request takes a while custom_app_context.verify("", None) raise HTTPError(status_code=403, log_message="Bad credentials") def _get_refresh_token(self, token_id=None): if not token_id: token_id = self.get_refresh_id_from_cookie() if not token_id and self.request.headers: token_id = self.request.headers.get("X-BG-RefreshID", None) if token_id: try: return RefreshToken.objects.get(id=token_id) except DoesNotExist: pass return None def _refresh_token(self, token_id=None): token = self._get_refresh_token(token_id) if token and datetime.utcnow() < token.expires: return {"token": generate_access_token(token.payload)} else: raise HTTPError(status_code=403, log_message="Bad credentials")
def setUpClass(cls): # brew_view.load_app(environment="test") cls.parser = MongoParser()
def patch(self, user_id): """ --- summary: Partially update a User description: | The body of the request needs to contain a set of instructions detailing the updates to apply: ```JSON { "operations": [ { "operation": "add", "path": "/roles", "value": "admin" } ] } ``` parameters: - name: user_id in: path required: true description: The ID of the User type: string - name: patch in: body required: true description: Instructions for how to update the User schema: $ref: '#/definitions/Patch' responses: 200: description: User with the given ID schema: $ref: '#/definitions/User' 400: $ref: '#/definitions/400Error' 404: $ref: '#/definitions/404Error' 50x: $ref: '#/definitions/50xError' tags: - Users """ principal = Principal.objects.get(id=str(user_id)) operations = MongoParser.parse_patch(self.request.decoded_body, many=True, from_string=True) # Most things only need a permission check if updating a different user if user_id != str(self.current_user.id): check_permission(self.current_user, [Permissions.USER_UPDATE]) for op in operations: if op.path == "/roles": # Updating roles always requires USER_UPDATE check_permission(self.current_user, [Permissions.USER_UPDATE]) try: if op.operation == "add": principal.roles.append(Role.objects.get(name=op.value)) elif op.operation == "remove": principal.roles.remove(Role.objects.get(name=op.value)) elif op.operation == "set": principal.roles = [ Role.objects.get(name=name) for name in op.value ] else: raise ModelValidationError( "Unsupported operation '%s'" % op.operation) except DoesNotExist: raise ModelValidationError("Role '%s' does not exist" % op.value) elif op.path == "/username": if op.operation == "update": principal.username = op.value else: raise ModelValidationError("Unsupported operation '%s'" % op.operation) elif op.path == "/password": if op.operation != "update": raise ModelValidationError("Unsupported operation '%s'" % op.operation) if isinstance(op.value, dict): current_password = op.value.get("current_password") new_password = op.value.get("new_password") else: current_password = None new_password = op.value if user_id == str(self.current_user.id): if current_password is None: raise ModelValidationError( "In order to update your own password, you must provide " "your current password") if not custom_app_context.verify(current_password, self.current_user.hash): raise RequestForbidden("Invalid password") principal.hash = custom_app_context.hash(new_password) if "changed" in principal.metadata: principal.metadata["changed"] = True elif op.path == "/preferences/theme": if op.operation == "set": principal.preferences["theme"] = op.value else: raise ModelValidationError("Unsupported operation '%s'" % op.operation) else: raise ModelValidationError("Unsupported path '%s'" % op.path) principal.save() principal.permissions = coalesce_permissions(principal.roles)[1] self.write(MongoParser.serialize_principal(principal, to_string=False))
class InstanceAPI(BaseHandler): parser = MongoParser() 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 QueueListAPI(BaseHandler): parser = MongoParser() 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 SystemAPI(BaseHandler): parser = MongoParser() 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.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))
class SystemListAPI(BaseHandler): parser = MongoParser() 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 mongo_role(role_dict): role = role_dict.copy() role["roles"] = [] return MongoParser().parse_role(role, False)
def mongo_principal(principal_dict): principal = principal_dict.copy() del principal["permissions"] return MongoParser().parse_principal(principal, False)
class JobListAPI(BaseHandler): parser = MongoParser() 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=document.max_instances, 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 RequestListAPI(BaseHandler): parser = MongoParser() logger = logging.getLogger(__name__) indexes = [index["name"] for index in Request._meta["indexes"]] @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") query_set, requested_fields = self._get_query_set() # Actually execute the query. The slicing greatly reduces load time. start = int(self.get_argument("start", default=0)) length = int(self.get_argument("length", default=100)) requests = query_set[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": query_set.count(), # This is another query "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)) 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 _get_query_set(self): """Get Requests matching the HTTP request query parameters. :return query_set: The QuerySet representing this query :return requested_fields: The fields to be returned for each Request """ search_params = [] requested_fields = [] order_by = None overall_search = None include_children = False hint = [] query_set = 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("~") start_query = Q() end_query = Q() if search_dates[0]: start_query = Q( **{column["data"] + "__gte": search_dates[0]}) if search_dates[1]: end_query = Q( **{column["data"] + "__lte": search_dates[1]}) search_query = start_query & end_query elif column["data"] == "status": search_query = Q(**{ column["data"] + "__exact": column["search"]["value"] }) elif column["data"] == "comment": search_query = Q( **{ column["data"] + "__contains": column["search"]["value"] }) else: search_query = Q( **{ column["data"] + "__startswith": column["search"]["value"] }) search_params.append(search_query) hint.append(column["data"]) raw_order = self.get_query_argument("order", default=None) if raw_order: order = json.loads(raw_order) order_by = columns[order.get("column")]["data"] hint.append(order_by) 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)) include_children = True # Now we can construct the actual query parameters query_params = reduce(lambda x, y: x & y, search_params, Q()) query_set = query_set.filter(query_params) # And set the ordering if order_by: query_set = query_set.order_by(order_by) # Marshmallow treats [] as 'serialize nothing' which is not what we # want, so translate to None if requested_fields: query_set = query_set.only(*requested_fields) else: requested_fields = None # Mongo seems to prefer using only the ['parent', '<sort field>'] # index, even when also filtering. So we have to help it pick the right index. # BUT pymongo will blow up if you try to use a hint with a text search. if overall_search: query_set = query_set.search_text(overall_search) else: real_hint = [] if include_children: real_hint.append("parent") if "created_at" in hint: real_hint.append("created_at") for index in ["command", "system", "instance_name", "status"]: if index in hint: real_hint.append(index) break real_hint.append("index") # Sanity check - if index is 'bad' just let mongo deal with it index_name = "_".join(real_hint) if index_name in self.indexes: query_set = query_set.hint(index_name) return query_set, requested_fields
class RequestAPI(BaseHandler): parser = MongoParser() 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() # Metrics request_updated(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 patch(self, role_id): """ --- summary: Partially update a Role description: | The body of the request needs to contain a set of instructions detailing the updates to apply: ```JSON { "operations": [ { "operation": "add", "path": "/permissions", "value": "ALL" } ] } ``` parameters: - name: role_id in: path required: true description: The ID of the Role type: string - name: patch in: body required: true description: Instructions for how to update the Role schema: $ref: '#/definitions/Patch' responses: 200: description: Role with the given ID schema: $ref: '#/definitions/Role' 400: $ref: '#/definitions/400Error' 404: $ref: '#/definitions/404Error' 50x: $ref: '#/definitions/50xError' tags: - Roles """ role = Role.objects.get(id=str(role_id)) operations = MongoParser.parse_patch(self.request.decoded_body, many=True, from_string=True) for op in operations: if op.path == "/permissions": try: if op.operation == "add": role.permissions.append(Permissions(op.value).value) elif op.operation == "remove": role.permissions.remove(Permissions(op.value).value) elif op.operation == "set": role.permissions = [ Permissions(perm).value for perm in op.value ] else: raise ModelValidationError( "Unsupported operation '%s'" % op.operation) except ValueError: raise ModelValidationError( "Permission '%s' does not exist" % op.value) elif op.path == "/roles": try: if op.operation == "add": new_nested = Role.objects.get(name=op.value) ensure_no_cycles(role, new_nested) role.roles.append(new_nested) elif op.operation == "remove": role.roles.remove(Role.objects.get(name=op.value)) elif op.operation == "set": # Do this one at a time to be super sure about cycles role.roles = [] for role_name in op.value: new_role = Role.objects.get(name=role_name) ensure_no_cycles(role, new_role) role.roles.append(new_role) else: raise ModelValidationError( "Unsupported operation '%s'" % op.operation) except DoesNotExist: raise ModelValidationError("Role '%s' does not exist" % op.value) elif op.path == "/description": if op.operation != "update": raise ModelValidationError("Unsupported operation '%s'" % op.operation) role.description = op.value else: raise ModelValidationError("Unsupported path '%s'" % op.path) role.save() # Any modification to roles will possibly modify the anonymous user brew_view.anonymous_principal = anonymous_principal() self.write(MongoParser.serialize_role(role, to_string=False))
class LoggingConfigAPI(BaseHandler): parser = MongoParser() 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))