#
# fcoe.py - fcoe class
#
# Copyright (C) 2009  Red Hat, Inc.  All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#

import os

import gi
gi.require_version("BlockDev", "3.0")

from gi.repository import BlockDev

from . import errors
from . import udev
from . import util
import logging
import time
import shutil
from .i18n import _
log = logging.getLogger("blivet")

_fcoe_module_loaded = False


def has_fcoe():
    global _fcoe_module_loaded
    if not _fcoe_module_loaded:
        try:
            BlockDev.utils.load_kernel_module("libfc", None)
        except BlockDev.UtilsError as e:
            log.error("failed to load libfc: %s", str(e))
        else:
            _fcoe_module_loaded = True
        if "bnx2x" in util.lsmod():
            log.info("fcoe: loading bnx2fc")
            try:
                BlockDev.utils.load_kernel_module("bnx2fc", None)
            except BlockDev.UtilsError as e:
                log.error("failed to load bnx2fc: %s", str(e))

    return os.access("/sys/module/libfc", os.X_OK)


class FCoE(object):

    """ FCoE utility class.

        This class will automatically discover and connect to EDD configured
        FCoE SAN's when the startup() method gets called. It can also be
        used to manually configure FCoE SAN's through the add_san() method.

        As this class needs to make sure certain things like starting fcoe
        daemons and connecting to firmware discovered SAN's only happens once
        and as it keeps a global list of all FCoE devices it is
        implemented as a Singleton.

        .. warning::
            Since this is a singleton class, calling deepcopy() on the instance
            just returns ``self`` with no copy being created.
    """

    def __init__(self):
        self.started = False
        self.lldpad_started = False
        self.nics = []
        self.added_nics = []

    # So that users can write fcoe() to get the singleton instance
    def __call__(self):
        return self

    def __deepcopy__(self, memo_dict):
        # pylint: disable=unused-argument
        return self

    def _stabilize(self):
        # I have no clue how long we need to wait, this ought to do the trick
        time.sleep(10)
        udev.settle()

    def _start_edd(self):
        try:
            buf = util.capture_output(["/usr/libexec/fcoe/fcoe_edd.sh", "-i"])
        except OSError as e:
            log.info("Failed to read FCoE EDD info: %s", e.strerror)
            return

        (key, _equals, val) = buf.strip().partition("=")
        if not val or key != "NIC":
            log.info("No FCoE EDD info found: %s", buf.rstrip())
            return

        dcb = self._iface_driver(val) not in ("bnx2x", "bnx2fc")

        log.info("FCoE NIC found in EDD: %s, using dcb: %s", val, dcb)
        self.add_san(val, dcb=dcb, auto_vlan=True)

    def startup(self):
        if self.started:
            return

        if not has_fcoe():
            return

        self._start_edd()
        self.started = True

    def _start_lldpad(self):
        if self.lldpad_started:
            return

        util.run_program(["systemctl", "start", "lldpad.service"])
        self.lldpad_started = True

    def add_san(self, nic, dcb=False, auto_vlan=True):
        """Activates FCoE SANs attached to interface specified by nic.

           Returns error message, or empty string if succeeded.
        """
        if not has_fcoe():
            raise errors.FCoEError(_("FCoE not available"))

        log.info("Activating FCoE SAN attached to %s, dcb: %s autovlan: %s",
                 nic, dcb, auto_vlan)

        rc = 0
        out = ""
        error_msg = ""
        if dcb:
            timeout = 60
            timeout_msg = ""
            self._start_lldpad()

            command_list = [
                ("waiting for lldpad to be ready", ["lldptool", "-p"]),
                ("retrying to turn dcb on", ["dcbtool", "sc", nic, "dcb", "on"]),
                ("retrying to set up dcb with pfc", ["dcbtool", "sc", nic, "pfc", "e:1", "a:1", "w:1"]),
                ("retrying to set up dcb for fcoe", ["dcbtool", "sc", nic, "app:fcoe", "e:1", "a:1", "w:1"]),
            ]

            for timeout_msg, cmd in command_list:
                rc, out = util.run_program_and_capture_output(cmd)
                while rc != 0:
                    timeout -= 1
                    time.sleep(1)
                    if timeout <= 0:
                        break
                    rc, out = util.run_program_and_capture_output(cmd)
                if timeout <= 0:
                    break

            if rc == 0:
                time.sleep(1)
            else:
                log.info("Timed out when %s", timeout_msg)

        if rc == 0:
            self.write_nic_fcoe_cfg(nic, dcb=dcb, auto_vlan=auto_vlan)
            rc, out = util.run_program_and_capture_output(
                ["systemctl", "restart", "fcoe.service"])

        if rc == 0:
            self._stabilize()
            self.nics.append((nic, dcb, auto_vlan))
        else:
            log.debug("Activating FCoE SAN failed: %s %s", rc, out)
            error_msg = out

        return error_msg

    def _iface_driver(self, nic):
        try:
            dpath = os.readlink("/sys/class/net/%s/device/driver" % nic)
        except OSError as e:
            log.debug("Can't find driver of device %s, %s", nic, e)
            driver = ""
        else:
            driver = os.path.basename(dpath)
        return driver

    def write(self, root):
        if not self.nics:
            return

        # Done before packages are installed so don't call
        # write_nic_fcoe_cfg in target root but just copy the cfgs
        dest_cfg_dir = root + "/etc/fcoe"
        # The directory may already exist on image installation (eg RHEV).
        if os.path.isdir(dest_cfg_dir):
            shutil.rmtree(dest_cfg_dir)
        shutil.copytree("/etc/fcoe", dest_cfg_dir)

    def write_nic_fcoe_cfg(self, nic, dcb=True, auto_vlan=True, enable=True, mode=None, root=""):
        cfg_dir = root + "/etc/fcoe"
        example_cfg = os.path.join(cfg_dir, "cfg-ethx")
        if os.access(example_cfg, os.R_OK):
            lines = open(example_cfg, "r").readlines()
        else:
            anaconda_cfg = """FCOE_ENABLE="yes"
DCB_REQUIRED="yes"
AUTO_VLAN="yes"
MODE="fabric"
"""
            lines = anaconda_cfg.splitlines(True)

        with open(os.path.join(cfg_dir, "cfg-%s" % nic), "w") as new_cfg:
            new_cfg.write("# Generated by Anaconda installer\n")
            for line in lines:
                if not line.strip().startswith("#"):
                    if line.startswith("FCOE_ENABLE"):
                        if enable:
                            line = 'FCOE_ENABLE="yes"\n'
                        else:
                            line = 'FCOE_ENABLE="no"\n'
                    elif line.startswith("DCB_REQUIRED"):
                        if dcb:
                            line = 'DCB_REQUIRED="yes"\n'
                        else:
                            line = 'DCB_REQUIRED="no"\n'
                    elif line.startswith("AUTO_VLAN"):
                        if auto_vlan:
                            line = 'AUTO_VLAN="yes"\n'
                        else:
                            line = 'AUTO_VLAN="no"\n'
                    elif line.startswith("MODE"):
                        if mode:
                            line = 'MODE="%s"\n' % mode
                new_cfg.write(line)


# Create FCoE singleton
fcoe = FCoE()
