Skip to content

Commit 986f60e

Browse files
[dns] Implement config and show commands for static DNS.
Implement unit tests for all added commands. Coverage for config/dns.py : 94% Coverage for show/dns.py : 86%
1 parent d5544b4 commit 986f60e

5 files changed

Lines changed: 325 additions & 0 deletions

File tree

config/dns.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
2+
import click
3+
from swsscommon.swsscommon import ConfigDBConnector
4+
from .validated_config_db_connector import ValidatedConfigDBConnector
5+
import ipaddress
6+
7+
8+
ADHOC_VALIDATION = True
9+
NAMESERVERS_MAX_NUM = 3
10+
11+
12+
def to_ip_address(address):
13+
"""Check if the given IP address is valid"""
14+
try:
15+
ip = ipaddress.ip_address(address)
16+
17+
if ADHOC_VALIDATION:
18+
if ip.is_reserved or ip.is_multicast or ip.is_loopback:
19+
return
20+
21+
invalid_ips = [
22+
ipaddress.IPv4Address('0.0.0.0'),
23+
ipaddress.IPv4Address('255.255.255.255'),
24+
ipaddress.IPv6Address("0::0"),
25+
ipaddress.IPv6Address("0::1")
26+
]
27+
if ip in invalid_ips:
28+
return
29+
30+
return ip
31+
except Exception:
32+
return
33+
34+
35+
def get_nameservers(db):
36+
nameservers = db.get_table('DNS_NAMESERVER')
37+
return [ipaddress.ip_address(ip) for ip in nameservers]
38+
39+
40+
# 'dns' group ('config dns ...')
41+
@click.group()
42+
@click.pass_context
43+
def dns(ctx):
44+
"""Static DNS configuration"""
45+
config_db = ValidatedConfigDBConnector(ConfigDBConnector())
46+
config_db.connect()
47+
ctx.obj = {'db': config_db}
48+
49+
50+
# dns nameserver config
51+
@dns.group('nameserver')
52+
@click.pass_context
53+
def nameserver(ctx):
54+
"""Static DNS nameservers configuration"""
55+
pass
56+
57+
58+
# dns nameserver add
59+
@nameserver.command('add')
60+
@click.argument('ip_address_str', metavar='<ip_address>', required=True)
61+
@click.pass_context
62+
def add_dns_nameserver(ctx, ip_address_str):
63+
"""Add static DNS nameserver entry"""
64+
ip_address = to_ip_address(ip_address_str)
65+
if not ip_address:
66+
ctx.fail(f"{ip_address_str} invalid nameserver ip address")
67+
68+
db = ctx.obj['db']
69+
70+
nameservers = get_nameservers(db)
71+
if ip_address in nameservers:
72+
ctx.fail(f"{ip_address} nameserver is already configured")
73+
74+
if len(nameservers) >= NAMESERVERS_MAX_NUM:
75+
ctx.fail(f"The maximum number ({NAMESERVERS_MAX_NUM}) of nameservers exceeded.")
76+
77+
db.set_entry('DNS_NAMESERVER', ip_address, {})
78+
79+
# dns nameserver delete
80+
@nameserver.command('del')
81+
@click.argument('ip_address_str', metavar='<ip_address>', required=True)
82+
@click.pass_context
83+
def del_dns_nameserver(ctx, ip_address_str):
84+
"""Delete static DNS nameserver entry"""
85+
86+
ip_address = to_ip_address(ip_address_str)
87+
if not ip_address:
88+
ctx.fail(f"{ip_address_str} invalid nameserver ip address")
89+
90+
db = ctx.obj['db']
91+
92+
nameservers = get_nameservers(db)
93+
if ip_address not in nameservers:
94+
ctx.fail(f"DNS nameserver {ip_address} is not configured")
95+
96+
db.set_entry('DNS_NAMESERVER', ip_address, None)

config/main.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
from .config_mgmt import ConfigMgmtDPB, ConfigMgmt
5555
from . import mclag
5656
from . import syslog
57+
from . import dns
5758

5859
# mock masic APIs for unit test
5960
try:
@@ -1200,6 +1201,9 @@ def config(ctx):
12001201
# syslog module
12011202
config.add_command(syslog.syslog)
12021203

1204+
# DNS module
1205+
config.add_command(dns.dns)
1206+
12031207
@config.command()
12041208
@click.option('-y', '--yes', is_flag=True, callback=_abort_if_false,
12051209
expose_value=False, prompt='Existing files will be overwritten, continue?')

show/dns.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import click
2+
import utilities_common.cli as clicommon
3+
from natsort import natsorted
4+
from tabulate import tabulate
5+
6+
from swsscommon.swsscommon import ConfigDBConnector
7+
from utilities_common.cli import pass_db
8+
9+
10+
# 'dns' group ("show dns ...")
11+
@click.group(cls=clicommon.AliasedGroup)
12+
@click.pass_context
13+
def dns(ctx):
14+
"""Show details of the static DNS configuration """
15+
config_db = ConfigDBConnector()
16+
config_db.connect()
17+
ctx.obj = {'db': config_db}
18+
19+
20+
# 'nameserver' subcommand ("show dns nameserver")
21+
@dns.command()
22+
@click.pass_context
23+
def nameserver(ctx):
24+
""" Show static DNS configuration """
25+
header = ["Nameserver"]
26+
db = ctx.obj['db']
27+
28+
nameservers = db.get_table('DNS_NAMESERVER')
29+
30+
click.echo(tabulate([(ns,) for ns in nameservers.keys()], header, tablefmt='simple', stralign='right'))

show/main.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
from . import warm_restart
6464
from . import plugins
6565
from . import syslog
66+
from . import dns
6667

6768
# Global Variables
6869
PLATFORM_JSON = 'platform.json'
@@ -289,6 +290,7 @@ def cli(ctx):
289290
cli.add_command(vxlan.vxlan)
290291
cli.add_command(system_health.system_health)
291292
cli.add_command(warm_restart.warm_restart)
293+
cli.add_command(dns.dns)
292294

293295
# syslog module
294296
cli.add_command(syslog.syslog)

tests/dns_test.py

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
import os
2+
import pytest
3+
4+
from click.testing import CliRunner
5+
6+
import config.main as config
7+
import show.main as show
8+
from utilities_common.db import Db
9+
10+
test_path = os.path.dirname(os.path.abspath(__file__))
11+
12+
dns_show_nameservers_header = """\
13+
Nameserver
14+
------------
15+
"""
16+
17+
dns_show_nameservers = """\
18+
Nameserver
19+
--------------------
20+
1.1.1.1
21+
2001:4860:4860::8888
22+
"""
23+
24+
class TestDns(object):
25+
26+
valid_nameservers = (
27+
("1.1.1.1",),
28+
("1.1.1.1", "8.8.8.8", "10.10.10.10",),
29+
("1.1.1.1", "2001:4860:4860::8888"),
30+
("2001:4860:4860::8888", "2001:4860:4860::8844", "2001:4860:4860::8800")
31+
)
32+
33+
invalid_nameservers = (
34+
"0.0.0.0",
35+
"255.255.255.255",
36+
"224.0.0.0",
37+
"0::0",
38+
"0::1",
39+
"1.1.1.x",
40+
"2001:4860:4860.8888",
41+
"ff02::1"
42+
)
43+
44+
config_dns_ns_add = config.config.commands["dns"].commands["nameserver"].commands["add"]
45+
config_dns_ns_del = config.config.commands["dns"].commands["nameserver"].commands["del"]
46+
show_dns_ns = show.cli.commands["dns"].commands["nameserver"]
47+
48+
@classmethod
49+
def setup_class(cls):
50+
print("SETUP")
51+
os.environ["UTILITIES_UNIT_TESTING"] = "1"
52+
53+
@classmethod
54+
def teardown_class(cls):
55+
os.environ['UTILITIES_UNIT_TESTING'] = "0"
56+
print("TEARDOWN")
57+
58+
@pytest.mark.parametrize('nameservers', valid_nameservers)
59+
def test_dns_config_nameserver_add_del_with_valid_ip_addresses(self, nameservers):
60+
db = Db()
61+
runner = CliRunner()
62+
obj = {'db': db.cfgdb}
63+
64+
for ip in nameservers:
65+
# config dns nameserver add <ip>
66+
result = runner.invoke(self.config_dns_ns_add, [ip], obj=obj)
67+
print(result.exit_code, result.output)
68+
assert result.exit_code == 0
69+
assert ip in db.cfgdb.get_table('DNS_NAMESERVER')
70+
71+
for ip in nameservers:
72+
# config dns nameserver del <ip>
73+
result = runner.invoke(self.config_dns_ns_del, [ip], obj=obj)
74+
print(result.exit_code, result.output)
75+
assert result.exit_code == 0
76+
assert ip not in db.cfgdb.get_table('DNS_NAMESERVER')
77+
78+
@pytest.mark.parametrize('nameserver', invalid_nameservers)
79+
def test_dns_config_nameserver_add_del_with_invalid_ip_addresses(self, nameserver):
80+
db = Db()
81+
runner = CliRunner()
82+
obj = {'db': db.cfgdb}
83+
84+
# config dns nameserver add <nameserver>
85+
result = runner.invoke(self.config_dns_ns_add, [nameserver], obj=obj)
86+
print(result.exit_code, result.output)
87+
assert result.exit_code != 0
88+
assert "invalid nameserver ip address" in result.output
89+
90+
# config dns nameserver del <nameserver>
91+
result = runner.invoke(self.config_dns_ns_del, [nameserver], obj=obj)
92+
print(result.exit_code, result.output)
93+
assert result.exit_code != 0
94+
assert "invalid nameserver ip address" in result.output
95+
96+
@pytest.mark.parametrize('nameservers', valid_nameservers)
97+
def test_dns_config_nameserver_add_existing_ip(self, nameservers):
98+
db = Db()
99+
runner = CliRunner()
100+
obj = {'db': db.cfgdb}
101+
102+
for ip in nameservers:
103+
# config dns nameserver add <ip>
104+
result = runner.invoke(self.config_dns_ns_add, [ip], obj=obj)
105+
print(result.exit_code, result.output)
106+
assert result.exit_code == 0
107+
assert ip in db.cfgdb.get_table('DNS_NAMESERVER')
108+
109+
# Execute command once more
110+
result = runner.invoke(self.config_dns_ns_add, [ip], obj=obj)
111+
print(result.exit_code, result.output)
112+
assert result.exit_code != 0
113+
assert "nameserver is already configured" in result.output
114+
115+
# config dns nameserver del <ip>
116+
result = runner.invoke(self.config_dns_ns_del, [ip], obj=obj)
117+
print(result.exit_code, result.output)
118+
assert result.exit_code == 0
119+
120+
@pytest.mark.parametrize('nameservers', valid_nameservers)
121+
def test_dns_config_nameserver_del_unexisting_ip(self, nameservers):
122+
db = Db()
123+
runner = CliRunner()
124+
obj = {'db': db.cfgdb}
125+
126+
for ip in nameservers:
127+
# config dns nameserver del <ip>
128+
result = runner.invoke(self.config_dns_ns_del, [ip], obj=obj)
129+
print(result.exit_code, result.output)
130+
assert result.exit_code != 0
131+
assert "is not configured" in result.output
132+
133+
def test_dns_config_nameserver_add_max_number(self):
134+
db = Db()
135+
runner = CliRunner()
136+
obj = {'db': db.cfgdb}
137+
138+
nameservers = ("1.1.1.1", "2.2.2.2", "3.3.3.3")
139+
for ip in nameservers:
140+
# config dns nameserver add <ip>
141+
result = runner.invoke(self.config_dns_ns_add, [ip], obj=obj)
142+
print(result.exit_code, result.output)
143+
assert result.exit_code == 0
144+
145+
# config dns nameserver add <ip>
146+
result = runner.invoke(self.config_dns_ns_add, ["4.4.4.4"], obj=obj)
147+
print(result.exit_code, result.output)
148+
assert result.exit_code != 0
149+
assert "nameservers exceeded" in result.output
150+
151+
for ip in nameservers:
152+
# config dns nameserver del <ip>
153+
result = runner.invoke(self.config_dns_ns_del, [ip], obj=obj)
154+
print(result.exit_code, result.output)
155+
assert result.exit_code == 0
156+
157+
def test_dns_show_nameserver_empty_table(self):
158+
db = Db()
159+
runner = CliRunner()
160+
obj = {'db': db.cfgdb}
161+
162+
# show dns nameserver
163+
result = runner.invoke(self.show_dns_ns, [], obj=obj)
164+
print(result.exit_code, result.output)
165+
assert result.exit_code == 0
166+
assert result.output == dns_show_nameservers_header
167+
168+
def test_dns_show_nameserver(self):
169+
db = Db()
170+
runner = CliRunner()
171+
obj = {'db': db.cfgdb}
172+
173+
nameservers = ("1.1.1.1", "2001:4860:4860::8888")
174+
175+
for ip in nameservers:
176+
# config dns nameserver add <ip>
177+
result = runner.invoke(self.config_dns_ns_add, [ip], obj=obj)
178+
print(result.exit_code, result.output)
179+
assert result.exit_code == 0
180+
assert ip in db.cfgdb.get_table('DNS_NAMESERVER')
181+
182+
# show dns nameserver
183+
result = runner.invoke(self.show_dns_ns, [], obj=obj)
184+
print(result.exit_code, result.output)
185+
assert result.exit_code == 0
186+
assert result.output == dns_show_nameservers
187+
188+
for ip in nameservers:
189+
# config dns nameserver del <ip>
190+
result = runner.invoke(self.config_dns_ns_del, [ip], obj=obj)
191+
print(result.exit_code, result.output)
192+
assert result.exit_code == 0
193+
assert ip not in db.cfgdb.get_table('DNS_NAMESERVER')

0 commit comments

Comments
 (0)