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,741 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
This module contains general functions and tasks related to Nornir.
|
4
|
+
|
5
|
+
The functions are ordered as followed:
|
6
|
+
- Helper Functions
|
7
|
+
- Nornir print functions
|
8
|
+
- Nornir Helper Tasks
|
9
|
+
- Nornir print_result deviation
|
10
|
+
"""
|
11
|
+
|
12
|
+
import os
|
13
|
+
import sys
|
14
|
+
import argparse
|
15
|
+
import json
|
16
|
+
import hashlib
|
17
|
+
import logging
|
18
|
+
import pprint
|
19
|
+
import threading
|
20
|
+
import urllib
|
21
|
+
import operator
|
22
|
+
from functools import reduce
|
23
|
+
from typing import Generator, Tuple, List, NoReturn, Union
|
24
|
+
from collections import OrderedDict
|
25
|
+
from datetime import datetime
|
26
|
+
import yaml
|
27
|
+
import __main__
|
28
|
+
from colorama import Fore, Style, init
|
29
|
+
from pyfiglet import figlet_format
|
30
|
+
from pandas import DataFrame
|
31
|
+
from nornir.core import Nornir
|
32
|
+
from nornir.core.filter import F
|
33
|
+
from nornir.core.task import AggregatedResult, MultiResult, Result
|
34
|
+
from nornir_salt.plugins.functions import FFun
|
35
|
+
|
36
|
+
|
37
|
+
init(autoreset=True, strip=False)
|
38
|
+
|
39
|
+
#### Helper Functions ########################################################################################
|
40
|
+
|
41
|
+
|
42
|
+
class CustomArgParse(argparse.ArgumentParser):
|
43
|
+
"""
|
44
|
+
This class takes the argparse.ArgumentParser function as a superclass and overwrites the argparse error
|
45
|
+
function. Every time that argparse calls the error function the following error function will be executed.
|
46
|
+
"""
|
47
|
+
|
48
|
+
def error(self, message):
|
49
|
+
"""
|
50
|
+
This function overwrites the standard argparse error function
|
51
|
+
"""
|
52
|
+
print(task_error(text="ARGPARSE verify arguments", changed=False))
|
53
|
+
print("'ARGPARSE verify arguments' -> ArgparseResponse <Success: False>\n")
|
54
|
+
print(f"error: {message}\n")
|
55
|
+
self.print_help()
|
56
|
+
print("\n")
|
57
|
+
sys.exit(1)
|
58
|
+
|
59
|
+
|
60
|
+
class CustomArgParseWidthFormatter(argparse.RawTextHelpFormatter):
|
61
|
+
"""
|
62
|
+
This class can be specified as formatter_class argument in argparse.ArgumentParser. This solution is
|
63
|
+
preferred as formatter_class argument expects to use a class, not a class instance.
|
64
|
+
"""
|
65
|
+
|
66
|
+
def __init__(self, prog) -> None:
|
67
|
+
super().__init__(prog, width=100)
|
68
|
+
|
69
|
+
|
70
|
+
def get_env_vars(envs: List[str], task_text: str) -> Tuple:
|
71
|
+
"""
|
72
|
+
This function loads the environment variables from a list and returns them as a Tuple. If an environment
|
73
|
+
variable is not found, the script will exit with an error message.
|
74
|
+
"""
|
75
|
+
# Initialize an empty dictionary
|
76
|
+
env_vars = {}
|
77
|
+
|
78
|
+
try:
|
79
|
+
# Load the environment variables from the list
|
80
|
+
for env in envs:
|
81
|
+
env_vars[env] = os.environ[env]
|
82
|
+
|
83
|
+
except KeyError as error:
|
84
|
+
# Print the error message and exit the script
|
85
|
+
print(task_error(text=task_text, changed=False))
|
86
|
+
print(f"'Load environment variable {error}' -> OS.EnvironResponse <Success: False>")
|
87
|
+
print(f"-> Environment variable {error} not found\n")
|
88
|
+
sys.exit(1)
|
89
|
+
|
90
|
+
# Return the environment variables as a dictionary
|
91
|
+
return env_vars
|
92
|
+
|
93
|
+
|
94
|
+
def get_dict_value_by_path(data_dict, map_list):
|
95
|
+
"""
|
96
|
+
Access a nested object in data_dict by map_list sequence.
|
97
|
+
"""
|
98
|
+
return reduce(operator.getitem, map_list, data_dict)
|
99
|
+
|
100
|
+
|
101
|
+
def set_dict_value_by_path(data_dict, map_list, value):
|
102
|
+
"""
|
103
|
+
Set a value in a nested object in data_dict by map_list sequence.
|
104
|
+
"""
|
105
|
+
get_dict_value_by_path(data_dict, map_list[:-1])[map_list[-1]] = value
|
106
|
+
|
107
|
+
|
108
|
+
def del_dict_key_value_by_path(data_dict, map_list):
|
109
|
+
"""
|
110
|
+
Delete a key-value in a nested object in data_dict by map_list sequence.
|
111
|
+
"""
|
112
|
+
del get_dict_value_by_path(data_dict, map_list[:-1])[map_list[-1]]
|
113
|
+
|
114
|
+
|
115
|
+
def get_rel_path_from_cwd(path):
|
116
|
+
"""
|
117
|
+
TBD
|
118
|
+
"""
|
119
|
+
# Compute the absolute path if path is a relative path
|
120
|
+
if not os.path.isabs(path):
|
121
|
+
path = os.path.join(os.path.abspath(os.path.dirname(__main__.__file__)), path)
|
122
|
+
|
123
|
+
# Get the relative path from the current working directory to to the end of the absolute path
|
124
|
+
relative_path_from_cwd = os.path.relpath(path, os.getcwd())
|
125
|
+
|
126
|
+
return relative_path_from_cwd
|
127
|
+
|
128
|
+
|
129
|
+
def load_multiple_yaml_files_to_string(file_list: list, silent: bool = False) -> str:
|
130
|
+
"""
|
131
|
+
This function loads multiple yaml files into a string
|
132
|
+
"""
|
133
|
+
# pylint: disable=invalid-name
|
134
|
+
|
135
|
+
task_text = "Load multiple yaml files to string"
|
136
|
+
yaml_string = str()
|
137
|
+
|
138
|
+
# Exit the function if the file_list is empty
|
139
|
+
if not file_list:
|
140
|
+
print(task_error(text=task_text, changed=False))
|
141
|
+
print(f"'{task_text}' -> NornirResponse <Success: False>")
|
142
|
+
print("-> No files provided for yaml string loading ('file_list' is empty)")
|
143
|
+
return yaml_string
|
144
|
+
|
145
|
+
# Load the yaml files from file_list and concatinate them to one string
|
146
|
+
# TestsProcessor templates the yaml string with Jinja2 and loads the yaml string into a dict
|
147
|
+
try:
|
148
|
+
for file in file_list:
|
149
|
+
with open(file, "r", encoding="utf-8") as f:
|
150
|
+
for line in f.readlines():
|
151
|
+
yaml_string += line if "---" not in line else ""
|
152
|
+
yaml_string += "\n"
|
153
|
+
|
154
|
+
if not silent:
|
155
|
+
print(task_info(text=task_text, changed=False))
|
156
|
+
print(f"'{task_text}' -> NornirResponse <Success: True>")
|
157
|
+
for file in file_list:
|
158
|
+
print(f"-> {file}")
|
159
|
+
|
160
|
+
except FileNotFoundError as error:
|
161
|
+
print(task_error(text=task_text, changed=False))
|
162
|
+
print(f"'{task_text}' -> NornirResponse <Success: False>")
|
163
|
+
print(f"-> {error}")
|
164
|
+
|
165
|
+
return yaml_string
|
166
|
+
|
167
|
+
|
168
|
+
def load_yaml_file(file: str, text: str = False, silent: bool = False, verbose: bool = False):
|
169
|
+
"""
|
170
|
+
Load the yaml file into a variable.
|
171
|
+
"""
|
172
|
+
text = text if text else "YAML Load File"
|
173
|
+
success_message = (
|
174
|
+
f"{task_name(text=text)}\n"
|
175
|
+
f"{task_info(text=text, changed=False)}\n"
|
176
|
+
f"'{text}' -> PythonResult <Success: True>"
|
177
|
+
)
|
178
|
+
error_message = (
|
179
|
+
f"{task_name(text=text)}\n"
|
180
|
+
f"{task_error(text=text, changed=False)}\n"
|
181
|
+
f"'{text}' -> PythonResult <Success: False>"
|
182
|
+
)
|
183
|
+
|
184
|
+
try:
|
185
|
+
with open(file, "r", encoding="utf-8") as stream:
|
186
|
+
yaml_dict = yaml.safe_load(stream)
|
187
|
+
|
188
|
+
if not silent:
|
189
|
+
print(success_message)
|
190
|
+
print(f"-> Loaded YAML file: {file}")
|
191
|
+
if verbose:
|
192
|
+
print("\n" + json.dumps(yaml_dict, indent=4))
|
193
|
+
|
194
|
+
# Return the loaded yaml file as python dictionary
|
195
|
+
return yaml_dict
|
196
|
+
|
197
|
+
except (TypeError, FileNotFoundError, yaml.parser.ParserError) as yaml_error:
|
198
|
+
print(error_message)
|
199
|
+
print(f"\n{yaml_error}")
|
200
|
+
|
201
|
+
# Return an empty python dictionary
|
202
|
+
return {}
|
203
|
+
|
204
|
+
|
205
|
+
def construct_filename_with_current_date(filename: str, name: Union[str, None], silent: bool = False) -> str:
|
206
|
+
"""
|
207
|
+
Construct the new path and filename from the filename argument string variable. The current date will be
|
208
|
+
added at the end of the filename. The function returns the new constructed filename.
|
209
|
+
"""
|
210
|
+
# Set a custom name for stdout print if name is set
|
211
|
+
name = name if name else "PYTHON construct file path with current date"
|
212
|
+
|
213
|
+
# Create some variables to construct the destination path and filename
|
214
|
+
# Get the path and the filename from file variable string
|
215
|
+
path, filename = os.path.split(filename)
|
216
|
+
|
217
|
+
# Create the path folder if it don't exists
|
218
|
+
if not os.path.exists(path):
|
219
|
+
os.makedirs(path)
|
220
|
+
|
221
|
+
# Get the filename and the extension from the filename variable
|
222
|
+
filename, file_extension = os.path.splitext(filename)
|
223
|
+
|
224
|
+
# Destination filename with current date time in format YYYY-mm-dd
|
225
|
+
filename = f"{path}/{filename}_{datetime.today().date()}{file_extension}"
|
226
|
+
|
227
|
+
if not silent:
|
228
|
+
print_task_name(text=name)
|
229
|
+
print(task_info(text=name, changed=False))
|
230
|
+
print(f"'{name}' -> PythonResult <Success: True>")
|
231
|
+
print(f"-> Constructed {filename}")
|
232
|
+
|
233
|
+
return filename
|
234
|
+
|
235
|
+
|
236
|
+
def get_pandas_column_width(df: DataFrame) -> List[int]: # pylint: disable=invalid-name
|
237
|
+
"""
|
238
|
+
Helper function to get the width of each pandas dataframe column.
|
239
|
+
"""
|
240
|
+
# Find the maximum length of the index column
|
241
|
+
idx_max = max([len(str(s)) for s in df.index.values] + [len(str(df.index.name))])
|
242
|
+
|
243
|
+
# Concatenate this to the max of the lengths of column name and its values for each column, left to right
|
244
|
+
return [idx_max] + [max([len(str(s)) for s in df[col].values] + [len(col)]) for col in df.columns]
|
245
|
+
|
246
|
+
|
247
|
+
def list_flatten(original_list: list) -> list:
|
248
|
+
"""
|
249
|
+
This function creates with recursion a flat list from a list of lists and strings or other data types.
|
250
|
+
"""
|
251
|
+
new_list = []
|
252
|
+
for item in original_list:
|
253
|
+
if isinstance(item, list):
|
254
|
+
new_list.extend(list_flatten(item))
|
255
|
+
else:
|
256
|
+
new_list.append(item)
|
257
|
+
|
258
|
+
return new_list
|
259
|
+
|
260
|
+
|
261
|
+
def compute_hash(source: str, algorithm: str = "md5") -> str:
|
262
|
+
"""
|
263
|
+
This is a helper function which takes a file path or a http url as argument and computes a md5 hash which
|
264
|
+
is the return of the function. Additionally the default hash algorithm can be changed from md5 to sha1,
|
265
|
+
sha265, sha384 or sha512.
|
266
|
+
"""
|
267
|
+
# Use mapping with lambda to avoid long if elif else statements
|
268
|
+
algorithms = {
|
269
|
+
"md5": hashlib.md5, # nosec
|
270
|
+
"sha1": hashlib.sha1, # nosec
|
271
|
+
"sha256": hashlib.sha256, # nosec
|
272
|
+
"sha384": hashlib.sha384, # nosec
|
273
|
+
"sha512": hashlib.sha512, # nosec
|
274
|
+
}
|
275
|
+
# Execute the correct lambda hash function by the dictionary key which matches the algorithm argument
|
276
|
+
hash_obj = algorithms[algorithm]()
|
277
|
+
|
278
|
+
if source.lower().startswith("http"):
|
279
|
+
# Bandit "B310: urllib_urlopen" if solved to raise a ValueError is the value starts not with http
|
280
|
+
if source.lower().startswith("http"):
|
281
|
+
response = urllib.request.Request(source)
|
282
|
+
with urllib.request.urlopen(response) as response: # nosec
|
283
|
+
for chunk in iter(lambda: response.read(4096), b""):
|
284
|
+
hash_obj.update(chunk)
|
285
|
+
else:
|
286
|
+
raise ValueError from None
|
287
|
+
else:
|
288
|
+
with open(source, "rb") as file:
|
289
|
+
for chunk in iter(lambda: file.read(4096), b""):
|
290
|
+
hash_obj.update(chunk)
|
291
|
+
|
292
|
+
return hash_obj.hexdigest()
|
293
|
+
|
294
|
+
|
295
|
+
def iterate_all(iterable: Union[list, dict], returned: str = "key") -> Generator:
|
296
|
+
"""Returns an iterator that returns all keys or values of a (nested) iterable.
|
297
|
+
Arguments:
|
298
|
+
- iterable: <list> or <dictionary>
|
299
|
+
- returned: <string> "key" or "value" or <tuple of strings> "key-value"
|
300
|
+
Returns:
|
301
|
+
- <Generator>
|
302
|
+
"""
|
303
|
+
if isinstance(iterable, dict):
|
304
|
+
for key, value in iterable.items():
|
305
|
+
if returned == "key":
|
306
|
+
yield key
|
307
|
+
elif returned == "value":
|
308
|
+
if not isinstance(value, dict) or isinstance(value, list):
|
309
|
+
yield value
|
310
|
+
elif returned == "key-value":
|
311
|
+
if not isinstance(value, dict) or isinstance(value, list):
|
312
|
+
yield key, value
|
313
|
+
else:
|
314
|
+
raise ValueError("'returned' keyword only accepts 'key' or 'value' or 'key-value'.")
|
315
|
+
for ret in iterate_all(value, returned=returned):
|
316
|
+
yield ret
|
317
|
+
elif isinstance(iterable, list):
|
318
|
+
for item in iterable:
|
319
|
+
for ret in iterate_all(item, returned=returned):
|
320
|
+
yield ret
|
321
|
+
|
322
|
+
|
323
|
+
def transform_env(iterable: dict, startswith: str = "env_") -> dict:
|
324
|
+
"""
|
325
|
+
This function loops over a nested dictionary and if the key startswith the specific string and the value
|
326
|
+
is a string, it loads the environment variable specified by the value and replace the value with the
|
327
|
+
environment variable.
|
328
|
+
"""
|
329
|
+
for key, value in iterable.copy().items():
|
330
|
+
# If Value == DICT -> Continue with nested dict
|
331
|
+
if isinstance(value, dict):
|
332
|
+
iterable[key] = transform_env(value, startswith)
|
333
|
+
# If Value == LIST -> Replace the value of each list item with the env variable
|
334
|
+
elif isinstance(value, list):
|
335
|
+
if key.startswith(startswith):
|
336
|
+
for index, item in enumerate(value.copy()):
|
337
|
+
iterable[key][index] = os.environ[item]
|
338
|
+
# If Value == STR -> Replace the value with the env variable
|
339
|
+
elif isinstance(value, str):
|
340
|
+
if key.startswith(startswith):
|
341
|
+
iterable[key] = os.environ[value]
|
342
|
+
|
343
|
+
return iterable
|
344
|
+
|
345
|
+
|
346
|
+
#### Nornir Helper Functions #################################################################################
|
347
|
+
|
348
|
+
|
349
|
+
def nr_filter_by_role_and_tag(
|
350
|
+
nr: Nornir, task_text: str = None, role: str = None, tags: list[str] = None, silent: bool = False
|
351
|
+
):
|
352
|
+
"""
|
353
|
+
TBD
|
354
|
+
"""
|
355
|
+
# pylint: disable=invalid-name
|
356
|
+
|
357
|
+
if not task_text:
|
358
|
+
task_text = "NORNIR filter inventory"
|
359
|
+
provided_filters = ""
|
360
|
+
|
361
|
+
if role:
|
362
|
+
# Filter by device role (A device can have only one role)
|
363
|
+
nr = nr.filter(F(role__slug__contains=role))
|
364
|
+
provided_filters += f"-> Provided role: '{role}'\n"
|
365
|
+
|
366
|
+
if tags:
|
367
|
+
# Loop over all provided tags and filter by each tag
|
368
|
+
for tag in tags:
|
369
|
+
nr = nr.filter(F(tags__contains=tag))
|
370
|
+
provided_filters += f"-> Provided tags: {tags}\n"
|
371
|
+
|
372
|
+
# If the filteres object have no hosts, exit with a error message
|
373
|
+
if not nr.inventory.hosts.keys():
|
374
|
+
print(task_error(text=task_text, changed=False))
|
375
|
+
print(f"'{task_text} by role and/or tags' -> NornirResult <Success: False>")
|
376
|
+
exit_error(
|
377
|
+
task_text=task_text,
|
378
|
+
text="ALERT: NO HOST WITH ROLE AND/OR TAGS IN NORNIR INVENTORY",
|
379
|
+
msg=[
|
380
|
+
"-> Analyse the Nornir inventory and filter for a role and/or tags assigned to hosts",
|
381
|
+
f"-> {provided_filters}",
|
382
|
+
],
|
383
|
+
)
|
384
|
+
|
385
|
+
if not silent:
|
386
|
+
print(task_info(text=task_text, changed=False))
|
387
|
+
print(
|
388
|
+
f"'{task_text} by role and/or tags' -> NornirResult <Success: True>\n" + f"{provided_filters}",
|
389
|
+
)
|
390
|
+
|
391
|
+
return nr
|
392
|
+
|
393
|
+
|
394
|
+
def nr_filter_by_hosts(nr: Nornir, hosts: Union[str, list], task_text: str = None, silent: bool = False):
|
395
|
+
"""
|
396
|
+
TBD
|
397
|
+
"""
|
398
|
+
# pylint: disable=invalid-name
|
399
|
+
|
400
|
+
if not task_text:
|
401
|
+
task_text = "NORNIR filter inventory"
|
402
|
+
|
403
|
+
if isinstance(hosts, str):
|
404
|
+
# Create a list from the comma separated hosts argument
|
405
|
+
hosts = hosts.split(",")
|
406
|
+
|
407
|
+
# Use NornirSalt FFun Filter-List option to filter on a list of hosts
|
408
|
+
nr_filtered = FFun(nr, FL=hosts)
|
409
|
+
|
410
|
+
# Create a list with all filtered Nornir hosts for verification
|
411
|
+
nr_hosts = list(nr_filtered.inventory.hosts.keys())
|
412
|
+
|
413
|
+
# Verify that each host in from the hosts argument is part of the filtered Nornir inventory, else
|
414
|
+
# the diff host will be in the list host_diff list
|
415
|
+
host_diff = [host for host in hosts if host not in nr_hosts]
|
416
|
+
if host_diff:
|
417
|
+
print(task_error(text=task_text, changed=False))
|
418
|
+
print(f"'{task_text} by hosts' -> NornirResponse <Success: False>")
|
419
|
+
for host in host_diff:
|
420
|
+
print(f"-> {host}")
|
421
|
+
exit_error(
|
422
|
+
task_text=task_text,
|
423
|
+
text="ALERT: ONE OR MORE HOSTS ARE NOT PART OF THE NORNIR INVENTORY",
|
424
|
+
msg="-> Analyse the Nornir inventory and filter for an existing host",
|
425
|
+
)
|
426
|
+
|
427
|
+
if not silent:
|
428
|
+
print(task_info(text=task_text, changed=False))
|
429
|
+
print(f"'{task_text} by hosts' -> NornirResponse <Success: True>")
|
430
|
+
for host in nr_hosts:
|
431
|
+
print(f"-> {host}")
|
432
|
+
|
433
|
+
return nr_filtered
|
434
|
+
|
435
|
+
|
436
|
+
def nr_filter_args(nr: Nornir, args: argparse.Namespace) -> Nornir:
|
437
|
+
"""
|
438
|
+
This function filters the Nornir inventory with a tag or a host argument provided by argparse. Prior
|
439
|
+
Argparse validation needs to ensure that only one argument is present and that the tag or host argument
|
440
|
+
creates a correct inventory filtering will be verified. The new filtered Nornir object will be returned
|
441
|
+
or the script terminates with an error message.
|
442
|
+
"""
|
443
|
+
# pylint: disable=invalid-name
|
444
|
+
|
445
|
+
task_text = "NORNIR filter inventory"
|
446
|
+
print_task_name(task_text)
|
447
|
+
|
448
|
+
# If the --hosts argument is set, verify that the host exist
|
449
|
+
if hasattr(args, "hosts"):
|
450
|
+
nr = nr_filter_by_hosts(nr=nr, hosts=args.hosts, silent=False)
|
451
|
+
|
452
|
+
return nr
|
453
|
+
|
454
|
+
# If the --role and/or --tags argument is set, verify that the tag has hosts assigned to
|
455
|
+
if hasattr(args, "role") or hasattr(args, "tags"):
|
456
|
+
role = args.role if hasattr(args, "role") else None
|
457
|
+
tags = args.tags.split(",") if hasattr(args, "tags") else None
|
458
|
+
nr = nr_filter_by_role_and_tag(nr=nr, role=role, tags=tags, silent=False)
|
459
|
+
|
460
|
+
return nr
|
461
|
+
|
462
|
+
exit_error(
|
463
|
+
task_text=task_text,
|
464
|
+
text="ALERT: NOT SUPPORTET ARGPARSE ARGUMENT FOR NORNIR INVENTORY FILTERING!",
|
465
|
+
msg="-> Analyse the python function for missing Argparse filtering",
|
466
|
+
)
|
467
|
+
|
468
|
+
|
469
|
+
def nr_filter_inventory_from_host_list(nr: Nornir, filter_reason: str, host_list: List[str]) -> Nornir:
|
470
|
+
"""
|
471
|
+
This function takes a Nornir object, a filter reason to print in Nornir style to std-out and a list of
|
472
|
+
hosts. It can be a list of hosts or a list of strings where the hostname is part of the string, as the
|
473
|
+
function checks if the hostname from the Nornir object is in that host list or list of strings. Every host
|
474
|
+
that matches will be added to the new filter target and the new filtered Nornir object will be returned.
|
475
|
+
"""
|
476
|
+
# pylint: disable=invalid-name
|
477
|
+
|
478
|
+
task_text = "NORNIR re-filter inventory"
|
479
|
+
print_task_name(task_text)
|
480
|
+
|
481
|
+
# Re-filter the Nornir inventory only on hosts that need to be reconfigured.
|
482
|
+
# Create an empty list to fill with all hosts that need reconfiguration.
|
483
|
+
filter_target = []
|
484
|
+
|
485
|
+
# Iterate over all diff files and add the host to the filter_target list if the Nornir inventory host is
|
486
|
+
# in the diff file name
|
487
|
+
for item in host_list:
|
488
|
+
for host in nr.inventory.hosts.keys():
|
489
|
+
if host == item:
|
490
|
+
filter_target.append(host)
|
491
|
+
break
|
492
|
+
|
493
|
+
# Remove possible duplicate hosts in the list
|
494
|
+
filter_target = list(set(filter_target))
|
495
|
+
|
496
|
+
# Use Nornir-Salt FFun Filter-List option to filter on a list of hosts
|
497
|
+
nr = FFun(nr, FL=filter_target)
|
498
|
+
|
499
|
+
print(task_info(text=task_text, changed=False))
|
500
|
+
print(f"'{task_text} for hosts' -> NornirResponse <Success: True>")
|
501
|
+
print(f"{filter_reason}")
|
502
|
+
for host in nr.inventory.hosts.keys():
|
503
|
+
print(f"-> {host}")
|
504
|
+
|
505
|
+
return nr
|
506
|
+
|
507
|
+
|
508
|
+
#### Nornir Print Functions ##################################################################################
|
509
|
+
|
510
|
+
|
511
|
+
def print_script_banner(title: str, font: str = "small", text: str = None) -> None:
|
512
|
+
"""
|
513
|
+
Print a custom script banner with pyfiglet.
|
514
|
+
"""
|
515
|
+
banner = figlet_format(title, font, width=110)
|
516
|
+
print("\n")
|
517
|
+
for line in banner.splitlines(): # Workaround to print in Azure DevOps all lines in color
|
518
|
+
print(f"{Style.BRIGHT}{Fore.GREEN}{line}")
|
519
|
+
if text:
|
520
|
+
print(f"{Style.BRIGHT}{Fore.GREEN}{text}")
|
521
|
+
|
522
|
+
|
523
|
+
def print_task_title(title: str) -> None:
|
524
|
+
"""
|
525
|
+
Prints a Nornir style title.
|
526
|
+
"""
|
527
|
+
msg = f"**** {title} "
|
528
|
+
print(f"\n{Style.BRIGHT}{Fore.GREEN}{msg}{'*' * (90 - len(msg))}{Fore.RESET}{Style.RESET_ALL}")
|
529
|
+
|
530
|
+
|
531
|
+
def print_task_name(text: str) -> None:
|
532
|
+
"""
|
533
|
+
Prints a Nornir style host task title.
|
534
|
+
"""
|
535
|
+
msg = f"{text} "
|
536
|
+
print(f"\n{Style.BRIGHT}{Fore.CYAN}{msg}{'*' * (90 - len(msg))}{Fore.RESET}{Style.RESET_ALL}")
|
537
|
+
|
538
|
+
|
539
|
+
def task_name(text: str) -> None:
|
540
|
+
"""
|
541
|
+
Prints a Nornir style host task title.
|
542
|
+
"""
|
543
|
+
msg = f"{text} "
|
544
|
+
return f"\n{Style.BRIGHT}{Fore.CYAN}{msg}{'*' * (90 - len(msg))}{Fore.RESET}{Style.RESET_ALL}"
|
545
|
+
|
546
|
+
|
547
|
+
def task_host(host: str, changed: bool) -> str:
|
548
|
+
"""
|
549
|
+
Returns a Nornir style host task name.
|
550
|
+
"""
|
551
|
+
msg = f"* {host} ** changed : {str(changed)} "
|
552
|
+
return f"{Style.BRIGHT}{Fore.BLUE}{msg}{'*' * (90 - len(msg))}{Fore.RESET}{Style.RESET_ALL}"
|
553
|
+
|
554
|
+
|
555
|
+
def task_result(text: str, changed: bool, level_name, failed: bool = False) -> str:
|
556
|
+
"""
|
557
|
+
Returns a Nornir style task info or error message based on the arguments.
|
558
|
+
This function should be the successor of task_info and task_error.
|
559
|
+
"""
|
560
|
+
if failed or level_name == "ERROR":
|
561
|
+
color = Fore.RED
|
562
|
+
elif changed:
|
563
|
+
color = Fore.YELLOW
|
564
|
+
else:
|
565
|
+
color = Fore.GREEN
|
566
|
+
|
567
|
+
msg = f"---- {text} ** changed : {str(changed)} "
|
568
|
+
return f"{Style.BRIGHT}{color}{msg}{'-' * (90 - len(msg))} {level_name}{Fore.RESET}{Style.RESET_ALL}"
|
569
|
+
|
570
|
+
|
571
|
+
def task_info(text: str, changed: bool) -> str:
|
572
|
+
"""
|
573
|
+
Returns a Nornir style task info message.
|
574
|
+
"""
|
575
|
+
color = Fore.YELLOW if changed else Fore.GREEN
|
576
|
+
msg = f"---- {text} ** changed : {str(changed)} "
|
577
|
+
return f"{Style.BRIGHT}{color}{msg}{'-' * (90 - len(msg))} INFO{Fore.RESET}{Style.RESET_ALL}"
|
578
|
+
|
579
|
+
|
580
|
+
def task_error(text: str, changed: bool) -> str:
|
581
|
+
"""
|
582
|
+
Returns a Nornir style task error message.
|
583
|
+
"""
|
584
|
+
msg = f"---- {text} ** changed : {str(changed)} "
|
585
|
+
return f"{Style.BRIGHT}{Fore.RED}{msg}{'-' * (90 - len(msg))} ERROR{Fore.RESET}{Style.RESET_ALL}"
|
586
|
+
|
587
|
+
|
588
|
+
def exit_info(
|
589
|
+
task_text: str, text: str = False, msg: Union[list[str], str] = False, changed: bool = False
|
590
|
+
) -> NoReturn:
|
591
|
+
"""
|
592
|
+
TBD
|
593
|
+
"""
|
594
|
+
# Set text to task_text if text if False
|
595
|
+
text = text if text else task_text
|
596
|
+
|
597
|
+
# Print the info and exit the script with exit code 0
|
598
|
+
print(task_info(text=task_text, changed=changed))
|
599
|
+
print(f"\u2728 {text.upper()} \u2728")
|
600
|
+
if isinstance(msg, list):
|
601
|
+
for line in msg:
|
602
|
+
print(f"{Style.BRIGHT}{Fore.GREEN}{line}")
|
603
|
+
elif isinstance(msg, str):
|
604
|
+
print(f"{Style.BRIGHT}{Fore.GREEN}{msg}")
|
605
|
+
print("\n")
|
606
|
+
sys.exit(0)
|
607
|
+
|
608
|
+
|
609
|
+
def exit_error(task_text: str, text: str = False, msg: Union[list[str], str, None] = "default") -> NoReturn:
|
610
|
+
"""
|
611
|
+
TBD
|
612
|
+
"""
|
613
|
+
# Set text to task_text if text if False
|
614
|
+
text = text if text else task_text
|
615
|
+
|
616
|
+
# Print the error and exit the script with exit code 1
|
617
|
+
print(task_error(text=task_text, changed=False))
|
618
|
+
print(f"\U0001f4a5 {text.upper()} \U0001f4a5")
|
619
|
+
if isinstance(msg, list):
|
620
|
+
for line in msg:
|
621
|
+
print(f"{Style.BRIGHT}{Fore.RED}{line}")
|
622
|
+
elif isinstance(msg, str) and "default" not in msg:
|
623
|
+
print(f"{Style.BRIGHT}{Fore.RED}{msg}")
|
624
|
+
elif "default" in msg:
|
625
|
+
print(
|
626
|
+
f"{Style.BRIGHT}{Fore.RED}-> Analyse the Nornir output for failed task results\n"
|
627
|
+
"-> May apply Nornir inventory changes and run the script again"
|
628
|
+
)
|
629
|
+
print("\n")
|
630
|
+
sys.exit(1)
|
631
|
+
|
632
|
+
|
633
|
+
#### Nornir print_result Deviation ###########################################################################
|
634
|
+
|
635
|
+
|
636
|
+
def _print_individual_result(
|
637
|
+
result: Result, result_sub_list: bool, attrs: List[str], failed: bool, severity_level: int
|
638
|
+
) -> None:
|
639
|
+
"""
|
640
|
+
This function is part of the deviation of the official Nornir print_result function.
|
641
|
+
"""
|
642
|
+
|
643
|
+
if result.severity_level < severity_level:
|
644
|
+
return
|
645
|
+
|
646
|
+
# Get the task level INFO or ERROR, the colorama color and the changed boolian
|
647
|
+
level_name = logging.getLevelName(result.severity_level)
|
648
|
+
changed = "" if result.changed is None else result.changed
|
649
|
+
|
650
|
+
for attribute in attrs:
|
651
|
+
item = getattr(result, attribute, "")
|
652
|
+
if isinstance(item, BaseException):
|
653
|
+
# Deviation to print the dsc_nornir task_result function
|
654
|
+
print(task_result(text=result.name, changed=changed, level_name=level_name, failed=failed))
|
655
|
+
# for consistency between py3.6 and py3.7
|
656
|
+
print(f"{item.__class__.__name__}{item.args}")
|
657
|
+
|
658
|
+
# Deviation to print the dsc_nornir task_result function
|
659
|
+
elif item and result_sub_list and isinstance(item, list):
|
660
|
+
for list_item in item:
|
661
|
+
print(list_item)
|
662
|
+
|
663
|
+
elif item and not isinstance(item, str):
|
664
|
+
if isinstance(item, OrderedDict):
|
665
|
+
# Deviation to print the dsc_nornir task_result function
|
666
|
+
print(task_result(text=result.name, changed=changed, level_name=level_name, failed=failed))
|
667
|
+
print(json.dumps(item, indent=4))
|
668
|
+
else:
|
669
|
+
# Deviation to print the dsc_nornir task_result function
|
670
|
+
print(task_result(text=result.name, changed=changed, level_name=level_name, failed=failed))
|
671
|
+
pprint.pprint(item, indent=4)
|
672
|
+
elif item:
|
673
|
+
# Deviation to print the dsc_nornir task_result function
|
674
|
+
print(task_result(text=result.name, changed=changed, level_name=level_name, failed=failed))
|
675
|
+
print(item)
|
676
|
+
|
677
|
+
|
678
|
+
def _print_result(
|
679
|
+
result: Result,
|
680
|
+
result_sub_list: bool = False,
|
681
|
+
attrs: List[str] = None,
|
682
|
+
failed: bool = False,
|
683
|
+
severity_level: int = logging.INFO,
|
684
|
+
) -> None:
|
685
|
+
"""
|
686
|
+
This function is part of the deviation of the official Nornir print_result function.
|
687
|
+
"""
|
688
|
+
|
689
|
+
# If attrs is not None use attrs else use the list below
|
690
|
+
attrs = attrs or ["diff", "result", "stdout"]
|
691
|
+
if isinstance(attrs, str):
|
692
|
+
attrs = [attrs]
|
693
|
+
|
694
|
+
if isinstance(result, AggregatedResult):
|
695
|
+
# Deviation to print the dsc_nornir print_task_name function
|
696
|
+
print_task_name(text=result.name)
|
697
|
+
|
698
|
+
for host, host_data in sorted(result.items()):
|
699
|
+
changed = "" if host_data.changed is None else host_data.changed
|
700
|
+
# Deviation to print the dsc_nornir task_host function
|
701
|
+
print(task_host(host=host, changed=changed))
|
702
|
+
# Recursion to print all MultiResult objects of the Nornir AggregatedResult object
|
703
|
+
_print_result(host_data, result_sub_list, attrs, failed, severity_level)
|
704
|
+
|
705
|
+
elif isinstance(result, MultiResult):
|
706
|
+
# Deviation to not print the task MultiResult or Subtask failed result
|
707
|
+
if not (str(result[0]).startswith("MultiResult") or str(result[0]).startswith("Subtask")):
|
708
|
+
_print_individual_result(result[0], result_sub_list, attrs, failed, severity_level)
|
709
|
+
# Recursion to print all results of the Nornir MultiResult object
|
710
|
+
for result_item in result[1:]:
|
711
|
+
_print_result(result_item, result_sub_list, attrs, failed, severity_level)
|
712
|
+
|
713
|
+
elif isinstance(result, Result):
|
714
|
+
# Print the Nornir Result object
|
715
|
+
_print_individual_result(result, result_sub_list, attrs, failed, severity_level)
|
716
|
+
|
717
|
+
|
718
|
+
def print_result(
|
719
|
+
result: Result,
|
720
|
+
result_sub_list: bool = False,
|
721
|
+
attrs: List[str] = None,
|
722
|
+
failed: bool = False,
|
723
|
+
severity_level: int = logging.INFO,
|
724
|
+
) -> None:
|
725
|
+
"""
|
726
|
+
This function is a deviation of the official Nornir print_result function.
|
727
|
+
Prints an object of type `nornir.core.task.Result`
|
728
|
+
Arguments:
|
729
|
+
result: from a previous task
|
730
|
+
attrs: Which attributes you want to print
|
731
|
+
failed: if ``True`` assume the task failed
|
732
|
+
severity_level: Print only errors with this severity level or higher
|
733
|
+
"""
|
734
|
+
# pylint: disable=consider-using-with
|
735
|
+
|
736
|
+
lock = threading.Lock()
|
737
|
+
lock.acquire()
|
738
|
+
try:
|
739
|
+
_print_result(result, result_sub_list, attrs, failed, severity_level)
|
740
|
+
finally:
|
741
|
+
lock.release()
|