diff --git a/.travis.yml b/.travis.yml index 8f218134b..ed9084f19 100644 --- a/.travis.yml +++ b/.travis.yml @@ -27,6 +27,7 @@ install: docker run --rm -t --name motoserver -e TEST_SERVER_MODE=true -e AWS_SECRET_ACCESS_KEY=server_secret -e AWS_ACCESS_KEY_ID=server_key -v `pwd`:/moto -p 5000:5000 -v /var/run/docker.sock:/var/run/docker.sock python:${PYTHON_DOCKER_TAG} /moto/travis_moto_server.sh & fi travis_retry pip install -r requirements-dev.txt + travis_retry pip install "docker>=2.5.1,<=4.2.2" # Limit version due to old Docker Engine in Travis https://github.com/docker/docker-py/issues/2639 travis_retry pip install boto==2.45.0 travis_retry pip install boto3 travis_retry pip install dist/moto*.gz diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md index d2696e6af..721c9c977 100644 --- a/IMPLEMENTATION_COVERAGE.md +++ b/IMPLEMENTATION_COVERAGE.md @@ -2639,7 +2639,7 @@ - [X] create_internet_gateway - [X] create_key_pair - [X] create_launch_template -- [ ] create_launch_template_version +- [x] create_launch_template_version - [ ] create_local_gateway_route - [ ] create_local_gateway_route_table_vpc_association - [X] create_nat_gateway @@ -5110,7 +5110,7 @@ - [ ] delete_alias - [X] delete_event_source_mapping - [X] delete_function -- [ ] delete_function_concurrency +- [X] delete_function_concurrency - [ ] delete_function_event_invoke_config - [ ] delete_layer_version - [ ] delete_provisioned_concurrency_config @@ -5118,7 +5118,7 @@ - [ ] get_alias - [X] get_event_source_mapping - [X] get_function -- [ ] get_function_concurrency +- [X] get_function_concurrency - [ ] get_function_configuration - [ ] get_function_event_invoke_config - [ ] get_layer_version @@ -5139,7 +5139,7 @@ - [X] list_versions_by_function - [ ] publish_layer_version - [ ] publish_version -- [ ] put_function_concurrency +- [X] put_function_concurrency - [ ] put_function_event_invoke_config - [ ] put_provisioned_concurrency_config - [ ] remove_layer_version_permission diff --git a/moto/autoscaling/exceptions.py b/moto/autoscaling/exceptions.py index 6f73eff8f..2fddd18ec 100644 --- a/moto/autoscaling/exceptions.py +++ b/moto/autoscaling/exceptions.py @@ -21,3 +21,8 @@ class InvalidInstanceError(AutoscalingClientError): super(InvalidInstanceError, self).__init__( "ValidationError", "Instance [{0}] is invalid.".format(instance_id) ) + + +class ValidationError(AutoscalingClientError): + def __init__(self, message): + super(ValidationError, self).__init__("ValidationError", message) diff --git a/moto/autoscaling/models.py b/moto/autoscaling/models.py index d82f15095..1a25a656d 100644 --- a/moto/autoscaling/models.py +++ b/moto/autoscaling/models.py @@ -7,6 +7,7 @@ from moto.ec2.exceptions import InvalidInstanceIdError from moto.compat import OrderedDict from moto.core import BaseBackend, BaseModel, CloudFormationModel +from moto.core.utils import camelcase_to_underscores from moto.ec2 import ec2_backends from moto.elb import elb_backends from moto.elbv2 import elbv2_backends @@ -15,6 +16,7 @@ from .exceptions import ( AutoscalingClientError, ResourceContentionError, InvalidInstanceError, + ValidationError, ) # http://docs.aws.amazon.com/AutoScaling/latest/DeveloperGuide/AS_Concepts.html#Cooldown @@ -233,6 +235,7 @@ class FakeAutoScalingGroup(CloudFormationModel): max_size, min_size, launch_config_name, + launch_template, vpc_zone_identifier, default_cooldown, health_check_period, @@ -242,10 +245,12 @@ class FakeAutoScalingGroup(CloudFormationModel): placement_group, termination_policies, autoscaling_backend, + ec2_backend, tags, new_instances_protected_from_scale_in=False, ): self.autoscaling_backend = autoscaling_backend + self.ec2_backend = ec2_backend self.name = name self._set_azs_and_vpcs(availability_zones, vpc_zone_identifier) @@ -253,10 +258,10 @@ class FakeAutoScalingGroup(CloudFormationModel): self.max_size = max_size self.min_size = min_size - self.launch_config = self.autoscaling_backend.launch_configurations[ - launch_config_name - ] - self.launch_config_name = launch_config_name + self.launch_template = None + self.launch_config = None + + self._set_launch_configuration(launch_config_name, launch_template) self.default_cooldown = ( default_cooldown if default_cooldown else DEFAULT_COOLDOWN @@ -310,6 +315,34 @@ class FakeAutoScalingGroup(CloudFormationModel): self.availability_zones = availability_zones self.vpc_zone_identifier = vpc_zone_identifier + def _set_launch_configuration(self, launch_config_name, launch_template): + if launch_config_name: + self.launch_config = self.autoscaling_backend.launch_configurations[ + launch_config_name + ] + self.launch_config_name = launch_config_name + + if launch_template: + launch_template_id = launch_template.get("launch_template_id") + launch_template_name = launch_template.get("launch_template_name") + + if not (launch_template_id or launch_template_name) or ( + launch_template_id and launch_template_name + ): + raise ValidationError( + "Valid requests must contain either launchTemplateId or LaunchTemplateName" + ) + + if launch_template_id: + self.launch_template = self.ec2_backend.get_launch_template( + launch_template_id + ) + elif launch_template_name: + self.launch_template = self.ec2_backend.get_launch_template_by_name( + launch_template_name + ) + self.launch_template_version = int(launch_template["version"]) + @staticmethod def __set_string_propagate_at_launch_booleans_on_tags(tags): bool_to_string = {True: "true", False: "false"} @@ -334,6 +367,10 @@ class FakeAutoScalingGroup(CloudFormationModel): properties = cloudformation_json["Properties"] launch_config_name = properties.get("LaunchConfigurationName") + launch_template = { + camelcase_to_underscores(k): v + for k, v in properties.get("LaunchTemplate", {}).items() + } load_balancer_names = properties.get("LoadBalancerNames", []) target_group_arns = properties.get("TargetGroupARNs", []) @@ -345,6 +382,7 @@ class FakeAutoScalingGroup(CloudFormationModel): max_size=properties.get("MaxSize"), min_size=properties.get("MinSize"), launch_config_name=launch_config_name, + launch_template=launch_template, vpc_zone_identifier=( ",".join(properties.get("VPCZoneIdentifier", [])) or None ), @@ -393,6 +431,38 @@ class FakeAutoScalingGroup(CloudFormationModel): def physical_resource_id(self): return self.name + @property + def image_id(self): + if self.launch_template: + version = self.launch_template.get_version(self.launch_template_version) + return version.image_id + + return self.launch_config.image_id + + @property + def instance_type(self): + if self.launch_template: + version = self.launch_template.get_version(self.launch_template_version) + return version.instance_type + + return self.launch_config.instance_type + + @property + def user_data(self): + if self.launch_template: + version = self.launch_template.get_version(self.launch_template_version) + return version.user_data + + return self.launch_config.user_data + + @property + def security_groups(self): + if self.launch_template: + version = self.launch_template.get_version(self.launch_template_version) + return version.security_groups + + return self.launch_config.security_groups + def update( self, availability_zones, @@ -400,6 +470,7 @@ class FakeAutoScalingGroup(CloudFormationModel): max_size, min_size, launch_config_name, + launch_template, vpc_zone_identifier, default_cooldown, health_check_period, @@ -421,11 +492,8 @@ class FakeAutoScalingGroup(CloudFormationModel): if max_size is not None and max_size < len(self.instance_states): desired_capacity = max_size - if launch_config_name: - self.launch_config = self.autoscaling_backend.launch_configurations[ - launch_config_name - ] - self.launch_config_name = launch_config_name + self._set_launch_configuration(launch_config_name, launch_template) + if health_check_period is not None: self.health_check_period = health_check_period if health_check_type is not None: @@ -489,12 +557,13 @@ class FakeAutoScalingGroup(CloudFormationModel): def replace_autoscaling_group_instances(self, count_needed, propagated_tags): propagated_tags[ASG_NAME_TAG] = self.name + reservation = self.autoscaling_backend.ec2_backend.add_instances( - self.launch_config.image_id, + self.image_id, count_needed, - self.launch_config.user_data, - self.launch_config.security_groups, - instance_type=self.launch_config.instance_type, + self.user_data, + self.security_groups, + instance_type=self.instance_type, tags={"instance": propagated_tags}, placement=random.choice(self.availability_zones), ) @@ -586,6 +655,7 @@ class AutoScalingBackend(BaseBackend): max_size, min_size, launch_config_name, + launch_template, vpc_zone_identifier, default_cooldown, health_check_period, @@ -609,7 +679,19 @@ class AutoScalingBackend(BaseBackend): health_check_period = 300 else: health_check_period = make_int(health_check_period) - if launch_config_name is None and instance_id is not None: + + # TODO: Add MixedInstancesPolicy once implemented. + # Verify only a single launch config-like parameter is provided. + params = [launch_config_name, launch_template, instance_id] + num_params = sum([1 for param in params if param]) + + if num_params != 1: + raise ValidationError( + "Valid requests must contain either LaunchTemplate, LaunchConfigurationName, " + "InstanceId or MixedInstancesPolicy parameter." + ) + + if instance_id: try: instance = self.ec2_backend.get_instance(instance_id) launch_config_name = name @@ -626,6 +708,7 @@ class AutoScalingBackend(BaseBackend): max_size=max_size, min_size=min_size, launch_config_name=launch_config_name, + launch_template=launch_template, vpc_zone_identifier=vpc_zone_identifier, default_cooldown=default_cooldown, health_check_period=health_check_period, @@ -635,6 +718,7 @@ class AutoScalingBackend(BaseBackend): placement_group=placement_group, termination_policies=termination_policies, autoscaling_backend=self, + ec2_backend=self.ec2_backend, tags=tags, new_instances_protected_from_scale_in=new_instances_protected_from_scale_in, ) @@ -652,6 +736,7 @@ class AutoScalingBackend(BaseBackend): max_size, min_size, launch_config_name, + launch_template, vpc_zone_identifier, default_cooldown, health_check_period, @@ -660,19 +745,28 @@ class AutoScalingBackend(BaseBackend): termination_policies, new_instances_protected_from_scale_in=None, ): + # TODO: Add MixedInstancesPolicy once implemented. + # Verify only a single launch config-like parameter is provided. + if launch_config_name and launch_template: + raise ValidationError( + "Valid requests must contain either LaunchTemplate, LaunchConfigurationName " + "or MixedInstancesPolicy parameter." + ) + group = self.autoscaling_groups[name] group.update( - availability_zones, - desired_capacity, - max_size, - min_size, - launch_config_name, - vpc_zone_identifier, - default_cooldown, - health_check_period, - health_check_type, - placement_group, - termination_policies, + availability_zones=availability_zones, + desired_capacity=desired_capacity, + max_size=max_size, + min_size=min_size, + launch_config_name=launch_config_name, + launch_template=launch_template, + vpc_zone_identifier=vpc_zone_identifier, + default_cooldown=default_cooldown, + health_check_period=health_check_period, + health_check_type=health_check_type, + placement_group=placement_group, + termination_policies=termination_policies, new_instances_protected_from_scale_in=new_instances_protected_from_scale_in, ) return group diff --git a/moto/autoscaling/responses.py b/moto/autoscaling/responses.py index 06b68aa4b..a9651a774 100644 --- a/moto/autoscaling/responses.py +++ b/moto/autoscaling/responses.py @@ -81,6 +81,7 @@ class AutoScalingResponse(BaseResponse): min_size=self._get_int_param("MinSize"), instance_id=self._get_param("InstanceId"), launch_config_name=self._get_param("LaunchConfigurationName"), + launch_template=self._get_dict_param("LaunchTemplate."), vpc_zone_identifier=self._get_param("VPCZoneIdentifier"), default_cooldown=self._get_int_param("DefaultCooldown"), health_check_period=self._get_int_param("HealthCheckGracePeriod"), @@ -197,6 +198,7 @@ class AutoScalingResponse(BaseResponse): max_size=self._get_int_param("MaxSize"), min_size=self._get_int_param("MinSize"), launch_config_name=self._get_param("LaunchConfigurationName"), + launch_template=self._get_dict_param("LaunchTemplate."), vpc_zone_identifier=self._get_param("VPCZoneIdentifier"), default_cooldown=self._get_int_param("DefaultCooldown"), health_check_period=self._get_int_param("HealthCheckGracePeriod"), @@ -573,14 +575,31 @@ DESCRIBE_AUTOSCALING_GROUPS_TEMPLATE = """{{ group.health_check_type }} 2013-05-06T17:47:15.107Z + {% if group.launch_config_name %} {{ group.launch_config_name }} + {% elif group.launch_template %} + + {{ group.launch_template.id }} + {{ group.launch_template_version }} + {{ group.launch_template.name }} + + {% endif %} {% for instance_state in group.instance_states %} {{ instance_state.health_status }} {{ instance_state.instance.placement }} {{ instance_state.instance.id }} + {{ instance_state.instance.instance_type }} + {% if group.launch_config_name %} {{ group.launch_config_name }} + {% elif group.launch_template %} + + {{ group.launch_template.id }} + {{ group.launch_template_version }} + {{ group.launch_template.name }} + + {% endif %} {{ instance_state.lifecycle_state }} {{ instance_state.protected_from_scale_in|string|lower }} @@ -666,7 +685,16 @@ DESCRIBE_AUTOSCALING_INSTANCES_TEMPLATE = """{{ instance_state.instance.autoscaling_group.name }} {{ instance_state.instance.placement }} {{ instance_state.instance.id }} + {{ instance_state.instance.instance_type }} + {% if instance_state.instance.autoscaling_group.launch_config_name %} {{ instance_state.instance.autoscaling_group.launch_config_name }} + {% elif instance_state.instance.autoscaling_group.launch_template %} + + {{ instance_state.instance.autoscaling_group.launch_template.id }} + {{ instance_state.instance.autoscaling_group.launch_template_version }} + {{ instance_state.instance.autoscaling_group.launch_template.name }} + + {% endif %} {{ instance_state.lifecycle_state }} {{ instance_state.protected_from_scale_in|string|lower }} diff --git a/moto/awslambda/models.py b/moto/awslambda/models.py index a234fbe01..2aa207da9 100644 --- a/moto/awslambda/models.py +++ b/moto/awslambda/models.py @@ -165,6 +165,7 @@ class LambdaFunction(CloudFormationModel): self.docker_client = docker.from_env() self.policy = None self.state = "Active" + self.reserved_concurrency = spec.get("ReservedConcurrentExecutions", None) # Unfortunately mocking replaces this method w/o fallback enabled, so we # need to replace it if we detect it's been mocked @@ -285,7 +286,7 @@ class LambdaFunction(CloudFormationModel): return config def get_code(self): - return { + code = { "Code": { "Location": "s3://awslambda-{0}-tasks.s3-{0}.amazonaws.com/{1}".format( self.region, self.code["S3Key"] @@ -294,6 +295,15 @@ class LambdaFunction(CloudFormationModel): }, "Configuration": self.get_configuration(), } + if self.reserved_concurrency: + code.update( + { + "Concurrency": { + "ReservedConcurrentExecutions": self.reserved_concurrency + } + } + ) + return code def update_configuration(self, config_updates): for key, value in config_updates.items(): @@ -388,11 +398,16 @@ class LambdaFunction(CloudFormationModel): # also need to hook it up to the other services so it can make kws/s3 etc calls # Should get invoke_id /RequestId from invocation env_vars = { + "_HANDLER": self.handler, + "AWS_EXECUTION_ENV": "AWS_Lambda_{}".format(self.run_time), "AWS_LAMBDA_FUNCTION_TIMEOUT": self.timeout, "AWS_LAMBDA_FUNCTION_NAME": self.function_name, "AWS_LAMBDA_FUNCTION_MEMORY_SIZE": self.memory_size, "AWS_LAMBDA_FUNCTION_VERSION": self.version, "AWS_REGION": self.region, + "AWS_ACCESS_KEY_ID": "role-account-id", + "AWS_SECRET_ACCESS_KEY": "role-secret-key", + "AWS_SESSION_TOKEN": "session-token", } env_vars.update(self.environment_vars) @@ -506,6 +521,15 @@ class LambdaFunction(CloudFormationModel): cls, resource_name, cloudformation_json, region_name ): properties = cloudformation_json["Properties"] + optional_properties = ( + "Description", + "MemorySize", + "Publish", + "Timeout", + "VpcConfig", + "Environment", + "ReservedConcurrentExecutions", + ) # required spec = { @@ -515,9 +539,7 @@ class LambdaFunction(CloudFormationModel): "Role": properties["Role"], "Runtime": properties["Runtime"], } - optional_properties = ( - "Description MemorySize Publish Timeout VpcConfig Environment".split() - ) + # NOTE: Not doing `properties.get(k, DEFAULT)` to avoid duplicating the # default logic for prop in optional_properties: @@ -1152,6 +1174,20 @@ class LambdaBackend(BaseBackend): else: return None + def put_function_concurrency(self, function_name, reserved_concurrency): + fn = self.get_function(function_name) + fn.reserved_concurrency = reserved_concurrency + return fn.reserved_concurrency + + def delete_function_concurrency(self, function_name): + fn = self.get_function(function_name) + fn.reserved_concurrency = None + return fn.reserved_concurrency + + def get_function_concurrency(self, function_name): + fn = self.get_function(function_name) + return fn.reserved_concurrency + def do_validate_s3(): return os.environ.get("VALIDATE_LAMBDA_S3", "") in ["", "1", "true"] diff --git a/moto/awslambda/responses.py b/moto/awslambda/responses.py index a4f559fc2..6447cde13 100644 --- a/moto/awslambda/responses.py +++ b/moto/awslambda/responses.py @@ -141,6 +141,19 @@ class LambdaResponse(BaseResponse): else: raise ValueError("Cannot handle request") + def function_concurrency(self, request, full_url, headers): + http_method = request.method + self.setup_class(request, full_url, headers) + + if http_method == "GET": + return self._get_function_concurrency(request) + elif http_method == "DELETE": + return self._delete_function_concurrency(request) + elif http_method == "PUT": + return self._put_function_concurrency(request) + else: + raise ValueError("Cannot handle request") + def _add_policy(self, request, full_url, headers): path = request.path if hasattr(request, "path") else path_url(request.url) function_name = path.split("/")[-2] @@ -359,3 +372,38 @@ class LambdaResponse(BaseResponse): return 200, {}, json.dumps(resp) else: return 404, {}, "{}" + + def _get_function_concurrency(self, request): + path_function_name = self.path.rsplit("/", 2)[-2] + function_name = self.lambda_backend.get_function(path_function_name) + + if function_name is None: + return 404, {}, "{}" + + resp = self.lambda_backend.get_function_concurrency(path_function_name) + return 200, {}, json.dumps({"ReservedConcurrentExecutions": resp}) + + def _delete_function_concurrency(self, request): + path_function_name = self.path.rsplit("/", 2)[-2] + function_name = self.lambda_backend.get_function(path_function_name) + + if function_name is None: + return 404, {}, "{}" + + self.lambda_backend.delete_function_concurrency(path_function_name) + + return 204, {}, "{}" + + def _put_function_concurrency(self, request): + path_function_name = self.path.rsplit("/", 2)[-2] + function = self.lambda_backend.get_function(path_function_name) + + if function is None: + return 404, {}, "{}" + + concurrency = self._get_param("ReservedConcurrentExecutions", None) + resp = self.lambda_backend.put_function_concurrency( + path_function_name, concurrency + ) + + return 200, {}, json.dumps({"ReservedConcurrentExecutions": resp}) diff --git a/moto/awslambda/urls.py b/moto/awslambda/urls.py index c25e58dba..03cedc5e4 100644 --- a/moto/awslambda/urls.py +++ b/moto/awslambda/urls.py @@ -19,4 +19,5 @@ url_paths = { r"{0}/(?P[^/]+)/functions/(?P[\w_-]+)/policy/?$": response.policy, r"{0}/(?P[^/]+)/functions/(?P[\w_-]+)/configuration/?$": response.configuration, r"{0}/(?P[^/]+)/functions/(?P[\w_-]+)/code/?$": response.code, + r"{0}/(?P[^/]+)/functions/(?P[\w_-]+)/concurrency/?$": response.function_concurrency, } diff --git a/moto/core/responses.py b/moto/core/responses.py index 676d7549d..fdac22c18 100644 --- a/moto/core/responses.py +++ b/moto/core/responses.py @@ -538,8 +538,8 @@ class BaseResponse(_TemplateEnvironmentMixin, ActionAuthenticatorMixin): returns { - "SlaveInstanceType": "m1.small", - "InstanceCount": "1", + "slave_instance_type": "m1.small", + "instance_count": "1", } """ params = {} diff --git a/moto/ec2/models.py b/moto/ec2/models.py index 63ebd1738..f0ce89d8a 100644 --- a/moto/ec2/models.py +++ b/moto/ec2/models.py @@ -1866,7 +1866,9 @@ class SecurityGroup(TaggedEC2Resource, CloudFormationModel): self.name = name self.description = description self.ingress_rules = [] - self.egress_rules = [SecurityRule("-1", None, None, ["0.0.0.0/0"], [])] + self.egress_rules = [ + SecurityRule("-1", None, None, [{"CidrIp": "0.0.0.0/0"}], []) + ] self.enis = {} self.vpc_id = vpc_id self.owner_id = OWNER_ID @@ -2266,13 +2268,16 @@ class SecurityGroupBackend(object): if source_group: source_groups.append(source_group) - for ip in ip_ranges: - ip_ranges = [ip.get("CidrIp") if ip.get("CidrIp") == "0.0.0.0/0" else ip] + # I don't believe this is required after changing the default egress rule + # to be {'CidrIp': '0.0.0.0/0'} instead of just '0.0.0.0/0' + # Not sure why this would return only the IP if it was 0.0.0.0/0 instead of + # the ip_range? + # for ip in ip_ranges: + # ip_ranges = [ip.get("CidrIp") if ip.get("CidrIp") == "0.0.0.0/0" else ip] security_rule = SecurityRule( ip_protocol, from_port, to_port, ip_ranges, source_groups ) - if security_rule in group.egress_rules: group.egress_rules.remove(security_rule) return security_rule @@ -5381,6 +5386,22 @@ class LaunchTemplateVersion(object): self.description = description self.create_time = utc_date_and_time() + @property + def image_id(self): + return self.data.get("ImageId", "") + + @property + def instance_type(self): + return self.data.get("InstanceType", "") + + @property + def security_groups(self): + return self.data.get("SecurityGroups", []) + + @property + def user_data(self): + return self.data.get("UserData", "") + class LaunchTemplate(TaggedEC2Resource): def __init__(self, backend, name, template_data, version_description): diff --git a/moto/ec2/responses/security_groups.py b/moto/ec2/responses/security_groups.py index af84b7738..5c0d1c852 100644 --- a/moto/ec2/responses/security_groups.py +++ b/moto/ec2/responses/security_groups.py @@ -250,7 +250,10 @@ DESCRIBE_SECURITY_GROUPS_RESPONSE = ( {% for ip_range in rule.ip_ranges %} - {{ ip_range }} + {{ ip_range['CidrIp'] }} + {% if ip_range['Description'] %} + {{ ip_range['Description'] }} + {% endif %} {% endfor %} diff --git a/moto/ses/models.py b/moto/ses/models.py index e90f66fa8..d9a44a370 100644 --- a/moto/ses/models.py +++ b/moto/ses/models.py @@ -103,6 +103,8 @@ class SESBackend(BaseBackend): _, address = parseaddr(source) if address in self.addresses: return True + if address in self.email_addresses: + return True user, host = address.split("@", 1) return host in self.domains @@ -202,7 +204,7 @@ class SESBackend(BaseBackend): if sns_topic is not None: message = self.__generate_feedback__(msg_type) if message: - sns_backends[region].publish(sns_topic, message) + sns_backends[region].publish(message, arn=sns_topic) def send_raw_email(self, source, destinations, raw_data, region): if source is not None: diff --git a/moto/sns/models.py b/moto/sns/models.py index 8a4771a37..779a0fb06 100644 --- a/moto/sns/models.py +++ b/moto/sns/models.py @@ -35,6 +35,7 @@ from moto.core import ACCOUNT_ID as DEFAULT_ACCOUNT_ID DEFAULT_PAGE_SIZE = 100 MAXIMUM_MESSAGE_LENGTH = 262144 # 256 KiB +MAXIMUM_SMS_MESSAGE_BYTES = 1600 # Amazon limit for a single publish SMS action class Topic(CloudFormationModel): @@ -365,6 +366,7 @@ class SNSBackend(BaseBackend): self.platform_endpoints = {} self.region_name = region_name self.sms_attributes = {} + self.sms_messages = OrderedDict() self.opt_out_numbers = [ "+447420500600", "+447420505401", @@ -432,12 +434,6 @@ class SNSBackend(BaseBackend): except KeyError: raise SNSNotFoundError("Topic with arn {0} not found".format(arn)) - def get_topic_from_phone_number(self, number): - for subscription in self.subscriptions.values(): - if subscription.protocol == "sms" and subscription.endpoint == number: - return subscription.topic.arn - raise SNSNotFoundError("Could not find valid subscription") - def set_topic_attribute(self, topic_arn, attribute_name, attribute_value): topic = self.get_topic(topic_arn) setattr(topic, attribute_name, attribute_value) @@ -501,11 +497,27 @@ class SNSBackend(BaseBackend): else: return self._get_values_nexttoken(self.subscriptions, next_token) - def publish(self, arn, message, subject=None, message_attributes=None): + def publish( + self, + message, + arn=None, + phone_number=None, + subject=None, + message_attributes=None, + ): if subject is not None and len(subject) > 100: # Note that the AWS docs around length are wrong: https://github.com/spulec/moto/issues/1503 raise ValueError("Subject must be less than 100 characters") + if phone_number: + # This is only an approximation. In fact, we should try to use GSM-7 or UCS-2 encoding to count used bytes + if len(message) > MAXIMUM_SMS_MESSAGE_BYTES: + raise ValueError("SMS message must be less than 1600 bytes") + + message_id = six.text_type(uuid.uuid4()) + self.sms_messages[message_id] = (phone_number, message) + return message_id + if len(message) > MAXIMUM_MESSAGE_LENGTH: raise InvalidParameterValue( "An error occurred (InvalidParameter) when calling the Publish operation: Invalid parameter: Message too long" diff --git a/moto/sns/responses.py b/moto/sns/responses.py index c2eb3e7c3..7fdc37ab6 100644 --- a/moto/sns/responses.py +++ b/moto/sns/responses.py @@ -6,7 +6,7 @@ from collections import defaultdict from moto.core.responses import BaseResponse from moto.core.utils import camelcase_to_underscores from .models import sns_backends -from .exceptions import SNSNotFoundError, InvalidParameterValue +from .exceptions import InvalidParameterValue from .utils import is_e164 @@ -327,6 +327,7 @@ class SNSResponse(BaseResponse): message_attributes = self._parse_message_attributes() + arn = None if phone_number is not None: # Check phone is correct syntax (e164) if not is_e164(phone_number): @@ -336,18 +337,6 @@ class SNSResponse(BaseResponse): ), dict(status=400), ) - - # Look up topic arn by phone number - try: - arn = self.backend.get_topic_from_phone_number(phone_number) - except SNSNotFoundError: - return ( - self._error( - "ParameterValueInvalid", - "Could not find topic associated with phone number", - ), - dict(status=400), - ) elif target_arn is not None: arn = target_arn else: @@ -357,7 +346,11 @@ class SNSResponse(BaseResponse): try: message_id = self.backend.publish( - arn, message, subject=subject, message_attributes=message_attributes + message, + arn=arn, + phone_number=phone_number, + subject=subject, + message_attributes=message_attributes, ) except ValueError as err: error_response = self._error("InvalidParameter", str(err)) diff --git a/tests/test_autoscaling/test_autoscaling.py b/tests/test_autoscaling/test_autoscaling.py index 93a8c5a48..1e7121381 100644 --- a/tests/test_autoscaling/test_autoscaling.py +++ b/tests/test_autoscaling/test_autoscaling.py @@ -17,6 +17,7 @@ from moto import ( mock_elb, mock_autoscaling_deprecated, mock_ec2, + mock_cloudformation, ) from tests.helpers import requires_boto_gte @@ -164,7 +165,7 @@ def test_list_many_autoscaling_groups(): @mock_autoscaling @mock_ec2 -def test_list_many_autoscaling_groups(): +def test_propogate_tags(): mocked_networking = setup_networking() conn = boto3.client("autoscaling", region_name="us-east-1") conn.create_launch_configuration(LaunchConfigurationName="TestLC") @@ -692,7 +693,7 @@ def test_detach_load_balancer(): def test_create_autoscaling_group_boto3(): mocked_networking = setup_networking() client = boto3.client("autoscaling", region_name="us-east-1") - _ = client.create_launch_configuration( + client.create_launch_configuration( LaunchConfigurationName="test_launch_configuration" ) response = client.create_auto_scaling_group( @@ -798,13 +799,171 @@ def test_create_autoscaling_group_from_invalid_instance_id(): @mock_autoscaling -def test_describe_autoscaling_groups_boto3(): +@mock_ec2 +def test_create_autoscaling_group_from_template(): + mocked_networking = setup_networking() + + ec2_client = boto3.client("ec2", region_name="us-east-1") + template = ec2_client.create_launch_template( + LaunchTemplateName="test_launch_template", + LaunchTemplateData={ + "ImageId": "ami-0cc293023f983ed53", + "InstanceType": "t2.micro", + }, + )["LaunchTemplate"] + client = boto3.client("autoscaling", region_name="us-east-1") + response = client.create_auto_scaling_group( + AutoScalingGroupName="test_asg", + LaunchTemplate={ + "LaunchTemplateId": template["LaunchTemplateId"], + "Version": str(template["LatestVersionNumber"]), + }, + MinSize=1, + MaxSize=3, + DesiredCapacity=2, + VPCZoneIdentifier=mocked_networking["subnet1"], + NewInstancesProtectedFromScaleIn=False, + ) + response["ResponseMetadata"]["HTTPStatusCode"].should.equal(200) + + +@mock_autoscaling +@mock_ec2 +def test_create_autoscaling_group_no_template_ref(): + mocked_networking = setup_networking() + + ec2_client = boto3.client("ec2", region_name="us-east-1") + template = ec2_client.create_launch_template( + LaunchTemplateName="test_launch_template", + LaunchTemplateData={ + "ImageId": "ami-0cc293023f983ed53", + "InstanceType": "t2.micro", + }, + )["LaunchTemplate"] + client = boto3.client("autoscaling", region_name="us-east-1") + + with assert_raises(ClientError) as ex: + client.create_auto_scaling_group( + AutoScalingGroupName="test_asg", + LaunchTemplate={"Version": str(template["LatestVersionNumber"])}, + MinSize=0, + MaxSize=20, + DesiredCapacity=5, + VPCZoneIdentifier=mocked_networking["subnet1"], + NewInstancesProtectedFromScaleIn=False, + ) + ex.exception.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400) + ex.exception.response["Error"]["Code"].should.equal("ValidationError") + ex.exception.response["Error"]["Message"].should.equal( + "Valid requests must contain either launchTemplateId or LaunchTemplateName" + ) + + +@mock_autoscaling +@mock_ec2 +def test_create_autoscaling_group_multiple_template_ref(): + mocked_networking = setup_networking() + + ec2_client = boto3.client("ec2", region_name="us-east-1") + template = ec2_client.create_launch_template( + LaunchTemplateName="test_launch_template", + LaunchTemplateData={ + "ImageId": "ami-0cc293023f983ed53", + "InstanceType": "t2.micro", + }, + )["LaunchTemplate"] + client = boto3.client("autoscaling", region_name="us-east-1") + + with assert_raises(ClientError) as ex: + client.create_auto_scaling_group( + AutoScalingGroupName="test_asg", + LaunchTemplate={ + "LaunchTemplateId": template["LaunchTemplateId"], + "LaunchTemplateName": template["LaunchTemplateName"], + "Version": str(template["LatestVersionNumber"]), + }, + MinSize=0, + MaxSize=20, + DesiredCapacity=5, + VPCZoneIdentifier=mocked_networking["subnet1"], + NewInstancesProtectedFromScaleIn=False, + ) + ex.exception.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400) + ex.exception.response["Error"]["Code"].should.equal("ValidationError") + ex.exception.response["Error"]["Message"].should.equal( + "Valid requests must contain either launchTemplateId or LaunchTemplateName" + ) + + +@mock_autoscaling +def test_create_autoscaling_group_boto3_no_launch_configuration(): mocked_networking = setup_networking() client = boto3.client("autoscaling", region_name="us-east-1") - _ = client.create_launch_configuration( + with assert_raises(ClientError) as ex: + client.create_auto_scaling_group( + AutoScalingGroupName="test_asg", + MinSize=0, + MaxSize=20, + DesiredCapacity=5, + VPCZoneIdentifier=mocked_networking["subnet1"], + NewInstancesProtectedFromScaleIn=False, + ) + ex.exception.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400) + ex.exception.response["Error"]["Code"].should.equal("ValidationError") + ex.exception.response["Error"]["Message"].should.equal( + "Valid requests must contain either LaunchTemplate, LaunchConfigurationName, " + "InstanceId or MixedInstancesPolicy parameter." + ) + + +@mock_autoscaling +@mock_ec2 +def test_create_autoscaling_group_boto3_multiple_launch_configurations(): + mocked_networking = setup_networking() + + ec2_client = boto3.client("ec2", region_name="us-east-1") + template = ec2_client.create_launch_template( + LaunchTemplateName="test_launch_template", + LaunchTemplateData={ + "ImageId": "ami-0cc293023f983ed53", + "InstanceType": "t2.micro", + }, + )["LaunchTemplate"] + client = boto3.client("autoscaling", region_name="us-east-1") + client.create_launch_configuration( LaunchConfigurationName="test_launch_configuration" ) - _ = client.create_auto_scaling_group( + + with assert_raises(ClientError) as ex: + client.create_auto_scaling_group( + AutoScalingGroupName="test_asg", + LaunchConfigurationName="test_launch_configuration", + LaunchTemplate={ + "LaunchTemplateId": template["LaunchTemplateId"], + "Version": str(template["LatestVersionNumber"]), + }, + MinSize=0, + MaxSize=20, + DesiredCapacity=5, + VPCZoneIdentifier=mocked_networking["subnet1"], + NewInstancesProtectedFromScaleIn=False, + ) + ex.exception.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400) + ex.exception.response["Error"]["Code"].should.equal("ValidationError") + ex.exception.response["Error"]["Message"].should.equal( + "Valid requests must contain either LaunchTemplate, LaunchConfigurationName, " + "InstanceId or MixedInstancesPolicy parameter." + ) + + +@mock_autoscaling +def test_describe_autoscaling_groups_boto3_launch_config(): + mocked_networking = setup_networking() + client = boto3.client("autoscaling", region_name="us-east-1") + client.create_launch_configuration( + LaunchConfigurationName="test_launch_configuration", InstanceType="t2.micro", + ) + client.create_auto_scaling_group( AutoScalingGroupName="test_asg", LaunchConfigurationName="test_launch_configuration", MinSize=0, @@ -818,22 +977,72 @@ def test_describe_autoscaling_groups_boto3(): response["ResponseMetadata"]["HTTPStatusCode"].should.equal(200) group = response["AutoScalingGroups"][0] group["AutoScalingGroupName"].should.equal("test_asg") + group["LaunchConfigurationName"].should.equal("test_launch_configuration") + group.should_not.have.key("LaunchTemplate") group["AvailabilityZones"].should.equal(["us-east-1a"]) group["VPCZoneIdentifier"].should.equal(mocked_networking["subnet1"]) group["NewInstancesProtectedFromScaleIn"].should.equal(True) for instance in group["Instances"]: + instance["LaunchConfigurationName"].should.equal("test_launch_configuration") + instance.should_not.have.key("LaunchTemplate") instance["AvailabilityZone"].should.equal("us-east-1a") instance["ProtectedFromScaleIn"].should.equal(True) + instance["InstanceType"].should.equal("t2.micro") @mock_autoscaling -def test_describe_autoscaling_instances_boto3(): +@mock_ec2 +def test_describe_autoscaling_groups_boto3_launch_template(): + mocked_networking = setup_networking() + ec2_client = boto3.client("ec2", region_name="us-east-1") + template = ec2_client.create_launch_template( + LaunchTemplateName="test_launch_template", + LaunchTemplateData={ + "ImageId": "ami-0cc293023f983ed53", + "InstanceType": "t2.micro", + }, + )["LaunchTemplate"] + client = boto3.client("autoscaling", region_name="us-east-1") + client.create_auto_scaling_group( + AutoScalingGroupName="test_asg", + LaunchTemplate={"LaunchTemplateName": "test_launch_template", "Version": "1"}, + MinSize=0, + MaxSize=20, + DesiredCapacity=5, + VPCZoneIdentifier=mocked_networking["subnet1"], + NewInstancesProtectedFromScaleIn=True, + ) + expected_launch_template = { + "LaunchTemplateId": template["LaunchTemplateId"], + "LaunchTemplateName": "test_launch_template", + "Version": "1", + } + + response = client.describe_auto_scaling_groups(AutoScalingGroupNames=["test_asg"]) + response["ResponseMetadata"]["HTTPStatusCode"].should.equal(200) + group = response["AutoScalingGroups"][0] + group["AutoScalingGroupName"].should.equal("test_asg") + group["LaunchTemplate"].should.equal(expected_launch_template) + group.should_not.have.key("LaunchConfigurationName") + group["AvailabilityZones"].should.equal(["us-east-1a"]) + group["VPCZoneIdentifier"].should.equal(mocked_networking["subnet1"]) + group["NewInstancesProtectedFromScaleIn"].should.equal(True) + for instance in group["Instances"]: + instance["LaunchTemplate"].should.equal(expected_launch_template) + instance.should_not.have.key("LaunchConfigurationName") + instance["AvailabilityZone"].should.equal("us-east-1a") + instance["ProtectedFromScaleIn"].should.equal(True) + instance["InstanceType"].should.equal("t2.micro") + + +@mock_autoscaling +def test_describe_autoscaling_instances_boto3_launch_config(): mocked_networking = setup_networking() client = boto3.client("autoscaling", region_name="us-east-1") - _ = client.create_launch_configuration( - LaunchConfigurationName="test_launch_configuration" + client.create_launch_configuration( + LaunchConfigurationName="test_launch_configuration", InstanceType="t2.micro", ) - _ = client.create_auto_scaling_group( + client.create_auto_scaling_group( AutoScalingGroupName="test_asg", LaunchConfigurationName="test_launch_configuration", MinSize=0, @@ -846,9 +1055,51 @@ def test_describe_autoscaling_instances_boto3(): response = client.describe_auto_scaling_instances() len(response["AutoScalingInstances"]).should.equal(5) for instance in response["AutoScalingInstances"]: + instance["LaunchConfigurationName"].should.equal("test_launch_configuration") + instance.should_not.have.key("LaunchTemplate") instance["AutoScalingGroupName"].should.equal("test_asg") instance["AvailabilityZone"].should.equal("us-east-1a") instance["ProtectedFromScaleIn"].should.equal(True) + instance["InstanceType"].should.equal("t2.micro") + + +@mock_autoscaling +@mock_ec2 +def test_describe_autoscaling_instances_boto3_launch_template(): + mocked_networking = setup_networking() + ec2_client = boto3.client("ec2", region_name="us-east-1") + template = ec2_client.create_launch_template( + LaunchTemplateName="test_launch_template", + LaunchTemplateData={ + "ImageId": "ami-0cc293023f983ed53", + "InstanceType": "t2.micro", + }, + )["LaunchTemplate"] + client = boto3.client("autoscaling", region_name="us-east-1") + client.create_auto_scaling_group( + AutoScalingGroupName="test_asg", + LaunchTemplate={"LaunchTemplateName": "test_launch_template", "Version": "1"}, + MinSize=0, + MaxSize=20, + DesiredCapacity=5, + VPCZoneIdentifier=mocked_networking["subnet1"], + NewInstancesProtectedFromScaleIn=True, + ) + expected_launch_template = { + "LaunchTemplateId": template["LaunchTemplateId"], + "LaunchTemplateName": "test_launch_template", + "Version": "1", + } + + response = client.describe_auto_scaling_instances() + len(response["AutoScalingInstances"]).should.equal(5) + for instance in response["AutoScalingInstances"]: + instance["LaunchTemplate"].should.equal(expected_launch_template) + instance.should_not.have.key("LaunchConfigurationName") + instance["AutoScalingGroupName"].should.equal("test_asg") + instance["AvailabilityZone"].should.equal("us-east-1a") + instance["ProtectedFromScaleIn"].should.equal(True) + instance["InstanceType"].should.equal("t2.micro") @mock_autoscaling @@ -885,13 +1136,16 @@ def test_describe_autoscaling_instances_instanceid_filter(): @mock_autoscaling -def test_update_autoscaling_group_boto3(): +def test_update_autoscaling_group_boto3_launch_config(): mocked_networking = setup_networking() client = boto3.client("autoscaling", region_name="us-east-1") - _ = client.create_launch_configuration( + client.create_launch_configuration( LaunchConfigurationName="test_launch_configuration" ) - _ = client.create_auto_scaling_group( + client.create_launch_configuration( + LaunchConfigurationName="test_launch_configuration_new" + ) + client.create_auto_scaling_group( AutoScalingGroupName="test_asg", LaunchConfigurationName="test_launch_configuration", MinSize=0, @@ -901,8 +1155,9 @@ def test_update_autoscaling_group_boto3(): NewInstancesProtectedFromScaleIn=True, ) - _ = client.update_auto_scaling_group( + client.update_auto_scaling_group( AutoScalingGroupName="test_asg", + LaunchConfigurationName="test_launch_configuration_new", MinSize=1, VPCZoneIdentifier="{subnet1},{subnet2}".format( subnet1=mocked_networking["subnet1"], subnet2=mocked_networking["subnet2"] @@ -912,6 +1167,64 @@ def test_update_autoscaling_group_boto3(): response = client.describe_auto_scaling_groups(AutoScalingGroupNames=["test_asg"]) group = response["AutoScalingGroups"][0] + group["LaunchConfigurationName"].should.equal("test_launch_configuration_new") + group["MinSize"].should.equal(1) + set(group["AvailabilityZones"]).should.equal({"us-east-1a", "us-east-1b"}) + group["NewInstancesProtectedFromScaleIn"].should.equal(False) + + +@mock_autoscaling +@mock_ec2 +def test_update_autoscaling_group_boto3_launch_template(): + mocked_networking = setup_networking() + ec2_client = boto3.client("ec2", region_name="us-east-1") + ec2_client.create_launch_template( + LaunchTemplateName="test_launch_template", + LaunchTemplateData={ + "ImageId": "ami-0cc293023f983ed53", + "InstanceType": "t2.micro", + }, + ) + template = ec2_client.create_launch_template( + LaunchTemplateName="test_launch_template_new", + LaunchTemplateData={ + "ImageId": "ami-1ea5b10a3d8867db4", + "InstanceType": "t2.micro", + }, + )["LaunchTemplate"] + client = boto3.client("autoscaling", region_name="us-east-1") + client.create_auto_scaling_group( + AutoScalingGroupName="test_asg", + LaunchTemplate={"LaunchTemplateName": "test_launch_template", "Version": "1"}, + MinSize=0, + MaxSize=20, + DesiredCapacity=5, + VPCZoneIdentifier=mocked_networking["subnet1"], + NewInstancesProtectedFromScaleIn=True, + ) + + client.update_auto_scaling_group( + AutoScalingGroupName="test_asg", + LaunchTemplate={ + "LaunchTemplateName": "test_launch_template_new", + "Version": "1", + }, + MinSize=1, + VPCZoneIdentifier="{subnet1},{subnet2}".format( + subnet1=mocked_networking["subnet1"], subnet2=mocked_networking["subnet2"] + ), + NewInstancesProtectedFromScaleIn=False, + ) + + expected_launch_template = { + "LaunchTemplateId": template["LaunchTemplateId"], + "LaunchTemplateName": "test_launch_template_new", + "Version": "1", + } + + response = client.describe_auto_scaling_groups(AutoScalingGroupNames=["test_asg"]) + group = response["AutoScalingGroups"][0] + group["LaunchTemplate"].should.equal(expected_launch_template) group["MinSize"].should.equal(1) set(group["AvailabilityZones"]).should.equal({"us-east-1a", "us-east-1b"}) group["NewInstancesProtectedFromScaleIn"].should.equal(False) @@ -966,7 +1279,7 @@ def test_update_autoscaling_group_max_size_desired_capacity_change(): @mock_autoscaling -def test_autoscaling_taqs_update_boto3(): +def test_autoscaling_tags_update_boto3(): mocked_networking = setup_networking() client = boto3.client("autoscaling", region_name="us-east-1") _ = client.create_launch_configuration( diff --git a/tests/test_autoscaling/test_cloudformation.py b/tests/test_autoscaling/test_cloudformation.py new file mode 100644 index 000000000..240ba66e0 --- /dev/null +++ b/tests/test_autoscaling/test_cloudformation.py @@ -0,0 +1,276 @@ +import boto3 +import sure # noqa + +from moto import ( + mock_autoscaling, + mock_cloudformation, + mock_ec2, +) + +from utils import setup_networking + + +@mock_autoscaling +@mock_cloudformation +def test_launch_configuration(): + cf_client = boto3.client("cloudformation", region_name="us-east-1") + client = boto3.client("autoscaling", region_name="us-east-1") + + stack_name = "test-launch-configuration" + + cf_template = """ +Resources: + LaunchConfiguration: + Type: AWS::AutoScaling::LaunchConfiguration + Properties: + ImageId: ami-0cc293023f983ed53 + InstanceType: t2.micro + LaunchConfigurationName: test_launch_configuration +Outputs: + LaunchConfigurationName: + Value: !Ref LaunchConfiguration +""".strip() + + cf_client.create_stack( + StackName=stack_name, TemplateBody=cf_template, + ) + stack = cf_client.describe_stacks(StackName=stack_name)["Stacks"][0] + stack["Outputs"][0]["OutputValue"].should.be.equal("test_launch_configuration") + + lc = client.describe_launch_configurations()["LaunchConfigurations"][0] + lc["LaunchConfigurationName"].should.be.equal("test_launch_configuration") + lc["ImageId"].should.be.equal("ami-0cc293023f983ed53") + lc["InstanceType"].should.be.equal("t2.micro") + + cf_template = """ +Resources: + LaunchConfiguration: + Type: AWS::AutoScaling::LaunchConfiguration + Properties: + ImageId: ami-1ea5b10a3d8867db4 + InstanceType: m5.large + LaunchConfigurationName: test_launch_configuration +Outputs: + LaunchConfigurationName: + Value: !Ref LaunchConfiguration +""".strip() + + cf_client.update_stack( + StackName=stack_name, TemplateBody=cf_template, + ) + stack = cf_client.describe_stacks(StackName=stack_name)["Stacks"][0] + stack["Outputs"][0]["OutputValue"].should.be.equal("test_launch_configuration") + + lc = client.describe_launch_configurations()["LaunchConfigurations"][0] + lc["LaunchConfigurationName"].should.be.equal("test_launch_configuration") + lc["ImageId"].should.be.equal("ami-1ea5b10a3d8867db4") + lc["InstanceType"].should.be.equal("m5.large") + + +@mock_autoscaling +@mock_cloudformation +def test_autoscaling_group_from_launch_config(): + subnet_id = setup_networking()["subnet1"] + + cf_client = boto3.client("cloudformation", region_name="us-east-1") + client = boto3.client("autoscaling", region_name="us-east-1") + + client.create_launch_configuration( + LaunchConfigurationName="test_launch_configuration", InstanceType="t2.micro", + ) + stack_name = "test-auto-scaling-group" + + cf_template = """ +Parameters: + SubnetId: + Type: AWS::EC2::Subnet::Id +Resources: + AutoScalingGroup: + Type: AWS::AutoScaling::AutoScalingGroup + Properties: + AutoScalingGroupName: test_auto_scaling_group + AvailabilityZones: + - us-east-1a + LaunchConfigurationName: test_launch_configuration + MaxSize: "5" + MinSize: "1" + VPCZoneIdentifier: + - !Ref SubnetId +Outputs: + AutoScalingGroupName: + Value: !Ref AutoScalingGroup +""".strip() + + cf_client.create_stack( + StackName=stack_name, + TemplateBody=cf_template, + Parameters=[{"ParameterKey": "SubnetId", "ParameterValue": subnet_id}], + ) + stack = cf_client.describe_stacks(StackName=stack_name)["Stacks"][0] + stack["Outputs"][0]["OutputValue"].should.be.equal("test_auto_scaling_group") + + asg = client.describe_auto_scaling_groups()["AutoScalingGroups"][0] + asg["AutoScalingGroupName"].should.be.equal("test_auto_scaling_group") + asg["MinSize"].should.be.equal(1) + asg["MaxSize"].should.be.equal(5) + asg["LaunchConfigurationName"].should.be.equal("test_launch_configuration") + + client.create_launch_configuration( + LaunchConfigurationName="test_launch_configuration_new", + InstanceType="t2.micro", + ) + + cf_template = """ +Parameters: + SubnetId: + Type: AWS::EC2::Subnet::Id +Resources: + AutoScalingGroup: + Type: AWS::AutoScaling::AutoScalingGroup + Properties: + AutoScalingGroupName: test_auto_scaling_group + AvailabilityZones: + - us-east-1a + LaunchConfigurationName: test_launch_configuration_new + MaxSize: "6" + MinSize: "2" + VPCZoneIdentifier: + - !Ref SubnetId +Outputs: + AutoScalingGroupName: + Value: !Ref AutoScalingGroup +""".strip() + + cf_client.update_stack( + StackName=stack_name, + TemplateBody=cf_template, + Parameters=[{"ParameterKey": "SubnetId", "ParameterValue": subnet_id}], + ) + stack = cf_client.describe_stacks(StackName=stack_name)["Stacks"][0] + stack["Outputs"][0]["OutputValue"].should.be.equal("test_auto_scaling_group") + + asg = client.describe_auto_scaling_groups()["AutoScalingGroups"][0] + asg["AutoScalingGroupName"].should.be.equal("test_auto_scaling_group") + asg["MinSize"].should.be.equal(2) + asg["MaxSize"].should.be.equal(6) + asg["LaunchConfigurationName"].should.be.equal("test_launch_configuration_new") + + +@mock_autoscaling +@mock_cloudformation +@mock_ec2 +def test_autoscaling_group_from_launch_template(): + subnet_id = setup_networking()["subnet1"] + + cf_client = boto3.client("cloudformation", region_name="us-east-1") + ec2_client = boto3.client("ec2", region_name="us-east-1") + client = boto3.client("autoscaling", region_name="us-east-1") + + template_response = ec2_client.create_launch_template( + LaunchTemplateName="test_launch_template", + LaunchTemplateData={ + "ImageId": "ami-0cc293023f983ed53", + "InstanceType": "t2.micro", + }, + ) + launch_template_id = template_response["LaunchTemplate"]["LaunchTemplateId"] + stack_name = "test-auto-scaling-group" + + cf_template = """ +Parameters: + SubnetId: + Type: AWS::EC2::Subnet::Id + LaunchTemplateId: + Type: String +Resources: + AutoScalingGroup: + Type: AWS::AutoScaling::AutoScalingGroup + Properties: + AutoScalingGroupName: test_auto_scaling_group + AvailabilityZones: + - us-east-1a + LaunchTemplate: + LaunchTemplateId: !Ref LaunchTemplateId + Version: "1" + MaxSize: "5" + MinSize: "1" + VPCZoneIdentifier: + - !Ref SubnetId +Outputs: + AutoScalingGroupName: + Value: !Ref AutoScalingGroup +""".strip() + + cf_client.create_stack( + StackName=stack_name, + TemplateBody=cf_template, + Parameters=[ + {"ParameterKey": "SubnetId", "ParameterValue": subnet_id}, + {"ParameterKey": "LaunchTemplateId", "ParameterValue": launch_template_id}, + ], + ) + stack = cf_client.describe_stacks(StackName=stack_name)["Stacks"][0] + stack["Outputs"][0]["OutputValue"].should.be.equal("test_auto_scaling_group") + + asg = client.describe_auto_scaling_groups()["AutoScalingGroups"][0] + asg["AutoScalingGroupName"].should.be.equal("test_auto_scaling_group") + asg["MinSize"].should.be.equal(1) + asg["MaxSize"].should.be.equal(5) + lt = asg["LaunchTemplate"] + lt["LaunchTemplateId"].should.be.equal(launch_template_id) + lt["LaunchTemplateName"].should.be.equal("test_launch_template") + lt["Version"].should.be.equal("1") + + template_response = ec2_client.create_launch_template( + LaunchTemplateName="test_launch_template_new", + LaunchTemplateData={ + "ImageId": "ami-1ea5b10a3d8867db4", + "InstanceType": "m5.large", + }, + ) + launch_template_id = template_response["LaunchTemplate"]["LaunchTemplateId"] + + cf_template = """ +Parameters: + SubnetId: + Type: AWS::EC2::Subnet::Id + LaunchTemplateId: + Type: String +Resources: + AutoScalingGroup: + Type: AWS::AutoScaling::AutoScalingGroup + Properties: + AutoScalingGroupName: test_auto_scaling_group + AvailabilityZones: + - us-east-1a + LaunchTemplate: + LaunchTemplateId: !Ref LaunchTemplateId + Version: "1" + MaxSize: "6" + MinSize: "2" + VPCZoneIdentifier: + - !Ref SubnetId +Outputs: + AutoScalingGroupName: + Value: !Ref AutoScalingGroup +""".strip() + + cf_client.update_stack( + StackName=stack_name, + TemplateBody=cf_template, + Parameters=[ + {"ParameterKey": "SubnetId", "ParameterValue": subnet_id}, + {"ParameterKey": "LaunchTemplateId", "ParameterValue": launch_template_id}, + ], + ) + stack = cf_client.describe_stacks(StackName=stack_name)["Stacks"][0] + stack["Outputs"][0]["OutputValue"].should.be.equal("test_auto_scaling_group") + + asg = client.describe_auto_scaling_groups()["AutoScalingGroups"][0] + asg["AutoScalingGroupName"].should.be.equal("test_auto_scaling_group") + asg["MinSize"].should.be.equal(2) + asg["MaxSize"].should.be.equal(6) + lt = asg["LaunchTemplate"] + lt["LaunchTemplateId"].should.be.equal(launch_template_id) + lt["LaunchTemplateName"].should.be.equal("test_launch_template_new") + lt["Version"].should.be.equal("1") diff --git a/tests/test_awslambda/test_lambda.py b/tests/test_awslambda/test_lambda.py index 1cd943f04..ca05d4aa4 100644 --- a/tests/test_awslambda/test_lambda.py +++ b/tests/test_awslambda/test_lambda.py @@ -489,7 +489,7 @@ def test_get_function(): {"test_variable": "test_value"} ) - # Test get function with + # Test get function with qualifier result = conn.get_function(FunctionName="testFunction", Qualifier="$LATEST") result["Configuration"]["Version"].should.equal("$LATEST") result["Configuration"]["FunctionArn"].should.equal( @@ -1721,6 +1721,82 @@ def test_remove_function_permission(): policy["Statement"].should.equal([]) +@mock_lambda +def test_put_function_concurrency(): + expected_concurrency = 15 + function_name = "test" + + conn = boto3.client("lambda", _lambda_region) + conn.create_function( + FunctionName=function_name, + Runtime="python3.8", + Role=(get_role_name()), + Handler="lambda_function.handler", + Code={"ZipFile": get_test_zip_file1()}, + Description="test lambda function", + Timeout=3, + MemorySize=128, + Publish=True, + ) + result = conn.put_function_concurrency( + FunctionName=function_name, ReservedConcurrentExecutions=expected_concurrency + ) + + result["ReservedConcurrentExecutions"].should.equal(expected_concurrency) + + +@mock_lambda +def test_delete_function_concurrency(): + function_name = "test" + + conn = boto3.client("lambda", _lambda_region) + conn.create_function( + FunctionName=function_name, + Runtime="python3.8", + Role=(get_role_name()), + Handler="lambda_function.handler", + Code={"ZipFile": get_test_zip_file1()}, + Description="test lambda function", + Timeout=3, + MemorySize=128, + Publish=True, + ) + conn.put_function_concurrency( + FunctionName=function_name, ReservedConcurrentExecutions=15 + ) + + conn.delete_function_concurrency(FunctionName=function_name) + result = conn.get_function(FunctionName=function_name) + + result.doesnt.have.key("Concurrency") + + +@mock_lambda +def test_get_function_concurrency(): + expected_concurrency = 15 + function_name = "test" + + conn = boto3.client("lambda", _lambda_region) + conn.create_function( + FunctionName=function_name, + Runtime="python3.8", + Role=(get_role_name()), + Handler="lambda_function.handler", + Code={"ZipFile": get_test_zip_file1()}, + Description="test lambda function", + Timeout=3, + MemorySize=128, + Publish=True, + ) + conn.put_function_concurrency( + FunctionName=function_name, ReservedConcurrentExecutions=expected_concurrency + ) + + result = conn.get_function_concurrency(FunctionName=function_name) + + result["ReservedConcurrentExecutions"].should.equal(expected_concurrency) + + def create_invalid_lambda(role): conn = boto3.client("lambda", _lambda_region) zip_content = get_test_zip_file1() diff --git a/tests/test_cloudformation/test_cloudformation_stack_integration.py b/tests/test_cloudformation/test_cloudformation_stack_integration.py index 5a8e9cd68..ee2fbc94c 100644 --- a/tests/test_cloudformation/test_cloudformation_stack_integration.py +++ b/tests/test_cloudformation/test_cloudformation_stack_integration.py @@ -1777,6 +1777,7 @@ def lambda_handler(event, context): "Role": {"Fn::GetAtt": ["MyRole", "Arn"]}, "Runtime": "python2.7", "Environment": {"Variables": {"TEST_ENV_KEY": "test-env-val"}}, + "ReservedConcurrentExecutions": 10, }, }, "MyRole": { @@ -1811,6 +1812,11 @@ def lambda_handler(event, context): {"Variables": {"TEST_ENV_KEY": "test-env-val"}} ) + function_name = result["Functions"][0]["FunctionName"] + result = conn.get_function(FunctionName=function_name) + + result["Concurrency"]["ReservedConcurrentExecutions"].should.equal(10) + @mock_cloudformation @mock_ec2 diff --git a/tests/test_ec2/test_security_groups.py b/tests/test_ec2/test_security_groups.py index 90f395507..10885df18 100644 --- a/tests/test_ec2/test_security_groups.py +++ b/tests/test_ec2/test_security_groups.py @@ -275,8 +275,9 @@ def test_authorize_ip_range_and_revoke(): int(egress_security_group.rules_egress[1].to_port).should.equal(2222) actual_cidr = egress_security_group.rules_egress[1].grants[0].cidr_ip # Deal with Python2 dict->unicode, instead of dict->string - actual_cidr = json.loads(actual_cidr.replace("u'", "'").replace("'", '"')) - actual_cidr.should.equal({"CidrIp": "123.123.123.123/32"}) + if type(actual_cidr) == "unicode": + actual_cidr = json.loads(actual_cidr.replace("u'", "'").replace("'", '"')) + actual_cidr.should.equal("123.123.123.123/32") # Wrong Cidr should throw error egress_security_group.revoke.when.called_with( @@ -810,7 +811,9 @@ def test_authorize_and_revoke_in_bulk(): sg03 = ec2.create_security_group( GroupName="sg03", Description="Test security group sg03" ) - + sg04 = ec2.create_security_group( + GroupName="sg04", Description="Test security group sg04" + ) ip_permissions = [ { "IpProtocol": "tcp", @@ -835,13 +838,31 @@ def test_authorize_and_revoke_in_bulk(): "UserIdGroupPairs": [{"GroupName": "sg03", "UserId": sg03.owner_id}], "IpRanges": [], }, + { + "IpProtocol": "tcp", + "FromPort": 27015, + "ToPort": 27015, + "UserIdGroupPairs": [{"GroupName": "sg04", "UserId": sg04.owner_id}], + "IpRanges": [ + {"CidrIp": "10.10.10.0/24", "Description": "Some Description"} + ], + }, + { + "IpProtocol": "tcp", + "FromPort": 27016, + "ToPort": 27016, + "UserIdGroupPairs": [{"GroupId": sg04.id, "UserId": sg04.owner_id}], + "IpRanges": [{"CidrIp": "10.10.10.0/24"}], + }, ] expected_ip_permissions = copy.deepcopy(ip_permissions) expected_ip_permissions[1]["UserIdGroupPairs"][0]["GroupName"] = "sg02" expected_ip_permissions[2]["UserIdGroupPairs"][0]["GroupId"] = sg03.id + expected_ip_permissions[3]["UserIdGroupPairs"][0]["GroupId"] = sg04.id + expected_ip_permissions[4]["UserIdGroupPairs"][0]["GroupName"] = "sg04" sg01.authorize_ingress(IpPermissions=ip_permissions) - sg01.ip_permissions.should.have.length_of(3) + sg01.ip_permissions.should.have.length_of(5) for ip_permission in expected_ip_permissions: sg01.ip_permissions.should.contain(ip_permission) @@ -851,7 +872,7 @@ def test_authorize_and_revoke_in_bulk(): sg01.ip_permissions.shouldnt.contain(ip_permission) sg01.authorize_egress(IpPermissions=ip_permissions) - sg01.ip_permissions_egress.should.have.length_of(4) + sg01.ip_permissions_egress.should.have.length_of(6) for ip_permission in expected_ip_permissions: sg01.ip_permissions_egress.should.contain(ip_permission) @@ -930,11 +951,10 @@ def test_revoke_security_group_egress(): sg.revoke_egress( IpPermissions=[ { - "FromPort": 0, "IpProtocol": "-1", "IpRanges": [{"CidrIp": "0.0.0.0/0"}], - "ToPort": 123, - }, + "UserIdGroupPairs": [], + } ] ) diff --git a/tests/test_ses/test_ses_boto3.py b/tests/test_ses/test_ses_boto3.py index de8ec7261..efd4b980c 100644 --- a/tests/test_ses/test_ses_boto3.py +++ b/tests/test_ses/test_ses_boto3.py @@ -84,6 +84,35 @@ def test_send_email(): sent_count.should.equal(3) +@mock_ses +def test_send_email_when_verify_source(): + conn = boto3.client("ses", region_name="us-east-1") + + kwargs = dict( + Destination={"ToAddresses": ["test_to@example.com"],}, + Message={ + "Subject": {"Data": "test subject"}, + "Body": {"Text": {"Data": "test body"}}, + }, + ) + + conn.send_email.when.called_with( + Source="verify_email_address@example.com", **kwargs + ).should.throw(ClientError) + conn.verify_email_address(EmailAddress="verify_email_address@example.com") + conn.send_email(Source="verify_email_address@example.com", **kwargs) + + conn.send_email.when.called_with( + Source="verify_email_identity@example.com", **kwargs + ).should.throw(ClientError) + conn.verify_email_identity(EmailAddress="verify_email_identity@example.com") + conn.send_email(Source="verify_email_identity@example.com", **kwargs) + + send_quota = conn.get_send_quota() + sent_count = int(send_quota["SentLast24Hours"]) + sent_count.should.equal(2) + + @mock_ses def test_send_templated_email(): conn = boto3.client("ses", region_name="us-east-1") diff --git a/tests/test_sns/test_publishing_boto3.py b/tests/test_sns/test_publishing_boto3.py index fddd9125c..99e7ae7a4 100644 --- a/tests/test_sns/test_publishing_boto3.py +++ b/tests/test_sns/test_publishing_boto3.py @@ -11,8 +11,9 @@ import sure # noqa import responses from botocore.exceptions import ClientError from nose.tools import assert_raises -from moto import mock_sns, mock_sqs +from moto import mock_sns, mock_sqs, settings from moto.core import ACCOUNT_ID +from moto.sns import sns_backend MESSAGE_FROM_SQS_TEMPLATE = ( '{\n "Message": "%s",\n "MessageId": "%s",\n "Signature": "EXAMPLElDMXvB8r9R83tGoNn0ecwd5UjllzsvSvbItzfaMpN2nk5HVSw7XnOn/49IkxDKz8YrlH2qJXj2iZB0Zo2O71c4qQk1fMUDi3LGpij7RCW7AW9vYYsSqIKRnFS94ilu7NFhUzLiieYr4BKHpdTmdD6c0esKEYBpabxDSc=",\n "SignatureVersion": "1",\n "SigningCertURL": "https://sns.us-east-1.amazonaws.com/SimpleNotificationService-f3ecfb7224c7233fe7bb5f59f96de52f.pem",\n "Subject": "my subject",\n "Timestamp": "2015-01-01T12:00:00.000Z",\n "TopicArn": "arn:aws:sns:%s:' @@ -223,36 +224,31 @@ def test_publish_to_sqs_msg_attr_number_type(): @mock_sns def test_publish_sms(): client = boto3.client("sns", region_name="us-east-1") - client.create_topic(Name="some-topic") - resp = client.create_topic(Name="some-topic") - arn = resp["TopicArn"] - - client.subscribe(TopicArn=arn, Protocol="sms", Endpoint="+15551234567") result = client.publish(PhoneNumber="+15551234567", Message="my message") + result.should.contain("MessageId") + if not settings.TEST_SERVER_MODE: + sns_backend.sms_messages.should.have.key(result["MessageId"]).being.equal( + ("+15551234567", "my message") + ) @mock_sns def test_publish_bad_sms(): client = boto3.client("sns", region_name="us-east-1") - client.create_topic(Name="some-topic") - resp = client.create_topic(Name="some-topic") - arn = resp["TopicArn"] - client.subscribe(TopicArn=arn, Protocol="sms", Endpoint="+15551234567") - - try: - # Test invalid number + # Test invalid number + with assert_raises(ClientError) as cm: client.publish(PhoneNumber="NAA+15551234567", Message="my message") - except ClientError as err: - err.response["Error"]["Code"].should.equal("InvalidParameter") + cm.exception.response["Error"]["Code"].should.equal("InvalidParameter") + cm.exception.response["Error"]["Message"].should.contain("not meet the E164") - try: - # Test not found number - client.publish(PhoneNumber="+44001234567", Message="my message") - except ClientError as err: - err.response["Error"]["Code"].should.equal("ParameterValueInvalid") + # Test to long ASCII message + with assert_raises(ClientError) as cm: + client.publish(PhoneNumber="+15551234567", Message="a" * 1601) + cm.exception.response["Error"]["Code"].should.equal("InvalidParameter") + cm.exception.response["Error"]["Message"].should.contain("must be less than 1600") @mock_sqs diff --git a/travis_moto_server.sh b/travis_moto_server.sh index 902644b20..4be26073e 100755 --- a/travis_moto_server.sh +++ b/travis_moto_server.sh @@ -1,5 +1,9 @@ #!/usr/bin/env bash set -e pip install flask +# TravisCI on bionic dist uses old version of Docker Engine +# which is incompatibile with newer docker-py +# See https://github.com/docker/docker-py/issues/2639 +pip install "docker>=2.5.1,<=4.2.2" pip install /moto/dist/moto*.gz moto_server -H 0.0.0.0 -p 5000 \ No newline at end of file