#!/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_pvc

short_description: Manage PVCs on Kubernetes


author: KubeVirt Team (@kubevirt)

description:
  - Use Openshift Python SDK to manage PVCs on Kubernetes
  - Support Containerized Data Importer out of the box

options:
  resource_definition:
    description:
    - "A partial YAML definition of the PVC object being created/updated. Here you can define Kubernetes
      PVC Resource parameters not covered by this module's parameters."
    - "NOTE: I(resource_definition) has lower priority than module parameters. If you try to define e.g.
      I(metadata.namespace) here, that value will be ignored and I(namespace) used instead."
    aliases:
    - definition
    - inline
    type: dict
  state:
    description:
    - "Determines if an object should be created, patched, or deleted. When set to C(present), an object will be
      created, if it does not already exist. If set to C(absent), an existing object will be deleted. If set to
      C(present), an existing object will be patched, if its attributes differ from those specified using
      module options and I(resource_definition)."
    default: present
    choices:
    - present
    - absent
  force:
    description:
    - If set to C(True), and I(state) is C(present), an existing object will be replaced.
    default: false
    type: bool
  merge_type:
    description:
    - Whether to override the default patch merge approach with a specific type.
    - "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."
    - See U(https://kubernetes.io/docs/tasks/run-application/update-api-object-kubectl-patch/#use-a-json-merge-patch-to-update-a-deployment)
    - If more than one merge_type is given, the merge_types will be tried in order
    choices:
    - json
    - merge
    - strategic-merge
    type: list
  name:
    description:
      - Use to specify a PVC object name.
    required: true
    type: str
  namespace:
    description:
      - Use to specify a PVC object namespace.
    required: true
    type: str
  annotations:
    description:
      - Annotations attached to this object.
      - U(https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/)
    type: dict
  labels:
    description:
      - Labels attached to this object.
      - U(https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/)
    type: dict
  selector:
    description:
      - A label query over volumes to consider for binding.
      - U(https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/)
    type: dict
  access_modes:
    description:
      - Contains the desired access modes the volume should have.
      - "More info: U(https://kubernetes.io/docs/concepts/storage/persistent-volumes#access-modes)"
    type: list
  size:
    description:
      - How much storage to allocate to the PVC.
    type: str
    aliases:
    - storage
  storage_class_name:
    description:
      - Name of the StorageClass required by the claim.
      - "More info: U(https://kubernetes.io/docs/concepts/storage/persistent-volumes#class-1)"
    type: str
  volume_mode:
    description:
      - "This defines what type of volume is required by the claim. Value of Filesystem is implied when not
        included in claim spec. This is an alpha feature of kubernetes and may change in the future."
    type: str
  volume_name:
    description:
      - This is the binding reference to the PersistentVolume backing this claim.
    type: str
  cdi_source:
    description:
      - "If data is to be copied onto the PVC using the Containerized Data Importer you can specify the source of
        the data (along with any additional configuration) as well as it's format."
      - "Valid source types are: blank, http, s3, registry, pvc and upload. The last one requires using the
        M(community.general.kubevirt_cdi_upload) module to actually perform an upload."
      - "Source data format is specified using the optional I(content_type). Valid options are C(kubevirt)
        (default; raw image) and C(archive) (tar.gz)."
      - "This uses the DataVolume source syntax:
        U(https://github.com/kubevirt/containerized-data-importer/blob/master/doc/datavolumes.md#https3registry-source)"
    type: dict
  wait:
    description:
      - "If set, this module will wait for the PVC to become bound and CDI (if enabled) to finish its operation
        before returning."
      - "Used only if I(state) set to C(present)."
      - "Unless used in conjunction with I(cdi_source), this might result in a timeout, as clusters may be configured
        to not bind PVCs until first usage."
    default: false
    type: bool
  wait_timeout:
    description:
      - Specifies how much time in seconds to wait for PVC creation to complete if I(wait) option is enabled.
      - Default value is reasonably high due to an expectation that CDI might take a while to finish its operation.
    type: int
    default: 300

extends_documentation_fragment:
- community.kubernetes.k8s_auth_options


requirements:
  - python >= 2.7
  - openshift >= 0.8.2
'''

EXAMPLES = '''
- name: Create a PVC and import data from an external source
  kubevirt_pvc:
    name: pvc1
    namespace: default
    size: 100Mi
    access_modes:
      - ReadWriteOnce
    cdi_source:
      http:
        url: https://www.source.example/path/of/data/vm.img
      # If the URL points to a tar.gz containing the disk image, uncomment the line below:
      #content_type: archive

- name: Create a PVC as a clone from a different PVC
  kubevirt_pvc:
    name: pvc2
    namespace: default
    size: 100Mi
    access_modes:
      - ReadWriteOnce
    cdi_source:
      pvc:
        namespace: source-ns
        name: source-pvc

- name: Create a PVC ready for data upload
  kubevirt_pvc:
    name: pvc3
    namespace: default
    size: 100Mi
    access_modes:
      - ReadWriteOnce
    cdi_source:
      upload: yes
    # You need the kubevirt_cdi_upload module to actually upload something

- name: Create a PVC with a blank raw image
  kubevirt_pvc:
    name: pvc4
    namespace: default
    size: 100Mi
    access_modes:
      - ReadWriteOnce
    cdi_source:
      blank: yes

- name: Create a PVC and fill it with data from a container
  kubevirt_pvc:
    name: pvc5
    namespace: default
    size: 100Mi
    access_modes:
      - ReadWriteOnce
    cdi_source:
      registry:
        url: "docker://kubevirt/fedora-cloud-registry-disk-demo"

'''

RETURN = '''
result:
  description:
  - The created, patched, or otherwise present object. Will be empty in the case of a deletion.
  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: complex
     spec:
       description: Specific attributes of the object. Will vary based on the I(api_version) and I(kind).
       returned: success
       type: complex
     status:
       description: Current status details for the object.
       returned: success
       type: complex
     items:
       description: Returned only when multiple yaml documents are passed to src or resource_definition
       returned: when resource_definition or src contains list of objects
       type: list
     duration:
       description: elapsed time of task in seconds
       returned: when C(wait) is true
       type: int
       sample: 48
'''


import copy
import traceback

from collections import defaultdict

from ansible_collections.community.kubernetes.plugins.module_utils.common import AUTH_ARG_SPEC
from ansible_collections.community.kubernetes.plugins.module_utils.raw import KubernetesRawModule
from ansible_collections.community.general.plugins.module_utils.kubevirt import virtdict, KubeVirtRawModule


PVC_ARG_SPEC = {
    'name': {'required': True},
    'namespace': {'required': True},
    'state': {
        'type': 'str',
        'choices': [
            'present', 'absent'
        ],
        'default': 'present'
    },
    'force': {
        'type': 'bool',
        'default': False,
    },
    'merge_type': {
        'type': 'list',
        'choices': ['json', 'merge', 'strategic-merge']
    },
    'resource_definition': {
        'type': 'dict',
        'aliases': ['definition', 'inline']
    },
    'labels': {'type': 'dict'},
    'annotations': {'type': 'dict'},
    'selector': {'type': 'dict'},
    'access_modes': {'type': 'list'},
    'size': {
        'type': 'str',
        'aliases': ['storage']
    },
    'storage_class_name': {'type': 'str'},
    'volume_mode': {'type': 'str'},
    'volume_name': {'type': 'str'},
    'cdi_source': {'type': 'dict'},
    'wait': {
        'type': 'bool',
        'default': False
    },
    'wait_timeout': {
        'type': 'int',
        'default': 300
    }
}


class CreatePVCFailed(Exception):
    pass


class KubevirtPVC(KubernetesRawModule):
    def __init__(self):
        super(KubevirtPVC, self).__init__()

    @property
    def argspec(self):
        argument_spec = copy.deepcopy(AUTH_ARG_SPEC)
        argument_spec.update(PVC_ARG_SPEC)
        return argument_spec

    @staticmethod
    def fix_serialization(obj):
        if obj and hasattr(obj, 'to_dict'):
            return obj.to_dict()
        return obj

    def _parse_cdi_source(self, _cdi_src, metadata):
        cdi_src = copy.deepcopy(_cdi_src)
        annotations = metadata['annotations']
        labels = metadata['labels']

        valid_content_types = ('kubevirt', 'archive')
        valid_sources = ('http', 's3', 'pvc', 'upload', 'blank', 'registry')

        if 'content_type' in cdi_src:
            content_type = cdi_src.pop('content_type')
            if content_type not in valid_content_types:
                raise ValueError("cdi_source.content_type must be one of {0}, not: '{1}'".format(
                    valid_content_types, content_type))
            annotations['cdi.kubevirt.io/storage.contentType'] = content_type

        if len(cdi_src) != 1:
            raise ValueError("You must specify exactly one valid CDI source, not {0}: {1}".format(len(cdi_src), tuple(cdi_src.keys())))

        src_type = tuple(cdi_src.keys())[0]
        src_spec = cdi_src[src_type]

        if src_type not in valid_sources:
            raise ValueError("Got an invalid CDI source type: '{0}', must be one of {1}".format(src_type, valid_sources))

        # True for all cases save one
        labels['app'] = 'containerized-data-importer'

        if src_type == 'upload':
            annotations['cdi.kubevirt.io/storage.upload.target'] = ''
        elif src_type == 'blank':
            annotations['cdi.kubevirt.io/storage.import.source'] = 'none'
        elif src_type == 'pvc':
            if not isinstance(src_spec, dict) or sorted(src_spec.keys()) != ['name', 'namespace']:
                raise ValueError("CDI Source 'pvc' requires specifying 'name' and 'namespace' (and nothing else)")
            labels['app'] = 'host-assisted-cloning'
            annotations['k8s.io/CloneRequest'] = '{0}/{1}'.format(src_spec['namespace'], src_spec['name'])
        elif src_type in ('http', 's3', 'registry'):
            if not isinstance(src_spec, dict) or 'url' not in src_spec:
                raise ValueError("CDI Source '{0}' requires specifying 'url'".format(src_type))
            unknown_params = set(src_spec.keys()).difference(set(('url', 'secretRef', 'certConfigMap')))
            if unknown_params:
                raise ValueError("CDI Source '{0}' does not know recognize params: {1}".format(src_type, tuple(unknown_params)))
            annotations['cdi.kubevirt.io/storage.import.source'] = src_type
            annotations['cdi.kubevirt.io/storage.import.endpoint'] = src_spec['url']
            if 'secretRef' in src_spec:
                annotations['cdi.kubevirt.io/storage.import.secretName'] = src_spec['secretRef']
            if 'certConfigMap' in src_spec:
                annotations['cdi.kubevirt.io/storage.import.certConfigMap'] = src_spec['certConfigMap']

    def _wait_for_creation(self, resource, uid):
        return_obj = None
        desired_cdi_status = 'Succeeded'
        use_cdi = True if self.params.get('cdi_source') else False
        if use_cdi and 'upload' in self.params['cdi_source']:
            desired_cdi_status = 'Running'

        for event in resource.watch(namespace=self.namespace, timeout=self.params.get('wait_timeout')):
            entity = event['object']
            metadata = entity.metadata
            if not hasattr(metadata, 'uid') or metadata.uid != uid:
                continue
            if entity.status.phase == 'Bound':
                if use_cdi and hasattr(metadata, 'annotations'):
                    import_status = metadata.annotations.get('cdi.kubevirt.io/storage.pod.phase')
                    if import_status == desired_cdi_status:
                        return_obj = entity
                        break
                    elif import_status == 'Failed':
                        raise CreatePVCFailed("PVC creation incomplete; importing data failed")
                else:
                    return_obj = entity
                    break
            elif entity.status.phase == 'Failed':
                raise CreatePVCFailed("PVC creation failed")

        if not return_obj:
            raise CreatePVCFailed("PVC creation timed out")

        return self.fix_serialization(return_obj)

    def execute_module(self):
        KIND = 'PersistentVolumeClaim'
        API = 'v1'

        definition = virtdict()
        definition['kind'] = KIND
        definition['apiVersion'] = API

        metadata = definition['metadata']
        metadata['name'] = self.params.get('name')
        metadata['namespace'] = self.params.get('namespace')
        if self.params.get('annotations'):
            metadata['annotations'] = self.params.get('annotations')
        if self.params.get('labels'):
            metadata['labels'] = self.params.get('labels')
        if self.params.get('cdi_source'):
            self._parse_cdi_source(self.params.get('cdi_source'), metadata)

        spec = definition['spec']
        if self.params.get('access_modes'):
            spec['accessModes'] = self.params.get('access_modes')
        if self.params.get('size'):
            spec['resources']['requests']['storage'] = self.params.get('size')
        if self.params.get('storage_class_name'):
            spec['storageClassName'] = self.params.get('storage_class_name')
        if self.params.get('selector'):
            spec['selector'] = self.params.get('selector')
        if self.params.get('volume_mode'):
            spec['volumeMode'] = self.params.get('volume_mode')
        if self.params.get('volume_name'):
            spec['volumeName'] = self.params.get('volume_name')

        # 'resource_definition:' has lower priority than module parameters
        definition = dict(KubeVirtRawModule.merge_dicts(definition, self.resource_definitions[0]))

        self.client = self.get_api_client()
        resource = self.find_resource(KIND, API, fail=True)
        definition = self.set_defaults(resource, definition)
        result = self.perform_action(resource, definition)
        if self.params.get('wait') and self.params.get('state') == 'present':
            result['result'] = self._wait_for_creation(resource, result['result']['metadata']['uid'])

        self.exit_json(**result)


def main():
    module = KubevirtPVC()
    try:
        module.execute_module()
    except Exception as e:
        module.fail_json(msg=str(e), exception=traceback.format_exc())


if __name__ == '__main__':
    main()
