# Copyright 2015 Red Hat, 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.


from ironic_lib import disk_utils
from oslo_concurrency import processutils
from oslo_log import log
from oslo_utils import uuidutils
try:
    import rtslib_fb
except ImportError:
    import rtslib as rtslib_fb

from ironic_python_agent import errors
from ironic_python_agent.extensions import base
from ironic_python_agent import hardware
from ironic_python_agent import netutils
from ironic_python_agent import utils

LOG = log.getLogger(__name__)
DEFAULT_ISCSI_PORTAL_PORT = 3260


def _execute(cmd, error_msg, **kwargs):
    try:
        stdout, stderr = utils.execute(*cmd, **kwargs)
    except processutils.ProcessExecutionError as e:
        LOG.error(error_msg)
        raise errors.ISCSICommandError(error_msg, e.exit_code,
                                       e.stdout, e.stderr)
    except OSError as e:
        LOG.error("Error: %(error)s: OS Error: %(os_error)s",
                  {'error': error_msg, 'os_error': e})
        raise errors.ISCSICommandError(e, e.errno, None, None)


def _wait_for_tgtd(attempts=10):
    """Wait for the ISCSI daemon to start."""
    # here, iscsi daemon is considered not running in case
    # tgtadm is not able to talk to tgtd to show iscsi targets
    cmd = ['tgtadm', '--lld', 'iscsi', '--mode', 'target', '--op', 'show']
    _execute(cmd, "ISCSI daemon didn't initialize", attempts=attempts)


def _start_tgtd(iqn, portal_port, device):
    """Start a ISCSI target for the device."""
    # Start ISCSI Target daemon
    _execute(['tgtd'], "Unable to start the ISCSI daemon")

    _wait_for_tgtd()

    # tgt service will create default portal on default port 3260.
    # so no need to create again if input portal_port == 3260.
    if portal_port != DEFAULT_ISCSI_PORTAL_PORT:
        cmd = ['tgtadm', '--lld', 'iscsi', '--mode', 'portal', '--op',
               'new', '--param', 'portal=0.0.0.0:' + str(portal_port)]
        _execute(cmd, "Error when adding a new portal with portal_port %d"
                 % portal_port)

    cmd = ['tgtadm', '--lld', 'iscsi', '--mode', 'target', '--op',
           'new', '--tid', '1', '--targetname', iqn]
    _execute(cmd, "Error when adding a new target for iqn %s" % iqn)

    cmd = ['tgtadm', '--lld', 'iscsi', '--mode', 'logicalunit', '--op',
           'new', '--tid', '1', '--lun', '1', '--backing-store', device]
    _execute(cmd, "Error when adding a new logical unit for iqn %s" % iqn)

    cmd = ['tgtadm', '--lld', 'iscsi', '--mode', 'target', '--op',
           'bind', '--tid', '1', '--initiator-address', 'ALL']
    _execute(cmd, "Error when enabling the target to accept the specific "
                  "initiators for iqn %s" % iqn)


def _start_lio(iqn, portal_port, device):
    try:
        storage = rtslib_fb.BlockStorageObject(name=iqn, dev=device)
        target = rtslib_fb.Target(rtslib_fb.FabricModule('iscsi'), iqn,
                                  mode='create')
        tpg = rtslib_fb.TPG(target, mode='create')
        # disable all authentication
        tpg.set_attribute('authentication', '0')
        tpg.set_attribute('demo_mode_write_protect', '0')
        tpg.set_attribute('generate_node_acls', '1')
        # lun=1 is hardcoded in ironic
        rtslib_fb.LUN(tpg, storage_object=storage, lun=1)
        tpg.enable = 1
    except rtslib_fb.utils.RTSLibError as exc:
        msg = 'Failed to create a target: {}'.format(exc)
        raise errors.ISCSIError(msg)

    try:
        # bind to the default port on all interfaces
        listen_ip = netutils.wrap_ipv6(netutils.get_wildcard_address())
        rtslib_fb.NetworkPortal(tpg, listen_ip, portal_port)
    except rtslib_fb.utils.RTSLibError as exc:
        msg = 'Failed to publish a target: {}'.format(exc)
        raise errors.ISCSIError(msg)


def clean_up(device):
    """Clean up iSCSI for a given device."""
    try:
        rts_root = rtslib_fb.RTSRoot()
    except (OSError, EnvironmentError, rtslib_fb.RTSLibError) as exc:
        try:
            LOG.info('Linux-IO is not available, attemting to stop tgtd '
                     'mapping. Error: %s.', exc)
            cmd = ['tgtadm', '--lld', 'iscsi', '--mode', 'target', '--op',
                   'unbind', '--tid', '1', '--initiator-address', 'ALL']
            _execute(cmd, "Error when cleaning up iscsi binds.")
        except errors.ISCSICommandError:
            # This command may fail if the target was already torn down
            # and that is okay, we just want to ensure it has been torn
            # down so there should be no disk locks persisting.
            pass
        cmd = ['sync']
        _execute(cmd, "Error flushing buffers to disk.")
        try:
            cmd = ['tgtadm', '--lld', 'iscsi', '--mode', 'target', '--op',
                   'delete', '--tid', '1']
            _execute(cmd, "Error deleting the iscsi target configuration.")
        except errors.ISCSICommandError:
            # This command should remove the target from being offered.
            # It is just proper clean-up, and often previously the IPA
            # side, or "target" was never really torn down in many cases.
            pass
        return

    storage = None
    for x in rts_root.storage_objects:
        if x.udev_path == device:
            storage = x
            break

    if storage is None:
        LOG.info('Device %(dev)s not found in the current iSCSI mounts '
                 '%(mounts)s.',
                 {'dev': device,
                  'mounts': [x.udev_path for x in rts_root.storage_objects]})
        return
    else:
        LOG.info('Deleting iSCSI target %(target)s for device %(dev)s.',
                 {'target': storage.name, 'dev': device})

    try:
        for x in rts_root.targets:
            if x.wwn == storage.name:
                x.delete()
                break

        storage.delete()
    except rtslib_fb.utils.RTSLibError as exc:
        msg = ('Failed to delete iSCSI target %(target)s for device %(dev)s: '
               '%(error)s') % {'target': storage.name,
                               'dev': device,
                               'error': exc}
        raise errors.ISCSIError(msg)


class ISCSIExtension(base.BaseAgentExtension):
    @base.sync_command('start_iscsi_target')
    def start_iscsi_target(self, iqn=None, wipe_disk_metadata=False,
                           portal_port=None):
        """Expose the disk as an ISCSI target.

        :param iqn: IQN for iSCSI target. If None, a new IQN is generated.
        :param wipe_disk_metadata: if the disk metadata should be wiped out
                                   before the disk is exposed.
        :param portal_port: customized port for iSCSI port, can be None.
        :returns: a dict that provides IQN of iSCSI target.
        """
        # If iqn is not given, generate one
        if iqn is None:
            iqn = 'iqn.2008-10.org.openstack:%s' % uuidutils.generate_uuid()

        device = hardware.dispatch_to_managers('get_os_install_device',
                                               permit_refresh=True)

        if wipe_disk_metadata:
            disk_utils.destroy_disk_metadata(
                device,
                self.agent.get_node_uuid())

        LOG.debug("Starting ISCSI target with iqn %(iqn)s on device "
                  "%(device)s", {'iqn': iqn, 'device': device})

        try:
            rts_root = rtslib_fb.RTSRoot()
        except (EnvironmentError, rtslib_fb.RTSLibError) as exc:
            LOG.warning('Linux-IO is not available, falling back to TGT. '
                        'Error: %s.', exc)
            rts_root = None

        if portal_port is None:
            portal_port = DEFAULT_ISCSI_PORTAL_PORT

        if rts_root is None:
            _start_tgtd(iqn, portal_port, device)
        else:
            _start_lio(iqn, portal_port, device)
            LOG.debug('Linux-IO configuration: %s', rts_root.dump())
        # Mark iscsi as previously started
        self.agent.iscsi_started = True
        LOG.info('Created iSCSI target with iqn %(iqn)s, portal port %(port)d,'
                 ' on device %(dev)s using %(method)s',
                 {'iqn': iqn, 'port': portal_port, 'dev': device,
                  'method': 'tgtd' if rts_root is None else 'linux-io'})

        return {"iscsi_target_iqn": iqn}
