Ejemplo n.º 1
0
class OtterAdmin(object):
    """
    OtterAdmin is a RESTful interface to manage and otter.
    """
    app = OtterApp()

    def __init__(self, store):
        """
        Initialize OtterAdmin.
        """
        self.store = store

    @app.route('/', methods=['GET'])
    def root(self, request):
        """
        Root response for OtterAdmin.
        """
        return ''

    @app.route('/metrics/', branch=True)
    def metrics(self, request):
        """
        Routes related to metrics are delegated to OtterMetrics.
        """
        return OtterMetrics(self.store).app.resource()
Ejemplo n.º 2
0
class OtterLimits(object):
    """
    REST endpoints for returning group limits.
    """
    app = OtterApp()

    def __init__(self, store, tenant_id):
        self.log = log.bind(system='otter.log.limits', tenant_id=tenant_id)
        self.store = store
        self.tenant_id = tenant_id

    @app.route('/', methods=['GET'])
    @with_transaction_id()
    @fails_with(exception_codes)
    @succeeds_with(200)
    def list_limits(self, request):
        """
        returns application limits
        """
        data = {"limits": {"absolute": config_value("limits.absolute")}}
        accept = request.getHeader("accept")

        if accept and 'xml' in accept:
            url = "http://docs.openstack.org/common/api/v1.0"

            xml = etree.Element("limits", xmlns=url)
            absolute = etree.SubElement(xml, "absolute")

            for key, val in data['limits']['absolute'].iteritems():
                etree.SubElement(absolute, "limit", name=key, value=str(val))

            request.setHeader("Content-Type", "application/xml")
            return etree.tostring(xml, encoding="UTF-8", xml_declaration=True)

        return json.dumps(data)
Ejemplo n.º 3
0
class OtterServers(object):
    """
    REST endpoints to access servers in a scaling group
    """
    app = OtterApp()

    def __init__(self, store, tenant_id, scaling_group_id, dispatcher):
        self.log = log.bind(system='otter.rest.group.servers',
                            tenant_id=tenant_id,
                            scaling_group_id=scaling_group_id)
        self.store = store
        self.tenant_id = tenant_id
        self.scaling_group_id = scaling_group_id
        self.dispatcher = dispatcher

    @app.route('/', methods=['GET'])
    @with_transaction_id()
    @fails_with(exception_codes)
    @succeeds_with(200)
    @paginatable
    def list_servers(self, request, paginate):
        """
        Get a list of servers in the group.
        """
        raise NotImplementedError

    @app.route('/<string:server_id>', methods=['GET'])
    @with_transaction_id()
    @fails_with(exception_codes)
    @succeeds_with(200)
    def get_server(self, request, server_id):
        """
        Get particular server from the group
        """
        raise NotImplementedError

    @app.route('/<string:server_id>/', methods=['DELETE'])
    @with_transaction_id()
    @fails_with(exception_codes)
    @succeeds_with(202)
    def delete_server(self, request, server_id):
        """
        Delete a server from the group.
        """
        group = self.store.get_scaling_group(
            self.log, self.tenant_id, self.scaling_group_id)
        log = self.log.bind(server_id=server_id)
        d = controller.modify_and_trigger(
            self.dispatcher,
            group,
            bound_log_kwargs(log),
            partial(controller.remove_server_from_group,
                    self.dispatcher,
                    log,
                    transaction_id(request), server_id,
                    extract_bool_arg(request, 'replace', True),
                    extract_bool_arg(request, 'purge', True)),
            modify_state_reason='delete_server')
        return d
Ejemplo n.º 4
0
class OtterExecute(object):
    """
    REST endpoint for executing a webhook.
    """
    app = OtterApp()

    def __init__(self, store, capability_version, capability_hash, dispatcher):
        self.log = log.bind(system='otter.rest.execute',
                            capability_version=capability_version,
                            capability_hash=capability_hash)
        self.store = store
        self.capability_version = capability_version
        self.capability_hash = capability_hash
        self.dispatcher = dispatcher

    @app.route('/', methods=['POST'])
    @with_transaction_id()
    @fails_with({}
                )  # This will allow us to surface internal server error only.
    @succeeds_with(202)
    def execute_webhook(self, request):
        """
        Execute a scaling policy based the capability hash.
        This returns a 202 in all cases except internal server error,
        and does not wait for execution to finish.
        """
        logl = [self.log]

        d = self.store.webhook_info_by_hash(self.log, self.capability_hash)

        def log_informational_webhook_failure(failure):
            failure.trap(UnrecognizedCapabilityError, CannotExecutePolicyError,
                         GroupPausedError, NoSuchPolicyError,
                         NoSuchScalingGroupError)
            logl[0].msg("Non-fatal error during webhook execution: {exc!r}",
                        exc=failure.value)

        def execute_policy((tenant_id, group_id, policy_id)):
            bound_log = self.log.bind(tenant_id=tenant_id,
                                      scaling_group_id=group_id,
                                      policy_id=policy_id)
            logl[0] = bound_log
            group = self.store.get_scaling_group(bound_log, tenant_id,
                                                 group_id)
            return controller.modify_and_trigger(
                self.dispatcher,
                group,
                bound_log_kwargs(bound_log),
                partial(controller.maybe_execute_scaling_policy,
                        bound_log,
                        transaction_id(request),
                        policy_id=policy_id),
                modify_state_reason='execute_webhook')

        d.addCallback(execute_policy)
        d.addErrback(log_informational_webhook_failure)
        d.addErrback(
            lambda f: logl[0].err(f, "Unhandled exception executing webhook."))
Ejemplo n.º 5
0
        class FakeSubApp(object):
            app = OtterApp()
            log = self.mock_log

            @app.route('/<string:extra_arg1>/')
            @with_transaction_id()
            @log_arguments
            def doWork(self, request, extra_arg1):
                return 'empty response'
Ejemplo n.º 6
0
        class FakeApp(object):
            app = OtterApp()
            log = mock.Mock()

            @app.route('/v1.0/foo')
            @with_transaction_id()
            def foo(self, request):
                transaction_ids.append(transaction_id(request))
                return 'ok'
Ejemplo n.º 7
0
        class FakeApp(object):
            app = OtterApp()
            log = mock.Mock()

            @app.route('/v1.0/foo/')
            @with_transaction_id()
            def foo(self, request):
                requests[0] += 1
                return 'ok'
Ejemplo n.º 8
0
        class FakeApp(object):
            app = OtterApp()

            @app.route('/', branch=True)
            def delegate_to_dowork(self, request):
                return FakeSubApp().app.resource()
Ejemplo n.º 9
0
class OtterWebhooks(object):
    """
    REST endpoints for managing scaling group webhooks.
    """
    app = OtterApp()

    def __init__(self, store, tenant_id, group_id, policy_id):
        self.log = log.bind(system='otter.rest.webhooks',
                            tenant_id=tenant_id,
                            scaling_group_id=group_id,
                            policy_id=policy_id)
        self.store = store
        self.tenant_id = tenant_id
        self.group_id = group_id
        self.policy_id = policy_id

    @app.route('/', methods=['GET'])
    @with_transaction_id()
    @fails_with(exception_codes)
    @succeeds_with(200)
    @paginatable
    def list_webhooks(self, request, paginate):
        """
        Get a list of all webhooks (capability URL) associated with a particular
        scaling policy. This data is returned in the body of the response in JSON
        format.

        Example response::

            {
                "webhooks": [
                    {
                        "id":"{webhookId1}",
                        "name": "alice",
                        "metadata": {
                            "notes": "this is for Alice"
                        },
                        "links": [
                            {
                                "href": ".../{groupId1}/policies/{policyId1}/webhooks/{webhookId1}/",
                                "rel": "self"
                            },
                            {
                                "href": ".../execute/1/{capability_hash1}/,
                                "rel": "capability"
                            }
                        ]
                    },
                    {
                        "id":"{webhookId2}",
                        "name": "alice",
                        "metadata": {
                            "notes": "this is for Bob"
                        },
                        "links": [
                            {
                                "href": ".../{groupId1}/policies/{policyId1}/webhooks/{webhookId2}/",
                                "rel": "self"
                            },
                            {
                                "href": ".../execute/1/{capability_hash2}/,
                                "rel": "capability"
                            }
                        ]
                    }
                ],
                "webhooks_links": []
            }
        """
        def format_webhooks(webhook_list):
            webhook_list = [
                _format_webhook(webhook_model, self.tenant_id, self.group_id,
                                self.policy_id)
                for webhook_model in webhook_list
            ]

            return {
                'webhooks':
                webhook_list,
                "webhooks_links":
                get_webhooks_links(webhook_list, self.tenant_id, self.group_id,
                                   self.policy_id, None, **paginate)
            }

        rec = self.store.get_scaling_group(self.log, self.tenant_id,
                                           self.group_id)
        deferred = rec.list_webhooks(self.policy_id, **paginate)
        deferred.addCallback(format_webhooks)
        deferred.addCallback(json.dumps)
        return deferred

    @app.route('/', methods=['POST'])
    @with_transaction_id()
    @fails_with(exception_codes)
    @succeeds_with(201)
    @validate_body(rest_schemas.create_webhooks_request)
    def create_webhooks(self, request, data):
        """
        Create one or many new webhooks associated with a particular scaling policy.
        Webhooks may (but do not need to) include some arbitrary medata, and must
        include a name.

        The response header will point to the list webhooks endpoint.
        An array of webhooks is provided in the request body in JSON format.

        Example request::

            [
                {
                    "name": "alice",
                    "metadata": {
                        "notes": "this is for Alice"
                    }
                },
                {
                    "name": "bob"
                }
            ]


        Example response::

            {
                "webhooks": [
                    {
                        "id":"{webhookId1}",
                        "alice",
                        "metadata": {
                            "notes": "this is for Alice"
                        },
                        "links": [
                            {
                                "href": ".../{groupId1}/policies/{policyId1}/webhooks/{webhookId1}/",
                                "rel": "self"
                            },
                            {
                                "href": ".../execute/1/{capability_hash1}/,
                                "rel": "capability"
                            }
                        ]
                    },
                    {
                        "id":"{webhookId2}",
                        "name": "bob",
                        "metadata": {},
                        "links": [
                            {
                                "href": ".../{groupId1}/policies/{policyId1}/webhooks/{webhookId2}/",
                                "rel": "self"
                            },
                            {
                                "href": ".../execute/1/{capability_hash2}/,
                                "rel": "capability"
                            }
                        ]
                    }
                ]
            }
        """
        def format_webhooks_and_send_redirect(webhook_list):
            request.setHeader(
                "Location",
                get_autoscale_links(self.tenant_id,
                                    self.group_id,
                                    self.policy_id,
                                    "",
                                    format=None))

            webhook_list = [
                _format_webhook(webhook_model, self.tenant_id, self.group_id,
                                self.policy_id)
                for webhook_model in webhook_list
            ]

            return {'webhooks': webhook_list}

        rec = self.store.get_scaling_group(self.log, self.tenant_id,
                                           self.group_id)
        deferred = rec.create_webhooks(self.policy_id, data)
        deferred.addCallback(format_webhooks_and_send_redirect)
        deferred.addCallback(json.dumps)
        return deferred

    @app.route('/<string:webhook_id>/', branch=True)
    def webhook(self, request, webhook_id):
        """
        Delegate routes for specific webhooks to OtterWebhook.
        """
        return OtterWebhook(self.store, self.tenant_id, self.group_id,
                            self.policy_id, webhook_id).app.resource()
Ejemplo n.º 10
0
class OtterPolicies(object):
    """
    REST endpoints for policies of a scaling group.
    """
    app = OtterApp()

    def __init__(self, store, tenant_id, scaling_group_id, dispatcher):
        self.log = log.bind(system='otter.rest.policies',
                            tenant_id=tenant_id,
                            scaling_group_id=scaling_group_id)
        self.store = store
        self.tenant_id = tenant_id
        self.scaling_group_id = scaling_group_id
        self.dispatcher = dispatcher

    @app.route('/', methods=['GET'])
    @with_transaction_id()
    @fails_with(exception_codes)
    @succeeds_with(200)
    @paginatable
    def list_policies(self, request, paginate):
        """
        Get a list of scaling policies in the group. Each policy describes an id,
        name, type, adjustment, cooldown, and links. This data is returned in the
        body of the response in JSON format.

        Example response::

            {
                "policies": [
                    {
                        "id":"{policyId1}",
                        "data": {
                            "name": "scale up by one server",
                            "change": 1,
                            "cooldown": 150
                        },
                        "links": [
                            {
                                "href": "{url_root}/v1.0/010101/groups/{groupId1}/policy/{policyId1}/"
                                "rel": "self"
                            }
                        ]
                    },
                    {
                        "id": "{policyId2}",
                        "data": {
                            "name": "scale up ten percent",
                            "changePercent": 10,
                            "cooldown": 150
                        },
                        "links": [
                            {
                                "href": "{url_root}/v1.0/010101/groups/{groupId1}/policy/{policyId2}/"
                                "rel": "self"
                            }
                        ]
                    },
                    {
                        "id":"{policyId3}",
                        "data": {
                            "name": "scale down one server",
                            "change": -1,
                            "cooldown": 150
                        },
                        "links": [
                            {
                                "href": "{url_root}/v1.0/010101/groups/{groupId1}/policy/{policyId3}/"
                                "rel": "self"
                            }
                        ]
                    },
                    {
                        "id": "{policyId4}",
                        "data": {
                            "name": "scale down ten percent",
                            "changePercent": -10,
                            "cooldown": 150
                        },
                        "links": [
                            {
                                "href": "{url_root}/v1.0/010101/groups/{groupId1}/policy/{policyId4}/"
                                "rel": "self"
                            }
                        ]
                    }
                ]
            }
        """
        def format_policies(policy_list):
            linkify_policy_list(policy_list, self.tenant_id,
                                self.scaling_group_id)
            return {
                'policies':
                policy_list,
                "policies_links":
                get_policies_links(policy_list, self.tenant_id,
                                   self.scaling_group_id, None, **paginate)
            }

        rec = self.store.get_scaling_group(self.log, self.tenant_id,
                                           self.scaling_group_id)
        deferred = rec.list_policies(**paginate)
        deferred.addCallback(format_policies)
        deferred.addCallback(json.dumps)
        return deferred

    @app.route('/', methods=['POST'])
    @with_transaction_id()
    @fails_with(exception_codes)
    @succeeds_with(201)
    @auditable('request.policy.create', 'Created policies.')
    @validate_body(rest_schemas.create_policies_request)
    def create_policies(self, request, data, audit_logger):
        """
        Create one or many new scaling policies.
        Scaling policies must include a name, type, adjustment, and cooldown.
        The response header will point to the list policies endpoint.
        An array of scaling policies is provided in the request body in JSON format.

        Example request::

            [
                {
                    "name": "scale up by one server",
                    "change": 1,
                    "cooldown": 150
                },
                {
                    "name": 'scale down by 5.5 percent',
                    "changePercent": -5.5,
                    "cooldown": 6
                }
            ]

        Example response::

            {
                "policies": [
                    {
                        "id": {policyId1},
                        "links": [
                            {
                                "href": "{url_root}/v1.0/010101/groups/{groupId}/policy/{policyId1}/"
                                "rel": "self"
                            }
                        ],
                        "name": "scale up by one server",
                        "change": 1,
                        "cooldown": 150
                    },
                    {
                        "id": {policyId2},
                        "links": [
                            {
                                "href": "{url_root}/v1.0/010101/groups/{groupId}/policy/{policyId2}/"
                                "rel": "self"
                            }
                        ],
                        "name": 'scale down by 5.5 percent',
                        "changePercent": -5.5,
                        "cooldown": 6
                    }
                ]
            }
        """
        def format_policies_and_send_redirect(policy_list):
            request.setHeader(
                "Location",
                get_autoscale_links(self.tenant_id,
                                    self.scaling_group_id,
                                    "",
                                    format=None))
            linkify_policy_list(policy_list, self.tenant_id,
                                self.scaling_group_id)
            return {'policies': policy_list}

        def _add_to_bobby(policy_list, client):
            d = defer.succeed(policy_list)
            for policy_item in policy_list:
                if policy_item['type'] == 'cloud_monitoring':
                    client.create_policy(
                        self.tenant_id, self.scaling_group_id,
                        policy_item['id'], policy_item['args']['check'],
                        policy_item['args']['alarm_criteria']['criteria'])
            return d.addCallback(lambda _: policy_list)

        from otter.rest.bobby import get_bobby

        bobby = get_bobby()
        e = extra_policy_validation(data, bobby)
        if e is not None:
            return defer.fail(e)

        rec = self.store.get_scaling_group(self.log, self.tenant_id,
                                           self.scaling_group_id)
        deferred = rec.create_policies(data)

        if bobby is not None:
            deferred.addCallback(_add_to_bobby, bobby)

        deferred.addCallback(format_policies_and_send_redirect)

        def audit_data(result):
            audit_logger.add(data=result)
            return result

        deferred.addCallback(audit_data)
        deferred.addCallback(json.dumps)
        return deferred

    @app.route('/<string:policy_id>/', branch=True)
    def policy(self, request, policy_id):
        """
        Delegate routes for specific policies to OtterPolicy.
        """
        return OtterPolicy(self.store, self.tenant_id, self.scaling_group_id,
                           policy_id, self.dispatcher).app.resource()
Ejemplo n.º 11
0
class OtterConfig(object):
    """
    REST endpoints for the configuration of scaling groups.
    """
    app = OtterApp()

    def __init__(self, store, tenant_id, group_id, dispatcher):
        self.log = log.bind(system='otter.rest.config',
                            tenant_id=tenant_id,
                            scaling_group_id=group_id)
        self.store = store
        self.tenant_id = tenant_id
        self.group_id = group_id
        self.dispatcher = dispatcher

    @app.route('/', methods=['GET'])
    @with_transaction_id()
    @fails_with(exception_codes)
    @succeeds_with(200)
    def view_config_for_scaling_group(self, request):
        """
        Get the configuration for a scaling group, which includes the minimum
        number of entities, the maximum number of entities, global cooldown,
        and other metadata.  This data is returned in the body of the response
        in JSON format.

        Example response::

            {
                "groupConfiguration": {
                    "name": "workers",
                    "cooldown": 60,
                    "minEntities": 5,
                    "maxEntities": 100,
                    "metadata": {
                        "firstkey": "this is a string",
                        "secondkey": "1",
                    }
                }
            }
        """
        rec = self.store.get_scaling_group(
            self.log, self.tenant_id, self.group_id)
        deferred = rec.view_config()
        deferred.addCallback(
            lambda conf: json.dumps({"groupConfiguration": conf}))
        return deferred

    @app.route('/', methods=['PUT'])
    @with_transaction_id()
    @fails_with(exception_codes)
    @succeeds_with(204)
    @validate_body(group_schemas.update_config)
    def edit_config_for_scaling_group(self, request, data):
        """
        Edit the configuration for a scaling group, which includes the minimum
        number of entities, the maximum number of entities, global cooldown,
        and other metadata.  This data provided in the request body in JSON
        format.  If successful, no response body will be returned.

        Example request::

            {
                "name": "workers",
                "cooldown": 60,
                "minEntities": 5,
                "maxEntities": 100,
                "metadata": {
                    "firstkey": "this is a string",
                    "secondkey": "1",
                }
            }

        The entire schema body must be provided.
        """
        if data['minEntities'] > data['maxEntities']:
            raise InvalidMinEntities(
                "minEntities must be less than or equal to maxEntities")

        def _get_launch_and_obey_config_change(scaling_group, state):
            d = scaling_group.view_launch_config()
            d.addCallback(partial(
                controller.obey_config_change,
                self.log,
                transaction_id(request),
                data, scaling_group, state))
            return d

        group = self.store.get_scaling_group(
            self.log, self.tenant_id, self.group_id)
        deferred = group.update_config(data)
        deferred.addCallback(
            lambda _: controller.modify_and_trigger(
                self.dispatcher,
                group,
                bound_log_kwargs(log),
                _get_launch_and_obey_config_change,
                modify_state_reason='edit_config_for_scaling_group'))
        return deferred
Ejemplo n.º 12
0
class OtterLaunch(object):
    """
    REST endpoints for launch configurations.
    """
    app = OtterApp()

    def __init__(self, store, tenant_id, group_id):
        self.log = log.bind(system='otter.rest.launch',
                            tenant_id=tenant_id,
                            scaling_group_id=group_id)
        self.store = store
        self.tenant_id = tenant_id
        self.group_id = group_id

    @app.route('/', methods=['GET'])
    @with_transaction_id()
    @fails_with(exception_codes)
    @succeeds_with(200)
    def view_launch_config(self, request):
        """
        Get the launch configuration for a scaling group, which includes the
        details of how to create a server, from what image, which load
        balancers to join it to, and what networks to add it to, and other
        metadata.  This data is returned in the body of the response in JSON
        format.

        Example response::

            {
                "launchConfiguration": {
                    "type": "launch_server",
                    "args": {
                        "server": {
                            "flavorRef": 3,
                            "name": "webhead",
                            "imageRef": "0d589460-f177-4b0f-81c1-8ab8903ac7d8",
                            "OS-DCF:diskConfig": "AUTO",
                            "metadata": {
                                "mykey": "myvalue"
                            },
                            "personality": [
                                {
                                    "path": '/root/.ssh/authorized_keys',
                                    "contents": "ssh-rsa A... [email protected]"
                                }
                            ],
                            "networks": [{
                                "uuid": "11111111-1111-1111-1111-111111111111"
                            }],
                        },
                        "loadBalancers": [
                            {
                                "loadBalancerId": 2200,
                                "port": 8081
                            }
                        ]
                    }
                }
            }
        """
        rec = self.store.get_scaling_group(
            self.log, self.tenant_id, self.group_id)
        deferred = rec.view_launch_config()
        deferred.addCallback(
            lambda conf: json.dumps({"launchConfiguration": conf}))
        return deferred

    @app.route('/', methods=['PUT'])
    @with_transaction_id()
    @fails_with(exception_codes)
    @succeeds_with(204)
    @validate_body(group_schemas.launch_config)
    def edit_launch_config(self, request, data):
        """
        Edit the launch configuration for a scaling group, which includes the
        details of how to create a server, from what image, which load
        balancers to join it to, and what networks to add it to, and other
        metadata.  This data provided in the request body in JSON format.  If
        successful, no response body will be returned.

        Example request::

            {
                "type": "launch_server",
                "args": {
                    "server": {
                        "flavorRef": 3,
                        "name": "webhead",
                        "imageRef": "0d589460-f177-4b0f-81c1-8ab8903ac7d8",
                        "OS-DCF:diskConfig": "AUTO",
                        "metadata": {
                            "mykey": "myvalue"
                        },
                        "personality": [
                            {
                                "path": '/root/.ssh/authorized_keys',
                                "contents": "ssh-rsa A... [email protected]"
                            }
                        ],
                        "networks": [
                            {
                                "uuid": "11111111-1111-1111-1111-111111111111"
                            }
                        ],
                    },
                    "loadBalancers": [
                        {
                            "loadBalancerId": 2200,
                            "port": 8081
                        }
                    ]
                }
            }

        The exact update cases are still up in the air -- can the user provide
        a mimimal schema, and if so, what happens with defaults?

        Nova should validate the image before saving the new config.
        Users may have an invalid configuration based on dependencies.
        """
        rec = self.store.get_scaling_group(
            self.log, self.tenant_id, self.group_id)
        data = normalize_launch_config(data)
        group_schemas.validate_launch_config_servicenet(data)
        deferred = get_supervisor().validate_launch_config(
            self.log, self.tenant_id, data)
        deferred.addCallback(lambda _: rec.update_launch_config(data))
        return deferred
Ejemplo n.º 13
0
class OtterGroup(object):
    """
    REST endpoints for managing a specific scaling group.
    """
    app = OtterApp()

    def __init__(self, store, tenant_id, group_id, dispatcher):
        self.log = log.bind(system='otter.rest.group',
                            tenant_id=tenant_id,
                            scaling_group_id=group_id)
        self.store = store
        self.tenant_id = tenant_id
        self.group_id = group_id
        self.dispatcher = dispatcher

    def with_active_cache(self, get_func, *args, **kwargs):
        """
        Return result of `get_func` and active cache from servers table
        if this is convergence enabled tenant
        """
        if tenant_is_enabled(self.tenant_id, config_value):
            cache_d = get_active_cache(
                self.store.reactor, self.store.connection, self.tenant_id,
                self.group_id)
        else:
            cache_d = succeed(None)
        return gatherResults([get_func(*args, **kwargs), cache_d],
                             consumeErrors=True)

    @app.route('/', methods=['GET'])
    @with_transaction_id()
    @fails_with(exception_codes)
    @succeeds_with(200)
    def view_manifest_config_for_scaling_group(self, request):
        """
        View manifested view of the scaling group configuration, including the
        launch configuration, and the scaling policies.  This data is
        returned in the body of the response in JSON format.

        Example response::


            {
                "group": {
                    "groupConfiguration": {
                        "cooldown": 60,
                        "maxEntities": 0,
                        "metadata": {},
                        "minEntities": 0,
                        "name": "smallest possible launch config group"
                    },
                    "state": {
                        "active": [],
                        "activeCapacity": 0,
                        "desiredCapacity": 0,
                        "paused": false,
                        "pendingCapacity": 0
                    },
                    "id": "605e13f6-1452-4588-b5da-ac6bb468c5bf",
                    "launchConfiguration": {
                        "args": {
                            "server": {}
                        },
                        "type": "launch_server"
                    },
                    "links": [
                        {
                            "href": "https://dfw.autoscale.api.
                            rackspacecloud.com/
                            v1.0/676873
                            /groups/605e13f6-1452-4588-b5da-ac6bb468c5bf/",
                            "rel": "self"
                        }
                    ],
                    "scalingPolicies": [
                        {
                            "changePercent": -5.5,
                            "cooldown": 1800,
                            "id": "eb0fe1bf-3428-4f34-afd9-a5ac36f60511",
                            "links": [
                                {
                                    "href": "https://dfw.autoscale.api.
                                    rackspacecloud.com/
                                    v1.0/676873/groups/
                                    605e13f6-1452-4588-b5da-ac6bb468c5bf/
                                    policies/
                                    eb0fe1bf-3428-4f34-afd9-a5ac36f60511/",
                                    "rel": "self"
                                }
                            ],
                            "name": "scale down by 5.5 percent",
                            "type": "webhook"
                        },
                    ]
                }
            }
        """
        def with_webhooks(_request):
            return ('webhooks' in _request.args and
                    _request.args['webhooks'][0].lower() == 'true')

        def add_webhooks_links(policies):
            for policy in policies:
                webhook_list = [_format_webhook(webhook_model, self.tenant_id,
                                                self.group_id, policy['id'])
                                for webhook_model in policy['webhooks']]
                policy['webhooks'] = webhook_list
                policy['webhooks_links'] = get_webhooks_links(
                    webhook_list,
                    self.tenant_id,
                    self.group_id,
                    policy['id'],
                    rel='webhooks')

        def openstack_formatting(results):
            data, active = results
            data["links"] = get_autoscale_links(self.tenant_id, self.group_id)
            data["state"] = format_state_dict(data["state"], active)
            linkify_policy_list(
                data["scalingPolicies"], self.tenant_id, self.group_id)
            data['scalingPolicies_links'] = get_policies_links(
                data['scalingPolicies'], self.tenant_id, self.group_id,
                rel='policies')
            if with_webhooks(request):
                add_webhooks_links(data["scalingPolicies"])
            return {"group": data}

        group = self.store.get_scaling_group(
            self.log, self.tenant_id, self.group_id)
        deferred = self.with_active_cache(
            group.view_manifest, with_webhooks=with_webhooks(request))
        deferred.addCallback(openstack_formatting)
        deferred.addCallback(json.dumps)
        return deferred

    # Feature: Force delete, which stops scaling, deletes all servers for
    #       you, then deletes the scaling group.
    @app.route('/', methods=['DELETE'])
    @with_transaction_id()
    @fails_with(exception_codes)
    @succeeds_with(204)
    def delete_scaling_group(self, request):
        """
        Delete a scaling group if there are no entities belonging to the
        scaling group.  If successful, no response body will be returned.
        """
        group = self.store.get_scaling_group(self.log, self.tenant_id,
                                             self.group_id)
        force = extract_bool_arg(request, 'force', False)
        return controller.delete_group(
            self.dispatcher, log, transaction_id(request), group, force)

    @app.route('/state/', methods=['GET'])
    @with_transaction_id()
    @fails_with(exception_codes)
    @succeeds_with(200)
    def get_scaling_group_state(self, request):
        """
        Get the current state of the scaling group, including the current set
        of active entities, number of pending entities, and the desired
        number of entities.  This data is returned in the body of the
        response in JSON format.

        There is no guarantee about the sort order of the list of active
        entities.

        Example response::

            {
                "group": {
                    "paused": false,
                    "pendingCapacity": 0,
                    "name": "testscalinggroup198547",
                    "active": [],
                    "activeCapacity": 0,
                    "desiredCapacity": 0
                }
            }
        """
        def _format_and_stackify(results):
            state, active = results
            return {"group": format_state_dict(state, active)}

        group = self.store.get_scaling_group(
            self.log, self.tenant_id, self.group_id)
        deferred = self.with_active_cache(group.view_state)
        deferred.addCallback(_format_and_stackify)
        deferred.addCallback(json.dumps)
        return deferred

    @app.route('/converge/', methods=['POST'])
    @with_transaction_id()
    @fails_with(exception_codes)
    @succeeds_with(204)
    def converge_scaling_group(self, request):
        """
        Trigger convergence on given scaling group
        """

        class ConvergeErrorGroup(Exception):
            pass

        def can_converge(group, state):
            if state.paused:
                raise GroupPausedError(group.tenant_id, group.uuid, "converge")
            conv_on_error = extract_bool_arg(request, 'on_error', True)
            if not conv_on_error and state.status == ScalingGroupStatus.ERROR:
                raise ConvergeErrorGroup()
            return state

        def converge_error_group_header(f):
            f.trap(ConvergeErrorGroup)
            request.setHeader("x-not-converging", "true")

        if tenant_is_enabled(self.tenant_id, config_value):
            group = self.store.get_scaling_group(
                self.log, self.tenant_id, self.group_id)
            return controller.modify_and_trigger(
                self.dispatcher,
                group,
                bound_log_kwargs(self.log),
                can_converge).addErrback(converge_error_group_header)
        else:
            request.setResponseCode(404)

    @app.route('/pause/', methods=['POST'])
    @with_transaction_id()
    @fails_with(exception_codes)
    @succeeds_with(204)
    def pause_scaling_group(self, request):
        """
        Pause a scaling group.  This means that no scaling policies will get
        executed (execution will be rejected).  This is an idempotent
        operation - pausing an already paused group does nothing.
        """
        group = self.store.get_scaling_group(
            self.log, self.tenant_id, self.group_id)
        return controller.pause_scaling_group(
            self.log, transaction_id(request), group, self.dispatcher)

    @app.route('/resume/', methods=['POST'])
    @with_transaction_id()
    @fails_with(exception_codes)
    @succeeds_with(204)
    def resume_scaling_group(self, request):
        """
        Resume a scaling group.  This means that scaling policies will now get
        executed as usual.  This is an idempotent operation - resuming an
        already running group does nothing.
        """
        group = self.store.get_scaling_group(
            self.log, self.tenant_id, self.group_id)
        return controller.resume_scaling_group(
            self.log, transaction_id(request), group, self.dispatcher)

    @app.route('/servers/', branch=True)
    def servers(self, request):
        """
        servers/ route handling
        """
        servers = OtterServers(self.store, self.tenant_id, self.group_id,
                               self.dispatcher)
        return servers.app.resource()

    @app.route('/config/')
    def config(self, request):
        """
        config route handled by OtterConfig
        """
        config = OtterConfig(self.store, self.tenant_id, self.group_id,
                             self.dispatcher)
        return config.app.resource()

    @app.route('/launch/')
    def launch(self, request):
        """
        launch route handled by OtterLaunch
        """
        launch = OtterLaunch(self.store, self.tenant_id, self.group_id)
        return launch.app.resource()

    @app.route('/policies/', branch=True)
    def policies(self, request):
        """
        policies routes handled by OtterPolicies
        """
        policies = OtterPolicies(self.store, self.tenant_id, self.group_id,
                                 self.dispatcher)
        return policies.app.resource()
Ejemplo n.º 14
0
class OtterGroups(object):
    """
    REST endpoints for managing scaling groups.
    """
    app = OtterApp()

    def __init__(self, store, tenant_id, dispatcher):
        self.log = log.bind(system='otter.rest.groups',
                            tenant_id=tenant_id)
        self.store = store
        self.tenant_id = tenant_id
        self.dispatcher = dispatcher

    @app.route('/', methods=['GET'])
    @with_transaction_id()
    @fails_with(exception_codes)
    @succeeds_with(200)
    @paginatable
    def list_all_scaling_groups(self, request, paginate):
        """
        Lists all the autoscaling groups and their states per for a given
        tenant ID.

        Example response::

            {
                "groups": [
                    {
                        "id": "e41380ae-173c-4b40-848a-25c16d7fa83d",
                        "links": [
                            {
                                "href": "https://dfw.autoscale.api
                                .rackspacecloud.com/
                                v1.0/676873/
                                groups/e41380ae-173c-4b40-848a-25c16d7fa83d/",
                                "rel": "self"
                            }
                        ],
                        "state": {
                            "active": [],
                            "activeCapacity": 0,
                            "desiredCapacity": 0,
                            "paused": false,
                            "pendingCapacity": 0,
                            "name": "testscalinggroup198547"
                        }
                    },
                    {
                        "id": "f82bb000-f451-40c8-9dc3-6919097d2f7e",
                        "state": {
                            "active": [],
                            "activeCapacity": 0,
                            "desiredCapacity": 0,
                            "paused": false,
                            "pendingCapacity": 0,
                            "name": "testscalinggroup194547"
                        },
                        "links": [
                            {
                                "href": "https://dfw.autoscale.api
                                .rackspacecloud.com/
                                v1.0/676873/
                                groups/f82bb000-f451-40c8-9dc3-6919097d2f7e/",
                                "rel": "self"
                            }
                        ]
                    }
                ],
                "groups_links": []
            }


        """

        def format_list(results):
            group_states, actives = results
            groups = [{
                'id': state.group_id,
                'links': get_autoscale_links(state.tenant_id, state.group_id),
                'state': format_state_dict(state, active)
            } for state, active in zip(group_states, actives)]
            return {
                "groups": groups,
                "groups_links": get_groups_links(
                    groups, self.tenant_id, None, **paginate)
            }

        def fetch_active_caches(group_states):
            if not tenant_is_enabled(self.tenant_id, config_value):
                return group_states, [None] * len(group_states)
            d = gatherResults(
                [get_active_cache(
                    self.store.reactor, self.store.connection, self.tenant_id,
                    state.group_id)
                 for state in group_states])
            return d.addCallback(lambda cache: (group_states, cache))

        deferred = self.store.list_scaling_group_states(
            self.log, self.tenant_id, **paginate)
        deferred.addCallback(fetch_active_caches)
        deferred.addCallback(format_list)
        deferred.addCallback(json.dumps)
        return deferred

    # -------------------------- CRD a scaling group -------------------------
    # (CRD = CRUD - U, because updating happens at suburls - so you can update
    # different parts)

    @app.route('/', methods=['POST'])
    @with_transaction_id()
    @fails_with(exception_codes)
    @succeeds_with(201)
    @validate_body(create_group_request)
    def create_new_scaling_group(self, request, data):
        """
        Create a new scaling group, given the general scaling group
        configuration, launch configuration, and optional scaling policies.
        This data provided in the request body in JSON format. If
        successful, the created group in JSON format containing id and links
        is returned.

        Example request body containing some scaling policies::

            {
              "launchConfiguration": {
                "args": {
                  "loadBalancers": [
                    {
                      "port": 8080,
                      "loadBalancerId": 9099
                    }
                  ],
                  "server": {
                    "name": "autoscale_server",
                    "imageRef": "0d589460-f177-4b0f-81c1-8ab8903ac7d8",
                    "flavorRef": "2",
                    "OS-DCF:diskConfig": "AUTO",
                    "metadata": {
                      "meta_key_1": "meta_value_1",
                      "meta_key_2": "meta_value_2"
                    },
                    "networks": [
                      {
                        "uuid": "11111111-1111-1111-1111-111111111111"
                      },
                      {
                        "uuid": "00000000-0000-0000-0000-000000000000"
                      }
                    ],
                    "personality": [
                      {
                        "path": "/root/.csivh",
                        "contents": "VGhpcyBpcyBhIHRlc3QgZmlsZS4="
                      }
                    ]
                  }
                },
                "type": "launch_server"
              },
              "groupConfiguration": {
                "maxEntities": 10,
                "cooldown": 360,
                "name": "testscalinggroup198547",
                "minEntities": 0,
                "metadata": {
                  "gc_meta_key_2": "gc_meta_value_2",
                  "gc_meta_key_1": "gc_meta_value_1"
                }
              },
              "scalingPolicies": [
                {
                  "cooldown": 0,
                  "type": "webhook",
                  "name": "scale up by 1",
                  "change": 1
                }
              ]
            }


        The ``scalingPolicies`` attribute can also be an empty list, or just
        left out entirely.

        Example response body to the above request::

            {
              "group": {
                "launchConfiguration": {
                  "args": {
                    "loadBalancers": [
                      {
                        "port": 8080,
                        "loadBalancerId": 9099
                      }
                    ],
                    "server": {
                      "name": "autoscale_server",
                      "imageRef": "0d589460-f177-4b0f-81c1-8ab8903ac7d8",
                      "flavorRef": "2",
                      "OS-DCF:diskConfig": "AUTO",
                      "personality": [
                        {
                          "path": "/root/.csivh",
                          "contents": "VGhpcyBpcyBhIHRlc3QgZmlsZS4="
                        }
                      ],
                      "networks": [
                        {
                          "uuid": "11111111-1111-1111-1111-111111111111"
                        },
                        {
                          "uuid": "00000000-0000-0000-0000-000000000000"
                        }
                      ],
                      "metadata": {
                        "meta_key_1": "meta_value_1",
                        "meta_key_2": "meta_value_2"
                      }
                    }
                  },
                  "type": "launch_server"
                },
                "groupConfiguration": {
                  "maxEntities": 10,
                  "cooldown": 360,
                  "name": "testscalinggroup198547",
                  "minEntities": 0,
                  "metadata": {
                    "gc_meta_key_2": "gc_meta_value_2",
                    "gc_meta_key_1": "gc_meta_value_1"
                  }
                },
                "state": {
                  "active": [],
                  "activeCapacity": 0,
                  "desiredCapacity": 0,
                  "paused": false,
                  "pendingCapacity": 0,
                  "name": "testscalinggroup198547"
                },
                "scalingPolicies": [
                  {
                    "name": "scale up by 1",
                    "links": [
                      {
                        "href": "https://ord.autoscale.api.rackspacecloud.com/
                        v1.0/829409/groups/6791761b-821a-4d07-820d-0b2afc7dd7f6/
                        policies/dceb14ac-b2b3-4f06-aac9-a5b6cd5d40e1/",
                        "rel": "self"
                      }
                    ],
                    "cooldown": 0,
                    "type": "webhook",
                    "id": "dceb14ac-b2b3-4f06-aac9-a5b6cd5d40e1",
                    "change": 1
                  }
                ],
                "links": [
                  {
                    "href": "https://ord.autoscale.api.rackspacecloud.com/
                    v1.0/829409/groups/6791761b-821a-4d07-820d-0b2afc7dd7f6/",
                    "rel": "self"
                  }
                ],
                "id": "6791761b-821a-4d07-820d-0b2afc7dd7f6"
              }
            }

        """
        group_cfg = data['groupConfiguration']

        group_cfg.setdefault('maxEntities', MAX_ENTITIES)
        group_cfg.setdefault('metadata', {})

        if group_cfg['minEntities'] > group_cfg['maxEntities']:
            raise InvalidMinEntities(
                "minEntities must be less than or equal to maxEntities")

        if data['launchConfiguration']['type'] == 'launch_server':
            validate_launch_config_servicenet(data['launchConfiguration'])

        deferred = get_supervisor().validate_launch_config(
            self.log, self.tenant_id, data['launchConfiguration'])

        deferred.addCallback(
            lambda _: self.store.create_scaling_group(
                self.log, self.tenant_id,
                group_cfg,
                normalize_launch_config(data['launchConfiguration']),
                data.get('scalingPolicies', None)))

        def _do_obey_config_change(result):
            group_id = result['id']
            config = result['groupConfiguration']
            launch = result['launchConfiguration']
            group = self.store.get_scaling_group(
                self.log, self.tenant_id, group_id)
            log = self.log.bind(scaling_group_id=group_id)
            d = controller.modify_and_trigger(
                self.dispatcher,
                group,
                bound_log_kwargs(log),
                partial(
                    controller.obey_config_change, log,
                    transaction_id(request), config, launch_config=launch),
                modify_state_reason='create_new_scaling_group')
            return d.addCallback(lambda _: result)

        deferred.addCallback(_do_obey_config_change)

        def _add_to_bobby(result, client):
            d = client.create_group(self.tenant_id, result['id'])
            return d.addCallback(lambda _: result)

        bobby = get_bobby()
        if bobby is not None:
            deferred.addCallback(_add_to_bobby, bobby)

        def _format_output(result):
            uuid = result['id']
            result["state"] = format_state_dict(result["state"])
            request.setHeader(
                "Location",
                get_autoscale_links(self.tenant_id, uuid, format=None))
            result["links"] = get_autoscale_links(self.tenant_id, uuid)
            linkify_policy_list(
                result['scalingPolicies'], self.tenant_id, uuid)
            result['scalingPolicies_links'] = get_policies_links(
                result['scalingPolicies'],
                self.tenant_id, uuid, rel='policies')
            return {"group": result}

        deferred.addCallback(_format_output)
        deferred.addCallback(json.dumps)
        return deferred

    @app.route('/<string:group_id>/', branch=True)
    def group(self, request, group_id):
        """
        Routes requiring a specific group_id are delegated to
        OtterGroup.
        """
        return OtterGroup(self.store, self.tenant_id,
                          group_id, self.dispatcher).app.resource()
Ejemplo n.º 15
0
class OtterWebhook(object):
    """
    REST endpoints for managing a specific scaling group webhook.
    """
    app = OtterApp()

    def __init__(self, store, tenant_id, group_id, policy_id, webhook_id):
        self.log = log.bind(system='otter.rest.webhook',
                            tenant_id=tenant_id,
                            scaling_group_id=group_id,
                            policy_id=policy_id,
                            webhook_id=webhook_id)
        self.store = store
        self.tenant_id = tenant_id
        self.group_id = group_id
        self.policy_id = policy_id
        self.webhook_id = webhook_id

    @app.route('/', methods=['GET'])
    @with_transaction_id()
    @fails_with(exception_codes)
    @succeeds_with(200)
    def get_webhook(self, request):
        """
        Get a webhook which has a name, some arbitrary metdata, and a capability
        URL.  This data is returned in the body of the response in JSON format.

        Example response::

            {
                "webhook": {
                    "id":"{webhookId}",
                    "name": "webhook name",
                    "metadata": {},
                    "links": [
                        {
                            "href": ".../{groupId1}/policies/{policyId1}/webhooks/{webhookId}/",
                            "rel": "self"
                        },
                        {
                            "href": ".../execute/1/{capability_hash2},
                            "rel": "capability"
                        }
                    ]
                }
            }
        """
        def format_one_webhook(webhook_model):
            result = _format_webhook(webhook_model,
                                     self.tenant_id,
                                     self.group_id,
                                     self.policy_id,
                                     webhook_id=self.webhook_id)
            return {'webhook': result}

        rec = self.store.get_scaling_group(self.log, self.tenant_id,
                                           self.group_id)
        deferred = rec.get_webhook(self.policy_id, self.webhook_id)
        deferred.addCallback(format_one_webhook)
        deferred.addCallback(json.dumps)
        return deferred

    @app.route('/', methods=['PUT'])
    @with_transaction_id()
    @fails_with(exception_codes)
    @succeeds_with(204)
    @validate_body(group_schemas.update_webhook)
    def update_webhook(self, request, data):
        """
        Update a particular webhook.
        A webhook may (but do not need to) include some arbitrary medata, and must
        include a name.
        If successful, no response body will be returned.

        Example request::

            {
                "name": "alice",
                "metadata": {
                    "notes": "this is for Alice"
                }
            }
        """
        rec = self.store.get_scaling_group(self.log, self.tenant_id,
                                           self.group_id)
        deferred = rec.update_webhook(self.policy_id, self.webhook_id, data)
        return deferred

    @app.route('/', methods=['DELETE'])
    @with_transaction_id()
    @fails_with(exception_codes)
    @succeeds_with(204)
    def delete_webhook(self, request):
        """
        Deletes a particular webhook.
        If successful, no response body will be returned.
        """
        rec = self.store.get_scaling_group(self.log, self.tenant_id,
                                           self.group_id)
        deferred = rec.delete_webhook(self.policy_id, self.webhook_id)
        return deferred
Ejemplo n.º 16
0
class Otter(object):
    """
    Otter holds the Klein app and routes for the REST service.
    """
    app = OtterApp()

    def __init__(self, store, region, health_check_function=None, _treq=None):
        self.store = store
        self.region = region
        self.health_check_function = health_check_function
        self.scheduler = None
        self.treq = _treq
        # Effect dispatcher for all otter intents
        self.dispatcher = None

    @app.route('/', methods=['GET'])
    def base(self, request):
        """
        base root route.

        :returns: Whatever is configured to be returned by the root
        """
        code = config_value('root.code')
        if code is not None:
            request.setResponseCode(code)

        headers = config_value('root.headers')
        if headers is not None:
            for header in headers:
                for value in headers[header]:
                    request.setHeader(str(header), str(value))

        body = config_value('root.body')
        if body is not None:
            return body

        return ''

    @app.route('/v1.0/<string:tenant_id>/groups/', branch=True)
    def groups(self, request, tenant_id):
        """
        group routes delegated to OtterGroups.
        """
        return OtterGroups(
            self.store, tenant_id, self.dispatcher).app.resource()

    @app.route('/v1.0/execute/<string:cap_version>/<string:cap_hash>/')
    def execute(self, request, cap_version, cap_hash):
        """
        execute route handled by OtterExecute
        """
        return OtterExecute(self.store, cap_version, cap_hash,
                            self.dispatcher).app.resource()

    @app.route('/v1.0/<string:tenant_id>/limits')
    def limits(self, request, tenant_id):
        """
        return group limit maximums
        """
        return OtterLimits(self.store, tenant_id).app.resource()

    @app.route('/health', methods=['GET'])
    def health_check(self, request):
        """
        Return whether health checks succeeded
        """
        request.setHeader('X-Response-Id', 'health_check')
        if self.health_check_function:
            return self.health_check_function().addCallback(json.dumps, sort_keys=True,
                                                            indent=4)

        return json.dumps({'healthy': True})

    @app.route('/scheduler/reset', methods=['POST'])
    def scheduler_reset(self, request):
        """
        Reset the scheduler with new path
        """
        new_path = request.args.get('path')[0]
        request.setHeader('X-Response-Id', 'scheduler_reset')
        try:
            self.scheduler.reset(new_path)
        except ValueError as e:
            request.setResponseCode(400)
            return e.message
        else:
            return ''

    @app.route('/scheduler/stop', methods=['POST'])
    def scheduler_stop(self, request):
        """
        Stop the scheduler
        """
        request.setHeader('X-Response-Id', 'scheduler_stop')
        d = maybeDeferred(self.scheduler.stopService)
        return d.addCallback(lambda _: '')
Ejemplo n.º 17
0
class OtterPolicy(object):
    """
    REST endpoints for a specific policy of a scaling group.
    """
    app = OtterApp()

    def __init__(self, store, tenant_id, scaling_group_id, policy_id,
                 dispatcher):
        self.log = log.bind(system='otter.log.policy',
                            tenant_id=tenant_id,
                            scaling_group_id=scaling_group_id,
                            policy_id=policy_id)
        self.store = store
        self.tenant_id = tenant_id
        self.scaling_group_id = scaling_group_id
        self.policy_id = policy_id
        self.dispatcher = dispatcher

    @app.route('/', methods=['GET'])
    @with_transaction_id()
    @fails_with(exception_codes)
    @succeeds_with(200)
    def get_policy(self, request):
        """
        Get a scaling policy which describes an id, name, type, adjustment, and
        cooldown, and links.  This data is returned in the body of the response in
        JSON format.

        Example response::

            {
                "policy": {
                    "id": {policyId},
                    "links": [
                        {
                            "href": "{url_root}/v1.0/010101/groups/{groupId}/policy/{policyId}/"
                            "rel": "self"
                        }
                    ],
                    "name": "scale up by one server",
                    "change": 1,
                    "cooldown": 150
                }
            }
        """
        def openstackify(policy_dict):
            policy_dict['id'] = self.policy_id
            policy_dict['links'] = get_autoscale_links(self.tenant_id,
                                                       self.scaling_group_id,
                                                       self.policy_id)
            return {'policy': policy_dict}

        rec = self.store.get_scaling_group(self.log, self.tenant_id,
                                           self.scaling_group_id)
        deferred = rec.get_policy(self.policy_id)
        deferred.addCallback(openstackify)
        deferred.addCallback(json.dumps)
        return deferred

    @app.route('/', methods=['PUT'])
    @with_transaction_id()
    @fails_with(exception_codes)
    @succeeds_with(204)
    @validate_body(group_schemas.policy)
    def update_policy(self, request, data):
        """
        Updates a scaling policy. Scaling policies must include a name, type,
        adjustment, and cooldown.
        If successful, no response body will be returned.

        Example request::

            {
                "name": "scale up by two servers",
                "change": 2,
                "cooldown": 150
            }


        """
        rec = self.store.get_scaling_group(self.log, self.tenant_id,
                                           self.scaling_group_id)
        deferred = rec.update_policy(self.policy_id, data)
        return deferred

    @app.route('/', methods=['DELETE'])
    @with_transaction_id()
    @fails_with(exception_codes)
    @succeeds_with(204)
    @auditable('request.policy.delete', 'Deleted scaling policy {policy_id}.')
    def delete_policy(self, request, audit_logger):
        """
        Delete a scaling policy. If successful, no response body will be returned.
        """
        rec = self.store.get_scaling_group(self.log, self.tenant_id,
                                           self.scaling_group_id)
        deferred = rec.delete_policy(self.policy_id)
        return deferred

    @app.route('/execute/', methods=['POST'])
    @with_transaction_id()
    @fails_with(exception_codes)
    @succeeds_with(202)
    def execute_policy(self, request):
        """
        Execute this scaling policy.

        TBD: Response body.

        Example response::

            {}
        """
        group = self.store.get_scaling_group(self.log, self.tenant_id,
                                             self.scaling_group_id)
        d = controller.modify_and_trigger(
            self.dispatcher,
            group,
            bound_log_kwargs(self.log),
            partial(controller.maybe_execute_scaling_policy,
                    self.log,
                    transaction_id(request),
                    policy_id=self.policy_id),
            modify_state_reason='execute_policy')
        d.addCallback(lambda _: "{}")  # Return value TBD
        return d

    @app.route('/webhooks/', branch=True)
    def webhooks(self, request):
        """
        webhook routes handled by OtterWebhooks
        """
        return OtterWebhooks(self.store, self.tenant_id, self.scaling_group_id,
                             self.policy_id).app.resource()