summaryrefslogtreecommitdiff
path: root/trove/dns/designate/driver.py
blob: 13e441dd3b69fa60a6f6e417de7c1482e20fa3b4 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
# Copyright (c) 2013 OpenStack Foundation
# All Rights Reserved.
#
#    Licensed 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.

"""
Dns Driver that uses Designate DNSaaS.
"""

import base64
import hashlib

from designateclient import client
from designateclient.v1.records import Record
from keystoneauth1 import loading
from keystoneauth1 import session
from oslo_log import log as logging
from oslo_utils import encodeutils
import six

from trove.common import cfg
from trove.common import exception
from trove.common.i18n import _
from trove.dns import driver


CONF = cfg.CONF

DNS_TENANT_ID = CONF.dns_account_id
DNS_AUTH_URL = CONF.dns_auth_url
DNS_ENDPOINT_URL = CONF.dns_endpoint_url
DNS_SERVICE_TYPE = CONF.dns_service_type
DNS_REGION = CONF.dns_region
DNS_USERNAME = CONF.dns_username
DNS_PASSKEY = CONF.dns_passkey
DNS_TTL = CONF.dns_ttl
DNS_DOMAIN_ID = CONF.dns_domain_id
DNS_DOMAIN_NAME = CONF.dns_domain_name
DNS_USER_DOMAIN_ID = CONF.dns_user_domain_id
DNS_PROJECT_DOMAIN_ID = CONF.dns_project_domain_id

LOG = logging.getLogger(__name__)


class DesignateObjectConverter(object):

    @staticmethod
    def domain_to_zone(domain):
        return DesignateDnsZone(id=domain.id, name=domain.name)

    def record_to_entry(self, record, dns_zone):
        return driver.DnsEntry(name=record.name, content=record.data,
                               type=record.type, ttl=record.ttl,
                               priority=record.priority, dns_zone=dns_zone)


def create_designate_client(api_version='2'):
    """Creates a Designate DNSaaS client."""
    loader = loading.get_plugin_loader('password')
    auth = loader.load_from_options(auth_url=DNS_AUTH_URL,
                                    username=DNS_USERNAME,
                                    password=DNS_PASSKEY,
                                    project_id=DNS_TENANT_ID,
                                    user_domain_id=DNS_USER_DOMAIN_ID,
                                    project_domain_id=DNS_PROJECT_DOMAIN_ID)
    sesh = session.Session(auth=auth)
    return client.Client(api_version, session=sesh)


class DesignateDriver(driver.DnsDriver):

    def __init__(self):
        self.dns_client = create_designate_client(api_version='1')
        self.converter = DesignateObjectConverter()
        self.default_dns_zone = DesignateDnsZone(id=DNS_DOMAIN_ID,
                                                 name=DNS_DOMAIN_NAME)

    def create_entry(self, entry, content):
        """Creates the entry in the driver at the given dns zone."""
        dns_zone = entry.dns_zone or self.default_dns_zone
        if not dns_zone.id:
            raise TypeError(_("The entry's dns_zone must have an ID "
                              "specified."))
        name = entry.name
        LOG.debug("Creating DNS entry %s.", name)
        client = self.dns_client
        # Record name has to end with a '.' by dns standard
        record = Record(name=entry.name + '.',
                        type=entry.type,
                        data=content,
                        ttl=entry.ttl,
                        priority=entry.priority)
        client.records.create(dns_zone.id, record)

    def delete_entry(self, name, type, dns_zone=None):
        """Deletes an entry with the given name and type from a dns zone."""
        dns_zone = dns_zone or self.default_dns_zone
        records = self._get_records(dns_zone)
        matching_record = [rec for rec in records
                           if rec.name == name + '.' and rec.type == type]
        if not matching_record:
            raise exception.DnsRecordNotFound(name=name)
        LOG.debug("Deleting DNS entry %s.", name)
        self.dns_client.records.delete(dns_zone.id, matching_record[0].id)

    def get_entries_by_content(self, content, dns_zone=None):
        """Retrieves all entries in a DNS zone with matching content field."""
        records = self._get_records(dns_zone)
        return [self.converter.record_to_entry(record, dns_zone)
                for record in records if record.data == content]

    def get_entries_by_name(self, name, dns_zone):
        records = self._get_records(dns_zone)
        return [self.converter.record_to_entry(record, dns_zone)
                for record in records if record.name == name]

    def get_dns_zones(self, name=None):
        """Returns all dns zones (optionally filtered by the name argument."""
        domains = self.dns_client.domains.list()
        return [self.converter.domain_to_zone(domain)
                for domain in domains if not name or domain.name == name]

    def modify_content(self, name, content, dns_zone):
        # We dont need this in trove for now
        raise NotImplementedError(_("Not implemented for Designate DNS."))

    def rename_entry(self, content, name, dns_zone):
        # We dont need this in trove for now
        raise NotImplementedError(_("Not implemented for Designate DNS."))

    def _get_records(self, dns_zone):
        dns_zone = dns_zone or self.default_dns_zone
        if not dns_zone:
            raise TypeError(_('DNS domain is must be specified'))
        return self.dns_client.records.list(dns_zone.id)


class DesignateDriverV2(driver.DnsDriver):

    def __init__(self):
        self.dns_client = create_designate_client()
        self.default_dns_zone = DesignateDnsZone(id=DNS_DOMAIN_ID,
                                                 name=DNS_DOMAIN_NAME)

    def create_entry(self, entry, content):
        """Creates the entry in the driver at the given dns zone."""
        dns_zone = entry.dns_zone or self.default_dns_zone
        if not dns_zone.id:
            raise TypeError(_("The entry's dns_zone must have an ID "
                              "specified."))
        name = entry.name
        LOG.debug("Creating DNS entry %s.", name)
        client = self.dns_client
        # Record name has to end with a '.' by dns standard
        client.recordsets.create(DNS_DOMAIN_ID, entry.name + '.', entry.type,
                                 records=[content])

    def delete_entry(self, name, type, dns_zone=None):
        """Deletes an entry with the given name and type from a dns zone."""
        dns_zone = dns_zone or self.default_dns_zone
        records = self._get_records(dns_zone)
        matching_record = [rec for rec in records
                           if rec['name'] == name + '.'
                           and rec['type'] == type]
        if not matching_record:
            raise exception.DnsRecordNotFound(name)
        LOG.debug("Deleting DNS entry %s.", name)
        self.dns_client.recordsets.delete(dns_zone.id,
                                          matching_record[0]['id'])

    def _get_records(self, dns_zone):
        dns_zone = dns_zone or self.default_dns_zone
        if not dns_zone:
            raise TypeError(_('DNS domain is must be specified'))
        return self.dns_client.recordsets.list(dns_zone.id)


class DesignateInstanceEntryFactory(driver.DnsInstanceEntryFactory):
    """Defines how instance DNS entries are created for instances."""

    def create_entry(self, instance_id):
        zone = DesignateDnsZone(id=DNS_DOMAIN_ID, name=DNS_DOMAIN_NAME)
        # Constructing the hostname by hashing the instance ID.
        name = encodeutils.to_utf8(instance_id)
        name = hashlib.md5(name).digest()
        name = base64.b32encode(name)[:11].lower()
        if six.PY3:
            name = name.decode('ascii')
        hostname = ("%s.%s" % (name, zone.name))
        # Removing the leading dot if present
        if hostname.endswith('.'):
            hostname = hostname[:-1]

        return driver.DnsEntry(name=hostname, content=None, type="A",
                               ttl=DNS_TTL, dns_zone=zone)


class DesignateDnsZone(driver.DnsZone):

    def __init__(self, id, name):
        self._name = name
        self._id = id

    @property
    def name(self):
        return self._name

    @name.setter
    def name(self, value):
        self._name = value

    @property
    def id(self):
        return self._id

    @id.setter
    def id(self, value):
        self._id = value

    def __eq__(self, other):
        return (isinstance(other, DesignateDnsZone) and
                self.name == other.name and
                self.id == other.id)

    def __str__(self):
        return "%s:%s" % (self.id, self.name)