mirror of
https://github.com/ansible/awx.git
synced 2026-05-22 08:17:39 -02:30
Vendoring libcloud.
This commit is contained in:
527
awx/lib/site-packages/libcloud/dns/drivers/route53.py
Normal file
527
awx/lib/site-packages/libcloud/dns/drivers/route53.py
Normal file
@@ -0,0 +1,527 @@
|
||||
# Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
# contributor license agreements. See the NOTICE file distributed with
|
||||
# this work for additional information regarding copyright ownership.
|
||||
# The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
# (the "License"); you may not use this file except in compliance with
|
||||
# the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
__all__ = [
|
||||
'Route53DNSDriver'
|
||||
]
|
||||
|
||||
import base64
|
||||
import hmac
|
||||
import datetime
|
||||
import uuid
|
||||
import copy
|
||||
from libcloud.utils.py3 import httplib
|
||||
|
||||
from hashlib import sha1
|
||||
|
||||
try:
|
||||
from lxml import etree as ET
|
||||
except ImportError:
|
||||
from xml.etree import ElementTree as ET
|
||||
|
||||
from libcloud.utils.py3 import b, urlencode
|
||||
|
||||
from libcloud.utils.xml import findtext, findall, fixxpath
|
||||
from libcloud.dns.types import Provider, RecordType
|
||||
from libcloud.dns.types import ZoneDoesNotExistError, RecordDoesNotExistError
|
||||
from libcloud.dns.base import DNSDriver, Zone, Record
|
||||
from libcloud.common.types import LibcloudError
|
||||
from libcloud.common.aws import AWSGenericResponse
|
||||
from libcloud.common.base import ConnectionUserAndKey
|
||||
|
||||
|
||||
API_VERSION = '2012-02-29'
|
||||
API_HOST = 'route53.amazonaws.com'
|
||||
API_ROOT = '/%s/' % (API_VERSION)
|
||||
|
||||
NAMESPACE = 'https://%s/doc%s' % (API_HOST, API_ROOT)
|
||||
|
||||
|
||||
class InvalidChangeBatch(LibcloudError):
|
||||
pass
|
||||
|
||||
|
||||
class Route53DNSResponse(AWSGenericResponse):
|
||||
"""
|
||||
Amazon Route53 response class.
|
||||
"""
|
||||
|
||||
namespace = NAMESPACE
|
||||
xpath = 'Error'
|
||||
|
||||
exceptions = {
|
||||
'NoSuchHostedZone': ZoneDoesNotExistError,
|
||||
'InvalidChangeBatch': InvalidChangeBatch,
|
||||
}
|
||||
|
||||
|
||||
class Route53Connection(ConnectionUserAndKey):
|
||||
host = API_HOST
|
||||
responseCls = Route53DNSResponse
|
||||
|
||||
def pre_connect_hook(self, params, headers):
|
||||
time_string = datetime.datetime.utcnow() \
|
||||
.strftime('%a, %d %b %Y %H:%M:%S GMT')
|
||||
headers['Date'] = time_string
|
||||
tmp = []
|
||||
|
||||
signature = self._get_aws_auth_b64(self.key, time_string)
|
||||
auth = {'AWSAccessKeyId': self.user_id, 'Signature': signature,
|
||||
'Algorithm': 'HmacSHA1'}
|
||||
|
||||
for k, v in auth.items():
|
||||
tmp.append('%s=%s' % (k, v))
|
||||
|
||||
headers['X-Amzn-Authorization'] = 'AWS3-HTTPS ' + ','.join(tmp)
|
||||
|
||||
return params, headers
|
||||
|
||||
def _get_aws_auth_b64(self, secret_key, time_string):
|
||||
b64_hmac = base64.b64encode(
|
||||
hmac.new(b(secret_key), b(time_string), digestmod=sha1).digest()
|
||||
)
|
||||
|
||||
return b64_hmac.decode('utf-8')
|
||||
|
||||
|
||||
class Route53DNSDriver(DNSDriver):
|
||||
type = Provider.ROUTE53
|
||||
name = 'Route53 DNS'
|
||||
website = 'http://aws.amazon.com/route53/'
|
||||
connectionCls = Route53Connection
|
||||
|
||||
RECORD_TYPE_MAP = {
|
||||
RecordType.A: 'A',
|
||||
RecordType.AAAA: 'AAAA',
|
||||
RecordType.CNAME: 'CNAME',
|
||||
RecordType.MX: 'MX',
|
||||
RecordType.NS: 'NS',
|
||||
RecordType.PTR: 'PTR',
|
||||
RecordType.SOA: 'SOA',
|
||||
RecordType.SPF: 'SPF',
|
||||
RecordType.SRV: 'SRV',
|
||||
RecordType.TXT: 'TXT',
|
||||
}
|
||||
|
||||
def iterate_zones(self):
|
||||
return self._get_more('zones')
|
||||
|
||||
def iterate_records(self, zone):
|
||||
return self._get_more('records', zone=zone)
|
||||
|
||||
def get_zone(self, zone_id):
|
||||
self.connection.set_context({'zone_id': zone_id})
|
||||
uri = API_ROOT + 'hostedzone/' + zone_id
|
||||
data = self.connection.request(uri).object
|
||||
elem = findall(element=data, xpath='HostedZone',
|
||||
namespace=NAMESPACE)[0]
|
||||
return self._to_zone(elem)
|
||||
|
||||
def get_record(self, zone_id, record_id):
|
||||
zone = self.get_zone(zone_id=zone_id)
|
||||
record_type, name = record_id.split(':', 1)
|
||||
if name:
|
||||
full_name = ".".join((name, zone.domain))
|
||||
else:
|
||||
full_name = zone.domain
|
||||
self.connection.set_context({'zone_id': zone_id})
|
||||
params = urlencode({
|
||||
'name': full_name,
|
||||
'type': record_type,
|
||||
'maxitems': '1'
|
||||
})
|
||||
uri = API_ROOT + 'hostedzone/' + zone_id + '/rrset?' + params
|
||||
data = self.connection.request(uri).object
|
||||
|
||||
record = self._to_records(data=data, zone=zone)[0]
|
||||
|
||||
# A cute aspect of the /rrset filters is that they are more pagination
|
||||
# hints than filters!!
|
||||
# So will return a result even if its not what you asked for.
|
||||
record_type_num = self._string_to_record_type(record_type)
|
||||
if record.name != name or record.type != record_type_num:
|
||||
raise RecordDoesNotExistError(value='', driver=self,
|
||||
record_id=record_id)
|
||||
|
||||
return record
|
||||
|
||||
def create_zone(self, domain, type='master', ttl=None, extra=None):
|
||||
zone = ET.Element('CreateHostedZoneRequest', {'xmlns': NAMESPACE})
|
||||
ET.SubElement(zone, 'Name').text = domain
|
||||
ET.SubElement(zone, 'CallerReference').text = str(uuid.uuid4())
|
||||
|
||||
if extra and 'Comment' in extra:
|
||||
hzg = ET.SubElement(zone, 'HostedZoneConfig')
|
||||
ET.SubElement(hzg, 'Comment').text = extra['Comment']
|
||||
|
||||
uri = API_ROOT + 'hostedzone'
|
||||
data = ET.tostring(zone)
|
||||
rsp = self.connection.request(uri, method='POST', data=data).object
|
||||
|
||||
elem = findall(element=rsp, xpath='HostedZone', namespace=NAMESPACE)[0]
|
||||
return self._to_zone(elem=elem)
|
||||
|
||||
def delete_zone(self, zone, ex_delete_records=False):
|
||||
self.connection.set_context({'zone_id': zone.id})
|
||||
|
||||
if ex_delete_records:
|
||||
self.ex_delete_all_records(zone=zone)
|
||||
|
||||
uri = API_ROOT + 'hostedzone/%s' % (zone.id)
|
||||
response = self.connection.request(uri, method='DELETE')
|
||||
return response.status in [httplib.OK]
|
||||
|
||||
def create_record(self, name, zone, type, data, extra=None):
|
||||
extra = extra or {}
|
||||
batch = [('CREATE', name, type, data, extra)]
|
||||
self._post_changeset(zone, batch)
|
||||
id = ':'.join((self.RECORD_TYPE_MAP[type], name))
|
||||
return Record(id=id, name=name, type=type, data=data, zone=zone,
|
||||
driver=self, extra=extra)
|
||||
|
||||
def update_record(self, record, name=None, type=None, data=None,
|
||||
extra=None):
|
||||
name = name or record.name
|
||||
type = type or record.type
|
||||
extra = extra or record.extra
|
||||
|
||||
if not extra:
|
||||
extra = record.extra
|
||||
|
||||
# Multiple value records need to be handled specially - we need to
|
||||
# pass values for other records as well
|
||||
multiple_value_record = record.extra.get('_multi_value', False)
|
||||
other_records = record.extra.get('_other_records', [])
|
||||
|
||||
if multiple_value_record and other_records:
|
||||
self._update_multi_value_record(record=record, name=name,
|
||||
type=type, data=data,
|
||||
extra=extra)
|
||||
else:
|
||||
self._update_single_value_record(record=record, name=name,
|
||||
type=type, data=data,
|
||||
extra=extra)
|
||||
|
||||
id = ':'.join((self.RECORD_TYPE_MAP[type], name))
|
||||
return Record(id=id, name=name, type=type, data=data, zone=record.zone,
|
||||
driver=self, extra=extra)
|
||||
|
||||
def delete_record(self, record):
|
||||
try:
|
||||
r = record
|
||||
batch = [('DELETE', r.name, r.type, r.data, r.extra)]
|
||||
self._post_changeset(record.zone, batch)
|
||||
except InvalidChangeBatch:
|
||||
raise RecordDoesNotExistError(value='', driver=self,
|
||||
record_id=r.id)
|
||||
return True
|
||||
|
||||
def ex_create_multi_value_record(self, name, zone, type, data, extra=None):
|
||||
"""
|
||||
Create a record with multiple values with a single call.
|
||||
|
||||
:return: A list of created records.
|
||||
:rtype: ``list`` of :class:`libcloud.dns.base.Record`
|
||||
"""
|
||||
extra = extra or {}
|
||||
|
||||
attrs = {'xmlns': NAMESPACE}
|
||||
changeset = ET.Element('ChangeResourceRecordSetsRequest', attrs)
|
||||
batch = ET.SubElement(changeset, 'ChangeBatch')
|
||||
changes = ET.SubElement(batch, 'Changes')
|
||||
|
||||
change = ET.SubElement(changes, 'Change')
|
||||
ET.SubElement(change, 'Action').text = 'CREATE'
|
||||
|
||||
rrs = ET.SubElement(change, 'ResourceRecordSet')
|
||||
ET.SubElement(rrs, 'Name').text = name + '.' + zone.domain
|
||||
ET.SubElement(rrs, 'Type').text = self.RECORD_TYPE_MAP[type]
|
||||
ET.SubElement(rrs, 'TTL').text = str(extra.get('ttl', '0'))
|
||||
|
||||
rrecs = ET.SubElement(rrs, 'ResourceRecords')
|
||||
|
||||
# Value is provided as a multi line string
|
||||
values = [value.strip() for value in data.split('\n') if
|
||||
value.strip()]
|
||||
|
||||
for value in values:
|
||||
rrec = ET.SubElement(rrecs, 'ResourceRecord')
|
||||
ET.SubElement(rrec, 'Value').text = value
|
||||
|
||||
uri = API_ROOT + 'hostedzone/' + zone.id + '/rrset'
|
||||
data = ET.tostring(changeset)
|
||||
self.connection.set_context({'zone_id': zone.id})
|
||||
self.connection.request(uri, method='POST', data=data)
|
||||
|
||||
id = ':'.join((self.RECORD_TYPE_MAP[type], name))
|
||||
|
||||
records = []
|
||||
for value in values:
|
||||
record = Record(id=id, name=name, type=type, data=value, zone=zone,
|
||||
driver=self, extra=extra)
|
||||
records.append(record)
|
||||
|
||||
return record
|
||||
|
||||
def ex_delete_all_records(self, zone):
|
||||
"""
|
||||
Remove all the records for the provided zone.
|
||||
|
||||
:param zone: Zone to delete records for.
|
||||
:type zone: :class:`Zone`
|
||||
"""
|
||||
deletions = []
|
||||
for r in zone.list_records():
|
||||
if r.type in (RecordType.NS, RecordType.SOA):
|
||||
continue
|
||||
deletions.append(('DELETE', r.name, r.type, r.data, r.extra))
|
||||
|
||||
if deletions:
|
||||
self._post_changeset(zone, deletions)
|
||||
|
||||
def _update_single_value_record(self, record, name=None, type=None,
|
||||
data=None, extra=None):
|
||||
batch = [
|
||||
('DELETE', record.name, record.type, record.data, record.extra),
|
||||
('CREATE', name, type, data, extra)
|
||||
]
|
||||
|
||||
return self._post_changeset(record.zone, batch)
|
||||
|
||||
def _update_multi_value_record(self, record, name=None, type=None,
|
||||
data=None, extra=None):
|
||||
other_records = record.extra.get('_other_records', [])
|
||||
|
||||
attrs = {'xmlns': NAMESPACE}
|
||||
changeset = ET.Element('ChangeResourceRecordSetsRequest', attrs)
|
||||
batch = ET.SubElement(changeset, 'ChangeBatch')
|
||||
changes = ET.SubElement(batch, 'Changes')
|
||||
|
||||
# Delete existing records
|
||||
change = ET.SubElement(changes, 'Change')
|
||||
ET.SubElement(change, 'Action').text = 'DELETE'
|
||||
|
||||
rrs = ET.SubElement(change, 'ResourceRecordSet')
|
||||
ET.SubElement(rrs, 'Name').text = record.name + '.' + \
|
||||
record.zone.domain
|
||||
ET.SubElement(rrs, 'Type').text = self.RECORD_TYPE_MAP[record.type]
|
||||
ET.SubElement(rrs, 'TTL').text = str(record.extra.get('ttl', '0'))
|
||||
|
||||
rrecs = ET.SubElement(rrs, 'ResourceRecords')
|
||||
|
||||
rrec = ET.SubElement(rrecs, 'ResourceRecord')
|
||||
ET.SubElement(rrec, 'Value').text = record.data
|
||||
|
||||
for other_record in other_records:
|
||||
rrec = ET.SubElement(rrecs, 'ResourceRecord')
|
||||
ET.SubElement(rrec, 'Value').text = other_record['data']
|
||||
|
||||
# Re-create new (updated) records. Since we are updating a multi value
|
||||
# record, only a single record is updated and others are left as is.
|
||||
change = ET.SubElement(changes, 'Change')
|
||||
ET.SubElement(change, 'Action').text = 'CREATE'
|
||||
|
||||
rrs = ET.SubElement(change, 'ResourceRecordSet')
|
||||
ET.SubElement(rrs, 'Name').text = name + '.' + record.zone.domain
|
||||
ET.SubElement(rrs, 'Type').text = self.RECORD_TYPE_MAP[type]
|
||||
ET.SubElement(rrs, 'TTL').text = str(extra.get('ttl', '0'))
|
||||
|
||||
rrecs = ET.SubElement(rrs, 'ResourceRecords')
|
||||
|
||||
rrec = ET.SubElement(rrecs, 'ResourceRecord')
|
||||
ET.SubElement(rrec, 'Value').text = data
|
||||
|
||||
for other_record in other_records:
|
||||
rrec = ET.SubElement(rrecs, 'ResourceRecord')
|
||||
ET.SubElement(rrec, 'Value').text = other_record['data']
|
||||
|
||||
uri = API_ROOT + 'hostedzone/' + record.zone.id + '/rrset'
|
||||
data = ET.tostring(changeset)
|
||||
self.connection.set_context({'zone_id': record.zone.id})
|
||||
response = self.connection.request(uri, method='POST', data=data)
|
||||
|
||||
return response.status == httplib.OK
|
||||
|
||||
def _post_changeset(self, zone, changes_list):
|
||||
attrs = {'xmlns': NAMESPACE}
|
||||
changeset = ET.Element('ChangeResourceRecordSetsRequest', attrs)
|
||||
batch = ET.SubElement(changeset, 'ChangeBatch')
|
||||
changes = ET.SubElement(batch, 'Changes')
|
||||
|
||||
for action, name, type_, data, extra in changes_list:
|
||||
change = ET.SubElement(changes, 'Change')
|
||||
ET.SubElement(change, 'Action').text = action
|
||||
|
||||
rrs = ET.SubElement(change, 'ResourceRecordSet')
|
||||
ET.SubElement(rrs, 'Name').text = name + '.' + zone.domain
|
||||
ET.SubElement(rrs, 'Type').text = self.RECORD_TYPE_MAP[type_]
|
||||
ET.SubElement(rrs, 'TTL').text = str(extra.get('ttl', '0'))
|
||||
|
||||
rrecs = ET.SubElement(rrs, 'ResourceRecords')
|
||||
rrec = ET.SubElement(rrecs, 'ResourceRecord')
|
||||
ET.SubElement(rrec, 'Value').text = data
|
||||
|
||||
uri = API_ROOT + 'hostedzone/' + zone.id + '/rrset'
|
||||
data = ET.tostring(changeset)
|
||||
self.connection.set_context({'zone_id': zone.id})
|
||||
response = self.connection.request(uri, method='POST', data=data)
|
||||
|
||||
return response.status == httplib.OK
|
||||
|
||||
def _to_zones(self, data):
|
||||
zones = []
|
||||
for element in data.findall(fixxpath(xpath='HostedZones/HostedZone',
|
||||
namespace=NAMESPACE)):
|
||||
zones.append(self._to_zone(element))
|
||||
|
||||
return zones
|
||||
|
||||
def _to_zone(self, elem):
|
||||
name = findtext(element=elem, xpath='Name', namespace=NAMESPACE)
|
||||
id = findtext(element=elem, xpath='Id',
|
||||
namespace=NAMESPACE).replace('/hostedzone/', '')
|
||||
comment = findtext(element=elem, xpath='Config/Comment',
|
||||
namespace=NAMESPACE)
|
||||
resource_record_count = int(findtext(element=elem,
|
||||
xpath='ResourceRecordSetCount',
|
||||
namespace=NAMESPACE))
|
||||
|
||||
extra = {'Comment': comment, 'ResourceRecordSetCount':
|
||||
resource_record_count}
|
||||
|
||||
zone = Zone(id=id, domain=name, type='master', ttl=0, driver=self,
|
||||
extra=extra)
|
||||
return zone
|
||||
|
||||
def _to_records(self, data, zone):
|
||||
records = []
|
||||
elems = data.findall(
|
||||
fixxpath(xpath='ResourceRecordSets/ResourceRecordSet',
|
||||
namespace=NAMESPACE))
|
||||
for elem in elems:
|
||||
record_set = elem.findall(fixxpath(
|
||||
xpath='ResourceRecords/ResourceRecord',
|
||||
namespace=NAMESPACE))
|
||||
record_count = len(record_set)
|
||||
multiple_value_record = (record_count > 1)
|
||||
|
||||
record_set_records = []
|
||||
|
||||
for index, record in enumerate(record_set):
|
||||
# Need to special handling for records with multiple values for
|
||||
# update to work correctly
|
||||
record = self._to_record(elem=elem, zone=zone, index=index)
|
||||
record.extra['_multi_value'] = multiple_value_record
|
||||
|
||||
if multiple_value_record:
|
||||
record.extra['_other_records'] = []
|
||||
|
||||
record_set_records.append(record)
|
||||
|
||||
# Store reference to other records so update works correctly
|
||||
if multiple_value_record:
|
||||
for index in range(0, len(record_set_records)):
|
||||
record = record_set_records[index]
|
||||
|
||||
for other_index, other_record in \
|
||||
enumerate(record_set_records):
|
||||
if index == other_index:
|
||||
# Skip current record
|
||||
continue
|
||||
|
||||
extra = copy.deepcopy(other_record.extra)
|
||||
extra.pop('_multi_value')
|
||||
extra.pop('_other_records')
|
||||
|
||||
item = {'name': other_record.name,
|
||||
'data': other_record.data,
|
||||
'type': other_record.type,
|
||||
'extra': extra}
|
||||
record.extra['_other_records'].append(item)
|
||||
|
||||
records.extend(record_set_records)
|
||||
|
||||
return records
|
||||
|
||||
def _to_record(self, elem, zone, index=0):
|
||||
name = findtext(element=elem, xpath='Name',
|
||||
namespace=NAMESPACE)
|
||||
name = name[:-len(zone.domain) - 1]
|
||||
|
||||
type = self._string_to_record_type(findtext(element=elem, xpath='Type',
|
||||
namespace=NAMESPACE))
|
||||
ttl = int(findtext(element=elem, xpath='TTL', namespace=NAMESPACE))
|
||||
|
||||
value_elem = elem.findall(
|
||||
fixxpath(xpath='ResourceRecords/ResourceRecord',
|
||||
namespace=NAMESPACE))[index]
|
||||
data = findtext(element=(value_elem), xpath='Value',
|
||||
namespace=NAMESPACE)
|
||||
|
||||
extra = {'ttl': ttl}
|
||||
|
||||
if type == 'MX':
|
||||
split = data.split()
|
||||
priority, data = split
|
||||
extra['priority'] = int(priority)
|
||||
elif type == 'SRV':
|
||||
split = data.split()
|
||||
priority, weight, port, data = split
|
||||
extra['priority'] = int(priority)
|
||||
extra['weight'] = int(weight)
|
||||
extra['port'] = int(port)
|
||||
|
||||
id = ':'.join((self.RECORD_TYPE_MAP[type], name))
|
||||
record = Record(id=id, name=name, type=type, data=data, zone=zone,
|
||||
driver=self, extra=extra)
|
||||
return record
|
||||
|
||||
def _get_more(self, rtype, **kwargs):
|
||||
exhausted = False
|
||||
last_key = None
|
||||
while not exhausted:
|
||||
items, last_key, exhausted = self._get_data(rtype, last_key,
|
||||
**kwargs)
|
||||
for item in items:
|
||||
yield item
|
||||
|
||||
def _get_data(self, rtype, last_key, **kwargs):
|
||||
params = {}
|
||||
if last_key:
|
||||
params['name'] = last_key
|
||||
path = API_ROOT + 'hostedzone'
|
||||
|
||||
if rtype == 'zones':
|
||||
response = self.connection.request(path, params=params)
|
||||
transform_func = self._to_zones
|
||||
elif rtype == 'records':
|
||||
zone = kwargs['zone']
|
||||
path += '/%s/rrset' % (zone.id)
|
||||
self.connection.set_context({'zone_id': zone.id})
|
||||
response = self.connection.request(path, params=params)
|
||||
transform_func = self._to_records
|
||||
|
||||
if response.status == httplib.OK:
|
||||
is_truncated = findtext(element=response.object,
|
||||
xpath='IsTruncated',
|
||||
namespace=NAMESPACE)
|
||||
exhausted = is_truncated != 'true'
|
||||
last_key = findtext(element=response.object,
|
||||
xpath='NextRecordName',
|
||||
namespace=NAMESPACE)
|
||||
items = transform_func(data=response.object, **kwargs)
|
||||
return items, last_key, exhausted
|
||||
else:
|
||||
return [], None, True
|
||||
Reference in New Issue
Block a user