Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

We can now retrieve RRsets, paginate correctly, and instantiate corre…

…ctly.
  • Loading branch information...
commit dee90ffd7cde70b44407e752935ee36a9bcc96f9 1 parent f0b7b92
@gtaylor authored
View
65 route53/connection.py
@@ -41,7 +41,9 @@ def _send_request(self, path, data, method):
print(prettyprint_xml(root))
return root
- def _do_autopaginating_api_call(self, path, params, method, parser_func):
+ def _do_autopaginating_api_call(self, path, params, method, parser_func,
+ next_marker_xpath, next_marker_param_name,
+ next_type_xpath=None):
"""
Given an API method, the arguments passed to it, and a function to
hand parsing off to, loop through the record sets in the API call
@@ -52,6 +54,14 @@ def _do_autopaginating_api_call(self, path, params, method, parser_func):
:param dict params: The kwargs from the top-level API method.
:param callable parser_func: A callable that is used for parsing the
output from the API call.
+ :param str next_marker_param_name: The XPath to the marker tag that
+ will determine whether we continue paginating.
+ :param str next_marker_param_name: The parameter name to manipulate
+ in the request data to bring up the next page on the next
+ request loop.
+ :keyword str next_type_xpath: For the
+ py:meth:`list_resource_record_sets_by_zone_id` method, there's
+ an additional paginator token. Specifying this XPath looks for it.
:rtype: generator
:returns: Returns a generator that may be returned by the top-level
API method.
@@ -68,7 +78,7 @@ def _do_autopaginating_api_call(self, path, params, method, parser_func):
yield record
# This will determine at what offset we start the next query.
- next_marker = root.find("./{*}NextMarker")
+ next_marker = root.find(next_marker_xpath)
if next_marker is None:
# If the NextMarker tag is absent, we know we've hit the
# last page.
@@ -76,7 +86,15 @@ def _do_autopaginating_api_call(self, path, params, method, parser_func):
# if NextMarker is present, we'll adjust our API request params
# and query again for the next page.
- params["marker"] = next_marker.text
+ params[next_marker_param_name] = next_marker.text
+
+ if next_type_xpath:
+ # This is a list_resource_record_sets_by_zone_id call. Look
+ # for the given tag via XPath and adjust our type arg for
+ # the next request. Without specifying this, we loop
+ # infinitely.
+ next_type = root.find(next_type_xpath)
+ params['type'] = next_type.text
def list_hosted_zones(self, page_chunks=100):
"""
@@ -98,6 +116,8 @@ def list_hosted_zones(self, page_chunks=100):
params={'maxitems': page_chunks},
method='GET',
parser_func=xml_parsers.list_hosted_zones_parser,
+ next_marker_xpath="./{*}NextMarker",
+ next_marker_param_name="marker",
)
def create_hosted_zone(self, name, caller_reference=None, comment=None):
@@ -174,4 +194,43 @@ def delete_hosted_zone_by_id(self, id):
return xml_parsers.delete_hosted_zone_by_id_parser(
root=root,
connection=self,
+ )
+
+ def list_resource_record_sets_by_zone_id(self, id, rrset_type=None,
+ identifier=None, name=None,
+ page_chunks=100):
+ """
+ Lists resource record sets by Zone ID.
+
+ :param str id: The ID of the zone whose record sets we're listing.
+ :keyword str rrset_type: The type of resource record set to begin the
+ record listing from.
+ :keyword str identifier: Weighted and latency resource record sets
+ only: If results were truncated for a given DNS name and type,
+ the value of SetIdentifier for the next resource record set
+ that has the current DNS name and type.
+ :keyword str name: Not really sure what this does.
+ :keyword int page_chunks: This API call is paginated behind-the-scenes
+ by this many ResourceRecordSet instances. The default should be
+ fine for just about everybody, aside from those with tons of RRS.
+
+ :rtype: generator
+ :returns: A generator of ResourceRecordSet instances.
+ """
+
+ params = {
+ 'name': name,
+ 'type': rrset_type,
+ 'identifier': identifier,
+ 'maxitems': page_chunks,
+ }
+
+ return self._do_autopaginating_api_call(
+ path='hostedzone/%s/rrset' % id,
+ params=params,
+ method='GET',
+ parser_func=xml_parsers.list_resource_record_sets_by_zone_id_parser,
+ next_marker_xpath="./{*}NextRecordName",
+ next_marker_param_name="name",
+ next_type_xpath="./{*}NextRecordType"
)
View
114 route53/resource_record_set.py
@@ -0,0 +1,114 @@
+
+class ResourceRecordSet(object):
+ """
+ A Resource Record Set is an entry within a Hosted Zone. These can be
+ anything from TXT entries, to A entries, to CNAMEs.
+
+ .. warning:: Do not instantiate this directly yourself. Go through
+ one of the methods on:py:class:``route53.connection.Route53Connection`.
+ """
+
+ def __init__(self, connection, name, rrset_type, ttl, records):
+ """
+ :param Route53Connection connection: The connection instance that
+ was used to query the Route53 API, leading to this object's
+ creation.
+ :param str name: The fully qualified name of the resource record set.
+ :param int ttl: The time-to-live. A Aliases have no TTL, so this can
+ be None in that case.
+ :param list records: A list of resource record strings. For some
+ types (A entries that are Aliases), this is an empty list.
+ """
+
+ self.connection = connection
+ self.name = name
+ self.rrset_type = rrset_type
+ self.ttl = int(ttl) if ttl else None
+ self.records = records
+
+ def __str__(self):
+ return '<ResourceRecordSet: %s -- %s>' % (self.name, self.rrset_type)
+
+
+class AResourceRecordSet(ResourceRecordSet):
+ """
+ Specific A record class.
+ """
+
+ def __init__(self, alias_hosted_zone_id=None, alias_dns_name=None, *args, **kwargs):
+ self.alias_hosted_zone_id = alias_hosted_zone_id
+ self.alias_dns_name = alias_dns_name
+
+ super(AResourceRecordSet, self).__init__(*args, **kwargs)
+
+
+class AAAAResourceRecordSet(ResourceRecordSet):
+ """
+ Specific AAAA record class.
+ """
+
+ pass
+
+
+class CNAMEResourceRecordSet(ResourceRecordSet):
+ """
+ Specific CNAME record class.
+ """
+
+ pass
+
+
+class MXResourceRecordSet(ResourceRecordSet):
+ """
+ Specific MX record class.
+ """
+
+ pass
+
+
+class NSResourceRecordSet(ResourceRecordSet):
+ """
+ Specific NS record class.
+ """
+
+ pass
+
+
+class PTRResourceRecordSet(ResourceRecordSet):
+ """
+ Specific PTR record class.
+ """
+
+ pass
+
+
+class SOAResourceRecordSet(ResourceRecordSet):
+ """
+ Specific SOA record class.
+ """
+
+ pass
+
+
+class SPFResourceRecordSet(ResourceRecordSet):
+ """
+ Specific SPF record class.
+ """
+
+ pass
+
+
+class SRVResourceRecordSet(ResourceRecordSet):
+ """
+ Specific SRV record class.
+ """
+
+ pass
+
+
+class TXTResourceRecordSet(ResourceRecordSet):
+ """
+ Specific TXT record class.
+ """
+
+ pass
View
3  route53/xml_parsers/__init__.py
@@ -1,4 +1,5 @@
from .list_hosted_zones import list_hosted_zones_parser
from .created_hosted_zone import created_hosted_zone_parser
from .get_hosted_zone_by_id import get_hosted_zone_by_id_parser
-from .delete_hosted_zone_by_id import delete_hosted_zone_by_id_parser
+from .delete_hosted_zone_by_id import delete_hosted_zone_by_id_parser
+from .list_resource_record_sets_by_zone_id import list_resource_record_sets_by_zone_id_parser
View
14 route53/xml_parsers/common_hosted_zone.py
@@ -16,12 +16,12 @@
'ResourceRecordSetCount': 'resource_record_set_count',
}
-def parse_hosted_zone(zone, connection):
+def parse_hosted_zone(e_zone, connection):
"""
This a common parser that allows the passing of any valid HostedZone
tag. It will spit out the appropriate HostedZone object for the tag.
- :param lxml.etree._Element zone: The root node of the etree parsed
+ :param lxml.etree._Element e_zone: The root node of the etree parsed
response from the API.
:param Route53Connection connection: The connection instance used to
query the API.
@@ -33,16 +33,16 @@ def parse_hosted_zone(zone, connection):
kwargs = {}
# Within HostedZone tags are a number of sub-tags that include info
# about the instance.
- for field in zone:
+ for e_field in e_zone:
# Cheesy way to strip off the namespace.
- tag_name = field.tag.split('}')[1]
- field_text = field.text
+ tag_name = e_field.tag.split('}')[1]
+ field_text = e_field.text
if tag_name == 'Config':
# Config has the Comment tag beneath it, needing
# special handling.
- comment = field.find('./{*}Comment')
- kwargs['comment'] = comment.text if comment is not None else None
+ e_comment = e_field.find('./{*}Comment')
+ kwargs['comment'] = e_comment.text if e_comment is not None else None
continue
elif tag_name == 'Id':
# This comes back with a path prepended. Yank that sillyness.
View
132 route53/xml_parsers/list_resource_record_sets_by_zone_id.py
@@ -0,0 +1,132 @@
+from route53.exceptions import Route53Error
+from route53.resource_record_set import AResourceRecordSet, AAAAResourceRecordSet, CNAMEResourceRecordSet, MXResourceRecordSet, NSResourceRecordSet, PTRResourceRecordSet, SOAResourceRecordSet, SPFResourceRecordSet, SRVResourceRecordSet, TXTResourceRecordSet
+
+# Maps ResourceRecordSet subtag names to kwargs in RRSet subclasses.
+RRSET_TAG_TO_KWARG_MAP = {
+ 'Name': 'name',
+ 'Type': 'rrset_type',
+ 'TTL': 'ttl',
+}
+
+# Maps the various ResourceRecordSet Types to various RRSet subclasses.
+RRSET_TYPE_TO_RSET_SUBCLASS_MAP = {
+ 'A': AResourceRecordSet,
+ 'AAAA': AAAAResourceRecordSet,
+ 'CNAME': CNAMEResourceRecordSet,
+ 'MX': MXResourceRecordSet,
+ 'NS': NSResourceRecordSet,
+ 'PTR': PTRResourceRecordSet,
+ 'SOA': SOAResourceRecordSet,
+ 'SPF': SPFResourceRecordSet,
+ 'SRV': SRVResourceRecordSet,
+ 'TXT': TXTResourceRecordSet,
+}
+
+def parse_rrset_alias(e_alias):
+ """
+ Parses an Alias tag beneath a ResourceRecordSet, spitting out the two values
+ found within. This is specific to A records that are set to Alias.
+
+ :param lxml.etree._Element e_alias: An Alias tag beneath a ResourceRecordSet.
+ :rtype: tuple
+ :returns: A tuple in the form of ``(alias_hosted_zone_id, alias_dns_name)``.
+ """
+
+ alias_hosted_zone_id = e_alias.find('./{*}HostedZoneId').text
+ alias_dns_name = e_alias.find('./{*}DNSName').text
+ return alias_hosted_zone_id, alias_dns_name
+
+def parse_rrset_record_values(e_resource_records):
+ """
+ Used to parse the various Values from the ResourceRecords tags on
+ most rrset types.
+
+ :param lxml.etree._Element e_resource_records: A ResourceRecords tag
+ beneath a ResourceRecordSet.
+ :rtype: list
+ :returns: A list of resource record strings.
+ """
+
+ records = []
+
+ for e_record in e_resource_records:
+ for e_value in e_record:
+ records.append(e_value.text)
+
+ return records
+
+def parse_rrset(e_rrset, connection):
+ """
+ This a parser that allows the passing of any valid ResourceRecordSet
+ tag. It will spit out the appropriate ResourceRecordSet object for the tag.
+
+ :param lxml.etree._Element e_rrset: The root node of the etree parsed
+ response from the API.
+ :param Route53Connection connection: The connection instance used to
+ query the API.
+ :rtype: ResourceRecordSet
+ :returns: An instantiated ResourceRecordSet object.
+ """
+
+ # This dict will be used to instantiate a ResourceRecordSet instance to yield.
+ kwargs = {'connection': connection}
+ rrset_type = None
+
+ for e_field in e_rrset:
+ # Cheesy way to strip off the namespace.
+ tag_name = e_field.tag.split('}')[1]
+ field_text = e_field.text
+
+ if tag_name == 'Type':
+ # Need to store this to determine which ResourceRecordSet
+ # subclass to instantiate.
+ rrset_type = field_text
+ elif tag_name == 'AliasTarget':
+ # A records have some special field values we need.
+ alias_hosted_zone_id, alias_dns_name = parse_rrset_alias(e_field)
+ kwargs['alias_hosted_zone_id'] = alias_hosted_zone_id
+ kwargs['alias_dns_name'] = alias_dns_name
+ # Alias A entries have no TTL.
+ kwargs['ttl'] = None
+ continue
+ elif tag_name == 'ResourceRecords':
+ kwargs['records'] = parse_rrset_record_values(e_field)
+ continue
+
+ # Map the XML tag name to a kwarg name.
+ kw_name = RRSET_TAG_TO_KWARG_MAP[tag_name]
+ # This will be the key/val pair used to instantiate the
+ # ResourceRecordSet instance.
+ kwargs[kw_name] = field_text
+
+ if not rrset_type:
+ raise Route53Error("No Type tag found in ListResourceRecordSetsResponse.")
+
+ if 'records' not in kwargs:
+ # Not all rrsets have records.
+ kwargs['records'] = []
+
+ RRSetSubclass = RRSET_TYPE_TO_RSET_SUBCLASS_MAP[rrset_type]
+ return RRSetSubclass(**kwargs)
+
+def list_resource_record_sets_by_zone_id_parser(e_root, connection):
+ """
+ Parses the API responses for the
+ :py:meth:`route53.connection.Route53Connection.list_resource_record_sets_by_zone_id`
+ method.
+
+ :param lxml.etree._Element e_root: The root node of the etree parsed
+ response from the API.
+ :param Route53Connection connection: The connection instance used to
+ query the API.
+ :rtype: ResourceRecordSet
+ :returns: A generator of fully formed ResourceRecordSet instances.
+ """
+
+ # The rest of the list pagination tags are handled higher up in the stack.
+ # We'll just worry about the ResourceRecordSets tag, which has
+ # ResourceRecordSet tags nested beneath it.
+ e_rrsets = e_root.find('./{*}ResourceRecordSets')
+
+ for e_rrset in e_rrsets:
+ yield parse_rrset(e_rrset, connection)
Please sign in to comment.
Something went wrong with that request. Please try again.