mirror of
https://github.com/github/octodns.git
synced 2024-05-11 05:55:00 +00:00
198 lines
6.8 KiB
Python
198 lines
6.8 KiB
Python
#
|
|
#
|
|
#
|
|
|
|
from __future__ import absolute_import, division, print_function, \
|
|
unicode_literals
|
|
|
|
from azure.common.credentials import ServicePrincipalCredentials
|
|
from azure.mgmt.dns import DnsManagementClient
|
|
from azure.mgmt.dns.models import *
|
|
|
|
from collections import defaultdict
|
|
# from incf.countryutils.transformations import cca_to_ctca2 TODO: add geo sup.
|
|
import logging
|
|
import re
|
|
|
|
from ..record import Record, Update
|
|
from .base import BaseProvider
|
|
|
|
# Only made for A records. will have to adjust for more generic params types
|
|
class _AzureRecord(object):
|
|
def __init__(self, resource_group_name, record, values=None):
|
|
self.resource_group_name = resource_group_name
|
|
self.zone_name = record.zone.name
|
|
self.relative_record_set_name = record.name
|
|
self.record_type = record._type
|
|
|
|
type_name = '{}records'.format(self.record_type)
|
|
class_name = '{}'.format(self.record_type).capitalize() + \
|
|
'Record'.format(self.record_type)
|
|
_values = [record._process_values]
|
|
self.params = {'ttl':record.ttl or 1800, \
|
|
type_name:[eval(class_name)(value) for value in _values] or []}
|
|
|
|
|
|
|
|
class AzureProvider(BaseProvider):
|
|
'''
|
|
Azure DNS Provider
|
|
|
|
azure.py:
|
|
class: octodns.provider.azure.AzureProvider
|
|
# Current support of authentication of access to Azure services only
|
|
# includes using a Service Principal:
|
|
# https://docs.microsoft.com/en-us/azure/azure-resource-manager/
|
|
# resource-group-create-service-principal-portal
|
|
# The Azure Active Directory Application ID (referred to client ID) req:
|
|
client_id:
|
|
# Authentication Key Value req:
|
|
key:
|
|
# Directory ID (referred to tenant ID) req:
|
|
directory_id:
|
|
# Subscription ID req:
|
|
sub_id:
|
|
# Resource Group name req:
|
|
resource_group:
|
|
|
|
testing: test authentication vars located in /home/t-hehwan/vars.txt
|
|
'''
|
|
|
|
# TODO. Will add support as project progresses.
|
|
SUPPORTS_GEO = False
|
|
|
|
def __init__(self, id, client_id, key, directory_id, sub_id, \
|
|
resource_group, *args, **kwargs):
|
|
self.log = logging.getLogger('AzureProvider[{}]'.format(id))
|
|
self.log.debug('__init__: id=%s, client_id=%s, '
|
|
'key=***, directory_id:%s', id, client_id, directory_id)
|
|
super(AzureProvider, self).__init__(id, *args, **kwargs)
|
|
|
|
credentials = ServicePrincipalCredentials(
|
|
client_id = client_id, secret = key, tenant = directory_id
|
|
)
|
|
self._dns_client = DnsManagementClient(credentials, sub_id)
|
|
self._resource_group = resource_group
|
|
|
|
|
|
self._azure_zones = None # will be a dictionary. key: name. val: id.
|
|
self._azure_records = None # will be dict by octodns record, az record
|
|
|
|
self._supported_types = ['A']
|
|
|
|
# TODO: health checks a la route53.
|
|
|
|
|
|
|
|
# TODO: add support for all types. First skeleton: add A.
|
|
def supports(self, record):
|
|
# TODO: possibly refactor
|
|
return record._type in self._supported_types
|
|
|
|
@property
|
|
def azure_zones(self):
|
|
if self._azure_zones is None:
|
|
self.log.debug('azure_zones: loading')
|
|
zones = {}
|
|
for zone in self._dns_client.zones.list():
|
|
zones[zone['name']] = zone['id']
|
|
self._azure_zones = zones
|
|
return self._azure_zones
|
|
|
|
# Given a zone name, returns the zone id. If DNE, creates it.
|
|
def _get_zone_id(self, name, create=False):
|
|
self.log.debug('_get_zone_id: name=%s', name)
|
|
if name in self.azure_zones:
|
|
id = self.azure_zones[name]
|
|
self.log.debug('_get_zone_id: id=%s', id)
|
|
return id
|
|
if create:
|
|
#TODO
|
|
return None
|
|
|
|
# Create a dictionary of record objects by zone and octodns record names
|
|
# TODO: add geo parsing
|
|
def populate(self, zone, target):
|
|
self.log.debug('populate: name=%s', zone.name)
|
|
before = len(zone.records)
|
|
|
|
zone_id = self._get_zone_id(zone.name)
|
|
if zone_id:
|
|
records = defaultdict(list)
|
|
for type in self._supported_types:
|
|
for azrecord in self.dns_client.record_sets.list_by_type(self._resource_group, zone.name, type):
|
|
record_name = azrecord.name
|
|
data = getattr(self, '_data_for_{}'.format(type))(type, azrecord)
|
|
record = Record.new(zone, record_name, data, source=self)
|
|
zone.add_record(record)
|
|
self._azure_records[record] = _AzureRecord(self._resource_group, record, record.data)
|
|
|
|
self.log.info('populate: found %s records', len(zone.records)-before)
|
|
|
|
# might not need
|
|
def _get_type(azrecord):
|
|
azrecord['type'].split('/')[-1]
|
|
|
|
def _data_for_A(self, type, azrecord):
|
|
return {
|
|
'type': type
|
|
'ttl': azrecord['ttl'],
|
|
'values': [ar.ipv4_address for ar in azrecord.arecords]
|
|
}
|
|
|
|
def _get_azure_record(record):
|
|
try:
|
|
return self._azure_records[record]
|
|
except:
|
|
raise
|
|
|
|
def _apply_Create(self, change):
|
|
new = change.new
|
|
ar = self._get_azure_record(new)
|
|
|
|
create = self._dns_client.record_sets.create_or_update
|
|
create(ar.resource_group_name, ar.zone_name, ar.relative_record_set_name \
|
|
ar.record_type, ar.params)
|
|
|
|
# type plan: Plan class from .base
|
|
def _apply(self, plan):
|
|
desired = plan.desired
|
|
changes = plan.changes
|
|
self.log.debug('_apply: zone=%s, len(changes)=%d', desired.name,
|
|
len(changes))
|
|
|
|
|
|
# validate that the zone exists. function creates zone if DNE.
|
|
self._get_zone_id(desired.name)
|
|
|
|
|
|
|
|
# Some parsing bits to call _mod_Create or _mod_Delete.
|
|
# changes is a list of Delete and Create objects.
|
|
for change in changes:
|
|
class_name = change.__class__.__name__
|
|
getattr(self, '_apply_{}'.format(class_name))(change)
|
|
|
|
|
|
|
|
# **********
|
|
# Figuring out what object plan is.
|
|
|
|
# self._executor = ThreadPoolExecutor(max_workers)
|
|
# futures.append(self._executor.submit(self._populate_and_plan,
|
|
# zone_name, sources, targets))
|
|
# plans = [p for f in futures for p in f.results()]
|
|
# type of plans[0] == type of one output of _populate_and_plan
|
|
|
|
# for target, plan in plans:
|
|
# apply(plan)
|
|
|
|
|
|
# type(target) == BaseProvider
|
|
# type(plan) == Plan()
|
|
|
|
# Plan(existing, desired, changes)
|
|
# existing.type == desired.type == Zone(desired.name, desired.sub_zones)
|
|
# Zone(name, sub_zones) (str and set of strs)
|
|
# changes.type = [Delete/Create]
|
|
|