Exemplo n.º 1
0
    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)
Exemplo n.º 2
0
    def delete(self, queue_name):
        """
        ---
        summary: Clear a queue by canceling all requests
        parameters:
          - name: queue_name
            in: path
            required: true
            description: The name of the queue to clear
            type: string
        responses:
          204:
            description: Queue successfully cleared
          404:
            $ref: '#/definitions/404Error'
          50x:
            $ref: '#/definitions/50xError'
        tags:
          - Queues
        """
        self.request.event.name = Events.QUEUE_CLEARED.name
        self.request.event.payload = {'queue_name': queue_name}

        with thrift_context() as client:
            yield client.clearQueue(queue_name)

        self.set_status(204)
Exemplo n.º 3
0
    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))
Exemplo n.º 4
0
    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)
Exemplo n.º 5
0
    def get(self):
        try:
            with thrift_context() as client:
                bartender_version = yield client.getVersion()
        except Exception as ex:
            logger = logging.getLogger(__name__)
            logger.error("Could not get Bartender Version.")
            logger.exception(ex)
            bartender_version = "unknown"

        self.write({
            "brew_view_version": brew_view.__version__,
            "bartender_version": bartender_version,
            "current_api_version": "v1",
            "supported_api_versions": ["v1"],
        })
Exemplo n.º 6
0
    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)
Exemplo n.º 7
0
    def post(self):
        """
        ---
        summary: Initiate a rescan of the plugin directory
        deprecated: true
        description: |
          This endpoint is DEPRECATED - Use PATCH /api/v1/admin instead.

          Will initiate a rescan of the plugins directory.
          * Will remove from the registry and database any currently stopped
            plugins who's directory has been removed.
          * Will add and start any new plugin directories.
        responses:
          204:
            description: Rescan successfully initiated
          50x:
            $ref: '#/definitions/50xError'
        tags:
          - Deprecated
        """
        with thrift_context() as client:
            yield client.rescanSystemDirectory()

        self.set_status(204)
Exemplo n.º 8
0
    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))
Exemplo n.º 9
0
    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))
Exemplo n.º 10
0
    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))
Exemplo n.º 11
0
    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)
Exemplo n.º 12
0
    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))