From 78758d024500c64fc71414c6423da97b4f8cad05 Mon Sep 17 00:00:00 2001 From: David Barroso Date: Tue, 17 Jul 2018 12:06:47 +0200 Subject: [PATCH] Filter object (#176) --- docs/tutorials/intro/inventory.ipynb | 382 +++++++++++++++----- docs/tutorials/intro/inventory/hosts.yaml | 12 + nornir/core/__init__.py | 4 +- nornir/core/filter.py | 88 +++++ nornir/core/inventory.py | 18 +- tests/core/test_filter.py | 114 ++++++ tests/core/test_inventory.py | 13 +- tests/inventory_data/groups.yaml | 5 + tests/inventory_data/hosts.yaml | 12 + tests/plugins/inventory/helpers/__init__.py | 0 10 files changed, 542 insertions(+), 106 deletions(-) create mode 100644 nornir/core/filter.py create mode 100644 tests/core/test_filter.py create mode 100644 tests/plugins/inventory/helpers/__init__.py diff --git a/docs/tutorials/intro/inventory.ipynb b/docs/tutorials/intro/inventory.ipynb index 24611c5f..8bbadabb 100644 --- a/docs/tutorials/intro/inventory.ipynb +++ b/docs/tutorials/intro/inventory.ipynb @@ -117,132 +117,144 @@ " 10 - cmh\n", " 11 nornir_nos: linux\n", " 12 type: host\n", - " 13 \n", - " 14 host2.cmh:\n", - " 15 nornir_host: 127.0.0.1\n", - " 16 nornir_ssh_port: 2202\n", - " 17 nornir_username: vagrant\n", - " 18 nornir_password: vagrant\n", - " 19 site: cmh\n", - " 20 role: host\n", - " 21 groups:\n", - " 22 - cmh\n", - " 23 nornir_nos: linux\n", - " 24 type: host\n", - " 25 \n", - " 26 spine00.cmh:\n", - " 27 nornir_host: 127.0.0.1\n", - " 28 nornir_username: vagrant\n", - " 29 nornir_password: vagrant\n", - " 30 nornir_network_api_port: 12444\n", - " 31 site: cmh\n", - " 32 role: spine\n", - " 33 groups:\n", - " 34 - cmh\n", - " 35 nornir_nos: eos\n", - " 36 type: network_device\n", + " 13 nested_data:\n", + " 14 a_dict:\n", + " 15 a: 1\n", + " 16 b: 2\n", + " 17 a_list: [1, 2]\n", + " 18 a_string: "asdasd"\n", + " 19 \n", + " 20 host2.cmh:\n", + " 21 nornir_host: 127.0.0.1\n", + " 22 nornir_ssh_port: 2202\n", + " 23 nornir_username: vagrant\n", + " 24 nornir_password: vagrant\n", + " 25 site: cmh\n", + " 26 role: host\n", + " 27 groups:\n", + " 28 - cmh\n", + " 29 nornir_nos: linux\n", + " 30 type: host\n", + " 31 nested_data:\n", + " 32 a_dict:\n", + " 33 b: 2\n", + " 34 c: 3\n", + " 35 a_list: [1, 2]\n", + " 36 a_string: "qwe"\n", " 37 \n", - " 38 spine01.cmh:\n", + " 38 spine00.cmh:\n", " 39 nornir_host: 127.0.0.1\n", " 40 nornir_username: vagrant\n", - " 41 nornir_password: ""\n", - " 42 nornir_network_api_port: 12204\n", + " 41 nornir_password: vagrant\n", + " 42 nornir_network_api_port: 12444\n", " 43 site: cmh\n", " 44 role: spine\n", " 45 groups:\n", " 46 - cmh\n", - " 47 nornir_nos: junos\n", + " 47 nornir_nos: eos\n", " 48 type: network_device\n", " 49 \n", - " 50 leaf00.cmh:\n", + " 50 spine01.cmh:\n", " 51 nornir_host: 127.0.0.1\n", " 52 nornir_username: vagrant\n", - " 53 nornir_password: vagrant\n", - " 54 nornir_network_api_port: 12443\n", + " 53 nornir_password: ""\n", + " 54 nornir_network_api_port: 12204\n", " 55 site: cmh\n", - " 56 role: leaf\n", + " 56 role: spine\n", " 57 groups:\n", " 58 - cmh\n", - " 59 nornir_nos: eos\n", + " 59 nornir_nos: junos\n", " 60 type: network_device\n", - " 61 asn: 65100\n", - " 62 \n", - " 63 leaf01.cmh:\n", - " 64 nornir_host: 127.0.0.1\n", - " 65 nornir_username: vagrant\n", - " 66 nornir_password: ""\n", - " 67 nornir_network_api_port: 12203\n", - " 68 site: cmh\n", - " 69 role: leaf\n", - " 70 groups:\n", - " 71 - cmh\n", - " 72 nornir_nos: junos\n", - " 73 type: network_device\n", - " 74 asn: 65101\n", - " 75 \n", - " 76 host1.bma:\n", - " 77 site: bma\n", - " 78 role: host\n", - " 79 groups:\n", - " 80 - bma\n", - " 81 nornir_nos: linux\n", - " 82 type: host\n", - " 83 \n", - " 84 host2.bma:\n", - " 85 site: bma\n", - " 86 role: host\n", - " 87 groups:\n", - " 88 - bma\n", - " 89 nornir_nos: linux\n", - " 90 type: host\n", - " 91 \n", - " 92 spine00.bma:\n", - " 93 nornir_host: 127.0.0.1\n", - " 94 nornir_username: vagrant\n", - " 95 nornir_password: vagrant\n", - " 96 nornir_network_api_port: 12444\n", + " 61 \n", + " 62 leaf00.cmh:\n", + " 63 nornir_host: 127.0.0.1\n", + " 64 nornir_username: vagrant\n", + " 65 nornir_password: vagrant\n", + " 66 nornir_network_api_port: 12443\n", + " 67 site: cmh\n", + " 68 role: leaf\n", + " 69 groups:\n", + " 70 - cmh\n", + " 71 nornir_nos: eos\n", + " 72 type: network_device\n", + " 73 asn: 65100\n", + " 74 \n", + " 75 leaf01.cmh:\n", + " 76 nornir_host: 127.0.0.1\n", + " 77 nornir_username: vagrant\n", + " 78 nornir_password: ""\n", + " 79 nornir_network_api_port: 12203\n", + " 80 site: cmh\n", + " 81 role: leaf\n", + " 82 groups:\n", + " 83 - cmh\n", + " 84 nornir_nos: junos\n", + " 85 type: network_device\n", + " 86 asn: 65101\n", + " 87 \n", + " 88 host1.bma:\n", + " 89 site: bma\n", + " 90 role: host\n", + " 91 groups:\n", + " 92 - bma\n", + " 93 nornir_nos: linux\n", + " 94 type: host\n", + " 95 \n", + " 96 host2.bma:\n", " 97 site: bma\n", - " 98 role: spine\n", + " 98 role: host\n", " 99 groups:\n", "100 - bma\n", - "101 nornir_nos: eos\n", - "102 type: network_device\n", + "101 nornir_nos: linux\n", + "102 type: host\n", "103 \n", - "104 spine01.bma:\n", + "104 spine00.bma:\n", "105 nornir_host: 127.0.0.1\n", "106 nornir_username: vagrant\n", - "107 nornir_password: ""\n", - "108 nornir_network_api_port: 12204\n", + "107 nornir_password: vagrant\n", + "108 nornir_network_api_port: 12444\n", "109 site: bma\n", "110 role: spine\n", "111 groups:\n", "112 - bma\n", - "113 nornir_nos: junos\n", + "113 nornir_nos: eos\n", "114 type: network_device\n", "115 \n", - "116 leaf00.bma:\n", + "116 spine01.bma:\n", "117 nornir_host: 127.0.0.1\n", "118 nornir_username: vagrant\n", - "119 nornir_password: vagrant\n", - "120 nornir_network_api_port: 12443\n", + "119 nornir_password: ""\n", + "120 nornir_network_api_port: 12204\n", "121 site: bma\n", - "122 role: leaf\n", + "122 role: spine\n", "123 groups:\n", "124 - bma\n", - "125 nornir_nos: eos\n", + "125 nornir_nos: junos\n", "126 type: network_device\n", "127 \n", - "128 leaf01.bma:\n", + "128 leaf00.bma:\n", "129 nornir_host: 127.0.0.1\n", "130 nornir_username: vagrant\n", - "131 nornir_password: wrong_password\n", - "132 nornir_network_api_port: 12203\n", + "131 nornir_password: vagrant\n", + "132 nornir_network_api_port: 12443\n", "133 site: bma\n", "134 role: leaf\n", "135 groups:\n", "136 - bma\n", - "137 nornir_nos: junos\n", + "137 nornir_nos: eos\n", "138 type: network_device\n", + "139 \n", + "140 leaf01.bma:\n", + "141 nornir_host: 127.0.0.1\n", + "142 nornir_username: vagrant\n", + "143 nornir_password: wrong_password\n", + "144 nornir_network_api_port: 12203\n", + "145 site: bma\n", + "146 role: leaf\n", + "147 groups:\n", + "148 - bma\n", + "149 nornir_nos: junos\n", + "150 type: network_device\n", "\n", "\n" ], @@ -408,7 +420,7 @@ { "data": { "text/plain": [ - "" + "" ] }, "execution_count": 4, @@ -438,18 +450,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, @@ -469,10 +481,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, @@ -965,10 +977,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, @@ -986,7 +998,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." ] }, { @@ -1032,6 +1051,169 @@ "# 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())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can also access nested data and even check if dicts/lists/strings contains elements. Again, let's see by example:" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "dict_keys(['host1.cmh'])\n" + ] + } + ], + "source": [ + "nested_string_asd = nr.filter(F(nested_data__a_string__contains=\"asd\"))\n", + "print(nested_string_asd.inventory.hosts.keys())" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "dict_keys(['host2.cmh'])\n" + ] + } + ], + "source": [ + "a_dict_element_equals = nr.filter(F(nested_data__a_dict__c=3))\n", + "print(a_dict_element_equals.inventory.hosts.keys())" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "dict_keys(['host1.cmh', 'host2.cmh'])\n" + ] + } + ], + "source": [ + "a_list_contains = nr.filter(F(nested_data__a_list__contains=2))\n", + "print(a_list_contains.inventory.hosts.keys())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can basically access any nested data by separating the elements in the path with two underscores `__`. Then you can use `__contains` to check if an element exists or if a string has a particular substring." + ] } ], "metadata": { diff --git a/docs/tutorials/intro/inventory/hosts.yaml b/docs/tutorials/intro/inventory/hosts.yaml index cc253298..d48d5c21 100644 --- a/docs/tutorials/intro/inventory/hosts.yaml +++ b/docs/tutorials/intro/inventory/hosts.yaml @@ -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 @@ -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 diff --git a/nornir/core/__init__.py b/nornir/core/__init__.py index 6905fb76..ce08fe41 100644 --- a/nornir/core/__init__.py +++ b/nornir/core/__init__.py @@ -131,7 +131,7 @@ 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` @@ -139,7 +139,7 @@ def filter(self, **kwargs): :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): diff --git a/nornir/core/filter.py b/nornir/core/filter.py new file mode 100644 index 00000000..8a67687b --- /dev/null +++ b/nornir/core/filter.py @@ -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 "".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 "".format(self.filters) diff --git a/nornir/core/inventory.py b/nornir/core/inventory.py index 7563188b..37744888 100644 --- a/nornir/core/inventory.py +++ b/nornir/core/inventory.py @@ -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: @@ -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:: @@ -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: diff --git a/tests/core/test_filter.py b/tests/core/test_filter.py new file mode 100644 index 00000000..a8c3018d --- /dev/null +++ b/tests/core/test_filter.py @@ -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"] diff --git a/tests/core/test_inventory.py b/tests/core/test_inventory.py index 055b4a7e..600d8cfe 100644 --- a/tests/core/test_inventory.py +++ b/tests/core/test_inventory.py @@ -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", @@ -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"}, }, diff --git a/tests/inventory_data/groups.yaml b/tests/inventory_data/groups.yaml index 81ecaa51..fc8b14ae 100644 --- a/tests/inventory_data/groups.yaml +++ b/tests/inventory_data/groups.yaml @@ -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 diff --git a/tests/inventory_data/hosts.yaml b/tests/inventory_data/hosts.yaml index 86078f08..329d4b57 100644 --- a/tests/inventory_data/hosts.yaml +++ b/tests/inventory_data/hosts.yaml @@ -7,6 +7,12 @@ 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: @@ -14,6 +20,12 @@ dev2.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: diff --git a/tests/plugins/inventory/helpers/__init__.py b/tests/plugins/inventory/helpers/__init__.py new file mode 100644 index 00000000..e69de29b