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