Beispiel #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 = BeerGardenSchemaParser.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(BeerGardenSchemaParser.serialize_role(role,
                                                         to_string=False))
Beispiel #2
0
class CommandAPI(BaseHandler):

    parser = BeerGardenSchemaParser()
    logger = logging.getLogger(__name__)

    @authenticated(permissions=[Permissions.COMMAND_READ])
    def get(self, command_id):
        """
        ---
        summary: Retrieve a specific Command
        parameters:
          - name: command_id
            in: path
            required: true
            description: The ID of the Command
            type: string
        responses:
          200:
            description: Command with the given ID
            schema:
              $ref: '#/definitions/Command'
          404:
            $ref: '#/definitions/404Error'
          50x:
            $ref: '#/definitions/50xError'
        tags:
          - Commands
        """
        self.logger.debug("Getting Command: %s", command_id)

        self.write(
            self.parser.serialize_command(
                Command.objects.get(id=str(command_id)), to_string=False))
Beispiel #3
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(
         BeerGardenSchemaParser.serialize_role(
             Role.objects.get(id=str(role_id)), to_string=False))
Beispiel #4
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(BeerGardenSchemaParser.serialize_principal(
            principals,
            to_string=True,
            many=True
        ))
Beispiel #5
0
class AdminAPI(BaseHandler):

    parser = BeerGardenSchemaParser()
    logger = logging.getLogger(__name__)

    @coroutine
    def patch(self):
        """
        ---
        summary: Initiate a rescan of the plugin directory
        description: |
          The body of the request needs to contain a set of instructions
          detailing the operations to perform.

          Currently the only operation supported is `rescan`:
          ```JSON
          {
            "operations": [
              { "operation": "rescan" }
            ]
          }
          ```
          * Will remove from the registry and database any currently stopped
            plugins who's directory has been removed.
          * Will add and start any new plugin directories.
        parameters:
          - name: patch
            in: body
            required: true
            description: Instructions for operations
            schema:
              $ref: '#/definitions/Patch'
        responses:
          204:
            description: Rescan successfully initiated
          50x:
            $ref: '#/definitions/50xError'
        tags:
          - Admin
        """
        operations = self.parser.parse_patch(
            self.request.decoded_body, many=True, from_string=True)

        for op in operations:
            if op.operation == 'rescan':
                check_permission(self.current_user, [Permissions.SYSTEM_CREATE])
                with thrift_context() as client:
                    yield client.rescanSystemDirectory()
            else:
                error_msg = "Unsupported operation '%s'" % op.operation
                self.logger.warning(error_msg)
                raise ModelValidationError(error_msg)

        self.set_status(204)
Beispiel #6
0
class EventSocket(AuthMixin, WebSocketHandler):

    logger = logging.getLogger(__name__)
    parser = BeerGardenSchemaParser()

    closing = False
    listeners = set()

    def __init__(self, *args, **kwargs):
        super(EventSocket, self).__init__(*args, **kwargs)

        self.auth_providers.append(query_token_auth)

    def check_origin(self, origin):
        return True

    def open(self):
        if EventSocket.closing:
            self.close(reason='Shutting down')
            return

        # We can't go though the 'normal' BaseHandler exception translation
        try:
            check_permission(self.current_user, [Permissions.EVENT_READ])
        except (HTTPError, RequestForbidden) as ex:
            self.close(reason=str(ex))
            return

        EventSocket.listeners.add(self)

    def on_close(self):
        EventSocket.listeners.discard(self)

    def on_message(self, message):
        pass

    @classmethod
    def publish(cls, message):
        # Don't bother if nobody is listening
        if not len(cls.listeners):
            return

        for listener in cls.listeners:
            listener.write_message(message)

    @classmethod
    def shutdown(cls):
        cls.logger.debug('Closing websocket connections')
        EventSocket.closing = True

        for listener in cls.listeners:
            listener.close(reason='Shutting down')
    def test_get(self):
        response = self.app.get("/api/v1/requests/id")

        self.assertEqual(200, response.status_code)
        self.objects_mock.get.assert_called_with(id='id')
        self.objects_mock.assert_called_with(parent=self.default_request)

        response_request = BeerGardenSchemaParser().parse_request(
            response.data, from_string=True)
        self.assertEqual(self.default_request.system, response_request.system)
        self.assertEqual(self.default_request.command,
                         response_request.command)
        self.assertDictEqual(self.default_request.parameters,
                             response_request.parameters)
        self.assertEqual(self.default_request.output, response_request.output)
        self.assertEqual(self.default_request.status, response_request.status)
Beispiel #8
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(
         BeerGardenSchemaParser.serialize_role(Role.objects.all(),
                                               many=True,
                                               to_string=True))
Beispiel #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 [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(BeerGardenSchemaParser.serialize_principal(
            principal, to_string=False))
Beispiel #10
0
class CommandListAPI(BaseHandler):

    parser = BeerGardenSchemaParser()
    logger = logging.getLogger(__name__)

    @authenticated(permissions=[Permissions.COMMAND_READ])
    def get(self):
        """
        ---
        summary: Retrieve all Commands
        responses:
          200:
            description: All Commands
            schema:
              type: array
              items:
                $ref: '#/definitions/Command'
          50x:
            $ref: '#/definitions/50xError'
        tags:
          - Commands
        """
        self.logger.debug("Getting Commands")

        self.set_header('Content-Type', 'application/json; charset=UTF-8')
        try:
            self.write(
                self.parser.serialize_command(Command.objects.all(),
                                              many=True,
                                              to_string=True))
        except mongoengine.errors.DoesNotExist as ex:
            self.logger.error(
                "Got an error while attempting to serialize commands. "
                "This error usually indicates "
                "there are orphans in the database.")
            raise mongoengine.errors.InvalidDocumentError(ex)
Beispiel #11
0
class JobListAPI(BaseHandler):

    parser = BeerGardenSchemaParser()
    logger = logging.getLogger(__name__)

    @authenticated(permissions=[Permissions.JOB_READ])
    def get(self):
        """
        ---
        summary: Retrieve all Jobs.
        responses:
          200:
            description: Successfully retrieved all systems.
            schema:
              type: array
              items:
                $ref: '#/definitions/Job'
          50x:
            $ref: '#/definitions/50xError'
        tags:
          - Jobs
        """
        filter_params = {}
        for key in self.request.arguments.keys():
            if key in JobSchema.get_attribute_names():
                filter_params[key] = self.get_query_argument(key)

        self.set_header('Content-Type', 'application/json; charset=UTF-8')
        self.write(
            self.parser.serialize_job(Job.objects.filter(**filter_params),
                                      to_string=True,
                                      many=True))

    @coroutine
    @authenticated(permissions=[Permissions.JOB_CREATE])
    def post(self):
        """
        ---
        summary: Schedules a Job to be run.
        description: |
          Given a job, it will be scheduled to run on the interval
          set in the trigger argument.
        parameters:
          - name: job
            in: body
            description: The Job to create/schedule
            schema:
              $ref: '#/definitions/Job'
        responses:
          201:
            description: A new job has been created
            schema:
              $ref: '#/definitions/Job'
          400:
            $ref: '#/definitions/400Error'
          50x:
            $ref: '#/definitions/50xError'
        tags:
          - Jobs
        """
        document = self.parser.parse_job(self.request.decoded_body,
                                         from_string=True)
        # We have to save here, because we need an ID to pass
        # to the scheduler.
        document.save()

        try:
            brew_view.request_scheduler.add_job(
                run_job,
                None,
                kwargs={
                    'request_template': document.request_template,
                    'job_id': str(document.id),
                },
                name=document.name,
                misfire_grace_time=document.misfire_grace_time,
                coalesce=document.coalesce,
                max_instances=3,
                jobstore='beer_garden',
                replace_existing=False,
                id=str(document.id),
            )
        except Exception:
            document.delete()
            raise

        self.set_status(201)
        self.set_header('Content-Type', 'application/json; charset=UTF-8')
        self.write(self.parser.serialize_job(document, to_string=False))
Beispiel #12
0
class SystemListAPI(BaseHandler):

    parser = BeerGardenSchemaParser()
    logger = logging.getLogger(__name__)

    REQUEST_FIELDS = set(SystemSchema.get_attribute_names())

    # Need to ensure that Systems are updated atomically
    system_lock = Lock()

    @authenticated(permissions=[Permissions.SYSTEM_READ])
    def get(self):
        """
        ---
        summary: Retrieve all Systems
        description: |
          This endpoint allows for querying Systems.

          There are several parameters that control which fields are returned
          and what information is available. Things to be aware of:

          * The `include_commands` parameter is __deprecated__. Don't use it.
            Use `exclude_fields=commands` instead.

          * It's possible to specify `include_fields` _and_ `exclude_fields`.
            This doesn't make a lot of sense, but you can do it. If the same
            field is in both `exclude_fields` takes priority (the field will
            NOT be included in the response).

          Systems matching specific criteria can be filtered using additional
          query parameters. This is a very basic capability:

          * ?name=foo&version=1.0.0
            This will return the system named 'foo' with version '1.0.0'
          * ?name=foo&name=bar
            This will not do what you expect: only return the system named
            'bar' will be returned.
        parameters:
          - name: include_fields
            in: query
            required: false
            description: Specify fields to include in the response. All other
              fields will be excluded.
            type: array
            collectionFormat: csv
            items:
              type: string
          - name: exclude_fields
            in: query
            required: false
            description: Specify fields to exclude from the response
            type: array
            collectionFormat: csv
            items:
              type: string
          - name: dereference_nested
            in: query
            required: false
            description: Commands and instances will be an object id
            type: boolean
            default: true
          - name: include_commands
            in: query
            required: false
            description: __DEPRECATED__ Include commands in the response.
              Use `exclude_fields=commands` instead.
            type: boolean
            default: true
        responses:
          200:
            description: All Systems
            schema:
              type: array
              items:
                $ref: '#/definitions/System'
          50x:
            $ref: '#/definitions/50xError'
        tags:
          - Systems
        """
        query_set = System.objects.order_by(
            self.request.headers.get('order_by', 'name'))
        serialize_params = {'to_string': True, 'many': True}

        include_fields = self.get_query_argument('include_fields', None)
        exclude_fields = self.get_query_argument('exclude_fields', None)
        dereference_nested = self.get_query_argument('dereference_nested',
                                                     None)
        include_commands = self.get_query_argument('include_commands', None)

        if include_fields:
            include_fields = set(
                include_fields.split(',')) & self.REQUEST_FIELDS
            query_set = query_set.only(*include_fields)
            serialize_params['only'] = include_fields

        if exclude_fields:
            exclude_fields = set(
                exclude_fields.split(',')) & self.REQUEST_FIELDS
            query_set = query_set.exclude(*exclude_fields)
            serialize_params['exclude'] = exclude_fields

        if include_commands and include_commands.lower() == 'false':
            query_set = query_set.exclude('commands')

            if 'exclude' not in serialize_params:
                serialize_params['exclude'] = set()
            serialize_params['exclude'].add('commands')

        if dereference_nested and dereference_nested.lower() == 'false':
            query_set = query_set.no_dereference()

        # TODO - Handle multiple query arguments with the same key
        # for example: (?name=foo&name=bar) ... what should that mean?
        filter_params = {}

        # Need to use self.request.query_arguments to get all the query args
        for key in self.request.query_arguments:
            if key in self.REQUEST_FIELDS:
                # Now use get_query_argument to get the decoded value
                filter_params[key] = self.get_query_argument(key)

        result_set = query_set.filter(**filter_params)

        self.set_header('Content-Type', 'application/json; charset=UTF-8')
        self.write(self.parser.serialize_system(result_set,
                                                **serialize_params))

    @coroutine
    @authenticated(permissions=[Permissions.SYSTEM_CREATE])
    def post(self):
        """
        ---
        summary: Create a new System or update an existing System
        description: If the System does not exist it will be created. If the System already exists
            it will be updated (assuming it passes validation).
        parameters:
          - name: system
            in: body
            description: The System definition to create / update
            schema:
              $ref: '#/definitions/System'
        responses:
          200:
            description: An existing System has been updated
            schema:
              $ref: '#/definitions/System'
          201:
            description: A new System has been created
            schema:
              $ref: '#/definitions/System'
          400:
            $ref: '#/definitions/400Error'
          50x:
            $ref: '#/definitions/50xError'
        tags:
          - Systems
        """
        self.request.event.name = Events.SYSTEM_CREATED.name

        system_model = self.parser.parse_system(self.request.decoded_body,
                                                from_string=True)

        with (yield self.system_lock.acquire()):
            # See if we already have a system with this name + version
            existing_system = System.find_unique(system_model.name,
                                                 system_model.version)

            if not existing_system:
                self.logger.debug("Creating a new system: %s" %
                                  system_model.name)
                saved_system, status_code = self._create_new_system(
                    system_model)
            else:
                self.logger.debug("System %s already exists. Updating it." %
                                  system_model.name)
                self.request.event.name = Events.SYSTEM_UPDATED.name
                saved_system, status_code = self._update_existing_system(
                    existing_system, system_model)

            saved_system.deep_save()

        self.request.event_extras = {'system': saved_system}

        self.set_status(status_code)
        self.write(
            self.parser.serialize_system(saved_system,
                                         to_string=False,
                                         include_commands=True))

    @staticmethod
    def _create_new_system(system_model):
        new_system = system_model

        # Assign a default 'main' instance if there aren't any instances and there can only be one
        if not new_system.instances or len(new_system.instances) == 0:
            if new_system.max_instances is None or new_system.max_instances == 1:
                new_system.instances = [Instance(name='default')]
                new_system.max_instances = 1
            else:
                raise ModelValidationError(
                    'Could not create system %s-%s: Systems with '
                    'max_instances > 1 must also define their instances' %
                    (system_model.name, system_model.version))
        else:
            if not new_system.max_instances:
                new_system.max_instances = len(new_system.instances)

        return new_system, 201

    @staticmethod
    def _update_existing_system(existing_system, system_model):
        # Raise an exception if commands already exist for this system and they differ from what's
        # already in the database in a significant way
        if existing_system.commands and 'dev' not in existing_system.version and \
                existing_system.has_different_commands(system_model.commands):
            raise ModelValidationError(
                'System %s-%s already exists with different commands' %
                (system_model.name, system_model.version))
        else:
            existing_system.upsert_commands(system_model.commands)

        # Update instances
        if not system_model.instances or len(system_model.instances) == 0:
            system_model.instances = [Instance(name='default')]

        for attr in ['description', 'icon_name', 'display_name']:
            value = getattr(system_model, attr)

            # If we set an attribute on the model as None, mongoengine marks the attribute for
            # deletion. We want to prevent this, so we set it to an emtpy string.
            if value is None:
                setattr(existing_system, attr, "")
            else:
                setattr(existing_system, attr, value)

        # Update metadata
        new_metadata = system_model.metadata or {}
        existing_system.metadata.update(new_metadata)

        old_instances = [
            inst for inst in existing_system.instances
            if system_model.has_instance(inst.name)
        ]
        new_instances = [
            inst for inst in system_model.instances
            if not existing_system.has_instance(inst.name)
        ]
        existing_system.instances = old_instances + new_instances

        return existing_system, 200
Beispiel #13
0
class LoggingConfigAPI(BaseHandler):

    parser = BeerGardenSchemaParser()
    logger = logging.getLogger(__name__)

    def get(self):
        """
        ---
        summary: Get the plugin logging configuration
        parameters:
          - name: system_name
            in: query
            required: false
            description: Specific system name to get logging configuration
            type: string
        responses:
          200:
            description: Logging Configuration for system
            schema:
                $ref: '#/definitions/LoggingConfig'
          50x:
            $ref: '#/definitions/50xError'
        tags:
          - Config
        """
        system_name = self.get_query_argument('system_name', default=None)
        log_config = brew_view.plugin_logging_config.get_plugin_log_config(
            system_name=system_name)
        self.write(
            self.parser.serialize_logging_config(log_config, to_string=False))

    @coroutine
    def patch(self):
        """
        ---
        summary: Reload the plugin logging configuration
        description: |
          The body of the request needs to contain a set of instructions detailing the operation
          to make. Currently supported operations are below:
          ```JSON
          {
            "operations": [
                { "operation": "reload" }
            ]
          }
          ```
        parameters:
          - name: patch
            in: body
            required: true
            description: Operation to perform
            schema:
              $ref: '#/definitions/Patch'
        responses:
          200:
            description: Updated plugin logging configuration
            schema:
              $ref: '#/definitions/LoggingConfig'
          50x:
            $ref: '#/definitions/50xError'
        tags:
          - Config
        """
        operations = self.parser.parse_patch(self.request.decoded_body,
                                             many=True,
                                             from_string=True)

        for op in operations:
            if op.operation == 'reload':
                brew_view.load_plugin_logging_config(brew_view.config)
            else:
                error_msg = "Unsupported operation '%s'" % op.operation
                self.logger.warning(error_msg)
                raise ModelValidationError('value', error_msg)

        self.set_status(200)
        self.write(
            self.parser.serialize_logging_config(
                brew_view.plugin_logging_config, to_string=False))
Beispiel #14
0
 def setUpClass(cls):
     # brew_view.load_app(environment="test")
     cls.parser = BeerGardenSchemaParser()
Beispiel #15
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 = BeerGardenSchemaParser.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)

            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(BeerGardenSchemaParser.serialize_role(role,
                                                         to_string=False))
Beispiel #16
0
class SystemAPI(BaseHandler):

    parser = BeerGardenSchemaParser()
    logger = logging.getLogger(__name__)

    @authenticated(permissions=[Permissions.SYSTEM_READ])
    def get(self, system_id):
        """
        ---
        summary: Retrieve a specific System
        parameters:
          - name: system_id
            in: path
            required: true
            description: The ID of the System
            type: string
          - name: include_commands
            in: query
            required: false
            description: Include the System's commands in the response
            type: boolean
            default: true
        responses:
          200:
            description: System with the given ID
            schema:
              $ref: '#/definitions/System'
          404:
            $ref: '#/definitions/404Error'
          50x:
            $ref: '#/definitions/50xError'
        tags:
          - Systems
        """
        self.logger.debug("Getting System: %s", system_id)

        include_commands = self.get_query_argument(
            'include_commands', default='true').lower() != 'false'
        self.write(self.parser.serialize_system(System.objects.get(id=system_id), to_string=False,
                                                include_commands=include_commands))

    @coroutine
    @authenticated(permissions=[Permissions.SYSTEM_DELETE])
    def delete(self, system_id):
        """
        Will give Bartender a chance to remove instances of this system from the registry but will
        always delete the system regardless of whether the Bartender operation succeeds.
        ---
        summary: Delete a specific System
        description: Will remove instances of local plugins from the registry, clear and remove
            message queues, and remove the system from the database.
        parameters:
          - name: system_id
            in: path
            required: true
            description: The ID of the System
            type: string
        responses:
          204:
            description: System has been successfully deleted
          404:
            $ref: '#/definitions/404Error'
          50x:
            $ref: '#/definitions/50xError'
        tags:
          - Systems
        """
        self.request.event.name = Events.SYSTEM_REMOVED.name
        self.request.event_extras = {'system': System.objects.get(id=system_id)}

        with thrift_context() as client:
            yield client.removeSystem(str(system_id))

        self.set_status(204)

    @coroutine
    @authenticated(permissions=[Permissions.SYSTEM_UPDATE])
    def patch(self, system_id):
        """
        ---
        summary: Partially update a System
        description: |
          The body of the request needs to contain a set of instructions detailing the updates to
          apply.
          Currently supported operations are below:
          ```JSON
          {
            "operations": [
              { "operation": "replace", "path": "/commands", "value": "" },
              { "operation": "replace", "path": "/description", "value": "new description"},
              { "operation": "replace", "path": "/display_name", "value": "new display name"},
              { "operation": "replace", "path": "/icon_name", "value": "new icon name"},
              { "operation": "update", "path": "/metadata", "value": {"foo": "bar"}}
            ]
          }
          ```
          Where `value` is a list of new Commands.
        parameters:
          - name: system_id
            in: path
            required: true
            description: The ID of the System
            type: string
          - name: patch
            in: body
            required: true
            description: Instructions for how to update the System
            schema:
              $ref: '#/definitions/Patch'
        responses:
          200:
            description: System with the given ID
            schema:
              $ref: '#/definitions/System'
          400:
            $ref: '#/definitions/400Error'
          404:
            $ref: '#/definitions/404Error'
          50x:
            $ref: '#/definitions/50xError'
        tags:
          - Systems
        """
        self.request.event.name = Events.SYSTEM_UPDATED.name

        system = System.objects.get(id=system_id)
        operations = self.parser.parse_patch(self.request.decoded_body, many=True, from_string=True)

        for op in operations:
            if op.operation == 'replace':
                if op.path == '/commands':
                    new_commands = self.parser.parse_command(op.value, many=True)
                    if system.has_different_commands(new_commands):
                        if system.commands and 'dev' not in system.version:
                            raise ModelValidationError('System %s-%s already exists with '
                                                       'different commands' %
                                                       (system.name, system.version))
                        else:
                            system.upsert_commands(new_commands)
                elif op.path in ['/description', '/icon_name', '/display_name']:
                    if op.value is None:
                        # If we set an attribute to None, mongoengine marks that attribute
                        # for deletion, so we don't do that.
                        value = ""
                    else:
                        value = op.value
                    attr = op.path.strip("/")
                    self.logger.debug("Updating system %s" % attr)
                    self.logger.debug("Old: %s" % getattr(system, attr))
                    setattr(system, attr, value)
                    self.logger.debug("Updated: %s" % getattr(system, attr))
                    system.save()
                else:
                    error_msg = "Unsupported path '%s'" % op.path
                    self.logger.warning(error_msg)
                    raise ModelValidationError('value', error_msg)
            elif op.operation == 'update':
                if op.path == '/metadata':
                    self.logger.debug("Updating system metadata")
                    self.logger.debug("Old: %s" % system.metadata)
                    system.metadata.update(op.value)
                    self.logger.debug("Updated: %s" % system.metadata)
                    system.save()
                else:
                    error_msg = "Unsupported path for update '%s'" % op.path
                    self.logger.warning(error_msg)
                    raise ModelValidationError('path', error_msg)
            elif op.operation == 'reload':
                with thrift_context() as client:
                    yield client.reloadSystem(system_id)
            else:
                error_msg = "Unsupported operation '%s'" % op.operation
                self.logger.warning(error_msg)
                raise ModelValidationError('value', error_msg)

        system.reload()

        self.request.event_extras = {'system': system, 'patch': operations}

        self.write(self.parser.serialize_system(system, to_string=False))
Beispiel #17
0
 def _event_serialize(self, event, **kwargs):
     return BeerGardenSchemaParser.parse_event(
         BeerGardenSchemaParser.serialize_event(event, to_string=False))
Beispiel #18
0
class RequestListAPI(BaseHandler):

    parser = BeerGardenSchemaParser()
    logger = logging.getLogger(__name__)

    @authenticated(permissions=[Permissions.REQUEST_READ])
    def get(self):
        """
        ---
        summary: Retrieve a page of all Requests
        description: |
          This endpoint queries multiple requests at once. Because it's intended to be used with
          Datatables the query parameters are ... complicated. Here are things to keep in mind:

          * With no query parameters this endpoint will return the first 100 non-child requests.
            This can be controlled by passing the `start` and `length` query parameters.

          * This endpoint does NOT return child request definitions. If you want to see child
            requests you must use the /api/v1/requests/{request_id} endpoint.

          * By default this endpoint also does not include child requests in the response. That
            is, if a request has a non-null `parent` field it will not be included in the response
            array. Use the `include_children` query parameter to change this behavior.

          To filter, search, and order you need to conform to how Datatables structures its query
          parameters.

          * To indicate fields that should be included in the response specify multiple `columns`
            query parameters:
          ```JSON
          {
            "data": "command",
            "name": "",
            "searchable": true,
            "orderable": true,
            "search": {"value":"","regex":false}
          }
          {
            "data": "system",
            "name": "",
            "searchable": true,
            "orderable": true,
            "search": {"value": "","regex": false}
          }
          ```
          * To filter a specific field set the value in the `search` key of its `column` definition:
          ```JSON
          {
            "data": "status",
            "name": "",
            "searchable": true,
            "orderable": true,
            "search": {"value": "SUCCESS", "regex":false}
          }
          ```

          * To sort by a field use the `order` parameter. The `column` value should be the index
            of the column to sort and the `dir` value can be either "asc" or "desc."
          ```JSON
          {"column": 3,"dir": "asc"}
          ```

          * To perform a text-based search across all fields use the `search` parameter:
          ```JSON
          { "value": "SEARCH VALUE", "regex": false }
          ```
        parameters:
          - name: include_children
            in: query
            required: false
            description: Flag indicating whether to include child requests in the response list
            type: boolean
            default: false
          - name: start
            in: query
            required: false
            description: The starting index for the page
            type: integer
          - name: length
            in: query
            required: false
            description: The maximum number of Requests to include in the page
            type: integer
          - name: draw
            in: query
            required: false
            description: Used by datatables, will be echoed in a response header
            type: integer
          - name: columns
            in: query
            required: false
            description: Datatables column definitions
            type: array
            collectionFormat: multi
            items:
              properties:
                data:
                  type: string
                name:
                  type: string
                searchable:
                  type: boolean
                  default: true
                orderable:
                  type: boolean
                  default: true
                search:
                  properties:
                    value:
                      type: string
                    regex:
                      type: boolean
                      default: false
          - name: search
            in: query
            required: false
            description: Datatables search object
            type: string
          - name: order
            in: query
            required: false
            description: Datatables order object
            type: string
        responses:
          200:
            description: A page of Requests
            schema:
              type: array
              items:
                $ref: '#/definitions/Request'
            headers:
              start:
                type: integer
                description: Echo of 'start' query parameter or '0'
              length:
                type: integer
                description: Number of Requests in the response
              draw:
                type: integer
                description: Echo of the 'draw' query parameter
              recordsFiltered:
                type: integer
                description: The number of Requests that satisfied the search filters
              recordsTotal:
                type: integer
                description: The total number of Requests
          50x:
            $ref: '#/definitions/50xError'
        tags:
          - Requests
        """
        self.logger.debug("Getting Requests")

        # We need to do the slicing on the query. This greatly reduces load time.
        start = int(self.get_argument('start', default=0))
        length = int(self.get_argument('length', default=100))
        requests, filtered_count, requested_fields = self._get_requests(
            start, start + length)

        # Sweet, we have data. Now setup some headers for the response
        response_headers = {
            # These are a courtesy for non-datatables requests. We want people making a request
            # with no headers to realize they probably aren't getting the full dataset
            'start': start,
            'length': len(requests),

            # And these are required by datatables
            'recordsFiltered': filtered_count,
            'recordsTotal': Request.objects.count(),
            'draw': self.get_argument('draw', '')
        }

        for key, value in response_headers.items():
            self.add_header(key, value)
            self.add_header('Access-Control-Expose-Headers', key)

        self.set_header('Content-Type', 'application/json; charset=UTF-8')
        self.write(
            self.parser.serialize_request(requests,
                                          to_string=True,
                                          many=True,
                                          only=requested_fields))

    @coroutine
    @authenticated(permissions=[Permissions.REQUEST_CREATE])
    def post(self):
        """
        ---
        summary: Create a new Request
        parameters:
          - name: request
            in: body
            description: The Request definition
            schema:
              $ref: '#/definitions/Request'
          - name: blocking
            in: query
            required: false
            description: Flag indicating whether to wait for request completion
            type: boolean
            default: false
          - name: timeout
            in: query
            required: false
            description: Maximum time (seconds) to wait for request completion
            type: integer
            default: None (Wait forever)
        consumes:
          - application/json
          - application/x-www-form-urlencoded
        responses:
          201:
            description: A new Request has been created
            schema:
              $ref: '#/definitions/Request'
            headers:
              Instance-Status:
                type: string
                description: Current status of the Instance that will process the created Request
          400:
            $ref: '#/definitions/400Error'
          50x:
            $ref: '#/definitions/50xError'
        tags:
          - Requests
        """
        self.request.event.name = Events.REQUEST_CREATED.name

        if self.request.mime_type == 'application/json':
            request_model = self.parser.parse_request(
                self.request.decoded_body, from_string=True)
        elif self.request.mime_type == 'application/x-www-form-urlencoded':
            args = {'parameters': {}}
            for key, value in self.request.body_arguments.items():
                if key.startswith('parameters.'):
                    args['parameters'][key.replace('parameters.',
                                                   '')] = value[0].decode(
                                                       self.request.charset)
                else:
                    args[key] = value[0].decode(self.request.charset)
            request_model = Request(**args)
        else:
            raise ModelValidationError(
                'Unsupported or missing content-type header')

        if request_model.parent:
            request_model.parent = Request.objects.get(
                id=str(request_model.parent.id))
            request_model.has_parent = True
        else:
            request_model.has_parent = False

        if self.current_user:
            request_model.requester = self.current_user.username

        # Ok, ready to save
        request_model.save()
        request_id = str(request_model.id)

        # Set up the wait event BEFORE yielding the processRequest call
        blocking = self.get_argument('blocking', default='').lower() == 'true'
        if blocking:
            brew_view.request_map[request_id] = Event()

        with thrift_context() as client:
            try:
                yield client.processRequest(request_id)
            except bg_utils.bg_thrift.InvalidRequest as ex:
                request_model.delete()
                raise ModelValidationError(ex.message)
            except bg_utils.bg_thrift.PublishException as ex:
                request_model.delete()
                raise RequestPublishException(ex.message)
            except Exception:
                if request_model.id:
                    request_model.delete()
                raise
            else:
                if blocking:
                    timeout = self.get_argument('timeout', default=None)
                    if timeout is None:
                        timeout_delta = None
                    else:
                        timeout_delta = timedelta(seconds=int(timeout))

                    try:
                        event = brew_view.request_map.get(request_id)
                        yield event.wait(timeout_delta)
                    except TimeoutError:
                        raise TimeoutExceededError()
                    finally:
                        brew_view.request_map.pop(request_id, None)

        request_model.reload()

        # Query for request from body id
        req = Request.objects.get(id=request_id)

        # Now attempt to add the instance status as a header.
        # The Request is already created at this point so it's a best-effort thing
        self.set_header("Instance-Status", 'UNKNOWN')

        try:
            # Since request has system info we can query for a system object
            system = System.objects.get(name=req.system,
                                        version=req.system_version)

            # Loop through all instances in the system until we find the instance that matches
            # the request instance
            for instance in system.instances:
                if instance.name == req.instance_name:
                    self.set_header("Instance-Status", instance.status)

        # The Request is already created at this point so adding the Instance status header is a
        # best-effort thing
        except Exception as ex:
            self.logger.exception(
                'Unable to get Instance status for Request %s: %s', request_id,
                ex)

        self.request.event_extras = {'request': req}

        self.set_status(201)
        self.queued_request_gauge.labels(
            system=request_model.system,
            system_version=request_model.system_version,
            instance_name=request_model.instance_name,
        ).inc()
        self.request_counter_total.labels(
            system=request_model.system,
            system_version=request_model.system_version,
            instance_name=request_model.instance_name,
            command=request_model.command,
        ).inc()
        self.write(
            self.parser.serialize_request(request_model, to_string=False))

    def _get_requests(self, start, end):
        """Get Requests matching the HTTP request query parameters.

        :return requests: The collection of requests
        :return requested_fields: The fields to be returned for each Request
        """
        search_params = []
        requested_fields = []
        order_by = None
        overall_search = None
        query = Request.objects

        raw_columns = self.get_query_arguments('columns')
        if raw_columns:
            columns = []

            for raw_column in raw_columns:
                column = json.loads(raw_column)
                columns.append(column)

                if column['data']:
                    requested_fields.append(column['data'])

                if 'searchable' in column and column['searchable'] and column[
                        'search']['value']:
                    if column['data'] in ['created_at', 'updated_at']:
                        search_dates = column['search']['value'].split('~')

                        if search_dates[0]:
                            search_params.append(
                                Q(**
                                  {column['data'] + '__gte': search_dates[0]}))

                        if search_dates[1]:
                            search_params.append(
                                Q(**
                                  {column['data'] + '__lte': search_dates[1]}))
                    else:
                        search_query = Q(
                            **{
                                column['data'] + '__contains':
                                column['search']['value']
                            })
                        search_params.append(search_query)

            raw_order = self.get_query_argument('order', default=None)
            if raw_order:
                order = json.loads(raw_order)
                order_by = columns[order.get('column')]['data']
                if order.get('dir') == 'desc':
                    order_by = '-' + order_by

        raw_search = self.get_query_argument('search', default=None)
        if raw_search:
            search = json.loads(raw_search)
            if search['value']:
                overall_search = '"' + search['value'] + '"'

        # Default to only top-level requests
        if self.get_query_argument('include_children',
                                   default='false').lower() != 'true':
            search_params.append(Q(has_parent=False))

        # Now we can construct the actual query parameters
        query_params = reduce(lambda x, y: x & y, search_params, Q())

        # Further modify the query itself
        if overall_search:
            query = query.search_text(overall_search)

        if order_by:
            query = query.order_by(order_by)

        # Marshmallow treats [] as 'serialize nothing' which is not what we
        # want, so translate to None
        if requested_fields:
            query = query.only(*requested_fields)
        else:
            requested_fields = None

        # Execute the query / count
        requests = query.filter(query_params)
        filtered_count = requests.count()

        # Only return the correct slice of the QuerySet
        return requests[start:end], filtered_count, requested_fields
Beispiel #19
0
class QueueListAPI(BaseHandler):

    parser = BeerGardenSchemaParser()
    logger = logging.getLogger(__name__)

    @coroutine
    @authenticated(permissions=[Permissions.QUEUE_READ])
    def get(self):
        """
        ---
        summary: Retrieve all queue information
        responses:
          200:
            description: List of all queue information objects
            schema:
              type: array
              items:
                $ref: '#/definitions/Queue'
          50x:
            $ref: '#/definitions/50xError'
        tags:
          - Queues
        """
        self.logger.debug("Getting all queues")

        queues = []
        systems = System.objects.all().select_related(max_depth=1)

        for system in systems:
            for instance in system.instances:

                queue = Queue(name='UNKNOWN',
                              system=system.name,
                              version=system.version,
                              instance=instance.name,
                              system_id=str(system.id),
                              display=system.display_name,
                              size=-1)

                with thrift_context() as client:
                    try:
                        queue_info = yield client.getQueueInfo(
                            system.name, system.version, instance.name)
                        queue.name = queue_info.name
                        queue.size = queue_info.size
                    except Exception:
                        self.logger.error(
                            "Error getting queue size for %s[%s]-%s" %
                            (system.name, instance.name, system.version))

                queues.append(queue)

        self.set_header('Content-Type', 'application/json; charset=UTF-8')
        self.write(
            self.parser.serialize_queue(queues, to_string=True, many=True))

    @coroutine
    @authenticated(permissions=[Permissions.QUEUE_DELETE])
    def delete(self):
        """
        ---
        summary: Cancel and clear all requests in all queues
        responses:
          204:
            description: All queues successfully cleared
          50x:
            $ref: '#/definitions/50xError'
        tags:
          - Queues
        """
        self.request.event.name = Events.ALL_QUEUES_CLEARED.name

        with thrift_context() as client:
            yield client.clearAllQueues()

        self.set_status(204)
Beispiel #20
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 = BeerGardenSchemaParser.parse_patch(
            self.request.decoded_body,
            many=True,
            from_string=True
        )

        for op in operations:
            if op.path == '/roles':
                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 == '/preferences/theme':
                if user_id != self.current_user.id:
                    check_permission(self.current_user, [Permissions.USER_UPDATE])

                if op.operation == 'set':
                    principal.preferences['theme'] = op.value
                else:
                    raise ModelValidationError("Unsupported operation '%s'" % op.operation)

            else:
                check_permission(self.current_user, [Permissions.USER_UPDATE])
                raise ModelValidationError("Unsupported path '%s'" % op.path)

        principal.save()

        self.write(BeerGardenSchemaParser.serialize_principal(principal,
                                                              to_string=False))
Beispiel #21
0
class InstanceAPI(BaseHandler):

    parser = BeerGardenSchemaParser()
    logger = logging.getLogger(__name__)

    event_dict = {
        'initialize': Events.INSTANCE_INITIALIZED.name,
        'start': Events.INSTANCE_STARTED.name,
        'stop': Events.INSTANCE_STOPPED.name
    }

    @authenticated(permissions=[Permissions.INSTANCE_READ])
    def get(self, instance_id):
        """
        ---
        summary: Retrieve a specific Instance
        parameters:
          - name: instance_id
            in: path
            required: true
            description: The ID of the Instance
            type: string
        responses:
          200:
            description: Instance with the given ID
            schema:
              $ref: '#/definitions/Instance'
          404:
            $ref: '#/definitions/404Error'
          50x:
            $ref: '#/definitions/50xError'
        tags:
          - Instances
        """
        self.logger.debug("Getting Instance: %s", instance_id)

        self.write(self.parser.serialize_instance(Instance.objects.get(id=instance_id),
                                                  to_string=False))

    @authenticated(permissions=[Permissions.INSTANCE_DELETE])
    def delete(self, instance_id):
        """
        ---
        summary: Delete a specific Instance
        parameters:
          - name: instance_id
            in: path
            required: true
            description: The ID of the Instance
            type: string
        responses:
          204:
            description: Instance has been successfully deleted
          404:
            $ref: '#/definitions/404Error'
          50x:
            $ref: '#/definitions/50xError'
        tags:
          - Instances
        """
        self.logger.debug("Deleting Instance: %s", instance_id)

        Instance.objects.get(id=instance_id).delete()

        self.set_status(204)

    @coroutine
    @authenticated(permissions=[Permissions.INSTANCE_UPDATE])
    def patch(self, instance_id):
        """
        ---
        summary: Partially update an Instance
        description: |
          The body of the request needs to contain a set of instructions detailing the updates to
          apply. Currently the only operations are:

          * initialize
          * start
          * stop
          * heartbeat

          ```JSON
          {
            "operations": [
              { "operation": "" }
            ]
          }
          ```
        parameters:
          - name: instance_id
            in: path
            required: true
            description: The ID of the Instance
            type: string
          - name: patch
            in: body
            required: true
            description: Instructions for how to update the Instance
            schema:
              $ref: '#/definitions/Patch'
        responses:
          200:
            description: Instance with the given ID
            schema:
              $ref: '#/definitions/Instance'
          400:
            $ref: '#/definitions/400Error'
          404:
            $ref: '#/definitions/404Error'
          50x:
            $ref: '#/definitions/50xError'
        tags:
          - Instances
        """
        response = {}
        instance = Instance.objects.get(id=instance_id)
        operations = self.parser.parse_patch(self.request.decoded_body, many=True, from_string=True)

        for op in operations:
            if op.operation.lower() in ('initialize', 'start', 'stop'):
                self.request.event.name = self.event_dict[op.operation.lower()]
                with thrift_context() as client:
                    response = yield getattr(client, op.operation.lower()+'Instance')(instance_id)

            elif op.operation.lower() == 'heartbeat':
                instance.status_info.heartbeat = datetime.utcnow()
                instance.save()
                response = self.parser.serialize_instance(instance, to_string=False)

            elif op.operation.lower() == 'replace':
                if op.path.lower() == '/status':
                    if op.value.upper() == 'INITIALIZING':
                        self.request.event.name = Events.INSTANCE_INITIALIZED.name
                        with thrift_context() as client:
                            response = yield client.initializeInstance(instance_id)

                    elif op.value.upper() == 'STOPPING':
                        self.request.event.name = Events.INSTANCE_STOPPED.name
                        with thrift_context() as client:
                            response = yield client.stopInstance(instance_id)

                    elif op.value.upper() == 'STARTING':
                        self.request.event.name = Events.INSTANCE_STARTED.name
                        with thrift_context() as client:
                            response = yield client.startInstance(instance_id)

                    elif op.value.upper() in ['RUNNING', 'STOPPED']:
                        instance.status = op.value.upper()
                        instance.save()
                        response = self.parser.serialize_instance(instance, to_string=False)

                    else:
                        error_msg = "Unsupported status value '%s'" % op.value
                        self.logger.warning(error_msg)
                        raise ModelValidationError('value', error_msg)
                else:
                    error_msg = "Unsupported path '%s'" % op.path
                    self.logger.warning(error_msg)
                    raise ModelValidationError('value', error_msg)
            else:
                error_msg = "Unsupported operation '%s'" % op.operation
                self.logger.warning(error_msg)
                raise ModelValidationError('value', error_msg)

        if self.request.event.name:
            self.request.event_extras = {'instance': instance}

        self.write(response)
Beispiel #22
0
class RequestAPI(BaseHandler):

    parser = BeerGardenSchemaParser()
    logger = logging.getLogger(__name__)

    @authenticated(permissions=[Permissions.REQUEST_READ])
    def get(self, request_id):
        """
        ---
        summary: Retrieve a specific Request
        parameters:
          - name: request_id
            in: path
            required: true
            description: The ID of the Request
            type: string
        responses:
          200:
            description: Request with the given ID
            schema:
              $ref: '#/definitions/Request'
          404:
            $ref: '#/definitions/404Error'
          50x:
            $ref: '#/definitions/50xError'
        tags:
          - Requests
        """
        self.logger.debug("Getting Request: %s", request_id)

        req = Request.objects.get(id=str(request_id))
        req.children = Request.objects(parent=req)

        self.write(self.parser.serialize_request(req, to_string=False))

    @authenticated(permissions=[Permissions.REQUEST_UPDATE])
    def patch(self, request_id):
        """
        ---
        summary: Partially update a Request
        description: |
          The body of the request needs to contain a set of instructions detailing the updates to
          apply. Currently the only operation supported is `replace`, with paths `/status`,
          `/output`, and `/error_class`:
          ```JSON
          {
            "operations": [
              { "operation": "replace", "path": "/status", "value": "" }
            ]
          }
          ```
        parameters:
          - name: request_id
            in: path
            required: true
            description: The ID of the Request
            type: string
          - name: patch
            in: body
            required: true
            description: Instructions for how to update the Request
            schema:
              $ref: '#/definitions/Patch'
        responses:
          200:
            description: Request with the given ID
            schema:
              $ref: '#/definitions/Request'
          400:
            $ref: '#/definitions/400Error'
          404:
            $ref: '#/definitions/404Error'
          50x:
            $ref: '#/definitions/50xError'
        tags:
          - Requests
        """
        req = Request.objects.get(id=request_id)
        operations = self.parser.parse_patch(self.request.decoded_body,
                                             many=True,
                                             from_string=True)
        wait_event = None

        # We note the status before the operations, because it is possible for the operations to
        # update the status of the request. In that case, because the updates are coming in in a
        # single request it is okay to update the output or error_class. Ideally this would be
        # handled correctly when we better integrate PatchOperations with their models.
        status_before = req.status

        for op in operations:
            if op.operation == 'replace':
                if op.path == '/status':
                    if op.value.upper() in BrewtilsRequest.STATUS_LIST:
                        req.status = op.value.upper()

                        if op.value.upper() == 'IN_PROGRESS':
                            self.request.event.name = Events.REQUEST_STARTED.name

                        elif op.value.upper(
                        ) in BrewtilsRequest.COMPLETED_STATUSES:
                            self.request.event.name = Events.REQUEST_COMPLETED.name

                            if request_id in brew_view.request_map:
                                wait_event = brew_view.request_map[request_id]
                    else:
                        error_msg = "Unsupported status value '%s'" % op.value
                        self.logger.warning(error_msg)
                        raise ModelValidationError(error_msg)
                elif op.path == '/output':
                    if req.output == op.value:
                        continue

                    if status_before in Request.COMPLETED_STATUSES:
                        raise ModelValidationError(
                            "Cannot update output for a request "
                            "that is already completed")
                    req.output = op.value
                elif op.path == '/error_class':
                    if req.error_class == op.value:
                        continue

                    if status_before in Request.COMPLETED_STATUSES:
                        raise ModelValidationError(
                            "Cannot update error_class for a "
                            "request that is already completed")
                    req.error_class = op.value
                    self.request.event.error = True
                else:
                    error_msg = "Unsupported path '%s'" % op.path
                    self.logger.warning(error_msg)
                    raise ModelValidationError(error_msg)
            else:
                error_msg = "Unsupported operation '%s'" % op.operation
                self.logger.warning(error_msg)
                raise ModelValidationError(error_msg)

        req.save()
        self._update_metrics(req, status_before)
        self._update_job_numbers(req, status_before)

        if wait_event:
            wait_event.set()

        self.request.event_extras = {'request': req, 'patch': operations}

        self.write(self.parser.serialize_request(req, to_string=False))

    def _update_job_numbers(self, request, status_before):
        if (not request.metadata.get('_bg_job_id')
                or status_before == request.status
                or request.status not in Request.COMPLETED_STATUSES):
            return

        try:
            job_id = request.metadata.get('_bg_job_id')
            document = Job.objects.get(id=job_id)
            if request.status == 'ERROR':
                document.error_count += 1
            elif request.status == 'SUCCESS':
                document.success_count += 1
            document.save()
        except Exception as exc:
            self.logger.warning('Could not update job counts.')
            self.logger.exception(exc)

    def _update_metrics(self, request, status_before):
        """Update metrics associated with this new request.

        We have already confirmed that this request has had a valid state
        transition because updating the status verifies that the updates are
        correct.

        In addition, this call is happening after the save to the
        database. Now we just need to update the metrics.
        """
        if (status_before == request.status
                or status_before in BrewtilsRequest.COMPLETED_STATUSES):
            return

        labels = {
            'system': request.system,
            'system_version': request.system_version,
            'instance_name': request.instance_name,
        }

        if status_before == 'CREATED':
            self.queued_request_gauge.labels(**labels).dec()
        elif status_before == 'IN_PROGRESS':
            self.in_progress_request_gauge.labels(**labels).dec()

        if request.status == 'IN_PROGRESS':
            self.in_progress_request_gauge.labels(**labels).inc()

        elif request.status in BrewtilsRequest.COMPLETED_STATUSES:
            # We don't use _measure_latency here because the request times are
            # stored in UTC and we need to make sure we're comparing apples to
            # apples.
            latency = (datetime.datetime.utcnow() -
                       request.created_at).total_seconds()
            labels['command'] = request.command
            labels['status'] = request.status

            self.completed_request_counter.labels(**labels).inc()
            self.plugin_command_latency.labels(**labels).observe(latency)
Beispiel #23
0
class JobAPI(BaseHandler):
    logger = logging.getLogger(__name__)
    parser = BeerGardenSchemaParser()

    @authenticated(permissions=[Permissions.JOB_READ])
    def get(self, job_id):
        """
        ---
        summary: Retrieve a specific Job
        parameters:
          - name: job_id
            in: path
            required: true
            description: The ID of the Job
            type: string
        responses:
          200:
            description: Job with the given ID
            schema:
              $ref: '#/definitions/Job'
          404:
            $ref: '#/definitions/404Error'
          50x:
            $ref: '#/definitions/50xError'
        tags:
          - Jobs
        """
        document = Job.objects.get(id=job_id)
        self.set_header('Content-Type', 'application/json; charset=UTF-8')
        self.write(self.parser.serialize_job(document, to_string=False))

    @authenticated(permissions=[Permissions.JOB_UPDATE])
    def patch(self, job_id):
        """
        ---
        summary: Pause/Resume a job
        description: |
          The body of the request needs to contain a set of instructions
          detailing the actions to take. Currently the only operation
          supported is `update` with `path` of `/status`.


          You can pause a job with:
          ```JSON
          {
            "operations": [
              { "operation": "update", "path": "/status", "value": "PAUSED" }
            ]
          }
          ```

          And resume it with:
          ```JSON
          {
            "operations": [
                { "operation": "update", "path": "/status", "value": "RUNNING" }
            ]
          }
          ```
        parameters:
          - name: job_id
            in: path
            required: true
            description: The ID of the Job
            type: string
          - name: patch
            in: body
            required: true
            description: Instructions for the actions to take
            schema:
              $ref: '#/definitions/Patch'
        responses:
          200:
            description: Job with the given ID
            schema:
              $ref: '#/definitions/Job'
          400:
            $ref: '#/definitions/400Error'
          404:
            $ref: '#/definitions/404Error'
          50x:
            $ref: '#/definitions/50xError'
        tags:
          - Jobs
        """
        job = Job.objects.get(id=job_id)
        operations = self.parser.parse_patch(
            self.request.decoded_body,
            many=True,
            from_string=True
        )

        for op in operations:
            if op.operation == 'update':
                if op.path == '/status':
                    if str(op.value).upper() == 'PAUSED':
                        brew_view.request_scheduler.pause_job(
                            job_id, jobstore='beer_garden'
                        )
                        job.status = 'PAUSED'
                    elif str(op.value).upper() == 'RUNNING':
                        brew_view.request_scheduler.resume_job(
                            job_id, jobstore='beer_garden'
                        )
                        job.status = 'RUNNING'
                    else:
                        error_msg = "Unsupported status value '%s'" % op.value
                        self.logger.warning(error_msg)
                        raise ModelValidationError(error_msg)
                else:
                    error_msg = "Unsupported path value '%s'" % op.path
                    self.logger.warning(error_msg)
                    raise ModelValidationError(error_msg)
            else:
                error_msg = "Unsupported operation '%s'" % op.operation
                self.logger.warning(error_msg)
                raise ModelValidationError(error_msg)

        job.save()
        self.set_header('Content-Type', 'application/json; charset=UTF-8')
        self.write(self.parser.serialize_job(job, to_string=False))

    @coroutine
    @authenticated(permissions=[Permissions.JOB_DELETE])
    def delete(self, job_id):
        """
        ---
        summary: Delete a specific Job.
        description: Will remove a specific job. No further executions will occur.
        parameters:
          - name: job_id
            in: path
            required: true
            description: The ID of the Job
            type: string
        responses:
          204:
            description: Job has been successfully deleted.
          404:
            $ref: '#/definitions/404Error'
          50x:
            $ref: '#/definitions/50xError'
        tags:
          - Jobs
        """
        brew_view.request_scheduler.remove_job(job_id, jobstore='beer_garden')
        self.set_status(204)