From 05a2715f4b7f273f6f698d21a9127c068dd2c5df Mon Sep 17 00:00:00 2001 From: Toshiya Kawasaki Date: Wed, 16 Aug 2017 21:09:14 +0900 Subject: [PATCH 01/10] add create_rule to elbv2 --- moto/elbv2/exceptions.py | 41 +++++++ moto/elbv2/models.py | 80 +++++++++++++ moto/elbv2/responses.py | 61 ++++++++++ tests/test_elbv2/test_elbv2.py | 204 +++++++++++++++++++++++++++++++++ 4 files changed, 386 insertions(+) diff --git a/moto/elbv2/exceptions.py b/moto/elbv2/exceptions.py index 397aa115b..a03cf9a98 100644 --- a/moto/elbv2/exceptions.py +++ b/moto/elbv2/exceptions.py @@ -101,3 +101,44 @@ class EmptyListenersError(ELBClientError): super(EmptyListenersError, self).__init__( "ValidationError", "Listeners cannot be empty") + + +class PriorityInUseError(ELBClientError): + + def __init__(self): + super(PriorityInUseError, self).__init__( + "PriorityInUse", + "The specified priority is in use.") + + +class InvalidConditionFieldError(ELBClientError): + + def __init__(self, invalid_name): + super(InvalidConditionFieldError, self).__init__( + "ValidationError", + "Condition field '%s' must be one of '[path-pattern, host-header]" % (invalid_name)) + + +class InvalidConditionValueError(ELBClientError): + + def __init__(self, msg): + super(InvalidConditionValueError, self).__init__( + "ValidationError", msg) + + +class InvalidActionTypeError(ELBClientError): + + def __init__(self, invalid_name, index): + super(InvalidActionTypeError, self).__init__( + "ValidationError", + "1 validation error detected: Value '%s' at 'actions.%s.member.type' failed to satisfy constraint: Member must satisfy enum value set: [forward]" % (invalid_name, index) + ) + + +class ActionTargetGroupNotFoundError(ELBClientError): + + def __init__(self, arn): + super(ActionTargetGroupNotFoundError, self).__init__( + "TargetGroupNotFound", + "Target group '%s' not found" % arn + ) diff --git a/moto/elbv2/models.py b/moto/elbv2/models.py index 10d9ad220..a31e07927 100644 --- a/moto/elbv2/models.py +++ b/moto/elbv2/models.py @@ -14,6 +14,11 @@ from .exceptions import ( SubnetNotFoundError, TargetGroupNotFoundError, TooManyTagsError, + PriorityInUseError, + InvalidConditionFieldError, + InvalidConditionValueError, + InvalidActionTypeError, + ActionTargetGroupNotFoundError, ) @@ -92,6 +97,34 @@ class FakeListener(BaseModel): self.ssl_policy = ssl_policy self.certificate = certificate self.default_actions = default_actions + self._non_default_rules = [] + self._default_rule = FakeRule( + listener_arn=self.arn, + conditions=[], + priority='default', + actions=default_actions, + is_default=True + ) + + @property + def rules(self): + return self._non_default_rules + [self._default_rule] + + + def register(self, rule): + self._non_default_rules.append(rule) + self._non_default_rules = sorted(self._non_default_rules, key=lambda x: x.priority) + + +class FakeRule(BaseModel): + + def __init__(self, listener_arn, conditions, priority, actions, is_default): + self.listener_arn = listener_arn + self.arn = listener_arn.replace(':listener/', ':listener-rule/') + "/%s" % (id(self)) + self.conditions = conditions + self.priority = priority # int or 'default' + self.actions = actions + self.is_default = is_default class FakeBackend(BaseModel): @@ -181,6 +214,53 @@ class ELBv2Backend(BaseBackend): self.load_balancers[arn] = new_load_balancer return new_load_balancer + def create_rule(self, listener_arn, conditions, priority, actions): + listeners = self.describe_listeners(None, [listener_arn]) + if not listeners: + raise ListenerNotFound() + listener = listeners[0] + + # validate conditions + for condition in conditions: + field = condition['field'] + if field not in ['path-pattern', 'host-header']: + raise InvalidConditionFieldError(field) + + values = condition['values'] + if len(values) == 0: + raise InvalidConditionValueError('A condition value must be specified') + if len(values) > 1: + raise InvalidConditionValueError( + "The '%s' field contains too many values; the limit is '1'" % field + ) + + # TODO: check pattern of value for 'host-header' + # TODO: check pattern of value for 'path-pattern' + + # validate Priority + for rule in listener.rules: + if rule.priority == priority: + raise PriorityInUseError() + + # validate Actions + target_group_arns = [target_group.arn for target_group in self.target_groups.values()] + for i, action in enumerate(actions): + index = i + 1 + action_type = action['type'] + if action_type not in ['forward']: + raise InvalidActionTypeError(action_type, index) + action_target_group_arn = action['target_group_arn'] + if action_target_group_arn not in target_group_arns: + raise ActionTargetGroupNotFoundError(action_target_group_arn) + + # TODO: check for error 'TooManyRegistrationsForTargetId' + # TODO: check for error 'TooManyRules' + + # create rule + rule = FakeRule(listener.arn, conditions, priority, actions, is_default=False) + listener.register(rule) + return listener.rules + def create_target_group(self, name, **kwargs): for target_group in self.target_groups.values(): if target_group.name == name: diff --git a/moto/elbv2/responses.py b/moto/elbv2/responses.py index 751652901..fe4518b25 100644 --- a/moto/elbv2/responses.py +++ b/moto/elbv2/responses.py @@ -28,6 +28,30 @@ class ELBV2Response(BaseResponse): template = self.response_template(CREATE_LOAD_BALANCER_TEMPLATE) return template.render(load_balancer=load_balancer) + def create_rule(self): + lister_arn = self._get_param('ListenerArn') + _conditions = self._get_list_prefix('Conditions.member') + conditions = [] + for _condition in _conditions: + condition = {} + condition['field'] = _condition['field'] + values = sorted( + [e for e in _condition.items() if e[0].startswith('values.member')], + key=lambda x: x[0] + ) + condition['values'] = [e[1] for e in values] + conditions.append(condition) + priority = self._get_int_param('Priority') + actions = self._get_list_prefix('Actions.member') + rules = self.elbv2_backend.create_rule( + listener_arn=lister_arn, + conditions=conditions, + priority=priority, + actions=actions + ) + template = self.response_template(CREATE_RULE_TEMPLATE) + return template.render(rules=rules) + def create_target_group(self): name = self._get_param('Name') vpc_id = self._get_param('VpcId') @@ -321,6 +345,43 @@ CREATE_LOAD_BALANCER_TEMPLATE = """ + + + {% for rule in rules %} + + {{ "true" if rule.is_default else "false" }} + + {% for condition in rule.conditions %} + + {{ condition["field"] }} + + {% for value in condition["values"] %} + {{ value }} + {% endfor %} + + + {% endfor %} + + {{ rule.priority }} + + {% for action in rule.actions %} + + {{ action["type"] }} + {{ action["target_group_arn"] }} + + {% endfor %} + + {{ rule.arn }} + + {% endfor %} + + + + c5478c83-f397-11e5-bb98-57195a6eb84a + +""" + CREATE_TARGET_GROUP_TEMPLATE = """ diff --git a/tests/test_elbv2/test_elbv2.py b/tests/test_elbv2/test_elbv2.py index ece17571d..580cb6308 100644 --- a/tests/test_elbv2/test_elbv2.py +++ b/tests/test_elbv2/test_elbv2.py @@ -518,3 +518,207 @@ def test_target_group_attributes(): attributes = {attr['Key']: attr['Value'] for attr in response['Attributes']} attributes['stickiness.type'].should.equal('lb_cookie') attributes['stickiness.enabled'].should.equal('true') + + +@mock_elbv2 +@mock_ec2 +def test_create_listener_rules(): + 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] + + # 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') + + # create first rule + priority = 100 + host = 'xxx.example.com' + path_pattern = 'foobar' + rules = conn.create_rule( + ListenerArn=http_listener_arn, + Priority=priority, + Conditions=[{ + 'Field': 'host-header', + 'Values': [ host ] + }, + { + 'Field': 'path-pattern', + 'Values': [ path_pattern ] + }], + Actions=[{ + 'TargetGroupArn': target_group.get('TargetGroupArn'), + 'Type': 'forward' + }] + ) + rules['Rules'][0].get('Priority').should.equal('100') + + # check if rules is sorted by priority + priority = 50 + host = 'yyy.example.com' + path_pattern = 'foobar' + rules = conn.create_rule( + ListenerArn=http_listener_arn, + Priority=priority, + Conditions=[{ + 'Field': 'host-header', + 'Values': [ host ] + }, + { + 'Field': 'path-pattern', + 'Values': [ path_pattern ] + }], + Actions=[{ + 'TargetGroupArn': target_group.get('TargetGroupArn'), + 'Type': 'forward' + }] + ) + priorities = [rule['Priority'] for rule in rules['Rules']] + priorities.should.equal(['50', '100', 'default']) + + # test for invalid action type + safe_priority = 2 + with assert_raises(ClientError): + r = conn.create_rule( + ListenerArn=http_listener_arn, + Priority=safe_priority, + Conditions=[{ + 'Field': 'host-header', + 'Values': [ host ] + }, + { + 'Field': 'path-pattern', + 'Values': [ path_pattern ] + }], + Actions=[{ + 'TargetGroupArn': target_group.get('TargetGroupArn'), + 'Type': 'forward2' + }] + ) + + # test for invalid action type + safe_priority = 2 + invalid_target_group_arn = target_group.get('TargetGroupArn') + 'x' + with assert_raises(ClientError): + r = conn.create_rule( + ListenerArn=http_listener_arn, + Priority=safe_priority, + Conditions=[{ + 'Field': 'host-header', + 'Values': [ host ] + }, + { + 'Field': 'path-pattern', + 'Values': [ path_pattern ] + }], + Actions=[{ + 'TargetGroupArn': invalid_target_group_arn, + 'Type': 'forward' + }] + ) + + # test for PriorityInUse + host2 = 'yyy.example.com' + with assert_raises(ClientError): + r = conn.create_rule( + ListenerArn=http_listener_arn, + Priority=priority, + Conditions=[{ + 'Field': 'host-header', + 'Values': [ host ] + }, + { + 'Field': 'path-pattern', + 'Values': [ path_pattern ] + }], + Actions=[{ + 'TargetGroupArn': target_group.get('TargetGroupArn'), + 'Type': 'forward' + }] + ) + + # test for invalid condition field_name + safe_priority = 2 + with assert_raises(ClientError): + r = conn.create_rule( + ListenerArn=http_listener_arn, + Priority=safe_priority, + Conditions=[{ + 'Field': 'xxxxxxx', + 'Values': [ host ] + }], + Actions=[{ + 'TargetGroupArn': target_group.get('TargetGroupArn'), + 'Type': 'forward' + }] + ) + + # test for emptry condition value + safe_priority = 2 + with assert_raises(ClientError): + r = conn.create_rule( + ListenerArn=http_listener_arn, + Priority=safe_priority, + Conditions=[{ + 'Field': 'host-header', + 'Values': [] + }], + Actions=[{ + 'TargetGroupArn': target_group.get('TargetGroupArn'), + 'Type': 'forward' + }] + ) + + # test for multiple condition value + safe_priority = 2 + with assert_raises(ClientError): + r = conn.create_rule( + ListenerArn=http_listener_arn, + Priority=safe_priority, + Conditions=[{ + 'Field': 'host-header', + 'Values': [host, host] + }], + Actions=[{ + 'TargetGroupArn': target_group.get('TargetGroupArn'), + 'Type': 'forward' + }] + ) From 3ac10945c12f3b7ca8fa7bed4015c4d49ca28798 Mon Sep 17 00:00:00 2001 From: Toshiya Kawasaki Date: Thu, 17 Aug 2017 00:35:45 +0900 Subject: [PATCH 02/10] add delete_rule to elbv2 --- moto/elbv2/models.py | 12 ++++++++++++ moto/elbv2/responses.py | 13 +++++++++++++ tests/test_elbv2/test_elbv2.py | 4 ++++ 3 files changed, 29 insertions(+) diff --git a/moto/elbv2/models.py b/moto/elbv2/models.py index a31e07927..9182c28d5 100644 --- a/moto/elbv2/models.py +++ b/moto/elbv2/models.py @@ -357,6 +357,18 @@ class ELBv2Backend(BaseBackend): def delete_load_balancer(self, arn): self.load_balancers.pop(arn, None) + def delete_rule(self, arn): + for load_balancer_arn in self.load_balancers: + listeners = self.load_balancers.get(load_balancer_arn).listeners.values() + for listener in listeners: + for rule in listener.rules: + if rule.arn == arn: + listener.rules.remove(rule) + return + + # should raise RuleNotFound Error according to the AWS API doc + # however, boto3 does't raise error even if rule is not found + def delete_target_group(self, target_group_arn): target_group = self.target_groups.pop(target_group_arn) if target_group: diff --git a/moto/elbv2/responses.py b/moto/elbv2/responses.py index fe4518b25..05e21effe 100644 --- a/moto/elbv2/responses.py +++ b/moto/elbv2/responses.py @@ -157,6 +157,12 @@ class ELBV2Response(BaseResponse): template = self.response_template(DELETE_LOAD_BALANCER_TEMPLATE) return template.render() + def delete_rule(self): + arn = self._get_param('RuleArn') + self.elbv2_backend.delete_rule(arn) + template = self.response_template(DELETE_RULE_TEMPLATE) + return template.render() + def delete_target_group(self): arn = self._get_param('TargetGroupArn') self.elbv2_backend.delete_target_group(arn) @@ -448,6 +454,13 @@ DELETE_LOAD_BALANCER_TEMPLATE = """ + + + 1549581b-12b7-11e3-895e-1334aEXAMPLE + +""" + DELETE_TARGET_GROUP_TEMPLATE = """ diff --git a/tests/test_elbv2/test_elbv2.py b/tests/test_elbv2/test_elbv2.py index 580cb6308..81fda21b4 100644 --- a/tests/test_elbv2/test_elbv2.py +++ b/tests/test_elbv2/test_elbv2.py @@ -614,6 +614,10 @@ def test_create_listener_rules(): priorities = [rule['Priority'] for rule in rules['Rules']] priorities.should.equal(['50', '100', 'default']) + arn = rules['Rules'][0]['RuleArn'] + conn.delete_rule(RuleArn=arn) + # TODO: describe rule and ensure rule is removed + # test for invalid action type safe_priority = 2 with assert_raises(ClientError): From a73fa64043b6174518fbbee1a793893e99881968 Mon Sep 17 00:00:00 2001 From: Toshiya Kawasaki Date: Thu, 17 Aug 2017 01:57:02 +0900 Subject: [PATCH 03/10] add describe_rules to elbv2 --- moto/elbv2/exceptions.py | 8 +++++ moto/elbv2/models.py | 24 ++++++++++++++ moto/elbv2/responses.py | 60 ++++++++++++++++++++++++++++++++++ tests/test_elbv2/test_elbv2.py | 27 ++++++++++++++- 4 files changed, 118 insertions(+), 1 deletion(-) diff --git a/moto/elbv2/exceptions.py b/moto/elbv2/exceptions.py index a03cf9a98..a547c4f88 100644 --- a/moto/elbv2/exceptions.py +++ b/moto/elbv2/exceptions.py @@ -142,3 +142,11 @@ class ActionTargetGroupNotFoundError(ELBClientError): "TargetGroupNotFound", "Target group '%s' not found" % arn ) + + +class InvalidDescribeRulesRequest(ELBClientError): + + def __init__(self, msg): + super(InvalidDescribeRulesRequest, self).__init__( + "ValidationError", msg + ) diff --git a/moto/elbv2/models.py b/moto/elbv2/models.py index 9182c28d5..25803ea79 100644 --- a/moto/elbv2/models.py +++ b/moto/elbv2/models.py @@ -19,6 +19,7 @@ from .exceptions import ( InvalidConditionValueError, InvalidActionTypeError, ActionTargetGroupNotFoundError, + InvalidDescribeRulesRequest ) @@ -313,6 +314,29 @@ class ELBv2Backend(BaseBackend): return matched_balancers + def describe_rules(self, listener_arn, rule_arns): + if listener_arn is None and not rule_arns: + raise InvalidDescribeRulesRequest( + "You must specify either listener rule ARNs or a listener ARN" + ) + if listener_arn is not None and rule_arns is not None: + raise InvalidDescribeRulesRequest( + 'Listener rule ARNs and a listener ARN cannot be specified at the same time' + ) + if listener_arn: + listener = self.describe_listeners(None, [listener_arn])[0] + return listener.rules + + # search for rule arns + matched_rules = [] + for load_balancer_arn in self.load_balancers: + listeners = self.load_balancers.get(load_balancer_arn).listeners.values() + for listener in listeners: + for rule in listener.rules: + if rule.arn in rule_arns: + matched_rules.append(rule) + return matched_rules + 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: diff --git a/moto/elbv2/responses.py b/moto/elbv2/responses.py index 05e21effe..16b170c6a 100644 --- a/moto/elbv2/responses.py +++ b/moto/elbv2/responses.py @@ -1,4 +1,5 @@ from __future__ import unicode_literals +import base64 from moto.core.responses import BaseResponse from .models import elbv2_backends from .exceptions import DuplicateTagKeysError @@ -124,6 +125,26 @@ class ELBV2Response(BaseResponse): template = self.response_template(DESCRIBE_LOAD_BALANCERS_TEMPLATE) return template.render(load_balancers=load_balancers_resp, marker=next_marker) + def describe_rules(self): + listener_arn = self._get_param('ListenerArn') + rule_arns = self._get_multi_param('RuleArns.member') if any(k for k in list(self.querystring.keys()) if k.startswith('RuleArns.member')) else None + all_rules = self.elbv2_backend.describe_rules(listener_arn, rule_arns) + all_arns = [rule.arn for rule in all_rules] + all_arns = [base64.urlsafe_b64encode(bytes(rule.arn, 'UTF-8')) for rule in all_rules] + page_size = self._get_int_param('PageSize', 50) # set 50 for temporary + + marker = self._get_param('Marker') + if marker: + start = all_arns.index(marker) + 1 + else: + start = 0 + rules_resp = all_rules[start:start + page_size] + next_marker = None + if len(all_rules) > start + page_size: + next_marker = base64.urlsafe_b64encode(bytes(rules_resp[-1].arn, 'UTF-8')) + template = self.response_template(DESCRIBE_RULES_TEMPLATE) + return template.render(rules=rules_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') @@ -516,6 +537,45 @@ DESCRIBE_LOAD_BALANCERS_TEMPLATE = """ + + + {% for rule in rules %} + + {{ "true" if rule.is_default else "false" }} + + {% for condition in rule.conditions %} + + {{ condition["field"] }} + + {% for value in condition["values"] %} + {{ value }} + {% endfor %} + + + {% endfor %} + + {{ rule.priority }} + + {% for action in rule.actions %} + + {{ action["type"] }} + {{ action["target_group_arn"] }} + + {% endfor %} + + {{ rule.arn }} + + {% endfor %} + + {% if marker %} + {{ marker }} + {% endif %} + + + 74926cf3-f3a3-11e5-b543-9f2c3fbb9bee + +""" DESCRIBE_TARGET_GROUPS_TEMPLATE = """ diff --git a/tests/test_elbv2/test_elbv2.py b/tests/test_elbv2/test_elbv2.py index 81fda21b4..43847f510 100644 --- a/tests/test_elbv2/test_elbv2.py +++ b/tests/test_elbv2/test_elbv2.py @@ -614,7 +614,32 @@ def test_create_listener_rules(): priorities = [rule['Priority'] for rule in rules['Rules']] priorities.should.equal(['50', '100', 'default']) - arn = rules['Rules'][0]['RuleArn'] + # test for describe listeners + first_rule = rules['Rules'][0] + obtained_rules = conn.describe_rules(ListenerArn=http_listener_arn) + obtained_rules['Rules'].should.equal(rules['Rules']) + + obtained_rules = conn.describe_rules(RuleArns=[first_rule['RuleArn']]) + obtained_rules['Rules'].should.equal([first_rule]) + + # test for pagination + obtained_rules = conn.describe_rules(ListenerArn=http_listener_arn, PageSize=1) + len(obtained_rules['Rules']).should.equal(1) + obtained_rules.should.have.key('NextMarker') + + # test for invalid describe rule request + with assert_raises(ClientError): + conn.describe_rules() + with assert_raises(ClientError): + conn.describe_rules(RuleArns=[]) + with assert_raises(ClientError): + conn.describe_rules( + ListenerArn=http_listener_arn, + RuleArns=[first_rule['RuleArn']] + ) + + # delete + arn = first_rule['RuleArn'] conn.delete_rule(RuleArn=arn) # TODO: describe rule and ensure rule is removed From 9bc67794852f688348eded74aa9965a48a8acd32 Mon Sep 17 00:00:00 2001 From: Toshiya Kawasaki Date: Thu, 17 Aug 2017 02:25:39 +0900 Subject: [PATCH 04/10] add modify_rules to elbv2 --- moto/elbv2/exceptions.py | 8 +++++ moto/elbv2/models.py | 46 +++++++++++++++++++++++++- moto/elbv2/responses.py | 59 ++++++++++++++++++++++++++++++++++ tests/test_elbv2/test_elbv2.py | 24 +++++++++++++- 4 files changed, 135 insertions(+), 2 deletions(-) diff --git a/moto/elbv2/exceptions.py b/moto/elbv2/exceptions.py index a547c4f88..705aa9622 100644 --- a/moto/elbv2/exceptions.py +++ b/moto/elbv2/exceptions.py @@ -150,3 +150,11 @@ class InvalidDescribeRulesRequest(ELBClientError): super(InvalidDescribeRulesRequest, self).__init__( "ValidationError", msg ) + + +class RuleNotFoundError(ELBClientError): + + def __init__(self): + super(RuleNotFoundError, self).__init__( + "RuleNotFound", + "The specified rule does not exist.") diff --git a/moto/elbv2/models.py b/moto/elbv2/models.py index 25803ea79..664d2a40e 100644 --- a/moto/elbv2/models.py +++ b/moto/elbv2/models.py @@ -19,7 +19,8 @@ from .exceptions import ( InvalidConditionValueError, InvalidActionTypeError, ActionTargetGroupNotFoundError, - InvalidDescribeRulesRequest + InvalidDescribeRulesRequest, + RuleNotFoundError ) @@ -406,6 +407,49 @@ class ELBv2Backend(BaseBackend): return listener raise ListenerNotFoundError() + def modify_rule(self, rule_arn, conditions, actions): + rules = self.describe_rules(listener_arn=None, rule_arns=[rule_arn]) + if not rules: + raise RuleNotFoundError() + rule = rules[0] + + # validate conditions + for condition in conditions: + field = condition['field'] + if field not in ['path-pattern', 'host-header']: + raise InvalidConditionFieldError(field) + + values = condition['values'] + if len(values) == 0: + raise InvalidConditionValueError('A condition value must be specified') + if len(values) > 1: + raise InvalidConditionValueError( + "The '%s' field contains too many values; the limit is '1'" % field + ) + + # TODO: check pattern of value for 'host-header' + # TODO: check pattern of value for 'path-pattern' + + # validate Actions + target_group_arns = [target_group.arn for target_group in self.target_groups.values()] + for i, action in enumerate(actions): + index = i + 1 + action_type = action['type'] + if action_type not in ['forward']: + raise InvalidActionTypeError(action_type, index) + action_target_group_arn = action['target_group_arn'] + if action_target_group_arn not in target_group_arns: + raise ActionTargetGroupNotFoundError(action_target_group_arn) + + # TODO: check for error 'TooManyRegistrationsForTargetId' + # TODO: check for error 'TooManyRules' + + # modify rule + rule.conditions = conditions + rule.actions = actions + return [rule] + + def register_targets(self, target_group_arn, instances): target_group = self.target_groups.get(target_group_arn) if target_group is None: diff --git a/moto/elbv2/responses.py b/moto/elbv2/responses.py index 16b170c6a..2e5f1e299 100644 --- a/moto/elbv2/responses.py +++ b/moto/elbv2/responses.py @@ -196,6 +196,28 @@ class ELBV2Response(BaseResponse): template = self.response_template(DELETE_LISTENER_TEMPLATE) return template.render() + def modify_rule(self): + rule_arn = self._get_param('RuleArn') + _conditions = self._get_list_prefix('Conditions.member') + conditions = [] + for _condition in _conditions: + condition = {} + condition['field'] = _condition['field'] + values = sorted( + [e for e in _condition.items() if e[0].startswith('values.member')], + key=lambda x: x[0] + ) + condition['values'] = [e[1] for e in values] + conditions.append(condition) + actions = self._get_list_prefix('Actions.member') + rules = self.elbv2_backend.modify_rule( + rule_arn=rule_arn, + conditions=conditions, + actions=actions + ) + template = self.response_template(MODIFY_RULE_TEMPLATE) + return template.render(rules=rules) + def modify_target_group_attributes(self): target_group_arn = self._get_param('TargetGroupArn') target_group = self.elbv2_backend.target_groups.get(target_group_arn) @@ -678,6 +700,43 @@ CONFIGURE_HEALTH_CHECK_TEMPLATE = """ + + + {% for rule in rules %} + + {{ "true" if rule.is_default else "false" }} + + {% for condition in rule.conditions %} + + {{ condition["field"] }} + + {% for value in condition["values"] %} + {{ value }} + {% endfor %} + + + {% endfor %} + + {{ rule.priority }} + + {% for action in rule.actions %} + + {{ action["type"] }} + {{ action["target_group_arn"] }} + + {% endfor %} + + {{ rule.arn }} + + {% endfor %} + + + + c5478c83-f397-11e5-bb98-57195a6eb84a + +""" + MODIFY_TARGET_GROUP_ATTRIBUTES_TEMPLATE = """ diff --git a/tests/test_elbv2/test_elbv2.py b/tests/test_elbv2/test_elbv2.py index 43847f510..270dfaafc 100644 --- a/tests/test_elbv2/test_elbv2.py +++ b/tests/test_elbv2/test_elbv2.py @@ -615,10 +615,10 @@ def test_create_listener_rules(): priorities.should.equal(['50', '100', 'default']) # test for describe listeners - first_rule = rules['Rules'][0] obtained_rules = conn.describe_rules(ListenerArn=http_listener_arn) obtained_rules['Rules'].should.equal(rules['Rules']) + first_rule = obtained_rules['Rules'][0] obtained_rules = conn.describe_rules(RuleArns=[first_rule['RuleArn']]) obtained_rules['Rules'].should.equal([first_rule]) @@ -638,6 +638,28 @@ def test_create_listener_rules(): RuleArns=[first_rule['RuleArn']] ) + # modify + new_host = 'new.example.com' + new_path_pattern = 'new_path' + modified_rule = conn.modify_rule( + RuleArn=first_rule['RuleArn'], + Conditions=[{ + 'Field': 'host-header', + 'Values': [ new_host ] + }, + { + 'Field': 'path-pattern', + 'Values': [ new_path_pattern ] + }], + Actions=[{ + 'TargetGroupArn': target_group.get('TargetGroupArn'), + 'Type': 'forward' + }] + + )['Rules'][0] + rules = conn.describe_rules(ListenerArn=http_listener_arn) + modified_rule.should.equal(rules['Rules'][0]) + # delete arn = first_rule['RuleArn'] conn.delete_rule(RuleArn=arn) From 0aaa624205c324776a8f26b2d02116673d216430 Mon Sep 17 00:00:00 2001 From: Toshiya Kawasaki Date: Thu, 17 Aug 2017 02:29:49 +0900 Subject: [PATCH 05/10] Fix respose number of rules of create_rule --- moto/elbv2/models.py | 2 +- tests/test_elbv2/test_elbv2.py | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/moto/elbv2/models.py b/moto/elbv2/models.py index 664d2a40e..c51b6a561 100644 --- a/moto/elbv2/models.py +++ b/moto/elbv2/models.py @@ -261,7 +261,7 @@ class ELBv2Backend(BaseBackend): # create rule rule = FakeRule(listener.arn, conditions, priority, actions, is_default=False) listener.register(rule) - return listener.rules + return [rule] def create_target_group(self, name, **kwargs): for target_group in self.target_groups.values(): diff --git a/tests/test_elbv2/test_elbv2.py b/tests/test_elbv2/test_elbv2.py index 270dfaafc..0911ff4a1 100644 --- a/tests/test_elbv2/test_elbv2.py +++ b/tests/test_elbv2/test_elbv2.py @@ -573,7 +573,7 @@ def test_create_listener_rules(): priority = 100 host = 'xxx.example.com' path_pattern = 'foobar' - rules = conn.create_rule( + created_rule = conn.create_rule( ListenerArn=http_listener_arn, Priority=priority, Conditions=[{ @@ -588,8 +588,8 @@ def test_create_listener_rules(): 'TargetGroupArn': target_group.get('TargetGroupArn'), 'Type': 'forward' }] - ) - rules['Rules'][0].get('Priority').should.equal('100') + )['Rules'][0] + created_rule['Priority'].should.equal('100') # check if rules is sorted by priority priority = 50 @@ -611,12 +611,12 @@ def test_create_listener_rules(): 'Type': 'forward' }] ) - priorities = [rule['Priority'] for rule in rules['Rules']] - priorities.should.equal(['50', '100', 'default']) # test for describe listeners obtained_rules = conn.describe_rules(ListenerArn=http_listener_arn) - obtained_rules['Rules'].should.equal(rules['Rules']) + len(obtained_rules['Rules']).should.equal(3) + priorities = [rule['Priority'] for rule in obtained_rules['Rules']] + priorities.should.equal(['50', '100', 'default']) first_rule = obtained_rules['Rules'][0] obtained_rules = conn.describe_rules(RuleArns=[first_rule['RuleArn']]) From e07bce003c1b5ea8b9bf785bbef53052dbe14042 Mon Sep 17 00:00:00 2001 From: Toshiya Kawasaki Date: Thu, 17 Aug 2017 03:10:26 +0900 Subject: [PATCH 06/10] add set_rule_priorities to elbv2 --- moto/elbv2/exceptions.py | 8 +++++ moto/elbv2/models.py | 36 ++++++++++++++++++++- moto/elbv2/responses.py | 46 ++++++++++++++++++++++++++ tests/test_elbv2/test_elbv2.py | 59 +++++++++++++++++++++------------- 4 files changed, 126 insertions(+), 23 deletions(-) diff --git a/moto/elbv2/exceptions.py b/moto/elbv2/exceptions.py index 705aa9622..569fa7eed 100644 --- a/moto/elbv2/exceptions.py +++ b/moto/elbv2/exceptions.py @@ -158,3 +158,11 @@ class RuleNotFoundError(ELBClientError): super(RuleNotFoundError, self).__init__( "RuleNotFound", "The specified rule does not exist.") + + +class DuplicatePriorityError(ELBClientError): + + def __init__(self, invalid_value): + super(DuplicatePriorityError, self).__init__( + "ValidationError", + "Priority '%s' was provided multiple times" % invalid_value) diff --git a/moto/elbv2/models.py b/moto/elbv2/models.py index c51b6a561..398a63481 100644 --- a/moto/elbv2/models.py +++ b/moto/elbv2/models.py @@ -20,7 +20,8 @@ from .exceptions import ( InvalidActionTypeError, ActionTargetGroupNotFoundError, InvalidDescribeRulesRequest, - RuleNotFoundError + RuleNotFoundError, + DuplicatePriorityError ) @@ -471,6 +472,39 @@ class ELBv2Backend(BaseBackend): targets = target_group.targets.values() return [target_group.health_for(target) for target in targets] + def set_rule_priorities(self, rule_priorities): + # validate + priorities = [rule_priority['priority'] for rule_priority in rule_priorities] + for priority in set(priorities): + if priorities.count(priority) > 1: + raise DuplicatePriorityError(priority) + + # validate + for rule_priority in rule_priorities: + given_rule_arn = rule_priority['rule_arn'] + priority = rule_priority['priority'] + _given_rules = self.describe_rules(listener_arn=None, rule_arns=[given_rule_arn]) + if not _given_rules: + raise RuleNotFoundError() + given_rule = _given_rules[0] + listeners = self.describe_listeners(None, [given_rule.listener_arn]) + listener = listeners[0] + for rule_in_listener in listener.rules: + if rule_in_listener.priority == priority: + raise PriorityInUseError() + # modify + modified_rules = [] + for rule_priority in rule_priorities: + given_rule_arn = rule_priority['rule_arn'] + priority = rule_priority['priority'] + _given_rules = self.describe_rules(listener_arn=None, rule_arns=[given_rule_arn]) + if not _given_rules: + raise RuleNotFoundError() + given_rule = _given_rules[0] + given_rule.priority = priority + modified_rules.append(given_rule) + return modified_rules + elbv2_backends = {} for region in ec2_backends.keys(): diff --git a/moto/elbv2/responses.py b/moto/elbv2/responses.py index 2e5f1e299..a798d1e41 100644 --- a/moto/elbv2/responses.py +++ b/moto/elbv2/responses.py @@ -255,6 +255,14 @@ class ELBV2Response(BaseResponse): template = self.response_template(DESCRIBE_TARGET_HEALTH_TEMPLATE) return template.render(target_health_descriptions=target_health_descriptions) + def set_rule_priorities(self): + rule_priorities = self._get_list_prefix('RulePriorities.member') + for rule_priority in rule_priorities: + rule_priority['priority'] = int(rule_priority['priority']) + rules = self.elbv2_backend.set_rule_priorities(rule_priorities) + template = self.response_template(SET_RULE_PRIORITIES_TEMPLATE) + return template.render(rules=rules) + def add_tags(self): resource_arns = self._get_multi_param('ResourceArns.member') @@ -896,3 +904,41 @@ DESCRIBE_TARGET_HEALTH_TEMPLATE = """ + + + {% for rule in rules %} + + {{ "true" if rule.is_default else "false" }} + + {% for condition in rule.conditions %} + + {{ condition["field"] }} + + {% for value in condition["values"] %} + {{ value }} + {% endfor %} + + + {% endfor %} + + {{ rule.priority }} + + {% for action in rule.actions %} + + {{ action["type"] }} + {{ action["target_group_arn"] }} + + {% endfor %} + + {{ rule.arn }} + + {% endfor %} + + + + 4d7a8036-f3a7-11e5-9c02-8fd20490d5a6 + +""" diff --git a/tests/test_elbv2/test_elbv2.py b/tests/test_elbv2/test_elbv2.py index 0911ff4a1..d4220d4c8 100644 --- a/tests/test_elbv2/test_elbv2.py +++ b/tests/test_elbv2/test_elbv2.py @@ -612,6 +612,27 @@ def test_create_listener_rules(): }] ) + # test for PriorityInUse + host2 = 'yyy.example.com' + with assert_raises(ClientError): + r = conn.create_rule( + ListenerArn=http_listener_arn, + Priority=priority, + Conditions=[{ + 'Field': 'host-header', + 'Values': [ host ] + }, + { + 'Field': 'path-pattern', + 'Values': [ path_pattern ] + }], + Actions=[{ + 'TargetGroupArn': target_group.get('TargetGroupArn'), + 'Type': 'forward' + }] + ) + + # test for describe listeners obtained_rules = conn.describe_rules(ListenerArn=http_listener_arn) len(obtained_rules['Rules']).should.equal(3) @@ -619,6 +640,7 @@ def test_create_listener_rules(): priorities.should.equal(['50', '100', 'default']) first_rule = obtained_rules['Rules'][0] + second_rule = obtained_rules['Rules'][1] obtained_rules = conn.describe_rules(RuleArns=[first_rule['RuleArn']]) obtained_rules['Rules'].should.equal([first_rule]) @@ -638,7 +660,7 @@ def test_create_listener_rules(): RuleArns=[first_rule['RuleArn']] ) - # modify + # modify rule new_host = 'new.example.com' new_path_pattern = 'new_path' modified_rule = conn.modify_rule( @@ -660,10 +682,23 @@ def test_create_listener_rules(): rules = conn.describe_rules(ListenerArn=http_listener_arn) modified_rule.should.equal(rules['Rules'][0]) + # modify priority + conn.set_rule_priorities( + RulePriorities=[ + {'RuleArn': first_rule['RuleArn'], 'Priority': int(first_rule['Priority']) - 1} + ] + ) + with assert_raises(ClientError): + conn.set_rule_priorities( + RulePriorities=[ + {'RuleArn': first_rule['RuleArn'], 'Priority': 999}, + {'RuleArn': second_rule['RuleArn'], 'Priority': 999} + ] + ) + # delete arn = first_rule['RuleArn'] conn.delete_rule(RuleArn=arn) - # TODO: describe rule and ensure rule is removed # test for invalid action type safe_priority = 2 @@ -706,26 +741,6 @@ def test_create_listener_rules(): }] ) - # test for PriorityInUse - host2 = 'yyy.example.com' - with assert_raises(ClientError): - r = conn.create_rule( - ListenerArn=http_listener_arn, - Priority=priority, - Conditions=[{ - 'Field': 'host-header', - 'Values': [ host ] - }, - { - 'Field': 'path-pattern', - 'Values': [ path_pattern ] - }], - Actions=[{ - 'TargetGroupArn': target_group.get('TargetGroupArn'), - 'Type': 'forward' - }] - ) - # test for invalid condition field_name safe_priority = 2 with assert_raises(ClientError): From ea2a973813de24ff674bc809dd8d6334ec1bf87b Mon Sep 17 00:00:00 2001 From: Toshiya Kawasaki Date: Thu, 17 Aug 2017 03:22:40 +0900 Subject: [PATCH 07/10] fix syntax --- moto/elbv2/models.py | 6 ++---- moto/elbv2/responses.py | 5 ++--- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/moto/elbv2/models.py b/moto/elbv2/models.py index 398a63481..b30ab5764 100644 --- a/moto/elbv2/models.py +++ b/moto/elbv2/models.py @@ -113,7 +113,6 @@ class FakeListener(BaseModel): def rules(self): return self._non_default_rules + [self._default_rule] - def register(self, rule): self._non_default_rules.append(rule) self._non_default_rules = sorted(self._non_default_rules, key=lambda x: x.priority) @@ -125,7 +124,7 @@ class FakeRule(BaseModel): self.listener_arn = listener_arn self.arn = listener_arn.replace(':listener/', ':listener-rule/') + "/%s" % (id(self)) self.conditions = conditions - self.priority = priority # int or 'default' + self.priority = priority # int or 'default' self.actions = actions self.is_default = is_default @@ -220,7 +219,7 @@ class ELBv2Backend(BaseBackend): def create_rule(self, listener_arn, conditions, priority, actions): listeners = self.describe_listeners(None, [listener_arn]) if not listeners: - raise ListenerNotFound() + raise ListenerNotFoundError() listener = listeners[0] # validate conditions @@ -450,7 +449,6 @@ class ELBv2Backend(BaseBackend): rule.actions = actions return [rule] - def register_targets(self, target_group_arn, instances): target_group = self.target_groups.get(target_group_arn) if target_group is None: diff --git a/moto/elbv2/responses.py b/moto/elbv2/responses.py index a798d1e41..5dcc78a75 100644 --- a/moto/elbv2/responses.py +++ b/moto/elbv2/responses.py @@ -131,7 +131,7 @@ class ELBV2Response(BaseResponse): all_rules = self.elbv2_backend.describe_rules(listener_arn, rule_arns) all_arns = [rule.arn for rule in all_rules] all_arns = [base64.urlsafe_b64encode(bytes(rule.arn, 'UTF-8')) for rule in all_rules] - page_size = self._get_int_param('PageSize', 50) # set 50 for temporary + page_size = self._get_int_param('PageSize', 50) # set 50 for temporary marker = self._get_param('Marker') if marker: @@ -905,8 +905,7 @@ DESCRIBE_TARGET_HEALTH_TEMPLATE = """ +SET_RULE_PRIORITIES_TEMPLATE = """ {% for rule in rules %} From edc2e70fcf8a6840e12b0f83087b1a5943c67f11 Mon Sep 17 00:00:00 2001 From: Toshiya Kawasaki Date: Thu, 17 Aug 2017 03:56:01 +0900 Subject: [PATCH 08/10] make b64encode work both on python2 and python3 --- moto/elbv2/responses.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/moto/elbv2/responses.py b/moto/elbv2/responses.py index 5dcc78a75..ec26922ec 100644 --- a/moto/elbv2/responses.py +++ b/moto/elbv2/responses.py @@ -1,4 +1,5 @@ from __future__ import unicode_literals +import sys import base64 from moto.core.responses import BaseResponse from .models import elbv2_backends @@ -125,12 +126,17 @@ class ELBV2Response(BaseResponse): template = self.response_template(DESCRIBE_LOAD_BALANCERS_TEMPLATE) return template.render(load_balancers=load_balancers_resp, marker=next_marker) + def _b64_encode(self, s): + if sys.version_info >= (3, 0): + return base64.urlsafe_b64encode(bytes(s, 'UTF-8')) + return s + def describe_rules(self): listener_arn = self._get_param('ListenerArn') rule_arns = self._get_multi_param('RuleArns.member') if any(k for k in list(self.querystring.keys()) if k.startswith('RuleArns.member')) else None all_rules = self.elbv2_backend.describe_rules(listener_arn, rule_arns) all_arns = [rule.arn for rule in all_rules] - all_arns = [base64.urlsafe_b64encode(bytes(rule.arn, 'UTF-8')) for rule in all_rules] + all_arns = [self._b64_encode(rule.arn) for rule in all_rules] page_size = self._get_int_param('PageSize', 50) # set 50 for temporary marker = self._get_param('Marker') @@ -141,7 +147,7 @@ class ELBV2Response(BaseResponse): rules_resp = all_rules[start:start + page_size] next_marker = None if len(all_rules) > start + page_size: - next_marker = base64.urlsafe_b64encode(bytes(rules_resp[-1].arn, 'UTF-8')) + next_marker = rules_resp[-1].arn template = self.response_template(DESCRIBE_RULES_TEMPLATE) return template.render(rules=rules_resp, marker=next_marker) From 5c0d5e920ac3a9dba8a3377e5640a04717f1e311 Mon Sep 17 00:00:00 2001 From: Toshiya Kawasaki Date: Thu, 17 Aug 2017 04:28:32 +0900 Subject: [PATCH 09/10] rename test_create_listener_rules to test_handle_listener_rules --- tests/test_elbv2/test_elbv2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_elbv2/test_elbv2.py b/tests/test_elbv2/test_elbv2.py index d4220d4c8..dec708fd5 100644 --- a/tests/test_elbv2/test_elbv2.py +++ b/tests/test_elbv2/test_elbv2.py @@ -522,7 +522,7 @@ def test_target_group_attributes(): @mock_elbv2 @mock_ec2 -def test_create_listener_rules(): +def test_handle_listener_rules(): conn = boto3.client('elbv2', region_name='us-east-1') ec2 = boto3.resource('ec2', region_name='us-east-1') From 2b10ef85175cf96d567ca82afde5e182b82cf93b Mon Sep 17 00:00:00 2001 From: Toshiya Kawasaki Date: Thu, 17 Aug 2017 04:37:42 +0900 Subject: [PATCH 10/10] dont use base64 for marker at describe_rules --- moto/elbv2/responses.py | 9 +-------- tests/test_elbv2/test_elbv2.py | 6 ++++++ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/moto/elbv2/responses.py b/moto/elbv2/responses.py index ec26922ec..a9635029c 100644 --- a/moto/elbv2/responses.py +++ b/moto/elbv2/responses.py @@ -1,6 +1,4 @@ from __future__ import unicode_literals -import sys -import base64 from moto.core.responses import BaseResponse from .models import elbv2_backends from .exceptions import DuplicateTagKeysError @@ -126,17 +124,11 @@ class ELBV2Response(BaseResponse): template = self.response_template(DESCRIBE_LOAD_BALANCERS_TEMPLATE) return template.render(load_balancers=load_balancers_resp, marker=next_marker) - def _b64_encode(self, s): - if sys.version_info >= (3, 0): - return base64.urlsafe_b64encode(bytes(s, 'UTF-8')) - return s - def describe_rules(self): listener_arn = self._get_param('ListenerArn') rule_arns = self._get_multi_param('RuleArns.member') if any(k for k in list(self.querystring.keys()) if k.startswith('RuleArns.member')) else None all_rules = self.elbv2_backend.describe_rules(listener_arn, rule_arns) all_arns = [rule.arn for rule in all_rules] - all_arns = [self._b64_encode(rule.arn) for rule in all_rules] page_size = self._get_int_param('PageSize', 50) # set 50 for temporary marker = self._get_param('Marker') @@ -146,6 +138,7 @@ class ELBV2Response(BaseResponse): start = 0 rules_resp = all_rules[start:start + page_size] next_marker = None + if len(all_rules) > start + page_size: next_marker = rules_resp[-1].arn template = self.response_template(DESCRIBE_RULES_TEMPLATE) diff --git a/tests/test_elbv2/test_elbv2.py b/tests/test_elbv2/test_elbv2.py index dec708fd5..a37eaa9bc 100644 --- a/tests/test_elbv2/test_elbv2.py +++ b/tests/test_elbv2/test_elbv2.py @@ -648,6 +648,12 @@ def test_handle_listener_rules(): obtained_rules = conn.describe_rules(ListenerArn=http_listener_arn, PageSize=1) len(obtained_rules['Rules']).should.equal(1) obtained_rules.should.have.key('NextMarker') + next_marker = obtained_rules['NextMarker'] + + following_rules = conn.describe_rules(ListenerArn=http_listener_arn, PageSize=1, Marker=next_marker) + len(following_rules['Rules']).should.equal(1) + following_rules.should.have.key('NextMarker') + following_rules['Rules'][0]['RuleArn'].should_not.equal(obtained_rules['Rules'][0]['RuleArn']) # test for invalid describe rule request with assert_raises(ClientError):