
from __future__ import absolute_import
from __future__ import division

import errno
import logging
import os
import selinux

import blivet
import blivet.formats
import blivet.formats.fs
import blivet.size
from blivet.devices import LVMLogicalVolumeDevice
from blivet.devices import LVMThinLogicalVolumeDevice
from blivet import udev

from vdsm.common import cmdutils
from vdsm.common import commands
from vdsm.gluster import exception as ge
from vdsm.gluster import fstab
from . import gluster_mgmt_api


log = logging.getLogger("Gluster")
_pvCreateCommandPath = cmdutils.CommandPath("pvcreate",
                                            "/sbin/pvcreate",
                                            "/usr/sbin/pvcreate",)
_vgCreateCommandPath = cmdutils.CommandPath("vgcreate",
                                            "/sbin/vgcreate",
                                            "/usr/sbin/vgcreate",)
_lvconvertCommandPath = cmdutils.CommandPath("lvconvert",
                                             "/sbin/lvconvert",
                                             "/usr/sbin/lvconvert",)
_lvchangeCommandPath = cmdutils.CommandPath("lvchange",
                                            "/sbin/lvchange",
                                            "/usr/sbin/lvchange",)
_vgscanCommandPath = cmdutils.CommandPath("vgscan",
                                          "/sbin/vgscan",
                                          "/usr/sbin/vgscan",)
_semanageCommandPath = cmdutils.CommandPath("semanage",
                                            "/sbin/semanage",
                                            "/usr/sbin/semanage",)

DEFAULT_CHUNK_SIZE_KB = 256
DEFAULT_METADATA_SIZE_KB = 16777216
MIN_VG_SIZE = 1048576
MIN_METADATA_PERCENT = 0.005
DEFAULT_FS_TYPE = "xfs"
DEFAULT_MOUNT_OPTIONS = "inode64,noatime"


def _getDeviceSize(device, unitType='MiB'):
    if hasattr(blivet.size, 'MiB'):
        unit = getattr(blivet.size, unitType)
        return device.size.convertTo(unit)
    else:
        return device.size.convertTo(spec=unitType)


def _getDeviceDict(device, createBrick=False):
    info = {'name': device.name,
            'devPath': device.path,
            'devUuid': device.uuid or '',
            'bus': device.bus or '',
            'model': '',
            'fsType': '',
            'mountPoint': '',
            'uuid': '',
            'createBrick': createBrick}
    if isinstance(device.size, blivet.size.Size):
        info['size'] = '%s' % _getDeviceSize(device)
    else:
        info['size'] = '%s' % device.size
    if not info['bus'] and device.parents:
        info['bus'] = device.parents[0].bus
    if device.model:
        info['model'] = "%s (%s)" % (device.model, device.type)
    else:
        info['model'] = device.type
    if device.format:
        info['uuid'] = device.format.uuid or ''
        if hasattr(udev, 'get_device'):
            dev = udev.get_device(device.sysfsPath) or {}
        elif hasattr(udev, 'udev_get_device'):
            dev = udev.udev_get_device(device.sysfsPath) or {}
        else:
            dev = {}
        info['fsType'] = device.format.type or dev.get('ID_FS_TYPE', '')
    if hasattr(device.format, 'mountpoint'):
        info['mountPoint'] = device.format.mountpoint or ''
    return info


def _parseDevices(devices):
    deviceList = []
    for device in devices:
        deviceList.append(_getDeviceDict(device, _canCreateBrick(device)))
    return deviceList


def _canCreateBrick(device):
    if not device or device.kids > 0 or device.format.type or \
       hasattr(device.format, 'mountpoint') or \
       device.type in ['cdrom', 'lvmvg', 'lvmthinpool', 'lvmlv', 'lvmthinlv']:
        return False
    return True


def _reset_blivet(blivetEnv):
    try:
        blivetEnv.reset()
    except (blivet.errors.UnusableConfigurationError,
            blivet.errors.StorageError) as e:
        log.error("Error: %s" % e.message)


@gluster_mgmt_api
def storageDevicesList():
    blivetEnv = blivet.Blivet()
    _reset_blivet(blivetEnv)
    return _parseDevices(blivetEnv.devices)


@gluster_mgmt_api
def createBrick(brickName, mountPoint, devNameList, fsType=DEFAULT_FS_TYPE,
                raidParams={}):
    def _getDeviceList(devNameList):
        return [blivetEnv.devicetree.getDeviceByName(devName.split("/")[-1])
                for devName in devNameList]

    def _createPV(deviceList, alignment):
        for dev in deviceList:
            rc, out, err = commands.execCmd([_pvCreateCommandPath.cmd,
                                             '--dataalignment',
                                             '%sk' % alignment,
                                             dev.path])
            if rc:
                raise ge.GlusterHostStorageDevicePVCreateFailedException(
                    dev.path, alignment, rc, out, err)
        _reset_blivet(blivetEnv)
        return _getDeviceList([dev.name for dev in deviceList])

    def _createVG(vgName, deviceList, stripeSize):
        devices = ','.join([device.path for device in deviceList])
        rc, out, err = commands.execCmd([_vgCreateCommandPath.cmd,
                                         '-s', '%sk' % stripeSize,
                                         vgName, devices])
        if rc:
            raise ge.GlusterHostStorageDeviceVGCreateFailedException(
                vgName, devices, stripeSize, rc, out, err)
        blivetEnv.reset()
        return blivetEnv.devicetree.getDeviceByName(vgName)

    def _createThinPool(poolName, vg, alignment,
                        poolMetaDataSize, poolDataSize):
        metaName = "meta-%s" % poolName
        vgPoolName = "%s/%s" % (vg.name, poolName)
        metaLv = LVMLogicalVolumeDevice(
            metaName, parents=[vg],
            size=blivet.size.Size('%d KiB' % poolMetaDataSize))
        poolLv = LVMLogicalVolumeDevice(
            poolName, parents=[vg],
            size=blivet.size.Size('%d KiB' % poolDataSize))
        blivetEnv.createDevice(metaLv)
        blivetEnv.createDevice(poolLv)
        blivetEnv.doIt()

        rc, out, err = commands.execCmd([_lvconvertCommandPath.cmd,
                                         '--chunksize', '%sK' % alignment,
                                         '--thinpool', vgPoolName,
                                         '--poolmetadata',
                                         "%s/%s" % (vg.name, metaName),
                                         '--poolmetadataspar', 'n', '-y'])

        if rc:
            raise ge.GlusterHostStorageDeviceLVConvertFailedException(
                vg.path, alignment, rc, out, err)
        rc, out, err = commands.execCmd([_lvchangeCommandPath.cmd,
                                         '--zero', 'n', vgPoolName])
        if rc:
            raise ge.GlusterHostStorageDeviceLVChangeFailedException(
                vgPoolName, rc, out, err)
        _reset_blivet(blivetEnv)
        return blivetEnv.devicetree.getDeviceByName(poolLv.name)

    if os.path.ismount(mountPoint):
        raise ge.GlusterHostStorageMountPointInUseException(mountPoint)

    vgName = "vg-" + brickName
    poolName = "pool-" + brickName
    poolDataSize = 0
    count = 0
    raidType = raidParams.get('type')
    metaDataSizeKib = DEFAULT_METADATA_SIZE_KB
    if raidType == '6':
        count = raidParams['pdCount'] - 2
        alignment = raidParams['stripeSize'] * count
        chunkSize = alignment
    elif raidType == '10':
        count = raidParams['pdCount'] // 2
        alignment = raidParams['stripeSize'] * count
        chunkSize = DEFAULT_CHUNK_SIZE_KB
        alignment = DEFAULT_CHUNK_SIZE_KB
        chunkSize = DEFAULT_CHUNK_SIZE_KB

    blivetEnv = blivet.Blivet()
    _reset_blivet(blivetEnv)

    deviceList = _getDeviceList(devNameList)

    notFoundList = set(devNameList).difference(
        set([dev.name for dev in deviceList]))
    if notFoundList:
        raise ge.GlusterHostStorageDeviceNotFoundException(notFoundList)

    inUseList = set(devNameList).difference(set([not _canCreateBrick(
        dev) or dev.name for dev in deviceList]))
    if inUseList:
        raise ge.GlusterHostStorageDeviceInUseException(inUseList)

    pvDeviceList = _createPV(deviceList, alignment)
    vg = _createVG(vgName, pvDeviceList, alignment)

    vgSizeKib = int(_getDeviceSize(vg, 'KiB'))
    if _getDeviceSize(vg) < MIN_VG_SIZE:
        metaDataSizeKib = vgSizeKib * MIN_METADATA_PERCENT
    poolDataSize = vgSizeKib - metaDataSizeKib

    metaDataSizeKib = (metaDataSizeKib - (metaDataSizeKib % alignment))
    poolDataSize = (poolDataSize - (poolDataSize % alignment))

    pool = _createThinPool(poolName, vg, chunkSize, metaDataSizeKib,
                           poolDataSize)
    thinlv = LVMThinLogicalVolumeDevice(
        brickName, parents=[pool],
        size=blivet.size.Size('%d KiB' % poolDataSize),
        grow=True)
    blivetEnv.createDevice(thinlv)
    blivetEnv.doIt()

    if fsType != DEFAULT_FS_TYPE:
        log.error("fstype %s is currently unsupported" % fsType)
        raise ge.GlusterHostStorageDeviceMkfsFailedException(fsType)

    format = blivet.formats.getFormat(DEFAULT_FS_TYPE, device=thinlv.path,
                                      mountopts=DEFAULT_MOUNT_OPTIONS)
    format._defaultFormatOptions = ["-f", "-i", "size=512", "-n", "size=8192"]
    if raidParams.get('type') == '6':
        format._defaultFormatOptions += ["-d", "sw=%s,su=%sk" % (
            count, raidParams.get('stripeSize'))]
    blivetEnv.formatDevice(thinlv, format)
    blivetEnv.doIt()

    try:
        os.makedirs(mountPoint)
    except OSError as e:
        if errno.EEXIST != e.errno:
            errMsg = "[Errno %s] %s: '%s'" % (e.errno, e.strerror, e.filename)
            raise ge.GlusterHostStorageDeviceMakeDirsFailedException(
                err=[errMsg])
    thinlv.format.setup(mountpoint=mountPoint)
    blivetEnv.doIt()

    rc, out, err = commands.execCmd([_vgscanCommandPath.cmd])
    if rc:
        raise ge.GlusterHostStorageDeviceVGScanFailedException(rc, out, err)
    fstab.FsTab().add(thinlv.path, mountPoint,
                      DEFAULT_FS_TYPE, mntOpts=[DEFAULT_MOUNT_OPTIONS])

    if selinux.is_selinux_enabled():
        rc, out, err = commands.execCmd([_semanageCommandPath.cmd,
                                         'fcontext', '-a', '-t',
                                         'glusterd_brick_t', mountPoint])
        if rc:
            raise ge.GlusterHostFailedToSetSelinuxContext(mountPoint, rc,
                                                          out, err)
        try:
            selinux.restorecon(mountPoint.encode('utf-8'), recursive=True)
        except OSError as e:
            errMsg = "[Errno %s] %s: '%s'" % (e.errno, e.strerror, e.filename)
            raise ge.GlusterHostFailedToRunRestorecon(mountPoint, err=errMsg)
    return _getDeviceDict(thinlv)
