ApplicationAutoscaling: support autoscaling policies, deregister_scalable_target (#3350)

* ApplicationAutoscaling: support autoscaling policies, deregister_scalable_target.

* PR3350 comment changes: drop unnecessary pass statements, unit test three exception cases.

Co-authored-by: Joseph Weitekamp <jweite@amazon.com>
This commit is contained in:
jweite 2020-10-30 11:21:34 -04:00 committed by GitHub
parent d499d4d179
commit cbd4efb42d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 439 additions and 21 deletions

View File

@ -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
)

View File

@ -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(

View File

@ -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

View File

@ -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 .*"
)