nornir-collection 0.0.1__py3-none-any.whl

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 (59) hide show
  1. nornir_collection/__init__.py +0 -0
  2. nornir_collection/batfish/__init__.py +0 -0
  3. nornir_collection/batfish/assert_config.py +358 -0
  4. nornir_collection/batfish/utils.py +129 -0
  5. nornir_collection/cisco/__init__.py +0 -0
  6. nornir_collection/cisco/configuration_management/__init__.py +0 -0
  7. nornir_collection/cisco/configuration_management/cli/__init__.py +0 -0
  8. nornir_collection/cisco/configuration_management/cli/config_tasks.py +569 -0
  9. nornir_collection/cisco/configuration_management/cli/config_workflow.py +107 -0
  10. nornir_collection/cisco/configuration_management/cli/show_tasks.py +677 -0
  11. nornir_collection/cisco/configuration_management/netconf/__init__.py +0 -0
  12. nornir_collection/cisco/configuration_management/netconf/config_tasks.py +564 -0
  13. nornir_collection/cisco/configuration_management/netconf/config_workflow.py +298 -0
  14. nornir_collection/cisco/configuration_management/netconf/nr_cfg_iosxe_netconf.py +186 -0
  15. nornir_collection/cisco/configuration_management/netconf/ops_tasks.py +307 -0
  16. nornir_collection/cisco/configuration_management/processor.py +151 -0
  17. nornir_collection/cisco/configuration_management/pyats.py +236 -0
  18. nornir_collection/cisco/configuration_management/restconf/__init__.py +0 -0
  19. nornir_collection/cisco/configuration_management/restconf/cisco_rpc.py +514 -0
  20. nornir_collection/cisco/configuration_management/restconf/config_workflow.py +95 -0
  21. nornir_collection/cisco/configuration_management/restconf/tasks.py +325 -0
  22. nornir_collection/cisco/configuration_management/utils.py +511 -0
  23. nornir_collection/cisco/software_upgrade/__init__.py +0 -0
  24. nornir_collection/cisco/software_upgrade/cisco_software_upgrade.py +283 -0
  25. nornir_collection/cisco/software_upgrade/utils.py +794 -0
  26. nornir_collection/cisco/support_api/__init__.py +0 -0
  27. nornir_collection/cisco/support_api/api_calls.py +1173 -0
  28. nornir_collection/cisco/support_api/cisco_maintenance_report.py +221 -0
  29. nornir_collection/cisco/support_api/cisco_support.py +727 -0
  30. nornir_collection/cisco/support_api/reports.py +747 -0
  31. nornir_collection/cisco/support_api/utils.py +316 -0
  32. nornir_collection/fortinet/__init__.py +0 -0
  33. nornir_collection/fortinet/utils.py +36 -0
  34. nornir_collection/git.py +224 -0
  35. nornir_collection/netbox/__init__.py +0 -0
  36. nornir_collection/netbox/custom_script.py +107 -0
  37. nornir_collection/netbox/inventory.py +360 -0
  38. nornir_collection/netbox/scan_prefixes_and_update_ip_addresses.py +989 -0
  39. nornir_collection/netbox/set_device_status.py +67 -0
  40. nornir_collection/netbox/sync_datasource.py +111 -0
  41. nornir_collection/netbox/update_cisco_inventory_data.py +158 -0
  42. nornir_collection/netbox/update_cisco_support_plugin_data.py +339 -0
  43. nornir_collection/netbox/update_fortinet_inventory_data.py +161 -0
  44. nornir_collection/netbox/update_purestorage_inventory_data.py +144 -0
  45. nornir_collection/netbox/utils.py +261 -0
  46. nornir_collection/netbox/verify_device_primary_ip.py +202 -0
  47. nornir_collection/nornir_plugins/__init__.py +0 -0
  48. nornir_collection/nornir_plugins/inventory/__init__.py +0 -0
  49. nornir_collection/nornir_plugins/inventory/netbox.py +250 -0
  50. nornir_collection/nornir_plugins/inventory/staggered_yaml.py +143 -0
  51. nornir_collection/nornir_plugins/inventory/utils.py +277 -0
  52. nornir_collection/purestorage/__init__.py +0 -0
  53. nornir_collection/purestorage/utils.py +53 -0
  54. nornir_collection/utils.py +741 -0
  55. nornir_collection-0.0.1.dist-info/LICENSE +21 -0
  56. nornir_collection-0.0.1.dist-info/METADATA +136 -0
  57. nornir_collection-0.0.1.dist-info/RECORD +59 -0
  58. nornir_collection-0.0.1.dist-info/WHEEL +5 -0
  59. nornir_collection-0.0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,989 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ This module interacts with the NetBox API to scan active network prefixes with Nmap and update
4
+ IP address, and vlan information.
5
+ The Main function is intended to import and execute by other scripts.
6
+ """
7
+
8
+
9
+ import sys
10
+ import ipaddress
11
+ import urllib.parse
12
+ from typing import Callable, Literal, Union
13
+ from concurrent.futures import ThreadPoolExecutor
14
+ import requests
15
+ import nmap
16
+ from nornir_collection.netbox.utils import (
17
+ get_nb_resources,
18
+ post_nb_resources,
19
+ patch_nb_resources,
20
+ delete_nb_resources,
21
+ )
22
+ from nornir_collection.utils import (
23
+ print_task_title,
24
+ task_name,
25
+ exit_error,
26
+ load_yaml_file,
27
+ task_result,
28
+ )
29
+
30
+ # pylint: disable=invalid-name
31
+
32
+
33
+ __author__ = "Willi Kubny"
34
+ __maintainer__ = "Willi Kubny"
35
+ __version__ = "1.0"
36
+ __license__ = "MIT"
37
+ __email__ = "willi.kubny@dreyfusbank.ch"
38
+ __status__ = "Production"
39
+
40
+
41
+ def load_netbox_data(task_text: str, nb_api_url: str, query: dict) -> list[dict]:
42
+ """
43
+ Load NetBox data using the provided task text, NetBox API URL, and query parameters.
44
+ If no data is returned, the script will exit with an error message.
45
+
46
+ Args:
47
+ task_text (str): The task text to be printed.
48
+ nb_api_url (str): The URL of the NetBox API.
49
+ query (dict): The query parameters to be passed to the NetBox API.
50
+
51
+ Returns:
52
+ list[dict]: A list of dictionaries containing the NetBox API data.
53
+ """
54
+ # Print the task name
55
+ print(task_name(text=task_text))
56
+
57
+ # Get all NetBox API data
58
+ nb_data = get_nb_resources(url=nb_api_url, params=query)
59
+
60
+ # Exit the script if the nb_data list is empty
61
+ if not nb_data:
62
+ exit_error(task_text=f"{task_text} Failed", msg=["-> No Data returned from NetBox API"])
63
+
64
+ # Print the task result
65
+ print(task_result(text=task_text, changed=False, level_name="INFO"))
66
+ print(f"'{task_text}' -> NetBoxResponse <Success: True>")
67
+ print(f"-> NetBox API response count: {len(nb_data)}")
68
+
69
+ return nb_data
70
+
71
+
72
+ def base_url(url: str, with_path: bool = False) -> str:
73
+ """
74
+ Returns the base URL of a given URL.
75
+
76
+ Args:
77
+ url (str): The input URL.
78
+ with_path (bool, optional): Whether to include the path in the base URL. Defaults to False.
79
+
80
+ Returns:
81
+ str: The base URL of the input URL.
82
+ """
83
+ parsed = urllib.parse.urlparse(url)
84
+ path = "/".join(parsed.path.split("/")[:-1]) if with_path else ""
85
+ parsed = parsed._replace(path=path)
86
+ parsed = parsed._replace(params="")
87
+ parsed = parsed._replace(query="")
88
+ parsed = parsed._replace(fragment="")
89
+
90
+ return parsed.geturl()
91
+
92
+
93
+ def make_markdown_table(array: list[list[str]]) -> str:
94
+ """
95
+ Create a markdown table from a 2D array.
96
+
97
+ Args:
98
+ array (list[list[str]]): The 2D array containing the table data.
99
+
100
+ Returns:
101
+ str: The generated markdown table.
102
+ """
103
+ nl = "\n"
104
+ markdown = f"| {' | '.join(array[0])} |"
105
+ markdown += nl
106
+ markdown += f"| {' | '.join(['---']*len(array[0]))} |"
107
+ markdown += nl
108
+ for entry in array[1:]:
109
+ markdown += f"| {' | '.join(entry)} |{nl}"
110
+
111
+ return markdown
112
+
113
+
114
+ def create_nb_response_result(
115
+ resp: requests.Response,
116
+ nb_type: Literal["ip", "vlan"],
117
+ data: Union[dict, list],
118
+ task_text: str,
119
+ text: str,
120
+ ) -> tuple:
121
+ """
122
+ Verify the NetBox response and return the result.
123
+ For active scanned IP addresses of a prefixthe following fields are updated:
124
+ * Status, DNS Name, Ports
125
+ For all ip addresses of a prefix the following fields are updated:
126
+ * VRF, Tenant, Tags, Location
127
+ For a VLAN associated to a prefix the following fields are updated:
128
+ * Status, Tenant, Tags, Location
129
+ For a VLAN not associated to a prefix the following fields are updated:
130
+ * Tags
131
+
132
+ Args:
133
+ resp (requests.Response): The response object from the NetBox API.
134
+ data Union[dict, list]: A list of ip addresses or a dictionary containing the prefix information.
135
+ task_text (str): The task text.
136
+ text (str): The additional task text for the first line of the task result.
137
+
138
+ Returns:
139
+ tuple: A tuple containing the results and a boolean indicating success or failure.
140
+ """
141
+ # Create list to collect the results
142
+ result = []
143
+
144
+ # Verify the response code and print the result
145
+ if resp.status_code in [200, 201, 204]:
146
+ # Create a list of fields that have been updated
147
+ updated_fields = []
148
+
149
+ # If data is a list of ip addresses from a active scanned prefix
150
+ if nb_type in "ip" and isinstance(data, list):
151
+ # Create a list of ip addresses that have been updated
152
+ for ip in data:
153
+ address = ip["address"]
154
+ dns_name = ip["dns_name"] if "dns_name" in ip and ip["dns_name"] else "None"
155
+ ports = ", ".join([str(p) for p in ip["ports"]]) if "ports" in ip and ip["ports"] else "None"
156
+ updated_fields.append(f"- {address} (DNS: {dns_name}, Ports: {ports})")
157
+ # If data is a dictionary containing the prefix or information vlan associated with a prefix
158
+ elif nb_type in ("ip", "vlan") and isinstance(data, dict):
159
+ # If the response json contains the key 'vid' the response is from a vlan and has no VRF
160
+ if "vid" in resp.json():
161
+ updated_fields.append(f"- Status: {data['status']['label']}")
162
+ # It's the response from a ip-address and ha a VRF
163
+ else:
164
+ updated_fields.append(f"- VRF: {data['vrf'] if data['vrf'] else 'Global'}")
165
+ updated_fields.extend(
166
+ [
167
+ f"- Tenant: {data['tenant']['name'] if data['tenant'] else 'None'}",
168
+ f"- Tags: {(', '.join([i['name'] for i in data['tags']])) or 'None'}",
169
+ f"- Location: {', '.join(list(data['custom_fields']['ipam_location'])).upper() or 'None'}", # pylint: disable=line-too-long
170
+ ]
171
+ )
172
+ # If the type is 'vlan' and data is empty
173
+ elif nb_type == "vlan" and not data:
174
+ updated_fields.append("- Tags: L2 Only")
175
+ else:
176
+ result = [
177
+ f"{task_result(text=task_text, changed=True, level_name='ERROR')}\n"
178
+ + f"'{task_text}' -> NetBoxResponse <Success: False>\n-> {text}\n"
179
+ + "Check the source code as the data parameter is neither a list nor a dictionary!"
180
+ ]
181
+ return result, True
182
+
183
+ # Add the result to the list
184
+ updated_fields = "\n".join(updated_fields)
185
+ result = [
186
+ f"{task_result(text=task_text, changed=True, level_name='INFO')}\n"
187
+ + f"'{task_text}' -> NetBoxResponse <Success: True>\n-> {text}\n"
188
+ + f"{updated_fields}"
189
+ ]
190
+ return result, False
191
+
192
+ result = [
193
+ f"{task_result(text=task_text, changed=False, level_name='ERROR')}\n"
194
+ + f"'{task_text}' -> NetBoxResponse <Success: False>\n"
195
+ + f"-> Response Status Code: {resp.status_code}\n"
196
+ + f"-> Response Json:\n{resp.json()}"
197
+ ]
198
+ return result, True
199
+
200
+
201
+ def create_nb_ip_payload(parent_prefix: dict, data: list, desired_status: str = None) -> dict:
202
+ """
203
+ Create a NetBox REST API payload.
204
+ To add or delete IP addresses of an active scanned prefix or update the following fields:
205
+ * Status, DNS Name, Open Ports
206
+ To update all IP addresses or the vlan associated with a prefix with the following information:
207
+ * VRF, Tenant, Tags, Location
208
+
209
+ Args:
210
+ parent_prefix (dict): The parent prefix information.
211
+ data (list[dict]): The list of IP addresses information.
212
+ desired_status (str): The desired status for the IP addresses.
213
+
214
+ Returns:
215
+ dict: The NetBox payload for the REST API.
216
+ """
217
+ # Create the payload list of dicts
218
+ payload = []
219
+
220
+ for ip in data:
221
+ # Create an empty dict for the payload item
222
+ item = {}
223
+ # Add the 'id' if it exists (not needed for post requests)
224
+ if "id" in ip:
225
+ item["id"] = ip["id"]
226
+ # If desired_status is not None, the payload is for active scanned ip addresses
227
+ if desired_status:
228
+ # Add the 'address' and 'status' to the payload
229
+ item["address"] = ip["address"]
230
+ item["status"] = desired_status
231
+ # If the ip address exists in the nmap_ips list
232
+ nmap_ip = [x for x in parent_prefix["nmap_ips"] if x["address"] == ip["address"]]
233
+ # Add the 'dns_name'
234
+ item["dns_name"] = nmap_ip[0]["dns_name"] if nmap_ip else ""
235
+ # Format the 'ports' as a Markdown table and add the 'ports'
236
+ if nmap_ip and nmap_ip[0]["ports"]:
237
+ md = [["Port", "State", "Service"]]
238
+ md.extend([[f"{k}/tcp", v["state"], v["name"]] for k, v in nmap_ip[0]["ports"].items()])
239
+ item["custom_fields"] = {"ipaddress_ports": make_markdown_table(md)}
240
+ else:
241
+ item["custom_fields"] = {"ipaddress_ports": None}
242
+ # If the desired_status is None, the payload is for all ip addresses of a prefix
243
+ else:
244
+ # Add the 'vrf' to the payload
245
+ item["vrf"] = parent_prefix["vrf"]["id"] if parent_prefix["vrf"] is not None else None
246
+ # Add the 'tenant' to the payload
247
+ item["tenant"] = parent_prefix["tenant"]["id"] if parent_prefix["tenant"] is not None else None
248
+ # Add the 'tags' of the parent prefix to the payload
249
+ item["tags"] = [tag["id"] for tag in parent_prefix["tags"]]
250
+ # Add the custom field 'ipam_location' of the parent prefix to the payload
251
+ item["custom_fields"] = {"ipam_location": parent_prefix["custom_fields"]["ipam_location"]}
252
+
253
+ # Add the item to the payload list
254
+ payload.append(item)
255
+
256
+ return payload
257
+
258
+
259
+ def get_netbox_ip_addresses_and_scan_prefix(nb_url: str, prefix: dict) -> tuple:
260
+ """
261
+ Get NetBox IP addresses and scan a prefix with nmap.
262
+ This function can be used within a ThreadPoolExecutor.
263
+
264
+ Args:
265
+ nb_url (str): The URL of the NetBox instance.
266
+ prefix (dict): The prefix to scan and retrieve IP addresses for.
267
+
268
+ Returns:
269
+ tuple: A tuple containing the result list and the updated prefix dictionary.
270
+ """
271
+ # Create list to collect the results
272
+ result = []
273
+
274
+ #### Scan the prefix with nmap ###########################################################################
275
+
276
+ # Get the prefix length from the prefix
277
+ prefixlen = ipaddress.ip_network(prefix["prefix"]).prefixlen
278
+ # Get the network and broadcast address of the prefix to exclude them from the nmap scan
279
+ network_addr = ipaddress.ip_network(prefix["prefix"]).network_address
280
+ broadcast_addr = ipaddress.ip_network(prefix["prefix"]).broadcast_address
281
+ # Set the nmap scan arguments
282
+ arguments = f"-PE -PP -PA21 -PS80,443,3389 -PU161,40125 --source-port 53 --exclude {network_addr},{broadcast_addr}" # pylint: disable=line-too-long
283
+
284
+ # Nmap ARP scan for the prefix and add a list of active ip-addresses and other details to prefix
285
+ nm = nmap.PortScanner()
286
+ nm.scan(hosts=prefix["prefix"], arguments=arguments, sudo=True)
287
+ if nm.all_hosts():
288
+ prefix["nmap_ips"] = [
289
+ {
290
+ "address": f"{nm[host]['addresses']['ipv4']}/{prefixlen}",
291
+ "dns_name": nm[host]["hostnames"][0]["name"],
292
+ "ports": nm[host]["tcp"] if "tcp" in nm[host] else {},
293
+ }
294
+ for host in nm.all_hosts()
295
+ ]
296
+ else:
297
+ prefix["nmap_ips"] = []
298
+
299
+ # Print the task result
300
+ text = f"Nmap Scan Prefix {prefix['prefix']} for active IP-Addresses"
301
+ result.append(
302
+ f"{task_result(text=text, changed=False, level_name='INFO')}\n"
303
+ + f"'{text}' -> NetBoxResponse <Success: True>\n"
304
+ + f"-> Nmap prefix scan ip-address count: {len(prefix['nmap_ips'])}",
305
+ )
306
+
307
+ #### Get NetBox ip-addresses ############################################################################
308
+
309
+ # Add a list of dicts (id & address) of all NetBox ip-addresses for prefix
310
+ query = {"parent": prefix["prefix"]}
311
+ resp = get_nb_resources(url=f"{nb_url}/api/ipam/ip-addresses/", params=query)
312
+ prefix["all_ips"] = [{"id": i["id"], "address": i["address"]} for i in resp] if resp else []
313
+
314
+ # Add a list of dicts (id & address) of NetBox ip-addresses with the status 'auto_discovered' for prefix
315
+ query = {"parent": prefix["prefix"], "status": "auto_discovered"}
316
+ resp = get_nb_resources(url=f"{nb_url}/api/ipam/ip-addresses/", params=query)
317
+ prefix["discovered_ips"] = [{"id": i["id"], "address": i["address"]} for i in resp] if resp else []
318
+
319
+ # Add a list of dicts (id & address) of NetBox ip-addresses with the status 'reserved' for prefix
320
+ query = {"parent": prefix["prefix"], "status": "reserved"}
321
+ resp = get_nb_resources(url=f"{nb_url}/api/ipam/ip-addresses/", params=query)
322
+ prefix["reserved_ips"] = [{"id": i["id"], "address": i["address"]} for i in resp] if resp else []
323
+
324
+ # Add a list of dicts (id & address) of NetBox ip-addresses with the status 'active' for prefix
325
+ query = {"parent": prefix["prefix"], "status": "active"}
326
+ resp = get_nb_resources(url=f"{nb_url}/api/ipam/ip-addresses/", params=query)
327
+ prefix["active_ips"] = [{"id": i["id"], "address": i["address"]} for i in resp] if resp else []
328
+
329
+ # Add a list of dicts (id & address) of NetBox ip-addresses with the status 'inactive' for prefix
330
+ query = {"parent": prefix["prefix"], "status": "inactive"}
331
+ resp = get_nb_resources(url=f"{nb_url}/api/ipam/ip-addresses/", params=query)
332
+ prefix["inactive_ips"] = [{"id": i["id"], "address": i["address"]} for i in resp] if resp else []
333
+
334
+ # Add a list of dicts (id & address) of NetBox ip-addresses with the status 'deprecated' for prefix
335
+ query = {"parent": prefix["prefix"], "status": "deprecated"}
336
+ resp = get_nb_resources(url=f"{nb_url}/api/ipam/ip-addresses/", params=query)
337
+ prefix["deprecated_ips"] = [{"id": i["id"], "address": i["address"]} for i in resp] if resp else []
338
+
339
+ # Print the task result
340
+ text = f"Get NetBox IP-Addresses of Prefix {prefix['prefix']}"
341
+ result.append(
342
+ f"{task_result(text=text, changed=False, level_name='INFO')}\n"
343
+ + f"'{text}' -> NetBoxResponse <Success: True>\n"
344
+ + f"-> All ip-address count: {len(prefix['all_ips'])}\n"
345
+ + f"-> Auto-Discovered ip-address count: {len(prefix['discovered_ips'])}\n"
346
+ + f"-> Reserved ip-address count: {len(prefix['reserved_ips'])}\n"
347
+ + f"-> Active ip-address count: {len(prefix['active_ips'])}\n"
348
+ + f"-> Inactive ip-address count: {len(prefix['inactive_ips'])}\n"
349
+ + f"-> Deprecated ip-address count: {len(prefix['deprecated_ips'])}",
350
+ )
351
+
352
+ return result, prefix
353
+
354
+
355
+ def create_ip_list(loop_list: list[dict], check_list: list[dict], is_in_both: bool) -> list[dict]:
356
+ """
357
+ Create a list of IP addresses with additional information based on the verification list and Nmap list.
358
+
359
+ Args:
360
+ loop_list (list[dict]): The list of IP addresses dicts to verify.
361
+ check_list (list[dict]): The list of IP addresses dicts from Nmap scan.
362
+ is_in_both (bool): Flag indicating whether the IP addresses should be in the Nmap list.
363
+
364
+ Returns:
365
+ list[dict]: The updated list of IP addresses with additional information.
366
+
367
+ """
368
+ # Create a list to collect the ip-addresses
369
+ ip_list = []
370
+ # Loop through the loop_list and check if the ip-address is in the check_list
371
+ if is_in_both:
372
+ for ip in loop_list:
373
+ for x in check_list:
374
+ if ip["address"] == x["address"]:
375
+ ip_list.append({**ip, "dns_name": x["dns_name"], "ports": x["ports"]})
376
+ # Loop through the loop_list and check if the ip-address is not in the check_list
377
+ else:
378
+ for ip in loop_list:
379
+ if ip["address"] not in [x["address"] for x in check_list]:
380
+ ip_list.append(ip)
381
+
382
+ return ip_list
383
+
384
+
385
+ def update_discovered_ip_addresses(nb_url: str, prefix: dict) -> tuple:
386
+ """
387
+ Posts new auto-discovered IP addresses to NetBox.
388
+
389
+ Args:
390
+ nb_url (str): The URL of the NetBox instance.
391
+ prefix (dict): The prefix dictionary containing the IP addresses.
392
+
393
+ Returns:
394
+ tuple: A tuple containing the result and the status (True if IPs were added, False otherwise).
395
+ """
396
+ # Set the default result and failed boolian
397
+ result = []
398
+ failed = False
399
+ task_text = "Add Auto-Discovered IP-Addresses"
400
+
401
+ # Add nmap scan ip-addresses that are not in the existing ip-addresses dict
402
+ add_ips = create_ip_list(loop_list=prefix["nmap_ips"], check_list=prefix["all_ips"], is_in_both=False)
403
+
404
+ # If ip-addresses have been found
405
+ if add_ips:
406
+ # Create the payload to create the ip-addresses
407
+ payload = create_nb_ip_payload(parent_prefix=prefix, data=add_ips, desired_status="auto_discovered")
408
+ # POST request to update the ip-addresses
409
+ resp = post_nb_resources(url=f"{nb_url}/api/ipam/ip-addresses/", payload=payload)
410
+
411
+ # Verify the response code and print the result
412
+ text = "The following 'Auto-Discovered' ip-addresses had been added:"
413
+ # The function returns the result list and True if the response is successful else False
414
+ sub_result, sub_failed = create_nb_response_result(
415
+ resp=resp, nb_type="ip", data=add_ips, task_text=task_text, text=text
416
+ )
417
+ if "Response Json:" in sub_result[0]:
418
+ sub_result[0] += f"\n** DEBUG **\nPayload:\n{payload}\nResponse:\n{resp.json()}\n** DEBUG **\n"
419
+ result.extend(sub_result)
420
+ failed = True if sub_failed else failed
421
+
422
+ # Update the ip-addresses with the status 'auto_discovered' that are part of the nmap scan list
423
+ update_ips = create_ip_list(
424
+ loop_list=prefix["discovered_ips"], check_list=prefix["nmap_ips"], is_in_both=True
425
+ )
426
+
427
+ # If ip-addresses have been found
428
+ if update_ips:
429
+ # Create the payload to create the ip-addresses
430
+ payload = create_nb_ip_payload(
431
+ parent_prefix=prefix, data=update_ips, desired_status="auto_discovered"
432
+ )
433
+ # PATCH request to update the ip-addresses
434
+ resp = patch_nb_resources(url=f"{nb_url}/api/ipam/ip-addresses/", payload=payload)
435
+
436
+ # Verify the response code and print the result
437
+ text = "The following 'Auto-Discovered' ip-addresses had been updated:"
438
+ # The function returns the result list and True if the response is successful else False
439
+ sub_result, sub_failed = create_nb_response_result(
440
+ resp=resp, nb_type="ip", data=update_ips, task_text=task_text, text=text
441
+ )
442
+ result.extend(sub_result)
443
+ failed = True if sub_failed else failed
444
+
445
+ return result, failed
446
+
447
+
448
+ def delete_inactive_auto_discovered_ip_addresses(nb_url: str, prefix: dict) -> tuple:
449
+ """
450
+ Deletes inactive auto-discovered IP addresses from NetBox.
451
+
452
+ Args:
453
+ nb_url (str): The URL of the NetBox instance.
454
+ prefix (dict): The prefix dictionary containing the IP addresses.
455
+
456
+ Returns:
457
+ tuple: A tuple containing the result and the status (True if IPs were deleted, False otherwise).
458
+ """
459
+ # Set the default result and failed status
460
+ result = []
461
+ failed = False
462
+ task_text = "Delete Auto-Discovered IP-Addresses"
463
+
464
+ # Delete the ip-addresses with the status 'auto_discovered' that are not in the nmap scan list
465
+ delete_ips = create_ip_list(
466
+ loop_list=prefix["discovered_ips"], check_list=prefix["nmap_ips"], is_in_both=False
467
+ )
468
+
469
+ # If ip-addresses have been found
470
+ if delete_ips:
471
+ # PATCH request to delete the ip-addresses
472
+ # The delete_ips list contains already 'id' and 'address' and can be used as payload
473
+ resp = delete_nb_resources(url=f"{nb_url}/api/ipam/ip-addresses/", payload=delete_ips)
474
+
475
+ # Verify the response code and print the result
476
+ text = "The following 'Auto-Discovered' ip-addresses had been deleted:"
477
+ # The function returns the result list and True if the response is successful else False
478
+ result, failed = create_nb_response_result(
479
+ resp=resp, nb_type="ip", data=delete_ips, task_text=task_text, text=text
480
+ )
481
+
482
+ return result, failed
483
+
484
+
485
+ def update_reserved_ip_addresses(nb_url: str, prefix: dict) -> tuple:
486
+ """
487
+ Updates the status of reserved IP addresses in NetBox if they are reachable by nmap.
488
+
489
+ Args:
490
+ nb_url (str): The URL of the NetBox instance.
491
+ prefix (dict): The prefix dictionary containing the IP addresses.
492
+
493
+ Returns:
494
+ tuple: A tuple containing the result and the status (True if IPs were updated, False otherwise).
495
+ """
496
+ # Set the default result and failed status
497
+ result = []
498
+ failed = False
499
+ task_text = "Update Reserved IP-Addresses Status"
500
+
501
+ # Update the ip-addresses with the status 'reserved' that are part of the nmap scan list
502
+ update_ips = create_ip_list(
503
+ loop_list=prefix["reserved_ips"], check_list=prefix["nmap_ips"], is_in_both=True
504
+ )
505
+
506
+ # If ip-addresses have been found
507
+ if update_ips:
508
+ # Create the payload to update the ip-addresses
509
+ payload = create_nb_ip_payload(parent_prefix=prefix, data=update_ips, desired_status="active")
510
+ # PATCH request to update the ip-addresses
511
+ resp = patch_nb_resources(url=f"{nb_url}/api/ipam/ip-addresses/", payload=payload)
512
+
513
+ # Verify the response code and print the result
514
+ text = "The following 'Reserved' ip-addresses had been set to status 'Active':"
515
+ # The function returns the result list and True if the response is successful else False
516
+ result, failed = create_nb_response_result(
517
+ resp=resp, nb_type="ip", data=update_ips, task_text=task_text, text=text
518
+ )
519
+
520
+ return result, failed
521
+
522
+
523
+ def update_inactive_ip_addresses(nb_url: str, prefix: dict) -> tuple:
524
+ """
525
+ Updates the status of inactive IP addresses in NetBox if they are reachable by nmap.
526
+
527
+ Args:
528
+ nb_url (str): The URL of the NetBox instance.
529
+ prefix (dict): The prefix dictionary containing the IP addresses.
530
+
531
+ Returns:
532
+ tuple: A tuple containing the result and the status (True if IPs were updated, False otherwise).
533
+ """
534
+ # Set the default result and failed status
535
+ result = []
536
+ failed = False
537
+ task_text = "Update Inactive IP-Addresses Status"
538
+
539
+ # Update the ip-addresses with the status 'inactive' that are part of the nmap scan list
540
+ inactive_ips = create_ip_list(
541
+ loop_list=prefix["inactive_ips"], check_list=prefix["nmap_ips"], is_in_both=True
542
+ )
543
+
544
+ # If ip-addresses have been found
545
+ if inactive_ips:
546
+ # Create the payload to update the ip-addresses
547
+ payload = create_nb_ip_payload(parent_prefix=prefix, data=inactive_ips, desired_status="active")
548
+ # PATCH request to update the ip-addresses
549
+ resp = patch_nb_resources(url=f"{nb_url}/api/ipam/ip-addresses/", payload=payload)
550
+
551
+ # Verify the response code and print the result
552
+ text = "The following 'Inactive' ip-addresses had been set to status 'Active':"
553
+ # The function returns the result list and True if the response is successful else False
554
+ result, failed = create_nb_response_result(
555
+ resp=resp, nb_type="ip", data=inactive_ips, task_text=task_text, text=text
556
+ )
557
+
558
+ return result, failed
559
+
560
+
561
+ def update_active_ip_addresses(nb_url: str, prefix: dict, overwrite_active: list[str]) -> tuple:
562
+ """
563
+ Updates the status of active IP addresses in NetBox if they are not reachable by nmap.
564
+
565
+ Args:
566
+ nb_url (str): The URL of the NetBox instance.
567
+ prefix (dict): The prefix dictionary containing the IP addresses.
568
+
569
+ Returns:
570
+ tuple: A tuple containing the result and the status (True if IPs were updated, False otherwise).
571
+ """
572
+ # Set the default result and failed status
573
+ result = []
574
+ failed = False
575
+ task_text = "Update Active IP-Addresses Status"
576
+
577
+ # Update the ip-addresses with the status 'active' that are part of the nmap scan list
578
+ active_ips = create_ip_list(
579
+ loop_list=prefix["active_ips"], check_list=prefix["nmap_ips"], is_in_both=True
580
+ )
581
+
582
+ # If ip-addresses have been found
583
+ if active_ips:
584
+ # Create the payload to update the ip-addresses
585
+ payload = create_nb_ip_payload(parent_prefix=prefix, data=active_ips, desired_status="active")
586
+ # PATCH request to update the ip-addresses
587
+ resp = patch_nb_resources(url=f"{nb_url}/api/ipam/ip-addresses/", payload=payload)
588
+
589
+ # Verify the response code and print the result
590
+ text = "The following 'Active' ip-addresses had been updated:"
591
+ # The function returns the result list and True if the response is successful else False
592
+ sub_result, sub_failed = create_nb_response_result(
593
+ resp=resp, nb_type="ip", data=active_ips, task_text=task_text, text=text
594
+ )
595
+ result.extend(sub_result)
596
+ failed = True if sub_failed else failed
597
+
598
+ # Update the ip-addresses with the status 'active' that are not part of the nmap scan list
599
+ inactive_ips = create_ip_list(
600
+ loop_list=prefix["active_ips"], check_list=prefix["nmap_ips"], is_in_both=False
601
+ )
602
+ # Create a new list to exclude the overwrite_active ip-addresses
603
+ inactive_ips = [ip for ip in inactive_ips if ip["address"] not in overwrite_active]
604
+
605
+ # If ip-addresses have been found
606
+ if inactive_ips:
607
+ # Create the payload to update the ip-addresses
608
+ payload = create_nb_ip_payload(parent_prefix=prefix, data=inactive_ips, desired_status="inactive")
609
+ # PATCH request to update the ip-addresses
610
+ resp = patch_nb_resources(url=f"{nb_url}/api/ipam/ip-addresses/", payload=payload)
611
+
612
+ # Verify the response code and print the result
613
+ text = "The following 'Active' ip-addresses had been set to status 'Inactive':"
614
+ # The function returns the result list and True if the response is successful else False
615
+ sub_result, sub_failed = create_nb_response_result(
616
+ resp=resp, nb_type="ip", data=inactive_ips, task_text=task_text, text=text
617
+ )
618
+ result.extend(sub_result)
619
+ failed = True if sub_failed else failed
620
+
621
+ return result, failed
622
+
623
+
624
+ def set_results_changed_failed(results, result, changed, sub_failed, failed) -> tuple:
625
+ """
626
+ Sets the values of 'changed' and 'failed' based on the given 'result' and 'sub_failed' values.
627
+
628
+ Parameters:
629
+ results (list): The list of results to be extended with the 'result'.
630
+ result (list): The result to be added to the 'results' list.
631
+ changed (bool): The current value of 'changed'.
632
+ sub_failed (bool): The value indicating if a sub-task has failed.
633
+ failed (bool): The current value of 'failed'.
634
+
635
+ Returns:
636
+ tuple: A tuple containing the updated 'results', 'changed', and 'failed' values.
637
+ """
638
+ results.extend(result)
639
+ changed = True if result else changed
640
+ failed = True if sub_failed else failed
641
+
642
+ return results, changed, failed
643
+
644
+
645
+ def update_active_netbox_prefix_ip_addresses(prefix: list, *overwrite_active) -> tuple:
646
+ """
647
+ This function can be used within a ThreadPoolExecutor. Update the IP addresses of a active NetBox prefix
648
+ with the following information:
649
+ - Status
650
+ - DNS-Name
651
+ - Open Ports
652
+
653
+ Args:
654
+ prefix (dict): The prefix to update, containing information about the prefix.
655
+ nb_url (str): The URL of the NetBox instance.
656
+
657
+ Returns:
658
+ list: A tuple of results from the update tasks and a boolean indicating if the task failed.
659
+ """
660
+ # Create a list to collect the task results
661
+ results = []
662
+ # overwrite_active is a tuple as its passed by the thread pool executor via the *args
663
+ # Therefor the tuple will be changes into a list
664
+ overwrite_active = list(overwrite_active)
665
+ # Boolian to check if any ip-addresses have been changed and the overall failed status
666
+ changed = False
667
+ failed = False
668
+
669
+ # Print the task title
670
+ results.append(task_name(text=f"Update NetBox IP-Addresses of Prefix {prefix['prefix']}"))
671
+
672
+ # Get the base url of the NetBox instance
673
+ nb_url = base_url(url=prefix["url"], with_path=False)
674
+
675
+ # Get all NetBox ip-addresses, scan the prefix with nmap add lists to the prefix dict
676
+ result, prefix = get_netbox_ip_addresses_and_scan_prefix(nb_url=nb_url, prefix=prefix)
677
+ results.extend(result)
678
+
679
+ # Add new 'auto-discovered' ip-addresses
680
+ result, sub_failed = update_discovered_ip_addresses(nb_url=nb_url, prefix=prefix)
681
+ results, changed, failed = set_results_changed_failed(results, result, changed, sub_failed, failed)
682
+
683
+ # Delete inactive 'auto-discovered' ip-addresses
684
+ result, sub_failed = delete_inactive_auto_discovered_ip_addresses(nb_url=nb_url, prefix=prefix)
685
+ results, changed, failed = set_results_changed_failed(results, result, changed, sub_failed, failed)
686
+
687
+ # Update 'reserved' ip-addresses -> set status to 'active'
688
+ result, sub_failed = update_reserved_ip_addresses(nb_url=nb_url, prefix=prefix)
689
+ results, changed, failed = set_results_changed_failed(results, result, changed, sub_failed, failed)
690
+
691
+ # Update 'inactive' ip-addresses -> set status to 'active'
692
+ result, sub_failed = update_inactive_ip_addresses(nb_url=nb_url, prefix=prefix)
693
+ results, changed, failed = set_results_changed_failed(results, result, changed, sub_failed, failed)
694
+
695
+ # Update 'active' ip-addresses -> set status to 'active' or 'inactive'
696
+ result, sub_failed = update_active_ip_addresses(
697
+ nb_url=nb_url, prefix=prefix, overwrite_active=overwrite_active
698
+ )
699
+ results, changed, failed = set_results_changed_failed(results, result, changed, sub_failed, failed)
700
+
701
+ # Print a message if no ip-addresses have been changed
702
+ if not changed:
703
+ text = f"No IP-Address to update for Prefix {prefix['prefix']}"
704
+ results.append(
705
+ f"{task_result(text=text, changed=False, level_name='INFO')}\n"
706
+ + f"'{text}' -> NetBoxResponse <Success: True>\n"
707
+ + f"-> {prefix['prefix']} have no ip-addresses to add, update or delete"
708
+ )
709
+
710
+ return results, failed
711
+
712
+
713
+ def update_all_netbox_ip_addresses(prefix: dict) -> tuple:
714
+ """
715
+ Update all NetBox IP addresses VRF, Tenant, Tags and Location of a given prefix.
716
+
717
+ Args:
718
+ prefix (dict): The NetBox prefix for which the IP addresses need to be updated.
719
+ nb_url (str): The URL of the NetBox instance.
720
+
721
+ Returns:
722
+ list: A tuple of results from the update tasks and a boolean indicating if the task failed.
723
+ """
724
+ # Create a list to collect the task results
725
+ results = []
726
+ # Boolian to check if any ip-addresses have been changed and the overall failed status
727
+ changed = False
728
+ failed = False
729
+
730
+ # Print the task title
731
+ results.append(task_name(text=f"Update NetBox IP-Addresses of Prefix {prefix['prefix']}"))
732
+
733
+ # Get the base url of the NetBox instance
734
+ nb_url = base_url(url=prefix["url"], with_path=False)
735
+ # Get all ip-addresses of the parent prefix
736
+ query = {"parent": prefix["prefix"]}
737
+ ip_addresses = get_nb_resources(url=f"{nb_url}/api/ipam/ip-addresses/", params=query)
738
+ # Create the payload to update all the ip-addresses
739
+ payload = create_nb_ip_payload(parent_prefix=prefix, data=ip_addresses)
740
+ # PATCH request to update the ip-addresses tags
741
+ resp = patch_nb_resources(url=f"{nb_url}/api/ipam/ip-addresses/", payload=payload)
742
+
743
+ # If the response json is not empty
744
+ if resp.json():
745
+ # Verify the response code and print the result
746
+ task_text = "Update IP-Addresses Fields"
747
+ text = "The following ip-addresses fields had been updated:"
748
+ # The function returns the result list and True if the response is successful else False
749
+ result, sub_failed = create_nb_response_result(
750
+ resp=resp, nb_type="ip", data=prefix, task_text=task_text, text=text
751
+ )
752
+ results, changed, failed = set_results_changed_failed(results, result, changed, sub_failed, failed)
753
+
754
+ # Print a message if no ip-addresses have been updated
755
+ if not changed:
756
+ text = f"No IP-Address to update for Prefix {prefix['prefix']}"
757
+ results.append(
758
+ f"{task_result(text=text, changed=False, level_name='INFO')}\n"
759
+ + f"'{text}' -> NetBoxResponse <Success: True>\n"
760
+ + f"-> Prefix {prefix['prefix']} have no ip-addresses to update"
761
+ )
762
+
763
+ return results, failed
764
+
765
+
766
+ def update_all_netbox_vlans(vlan: dict) -> tuple:
767
+ """
768
+ Update NetBox VLANs.
769
+ To update a VLAN associated to a prefix with the following information:
770
+ * Status, Tenant, Tags, Location
771
+ To update a VLAN without associated to a prefix with the following information:
772
+ * Tags
773
+
774
+ Args:
775
+ prefix (dict): The prefix to get the vlan from to update.
776
+
777
+ Returns:
778
+ tuple: A tuple containing the results of the update and the overall failed status.
779
+
780
+ """
781
+ # pylint: disable=unsubscriptable-object
782
+
783
+ # Create a list to collect the task results
784
+ results = []
785
+ # Boolian to check if any ip-addresses have been changed and the overall failed status
786
+ changed = False
787
+ failed = False
788
+
789
+ # Print the task title
790
+ results.append(task_name(text=f"Update NetBox VLAN {vlan['name']} (VLAN {vlan['vid']})"))
791
+
792
+ # Get the base url of the NetBox instance
793
+ nb_url = base_url(url=vlan["url"], with_path=False)
794
+ # Get the prefix assigned to the vlan
795
+ query = {"vlan_id": vlan["id"]}
796
+ prefix = get_nb_resources(url=f"{nb_url}/api/ipam/prefixes/", params=query)
797
+ prefix = prefix[0] if prefix else None
798
+ # Create the payload to update the vlan
799
+ if prefix:
800
+ payload = {
801
+ "id": vlan["id"],
802
+ "status": prefix["status"]["value"],
803
+ "tenant": prefix["tenant"]["id"] if prefix["tenant"] is not None else None,
804
+ "tags": [tag["id"] for tag in prefix["tags"]],
805
+ "custom_fields": {"ipam_location": prefix["custom_fields"]["ipam_location"]},
806
+ }
807
+ else:
808
+ payload = {"id": vlan["id"], "tags": [{"slug": "l2-only"}]}
809
+ # PATCH request to update the vlan
810
+ resp = patch_nb_resources(url=f"{nb_url}/api/ipam/vlans/{vlan['id']}/", payload=payload)
811
+
812
+ # Verify the response code and print the result
813
+ task_text = "Update VLAN Fields"
814
+ text = "The following VLAN fields had been updated:"
815
+ # The function returns the result list and True if the response is successful else False
816
+ result, sub_failed = create_nb_response_result(
817
+ resp=resp, nb_type="vlan", data=prefix, task_text=task_text, text=text
818
+ )
819
+ results, changed, failed = set_results_changed_failed(results, result, changed, sub_failed, failed)
820
+
821
+ return results, failed
822
+
823
+
824
+ def run_thread_pool(title: str, task: Callable, thread_list: list[dict], args: tuple = ()) -> list:
825
+ """
826
+ Runs a thread pool with a given task for each item in the thread list.
827
+
828
+ Args:
829
+ title (str): The title of the task.
830
+ task (Callable): The task to be executed for each item in the thread list.
831
+ thread_list (list[dict]): The list of items to be processed by the task.
832
+ args (tuple, optional): Additional arguments to be passed to the task. Defaults to ().
833
+
834
+ Returns:
835
+ list: A list of threads representing the submitted tasks.
836
+ """
837
+ # Print the task title
838
+ print_task_title(title=title)
839
+
840
+ # If the thread list is empty return the result list
841
+ if not thread_list:
842
+ print(
843
+ f"{task_name(text=title)}\n"
844
+ f"{task_result(text=title, changed=False, level_name='INFO')}\n"
845
+ + f"'{title}' -> NetBoxResponse <Success: True>\n"
846
+ + "-> No items to process in the thread list"
847
+ )
848
+ return []
849
+
850
+ # Create a ThreadPoolExecutor
851
+ with ThreadPoolExecutor(max_workers=100) as executor:
852
+ # Submit a new task for each prefix to update and collect the tasks results
853
+ threads = [executor.submit(task, item, *args) for item in thread_list]
854
+
855
+ return threads
856
+
857
+
858
+ def print_thread_pool_results(title: str, thread_result, fail_hard: bool = False) -> bool:
859
+ """
860
+ Print the results of a ThreadPoolExecutor.
861
+
862
+ Args:
863
+ threads (list): The list of threads to print the results for.
864
+
865
+ Returns:
866
+ bool: The overall failed status. True if any of the threads failed, False otherwise.
867
+ """
868
+ # Overal failed status
869
+ failed_task = False
870
+
871
+ # Interate over threads and print the result for each thread
872
+ for thread in thread_result:
873
+ # Get the results and failed status from the thread
874
+ results, failed = thread.result()
875
+ # Print the results
876
+ for result in results:
877
+ print(result)
878
+ # If failed is True set the overall failed status to True
879
+ failed_task = True if failed else failed_task
880
+
881
+ # Print a message if any task has failed and exit the script
882
+ if fail_hard and failed_task:
883
+ print("\n")
884
+ exit_error(task_text=title, msg=["-> Verify the result for failes tasks"])
885
+
886
+ return failed_task
887
+
888
+
889
+ def main(nr_config: str, overwrite_active: list[str] = None) -> None:
890
+ """
891
+ Main function is intended to import and execute by other scripts.
892
+ It loads NetBox inventory, scan active prefixes, and update IP addresses and VLANs.
893
+
894
+ * Args:
895
+ * nr_config (str): Path to the Nornir configuration YAML file.
896
+ * overwrite_active (list[str], optional): List of active IP addresses to overwrite. Defaults to None.
897
+
898
+ * Steps:
899
+ * Load the Nornir configuration file.
900
+ * Load active NetBox prefixes.
901
+ * Load all non-container NetBox prefixes.
902
+ * Load NetBox VLANs.
903
+ * Scan active NetBox prefixes with Nmap and update IP addresses status, DNS names, and ports.
904
+ * Update all IP addresses vrf, tenant, tags, and location.
905
+ * Update all VLANs status, tenant, tags, and location.
906
+
907
+ * Exits:
908
+ * Exits with code 1 if the Nornir configuration file is empty or if any task fails.
909
+ """
910
+
911
+ #### Load NetBox Inventory ##############################################################################
912
+
913
+ task_text = "Load NetBox Inventory"
914
+ print_task_title(title=task_text)
915
+
916
+ # Load the Nornir yaml config file as dict and print a error message
917
+ nr_config_dict = load_yaml_file(
918
+ file=nr_config, text="Load Nornir Config File", silent=False, verbose=False
919
+ )
920
+ # Check the loaded config file and exit the script with exit code 1 if the dict is empty
921
+ if not nr_config_dict:
922
+ sys.exit(1)
923
+ # Get the NetBox URL (Authentication token will be loaded as nb_token env variable)
924
+ nb_url = nr_config_dict["inventory"]["options"]["nb_url"]
925
+
926
+ # Load Active NetBox Prefixes
927
+ nb_active_prefixes = load_netbox_data(
928
+ task_text="Load Active NetBox Prefixes",
929
+ nb_api_url=f"{nb_url}/api/ipam/prefixes/",
930
+ query={"status": "active", "mark_utilized": "false"},
931
+ )
932
+
933
+ # Load NetBox Non-Container Prefixes
934
+ # Query "status__n" not working anymore since v4.1.3
935
+ nb_subnet_prefixes = load_netbox_data(
936
+ task_text="Load All Non-Container NetBox Prefixes",
937
+ nb_api_url=f"{nb_url}/api/ipam/prefixes/",
938
+ query={"status": ["active", "reserved", "deprecated", "inventory"]},
939
+ )
940
+
941
+ # Load NetBox VLANs
942
+ nb_vlans = load_netbox_data(
943
+ task_text="Load NetBox VLANs",
944
+ nb_api_url=f"{nb_url}/api/ipam/vlans/",
945
+ query={},
946
+ )
947
+
948
+ #### Scan Active NetBox Prefixes and Update IP-Addresses Status, DNS-Name and Ports ######################
949
+
950
+ # Set the task title
951
+ title = "Scan Active NetBox Prefixes and Update IP-Addresses Status, DNS-Name and Ports"
952
+
953
+ # Run the thread pool to update all NetBox IP-Addresses Status and DNS-Name
954
+ thread_result = run_thread_pool(
955
+ title=title,
956
+ task=update_active_netbox_prefix_ip_addresses,
957
+ thread_list=nb_active_prefixes,
958
+ args=overwrite_active,
959
+ )
960
+ # Print the thread pool results and exit the script if any task has failed
961
+ print_thread_pool_results(title=title, thread_result=thread_result, fail_hard="hard")
962
+
963
+ #### Update all IP-Addresses Tags and other Fields ######################################################
964
+
965
+ # Set the task title
966
+ title = "Update All NetBox Prefixes IP-Addresses Tags and other Fields"
967
+
968
+ # Run the thread pool to update all NetBox IP-Addresses Tags and other Fields
969
+ thread_result = run_thread_pool(
970
+ title=title,
971
+ task=update_all_netbox_ip_addresses,
972
+ thread_list=nb_subnet_prefixes,
973
+ )
974
+ # Print the thread pool results and exit the script if any task has failed
975
+ print_thread_pool_results(title=title, thread_result=thread_result, fail_hard="hard")
976
+
977
+ #### Update all VLANs Tags and other Fields #############################################################
978
+
979
+ # Set the task title
980
+ title = "Update All NetBox VLANs Tags and other Fields"
981
+
982
+ # Run the thread pool to update all NetBox VLANs Tags and other Fields
983
+ thread_result = run_thread_pool(
984
+ title=title,
985
+ task=update_all_netbox_vlans,
986
+ thread_list=nb_vlans,
987
+ )
988
+ # Print the thread pool results and exit the script if any task has failed
989
+ print_thread_pool_results(title=title, thread_result=thread_result, fail_hard="hard")