X-Git-Url: https://gerrit.opnfv.org/gerrit/gitweb?a=blobdiff_plain;f=yardstick%2Forchestrator%2Fheat.py;h=e0c0db2620141ddfbccfe087838f44bbfcd609a6;hb=a562d505bf1549b41e93ecae4d8461f31851aa20;hp=d58ae5618a4eed1da0f47f96199102c5faaabdc4;hpb=3e00b3a1eb53889ede21578d75856ebaf633e5d9;p=yardstick.git diff --git a/yardstick/orchestrator/heat.py b/yardstick/orchestrator/heat.py index d58ae5618..e0c0db262 100644 --- a/yardstick/orchestrator/heat.py +++ b/yardstick/orchestrator/heat.py @@ -10,149 +10,132 @@ """Heat template and stack management""" from __future__ import absolute_import -from __future__ import print_function -from six.moves import range - import collections import datetime import getpass import logging - +import pkg_resources +import pprint import socket +import tempfile import time -import heatclient.client -import pkg_resources - +from oslo_serialization import jsonutils from oslo_utils import encodeutils +from shade._heat import event_utils -import yardstick.common.openstack_utils as op_utils +from yardstick.common import constants as consts +from yardstick.common import exceptions from yardstick.common import template_format +from yardstick.common import openstack_utils as op_utils -log = logging.getLogger(__name__) +log = logging.getLogger(__name__) -HEAT_KEY_UUID_LENGTH = 8 PROVIDER_SRIOV = "sriov" +_DEPLOYED_STACKS = {} -def get_short_key_uuid(uuid): - return str(uuid)[:HEAT_KEY_UUID_LENGTH] +class HeatStack(object): + """Represents a Heat stack (deployed template) """ -class HeatObject(object): - """base class for template and stack""" - - def __init__(self): - self._heat_client = None - self.uuid = None + def __init__(self, name, os_cloud_config=None): + self.name = name + self.outputs = {} + os_cloud_config = {} if not os_cloud_config else os_cloud_config + self._cloud = op_utils.get_shade_client(**os_cloud_config) + self._stack = None - @property - def heat_client(self): - """returns a heat client instance""" + def _update_stack_tracking(self): + outputs = self._stack.outputs + self.outputs = {output['output_key']: output['output_value'] for output + in outputs} + if self.uuid: + _DEPLOYED_STACKS[self.uuid] = self._stack - if self._heat_client is None: - sess = op_utils.get_session() - heat_endpoint = op_utils.get_endpoint(service_type='orchestration') - self._heat_client = heatclient.client.Client( - op_utils.get_heat_api_version(), - endpoint=heat_endpoint, session=sess) + def create(self, template, heat_parameters, wait, timeout): + """Creates an OpenStack stack from a template""" + with tempfile.NamedTemporaryFile('wb', delete=False) as template_file: + template_file.write(jsonutils.dump_as_bytes(template)) + template_file.close() + self._stack = self._cloud.create_stack( + self.name, template_file=template_file.name, wait=wait, + timeout=timeout, **heat_parameters) - return self._heat_client + self._update_stack_tracking() - def status(self): - """returns stack state as a string""" - heat_client = self.heat_client - stack = heat_client.stacks.get(self.uuid) - return stack.stack_status + def get_failures(self): + return event_utils.get_events(self._cloud, self._stack.id, + event_args={'resource_status': 'FAILED'}) + def get(self): + """Retrieves an existing stack from the target cloud -class HeatStack(HeatObject): - """Represents a Heat stack (deployed template) """ - stacks = [] + Returns a bool indicating whether the stack exists in the target cloud + If the stack exists, it will be stored as self._stack + """ + self._stack = self._cloud.get_stack(self.name) + if not self._stack: + return False - def __init__(self, name): - super(HeatStack, self).__init__() - self.uuid = None - self.name = name - self.outputs = None - HeatStack.stacks.append(self) + self._update_stack_tracking() + return True @staticmethod def stacks_exist(): - """check if any stack has been deployed""" - return len(HeatStack.stacks) > 0 + """Check if any stack has been deployed""" + return len(_DEPLOYED_STACKS) > 0 - def _delete(self): - """deletes a stack from the target cloud using heat""" + def delete(self, wait=True): + """Deletes a stack in the target cloud""" if self.uuid is None: return - log.info("Deleting stack '%s' START, uuid:%s", self.name, self.uuid) - heat = self.heat_client - template = heat.stacks.get(self.uuid) - start_time = time.time() - template.delete() + try: + ret = self._cloud.delete_stack(self.uuid, wait=wait) + except TypeError: + # NOTE(ralonsoh): this exception catch solves a bug in Shade, which + # tries to retrieve and read the stack status when it's already + # deleted. + ret = True - for status in iter(self.status, u'DELETE_COMPLETE'): - log.debug("Deleting stack state: %s", status) - if status == u'DELETE_FAILED': - raise RuntimeError( - heat.stacks.get(self.uuid).stack_status_reason) - - time.sleep(2) - - end_time = time.time() - log.info("Deleting stack '%s' DONE in %d secs", self.name, - end_time - start_time) - self.uuid = None - - def delete(self, block=True, retries=3): - """deletes a stack in the target cloud using heat (with retry) - Sometimes delete fail with "InternalServerError" and the next attempt - succeeds. So it is worthwhile to test a couple of times. - """ - if self.uuid is None: - return - - if not block: - self._delete() - return - - for _ in range(retries): - try: - self._delete() - break - except RuntimeError as err: - log.warning(err.args) - time.sleep(2) - - # if still not deleted try once more and let it fail everything - if self.uuid is not None: - self._delete() - - HeatStack.stacks.remove(self) + _DEPLOYED_STACKS.pop(self.uuid) + self._stack = None + return ret @staticmethod def delete_all(): - for stack in HeatStack.stacks[:]: + """Delete all deployed stacks""" + for stack in _DEPLOYED_STACKS: stack.delete() - def update(self): - """update a stack""" - raise RuntimeError("not implemented") + @property + def status(self): + """Retrieve the current stack status""" + if self._stack: + return self._stack.status + + @property + def uuid(self): + """Retrieve the current stack ID""" + if self._stack: + return self._stack.id -class HeatTemplate(HeatObject): +class HeatTemplate(object): """Describes a Heat template and a method to deploy template to a stack""" - DESCRIPTION_TEMPLATE = """\ + 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).\ +name (i.e. %s). """ + HEAT_WAIT_LOOP_INTERVAL = 2 + HEAT_STATUS_COMPLETE = 'COMPLETE' + def _init_template(self): timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") self._template = { @@ -170,12 +153,12 @@ name (i.e. %s).\ # short hand for resources part of template self.resources = self._template['resources'] - def __init__(self, name, template_file=None, heat_parameters=None): - super(HeatTemplate, self).__init__() + def __init__(self, name, template_file=None, heat_parameters=None, + os_cloud_config=None): self.name = name - self.state = "NOT_CREATED" self.keystone_client = None self.heat_parameters = {} + self._os_cloud_config = {} if not os_cloud_config else os_cloud_config # heat_parameters is passed to heat in stack create, empty dict when # yardstick creates the template (no get_param in resources part) @@ -184,16 +167,13 @@ name (i.e. %s).\ if template_file: with open(template_file) as stream: - print("Parsing external template:", template_file) + log.info('Parsing external template: %s', template_file) template_str = stream.read() self._template = template_format.parse(template_str) self._parameters = heat_parameters else: self._init_template() - # holds results of requested output after deployment - self.outputs = {} - log.debug("template object '%s' created", name) def add_flavor(self, name, vcpus=1, ram=1024, disk=1, ephemeral=0, @@ -202,9 +182,9 @@ name (i.e. %s).\ """add to the template a Flavor description""" if name is None: name = 'auto' - log.debug("adding Nova::Flavor '%s' vcpus '%d' ram '%d' disk '%d' " + - "ephemeral '%d' is_public '%s' rxtx_factor '%d' " + - "swap '%d' extra_specs '%s' ", + log.debug("adding Nova::Flavor '%s' vcpus '%d' ram '%d' disk '%d' " + "ephemeral '%d' is_public '%s' rxtx_factor '%d' " + "swap '%d' extra_specs '%s'", name, vcpus, ram, disk, ephemeral, is_public, rxtx_factor, swap, str(extra_specs)) @@ -363,21 +343,24 @@ name (i.e. %s).\ } } - def add_port(self, name, network_name, subnet_name, vnic_type, sec_group_id=None, + def add_port(self, name, network, sec_group_id=None, provider=None, allowed_address_pairs=None): """add to the template a named Neutron Port """ - log.debug("adding Neutron::Port '%s', network:'%s', subnet:'%s', vnic_type:'%s', " - "secgroup:%s", name, network_name, subnet_name, vnic_type, sec_group_id) + net_is_existing = network.net_flags.get(consts.IS_EXISTING) + depends_on = [] if net_is_existing else [network.subnet_stack_name] + fixed_ips = [{'subnet': network.subnet}] if net_is_existing else [ + {'subnet': {'get_resource': network.subnet_stack_name}}] + network_ = network.name if net_is_existing else { + 'get_resource': network.stack_name} self.resources[name] = { 'type': 'OS::Neutron::Port', - 'depends_on': [subnet_name], + 'depends_on': depends_on, 'properties': { 'name': name, - 'binding:vnic_type': vnic_type, - 'fixed_ips': [{'subnet': {'get_resource': subnet_name}}], - 'network_id': {'get_resource': network_name}, - 'replacement_policy': 'AUTO', + 'binding:vnic_type': network.vnic_type, + 'fixed_ips': fixed_ips, + 'network': network_, } } @@ -394,6 +377,8 @@ name (i.e. %s).\ self.resources[name]['properties'][ 'allowed_address_pairs'] = allowed_address_pairs + log.debug("adding Neutron::Port %s", self.resources[name]) + self._template['outputs'][name] = { 'description': 'Address for interface %s' % name, 'value': {'get_attr': [name, 'fixed_ips', 0, 'ip_address']} @@ -454,7 +439,7 @@ name (i.e. %s).\ } } - def add_keypair(self, name, key_uuid): + def add_keypair(self, name, key_id): """add to the template a Nova KeyPair""" log.debug("adding Nova::KeyPair '%s'", name) self.resources[name] = { @@ -466,7 +451,7 @@ name (i.e. %s).\ pkg_resources.resource_string( 'yardstick.resources', 'files/yardstick_key-' + - get_short_key_uuid(key_uuid) + '.pub'), + key_id + '.pub'), 'utf-8') } } @@ -521,7 +506,36 @@ name (i.e. %s).\ 'port_range_max': '65535'}, {'remote_ip_prefix': '::/0', 'ethertype': 'IPv6', - 'protocol': 'ipv6-icmp'} + 'protocol': 'ipv6-icmp'}, + {'remote_ip_prefix': '0.0.0.0/0', + 'direction': 'egress', + 'protocol': 'tcp', + 'port_range_min': '1', + 'port_range_max': '65535'}, + {'remote_ip_prefix': '0.0.0.0/0', + 'direction': 'egress', + 'protocol': 'udp', + 'port_range_min': '1', + 'port_range_max': '65535'}, + {'remote_ip_prefix': '0.0.0.0/0', + 'direction': 'egress', + 'protocol': 'icmp'}, + {'remote_ip_prefix': '::/0', + 'direction': 'egress', + 'ethertype': 'IPv6', + 'protocol': 'tcp', + 'port_range_min': '1', + 'port_range_max': '65535'}, + {'remote_ip_prefix': '::/0', + 'direction': 'egress', + 'ethertype': 'IPv6', + 'protocol': 'udp', + 'port_range_min': '1', + 'port_range_max': '65535'}, + {'remote_ip_prefix': '::/0', + 'direction': 'egress', + 'ethertype': 'IPv6', + 'protocol': 'ipv6-icmp'}, ] } } @@ -600,57 +614,31 @@ name (i.e. %s).\ 'value': {'get_resource': name} } - HEAT_WAIT_LOOP_INTERVAL = 2 - HEAT_CREATE_COMPLETE_STATUS = u'CREATE_COMPLETE' - 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 + """Creates a stack in the target based on the stored 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 + :param block: (bool) Wait for Heat create to finish + :param timeout: (int) Timeout in seconds for Heat create, + default 3600s + :return A dict with the requested output values from the template """ log.info("Creating stack '%s' START", self.name) - # create stack early to support cleanup, e.g. ctrl-c while waiting - stack = HeatStack(self.name) - - heat_client = self.heat_client start_time = time.time() - stack.uuid = self.uuid = heat_client.stacks.create( - stack_name=self.name, template=self._template, - parameters=self.heat_parameters)['stack']['id'] + stack = HeatStack(self.name, os_cloud_config=self._os_cloud_config) + stack.create(self._template, self.heat_parameters, block, timeout) if not block: - self.outputs = stack.outputs = {} - end_time = time.time() log.info("Creating stack '%s' DONE in %d secs", - self.name, end_time - start_time) + self.name, time.time() - start_time) return stack - time_limit = start_time + timeout - for status in iter(self.status, self.HEAT_CREATE_COMPLETE_STATUS): - log.debug("Creating stack state: %s", status) - if status == u'CREATE_FAILED': - stack_status_reason = heat_client.stacks.get(self.uuid).stack_status_reason - heat_client.stacks.delete(self.uuid) - raise RuntimeError(stack_status_reason) - if time.time() > time_limit: - raise RuntimeError("Heat stack create timeout") + if stack.status != self.HEAT_STATUS_COMPLETE: + for event in stack.get_failures(): + log.error("%s", event.resource_status_reason) + log.error(pprint.pformat(self._template)) + raise exceptions.HeatTemplateError(stack_name=self.name) - time.sleep(self.HEAT_WAIT_LOOP_INTERVAL) - - end_time = time.time() - outputs = heat_client.stacks.get(self.uuid).outputs log.info("Creating stack '%s' DONE in %d secs", - self.name, end_time - start_time) - - # keep outputs as unicode - self.outputs = {output["output_key"]: output["output_value"] for output - in outputs} - - stack.outputs = self.outputs + self.name, time.time() - start_time) return stack