

from __future__ import absolute_import
from __future__ import division

import functools
import json
import logging
import os
from contextlib import closing

try:
    import os_brick
except ImportError:
    os_brick = None

from vdsm.common import cmdutils
from vdsm.common import commands
from vdsm.common import supervdsm

from vdsm.storage import exception as se
from vdsm.storage import lvm
from vdsm.storage import managedvolumedb

HELPER = '/usr/libexec/vdsm/managedvolume-helper'
DEV_MAPPER = "/dev/mapper"
DEV_RBD = "/dev/rbd"

log = logging.getLogger("storage.managedvolume")


def requires_os_brick(func):

    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        if os_brick is None:
            raise se.ManagedVolumeNotSupported("Cannot import os_brick")
        return func(*args, **kwargs)

    return wrapper




@requires_os_brick
def connector_info():
    log.debug("Starting get connector_info")
    return run_helper("connector_info")


@requires_os_brick
def attach_volume(vol_id, connection_info):
    db = managedvolumedb.open()
    with closing(db):
        _add_volume(db, vol_id, connection_info)

        log.debug("Starting attach volume %s connection_info=%s",
                  vol_id, connection_info)

        try:
            attachment = run_helper("attach", connection_info)
            try:
                path = _resolve_path(vol_id, connection_info, attachment)
                db.update_volume(
                    vol_id,
                    path=path,
                    attachment=attachment,
                    multipath_id=attachment.get("multipath_id"))
                _invalidate_lvm_filter(attachment)
            except:
                _silent_detach(connection_info, attachment)
                raise
        except:
            _silent_remove(db, vol_id)
            raise

    log.debug("Attached volume %s attachment=%s", vol_id, attachment)

    return {"result": {'attachment': attachment, 'path': path,
                       'vol_id': vol_id}}


@requires_os_brick
def detach_volume(vol_id):
    db = managedvolumedb.open()
    with closing(db):
        try:
            vol_info = db.get_volume(vol_id)
        except managedvolumedb.NotFound:
            return

        log.debug("Starting detach volume %s vol_info=%s", vol_id, vol_info)

        if "path" in vol_info and os.path.exists(vol_info["path"]):
            run_helper("detach", vol_info)

        db.remove_volume(vol_id)


def volumes_info(vol_ids=()):
    db = managedvolumedb.open()
    with closing(db):
        result = []
        for vol_info in db.iter_volumes(vol_ids):
            if "path" in vol_info:
                vol_info["exists"] = os.path.exists(vol_info["path"])
            vol_info.pop("multipath_id", None)
            result.append(vol_info)

    return {"result": result}




def run_helper(sub_cmd, cmd_input=None):
    if os.geteuid() != 0:
        return supervdsm.getProxy().managedvolume_run_helper(
            sub_cmd, cmd_input=cmd_input)
    try:
        if cmd_input:
            cmd_input = json.dumps(cmd_input).encode("utf-8")
        result = commands.run([HELPER, sub_cmd], input=cmd_input)
    except cmdutils.Error as e:
        raise se.ManagedVolumeHelperFailed("Error executing helper: %s" % e)
    try:
        return json.loads(result)
    except ValueError as e:
        raise se.ManagedVolumeHelperFailed("Error loading result: %s" % e)




def _add_volume(db, vol_id, connection_info):
    try:
        db.add_volume(vol_id, connection_info)
    except managedvolumedb.VolumeAlreadyExists:
        vol_info = db.get_volume(vol_id)
        if vol_info["connection_info"] != connection_info:
            raise se.ManagedVolumeConnectionMismatch(
                vol_id, vol_info["connection_info"], connection_info)

        if "path" in vol_info and os.path.exists(vol_info["path"]):
            raise se.ManagedVolumeAlreadyAttached(
                vol_id, vol_info["path"], vol_info.get('attachment'))


def _resolve_path(vol_id, connection_info, attachment):
    vol_type = connection_info['driver_volume_type']
    if vol_type in ("iscsi", "fibre_channel"):
        if "multipath_id" not in attachment:
            raise se.ManagedVolumeUnsupportedDevice(vol_id, attachment)
        return os.path.join(DEV_MAPPER, attachment["multipath_id"])
    elif vol_type == "rbd":
        return os.path.join(DEV_RBD, connection_info['data']['name'])
    else:
        log.warning("Managed Volume without multipath info: %s",
                    attachment)
        return attachment["path"]


def _invalidate_lvm_filter(attachment):
    if "multipath_id" in attachment:
        lvm.invalidateFilter()


def _silent_remove(db, vol_id):
    try:
        db.remove_volume(vol_id)
    except Exception:
        log.exception("Failed to remove managed volume %s from DB", vol_id)


def _silent_detach(connection_info, attachment):
    vol_info = {"connection_info": connection_info,
                "attachment": attachment}
    try:
        run_helper("detach", vol_info)
    except Exception:
        log.exception("Failed to detach managed volume %s", vol_info)
