| # 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. |
| |
| from __future__ import with_statement |
| |
| __all__ = ["GandiDNSDriver"] |
| |
| from libcloud.common.gandi import BaseGandiDriver, GandiConnection |
| from libcloud.common.gandi import GandiResponse |
| from libcloud.dns.types import Provider, RecordType |
| from libcloud.dns.types import RecordError |
| from libcloud.dns.types import ZoneDoesNotExistError, RecordDoesNotExistError |
| from libcloud.dns.base import DNSDriver, Zone, Record |
| |
| |
| TTL_MIN = 30 |
| TTL_MAX = 2592000 # 30 days |
| |
| |
| class NewZoneVersion(object): |
| """ |
| Changes to a zone in the Gandi DNS service need to be wrapped in a new |
| version object. The changes are made to the new version, then that |
| version is made active. |
| |
| In effect, this is a transaction. |
| |
| Any calls made inside this context manager will be applied to a new version |
| id. If your changes are successful (and only if they are successful) they |
| are activated. |
| """ |
| |
| def __init__(self, driver, zone): |
| self.driver = driver |
| self.connection = driver.connection |
| self.zone = zone |
| |
| def __enter__(self): |
| zid = int(self.zone.id) |
| self.connection.set_context({"zone_id": self.zone.id}) |
| vid = self.connection.request("domain.zone.version.new", zid).object |
| self.vid = vid |
| return vid |
| |
| def __exit__(self, type, value, traceback): |
| if not traceback: |
| zid = int(self.zone.id) |
| con = self.connection |
| con.set_context({"zone_id": self.zone.id}) |
| con.request("domain.zone.version.set", zid, self.vid).object |
| |
| |
| class GandiDNSResponse(GandiResponse): |
| exceptions = { |
| 581042: ZoneDoesNotExistError, |
| } |
| |
| |
| class GandiDNSConnection(GandiConnection): |
| responseCls = GandiDNSResponse |
| |
| |
| class GandiDNSDriver(BaseGandiDriver, DNSDriver): |
| """ |
| API reference can be found at: |
| |
| http://doc.rpc.gandi.net/domain/reference.html |
| """ |
| |
| type = Provider.GANDI |
| name = "Gandi DNS" |
| website = "http://www.gandi.net/domain" |
| |
| connectionCls = GandiDNSConnection |
| |
| RECORD_TYPE_MAP = { |
| RecordType.A: "A", |
| RecordType.AAAA: "AAAA", |
| RecordType.CNAME: "CNAME", |
| RecordType.LOC: "LOC", |
| RecordType.MX: "MX", |
| RecordType.NS: "NS", |
| RecordType.SPF: "SPF", |
| RecordType.SRV: "SRV", |
| RecordType.TXT: "TXT", |
| RecordType.WKS: "WKS", |
| } |
| |
| def _to_zone(self, zone): |
| return Zone( |
| id=str(zone["id"]), |
| domain=zone["name"], |
| type="master", |
| ttl=0, |
| driver=self, |
| extra={}, |
| ) |
| |
| def _to_zones(self, zones): |
| ret = [] |
| for z in zones: |
| ret.append(self._to_zone(z)) |
| return ret |
| |
| def list_zones(self): |
| zones = self.connection.request("domain.zone.list") |
| return self._to_zones(zones.object) |
| |
| def get_zone(self, zone_id): |
| zid = int(zone_id) |
| self.connection.set_context({"zone_id": zone_id}) |
| zone = self.connection.request("domain.zone.info", zid) |
| return self._to_zone(zone.object) |
| |
| def create_zone(self, domain, type="master", ttl=None, extra=None): |
| params = { |
| "name": domain, |
| } |
| info = self.connection.request("domain.zone.create", params) |
| return self._to_zone(info.object) |
| |
| def update_zone(self, zone, domain=None, type=None, ttl=None, extra=None): |
| zid = int(zone.id) |
| params = {"name": domain} |
| self.connection.set_context({"zone_id": zone.id}) |
| zone = self.connection.request("domain.zone.update", zid, params) |
| return self._to_zone(zone.object) |
| |
| def delete_zone(self, zone): |
| zid = int(zone.id) |
| self.connection.set_context({"zone_id": zone.id}) |
| res = self.connection.request("domain.zone.delete", zid) |
| return res.object |
| |
| def _to_record(self, record, zone): |
| extra = {"ttl": int(record["ttl"])} |
| value = record["value"] |
| if record["type"] == "MX": |
| # Record is in the following form: |
| # <priority> <value> |
| # e.g. 15 aspmx.l.google.com |
| split = record["value"].split(" ") |
| extra["priority"] = int(split[0]) |
| value = split[1] |
| return Record( |
| id="%s:%s" % (record["type"], record["name"]), |
| name=record["name"], |
| type=self._string_to_record_type(record["type"]), |
| data=value, |
| zone=zone, |
| driver=self, |
| ttl=record["ttl"], |
| extra=extra, |
| ) |
| |
| def _to_records(self, records, zone): |
| retval = [] |
| for r in records: |
| retval.append(self._to_record(r, zone)) |
| return retval |
| |
| def list_records(self, zone): |
| zid = int(zone.id) |
| self.connection.set_context({"zone_id": zone.id}) |
| records = self.connection.request("domain.zone.record.list", zid, 0) |
| return self._to_records(records.object, zone) |
| |
| def get_record(self, zone_id, record_id): |
| zid = int(zone_id) |
| record_type, name = record_id.split(":", 1) |
| filter_opts = {"name": name, "type": record_type} |
| self.connection.set_context({"zone_id": zone_id}) |
| records = self.connection.request( |
| "domain.zone.record.list", zid, 0, filter_opts |
| ).object |
| |
| if len(records) == 0: |
| raise RecordDoesNotExistError(value="", driver=self, record_id=record_id) |
| |
| return self._to_record(records[0], self.get_zone(zone_id)) |
| |
| def _validate_record(self, record_id, name, record_type, data, extra): |
| if len(data) > 1024: |
| raise RecordError( |
| "Record data must be <= 1024 characters", |
| driver=self, |
| record_id=record_id, |
| ) |
| if extra and "ttl" in extra: |
| if extra["ttl"] < TTL_MIN: |
| raise RecordError( |
| "TTL must be at least 30 seconds", driver=self, record_id=record_id |
| ) |
| if extra["ttl"] > TTL_MAX: |
| raise RecordError( |
| "TTL must not excdeed 30 days", driver=self, record_id=record_id |
| ) |
| |
| def create_record(self, name, zone, type, data, extra=None): |
| self._validate_record(None, name, type, data, extra) |
| |
| zid = int(zone.id) |
| |
| create = {"name": name, "type": self.RECORD_TYPE_MAP[type], "value": data} |
| |
| if "ttl" in extra: |
| create["ttl"] = extra["ttl"] |
| |
| with NewZoneVersion(self, zone) as vid: |
| con = self.connection |
| con.set_context({"zone_id": zone.id}) |
| rec = con.request("domain.zone.record.add", zid, vid, create).object |
| |
| return self._to_record(rec, zone) |
| |
| def update_record(self, record, name, type, data, extra): |
| self._validate_record(record.id, name, type, data, extra) |
| |
| filter_opts = {"name": record.name, "type": self.RECORD_TYPE_MAP[record.type]} |
| |
| update = {"name": name, "type": self.RECORD_TYPE_MAP[type], "value": data} |
| |
| if "ttl" in extra: |
| update["ttl"] = extra["ttl"] |
| |
| zid = int(record.zone.id) |
| |
| with NewZoneVersion(self, record.zone) as vid: |
| con = self.connection |
| con.set_context({"zone_id": record.zone.id}) |
| con.request("domain.zone.record.delete", zid, vid, filter_opts) |
| res = con.request("domain.zone.record.add", zid, vid, update).object |
| |
| return self._to_record(res, record.zone) |
| |
| def delete_record(self, record): |
| zid = int(record.zone.id) |
| |
| filter_opts = {"name": record.name, "type": self.RECORD_TYPE_MAP[record.type]} |
| |
| with NewZoneVersion(self, record.zone) as vid: |
| con = self.connection |
| con.set_context({"zone_id": record.zone.id}) |
| count = con.request( |
| "domain.zone.record.delete", zid, vid, filter_opts |
| ).object |
| |
| if count == 1: |
| return True |
| |
| raise RecordDoesNotExistError( |
| value="No such record", driver=self, record_id=record.id |
| ) |