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.
- {nornir_collection-0.0.27 → nornir_collection-0.0.29}/PKG-INFO +9 -9
- {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/netbox/inventory.py +9 -8
- {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/netbox/update_prefixes_ip_addresses.py +268 -58
- {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/nornir_plugins/inventory/netbox.py +10 -5
- {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/nornir_plugins/inventory/utils.py +48 -34
- {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/utils.py +6 -6
- {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection.egg-info/PKG-INFO +9 -9
- {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection.egg-info/requires.txt +8 -8
- {nornir_collection-0.0.27 → nornir_collection-0.0.29}/setup.py +1 -1
- {nornir_collection-0.0.27 → nornir_collection-0.0.29}/LICENSE +0 -0
- {nornir_collection-0.0.27 → nornir_collection-0.0.29}/README.md +0 -0
- {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/__init__.py +0 -0
- {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/batfish/__init__.py +0 -0
- {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/batfish/assert_config.py +0 -0
- {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/batfish/utils.py +0 -0
- {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/cisco/__init__.py +0 -0
- {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/cisco/configuration_management/__init__.py +0 -0
- {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/cisco/configuration_management/cli/__init__.py +0 -0
- {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/cisco/configuration_management/cli/config_tasks.py +0 -0
- {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/cisco/configuration_management/cli/config_workflow.py +0 -0
- {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/cisco/configuration_management/cli/show_tasks.py +0 -0
- {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/cisco/configuration_management/netconf/__init__.py +0 -0
- {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/cisco/configuration_management/netconf/config_tasks.py +0 -0
- {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/cisco/configuration_management/netconf/config_workflow.py +0 -0
- {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/cisco/configuration_management/netconf/nr_cfg_iosxe_netconf.py +0 -0
- {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/cisco/configuration_management/netconf/ops_tasks.py +0 -0
- {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/cisco/configuration_management/processor.py +0 -0
- {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/cisco/configuration_management/pyats.py +0 -0
- {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/cisco/configuration_management/restconf/__init__.py +0 -0
- {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/cisco/configuration_management/restconf/cisco_rpc.py +0 -0
- {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/cisco/configuration_management/restconf/config_workflow.py +0 -0
- {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/cisco/configuration_management/restconf/tasks.py +0 -0
- {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/cisco/configuration_management/utils.py +0 -0
- {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/cisco/software_upgrade/__init__.py +0 -0
- {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/cisco/software_upgrade/cisco_software_upgrade.py +0 -0
- {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/cisco/software_upgrade/utils.py +0 -0
- {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/cisco/support_api/__init__.py +0 -0
- {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/cisco/support_api/api_calls.py +0 -0
- {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/cisco/support_api/cisco_maintenance_report.py +0 -0
- {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/cisco/support_api/cisco_support.py +0 -0
- {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/cisco/support_api/reports.py +0 -0
- {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/cisco/support_api/utils.py +0 -0
- {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/fortinet/__init__.py +0 -0
- {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/fortinet/utils.py +0 -0
- {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/git.py +0 -0
- {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/netbox/__init__.py +0 -0
- {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/netbox/custom_script.py +0 -0
- {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/netbox/set_device_status.py +0 -0
- {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/netbox/sync_datasource.py +0 -0
- {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/netbox/update_cisco_inventory_data.py +0 -0
- {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/netbox/update_cisco_support_plugin_data.py +0 -0
- {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/netbox/update_fortinet_inventory_data.py +0 -0
- {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/netbox/update_purestorage_inventory_data.py +0 -0
- {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/netbox/utils.py +0 -0
- {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/netbox/verify_device_primary_ip.py +0 -0
- {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/nornir_plugins/__init__.py +0 -0
- {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/nornir_plugins/inventory/__init__.py +0 -0
- {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/nornir_plugins/inventory/staggered_yaml.py +0 -0
- {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/purestorage/__init__.py +0 -0
- {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/purestorage/utils.py +0 -0
- {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection.egg-info/SOURCES.txt +0 -0
- {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection.egg-info/dependency_links.txt +0 -0
- {nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection.egg-info/top_level.txt +0 -0
- {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.
|
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.
|
13
|
+
Requires-Dist: nornir==3.5.0
|
14
14
|
Requires-Dist: nornir-jinja2==0.2.0
|
15
|
-
Requires-Dist: nornir-salt==0.22.
|
15
|
+
Requires-Dist: nornir-salt==0.22.5
|
16
16
|
Requires-Dist: nornir-netmiko==1.0.1
|
17
|
-
Requires-Dist: nornir-scrapli==
|
18
|
-
Requires-Dist: scrapli-netconf==
|
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.
|
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.
|
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
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
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"] = {"
|
276
|
+
item["custom_fields"] = {"ipam_ports": make_markdown_table(md)}
|
250
277
|
else:
|
251
|
-
item["custom_fields"] = {"
|
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
|
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
|
353
|
+
# Return the active ip-addresses list
|
324
354
|
return verified_ips
|
325
355
|
|
326
356
|
|
327
|
-
def
|
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"] =
|
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
|
-
|
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
|
-
|
672
|
-
|
673
|
-
|
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
|
-
|
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
|
-
|
738
|
-
|
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
|
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
|
-
#
|
816
|
-
|
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' (
|
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' (
|
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
|
-
*
|
1049
|
-
* Update all IP
|
1050
|
-
* Update all
|
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
|
-
####
|
1242
|
+
#### Initialize Nornir ##################################################################################
|
1057
1243
|
|
1058
|
-
|
1059
|
-
|
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
|
-
|
1062
|
-
|
1063
|
-
|
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
|
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
|
-
|
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
|
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
|
1309
|
+
#### IP-Fabric Update Active NetBox Prefixes IP-Addresses ###############################################
|
1126
1310
|
|
1127
1311
|
# Set the task title
|
1128
|
-
title = "IP-Fabric Update Active
|
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=
|
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
|
-
|
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
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
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
|
-
|
44
|
-
|
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
|
-
|
102
|
-
|
103
|
-
|
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
|
-
|
152
|
-
|
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
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
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
|
-
|
186
|
-
|
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
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
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
|
-
|
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
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
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
|
-
|
442
|
-
|
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=
|
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=
|
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.
|
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.
|
13
|
+
Requires-Dist: nornir==3.5.0
|
14
14
|
Requires-Dist: nornir-jinja2==0.2.0
|
15
|
-
Requires-Dist: nornir-salt==0.22.
|
15
|
+
Requires-Dist: nornir-salt==0.22.5
|
16
16
|
Requires-Dist: nornir-netmiko==1.0.1
|
17
|
-
Requires-Dist: nornir-scrapli==
|
18
|
-
Requires-Dist: scrapli-netconf==
|
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.
|
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.
|
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
|
{nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection.egg-info/requires.txt
RENAMED
@@ -1,16 +1,16 @@
|
|
1
|
-
nornir==3.
|
1
|
+
nornir==3.5.0
|
2
2
|
nornir-jinja2==0.2.0
|
3
|
-
nornir-salt==0.22.
|
3
|
+
nornir-salt==0.22.5
|
4
4
|
nornir-netmiko==1.0.1
|
5
|
-
nornir-scrapli==
|
6
|
-
scrapli-netconf==
|
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.
|
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.
|
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.
|
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 \
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/batfish/assert_config.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/cisco/support_api/reports.py
RENAMED
File without changes
|
{nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/cisco/support_api/utils.py
RENAMED
File without changes
|
{nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/fortinet/__init__.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/netbox/custom_script.py
RENAMED
File without changes
|
{nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/netbox/set_device_status.py
RENAMED
File without changes
|
{nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/netbox/sync_datasource.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/nornir_plugins/__init__.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
{nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/purestorage/__init__.py
RENAMED
File without changes
|
{nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection/purestorage/utils.py
RENAMED
File without changes
|
{nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection.egg-info/SOURCES.txt
RENAMED
File without changes
|
File without changes
|
{nornir_collection-0.0.27 → nornir_collection-0.0.29}/nornir_collection.egg-info/top_level.txt
RENAMED
File without changes
|
File without changes
|