Skip to content

Commit 7d840ba

Browse files
pavel-shirshovPavel Shirshov
authored andcommitted
[pfx_filter]: Add a prefix mask by default in pfx_filter, when there is no one (#4860)
If some table with a list of tuples (interface name, ip prefix) has ip prefixes without a mask length, it will cause issues in SONiC. For example quagga and frr will treat ipv4 address without a mask, so "10.20.30.40" address will be treated as "10.0.0.0/8", which is dangerous. The fix here is that when pfx_filter get a tuple (interface name, ip prefix), where the ip prefix doesn't have prefix mask length, add a mask by default: "/32 for ipv4 addresses, /128 for ipv6 addresses". Co-authored-by: Pavel Shirshov <[email protected]>
1 parent 369bf88 commit 7d840ba

File tree

7 files changed

+293
-1
lines changed

7 files changed

+293
-1
lines changed

src/sonic-bgpcfgd/app/template.py

+108
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
from collections import OrderedDict
2+
from functools import partial
3+
4+
import jinja2
5+
import netaddr
6+
7+
from .log import log_err
8+
9+
class TemplateFabric(object):
10+
""" Fabric for rendering jinja2 templates """
11+
def __init__(self, template_path = '/usr/share/sonic/templates'):
12+
j2_template_paths = [template_path]
13+
j2_loader = jinja2.FileSystemLoader(j2_template_paths)
14+
j2_env = jinja2.Environment(loader=j2_loader, trim_blocks=False)
15+
j2_env.filters['ipv4'] = self.is_ipv4
16+
j2_env.filters['ipv6'] = self.is_ipv6
17+
j2_env.filters['pfx_filter'] = self.pfx_filter
18+
for attr in ['ip', 'network', 'prefixlen', 'netmask']:
19+
j2_env.filters[attr] = partial(self.prefix_attr, attr)
20+
self.env = j2_env
21+
22+
def from_file(self, filename):
23+
"""
24+
Read a template from a file
25+
:param filename: filename of the file. Type String
26+
:return: Jinja2 template object
27+
"""
28+
return self.env.get_template(filename)
29+
30+
def from_string(self, tmpl):
31+
"""
32+
Read a template from a string
33+
:param tmpl: Text representation of Jinja2 template
34+
:return: Jinja2 template object
35+
"""
36+
return self.env.from_string(tmpl)
37+
38+
@staticmethod
39+
def is_ipv4(value):
40+
""" Return True if the value is an ipv4 address """
41+
if not value:
42+
return False
43+
if isinstance(value, netaddr.IPNetwork):
44+
addr = value
45+
else:
46+
try:
47+
addr = netaddr.IPNetwork(str(value))
48+
except (netaddr.NotRegisteredError, netaddr.AddrFormatError, netaddr.AddrConversionError):
49+
return False
50+
return addr.version == 4
51+
52+
@staticmethod
53+
def is_ipv6(value):
54+
""" Return True if the value is an ipv6 address """
55+
if not value:
56+
return False
57+
if isinstance(value, netaddr.IPNetwork):
58+
addr = value
59+
else:
60+
try:
61+
addr = netaddr.IPNetwork(str(value))
62+
except (netaddr.NotRegisteredError, netaddr.AddrFormatError, netaddr.AddrConversionError):
63+
return False
64+
return addr.version == 6
65+
66+
@staticmethod
67+
def prefix_attr(attr, value):
68+
"""
69+
Extract attribute from IPNetwork object
70+
:param attr: attribute to extract
71+
:param value: the string representation of ip prefix which will be converted to IPNetwork.
72+
:return: the value of the extracted attribute
73+
"""
74+
if not value:
75+
return None
76+
else:
77+
try:
78+
prefix = netaddr.IPNetwork(str(value))
79+
except (netaddr.NotRegisteredError, netaddr.AddrFormatError, netaddr.AddrConversionError):
80+
return None
81+
return str(getattr(prefix, attr))
82+
83+
@staticmethod
84+
def pfx_filter(value):
85+
"""INTERFACE Table can have keys in one of the two formats:
86+
string or tuple - This filter skips the string keys and only
87+
take into account the tuple.
88+
For eg - VLAN_INTERFACE|Vlan1000 vs VLAN_INTERFACE|Vlan1000|192.168.0.1/21
89+
"""
90+
table = OrderedDict()
91+
92+
if not value:
93+
return table
94+
95+
for key, val in value.items():
96+
if not isinstance(key, tuple):
97+
continue
98+
intf, ip_address = key
99+
if '/' not in ip_address:
100+
if TemplateFabric.is_ipv4(ip_address):
101+
table[(intf, "%s/32" % ip_address)] = val
102+
elif TemplateFabric.is_ipv6(ip_address):
103+
table[(intf, "%s/128" % ip_address)] = val
104+
else:
105+
log_err("'%s' is invalid ip address" % ip_address)
106+
else:
107+
table[key] = val
108+
return table
+139
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
from app.template import TemplateFabric
2+
from collections import OrderedDict
3+
import pytest
4+
5+
6+
def test_pfx_filter_none():
7+
res = TemplateFabric.pfx_filter(None)
8+
assert isinstance(res, OrderedDict) and len(res) == 0
9+
10+
def test_pfx_filter_empty_tuple():
11+
res = TemplateFabric.pfx_filter(())
12+
assert isinstance(res, OrderedDict) and len(res) == 0
13+
14+
def test_pfx_filter_empty_list():
15+
res = TemplateFabric.pfx_filter([])
16+
assert isinstance(res, OrderedDict) and len(res) == 0
17+
18+
def test_pfx_filter_empty_dict():
19+
res = TemplateFabric.pfx_filter({})
20+
assert isinstance(res, OrderedDict) and len(res) == 0
21+
22+
def test_pfx_filter_strings():
23+
src = {
24+
'Loopback0': {},
25+
'Loopback1': {},
26+
}
27+
expected = OrderedDict([])
28+
res = TemplateFabric.pfx_filter(src)
29+
assert res == expected
30+
31+
def test_pfx_filter_mixed_keys():
32+
src = {
33+
'Loopback0': {},
34+
('Loopback0', '11.11.11.11/32'): {},
35+
'Loopback1': {},
36+
('Loopback1', '55.55.55.55/32'): {},
37+
}
38+
expected = OrderedDict(
39+
[
40+
(('Loopback1', '55.55.55.55/32'), {}),
41+
(('Loopback0', '11.11.11.11/32'), {}),
42+
]
43+
)
44+
res = TemplateFabric.pfx_filter(src)
45+
assert res == expected
46+
47+
48+
def test_pfx_filter_pfx_v4_w_mask():
49+
src = {
50+
('Loopback0', '11.11.11.11/32'): {},
51+
('Loopback1', '55.55.55.55/32'): {},
52+
}
53+
expected = OrderedDict(
54+
[
55+
(('Loopback1', '55.55.55.55/32'), {}),
56+
(('Loopback0', '11.11.11.11/32'), {}),
57+
]
58+
)
59+
res = TemplateFabric.pfx_filter(src)
60+
assert res == expected
61+
62+
def test_pfx_filter_pfx_v6_w_mask():
63+
src = {
64+
('Loopback0', 'fc00::/128'): {},
65+
('Loopback1', 'fc00::1/128'): {},
66+
}
67+
expected = OrderedDict(
68+
[
69+
(('Loopback0', 'fc00::/128'), {}),
70+
(('Loopback1', 'fc00::1/128'), {}),
71+
]
72+
)
73+
res = TemplateFabric.pfx_filter(src)
74+
assert res == expected
75+
76+
def test_pfx_filter_pfx_v4_no_mask():
77+
src = {
78+
('Loopback0', '11.11.11.11'): {},
79+
('Loopback1', '55.55.55.55'): {},
80+
}
81+
expected = OrderedDict(
82+
[
83+
(('Loopback1', '55.55.55.55/32'), {}),
84+
(('Loopback0', '11.11.11.11/32'), {}),
85+
]
86+
)
87+
res = TemplateFabric.pfx_filter(src)
88+
assert res == expected
89+
90+
def test_pfx_filter_pfx_v6_no_mask():
91+
src = {
92+
('Loopback0', 'fc00::'): {},
93+
('Loopback1', 'fc00::1'): {},
94+
}
95+
expected = OrderedDict(
96+
[
97+
(('Loopback0', 'fc00::/128'), {}),
98+
(('Loopback1', 'fc00::1/128'), {}),
99+
]
100+
)
101+
res = TemplateFabric.pfx_filter(src)
102+
assert res == expected
103+
104+
105+
def test_pfx_filter_pfx_comprehensive():
106+
src = {
107+
'Loopback0': {},
108+
('Loopback0', 'fc00::'): {},
109+
'Loopback1': {},
110+
('Loopback1', 'fc00::1/128'): {},
111+
('Loopback2', '11.11.11.11/32'): {},
112+
('Loopback3', '55.55.55.55'): {},
113+
'Loopback2': {},
114+
'Loopback3': {},
115+
('Loopback5', '22.22.22.1/24'): {},
116+
('Loopback6', 'fc00::55/64'): {},
117+
}
118+
expected = OrderedDict(
119+
[
120+
(('Loopback1', 'fc00::1/128'), {}),
121+
(('Loopback3', '55.55.55.55/32'), {}),
122+
(('Loopback6', 'fc00::55/64'), {}),
123+
(('Loopback2', '11.11.11.11/32'), {}),
124+
(('Loopback0', 'fc00::/128'), {}),
125+
(('Loopback5', '22.22.22.1/24'), {}),
126+
]
127+
)
128+
res = TemplateFabric.pfx_filter(src)
129+
assert res == expected
130+
131+
@pytest.fixture
132+
def test_pfx_filter_wrong_ip(caplog):
133+
src = {
134+
('Loopback0', 'wrong_ip'): {},
135+
}
136+
res = TemplateFabric.pfx_filter(src)
137+
assert "'wrong_ip' is invalid ip address" in caplog.text
138+
assert isinstance(res, OrderedDict) and len(res) == 0
139+

src/sonic-config-engine/sonic-cfggen

+11-1
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,17 @@ def pfx_filter(value):
116116
for key,val in value.items():
117117
if not isinstance(key, tuple):
118118
continue
119-
table[key] = val
119+
intf, ip_address = key
120+
if '/' not in ip_address:
121+
if is_ipv4(ip_address):
122+
new_ip_address = "%s/32" % ip_address
123+
elif is_ipv6(ip_address):
124+
new_ip_address = "%s/128" % ip_address
125+
else:
126+
raise ValueError("'%s' is invalid ip address" % ip_address)
127+
table[(intf, new_ip_address)] = val
128+
else:
129+
table[key] = val
120130
return table
121131

122132
def ip_network(value):
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"VLAN_INTERFACE": {
3+
"Vlan1": {},
4+
"Vlan1|1.1.1.1/32": {},
5+
"Vlan2": {},
6+
"Vlan2|2.2.2.2": {},
7+
"Vlan3": {},
8+
"Vlan3|fc00::1": {},
9+
"Vlan4": {},
10+
"Vlan4|fc00::2/64": {}
11+
}
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
"Vlan1"="1.1.1.1/32"
2+
"Vlan2"="2.2.2.2/32"
3+
"Vlan3"="fc00::1/128"
4+
"Vlan4"="fc00::2/64"
5+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{% for intf, addr in VLAN_INTERFACE|pfx_filter %}
2+
"{{ intf }}"="{{ addr }}"
3+
{% endfor %}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from unittest import TestCase
2+
import subprocess
3+
4+
class TestPfxFilter(TestCase):
5+
def test_comprehensive(self):
6+
# Generate output
7+
data_dir = "tests/data/pfx_filter"
8+
cmd = "./sonic-cfggen -j %s/param_1.json -t %s/tmpl_1.txt.j2 > /tmp/result_1.txt" % (data_dir, data_dir)
9+
subprocess.check_output(cmd, shell=True)
10+
# Compare outputs
11+
cmd = "diff -u tests/data/pfx_filter/result_1.txt /tmp/result_1.txt"
12+
try:
13+
res = subprocess.check_output(cmd, shell=True)
14+
except subprocess.CalledProcessError as e:
15+
assert False, "Wrong output. return code: %d, Diff: %s" % (e.returncode, e.output)

0 commit comments

Comments
 (0)