#!/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'
  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: 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
  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: |-
          #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
  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'
  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()
