#    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 copy
import sys
import traceback

from neutron_lib import context as n_context
from neutron_lib import rpc as n_rpc
import oslo_messaging as messaging

from gbpservice._i18n import _
from gbpservice.neutron.db import api as db_api
from gbpservice.nfp.common import constants as nfp_constants
from gbpservice.nfp.common import topics as nsf_topics
from gbpservice.nfp.common import utils as nfp_utils
from gbpservice.nfp.core import context as module_context
from gbpservice.nfp.core.event import Event
from gbpservice.nfp.core import log as nfp_logging
from gbpservice.nfp.core import module as nfp_api
from gbpservice.nfp.core.rpc import RpcAgent
from gbpservice.nfp.lib import nfp_context_manager as nfp_ctx_mgr
from gbpservice.nfp.lib import transport
from gbpservice.nfp.orchestrator.db import nfp_db as nfp_db
from gbpservice.nfp.orchestrator.drivers import orchestration_driver
from gbpservice.nfp.orchestrator.openstack import openstack_driver


LOG = nfp_logging.getLogger(__name__)

STOP_POLLING = {'poll': False}
CONTINUE_POLLING = {'poll': True}


def rpc_init(controller, config):
    rpcmgr = RpcHandler(config, controller)
    agent = RpcAgent(
        controller,
        host=config.host,
        topic=nsf_topics.NFP_CONFIGURATOR_NDO_TOPIC,
        manager=rpcmgr)
    controller.register_rpc_agents([agent])


def events_init(controller, config, device_orchestrator):
    events = ['CREATE_NETWORK_FUNCTION_DEVICE', 'DEVICE_SPAWNING',
              'DEVICE_HEALTHY', 'HEALTH_MONITOR_COMPLETE',
              'DEVICE_UP',
              'CONFIGURE_DEVICE', 'CREATE_DEVICE_CONFIGURATION',
              'CONFIGURATION_COMPLETE',
              'DEVICE_CONFIGURED', "DELETE_CONFIGURATION",
              'DELETE_NETWORK_FUNCTION_DEVICE',
              'DELETE_CONFIGURATION_COMPLETED',
              'DEVICE_BEING_DELETED',
              'DEVICE_NOT_REACHABLE',
              'DEVICE_CONFIGURATION_FAILED',
              'PLUG_INTERFACES', 'UNPLUG_INTERFACES',
              'UPDATE_DEVICE_CONFIG_PARAMETERS',
              'DEVICE_CONFIG_PARAMETERS_UPDATED',
              'PERIODIC_HM_DEVICE_REACHABLE',
              'PERIODIC_HM_DEVICE_NOT_REACHABLE',
              'PERFORM_INITIAL_HEALTH_CHECK',
              'PERFORM_PERIODIC_HEALTH_CHECK',
              'PERFORM_CLEAR_HM']
    events_to_register = []
    for event in events:
        events_to_register.append(
            Event(id=event, handler=device_orchestrator))
    controller.register_events(events_to_register)


def nfp_module_init(controller, config):
    events_init(controller, config, DeviceOrchestrator(controller, config))
    rpc_init(controller, config)
    LOG.debug("Device Orchestrator: module_init")


class RpcHandler(object):
    RPC_API_VERSION = '1.0'

    def __init__(self, conf, controller):
        super(RpcHandler, self).__init__()
        self.conf = conf
        self._controller = controller
        self.rpc_event_mapping = {
            nfp_constants.HEALTHMONITOR_RESOURCE: [
                'HEALTH_MONITOR_COMPLETE',
                'DEVICE_NOT_REACHABLE',
                'DEVICE_NOT_REACHABLE',
                'PERIODIC_HM_DEVICE_REACHABLE',
                'PERIODIC_HM_DEVICE_NOT_REACHABLE', ],
            nfp_constants.GENERIC_CONFIG: [
                'DEVICE_CONFIGURED',
                'DELETE_CONFIGURATION_COMPLETED',
                'DEVICE_CONFIGURATION_FAILED'],
        }

    def _log_event_created(self, event_id, event_data):
        NFD = event_data.get('network_function_device_id')
        NF = event_data.get('network_function_id')
        NFI = event_data.get('network_function_instance_id')

        if NFD and NF and NFI:
            LOG.info("Created event %(event_name)s with"
                     " NF:%(nf)s ,NFI:%(nfi)s and NFD:%(nfd)s",
                     {'event_name': event_id,
                      'nf': NF,
                      'nfi': NFI,
                      'nfd': NFD})
        else:
            LOG.info("Created event %(event_name)s ",
                     {'event_name': event_id})

    def _create_event(self, event_id, event_data=None, key=None,
                      is_poll_event=False, original_event=False, max_times=10):
        if is_poll_event:
            ev = self._controller.new_event(
                id=event_id, data=event_data,
                serialize=original_event.sequence,
                binding_key=original_event.binding_key,
                key=original_event.desc.uuid)
            LOG.debug("poll event started for %s", ev.id)
            self._controller.poll_event(ev, max_times=10)
        else:
            ev = self._controller.new_event(
                id=event_id,
                key=key,
                data=event_data)
            self._controller.post_event(ev)
        self._log_event_created(event_id, event_data)

    def handle_periodic_hm_resource(self, result):
        if result == nfp_constants.SUCCESS:
            event_id = self.rpc_event_mapping[
                nfp_constants.HEALTHMONITOR_RESOURCE][3]
        else:
            event_id = self.rpc_event_mapping[
                nfp_constants.HEALTHMONITOR_RESOURCE][4]
        return event_id

    # RPC APIs status notification from Configurator
    def network_function_notification(self, context, notification_data):
        nfp_context = module_context.init()
        info = notification_data.get('info')
        responses = notification_data.get('notification')
        request_info = info.get('context')
        operation = request_info.get('operation')
        logging_context = request_info.get('logging_context', {})
        # nfp_context = request_info.get('nfp_context')
        nfp_context['log_context'] = logging_context
        if 'nfp_context' in request_info:
            nfp_context.update(request_info['nfp_context'])

        for response in responses:
            resource = response.get('resource')
            data = response.get('data')
            result = data.get('status_code')
            if resource not in [nfp_constants.HEALTHMONITOR_RESOURCE,
                                nfp_constants.PERIODIC_HM]:
                resource = nfp_constants.GENERIC_CONFIG

            is_delete_request = True if operation == 'delete' else False

            if resource == nfp_constants.PERIODIC_HM:
                event_id = self.handle_periodic_hm_resource(result)
                break

            if is_delete_request:
                event_id = self.rpc_event_mapping[resource][1]
            else:
                event_id = self.rpc_event_mapping[resource][0]

            if result.lower() != 'success':
                LOG.info("RPC Handler response data:%(data)s",
                         {'data': data})
                if is_delete_request:
                    # Ignore any deletion errors, generate SUCCESS event
                    event_id = self.rpc_event_mapping[resource][1]
                else:
                    event_id = self.rpc_event_mapping[resource][2]
                break

        nf_id = request_info.pop('nf_id')
        nfi_id = request_info.pop('nfi_id')
        nfd_id = request_info.pop('nfd_id')
        request_info['network_function_id'] = nf_id
        request_info['network_function_instance_id'] = nfi_id
        request_info['network_function_device_id'] = nfd_id
        event_data = request_info
        event_data['id'] = request_info['network_function_device_id']

        key = nf_id
        self._create_event(event_id=event_id,
                           event_data=event_data,
                           key=key)


class DeviceOrchestrator(nfp_api.NfpEventHandler):
    """device Orchestrator For Network Services

    This class handles the orchestration of Network Function Device lifecycle.
    It deals with physical service resources - Network Devices. This module
    interacts with Service Orchestrator and Configurator. Service Orchestrator
    sends device create/delete request, Device orchestrator sends/receieves
    RPC to/from configurator to create generic config. Device Orchestrator
    loads drivers specified in config file, and selects corresponding drivers
    based on service vendor.

    Workflow for create:
    1) Service Orchestarator calls Device Orcehstrator(NDO) for creating new
    device, create_network_function_device gets called in the context of
    event handler. This method checks with drivers for device sharing, if
    device sharing supported then request the driver to plug the
    interfaces(step-3) else request driver to create a new device and poll
    for its status(Here a DB entry is created with status as PENDING_CREATE).
    2) If the driver returns status as ACTIVE or ERROR, the poll event is
    stopped, if it returns any other status, the poll event is continued.
    2) In case of new device creation, once device become ACTIVE, NDO plug
    the interfaces.
    3) After plugging the interfaces NDO sends RPC call to configurator for
    creating generic config.
    4) Rpc Handler receives notification API from configurator, In case of
    success update DB with status as ACTIVE and create event DEVICE_CREATED
    for Service Orchestrator. In case of any error, create event
    DEVICE_CREATE_FAILED and update DB as ERROR.

    """

    def __init__(self, controller, config):
        self._controller = controller
        self.config = config
        self.nsf_db = nfp_db.NFPDbBase()
        self.gbpclient = openstack_driver.GBPClient(config)
        self.keystoneclient = openstack_driver.KeystoneClient(config)

        neutron_context = n_context.get_admin_context()
        self.configurator_rpc = NDOConfiguratorRpcApi(neutron_context,
                                                      self.config)

        self.status_map = {
            'INIT': 'Created Network Service Device with status INIT.',
            'PENDING_CREATE': '',
            'DEVICE_SPAWNING': ('Creating NFD, launched the new device, ' +
                                'polling on its status'),
            'DEVICE_UP': 'Device is UP/ACTIVE',
            'PERFORM_HEALTH_CHECK': 'perform health check of service vm',
            'HEALTH_CHECK_PENDING': ('Device health check is going on ' +
                                     ' through configurator'),
            'HEALTH_CHECK_COMPLETED': 'Health check succesfull for device',
            'INTERFACES_PLUGGED': 'Interfaces Plugging successfull',
            'PENDING_CONFIGURATION_CREATE': ('Started configuring device ' +
                                             'for routes, license, etc'),
            'DEVICE_READY': 'Device is ready to use',
            'ACTIVE': 'Device is Active.',
            'DEVICE_NOT_UP': 'Device not became UP/ACTIVE',
        }
        self.orchestration_driver = orchestration_driver.OrchestrationDriver(
            self.config)

    @property
    def db_session(self):
        return db_api.get_writer_session()

    def event_method_mapping(self, event_id):
        event_handler_mapping = {
            "CREATE_NETWORK_FUNCTION_DEVICE": (
                self.create_network_function_device),
            "PERFORM_INITIAL_HEALTH_CHECK": self.perform_initial_health_check,
            "PERFORM_PERIODIC_HEALTH_CHECK":
                self.perform_periodic_health_check,
            "PERFORM_CLEAR_HM": self.perform_clear_hm,
            "DEVICE_UP": self.device_up,
            "PLUG_INTERFACES": self.plug_interfaces_fast,
            "DEVICE_HEALTHY": self.plug_interfaces,
            "HEALTH_MONITOR_COMPLETE": self.health_monitor_complete,
            "CONFIGURE_DEVICE": self.configure_device,
            "CREATE_DEVICE_CONFIGURATION": self.create_device_configuration,
            "CONFIGURATION_COMPLETE": self.configuration_complete,
            "DEVICE_CONFIGURED": self.device_configuration_complete,

            "DELETE_NETWORK_FUNCTION_DEVICE": (
                self.delete_network_function_device),
            "DELETE_CONFIGURATION_COMPLETED": (
                self.delete_configuration_complete),
            "UNPLUG_INTERFACES": self.unplug_interfaces,
            "DELETE_DEVICE": self.delete_device,
            "DELETE_CONFIGURATION": self.delete_device_configuration,
            "DEVICE_NOT_REACHABLE": self.handle_device_not_reachable,
            "PERIODIC_HM_DEVICE_REACHABLE": (
                self.periodic_hm_handle_device_reachable),
            "PERIODIC_HM_DEVICE_NOT_REACHABLE": (
                self.periodic_hm_handle_device_not_reachable),
            "PLUG_INTERFACE_FAILED": self.handle_plug_interface_failed,
            "DEVICE_CONFIGURATION_FAILED": self.handle_device_config_failed,
            "DEVICE_ERROR": self.handle_device_create_error,
            "DEVICE_NOT_UP": self.handle_device_not_up,
            "DRIVER_ERROR": self.handle_driver_error,
            'UPDATE_DEVICE_CONFIG_PARAMETERS': self.update_config_params,
            'DEVICE_CONFIG_PARAMETERS_UPDATED': (
                self.device_configuration_updated)
        }
        if event_id not in event_handler_mapping:
            raise Exception(_("Invalid event ID"))
        else:
            return event_handler_mapping[event_id]

    def handle_event(self, event):
        try:
            event_data = event.context
            NFD = event_data.get('network_function_device_id')
            NF = event_data.get('network_function_id')
            NFI = event_data.get('network_function_instance_id')

            if NFD and NF and NFI:
                LOG.info("Received event %(event_name)s with "
                         "NF:%(nf)s ,NFI:%(nfi)s and NFD:%(nfd)s",
                         {'event_name': event.id,
                          'nf': NF,
                          'nfi': NFI,
                          'nfd': NFD})
            else:
                LOG.info("Received event %(event_name)s ",
                         {'event_name': event.id})
            event_handler = self.event_method_mapping(event.id)
            event_handler(event)
        except Exception as e:
            LOG.error("error in processing event: %(event_id)s for "
                      "event data %(event_data)s. error: %(error)s",
                      {'event_id': event.id, 'event_data': event.data,
                       'error': e})
            _, _, tb = sys.exc_info()
            traceback.print_tb(tb)
            raise e

    def handle_exception(self, event, exception):
        return ExceptionHandler.handle(self, event, exception)

    # Helper functions
    def _log_event_created(self, event_id, event_data):
        network_function_instance = event_data.get('network_function_instance')
        if network_function_instance:
            nf = network_function_instance.get('network_function_id')
            nfi = network_function_instance.get('id')
        else:
            nf = None
            nfi = None
        if nf and nfi:
            LOG.info("Created event %(event_name)s with NF:%(nf)s and "
                     "NFI:%(nfi)s ",
                     {'event_name': event_id,
                      'nf': nf,
                      'nfi': nfi})
        else:
            LOG.info("Created event %(event_name)s ",
                     {'event_name': event_id})

    def _create_event(self, event_id, event_data=None,
                      is_poll_event=False, original_event=False,
                      is_internal_event=False, max_times=10):
        if not is_internal_event:
            if is_poll_event:
                ev = self._controller.new_event(
                    id=event_id, data=event_data,
                    serialize=original_event.sequence,
                    binding_key=original_event.binding_key,
                    key=original_event.desc.uuid)
                LOG.debug("poll event started for %s", ev.id)
                self._controller.poll_event(ev, max_times=max_times)
            else:
                ev = self._controller.new_event(
                    id=event_id,
                    data=event_data)
                self._controller.post_event(ev)
            nfp_context = module_context.get()
            self._log_event_created(event_id, nfp_context)
        else:
            # Same module API, so calling corresponding function directly.
            nfp_context = module_context.get()
            event = self._controller.new_event(
                id=event_id,
                data=event_data,
                context=nfp_context)
            self.handle_event(event)

    def _release_cnfd_lock(self, device):
        nf_id = device['network_function_id']
        nfi_id = device['network_function_instance_id']
        ev = self._controller.new_event(
            id='CREATE_NETWORK_FUNCTION_DEVICE',
            data=device, key=nf_id + nfi_id)
        if device.get('binding_key'):
            ev.binding_key = device.get('binding_key')
            LOG.debug("Releasing tenant based lock for "
                      "CREATE_NETWORK_FUNCTION_DEVICE event with binding "
                      "key: %s", ev.binding_key)
        self._controller.event_complete(ev)

    def event_cancelled(self, ev, reason):
        LOG.info("Poll event %(event_id)s cancelled.",
                 {'event_id': ev.id})

        if ev.id == 'DEVICE_SPAWNING':
            LOG.info("Device is not up still after 10secs of launch")
            # create event DEVICE_NOT_UP
            device = self._prepare_failure_case_device_data(ev.data)
            self._create_event(event_id='DEVICE_NOT_UP',
                               event_data=device,
                               is_internal_event=True)
            self._update_network_function_device_db(device,
                                                    'DEVICE_NOT_UP')
        if ev.id == 'DEVICE_BEING_DELETED':
            LOG.info("Device is not deleted completely."
                     " Continuing further cleanup of resources."
                     " Possibly there could be stale port resources"
                     " on Compute")
            device = ev.data
            orchestration_driver = self._get_orchestration_driver(
                device['service_details']['service_vendor'])
            device_id = device['id']
            del device['id']
            nf_id = device['network_function_id']
            orchestration_driver.delete_network_function_device(device)
            self._delete_network_function_device_db(device_id, device)
            dnfd_event = (
                self._controller.new_event(id='DELETE_NETWORK_FUNCTION_DEVICE',
                                           key=nf_id,
                                           binding_key=nf_id,
                                           desc_dict=device.get(
                                               'event_desc')))
            self._controller.event_complete(dnfd_event, result='FAILED')

    def _update_device_status(self, device, state, status_desc=None):
        device['status'] = state
        if status_desc:
            device['status_description'] = status_desc
        else:
            device['status_description'] = self.status_map.get(state)

    def _get_port(self, port_id):
        with nfp_ctx_mgr.DbContextManager:
            return self.nsf_db.get_port_info(self.db_session, port_id)

    def _get_ports(self, port_ids):
        data_ports = []
        for port_id in port_ids:
            with nfp_ctx_mgr.DbContextManager:
                port_info = self.nsf_db.get_port_info(self.db_session,
                                                      port_id)
            data_ports.append(port_info)
            return data_ports

    def _create_network_function_device_db(self, device_info, state):

        self._update_device_status(device_info, state)
        # (ashu) driver should return device_id as vm_id
        device_id = device_info.pop('id')
        device_info['id'] = device_id
        device_info['reference_count'] = 0
        device_info['interfaces_in_use'] = 0
        with nfp_ctx_mgr.DbContextManager:
            device = self.nsf_db.create_network_function_device(
                self.db_session,
                device_info)
        return device

    def _update_network_function_device_db(self, device, state,
                                           status_desc=''):
        self._update_device_status(device, state, status_desc)
        updated_device = copy.deepcopy(device)
        updated_device.pop('reference_count', None)
        updated_device.pop('interfaces_in_use', None)
        with nfp_ctx_mgr.DbContextManager:
            self.nsf_db.update_network_function_device(self.db_session,
                                                       updated_device['id'],
                                                       updated_device)
        device.update(updated_device)

    def _delete_network_function_device_db(self, device_id, device):
        with nfp_ctx_mgr.DbContextManager:
            self.nsf_db.delete_network_function_device(self.db_session,
                                                       device_id)

    def _get_network_function_info(self, device_id):
        nfi_filters = {'network_function_device_id': [device_id]}
        with nfp_ctx_mgr.DbContextManager:
            network_function_instances = (
                self.nsf_db.get_network_function_instances(self.db_session,
                                                           nfi_filters))
            network_function_ids = [nf['network_function_id']
                                    for nf in network_function_instances]
            network_functions = (
                self.nsf_db.get_network_functions(
                    self.db_session,
                    {'id': network_function_ids}))
            return network_functions

    def _get_network_function_devices(self, filters=None):
        with nfp_ctx_mgr.DbContextManager:
            network_function_devices = (
                self.nsf_db.get_network_function_devices(self.db_session,
                                                         filters))
        for device in network_function_devices:
            mgmt_port_id = device.pop('mgmt_port_id')
            mgmt_port_id = self._get_port(mgmt_port_id)
            device['mgmt_port_id'] = mgmt_port_id

            network_functions = (
                self._get_network_function_info(device['id']))
            device['network_functions'] = network_functions
        return network_function_devices

    def _increment_device_ref_count(self, device):
        with nfp_ctx_mgr.DbContextManager:
            self.nsf_db.increment_network_function_device_count(
                self.db_session,
                device['id'],
                'reference_count')
        device['reference_count'] += 1

    def _decrement_device_ref_count(self, device):
        with nfp_ctx_mgr.DbContextManager:
            self.nsf_db.decrement_network_function_device_count(
                self.db_session,
                device['id'],
                'reference_count')
        device['reference_count'] -= 1

    def _increment_device_interface_count(self, device):
        with nfp_ctx_mgr.DbContextManager:
            self.nsf_db.increment_network_function_device_count(
                self.db_session,
                device['id'],
                'interfaces_in_use',
                len(device['ports']))

        device['interfaces_in_use'] += len(device['ports'])

    def _decrement_device_interface_count(self, device):
        with nfp_ctx_mgr.DbContextManager:
            self.nsf_db.decrement_network_function_device_count(
                self.db_session,
                device['id'],
                'interfaces_in_use',
                len(device['ports']))

        device['interfaces_in_use'] -= len(device['ports'])

    def _get_orchestration_driver(self, service_vendor):
        return self.orchestration_driver

    def _get_device_data(self, nfd_request):

        device_data = {}
        network_function = nfd_request.get('network_function')
        network_function_instance = nfd_request['network_function_instance']
        service_details = nfd_request['service_details']
        device_data['name'] = network_function_instance['name']
        device_data['share_existing_device'] = (
            nfd_request.get('share_existing_device'))
        device_data['management_network_info'] = (
            nfd_request.get('management_network_info'))

        if network_function:
            device_data['network_function_id'] = network_function['id']
            device_data['service_chain_id'] = (
                network_function['service_chain_id'])

        device_data['network_function_instance_id'] = (
            network_function_instance['id'])
        device_data['tenant_id'] = network_function_instance['tenant_id']

        nsi_port_info = []
        for port_id in network_function_instance.pop('port_info'):
            with nfp_ctx_mgr.DbContextManager:
                port_info = self.nsf_db.get_port_info(self.db_session,
                                                      port_id)
            nsi_port_info.append(port_info)

        device_data['ports'] = nsi_port_info

        device_data['service_details'] = service_details
        if nsi_port_info[0]['port_model'] == nfp_constants.GBP_PORT:
            device_data['service_details']['network_mode'] = (
                nfp_constants.GBP_MODE)
        else:
            device_data['service_details']['network_mode'] = (
                nfp_constants.NEUTRON_MODE)
        device_data['service_vendor'] = service_details['service_vendor']

        return device_data

    def _get_nsf_db_resource(self, resource_name, resource_id):
        db_method = getattr(self.nsf_db, 'get_' + resource_name)
        return db_method(self.db_session, resource_id)

    def _update_device_data(self, device, device_data):
        device.update(device_data)
        return device

    def _make_ports_dict(self, consumer, provider, port_type):

        t_ports = []
        for ptg in [consumer, provider]:
            if (port_type in list(ptg.keys())) and ptg[port_type]:
                t_ports.append({
                    'id': ptg[port_type].get('id'),
                    'port_classification': ptg.get(
                        'port_classification'),
                    'port_model': ptg.get('port_model')
                })
        return t_ports

    def _prepare_device_data_from_nfp_context(self, nfp_context):
        device_data = {}

        network_function = nfp_context['network_function']
        network_function_instance = nfp_context['network_function_instance']
        service_details = nfp_context['service_details']

        device_data['token'] = nfp_context[
            'resource_owner_context']['admin_token']
        device_data['admin_tenant_id'] = nfp_context[
            'resource_owner_context']['admin_tenant_id']
        device_data['name'] = network_function_instance['name']
        device_data['share_existing_device'] = nfp_context[
            'share_existing_device']

        management_network_info = {
            'id': nfp_context['management_ptg_id'],
            'port_model': nfp_constants.GBP_NETWORK
        }

        consumer = nfp_context['consumer']
        provider = nfp_context['provider']

        ports = self._make_ports_dict(nfp_context.get(
            'explicit_consumer', consumer), provider, 'pt')

        device_data['provider_name'] = provider['ptg']['name']
        device_data['management_network_info'] = management_network_info

        device_data['network_function_id'] = network_function['id']
        device_data['service_chain_id'] = network_function['service_chain_id']
        device_data[
            'network_function_instance_id'] = network_function_instance['id']
        device_data['tenant_id'] = network_function_instance['tenant_id']
        device_data['ports'] = ports
        device_data['service_details'] = service_details
        device_data['service_details']['network_mode'] = nfp_constants.GBP_MODE
        device_data['service_vendor'] = service_details['service_vendor']
        device_data['server_grp_id'] = nfp_context.get('server_grp_id')
        device_data['interfaces_to_attach'] = (
            nfp_context.get('interfaces_to_attach'))

        if nfp_context.get('files'):
            device_data['files'] = nfp_context['files']
        if nfp_context.get('user_data'):
            device_data['user_data'] = nfp_context['user_data']
        return device_data

    def _create_nfd_entry(self, nfp_context, driver_device_info,
                          device_data, service_details):
        nfp_context['provider_metadata'] = driver_device_info.get(
            'provider_metadata')
        # Update nfp_context management with newly created mgmt port
        management = nfp_context['management']
        management['port'] = driver_device_info[
            'mgmt_neutron_port_info']['neutron_port']
        management['port']['ip_address'] = management[
            'port']['fixed_ips'][0]['ip_address']
        management['subnet'] = driver_device_info[
            'mgmt_neutron_port_info']['neutron_subnet']

        # Update newly created device with required params
        device = self._update_device_data(driver_device_info, device_data)
        device['network_function_device_id'] = device['id']

        # check for any explicit interface and its type.
        for interface in nfp_context.get('explicit_interfaces', []):
            if interface['type'] == 'gateway':
                device['gateway_port'] = interface['port']

        name = '%s_%s_%s_%s' % (
            device['provider_name'],
            service_details['service_type'],
            nfp_context['resource_owner_context']['tenant_name'][:6],
            device['network_function_device_id'][:3])
        device['name'] = name
        # Create DB entry with status as DEVICE_SPAWNING
        network_function_device = (
            self._create_network_function_device_db(device,
                                                    'DEVICE_SPAWNING'))

        nfp_context['network_function_device'] = network_function_device
        return device

    def _update_nfp_context_with_ports(self, nfp_context, device):
        # REVISIT(mak) Wrong but nfp_db method needs in this format
        network_function_device = nfp_context['network_function_device']
        network_function_device['mgmt_port_id'] = device['mgmt_port_id']

    def _post_create_nfd_events(self, event, nfp_context, device):

        nfp_context['event_desc'] = event.desc.to_dict()
        # Updating nfi with nfd_id before device spawning
        # to stop orchestration to move further.
        nfi = {
            'network_function_device_id': device['id'],
        }
        with nfp_ctx_mgr.DbContextManager:
            nfi = self.nsf_db.update_network_function_instance(
                self.db_session,
                device['network_function_instance_id'], nfi)
        # This event is act as a dummy event for nfp,
        # for non-hotplug sharing it will be used
        self._create_event(event_id='DEVICE_CREATED',
                           event_data=device)

        self._create_event(event_id='DEVICE_SPAWNING',
                           event_data=nfp_context,
                           is_poll_event=True,
                           original_event=event,
                           max_times=nfp_constants.DEVICE_SPAWNING_MAXRETRY)

    # Create path
    def create_network_function_device(self, event):
        """ Returns device instance for a new service

        This method either returns existing device which could be reused for a
        new service or it creates new device instance
        """

        nfp_context = event.context
        nfd_request = self._prepare_failure_case_device_data(nfp_context)
        service_details = nfp_context['service_details']

        LOG.info("Received event CREATE NETWORK FUNCTION "
                 "DEVICE request.")

        orchestration_driver = self._get_orchestration_driver(
            service_details['service_vendor'])

        device_data = self._prepare_device_data_from_nfp_context(nfp_context)

        LOG.info("Creating new device:%(device)s",
                 {'device': nfd_request})
        device_data['volume_support'] = (
            self.config.device_orchestrator.volume_support)
        device_data['volume_size'] = (
            self.config.device_orchestrator.volume_size)
        device_data['explicit_interfaces'] = nfp_context.get(
            'explicit_interfaces', [])
        driver_device_info = (
            orchestration_driver.create_network_function_device(
                device_data))
        if not driver_device_info:
            LOG.info("Device creation failed")
            self._create_event(event_id='DEVICE_ERROR',
                               event_data=nfd_request,
                               is_internal_event=True)
            self._controller.event_complete(event)
            return None

        device = self._create_nfd_entry(nfp_context, driver_device_info,
                                        device_data, service_details)
        self._increment_device_ref_count(device)
        self._increment_device_interface_count(device)
        nfd_id = device.get('network_function_device_id',
                            '-') if device else '-'
        nfp_context['log_context']['nfd_id'] = nfd_id
        self._update_nfp_context_with_ports(nfp_context, driver_device_info)

        self._post_create_nfd_events(event, nfp_context, device)

    def _post_device_up_event_graph(self, nfp_context):
        nf_id = nfp_context['network_function']['id']
        nfi_id = nfp_context['network_function_instance']['id']
        du_event = self._controller.new_event(id="DEVICE_UP",
                                              key=nf_id + nfi_id)

        hc_event = self._controller.new_event(
            id="PERFORM_INITIAL_HEALTH_CHECK",
            key=nf_id + nfi_id)

        plug_int_event = self._controller.new_event(id="PLUG_INTERFACES",
                                                    key=nf_id + nfi_id)
        GRAPH = ({
            du_event: [hc_event, plug_int_event]})

        self._controller.post_graph(
            GRAPH, du_event, graph_str='HEALTH_MONITOR_GRAPH')

    @nfp_api.poll_event_desc(event='DEVICE_SPAWNING',
                             spacing=nfp_constants.DEVICE_SPAWNING_SPACING)
    def check_device_is_up(self, event):
        nfp_context = event.context

        service_details = nfp_context['service_details']
        network_function_device = nfp_context['network_function_device']
        token = nfp_context['resource_owner_context']['admin_token']
        tenant_id = nfp_context['resource_owner_context']['admin_tenant_id']

        device = {
            'token': token,
            'tenant_id': tenant_id,
            'id': network_function_device['id'],
            'service_details': service_details}

        orchestration_driver = self._get_orchestration_driver(
            service_details['service_vendor'])

        is_device_up = (
            orchestration_driver.get_network_function_device_status(device))

        if is_device_up == nfp_constants.ACTIVE:
            LOG.info("Device with NFD:%(id)s came up for "
                     "tenant:%(tenant)s ",
                     {'id': network_function_device['id'],
                      'tenant': tenant_id})
            self._post_device_up_event_graph(nfp_context)

            return STOP_POLLING
        elif is_device_up == nfp_constants.ERROR:
            # create event DEVICE_NOT_UP
            device = self._prepare_failure_case_device_data(nfp_context)
            self._create_event(event_id='DEVICE_NOT_UP',
                               event_data=device,
                               is_internal_event=True)
            self._update_network_function_device_db(device,
                                                    'DEVICE_NOT_UP')
            return STOP_POLLING
        else:
            # Continue polling until device status became ACTIVE/ERROR.
            return CONTINUE_POLLING

    def _post_configure_device_graph(self, nfp_context, serialize=False):
        nf_id = nfp_context['network_function']['id']
        nfi_id = nfp_context['network_function_instance']['id']
        sc_instance_id = nfp_context['service_chain_instance']['id']
        binding_key = nfp_context['service_details'][
            'service_vendor'].lower() + nf_id
        device_configure_event = self._controller.new_event(
            id='CREATE_DEVICE_CONFIGURATION',
            key=nf_id,
            serialize=serialize,
            binding_key=binding_key)
        check_heat_config = self._controller.new_event(
            id='SEND_USER_CONFIG',
            key=nf_id)
        user_config_event = self._controller.new_event(
            id='INITIATE_USER_CONFIG',
            key=nf_id,
            serialize=serialize,
            binding_key=binding_key)
        device_configured_event = self._controller.new_event(
            id='CONFIGURATION_COMPLETE',
            key=nf_id,
            serialize=serialize,
            binding_key=sc_instance_id)
        device_periodic_hm_event = self._controller.new_event(
            id='PERFORM_PERIODIC_HEALTH_CHECK',
            key=nf_id + nfi_id)

        # Start periodic health monitor after device configuration

        GRAPH = ({
            device_periodic_hm_event: [device_configured_event],
            device_configured_event: [device_configure_event,
                                      user_config_event],
            user_config_event: [check_heat_config]})

        self._controller.post_graph(GRAPH, device_periodic_hm_event,
                                    graph_str='DEVICE_CONFIGURATION_GRAPH')

    def device_up(self, event, serialize_config=False):
        nfp_context = event.context

        # Get the results of PLUG_INTERFACES & PERFORM_INITIAL_HEALTH_CHECK
        # events results.
        nf_id = nfp_context['network_function']['id']
        nfi_id = nfp_context['network_function_instance']['id']
        device = self._prepare_failure_case_device_data(nfp_context)
        # Get the results of PLUG_INTERFACES & PERFORM_INITIAL_HEALTH_CHECK
        # events results.
        results = event.result
        for result in results:
            if result.result.lower() != 'success':
                # Release CNFD Event lock
                self._release_cnfd_lock(device)
                self._create_event(event_id='DEVICE_CREATE_FAILED',
                                   event_data=device)
                return self._controller.event_complete(event, result='FAILED')

        network_function_device = nfp_context['network_function_device']

        nfd_id = '-'
        if network_function_device:
            nfd_id = network_function_device.get('id', '-')
        nfp_context['log_context']['nfd_id'] = nfd_id
        # Update NFI to ACTIVE State
        nfi = {
            'status': nfp_constants.ACTIVE}
        nfi = self.nsf_db.update_network_function_instance(
            self.db_session, nfi_id, nfi)
        self._update_network_function_device_db(
            network_function_device, nfp_constants.ACTIVE)

        LOG.info(
            "Configuration completed for device with NFD:%(device_id)s. "
            "Updated DB status to ACTIVE.",
            {'device_id': network_function_device['id']})
        LOG.debug("Device detail:%s",
                  network_function_device)
        # Release CNFD Event lock
        self._release_cnfd_lock(device)
        self._post_configure_device_graph(nfp_context,
                                          serialize=serialize_config)
        event.key = nf_id + nfi_id
        self._controller.event_complete(event)

    def prepare_health_check_device_info(self, event, periodicity):

        nfp_context = event.context

        service_details = nfp_context['service_details']
        network_function_device = nfp_context['network_function_device']
        network_function = nfp_context['network_function']
        network_function_instance = nfp_context['network_function_instance']
        mgmt_ip_address = nfp_context['management']['port']['ip_address']
        tenant_id = nfp_context['resource_owner_context']['admin_tenant_id']

        # The driver tells which protocol / port to monitor ??
        orchestration_driver = self._get_orchestration_driver(
            service_details['service_vendor'])
        nfp_context['event_desc'] = event.desc.to_dict()
        device = {
            'id': network_function_device['id'],
            'tenant_id': tenant_id,
            'mgmt_ip_address': mgmt_ip_address,
            'service_details': service_details,
            'network_function_id': network_function['id'],
            'periodicity': periodicity,
            'network_function_instance_id': network_function_instance['id'],
            'nfp_context': {'event_desc': nfp_context['event_desc'],
                            'id': event.id, 'key': event.key},
        }
        return device, orchestration_driver

    def perform_clear_hm(self, event):
        nfp_context = event.data
        network_function = nfp_context['network_function']
        service_details = nfp_context['service_details']
        orchestration_driver = self._get_orchestration_driver(
            service_details['service_vendor'])
        nfp_context['event_desc'] = event.desc.to_dict()
        device = {
            'id': nfp_context['network_function_device_id'],
            'tenant_id': nfp_context['tenant_id'],
            'mgmt_ip_address': nfp_context['mgmt_ip_address'],
            'service_details': service_details,
            'network_function_id': network_function['id'],
            'network_function_instance_id': nfp_context[
                'network_function_instance_id'],
            'nfp_context': {'event_desc': nfp_context['event_desc'],
                            'id': event.id, 'key': event.key},
        }
        clear_hm_req = (
            orchestration_driver.get_network_function_device_config(
                device, nfp_constants.HEALTHMONITOR_RESOURCE))
        if not clear_hm_req:
            self._controller.event_complete(event, result="FAILED")
            return None

        self.configurator_rpc.delete_network_function_device_config(
            device,
            clear_hm_req)
        LOG.debug("Clear HM RPC sent to configurator for device: "
                  "%s with parameters: %s", (
                      device['id'], clear_hm_req))
        self._controller.event_complete(event, result="SUCCESS")

    def perform_periodic_health_check(self, event):
        event_results = event.result
        for result in event_results:
            if result.result.lower() != "success":
                return self._controller.event_complete(event, result="FAILED")

        device, orchestration_driver = (
            self.prepare_health_check_device_info(event,
                                                  nfp_constants.FOREVER))
        hm_req = (
            orchestration_driver.get_network_function_device_config(
                device, nfp_constants.HEALTHMONITOR_RESOURCE))
        if not hm_req:
            self._controller.event_complete(event, result="FAILED")
            return None

        self.configurator_rpc.create_network_function_device_config(device,
                                                                    hm_req)
        LOG.debug("Health Check RPC sent to configurator for device: "
                  "%s with health check parameters: %s", (
                      device['id'], hm_req))
        self._controller.event_complete(event, result="SUCCESS")

    def perform_initial_health_check(self, event):
        device, orchestration_driver = (
            self.prepare_health_check_device_info(event,
                                                  nfp_constants.INITIAL))
        hm_req = (
            orchestration_driver.get_network_function_device_config(
                device, nfp_constants.HEALTHMONITOR_RESOURCE))
        if not hm_req:
            self._controller.event_complete(event, result="FAILED")
            return None
        self.configurator_rpc.create_network_function_device_config(device,
                                                                    hm_req)
        LOG.debug("Health Check RPC sent to configurator for device: "
                  "%s with health check parameters: %s", (
                      device['id'], hm_req))

    def _get_service_type(self, service_profile_id):
        with nfp_ctx_mgr.KeystoneContextManager as kcm:
            admin_token = kcm.retry(
                self.keystoneclient.get_admin_token, tries=3)
        with nfp_ctx_mgr.GBPContextManager as gcm:
            service_profile = gcm.retry(self.gbpclient.get_service_profile,
                                        admin_token, service_profile_id)
        return service_profile['service_type'].lower()

    def _prepare_device_data(self, device_info):
        network_function_id = device_info['network_function_id']
        network_function_device_id = device_info['network_function_device_id']
        network_function_instance_id = (
            device_info['network_function_instance_id'])

        network_function = self._get_nsf_db_resource(
            'network_function',
            network_function_id)
        network_function_device = self._get_nsf_db_resource(
            'network_function_device',
            network_function_device_id)
        network_function_instance = self._get_nsf_db_resource(
            'network_function_instance',
            network_function_instance_id)

        with nfp_ctx_mgr.KeystoneContextManager as kcm:
            admin_token = kcm.retry(
                self.keystoneclient.get_admin_token, tries=3)
        with nfp_ctx_mgr.GBPContextManager as gcm:
            service_profile = gcm.retry(
                self.gbpclient.get_service_profile,
                admin_token,
                network_function['service_profile_id'])
        service_details = transport.parse_service_flavor_string(
            service_profile['service_flavor'])

        device_info.update({
            'network_function_instance': network_function_instance})
        device_info.update({'id': network_function_device_id})
        service_details.update({'service_type': self._get_service_type(
            network_function['service_profile_id'])})
        device_info.update({'service_details': service_details})

        device = self._get_device_data(device_info)
        device = self._update_device_data(device, network_function_device)

        mgmt_port_id = network_function_device.pop('mgmt_port_id')
        mgmt_port_id = self._get_port(mgmt_port_id)
        device['mgmt_port_id'] = mgmt_port_id
        device['network_function_id'] = network_function_id

        return device

    def _prepare_device_data_fast(self, network_function_details):
        network_function = network_function_details['network_function']
        network_function_device = network_function_details[
            'network_function_device']
        admin_token = network_function_details['admin_token']
        service_profile = network_function_details['service_profile']
        service_details = network_function_details['service_details']
        service_details.update(
            {'service_type': service_profile['service_type']})
        device = self._get_device_data(network_function_details)
        device = self._update_device_data(device, network_function_device)
        mgmt_port_id = network_function_device.pop('mgmt_port_id')
        mgmt_port_id = self._get_port(mgmt_port_id)
        device['mgmt_port_id'] = mgmt_port_id
        device['network_function_id'] = network_function['id']
        device['network_function_device_id'] = (
            network_function_device['id'])
        device['token'] = admin_token
        device['tenant_id'] = (
            network_function_details['admin_tenant_id'])
        device['service_profile'] = service_profile
        return device

    def health_monitor_complete(self, event, result='SUCCESS'):
        nfp_context = event.data['nfp_context']
        # device = nfp_context['network_function_device']
        # network_function = nfp_context['network_function']

        # Invoke event_complete for original event which is
        # PERFORM_INITIAL_HEALTH_CHECK
        event_desc = nfp_context.pop('event_desc', None)
        nfp_context.pop('id', None)
        key = nfp_context.pop('key', None)
        self._controller.event_complete(event)
        new_event = self._controller.new_event(
            id="PERFORM_INITIAL_HEALTH_CHECK",
            key=key, desc_dict=event_desc)
        self._controller.event_complete(new_event, result=result)

    def plug_interfaces(self, event, is_event_call=True):
        if is_event_call:
            device_info = event.data
        else:
            device_info = event
        # Get event data, as configurator sends back only request_info, which
        # contains nf_id, nfi_id, nfd_id.
        device = self._prepare_device_data(device_info)
        self._update_network_function_device_db(device,
                                                'HEALTH_CHECK_COMPLETED')
        orchestration_driver = self._get_orchestration_driver(
            device['service_details']['service_vendor'])

        _ifaces_plugged_in = (
            orchestration_driver.plug_network_function_device_interfaces(
                device))
        if _ifaces_plugged_in:
            self._increment_device_interface_count(device)
            self._create_event(event_id='CONFIGURE_DEVICE',
                               event_data=device,
                               is_internal_event=True)
        else:
            self._create_event(event_id='DEVICE_CONFIGURATION_FAILED',
                               event_data=device,
                               is_internal_event=True)

    def plug_interfaces_fast(self, event):

        # In this case, the event will be
        # happening in parallel with HEALTHMONITORING,
        # so, we should not generate CONFIGURE_DEVICE & should not update
        # DB with HEALTH_CHECK_COMPLETED.

        nfp_context = event.context

        service_details = nfp_context['service_details']
        network_function_device = nfp_context['network_function_device']
        nf_id = network_function_device['id']
        nfi_id = nfp_context['network_function_instance']['id']
        token = nfp_context['resource_owner_context']['admin_token']
        tenant_id = nfp_context['resource_owner_context']['admin_tenant_id']

        consumer = nfp_context['consumer']
        provider = nfp_context['provider']

        event.key = nf_id + nfi_id

        orchestration_driver = self._get_orchestration_driver(
            service_details['service_vendor'])
        ports = self._make_ports_dict(
            nfp_context.get('explicit_consumer', consumer),
            provider, 'port')

        device = {
            'id': network_function_device['id'],
            'ports': ports,
            'service_details': service_details,
            'token': token,
            'tenant_id': tenant_id,
            'interfaces_in_use': network_function_device['interfaces_in_use'],
            'status': network_function_device['status'],
            'provider_metadata': nfp_context['provider_metadata'],
            'enable_port_security': nfp_context.get('enable_port_security')
        }

        _ifaces_plugged_in = (
            orchestration_driver.plug_network_function_device_interfaces(
                device))
        if _ifaces_plugged_in:
            # self._increment_device_interface_count(device)
            # [REVISIT(mak)] - Check how incremented ref count can be
            # updated in DB
            self._controller.event_complete(event, result="SUCCESS")
        else:
            self._create_event(event_id="PLUG_INTERFACE_FAILED",
                               is_internal_event=True)
            self._controller.event_complete(event, result="FAILED")

    def configure_device(self, event):
        device = event.data
        orchestration_driver = self._get_orchestration_driver(
            device['service_details']['service_vendor'])
        config_params = (
            orchestration_driver.get_network_function_device_config(
                device, nfp_constants.GENERIC_CONFIG))
        if not config_params:
            self._create_event(event_id='DRIVER_ERROR',
                               event_data=device,
                               is_internal_event=True)
            return None
        # Sends RPC to configurator to create generic config
        self.configurator_rpc.create_network_function_device_config(
            device, config_params)

    def create_device_configuration(self, event):
        nfp_context = event.context

        service_details = nfp_context['service_details']
        consumer = nfp_context['consumer']
        provider = nfp_context['provider']
        management = nfp_context['management']
        network_function = nfp_context['network_function']
        network_function_instance = nfp_context['network_function_instance']
        network_function_device = nfp_context['network_function_device']
        tenant_id = nfp_context['resource_owner_context']['admin_tenant_id']

        binding_key = service_details[
            'service_vendor'].lower() + network_function['id']

        orchestration_driver = self._get_orchestration_driver(
            service_details['service_vendor'])
        device = {
            'tenant_id': tenant_id,
            'mgmt_ip_address': management['port']['ip_address'],
            'mgmt_ip': network_function_device['mgmt_ip_address'],
            'provider_ip': provider['port']['ip_address'],
            'provider_cidr': provider['subnet']['cidr'],
            'provider_mac': provider['port']['mac_address'],
            'provider_gateway_ip': provider['subnet']['gateway_ip']}

        if consumer['port'] and consumer['subnet']:
            device.update({'consumer_ip': consumer['port']['ip_address'],
                           'consumer_cidr': consumer['subnet']['cidr'],
                           'consumer_mac': consumer['port']['mac_address'],
                           'consumer_gateway_ip': consumer[
                               'subnet']['gateway_ip']})

        nfp_context['event_desc'] = event.desc.to_dict()
        device.update({
            'id': network_function_device['id'],
            'mgmt_ip_address': network_function_device['mgmt_ip_address'],
            'service_details': service_details,
            'network_function_id': network_function['id'],
            'network_function_instance_id': network_function_instance['id'],
            'nfp_context': {
                'event_desc': nfp_context['event_desc'],
                'id': event.id, 'key': event.key,
                'network_function_device': network_function_device,
                'binding_key': binding_key}})

        config_params = (
            orchestration_driver.
            get_network_function_device_config(
                device, nfp_constants.GENERIC_CONFIG))

        if not config_params:
            self._create_event(event_id='DRIVER_ERROR',
                               event_data=device,
                               is_internal_event=True)
            self._controller.event_complete(event, result="FAILED")
            return None

        event_data = {'device': device, 'nfp_context': nfp_context,
                      'config_params': config_params}
        self._create_event(event_id='UPDATE_DEVICE_CONFIG_PARAMETERS',
                           event_data=event_data)

    def device_configuration_updated(self, event):
        nfp_context, config_params, device = (
            event.data['nfp_context'], event.data['config_params'],
            event.data['device'])
        # Set forward_route as False in resource_data for configurator to
        # handle routes differently, when vpn is in service chain
        if nfp_utils.is_vpn_in_service_chain(
                nfp_context['service_chain_specs']):
            for cfg in config_params['config']:
                cfg['resource_data']['forward_route'] = False
        else:
            for cfg in config_params['config']:
                cfg['resource_data']['forward_route'] = True
        # Sends RPC to configurator to create generic config
        self.configurator_rpc.create_network_function_device_config(
            device, config_params)
        self._controller.event_complete(event=event, result='SUCCESS')

    def configuration_complete(self, event):
        nfp_context = event.context
        nf_id = nfp_context['network_function']['id']
        event_results = event.result
        for result in event_results:
            if result.result.lower() != "success":
                device = self._prepare_failure_case_device_data(nfp_context)
                self._create_event(event_id='DEVICE_CREATE_FAILED',
                                   event_data=device)
                return self._controller.event_complete(event, result="FAILED")
        sc_event = self._controller.new_event(id="SERVICE_CONFIGURED",
                                              key=nf_id)
        self._controller.post_event(sc_event)
        self._controller.event_complete(event, result="SUCCESS")

    def device_configuration_complete(self, event, result='SUCCESS'):
        nfp_context = event.data['nfp_context']

        # Invoke event_complete for original event which is
        # CREATE_DEVICE_CONFIGURATION
        event_desc = nfp_context.pop('event_desc', None)
        key = nfp_context.pop('key', None)
        self._controller.event_complete(event)
        event = self._controller.new_event(id="CREATE_DEVICE_CONFIGURATION",
                                           key=key, desc_dict=event_desc)
        event.binding_key = nfp_context.pop('binding_key', None)
        self._controller.event_complete(event, result=result)

    def delete_network_function_device(self, event):
        network_function_details = event.context
        nfd = network_function_details['network_function_device']
        if not nfd:
            self._controller.event_complete(event, result="SUCCESS")
            return
        device = self._prepare_device_data_fast(network_function_details)
        LOG.info("Recieved DELETE NETWORK FUNCTION "
                 "DEVICE request ")
        device['event_desc'] = event.desc.to_dict()
        self._create_event(event_id='DELETE_CONFIGURATION',
                           event_data=device,
                           is_internal_event=True)

    def delete_device_configuration(self, event):
        device = event.data
        orchestration_driver = self._get_orchestration_driver(
            device['service_details']['service_vendor'])
        nfp_context = module_context.get()
        config_params = (
            orchestration_driver.get_network_function_device_config(
                device, nfp_constants.GENERIC_CONFIG, is_delete=True,
                devices_data=nfp_context))
        if not config_params:
            self._create_event(event_id='DRIVER_ERROR',
                               event_data=device,
                               is_internal_event=True)
            nf_id = device['network_function_id']
            dnfd_event = (
                self._controller.new_event(
                    id='DELETE_NETWORK_FUNCTION_DEVICE',
                    key=nf_id,
                    binding_key=nf_id,
                    desc_dict=device.get('event_desc')))
            self._controller.event_complete(dnfd_event, result='FAILED')
            # TODO(mak): If driver returns ERROR,
            # then we are not proceeding further
            # Stale vms will exist in this case.
            # Need to handle this case where
            # driver returned None So dont initiate configurator API but call
            # unplug_interfaces and device delete to delete vms.
            return None

        nf_data = {
            'service_chain_instance': nfp_context.get(
                'service_chain_instance'),
            'provider': nfp_context.get('provider'),
            'consumer': nfp_context.get('consumer')
        }
        device.update(nf_data)
        # Sends RPC call to configurator to delete generic config API
        self.configurator_rpc.delete_network_function_device_config(
            device, config_params)

    def unplug_interfaces(self, event):
        result = "SUCCESS"
        device = event.data
        self._decrement_device_ref_count(device)
        orchestration_driver = self._get_orchestration_driver(
            device['service_details']['service_vendor'])

        is_interface_unplugged = (
            orchestration_driver.unplug_network_function_device_interfaces(
                device))
        if is_interface_unplugged:
            mgmt_port_id = device['mgmt_port_id']
            self._decrement_device_interface_count(device)
            device['mgmt_port_id'] = mgmt_port_id
        else:
            result = "FAILED"
        self._create_event(event_id='DELETE_DEVICE',
                           event_data=device,
                           is_internal_event=True)
        self._controller.event_complete(event, result=result)

    def delete_configuration_complete(self, event):
        device = event.data['nfp_context']
        nfd_id = event.data['network_function_device_id']
        nf_id = event.data['network_function_id']
        unplug_interfaces = (
            self._controller.new_event(id='UNPLUG_INTERFACES',
                                       data=device,
                                       key=nf_id,
                                       binding_key=nfd_id,
                                       serialize=True))
        self._controller.post_event(unplug_interfaces)
        self._controller.event_complete(event)

    def delete_device(self, event):
        # Update status in DB, send DEVICE_DELETED event to NSO.
        device = event.data
        orchestration_driver = self._get_orchestration_driver(
            device['service_details']['service_vendor'])

        network_function = (
            self.nsf_db.get_network_function(
                self.db_session,
                device['network_function_id']))
        device['network_function'] = network_function
        chm_event = self._controller.new_event(
            id='PERFORM_CLEAR_HM',
            key=device['network_function_id'],
            data=device)
        self._controller.post_event(chm_event)

        orchestration_driver.delete_network_function_device(device)
        self._create_event(
            event_id='DEVICE_BEING_DELETED',
            event_data=device,
            is_poll_event=True,
            original_event=event,
            max_times=nfp_constants.DEVICE_BEING_DELETED_MAXRETRY)

    @nfp_api.poll_event_desc(
        event='DEVICE_BEING_DELETED',
        spacing=nfp_constants.DEVICE_BEING_DELETED_SPACING)
    def check_device_deleted(self, event):
        device = event.data
        orchestration_driver = self._get_orchestration_driver(
            device['service_details']['service_vendor'])
        status = orchestration_driver.get_network_function_device_status(
            device, ignore_failure=True)
        if not status:
            try:
                device_id = device['id']
                del device['id']
                orchestration_driver.delete_network_function_device(device)
                self._delete_network_function_device_db(device_id, device)
                if device.get('event_desc'):
                    nf_id = device['network_function_id']
                    dnfd_event = (
                        self._controller.new_event(
                            id='DELETE_NETWORK_FUNCTION_DEVICE',
                            key=nf_id,
                            binding_key=nf_id,
                            desc_dict=device['event_desc']))
                    self._controller.event_complete(
                        dnfd_event, result='SUCCESS')
                return STOP_POLLING
            except Exception as exc:
                device['id'] = device_id
                err = ("Exception - %s - in DEVICE_BEING_DELETED" % (exc))
                LOG.error(err)
                return CONTINUE_POLLING
        else:
            return CONTINUE_POLLING

    # Error Handling
    def handle_device_create_error(self, event):
        device = event.data
        LOG.error("Device creation failed, for device %(device)s",
                  {'device': device})
        device['network_function_device_id'] = device.get('id')
        self._create_event(event_id='DEVICE_CREATE_FAILED',
                           event_data=device)

    def handle_device_not_up(self, event):
        device = event.data
        self._release_cnfd_lock(device)
        status = nfp_constants.ERROR
        desc = 'Device not became ACTIVE'
        self._update_network_function_device_db(device, status, desc)
        device['network_function_device_id'] = device['id']
        self._create_event(event_id='DEVICE_CREATE_FAILED',
                           event_data=device)

    def _prepare_failure_case_device_data(self, nfp_context):
        network_function = nfp_context['network_function']
        network_function_instance = nfp_context['network_function_instance']
        device = {'network_function_id': network_function['id'],
                  'network_function_instance_id': network_function_instance[
            'id'], 'binding_key': nfp_context.get('binding_key')}
        network_function_device = nfp_context.get('network_function_device')
        if network_function_device:
            device.update(
                {'network_function_device_id': network_function_device['id']})
            device.update(network_function_device)
        return device

    def handle_plug_interface_failed(self, event):
        nfp_context = event.context
        device = self._prepare_failure_case_device_data(nfp_context)
        # self._release_cnfd_lock(device)
        status = nfp_context['network_function_device']['status']
        desc = "Failed to plug interfaces"
        self._update_network_function_device_db(device, status, desc)
        # self._create_event(event_id='DEVICE_CREATE_FAILED',
        #                    event_data=device)

    def handle_device_not_reachable(self, event):
        device = event.data
        status = nfp_constants.ERROR
        desc = 'Device not reachable, Health Check Failed'
        self._update_network_function_device_db(device, status, desc)
        device['network_function_device_id'] = device['id']
        # self._create_event(event_id='DEVICE_CREATE_FAILED',
        #                    event_data=device)
        self.health_monitor_complete(event, result='FAILED')

    def periodic_hm_handle_device_reachable(self, event):
        device = event.data
        status = nfp_constants.ACTIVE
        desc = 'Device is ACTIVE'
        self._update_network_function_device_db(device, status, desc)

    def periodic_hm_handle_device_not_reachable(self, event):
        device = event.data
        status = nfp_constants.ERROR
        desc = 'Device not reachable, Health Check Failed'
        self._update_network_function_device_db(device, status, desc)

    def handle_device_config_failed(self, event):
        # device = event.data
        nfp_context = event.data['nfp_context']

        device = nfp_context['network_function_device']
        status = device['status']
        desc = 'Configuring Device Failed.'
        self._update_network_function_device_db(device, status, desc)
        device['network_function_device_id'] = device['id']
        # self._create_event(event_id='DEVICE_CREATE_FAILED',
        #                    event_data=event.data)
        LOG.debug("Device create failed for device: %s, with "
                  "data: %s", (device['id'], device))
        self.device_configuration_complete(event, result='FAILED')

    def handle_interfaces_setup_failed(self, event):
        device = event.data
        status = nfp_constants.ERROR
        desc = 'Interfaces Plugging failed'
        self._update_network_function_device_db(device, status, desc)
        device['network_function_device_id'] = device['id']
        self._create_event(event_id='DEVICE_CREATE_FAILED',
                           event_data=device)
        LOG.debug("Interface Plugging failed for device: %s,"
                  "with config: %s", (device['id'], device))

    def handle_driver_error(self, event):
        device = event.data
        LOG.error("Exception occured in driver, driver returned None "
                  " for device %(device)s", {'device': device})
        status = nfp_constants.ERROR
        desc = 'Exception in driver, driver return None'
        self._update_network_function_device_db(device, status, desc)
        # device['network_function_device_id'] = device['id']
        # self._create_event(event_id='DEVICE_CREATE_FAILED',
        #                    event_data=device)

    def update_config_params(self, event):
        self._create_event(event_id='DEVICE_CONFIG_PARAMETERS_UPDATED',
                           event_data=event.data, is_internal_event=True)
        self._controller.event_complete(event=event, result='SUCCESS')


class NDOConfiguratorRpcApi(object):
    """Service Manager side of the Service Manager to Service agent RPC API"""
    API_VERSION = '1.0'
    target = messaging.Target(version=API_VERSION)

    def __init__(self, context, conf):
        super(NDOConfiguratorRpcApi, self).__init__()
        self.conf = conf
        self.context = context
        self.client = n_rpc.get_client(self.target)
        self.rpc_api = self.client.prepare(
            version=self.API_VERSION,
            topic=nsf_topics.NFP_NDO_CONFIGURATOR_TOPIC)

    def _get_request_info(self, device, operation):
        nfp_context = module_context.get()
        request_info = {
            'nf_id': device['network_function_id'],
            'nfi_id': (
                device['network_function_instance_id']),
            'nfd_id': device['id'],
            'requester': nfp_constants.DEVICE_ORCHESTRATOR,
            'operation': operation,
            'logging_context': nfp_context['log_context'],
            # So that notification callbacks can work on cached data
            # 'orig_nfp_context': device.get('orig_nfp_context'),
            'nfp_context': device.get('nfp_context', None),
            'service_profile': device.get('service_profile'),
            'service_vm_context': nfp_utils.get_service_vm_context(
                device['service_details']['service_vendor']),
        }
        nfd_ip = device.get('mgmt_ip_address')
        request_info.update({'device_ip': nfd_ip})
        return request_info

    def _update_params(self, device_data, config_params, operation):
        request_info = self._get_request_info(device_data, operation)
        if not config_params:
            return None
        config_params['info'] = {
            'service_type': device_data['service_details']['service_type'],
            'service_vendor': device_data['service_details']['service_vendor'],
            'context': request_info,
        }
        if device_data.get('service_feature'):
            config_params['info'].update(
                {'service_feature': device_data.get('service_feature')})
        if config_params.get('service_info'):
            config_params['info'].update(config_params.pop('service_info'))

    def create_network_function_device_config(self, device_data,
                                              config_params):
        self._update_params(device_data, config_params, operation='create')
        LOG.info("Sending create NFD config request to configurator "
                 "for NF:%(nf_id)s ",
                 {'nf_id': config_params['info']['context']['nf_id']})

        transport.send_request_to_configurator(self.conf,
                                               self.context,
                                               config_params,
                                               'CREATE',
                                               True)

    def delete_network_function_device_config(self, device_data,
                                              config_params):
        self._update_params(device_data, config_params, operation='delete')
        config_params['info']['context']['nfp_context'] = device_data
        LOG.info("Sending delete NFD config request to configurator ")

        transport.send_request_to_configurator(self.conf,
                                               self.context,
                                               config_params,
                                               'DELETE',
                                               True)


class ExceptionHandler(object):

    @staticmethod
    def event_method_mapping(event_id):
        event_handler_mapping = {
            "CREATE_NETWORK_FUNCTION_DEVICE": (
                ExceptionHandler.create_network_function_device),
            "DEVICE_SPAWNING": ExceptionHandler.device_spawning,
            "PERFORM_INITIAL_HEALTH_CHECK":
                ExceptionHandler.perform_initial_health_check,
            "DEVICE_UP": ExceptionHandler.device_up,
            "PLUG_INTERFACES": ExceptionHandler.plug_interfaces,
            "HEALTH_MONITOR_COMPLETE":
                ExceptionHandler.health_monitor_complete,
            "CREATE_DEVICE_CONFIGURATION":
                ExceptionHandler.create_device_configuration,
            "DEVICE_CONFIGURED":
                ExceptionHandler.device_configuration_complete,
            "CONFIGURATION_COMPLETE": ExceptionHandler.configuration_complete,
            "DELETE_NETWORK_FUNCTION_DEVICE": (
                ExceptionHandler.delete_network_function_device),
            "DELETE_CONFIGURATION":
                ExceptionHandler.delete_device_configuration,
            "DELETE_CONFIGURATION_COMPLETED": (
                ExceptionHandler.delete_configuration_complete),
            "UNPLUG_INTERFACES": ExceptionHandler.unplug_interfaces,
            "DELETE_DEVICE": ExceptionHandler.delete_device,
            "DEVICE_BEING_DELETED": ExceptionHandler.device_being_deleted,
            "PERIODIC_HM_DEVICE_NOT_REACHABLE": (
                ExceptionHandler.periodic_hm_handle_device_not_reachable),
            "DEVICE_NOT_REACHABLE": (
                ExceptionHandler.health_monitor_complete),
            "DEVICE_CONFIGURATION_FAILED": (
                ExceptionHandler.device_configuration_complete),
            "PERFORM_PERIODIC_HEALTH_CHECK": (
                ExceptionHandler.perform_periodic_health_check),
        }
        if event_id not in event_handler_mapping:
            raise Exception(_("Invalid event ID"))
        else:
            return event_handler_mapping[event_id]

    @staticmethod
    def handle(orchestrator, event, exception):
        exc_type, exc_value, exc_traceback = sys.exc_info()
        message = "Traceback: %s" % traceback.format_exception(
            exc_type, exc_value, exc_traceback)
        LOG.error(message)

        exception_handler = ExceptionHandler.event_method_mapping(event.id)
        return exception_handler(orchestrator, event, exception)

    @staticmethod
    def create_network_function_device(orchestrator, event, exception):
        nfp_context = event.context
        network_function = nfp_context['network_function']
        # [REVISIT: AKASH] Updating NF from device_orchestrator is wrong way
        # of doing, but still doing it, will correct it later
        orchestrator.nsf_db.update_network_function(
            orchestrator.db_session,
            network_function['id'],
            {'status': nfp_constants.ERROR})
        orchestrator._controller.event_complete(event, result='FAILED')

    @staticmethod
    def perform_initial_health_check(orchestrator, event, exception):
        orchestrator._controller.event_complete(event, result='FAILED')

    @staticmethod
    def device_up(orchestrator, event, exception):
        nfp_context = event.context
        network_function = nfp_context['network_function']
        device = orchestrator._prepare_failure_case_device_data(nfp_context)
        orchestrator._release_cnfd_lock(device)
        orchestrator.nsf_db.update_network_function(
            orchestrator.db_session,
            network_function['id'],
            {'status': nfp_constants.ERROR})
        orchestrator._controller.event_complete(event, result='FAILED')

    @staticmethod
    def plug_interfaces(orchestrator, event, exception):
        nfp_context = event.context
        device = orchestrator._prepare_failure_case_device_data(nfp_context)
        status = nfp_context['network_function_device']['status']
        desc = "Failed to plug interfaces"
        orchestrator._update_network_function_device_db(device, status, desc)
        orchestrator._controller.event_complete(event, result='FAILED')

    @staticmethod
    def health_monitor_complete(orchestrator, event, exception):
        nfp_context = event.data['nfp_context']
        event_desc = nfp_context.pop('event_desc', None)
        nfp_context.pop('id', None)
        key = nfp_context.pop('key', None)
        ev = orchestrator._controller.new_event(
            id="PERFORM_INITIAL_HEALTH_CHECK",
            key=key, desc_dict=event_desc)
        orchestrator._controller.event_complete(ev, result='FAILED')
        orchestrator._controller.event_complete(event, result='FAILED')

    @staticmethod
    def create_device_configuration(orchestrator, event, exception):
        orchestrator._controller.event_complete(event, result='FAILED')

    @staticmethod
    def device_configuration_complete(orchestrator, event, exception):
        nfp_context = event.data['nfp_context']
        event_desc = nfp_context.pop('event_desc')
        key = nfp_context.pop('key')
        ev = orchestrator._controller.new_event(
            id="CREATE_DEVICE_CONFIGURATION",
            key=key, desc_dict=event_desc)
        ev.binding_key = nfp_context.pop('binding_key')
        orchestrator._controller.event_complete(ev, result='FAILED')
        orchestrator._controller.event_complete(event, result='FAILED')

    @staticmethod
    def configuration_complete(orchestrator, event, exception):
        nfp_context = event.context
        network_function = nfp_context['network_function']
        orchestrator.nsf_db.update_network_function(
            orchestrator.db_session,
            network_function['id'],
            {'status': nfp_constants.ERROR})
        orchestrator._controller.event_complete(event, result='FAILED')

    @staticmethod
    def delete_network_function_device(orchestrator, event, exception):
        network_function_details = event.context
        device = network_function_details['network_function_device']
        status = nfp_constants.ERROR
        desc = 'Exception in driver, driver return None'
        orchestrator._update_network_function_device_db(device, status, desc)
        orchestrator._controller.event_complete(event, result='FAILED')

    @staticmethod
    def delete_device_configuration(orchestrator, event, exception):
        device = event.data
        status = nfp_constants.ERROR
        desc = 'Exception in driver, driver return None'
        orchestrator._update_network_function_device_db(device, status, desc)
        nf_id = device['network_function_id']
        dnfd_event = (
            orchestrator._controller.new_event(
                id='DELETE_NETWORK_FUNCTION_DEVICE',
                key=nf_id,
                binding_key=nf_id,
                desc_dict=device.get('event_desc')))
        orchestrator._controller.event_complete(dnfd_event, result='FAILED')
        orchestrator._controller.event_complete(event, result='FAILED')

    @staticmethod
    def delete_configuration_complete(orchestrator, event, exception):
        device = event.data
        status = nfp_constants.ERROR
        desc = 'Exception in driver, driver return None'
        orchestrator._update_network_function_device_db(device, status, desc)
        nf_id = device['network_function_id']
        dnfd_event = (
            orchestrator._controller.new_event(
                id='DELETE_NETWORK_FUNCTION_DEVICE',
                key=nf_id,
                binding_key=nf_id,
                desc_dict=device.get('event_desc')))
        orchestrator._controller.event_complete(dnfd_event, result='FAILED')
        orchestrator._controller.event_complete(event, result='FAILED')

    @staticmethod
    def unplug_interfaces(orchestrator, event, exception):
        device = event.data
        nf_id = device['network_function_id']
        dnfd_event = (
            orchestrator._controller.new_event(
                id='DELETE_NETWORK_FUNCTION_DEVICE',
                key=nf_id,
                binding_key=nf_id,
                desc_dict=device.get('event_desc')))
        orchestrator._controller.event_complete(dnfd_event, result='FAILED')
        orchestrator._controller.event_complete(event, result='FAILED')

    @staticmethod
    def delete_device(orchestrator, event, exception):
        device = event.data
        status = nfp_constants.ERROR
        desc = 'Exception in driver, driver return None'
        orchestrator._update_network_function_device_db(device, status, desc)
        nf_id = device['network_function_id']
        dnfd_event = (
            orchestrator._controller.new_event(
                id='DELETE_NETWORK_FUNCTION_DEVICE',
                key=nf_id,
                binding_key=nf_id,
                desc_dict=device.get('event_desc')))
        orchestrator._controller.event_complete(dnfd_event, result='FAILED')
        orchestrator._controller.event_complete(event, result='FAILED')

    @staticmethod
    def device_being_deleted(orchestrator, event, exception):
        return {'poll': True}

    @staticmethod
    def device_spawning(orchestrator, event, exception):
        nfp_context = event.context
        network_function = nfp_context['network_function']
        device = orchestrator._prepare_failure_case_device_data(nfp_context)
        status = nfp_constants.ERROR
        desc = 'Exception in driver, driver return None'
        orchestrator._update_network_function_device_db(device, status, desc)
        orchestrator._release_cnfd_lock(device)
        orchestrator.nsf_db.update_network_function(
            orchestrator.db_session,
            network_function['id'],
            {'status': nfp_constants.ERROR})
        orchestrator._controller.event_complete(event, result='FAILED')
        return {'poll': False}

    @staticmethod
    def periodic_hm_handle_device_not_reachable(orchestrator,
                                                event, exception):

        orchestrator._controller.event_complete(event, result='FAILED')

    @staticmethod
    def perform_periodic_health_check(orchestrator, event, exception):
        orchestrator._controller.event_complete(event, result='FAILED')
