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