mirror of
https://github.com/kubevirt/kubevirt.core.git
synced 2026-07-29 19:04:35 +00:00
Merge pull request #75 from 0xFelix/winrm
feat: Set ansible_connection to winrm for Windows hosts
This commit is contained in:
@@ -202,10 +202,13 @@ from ansible_collections.kubernetes.core.plugins.module_utils.k8s.client import
|
|||||||
K8SClient,
|
K8SClient,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
ANNOTATION_KUBEVIRT_IO_CLUSTER_PREFERENCE_NAME = "kubevirt.io/cluster-preference-name"
|
||||||
|
ANNOTATION_KUBEVIRT_IO_PREFERENCE_NAME = "kubevirt.io/preference-name"
|
||||||
|
ANNOTATION_VM_KUBEVIRT_IO_OS = "vm.kubevirt.io/os"
|
||||||
LABEL_KUBEVIRT_IO_DOMAIN = "kubevirt.io/domain"
|
LABEL_KUBEVIRT_IO_DOMAIN = "kubevirt.io/domain"
|
||||||
TYPE_LOADBALANCER = "LoadBalancer"
|
TYPE_LOADBALANCER = "LoadBalancer"
|
||||||
TYPE_NODEPORT = "NodePort"
|
TYPE_NODEPORT = "NodePort"
|
||||||
|
ID_MSWINDOWS = "mswindows"
|
||||||
|
|
||||||
|
|
||||||
class KubeVirtInventoryException(Exception):
|
class KubeVirtInventoryException(Exception):
|
||||||
@@ -318,6 +321,26 @@ class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable):
|
|||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def is_windows(guest_os_info: Dict, annotations: Dict) -> bool:
|
||||||
|
if "id" in guest_os_info:
|
||||||
|
return guest_os_info["id"] == ID_MSWINDOWS
|
||||||
|
|
||||||
|
if ANNOTATION_KUBEVIRT_IO_CLUSTER_PREFERENCE_NAME in annotations:
|
||||||
|
return annotations[
|
||||||
|
ANNOTATION_KUBEVIRT_IO_CLUSTER_PREFERENCE_NAME
|
||||||
|
].startswith("windows")
|
||||||
|
|
||||||
|
if ANNOTATION_KUBEVIRT_IO_PREFERENCE_NAME in annotations:
|
||||||
|
return annotations[ANNOTATION_KUBEVIRT_IO_PREFERENCE_NAME].startswith(
|
||||||
|
"windows"
|
||||||
|
)
|
||||||
|
|
||||||
|
if ANNOTATION_VM_KUBEVIRT_IO_OS in annotations:
|
||||||
|
return annotations[ANNOTATION_VM_KUBEVIRT_IO_OS].startswith("windows")
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.host_format = None
|
self.host_format = None
|
||||||
@@ -523,16 +546,6 @@ class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable):
|
|||||||
self.inventory.add_group(group)
|
self.inventory.add_group(group)
|
||||||
self.inventory.add_child(group, vmi_name)
|
self.inventory.add_child(group, vmi_name)
|
||||||
|
|
||||||
# Set up the connection
|
|
||||||
self.inventory.set_variable(vmi_name, "ansible_connection", "ssh")
|
|
||||||
self.set_ansible_host_and_port(
|
|
||||||
vmi,
|
|
||||||
vmi_name,
|
|
||||||
interface.ipAddress,
|
|
||||||
services.get(vmi.metadata.labels.get(LABEL_KUBEVIRT_IO_DOMAIN)),
|
|
||||||
opts,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add hostvars from metadata
|
# Add hostvars from metadata
|
||||||
self.inventory.set_variable(vmi_name, "object_type", "vmi")
|
self.inventory.set_variable(vmi_name, "object_type", "vmi")
|
||||||
self.inventory.set_variable(vmi_name, "labels", vmi_labels)
|
self.inventory.set_variable(vmi_name, "labels", vmi_labels)
|
||||||
@@ -605,6 +618,23 @@ class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable):
|
|||||||
self.inventory.set_variable(
|
self.inventory.set_variable(
|
||||||
vmi_name, "vmi_volume_status", vmi_volume_status
|
vmi_name, "vmi_volume_status", vmi_volume_status
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Set up the connection
|
||||||
|
service = None
|
||||||
|
if self.is_windows(vmi_guest_os_info, vmi_annotations):
|
||||||
|
self.inventory.set_variable(vmi_name, "ansible_connection", "winrm")
|
||||||
|
else:
|
||||||
|
service = services.get(
|
||||||
|
vmi.metadata.labels.get(LABEL_KUBEVIRT_IO_DOMAIN)
|
||||||
|
)
|
||||||
|
self.set_ansible_host_and_port(
|
||||||
|
vmi,
|
||||||
|
vmi_name,
|
||||||
|
interface.ipAddress,
|
||||||
|
service,
|
||||||
|
opts,
|
||||||
|
)
|
||||||
|
|
||||||
self.set_composable_vars(vmi_name)
|
self.set_composable_vars(vmi_name)
|
||||||
|
|
||||||
def set_composable_vars(self, vmi_name):
|
def set_composable_vars(self, vmi_name):
|
||||||
@@ -614,9 +644,15 @@ class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable):
|
|||||||
"""
|
"""
|
||||||
host_vars = self.inventory.get_host(vmi_name).get_vars()
|
host_vars = self.inventory.get_host(vmi_name).get_vars()
|
||||||
strict = self.get_option("strict")
|
strict = self.get_option("strict")
|
||||||
self._set_composite_vars(self.get_option("compose"), host_vars, vmi_name, strict=True)
|
self._set_composite_vars(
|
||||||
self._add_host_to_composed_groups(self.get_option("groups"), host_vars, vmi_name, strict=strict)
|
self.get_option("compose"), host_vars, vmi_name, strict=True
|
||||||
self._add_host_to_keyed_groups(self.get_option("keyed_groups"), host_vars, vmi_name, strict=strict)
|
)
|
||||||
|
self._add_host_to_composed_groups(
|
||||||
|
self.get_option("groups"), host_vars, vmi_name, strict=strict
|
||||||
|
)
|
||||||
|
self._add_host_to_keyed_groups(
|
||||||
|
self.get_option("keyed_groups"), host_vars, vmi_name, strict=strict
|
||||||
|
)
|
||||||
|
|
||||||
def get_ssh_services_for_namespace(self, client: K8SClient, namespace: str) -> Dict:
|
def get_ssh_services_for_namespace(self, client: K8SClient, namespace: str) -> Dict:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
addict
|
||||||
pytest
|
pytest
|
||||||
pytest-ansible
|
pytest-ansible
|
||||||
pytest-mock
|
pytest-mock
|
||||||
|
|||||||
@@ -8,12 +8,131 @@ __metaclass__ = type
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from ansible_collections.kubevirt.core.plugins.inventory.kubevirt import InventoryModule
|
from addict import Dict
|
||||||
|
|
||||||
|
from ansible_collections.kubevirt.core.plugins.inventory.kubevirt import (
|
||||||
|
GetVmiOptions,
|
||||||
|
InventoryModule,
|
||||||
|
)
|
||||||
|
|
||||||
|
from ansible_collections.kubevirt.core.tests.unit.utils.merge_dicts import (
|
||||||
|
merge_dicts,
|
||||||
|
)
|
||||||
|
|
||||||
|
DEFAULT_NAMESPACE = "default"
|
||||||
|
DEFAULT_BASE_DOMAIN = "example.com"
|
||||||
|
|
||||||
|
BASE_VMI = {
|
||||||
|
"apiVersion": "kubevirt.io/v1",
|
||||||
|
"kind": "VirtualMachineInstance",
|
||||||
|
"metadata": {
|
||||||
|
"name": "testvmi",
|
||||||
|
"namespace": "default",
|
||||||
|
"uid": "f8abae7c-d792-4b9b-af95-62d322ae5bc1",
|
||||||
|
},
|
||||||
|
"spec": {
|
||||||
|
"domain": {"devices": {}},
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"interfaces": [{"ipAddress": "10.10.10.10"}],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
WINDOWS_VMI_1 = merge_dicts(
|
||||||
|
BASE_VMI,
|
||||||
|
{
|
||||||
|
"status": {
|
||||||
|
"guestOSInfo": {"id": "mswindows"},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
WINDOWS_VMI_2 = merge_dicts(
|
||||||
|
BASE_VMI,
|
||||||
|
{
|
||||||
|
"metadata": {
|
||||||
|
"annotations": {"kubevirt.io/cluster-preference-name": "windows.2k22"}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
WINDOWS_VMI_3 = merge_dicts(
|
||||||
|
BASE_VMI,
|
||||||
|
{
|
||||||
|
"metadata": {"annotations": {"kubevirt.io/preference-name": "windows.2k22"}},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
WINDOWS_VMI_4 = merge_dicts(
|
||||||
|
BASE_VMI,
|
||||||
|
{
|
||||||
|
"metadata": {"annotations": {"vm.kubevirt.io/os": "windows2k22"}},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture(scope="function")
|
||||||
def inventory():
|
def inventory(mocker):
|
||||||
return InventoryModule()
|
inventory = InventoryModule()
|
||||||
|
inventory.inventory = mocker.Mock()
|
||||||
|
mocker.patch.object(inventory, "set_composable_vars")
|
||||||
|
return inventory
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="function")
|
||||||
|
def host_vars(monkeypatch, inventory):
|
||||||
|
host_vars = {}
|
||||||
|
|
||||||
|
def set_variable(host, key, value):
|
||||||
|
if host not in host_vars:
|
||||||
|
host_vars[host] = {}
|
||||||
|
host_vars[host][key] = value
|
||||||
|
|
||||||
|
monkeypatch.setattr(inventory.inventory, "set_variable", set_variable)
|
||||||
|
return host_vars
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="function")
|
||||||
|
def client(mocker, request):
|
||||||
|
namespaces = mocker.Mock()
|
||||||
|
namespaces.items = [
|
||||||
|
Dict(item)
|
||||||
|
for item in request.param.get(
|
||||||
|
"namespaces", [{"metadata": {"name": DEFAULT_NAMESPACE}}]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
vmis = mocker.Mock()
|
||||||
|
vmis.items = [Dict(item) for item in request.param.get("vmis", [])]
|
||||||
|
services = mocker.Mock()
|
||||||
|
services.items = [Dict(item) for item in request.param.get("services", [])]
|
||||||
|
dns = Dict(
|
||||||
|
{"spec": {"baseDomain": request.param.get("base_domain", DEFAULT_BASE_DOMAIN)}}
|
||||||
|
)
|
||||||
|
|
||||||
|
namespace_client = mocker.Mock()
|
||||||
|
namespace_client.get = mocker.Mock(return_value=namespaces)
|
||||||
|
vmi_client = mocker.Mock()
|
||||||
|
vmi_client.get = mocker.Mock(return_value=vmis)
|
||||||
|
service_client = mocker.Mock()
|
||||||
|
service_client.get = mocker.Mock(return_value=services)
|
||||||
|
dns_client = mocker.Mock()
|
||||||
|
dns_client.get = mocker.Mock(return_value=dns)
|
||||||
|
|
||||||
|
def resources_get(api_version="", kind=""):
|
||||||
|
if api_version.lower() == "v1":
|
||||||
|
if kind.lower() == "namespace":
|
||||||
|
return namespace_client
|
||||||
|
elif kind.lower() == "service":
|
||||||
|
return service_client
|
||||||
|
elif api_version.lower() == "config.openshift.io/v1" and kind.lower() == "dns":
|
||||||
|
return dns_client
|
||||||
|
elif (
|
||||||
|
"kubevirt.io/" in api_version.lower()
|
||||||
|
and kind.lower() == "virtualmachineinstance"
|
||||||
|
):
|
||||||
|
return vmi_client
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
client = mocker.Mock()
|
||||||
|
client.resources.get = resources_get
|
||||||
|
return client
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
@@ -50,3 +169,69 @@ def test_verify_file(tmp_path, inventory, file_name, expected):
|
|||||||
)
|
)
|
||||||
def test_verify_file_bad_config(inventory, file_name):
|
def test_verify_file_bad_config(inventory, file_name):
|
||||||
assert inventory.verify_file(file_name) is False
|
assert inventory.verify_file(file_name) is False
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"guest_os_info,annotations,expected",
|
||||||
|
[
|
||||||
|
({"id": "mswindows"}, {}, True),
|
||||||
|
({}, {"kubevirt.io/preference-name": "windows.2k22"}, True),
|
||||||
|
({}, {"vm.kubevirt.io/os": "windows2k22"}, True),
|
||||||
|
({}, {}, False),
|
||||||
|
({"id": "fedora"}, {}, False),
|
||||||
|
(
|
||||||
|
{"id": "fedora"},
|
||||||
|
{"kubevirt.io/cluster-preference-name": "windows.2k22"},
|
||||||
|
False,
|
||||||
|
),
|
||||||
|
({"id": "fedora"}, {"kubevirt.io/preference-name": "windows.2k22"}, False),
|
||||||
|
({"id": "fedora"}, {"vm.kubevirt.io/os": "windows2k22"}, False),
|
||||||
|
(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
"kubevirt.io/cluster-preference-name": "fedora",
|
||||||
|
"kubevirt.io/preference-name": "windows.2k22",
|
||||||
|
},
|
||||||
|
False,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
"kubevirt.io/cluster-preference-name": "fedora",
|
||||||
|
"vm.kubevirt.io/os": "windows2k22",
|
||||||
|
},
|
||||||
|
False,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
"kubevirt.io/preference-name": "fedora",
|
||||||
|
"vm.kubevirt.io/os": "windows2k22",
|
||||||
|
},
|
||||||
|
False,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_is_windows(inventory, guest_os_info, annotations, expected):
|
||||||
|
assert inventory.is_windows(guest_os_info, annotations) is expected
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"client,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),
|
||||||
|
],
|
||||||
|
indirect=["client"],
|
||||||
|
)
|
||||||
|
def test_ansible_connection_winrm(inventory, host_vars, client, vmi, expected):
|
||||||
|
inventory.get_vmis_for_namespace(client, "", DEFAULT_NAMESPACE, GetVmiOptions())
|
||||||
|
|
||||||
|
host = f"{DEFAULT_NAMESPACE}-{vmi['metadata']['name']}"
|
||||||
|
if expected:
|
||||||
|
assert host_vars[host]["ansible_connection"] == "winrm"
|
||||||
|
else:
|
||||||
|
assert "ansible_connection" not in host_vars[host]
|
||||||
|
|||||||
@@ -24,11 +24,13 @@ def set_module_args(args):
|
|||||||
|
|
||||||
class AnsibleExitJson(Exception):
|
class AnsibleExitJson(Exception):
|
||||||
"""Exception class to be raised by module.exit_json and caught by the test case"""
|
"""Exception class to be raised by module.exit_json and caught by the test case"""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class AnsibleFailJson(Exception):
|
class AnsibleFailJson(Exception):
|
||||||
"""Exception class to be raised by module.fail_json and caught by the test case"""
|
"""Exception class to be raised by module.fail_json and caught by the test case"""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
20
tests/unit/utils/merge_dicts.py
Normal file
20
tests/unit/utils/merge_dicts.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright 2024 Red Hat, Inc.
|
||||||
|
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
|
||||||
|
from __future__ import absolute_import, division, print_function
|
||||||
|
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
|
from copy import deepcopy
|
||||||
|
|
||||||
|
|
||||||
|
def merge_dicts(dict1, dict2):
|
||||||
|
merged = deepcopy(dict1)
|
||||||
|
for key, value in dict2.items():
|
||||||
|
if key in merged and isinstance(merged[key], dict) and isinstance(value, dict):
|
||||||
|
merged[key] = merge_dicts(merged[key], value)
|
||||||
|
else:
|
||||||
|
merged[key] = value
|
||||||
|
|
||||||
|
return merged
|
||||||
Reference in New Issue
Block a user