# Copyright 2015, 2017 IBM Corp.
#
# 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.

"""Utilities related to the PowerVM management partition.

The management partition is a special LPAR that runs the PowerVM REST API
service.  It itself appears through the REST API as a LogicalPartition of type
aixlinux, but with the is_mgmt_partition property set to True.

The PowerVM Nova Compute service runs on the management partition.
"""
import glob
from nova import exception
from nova import utils
import os
from os import path
from oslo_concurrency import lockutils
from oslo_log import log as logging
from pypowervm.tasks import partition as pvm_par
import retrying

from nova_powervm.virt.powervm import exception as npvmex


LOG = logging.getLogger(__name__)

_MP_UUID = None


@lockutils.synchronized("mgmt_lpar_uuid")
def mgmt_uuid(adapter):
    """Returns the management partitions UUID."""
    global _MP_UUID
    if not _MP_UUID:
        _MP_UUID = pvm_par.get_this_partition(adapter).uuid
    return _MP_UUID


def _tee_as_root(fpath, payload):
    """Executes 'echo $payload | sudo tee -a $fpath'.

    :param fpath: The file system path to which to tee.
    :param payload: The string to pipe to the file.
    """
    utils.execute('tee', '-a', fpath, process_input=payload, run_as_root=True)


def discover_vscsi_disk(mapping, scan_timeout=300):
    """Bring a mapped device into the management partition and find its name.

    Based on a VSCSIMapping, scan the appropriate virtual SCSI host bus,
    causing the operating system to discover the mapped device.  Find and
    return the path of the newly-discovered device based on its UDID in the
    mapping.

    Note: scanning the bus will cause the operating system to discover *all*
    devices on that bus.  However, this method will only return the path for
    the specific device from the input mapping, based on its UDID.

    :param mapping: The pypowervm.wrappers.virtual_io_server.VSCSIMapping
                    representing the mapping of the desired disk to the
                    management partition.
    :param scan_timeout: The maximum number of seconds after scanning to wait
                         for the specified device to appear.
    :return: The udev-generated ("/dev/sdX") name of the discovered disk.
    :raise NoDiskDiscoveryException: If the disk did not appear after the
                                     specified timeout.
    :raise UniqueDiskDiscoveryException: If more than one disk appears with the
                                         expected UDID.
    """
    # TODO(IBM): Support for other host platforms.

    # Calculate the Linux slot number from the client adapter slot number.
    lslot = 0x30000000 | mapping.client_adapter.lpar_slot_num
    # We'll match the device ID based on the UDID, which is actually the last
    # 32 chars of the field we get from PowerVM.
    udid = mapping.backing_storage.udid[-32:]

    LOG.debug("Trying to discover VSCSI disk with UDID %(udid)s on slot "
              "%(slot)x.", {'udid': udid, 'slot': lslot})

    # Find the special file to scan the bus, and scan it.
    # This glob should yield exactly one result, but use the loop just in case.
    for scanpath in glob.glob(
            '/sys/bus/vio/devices/%x/host*/scsi_host/host*/scan' % lslot):
        # echo '- - -' | sudo tee -a /path/to/scan
        _tee_as_root(scanpath, '- - -')

    # Now see if our device showed up.  If so, we can reliably match it based
    # on its Linux ID, which ends with the disk's UDID.
    dpathpat = '/dev/disk/by-id/*%s' % udid

    # The bus scan is asynchronous.  Need to poll, waiting for the device to
    # spring into existence.  Stop when glob finds at least one device, or
    # after the specified timeout.  Sleep 1/4 second between polls.
    @retrying.retry(retry_on_result=lambda result: not result, wait_fixed=250,
                    stop_max_delay=scan_timeout * 1000)
    def _poll_for_dev(globpat):
        return glob.glob(globpat)
    try:
        disks = _poll_for_dev(dpathpat)
    except retrying.RetryError as re:
        raise npvmex.NoDiskDiscoveryException(
            bus=lslot, udid=udid, polls=re.last_attempt.attempt_number,
            timeout=scan_timeout)
    # If we get here, _poll_for_dev returned a nonempty list.  If not exactly
    # one entry, this is an error.
    if len(disks) != 1:
        raise npvmex.UniqueDiskDiscoveryException(path_pattern=dpathpat,
                                                  count=len(disks))

    # The by-id path is a symlink.  Resolve to the /dev/sdX path
    dpath = path.realpath(disks[0])
    LOG.debug("Discovered VSCSI disk with UDID %(udid)s on slot %(slot)x at "
              "path %(devname)s.",
              {'udid': udid, 'slot': lslot, 'devname': dpath})
    return dpath


def remove_block_dev(devpath, scan_timeout=10):
    """Remove a block device from the management partition.

    This method causes the operating system of the management partition to
    delete the device special files associated with the specified block device.

    :param devpath: Any path to the block special file associated with the
                    device to be removed.
    :param scan_timeout: The maximum number of seconds after scanning to wait
                         for the specified device to disappear.
    :raise InvalidDevicePath: If the specified device or its 'delete' special
                              file cannot be found.
    :raise DeviceDeletionException: If the deletion was attempted, but the
                                    device special file is still present
                                    afterward.
    """
    # Resolve symlinks, if any, to get to the /dev/sdX path
    devpath = path.realpath(devpath)
    try:
        os.stat(devpath)
    except OSError:
        raise exception.InvalidDevicePath(path=devpath)
    devname = devpath.rsplit('/', 1)[-1]
    delpath = '/sys/block/%s/device/delete' % devname
    try:
        os.stat(delpath)
    except OSError:
        raise exception.InvalidDevicePath(path=delpath)
    LOG.debug("Deleting block device %(devpath)s from the management "
              "partition via special file %(delpath)s.",
              {'devpath': devpath, 'delpath': delpath})
    _tee_as_root(delpath, '1')

    # The bus scan is asynchronous.  Need to poll, waiting for the device to
    # disappear.  Stop when stat raises OSError (dev file not found) - which is
    # success - or after the specified timeout (which is failure).  Sleep 1/4
    # second between polls.
    @retrying.retry(retry_on_result=lambda result: result, wait_fixed=250,
                    stop_max_delay=scan_timeout * 1000)
    def _poll_for_del(statpath):
        try:
            os.stat(statpath)
            return True
        except OSError:
            # Device special file is absent, as expected
            return False
    try:
        _poll_for_del(devpath)
    except retrying.RetryError as re:
        # stat just kept returning (dev file continued to exist).
        raise npvmex.DeviceDeletionException(
            devpath=devpath, polls=re.last_attempt.attempt_number,
            timeout=scan_timeout)
    # Else stat raised - the device disappeared - all done.
