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,307 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ This module contains the Nornir NETCONF operation RPC tasks lock, validate, discard, commit and unlock.
4
+ NETCONF RPC tasks like get-config or edit-config are not part of this module. Please take a look to
5
+ the module config_tasks for these tasks.
6
+
7
+ The functions are ordered as followed:
8
+ - Nornir NETCONF tasks
9
+ - Nornir NETCONF tasks in regular function
10
+ """
11
+
12
+ import traceback
13
+ from typing import Literal
14
+ from nornir.core import Nornir
15
+ from nornir.core.task import Task, Result
16
+ from nornir_collection.utils import print_result
17
+
18
+
19
+ #### Nornir NETCONF Tasks ###################################################################################
20
+
21
+
22
+ def nc_ops_rpc( # pylint: disable=too-many-arguments,invalid-name,too-many-branches
23
+ task: Task,
24
+ rpc: Literal["lock", "validate", "discard", "commit", "unlock"],
25
+ datastore: Literal["candidate", "running"] = "candidate",
26
+ confirm: bool = False,
27
+ confirm_timeout: int = 300,
28
+ verbose: bool = False,
29
+ ) -> Result:
30
+ """
31
+ This function is a Nornir Task using Scrapli to run a NETCONF RPC operation. The `rpc` argument
32
+ specifies the NETCONF operation to perform. The `datastore` argument specifies the NETCONF datastore
33
+ to perform the operation on. The `confirm` argument specifies if the commit operation should be
34
+ confirm. The `confirm_timeout` argument sets the timeout period for the commit-confirm operation.
35
+ The `verbose` argument sets the verbosity level of the result output.
36
+ """
37
+ # Track if the overall task has failed
38
+ failed = False
39
+
40
+ # Verify that the rpc argument is valid
41
+ if rpc not in ("lock", "validate", "discard", "commit", "unlock"):
42
+ result = (
43
+ f"'{task.name}' -> NetconfResponse <Success: False>\n"
44
+ + f"-> ValueError: '{rpc}' is not a supported NETCONF RPC"
45
+ )
46
+ return Result(host=task.host, result=result, changed=False, failed=True)
47
+
48
+ # Get the Scrapli Netconf connection manually
49
+ scrapli_conn = task.host.get_connection("scrapli_netconf", task.nornir.config)
50
+ scrapli_conn.open()
51
+
52
+ try:
53
+ if rpc == "lock":
54
+ nc_result = scrapli_conn.lock(target=datastore)
55
+ elif rpc == "unlock":
56
+ nc_result = scrapli_conn.unlock(target=datastore)
57
+ elif rpc == "validate":
58
+ nc_result = scrapli_conn.validate(source=datastore)
59
+ elif rpc == "discard":
60
+ nc_result = scrapli_conn.discard()
61
+ elif rpc == "commit":
62
+ if confirm:
63
+ # Set a "bare" NETCONF commit-confirm string
64
+ filter_commit_confirm = (
65
+ "<commit>\n"
66
+ " <confirmed/>\n"
67
+ f" <confirm-timeout>{confirm_timeout}</confirm-timeout>\n"
68
+ "</commit>"
69
+ )
70
+ nc_result = scrapli_conn.rpc(filter_=filter_commit_confirm)
71
+
72
+ else:
73
+ nc_result = scrapli_conn.commit()
74
+
75
+ except Exception: # pylint: disable=broad-exception-caught # nosec
76
+ result = (
77
+ f"'{task.name}' -> NetconfResponse <Success: False>\n"
78
+ + "-> Configuration attempt failed\n"
79
+ + f"\n{traceback.format_exc()}"
80
+ )
81
+ return Result(host=task.host, result=result, changed=False, failed=True)
82
+
83
+ # No Exception -> Continue to return the result
84
+ ########################################### WORKAROUND ##################################################
85
+ ### It seems that since 17.12.4 the NETCONF server implementation has a bug ! ###
86
+ ### If the database is locked by 'yang_mgmt_infra' or 'system system', the NETCONF rpc reply failed ###
87
+ ### The rpc reply contains one of the following errors: ###
88
+ ### - the configuration database is locked by session 20 yang_mgmt_infra tcp (system from 127.0.0.1) ###
89
+ ### - the configuration database is locked by session 238 system system (system from 0.0.0.0) ###
90
+ ### -> So we don't set the task as failed if this error occurs, as the configuration is still applied ###
91
+ #########################################################################################################
92
+ # Exclude the 'yang_mgmt_infra' and 'system system' locked database error
93
+ if "database is locked by" in str(nc_result.result):
94
+ if any(x in str(nc_result.result) for x in ["yang_mgmt_infra", "system system"]):
95
+ pass
96
+ else:
97
+ failed = True
98
+ # Else if the result XML payload dont't contain '<ok/>', the NETCONF rpc reply failed
99
+ elif "<ok/>" not in str(nc_result.result):
100
+ failed = True
101
+
102
+ # Set the task result
103
+ result = f"'{task.name}' -> {str(nc_result)}"
104
+ # Add more details to the task result
105
+ if rpc == "commit" and confirm and not failed:
106
+ result += f"\n-> NETCONF commit-confirm timeout period is set to {confirm_timeout}s"
107
+ if failed or verbose:
108
+ if confirm:
109
+ result += f"\n\n{filter_commit_confirm}"
110
+ result += f"\n\n{nc_result.result}"
111
+
112
+ # Set changed for the Nornir print result
113
+ changed = not failed
114
+
115
+ return Result(host=task.host, result=result, changed=changed, failed=failed)
116
+
117
+
118
+ #### Nornir NETCONF Tasks in regular Function ###############################################################
119
+
120
+
121
+ def nc_lock(
122
+ nr: Nornir,
123
+ datastore: Literal["candidate", "running"] = "candidate",
124
+ cfg_status: bool = True,
125
+ verbose: bool = False,
126
+ ) -> bool:
127
+ """
128
+ This function uses Nornir to run a NETCONF lock operation on the specified datastore using Scrapli.
129
+ If the `cfg_status` argument is set to False, the function will immediately return False without
130
+ performing any operations, in order to skip the NETCONF lock operation.
131
+ """
132
+ # pylint: disable=invalid-name
133
+
134
+ # Return False if cfg_status argument is False
135
+ if not cfg_status:
136
+ return False
137
+
138
+ # Lock the NETCONF datastore with Scrapli netconf_lock
139
+ nc_result = nr.run(
140
+ name="NETCONF lock datastore",
141
+ task=nc_ops_rpc,
142
+ rpc="lock",
143
+ datastore=datastore,
144
+ verbose=verbose,
145
+ on_failed=True,
146
+ )
147
+
148
+ # Print the result
149
+ print_result(result=nc_result)
150
+
151
+ # If the task failed -> nc_result.failed is True. So return False if nc_result.failed is True
152
+ if nc_result.failed:
153
+ cfg_status = False
154
+
155
+ return cfg_status
156
+
157
+
158
+ def nc_validate(
159
+ nr: Nornir,
160
+ datastore: Literal["candidate", "running"] = "candidate",
161
+ cfg_status: bool = True,
162
+ verbose: bool = False,
163
+ ) -> bool:
164
+ """
165
+ This function uses Nornir to run a NETCONF validate operation on the specified datastore using Scrapli.
166
+ If the `cfg_status` argument is set to False, the function will immediately return False without
167
+ performing any operations, in order to skip the NETCONF validate operation.
168
+ """
169
+ # pylint: disable=invalid-name
170
+
171
+ # Return False if cfg_status argument is False
172
+ if not cfg_status:
173
+ return False
174
+
175
+ # Lock the NETCONF datastore with Scrapli netconf_validate
176
+ nc_result = nr.run(
177
+ name="NETCONF validate datastore",
178
+ task=nc_ops_rpc,
179
+ rpc="validate",
180
+ datastore=datastore,
181
+ verbose=verbose,
182
+ on_failed=True,
183
+ )
184
+
185
+ # Print the result
186
+ print_result(result=nc_result)
187
+
188
+ # If the task failed -> nc_result.failed is True. So return False if nc_result.failed is True
189
+ if nc_result.failed:
190
+ cfg_status = False
191
+
192
+ return cfg_status
193
+
194
+
195
+ def nc_commit(
196
+ nr: Nornir,
197
+ confirm: bool = False,
198
+ confirm_timeout: int = 300,
199
+ cfg_status: bool = True,
200
+ verbose: bool = False,
201
+ ) -> bool:
202
+ """
203
+ This function uses Nornir to run a NETCONF commit operation using Scrapli. Optionally, the function
204
+ can perform a commit-confirm operation by setting the `confirm` argument to True. The
205
+ `confirm_timeout` argument sets the timeout period for the commit-confirm operation. If the
206
+ `cfg_status` argument is set to False, the function will immediately return False without performing
207
+ any operations, in order to skip the NETCONF commit operation.
208
+ """
209
+ # pylint: disable=invalid-name
210
+
211
+ # Return False if cfg_status argument is False
212
+ if not cfg_status:
213
+ return False
214
+
215
+ # Set the task name
216
+ if confirm:
217
+ task_name = "NETCONF commit-confirm datastore"
218
+ else:
219
+ task_name = "NETCONF commit datastore"
220
+
221
+ # Lock the NETCONF datastore with Scrapli netconf_commit
222
+ nc_result = nr.run(
223
+ name=task_name,
224
+ task=nc_ops_rpc,
225
+ rpc="commit",
226
+ confirm=confirm,
227
+ confirm_timeout=confirm_timeout,
228
+ verbose=verbose,
229
+ on_failed=True,
230
+ )
231
+
232
+ # Print the result
233
+ print_result(result=nc_result)
234
+
235
+ # If the task failed -> nc_result.failed is True. So return False if nc_result.failed is True
236
+ if nc_result.failed:
237
+ cfg_status = False
238
+
239
+ return cfg_status
240
+
241
+
242
+ def nc_unlock(
243
+ nr: Nornir,
244
+ datastore: Literal["candidate", "running"] = "candidate",
245
+ cfg_status: bool = True,
246
+ verbose: bool = False,
247
+ ) -> bool:
248
+ """
249
+ This function uses Nornir to run a NETCONF unlock operation on the specified datastore using Scrapli.
250
+ If the `cfg_status` argument is set to False, the function will immediately return False without
251
+ performing any operations, in order to skip the NETCONF unlock operation.
252
+ """
253
+ # pylint: disable=invalid-name
254
+
255
+ # Return False if cfg_status argument is False
256
+ if not cfg_status:
257
+ return False
258
+
259
+ # Lock the NETCONF datastore with Scrapli netconf_unlock
260
+ nc_result = nr.run(
261
+ name="NETCONF unlock datastore",
262
+ task=nc_ops_rpc,
263
+ rpc="unlock",
264
+ datastore=datastore,
265
+ verbose=verbose,
266
+ on_failed=True,
267
+ )
268
+
269
+ # Print the result
270
+ print_result(result=nc_result)
271
+
272
+ # If the task failed -> nc_result.failed is True. So return False if nc_result.failed is True
273
+ if nc_result.failed:
274
+ cfg_status = False
275
+
276
+ return cfg_status
277
+
278
+
279
+ def nc_discard(nr: Nornir, cfg_status: bool = False, verbose: bool = False) -> bool:
280
+ """
281
+ This function uses Nornir to run a NETCONF discard operation using Scrapli. If the `cfg_status`
282
+ argument is set to True, the function will immediately return True without performing any operations,
283
+ as no NETCONF discard is needed.
284
+ """
285
+ # pylint: disable=invalid-name
286
+
287
+ # Return True if cfg_status argument is True
288
+ if cfg_status:
289
+ return True
290
+
291
+ # Lock the NETCONF datastore with Scrapli netconf_discard
292
+ nc_result = nr.run(
293
+ name="NETCONF discard datastore",
294
+ task=nc_ops_rpc,
295
+ rpc="discard",
296
+ verbose=verbose,
297
+ on_failed=True,
298
+ )
299
+
300
+ # Print the result
301
+ print_result(result=nc_result)
302
+
303
+ # If the task failed -> nc_result.failed is True. So return False if nc_result.failed is True
304
+ if nc_result.failed:
305
+ cfg_status = False
306
+
307
+ return cfg_status
@@ -0,0 +1,151 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ This module contains functions regarding Nornir run with Processor.
4
+
5
+ The functions are ordered as followed:
6
+ - Nornir Processor Task in Functions
7
+ - Nornir Processor Print Functions
8
+ """
9
+
10
+ from colorama import Style, init
11
+ from beautifultable import BeautifulTable
12
+ from nornir.core import Nornir
13
+ from nornir.core.task import AggregatedResult
14
+ from nornir_salt.plugins.processors import TestsProcessor
15
+ from nornir_salt.plugins.tasks import scrapli_send_commands
16
+ from nornir_collection.utils import (
17
+ print_task_name,
18
+ task_host,
19
+ task_info,
20
+ task_result,
21
+ list_flatten,
22
+ load_multiple_yaml_files_to_string,
23
+ nr_filter_by_role_and_tag,
24
+ print_result,
25
+ )
26
+
27
+ init(autoreset=True, strip=False)
28
+
29
+
30
+ #### Nornir Processor Task in Functions #####################################################################
31
+
32
+
33
+ def nr_testsprocessor(nr: Nornir, name: str, inv_key: str, role: str = None, tags: list[str] = None) -> bool:
34
+ """
35
+ This function filters the Nornir object by the string of the argument filter_tag and searches all values
36
+ of the Nornir inventory which starts with the string of the argument inv_key. These values can be a
37
+ string or a list of strings which are the TestProcessor test suite yaml files.
38
+ As Nornir with processors works on the nr object level and not on the task level, it have to be ensured
39
+ that all filtered hosts have access to all files or the TestsProcessor task will fail.
40
+ The test suite yaml file supports all NornirSalt TestsProcessor values including Jinja2 host templating.
41
+ """
42
+ # pylint: disable=invalid-name
43
+
44
+ task_text = f"NORNIR prepare TestsProcessor '{name}'"
45
+ print_task_name(task_text)
46
+
47
+ # If filter_tag is True, filter the inventory based on the string from the argument filter_tag
48
+ # If the filteres object have no hosts, exit with a error message
49
+ if role or tags:
50
+ nr = nr_filter_by_role_and_tag(nr=nr, role=role, tags=tags, silent=False)
51
+
52
+ # Create a list with the values of all inventory keys starting with a specific string
53
+ file_list = []
54
+ for host in nr.inventory.hosts.values():
55
+ file_list += [value for key, value in host.items() if key.startswith(inv_key)]
56
+
57
+ # Flatten the file_list if it contains lists of lists
58
+ file_list = list_flatten(file_list)
59
+ # Create a union of the files in the file_list -> no duplicate items
60
+ file_list = list(set().union(file_list))
61
+
62
+ # Load the test suite yaml files from file_list as one string to render jinja2 host inventory data
63
+ yaml_string = load_multiple_yaml_files_to_string(file_list=file_list, silent=False)
64
+ # Return False if the yaml_string is empty
65
+ if not yaml_string:
66
+ return False
67
+
68
+ task_text = f"NORNIR run TestsProcessor '{name}'"
69
+ print_task_name(task_text)
70
+
71
+ # Add the nornir salt TestsProcessor processor
72
+ # TestsProcessor expects a list, therefor each yaml string needs to be packed into a list
73
+ # TestsProcessor templates the yaml string with Jinja2 and loads the yaml string into a dict
74
+ nr_with_testsprocessor = nr.with_processors(
75
+ [TestsProcessor(tests=[yaml_string], build_per_host_tests=True)]
76
+ )
77
+
78
+ # Collect output from the devices using scrapli send_commands task plugin
79
+ try:
80
+ results = nr_with_testsprocessor.run(task=scrapli_send_commands, on_failed=True)
81
+ except ValueError: # pylint: disable=broad-except
82
+ print(task_info(text=task_text, changed=False))
83
+ print(f"'{task_text}' -> NornirResponse <Success: True>")
84
+ print("-> Test files have no tests or are empty")
85
+ return True
86
+
87
+ # Print the TestsProcessor results
88
+ cfg_status = print_testsprocessor_results(nr_result=results, name=name)
89
+
90
+ return cfg_status
91
+
92
+
93
+ #### Print Functions ########################################################################################
94
+
95
+
96
+ def print_testsprocessor_results(nr_result: AggregatedResult, name: str) -> None:
97
+ """
98
+ This function prints a NornirSalt TestsProcessor result in a nice table with the library rich
99
+ """
100
+ # Track if the overall task has failed
101
+ cfg_status = True
102
+
103
+ # Print for each host a table with the Nornir testsprocessor result
104
+ for host, multiresult in nr_result.items():
105
+ # Print the host
106
+ print(task_host(host=str(host), changed=False))
107
+ # Print the overal TestsProcessor task result as INFO is all tests are successful, else ERROR
108
+ level_name = "INFO" if all("PASS" in result.result for result in multiresult) else "ERROR"
109
+ print(task_result(text=f"NORNIR run TestsProcessor '{name}'", changed=False, level_name=level_name))
110
+ # Update the overall task status if the level name if ERROR
111
+ if level_name == "ERROR":
112
+ cfg_status = False
113
+
114
+ try:
115
+ # Create a table with the Python library beautifultable
116
+ table = BeautifulTable()
117
+ table.set_style(BeautifulTable.STYLE_NONE)
118
+ table.columns.width = [50, 25, 10]
119
+ table.columns.header = [
120
+ f"{Style.BRIGHT}Name / Task",
121
+ f"{Style.BRIGHT}Criteria / Test",
122
+ f"{Style.BRIGHT}Result",
123
+ ]
124
+ # Create a custom table styling
125
+ table.columns.header.separator = "-"
126
+ table.columns.separator = "|"
127
+ table.rows.separator = "-"
128
+ table.columns.alignment = BeautifulTable.ALIGN_LEFT
129
+ # Add a row for each test result
130
+ for result in multiresult:
131
+ # Expression test using evan have an empty criteria in the Nornir result
132
+ if result.criteria:
133
+ criteria = f"{Style.DIM}\x1b[3m({result.test})\n{result.criteria}"
134
+ else:
135
+ criteria = f"{Style.DIM}\x1b[3mCritera not available\n"
136
+ criteria += f"{Style.DIM}\x1b[3mfor this test"
137
+ table.rows.append(
138
+ [
139
+ f"{Style.DIM}\x1b[3m({result.task})\n{result.name}",
140
+ f"{criteria}",
141
+ f"{result.result} ✅" if result.result == "PASS" else f"{result.result} ❌",
142
+ ]
143
+ )
144
+ # Print the TestProcessor result as beautifultable
145
+ print(f"\n{table}")
146
+ except: # pylint: disable=bare-except
147
+ # Print the Nornir result to stdout
148
+ print_result(multiresult)
149
+
150
+ # Return a config status boolian True if all tests were successful or False if not
151
+ return cfg_status
@@ -0,0 +1,236 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ This module contains Nornir pyATS functions and tasks.
4
+
5
+ The functions are ordered as followed:
6
+ - Nornir pyATS tasks in regular functions
7
+ """
8
+
9
+ import subprocess # nosec
10
+ import sys
11
+ from typing import Union
12
+ import yaml
13
+ from colorama import Fore, Style, init
14
+ from nornir.core import Nornir
15
+ from nornir_collection.utils import (
16
+ print_task_title,
17
+ print_task_name,
18
+ task_host,
19
+ task_info,
20
+ task_error,
21
+ )
22
+ from nornir_collection.cisco.configuration_management.cli.show_tasks import write_commands_to_file
23
+ from nornir_collection.cisco.configuration_management.restconf.cisco_rpc import (
24
+ rc_cisco_rpc_copy_file,
25
+ rc_cisco_rpc_is_syncing,
26
+ )
27
+
28
+ init(autoreset=True, strip=False)
29
+
30
+
31
+ #### Nornir pyATS Functions ##################################################################################
32
+
33
+
34
+ def pyats_create_testbed(nr: Nornir, pyats_testbed_file: str) -> None:
35
+ """
36
+ This function create a pyATS testbed from a nornir inventory. The nornir object can be the whole inventory
37
+ or a filtered subset.
38
+ """
39
+ # pylint: disable=invalid-name
40
+
41
+ # Create a empty dictionary to populate with every nornir host details
42
+ pyats_testbed = {}
43
+ pyats_testbed["devices"] = {}
44
+
45
+ try:
46
+ # Add each nornir host details to the dictionary
47
+ for host in nr.inventory.hosts:
48
+ # Create a nornir host object
49
+ host_obj = nr.inventory.hosts[host]
50
+ # Add a dictionary key for the nornir host
51
+ pyats_testbed["devices"][str(host_obj)] = {}
52
+ # Add dictionary keys and values from nornir inventory
53
+ pyats_testbed["devices"][str(host_obj)].update(
54
+ {
55
+ "type": host_obj["pyats"]["type"],
56
+ "os": host_obj["pyats"]["os"],
57
+ "platform": host_obj["pyats"]["platform"],
58
+ "credentials": {
59
+ "default": {
60
+ "username": nr.inventory.defaults.username,
61
+ "password": nr.inventory.defaults.password,
62
+ }
63
+ },
64
+ "connections": {
65
+ "cli": {
66
+ "protocol": host_obj["pyats"]["protocol"],
67
+ "ip": host_obj.hostname,
68
+ }
69
+ },
70
+ }
71
+ )
72
+
73
+ except KeyError as error:
74
+ # KeyError exception handles not existing host inventory data keys
75
+ print(task_host(host=host, changed=False))
76
+ print(task_error(text="PyATS create testbed failed", changed=False))
77
+ print(f"Nornir inventory host key ['data']['pyats'][{error}] don't exist\n")
78
+ sys.exit(1)
79
+
80
+ # Write the pyATS testbed to a file encoded in yaml
81
+ with open(pyats_testbed_file, "w", encoding="utf-8") as stream:
82
+ stream.writelines(["---\n"])
83
+ yaml.dump(pyats_testbed, stream, default_flow_style=False)
84
+
85
+
86
+ def pyats_update_golden_config(pyats_testbed_file: str) -> None:
87
+ """
88
+ This function creates or updates the pyATS golden config and prints a Nornir output to std-out
89
+ """
90
+ try:
91
+ golden_config = "network_state/golden-config-cli"
92
+
93
+ # fmt: off
94
+ subprocess.run( # nosec
95
+ ["pyats", "learn", "config", "--testbed-file",pyats_testbed_file, "--output", golden_config,],
96
+ check=True, capture_output=True,
97
+ )
98
+ # fmt: on
99
+
100
+ task_info_text = "PyATS update golden config"
101
+ print(task_info(text=task_info_text, changed=False))
102
+ print(f"'Update {golden_config}' -> PyATSResponse <Success: True>")
103
+
104
+ except subprocess.CalledProcessError as error:
105
+ # Exception is raised if subprocess exits with a non 0 status code
106
+ task_error_text = "PyATS update golden config"
107
+ print(task_error(text=task_error_text, changed=False))
108
+ print(f"'Update {golden_config}' -> PyATSResponse <Success: False>\n")
109
+ print(f"{error}")
110
+
111
+
112
+ def pyats_artifacts_cleanup(artifacts_list: list) -> None:
113
+ """
114
+ This function deletes a list of pyATS artifact files and prints a Nornir output to std-out
115
+ """
116
+ # Define a empty string to return at the end of the function
117
+ task_info_result = ""
118
+ task_error_result = ""
119
+
120
+ # Delete each artifact in the artifacts_list
121
+ for artifact in artifacts_list:
122
+ try:
123
+ # Delete all pyATS artifacts
124
+ # fmt: off
125
+ subprocess.run(["rm", "-r", artifact,],check=True, capture_output=True,) # nosec
126
+ # fmt: on
127
+
128
+ task_info_result += f"'Delete {artifact}' -> SubprocessResponse <Success:True>\n"
129
+
130
+ except subprocess.CalledProcessError as error:
131
+ # Exception is raised if subprocess exits with a non 0 status code
132
+ task_error_result += f"{error}\n"
133
+
134
+ task_text = "PyATS clean-up of artifact"
135
+
136
+ # Return if all subprocess failed
137
+ if not task_info_result:
138
+ # Return error result only
139
+ print(f"{task_error(text=task_text, changed=False)}\n" + f"{task_error_result.rstrip()}")
140
+ # Return if all subprocess were successful
141
+ elif not task_error_result:
142
+ # Return info result only
143
+ print(f"{task_info(text=task_text, changed=True)}\n" + f"{task_info_result.rstrip()}")
144
+ # Return if it's a mix of failed and successfull subprocess
145
+ else:
146
+ # Return info and error result
147
+ print(
148
+ f"{task_info(text=task_text, changed=True)}\n"
149
+ + f"{task_info_result.rstrip()}\n"
150
+ + f"{task_error(text=task_text, changed=False)}\n"
151
+ + f"{task_error_result.rstrip()}"
152
+ )
153
+
154
+
155
+ def pyats_update_golden_cleanup(
156
+ nr: Nornir, cfg_status: bool, task_text: str, pyats: dict, verbose: bool = False
157
+ ) -> Union[str, None]:
158
+ """
159
+ This function is used to verify if a new golden config can be created based on the cfg_status boolian.
160
+ If veriy_status is True a new golden config will be created and the list of artifact files can be deleted.
161
+ """
162
+ # pylint: disable=invalid-name
163
+
164
+ # Verify status of the config results
165
+ print_task_title("Verify network from code config status")
166
+ print_task_name(text="Verify config results")
167
+
168
+ if cfg_status:
169
+ # If the config is successful -> update the golden config
170
+ print(task_info(text="Verify config results", changed=True))
171
+ print(task_text[0])
172
+
173
+ # Save the running-config as text-file to local disk
174
+ print_task_title("Backup config file to local disk")
175
+
176
+ write_commands_to_file(
177
+ nr=nr,
178
+ name="Backup config file to local disk",
179
+ commands=["show running-config"],
180
+ path="config_backup",
181
+ filename_suffix="_config.txt",
182
+ backup_config=True,
183
+ verbose=verbose,
184
+ )
185
+
186
+ # Update golden config and clean-up artifacts
187
+ print_task_title("Update golden config and clean-up artifacts")
188
+
189
+ if pyats["generate_testbed"]:
190
+ text = "PyATS generate testbed from Nornir inventory"
191
+ print_task_name(text=text)
192
+ # Create a pyATS testbed from the filtered Nornir inventory
193
+ # Exceptions are handled inside the function
194
+ pyats_create_testbed(nr=nr, pyats_testbed_file=pyats["testbed"])
195
+ print(task_info(text=text, changed=True))
196
+ print(f"'Generate {pyats['testbed']}' -> PyATSResponse <Success: True>")
197
+
198
+ print_task_name(text="PyATS update golden config")
199
+ pyats_update_golden_config(pyats_testbed_file=pyats["testbed"])
200
+
201
+ # Checks to see if sync from the network element to the running datastore is
202
+ # in progress and wait until the configuration datastore if unlocked again
203
+ rc_cisco_rpc_is_syncing(nr=nr, verbose=verbose)
204
+
205
+ rc_cisco_rpc_copy_file(
206
+ nr=nr,
207
+ name="RESTCONF save golden config",
208
+ source="running-config",
209
+ destination="flash:golden-config",
210
+ verbose=verbose,
211
+ )
212
+
213
+ print_task_name(text="PyATS clean-up artifacts")
214
+ pyats_artifacts_cleanup(artifacts_list=pyats["artifacts_list"])
215
+
216
+ # No configuration error message to be returned
217
+ error_msg = None
218
+
219
+ else:
220
+ # If one or more of the network from code config tasks failed
221
+ task_error_text = "Config from code failed"
222
+ print(task_error(text=task_error_text, changed=True))
223
+ print(task_text[1])
224
+
225
+ if pyats["cleanup_else"]:
226
+ print_task_name(text="PyATS clean-up artifacts")
227
+ pyats_artifacts_cleanup(artifacts_list=pyats["artifacts_list"])
228
+
229
+ error_msg = (
230
+ f"\n{Style.BRIGHT}{Fore.RED}-> Analyse the Nornir output for failed config tasks\n"
231
+ "-> May apply Nornir inventory changes and run the script again\n\n"
232
+ f"The golden config has not been updated yet!{Fore.RESET}{Style.RESET_ALL}\n"
233
+ )
234
+
235
+ # Return error_msg if exists or return None
236
+ return error_msg if error_msg else None