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