Browse Source

Fixed bug for MX and SRV. Added Azure test suite as well.

pull/84/head
Heesu Hwang 9 years ago
parent
commit
cc47bd7034
3 changed files with 342 additions and 124 deletions
  1. +171
    -116
      octodns/provider/azuredns.py
  2. +0
    -8
      rb.txt
  3. +171
    -0
      tests/test_octodns_provider_azuredns.py

+ 171
- 116
octodns/provider/azuredns.py View File

@ -4,7 +4,6 @@
from __future__ import absolute_import, division, print_function, \ from __future__ import absolute_import, division, print_function, \
unicode_literals unicode_literals
import sys
from azure.common.credentials import ServicePrincipalCredentials from azure.common.credentials import ServicePrincipalCredentials
from azure.mgmt.dns import DnsManagementClient from azure.mgmt.dns import DnsManagementClient
@ -14,53 +13,120 @@ from azure.mgmt.dns.models import ARecord, AaaaRecord, CnameRecord, MxRecord, \
from functools import reduce from functools import reduce
import logging import logging
import re
from ..record import Record, Update
from ..record import Record
from .base import BaseProvider from .base import BaseProvider
class _AzureRecord(object): class _AzureRecord(object):
'''
Wrapper for OctoDNS record.
azuredns.py:
''' Wrapper for OctoDNS record.
azuredns.py:
class: octodns.provider.azuredns._AzureRecord class: octodns.provider.azuredns._AzureRecord
An _AzureRecord is easily accessible to the Azure DNS Management library
functions and is used to wrap all relevant data to create a record in
An _AzureRecord is easily accessible to Azure DNS Management library
functions and is used to wrap all relevant data to create a record in
Azure. Azure.
''' '''
def __init__(self, resource_group, record, values=None): def __init__(self, resource_group, record, values=None):
'''
'''
:param resource_group: The name of resource group in Azure :param resource_group: The name of resource group in Azure
:type resource_group: str
:param record: An OctoDNS record
:type resource_group: str
:param record: An OctoDNS record
:type record: ..record.Record :type record: ..record.Record
:param values: Parameters for a record. eg IP address, port, domain :param values: Parameters for a record. eg IP address, port, domain
name, etc. Values usually read from record.data name, etc. Values usually read from record.data
:type values: {'values': [...]} or {'value': [...]} :type values: {'values': [...]} or {'value': [...]}
:type return: _AzureRecord :type return: _AzureRecord
''' '''
self.resource_group = resource_group self.resource_group = resource_group
self.zone_name = record.zone.name[0:len(record.zone.name)-1]
self.zone_name = record.zone.name[0:len(record.zone.name) - 1]
self.relative_record_set_name = record.name or '@' self.relative_record_set_name = record.name or '@'
self.record_type = record._type self.record_type = record._type
data = values or record.data data = values or record.data
format_u_s = '' if record._type == 'A' else '_' format_u_s = '' if record._type == 'A' else '_'
key_name ='{}{}records'.format(self.record_type, format_u_s).lower()
key_name = '{}{}records'.format(self.record_type, format_u_s).lower()
class_name = '{}'.format(self.record_type).capitalize() + \ class_name = '{}'.format(self.record_type).capitalize() + \
'Record'.format(self.record_type) 'Record'.format(self.record_type)
self.params = None
if not self.record_type == 'CNAME':
self.params = self._params(data, key_name, eval(class_name))
else:
self.params = {'cname_record': CnameRecord(data['value'])}
self.params = getattr(self, '_params_for_{}'.format(record._type))
self.params = self.params(data, key_name, eval(class_name))
self.params['ttl'] = record.ttl self.params['ttl'] = record.ttl
def _params(self, data, key_name, azure_class): def _params(self, data, key_name, azure_class):
return {key_name: [azure_class(v) for v in data['values']]} \ return {key_name: [azure_class(v) for v in data['values']]} \
if 'values' in data else {key_name: [azure_class(data['value'])]} if 'values' in data else {key_name: [azure_class(data['value'])]}
_params_for_A = _params
_params_for_AAAA = _params
_params_for_NS = _params
_params_for_PTR = _params
_params_for_TXT = _params
def _params_for_SRV(self, data, key_name, azure_class):
params = []
if 'values' in data:
for vals in data['values']:
params.append(azure_class(vals['priority'],
vals['weight'],
vals['port'],
vals['target']))
else:
params.append(azure_class(data['value']['priority'],
data['value']['weight'],
data['value']['port'],
data['value']['target']))
return {key_name: params}
def _params_for_MX(self, data, key_name, azure_class):
params = []
if 'values' in data:
for vals in data['values']:
params.append(azure_class(vals['priority'],
vals['value']))
else:
params.append(azure_class(data['value']['priority'],
data['value']['value']))
return {key_name: params}
def _params_for_CNAME(self, data, key_name, azure_class):
return {'cname_record': CnameRecord(data['value'])}
def _equals(self, b):
def parse_dict(params):
vals = []
for char in params:
if char != 'ttl':
list_records = params[char]
try:
for record in list_records:
vals.append(record.__dict__)
except:
vals.append(list_records.__dict__)
vals.sort()
return vals
return (self.resource_group == b.resource_group) & \
(self.zone_name == b.zone_name) & \
(self.record_type == b.record_type) & \
(self.params['ttl'] == b.params['ttl']) & \
(parse_dict(self.params) == parse_dict(b.params)) & \
(self.relative_record_set_name == b.relative_record_set_name)
def __str__(self):
string = 'Zone: {}; '.format(self.zone_name)
string += 'Name: {}; '.format(self.relative_record_set_name)
string += 'Type: {}; '.format(self.record_type)
string += 'Ttl: {}; '.format(self.params['ttl'])
for char in self.params:
if char != 'ttl':
try:
for rec in self.params[char]:
string += 'Record: {}; '.format(rec.__dict__)
except:
string += 'Record: {}; '.format(self.params[char].__dict__)
return string
class AzureProvider(BaseProvider): class AzureProvider(BaseProvider):
''' '''
@ -68,11 +134,11 @@ class AzureProvider(BaseProvider):
azuredns.py: azuredns.py:
class: octodns.provider.azuredns.AzureProvider class: octodns.provider.azuredns.AzureProvider
# Current support of authentication of access to Azure services only
# includes using a Service Principal:
# 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/ # https://docs.microsoft.com/en-us/azure/azure-resource-manager/
# resource-group-create-service-principal-portal # resource-group-create-service-principal-portal
# The Azure Active Directory Application ID (referred to client ID) req:
# The Azure Active Directory Application ID (aka client ID) req:
client_id: client_id:
# Authentication Key Value req: # Authentication Key Value req:
key: key:
@ -82,32 +148,33 @@ class AzureProvider(BaseProvider):
sub_id: sub_id:
# Resource Group name req: # Resource Group name req:
resource_group: resource_group:
TODO: change the config file to use env variables instead of hard-coded keys?
personal notes: testing: test authentication vars located in /home/t-hehwan/vars.txt
TODO: change config file to use env vars instead of hard-coded keys
personal notes: testing: test authentication vars located in
/home/t-hehwan/vars.txt
''' '''
SUPPORTS_GEO = False SUPPORTS_GEO = False
SUPPORTS = set(('A', 'AAAA', 'CNAME', 'MX', 'NS', 'PTR', 'SRV', 'TXT')) SUPPORTS = set(('A', 'AAAA', 'CNAME', 'MX', 'NS', 'PTR', 'SRV', 'TXT'))
def __init__(self, id, client_id, key, directory_id, sub_id, resource_group,
*args, **kwargs):
def __init__(self, id, client_id, key, directory_id, sub_id,
resource_group, *args, **kwargs):
self.log = logging.getLogger('AzureProvider[{}]'.format(id)) self.log = logging.getLogger('AzureProvider[{}]'.format(id))
self.log.debug('__init__: id=%s, client_id=%s, ' self.log.debug('__init__: id=%s, client_id=%s, '
'key=***, directory_id:%s', id, client_id, directory_id)
'key=***, directory_id:%s', id, client_id, directory_id)
super(AzureProvider, self).__init__(id, *args, **kwargs) super(AzureProvider, self).__init__(id, *args, **kwargs)
credentials = ServicePrincipalCredentials( credentials = ServicePrincipalCredentials(
client_id, secret = key, tenant = directory_id
client_id, secret=key, tenant=directory_id
) )
self._dns_client = DnsManagementClient(credentials, sub_id) self._dns_client = DnsManagementClient(credentials, sub_id)
self._resource_group = resource_group self._resource_group = resource_group
self._azure_zones = set() self._azure_zones = set()
def _populate_zones(self): def _populate_zones(self):
self.log.debug('azure_zones: loading') self.log.debug('azure_zones: loading')
for zone in self._dns_client.zones.list_by_resource_group(
self._resource_group):
list_zones = self._dns_client.zones.list_by_resource_group
for zone in list_zones(self._resource_group):
self._azure_zones.add(zone.name) self._azure_zones.add(zone.name)
def _check_zone(self, name, create=False): def _check_zone(self, name, create=False):
@ -115,12 +182,12 @@ class AzureProvider(BaseProvider):
Checks whether a zone specified in a source exist in Azure server. Checks whether a zone specified in a source exist in Azure server.
Note that Azure zones omit end '.' eg: contoso.com vs contoso.com. Note that Azure zones omit end '.' eg: contoso.com vs contoso.com.
Returns the name if it exists. Returns the name if it exists.
:param name: Name of a zone to checks :param name: Name of a zone to checks
:type name: str :type name: str
:param create: If True, creates the zone of that name. :param create: If True, creates the zone of that name.
:type create: bool :type create: bool
:type return: str or None :type return: str or None
''' '''
self.log.debug('_check_zone: name=%s', name) self.log.debug('_check_zone: name=%s', name)
@ -130,163 +197,151 @@ class AzureProvider(BaseProvider):
if self._dns_client.zones.get(self._resource_group, name): if self._dns_client.zones.get(self._resource_group, name):
self._azure_zones.add(name) self._azure_zones.add(name)
return name return name
except:
except: # TODO: figure out what location should be
if create: if create:
try: try:
self.log.debug('_check_zone: no matching zone; creating %s',
name)
if self._dns_client.zones.create_or_update(
self._resource_group, name, Zone('global')): #TODO: figure out what location should be
self.log.debug('_check_zone:no matching zone; creating %s',
name)
create_zone = self._dns_client.zones.create_or_update
if create_zone(self._resource_group, name, Zone('global')):
return name return name
except: except:
raise raise
return None return None
def populate(self, zone, target=False):
def populate(self, zone, target=False, lenient=False):
''' '''
Required function of manager.py. Required function of manager.py.
Special notes for Azure. Azure zone names omit final '.' Special notes for Azure. Azure zone names omit final '.'
Azure record names for '' are represented by '@' Azure record names for '' are represented by '@'
Azure records created through online interface may have null values Azure records created through online interface may have null values
(eg, no IP address for A record). Specific quirks such as these are (eg, no IP address for A record). Specific quirks such as these are
responsible for any strange parsing. responsible for any strange parsing.
:param zone: A dns zone :param zone: A dns zone
:type zone: octodns.zone.Zone :type zone: octodns.zone.Zone
:param target: Checks if Azure is source or target of config.
:param target: Checks if Azure is source or target of config.
Currently only supports as a target. Does not use. Currently only supports as a target. Does not use.
:type target: bool :type target: bool
TODO: azure interface allows null values. If this attempts to populate with them, will fail. add safety check (simply delete records with null values?)
TODO: azure interface allows null values. If this attempts to
populate with them, will fail. add safety check (simply delete
records with null values?)
:type return: void :type return: void
''' '''
zone_name = zone.name[0:len(zone.name)-1]
zone_name = zone.name[0:len(zone.name) - 1]
self.log.debug('populate: name=%s', zone_name) self.log.debug('populate: name=%s', zone_name)
before = len(zone.records) before = len(zone.records)
self._populate_zones() self._populate_zones()
if self._check_zone(zone_name): if self._check_zone(zone_name):
for typ in self.SUPPORTS: for typ in self.SUPPORTS:
for azrecord in self._dns_client.record_sets.list_by_type(
self._resource_group, zone_name, typ):
records = self._dns_client.record_sets.list_by_type
for azrecord in records(self._resource_group, zone_name, typ):
record_name = azrecord.name if azrecord.name != '@' else '' record_name = azrecord.name if azrecord.name != '@' else ''
data = self._type_and_ttl(typ, azrecord.ttl,
getattr(self, '_data_for_{}'.format(typ))(azrecord))
data = getattr(self, '_data_for_{}'.format(typ))(azrecord)
data['type'] = typ
data['ttl'] = azrecord.ttl
record = Record.new(zone, record_name, data, source=self) record = Record.new(zone, record_name, data, source=self)
zone.add_record(record) zone.add_record(record)
self.log.info('populate: found %s records', len(zone.records)-before)
def _type_and_ttl(self, typ, ttl, data):
''' Adds type and ttl fields to return dictionary.
:param typ: The type of a record
:type typ: str
:param ttl: The ttl of a record
:type ttl: int
:param data: Dictionary holding values of a record. eg, IP addresses
:type data: {'values': [...]} or {'value': [...]}
:type return: {...}
'''
data['type'] = typ
data['ttl'] = ttl
return data
self.log.info('populate: found %s records', len(zone.records) - before)
def _data_for_A(self, azrecord): def _data_for_A(self, azrecord):
return {'values': [ar.ipv4_address for ar in azrecord.arecords]} return {'values': [ar.ipv4_address for ar in azrecord.arecords]}
def _data_for_AAAA(self, azrecord): def _data_for_AAAA(self, azrecord):
return {'values': [ar.ipv6_address for ar in azrecord.aaaa_records]} return {'values': [ar.ipv6_address for ar in azrecord.aaaa_records]}
def _data_for_TXT(self, azrecord): def _data_for_TXT(self, azrecord):
return {'values': \
[reduce((lambda a,b:a+b), ar.value) for ar in azrecord.txt_records]}
return {'values': [reduce((lambda a, b: a + b), ar.value)
for ar in azrecord.txt_records]}
def _data_for_CNAME(self, azrecord): #TODO: see TODO in population comment.
def _data_for_CNAME(self, azrecord): # TODO: see TODO in pop comment.
try: try:
val = azrecord.cname_record.cname val = azrecord.cname_record.cname
if not val.endswith('.'): if not val.endswith('.'):
val += '.' val += '.'
return {'value': val} return {'value': val}
except: except:
return {'value': '.'} #TODO: this is a bad fix. but octo checks that cnames have trailing '.' while azure allows creating cnames on the online interface with no value.
def _data_for_PTR(self, azrecord): #TODO: see TODO in population comment.
return {'value': '.'} # TODO: this is a bad fix. but octo checks
# that cnames have trailing '.' while azure allows creating cnames
# on the online interface with no value.
def _data_for_PTR(self, azrecord): # TODO: see TODO in population comment.
try: try:
val = azrecord.ptr_records[0].ptdrname val = azrecord.ptr_records[0].ptdrname
if not val.endswith('.'): if not val.endswith('.'):
val += '.' val += '.'
return {'value': val} return {'value': val}
except: except:
return {'value': '.' } #TODO: this is a bad fix. but octo checks that cnames have trailing '.' while azure allows creating cnames on the online interface with no value.
return {'value': '.'}
def _data_for_MX(self, azrecord): def _data_for_MX(self, azrecord):
return {'values': [{'priority':ar.preference,
'value':ar.exchange} for ar in azrecord.mx_records]}
return {'values': [{'priority': ar.preference, 'value': ar.exchange}
for ar in azrecord.mx_records]
}
def _data_for_SRV(self, azrecord): def _data_for_SRV(self, azrecord):
return {'values': [{'priority': ar.priority,
'weight': ar.weight,
'port': ar.port,
'target': ar.target} for ar in azrecord.srv_records]
}
def _data_for_NS(self, azrecord): #TODO: see TODO in population comment.
return {'values': [{'priority': ar.priority, 'weight': ar.weight,
'port': ar.port, 'target': ar.target}
for ar in azrecord.srv_records]
}
def _data_for_NS(self, azrecord): # TODO: see TODO in population comment.
def period_validate(string): def period_validate(string):
return string if string.endswith('.') else string + '.' return string if string.endswith('.') else string + '.'
vals = [ar.nsdname for ar in azrecord.ns_records] vals = [ar.nsdname for ar in azrecord.ns_records]
return {'values': [period_validate(val) for val in vals]} return {'values': [period_validate(val) for val in vals]}
def _apply_Create(self, change): def _apply_Create(self, change):
''' A record from change must be created.
'''A record from change must be created.
:param change: a change object :param change: a change object
:type change: octodns.record.Change :type change: octodns.record.Change
:type return: void :type return: void
''' '''
ar = _AzureRecord(self._resource_group, change.new) ar = _AzureRecord(self._resource_group, change.new)
create = self._dns_client.record_sets.create_or_update create = self._dns_client.record_sets.create_or_update
create(resource_group_name=ar.resource_group,
zone_name=ar.zone_name,
relative_record_set_name=ar.relative_record_set_name,
record_type=ar.record_type,
create(resource_group_name=ar.resource_group,
zone_name=ar.zone_name,
relative_record_set_name=ar.relative_record_set_name,
record_type=ar.record_type,
parameters=ar.params) parameters=ar.params)
def _apply_Delete(self, change): def _apply_Delete(self, change):
ar = _AzureRecord(self._resource_group, change.existing) ar = _AzureRecord(self._resource_group, change.existing)
delete = self._dns_client.record_sets.delete delete = self._dns_client.record_sets.delete
delete(self._resource_group, ar.zone_name, ar.relative_record_set_name,
delete(self._resource_group, ar.zone_name, ar.relative_record_set_name,
ar.record_type) ar.record_type)
def _apply_Update(self, change): def _apply_Update(self, change):
self._apply_Create(change) self._apply_Create(change)
def _apply(self, plan): def _apply(self, plan):
''' '''
Required function of manager.py Required function of manager.py
:param plan: Contains the zones and changes to be made :param plan: Contains the zones and changes to be made
:type plan: octodns.provider.base.Plan :type plan: octodns.provider.base.Plan
:type return: void :type return: void
''' '''
desired = plan.desired desired = plan.desired
changes = plan.changes changes = plan.changes
self.log.debug('_apply: zone=%s, len(changes)=%d', desired.name,
self.log.debug('_apply: zone=%s, len(changes)=%d', desired.name,
len(changes)) len(changes))
azure_zone_name = desired.name[0:len(desired.name)-1]
azure_zone_name = desired.name[0:len(desired.name) - 1]
self._check_zone(azure_zone_name, create=True) self._check_zone(azure_zone_name, create=True)
for change in changes: for change in changes:
class_name = change.__class__.__name__ class_name = change.__class__.__name__
getattr(self, '_apply_{}'.format(class_name))(change) getattr(self, '_apply_{}'.format(class_name))(change)

+ 0
- 8
rb.txt View File

@ -1,8 +0,0 @@
#!/bin/bash
#script to rebuild octodns quickly
sudo rm -r /home/t-hehwan/GitHub/octodns/build
sudo rm -r /home/t-hehwan/GitHub/octodns/octodns.egg-info
sudo python /home/t-hehwan/GitHub/octodns/setup.py -q build
sudo python /home/t-hehwan/GitHub/octodns/setup.py -q install
octodns-sync --config-file=./config/production.yaml

+ 171
- 0
tests/test_octodns_provider_azuredns.py View File

@ -0,0 +1,171 @@
#
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from octodns.record import Create, Delete, Record, Update
from octodns.provider.azuredns import _AzureRecord, AzureProvider
from octodns.zone import Zone
from azure.mgmt.dns.models import ARecord, AaaaRecord, CnameRecord, MxRecord, \
SrvRecord, NsRecord, PtrRecord, TxtRecord, Zone as AzureZone
from octodns.zone import Zone
from unittest import TestCase
import sys
class Test_AzureRecord(TestCase):
zone = Zone(name='unit.tests.', sub_zones=[])
octo_records = []
octo_records.append(Record.new(zone, '', {
'ttl': 0,
'type': 'A',
'values': ['1.2.3.4', '10.10.10.10']
}))
octo_records.append(Record.new(zone, 'a', {
'ttl': 1,
'type': 'A',
'values': ['1.2.3.4', '1.1.1.1'],
}))
octo_records.append(Record.new(zone, 'aa', {
'ttl': 9001,
'type': 'A',
'values': ['1.2.4.3']
}))
octo_records.append(Record.new(zone, 'aaa', {
'ttl': 2,
'type': 'A',
'values': ['1.1.1.3']
}))
octo_records.append(Record.new(zone, 'cname', {
'ttl': 3,
'type': 'CNAME',
'value': 'a.unit.tests.',
}))
octo_records.append(Record.new(zone, '', {
'ttl': 3,
'type': 'MX',
'values': [{
'priority': 10,
'value': 'mx1.unit.tests.',
}, {
'priority': 20,
'value': 'mx2.unit.tests.',
}]
}))
octo_records.append(Record.new(zone, '', {
'ttl': 4,
'type': 'NS',
'values': ['ns1.unit.tests.', 'ns2.unit.tests.'],
}))
octo_records.append(Record.new(zone, '', {
'ttl': 5,
'type': 'NS',
'value': 'ns1.unit.tests.',
}))
octo_records.append(Record.new(zone, '_srv._tcp', {
'ttl': 6,
'type': 'SRV',
'values': [{
'priority': 10,
'weight': 20,
'port': 30,
'target': 'foo-1.unit.tests.',
}, {
'priority': 12,
'weight': 30,
'port': 30,
'target': 'foo-2.unit.tests.',
}]
}))
azure_records = []
_base0 = _AzureRecord('TestAzure', octo_records[0])
_base0.zone_name = 'unit.tests'
_base0.relative_record_set_name = '@'
_base0.record_type = 'A'
_base0.params['ttl'] = 0
_base0.params['arecords'] = [ARecord('1.2.3.4'), ARecord('10.10.10.10')]
azure_records.append(_base0)
_base1 = _AzureRecord('TestAzure', octo_records[1])
_base1.zone_name = 'unit.tests'
_base1.relative_record_set_name = 'a'
_base1.record_type = 'A'
_base1.params['ttl'] = 1
_base1.params['arecords'] = [ARecord('1.2.3.4'), ARecord('1.1.1.1')]
azure_records.append(_base1)
_base2 = _AzureRecord('TestAzure', octo_records[2])
_base2.zone_name = 'unit.tests'
_base2.relative_record_set_name = 'aa'
_base2.record_type = 'A'
_base2.params['ttl'] = 9001
_base2.params['arecords'] = ARecord('1.2.4.3')
azure_records.append(_base2)
_base3 = _AzureRecord('TestAzure', octo_records[3])
_base3.zone_name = 'unit.tests'
_base3.relative_record_set_name = 'aaa'
_base3.record_type = 'A'
_base3.params['ttl'] = 2
_base3.params['arecords'] = ARecord('1.1.1.3')
azure_records.append(_base3)
_base4 = _AzureRecord('TestAzure', octo_records[4])
_base4.zone_name = 'unit.tests'
_base4.relative_record_set_name = 'cname'
_base4.record_type = 'CNAME'
_base4.params['ttl'] = 3
_base4.params['cname_record'] = CnameRecord('a.unit.tests.')
azure_records.append(_base4)
_base5 = _AzureRecord('TestAzure', octo_records[5])
_base5.zone_name = 'unit.tests'
_base5.relative_record_set_name = '@'
_base5.record_type = 'MX'
_base5.params['ttl'] = 3
_base5.params['mx_records'] = [MxRecord(10, 'mx1.unit.tests.'),
MxRecord(20, 'mx2.unit.tests.')]
azure_records.append(_base5)
_base6 = _AzureRecord('TestAzure', octo_records[6])
_base6.zone_name = 'unit.tests'
_base6.relative_record_set_name = '@'
_base6.record_type = 'NS'
_base6.params['ttl'] = 4
_base6.params['ns_records'] = [NsRecord('ns1.unit.tests.'),
NsRecord('ns2.unit.tests.')]
azure_records.append(_base6)
_base7 = _AzureRecord('TestAzure', octo_records[7])
_base7.zone_name = 'unit.tests'
_base7.relative_record_set_name = '@'
_base7.record_type = 'NS'
_base7.params['ttl'] = 5
_base7.params['ns_records'] = [NsRecord('ns1.unit.tests.')]
azure_records.append(_base7)
_base8 = _AzureRecord('TestAzure', octo_records[8])
_base8.zone_name = 'unit.tests'
_base8.relative_record_set_name = '_srv._tcp'
_base8.record_type = 'SRV'
_base8.params['ttl'] = 6
_base8.params['srv_records'] = [SrvRecord(10, 20, 30, 'foo-1.unit.tests.'),
SrvRecord(12, 30, 30, 'foo-2.unit.tests.')]
azure_records.append(_base8)
def test_azure_record(self):
assert(len(self.azure_records) == len(self.octo_records))
for i in range(len(self.azure_records)):
octo = _AzureRecord('TestAzure', self.octo_records[i])
assert(self.azure_records[i]._equals(octo))
class TestAzureDnsProvider(TestCase):
def test_populate(self):
pass # placeholder

Loading…
Cancel
Save