Merge branch 'master' into ssm_docs
This commit is contained in:
commit
bedcc83995
17 changed files with 727 additions and 7 deletions
|
|
@ -15,6 +15,9 @@ mock_acm = lazy_load(".acm", "mock_acm")
|
|||
mock_apigateway = lazy_load(".apigateway", "mock_apigateway")
|
||||
mock_apigateway_deprecated = lazy_load(".apigateway", "mock_apigateway_deprecated")
|
||||
mock_athena = lazy_load(".athena", "mock_athena")
|
||||
mock_applicationautoscaling = lazy_load(
|
||||
".applicationautoscaling", "mock_applicationautoscaling"
|
||||
)
|
||||
mock_autoscaling = lazy_load(".autoscaling", "mock_autoscaling")
|
||||
mock_autoscaling_deprecated = lazy_load(".autoscaling", "mock_autoscaling_deprecated")
|
||||
mock_lambda = lazy_load(".awslambda", "mock_lambda")
|
||||
|
|
|
|||
6
moto/applicationautoscaling/__init__.py
Normal file
6
moto/applicationautoscaling/__init__.py
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
from __future__ import unicode_literals
|
||||
from .models import applicationautoscaling_backends
|
||||
from ..core.models import base_decorator
|
||||
|
||||
applicationautoscaling_backend = applicationautoscaling_backends["us-east-1"]
|
||||
mock_applicationautoscaling = base_decorator(applicationautoscaling_backends)
|
||||
22
moto/applicationautoscaling/exceptions.py
Normal file
22
moto/applicationautoscaling/exceptions.py
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
from __future__ import unicode_literals
|
||||
import json
|
||||
|
||||
|
||||
class AWSError(Exception):
|
||||
""" Copied from acm/models.py; this class now exists in >5 locations,
|
||||
maybe this should be centralised for use by any module?
|
||||
"""
|
||||
|
||||
TYPE = None
|
||||
STATUS = 400
|
||||
|
||||
def __init__(self, message):
|
||||
self.message = message
|
||||
|
||||
def response(self):
|
||||
resp = {"__type": self.TYPE, "message": self.message}
|
||||
return json.dumps(resp), dict(status=self.STATUS)
|
||||
|
||||
|
||||
class AWSValidationException(AWSError):
|
||||
TYPE = "ValidationException"
|
||||
179
moto/applicationautoscaling/models.py
Normal file
179
moto/applicationautoscaling/models.py
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
from __future__ import unicode_literals
|
||||
from moto.core import BaseBackend, BaseModel
|
||||
from moto.ecs import ecs_backends
|
||||
from .exceptions import AWSValidationException
|
||||
from collections import OrderedDict
|
||||
from enum import Enum, unique
|
||||
import time
|
||||
|
||||
|
||||
@unique
|
||||
class ServiceNamespaceValueSet(Enum):
|
||||
APPSTREAM = "appstream"
|
||||
RDS = "rds"
|
||||
LAMBDA = "lambda"
|
||||
CASSANDRA = "cassandra"
|
||||
DYNAMODB = "dynamodb"
|
||||
CUSTOM_RESOURCE = "custom-resource"
|
||||
ELASTICMAPREDUCE = "elasticmapreduce"
|
||||
EC2 = "ec2"
|
||||
COMPREHEND = "comprehend"
|
||||
ECS = "ecs"
|
||||
SAGEMAKER = "sagemaker"
|
||||
|
||||
|
||||
@unique
|
||||
class ScalableDimensionValueSet(Enum):
|
||||
CASSANDRA_TABLE_READ_CAPACITY_UNITS = "cassandra:table:ReadCapacityUnits"
|
||||
CASSANDRA_TABLE_WRITE_CAPACITY_UNITS = "cassandra:table:WriteCapacityUnits"
|
||||
DYNAMODB_INDEX_READ_CAPACITY_UNITS = "dynamodb:index:ReadCapacityUnits"
|
||||
DYNAMODB_INDEX_WRITE_CAPACITY_UNITS = "dynamodb:index:WriteCapacityUnits"
|
||||
DYNAMODB_TABLE_READ_CAPACITY_UNITS = "dynamodb:table:ReadCapacityUnits"
|
||||
DYNAMODB_TABLE_WRITE_CAPACITY_UNITS = "dynamodb:table:WriteCapacityUnits"
|
||||
RDS_CLUSTER_READ_REPLICA_COUNT = "rds:cluster:ReadReplicaCount"
|
||||
RDS_CLUSTER_CAPACITY = "rds:cluster:Capacity"
|
||||
COMPREHEND_DOCUMENT_CLASSIFIER_ENDPOINT_DESIRED_INFERENCE_UNITS = (
|
||||
"comprehend:document-classifier-endpoint:DesiredInferenceUnits"
|
||||
)
|
||||
ELASTICMAPREDUCE_INSTANCE_FLEET_ON_DEMAND_CAPACITY = (
|
||||
"elasticmapreduce:instancefleet:OnDemandCapacity"
|
||||
)
|
||||
ELASTICMAPREDUCE_INSTANCE_FLEET_SPOT_CAPACITY = (
|
||||
"elasticmapreduce:instancefleet:SpotCapacity"
|
||||
)
|
||||
ELASTICMAPREDUCE_INSTANCE_GROUP_INSTANCE_COUNT = (
|
||||
"elasticmapreduce:instancegroup:InstanceCount"
|
||||
)
|
||||
LAMBDA_FUNCTION_PROVISIONED_CONCURRENCY = "lambda:function:ProvisionedConcurrency"
|
||||
APPSTREAM_FLEET_DESIRED_CAPACITY = "appstream:fleet:DesiredCapacity"
|
||||
CUSTOM_RESOURCE_RESOURCE_TYPE_PROPERTY = "custom-resource:ResourceType:Property"
|
||||
SAGEMAKER_VARIANT_DESIRED_INSTANCE_COUNT = "sagemaker:variant:DesiredInstanceCount"
|
||||
EC2_SPOT_FLEET_REQUEST_TARGET_CAPACITY = "ec2:spot-fleet-request:TargetCapacity"
|
||||
ECS_SERVICE_DESIRED_COUNT = "ecs:service:DesiredCount"
|
||||
|
||||
|
||||
class ApplicationAutoscalingBackend(BaseBackend):
|
||||
def __init__(self, region, ecs):
|
||||
super(ApplicationAutoscalingBackend, self).__init__()
|
||||
self.region = region
|
||||
self.ecs_backend = ecs
|
||||
self.targets = OrderedDict()
|
||||
|
||||
def reset(self):
|
||||
region = self.region
|
||||
ecs = self.ecs_backend
|
||||
self.__dict__ = {}
|
||||
self.__init__(region, ecs)
|
||||
|
||||
@property
|
||||
def applicationautoscaling_backend(self):
|
||||
return applicationautoscaling_backends[self.region]
|
||||
|
||||
def describe_scalable_targets(
|
||||
self, namespace, r_ids=None, dimension=None,
|
||||
):
|
||||
""" Describe scalable targets. """
|
||||
if r_ids is None:
|
||||
r_ids = []
|
||||
targets = self._flatten_scalable_targets(namespace)
|
||||
if dimension is not None:
|
||||
targets = [t for t in targets if t.scalable_dimension == dimension]
|
||||
if len(r_ids) > 0:
|
||||
targets = [t for t in targets if t.resource_id in r_ids]
|
||||
return targets
|
||||
|
||||
def _flatten_scalable_targets(self, namespace):
|
||||
""" Flatten scalable targets for a given service namespace down to a list. """
|
||||
targets = []
|
||||
for dimension in self.targets.keys():
|
||||
for resource_id in self.targets[dimension].keys():
|
||||
targets.append(self.targets[dimension][resource_id])
|
||||
targets = [t for t in targets if t.service_namespace == namespace]
|
||||
return targets
|
||||
|
||||
def register_scalable_target(self, namespace, r_id, dimension, **kwargs):
|
||||
""" Registers or updates a scalable target. """
|
||||
_ = _target_params_are_valid(namespace, r_id, dimension)
|
||||
if namespace == ServiceNamespaceValueSet.ECS.value:
|
||||
_ = self._ecs_service_exists_for_target(r_id)
|
||||
if self._scalable_target_exists(r_id, dimension):
|
||||
target = self.targets[dimension][r_id]
|
||||
target.update(kwargs)
|
||||
else:
|
||||
target = FakeScalableTarget(self, namespace, r_id, dimension, **kwargs)
|
||||
self._add_scalable_target(target)
|
||||
return target
|
||||
|
||||
def _scalable_target_exists(self, r_id, dimension):
|
||||
return r_id in self.targets.get(dimension, [])
|
||||
|
||||
def _ecs_service_exists_for_target(self, r_id):
|
||||
""" Raises a ValidationException if an ECS service does not exist
|
||||
for the specified resource ID.
|
||||
"""
|
||||
resource_type, cluster, service = r_id.split("/")
|
||||
result = self.ecs_backend.describe_services(cluster, [service])
|
||||
if len(result) != 1:
|
||||
raise AWSValidationException("ECS service doesn't exist: {}".format(r_id))
|
||||
return True
|
||||
|
||||
def _add_scalable_target(self, target):
|
||||
if target.scalable_dimension not in self.targets:
|
||||
self.targets[target.scalable_dimension] = OrderedDict()
|
||||
if target.resource_id not in self.targets[target.scalable_dimension]:
|
||||
self.targets[target.scalable_dimension][target.resource_id] = target
|
||||
return target
|
||||
|
||||
|
||||
def _target_params_are_valid(namespace, r_id, dimension):
|
||||
""" Check whether namespace, resource_id and dimension are valid and consistent with each other. """
|
||||
is_valid = True
|
||||
valid_namespaces = [n.value for n in ServiceNamespaceValueSet]
|
||||
if namespace not in valid_namespaces:
|
||||
is_valid = False
|
||||
if dimension is not None:
|
||||
try:
|
||||
valid_dimensions = [d.value for d in ScalableDimensionValueSet]
|
||||
d_namespace, d_resource_type, scaling_property = dimension.split(":")
|
||||
resource_type, cluster, service = r_id.split("/")
|
||||
if (
|
||||
dimension not in valid_dimensions
|
||||
or d_namespace != namespace
|
||||
or resource_type != d_resource_type
|
||||
):
|
||||
is_valid = False
|
||||
except ValueError:
|
||||
is_valid = False
|
||||
if not is_valid:
|
||||
raise AWSValidationException(
|
||||
"Unsupported service namespace, resource type or scalable dimension"
|
||||
)
|
||||
return is_valid
|
||||
|
||||
|
||||
class FakeScalableTarget(BaseModel):
|
||||
def __init__(
|
||||
self, backend, service_namespace, resource_id, scalable_dimension, **kwargs
|
||||
):
|
||||
self.applicationautoscaling_backend = backend
|
||||
self.service_namespace = service_namespace
|
||||
self.resource_id = resource_id
|
||||
self.scalable_dimension = scalable_dimension
|
||||
self.min_capacity = kwargs["min_capacity"]
|
||||
self.max_capacity = kwargs["max_capacity"]
|
||||
self.role_arn = kwargs["role_arn"]
|
||||
self.suspended_state = kwargs["suspended_state"]
|
||||
self.creation_time = time.time()
|
||||
|
||||
def update(self, **kwargs):
|
||||
if kwargs["min_capacity"] is not None:
|
||||
self.min_capacity = kwargs["min_capacity"]
|
||||
if kwargs["max_capacity"] is not None:
|
||||
self.max_capacity = kwargs["max_capacity"]
|
||||
|
||||
|
||||
applicationautoscaling_backends = {}
|
||||
for region_name, ecs_backend in ecs_backends.items():
|
||||
applicationautoscaling_backends[region_name] = ApplicationAutoscalingBackend(
|
||||
region_name, ecs_backend
|
||||
)
|
||||
97
moto/applicationautoscaling/responses.py
Normal file
97
moto/applicationautoscaling/responses.py
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
from __future__ import unicode_literals
|
||||
from moto.core.responses import BaseResponse
|
||||
import json
|
||||
from .models import (
|
||||
applicationautoscaling_backends,
|
||||
ScalableDimensionValueSet,
|
||||
ServiceNamespaceValueSet,
|
||||
)
|
||||
from .exceptions import AWSValidationException
|
||||
|
||||
|
||||
class ApplicationAutoScalingResponse(BaseResponse):
|
||||
@property
|
||||
def applicationautoscaling_backend(self):
|
||||
return applicationautoscaling_backends[self.region]
|
||||
|
||||
def describe_scalable_targets(self):
|
||||
try:
|
||||
self._validate_params()
|
||||
except AWSValidationException as e:
|
||||
return e.response()
|
||||
service_namespace = self._get_param("ServiceNamespace")
|
||||
resource_ids = self._get_param("ResourceIds")
|
||||
scalable_dimension = self._get_param("ScalableDimension")
|
||||
max_results = self._get_int_param("MaxResults", 50)
|
||||
marker = self._get_param("NextToken")
|
||||
all_scalable_targets = self.applicationautoscaling_backend.describe_scalable_targets(
|
||||
service_namespace, resource_ids, scalable_dimension
|
||||
)
|
||||
start = int(marker) + 1 if marker else 0
|
||||
next_token = None
|
||||
scalable_targets_resp = all_scalable_targets[start : start + max_results]
|
||||
if len(all_scalable_targets) > start + max_results:
|
||||
next_token = str(len(scalable_targets_resp) - 1)
|
||||
targets = [_build_target(t) for t in scalable_targets_resp]
|
||||
return json.dumps({"ScalableTargets": targets, "NextToken": next_token})
|
||||
|
||||
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()
|
||||
return json.dumps({})
|
||||
|
||||
def _validate_params(self):
|
||||
""" Validate parameters.
|
||||
TODO Integrate this validation with the validation in models.py
|
||||
"""
|
||||
namespace = self._get_param("ServiceNamespace")
|
||||
dimension = self._get_param("ScalableDimension")
|
||||
messages = []
|
||||
dimensions = [d.value for d in ScalableDimensionValueSet]
|
||||
message = None
|
||||
if dimension is not None and dimension not in dimensions:
|
||||
messages.append(
|
||||
"Value '{}' at 'scalableDimension' "
|
||||
"failed to satisfy constraint: Member must satisfy enum value set: "
|
||||
"{}".format(dimension, dimensions)
|
||||
)
|
||||
namespaces = [n.value for n in ServiceNamespaceValueSet]
|
||||
if namespace is not None and namespace not in namespaces:
|
||||
messages.append(
|
||||
"Value '{}' at 'serviceNamespace' "
|
||||
"failed to satisfy constraint: Member must satisfy enum value set: "
|
||||
"{}".format(namespace, namespaces)
|
||||
)
|
||||
if len(messages) == 1:
|
||||
message = "1 validation error detected: {}".format(messages[0])
|
||||
elif len(messages) > 1:
|
||||
message = "{} validation errors detected: {}".format(
|
||||
len(messages), "; ".join(messages)
|
||||
)
|
||||
if message:
|
||||
raise AWSValidationException(message)
|
||||
|
||||
|
||||
def _build_target(t):
|
||||
return {
|
||||
"CreationTime": t.creation_time,
|
||||
"ServiceNamespace": t.service_namespace,
|
||||
"ResourceId": t.resource_id,
|
||||
"RoleARN": t.role_arn,
|
||||
"ScalableDimension": t.scalable_dimension,
|
||||
"MaxCapacity": t.max_capacity,
|
||||
"MinCapacity": t.min_capacity,
|
||||
"SuspendedState": t.suspended_state,
|
||||
}
|
||||
8
moto/applicationautoscaling/urls.py
Normal file
8
moto/applicationautoscaling/urls.py
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
from __future__ import unicode_literals
|
||||
from .responses import ApplicationAutoScalingResponse
|
||||
|
||||
url_bases = ["https?://application-autoscaling.(.+).amazonaws.com"]
|
||||
|
||||
url_paths = {
|
||||
"{0}/$": ApplicationAutoScalingResponse.dispatch,
|
||||
}
|
||||
10
moto/applicationautoscaling/utils.py
Normal file
10
moto/applicationautoscaling/utils.py
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
from six.moves.urllib.parse import urlparse
|
||||
|
||||
|
||||
def region_from_applicationautoscaling_url(url):
|
||||
domain = urlparse(url).netloc
|
||||
|
||||
if "." in domain:
|
||||
return domain.split(".")[1]
|
||||
else:
|
||||
return "us-east-1"
|
||||
|
|
@ -6,6 +6,10 @@ BACKENDS = {
|
|||
"acm": ("acm", "acm_backends"),
|
||||
"apigateway": ("apigateway", "apigateway_backends"),
|
||||
"athena": ("athena", "athena_backends"),
|
||||
"applicationautoscaling": (
|
||||
"applicationautoscaling",
|
||||
"applicationautoscaling_backends",
|
||||
),
|
||||
"autoscaling": ("autoscaling", "autoscaling_backends"),
|
||||
"batch": ("batch", "batch_backends"),
|
||||
"cloudformation": ("cloudformation", "cloudformation_backends"),
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import six
|
|||
import types
|
||||
from io import BytesIO
|
||||
from collections import defaultdict
|
||||
from botocore.config import Config
|
||||
from botocore.handlers import BUILTIN_HANDLERS
|
||||
from botocore.awsrequest import AWSResponse
|
||||
from six.moves.urllib.parse import urlparse
|
||||
|
|
@ -416,6 +417,13 @@ class ServerModeMockAWS(BaseMockAWS):
|
|||
import mock
|
||||
|
||||
def fake_boto3_client(*args, **kwargs):
|
||||
region = self._get_region(*args, **kwargs)
|
||||
if region:
|
||||
if "config" in kwargs:
|
||||
kwargs["config"].__dict__["user_agent_extra"] += " region/" + region
|
||||
else:
|
||||
config = Config(user_agent_extra="region/" + region)
|
||||
kwargs["config"] = config
|
||||
if "endpoint_url" not in kwargs:
|
||||
kwargs["endpoint_url"] = "http://localhost:5000"
|
||||
return real_boto3_client(*args, **kwargs)
|
||||
|
|
@ -463,6 +471,14 @@ class ServerModeMockAWS(BaseMockAWS):
|
|||
if six.PY2:
|
||||
self._httplib_patcher.start()
|
||||
|
||||
def _get_region(self, *args, **kwargs):
|
||||
if "region_name" in kwargs:
|
||||
return kwargs["region_name"]
|
||||
if type(args) == tuple and len(args) == 2:
|
||||
service, region = args
|
||||
return region
|
||||
return None
|
||||
|
||||
def disable_patching(self):
|
||||
if self._client_patcher:
|
||||
self._client_patcher.stop()
|
||||
|
|
|
|||
|
|
@ -188,6 +188,9 @@ class BaseResponse(_TemplateEnvironmentMixin, ActionAuthenticatorMixin):
|
|||
default_region = "us-east-1"
|
||||
# to extract region, use [^.]
|
||||
region_regex = re.compile(r"\.(?P<region>[a-z]{2}-[a-z]+-\d{1})\.amazonaws\.com")
|
||||
region_from_useragent_regex = re.compile(
|
||||
r"region/(?P<region>[a-z]{2}-[a-z]+-\d{1})"
|
||||
)
|
||||
param_list_regex = re.compile(r"(.*)\.(\d+)\.")
|
||||
access_key_regex = re.compile(
|
||||
r"AWS.*(?P<access_key>(?<![A-Z0-9])[A-Z0-9]{20}(?![A-Z0-9]))[:/]"
|
||||
|
|
@ -272,9 +275,14 @@ class BaseResponse(_TemplateEnvironmentMixin, ActionAuthenticatorMixin):
|
|||
self.response_headers = {"server": "amazon.com"}
|
||||
|
||||
def get_region_from_url(self, request, full_url):
|
||||
match = self.region_regex.search(full_url)
|
||||
if match:
|
||||
region = match.group(1)
|
||||
url_match = self.region_regex.search(full_url)
|
||||
user_agent_match = self.region_from_useragent_regex.search(
|
||||
request.headers.get("User-Agent", "")
|
||||
)
|
||||
if url_match:
|
||||
region = url_match.group(1)
|
||||
elif user_agent_match:
|
||||
region = user_agent_match.group(1)
|
||||
elif (
|
||||
"Authorization" in request.headers
|
||||
and "AWS4" in request.headers["Authorization"]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue