Example #1
0
    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))
Example #2
0
    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))
Example #3
0
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))
Example #4
0
 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))
Example #5
0
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)
Example #6
0
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")
Example #7
0
    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)
Example #9
0
    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))
Example #10
0
 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))
Example #11
0
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)
Example #12
0
    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
Example #13
0
 def _event_serialize(self, event, **kwargs):
     return MongoParser.parse_event(
         MongoParser.serialize_event(event, to_string=False))
Example #14
0
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)
Example #15
0
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")
Example #16
0
 def setUpClass(cls):
     # brew_view.load_app(environment="test")
     cls.parser = MongoParser()
Example #17
0
    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))
Example #18
0
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)
Example #19
0
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)
Example #20
0
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))
Example #21
0
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
Example #22
0
def mongo_role(role_dict):
    role = role_dict.copy()
    role["roles"] = []
    return MongoParser().parse_role(role, False)
Example #23
0
def mongo_principal(principal_dict):
    principal = principal_dict.copy()
    del principal["permissions"]
    return MongoParser().parse_principal(principal, False)
Example #24
0
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))
Example #25
0
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
Example #26
0
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)
Example #27
0
    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))
Example #28
0
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))