# Copyright 2014: Mirantis Inc.
# 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.

from oslo_config import cfg
import requests

from rally.common import logging
from rally import exceptions
from rally.plugins.openstack import scenario
from rally.task import atomic
from rally.task import utils


LOG = logging.getLogger(__name__)


CONF = cfg.CONF


class HeatScenario(scenario.OpenStackScenario):
    """Base class for Heat scenarios with basic atomic actions."""

    @atomic.action_timer("heat.list_stacks")
    def _list_stacks(self):
        """Return user stack list."""

        return list(self.clients("heat").stacks.list())

    @atomic.action_timer("heat.create_stack")
    def _create_stack(self, template, parameters=None,
                      files=None, environment=None):
        """Create a new stack.

        :param template: template with stack description.
        :param parameters: template parameters used during stack creation
        :param files: additional files used in template
        :param environment: stack environment definition

        :returns: object of stack
        """
        stack_name = self.generate_random_name()
        kw = {
            "stack_name": stack_name,
            "disable_rollback": True,
            "parameters": parameters or {},
            "template": template,
            "files": files or {},
            "environment": environment or {}
        }

        # heat client returns body instead manager object, so we should
        # get manager object using stack_id
        stack_id = self.clients("heat").stacks.create(**kw)["stack"]["id"]
        stack = self.clients("heat").stacks.get(stack_id)

        self.sleep_between(CONF.openstack.heat_stack_create_prepoll_delay)

        stack = utils.wait_for_status(
            stack,
            ready_statuses=["CREATE_COMPLETE"],
            failure_statuses=["CREATE_FAILED", "ERROR"],
            update_resource=utils.get_from_manager(),
            timeout=CONF.openstack.heat_stack_create_timeout,
            check_interval=CONF.openstack.heat_stack_create_poll_interval)

        return stack

    @atomic.action_timer("heat.update_stack")
    def _update_stack(self, stack, template, parameters=None,
                      files=None, environment=None):
        """Update an existing stack

        :param stack: stack that need to be updated
        :param template: Updated template
        :param parameters: template parameters for stack update
        :param files: additional files used in template
        :param environment: stack environment definition

        :returns: object of updated stack
        """

        kw = {
            "stack_name": stack.stack_name,
            "disable_rollback": True,
            "parameters": parameters or {},
            "template": template,
            "files": files or {},
            "environment": environment or {}
        }
        self.clients("heat").stacks.update(stack.id, **kw)

        self.sleep_between(CONF.openstack.heat_stack_update_prepoll_delay)

        stack = utils.wait_for_status(
            stack,
            ready_statuses=["UPDATE_COMPLETE"],
            failure_statuses=["UPDATE_FAILED", "ERROR"],
            update_resource=utils.get_from_manager(),
            timeout=CONF.openstack.heat_stack_update_timeout,
            check_interval=CONF.openstack.heat_stack_update_poll_interval)
        return stack

    @atomic.action_timer("heat.check_stack")
    def _check_stack(self, stack):
        """Check given stack.

        Check the stack and stack resources.

        :param stack: stack that needs to be checked
        """
        self.clients("heat").actions.check(stack.id)
        utils.wait_for_status(
            stack,
            ready_statuses=["CHECK_COMPLETE"],
            failure_statuses=["CHECK_FAILED", "ERROR"],
            update_resource=utils.get_from_manager(["CHECK_FAILED"]),
            timeout=CONF.openstack.heat_stack_check_timeout,
            check_interval=CONF.openstack.heat_stack_check_poll_interval)

    @atomic.action_timer("heat.delete_stack")
    def _delete_stack(self, stack):
        """Delete given stack.

        Returns when the stack is actually deleted.

        :param stack: stack object
        """
        stack.delete()
        utils.wait_for_status(
            stack,
            ready_statuses=["DELETE_COMPLETE"],
            failure_statuses=["DELETE_FAILED", "ERROR"],
            check_deletion=True,
            update_resource=utils.get_from_manager(),
            timeout=CONF.openstack.heat_stack_delete_timeout,
            check_interval=CONF.openstack.heat_stack_delete_poll_interval)

    @atomic.action_timer("heat.suspend_stack")
    def _suspend_stack(self, stack):
        """Suspend given stack.

        :param stack: stack that needs to be suspended
        """

        self.clients("heat").actions.suspend(stack.id)
        utils.wait_for_status(
            stack,
            ready_statuses=["SUSPEND_COMPLETE"],
            failure_statuses=["SUSPEND_FAILED", "ERROR"],
            update_resource=utils.get_from_manager(),
            timeout=CONF.openstack.heat_stack_suspend_timeout,
            check_interval=CONF.openstack.heat_stack_suspend_poll_interval)

    @atomic.action_timer("heat.resume_stack")
    def _resume_stack(self, stack):
        """Resume given stack.

        :param stack: stack that needs to be resumed
        """

        self.clients("heat").actions.resume(stack.id)
        utils.wait_for_status(
            stack,
            ready_statuses=["RESUME_COMPLETE"],
            failure_statuses=["RESUME_FAILED", "ERROR"],
            update_resource=utils.get_from_manager(),
            timeout=CONF.openstack.heat_stack_resume_timeout,
            check_interval=CONF.openstack.heat_stack_resume_poll_interval)

    @atomic.action_timer("heat.snapshot_stack")
    def _snapshot_stack(self, stack):
        """Creates a snapshot for given stack.

        :param stack: stack that will be used as base for snapshot
        :returns: snapshot created for given stack
        """
        snapshot = self.clients("heat").stacks.snapshot(
            stack.id)
        utils.wait_for_status(
            stack,
            ready_statuses=["SNAPSHOT_COMPLETE"],
            failure_statuses=["SNAPSHOT_FAILED", "ERROR"],
            update_resource=utils.get_from_manager(),
            timeout=CONF.openstack.heat_stack_snapshot_timeout,
            check_interval=CONF.openstack.heat_stack_snapshot_poll_interval)
        return snapshot

    @atomic.action_timer("heat.restore_stack")
    def _restore_stack(self, stack, snapshot_id):
        """Restores stack from given snapshot.

        :param stack: stack that will be restored from snapshot
        :param snapshot_id: id of given snapshot
        """
        self.clients("heat").stacks.restore(stack.id, snapshot_id)
        utils.wait_for_status(
            stack,
            ready_statuses=["RESTORE_COMPLETE"],
            failure_statuses=["RESTORE_FAILED", "ERROR"],
            update_resource=utils.get_from_manager(),
            timeout=CONF.openstack.heat_stack_restore_timeout,
            check_interval=CONF.openstack.heat_stack_restore_poll_interval
        )

    @atomic.action_timer("heat.show_output")
    def _stack_show_output(self, stack, output_key):
        """Execute output_show for specified "output_key".

        This method uses new output API call.
        :param stack: stack with output_key output.
        :param output_key: The name of the output.
        """
        output = self.clients("heat").stacks.output_show(stack.id, output_key)
        return output

    @atomic.action_timer("heat.show_output_via_API")
    def _stack_show_output_via_API(self, stack, output_key):
        """Execute output_show for specified "output_key".

        This method uses old way for getting output value.
        It gets whole stack object and then finds necessary "output_key".
        :param stack: stack with output_key output.
        :param output_key: The name of the output.
        """
        # this code copy-pasted and adopted for rally from old client version
        # https://github.com/openstack/python-heatclient/blob/0.8.0/heatclient/
        # v1/shell.py#L682-L699
        stack = self.clients("heat").stacks.get(stack_id=stack.id)
        for output in stack.to_dict().get("outputs", []):
            if output["output_key"] == output_key:
                return output

    @atomic.action_timer("heat.list_output")
    def _stack_list_output(self, stack):
        """Execute output_list for specified "stack".

        This method uses new output API call.
        :param stack: stack to call output-list.
        """
        output_list = self.clients("heat").stacks.output_list(stack.id)
        return output_list

    @atomic.action_timer("heat.list_output_via_API")
    def _stack_list_output_via_API(self, stack):
        """Execute output_list for specified "stack".

        This method uses old way for getting output value.
        It gets whole stack object and then prints all outputs
        belongs this stack.
        :param stack: stack to call output-list.
        """
        # this code copy-pasted and adopted for rally from old client version
        # https://github.com/openstack/python-heatclient/blob/0.8.0/heatclient/
        # v1/shell.py#L649-L663
        stack = self.clients("heat").stacks.get(stack_id=stack.id)
        output_list = stack.to_dict()["outputs"]
        return output_list

    def _count_instances(self, stack):
        """Count instances in a Heat stack.

        :param stack: stack to count instances in.
        """
        return len([
            r for r in self.clients("heat").resources.list(stack.id,
                                                           nested_depth=1)
            if r.resource_type == "OS::Nova::Server"])

    def _scale_stack(self, stack, output_key, delta):
        """Scale a stack up or down.

        Calls the webhook given in the output value identified by
        'output_key', and waits for the stack size to change by
        'delta'.

        :param stack: stack to scale up or down
        :param output_key: The name of the output to get the URL from
        :param delta: The expected change in number of instances in
                      the stack (signed int)
        """
        num_instances = self._count_instances(stack)
        expected_instances = num_instances + delta
        LOG.debug("Scaling stack %s from %s to %s instances with %s"
                  % (stack.id, num_instances, expected_instances, output_key))
        with atomic.ActionTimer(self, "heat.scale_with_%s" % output_key):
            self._stack_webhook(stack, output_key)
            utils.wait_for(
                stack,
                is_ready=lambda s: (
                    self._count_instances(s) == expected_instances),
                failure_statuses=["UPDATE_FAILED", "ERROR"],
                update_resource=utils.get_from_manager(),
                timeout=CONF.openstack.heat_stack_scale_timeout,
                check_interval=CONF.openstack.heat_stack_scale_poll_interval)

    def _stack_webhook(self, stack, output_key):
        """POST to the URL given in the output value identified by output_key.

        This can be used to scale stacks up and down, for instance.

        :param stack: stack to call a webhook on
        :param output_key: The name of the output to get the URL from
        :raises InvalidConfigException: if the output key is not found
        """
        url = None
        for output in stack.outputs:
            if output["output_key"] == output_key:
                url = output["output_value"]
                break
        else:
            raise exceptions.InvalidConfigException(
                "No output key %(key)s found in stack %(id)s" %
                {"key": output_key, "id": stack.id})

        with atomic.ActionTimer(self, "heat.%s_webhook" % output_key):
            requests.post(url).raise_for_status()
