From 42fd3fecfd8ed9fc8fbf6fdf48301882b0d7fbc8 Mon Sep 17 00:00:00 2001 From: Javier Cano Cano Date: Wed, 6 May 2026 15:49:05 +0200 Subject: [PATCH] feat(inventory): add default_win_ansible_connection option It adds the option `default_win_ansible_connection` to allow users to configure the default `ansible_connection` for Windows VMs detected by the inventory plugin, instead of always hardcoding `winrm`. This enables use of alternative connection plugins like `psrp` or `ssh` without per-host overrides. Defaults to `winrm` for backward compatibility. Assisted-by: Claude Opus 4.6 (1M context) Signed-off-by: Javier Cano Cano --- plugins/inventory/kubevirt.py | 62 ++++++++++++++----- ...> test_kubevirt_win_ansible_connection.py} | 62 ++++++++++++++----- .../test_kubevirt_inventory_options.py | 4 ++ .../test_kubevirt_set_vars_from_vmi.py | 42 ++++++++++++- 4 files changed, 139 insertions(+), 31 deletions(-) rename tests/unit/plugins/inventory/blackbox/{test_kubevirt_ansible_connection_winrm.py => test_kubevirt_win_ansible_connection.py} (58%) diff --git a/plugins/inventory/kubevirt.py b/plugins/inventory/kubevirt.py index 5cc3415..dbf0ef0 100644 --- a/plugins/inventory/kubevirt.py +++ b/plugins/inventory/kubevirt.py @@ -83,6 +83,21 @@ options: - Append the base domain of the cluster to host names constructed from SSH C(Services) of type C(NodePort). type: bool default: False + default_win_ansible_connection: + description: + - Set the default value of C(ansible_connection) when a Windows virtual machines is detected. + - Common values include V(winrm), V(ansible.builtin.winrm), V(psrp), V(ansible.builtin.psrp), V(ssh), and V(ansible.builtin.ssh). + - When set to V(ssh) or V(ansible.builtin.ssh), the service port lookup uses SSH port (22) instead of WinRM/PSRP ports (5986/5985). + type: str + choices: + - winrm + - ansible.builtin.winrm + - psrp + - ansible.builtin.psrp + - ssh + - ansible.builtin.ssh + default: "winrm" + version_added: 2.3.0 api_version: description: - Specify the used KubeVirt API version. @@ -183,8 +198,8 @@ TYPE_LOADBALANCER = "LoadBalancer" TYPE_NODEPORT = "NodePort" ID_MSWINDOWS = "mswindows" SERVICE_TARGET_PORT_SSH = 22 -SERVICE_TARGET_PORT_WINRM_HTTP = 5985 -SERVICE_TARGET_PORT_WINRM_HTTPS = 5986 +SERVICE_TARGET_PORT_WIN_MGMT_HTTP = 5985 +SERVICE_TARGET_PORT_WIN_MGMT_HTTPS = 5986 class KubeVirtInventoryException(Exception): @@ -208,6 +223,7 @@ class InventoryOptions: create_groups: Optional[bool] = None base_domain: Optional[str] = None append_base_domain: Optional[bool] = None + default_win_ansible_connection: Optional[str] = None host_format: Optional[str] = None namespaces: Optional[List[str]] = None name: Optional[str] = None @@ -263,6 +279,11 @@ class InventoryOptions: if self.append_base_domain is not None else config_data.get("append_base_domain", False) ) + self.default_win_ansible_connection = ( + self.default_win_ansible_connection + if self.default_win_ansible_connection is not None + else config_data.get("default_win_ansible_connection", "winrm") + ) self.host_format = ( self.host_format if self.host_format is not None @@ -657,8 +678,8 @@ class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable): or ports[0].get("targetPort") not in [ SERVICE_TARGET_PORT_SSH, - SERVICE_TARGET_PORT_WINRM_HTTP, - SERVICE_TARGET_PORT_WINRM_HTTPS, + SERVICE_TARGET_PORT_WIN_MGMT_HTTP, + SERVICE_TARGET_PORT_WIN_MGMT_HTTPS, ] ): continue @@ -790,23 +811,36 @@ class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable): ) # Set up the connection - service = None - if self._is_windows( + ansible_connection = ( + self.inventory.get_host(hostname).get_vars().get("ansible_connection") + ) + if ansible_connection is None and self._is_windows( vmi["status"].get("guestOSInfo", {}), vmi["metadata"].get("annotations", {}), ): - self.inventory.set_variable(hostname, "ansible_connection", "winrm") - service = self._find_service_with_target_port( - _services, SERVICE_TARGET_PORT_WINRM_HTTPS + ansible_connection = opts.default_win_ansible_connection + self.inventory.set_variable( + hostname, "ansible_connection", ansible_connection ) - if service is None: - service = self._find_service_with_target_port( - _services, SERVICE_TARGET_PORT_WINRM_HTTP - ) - else: + + service = None + if ansible_connection in (None, "ssh", "ansible.builtin.ssh"): service = self._find_service_with_target_port( _services, SERVICE_TARGET_PORT_SSH ) + elif ansible_connection in ( + "winrm", + "ansible.builtin.winrm", + "psrp", + "ansible.builtin.psrp", + ): + service = self._find_service_with_target_port( + _services, SERVICE_TARGET_PORT_WIN_MGMT_HTTPS + ) + if service is None: + service = self._find_service_with_target_port( + _services, SERVICE_TARGET_PORT_WIN_MGMT_HTTP + ) self._set_ansible_host_and_port( vmi, diff --git a/tests/unit/plugins/inventory/blackbox/test_kubevirt_ansible_connection_winrm.py b/tests/unit/plugins/inventory/blackbox/test_kubevirt_win_ansible_connection.py similarity index 58% rename from tests/unit/plugins/inventory/blackbox/test_kubevirt_ansible_connection_winrm.py rename to tests/unit/plugins/inventory/blackbox/test_kubevirt_win_ansible_connection.py index 8b4dfaa..3735c22 100644 --- a/tests/unit/plugins/inventory/blackbox/test_kubevirt_ansible_connection_winrm.py +++ b/tests/unit/plugins/inventory/blackbox/test_kubevirt_win_ansible_connection.py @@ -78,23 +78,54 @@ SVC_LB_WINRM_HTTPS = { }, "status": {"loadBalancer": {"ingress": [{"ip": "192.168.1.100"}]}}, } +SVC_LB_SSH = merge_dicts( + SVC_LB_WINRM_HTTPS, + { + "metadata": { + "name": "test-lb-ssh", + "uid": "33a31042-f58c-5044-cf78-ef211cedb1d4", + }, + "spec": { + "ports": [ + { + "protocol": "TCP", + "port": 12345, + "targetPort": 22, + }, + ], + }, + }, +) @pytest.mark.parametrize( - "vmi,expected,use_service", + "vmi,expected,use_service,default_win_ansible_connection,services", [ - (BASE_VMI, False, False), - (WINDOWS_VMI_1, True, True), - (WINDOWS_VMI_2, True, True), - (WINDOWS_VMI_3, True, True), - (WINDOWS_VMI_4, True, True), - (WINDOWS_VMI_1, True, False), - (WINDOWS_VMI_2, True, False), - (WINDOWS_VMI_3, True, False), - (WINDOWS_VMI_4, True, False), + (BASE_VMI, False, False, "winrm", [SVC_LB_WINRM_HTTPS]), + (WINDOWS_VMI_1, True, True, "winrm", [SVC_LB_WINRM_HTTPS]), + (WINDOWS_VMI_2, True, True, "winrm", [SVC_LB_WINRM_HTTPS]), + (WINDOWS_VMI_3, True, True, "winrm", [SVC_LB_WINRM_HTTPS]), + (WINDOWS_VMI_4, True, True, "winrm", [SVC_LB_WINRM_HTTPS]), + (WINDOWS_VMI_1, True, False, "winrm", [SVC_LB_WINRM_HTTPS]), + (WINDOWS_VMI_2, True, False, "winrm", [SVC_LB_WINRM_HTTPS]), + (WINDOWS_VMI_3, True, False, "winrm", [SVC_LB_WINRM_HTTPS]), + (WINDOWS_VMI_4, True, False, "winrm", [SVC_LB_WINRM_HTTPS]), + (WINDOWS_VMI_1, True, True, "psrp", [SVC_LB_WINRM_HTTPS]), + (WINDOWS_VMI_1, True, False, "psrp", [SVC_LB_WINRM_HTTPS]), + (WINDOWS_VMI_2, True, True, "ansible.builtin.psrp", [SVC_LB_WINRM_HTTPS]), + (WINDOWS_VMI_3, True, False, "ssh", [SVC_LB_SSH]), + (WINDOWS_VMI_4, True, True, "ansible.builtin.ssh", [SVC_LB_SSH]), ], ) -def test_ansible_connection_winrm(inventory, hosts, vmi, expected, use_service): +def test_default_win_ansible_connection( + inventory, + hosts, + vmi, + expected, + use_service, + default_win_ansible_connection, + services, +): inventory._populate_inventory( { "default_hostname": "test", @@ -103,16 +134,19 @@ def test_ansible_connection_winrm(inventory, hosts, vmi, expected, use_service): "default": { "vms": [], "vmis": [vmi], - "services": {"testdomain": [SVC_LB_WINRM_HTTPS]}, + "services": {"testdomain": services}, } }, }, - InventoryOptions(use_service=use_service), + InventoryOptions( + use_service=use_service, + default_win_ansible_connection=default_win_ansible_connection, + ), ) host = f"{DEFAULT_NAMESPACE}-{vmi['metadata']['name']}" if expected: - assert hosts[host]["ansible_connection"] == "winrm" + assert hosts[host]["ansible_connection"] == default_win_ansible_connection else: assert "ansible_connection" not in hosts[host] if use_service: diff --git a/tests/unit/plugins/inventory/test_kubevirt_inventory_options.py b/tests/unit/plugins/inventory/test_kubevirt_inventory_options.py index 68c2989..d073b06 100644 --- a/tests/unit/plugins/inventory/test_kubevirt_inventory_options.py +++ b/tests/unit/plugins/inventory/test_kubevirt_inventory_options.py @@ -22,6 +22,7 @@ def test_inventory_options_defaults(): assert opts.create_groups is False assert opts.base_domain is None assert opts.append_base_domain is False + assert opts.default_win_ansible_connection == "winrm" assert opts.host_format == "{namespace}-{name}" @@ -35,6 +36,7 @@ def test_inventory_options_override_defaults(): create_groups = True base_domain = "test-domain.com" append_base_domain = True + default_win_ansible_connection = "psrp" host_format = "{name}-testhost" opts = InventoryOptions( @@ -47,6 +49,7 @@ def test_inventory_options_override_defaults(): create_groups=create_groups, base_domain=base_domain, append_base_domain=append_base_domain, + default_win_ansible_connection=default_win_ansible_connection, host_format=host_format, ) assert opts.api_version == api_version @@ -58,4 +61,5 @@ def test_inventory_options_override_defaults(): assert opts.create_groups == create_groups assert opts.base_domain == base_domain assert opts.append_base_domain == append_base_domain + assert opts.default_win_ansible_connection == default_win_ansible_connection assert opts.host_format == host_format diff --git a/tests/unit/plugins/inventory/test_kubevirt_set_vars_from_vmi.py b/tests/unit/plugins/inventory/test_kubevirt_set_vars_from_vmi.py index dbb5325..8845ecd 100644 --- a/tests/unit/plugins/inventory/test_kubevirt_set_vars_from_vmi.py +++ b/tests/unit/plugins/inventory/test_kubevirt_set_vars_from_vmi.py @@ -86,17 +86,38 @@ def test_ignore_vmi_without_named_interface(mocker, inventory): set_ansible_host_and_port.assert_not_called() -def test_set_winrm_if_windows(mocker, inventory): +@pytest.mark.parametrize( + "default_win_ansible_connection", + [ + "winrm", + "ansible.builtin.winrm", + "psrp", + "ansible.builtin.psrp", + "ssh", + "ansible.builtin.ssh", + ], +) +def test_set_connection_if_windows(mocker, inventory, default_win_ansible_connection): mocker.patch.object(inventory, "_set_common_vars") mocker.patch.object(inventory, "_is_windows", return_value=True) mocker.patch.object(inventory, "_set_ansible_host_and_port") + mocker.patch.object( + inventory.inventory, + "get_host", + return_value=mocker.Mock(get_vars=mocker.Mock(return_value={})), + ) set_variable = mocker.patch.object(inventory.inventory, "set_variable") hostname = "default-testvm" vmi = {"metadata": {}, "status": {"interfaces": [{"ipAddress": "1.1.1.1"}]}} - inventory._set_vars_from_vmi(hostname, vmi, {}, InventoryOptions()) + opts = InventoryOptions( + default_win_ansible_connection=default_win_ansible_connection + ) + inventory._set_vars_from_vmi(hostname, vmi, {}, opts) - set_variable.assert_called_once_with(hostname, "ansible_connection", "winrm") + set_variable.assert_called_once_with( + hostname, "ansible_connection", default_win_ansible_connection + ) @pytest.mark.parametrize( @@ -110,6 +131,11 @@ def test_set_winrm_if_windows(mocker, inventory): def test_service_lookup(mocker, inventory, is_windows, target_port): mocker.patch.object(inventory, "_set_common_vars") mocker.patch.object(inventory, "_is_windows", return_value=is_windows) + mocker.patch.object( + inventory.inventory, + "get_host", + return_value=mocker.Mock(get_vars=mocker.Mock(return_value={})), + ) set_ansible_host_and_port = mocker.patch.object( inventory, "_set_ansible_host_and_port" ) @@ -144,6 +170,11 @@ def test_service_ignore_not_matching_connection( ): mocker.patch.object(inventory, "_set_common_vars") mocker.patch.object(inventory, "_is_windows", return_value=is_windows) + mocker.patch.object( + inventory.inventory, + "get_host", + return_value=mocker.Mock(get_vars=mocker.Mock(return_value={})), + ) set_ansible_host_and_port = mocker.patch.object( inventory, "_set_ansible_host_and_port" ) @@ -168,6 +199,11 @@ def test_service_ignore_not_matching_connection( def test_service_prefer_winrm_https(mocker, inventory): mocker.patch.object(inventory, "_set_common_vars") mocker.patch.object(inventory, "_is_windows", return_value=True) + mocker.patch.object( + inventory.inventory, + "get_host", + return_value=mocker.Mock(get_vars=mocker.Mock(return_value={})), + ) set_ansible_host_and_port = mocker.patch.object( inventory, "_set_ansible_host_and_port" )