Sfoglia il codice sorgente

Added openstack implementation of dns service

Nuwan Goonasekera 6 anni fa
parent
commit
5c4b2df8aa

+ 1 - 1
cloudbridge/base/helpers.py

@@ -10,7 +10,7 @@ from cryptography.hazmat.backends import default_backend
 from cryptography.hazmat.primitives import serialization as crypt_serialization
 from cryptography.hazmat.primitives.asymmetric import rsa
 
-from deprecation import deprecated
+from deprecated import deprecated
 
 import six
 

+ 4 - 0
cloudbridge/base/services.py

@@ -375,6 +375,10 @@ class BaseDnsZoneService(BasePageableObjectMixin, DnsZoneService,
     def __init__(self, provider):
         super(BaseDnsZoneService, self).__init__(provider)
 
+    def _get_fully_qualified_dns(self, name):
+        # Add a trailing dot to fully qualify
+        return name + '.' if not name.endswith('.') else name
+
 
 class BaseDnsRecordService(BasePageableObjectMixin, DnsRecordService,
                            BaseCloudService):

+ 12 - 3
cloudbridge/interfaces/resources.py

@@ -1316,6 +1316,18 @@ class DnsZone(CloudResource):
     """
     __metaclass__ = ABCMeta
 
+    @property
+    def admin_email(self):
+        """
+        Email address of this zone's administrator. Some cloud providers do not
+        support this field, and therefore, it may be stored in an extra field
+        such as description or not supported at all. (This field is mandatory
+        in OpenStack)
+
+        :return: Administrator's email as a string
+        """
+        pass
+
     @abstractmethod
     def delete(self):
         """
@@ -1342,14 +1354,11 @@ class DnsRecordType(object):
     AAAA = 'AAAA'
     CNAME = 'CNAME'
     MX = 'MX'
-    NAPTR = 'NAPTR'
     NS = 'NS'
     PTR = 'PTR'
-    SOA = 'SOA'
     SPF = 'SPF'
     SRV = 'SRV'
     SSHFP = 'SSHFP'
-    TLSA = 'TLSA'
     TXT = 'TXT'
 
 

+ 4 - 1
cloudbridge/interfaces/services.py

@@ -948,13 +948,16 @@ class DnsZoneService(PageableObjectMixin, CloudService):
         pass
 
     @abstractmethod
-    def create(self, label):
+    def create(self, label, admin_email):
         """
         Create a new host zone.
 
         :type label: ``str``
         :param label: A host zone label.
 
+        :type admin_email: ``str``
+        :param admin_email: Email address of this zone's administrator.
+
         :rtype: ``object`` of :class:`.DnsZone`
         :return:  A DnsZone object
         """

+ 4 - 0
cloudbridge/providers/aws/resources.py

@@ -1158,6 +1158,10 @@ class AWSDnsZone(BaseDnsZone):
     def name(self):
         return self._dns_zone.get('Name')
 
+    @property
+    def admin_email(self):
+        return self._dns_zone.get('Name')
+
     @property
     def records(self):
         return self._dns_record_container

+ 5 - 5
cloudbridge/providers/aws/services.py

@@ -1335,7 +1335,7 @@ class AWSDnsZoneService(BaseDnsZoneService):
         super(AWSDnsZoneService, self).__init__(provider)
 
     @dispatch(event="provider.dns.host_zones.get",
-              priority=BaseNetworkService.STANDARD_EVENT_PRIORITY)
+              priority=BaseDnsZoneService.STANDARD_EVENT_PRIORITY)
     def get(self, dns_zone_id):
         try:
             dns_zone = self.provider.dns.client.get_hosted_zone(Id=dns_zone_id)
@@ -1344,7 +1344,7 @@ class AWSDnsZoneService(BaseDnsZoneService):
             return None
 
     @dispatch(event="provider.dns.host_zones.list",
-              priority=BaseNetworkService.STANDARD_EVENT_PRIORITY)
+              priority=BaseDnsZoneService.STANDARD_EVENT_PRIORITY)
     def list(self, limit=None, marker=None):
         response = self.provider.dns.client.list_hosted_zones(
             **trim_empty_params({'MaxItems': limit, 'Marker': marker}))
@@ -1356,7 +1356,7 @@ class AWSDnsZoneService(BaseDnsZoneService):
                                      data=cb_objs)
 
     @dispatch(event="provider.dns.host_zones.find",
-              priority=BaseNetworkService.STANDARD_EVENT_PRIORITY)
+              priority=BaseDnsZoneService.STANDARD_EVENT_PRIORITY)
     def find(self, **kwargs):
         filters = ['name']
         matches = cb_helpers.generic_find(filters, kwargs, self)
@@ -1364,7 +1364,7 @@ class AWSDnsZoneService(BaseDnsZoneService):
                                      limit=None, marker=None)
 
     @dispatch(event="provider.dns.host_zones.create",
-              priority=BaseNetworkService.STANDARD_EVENT_PRIORITY)
+              priority=BaseDnsZoneService.STANDARD_EVENT_PRIORITY)
     def create(self, name):
         AWSDnsZone.assert_valid_resource_name(name)
 
@@ -1373,7 +1373,7 @@ class AWSDnsZoneService(BaseDnsZoneService):
         return AWSDnsZone(self.provider, response.get('HostedZone'))
 
     @dispatch(event="provider.dns.host_zones.delete",
-              priority=BaseNetworkService.STANDARD_EVENT_PRIORITY)
+              priority=BaseDnsZoneService.STANDARD_EVENT_PRIORITY)
     def delete(self, dns_zone):
         dns_zone = (dns_zone if isinstance(dns_zone, AWSDnsZone)
                     else self.get(dns_zone))

+ 6 - 6
cloudbridge/providers/gcp/services.py

@@ -1632,14 +1632,14 @@ class GCPDnsZoneService(BaseDnsZoneService):
         super(GCPDnsZoneService, self).__init__(provider)
 
     @dispatch(event="provider.dns.host_zones.get",
-              priority=BaseNetworkService.STANDARD_EVENT_PRIORITY)
+              priority=BaseDnsZoneService.STANDARD_EVENT_PRIORITY)
     def get(self, dns_zone_id):
         dns_zone = self.provider.get_resource(
             'managedZones', dns_zone_id, project=self._provider.project_name)
         return GCPDnsZone(self.provider, dns_zone) if dns_zone else None
 
     @dispatch(event="provider.dns.host_zones.list",
-              priority=BaseNetworkService.STANDARD_EVENT_PRIORITY)
+              priority=BaseDnsZoneService.STANDARD_EVENT_PRIORITY)
     def list(self, limit=None, marker=None):
         max_result = limit if limit is not None and limit < 500 else 500
         response = (self.provider
@@ -1660,7 +1660,7 @@ class GCPDnsZoneService(BaseDnsZoneService):
                                      False, data=dns_zones)
 
     @dispatch(event="provider.dns.host_zones.find",
-              priority=BaseNetworkService.STANDARD_EVENT_PRIORITY)
+              priority=BaseDnsZoneService.STANDARD_EVENT_PRIORITY)
     def find(self, **kwargs):
         filters = ['name']
         matches = cb_helpers.generic_find(filters, kwargs, self)
@@ -1668,13 +1668,13 @@ class GCPDnsZoneService(BaseDnsZoneService):
                                      limit=None, marker=None)
 
     @dispatch(event="provider.dns.host_zones.create",
-              priority=BaseNetworkService.STANDARD_EVENT_PRIORITY)
+              priority=BaseDnsZoneService.STANDARD_EVENT_PRIORITY)
     def create(self, name):
         GCPDnsZone.assert_valid_resource_name(name)
         body = {
             'kind': 'dns#managedZone',
             'name': cb_helpers.to_resource_name(name),
-            'dnsName':  name + '.' if not name.endswith('.') else name,
+            'dnsName':  self._get_fully_qualified_dns(name),
             'description': name,
             'visibility': 'public'
         }
@@ -1695,7 +1695,7 @@ class GCPDnsZoneService(BaseDnsZoneService):
                 raise
 
     @dispatch(event="provider.dns.host_zones.delete",
-              priority=BaseNetworkService.STANDARD_EVENT_PRIORITY)
+              priority=BaseDnsZoneService.STANDARD_EVENT_PRIORITY)
     def delete(self, dns_zone):
         zone = (dns_zone if isinstance(dns_zone, GCPDnsZone)
                 else self.get(dns_zone))

+ 6 - 0
cloudbridge/providers/openstack/provider.py

@@ -21,6 +21,7 @@ from cloudbridge.base import BaseCloudProvider
 from cloudbridge.base.helpers import get_env
 
 from .services import OpenStackComputeService
+from .services import OpenStackDnsService
 from .services import OpenStackNetworkingService
 from .services import OpenStackSecurityService
 from .services import OpenStackStorageService
@@ -84,6 +85,7 @@ class OpenStackCloudProvider(BaseCloudProvider):
         self._networking = OpenStackNetworkingService(self)
         self._security = OpenStackSecurityService(self)
         self._storage = OpenStackStorageService(self)
+        self._dns = OpenStackDnsService(self)
 
     @property
     def nova(self):
@@ -196,6 +198,10 @@ class OpenStackCloudProvider(BaseCloudProvider):
     def storage(self):
         return self._storage
 
+    @property
+    def dns(self):
+        return self._dns
+
     def _connect_nova(self):
         return self._connect_nova_region(self.region_name)
 

+ 64 - 0
cloudbridge/providers/openstack/resources.py

@@ -26,6 +26,8 @@ from swiftclient.utils import generate_temp_url
 from cloudbridge.base.resources import BaseAttachmentInfo
 from cloudbridge.base.resources import BaseBucket
 from cloudbridge.base.resources import BaseBucketObject
+from cloudbridge.base.resources import BaseDnsRecord
+from cloudbridge.base.resources import BaseDnsZone
 from cloudbridge.base.resources import BaseFloatingIP
 from cloudbridge.base.resources import BaseInstance
 from cloudbridge.base.resources import BaseInternetGateway
@@ -52,6 +54,7 @@ from cloudbridge.interfaces.resources import TrafficDirection
 from cloudbridge.interfaces.resources import VolumeState
 
 from .subservices import OpenStackBucketObjectSubService
+from .subservices import OpenStackDnsRecordSubService
 from .subservices import OpenStackFloatingIPSubService
 from .subservices import OpenStackGatewaySubService
 from .subservices import OpenStackSubnetSubService
@@ -1344,3 +1347,64 @@ class OpenStackBucket(BaseBucket):
     @property
     def objects(self):
         return self._object_container
+
+
+class OpenStackDnsZone(BaseDnsZone):
+
+    def __init__(self, provider, dns_zone):
+        super(OpenStackDnsZone, self).__init__(provider)
+        self._dns_zone = dns_zone
+        self._dns_record_container = OpenStackDnsRecordSubService(
+            provider, self)
+
+    @property
+    def id(self):
+        return self._dns_zone.id
+
+    @property
+    def name(self):
+        return self._dns_zone.name
+
+    @property
+    def admin_email(self):
+        return self._dns_zone.email
+
+    @property
+    def records(self):
+        return self._dns_record_container
+
+
+class OpenStackDnsRecord(BaseDnsRecord):
+
+    def __init__(self, provider, dns_zone, dns_record):
+        super(OpenStackDnsRecord, self).__init__(provider)
+        self._dns_zone = dns_zone
+        self._dns_rec = dns_record
+
+    @property
+    def id(self):
+        return self._dns_rec.id
+
+    @property
+    def name(self):
+        return self._dns_rec.name
+
+    @property
+    def zone_id(self):
+        return self._dns_zone.id
+
+    @property
+    def type(self):
+        return self._dns_rec.type
+
+    @property
+    def data(self):
+        return self._dns_rec.records
+
+    @property
+    def ttl(self):
+        return self._dns_rec.ttl
+
+    def delete(self):
+        # pylint:disable=protected-access
+        return self._provider.dns._records.delete(self._dns_zone, self)

+ 130 - 0
cloudbridge/providers/openstack/services.py

@@ -10,6 +10,7 @@ from neutronclient.common.exceptions import PortNotFoundClient
 
 from novaclient.exceptions import NotFound as NovaNotFound
 
+from openstack.exceptions import BadRequestException
 from openstack.exceptions import HttpException
 from openstack.exceptions import NotFoundException
 from openstack.exceptions import ResourceNotFound
@@ -23,6 +24,9 @@ from cloudbridge.base.resources import ClientPagedResultList
 from cloudbridge.base.services import BaseBucketObjectService
 from cloudbridge.base.services import BaseBucketService
 from cloudbridge.base.services import BaseComputeService
+from cloudbridge.base.services import BaseDnsRecordService
+from cloudbridge.base.services import BaseDnsService
+from cloudbridge.base.services import BaseDnsZoneService
 from cloudbridge.base.services import BaseFloatingIPService
 from cloudbridge.base.services import BaseGatewayService
 from cloudbridge.base.services import BaseImageService
@@ -59,6 +63,8 @@ from cloudbridge.interfaces.resources import Volume
 from . import helpers as oshelpers
 from .resources import OpenStackBucket
 from .resources import OpenStackBucketObject
+from .resources import OpenStackDnsRecord
+from .resources import OpenStackDnsZone
 from .resources import OpenStackFloatingIP
 from .resources import OpenStackInstance
 from .resources import OpenStackInternetGateway
@@ -1292,3 +1298,127 @@ class OpenStackFloatingIPService(BaseFloatingIPService):
                 log.debug("Floating IP %s not found.", fip)
                 return True
         os_ip.delete(self._provider.os_conn.session)
+
+
+class OpenStackDnsService(BaseDnsService):
+
+    def __init__(self, provider):
+        super(OpenStackDnsService, self).__init__(provider)
+
+        # Initialize provider services
+        self._zone_svc = OpenStackDnsZoneService(self.provider)
+        self._record_svc = OpenStackDnsRecordService(self.provider)
+
+    @property
+    def host_zones(self):
+        return self._zone_svc
+
+    @property
+    def _records(self):
+        return self._record_svc
+
+
+class OpenStackDnsZoneService(BaseDnsZoneService):
+
+    def __init__(self, provider):
+        super(OpenStackDnsZoneService, self).__init__(provider)
+
+    @dispatch(event="provider.dns.host_zones.get",
+              priority=BaseDnsZoneService.STANDARD_EVENT_PRIORITY)
+    def get(self, dns_zone_id):
+        try:
+            return OpenStackDnsZone(
+                self.provider,
+                self.provider.os_conn.dns.get_zone(dns_zone_id))
+        except (ResourceNotFound, NotFoundException, BadRequestException):
+            log.debug("Dns Zone %s not found.", dns_zone_id)
+            return None
+
+    @dispatch(event="provider.dns.host_zones.list",
+              priority=BaseDnsZoneService.STANDARD_EVENT_PRIORITY)
+    def list(self, limit=None, marker=None):
+        zones = [OpenStackDnsZone(self.provider, zone)
+                 for zone in self.provider.os_conn.dns.zones()]
+        return ClientPagedResultList(self.provider, zones,
+                                     limit=limit, marker=marker)
+
+    @dispatch(event="provider.dns.host_zones.find",
+              priority=BaseDnsZoneService.STANDARD_EVENT_PRIORITY)
+    def find(self, **kwargs):
+        filters = ['name']
+        matches = cb_helpers.generic_find(filters, kwargs, self)
+        return ClientPagedResultList(self.provider, list(matches),
+                                     limit=None, marker=None)
+
+    @dispatch(event="provider.dns.host_zones.create",
+              priority=BaseDnsZoneService.STANDARD_EVENT_PRIORITY)
+    def create(self, name, admin_email):
+        OpenStackDnsZone.assert_valid_resource_name(name)
+
+        return OpenStackDnsZone(
+            self.provider, self.provider.os_conn.dns.create_zone(
+                name=self._get_fully_qualified_dns(name),
+                email=admin_email, ttl=3600))
+
+    @dispatch(event="provider.dns.host_zones.delete",
+              priority=BaseDnsZoneService.STANDARD_EVENT_PRIORITY)
+    def delete(self, dns_zone):
+        zone_id = (dns_zone.id if isinstance(dns_zone, OpenStackDnsZone)
+                   else dns_zone)
+        if zone_id:
+            self.provider.os_conn.dns.delete_zone(zone_id)
+
+
+class OpenStackDnsRecordService(BaseDnsRecordService):
+
+    def __init__(self, provider):
+        super(OpenStackDnsRecordService, self).__init__(provider)
+
+    def _to_resource_records(self, data, rec_type):
+        """
+        Converts a record to what OpenStack expects. For example,
+        OpenStack expects a fully qualified name for all CNAME records.
+        """
+        if isinstance(data, list):
+            records = data
+        else:
+            records = [data]
+        return [self._standardize_record(r, rec_type) for r in records]
+
+    def get(self, dns_zone, rec_id):
+        try:
+            return OpenStackDnsRecord(
+                self.provider, dns_zone,
+                self.provider.os_conn.dns.get_recordset(rec_id, dns_zone.id))
+        except (ResourceNotFound, NotFoundException, BadRequestException):
+            log.debug("Dns Record %s not found.", rec_id)
+            return None
+
+    def list(self, dns_zone, limit=None, marker=None):
+        recs = [OpenStackDnsRecord(self.provider, dns_zone, rec)
+                for rec in self.provider.os_conn.dns.recordsets(dns_zone.id)]
+        return ClientPagedResultList(self.provider, recs,
+                                     limit=limit, marker=marker)
+
+    def find(self, dns_zone, **kwargs):
+        filters = ['name']
+        matches = cb_helpers.generic_find(filters, kwargs, dns_zone.records)
+        return ClientPagedResultList(self.provider, list(matches),
+                                     limit=None, marker=None)
+
+    def create(self, dns_zone, name, type, data, ttl=None):
+        OpenStackDnsZone.assert_valid_resource_name(name)
+
+        return OpenStackDnsRecord(
+            self.provider, dns_zone,
+            self.provider.os_conn.dns.create_recordset(
+                zone=dns_zone.id, name=name, type=type,
+                records=self._to_resource_records(data, type),
+                ttl=ttl or 3600))
+
+    def delete(self, dns_zone, record):
+        rec_id = (record.id if isinstance(record, OpenStackDnsRecord)
+                  else record)
+        if rec_id:
+            self.provider.os_conn.dns.delete_recordset(
+                rec_id, zone=dns_zone.id)

+ 7 - 0
cloudbridge/providers/openstack/subservices.py

@@ -1,6 +1,7 @@
 import logging
 
 from cloudbridge.base.subservices import BaseBucketObjectSubService
+from cloudbridge.base.subservices import BaseDnsRecordSubService
 from cloudbridge.base.subservices import BaseFloatingIPSubService
 from cloudbridge.base.subservices import BaseGatewaySubService
 from cloudbridge.base.subservices import BaseSubnetSubService
@@ -39,3 +40,9 @@ class OpenStackSubnetSubService(BaseSubnetSubService):
 
     def __init__(self, provider, network):
         super(OpenStackSubnetSubService, self).__init__(provider, network)
+
+
+class OpenStackDnsRecordSubService(BaseDnsRecordSubService):
+
+    def __init__(self, provider, dns_zone):
+        super(OpenStackDnsRecordSubService, self).__init__(provider, dns_zone)

+ 1 - 1
setup.py

@@ -50,7 +50,7 @@ REQS_GCP = [
     'oauth2client<=4.1.3'
 ]
 REQS_OPENSTACK = [
-    'openstacksdk>=0.12.0,<=0.17',
+    'openstacksdk>=0.12.0',
     'python-novaclient>=7.0.0,<=11.0',
     'python-glanceclient>=2.5.0,<=2.12',
     'python-cinderclient>=1.9.0,<=4.0',

+ 14 - 6
tests/test_dns_service.py

@@ -18,15 +18,19 @@ class CloudDnsServiceTestCase(ProviderTestBase):
         def create_dns_zone(name):
             if name:
                 name = name + ".com."
-            return self.provider.dns.host_zones.create(name)
+            return self.provider.dns.host_zones.create(
+                name, "admin@cloudve.org")
 
         def cleanup_dns_zone(dns_zone):
             if dns_zone:
                 dns_zone.delete()
 
+        def test_zone_props(dns_zone):
+            self.assertEqual(dns_zone.admin_email, "admin@cloudve.org")
+
         sit.check_crud(self, self.provider.dns.host_zones, DnsZone,
                        "cb-crudzone", create_dns_zone, cleanup_dns_zone,
-                       skip_name_check=True)
+                       skip_name_check=True, extra_test_func=test_zone_props)
 
     @helpers.skipIfNoService(['dns.host_zones'])
     def test_create_dns_zones_not_fully_qualified(self):
@@ -35,7 +39,8 @@ class CloudDnsServiceTestCase(ProviderTestBase):
         with cb_helpers.cleanup_action(lambda: test_zone.delete()):
             # If zone name is not fully qualified, it should automatically be
             # handled
-            test_zone = self.provider.dns.host_zones.create(zone_name)
+            test_zone = self.provider.dns.host_zones.create(
+                zone_name, "admin@cloudve.org")
 
     @helpers.skipIfNoService(['dns.host_zones'])
     def test_crud_dns_record(self):
@@ -55,7 +60,8 @@ class CloudDnsServiceTestCase(ProviderTestBase):
                 dns_rec.delete()
 
         with cb_helpers.cleanup_action(lambda: test_zone.delete()):
-            test_zone = self.provider.dns.host_zones.create(zone_name)
+            test_zone = self.provider.dns.host_zones.create(
+                zone_name, "admin@cloudve.org")
             sit.check_crud(self, test_zone.records, DnsRecord,
                            "cb-dnsrec", create_dns_rec,
                            cleanup_dns_rec, skip_name_check=True)
@@ -66,7 +72,8 @@ class CloudDnsServiceTestCase(ProviderTestBase):
         zone_name = "cb-recprop-{0}.com.".format(helpers.get_uuid())
 
         with cb_helpers.cleanup_action(lambda: test_zone.delete()):
-            test_zone = self.provider.dns.host_zones.create(zone_name)
+            test_zone = self.provider.dns.host_zones.create(
+                zone_name, "admin@cloudve.org")
             test_rec = None
 
             with cb_helpers.cleanup_action(lambda: test_rec.delete()):
@@ -95,7 +102,8 @@ class CloudDnsServiceTestCase(ProviderTestBase):
         root_zone_name = "cb-recprop-{0}.com.".format(helpers.get_uuid())
 
         with cb_helpers.cleanup_action(lambda: test_zone.delete()):
-            test_zone = self.provider.dns.host_zones.create(root_zone_name)
+            test_zone = self.provider.dns.host_zones.create(
+                root_zone_name, "admin@cloudve.org")
             test_rec = None
 
             with cb_helpers.cleanup_action(lambda: test_rec.delete()):