Skip to content

Commit

Permalink
Support empty and delegated zone creation
Browse files Browse the repository at this point in the history
  • Loading branch information
asyncon committed Jun 18, 2022
1 parent 1e3e6a2 commit b14791a
Show file tree
Hide file tree
Showing 5 changed files with 83 additions and 12 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ providers:
# soa_default_ttl: 3600
# view: default
# use_grid_zone_timer: true
# zone_type: zone_delegated
```

## Alias Record Update Behaviour
Expand Down
61 changes: 50 additions & 11 deletions octoblox/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from collections import defaultdict
from functools import lru_cache
from octodns.provider.base import BaseProvider
from octodns.record import Record
from octodns.record import Change, Record

# fmt: off
single_types = {'ALIAS', 'CNAME', 'PTR'}
Expand Down Expand Up @@ -52,6 +52,19 @@
# fmt: on


class Create(Change):
"""Create Zone Change"""

CLASS_ORDERING = 0

def __init__(self, new):
super().__init__(None, new)

def __repr__(self, leader=''):
source = self.new.source.id if self.new.source else ''
return f'Create Zone {self.new.fqdn} ({source})'


class InfoBlox(requests.Session):
"""Encapsulates all traffic with the InfoBlox WAPI"""

Expand All @@ -67,6 +80,7 @@ def __init__(
log_change=False,
new_zone_fields=None,
log=None,
zone_type='zone_auth',
):
super(InfoBlox, self).__init__()
self.fqdn = fqdn
Expand All @@ -78,7 +92,8 @@ def __init__(
self.log_change = log_change
self.new_zone_fields = new_zone_fields or {}
self.log = log
if not apiver:
self.zone_type = zone_type or 'zone_auth'
if not apiver: # pragma: no branch
self.apiver = self.get_api_version()

def url(self, url):
Expand All @@ -90,7 +105,7 @@ def request(self, method, url, **kwargs):
ret = super().request(method, self.url(url), **kwargs)
try:
ret.raise_for_status()
except requests.HTTPError:
except requests.HTTPError: # pragma: no cover
self.log.error(
'InfoBlox.request: %d %s %s %r %s',
ret.status_code,
Expand Down Expand Up @@ -128,7 +143,7 @@ def get_zone_fqdn(self, zone):

def get_zone(self, zone):
return self.get(
'zone_auth',
self.zone_type,
params={
'fqdn': self.get_zone_fqdn(zone),
'_return_fields+': 'soa_default_ttl',
Expand All @@ -140,7 +155,7 @@ def add_zone(self, zone):
fqdn = self.get_zone_fqdn(zone)
zone_format = 'IPV6' if ':' in fqdn else 'IPV4' if '/' in fqdn else 'FORWARDING'
return self.post(
'zone_auth',
self.zone_type,
json={
'fqdn': fqdn,
'zone_format': zone_format,
Expand All @@ -166,7 +181,7 @@ def get_records(self, type, fields, zone, default_ttl, **extra):
},
).json()
data = ret['result']
while 'next_page_id' in ret:
while 'next_page_id' in ret: # pragma: no cover
ret = self.get(
'record:{0}'.format(type.lower()),
params={'_page_id': ret['next_page_id']},
Expand Down Expand Up @@ -252,6 +267,7 @@ def __init__(
log_change=False,
create_zones=False,
new_zone_fields=None,
zone_type='zone_auth',
*args,
**kwargs,
):
Expand All @@ -267,6 +283,7 @@ def __init__(
log_change,
new_zone_fields,
self.log,
zone_type,
)
self.create_zones = create_zones
self.log.debug(
Expand Down Expand Up @@ -313,8 +330,10 @@ def populate(self, zone, target=False, lenient=False):

zone_data = self.conn.get_zone(zone.name)

zone.exists = bool(zone_data)

if not zone_data:
if target and not self.create_zones:
if target and not self.create_zones: # pragma: no cover
raise ValueError(f'Zone does not exist in InfoBlox: {zone.name}')
return False

Expand All @@ -339,6 +358,24 @@ def populate(self, zone, target=False, lenient=False):

return True

def _extra_changes(self, existing, changes, **kwargs):
if self.create_zones and not existing.exists and not changes:
return [
Create(
Record.new(
existing,
'',
{
'type': 'NS',
'ttl': 3600,
'values': [existing.name + 'invalid.'],
},
source=self,
)
)
]
return []

def _apply_Create(self, zone, change, default_ttl):
new = change.new
type = new._type
Expand Down Expand Up @@ -370,7 +407,7 @@ def _apply_Update(self, zone, change, default_ttl):
v = {spec[0]: value[:-1], spec[1]: t}
self.conn.add_record(type, zone, new.name, v, new.ttl, default_ttl)
for t in self.conn.alias_types & {*refs}:
if refs[t][spec[0]] != value:
if refs[t][spec[0]] != value: # pragma: no branch
v = {spec[0]: value[:-1], spec[1]: t}
self.conn.mod_record(type, refs[t], v, new.ttl, default_ttl)
self.conn.del_record(
Expand All @@ -379,15 +416,15 @@ def _apply_Update(self, zone, change, default_ttl):
elif type in single_types:
self.conn.mod_record(type, ext.refs[0], value, new.ttl, default_ttl)
elif value in evalues:
if update:
if update: # pragma: no branch
self.conn.mod_record(
type,
ext.refs[evalues.index(value)],
value,
new.ttl,
default_ttl,
)
else:
else: # pragma: no cover
self.conn.add_record(type, zone, new.name, value, new.ttl, default_ttl)
if type not in single_types:
self.conn.del_record(
Expand All @@ -401,12 +438,14 @@ def _apply(self, plan):
zone_data = self.conn.get_zone(zone)

if not zone_data:
if not self.create_zones:
if not self.create_zones: # pragma: no cover
raise ValueError(f'Zone does not exist in InfoBlox: {zone}')
zone_data = [self.conn.add_zone(zone)]

default_ttl = zone_data[0].get('soa_default_ttl', 3600)

for change in plan.changes:
if isinstance(change, Create):
continue
class_name = change.__class__.__name__
getattr(self, f'_apply_{class_name}')(zone[:-1], change, default_ttl)
1 change: 1 addition & 0 deletions tests/config/empty.tests.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
--- {}
9 changes: 8 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ def new_zone_name():
return 'create.tests.'


@pytest.fixture
def empty_zone_name():
return 'empty.tests.'


@pytest.fixture
def new_ipv4_zone():
return '12.11.10.in-addr.arpa.'
Expand Down Expand Up @@ -59,7 +64,7 @@ def schema():


@pytest.fixture
def zones(zone_name, new_zone_name, new_ipv4_cidr, new_ipv6_cidr):
def zones(zone_name, new_zone_name, empty_zone_name, new_ipv4_cidr, new_ipv6_cidr):
return {
f'/wapi/v1.0/zone_auth?fqdn={zone_name[:-1]}': [
{
Expand All @@ -70,6 +75,8 @@ def zones(zone_name, new_zone_name, new_ipv4_cidr, new_ipv6_cidr):
}
],
f'/wapi/v1.0/zone_auth?fqdn={new_zone_name[:-1]}': [],
f'/wapi/v1.0/zone_auth?fqdn={empty_zone_name[:-1]}': [],
f'/wapi/v1.0/zone_delegated?fqdn={empty_zone_name[:-1]}': [],
f'/wapi/v1.0/zone_auth?fqdn={quote_plus(new_ipv4_cidr)}': [],
f'/wapi/v1.0/zone_auth?fqdn={quote_plus(new_ipv6_cidr)}': [],
}
Expand Down
23 changes: 23 additions & 0 deletions tests/test_infoblox_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,29 @@ def test_zone_creation(provider, new_zone_name):
provider.apply(plan)


def test_empty_zone_creation(provider, empty_zone_name):
expected = Zone(empty_zone_name, [])
source = YamlProvider('test', os.path.join(os.path.dirname(__file__), 'config'))
source.populate(expected)
assert len(expected.records) == 0
zone = Zone(empty_zone_name, [])
provider.populate(zone)
plan = provider.plan(expected)
provider.apply(plan)


def test_delegated_zone_creation(provider, empty_zone_name):
provider.conn.zone_type = 'zone_delegated'
expected = Zone(empty_zone_name, [])
source = YamlProvider('test', os.path.join(os.path.dirname(__file__), 'config'))
source.populate(expected)
assert len(expected.records) == 0
zone = Zone(empty_zone_name, [])
provider.populate(zone)
plan = provider.plan(expected)
provider.apply(plan)


def test_ipv4_zone(provider, new_ipv4_zone):
expected = Zone(new_ipv4_zone, [])
source = YamlProvider('test', os.path.join(os.path.dirname(__file__), 'config'))
Expand Down

0 comments on commit b14791a

Please sign in to comment.