Permalink
Browse files

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

…ctly.
  • Loading branch information...
1 parent f0b7b92 commit dee90ffd7cde70b44407e752935ee36a9bcc96f9 @gtaylor committed Nov 2, 2012
View
@@ -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,15 +78,23 @@ 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.
break
# 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"
)
@@ -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
@@ -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
@@ -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.
@@ -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)

0 comments on commit dee90ff

Please sign in to comment.