From a271a543615185746e3e7a6925a4e508407ce37c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20Krienb=C3=BChl?= Date: Wed, 3 Jul 2024 00:17:39 +0200 Subject: [PATCH] Add ability to rename host inventory variables using patterns (#1273) Certain environments may have conflicts with the variables given by Netbox, and the variables used in the existing Ansible codebase. With this change, such cases can be worked around by renaming individual variables, or whole groups of them. For example, this will rename all `cluster*` variables to have a `netbox__` prefix instead (e.g., `cluster_group` -> `netbox__cluster_group`): ```yaml rename_variables: - pattern: 'cluster(.*)' repl: 'netbox__cluster\1' ``` Uses a list, instead of a dict, to ensure that the order of evaluation is strictly defined across all Python versions, and to add the ability to exclude certain variables from being rewritten. For example: ```yaml rename_variables: # Keep cluster_type the same - pattern: 'cluster_type' repl: 'cluster_type' # Rename all other cluster* variables - pattern: 'cluster(.*)' repl: 'netbox__cluster\1' ``` --- ...-ability-to-rename-host-inventory-vars.yml | 2 + plugins/inventory/nb_inventory.py | 48 +++++++++++++++---- tests/unit/inventory/test_nb_inventory.py | 36 ++++++++++++++ 3 files changed, 76 insertions(+), 10 deletions(-) create mode 100644 changelogs/fragments/1273-ability-to-rename-host-inventory-vars.yml diff --git a/changelogs/fragments/1273-ability-to-rename-host-inventory-vars.yml b/changelogs/fragments/1273-ability-to-rename-host-inventory-vars.yml new file mode 100644 index 00000000..afba5880 --- /dev/null +++ b/changelogs/fragments/1273-ability-to-rename-host-inventory-vars.yml @@ -0,0 +1,2 @@ +minor_changes: + - Add ability to rename variables set on the host by ``netbox.netbox.nb_inventory`` through configuration. diff --git a/plugins/inventory/nb_inventory.py b/plugins/inventory/nb_inventory.py index 889354bd..03444b6b 100644 --- a/plugins/inventory/nb_inventory.py +++ b/plugins/inventory/nb_inventory.py @@ -249,6 +249,16 @@ description: Use out of band IP as `ansible host` type: boolean default: false + rename_variables: + description: + - Rename variables evaluated by nb_inventory, before writing them. + - Each list entry contains a dict with a 'pattern' and a 'repl'. + - Both 'pattern' and 'repl' are regular expressions. + - The first matching expression is used, subsequent matches are ignored. + - Internally `re.sub` is used. + type: list + elements: dict + default: [] """ EXAMPLES = """ @@ -364,6 +374,7 @@ import uuid import math import os +import re import datetime from copy import deepcopy from functools import partial @@ -1905,31 +1916,37 @@ def _setup_nested_groups(self, group, lookup, parent_lookup): return transformed_group_names + def _set_variable(self, hostname, key, value): + for item in self.rename_variables: + if item["pattern"].match(key): + key = item["pattern"].sub(item["repl"], key) + break + + self.inventory.set_variable(hostname, key, value) + def _fill_host_variables(self, host, hostname): extracted_primary_ip = self.extract_primary_ip(host=host) if extracted_primary_ip: - self.inventory.set_variable(hostname, "ansible_host", extracted_primary_ip) + self._set_variable(hostname, "ansible_host", extracted_primary_ip) if self.ansible_host_dns_name: extracted_dns_name = self.extract_dns_name(host=host) if extracted_dns_name: - self.inventory.set_variable( - hostname, "ansible_host", extracted_dns_name - ) + self._set_variable(hostname, "ansible_host", extracted_dns_name) extracted_primary_ip4 = self.extract_primary_ip4(host=host) if extracted_primary_ip4: - self.inventory.set_variable(hostname, "primary_ip4", extracted_primary_ip4) + self._set_variable(hostname, "primary_ip4", extracted_primary_ip4) extracted_primary_ip6 = self.extract_primary_ip6(host=host) if extracted_primary_ip6: - self.inventory.set_variable(hostname, "primary_ip6", extracted_primary_ip6) + self._set_variable(hostname, "primary_ip6", extracted_primary_ip6) extracted_oob_ip = self.extract_oob_ip(host=host) if extracted_oob_ip: - self.inventory.set_variable(hostname, "oob_ip", extracted_oob_ip) + self._set_variable(hostname, "oob_ip", extracted_oob_ip) if self.oob_ip_as_primary_ip: - self.inventory.set_variable(hostname, "ansible_host", extracted_oob_ip) + self._set_variable(hostname, "ansible_host", extracted_oob_ip) for attribute, extractor in self.group_extractors.items(): extracted_value = extractor(host) @@ -1965,9 +1982,9 @@ def _fill_host_variables(self, host, hostname): ) ): for key, value in extracted_value.items(): - self.inventory.set_variable(hostname, key, value) + self._set_variable(hostname, key, value) else: - self.inventory.set_variable(hostname, attribute, extracted_value) + self._set_variable(hostname, attribute, extracted_value) def _get_host_virtual_chassis_master(self, host): virtual_chassis = host.get("virtual_chassis", None) @@ -2146,4 +2163,15 @@ def parse(self, inventory, loader, path, cache=True): self.ansible_host_dns_name = self.get_option("ansible_host_dns_name") self.racks = self.get_option("racks") + # Compile regular expressions, if any + self.rename_variables = self.parse_rename_variables( + self.get_option("rename_variables") + ) + self.main() + + def parse_rename_variables(self, rename_variables): + return [ + {"pattern": re.compile(i["pattern"]), "repl": i["repl"]} + for i in rename_variables or () + ] diff --git a/tests/unit/inventory/test_nb_inventory.py b/tests/unit/inventory/test_nb_inventory.py index 4b502a8a..93a59217 100644 --- a/tests/unit/inventory/test_nb_inventory.py +++ b/tests/unit/inventory/test_nb_inventory.py @@ -34,6 +34,18 @@ ) +class MockInventory: + + def __init__(self): + self.variables = {} + + def set_variable(self, hostname, key, value): + if hostname not in self.variables: + self.variables[hostname] = {} + + self.variables[hostname][key] = value + + @pytest.fixture def inventory_fixture( allowed_device_query_parameters_fixture, allowed_vm_query_parameters_fixture @@ -46,6 +58,9 @@ def inventory_fixture( inventory.allowed_device_query_parameters = allowed_device_query_parameters_fixture inventory.allowed_vm_query_parameters = allowed_vm_query_parameters_fixture + # Inventory mock, to validate what has been set via inventory.inventory.set_variable + inventory.inventory = MockInventory() + return inventory @@ -260,3 +275,24 @@ def test_extract_custom_fields(inventory_fixture, custom_fields, expected): ) assert extracted_custom_fields == expected + + +def test_rename_variables(inventory_fixture): + inventory_fixture.rename_variables = inventory_fixture.parse_rename_variables( + ( + {"pattern": r"cluster(.*)", "repl": r"netbox_cluster\1"}, + {"pattern": r"ansible_host", "repl": r"host"}, + ) + ) + + inventory_fixture._set_variable("host", "ansible_fqdn", "host.example.org") + inventory_fixture._set_variable("host", "ansible_host", "host") + inventory_fixture._set_variable("host", "cluster", "staging") + inventory_fixture._set_variable("host", "cluster_id", "0xdeadbeef") + + assert inventory_fixture.inventory.variables["host"] == { + "ansible_fqdn": "host.example.org", + "host": "host", + "netbox_cluster": "staging", + "netbox_cluster_id": "0xdeadbeef", + }