nornir-collection 0.0.27__tar.gz → 0.0.29__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 (64) hide show
  1. {nornir_collection-0.0.27 → nornir_collection-0.0.29}/PKG-INFO +9 -9
  2. {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/netbox/inventory.py +9 -8
  3. {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/netbox/update_prefixes_ip_addresses.py +268 -58
  4. {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/nornir_plugins/inventory/netbox.py +10 -5
  5. {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/nornir_plugins/inventory/utils.py +48 -34
  6. {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/utils.py +6 -6
  7. {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection.egg-info/PKG-INFO +9 -9
  8. {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection.egg-info/requires.txt +8 -8
  9. {nornir_collection-0.0.27 → nornir_collection-0.0.29}/setup.py +1 -1
  10. {nornir_collection-0.0.27 → nornir_collection-0.0.29}/LICENSE +0 -0
  11. {nornir_collection-0.0.27 → nornir_collection-0.0.29}/README.md +0 -0
  12. {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/__init__.py +0 -0
  13. {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/batfish/__init__.py +0 -0
  14. {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/batfish/assert_config.py +0 -0
  15. {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/batfish/utils.py +0 -0
  16. {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/cisco/__init__.py +0 -0
  17. {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/cisco/configuration_management/__init__.py +0 -0
  18. {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/cisco/configuration_management/cli/__init__.py +0 -0
  19. {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/cisco/configuration_management/cli/config_tasks.py +0 -0
  20. {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/cisco/configuration_management/cli/config_workflow.py +0 -0
  21. {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/cisco/configuration_management/cli/show_tasks.py +0 -0
  22. {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/cisco/configuration_management/netconf/__init__.py +0 -0
  23. {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/cisco/configuration_management/netconf/config_tasks.py +0 -0
  24. {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/cisco/configuration_management/netconf/config_workflow.py +0 -0
  25. {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/cisco/configuration_management/netconf/nr_cfg_iosxe_netconf.py +0 -0
  26. {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/cisco/configuration_management/netconf/ops_tasks.py +0 -0
  27. {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/cisco/configuration_management/processor.py +0 -0
  28. {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/cisco/configuration_management/pyats.py +0 -0
  29. {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/cisco/configuration_management/restconf/__init__.py +0 -0
  30. {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/cisco/configuration_management/restconf/cisco_rpc.py +0 -0
  31. {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/cisco/configuration_management/restconf/config_workflow.py +0 -0
  32. {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/cisco/configuration_management/restconf/tasks.py +0 -0
  33. {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/cisco/configuration_management/utils.py +0 -0
  34. {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/cisco/software_upgrade/__init__.py +0 -0
  35. {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/cisco/software_upgrade/cisco_software_upgrade.py +0 -0
  36. {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/cisco/software_upgrade/utils.py +0 -0
  37. {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/cisco/support_api/__init__.py +0 -0
  38. {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/cisco/support_api/api_calls.py +0 -0
  39. {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/cisco/support_api/cisco_maintenance_report.py +0 -0
  40. {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/cisco/support_api/cisco_support.py +0 -0
  41. {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/cisco/support_api/reports.py +0 -0
  42. {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/cisco/support_api/utils.py +0 -0
  43. {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/fortinet/__init__.py +0 -0
  44. {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/fortinet/utils.py +0 -0
  45. {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/git.py +0 -0
  46. {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/netbox/__init__.py +0 -0
  47. {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/netbox/custom_script.py +0 -0
  48. {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/netbox/set_device_status.py +0 -0
  49. {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/netbox/sync_datasource.py +0 -0
  50. {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/netbox/update_cisco_inventory_data.py +0 -0
  51. {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/netbox/update_cisco_support_plugin_data.py +0 -0
  52. {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/netbox/update_fortinet_inventory_data.py +0 -0
  53. {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/netbox/update_purestorage_inventory_data.py +0 -0
  54. {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/netbox/utils.py +0 -0
  55. {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/netbox/verify_device_primary_ip.py +0 -0
  56. {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/nornir_plugins/__init__.py +0 -0
  57. {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/nornir_plugins/inventory/__init__.py +0 -0
  58. {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/nornir_plugins/inventory/staggered_yaml.py +0 -0
  59. {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/purestorage/__init__.py +0 -0
  60. {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/purestorage/utils.py +0 -0
  61. {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection.egg-info/SOURCES.txt +0 -0
  62. {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection.egg-info/dependency_links.txt +0 -0
  63. {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection.egg-info/top_level.txt +0 -0
  64. {nornir_collection-0.0.27 → nornir_collection-0.0.29}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nornir-collection
3
- Version: 0.0.27
3
+ Version: 0.0.29
4
4
  Summary: Nornir-Collection contains network automation functions and complete IaC workflows with Nornir and other python libraries. It contains Nornir tasks and general functions in Nornir style.
5
5
  Author: Willi Kubny
6
6
  Author-email: willi.kubny@gmail.ch
@@ -10,19 +10,19 @@ Classifier: Operating System :: OS Independent
10
10
  Requires-Python: >=3.9
11
11
  Description-Content-Type: text/markdown
12
12
  License-File: LICENSE
13
- Requires-Dist: nornir==3.3.0
13
+ Requires-Dist: nornir==3.5.0
14
14
  Requires-Dist: nornir-jinja2==0.2.0
15
- Requires-Dist: nornir-salt==0.22.2
15
+ Requires-Dist: nornir-salt==0.22.5
16
16
  Requires-Dist: nornir-netmiko==1.0.1
17
- Requires-Dist: nornir-scrapli==2023.7.30
18
- Requires-Dist: scrapli-netconf==2023.7.30
17
+ Requires-Dist: nornir-scrapli==2024.7.30
18
+ Requires-Dist: scrapli-netconf==2024.1.30
19
+ Requires-Dist: nornir-utils==0.2.0
20
+ Requires-Dist: nornir-pyfgt==1.0.3
19
21
  Requires-Dist: scrapli[ssh2]
20
22
  Requires-Dist: scrapli[genie]
21
- Requires-Dist: nornir-utils==0.2.0
22
23
  Requires-Dist: pynetbox==7.4.0
23
- Requires-Dist: ipfabric==7.0.2
24
+ Requires-Dist: ipfabric==7.2.2
24
25
  Requires-Dist: py-pure-client==1.51.0
25
- Requires-Dist: nornir-pyfgt==1.0.3
26
26
  Requires-Dist: python-nmap==0.7.1
27
27
  Requires-Dist: pybatfish==2023.12.16.1270
28
28
  Requires-Dist: requests==2.31.0
@@ -30,7 +30,7 @@ Requires-Dist: pre-commit==3.6.0
30
30
  Requires-Dist: pyyaml==6.0.1
31
31
  Requires-Dist: pyfiglet==1.0.2
32
32
  Requires-Dist: GitPython==3.1.40
33
- Requires-Dist: python-dotenv==1.0.0
33
+ Requires-Dist: python-dotenv==1.1.1
34
34
  Requires-Dist: beautifultable==1.1.0
35
35
  Requires-Dist: pandas==2.2.2
36
36
  Requires-Dist: openpyxl==3.1.2
@@ -248,7 +248,7 @@ def _load_cisco_support_data(task: Task, all_csupp: List[Dict]) -> Result:
248
248
  #### Nornir Tasks in regular Function #######################################################################
249
249
 
250
250
 
251
- def load_additional_netbox_data(nr: Nornir, add_netbox_data: dict[str:bool]) -> Nornir:
251
+ def load_additional_netbox_data(nr: Nornir, add_netbox_data: dict[str:bool], silent: bool = False) -> Nornir:
252
252
  """
253
253
  Load additional data from NetBox into Nornir inventory based on 'add_netbox_data' options.
254
254
  """
@@ -352,10 +352,11 @@ def load_additional_netbox_data(nr: Nornir, add_netbox_data: dict[str:bool]) ->
352
352
  # Append the result message
353
353
  result_msg.append(" - Load Interfaces")
354
354
 
355
- # Print the result of the additional loaded data
356
- print_task_name(text=task_text)
357
- print(task_info(text=task_text, changed=False))
358
- print("'Load NetBox additional inventory data' -> NornirResponse <Success: True>")
359
- print("-> Additional loaded data:")
360
- for msg in result_msg:
361
- print(msg)
355
+ if not silent:
356
+ # Print the result of the additional loaded data
357
+ print_task_name(text=task_text)
358
+ print(task_info(text=task_text, changed=False))
359
+ print("'Load NetBox additional inventory data' -> NornirResponse <Success: True>")
360
+ print("-> Additional loaded data:")
361
+ for msg in result_msg:
362
+ print(msg)
@@ -6,7 +6,6 @@ The Main function is intended to import and execute by other scripts.
6
6
  """
7
7
 
8
8
  import os
9
- import sys
10
9
  import ipaddress
11
10
  import urllib.parse
12
11
  from typing import Callable, Literal, Union
@@ -14,19 +13,17 @@ from concurrent.futures import ThreadPoolExecutor
14
13
  import requests
15
14
  import nmap
16
15
  from ipfabric import IPFClient
16
+ from nornir_salt.plugins.processors import TestsProcessor
17
+ from nornir_salt.plugins.functions import FFun
18
+ from nornir_salt.plugins.tasks import scrapli_send_commands
19
+ from nornir_collection.nornir_plugins.inventory.utils import init_nornir
17
20
  from nornir_collection.netbox.utils import (
18
21
  get_nb_resources,
19
22
  post_nb_resources,
20
23
  patch_nb_resources,
21
24
  delete_nb_resources,
22
25
  )
23
- from nornir_collection.utils import (
24
- print_task_title,
25
- task_name,
26
- exit_error,
27
- load_yaml_file,
28
- task_result,
29
- )
26
+ from nornir_collection.utils import print_task_title, task_name, exit_error, task_result
30
27
 
31
28
 
32
29
  __author__ = "Willi Kubny"
@@ -37,6 +34,36 @@ __email__ = "willi.kubny@dreyfusbank.ch"
37
34
  __status__ = "Production"
38
35
 
39
36
 
37
+ def nr_ping_test(nr, hosts: list[str], ip_list: list[str], criteria: Literal["PASS", "FAIL"]) -> list[str]:
38
+ """
39
+ TBD
40
+ """
41
+ # Create the test suite list for each ip-address to ping
42
+ tests_suite = [
43
+ {
44
+ "name": ip["address"],
45
+ "task": f"ping {ipaddress.ip_interface(ip['address']).ip}",
46
+ "test": "!contains_lines",
47
+ "pattern": ["Success rate is 0", "VRF Mgmt-vrf does not have a usable source address"],
48
+ }
49
+ for ip in ip_list
50
+ ]
51
+ # Use NornirSalt FFun Filter-List option to filter on a list of hosts
52
+ nr_filtered = FFun(nr, FL=hosts)
53
+ # Add the nornir salt TestsProcessor processor
54
+ nr_with_testsprocessor = nr_filtered.with_processors(
55
+ [TestsProcessor(tests=tests_suite, build_per_host_tests=True)]
56
+ )
57
+ # Run the TestsProcessor task
58
+ results = nr_with_testsprocessor.run(task=scrapli_send_commands, on_failed=True)
59
+ # Create a list of the ip-addresses based on the required criteria
60
+ result_list = [result.name for host in results for result in results[host] if result.result == criteria]
61
+ # Create a list of ip-addresses that are part of the result_list -> This matchs the criteria PASS/FAIL
62
+ verified_ip_list = [ip for ip in ip_list if ip["address"] in result_list]
63
+
64
+ return verified_ip_list
65
+
66
+
40
67
  def load_netbox_data(task_text: str, nb_api_url: str, query: dict) -> list[dict]:
41
68
  """
42
69
  Load NetBox data using the provided task text, NetBox API URL, and query parameters.
@@ -246,9 +273,9 @@ def create_nb_ip_payload(
246
273
  md.extend(
247
274
  [[f"{k}/tcp", v["state"], v["name"]] for k, v in datasource_ip[0]["ports"].items()]
248
275
  )
249
- item["custom_fields"] = {"ipaddress_ports": make_markdown_table(md)}
276
+ item["custom_fields"] = {"ipam_ports": make_markdown_table(md)}
250
277
  else:
251
- item["custom_fields"] = {"ipaddress_ports": None}
278
+ item["custom_fields"] = {"ipam_ports": None}
252
279
  # If the desired_status is None, the payload is for all ip addresses of a prefix
253
280
  else:
254
281
  # Add the 'vrf' to the payload
@@ -306,25 +333,28 @@ def nmap_scan_host_ip_or_subnet(hosts: str) -> list:
306
333
  return nmap_scan_result
307
334
 
308
335
 
309
- def nmap_double_check_ips(ip_list: dict) -> list:
336
+ def nmap_double_check_ips(ip_list: dict, criteria: Literal["PASS", "FAIL"]) -> list:
310
337
  """
311
338
  TBD
312
339
  """
313
340
  verified_ips = []
314
- # Nmap scan ip-addresses of the maybe_delete_ips list
341
+ # Nmap scan ip-addresses of the ip list
315
342
  for ip in ip_list:
316
343
  # Scan the prefix with nmap
317
344
  scan_result = nmap_scan_host_ip_or_subnet(hosts=ip["address"])
318
345
  # Add the nmap scan result to the inactive_ips list with the ID of the ip-address
319
- if scan_result:
346
+ if scan_result and criteria == "PASS":
320
347
  # As a single ip-address is scanned the scan_result can have only one list item
321
348
  verified_ips.append({"id": ip["id"], **scan_result[0]})
349
+ elif not scan_result and criteria == "FAIL":
350
+ # As the single ip-address is not reachable, add the ip-address to the verified_ips list
351
+ verified_ips.append(ip)
322
352
 
323
- # Return the verified ip-addresses list
353
+ # Return the active ip-addresses list
324
354
  return verified_ips
325
355
 
326
356
 
327
- def get_ipfabric_data_for_prefix(prefix: dict) -> tuple:
357
+ def get_ipfabric_ip_addresses_for_prefix(prefix: dict) -> tuple:
328
358
  """
329
359
  TBD
330
360
  """
@@ -402,7 +432,7 @@ def get_nb_ips_and_external_datasource(nb_url: str, prefix: dict, ds: Literal["n
402
432
  )
403
433
  elif ds == "ip-fabric":
404
434
  # Get the ip-addresses from the IP-Fabric
405
- prefix["datasource_ips"] = get_ipfabric_data_for_prefix(prefix=prefix)
435
+ prefix["datasource_ips"] = get_ipfabric_ip_addresses_for_prefix(prefix=prefix)
406
436
 
407
437
  # Print the task result
408
438
  text = f"IP-Fabric Get Data for Prefix {prefix['prefix']} IP-Addresses"
@@ -644,7 +674,7 @@ def update_reserved_ip_addresses(nb_url: str, prefix: dict, ds: Literal["nmap",
644
674
  return result, failed
645
675
 
646
676
 
647
- def update_inactive_ip_addresses(nb_url: str, prefix: dict, ds: Literal["nmap", "ip-fabric"]) -> tuple:
677
+ def update_inactive_ip_addresses(nr, nb_url: str, prefix: dict, ds: Literal["nmap", "ip-fabric"]) -> tuple:
648
678
  """
649
679
  Updates the status of inactive IP addresses in NetBox if they are reachable by nmap.
650
680
 
@@ -665,12 +695,18 @@ def update_inactive_ip_addresses(nb_url: str, prefix: dict, ds: Literal["nmap",
665
695
  loop_list=prefix["inactive_ips"], check_list=prefix["datasource_ips"], is_in_both=True
666
696
  )
667
697
  # Double-check ip-addresses with the status 'inactive' that are not part of the datasource list
668
- maybe_active_ips = create_ip_list(
698
+ may_active_ips = create_ip_list(
669
699
  loop_list=prefix["inactive_ips"], check_list=prefix["datasource_ips"], is_in_both=False
670
700
  )
671
- # Nmap scan ip-addresses of the maybe_active_ips list
672
- nmap_result = nmap_double_check_ips(ip_list=maybe_active_ips)
673
- active_ips.extend(nmap_result)
701
+ if may_active_ips and ds == "ip-fabric":
702
+ # Double-check ip-addresses which can be scanned with nmap (all except Tier-3 and Tier-4)
703
+ if ds == "ip-fabric" and (prefix["tenant"]["slug"] not in ("tier-3", "tier-4")):
704
+ # Nmap scan ip-addresses of the may_active_ips list and add the result to the active_ips list
705
+ active_ips.extend(nmap_double_check_ips(ip_list=may_active_ips, criteria="PASS"))
706
+ # Double-check ip-addresses with a Ping from the core-switch (Tier-3 and Tier-4)
707
+ elif ds == "ip-fabric" and (prefix["tenant"]["slug"] in ("tier-3", "tier-4")):
708
+ t3_core_sw = ["T3-AES99-SWRSU1"]
709
+ active_ips.extend(nr_ping_test(nr=nr, hosts=t3_core_sw, ip_list=may_active_ips, criteria="PASS"))
674
710
 
675
711
  # If ip-addresses have been found
676
712
  if active_ips:
@@ -690,7 +726,7 @@ def update_inactive_ip_addresses(nb_url: str, prefix: dict, ds: Literal["nmap",
690
726
 
691
727
 
692
728
  def update_active_ip_addresses(
693
- nb_url: str, prefix: dict, overwrite_active: list[str], ds: Literal["nmap", "ip-fabric"]
729
+ nr, nb_url: str, prefix: dict, overwrite_active: list[str], ds: Literal["nmap", "ip-fabric"]
694
730
  ) -> tuple:
695
731
  """
696
732
  Updates the status of active IP addresses in NetBox if they are not reachable by nmap.
@@ -728,16 +764,25 @@ def update_active_ip_addresses(
728
764
  result.extend(sub_result)
729
765
  if sub_failed:
730
766
  failed = True
767
+ result.append(f"-> Data for Payload:\n{active_ips}")
731
768
  result.append(f"-> Payload:\n{payload}")
732
769
 
733
770
  # Update the ip-addresses with the status 'active' that are not part of the datacenter list
734
- maybe_inactive_ips = create_ip_list(
771
+ may_inactive = create_ip_list(
735
772
  loop_list=prefix["active_ips"], check_list=prefix["datasource_ips"], is_in_both=False
736
773
  )
737
- # Nmap scan ip-addresses of the maybe_inactive_ips list
738
- nmap_result = nmap_double_check_ips(ip_list=maybe_inactive_ips)
774
+ if may_inactive and ds == "ip-fabric":
775
+ # Double-check ip-addresses which can be scanned with nmap (all except Tier-3 and Tier-4)
776
+ if prefix["tenant"]["slug"] not in ("tier-3", "tier-4"):
777
+ # Nmap scan ip-addresses of the may_inactive list
778
+ may_inactive = nmap_double_check_ips(ip_list=may_inactive, criteria="FAIL")
779
+ # Double-check ip-addresses with a Ping from the core-switch (Tier-3 and Tier-4)
780
+ elif prefix["tenant"]["slug"] in ("tier-3", "tier-4"):
781
+ t3_core_sw = ["T3-AES99-SWRSU1"]
782
+ may_inactive = nr_ping_test(nr=nr, hosts=t3_core_sw, ip_list=may_inactive, criteria="FAIL")
783
+
739
784
  # Create a new list to exclude the overwrite_active ip-addresses
740
- inactive_ips = [ip for ip in nmap_result if ip["address"] not in overwrite_active]
785
+ inactive_ips = [ip for ip in may_inactive if ip["address"] not in overwrite_active]
741
786
 
742
787
  # If ip-addresses have been found
743
788
  if inactive_ips:
@@ -805,6 +850,8 @@ def update_netbox_prefix_ip_addresses(prefix: list, *args) -> tuple:
805
850
  ds = str(args[0])
806
851
  # Set the ip-address list to overwrite the status as active by the second arg and make sure its a list
807
852
  overwrite_active = list(args[1])
853
+ # Set the Nornir config file by the third arg
854
+ nr_config = args[2]
808
855
  # Boolian to check if any ip-addresses have been changed and the overall failed status
809
856
  changed = False
810
857
  failed = False
@@ -812,8 +859,12 @@ def update_netbox_prefix_ip_addresses(prefix: list, *args) -> tuple:
812
859
  # Print the task title
813
860
  results.append(task_name(text=f"Update NetBox IP-Addresses of Prefix {prefix['prefix']}"))
814
861
 
815
- # Get the base url of the NetBox instance
816
- nb_url = base_url(url=prefix["url"], with_path=False)
862
+ # Initialize, transform and filter the Nornir inventory are return the filtered Nornir object
863
+ # Define data to load from NetBox in addition to the base Nornir inventory plugin
864
+ add_netbox_data = {"load_virtual_chassis_data": True}
865
+ nr = init_nornir(config_file=nr_config, add_netbox_data=add_netbox_data, silent=True)
866
+ # Get the NetBox url from the inventory options
867
+ nb_url = nr.config.inventory.options["nb_url"]
817
868
 
818
869
  # Scan the prefix with nmap and add the list to the prefix dict or get the ip-addresses from IP-Fabric
819
870
  result, prefix, failed = get_nb_ips_and_external_datasource(nb_url=nb_url, prefix=prefix, ds=ds)
@@ -833,13 +884,13 @@ def update_netbox_prefix_ip_addresses(prefix: list, *args) -> tuple:
833
884
  result, sub_failed = update_reserved_ip_addresses(nb_url=nb_url, prefix=prefix, ds=ds)
834
885
  results, changed, failed = set_results_changed_failed(results, result, changed, sub_failed, failed)
835
886
 
836
- # Update 'inactive' ip-addresses -> set status to 'active' (double check with partial nmap scan)
837
- result, sub_failed = update_inactive_ip_addresses(nb_url=nb_url, prefix=prefix, ds=ds)
887
+ # Update 'inactive' ip-addresses -> set status to 'active' (with partial double-check)
888
+ result, sub_failed = update_inactive_ip_addresses(nr=nr, nb_url=nb_url, prefix=prefix, ds=ds)
838
889
  results, changed, failed = set_results_changed_failed(results, result, changed, sub_failed, failed)
839
890
 
840
- # Update 'active' ip-addresses -> set status to 'active' or 'inactive' (check with partial nmap scan)
891
+ # Update 'active' ip-addresses -> set status to 'active' or 'inactive' (with partial double-check)
841
892
  result, sub_failed = update_active_ip_addresses(
842
- nb_url=nb_url, prefix=prefix, overwrite_active=overwrite_active, ds=ds
893
+ nr=nr, nb_url=nb_url, prefix=prefix, overwrite_active=overwrite_active, ds=ds
843
894
  )
844
895
  results, changed, failed = set_results_changed_failed(results, result, changed, sub_failed, failed)
845
896
 
@@ -855,6 +906,139 @@ def update_netbox_prefix_ip_addresses(prefix: list, *args) -> tuple:
855
906
  return results, failed
856
907
 
857
908
 
909
+ def update_netbox_ip_address_switchport(prefix: list, *args) -> tuple:
910
+ """
911
+ This function can be used within a ThreadPoolExecutor. Update additional the IP addresses of a active
912
+ NetBox Prefix with the following information about where this IP-Address is activ in the network:
913
+ - Switch
914
+ - Switchport
915
+ - Switchport Config-Mode
916
+
917
+ Args:
918
+ prefix (dict): The prefix to update, containing information about the prefix.
919
+ nb_url (str): The URL of the NetBox instance.
920
+
921
+ Returns:
922
+ list: A tuple of results from the update tasks and a boolean indicating if the task failed.
923
+ """
924
+ # Create a list to collect the task results
925
+ results = []
926
+ # Boolian to check if any ip-addresses have been changed and the overall failed status
927
+ failed = False
928
+ # Set the IP-Fabric MAC addresses by the first arg
929
+ ipf_edge_mac_dict = args[0]
930
+ # Set the Nornir object by the second arg
931
+ nr = args[1]
932
+
933
+ # Print the task title
934
+ results.append(task_name(text=f"Update NetBox IP-Addresses of Prefix {prefix['prefix']}"))
935
+
936
+ # Get the base url of the NetBox instance
937
+ nb_url = base_url(url=prefix["url"], with_path=False)
938
+
939
+ # Add a list of dicts (id & address) of NetBox ip-addresses with the status 'active' for prefix
940
+ query = {"parent": prefix["prefix"], "status": "active"}
941
+ resp = get_nb_resources(url=f"{nb_url}/api/ipam/ip-addresses/", params=query)
942
+ prefix["active_ips"] = [{"id": i["id"], "address": i["address"]} for i in resp] if resp else []
943
+
944
+ # Print the task result
945
+ text = f"Get NetBox IP-Addresses of Prefix {prefix['prefix']}"
946
+ if len(prefix["active_ips"]) == 0:
947
+ results.append(
948
+ f"{task_result(text=text, changed=False, level_name='INFO')}\n"
949
+ + f"'{text}' -> NetBoxResponse <Success: True>\n"
950
+ + f"-> Prefix {prefix['prefix']} have no ip-addresses to update"
951
+ )
952
+ # Return the results and status
953
+ return results, failed
954
+ results.append(
955
+ f"{task_result(text=text, changed=False, level_name='INFO')}\n"
956
+ + f"'{text}' -> NetBoxResponse <Success: True>\n"
957
+ + f"-> NetBox active ip-address count: {len(prefix['active_ips'])}"
958
+ )
959
+
960
+ # Connect to IP-Fabric
961
+ ipf = IPFClient(
962
+ base_url=os.environ["IPF_URL"], auth=os.environ["IPF_TOKEN"], snapshot_id="$last", verify=False
963
+ )
964
+
965
+ # Get the prefix length from the prefix
966
+ prefixlen = ipaddress.ip_network(prefix["prefix"]).prefixlen
967
+
968
+ # Create some variables to store the data later
969
+ ip_detail_dict = {}
970
+ updated_fields = []
971
+ # Get all ip-addresses and mac-addresses of the prefix from the IP-Fabric technology arp table
972
+ filters = {"ip": ["cidr", prefix["prefix"]], "proxy": ["eq", False]}
973
+ columns = ["ip", "mac"]
974
+ ipf_arp_table = ipf.technology.addressing.arp_table.all(filters=filters, columns=columns)
975
+ # Create a mac-address dict only for the ip-addresses that are in the ipf_arp_table for the prefix
976
+ ipf_arp_table = [x for x in ipf_arp_table if x["mac"] in ipf_edge_mac_dict.keys()]
977
+ results.append(f"-> IP-Fabric edge ip-address count: {len(ipf_arp_table)}")
978
+
979
+ for x in ipf_arp_table:
980
+ # Set some variables for easier code reading
981
+ switch = ipf_edge_mac_dict[x["mac"]]["hostname"]
982
+ switch_id = nr.inventory.hosts[switch].get("id")
983
+ switchport = ipf_edge_mac_dict[x["mac"]]["intName"]
984
+ # Get the switchport information from IPF
985
+ ipf_switchport = ipf.technology.interfaces.switchport.all(
986
+ filters={"hostname": ["eq", switch], "intName": ["eq", switchport]}
987
+ )
988
+ if ipf_switchport[0]["mode"] == "static access":
989
+ config_mode = "Access-Port"
990
+ elif ipf_switchport[0]["mode"] == "trunk":
991
+ config_mode = "Trunk-Port"
992
+ else:
993
+ config_mode = ipf_switchport[0]["mode"]
994
+ nw_team_needed = True if config_mode == "Access-Port" else False
995
+ # Create the ip-detail dict with the infos to ceate the payload
996
+ ip_detail_dict[f"{x['ip']}/{prefixlen}"] = {
997
+ "ipam_1_switch": switch_id,
998
+ "ipam_2_switchport": switchport,
999
+ "ipam_3_switchport_config_mode": config_mode,
1000
+ "ipam_4_mac": x["mac"],
1001
+ "p0_4_nws_team_needed": nw_team_needed,
1002
+ }
1003
+ updated_fields.append(
1004
+ f" - {x['ip']}/{prefixlen} (Switch ID: {switch_id}, Switchport: {switchport}, "
1005
+ f"Config-Mode: {config_mode}, MAC: {x['mac']}, NWS-Team: {nw_team_needed})"
1006
+ )
1007
+
1008
+ # Create the payload to update the ip-addresses
1009
+ payload = []
1010
+ for ip in prefix["active_ips"]:
1011
+ # Check if the IP address is in the ip_detail_dict
1012
+ if ip["address"] in ip_detail_dict.keys():
1013
+ payload.append({"id": ip["id"], "custom_fields": {**ip_detail_dict[ip["address"]]}})
1014
+
1015
+ # PATCH request to update the ip-addresses
1016
+ resp = patch_nb_resources(url=f"{nb_url}/api/ipam/ip-addresses/", payload=payload)
1017
+
1018
+ # Verify the response code and print the result
1019
+ if resp.status_code in [200, 201, 204]:
1020
+ task_text = "Update Edge IP-Addresses Fields"
1021
+ text = "The following edge ip-addresses fields had been updated:"
1022
+ updated_fields = "\n".join(updated_fields)
1023
+ results.append(
1024
+ f"{task_result(text=task_text, changed=False, level_name='INFO')}\n"
1025
+ + f"'{task_text}' -> NetBoxResponse <Success: True>\n"
1026
+ + "-> The following edge ip-addresses fields had been updated:\n"
1027
+ + f"{updated_fields}"
1028
+ )
1029
+ else:
1030
+ results.append(
1031
+ [
1032
+ f"{task_result(text=task_text, changed=False, level_name='ERROR')}\n"
1033
+ + f"'{task_text}' -> NetBoxResponse <Success: False>\n"
1034
+ + f"-> Response Status Code: {resp.status_code}\n"
1035
+ + f"-> Response Json:\n{resp.json()}"
1036
+ ]
1037
+ )
1038
+
1039
+ return results, failed
1040
+
1041
+
858
1042
  def update_all_netbox_ip_addresses(prefix: dict) -> tuple:
859
1043
  """
860
1044
  Update all NetBox IP addresses VRF, Tenant, Tags and Location of a given prefix.
@@ -1045,28 +1229,29 @@ def main(nr_config: str, nmap_scan: bool = False, overwrite_active: list[str] =
1045
1229
  * Load active NetBox prefixes.
1046
1230
  * Load all non-container NetBox prefixes.
1047
1231
  * Load NetBox VLANs.
1048
- * Scan active NetBox prefixes with Nmap and update IP addresses status, DNS names, and ports.
1049
- * Update all IP addresses vrf, tenant, tags, and location.
1050
- * Update all VLANs status, tenant, tags, and location.
1232
+ * Update OOB/T1/T2 prefixes IP-addresses status, DNS names, and ports with a Nmap scan
1233
+ * Update all prefixes IP-addresses status and DNS names from IP-Fabric
1234
+ * Update all active IP-addresses switch/switchport info from IP-Fabric
1235
+ * Update all prefixes IP-addresses vrf, tenant, tags, and location
1236
+ * Update all VLANs status, tenant, tags, and location
1051
1237
 
1052
1238
  * Exits:
1053
1239
  * Exits with code 1 if the Nornir configuration file is empty or if any task fails.
1054
1240
  """
1055
1241
 
1056
- #### Load NetBox Inventory ##############################################################################
1242
+ #### Initialize Nornir ##################################################################################
1057
1243
 
1058
- task_text = "Load NetBox Inventory"
1059
- print_task_title(title=task_text)
1244
+ # Initialize, transform and filter the Nornir inventory are return the filtered Nornir object
1245
+ # Define data to load from NetBox in addition to the base Nornir inventory plugin
1246
+ add_netbox_data = {"load_virtual_chassis_data": True}
1247
+ nr = init_nornir(config_file=nr_config, add_netbox_data=add_netbox_data)
1248
+ # Get the NetBox url from the inventory options
1249
+ nb_url = nr.config.inventory.options["nb_url"]
1060
1250
 
1061
- # Load the Nornir yaml config file as dict and print a error message
1062
- nr_config_dict = load_yaml_file(
1063
- file=nr_config, text="Load Nornir Config File", silent=False, verbose=False
1064
- )
1065
- # Check the loaded config file and exit the script with exit code 1 if the dict is empty
1066
- if not nr_config_dict:
1067
- sys.exit(1)
1068
- # Get the NetBox URL (Authentication token will be loaded as nb_token env variable)
1069
- nb_url = nr_config_dict["inventory"]["options"]["nb_url"]
1251
+ #### Load NetBox IPAM Data ##############################################################################
1252
+
1253
+ task_text = "Load NetBox IPAM Data"
1254
+ print_task_title(title=task_text)
1070
1255
 
1071
1256
  # Load Active NetBox Prefixes from Tenant 'none', 'oob', 'tier-1' and 'tier-2' (except marked utilized)
1072
1257
  # These prefixes will be scanned with nmap and updated
@@ -1076,9 +1261,9 @@ def main(nr_config: str, nmap_scan: bool = False, overwrite_active: list[str] =
1076
1261
  query={"status": "active", "tenant": ["none", "oob", "tier-1", "tier-2"], "mark_utilized": "false"},
1077
1262
  )
1078
1263
 
1079
- # Load Active and Inventory NetBox Prefixes from all Tenants (except marked utilized)
1264
+ # Load Active and NetBox Prefixes from all Tenants (except marked utilized)
1080
1265
  # These prefixes will be updated with input from IP-Fabric
1081
- nb_active_inventory_all_prefixes = load_netbox_data(
1266
+ nb_active_all_prefixes = load_netbox_data(
1082
1267
  task_text="Load Active OOB/T1/T2/T3/T4 NetBox Prefixes",
1083
1268
  nb_api_url=f"{nb_url}/api/ipam/prefixes/",
1084
1269
  query={
@@ -1089,7 +1274,6 @@ def main(nr_config: str, nmap_scan: bool = False, overwrite_active: list[str] =
1089
1274
  )
1090
1275
 
1091
1276
  # Load NetBox Non-Container Prefixes
1092
- # Query "status__n" not working anymore since v4.1.3
1093
1277
  nb_subnet_prefixes = load_netbox_data(
1094
1278
  task_text="Load All Non-Container NetBox Prefixes",
1095
1279
  nb_api_url=f"{nb_url}/api/ipam/prefixes/",
@@ -1109,23 +1293,23 @@ def main(nr_config: str, nmap_scan: bool = False, overwrite_active: list[str] =
1109
1293
  # Set the task title
1110
1294
  title = "Nmap Scan and Update Active OOB/T1/T2 NetBox Prefixes IP-Addresses"
1111
1295
 
1112
- # Run the thread pool to update all NetBox IP-Addresses Status and DNS-Name
1296
+ # Run the thread pool to update all NetBox IP-Addresses Status, DNS-Name and Open Ports
1113
1297
  # 1. arg is the input type ('nmap' or 'ip-fabric')
1114
1298
  # 2. arg is the list of active IP addresses to overwrite
1115
1299
  thread_result = run_thread_pool(
1116
1300
  title=title,
1117
1301
  task=update_netbox_prefix_ip_addresses,
1118
1302
  thread_list=nb_active_oob_t1_t2_prefixes,
1119
- max_workers=50,
1120
- args=("nmap", overwrite_active),
1303
+ max_workers=50, # NetBox have no rate limit, but 50 threads are a good tested value with Nmap
1304
+ args=("nmap", overwrite_active, nr_config),
1121
1305
  )
1122
1306
  # Print the thread pool results and exit the script if any task has failed
1123
1307
  print_thread_pool_results(title=title, thread_result=thread_result, fail_hard=True)
1124
1308
 
1125
- #### IP-Fabric Update Active & Inventory NetBox Prefixes IP-Addresses ###################################
1309
+ #### IP-Fabric Update Active NetBox Prefixes IP-Addresses ###############################################
1126
1310
 
1127
1311
  # Set the task title
1128
- title = "IP-Fabric Update Active OOB/T1/T2/T3/T4 NetBox Prefixes IP-Addresses"
1312
+ title = "IP-Fabric Update All Active NetBox Prefixes IP-Addresses"
1129
1313
 
1130
1314
  # Run the thread pool to update all NetBox IP-Addresses Status and DNS-Name
1131
1315
  # 1. arg is the input type ('nmap' or 'ip-fabric')
@@ -1133,9 +1317,35 @@ def main(nr_config: str, nmap_scan: bool = False, overwrite_active: list[str] =
1133
1317
  thread_result = run_thread_pool(
1134
1318
  title=title,
1135
1319
  task=update_netbox_prefix_ip_addresses,
1136
- thread_list=nb_active_inventory_all_prefixes,
1137
- max_workers=5,
1138
- args=("ip-fabric", overwrite_active),
1320
+ thread_list=nb_active_all_prefixes,
1321
+ max_workers=5, # Each thread will create the own Nornir object == Max. 5 concurrent Nornir logins
1322
+ args=("ip-fabric", overwrite_active, nr_config),
1323
+ )
1324
+ # Print the thread pool results and exit the script if any task has failed
1325
+ print_thread_pool_results(title=title, thread_result=thread_result, fail_hard=True)
1326
+
1327
+ #### IP-Fabric Update Switch & Switchport Info for IP-Addresses #########################################
1328
+
1329
+ # Set the task title
1330
+ title = "IP-Fabric Update All Active NetBox IP-Addresses Switch/Switchport Info"
1331
+
1332
+ # Connect to IP-Fabric
1333
+ ipf = IPFClient(
1334
+ base_url=os.environ["IPF_URL"], auth=os.environ["IPF_TOKEN"], snapshot_id="$last", verify=False
1335
+ )
1336
+ # Get all mac-addresses on edge ports from the IP-Fabric technology mac table
1337
+ filters = {"edge": ["eq", True]}
1338
+ columns = ["hostname", "intName", "mac", "vendor"]
1339
+ ipf_mac_table = ipf.technology.addressing.mac_table.all(filters=filters, columns=columns)
1340
+ ipf_edge_mac_dict = {str(x["mac"]): x for x in ipf_mac_table}
1341
+
1342
+ # Run the thread pool to update all NetBox IP-Addresses Switch and Switchport Info
1343
+ thread_result = run_thread_pool(
1344
+ title=title,
1345
+ task=update_netbox_ip_address_switchport,
1346
+ thread_list=nb_active_all_prefixes,
1347
+ max_workers=3, # More then 3 threads will run into rate limit errors of IP-Fabric
1348
+ args=(ipf_edge_mac_dict, nr),
1139
1349
  )
1140
1350
  # Print the thread pool results and exit the script if any task has failed
1141
1351
  print_thread_pool_results(title=title, thread_result=thread_result, fail_hard=True)
@@ -111,6 +111,7 @@ class DSCNetBoxInventory:
111
111
  nb_url: Optional[str] = None,
112
112
  nb_token: Optional[str] = None,
113
113
  ssl_verify: Union[bool, str] = True,
114
+ nb_silent: bool = False,
114
115
  **kwargs: Any,
115
116
  ) -> None:
116
117
  """
@@ -118,12 +119,14 @@ class DSCNetBoxInventory:
118
119
  """
119
120
  nb_url = nb_url or os.environ.get("NB_URL")
120
121
  nb_token = nb_token or os.environ.get("NB_TOKEN")
122
+ nb_silent = nb_silent or os.environ.get("NB_SILENT")
121
123
 
122
124
  self.task_text = "NORNIR initialize inventory plugin DSCNetBoxInventory"
123
125
  self.nb_url = nb_url
124
126
  self.session = requests.Session()
125
127
  self.session.headers.update({"Authorization": f"Token {nb_token}"})
126
128
  self.session.verify = ssl_verify
129
+ self.nb_silent = nb_silent
127
130
 
128
131
  def _get_resources(self, url: str, params: Dict[str, Any] = {}) -> List[Dict[str, Any]]:
129
132
  """
@@ -154,7 +157,8 @@ class DSCNetBoxInventory:
154
157
  """
155
158
  yml = ruamel.yaml.YAML(typ="safe")
156
159
 
157
- print_task_name(text=self.task_text)
160
+ if not self.nb_silent:
161
+ print_task_name(text=self.task_text)
158
162
 
159
163
  # Load all NetBox devices with a primary IP address assigned
160
164
  devices: List[Dict[str, Any]] = []
@@ -237,9 +241,10 @@ class DSCNetBoxInventory:
237
241
  for h in hosts.values():
238
242
  h.groups = ParentGroups([groups[g] for g in h.groups])
239
243
 
240
- # Print the DSCNetBoxInventory result
241
- print(task_info(text=self.task_text, changed=False))
242
- print(f"'{self.task_text}' -> NornirResponse <Success: True>")
243
- print(f"-> Loaded NetBox base inventory data of {len(hosts)} devices")
244
+ if not self.nb_silent:
245
+ # Print the DSCNetBoxInventory result
246
+ print(task_info(text=self.task_text, changed=False))
247
+ print(f"'{self.task_text}' -> NornirResponse <Success: True>")
248
+ print(f"-> Loaded NetBox base inventory data of {len(hosts)} devices")
244
249
 
245
250
  return Inventory(hosts=hosts, groups=groups, defaults=defaults)
@@ -34,14 +34,15 @@ from nornir_collection.utils import (
34
34
  #### Inventory Transform Functions ##########################################################################
35
35
 
36
36
 
37
- def nr_transform_host_creds_from_env(nr: Nornir) -> None:
37
+ def nr_transform_host_creds_from_env(nr: Nornir, silent: bool = False) -> None:
38
38
  """
39
39
  This function loads the host login credentials from environment variables and stores them directly under
40
40
  the host inventory level. This function can be extended to to more host transformation.
41
41
  """
42
42
 
43
- task_text = "NORNIR transform host credentials env variable"
44
- print(task_name(text=task_text))
43
+ if not silent:
44
+ task_text = "NORNIR transform host credentials env variable"
45
+ print(task_name(text=task_text))
45
46
 
46
47
  # Recreate Nornir 2.x transform function behavior
47
48
  for host in nr.inventory.hosts.values():
@@ -98,9 +99,10 @@ def nr_transform_host_creds_from_env(nr: Nornir) -> None:
98
99
  msg="-> Analyse the error message and identify the root cause",
99
100
  )
100
101
 
101
- print(task_info(text=task_text, changed=False))
102
- print(f"'{task_text}' -> OS.EnvironResponse <Success: True>")
103
- print(f"-> Transformed {len(nr.inventory.hosts)} host credentials env variables")
102
+ if not silent:
103
+ print(task_info(text=task_text, changed=False))
104
+ print(f"'{task_text}' -> OS.EnvironResponse <Success: True>")
105
+ print(f"-> Transformed {len(nr.inventory.hosts)} host credentials env variables")
104
106
 
105
107
 
106
108
  def _tuple_with_env_list(iterable: dict) -> tuple:
@@ -124,6 +126,7 @@ def nr_transform_inv_from_env(
124
126
  iterable: dict,
125
127
  env_mandatory: dict = False,
126
128
  verbose: bool = False,
129
+ silent: bool = False,
127
130
  ) -> None:
128
131
  """
129
132
  This function transforms all environment variables in the iterable. It loops over a nested dictionary and
@@ -148,8 +151,9 @@ def nr_transform_inv_from_env(
148
151
 
149
152
  # If the mandatory_transform_env_keys list it not empty -> Transform all env variables in the list
150
153
  if mandatory_transform_env_keys:
151
- task_text = f"NORNIR transform mandatory {inv_type} env variable"
152
- print(task_name(text=task_text))
154
+ if not silent:
155
+ task_text = f"NORNIR transform mandatory {inv_type} env variable"
156
+ print(task_name(text=task_text))
153
157
 
154
158
  # Verify that all mandatory env key-value pairs exists in the Nornir inventory
155
159
  for key, value in env_mandatory.items():
@@ -165,12 +169,13 @@ def nr_transform_inv_from_env(
165
169
  # If the environ load fails a KeyError would be raised and catched by the exception
166
170
  transform_env(iterable=iterable, startswith=env_key)
167
171
 
168
- print(task_info(text=task_text, changed=False))
169
- print(f"'{task_text}' -> OS.EnvironResponse <Success: True>")
170
- print(f"-> Transformed {len(mandatory_transform_envs)} mandatory env variables")
171
- if verbose:
172
- for env in mandatory_transform_envs:
173
- print(f" - {env}")
172
+ if not silent:
173
+ print(task_info(text=task_text, changed=False))
174
+ print(f"'{task_text}' -> OS.EnvironResponse <Success: True>")
175
+ print(f"-> Transformed {len(mandatory_transform_envs)} mandatory env variables")
176
+ if verbose:
177
+ for env in mandatory_transform_envs:
178
+ print(f" - {env}")
174
179
 
175
180
  except KeyError as error:
176
181
  print(task_error(text=task_text, changed=False))
@@ -182,8 +187,9 @@ def nr_transform_inv_from_env(
182
187
 
183
188
  # If the transform_env_keys list it not empty -> Transform all env variables in the list
184
189
  if transform_envs:
185
- task_text = f"NORNIR transform non-mandatory {inv_type} env variable"
186
- print(task_name(text=task_text))
190
+ if not silent:
191
+ task_text = f"NORNIR transform non-mandatory {inv_type} env variable"
192
+ print(task_name(text=task_text))
187
193
 
188
194
  try:
189
195
  # Loop over the generator object items and add the matching elemens based on the key to a list
@@ -191,12 +197,13 @@ def nr_transform_inv_from_env(
191
197
  # If the environ load fails a KeyError would be raised and catched by the exception
192
198
  transform_env(iterable=iterable, startswith=env_key)
193
199
 
194
- print(task_info(text=task_text, changed=False))
195
- print(f"'{task_text}' -> OS.EnvironResponse <Success: True>")
196
- print(f"-> Transformed {len(transform_envs)} non-mandatory env variables")
197
- if verbose:
198
- for env in transform_envs:
199
- print(f" - {env}")
200
+ if not silent:
201
+ print(task_info(text=task_text, changed=False))
202
+ print(f"'{task_text}' -> OS.EnvironResponse <Success: True>")
203
+ print(f"-> Transformed {len(transform_envs)} non-mandatory env variables")
204
+ if verbose:
205
+ for env in transform_envs:
206
+ print(f" - {env}")
200
207
 
201
208
  except KeyError as error:
202
209
  print(task_error(text=task_text, changed=False))
@@ -213,6 +220,7 @@ def init_nornir(
213
220
  env_mandatory: dict = False,
214
221
  args: argparse.Namespace = False,
215
222
  add_netbox_data: dict[str:bool] = None,
223
+ silent: bool = False,
216
224
  ) -> Nornir:
217
225
  """
218
226
  The Nornir inventory will be initialized, the host username and password will be transformed and loaded
@@ -221,8 +229,8 @@ def init_nornir(
221
229
  are defined in the inventory. The function returns a filtered Nornir object or quits with an error message
222
230
  in case of issues during the function.
223
231
  """
224
-
225
- print_task_title("Initialize Nornir Inventory")
232
+ if not silent:
233
+ print_task_title("Initialize Nornir Inventory")
226
234
 
227
235
  # Set a timer to track how long the init_nornir functions takes to finish
228
236
  timer_start = time.time()
@@ -236,14 +244,17 @@ def init_nornir(
236
244
  load_dotenv(".env", override=True)
237
245
 
238
246
  # Initialize Nornir Object with a config file
247
+ # Set and unset the NB_SILENT environment variable to control the Nornir output of the inventory plugin
248
+ os.environ["NB_SILENT"] = str(silent)
239
249
  nr = InitNornir(config_file=config_file)
250
+ os.environ.pop("NB_SILENT", None)
240
251
 
241
252
  # Load additional data from NetBox into the Nornir inventory
242
253
  if add_netbox_data:
243
- load_additional_netbox_data(nr=nr, add_netbox_data=add_netbox_data)
254
+ load_additional_netbox_data(nr=nr, add_netbox_data=add_netbox_data, silent=silent)
244
255
 
245
256
  # Transform the host username and password from environment variables
246
- nr_transform_host_creds_from_env(nr=nr)
257
+ nr_transform_host_creds_from_env(nr=nr, silent=silent)
247
258
 
248
259
  # Transform the inventory and load all env variables staring with "env_" in the defaults yaml files
249
260
  nr_transform_inv_from_env(
@@ -251,6 +262,7 @@ def init_nornir(
251
262
  iterable=nr.inventory.defaults.data,
252
263
  verbose=args.verbose if args else False,
253
264
  env_mandatory=env_mandatory,
265
+ silent=silent,
254
266
  )
255
267
 
256
268
  # Transform the inventory and load all env variables staring with "env_" in the groups yaml files
@@ -259,17 +271,19 @@ def init_nornir(
259
271
  inv_type="groups",
260
272
  iterable=nr.inventory.groups[group].data,
261
273
  verbose=args.verbose if args else False,
274
+ silent=silent,
262
275
  )
263
276
 
264
277
  # Filter the Nornir inventory based on the provided arguments from argparse
265
278
  if args:
266
- nr = nr_filter_args(nr=nr, args=args)
267
-
268
- # Print the timer result
269
- task_text = "NORNIR initialize timer"
270
- print(f"{task_name(text=task_text)}")
271
- print(task_info(text=task_text, changed=False))
272
- print(f"'{task_text}' -> NornirResponse <Success: True>")
273
- print(f"-> Initialize Nornir finished in: {round(time.time() - timer_start, 1)}s")
279
+ nr = nr_filter_args(nr=nr, args=args, silent=silent)
280
+
281
+ if not silent:
282
+ # Print the timer result
283
+ task_text = "NORNIR initialize timer"
284
+ print(f"{task_name(text=task_text)}")
285
+ print(task_info(text=task_text, changed=False))
286
+ print(f"'{task_text}' -> NornirResponse <Success: True>")
287
+ print(f"-> Initialize Nornir finished in: {round(time.time() - timer_start, 1)}s")
274
288
 
275
289
  return nr
@@ -430,20 +430,20 @@ def nr_filter_by_hosts(nr: Nornir, hosts: Union[str, list], task_text: str = Non
430
430
  return nr_filtered
431
431
 
432
432
 
433
- def nr_filter_args(nr: Nornir, args: argparse.Namespace) -> Nornir:
433
+ def nr_filter_args(nr: Nornir, args: argparse.Namespace, silent: bool = False) -> Nornir:
434
434
  """
435
435
  This function filters the Nornir inventory with a tag or a host argument provided by argparse. Prior
436
436
  Argparse validation needs to ensure that only one argument is present and that the tag or host argument
437
437
  creates a correct inventory filtering will be verified. The new filtered Nornir object will be returned
438
438
  or the script terminates with an error message.
439
439
  """
440
-
441
- task_text = "NORNIR filter inventory"
442
- print_task_name(task_text)
440
+ if not silent:
441
+ task_text = "NORNIR filter inventory"
442
+ print_task_name(task_text)
443
443
 
444
444
  # If the --hosts argument is set, verify that the host exist
445
445
  if hasattr(args, "hosts"):
446
- nr = nr_filter_by_hosts(nr=nr, hosts=args.hosts, silent=False)
446
+ nr = nr_filter_by_hosts(nr=nr, hosts=args.hosts, silent=silent)
447
447
 
448
448
  return nr
449
449
 
@@ -451,7 +451,7 @@ def nr_filter_args(nr: Nornir, args: argparse.Namespace) -> Nornir:
451
451
  if hasattr(args, "role") or hasattr(args, "tags"):
452
452
  role = args.role if hasattr(args, "role") else None
453
453
  tags = args.tags.split(",") if hasattr(args, "tags") else None
454
- nr = nr_filter_by_role_and_tag(nr=nr, role=role, tags=tags, silent=False)
454
+ nr = nr_filter_by_role_and_tag(nr=nr, role=role, tags=tags, silent=silent)
455
455
 
456
456
  return nr
457
457
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nornir-collection
3
- Version: 0.0.27
3
+ Version: 0.0.29
4
4
  Summary: Nornir-Collection contains network automation functions and complete IaC workflows with Nornir and other python libraries. It contains Nornir tasks and general functions in Nornir style.
5
5
  Author: Willi Kubny
6
6
  Author-email: willi.kubny@gmail.ch
@@ -10,19 +10,19 @@ Classifier: Operating System :: OS Independent
10
10
  Requires-Python: >=3.9
11
11
  Description-Content-Type: text/markdown
12
12
  License-File: LICENSE
13
- Requires-Dist: nornir==3.3.0
13
+ Requires-Dist: nornir==3.5.0
14
14
  Requires-Dist: nornir-jinja2==0.2.0
15
- Requires-Dist: nornir-salt==0.22.2
15
+ Requires-Dist: nornir-salt==0.22.5
16
16
  Requires-Dist: nornir-netmiko==1.0.1
17
- Requires-Dist: nornir-scrapli==2023.7.30
18
- Requires-Dist: scrapli-netconf==2023.7.30
17
+ Requires-Dist: nornir-scrapli==2024.7.30
18
+ Requires-Dist: scrapli-netconf==2024.1.30
19
+ Requires-Dist: nornir-utils==0.2.0
20
+ Requires-Dist: nornir-pyfgt==1.0.3
19
21
  Requires-Dist: scrapli[ssh2]
20
22
  Requires-Dist: scrapli[genie]
21
- Requires-Dist: nornir-utils==0.2.0
22
23
  Requires-Dist: pynetbox==7.4.0
23
- Requires-Dist: ipfabric==7.0.2
24
+ Requires-Dist: ipfabric==7.2.2
24
25
  Requires-Dist: py-pure-client==1.51.0
25
- Requires-Dist: nornir-pyfgt==1.0.3
26
26
  Requires-Dist: python-nmap==0.7.1
27
27
  Requires-Dist: pybatfish==2023.12.16.1270
28
28
  Requires-Dist: requests==2.31.0
@@ -30,7 +30,7 @@ Requires-Dist: pre-commit==3.6.0
30
30
  Requires-Dist: pyyaml==6.0.1
31
31
  Requires-Dist: pyfiglet==1.0.2
32
32
  Requires-Dist: GitPython==3.1.40
33
- Requires-Dist: python-dotenv==1.0.0
33
+ Requires-Dist: python-dotenv==1.1.1
34
34
  Requires-Dist: beautifultable==1.1.0
35
35
  Requires-Dist: pandas==2.2.2
36
36
  Requires-Dist: openpyxl==3.1.2
@@ -1,16 +1,16 @@
1
- nornir==3.3.0
1
+ nornir==3.5.0
2
2
  nornir-jinja2==0.2.0
3
- nornir-salt==0.22.2
3
+ nornir-salt==0.22.5
4
4
  nornir-netmiko==1.0.1
5
- nornir-scrapli==2023.7.30
6
- scrapli-netconf==2023.7.30
5
+ nornir-scrapli==2024.7.30
6
+ scrapli-netconf==2024.1.30
7
+ nornir-utils==0.2.0
8
+ nornir-pyfgt==1.0.3
7
9
  scrapli[ssh2]
8
10
  scrapli[genie]
9
- nornir-utils==0.2.0
10
11
  pynetbox==7.4.0
11
- ipfabric==7.0.2
12
+ ipfabric==7.2.2
12
13
  py-pure-client==1.51.0
13
- nornir-pyfgt==1.0.3
14
14
  python-nmap==0.7.1
15
15
  pybatfish==2023.12.16.1270
16
16
  requests==2.31.0
@@ -18,7 +18,7 @@ pre-commit==3.6.0
18
18
  pyyaml==6.0.1
19
19
  pyfiglet==1.0.2
20
20
  GitPython==3.1.40
21
- python-dotenv==1.0.0
21
+ python-dotenv==1.1.1
22
22
  beautifultable==1.1.0
23
23
  pandas==2.2.2
24
24
  openpyxl==3.1.2
@@ -11,7 +11,7 @@ with open("requirements.txt", encoding="utf-8") as f:
11
11
 
12
12
  setuptools.setup(
13
13
  name="nornir-collection",
14
- version="0.0.27",
14
+ version="0.0.29",
15
15
  author="Willi Kubny",
16
16
  author_email="willi.kubny@gmail.ch",
17
17
  description="Nornir-Collection contains network automation functions and complete IaC workflows with \