diff --git a/moto/awslambda/models.py b/moto/awslambda/models.py index 935abbcd6..6306acd5c 100644 --- a/moto/awslambda/models.py +++ b/moto/awslambda/models.py @@ -298,7 +298,12 @@ class LambdaFunction(BaseModel): volumes=["{}:/var/task".format(data_vol.name)], environment=env_vars, detach=True, **run_kwargs) finally: if container: - exit_code = container.wait() + try: + exit_code = container.wait(timeout=300) + except requests.exceptions.ReadTimeout: + exit_code = -1 + container.stop() + container.kill() output = container.logs(stdout=False, stderr=True) output += container.logs(stdout=True, stderr=False) container.remove() diff --git a/moto/cloudformation/parsing.py b/moto/cloudformation/parsing.py index 05a408be1..1c13c5058 100644 --- a/moto/cloudformation/parsing.py +++ b/moto/cloudformation/parsing.py @@ -15,6 +15,7 @@ from moto.dynamodb import models as dynamodb_models from moto.ec2 import models as ec2_models from moto.ecs import models as ecs_models from moto.elb import models as elb_models +from moto.elbv2 import models as elbv2_models from moto.iam import models as iam_models from moto.kinesis import models as kinesis_models from moto.kms import models as kms_models @@ -61,6 +62,9 @@ MODEL_MAP = { "AWS::ECS::TaskDefinition": ecs_models.TaskDefinition, "AWS::ECS::Service": ecs_models.Service, "AWS::ElasticLoadBalancing::LoadBalancer": elb_models.FakeLoadBalancer, + "AWS::ElasticLoadBalancingV2::LoadBalancer": elbv2_models.FakeLoadBalancer, + "AWS::ElasticLoadBalancingV2::TargetGroup": elbv2_models.FakeTargetGroup, + "AWS::ElasticLoadBalancingV2::Listener": elbv2_models.FakeListener, "AWS::DataPipeline::Pipeline": datapipeline_models.Pipeline, "AWS::IAM::InstanceProfile": iam_models.InstanceProfile, "AWS::IAM::Role": iam_models.Role, @@ -326,7 +330,7 @@ def parse_output(output_logical_id, output_json, resources_map): output_json = clean_json(output_json, resources_map) output = Output() output.key = output_logical_id - output.value = output_json['Value'] + output.value = clean_json(output_json['Value'], resources_map) output.description = output_json.get('Description') return output diff --git a/moto/ec2/models.py b/moto/ec2/models.py index 1835f4dfc..89b8408c8 100755 --- a/moto/ec2/models.py +++ b/moto/ec2/models.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals import copy import itertools +import ipaddress import json import os import re @@ -402,6 +403,10 @@ class Instance(TaggedEC2Resource, BotoInstance): subnet = ec2_backend.get_subnet(self.subnet_id) self.vpc_id = subnet.vpc_id self._placement.zone = subnet.availability_zone + + if associate_public_ip is None: + # Mapping public ip hasnt been explicitly enabled or disabled + associate_public_ip = subnet.map_public_ip_on_launch == 'true' elif placement: self._placement.zone = placement else: @@ -409,10 +414,22 @@ class Instance(TaggedEC2Resource, BotoInstance): self.block_device_mapping = BlockDeviceMapping() - self.prep_nics(kwargs.get("nics", {}), - subnet_id=self.subnet_id, - private_ip=kwargs.get("private_ip"), - associate_public_ip=associate_public_ip) + self._private_ips = set() + self.prep_nics( + kwargs.get("nics", {}), + private_ip=kwargs.get("private_ip"), + associate_public_ip=associate_public_ip + ) + + def __del__(self): + try: + subnet = self.ec2_backend.get_subnet(self.subnet_id) + for ip in self._private_ips: + subnet.del_subnet_ip(ip) + except Exception: + # Its not "super" critical we clean this up, as reset will do this + # worst case we'll get IP address exaustion... rarely + pass def setup_defaults(self): # Default have an instance with root volume should you not wish to @@ -547,14 +564,23 @@ class Instance(TaggedEC2Resource, BotoInstance): else: return self.security_groups - def prep_nics(self, nic_spec, subnet_id=None, private_ip=None, associate_public_ip=None): + def prep_nics(self, nic_spec, private_ip=None, associate_public_ip=None): self.nics = {} - if not private_ip: + if self.subnet_id: + subnet = self.ec2_backend.get_subnet(self.subnet_id) + if not private_ip: + private_ip = subnet.get_available_subnet_ip(instance=self) + else: + subnet.request_ip(private_ip, instance=self) + + self._private_ips.add(private_ip) + elif private_ip is None: + # Preserve old behaviour if in EC2-Classic mode private_ip = random_private_ip() # Primary NIC defaults - primary_nic = {'SubnetId': subnet_id, + primary_nic = {'SubnetId': self.subnet_id, 'PrivateIpAddress': private_ip, 'AssociatePublicIpAddress': associate_public_ip} primary_nic = dict((k, v) for k, v in primary_nic.items() if v) @@ -2114,10 +2140,17 @@ class Subnet(TaggedEC2Resource): self.id = subnet_id self.vpc_id = vpc_id self.cidr_block = cidr_block + self.cidr = ipaddress.ip_network(six.text_type(self.cidr_block)) self._availability_zone = availability_zone self.default_for_az = default_for_az self.map_public_ip_on_launch = map_public_ip_on_launch + # Theory is we assign ip's as we go (as 16,777,214 usable IPs in a /8) + self._subnet_ip_generator = self.cidr.hosts() + self.reserved_ips = [six.next(self._subnet_ip_generator) for _ in range(0, 3)] # Reserved by AWS + self._unused_ips = set() # if instance is destroyed hold IP here for reuse + self._subnet_ips = {} # has IP: instance + @classmethod def create_from_cloudformation_json(cls, resource_name, cloudformation_json, region_name): properties = cloudformation_json['Properties'] @@ -2184,6 +2217,46 @@ class Subnet(TaggedEC2Resource): '"Fn::GetAtt" : [ "{0}" , "AvailabilityZone" ]"') raise UnformattedGetAttTemplateException() + def get_available_subnet_ip(self, instance): + try: + new_ip = self._unused_ips.pop() + except KeyError: + new_ip = six.next(self._subnet_ip_generator) + + # Skips any IP's if they've been manually specified + while str(new_ip) in self._subnet_ips: + new_ip = six.next(self._subnet_ip_generator) + + if new_ip == self.cidr.broadcast_address: + raise StopIteration() # Broadcast address cant be used obviously + # TODO StopIteration will be raised if no ip's available, not sure how aws handles this. + + new_ip = str(new_ip) + self._subnet_ips[new_ip] = instance + + return new_ip + + def request_ip(self, ip, instance): + if ipaddress.ip_address(ip) not in self.cidr: + raise Exception('IP does not fall in the subnet CIDR of {0}'.format(self.cidr)) + + if ip in self._subnet_ips: + raise Exception('IP already in use') + try: + self._unused_ips.remove(ip) + except KeyError: + pass + + self._subnet_ips[ip] = instance + return ip + + def del_subnet_ip(self, ip): + try: + del self._subnet_ips[ip] + self._unused_ips.add(ip) + except KeyError: + pass # Unknown IP + class SubnetBackend(object): def __init__(self): diff --git a/moto/elbv2/models.py b/moto/elbv2/models.py index 53eedc16b..71abaf110 100644 --- a/moto/elbv2/models.py +++ b/moto/elbv2/models.py @@ -57,7 +57,8 @@ class FakeTargetGroup(BaseModel): healthcheck_timeout_seconds, healthy_threshold_count, unhealthy_threshold_count, - http_codes): + matcher=None, + target_type=None): self.name = name self.arn = arn self.vpc_id = vpc_id @@ -72,7 +73,8 @@ class FakeTargetGroup(BaseModel): self.unhealthy_threshold_count = unhealthy_threshold_count self.load_balancer_arns = [] self.tags = {} - self.http_status_codes = http_codes + self.matcher = matcher + self.target_type = target_type self.attributes = { 'deregistration_delay.timeout_seconds': 300, @@ -81,6 +83,10 @@ class FakeTargetGroup(BaseModel): self.targets = OrderedDict() + @property + def physical_resource_id(self): + return self.arn + def register(self, targets): for target in targets: self.targets[target['id']] = { @@ -105,6 +111,46 @@ class FakeTargetGroup(BaseModel): raise InvalidTargetError() return FakeHealthStatus(t['id'], t['port'], self.healthcheck_port, 'healthy') + @classmethod + def create_from_cloudformation_json(cls, resource_name, cloudformation_json, region_name): + properties = cloudformation_json['Properties'] + + elbv2_backend = elbv2_backends[region_name] + + # per cloudformation docs: + # The target group name should be shorter than 22 characters because + # AWS CloudFormation uses the target group name to create the name of the load balancer. + name = properties.get('Name', resource_name[:22]) + vpc_id = properties.get("VpcId") + protocol = properties.get('Protocol') + port = properties.get("Port") + healthcheck_protocol = properties.get("HealthCheckProtocol") + healthcheck_port = properties.get("HealthCheckPort") + healthcheck_path = properties.get("HealthCheckPath") + healthcheck_interval_seconds = properties.get("HealthCheckIntervalSeconds") + healthcheck_timeout_seconds = properties.get("HealthCheckTimeoutSeconds") + healthy_threshold_count = properties.get("HealthyThresholdCount") + unhealthy_threshold_count = properties.get("UnhealthyThresholdCount") + matcher = properties.get("Matcher") + target_type = properties.get("TargetType") + + target_group = elbv2_backend.create_target_group( + name=name, + vpc_id=vpc_id, + protocol=protocol, + port=port, + healthcheck_protocol=healthcheck_protocol, + healthcheck_port=healthcheck_port, + healthcheck_path=healthcheck_path, + healthcheck_interval_seconds=healthcheck_interval_seconds, + healthcheck_timeout_seconds=healthcheck_timeout_seconds, + healthy_threshold_count=healthy_threshold_count, + unhealthy_threshold_count=unhealthy_threshold_count, + matcher=matcher, + target_type=target_type, + ) + return target_group + class FakeListener(BaseModel): @@ -126,6 +172,10 @@ class FakeListener(BaseModel): is_default=True ) + @property + def physical_resource_id(self): + return self.arn + @property def rules(self): return self._non_default_rules + [self._default_rule] @@ -137,6 +187,28 @@ class FakeListener(BaseModel): self._non_default_rules.append(rule) self._non_default_rules = sorted(self._non_default_rules, key=lambda x: x.priority) + @classmethod + def create_from_cloudformation_json(cls, resource_name, cloudformation_json, region_name): + properties = cloudformation_json['Properties'] + + elbv2_backend = elbv2_backends[region_name] + load_balancer_arn = properties.get("LoadBalancerArn") + protocol = properties.get("Protocol") + port = properties.get("Port") + ssl_policy = properties.get("SslPolicy") + certificates = properties.get("Certificates") + # transform default actions to confirm with the rest of the code and XML templates + if "DefaultActions" in properties: + default_actions = [] + for action in properties['DefaultActions']: + default_actions.append({'type': action['Type'], 'target_group_arn': action['TargetGroupArn']}) + else: + default_actions = None + + listener = elbv2_backend.create_listener( + load_balancer_arn, protocol, port, ssl_policy, certificates, default_actions) + return listener + class FakeRule(BaseModel): @@ -186,7 +258,7 @@ class FakeLoadBalancer(BaseModel): @property def physical_resource_id(self): - return self.name + return self.arn def add_tag(self, key, value): if len(self.tags) >= 10 and key not in self.tags: @@ -204,6 +276,27 @@ class FakeLoadBalancer(BaseModel): ''' Not exposed as part of the ELB API - used for CloudFormation. ''' elbv2_backends[region].delete_load_balancer(self.arn) + @classmethod + def create_from_cloudformation_json(cls, resource_name, cloudformation_json, region_name): + properties = cloudformation_json['Properties'] + + elbv2_backend = elbv2_backends[region_name] + + name = properties.get('Name', resource_name) + security_groups = properties.get("SecurityGroups") + subnet_ids = properties.get('Subnets') + scheme = properties.get('Scheme', 'internet-facing') + + load_balancer = elbv2_backend.create_load_balancer(name, security_groups, subnet_ids, scheme=scheme) + return load_balancer + + def get_cfn_attribute(self, attribute_name): + attributes = { + 'DNSName': self.dns_name, + 'LoadBalancerName': self.name, + } + return attributes[attribute_name] + class ELBv2Backend(BaseBackend): @@ -316,7 +409,7 @@ class ELBv2Backend(BaseBackend): def create_target_group(self, name, **kwargs): if len(name) > 32: raise InvalidTargetGroupNameError( - "Target group name '%s' cannot be longer than '32' characters" % name + "Target group name '%s' cannot be longer than '22' characters" % name ) if not re.match('^[a-zA-Z0-9\-]+$', name): raise InvalidTargetGroupNameError( diff --git a/moto/elbv2/responses.py b/moto/elbv2/responses.py index 45c35d426..9a189b1c9 100644 --- a/moto/elbv2/responses.py +++ b/moto/elbv2/responses.py @@ -734,9 +734,14 @@ CREATE_TARGET_GROUP_TEMPLATE = """ 30: + if elapsed_s > 60: raise print('.')