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.
- nornir_collection/__init__.py +0 -0
- nornir_collection/batfish/__init__.py +0 -0
- nornir_collection/batfish/assert_config.py +358 -0
- nornir_collection/batfish/utils.py +129 -0
- nornir_collection/cisco/__init__.py +0 -0
- nornir_collection/cisco/configuration_management/__init__.py +0 -0
- nornir_collection/cisco/configuration_management/cli/__init__.py +0 -0
- nornir_collection/cisco/configuration_management/cli/config_tasks.py +569 -0
- nornir_collection/cisco/configuration_management/cli/config_workflow.py +107 -0
- nornir_collection/cisco/configuration_management/cli/show_tasks.py +677 -0
- nornir_collection/cisco/configuration_management/netconf/__init__.py +0 -0
- nornir_collection/cisco/configuration_management/netconf/config_tasks.py +564 -0
- nornir_collection/cisco/configuration_management/netconf/config_workflow.py +298 -0
- nornir_collection/cisco/configuration_management/netconf/nr_cfg_iosxe_netconf.py +186 -0
- nornir_collection/cisco/configuration_management/netconf/ops_tasks.py +307 -0
- nornir_collection/cisco/configuration_management/processor.py +151 -0
- nornir_collection/cisco/configuration_management/pyats.py +236 -0
- nornir_collection/cisco/configuration_management/restconf/__init__.py +0 -0
- nornir_collection/cisco/configuration_management/restconf/cisco_rpc.py +514 -0
- nornir_collection/cisco/configuration_management/restconf/config_workflow.py +95 -0
- nornir_collection/cisco/configuration_management/restconf/tasks.py +325 -0
- nornir_collection/cisco/configuration_management/utils.py +511 -0
- nornir_collection/cisco/software_upgrade/__init__.py +0 -0
- nornir_collection/cisco/software_upgrade/cisco_software_upgrade.py +283 -0
- nornir_collection/cisco/software_upgrade/utils.py +794 -0
- nornir_collection/cisco/support_api/__init__.py +0 -0
- nornir_collection/cisco/support_api/api_calls.py +1173 -0
- nornir_collection/cisco/support_api/cisco_maintenance_report.py +221 -0
- nornir_collection/cisco/support_api/cisco_support.py +727 -0
- nornir_collection/cisco/support_api/reports.py +747 -0
- nornir_collection/cisco/support_api/utils.py +316 -0
- nornir_collection/fortinet/__init__.py +0 -0
- nornir_collection/fortinet/utils.py +36 -0
- nornir_collection/git.py +224 -0
- nornir_collection/netbox/__init__.py +0 -0
- nornir_collection/netbox/custom_script.py +107 -0
- nornir_collection/netbox/inventory.py +360 -0
- nornir_collection/netbox/scan_prefixes_and_update_ip_addresses.py +989 -0
- nornir_collection/netbox/set_device_status.py +67 -0
- nornir_collection/netbox/sync_datasource.py +111 -0
- nornir_collection/netbox/update_cisco_inventory_data.py +158 -0
- nornir_collection/netbox/update_cisco_support_plugin_data.py +339 -0
- nornir_collection/netbox/update_fortinet_inventory_data.py +161 -0
- nornir_collection/netbox/update_purestorage_inventory_data.py +144 -0
- nornir_collection/netbox/utils.py +261 -0
- nornir_collection/netbox/verify_device_primary_ip.py +202 -0
- nornir_collection/nornir_plugins/__init__.py +0 -0
- nornir_collection/nornir_plugins/inventory/__init__.py +0 -0
- nornir_collection/nornir_plugins/inventory/netbox.py +250 -0
- nornir_collection/nornir_plugins/inventory/staggered_yaml.py +143 -0
- nornir_collection/nornir_plugins/inventory/utils.py +277 -0
- nornir_collection/purestorage/__init__.py +0 -0
- nornir_collection/purestorage/utils.py +53 -0
- nornir_collection/utils.py +741 -0
- nornir_collection-0.0.1.dist-info/LICENSE +21 -0
- nornir_collection-0.0.1.dist-info/METADATA +136 -0
- nornir_collection-0.0.1.dist-info/RECORD +59 -0
- nornir_collection-0.0.1.dist-info/WHEEL +5 -0
- 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")
|