From 585ef7b768b0793048281cc679e3031c808ebc08 Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Sat, 17 Jan 2015 19:06:43 -0500 Subject: [PATCH] Add route53 health checks. --- moto/cloudformation/parsing.py | 1 + moto/route53/models.py | 71 ++++++++++++++++- moto/route53/responses.py | 49 ++++++++++++ moto/route53/urls.py | 9 ++- .../fixtures/route53_health_check.py | 39 ++++++++++ .../test_cloudformation_stack_integration.py | 36 +++++++++ tests/test_route53/test_route53.py | 77 +++++++++++++++++++ 7 files changed, 277 insertions(+), 5 deletions(-) create mode 100644 tests/test_cloudformation/fixtures/route53_health_check.py diff --git a/moto/cloudformation/parsing.py b/moto/cloudformation/parsing.py index cb5283b99..ea8ca646b 100644 --- a/moto/cloudformation/parsing.py +++ b/moto/cloudformation/parsing.py @@ -37,6 +37,7 @@ MODEL_MAP = { "AWS::RDS::DBInstance": rds_models.Database, "AWS::RDS::DBSecurityGroup": rds_models.SecurityGroup, "AWS::RDS::DBSubnetGroup": rds_models.SubnetGroup, + "AWS::Route53::HealthCheck": route53_models.HealthCheck, "AWS::Route53::HostedZone": route53_models.FakeZone, "AWS::Route53::RecordSet": route53_models.RecordSet, "AWS::Route53::RecordSetGroup": route53_models.RecordSetGroup, diff --git a/moto/route53/models.py b/moto/route53/models.py index e6bb1c7a2..58c559f25 100644 --- a/moto/route53/models.py +++ b/moto/route53/models.py @@ -1,11 +1,65 @@ from __future__ import unicode_literals +import uuid from jinja2 import Template from moto.core import BaseBackend from moto.core.utils import get_random_hex +class HealthCheck(object): + def __init__(self, health_check_id, health_check_args): + self.id = health_check_id + self.ip_address = health_check_args.get("ip_address") + self.port = health_check_args.get("port", 80) + self._type = health_check_args.get("type") + self.resource_path = health_check_args.get("resource_path") + self.fqdn = health_check_args.get("fqdn") + self.search_string = health_check_args.get("search_string") + self.request_interval = health_check_args.get("request_interval", 30) + self.failure_threshold = health_check_args.get("failure_threshold", 3) + + @property + def physical_resource_id(self): + return self.id + + @classmethod + def create_from_cloudformation_json(cls, resource_name, cloudformation_json, region_name): + properties = cloudformation_json['Properties']['HealthCheckConfig'] + health_check_args = { + "ip_address": properties.get('IPAddress'), + "port": properties.get('Port'), + "type": properties['Type'], + "resource_path": properties.get('ResourcePath'), + "fqdn": properties.get('FullyQualifiedDomainName'), + "search_string": properties.get('SearchString'), + "request_interval": properties.get('RequestInterval'), + "failure_threshold": properties.get('FailureThreshold'), + } + health_check = route53_backend.create_health_check(health_check_args) + return health_check + + def to_xml(self): + template = Template(""" + {{ health_check.id }} + example.com 192.0.2.17 + + {{ health_check.ip_address }} + {{ health_check.port }} + {{ health_check._type }} + {{ health_check.resource_path }} + {{ health_check.fqdn }} + {{ health_check.request_interval }} + {{ health_check.failure_threshold }} + {% if health_check.search_string %} + {{ health_check.search_string }} + {% endif %} + + 1 + """) + return template.render(health_check=self) + + class RecordSet(object): def __init__(self, kwargs): self.name = kwargs.get('Name') @@ -14,6 +68,7 @@ class RecordSet(object): self.records = kwargs.get('ResourceRecords', []) self.set_identifier = kwargs.get('SetIdentifier') self.weight = kwargs.get('Weight') + self.health_check = kwargs.get('HealthCheckId') @classmethod def create_from_cloudformation_json(cls, resource_name, cloudformation_json, region_name): @@ -42,7 +97,9 @@ class RecordSet(object): {% endfor %} - + {% if record_set.health_check %} + {{ record_set.health_check }} + {% endif %} """) return template.render(record_set=self) @@ -111,6 +168,7 @@ class Route53Backend(BaseBackend): def __init__(self): self.zones = {} + self.health_checks = {} def create_hosted_zone(self, name): new_id = get_random_hex() @@ -135,5 +193,16 @@ class Route53Backend(BaseBackend): del self.zones[id_] return zone + def create_health_check(self, health_check_args): + health_check_id = str(uuid.uuid4()) + health_check = HealthCheck(health_check_id, health_check_args) + self.health_checks[health_check_id] = health_check + return health_check + + def get_health_checks(self): + return self.health_checks.values() + + def delete_health_check(self, health_check_id): + return self.health_checks.pop(health_check_id, None) route53_backend = Route53Backend() diff --git a/moto/route53/responses.py b/moto/route53/responses.py index d110ebd55..5bbb8f451 100644 --- a/moto/route53/responses.py +++ b/moto/route53/responses.py @@ -70,6 +70,35 @@ def rrset_response(request, full_url, headers): return 200, headers, template.render(record_sets=record_sets) +def health_check_response(request, full_url, headers): + parsed_url = urlparse(full_url) + method = request.method + + if method == "POST": + properties = xmltodict.parse(request.body)['CreateHealthCheckRequest']['HealthCheckConfig'] + health_check_args = { + "ip_address": properties.get('IPAddress'), + "port": properties.get('Port'), + "type": properties['Type'], + "resource_path": properties.get('ResourcePath'), + "fqdn": properties.get('FullyQualifiedDomainName'), + "search_string": properties.get('SearchString'), + "request_interval": properties.get('RequestInterval'), + "failure_threshold": properties.get('FailureThreshold'), + } + health_check = route53_backend.create_health_check(health_check_args) + template = Template(CREATE_HEALTH_CHECK_RESPONSE) + return 201, headers, template.render(health_check=health_check) + elif method == "DELETE": + health_check_id = parsed_url.path.split("/")[-1] + route53_backend.delete_health_check(health_check_id) + return 200, headers, DELETE_HEALTH_CHECK_REPONSE + elif method == "GET": + template = Template(LIST_HEALTH_CHECKS_REPONSE) + health_checks = route53_backend.get_health_checks() + return 200, headers, template.render(health_checks=health_checks) + + LIST_RRSET_REPONSE = """ {% for record_set in record_sets %} @@ -126,3 +155,23 @@ LIST_HOSTED_ZONES_RESPONSE = """ + + {{ health_check.to_xml() }} +""" + +LIST_HEALTH_CHECKS_REPONSE = """ + + + {% for health_check in health_checks %} + {{ health_check.to_xml() }} + {% endfor %} + + false + {{ health_checks|length }} +""" + +DELETE_HEALTH_CHECK_REPONSE = """ + +""" diff --git a/moto/route53/urls.py b/moto/route53/urls.py index c4031aded..69f776ff0 100644 --- a/moto/route53/urls.py +++ b/moto/route53/urls.py @@ -2,11 +2,12 @@ from __future__ import unicode_literals from . import responses url_bases = [ - "https://route53.amazonaws.com/201.-..-../hostedzone", + "https://route53.amazonaws.com/201.-..-../", ] url_paths = { - '{0}$': responses.list_or_create_hostzone_response, - '{0}/[^/]+$': responses.get_or_delete_hostzone_response, - '{0}/[^/]+/rrset$': responses.rrset_response, + '{0}hostedzone$': responses.list_or_create_hostzone_response, + '{0}hostedzone/[^/]+$': responses.get_or_delete_hostzone_response, + '{0}hostedzone/[^/]+/rrset$': responses.rrset_response, + '{0}healthcheck': responses.health_check_response, } diff --git a/tests/test_cloudformation/fixtures/route53_health_check.py b/tests/test_cloudformation/fixtures/route53_health_check.py new file mode 100644 index 000000000..6c6159fde --- /dev/null +++ b/tests/test_cloudformation/fixtures/route53_health_check.py @@ -0,0 +1,39 @@ +from __future__ import unicode_literals + +template = { + "Resources" : { + "HostedZone": { + "Type" : "AWS::Route53::HostedZone", + "Properties" : { + "Name" : "my_zone" + } + }, + + "my_health_check": { + "Type": "AWS::Route53::HealthCheck", + "Properties" : { + "HealthCheckConfig" : { + "FailureThreshold" : 3, + "IPAddress" : "10.0.0.4", + "Port" : 80, + "RequestInterval" : 10, + "ResourcePath" : "/", + "Type" : "HTTP", + } + } + }, + + "myDNSRecord" : { + "Type" : "AWS::Route53::RecordSet", + "Properties" : { + "HostedZoneName" : { "Ref" : "HostedZone" }, + "Comment" : "DNS name for my instance.", + "Name" : "my_record_set", + "Type" : "A", + "TTL" : "900", + "ResourceRecords" : ["my.example.com"], + "HealthCheckId": {"Ref": "my_health_check"}, + } + } + }, +} \ No newline at end of file diff --git a/tests/test_cloudformation/test_cloudformation_stack_integration.py b/tests/test_cloudformation/test_cloudformation_stack_integration.py index 636cdea24..944457ac4 100644 --- a/tests/test_cloudformation/test_cloudformation_stack_integration.py +++ b/tests/test_cloudformation/test_cloudformation_stack_integration.py @@ -28,6 +28,7 @@ from .fixtures import ( fn_join, rds_mysql_with_read_replica, route53_ec2_instance_with_public_ip, + route53_health_check, route53_roundrobin, single_instance_with_ebs_volume, vpc_eip, @@ -845,3 +846,38 @@ def test_route53_ec2_instance_with_public_ip(): record_set1.ttl.should.equal('900') record_set1.weight.should.equal(None) record_set1.resource_records[0].should.equal("10.0.0.25") + + +@mock_cloudformation() +@mock_route53() +def test_route53_associate_health_check(): + route53_conn = boto.connect_route53() + + template_json = json.dumps(route53_health_check.template) + conn = boto.cloudformation.connect_to_region("us-west-1") + stack = conn.create_stack( + "test_stack", + template_body=template_json, + ) + + checks = route53_conn.get_list_health_checks()['ListHealthChecksResponse']['HealthChecks'] + list(checks).should.have.length_of(1) + check = checks[0] + health_check_id = check['Id'] + config = check['HealthCheckConfig'] + config["FailureThreshold"].should.equal("3") + config["IPAddress"].should.equal("10.0.0.4") + config["Port"].should.equal("80") + config["RequestInterval"].should.equal("10") + config["ResourcePath"].should.equal("/") + config["Type"].should.equal("HTTP") + + zones = route53_conn.get_all_hosted_zones()['ListHostedZonesResponse']['HostedZones'] + list(zones).should.have.length_of(1) + zone_id = zones[0]['Id'] + + rrsets = route53_conn.get_all_rrsets(zone_id) + rrsets.should.have.length_of(1) + + record_set = rrsets[0] + record_set.health_check.should.equal(health_check_id) diff --git a/tests/test_route53/test_route53.py b/tests/test_route53/test_route53.py index fbfd6f2a5..5ba8907ef 100644 --- a/tests/test_route53/test_route53.py +++ b/tests/test_route53/test_route53.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals import boto +from boto.route53.healthcheck import HealthCheck from boto.route53.record import ResourceRecordSets import sure # noqa @@ -89,3 +90,79 @@ def test_rrset(): rrsets = conn.get_all_rrsets(zoneid, name="foo.foo.testdns.aws.com", type="A") rrsets.should.have.length_of(0) + + +@mock_route53 +def test_create_health_check(): + conn = boto.connect_route53('the_key', 'the_secret') + + check = HealthCheck( + ip_addr="10.0.0.25", + port=80, + hc_type="HTTP", + resource_path="/", + fqdn="example.com", + string_match="a good response", + request_interval=10, + failure_threshold=2, + ) + conn.create_health_check(check) + + checks = conn.get_list_health_checks()['ListHealthChecksResponse']['HealthChecks'] + list(checks).should.have.length_of(1) + check = checks[0] + config = check['HealthCheckConfig'] + config['IPAddress'].should.equal("10.0.0.25") + config['Port'].should.equal("80") + config['Type'].should.equal("HTTP") + config['ResourcePath'].should.equal("/") + config['FullyQualifiedDomainName'].should.equal("example.com") + config['SearchString'].should.equal("a good response") + config['RequestInterval'].should.equal("10") + config['FailureThreshold'].should.equal("2") + + +@mock_route53 +def test_delete_health_check(): + conn = boto.connect_route53('the_key', 'the_secret') + + check = HealthCheck( + ip_addr="10.0.0.25", + port=80, + hc_type="HTTP", + resource_path="/", + ) + conn.create_health_check(check) + + checks = conn.get_list_health_checks()['ListHealthChecksResponse']['HealthChecks'] + list(checks).should.have.length_of(1) + health_check_id = checks[0]['Id'] + + conn.delete_health_check(health_check_id) + checks = conn.get_list_health_checks()['ListHealthChecksResponse']['HealthChecks'] + list(checks).should.have.length_of(0) + + +@mock_route53 +def test_use_health_check_in_resource_record_set(): + conn = boto.connect_route53('the_key', 'the_secret') + + check = HealthCheck( + ip_addr="10.0.0.25", + port=80, + hc_type="HTTP", + resource_path="/", + ) + check = conn.create_health_check(check)['CreateHealthCheckResponse']['HealthCheck'] + check_id = check['Id'] + + zone = conn.create_hosted_zone("testdns.aws.com") + zone_id = zone["CreateHostedZoneResponse"]["HostedZone"]["Id"].split("/")[-1] + + changes = ResourceRecordSets(conn, zone_id) + change = changes.add_change("CREATE", "foo.bar.testdns.aws.com", "A", health_check=check_id) + change.add_value("1.2.3.4") + changes.commit() + + record_sets = conn.get_all_rrsets(zone_id) + record_sets[0].health_check.should.equal(check_id)