blob: 2576b24aea48455332ac3f48645e9bab9b214fb7 [file] [log] [blame]
# 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
)