Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Support for Route53 health checks and health checked and failover records #1496

Closed
wants to merge 4 commits into from

8 participants

@calind
  • Ads support for Route53 health checks (list/create/delete)
  • boto.route53.zone.Zone methods add_record() and update_record() support failover and health checked records
  • Merges @xbe the changes from pull reuqest #1473
xbe and others added some commits
@toastdriven toastdriven commented on the diff
boto/route53/healthckeck.py
((28 lines not shown))
+
+ :ivar Route53Connection route53connection
+ :ivar str id: The ID of the healthcheck
+ :ivar str type: Type of the heathcheck ('tcp' or 'http')
+ :ivar str ip: The target ip of the check
+ :ivar int port: The target port of the check
+ :ivar str path: The path of 'http' check
+ :ivar str host: The host header for 'http' check
+ """
+ def __init__(self, route53connection, check_dict):
+ self.route53connection = route53connection
+ id = check_dict['Id']
+ check_dict = check_dict['HealthCheckConfig']
+ for key in check_dict:
+ if key == 'FullyQualifiedDomainName':
+ self.__setattr__('host', check_dict[key])

Why are these self.__setattr__(...) calls instead of setattr(self, ...) calls?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@toastdriven

Thanks for the pull request, patch looks alright. However, given how much this touches, we definitely need some tests to make sure we don't regress as mentioned in the contributing guide.

If you could write some tests, that would help a lot. Otherwise this will have to wait until a core dev can find some time to write tests for this.

Thanks again for the pull request.

@nonflet

Hey, people, so how about health checks ?

@boyand

Any plans for this to get merged ?

@danielgtaylor

@boyand somebody needs to add tests to get this merged as mentioned above. At the moment the core developers are working on other issues.

@mattdeboard

@danielgtaylor This pull request was fixed by e49dc3c no?

For people finding this from Google (it's the top-ranked result for "boto route 53 healthcheck" for me), this functionality is supported in Boto already.

@danielgtaylor

Yes, sorry I forgot to close this issue. Please see #2054 and specifically e49dc3c for the final implementation. This should go out in the next release.

@leezen

Looking at #2054 I didn't see the support for the Failover tag included, which was part of #1496. Can that part of this pull get merged in as well? PressLabs@3281f01 had this change as did 0e261dc

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on May 3, 2013
  1. @xbe
Commits on May 15, 2013
  1. @calind
  2. @calind
  3. @calind

    Merge branch 'route53-healtcheck' into develop, adding support for Ro…

    calind authored
    …ute53 healthchecked records
    
        * Includes commit '0e261dcb09398e6eab1e60fefd2f4f14c9526cf9' from @xbe fork
        * Adds support for creating/deleting healthchecks
This page is out of date. Refresh to see the latest.
View
171 boto/route53/connection.py
@@ -32,6 +32,7 @@
from boto import handler
from boto.route53.record import ResourceRecordSets
from boto.route53.zone import Zone
+from boto.route53.healthckeck import Healthcheck
import boto.jsonresponse
import exception
@@ -43,18 +44,29 @@
<Comment>%(comment)s</Comment>
</HostedZoneConfig>
</CreateHostedZoneRequest>"""
-
-#boto.set_stream_logger('dns')
+HCXML = """
+<CreateHealthCheckRequest xmlns="%(xmlns)s">
+ <CallerReference>%(caller_ref)s</CallerReference>
+ <HealthCheckConfig>
+ <IPAddress>%(ip)s</IPAddress>
+ <Port>%(port)d</Port>
+ <Type>%(type)s</Type>
+ %(custom)s
+ </HealthCheckConfig>
+</CreateHealthCheckRequest>
+"""
+
+#boto.set_stream_logger('dns')
class Route53Connection(AWSAuthConnection):
DefaultHost = 'route53.amazonaws.com'
"""The default Route53 API endpoint to connect to."""
- Version = '2012-02-29'
+ Version = '2012-12-12'
"""Route53 API version."""
- XMLNameSpace = 'https://route53.amazonaws.com/doc/2012-02-29/'
+ XMLNameSpace = 'https://route53.amazonaws.com/doc/2012-12-12/'
"""XML schema for this Route53 API version."""
def __init__(self, aws_access_key_id=None, aws_secret_access_key=None,
@@ -401,3 +413,154 @@ def _make_qualified(self, value):
if value and not value[-1] == '.':
value = "%s." % value
return value
+
+ # Health Checks
+ def get_all_healthchecks(self, start_marker=None, healthcheck_list=None):
+ """
+ Returns a Python data structure with information about all
+ health checks defined for the AWS account.
+
+ :param int start_marker: start marker to pass when fetching additional
+ results after a truncated list
+ :param list healthcheck_list: a HealthChecks list to prepend to results
+ """
+
+ params = {}
+ if start_marker:
+ params = {'marker': start_marker}
+ response = self.make_request('GET', '/%s/healthcheck' % self.Version,
+ 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)
+
+ if healthcheck_list:
+ e['ListHealthChecksResponse']['HealthChecks'].extend(
+ healthcheck_list)
+ while 'NextMarker' in e['ListHealthChecksResponse']:
+ next_marker = e['ListHealthChecksResponse']['NextMarker']
+ healthcheck_list = e['ListHealthChecksResponse']['HealthChecks']
+ e = self.get_all_healthchecks(next_marker, healthcheck_list)
+ return e
+
+ def get_healthchecks(self):
+ """
+ Returns a list of Healthcheck objects for all healthchecks defined
+ in the AWS Account
+ """
+ healthchecks = self.get_all_healthchecks()
+ return [Healthcheck(self, check) for check in
+ healthchecks['ListHealthChecksResponse']['HealthChecks']]
+
+ def get_healthcheck(self, healthcheck_id):
+ """
+ Return a Healthcheck for a healtch check specified by id
+
+ :type healthckeck_id: str
+ :param healthcheck_id: The unique identifier for the Health Check
+
+ """
+
+ uri = '/%s/healthcheck/%s' % (self.Version, healthcheck_id)
+ response = self.make_request('GET', uri)
+ 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='CreateHealthCheckResponse',
+ item_marker=('HealthCheck',))
+ h = boto.jsonresponse.XmlHandler(e, None)
+ h.parse(body)
+ return Healthcheck(self, e['GetHealthCheckResponse'][0])
+
+ def create_healthcheck(self, ip, port=None, type='tcp', path=None,
+ host=None, caller_ref=None):
+ """
+ Create a new Health Check. Returns Healthcheck object with the newly
+ created health check
+
+ :type ip: str
+ :param ip: The target IP of the healthcheck
+
+ :type port: int
+ :param port: The target port of the healthcheck. It healtchcheck type
+ if http and port is not specified it defaults to 80
+
+ :type type: str
+ :param type: The type of healthcheck. Can be one of 'tcp' or 'http'
+
+ :type path: str
+ :param path: Required for 'http' check type. The path must start
+ with '/'. If not specified defaults to '/'.
+
+ :type host: str
+ :param host: Sets the host header of a 'http' check type.
+
+ :type caller_ref: str
+ :param caller_ref: A unique string that identifies the request
+ and that allows failed CreateHostedZone 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())
+ custom = ''
+ if type == 'http':
+ port = port or 80
+ if path:
+ custom += "\n<ResourcePath>%s</ResourcePath>" % path
+ if host:
+ custom += ("\n<FullyQualifiedDomainName>%s"
+ "</FullyQualifiedDomainName>" % host)
+ if type == 'tcp':
+ path = ''
+ host = ''
+
+ params = {'ip': ip,
+ 'port': port,
+ 'type': type.upper(),
+ 'custom': custom,
+ 'caller_ref': caller_ref,
+ 'xmlns': self.XMLNameSpace}
+ xml_body = HCXML % params
+ uri = '/%s/healthcheck' % self.Version
+ response = self.make_request('POST', uri,
+ {'Content-Type': 'text/xml'}, xml_body)
+ body = response.read()
+ boto.log.debug(body)
+ if response.status != 201:
+ raise exception.DNSServerError(response.status,
+ response.reason,
+ body)
+
+ e = boto.jsonresponse.Element(list_marker='CreateHealthCheckRequest',
+ item_marker=('HealthCheck',))
+ h = boto.jsonresponse.XmlHandler(e, None)
+ h.parse(body)
+ return Healthcheck(self, e['CreateHealthCheckResponse']['HealthCheck'])
+
+ def delete_healthcheck(self, healthcheck_id):
+ uri = '/%s/healthcheck/%s' % (self.Version, healthcheck_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
View
61 boto/route53/healthckeck.py
@@ -0,0 +1,61 @@
+# Copyright (c) 2013 PressLabs SRL, Calin Don
+# www.presslabs.com
+# All rights reserved.
+#
+# 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.
+
+
+class Healthcheck(object):
+ """
+ A Route53 Healthcheck.
+
+ :ivar Route53Connection route53connection
+ :ivar str id: The ID of the healthcheck
+ :ivar str type: Type of the heathcheck ('tcp' or 'http')
+ :ivar str ip: The target ip of the check
+ :ivar int port: The target port of the check
+ :ivar str path: The path of 'http' check
+ :ivar str host: The host header for 'http' check
+ """
+ def __init__(self, route53connection, check_dict):
+ self.route53connection = route53connection
+ id = check_dict['Id']
+ check_dict = check_dict['HealthCheckConfig']
+ for key in check_dict:
+ if key == 'FullyQualifiedDomainName':
+ self.__setattr__('host', check_dict[key])

Why are these self.__setattr__(...) calls instead of setattr(self, ...) calls?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ elif key == 'IPAddress':
+ self.__setattr__('ip', check_dict[key])
+ elif key == 'ResourcePath':
+ self.__setattr__('path', check_dict[key])
+ elif key == 'Type':
+ self.__setattr__('type', check_dict[key].lower())
+ elif key == 'Port':
+ self.__setattr__('port', int(check_dict[key]))
+ else:
+ self.__setattr__(key.lower(), check_dict[key])
+ self.id = id
+
+ def __repr__(self):
+ return '<Healthcheck:%s (%s:%d)>' % (self.id, self.ip, self.port)
+
+ def delete(self):
+ """ Request AWS to delete this healthcheck """
+ self.route53connection.delete_healthcheck(self.id)
View
93 boto/route53/record.py
@@ -35,7 +35,7 @@ class ResourceRecordSets(ResultSet):
"""
ChangeResourceRecordSetsBody = """<?xml version="1.0" encoding="UTF-8"?>
- <ChangeResourceRecordSetsRequest xmlns="https://route53.amazonaws.com/doc/2012-02-29/">
+ <ChangeResourceRecordSetsRequest xmlns="https://route53.amazonaws.com/doc/2012-12-12/">
<ChangeBatch>
<Comment>%(comment)s</Comment>
<Changes>%(changes)s</Changes>
@@ -64,9 +64,9 @@ def __repr__(self):
return '<ResourceRecordSets:%s [%s]' % (self.hosted_zone_id,
record_list)
- def add_change(self, action, name, type, ttl=600,
- alias_hosted_zone_id=None, alias_dns_name=None, identifier=None,
- weight=None, region=None):
+ def add_change(self, action, name, type, ttl=600, alias_hosted_zone_id=None,
+ alias_dns_name=None, alias_evaluate_target_health=None, identifier=None,
+ failover_type=None, weight=None, region=None, health_check_id=None):
"""
Add a change request to the set.
@@ -94,35 +94,59 @@ def add_change(self, action, name, type, ttl=600,
:param ttl: The resource record cache time to live (TTL), in seconds.
:type alias_hosted_zone_id: str
- :param alias_dns_name: *Alias resource record sets only* The value
+ :param alias_hosted_zone_id: *Alias resource record sets only* The value
of the hosted zone ID, CanonicalHostedZoneNameId, for
the LoadBalancer.
:type alias_dns_name: str
- :param alias_hosted_zone_id: *Alias resource record sets only*
+ :param alias_dns_name: *Alias resource record sets only*
Information about the domain to which you are redirecting traffic.
+ :type alias_evaluate_target_health: str
+ :param alias_evaluate_target_health: *Alias resource record sets only*
+ Whether or not you want Route 53 to check the health of the alias
+ target that this alias record points to. The alias target must be
+ associated with a health check for this setting to have an effect.
+ Note that this variable defaults to "false".
+ Valid values are:
+
+ * true
+ * false
+
:type identifier: str
:param identifier: *Weighted and latency-based resource record sets
only* An identifier that differentiates among multiple resource
record sets that have the same combination of DNS name and type.
+ :type failover_type: str
+ :param failover_type: *Failover resource record sets only* Determines
+ whether this record set is active or passive. Note that this value
+ has no default.
+ Valid values are:
+
+ * PRIMARY
+ * SECONDARY
+
:type weight: int
:param weight: *Weighted resource record sets only* Among resource
record sets that have the same combination of DNS name and type,
a value that determines what portion of traffic for the current
- resource record set is routed to the associated location
+ resource record set is routed to the associated location.
:type region: str
:param region: *Latency-based resource record sets only* Among resource
record sets that have the same combination of DNS name and type,
a value that determines which region this should be associated with
- for the latency-based routing
+ for the latency-based routing.
+
+ :type health_check_id: str
+ :param health_check_id: The ID of the health check you wish 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_hosted_zone_id=alias_hosted_zone_id, alias_dns_name=alias_dns_name,
+ alias_evaluate_target_health=alias_evaluate_target_health, identifier=identifier,
+ failover_type=failover_type, weight=weight, region=region, health_check_id=health_check_id)
self.changes.append([action, change])
return change
@@ -180,6 +204,7 @@ class Record(object):
<Type>%(type)s</Type>
%(weight)s
%(body)s
+ %(health_check)s
</ResourceRecordSet>"""
WRRBody = """
@@ -192,6 +217,11 @@ class Record(object):
<Region>%(region)s</Region>
"""
+ FailoverBody = """
+ <SetIdentifier>%(identifier)s</SetIdentifier>
+ <Failover>%(failover_type)s</Failover>
+ """
+
ResourceRecordsBody = """
<TTL>%(ttl)s</TTL>
<ResourceRecords>
@@ -205,13 +235,16 @@ class Record(object):
AliasBody = """<AliasTarget>
<HostedZoneId>%s</HostedZoneId>
<DNSName>%s</DNSName>
+ <EvaluateTargetHealth>%s</EvaluateTargetHealth>
</AliasTarget>"""
+ HealthCheckBody = """<HealthCheckId>%s</HealthCheckId>"""
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_hosted_zone_id=None, alias_dns_name=None,
+ alias_evaluate_target_health=None, identifier=None,
+ failover_type=None, weight=None, region=None, health_check_id=None):
self.name = name
self.type = type
self.ttl = ttl
@@ -220,9 +253,14 @@ def __init__(self, name=None, type=None, ttl=600, resource_records=None,
self.resource_records = resource_records
self.alias_hosted_zone_id = alias_hosted_zone_id
self.alias_dns_name = alias_dns_name
+ if alias_evaluate_target_health == None:
+ alias_evaluate_target_health = "false"
+ self.alias_evaluate_target_health = alias_evaluate_target_health
self.identifier = identifier
+ self.failover_type = failover_type
self.weight = weight
self.region = region
+ self.health_check_id = health_check_id
def __repr__(self):
return '<Record:%s:%s:%s>' % (self.name, self.type, self.to_print())
@@ -240,7 +278,8 @@ def to_xml(self):
"""Spit this resource record set out as XML"""
if self.alias_hosted_zone_id != None and self.alias_dns_name != None:
# Use alias
- body = self.AliasBody % (self.alias_hosted_zone_id, self.alias_dns_name)
+ body = self.AliasBody % (self.alias_hosted_zone_id,
+ self.alias_dns_name, self.alias_evaluate_target_health)
else:
# Use resource record(s)
records = ""
@@ -257,12 +296,20 @@ def to_xml(self):
elif self.identifier != None and self.region != None:
weight = self.RRRBody % {"identifier": self.identifier, "region":
self.region}
-
+ elif self.identifier != None and self.failover_type != None:
+ weight = self.FailoverBody % {"identifier": self.identifier,
+ "failover_type": self.failover_type}
+
+ health_check = ""
+ if self.health_check_id != None:
+ health_check = self.HealthCheckBody % self.health_check_id
+
params = {
"name": self.name,
"type": self.type,
"weight": weight,
"body": body,
+ "health_check": health_check
}
return self.XMLBody % params
@@ -270,7 +317,8 @@ def to_print(self):
rr = ""
if self.alias_hosted_zone_id != None and self.alias_dns_name != None:
# Show alias
- rr = 'ALIAS ' + self.alias_hosted_zone_id + ' ' + self.alias_dns_name
+ rr = 'ALIAS %s %s Check alias health: %s' % (self.alias_hosted_zone_id,
+ self.alias_dns_name, self.alias_evaluate_target_health)
else:
# Show resource record(s)
rr = ",".join(self.resource_records)
@@ -279,6 +327,13 @@ def to_print(self):
rr += ' (WRR id=%s, w=%s)' % (self.identifier, self.weight)
elif self.identifier != None and self.region != None:
rr += ' (LBR id=%s, region=%s)' % (self.identifier, self.region)
+ elif self.identifier != None and self.failover_type != None and self.health_check_id != "":
+ rr += ' (Failover id=%s, Failover Type=%s, Health Check id=%s)' % (self.identifier,
+ self.failover_type, self.health_check_id)
+
+ #associated with a health check but is not a failover policy
+ if self.identifier != None and self.failover_type == None and self.health_check_id != "":
+ rr += ' (Health Check id=%s)' % (self.health_check_id)
return rr
@@ -295,12 +350,18 @@ def endElement(self, name, value, connection):
self.alias_hosted_zone_id = value
elif name == 'DNSName':
self.alias_dns_name = value
+ elif name == 'EvaluateTargetHealth':
+ self.alias_evaluate_target_health = value
elif name == 'SetIdentifier':
self.identifier = value
+ elif name == 'Failover':
+ self.failover_type = value
elif name == 'Weight':
self.weight = value
elif name == 'Region':
self.region = value
+ elif name == 'HealthCheckId':
+ self.health_check_id = value
def startElement(self, name, attrs, connection):
return None
View
41 boto/route53/zone.py
@@ -60,7 +60,7 @@ def _commit(self, changes):
return response['ChangeResourceRecordSetsResponse']['ChangeInfo']
def _new_record(self, changes, resource_type, name, value, ttl, identifier,
- comment=""):
+ health_check_id=None, failover_type=None, comment=""):
"""
Add a CREATE change record to an existing ResourceRecordSets
@@ -79,19 +79,36 @@ def _new_record(self, changes, resource_type, name, value, ttl, identifier,
:type ttl: int
:param ttl: The resource record cache time to live (TTL), in seconds.
- :type identifier: tuple
- :param identifier: A tuple for setting WRR or LBR attributes. Valid
- forms are:
+ :type identifier: tuple or string
+ :param identifier: In case of failover records, a string representing
+ the failover identifier. For WRR or LBR records, a tuple
+ containing identifier and weight/region. Valid forms:
* (str, int): WRR record [e.g. ('foo',10)]
* (str, str): LBR record [e.g. ('foo','us-east-1')
+ * str: Failover record [e.g. 'failover-active']
+
+ :type health_check_id: str
+ :param health_check_id: ID of the healthcheck to associate with. Don't
+ set if the record should not be associated with any healthcheck
+
+ :type failover_type: str
+ :param failover_type: The failover type of the record. Valid values
+ are. Set only if this record is part of active-passive
+ healthchecked record:
+
+ * PRIMARY
+ * SECONDARY
:type comment: str
:param comment: A comment that will be stored with the change.
"""
+
weight = None
region = None
- if identifier is not None:
+
+ if (identifier is not None and
+ type(identifier) not in [str, unicode]):
try:
int(identifier[1])
weight = identifier[1]
@@ -101,7 +118,9 @@ def _new_record(self, changes, resource_type, name, value, ttl, identifier,
identifier = identifier[0]
change = changes.add_change("CREATE", name, resource_type, ttl,
identifier=identifier, weight=weight,
- region=region)
+ region=region,
+ failover_type=failover_type,
+ health_check_id=health_check_id)
if type(value) in [list, tuple, set]:
for record in value:
change.add_value(record)
@@ -109,18 +128,19 @@ def _new_record(self, changes, resource_type, name, value, ttl, identifier,
change.add_value(value)
def add_record(self, resource_type, name, value, ttl=60, identifier=None,
- comment=""):
+ health_check_id=None, failover_type=None, comment=""):
"""
Add a new record to this Zone. See _new_record for parameter
documentation. Returns a Status object.
"""
changes = ResourceRecordSets(self.route53connection, self.id, comment)
self._new_record(changes, resource_type, name, value, ttl, identifier,
- comment)
+ health_check_id, failover_type, comment)
return Status(self.route53connection, self._commit(changes))
def update_record(self, old_record, new_value, new_ttl=None,
- new_identifier=None, comment=""):
+ new_identifier=None, new_health_check_id=None,
+ new_failover_type=None, comment=""):
"""
Update an existing record in this Zone. Returns a Status object.
@@ -134,7 +154,8 @@ def update_record(self, old_record, new_value, new_ttl=None,
changes = ResourceRecordSets(self.route53connection, self.id, comment)
changes.add_change_record("DELETE", record)
self._new_record(changes, record.type, record.name,
- new_value, new_ttl, new_identifier, comment)
+ new_value, new_ttl, new_identifier,
+ new_health_check_id, new_failover_type, comment)
return Status(self.route53connection, self._commit(changes))
def delete_record(self, record, comment=""):
Something went wrong with that request. Please try again.