#!/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 DOCUMENTATION = ''' --- module: kubevirt_vm short_description: Manage KubeVirt virtual machine description: - Use Openshift Python SDK to manage the state of KubeVirt virtual machines. 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 a virtual machine. (And run it if it's ephemeral.)" - "I(absent) - Remove a virtual machine." - "I(running) - Create or update a virtual machine and run it." - "I(stopped) - Stop a virtual machine. (This deletes ephemeral VMs.)" 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 virtual 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 datavolumes: description: - "DataVolumes are a way to automate importing virtual machine disks onto pvcs during the virtual machine's launch flow. Without using a DataVolume, users have to prepare a pvc with a disk image before assigning it to a VM or VMI manifest. With a DataVolume, both the pvc creation and import is automated on behalf of the user." type: list template: description: - "Name of Template to be used in creation of a virtual machine." type: str template_parameters: description: - "New values of parameters from Template." type: dict extends_documentation_fragment: - community.kubernetes.k8s_auth_options - community.general.kubevirt_vm_options - community.general.kubevirt_common_options requirements: - python >= 2.7 - openshift >= 0.8.2 ''' EXAMPLES = ''' - name: Start virtual machine 'myvm' community.general.kubevirt_vm: state: running name: myvm namespace: vms - name: Create virtual machine 'myvm' and start it community.general.kubevirt_vm: state: running name: myvm namespace: vms memory: 64Mi cpu_cores: 1 bootloader: efi smbios_uuid: 5d307ca9-b3ef-428c-8861-06e72d69f223 cpu_model: Conroe headless: true hugepage_size: 2Mi tablets: - bus: virtio name: tablet1 cpu_limit: 3 cpu_shares: 2 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 community.general.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 community.general.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 community.general.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 community.general.kubevirt_vm: state: running wait: true name: myvm namespace: vms memory: 1024M cloud_init_nocloud: userData: |- #cloud-config 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 node_affinity: soft: - weight: 1 term: match_expressions: - key: security operator: In values: - S2 - name: Create virtual machine with datavolume and specify node affinity community.general.kubevirt_vm: name: myvm namespace: default memory: 1024Mi datavolumes: - name: mydv source: http: url: https://url/disk.qcow2 pvc: accessModes: - ReadWriteOnce storage: 5Gi node_affinity: hard: - term: match_expressions: - key: security operator: In values: - S1 - name: Remove virtual machine 'myvm' community.general.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 from ansible_collections.community.kubernetes.plugins.module_utils.common import AUTH_ARG_SPEC from ansible_collections.community.general.plugins.module_utils.kubevirt import ( virtdict, KubeVirtRawModule, VM_COMMON_ARG_SPEC, VM_SPEC_DEF_ARG_SPEC ) VM_ARG_SPEC = { 'ephemeral': {'type': 'bool', 'default': False}, 'state': { 'type': 'str', 'choices': [ 'present', 'absent', 'running', 'stopped' ], 'default': 'present' }, 'datavolumes': {'type': 'list'}, 'template': {'type': 'str'}, 'template_parameters': {'type': 'dict'}, } # Which params (can) modify 'spec:' contents of a VM: VM_SPEC_PARAMS = list(VM_SPEC_DEF_ARG_SPEC.keys()) + ['datavolumes', 'template', 'template_parameters'] 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 @staticmethod def fix_serialization(obj): if obj and hasattr(obj, 'to_dict'): return obj.to_dict() return obj def _wait_for_vmi_running(self): for event in self._kind_resource.watch(namespace=self.namespace, timeout=self.params.get('wait_timeout')): entity = event['object'] if entity.metadata.name != self.name: continue status = entity.get('status', {}) phase = status.get('phase', None) if phase == 'Running': return entity self.fail("Timeout occurred while waiting for virtual machine to start. Maybe try a higher wait_timeout value?") def _wait_for_vm_state(self, new_state): if new_state == 'running': want_created = want_ready = True else: want_created = want_ready = False for event in self._kind_resource.watch(namespace=self.namespace, timeout=self.params.get('wait_timeout')): entity = event['object'] if entity.metadata.name != self.name: continue status = entity.get('status', {}) created = status.get('created', False) ready = status.get('ready', False) if (created, ready) == (want_created, want_ready): return entity self.fail("Timeout occurred while waiting for virtual machine to achieve '{0}' state. " "Maybe try a higher wait_timeout value?".format(new_state)) def manage_vm_state(self, new_state, already_changed): new_running = True if new_state == 'running' else False changed = False k8s_obj = {} if not already_changed: k8s_obj = self.get_resource(self._kind_resource) if not k8s_obj: self.fail("VirtualMachine object disappeared during module operation, aborting.") if k8s_obj.spec.get('running', False) == new_running: return False, k8s_obj newdef = dict(metadata=dict(name=self.name, namespace=self.namespace), spec=dict(running=new_running)) k8s_obj, err = self.patch_resource(self._kind_resource, newdef, k8s_obj, self.name, self.namespace, merge_type='merge') if err: self.fail_json(**err) else: changed = True if self.params.get('wait'): k8s_obj = self._wait_for_vm_state(new_state) return changed, k8s_obj def _process_template_defaults(self, proccess_template, processedtemplate, defaults): def set_template_default(default_name, default_name_index, definition_spec): default_value = proccess_template['metadata']['annotations'][default_name] if default_value: values = definition_spec[default_name_index] default_values = [d for d in values if d.get('name') == default_value] defaults[default_name_index] = default_values if definition_spec[default_name_index] is None: definition_spec[default_name_index] = [] definition_spec[default_name_index].extend([d for d in values if d.get('name') != default_value]) devices = processedtemplate['spec']['template']['spec']['domain']['devices'] spec = processedtemplate['spec']['template']['spec'] set_template_default('defaults.template.cnv.io/disk', 'disks', devices) set_template_default('defaults.template.cnv.io/volume', 'volumes', spec) set_template_default('defaults.template.cnv.io/nic', 'interfaces', devices) set_template_default('defaults.template.cnv.io/network', 'networks', spec) def construct_definition(self, kind, our_state, ephemeral): definition = virtdict() processedtemplate = {} # Construct the API object definition: defaults = {'disks': [], 'volumes': [], 'interfaces': [], 'networks': []} vm_template = self.params.get('template') if vm_template: # Find the template the VM should be created from: template_resource = self.client.resources.get(api_version='template.openshift.io/v1', kind='Template', name='templates') proccess_template = template_resource.get(name=vm_template, namespace=self.params.get('namespace')) # Set proper template values taken from module option 'template_parameters': for k, v in self.params.get('template_parameters', {}).items(): for parameter in proccess_template.parameters: if parameter.name == k: parameter.value = v # Proccess the template: processedtemplates_res = self.client.resources.get(api_version='template.openshift.io/v1', kind='Template', name='processedtemplates') processedtemplate = processedtemplates_res.create(proccess_template.to_dict()).to_dict()['objects'][0] # Process defaults of the template: self._process_template_defaults(proccess_template, processedtemplate, defaults) if not ephemeral: definition['spec']['running'] = our_state == 'running' template = definition if ephemeral else definition['spec']['template'] template['metadata']['labels']['vm.cnv.io/name'] = self.params.get('name') dummy, definition = self.construct_vm_definition(kind, definition, template, defaults) return self.merge_dicts(definition, processedtemplate) def execute_module(self): # Parse parameters specific to this module: ephemeral = self.params.get('ephemeral') k8s_state = our_state = self.params.get('state') kind = 'VirtualMachineInstance' if ephemeral else 'VirtualMachine' _used_params = [name for name in self.params if self.params[name] is not None] # Is 'spec:' getting changed? vm_spec_change = True if set(VM_SPEC_PARAMS).intersection(_used_params) else False changed = False crud_executed = False method = '' # Underlying module_utils/k8s/* code knows only of state == present/absent; let's make sure not to confuse it if ephemeral: # Ephemerals don't actually support running/stopped; we treat those as aliases for present/absent instead if our_state == 'running': self.params['state'] = k8s_state = 'present' elif our_state == 'stopped': self.params['state'] = k8s_state = 'absent' else: if our_state != 'absent': self.params['state'] = k8s_state = 'present' # Start with fetching the current object to make sure it exists # If it does, but we end up not performing any operations on it, at least we'll be able to return # its current contents as part of the final json self.client = self.get_api_client() self._kind_resource = self.find_supported_resource(kind) k8s_obj = self.get_resource(self._kind_resource) if not self.check_mode and not vm_spec_change and k8s_state != 'absent' and not k8s_obj: self.fail("It's impossible to create an empty VM or change state of a non-existent VM.") # If there are (potential) changes to `spec:` or we want to delete the object, that warrants a full CRUD # Also check_mode always warrants a CRUD, as that'll produce a sane result if vm_spec_change or k8s_state == 'absent' or self.check_mode: definition = self.construct_definition(kind, our_state, ephemeral) result = self.execute_crud(kind, definition) changed = result['changed'] k8s_obj = result['result'] method = result['method'] crud_executed = True if ephemeral and self.params.get('wait') and k8s_state == 'present' and not self.check_mode: # Waiting for k8s_state==absent is handled inside execute_crud() k8s_obj = self._wait_for_vmi_running() if not ephemeral and our_state in ['running', 'stopped'] and not self.check_mode: # State==present/absent doesn't involve any additional VMI state management and is fully # handled inside execute_crud() (including wait logic) patched, k8s_obj = self.manage_vm_state(our_state, crud_executed) changed = changed or patched if changed: method = method or 'patch' # Return from the module: self.exit_json(**{ 'changed': changed, 'kubevirt_vm': self.fix_serialization(k8s_obj), 'method': method }) def main(): module = KubeVirtVM() try: module.execute_module() except Exception as e: module.fail_json(msg=str(e), exception=traceback.format_exc()) if __name__ == '__main__': main()