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