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,202 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
This module load the NetBox device inventory, find active management IP addresses, and verify their integrity.
|
4
|
+
The Main function is intended to import and execute by other scripts.
|
5
|
+
"""
|
6
|
+
|
7
|
+
|
8
|
+
import sys
|
9
|
+
import subprocess # nosec
|
10
|
+
from nornir_collection.netbox.utils import get_nb_resources
|
11
|
+
from nornir_collection.utils import (
|
12
|
+
print_task_title,
|
13
|
+
task_name,
|
14
|
+
exit_error,
|
15
|
+
load_yaml_file,
|
16
|
+
task_result,
|
17
|
+
)
|
18
|
+
|
19
|
+
|
20
|
+
__author__ = "Willi Kubny"
|
21
|
+
__maintainer__ = "Willi Kubny"
|
22
|
+
__version__ = "1.0"
|
23
|
+
__license__ = "MIT"
|
24
|
+
__email__ = "willi.kubny@dreyfusbank.ch"
|
25
|
+
__status__ = "Production"
|
26
|
+
|
27
|
+
|
28
|
+
def main( # pylint: disable=dangerous-default-value,too-many-arguments,too-many-statements
|
29
|
+
nr_config: str,
|
30
|
+
mgmt_subnets: list[str],
|
31
|
+
add_addresses: list[str] = [],
|
32
|
+
exclude_addresses: list[str] = [],
|
33
|
+
) -> None:
|
34
|
+
"""
|
35
|
+
Main function is intended to import and execute by other scripts.
|
36
|
+
It verify the integrity of NetBox primary device management IP addresses.
|
37
|
+
|
38
|
+
* Args:
|
39
|
+
* nr_config (str): Path to the Nornir configuration file.
|
40
|
+
* mgmt_subnets (list[str]): List of management subnets to scan for active IP addresses.
|
41
|
+
* add_addresses (list[str], optional): List of additional IP addresses to scan. Defaults to [].
|
42
|
+
* exclude_addresses (list[str], optional): List of IP addresses to exclude. Defaults to [].
|
43
|
+
|
44
|
+
* Steps:
|
45
|
+
* Loads the NetBox device inventory.
|
46
|
+
* Finds all active NetBox primary device management IP addresses.
|
47
|
+
* Scans all provided management subnets and additional addresses for active IP addresses using fping.
|
48
|
+
* Verifies the integrity of NetBox primary device management IP addresses by checking:
|
49
|
+
* If any NetBox primary IP addresses are missing from the scan.
|
50
|
+
* If any active IP addresses from the scan are not part of the NetBox inventory.
|
51
|
+
|
52
|
+
* Exits:
|
53
|
+
* Exits with code 1 if the Nornir configuration file is empty or if any task fails.
|
54
|
+
"""
|
55
|
+
# pylint: disable=invalid-name,too-many-locals
|
56
|
+
|
57
|
+
#### Load NetBox Device Inventory ########################################################################
|
58
|
+
|
59
|
+
task_title = "Load NetBox Device Inventory"
|
60
|
+
print_task_title(title=task_title)
|
61
|
+
|
62
|
+
# Load the Nornir yaml config file as dict and print a error message
|
63
|
+
nr_config_dict = load_yaml_file(
|
64
|
+
file=nr_config, text="Load Nornir Config File", silent=False, verbose=False
|
65
|
+
)
|
66
|
+
# Check the loaded config file and exit the script with exit code 1 if the dict is empty
|
67
|
+
if not nr_config_dict:
|
68
|
+
sys.exit(1)
|
69
|
+
|
70
|
+
task_text = "Load NetBox Device Inventory"
|
71
|
+
print(task_name(text=task_text))
|
72
|
+
|
73
|
+
# Get the NetBox URL (Authentication token will be loaded as nb_token env variable)
|
74
|
+
nb_url = nr_config_dict["inventory"]["options"]["nb_url"]
|
75
|
+
params = {"status": "active", "has_primary_ip": True}
|
76
|
+
nb_devices = get_nb_resources(url=f"{nb_url}/api/dcim/devices/?limit=0", params=params)
|
77
|
+
|
78
|
+
# Set the task level to INFO if the nb_devices list if not empty
|
79
|
+
level_name = "INFO" if nb_devices else "ERROR"
|
80
|
+
success = "True" if level_name == "INFO" else "False"
|
81
|
+
print(task_result(text=task_text, changed=False, level_name=level_name))
|
82
|
+
print(
|
83
|
+
f"'Load NetBox device inventory' -> NetBoxResult <Success: {success}>\n"
|
84
|
+
+ f"-> NetBox device count: {len(nb_devices)}",
|
85
|
+
)
|
86
|
+
# Exit the script if fping failed and the nb_devices list is empty
|
87
|
+
if not nb_devices:
|
88
|
+
exit_error(task_text=f"{task_text} Failed", msg="-> No NetBox device in API response")
|
89
|
+
|
90
|
+
#### Find all NetBox primary device MGMT ip-addresses ###################################################
|
91
|
+
|
92
|
+
task_text = "Find all active NetBox primary device mgmt ip-addresses"
|
93
|
+
print(task_name(text=task_text))
|
94
|
+
|
95
|
+
# Loop over all NetBox devices and create a list of their primary ipv4 address
|
96
|
+
nb_mgmt_ips = [str(device["primary_ip"]["address"])[:-3] for device in nb_devices]
|
97
|
+
|
98
|
+
# Set the task level to INFO if the nb_mgmt_ips list if not empty
|
99
|
+
level_name = "INFO" if nb_mgmt_ips else "ERROR"
|
100
|
+
success = "True" if level_name == "INFO" else "False"
|
101
|
+
print(task_result(text=task_text, changed=False, level_name=level_name))
|
102
|
+
print(
|
103
|
+
f"'Find active mgmt ip-addresses' -> NornirResult <Success: {success}>\n"
|
104
|
+
+ f"-> NetBox primary device mgmt ip-address count: {len(nb_mgmt_ips)}",
|
105
|
+
)
|
106
|
+
# Exit the script if fping failed and the nb_mgmt_ips list is empty
|
107
|
+
if not nb_mgmt_ips:
|
108
|
+
exit_error(task_text=f"{task_text} Failed", msg="-> No NetBox primary device mgmt ip-addresses found")
|
109
|
+
|
110
|
+
#### Find all active MGMT ip-addresses with fping #######################################################
|
111
|
+
|
112
|
+
task_title = "Scan all script input mgmt ip-addresses with fping"
|
113
|
+
print_task_title(title=task_title)
|
114
|
+
|
115
|
+
task_text = "Find all active mgmt ip-addresses with fping"
|
116
|
+
print(task_name(text=task_text))
|
117
|
+
|
118
|
+
# Loop over all mgmt_subnets and create a list of active ip-addresses
|
119
|
+
alive_ips = []
|
120
|
+
for subnet in mgmt_subnets:
|
121
|
+
# fmt: off
|
122
|
+
fping = subprocess.run(["fping", "-a", "-g", subnet,], check=False, capture_output=True) # nosec
|
123
|
+
# fmt: on
|
124
|
+
output = fping.stdout.decode("utf-8").splitlines()
|
125
|
+
alive_ips.extend([ip for ip in output if ip not in exclude_addresses])
|
126
|
+
# Loop over all add_addresses and add them to the list of active ip-addresses
|
127
|
+
for ip in add_addresses:
|
128
|
+
# fmt: off
|
129
|
+
fping = subprocess.run(["fping", "-a", ip,], check=False, capture_output=True) # nosec
|
130
|
+
# fmt: on
|
131
|
+
output = fping.stdout.decode("utf-8").splitlines()
|
132
|
+
alive_ips.extend([ip for ip in output if ip not in exclude_addresses])
|
133
|
+
|
134
|
+
# Set the task level to INFO if the alive_ip list if not empty
|
135
|
+
level_name = "INFO" if alive_ips else "ERROR"
|
136
|
+
success = "True" if level_name == "INFO" else "False"
|
137
|
+
print(task_result(text=task_text, changed=False, level_name=level_name))
|
138
|
+
print(
|
139
|
+
f"'Find active mgmt ip-addresses' -> NornirResult <Success: {success}>\n"
|
140
|
+
+ f"-> Active mgmt ip-address count: {len(alive_ips)}",
|
141
|
+
)
|
142
|
+
# Exit the script if fping failed and the alive_ips list is empty
|
143
|
+
if not alive_ips:
|
144
|
+
exit_error(
|
145
|
+
task_text=f"{task_text} Failed",
|
146
|
+
msg=["-> No active mgmt ip-addresses found", "-> Verify the provided 'mgmt_subnets' input list"],
|
147
|
+
)
|
148
|
+
|
149
|
+
#### Verify NetBox primary device MGMT ip-addresses integrity ###########################################
|
150
|
+
|
151
|
+
# Set a boolean to True if the integrity check fails
|
152
|
+
integrity_failed = False
|
153
|
+
|
154
|
+
task_title = "Verify NetBox primary device mgmt ip-address integrity"
|
155
|
+
print_task_title(title=task_title)
|
156
|
+
|
157
|
+
#### Verify NetBox ip-addresses not part of the script input parameters
|
158
|
+
|
159
|
+
task_text = "Verify missing NetBox device ip-addresses for scan"
|
160
|
+
print(task_name(text=task_text))
|
161
|
+
|
162
|
+
# Find all NetBox primary ip-addresses which are not covered by the script input parameters
|
163
|
+
ip_diff = list(set(nb_mgmt_ips) - set(alive_ips))
|
164
|
+
|
165
|
+
# Set the task level to INFO if the ip_diff list if not empty
|
166
|
+
level_name = "ERROR" if ip_diff else "INFO"
|
167
|
+
success = "True" if level_name == "INFO" else "False"
|
168
|
+
print(task_result(text=task_text, changed=False, level_name=level_name))
|
169
|
+
print(f"'{task_text}' -> NornirResult <Success: {success}>")
|
170
|
+
if ip_diff:
|
171
|
+
integrity_failed = True
|
172
|
+
print("-> The following NetBox device ip-addresses are not part of the script input parameters")
|
173
|
+
for ip in ip_diff:
|
174
|
+
print(f"- {ip}")
|
175
|
+
else:
|
176
|
+
print("-> All NetBox device ip-addresses are part of the script input parameters")
|
177
|
+
|
178
|
+
#### Verify NetBox primary device mgmt ip-address integrity
|
179
|
+
|
180
|
+
task_text = "Verify active device ip-addresses covered by NetBox"
|
181
|
+
print(task_name(text=task_text))
|
182
|
+
|
183
|
+
# Find all active ip-addresses which are not part of a NetBox primary ip-address
|
184
|
+
ip_diff = list(set(alive_ips) - set(nb_mgmt_ips))
|
185
|
+
|
186
|
+
# Set the task level to INFO if the ip_diff list if not empty
|
187
|
+
level_name = "ERROR" if ip_diff else "INFO"
|
188
|
+
success = "True" if level_name == "INFO" else "False"
|
189
|
+
print(task_result(text=task_text, changed=False, level_name=level_name))
|
190
|
+
print(f"'{task_text}' -> NornirResult <Success: {success}>")
|
191
|
+
if ip_diff:
|
192
|
+
integrity_failed = True
|
193
|
+
print("-> The following active device ip-addresses are not part of the NetBox inventory")
|
194
|
+
for ip in ip_diff:
|
195
|
+
print(f"- {ip}")
|
196
|
+
else:
|
197
|
+
print("-> All active device ip-addresses are part of the NetBox inventory")
|
198
|
+
|
199
|
+
# Exit the script with an error if the integrity check failed
|
200
|
+
if integrity_failed:
|
201
|
+
print("\r")
|
202
|
+
exit_error(task_text=f"{task_title} Failed", msg="-> Verify the script results for failes tasks")
|
File without changes
|
File without changes
|
@@ -0,0 +1,250 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
This module contains the Nornir inventory plugin DSCNetBoxInventory.
|
4
|
+
|
5
|
+
The functions are ordered as followed:
|
6
|
+
- Helper Functions
|
7
|
+
- Inventory Plugin
|
8
|
+
"""
|
9
|
+
|
10
|
+
import os
|
11
|
+
import logging
|
12
|
+
from typing import Any, Dict, List, Optional, Union, Type
|
13
|
+
import requests
|
14
|
+
import ruamel.yaml
|
15
|
+
from nornir.core.inventory import (
|
16
|
+
ConnectionOptions,
|
17
|
+
Defaults,
|
18
|
+
Group,
|
19
|
+
Groups,
|
20
|
+
Host,
|
21
|
+
HostOrGroup,
|
22
|
+
Hosts,
|
23
|
+
Inventory,
|
24
|
+
ParentGroups,
|
25
|
+
)
|
26
|
+
from nornir_collection.utils import print_task_name, task_info
|
27
|
+
|
28
|
+
logger = logging.getLogger(__name__)
|
29
|
+
|
30
|
+
|
31
|
+
#### Helper Functions #######################################################################################
|
32
|
+
|
33
|
+
|
34
|
+
def _get_connection_options(data: Dict[str, Any]) -> Dict[str, ConnectionOptions]:
|
35
|
+
"""
|
36
|
+
TBD
|
37
|
+
"""
|
38
|
+
# pylint: disable=invalid-name
|
39
|
+
cp = {}
|
40
|
+
for cn, c in data.items():
|
41
|
+
cp[cn] = ConnectionOptions(
|
42
|
+
hostname=c.get("hostname"),
|
43
|
+
port=c.get("port"),
|
44
|
+
username=c.get("username"),
|
45
|
+
password=c.get("password"),
|
46
|
+
platform=c.get("platform"),
|
47
|
+
extras=c.get("extras"),
|
48
|
+
)
|
49
|
+
return cp
|
50
|
+
|
51
|
+
|
52
|
+
def _get_defaults(data: Dict[str, Any]) -> Defaults:
|
53
|
+
"""
|
54
|
+
TBD
|
55
|
+
"""
|
56
|
+
return Defaults(
|
57
|
+
hostname=data.get("hostname"),
|
58
|
+
port=data.get("port"),
|
59
|
+
username=data.get("username"),
|
60
|
+
password=data.get("password"),
|
61
|
+
platform=data.get("platform"),
|
62
|
+
data=data.get("data"),
|
63
|
+
connection_options=_get_connection_options(data.get("connection_options", {})),
|
64
|
+
)
|
65
|
+
|
66
|
+
|
67
|
+
def _get_inventory_element(
|
68
|
+
typ: Type[HostOrGroup], data: Dict[str, Any], name: str, defaults: Defaults
|
69
|
+
) -> HostOrGroup:
|
70
|
+
"""
|
71
|
+
TBD
|
72
|
+
"""
|
73
|
+
return typ(
|
74
|
+
name=name,
|
75
|
+
hostname=data.get("hostname"),
|
76
|
+
port=data.get("port"),
|
77
|
+
username=data.get("username"),
|
78
|
+
password=data.get("password"),
|
79
|
+
platform=data.get("platform"),
|
80
|
+
data=data.get("data"),
|
81
|
+
groups=data.get("groups"), # this is a hack, we will convert it later to the correct type
|
82
|
+
defaults=defaults,
|
83
|
+
connection_options=_get_connection_options(data.get("connection_options", {})),
|
84
|
+
)
|
85
|
+
|
86
|
+
|
87
|
+
#### Inventory Plugin #######################################################################################
|
88
|
+
|
89
|
+
|
90
|
+
class DSCNetBoxInventory:
|
91
|
+
"""
|
92
|
+
This class is a deviation of the Nornir plugin NetBoxInventory2.
|
93
|
+
This plugin have to be registered since Nornir 3.0
|
94
|
+
|
95
|
+
Inventory plugin that uses `NetBox <https://github.com/netbox-community/netbox>`_ as backend.
|
96
|
+
Note:
|
97
|
+
Additional data provided by the NetBox devices API endpoint will be
|
98
|
+
available through the NetBox Host data attribute.
|
99
|
+
Environment Variables:
|
100
|
+
* ``NB_URL``: Corresponds to nb_url argument
|
101
|
+
* ``NB_TOKEN``: Corresponds to nb_token argument
|
102
|
+
Arguments:
|
103
|
+
nb_url: NetBox url
|
104
|
+
nb_token: NetBox API token
|
105
|
+
ssl_verify: Enable/disable certificate validation
|
106
|
+
(defaults to True)
|
107
|
+
"""
|
108
|
+
|
109
|
+
# pylint: disable=invalid-name
|
110
|
+
|
111
|
+
def __init__( # pylint: disable=unused-argument,too-many-arguments
|
112
|
+
self,
|
113
|
+
nb_url: Optional[str] = None,
|
114
|
+
nb_token: Optional[str] = None,
|
115
|
+
ssl_verify: Union[bool, str] = True,
|
116
|
+
**kwargs: Any,
|
117
|
+
) -> None:
|
118
|
+
"""
|
119
|
+
Init function
|
120
|
+
"""
|
121
|
+
nb_url = nb_url or os.environ.get("NB_URL")
|
122
|
+
nb_token = nb_token or os.environ.get("NB_TOKEN")
|
123
|
+
|
124
|
+
self.task_text = "NORNIR initialize inventory plugin DSCNetBoxInventory"
|
125
|
+
self.nb_url = nb_url
|
126
|
+
self.session = requests.Session()
|
127
|
+
self.session.headers.update({"Authorization": f"Token {nb_token}"})
|
128
|
+
self.session.verify = ssl_verify
|
129
|
+
|
130
|
+
def _get_resources( # pylint: disable=dangerous-default-value
|
131
|
+
self, url: str, params: Dict[str, Any] = {}
|
132
|
+
) -> List[Dict[str, Any]]:
|
133
|
+
"""
|
134
|
+
TBD
|
135
|
+
"""
|
136
|
+
# Define the resource list
|
137
|
+
resources: List[Dict[str, Any]] = []
|
138
|
+
# While there is a next page continue the loop
|
139
|
+
while url:
|
140
|
+
# Do the http request
|
141
|
+
r = self.session.get(url, params=params)
|
142
|
+
# Verify the response code
|
143
|
+
if not r.status_code == 200:
|
144
|
+
raise ValueError(f"Failed to get data from NetBox instance {self.nb_url}")
|
145
|
+
# Extract the json data from the http response
|
146
|
+
resp = r.json()
|
147
|
+
# Add the response to the resource list
|
148
|
+
resources.extend(resp.get("results"))
|
149
|
+
# Get the url of the next page
|
150
|
+
url = resp.get("next")
|
151
|
+
|
152
|
+
# Retrun the resources list
|
153
|
+
return resources
|
154
|
+
|
155
|
+
def load(self) -> Inventory:
|
156
|
+
"""
|
157
|
+
TBD
|
158
|
+
"""
|
159
|
+
# pylint: disable=too-many-statements,too-many-branches,too-many-locals
|
160
|
+
yml = ruamel.yaml.YAML(typ="safe")
|
161
|
+
|
162
|
+
print_task_name(text=self.task_text)
|
163
|
+
|
164
|
+
# Load all NetBox devices with a primary IP address assigned
|
165
|
+
devices: List[Dict[str, Any]] = []
|
166
|
+
devices.extend(
|
167
|
+
self._get_resources(
|
168
|
+
url=f"{self.nb_url}/api/dcim/devices/",
|
169
|
+
params={"limit": 1000, "has_primary_ip": True},
|
170
|
+
)
|
171
|
+
)
|
172
|
+
|
173
|
+
# Create the defaults
|
174
|
+
defaults = Defaults()
|
175
|
+
defaults_dict: Dict[str, Any] = {}
|
176
|
+
with open("inventory/defaults.yaml", "r", encoding="utf-8") as f:
|
177
|
+
defaults_dict = yml.load(f) or {}
|
178
|
+
defaults = _get_defaults(defaults_dict)
|
179
|
+
|
180
|
+
# Create the hosts
|
181
|
+
hosts = Hosts()
|
182
|
+
for device in devices:
|
183
|
+
serialized_device: Dict[Any, Any] = {}
|
184
|
+
serialized_device["data"] = device
|
185
|
+
|
186
|
+
# Remove the custom_fields key and make the custom_field native to Nornir
|
187
|
+
for cf, value in device["custom_fields"].items():
|
188
|
+
serialized_device["data"][cf] = value
|
189
|
+
serialized_device["data"].pop("custom_fields")
|
190
|
+
|
191
|
+
# Remove the config_context key and make the config_context native to Nornir
|
192
|
+
for cc, value in device["config_context"].items():
|
193
|
+
serialized_device["data"][cc] = value
|
194
|
+
serialized_device["data"].pop("config_context")
|
195
|
+
|
196
|
+
# Set the Nornir hostname (ip address or fqdn)
|
197
|
+
hostname = None
|
198
|
+
if device.get("primary_ip"):
|
199
|
+
hostname = device.get("primary_ip", {}).get("address", "").split("/")[0]
|
200
|
+
else:
|
201
|
+
if device.get("name") is not None:
|
202
|
+
hostname = device["name"]
|
203
|
+
serialized_device["hostname"] = hostname
|
204
|
+
|
205
|
+
# Set the Nornir host name
|
206
|
+
if serialized_device["data"].get("virtual_chassis"):
|
207
|
+
name = serialized_device["data"].get("virtual_chassis").get("name")
|
208
|
+
else:
|
209
|
+
name = serialized_device["data"].get("name") or str(serialized_device["data"].get("id"))
|
210
|
+
|
211
|
+
# Add virtual chassis master serial to the virtual_chassis dict
|
212
|
+
if serialized_device["data"].get("virtual_chassis"):
|
213
|
+
master_serial = device["serial"] if device["serial"] else None
|
214
|
+
serialized_device["data"]["virtual_chassis"]["master"]["serial"] = master_serial
|
215
|
+
|
216
|
+
# Flatten the list of Tags which contains only the slug of each NetBox tag
|
217
|
+
device["tags"] = [tag["slug"] for tag in device["tags"]]
|
218
|
+
|
219
|
+
# Extract the device connection options
|
220
|
+
# Add Nornir connection options from the device context data
|
221
|
+
if "connection_options" in device and device["connection_options"] is not None:
|
222
|
+
serialized_device["connection_options"] = device["connection_options"]
|
223
|
+
else:
|
224
|
+
serialized_device["connection_options"] = {}
|
225
|
+
# Remove the connection_options key from the serialized_device data
|
226
|
+
if (
|
227
|
+
"connection_options" in serialized_device["data"]
|
228
|
+
and serialized_device["data"]["connection_options"] is not None
|
229
|
+
):
|
230
|
+
serialized_device["data"].pop("connection_options")
|
231
|
+
|
232
|
+
# Set the Nornir host elements
|
233
|
+
hosts[name] = _get_inventory_element(Host, serialized_device, name, defaults)
|
234
|
+
|
235
|
+
# Create the groups
|
236
|
+
groups = Groups()
|
237
|
+
groups_dict: Dict[str, Any] = {}
|
238
|
+
for n, g in groups_dict.items():
|
239
|
+
groups[n] = _get_inventory_element(Group, g, n, defaults)
|
240
|
+
for g in groups.values():
|
241
|
+
g.groups = ParentGroups([groups[g] for g in g.groups])
|
242
|
+
for h in hosts.values():
|
243
|
+
h.groups = ParentGroups([groups[g] for g in h.groups])
|
244
|
+
|
245
|
+
# Print the DSCNetBoxInventory result
|
246
|
+
print(task_info(text=self.task_text, changed=False))
|
247
|
+
print(f"'{self.task_text}' -> NornirResponse <Success: True>")
|
248
|
+
print(f"-> Loaded NetBox base inventory data of {len(hosts)} devices")
|
249
|
+
|
250
|
+
return Inventory(hosts=hosts, groups=groups, defaults=defaults)
|
@@ -0,0 +1,143 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
This module contains Nornir inventory plugin StaggeredYamlInventory.
|
4
|
+
|
5
|
+
The functions are ordered as followed:
|
6
|
+
- Helper Functions
|
7
|
+
- Inventory Plugin
|
8
|
+
"""
|
9
|
+
|
10
|
+
import pathlib
|
11
|
+
import glob
|
12
|
+
from typing import Literal
|
13
|
+
import ruamel.yaml
|
14
|
+
from nornir.core.inventory import Inventory, Group, Groups, Host, Hosts, Defaults, ParentGroups
|
15
|
+
from nornir.plugins.inventory.simple import _get_defaults, _get_inventory_element
|
16
|
+
from nornir_collection.utils import (
|
17
|
+
print_task_name,
|
18
|
+
task_info,
|
19
|
+
task_error,
|
20
|
+
exit_error,
|
21
|
+
load_multiple_yaml_files_to_string,
|
22
|
+
)
|
23
|
+
|
24
|
+
|
25
|
+
#### Inventory Plugin #######################################################################################
|
26
|
+
|
27
|
+
|
28
|
+
class StaggeredYamlInventory:
|
29
|
+
"""
|
30
|
+
This class is a deviation of the Nornir builtin SimpleInventory.
|
31
|
+
This plugin have to be registered since Nornir 3.0
|
32
|
+
"""
|
33
|
+
|
34
|
+
# pylint: disable=invalid-name
|
35
|
+
|
36
|
+
def __init__(
|
37
|
+
self,
|
38
|
+
hosts_base: str = "hosts.yaml",
|
39
|
+
groups_base: str = "groups.yaml",
|
40
|
+
defaults_base: str = "defaults.yaml",
|
41
|
+
encoding: str = "utf-8",
|
42
|
+
) -> None:
|
43
|
+
"""
|
44
|
+
StaggeredYamlInventory is an inventory plugin that loads data from multiple yaml files. The the base
|
45
|
+
path all yaml files starting with hosts, groups, or defaults will be concatenated and loaded as dict.
|
46
|
+
The yaml files follow the same structure as the native objects
|
47
|
+
Args:
|
48
|
+
hosts_base: base path recursively to files with hosts definition
|
49
|
+
groups_base: basepath recursively to files with groups definition.
|
50
|
+
If it doesn't exist it will be skipped
|
51
|
+
defaults_base: base path recursively to files with defaults definition.
|
52
|
+
If it doesn't exist it will be skipped
|
53
|
+
encoding: Encoding used to save inventory files. Defaults to utf-8
|
54
|
+
"""
|
55
|
+
|
56
|
+
self.task_text = "NORNIR initialize inventory plugin StaggeredYamlInventory"
|
57
|
+
# Get the base path for the hosts, groups and defaults filed
|
58
|
+
self.hosts_base = pathlib.Path(hosts_base).expanduser()
|
59
|
+
self.groups_base = pathlib.Path(groups_base).expanduser()
|
60
|
+
self.defaults_base = pathlib.Path(defaults_base).expanduser()
|
61
|
+
# Create lists with all files recursively which starts with hosts, groups or defaults
|
62
|
+
self.hosts_files = glob.glob(str(self.hosts_base) + "/**/hosts*.yaml", recursive=True)
|
63
|
+
self.groups_files = glob.glob(str(self.groups_base) + "/**/groups*.yaml", recursive=True)
|
64
|
+
self.defaults_files = glob.glob(str(self.defaults_base) + "/**/defaults*.yaml", recursive=True)
|
65
|
+
self.encoding = encoding
|
66
|
+
|
67
|
+
def load_multiple_yaml_files(self, inv_type: Literal["hosts", "groups", "defaults"]) -> dict:
|
68
|
+
"""
|
69
|
+
TBD
|
70
|
+
"""
|
71
|
+
yml = ruamel.yaml.YAML(typ="safe")
|
72
|
+
yaml_string = str()
|
73
|
+
|
74
|
+
# Use mapping to avoid long if elif else statements
|
75
|
+
file_list = {"hosts": self.hosts_files, "groups": self.groups_files, "defaults": self.defaults_files}
|
76
|
+
# Load the yaml files from file_list and concatinate them to one string
|
77
|
+
yaml_string = load_multiple_yaml_files_to_string(file_list=file_list[inv_type], silent=True)
|
78
|
+
|
79
|
+
try:
|
80
|
+
yaml_dict = yml.load(yaml_string) or {}
|
81
|
+
|
82
|
+
except ruamel.yaml.constructor.DuplicateKeyError as error:
|
83
|
+
print(task_error(text=self.task_text, changed=False))
|
84
|
+
print(f"'Load {inv_type} inventory yaml files' -> NornirResponse <Success: False>")
|
85
|
+
print("-> Duplicate key in yaml files", "\n\n", error)
|
86
|
+
# Exit the script with a proper message
|
87
|
+
exit_error(
|
88
|
+
task_text=self.task_text,
|
89
|
+
text=f"ALERT: LOAD {inv_type.upper()} INVENTORY YAML FILES FAILED!",
|
90
|
+
msg=f"-> Analyse the {inv_type} inventory yaml files and identify the root cause",
|
91
|
+
)
|
92
|
+
|
93
|
+
return yaml_dict
|
94
|
+
|
95
|
+
def load(self) -> Inventory:
|
96
|
+
"""
|
97
|
+
TBD
|
98
|
+
"""
|
99
|
+
print_task_name(text=self.task_text)
|
100
|
+
|
101
|
+
if self.defaults_files:
|
102
|
+
defaults_dict = self.load_multiple_yaml_files(inv_type="defaults")
|
103
|
+
|
104
|
+
defaults = _get_defaults(defaults_dict)
|
105
|
+
else:
|
106
|
+
defaults = Defaults()
|
107
|
+
|
108
|
+
hosts = Hosts()
|
109
|
+
hosts_dict = self.load_multiple_yaml_files(inv_type="hosts")
|
110
|
+
|
111
|
+
for n, h in hosts_dict.items():
|
112
|
+
hosts[n] = _get_inventory_element(Host, h, n, defaults)
|
113
|
+
|
114
|
+
groups = Groups()
|
115
|
+
if self.groups_files:
|
116
|
+
groups_dict = self.load_multiple_yaml_files(inv_type="groups")
|
117
|
+
|
118
|
+
for n, g in groups_dict.items():
|
119
|
+
groups[n] = _get_inventory_element(Group, g, n, defaults)
|
120
|
+
|
121
|
+
for g in groups.values():
|
122
|
+
g.groups = ParentGroups([groups[g] for g in g.groups])
|
123
|
+
|
124
|
+
try:
|
125
|
+
for h in hosts.values():
|
126
|
+
h.groups = ParentGroups([groups[g] for g in h.groups])
|
127
|
+
|
128
|
+
except KeyError as error:
|
129
|
+
print(task_error(text=self.task_text, changed=False))
|
130
|
+
print("'Assign groups to hosts' -> NornirResponse <Success: False>")
|
131
|
+
print(f"-> Group {error} not found")
|
132
|
+
# Exit the script with a proper message
|
133
|
+
exit_error(
|
134
|
+
task_text=self.task_text,
|
135
|
+
text="ALERT: ASSIGN GROUPS TO HOSTS FAILED!",
|
136
|
+
msg="-> Analyse the Nornir group files",
|
137
|
+
)
|
138
|
+
|
139
|
+
print(task_info(text=self.task_text, changed=False))
|
140
|
+
print(f"'{self.task_text}' -> NornirResponse <Success: True>")
|
141
|
+
print("-> Loaded YAML inventory data")
|
142
|
+
|
143
|
+
return Inventory(hosts=hosts, groups=groups, defaults=defaults)
|