Skip to content

Commit

Permalink
Merge 4feca8a into 631cbbe
Browse files Browse the repository at this point in the history
  • Loading branch information
dbarrosop committed Jul 10, 2018
2 parents 631cbbe + 4feca8a commit 8d151c8
Show file tree
Hide file tree
Showing 6 changed files with 269 additions and 18 deletions.
132 changes: 117 additions & 15 deletions docs/tutorials/intro/inventory.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -408,7 +408,7 @@
{
"data": {
"text/plain": [
"<nornir.plugins.inventory.simple.SimpleInventory at 0x10fb17cf8>"
"<nornir.plugins.inventory.simple.SimpleInventory at 0x10c4d8080>"
]
},
"execution_count": 4,
Expand Down Expand Up @@ -438,18 +438,18 @@
{
"data": {
"text/plain": [
"{'host1.bma': Host: host1.bma,\n",
" 'host1.cmh': Host: host1.cmh,\n",
" 'host2.bma': Host: host2.bma,\n",
"{'host1.cmh': Host: host1.cmh,\n",
" 'host2.cmh': Host: host2.cmh,\n",
" 'leaf00.bma': Host: leaf00.bma,\n",
" 'spine00.cmh': Host: spine00.cmh,\n",
" 'spine01.cmh': Host: spine01.cmh,\n",
" 'leaf00.cmh': Host: leaf00.cmh,\n",
" 'leaf01.bma': Host: leaf01.bma,\n",
" 'leaf01.cmh': Host: leaf01.cmh,\n",
" 'host1.bma': Host: host1.bma,\n",
" 'host2.bma': Host: host2.bma,\n",
" 'spine00.bma': Host: spine00.bma,\n",
" 'spine00.cmh': Host: spine00.cmh,\n",
" 'spine01.bma': Host: spine01.bma,\n",
" 'spine01.cmh': Host: spine01.cmh}"
" 'leaf00.bma': Host: leaf00.bma,\n",
" 'leaf01.bma': Host: leaf01.bma}"
]
},
"execution_count": 5,
Expand All @@ -469,10 +469,10 @@
{
"data": {
"text/plain": [
"{'bma': Group: bma,\n",
" 'cmh': Group: cmh,\n",
"{'global': Group: global,\n",
" 'eu': Group: eu,\n",
" 'global': Group: global}"
" 'bma': Group: bma,\n",
" 'cmh': Group: cmh}"
]
},
"execution_count": 6,
Expand Down Expand Up @@ -965,10 +965,10 @@
"text/plain": [
"{'host1.bma': Host: host1.bma,\n",
" 'host2.bma': Host: host2.bma,\n",
" 'leaf00.bma': Host: leaf00.bma,\n",
" 'leaf01.bma': Host: leaf01.bma,\n",
" 'spine00.bma': Host: spine00.bma,\n",
" 'spine01.bma': Host: spine01.bma}"
" 'spine01.bma': Host: spine01.bma,\n",
" 'leaf00.bma': Host: leaf00.bma,\n",
" 'leaf01.bma': Host: leaf01.bma}"
]
},
"execution_count": 21,
Expand All @@ -986,7 +986,14 @@
"source": [
"#### Advanced filtering\n",
"\n",
"Sometimes you need more fancy filtering. For those cases you can use a filter function:"
"Sometimes you need more fancy filtering. For those cases you have two options:\n",
"\n",
"1. Use a filter function.\n",
"2. Use a filter object.\n",
"\n",
"##### Filter functions\n",
"\n",
"The ``filter_func`` parameter let's you run your own code to filter the hosts. The function signature is as simple as ``my_func(host)`` where host is an object of type [Host](../../ref/api/inventory.rst#nornir.core.inventory.Host) and it has to return either ``True`` or ``False`` to indicate if you want to host or not."
]
},
{
Expand Down Expand Up @@ -1032,6 +1039,101 @@
"# Or a lambda function\n",
"nr.filter(filter_func=lambda h: len(h.name) == 9).inventory.hosts.keys()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"##### Filter Object\n",
"\n",
"You can also use a filter object to create incrementally a complext query object. Let's see how it works by example:"
]
},
{
"cell_type": "code",
"execution_count": 24,
"metadata": {},
"outputs": [],
"source": [
"# first you need to import the F object\n",
"from nornir.core.filter import F"
]
},
{
"cell_type": "code",
"execution_count": 25,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"dict_keys(['host1.cmh', 'host2.cmh', 'spine00.cmh', 'spine01.cmh', 'leaf00.cmh', 'leaf01.cmh'])\n"
]
}
],
"source": [
"# hosts in group cmh\n",
"cmh = nr.filter(F(groups__contains=\"cmh\"))\n",
"print(cmh.inventory.hosts.keys())"
]
},
{
"cell_type": "code",
"execution_count": 26,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"dict_keys(['host1.cmh', 'host2.cmh', 'spine00.cmh', 'leaf00.cmh', 'host1.bma', 'host2.bma', 'spine00.bma', 'leaf00.bma'])\n"
]
}
],
"source": [
"# devices running either linux or eos\n",
"linux_or_eos = nr.filter(F(nornir_nos=\"linux\") | F(nornir_nos=\"eos\"))\n",
"print(linux_or_eos.inventory.hosts.keys())"
]
},
{
"cell_type": "code",
"execution_count": 27,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"dict_keys(['spine00.cmh', 'spine01.cmh'])\n"
]
}
],
"source": [
"# spines in cmh\n",
"cmh_and_spine = nr.filter(F(groups__contains=\"cmh\") & F(role=\"spine\"))\n",
"print(cmh_and_spine.inventory.hosts.keys())"
]
},
{
"cell_type": "code",
"execution_count": 28,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"dict_keys(['host1.cmh', 'host2.cmh', 'leaf00.cmh', 'leaf01.cmh'])\n"
]
}
],
"source": [
"# cmh devices that are not spines\n",
"cmh_and_not_spine = nr.filter(F(groups__contains=\"cmh\") & ~F(role=\"spine\"))\n",
"print(cmh_and_not_spine.inventory.hosts.keys())"
]
}
],
"metadata": {
Expand Down
4 changes: 2 additions & 2 deletions nornir/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,15 +162,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
81 changes: 81 additions & 0 deletions nornir/core/filter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
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)

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)
4 changes: 3 additions & 1 deletion nornir/core/inventory.py
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,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 @@ -336,9 +336,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
66 changes: 66 additions & 0 deletions tests/core/test_filter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
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"]
Empty file.

0 comments on commit 8d151c8

Please sign in to comment.