aboutsummaryrefslogtreecommitdiffstats
path: root/yardstick
diff options
context:
space:
mode:
Diffstat (limited to 'yardstick')
-rw-r--r--yardstick/benchmark/contexts/heat.py77
-rw-r--r--yardstick/benchmark/contexts/model.py2
-rw-r--r--yardstick/benchmark/core/task.py3
-rw-r--r--yardstick/benchmark/scenarios/networking/vnf_generic.py130
-rw-r--r--yardstick/network_services/vnf_generic/vnfdgen.py18
-rw-r--r--yardstick/orchestrator/heat.py122
6 files changed, 267 insertions, 85 deletions
diff --git a/yardstick/benchmark/contexts/heat.py b/yardstick/benchmark/contexts/heat.py
index b689ac09c..aa134d694 100644
--- a/yardstick/benchmark/contexts/heat.py
+++ b/yardstick/benchmark/contexts/heat.py
@@ -13,9 +13,10 @@ from __future__ import print_function
import collections
import logging
import os
-import sys
import uuid
+from collections import OrderedDict
+import ipaddress
import paramiko
import pkg_resources
@@ -29,6 +30,8 @@ from yardstick.common.constants import YARDSTICK_ROOT_PATH
LOG = logging.getLogger(__name__)
+DEFAULT_HEAT_TIMEOUT = 3600
+
class HeatContext(Context):
"""Class that represents a context in the logical model"""
@@ -38,7 +41,7 @@ class HeatContext(Context):
def __init__(self):
self.name = None
self.stack = None
- self.networks = []
+ self.networks = OrderedDict()
self.servers = []
self.placement_groups = []
self.server_groups = []
@@ -68,6 +71,7 @@ class HeatContext(Context):
# no external net defined, assign it to first network usig os.environ
if sorted_networks and not have_external_network:
sorted_networks[0][1]["external_network"] = external_network
+ return sorted_networks
def init(self, attrs): # pragma: no cover
"""initializes itself from the supplied arguments"""
@@ -87,6 +91,8 @@ class HeatContext(Context):
self._flavor = attrs.get("flavor")
+ self.heat_timeout = attrs.get("timeout", DEFAULT_HEAT_TIMEOUT)
+
self.placement_groups = [PlacementGroup(name, self, pgattrs["policy"])
for name, pgattrs in attrs.get(
"placement_groups", {}).items()]
@@ -95,12 +101,15 @@ class HeatContext(Context):
for name, sgattrs in attrs.get(
"server_groups", {}).items()]
- self.assign_external_network(attrs["networks"])
+ # we have to do this first, because we are injecting external_network
+ # into the dict
+ sorted_networks = self.assign_external_network(attrs["networks"])
- self.networks = [Network(name, self, netattrs) for name, netattrs in
- sorted(attrs["networks"].items())]
+ self.networks = OrderedDict(
+ (name, Network(name, self, netattrs)) for name, netattrs in
+ sorted_networks)
- for name, serverattrs in attrs["servers"].items():
+ for name, serverattrs in sorted(attrs["servers"].items()):
server = Server(name, self, serverattrs)
self.servers.append(server)
self._server_map[server.dn] = server
@@ -140,7 +149,7 @@ class HeatContext(Context):
template.add_keypair(self.keypair_name, self.key_uuid)
template.add_security_group(self.secgroup_name)
- for network in self.networks:
+ for network in self.networks.values():
template.add_network(network.stack_name,
network.physical_network,
network.provider)
@@ -190,17 +199,17 @@ class HeatContext(Context):
if not scheduler_hints["different_host"]:
scheduler_hints.pop("different_host", None)
server.add_to_template(template,
- self.networks,
+ list(self.networks.values()),
scheduler_hints)
else:
scheduler_hints["different_host"] = \
scheduler_hints["different_host"][0]
server.add_to_template(template,
- self.networks,
+ list(self.networks.values()),
scheduler_hints)
else:
server.add_to_template(template,
- self.networks,
+ list(self.networks.values()),
scheduler_hints)
added_servers.append(server.stack_name)
@@ -219,7 +228,8 @@ class HeatContext(Context):
scheduler_hints = {}
for pg in server.placement_groups:
update_scheduler_hints(scheduler_hints, added_servers, pg)
- server.add_to_template(template, self.networks, scheduler_hints)
+ server.add_to_template(template, list(self.networks.values()),
+ scheduler_hints)
added_servers.append(server.stack_name)
# add server group
@@ -236,7 +246,8 @@ class HeatContext(Context):
if sg:
scheduler_hints["group"] = {'get_resource': sg.name}
server.add_to_template(template,
- self.networks, scheduler_hints)
+ list(self.networks.values()),
+ scheduler_hints)
def deploy(self):
"""deploys template into a stack using cloud"""
@@ -249,13 +260,14 @@ class HeatContext(Context):
self._add_resources_to_template(heat_template)
try:
- self.stack = heat_template.create()
+ self.stack = heat_template.create(block=True,
+ timeout=self.heat_timeout)
except KeyboardInterrupt:
- sys.exit("\nStack create interrupted")
- except RuntimeError as err:
- sys.exit("error: failed to deploy stack: '%s'" % err.args)
- except Exception as err:
- sys.exit("error: failed to deploy stack: '%s'" % err)
+ raise SystemExit("\nStack create interrupted")
+ except:
+ LOG.exception("stack failed")
+ raise
+ # let the other failures happend, we want stack trace
# copy some vital stack output into server objects
for server in self.servers:
@@ -263,6 +275,11 @@ class HeatContext(Context):
# TODO(hafe) can only handle one internal network for now
port = next(iter(server.ports.values()))
server.private_ip = self.stack.outputs[port["stack_name"]]
+ server.interfaces = {}
+ for network_name, port in server.ports.items():
+ self.make_interface_dict(network_name, port['stack_name'],
+ server,
+ self.stack.outputs)
if server.floating_ip:
server.public_ip = \
@@ -270,6 +287,27 @@ class HeatContext(Context):
print("Context '%s' deployed" % self.name)
+ def make_interface_dict(self, network_name, stack_name, server, outputs):
+ server.interfaces[network_name] = {
+ "private_ip": outputs[stack_name],
+ "subnet_id": outputs[stack_name + "-subnet_id"],
+ "subnet_cidr": outputs[
+ "{}-{}-subnet-cidr".format(self.name, network_name)],
+ "netmask": str(ipaddress.ip_network(
+ outputs["{}-{}-subnet-cidr".format(self.name,
+ network_name)]).netmask),
+ "gateway_ip": outputs[
+ "{}-{}-subnet-gateway_ip".format(self.name, network_name)],
+ "mac_address": outputs[stack_name + "-mac_address"],
+ "device_id": outputs[stack_name + "-device_id"],
+ "network_id": outputs[stack_name + "-network_id"],
+ "network_name": network_name,
+ # to match vnf_generic
+ "local_mac": outputs[stack_name + "-mac_address"],
+ "local_ip": outputs[stack_name],
+ "vld_id": self.networks[network_name].vld_id,
+ }
+
def undeploy(self):
"""undeploys stack from cloud"""
if self.stack:
@@ -324,7 +362,8 @@ class HeatContext(Context):
result = {
"user": server.context.user,
"key_filename": key_filename,
- "private_ip": server.private_ip
+ "private_ip": server.private_ip,
+ "interfaces": server.interfaces,
}
# Target server may only have private_ip
if server.public_ip:
diff --git a/yardstick/benchmark/contexts/model.py b/yardstick/benchmark/contexts/model.py
index 546201e9b..1f8c6f11c 100644
--- a/yardstick/benchmark/contexts/model.py
+++ b/yardstick/benchmark/contexts/model.py
@@ -111,6 +111,7 @@ class Network(Object):
if "external_network" in attrs:
self.router = Router("router", self.name,
context, attrs["external_network"])
+ self.vld_id = attrs.get("vld_id", "")
Network.list.append(self)
@@ -152,6 +153,7 @@ class Server(Object): # pragma: no cover
self.public_ip = None
self.private_ip = None
self.user_data = ''
+ self.interfaces = {}
if attrs is None:
attrs = {}
diff --git a/yardstick/benchmark/core/task.py b/yardstick/benchmark/core/task.py
index 3a151dbba..7eb9a1f29 100644
--- a/yardstick/benchmark/core/task.py
+++ b/yardstick/benchmark/core/task.py
@@ -383,6 +383,9 @@ class TaskParser(object): # pragma: no cover
task_name = os.path.splitext(os.path.basename(self.path))[0]
scenario["tc"] = task_name
scenario["task_id"] = task_id
+ # embed task path into scenario so we can load other files
+ # relative to task path
+ scenario["task_path"] = os.path.dirname(self.path)
change_server_name(scenario, name_suffix)
diff --git a/yardstick/benchmark/scenarios/networking/vnf_generic.py b/yardstick/benchmark/scenarios/networking/vnf_generic.py
index be179631e..594edeaa8 100644
--- a/yardstick/benchmark/scenarios/networking/vnf_generic.py
+++ b/yardstick/benchmark/scenarios/networking/vnf_generic.py
@@ -15,6 +15,14 @@
from __future__ import absolute_import
import logging
+
+import errno
+import os
+
+import re
+from operator import itemgetter
+from collections import defaultdict
+
import yaml
from yardstick.benchmark.scenarios import base
@@ -72,6 +80,15 @@ class SshManager(object):
self.conn.close()
+def open_relative_file(path, task_path):
+ try:
+ return open(path)
+ except IOError as e:
+ if e.errno == errno.ENOENT:
+ return open(os.path.join(task_path, path))
+ raise
+
+
class NetworkServiceTestCase(base.Scenario):
"""Class handles Generic framework to do pre-deployment VNF &
Network service testing """
@@ -84,8 +101,11 @@ class NetworkServiceTestCase(base.Scenario):
self.context_cfg = context_cfg
# fixme: create schema to validate all fields have been provided
- with open(scenario_cfg["topology"]) as stream:
- self.topology = yaml.load(stream)["nsd:nsd-catalog"]["nsd"][0]
+ with open_relative_file(scenario_cfg["topology"],
+ scenario_cfg['task_path']) as stream:
+ topology_yaml = yaml.load(stream)
+
+ self.topology = topology_yaml["nsd:nsd-catalog"]["nsd"][0]
self.vnfs = []
self.collector = None
self.traffic_profile = None
@@ -114,7 +134,8 @@ class NetworkServiceTestCase(base.Scenario):
private = {}
public = {}
try:
- with open(scenario_cfg["traffic_profile"]) as infile:
+ with open_relative_file(scenario_cfg["traffic_profile"],
+ scenario_cfg["task_path"]) as infile:
traffic_profile_tpl = infile.read()
except (KeyError, IOError, OSError):
@@ -123,8 +144,6 @@ class NetworkServiceTestCase(base.Scenario):
return [traffic_profile_tpl, private, public]
def _fill_traffic_profile(self, scenario_cfg, context_cfg):
- traffic_profile = {}
-
flow = self._get_traffic_flow(scenario_cfg)
imix = self._get_traffic_imix(scenario_cfg)
@@ -193,6 +212,26 @@ class NetworkServiceTestCase(base.Scenario):
list_idx = self._find_list_index_from_vnf_idx(topology, vnf_idx)
nodes[node].update(topology["constituent-vnfd"][list_idx])
+ @staticmethod
+ def _sort_dpdk_port_num(netdevs):
+ # dpdk_port_num is PCI BUS ID ordering, lowest first
+ s = sorted(netdevs.values(), key=itemgetter('pci_bus_id'))
+ for dpdk_port_num, netdev in enumerate(s, 1):
+ netdev['dpdk_port_num'] = dpdk_port_num
+
+ @classmethod
+ def _probe_missing_values(cls, netdevs, network, missing):
+ mac = network['local_mac']
+ for netdev in netdevs.values():
+ if netdev['address'].lower() == mac.lower():
+ network['driver'] = netdev['driver']
+ network['vpci'] = netdev['pci_bus_id']
+ network['dpdk_port_num'] = netdev['dpdk_port_num']
+ network['ifindex'] = netdev['ifindex']
+
+ TOPOLOGY_REQUIRED_KEYS = frozenset({
+ "vpci", "local_ip", "netmask", "local_mac", "driver", "dpdk_port_num"})
+
def map_topology_to_infrastructure(self, context_cfg, topology):
""" This method should verify if the available resources defined in pod.yaml
match the topology.yaml file.
@@ -208,21 +247,66 @@ class NetworkServiceTestCase(base.Scenario):
exit_status = conn.execute(cmd)[0]
if exit_status != 0:
raise IncorrectSetup("Node's %s lacks ip tool." % node)
-
- for interface in node_dict["interfaces"]:
- network = node_dict["interfaces"][interface]
- keys = ["vpci", "local_ip", "netmask",
- "local_mac", "driver", "dpdk_port_num"]
- missing = set(keys).difference(network)
+ exit_status, stdout, _ = conn.execute(
+ self.FIND_NETDEVICE_STRING)
+ if exit_status != 0:
+ raise IncorrectSetup(
+ "Cannot find netdev info in sysfs" % node)
+ netdevs = node_dict['netdevs'] = self.parse_netdev_info(
+ stdout)
+ self._sort_dpdk_port_num(netdevs)
+
+ for network in node_dict["interfaces"].values():
+ missing = self.TOPOLOGY_REQUIRED_KEYS.difference(network)
if missing:
- raise IncorrectConfig("Require interface fields '%s' "
- "not found, topology file "
- "corrupted" % ', '.join(missing))
+ try:
+ self._probe_missing_values(netdevs, network,
+ missing)
+ except KeyError:
+ pass
+ else:
+ missing = self.TOPOLOGY_REQUIRED_KEYS.difference(
+ network)
+ if missing:
+ raise IncorrectConfig(
+ "Require interface fields '%s' "
+ "not found, topology file "
+ "corrupted" % ', '.join(missing))
# 3. Use topology file to find connections & resolve dest address
self._resolve_topology(context_cfg, topology)
self._update_context_with_topology(context_cfg, topology)
+ FIND_NETDEVICE_STRING = r"""find /sys/devices/pci* -type d -name net -exec sh -c '{ grep -sH ^ \
+$1/ifindex $1/address $1/operstate $1/device/vendor $1/device/device \
+$1/device/subsystem_vendor $1/device/subsystem_device ; \
+printf "%s/driver:" $1 ; basename $(readlink -s $1/device/driver); } \
+' sh \{\}/* \;
+"""
+ BASE_ADAPTER_RE = re.compile(
+ '^/sys/devices/(.*)/net/([^/]*)/([^:]*):(.*)$', re.M)
+
+ @classmethod
+ def parse_netdev_info(cls, stdout):
+ network_devices = defaultdict(dict)
+ matches = cls.BASE_ADAPTER_RE.findall(stdout)
+ for bus_path, interface_name, name, value in matches:
+ dirname, bus_id = os.path.split(bus_path)
+ if 'virtio' in bus_id:
+ # for some stupid reason VMs include virtio1/
+ # in PCI device path
+ bus_id = os.path.basename(dirname)
+ # remove extra 'device/' from 'device/vendor,
+ # device/subsystem_vendor', etc.
+ if 'device/' in name:
+ name = name.split('/')[1]
+ network_devices[interface_name][name] = value
+ network_devices[interface_name][
+ 'interface_name'] = interface_name
+ network_devices[interface_name]['pci_bus_id'] = bus_id
+ # convert back to regular dict
+ return dict(network_devices)
+
@classmethod
def get_vnf_impl(cls, vnf_model):
""" Find the implementing class from vnf_model["vnf"]["name"] field
@@ -240,21 +324,24 @@ class NetworkServiceTestCase(base.Scenario):
except StopIteration:
raise IncorrectConfig("No implementation for %s", expected_name)
- def load_vnf_models(self, context_cfg):
+ def load_vnf_models(self, scenario_cfg, context_cfg):
""" Create VNF objects based on YAML descriptors
+ :param scenario_cfg:
+ :type scenario_cfg:
:param context_cfg:
:return:
"""
vnfs = []
- for node in context_cfg["nodes"]:
- LOG.debug(context_cfg["nodes"][node])
- with open(context_cfg["nodes"][node]["VNF model"]) as stream:
+ for node_name, node in context_cfg["nodes"].items():
+ LOG.debug(node)
+ with open_relative_file(node["VNF model"],
+ scenario_cfg['task_path']) as stream:
vnf_model = stream.read()
- vnfd = vnfdgen.generate_vnfd(vnf_model, context_cfg["nodes"][node])
+ vnfd = vnfdgen.generate_vnfd(vnf_model, node)
vnf_impl = self.get_vnf_impl(vnfd["vnfd:vnfd-catalog"]["vnfd"][0])
vnf_instance = vnf_impl(vnfd["vnfd:vnfd-catalog"]["vnfd"][0])
- vnf_instance.name = node
+ vnf_instance.name = node_name
vnfs.append(vnf_instance)
return vnfs
@@ -264,11 +351,10 @@ class NetworkServiceTestCase(base.Scenario):
:return:
"""
-
# 1. Verify if infrastructure mapping can meet topology
self.map_topology_to_infrastructure(self.context_cfg, self.topology)
# 1a. Load VNF models
- self.vnfs = self.load_vnf_models(self.context_cfg)
+ self.vnfs = self.load_vnf_models(self.scenario_cfg, self.context_cfg)
# 1b. Fill traffic profile with information from topology
self.traffic_profile = self._fill_traffic_profile(self.scenario_cfg,
self.context_cfg)
diff --git a/yardstick/network_services/vnf_generic/vnfdgen.py b/yardstick/network_services/vnf_generic/vnfdgen.py
index 97dd97198..40cc14a49 100644
--- a/yardstick/network_services/vnf_generic/vnfdgen.py
+++ b/yardstick/network_services/vnf_generic/vnfdgen.py
@@ -15,9 +15,20 @@
from __future__ import absolute_import
import collections
+
+import jinja2
import yaml
-from yardstick.common.task_template import TaskTemplate
+
+def render(vnf_model, **kwargs):
+ """Render jinja2 VNF template
+
+ :param vnf_model: string that contains template
+ :param kwargs: Dict with template arguments
+ :returns:rendered template str
+ """
+
+ return jinja2.Template(vnf_model).render(**kwargs)
def generate_vnfd(vnf_model, node):
@@ -31,7 +42,10 @@ def generate_vnfd(vnf_model, node):
# get is unused as global method inside template
node["get"] = get
# Set Node details to default if not defined in pod file
- rendered_vnfd = TaskTemplate.render(vnf_model, **node)
+ # we CANNOT use TaskTemplate.render because it does not allow
+ # for missing variables, we need to allow password for key_filename
+ # to be undefined
+ rendered_vnfd = render(vnf_model, **node)
# This is done to get rid of issues with serializing node
del node["get"]
filled_vnfd = yaml.load(rendered_vnfd)
diff --git a/yardstick/orchestrator/heat.py b/yardstick/orchestrator/heat.py
index ea9bd1b08..a99d4631d 100644
--- a/yardstick/orchestrator/heat.py
+++ b/yardstick/orchestrator/heat.py
@@ -1,5 +1,5 @@
##############################################################################
-# Copyright (c) 2015 Ericsson AB and others.
+# Copyright (c) 2015-2017 Ericsson AB and others.
#
# All rights reserved. This program and the accompanying materials
# are made available under the terms of the Apache License, Version 2.0
@@ -11,6 +11,7 @@
from __future__ import absolute_import
from __future__ import print_function
+from six.moves import range
import collections
import datetime
@@ -47,7 +48,8 @@ class HeatObject(object):
self._heat_client = None
self.uuid = None
- def _get_heat_client(self):
+ @property
+ def heat_client(self):
"""returns a heat client instance"""
if self._heat_client is None:
@@ -61,9 +63,9 @@ class HeatObject(object):
def status(self):
"""returns stack state as a string"""
- heat = self._get_heat_client()
- stack = heat.stacks.get(self.uuid)
- return getattr(stack, 'stack_status')
+ heat_client = self.heat_client
+ stack = heat_client.stacks.get(self.uuid)
+ return stack.stack_status
class HeatStack(HeatObject):
@@ -88,20 +90,18 @@ class HeatStack(HeatObject):
return
log.info("Deleting stack '%s', uuid:%s", self.name, self.uuid)
- heat = self._get_heat_client()
+ heat = self.heat_client
template = heat.stacks.get(self.uuid)
start_time = time.time()
template.delete()
- status = self.status()
- while status != u'DELETE_COMPLETE':
+ for status in iter(self.status, u'DELETE_COMPLETE'):
log.debug("stack state %s", status)
if status == u'DELETE_FAILED':
raise RuntimeError(
heat.stacks.get(self.uuid).stack_status_reason)
time.sleep(2)
- status = self.status()
end_time = time.time()
log.info("Deleted stack '%s' in %d secs", self.name,
@@ -120,15 +120,13 @@ class HeatStack(HeatObject):
self._delete()
return
- i = 0
- while i < retries:
+ for _ in range(retries):
try:
self._delete()
break
except RuntimeError as err:
log.warning(err.args)
time.sleep(2)
- i += 1
# if still not deleted try once more and let it fail everything
if self.uuid is not None:
@@ -149,28 +147,34 @@ class HeatStack(HeatObject):
class HeatTemplate(HeatObject):
"""Describes a Heat template and a method to deploy template to a stack"""
- def _init_template(self):
- self._template = {}
- self._template['heat_template_version'] = '2013-05-23'
+ DESCRIPTION_TEMPLATE = """\
+Stack built by the yardstick framework for %s on host %s %s.
+All referred generated resources are prefixed with the template
+name (i.e. %s).\
+"""
+ def _init_template(self):
timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
- self._template['description'] = \
- """Stack built by the yardstick framework for %s on host %s %s.
- All referred generated resources are prefixed with the template
- name (i.e. %s).""" % (getpass.getuser(), socket.gethostname(),
- timestamp, self.name)
+ self._template = {
+ 'heat_template_version': '2013-05-23',
+ 'description': self.DESCRIPTION_TEMPLATE % (
+ getpass.getuser(),
+ socket.gethostname(),
+ timestamp,
+ self.name
+ ),
+ 'resources': {},
+ 'outputs': {}
+ }
# short hand for resources part of template
- self.resources = self._template['resources'] = {}
-
- self._template['outputs'] = {}
+ self.resources = self._template['resources']
def __init__(self, name, template_file=None, heat_parameters=None):
super(HeatTemplate, self).__init__()
self.name = name
self.state = "NOT_CREATED"
self.keystone_client = None
- self.heat_client = None
self.heat_parameters = {}
# heat_parameters is passed to heat in stack create, empty dict when
@@ -272,6 +276,14 @@ class HeatTemplate(HeatObject):
'description': 'subnet %s ID' % name,
'value': {'get_resource': name}
}
+ self._template['outputs'][name + "-cidr"] = {
+ 'description': 'subnet %s cidr' % name,
+ 'value': {'get_attr': [name, 'cidr']}
+ }
+ self._template['outputs'][name + "-gateway_ip"] = {
+ 'description': 'subnet %s gateway_ip' % name,
+ 'value': {'get_attr': [name, 'gateway_ip']}
+ }
def add_router(self, name, ext_gw_net, subnet_name):
"""add to the template a Neutron Router and interface"""
@@ -329,6 +341,22 @@ class HeatTemplate(HeatObject):
'description': 'Address for interface %s' % name,
'value': {'get_attr': [name, 'fixed_ips', 0, 'ip_address']}
}
+ self._template['outputs'][name + "-subnet_id"] = {
+ 'description': 'Address for interface %s' % name,
+ 'value': {'get_attr': [name, 'fixed_ips', 0, 'subnet_id']}
+ }
+ self._template['outputs'][name + "-mac_address"] = {
+ 'description': 'MAC Address for interface %s' % name,
+ 'value': {'get_attr': [name, 'mac_address']}
+ }
+ self._template['outputs'][name + "-device_id"] = {
+ 'description': 'Device ID for interface %s' % name,
+ 'value': {'get_attr': [name, 'device_id']}
+ }
+ self._template['outputs'][name + "-network_id"] = {
+ 'description': 'Network ID for interface %s' % name,
+ 'value': {'get_attr': [name, 'network_id']}
+ }
def add_floating_ip(self, name, network_name, port_name, router_if_name,
secgroup_name=None):
@@ -501,38 +529,48 @@ class HeatTemplate(HeatObject):
'value': {'get_resource': name}
}
- def create(self, block=True):
- """creates a template in the target cloud using heat
+ HEAT_WAIT_LOOP_INTERVAL = 2
+
+ def create(self, block=True, timeout=3600):
+ """
+ creates a template in the target cloud using heat
returns a dict with the requested output values from the template
+
+ :param block: Wait for Heat create to finish
+ :type block: bool
+ :param: timeout: timeout in seconds for Heat create, default 3600s
+ :type timeout: int
"""
log.info("Creating stack '%s'", self.name)
# create stack early to support cleanup, e.g. ctrl-c while waiting
stack = HeatStack(self.name)
- heat = self._get_heat_client()
+ heat_client = self.heat_client
start_time = time.time()
- stack.uuid = self.uuid = heat.stacks.create(
+ stack.uuid = self.uuid = heat_client.stacks.create(
stack_name=self.name, template=self._template,
parameters=self.heat_parameters)['stack']['id']
- status = self.status()
- outputs = []
+ if not block:
+ self.outputs = stack.outputs = {}
+ return stack
- if block:
- while status != u'CREATE_COMPLETE':
- log.debug("stack state %s", status)
- if status == u'CREATE_FAILED':
- raise RuntimeError(getattr(heat.stacks.get(self.uuid),
- 'stack_status_reason'))
+ time_limit = start_time + timeout
+ for status in iter(self.status, u'CREATE_COMPLETE'):
+ log.debug("stack state %s", status)
+ if status == u'CREATE_FAILED':
+ raise RuntimeError(
+ heat_client.stacks.get(self.uuid).stack_status_reason)
+ if time.time() > time_limit:
+ raise RuntimeError("Heat stack create timeout")
- time.sleep(2)
- status = self.status()
+ time.sleep(self.HEAT_WAIT_LOOP_INTERVAL)
- end_time = time.time()
- outputs = getattr(heat.stacks.get(self.uuid), 'outputs')
- log.info("Created stack '%s' in %d secs",
- self.name, end_time - start_time)
+ end_time = time.time()
+ outputs = heat_client.stacks.get(self.uuid).outputs
+ log.info("Created stack '%s' in %d secs",
+ self.name, end_time - start_time)
# keep outputs as unicode
self.outputs = {output["output_key"]: output["output_value"] for output