Source code for libcloud.dns.drivers.cloudflare

# 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__ = [
    'CloudFlareDNSDriver'
]

import copy

from libcloud.common.base import JsonResponse, ConnectionUserAndKey
from libcloud.common.types import InvalidCredsError, LibcloudError
from libcloud.utils.py3 import httplib
from libcloud.dns.base import DNSDriver, Zone, Record
from libcloud.dns.types import Provider, RecordType
from libcloud.dns.types import ZoneDoesNotExistError, RecordDoesNotExistError

API_URL = 'https://www.cloudflare.com/api_json.html'
API_HOST = 'www.cloudflare.com'
API_PATH = '/api_json.html'

ZONE_EXTRA_ATTRIBUTES = [
    'display_name',
    'zone_status',
    'zone_type',
    'host_id',
    'host_pubname',
    'host_website',
    'fqdns',
    'vtxt',
    'step',
    'zone_status_class',
    'zone_status_desc',
    'orig_registrar',
    'orig_dnshost',
    'orig_ns_names'
]

RECORD_EXTRA_ATTRIBUTES = [
    'rec_tag',
    'display_name',
    'pro',
    'display_content',
    'ttl_ceil',
    'ssl_id',
    'ssl_status',
    'ssl_expires_on',
    'auto_ttl',
    'service_mode'
]


class CloudFlareDNSResponse(JsonResponse):
    def success(self):
        return self.status in [httplib.OK, httplib.CREATED, httplib.ACCEPTED]

    def parse_body(self):
        body = super(CloudFlareDNSResponse, self).parse_body()
        body = body or {}

        result = body.get('result', None)
        error_code = body.get('err_code', None)
        msg = body.get('msg', None)
        is_error_result = result == 'error'

        context = self.connection.context or {}
        context_record_id = context.get('record_id', None)
        context_zone_domain = context.get('zone_domain', None)

        if (is_error_result and 'invalid record id' in msg.lower() and
                context_record_id):
            raise RecordDoesNotExistError(value=msg,
                                          driver=self.connection.driver,
                                          record_id=context_record_id)
        elif (is_error_result and 'invalid zone' in msg.lower() and
              context_zone_domain):
            raise ZoneDoesNotExistError(value=msg,
                                        driver=self.connection.driver,
                                        zone_id=context_zone_domain)

        if error_code == 'E_UNAUTH':
            raise InvalidCredsError(msg)
        elif result == 'error' or error_code is not None:
            msg = 'Request failed: %s' % (self.body)
            raise LibcloudError(value=msg, driver=self.connection.driver)

        return body


class CloudFlareDNSConnection(ConnectionUserAndKey):
    host = API_HOST
    secure = True
    responseCls = CloudFlareDNSResponse

    def request(self, action, params=None, data=None, headers=None,
                method='GET'):
        params = params or {}
        data = data or {}

        base_params = {
            'email': self.user_id,
            'tkn': self.key,
            'a': action
        }
        params = copy.deepcopy(params)
        params.update(base_params)

        return super(CloudFlareDNSConnection, self).request(action=API_PATH,
                                                            params=params,
                                                            data=None,
                                                            method=method,
                                                            headers=headers)


[docs]class CloudFlareDNSDriver(DNSDriver): type = Provider.CLOUDFLARE name = 'CloudFlare DNS' website = 'https://www.cloudflare.com' connectionCls = CloudFlareDNSConnection RECORD_TYPE_MAP = { RecordType.A: 'A', RecordType.AAAA: 'AAAA', RecordType.CNAME: 'CNAME', RecordType.MX: 'MX', RecordType.TXT: 'TXT', RecordType.SPF: 'SPF', RecordType.NS: 'NS', RecordType.SRV: 'SRV', RecordType.URL: 'LOC' }
[docs] def iterate_zones(self): # TODO: Support pagination result = self.connection.request(action='zone_load_multi').object zones = self._to_zones(data=result['response']['zones']['objs']) return zones
[docs] def iterate_records(self, zone): # TODO: Support pagination params = {'z': zone.domain} self.connection.set_context({'zone_domain': zone.domain}) resp = self.connection.request(action='rec_load_all', params=params) data = resp.object['response']['recs']['objs'] records = self._to_records(zone=zone, data=data) return records
[docs] def get_zone(self, zone_id): # TODO: This is not efficient zones = self.list_zones() try: zone = [z for z in zones if z.id == zone_id][0] except IndexError: raise ZoneDoesNotExistError(value='', driver=self, zone_id=zone_id) return zone
[docs] def create_record(self, name, zone, type, data, extra=None): extra = extra or {} params = {'name': name, 'z': zone.domain, 'type': type, 'content': data} params['ttl'] = extra.get('ttl', 120) if 'priority' in extra: # For MX and SRV records params['prio'] = extra['priority'] self.connection.set_context({'zone_domain': zone.domain}) resp = self.connection.request(action='rec_new', params=params) item = resp.object['response']['rec']['obj'] record = self._to_record(zone=zone, item=item) return record
[docs] def update_record(self, record, name=None, type=None, data=None, extra=None): extra = extra or {} params = {'z': record.zone.domain, 'id': record.id} params['name'] = name or record.name params['type'] = type or record.type params['content'] = data or record.data params['ttl'] = extra.get('ttl', None) or record.extra['ttl'] self.connection.set_context({'zone_domain': record.zone.domain}) self.connection.set_context({'record_id': record.id}) resp = self.connection.request(action='rec_edit', params=params) item = resp.object['response']['rec']['obj'] record = self._to_record(zone=record.zone, item=item) return record
[docs] def delete_record(self, record): params = {'z': record.zone.domain, 'id': record.id} self.connection.set_context({'zone_domain': record.zone.domain}) self.connection.set_context({'record_id': record.id}) resp = self.connection.request(action='rec_delete', params=params) result = resp.object return result.get('result', None) == 'success'
[docs] def ex_get_zone_stats(self, zone, interval=30): params = {'z': zone.domain, 'interval': interval} self.connection.set_context({'zone_domain': zone.domain}) resp = self.connection.request(action='stats', params=params) result = resp.object['response']['result']['objs'][0] return result
[docs] def ex_zone_check(self, zones): zone_domains = [zone.domain for zone in zones] zone_domains = ','.join(zone_domains) params = {'zones': zone_domains} resp = self.connection.request(action='zone_check', params=params) result = resp.object['response']['zones'] return result
[docs] def ex_get_ip_threat_score(self, ip): """ Retrieve current threat score for a given IP. Note that scores are on a logarithmic scale, where a higher score indicates a higher threat. """ params = {'ip': ip} resp = self.connection.request(action='ip_lkup', params=params) result = resp.object['response'] return result
[docs] def ex_get_zone_settings(self, zone): """ Retrieve all current settings for a given zone. """ params = {'z': zone.domain} self.connection.set_context({'zone_domain': zone.domain}) resp = self.connection.request(action='zone_settings', params=params) result = resp.object['response']['result']['objs'][0] return result
[docs] def ex_set_zone_security_level(self, zone, level): """ Set the zone Basic Security Level to I'M UNDER ATTACK! / HIGH / MEDIUM / LOW / ESSENTIALLY OFF. :param level: Security level. Valid values are: help, high, med, low, eoff. :type level: ``str`` """ params = {'z': zone.domain, 'v': level} self.connection.set_context({'zone_domain': zone.domain}) resp = self.connection.request(action='sec_lvl', params=params) result = resp.object return result.get('result', None) == 'success'
[docs] def ex_set_zone_cache_level(self, zone, level): """ Set the zone caching level. :param level: Caching level. Valid values are: agg (aggresive), basic. :type level: ``str`` """ params = {'z': zone.domain, 'v': level} self.connection.set_context({'zone_domain': zone.domain}) resp = self.connection.request(action='cache_lvl', params=params) result = resp.object return result.get('result', None) == 'success'
[docs] def ex_enable_development_mode(self, zone): """ Enable development mode. When Development Mode is on the cache is bypassed. Development mode remains on for 3 hours or until when it is toggled back off. """ params = {'z': zone.domain, 'v': 1} self.connection.set_context({'zone_domain': zone.domain}) resp = self.connection.request(action='devmode', params=params) result = resp.object return result.get('result', None) == 'success'
[docs] def ex_disable_development_mode(self, zone): """ Disable development mode. """ params = {'z': zone.domain, 'v': 0} self.connection.set_context({'zone_domain': zone.domain}) resp = self.connection.request(action='devmode', params=params) result = resp.object return result.get('result', None) == 'success'
[docs] def ex_purge_cached_files(self, zone): """ Purge CloudFlare of any cached files. """ params = {'z': zone.domain, 'v': 1} self.connection.set_context({'zone_domain': zone.domain}) resp = self.connection.request(action='fpurge_ts', params=params) result = resp.object return result.get('result', None) == 'success'
[docs] def ex_purge_cached_file(self, zone, url): """ Purge single file from CloudFlare's cache. :param url: URL to the file to purge from cache. :type url: ``str`` """ params = {'z': zone.domain, 'url': url} self.connection.set_context({'zone_domain': zone.domain}) resp = self.connection.request(action='zone_file_purge', params=params) result = resp.object return result.get('result', None) == 'success'
[docs] def ex_whitelist_ip(self, zone, ip): """ Whitelist the provided IP. """ params = {'z': zone.domain, 'key': ip} self.connection.set_context({'zone_domain': zone.domain}) resp = self.connection.request(action='wl', params=params) result = resp.object return result.get('result', None) == 'success'
[docs] def ex_blacklist_ip(self, zone, ip): """ Blacklist the provided IP. """ params = {'z': zone.domain, 'key': ip} self.connection.set_context({'zone_domain': zone.domain}) resp = self.connection.request(action='ban', params=params) result = resp.object return result.get('result', None) == 'success'
[docs] def ex_unlist_ip(self, zone, ip): """ Remove provided ip from the whitelist and blacklist. """ params = {'z': zone.domain, 'key': ip} self.connection.set_context({'zone_domain': zone.domain}) resp = self.connection.request(action='nul', params=params) result = resp.object return result.get('result', None) == 'success'
[docs] def ex_enable_ipv6_support(self, zone): """ Enable IPv6 support for the provided zone. """ params = {'z': zone.domain, 'v': 3} self.connection.set_context({'zone_domain': zone.domain}) resp = self.connection.request(action='ipv46', params=params) result = resp.object return result.get('result', None) == 'success'
[docs] def ex_disable_ipv6_support(self, zone): """ Disable IPv6 support for the provided zone. """ params = {'z': zone.domain, 'v': 0} self.connection.set_context({'zone_domain': zone.domain}) resp = self.connection.request(action='ipv46', params=params) result = resp.object return result.get('result', None) == 'success'
def _to_zones(self, data): zones = [] for item in data: zone = self._to_zone(item=item) zones.append(zone) return zones def _to_zone(self, item): type = 'master' extra = {} extra['props'] = item.get('props', {}) extra['confirm_code'] = item.get('confirm_code', {}) extra['allow'] = item.get('allow', {}) for attribute in ZONE_EXTRA_ATTRIBUTES: value = item.get(attribute, None) extra[attribute] = value zone = Zone(id=str(item['zone_id']), domain=item['zone_name'], type=type, ttl=None, driver=self, extra=extra) return zone def _to_records(self, zone, data): records = [] for item in data: record = self._to_record(zone=zone, item=item) records.append(record) return records def _to_record(self, zone, item): name = self._get_record_name(item=item) type = item['type'] data = item['content'] if item.get('ttl', None): ttl = int(item['ttl']) else: ttl = None extra = {} extra['ttl'] = ttl extra['props'] = item.get('props', {}) for attribute in RECORD_EXTRA_ATTRIBUTES: value = item.get(attribute, None) extra[attribute] = value record = Record(id=str(item['rec_id']), name=name, type=type, data=data, zone=zone, driver=self, ttl=ttl, extra=extra) return record def _get_record_name(self, item): name = item['name'].replace('.' + item['zone_name'], '') or None if name: name = name.replace(item['zone_name'], '') or None return name