1
0
mirror of https://github.com/github/octodns.git synced 2024-05-11 05:55:00 +00:00

Start sketchin of Rackspace provider, half rewritten from powerdns...

This commit is contained in:
Vietor Davis
2017-06-26 17:03:15 -07:00
parent 3b349c96f0
commit 679c2be0e0
6 changed files with 903 additions and 0 deletions

View File

@@ -0,0 +1,386 @@
#
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from requests import HTTPError, Session, post
import json
import logging
from ..record import Create, Record
from .base import BaseProvider
class RackspaceProvider(BaseProvider):
SUPPORTS_GEO = False
TIMEOUT = 5
def __init__(self, username, api_key, *args, **kwargs):
'''
Rackspace API v1 Provider
rackspace:
class: octodns.provider.rackspace.RackspaceProvider
# The the username to authenticate with (required)
username: username
# The api key that grants access for that user (required)
api_key: api-key
'''
self.log = logging.getLogger('RackspaceProvider[{}]'.format(username))
super(RackspaceProvider, self).__init__(id, *args, **kwargs)
auth_token, dns_endpoint = self._get_auth_token(username, api_key)
self.dns_endpoint = dns_endpoint
sess = Session()
sess.headers.update({'X-Auth-Token': auth_token})
self._sess = sess
def _get_auth_token(self, username, api_key):
ret = post('https://identity.api.rackspacecloud.com/v2.0/tokens',
json={"auth": {"RAX-KSKEY:apiKeyCredentials": {"username": username, "apiKey": api_key}}},
)
cloud_dns_endpoint = [x for x in ret.json()['access']['serviceCatalog'] if x['name'] == 'cloudDNS'][0]['endpoints'][0]['publicURL']
return ret.json()['access']['token']['id'], cloud_dns_endpoint
def _get_zone_id_for(self, zone_name):
ret = self._request('GET', 'domains', pagination_key='domains')
if ret and 'name' in ret:
return [x for x in ret if x['name'] == zone_name][0]['id']
else:
return None
def _request(self, method, path, data=None, pagination_key=None):
self.log.debug('_request: method=%s, path=%s', method, path)
url = '{}/{}'.format(self.dns_endpoint, path)
if pagination_key:
return self._paginated_request_for_url(method, url, data, pagination_key)
else:
return self._request_for_url(method, url, data)
def _request_for_url(self, method, url, data):
resp = self._sess.request(method, url, json=data, timeout=self.TIMEOUT)
self.log.debug('_request: status=%d', resp.status_code)
resp.raise_for_status()
return resp
def _paginated_request_for_url(self, method, url, data, pagination_key):
acc = []
resp = self._sess.request(method, url, json=data, timeout=self.TIMEOUT)
self.log.debug('_request: status=%d', resp.status_code)
resp.raise_for_status()
acc.extend(resp.json()[pagination_key])
next_page = [x for x in resp.json().get('links', []) if x['rel'] == 'next']
if next_page:
url = next_page[0]['href']
return acc.extend(self._paginated_request_for_url(method, url, data, pagination_key))
else:
return acc
def _get(self, path, data=None):
return self._request('GET', path, data=data)
def _post(self, path, data=None):
return self._request('POST', path, data=data)
def _patch(self, path, data=None):
return self._request('PATCH', path, data=data)
def _data_for_multiple(self, rrset):
# TODO: geo not supported
return {
'type': rrset['type'],
'values': [r['content'] for r in rrset['records']],
'ttl': rrset['ttl']
}
_data_for_A = _data_for_multiple
_data_for_AAAA = _data_for_multiple
_data_for_NS = _data_for_multiple
def _data_for_single(self, record):
return {
'type': record['type'],
'value': record['data'],
'ttl': record['ttl']
}
_data_for_ALIAS = _data_for_single
_data_for_CNAME = _data_for_single
_data_for_PTR = _data_for_single
def _data_for_quoted(self, rrset):
return {
'type': rrset['type'],
'values': [r['content'][1:-1] for r in rrset['records']],
'ttl': rrset['ttl']
}
_data_for_SPF = _data_for_quoted
_data_for_TXT = _data_for_quoted
def _data_for_MX(self, rrset):
values = []
for record in rrset['records']:
priority, value = record['content'].split(' ', 1)
values.append({
'priority': priority,
'value': value,
})
return {
'type': rrset['type'],
'values': values,
'ttl': rrset['ttl']
}
def _data_for_NAPTR(self, rrset):
values = []
for record in rrset['records']:
order, preference, flags, service, regexp, replacement = \
record['content'].split(' ', 5)
values.append({
'order': order,
'preference': preference,
'flags': flags[1:-1],
'service': service[1:-1],
'regexp': regexp[1:-1],
'replacement': replacement,
})
return {
'type': rrset['type'],
'values': values,
'ttl': rrset['ttl']
}
def _data_for_SSHFP(self, rrset):
values = []
for record in rrset['records']:
algorithm, fingerprint_type, fingerprint = \
record['content'].split(' ', 2)
values.append({
'algorithm': algorithm,
'fingerprint_type': fingerprint_type,
'fingerprint': fingerprint,
})
return {
'type': rrset['type'],
'values': values,
'ttl': rrset['ttl']
}
def _data_for_SRV(self, rrset):
values = []
for record in rrset['records']:
priority, weight, port, target = \
record['content'].split(' ', 3)
values.append({
'priority': priority,
'weight': weight,
'port': port,
'target': target,
})
return {
'type': rrset['type'],
'values': values,
'ttl': rrset['ttl']
}
def populate(self, zone, target=False):
self.log.debug('populate: name=%s', zone.name)
resp = None
try:
domain_id = self._get_zone_id_for(zone.name)
resp = self._request('GET', '/domains/{}/records'.format(domain_id), pagination_key='records')
self.log.debug('populate: loaded')
except HTTPError as e:
if e.response.status_code == 401:
# Nicer error message for auth problems
raise Exception('Rackspace request unauthorized')
elif e.response.status_code == 422:
# 422 means powerdns doesn't know anything about the requsted
# domain. We'll just ignore it here and leave the zone
# untouched.
pass
else:
# just re-throw
raise
before = len(zone.records)
if resp:
for record in resp.json()['records']:
record_type = record['type']
if record_type == 'SOA':
continue
data_for = getattr(self, '_data_for_{}'.format(record_type))
record_name = zone.hostname_from_fqdn(record['name'])
record = Record.new(zone, record_name, data_for(record),
source=self)
zone.add_record(record)
self.log.info('populate: found %s records',
len(zone.records) - before)
def _records_for_multiple(self, record):
return [{'content': v, 'disabled': False}
for v in record.values]
_records_for_A = _records_for_multiple
_records_for_AAAA = _records_for_multiple
_records_for_NS = _records_for_multiple
def _records_for_single(self, record):
return [{'content': record.value, 'disabled': False}]
_records_for_ALIAS = _records_for_single
_records_for_CNAME = _records_for_single
_records_for_PTR = _records_for_single
def _records_for_quoted(self, record):
return [{'content': '"{}"'.format(v), 'disabled': False}
for v in record.values]
_records_for_SPF = _records_for_quoted
_records_for_TXT = _records_for_quoted
def _records_for_MX(self, record):
return [{
'content': '{} {}'.format(v.priority, v.value),
'disabled': False
} for v in record.values]
def _records_for_NAPTR(self, record):
return [{
'content': '{} {} "{}" "{}" "{}" {}'.format(v.order, v.preference,
v.flags, v.service,
v.regexp,
v.replacement),
'disabled': False
} for v in record.values]
def _records_for_SSHFP(self, record):
return [{
'content': '{} {} {}'.format(v.algorithm, v.fingerprint_type,
v.fingerprint),
'disabled': False
} for v in record.values]
def _records_for_SRV(self, record):
return [{
'content': '{} {} {} {}'.format(v.priority, v.weight, v.port,
v.target),
'disabled': False
} for v in record.values]
def _mod_Create(self, change):
new = change.new
records_for = getattr(self, '_records_for_{}'.format(new._type))
return {
'name': new.fqdn,
'type': new._type,
'ttl': new.ttl,
'changetype': 'REPLACE',
'records': records_for(new)
}
_mod_Update = _mod_Create
def _mod_Delete(self, change):
existing = change.existing
records_for = getattr(self, '_records_for_{}'.format(existing._type))
return {
'name': existing.fqdn,
'type': existing._type,
'ttl': existing.ttl,
'changetype': 'DELETE',
'records': records_for(existing)
}
def _get_nameserver_record(self, existing):
return None
def _extra_changes(self, existing, _):
self.log.debug('_extra_changes: zone=%s', existing.name)
ns = self._get_nameserver_record(existing)
if not ns:
return []
# sorting mostly to make things deterministic for testing, but in
# theory it let us find what we're after quickier (though sorting would
# ve more exepensive.)
for record in sorted(existing.records):
if record == ns:
# We've found the top-level NS record, return any changes
change = record.changes(ns, self)
self.log.debug('_extra_changes: change=%s', change)
if change:
# We need to modify an existing record
return [change]
# No change is necessary
return []
# No existing top-level NS
self.log.debug('_extra_changes: create')
return [Create(ns)]
def _get_error(self, http_error):
try:
return http_error.response.json()['error']
except Exception:
return ''
def _apply(self, plan):
desired = plan.desired
changes = plan.changes
self.log.debug('_apply: zone=%s, len(changes)=%d', desired.name,
len(changes))
mods = []
for change in changes:
class_name = change.__class__.__name__
mods.append(getattr(self, '_mod_{}'.format(class_name))(change))
self.log.debug('_apply: sending change request')
try:
self._patch('zones/{}'.format(desired.name),
data={'rrsets': mods})
self.log.debug('_apply: patched')
except HTTPError as e:
error = self._get_error(e)
if e.response.status_code != 422 or \
not error.startswith('Could not find domain '):
self.log.error('_apply: status=%d, text=%s',
e.response.status_code,
e.response.text)
raise
self.log.info('_apply: creating zone=%s', desired.name)
# 422 means powerdns doesn't know anything about the requsted
# domain. We'll try to create it with the correct records instead
# of update. Hopefully all the mods are creates :-)
data = {
'name': desired.name,
'kind': 'Master',
'masters': [],
'nameservers': [],
'rrsets': mods,
'soa_edit_api': 'INCEPTION-INCREMENT',
'serial': 0,
}
try:
self._post('zones', data)
except HTTPError as e:
self.log.error('_apply: status=%d, text=%s',
e.response.status_code,
e.response.text)
raise
self.log.debug('_apply: created')
self.log.debug('_apply: complete')