fda172a4ce
Change-Id: I99a6f09bd5e4385f9a7710f374a1eb41bad28c74
329 lines
12 KiB
Python
329 lines
12 KiB
Python
# 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 rally.common import cfg
|
|
from rally.common import logging
|
|
from rally import exceptions
|
|
from rally.task import atomic
|
|
from rally.task import utils
|
|
import requests
|
|
|
|
from rally_openstack import scenario
|
|
|
|
|
|
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()
|