Compare commits

...
12 Commits
Author SHA1 Message Date
Sven Luethi 8c4bc91f6e modified: .gitignore 2025-11-23 15:56:23 +01:00
Sven Luethi c8e28d2397 modified: README.md
modified:   src/netbox_plugin_bind_provisioner/__init__.py
modified:   src/netbox_plugin_bind_provisioner/management/commands/bind-transfer-endpoint.py
new file:   src/netbox_plugin_bind_provisioner/migrations/0001_initial.py
new file:   src/netbox_plugin_bind_provisioner/migrations/__init__.py
new file:   src/netbox_plugin_bind_provisioner/models.py

Adds a database model to track the catalog zone serial instead of tracking it inside a flat file.
2025-11-23 15:56:06 +01:00
Sven Luethi 0349204aff modified: README.md
In the previous commit Matt Kollross pointed out a naming error where the plugin settings were referenced as 'netbox_bind_provisioner' instead of the correct name 'netbox_plugin_bind_provisioner'.
His commit missed the second instance of the same mistake further down in the README however. This fixes that one too.
2025-11-18 20:51:37 +01:00
SL e4dbd7b014 Merge pull request #2 from kollross/main
Updated REAME.md with corrected info
2025-11-18 20:35:41 +01:00
Matt Kollross 07f8387963 Merge pull request #1 from kollross/kollross-patch-1
Change plugin key from netbox_bind_provisioner to netbox_plugin_bind_…
2025-11-18 10:26:30 -06:00
Matt Kollross d8acd3f525 Change plugin key from netbox_bind_provisioner to netbox_plugin_bind_provisioner
The PLUGIN_CONFIG needs to reference the correct name of the plugin "netbox_plugin_bind_provisioner" not "netbox_bind_provisioner"
2025-11-18 10:25:55 -06:00
Sven Luethi 8c1e9502f4 modified: README.md 2025-11-17 22:44:06 +01:00
Sven Luthi 86457cc858 modified: README.md
Add catalog zone serial counter info.
2025-11-12 13:52:13 +01:00
SL 0bad0e31df Update requirements.txt
Adds dnspython as dependency instead of relying indirectly on netbox-plugin-dns to require it.
2025-10-30 11:30:52 +01:00
Sven Luethi 7b6556cf01 modified: pyproject.toml
renamed:    src/netbox_bind_provisioner/__init__.py -> src/netbox_plugin_bind_provisioner/__init__.py
renamed:    src/netbox_bind_provisioner/management/commands/bind-transfer-endpoint.py -> src/netbox_plugin_bind_provisioner/management/commands/bind-transfer-endpoint.py
2025-10-05 03:54:42 +02:00
Sven Luethi 61c83a368c modified: pyproject.toml
renamed:    src/netbox_plugin_bind_provisioner/__init__.py -> src/netbox_bind_provisioner/__init__.py
renamed:    src/netbox_plugin_bind_provisioner/management/commands/bind-transfer-endpoint.py -> src/netbox_bind_provisioner/management/commands/bind-transfer-endpoint.py
2025-10-05 03:39:19 +02:00
Sven Luethi 86ad3f3fc7 modified: pyproject.toml
Add Hompage URL

modified:   src/netbox_plugin_bind_provisioner/__init__.py
bump version 0.9.3
2025-10-05 02:56:47 +02:00
9 changed files with 287 additions and 178 deletions

No files matched your search

+1
View File
@@ -1,2 +1,3 @@
src/netbox_plugin_bind_provisioner.egg-info src/netbox_plugin_bind_provisioner.egg-info
dist/ dist/
__pycache__
+178 -94
View File
@@ -1,124 +1,208 @@
# Netbox Bind Provisioner # Netbox Bind Provisioner
A Netbox Plugin that provides a minimal DNS Server for the automatic A NetBox plugin that implements a lightweight DNS server to automate the
provisioning of a Bind9 Server from Netbox DNS data (netbox-plugin-dns). provisioning and configuration of a BIND9 server based on DNS records
maintained in NetBox. (netbox-plugin-dns).
## Plugin configuration ## Plugin configuration
The plugin was re-worked; Instead of exporting zone files, it provides an The plugin has been reworked: instead of exporting zone files, it now provides
entire (though minimal) DNS Server that is fed directly from Netbox's DNS data. a minimal DNS server that is fed directly from NetBox’s DNS data. This server
The Server also provides specialized "catalog" zones that bind uses to also exposes specialized catalog zones that BIND uses to automatically discover
automatically discover new zones and remove deleted ones. The plugin supports newly created zones and remove deleted ones. The plugin supports BIND views and
view as well as basic dns security using TSIG. basic DNS security via TSIG.
To work correctly, each view needs a tsig key installed and the The plugin exposes one catalog zone per view. Each catalog zone is available
bind-transfer-endpoint needs to be running as its own service in the background under the special zone name **"catz"** and can be queried through the built-in
via the `manage.py` command. Note that dnssec support will be implemented as DNS server like any other zone.
soon as bind9 has a mechanism to allow configuration of such via the Catalog
Zones mechanism. For proper operation, each view requires an installed TSIG key, and the
`bind-transfer-endpoint` must be running as a separate background service using
the `manage.py` command. Note that DNSSEC support will be added once BIND9
provides a mechanism to configure it through the Catalog Zones system.
To start the service:
``` ```
venv/bin/python3 netbox/netbox/manage.py bind-transfer-endpoint --port 5354 manage.py bind-transfer-endpoint --port 5354
``` ```
### Parameters When configuring the plugin, you need to provide a path on the filesystem for
this file where the plugin can create and manage it. Please do not update it by
hand or remove it unless you know what you are doing.
### Service parameters
Parameter | Description Parameter | Description
--------- | -------------------------------------------------------- --------- | -------------------------------------------------------------------
--port | Port to listen on for requests (defaults to 5354) --port | Port to listen on for requests (defaults to 5354)
--address | IP of interface to bind to (defaults to 0.0.0.0) --address | IP of interface to bind to (defaults to 0.0.0.0)
``` ### Plugin settings
PLUGINS_CONFIG = { Setting | Description
"netbox_bind_provisioner": { --------------------| ---------------------------------------------------------
"tsig_keys": { catalog_serial_file | !!!NO LONGER USED!!! Replaced by DB entry.
"key1name": { tsig_keys | Maps a TSIG Key to be used for each view.
"view": "public",
"keyname": "view1key", ## Installation guide
"algorithm": "hmac-sha256", This setup provisions a BIND9 server directly with DNS data from NetBox.
"secret": "base64-encoded-secret" BIND9 can optionally run on a separate server. If so, any reference to
}, 127.0.0.1 in step 6 must be replaced with the IP address of the NetBox host.
"key2name": { TCP and UDP traffic from the BIND9 server to the NetBox host must be allowed on
"view": "private", port 5354 (or the port you have configured).
"keyname": "view2key",
"algorithm": "hmac-sha256", This guide assumes:
"secret": "base64-encoded-secret" - Netbox has been installed under /opt/netbox
- Bind9 is installed on the same host as Netbox
- The Netbox DNS Plugin netbox-plugin-dns is installed
- The following dns views exist in Netbox DNS:
- `public` (the default)
- `private`
1. Preliminaries
- Install Bind9 on the same host that netbox is on.
- Generate a TSIG Key for the `public` and `private` dns views respectively.
2. Adding required package
```
cd netbox
echo netbox-plugin-bind-provisioner >> local_requirements.txt
. venv/bin/activate
pip install -r local_requirements.txt
```
3. Updating netbox plugin configuration (configuration.py)
Change following line from
```
PLUGINS = ['netbox_dns']
```
to
```
PLUGINS = ['netbox_dns', 'netbox_plugin_bind_provisioner']
```
Configure the Bind Exporter Plugin using the PLUGINS_CONFIG dictionary.
Change
```
PLUGINS_CONFIG = {}
```
to
```
PLUGINS_CONFIG = {
"netbox_plugin_bind_provisioner": {
"tsig_keys": {
"public": {
"keyname": "public_view_key",
"algorithm": "hmac-sha256",
"secret": "base64-encoded-secret"
},
"private": {
"keyname": "private_view_key",
"algorithm": "hmac-sha256",
"secret": "base64-encoded-secret"
}
} }
}, }
} }
} ```
``` Note that the tsig-key attributes keyname, algorithm and secret form a
dictionary in following python structure path:
```
PLUGINS_CONFIG.netbox_plugin_bind_provisioner.tsig_keys.<dns_view_name>
```
This allows the plugin to map requests to the right dns view using the tsig
signature from each request.
## Bind configuration 4. Run migrations (This step is not yet required. Just adding for completeness)
``` ```
options { python3 netbox/manage.py migrate
... ```
...
allow-update { none; };
allow-query { any; };
allow-recursion { none; };
notify yes;
...
...
};
# ACLs 5. Start listener
This step runs the DNS endpoint used by bind to configure itself. You may want
to write a service wrapper that runs this in the background. Dont forget to
activate the venv if you do decide to run this service in the background.
Note that `--port 5354` is optional. The listener will bind this port
by default.
```
python3 netbox/manage.py bind-transfer-endpoint --port 5354
```
acl public { 6. Configuring a Bind9 to interact with Netbox via the bind-transfer-endpoint
!10.0.0.0/8; endpoint. Note that its not possible to give all the correct details of the
!172.16.0.0/12; `options` block as it is heavily dependent on the Operating System used.
!192.168.0.0/16; Please dont forget to adjust as required.
any;
};
acl private { ```
10.0.0.0/8; ########## OPTIONS ##########
172.16.0.0/12;
192.168.0.0/16;
};
# Zone definitions options {
allow-update { none; };
view "public" { allow-query { any; };
key "key1name" { allow-recursion { none; };
algorithm hmac-sha256; notify yes;
secret "base64-encoded-secret"; min-refresh-time 60;
}; };
match-clients { public; }; ########## ACLs ##########
catalog-zones { acl public {
zone "catz" !10.0.0.0/8;
default-masters { 127.0.0.1 port 5354 key "key1name"; } !172.16.0.0/12;
zone-directory "/var/lib/bind/zones" !192.168.0.0/16;
min-update-interval 1; any;
}; };
zone "catz" { acl private {
type slave; 10.0.0.0/8;
file "/var/lib/bind/zones/catz_public"; 172.16.0.0/12;
masters { 127.0.0.1 port 5354 key "key1name"; }; 192.168.0.0/16;
notify no;
};
};
view "private" {
key "key2name" {
algorithm hmac-sha256;
secret "base64-encoded-secret";
}; };
match-clients { private; }; ########## ZONES ##########
catalog-zones { view "public" {
zone "catz" key "public_view_key" {
default-masters { 127.0.0.1 port 5354 key "key2name"; } algorithm hmac-sha256;
zone-directory "/var/lib/bind/zones" secret "base64-encoded-secret";
min-update-interval 1; };
match-clients { public; };
catalog-zones {
zone "catz"
default-masters { 127.0.0.1 port 5354 key "public_view_key"; }
zone-directory "/var/lib/bind/zones"
min-update-interval 1;
};
zone "catz" {
type slave;
file "/var/lib/bind/zones/catz_public";
masters { 127.0.0.1 port 5354 key "public_view_key"; };
notify no;
};
}; };
zone "catz" { view "private" {
type slave; key "private_view_key" {
file "/var/lib/bind/zones/catz_private"; algorithm hmac-sha256;
masters { 127.0.0.1 port 5354 key "key2name"; }; secret "base64-encoded-secret";
notify no; };
match-clients { private; };
catalog-zones {
zone "catz"
default-masters { 127.0.0.1 port 5354 key "private_view_key"; }
zone-directory "/var/lib/bind/zones"
min-update-interval 1;
};
zone "catz" {
type slave;
file "/var/lib/bind/zones/catz_private";
masters { 127.0.0.1 port 5354 key "private_view_key"; };
notify no;
};
}; };
}; ```
```
7. Restart bind - Done
+3
View File
@@ -25,3 +25,6 @@ where = ["src"]
[tool.setuptools.dynamic] [tool.setuptools.dynamic]
version = { attr = "netbox_plugin_bind_provisioner.__version__" } version = { attr = "netbox_plugin_bind_provisioner.__version__" }
[project.urls]
Homepage = "https://github.com/Suraxius/netbox-plugin-bind-provisioner"
Issues = "https://github.com/Suraxius/netbox-plugin-bind-provisioner/issues"
+1
View File
@@ -1 +1,2 @@
netbox-plugin-dns>=1.2.0 netbox-plugin-dns>=1.2.0
dnspython>=2.8.0
+2 -21
View File
@@ -5,7 +5,7 @@ from django.utils.translation import gettext_lazy as _
from django.dispatch import receiver from django.dispatch import receiver
from django.conf import settings from django.conf import settings
__version__ = "0.9.2" __version__ = "0.9.6"
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -17,7 +17,7 @@ logging.basicConfig(
class BindProvisionerConfig(PluginConfig): class BindProvisionerConfig(PluginConfig):
name = "netbox_bind_provisioner" name = "netbox_plugin_bind_provisioner"
verbose_name = "Netbox Bind Provisioner" verbose_name = "Netbox Bind Provisioner"
description = "Provisions Zones to a Bind Server configured as hidden master" description = "Provisions Zones to a Bind Server configured as hidden master"
version = __version__ version = __version__
@@ -32,24 +32,5 @@ class BindProvisionerConfig(PluginConfig):
f"{self.name}: Plugin {self.verbose_name} failed to initialize due to missing settings. Terminating Netbox." f"{self.name}: Plugin {self.verbose_name} failed to initialize due to missing settings. Terminating Netbox."
) )
# from . import bind, rndc
#
# if not bind.zoneDirWritable():
# BIND_ZONE_DIR = settings.get("BIND_ZONE_DIR")
# logger.error(f"Bind's zone file directory {BIND_ZONE_DIR} is not writable.")
# raise RuntimeError(
# "Plugin initialization failed due to failed writable-zone-directory healthcheck."
# )
# if not rndc.tsigKeyFileAccessible():
# RNDC_KEY_FILE = settings.get("RNDC_KEY_FILE")
# logger.error(f"The RNDC key file {RNDC_KEY_FILE} is inaccessible.")
# raise RuntimeError(
# "Plugin initialization failed due to failed rndc-key-accessible healthcheck."
# )
#
# # Import signal callbacks from signals.py (With @receiver decorations)
# from . import signals
config = BindProvisionerConfig config = BindProvisionerConfig
default_app_config = ".apps.NetboxBindProvisionerConfig" default_app_config = ".apps.NetboxBindProvisionerConfig"
@@ -20,70 +20,44 @@ from django.utils import timezone
from netbox_dns.models import Zone, Record, View from netbox_dns.models import Zone, Record, View
from netbox_dns.choices import ZoneStatusChoices, RecordStatusChoices from netbox_dns.choices import ZoneStatusChoices, RecordStatusChoices
from datetime import datetime from datetime import datetime
from netbox_plugin_bind_provisioner.models import IntegerKeyValueSetting
logger = logging.getLogger('bind-transfer-endpoint') logger = logging.getLogger('bind-transfer-endpoint')
class CatalogZoneAssembler(): class CatalogZone():
lock = threading.Lock() lock = threading.Lock()
_serial_max = 0xFFFFFFFF _serial_max = 0xFFFFFFFF
_serial = 1 _serial_obj = None
_serial_file = None
_previous_last_zone_update = None _previous_last_zone_update = None
# Following function loads the last serial from file. No return value # Following function loads the last serial from the DB. No return value
# but it sets the setting "catalog_serial" or terminates the plugin on failure. # but it sets the setting "catalog_serial" or terminates the plugin on failure.
@classmethod @classmethod
def initCatalogZoneSerial(cls, serial_file): def initSerial(cls):
""" try:
Load a 32-bit unsigned int serial from a text file. cls._serial_obj = IntegerKeyValueSetting.objects.get(key="catalog-zone-soa-serial")
If the file doesn't exist, initialize it with 1. logger.debug(f"Catalog zone SOA serial number {cls._serial_obj.value} loaded from database")
Returns the serial as an int. except IntegerKeyValueSetting.DoesNotExist:
""" cls._serial_obj = IntegerKeyValueSetting.objects.create(key="catalog-zone-soa-serial", value=1)
cls._serial_file = serial_file logger.debug(f"Catalog zone SOA serial number was not set in the database. Set to {cls._serial_obj.value}")
if not os.path.exists(cls._serial_file):
try:
with open(cls._serial_file, "w") as f:
f.write(f"{cls._serial}\n")
return
except OSError as e:
raise RuntimeError(
f"Failed to initialize serial file {cls._serial_file}: {e}"
)
else:
try:
with open(cls._serial_file, "r") as f:
value = int(f.read().strip())
if 0 < value <= cls._serial_max:
cls._serial = value
else:
raise ValueError(f"Serial value out of 32-bit range: {value}")
except (OSError, ValueError) as e:
raise RuntimeError(f"Failed to read serial from {cls._serial_file}: {e}")
@classmethod @classmethod
def incrementCatalogZoneSerial(cls): def _incrementSerial(cls):
""" if 0 < cls._serial_obj.value < cls._serial_max:
Increments the in-memory catalog serial and saves it to file. cls._serial_obj.value += 1
Updates _serial. cls._serial_obj.save()
"""
if 0 < cls._serial < cls._serial_max:
cls._serial += 1
else: else:
logger.warn( logger.warn(
f"Failed to incremenet catalog serial {cls._serial}. Will overflow serial back to 1." f"Failed to incremenet catalog serial {cls._serial_obj.value}. Will overflow serial back to 1"
) )
cls._serial = 1 cls._serial_obj = 1
cls._serial_obj.save()
logger.debug(f"Catalog zone SOA serial number incremented to {_serial_obj.value}")
try:
with open(cls._serial_file, "w") as f:
f.write(f"{cls._serial}\n")
except OSError as e:
logger.error(f"Failed to write updated serial to {cls._serial_file}: {e}")
@classmethod @classmethod
def createCatalogZone(cls, view_name, soa_only=False): def create(cls, view_name, soa_only=False):
# Synchronize following across threads as TCP and UDP listener both use it. # Synchronize following across threads as TCP and UDP listener both use it.
with cls.lock: with cls.lock:
#cls = self.__class__ #cls = self.__class__
@@ -102,7 +76,7 @@ class CatalogZoneAssembler():
) )
# Setting previous last zone update for next iteration: # Setting previous last zone update for next iteration:
cls._previous_last_zone_update = last_zone_update cls._previous_last_zone_update = last_zone_update
cls.incrementCatalogZoneSerial() cls._incrementSerial()
# If there was no update, do nothing. # If there was no update, do nothing.
#else: #else:
# logger.debug(f"No need to update serial for catalog zone") # logger.debug(f"No need to update serial for catalog zone")
@@ -159,7 +133,7 @@ class CatalogZoneAssembler():
rtype = dns.rdatatype.SOA rtype = dns.rdatatype.SOA
mname = dns.name.root # dns.name.from_text("invalid.", origin) mname = dns.name.root # dns.name.from_text("invalid.", origin)
rname = dns.name.root # dns.name.from_text("contact.master.", origin) rname = dns.name.root # dns.name.from_text("contact.master.", origin)
serial = cls._serial serial = cls._serial_obj.value
refresh = 60 refresh = 60
retry = 10 retry = 10
expire = 1209600 expire = 1209600
@@ -208,6 +182,19 @@ class CatalogZoneAssembler():
return zone return zone
@classmethod
def importLegacySerialFile(cls, file_path):
if os.path.exists(file_path):
try:
with open(file_path, "r") as f:
serial = int(f.read().strip())
cls._serial_obj.value = int(serial)
cls._serial_obj.save()
logger.info(f"Legacy catalog serial file {file_path} imported into database and file removed. Serial is now {serial}")
os.remove(file_path)
except OSError as e:
logger.error(f"Failed to import catalog zone serial from legacy file {file_path}")
class DNSBaseRequestHandler(socketserver.BaseRequestHandler): class DNSBaseRequestHandler(socketserver.BaseRequestHandler):
# getZoneFromNB rewritten # getZoneFromNB rewritten
@@ -356,7 +343,7 @@ class UDPRequestHandler(DNSBaseRequestHandler):
# Check if catalog zone # Check if catalog zone
if dname == "catz": if dname == "catz":
zone = CatalogZoneAssembler.createCatalogZone(nb_view.name, soa_only=True) zone = CatalogZone.create(nb_view.name, soa_only=True)
# If generic zone, retreive from NB # If generic zone, retreive from NB
else: else:
zone = self.getZoneFromNB(dname, nb_view, soa_only=True) zone = self.getZoneFromNB(dname, nb_view, soa_only=True)
@@ -471,8 +458,7 @@ class TCPRequestHandler(DNSBaseRequestHandler):
# Check if catalog zone # Check if catalog zone
if dname == "catz": if dname == "catz":
#zone = self.createCatalogZone(str(nb_view)) zone = CatalogZone.create(nb_view.name)
zone = CatalogZoneAssembler.createCatalogZone(nb_view.name)
else: else:
zone = self.getZoneFromNB(dname, nb_view) zone = self.getZoneFromNB(dname, nb_view)
@@ -598,7 +584,7 @@ class Command(BaseCommand):
help = "Run a minimal AXFR DNS server using NetBox DNS plugin data" help = "Run a minimal AXFR DNS server using NetBox DNS plugin data"
def loadSettings(self): def loadSettings(self):
self.settings = settings.PLUGINS_CONFIG.get("netbox_bind_provisioner", None) self.settings = settings.PLUGINS_CONFIG.get("netbox_plugin_bind_provisioner", None)
if not self.settings: if not self.settings:
raise RuntimeError( raise RuntimeError(
"Command failed to initialize due to missing settings. Terminating Netbox." "Command failed to initialize due to missing settings. Terminating Netbox."
@@ -610,17 +596,8 @@ class Command(BaseCommand):
"tsig_keys variable not set in plugin settings." "tsig_keys variable not set in plugin settings."
) )
def loadCatalogZoneSettings(self): # Load TSIG keys and map them to views
serial_file = self.settings.get("catalog_serial_file", None)
if not serial_file:
raise RuntimeError(
"catalog_serial_file variable not set in plugin settings."
)
else:
CatalogZoneAssembler.initCatalogZoneSerial(serial_file)
def loadTSIGKeySettings(self): def loadTSIGKeySettings(self):
# Load TSIG keys and map them to views
self.keyring = {} self.keyring = {}
self.tsig_view_map = {} self.tsig_view_map = {}
@@ -668,10 +645,15 @@ class Command(BaseCommand):
# Load parameters # Load parameters
port = options['port'] port = options['port']
address = options['address'] address = options['address']
CatalogZone.initSerial()
# Initialize settings # Initialize settings
self.loadSettings() self.loadSettings()
self.loadCatalogZoneSettings() #self.loadCatalogZoneSettings()
self.loadTSIGKeySettings() self.loadTSIGKeySettings()
# Following is temporary. Function and invocation can be deleted once v1.0 is reached.
serial_file = self.settings.get("catalog_serial_file", None)
if serial_file:
CatalogZone.importLegacySerialFile(serial_file)
udp_server = UDPDNSServer((address, port), UDPRequestHandler, self.keyring, self.tsig_view_map) udp_server = UDPDNSServer((address, port), UDPRequestHandler, self.keyring, self.tsig_view_map)
tcp_server = TCPDNSServer((address, port), TCPRequestHandler, self.keyring, self.tsig_view_map) tcp_server = TCPDNSServer((address, port), TCPRequestHandler, self.keyring, self.tsig_view_map)
@@ -0,0 +1,34 @@
# Generated by Django 5.2.8 on 2025-11-23 14:36
import netbox.models.deletion
import taggit.managers
import utilities.json
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('extras', '0133_make_cf_minmax_decimal'),
]
operations = [
migrations.CreateModel(
name='IntegerKeyValueSetting',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
('created', models.DateTimeField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder)),
('key', models.CharField(max_length=64)),
('value', models.IntegerField()),
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
],
options={
'abstract': False,
},
bases=(netbox.models.deletion.DeleteMixin, models.Model),
),
]
Whitespace-only changes.
@@ -0,0 +1,23 @@
from django.db import models
import netbox.models
import netbox_dns.models
#class CatalogZoneSerial(models.Model):
class IntegerKeyValueSetting(netbox.models.NetBoxModel):
#view = models.ForeignKey(netbox_dns.models.View, on_delete=models.CASCADE)
key = models.CharField(max_length=64)
value = models.IntegerField()
def __str__(self):
#return f'{self.view.name}: {self.serial}'
return f'{self.key}: {str(self.value)}'
#class TSIGKey(netbox.models.NetBoxModel):
# view = models.ForeignKey(netbox_dns.models.View, on_delete=models.CASCADE)
# name = models.CharField(max_length=256)
# algorithm = models.CharField(max_length=25)
# secret = models.CharField(max_length=50)
#
# def __str__(self):
# return f'key "{self.name}" {{ algorithm {self.algorithm}; secret "{self.secret}"; }};'