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,569 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
This module contains screen-scraping functions and tasks related to Nornir.
|
4
|
+
|
5
|
+
The functions are ordered as followed:
|
6
|
+
- Screen-Scraping Helper Functions
|
7
|
+
- Single Nornir Screen-Scraping Tasks
|
8
|
+
- Nornir Screen-Scraping Tasks in regular Functions
|
9
|
+
"""
|
10
|
+
|
11
|
+
import sys
|
12
|
+
import time
|
13
|
+
from typing import Literal
|
14
|
+
from colorama import Fore, Style, init
|
15
|
+
from nornir_scrapli.tasks import send_config, send_configs, send_command, send_commands
|
16
|
+
from nornir_netmiko.tasks import netmiko_save_config
|
17
|
+
from nornir.core import Nornir
|
18
|
+
from nornir.core.task import Task, Result, AggregatedResult
|
19
|
+
from nornir_collection.utils import (
|
20
|
+
print_task_name,
|
21
|
+
task_host,
|
22
|
+
task_info,
|
23
|
+
task_error,
|
24
|
+
)
|
25
|
+
from nornir_collection.cisco.configuration_management.utils import (
|
26
|
+
create_tpl_int_list,
|
27
|
+
create_single_interface_list,
|
28
|
+
template_file_custom,
|
29
|
+
)
|
30
|
+
|
31
|
+
init(autoreset=True, strip=False)
|
32
|
+
|
33
|
+
|
34
|
+
#### Screen-Scraping Helper Functions ########################################################################
|
35
|
+
|
36
|
+
|
37
|
+
def create_tpl_int_config_content(task_obj: Task, tpl_int_group: str) -> list:
|
38
|
+
"""
|
39
|
+
This is a helper function for scrapli_replace_tpl_int_config() and creates the interface config contect
|
40
|
+
from the Nornir host inventory object which is then appended to the interface configuration
|
41
|
+
"""
|
42
|
+
# Create an empty list to append with the config
|
43
|
+
config = []
|
44
|
+
base_config = []
|
45
|
+
add_config = []
|
46
|
+
|
47
|
+
# Iterate over all host inventory keys
|
48
|
+
for key in task_obj.host.keys():
|
49
|
+
# Match the interface template config prefix
|
50
|
+
if key.startswith(f"cfg_{tpl_int_group}"):
|
51
|
+
# Match the key cfg_{tpl_int_group}_base
|
52
|
+
if key in f"cfg_{tpl_int_group}_base":
|
53
|
+
# Add the interface base config list to base_config
|
54
|
+
base_config = task_obj[key]
|
55
|
+
|
56
|
+
# Match all keys except cfg_{tpl_int_group}_base
|
57
|
+
if key not in f"cfg_{tpl_int_group}_base":
|
58
|
+
# Add all other interface config lists to add_config
|
59
|
+
add_config += task_obj[key]
|
60
|
+
|
61
|
+
# Add the base_config first and then the add_config
|
62
|
+
config += base_config
|
63
|
+
config += add_config
|
64
|
+
|
65
|
+
# Return the list of the interface template config content
|
66
|
+
return config
|
67
|
+
|
68
|
+
|
69
|
+
#### Single Nornir Screen Scraping Tasks #####################################################################
|
70
|
+
|
71
|
+
|
72
|
+
def scrapli_apply_jinja2_config(task: Task, jinja2_result_obj: AggregatedResult) -> Result:
|
73
|
+
"""
|
74
|
+
This function takes the Nornir AggregatedResult object from function jinja2_generate_config() and applies
|
75
|
+
the Jinja2 rendered configuration to each host.
|
76
|
+
"""
|
77
|
+
|
78
|
+
# Access the jinja2_result which is a AggregatedResult object and split the config into a list of strings.
|
79
|
+
config_list = jinja2_result_obj[str(task.host)][1].result.splitlines()
|
80
|
+
|
81
|
+
# Run the standard Nornir Scrapli task send_configs
|
82
|
+
result = task.run(
|
83
|
+
name="Scrapli apply Jinja2 rendered config",
|
84
|
+
task=send_configs,
|
85
|
+
configs=config_list,
|
86
|
+
strip_prompt=False,
|
87
|
+
timeout_ops=180,
|
88
|
+
)
|
89
|
+
|
90
|
+
return Result(host=task.host, result=result)
|
91
|
+
|
92
|
+
|
93
|
+
def scrapli_replace_tpl_int_config(task: Task, tpl_int_group: str) -> Result:
|
94
|
+
"""
|
95
|
+
Sets each interface in the interface_group to default and applies then the config template to the
|
96
|
+
specified interfaces defined in hosts.yaml
|
97
|
+
"""
|
98
|
+
try:
|
99
|
+
# Create an interface list with full interface names, Gi -> GigabitEthernet
|
100
|
+
single_interface_list = create_single_interface_list(task.host[tpl_int_group])
|
101
|
+
|
102
|
+
# Construct config for each interface and apply the config
|
103
|
+
for interface in single_interface_list:
|
104
|
+
# Empty the list to append with the interface config
|
105
|
+
interface_config = []
|
106
|
+
|
107
|
+
# Set the interface to default and then enter the interface config
|
108
|
+
interface_config.append("!")
|
109
|
+
interface_config.append(f"default interface {interface}")
|
110
|
+
interface_config.append("!")
|
111
|
+
interface_config.append(f"interface {interface}")
|
112
|
+
|
113
|
+
# Create the interface template content
|
114
|
+
config = create_tpl_int_config_content(task_obj=task, tpl_int_group=tpl_int_group)
|
115
|
+
|
116
|
+
# Add the interface template content to the interface config
|
117
|
+
for line in config:
|
118
|
+
interface_config.append(line)
|
119
|
+
|
120
|
+
# Exit the interface config mode back to the config mode
|
121
|
+
interface_config.append("exit")
|
122
|
+
interface_config.append("!")
|
123
|
+
|
124
|
+
# Apply interface config
|
125
|
+
task.run(
|
126
|
+
name="Scrapli interface config",
|
127
|
+
task=send_configs,
|
128
|
+
configs=interface_config,
|
129
|
+
strip_prompt=False,
|
130
|
+
stop_on_failed=False,
|
131
|
+
timeout_ops=180,
|
132
|
+
)
|
133
|
+
|
134
|
+
return Result(host=task.host)
|
135
|
+
|
136
|
+
except TypeError:
|
137
|
+
# TypeError Exception handles empty host inventory interface lists
|
138
|
+
# Print the exception result to avoid that Nornir interrupts the script
|
139
|
+
return Result(host=task.host, result=f"No interface in template group {tpl_int_group}")
|
140
|
+
# No interfaces in tpl_int_group (emtpy list)
|
141
|
+
# Return the Nornir result as True as no interface should be configured
|
142
|
+
|
143
|
+
except KeyError:
|
144
|
+
# KeyError exception handles not existing host inventory data keys
|
145
|
+
return Result(host=task.host, result=f"No template group {tpl_int_group} associated")
|
146
|
+
# No tpl_int_group (template group key not in host inventory)
|
147
|
+
# Return the Nornir result as True as no interface should be configured
|
148
|
+
|
149
|
+
|
150
|
+
#### Nornir Task in regular Functions ########################################################################
|
151
|
+
|
152
|
+
|
153
|
+
def cli_confirm_or_revert_config(
|
154
|
+
nr: Nornir, action: Literal["confirm", "revert"], cfg_status: bool = True, verbose: bool = False
|
155
|
+
) -> bool:
|
156
|
+
"""
|
157
|
+
TBD
|
158
|
+
"""
|
159
|
+
# pylint: disable=invalid-name
|
160
|
+
|
161
|
+
# Return False if cfg_status argument is False
|
162
|
+
if not cfg_status:
|
163
|
+
return False
|
164
|
+
|
165
|
+
# Run the standard Nornir Scrapli task send_configs
|
166
|
+
results = nr.run(
|
167
|
+
name=f"SCRAPLI CLI {action} config",
|
168
|
+
task=send_command,
|
169
|
+
command="configure confirm" if action == "confirm" else "configure revert now",
|
170
|
+
strip_prompt=True,
|
171
|
+
on_failed=True,
|
172
|
+
)
|
173
|
+
|
174
|
+
# Define the custom result to print
|
175
|
+
print_task_name(text=results.name)
|
176
|
+
for host in results:
|
177
|
+
# Print the base result
|
178
|
+
if results[host].failed or "%No Rollback Confirmed Change pending" in results[host].result:
|
179
|
+
cfg_status = False
|
180
|
+
print(task_host(host=host, changed=False))
|
181
|
+
print(task_error(text=results.name, changed=False))
|
182
|
+
print(f"'{results.name}' -> CliResponse <Success: False>")
|
183
|
+
print(f"-> Configure {action} failed")
|
184
|
+
print(f"\nVerbose result:\n{results[host].result}")
|
185
|
+
else:
|
186
|
+
print(task_host(host=host, changed=True))
|
187
|
+
print(task_info(text=results.name, changed=True))
|
188
|
+
print(f"'{results.name}' -> CliResponse <Success: True>")
|
189
|
+
print(f"-> Configure {action} successful")
|
190
|
+
if verbose:
|
191
|
+
print(f"\nVerbose result:\n{results[host].result}")
|
192
|
+
|
193
|
+
# Return the cfg_status variable
|
194
|
+
return cfg_status
|
195
|
+
|
196
|
+
|
197
|
+
def save_config_cli(nr: Nornir, name: str, verbose: bool = False) -> bool:
|
198
|
+
"""
|
199
|
+
#### CODE REFACTORING NEEDED -> INTRODUCE print_result ####
|
200
|
+
|
201
|
+
This function runs a Nornir task to execute with Netmiko netmiko_save_config a write memory on each device
|
202
|
+
and prints the result to std-out
|
203
|
+
"""
|
204
|
+
# pylint: disable=invalid-name
|
205
|
+
|
206
|
+
print_task_name(text=name)
|
207
|
+
|
208
|
+
task = nr.run(task=netmiko_save_config, cmd="write memory", on_failed=True)
|
209
|
+
|
210
|
+
for host in task:
|
211
|
+
print(task_host(host=host, changed=task[host].changed))
|
212
|
+
|
213
|
+
# If the host failed -> print the failed configuration
|
214
|
+
if task[host].failed:
|
215
|
+
print(task_error(text=name, changed=task[host].changed))
|
216
|
+
print(task[host][0].result)
|
217
|
+
|
218
|
+
# If verbose is True -> print all results
|
219
|
+
elif verbose:
|
220
|
+
print(task_info(text=name, changed=task[host].changed))
|
221
|
+
print(task[host][0].result)
|
222
|
+
|
223
|
+
# If the host succeeded and verbose is False -> print info
|
224
|
+
else:
|
225
|
+
print(task_info(text=name, changed=task[host].changed))
|
226
|
+
print("Saved config to startup-config successfully")
|
227
|
+
|
228
|
+
# Return True if the task were successful
|
229
|
+
if not task.failed:
|
230
|
+
return True
|
231
|
+
return False
|
232
|
+
|
233
|
+
|
234
|
+
def cfg_eem_replace_config(nr: Nornir, name: str, eem_name: str, file: str, verbose: bool = False) -> bool:
|
235
|
+
"""
|
236
|
+
#### CODE REFACTORING NEEDED -> INTRODUCE print_result ####
|
237
|
+
|
238
|
+
This function uses Scrapli send_configs to configure a EEM applet which replace the config from a
|
239
|
+
specified file path. After this config will be executed to replace the configuration. It's basically a
|
240
|
+
rollback function.
|
241
|
+
"""
|
242
|
+
# pylint: disable=invalid-name
|
243
|
+
|
244
|
+
print_task_name(text=name)
|
245
|
+
|
246
|
+
# Load day0 EEM applet config
|
247
|
+
# fmt: off
|
248
|
+
config_list = [
|
249
|
+
f'event manager applet {eem_name}',
|
250
|
+
'event none maxrun 60',
|
251
|
+
'action 1.0 cli command "enable"',
|
252
|
+
f'action 2.0 syslog msg "{eem_name} -> Started"',
|
253
|
+
f'action 2.1 cli command "{eem_name} -> Started"',
|
254
|
+
f'action 3.0 cli command "configure replace {file} force"',
|
255
|
+
f'action 4.0 syslog msg "{eem_name} -> Finish"',
|
256
|
+
f'action 4.1 cli command "{eem_name} -> Finish"',
|
257
|
+
]
|
258
|
+
# fmt: on
|
259
|
+
|
260
|
+
# Execute the EEM applet configuration. Run the standard Nornir Scrapli task send_configs
|
261
|
+
task1 = nr.run(
|
262
|
+
name=f"Scrapli configure EEM applet {eem_name}",
|
263
|
+
task=send_configs,
|
264
|
+
configs=config_list,
|
265
|
+
strip_prompt=False,
|
266
|
+
timeout_ops=180,
|
267
|
+
)
|
268
|
+
|
269
|
+
# Execute the EEM applet to replace the config. Run the standard Nornir Scrapli task send_configs
|
270
|
+
task2 = nr.run(
|
271
|
+
name=f"Scrapli execute EEM applet {eem_name}",
|
272
|
+
task=send_commands,
|
273
|
+
commands=[f"event manager run {eem_name}"],
|
274
|
+
strip_prompt=False,
|
275
|
+
timeout_ops=180,
|
276
|
+
)
|
277
|
+
|
278
|
+
# Wait some seconds to allow the EEM applet to replace the config
|
279
|
+
# Tested on a C9300 with 1700 lines of config to replace -> needs arround 25s
|
280
|
+
time.sleep(40)
|
281
|
+
|
282
|
+
# Delete the EEM applet after it have been executed. Run the standard Nornir Scrapli task send_configs
|
283
|
+
task3 = nr.run(
|
284
|
+
name=f"Scrapli delete EEM applet {eem_name}",
|
285
|
+
task=send_configs,
|
286
|
+
configs=[f"no event manager applet {eem_name}"],
|
287
|
+
strip_prompt=False,
|
288
|
+
timeout_ops=180,
|
289
|
+
)
|
290
|
+
|
291
|
+
task1_msg = "Scrapli configure EEM applet"
|
292
|
+
task2_msg = "Scrapli execute EEM applet"
|
293
|
+
task3_msg = "Scrapli delete EEM applet"
|
294
|
+
|
295
|
+
for host in task1:
|
296
|
+
print(task_host(host=host, changed=task1[host].changed))
|
297
|
+
|
298
|
+
# If the host failed -> print the failed configuration
|
299
|
+
if task1[host].failed:
|
300
|
+
print(task_error(text=task1_msg, changed=task1[host].changed))
|
301
|
+
print(f"'Configure {eem_name}' -> CliResponse <Success: False>\n")
|
302
|
+
print(task1[host][0].result)
|
303
|
+
print(task_error(text=task2_msg, changed=False))
|
304
|
+
print(f"'Execute {eem_name}' -> CliResponse <Success: False>\n")
|
305
|
+
print(task2[host][0].result)
|
306
|
+
print(task_error(text=task3_msg, changed=False))
|
307
|
+
print(f"'Delete {eem_name}' -> CliResponse <Success: False>\n")
|
308
|
+
print(task3[host][0].result)
|
309
|
+
|
310
|
+
# If verbose is True -> print all results
|
311
|
+
elif verbose:
|
312
|
+
print(task_info(text=task1_msg, changed=task1[host].changed))
|
313
|
+
print(f"'Configure {eem_name}' -> CliResponse <Success: True>\n")
|
314
|
+
print(task1[host][0].result)
|
315
|
+
print(task_info(text=task2_msg, changed=True))
|
316
|
+
print(f"'Execute {eem_name}' -> CliResponse <Success: True>\n")
|
317
|
+
print(task2[host][0].result)
|
318
|
+
print(task_info(text=task3_msg, changed=True))
|
319
|
+
print(f"'Delete {eem_name}' -> CliResponse <Success: True>\n")
|
320
|
+
print(task3[host][0].result)
|
321
|
+
|
322
|
+
# If the host succeeded and verbose is False -> print info
|
323
|
+
else:
|
324
|
+
print(task_info(text=task1_msg, changed=task1[host].changed))
|
325
|
+
print(f"'Configure {eem_name}' -> CliResponse <Success: True>")
|
326
|
+
print(task_info(text=task2_msg, changed=True))
|
327
|
+
print(f"'Execute {eem_name}' -> CliResponse <Success: True>")
|
328
|
+
print(task_info(text=task3_msg, changed=True))
|
329
|
+
print(f"'Delete {eem_name}' -> CliResponse <Success: True>")
|
330
|
+
|
331
|
+
# Return True if both tasks were successful
|
332
|
+
if not (task1.failed and task2.failed):
|
333
|
+
return True
|
334
|
+
return False
|
335
|
+
|
336
|
+
|
337
|
+
def jinja2_generate_config(
|
338
|
+
nr: Nornir, name: str, path: str, template: str, verbose: bool = False
|
339
|
+
) -> AggregatedResult:
|
340
|
+
"""
|
341
|
+
#### CODE REFACTORING NEEDED -> INTRODUCE print_result ####
|
342
|
+
|
343
|
+
This function runs the standard Nornir task template_file and generates a config for each host. If one or
|
344
|
+
more hosts failed the function print a error std-out message and terminates the script. Only when all
|
345
|
+
hosts were successful the Nornir AggregatedResult object will be returned
|
346
|
+
"""
|
347
|
+
# pylint: disable=invalid-name
|
348
|
+
|
349
|
+
print_task_name(text=name)
|
350
|
+
|
351
|
+
# Run the Nornir Task template_file
|
352
|
+
j2_config = nr.run(task=template_file_custom, task_msg=name, template=template, path=path)
|
353
|
+
|
354
|
+
for host in j2_config:
|
355
|
+
print(task_host(host=host, changed=False))
|
356
|
+
|
357
|
+
if j2_config[host].failed:
|
358
|
+
# If the subtask failed print its exception
|
359
|
+
if j2_config[host].result.startswith("Subtask:"):
|
360
|
+
print(task_error(text=name, changed=False))
|
361
|
+
|
362
|
+
# If the Jinja2 template is not found by the template_file task
|
363
|
+
if "TemplateNotFound" in j2_config[host][1].result:
|
364
|
+
file = j2_config[host][1].exception
|
365
|
+
print(f"Jinja2 template '{file}' not found")
|
366
|
+
|
367
|
+
# If the Jinja2 templating rendering catches another exception
|
368
|
+
else:
|
369
|
+
print(j2_config[host][1].result)
|
370
|
+
|
371
|
+
# If the task fails print the returned result
|
372
|
+
elif j2_config[host].failed:
|
373
|
+
print(j2_config[host].result)
|
374
|
+
|
375
|
+
# If no condition matched the task was successful
|
376
|
+
else:
|
377
|
+
print(task_info(text="Jinja2 template file", changed=False))
|
378
|
+
# Read the template filename from the Nornir inventory
|
379
|
+
file = nr.inventory.hosts[host][template]
|
380
|
+
print(f"Jinja2 template '{file}' rendered successfully")
|
381
|
+
|
382
|
+
if verbose:
|
383
|
+
print(f"\n{j2_config[host][1].result}")
|
384
|
+
|
385
|
+
if j2_config.failed_hosts:
|
386
|
+
# If one or more of the Jinja2 template tasks failed
|
387
|
+
print("\n")
|
388
|
+
print(task_error(text=name, changed=False))
|
389
|
+
print("\U0001f4a5 ALERT: JINJA2 CONFIG TEMPLATING FAILED! \U0001f4a5")
|
390
|
+
print(
|
391
|
+
f"\n{Style.BRIGHT}{Fore.RED}"
|
392
|
+
"-> Analyse the Nornir output for failed Jinja2 tasks\n"
|
393
|
+
"-> May apply Nornir inventory changes and run the script again\n\n"
|
394
|
+
"No config changes has been made yet!\n"
|
395
|
+
)
|
396
|
+
# Terminate the script with successful exit code 0
|
397
|
+
sys.exit(1)
|
398
|
+
|
399
|
+
# If the task was successful return its result object
|
400
|
+
return j2_config
|
401
|
+
|
402
|
+
|
403
|
+
def cfg_jinja2_config(nr: Nornir, name: str, jinja2_result: AggregatedResult, verbose: bool = False) -> bool:
|
404
|
+
"""
|
405
|
+
#### CODE REFACTORING NEEDED -> INTRODUCE print_result ####
|
406
|
+
|
407
|
+
This function takes a Nornir AggregatedResult object which contains the configuration that should be
|
408
|
+
applied to each device. It can continue with the result of the function jinja2_generate_config to apply a
|
409
|
+
Jinja2 config to devices and prints the result is the Nornir style.
|
410
|
+
"""
|
411
|
+
# pylint: disable=invalid-name
|
412
|
+
|
413
|
+
print_task_name(text=name)
|
414
|
+
|
415
|
+
# Run the custom nornir task scrapli_apply_jinja2_config
|
416
|
+
task = nr.run(task=scrapli_apply_jinja2_config, jinja2_result_obj=jinja2_result, on_failed=True)
|
417
|
+
|
418
|
+
for host in task:
|
419
|
+
print(task_host(host=host, changed=task[host].changed))
|
420
|
+
|
421
|
+
# If the host failed -> print the failed configuration
|
422
|
+
if task[host].failed:
|
423
|
+
print(task_error(text=name, changed=task[host].changed))
|
424
|
+
|
425
|
+
# If verbose is True -> print all results
|
426
|
+
if verbose:
|
427
|
+
print(task[host][1].result)
|
428
|
+
|
429
|
+
# If verbose is False -> print only failed results
|
430
|
+
else:
|
431
|
+
lines = task[host][1].result.splitlines()
|
432
|
+
for index, line in enumerate(lines):
|
433
|
+
if "'^'" in line:
|
434
|
+
print(lines[index - 3])
|
435
|
+
print(lines[index - 2])
|
436
|
+
print(lines[index - 1])
|
437
|
+
print(line)
|
438
|
+
|
439
|
+
# If the host succeeded and verbose is False -> print info
|
440
|
+
else:
|
441
|
+
print(task_info(text=name, changed=task[host].changed))
|
442
|
+
|
443
|
+
# If verbose is True -> print all results
|
444
|
+
if verbose:
|
445
|
+
print(task[host][1].result)
|
446
|
+
else:
|
447
|
+
print("Configuration successful")
|
448
|
+
|
449
|
+
return not task.failed
|
450
|
+
|
451
|
+
|
452
|
+
def cfg_multiline_banner(nr: Nornir, name: str, multiline_banner: str, verbose: bool = False) -> bool:
|
453
|
+
"""
|
454
|
+
#### CODE REFACTORING NEEDED -> INTRODUCE print_result ####
|
455
|
+
|
456
|
+
This function takes a multiline string as variable multiline_banner and configures this banner with the
|
457
|
+
help of Scrapli eager=True option. Because the banner "input mode" is basically like a text editor where
|
458
|
+
we dont get the prompt printed out between sending lines of banner config we need to use the 'eager' mode
|
459
|
+
to force scrapli to blindly send the banner/macro lines without looking for the prompt in between each
|
460
|
+
line. You should *not* use eager unless you need to and know what you are doing as it basically disables
|
461
|
+
one of the core features that makes scrapli reliable!
|
462
|
+
"""
|
463
|
+
# pylint: disable=invalid-name
|
464
|
+
|
465
|
+
print_task_name(text=name)
|
466
|
+
|
467
|
+
# Run the standard nornir task send_config
|
468
|
+
task = nr.run(
|
469
|
+
task=send_config,
|
470
|
+
config=multiline_banner,
|
471
|
+
strip_prompt=False,
|
472
|
+
eager=True,
|
473
|
+
on_failed=True,
|
474
|
+
timeout_ops=180,
|
475
|
+
)
|
476
|
+
|
477
|
+
for host in task:
|
478
|
+
print(task_host(host=host, changed=task[host].changed))
|
479
|
+
|
480
|
+
# If the host failed -> print the failed configuration
|
481
|
+
if task[host].failed:
|
482
|
+
print(task_error(text=name, changed=task[host].changed))
|
483
|
+
print(task[host][0].result)
|
484
|
+
|
485
|
+
# If verbose is True -> print all results
|
486
|
+
elif verbose:
|
487
|
+
print(task_info(text=name, changed=task[host].changed))
|
488
|
+
print(task[host][0].result)
|
489
|
+
|
490
|
+
# If the host succeeded and verbose is False -> print info
|
491
|
+
else:
|
492
|
+
print(task_info(text=name, changed=task[host].changed))
|
493
|
+
print("Configured multi-line banner successfully")
|
494
|
+
|
495
|
+
# Return True if both tasks were successful
|
496
|
+
if not task.failed:
|
497
|
+
return True
|
498
|
+
return False
|
499
|
+
|
500
|
+
|
501
|
+
def cfg_tpl_int_cli(nr: Nornir, name: str, verbose: bool = False) -> bool:
|
502
|
+
"""
|
503
|
+
#### CODE REFACTORING NEEDED -> INTRODUCE print_result ####
|
504
|
+
|
505
|
+
This function takes a Nornir object and a filter tag to execute all tasks within this function on the
|
506
|
+
given inventory subset. Each interface group will be configured by Scrapli.
|
507
|
+
"""
|
508
|
+
# pylint: disable=invalid-name
|
509
|
+
|
510
|
+
# Set the variable to return at the end of the function to True
|
511
|
+
cfg_status = True
|
512
|
+
|
513
|
+
# Gather the tpl_int template groups from all hosts
|
514
|
+
result = nr.run(task=create_tpl_int_list)
|
515
|
+
|
516
|
+
# Create a union of the results from all hosts -> no duplicate items
|
517
|
+
tpl_int_groups = []
|
518
|
+
for host in result:
|
519
|
+
tpl_int_groups = list(set().union(tpl_int_groups, result[host].result))
|
520
|
+
|
521
|
+
for group in tpl_int_groups:
|
522
|
+
print_task_name(text=f"{name} {group}")
|
523
|
+
|
524
|
+
# Run the custom nornir task scrapli_replace_tpl_int_config
|
525
|
+
task = nr.run(task=scrapli_replace_tpl_int_config, tpl_int_group=group, on_failed=True)
|
526
|
+
|
527
|
+
for host, multi_result in task.items():
|
528
|
+
print(task_host(host=host, changed=task[host].changed))
|
529
|
+
|
530
|
+
for result in multi_result:
|
531
|
+
# Nornir result is None and therefor also the Scrapli result is None. If the result starts
|
532
|
+
# with Subtask and configuration error has occured and the parent task error won't be printed
|
533
|
+
if (result.result is None) or result.result.startswith("Subtask:"):
|
534
|
+
continue
|
535
|
+
|
536
|
+
# Get the attribute scrapli_response from result into a variable. Return None if the attribute
|
537
|
+
# scrapli_response is not existing
|
538
|
+
scrapli_multi_result = getattr(result, "scrapli_response", None)
|
539
|
+
|
540
|
+
# If the scrapli_multi_result is None but the result.result is present this means an TypeError
|
541
|
+
# or KeyError handled inside the task is present
|
542
|
+
if scrapli_multi_result is None:
|
543
|
+
print(task_info(text=result.name, changed=result.changed))
|
544
|
+
print(result.result)
|
545
|
+
continue
|
546
|
+
|
547
|
+
# Find the interface from the Scrapli result
|
548
|
+
for scrapli_result in scrapli_multi_result:
|
549
|
+
if scrapli_result.channel_input.startswith("interface"):
|
550
|
+
interface = scrapli_result.channel_input
|
551
|
+
# Remove the word interface and leading whitespaces
|
552
|
+
interface = interface.replace("interface", "").lstrip()
|
553
|
+
|
554
|
+
# Print the result for a failed task
|
555
|
+
if result.failed:
|
556
|
+
print(task_error(text=result.name, changed=result.changed))
|
557
|
+
print(f"{interface} -> {scrapli_multi_result}")
|
558
|
+
print(result.result)
|
559
|
+
cfg_status = False
|
560
|
+
|
561
|
+
# Print the result for a successful task
|
562
|
+
else:
|
563
|
+
print(task_info(text=result.name, changed=result.changed))
|
564
|
+
print(f"{interface} -> {scrapli_multi_result}")
|
565
|
+
if verbose:
|
566
|
+
# Print the whole CLI result
|
567
|
+
print(result.result)
|
568
|
+
|
569
|
+
return cfg_status
|
@@ -0,0 +1,107 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
This module contains complete CLI screen-scraping configuration workflows with Nornir.
|
4
|
+
|
5
|
+
The functions are ordered as followed:
|
6
|
+
- Complete CLI configuration workflows
|
7
|
+
"""
|
8
|
+
|
9
|
+
from nornir.core import Nornir
|
10
|
+
from nornir_collection.cisco.configuration_management.cli.config_tasks import (
|
11
|
+
jinja2_generate_config,
|
12
|
+
cfg_eem_replace_config,
|
13
|
+
cfg_jinja2_config,
|
14
|
+
cfg_multiline_banner,
|
15
|
+
cfg_tpl_int_cli,
|
16
|
+
save_config_cli,
|
17
|
+
)
|
18
|
+
from nornir_collection.utils import (
|
19
|
+
print_task_title,
|
20
|
+
)
|
21
|
+
|
22
|
+
#### Complete CLI Configuration Workflow 01 ##################################################################
|
23
|
+
|
24
|
+
|
25
|
+
def cli_cfg_network_from_code_01(
|
26
|
+
nr: Nornir, banner_motd: str, rebuild: bool = False, verbose: bool = False
|
27
|
+
) -> bool:
|
28
|
+
"""
|
29
|
+
This function improves modularity as it is used within multiple scripts. The network will be reconfigured
|
30
|
+
to from the day0 config its desired state.
|
31
|
+
"""
|
32
|
+
# pylint: disable=invalid-name
|
33
|
+
|
34
|
+
print_task_title("Configure network from code")
|
35
|
+
|
36
|
+
# Set a variable to verify the network config status
|
37
|
+
cfg_status = True
|
38
|
+
|
39
|
+
# Returns a Nornir AggregatedResult object containing all generated configs by Jinja2 or terminates the
|
40
|
+
# script if one or more tasks failed
|
41
|
+
j2_base_config = jinja2_generate_config(
|
42
|
+
nr=nr,
|
43
|
+
name="Jinja2 render base config",
|
44
|
+
path="cli_path_tpl_base_config",
|
45
|
+
template="cli_payload_tpl_base_config",
|
46
|
+
verbose=verbose,
|
47
|
+
)
|
48
|
+
# cfg_status validation is not needed as the script ends with an error in case there is an issue with
|
49
|
+
# one or more config renderings
|
50
|
+
|
51
|
+
# If args.rebuild it True load the day0 config, otherwise load the golden-config
|
52
|
+
if rebuild:
|
53
|
+
# Replace the running-config with the day0 config from the switch flash:
|
54
|
+
cfg_status = cfg_eem_replace_config(
|
55
|
+
nr=nr,
|
56
|
+
name="Scrapli load day0-config",
|
57
|
+
eem_name="eem_load_day0_config",
|
58
|
+
file="flash:day0-config",
|
59
|
+
verbose=verbose,
|
60
|
+
)
|
61
|
+
|
62
|
+
else:
|
63
|
+
# Replace the running-config with the golden-config from the switch flash:
|
64
|
+
cfg_status = cfg_eem_replace_config(
|
65
|
+
nr=nr,
|
66
|
+
name="Scrapli load golden-config",
|
67
|
+
eem_name="eem_load_golden_config",
|
68
|
+
file="flash:golden-config",
|
69
|
+
verbose=verbose,
|
70
|
+
)
|
71
|
+
|
72
|
+
# Configures the Jinja2 generated config from the jinja2_generate_config() function returned
|
73
|
+
# AggregatedResult object -> j2_config
|
74
|
+
if cfg_status:
|
75
|
+
cfg_status = cfg_jinja2_config(
|
76
|
+
nr=nr,
|
77
|
+
name="Scrapli apply Jinja2 rendered base config",
|
78
|
+
jinja2_result=j2_base_config,
|
79
|
+
verbose=verbose,
|
80
|
+
)
|
81
|
+
|
82
|
+
# Configures the Cisco motd multi-line banner
|
83
|
+
if cfg_status:
|
84
|
+
cfg_status = cfg_multiline_banner(
|
85
|
+
nr=nr,
|
86
|
+
name="Scrapli configure motd multi-line banner",
|
87
|
+
multiline_banner=banner_motd,
|
88
|
+
verbose=verbose,
|
89
|
+
)
|
90
|
+
|
91
|
+
# Configures all interfaces which are part of an interface template template
|
92
|
+
if cfg_status:
|
93
|
+
cfg_status = cfg_tpl_int_cli(
|
94
|
+
nr=nr,
|
95
|
+
name="Scrapli configure interface templates",
|
96
|
+
verbose=verbose,
|
97
|
+
)
|
98
|
+
|
99
|
+
# Save the config to startup-config
|
100
|
+
if cfg_status:
|
101
|
+
cfg_status = save_config_cli(
|
102
|
+
nr=nr,
|
103
|
+
name="Netmiko save running-config to startup-config",
|
104
|
+
verbose=verbose,
|
105
|
+
)
|
106
|
+
|
107
|
+
return cfg_status
|