diff --git a/moto/applicationautoscaling/exceptions.py b/moto/applicationautoscaling/exceptions.py index 2e2e0ef9..8d5fb3c0 100644 --- a/moto/applicationautoscaling/exceptions.py +++ b/moto/applicationautoscaling/exceptions.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals import json +from moto.core.exceptions import JsonRESTError class AWSError(Exception): @@ -18,5 +19,8 @@ class AWSError(Exception): return json.dumps(resp), dict(status=self.STATUS) -class AWSValidationException(AWSError): - TYPE = "ValidationException" +class AWSValidationException(JsonRESTError): + def __init__(self, message, **kwargs): + super(AWSValidationException, self).__init__( + "ValidationException", message, **kwargs + ) diff --git a/moto/applicationautoscaling/models.py b/moto/applicationautoscaling/models.py index ebf6594d..47a1adad 100644 --- a/moto/applicationautoscaling/models.py +++ b/moto/applicationautoscaling/models.py @@ -5,6 +5,7 @@ from .exceptions import AWSValidationException from collections import OrderedDict from enum import Enum, unique import time +import uuid @unique @@ -58,6 +59,7 @@ class ApplicationAutoscalingBackend(BaseBackend): self.region = region self.ecs_backend = ecs self.targets = OrderedDict() + self.policies = {} def reset(self): region = self.region @@ -124,6 +126,100 @@ class ApplicationAutoscalingBackend(BaseBackend): self.targets[target.scalable_dimension][target.resource_id] = target return target + def deregister_scalable_target(self, namespace, r_id, dimension): + """ Registers or updates a scalable target. """ + if self._scalable_target_exists(r_id, dimension): + del self.targets[dimension][r_id] + else: + raise AWSValidationException( + "No scalable target found for service namespace: {}, resource ID: {}, scalable dimension: {}".format( + namespace, r_id, dimension + ) + ) + + def put_scaling_policy( + self, + policy_name, + service_namespace, + resource_id, + scalable_dimension, + policy_body, + policy_type=None, + ): + policy_key = FakeApplicationAutoscalingPolicy.formulate_key( + service_namespace, resource_id, scalable_dimension, policy_name + ) + if policy_key in self.policies: + old_policy = self.policies[policy_name] + policy = FakeApplicationAutoscalingPolicy( + region_name=self.region, + policy_name=policy_name, + service_namespace=service_namespace, + resource_id=resource_id, + scalable_dimension=scalable_dimension, + policy_type=policy_type if policy_type else old_policy.policy_type, + policy_body=policy_body if policy_body else old_policy._policy_body, + ) + else: + policy = FakeApplicationAutoscalingPolicy( + region_name=self.region, + policy_name=policy_name, + service_namespace=service_namespace, + resource_id=resource_id, + scalable_dimension=scalable_dimension, + policy_type=policy_type, + policy_body=policy_body, + ) + self.policies[policy_key] = policy + return policy + + def describe_scaling_policies(self, service_namespace, **kwargs): + policy_names = kwargs.get("policy_names") + resource_id = kwargs.get("resource_id") + scalable_dimension = kwargs.get("scalable_dimension") + max_results = kwargs.get("max_results") or 100 + next_token = kwargs.get("next_token") + policies = [ + policy + for policy in self.policies.values() + if policy.service_namespace == service_namespace + ] + if policy_names: + policies = [ + policy for policy in policies if policy.policy_name in policy_names + ] + if resource_id: + policies = [ + policy for policy in policies if policy.resource_id in resource_id + ] + if scalable_dimension: + policies = [ + policy + for policy in policies + if policy.scalable_dimension in scalable_dimension + ] + starting_point = int(next_token) if next_token else 0 + ending_point = starting_point + max_results + policies_page = policies[starting_point:ending_point] + new_next_token = str(ending_point) if ending_point < len(policies) else None + return new_next_token, policies_page + + def delete_scaling_policy( + self, policy_name, service_namespace, resource_id, scalable_dimension + ): + policy_key = FakeApplicationAutoscalingPolicy.formulate_key( + service_namespace, resource_id, scalable_dimension, policy_name + ) + if policy_key in self.policies: + del self.policies[policy_key] + return {} + else: + raise AWSValidationException( + "No scaling policy found for service namespace: {}, resource ID: {}, scalable dimension: {}, policy name: {}".format( + service_namespace, resource_id, scalable_dimension, policy_name + ) + ) + def _target_params_are_valid(namespace, r_id, dimension): """ Check whether namespace, resource_id and dimension are valid and consistent with each other. """ @@ -201,6 +297,50 @@ class FakeScalableTarget(BaseModel): self.suspended_state = kwargs["suspended_state"] +class FakeApplicationAutoscalingPolicy(BaseModel): + def __init__( + self, + region_name, + policy_name, + service_namespace, + resource_id, + scalable_dimension, + policy_type, + policy_body, + ): + self.step_scaling_policy_configuration = None + self.target_tracking_scaling_policy_configuration = None + + if "policy_type" == "StepScaling": + self.step_scaling_policy_configuration = policy_body + self.target_tracking_scaling_policy_configuration = None + elif policy_type == "TargetTrackingScaling": + self.step_scaling_policy_configuration = None + self.target_tracking_scaling_policy_configuration = policy_body + else: + raise AWSValidationException( + "Unknown policy type {} specified.".format(policy_type) + ) + + self._policy_body = policy_body + self.service_namespace = service_namespace + self.resource_id = resource_id + self.scalable_dimension = scalable_dimension + self.policy_name = policy_name + self.policy_type = policy_type + self._guid = uuid.uuid4() + self.policy_arn = "arn:aws:autoscaling:{}:scalingPolicy:{}:resource/sagemaker/{}:policyName/{}".format( + region_name, self._guid, self.resource_id, self.policy_name + ) + self.creation_time = time.time() + + @staticmethod + def formulate_key(service_namespace, resource_id, scalable_dimension, policy_name): + return "{}\t{}\t{}\t{}".format( + service_namespace, resource_id, scalable_dimension, policy_name + ) + + applicationautoscaling_backends = {} for region_name, ecs_backend in ecs_backends.items(): applicationautoscaling_backends[region_name] = ApplicationAutoscalingBackend( diff --git a/moto/applicationautoscaling/responses.py b/moto/applicationautoscaling/responses.py index 9a2905d7..5bb0a414 100644 --- a/moto/applicationautoscaling/responses.py +++ b/moto/applicationautoscaling/responses.py @@ -15,10 +15,7 @@ class ApplicationAutoScalingResponse(BaseResponse): return applicationautoscaling_backends[self.region] def describe_scalable_targets(self): - try: - self._validate_params() - except AWSValidationException as e: - return e.response() + self._validate_params() service_namespace = self._get_param("ServiceNamespace") resource_ids = self._get_param("ResourceIds") scalable_dimension = self._get_param("ScalableDimension") @@ -37,19 +34,65 @@ class ApplicationAutoScalingResponse(BaseResponse): def register_scalable_target(self): """ Registers or updates a scalable target. """ - try: - self._validate_params() - self.applicationautoscaling_backend.register_scalable_target( - self._get_param("ServiceNamespace"), - self._get_param("ResourceId"), - self._get_param("ScalableDimension"), - min_capacity=self._get_int_param("MinCapacity"), - max_capacity=self._get_int_param("MaxCapacity"), - role_arn=self._get_param("RoleARN"), - suspended_state=self._get_param("SuspendedState"), - ) - except AWSValidationException as e: - return e.response() + self._validate_params() + self.applicationautoscaling_backend.register_scalable_target( + self._get_param("ServiceNamespace"), + self._get_param("ResourceId"), + self._get_param("ScalableDimension"), + min_capacity=self._get_int_param("MinCapacity"), + max_capacity=self._get_int_param("MaxCapacity"), + role_arn=self._get_param("RoleARN"), + suspended_state=self._get_param("SuspendedState"), + ) + return json.dumps({}) + + def deregister_scalable_target(self): + """ Deregisters a scalable target. """ + self._validate_params() + self.applicationautoscaling_backend.deregister_scalable_target( + self._get_param("ServiceNamespace"), + self._get_param("ResourceId"), + self._get_param("ScalableDimension"), + ) + return json.dumps({}) + + def put_scaling_policy(self): + policy = self.applicationautoscaling_backend.put_scaling_policy( + policy_name=self._get_param("PolicyName"), + service_namespace=self._get_param("ServiceNamespace"), + resource_id=self._get_param("ResourceId"), + scalable_dimension=self._get_param("ScalableDimension"), + policy_type=self._get_param("PolicyType"), + policy_body=self._get_param( + "StepScalingPolicyConfiguration", + self._get_param("TargetTrackingScalingPolicyConfiguration"), + ), + ) + return json.dumps({"PolicyARN": policy.policy_arn, "Alarms": []}) # ToDo + + def describe_scaling_policies(self): + ( + next_token, + policy_page, + ) = self.applicationautoscaling_backend.describe_scaling_policies( + service_namespace=self._get_param("ServiceNamespace"), + resource_id=self._get_param("ResourceId"), + scalable_dimension=self._get_param("ScalableDimension"), + max_results=self._get_param("MaxResults"), + next_token=self._get_param("NextToken"), + ) + response_obj = {"ScalingPolicies": [_build_policy(p) for p in policy_page]} + if next_token: + response_obj["NextToken"] = next_token + return json.dumps(response_obj) + + def delete_scaling_policy(self): + self.applicationautoscaling_backend.delete_scaling_policy( + policy_name=self._get_param("PolicyName"), + service_namespace=self._get_param("ServiceNamespace"), + resource_id=self._get_param("ResourceId"), + scalable_dimension=self._get_param("ScalableDimension"), + ) return json.dumps({}) def _validate_params(self): @@ -95,3 +138,22 @@ def _build_target(t): "MinCapacity": t.min_capacity, "SuspendedState": t.suspended_state, } + + +def _build_policy(p): + response = { + "PolicyARN": p.policy_arn, + "PolicyName": p.policy_name, + "ServiceNamespace": p.service_namespace, + "ResourceId": p.resource_id, + "ScalableDimension": p.scalable_dimension, + "PolicyType": p.policy_type, + "CreationTime": p.creation_time, + } + if p.policy_type == "StepScaling": + response["StepScalingPolicyConfiguration"] = p.step_scaling_policy_configuration + elif p.policy_type == "TargetTrackingScaling": + response[ + "TargetTrackingScalingPolicyConfiguration" + ] = p.target_tracking_scaling_policy_configuration + return response diff --git a/tests/test_applicationautoscaling/test_applicationautoscaling.py b/tests/test_applicationautoscaling/test_applicationautoscaling.py index ce835199..9b1c0b67 100644 --- a/tests/test_applicationautoscaling/test_applicationautoscaling.py +++ b/tests/test_applicationautoscaling/test_applicationautoscaling.py @@ -1,8 +1,10 @@ from __future__ import unicode_literals +import botocore import boto3 -from moto import mock_applicationautoscaling, mock_ecs import sure # noqa -from nose.tools import with_setup +from nose.tools import assert_raises +from moto import mock_applicationautoscaling, mock_ecs +from moto.applicationautoscaling.exceptions import AWSValidationException DEFAULT_REGION = "us-east-1" DEFAULT_ECS_CLUSTER = "default" @@ -250,6 +252,8 @@ def test_register_scalable_target_resource_id_variations(): ServiceNamespace=namespace, ResourceId=resource_id, ScalableDimension=scalable_dimension, + MinCapacity=1, + MaxCapacity=8, ) response = client.describe_scalable_targets(ServiceNamespace=namespace) response["ResponseMetadata"]["HTTPStatusCode"].should.equal(200) @@ -304,3 +308,211 @@ def test_register_scalable_target_updates_existing_target(): t["SuspendedState"]["ScheduledScalingSuspended"].should.equal( updated_suspended_state["ScheduledScalingSuspended"] ) + + +@mock_applicationautoscaling +def test_put_scaling_policy(): + client = boto3.client("application-autoscaling", region_name=DEFAULT_REGION) + namespace = "sagemaker" + resource_id = "endpoint/MyEndPoint/variant/MyVariant" + scalable_dimension = "sagemaker:variant:DesiredInstanceCount" + + client.register_scalable_target( + ServiceNamespace=namespace, + ResourceId=resource_id, + ScalableDimension=scalable_dimension, + MinCapacity=1, + MaxCapacity=8, + ) + + policy_name = "MyPolicy" + policy_type = "TargetTrackingScaling" + policy_body = { + "TargetValue": 70.0, + "PredefinedMetricSpecification": { + "PredefinedMetricType": "SageMakerVariantInvocationsPerInstance" + }, + } + + with assert_raises(client.exceptions.ValidationException) as e: + client.put_scaling_policy( + PolicyName=policy_name, + ServiceNamespace=namespace, + ResourceId=resource_id, + ScalableDimension=scalable_dimension, + PolicyType="ABCDEFG", + TargetTrackingScalingPolicyConfiguration=policy_body, + ) + e.exception.response["Error"]["Message"].should.match( + r"Unknown policy type .* specified." + ) + + response = client.put_scaling_policy( + PolicyName=policy_name, + ServiceNamespace=namespace, + ResourceId=resource_id, + ScalableDimension=scalable_dimension, + PolicyType=policy_type, + TargetTrackingScalingPolicyConfiguration=policy_body, + ) + response["ResponseMetadata"]["HTTPStatusCode"].should.equal(200) + response["PolicyARN"].should.match( + r"arn:aws:autoscaling:.*1:scalingPolicy:.*:resource/{}/{}:policyName/{}".format( + namespace, resource_id, policy_name + ) + ) + + +@mock_applicationautoscaling +def test_describe_scaling_policies(): + client = boto3.client("application-autoscaling", region_name=DEFAULT_REGION) + namespace = "sagemaker" + resource_id = "endpoint/MyEndPoint/variant/MyVariant" + scalable_dimension = "sagemaker:variant:DesiredInstanceCount" + + client.register_scalable_target( + ServiceNamespace=namespace, + ResourceId=resource_id, + ScalableDimension=scalable_dimension, + MinCapacity=1, + MaxCapacity=8, + ) + + policy_name = "MyPolicy" + policy_type = "TargetTrackingScaling" + policy_body = { + "TargetValue": 70.0, + "PredefinedMetricSpecification": { + "PredefinedMetricType": "SageMakerVariantInvocationsPerInstance" + }, + } + + response = client.put_scaling_policy( + PolicyName=policy_name, + ServiceNamespace=namespace, + ResourceId=resource_id, + ScalableDimension=scalable_dimension, + PolicyType=policy_type, + TargetTrackingScalingPolicyConfiguration=policy_body, + ) + response["ResponseMetadata"]["HTTPStatusCode"].should.equal(200) + + response = client.describe_scaling_policies( + PolicyNames=[policy_name], + ServiceNamespace=namespace, + ResourceId=resource_id, + ScalableDimension=scalable_dimension, + ) + response["ResponseMetadata"]["HTTPStatusCode"].should.equal(200) + policy = response["ScalingPolicies"][0] + policy["PolicyName"].should.equal(policy_name) + policy["ServiceNamespace"].should.equal(namespace) + policy["ResourceId"].should.equal(resource_id) + policy["ScalableDimension"].should.equal(scalable_dimension) + policy["PolicyType"].should.equal(policy_type) + policy["TargetTrackingScalingPolicyConfiguration"].should.equal(policy_body) + policy["PolicyARN"].should.match( + r"arn:aws:autoscaling:.*1:scalingPolicy:.*:resource/{}/{}:policyName/{}".format( + namespace, resource_id, policy_name + ) + ) + policy.should.have.key("CreationTime").which.should.be.a("datetime.datetime") + + +@mock_applicationautoscaling +def test_delete_scaling_policies(): + client = boto3.client("application-autoscaling", region_name=DEFAULT_REGION) + namespace = "sagemaker" + resource_id = "endpoint/MyEndPoint/variant/MyVariant" + scalable_dimension = "sagemaker:variant:DesiredInstanceCount" + + client.register_scalable_target( + ServiceNamespace=namespace, + ResourceId=resource_id, + ScalableDimension=scalable_dimension, + MinCapacity=1, + MaxCapacity=8, + ) + + policy_name = "MyPolicy" + policy_type = "TargetTrackingScaling" + policy_body = { + "TargetValue": 70.0, + "PredefinedMetricSpecification": { + "PredefinedMetricType": "SageMakerVariantInvocationsPerInstance" + }, + } + + with assert_raises(client.exceptions.ValidationException) as e: + client.delete_scaling_policy( + PolicyName=policy_name, + ServiceNamespace=namespace, + ResourceId=resource_id, + ScalableDimension=scalable_dimension, + ) + e.exception.response["Error"]["Message"].should.match(r"No scaling policy found .*") + + response = client.put_scaling_policy( + PolicyName=policy_name, + ServiceNamespace=namespace, + ResourceId=resource_id, + ScalableDimension=scalable_dimension, + PolicyType=policy_type, + TargetTrackingScalingPolicyConfiguration=policy_body, + ) + response["ResponseMetadata"]["HTTPStatusCode"].should.equal(200) + + response = client.delete_scaling_policy( + PolicyName=policy_name, + ServiceNamespace=namespace, + ResourceId=resource_id, + ScalableDimension=scalable_dimension, + ) + response["ResponseMetadata"]["HTTPStatusCode"].should.equal(200) + + response = client.describe_scaling_policies( + PolicyNames=[policy_name], + ServiceNamespace=namespace, + ResourceId=resource_id, + ScalableDimension=scalable_dimension, + ) + response["ResponseMetadata"]["HTTPStatusCode"].should.equal(200) + len(response["ScalingPolicies"]).should.equal(0) + + +@mock_applicationautoscaling +def test_deregister_scalable_target(): + client = boto3.client("application-autoscaling", region_name=DEFAULT_REGION) + namespace = "sagemaker" + resource_id = "endpoint/MyEndPoint/variant/MyVariant" + scalable_dimension = "sagemaker:variant:DesiredInstanceCount" + + client.register_scalable_target( + ServiceNamespace=namespace, + ResourceId=resource_id, + ScalableDimension=scalable_dimension, + MinCapacity=1, + MaxCapacity=8, + ) + + response = client.describe_scalable_targets(ServiceNamespace=namespace) + len(response["ScalableTargets"]).should.equal(1) + + client.deregister_scalable_target( + ServiceNamespace=namespace, + ResourceId=resource_id, + ScalableDimension=scalable_dimension, + ) + + response = client.describe_scalable_targets(ServiceNamespace=namespace) + len(response["ScalableTargets"]).should.equal(0) + + with assert_raises(client.exceptions.ValidationException) as e: + client.deregister_scalable_target( + ServiceNamespace=namespace, + ResourceId=resource_id, + ScalableDimension=scalable_dimension, + ) + e.exception.response["Error"]["Message"].should.match( + r"No scalable target found .*" + )