Compare commits
3 commits
e27f8938a5
...
59a9b01fef
Author | SHA1 | Date | |
---|---|---|---|
59a9b01fef | |||
6612daeee7 | |||
792b9d5429 |
4 changed files with 100 additions and 113 deletions
|
@ -1,11 +1,10 @@
|
||||||
---
|
---
|
||||||
? ''
|
? ''
|
||||||
:
|
: ttl: 172800
|
||||||
ttl: 172800
|
|
||||||
type: NS
|
type: NS
|
||||||
values:
|
values:
|
||||||
- ns1.example.com.
|
- ns1.example.com.
|
||||||
- ns2.example.com.
|
- ns2.example.com.
|
||||||
ns1:
|
ns1:
|
||||||
type: A
|
type: A
|
||||||
value: 192.168.1.1
|
value: 192.168.1.1
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
---
|
---
|
||||||
? ''
|
? ''
|
||||||
:
|
: ttl: 172800
|
||||||
ttl: 172800
|
|
||||||
type: NS
|
type: NS
|
||||||
values:
|
values:
|
||||||
- ns1.example.com.
|
- ns1.example.com.
|
||||||
- ns2.example.com.
|
- ns2.example.com.
|
||||||
record1:
|
record1:
|
||||||
type: CNAME
|
type: CNAME
|
||||||
value: record11.test.example.com.
|
value: record11.test.example.com.
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
---
|
---
|
||||||
? ''
|
? ''
|
||||||
:
|
: ttl: 172800
|
||||||
ttl: 172800
|
|
||||||
type: NS
|
type: NS
|
||||||
values:
|
values:
|
||||||
- ns1.example.com.
|
- ns1.example.com.
|
||||||
- ns2.example.com.
|
- ns2.example.com.
|
||||||
record1:
|
record1:
|
||||||
type: CNAME
|
type: CNAME
|
||||||
value: record11.view.example.com.
|
value: record11.view.example.com.
|
||||||
|
@ -14,4 +13,4 @@ record11:
|
||||||
value: 192.168.1.11
|
value: 192.168.1.11
|
||||||
record12:
|
record12:
|
||||||
type: A
|
type: A
|
||||||
value: 192.168.1.12
|
value: 192.168.1.13
|
||||||
|
|
|
@ -11,9 +11,7 @@ import pynetbox.core.response
|
||||||
|
|
||||||
|
|
||||||
class NetBoxDNSSource(octodns.provider.base.BaseProvider):
|
class NetBoxDNSSource(octodns.provider.base.BaseProvider):
|
||||||
"""
|
"""OctoDNS provider for NetboxDNS"""
|
||||||
OctoDNS provider for NetboxDNS
|
|
||||||
"""
|
|
||||||
|
|
||||||
SUPPORTS_GEO = False
|
SUPPORTS_GEO = False
|
||||||
SUPPORTS_DYNAMIC = False
|
SUPPORTS_DYNAMIC = False
|
||||||
|
@ -62,9 +60,7 @@ class NetBoxDNSSource(octodns.provider.base.BaseProvider):
|
||||||
*args,
|
*args,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
"""
|
"""initialize the NetboxDNSSource"""
|
||||||
Initialize the NetboxDNSSource
|
|
||||||
"""
|
|
||||||
self.log = logging.getLogger(f"NetboxDNSSource[{id}]")
|
self.log = logging.getLogger(f"NetboxDNSSource[{id}]")
|
||||||
self.log.debug(f"__init__: {id=}, {url=}, {view=}, {replace_duplicates=}, {make_absolute=}")
|
self.log.debug(f"__init__: {id=}, {url=}, {view=}, {replace_duplicates=}, {make_absolute=}")
|
||||||
super().__init__(id, *args, **kwargs)
|
super().__init__(id, *args, **kwargs)
|
||||||
|
@ -76,8 +72,7 @@ class NetBoxDNSSource(octodns.provider.base.BaseProvider):
|
||||||
self.make_absolute = make_absolute
|
self.make_absolute = make_absolute
|
||||||
|
|
||||||
def _make_absolute(self, value: str) -> str:
|
def _make_absolute(self, value: str) -> str:
|
||||||
"""
|
"""return dns name with trailing dot to make it absolute
|
||||||
Return dns name with trailing dot to make it absolute
|
|
||||||
|
|
||||||
@param value: dns record value
|
@param value: dns record value
|
||||||
|
|
||||||
|
@ -92,8 +87,7 @@ class NetBoxDNSSource(octodns.provider.base.BaseProvider):
|
||||||
return absolute_value
|
return absolute_value
|
||||||
|
|
||||||
def _get_nb_view(self, view: str | None | Literal[False]) -> dict[str, int | str]:
|
def _get_nb_view(self, view: str | None | Literal[False]) -> dict[str, int | str]:
|
||||||
"""
|
"""get the correct netbox view when requested
|
||||||
Get the correct netbox view when requested
|
|
||||||
|
|
||||||
@param view: `False` for no view, `None` for zones without a view, else the view name
|
@param view: `False` for no view, `None` for zones without a view, else the view name
|
||||||
|
|
||||||
|
@ -115,8 +109,7 @@ class NetBoxDNSSource(octodns.provider.base.BaseProvider):
|
||||||
return {"view_id": nb_view.id}
|
return {"view_id": nb_view.id}
|
||||||
|
|
||||||
def _get_nb_zone(self, name: str, view: dict[str, str | int]) -> pynetbox.core.response.Record:
|
def _get_nb_zone(self, name: str, view: dict[str, str | int]) -> pynetbox.core.response.Record:
|
||||||
"""
|
"""given a zone name and a view name, look it up in NetBox.
|
||||||
Given a zone name and a view name, look it up in NetBox.
|
|
||||||
|
|
||||||
@param name: name of the dns zone
|
@param name: name of the dns zone
|
||||||
@param view: the netbox view id in the api query format
|
@param view: the netbox view id in the api query format
|
||||||
|
@ -132,15 +125,17 @@ class NetBoxDNSSource(octodns.provider.base.BaseProvider):
|
||||||
|
|
||||||
return nb_zone
|
return nb_zone
|
||||||
|
|
||||||
def _format_rdata(self, rdata: dns.rdata.Rdata, raw_value: str) -> str | dict[str, Any]:
|
def _format_rdata(
|
||||||
"""
|
self, nb_record: pynetbox.core.response.Record, raw_value: str
|
||||||
Format netbox record values to correct octodns record values
|
) -> str | dict[str, Any]:
|
||||||
|
"""format netbox record values to correct octodns record values
|
||||||
|
|
||||||
@param rdata: rrdata record value
|
@param nb_record: netbox record
|
||||||
@param raw_value: raw record value
|
@param raw_value: raw record value
|
||||||
|
|
||||||
@return: formatted rrdata value
|
@return: formatted rrdata value
|
||||||
"""
|
"""
|
||||||
|
rdata = dns.rdata.from_text("IN", nb_record.type, raw_value)
|
||||||
match rdata.rdtype.name:
|
match rdata.rdtype.name:
|
||||||
case "A" | "AAAA":
|
case "A" | "AAAA":
|
||||||
value = rdata.address
|
value = rdata.address
|
||||||
|
@ -221,8 +216,7 @@ class NetBoxDNSSource(octodns.provider.base.BaseProvider):
|
||||||
return value # type:ignore
|
return value # type:ignore
|
||||||
|
|
||||||
def _format_nb_records(self, zone: octodns.zone.Zone) -> list[dict[str, Any]]:
|
def _format_nb_records(self, zone: octodns.zone.Zone) -> list[dict[str, Any]]:
|
||||||
"""
|
"""format netbox dns records to the octodns format
|
||||||
Format netbox dns records to the octodns format
|
|
||||||
|
|
||||||
@param zone: octodns zone
|
@param zone: octodns zone
|
||||||
|
|
||||||
|
@ -255,9 +249,8 @@ class NetBoxDNSSource(octodns.provider.base.BaseProvider):
|
||||||
|
|
||||||
self.log.debug(f"record data={rcd_data}")
|
self.log.debug(f"record data={rcd_data}")
|
||||||
|
|
||||||
rdata = dns.rdata.from_text("IN", nb_record.type, raw_value)
|
|
||||||
try:
|
try:
|
||||||
rcd_value = self._format_rdata(rdata, raw_value)
|
rcd_value = self._format_rdata(nb_record, raw_value)
|
||||||
except NotImplementedError:
|
except NotImplementedError:
|
||||||
continue
|
continue
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
|
@ -273,12 +266,13 @@ class NetBoxDNSSource(octodns.provider.base.BaseProvider):
|
||||||
def populate(
|
def populate(
|
||||||
self, zone: octodns.zone.Zone, target: bool = False, lenient: bool = False
|
self, zone: octodns.zone.Zone, target: bool = False, lenient: bool = False
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""
|
"""get all the records of a zone from NetBox and add them to the OctoDNS zone
|
||||||
Get all the records of a zone from NetBox and add them to the OctoDNS zone
|
|
||||||
|
|
||||||
@param zone: octodns zone
|
@param zone: octodns zone
|
||||||
@param target: when `True`, load the current state of the provider.
|
@param target: when `True`, load the current state of the provider.
|
||||||
@param lenient: when `True`, skip record validation and do a "best effort" load of data.
|
@param lenient: when `True`, skip record validation and do a "best effort" load of data.
|
||||||
|
|
||||||
|
@return: true if the zone exists, else false.
|
||||||
"""
|
"""
|
||||||
self.log.info(f"populate -> '{zone.name}', target={target}, lenient={lenient}")
|
self.log.info(f"populate -> '{zone.name}', target={target}, lenient={lenient}")
|
||||||
|
|
||||||
|
@ -306,118 +300,114 @@ class NetBoxDNSSource(octodns.provider.base.BaseProvider):
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _include_change(self, change: octodns.record.change.Change) -> bool:
|
@staticmethod
|
||||||
"""Filter out record types which the provider can't create in netbox"""
|
def _format_changeset(change: Any) -> set[str]:
|
||||||
if change.new._type in ["SOA", "PTR", "NS"]:
|
"""format the changeset
|
||||||
|
|
||||||
|
@param change: the raw changes
|
||||||
|
|
||||||
|
@return: the formatted changeset
|
||||||
|
"""
|
||||||
|
match change:
|
||||||
|
case octodns.record.ValueMixin():
|
||||||
|
changeset = {repr(change.value)[1:-1]}
|
||||||
|
case octodns.record.ValuesMixin():
|
||||||
|
changeset = {repr(v)[1:-1] for v in change.values}
|
||||||
|
case _:
|
||||||
|
raise ValueError
|
||||||
|
|
||||||
|
return changeset
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _include_change(change: octodns.record.change.Change) -> bool:
|
||||||
|
"""filter out record types which the provider can't create in netbox
|
||||||
|
@param change: the planned change
|
||||||
|
|
||||||
|
@return: false if the change should be discarded, true if it should be kept.
|
||||||
|
"""
|
||||||
|
if change.record._type in ["SOA", "PTR", "NS"]:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _apply(self, plan: octodns.provider.plan.Plan) -> None:
|
def _apply(self, plan: octodns.provider.plan.Plan) -> None:
|
||||||
"""Apply the changes to the NetBox DNS zone."""
|
"""apply the changes to the NetBox DNS zone."""
|
||||||
self.log.debug(f"_apply: zone={plan.desired.name}, len(changes)={len(plan.changes)}")
|
self.log.debug(f"_apply: zone={plan.desired.name}, changes={len(plan.changes)}")
|
||||||
|
|
||||||
nb_zone = self._get_nb_zone(plan.desired.name, view=self.nb_view)
|
nb_zone = self._get_nb_zone(plan.desired.name, view=self.nb_view)
|
||||||
|
|
||||||
for change in plan.changes:
|
for change in plan.changes:
|
||||||
match change:
|
match change:
|
||||||
case octodns.record.Create():
|
case octodns.record.Create():
|
||||||
name = change.new.name
|
rcd_name = "@" if change.new.name == "" else change.new.name
|
||||||
if name == "":
|
|
||||||
name = "@"
|
|
||||||
|
|
||||||
match change.new:
|
new_changeset = self._format_changeset(change.new)
|
||||||
case octodns.record.ValueMixin():
|
for record in new_changeset:
|
||||||
new = {repr(change.new.value)[1:-1]}
|
nb_record: pynetbox.core.response.Record = (
|
||||||
case octodns.record.ValuesMixin():
|
self.api.plugins.netbox_dns.records.create(
|
||||||
new = {repr(v)[1:-1] for v in change.new.values}
|
zone=nb_zone.id,
|
||||||
case _:
|
name=rcd_name,
|
||||||
raise ValueError
|
type=change.new._type,
|
||||||
|
ttl=change.new.ttl,
|
||||||
for value in new:
|
value=record.replace("\\\\", "\\").replace("\\;", ";"),
|
||||||
nb_record = self.api.plugins.netbox_dns.records.create(
|
disable_ptr=True,
|
||||||
zone=nb_zone.id,
|
)
|
||||||
name=name,
|
|
||||||
type=change.new._type,
|
|
||||||
ttl=change.new.ttl,
|
|
||||||
value=value.replace("\\\\", "\\").replace("\\;", ";"),
|
|
||||||
disable_ptr=True,
|
|
||||||
)
|
)
|
||||||
self.log.debug(f"{nb_record!r}")
|
self.log.debug(f"{nb_record!r}")
|
||||||
|
|
||||||
case octodns.record.Delete():
|
case octodns.record.Delete():
|
||||||
name = change.existing.name
|
nb_records: pynetbox.core.response.RecordSet = (
|
||||||
if name == "":
|
self.api.plugins.netbox_dns.records.filter(
|
||||||
name = "@"
|
zone_id=nb_zone.id,
|
||||||
|
name=change.existing.name,
|
||||||
nb_records = self.api.plugins.netbox_dns.records.filter(
|
type=change.existing._type,
|
||||||
zone_id=nb_zone.id,
|
)
|
||||||
name=change.existing.name,
|
|
||||||
type=change.existing._type,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
match change.existing:
|
existing_changeset = self._format_changeset(change.existing)
|
||||||
case octodns.record.ValueMixin():
|
|
||||||
existing = {repr(change.existing.value)[1:-1]}
|
|
||||||
case octodns.record.ValuesMixin():
|
|
||||||
existing = {repr(v)[1:-1] for v in change.existing.values}
|
|
||||||
case _:
|
|
||||||
raise ValueError
|
|
||||||
|
|
||||||
for nb_record in nb_records:
|
for nb_record in nb_records:
|
||||||
for value in existing:
|
for record in existing_changeset:
|
||||||
if nb_record.value == value:
|
if nb_record.value == record:
|
||||||
self.log.debug(
|
self.log.debug(
|
||||||
f"{nb_record.id} {nb_record.name} {nb_record.type} {nb_record.value} {value}"
|
f"{nb_record.id} {nb_record.name} {nb_record.type} {nb_record.value} {record}"
|
||||||
)
|
)
|
||||||
self.log.debug(f"{nb_record.url} {nb_record.endpoint.url}")
|
self.log.debug(f"{nb_record.url} {nb_record.endpoint.url}")
|
||||||
nb_record.delete()
|
nb_record.delete()
|
||||||
|
|
||||||
case octodns.record.Update():
|
case octodns.record.Update():
|
||||||
name = change.existing.name
|
rcd_name = "@" if change.existing.name == "" else change.existing.name
|
||||||
if name == "":
|
|
||||||
name = "@"
|
|
||||||
|
|
||||||
nb_records = self.api.plugins.netbox_dns.records.filter(
|
nb_records: pynetbox.core.response.RecordSet = (
|
||||||
zone_id=nb_zone.id,
|
self.api.plugins.netbox_dns.records.filter(
|
||||||
name=name,
|
zone_id=nb_zone.id,
|
||||||
type=change.existing._type,
|
name=rcd_name,
|
||||||
|
type=change.existing._type,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
match change.existing:
|
existing_changeset = self._format_changeset(change.existing)
|
||||||
case octodns.record.ValueMixin():
|
new_changeset = self._format_changeset(change.new)
|
||||||
existing = {repr(change.existing.value)[1:-1]}
|
|
||||||
case octodns.record.ValuesMixin():
|
|
||||||
existing = {repr(v)[1:-1] for v in change.existing.values}
|
|
||||||
case _:
|
|
||||||
raise ValueError
|
|
||||||
|
|
||||||
match change.new:
|
to_delete = existing_changeset.difference(new_changeset)
|
||||||
case octodns.record.ValueMixin():
|
to_update = existing_changeset.intersection(new_changeset)
|
||||||
new = {repr(change.new.value)[1:-1]}
|
to_create = new_changeset.difference(existing_changeset)
|
||||||
case octodns.record.ValuesMixin():
|
|
||||||
new = {repr(v)[1:-1] for v in change.new.values}
|
|
||||||
case _:
|
|
||||||
raise ValueError
|
|
||||||
|
|
||||||
delete = existing.difference(new)
|
|
||||||
update = existing.intersection(new)
|
|
||||||
create = new.difference(existing)
|
|
||||||
|
|
||||||
for nb_record in nb_records:
|
for nb_record in nb_records:
|
||||||
if nb_record.value in delete:
|
if nb_record.value in to_delete:
|
||||||
nb_record.delete()
|
nb_record.delete()
|
||||||
if nb_record.value in update:
|
if nb_record.value in to_update:
|
||||||
nb_record.ttl = change.new.ttl
|
nb_record.ttl = change.new.ttl
|
||||||
nb_record.save()
|
nb_record.save()
|
||||||
|
|
||||||
for value in create:
|
for record in to_create:
|
||||||
nb_record = self.api.plugins.netbox_dns.records.create(
|
nb_record: pynetbox.core.response.Record = (
|
||||||
zone=nb_zone.id,
|
self.api.plugins.netbox_dns.records.create(
|
||||||
name=name,
|
zone=nb_zone.id,
|
||||||
type=change.new._type,
|
name=rcd_name,
|
||||||
ttl=change.new.ttl,
|
type=change.new._type,
|
||||||
value=value.replace("\\\\", "\\").replace("\\;", ";"),
|
ttl=change.new.ttl,
|
||||||
disable_ptr=True,
|
value=record.replace("\\\\", "\\").replace("\\;", ";"),
|
||||||
|
disable_ptr=True,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
self.log.debug(f"{nb_record!r}")
|
self.log.debug(f"{nb_record!r}")
|
||||||
|
|
Loading…
Reference in a new issue