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,677 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
This module contains screen-scraping functions and tasks related to Nornir.
|
4
|
+
|
5
|
+
The functions are ordered as followed:
|
6
|
+
- Helper Functions
|
7
|
+
- Single Nornir Screen-Scraping Tasks
|
8
|
+
- Nornir Screen-Scraping Tasks in regular Functions
|
9
|
+
"""
|
10
|
+
|
11
|
+
import sys
|
12
|
+
import json
|
13
|
+
import difflib
|
14
|
+
from colorama import Fore, Style, init
|
15
|
+
from nornir_utils.plugins.tasks.files import write_file
|
16
|
+
from nornir.core import Nornir
|
17
|
+
from nornir.core.task import Task, Result
|
18
|
+
from nornir_collection.utils import (
|
19
|
+
print_task_title,
|
20
|
+
print_result,
|
21
|
+
task_name,
|
22
|
+
task_host,
|
23
|
+
task_info,
|
24
|
+
task_error,
|
25
|
+
)
|
26
|
+
|
27
|
+
init(autoreset=True, strip=False)
|
28
|
+
|
29
|
+
|
30
|
+
#### Helper Functions ########################################################################################
|
31
|
+
|
32
|
+
|
33
|
+
def _cisco_config_normalization(config: str) -> str:
|
34
|
+
"""
|
35
|
+
Normalize a Cisco device configuration by removing specific lines and empty lines.
|
36
|
+
"""
|
37
|
+
# Create a list with strings to skip if they exists in a line of the config
|
38
|
+
skip_if_line_contains = ["Building configuration", "Current configuration", "Last configuration change"]
|
39
|
+
|
40
|
+
# List comprehension to skip empty lines
|
41
|
+
config_list = [i for i in config.splitlines() if i]
|
42
|
+
|
43
|
+
# Create a string from the list of strings with some data normalization
|
44
|
+
config_normalized = ""
|
45
|
+
for line in config_list:
|
46
|
+
for item in skip_if_line_contains:
|
47
|
+
if item in line:
|
48
|
+
break
|
49
|
+
else:
|
50
|
+
config_normalized += f"{line}\n"
|
51
|
+
|
52
|
+
return config_normalized
|
53
|
+
|
54
|
+
|
55
|
+
def _get_software_version(task: Task) -> tuple:
|
56
|
+
"""
|
57
|
+
TBD
|
58
|
+
"""
|
59
|
+
# Manually create Scrapli connection
|
60
|
+
conn = task.host.get_connection("scrapli", task.nornir.config)
|
61
|
+
# Run Scrapli send_command to get show version
|
62
|
+
output = conn.send_command(command="show version")
|
63
|
+
|
64
|
+
# Parse the show version scrapli response with the genie parter
|
65
|
+
output_parsed = output.genie_parse_output()
|
66
|
+
verbose_result = json.dumps(output_parsed, indent=4)
|
67
|
+
|
68
|
+
# Find the correct software version of the device
|
69
|
+
version = None
|
70
|
+
if "nxos" in conn.genie_platform:
|
71
|
+
version = output_parsed["platform"]["software"]["system_version"].lower()
|
72
|
+
elif "iosxe" in conn.genie_platform:
|
73
|
+
if "version" in output_parsed["version"]:
|
74
|
+
# Replace all "(" and ")" in the version to normalizing version format to the Cisco support API
|
75
|
+
# -> Make 15.2(7)E6 to 15.2.7E6
|
76
|
+
version = output_parsed["version"]["version"].replace("(", ".")
|
77
|
+
version = version.replace(")", "")
|
78
|
+
elif "xe_version" in output_parsed["version"]:
|
79
|
+
# Replace all 0 in the xe_version to normalizing iosxe and non-iosxe version format
|
80
|
+
# -> Make 17.03.05 to 17.3.5
|
81
|
+
version = output_parsed["version"]["xe_version"].replace("0", "")
|
82
|
+
else:
|
83
|
+
result = (
|
84
|
+
f"'{task.name}' -> CliResponse <Success: False>\n"
|
85
|
+
f"-> Unsupport scrapli genie platform: {conn.genie_platform}\n"
|
86
|
+
f"-> Or no software version identified for that platform type\n\n"
|
87
|
+
f"\n{verbose_result}"
|
88
|
+
)
|
89
|
+
# Return the version which should still be None, the result string and a boolian for failed
|
90
|
+
return version, result, True
|
91
|
+
|
92
|
+
# Return the successfully found version, the verbose result string and a boolian for failed
|
93
|
+
return version, verbose_result, False
|
94
|
+
|
95
|
+
|
96
|
+
def _get_serial_numbers(task: Task) -> tuple:
|
97
|
+
"""
|
98
|
+
TBD
|
99
|
+
"""
|
100
|
+
# Manually create Scrapli connection
|
101
|
+
conn = task.host.get_connection("scrapli", task.nornir.config)
|
102
|
+
# Run Scrapli send_command to get show version
|
103
|
+
output = conn.send_command(command="show version")
|
104
|
+
|
105
|
+
# Parse the show version scrapli response with the genie parter
|
106
|
+
output_parsed = output.genie_parse_output()
|
107
|
+
verbose_result = json.dumps(output_parsed, indent=4)
|
108
|
+
|
109
|
+
# Find the correct serial numbers of the device (multiple serials if its a stack)
|
110
|
+
serials = {}
|
111
|
+
# Dict for additional serials which are managed by that host, e.g. WLC
|
112
|
+
add_serials = {}
|
113
|
+
|
114
|
+
# If the devices is from the platform family nexus
|
115
|
+
if "nxos" in conn.genie_platform:
|
116
|
+
# Run Scrapli send_command to get show inventory
|
117
|
+
output = conn.send_command(command="show inventory")
|
118
|
+
# Parse the show inventory scrapli response with the genie parter
|
119
|
+
output_parsed = output.genie_parse_output()
|
120
|
+
verbose_result = json.dumps(output_parsed, indent=4)
|
121
|
+
# Get the serial number from show licence all
|
122
|
+
serials["1"] = output_parsed["name"]["Chassis"]["serial_number"].upper()
|
123
|
+
|
124
|
+
# If the devices is from the platform family iosxe
|
125
|
+
elif "iosxe" in conn.genie_platform:
|
126
|
+
# If the device have a catalyst 9800 WLC
|
127
|
+
if "C9800_IOSXE-K9" in output_parsed["version"]["image_id"]:
|
128
|
+
# Run Scrapli send_command to get show inventory
|
129
|
+
output = conn.send_command(command="show license all")
|
130
|
+
# Parse the show inventory scrapli response with the genie parter
|
131
|
+
output_parsed = output.genie_parse_output()
|
132
|
+
verbose_result = json.dumps(output_parsed, indent=4)
|
133
|
+
# Get the WLC serial numbers from show licence all
|
134
|
+
for num, item in enumerate(output_parsed["product_information"]["ha_udi_list"].values(), start=1):
|
135
|
+
serials[str(num)] = item["sn"]
|
136
|
+
# Run Scrapli send_command to get show ap config general
|
137
|
+
output = conn.send_command(command="show ap config general")
|
138
|
+
# Parse the show ap config general scrapli response with the genie parter to get all APs
|
139
|
+
output_parsed = output.genie_parse_output()
|
140
|
+
verbose_result += "\n" + json.dumps(output_parsed, indent=4)
|
141
|
+
# Find the correct serial numbers of all associated APs and create an dict
|
142
|
+
for ap_name, ap_data in output_parsed["ap_name"].items():
|
143
|
+
add_serials[ap_data["ap_serial_number"]] = ap_name
|
144
|
+
|
145
|
+
# If the device is a catalyst 9000 router
|
146
|
+
elif "c8000be" in output_parsed["version"]["platform"]:
|
147
|
+
serials["1"] = output_parsed["version"]["chassis_sn"].upper()
|
148
|
+
|
149
|
+
# If the device have a catalyst 9000 IOS-XE or a previous IOS image
|
150
|
+
elif output_parsed["version"]["os"] == "IOS-XE" or output_parsed["version"]["os"] == "IOS":
|
151
|
+
for switch_num, switch_details in output_parsed["version"]["switch_num"].items():
|
152
|
+
serials[switch_num] = switch_details["system_sn"].upper()
|
153
|
+
|
154
|
+
# If nothing matched before
|
155
|
+
else:
|
156
|
+
result = (
|
157
|
+
f"'{task.name}' -> CliResponse <Success: False>\n"
|
158
|
+
f"-> Or no serials identified for that platform type {conn.genie_platform}\n\n"
|
159
|
+
f"\n{verbose_result}"
|
160
|
+
)
|
161
|
+
# Return the serials and add_serials dict, the result string and a boolian for failed
|
162
|
+
return serials, add_serials, result, True
|
163
|
+
|
164
|
+
else:
|
165
|
+
result = (
|
166
|
+
f"'{task.name}' -> CliResponse <Success: False>\n"
|
167
|
+
f"-> Unsupport scrapli genie platform: {conn.genie_platform}\n"
|
168
|
+
f"-> Or no serials identified for that platform type\n\n"
|
169
|
+
f"\n{verbose_result}"
|
170
|
+
)
|
171
|
+
# Return the serials and add_serials dict, the result string and a boolian for failed
|
172
|
+
return serials, add_serials, result, True
|
173
|
+
|
174
|
+
# Order the serials dict by the switch_num in ascending order
|
175
|
+
serials = {key: serials[key] for key in sorted(list(serials.keys()))}
|
176
|
+
|
177
|
+
# Return the serials and add_serials dict, the verbose result string and a boolian for failed
|
178
|
+
return serials, add_serials, verbose_result, False
|
179
|
+
|
180
|
+
|
181
|
+
#### Single Nornir Screen Scraping Tasks #####################################################################
|
182
|
+
|
183
|
+
|
184
|
+
def create_diff_running_config_golden_config_task(task: Task) -> Result:
|
185
|
+
"""
|
186
|
+
TBD
|
187
|
+
"""
|
188
|
+
# Manually create Scrapli connection
|
189
|
+
conn = task.host.get_connection("scrapli", task.nornir.config)
|
190
|
+
|
191
|
+
# Run Scrapli send_command to get show running-config
|
192
|
+
running_config = conn.send_command(command="show running-config")
|
193
|
+
if running_config.failed:
|
194
|
+
result = (
|
195
|
+
f"'{task.name}' -> ScrapliResponse <Success: False>\n"
|
196
|
+
+ "-> Scrapli failed to get 'show running-config'"
|
197
|
+
)
|
198
|
+
return Result(host=task.host, result=result, failed=True)
|
199
|
+
|
200
|
+
# Cisco Config normalization
|
201
|
+
running_config_str = _cisco_config_normalization(running_config.result)
|
202
|
+
|
203
|
+
# Run Scrapli send_command to get more flash:golden-config
|
204
|
+
golden_config = conn.send_command(command="more flash:golden-config")
|
205
|
+
if golden_config.failed:
|
206
|
+
result = (
|
207
|
+
f"'{task.name}' -> ScrapliResponse <Success: False>\n"
|
208
|
+
+ "-> Scrapli failed to get 'more flash:golden-config'"
|
209
|
+
)
|
210
|
+
return Result(host=task.host, result=result, failed=True)
|
211
|
+
|
212
|
+
# Cisco Config normalization
|
213
|
+
golden_config_str = _cisco_config_normalization(golden_config.result)
|
214
|
+
|
215
|
+
# Create a unified diff with the Python standard library difflib
|
216
|
+
diff = difflib.unified_diff(
|
217
|
+
running_config_str.splitlines(),
|
218
|
+
golden_config_str.splitlines(),
|
219
|
+
"running-config",
|
220
|
+
"golden-config",
|
221
|
+
)
|
222
|
+
|
223
|
+
# Create a string of the diff generator
|
224
|
+
diff = "\n".join([i.rstrip() for i in diff])
|
225
|
+
|
226
|
+
# Check the diff string and set the result
|
227
|
+
if diff:
|
228
|
+
result = (
|
229
|
+
f"'{task.name}' -> NornirResponse <Success: False>\n"
|
230
|
+
+ "-> There is a diff between the running-config and the golden-config\n"
|
231
|
+
+ f"\n{diff}"
|
232
|
+
)
|
233
|
+
return Result(host=task.host, result=result, failed=True)
|
234
|
+
|
235
|
+
result = (
|
236
|
+
f"'{task.name}' -> NornirResponse <Success: True>\n"
|
237
|
+
+ "-> The running-config and the golden-config are identical"
|
238
|
+
)
|
239
|
+
return Result(host=task.host, result=result, failed=False)
|
240
|
+
|
241
|
+
|
242
|
+
def cli_verify_current_software_version_task(task: Task, verbose=False) -> Result:
|
243
|
+
"""
|
244
|
+
TBD
|
245
|
+
"""
|
246
|
+
# Get the desired version from the Nornir inventory
|
247
|
+
desired_version = task.host["software"]["version"]
|
248
|
+
|
249
|
+
# Use the helper function _get_software_version to get the software version
|
250
|
+
current_version, verbose_result, failed = _get_software_version(task=task)
|
251
|
+
|
252
|
+
# If failed is the the verbose result contains the full Nornir result string (result + verbose_result)
|
253
|
+
if failed:
|
254
|
+
# Return the Nornir result as failed
|
255
|
+
result = verbose_result
|
256
|
+
return Result(host=task.host, result=result, failed=True, overall_task_failed=True)
|
257
|
+
|
258
|
+
# Write the current version into the Nornir inventory
|
259
|
+
task.host["software"]["current_version"] = current_version
|
260
|
+
|
261
|
+
# If the desired version and the current version are the same
|
262
|
+
if desired_version in current_version:
|
263
|
+
result = (
|
264
|
+
f"'{task.name}' -> CliResponse <Success: True>\n"
|
265
|
+
f"-> Desired version {desired_version} match installed version {current_version}"
|
266
|
+
)
|
267
|
+
# Define the result variable for print_result
|
268
|
+
result = result + "\n\n" + verbose_result if verbose else result
|
269
|
+
|
270
|
+
# Return the Nornir result as success
|
271
|
+
return Result(host=task.host, result=result)
|
272
|
+
|
273
|
+
# Else the desired version and the current version are not the same
|
274
|
+
result = (
|
275
|
+
f"'{task.name}' -> CliResponse <Success: False>\n"
|
276
|
+
f"-> Desired version {desired_version} don't match installed version {current_version}"
|
277
|
+
)
|
278
|
+
# Define the result variable for print_result
|
279
|
+
result = result + "\n\n" + verbose_result if verbose else result
|
280
|
+
|
281
|
+
# Return the Nornir result as failed
|
282
|
+
return Result(host=task.host, result=result, failed=True, need_upgrade=True)
|
283
|
+
|
284
|
+
|
285
|
+
def cli_install_one_shot_task(task: Task, issu: bool = False, verbose: bool = False) -> Result:
|
286
|
+
"""
|
287
|
+
TBD
|
288
|
+
"""
|
289
|
+
# Get the host destination file from the Nornir inventory
|
290
|
+
dest_file = task.host["software"]["dest_file"]
|
291
|
+
|
292
|
+
# Manually create Netmiko connection
|
293
|
+
net_connect = task.host.get_connection("netmiko", task.nornir.config)
|
294
|
+
|
295
|
+
# Define the command string for the one-shot software install with or without ISSU
|
296
|
+
if issu:
|
297
|
+
command_string = f"install add file flash:{dest_file} activate issu commit"
|
298
|
+
expect_string = "install_add_activate_commit: Activating PACKAGE"
|
299
|
+
else:
|
300
|
+
command_string = f"install add file flash:{dest_file} activate commit prompt-level none"
|
301
|
+
expect_string = "install_add_activate_commit: Activating PACKAGE"
|
302
|
+
|
303
|
+
# As the command is really long Netmiko stops when the expect_string is seen. When the package activating
|
304
|
+
# start the software is added, nothing should go wrong anymore and the fping_track_upgrade_process()
|
305
|
+
# can be started to verify the upgrade and reload process.
|
306
|
+
output = net_connect.send_command(
|
307
|
+
command_string=command_string, expect_string=expect_string, read_timeout=1200
|
308
|
+
)
|
309
|
+
|
310
|
+
if expect_string in output:
|
311
|
+
result_summary = f"'{task.name}' -> CliResponse <Success: True>"
|
312
|
+
# Define the result variable for print_result
|
313
|
+
result = result_summary + "\n\n" + output if verbose else result_summary
|
314
|
+
|
315
|
+
# Return the Nornir result as success
|
316
|
+
return Result(host=task.host, result=result)
|
317
|
+
|
318
|
+
# Else the cli one-shot software install failed
|
319
|
+
result = f"'{task.name}' -> CliResponse <Success: False>\n\n" f"{result}"
|
320
|
+
|
321
|
+
# Return the custom Nornir result as failed
|
322
|
+
return Result(host=task.host, result=result, failed=True)
|
323
|
+
|
324
|
+
|
325
|
+
def cli_install_remove_inactive_task(task: Task, verbose=False) -> Result:
|
326
|
+
"""
|
327
|
+
TBD
|
328
|
+
"""
|
329
|
+
# Set the result_summary for a successful task
|
330
|
+
result_summary = f"'{task.name}' -> CliResponse <Success: True>"
|
331
|
+
|
332
|
+
# Manually create Netmiko connection
|
333
|
+
net_connect = task.host.get_connection("netmiko", task.nornir.config)
|
334
|
+
|
335
|
+
# Execute send_command with an expect_string to finish. Netmiko feeds the expect_string into pythons RE
|
336
|
+
# library and therefor the pipe can be used as logical "or" statement.
|
337
|
+
output = net_connect.send_command(
|
338
|
+
command_string="install remove inactive",
|
339
|
+
expect_string="Do you want to remove the above files?|SUCCESS: install_remove",
|
340
|
+
read_timeout=600,
|
341
|
+
)
|
342
|
+
|
343
|
+
if "Do you want to remove the above files?" in output:
|
344
|
+
output += net_connect.send_command(
|
345
|
+
command_string="y",
|
346
|
+
expect_string=f"{task.host}#",
|
347
|
+
read_timeout=600,
|
348
|
+
)
|
349
|
+
# Define the result variable for print_result
|
350
|
+
result = result_summary + "\n\n" + output if verbose else result_summary
|
351
|
+
|
352
|
+
# Return the custom Nornir result as success
|
353
|
+
return Result(host=task.host, result=result)
|
354
|
+
|
355
|
+
if "SUCCESS: install_remove" in output:
|
356
|
+
# Define the result variable for print_result
|
357
|
+
result = result_summary + "\n\n" + output if verbose else result_summary
|
358
|
+
|
359
|
+
# Return the custom Nornir result as success
|
360
|
+
return Result(host=task.host, result=result)
|
361
|
+
|
362
|
+
# Else the command install remove inactive failed without traceback exception
|
363
|
+
result = f"'{task.name}' -> CliResponse <Success: False>\n\n{output}"
|
364
|
+
# Return the Nornir result as failed
|
365
|
+
return Result(host=task.host, result=result, failed=True)
|
366
|
+
|
367
|
+
|
368
|
+
def cli_get_serial_numbers(task: Task, verbose: bool = False) -> Result:
|
369
|
+
"""
|
370
|
+
TBD
|
371
|
+
"""
|
372
|
+
# Use the helper function _get_serial_numbers to get the software version
|
373
|
+
serials, add_serials, verbose_result, failed = _get_serial_numbers(task=task)
|
374
|
+
|
375
|
+
# If failed is the the verbose result contains the full Nornir result string (result + verbose_result)
|
376
|
+
if failed:
|
377
|
+
# Return the Nornir result as failed
|
378
|
+
result = verbose_result
|
379
|
+
return Result(host=task.host, result=result, failed=True)
|
380
|
+
|
381
|
+
# Order the serials dict by the switch_num in ascending order
|
382
|
+
order_list = sorted(list(serials.keys()))
|
383
|
+
serials = {key: serials[key] for key in order_list}
|
384
|
+
|
385
|
+
# The serial number was found, so the serials dict in not empty
|
386
|
+
result = f"'{task.name}' -> CliResponse <Success: True>"
|
387
|
+
for switch_num, serial in serials.items():
|
388
|
+
result += f"\n-> Device {switch_num}: {serial}"
|
389
|
+
for serial in add_serials.keys(): # pylint: disable=consider-iterating-dictionary
|
390
|
+
result += f"\n-> Sub-Device: {serial}"
|
391
|
+
# Set the result print level to summary or verbose
|
392
|
+
result = result + "\n\n" + verbose_result if verbose else result
|
393
|
+
|
394
|
+
# Return the Nornir result as successful
|
395
|
+
return Result(host=task.host, result=result, serials=serials, add_serials=add_serials)
|
396
|
+
|
397
|
+
|
398
|
+
def cli_get_software_version(task: Task, verbose: bool = False) -> Result:
|
399
|
+
"""
|
400
|
+
TBD
|
401
|
+
"""
|
402
|
+
# Use the helper function _get_software_version to get the software version
|
403
|
+
version, verbose_result, failed = _get_software_version(task=task)
|
404
|
+
|
405
|
+
# If failed is the the verbose_result contains the full Nornir result string (result + verbose_result)
|
406
|
+
if failed:
|
407
|
+
# Return the Nornir result as failed
|
408
|
+
result = verbose_result
|
409
|
+
return Result(host=task.host, result=result, failed=True)
|
410
|
+
|
411
|
+
# The software version was found and is not None
|
412
|
+
result = f"'{task.name}' -> CliResponse <Success: True>\n" f"-> Device software version {version}"
|
413
|
+
# Set the result print level to summary or verbose
|
414
|
+
result = result + "\n\n" + verbose_result if verbose else result
|
415
|
+
|
416
|
+
# Return the custom Nornir result as successful
|
417
|
+
return Result(host=task.host, result=result, version=version)
|
418
|
+
|
419
|
+
|
420
|
+
def cli_verify_destination_md5_hash_task(task: Task) -> Result:
|
421
|
+
"""
|
422
|
+
TBD
|
423
|
+
"""
|
424
|
+
|
425
|
+
# Prepare all needed variables form the Nornir inventory
|
426
|
+
dest_file = task.host["software"]["dest_file"]
|
427
|
+
source_md5 = task.host["software"]["source_md5"]
|
428
|
+
|
429
|
+
# Manually create Scrapli connection
|
430
|
+
conn = task.host.get_connection("scrapli", task.nornir.config)
|
431
|
+
# Run the Scrapli Nornir task send_command to verify the destination file md5 hash
|
432
|
+
output = conn.send_command(
|
433
|
+
command=f"verify /md5 flash:{dest_file} {source_md5}",
|
434
|
+
strip_prompt=True,
|
435
|
+
timeout_ops=180,
|
436
|
+
)
|
437
|
+
|
438
|
+
# Extract the md5 hash from the output string
|
439
|
+
for line in output.result.splitlines():
|
440
|
+
# Source and destination md5 hast are identical
|
441
|
+
if line.startswith("Verified"):
|
442
|
+
# Split the line string into words
|
443
|
+
dest_md5 = line.split()
|
444
|
+
# Slicing the list -> -1 means the last element of the list which is the md5 hash
|
445
|
+
dest_md5 = dest_md5[-1]
|
446
|
+
result = (
|
447
|
+
f"'{task.name}' -> CliResponse <Success: True>\n"
|
448
|
+
"MD5-Hashes are identical:\n"
|
449
|
+
f"-> Source MD5-Hash: {source_md5}\n"
|
450
|
+
f"-> Destination MD5-Hash: {dest_md5}"
|
451
|
+
)
|
452
|
+
# Return the custom Nornir result as successful
|
453
|
+
return Result(host=task.host, result=result)
|
454
|
+
|
455
|
+
# Source and destination md5 hash are different -> Upload failed
|
456
|
+
if line.startswith("Computed signature"):
|
457
|
+
# Split the string into words
|
458
|
+
dest_md5 = line.split()
|
459
|
+
# Slicing the list -> -1 means the last element of the list which is the md5 hash
|
460
|
+
dest_md5 = dest_md5[-1]
|
461
|
+
result = (
|
462
|
+
f"'{task.name}' -> CliResponse <Success: False>\n"
|
463
|
+
"MD5-Hashes are not identical:\n"
|
464
|
+
f"-> Source MD5-Hash: {source_md5}\n"
|
465
|
+
f"-> Destination MD5-Hash: {dest_md5}"
|
466
|
+
)
|
467
|
+
# Return the custom Nornir result as failed
|
468
|
+
return Result(host=task.host, result=result, failed=True)
|
469
|
+
|
470
|
+
# There is an %Error as the file don't exist
|
471
|
+
if line.startswith("%Error"):
|
472
|
+
result = f"'{task.name}' -> CliResponse <Success: False>\n" + f"-> {line}"
|
473
|
+
# Return the custom Nornir result as failed
|
474
|
+
return Result(host=task.host, result=result, failed=True)
|
475
|
+
|
476
|
+
# If no if statement match the whole for loop -> Return the custom Nornir result as failed
|
477
|
+
result = f"'{task.name}' -> CliResponse <Success: False>\n" + f"-> {output.result}"
|
478
|
+
return Result(host=task.host, result=result, failed=True, overall_task_failed=True)
|
479
|
+
|
480
|
+
|
481
|
+
def custom_write_file(
|
482
|
+
task: Task, commands: list, path: str, filename_suffix: str, backup_config: bool = False
|
483
|
+
) -> Result:
|
484
|
+
"""
|
485
|
+
This custom Nornir task takes a list of commands to execute, a path and a filename suffix as arguments and
|
486
|
+
writes the output of the commands to that file. The start of the filename is the hostname and as suffix
|
487
|
+
can any text be added.
|
488
|
+
"""
|
489
|
+
# Manually create Scrapli connection
|
490
|
+
conn = task.host.get_connection("scrapli", task.nornir.config)
|
491
|
+
|
492
|
+
# The emtpy string will be filled and written to file
|
493
|
+
content = ""
|
494
|
+
|
495
|
+
# Execute each command individual to add custom content for each command
|
496
|
+
for command in commands:
|
497
|
+
output = conn.send_command(command=command, strip_prompt=True, timeout_ops=180)
|
498
|
+
|
499
|
+
if backup_config:
|
500
|
+
# If the file should be a backup of the configuration, then remove unwanted lines which are not
|
501
|
+
# real configurations and to avoid unwanted git commits
|
502
|
+
for line in output.result.splitlines():
|
503
|
+
if line.startswith("Building configuration..."):
|
504
|
+
continue
|
505
|
+
if line.startswith("Current configuration :"):
|
506
|
+
continue
|
507
|
+
if line.startswith("! Last configuration change"):
|
508
|
+
continue
|
509
|
+
if line.startswith("! NVRAM config last updated"):
|
510
|
+
continue
|
511
|
+
|
512
|
+
# If no line to exclude matched, add the line to the content
|
513
|
+
content += f"{line}\n"
|
514
|
+
|
515
|
+
else:
|
516
|
+
# Add the promt with the command and then the command result
|
517
|
+
content += f"{task.host}#{command}\n{output.result}\n\n"
|
518
|
+
|
519
|
+
# Remove any possible starting or ending blank lines
|
520
|
+
content = content.rstrip()
|
521
|
+
content = content.lstrip()
|
522
|
+
|
523
|
+
# Write the content variable to the file specified by the arguments
|
524
|
+
task.run(task=write_file, filename=f"{path}/{task.host}{filename_suffix}", content=content, append=False)
|
525
|
+
|
526
|
+
|
527
|
+
#### Nornir Task in regular Functions ########################################################################
|
528
|
+
|
529
|
+
|
530
|
+
def nr_pre_config_check(nr: Nornir) -> bool:
|
531
|
+
"""
|
532
|
+
Perform a pre-configuration check by verifying the running configuration against the golden configuration.
|
533
|
+
This function runs a custom Nornir Scrapli task to create a diff between the running configuration and
|
534
|
+
the golden configuration. It prints the task title and the results of the task. If the task fails, it
|
535
|
+
indicates that the running configuration and the golden configuration are not identical.
|
536
|
+
"""
|
537
|
+
# pylint: disable=invalid-name
|
538
|
+
|
539
|
+
print_task_title("Pre-configuration check")
|
540
|
+
|
541
|
+
# Run the custom Nornir Scrapli task create_diff_running_config_golden_config
|
542
|
+
task_text = "NORNIR verify running-config against golden-config"
|
543
|
+
result = nr.run(
|
544
|
+
name=task_text,
|
545
|
+
task=create_diff_running_config_golden_config_task,
|
546
|
+
on_failed=True,
|
547
|
+
)
|
548
|
+
# Print the results
|
549
|
+
print_result(result)
|
550
|
+
|
551
|
+
# Return False if the overall task failed as the running-config and golden-config are not identical
|
552
|
+
return not result.failed
|
553
|
+
|
554
|
+
|
555
|
+
def cli_verify_destination_md5_hash(nr):
|
556
|
+
"""
|
557
|
+
Verifies the MD5 hash of a destination file on network devices using Nornir. This function runs a custom
|
558
|
+
Nornir task to verify the MD5 hash of a destination file on network devices. It prints the result of the
|
559
|
+
task and checks if the overall task failed. If the task fails, it prints an error message and exits the
|
560
|
+
script. It also collects and returns a list of hosts where the destination file MD5 hash does not match
|
561
|
+
or the destination file does not exist.
|
562
|
+
"""
|
563
|
+
# pylint: disable=invalid-name
|
564
|
+
|
565
|
+
# Run the custom Nornir task verify_destination_md5_hash
|
566
|
+
task_result = nr.run(
|
567
|
+
task=cli_verify_destination_md5_hash_task,
|
568
|
+
name="CLI verify destination file",
|
569
|
+
on_failed=True,
|
570
|
+
)
|
571
|
+
|
572
|
+
# Print the Nornir task result
|
573
|
+
print_result(task_result)
|
574
|
+
|
575
|
+
# If the task overall task result failed -> Print results and exit the script
|
576
|
+
for host in task_result:
|
577
|
+
if hasattr(task_result[host], "overall_task_failed"):
|
578
|
+
print("\n")
|
579
|
+
print(task_error(text="CLI verify destination file", changed=False))
|
580
|
+
print("\U0001f4a5 ALERT: CLI DESTINATION FILE MD5 HASH VERIFICATION FAILED! \U0001f4a5")
|
581
|
+
print(
|
582
|
+
f"\n{Style.BRIGHT}{Fore.RED}-> Analyse the Nornir output for failed task results\n"
|
583
|
+
"-> May apply Nornir inventory changes and run the script again\n"
|
584
|
+
)
|
585
|
+
sys.exit(1)
|
586
|
+
|
587
|
+
# List to fill with hosts with not matching destination file md5 hash or not existing destination file
|
588
|
+
failed_hosts = list(task_result.failed_hosts)
|
589
|
+
|
590
|
+
return failed_hosts
|
591
|
+
|
592
|
+
|
593
|
+
def write_commands_to_file(
|
594
|
+
nr: Nornir,
|
595
|
+
name: str,
|
596
|
+
commands: list,
|
597
|
+
path: str,
|
598
|
+
filename_suffix: str,
|
599
|
+
backup_config: bool = False,
|
600
|
+
verbose: bool = False,
|
601
|
+
) -> bool:
|
602
|
+
"""
|
603
|
+
#### CODE REFACTORING NEEDED -> INTRODUCE print_result ####
|
604
|
+
|
605
|
+
This function takes a list of commands to execute, a path and a filename suffix as arguments and writes
|
606
|
+
the output of the commands to that file. The start of the filename is the hostname and as suffix can any
|
607
|
+
text be added.
|
608
|
+
"""
|
609
|
+
# pylint: disable=invalid-name,too-many-arguments,too-many-locals
|
610
|
+
|
611
|
+
# Set the variable to return at the end of the function to True
|
612
|
+
cfg_status = True
|
613
|
+
|
614
|
+
print(task_name(text=name))
|
615
|
+
|
616
|
+
task_result = nr.run(
|
617
|
+
task=custom_write_file,
|
618
|
+
commands=commands,
|
619
|
+
path=path,
|
620
|
+
filename_suffix=filename_suffix,
|
621
|
+
backup_config=backup_config,
|
622
|
+
on_failed=True,
|
623
|
+
)
|
624
|
+
|
625
|
+
for host, multi_result in task_result.items():
|
626
|
+
print(task_host(host=host, changed=task_result[host].changed))
|
627
|
+
|
628
|
+
# Print first all Scrapli send_command results
|
629
|
+
for result in multi_result:
|
630
|
+
# Get the attribute scrapli_response from result into a variable -> Return None if don't exist
|
631
|
+
scrapli_result = getattr(result, "scrapli_response", None)
|
632
|
+
|
633
|
+
if scrapli_result is None:
|
634
|
+
continue
|
635
|
+
|
636
|
+
task_text = "Execute command"
|
637
|
+
cmd = str(scrapli_result.channel_input)
|
638
|
+
|
639
|
+
if scrapli_result.failed:
|
640
|
+
print(task_error(text=task_text, changed=scrapli_result.failed))
|
641
|
+
print(f"'{cmd}' -> CliResponse <Success: False>\n")
|
642
|
+
print(f"{host}#{cmd}")
|
643
|
+
print(scrapli_result.result)
|
644
|
+
else:
|
645
|
+
print(task_info(text=task_text, changed=scrapli_result.failed))
|
646
|
+
print(f"'{cmd}' -> CliResponse <Success: True>")
|
647
|
+
if verbose:
|
648
|
+
print(f"{host}#{cmd}")
|
649
|
+
print(scrapli_result.result)
|
650
|
+
|
651
|
+
# The write_file result is only present if all Scrapli send_command tasks were successful
|
652
|
+
filepath = f"{path}/{host}{filename_suffix}"
|
653
|
+
|
654
|
+
# pylint: disable=undefined-loop-variable
|
655
|
+
if [True for result in multi_result if result.name == "write_file"]:
|
656
|
+
# The write_file result is present and no exception exists
|
657
|
+
if result.exception is None:
|
658
|
+
print(task_info(text="Save command(s) to file", changed=False))
|
659
|
+
print(f"'{filepath}' -> NornirResponse <Success: True>")
|
660
|
+
|
661
|
+
# An exception is raised when the folder don't exist
|
662
|
+
else:
|
663
|
+
print(task_error(text="Save command(s) to file", changed=False))
|
664
|
+
print(f"'{filepath}' -> NornirResponse <Success: False>\n")
|
665
|
+
print(result.exception)
|
666
|
+
cfg_status = False
|
667
|
+
|
668
|
+
# The write_file result is not present as one or more commands have failed
|
669
|
+
else:
|
670
|
+
print(task_error(text="Save command(s) to file", changed=False))
|
671
|
+
print(f"'{filepath}' -> NornirResponse <Success: False>\n")
|
672
|
+
print("Command(s) failed -> Command(s) have not been written to file\n")
|
673
|
+
print(result.exception)
|
674
|
+
print(result.result)
|
675
|
+
cfg_status = False
|
676
|
+
|
677
|
+
return cfg_status
|
File without changes
|