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:
parent
d499d4d179
commit
cbd4efb42d
@ -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
|
||||
)
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
|
@ -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 .*"
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user