From 04e623ea144e9759ba7f33574381eff913444dfc Mon Sep 17 00:00:00 2001 From: Jack Danger Date: Thu, 20 Jul 2017 15:00:30 -0700 Subject: [PATCH 1/9] Implemented core endpoints of ELBv2 --- docs/index.rst | 1 + moto/__init__.py | 1 + moto/elbv2/__init__.py | 6 + moto/elbv2/exceptions.py | 103 +++++ moto/elbv2/models.py | 312 +++++++++++++++ moto/elbv2/responses.py | 649 ++++++++++++++++++++++++++++++++ moto/elbv2/urls.py | 10 + tests/test_elbv2/test_elbv2.py | 447 ++++++++++++++++++++++ tests/test_elbv2/test_server.py | 17 + 9 files changed, 1546 insertions(+) create mode 100644 moto/elbv2/__init__.py create mode 100644 moto/elbv2/exceptions.py create mode 100644 moto/elbv2/models.py create mode 100644 moto/elbv2/responses.py create mode 100644 moto/elbv2/urls.py create mode 100644 tests/test_elbv2/test_elbv2.py create mode 100644 tests/test_elbv2/test_server.py diff --git a/docs/index.rst b/docs/index.rst index 2ce31febd..9a9fa5261 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -43,6 +43,7 @@ Currently implemented Services: | ECS | @mock_ecs | basic endpoints done | +-----------------------+---------------------+-----------------------------------+ | ELB | @mock_elb | core endpoints done | +| | @mock_elbv2 | core endpoints done | +-----------------------+---------------------+-----------------------------------+ | EMR | @mock_emr | core endpoints done | +-----------------------+---------------------+-----------------------------------+ diff --git a/moto/__init__.py b/moto/__init__.py index 304e25cc5..728d8db71 100644 --- a/moto/__init__.py +++ b/moto/__init__.py @@ -17,6 +17,7 @@ from .ec2 import mock_ec2, mock_ec2_deprecated # flake8: noqa from .ecr import mock_ecr, mock_ecr_deprecated # flake8: noqa from .ecs import mock_ecs, mock_ecs_deprecated # flake8: noqa from .elb import mock_elb, mock_elb_deprecated # flake8: noqa +from .elbv2 import mock_elbv2 # flake8: noqa from .emr import mock_emr, mock_emr_deprecated # flake8: noqa from .events import mock_events # flake8: noqa from .glacier import mock_glacier, mock_glacier_deprecated # flake8: noqa diff --git a/moto/elbv2/__init__.py b/moto/elbv2/__init__.py new file mode 100644 index 000000000..21a6d06c6 --- /dev/null +++ b/moto/elbv2/__init__.py @@ -0,0 +1,6 @@ +from __future__ import unicode_literals +from .models import elbv2_backends +from ..core.models import base_decorator + +elb_backend = elbv2_backends['us-east-1'] +mock_elbv2 = base_decorator(elbv2_backends) diff --git a/moto/elbv2/exceptions.py b/moto/elbv2/exceptions.py new file mode 100644 index 000000000..397aa115b --- /dev/null +++ b/moto/elbv2/exceptions.py @@ -0,0 +1,103 @@ +from __future__ import unicode_literals +from moto.core.exceptions import RESTError + + +class ELBClientError(RESTError): + code = 400 + + +class DuplicateTagKeysError(ELBClientError): + + def __init__(self, cidr): + super(DuplicateTagKeysError, self).__init__( + "DuplicateTagKeys", + "Tag key was specified more than once: {0}" + .format(cidr)) + + +class LoadBalancerNotFoundError(ELBClientError): + + def __init__(self): + super(LoadBalancerNotFoundError, self).__init__( + "LoadBalancerNotFound", + "The specified load balancer does not exist.") + + +class ListenerNotFoundError(ELBClientError): + + def __init__(self): + super(ListenerNotFoundError, self).__init__( + "ListenerNotFound", + "The specified listener does not exist.") + + +class SubnetNotFoundError(ELBClientError): + + def __init__(self): + super(SubnetNotFoundError, self).__init__( + "SubnetNotFound", + "The specified subnet does not exist.") + + +class TargetGroupNotFoundError(ELBClientError): + + def __init__(self): + super(TooManyTagsError, self).__init__( + "TargetGroupNotFound", + "The specified target group does not exist.") + + +class TooManyTagsError(ELBClientError): + + def __init__(self): + super(TooManyTagsError, self).__init__( + "TooManyTagsError", + "The quota for the number of tags that can be assigned to a load balancer has been reached") + + +class BadHealthCheckDefinition(ELBClientError): + + def __init__(self): + super(BadHealthCheckDefinition, self).__init__( + "ValidationError", + "HealthCheck Target must begin with one of HTTP, TCP, HTTPS, SSL") + + +class DuplicateListenerError(ELBClientError): + + def __init__(self): + super(DuplicateListenerError, self).__init__( + "DuplicateListener", + "A listener with the specified port already exists.") + + +class DuplicateLoadBalancerName(ELBClientError): + + def __init__(self): + super(DuplicateLoadBalancerName, self).__init__( + "DuplicateLoadBalancerName", + "A load balancer with the specified name already exists.") + + +class DuplicateTargetGroupName(ELBClientError): + + def __init__(self): + super(DuplicateTargetGroupName, self).__init__( + "DuplicateTargetGroupName", + "A target group with the specified name already exists.") + + +class InvalidTargetError(ELBClientError): + + def __init__(self): + super(InvalidTargetError, self).__init__( + "InvalidTarget", + "The specified target does not exist or is not in the same VPC as the target group.") + + +class EmptyListenersError(ELBClientError): + + def __init__(self): + super(EmptyListenersError, self).__init__( + "ValidationError", + "Listeners cannot be empty") diff --git a/moto/elbv2/models.py b/moto/elbv2/models.py new file mode 100644 index 000000000..7682ae097 --- /dev/null +++ b/moto/elbv2/models.py @@ -0,0 +1,312 @@ +from __future__ import unicode_literals + +import datetime +from moto.compat import OrderedDict +from moto.core import BaseBackend, BaseModel +from moto.ec2.models import ec2_backends +from .exceptions import ( + DuplicateLoadBalancerName, + DuplicateListenerError, + DuplicateTargetGroupName, + InvalidTargetError, + ListenerNotFoundError, + LoadBalancerNotFoundError, + SubnetNotFoundError, + TargetGroupNotFoundError, + TooManyTagsError, +) + + +class FakeHealthStatus(BaseModel): + + def __init__(self, instance_id, port, health_port, status, reason=None): + self.instance_id = instance_id + self.port = port + self.health_port = health_port + self.status = status + self.reason = reason + + +class FakeTargetGroup(BaseModel): + def __init__(self, + name, + arn, + vpc_id, + protocol, + port, + healthcheck_protocol, + healthcheck_port, + healthcheck_path, + healthcheck_interval_seconds, + healthcheck_timeout_seconds, + healthy_threshold_count, + unhealthy_threshold_count): + self.name = name + self.arn = arn + self.vpc_id = vpc_id + self.protocol = protocol + self.port = port + self.healthcheck_protocol = healthcheck_protocol + self.healthcheck_port = healthcheck_port + self.healthcheck_path = healthcheck_path + self.healthcheck_interval_seconds = healthcheck_interval_seconds + self.healthcheck_timeout_seconds = healthcheck_timeout_seconds + self.healthy_threshold_count = healthy_threshold_count + self.unhealthy_threshold_count = unhealthy_threshold_count + self.load_balancer_arns = [] + + self.targets = OrderedDict() + + def register(self, targets): + for target in targets: + self.targets[target['id']] = { + 'id': target['id'], + 'port': target.get('port', self.port), + } + + def deregister(self, targets): + for target in targets: + t = self.targets.pop(target['id']) + if not t: + raise InvalidTargetError() + + def health_for(self, target): + t = self.targets.get(target['id']) + if t is None: + raise InvalidTargetError() + return FakeHealthStatus(t['id'], t['port'], self.healthcheck_port, 'healthy') + + +class FakeListener(BaseModel): + + def __init__(self, load_balancer_arn, arn, protocol, port, ssl_policy, certificate, default_actions): + self.load_balancer_arn = load_balancer_arn + self.arn = arn + self.protocol = protocol.upper() + self.port = port + self.ssl_policy = ssl_policy + self.certificate = certificate + self.default_actions = default_actions + + +class FakeBackend(BaseModel): + + def __init__(self, instance_port): + self.instance_port = instance_port + self.policy_names = [] + + def __repr__(self): + return "FakeBackend(inp: %s, policies: %s)" % (self.instance_port, self.policy_names) + + +class FakeLoadBalancer(BaseModel): + + def __init__(self, name, security_groups, subnets, vpc_id, arn, dns_name, scheme='internet-facing'): + self.name = name + self.created_time = datetime.datetime.now() + self.scheme = scheme + self.security_groups = security_groups + self.subnets = subnets or [] + self.vpc_id = vpc_id + self.listeners = OrderedDict() + self.tags = {} + self.arn = arn + self.dns_name = dns_name + + @property + def physical_resource_id(self): + return self.name + + def add_tag(self, key, value): + if len(self.tags) >= 10 and key not in self.tags: + raise TooManyTagsError() + self.tags[key] = value + + def list_tags(self): + return self.tags + + def remove_tag(self, key): + if key in self.tags: + del self.tags[key] + + def delete(self, region): + ''' Not exposed as part of the ELB API - used for CloudFormation. ''' + elbv2_backends[region].delete_load_balancer(self.arn) + + +class ELBv2Backend(BaseBackend): + + def __init__(self, region_name=None): + self.region_name = region_name + self.target_groups = OrderedDict() + self.load_balancers = OrderedDict() + + def reset(self): + region_name = self.region_name + self.__dict__ = {} + self.__init__(region_name) + + def create_load_balancer(self, name, security_groups, subnet_ids, scheme='internet-facing'): + vpc_id = None + ec2_backend = ec2_backends[self.region_name] + subnets = [] + if not subnet_ids: + raise SubnetNotFoundError() + for subnet_id in subnet_ids: + subnet = ec2_backend.get_subnet(subnet_id) + if subnet is None: + raise SubnetNotFoundError() + subnets.append(subnet) + + vpc_id = subnets[0].vpc_id + arn = "arn:aws:elasticloadbalancing:%s:1:loadbalancer/%s/50dc6c495c0c9188" % (self.region_name, name) + dns_name = "%s-1.%s.elb.amazonaws.com" % (name, self.region_name) + + if arn in self.load_balancers: + raise DuplicateLoadBalancerName() + + new_load_balancer = FakeLoadBalancer( + name=name, + security_groups=security_groups, + arn=arn, + scheme=scheme, + subnets=subnets, + vpc_id=vpc_id, + dns_name=dns_name) + self.load_balancers[arn] = new_load_balancer + return new_load_balancer + + def create_target_group(self, name, **kwargs): + for target_group in self.target_groups.values(): + if target_group.name == name: + raise DuplicateTargetGroupName() + + arn = "arn:aws:elasticloadbalancing:%s:1:targetgroup/%s/50dc6c495c0c9188" % (self.region_name, name) + target_group = FakeTargetGroup(name, arn, **kwargs) + self.target_groups[target_group.arn] = target_group + return target_group + + def create_listener(self, load_balancer_arn, protocol, port, ssl_policy, certificate, default_actions): + balancer = self.load_balancers.get(load_balancer_arn) + if balancer is None: + raise LoadBalancerNotFoundError() + if port in balancer.listeners: + raise DuplicateListenerError() + + arn = load_balancer_arn.replace(':loadbalancer/', ':listener/') + "/%s%s" % (port, id(self)) + listener = FakeListener(load_balancer_arn, arn, protocol, port, ssl_policy, certificate, default_actions) + balancer.listeners[listener.arn] = listener + return listener + + def describe_load_balancers(self, arns, names): + balancers = self.load_balancers.values() + arns = arns or [] + names = names or [] + if not arns and not names: + return balancers + + matched_balancers = [] + matched_balancer = None + + for arn in arns: + for balancer in balancers: + if balancer.arn == arn: + matched_balancer = balancer + if matched_balancer is None: + raise LoadBalancerNotFoundError() + elif matched_balancer not in matched_balancers: + matched_balancers.append(matched_balancer) + + for name in names: + for balancer in balancers: + if balancer.name == name: + matched_balancer = balancer + if matched_balancer is None: + raise LoadBalancerNotFoundError() + elif matched_balancer not in matched_balancers: + matched_balancers.append(matched_balancer) + + return matched_balancers + + def describe_target_groups(self, load_balancer_arn, target_group_arns, names): + if load_balancer_arn: + if load_balancer_arn not in self.load_balancers: + raise LoadBalancerNotFoundError() + return [tg for tg in self.target_groups.values() + if load_balancer_arn in tg.load_balancer_arns] + + if target_group_arns: + try: + return [self.target_groups[arn] for arn in target_group_arns] + except KeyError: + raise TargetGroupNotFoundError() + if names: + matched = [] + for name in names: + found = None + for target_group in self.target_groups: + if target_group.name == name: + found = target_group + if not found: + raise TargetGroupNotFoundError() + matched.append(found) + return matched + + return self.target_groups.values() + + def describe_listeners(self, load_balancer_arn, listener_arns): + if load_balancer_arn: + if load_balancer_arn not in self.load_balancers: + raise LoadBalancerNotFoundError() + return self.load_balancers.get(load_balancer_arn).listeners.values() + + matched = [] + for load_balancer in self.load_balancers.values(): + for listener_arn in listener_arns: + listener = load_balancer.listeners.get(listener_arn) + if not listener: + raise ListenerNotFoundError() + matched.append(listener) + return matched + + def delete_load_balancer(self, arn): + self.load_balancers.pop(arn, None) + + def delete_target_group(self, target_group_arn): + target_group = self.target_groups.pop(target_group_arn) + if target_group: + return target_group + raise TargetGroupNotFoundError() + + def delete_listener(self, listener_arn): + for load_balancer in self.load_balancers.values(): + listener = load_balancer.listeners.pop(listener_arn) + if listener: + return listener + raise ListenerNotFoundError() + + def register_targets(self, target_group_arn, instances): + target_group = self.target_groups.get(target_group_arn) + if target_group is None: + raise TargetGroupNotFoundError() + target_group.register(instances) + + def deregister_targets(self, target_group_arn, instances): + target_group = self.target_groups.get(target_group_arn) + if target_group is None: + raise TargetGroupNotFoundError() + target_group.deregister(instances) + + def describe_target_health(self, target_group_arn, targets): + target_group = self.target_groups.get(target_group_arn) + if target_group is None: + raise TargetGroupNotFoundError() + + if not targets: + targets = target_group.targets.values() + return [target_group.health_for(target) for target in targets] + + +elbv2_backends = {} +for region in ec2_backends.keys(): + elbv2_backends[region] = ELBv2Backend(region) diff --git a/moto/elbv2/responses.py b/moto/elbv2/responses.py new file mode 100644 index 000000000..585a413d4 --- /dev/null +++ b/moto/elbv2/responses.py @@ -0,0 +1,649 @@ +from __future__ import unicode_literals +from moto.core.responses import BaseResponse +from .models import elbv2_backends +from .exceptions import DuplicateTagKeysError, LoadBalancerNotFoundError + + +class ELBResponse(BaseResponse): + + @property + def elb_backend(self): + return elbv2_backends[self.region] + + def create_load_balancer(self): + load_balancer_name = self._get_param('Name') + subnet_ids = self._get_multi_param("Subnets.member") + security_groups = self._get_multi_param("SecurityGroups.member") + scheme = self._get_param('Scheme') + + load_balancer = self.elb_backend.create_load_balancer( + name=load_balancer_name, + security_groups=security_groups, + subnet_ids=subnet_ids, + scheme=scheme, + ) + self._add_tags(load_balancer) + template = self.response_template(CREATE_LOAD_BALANCER_TEMPLATE) + return template.render(load_balancer=load_balancer) + + def create_target_group(self): + name = self._get_param('Name') + vpc_id = self._get_param('VpcId') + protocol = self._get_param('Protocol') + port = self._get_param('Port') + healthcheck_protocol = self._get_param('HealthCheckProtocol', 'HTTP') + healthcheck_port = self._get_param('HealthCheckPort', 'traffic-port') + healthcheck_path = self._get_param('HealthCheckPath', '/') + healthcheck_interval_seconds = self._get_param('HealthCheckIntervalSeconds', '30') + healthcheck_timeout_seconds = self._get_param('HealthCheckTimeoutSeconds', '5') + healthy_threshold_count = self._get_param('HealthyThresholdCount', '5') + unhealthy_threshold_count = self._get_param('UnhealthyThresholdCount', '2') + + target_group = self.elb_backend.create_target_group( + 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, + ) + + template = self.response_template(CREATE_TARGET_GROUP_TEMPLATE) + return template.render(target_group=target_group) + + def create_listener(self): + load_balancer_arn = self._get_param('LoadBalancerArn') + protocol = self._get_param('Protocol') + port = self._get_param('Port') + ssl_policy = self._get_param('SslPolicy', 'ELBSecurityPolicy-2016-08') + certificates = self._get_list_prefix('Certificates.member') + if certificates: + certificate = certificates[0].get('certificate_arn') + else: + certificate = None + default_actions = self._get_list_prefix('DefaultActions.member') + + listener = self.elb_backend.create_listener( + load_balancer_arn=load_balancer_arn, + protocol=protocol, + port=port, + ssl_policy=ssl_policy, + certificate=certificate, + default_actions=default_actions) + + template = self.response_template(CREATE_LISTENER_TEMPLATE) + return template.render(listener=listener) + + def describe_load_balancers(self): + arns = self._get_multi_param("LoadBalancerArns.member") + names = self._get_multi_param("Names.member") + all_load_balancers = list(self.elb_backend.describe_load_balancers(arns, names)) + marker = self._get_param('Marker') + all_names = [balancer.name for balancer in all_load_balancers] + if marker: + start = all_names.index(marker) + 1 + else: + start = 0 + page_size = self._get_param('PageSize', 50) # the default is 400, but using 50 to make testing easier + load_balancers_resp = all_load_balancers[start:start + page_size] + next_marker = None + if len(all_load_balancers) > start + page_size: + next_marker = load_balancers_resp[-1].name + + template = self.response_template(DESCRIBE_LOAD_BALANCERS_TEMPLATE) + return template.render(load_balancers=load_balancers_resp, marker=next_marker) + + def describe_target_groups(self): + load_balancer_arn = self._get_param('LoadBalancerArn') + target_group_arns = self._get_multi_param('TargetGroupArns.member') + names = self._get_multi_param('Names.member') + + target_groups = self.elb_backend.describe_target_groups(load_balancer_arn, target_group_arns, names) + template = self.response_template(DESCRIBE_TARGET_GROUPS_TEMPLATE) + return template.render(target_groups=target_groups) + + def describe_listeners(self): + load_balancer_arn = self._get_param('LoadBalancerArn') + listener_arns = self._get_multi_param('ListenerArns.member') + if not load_balancer_arn and not listener_arns: + raise LoadBalancerNotFoundError() + + listeners = self.elb_backend.describe_listeners(load_balancer_arn, listener_arns) + template = self.response_template(DESCRIBE_LISTENERS_TEMPLATE) + return template.render(listeners=listeners) + + def delete_load_balancer(self): + arn = self._get_param('LoadBalancerArn') + self.elb_backend.delete_load_balancer(arn) + template = self.response_template(DELETE_LOAD_BALANCER_TEMPLATE) + return template.render() + + def delete_target_group(self): + arn = self._get_param('TargetGroupArn') + self.elb_backend.delete_target_group(arn) + template = self.response_template(DELETE_TARGET_GROUP_TEMPLATE) + return template.render() + + def delete_listener(self): + arn = self._get_param('ListenerArn') + self.elb_backend.delete_listener(arn) + template = self.response_template(DELETE_LISTENER_TEMPLATE) + return template.render() + + def register_targets(self): + target_group_arn = self._get_param('TargetGroupArn') + targets = self._get_list_prefix('Targets.member') + self.elb_backend.register_targets(target_group_arn, targets) + + template = self.response_template(REGISTER_TARGETS_TEMPLATE) + return template.render() + + def deregister_targets(self): + target_group_arn = self._get_param('TargetGroupArn') + targets = self._get_list_prefix('Targets.member') + self.elb_backend.deregister_targets(target_group_arn, targets) + + template = self.response_template(DEREGISTER_TARGETS_TEMPLATE) + return template.render() + + def describe_target_health(self): + target_group_arn = self._get_param('TargetGroupArn') + targets = self._get_list_prefix('Targets.member') + target_health_descriptions = self.elb_backend.describe_target_health(target_group_arn, targets) + + template = self.response_template(DESCRIBE_TARGET_HEALTH_TEMPLATE) + return template.render(target_health_descriptions=target_health_descriptions) + + def add_tags(self): + resource_arns = self._get_multi_param('ResourceArns.member') + + for arn in resource_arns: + load_balancer = self.elb_backend.load_balancers.get(arn) + if not load_balancer: + raise LoadBalancerNotFoundError() + self._add_tags(load_balancer) + + template = self.response_template(ADD_TAGS_TEMPLATE) + return template.render() + + def remove_tags(self): + resource_arns = self._get_multi_param('ResourceArns.member') + tag_keys = self._get_multi_param('TagKeys.member') + + for arn in resource_arns: + load_balancer = self.elb_backend.load_balancers.get(arn) + if not load_balancer: + raise LoadBalancerNotFoundError() + [load_balancer.remove_tag(key) for key in tag_keys] + + template = self.response_template(REMOVE_TAGS_TEMPLATE) + return template.render() + + def describe_tags(self): + elbs = [] + for key, value in self.querystring.items(): + if "ResourceArns.member" in key: + number = key.split('.')[2] + load_balancer_arn = self._get_param( + 'ResourceArns.member.{0}'.format(number)) + elb = self.elb_backend.load_balancers.get(load_balancer_arn) + if not elb: + raise LoadBalancerNotFoundError() + elbs.append(elb) + + template = self.response_template(DESCRIBE_TAGS_TEMPLATE) + return template.render(load_balancers=elbs) + + def _add_tags(self, elb): + tag_values = [] + tag_keys = [] + + for t_key, t_val in sorted(self.querystring.items()): + if t_key.startswith('Tags.member.'): + if t_key.split('.')[3] == 'Key': + tag_keys.extend(t_val) + elif t_key.split('.')[3] == 'Value': + tag_values.extend(t_val) + + counts = {} + for i in tag_keys: + counts[i] = tag_keys.count(i) + + counts = sorted(counts.items(), key=lambda i: i[1], reverse=True) + + if counts and counts[0][1] > 1: + # We have dupes... + raise DuplicateTagKeysError(counts[0]) + + for tag_key, tag_value in zip(tag_keys, tag_values): + elb.add_tag(tag_key, tag_value) + + +ADD_TAGS_TEMPLATE = """ + + + 360e81f7-1100-11e4-b6ed-0f30EXAMPLE + +""" + +REMOVE_TAGS_TEMPLATE = """ + + + 360e81f7-1100-11e4-b6ed-0f30EXAMPLE + +""" + +DESCRIBE_TAGS_TEMPLATE = """ + + + {% for load_balancer in load_balancers %} + + {{ load_balancer.arn }} + + {% for key, value in load_balancer.tags.items() %} + + {{ value }} + {{ key }} + + {% endfor %} + + + {% endfor %} + + + + 360e81f7-1100-11e4-b6ed-0f30EXAMPLE + +""" + + +CREATE_LOAD_BALANCER_TEMPLATE = """ + + + + {{ load_balancer.arn }} + {{ load_balancer.scheme }} + {{ load_balancer.name }} + {{ load_balancer.vpc_id }} + Z2P70J7EXAMPLE + {{ load_balancer.created_time }} + + {% for subnet in load_balancer.subnets %} + + {{ subnet.id }} + {{ subnet.availability_zone }} + + {% endfor %} + + + {% for security_group in load_balancer.security_groups %} + {{ security_group }} + {% endfor %} + + {{ load_balancer.dns_name }} + + provisioning + + application + + + + + 32d531b2-f2d0-11e5-9192-3fff33344cfa + +""" + +CREATE_TARGET_GROUP_TEMPLATE = """ + + + + {{ target_group.arn }} + {{ target_group.name }} + {{ target_group.protocol }} + {{ target_group.port }} + {{ target_group.vpc_id }} + {{ target_group.health_check_protocol }} + {{ target_group.healthcheck_port }} + {{ target_group.healthcheck_path }} + {{ target_group.healthcheck_interval_seconds }} + {{ target_group.healthcheck_timeout_seconds }} + {{ target_group.healthy_threshold_count }} + {{ target_group.unhealthy_threshold_count }} + + 200 + + + + + + b83fe90e-f2d5-11e5-b95d-3b2c1831fc26 + +""" + +CREATE_LISTENER_TEMPLATE = """ + + + + {{ listener.load_balancer_arn }} + {{ listener.protocol }} + {% if listener.certificate %} + + + {{ listener.certificate }} + + + {% endif %} + {{ listener.port }} + {{ listener.ssl_policy }} + {{ listener.arn }} + + {% for action in listener.default_actions %} + + {{ action.type }} + {{ action.target_group_arn }} + + {% endfor %} + + + + + + 97f1bb38-f390-11e5-b95d-3b2c1831fc26 + +""" + +DELETE_LOAD_BALANCER_TEMPLATE = """ + + + 1549581b-12b7-11e3-895e-1334aEXAMPLE + +""" + +DELETE_TARGET_GROUP_TEMPLATE = """ + + + 1549581b-12b7-11e3-895e-1334aEXAMPLE + +""" + +DELETE_LISTENER_TEMPLATE = """ + + + 1549581b-12b7-11e3-895e-1334aEXAMPLE + +""" + +DESCRIBE_LOAD_BALANCERS_TEMPLATE = """ + + + {% for load_balancer in load_balancers %} + + {{ load_balancer.arn }} + {{ load_balancer.scheme }} + {{ load_balancer.name }} + {{ load_balancer.vpc_id }} + Z2P70J7EXAMPLE + {{ load_balancer.created_time }} + + {% for subnet in load_balancer.subnets %} + + {{ subnet.id }} + {{ subnet.availability_zone }} + + {% endfor %} + + + {% for security_group in load_balancer.security_groups %} + {{ security_group }} + {% endfor %} + + {{ load_balancer.dns_name }} + + provisioning + + application + + {% endfor %} + + {% if marker %} + {{ marker }} + {% endif %} + + + f9880f01-7852-629d-a6c3-3ae2-666a409287e6dc0c + +""" + + +DESCRIBE_TARGET_GROUPS_TEMPLATE = """ + + + {% for target_group in target_groups %} + + {{ target_group.arn }} + {{ target_group.name }} + {{ target_group.protocol }} + {{ target_group.port }} + {{ target_group.vpc_id }} + {{ target_group.health_check_protocol }} + {{ target_group.healthcheck_port }} + {{ target_group.healthcheck_path }} + {{ target_group.healthcheck_interval_seconds }} + {{ target_group.healthcheck_timeout_seconds }} + {{ target_group.healthy_threshold_count }} + {{ target_group.unhealthy_threshold_count }} + + 200 + + + {% for load_balancer_arn in target_group.load_balancer_arns %} + {{ load_balancer_arn }} + {% endfor %} + + + {% endfor %} + + + + 70092c0e-f3a9-11e5-ae48-cff02092876b + +""" + + +DESCRIBE_LISTENERS_TEMPLATE = """ + + + {% for listener in listeners %} + + {{ listener.load_balancer_arn }} + {{ listener.protocol }} + {% if listener.certificate %} + + + {{ listener.certificate }} + + + {% endif %} + {{ listener.port }} + {{ listener.ssl_policy }} + {{ listener.arn }} + + {% for action in listener.default_actions %} + + {{ action.type }} + {{ action.target_group_arn }} + + {% endfor %} + + + {% endfor %} + + + + 65a3a7ea-f39c-11e5-b543-9f2c3fbb9bee + +""" + +CONFIGURE_HEALTH_CHECK_TEMPLATE = """ + + + {{ check.interval }} + {{ check.target }} + {{ check.healthy_threshold }} + {{ check.timeout }} + {{ check.unhealthy_threshold }} + + + + f9880f01-7852-629d-a6c3-3ae2-666a409287e6dc0c + +""" + +REGISTER_TARGETS_TEMPLATE = """ + + + + f9880f01-7852-629d-a6c3-3ae2-666a409287e6dc0c + +""" + +DEREGISTER_TARGETS_TEMPLATE = """ + + + + f9880f01-7852-629d-a6c3-3ae2-666a409287e6dc0c + +""" + +SET_LOAD_BALANCER_SSL_CERTIFICATE = """ + + + 83c88b9d-12b7-11e3-8b82-87b12EXAMPLE + +""" + + +DELETE_LOAD_BALANCER_LISTENERS = """ + + + 83c88b9d-12b7-11e3-8b82-87b12EXAMPLE + +""" + +DESCRIBE_ATTRIBUTES_TEMPLATE = """ + + + + {{ attributes.access_log.enabled }} + {% if attributes.access_log.enabled %} + {{ attributes.access_log.s3_bucket_name }} + {{ attributes.access_log.s3_bucket_prefix }} + {{ attributes.access_log.emit_interval }} + {% endif %} + + + {{ attributes.connecting_settings.idle_timeout }} + + + {{ attributes.cross_zone_load_balancing.enabled }} + + + {% if attributes.connection_draining.enabled %} + true + {{ attributes.connection_draining.timeout }} + {% else %} + false + {% endif %} + + + + + 83c88b9d-12b7-11e3-8b82-87b12EXAMPLE + + +""" + +MODIFY_ATTRIBUTES_TEMPLATE = """ + + {{ load_balancer.name }} + + + {{ attributes.access_log.enabled }} + {% if attributes.access_log.enabled %} + {{ attributes.access_log.s3_bucket_name }} + {{ attributes.access_log.s3_bucket_prefix }} + {{ attributes.access_log.emit_interval }} + {% endif %} + + + {{ attributes.connecting_settings.idle_timeout }} + + + {{ attributes.cross_zone_load_balancing.enabled }} + + + {% if attributes.connection_draining.enabled %} + true + {{ attributes.connection_draining.timeout }} + {% else %} + false + {% endif %} + + + + + 83c88b9d-12b7-11e3-8b82-87b12EXAMPLE + + +""" + +CREATE_LOAD_BALANCER_POLICY_TEMPLATE = """ + + + 83c88b9d-12b7-11e3-8b82-87b12EXAMPLE + + +""" + +SET_LOAD_BALANCER_POLICIES_OF_LISTENER_TEMPLATE = """ + + + 07b1ecbc-1100-11e3-acaf-dd7edEXAMPLE + + +""" + +SET_LOAD_BALANCER_POLICIES_FOR_BACKEND_SERVER_TEMPLATE = """ + + + 0eb9b381-dde0-11e2-8d78-6ddbaEXAMPLE + + +""" + +DESCRIBE_TARGET_HEALTH_TEMPLATE = """ + + + {% for target_health in target_health_descriptions %} + + {{ target_health.health_port }} + + {{ target_health.status }} + + + {{ target_health.port }} + {{ target_health.instance_id }} + + + {% endfor %} + + + + c534f810-f389-11e5-9192-3fff33344cfa + +""" diff --git a/moto/elbv2/urls.py b/moto/elbv2/urls.py new file mode 100644 index 000000000..48fcb37ab --- /dev/null +++ b/moto/elbv2/urls.py @@ -0,0 +1,10 @@ +from __future__ import unicode_literals +from .responses import ELBResponse + +url_bases = [ + "https?://elasticloadbalancing.(.+).amazonaws.com", +] + +url_paths = { + '{0}/$': ELBResponse.dispatch, +} diff --git a/tests/test_elbv2/test_elbv2.py b/tests/test_elbv2/test_elbv2.py new file mode 100644 index 000000000..c9eb9ea43 --- /dev/null +++ b/tests/test_elbv2/test_elbv2.py @@ -0,0 +1,447 @@ +from __future__ import unicode_literals +import boto3 +import botocore +from botocore.exceptions import ClientError +from nose.tools import assert_raises +import sure # noqa + +from moto import mock_elbv2, mock_ec2 + + +@mock_elbv2 +@mock_ec2 +def test_create_load_balancer(): + conn = boto3.client('elbv2', region_name='us-east-1') + ec2 = boto3.resource('ec2', region_name='us-east-1') + + security_group = ec2.create_security_group(GroupName='a-security-group', Description='First One') + vpc = ec2.create_vpc(CidrBlock='172.28.7.0/24', InstanceTenancy='default') + subnet1 = ec2.create_subnet(VpcId=vpc.id, CidrBlock='172.28.7.192/26', AvailabilityZone='us-east-1a') + subnet2 = ec2.create_subnet(VpcId=vpc.id, CidrBlock='172.28.7.192/26', AvailabilityZone='us-east-1b') + + response = conn.create_load_balancer( + Name='my-lb', + Subnets=[subnet1.id, subnet2.id], + SecurityGroups=[security_group.id], + Scheme='internal', + Tags=[{'Key': 'key_name', 'Value': 'a_value'}]) + + lb = response.get('LoadBalancers')[0] + + lb.get('DNSName').should.equal("my-lb-1.us-east-1.elb.amazonaws.com") + lb.get('LoadBalancerArn').should.equal('arn:aws:elasticloadbalancing:us-east-1:1:loadbalancer/my-lb/50dc6c495c0c9188') + lb.get('SecurityGroups').should.equal([security_group.id]) + lb.get('AvailabilityZones').should.equal([ + {'SubnetId': subnet1.id, 'ZoneName': 'us-east-1a'}, + {'SubnetId': subnet2.id, 'ZoneName': 'us-east-1b'}]) + + # Ensure the tags persisted + response = conn.describe_tags(ResourceArns=[lb.get('LoadBalancerArn')]) + tags = {d['Key']: d['Value'] for d in response['TagDescriptions'][0]['Tags']} + tags.should.equal({'key_name': 'a_value'}) + + +@mock_elbv2 +@mock_ec2 +def test_describe_load_balancers(): + conn = boto3.client('elbv2', region_name='us-east-1') + ec2 = boto3.resource('ec2', region_name='us-east-1') + + security_group = ec2.create_security_group(GroupName='a-security-group', Description='First One') + vpc = ec2.create_vpc(CidrBlock='172.28.7.0/24', InstanceTenancy='default') + subnet1 = ec2.create_subnet(VpcId=vpc.id, CidrBlock='172.28.7.192/26', AvailabilityZone='us-east-1a') + subnet2 = ec2.create_subnet(VpcId=vpc.id, CidrBlock='172.28.7.192/26', AvailabilityZone='us-east-1b') + + conn.create_load_balancer( + Name='my-lb', + Subnets=[subnet1.id, subnet2.id], + SecurityGroups=[security_group.id], + Scheme='internal', + Tags=[{'Key': 'key_name', 'Value': 'a_value'}]) + + response = conn.describe_load_balancers() + + response.get('LoadBalancers').should.have.length_of(1) + lb = response.get('LoadBalancers')[0] + lb.get('LoadBalancerName').should.equal('my-lb') + + response = conn.describe_load_balancers(LoadBalancerArns=[lb.get('LoadBalancerArn')]) + response.get('LoadBalancers')[0].get('LoadBalancerName').should.equal('my-lb') + + response = conn.describe_load_balancers(Names=['my-lb']) + response.get('LoadBalancers')[0].get('LoadBalancerName').should.equal('my-lb') + + with assert_raises(ClientError): + conn.describe_load_balancers(LoadBalancerArns=['not-a/real/arn']) + with assert_raises(ClientError): + conn.describe_load_balancers(Names=['nope']) + + +@mock_elbv2 +@mock_ec2 +def test_add_remove_tags(): + conn = boto3.client('elbv2', region_name='us-east-1') + + ec2 = boto3.resource('ec2', region_name='us-east-1') + + security_group = ec2.create_security_group(GroupName='a-security-group', Description='First One') + vpc = ec2.create_vpc(CidrBlock='172.28.7.0/24', InstanceTenancy='default') + subnet1 = ec2.create_subnet(VpcId=vpc.id, CidrBlock='172.28.7.192/26', AvailabilityZone='us-east-1a') + subnet2 = ec2.create_subnet(VpcId=vpc.id, CidrBlock='172.28.7.192/26', AvailabilityZone='us-east-1b') + + conn.create_load_balancer( + Name='my-lb', + Subnets=[subnet1.id, subnet2.id], + SecurityGroups=[security_group.id], + Scheme='internal', + Tags=[{'Key': 'key_name', 'Value': 'a_value'}]) + + lbs = conn.describe_load_balancers()['LoadBalancers'] + lbs.should.have.length_of(1) + lb = lbs[0] + + with assert_raises(ClientError): + conn.add_tags(ResourceArns=['missing-arn'], + Tags=[{ + 'Key': 'a', + 'Value': 'b' + }]) + + conn.add_tags(ResourceArns=[lb.get('LoadBalancerArn')], + Tags=[{ + 'Key': 'a', + 'Value': 'b' + }]) + + tags = {d['Key']: d['Value'] for d in conn.describe_tags( + ResourceArns=[lb.get('LoadBalancerArn')])['TagDescriptions'][0]['Tags']} + tags.should.have.key('a').which.should.equal('b') + + conn.add_tags(ResourceArns=[lb.get('LoadBalancerArn')], + Tags=[{ + 'Key': 'a', + 'Value': 'b' + }, { + 'Key': 'b', + 'Value': 'b' + }, { + 'Key': 'c', + 'Value': 'b' + }, { + 'Key': 'd', + 'Value': 'b' + }, { + 'Key': 'e', + 'Value': 'b' + }, { + 'Key': 'f', + 'Value': 'b' + }, { + 'Key': 'g', + 'Value': 'b' + }, { + 'Key': 'h', + 'Value': 'b' + }, { + 'Key': 'j', + 'Value': 'b' + }]) + + conn.add_tags.when.called_with(ResourceArns=[lb.get('LoadBalancerArn')], + Tags=[{ + 'Key': 'k', + 'Value': 'b' + }]).should.throw(botocore.exceptions.ClientError) + + conn.add_tags(ResourceArns=[lb.get('LoadBalancerArn')], + Tags=[{ + 'Key': 'j', + 'Value': 'c' + }]) + + tags = {d['Key']: d['Value'] for d in conn.describe_tags( + ResourceArns=[lb.get('LoadBalancerArn')])['TagDescriptions'][0]['Tags']} + + tags.should.have.key('a').which.should.equal('b') + tags.should.have.key('b').which.should.equal('b') + tags.should.have.key('c').which.should.equal('b') + tags.should.have.key('d').which.should.equal('b') + tags.should.have.key('e').which.should.equal('b') + tags.should.have.key('f').which.should.equal('b') + tags.should.have.key('g').which.should.equal('b') + tags.should.have.key('h').which.should.equal('b') + tags.should.have.key('j').which.should.equal('c') + tags.shouldnt.have.key('k') + + conn.remove_tags(ResourceArns=[lb.get('LoadBalancerArn')], + TagKeys=['a']) + + tags = {d['Key']: d['Value'] for d in conn.describe_tags( + ResourceArns=[lb.get('LoadBalancerArn')])['TagDescriptions'][0]['Tags']} + + tags.shouldnt.have.key('a') + tags.should.have.key('b').which.should.equal('b') + tags.should.have.key('c').which.should.equal('b') + tags.should.have.key('d').which.should.equal('b') + tags.should.have.key('e').which.should.equal('b') + tags.should.have.key('f').which.should.equal('b') + tags.should.have.key('g').which.should.equal('b') + tags.should.have.key('h').which.should.equal('b') + tags.should.have.key('j').which.should.equal('c') + + +@mock_elbv2 +@mock_ec2 +def test_create_elb_in_multiple_region(): + for region in ['us-west-1', 'us-west-2']: + conn = boto3.client('elbv2', region_name=region) + ec2 = boto3.resource('ec2', region_name=region) + + security_group = ec2.create_security_group(GroupName='a-security-group', Description='First One') + vpc = ec2.create_vpc(CidrBlock='172.28.7.0/24', InstanceTenancy='default') + subnet1 = ec2.create_subnet(VpcId=vpc.id, CidrBlock='172.28.7.192/26', AvailabilityZone=region + 'a') + subnet2 = ec2.create_subnet(VpcId=vpc.id, CidrBlock='172.28.7.192/26', AvailabilityZone=region + 'b') + + conn.create_load_balancer( + Name='my-lb', + Subnets=[subnet1.id, subnet2.id], + SecurityGroups=[security_group.id], + Scheme='internal', + Tags=[{'Key': 'key_name', 'Value': 'a_value'}]) + + list( + boto3.client('elbv2', region_name='us-west-1').describe_load_balancers().get('LoadBalancers') + ).should.have.length_of(1) + list( + boto3.client('elbv2', region_name='us-west-2').describe_load_balancers().get('LoadBalancers') + ).should.have.length_of(1) + + +@mock_elbv2 +@mock_ec2 +def test_create_target_group_and_listeners(): + conn = boto3.client('elbv2', region_name='us-east-1') + ec2 = boto3.resource('ec2', region_name='us-east-1') + + security_group = ec2.create_security_group(GroupName='a-security-group', Description='First One') + vpc = ec2.create_vpc(CidrBlock='172.28.7.0/24', InstanceTenancy='default') + subnet1 = ec2.create_subnet(VpcId=vpc.id, CidrBlock='172.28.7.192/26', AvailabilityZone='us-east-1a') + subnet2 = ec2.create_subnet(VpcId=vpc.id, CidrBlock='172.28.7.192/26', AvailabilityZone='us-east-1b') + + response = conn.create_load_balancer( + Name='my-lb', + Subnets=[subnet1.id, subnet2.id], + SecurityGroups=[security_group.id], + Scheme='internal', + Tags=[{'Key': 'key_name', 'Value': 'a_value'}]) + + load_balancer_arn = response.get('LoadBalancers')[0].get('LoadBalancerArn') + + response = conn.create_target_group( + Name='a-target', + Protocol='HTTP', + Port=8080, + VpcId=vpc.id, + HealthCheckProtocol='HTTP', + HealthCheckPort='8080', + HealthCheckPath='/', + HealthCheckIntervalSeconds=5, + HealthCheckTimeoutSeconds=5, + HealthyThresholdCount=5, + UnhealthyThresholdCount=2, + Matcher={'HttpCode': '200'}) + target_group = response.get('TargetGroups')[0] + + # Check it's in the describe_target_groups response + response = conn.describe_target_groups() + response.get('TargetGroups').should.have.length_of(1) + + # Plain HTTP listener + response = conn.create_listener( + LoadBalancerArn=load_balancer_arn, + Protocol='HTTP', + Port=80, + DefaultActions=[{'Type': 'forward', 'TargetGroupArn': target_group.get('TargetGroupArn')}]) + listener = response.get('Listeners')[0] + listener.get('Port').should.equal(80) + listener.get('Protocol').should.equal('HTTP') + listener.get('DefaultActions').should.equal([{ + 'TargetGroupArn': target_group.get('TargetGroupArn'), + 'Type': 'forward'}]) + http_listener_arn = listener.get('ListenerArn') + + # And another with SSL + response = conn.create_listener( + LoadBalancerArn=load_balancer_arn, + Protocol='HTTPS', + Port=443, + Certificates=[{'CertificateArn': 'arn:aws:iam:123456789012:server-certificate/test-cert'}], + DefaultActions=[{'Type': 'forward', 'TargetGroupArn': target_group.get('TargetGroupArn')}]) + listener = response.get('Listeners')[0] + listener.get('Port').should.equal(443) + listener.get('Protocol').should.equal('HTTPS') + listener.get('Certificates').should.equal([{ + 'CertificateArn': 'arn:aws:iam:123456789012:server-certificate/test-cert', + }]) + listener.get('DefaultActions').should.equal([{ + 'TargetGroupArn': target_group.get('TargetGroupArn'), + 'Type': 'forward'}]) + + https_listener_arn = listener.get('ListenerArn') + + response = conn.describe_listeners(LoadBalancerArn=load_balancer_arn) + response.get('Listeners').should.have.length_of(2) + response = conn.describe_listeners(ListenerArns=[https_listener_arn]) + response.get('Listeners').should.have.length_of(1) + listener = response.get('Listeners')[0] + listener.get('Port').should.equal(443) + listener.get('Protocol').should.equal('HTTPS') + + response = conn.describe_listeners(ListenerArns=[http_listener_arn, https_listener_arn]) + response.get('Listeners').should.have.length_of(2) + + # Delete one listener + response = conn.describe_listeners(LoadBalancerArn=load_balancer_arn) + response.get('Listeners').should.have.length_of(2) + conn.delete_listener(ListenerArn=http_listener_arn) + response = conn.describe_listeners(LoadBalancerArn=load_balancer_arn) + response.get('Listeners').should.have.length_of(1) + + # Then delete the load balancer + conn.delete_load_balancer(LoadBalancerArn=load_balancer_arn) + + # It's gone + response = conn.describe_load_balancers() + response.get('LoadBalancers').should.have.length_of(0) + + # And it deleted the remaining listener + response = conn.describe_listeners(ListenerArns=[http_listener_arn, https_listener_arn]) + response.get('Listeners').should.have.length_of(0) + + # But not the target groups + response = conn.describe_target_groups() + response.get('TargetGroups').should.have.length_of(1) + + # Which we'll now delete + conn.delete_target_group(TargetGroupArn=target_group.get('TargetGroupArn')) + response = conn.describe_target_groups() + response.get('TargetGroups').should.have.length_of(0) + + +@mock_elbv2 +@mock_ec2 +def test_describe_paginated_balancers(): + conn = boto3.client('elbv2', region_name='us-east-1') + ec2 = boto3.resource('ec2', region_name='us-east-1') + + security_group = ec2.create_security_group(GroupName='a-security-group', Description='First One') + vpc = ec2.create_vpc(CidrBlock='172.28.7.0/24', InstanceTenancy='default') + subnet1 = ec2.create_subnet(VpcId=vpc.id, CidrBlock='172.28.7.192/26', AvailabilityZone='us-east-1a') + subnet2 = ec2.create_subnet(VpcId=vpc.id, CidrBlock='172.28.7.192/26', AvailabilityZone='us-east-1b') + + for i in range(51): + conn.create_load_balancer( + Name='my-lb%d' % i, + Subnets=[subnet1.id, subnet2.id], + SecurityGroups=[security_group.id], + Scheme='internal', + Tags=[{'Key': 'key_name', 'Value': 'a_value'}]) + + resp = conn.describe_load_balancers() + resp['LoadBalancers'].should.have.length_of(50) + resp['NextMarker'].should.equal(resp['LoadBalancers'][-1]['LoadBalancerName']) + resp2 = conn.describe_load_balancers(Marker=resp['NextMarker']) + resp2['LoadBalancers'].should.have.length_of(1) + assert 'NextToken' not in resp2.keys() + + +@mock_elbv2 +@mock_ec2 +def test_delete_load_balancer(): + conn = boto3.client('elbv2', region_name='us-east-1') + ec2 = boto3.resource('ec2', region_name='us-east-1') + + security_group = ec2.create_security_group(GroupName='a-security-group', Description='First One') + vpc = ec2.create_vpc(CidrBlock='172.28.7.0/24', InstanceTenancy='default') + subnet1 = ec2.create_subnet(VpcId=vpc.id, CidrBlock='172.28.7.192/26', AvailabilityZone='us-east-1a') + subnet2 = ec2.create_subnet(VpcId=vpc.id, CidrBlock='172.28.7.192/26', AvailabilityZone='us-east-1b') + + response = conn.create_load_balancer( + Name='my-lb', + Subnets=[subnet1.id, subnet2.id], + SecurityGroups=[security_group.id], + Scheme='internal', + Tags=[{'Key': 'key_name', 'Value': 'a_value'}]) + + response.get('LoadBalancers').should.have.length_of(1) + lb = response.get('LoadBalancers')[0] + + conn.delete_load_balancer(LoadBalancerArn=lb.get('LoadBalancerArn')) + balancers = conn.describe_load_balancers().get('LoadBalancers') + balancers.should.have.length_of(0) + + +@mock_ec2 +@mock_elbv2 +def test_register_targets(): + conn = boto3.client('elbv2', region_name='us-east-1') + ec2 = boto3.resource('ec2', region_name='us-east-1') + + security_group = ec2.create_security_group(GroupName='a-security-group', Description='First One') + vpc = ec2.create_vpc(CidrBlock='172.28.7.0/24', InstanceTenancy='default') + subnet1 = ec2.create_subnet(VpcId=vpc.id, CidrBlock='172.28.7.192/26', AvailabilityZone='us-east-1a') + subnet2 = ec2.create_subnet(VpcId=vpc.id, CidrBlock='172.28.7.192/26', AvailabilityZone='us-east-1b') + + conn.create_load_balancer( + Name='my-lb', + Subnets=[subnet1.id, subnet2.id], + SecurityGroups=[security_group.id], + Scheme='internal', + Tags=[{'Key': 'key_name', 'Value': 'a_value'}]) + + response = conn.create_target_group( + Name='a-target', + Protocol='HTTP', + Port=8080, + VpcId=vpc.id, + HealthCheckProtocol='HTTP', + HealthCheckPort='8080', + HealthCheckPath='/', + HealthCheckIntervalSeconds=5, + HealthCheckTimeoutSeconds=5, + HealthyThresholdCount=5, + UnhealthyThresholdCount=2, + Matcher={'HttpCode': '200'}) + target_group = response.get('TargetGroups')[0] + + # No targets registered yet + response = conn.describe_target_health(TargetGroupArn=target_group.get('TargetGroupArn')) + response.get('TargetHealthDescriptions').should.have.length_of(0) + + response = ec2.create_instances( + ImageId='ami-1234abcd', MinCount=2, MaxCount=2) + instance_id1 = response[0].id + instance_id2 = response[1].id + + response = conn.register_targets( + TargetGroupArn=target_group.get('TargetGroupArn'), + Targets=[ + { + 'Id': instance_id1, + 'Port': 5060, + }, + { + 'Id': instance_id2, + 'Port': 4030, + }, + ]) + + response = conn.describe_target_health(TargetGroupArn=target_group.get('TargetGroupArn')) + response.get('TargetHealthDescriptions').should.have.length_of(2) + + response = conn.deregister_targets( + TargetGroupArn=target_group.get('TargetGroupArn'), + Targets=[{'Id': instance_id2}]) + + response = conn.describe_target_health(TargetGroupArn=target_group.get('TargetGroupArn')) + response.get('TargetHealthDescriptions').should.have.length_of(1) diff --git a/tests/test_elbv2/test_server.py b/tests/test_elbv2/test_server.py new file mode 100644 index 000000000..6dc271920 --- /dev/null +++ b/tests/test_elbv2/test_server.py @@ -0,0 +1,17 @@ +from __future__ import unicode_literals +import sure # noqa + +import moto.server as server + +''' +Test the different server responses +''' + + +def test_elb_describe_instances(): + backend = server.create_backend_app("elbv2") + test_client = backend.test_client() + + res = test_client.get('/?Action=DescribeLoadBalancers') + + res.data.should.contain(b'DescribeLoadBalancersResponse') From ee6d2537004a2591ec3fd5466a41aa1ff30eb737 Mon Sep 17 00:00:00 2001 From: Jack Danger Date: Fri, 21 Jul 2017 16:28:56 -0700 Subject: [PATCH 2/9] updating reference in server test --- tests/test_elbv2/test_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_elbv2/test_server.py b/tests/test_elbv2/test_server.py index 6dc271920..05786104d 100644 --- a/tests/test_elbv2/test_server.py +++ b/tests/test_elbv2/test_server.py @@ -8,7 +8,7 @@ Test the different server responses ''' -def test_elb_describe_instances(): +def test_elbv2_describe_instances(): backend = server.create_backend_app("elbv2") test_client = backend.test_client() From 5cd1e2450d50917202c6648fb5befb34ef47a944 Mon Sep 17 00:00:00 2001 From: Jack Danger Date: Sun, 23 Jul 2017 22:06:55 -0700 Subject: [PATCH 3/9] adding elbv2 backend --- moto/backends.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/moto/backends.py b/moto/backends.py index 0af4ae2e2..b452b45fd 100644 --- a/moto/backends.py +++ b/moto/backends.py @@ -13,6 +13,7 @@ from moto.ec2 import ec2_backends from moto.ecr import ecr_backends from moto.ecs import ecs_backends from moto.elb import elb_backends +from moto.elbv2 import elbv2_backends from moto.emr import emr_backends from moto.events import events_backends from moto.glacier import glacier_backends @@ -43,6 +44,7 @@ BACKENDS = { 'ecr': ecr_backends, 'ecs': ecs_backends, 'elb': elb_backends, + 'elbv2': elbv2_backends, 'events': events_backends, 'emr': emr_backends, 'glacier': glacier_backends, From ce392fab79ba8a160f34c9ad90025ab3ccaefe98 Mon Sep 17 00:00:00 2001 From: Jack Danger Date: Tue, 1 Aug 2017 18:12:36 -0700 Subject: [PATCH 4/9] Properly dispatch by api version in server mode I'm not happy with this solution. Please think of a fix if you're reading this. --- moto/elb/urls.py | 30 +++++++++++++++++++++++++++++- moto/elbv2/responses.py | 34 +++++++++++++++++----------------- moto/elbv2/urls.py | 10 +++------- 3 files changed, 49 insertions(+), 25 deletions(-) diff --git a/moto/elb/urls.py b/moto/elb/urls.py index 48fcb37ab..a81ebc3e0 100644 --- a/moto/elb/urls.py +++ b/moto/elb/urls.py @@ -1,10 +1,38 @@ from __future__ import unicode_literals from .responses import ELBResponse +from moto.elbv2.responses import ELBV2Response + + +def api_version_elb_backend(*args, **kwargs): + """ + ELB and ELBV2 (Classic and Application load balancers) use the same + hostname and url space. To differentiate them we must read the + `Version` parameter out of the url-encoded request body. TODO: There + has _got_ to be a better way to do this. Please help us think of + one. + """ + request = args[0] + + if hasattr(request, 'values'): + # boto3 + version = request.values.get('Version') + else: + # boto + request.parse_request() + version = request.querystring.get('Version')[0] + + if '2012-06-01' == version: + return ELBResponse.dispatch(*args, **kwargs) + elif '2015-12-01' == version: + return ELBV2Response.dispatch(*args, **kwargs) + else: + raise Exception("Unknown ELB API version: {}".format(version)) + url_bases = [ "https?://elasticloadbalancing.(.+).amazonaws.com", ] url_paths = { - '{0}/$': ELBResponse.dispatch, + '{0}/$': api_version_elb_backend, } diff --git a/moto/elbv2/responses.py b/moto/elbv2/responses.py index 585a413d4..c000dd0c9 100644 --- a/moto/elbv2/responses.py +++ b/moto/elbv2/responses.py @@ -4,10 +4,10 @@ from .models import elbv2_backends from .exceptions import DuplicateTagKeysError, LoadBalancerNotFoundError -class ELBResponse(BaseResponse): +class ELBV2Response(BaseResponse): @property - def elb_backend(self): + def elbv2_backend(self): return elbv2_backends[self.region] def create_load_balancer(self): @@ -16,7 +16,7 @@ class ELBResponse(BaseResponse): security_groups = self._get_multi_param("SecurityGroups.member") scheme = self._get_param('Scheme') - load_balancer = self.elb_backend.create_load_balancer( + load_balancer = self.elbv2_backend.create_load_balancer( name=load_balancer_name, security_groups=security_groups, subnet_ids=subnet_ids, @@ -39,7 +39,7 @@ class ELBResponse(BaseResponse): healthy_threshold_count = self._get_param('HealthyThresholdCount', '5') unhealthy_threshold_count = self._get_param('UnhealthyThresholdCount', '2') - target_group = self.elb_backend.create_target_group( + target_group = self.elbv2_backend.create_target_group( name, vpc_id=vpc_id, protocol=protocol, @@ -68,7 +68,7 @@ class ELBResponse(BaseResponse): certificate = None default_actions = self._get_list_prefix('DefaultActions.member') - listener = self.elb_backend.create_listener( + listener = self.elbv2_backend.create_listener( load_balancer_arn=load_balancer_arn, protocol=protocol, port=port, @@ -82,7 +82,7 @@ class ELBResponse(BaseResponse): def describe_load_balancers(self): arns = self._get_multi_param("LoadBalancerArns.member") names = self._get_multi_param("Names.member") - all_load_balancers = list(self.elb_backend.describe_load_balancers(arns, names)) + all_load_balancers = list(self.elbv2_backend.describe_load_balancers(arns, names)) marker = self._get_param('Marker') all_names = [balancer.name for balancer in all_load_balancers] if marker: @@ -103,7 +103,7 @@ class ELBResponse(BaseResponse): target_group_arns = self._get_multi_param('TargetGroupArns.member') names = self._get_multi_param('Names.member') - target_groups = self.elb_backend.describe_target_groups(load_balancer_arn, target_group_arns, names) + target_groups = self.elbv2_backend.describe_target_groups(load_balancer_arn, target_group_arns, names) template = self.response_template(DESCRIBE_TARGET_GROUPS_TEMPLATE) return template.render(target_groups=target_groups) @@ -113,32 +113,32 @@ class ELBResponse(BaseResponse): if not load_balancer_arn and not listener_arns: raise LoadBalancerNotFoundError() - listeners = self.elb_backend.describe_listeners(load_balancer_arn, listener_arns) + listeners = self.elbv2_backend.describe_listeners(load_balancer_arn, listener_arns) template = self.response_template(DESCRIBE_LISTENERS_TEMPLATE) return template.render(listeners=listeners) def delete_load_balancer(self): arn = self._get_param('LoadBalancerArn') - self.elb_backend.delete_load_balancer(arn) + self.elbv2_backend.delete_load_balancer(arn) template = self.response_template(DELETE_LOAD_BALANCER_TEMPLATE) return template.render() def delete_target_group(self): arn = self._get_param('TargetGroupArn') - self.elb_backend.delete_target_group(arn) + self.elbv2_backend.delete_target_group(arn) template = self.response_template(DELETE_TARGET_GROUP_TEMPLATE) return template.render() def delete_listener(self): arn = self._get_param('ListenerArn') - self.elb_backend.delete_listener(arn) + self.elbv2_backend.delete_listener(arn) template = self.response_template(DELETE_LISTENER_TEMPLATE) return template.render() def register_targets(self): target_group_arn = self._get_param('TargetGroupArn') targets = self._get_list_prefix('Targets.member') - self.elb_backend.register_targets(target_group_arn, targets) + self.elbv2_backend.register_targets(target_group_arn, targets) template = self.response_template(REGISTER_TARGETS_TEMPLATE) return template.render() @@ -146,7 +146,7 @@ class ELBResponse(BaseResponse): def deregister_targets(self): target_group_arn = self._get_param('TargetGroupArn') targets = self._get_list_prefix('Targets.member') - self.elb_backend.deregister_targets(target_group_arn, targets) + self.elbv2_backend.deregister_targets(target_group_arn, targets) template = self.response_template(DEREGISTER_TARGETS_TEMPLATE) return template.render() @@ -154,7 +154,7 @@ class ELBResponse(BaseResponse): def describe_target_health(self): target_group_arn = self._get_param('TargetGroupArn') targets = self._get_list_prefix('Targets.member') - target_health_descriptions = self.elb_backend.describe_target_health(target_group_arn, targets) + target_health_descriptions = self.elbv2_backend.describe_target_health(target_group_arn, targets) template = self.response_template(DESCRIBE_TARGET_HEALTH_TEMPLATE) return template.render(target_health_descriptions=target_health_descriptions) @@ -163,7 +163,7 @@ class ELBResponse(BaseResponse): resource_arns = self._get_multi_param('ResourceArns.member') for arn in resource_arns: - load_balancer = self.elb_backend.load_balancers.get(arn) + load_balancer = self.elbv2_backend.load_balancers.get(arn) if not load_balancer: raise LoadBalancerNotFoundError() self._add_tags(load_balancer) @@ -176,7 +176,7 @@ class ELBResponse(BaseResponse): tag_keys = self._get_multi_param('TagKeys.member') for arn in resource_arns: - load_balancer = self.elb_backend.load_balancers.get(arn) + load_balancer = self.elbv2_backend.load_balancers.get(arn) if not load_balancer: raise LoadBalancerNotFoundError() [load_balancer.remove_tag(key) for key in tag_keys] @@ -191,7 +191,7 @@ class ELBResponse(BaseResponse): number = key.split('.')[2] load_balancer_arn = self._get_param( 'ResourceArns.member.{0}'.format(number)) - elb = self.elb_backend.load_balancers.get(load_balancer_arn) + elb = self.elbv2_backend.load_balancers.get(load_balancer_arn) if not elb: raise LoadBalancerNotFoundError() elbs.append(elb) diff --git a/moto/elbv2/urls.py b/moto/elbv2/urls.py index 48fcb37ab..ff72e3605 100644 --- a/moto/elbv2/urls.py +++ b/moto/elbv2/urls.py @@ -1,10 +1,6 @@ from __future__ import unicode_literals -from .responses import ELBResponse +from .responses import ELBV2Response -url_bases = [ - "https?://elasticloadbalancing.(.+).amazonaws.com", -] +url_bases = [] -url_paths = { - '{0}/$': ELBResponse.dispatch, -} +url_paths = {} From d56c30932f8e2c3a50b4a01f9381887f727fc9c8 Mon Sep 17 00:00:00 2001 From: Jack Danger Date: Tue, 1 Aug 2017 18:19:26 -0700 Subject: [PATCH 5/9] this is handled in moto/elb/urls.py --- moto/elbv2/urls.py | 1 - 1 file changed, 1 deletion(-) diff --git a/moto/elbv2/urls.py b/moto/elbv2/urls.py index ff72e3605..13e04a224 100644 --- a/moto/elbv2/urls.py +++ b/moto/elbv2/urls.py @@ -1,5 +1,4 @@ from __future__ import unicode_literals -from .responses import ELBV2Response url_bases = [] From 8188fea0ced1e75610b835745d78c6a18532fd02 Mon Sep 17 00:00:00 2001 From: Jack Danger Date: Tue, 1 Aug 2017 18:26:38 -0700 Subject: [PATCH 6/9] This is required for the server test to work --- moto/elbv2/urls.py | 9 +++++++-- tests/test_elbv2/test_server.py | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/moto/elbv2/urls.py b/moto/elbv2/urls.py index 13e04a224..13a8e056f 100644 --- a/moto/elbv2/urls.py +++ b/moto/elbv2/urls.py @@ -1,5 +1,10 @@ from __future__ import unicode_literals +from .responses import ELBV2Response -url_bases = [] +url_bases = [ + "https?://elasticloadbalancing.(.+).amazonaws.com", +] -url_paths = {} +url_paths = { + '{0}/$': ELBV2Response.dispatch, +} diff --git a/tests/test_elbv2/test_server.py b/tests/test_elbv2/test_server.py index 05786104d..5acad4051 100644 --- a/tests/test_elbv2/test_server.py +++ b/tests/test_elbv2/test_server.py @@ -8,7 +8,7 @@ Test the different server responses ''' -def test_elbv2_describe_instances(): +def test_elbv2_describe_load_balancers(): backend = server.create_backend_app("elbv2") test_client = backend.test_client() From 2f05f6c9eaec6eeae5b6d47c5ede92a7c70f8723 Mon Sep 17 00:00:00 2001 From: Jack Danger Date: Wed, 2 Aug 2017 13:29:14 -0700 Subject: [PATCH 7/9] Adding version string to server tests --- tests/test_elb/test_server.py | 2 +- tests/test_elbv2/test_server.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_elb/test_server.py b/tests/test_elb/test_server.py index 04b12524e..0033284d7 100644 --- a/tests/test_elb/test_server.py +++ b/tests/test_elb/test_server.py @@ -12,6 +12,6 @@ def test_elb_describe_instances(): backend = server.create_backend_app("elb") test_client = backend.test_client() - res = test_client.get('/?Action=DescribeLoadBalancers') + res = test_client.get('/?Action=DescribeLoadBalancers&Version=2015-12-01') res.data.should.contain(b'DescribeLoadBalancersResponse') diff --git a/tests/test_elbv2/test_server.py b/tests/test_elbv2/test_server.py index 5acad4051..ddd40a02d 100644 --- a/tests/test_elbv2/test_server.py +++ b/tests/test_elbv2/test_server.py @@ -12,6 +12,6 @@ def test_elbv2_describe_load_balancers(): backend = server.create_backend_app("elbv2") test_client = backend.test_client() - res = test_client.get('/?Action=DescribeLoadBalancers') + res = test_client.get('/?Action=DescribeLoadBalancers&Version=2015-12-01') res.data.should.contain(b'DescribeLoadBalancersResponse') From 08a932f5f104a9174d2118fafc0c95e812161936 Mon Sep 17 00:00:00 2001 From: Jack Danger Date: Wed, 2 Aug 2017 13:29:26 -0700 Subject: [PATCH 8/9] handling AWSPreparedRequest instances in dispatch --- moto/elb/urls.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/moto/elb/urls.py b/moto/elb/urls.py index a81ebc3e0..6b754fcce 100644 --- a/moto/elb/urls.py +++ b/moto/elb/urls.py @@ -1,5 +1,8 @@ from __future__ import unicode_literals -from .responses import ELBResponse +from six.moves.urllib.parse import parse_qs +from botocore.awsrequest import AWSPreparedRequest + +from moto.elb.responses import ELBResponse from moto.elbv2.responses import ELBV2Response @@ -16,6 +19,9 @@ def api_version_elb_backend(*args, **kwargs): if hasattr(request, 'values'): # boto3 version = request.values.get('Version') + elif isinstance(request, AWSPreparedRequest): + # botocore + version = parse_qs(request.body).get('Version')[0] else: # boto request.parse_request() From 161a187ee595ff8cf2061b02c4e9951a9301f23d Mon Sep 17 00:00:00 2001 From: Jack Danger Date: Wed, 2 Aug 2017 13:30:07 -0700 Subject: [PATCH 9/9] updating explanation of boto client usage --- moto/elb/urls.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/moto/elb/urls.py b/moto/elb/urls.py index 6b754fcce..3d96e1892 100644 --- a/moto/elb/urls.py +++ b/moto/elb/urls.py @@ -20,10 +20,10 @@ def api_version_elb_backend(*args, **kwargs): # boto3 version = request.values.get('Version') elif isinstance(request, AWSPreparedRequest): - # botocore + # boto in-memory version = parse_qs(request.body).get('Version')[0] else: - # boto + # boto in server mode request.parse_request() version = request.querystring.get('Version')[0]