#!/usr/bin/python3

# SPDX-FileCopyrightText: Red Hat, Inc.
# SPDX-License-Identifier: GPL-2.0-or-later

from __future__ import absolute_import

"""
FCoE hook:
   if fcoe = true custom networks was specified enable FCoE for specified NIC
syntax:
   fcoe = true|false
"""


import os
import shlex
import traceback

import six

import hooking
from vdsm.common import fileutils
from vdsm.network.netconfpersistence import RunningConfig


FCOE_CONFIG_DIR = '/etc/fcoe/'
FCOE_DEFAULT_CONFIG = os.path.join(FCOE_CONFIG_DIR, 'cfg-ethx')
CONFFILE_HEADER = '# Generated by VDSM fcoe hook'


def _has_fcoe(net_attr):
    """
    Check if fcoe parameter was specified as custom network property
    """
    return net_attr.get('custom', {}).get('fcoe') is not None


def _get_config_name(nic):
    """
    helper to return filename of configuration file
    """
    return os.path.join(FCOE_CONFIG_DIR, 'cfg-%s' % nic)


def _parse_config(modificators):
    """
    Parse default config into dictionary
    """
    config = {}
    with open(FCOE_DEFAULT_CONFIG) as configfile:
        for line in shlex.split(configfile, comments=True):
            if not line:
                continue
            name, value = line.split('=', 1)
            if name in modificators:
                config[name] = modificators[name]
            else:
                config[name] = value
    return config


def _custom_parameter_to_config(custom_params):
    """
    Traslate custom_parameter: value dict into
    corresponding configuration options
    """

    # Check man fcoe-utils to add more parameters
    # TODO add mode to regexp in engine-config
    parameter_config_mapping = {'dcb': 'DCB_REQUIRED',
                                'enable': 'FCOE_ENABLE',
                                'auto_vlan': 'AUTO_VLAN', }

    config_params = {}
    for name, value in six.iteritems(custom_params):
        if name in parameter_config_mapping:
            config_params[parameter_config_mapping[name]] = value
    return config_params


def _configure(interface, custom_params):
    """
    Enable FCoE on specified interface by coping default configuration
    Change parameters specified in custom_params argument
    """
    # Map parameters to configuration mane
    # Check man fcoe-utils to add more parameters
    filename = _get_config_name(interface)
    config = _parse_config(_custom_parameter_to_config(custom_params))
    with open(filename, 'w') as outfile:
        outfile.write(CONFFILE_HEADER + "\n")
        for name, value in six.iteritems(config):
            outfile.write('%s="%s"\n' % (name, value))


def _unconfigure(interface):
    """
    Remove config file for specified interface
    """
    filename = _get_config_name(interface)
    if os.path.exists(filename):
        fileutils.rm_file(filename)


def _all_configured_fcoe_networks():
    """
    Return a mapping of configured fcoe networks in format
    (network_name, nic_name)
    """
    existing_fcoe_networks = {}
    config = RunningConfig()
    for net, net_attr in six.iteritems(config.networks):
        if _has_fcoe(net_attr):
            nic = net_attr.get('nic')
            if nic:
                existing_fcoe_networks[net] = nic
            else:
                hooking.log("WARNING: Invalid FCoE configuration of %s "
                            "detected. Please check documentation" % (net))

    return existing_fcoe_networks


def _unconfigure_removed(configured, removed_networks):
    """
    Unconfigure fcoe if network was removed from the DC
    """
    service_restart_required = False
    for net, _ in six.iteritems(removed_networks):
        if net in configured:
            if configured[net] is not None:
                _unconfigure(configured[net])
                service_restart_required = True
    return service_restart_required


def _unconfigure_non_fcoe(configured, changed_non_fcoe):
    """
    Unconfigure networks which are not longer has fcoe enabled
    Example:  Fcoe attribute was removed
    """
    service_restart_required = False
    for net, net_nic in six.iteritems(changed_non_fcoe):
        if net in configured and net_nic is not None:
            _unconfigure(net_nic)
            service_restart_required = True
    return service_restart_required


def _reconfigure_fcoe(configured, changed_fcoe, custom_params):
    """
    Configure all fcoe interfaces and unconfigure NIC which are not longer
    fcoe enabled
    Example: Moved from one NIC to another
    """
    service_restart_required = False
    for net, net_nic in six.iteritems(changed_fcoe):
        if net in configured and configured[net] != net_nic:
            _unconfigure(configured[net])
            service_restart_required = True
        if net_nic:
            _configure(net_nic, custom_params.get(net, {}))
            service_restart_required = True
        else:
            hooking.exit_hook("Failed to configure fcoe "
                              "on %s with no physical nic" % (net))
    return service_restart_required


def _parse_custom(custom_params):
    """
    Parse custom parameters list into dict
    Validation should be done on engine side
    """
    retval = {}
    params = [param.strip() for param in custom_params.split(',')]
    for param in params:
        if not param:
            # Empty string if custom parameter was not specified
            continue
        name, value = param.split('=', 1)
        retval[name.strip()] = value.strip()
    return retval


def main():
    """
    Create lists of running networks
    and networks to be (un)configured as FCoE or removed.
    """
    existing_fcoe_networks = _all_configured_fcoe_networks()

    changed_fcoe = {}
    changed_non_fcoe = {}
    removed_networks = {}
    custom_parameters = {}

    setup_nets_config = hooking.read_json()
    changed_all = setup_nets_config['request']['networks']

    for net, net_attr in six.iteritems(changed_all):
        custom_parameters_string = net_attr.get('custom', {}).get('fcoe', '')
        custom_parameters[net] = _parse_custom(custom_parameters_string)
        if _has_fcoe(net_attr):
            changed_fcoe[net] = net_attr.get('nic')
        elif hooking.tobool(net_attr.get('remove')):
            removed_networks[net] = net_attr.get('nic')
        else:
            changed_non_fcoe[net] = net_attr.get('nic')

    removed_service_restart_required = _unconfigure_removed(
        existing_fcoe_networks, removed_networks
    )
    non_fcoe_service_restart_required = _unconfigure_non_fcoe(
        existing_fcoe_networks, changed_non_fcoe
    )
    reconfigure_service_restart_required = _reconfigure_fcoe(
        existing_fcoe_networks, changed_fcoe, custom_parameters
    )

    if (
        removed_service_restart_required
        or non_fcoe_service_restart_required
        or reconfigure_service_restart_required
    ):
        # TODO If services are failed to start restore previous configuration
        # and notify user
        ret, _, err = hooking.execCmd(['/bin/systemctl', 'restart', 'lldpad'])
        if ret:
            hooking.log('Failed to restart lldpad service. err = %s' % (err))

        ret, _, err = hooking.execCmd(['/bin/systemctl', 'restart', 'fcoe'])
        if ret:
            hooking.log('Failed to restart fcoe service. err = %s' % (err))


if __name__ == '__main__':
    try:
        main()
    except:
        hooking.exit_hook(traceback.format_exc())
