Skip to content

Commit 9c90f5b

Browse files
committed
Make netlab Jinja2 handling more Ansible-like
* The undefined variables are handled with a derived Undefined type that behaves like Ansible variables. For example 'x.y is defined' returns false (instead of crashing) if x is not defined. * All Jinja2 filters are defined as having 'ansible.utils.X' synonim. With these enhancements, we can revert the changes made to Bird and Linux templates Also: fix the ipaddr-like templates * ipv4() and ipv6() should accept zero arguments (and act like a "select addresses from list" filter) * ipaddr(int) should return a prefix, not an IP address
1 parent 1418995 commit 9c90f5b

File tree

4 files changed

+70
-13
lines changed

4 files changed

+70
-13
lines changed

netsim/ansible/templates/initial/bird-clab.j2

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
set -x
22
{% include 'linux-clab.j2' +%}
33
#
4-
{% if loopback is defined and loopback.ipv6 is defined %}
4+
{% if loopback.ipv6 is defined %}
55
set +e
66
ip addr add fe80::1/64 dev lo scope link
77
{% endif %}
Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
{% include 'frr.j2' +%}
22
{% from "initial/linux/vanilla-ifconfig.j2" import ifconfig %}
33
{% for intf in interfaces
4-
if intf.type == 'svi' or
5-
intf.vlan is defined and intf.vlan.mode|default('') == 'route' %}
4+
if intf.type == 'svi' or intf.vlan.mode|default('') == 'route' %}
65
{{ ifconfig(intf) }}
76
{% endfor %}

netsim/utils/filters.py

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import typing
1515

1616
import netaddr
17+
from jinja2.runtime import StrictUndefined
1718

1819

1920
def ipaddr_filter(
@@ -49,7 +50,7 @@ def j2_ipaddr(
4950

5051
addr = netaddr.IPNetwork(value)
5152
if isinstance(arg,int):
52-
return str(addr[arg])
53+
return str(addr[arg]) + "/" + str(addr.prefixlen)
5354

5455
if arg in MAP_IPADDR:
5556
arg = MAP_IPADDR[arg]
@@ -62,10 +63,10 @@ def j2_ipaddr(
6263

6364
raise ValueError(f'Invalid argument {arg} passed to built-in ipaddr filter')
6465

65-
def j2_ipv4(value: typing.Any, arg: typing.Union[int,str]) -> typing.Union[list,str]:
66+
def j2_ipv4(value: typing.Any, arg: typing.Union[int,str] = '') -> typing.Union[list,str]:
6667
return j2_ipaddr(value,arg,4)
6768

68-
def j2_ipv6(value: typing.Any, arg: typing.Union[int,str]) -> typing.Union[list,str]:
69+
def j2_ipv6(value: typing.Any, arg: typing.Union[int,str] = '') -> typing.Union[list,str]:
6970
return j2_ipaddr(value,arg,6)
7071

7172
# Format MAC addresses in Cisco/Unix/... format
@@ -85,3 +86,58 @@ def j2_hwaddr(value: typing.Any, format: str = '') -> str:
8586
raise ValueError(f'{value} is not a valid MAC address and cannot be formatted as {format}')
8687
else: # Otherwise it was a filter query, return empty string
8788
return ''
89+
90+
class j2_Undefined(StrictUndefined):
91+
"""
92+
Mimics Ansible's undefined variable handling in Jinja2 templates.
93+
Accessing attributes or items of an undefined variable returns another undefined,
94+
and expressions like 'x.y is defined' return False when x is undefined,
95+
rather than raising an error.
96+
"""
97+
98+
# Override attribute access
99+
def __getattr__(self, name: typing.Any) -> typing.Any:
100+
return j2_Undefined(name=name, hint=self._undefined_hint)
101+
102+
# Override item access
103+
def __getitem__(self, key: typing.Any) -> typing.Any:
104+
return j2_Undefined(name=key, hint=self._undefined_hint)
105+
106+
# Override boolean evaluation
107+
def __bool__(self) -> bool:
108+
return False
109+
110+
# Override is defined
111+
@property
112+
def defined(self) -> bool:
113+
return False
114+
115+
UTILS_FILTERS: dict = {
116+
'ipaddr': j2_ipaddr,
117+
'ipv4': j2_ipv4,
118+
'ipv6': j2_ipv6,
119+
'hwaddr': j2_hwaddr
120+
}
121+
122+
"""
123+
Simplified 'flatten' filter -- returns a flattened list
124+
"""
125+
def bi_flatten(mylist: typing.Any, levels: typing.Optional[int] = None) -> list:
126+
if not isinstance(mylist,list):
127+
return [ mylist ]
128+
129+
result = []
130+
for value in mylist:
131+
if isinstance(value,list):
132+
if (levels is None or levels >= 1):
133+
result.extend(bi_flatten(value,None if levels is None else levels - 1))
134+
else:
135+
result.extend(value)
136+
else:
137+
result.append(value)
138+
139+
return result
140+
141+
BUILTIN_FILTERS: dict = {
142+
'flatten': bi_flatten
143+
}

netsim/utils/templates.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import typing
88

99
from box import Box
10-
from jinja2 import Environment, FileSystemLoader, StrictUndefined, make_logging_undefined
10+
from jinja2 import Environment, FileSystemLoader
1111

1212
from ..augment import devices
1313
from ..outputs import common as outputs_common
@@ -18,11 +18,13 @@
1818

1919

2020
def add_j2_filters(ENV: Environment) -> None:
21-
for fname in dir(filters): # Get all attributes of the "filters" module
22-
if not fname.startswith('j2_'): # Filters have to start with 'j2_' prefix
23-
continue
24-
fcode = getattr(filters,fname) # Get a pointer to filter function
25-
ENV.filters[fname.replace('j2_','')] = fcode # And define a new Jinja2 filter
21+
for fname,fcode in filters.UTILS_FILTERS.items(): # Iterate over internal filter definitions
22+
ENV.filters[fname] = fcode # ... emulating the ansible.utils filters we use
23+
ENV.filters[f'ansible.utils.{fname}'] = fcode # ... define simple filter name and its Ansible FQFN
24+
25+
for fname,fcode in filters.BUILTIN_FILTERS.items(): # Do the same for ansible.builtin filters we use
26+
ENV.filters[fname] = fcode
27+
ENV.filters[f'ansible.builtin.{fname}'] = fcode
2628

2729
"""
2830
Render a Jinja2 template
@@ -41,7 +43,7 @@ def add_j2_filters(ENV: Environment) -> None:
4143
def get_jinja2_env_for_path(template_path: tuple) -> Environment:
4244
ENV = Environment(loader=FileSystemLoader(template_path), \
4345
trim_blocks=True,lstrip_blocks=True, \
44-
undefined=make_logging_undefined(base=StrictUndefined))
46+
undefined=filters.j2_Undefined)
4547
add_j2_filters(ENV)
4648
return ENV
4749

0 commit comments

Comments
 (0)