mirror of
https://github.com/kubevirt/kubevirt.core.git
synced 2026-03-26 19:03:16 +00:00
Merge pull request #114 from 0xFelix/vm-vmi-2
feat,test(inventory): Support listing stopped VMs and major rework of unit tests
This commit is contained in:
@@ -10,13 +10,13 @@ __metaclass__ = type
|
||||
DOCUMENTATION = """
|
||||
name: kubevirt
|
||||
|
||||
short_description: Inventory source for KubeVirt VirtualMachines
|
||||
short_description: Inventory source for KubeVirt VirtualMachines and VirtualMachineInstances
|
||||
|
||||
author:
|
||||
- "KubeVirt.io Project (!UNKNOWN)"
|
||||
|
||||
description:
|
||||
- Fetch running C(VirtualMachineInstances) for one or more namespaces with an optional label selector.
|
||||
- Fetch virtual machines from one or more namespaces with an optional label selector.
|
||||
- Groups by cluster name, namespace and labels.
|
||||
- Uses the M(kubernetes.core.kubectl) connection plugin to access the Kubernetes cluster.
|
||||
- Uses V(*.kubevirt.[yml|yaml]) YAML configuration file to set parameter values.
|
||||
@@ -94,14 +94,14 @@ options:
|
||||
aliases: [ verify_ssl ]
|
||||
namespaces:
|
||||
description:
|
||||
- List of namespaces. If not specified, will fetch all C(VirtualMachineInstances) for all namespaces
|
||||
- List of namespaces. If not specified, will fetch virtual machines from all namespaces
|
||||
the user is authorized to access.
|
||||
label_selector:
|
||||
description:
|
||||
- Define a label selector to select a subset of the fetched C(VirtualMachineInstances).
|
||||
- Define a label selector to select a subset of the fetched virtual machines.
|
||||
network_name:
|
||||
description:
|
||||
- In case multiple networks are attached to a C(VirtualMachineInstance), define which interface should
|
||||
- In case multiple networks are attached to a virtual machine, define which interface should
|
||||
be returned as primary IP address.
|
||||
aliases: [ interface_name ]
|
||||
kube_secondary_dns:
|
||||
@@ -111,18 +111,18 @@ options:
|
||||
default: False
|
||||
use_service:
|
||||
description:
|
||||
- Enable the use of C(Services) to establish an SSH connection to the C(VirtualMachine).
|
||||
- Enable the use of C(Services) to establish an SSH connection to a virtual machine.
|
||||
- Services are only used if no O(connections.network_name) was provided.
|
||||
type: bool
|
||||
default: True
|
||||
create_groups:
|
||||
description:
|
||||
- Enable the creation of groups from labels on C(VirtualMachines).
|
||||
- Enable the creation of groups from labels on C(VirtualMachines) and C(VirtualMachineInstances).
|
||||
type: bool
|
||||
default: False
|
||||
base_domain:
|
||||
description:
|
||||
- Override the base domain used to construct host names of C(VirtualMachines). Used in case of
|
||||
- Override the base domain used to construct host names. Used in case of
|
||||
C(kubesecondarydns) or C(Services) of type C(NodePort) if O(connections.append_base_domain) is set.
|
||||
append_base_domain:
|
||||
description:
|
||||
@@ -143,21 +143,21 @@ requirements:
|
||||
EXAMPLES = """
|
||||
# Filename must end with kubevirt.[yml|yaml]
|
||||
|
||||
- name: Authenticate with token and return all VirtualMachineInstances for all accessible namespaces
|
||||
- name: Authenticate with token and return all virtual machines from all accessible namespaces
|
||||
plugin: kubevirt.core.kubevirt
|
||||
connections:
|
||||
- host: https://192.168.64.4:8443
|
||||
api_key: xxxxxxxxxxxxxxxx
|
||||
validate_certs: false
|
||||
|
||||
- name: Use default ~/.kube/config and return VirtualMachineInstances from namespace testing connected to network bridge-network
|
||||
- name: Use default ~/.kube/config and return virtual machines from namespace testing connected to network bridge-network
|
||||
plugin: kubevirt.core.kubevirt
|
||||
connections:
|
||||
- namespaces:
|
||||
- testing
|
||||
network_name: bridge-network
|
||||
|
||||
- name: Use default ~/.kube/config and return VirtualMachineInstances from namespace testing with label app=test
|
||||
- name: Use default ~/.kube/config and return virtual machines from namespace testing with label app=test
|
||||
plugin: kubevirt.core.kubevirt
|
||||
connections:
|
||||
- namespaces:
|
||||
@@ -173,6 +173,7 @@ EXAMPLES = """
|
||||
|
||||
from dataclasses import dataclass
|
||||
from json import loads
|
||||
from re import compile as re_compile
|
||||
from typing import (
|
||||
Any,
|
||||
Dict,
|
||||
@@ -180,22 +181,29 @@ from typing import (
|
||||
Optional,
|
||||
)
|
||||
|
||||
|
||||
# Handle import errors of python kubernetes client.
|
||||
# Set HAS_K8S_MODULE_HELPER and k8s_import exception accordingly to
|
||||
# potentially print a warning to the user if the client is missing.
|
||||
try:
|
||||
from kubernetes.dynamic.exceptions import DynamicApiError
|
||||
from kubernetes.dynamic.resource import ResourceField
|
||||
|
||||
HAS_K8S_MODULE_HELPER = True
|
||||
k8s_import_exception = None
|
||||
K8S_IMPORT_EXCEPTION = None
|
||||
except ImportError as e:
|
||||
|
||||
class DynamicApiError(Exception):
|
||||
pass
|
||||
"""
|
||||
Dummy class, mainly used for ansible-test sanity.
|
||||
"""
|
||||
|
||||
class ResourceField:
|
||||
"""
|
||||
Dummy class, mainly used for ansible-test sanity.
|
||||
"""
|
||||
|
||||
HAS_K8S_MODULE_HELPER = False
|
||||
k8s_import_exception = e
|
||||
K8S_IMPORT_EXCEPTION = e
|
||||
|
||||
from ansible.plugins.inventory import BaseInventoryPlugin, Constructable, Cacheable
|
||||
|
||||
@@ -215,11 +223,13 @@ ID_MSWINDOWS = "mswindows"
|
||||
|
||||
|
||||
class KubeVirtInventoryException(Exception):
|
||||
pass
|
||||
"""
|
||||
This class is used for exceptions raised by this inventory.
|
||||
"""
|
||||
|
||||
|
||||
@dataclass
|
||||
class GetVmiOptions:
|
||||
class InventoryOptions:
|
||||
"""
|
||||
This class holds the options defined by the user.
|
||||
"""
|
||||
@@ -257,8 +267,8 @@ class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable):
|
||||
|
||||
NAME = "kubevirt.core.kubevirt"
|
||||
|
||||
connection_plugin = "kubernetes.core.kubectl"
|
||||
transport = "kubectl"
|
||||
# Used to convert camel case variable names into snake case
|
||||
snake_case_pattern = re_compile(r"(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])")
|
||||
|
||||
@staticmethod
|
||||
def get_default_host_name(host: str) -> str:
|
||||
@@ -289,21 +299,29 @@ class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable):
|
||||
return f"{exc.status} Reason: {exc.reason}"
|
||||
|
||||
@staticmethod
|
||||
def get_host_from_service(service: Dict, node_name: str) -> Optional[str]:
|
||||
def format_var_name(name: str) -> str:
|
||||
"""
|
||||
format_var_name formats a CamelCase variable name into a snake_case name
|
||||
suitable for use as a inventory variable name.
|
||||
"""
|
||||
return InventoryModule.snake_case_pattern.sub("_", name).lower()
|
||||
|
||||
@staticmethod
|
||||
def get_host_from_service(service: Dict, node_name: Optional[str]) -> Optional[str]:
|
||||
"""
|
||||
get_host_from_service extracts the hostname to be used from the
|
||||
passed in service.
|
||||
"""
|
||||
# LoadBalancer services can return a hostname or an IP address
|
||||
if service["spec"]["type"] == TYPE_LOADBALANCER:
|
||||
ingress = service["status"]["loadBalancer"].get("ingress")
|
||||
service_type = service.get("spec", {}).get("type")
|
||||
if service_type == TYPE_LOADBALANCER:
|
||||
# LoadBalancer services can return a hostname or an IP address
|
||||
ingress = service.get("status", {}).get("loadBalancer", {}).get("ingress")
|
||||
if ingress is not None and len(ingress) > 0:
|
||||
hostname = ingress[0].get("hostname")
|
||||
ip_address = ingress[0].get("ip")
|
||||
return hostname if hostname is not None else ip_address
|
||||
|
||||
# NodePort services use the node name as host
|
||||
if service["spec"]["type"] == TYPE_NODEPORT:
|
||||
elif service_type == TYPE_NODEPORT:
|
||||
# NodePort services use the node name as host
|
||||
return node_name
|
||||
|
||||
return None
|
||||
@@ -314,21 +332,32 @@ class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable):
|
||||
get_port_from_service extracts the port to be used from the
|
||||
passed in service.
|
||||
"""
|
||||
# LoadBalancer services use the port attribute
|
||||
if service["spec"]["type"] == TYPE_LOADBALANCER:
|
||||
return service["spec"]["ports"][0]["port"]
|
||||
ports = service.get("spec", {}).get("ports", [])
|
||||
if not ports:
|
||||
return None
|
||||
|
||||
# NodePort services use the nodePort attribute
|
||||
if service["spec"]["type"] == TYPE_NODEPORT:
|
||||
return service["spec"]["ports"][0]["nodePort"]
|
||||
service_type = service.get("spec", {}).get("type")
|
||||
if service_type == TYPE_LOADBALANCER:
|
||||
# LoadBalancer services use the port attribute
|
||||
return ports[0].get("port")
|
||||
if service_type == TYPE_NODEPORT:
|
||||
# NodePort services use the nodePort attribute
|
||||
return ports[0].get("nodePort")
|
||||
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def is_windows(guest_os_info: Dict, annotations: Dict) -> bool:
|
||||
if "id" in guest_os_info:
|
||||
def is_windows(guest_os_info: Optional[Dict], annotations: Optional[Dict]) -> bool:
|
||||
"""
|
||||
is_windows checkes whether a given VM is running a Windows guest
|
||||
by checking its GuestOSInfo and annotations.
|
||||
"""
|
||||
if guest_os_info and "id" in guest_os_info:
|
||||
return guest_os_info["id"] == ID_MSWINDOWS
|
||||
|
||||
if not annotations:
|
||||
return False
|
||||
|
||||
if ANNOTATION_KUBEVIRT_IO_CLUSTER_PREFERENCE_NAME in annotations:
|
||||
return annotations[
|
||||
ANNOTATION_KUBEVIRT_IO_CLUSTER_PREFERENCE_NAME
|
||||
@@ -372,12 +401,10 @@ class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable):
|
||||
gets the configured connections and runs fetch_objects on them.
|
||||
If there is a cache it is returned instead.
|
||||
"""
|
||||
connections = config_data.get("connections")
|
||||
|
||||
if not HAS_K8S_MODULE_HELPER:
|
||||
raise KubeVirtInventoryException(
|
||||
"This module requires the Kubernetes Python client. "
|
||||
+ f"Try `pip install kubernetes`. Detail: {k8s_import_exception}"
|
||||
+ f"Try `pip install kubernetes`. Detail: {K8S_IMPORT_EXCEPTION}"
|
||||
)
|
||||
|
||||
source_data = None
|
||||
@@ -388,9 +415,9 @@ class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable):
|
||||
pass
|
||||
|
||||
if not source_data:
|
||||
self.fetch_objects(connections)
|
||||
self.fetch_objects(config_data.get("connections"))
|
||||
|
||||
def fetch_objects(self, connections: Dict) -> None:
|
||||
def fetch_objects(self, connections: Optional[List[Dict]]) -> None:
|
||||
"""
|
||||
fetch_objects populates the inventory with every configured connection.
|
||||
"""
|
||||
@@ -412,7 +439,7 @@ class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable):
|
||||
else:
|
||||
namespaces = self.get_available_namespaces(client)
|
||||
|
||||
opts = GetVmiOptions(
|
||||
opts = InventoryOptions(
|
||||
connection.get("api_version"),
|
||||
connection.get("label_selector"),
|
||||
connection.get("network_name", connection.get("interface_name")),
|
||||
@@ -424,14 +451,19 @@ class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable):
|
||||
self.host_format,
|
||||
)
|
||||
for namespace in namespaces:
|
||||
self.get_vmis_for_namespace(client, name, namespace, opts)
|
||||
self.populate_inventory_from_namespace(
|
||||
client, name, namespace, opts
|
||||
)
|
||||
else:
|
||||
client = get_api_client()
|
||||
name = self.get_default_host_name(client.configuration.host)
|
||||
namespaces = self.get_available_namespaces(client)
|
||||
opts = GetVmiOptions(host_format=self.host_format)
|
||||
opts = InventoryOptions(
|
||||
host_format=self.host_format,
|
||||
base_domain=self.get_cluster_domain(client),
|
||||
)
|
||||
for namespace in namespaces:
|
||||
self.get_vmis_for_namespace(client, name, namespace, opts)
|
||||
self.populate_inventory_from_namespace(client, name, namespace, opts)
|
||||
|
||||
def get_cluster_domain(self, client: K8SClient) -> Optional[str]:
|
||||
"""
|
||||
@@ -453,208 +485,59 @@ class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable):
|
||||
return None
|
||||
return obj.get("spec", {}).get("baseDomain")
|
||||
|
||||
def get_available_namespaces(self, client: K8SClient) -> List:
|
||||
def get_resources(
|
||||
self, client: K8SClient, api_version: str, kind: str, **kwargs
|
||||
) -> List[ResourceField]:
|
||||
"""
|
||||
get_resources uses a dynamic K8SClient to fetch resources from the K8S API.
|
||||
"""
|
||||
client = client.resources.get(api_version=api_version, kind=kind)
|
||||
try:
|
||||
result = client.get(**kwargs)
|
||||
except DynamicApiError as exc:
|
||||
self.display.debug(exc)
|
||||
raise KubeVirtInventoryException(
|
||||
f"Error fetching {kind} list: {self.format_dynamic_api_exc(exc)}"
|
||||
) from exc
|
||||
|
||||
return result.items
|
||||
|
||||
def get_available_namespaces(self, client: K8SClient) -> List[str]:
|
||||
"""
|
||||
get_available_namespaces lists all namespaces accessible with the
|
||||
configured credentials and returns them.
|
||||
"""
|
||||
v1_namespace = client.resources.get(api_version="v1", kind="Namespace")
|
||||
try:
|
||||
obj = v1_namespace.get()
|
||||
except DynamicApiError as exc:
|
||||
self.display.debug(exc)
|
||||
raise KubeVirtInventoryException(
|
||||
f"Error fetching Namespace list: {self.format_dynamic_api_exc(exc)}"
|
||||
) from exc
|
||||
return [namespace.metadata.name for namespace in obj.items]
|
||||
return [
|
||||
namespace.metadata.name
|
||||
for namespace in self.get_resources(client, "v1", "Namespace")
|
||||
]
|
||||
|
||||
def get_vms_for_namespace(
|
||||
self, client: K8SClient, namespace: str, opts: InventoryOptions
|
||||
) -> List[ResourceField]:
|
||||
"""
|
||||
get_vms_for_namespace returns a list of all VirtualMachines in a namespace.
|
||||
"""
|
||||
return self.get_resources(
|
||||
client,
|
||||
opts.api_version,
|
||||
"VirtualMachine",
|
||||
namespace=namespace,
|
||||
label_selector=opts.label_selector,
|
||||
)
|
||||
|
||||
def get_vmis_for_namespace(
|
||||
self, client: K8SClient, name: str, namespace: str, opts: GetVmiOptions
|
||||
) -> None:
|
||||
self, client: K8SClient, namespace: str, opts: InventoryOptions
|
||||
) -> List[ResourceField]:
|
||||
"""
|
||||
get_vmis_for_namespace lists all VirtualMachineInstances in a namespace
|
||||
and adds groups and hosts to the inventory.
|
||||
get_vmis_for_namespace returns a list of all VirtualMachineInstances in a namespace.
|
||||
"""
|
||||
vmi_client = client.resources.get(
|
||||
api_version=opts.api_version, kind="VirtualMachineInstance"
|
||||
)
|
||||
try:
|
||||
vmi_list = vmi_client.get(
|
||||
namespace=namespace, label_selector=opts.label_selector
|
||||
)
|
||||
except DynamicApiError as exc:
|
||||
self.display.debug(exc)
|
||||
raise KubeVirtInventoryException(
|
||||
f"Error fetching VirtualMachineInstance list: {self.format_dynamic_api_exc(exc)}"
|
||||
) from exc
|
||||
|
||||
if not vmi_list.items:
|
||||
# Return early if no VMIs were found to avoid adding empty groups.
|
||||
return
|
||||
|
||||
services = self.get_ssh_services_for_namespace(client, namespace)
|
||||
|
||||
name = self._sanitize_group_name(name)
|
||||
namespace_group = self._sanitize_group_name(f"namespace_{namespace}")
|
||||
|
||||
self.inventory.add_group(name)
|
||||
self.inventory.add_group(namespace_group)
|
||||
self.inventory.add_child(name, namespace_group)
|
||||
|
||||
for vmi in vmi_list.items:
|
||||
if not (vmi.status and vmi.status.interfaces):
|
||||
continue
|
||||
|
||||
# Find interface by its name:
|
||||
if opts.network_name is None:
|
||||
interface = vmi.status.interfaces[0]
|
||||
else:
|
||||
interface = next(
|
||||
(i for i in vmi.status.interfaces if i.name == opts.network_name),
|
||||
None,
|
||||
)
|
||||
|
||||
# If interface is not found or IP address is not reported skip this VM:
|
||||
if interface is None or interface.ipAddress is None:
|
||||
continue
|
||||
|
||||
vmi_name = opts.host_format.format(
|
||||
namespace=vmi.metadata.namespace,
|
||||
name=vmi.metadata.name,
|
||||
uid=vmi.metadata.uid,
|
||||
)
|
||||
vmi_annotations = (
|
||||
{}
|
||||
if not vmi.metadata.annotations
|
||||
else vmi.metadata.annotations.to_dict()
|
||||
)
|
||||
vmi_labels = (
|
||||
{} if not vmi.metadata.labels else vmi.metadata.labels.to_dict()
|
||||
)
|
||||
|
||||
# Add vmi to the namespace group
|
||||
self.inventory.add_host(vmi_name)
|
||||
self.inventory.add_child(namespace_group, vmi_name)
|
||||
|
||||
# Create label groups and add vmi to it if enabled
|
||||
if vmi.metadata.labels and opts.create_groups:
|
||||
# Create a group for each label_value
|
||||
vmi_groups = []
|
||||
for key, value in vmi.metadata.labels.items():
|
||||
group_name = self._sanitize_group_name(f"label_{key}_{value}")
|
||||
if group_name not in vmi_groups:
|
||||
vmi_groups.append(group_name)
|
||||
# Add vmi to each label_value group
|
||||
for group in vmi_groups:
|
||||
self.inventory.add_group(group)
|
||||
self.inventory.add_child(group, vmi_name)
|
||||
|
||||
# Add hostvars from metadata
|
||||
self.inventory.set_variable(vmi_name, "object_type", "vmi")
|
||||
self.inventory.set_variable(vmi_name, "labels", vmi_labels)
|
||||
self.inventory.set_variable(vmi_name, "annotations", vmi_annotations)
|
||||
self.inventory.set_variable(
|
||||
vmi_name, "cluster_name", vmi.metadata.clusterName
|
||||
)
|
||||
self.inventory.set_variable(
|
||||
vmi_name, "resource_version", vmi.metadata.resourceVersion
|
||||
)
|
||||
self.inventory.set_variable(vmi_name, "uid", vmi.metadata.uid)
|
||||
|
||||
# Add hostvars from status
|
||||
vmi_active_pods = (
|
||||
{} if not vmi.status.activePods else vmi.status.activePods.to_dict()
|
||||
)
|
||||
self.inventory.set_variable(vmi_name, "vmi_active_pods", vmi_active_pods)
|
||||
vmi_conditions = (
|
||||
[]
|
||||
if not vmi.status.conditions
|
||||
else [c.to_dict() for c in vmi.status.conditions]
|
||||
)
|
||||
self.inventory.set_variable(vmi_name, "vmi_conditions", vmi_conditions)
|
||||
vmi_guest_os_info = (
|
||||
{} if not vmi.status.guestOSInfo else vmi.status.guestOSInfo.to_dict()
|
||||
)
|
||||
self.inventory.set_variable(
|
||||
vmi_name, "vmi_guest_os_info", vmi_guest_os_info
|
||||
)
|
||||
vmi_interfaces = (
|
||||
[]
|
||||
if not vmi.status.interfaces
|
||||
else [i.to_dict() for i in vmi.status.interfaces]
|
||||
)
|
||||
self.inventory.set_variable(vmi_name, "vmi_interfaces", vmi_interfaces)
|
||||
self.inventory.set_variable(
|
||||
vmi_name,
|
||||
"vmi_launcher_container_image_version",
|
||||
vmi.status.launcherContainerImageVersion,
|
||||
)
|
||||
self.inventory.set_variable(
|
||||
vmi_name, "vmi_migration_method", vmi.status.migrationMethod
|
||||
)
|
||||
self.inventory.set_variable(
|
||||
vmi_name, "vmi_migration_transport", vmi.status.migrationTransport
|
||||
)
|
||||
self.inventory.set_variable(vmi_name, "vmi_node_name", vmi.status.nodeName)
|
||||
self.inventory.set_variable(vmi_name, "vmi_phase", vmi.status.phase)
|
||||
vmi_phase_transition_timestamps = (
|
||||
[]
|
||||
if not vmi.status.phaseTransitionTimestamps
|
||||
else [p.to_dict() for p in vmi.status.phaseTransitionTimestamps]
|
||||
)
|
||||
self.inventory.set_variable(
|
||||
vmi_name,
|
||||
"vmi_phase_transition_timestamps",
|
||||
vmi_phase_transition_timestamps,
|
||||
)
|
||||
self.inventory.set_variable(vmi_name, "vmi_qos_class", vmi.status.qosClass)
|
||||
self.inventory.set_variable(
|
||||
vmi_name,
|
||||
"vmi_virtual_machine_revision_name",
|
||||
vmi.status.virtualMachineRevisionName,
|
||||
)
|
||||
vmi_volume_status = (
|
||||
[]
|
||||
if not vmi.status.volumeStatus
|
||||
else [v.to_dict() for v in vmi.status.volumeStatus]
|
||||
)
|
||||
self.inventory.set_variable(
|
||||
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)
|
||||
|
||||
def set_composable_vars(self, vmi_name):
|
||||
"""
|
||||
set_composable_vars sets vars per
|
||||
https://docs.ansible.com/ansible/latest/dev_guide/developing_inventory.html
|
||||
"""
|
||||
host_vars = self.inventory.get_host(vmi_name).get_vars()
|
||||
strict = self.get_option("strict")
|
||||
self._set_composite_vars(
|
||||
self.get_option("compose"), host_vars, vmi_name, strict=True
|
||||
)
|
||||
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
|
||||
return self.get_resources(
|
||||
client,
|
||||
opts.api_version,
|
||||
"VirtualMachineInstance",
|
||||
namespace=namespace,
|
||||
label_selector=opts.label_selector,
|
||||
)
|
||||
|
||||
def get_ssh_services_for_namespace(self, client: K8SClient, namespace: str) -> Dict:
|
||||
@@ -662,21 +545,20 @@ class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable):
|
||||
get_ssh_services_for_namespace retrieves all services of a namespace exposing port 22/ssh.
|
||||
The services are mapped to the name of the corresponding domain.
|
||||
"""
|
||||
v1_service = client.resources.get(api_version="v1", kind="Service")
|
||||
try:
|
||||
service_list = v1_service.get(
|
||||
namespace=namespace,
|
||||
)
|
||||
except DynamicApiError as exc:
|
||||
self.display.debug(exc)
|
||||
raise KubeVirtInventoryException(
|
||||
f"Error fetching Service list: {self.format_dynamic_api_exc(exc)}"
|
||||
) from exc
|
||||
service_list = self.get_resources(
|
||||
client,
|
||||
"v1",
|
||||
"Service",
|
||||
namespace=namespace,
|
||||
)
|
||||
|
||||
services = {}
|
||||
for service in service_list.items:
|
||||
for service in service_list:
|
||||
# Continue if service is not of type LoadBalancer or NodePort
|
||||
if service.get("spec", {}).get("type") not in (
|
||||
if service.get("spec") is None:
|
||||
continue
|
||||
|
||||
if service["spec"].get("type") not in (
|
||||
TYPE_LOADBALANCER,
|
||||
TYPE_NODEPORT,
|
||||
):
|
||||
@@ -695,13 +577,169 @@ class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable):
|
||||
|
||||
return services
|
||||
|
||||
def populate_inventory_from_namespace(
|
||||
self, client: K8SClient, name: str, namespace: str, 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)
|
||||
}
|
||||
vmis = {
|
||||
vmi.metadata.name: vmi
|
||||
for vmi in self.get_vmis_for_namespace(client, namespace, opts)
|
||||
}
|
||||
|
||||
if not vms and not vmis:
|
||||
# Return early if no VMs and VMIs were found to avoid adding empty groups.
|
||||
return
|
||||
|
||||
services = self.get_ssh_services_for_namespace(client, namespace)
|
||||
|
||||
name = self._sanitize_group_name(name)
|
||||
namespace_group = self._sanitize_group_name(f"namespace_{namespace}")
|
||||
|
||||
self.inventory.add_group(name)
|
||||
self.inventory.add_group(namespace_group)
|
||||
self.inventory.add_child(name, namespace_group)
|
||||
|
||||
# Add found VMs and optionally enhance with VMI data
|
||||
for name, vm in vms.items():
|
||||
hostname = self.add_host(vm, opts.host_format, namespace_group)
|
||||
self.set_vars_from_vm(hostname, vm, opts)
|
||||
if name in vmis:
|
||||
self.set_vars_from_vmi(hostname, vmis[name], services, opts)
|
||||
self.set_composable_vars(hostname)
|
||||
|
||||
# Add remaining VMIs without VM
|
||||
for name, vmi in vmis.items():
|
||||
if name in vms:
|
||||
continue
|
||||
hostname = self.add_host(vmi, opts.host_format, namespace_group)
|
||||
self.set_vars_from_vmi(hostname, vmi, services, opts)
|
||||
self.set_composable_vars(hostname)
|
||||
|
||||
def add_host(
|
||||
self, obj: ResourceField, host_format: str, namespace_group: str
|
||||
) -> str:
|
||||
"""
|
||||
add_hosts adds a host to the inventory.
|
||||
"""
|
||||
hostname = host_format.format(
|
||||
namespace=obj.metadata.namespace,
|
||||
name=obj.metadata.name,
|
||||
uid=obj.metadata.uid,
|
||||
)
|
||||
self.inventory.add_host(hostname)
|
||||
self.inventory.add_child(namespace_group, hostname)
|
||||
|
||||
return hostname
|
||||
|
||||
def set_vars_from_vm(
|
||||
self, hostname: str, vm: ResourceField, opts: InventoryOptions
|
||||
) -> None:
|
||||
"""
|
||||
set_vars_from_vm sets inventory variables from a VM prefixed with vm_.
|
||||
"""
|
||||
self.set_common_vars(hostname, "vm", vm, opts)
|
||||
|
||||
def set_vars_from_vmi(
|
||||
self, hostname: str, vmi: ResourceField, services: Dict, opts: InventoryOptions
|
||||
) -> None:
|
||||
"""
|
||||
set_vars_from_vmi sets inventory variables from a VMI prefixed with vmi_ and
|
||||
looks up the interface to set ansible_host and ansible_port.
|
||||
"""
|
||||
self.set_common_vars(hostname, "vmi", vmi, opts)
|
||||
|
||||
if opts.network_name is None:
|
||||
# Use first interface
|
||||
interface = vmi.status.interfaces[0] if vmi.status.interfaces else None
|
||||
else:
|
||||
# Find interface by its name
|
||||
interface = next(
|
||||
(i for i in vmi.status.interfaces if i.name == opts.network_name),
|
||||
None,
|
||||
)
|
||||
|
||||
# If interface is not found or IP address is not reported skip this VMI
|
||||
if interface is None or interface.ipAddress is None:
|
||||
return
|
||||
|
||||
# Set up the connection
|
||||
service = None
|
||||
if self.is_windows(
|
||||
{} if not vmi.status.guestOSInfo else vmi.status.guestOSInfo.to_dict(),
|
||||
{} if not vmi.metadata.annotations else vmi.metadata.annotations.to_dict(),
|
||||
):
|
||||
self.inventory.set_variable(hostname, "ansible_connection", "winrm")
|
||||
else:
|
||||
service = services.get(vmi.metadata.labels.get(LABEL_KUBEVIRT_IO_DOMAIN))
|
||||
self.set_ansible_host_and_port(
|
||||
vmi,
|
||||
hostname,
|
||||
interface.ipAddress,
|
||||
service,
|
||||
opts,
|
||||
)
|
||||
|
||||
def set_common_vars(
|
||||
self, hostname: str, prefix: str, obj: ResourceField, opts: InventoryOptions
|
||||
):
|
||||
"""
|
||||
set_common_vars sets common inventory variables from VMs or VMIs.
|
||||
"""
|
||||
# Add hostvars from metadata
|
||||
if metadata := obj.metadata:
|
||||
if metadata.annotations:
|
||||
self.inventory.set_variable(
|
||||
hostname, f"{prefix}_annotations", metadata.annotations.to_dict()
|
||||
)
|
||||
if metadata.labels:
|
||||
self.inventory.set_variable(
|
||||
hostname, f"{prefix}_labels", metadata.labels.to_dict()
|
||||
)
|
||||
# Create label groups and add vm to it if enabled
|
||||
if opts.create_groups:
|
||||
self.set_groups_from_labels(hostname, metadata.labels)
|
||||
if metadata.resourceVersion:
|
||||
self.inventory.set_variable(
|
||||
hostname, f"{prefix}_resource_version", metadata.resourceVersion
|
||||
)
|
||||
if metadata.uid:
|
||||
self.inventory.set_variable(hostname, f"{prefix}_uid", metadata.uid)
|
||||
|
||||
# Add hostvars from status
|
||||
if obj.status:
|
||||
for key, value in obj.status.to_dict().items():
|
||||
name = self.format_var_name(key)
|
||||
self.inventory.set_variable(hostname, f"{prefix}_{name}", value)
|
||||
|
||||
def set_groups_from_labels(self, hostname: str, labels: ResourceField) -> None:
|
||||
"""
|
||||
set_groups_from_labels adds groups for each label of a VM or VMI and
|
||||
adds the host to each group.
|
||||
"""
|
||||
groups = []
|
||||
for key, value in labels.to_dict().items():
|
||||
group_name = self._sanitize_group_name(f"label_{key}_{value}")
|
||||
if group_name not in groups:
|
||||
groups.append(group_name)
|
||||
# Add host to each label_value group
|
||||
for group in groups:
|
||||
self.inventory.add_group(group)
|
||||
self.inventory.add_child(group, hostname)
|
||||
|
||||
def set_ansible_host_and_port(
|
||||
self,
|
||||
vmi: Dict,
|
||||
vmi_name: str,
|
||||
vmi: ResourceField,
|
||||
hostname: str,
|
||||
ip_address: str,
|
||||
service: Optional[Dict],
|
||||
opts: GetVmiOptions,
|
||||
opts: InventoryOptions,
|
||||
) -> None:
|
||||
"""
|
||||
set_ansible_host_and_port sets the ansible_host and possibly the ansible_port var.
|
||||
@@ -709,22 +747,20 @@ class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable):
|
||||
"""
|
||||
ansible_host = None
|
||||
ansible_port = None
|
||||
if opts.kube_secondary_dns and opts.network_name is not None:
|
||||
if opts.kube_secondary_dns and opts.network_name:
|
||||
# Set ansible_host to the kubesecondarydns derived host name if enabled
|
||||
# See https://github.com/kubevirt/kubesecondarydns#parameters
|
||||
ansible_host = (
|
||||
f"{opts.network_name}.{vmi.metadata.name}.{vmi.metadata.namespace}.vm"
|
||||
)
|
||||
if opts.base_domain is not None:
|
||||
if opts.base_domain:
|
||||
ansible_host += f".{opts.base_domain}"
|
||||
elif opts.use_service and service is not None and opts.network_name is None:
|
||||
elif opts.use_service and service and not opts.network_name:
|
||||
# Set ansible_host and ansible_port to the host and port from the LoadBalancer
|
||||
# or NodePort service exposing SSH
|
||||
node_name = (
|
||||
f"{vmi.status.nodeName}.{opts.base_domain}"
|
||||
if opts.append_base_domain
|
||||
else vmi.status.nodeName
|
||||
)
|
||||
node_name = vmi.status.nodeName
|
||||
if node_name and opts.append_base_domain and opts.base_domain:
|
||||
node_name += f".{opts.base_domain}"
|
||||
host = self.get_host_from_service(service, node_name)
|
||||
port = self.get_port_from_service(service)
|
||||
if host is not None and port is not None:
|
||||
@@ -735,5 +771,22 @@ class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable):
|
||||
if ansible_host is None:
|
||||
ansible_host = ip_address
|
||||
|
||||
self.inventory.set_variable(vmi_name, "ansible_host", ansible_host)
|
||||
self.inventory.set_variable(vmi_name, "ansible_port", ansible_port)
|
||||
self.inventory.set_variable(hostname, "ansible_host", ansible_host)
|
||||
self.inventory.set_variable(hostname, "ansible_port", ansible_port)
|
||||
|
||||
def set_composable_vars(self, hostname: str) -> None:
|
||||
"""
|
||||
set_composable_vars sets vars per
|
||||
https://docs.ansible.com/ansible/latest/dev_guide/developing_inventory.html
|
||||
"""
|
||||
hostvars = self.inventory.get_host(hostname).get_vars()
|
||||
strict = self.get_option("strict")
|
||||
self._set_composite_vars(
|
||||
self.get_option("compose"), hostvars, hostname, strict=True
|
||||
)
|
||||
self._add_host_to_composed_groups(
|
||||
self.get_option("groups"), hostvars, hostname, strict=strict
|
||||
)
|
||||
self._add_host_to_keyed_groups(
|
||||
self.get_option("keyed_groups"), hostvars, hostname, strict=strict
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user