Просмотр исходного кода

Added AWS implementation of dns service

Nuwan Goonasekera 6 лет назад
Родитель
Сommit
a7f412c954

+ 71 - 1
cloudbridge/base/resources.py

@@ -21,6 +21,8 @@ from cloudbridge.interfaces.resources import AttachmentInfo
 from cloudbridge.interfaces.resources import Bucket
 from cloudbridge.interfaces.resources import BucketObject
 from cloudbridge.interfaces.resources import CloudResource
+from cloudbridge.interfaces.resources import DnsRecord
+from cloudbridge.interfaces.resources import DnsZone
 from cloudbridge.interfaces.resources import FloatingIP
 from cloudbridge.interfaces.resources import FloatingIpState
 from cloudbridge.interfaces.resources import GatewayState
@@ -868,7 +870,6 @@ class BaseInternetGateway(BaseCloudResource, BaseObjectLifeCycleMixin,
 
     def __init__(self, provider):
         super(BaseInternetGateway, self).__init__(provider)
-        self.__provider = provider
 
     def __eq__(self, other):
         return (isinstance(other, InternetGateway) and
@@ -886,3 +887,72 @@ class BaseInternetGateway(BaseCloudResource, BaseObjectLifeCycleMixin,
     def delete(self):
         return self._provider.networking._gateways.delete(self.network_id,
                                                           self)
+
+
+class BaseDnsZone(BaseCloudResource, DnsZone):
+
+    CB_NAME_PATTERN = re.compile(
+        r"^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9]"
+        r"[a-z0-9-]{0,61}[a-z0-9]\.?$")
+
+    def __init__(self, provider):
+        super(BaseDnsZone, self).__init__(provider)
+
+    def __eq__(self, other):
+        return (isinstance(other, BaseDnsZone) and
+                # pylint:disable=protected-access
+                self._provider == other._provider and
+                self.id == other.id)
+
+    @staticmethod
+    def is_valid_resource_name(name):
+        if not name:
+            return False
+        else:
+            return (True if BaseDnsZone.CB_NAME_PATTERN.match(name)
+                    else False)
+
+    @staticmethod
+    def assert_valid_resource_name(name):
+        if not BaseDnsZone.is_valid_resource_name(name):
+            log.debug("InvalidLabelException raised on %s", name,
+                      exc_info=True)
+            raise InvalidLabelException(
+                u"Invalid object name: %s. Name must match criteria defined "
+                "in: https://stackoverflow.com/q/10306690/10971151" % name)
+
+    def delete(self):
+        return self._provider.dns.host_zones.delete(self.id)
+
+
+class BaseDnsRecord(BaseCloudResource, DnsRecord):
+
+    CB_NAME_PATTERN = re.compile(
+        r"^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9]"
+        r"[a-z0-9-]{0,61}[a-z0-9]$")
+
+    def __init__(self, provider):
+        super(BaseDnsRecord, self).__init__(provider)
+
+    def __eq__(self, other):
+        return (isinstance(other, BaseDnsRecord) and
+                # pylint:disable=protected-access
+                self._provider == other._provider and
+                self.id == other.id)
+
+    @staticmethod
+    def is_valid_resource_name(name):
+        if not name:
+            return False
+        else:
+            return (True if BaseDnsRecord.CB_NAME_PATTERN.match(name)
+                    else False)
+
+    @staticmethod
+    def assert_valid_resource_name(name):
+        if not BaseDnsRecord.is_valid_resource_name(name):
+            log.debug("InvalidLabelException raised on %s", name,
+                      exc_info=True)
+            raise InvalidLabelException(
+                u"Invalid object name: %s. Name must match criteria defined "
+                "in: https://stackoverflow.com/q/10306690/10971151" % name)

+ 23 - 10
cloudbridge/base/services.py

@@ -9,6 +9,9 @@ from cloudbridge.interfaces.services import BucketObjectService
 from cloudbridge.interfaces.services import BucketService
 from cloudbridge.interfaces.services import CloudService
 from cloudbridge.interfaces.services import ComputeService
+from cloudbridge.interfaces.services import DnsRecordService
+from cloudbridge.interfaces.services import DnsService
+from cloudbridge.interfaces.services import DnsZoneService
 from cloudbridge.interfaces.services import FloatingIPService
 from cloudbridge.interfaces.services import GatewayService
 from cloudbridge.interfaces.services import ImageService
@@ -343,22 +346,12 @@ class BaseGatewayService(GatewayService, BaseCloudService):
 
     def __init__(self, provider):
         super(BaseGatewayService, self).__init__(provider)
-        self._provider = provider
-
-    @property
-    def provider(self):
-        return self._provider
 
 
 class BaseFloatingIPService(FloatingIPService, BaseCloudService):
 
     def __init__(self, provider):
         super(BaseFloatingIPService, self).__init__(provider)
-        self._provider = provider
-
-    @property
-    def provider(self):
-        return self._provider
 
     @dispatch(event="provider.networking.floating_ips.find",
               priority=BaseCloudService.STANDARD_EVENT_PRIORITY)
@@ -367,3 +360,23 @@ class BaseFloatingIPService(FloatingIPService, BaseCloudService):
         filters = ['name', 'public_ip']
         matches = cb_helpers.generic_find(filters, kwargs, obj_list)
         return ClientPagedResultList(self._provider, list(matches))
+
+
+class BaseDnsService(DnsService, BaseCloudService):
+
+    def __init__(self, provider):
+        super(BaseDnsService, self).__init__(provider)
+
+
+class BaseDnsZoneService(BasePageableObjectMixin, DnsZoneService,
+                         BaseCloudService):
+
+    def __init__(self, provider):
+        super(BaseDnsZoneService, self).__init__(provider)
+
+
+class BaseDnsRecordService(BasePageableObjectMixin, DnsRecordService,
+                           BaseCloudService):
+
+    def __init__(self, provider):
+        super(BaseDnsRecordService, self).__init__(provider)

+ 34 - 0
cloudbridge/base/subservices.py

@@ -1,6 +1,7 @@
 import logging
 
 from cloudbridge.interfaces.subservices import BucketObjectSubService
+from cloudbridge.interfaces.subservices import DnsRecordSubService
 from cloudbridge.interfaces.subservices import FloatingIPSubService
 from cloudbridge.interfaces.subservices import GatewaySubService
 from cloudbridge.interfaces.subservices import SubnetSubService
@@ -166,3 +167,36 @@ class BaseSubnetSubService(SubnetSubService, BasePageableObjectMixin):
 
     def delete(self, subnet):
         return self._provider.networking.subnets.delete(subnet)
+
+
+class BaseDnsRecordSubService(DnsRecordSubService, BasePageableObjectMixin):
+
+    def __init__(self, provider, dns_zone):
+        self.__provider = provider
+        self.dns_zone = dns_zone
+
+    @property
+    def _provider(self):
+        return self.__provider
+
+    def get(self, rec_id):
+        # pylint:disable=protected-access
+        return self._provider.dns._records.get(self.dns_zone, rec_id)
+
+    def list(self, limit=None, marker=None):
+        # pylint:disable=protected-access
+        return self._provider.dns._records.list(
+            dns_zone=self.dns_zone, limit=limit, marker=marker)
+
+    def find(self, **kwargs):
+        # pylint:disable=protected-access
+        return self._provider.dns._records.find(
+            dns_zone=self.dns_zone, **kwargs)
+
+    def create(self, name, type, data, ttl=None):
+        # pylint:disable=protected-access
+        return self._provider.dns._records.create(
+            self.dns_zone, name, type, data, ttl)
+
+    def delete(self, rec):
+        return self._provider.dns._records.delete(self.dns_zone, rec)

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

@@ -7,6 +7,7 @@ from cloudbridge.base import BaseCloudProvider
 from cloudbridge.base.helpers import get_env
 
 from .services import AWSComputeService
+from .services import AWSDnsService
 from .services import AWSNetworkingService
 from .services import AWSSecurityService
 from .services import AWSStorageService
@@ -55,6 +56,7 @@ class AWSCloudProvider(BaseCloudProvider):
         self._networking = AWSNetworkingService(self)
         self._security = AWSSecurityService(self)
         self._storage = AWSStorageService(self)
+        self._dns = AWSDnsService(self)
 
     @property
     def session(self):
@@ -94,6 +96,10 @@ class AWSCloudProvider(BaseCloudProvider):
     def storage(self):
         return self._storage
 
+    @property
+    def dns(self):
+        return self._dns
+
     def _connect_ec2(self):
         """
         Get a boto ec2 connection object.

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

@@ -10,6 +10,8 @@ from botocore.exceptions import ClientError
 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
@@ -38,6 +40,7 @@ from cloudbridge.interfaces.resources import VolumeState
 from .helpers import find_tag_value
 from .helpers import trim_empty_params
 from .subservices import AWSBucketObjectSubService
+from .subservices import AWSDnsRecordSubService
 from .subservices import AWSFloatingIPSubService
 from .subservices import AWSGatewaySubService
 from .subservices import AWSSubnetSubService
@@ -1138,3 +1141,60 @@ class AWSLaunchConfig(BaseLaunchConfig):
 
     def __init__(self, provider):
         super(AWSLaunchConfig, self).__init__(provider)
+
+
+class AWSDnsZone(BaseDnsZone):
+
+    def __init__(self, provider, dns_zone):
+        super(AWSDnsZone, self).__init__(provider)
+        self._dns_zone = dns_zone
+        self._dns_record_container = AWSDnsRecordSubService(provider, self)
+
+    @property
+    def id(self):
+        return self._dns_zone.get('Id')
+
+    @property
+    def name(self):
+        return self._dns_zone.get('Name')
+
+    @property
+    def records(self):
+        return self._dns_record_container
+
+
+class AWSDnsRecord(BaseDnsRecord):
+
+    def __init__(self, provider, dns_zone, dns_record):
+        super(AWSDnsRecord, self).__init__(provider)
+        self._dns_zone = dns_zone
+        self._dns_rec = dns_record
+
+    @property
+    def id(self):
+        return self._dns_rec.get('Name') + ":" + self._dns_rec.get('Type')
+
+    @property
+    def name(self):
+        return self._dns_rec.get('Name')
+
+    @property
+    def zone_id(self):
+        return self._dns_zone.id
+
+    @property
+    def type(self):
+        return self._dns_rec.get('Type')
+
+    @property
+    def data(self):
+        # We support only one value per record type
+        return self._dns_rec.get('ResourceRecords')[0].get('Value')
+
+    @property
+    def ttl(self):
+        return self._dns_rec.get('TTL')
+
+    def delete(self):
+        # pylint:disable=protected-access
+        return self._provider.dns._records.delete(self._dns_zone, self)

+ 175 - 0
cloudbridge/providers/aws/services.py

@@ -2,6 +2,7 @@
 import ipaddress
 import logging
 import string
+import uuid
 
 from botocore.exceptions import ClientError
 
@@ -12,9 +13,13 @@ import requests
 import cloudbridge.base.helpers as cb_helpers
 from cloudbridge.base.middleware import dispatch
 from cloudbridge.base.resources import ClientPagedResultList
+from cloudbridge.base.resources import ServerPagedResultList
 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
@@ -51,6 +56,8 @@ from .helpers import BotoS3Service
 from .helpers import trim_empty_params
 from .resources import AWSBucket
 from .resources import AWSBucketObject
+from .resources import AWSDnsRecord
+from .resources import AWSDnsZone
 from .resources import AWSFloatingIP
 from .resources import AWSInstance
 from .resources import AWSInternetGateway
@@ -1300,3 +1307,171 @@ class AWSFloatingIPService(BaseFloatingIPService):
         else:
             aws_fip = self.svc.get_raw(fip)
         aws_fip.release()
+
+
+class AWSDnsService(BaseDnsService):
+
+    def __init__(self, provider):
+        super(AWSDnsService, self).__init__(provider)
+        self.client = self._provider.session.client(
+            'route53', region_name=self._provider.region_name)
+
+        # Initialize provider services
+        self._zone_svc = AWSDnsZoneService(self.provider)
+        self._record_svc = AWSDnsRecordService(self.provider)
+
+    @property
+    def host_zones(self):
+        return self._zone_svc
+
+    @property
+    def _records(self):
+        return self._record_svc
+
+
+class AWSDnsZoneService(BaseDnsZoneService):
+
+    def __init__(self, provider):
+        super(AWSDnsZoneService, self).__init__(provider)
+
+    @dispatch(event="provider.dns.host_zones.get",
+              priority=BaseNetworkService.STANDARD_EVENT_PRIORITY)
+    def get(self, dns_zone_id):
+        try:
+            dns_zone = self.provider.dns.client.get_hosted_zone(Id=dns_zone_id)
+            return AWSDnsZone(self.provider, dns_zone.get('HostedZone'))
+        except self.provider.dns.client.exceptions.NoSuchHostedZone:
+            return None
+
+    @dispatch(event="provider.dns.host_zones.list",
+              priority=BaseNetworkService.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}))
+        cb_objs = [AWSDnsZone(self.provider, zone)
+                   for zone in response.get('HostedZones')]
+        return ServerPagedResultList(is_truncated=response.get('IsTruncated'),
+                                     marker=response.get('NextMarker'),
+                                     supports_total=False,
+                                     data=cb_objs)
+
+    @dispatch(event="provider.dns.host_zones.find",
+              priority=BaseNetworkService.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=BaseNetworkService.STANDARD_EVENT_PRIORITY)
+    def create(self, name):
+        AWSDnsZone.assert_valid_resource_name(name)
+
+        response = self.provider.dns.client.create_hosted_zone(
+            Name=name, CallerReference=uuid.uuid4().hex)
+        return AWSDnsZone(self.provider, response.get('HostedZone'))
+
+    @dispatch(event="provider.dns.host_zones.delete",
+              priority=BaseNetworkService.STANDARD_EVENT_PRIORITY)
+    def delete(self, dns_zone):
+        dns_zone = (dns_zone if isinstance(dns_zone, AWSDnsZone)
+                    else self.get(dns_zone))
+        if dns_zone:
+            self.provider.dns.client.delete_hosted_zone(Id=dns_zone.id)
+
+
+class AWSDnsRecordService(BaseDnsRecordService):
+
+    def __init__(self, provider):
+        super(AWSDnsRecordService, self).__init__(provider)
+
+    def get(self, dns_zone, rec_id):
+        try:
+            if ":" in rec_id:
+                rec_name, rec_type = rec_id.split(":")
+                response = self.provider.dns.client.list_resource_record_sets(
+                    HostedZoneId=dns_zone.id,
+                    StartRecordName=rec_name,
+                    StartRecordType=rec_type,
+                    MaxItems='1')
+                return AWSDnsRecord(self.provider, dns_zone,
+                                    response.get('ResourceRecordSets')[0])
+            else:
+                return None
+        except ClientError as exc:
+            error_code = exc.response['Error']['Code']
+            if any(status in error_code for status in
+                   ('NotFound', 'InvalidParameterValue', 'Malformed', '404')):
+                log.debug("Object not found: %s", rec_id)
+                return None
+            else:
+                raise exc
+
+    def list(self, dns_zone, limit=None, marker=None):
+        response = self.provider.dns.client.list_resource_record_sets(
+            **trim_empty_params({
+                'HostedZoneId': dns_zone.id,
+                'MaxItems': limit,
+                'StartRecordIdentifier': marker
+            })
+        )
+        cb_objs = [AWSDnsRecord(self.provider, dns_zone, rec)
+                   for rec in response.get('ResourceRecordSets')]
+        return ServerPagedResultList(
+            is_truncated=response.get('IsTruncated'),
+            marker=response.get('NextRecordIdentifier'),
+            supports_total=False, data=cb_objs)
+
+    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):
+        AWSDnsRecord.assert_valid_resource_name(name)
+
+        response = self.provider.dns.client.change_resource_record_sets(
+            HostedZoneId=dns_zone.id,
+            ChangeBatch={
+                'Changes': [{
+                    'Action': 'CREATE',
+                    'ResourceRecordSet': trim_empty_params({
+                        'Name': name,
+                        'Type': type,
+                        'TTL': ttl or 300,
+                        'ResourceRecords': [{
+                            'Value': data
+                        }]
+                    })
+                }]
+            }
+        )
+        waiter = self.provider.dns.client.get_waiter(
+            'resource_record_sets_changed')
+        waiter.wait(Id=response.get('ChangeInfo').get('Id'))
+        return self.get(dns_zone, name + ":" + type)
+
+    def delete(self, dns_zone, record):
+        rec_id = record.id if isinstance(record, AWSDnsRecord) else record
+
+        rec_name, rec_type = rec_id.split(":")
+        response = self.provider.dns.client.change_resource_record_sets(
+            HostedZoneId=dns_zone.id,
+            ChangeBatch={
+                'Changes': [{
+                    'Action': 'DELETE',
+                    'ResourceRecordSet': {
+                        'Name': rec_name,
+                        'Type': rec_type,
+                        'TTL': record.ttl,
+                        'ResourceRecords': [{
+                            'Value': record.data
+                        }]
+                    }
+                }]
+            })
+        waiter = self.provider.dns.client.get_waiter(
+            'resource_record_sets_changed')
+        waiter.wait(Id=response.get('ChangeInfo').get('Id'))

+ 7 - 0
cloudbridge/providers/aws/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
@@ -37,3 +38,9 @@ class AWSSubnetSubService(BaseSubnetSubService):
 
     def __init__(self, provider, network):
         super(AWSSubnetSubService, self).__init__(provider, network)
+
+
+class AWSDnsRecordSubService(BaseDnsRecordSubService):
+
+    def __init__(self, provider, dns_zone):
+        super(AWSDnsRecordSubService, self).__init__(provider, dns_zone)