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,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
|
File without changes
|