Re-structure for packaging
This commit is contained in:
@@ -7,3 +7,4 @@ Manage DNS zones and dynamic records.
|
|||||||
* [Bind-Tools](https://www.isc.org/bind/) (if not bundled with Bind)
|
* [Bind-Tools](https://www.isc.org/bind/) (if not bundled with Bind)
|
||||||
* [dnspython](https://www.dnspython.org)
|
* [dnspython](https://www.dnspython.org)
|
||||||
* [prettytable](https://zetcode.com/python/prettytable/)
|
* [prettytable](https://zetcode.com/python/prettytable/)
|
||||||
|
* [pyyaml](https://pyyaml.org)
|
||||||
|
|||||||
42
pyproject.toml
Normal file
42
pyproject.toml
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
[build-system]
|
||||||
|
requires = ["setuptools >= 77.0.3"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[tool.setuptools.dynamic]
|
||||||
|
version = {attr = "dnsmgr.__version__"}
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "dnsmgr"
|
||||||
|
dynamic = ["version"]
|
||||||
|
dependencies = [
|
||||||
|
"dnspython>=2.8.0",
|
||||||
|
"prettytable>=3.17.0",
|
||||||
|
"pyyaml>=6.0.3"
|
||||||
|
]
|
||||||
|
requires-python = ">=3.11"
|
||||||
|
authors = [
|
||||||
|
{name = "Thomas Oettli", email = "spacefreak@noop.ch"}
|
||||||
|
]
|
||||||
|
maintainers = [
|
||||||
|
{name = "Thomas Oettli", email = "spacefreak@noop.ch"}
|
||||||
|
]
|
||||||
|
description = "Manage DNS zones and dynamic records."
|
||||||
|
readme = "README.md"
|
||||||
|
license = "GPL-3.0-only"
|
||||||
|
keywords = ["dns"]
|
||||||
|
classifiers = [
|
||||||
|
"Development Status :: 4 - Beta",
|
||||||
|
"Topic :: Internet :: Name Service (DNS)",
|
||||||
|
"Intended Audience :: System Administrators",
|
||||||
|
"Programming Language :: Python :: 3"
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
dns-confgen = "dnsmgr.confgen:main"
|
||||||
|
dns-list-zones = "dnsmgr.list-zones:main"
|
||||||
|
dns-record-add = "dnsmgr.record-add:main"
|
||||||
|
dns-record-delete = "dnsmgr.record-delete:main"
|
||||||
|
dns-zone-add = "dnsmgr.zone-add:main"
|
||||||
|
dns-zone-delete = "dnsmgr.zone-delete:main"
|
||||||
|
dns-zone-list = "dnsmgr.zone-list:main"
|
||||||
|
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
import dns.name
|
import dns.name
|
||||||
import dns.tsig
|
import dns.tsig
|
||||||
import dns.rcode
|
import dns.rcode
|
||||||
@@ -20,6 +18,8 @@ from hashlib import sha1
|
|||||||
from prettytable import PrettyTable
|
from prettytable import PrettyTable
|
||||||
from shutil import chown
|
from shutil import chown
|
||||||
|
|
||||||
|
__version__ = "1.0.0"
|
||||||
|
__author__ = "Thomas Oettli <spacefreak@noop.ch>"
|
||||||
|
|
||||||
DEFAULT_CFGFILE = '/etc/dns-manager/config.yml'
|
DEFAULT_CFGFILE = '/etc/dns-manager/config.yml'
|
||||||
|
|
||||||
15
dns-confgen → src/dnsmgr/confgen.py
Executable file → Normal file
15
dns-confgen → src/dnsmgr/confgen.py
Executable file → Normal file
@@ -1,19 +1,18 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import dnsmgr
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
from . import DEFAULT_CFGFILE, DNSManager, printe
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser(description='Generate Bind config files.')
|
parser = argparse.ArgumentParser(description='Generate Bind config files.')
|
||||||
parser.add_argument('-c', '--config', help='path to config file', default=dnsmgr.DEFAULT_CFGFILE)
|
parser.add_argument('-c', '--config', help='path to config file', default=DEFAULT_CFGFILE)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
manager = dnsmgr.DNSManager(cfgfile=args.config)
|
manager = DNSManager(cfgfile=args.config)
|
||||||
except RuntimeError as e:
|
except RuntimeError as e:
|
||||||
dnsmgr.printe(f'config: {e}')
|
printe(f'config: {e}')
|
||||||
sys.exit(100)
|
sys.exit(100)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -25,7 +24,7 @@ def main():
|
|||||||
print('OK')
|
print('OK')
|
||||||
|
|
||||||
except RuntimeError as e:
|
except RuntimeError as e:
|
||||||
dnsmgr.printe(e)
|
printe(e)
|
||||||
sys.exit(150)
|
sys.exit(150)
|
||||||
|
|
||||||
print('Reloading named... ', end='')
|
print('Reloading named... ', end='')
|
||||||
@@ -34,7 +33,7 @@ def main():
|
|||||||
manager.named_reload()
|
manager.named_reload()
|
||||||
print('OK')
|
print('OK')
|
||||||
except RuntimeError as e:
|
except RuntimeError as e:
|
||||||
dnsmgr.printe(e)
|
printe(e)
|
||||||
sys.exit(170)
|
sys.exit(170)
|
||||||
|
|
||||||
|
|
||||||
14
dns-list-zones → src/dnsmgr/list-zones.py
Executable file → Normal file
14
dns-list-zones → src/dnsmgr/list-zones.py
Executable file → Normal file
@@ -1,9 +1,7 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import dnsmgr
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
from . import DEFAULT_CFGFILE, DNSManager, printe, prettytable
|
||||||
from json import dumps
|
from json import dumps
|
||||||
|
|
||||||
|
|
||||||
@@ -13,7 +11,7 @@ def main():
|
|||||||
formatter_class=lambda prog: argparse.HelpFormatter(
|
formatter_class=lambda prog: argparse.HelpFormatter(
|
||||||
prog, max_help_position=45, width=140))
|
prog, max_help_position=45, width=140))
|
||||||
parser.add_argument('-a', '--all-zones', help='do not ignore zones that are not managed', action='store_true')
|
parser.add_argument('-a', '--all-zones', help='do not ignore zones that are not managed', action='store_true')
|
||||||
parser.add_argument('-c', '--config', help='path to config file', default=dnsmgr.DEFAULT_CFGFILE)
|
parser.add_argument('-c', '--config', help='path to config file', default=DEFAULT_CFGFILE)
|
||||||
parser.add_argument('-d', '--decode', help='decode internationalized domain names (IDN)', action='store_true')
|
parser.add_argument('-d', '--decode', help='decode internationalized domain names (IDN)', action='store_true')
|
||||||
output = parser.add_mutually_exclusive_group()
|
output = parser.add_mutually_exclusive_group()
|
||||||
output.add_argument('-j', '--json', help='print json format', action='store_true')
|
output.add_argument('-j', '--json', help='print json format', action='store_true')
|
||||||
@@ -22,15 +20,15 @@ def main():
|
|||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
manager = dnsmgr.DNSManager(cfgfile=args.config)
|
manager = DNSManager(cfgfile=args.config)
|
||||||
except RuntimeError as e:
|
except RuntimeError as e:
|
||||||
dnsmgr.printe(f'config: {e}')
|
printe(f'config: {e}')
|
||||||
sys.exit(100)
|
sys.exit(100)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
zones = manager.all_zones if args.all_zones else manager.zones
|
zones = manager.all_zones if args.all_zones else manager.zones
|
||||||
except RuntimeError as e:
|
except RuntimeError as e:
|
||||||
dnsmgr.printe(e)
|
printe(e)
|
||||||
sys.exit(150)
|
sys.exit(150)
|
||||||
|
|
||||||
zones.sort(key=lambda zone: zone.origin.to_unicode() if args.decode else zone.origin.to_text())
|
zones.sort(key=lambda zone: zone.origin.to_unicode() if args.decode else zone.origin.to_text())
|
||||||
@@ -63,7 +61,7 @@ def main():
|
|||||||
if args.all_zones:
|
if args.all_zones:
|
||||||
row.append(zone.cfgfile is not None)
|
row.append(zone.cfgfile is not None)
|
||||||
rows.append(row)
|
rows.append(row)
|
||||||
print(dnsmgr.prettytable(field_names, rows))
|
print(prettytable(field_names, rows))
|
||||||
print(f'\nTotal: {len(rows)}\n')
|
print(f'\nTotal: {len(rows)}\n')
|
||||||
|
|
||||||
|
|
||||||
49
dns-record-add → src/dnsmgr/record-add.py
Executable file → Normal file
49
dns-record-add → src/dnsmgr/record-add.py
Executable file → Normal file
@@ -1,11 +1,24 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import dns.rdataclass
|
import dns.rdataclass
|
||||||
import dns.rdataset
|
import dns.rdataset
|
||||||
import dnsmgr
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
from . import (
|
||||||
|
DEFAULT_CFGFILE,
|
||||||
|
NAMED_DEFAULT_VIEW,
|
||||||
|
DNSManager,
|
||||||
|
input_name,
|
||||||
|
input_rdata,
|
||||||
|
input_ttl,
|
||||||
|
input_yes_no,
|
||||||
|
name_from_text,
|
||||||
|
printe,
|
||||||
|
rdata_from_text,
|
||||||
|
select_type,
|
||||||
|
ttl_from_text,
|
||||||
|
type_from_text
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
preparser = argparse.ArgumentParser(add_help=False)
|
preparser = argparse.ArgumentParser(add_help=False)
|
||||||
@@ -18,7 +31,7 @@ def main():
|
|||||||
parser.add_argument('-a', '--all-zones', help='allow zones that are not managed', action='store_true')
|
parser.add_argument('-a', '--all-zones', help='allow zones that are not managed', action='store_true')
|
||||||
parser.add_argument('-A', '--all-types', help='allow unsupported record types', action='store_true')
|
parser.add_argument('-A', '--all-types', help='allow unsupported record types', action='store_true')
|
||||||
parser.add_argument('-b', '--batch', help='run in batch mode (no user input)', action='store_true')
|
parser.add_argument('-b', '--batch', help='run in batch mode (no user input)', action='store_true')
|
||||||
parser.add_argument('-c', '--config', help='path to config file', default=dnsmgr.DEFAULT_CFGFILE)
|
parser.add_argument('-c', '--config', help='path to config file', default=DEFAULT_CFGFILE)
|
||||||
parser.add_argument('zone', metavar='ZONE[@VIEWS]', nargs=nargs, help='DNS zone name and optional list of views (comma separated or asterisk to select all views)', default=None)
|
parser.add_argument('zone', metavar='ZONE[@VIEWS]', nargs=nargs, help='DNS zone name and optional list of views (comma separated or asterisk to select all views)', default=None)
|
||||||
parser.add_argument('name', metavar='NAME', nargs=nargs, help='DNS record name', default=None)
|
parser.add_argument('name', metavar='NAME', nargs=nargs, help='DNS record name', default=None)
|
||||||
parser.add_argument('ttl', metavar='TTL', nargs=nargs, help='DNS record TTL in seconds', type=int, default=None)
|
parser.add_argument('ttl', metavar='TTL', nargs=nargs, help='DNS record TTL in seconds', type=int, default=None)
|
||||||
@@ -27,9 +40,9 @@ def main():
|
|||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
manager = dnsmgr.DNSManager(cfgfile=args.config)
|
manager = DNSManager(cfgfile=args.config)
|
||||||
except RuntimeError as e:
|
except RuntimeError as e:
|
||||||
dnsmgr.printe(f'config: {e}')
|
printe(f'config: {e}')
|
||||||
sys.exit(100)
|
sys.exit(100)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -41,27 +54,27 @@ def main():
|
|||||||
origin = zones[0].origin
|
origin = zones[0].origin
|
||||||
|
|
||||||
if args.name is None:
|
if args.name is None:
|
||||||
name = dnsmgr.input_name(origin, prompt='Record name')
|
name = input_name(origin, prompt='Record name')
|
||||||
else:
|
else:
|
||||||
name = dnsmgr.name_from_text(args.name, origin)
|
name = name_from_text(args.name, origin)
|
||||||
|
|
||||||
if args.ttl is None:
|
if args.ttl is None:
|
||||||
ttl = dnsmgr.input_ttl()
|
ttl = input_ttl()
|
||||||
else:
|
else:
|
||||||
ttl = dnsmgr.ttl_from_text(args.ttl)
|
ttl = ttl_from_text(args.ttl)
|
||||||
|
|
||||||
if args.type is None:
|
if args.type is None:
|
||||||
rdtype = dnsmgr.select_type(args.all_types)
|
rdtype = select_type(args.all_types)
|
||||||
else:
|
else:
|
||||||
rdtype = dnsmgr.type_from_text(args.type, args.all_types)
|
rdtype = type_from_text(args.type, args.all_types)
|
||||||
|
|
||||||
if not args.value:
|
if not args.value:
|
||||||
rdata = dnsmgr.input_rdata(rdtype, origin)
|
rdata = input_rdata(rdtype, origin)
|
||||||
else:
|
else:
|
||||||
rdata = dnsmgr.rdata_from_text(rdtype, ' '.join(args.value), origin)
|
rdata = rdata_from_text(rdtype, ' '.join(args.value), origin)
|
||||||
|
|
||||||
except RuntimeError as e:
|
except RuntimeError as e:
|
||||||
dnsmgr.printe(e)
|
printe(e)
|
||||||
sys.exit(150)
|
sys.exit(150)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
@@ -75,12 +88,12 @@ def main():
|
|||||||
print(f'View: {zone.view}')
|
print(f'View: {zone.view}')
|
||||||
print(f'\033[32m+ {name} {text}\033[0m\n')
|
print(f'\033[32m+ {name} {text}\033[0m\n')
|
||||||
|
|
||||||
if not dnsmgr.input_yes_no():
|
if not input_yes_no():
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
for zone in zones:
|
for zone in zones:
|
||||||
origin = zone.origin.to_text(omit_final_dot=True)
|
origin = zone.origin.to_text(omit_final_dot=True)
|
||||||
if len(zones) > 1 or zone.view != dnsmgr.NAMED_DEFAULT_VIEW:
|
if len(zones) > 1 or zone.view != NAMED_DEFAULT_VIEW:
|
||||||
origin = f'{origin}@{zone.view}'
|
origin = f'{origin}@{zone.view}'
|
||||||
print(f"Sending DDNS updates for '{origin}'... ", end='')
|
print(f"Sending DDNS updates for '{origin}'... ", end='')
|
||||||
|
|
||||||
@@ -88,7 +101,7 @@ def main():
|
|||||||
manager.add_zone_record(zone, name, rdataset)
|
manager.add_zone_record(zone, name, rdataset)
|
||||||
print('OK')
|
print('OK')
|
||||||
except RuntimeError as e:
|
except RuntimeError as e:
|
||||||
dnsmgr.printe(e)
|
printe(e)
|
||||||
sys.exit(160)
|
sys.exit(160)
|
||||||
|
|
||||||
|
|
||||||
46
dns-record-delete → src/dnsmgr/record-delete.py
Executable file → Normal file
46
dns-record-delete → src/dnsmgr/record-delete.py
Executable file → Normal file
@@ -1,10 +1,20 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import dns.rdataclass
|
import dns.rdataclass
|
||||||
import dnsmgr
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
from . import (
|
||||||
|
DEFAULT_CFGFILE,
|
||||||
|
NAMED_DEFAULT_VIEW,
|
||||||
|
RECORD_TYPES,
|
||||||
|
DNSManager,
|
||||||
|
input_yes_no,
|
||||||
|
name_from_text,
|
||||||
|
prettyselect,
|
||||||
|
printe,
|
||||||
|
rdata_from_text,
|
||||||
|
type_from_text,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
preparser = argparse.ArgumentParser(add_help=False)
|
preparser = argparse.ArgumentParser(add_help=False)
|
||||||
@@ -16,7 +26,7 @@ def main():
|
|||||||
parser.add_argument('-a', '--all-zones', help='allow zones that are not managed', action='store_true')
|
parser.add_argument('-a', '--all-zones', help='allow zones that are not managed', action='store_true')
|
||||||
parser.add_argument('-A', '--all-types', help='allow unsupported record types', action='store_true')
|
parser.add_argument('-A', '--all-types', help='allow unsupported record types', action='store_true')
|
||||||
parser.add_argument('-b', '--batch', help='run in batch mode (no user input)', action='store_true')
|
parser.add_argument('-b', '--batch', help='run in batch mode (no user input)', action='store_true')
|
||||||
parser.add_argument('-c', '--config', help='path to config file', default=dnsmgr.DEFAULT_CFGFILE)
|
parser.add_argument('-c', '--config', help='path to config file', default=DEFAULT_CFGFILE)
|
||||||
parser.add_argument('zone', metavar='ZONE[@VIEWS]', nargs=nargs, help='DNS zone name and optional list of views (comma separated or asterisk to select all views)', default=None)
|
parser.add_argument('zone', metavar='ZONE[@VIEWS]', nargs=nargs, help='DNS zone name and optional list of views (comma separated or asterisk to select all views)', default=None)
|
||||||
parser.add_argument('name', metavar='NAME', nargs=nargs, help='DNS record name', default=None)
|
parser.add_argument('name', metavar='NAME', nargs=nargs, help='DNS record name', default=None)
|
||||||
parser.add_argument('type', metavar='TYPE', nargs=nargs, help='DNS record type', default=None)
|
parser.add_argument('type', metavar='TYPE', nargs=nargs, help='DNS record type', default=None)
|
||||||
@@ -24,9 +34,9 @@ def main():
|
|||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
manager = dnsmgr.DNSManager(cfgfile=args.config)
|
manager = DNSManager(cfgfile=args.config)
|
||||||
except RuntimeError as e:
|
except RuntimeError as e:
|
||||||
dnsmgr.printe(f'config: {e}')
|
printe(f'config: {e}')
|
||||||
sys.exit(100)
|
sys.exit(100)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -43,10 +53,10 @@ def main():
|
|||||||
if args.name is None:
|
if args.name is None:
|
||||||
names = sorted(set([name.to_unicode() for name in zone for zone in zones]))
|
names = sorted(set([name.to_unicode() for name in zone for zone in zones]))
|
||||||
rows = [[name] for name in names]
|
rows = [[name] for name in names]
|
||||||
index = dnsmgr.prettyselect(['Record name'], rows, prompt='Select record name')
|
index = prettyselect(['Record name'], rows, prompt='Select record name')
|
||||||
args.name = names[index]
|
args.name = names[index]
|
||||||
|
|
||||||
name = dnsmgr.name_from_text(args.name, origin)
|
name = name_from_text(args.name, origin)
|
||||||
|
|
||||||
for zone in zones:
|
for zone in zones:
|
||||||
zone.filter_by_name(name, origin)
|
zone.filter_by_name(name, origin)
|
||||||
@@ -58,13 +68,13 @@ def main():
|
|||||||
if args.type is None:
|
if args.type is None:
|
||||||
rdtypes = sorted(set([rdataset.rdtype for rdataset in zone.get_node(name) for zone in zones]))
|
rdtypes = sorted(set([rdataset.rdtype for rdataset in zone.get_node(name) for zone in zones]))
|
||||||
if not args.all_types:
|
if not args.all_types:
|
||||||
rdtypes = list(filter(lambda rdtype: rdtype in dnsmgr.RECORD_TYPES, rdtypes))
|
rdtypes = list(filter(lambda rdtype: rdtype in RECORD_TYPES, rdtypes))
|
||||||
rdtypes = [rdtype.to_text(rdtype) for rdtype in rdtypes]
|
rdtypes = [rdtype.to_text(rdtype) for rdtype in rdtypes]
|
||||||
rows = [[rdtype] for rdtype in rdtypes]
|
rows = [[rdtype] for rdtype in rdtypes]
|
||||||
index = dnsmgr.prettyselect(['Record type'], rows, prompt='Select record type')
|
index = prettyselect(['Record type'], rows, prompt='Select record type')
|
||||||
args.type = rdtypes[index]
|
args.type = rdtypes[index]
|
||||||
|
|
||||||
rdtype = dnsmgr.type_from_text(args.type, args.all_types)
|
rdtype = type_from_text(args.type, args.all_types)
|
||||||
|
|
||||||
for zone in zones:
|
for zone in zones:
|
||||||
zone.filter_by_rdtype(rdtype)
|
zone.filter_by_rdtype(rdtype)
|
||||||
@@ -74,7 +84,7 @@ def main():
|
|||||||
raise RuntimeError(f"No such {rdtype.to_text(rdtype)} record -- '{name.to_text(True)}'")
|
raise RuntimeError(f"No such {rdtype.to_text(rdtype)} record -- '{name.to_text(True)}'")
|
||||||
|
|
||||||
rdata = None
|
rdata = None
|
||||||
if not args.value and not args.batch and not dnsmgr.input_yes_no(f'Delete all {rdtype.to_text(rdtype)}-records?'):
|
if not args.value and not args.batch and not input_yes_no(f'Delete all {rdtype.to_text(rdtype)}-records?'):
|
||||||
values = []
|
values = []
|
||||||
for zone in zones:
|
for zone in zones:
|
||||||
for rdataset in zone.get_node(name):
|
for rdataset in zone.get_node(name):
|
||||||
@@ -82,12 +92,12 @@ def main():
|
|||||||
values.append(rdata.to_text(origin=zone.origin, relativize=False))
|
values.append(rdata.to_text(origin=zone.origin, relativize=False))
|
||||||
values = sorted(set(values))
|
values = sorted(set(values))
|
||||||
rows = [[value] for value in values]
|
rows = [[value] for value in values]
|
||||||
index = dnsmgr.prettyselect(['Record value'], rows, prompt='Select record value', truncate=True)
|
index = prettyselect(['Record value'], rows, prompt='Select record value', truncate=True)
|
||||||
args.value = [values[index]]
|
args.value = [values[index]]
|
||||||
|
|
||||||
if args.value:
|
if args.value:
|
||||||
value = ' '.join(args.value)
|
value = ' '.join(args.value)
|
||||||
rdata = dnsmgr.rdata_from_text(rdtype, value, origin)
|
rdata = rdata_from_text(rdtype, value, origin)
|
||||||
for zone in zones:
|
for zone in zones:
|
||||||
zone.filter_by_rdata(rdata)
|
zone.filter_by_rdata(rdata)
|
||||||
|
|
||||||
@@ -96,7 +106,7 @@ def main():
|
|||||||
raise RuntimeError(f"No such DNS record found -- {name.to_text(True)} IN {rdtype.to_text(rdtype)} {value}")
|
raise RuntimeError(f"No such DNS record found -- {name.to_text(True)} IN {rdtype.to_text(rdtype)} {value}")
|
||||||
|
|
||||||
except RuntimeError as e:
|
except RuntimeError as e:
|
||||||
dnsmgr.printe(e)
|
printe(e)
|
||||||
sys.exit(150)
|
sys.exit(150)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
@@ -115,12 +125,12 @@ def main():
|
|||||||
print(f'\033[31m- {name} {rdataset.ttl} {rdclassstr} {rdtypestr} {text}\033[0m')
|
print(f'\033[31m- {name} {rdataset.ttl} {rdclassstr} {rdtypestr} {text}\033[0m')
|
||||||
print()
|
print()
|
||||||
|
|
||||||
if not dnsmgr.input_yes_no():
|
if not input_yes_no():
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
for zone in zones:
|
for zone in zones:
|
||||||
origin = zone.origin.to_text(omit_final_dot=True)
|
origin = zone.origin.to_text(omit_final_dot=True)
|
||||||
if len(zones) > 1 or zone.view != dnsmgr.NAMED_DEFAULT_VIEW:
|
if len(zones) > 1 or zone.view != NAMED_DEFAULT_VIEW:
|
||||||
origin = f'{origin}@{zone.view}'
|
origin = f'{origin}@{zone.view}'
|
||||||
print(f"Sending DDNS updates for '{origin}'... ", end='')
|
print(f"Sending DDNS updates for '{origin}'... ", end='')
|
||||||
|
|
||||||
@@ -131,7 +141,7 @@ def main():
|
|||||||
manager.delete_zone_record(zone, name, rdataset)
|
manager.delete_zone_record(zone, name, rdataset)
|
||||||
print('OK')
|
print('OK')
|
||||||
except RuntimeError as e:
|
except RuntimeError as e:
|
||||||
dnsmgr.printe(e)
|
printe(e)
|
||||||
sys.exit(160)
|
sys.exit(160)
|
||||||
|
|
||||||
|
|
||||||
46
dns-zone-add → src/dnsmgr/zone-add.py
Executable file → Normal file
46
dns-zone-add → src/dnsmgr/zone-add.py
Executable file → Normal file
@@ -1,12 +1,20 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import dns.rdataclass
|
import dns.rdataclass
|
||||||
import dns.rdataset
|
import dns.rdataset
|
||||||
import dns.rdatatype
|
import dns.rdatatype
|
||||||
import dnsmgr
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
from . import (
|
||||||
|
DEFAULT_CFGFILE,
|
||||||
|
NAMED_DEFAULT_VIEW,
|
||||||
|
DNSManager,
|
||||||
|
input_name,
|
||||||
|
input_yes_no,
|
||||||
|
name_views_from_text,
|
||||||
|
prettyselect,
|
||||||
|
printe,
|
||||||
|
rdata_from_text,
|
||||||
|
)
|
||||||
from time import sleep
|
from time import sleep
|
||||||
|
|
||||||
|
|
||||||
@@ -18,32 +26,32 @@ def main():
|
|||||||
|
|
||||||
parser = argparse.ArgumentParser(description='Add DNS zones.')
|
parser = argparse.ArgumentParser(description='Add DNS zones.')
|
||||||
parser.add_argument('-b', '--batch', help='run in batch mode (no user input)', action='store_true')
|
parser.add_argument('-b', '--batch', help='run in batch mode (no user input)', action='store_true')
|
||||||
parser.add_argument('-c', '--config', help='path to config file', default=dnsmgr.DEFAULT_CFGFILE)
|
parser.add_argument('-c', '--config', help='path to config file', default=DEFAULT_CFGFILE)
|
||||||
parser.add_argument('-t', '--config-template', help='config file/template (overrides value set in ZONE_TEMPLATES config option)', default=None)
|
parser.add_argument('-t', '--config-template', help='config file/template (overrides value set in ZONE_TEMPLATES config option)', default=None)
|
||||||
parser.add_argument('-z', '--zone-template', help='zone file/template (overrides value set in ZONE_TEMPLATES config option)', default=None)
|
parser.add_argument('-z', '--zone-template', help='zone file/template (overrides value set in ZONE_TEMPLATES config option)', default=None)
|
||||||
parser.add_argument('zone', metavar='ZONE[@VIEWS]', nargs=nargs, help='DNS zone name and optional list of views (comma separated or asterisk to select all views)', default=None)
|
parser.add_argument('zone', metavar='ZONE[@VIEWS]', nargs=nargs, help='DNS zone name and optional list of views (comma separated or asterisk to select all views)', default=None)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
manager = dnsmgr.DNSManager(cfgfile=args.config)
|
manager = DNSManager(cfgfile=args.config)
|
||||||
except RuntimeError as e:
|
except RuntimeError as e:
|
||||||
dnsmgr.printe(f'config: {e}')
|
printe(f'config: {e}')
|
||||||
sys.exit(100)
|
sys.exit(100)
|
||||||
|
|
||||||
managed_views = sorted(manager.config.zones_config.keys())
|
managed_views = sorted(manager.config.zones_config.keys())
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if args.zone is None:
|
if args.zone is None:
|
||||||
name = dnsmgr.input_name()
|
name = input_name()
|
||||||
rows = [[view] for view in managed_views]
|
rows = [[view] for view in managed_views]
|
||||||
index = dnsmgr.prettyselect(['View'], rows, prompt='Select view', also_valid=['*'])
|
index = prettyselect(['View'], rows, prompt='Select view', also_valid=['*'])
|
||||||
views = managed_views if index == '*' else [managed_views[index]]
|
views = managed_views if index == '*' else [managed_views[index]]
|
||||||
else:
|
else:
|
||||||
(name, views) = dnsmgr.name_views_from_text(args.zone)
|
(name, views) = name_views_from_text(args.zone)
|
||||||
if views is None:
|
if views is None:
|
||||||
if len(managed_views) > 1:
|
if len(managed_views) > 1:
|
||||||
raise RuntimeError('multiple managed views configured but none specified')
|
raise RuntimeError('multiple managed views configured but none specified')
|
||||||
elif managed_views[0] != dnsmgr.NAMED_DEFAULT_VIEW:
|
elif managed_views[0] != NAMED_DEFAULT_VIEW:
|
||||||
raise RuntimeError('the default view is not managed')
|
raise RuntimeError('the default view is not managed')
|
||||||
views = managed_views
|
views = managed_views
|
||||||
elif views == '*':
|
elif views == '*':
|
||||||
@@ -59,7 +67,7 @@ def main():
|
|||||||
raise RuntimeError(f'zone already exists in view {views}')
|
raise RuntimeError(f'zone already exists in view {views}')
|
||||||
|
|
||||||
except RuntimeError as e:
|
except RuntimeError as e:
|
||||||
dnsmgr.printe(e)
|
printe(e)
|
||||||
sys.exit(150)
|
sys.exit(150)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
@@ -70,13 +78,13 @@ def main():
|
|||||||
print(f'View: {view}')
|
print(f'View: {view}')
|
||||||
print(f'\033[32m+ {origin}\033[0m\n')
|
print(f'\033[32m+ {origin}\033[0m\n')
|
||||||
|
|
||||||
if not dnsmgr.input_yes_no():
|
if not input_yes_no():
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
zones = []
|
zones = []
|
||||||
for view in views:
|
for view in views:
|
||||||
origin = name.to_text(omit_final_dot=True)
|
origin = name.to_text(omit_final_dot=True)
|
||||||
if len(views) > 1 or view != dnsmgr.NAMED_DEFAULT_VIEW:
|
if len(views) > 1 or view != NAMED_DEFAULT_VIEW:
|
||||||
origin = f'{origin}@{view}'
|
origin = f'{origin}@{view}'
|
||||||
print(f"Adding zone '{origin}'... ", end='')
|
print(f"Adding zone '{origin}'... ", end='')
|
||||||
|
|
||||||
@@ -87,7 +95,7 @@ def main():
|
|||||||
if manager.config.zones_config[view].catalog_zone:
|
if manager.config.zones_config[view].catalog_zone:
|
||||||
zones.append(zone)
|
zones.append(zone)
|
||||||
except RuntimeError as e:
|
except RuntimeError as e:
|
||||||
dnsmgr.printe(e)
|
printe(e)
|
||||||
sys.exit(160)
|
sys.exit(160)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -95,7 +103,7 @@ def main():
|
|||||||
manager.named_reload()
|
manager.named_reload()
|
||||||
print('OK')
|
print('OK')
|
||||||
except RuntimeError as e:
|
except RuntimeError as e:
|
||||||
dnsmgr.printe(e)
|
printe(e)
|
||||||
sys.exit(170)
|
sys.exit(170)
|
||||||
|
|
||||||
if zones:
|
if zones:
|
||||||
@@ -109,16 +117,16 @@ def main():
|
|||||||
raise RuntimeError(f'catalog zone of view \'{zone.view}\': {e}')
|
raise RuntimeError(f'catalog zone of view \'{zone.view}\': {e}')
|
||||||
|
|
||||||
origin = zone.origin.to_text(omit_final_dot=True)
|
origin = zone.origin.to_text(omit_final_dot=True)
|
||||||
if len(zones) > 1 or zone.view != dnsmgr.NAMED_DEFAULT_VIEW:
|
if len(zones) > 1 or zone.view != NAMED_DEFAULT_VIEW:
|
||||||
origin = f'{origin}@{zone.view}'
|
origin = f'{origin}@{zone.view}'
|
||||||
|
|
||||||
for catalog_zone in catalog_zones:
|
for catalog_zone in catalog_zones:
|
||||||
rdata = dnsmgr.rdata_from_text(dns.rdatatype.PTR, zone.origin.to_text(), catalog_zone.origin)
|
rdata = rdata_from_text(dns.rdatatype.PTR, zone.origin.to_text(), catalog_zone.origin)
|
||||||
rdataset = dns.rdataset.Rdataset(dns.rdataclass.IN, dns.rdatatype.PTR, ttl=3600)
|
rdataset = dns.rdataset.Rdataset(dns.rdataclass.IN, dns.rdatatype.PTR, ttl=3600)
|
||||||
rdataset.add(rdata)
|
rdataset.add(rdata)
|
||||||
rdname = dns.name.from_text(zone.nfz() + '.zones', catalog_zone.origin)
|
rdname = dns.name.from_text(zone.nfz() + '.zones', catalog_zone.origin)
|
||||||
catalog_zone_origin = catalog_zone.origin.to_text(omit_final_dot=True)
|
catalog_zone_origin = catalog_zone.origin.to_text(omit_final_dot=True)
|
||||||
if catalog_zone.view != dnsmgr.NAMED_DEFAULT_VIEW:
|
if catalog_zone.view != NAMED_DEFAULT_VIEW:
|
||||||
catalog_zone_origin += f'@{catalog_zone.view}'
|
catalog_zone_origin += f'@{catalog_zone.view}'
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -126,7 +134,7 @@ def main():
|
|||||||
manager.add_zone_record(catalog_zone, rdname, rdataset)
|
manager.add_zone_record(catalog_zone, rdname, rdataset)
|
||||||
print('OK')
|
print('OK')
|
||||||
except RuntimeError as e:
|
except RuntimeError as e:
|
||||||
dnsmgr.printe(e)
|
printe(e)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
27
dns-zone-delete → src/dnsmgr/zone-delete.py
Executable file → Normal file
27
dns-zone-delete → src/dnsmgr/zone-delete.py
Executable file → Normal file
@@ -1,12 +1,11 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import dns.rdataclass
|
import dns.rdataclass
|
||||||
import dns.rdataset
|
import dns.rdataset
|
||||||
import dns.rdatatype
|
import dns.rdatatype
|
||||||
import dnsmgr
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
from . import DEFAULT_CFGFILE, NAMED_DEFAULT_VIEW, DNSManager, printe, input_yes_no
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
preparser = argparse.ArgumentParser(add_help=False)
|
preparser = argparse.ArgumentParser(add_help=False)
|
||||||
@@ -16,14 +15,14 @@ def main():
|
|||||||
|
|
||||||
parser = argparse.ArgumentParser(description='Delete DNS zones.')
|
parser = argparse.ArgumentParser(description='Delete DNS zones.')
|
||||||
parser.add_argument('-b', '--batch', help='run in batch mode (no user input)', action='store_true')
|
parser.add_argument('-b', '--batch', help='run in batch mode (no user input)', action='store_true')
|
||||||
parser.add_argument('-c', '--config', help='path to config file', default=dnsmgr.DEFAULT_CFGFILE)
|
parser.add_argument('-c', '--config', help='path to config file', default=DEFAULT_CFGFILE)
|
||||||
parser.add_argument('zone', metavar='ZONE[@VIEWS]', nargs=nargs, help='DNS zone name and optional list of views (comma separated or asterisk to select all views)', default=None)
|
parser.add_argument('zone', metavar='ZONE[@VIEWS]', nargs=nargs, help='DNS zone name and optional list of views (comma separated or asterisk to select all views)', default=None)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
manager = dnsmgr.DNSManager(cfgfile=args.config)
|
manager = DNSManager(cfgfile=args.config)
|
||||||
except RuntimeError as e:
|
except RuntimeError as e:
|
||||||
dnsmgr.printe(f'config: {e}')
|
printe(f'config: {e}')
|
||||||
sys.exit(100)
|
sys.exit(100)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -32,7 +31,7 @@ def main():
|
|||||||
else:
|
else:
|
||||||
zones = manager.get_zones(args.zone)
|
zones = manager.get_zones(args.zone)
|
||||||
except RuntimeError as e:
|
except RuntimeError as e:
|
||||||
dnsmgr.printe(e)
|
printe(e)
|
||||||
sys.exit(150)
|
sys.exit(150)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
@@ -43,12 +42,12 @@ def main():
|
|||||||
print(f'View: {zone.view}')
|
print(f'View: {zone.view}')
|
||||||
print(f'\033[31m- {origin}\033[0m\n')
|
print(f'\033[31m- {origin}\033[0m\n')
|
||||||
|
|
||||||
if not dnsmgr.input_yes_no():
|
if not input_yes_no():
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
for zone in zones:
|
for zone in zones:
|
||||||
origin = zone.origin.to_text(omit_final_dot=True)
|
origin = zone.origin.to_text(omit_final_dot=True)
|
||||||
if len(zones) > 1 or zone.view != dnsmgr.NAMED_DEFAULT_VIEW:
|
if len(zones) > 1 or zone.view != NAMED_DEFAULT_VIEW:
|
||||||
origin = f'{origin}@{zone.view}'
|
origin = f'{origin}@{zone.view}'
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -72,7 +71,7 @@ def main():
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
catalog_zone_origin = catalog_zone.origin.to_text(omit_final_dot=True)
|
catalog_zone_origin = catalog_zone.origin.to_text(omit_final_dot=True)
|
||||||
if catalog_zone.view != dnsmgr.NAMED_DEFAULT_VIEW:
|
if catalog_zone.view != NAMED_DEFAULT_VIEW:
|
||||||
catalog_zone_origin += f'@{catalog_zone.view}'
|
catalog_zone_origin += f'@{catalog_zone.view}'
|
||||||
print(f'Removing zone \'{origin}\' from catalog zone \'{catalog_zone_origin}\'... ', end='')
|
print(f'Removing zone \'{origin}\' from catalog zone \'{catalog_zone_origin}\'... ', end='')
|
||||||
manager.delete_zone_record(catalog_zone, rdname, rdataset)
|
manager.delete_zone_record(catalog_zone, rdname, rdataset)
|
||||||
@@ -82,7 +81,7 @@ def main():
|
|||||||
manager.delete_zone(zone)
|
manager.delete_zone(zone)
|
||||||
print('OK')
|
print('OK')
|
||||||
except RuntimeError as e:
|
except RuntimeError as e:
|
||||||
dnsmgr.printe(e)
|
printe(e)
|
||||||
sys.exit(160)
|
sys.exit(160)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -90,12 +89,12 @@ def main():
|
|||||||
manager.named_reload()
|
manager.named_reload()
|
||||||
print('OK')
|
print('OK')
|
||||||
except RuntimeError as e:
|
except RuntimeError as e:
|
||||||
dnsmgr.printe(e)
|
printe(e)
|
||||||
sys.exit(170)
|
sys.exit(170)
|
||||||
|
|
||||||
for zone in zones:
|
for zone in zones:
|
||||||
origin = zone.origin.to_text(omit_final_dot=True)
|
origin = zone.origin.to_text(omit_final_dot=True)
|
||||||
if len(zones) > 1 or zone.view != dnsmgr.NAMED_DEFAULT_VIEW:
|
if len(zones) > 1 or zone.view != NAMED_DEFAULT_VIEW:
|
||||||
origin = f'{origin}@{zone.view}'
|
origin = f'{origin}@{zone.view}'
|
||||||
print(f"Cleanup zone files of zone '{origin}'... ", end='')
|
print(f"Cleanup zone files of zone '{origin}'... ", end='')
|
||||||
|
|
||||||
@@ -103,7 +102,7 @@ def main():
|
|||||||
manager.cleanup_zone(zone)
|
manager.cleanup_zone(zone)
|
||||||
print('OK')
|
print('OK')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
dnsmgr.printe(e)
|
printe(e)
|
||||||
sys.exit(180)
|
sys.exit(180)
|
||||||
|
|
||||||
|
|
||||||
20
dns-zone-list → src/dnsmgr/zone-list.py
Executable file → Normal file
20
dns-zone-list → src/dnsmgr/zone-list.py
Executable file → Normal file
@@ -1,10 +1,8 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import dnsmgr
|
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
from . import NAMED_DEFAULT_VIEW, RECORD_TYPES, DEFAULT_CFGFILE, DNSManager, printe, prettytable
|
||||||
from dns.reversename import ipv4_reverse_domain, ipv6_reverse_domain
|
from dns.reversename import ipv4_reverse_domain, ipv6_reverse_domain
|
||||||
from json import dumps
|
from json import dumps
|
||||||
|
|
||||||
@@ -19,7 +17,7 @@ def main():
|
|||||||
parser.add_argument('-a', '--all-zones', help='do not ignore zones that are not managed', action='store_true')
|
parser.add_argument('-a', '--all-zones', help='do not ignore zones that are not managed', action='store_true')
|
||||||
parser.add_argument('-A', '--all-records', help='do not ignore unsupported record types', action='store_true')
|
parser.add_argument('-A', '--all-records', help='do not ignore unsupported record types', action='store_true')
|
||||||
parser.add_argument('-b', '--batch', help='run in batch mode (no user input)', action='store_true')
|
parser.add_argument('-b', '--batch', help='run in batch mode (no user input)', action='store_true')
|
||||||
parser.add_argument('-c', '--config', help='path to config file', default=dnsmgr.DEFAULT_CFGFILE)
|
parser.add_argument('-c', '--config', help='path to config file', default=DEFAULT_CFGFILE)
|
||||||
parser.add_argument('-d', '--decode', help='decode internationalized domain names (IDN)', action='store_true')
|
parser.add_argument('-d', '--decode', help='decode internationalized domain names (IDN)', action='store_true')
|
||||||
parser.add_argument('zone', metavar='ZONE[@VIEWS]', nargs=nargs, help='DNS zone name and optional list of views (comma separated or asterisk to select all views)', default=None)
|
parser.add_argument('zone', metavar='ZONE[@VIEWS]', nargs=nargs, help='DNS zone name and optional list of views (comma separated or asterisk to select all views)', default=None)
|
||||||
output = parser.add_mutually_exclusive_group()
|
output = parser.add_mutually_exclusive_group()
|
||||||
@@ -29,9 +27,9 @@ def main():
|
|||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
manager = dnsmgr.DNSManager(cfgfile=args.config)
|
manager = DNSManager(cfgfile=args.config)
|
||||||
except RuntimeError as e:
|
except RuntimeError as e:
|
||||||
dnsmgr.printe(f'config: {e}')
|
printe(f'config: {e}')
|
||||||
sys.exit(100)
|
sys.exit(100)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -41,7 +39,7 @@ def main():
|
|||||||
zones = manager.get_zones(args.zone, args.all_zones)
|
zones = manager.get_zones(args.zone, args.all_zones)
|
||||||
|
|
||||||
except RuntimeError as e:
|
except RuntimeError as e:
|
||||||
dnsmgr.printe(e)
|
printe(e)
|
||||||
sys.exit(150)
|
sys.exit(150)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
@@ -53,13 +51,13 @@ def main():
|
|||||||
try:
|
try:
|
||||||
manager.get_zone_content(zone)
|
manager.get_zone_content(zone)
|
||||||
except RuntimeError as e:
|
except RuntimeError as e:
|
||||||
dnsmgr.printe(f"zone transfer of '{zone.origin.to_text(True)}@{zone.view}': {e}")
|
printe(f"zone transfer of '{zone.origin.to_text(True)}@{zone.view}': {e}")
|
||||||
sys.exit(160)
|
sys.exit(160)
|
||||||
|
|
||||||
records = []
|
records = []
|
||||||
for name, node in zone.items():
|
for name, node in zone.items():
|
||||||
for rdataset in node:
|
for rdataset in node:
|
||||||
if not args.all_records and rdataset.rdtype not in dnsmgr.RECORD_TYPES:
|
if not args.all_records and rdataset.rdtype not in RECORD_TYPES:
|
||||||
continue
|
continue
|
||||||
for value in rdataset:
|
for value in rdataset:
|
||||||
records.append({
|
records.append({
|
||||||
@@ -101,9 +99,9 @@ def main():
|
|||||||
row = [record['name'], record['ttl'], record['type'], record['value']]
|
row = [record['name'], record['ttl'], record['type'], record['value']]
|
||||||
rows.append(row)
|
rows.append(row)
|
||||||
|
|
||||||
if len(views) > 1 or view != dnsmgr.NAMED_DEFAULT_VIEW:
|
if len(views) > 1 or view != NAMED_DEFAULT_VIEW:
|
||||||
print(f'View: {view}')
|
print(f'View: {view}')
|
||||||
print(dnsmgr.prettytable(field_names, rows, truncate=True))
|
print(prettytable(field_names, rows, truncate=True))
|
||||||
print()
|
print()
|
||||||
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user