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,727 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
This module contains functions to get data from the Cisco Support APIs.
|
4
|
+
|
5
|
+
The functions are ordered as followed:
|
6
|
+
- Cisco Support API call functions
|
7
|
+
- Print functions for Cisco Support API call functions in Nornir style
|
8
|
+
"""
|
9
|
+
|
10
|
+
|
11
|
+
import json
|
12
|
+
import time
|
13
|
+
from typing import Any, Literal, Union
|
14
|
+
import requests
|
15
|
+
from nornir_collection.cisco.support_api.api_calls import get_cisco_support_token, SNI, EOX, SS
|
16
|
+
from nornir_collection.utils import task_name, task_host, task_info, task_error, iterate_all, exit_error
|
17
|
+
|
18
|
+
|
19
|
+
#### Cisco Support API Error Lists ##########################################################################
|
20
|
+
|
21
|
+
|
22
|
+
# fmt: off
|
23
|
+
SNI_ERRORS = [
|
24
|
+
"EXCEEDED_OUTPUT", "API_MISSING_PARAMETERS", "API_INVALID_INPUT", "EXCEEDED_INPUTS",
|
25
|
+
"API_NOTAUTHORIZED", "API_ERROR_01",
|
26
|
+
]
|
27
|
+
EOX_ERRORS = [
|
28
|
+
"SSA_GENERIC_ERR", "SSA_ERR_001", "SSA_ERR_003", "SSA_ERR_007", "SSA_ERR_009", "SSA_ERR_010",
|
29
|
+
"SSA_ERR_011", "SSA_ERR_012", "SSA_ERR_013", "SSA_ERR_014", "SSA_ERR_015", "SSA_ERR_016", "SSA_ERR_018",
|
30
|
+
"SSA_ERR_022", "SSA_ERR_023", "SSA_ERR_024", "SSA_ERR_028", "SSA_ERR_030", "SSA_ERR_031", "SSA_ERR_032",
|
31
|
+
"SSA_ERR_033", "SSA_ERR_034", "SSA_ERR_036", "SSA_ERR_037",
|
32
|
+
]
|
33
|
+
SS_ERRORS = [
|
34
|
+
"S3_BASEPID_NO_SUPPORT", "S3_BASEPID_REQ", "S3_HW_INFORMATION_NOT_SUPPORTED", "S3_INV_BASEPID",
|
35
|
+
"S3_INV_BASEPID", "S3_INV_CURR_IMG_REL", "S3_INV_IMAGE", "S3_INV_INPUT", "S3_INV_MDFID", "S3_INV_MDFID",
|
36
|
+
"S3_INV_QUERY_PARAM", "S3_INV_QUERY_PARAM", "S3_INV_QUERY_PARAM", "S3_INV_QUERY_PARAM",
|
37
|
+
"S3_INV_QUERY_PARAM", "S3_INV_QUERY_PARAM", "S3_INV_RELEASE", "S3_INV_RELEASE_IMAGE",
|
38
|
+
"S3_MDFID_NO_SUPPORT", "S3_MDFID_REQ", "S3_NO_SOFT_AVL",
|
39
|
+
"S3_SERVICE_EXCEPTION_OCCURED",
|
40
|
+
]
|
41
|
+
# fmt: on
|
42
|
+
|
43
|
+
|
44
|
+
#### Helper Functions #######################################################################################
|
45
|
+
|
46
|
+
|
47
|
+
def _success(value: bool) -> Literal["CISCOAPIResult <Success: True>", "CISCOAPIResult <Success: False>"]:
|
48
|
+
"""
|
49
|
+
TBD
|
50
|
+
"""
|
51
|
+
if value:
|
52
|
+
return "CISCOAPIResult <Success: True>"
|
53
|
+
return "CISCOAPIResult <Success: False>"
|
54
|
+
|
55
|
+
|
56
|
+
#### Cisco Support API Call Helper Functions ################################################################
|
57
|
+
|
58
|
+
|
59
|
+
def _get_total_num_pages(**kwargs) -> int:
|
60
|
+
"""
|
61
|
+
Helper function to get the total number of pages for the Cisco Support API call
|
62
|
+
"""
|
63
|
+
# Cisco Support API Object and API name as string
|
64
|
+
api_obj = kwargs["api_obj"]
|
65
|
+
api = kwargs["api_string"]
|
66
|
+
chunk = kwargs["chunk"]
|
67
|
+
sleep = kwargs["SLEEP"]
|
68
|
+
|
69
|
+
# Get the total number of pages for the chunk list
|
70
|
+
# Re-try the Cisco support API call with a backoff again in case of an error
|
71
|
+
for _ in range(kwargs["RETRY_ATTEMPTS"]):
|
72
|
+
# Call the Cisco support API for the chunk list to get the total number of pages
|
73
|
+
# Use mapping with lambda to avoid long if elif else statements
|
74
|
+
api_calls = {
|
75
|
+
"sni": lambda: api_obj.getCoverageSummaryBySerialNumbers(sr_no=chunk, page_index=1),
|
76
|
+
"eox": lambda: api_obj.getBySerialNumbers(serialNumber=chunk, pageIndex=1),
|
77
|
+
"ss": lambda: api_obj.getSuggestedReleasesByProductIDs(productIds=chunk, pageIndex=1),
|
78
|
+
}
|
79
|
+
|
80
|
+
# Execute the correct lambda API call by the dictionary key which matches to api.lower()
|
81
|
+
try:
|
82
|
+
response = api_calls[api.lower()]()
|
83
|
+
except requests.exceptions.JSONDecodeError:
|
84
|
+
continue
|
85
|
+
|
86
|
+
# Use mapping to avoid long if elif else statements
|
87
|
+
keys = {
|
88
|
+
"sni": ("pagination_response_record", "last_index"),
|
89
|
+
"eox": ("PaginationResponseRecord", "LastIndex"),
|
90
|
+
"ss": ("paginationResponseRecord", "lastIndex"),
|
91
|
+
}
|
92
|
+
|
93
|
+
# If the pagination details are present
|
94
|
+
# Select the correct API response keys by the dictionary key which matches to api.lower()
|
95
|
+
if keys[api.lower()][0] in response and response[keys[api.lower()][0]] is not None:
|
96
|
+
# Return the total number of pages to create API calls for all pages
|
97
|
+
return response[keys[api.lower()][0]][keys[api.lower()][1]]
|
98
|
+
|
99
|
+
# SLEEP and continue with next range() loop attempt
|
100
|
+
time.sleep(sleep)
|
101
|
+
sleep = sleep * kwargs["SLEEP_MULTIPLIER"]
|
102
|
+
|
103
|
+
# Ending for loop as iterable exhausted
|
104
|
+
try:
|
105
|
+
return verify_cisco_support_api_data(
|
106
|
+
api=api.lower(), iterable=response, force_failed=True, verbose=True
|
107
|
+
)
|
108
|
+
except UnboundLocalError:
|
109
|
+
return verify_cisco_support_api_data(api=api.lower(), iterable=None, force_failed=True, verbose=True)
|
110
|
+
|
111
|
+
|
112
|
+
def _get_data_all_pages(**kwargs) -> list[dict]:
|
113
|
+
"""
|
114
|
+
TBD
|
115
|
+
"""
|
116
|
+
# pylint: disable=cell-var-from-loop
|
117
|
+
|
118
|
+
# Cisco Support API Object and API name as string
|
119
|
+
api_obj = kwargs["api_obj"]
|
120
|
+
api = kwargs["api_string"]
|
121
|
+
api_sub = kwargs["api_string_sub"] if "api_string_sub" in kwargs else api
|
122
|
+
chunk = kwargs["chunk"]
|
123
|
+
sleep = kwargs["SLEEP"]
|
124
|
+
|
125
|
+
# Create a list to fill with the API response chunks
|
126
|
+
api_data = []
|
127
|
+
|
128
|
+
# Get the API data for each page of the chunk list
|
129
|
+
# Call the Cisco support API for each page of the chunk list
|
130
|
+
for page in range(1, int(kwargs["num_pages"]) + 1):
|
131
|
+
# Re-try the Cisco support API call with a backoff again in case of an error
|
132
|
+
for _ in range(kwargs["RETRY_ATTEMPTS"]):
|
133
|
+
# Call the Cisco support API for the chunk list
|
134
|
+
# Use mapping with lambda to avoid long if elif else statements
|
135
|
+
api_calls = {
|
136
|
+
"sni_owner": lambda: api_obj.getOwnerCoverageStatusBySerialNumbers(sr_no=chunk),
|
137
|
+
"sni_summary": lambda: api_obj.getCoverageSummaryBySerialNumbers(
|
138
|
+
sr_no=chunk, page_index=page
|
139
|
+
),
|
140
|
+
"eox": lambda: api_obj.getBySerialNumbers(serialNumber=chunk, pageIndex=page),
|
141
|
+
"ss": lambda: api_obj.getSuggestedReleasesByProductIDs(productIds=chunk, pageIndex=page),
|
142
|
+
}
|
143
|
+
try:
|
144
|
+
# Execute the correct lambda API call by the dictionary key which matches to api_sub.lower()
|
145
|
+
response = api_calls[api_sub.lower()]()
|
146
|
+
except requests.exceptions.JSONDecodeError:
|
147
|
+
continue
|
148
|
+
|
149
|
+
# Use mapping to avoid long if elif else statements
|
150
|
+
keys = {
|
151
|
+
"sni_owner": ("serial_numbers", "serial_numbers"),
|
152
|
+
"sni_summary": ("pagination_response_record", "serial_numbers"),
|
153
|
+
"eox": ("PaginationResponseRecord", "EOXRecord"),
|
154
|
+
"ss": ("paginationResponseRecord", "productList"),
|
155
|
+
}
|
156
|
+
|
157
|
+
# If the pagination details are present
|
158
|
+
# Select the correct API response keys by the dictionary key which matches to api_sub.lower()
|
159
|
+
if keys[api_sub.lower()][0] in response and response[keys[api_sub.lower()][0]] is not None:
|
160
|
+
# Update the api_data list
|
161
|
+
for item in response[keys[api_sub.lower()][1]]:
|
162
|
+
api_data.append(item)
|
163
|
+
|
164
|
+
# Check if the initial list and the response list have the same length. This verifies that
|
165
|
+
# there is a response for each pid of the initial list
|
166
|
+
if api_sub.lower() == "ss":
|
167
|
+
# Create a set without duplicated with set comprehension
|
168
|
+
chunk_response = list({item["product"]["basePID"] for item in response["productList"]})
|
169
|
+
# Exit the script if the length of both pid lists are not identical
|
170
|
+
if len(chunk) != len(chunk_response):
|
171
|
+
invalid_pid = [item for item in chunk if item not in chunk_response]
|
172
|
+
verify_cisco_support_api_data(
|
173
|
+
api=api.lower(),
|
174
|
+
iterable=response,
|
175
|
+
force_failed=True,
|
176
|
+
verbose=True,
|
177
|
+
add_info=f"-> Invalid PIDs: {invalid_pid}",
|
178
|
+
)
|
179
|
+
|
180
|
+
# Break out of the for loop and continue with the next page
|
181
|
+
break
|
182
|
+
|
183
|
+
# SLEEP and continue with next range() loop attempt
|
184
|
+
time.sleep(sleep)
|
185
|
+
sleep = sleep * kwargs["SLEEP_MULTIPLIER"]
|
186
|
+
|
187
|
+
else: # no break
|
188
|
+
# Ending for loop as iterable exhausted
|
189
|
+
return verify_cisco_support_api_data(
|
190
|
+
api=api.lower(), iterable=response, force_failed=True, verbose=True
|
191
|
+
)
|
192
|
+
|
193
|
+
# Return the API data list
|
194
|
+
return api_data
|
195
|
+
|
196
|
+
|
197
|
+
#### Cisco Support API Response Verification #################################################################
|
198
|
+
|
199
|
+
|
200
|
+
def verify_cisco_support_api_data(
|
201
|
+
api: Literal["sni", "eox", "ss"],
|
202
|
+
iterable: Union[dict, None],
|
203
|
+
force_failed: bool = False,
|
204
|
+
verbose: bool = False,
|
205
|
+
add_info: Any = False,
|
206
|
+
) -> bool:
|
207
|
+
"""
|
208
|
+
This function verifies the serials_dict which has been filled with data by various functions of these
|
209
|
+
module like eox_by_serial_numbers, sni_get_coverage_summary_by_serial_numbers, etc. and verifies that
|
210
|
+
there are no invalid serial numbers. In case of invalid serial numbers, the script quits with an error
|
211
|
+
message.
|
212
|
+
"""
|
213
|
+
# pylint: disable=too-many-branches
|
214
|
+
|
215
|
+
if not force_failed:
|
216
|
+
# Check if any value of the iterable is inside the API error lists
|
217
|
+
no_error = True
|
218
|
+
for value in iterate_all(iterable=iterable, returned="value"):
|
219
|
+
if value is None:
|
220
|
+
continue
|
221
|
+
if any(value == error for error in (SNI_ERRORS + EOX_ERRORS + SS_ERRORS)):
|
222
|
+
no_error = False
|
223
|
+
break
|
224
|
+
|
225
|
+
if no_error:
|
226
|
+
print(task_name(text=f"Verify Cisco support {api.upper()} API data"))
|
227
|
+
print(task_info(text=f"Verify Cisco support {api.upper()} API data", changed=False))
|
228
|
+
print(f"'Verify Cisco support {api.upper()} API data' -> {_success(True)}")
|
229
|
+
|
230
|
+
return True
|
231
|
+
|
232
|
+
error = (
|
233
|
+
f"{task_name(text=f'Verify Cisco support {api.upper()} API data')}\n"
|
234
|
+
f"{task_error(text=f'Verify Cisco support {api.upper()} API data', changed=False)}\n"
|
235
|
+
f"'Verify Cisco support {api.upper()} API data' -> {_success(False)}"
|
236
|
+
)
|
237
|
+
|
238
|
+
if isinstance(iterable, dict):
|
239
|
+
# Print additional information depending which Cisco support API has been used
|
240
|
+
for value in iterate_all(iterable=iterable, returned="value"):
|
241
|
+
if value is None:
|
242
|
+
continue
|
243
|
+
if api.lower() in "sni" and any(value == error for error in SNI_ERRORS):
|
244
|
+
print(error)
|
245
|
+
print(f"-> {api.upper()} API-Error: {value}")
|
246
|
+
if verbose:
|
247
|
+
print("\n" + json.dumps(iterable, indent=4))
|
248
|
+
if add_info:
|
249
|
+
print(add_info)
|
250
|
+
break
|
251
|
+
if api.lower() in "eox" and any(value == error for error in EOX_ERRORS):
|
252
|
+
print(error)
|
253
|
+
if "ErrorResponse" in iterable:
|
254
|
+
print(f"-> {api.upper()} API-Error: {value}")
|
255
|
+
if verbose:
|
256
|
+
print("\n" + json.dumps(iterable, indent=4))
|
257
|
+
if add_info:
|
258
|
+
print(add_info)
|
259
|
+
else:
|
260
|
+
print("-> The EOX API returned None. This could be a bug in the API.")
|
261
|
+
if add_info:
|
262
|
+
print("\n" + json.dumps(add_info, indent=4))
|
263
|
+
break
|
264
|
+
if api.lower() in "ss":
|
265
|
+
print(error)
|
266
|
+
if any(value == error for error in SS_ERRORS):
|
267
|
+
print(f"-> {api.upper()} API-Error: {value}")
|
268
|
+
if verbose:
|
269
|
+
print("\n" + json.dumps(iterable, indent=4))
|
270
|
+
if add_info:
|
271
|
+
print(add_info)
|
272
|
+
else:
|
273
|
+
print(
|
274
|
+
"-> The initial PID list contains invalid PIDs. The returned list is not identical."
|
275
|
+
)
|
276
|
+
if add_info:
|
277
|
+
print(add_info)
|
278
|
+
break
|
279
|
+
if api.lower() not in ["sni", "eox", "ss"]:
|
280
|
+
print(error)
|
281
|
+
print(f"-> Unknown API: {api.upper()}")
|
282
|
+
break
|
283
|
+
else:
|
284
|
+
# The Cisco support API response is invalid
|
285
|
+
print(error)
|
286
|
+
print("-> The API response is invalid. This could be a bug in the API.")
|
287
|
+
print(add_info)
|
288
|
+
|
289
|
+
# Exit the script with a proper message
|
290
|
+
exit_error(
|
291
|
+
task_text=f"CISCO-API get {api.upper()} data",
|
292
|
+
text="ALERT: GET CISCO SUPPORT API DATA FAILED!",
|
293
|
+
msg="-> Analyse the error message and identify the root cause",
|
294
|
+
)
|
295
|
+
|
296
|
+
|
297
|
+
#### Cisco Support API call functions ########################################################################
|
298
|
+
|
299
|
+
|
300
|
+
def cisco_support_check_authentication(api_creds: tuple, verbose: bool = False, silent: bool = False) -> bool:
|
301
|
+
"""
|
302
|
+
This function checks to Cisco support API authentication by generating an bearer access token. In case
|
303
|
+
of an invalid API client key or secret a error message is printed and the script exits.
|
304
|
+
"""
|
305
|
+
task_text = "CISCO-API check OAuth2 client credentials grant flow"
|
306
|
+
|
307
|
+
try:
|
308
|
+
# Try to generate an barer access token
|
309
|
+
token = get_cisco_support_token(*api_creds, verify=None, proxies=None)
|
310
|
+
|
311
|
+
if not silent:
|
312
|
+
print(task_name(text=task_text))
|
313
|
+
print(task_info(text=task_text, changed=False))
|
314
|
+
print(f"'Bearer access token generation' -> {_success(True)}")
|
315
|
+
if verbose:
|
316
|
+
print(f"-> Bearer token: {token}")
|
317
|
+
|
318
|
+
return True
|
319
|
+
|
320
|
+
except KeyError:
|
321
|
+
print(task_name(text=task_text))
|
322
|
+
print(task_error(text=task_text, changed=False))
|
323
|
+
print(f"'Bearer access token generation' -> {_success(False)}")
|
324
|
+
print("-> Invalid API client key and/or secret provided")
|
325
|
+
|
326
|
+
return False
|
327
|
+
|
328
|
+
|
329
|
+
def get_sni_owner_coverage_by_serial_number(serial_dict: dict, api_creds: tuple) -> dict:
|
330
|
+
"""
|
331
|
+
This function takes the serial_dict which contains all serial numbers and the Cisco support API creds to
|
332
|
+
get the owner coverage by serial number with the cisco-support library. The result of each serial will
|
333
|
+
be added with a new key to the dict. The function returns the updated serials dict. The format of the
|
334
|
+
serials_dict need to be as below.
|
335
|
+
"<serial>": {
|
336
|
+
"host": "<hostname>",
|
337
|
+
...
|
338
|
+
},
|
339
|
+
"""
|
340
|
+
# pylint: disable=invalid-name
|
341
|
+
|
342
|
+
# Maximum serial number API parameter value
|
343
|
+
MAX_SR_NO = 75
|
344
|
+
|
345
|
+
# Create the Cisco support API object
|
346
|
+
sni = SNI(*api_creds)
|
347
|
+
|
348
|
+
sni_vars = {
|
349
|
+
# Backoff sleep and attempt values
|
350
|
+
"RETRY_ATTEMPTS": 20,
|
351
|
+
"SLEEP": 1,
|
352
|
+
"SLEEP_MULTIPLIER": 1,
|
353
|
+
# Cisco Support API Object and API name as string
|
354
|
+
"api_obj": sni,
|
355
|
+
"api_string": "sni",
|
356
|
+
"api_string_sub": "sni_owner",
|
357
|
+
# The SNI coverage owner API have only one page
|
358
|
+
"num_pages": 1,
|
359
|
+
}
|
360
|
+
|
361
|
+
# API calls to the Cisco Support API coverage owner by serial number
|
362
|
+
# Loop over a list with all serial numbers with a step incrementation of MAX_ITEM
|
363
|
+
for index in range(0, len(list(serial_dict.keys())), MAX_SR_NO):
|
364
|
+
# Create a chunk list with the maximum allowed elements specified by MAX_ITEM
|
365
|
+
sni_vars["chunk"] = list(serial_dict.keys())[index : index + MAX_SR_NO]
|
366
|
+
# Get the API data for each page of the chunk list
|
367
|
+
api_data = _get_data_all_pages(**sni_vars)
|
368
|
+
|
369
|
+
# Add all records to the serial_dict dictionary
|
370
|
+
for record in api_data:
|
371
|
+
serial_dict[record["sr_no"]]["SNIgetOwnerCoverageStatusBySerialNumbers"] = record
|
372
|
+
|
373
|
+
return serial_dict
|
374
|
+
|
375
|
+
|
376
|
+
def get_sni_coverage_summary_by_serial_numbers(serial_dict: dict, api_creds: tuple) -> dict:
|
377
|
+
"""
|
378
|
+
This function takes the serial_dict which contains all serial numbers and the Cisco support API creds to
|
379
|
+
get the coverage summary by serial number with the cisco-support library. The result of each serial will
|
380
|
+
be added with a new key to the dict. The function returns the updated serials dict. The format of the
|
381
|
+
serials_dict need to be as below.
|
382
|
+
"<serial>": {
|
383
|
+
"host": "<hostname>",
|
384
|
+
...
|
385
|
+
},
|
386
|
+
"""
|
387
|
+
# pylint: disable=invalid-name
|
388
|
+
|
389
|
+
# Maximum serial number API parameter value
|
390
|
+
MAX_SR_NO = 75
|
391
|
+
|
392
|
+
# Create the Cisco support API object
|
393
|
+
sni = SNI(*api_creds)
|
394
|
+
|
395
|
+
sni_vars = {
|
396
|
+
# Backoff sleep and attempt values
|
397
|
+
"RETRY_ATTEMPTS": 20,
|
398
|
+
"SLEEP": 1,
|
399
|
+
"SLEEP_MULTIPLIER": 1,
|
400
|
+
# Cisco Support API Object and API name as string
|
401
|
+
"api_obj": sni,
|
402
|
+
"api_string": "sni",
|
403
|
+
"api_string_sub": "sni_summary",
|
404
|
+
}
|
405
|
+
|
406
|
+
# API calls to the Cisco Support API coverage summary by serial number
|
407
|
+
# Loop over a list with all serial numbers with a step incrementation of MAX_ITEM
|
408
|
+
for index in range(0, len(list(serial_dict.keys())), MAX_SR_NO):
|
409
|
+
# Create a chunk list with the maximum allowed elements specified by MAX_ITEM
|
410
|
+
sni_vars["chunk"] = list(serial_dict.keys())[index : index + MAX_SR_NO]
|
411
|
+
# Part 1: Get the total number of pages for the API call of this index
|
412
|
+
sni_vars["num_pages"] = _get_total_num_pages(**sni_vars)
|
413
|
+
# Part 2: Get the API data for each page of the chunk list
|
414
|
+
api_data = _get_data_all_pages(**sni_vars)
|
415
|
+
|
416
|
+
# Add all records to the serial_dict dictionary
|
417
|
+
for record in api_data:
|
418
|
+
sr_no = record["sr_no"]
|
419
|
+
serial_dict[sr_no]["SNIgetCoverageSummaryBySerialNumbers"] = record
|
420
|
+
|
421
|
+
return serial_dict
|
422
|
+
|
423
|
+
|
424
|
+
def get_eox_by_serial_numbers(serial_dict: dict, api_creds: tuple) -> dict:
|
425
|
+
"""
|
426
|
+
This function takes the serial_dict which contains all serial numbers and the Cisco support API creds to
|
427
|
+
run get the end of life data by serial number with the cisco-support library. The result of each serial
|
428
|
+
will be added with a new key to the dict. The function returns the updated serials dict. The format of
|
429
|
+
the serials_dict need to be as below.
|
430
|
+
"<serial>": {
|
431
|
+
"host": "<hostname>",
|
432
|
+
...
|
433
|
+
},
|
434
|
+
"""
|
435
|
+
# pylint: disable=invalid-name
|
436
|
+
|
437
|
+
# Maximum serial number API parameter value
|
438
|
+
MAX_SR_NO = 20
|
439
|
+
|
440
|
+
# Create the Cisco support API objec
|
441
|
+
eox = EOX(*api_creds)
|
442
|
+
|
443
|
+
eox_vars = {
|
444
|
+
# Backoff sleep and attempt values
|
445
|
+
"RETRY_ATTEMPTS": 20,
|
446
|
+
"SLEEP": 1,
|
447
|
+
"SLEEP_MULTIPLIER": 1,
|
448
|
+
# Cisco Support API Object and API name as string
|
449
|
+
"api_obj": eox,
|
450
|
+
"api_string": "eox",
|
451
|
+
}
|
452
|
+
|
453
|
+
# API calls to the Cisco Support API end of life by serial number
|
454
|
+
# Loop over a list with all serial numbers with a step incrementation of MAX_ITEM
|
455
|
+
for index in range(0, len(list(serial_dict.keys())), MAX_SR_NO):
|
456
|
+
# Create a chunk list with the maximum allowed elements specified by MAX_ITEM
|
457
|
+
eox_vars["chunk"] = list(serial_dict.keys())[index : index + MAX_SR_NO]
|
458
|
+
# Part 1: Get the total number of pages for the API call of this index
|
459
|
+
eox_vars["num_pages"] = _get_total_num_pages(**eox_vars)
|
460
|
+
# Part 2: Get the API data for each page of the chunk list
|
461
|
+
api_data = _get_data_all_pages(**eox_vars)
|
462
|
+
|
463
|
+
for record in api_data:
|
464
|
+
# The response value of "EOXInputValue" can be a single serial number or a comma separated
|
465
|
+
# string of serial numbers as the API response can collect multiple same EoX response together
|
466
|
+
for sr_no in record["EOXInputValue"].split(","):
|
467
|
+
serial_dict[sr_no]["EOXgetBySerialNumbers"] = record
|
468
|
+
|
469
|
+
return serial_dict
|
470
|
+
|
471
|
+
|
472
|
+
def get_ss_suggested_release_by_pid(serial_dict: dict, api_creds: tuple, pid_list: list = False) -> dict:
|
473
|
+
"""
|
474
|
+
This function takes the serial_dict which contains all serial numbers and the Cisco support API creds to
|
475
|
+
get the suggested software release by the PID with the cisco-support library. The result of each serial
|
476
|
+
will be added with a new key to the dict. The function returns the updated serials dict. The format of
|
477
|
+
the serials_dict need to be as below.
|
478
|
+
"<serial>": {
|
479
|
+
"host": "<hostname>",
|
480
|
+
...
|
481
|
+
},
|
482
|
+
"""
|
483
|
+
# pylint: disable=invalid-name
|
484
|
+
|
485
|
+
# Maximum PID API parameter value
|
486
|
+
MAX_PID = 10
|
487
|
+
|
488
|
+
# Create the Cisco support API objec
|
489
|
+
ss = SS(*api_creds)
|
490
|
+
|
491
|
+
ss_vars = {
|
492
|
+
# Backoff sleep and attempt values
|
493
|
+
"RETRY_ATTEMPTS": 20,
|
494
|
+
"SLEEP": 1,
|
495
|
+
"SLEEP_MULTIPLIER": 1,
|
496
|
+
# Cisco Support API Object and API name as string
|
497
|
+
"api_obj": ss,
|
498
|
+
"api_string": "ss",
|
499
|
+
}
|
500
|
+
|
501
|
+
# Create a list with the PIDs of all devices from the serials dict if the argument pid_list if False
|
502
|
+
if not pid_list:
|
503
|
+
pid_list = [
|
504
|
+
item[1]
|
505
|
+
for item in iterate_all(iterable=serial_dict, returned="key-value")
|
506
|
+
if item[0] == "base_pid" or item[0] == "orderable_pid"
|
507
|
+
]
|
508
|
+
# Remove pids if the match the condition for startswith
|
509
|
+
rm_prefixes = ["UCSC-C220-M5SX", "AIR-CAP"]
|
510
|
+
pid_list = [pid for pid in pid_list if not any(pid.startswith(prefix) for prefix in rm_prefixes)]
|
511
|
+
# Remove pids if the match the condition for endswith
|
512
|
+
rm_suffixes = ["AXI-E", "AXI-A"]
|
513
|
+
pid_list = [pid for pid in pid_list if not any(pid.endswith(suffix) for suffix in rm_suffixes)]
|
514
|
+
# Modify known wrong basePIDs to match API requirements
|
515
|
+
# The software package suffic -A or -E can be removed as the newer basePID don't have this anymore
|
516
|
+
# -> Makes the API calls more stable
|
517
|
+
chg_suffixes = ["-A", "-E"]
|
518
|
+
pid_list = [pid[:-2] if any(pid.endswith(suffix) for suffix in chg_suffixes) else pid for pid in pid_list]
|
519
|
+
# Remove duplicated and empty pids in the final pid_list
|
520
|
+
pid_list = [pid for pid in list(set(pid_list)) if pid]
|
521
|
+
|
522
|
+
# API calls to the Cisco Support API suggested release by PID
|
523
|
+
# Loop over a list with all serial numbers with a step incrementation of MAX_ITEM
|
524
|
+
api_data = []
|
525
|
+
for index in range(0, len(list(pid_list)), MAX_PID):
|
526
|
+
# Create a chunk list with the maximum allowed elements specified by MAX_ITEM
|
527
|
+
ss_vars["chunk"] = list(pid_list)[index : index + MAX_PID]
|
528
|
+
# Part 1: Get the total number of pages for the API call of this index
|
529
|
+
ss_vars["num_pages"] = _get_total_num_pages(**ss_vars)
|
530
|
+
# Part 2: Get the API data for each page of the chunk list
|
531
|
+
api_data.extend(_get_data_all_pages(**ss_vars))
|
532
|
+
|
533
|
+
# Add the suggested software responce for each device to the serial_dict dictionary
|
534
|
+
for record in serial_dict.values():
|
535
|
+
# Create a list with the product ids of all devices from the serials dict
|
536
|
+
for item in iterate_all(iterable=record, returned="key-value"):
|
537
|
+
# Skip empty PID values
|
538
|
+
if not item[1]:
|
539
|
+
continue
|
540
|
+
if item[0] == "orderable_pid":
|
541
|
+
pid = item[1]
|
542
|
+
elif item[0] == "base_pid":
|
543
|
+
pid = item[1]
|
544
|
+
|
545
|
+
# Each pid can have multiple suggestion records as the mdfId can be different for the same release
|
546
|
+
# Therefor a list will be created to fill with multiple dictionaries if needed
|
547
|
+
record["SSgetSuggestedReleasesByProductIDs"] = []
|
548
|
+
|
549
|
+
# Loop over the whole suggested software response and add the suggested releases
|
550
|
+
for item in api_data:
|
551
|
+
# If the pid match add the suggestion to the list
|
552
|
+
if item["product"]["basePID"] in pid:
|
553
|
+
record["SSgetSuggestedReleasesByProductIDs"].append(item)
|
554
|
+
|
555
|
+
# If no suggested releases were added
|
556
|
+
if not record["SSgetSuggestedReleasesByProductIDs"]:
|
557
|
+
record["SSgetSuggestedReleasesByProductIDs"].append("PID without Cisco software")
|
558
|
+
|
559
|
+
return serial_dict
|
560
|
+
|
561
|
+
|
562
|
+
#### Print functions for Cisco Support API call functions in Nornir style ###################################
|
563
|
+
|
564
|
+
|
565
|
+
def print_sni_owner_coverage_by_serial_number(serial_dict: dict, verbose: bool = False) -> None:
|
566
|
+
"""
|
567
|
+
This function prints the result of get_sni_owner_coverage_by_serial_number() in Nornir style to stdout.
|
568
|
+
"""
|
569
|
+
task_text = "CISCO-API get owner coverage status by serial number"
|
570
|
+
print(task_name(text=task_text))
|
571
|
+
|
572
|
+
for sr_no, records in serial_dict.items():
|
573
|
+
record = records["SNIgetOwnerCoverageStatusBySerialNumbers"]
|
574
|
+
host = records["host"] if records["host"] else sr_no
|
575
|
+
print(task_host(host=host, changed=False))
|
576
|
+
# Verify if the serial number is associated with the CCO ID
|
577
|
+
if "YES" in record["sr_no_owner"]:
|
578
|
+
print(task_info(text="Verify provided CCO ID", changed=False))
|
579
|
+
print(f"'Verify provided CCO ID' -> {_success(True)}")
|
580
|
+
print("-> Is associated to the provided CCO ID")
|
581
|
+
else:
|
582
|
+
print(task_error(text="Verify provided CCO ID", changed=False))
|
583
|
+
print(f"'Verify provided CCO ID' -> {_success(False)}")
|
584
|
+
print("-> Is not associated to the provided CCO ID")
|
585
|
+
|
586
|
+
# Verify if the serial is covered by a service contract
|
587
|
+
if "YES" in record["is_covered"]:
|
588
|
+
print(task_info(text="Verify service contract", changed=False))
|
589
|
+
print(f"'Verify service contract' -> {_success(True)}")
|
590
|
+
print("-> Is covered by a service contract")
|
591
|
+
# Verify the end date of the service contract coverage
|
592
|
+
if record["coverage_end_date"]:
|
593
|
+
print(task_info(text="Verify service contract end date", changed=False))
|
594
|
+
print(f"'Verify service contract end date' -> {_success(True)}")
|
595
|
+
print(f"-> Coverage end date is {record['coverage_end_date']}")
|
596
|
+
else:
|
597
|
+
print(task_error(text="Verify service contract end date", changed=False))
|
598
|
+
print(f"'Verify service contract end date' -> {_success(False)}")
|
599
|
+
print("-> Coverage end date not available")
|
600
|
+
else:
|
601
|
+
print(task_error(text="Verify service contract", changed=False))
|
602
|
+
print(f"'Verify service contract' -> {_success(False)}")
|
603
|
+
print("-> Is not covered by a service contract")
|
604
|
+
|
605
|
+
if verbose:
|
606
|
+
print("\n" + json.dumps(record, indent=4))
|
607
|
+
|
608
|
+
# Verify the whole serial_dict and exit the script in case of an found error
|
609
|
+
verify_cisco_support_api_data(api="sni", iterable=serial_dict, verbose=False)
|
610
|
+
|
611
|
+
|
612
|
+
def print_sni_coverage_summary_by_serial_numbers(serial_dict: dict, verbose: bool = False) -> None:
|
613
|
+
"""
|
614
|
+
This function prints the result of get_sni_coverage_summary_by_serial_numbers() in Nornir style to stdout.
|
615
|
+
"""
|
616
|
+
task_text = "CISCO-API get coverage summary data by serial number"
|
617
|
+
print(task_name(text=task_text))
|
618
|
+
|
619
|
+
for sr_no, records in serial_dict.items():
|
620
|
+
record = records["SNIgetCoverageSummaryBySerialNumbers"]
|
621
|
+
host = records["host"] if records["host"] else sr_no
|
622
|
+
print(task_host(host=host, changed=False))
|
623
|
+
if "ErrorResponse" in record:
|
624
|
+
error_response = record["ErrorResponse"]["APIError"]
|
625
|
+
print(task_error(text=task_text, changed=False))
|
626
|
+
print(f"'Get SNI data' -> {_success(False)}")
|
627
|
+
print(f"-> {error_response['ErrorDescription']} ({error_response['SuggestedAction']})")
|
628
|
+
else:
|
629
|
+
print(task_info(text=task_text, changed=False))
|
630
|
+
print(f"'Get SNI data' -> {_success(True)}")
|
631
|
+
print(f"-> Orderable pid: {record['orderable_pid_list'][0]['orderable_pid']}")
|
632
|
+
print(f"-> Customer name: {record['contract_site_customer_name']}")
|
633
|
+
print(f"-> Customer address: {record['contract_site_address1']}")
|
634
|
+
print(f"-> Customer city: {record['contract_site_city']}")
|
635
|
+
print(f"-> Customer province: {record['contract_site_state_province']}")
|
636
|
+
print(f"-> Customer country: {record['contract_site_country']}")
|
637
|
+
print(f"-> Is covered by service contract: {record['is_covered']}")
|
638
|
+
print(f"-> Covered product line end date: {record['covered_product_line_end_date']}")
|
639
|
+
print(f"-> Service contract number: {record['service_contract_number']}")
|
640
|
+
print(f"-> Service contract description: {record['service_line_descr']}")
|
641
|
+
print(f"-> Warranty end date: {record['warranty_end_date']}")
|
642
|
+
print(f"-> Warranty type: {record['warranty_type']}")
|
643
|
+
|
644
|
+
if verbose:
|
645
|
+
print("\n" + json.dumps(record, indent=4))
|
646
|
+
|
647
|
+
# Verify the whole serial_dict and exit the script in case of an found error
|
648
|
+
verify_cisco_support_api_data(api="sni", iterable=serial_dict, verbose=False)
|
649
|
+
|
650
|
+
|
651
|
+
def print_eox_by_serial_numbers(serial_dict: dict, verbose: bool = False) -> None:
|
652
|
+
"""
|
653
|
+
This function prints the result of get_eox_by_serial_numbers() in Nornir style to stdout.
|
654
|
+
"""
|
655
|
+
task_text = "CISCO-API get EoX data by serial number"
|
656
|
+
print(task_name(text=task_text))
|
657
|
+
|
658
|
+
for sr_no, records in serial_dict.items():
|
659
|
+
record = records["EOXgetBySerialNumbers"]
|
660
|
+
host = records["host"] if records["host"] else sr_no
|
661
|
+
print(task_host(host=host, changed=False))
|
662
|
+
if "EOXError" in record:
|
663
|
+
if "No product IDs were found" in record["EOXError"]["ErrorDescription"]:
|
664
|
+
print(task_error(text=task_text, changed=False))
|
665
|
+
print(f"'Get EoX data' -> {_success(False)}")
|
666
|
+
print(f"-> {record['EOXError']['ErrorDescription']} (Serial number does not exist)")
|
667
|
+
elif "EOX information does not exist" in record["EOXError"]["ErrorDescription"]:
|
668
|
+
print(task_info(text=task_text, changed=False))
|
669
|
+
print(f"'Get EoX data' -> {_success(True)}")
|
670
|
+
print(f"-> {record['EOXError']['ErrorDescription']}")
|
671
|
+
else:
|
672
|
+
print(task_info(text=task_text, changed=False))
|
673
|
+
print(f"'Get EoX data (Last updated {record['UpdatedTimeStamp']['value']})' -> {_success(True)}")
|
674
|
+
print(f"-> EoL product ID: {record['EOLProductID']}")
|
675
|
+
print(f"-> Product ID description: {record['ProductIDDescription']}")
|
676
|
+
print(f"-> EoL announcement date: {record['EOXExternalAnnouncementDate']['value']}")
|
677
|
+
print(f"-> End of sale date: {record['EndOfSaleDate']['value']}")
|
678
|
+
print(f"-> End of maintenance release: {record['EndOfSWMaintenanceReleases']['value']}")
|
679
|
+
print(f"-> End of vulnerability support: {record['EndOfSecurityVulSupportDate']['value']}")
|
680
|
+
print(f"-> Last day of support: {record['LastDateOfSupport']['value']}")
|
681
|
+
|
682
|
+
if verbose:
|
683
|
+
print("\n" + json.dumps(record, indent=4))
|
684
|
+
|
685
|
+
# Verify the whole serial_dict and exit the script in case of an found error
|
686
|
+
verify_cisco_support_api_data(api="eox", iterable=serial_dict, verbose=False)
|
687
|
+
|
688
|
+
|
689
|
+
def print_get_ss_suggested_release_by_pid(serial_dict: dict, verbose: bool = False) -> None:
|
690
|
+
"""
|
691
|
+
This function prints the result of get_ss_suggested_release_by_pid() in Nornir style to stdout.
|
692
|
+
"""
|
693
|
+
task_text = "CISCO-API get suggested release data by pid"
|
694
|
+
print(task_name(text=task_text))
|
695
|
+
|
696
|
+
for sr_no, records in serial_dict.items():
|
697
|
+
host = records["host"] if records["host"] else sr_no
|
698
|
+
# A Task error should not be possible as get_ss_suggested_release_by_pid() have error verification
|
699
|
+
# and exits the script in case of an error
|
700
|
+
print(task_host(host=host, changed=False))
|
701
|
+
print(task_info(text=task_text, changed=False))
|
702
|
+
print(f"'Get SS data' -> {_success(True)}")
|
703
|
+
# As there can be multiple suggestions with the same ID and release, but only with different mdfId,
|
704
|
+
# the no_duplicates list will be created to eliminate printing duplicate ID and release information.
|
705
|
+
no_duplicates = []
|
706
|
+
for item in records["SSgetSuggestedReleasesByProductIDs"]:
|
707
|
+
if isinstance(item, str):
|
708
|
+
if item not in no_duplicates:
|
709
|
+
no_duplicates.append(item)
|
710
|
+
print(f"-> {item}")
|
711
|
+
elif isinstance(item, dict):
|
712
|
+
for idx, suggestion in enumerate(item["suggestions"]):
|
713
|
+
idx = idx + 1
|
714
|
+
if suggestion["releaseFormat1"] and suggestion["releaseFormat1"] not in no_duplicates:
|
715
|
+
no_duplicates.append(suggestion["releaseFormat1"])
|
716
|
+
print(f"-> ID: {idx}, Release: {suggestion['releaseFormat1']}")
|
717
|
+
elif (
|
718
|
+
suggestion["errorDetailsResponse"]
|
719
|
+
and suggestion["errorDetailsResponse"]["errorDescription"] not in no_duplicates
|
720
|
+
):
|
721
|
+
no_duplicates.append(suggestion["errorDetailsResponse"]["errorDescription"])
|
722
|
+
print(f"-> {suggestion['errorDetailsResponse']['errorDescription']}")
|
723
|
+
if verbose:
|
724
|
+
print("\n" + json.dumps(item, indent=4))
|
725
|
+
|
726
|
+
# Verify the whole serial_dict and exit the script in case of an found error
|
727
|
+
verify_cisco_support_api_data(api="ss", iterable=serial_dict, verbose=False)
|