Skip to content

Commit

Permalink
Filter object (#176)
Browse files Browse the repository at this point in the history
  • Loading branch information
dbarrosop committed Jul 17, 2018
1 parent d9fd9a2 commit 78758d0
Show file tree
Hide file tree
Showing 10 changed files with 542 additions and 106 deletions.
382 changes: 282 additions & 100 deletions docs/tutorials/intro/inventory.ipynb

Large diffs are not rendered by default.

12 changes: 12 additions & 0 deletions docs/tutorials/intro/inventory/hosts.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ host1.cmh:
- cmh
nornir_nos: linux
type: host
nested_data:
a_dict:
a: 1
b: 2
a_list: [1, 2]
a_string: "asdasd"

host2.cmh:
nornir_host: 127.0.0.1
Expand All @@ -22,6 +28,12 @@ host2.cmh:
- cmh
nornir_nos: linux
type: host
nested_data:
a_dict:
b: 2
c: 3
a_list: [1, 2]
a_string: "qwe"

spine00.cmh:
nornir_host: 127.0.0.1
Expand Down
4 changes: 2 additions & 2 deletions nornir/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,15 +131,15 @@ def configure_logging(self):
if dictConfig["root"]["handlers"]:
logging.config.dictConfig(dictConfig)

def filter(self, **kwargs):
def filter(self, *args, **kwargs):
"""
See :py:meth:`nornir.core.inventory.Inventory.filter`
Returns:
:obj:`Nornir`: A new object with same configuration as ``self`` but filtered inventory.
"""
b = Nornir(dry_run=self.dry_run, **self.__dict__)
b.inventory = self.inventory.filter(**kwargs)
b.inventory = self.inventory.filter(*args, **kwargs)
return b

def _run_serial(self, task, hosts, **kwargs):
Expand Down
88 changes: 88 additions & 0 deletions nornir/core/filter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
class F_OP_BASE(object):

def __init__(self, op1, op2):
self.op1 = op1
self.op2 = op2

def __and__(self, other):
return AND(self, other)

def __or__(self, other):
return OR(self, other)

def __repr__(self):
return "( {} {} {} )".format(self.op1, self.__class__.__name__, self.op2)


class AND(F_OP_BASE):

def __call__(self, host):
return self.op1(host) and self.op2(host)


class OR(F_OP_BASE):

def __call__(self, host):
return self.op1(host) or self.op2(host)


class F(object):

def __init__(self, **kwargs):
self.filters = kwargs

def __call__(self, host):
return all(
F._verify_rules(host, k.split("__"), v) for k, v in self.filters.items()
)

def __and__(self, other):
return AND(self, other)

def __or__(self, other):
return OR(self, other)

def __invert__(self):
return NOT_F(**self.filters)

def __repr__(self):
return "<Filter ({})>".format(self.filters)

@staticmethod
def _verify_rules(data, rule, value):
if len(rule) > 1:
return F._verify_rules(data.get(rule[0], {}), rule[1:], value)

elif len(rule) == 1:
operator = "__{}__".format(rule[0])
if hasattr(data, operator):
return getattr(data, operator)(value)

elif hasattr(data, rule[0]):
if callable(getattr(data, rule[0])):
return getattr(data, rule[0])(value)

else:
return getattr(data, rule[0]) == value

else:
return data.get(rule[0]) == value

else:
raise Exception(
"I don't know how I got here:\n{}\n{}\n{}".format(data, rule, value)
)


class NOT_F(F):

def __call__(self, host):
return not any(
F._verify_rules(host, k.split("__"), v) for k, v in self.filters.items()
)

def __invert__(self):
return F(**self.filters)

def __repr__(self):
return "<Filter NOT ({})>".format(self.filters)
18 changes: 15 additions & 3 deletions nornir/core/inventory.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,11 +113,21 @@ def to_dict(self):

def has_parent_group(self, group):
"""Retuns whether the object is a child of the :obj:`Group` ``group``"""
if isinstance(group, str):
return self._has_parent_group_by_name(group)

else:
return self._has_parent_group_by_object(group)

def _has_parent_group_by_name(self, group):
for g in self.groups:
if g is group or g.has_parent_group(group):
if g.name == group or g.has_parent_group(group):
return True

return False
def _has_parent_group_by_object(self, group):
for g in self.groups:
if g is group or g.has_parent_group(group):
return True

def __getitem__(self, item):
try:
Expand Down Expand Up @@ -317,7 +327,7 @@ def _resolve_groups(self, groups):
r = groups
return r

def filter(self, filter_func=None, **kwargs):
def filter(self, filter_obj=None, filter_func=None, *args, **kwargs):
"""
Returns a new inventory after filtering the hosts by matching the data passed to the
function. For instance, assume an inventory with::
Expand All @@ -337,9 +347,11 @@ def filter(self, filter_func=None, **kwargs):
* ``my_inventory.filter(site="bma", role="db")`` will result in ``host3`` only
Arguments:
filter_obj (:obj:nornir.core.filter.F): Filter object to run
filter_func (callable): if filter_func is passed it will be called against each
device. If the call returns ``True`` the device will be kept in the inventory
"""
filter_func = filter_obj or filter_func
if filter_func:
filtered = {n: h for n, h in self.hosts.items() if filter_func(h, **kwargs)}
else:
Expand Down
114 changes: 114 additions & 0 deletions tests/core/test_filter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import os

from nornir.core.filter import F
from nornir.plugins.inventory.simple import SimpleInventory

dir_path = os.path.dirname(os.path.realpath(__file__))
inventory = SimpleInventory(
"{}/../inventory_data/hosts.yaml".format(dir_path),
"{}/../inventory_data/groups.yaml".format(dir_path),
)


class Test(object):

def test_simple(self):
f = F(site="site1")
filtered = sorted(list((inventory.filter(f).hosts.keys())))

assert filtered == ["dev1.group_1", "dev2.group_1"]

def test_and(self):
f = F(site="site1") & F(role="www")
filtered = sorted(list((inventory.filter(f).hosts.keys())))

assert filtered == ["dev1.group_1"]

def test_or(self):
f = F(site="site1") | F(role="www")
filtered = sorted(list((inventory.filter(f).hosts.keys())))

assert filtered == ["dev1.group_1", "dev2.group_1", "dev3.group_2"]

def test_combined(self):
f = F(site="site2") | (F(role="www") & F(my_var="comes_from_dev1.group_1"))
filtered = sorted(list((inventory.filter(f).hosts.keys())))

assert filtered == ["dev1.group_1", "dev3.group_2", "dev4.group_2"]

f = (F(site="site2") | F(role="www")) & F(my_var="comes_from_dev1.group_1")
filtered = sorted(list((inventory.filter(f).hosts.keys())))

assert filtered == ["dev1.group_1"]

def test_contains(self):
f = F(groups__contains="group_1")
filtered = sorted(list((inventory.filter(f).hosts.keys())))

assert filtered == ["dev1.group_1", "dev2.group_1"]

def test_negate(self):
f = ~F(groups__contains="group_1")
filtered = sorted(list((inventory.filter(f).hosts.keys())))

assert filtered == ["dev3.group_2", "dev4.group_2"]

def test_negate_and_second_negate(self):
f = F(site="site1") & ~F(role="www")
filtered = sorted(list((inventory.filter(f).hosts.keys())))

assert filtered == ["dev2.group_1"]

def test_negate_or_both_negate(self):
f = ~F(site="site1") | ~F(role="www")
filtered = sorted(list((inventory.filter(f).hosts.keys())))

assert filtered == ["dev2.group_1", "dev3.group_2", "dev4.group_2"]

def test_nested_data_a_string(self):
f = F(nested_data__a_string="asdasd")
filtered = sorted(list((inventory.filter(f).hosts.keys())))

assert filtered == ["dev1.group_1"]

def test_nested_data_a_string_contains(self):
f = F(nested_data__a_string__contains="asd")
filtered = sorted(list((inventory.filter(f).hosts.keys())))

assert filtered == ["dev1.group_1"]

def test_nested_data_a_dict_contains(self):
f = F(nested_data__a_dict__contains="a")
filtered = sorted(list((inventory.filter(f).hosts.keys())))

assert filtered == ["dev1.group_1"]

def test_nested_data_a_dict_element(self):
f = F(nested_data__a_dict__a=1)
filtered = sorted(list((inventory.filter(f).hosts.keys())))

assert filtered == ["dev1.group_1"]

def test_nested_data_a_dict_doesnt_contain(self):
f = ~F(nested_data__a_dict__contains="a")
filtered = sorted(list((inventory.filter(f).hosts.keys())))

assert filtered == ["dev2.group_1", "dev3.group_2", "dev4.group_2"]

def test_nested_data_a_list_contains(self):
f = F(nested_data__a_list__contains=2)
filtered = sorted(list((inventory.filter(f).hosts.keys())))

assert filtered == ["dev1.group_1", "dev2.group_1"]

def test_filtering_by_callable_has_parent_group(self):
f = F(has_parent_group="parent_group")
filtered = sorted(list((inventory.filter(f).hosts.keys())))

assert filtered == ["dev1.group_1", "dev2.group_1"]

def test_filtering_by_attribute_name(self):
f = F(name="dev1.group_1")
filtered = sorted(list((inventory.filter(f).hosts.keys())))

assert filtered == ["dev1.group_1"]
13 changes: 12 additions & 1 deletion tests/core/test_inventory.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,12 +204,19 @@ def test_has_parents(self):
assert not inventory.hosts["dev1.group_1"].has_parent_group(
inventory.groups["group_2"]
)
assert inventory.hosts["dev1.group_1"].has_parent_group("group_1")
assert not inventory.hosts["dev1.group_1"].has_parent_group("group_2")

def test_to_dict(self):
expected = {
"hosts": {
"dev1.group_1": {
"name": "dev1.group_1",
"nested_data": {
"a_dict": {"a": 1, "b": 2},
"a_list": [1, 2],
"a_string": "asdasd",
},
"groups": ["group_1"],
"my_var": "comes_from_dev1.group_1",
"www_server": "nginx",
Expand All @@ -230,8 +237,12 @@ def test_to_dict(self):
},
"groups": {
"defaults": {},
"parent_group": {"a_var": "blah", "name": "parent_group"},
"group_1": {
"name": "group_1", "my_var": "comes_from_group_1", "site": "site1"
"name": "group_1",
"my_var": "comes_from_group_1",
"site": "site1",
"groups": ["parent_group"],
},
"group_2": {"name": "group_2", "site": "site2"},
},
Expand Down
5 changes: 5 additions & 0 deletions tests/inventory_data/groups.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,14 @@ defaults:
nornir_password: docker
nornir_os: linux

parent_group:
a_var: blah

group_1:
my_var: comes_from_group_1
site: site1
groups:
- parent_group

group_2:
site: site2
12 changes: 12 additions & 0 deletions tests/inventory_data/hosts.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,25 @@ dev1.group_1:
role: www
nornir_ssh_port: 65001
nornir_nos: eos
nested_data:
a_dict:
a: 1
b: 2
a_list: [1, 2]
a_string: "asdasd"

dev2.group_1:
groups:
- group_1
role: db
nornir_ssh_port: 65002
nornir_nos: junos
nested_data:
a_dict:
b: 2
c: 3
a_list: [2, 3]
a_string: "qwe"

dev3.group_2:
groups:
Expand Down
Empty file.

0 comments on commit 78758d0

Please sign in to comment.