# Copyright (c) 2016 VMware, Inc.
# All Rights Reserved.
#
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
#    not use this file except in compliance with the License. You may obtain
#    a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing, software
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
#    License for the specific language governing permissions and limitations
#    under the License.

import os
import tempfile

from oslo_log import log as logging
from oslo_utils import fileutils
try:
    from oslo_vmware import api
    from oslo_vmware import exceptions as oslo_vmw_exceptions
    from oslo_vmware import image_transfer
    from oslo_vmware.objects import datastore
    from oslo_vmware import rw_handles
    from oslo_vmware import vim_util
except ImportError:
    vim_util = None

from os_brick import exception
from os_brick.i18n import _
from os_brick.initiator import initiator_connector

LOG = logging.getLogger(__name__)


class VmdkConnector(initiator_connector.InitiatorConnector):
    """Connector for volumes created by the VMDK driver.

    This connector is only used for backup and restore of Cinder volumes.
    """

    TMP_IMAGES_DATASTORE_FOLDER_PATH = "cinder_temp"

    def __init__(self, *args, **kwargs):
        # Check if oslo.vmware library is available.
        if vim_util is None:
            message = _("Missing oslo_vmware python module, ensure oslo.vmware"
                        " library is installed and available.")
            raise exception.BrickException(message=message)

        super(VmdkConnector, self).__init__(*args, **kwargs)

        self._ip = None
        self._port = None
        self._username = None
        self._password = None
        self._api_retry_count = None
        self._task_poll_interval = None
        self._ca_file = None
        self._insecure = None
        self._tmp_dir = None
        self._timeout = None

    @staticmethod
    def get_connector_properties(root_helper, *args, **kwargs):
        return {}

    def check_valid_device(self, path, *args, **kwargs):
        try:
            with open(path, 'r') as dev:
                dev.read(1)
        except IOError:
            LOG.exception(
                "Failed to access the device on the path "
                "%(path)s", {"path": path})
            return False
        return True

    def get_volume_paths(self, connection_properties):
        return []

    def get_search_path(self):
        return None

    def get_all_available_volumes(self, connection_properties=None):
        pass

    def _load_config(self, connection_properties):
        config = connection_properties['config']
        self._ip = config['vmware_host_ip']
        self._port = config['vmware_host_port']
        self._username = config['vmware_host_username']
        self._password = config['vmware_host_password']
        self._api_retry_count = config['vmware_api_retry_count']
        self._task_poll_interval = config['vmware_task_poll_interval']
        self._ca_file = config['vmware_ca_file']
        self._insecure = config['vmware_insecure']
        self._tmp_dir = config['vmware_tmp_dir']
        self._timeout = config['vmware_image_transfer_timeout_secs']

    def _create_session(self):
        return api.VMwareAPISession(self._ip,
                                    self._username,
                                    self._password,
                                    self._api_retry_count,
                                    self._task_poll_interval,
                                    port=self._port,
                                    cacert=self._ca_file,
                                    insecure=self._insecure)

    def _create_temp_file(self, *args, **kwargs):
        fileutils.ensure_tree(self._tmp_dir)
        fd, tmp = tempfile.mkstemp(dir=self._tmp_dir, *args, **kwargs)
        os.close(fd)
        return tmp

    def _download_vmdk(
            self, tmp_file_path, session, backing, vmdk_path, vmdk_size):
        with open(tmp_file_path, "wb") as tmp_file:
            image_transfer.copy_stream_optimized_disk(
                None,
                self._timeout,
                tmp_file,
                session=session,
                host=self._ip,
                port=self._port,
                vm=backing,
                vmdk_file_path=vmdk_path,
                vmdk_size=vmdk_size)

    def connect_volume(self, connection_properties):
        # Download the volume vmdk from vCenter server to a temporary file
        # and return its path.
        self._load_config(connection_properties)
        session = self._create_session()

        tmp_file_path = self._create_temp_file(
            suffix=".vmdk", prefix=connection_properties['volume_id'])
        backing = vim_util.get_moref(connection_properties['volume'],
                                     "VirtualMachine")
        vmdk_path = connection_properties['vmdk_path']
        vmdk_size = connection_properties['vmdk_size']
        try:
            self._download_vmdk(
                tmp_file_path, session, backing, vmdk_path, vmdk_size)
        finally:
            session.logout()

        # Save the last modified time of the temporary so that we can decide
        # whether to upload the file back to vCenter server during disconnect.
        last_modified = os.path.getmtime(tmp_file_path)
        return {'path': tmp_file_path, 'last_modified': last_modified}

    def _snapshot_exists(self, session, backing):
        snapshot = session.invoke_api(vim_util,
                                      'get_object_property',
                                      session.vim,
                                      backing,
                                      'snapshot')
        if snapshot is None or snapshot.rootSnapshotList is None:
            return False
        return len(snapshot.rootSnapshotList) != 0

    def _create_temp_ds_folder(self, session, ds_folder_path, dc_ref):
        fileManager = session.vim.service_content.fileManager
        try:
            session.invoke_api(session.vim,
                               'MakeDirectory',
                               fileManager,
                               name=ds_folder_path,
                               datacenter=dc_ref)
        except oslo_vmw_exceptions.FileAlreadyExistsException:
            pass

    # Note(vbala) remove this method when we implement it in oslo.vmware
    def _upload_vmdk(
            self, read_handle, host, port, dc_name, ds_name, cookies,
            upload_file_path, file_size, cacerts, timeout_secs):
        write_handle = rw_handles.FileWriteHandle(host,
                                                  port,
                                                  dc_name,
                                                  ds_name,
                                                  cookies,
                                                  upload_file_path,
                                                  file_size,
                                                  cacerts=cacerts)
        image_transfer._start_transfer(read_handle, write_handle, timeout_secs)

    def _get_disk_device(self, session, backing):
        hardware_devices = session.invoke_api(vim_util,
                                              'get_object_property',
                                              session.vim,
                                              backing,
                                              'config.hardware.device')
        if hardware_devices.__class__.__name__ == "ArrayOfVirtualDevice":
            hardware_devices = hardware_devices.VirtualDevice
        for device in hardware_devices:
            if device.__class__.__name__ == "VirtualDisk":
                return device

    def _create_spec_for_disk_remove(self, session, disk_device):
        cf = session.vim.client.factory
        disk_spec = cf.create('ns0:VirtualDeviceConfigSpec')
        disk_spec.operation = 'remove'
        disk_spec.fileOperation = 'destroy'
        disk_spec.device = disk_device
        return disk_spec

    def _reconfigure_backing(self, session, backing, reconfig_spec):
        LOG.debug("Reconfiguring backing VM: %(backing)s with spec: %(spec)s.",
                  {'backing': backing,
                   'spec': reconfig_spec})
        reconfig_task = session.invoke_api(session.vim,
                                           "ReconfigVM_Task",
                                           backing,
                                           spec=reconfig_spec)
        LOG.debug("Task: %s created for reconfiguring backing VM.",
                  reconfig_task)
        session.wait_for_task(reconfig_task)

    def _detach_disk_from_backing(self, session, backing, disk_device):
        LOG.debug("Reconfiguring backing VM: %(backing)s to remove disk: "
                  "%(disk_device)s.",
                  {'backing': backing, 'disk_device': disk_device})

        cf = session.vim.client.factory
        reconfig_spec = cf.create('ns0:VirtualMachineConfigSpec')
        spec = self._create_spec_for_disk_remove(session, disk_device)
        reconfig_spec.deviceChange = [spec]
        self._reconfigure_backing(session, backing, reconfig_spec)

    def _attach_disk_to_backing(self, session, backing, disk_device):
        LOG.debug("Reconfiguring backing VM: %(backing)s to add disk: "
                  "%(disk_device)s.",
                  {'backing': backing, 'disk_device': disk_device})

        cf = session.vim.client.factory
        reconfig_spec = cf.create('ns0:VirtualMachineConfigSpec')
        disk_spec = cf.create('ns0:VirtualDeviceConfigSpec')
        disk_spec.operation = 'add'
        disk_spec.device = disk_device
        reconfig_spec.deviceChange = [disk_spec]
        self._reconfigure_backing(session, backing, reconfig_spec)

    def _disconnect(
            self, backing, tmp_file_path, session, ds_ref, dc_ref, vmdk_path):
        # The restored volume is in compressed (streamOptimized) format.
        # So we upload it to a temporary location in vCenter datastore and copy
        # the compressed vmdk to the volume vmdk. The copy operation
        # decompresses the disk to a format suitable for attaching to Nova
        # instances in vCenter.
        dstore = datastore.get_datastore_by_ref(session, ds_ref)
        ds_path = dstore.build_path(
            VmdkConnector.TMP_IMAGES_DATASTORE_FOLDER_PATH,
            os.path.basename(tmp_file_path))
        self._create_temp_ds_folder(
            session, str(ds_path.parent), dc_ref)

        with open(tmp_file_path, "rb") as tmp_file:
            dc_name = session.invoke_api(
                vim_util, 'get_object_property', session.vim, dc_ref, 'name')
            cookies = session.vim.client.cookiejar
            cacerts = self._ca_file if self._ca_file else not self._insecure
            self._upload_vmdk(
                tmp_file, self._ip, self._port, dc_name, dstore.name, cookies,
                ds_path.rel_path, os.path.getsize(tmp_file_path), cacerts,
                self._timeout)

        disk_device = self._get_disk_device(session, backing)
        self._detach_disk_from_backing(session, backing, disk_device)

        src = str(ds_path)
        LOG.debug("Copying %(src)s to %(dest)s", {'src': src,
                                                  'dest': vmdk_path})
        disk_mgr = session.vim.service_content.virtualDiskManager
        task = session.invoke_api(session.vim,
                                  'CopyVirtualDisk_Task',
                                  disk_mgr,
                                  sourceName=src,
                                  sourceDatacenter=dc_ref,
                                  destName=vmdk_path,
                                  destDatacenter=dc_ref)
        session.wait_for_task(task)

        self._attach_disk_to_backing(session, backing, disk_device)

        # Delete the compressed vmdk at the temporary location.
        LOG.debug("Deleting %s", src)
        file_mgr = session.vim.service_content.fileManager
        task = session.invoke_api(session.vim,
                                  'DeleteDatastoreFile_Task',
                                  file_mgr,
                                  name=src,
                                  datacenter=dc_ref)
        session.wait_for_task(task)

    def disconnect_volume(self, connection_properties, device_info,
                          force=False, ignore_errors=False):
        tmp_file_path = device_info['path']
        if not os.path.exists(tmp_file_path):
            msg = _("Vmdk: %s not found.") % tmp_file_path
            raise exception.NotFound(message=msg)

        session = None
        try:
            # We upload the temporary file to vCenter server only if it is
            # modified after connect_volume.
            if os.path.getmtime(tmp_file_path) > device_info['last_modified']:
                self._load_config(connection_properties)
                session = self._create_session()
                backing = vim_util.get_moref(connection_properties['volume'],
                                             "VirtualMachine")
                # Currently there is no way we can restore the volume if it
                # contains redo-log based snapshots (bug 1599026).
                if self._snapshot_exists(session, backing):
                    msg = (_("Backing of volume: %s contains one or more "
                             "snapshots; cannot disconnect.") %
                           connection_properties['volume_id'])
                    raise exception.BrickException(message=msg)

                ds_ref = vim_util.get_moref(
                    connection_properties['datastore'], "Datastore")
                dc_ref = vim_util.get_moref(
                    connection_properties['datacenter'], "Datacenter")
                vmdk_path = connection_properties['vmdk_path']
                self._disconnect(
                    backing, tmp_file_path, session, ds_ref, dc_ref, vmdk_path)
        finally:
            os.remove(tmp_file_path)
            if session:
                session.logout()

    def extend_volume(self, connection_properties):
        raise NotImplementedError
