Skip to content
Browse files

Merge branch 'tellybug-develop' into develop.

Adds support for Route53 health checks. Fixes #2054.

Conflicts:
	boto/route53/record.py
	tests/integration/route53/test_resourcerecordsets.py
	tests/integration/route53/test_zone.py
  • Loading branch information...
2 parents 6843c16 + c8b4db6 commit e49dc3c7c75aee4d3d7ac1a65478bf0d9bec64f7 @danielgtaylor danielgtaylor committed
View
95 boto/route53/connection.py
@@ -226,6 +226,101 @@ def delete_hosted_zone(self, hosted_zone_id):
h.parse(body)
return e
+
+ # Health checks
+
+ POSTHCXMLBody = """<CreateHealthCheckRequest xmlns="%(xmlns)s">
+ <CallerReference>%(caller_ref)s</CallerReference>
+ %(health_check)s
+ </CreateHealthCheckRequest>"""
+
+ def create_health_check(self, health_check, caller_ref=None):
+ """
+ Create a new Health Check
+
+ :type health_check: HealthCheck
+ :param health_check: HealthCheck object
+
+ :type caller_ref: str
+ :param caller_ref: A unique string that identifies the request
+ and that allows failed CreateHealthCheckRequest requests to be retried
+ without the risk of executing the operation twice. If you don't
+ provide a value for this, boto will generate a Type 4 UUID and
+ use that.
+
+ """
+ if caller_ref is None:
+ caller_ref = str(uuid.uuid4())
+ uri = '/%s/healthcheck' % self.Version
+ params = {'xmlns': self.XMLNameSpace,
+ 'caller_ref': caller_ref,
+ 'health_check': health_check.to_xml()
+ }
+ xml_body = self.POSTHCXMLBody % params
+ response = self.make_request('POST', uri, {'Content-Type': 'text/xml'}, xml_body)
+ body = response.read()
+ boto.log.debug(body)
+ if response.status == 201:
+ e = boto.jsonresponse.Element()
+ h = boto.jsonresponse.XmlHandler(e, None)
+ h.parse(body)
+ return e
+ else:
+ raise exception.DNSServerError(response.status, response.reason, body)
+
+ def get_list_health_checks(self, maxitems=None, marker=None):
+ """
+ Return a list of health checks
+
+ :type maxitems: int
+ :param maxitems: Maximum number of items to return
+
+ :type marker: str
+ :param marker: marker to get next set of items to list
+
+ """
+
+ params = {}
+ if maxitems is not None:
+ params['maxitems'] = maxitems
+ if marker is not None:
+ params['marker'] = marker
+
+ uri = '/%s/healthcheck' % (self.Version, )
+ response = self.make_request('GET', uri, params=params)
+ body = response.read()
+ boto.log.debug(body)
+ if response.status >= 300:
+ raise exception.DNSServerError(response.status,
+ response.reason,
+ body)
+ e = boto.jsonresponse.Element(list_marker='HealthChecks', item_marker=('HealthCheck',))
+ h = boto.jsonresponse.XmlHandler(e, None)
+ h.parse(body)
+ return e
+
+ def delete_health_check(self, health_check_id):
+ """
+ Delete a health check
+
+ :type health_check_id: str
+ :param health_check_id: ID of the health check to delete
+
+ """
+ uri = '/%s/healthcheck/%s' % (self.Version, health_check_id)
+ response = self.make_request('DELETE', uri)
+ body = response.read()
+ boto.log.debug(body)
+ if response.status not in (200, 204):
+ raise exception.DNSServerError(response.status,
+ response.reason,
+ body)
+ e = boto.jsonresponse.Element()
+ h = boto.jsonresponse.XmlHandler(e, None)
+ h.parse(body)
+ return e
+
+
# Resource Record Sets
def get_all_rrsets(self, hosted_zone_id, type=None,
View
111 boto/route53/healthcheck.py
@@ -0,0 +1,111 @@
+# Copyright (c) 2014 Tellybug, Matt Millar
+#
+# Permission is hereby granted, free of charge, to any person obtaining a
+# copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish, dis-
+# tribute, sublicense, and/or sell copies of the Software, and to permit
+# persons to whom the Software is furnished to do so, subject to the fol-
+# lowing conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL-
+# ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
+# SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
+#
+
+
+"""
+From http://docs.aws.amazon.com/Route53/latest/APIReference/API_CreateHealthCheck.html
+
+POST /2013-04-01/healthcheck HTTP/1.1
+
+<?xml version="1.0" encoding="UTF-8"?>
+<CreateHealthCheckRequest xmlns="https://route53.amazonaws.com/doc/2013-04-01/">
+ <CallerReference>unique description</CallerReference>
+ <HealthCheckConfig>
+ <IPAddress>IP address of the endpoint to check</IPAddress>
+ <Port>port on the endpoint to check</Port>
+ <Type>HTTP | HTTPS | HTTP_STR_MATCH | HTTPS_STR_MATCH | TCP</Type>
+ <ResourcePath>path of the file that
+ you want Amazon Route 53 to request</ResourcePath>
+ <FullyQualifiedDomainName>domain name of the
+ endpoint to check</FullyQualifiedDomainName>
+ <SearchString>if Type is HTTP_STR_MATCH or HTTPS_STR_MATCH,
+ the string to search for in the response body
+ from the specified resource</SearchString>
+ </HealthCheckConfig>
+</CreateHealthCheckRequest"""
+
+
+class HealthCheck(object):
+
+ """An individual health check"""
+
+ POSTXMLBody = """
+ <HealthCheckConfig>
+ <IPAddress>%(ip_addr)s</IPAddress>
+ <Port>%(port)s</Port>
+ <Type>%(type)s</Type>
+ <ResourcePath>%(resource_path)s</ResourcePath>
+ %(fqdn_part)s
+ %(string_match_part)s
+ </HealthCheckConfig>
+ """
+
+ XMLFQDNPart = """<FullyQualifiedDomainName>%(fqdn)s</FullyQualifiedDomainName>"""
+
+ XMLStringMatchPart = """<SearchString>%(string_match)s</SearchString>"""
+
+ def __init__(self, ip_addr, port, hc_type, resource_path, fqdn=None, string_match=None):
+ """
+ HealthCheck object
+
+ :type ip_addr: str
+ :param ip_addr: IP Address
+
+ :type port: int
+ :param port: Port to check
+
+ :type hc_type: str
+ :param ip_addr: One of HTTP | HTTPS | HTTP_STR_MATCH | HTTPS_STR_MATCH | TCP
+
+ :type resource_path: str
+ :param resource_path: Path to check
+
+ :type fqdn: str
+ :param fqdn: domain name of the endpoint to check
+
+ :type string_match: str
+ :param string_match: if hc_type is HTTP_STR_MATCH or HTTPS_STR_MATCH, the string to search for in the response body from the specified resource
+
+ """
+ self.ip_addr = ip_addr
+ self.port = port
+ self.hc_type = hc_type
+ self.resource_path = resource_path
+ self.fqdn = fqdn
+ self.string_match = string_match
+
+ def to_xml(self):
+ params = {
+ 'ip_addr': self.ip_addr,
+ 'port': self.port,
+ 'type': self.hc_type,
+ 'resource_path': self.resource_path,
+ 'fqdn_part': "",
+ 'string_match_part': "",
+ }
+ if self.fqdn is not None:
+ params['fqdn_part'] = self.XMLFQDNPart % {'fqdn': self.fqdn}
+
+ if self.string_match is not None:
+ params['string_match_part'] = self.XMLStringMatchPart % {'string_match' : self.string_match}
+
+ return self.POSTXMLBody % params
View
21 boto/route53/record.py
@@ -66,7 +66,8 @@ def __repr__(self):
def add_change(self, action, name, type, ttl=600,
alias_hosted_zone_id=None, alias_dns_name=None, identifier=None,
- weight=None, region=None, alias_evaluate_target_health=None):
+ weight=None, region=None, alias_evaluate_target_health=None,
+ health_check=None):
"""
Add a change request to the set.
@@ -125,11 +126,15 @@ def add_change(self, action, name, type, ttl=600,
any health checks associated with the ALIAS target record which it is
linked to.
+ :type health_check: str
+ :param health_check: Health check to associate with this record
"""
change = Record(name, type, ttl,
alias_hosted_zone_id=alias_hosted_zone_id,
alias_dns_name=alias_dns_name, identifier=identifier,
- weight=weight, region=region, alias_evaluate_target_health=alias_evaluate_target_health)
+ weight=weight, region=region,
+ alias_evaluate_target_health=alias_evaluate_target_health,
+ health_check=health_check)
self.changes.append([action, change])
return change
@@ -185,11 +190,14 @@ def __iter__(self):
class Record(object):
"""An individual ResourceRecordSet"""
+ HealthCheckBody = """<HealthCheckId>%s</HealthCheckId>"""
+
XMLBody = """<ResourceRecordSet>
<Name>%(name)s</Name>
<Type>%(type)s</Type>
%(weight)s
%(body)s
+ %(health_check)s
</ResourceRecordSet>"""
WRRBody = """
@@ -223,7 +231,8 @@ class Record(object):
def __init__(self, name=None, type=None, ttl=600, resource_records=None,
alias_hosted_zone_id=None, alias_dns_name=None, identifier=None,
- weight=None, region=None, alias_evaluate_target_health=None):
+ weight=None, region=None, alias_evaluate_target_health=None,
+ health_check=None):
self.name = name
self.type = type
self.ttl = ttl
@@ -236,6 +245,7 @@ def __init__(self, name=None, type=None, ttl=600, resource_records=None,
self.weight = weight
self.region = region
self.alias_evaluate_target_health = alias_evaluate_target_health
+ self.health_check = health_check
def __repr__(self):
return '<Record:%s:%s:%s>' % (self.name, self.type, self.to_print())
@@ -282,11 +292,16 @@ def to_xml(self):
weight = self.RRRBody % {"identifier": self.identifier, "region":
self.region}
+ health_check = ""
+ if self.health_check is not None:
+ health_check = self.HealthCheckBody % (self.health_check)
+
params = {
"name": self.name,
"type": self.type,
"weight": weight,
"body": body,
+ "health_check": health_check
}
return self.XMLBody % params
View
18 tests/integration/route53/__init__.py
@@ -1,4 +1,5 @@
# Copyright (c) 2012 Mitch Garnaat http://garnaat.org/
+# Copyright (c) 2014 Tellybug, Matt Millar
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the
@@ -18,3 +19,20 @@
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
+
+import time
+import unittest
+from nose.plugins.attrib import attr
+from boto.route53.connection import Route53Connection
+
+@attr(route53=True)
+class Route53TestCase(unittest.TestCase):
+ def setUp(self):
+ super(Route53TestCase, self).setUp()
+ self.conn = Route53Connection()
+ self.base_domain = 'boto-test-%s.com' % str(int(time.time()))
+ self.zone = self.conn.create_zone(self.base_domain)
+
+ def tearDown(self):
+ self.zone.delete()
+ super(Route53TestCase, self).tearDown()
View
2 tests/integration/route53/test_cert_verification.py
@@ -25,12 +25,14 @@
Check that all of the certs on all service endpoints validate.
"""
import unittest
+from nose.plugins.attrib import attr
from tests.integration import ServiceCertVerificationTest
import boto.route53
+@attr(route53=True)
class Route53CertVerificationTest(unittest.TestCase, ServiceCertVerificationTest):
route53 = True
regions = boto.route53.regions()
View
117 tests/integration/route53/test_health_check.py
@@ -0,0 +1,117 @@
+# Copyright (c) 2014 Tellybug, Matt Millar
+#
+# Permission is hereby granted, free of charge, to any person obtaining a
+# copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish, dis-
+# tribute, sublicense, and/or sell copies of the Software, and to permit
+# persons to whom the Software is furnished to do so, subject to the fol-
+# lowing conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL-
+# ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
+# SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
+#
+
+from tests.integration.route53 import Route53TestCase
+
+from boto.route53.healthcheck import HealthCheck
+from boto.route53.record import ResourceRecordSets
+
+class TestRoute53HealthCheck(Route53TestCase):
+ def test_create_health_check(self):
+ hc = HealthCheck(ip_addr="54.217.7.118", port=80, hc_type="HTTP", resource_path="/testing")
+ result = self.conn.create_health_check(hc)
+ self.assertEquals(result[u'CreateHealthCheckResponse'][u'HealthCheck'][u'HealthCheckConfig'][u'Type'], 'HTTP')
+ self.assertEquals(result[u'CreateHealthCheckResponse'][
+ u'HealthCheck'][u'HealthCheckConfig'][u'IPAddress'], '54.217.7.118')
+ self.assertEquals(result[u'CreateHealthCheckResponse'][u'HealthCheck'][u'HealthCheckConfig'][u'Port'], '80')
+ self.assertEquals(result[u'CreateHealthCheckResponse'][
+ u'HealthCheck'][u'HealthCheckConfig'][u'ResourcePath'], '/testing')
+ self.conn.delete_health_check(result['CreateHealthCheckResponse']['HealthCheck']['Id'])
+
+ def test_create_https_health_check(self):
+ hc = HealthCheck(ip_addr="54.217.7.118", port=80, hc_type="HTTPS", resource_path="/testing")
+ result = self.conn.create_health_check(hc)
+ self.assertEquals(result[u'CreateHealthCheckResponse'][u'HealthCheck'][u'HealthCheckConfig'][u'Type'], 'HTTPS')
+ self.assertEquals(result[u'CreateHealthCheckResponse'][
+ u'HealthCheck'][u'HealthCheckConfig'][u'IPAddress'], '54.217.7.118')
+ self.assertEquals(result[u'CreateHealthCheckResponse'][u'HealthCheck'][u'HealthCheckConfig'][u'Port'], '80')
+ self.assertEquals(result[u'CreateHealthCheckResponse'][
+ u'HealthCheck'][u'HealthCheckConfig'][u'ResourcePath'], '/testing')
+ self.conn.delete_health_check(result['CreateHealthCheckResponse']['HealthCheck']['Id'])
+
+
+ def test_create_and_list_health_check(self):
+ hc = HealthCheck(ip_addr="54.217.7.118", port=80, hc_type="HTTP", resource_path="/testing")
+ result1 = self.conn.create_health_check(hc)
+ hc = HealthCheck(ip_addr="54.217.7.119", port=80, hc_type="HTTP", resource_path="/testing")
+ result2 = self.conn.create_health_check(hc)
+ result = self.conn.get_list_health_checks()
+ self.assertTrue(len(result['ListHealthChecksResponse']['HealthChecks']) > 1)
+ self.conn.delete_health_check(result1['CreateHealthCheckResponse']['HealthCheck']['Id'])
+ self.conn.delete_health_check(result2['CreateHealthCheckResponse']['HealthCheck']['Id'])
+
+ def test_delete_health_check(self):
+ hc = HealthCheck(ip_addr="54.217.7.118", port=80, hc_type="HTTP", resource_path="/testing")
+ result = self.conn.create_health_check(hc)
+ hc_id = result['CreateHealthCheckResponse']['HealthCheck']['Id']
+ result = self.conn.get_list_health_checks()
+ found = False
+ for hc in result['ListHealthChecksResponse']['HealthChecks']:
+ if hc['Id'] == hc_id:
+ found = True
+ break
+ self.assertTrue(found)
+ result = self.conn.delete_health_check(hc_id)
+ result = self.conn.get_list_health_checks()
+ for hc in result['ListHealthChecksResponse']['HealthChecks']:
+ self.assertFalse(hc['Id'] == hc_id)
+
+ def test_create_health_check_string_match(self):
+ hc = HealthCheck(ip_addr="54.217.7.118", port=80, hc_type="HTTP_STR_MATCH", resource_path="/testing", string_match="test")
+ result = self.conn.create_health_check(hc)
+ self.assertEquals(result[u'CreateHealthCheckResponse'][u'HealthCheck'][u'HealthCheckConfig'][u'Type'], 'HTTP_STR_MATCH')
+ self.assertEquals(result[u'CreateHealthCheckResponse'][
+ u'HealthCheck'][u'HealthCheckConfig'][u'IPAddress'], '54.217.7.118')
+ self.assertEquals(result[u'CreateHealthCheckResponse'][u'HealthCheck'][u'HealthCheckConfig'][u'Port'], '80')
+ self.assertEquals(result[u'CreateHealthCheckResponse'][
+ u'HealthCheck'][u'HealthCheckConfig'][u'ResourcePath'], '/testing')
+ self.assertEquals(result[u'CreateHealthCheckResponse'][u'HealthCheck'][u'HealthCheckConfig'][u'SearchString'], 'test')
+ self.conn.delete_health_check(result['CreateHealthCheckResponse']['HealthCheck']['Id'])
+
+ def test_create_health_check_https_string_match(self):
+ hc = HealthCheck(ip_addr="54.217.7.118", port=80, hc_type="HTTPS_STR_MATCH", resource_path="/testing", string_match="test")
+ result = self.conn.create_health_check(hc)
+ self.assertEquals(result[u'CreateHealthCheckResponse'][u'HealthCheck'][u'HealthCheckConfig'][u'Type'], 'HTTPS_STR_MATCH')
+ self.assertEquals(result[u'CreateHealthCheckResponse'][
+ u'HealthCheck'][u'HealthCheckConfig'][u'IPAddress'], '54.217.7.118')
+ self.assertEquals(result[u'CreateHealthCheckResponse'][u'HealthCheck'][u'HealthCheckConfig'][u'Port'], '80')
+ self.assertEquals(result[u'CreateHealthCheckResponse'][
+ u'HealthCheck'][u'HealthCheckConfig'][u'ResourcePath'], '/testing')
+ self.assertEquals(result[u'CreateHealthCheckResponse'][u'HealthCheck'][u'HealthCheckConfig'][u'SearchString'], 'test')
+ self.conn.delete_health_check(result['CreateHealthCheckResponse']['HealthCheck']['Id'])
+
+
+ def test_create_resource_record_set(self):
+ hc = HealthCheck(ip_addr="54.217.7.118", port=80, hc_type="HTTP", resource_path="/testing")
+ result = self.conn.create_health_check(hc)
+ records = ResourceRecordSets(
+ connection=self.conn, hosted_zone_id=self.zone.id, comment='Create DNS entry for test')
+ change = records.add_change('CREATE', 'unittest.%s.' % self.base_domain, 'A', ttl=30, identifier='test',
+ weight=1, health_check=result['CreateHealthCheckResponse']['HealthCheck']['Id'])
+ change.add_value("54.217.7.118")
+ records.commit()
+
+ records = ResourceRecordSets(self.conn, self.zone.id)
+ deleted = records.add_change('DELETE', "unittest.%s." % self.base_domain, "A", ttl=30, identifier='test',
+ weight=1, health_check=result['CreateHealthCheckResponse']['HealthCheck']['Id'])
+ deleted.add_value('54.217.7.118')
+ records.commit()
View
15 tests/integration/route53/test_resourcerecordsets.py
@@ -22,21 +22,12 @@
import time
import unittest
+from tests.integration.route53 import Route53TestCase
+
from boto.route53.connection import Route53Connection
from boto.route53.record import ResourceRecordSets
-
-class TestRoute53ResourceRecordSets(unittest.TestCase):
- def setUp(self):
- super(TestRoute53ResourceRecordSets, self).setUp()
- self.conn = Route53Connection()
- self.base_domain = 'boto-test-%s.com' % str(int(time.time()))
- self.zone = self.conn.create_zone(self.base_domain)
-
- def tearDown(self):
- self.zone.delete()
- super(TestRoute53ResourceRecordSets, self).tearDown()
-
+class TestRoute53ResourceRecordSets(Route53TestCase):
def test_add_change(self):
rrs = ResourceRecordSets(self.conn, self.zone.id)
View
2 tests/integration/route53/test_zone.py
@@ -24,10 +24,12 @@
import time
import unittest
+from nose.plugins.attrib import attr
from boto.route53.connection import Route53Connection
from boto.exception import TooManyRecordsException
+@attr(route53=True)
class TestRoute53Zone(unittest.TestCase):
@classmethod
def setUpClass(self):
View
2 tests/unit/route53/test_connection.py
@@ -28,10 +28,12 @@
from boto.route53.record import ResourceRecordSets, Record
from boto.route53.zone import Zone
+from nose.plugins.attrib import attr
from tests.unit import unittest
from tests.unit import AWSMockServiceTestCase
+@attr(route53=True)
class TestRoute53Connection(AWSMockServiceTestCase):
connection_class = Route53Connection

0 comments on commit e49dc3c

Please sign in to comment.
Something went wrong with that request. Please try again.