From 797a5218fb7274407a619437fe0e478eede34912 Mon Sep 17 00:00:00 2001 From: Ondra Machacek Date: Wed, 13 Feb 2019 11:17:01 +0100 Subject: [PATCH] kubevirt: Add new kubevirt_vm module (#50768) This module is managing virtual machines using KubeVirt. Signed-off-by: Ondra Machacek --- .github/BOTMETA.yml | 1 + lib/ansible/module_utils/kubevirt.py | 193 ++++++++++ .../modules/cloud/kubevirt/__init__.py | 0 .../modules/cloud/kubevirt/kubevirt_vm.py | 330 ++++++++++++++++++ .../doc_fragments/kubevirt_common_options.py | 70 ++++ .../doc_fragments/kubevirt_vm_options.py | 42 +++ test/runner/requirements/units.txt | 3 + test/units/modules/cloud/kubevirt/__init__.py | 0 .../cloud/kubevirt/test_kubevirt_vm.py | 153 ++++++++ 9 files changed, 792 insertions(+) create mode 100644 lib/ansible/module_utils/kubevirt.py create mode 100644 lib/ansible/modules/cloud/kubevirt/__init__.py create mode 100644 lib/ansible/modules/cloud/kubevirt/kubevirt_vm.py create mode 100644 lib/ansible/plugins/doc_fragments/kubevirt_common_options.py create mode 100644 lib/ansible/plugins/doc_fragments/kubevirt_vm_options.py create mode 100644 test/units/modules/cloud/kubevirt/__init__.py create mode 100644 test/units/modules/cloud/kubevirt/test_kubevirt_vm.py diff --git a/.github/BOTMETA.yml b/.github/BOTMETA.yml index 639f31eab2c..c88cfb0c98b 100644 --- a/.github/BOTMETA.yml +++ b/.github/BOTMETA.yml @@ -128,6 +128,7 @@ files: maintainers: $team_google ignored: supertom $modules/cloud/google/gc_storage.py: supertom + $modules/cloud/kubevirt/: machacekondra mmazur pkliczewski $modules/cloud/linode/: $team_linode $modules/cloud/lxd/: hnakamur $modules/cloud/memset/: glitchcrab diff --git a/lib/ansible/module_utils/kubevirt.py b/lib/ansible/module_utils/kubevirt.py new file mode 100644 index 00000000000..9e260b2d5d1 --- /dev/null +++ b/lib/ansible/module_utils/kubevirt.py @@ -0,0 +1,193 @@ +# -*- coding: utf-8 -*- +# + +# Copyright (c) 2018, KubeVirt Team <@kubevirt> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from collections import defaultdict + +from ansible.module_utils.k8s.common import list_dict_str +from ansible.module_utils.k8s.raw import KubernetesRawModule + +try: + from openshift import watch + from openshift.helper.exceptions import KubernetesException +except ImportError: + # Handled in k8s common: + pass + + +API_VERSION = 'kubevirt.io/v1alpha3' + + +VM_COMMON_ARG_SPEC = { + 'name': {}, + 'force': { + 'type': 'bool', + 'default': False, + }, + 'resource_definition': { + 'type': list_dict_str, + 'aliases': ['definition', 'inline'] + }, + 'src': { + 'type': 'path', + }, + 'namespace': {}, + 'api_version': {'type': 'str', 'default': API_VERSION, 'aliases': ['api', 'version']}, + 'merge_type': {'type': 'list', 'choices': ['json', 'merge', 'strategic-merge']}, + 'wait': {'type': 'bool', 'default': True}, + 'wait_timeout': {'type': 'int', 'default': 120}, + 'memory': {'type': 'str'}, + 'disks': {'type': 'list'}, + 'labels': {'type': 'dict'}, + 'interfaces': {'type': 'list'}, + 'machine_type': {'type': 'str'}, + 'cloud_init_nocloud': {'type': 'dict'}, +} + + +def virtdict(): + """ + This function create dictionary, with defaults to dictionary. + """ + return defaultdict(virtdict) + + +class KubeVirtRawModule(KubernetesRawModule): + def __init__(self, *args, **kwargs): + self.api_version = API_VERSION + super(KubeVirtRawModule, self).__init__(*args, **kwargs) + + @staticmethod + def merge_dicts(x, y): + """ + This function merge two dictionaries, where the first dict has + higher priority in merging two same keys. + """ + for k in set(x.keys()).union(y.keys()): + if k in x and k in y: + if isinstance(x[k], dict) and isinstance(y[k], dict): + yield (k, dict(KubeVirtRawModule.merge_dicts(x[k], y[k]))) + else: + yield (k, y[k]) + elif k in x: + yield (k, x[k]) + else: + yield (k, y[k]) + + def _create_stream(self, resource, namespace, wait_timeout): + """ Create a stream of events for the object """ + w = None + stream = None + try: + w = watch.Watch() + w._api_client = self.client.client + stream = w.stream(resource.get, serialize=False, namespace=namespace, timeout_seconds=wait_timeout) + except KubernetesException as exc: + self.fail_json(msg='Failed to initialize watch: {0}'.format(exc.message)) + return w, stream + + def get_resource(self, resource): + try: + existing = resource.get(name=self.name, namespace=self.namespace) + except Exception: + existing = None + + return existing + + def _define_cloud_init(self, cloud_init_nocloud, template_spec): + """ + Takes the user's cloud_init_nocloud parameter and fill it in kubevirt + API strucuture. The name of the volume is hardcoded to ansiblecloudinitvolume + and the name for disk is hardcoded to ansiblecloudinitdisk. + """ + if cloud_init_nocloud: + if not template_spec['volumes']: + template_spec['volumes'] = [] + if not template_spec['domain']['devices']['disks']: + template_spec['domain']['devices']['disks'] = [] + + template_spec['volumes'].append({'name': 'ansiblecloudinitvolume', 'cloudInitNoCloud': cloud_init_nocloud}) + template_spec['domain']['devices']['disks'].append({ + 'name': 'ansiblecloudinitdisk', + 'volumeName': 'ansiblecloudinitvolume', + 'disk': {'bus': 'virtio'}, + }) + + def _define_interfaces(self, interfaces, template_spec): + """ + Takes interfaces parameter of Ansible and create kubevirt API interfaces + and networks strucutre out from it. + """ + if interfaces: + # Extract interfaces k8s specification from interfaces list passed to Ansible: + spec_interfaces = [] + for i in interfaces: + spec_interfaces.append(dict((k, v) for k, v in i.items() if k != 'network')) + template_spec['domain']['devices']['interfaces'] = spec_interfaces + + # Extract networks k8s specification from interfaces list passed to Ansible: + spec_networks = [] + for i in interfaces: + net = i['network'] + net['name'] = i['name'] + spec_networks.append(net) + template_spec['networks'] = spec_networks + + def _define_disks(self, disks, template_spec): + """ + Takes disks parameter of Ansible and create kubevirt API disks and + volumes strucutre out from it. + """ + if disks: + # Extract k8s specification from disks list passed to Ansible: + spec_disks = [] + for d in disks: + spec_disks.append(dict((k, v) for k, v in d.items() if k != 'volume')) + template_spec['domain']['devices']['disks'] = spec_disks + + # Extract volumes k8s specification from disks list passed to Ansible: + spec_volumes = [] + for d in disks: + volume = d['volume'] + volume['name'] = d['name'] + spec_volumes.append(volume) + template_spec['volumes'] = spec_volumes + + def execute_crud(self, kind, definition, template): + """ Module execution """ + self.client = self.get_api_client() + + disks = self.params.get('disks', []) + memory = self.params.get('memory') + labels = self.params.get('labels') + interfaces = self.params.get('interfaces') + cloud_init_nocloud = self.params.get('cloud_init_nocloud') + machine_type = self.params.get('machine_type') + template_spec = template['spec'] + + # Merge additional flat parameters: + if memory: + template_spec['domain']['resources']['requests']['memory'] = memory + + if labels: + template['metadata']['labels'] = labels + + if machine_type: + template_spec['domain']['machine']['type'] = machine_type + + # Define cloud init disk if defined: + self._define_cloud_init(cloud_init_nocloud, template_spec) + + # Define disks + self._define_disks(disks, template_spec) + + # Define interfaces: + self._define_interfaces(interfaces, template_spec) + + # Perform create/absent action: + definition = dict(self.merge_dicts(self.resource_definitions[0], definition)) + resource = self.find_resource(kind, self.api_version, fail=True) + definition = self.set_defaults(resource, definition) + return self.perform_action(resource, definition) diff --git a/lib/ansible/modules/cloud/kubevirt/__init__.py b/lib/ansible/modules/cloud/kubevirt/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ansible/modules/cloud/kubevirt/kubevirt_vm.py b/lib/ansible/modules/cloud/kubevirt/kubevirt_vm.py new file mode 100644 index 00000000000..06ec2f15493 --- /dev/null +++ b/lib/ansible/modules/cloud/kubevirt/kubevirt_vm.py @@ -0,0 +1,330 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2019, 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 + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: kubevirt_vm + +short_description: Manage KubeVirt virtual machine + +description: + - Use Openshift Python SDK to manage the state of KubeVirt virtual machines. + +version_added: "2.8" + +author: KubeVirt Team (@kubevirt) + +options: + state: + description: + - Set the virtual machine to either I(present), I(absent), I(running) or I(stopped). + - "I(present) - Create or update virtual machine." + - "I(absent) - Removes virtual machine." + - "I(running) - Create or update virtual machine and run it." + - "I(stopped) - Stops the virtual machine." + default: "present" + choices: + - present + - absent + - running + - stopped + type: str + name: + description: + - Name of the virtual machine. + required: true + type: str + namespace: + description: + - Namespace where the virtual machine exists. + required: true + type: str + ephemeral: + description: + - If (true) ephemeral vitual machine will be created. When destroyed it won't be accessible again. + - Works only with C(state) I(present) and I(absent). + type: bool + default: false + +extends_documentation_fragment: + - k8s_auth_options + - k8s_resource_options + - kubevirt_vm_options + - kubevirt_common_options + +requirements: + - python >= 2.7 + - openshift >= 0.8.2 +''' + +EXAMPLES = ''' +- name: Start virtual machine 'myvm' + kubevirt_vm: + state: running + name: myvm + namespace: vms + +- name: Create virtual machine 'myvm' and start it + kubevirt_vm: + state: running + name: myvm + namespace: vms + memory: 64M + disks: + - name: containerdisk + volume: + containerDisk: + image: kubevirt/cirros-container-disk-demo:latest + path: /custom-disk/cirros.img + disk: + bus: virtio + +- name: Create virtual machine 'myvm' with multus network interface + kubevirt_vm: + name: myvm + namespace: vms + memory: 512M + interfaces: + - name: default + bridge: {} + network: + pod: {} + - name: mynet + bridge: {} + network: + multus: + networkName: mynetconf + +- name: Combine inline definition with Ansible parameters + kubevirt_vm: + # Kubernetes specification: + definition: + metadata: + labels: + app: galaxy + service: web + origin: vmware + + # Ansible parameters: + state: running + name: myvm + namespace: vms + memory: 64M + disks: + - name: containerdisk + volume: + containerDisk: + image: kubevirt/cirros-container-disk-demo:latest + path: /custom-disk/cirros.img + disk: + bus: virtio + +- name: Start ephemeral virtual machine 'myvm' and wait to be running + kubevirt_vm: + ephemeral: true + state: running + wait: true + wait_timeout: 180 + name: myvm + namespace: vms + memory: 64M + labels: + kubevirt.io/vm: myvm + disks: + - name: containerdisk + volume: + containerDisk: + image: kubevirt/cirros-container-disk-demo:latest + path: /custom-disk/cirros.img + disk: + bus: virtio + +- name: Start fedora vm with cloud init + kubevirt_vm: + state: running + wait: true + name: myvm + namespace: vms + memory: 1024M + cloud_init_nocloud: + userData: |- + password: fedora + chpasswd: { expire: False } + disks: + - name: containerdisk + volume: + containerDisk: + image: kubevirt/fedora-cloud-container-disk-demo:latest + path: /disk/fedora.qcow2 + disk: + bus: virtio + +- name: Remove virtual machine 'myvm' + kubevirt_vm: + state: absent + name: myvm + namespace: vms +''' + +RETURN = ''' +kubevirt_vm: + description: + - The virtual machine dictionary specification returned by the API. + - "This dictionary contains all values returned by the KubeVirt API all options + are described here U(https://kubevirt.io/api-reference/master/definitions.html#_v1_virtualmachine)" + returned: success + type: complex + contains: {} +''' + + +import copy +import traceback + +try: + from openshift.dynamic.client import ResourceInstance +except ImportError: + # Handled in module_utils + pass + +from ansible.module_utils.k8s.common import AUTH_ARG_SPEC +from ansible.module_utils.kubevirt import ( + virtdict, + KubeVirtRawModule, + VM_COMMON_ARG_SPEC, + API_VERSION, +) + + +VM_ARG_SPEC = { + 'ephemeral': {'type': 'bool', 'default': False}, + 'state': { + 'type': 'str', + 'choices': [ + 'present', 'absent', 'running', 'stopped' + ], + 'default': 'present' + }, +} + + +class KubeVirtVM(KubeVirtRawModule): + + @property + def argspec(self): + """ argspec property builder """ + argument_spec = copy.deepcopy(AUTH_ARG_SPEC) + argument_spec.update(VM_COMMON_ARG_SPEC) + argument_spec.update(VM_ARG_SPEC) + return argument_spec + + def _manage_state(self, running, resource, existing, wait, wait_timeout): + definition = {'metadata': {'name': self.name, 'namespace': self.namespace}, 'spec': {'running': running}} + self.patch_resource(resource, definition, existing, self.name, self.namespace, merge_type='merge') + + if wait: + resource = self.find_resource('VirtualMachineInstance', self.api_version, fail=True) + w, stream = self._create_stream(resource, self.namespace, wait_timeout) + + if wait and stream is not None: + self._read_stream(resource, w, stream, self.name, running) + + def _read_stream(self, resource, watcher, stream, name, running): + """ Wait for ready_replicas to equal the requested number of replicas. """ + for event in stream: + if event.get('object'): + obj = ResourceInstance(resource, event['object']) + if running: + if obj.metadata.name == name and hasattr(obj, 'status'): + phase = getattr(obj.status, 'phase', None) + if phase: + if phase == 'Running' and running: + watcher.stop() + return + else: + # TODO: wait for stopped state: + watcher.stop() + return + + self.fail_json(msg="Error waiting for virtual machine. Try a higher wait_timeout value. %s" % obj.to_dict()) + + def manage_state(self, state): + wait = self.params.get('wait') + wait_timeout = self.params.get('wait_timeout') + resource_version = self.params.get('resource_version') + + resource_vm = self.find_resource('VirtualMachine', self.api_version) + existing = self.get_resource(resource_vm) + if resource_version and resource_version != existing.metadata.resourceVersion: + return False + + existing_running = False + resource_vmi = self.find_resource('VirtualMachineInstance', self.api_version) + existing_running_vmi = self.get_resource(resource_vmi) + if existing_running_vmi and hasattr(existing_running_vmi.status, 'phase'): + existing_running = existing_running_vmi.status.phase == 'Running' + + if state == 'running': + if existing_running: + return False + else: + self._manage_state(True, resource_vm, existing, wait, wait_timeout) + return True + elif state == 'stopped': + if not existing_running: + return False + else: + self._manage_state(False, resource_vm, existing, wait, wait_timeout) + return True + + def execute_module(self): + # Parse parameters specific for this module: + definition = virtdict() + ephemeral = self.params.get('ephemeral') + state = self.params.get('state') + + if not ephemeral: + definition['spec']['running'] = state == 'running' + + # Execute the CURD of VM: + template = definition['spec']['template'] + kind = 'VirtualMachineInstance' if ephemeral else 'VirtualMachine' + result = self.execute_crud(kind, definition, template) + changed = result['changed'] + + # Manage state of the VM: + if state in ['running', 'stopped']: + if not self.check_mode: + ret = self.manage_state(state) + changed = changed or ret + + # Return from the module: + self.exit_json(**{ + 'changed': changed, + 'kubevirt_vm': result.pop('result'), + 'result': result, + }) + + +def main(): + module = KubeVirtVM() + try: + module.api_version = API_VERSION + module.execute_module() + except Exception as e: + module.fail_json(msg=str(e), exception=traceback.format_exc()) + + +if __name__ == '__main__': + main() diff --git a/lib/ansible/plugins/doc_fragments/kubevirt_common_options.py b/lib/ansible/plugins/doc_fragments/kubevirt_common_options.py new file mode 100644 index 00000000000..463b20ceb48 --- /dev/null +++ b/lib/ansible/plugins/doc_fragments/kubevirt_common_options.py @@ -0,0 +1,70 @@ +# -*- coding: utf-8 -*- +# + +# Copyright (c) 2018, KubeVirt Team <@kubevirt> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + + +class ModuleDocFragment(object): + + # Standard oVirt documentation fragment + DOCUMENTATION = ''' +options: + wait: + description: + - "I(True) if the module should wait for the resource to get into desired state." + default: true + type: bool + force: + description: + - If set to C(True), and I(state) is C(present), an existing object will be replaced. + default: false + type: bool + wait_timeout: + description: + - "The amount of time in seconds the module should wait for the resource to get into desired state." + default: 120 + type: int + api_version: + description: + - "Specify the API version to be used." + type: str + default: kubevirt.io/v1alpha3 + aliases: + - api + - version + memory: + description: + - "The amount of memory to be requested by virtual machine." + - "For example 1024Mi." + type: str + machine_type: + description: + - QEMU machine type is the actual chipset of the virtual machine. + type: str + merge_type: + description: + - Whether to override the default patch merge approach with a specific type. By default, the strategic + merge will typically be used. + - For example, Custom Resource Definitions typically aren't updatable by the usual strategic merge. You may + want to use C(merge) if you see "strategic merge patch format is not supported" + - See U(https://kubernetes.io/docs/tasks/run-application/update-api-object-kubectl-patch/#use-a-json-merge-patch-to-update-a-deployment) + - Requires openshift >= 0.6.2 + - If more than one merge_type is given, the merge_types will be tried in order + - If openshift >= 0.6.2, this defaults to C(['strategic-merge', 'merge']), which is ideal for using the same parameters + on resource kinds that combine Custom Resources and built-in resources. For openshift < 0.6.2, the default + is simply C(strategic-merge). + choices: + - json + - merge + - strategic-merge + type: list + +requirements: + - python >= 2.7 + - openshift >= 0.8.2 +notes: + - "In order to use this module you have to install Openshift Python SDK. + To ensure it's installed with correct version you can create the following task: + I(pip: name=openshift version=0.8.2)" +''' diff --git a/lib/ansible/plugins/doc_fragments/kubevirt_vm_options.py b/lib/ansible/plugins/doc_fragments/kubevirt_vm_options.py new file mode 100644 index 00000000000..cf9d1303df4 --- /dev/null +++ b/lib/ansible/plugins/doc_fragments/kubevirt_vm_options.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +# + +# Copyright (c) 2018, KubeVirt Team <@kubevirt> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + + +class ModuleDocFragment(object): + + # Standard oVirt documentation fragment + DOCUMENTATION = ''' +options: + disks: + description: + - List of dictionaries which specify disks of the virtual machine. + - "A disk can be made accessible via four different types: I(disk), I(lun), I(cdrom), I(floppy)." + - "All possible configuration options are available in U(https://kubevirt.io/api-reference/master/definitions.html#_v1_disk)" + - Each disk must have specified a I(volume) that declares which volume type of the disk + All possible configuration options of volume are available in U(https://kubevirt.io/api-reference/master/definitions.html#_v1_volume). + type: list + labels: + description: + - Labels are key/value pairs that are attached to virtual machines. Labels are intended to be used to + specify identifying attributes of virtual machines that are meaningful and relevant to users, but do not directly + imply semantics to the core system. Labels can be used to organize and to select subsets of virtual machines. + Labels can be attached to virtual machines at creation time and subsequently added and modified at any time. + - More on labels that are used for internal implementation U(https://kubevirt.io/user-guide/#/misc/annotations_and_labels) + type: dict + interfaces: + description: + - An interface defines a virtual network interface of a virtual machine (also called a frontend). + - All possible configuration options interfaces are available in U(https://kubevirt.io/api-reference/master/definitions.html#_v1_interface) + - Each interface must have specified a I(network) that declares which logical or physical device it is connected to (also called as backend). + All possible configuration options of network are available in U(https://kubevirt.io/api-reference/master/definitions.html#_v1_network). + type: list + cloud_init_nocloud: + description: + - "Represents a cloud-init NoCloud user-data source. The NoCloud data will be added + as a disk to the virtual machine. A proper cloud-init installation is required inside the guest. + More information U(https://kubevirt.io/api-reference/master/definitions.html#_v1_cloudinitnocloudsource)" + type: dict +''' diff --git a/test/runner/requirements/units.txt b/test/runner/requirements/units.txt index 8271f3d0ccd..1544b20b622 100644 --- a/test/runner/requirements/units.txt +++ b/test/runner/requirements/units.txt @@ -43,3 +43,6 @@ linode_api4 ; python_version > '2.6' # APIv4 # requirement for the gitlab module python-gitlab httmock + +# requirment for kubevirt modules +openshift ; python_version >= '2.7' diff --git a/test/units/modules/cloud/kubevirt/__init__.py b/test/units/modules/cloud/kubevirt/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/units/modules/cloud/kubevirt/test_kubevirt_vm.py b/test/units/modules/cloud/kubevirt/test_kubevirt_vm.py new file mode 100644 index 00000000000..235dec4108f --- /dev/null +++ b/test/units/modules/cloud/kubevirt/test_kubevirt_vm.py @@ -0,0 +1,153 @@ +import json +import pytest + +from units.compat.mock import patch, MagicMock + +from ansible.module_utils import basic +from ansible.module_utils._text import to_bytes +from ansible.module_utils.k8s.common import K8sAnsibleMixin +from ansible.module_utils.k8s.raw import KubernetesRawModule + +from ansible.modules.cloud.kubevirt import kubevirt_vm as mymodule + +openshiftdynamic = pytest.importorskip("openshift.dynamic", minversion="0.6.2") +helpexceptions = pytest.importorskip("openshift.helper.exceptions", minversion="0.6.2") + +KIND = 'VirtulMachine' +RESOURCE_DEFAULT_ARGS = {'api_version': 'v1', 'group': 'kubevirt.io', + 'prefix': 'apis', 'namespaced': True} + + +def set_module_args(args): + args = json.dumps({'ANSIBLE_MODULE_ARGS': args}) + basic._ANSIBLE_ARGS = to_bytes(args) + + +class AnsibleExitJson(Exception): + """Exception class to be raised by module.exit_json and caught + by the test case""" + def __init__(self, **kwargs): + for k in kwargs: + setattr(self, k, kwargs[k]) + + def __getitem__(self, attr): + return getattr(self, attr) + + +class AnsibleFailJson(Exception): + """Exception class to be raised by module.fail_json and caught + by the test case""" + def __init__(self, **kwargs): + for k in kwargs: + setattr(self, k, kwargs[k]) + + def __getitem__(self, attr): + return getattr(self, attr) + + +def exit_json(*args, **kwargs): + if 'changed' not in kwargs: + kwargs['changed'] = False + raise AnsibleExitJson(**kwargs) + + +def fail_json(*args, **kwargs): + raise AnsibleFailJson(**kwargs) + + +@pytest.fixture(autouse=True) +def setup_mixtures(self, monkeypatch): + monkeypatch.setattr( + KubernetesRawModule, "exit_json", exit_json) + monkeypatch.setattr( + KubernetesRawModule, "fail_json", fail_json) + # Create mock methods in Resource directly, otherwise dyn client + # tries binding those to corresponding methods in DynamicClient + # (with partial()), which is more problematic to intercept + openshiftdynamic.Resource.get = MagicMock() + openshiftdynamic.Resource.create = MagicMock() + openshiftdynamic.Resource.delete = MagicMock() + openshiftdynamic.Resource.patch = MagicMock() + # Globally mock some methods, since all tests will use this + K8sAnsibleMixin.get_api_client = MagicMock() + K8sAnsibleMixin.get_api_client.return_value = None + K8sAnsibleMixin.find_resource = MagicMock() + + +def test_vm_multus_creation(self): + args = dict( + state='present', name='testvm', + namespace='vms', api_version='v1', + interfaces=[ + {'bridge': {}, 'name': 'default', 'network': {'pod': {}}}, + {'bridge': {}, 'name': 'mynet', 'network': {'multus': {'networkName': 'mynet'}}}, + ], + wait=False, + ) + set_module_args(args) + + openshiftdynamic.Resource.get.return_value = None + resource_args = dict(kind=KIND, **RESOURCE_DEFAULT_ARGS) + K8sAnsibleMixin.find_resource.return_value = openshiftdynamic.Resource(**resource_args) + + # Actual test: + with pytest.raises(AnsibleExitJson) as result: + mymodule.KubeVirtVM().execute_module() + assert result.value['changed'] + assert result.value['result']['method'] == 'create' + + +@pytest.mark.parametrize("_wait", (False, True)) +def test_resource_absent(self, _wait): + # Desired state: + args = dict( + state='absent', name='testvmi', + namespace='vms', api_version='v1', + wait=_wait, + ) + set_module_args(args) + + openshiftdynamic.Resource.get.return_value = None + resource_args = dict(kind=KIND, **RESOURCE_DEFAULT_ARGS) + K8sAnsibleMixin.find_resource.return_value = openshiftdynamic.Resource(**resource_args) + + # Actual test: + with pytest.raises(AnsibleExitJson) as result: + mymodule.KubeVirtVM().execute_module() + assert result.value['result']['method'] == 'delete' + + +@patch('openshift.watch.Watch') +def test_stream_creation(self, mock_watch): + # Desired state: + args = dict( + state='running', name='testvmi', namespace='vms', + api_version='v1', wait=True, + ) + set_module_args(args) + + # Actual test: + mock_watch.side_effect = helpexceptions.KubernetesException("Test", value=42) + with pytest.raises(AnsibleFailJson): + mymodule.KubeVirtVM().execute_module() + + +def test_simple_merge_dicts(self): + dict1 = {'labels': {'label1': 'value'}} + dict2 = {'labels': {'label2': 'value'}} + dict3 = json.dumps({'labels': {'label1': 'value', 'label2': 'value'}}, sort_keys=True) + assert dict3 == json.dumps(dict(mymodule.KubeVirtVM.merge_dicts(dict1, dict2)), sort_keys=True) + + +def test_simple_multi_merge_dicts(self): + dict1 = {'labels': {'label1': 'value', 'label3': 'value'}} + dict2 = {'labels': {'label2': 'value'}} + dict3 = json.dumps({'labels': {'label1': 'value', 'label2': 'value', 'label3': 'value'}}, sort_keys=True) + assert dict3 == json.dumps(dict(mymodule.KubeVirtVM.merge_dicts(dict1, dict2)), sort_keys=True) + + +def test_double_nested_merge_dicts(self): + dict1 = {'metadata': {'labels': {'label1': 'value', 'label3': 'value'}}} + dict2 = {'metadata': {'labels': {'label2': 'value'}}} + dict3 = json.dumps({'metadata': {'labels': {'label1': 'value', 'label2': 'value', 'label3': 'value'}}}, sort_keys=True) + assert dict3 == json.dumps(dict(mymodule.KubeVirtVM.merge_dicts(dict1, dict2)), sort_keys=True)