diff --git a/.gitignore b/.gitignore index d006fb0..73ba8c2 100644 --- a/.gitignore +++ b/.gitignore @@ -145,7 +145,10 @@ kubevirt.core /tests/output/ /tests/integration/inventory /tests/integration/targets/inventory_kubevirt/all.yml +/tests/integration/targets/inventory_kubevirt/cache_after.yml +/tests/integration/targets/inventory_kubevirt/cache_before.yml /tests/integration/targets/inventory_kubevirt/empty.yml /tests/integration/targets/inventory_kubevirt/label.yml /tests/integration/targets/inventory_kubevirt/net.yml /tests/integration/targets/kubevirt_vm/files +kubevirt-cache diff --git a/examples/cache.kubevirt.yml b/examples/cache.kubevirt.yml new file mode 100644 index 0000000..237454b --- /dev/null +++ b/examples/cache.kubevirt.yml @@ -0,0 +1,5 @@ +--- +plugin: kubevirt.core.kubevirt +cache: true +cache_plugin: ansible.builtin.jsonfile +cache_connection: kubevirt-cache diff --git a/plugins/inventory/kubevirt.py b/plugins/inventory/kubevirt.py index e83b3e4..daed6f4 100644 --- a/plugins/inventory/kubevirt.py +++ b/plugins/inventory/kubevirt.py @@ -186,6 +186,8 @@ class InventoryOptions: base_domain: Optional[str] = None append_base_domain: Optional[bool] = None host_format: Optional[str] = None + namespaces: Optional[List[str]] = None + name: Optional[str] = None config_data: InitVar[Optional[Dict]] = None def __post_init__(self, config_data: Optional[Dict]) -> None: @@ -238,6 +240,12 @@ class InventoryOptions: if self.host_format is not None else config_data.get("host_format", "{namespace}-{name}") ) + self.namespaces = ( + self.namespaces + if self.namespaces is not None + else config_data.get("namespaces") + ) + self.name = self.name if self.name is not None else config_data.get("name") class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable): @@ -379,51 +387,38 @@ class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable): """ parse is the main entry point of the inventory. It checks for availability of the Kubernetes Python client, - gets the configuration and runs fetch_objects or - if there is a cache it is used instead. + gets the configuration, retrieves the cache or runs fetch_objects and + populates the inventory. """ - super().parse(inventory, loader, path) - cache_key = self._get_cache_prefix(path) - config_data = self._read_config_data(path) - if not HAS_K8S_MODULE_HELPER: raise KubeVirtInventoryException( "This module requires the Kubernetes Python client. " + f"Try `pip install kubernetes`. Detail: {K8S_IMPORT_EXCEPTION}" ) - source_data = None - if cache and cache_key in self._cache: - try: - source_data = self._cache[cache_key] - except KeyError: - pass + super().parse(inventory, loader, path) - if not source_data: - self.fetch_objects(config_data) - - def fetch_objects(self, config_data: Dict) -> None: - """ - fetch_objects populates the inventory with the specified parameters. - """ - if not config_data or not isinstance(config_data, dict): - config_data = {} + config_data = self._read_config_data(path) + cache_key = self.get_cache_key(path) + user_cache_setting = self.get_option("cache") + attempt_to_read_cache = user_cache_setting and cache + cache_needs_update = user_cache_setting and not cache self.connections_compatibility(config_data) - client = get_api_client(**config_data) - name = config_data.get( - "name", self.get_default_host_name(client.configuration.host) - ) - namespaces = ( - config_data["namespaces"] - if config_data.get("namespaces") - else self.get_available_namespaces(client) - ) opts = InventoryOptions(config_data=config_data) - if opts.base_domain is None: - opts.base_domain = self.get_cluster_domain(client) - for namespace in namespaces: - self.populate_inventory_from_namespace(client, name, namespace, opts) + + results = {} + if attempt_to_read_cache: + try: + results = self._cache[cache_key] + except KeyError: + cache_needs_update = True + if not attempt_to_read_cache or cache_needs_update: + results = self.fetch_objects(get_api_client(**config_data), opts) + if cache_needs_update: + self._cache[cache_key] = results + + self.populate_inventory(results, opts) def connections_compatibility(self, config_data: Dict) -> None: collection_name = "kubevirt.core" @@ -462,6 +457,35 @@ class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable): removed=True, ) + def fetch_objects(self, client: Any, opts: InventoryOptions) -> Dict: + """ + fetch_objects fetches all relevant objects from the K8S API. + """ + namespaces = {} + for namespace in ( + opts.namespaces + if opts.namespaces + else self.get_available_namespaces(client) + ): + vms = self.get_vms_for_namespace(client, namespace, opts) + vmis = self.get_vmis_for_namespace(client, namespace, opts) + + if not vms and not vmis: + # Continue if no VMs and VMIs were found to avoid adding empty groups. + continue + + namespaces[namespace] = { + "vms": vms, + "vmis": vmis, + "services": self.get_ssh_services_for_namespace(client, namespace), + } + + return { + "default_hostname": self.get_default_hostname(client.configuration.host), + "cluster_domain": self.get_cluster_domain(client), + "namespaces": namespaces, + } + def get_cluster_domain(self, client: K8SClient) -> Optional[str]: """ get_cluster_domain tries to get the base domain of an OpenShift cluster. @@ -577,22 +601,31 @@ class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable): return services + def populate_inventory(self, results: Dict, opts: InventoryOptions) -> None: + """ + populate_inventory populates the inventory by completing the InventoryOptions + and invoking populate_inventory_from_namespace for every namespace in results. + """ + if opts.base_domain is None: + opts.base_domain = results["cluster_domain"] + if opts.name is None: + opts.name = results["default_hostname"] + for namespace, data in results["namespaces"].items(): + self.populate_inventory_from_namespace(namespace, data, opts) def populate_inventory_from_namespace( - self, client: K8SClient, name: str, namespace: str, opts: InventoryOptions + self, namespace: str, data: Dict, opts: InventoryOptions ) -> None: """ populate_inventory_from_namespace adds groups and hosts from a namespace to the inventory. """ vms = { - vm["metadata"]["name"]: vm - for vm in self.get_vms_for_namespace(client, namespace, opts) - if self.obj_is_valid(vm) + vm["metadata"]["name"]: vm for vm in data["vms"] if self.obj_is_valid(vm) } vmis = { vmi["metadata"]["name"]: vmi - for vmi in self.get_vmis_for_namespace(client, namespace, opts) + for vmi in data["vmis"] if self.obj_is_valid(vmi) } @@ -600,9 +633,13 @@ class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable): # Return early if no VMs and VMIs were found to avoid adding empty groups. return - services = self.get_ssh_services_for_namespace(client, namespace) + services = { + domain: service + for domain, service in data["services"].items() + if self.obj_is_valid(service) + } - name = self._sanitize_group_name(name) + name = self._sanitize_group_name(opts.name) namespace_group = self._sanitize_group_name(f"namespace_{namespace}") self.inventory.add_group(name) diff --git a/tests/integration/targets/inventory_kubevirt/runme.sh b/tests/integration/targets/inventory_kubevirt/runme.sh index 97a0308..53a48fc 100755 --- a/tests/integration/targets/inventory_kubevirt/runme.sh +++ b/tests/integration/targets/inventory_kubevirt/runme.sh @@ -10,9 +10,12 @@ ansible-inventory -i test.kubevirt.yml -y --list --output empty.yml "$@" ansible-playbook playbook.yml "$@" ansible-inventory -i test.kubevirt.yml -y --list --output all.yml "$@" +ansible-inventory -i test.cache.kubevirt.yml -y --list --output cache_before.yml "$@" ansible-inventory -i test.label.kubevirt.yml -y --list --output label.yml "$@" ansible-inventory -i test.net.kubevirt.yml -y --list --output net.yml "$@" ansible-playbook cleanup.yml "$@" +ansible-inventory -i test.cache.kubevirt.yml -y --list --output cache_after.yml "$@" + ansible-playbook verify.yml "$@" diff --git a/tests/integration/targets/inventory_kubevirt/test.cache.kubevirt.yml b/tests/integration/targets/inventory_kubevirt/test.cache.kubevirt.yml new file mode 100644 index 0000000..16fa113 --- /dev/null +++ b/tests/integration/targets/inventory_kubevirt/test.cache.kubevirt.yml @@ -0,0 +1,9 @@ +--- +plugin: kubevirt.core.kubevirt +name: test +namespaces: + - default +create_groups: true +cache: true +cache_plugin: ansible.builtin.jsonfile +cache_connection: kubevirt-cache diff --git a/tests/integration/targets/inventory_kubevirt/verify.yml b/tests/integration/targets/inventory_kubevirt/verify.yml index 1864a82..36145d0 100644 --- a/tests/integration/targets/inventory_kubevirt/verify.yml +++ b/tests/integration/targets/inventory_kubevirt/verify.yml @@ -38,3 +38,16 @@ ansible.builtin.assert: that: - inv_net['all']['children']['label_app_test']['hosts'] | length == 1 + - name: Read cached inventory after VM creation + ansible.builtin.include_vars: + file: cache_before.yml + name: inv_cache_before + - name: Read cached inventory after VM deletion + ansible.builtin.include_vars: + file: cache_after.yml + name: inv_cache_after + - name: Assert cached inventories are populated + ansible.builtin.assert: + that: + - inv_cache_before == inv_all + - inv_cache_before == inv_cache_after diff --git a/tests/unit/plugins/inventory/blackbox/test_kubevirt_ansible_connection_winrm.py b/tests/unit/plugins/inventory/blackbox/test_kubevirt_ansible_connection_winrm.py index 2c232ff..78ad62f 100644 --- a/tests/unit/plugins/inventory/blackbox/test_kubevirt_ansible_connection_winrm.py +++ b/tests/unit/plugins/inventory/blackbox/test_kubevirt_ansible_connection_winrm.py @@ -8,7 +8,6 @@ __metaclass__ = type import pytest - from ansible_collections.kubevirt.core.plugins.inventory.kubevirt import ( InventoryOptions, ) @@ -60,19 +59,25 @@ WINDOWS_VMI_4 = merge_dicts( @pytest.mark.parametrize( - "client,vmi,expected", + "vmi,expected", [ - ({"vmis": [BASE_VMI]}, BASE_VMI, False), - ({"vmis": [WINDOWS_VMI_1]}, WINDOWS_VMI_1, True), - ({"vmis": [WINDOWS_VMI_2]}, WINDOWS_VMI_2, True), - ({"vmis": [WINDOWS_VMI_3]}, WINDOWS_VMI_3, True), - ({"vmis": [WINDOWS_VMI_4]}, WINDOWS_VMI_4, True), + (BASE_VMI, False), + (WINDOWS_VMI_1, True), + (WINDOWS_VMI_2, True), + (WINDOWS_VMI_3, True), + (WINDOWS_VMI_4, True), ], - indirect=["client"], ) -def test_ansible_connection_winrm(inventory, hosts, client, vmi, expected): - inventory.populate_inventory_from_namespace( - client, "", DEFAULT_NAMESPACE, InventoryOptions() +def test_ansible_connection_winrm(inventory, hosts, vmi, expected): + inventory.populate_inventory( + { + "default_hostname": "test", + "cluster_domain": "test.com", + "namespaces": { + "default": {"vms": [], "vmis": [vmi], "services": {}}, + }, + }, + InventoryOptions(), ) host = f"{DEFAULT_NAMESPACE}-{vmi['metadata']['name']}" diff --git a/tests/unit/plugins/inventory/blackbox/test_kubevirt_set_composable_vars.py b/tests/unit/plugins/inventory/blackbox/test_kubevirt_set_composable_vars.py index 36be197..18f931c 100644 --- a/tests/unit/plugins/inventory/blackbox/test_kubevirt_set_composable_vars.py +++ b/tests/unit/plugins/inventory/blackbox/test_kubevirt_set_composable_vars.py @@ -6,8 +6,6 @@ from __future__ import absolute_import, division, print_function __metaclass__ = type -import pytest - from ansible_collections.kubevirt.core.plugins.inventory.kubevirt import ( InventoryOptions, ) @@ -36,16 +34,10 @@ VMI = { } -@pytest.mark.parametrize( - "client", - [{"vmis": [VMI]}], - indirect=["client"], -) def test_set_composable_vars( inventory, groups, hosts, - client, ): inventory._options = { "compose": {"set_from_another_var": "vmi_node_name"}, @@ -53,8 +45,15 @@ def test_set_composable_vars( "keyed_groups": [{"prefix": "fedora", "key": "vmi_guest_os_info.versionId"}], "strict": True, } - inventory.populate_inventory_from_namespace( - client, "", DEFAULT_NAMESPACE, InventoryOptions() + inventory.populate_inventory( + { + "default_hostname": "test", + "cluster_domain": "test.com", + "namespaces": { + "default": {"vms": [], "vmis": [VMI], "services": {}}, + }, + }, + InventoryOptions(), ) host = f"{DEFAULT_NAMESPACE}-testvmi" diff --git a/tests/unit/plugins/inventory/blackbox/test_kubevirt_stopped_vm.py b/tests/unit/plugins/inventory/blackbox/test_kubevirt_stopped_vm.py index 1b7a93a..e8cd967 100644 --- a/tests/unit/plugins/inventory/blackbox/test_kubevirt_stopped_vm.py +++ b/tests/unit/plugins/inventory/blackbox/test_kubevirt_stopped_vm.py @@ -6,17 +6,10 @@ from __future__ import absolute_import, division, print_function __metaclass__ = type -import pytest - - from ansible_collections.kubevirt.core.plugins.inventory.kubevirt import ( InventoryOptions, ) -from ansible_collections.kubevirt.core.tests.unit.plugins.inventory.constants import ( - DEFAULT_NAMESPACE, -) - VM1 = { "metadata": { "name": "testvm1", @@ -50,16 +43,16 @@ VMI1 = { } -@pytest.mark.parametrize( - "client", - [ - ({"vms": [VM1, VM2], "vmis": [VMI1]}), - ], - indirect=["client"], -) -def test_stopped_vm(inventory, hosts, client): - inventory.populate_inventory_from_namespace( - client, "", DEFAULT_NAMESPACE, InventoryOptions() +def test_stopped_vm(inventory, hosts): + inventory.populate_inventory( + { + "default_hostname": "test", + "cluster_domain": "test.com", + "namespaces": { + "default": {"vms": [VM1, VM2], "vmis": [VMI1], "services": {}}, + }, + }, + InventoryOptions(), ) # The running VM should be present with ansible_host or ansible_port diff --git a/tests/unit/plugins/inventory/test_kubevirt.py b/tests/unit/plugins/inventory/test_kubevirt.py index a8f0c92..661d5d4 100644 --- a/tests/unit/plugins/inventory/test_kubevirt.py +++ b/tests/unit/plugins/inventory/test_kubevirt.py @@ -10,6 +10,7 @@ import pytest from ansible_collections.kubevirt.core.plugins.inventory.kubevirt import ( InventoryModule, + InventoryOptions, ) from ansible_collections.kubevirt.core.tests.unit.plugins.inventory.constants import ( @@ -257,6 +258,56 @@ def test_get_cluster_domain(inventory, client): assert inventory.get_cluster_domain(client) == DEFAULT_BASE_DOMAIN +@pytest.mark.parametrize( + "results,expected", + [ + ( + { + "cluster_domain": "example.com", + "default_hostname": "test", + "namespaces": {}, + }, + 0, + ), + ( + { + "cluster_domain": "example.com", + "default_hostname": "test", + "namespaces": {"test": {"vms": [], "vmis": [], "services": {}}}, + }, + 1, + ), + ( + { + "cluster_domain": "example.com", + "default_hostname": "test", + "namespaces": { + "test": {"vms": [], "vmis": [], "services": {}}, + "test2": {"vms": [], "vmis": [], "services": {}}, + }, + }, + 2, + ), + ], +) +def test_populate_inventory(mocker, inventory, results, expected): + populate_inventory_from_namespace = mocker.patch.object( + inventory, "populate_inventory_from_namespace" + ) + + inventory.populate_inventory(results, InventoryOptions()) + + opts = InventoryOptions( + base_domain=results["cluster_domain"], name=results["default_hostname"] + ) + calls = [ + mocker.call(namespace, data, opts) + for namespace, data in results["namespaces"].items() + ] + populate_inventory_from_namespace.assert_has_calls(calls) + assert len(calls) == expected + + @pytest.mark.parametrize( "labels,expected", [ diff --git a/tests/unit/plugins/inventory/test_kubevirt_fetch_objects.py b/tests/unit/plugins/inventory/test_kubevirt_fetch_objects.py index 485c41b..5b1ecea 100644 --- a/tests/unit/plugins/inventory/test_kubevirt_fetch_objects.py +++ b/tests/unit/plugins/inventory/test_kubevirt_fetch_objects.py @@ -17,127 +17,93 @@ from ansible_collections.kubevirt.core.tests.unit.plugins.inventory.constants im DEFAULT_NAMESPACE, ) -from ansible_collections.kubevirt.core.plugins.inventory import kubevirt - @pytest.mark.parametrize( - "config_data,expected", + "opts,namespaces", [ ( - None, - { - "name": "default-hostname", - "namespaces": [DEFAULT_NAMESPACE], - "opts": InventoryOptions(), - }, + InventoryOptions(), + [DEFAULT_NAMESPACE], ), ( - { - "name": "test", - }, - { - "name": "test", - "namespaces": [DEFAULT_NAMESPACE], - "opts": InventoryOptions(), - }, + InventoryOptions(namespaces=["test"]), + ["test"], ), ( - {"name": "test", "namespaces": ["test"]}, - {"name": "test", "namespaces": ["test"], "opts": InventoryOptions()}, - ), - ( - { - "name": "test", - "namespaces": ["test"], - "use_service": True, - "create_groups": True, - "append_base_domain": True, - "base_domain": "test-domain", - }, - { - "name": "test", - "namespaces": ["test"], - "opts": InventoryOptions( - use_service=True, - create_groups=True, - append_base_domain=True, - base_domain="test-domain", - ), - }, - ), - ( - { - "name": "test", - "namespaces": ["test"], - "use_service": True, - "create_groups": True, - "append_base_domain": True, - "base_domain": "test-domain", - "network_name": "test-network", - }, - { - "name": "test", - "namespaces": ["test"], - "opts": InventoryOptions( - use_service=True, - create_groups=True, - append_base_domain=True, - base_domain="test-domain", - network_name="test-network", - ), - }, - ), - ( - { - "name": "test", - "namespaces": ["test"], - "use_service": True, - "create_groups": True, - "append_base_domain": True, - "base_domain": "test-domain", - "interface_name": "test-interface", - }, - { - "name": "test", - "namespaces": ["test"], - "opts": InventoryOptions( - use_service=True, - create_groups=True, - append_base_domain=True, - base_domain="test-domain", - network_name="test-interface", - ), - }, + InventoryOptions(namespaces=["test1", "test2"]), + ["test1", "test2"], ), ], ) -def test_fetch_objects(mocker, inventory, config_data, expected): - mocker.patch.object(kubevirt, "get_api_client") - mocker.patch.object( - inventory, "get_default_host_name", return_value="default-hostname" - ) - - cluster_domain = "test.com" - mocker.patch.object(inventory, "get_cluster_domain", return_value=cluster_domain) - expected["opts"].base_domain = expected["opts"].base_domain or cluster_domain - +def test_fetch_objects(mocker, inventory, opts, namespaces): get_available_namespaces = mocker.patch.object( inventory, "get_available_namespaces", return_value=[DEFAULT_NAMESPACE] ) - populate_inventory_from_namespace = mocker.patch.object( - inventory, "populate_inventory_from_namespace" + get_vms_for_namespace = mocker.patch.object( + inventory, "get_vms_for_namespace", return_value=[{}] + ) + get_vmis_for_namespace = mocker.patch.object( + inventory, "get_vmis_for_namespace", return_value=[{}] + ) + get_ssh_services_for_namespace = mocker.patch.object( + inventory, "get_ssh_services_for_namespace", return_value=[] + ) + get_default_hostname = mocker.patch.object( + inventory, "get_default_hostname", return_value="default-hostname" + ) + get_cluster_domain = mocker.patch.object( + inventory, "get_cluster_domain", return_value="test.com" ) - inventory.fetch_objects(config_data) + inventory.fetch_objects(mocker.Mock(), opts) - if config_data and "namespaces" in config_data: + if opts.namespaces: get_available_namespaces.assert_not_called() else: get_available_namespaces.assert_called() - populate_inventory_from_namespace.assert_has_calls( - [ - mocker.call(mocker.ANY, expected["name"], namespace, expected["opts"]) - for namespace in expected["namespaces"] - ] + get_vms_for_namespace.assert_has_calls( + [mocker.call(mocker.ANY, namespace, opts) for namespace in namespaces] ) + get_vmis_for_namespace.assert_has_calls( + [mocker.call(mocker.ANY, namespace, opts) for namespace in namespaces] + ) + get_ssh_services_for_namespace.assert_has_calls( + [mocker.call(mocker.ANY, namespace) for namespace in namespaces] + ) + get_default_hostname.assert_called_once() + get_cluster_domain.assert_called_once() + + +def test_fetch_objects_early_return(mocker, inventory): + get_available_namespaces = mocker.patch.object( + inventory, "get_available_namespaces", return_value=[DEFAULT_NAMESPACE] + ) + get_vms_for_namespace = mocker.patch.object( + inventory, "get_vms_for_namespace", return_value=[] + ) + get_vmis_for_namespace = mocker.patch.object( + inventory, "get_vmis_for_namespace", return_value=[] + ) + get_ssh_services_for_namespace = mocker.patch.object( + inventory, "get_ssh_services_for_namespace" + ) + get_default_hostname = mocker.patch.object( + inventory, "get_default_hostname", return_value="default-hostname" + ) + get_cluster_domain = mocker.patch.object( + inventory, "get_cluster_domain", return_value="test.com" + ) + + inventory.fetch_objects(mocker.Mock(), InventoryOptions()) + + get_available_namespaces.assert_called_once() + get_vms_for_namespace.assert_called_once_with( + mocker.ANY, DEFAULT_NAMESPACE, InventoryOptions() + ) + get_vmis_for_namespace.assert_called_once_with( + mocker.ANY, DEFAULT_NAMESPACE, InventoryOptions() + ) + get_ssh_services_for_namespace.assert_not_called() + get_default_hostname.assert_called_once() + get_cluster_domain.assert_called_once() diff --git a/tests/unit/plugins/inventory/test_kubevirt_parse.py b/tests/unit/plugins/inventory/test_kubevirt_parse.py index 634752b..f7729c6 100644 --- a/tests/unit/plugins/inventory/test_kubevirt_parse.py +++ b/tests/unit/plugins/inventory/test_kubevirt_parse.py @@ -13,39 +13,151 @@ from ansible_collections.kubevirt.core.plugins.inventory import ( ) from ansible_collections.kubevirt.core.plugins.inventory.kubevirt import ( + InventoryOptions, KubeVirtInventoryException, ) @pytest.mark.parametrize( - "cache", + "config_data,expected", [ - True, - False, + ( + {}, + InventoryOptions(), + ), + ( + { + "name": "test", + }, + InventoryOptions(name="test"), + ), + ( + {"name": "test", "namespaces": ["test"]}, + InventoryOptions(name="test", namespaces=["test"]), + ), + ( + { + "name": "test", + "namespaces": ["test"], + "use_service": True, + "create_groups": True, + "append_base_domain": True, + "base_domain": "test-domain", + }, + InventoryOptions( + use_service=True, + create_groups=True, + append_base_domain=True, + base_domain="test-domain", + name="test", + namespaces=["test"], + ), + ), + ( + { + "name": "test", + "namespaces": ["test"], + "use_service": True, + "create_groups": True, + "append_base_domain": True, + "base_domain": "test-domain", + "network_name": "test-network", + }, + InventoryOptions( + use_service=True, + create_groups=True, + append_base_domain=True, + base_domain="test-domain", + network_name="test-network", + name="test", + namespaces=["test"], + ), + ), + ( + { + "name": "test", + "namespaces": ["test"], + "use_service": True, + "create_groups": True, + "append_base_domain": True, + "base_domain": "test-domain", + "interface_name": "test-interface", + }, + InventoryOptions( + use_service=True, + create_groups=True, + append_base_domain=True, + base_domain="test-domain", + network_name="test-interface", + name="test", + namespaces=["test"], + ), + ), ], ) -def test_parse(mocker, inventory, cache): - path = "/testpath" - cache_prefix = "test-prefix" - config_data = {"host_format": "test-format"} - - mocker.patch.dict(inventory._cache, {cache_prefix: {"test-key": "test-value"}}) - get_cache_prefix = mocker.patch.object( - inventory, "_get_cache_prefix", return_value=cache_prefix - ) +def test_config_data_to_opts(mocker, inventory, config_data, expected): read_config_data = mocker.patch.object( inventory, "_read_config_data", return_value=config_data ) + mocker.patch.object(inventory, "get_cache_key") + mocker.patch.object(inventory, "get_option") + mocker.patch.object(kubevirt, "get_api_client") + mocker.patch.object(inventory, "fetch_objects") + populate_inventory = mocker.patch.object(inventory, "populate_inventory") + + inventory.parse(None, None, "", False) + + populate_inventory.assert_called_once_with(mocker.ANY, expected) + + +@pytest.mark.parametrize( + "cache_parse,cache_option,cache_data,expected", + [ + (True, True, {"test-key": {"something": "something"}}, True), + (None, True, {"test-key": {"something": "something"}}, True), + (False, True, {"test-key": {"something": "something"}}, False), + (True, False, {"test-key": {"something": "something"}}, False), + (None, False, {"test-key": {"something": "something"}}, False), + (False, False, {"test-key": {"something": "something"}}, False), + (True, True, {"test-key2": {"something": "something"}}, False), + (None, True, {"test-key2": {"something": "something"}}, False), + ], +) +def test_use_of_cache( + mocker, inventory, cache_parse, cache_option, cache_data, expected +): + path = "/testpath" + config_data = {"host_format": "test-format"} + + mocker.patch.dict(inventory._cache, cache_data) + + read_config_data = mocker.patch.object( + inventory, "_read_config_data", return_value=config_data + ) + get_cache_key = mocker.patch.object( + inventory, "get_cache_key", return_value="test-key" + ) + get_option = mocker.patch.object(inventory, "get_option", return_value=cache_option) + get_api_client = mocker.patch.object(kubevirt, "get_api_client") fetch_objects = mocker.patch.object(inventory, "fetch_objects") + populate_inventory = mocker.patch.object(inventory, "populate_inventory") - inventory.parse(None, None, path, cache) + if cache_parse is None: + inventory.parse(None, None, path) + else: + inventory.parse(None, None, path, cache_parse) - get_cache_prefix.assert_called_once_with(path) + opts = InventoryOptions(config_data=config_data) + get_cache_key.assert_called_once_with(path) + get_option.assert_called_once_with("cache") read_config_data.assert_called_once_with(path) - if cache: + if expected: + get_api_client.assert_not_called() fetch_objects.assert_not_called() else: - fetch_objects.assert_called_once_with(config_data) + get_api_client.assert_called_once_with(**config_data) + fetch_objects.assert_called_once_with(mocker.ANY, opts) + populate_inventory.assert_called_once_with(mocker.ANY, opts) @pytest.mark.parametrize( @@ -57,8 +169,10 @@ def test_parse(mocker, inventory, cache): ) def test_k8s_client_missing(mocker, inventory, present): mocker.patch.object(kubevirt, "HAS_K8S_MODULE_HELPER", present) - mocker.patch.object(inventory, "_get_cache_prefix") - mocker.patch.object(inventory, "_read_config_data") + mocker.patch.object(kubevirt, "get_api_client") + mocker.patch.object(inventory, "_read_config_data", return_value={}) + mocker.patch.object(inventory, "get_cache_key") + mocker.patch.object(inventory, "get_option") fetch_objects = mocker.patch.object(inventory, "fetch_objects") if present: diff --git a/tests/unit/plugins/inventory/test_kubevirt_populate_inventory_from_namespace.py b/tests/unit/plugins/inventory/test_kubevirt_populate_inventory_from_namespace.py index c368064..bff847f 100644 --- a/tests/unit/plugins/inventory/test_kubevirt_populate_inventory_from_namespace.py +++ b/tests/unit/plugins/inventory/test_kubevirt_populate_inventory_from_namespace.py @@ -83,7 +83,7 @@ def test_populate_inventory_from_namespace( ): _vms = {vm["metadata"]["name"]: vm for vm in vms} _vmis = {vmi["metadata"]["name"]: vmi for vmi in vmis} - opts = InventoryOptions() + opts = InventoryOptions(name="test") def format_hostname(obj): return opts.host_format.format( @@ -99,6 +99,7 @@ def test_populate_inventory_from_namespace( f"namespace_{DEFAULT_NAMESPACE}", ) + obj_is_valid_calls = [] add_host_side_effects = [] add_host_calls = [] set_vars_from_vm_calls = [] @@ -108,33 +109,27 @@ def test_populate_inventory_from_namespace( # For each VM add the expected calls # Also add expected calls for VMIs for which a VM exists for name, vm in _vms.items(): + obj_is_valid_calls.append(mocker.call(vm)) hostname = format_hostname(vm) add_host_side_effects.append(hostname) add_host_calls.append(add_host_call(vm)) set_vars_from_vm_calls.append(mocker.call(hostname, vm, opts)) if name in _vmis.keys(): - set_vars_from_vmi_calls.append(mocker.call(hostname, _vmis[name], [], opts)) + set_vars_from_vmi_calls.append(mocker.call(hostname, _vmis[name], {}, opts)) set_composable_vars_calls.append(mocker.call(hostname)) # For each VMI add the expected calls # Do not add for VMIs for which a VM exists for name, vmi in _vmis.items(): + obj_is_valid_calls.append(mocker.call(vmi)) if name not in _vms.keys(): hostname = format_hostname(vmi) add_host_side_effects.append(hostname) add_host_calls.append(add_host_call(vmi)) - set_vars_from_vmi_calls.append(mocker.call(hostname, vmi, [], opts)) + set_vars_from_vmi_calls.append(mocker.call(hostname, vmi, {}, opts)) set_composable_vars_calls.append(mocker.call(hostname)) - get_vms_for_namespace = mocker.patch.object( - inventory, "get_vms_for_namespace", return_value=_vms.values() - ) - get_vmis_for_namespace = mocker.patch.object( - inventory, "get_vmis_for_namespace", return_value=_vmis.values() - ) - get_ssh_services_for_namespace = mocker.patch.object( - inventory, "get_ssh_services_for_namespace", return_value=[] - ) + obj_is_valid = mocker.patch.object(inventory, "obj_is_valid", return_value=True) add_host = mocker.patch.object( inventory, "add_host", side_effect=add_host_side_effects ) @@ -142,23 +137,20 @@ def test_populate_inventory_from_namespace( set_vars_from_vmi = mocker.patch.object(inventory, "set_vars_from_vmi") set_composable_vars = mocker.patch.object(inventory, "set_composable_vars") - inventory.populate_inventory_from_namespace(None, "test", DEFAULT_NAMESPACE, opts) - - # These should always get called once - get_vms_for_namespace.assert_called_once_with(None, DEFAULT_NAMESPACE, opts) - get_vmis_for_namespace.assert_called_once_with(None, DEFAULT_NAMESPACE, opts) + inventory.populate_inventory_from_namespace( + DEFAULT_NAMESPACE, {"vms": vms, "vmis": vmis, "services": {}}, opts + ) # Assert it tries to add the expected vars for all provided VMs/VMIs + obj_is_valid.assert_has_calls(obj_is_valid_calls) set_vars_from_vm.assert_has_calls(set_vars_from_vm_calls) set_vars_from_vmi.assert_has_calls(set_vars_from_vmi_calls) set_composable_vars.assert_has_calls(set_composable_vars_calls) # If no VMs or VMIs were provided the function should not add any groups if vms or vmis: - get_ssh_services_for_namespace.assert_called_once_with(None, DEFAULT_NAMESPACE) assert list(groups.keys()) == ["test", f"namespace_{DEFAULT_NAMESPACE}"] else: - get_ssh_services_for_namespace.assert_not_called() assert not list(groups.keys()) # Assert the expected amount of hosts was added