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,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)
|