Преглед изворни кода

Added GCP implementation of dns service

Nuwan Goonasekera пре 6 година
родитељ
комит
bb92d6b4b8

+ 16 - 0
cloudbridge/base/helpers.py

@@ -165,3 +165,19 @@ def rename_kwargs(func_name, kwargs, aliases):
                        details='{} is deprecated, use {} instead'.format(
                        details='{} is deprecated, use {} instead'.format(
                            alias, new))(lambda: None)()
                            alias, new))(lambda: None)()
             kwargs[new] = kwargs.pop(alias)
             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
         pass
 
 
 
 
-class RecordType(object):
+class DnsRecordType(object):
     """
     """
     DNS record types.
     DNS record types.
     """
     """
     A = 'A'
     A = 'A'
     AAAA = 'AAAA'
     AAAA = 'AAAA'
-    AFSDB = 'A'
-    ALIAS = 'ALIAS'
-    CERT = 'CERT'
     CNAME = 'CNAME'
     CNAME = 'CNAME'
-    DNAME = 'DNAME'
-    DNSKEY = 'DNSKEY'
-    DS = 'DS'
-    GEO = 'GEO'
-    HINFO = 'HINFO'
-    KEY = 'KEY'
-    LOC = 'LOC'
     MX = 'MX'
     MX = 'MX'
     NAPTR = 'NAPTR'
     NAPTR = 'NAPTR'
     NS = 'NS'
     NS = 'NS'
-    NSEC = 'NSEC'
-    OPENPGPKEY = 'OPENPGPKEY'
     PTR = 'PTR'
     PTR = 'PTR'
-    REDIRECT = 'REDIRECT'
-    RP = 'RP'
-    RRSIG = 'RRSIG'
     SOA = 'SOA'
     SOA = 'SOA'
     SPF = 'SPF'
     SPF = 'SPF'
     SRV = 'SRV'
     SRV = 'SRV'
     SSHFP = 'SSHFP'
     SSHFP = 'SSHFP'
     TLSA = 'TLSA'
     TLSA = 'TLSA'
     TXT = 'TXT'
     TXT = 'TXT'
-    URL = 'URL'
-    WKS = 'WKS'
 
 
 
 
 class DnsRecord(CloudResource):
 class DnsRecord(CloudResource):

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

@@ -1388,7 +1388,7 @@ class AWSDnsRecordService(BaseDnsRecordService):
 
 
     def get(self, dns_zone, rec_id):
     def get(self, dns_zone, rec_id):
         try:
         try:
-            if ":" in rec_id:
+            if rec_id and ":" in rec_id:
                 rec_name, rec_type = rec_id.split(":")
                 rec_name, rec_type = rec_id.split(":")
                 response = self.provider.dns.client.list_resource_record_sets(
                 response = self.provider.dns.client.list_resource_record_sets(
                     HostedZoneId=dns_zone.id,
                     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 cloudbridge.interfaces.exceptions import ProviderConnectionException
 
 
 from .services import GCPComputeService
 from .services import GCPComputeService
+from .services import GCPDnsService
 from .services import GCPNetworkingService
 from .services import GCPNetworkingService
 from .services import GCPSecurityService
 from .services import GCPSecurityService
 from .services import GCPStorageService
 from .services import GCPStorageService
@@ -236,14 +237,17 @@ class GCPCloudProvider(BaseCloudProvider):
         # service connections, lazily initialized
         # service connections, lazily initialized
         self._gcp_compute = None
         self._gcp_compute = None
         self._gcp_storage = None
         self._gcp_storage = None
+        self._gcp_dns = None
         self._compute_resources_cache = None
         self._compute_resources_cache = None
         self._storage_resources_cache = None
         self._storage_resources_cache = None
+        self._dns_resources_cache = None
 
 
         # Initialize provider services
         # Initialize provider services
         self._compute = GCPComputeService(self)
         self._compute = GCPComputeService(self)
         self._security = GCPSecurityService(self)
         self._security = GCPSecurityService(self)
         self._networking = GCPNetworkingService(self)
         self._networking = GCPNetworkingService(self)
         self._storage = GCPStorageService(self)
         self._storage = GCPStorageService(self)
+        self._dns = GCPDnsService(self)
 
 
     # Override base class implementation because it will cause
     # Override base class implementation because it will cause
     # an infinite loop
     # an infinite loop
@@ -267,6 +271,10 @@ class GCPCloudProvider(BaseCloudProvider):
     def storage(self):
     def storage(self):
         return self._storage
         return self._storage
 
 
+    @property
+    def dns(self):
+        return self._dns
+
     @property
     @property
     def gcp_compute(self):
     def gcp_compute(self):
         if not self._gcp_compute:
         if not self._gcp_compute:
@@ -279,6 +287,12 @@ class GCPCloudProvider(BaseCloudProvider):
             self._gcp_storage = self._connect_gcp_storage()
             self._gcp_storage = self._connect_gcp_storage()
         return self._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
     @property
     def _compute_resources(self):
     def _compute_resources(self):
         if not self._compute_resources_cache:
         if not self._compute_resources_cache:
@@ -295,6 +309,14 @@ class GCPCloudProvider(BaseCloudProvider):
             self._storage_resources_cache = GCPResources(self.gcp_storage)
             self._storage_resources_cache = GCPResources(self.gcp_storage)
         return self._storage_resources_cache
         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
     @property
     def _credentials(self):
     def _credentials(self):
         if not self.credentials_obj:
         if not self.credentials_obj:
@@ -322,6 +344,10 @@ class GCPCloudProvider(BaseCloudProvider):
         return discovery.build('compute', 'v1', credentials=self._credentials,
         return discovery.build('compute', 'v1', credentials=self._credentials,
                                cache_discovery=False)
                                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):
     def wait_for_operation(self, operation, region=None, zone=None):
         args = {'project': self.project_name, 'operation': operation['name']}
         args = {'project': self.project_name, 'operation': operation['name']}
         if not region and not zone:
         if not region and not zone:
@@ -353,6 +379,8 @@ class GCPCloudProvider(BaseCloudProvider):
             self._compute_resources.get_resource_url_with_default(
             self._compute_resources.get_resource_url_with_default(
                 resource, url_or_name, **kwargs) or
                 resource, url_or_name, **kwargs) or
             self._storage_resources.get_resource_url_with_default(
             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))
                 resource, url_or_name, **kwargs))
         if resource_url is None:
         if resource_url is None:
             return 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 BaseAttachmentInfo
 from cloudbridge.base.resources import BaseBucket
 from cloudbridge.base.resources import BaseBucket
 from cloudbridge.base.resources import BaseBucketObject
 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 BaseFloatingIP
 from cloudbridge.base.resources import BaseInstance
 from cloudbridge.base.resources import BaseInstance
 from cloudbridge.base.resources import BaseInternetGateway
 from cloudbridge.base.resources import BaseInternetGateway
@@ -46,6 +48,7 @@ from cloudbridge.interfaces.resources import VolumeState
 
 
 from . import helpers
 from . import helpers
 from .subservices import GCPBucketObjectSubService
 from .subservices import GCPBucketObjectSubService
+from .subservices import GCPDnsRecordSubService
 from .subservices import GCPFloatingIPSubService
 from .subservices import GCPFloatingIPSubService
 from .subservices import GCPGatewaySubService
 from .subservices import GCPGatewaySubService
 from .subservices import GCPSubnetSubService
 from .subservices import GCPSubnetSubService
@@ -2024,3 +2027,60 @@ class GCPLaunchConfig(BaseLaunchConfig):
 
 
     def __init__(self, provider):
     def __init__(self, provider):
         super(GCPLaunchConfig, self).__init__(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 BaseBucketObjectService
 from cloudbridge.base.services import BaseBucketService
 from cloudbridge.base.services import BaseBucketService
 from cloudbridge.base.services import BaseComputeService
 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 BaseFloatingIPService
 from cloudbridge.base.services import BaseGatewayService
 from cloudbridge.base.services import BaseGatewayService
 from cloudbridge.base.services import BaseImageService
 from cloudbridge.base.services import BaseImageService
@@ -33,12 +36,15 @@ from cloudbridge.base.services import BaseVMTypeService
 from cloudbridge.base.services import BaseVolumeService
 from cloudbridge.base.services import BaseVolumeService
 from cloudbridge.interfaces.exceptions import DuplicateResourceException
 from cloudbridge.interfaces.exceptions import DuplicateResourceException
 from cloudbridge.interfaces.exceptions import InvalidParamException
 from cloudbridge.interfaces.exceptions import InvalidParamException
+from cloudbridge.interfaces.resources import DnsRecordType
 from cloudbridge.interfaces.resources import TrafficDirection
 from cloudbridge.interfaces.resources import TrafficDirection
 from cloudbridge.interfaces.resources import VMFirewall
 from cloudbridge.interfaces.resources import VMFirewall
 from cloudbridge.providers.gcp import helpers
 from cloudbridge.providers.gcp import helpers
 
 
 from .resources import GCPBucket
 from .resources import GCPBucket
 from .resources import GCPBucketObject
 from .resources import GCPBucketObject
+from .resources import GCPDnsRecord
+from .resources import GCPDnsZone
 from .resources import GCPFirewallsDelegate
 from .resources import GCPFirewallsDelegate
 from .resources import GCPFloatingIP
 from .resources import GCPFloatingIP
 from .resources import GCPInstance
 from .resources import GCPInstance
@@ -1601,3 +1607,219 @@ class GCPFloatingIPService(BaseFloatingIPService):
                     .execute())
                     .execute())
         self.provider.wait_for_operation(response,
         self.provider.wait_for_operation(response,
                                          region=fip.region_name)
                                          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
 import logging
 
 
 from cloudbridge.base.subservices import BaseBucketObjectSubService
 from cloudbridge.base.subservices import BaseBucketObjectSubService
+from cloudbridge.base.subservices import BaseDnsRecordSubService
 from cloudbridge.base.subservices import BaseFloatingIPSubService
 from cloudbridge.base.subservices import BaseFloatingIPSubService
 from cloudbridge.base.subservices import BaseGatewaySubService
 from cloudbridge.base.subservices import BaseGatewaySubService
 from cloudbridge.base.subservices import BaseSubnetSubService
 from cloudbridge.base.subservices import BaseSubnetSubService
@@ -37,3 +38,9 @@ class GCPSubnetSubService(BaseSubnetSubService):
 
 
     def __init__(self, provider, network):
     def __init__(self, provider, network):
         super(GCPSubnetSubService, self).__init__(provider, network)
         super(GCPSubnetSubService, self).__init__(provider, network)
+
+
+class GCPDnsRecordSubService(BaseDnsRecordSubService):
+
+    def __init__(self, provider, dns_zone):
+        super(GCPDnsRecordSubService, self).__init__(provider, dns_zone)