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,511 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ This module contains general configuration management functions and tasks related to Nornir.
4
+
5
+ The functions are ordered as followed:
6
+ - Helper Functions
7
+ - Nornir Result Helper Functions
8
+ - Nornir Helper Tasks
9
+ """
10
+
11
+ import os
12
+ import re
13
+ import argparse
14
+ import ipaddress
15
+ from typing import Literal
16
+ import __main__
17
+ from requests import Response
18
+ from colorama import init
19
+ from nornir_jinja2.plugins.tasks import template_file
20
+ from nornir.core.task import Task, Result
21
+ from jinja2 import Environment, FileSystemLoader, StrictUndefined
22
+ from nornir_collection.utils import (
23
+ task_result,
24
+ print_task_name,
25
+ task_info,
26
+ task_error,
27
+ get_env_vars,
28
+ CustomArgParse,
29
+ CustomArgParseWidthFormatter,
30
+ )
31
+ from nornir_collection.netbox.inventory import get_nb_resources
32
+
33
+ init(autoreset=True, strip=False)
34
+
35
+
36
+ #### Helper Functions ########################################################################################
37
+
38
+
39
+ def create_tpl_int_list(task: Task) -> list:
40
+ """
41
+ This function loops over all host inventory keys and append the key which start with tpl_int to the list
42
+ of interface groups and returns a Nornir AggregatedResult Object
43
+ """
44
+ tpl_int_list = []
45
+ for key in task.host.keys():
46
+ if key.startswith("tpl_int"):
47
+ tpl_int_list.append(key)
48
+
49
+ return tpl_int_list
50
+
51
+
52
+ def _split_nb_interface_addresses(addresses: dict) -> dict:
53
+ """
54
+ TBD
55
+ """
56
+ # Split NetBox address field e.g. 10.1.4.253/24 into ip and mask and create new key-value pairs
57
+ for item in addresses:
58
+ # For a single address (e.g. GET ip-addresses)
59
+ if "address" in item:
60
+ ip_address = ipaddress.ip_interface(item["address"])
61
+ item["ip"] = str(ip_address.ip)
62
+ item["mask"] = str(ip_address.netmask)
63
+ # For a list of addresses (e.g. GET fhrp-groups)
64
+ if "ip_addresses" in item:
65
+ for address in item["ip_addresses"]:
66
+ ip_address = ipaddress.ip_interface(address["address"])
67
+ address["ip"] = str(ip_address.ip)
68
+ address["mask"] = str(ip_address.netmask)
69
+
70
+ return addresses
71
+
72
+
73
+ def _flatten_interface_custom_field(addresses: dict) -> dict:
74
+ """
75
+ TBD
76
+ """
77
+ # Flatten the NetBox custom_field (e.g. GET ip-addresses or GET fhrp-groups)
78
+ for address in addresses:
79
+ if "custom_fields" in address:
80
+ for custom_fields, value in address["custom_fields"].items():
81
+ address[custom_fields] = value
82
+ address.pop("custom_fields")
83
+
84
+ return addresses
85
+
86
+
87
+ def index_of_first_number(string: str) -> int:
88
+ """
89
+ Return the index of the first number in a string
90
+ """
91
+ # pylint: disable=invalid-name
92
+
93
+ for i, c in enumerate(string):
94
+ if c.isdigit():
95
+ index = i
96
+ break
97
+
98
+ return index
99
+
100
+
101
+ def extract_interface_number(string: str) -> str:
102
+ """
103
+ Removes the interface name and returns only the interface number
104
+ """
105
+ try:
106
+ index = index_of_first_number(string)
107
+ interface_number = string[index:]
108
+
109
+ except UnboundLocalError:
110
+ interface_number = string
111
+
112
+ return interface_number
113
+
114
+
115
+ def extract_interface_name(string: str) -> str:
116
+ """
117
+ Removes the interface number and returns only the interface name
118
+ """
119
+ try:
120
+ index = index_of_first_number(string)
121
+ interface_name = string[:index]
122
+
123
+ except UnboundLocalError:
124
+ interface_name = string
125
+
126
+ return interface_name
127
+
128
+
129
+ def complete_interface_name(
130
+ interface_string: str,
131
+ ) -> Literal[
132
+ "Ethernet",
133
+ "FastEthernet",
134
+ "GigabitEthernet",
135
+ "TenGigabitEthernet",
136
+ "TwentyFiveGigE",
137
+ "HundredGigE",
138
+ "Port-channel",
139
+ "Vlan",
140
+ ]:
141
+ """
142
+ This function takes a string with an interface name only or a full interface with its number and returns
143
+ the full interface name but without the number:
144
+ Gi -> GigabitEthernet
145
+ Tw -> TwentyFiveGigE
146
+ etc.
147
+ """
148
+ if isinstance(interface_string, str):
149
+ # Extract the interface name / delete the interface number
150
+ interface_name = extract_interface_name(interface_string)
151
+ # String normalization
152
+ interface_name = interface_name.lower()
153
+
154
+ # Starting string order from most characters frist to least characters last
155
+ interfaces = {
156
+ "eth": "Ethernet",
157
+ "twe": "TwentyFiveGigE",
158
+ "fa": "FastEthernet",
159
+ "gi": "GigabitEthernet",
160
+ "tw": "TwoGigabitEthernet",
161
+ "te": "TenGigabitEthernet",
162
+ "hu": "HundredGigE",
163
+ "po": "Port-channel",
164
+ "vlan": "Vlan",
165
+ }
166
+
167
+ # Return the correct full interface name
168
+ for key, value in interfaces.items():
169
+ if interface_name.startswith(key):
170
+ return value
171
+
172
+ raise ValueError(f"'{interface_string}' is not a known interface name")
173
+
174
+ raise TypeError(f"Variable interface_string has type '{type(interface_string)}' and not type string")
175
+
176
+
177
+ def create_single_interface_list(interface: str) -> list:
178
+ """
179
+ This function takes a list of interfaces that are like the cisco interface range command and makes a list
180
+ of full interface names for each interface:
181
+ Gi1/0/1 -> GigabitEthernet1/0/1
182
+ Gi1/0/1 - 10 -> GigabitEthernet1/0/1, GigabitEthernet1/0/2, etc.
183
+ Gi1/0/1 - Gi1/0/10 -> GigabitEthernet1/0/1, GigabitEthernet1/0/2, etc.
184
+ """
185
+ # pylint: disable=redefined-argument-from-local
186
+
187
+ # Define a list to return at the end of the function
188
+ single_interface_list = []
189
+
190
+ # Create the full name of the interface, eg. Gi -> GigabitEthernet
191
+ interface_name = complete_interface_name(interface)
192
+
193
+ # If the list element is a single interface add it to the list to return
194
+ if "-" not in interface or "Port-channel" in interface:
195
+ interface_number = extract_interface_number(interface)
196
+ single_interface = interface_name + interface_number
197
+ single_interface_list.append(single_interface)
198
+
199
+ return single_interface_list
200
+
201
+ # Else the list element is a interface range to fullfil every single interface
202
+ # Create a list with the two interfaces for the range
203
+ interface_range = interface.replace(" ", "")
204
+ interface_range = interface.split("-")
205
+
206
+ # Regex pattern to match only the last number after the /
207
+ pattern = r"(\d+)(?!.*\d)"
208
+
209
+ # 1. Match the interface number prefix without the last number
210
+ interface_prefix = extract_interface_number(interface_range[0])
211
+ interface_prefix = re.sub(pattern, "", interface_prefix)
212
+
213
+ # 2. Match the number after the last / in the interface number
214
+ last_interface_numbers = []
215
+ for interface in interface_range:
216
+ # Extract only the interface number
217
+ interface_number = extract_interface_number(interface)
218
+ last_interface_number = re.findall(pattern, interface_number)
219
+ last_interface_numbers.append(last_interface_number[0])
220
+
221
+ # Define integers for the first and the last number of the range
222
+ range_first_number = int(last_interface_numbers[0])
223
+ range_last_number = int(last_interface_numbers[1])
224
+ # Iterate over the range and construct each single interface
225
+ while range_first_number <= range_last_number:
226
+ single_interface = interface_name + interface_prefix + str(range_first_number)
227
+ single_interface = single_interface.replace(" ", "")
228
+ single_interface_list.append(single_interface)
229
+ range_first_number += 1
230
+
231
+ return single_interface_list
232
+
233
+
234
+ def add_interface_data(task: Task, interface: dict) -> dict:
235
+ """
236
+ TBD
237
+ """
238
+ # If the interface is part of a Port-channel
239
+ if "lag" in interface and interface["lag"] is not None:
240
+ # Set the int_config_context of the Port-channel to the int_config_context of the interface
241
+ po_interface = [i for i in task.host["interfaces"] if i["name"] == interface["lag"]["name"]]
242
+ interface["int_config_context"] = po_interface[0]["int_config_context"]
243
+ interface["description"] = po_interface[0]["description"]
244
+ interface["portchannel_number"] = extract_interface_number(interface["lag"]["name"])
245
+
246
+ # Get the NetBox url from the inventory options
247
+ nb_url = task.nornir.config.inventory.options["nb_url"]
248
+
249
+ # Add the ip-addresses if the counter is bigger then 0
250
+ if "count_ipaddresses" in interface and interface["count_ipaddresses"] > 0:
251
+ response = get_nb_resources(
252
+ url=f"{nb_url}/api/ipam/ip-addresses/?device={task.host['name']}&interface={interface['name']}"
253
+ )
254
+ # Flatten custom_fields
255
+ response = _flatten_interface_custom_field(addresses=response)
256
+ # Split NetBox address field e.g. 10.1.4.253/24 into ip and mask
257
+ response = _split_nb_interface_addresses(addresses=response)
258
+ # Add the modified response
259
+ interface["ipaddresses"] = response
260
+ else:
261
+ interface["ipaddresses"] = None
262
+
263
+ # Add the fhrp-groups if the counter is bigger then 0
264
+ if "count_fhrp_groups" in interface and interface["count_fhrp_groups"] > 0:
265
+ response = get_nb_resources(url=f"{nb_url}/api/ipam/fhrp-groups/?name={interface['name']}")
266
+ # Flatten custom_fields
267
+ response = _flatten_interface_custom_field(addresses=response)
268
+ # Split NetBox address field e.g. 10.1.4.253/24 into ip and mask
269
+ response = _split_nb_interface_addresses(addresses=response)
270
+ # Add the modified response
271
+ interface["fhrp_groups"] = response
272
+ else:
273
+ interface["fhrp_groups"] = None
274
+
275
+ # Add the fhrp group-assignement if the counter is bigger then 0
276
+ if "count_fhrp_groups" in interface and interface["count_fhrp_groups"] > 0:
277
+ # Didn't found a querry to filter to interface and device
278
+ # Therefor the loop to select the correct dicts matching interface and device
279
+ response = get_nb_resources(url=f"{nb_url}/api/ipam/fhrp-group-assignments/")
280
+ # Delete all assignments which are not for that interface
281
+ response = [i for i in response if i["interface"]["name"] == interface["name"]]
282
+ # Delete all assignments which are not for that host
283
+ response = [i for i in response if i["interface"]["device"]["name"] == task.host["name"]]
284
+ # Add the modified response
285
+ interface["fhrp_group_assignment"] = response if response else None
286
+
287
+ return interface
288
+
289
+
290
+ def init_args_for_netconf_cm() -> argparse.Namespace:
291
+ """
292
+ This function initialze all arguments which are needed for further script execution. The default arguments
293
+ will be supressed. Returned will be a tuple with a use_nornir variable which is a boolian to indicate if
294
+ Nornir should be used for dynamically information gathering or not.
295
+ """
296
+ task_text = "ARGPARSE verify arguments"
297
+ print_task_name(text=task_text)
298
+
299
+ # Load environment variables or raise a TypeError when is None
300
+ env_vars = get_env_vars(envs=["NR_CONFIG_PROD", "NR_CONFIG_TEST"], task_text=task_text)
301
+ nr_config_prod = env_vars["NR_CONFIG_PROD"]
302
+ nr_config_test = env_vars["NR_CONFIG_TEST"]
303
+
304
+ # Define the arguments which needs to be given to the script execution
305
+ argparser = CustomArgParse(
306
+ prog=os.path.basename(__main__.__file__),
307
+ description="Specify the NetBox instance and Filter the Nornir inventory based on various criterias",
308
+ epilog="At least one of the mandatory arguments role tags or hosts needs to be specified.",
309
+ argument_default=argparse.SUPPRESS,
310
+ formatter_class=CustomArgParseWidthFormatter,
311
+ )
312
+
313
+ # Add all NetBox arguments
314
+ argparser.add_argument(
315
+ "--prod",
316
+ action="store_true",
317
+ help=f"use the NetBox 'PROD' instance and Nornir config '{nr_config_prod}'",
318
+ )
319
+ argparser.add_argument(
320
+ "--test",
321
+ action="store_true",
322
+ help=f"use the NetBox 'TEST' instance and Nornir config '{nr_config_test}'",
323
+ )
324
+ argparser.add_argument(
325
+ "--role", type=str, metavar="<ROLE>", help="inventory filter for a single device role"
326
+ )
327
+
328
+ argparser.add_argument(
329
+ "--tags", type=str, metavar="<TAGS>", help="inventory filter for comma seperated device tags"
330
+ )
331
+
332
+ argparser.add_argument(
333
+ "--hosts", type=str, metavar="<HOST-NAMES>", help="inventory filter for comma seperated device hosts"
334
+ )
335
+
336
+ # Add the optional verbose argument
337
+ argparser.add_argument(
338
+ "--verbose", action="store_true", default=False, help="show extensive result details"
339
+ )
340
+
341
+ # Add the optional rebuild argument
342
+ argparser.add_argument(
343
+ "--rebuild",
344
+ action="store_true",
345
+ default=False,
346
+ help="rebuild the config from day0 (default: golden-config)",
347
+ )
348
+
349
+ # Add the optional rebuild argument
350
+ argparser.add_argument(
351
+ "--disable-commit-confirm",
352
+ action="store_true",
353
+ default=False,
354
+ help="disable NETCONF commit-confirm (default: enabled)",
355
+ )
356
+
357
+ # Add the optional rebuild argument
358
+ argparser.add_argument(
359
+ "--confirm-timeout",
360
+ type=int,
361
+ metavar="<INT>",
362
+ default=60,
363
+ help="set NETCONF commit-confirm timeout in seconds (default: 60s)",
364
+ )
365
+
366
+ # Add the optional rebuild argument
367
+ argparser.add_argument(
368
+ "--dryrun",
369
+ action="store_true",
370
+ default=False,
371
+ help="perform a NETCONF dry-run (default: no dry-run)",
372
+ )
373
+
374
+ # Add the optional rebuild argument
375
+ argparser.add_argument(
376
+ "--skip-pre-check",
377
+ action="store_true",
378
+ default=False,
379
+ help="disable the pre-configuration check (default: False)",
380
+ )
381
+
382
+ # Verify the provided arguments and print the custom argparse error message in case any error or wrong
383
+ # arguments are present and exit the script
384
+ args = argparser.parse_args()
385
+
386
+ # Verify the NetBox instance and Nornir config filepath
387
+ if not (hasattr(args, "prod") or hasattr(args, "test")):
388
+ argparser.error("No NetBox instance specified, add --prod or --test")
389
+ # Verify the Nornir filter arguments
390
+ if not (hasattr(args, "hosts") or hasattr(args, "role") or hasattr(args, "tags")):
391
+ argparser.error("No Nornir inventory filter specified, add --hosts and/or roles and/or --tags")
392
+
393
+ # Set the NetBox instance and the Nornir config file based on the arguments
394
+ nb_instance = "TEST" if hasattr(args, "test") else "PROD"
395
+ nr_config = nr_config_test if hasattr(args, "test") else nr_config_prod
396
+
397
+ # If argparser.parse_args() is successful -> no argparse error message
398
+ print(task_info(text=task_text, changed=False))
399
+ print(f"'{task_text}' -> ArgparseResponse <Success: True>")
400
+
401
+ print("-> Configuration-MGMT arguments:")
402
+ print(f" - Run on the '{nb_instance}' NetBox instance and Nornir config '{nr_config}'")
403
+ if args.skip_pre_check:
404
+ print(" - Skip the pre-configuration check (diff running-config/golden-config)")
405
+ else:
406
+ print(" - Perform the pre-configuration check (diff running-config/golden-config)")
407
+ if args.dryrun:
408
+ print(" - NETCONF dry-run is enabled")
409
+ else:
410
+ if args.rebuild:
411
+ print(" - Rebuild the configuration from the 'Day0-Config'")
412
+ else:
413
+ print(" - Rebuild the configuration from the 'Golden-Config'")
414
+ if args.disable_commit_confirm:
415
+ print(" - NETCONF commit-confirm is disabled")
416
+ else:
417
+ print(f" - NETCONF commit-confirm is enabled (timeout: {args.confirm_timeout}s)")
418
+
419
+ if args.verbose:
420
+ print(f"\n{args}")
421
+
422
+ return nr_config, args
423
+
424
+
425
+ #### Nornir Result Helper Functions #########################################################################
426
+
427
+
428
+ def set_restconf_result( # pylint: disable=too-many-arguments
429
+ task: Task,
430
+ task_text: str,
431
+ yang_query: str,
432
+ response: Response,
433
+ custom_result: list,
434
+ verbose: bool = False,
435
+ ) -> tuple:
436
+ """
437
+ TBD
438
+ """
439
+ # Set the verbose result string to add to the result summary
440
+ result_verbose = (
441
+ f"URL: {response['url']}\n"
442
+ + f"Method: {response['method']}\n"
443
+ + f"Response: {response['response']}\n"
444
+ + f"Text: {response['text']}"
445
+ )
446
+
447
+ # Set the Nornir result to return as failed if the RESTCONF response status_code is not 200
448
+ if response["status_code"] != 200:
449
+ custom_result.append(
450
+ f"{task_result(text=task_text, changed=False, level_name='ERROR', failed=True)}\n"
451
+ + f"'{task_text}' -> RestconfResponse <Success: False>\n"
452
+ + f"\n{response}"
453
+ )
454
+ return Result(host=task.host, custom_result=custom_result, failed=True)
455
+
456
+ # Set the Nornir result
457
+ msg = (
458
+ f"{task_result(text=task_text, changed=False, level_name='INFO', failed=False)}\n"
459
+ + f"'{task_text}' -> RestconfResponse <Success: True>\n"
460
+ + f"-> Get data for '{yang_query}'"
461
+ )
462
+ custom_result.append(msg + f"\n\n{result_verbose}" if verbose else msg)
463
+
464
+ return custom_result
465
+
466
+
467
+ def render_jinja2_template(task: Task, tpl_path: str, kwargs: dict = None) -> str:
468
+ """
469
+ TBD
470
+ """
471
+ # Split the string into path and file (template)
472
+ path, filename = os.path.split(tpl_path)
473
+ # Load the Jinja2 environment
474
+ env = Environment(
475
+ loader=FileSystemLoader(path), undefined=StrictUndefined, trim_blocks=True, autoescape=True
476
+ )
477
+ template = env.get_template(filename)
478
+ # Renders contants of a file with jinja2. All the host data is available in the template
479
+ kwargs = kwargs or {}
480
+ nc_config = template.render(host=task.host, **kwargs)
481
+
482
+ return nc_config
483
+
484
+
485
+ #### Nornir Helper Tasks ####################################################################################
486
+
487
+
488
+ def template_file_custom(task: Task, task_msg: str, path: str, template: str) -> Result:
489
+ """
490
+ This custom Nornir task generates a configuration from a Jinja2 template based on a path and a template
491
+ filename. The path and the template filename needs to be Nornir inventory keys which holds the needed
492
+ information as value.
493
+ """
494
+ try:
495
+ path = task.host[path]
496
+ template = task.host[template]
497
+
498
+ except KeyError as error:
499
+ # Jinja2 Nornir inventory key not found. Key which specify the path and the file don't exist
500
+ error_msg = (
501
+ f"{task_error(text=task_msg, changed=False)}\n"
502
+ + f"'nornir.core.inventory.Host object' has no attribute {error}"
503
+ )
504
+
505
+ # Return the Nornir result as error -> interface can not be configured
506
+ return Result(host=task.host, result=error_msg, failed=True)
507
+
508
+ # Run the Nornir Task template_file
509
+ j2_tpl_result = task.run(task=template_file, template=template, path=path, on_failed=True)
510
+
511
+ return Result(host=task.host, result=j2_tpl_result)
File without changes