tools/dns-sync/dns_sync/zones.py (115 lines of code) (raw):
# Copyright 2017 Google Inc.
#
# 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.
import json
import threading
import uuid
from google.cloud import datastore
import webapp2
from dns_sync import api
from dns_sync.auth import AdminRequestHandler
from dns_sync.config import get_project_id
class RegistryCachedPropertyBaseClass(object):
"""A property cached in a dictionary returned by the "registry" method.
The registry method is implemented in base classes.
"""
_DEFAULT_VALUE = object()
def registry(self):
"""implemented by subclasses."""
pass
def __init__(self, func, name=None, doc=None):
self.__name__ = name or func.__name__
self.__module__ = func.__module__
self.__doc__ = doc or func.__doc__
self.func = func
self.lock = threading.RLock()
def __get__(self, obj, _):
if obj is None:
return self
with self.lock:
cache = self.registry().get(obj)
if cache is None:
cache = dict()
self.registry()[obj] = cache
value = cache.get(self.__name__, self._DEFAULT_VALUE)
if value is self._DEFAULT_VALUE:
value = self.func(obj)
cache[self.__name__] = value
return value
class AppCachedProperty(RegistryCachedPropertyBaseClass):
"""Caches property value in global memory (application cache)."""
GLOBAL_CACHE = {}
def registry(self):
"""Return the application registry, or a global cache."""
if webapp2.get_app().registry:
return webapp2.get_app().registry
else:
return AppCachedProperty.GLOBAL_CACHE
class RequestCachedProperty(RegistryCachedPropertyBaseClass):
"""Cache a property value in request registry."""
def registry(self):
"""Return request registry."""
try:
return webapp2.get_request().registry
except AssertionError:
return {}
class ZoneConfigEntity(datastore.Entity):
"""Configuration settings for the application.
Stored in datastore.
"""
KEY = api.CLIENTS.datastore.key('ZoneConfigEntity', 'config_entity')
@classmethod
def get_entity(cls):
"""Returns the one config entity, get or create it."""
config_entity = api.CLIENTS.datastore.get(ZoneConfigEntity.KEY)
if config_entity is None:
config_entity = ZoneConfigEntity(None)
config_entity.put()
else:
config_entity = ZoneConfigEntity(config_entity)
return config_entity
def __init__(self, entity):
"""Construct from an entity, call get_entity instead."""
if entity:
super(ZoneConfigEntity, self).__init__(
entity.key, list(entity.exclude_from_indexes))
self.update(entity)
mapping = self['regular_expression_zone_mapping']
if isinstance(mapping, basestring):
self['regular_expression_zone_mapping'] = json.loads(mapping)
else:
super(ZoneConfigEntity, self).__init__(ZoneConfigEntity.KEY, [
'regular_expression_zone_mapping', 'default_zone',
'dns_project', 'pubsub_shared_secret', 'subscription_endpoint'
])
self.update({
'regular_expression_zone_mapping': None,
'dns_project': None,
'default_zone': None,
'pubsub_shared_secret': str(uuid.uuid4()),
'subscription_endpoint': None
})
def put(self):
"""Store in datastore."""
# Must first change list of lists property into something Datastore can
# accept (a json string).
mapping = self['regular_expression_zone_mapping']
if type(mapping) == list:
self['regular_expression_zone_mapping'] = json.dumps(mapping)
try:
api.CLIENTS.datastore.put(self)
finally:
self['regular_expression_zone_mapping'] = mapping
@property
def managed_zone_project(self):
"""Returns either the DNS project, or the current project."""
return (self.get('dns_project', None) or get_project_id())
class ZoneConfig(object):
"""Application DNS Zone configuration."""
@RequestCachedProperty
def config(self):
"""Return the configuration from the datastore.
Will be cached in the current request registry.
"""
return ZoneConfigEntity.get_entity()
@RequestCachedProperty
def regular_expression_zone_mapping(self):
"""Mapping from regular expression to DNS Cloud Zone."""
if self.config['regular_expression_zone_mapping']:
return json.loads(self.config['regular_expression_zone_mapping'])
else:
return []
@RequestCachedProperty
def default_zone(self):
"""Return the Cloud DNS default zone."""
return self.config['default_zone']
@RequestCachedProperty
def pubsub_shared_secret(self):
"""Return pub/sub shared secret."""
return self.config['pubsub_shared_secret']
@RequestCachedProperty
def managed_zone_project(self):
"""Return project owning all Cloud DNS zones."""
return self.config.get('dns_project', None) or get_project_id()
@AppCachedProperty
def managed_zone_dns_name_cache(self):
"""cache used to store Cloud DNS zone name to DNS name mapping."""
return dict()
def get_zone_dns_name(self, managed_zone_name):
""""Get dns name of the manged zone.
Requires an API call to look it up. Cache in instance memory.
"""
if managed_zone_name in self.managed_zone_dns_name_cache:
return self.managed_zone_dns_name_cache[managed_zone_name]
managed_zone = api.CLIENTS.dns.managedZones().get(
managedZone=managed_zone_name,
project = self.config.managed_zone_project).execute()
dns_name = managed_zone['dnsName']
self.managed_zone_dns_name_cache[managed_zone_name] = dns_name
return dns_name
class GetZoneConfig(AdminRequestHandler):
"""Return json describing current configuration."""
def get(self):
config_entity = ZoneConfigEntity.get_entity()
self.response.content_type = 'application/json'
self.response.write(json.dumps(config_entity))
class GetProjects(AdminRequestHandler):
""" Return list of projects application has access to."""
def get(self):
projects = [project.project_id for project in
api.CLIENTS.crm.list_projects()]
projects = sorted(projects)
self.response.content_type = 'application/json'
self.response.write(json.dumps(projects))
class GetProjectZones(AdminRequestHandler):
"""Returns list of Cloud DNS zones for the input project."""
def post(self):
project = self.request.body
def zone_pager(page_token):
return api.CLIENTS.dns.managedZones().list(
project=project,
pageToken=page_token)
zones = [zone['name'] for zone in api.resource_iterator(zone_pager)]
self.response.content_type = 'application/json'
self.response.write(json.dumps(zones))
class SetZoneConfig(AdminRequestHandler):
"""Save new configuration values input from the request."""
def post(self):
new_config = json.loads(self.request.body)
old_config = ZoneConfigEntity.get_entity()
old_config.update(new_config)
old_config.put()
self.response.content_type = 'application/json'
self.response.write(json.dumps(new_config))
CONFIG = ZoneConfig()