# Copyright 2016 Huawei Technologies Co.,LTD.
# All Rights Reserved.

#    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 base64
import gzip
import shutil
import tempfile
import traceback

from oslo_config import cfg
from oslo_log import log as logging
from oslo_utils import excutils
import six
import taskflow.engines
from taskflow.patterns import linear_flow

from mogan.common import exception
from mogan.common import flow_utils
from mogan.common.i18n import _
from mogan.common import utils
from mogan.engine import configdrive
from mogan.engine import metadata as server_metadata
from mogan import objects

LOG = logging.getLogger(__name__)

ACTION = 'server:create'
CONF = cfg.CONF


class OnFailureRescheduleTask(flow_utils.MoganTask):
    """Triggers a rescheduling request to be sent when reverting occurs.

    If rescheduling doesn't occur this task errors out the server.
    """

    def __init__(self, engine_rpcapi):
        requires = ['filter_properties', 'request_spec', 'server',
                    'requested_networks', 'user_data', 'injected_files',
                    'admin_password', 'key_pair', 'partitions', 'context']
        super(OnFailureRescheduleTask, self).__init__(addons=[ACTION],
                                                      requires=requires)
        self.engine_rpcapi = engine_rpcapi
        # These exception types will trigger the server to be set into error
        # status rather than being rescheduled.
        self.no_reschedule_exc_types = [
            # The server has been removed from the database, that can not
            # be fixed by rescheduling.
            exception.ServerNotFound,
            exception.ServerDeployAborted,
            exception.NetworkError,
        ]

    def execute(self, **kwargs):
        pass

    def _reschedule(self, context, cause, request_spec, filter_properties,
                    server, requested_networks, user_data, injected_files,
                    admin_password, key_pair, partitions):
        """Actions that happen during the rescheduling attempt occur here."""

        create_server = self.engine_rpcapi.schedule_and_create_servers

        if not filter_properties:
            filter_properties = {}
        if 'retry' not in filter_properties:
            filter_properties['retry'] = {}

        retry_info = filter_properties['retry']
        num_attempts = retry_info.get('num_attempts', 0)

        LOG.debug("Server %(server_id)s: re-scheduling %(method)s "
                  "attempt %(num)d due to %(reason)s",
                  {'server_id': server.uuid,
                   'method': utils.make_pretty_name(create_server),
                   'num': num_attempts,
                   'reason': cause.exception_str})

        if all(cause.exc_info):
            # Stringify to avoid circular ref problem in json serialization
            retry_info['exc'] = traceback.format_exception(*cause.exc_info)

        return create_server(context, [server], requested_networks,
                             user_data=user_data,
                             injected_files=injected_files,
                             admin_password=admin_password,
                             key_pair=key_pair,
                             partitions=partitions,
                             request_spec=request_spec,
                             filter_properties=filter_properties)

    def revert(self, context, result, flow_failures, server, **kwargs):
        # Clean up associated node
        server.node_uuid = None
        server.node = None
        server.save()

        # Check if we have a cause which can tell us not to reschedule and
        # set the server's status to error.
        for failure in flow_failures.values():
            if failure.check(*self.no_reschedule_exc_types):
                LOG.error("Server %s: create failed and no reschedule.",
                          server.uuid)
                return False

        cause = list(flow_failures.values())[0]
        try:
            self._reschedule(context, cause, server=server, **kwargs)
            return True
        except exception.MoganException:
            LOG.exception("Server %s: rescheduling failed",
                          server.uuid)

        return False


class BuildNetworkTask(flow_utils.MoganTask):
    """Build network for the server."""

    def __init__(self, manager):
        requires = ['server', 'requested_networks', 'context']
        super(BuildNetworkTask, self).__init__(addons=[ACTION],
                                               requires=requires)
        self.manager = manager

    def _build_networks(self, context, server, requested_networks):
        ports = self.manager.driver.get_portgroups_and_ports(server.node_uuid)
        if len(requested_networks) > len(ports):
            raise exception.InterfacePlugException(_(
                "Ironic node: %(id)s virtual to physical interface count"
                "  mismatch"
                " (Vif count: %(vif_count)d, Pif count: %(pif_count)d)")
                % {'id': server.node_uuid,
                   'vif_count': len(requested_networks),
                   'pif_count': len(ports)})

        nics_obj = objects.ServerNics(context)

        for vif in requested_networks:
            try:
                if vif.get('net_id'):
                    port = self.manager.network_api.create_port(
                        context, vif['net_id'], server.uuid)
                    preserve_on_delete = False
                elif vif.get('port_id'):
                    port = self.manager.network_api.show_port(
                        context, vif.get('port_id'))
                    self.manager.network_api.check_port_availability(port)
                    self.manager.network_api.bind_port(context,
                                                       port['id'],
                                                       server)
                    preserve_on_delete = True

                nic_dict = {'port_id': port['id'],
                            'network_id': port['network_id'],
                            'mac_address': port['mac_address'],
                            'fixed_ips': port['fixed_ips'],
                            'preserve_on_delete': preserve_on_delete,
                            'server_uuid': server.uuid}

                server_nic = objects.ServerNic(context, **nic_dict)
                nics_obj.objects.append(server_nic)

                self.manager.driver.plug_vif(server.node_uuid,
                                             port['id'])
                # Get updated VIF info
                port_dict = self.manager.network_api.show_port(
                    context, port.get('id'))

                # Update the real physical mac address from ironic.
                server_nic.mac_address = port_dict['mac_address']
            except Exception as e:
                # Set nics here, so we can clean up the
                # created networks during reverting.
                server.nics = nics_obj
                LOG.error("Server %(server)s: create or get network "
                          "failed. The reason is %(reason)s",
                          {"server": server.uuid, "reason": e})
                raise exception.NetworkError(_(
                    "Build network for server failed."))

        return nics_obj

    def execute(self, context, server, requested_networks):
        server_nics = self._build_networks(
            context,
            server,
            requested_networks)

        server.nics = server_nics
        server.save()

    def revert(self, context, result, flow_failures, server, **kwargs):
        # Check if we need to clean up networks.
        if server.nics:
            LOG.debug("Server %s: cleaning up node networks",
                      server.uuid)
            self.manager.destroy_networks(context, server)
            # Unset nics here as we have destroyed it.
            server.nics = None
            return True

        return False


class GenerateConfigDriveTask(flow_utils.MoganTask):
    """Generate ConfigDrive value the server."""

    def __init__(self):
        requires = ['server', 'user_data', 'injected_files', 'admin_password',
                    'key_pair', 'configdrive', 'context']
        super(GenerateConfigDriveTask, self).__init__(addons=[ACTION],
                                                      requires=requires)

    def _generate_configdrive(self, context, server, user_data=None,
                              files=None, admin_password=None, key_pair=None):
        """Generate a config drive."""
        extra_md = {}
        if admin_password:
            extra_md['admin_pass'] = admin_password

        i_meta = server_metadata.ServerMetadata(
            server, content=files, user_data=user_data, key_pair=key_pair,
            extra_md=extra_md)

        with tempfile.NamedTemporaryFile() as uncompressed:
            with configdrive.ConfigDriveBuilder(server_md=i_meta) as cdb:
                cdb.make_drive(uncompressed.name)

            with tempfile.NamedTemporaryFile() as compressed:
                # compress config drive
                with gzip.GzipFile(fileobj=compressed, mode='wb') as gzipped:
                    uncompressed.seek(0)
                    shutil.copyfileobj(uncompressed, gzipped)

                # base64 encode config drive
                compressed.seek(0)
                return base64.b64encode(compressed.read())

    def execute(self, context, server, user_data, injected_files,
                admin_password, key_pair, configdrive):
        try:
            configdrive['value'] = self._generate_configdrive(
                context, server, user_data=user_data, files=injected_files,
                admin_password=admin_password, key_pair=key_pair)
        except Exception as e:
            with excutils.save_and_reraise_exception():
                msg = ("Failed to build configdrive: %s" %
                       six.text_type(e))
                LOG.error(msg, server=server)

        LOG.info("Config drive for server %(server)s created.",
                 {'server': server.uuid})


class CreateServerTask(flow_utils.MoganTask):
    """Build and deploy the server."""

    def __init__(self, driver):
        requires = ['server', 'configdrive', 'context']
        super(CreateServerTask, self).__init__(addons=[ACTION],
                                               requires=requires)
        self.driver = driver

    def execute(self, context, server, configdrive, partitions):
        configdrive_value = configdrive.get('value')
        self.driver.spawn(context, server, configdrive_value, partitions)
        LOG.info('Successfully provisioned Ironic node %s',
                 server.node_uuid)

    def revert(self, context, result, flow_failures, server, **kwargs):
        LOG.debug("Server %s: destroy backend node", server.uuid)
        self.driver.destroy(context, server)
        return True


def get_flow(context, manager, server, requested_networks, user_data,
             injected_files, admin_password, key_pair, partitions,
             request_spec, filter_properties):

    """Constructs and returns the manager entrypoint flow

    This flow will do the following:

    1. Build networks for the server and set port id back to baremetal port
    2. Generate configdrive value for server.
    3. Do node deploy and handle errors.
    4. Reschedule if the tasks are on failure.
    """

    flow_name = ACTION.replace(":", "_") + "_manager"
    server_flow = linear_flow.Flow(flow_name)

    # This injects the initial starting flow values into the workflow so that
    # the dependency order of the tasks provides/requires can be correctly
    # determined.
    create_what = {
        'context': context,
        'filter_properties': filter_properties,
        'request_spec': request_spec,
        'server': server,
        'requested_networks': requested_networks,
        'user_data': user_data,
        'injected_files': injected_files,
        'admin_password': admin_password,
        'key_pair': key_pair,
        'partitions': partitions,
        'configdrive': {}
    }

    server_flow.add(OnFailureRescheduleTask(manager.engine_rpcapi),
                    BuildNetworkTask(manager),
                    GenerateConfigDriveTask(),
                    CreateServerTask(manager.driver))

    # Now load (but do not run) the flow using the provided initial data.
    return taskflow.engines.load(server_flow, store=create_what)
