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,794 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ This module contains general configuration management functions and tasks related to Nornir.
4
+
5
+ The functions are ordered as followed:
6
+ - Helper Functions
7
+ - Nornir print functions
8
+ - Nornir Helper Tasks
9
+ """
10
+
11
+ import os
12
+ import sys
13
+ import time
14
+ import subprocess # nosec
15
+ import argparse
16
+ import urllib
17
+ from typing import Literal
18
+ import __main__
19
+ from colorama import Fore, Style, init
20
+ from yaspin import yaspin
21
+ from yaspin.spinners import Spinners
22
+ from nornir_scrapli.tasks import send_commands
23
+ from nornir.core import Nornir
24
+ from nornir.core.task import Task, Result
25
+ from nornir_netmiko.tasks import netmiko_file_transfer
26
+ from nornir_collection.utils import (
27
+ CustomArgParse,
28
+ CustomArgParseWidthFormatter,
29
+ get_env_vars,
30
+ print_result,
31
+ print_task_name,
32
+ task_name,
33
+ task_host,
34
+ task_info,
35
+ compute_hash,
36
+ )
37
+
38
+ init(autoreset=True, strip=False)
39
+
40
+
41
+ #### Helper Functions ########################################################################################
42
+
43
+
44
+ def init_args_for_software_upgrade() -> argparse.Namespace:
45
+ """
46
+ This function initialze all arguments which are needed for further script execution. The default arguments
47
+ will be supressed. Returned will be a tuple with a use_nornir variable which is a boolian to indicate if
48
+ Nornir should be used for dynamically information gathering or not.
49
+ """
50
+ task_text = "ARGPARSE verify arguments"
51
+ print_task_name(text=task_text)
52
+
53
+ # Load environment variables or raise a TypeError when is None
54
+ env_vars = get_env_vars(envs=["NR_CONFIG_PROD", "NR_CONFIG_TEST"], task_text=task_text)
55
+ nr_config_prod = env_vars["NR_CONFIG_PROD"]
56
+ nr_config_test = env_vars["NR_CONFIG_TEST"]
57
+
58
+ # Define the arguments which needs to be given to the script execution
59
+ argparser = CustomArgParse(
60
+ prog=os.path.basename(__main__.__file__),
61
+ description="Specify the NetBox instance and filter the Nornir inventory based on a tag or a host",
62
+ epilog="At least one of the mandatory arguments role, tags or hosts needs to be specified.",
63
+ argument_default=argparse.SUPPRESS,
64
+ formatter_class=CustomArgParseWidthFormatter,
65
+ )
66
+
67
+ # Add all NetBox arguments
68
+ argparser.add_argument(
69
+ "--prod",
70
+ action="store_true",
71
+ help=f"use the NetBox 'PROD' instance and Nornir config '{nr_config_prod}'",
72
+ )
73
+ argparser.add_argument(
74
+ "--test",
75
+ action="store_true",
76
+ help=f"use the NetBox 'TEST' instance and Nornir config '{nr_config_test}'",
77
+ )
78
+ argparser.add_argument(
79
+ "--role", type=str, metavar="<ROLE>", help="inventory filter for a single device role"
80
+ )
81
+ argparser.add_argument(
82
+ "--tags", type=str, metavar="<TAGS>", help="inventory filter for comma seperated device tags"
83
+ )
84
+ argparser.add_argument(
85
+ "--hosts", type=str, metavar="<HOST-NAMES>", help="inventory filter for comma seperated device hosts"
86
+ )
87
+
88
+ # Add the optional commit_reload argument
89
+ argparser.add_argument(
90
+ "-c",
91
+ "--commit_reload",
92
+ action="store_true",
93
+ default=False,
94
+ help="commit the software upgrade to reload the device (default: False)",
95
+ )
96
+
97
+ # Add the optional issu argument
98
+ argparser.add_argument(
99
+ "-i",
100
+ "--issu",
101
+ action="store_true",
102
+ default=False,
103
+ help="enable ISSU software upgrade (default: False)",
104
+ )
105
+
106
+ # Add the optional remove_inactive argument
107
+ argparser.add_argument(
108
+ "-r",
109
+ "--remove_inactive",
110
+ action="store_true",
111
+ default=False,
112
+ help="remove inactive software files (default: False)",
113
+ )
114
+
115
+ # Add the optional rebuild argument
116
+ argparser.add_argument(
117
+ "-l",
118
+ "--local_upload",
119
+ action="store_true",
120
+ default=False,
121
+ help="enable local upload with SCP (default: HTTP download)",
122
+ )
123
+
124
+ # Add the optional verbose argument
125
+ argparser.add_argument(
126
+ "-v",
127
+ "--verbose",
128
+ action="store_true",
129
+ default=False,
130
+ help="show extensive result details (default: False)",
131
+ )
132
+
133
+ # Verify the provided arguments and print the custom argparse error message in case any error or wrong
134
+ # arguments are present and exit the script
135
+ args = argparser.parse_args()
136
+
137
+ # Verify the NetBox instance and Nornir config filepath
138
+ if not (hasattr(args, "prod") or hasattr(args, "test")):
139
+ argparser.error("No NetBox instance specified, add --prod or --test")
140
+ # Verify the Nornir filter arguments
141
+ if not (hasattr(args, "hosts") or hasattr(args, "role") or hasattr(args, "tags")):
142
+ argparser.error("No Nornir inventory filter specified, add --hosts and/or roles and/or --tags")
143
+
144
+ # Set the NetBox instance and the Nornir config file based on the arguments
145
+ nb_instance = "TEST" if hasattr(args, "test") else "PROD"
146
+ nr_config = nr_config_test if hasattr(args, "test") else nr_config_prod
147
+
148
+ # If argparser.parse_args() is successful -> no argparse error message
149
+ print(task_info(text=task_text, changed=False))
150
+ print(f"'{task_text}' -> ArgparseResponse <Success: True>")
151
+
152
+ print("-> Upgrade arguments:")
153
+ print(f" - Run on the '{nb_instance}' NetBox instance and Nornir config '{nr_config}'")
154
+ if args.commit_reload:
155
+ print(" - Commit the software upgrade to reload the device")
156
+ else:
157
+ print(" - No reload to commit the software (Software copy only)")
158
+ if args.issu:
159
+ print(" - Enable ISSU software upgrade without downtime (C9500 only)")
160
+ else:
161
+ print(" - Regular software upgrade with downtime (No ISSU)")
162
+ if args.remove_inactive:
163
+ print(" - Filesystem cleanup to remove inactive software files before upgrade")
164
+ else:
165
+ print(" - No filesystem cleanup before upgrade")
166
+ if args.local_upload:
167
+ print(" - Local software image upload by SCP")
168
+ else:
169
+ print(" - Remote software download by HTTP")
170
+
171
+ if args.verbose:
172
+ print(f"\n{args}")
173
+
174
+ return nr_config, args
175
+
176
+
177
+ def verify_issu_requirements(nr: Nornir) -> bool:
178
+ """
179
+ TBD
180
+ """
181
+ # pylint: disable=invalid-name
182
+
183
+ # Run the custom Nornir task verify_issu_requirements
184
+ task_result = nr.run(
185
+ task=verify_issu_requirements_task,
186
+ name="NORNIR verify ISSU requirements",
187
+ on_failed=True,
188
+ )
189
+
190
+ # Print the Nornir task result
191
+ print_result(task_result)
192
+
193
+ # Return False if the task failed or True if the task was successful
194
+ return not bool(task_result.failed)
195
+
196
+
197
+ def cli_track_issu_upgrade_process(nr: Nornir, max_time: int) -> None:
198
+ """
199
+ This function creates a dictionary with the installation process status of each host and runs the standard
200
+ Nornir Scrapli task send_commands in a range loop. In each loop the software installation status will be
201
+ updated and printed to std-out. There are three expected status which each host will go through the
202
+ installation process. When all hosts are upgraded successful the script exits the range loop and prints
203
+ the result to std-out. In case the software upgrade is not successful after the range loop is finish, an
204
+ info message will be printed and exit the script.
205
+ """
206
+ # pylint: disable=invalid-name,too-many-branches
207
+
208
+ # Printout sleep and refresh values
209
+ std_out_print = []
210
+ refresh_timer = 10
211
+ max_refresh = max_time // refresh_timer # double slash division is a int / single slash would be a float
212
+ elapsed_time = 0
213
+ # Set a elapsed timer to wait for the switch reload to start
214
+ elapsed_time_reload_switch_1 = 0
215
+ elapsed_time_reload_switch_2 = 0
216
+ # Stack reload and HA sync status list (except ready)
217
+ show_switch_status_not_ready = ["Removed", "Initializing", "HA sync in progress", "V-Mismatch"]
218
+
219
+ # Dict to track the host software upgrade status
220
+ update_status = {}
221
+ for host in nr.inventory.hosts:
222
+ update_status[
223
+ host
224
+ ] = f"{Fore.YELLOW}No ISSU operation is in progress (Installing software){Fore.RESET}"
225
+
226
+ print(task_name("Track ISSU software upgrade process"))
227
+
228
+ for _ in range(max_refresh):
229
+ # Run the standard Scrapli task send_command to get the software upgrade status
230
+ task = nr.run(
231
+ task=send_commands,
232
+ commands=["show issu state detail", "show switch"],
233
+ strip_prompt=False,
234
+ on_failed=True,
235
+ )
236
+ # Close all Nornir connections to ensure a reconnect after the switch reload is possible
237
+ nr.close_connections(on_failed=True)
238
+
239
+ # If std_out_print is not empty remove the previous print
240
+ # Cursor up the number of lines in std_out_print to overwrite/delete the previous print
241
+ if len(std_out_print) > 0:
242
+ sys.stdout.write("\033[F" * len(std_out_print)) # Cursor up the number of lines in std_out_print
243
+ sys.stdout.write("\033[2K") # Clear the line
244
+ sys.stdout.flush() # Flush the stdout buffer
245
+ std_out_print = []
246
+
247
+ # Update the host software upgrade status and print the result
248
+ for host in task:
249
+ output = str(task[host].result).rstrip()
250
+
251
+ # Get the lines from the Scrapli task result starting with "State transition:"
252
+ for line in output.splitlines():
253
+ # Step 1: Added Software and reload the Switch 2
254
+ if line == "State transition: Added":
255
+ # Wait to reload timer to exceed 120s to check the stack status
256
+ if elapsed_time_reload_switch_2 <= 120:
257
+ update_status[host] = f"{Fore.YELLOW}{line}{Fore.RESET}"
258
+ elapsed_time_reload_switch_2 += refresh_timer
259
+ # Check if a reload or stack sync is in progress
260
+ elif any(status in output for status in show_switch_status_not_ready):
261
+ update_status[host] = f"{Fore.YELLOW}{line} (Reloading Switch 2){Fore.RESET}"
262
+ else:
263
+ update_status[host] = f"{Fore.YELLOW}{line}{Fore.RESET}"
264
+ # Break the loop to continue with the next host
265
+ break
266
+
267
+ # Step 2: Activate the standby switch
268
+ if line == "State transition: Added -> Standby activated":
269
+ update_status[host] = f"{Fore.YELLOW}{line}{Fore.RESET}"
270
+ # Break the loop to continue with the next host
271
+ break
272
+
273
+ # Step 3: Reload the Switch 1
274
+ if line == "State transition: Added -> Standby activated -> Active switched-over":
275
+ # Wait to reload timer to exceed 120s to check the stack status
276
+ if elapsed_time_reload_switch_1 <= 120:
277
+ update_status[host] = f"{Fore.YELLOW}{line}{Fore.RESET}"
278
+ elapsed_time_reload_switch_1 += refresh_timer
279
+ # Check if a reload or stack sync is in progress
280
+ elif any(status in output for status in show_switch_status_not_ready):
281
+ update_status[host] = f"{Fore.YELLOW}{line} (Reloading Switch 1){Fore.RESET}"
282
+ else:
283
+ update_status[host] = f"{Fore.GREEN}ISSU Upgrade finish{Fore.RESET}"
284
+ # Break the loop to continue with the next host
285
+ break
286
+
287
+ # Add the host software upgrade status result to the std_out_print list
288
+ std_out_print.append(task_host(host=host, changed=False))
289
+ std_out_print.append(f"ISSU State: {update_status[host]}")
290
+
291
+ # Print empty line
292
+ std_out_print.append("")
293
+
294
+ # Check if all hosts have upgraded successfull
295
+ if not all(
296
+ f"{Fore.GREEN}ISSU Upgrade finish{Fore.RESET}" in value for value in update_status.values()
297
+ ):
298
+ # Continue the range loop to track to software upgrade status
299
+ total_time = max_refresh * refresh_timer
300
+ std_out_print.append(
301
+ f"{Style.BRIGHT}{Fore.YELLOW}Elapsed waiting time: {elapsed_time}/{total_time}s"
302
+ )
303
+ std_out_print.append(f"{Style.DIM}(The ISSU task result will refresh in {refresh_timer}s)")
304
+ # Print the loop result
305
+ for line in std_out_print:
306
+ print(line)
307
+ # Wait for the refresh timer to continue the range loop
308
+ elapsed_time += refresh_timer
309
+ time.sleep(refresh_timer)
310
+
311
+ else:
312
+ # Print result and exit the range loop
313
+ std_out_print.append(
314
+ f"{Style.BRIGHT}{Fore.GREEN}Elapsed waiting time: {elapsed_time}/{total_time}s"
315
+ )
316
+ std_out_print.append("Wait 120s to ensure the device NGINX RESTCONF server is ready")
317
+ # Print the loop result
318
+ for line in std_out_print:
319
+ print(line)
320
+ # Sleep for some seconds until the device NGINX RESTCONF server is ready
321
+ time.sleep(120)
322
+ break
323
+
324
+ # If the range loop reached the end -> Software upgrade not successful
325
+ else:
326
+ sys.stdout.write("\033[F") # Cursor up one line
327
+ sys.stdout.write("\033[2K") # Clear the line
328
+ sys.stdout.flush() # Flush the stdout buffer
329
+ print(
330
+ f"{Style.BRIGHT}{Fore.RED}"
331
+ f"Total ISSU software upgrade waiting time of {max_refresh * refresh_timer}s exceeded"
332
+ )
333
+
334
+
335
+ def fping_track_upgrade_process(nr: Nornir, max_time: int) -> None:
336
+ """
337
+ This function creates a dictionary with the installation process status of each host and runs the custom
338
+ Nornir task fping_task in a range loop. In each loop the software installation status will be updated and
339
+ printed to std-out. There are three expected status which each host will go through the installation
340
+ process. These status are "Installing software", "Rebooting device" and the final status will be "Upgrade
341
+ finish". When all hosts are upgraded successful the script exits the range loop and prints the result to
342
+ std-out. In case the software upgrade is not successful after the range loop is finish, an info message
343
+ will be printed and exit the script.
344
+ """
345
+ # pylint: disable=invalid-name
346
+
347
+ # Printout sleep and refresh values
348
+ std_out_print = []
349
+ refresh_timer = 10
350
+ max_refresh = max_time // refresh_timer # double slash division is a int / single slash would be a float
351
+ elapsed_time = 0
352
+
353
+ # Dict to track the host software upgrade status
354
+ update_status = {}
355
+ for host in nr.inventory.hosts:
356
+ update_status[host] = "Installing software"
357
+
358
+ print(task_name("Track software upgrade process"))
359
+
360
+ for _ in range(max_refresh):
361
+ # Run the custom Nornir task fping_task
362
+ task = nr.run(task=fping_task, on_failed=True)
363
+ # Close all Nornir connections to ensure a reconnect after the switch reload is possible
364
+ nr.close_connections(on_failed=True)
365
+
366
+ # If std_out_print is not empty remove the previous print
367
+ # Cursor up the number of lines in std_out_print to overwrite/delete the previous print
368
+ if len(std_out_print) > 0:
369
+ sys.stdout.write("\033[F" * len(std_out_print)) # Cursor up the number of lines in std_out_print
370
+ sys.stdout.write("\033[2K") # Clear the line
371
+ sys.stdout.flush() # Flush the stdout buffer
372
+ std_out_print = []
373
+
374
+ # Update the host software upgrade status and print the result
375
+ for host in task:
376
+ # host fping task result
377
+ fping = task[host].result["output"].rstrip()
378
+
379
+ # Initial status -> Host is alive and is installing the software
380
+ if "alive" in fping and "Installing software" in update_status[host]:
381
+ update_status[host] = f"{Fore.YELLOW}Installing software{Fore.RESET}"
382
+ # Second status -> Host is not alive and is rebooting
383
+ if "alive" not in fping and "Installing software" in update_status[host]:
384
+ update_status[host] = f"{Fore.RED}Reboot device{Fore.RESET}"
385
+ if "alive" not in fping and "Rebooting device" in update_status[host]:
386
+ pass
387
+ # Third status -> host is rebooted with new software release
388
+ if "alive" in fping and "Reboot device" in update_status[host]:
389
+ update_status[host] = f"{Fore.GREEN}Upgrade finish{Fore.RESET}"
390
+
391
+ # Add the host software upgrade status result to the std_out_print list
392
+ std_out_print.append(task_host(host=host, changed=False))
393
+ std_out_print.append(f"Status: {update_status[host]} (fping: {fping})")
394
+
395
+ # Print empty line
396
+ std_out_print.append("")
397
+
398
+ # Check if all hosts have upgraded successfull
399
+ if not all(f"{Fore.GREEN}Upgrade finish{Fore.RESET}" in value for value in update_status.values()):
400
+ # Continue the range loop to track to software upgrade status
401
+ total_time = max_refresh * refresh_timer
402
+ std_out_print.append(
403
+ f"{Style.BRIGHT}{Fore.YELLOW}Elapsed waiting time: {elapsed_time}/{total_time}s"
404
+ )
405
+ std_out_print.append(f"{Style.DIM}(The fping task result will refresh in {refresh_timer}s)")
406
+ # Print the loop result
407
+ for line in std_out_print:
408
+ print(line)
409
+ # Wait for the refresh timer to continue the range loop
410
+ elapsed_time += refresh_timer
411
+ time.sleep(refresh_timer)
412
+
413
+ else:
414
+ # Print result and exit the range loop
415
+ std_out_print.append(
416
+ f"{Style.BRIGHT}{Fore.GREEN}Elapsed waiting time: {elapsed_time}/{total_time}s"
417
+ )
418
+ std_out_print.append("Wait 120s to ensure the device NGINX RESTCONF server is ready")
419
+ # Print the loop result
420
+ for line in std_out_print:
421
+ print(line)
422
+ # Sleep for some seconds until the device NGINX RESTCONF server is ready
423
+ time.sleep(120)
424
+ break
425
+
426
+ # If the range loop reached the end -> Software upgrade not successful
427
+ else:
428
+ sys.stdout.write("\033[F") # Cursor up one line
429
+ sys.stdout.write("\033[2K") # Clear the line
430
+ sys.stdout.flush() # Flush the stdout buffer
431
+ print(
432
+ f"{Style.BRIGHT}{Fore.RED}"
433
+ f"Total software upgrade waiting time of {max_refresh * refresh_timer}s exceeded"
434
+ )
435
+
436
+
437
+ def cli_verify_switch_state(nr: Nornir, max_time: int) -> None:
438
+ """
439
+ This function runs the custom Nornir Scrapli task cli_verify_switch_state to get the switch stack state
440
+ of each host.
441
+ """
442
+ # pylint: disable=invalid-name
443
+
444
+ # Printout sleep and refresh values
445
+ std_out_print = []
446
+ refresh_timer = 10
447
+ max_refresh = max_time // refresh_timer # double slash division is a int / single slash would be a float
448
+ elapsed_time = 0
449
+ # Stack reload and HA sync status list (except ready)
450
+ show_switch_status_not_ready = ["Removed", "Initializing", "HA sync in progress", "V-Mismatch"]
451
+
452
+ # Dict to track the host software upgrade status
453
+ update_status = {}
454
+ for host in nr.inventory.hosts:
455
+ update_status[host] = f"{Fore.YELLOW}Not all switches are ready{Fore.RESET}"
456
+
457
+ print(task_name("Verify switch stack state"))
458
+
459
+ for _ in range(max_refresh):
460
+ # Run the standard Scrapli task send_command to get the switch stack state
461
+ task = nr.run(
462
+ task=send_commands,
463
+ commands=["show switch"],
464
+ strip_prompt=False,
465
+ on_failed=True,
466
+ )
467
+ # Close all Nornir connections to ensure a reconnect after the switch reload is possible
468
+ nr.close_connections(on_failed=True)
469
+
470
+ # If std_out_print is not empty remove the previous print
471
+ # Cursor up the number of lines in std_out_print to overwrite/delete the previous print
472
+ if len(std_out_print) > 0:
473
+ sys.stdout.write("\033[F" * len(std_out_print))
474
+ sys.stdout.flush()
475
+ std_out_print = []
476
+
477
+ # Update the host switch stack state and print the result
478
+ for host in task:
479
+ # Check if a reload or stack sync is in progress
480
+ output = str(task[host].result).rstrip()
481
+ if not any(status in output for status in show_switch_status_not_ready):
482
+ update_status[host] = f"{Fore.GREEN}All switches are ready{Fore.RESET}"
483
+
484
+ # Add the host software upgrade status result to the std_out_print list
485
+ std_out_print.append(task_host(host=host, changed=False))
486
+ std_out_print.append(f"Stack State: {update_status[host]}")
487
+
488
+ # Print empty line
489
+ std_out_print.append("")
490
+
491
+ # Check if all hosts have upgraded successfull
492
+ if not all(
493
+ f"{Fore.GREEN}All switches are ready{Fore.RESET}" in value for value in update_status.values()
494
+ ):
495
+ # Continue the range loop to track to software upgrade status
496
+ total_time = max_refresh * refresh_timer
497
+ std_out_print.append(
498
+ f"{Style.BRIGHT}{Fore.YELLOW}Elapsed waiting time: {elapsed_time}/{total_time}s"
499
+ )
500
+ std_out_print.append(f"{Style.DIM}(The task result will refresh in {refresh_timer}s)")
501
+ # Print the loop result
502
+ for line in std_out_print:
503
+ print(line)
504
+ # Wait for the refresh timer to continue the range loop
505
+ elapsed_time += refresh_timer
506
+ time.sleep(refresh_timer)
507
+
508
+ else:
509
+ # Print the loop result
510
+ for line in std_out_print:
511
+ print(line)
512
+ # Return True if all switches in the stack are ready
513
+ return True
514
+
515
+ # If the range loop reached the end -> Not all switches in the stack are ready
516
+ sys.stdout.write("\033[F") # Cursor up one line
517
+ sys.stdout.write("\033[2K") # Clear the line
518
+ sys.stdout.flush() # Flush the stdout buffer
519
+ print(f"{Style.BRIGHT}{Fore.RED}Total waiting time of {max_refresh * refresh_timer}s exceeded\n")
520
+
521
+ # Return False if not all switches in the stack are ready
522
+ return False
523
+
524
+
525
+ #### Nornir Helper Tasks #####################################################################################
526
+
527
+
528
+ def prepare_upgrade_data_task(task: Task, upgrade_type: Literal["http", "scp"]) -> Result:
529
+ """
530
+ This custom Nornir task verifies the source for the software upgrade which can be a http URL or a scp
531
+ filepath. The source md5 hash, the filesize as well as the destination file will be written to the Nornir
532
+ inventory for later usage. The task returns the Nornir Result object.
533
+ """
534
+ upgrade_type = upgrade_type.lower()
535
+
536
+ try:
537
+ desired_version = task.host["software"]["version"]
538
+
539
+ if "http" in upgrade_type:
540
+ http_url = task.host["software"]["http_url"]
541
+ if "filepath" in task.host["software"]:
542
+ source_file = task.host["software"]["filepath"]
543
+ else:
544
+ source_file = task.host["software"]["http_url"]
545
+ elif "scp" in upgrade_type:
546
+ source_file = task.host["software"]["filepath"]
547
+
548
+ except KeyError as error:
549
+ # KeyError exception handles not existing host inventory data keys
550
+ result = f"'Key task.host[{error}] not found' -> NornirResponse: <Success: False>"
551
+ # Return the Nornir result as error
552
+ return Result(host=task.host, result=result, failed=True)
553
+
554
+ # Compute the original md5 hash value
555
+ source_md5 = compute_hash(source=source_file, algorithm="md5")
556
+ # Extract only the filename and prepare the destination path
557
+ dest_file = os.path.basename(source_file)
558
+
559
+ if "http" in upgrade_type:
560
+ # Get the filesize and format to GB
561
+ # Bandit "B310: urllib_urlopen" if solved to raise a ValueError is the value starts not with http
562
+ if http_url.lower().startswith("http"):
563
+ response = urllib.request.Request(http_url, method="HEAD")
564
+ with urllib.request.urlopen(response) as response: # nosec
565
+ # pylint: disable=consider-using-f-string
566
+ file_size = "%.2f" % (int(response.headers["Content-Length"]) / (1024 * 1024 * 1024))
567
+ else:
568
+ raise ValueError from None
569
+
570
+ result = (
571
+ f"'{task.name}' -> OSResponse: <Success: True>\n"
572
+ f"-> Desired version: {desired_version}\n"
573
+ f"-> Source: {http_url}\n"
574
+ f"-> Source MD5-Hash: {source_md5}"
575
+ )
576
+
577
+ elif "scp" in upgrade_type:
578
+ # Verify that the software file exists
579
+ if not os.path.exists(source_file):
580
+ result = f"'File {source_file} not found' -> OSResponse: <Success: False>\n"
581
+ # Return the Nornir result as error
582
+ return Result(host=task.host, result=result, failed=True)
583
+
584
+ # Get the filesize and format to GB
585
+ # pylint: disable=consider-using-f-string
586
+ file_size = "%.2f" % (os.path.getsize(source_file) / (1024 * 1024 * 1024))
587
+
588
+ result = (
589
+ f"'{task.name}' -> OSResponse: <Success: True>\n"
590
+ f"-> Desired version: {desired_version}\n"
591
+ f"-> Source: {source_file}\n"
592
+ f"-> Source MD5-Hash: {source_md5}"
593
+ )
594
+
595
+ # Write the variables into the Nornir inventory
596
+ task.host["software"]["source_md5"] = source_md5
597
+ task.host["software"]["file_size"] = file_size
598
+ task.host["software"]["dest_file"] = dest_file
599
+
600
+ # Return the Nornir result as success
601
+ return Result(host=task.host, result=result)
602
+
603
+
604
+ def scp_upload_software_file_task(task: Task) -> Result:
605
+ """
606
+ This custom Nornir task runs the netmiko_file_transfer task with the source and destination file loaded
607
+ from the Nornir inventory to upload the software file to each host. The task returns the Nornir Result
608
+ object.
609
+ """
610
+
611
+ # Run the standard Nornir task netmiko_file_transfer
612
+ result = task.run(
613
+ task=netmiko_file_transfer,
614
+ source_file=task.host["software"]["filepath"],
615
+ dest_file=task.host["software"]["dest_file"],
616
+ direction="put",
617
+ )
618
+
619
+ # The netmiko_file_transfer result string is either True or False
620
+ result = f"'NETMIKO execute software file upload with SCP' -> SCPResponse <Success: {result.result}>"
621
+
622
+ return Result(host=task.host, result=result)
623
+
624
+
625
+ def cli_http_download_software_file_task(task: Task, verbose: bool = False) -> Result:
626
+ """
627
+ TBD
628
+ """
629
+ # Set the result_summary for a successful task
630
+ result_summary = f"'{task.name}' -> CliResponse <Success: True>"
631
+
632
+ # Get the host source http url and the destination file name from the Nornir inventory
633
+ dest_file = task.host["software"]["dest_file"]
634
+ http_url = task.host["software"]["http_url"]
635
+
636
+ # Manually create Netmiko connection
637
+ net_connect = task.host.get_connection("netmiko", task.nornir.config)
638
+
639
+ # Execute send_multiline to expect and enter the destination file name to start the file copy
640
+ output = net_connect.send_multiline(
641
+ [
642
+ [f"copy {http_url} flash:{dest_file}", r"Destination filename"],
643
+ ["\n", ""],
644
+ ],
645
+ read_timeout=600,
646
+ )
647
+
648
+ if "copied in" in output:
649
+ # Define the result variable for print_result
650
+ result = result_summary + "\n\n" + output if verbose else result_summary
651
+
652
+ # Return the custom Nornir result as success
653
+ return Result(host=task.host, result=result)
654
+
655
+ # Else the copy command failed without traceback exception
656
+ result = f"'{task.name}' -> CliResponse <Success: False>\n\n{output}"
657
+ # Return the Nornir result as failed
658
+ return Result(host=task.host, result=result, failed=True)
659
+
660
+
661
+ def fping_task(task: Task) -> Result:
662
+ """
663
+ This custom Nornir task runs the linux command fping to the host IP-address. The returned result is a
664
+ dictionary with the fping output and the retruncode.
665
+ """
666
+
667
+ # fmt: off
668
+ fping = subprocess.run( # nosec
669
+ ["fping", "-A", "-d", task.host.hostname,], check=False, capture_output=True
670
+ )
671
+ # fmt: on
672
+
673
+ result = {"returncode": fping.returncode, "output": fping.stdout.decode("utf-8")}
674
+
675
+ return Result(host=task.host, result=result)
676
+
677
+
678
+ def verify_issu_requirements_task(task: Task) -> Result:
679
+ """
680
+ TBD
681
+ """
682
+ # Get the current and the desired version from the Nornir inventory and slice the release to have
683
+ # only the first two characters of the version number
684
+ current_version = task.host["software"]["current_version"][:2]
685
+ desired_version = task.host["software"]["version"][:2]
686
+
687
+ # Verify that the desired version is greater or equal than the current version
688
+ if int(desired_version) >= int(current_version):
689
+ result = f"'{task.name}' -> NornirResponse <Success: True>\n" f"-> ISSU upgrade is supported"
690
+ # Return the Nornir result as success
691
+ return Result(host=task.host, result=result)
692
+
693
+ result = f"'{task.name}' -> NornirResponse <Success: False>\n" f"-> ISSU downgrade is not supported"
694
+ # Return the Nornir result as failed
695
+ return Result(host=task.host, result=result, failed=True)
696
+
697
+
698
+ #### Nornir Helper tasks in regular Function #################################################################
699
+
700
+
701
+ def prepare_upgrade_data(nr: Nornir, upgrade_type: Literal["http", "scp"]) -> bool:
702
+ """
703
+ This function runs the custom Nornir task prepare_upgrade_data_task to verify the source for the software
704
+ upgrade which can be a http URL or a scp filepath. The source md5 hash, the filesize as well as the
705
+ destination file will be written to the Nornir inventory for later usage. The Nornir task result will be
706
+ printed with print_result. In case of a source verification error a error message will be printed and the
707
+ script terminates. The function return False if the task failed or True if the task was successful.
708
+ """
709
+ # pylint: disable=invalid-name
710
+
711
+ # Run the custom Nornir task prepare_upgrade_data_task
712
+ task_result = nr.run(
713
+ task=prepare_upgrade_data_task,
714
+ name="NORNIR prepare upgrade data",
715
+ upgrade_type=upgrade_type,
716
+ on_failed=True,
717
+ )
718
+
719
+ # Print the Nornir task result
720
+ print_result(task_result)
721
+
722
+ # Return False if the task failed or True if the task was successful
723
+ return not bool(task_result.failed)
724
+
725
+
726
+ def scp_upload_software_file(nr: Nornir) -> None:
727
+ """
728
+ TBD
729
+ """
730
+ # pylint: disable=invalid-name
731
+
732
+ print_task_name("NETMIKO prepare software file upload with SCP")
733
+ # Print some info for each host
734
+ for host in nr.inventory.hosts:
735
+ dest_file = nr.inventory.hosts[host]["software"]["dest_file"]
736
+ file_size = nr.inventory.hosts[host]["software"]["file_size"]
737
+ print(task_host(host=host, changed=False))
738
+ print("'NETMIKO prepare software file upload with SCP' -> SCPResponse <Success: True>")
739
+ print(f"-> SCP copy {dest_file} ({file_size} GB) to flash:")
740
+
741
+ print("")
742
+ # Run the Nornir task scp_upload_software_file_task with a spinner
743
+ spinner_text = f"{Style.BRIGHT}{Fore.YELLOW}NETMIKO execute software file upload with SCP in progress ..."
744
+ with yaspin(Spinners.moon, text=spinner_text, side="right"):
745
+ task_result = nr.run(
746
+ task=scp_upload_software_file_task,
747
+ name="NETMIKO execute software file upload with SCP",
748
+ on_failed=True,
749
+ )
750
+ # Cursor up one line to overwrite/delete the spinner line
751
+ sys.stdout.write("\033[F")
752
+
753
+ print_result(task_result)
754
+
755
+ # Return False if the task failed or True if the task was successful
756
+ return not bool(task_result.failed)
757
+
758
+
759
+ def cli_http_download_software_file(nr: Nornir, verbose: bool = False) -> bool:
760
+ """
761
+ TBD
762
+ """
763
+ # pylint: disable=invalid-name
764
+
765
+ print_task_name("NETMIKO prepare software file download with HTTP")
766
+ # Print some info for each host
767
+ for host in nr.inventory.hosts:
768
+ http_url = nr.inventory.hosts[host]["software"]["http_url"]
769
+ file_size = nr.inventory.hosts[host]["software"]["file_size"]
770
+ print(task_host(host=host, changed=False))
771
+ print(task_info(text="NETMIKO prepare software file download with HTTP", changed=False))
772
+ print("'NETMIKO prepare software file download with HTTP' -> SCPResponse <Success: True>")
773
+ print(f"-> HTTP copy {http_url} ({file_size} GB) to flash:")
774
+
775
+ print("")
776
+ # Run the Nornir task cli_http_download_software_file_task with a spinner
777
+ spinner_text = (
778
+ f"{Style.BRIGHT}{Fore.YELLOW}NETMIKO execute software file download with HTTP in progress ..."
779
+ )
780
+ with yaspin(Spinners.moon, text=spinner_text, side="right"):
781
+ task_result = nr.run(
782
+ task=cli_http_download_software_file_task,
783
+ name="NETMIKO execute software file download with HTTP",
784
+ verbose=verbose,
785
+ on_failed=True,
786
+ )
787
+ # Cursor up one line to overwrite/delete the spinner line
788
+ sys.stdout.write("\033[F")
789
+
790
+ # Print the Nornir task result
791
+ print_result(task_result)
792
+
793
+ # Return False if the task failed or True if the task was successful
794
+ return not bool(task_result.failed)