# Copyright 2015 FUJITSU LIMITED
#
# 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.

"""
iRMC Power Driver using the Base Server Profile
"""
from ironic_lib import metrics_utils
from oslo_log import log as logging
from oslo_service import loopingcall
from oslo_utils import importutils

from ironic.common import exception
from ironic.common.i18n import _
from ironic.common import states
from ironic.conductor import task_manager
from ironic.conf import CONF
from ironic.drivers import base
from ironic.drivers.modules import ipmitool
from ironic.drivers.modules.irmc import boot as irmc_boot
from ironic.drivers.modules.irmc import common as irmc_common
from ironic.drivers.modules.redfish import power as redfish_power
from ironic.drivers.modules import snmp

scci = importutils.try_import('scciclient.irmc.scci')

LOG = logging.getLogger(__name__)

METRICS = metrics_utils.get_metrics_logger(__name__)

"""
SC2.mib: sc2srvCurrentBootStatus returns status of the current boot
"""
BOOT_STATUS_OID = "1.3.6.1.4.1.231.2.10.2.2.10.4.1.1.4.1"
BOOT_STATUS_VALUE = {
    'error': 0,
    'unknown': 1,
    'off': 2,
    'no-boot-cpu': 3,
    'self-test': 4,
    'setup': 5,
    'os-boot': 6,
    'diagnostic-boot': 7,
    'os-running': 8,
    'diagnostic-running': 9,
    'os-shutdown': 10,
    'diagnostic-shutdown': 11,
    'reset': 12
}
BOOT_STATUS = {v: k for k, v in BOOT_STATUS_VALUE.items()}

if scci:
    STATES_MAP = {states.POWER_OFF: scci.POWER_OFF,
                  states.POWER_ON: scci.POWER_ON,
                  states.REBOOT: scci.POWER_RESET,
                  states.SOFT_REBOOT: scci.POWER_SOFT_CYCLE,
                  states.SOFT_POWER_OFF: scci.POWER_SOFT_OFF}


def _is_expected_power_state(target_state, boot_status_value):
    """Predicate if target power state and boot status values match.

    :param target_state: Target power state.
    :param boot_status_value: SNMP BOOT_STATUS_VALUE.
    :returns: True if expected power state, otherwise Flase.
    """
    if (target_state == states.SOFT_POWER_OFF
        and boot_status_value in (BOOT_STATUS_VALUE['unknown'],
                                  BOOT_STATUS_VALUE['off'])):
        return True
    elif (target_state == states.SOFT_REBOOT
          and boot_status_value == BOOT_STATUS_VALUE['os-running']):
        return True

    return False


def _wait_power_state(task, target_state, timeout=None):
    """Wait for having changed to the target power state.

    :param task: A TaskManager instance containing the node to act on.
    :raises: IRMCOperationError if the target state acknowledge failed.
    :raises: SNMPFailure if SNMP request failed.
    """
    node = task.node
    d_info = irmc_common.parse_driver_info(node)
    snmp_client = snmp.SNMPClient(
        address=d_info['irmc_address'],
        port=d_info['irmc_snmp_port'],
        version=d_info['irmc_snmp_version'],
        read_community=d_info['irmc_snmp_community'],
        user=d_info.get('irmc_snmp_user'),
        auth_proto=d_info.get('irmc_snmp_auth_proto'),
        auth_key=d_info.get('irmc_snmp_auth_password'),
        priv_proto=d_info.get('irmc_snmp_priv_proto'),
        priv_key=d_info.get('irmc_snmp_priv_password'))

    interval = CONF.irmc.snmp_polling_interval
    retry_timeout_soft = timeout or CONF.conductor.soft_power_off_timeout
    max_retry = int(retry_timeout_soft / interval)

    def _wait(mutable):
        mutable['boot_status_value'] = snmp_client.get(BOOT_STATUS_OID)
        LOG.debug("iRMC SNMP agent of %(node_id)s returned "
                  "boot status value %(bootstatus)s on attempt %(times)s.",
                  {'node_id': node.uuid,
                   'bootstatus': BOOT_STATUS[mutable['boot_status_value']],
                   'times': mutable['times']})

        if _is_expected_power_state(target_state,
                                    mutable['boot_status_value']):
            mutable['state'] = target_state
            raise loopingcall.LoopingCallDone()

        mutable['times'] += 1
        if mutable['times'] > max_retry:
            mutable['state'] = states.ERROR
            raise loopingcall.LoopingCallDone()

    store = {'state': None, 'times': 0, 'boot_status_value': None}
    timer = loopingcall.FixedIntervalLoopingCall(_wait, store)
    timer.start(interval=interval).wait()

    if store['state'] == target_state:
        # iRMC acknowledged the target state
        node.last_error = None
        node.power_state = (states.POWER_OFF
                            if target_state == states.SOFT_POWER_OFF
                            else states.POWER_ON)
        node.target_power_state = states.NOSTATE
        node.save()
        LOG.info('iRMC successfully set node %(node_id)s '
                 'power state to %(bootstatus)s.',
                 {'node_id': node.uuid,
                  'bootstatus': BOOT_STATUS[store['boot_status_value']]})
    else:
        # iRMC failed to acknowledge the target state
        last_error = (_('iRMC returned unexpected boot status value %s') %
                      BOOT_STATUS[store['boot_status_value']])
        node.last_error = last_error
        node.power_state = states.ERROR
        node.target_power_state = states.NOSTATE
        node.save()
        LOG.error('iRMC failed to acknowledge the target state for node '
                  '%(node_id)s. Error: %(last_error)s',
                  {'node_id': node.uuid, 'last_error': last_error})
        error = _('unexpected boot status value')
        raise exception.IRMCOperationError(operation=target_state,
                                           error=error)


def _set_power_state(task, target_state, timeout=None):
    """Turn the server power on/off or do a reboot.

    :param task: a TaskManager instance containing the node to act on.
    :param target_state: target state of the node.
    :param timeout: timeout (in seconds) positive integer (> 0) for any
      power state. ``None`` indicates default timeout.
    :raises: InvalidParameterValue if an invalid power state was specified.
    :raises: MissingParameterValue if some mandatory information
      is missing on the node
    :raises: IRMCOperationError on an error from SCCI or SNMP
    """
    node = task.node
    irmc_client = irmc_common.get_irmc_client(node)

    if target_state in (states.POWER_ON, states.REBOOT, states.SOFT_REBOOT):
        irmc_boot.attach_boot_iso_if_needed(task)

    try:
        irmc_client(STATES_MAP[target_state])

    except KeyError:
        msg = _("_set_power_state called with invalid power state "
                "'%s'") % target_state
        raise exception.InvalidParameterValue(msg)

    except scci.SCCIClientError as irmc_exception:
        LOG.error("iRMC set_power_state failed to set state to %(tstate)s "
                  " for node %(node_id)s with error: %(error)s",
                  {'tstate': target_state, 'node_id': node.uuid,
                   'error': irmc_exception})
        operation = _('iRMC set_power_state')
        raise exception.IRMCOperationError(operation=operation,
                                           error=irmc_exception)

    try:
        if target_state in (states.SOFT_REBOOT, states.SOFT_POWER_OFF):
            # note (naohirot):
            # The following call covers both cases since SOFT_REBOOT matches
            # 'unknown' and SOFT_POWER_OFF matches 'off' or 'unknown'.
            _wait_power_state(task, states.SOFT_POWER_OFF, timeout=timeout)
        if target_state == states.SOFT_REBOOT:
            _wait_power_state(task, states.SOFT_REBOOT, timeout=timeout)

    except exception.SNMPFailure as snmp_exception:
        LOG.error("iRMC failed to acknowledge the target state "
                  "for node %(node_id)s. Error: %(error)s",
                  {'node_id': node.uuid, 'error': snmp_exception})
        raise exception.IRMCOperationError(operation=target_state,
                                           error=snmp_exception)


class IRMCPower(redfish_power.RedfishPower, base.PowerInterface):
    """Interface for power-related actions."""

    def get_properties(self):
        """Return the properties of the interface.

        :returns: dictionary of <property name>:<property description> entries.
        """
        return irmc_common.COMMON_PROPERTIES

    @METRICS.timer('IRMCPower.validate')
    def validate(self, task):
        """Validate the driver-specific Node power info.

        This method validates whether the 'driver_info' property of the
        supplied node contains the required information for this driver to
        manage the power state of the node.

        :param task: a TaskManager instance containing the node to act on.
        :raises: InvalidParameterValue if required driver_info attribute
                 is missing or invalid on the node.
        :raises: MissingParameterValue if a required parameter is missing.
        """
        # validate method of power interface is called at very first point
        # in verifying.
        # We take try-fallback approach against iRMC S6 2.00 and later
        # incompatibility in which iRMC firmware disables IPMI by default.
        # get_power_state method first try IPMI and if fails try Redfish
        # along with setting irmc_ipmi_succeed flag to indicate if IPMI works.
        if (task.node.driver_internal_info.get('irmc_ipmi_succeed')
            or (task.node.driver_internal_info.get('irmc_ipmi_succeed')
            is None)):
            irmc_common.parse_driver_info(task.node)
        else:
            irmc_common.parse_driver_info(task.node)
            super(IRMCPower, self).validate(task)

    @METRICS.timer('IRMCPower.get_power_state')
    def get_power_state(self, task):
        """Return the power state of the task's node.

        :param task: a TaskManager instance containing the node to act on.
        :returns: a power state. One of :mod:`ironic.common.states`.
        :raises: InvalidParameterValue if required parameters are incorrect.
        :raises: MissingParameterValue if required parameters are missing.
        :raises: IRMCOperationError If IPMI or Redfish operation fails
        """
        # If IPMI operation failed, iRMC may not enable/support IPMI,
        # so fallback to Redfish.
        # get_power_state is called at verifying and is called periodically
        # so this method is good choice to determine IPMI enablement.
        try:
            irmc_common.update_ipmi_properties(task)
            ipmi_power = ipmitool.IPMIPower()
            pw_state = ipmi_power.get_power_state(task)
            if (task.node.driver_internal_info.get('irmc_ipmi_succeed')
                is not True):
                task.upgrade_lock(purpose='update irmc_ipmi_succeed flag',
                                  retry=True)
                task.node.set_driver_internal_info('irmc_ipmi_succeed', True)
                task.node.save()
                task.downgrade_lock()
            return pw_state
        except exception.IPMIFailure:
            if (task.node.driver_internal_info.get('irmc_ipmi_succeed')
                is not False):
                task.upgrade_lock(purpose='update irmc_ipmi_succeed flag',
                                  retry=True)
                task.node.set_driver_internal_info('irmc_ipmi_succeed', False)
                task.node.save()
                task.downgrade_lock()
            try:
                return super(IRMCPower, self).get_power_state(task)
            except (exception.RedfishConnectionError,
                    exception.RedfishError):
                raise exception.IRMCOperationError(
                    operation='IPMI try and Redfish fallback operation')

    @METRICS.timer('IRMCPower.set_power_state')
    @task_manager.require_exclusive_lock
    def set_power_state(self, task, power_state, timeout=None):
        """Set the power state of the task's node.

        :param task: a TaskManager instance containing the node to act on.
        :param power_state: Any power state from :mod:`ironic.common.states`.
        :param timeout: timeout (in seconds) positive integer (> 0) for any
          power state. ``None`` indicates default timeout.
        :raises: InvalidParameterValue if an invalid power state was specified.
        :raises: MissingParameterValue if some mandatory information
          is missing on the node
        :raises: IRMCOperationError if failed to set the power state.
        """
        _set_power_state(task, power_state, timeout=timeout)

    @METRICS.timer('IRMCPower.reboot')
    @task_manager.require_exclusive_lock
    def reboot(self, task, timeout=None):
        """Perform a hard reboot of the task's node.

        :param task: a TaskManager instance containing the node to act on.
        :param timeout: timeout (in seconds) positive integer (> 0) for any
          power state. ``None`` indicates default timeout.
        :raises: InvalidParameterValue if an invalid power state was specified.
        :raises: IRMCOperationError if failed to set the power state.
        """
        current_pstate = self.get_power_state(task)
        if current_pstate == states.POWER_ON:
            _set_power_state(task, states.REBOOT, timeout=timeout)
        elif current_pstate == states.POWER_OFF:
            _set_power_state(task, states.POWER_ON, timeout=timeout)

    @METRICS.timer('IRMCPower.get_supported_power_states')
    def get_supported_power_states(self, task):
        """Get a list of the supported power states.

        :param task: A TaskManager instance containing the node to act on.
            currently not used.
        :returns: A list with the supported power states defined
                  in :mod:`ironic.common.states`.
        """
        return [states.POWER_ON, states.POWER_OFF, states.REBOOT,
                states.SOFT_REBOOT, states.SOFT_POWER_OFF]
