| # 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. |
| """ |
| NFSN DNS Driver |
| """ |
| import re |
| |
| from libcloud.common.exceptions import BaseHTTPError |
| from libcloud.common.nfsn import NFSNConnection |
| from libcloud.dns.base import DNSDriver, Zone, Record |
| from libcloud.dns.types import ZoneDoesNotExistError, RecordDoesNotExistError |
| from libcloud.dns.types import RecordAlreadyExistsError |
| from libcloud.dns.types import Provider, RecordType |
| from libcloud.utils.py3 import httplib |
| |
| __all__ = [ |
| "NFSNDNSDriver", |
| ] |
| |
| # The NFSN API does not return any internal "ID" strings for any DNS records. |
| # This means that we must set all returned Record objects' id properties to |
| # None. It also means that we cannot implement libcloud APIs that rely on |
| # record_id, such as get_record(). Instead, the NFSN-specific |
| # ex_get_records_by() method will return the desired Record objects. |
| # |
| # Additionally, the NFSN API does not provide ways to create, delete, or list |
| # all zones, so create_zone(), delete_zone(), and list_zones() are not |
| # implemented. |
| |
| |
| class NFSNDNSDriver(DNSDriver): |
| type = Provider.NFSN |
| name = "NFSN DNS" |
| website = "https://www.nearlyfreespeech.net" |
| connectionCls = NFSNConnection |
| |
| RECORD_TYPE_MAP = { |
| RecordType.A: "A", |
| RecordType.AAAA: "AAAA", |
| RecordType.CNAME: "CNAME", |
| RecordType.MX: "MX", |
| RecordType.NS: "NS", |
| RecordType.SRV: "SRV", |
| RecordType.TXT: "TXT", |
| RecordType.PTR: "PTR", |
| } |
| |
| def list_records(self, zone): |
| """ |
| Return a list of all records for the provided zone. |
| |
| :param zone: Zone to list records for. |
| :type zone: :class:`Zone` |
| |
| :return: ``list`` of :class:`Record` |
| """ |
| # Just use ex_get_records_by() with no name or type filters. |
| return self.ex_get_records_by(zone) |
| |
| def get_zone(self, zone_id): |
| """ |
| Return a Zone instance. |
| |
| :param zone_id: name of the required zone, for example "example.com". |
| :type zone_id: ``str`` |
| |
| :rtype: :class:`Zone` |
| :raises: ZoneDoesNotExistError: If no zone could be found. |
| """ |
| # We will check if there is a serial property for this zone. If so, |
| # then the zone exists. |
| try: |
| self.connection.request(action="/dns/%s/serial" % zone_id) |
| except BaseHTTPError as e: |
| if e.code == httplib.NOT_FOUND: |
| raise ZoneDoesNotExistError(zone_id=None, driver=self, value=e.message) |
| raise e |
| return Zone(id=None, domain=zone_id, type="master", ttl=3600, driver=self) |
| |
| def ex_get_records_by(self, zone, name=None, type=None): |
| """ |
| Return a list of records for the provided zone, filtered by name and/or |
| type. |
| |
| :param zone: Zone to list records for. |
| :type zone: :class:`Zone` |
| |
| :param zone: Zone where the requested records are found. |
| :type zone: :class:`Zone` |
| |
| :param name: name of the records, for example "www". (optional) |
| :type name: ``str`` |
| |
| :param type: DNS record type (A, MX, TXT). (optional) |
| :type type: :class:`RecordType` |
| |
| :return: ``list`` of :class:`Record` |
| """ |
| payload = {} |
| if name is not None: |
| payload["name"] = name |
| if type is not None: |
| payload["type"] = type |
| |
| action = "/dns/%s/listRRs" % zone.domain |
| response = self.connection.request(action=action, data=payload, method="POST") |
| return self._to_records(response, zone) |
| |
| def create_record(self, name, zone, type, data, extra=None): |
| """ |
| Create a new record. |
| |
| :param name: Record name without the domain name (e.g. www). |
| Note: If you want to create a record for a base domain |
| name, you should specify empty string ('') for this |
| argument. |
| :type name: ``str`` |
| |
| :param zone: Zone where the requested record is created. |
| :type zone: :class:`Zone` |
| |
| :param type: DNS record type (A, MX, TXT). |
| :type type: :class:`RecordType` |
| |
| :param data: Data for the record (depends on the record type). |
| :type data: ``str`` |
| |
| :param extra: Extra attributes (driver specific, e.g. 'ttl'). |
| (optional) |
| :type extra: ``dict`` |
| |
| :rtype: :class:`Record` |
| """ |
| action = "/dns/%s/addRR" % zone.domain |
| payload = {"name": name, "data": data, "type": type} |
| if extra is not None and extra.get("ttl", None) is not None: |
| payload["ttl"] = extra["ttl"] |
| try: |
| self.connection.request(action=action, data=payload, method="POST") |
| except BaseHTTPError as e: |
| exists_re = re.compile(r"That RR already exists on the domain") |
| if ( |
| e.code == httplib.BAD_REQUEST |
| and re.search(exists_re, e.message) is not None |
| ): |
| value = '"%s" already exists in %s' % (name, zone.domain) |
| raise RecordAlreadyExistsError(value=value, driver=self, record_id=None) |
| raise e |
| return self.ex_get_records_by(zone=zone, name=name, type=type)[0] |
| |
| def delete_record(self, record): |
| """ |
| Use this method to delete a record. |
| |
| :param record: record to delete |
| :type record: `Record` |
| |
| :rtype: Bool |
| """ |
| action = "/dns/%s/removeRR" % record.zone.domain |
| payload = {"name": record.name, "data": record.data, "type": record.type} |
| try: |
| self.connection.request(action=action, data=payload, method="POST") |
| except BaseHTTPError as e: |
| if e.code == httplib.NOT_FOUND: |
| raise RecordDoesNotExistError( |
| value=e.message, driver=self, record_id=None |
| ) |
| raise e |
| return True |
| |
| def _to_record(self, item, zone): |
| ttl = int(item["ttl"]) |
| return Record( |
| id=None, |
| name=item["name"], |
| data=item["data"], |
| type=item["type"], |
| zone=zone, |
| driver=self, |
| ttl=ttl, |
| ) |
| |
| def _to_records(self, items, zone): |
| records = [] |
| for item in items.object: |
| records.append(self._to_record(item, zone)) |
| return records |