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) <noreply@anthropic.com>
Signed-off-by: Javier Cano Cano <jcanocan@redhat.com>
This commit is contained in:
Javier Cano Cano
2026-05-06 15:49:05 +02:00
parent 19cc611159
commit 42fd3fecfd
4 changed files with 139 additions and 31 deletions

View File

@@ -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,22 +811,35 @@ 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")
ansible_connection = opts.default_win_ansible_connection
self.inventory.set_variable(
hostname, "ansible_connection", ansible_connection
)
service = None
if ansible_connection in (None, "ssh", "ansible.builtin.ssh"):
service = self._find_service_with_target_port(
_services, SERVICE_TARGET_PORT_WINRM_HTTPS
_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_WINRM_HTTP
)
else:
service = self._find_service_with_target_port(
_services, SERVICE_TARGET_PORT_SSH
_services, SERVICE_TARGET_PORT_WIN_MGMT_HTTP
)
self._set_ansible_host_and_port(

View File

@@ -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:

View File

@@ -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

View File

@@ -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"
)