nuts 3.2.0__tar.gz → 3.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. {nuts-3.2.0 → nuts-3.3.0}/PKG-INFO +8 -3
  2. {nuts-3.2.0 → nuts-3.3.0}/README.md +4 -0
  3. {nuts-3.2.0 → nuts-3.3.0}/nuts/base_tests/napalm_bgp_neighbors.py +20 -7
  4. {nuts-3.2.0 → nuts-3.3.0}/nuts/base_tests/napalm_get_arp.py +1 -0
  5. {nuts-3.2.0 → nuts-3.3.0}/nuts/base_tests/napalm_get_config.py +1 -0
  6. {nuts-3.2.0 → nuts-3.3.0}/nuts/base_tests/napalm_get_users.py +1 -0
  7. {nuts-3.2.0 → nuts-3.3.0}/nuts/base_tests/napalm_get_vlans.py +1 -0
  8. {nuts-3.2.0 → nuts-3.3.0}/nuts/base_tests/napalm_interfaces.py +1 -0
  9. {nuts-3.2.0 → nuts-3.3.0}/nuts/base_tests/napalm_lldp_neighbors.py +4 -3
  10. {nuts-3.2.0 → nuts-3.3.0}/nuts/base_tests/napalm_network_instances.py +1 -0
  11. {nuts-3.2.0 → nuts-3.3.0}/nuts/base_tests/napalm_ping.py +1 -0
  12. {nuts-3.2.0 → nuts-3.3.0}/nuts/base_tests/netmiko_cdp_neighbors.py +1 -0
  13. {nuts-3.2.0 → nuts-3.3.0}/nuts/base_tests/netmiko_ospf_neighbors.py +1 -0
  14. {nuts-3.2.0 → nuts-3.3.0}/nuts/context.py +2 -0
  15. {nuts-3.2.0 → nuts-3.3.0}/nuts/helpers/context.py +1 -0
  16. {nuts-3.2.0 → nuts-3.3.0}/nuts/helpers/filters.py +1 -0
  17. {nuts-3.2.0 → nuts-3.3.0}/nuts/helpers/result.py +1 -1
  18. nuts-3.3.0/nuts/hooks.py +9 -0
  19. {nuts-3.2.0 → nuts-3.3.0}/nuts/index.py +2 -0
  20. {nuts-3.2.0 → nuts-3.3.0}/nuts/plugin.py +18 -3
  21. {nuts-3.2.0 → nuts-3.3.0}/nuts/yamlloader.py +3 -0
  22. {nuts-3.2.0 → nuts-3.3.0}/pyproject.toml +18 -14
  23. {nuts-3.2.0 → nuts-3.3.0}/LICENSE +0 -0
  24. {nuts-3.2.0 → nuts-3.3.0}/nuts/__init__.py +0 -0
  25. {nuts-3.2.0 → nuts-3.3.0}/nuts/base_tests/__init__.py +0 -0
  26. {nuts-3.2.0 → nuts-3.3.0}/nuts/base_tests/netmiko_iperf.py +1 -1
  27. {nuts-3.2.0 → nuts-3.3.0}/nuts/helpers/__init__.py +0 -0
  28. {nuts-3.2.0 → nuts-3.3.0}/nuts/helpers/converters.py +0 -0
  29. {nuts-3.2.0 → nuts-3.3.0}/nuts/helpers/errors.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: nuts
3
- Version: 3.2.0
3
+ Version: 3.3.0
4
4
  Summary: Network Unit Testing System
5
5
  Home-page: https://github.com/network-unit-testing-system/nuts
6
6
  License: MIT
@@ -14,13 +14,14 @@ Classifier: Programming Language :: Python :: 3
14
14
  Classifier: Programming Language :: Python :: 3.9
15
15
  Classifier: Programming Language :: Python :: 3.10
16
16
  Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
17
18
  Classifier: Topic :: System :: Networking
18
19
  Requires-Dist: PyYAML (>=6.0,<7.0)
19
20
  Requires-Dist: nornir (>=3.3.0,<4.0.0)
20
- Requires-Dist: nornir-napalm (>=0.4.0,<0.5.0)
21
+ Requires-Dist: nornir-napalm (>=0.5.0,<0.6.0)
21
22
  Requires-Dist: nornir-netmiko (>=1.0.0,<2.0.0)
22
23
  Requires-Dist: nornir-utils (>=0.2.0,<0.3.0)
23
- Requires-Dist: pytest (>=7.3.0,<8.0.0)
24
+ Requires-Dist: pytest (>=7,<8)
24
25
  Project-URL: Repository, https://github.com/network-unit-testing-system/nuts
25
26
  Description-Content-Type: text/markdown
26
27
 
@@ -76,6 +77,7 @@ Each test bundle contains the following structure:
76
77
  test_class: <name of the test class>
77
78
  label: <label to uniquely identify the test> # optional
78
79
  test_execution: <additional data used to execute the test> # optional
80
+ test_extras: <additional data can be provided to the context for custom usage> # optional
79
81
  test_data: <data used to generate the test cases>
80
82
  ...
81
83
  ```
@@ -95,6 +97,9 @@ Therefore the key-value pairs must be consistent with the key-value pairs of the
95
97
  As an example, the test definition `napalm_ping.py` calls a nornir task to execute napalm's ping-command.
96
98
  This allows the additional `max_drop` parameter in `test_execution`, since it is in turn pre-defined by napalm.
97
99
 
100
+ `test_extras`: Additional data that can be accessed through the `nuts_parameters` property.
101
+ These data are not internally utilized and can be passed for use in custom code.
102
+
98
103
  `test_data`: Data that is used to parametrize the tests in the test class which have the `pytest.mark.nuts` annotation. It is additionally part of the `nuts_parameters` property.
99
104
 
100
105
  ### Example: CDP Neighbors
@@ -50,6 +50,7 @@ Each test bundle contains the following structure:
50
50
  test_class: <name of the test class>
51
51
  label: <label to uniquely identify the test> # optional
52
52
  test_execution: <additional data used to execute the test> # optional
53
+ test_extras: <additional data can be provided to the context for custom usage> # optional
53
54
  test_data: <data used to generate the test cases>
54
55
  ...
55
56
  ```
@@ -69,6 +70,9 @@ Therefore the key-value pairs must be consistent with the key-value pairs of the
69
70
  As an example, the test definition `napalm_ping.py` calls a nornir task to execute napalm's ping-command.
70
71
  This allows the additional `max_drop` parameter in `test_execution`, since it is in turn pre-defined by napalm.
71
72
 
73
+ `test_extras`: Additional data that can be accessed through the `nuts_parameters` property.
74
+ These data are not internally utilized and can be passed for use in custom code.
75
+
72
76
  `test_data`: Data that is used to parametrize the tests in the test class which have the `pytest.mark.nuts` annotation. It is additionally part of the `nuts_parameters` property.
73
77
 
74
78
  ### Example: CDP Neighbors
@@ -1,5 +1,6 @@
1
1
  """Query BGP neighbors of a device or count them."""
2
- from typing import Dict, Callable, List, Any
2
+
3
+ from typing import Dict, Callable, List, Any, Tuple
3
4
 
4
5
  import pytest
5
6
  from nornir.core.task import MultiResult, Result
@@ -12,15 +13,23 @@ from nuts.helpers.result import AbstractHostResultExtractor, NutsResult
12
13
  class BgpNeighborsExtractor(AbstractHostResultExtractor):
13
14
  def single_transform(
14
15
  self, single_result: MultiResult
15
- ) -> Dict[str, Dict[str, NutsResult]]:
16
+ ) -> Dict[Tuple[str, str], Dict[str, NutsResult]]:
17
+ test_exectuion = self._nuts_ctx.nuts_parameters.get("test_execution")
18
+ if test_exectuion:
19
+ vrf = test_exectuion.get("vrf", "global")
20
+ if not vrf:
21
+ vrf = "global"
22
+ else:
23
+ vrf = "global"
24
+
16
25
  neighbors = self._simple_extract(single_result)["bgp_neighbors"]
17
- if "global" not in neighbors:
26
+ if vrf not in neighbors:
18
27
  return {}
19
- global_scope = neighbors["global"]
20
- router_id = global_scope["router_id"]
28
+ scope = neighbors[vrf]
29
+ router_id = scope["router_id"]
21
30
  return {
22
31
  peer: self._add_local_id(details, router_id)
23
- for peer, details in global_scope["peers"].items()
32
+ for peer, details in scope["peers"].items()
24
33
  }
25
34
 
26
35
  def _add_local_id(self, element: Dict[str, Any], router_id: str) -> Dict[str, Any]:
@@ -33,6 +42,8 @@ class BgpNeighborsContext(NornirNutsContext):
33
42
  return napalm_get
34
43
 
35
44
  def nuts_arguments(self) -> Dict[str, List[str]]:
45
+ # Overrides nuts_arguments.nuts_arguments
46
+ # "test_execution" is not passed to the nuts_task
36
47
  return {"getters": ["bgp_neighbors"]}
37
48
 
38
49
  def nuts_extractor(self) -> BgpNeighborsExtractor:
@@ -48,7 +59,9 @@ class TestNapalmBgpNeighborsCount:
48
59
  self, single_result: NutsResult, neighbor_count: int
49
60
  ) -> None:
50
61
  assert single_result.result is not None
51
- assert len(single_result.result) == neighbor_count
62
+ assert (
63
+ len(single_result.result) == neighbor_count
64
+ ), f"Expected {neighbor_count}; got {len(single_result.result)}"
52
65
 
53
66
 
54
67
  class TestNapalmBgpNeighbors:
@@ -1,4 +1,5 @@
1
1
  """Query arp table of a device."""
2
+
2
3
  from typing import Dict, Callable, List, Any, Text
3
4
 
4
5
  import pytest
@@ -1,4 +1,5 @@
1
1
  """Query config of a device."""
2
+
2
3
  from typing import Dict, Callable, List, Any
3
4
 
4
5
  import pytest
@@ -1,4 +1,5 @@
1
1
  """Query users of a device."""
2
+
2
3
  from typing import Dict, Callable, List, Any
3
4
 
4
5
  import pytest
@@ -1,4 +1,5 @@
1
1
  """Query vlans of a device."""
2
+
2
3
  from typing import Dict, Callable, List, Any
3
4
 
4
5
  import pytest
@@ -1,4 +1,5 @@
1
1
  """Query interfaces and their information of a device."""
2
+
2
3
  from typing import Dict, Callable, List, Any
3
4
 
4
5
  import pytest
@@ -1,4 +1,5 @@
1
1
  """Query LLDP neighbors of a device."""
2
+
2
3
  from typing import Dict, Callable, List, Any
3
4
 
4
5
  import pytest
@@ -28,9 +29,9 @@ class LldpNeighborsExtractor(AbstractHostResultExtractor):
28
29
  return element
29
30
 
30
31
  def _add_expanded_remote_port(self, element: Dict[str, Any]) -> Dict[str, Any]:
31
- element[
32
- "remote_port_expanded"
33
- ] = InterfaceNameConverter().expand_interface_name(element["remote_port"])
32
+ element["remote_port_expanded"] = (
33
+ InterfaceNameConverter().expand_interface_name(element["remote_port"])
34
+ )
34
35
  return element
35
36
 
36
37
 
@@ -1,4 +1,5 @@
1
1
  """Query network instances of a device."""
2
+
2
3
  from typing import Dict, List, Callable, Any
3
4
 
4
5
  import pytest
@@ -1,4 +1,5 @@
1
1
  """Let a device ping another device."""
2
+
2
3
  from enum import Enum
3
4
  from typing import Dict, Callable, Any, List
4
5
 
@@ -1,4 +1,5 @@
1
1
  """Query CDP neighbors of a device."""
2
+
2
3
  from typing import Callable, Dict, Any
3
4
 
4
5
  import pytest
@@ -1,4 +1,5 @@
1
1
  """Query OSPF neighbors of a device or count them."""
2
+
2
3
  from typing import Callable, Dict, Any
3
4
 
4
5
  import pytest
@@ -1,4 +1,5 @@
1
1
  """Provide necessary information that is needed for a specific test."""
2
+
2
3
  import pathlib
3
4
  from typing import Any, Callable, Optional, Dict, List
4
5
  from pytest import Config
@@ -202,6 +203,7 @@ class NornirNutsContext(NutsContext):
202
203
  )
203
204
 
204
205
  self.teardown()
206
+ selected_hosts.close_connections(on_good=True, on_failed=True)
205
207
  return overall_results
206
208
 
207
209
  def setup(self) -> None:
@@ -1,6 +1,7 @@
1
1
  """
2
2
  Context helper functions
3
3
  """
4
+
4
5
  import types
5
6
  from nuts.context import NutsContext
6
7
  from typing import Dict, List, Any
@@ -2,6 +2,7 @@
2
2
  Functions to filter the nornir inventory and used in conjunction with
3
3
  a context's nornir_filter function.
4
4
  """
5
+
5
6
  from typing import Optional, Dict, Any, List, Union
6
7
  from nornir.core.filter import F, OR
7
8
 
@@ -46,7 +46,7 @@ class NutsResult:
46
46
  f"An exception has occurred while executing nornir:\n"
47
47
  f"{header}\n"
48
48
  f"{self._result}"
49
- )
49
+ ) from self.exception
50
50
  if self.failed:
51
51
  raise NutsNornirError(f"Nornir execution has failed:\n" f"{self._result}")
52
52
 
@@ -0,0 +1,9 @@
1
+ from pytest import FixtureRequest
2
+ from nuts.context import NutsContext
3
+ from nuts.helpers.result import NutsResult
4
+
5
+
6
+ def pytest_nuts_single_result(
7
+ request: FixtureRequest, nuts_ctx: NutsContext, result: NutsResult
8
+ ) -> None:
9
+ """Called in the single result fixture"""
@@ -1,4 +1,5 @@
1
1
  """Allows to indicate only the test class name in a test bundle."""
2
+
2
3
  from typing import Optional
3
4
 
4
5
  default_index = {
@@ -10,6 +11,7 @@ default_index = {
10
11
  "TestNapalmNetworkInstances": "nuts.base_tests.napalm_network_instances",
11
12
  "TestNapalmPing": "nuts.base_tests.napalm_ping",
12
13
  "TestNapalmUsers": "nuts.base_tests.napalm_get_users",
14
+ "TestNapalmOnlyDefinedUsersExist": "nuts.base_tests.napalm_get_users",
13
15
  "TestNapalmConfig": "nuts.base_tests.napalm_get_config",
14
16
  "TestNapalmVlans": "nuts.base_tests.napalm_get_vlans",
15
17
  "TestNapalmOnlyDefinedVlansExist": "nuts.base_tests.napalm_get_vlans",
@@ -1,4 +1,5 @@
1
1
  """Fixtures"""
2
+
2
3
  from typing import Optional, Dict, Any
3
4
  from pathlib import Path
4
5
 
@@ -16,6 +17,12 @@ from nuts.helpers.result import NutsResult
16
17
  from nuts.yamlloader import NutsYamlFile, get_parametrize_data
17
18
 
18
19
 
20
+ def pytest_addhooks(pluginmanager):
21
+ from nuts import hooks
22
+
23
+ pluginmanager.add_hookspecs(hooks)
24
+
25
+
19
26
  @pytest.fixture(scope="class")
20
27
  def nuts_ctx(request: FixtureRequest) -> NutsContext:
21
28
  params = request.node.params
@@ -26,7 +33,9 @@ def nuts_ctx(request: FixtureRequest) -> NutsContext:
26
33
 
27
34
 
28
35
  @pytest.fixture
29
- def single_result(nuts_ctx: NutsContext, nuts_test_entry: Dict[str, Any]) -> NutsResult:
36
+ def single_result(
37
+ nuts_ctx: NutsContext, nuts_test_entry: Dict[str, Any], request: FixtureRequest
38
+ ) -> NutsResult:
30
39
  """
31
40
  Returns the result which belongs to a specific host
32
41
  out of the overall set of results that has been returned by nornir's task.
@@ -39,6 +48,12 @@ def single_result(nuts_ctx: NutsContext, nuts_test_entry: Dict[str, Any]) -> Nut
39
48
  """
40
49
  res = nuts_ctx.extractor.single_result(nuts_test_entry)
41
50
  res.validate()
51
+
52
+ # Invoke the pytest_nuts_single_result hook to extend result reports.
53
+ request.config.hook.pytest_nuts_single_result(
54
+ request=request, nuts_ctx=nuts_ctx, result=res
55
+ )
56
+
42
57
  return res
43
58
 
44
59
 
@@ -64,12 +79,12 @@ def pytest_collect_file(parent: Session, file_path: Path) -> Optional[Collector]
64
79
  """
65
80
  Performs the collection phase for the given pytest session.
66
81
  Collects all test bundles if available, i.e. files starting
67
- with 'test' and ending in .yaml.
82
+ with 'test' and ending in .yaml or .yml.
68
83
  :param parent: pytest session object
69
84
  :param file_path: path to test file(s)
70
85
  :return: The pytest collector if found
71
86
  """
72
- if file_path.suffix == ".yaml" and file_path.name.startswith("test"):
87
+ if file_path.suffix in [".yaml", ".yml"] and file_path.name.startswith("test"):
73
88
  return NutsYamlFile.from_parent(parent, path=file_path)
74
89
  return None
75
90
 
@@ -1,6 +1,7 @@
1
1
  """Converts a test bundle (YAML file) into a test class for pytest.
2
2
  Based on https://docs.pytest.org/en/stable/example/nonpython.html#yaml-plugin
3
3
  """
4
+
4
5
  import importlib
5
6
  from nuts.helpers.context import load_context
6
7
  import types
@@ -106,12 +107,14 @@ class NutsTestFile(pytest.Module):
106
107
 
107
108
  test_data = self.test_entry.get("test_data", [])
108
109
  test_execution = self.test_entry.get("test_execution")
110
+ test_extras = self.test_entry.get("test_extras")
109
111
  yield NutsTestClass.from_parent(
110
112
  self,
111
113
  name=name,
112
114
  class_name=class_name,
113
115
  test_data=test_data,
114
116
  test_execution=test_execution,
117
+ test_extras=test_extras,
115
118
  )
116
119
 
117
120
 
@@ -4,10 +4,17 @@ build-backend = "poetry.core.masonry.api"
4
4
 
5
5
  [tool.poetry]
6
6
  name = "nuts"
7
- version = "3.2.0"
7
+ version = "3.3.0"
8
8
  description = "Network Unit Testing System"
9
- authors = ["Lukas Murer, Méline Sieber, Urs Baumann, Matthias Gabriel, Florian Bruhin", "Marco Martinez", "Severin Grimm"]
10
- maintainers = ["Marco Martinez <marco.maritnez@ost.ch>", "Urs Baumann <github@m.ubaumann.ch>"]
9
+ authors = [
10
+ "Lukas Murer, Méline Sieber, Urs Baumann, Matthias Gabriel, Florian Bruhin",
11
+ "Marco Martinez",
12
+ "Severin Grimm",
13
+ ]
14
+ maintainers = [
15
+ "Marco Martinez <marco.maritnez@ost.ch>",
16
+ "Urs Baumann <github@m.ubaumann.ch>",
17
+ ]
11
18
  classifiers = ["Framework :: Pytest", "Topic :: System :: Networking"]
12
19
  homepage = "https://github.com/network-unit-testing-system/nuts"
13
20
  repository = "https://github.com/network-unit-testing-system/nuts"
@@ -16,26 +23,26 @@ readme = "README.md"
16
23
 
17
24
  [tool.poetry.dependencies]
18
25
  python = "^3.8.1"
19
- pytest = "^7.3.0"
26
+ pytest = "^7"
20
27
  PyYAML = "^6.0"
21
28
  nornir = "^3.3.0"
22
- nornir-napalm = "^0.4.0"
29
+ nornir-napalm = "^0.5.0"
23
30
  nornir-netmiko = "^1.0.0"
24
31
  nornir-utils = "^0.2.0"
25
32
 
26
33
  [tool.poetry.dev-dependencies]
27
34
  tox = "^4.4.11"
28
35
  pytest-cov = "^4.0.0"
29
- black = "^23.3.0"
30
- sphinx = "^7.1.2"
36
+ black = "^24.2.0"
37
+ sphinx = "^7"
31
38
  mypy = "^1.0.1"
32
- flake8 = "^6.0.0"
39
+ flake8 = "^7.0.0"
33
40
  types-PyYAML = "*"
34
41
  types-setuptools = "*"
35
42
  types-toml = "*"
36
43
 
37
44
  [tool.poetry.plugins."pytest11"]
38
- "nuts"="nuts.plugin"
45
+ "nuts" = "nuts.plugin"
39
46
  # why "pytest11": https://github.com/pytest-dev/pytest/commit/ed03eef81b220199a819632a27f9c452b8e1fb81
40
47
  # https://docs.pytest.org/en/latest/how-to/writing_plugins.html#making-your-plugin-installable-by-others
41
48
 
@@ -50,12 +57,10 @@ filterwarnings = [
50
57
  "ignore:Using or importing the ABCs from 'collections':DeprecationWarning:jnpr\\.junos\\.factcache",
51
58
  "ignore:NutsYamlFile\\.fspath is deprecated and will be replaced by NutsYamlFile\\.path\\.",
52
59
  ]
53
- markers = [
54
- "nuts_test_ctx: A NutsContext to be used in tests."
55
- ]
60
+ markers = ["nuts_test_ctx: A NutsContext to be used in tests."]
56
61
 
57
62
  [tool.mypy]
58
- python_version = 3.8
63
+ python_version = "3.8"
59
64
 
60
65
  ### --strict
61
66
  warn_unused_configs = true
@@ -149,4 +154,3 @@ commands = pytest {posargs} --junitxml=test-reports/pytest.xml --cov="{envsitepa
149
154
  # envsitepackagesdir see: https://tox.readthedocs.io/en/latest/example/pytest.html
150
155
 
151
156
  """
152
-
File without changes
File without changes
File without changes
@@ -1,4 +1,5 @@
1
1
  """Query bandwidth performance between two devices."""
2
+
2
3
  import pytest
3
4
  import json
4
5
  from typing import Dict, Callable, Any
@@ -106,7 +107,6 @@ CONTEXT = IperfContext
106
107
 
107
108
 
108
109
  class IperfResultError(Error):
109
-
110
110
  """Error in iperf result JSON."""
111
111
 
112
112
 
File without changes
File without changes
File without changes