diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b1a193e..74c95ad 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -189,6 +189,7 @@ jobs: ansible_test_targets: >- [ "kubevirt_vm", + "kubevirt_vm_info", "inventory_kubevirt" ] name: "integration" diff --git a/changelogs/fragments/3_add_kubevirt_vm_info.yml b/changelogs/fragments/3_add_kubevirt_vm_info.yml new file mode 100644 index 0000000..cbd3a22 --- /dev/null +++ b/changelogs/fragments/3_add_kubevirt_vm_info.yml @@ -0,0 +1,2 @@ +major_changes: + - "Add kubevirt_vm_info module to describe existing VirtualMachines" diff --git a/examples/play-info-list.yml b/examples/play-info-list.yml new file mode 100644 index 0000000..0655a33 --- /dev/null +++ b/examples/play-info-list.yml @@ -0,0 +1,9 @@ +- name: Playbook describing a virtual machine + hosts: localhost + tasks: + - name: Describe VM + kubevirt.core.kubevirt_vm_info: + register: result + - name: Print return information from the previous task + ansible.builtin.debug: + var: result diff --git a/examples/play-info.yml b/examples/play-info.yml new file mode 100644 index 0000000..57628fd --- /dev/null +++ b/examples/play-info.yml @@ -0,0 +1,11 @@ +- name: Playbook describing a virtual machine + hosts: localhost + tasks: + - name: Describe VM + kubevirt.core.kubevirt_vm_info: + name: testvm + namespace: default + register: result + - name: Print return information from the previous task + ansible.builtin.debug: + var: result diff --git a/hack/e2e-setup.sh b/hack/e2e-setup.sh index 9e62edc..3df0f1c 100755 --- a/hack/e2e-setup.sh +++ b/hack/e2e-setup.sh @@ -26,14 +26,14 @@ set_default_params() { KIND_VERSION=${KIND_VERSION:-v0.20.0} KUBECTL=${KUBECTL:-$BIN_DIR/kubectl} - KUBECTL_VERSION=${KUBECTL_VERSION:-v1.27.3} + KUBECTL_VERSION=${KUBECTL_VERSION:-v1.28.1} KUBEVIRT_VERSION=${KUBEVIRT_VERSION:-v1.0.0} - KUBEVIRT_CDI_VERSION=${KUBEVIRT_CDI_VERSION:-v1.56.0} - KUBEVIRT_COMMON_INSTANCETYPES_VERSION=${KUBEVIRT_COMMON_INSTANCETYPES_VERSION:-v0.3.0} + KUBEVIRT_CDI_VERSION=${KUBEVIRT_CDI_VERSION:-v1.57.0} + KUBEVIRT_COMMON_INSTANCETYPES_VERSION=${KUBEVIRT_COMMON_INSTANCETYPES_VERSION:-v0.3.2} KUBEVIRT_USE_EMULATION=${KUBEVIRT_USE_EMULATION:-"false"} - CNAO_VERSION=${CNAO_VERSION:-v0.87.0} + CNAO_VERSION=${CNAO_VERSION:-v0.89.0} CLUSTER_NAME=${CLUSTER_NAME:-kind} SECONDARY_NETWORK_NAME=${NETWORK_NAME:-kindexgw} diff --git a/plugins/modules/kubevirt_vm_info.py b/plugins/modules/kubevirt_vm_info.py new file mode 100644 index 0000000..3495983 --- /dev/null +++ b/plugins/modules/kubevirt_vm_info.py @@ -0,0 +1,222 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2023 Red Hat, Inc. +# Based on the kubernetes.core.k8s_info module +# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +DOCUMENTATION = """ +module: kubevirt_vm_info + +short_description: Describe VirtualMachines on Kubernetes + +author: +- "KubeVirt.io Project (!UNKNOWN)" + +description: + - Use the Kubernetes Python client to perform read operations on KubeVirt VirtualMachines. + - Pass options to find VirtualMachines as module arguments. + - Authenticate using either a config file, certificates, password or token. + - Supports check mode. + +options: + api_version: + description: + - Use this to set the API version of KubeVirt. + type: str + default: kubevirt.io/v1 + name: + description: + - Specify the name of the VirtualMachine. + type: str + namespace: + description: + - Specify the namespace of VirtualMachines. + type: str + label_selectors: + description: List of label selectors to use to filter results + type: list + elements: str + default: [] + field_selectors: + description: List of field selectors to use to filter results + type: list + elements: str + default: [] + wait: + description: + - Whether to wait for the VirtualMachine to end up in the ready state. + type: bool + default: no + wait_sleep: + description: + - Number of seconds to sleep between checks. + - Ignored if C(wait) is not set. + default: 5 + type: int + wait_timeout: + description: + - How long in seconds to wait for the resource to end up in the desired state. + - Ignored if C(wait) is not set. + default: 120 + type: int + +extends_documentation_fragment: + - kubernetes.core.k8s_auth_options + +requirements: + - "python >= 3.6" + - "kubernetes >= 12.0.0" +""" + +EXAMPLES = """ +- name: Get an existing VirtualMachine + kubevirt.core.kubevirt_vm_info: + name: testvm + namespace: default + register: default_testvm + +- name: Get a list of all VirtualMachines + kubevirt.core.kubevirt_vm_info: + namespace: default + register: vm_list + +- name: Get a list of all VirtualMachines from any namespace + kubevirt.core.kubevirt_vm_info: + register: vm_list + +- name: Search for all VirtualMachines labelled app=test + kubevirt.core.kubevirt_vm_info: + label_selectors: + - app=test + +- name: Wait until the VirtualMachine is Ready + kubevirt.core.kubevirt_vm_info: + name: testvm + namespace: default + wait: yes +""" + +RETURN = """ +api_found: + description: + - Whether the specified api_version and VirtualMachine kind were successfully mapped to an existing API on the targeted cluster. + returned: always + type: bool +resources: + description: + - The VirtualMachine(s) that exists + returned: success + type: complex + contains: + api_version: + description: The versioned schema of this representation of an object. + returned: success + type: str + kind: + description: Represents the REST resource this object represents. + returned: success + type: str + metadata: + description: Standard object metadata. Includes name, namespace, annotations, labels, etc. + returned: success + type: dict + spec: + description: Specific attributes of the VirtualMachine. Can vary based on the I(api_version). + returned: success + type: dict + status: + description: Current status details for the VirtualMachine. + returned: success + type: dict +""" + +from copy import deepcopy + +from ansible_collections.kubernetes.core.plugins.module_utils.ansiblemodule import ( + AnsibleModule, +) +from ansible_collections.kubernetes.core.plugins.module_utils.args_common import ( + AUTH_ARG_SPEC, +) +from ansible_collections.kubernetes.core.plugins.module_utils.k8s.core import ( + AnsibleK8SModule, +) +from ansible_collections.kubernetes.core.plugins.module_utils.k8s.client import ( + get_api_client, +) +from ansible_collections.kubernetes.core.plugins.module_utils.k8s.exceptions import ( + CoreException, +) +from ansible_collections.kubernetes.core.plugins.module_utils.k8s.service import ( + K8sService, +) + + +def execute_module(module, svc): + """ + execute_module defines the kind and wait_condition and runs the lookup + of resources. + """ + # Set kind to query for VirtualMachines + KIND = "VirtualMachine" + + # Set wait_condition to allow waiting for the ready state of the VirtualMachine + WAIT_CONDITION = {"type": "Ready", "status": True} + + facts = svc.find( + kind=KIND, + api_version=module.params["api_version"], + name=module.params["name"], + namespace=module.params["namespace"], + label_selectors=module.params["label_selectors"], + field_selectors=module.params["field_selectors"], + wait=module.params["wait"], + wait_sleep=module.params["wait_sleep"], + wait_timeout=module.params["wait_timeout"], + condition=WAIT_CONDITION, + ) + + module.exit_json(changed=False, **facts) + + +def arg_spec(): + """ + arg_spec defines the argument spec of this module. + """ + spec = { + "api_version": {"default": "kubevirt.io/v1"}, + "name": {}, + "namespace": {}, + "label_selectors": {"type": "list", "elements": "str", "default": []}, + "field_selectors": {"type": "list", "elements": "str", "default": []}, + "wait": {"type": "bool", "default": False}, + "wait_sleep": {"type": "int", "default": 5}, + "wait_timeout": {"type": "int", "default": 120}, + } + spec.update(deepcopy(AUTH_ARG_SPEC)) + + return spec + + +def main(): + """ + main instantiates the AnsibleK8SModule and runs the module. + """ + module = AnsibleK8SModule( + module_class=AnsibleModule, argument_spec=arg_spec(), supports_check_mode=True + ) + + try: + client = get_api_client(module) + svc = K8sService(client, module) + execute_module(module, svc) + except CoreException as exc: + module.fail_from_exception(exc) + + +if __name__ == "__main__": + main() diff --git a/tests/integration/targets/kubevirt_vm_info/playbook.yml b/tests/integration/targets/kubevirt_vm_info/playbook.yml new file mode 100644 index 0000000..823196a --- /dev/null +++ b/tests/integration/targets/kubevirt_vm_info/playbook.yml @@ -0,0 +1,68 @@ +--- +- name: Create VM + connection: local + gather_facts: false + hosts: localhost + tasks: + - name: Create a VirtualMachine + kubevirt.core.kubevirt_vm: + name: testvm + namespace: default + instancetype: + name: u1.small + preference: + name: centos.9.stream + spec: + domain: + devices: {} + volumes: + - containerDisk: + image: quay.io/containerdisks/centos-stream:9 + name: containerdisk + state: present + wait: true + wait_timeout: 600 + +- name: Describe created VM + connection: local + gather_facts: false + hosts: localhost + tasks: + - name: Describe a VirtualMachine + kubevirt.core.kubevirt_vm_info: + name: testvm + namespace: default + register: describe + - name: Assert module reported no changes + ansible.builtin.assert: + that: + - not describe.changed + - describe.resources | length == 1 + +- name: Delete VM + connection: local + gather_facts: false + hosts: localhost + tasks: + - name: Delete a VirtualMachine + kubevirt.core.kubevirt_vm: + name: testvm + namespace: default + state: absent + wait: true + +- name: Verify VM deletion + connection: local + gather_facts: false + hosts: localhost + tasks: + - name: Delete a VirtualMachine + kubevirt.core.kubevirt_vm: + name: testvm + namespace: default + state: absent + register: delete + - name: Assert module reported no changes + ansible.builtin.assert: + that: + - not delete.changed diff --git a/tests/integration/targets/kubevirt_vm_info/runme.sh b/tests/integration/targets/kubevirt_vm_info/runme.sh new file mode 100755 index 0000000..820e399 --- /dev/null +++ b/tests/integration/targets/kubevirt_vm_info/runme.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +set -eux +set -o pipefail + +{ +export ANSIBLE_CALLBACKS_ENABLED=profile_tasks +ansible-playbook playbook.yml "$@" +} || { + exit 1 +} diff --git a/tests/unit/modules/test_module_kubevirt_vm_info.py b/tests/unit/modules/test_module_kubevirt_vm_info.py new file mode 100644 index 0000000..67dc279 --- /dev/null +++ b/tests/unit/modules/test_module_kubevirt_vm_info.py @@ -0,0 +1,93 @@ +# -*- coding: utf-8 -*- +# Copyright: (c) 2021, Ansible Project +# 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 unittest import TestCase +from unittest.mock import patch + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.kubernetes.core.plugins.module_utils.k8s.service import ( + K8sService, +) +from ansible_collections.kubevirt.core.plugins.modules import ( + kubevirt_vm_info, +) +from ansible_collections.kubevirt.core.tests.unit.utils.ansible_module_mock import ( + AnsibleExitJson, + exit_json, + fail_json, + set_module_args, + get_api_client, +) + +FIXTURE1 = { + "kind": "VirtualMachine", + "api_version": "kubevirt.io/v1", + "name": None, + "namespace": None, + "label_selectors": [], + "field_selectors": [], + "wait": False, + "wait_sleep": 5, + "wait_timeout": 120, + "condition": {"type": "Ready", "status": True}, +} + +FIXTURE2 = { + "kind": "VirtualMachine", + "api_version": "kubevirt.io/v1", + "name": "testvm", + "namespace": "default", + "label_selectors": [], + "field_selectors": [], + "wait": False, + "wait_sleep": 5, + "wait_timeout": 120, + "condition": {"type": "Ready", "status": True}, +} + + +class TestDescribeVM(TestCase): + def setUp(self): + self.mock_module_helper = patch.multiple( + AnsibleModule, exit_json=exit_json, fail_json=fail_json + ) + self.mock_module_helper.start() + + self.mock_main = patch.multiple(kubevirt_vm_info, get_api_client=get_api_client) + self.mock_main.start() + + # Stop the patch after test execution + # like tearDown but executed also when the setup failed + self.addCleanup(self.mock_module_helper.stop) + self.addCleanup(self.mock_main.stop) + + def run_module(self, fixture): + with patch.object(K8sService, "find") as mock_find_command: + mock_find_command.return_value = { + "api_found": True, + "failed": False, + "resources": [], + } # successful execution + with self.assertRaises(AnsibleExitJson): + kubevirt_vm_info.main() + mock_find_command.assert_called_once_with( + **fixture, + ) + + def test_describe_without_args(self): + set_module_args({}) + self.run_module(FIXTURE1) + + def test_describe_with_args(self): + set_module_args( + { + "name": "testvm", + "namespace": "default", + } + ) + self.run_module(FIXTURE2)