From 7a32c18b2fb3f220f099218871ba29115ef31ee9 Mon Sep 17 00:00:00 2001 From: Ross Brattain Date: Sun, 21 May 2017 21:07:50 -0700 Subject: add network info to topology we need to know which network each port is connected to so we can find VLAN or VXLAN ID. To do this we implement a new method for Contexts, Context.get_network(). This method is similar to Context.get_server(), it searches for a given network name in all the contexts. From this we generate a context_cfg["networks"] dict that stores all the network info for the nodes in the scenario. Then when we generate the topology for VNFD, we can lookup a given network by the vld_id and get the network_type, segmentation_id, etc. Then if we need to for example generated traffic on a given VLAN or VXLAN, we have this info available. Define default nd_route_tbl for ACL VNF we need default empty nd_route_tbl for IPv6 route. Change-Id: I9f9cfbd6acabeb4ae4675ca7354390efa57b29e7 Signed-off-by: Ross Brattain Signed-off-by: Edward MacGillivray --- yardstick/benchmark/contexts/base.py | 32 +++-- yardstick/benchmark/contexts/dummy.py | 3 + yardstick/benchmark/contexts/heat.py | 141 +++++++++++++++------ yardstick/benchmark/contexts/model.py | 7 +- yardstick/benchmark/contexts/node.py | 30 +++++ yardstick/benchmark/contexts/standalone.py | 32 +++++ yardstick/benchmark/core/task.py | 30 +++-- .../benchmark/scenarios/networking/vnf_generic.py | 106 +++++++++++----- yardstick/orchestrator/heat.py | 8 +- 9 files changed, 294 insertions(+), 95 deletions(-) (limited to 'yardstick') diff --git a/yardstick/benchmark/contexts/base.py b/yardstick/benchmark/contexts/base.py index 0be2eee77..e362c6a3d 100644 --- a/yardstick/benchmark/contexts/base.py +++ b/yardstick/benchmark/contexts/base.py @@ -23,7 +23,7 @@ class Context(object): @abc.abstractmethod def init(self, attrs): - "Initiate context." + """Initiate context.""" @staticmethod def get_cls(context_type): @@ -56,20 +56,34 @@ class Context(object): """get server info by name from context """ + @abc.abstractmethod + def _get_network(self, attr_name): + """get network info by name from context + """ + @staticmethod def get_server(attr_name): """lookup server info by name from context attr_name: either a name for a server created by yardstick or a dict with attribute name mapping when using external heat templates """ - server = None - for context in Context.list: - server = context._get_server(attr_name) - if server is not None: - break - - if server is None: + servers = (context._get_server(attr_name) for context in Context.list) + try: + return next(s for s in servers if s) + except StopIteration: raise ValueError("context not found for server '%r'" % attr_name) - return server + @staticmethod + def get_network(attr_name): + """lookup server info by name from context + attr_name: either a name for a server created by yardstick or a dict + with attribute name mapping when using external heat templates + """ + + networks = (context._get_network(attr_name) for context in Context.list) + try: + return next(n for n in networks if n) + except StopIteration: + raise ValueError("context not found for server '%r'" % + attr_name) diff --git a/yardstick/benchmark/contexts/dummy.py b/yardstick/benchmark/contexts/dummy.py index c658d3257..8ae4b65b8 100644 --- a/yardstick/benchmark/contexts/dummy.py +++ b/yardstick/benchmark/contexts/dummy.py @@ -37,3 +37,6 @@ class DummyContext(Context): def _get_server(self, attr_name): return None + + def _get_network(self, attr_name): + return None diff --git a/yardstick/benchmark/contexts/heat.py b/yardstick/benchmark/contexts/heat.py index fed8fc342..0a94dd976 100644 --- a/yardstick/benchmark/contexts/heat.py +++ b/yardstick/benchmark/contexts/heat.py @@ -25,6 +25,7 @@ from yardstick.benchmark.contexts.model import Network from yardstick.benchmark.contexts.model import PlacementGroup, ServerGroup from yardstick.benchmark.contexts.model import Server from yardstick.benchmark.contexts.model import update_scheduler_hints +from yardstick.common.openstack_utils import get_neutron_client from yardstick.orchestrator.heat import HeatTemplate, get_short_key_uuid from yardstick.common.constants import YARDSTICK_ROOT_PATH @@ -54,9 +55,11 @@ class HeatContext(Context): self._user = None self.template_file = None self.heat_parameters = None + self.neutron_client = None # generate an uuid to identify yardstick_key # the first 8 digits of the uuid will be used self.key_uuid = uuid.uuid4() + self.heat_timeout = None self.key_filename = ''.join( [YARDSTICK_ROOT_PATH, 'yardstick/resources/files/yardstick_key-', get_short_key_uuid(self.key_uuid)]) @@ -65,15 +68,16 @@ class HeatContext(Context): def assign_external_network(self, networks): sorted_networks = sorted(networks.items()) external_network = os.environ.get("EXTERNAL_NETWORK", "net04_ext") - have_external_network = [(name, net) - for name, net in sorted_networks if - net.get("external_network")] - # no external net defined, assign it to first network usig os.environ + + have_external_network = any(net.get("external_network") for net in networks.values()) if sorted_networks and not have_external_network: + # no external net defined, assign it to first network using os.environ sorted_networks[0][1]["external_network"] = external_network - return sorted_networks - def init(self, attrs): # pragma: no cover + self.networks = OrderedDict((name, Network(name, self, attrs)) + for name, attrs in sorted_networks) + + def init(self, attrs): """initializes itself from the supplied arguments""" self.name = attrs["name"] @@ -103,11 +107,7 @@ class HeatContext(Context): # 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 = OrderedDict( - (name, Network(name, self, netattrs)) for name, netattrs in - sorted_networks) + self.assign_external_network(attrs["networks"]) for name, serverattrs in sorted(attrs["servers"].items()): server = Server(name, self, serverattrs) @@ -120,7 +120,6 @@ class HeatContext(Context): with open(self.key_filename + ".pub", "w") as pubkey_file: pubkey_file.write( "%s %s\n" % (rsa_key.get_name(), rsa_key.get_base64())) - del rsa_key @property def image(self): @@ -194,7 +193,7 @@ class HeatContext(Context): scheduler_hints = {} for pg in server.placement_groups: update_scheduler_hints(scheduler_hints, added_servers, pg) - # workround for openstack nova bug, check JIRA: YARDSTICK-200 + # workaround for openstack nova bug, check JIRA: YARDSTICK-200 # for details if len(availability_servers) == 2: if not scheduler_hints["different_host"]: @@ -250,6 +249,20 @@ class HeatContext(Context): list(self.networks.values()), scheduler_hints) + def get_neutron_info(self): + if not self.neutron_client: + self.neutron_client = get_neutron_client() + + networks = self.neutron_client.list_networks() + for network in self.networks.values(): + for neutron_net in networks['networks']: + if neutron_net['name'] == network.stack_name: + network.segmentation_id = neutron_net.get('provider:segmentation_id') + # we already have physical_network + # network.physical_network = neutron_net.get('provider:physical_network') + network.network_type = neutron_net.get('provider:network_type') + network.neutron_info = neutron_net + def deploy(self): """deploys template into a stack using cloud""" print("Deploying context '%s'" % self.name) @@ -267,20 +280,16 @@ class HeatContext(Context): raise SystemExit("\nStack create interrupted") except: LOG.exception("stack failed") + # let the other failures happen, we want stack trace raise - # let the other failures happend, we want stack trace + + # TODO: use Neutron to get segementation-id + self.get_neutron_info() # copy some vital stack output into server objects for server in self.servers: if server.ports: - # 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) + self.add_server_port(server) if server.floating_ip: server.public_ip = \ @@ -288,24 +297,36 @@ 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], + def add_server_port(self, server): + # 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(): + server.interfaces[network_name] = self.make_interface_dict( + network_name, port['stack_name'], self.stack.outputs) + + def make_interface_dict(self, network_name, stack_name, outputs): + private_ip = outputs[stack_name] + mac_addr = outputs[stack_name + "-mac_address"] + subnet_cidr_key = "-".join([self.name, network_name, 'subnet', 'cidr']) + gateway_key = "-".join([self.name, network_name, 'subnet', 'gateway_ip']) + subnet_cidr = outputs[subnet_cidr_key] + subnet_ip = ipaddress.ip_network(subnet_cidr) + return { + "private_ip": private_ip, "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"], + "subnet_cidr": subnet_cidr, + "network": str(subnet_ip.network_address), + "netmask": str(subnet_ip.netmask), + "gateway_ip": outputs[gateway_key], + "mac_address": mac_addr, "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], + "local_mac": mac_addr, + "local_ip": private_ip, "vld_id": self.networks[network_name].vld_id, } @@ -326,6 +347,19 @@ class HeatContext(Context): super(HeatContext, self).undeploy() + @staticmethod + def generate_routing_table(server): + routes = [ + { + "network": intf["network"], + "netmask": intf["netmask"], + "if": name, + "gateway": intf["gateway_ip"], + } + for name, intf in server.interfaces.items() + ] + return routes + def _get_server(self, attr_name): """lookup server info by name from context attr_name: either a name for a server created by yardstick or a dict @@ -335,7 +369,10 @@ class HeatContext(Context): 'yardstick.resources', 'files/yardstick_key-' + get_short_key_uuid(self.key_uuid)) - if isinstance(attr_name, collections.Mapping): + if not isinstance(attr_name, collections.Mapping): + server = self._server_map.get(attr_name, None) + + else: cname = attr_name["name"].split(".")[1] if cname != self.name: return None @@ -352,10 +389,6 @@ class HeatContext(Context): server = Server(attr_name["name"].split(".")[0], self, {}) server.public_ip = public_ip server.private_ip = private_ip - else: - if attr_name not in self._server_map: - return None - server = self._server_map[attr_name] if server is None: return None @@ -365,9 +398,37 @@ class HeatContext(Context): "key_filename": key_filename, "private_ip": server.private_ip, "interfaces": server.interfaces, + "routing_table": self.generate_routing_table(server), + # empty IPv6 routing table + "nd_route_tbl": [], } # Target server may only have private_ip if server.public_ip: result["ip"] = server.public_ip return result + + def _get_network(self, attr_name): + if not isinstance(attr_name, collections.Mapping): + network = self.networks.get(attr_name, None) + + else: + # Don't generalize too much Just support vld_id + vld_id = attr_name.get('vld_id') + if vld_id is None: + return None + + network = next((n for n in self.networks.values() if + getattr(n, "vld_id", None) == vld_id), None) + + if network is None: + return None + + result = { + "name": network.name, + "vld_id": network.vld_id, + "segmentation_id": network.segmentation_id, + "network_type": network.network_type, + "physical_network": network.physical_network, + } + return result diff --git a/yardstick/benchmark/contexts/model.py b/yardstick/benchmark/contexts/model.py index 5077a9786..06538d8a9 100644 --- a/yardstick/benchmark/contexts/model.py +++ b/yardstick/benchmark/contexts/model.py @@ -106,13 +106,14 @@ class Network(Object): self.subnet_cidr = attrs.get('cidr', '10.0.1.0/24') self.router = None self.physical_network = attrs.get('physical_network', 'physnet1') - self.provider = attrs.get('provider', None) - self.segmentation_id = attrs.get('segmentation_id', None) + self.provider = attrs.get('provider') + self.segmentation_id = attrs.get('segmentation_id') + self.network_type = attrs.get('network_type') if "external_network" in attrs: self.router = Router("router", self.name, context, attrs["external_network"]) - self.vld_id = attrs.get("vld_id", "") + self.vld_id = attrs.get("vld_id") Network.list.append(self) diff --git a/yardstick/benchmark/contexts/node.py b/yardstick/benchmark/contexts/node.py index baa1cf5d6..b3f0aca0e 100644 --- a/yardstick/benchmark/contexts/node.py +++ b/yardstick/benchmark/contexts/node.py @@ -33,6 +33,7 @@ class NodeContext(Context): self.name = None self.file_path = None self.nodes = [] + self.networks = {} self.controllers = [] self.computes = [] self.baremetals = [] @@ -77,6 +78,9 @@ class NodeContext(Context): self.env = attrs.get('env', {}) LOG.debug("Env: %r", self.env) + # add optional static network definition + self.networks.update(cfg.get("networks", {})) + def deploy(self): config_type = self.env.get('type', '') if config_type == 'ansible': @@ -141,6 +145,32 @@ class NodeContext(Context): node["name"] = attr_name return node + def _get_network(self, attr_name): + if not isinstance(attr_name, collections.Mapping): + network = self.networks.get(attr_name) + + else: + # Don't generalize too much Just support vld_id + vld_id = attr_name.get('vld_id') + if vld_id is None: + return None + + network = next((n for n in self.networks.values() if + n.get("vld_id") == vld_id), None) + + if network is None: + return None + + result = { + # name is required + "name": network["name"], + "vld_id": network.get("vld_id"), + "segmentation_id": network.get("segmentation_id"), + "network_type": network.get("network_type"), + "physical_network": network.get("physical_network"), + } + return result + def _execute_script(self, node_name, info): if node_name == 'local': self._execute_local_script(info) diff --git a/yardstick/benchmark/contexts/standalone.py b/yardstick/benchmark/contexts/standalone.py index 78eaac7ee..8614f0cac 100644 --- a/yardstick/benchmark/contexts/standalone.py +++ b/yardstick/benchmark/contexts/standalone.py @@ -36,6 +36,7 @@ class StandaloneContext(Context): self.name = None self.file_path = None self.nodes = [] + self.networks = {} self.nfvi_node = [] super(StandaloneContext, self).__init__() @@ -66,8 +67,11 @@ class StandaloneContext(Context): self.nodes.extend(cfg["nodes"]) self.nfvi_node.extend([node for node in cfg["nodes"] if node["role"] == "nfvi_node"]) + # add optional static network definition + self.networks.update(cfg.get("networks", {})) LOG.debug("Nodes: %r", self.nodes) LOG.debug("NFVi Node: %r", self.nfvi_node) + LOG.debug("Networks: %r", self.networks) def deploy(self): """don't need to deploy""" @@ -114,3 +118,31 @@ class StandaloneContext(Context): node["name"] = attr_name return node + + def _get_network(self, attr_name): + if not isinstance(attr_name, collections.Mapping): + network = self.networks.get(attr_name) + + else: + # Don't generalize too much Just support vld_id + vld_id = attr_name.get('vld_id') + if vld_id is None: + return None + try: + network = next(n for n in self.networks.values() if + n.get("vld_id") == vld_id) + except StopIteration: + return None + + if network is None: + return None + + result = { + # name is required + "name": network["name"], + "vld_id": network.get("vld_id"), + "segmentation_id": network.get("segmentation_id"), + "network_type": network.get("network_type"), + "physical_network": network.get("physical_network"), + } + return result diff --git a/yardstick/benchmark/core/task.py b/yardstick/benchmark/core/task.py index 0e85e6316..b53d6446e 100644 --- a/yardstick/benchmark/core/task.py +++ b/yardstick/benchmark/core/task.py @@ -322,6 +322,8 @@ class Task(object): # pragma: no cover if "nodes" in scenario_cfg: context_cfg["nodes"] = parse_nodes_with_context(scenario_cfg) + context_cfg["networks"] = get_networks_from_nodes( + context_cfg["nodes"]) runner = base_runner.Runner.get(runner_cfg) print("Starting runner of type '%s'" % runner_cfg["type"]) @@ -518,7 +520,7 @@ class TaskParser(object): # pragma: no cover cfg_schema)) def _check_precondition(self, cfg): - """Check if the envrionment meet the preconditon""" + """Check if the environment meet the precondition""" if "precondition" in cfg: precondition = cfg["precondition"] @@ -573,14 +575,26 @@ def _is_background_scenario(scenario): def parse_nodes_with_context(scenario_cfg): - """paras the 'nodes' fields in scenario """ + """parse the 'nodes' fields in scenario """ nodes = scenario_cfg["nodes"] - - nodes_cfg = {} - for nodename in nodes: - nodes_cfg[nodename] = Context.get_server(nodes[nodename]) - - return nodes_cfg + return {nodename: Context.get_server(node) for nodename, node in nodes.items()} + + +def get_networks_from_nodes(nodes): + """parse the 'nodes' fields in scenario """ + networks = {} + for node in nodes.values(): + if not node: + continue + for interface in node['interfaces'].values(): + vld_id = interface.get('vld_id') + # mgmt network doesn't have vld_id + if not vld_id: + continue + network = Context.get_network({"vld_id": vld_id}) + if network: + networks[network['name']] = network + return networks def runner_join(runner): diff --git a/yardstick/benchmark/scenarios/networking/vnf_generic.py b/yardstick/benchmark/scenarios/networking/vnf_generic.py index 594edeaa8..9607e3005 100644 --- a/yardstick/benchmark/scenarios/networking/vnf_generic.py +++ b/yardstick/benchmark/scenarios/networking/vnf_generic.py @@ -164,38 +164,60 @@ class NetworkServiceTestCase(base.Scenario): for vnfd in topology["constituent-vnfd"] if vnf_id == vnfd["member-vnf-index"]), None) + @staticmethod + def get_vld_networks(networks): + return {n['vld_id']: n for n in networks.values()} + def _resolve_topology(self, context_cfg, topology): for vld in topology["vld"]: - if len(vld["vnfd-connection-point-ref"]) > 2: + try: + node_0, node_1 = vld["vnfd-connection-point-ref"] + except (TypeError, ValueError): raise IncorrectConfig("Topology file corrupted, " - "too many endpoint for connection") - - node_0, node_1 = vld["vnfd-connection-point-ref"] + "wrong number of endpoints for connection") - node0 = self._find_vnf_name_from_id(topology, - node_0["member-vnf-index-ref"]) - node1 = self._find_vnf_name_from_id(topology, - node_1["member-vnf-index-ref"]) + node_0_name = self._find_vnf_name_from_id(topology, + node_0["member-vnf-index-ref"]) + node_1_name = self._find_vnf_name_from_id(topology, + node_1["member-vnf-index-ref"]) - if0 = node_0["vnfd-connection-point-ref"] - if1 = node_1["vnfd-connection-point-ref"] + node_0_ifname = node_0["vnfd-connection-point-ref"] + node_1_ifname = node_1["vnfd-connection-point-ref"] + node_0_if = context_cfg["nodes"][node_0_name]["interfaces"][node_0_ifname] + node_1_if = context_cfg["nodes"][node_1_name]["interfaces"][node_1_ifname] try: - nodes = context_cfg["nodes"] - nodes[node0]["interfaces"][if0]["vld_id"] = vld["id"] - nodes[node1]["interfaces"][if1]["vld_id"] = vld["id"] - - nodes[node0]["interfaces"][if0]["dst_mac"] = \ - nodes[node1]["interfaces"][if1]["local_mac"] - nodes[node0]["interfaces"][if0]["dst_ip"] = \ - nodes[node1]["interfaces"][if1]["local_ip"] - - nodes[node1]["interfaces"][if1]["dst_mac"] = \ - nodes[node0]["interfaces"][if0]["local_mac"] - nodes[node1]["interfaces"][if1]["dst_ip"] = \ - nodes[node0]["interfaces"][if0]["local_ip"] + vld_networks = self.get_vld_networks(context_cfg["networks"]) + + node_0_if["vld_id"] = vld["id"] + node_1_if["vld_id"] = vld["id"] + + # set peer name + node_0_if["peer_name"] = node_1_name + node_1_if["peer_name"] = node_0_name + + # set peer interface name + node_0_if["peer_ifname"] = node_1_ifname + node_1_if["peer_ifname"] = node_0_ifname + + # just load the whole network dict + node_0_if["network"] = vld_networks.get(vld["id"], {}) + node_1_if["network"] = vld_networks.get(vld["id"], {}) + + node_0_if["dst_mac"] = node_1_if["local_mac"] + node_0_if["dst_ip"] = node_1_if["local_ip"] + + node_1_if["dst_mac"] = node_0_if["local_mac"] + node_1_if["dst_ip"] = node_0_if["local_ip"] + + # add peer interface dict, but remove circular link + # TODO: don't waste memory + node_0_copy = node_0_if.copy() + node_1_copy = node_1_if.copy() + node_0_if["peer_intf"] = node_1_copy + node_1_if["peer_intf"] = node_0_copy except KeyError: - raise IncorrectConfig("Required interface not found," + raise IncorrectConfig("Required interface not found, " "topology file corrupted") @classmethod @@ -308,21 +330,36 @@ printf "%s/driver:" $1 ; basename $(readlink -s $1/device/driver); } \ return dict(network_devices) @classmethod - def get_vnf_impl(cls, vnf_model): + def get_vnf_impl(cls, vnf_model_id): """ Find the implementing class from vnf_model["vnf"]["name"] field - :param vnf_model: dictionary containing a parsed vnfd + :param vnf_model_id: parsed vnfd model ID field :return: subclass of GenericVNF """ import_modules_from_package( "yardstick.network_services.vnf_generic.vnf") - expected_name = vnf_model['id'] - impl = (c for c in itersubclasses(GenericVNF) - if c.__name__ == expected_name) + expected_name = vnf_model_id + classes_found = [] + + def impl(): + for name, class_ in ((c.__name__, c) for c in itersubclasses(GenericVNF)): + if name == expected_name: + yield class_ + classes_found.append(name) + try: - return next(impl) + return next(impl()) except StopIteration: - raise IncorrectConfig("No implementation for %s", expected_name) + pass + + raise IncorrectConfig("No implementation for %s found in %s" % + (expected_name, classes_found)) + + @staticmethod + def update_interfaces_from_node(vnfd, node): + for intf in vnfd["vdu"][0]["external-interface"]: + node_intf = node['interfaces'][intf['name']] + intf['virtual-interface'].update(node_intf) def load_vnf_models(self, scenario_cfg, context_cfg): """ Create VNF objects based on YAML descriptors @@ -339,8 +376,11 @@ printf "%s/driver:" $1 ; basename $(readlink -s $1/device/driver); } \ scenario_cfg['task_path']) as stream: vnf_model = stream.read() 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]) + # TODO: here add extra context_cfg["nodes"] regardless of template + vnfd = vnfd["vnfd:vnfd-catalog"]["vnfd"][0] + self.update_interfaces_from_node(vnfd, node) + vnf_impl = self.get_vnf_impl(vnfd['id']) + vnf_instance = vnf_impl(vnfd) vnf_instance.name = node_name vnfs.append(vnf_instance) diff --git a/yardstick/orchestrator/heat.py b/yardstick/orchestrator/heat.py index 7958b1cfb..2a907d124 100644 --- a/yardstick/orchestrator/heat.py +++ b/yardstick/orchestrator/heat.py @@ -534,6 +534,7 @@ name (i.e. %s).\ } HEAT_WAIT_LOOP_INTERVAL = 2 + HEAT_CREATE_COMPLETE_STATUS = u'CREATE_COMPLETE' def create(self, block=True, timeout=3600): """ @@ -558,10 +559,13 @@ name (i.e. %s).\ if not block: self.outputs = stack.outputs = {} + end_time = time.time() + log.info("Created stack '%s' in %.3e secs", + self.name, end_time - start_time) return stack time_limit = start_time + timeout - for status in iter(self.status, u'CREATE_COMPLETE'): + for status in iter(self.status, self.HEAT_CREATE_COMPLETE_STATUS): log.debug("stack state %s", status) if status == u'CREATE_FAILED': stack_status_reason = heat_client.stacks.get(self.uuid).stack_status_reason @@ -574,7 +578,7 @@ name (i.e. %s).\ end_time = time.time() outputs = heat_client.stacks.get(self.uuid).outputs - log.info("Created stack '%s' in %d secs", + log.info("Created stack '%s' in %.3e secs", self.name, end_time - start_time) # keep outputs as unicode -- cgit 1.2.3-korg