Skip to content

Next Release #18

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 9 additions & 7 deletions CHANGELOG.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
Unreleased:
added: []
changed: []
deprecated: []
fixed: []
removed:
- python 3.7 support
security: []

0.2.0:
added:
- python to 3.9
Expand All @@ -7,10 +16,3 @@
removed:
- python 3.4 and 3.5 support
- Pipfile
Unreleased:
added: []
changed: []
deprecated: []
fixed: []
removed: []
security: []
1,648 changes: 886 additions & 762 deletions poetry.lock

Large diffs are not rendered by default.

14 changes: 6 additions & 8 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ classifiers = [
"Intended Audience :: System Administrators",
"Intended Audience :: Telecommunications Industry",
"License :: OSI Approved :: Apache Software License",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Topic :: Software Development :: Libraries :: Python Modules",
"Topic :: Internet",
]
Expand All @@ -30,11 +30,10 @@ netom = "netom.cli:main"


[tool.poetry.dependencies]
python = "^3.7"
Jinja2 = "^3"
tmpl = "^1"
confu = "^1.7.1"
munge = "^1.1.0"
python = "^3.8"
jinja2 = ">=3"
confu = ">=1.7.1"
munge = ">=1.1.0"

[tool.poetry.dev-dependencies]
# testing
Expand All @@ -57,11 +56,10 @@ pyupgrade = ">=2.19"
# docs
markdown = "*"
markdown-include = ">=0.5,<1"
mkdocs = "^1.2.3"
mkdocs = ">=1.2.3"

# ctl
ctl = ">=1"
jinja2 = ">=2"
tmpl = ">=1"


Expand Down
31 changes: 13 additions & 18 deletions src/netom/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from confu import schema
from pkg_resources import get_distribution

from . import filters
from .exception import NetomValidationError

# TODO move out of this namespace
Expand All @@ -15,21 +16,6 @@
__version__ = get_distribution("netom").version


def make_variable_name(value):
"""
Makes passed value into a variable name.
"""
value = str(value).translate(str.maketrans(" .:", "___"))
return value


def ip_version(value):
"""
Returns version of passed IP address.
"""
return ipaddress.ip_address(value).version


class Render:
"""
Renders data to defined type.
Expand All @@ -39,8 +25,9 @@ def __init__(self, model_version, model_type, search_path=None):
"""
Create a render object.

model_version is the data model version (currently at "netom0")

subtype is typically vendor name
version is the model version to use
"""

self.version = model_version
Expand All @@ -55,10 +42,18 @@ def __init__(self, model_version, model_type, search_path=None):

def set_search_path(self, search_path):
self.engine = tmpl.get_engine("jinja2")(search_path=search_path)
self.engine.engine.filters["make_variable_name"] = make_variable_name
self.engine.engine.filters["ip_version"] = ip_version
self.engine.engine.filters["make_variable_name"] = filters.make_variable_name
self.engine.engine.filters["ip_version"] = filters.ip_version
self.engine.engine.filters["address_to_mask"] = filters.address_to_mask
self.engine.engine.filters["address_to_wildcard"] = filters.address_to_wildcard
self.engine.engine.filters["line_to_mask"] = filters.line_to_mask
self.engine.engine.filters["line_to_wildcard"] = filters.line_to_wildcard
self.engine.engine.filters["ip_to_ipv4"] = filters.ip_to_ipv4
# self.engine.search_path = os.path.dirname(search_path)

for name in filters.__all__:
self.engine.engine.filters[name] = getattr(filters, name)

def _render(self, filename, data, fobj):
# engine.engine.undefined = IgnoreUndefined
output = self.engine._render(src=filename, env=data)
Expand Down
112 changes: 112 additions & 0 deletions src/netom/filters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import ipaddress

Check notice

Code scanning / CodeQL

Module is imported with 'import' and 'import from'

Module 'ipaddress' is imported with both 'import' and 'import from'.
import re
from ipaddress import IPv4Address
from jinja2 import pass_context


r_ip_nn = re.compile("[\d.]{7,15}/\d{1,2}")

__all__ = [
"make_variable_name",
"render_template",
"ip_address",
"ip_interface",
"ip_version",
]


@pass_context
def render_template(context, value):
"""
Renders a template from the value.

Does not get local variables.
"""
template = context.eval_ctx.environment.from_string(value)
# print(f"parent {context.parent}")
# print(f"ENV {context.get_all()['self_id']}")
# print(f"self-id {context.get('self_id')}")
result = template.render(**context)

# not sure we need this
# if context.eval_ctx.autoescape:
# result = Markup(result)
Comment on lines +32 to +33

Check notice

Code scanning / CodeQL

Commented-out code

This comment appears to contain commented-out code.

return result


def make_variable_name(value):
"""
Makes passed value into a variable name.
"""
value = str(value).translate(str.maketrans(" .:", "___"))
return value


def ip_address(value):
"""
Returns address of passed IP interface.
"""
return ipaddress.ip_interface(value).ip


# XXX use this
def ip_interface(value):
"""
Returns ip_interface of passed value.
"""
return ipaddress.ip_interface(value)


# XXX when was interface added?
def ip_version(value):
"""
Returns version of passed IP address.
"""
return ipaddress.ip_interface(value).version


def address_to_mask(addr):
"""
Transform A.B.C.D/nn into A.B.C.D M.M.M.M subnet mask format.
"""
ipaddr4 = ipaddress.ip_interface(addr)
return f"{ipaddr4.ip} {ipaddr4.netmask}"


def address_to_wildcard(addr):
"""
Transform A.B.C.D/nn into A.B.C.D W.W.W.W subnet wildcard format.

credit for the mask shenanigans to George Shuklin
https://medium.com/opsops/wildcard-masks-operations-in-python-16acf1c35683
"""
ipnet4 = ipaddress.ip_network(addr)
wildcard = str(IPv4Address(int(IPv4Address(ipnet4.netmask))^(2**32-1)))
return f"{ipnet4.network_address} {wildcard}"


def line_to_mask(line):
"""
Search for any IPv4/nn tokens in the `line` argument and transform them into mask format.
E.g. `permit ip 10.0.0.0/8` any -> `permit ip 10.0.0.0 255.0.0.0 any`
"""
tokens = line.split()
return " ".join([address_to_mask(token) if r_ip_nn.match(token) else token for token in tokens])


def line_to_wildcard(line):
"""
Search for any IPv4/nn tokens in the `line` argument and transform them into wildcard format.
E.g. `permit ip 10.0.0.0/8` any -> `permit ip 10.0.0.0 0.255.255.255 any`
"""
tokens = line.split()
return " ".join([address_to_wildcard(token) if r_ip_nn.match(token) else token for token in tokens])

def ip_to_ipv4(line):
"""
Replace token "ip" with "ipv4" to satisfy IOS-XR syntax.
"""
tokens = line.split()
return " ".join(["ipv4" if token == "ip" else token for token in tokens])

4 changes: 4 additions & 0 deletions tests/templates/filters.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@

ip_address {{ ip4 | ip_address }}
ip_interface {{ ip4 | ip_interface }}
ip_version {{ ip4 | ip_version }}
48 changes: 48 additions & 0 deletions tests/test_filters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@

from netom.filters import address_to_mask, address_to_wildcard, line_to_mask, line_to_wildcard
import pytest

Check notice

Code scanning / CodeQL

Unused import

Import of 'pytest' is not used.


def test_address_to_mask():
assert address_to_mask("192.168.0.1/4") == "192.168.0.1 240.0.0.0"
assert address_to_mask("192.168.0.1/8") == "192.168.0.1 255.0.0.0"
assert address_to_mask("192.168.0.1/9") == "192.168.0.1 255.128.0.0"
assert address_to_mask("192.168.0.1/16") == "192.168.0.1 255.255.0.0"
assert address_to_mask("192.168.0.1/24") == "192.168.0.1 255.255.255.0"
assert address_to_mask("192.168.0.1/25") == "192.168.0.1 255.255.255.128"
assert address_to_mask("192.168.0.1/30") == "192.168.0.1 255.255.255.252"
assert address_to_mask("192.168.0.1/31") == "192.168.0.1 255.255.255.254"
assert address_to_mask("192.168.0.1/32") == "192.168.0.1 255.255.255.255"

def test_address_to_wildcard():
assert address_to_wildcard("192.0.0.0/4") == "192.0.0.0 15.255.255.255"
assert address_to_wildcard("192.0.0.0/8") == "192.0.0.0 0.255.255.255"
assert address_to_wildcard("192.0.0.0/9") == "192.0.0.0 0.127.255.255"
assert address_to_wildcard("192.168.0.0/16") == "192.168.0.0 0.0.255.255"
assert address_to_wildcard("192.168.0.0/24") == "192.168.0.0 0.0.0.255"
assert address_to_wildcard("192.168.0.0/25") == "192.168.0.0 0.0.0.127"
assert address_to_wildcard("192.168.0.0/30") == "192.168.0.0 0.0.0.3"
assert address_to_wildcard("192.168.0.0/31") == "192.168.0.0 0.0.0.1"
assert address_to_wildcard("192.168.0.0/32") == "192.168.0.0 0.0.0.0"

def test_line_to_mask():
assert line_to_mask("ip route 192.168.0.0/4 198.51.100.1") == "ip route 192.168.0.0 240.0.0.0 198.51.100.1"
assert line_to_mask("ip route 192.168.0.0/8 198.51.100.1") == "ip route 192.168.0.0 255.0.0.0 198.51.100.1"
assert line_to_mask("ip route 192.168.0.0/9 198.51.100.1") == "ip route 192.168.0.0 255.128.0.0 198.51.100.1"
assert line_to_mask("ip route 192.168.0.0/16 198.51.100.1") == "ip route 192.168.0.0 255.255.0.0 198.51.100.1"
assert line_to_mask("ip route 192.168.0.0/24 198.51.100.1") == "ip route 192.168.0.0 255.255.255.0 198.51.100.1"
assert line_to_mask("ip route 192.168.0.0/25 198.51.100.1") == "ip route 192.168.0.0 255.255.255.128 198.51.100.1"
assert line_to_mask("ip route 192.168.0.0/30 198.51.100.1") == "ip route 192.168.0.0 255.255.255.252 198.51.100.1"
assert line_to_mask("ip route 192.168.0.0/31 198.51.100.1") == "ip route 192.168.0.0 255.255.255.254 198.51.100.1"
assert line_to_mask("ip route 192.168.0.0/32 198.51.100.1") == "ip route 192.168.0.0 255.255.255.255 198.51.100.1"

def test_line_to_wildcard():
assert line_to_wildcard("permit tcp 192.0.0.0/4 any eq 22") == "permit tcp 192.0.0.0 15.255.255.255 any eq 22"
assert line_to_wildcard("permit tcp 192.0.0.0/8 any eq 22") == "permit tcp 192.0.0.0 0.255.255.255 any eq 22"
assert line_to_wildcard("permit tcp 192.0.0.0/9 any eq 22") == "permit tcp 192.0.0.0 0.127.255.255 any eq 22"
assert line_to_wildcard("permit tcp 192.168.0.0/16 any eq 22") == "permit tcp 192.168.0.0 0.0.255.255 any eq 22"
assert line_to_wildcard("permit tcp 192.168.0.0/24 any eq 22") == "permit tcp 192.168.0.0 0.0.0.255 any eq 22"
assert line_to_wildcard("permit tcp 192.168.0.0/25 any eq 22") == "permit tcp 192.168.0.0 0.0.0.127 any eq 22"
assert line_to_wildcard("permit tcp 192.168.0.0/30 any eq 22") == "permit tcp 192.168.0.0 0.0.0.3 any eq 22"
assert line_to_wildcard("permit tcp 192.168.0.0/31 any eq 22") == "permit tcp 192.168.0.0 0.0.0.1 any eq 22"
assert line_to_wildcard("permit tcp 192.168.0.0/32 any eq 22") == "permit tcp 192.168.0.0 0.0.0.0 any eq 22"
17 changes: 17 additions & 0 deletions tests/test_init.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
import os

import netom

filter_data = dict(ip4="10.0.0.1/24")


def test_init():
assert netom.Render("test", "12")


def test_filters(this_dir):
expected = """
ip_address 10.0.0.1
ip_interface 10.0.0.1/24
ip_version 4
"""

robj = netom.Render("netom0", "test-0", os.path.join(this_dir, "templates"))
output = robj.render_string("filters.j2", filter_data)
print(output)
assert expected == output