# Copyright 2022 Nvidia
#
# 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.

import os
import re
import shutil
import tempfile
from urllib import error as urlError
from urllib.parse import urlparse
from urllib import request

from ironic_lib.common.i18n import _
from ironic_lib.exception import IronicException
from oslo_concurrency import processutils
from oslo_log import log
from oslo_utils import fileutils

from ironic_python_agent import utils

FW_VERSION_REGEX = r'FW Version:\s*\t*(?P<fw_ver>\d+\.\d+\.\d+)'
RUNNING_FW_VERSION_REGEX = \
    r'FW Version\(Running\):\s*\t*(?P<fw_ver>\d+\.\d+\.\d+)'
ARRAY_PARAM_REGEX = r'(?P<param_name>\w+)\[((?P<index>\d+)|' \
                    r'((?P<first_index>\d+)\.\.(?P<last_index>\d+)))\]'
ARRAY_PARAM_VALUE_REGEX = r'Array\[(?P<first_index>\d+)' \
                          r'\.\.(?P<last_index>\d+)\]'
PSID_REGEX = r'PSID:\s*\t*(?P<psid>\w+)'
NETWORK_DEVICE_REGEX = r'02\d\d'
LOG = log.getLogger()

"""
Example of Nvidia NIC Firmware images list:
[
  {
    "url": "file:///firmware_images/fw1.bin",
    "checksum": "a94e683ea16d9ae44768f0a65942234d",
    "checksumType": "md5",
    "componentFlavor": "MT_0000000540",
    "version": "24.34.1002"
  },
  {
    "url": "http://10.10.10.10/firmware_images/fw2.bin",
    "checksum": "a94e683ea16d9ae44768f0a65942234c",
    "checksumType": "sha512",
    "componentFlavor": "MT_0000000652",
    "version": "24.34.1002"
  }
]

Example of Nvidia NIC Firmware settings list:
[
  {
    "deviceID": "1017",
    "globalConfig": {
      "NUM_OF_VFS": 127,
      "SRIOV_EN": True
    },
    "function0Config": {
      "PF_TOTAL_SF": 500
    },
    "function1Config": {
      "PF_TOTAL_SF": 600
    }
  },
  {
    "deviceID": "101B",
    "globalConfig": {
      "NUM_OF_VFS": 127,
      "SRIOV_EN": True
    },
    "function0Config": {
      "PF_TOTAL_SF": 500
    },
    "function1Config": {
      "PF_TOTAL_SF": 600
    }
  }
]
"""


def check_prereq():
    """Check that all needed tools are available in the system.

    :returns:   None
    :raises:    processutils.ProcessExecutionError
    """
    try:
        # check for mstflint
        utils.execute('mstflint', '-v')
        # check for mstconfig
        utils.execute('mstconfig', '-v')
        # check for mstfwreset
        utils.execute('mstfwreset', '-v')
        # check for lspci
        utils.execute('lspci', '--version')
    except processutils.ProcessExecutionError as e:
        LOG.error('Failed Prerequisite check. %s', e)
        raise e


class InvalidFirmwareImageConfig(IronicException):
    _msg_fmt = _('Invalid firmware image config: %(error_msg)s')


class InvalidFirmwareSettingsConfig(IronicException):
    _msg_fmt = _('Invalid firmware settings config: %(error_msg)s')


class MismatchChecksumError(IronicException):
    _msg_fmt = _('Mismatch Checksum for the firmware image: %(error_msg)s')


class MismatchComponentFlavor(IronicException):
    _msg_fmt = _('Mismatch Component Flavor: %(error_msg)s')


class MismatchFWVersion(IronicException):
    _msg_fmt = _('Mismatch Firmware version: %(error_msg)s')


class DuplicateComponentFlavor(IronicException):
    _msg_fmt = _('Duplicate Component Flavor for the firmware image: '
                 '%(error_msg)s')


class DuplicateDeviceID(IronicException):
    _msg_fmt = _('Duplicate Device ID for firmware settings: '
                 '%(error_msg)s')


class UnSupportedConfigByMstflintPackage(IronicException):
    _msg_fmt = _('Unsupported config by mstflint package: %(error_msg)s')


class UnSupportedConfigByFW(IronicException):
    _msg_fmt = _('Unsupported config by Firmware: %(error_msg)s')


class InvalidURLScheme(IronicException):
    _msg_fmt = _('Invalid URL Scheme: %(error_msg)s')


class NvidiaNicFirmwareOps(object):
    """Perform various Firmware related operations on nic device"""

    def __init__(self, dev):
        self.dev = dev
        self.dev_info = {}

    def parse_mstflint_query_output(out):
        """Parse Mstflint query output

        For now just extract 'FW Version' and 'PSID'
        :param out: string, mstflint query output
        :returns:   dict of query attributes
        """
        query_info = {}
        for line in out.split('\n'):
            line = line.strip()
            fw_ver = re.match(FW_VERSION_REGEX, line)
            running_fw_ver = re.match(RUNNING_FW_VERSION_REGEX, line)
            psid = re.match(PSID_REGEX, line)
            if fw_ver is not None:
                query_info['fw_ver'] = fw_ver.group('fw_ver')
            if running_fw_ver is not None:
                query_info['running_fw_ver'] = running_fw_ver.group('fw_ver')
            if psid is not None:
                query_info['psid'] = psid.group('psid')
        return query_info

    def _query_device(self, force=False):
        """Get firmware information from nvidia nic device

        :param force:   bool, force device query, even if query was executed in
                        previous calls.
        :returns:       dict of firmware image attributes
        :raises:        processutils.ProcessExecutionError
        """
        if not force and self.dev_info.get('device', '') == self.dev:
            return self.dev_info
        try:
            cmd = ('mstflint', '-d', self.dev, '-qq', 'query')
            out, _r = utils.execute(*cmd)
        except processutils.ProcessExecutionError as e:
            LOG.error('Failed to query firmware of device %s: %s',
                      self.dev, e)
            raise e
        self.dev_info = NvidiaNicFirmwareOps.parse_mstflint_query_output(out)
        self.dev_info['device'] = self.dev
        return self.dev_info

    def get_nic_psid(self):
        """Get the psid of nvidia nic device

        :returns:   string, the psid of the nic device
        """
        return self._query_device().get('psid')

    def is_image_changed(self):
        """Check if image changed and nic device requires firmware reset

        before applying any configurations on the device.
        Currently the reset happens if image was changed
        :returns:    bool, True if image changed
        """
        self._query_device(force=True)
        is_image_changed = 'running_fw_ver' in self.dev_info and \
                           self.dev_info['running_fw_ver'] != \
                           self.dev_info['fw_ver']
        return is_image_changed

    def _need_update(self, fw_version):
        """Check if nic device requires firmware update

        :param fw_version:  string, the firmware version of image
        :returns:           bool, True if update is needed
        """
        self._query_device(force=True)
        LOG.info('Device firmware version: %s , Image firmware version: %s',
                 self.dev_info['fw_ver'], fw_version)
        return self.dev_info['fw_ver'] != fw_version

    def _burn_firmware(self, image_path):
        """Burn firmware on device

        :param image_path:  string, firmware binary file path
        :returns:           None
        :raises:            processutils.ProcessExecutionError
        """
        LOG.info('Updating firmware image (%s) for device: %s',
                 image_path, self.dev)
        try:
            cmd = ('mstflint', '-d', self.dev, '-i', image_path,
                   '-y', 'burn')
            utils.execute(*cmd)
        except processutils.ProcessExecutionError as e:
            LOG.error('Failed to update firmware image for device %s, %s',
                      self.dev, e)
            raise e
        LOG.info('Device %s: firmware image successfully updated.', self.dev)

    def reset_device(self, raise_exception=False):
        """Reset nvidia nic to load the new firmware image

        :returns:   None
        :raises:    processutils.ProcessExecutionError
        """
        LOG.info('Device %s: Performing firmware reset.', self.dev)
        cmd = ('mstfwreset', '-d', self.dev, '-y', '--sync', '1', 'reset')
        try:
            utils.execute(*cmd)
            LOG.info('Device %s: Firmware successfully reset.', self.dev)
        except processutils.ProcessExecutionError as e:
            LOG.error('Failed to reset device %s %s', self.dev, e)
            if raise_exception:
                raise e

    def fw_update_if_needed(self, version, image_path):
        """Update firmware if the current version not equal image version

        :param version:     string, the firmware version of image
        :param image_path:  string, the firmware image path
        :returns:           None
        """
        if self._need_update(version):
            if 'running_fw_ver' in self.dev_info:
                self.reset_device(raise_exception=True)
            self._burn_firmware(image_path)
        else:
            LOG.info('Firmware update is not required for Device.')


class NvidiaNic(object):
    """A class of nvidia nic contains pci, device ID,  device PSID and

    an instance of NvidiaNicFirmwareOps
    """

    def __init__(self, dev_pci, dev_id, dev_psid, dev_ops):
        self.dev_pci = dev_pci
        self.dev_id = dev_id
        self.dev_psid = dev_psid
        self.dev_ops = dev_ops


class NvidiaNics(object):
    """Discover and retrieve Nvidia Nics on the system.

    Can be used as an iterator once discover has been called.
    """

    def __init__(self):
        self._devs = []
        self._devs_psids = []
        self._dev_ids = []

    def discover(self):
        """Discover Nvidia Nics in the system.

        :returns:   None
        :raises:    processutils.ProcessExecutionError
        """
        if len(self._devs) > 0:
            return self._devs
        devs = []

        cmd = ('lspci', '-Dn', '-d', '15b3:')
        try:
            out, _r = utils.execute(*cmd)
        except processutils.ProcessExecutionError as e:
            LOG.error('Exception occurred while discovering Nvidia Nics %s',
                      e)
            raise e
        for line in out.strip().split('\n'):
            if not line:
                continue
            dev_class = line.split()[1].split(':')[0]
            if not re.match(NETWORK_DEVICE_REGEX, dev_class):
                continue
            dev_pci = line.split()[0]
            dev_id = line.split('15b3:')[1].split()[0]
            dev_ops = NvidiaNicFirmwareOps(dev_pci)
            dev_psid = dev_ops.get_nic_psid()
            self._dev_ids.append(dev_id)
            self._devs_psids.append(dev_psid)
            devs.append(NvidiaNic(dev_pci, dev_id, dev_psid, dev_ops))
        self._devs = devs

    def get_psids_list(self):
        """Get a list of PSIDs of Nvidia Nics in the system.

        :returns:   list of PSIDs of Nvidia Nics in the system
        """
        return set(self._devs_psids)

    def get_ids_list(self):
        """Get a list of IDs of Nvidia Nics in the system.

        :returns:   list of IDs of Nvidia Nics in the system
        """
        return set(self._dev_ids)

    def __iter__(self):
        return self._devs.__iter__()


class NvidiaNicFirmwareBinary(object):
    """A class of nvidia nic firmware binary which manages the binary

    firmware image, downloads it, validates it and provides its path on the
    system
    """

    def __init__(self, url, checksum, checksum_type,
                 component_flavor, version):
        self.url = url
        self.checksum = checksum
        self.checksum_type = checksum_type
        self.psid = component_flavor
        self.version = version
        self.image_info = {}
        self._process_url()
        self._validate_image_psid()
        self._validate_image_firmware_version()
        self._validate_image_checksum()

    def __del__(self):
        self._cleanup_file()

    def _cleanup_file(self):
        """Delete the temporary downloaded firmware image if exist in cleanup

        :returns:   None
        """
        if os.path.exists(os.path.dirname(self.dest_file_path)):
            try:
                shutil.rmtree(os.path.dirname(self.dest_file_path))
            except Exception as e:
                LOG.error('Failed to remove temporary directory for FW '
                          'binary: %s', e)

    def _download_file_based_fw(self):
        """Download the firmware image file from the provided file url (move)

        :returns:    None
        :raises:    Exception
        """
        src_file = self.parsed_url.path
        try:
            LOG.info('Moving file: %s to %s', self.url,
                     self.dest_file_path)
            shutil.move(src_file, self.dest_file_path)
        except Exception as e:
            LOG.error('Failed to move file: %s, %s', src_file, e)
            raise e

    def _download_http_based_fw(self):
        """Download the firmware image file from the provided url

        :returns:   None
        :raises:    urlError.HTTPError
        """
        try:
            LOG.info('Downloading file: %s to %s', self.url,
                     self.dest_file_path)
            url_data = request.urlopen(self.url)
        except urlError.URLError as url_error:
            LOG.error('Failed to open URL data: %s', url_error)
            raise url_error
        except urlError.HTTPError as http_error:
            LOG.error('Failed to download data: %s', http_error)
            raise http_error
        with open(self.dest_file_path, 'wb') as f:
            f.write(url_data.read())

    def _process_url(self):
        """Process the firmware url and download the image to a temporary

        destination in the system.
        The supported firmware URL schemes are (file://, http://)
        :returns:   None
        :raises:    InvalidURLScheme, for unsupported firmware url
        """
        parsed_url = urlparse(self.url)
        self.parsed_url = parsed_url
        file_name = os.path.basename(str(parsed_url.path))
        self.dest_file_path = os.path.join(tempfile.mkdtemp(
            prefix='nvidia_firmware'), file_name)
        url_scheme = parsed_url.scheme
        if url_scheme == 'file':
            self._download_file_based_fw()
        elif url_scheme == 'http':
            self._download_http_based_fw()
        else:
            err = 'Firmware URL scheme %s is not supported.' \
                  'The supported firmware URL schemes are' \
                  '(http://, file://)' % url_scheme
            raise InvalidURLScheme(error_msg=_(err))

    def _get_info(self):
        """Get firmware information from firmware binary image

        Caller should wrap this call under try catch to skip non compliant
        firmware binaries.
        :returns:   dict of firmware image attributes
        :raises:    processutils.ProcessExecutionError
        """
        if self.image_info:
            return self.image_info
        try:
            cmd = ('mstflint', '-i', self.dest_file_path, 'query')
            out, _r = utils.execute(*cmd)
        except processutils.ProcessExecutionError as e:
            LOG.error('Failed to query firmware image %s, %s',
                      self.dest_file_path, e)
            raise e
        self.image_info = NvidiaNicFirmwareOps.parse_mstflint_query_output(
            out)
        return self.image_info

    def _validate_image_psid(self):
        """Validate that the provided PSID same as the PSID in provided

        firmware image
        :raises:    MismatchComponentFlavor if they are not equal
        """

        image_psid = self._get_info().get('psid')
        if image_psid != self.psid:
            err = 'The provided psid %s does not match the image psid %s' % \
                  (self.psid, image_psid)
            LOG.error(err)
            raise MismatchComponentFlavor(error_msg=_(err))

    def _validate_image_firmware_version(self):
        """Validate that the provided firmware version same as the version

        in provided firmware image
        :raises:    MismatchFWVersion if they are not equal
        """

        image_version = self._get_info().get('fw_ver')
        if image_version != self.version:
            err = 'The provided firmware version %s does not match ' \
                  'image firmware version %s' % (self.version, image_version)
            LOG.error(err)
            raise MismatchFWVersion(error_msg=_(err))

    def _validate_image_checksum(self):
        """Validate the provided checksum with the calculated one of the

        provided firmware image
        :raises:    MismatchChecksumError if they are not equal
        """
        calculated_checksum = fileutils.compute_file_checksum(
            self.dest_file_path, algorithm=self.checksum_type)
        if self.checksum != calculated_checksum:
            err = 'Mismatch provided checksum %s for image %s' % (
                self.checksum, self.url)
            LOG.error(err)
            raise MismatchChecksumError(error_msg=_(err))


class NvidiaFirmwareImages(object):
    """A class of nvidia firmware images which manages the user provided

    firmware images list
    """

    def __init__(self, firmware_images):
        self.firmware_images = firmware_images
        self.filtered_images_psid_dict = {}

    def validate_images_schema(self):
        """Validate the provided firmware images list schema

        :raises:    InvalidFirmwareImageConfig if any param is missing
        """
        for image in self.firmware_images:
            if not (image.get('url')
                    and image.get('checksum')
                    and image.get('checksumType')
                    and image.get('componentFlavor')
                    and image.get('version')):
                err = 'Invalid parameters for image %s,' \
                      'please provide the following parameters ' \
                      'url, checksum, checksumType, componentFlavor, ' \
                      'version' % image
                LOG.error(err)
                raise InvalidFirmwareImageConfig(error_msg=_(err))

    def filter_images(self, psids_list):
        """Filter firmware images according to the system nics PSIDs,

        and create a map of PSIDs on the system and user provided images.
        Duplicate PSID is not allowed

        :param psids_list:  list of psids of machines nics
        :returns:           None
        :raises:            DuplicateComponentFlavor
        """
        for image in self.firmware_images:
            if image.get('componentFlavor') in psids_list:
                if self.filtered_images_psid_dict.get(
                        image.get('componentFlavor')):
                    err = 'Duplicate componentFlavor %s' % \
                          image['componentFlavor']
                    LOG.error(err)
                    raise DuplicateComponentFlavor(error_msg=_(err))
                else:
                    self.filtered_images_psid_dict[
                        image.get('componentFlavor')] = image
            else:
                LOG.debug('Image with component Flavor %s does not match '
                          'any nic in the system',
                          image.get('componentFlavor'))

    def apply_net_firmware_update(self, nvidia_nics):
        """Apply nic firmware update for all nvidia nics on the system

        which have mappings to the user provided firmware images
        :param nvidia_nics: an object of NvidiaNics
        """
        seen_nics = set()
        for nic in nvidia_nics:
            if self.filtered_images_psid_dict.get(nic.dev_psid):
                # pci_prefix is the pci address without the function number
                # we use it to check if we saw the nic before or not
                pci_prefix = nic.dev_pci[:-1]
                is_seen_nic = pci_prefix in seen_nics
                if not is_seen_nic:
                    seen_nics.add(pci_prefix)
                    fw_bin = NvidiaNicFirmwareBinary(
                        self.filtered_images_psid_dict[nic.dev_psid]['url'],
                        self.filtered_images_psid_dict[nic.dev_psid][
                            'checksum'],
                        self.filtered_images_psid_dict[nic.dev_psid][
                            'checksumType'],
                        self.filtered_images_psid_dict[nic.dev_psid][
                            'componentFlavor'],
                        self.filtered_images_psid_dict[nic.dev_psid][
                            'version'])
                    nic.dev_ops.fw_update_if_needed(
                        self.filtered_images_psid_dict[nic.dev_psid][
                            'version'],
                        fw_bin.dest_file_path)


class NvidiaNicConfig(object):
    """Get/Set Nvidia nics configurations"""

    def __init__(self, nvidia_dev, params):
        self.nvidia_dev = nvidia_dev
        self.params = params
        self._tool_confs = None
        self.device_conf_dict = {}

    def _mstconfig_parse_data(self, data):
        """Parsing the mstconfig out to json

        :param data:    mstconfig query output
        :returns:       dict of nic configuration
        """
        data = list(filter(None, data.split('\n')))
        data_dict = {}
        lines_counter = 0
        for line in data:
            lines_counter += 1
            if 'Configurations:' in line:
                break
        for i in range(lines_counter, len(data)):
            line_data = list(filter(None, data[i].strip().split()))
            data_dict[line_data[0]] = line_data[1]

        return data_dict

    def _get_device_conf_dict(self):
        """Get device Configurations

        :returns:   dict {"PARAM_NAME": "Param value", ....}
        :raises:    processutils.ProcessExecutionError
        """
        LOG.info('Getting configurations for device: %s',
                 self.nvidia_dev.dev_pci)
        if not self.device_conf_dict:
            try:
                cmd = ['mstconfig', '-d', self.nvidia_dev.dev_pci, 'q']
                out, _r = utils.execute(*cmd)
            except processutils.ProcessExecutionError as e:
                LOG.error('Failed to query firmware of device %s: %s',
                          self.nvidia_dev.dev_pci, e)
                raise e
            self.device_conf_dict = self._mstconfig_parse_data(out)
        return self.device_conf_dict

    def _param_supp_by_config_tool(self, param_name):
        """Check if configuration tool supports the provided configuration

        parameter.
        :param param_name:  string, configuration name
        :returns:           bool
        :raises:            processutils.ProcessExecutionError
        """
        if self._tool_confs is None:
            try:
                self._tool_confs, _r = utils.execute(
                    'mstconfig', '-d', self.nvidia_dev.dev_pci, 'i')
            except processutils.ProcessExecutionError as e:
                LOG.error('Failed to query tool configuration of device'
                          ' %s: %s', self.nvidia_dev.dev_pci, e)
                raise e
        # trim any array index if present
        indexed_param = re.match(ARRAY_PARAM_REGEX, param_name)
        if indexed_param:
            param_name = indexed_param.group('param_name')
        return param_name in self._tool_confs

    def _param_supp_by_fw(self, param_name):
        """Check if fw image supports the provided configuration

        parameter.
        :param param_name:  string, configuration name
        :returns:           bool
        :raises:            processutils.ProcessExecutionError
        """
        current_mlx_config = self._get_device_conf_dict()
        indexed_param = re.match(ARRAY_PARAM_REGEX, param_name)
        if indexed_param:
            param_name = indexed_param.group('param_name')
            if param_name not in current_mlx_config:
                return False
            indexed_value = re.match(ARRAY_PARAM_VALUE_REGEX,
                                     current_mlx_config[param_name])
            if not (indexed_value):
                return False
            value_first_index = int(indexed_value.group('first_index'))
            value_last_index = int(indexed_value.group('last_index'))
            param_index = indexed_param.group('index')
            if param_index:
                if int(param_index) in range(value_first_index,
                                             value_last_index):
                    return True
            else:
                param_first_index = int(indexed_param.group('first_index'))
                param_last_index = int(indexed_param.group('last_index'))
                if param_first_index in range(
                        value_first_index, value_last_index) \
                        and param_last_index in range(value_first_index,
                                                      value_last_index) \
                        and param_first_index < param_last_index:
                    return True
            return False
        else:
            return param_name in current_mlx_config

    def validate_config(self):
        """Validate that the firmware settings is supported by mstflint

        package and with current firmware image
        :returns: None
        :raises:    UnSupportedConfigByMstflintPackage
        :raises:    UnSupportedConfigByFW
        """
        LOG.info('Validating config for device %s',
                 self.nvidia_dev.dev_pci)
        for key, value in self.params.items():
            if not self._param_supp_by_config_tool(key):
                err = 'Configuraiton: %s is not supported by mstconfig, ' \
                      'please update to the latest mstflint package.' % key

                LOG.error(err)
                raise UnSupportedConfigByMstflintPackage(error_msg=_(err))

            if not self._param_supp_by_fw(key):
                err = 'Configuraiton %s for device %s is not supported with ' \
                      'current fw' % (key, self.nvidia_dev.dev_pci)
                LOG.error(err)
                raise UnSupportedConfigByFW(error_msg=_(err))

    def set_config(self):
        """Set device configurations

        :param conf_dict:   a dict of:
                            {'PARAM_NAME': 'Param value to set', ...}
        :returns:           None
        :raises:            processutils.ProcessExecutionError
        """
        LOG.info('Setting config for device %s', self.nvidia_dev.dev_pci)
        current_mlx_config = self._get_device_conf_dict()
        params_to_set = []
        for key, value in self.params.items():
            if re.match(ARRAY_PARAM_REGEX, key):
                params_to_set.append('%s=%s' % (key, value))
            else:
                try:
                    # Handle integer values
                    if int(value) != int(current_mlx_config.get(key)):
                        # Aggregate all configurations required to be modified
                        params_to_set.append('%s=%s' % (key, value))
                    else:
                        LOG.info('value of %s for device %s is already '
                                 'configured as %s no need to update it',
                                 key, self.nvidia_dev.dev_pci, value)
                except ValueError:
                    # Handle other values
                    # E.G:
                    # SRIOV_EN                            False(0)
                    # LINK_TYPE_P1                        ETH(2)
                    if str(value).lower() not in \
                            str(current_mlx_config.get(key)).lower():
                        # Aggregate all configurations required to be modified
                        params_to_set.append('%s=%s' % (key, value))
                    else:
                        LOG.info('value of %s for device %s  is already '
                                 'configured as %s, no need to update it',
                                 key, self.nvidia_dev.dev_pci, value)
        if len(params_to_set) > 0:
            try:
                cmd = ['mstconfig', '-d', self.nvidia_dev.dev_pci, '-y', 'set']
                cmd.extend(params_to_set)
                LOG.info('Setting configurations for device: %s',
                         )
                utils.execute(*cmd)
                LOG.info('Set device configurations: Setting %s '
                         'done successfully',
                         ' '.join(params_to_set))
            except processutils.ProcessExecutionError as e:
                LOG.error('Failed to set configuration of device %s, '
                          ' %s: %s', self.nvidia_dev.dev_pci,
                          params_to_set, e)
                raise e

        else:
            LOG.info('Set device configurations: No operation required')


class NvidiaNicsConfig(object):
    """A class of nvidia nics config which manages the user provided

     nics firmware settings
     """

    def __init__(self, nvidia_nics, settings):
        self.settings = settings
        self.nvidia_nics = nvidia_nics
        self.settings_map = {}
        self._nvidia_nics_to_be_reset_list = []
        self._nvidia_nics_config_list = []

    def create_settings_map(self):
        """Filter the user provided nics firmware settings according

        to the system nics IDs, and create a map of IDs on the system and
        user provided nics firmware settings.
        Duplicate IDs  and settings without IDs are not allowed
        :returns:   None
        :raises:    DuplicateDeviceID
        :raises:    InvalidFirmwareSettingsConfig
        """
        ids_list = self.nvidia_nics.get_ids_list()
        for setting in self.settings:
            if (setting.get('deviceID')
                    and setting.get('deviceID') in ids_list
                    and not self.settings_map.get(setting.get('deviceID'))):
                self.settings_map[setting.get('deviceID')] = setting
            elif setting.get('deviceID') and setting.get('deviceID') in \
                    ids_list:
                err = 'duplicate settings for device ID %s ' % \
                      setting.get('deviceID')
                LOG.error(err)
                raise DuplicateDeviceID(error_msg=_(err))
            elif setting.get('deviceID'):
                LOG.debug('There are no devices with ID %s on the system',
                          setting.get('deviceID'))
            else:
                err = 'There is no deviceID provided for this settings'
                LOG.error(err)
                raise InvalidFirmwareSettingsConfig(error_msg=_(err))

    def prepare_nvidia_nic_config(self):
        """Expand the settings map per devices PCI and create a list

        of all NvidiaNicConfig per PCI of nvidia nics on the system.
        Also create a list of all devices that require firmware reset
        :returns:   None
        """
        seen_nics = set()
        for nic in self.nvidia_nics:
            if self.settings_map.get(nic.dev_id):
                params = {}
                prefix = nic.dev_pci[:-1]
                is_seen_nic = prefix in seen_nics
                if not is_seen_nic:
                    seen_nics.add(prefix)
                    if self.settings_map[nic.dev_id].get('globalConfig'):
                        params.update(self.settings_map[nic.dev_id].get(
                            'globalConfig'))
                    if nic.dev_ops.is_image_changed():
                        self._nvidia_nics_to_be_reset_list.append(nic)
                is_first_device = nic.dev_pci[-1] == '0'
                if is_first_device and self.settings_map[nic.dev_id].get(
                        'function0Config'):
                    params.update(self.settings_map[nic.dev_id].get(
                        'function0Config'))
                elif not is_first_device and self.settings_map[nic.dev_id].get(
                        'function1Config'):
                    params.update(self.settings_map[nic.dev_id].get(
                        'function1Config'))
                if params:
                    device_config = NvidiaNicConfig(nic, params)
                    self._nvidia_nics_config_list.append(device_config)

    def reset_nvidia_nics(self):
        """Reset firmware image for all nics in _nvidia_nics_to_be_reset_list

        :returns:   None
        """
        for nvidia_nic in self._nvidia_nics_to_be_reset_list:
            nvidia_nic.dev_ops.reset_device()

    def validate_settings_config(self):
        """Validate firmware settings for all nics in _nvidia_nics_config_list

        :returns:   None
        """
        for nvidia_nic_config in self._nvidia_nics_config_list:
            nvidia_nic_config.validate_config()

    def set_settings_config(self):
        """Set firmware settings for all nics in _nvidia_nics_config_list

        :returns:    None
        """
        for nvidia_nic_config in self._nvidia_nics_config_list:
            nvidia_nic_config.set_config()

    def is_not_empty_reset_list(self):
        """Check if _nvidia_nics_to_be_reset_list is empty or not

        :returns:   bool, True if the list is not empty
        """
        return bool(len(self._nvidia_nics_to_be_reset_list))


def update_nvidia_nic_firmware_image(images):
    """Update nvidia nic firmware image from user provided list images

    :param images:     list of images
    :raises:    InvalidFirmwareImageConfig
    """
    if not type(images) is list:
        err = 'The images must be a list of images, %s' % images
        raise InvalidFirmwareImageConfig(error_msg=_(err))
    check_prereq()
    nvidia_fw_images = NvidiaFirmwareImages(images)
    nvidia_fw_images.validate_images_schema()
    nvidia_nics = NvidiaNics()
    nvidia_nics.discover()
    nvidia_fw_images.filter_images(nvidia_nics.get_psids_list())
    nvidia_fw_images.apply_net_firmware_update(nvidia_nics)


def update_nvidia_nic_firmware_settings(settings):
    """Update nvidia nic firmware settings from user provided list of settings

    :param settings:     list of settings
    :raises:    InvalidFirmwareSettingsConfig
    """
    if not type(settings) is list:
        err = 'The settings must be  list of settings, %s' % settings
        raise InvalidFirmwareSettingsConfig(error_msg=_(err))
    check_prereq()
    nvidia_nics = NvidiaNics()
    nvidia_nics.discover()
    nvidia_nics_config = NvidiaNicsConfig(nvidia_nics, settings)
    nvidia_nics_config.create_settings_map()
    nvidia_nics_config.prepare_nvidia_nic_config()
    if nvidia_nics_config.is_not_empty_reset_list():
        nvidia_nics_config.reset_nvidia_nics()
    nvidia_nics_config.validate_settings_config()
    nvidia_nics_config.set_settings_config()