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

Added GCP implementation of dns service

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

+ 16 - 0
cloudbridge/base/helpers.py

@@ -165,3 +165,19 @@ def rename_kwargs(func_name, kwargs, aliases):
                        details='{} is deprecated, use {} instead'.format(
                            alias, new))(lambda: None)()
             kwargs[new] = kwargs.pop(alias)
+
+
+NON_ALPHA_NUM = re.compile(r"[^A-Za-z0-9]+")
+
+
+def to_resource_name(value, replace_with="-"):
+    """
+    Converts a given string to a valid resource name by stripping
+    all characters that are not alphanumeric.
+
+    :param value: the value to strip
+    :param replace_with: the value to replace mismatching characters with
+    :return: a string with all mismatching characters removed.
+    """
+    val = re.sub(NON_ALPHA_NUM, replace_with, value)
+    return val.strip("-")

+ 1 - 18
cloudbridge/interfaces/resources.py

@@ -1334,40 +1334,23 @@ class DnsZone(CloudResource):
         pass
 
 
-class RecordType(object):
+class DnsRecordType(object):
     """
     DNS record types.
     """
     A = 'A'
     AAAA = 'AAAA'
-    AFSDB = 'A'
-    ALIAS = 'ALIAS'
-    CERT = 'CERT'
     CNAME = 'CNAME'
-    DNAME = 'DNAME'
-    DNSKEY = 'DNSKEY'
-    DS = 'DS'
-    GEO = 'GEO'
-    HINFO = 'HINFO'
-    KEY = 'KEY'
-    LOC = 'LOC'
     MX = 'MX'
     NAPTR = 'NAPTR'
     NS = 'NS'
-    NSEC = 'NSEC'
-    OPENPGPKEY = 'OPENPGPKEY'
     PTR = 'PTR'
-    REDIRECT = 'REDIRECT'
-    RP = 'RP'
-    RRSIG = 'RRSIG'
     SOA = 'SOA'
     SPF = 'SPF'
     SRV = 'SRV'
     SSHFP = 'SSHFP'
     TLSA = 'TLSA'
     TXT = 'TXT'
-    URL = 'URL'
-    WKS = 'WKS'
 
 
 class DnsRecord(CloudResource):

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

@@ -1388,7 +1388,7 @@ class AWSDnsRecordService(BaseDnsRecordService):
 
     def get(self, dns_zone, rec_id):
         try:
-            if ":" in rec_id:
+            if rec_id and ":" in rec_id:
                 rec_name, rec_type = rec_id.split(":")
                 response = self.provider.dns.client.list_resource_record_sets(
                     HostedZoneId=dns_zone.id,

+ 28 - 0
cloudbridge/providers/gcp/provider.py

@@ -19,6 +19,7 @@ from cloudbridge.base import BaseCloudProvider
 from cloudbridge.interfaces.exceptions import ProviderConnectionException
 
 from .services import GCPComputeService
+from .services import GCPDnsService
 from .services import GCPNetworkingService
 from .services import GCPSecurityService
 from .services import GCPStorageService
@@ -236,14 +237,17 @@ class GCPCloudProvider(BaseCloudProvider):
         # service connections, lazily initialized
         self._gcp_compute = None
         self._gcp_storage = None
+        self._gcp_dns = None
         self._compute_resources_cache = None
         self._storage_resources_cache = None
+        self._dns_resources_cache = None
 
         # Initialize provider services
         self._compute = GCPComputeService(self)
         self._security = GCPSecurityService(self)
         self._networking = GCPNetworkingService(self)
         self._storage = GCPStorageService(self)
+        self._dns = GCPDnsService(self)
 
     # Override base class implementation because it will cause
     # an infinite loop
@@ -267,6 +271,10 @@ class GCPCloudProvider(BaseCloudProvider):
     def storage(self):
         return self._storage
 
+    @property
+    def dns(self):
+        return self._dns
+
     @property
     def gcp_compute(self):
         if not self._gcp_compute:
@@ -279,6 +287,12 @@ class GCPCloudProvider(BaseCloudProvider):
             self._gcp_storage = self._connect_gcp_storage()
         return self._gcp_storage
 
+    @property
+    def gcp_dns(self):
+        if not self._gcp_dns:
+            self._gcp_dns = self._connect_gcp_dns()
+        return self._gcp_dns
+
     @property
     def _compute_resources(self):
         if not self._compute_resources_cache:
@@ -295,6 +309,14 @@ class GCPCloudProvider(BaseCloudProvider):
             self._storage_resources_cache = GCPResources(self.gcp_storage)
         return self._storage_resources_cache
 
+    @property
+    def _dns_resources(self):
+        if not self._dns_resources_cache:
+            self._dns_resources_cache = GCPResources(
+                self.gcp_dns,
+                project=self.project_name)
+        return self._dns_resources_cache
+
     @property
     def _credentials(self):
         if not self.credentials_obj:
@@ -322,6 +344,10 @@ class GCPCloudProvider(BaseCloudProvider):
         return discovery.build('compute', 'v1', credentials=self._credentials,
                                cache_discovery=False)
 
+    def _connect_gcp_dns(self):
+        return discovery.build('dns', 'v1', credentials=self._credentials,
+                               cache_discovery=False)
+
     def wait_for_operation(self, operation, region=None, zone=None):
         args = {'project': self.project_name, 'operation': operation['name']}
         if not region and not zone:
@@ -353,6 +379,8 @@ class GCPCloudProvider(BaseCloudProvider):
             self._compute_resources.get_resource_url_with_default(
                 resource, url_or_name, **kwargs) or
             self._storage_resources.get_resource_url_with_default(
+                resource, url_or_name, **kwargs) or
+            self._dns_resources.get_resource_url_with_default(
                 resource, url_or_name, **kwargs))
         if resource_url is None:
             return None

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

@@ -18,6 +18,8 @@ import googleapiclient
 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
@@ -46,6 +48,7 @@ from cloudbridge.interfaces.resources import VolumeState
 
 from . import helpers
 from .subservices import GCPBucketObjectSubService
+from .subservices import GCPDnsRecordSubService
 from .subservices import GCPFloatingIPSubService
 from .subservices import GCPGatewaySubService
 from .subservices import GCPSubnetSubService
@@ -2024,3 +2027,60 @@ class GCPLaunchConfig(BaseLaunchConfig):
 
     def __init__(self, provider):
         super(GCPLaunchConfig, self).__init__(provider)
+
+
+class GCPDnsZone(BaseDnsZone):
+
+    def __init__(self, provider, dns_zone):
+        super(GCPDnsZone, self).__init__(provider)
+        self._dns_zone = dns_zone
+        self._dns_record_container = GCPDnsRecordSubService(provider, self)
+
+    @property
+    def id(self):
+        return self._dns_zone.get('name')
+
+    @property
+    def name(self):
+        return self._dns_zone.get('dnsName')
+
+    @property
+    def records(self):
+        return self._dns_record_container
+
+
+class GCPDnsRecord(BaseDnsRecord):
+
+    def __init__(self, provider, dns_zone, dns_record):
+        super(GCPDnsRecord, 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('rrdatas')[0]
+
+    @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)

+ 222 - 0
cloudbridge/providers/gcp/services.py

@@ -14,6 +14,9 @@ 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
@@ -33,12 +36,15 @@ from cloudbridge.base.services import BaseVMTypeService
 from cloudbridge.base.services import BaseVolumeService
 from cloudbridge.interfaces.exceptions import DuplicateResourceException
 from cloudbridge.interfaces.exceptions import InvalidParamException
+from cloudbridge.interfaces.resources import DnsRecordType
 from cloudbridge.interfaces.resources import TrafficDirection
 from cloudbridge.interfaces.resources import VMFirewall
 from cloudbridge.providers.gcp import helpers
 
 from .resources import GCPBucket
 from .resources import GCPBucketObject
+from .resources import GCPDnsRecord
+from .resources import GCPDnsZone
 from .resources import GCPFirewallsDelegate
 from .resources import GCPFloatingIP
 from .resources import GCPInstance
@@ -1601,3 +1607,219 @@ class GCPFloatingIPService(BaseFloatingIPService):
                     .execute())
         self.provider.wait_for_operation(response,
                                          region=fip.region_name)
+
+
+class GCPDnsService(BaseDnsService):
+
+    def __init__(self, provider):
+        super(GCPDnsService, self).__init__(provider)
+
+        # Initialize provider services
+        self._zone_svc = GCPDnsZoneService(self.provider)
+        self._record_svc = GCPDnsRecordService(self.provider)
+
+    @property
+    def host_zones(self):
+        return self._zone_svc
+
+    @property
+    def _records(self):
+        return self._record_svc
+
+
+class GCPDnsZoneService(BaseDnsZoneService):
+
+    def __init__(self, provider):
+        super(GCPDnsZoneService, self).__init__(provider)
+
+    @dispatch(event="provider.dns.host_zones.get",
+              priority=BaseNetworkService.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)
+    def list(self, limit=None, marker=None):
+        max_result = limit if limit is not None and limit < 500 else 500
+        response = (self.provider
+                        .gcp_dns
+                        .managedZones()
+                        .list(project=self.provider.project_name,
+                              maxResults=max_result,
+                              pageToken=marker)
+                        .execute())
+        dns_zones = []
+        for dns_zone in response.get('managedZones', []):
+            dns_zones.append(GCPDnsZone(self.provider, dns_zone))
+        if len(dns_zones) > max_result:
+            log.warning('Expected at most %d results; got %d',
+                        max_result, len(dns_zones))
+        return ServerPagedResultList('nextPageToken' in response,
+                                     response.get('nextPageToken'),
+                                     False, data=dns_zones)
+
+    @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):
+        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,
+            'description': name,
+            'visibility': 'public'
+        }
+        try:
+            response = (self.provider
+                            .gcp_dns
+                            .managedZones()
+                            .create(project=self.provider.project_name,
+                                    body=body)
+                            .execute())
+            return GCPDnsZone(self.provider, response)
+        except googleapiclient.errors.HttpError as http_error:
+            # 409 = conflict
+            if http_error.resp.status in [409]:
+                raise DuplicateResourceException(
+                    'DNS Zone already exists with name {0}'.format(name))
+            else:
+                raise
+
+    @dispatch(event="provider.dns.host_zones.delete",
+              priority=BaseNetworkService.STANDARD_EVENT_PRIORITY)
+    def delete(self, dns_zone):
+        zone = (dns_zone if isinstance(dns_zone, GCPDnsZone)
+                else self.get(dns_zone))
+        if zone:
+            (self.provider
+                 .gcp_dns
+                 .managedZones()
+                 .delete(project=self.provider.project_name,
+                         managedZone=zone.id)
+                 .execute())
+
+
+class GCPDnsRecordService(BaseDnsRecordService):
+
+    def __init__(self, provider):
+        super(GCPDnsRecordService, 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
+
+    def _standardize_record(self, value, type):
+        """
+        Converts a record to what GCP expects. For example, GCP
+        expects a fully qualified name for all CNAME records.
+        """
+        return (self._get_fully_qualified_dns(value)
+                if type == DnsRecordType.CNAME else value)
+
+    def get(self, dns_zone, rec_id):
+        if rec_id and ":" in rec_id:
+            rec_name, rec_type = rec_id.split(":")
+            response = (self.provider
+                        .gcp_dns
+                        .resourceRecordSets()
+                        .list(project=self.provider.project_name,
+                              managedZone=dns_zone.id,
+                              name=self._get_fully_qualified_dns(rec_name),
+                              type=rec_type)
+                        .execute())
+            if len(response.get('rrsets', [])) > 1:
+                log.warning('Expected at most %d results; got %d',
+                            1, len(response.get('items', [])))
+            for rec in response.get('rrsets', []):
+                return GCPDnsRecord(self.provider, dns_zone, rec)
+            return None
+        else:
+            return None
+
+    def list(self, dns_zone, limit=None, marker=None, rec_id=None):
+        max_result = limit if limit is not None and limit < 500 else 500
+        response = (self.provider
+                        .gcp_dns
+                        .resourceRecordSets()
+                        .list(project=self.provider.project_name,
+                              managedZone=dns_zone.id,
+                              maxResults=max_result,
+                              pageToken=marker)
+                        .execute())
+        records = []
+        for rec in response.get('rrsets', []):
+            records.append(GCPDnsRecord(self.provider, dns_zone, rec))
+        if len(records) > max_result:
+            log.warning('Expected at most %d results; got %d',
+                        max_result, len(records))
+        return ServerPagedResultList('nextPageToken' in response,
+                                     response.get('nextPageToken'),
+                                     False, data=records)
+
+    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):
+        GCPDnsZone.assert_valid_resource_name(name)
+        body = {
+            'kind': 'dns#change',
+            "additions": [
+                {
+                    'kind': 'dns#resourceRecordSet',
+                    'name': self._get_fully_qualified_dns(name),
+                    'type': type,
+                    'ttl': ttl,
+                    'rrdatas': [
+                        self._standardize_record(data, type)
+                    ]
+                }
+            ]
+        }
+        (self.provider
+             .gcp_dns
+             .changes()
+             .create(project=self.provider.project_name,
+                     managedZone=dns_zone.id,
+                     body=body)
+             .execute())
+        rec_id = name + ":" + type
+        return self.get(dns_zone, rec_id)
+
+    def delete(self, dns_zone, record):
+        rec = record if isinstance(record, GCPDnsRecord) else self.get(record)
+
+        if rec:
+            body = {
+                'kind': 'dns#change',
+                "deletions": [
+                    {
+                        'kind': 'dns#resourceRecordSet',
+                        'name': self._get_fully_qualified_dns(rec.name),
+                        'type': rec.type,
+                        'ttl': rec.ttl,
+                        'rrdatas': [
+                            self._standardize_record(rec.data, rec.type)
+                        ]
+                    }
+                ]
+            }
+            (self.provider
+                 .gcp_dns
+                 .changes()
+                 .create(project=self.provider.project_name,
+                         managedZone=dns_zone.id,
+                         body=body)
+                 .execute())

+ 7 - 0
cloudbridge/providers/gcp/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 GCPSubnetSubService(BaseSubnetSubService):
 
     def __init__(self, provider, network):
         super(GCPSubnetSubService, self).__init__(provider, network)
+
+
+class GCPDnsRecordSubService(BaseDnsRecordSubService):
+
+    def __init__(self, provider, dns_zone):
+        super(GCPDnsRecordSubService, self).__init__(provider, dns_zone)