
The 'is_different' utility function was relying on the sequence of dictionary keys when determining whether the objects are different or not, which is incorrect, since Python does not guarantee the order of keys in regular dictionaries. This was causing the function to report false inequalities after the Heat Stack is pushed, thus causing murano to re-push the stack even if it was not modified in between. The calls of the 'is_different' method were replaced with simple equality check which are guaranteed to return appropriate result. The only known limitation of such approach is its inability to check for circular references. However this is not the case for Murano-generate Heat snippets, since all the variables in MuranoPL are immutable and it's impossible to create a circular-referencing snippet with MuranoPL. Thus it is safe to completely remove 'is_different' method and all related code. The same patch also introduces a simple check which verifies that the HeatStack.update_template() method introduces an actual change into the template, or does not cause a subsequent push() call otherwise. Change-Id: Ia71b44ed62f39d9c89630c5a5c21b79c7c17ea9d Closes-bug: #1594451
280 lines
9.7 KiB
Python
280 lines
9.7 KiB
Python
# Copyright (c) 2013 Mirantis Inc.
|
|
#
|
|
# 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.
|
|
|
|
import copy
|
|
|
|
import eventlet
|
|
import heatclient.client as hclient
|
|
import heatclient.exc as heat_exc
|
|
from oslo_config import cfg
|
|
from oslo_log import log as logging
|
|
import six
|
|
|
|
from murano.common import auth_utils
|
|
from murano.common.i18n import _LW
|
|
from murano.dsl import dsl
|
|
from murano.dsl import helpers
|
|
from murano.dsl import session_local_storage
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
CONF = cfg.CONF
|
|
|
|
HEAT_TEMPLATE_VERSION = '2013-05-23'
|
|
|
|
|
|
class HeatStackError(Exception):
|
|
pass
|
|
|
|
|
|
@dsl.name('io.murano.system.HeatStack')
|
|
class HeatStack(object):
|
|
def __init__(self, this, name, description=None):
|
|
self._name = name
|
|
self._template = None
|
|
self._parameters = {}
|
|
self._files = {}
|
|
self._hot_environment = ''
|
|
self._applied = True
|
|
self._description = description
|
|
self._last_stack_timestamps = (None, None)
|
|
self._tags = ''
|
|
self._owner = this.find_owner('io.murano.Environment')
|
|
|
|
@staticmethod
|
|
def _create_client(session, region_name):
|
|
parameters = auth_utils.get_session_client_parameters(
|
|
service_type='orchestration', region=region_name,
|
|
conf=CONF.heat, session=session)
|
|
return hclient.Client('1', **parameters)
|
|
|
|
@property
|
|
def _client(self):
|
|
region = None if self._owner is None else self._owner['region']
|
|
return self._get_client(region)
|
|
|
|
@staticmethod
|
|
@session_local_storage.execution_session_memoize
|
|
def _get_client(region_name):
|
|
session = auth_utils.get_client_session(conf=CONF.heat)
|
|
return HeatStack._create_client(session, region_name)
|
|
|
|
def _get_token_client(self):
|
|
ks_session = auth_utils.get_token_client_session(conf=CONF.heat)
|
|
region = None if self._owner is None else self._owner['region']
|
|
return self._create_client(ks_session, region)
|
|
|
|
def current(self):
|
|
if self._template is not None:
|
|
return self._template
|
|
try:
|
|
stack_info = self._client.stacks.get(stack_id=self._name)
|
|
template = self._client.stacks.template(
|
|
stack_id='{0}/{1}'.format(
|
|
stack_info.stack_name,
|
|
stack_info.id))
|
|
self._template = template
|
|
self._parameters.update(
|
|
HeatStack._remove_system_params(stack_info.parameters))
|
|
self._applied = True
|
|
return self._template.copy()
|
|
except heat_exc.HTTPNotFound:
|
|
self._applied = True
|
|
self._template = {}
|
|
self._parameters.clear()
|
|
return {}
|
|
|
|
def parameters(self):
|
|
self.current()
|
|
return self._parameters.copy()
|
|
|
|
def reload(self):
|
|
self._template = None
|
|
self._parameters.clear()
|
|
return self.current()
|
|
|
|
def set_template(self, template):
|
|
self._template = template
|
|
self._parameters.clear()
|
|
self._applied = False
|
|
|
|
def set_parameters(self, parameters):
|
|
self._parameters = parameters
|
|
self._applied = False
|
|
|
|
def set_files(self, files):
|
|
self._files = files
|
|
self._applied = False
|
|
|
|
def set_hot_environment(self, hot_environment):
|
|
self._hot_environment = hot_environment
|
|
self._applied = False
|
|
|
|
def update_template(self, template):
|
|
template_version = template.get('heat_template_version',
|
|
HEAT_TEMPLATE_VERSION)
|
|
if template_version != HEAT_TEMPLATE_VERSION:
|
|
err_msg = ("Currently only heat_template_version %s "
|
|
"is supported." % HEAT_TEMPLATE_VERSION)
|
|
raise HeatStackError(err_msg)
|
|
current = self.current()
|
|
self._template = helpers.merge_dicts(self._template, template)
|
|
self._applied = self._template == current and self._applied
|
|
|
|
@staticmethod
|
|
def _remove_system_params(parameters):
|
|
return dict((k, v) for k, v in six.iteritems(parameters) if
|
|
not k.startswith('OS::'))
|
|
|
|
def _get_status(self):
|
|
status = [None]
|
|
|
|
def status_func(state_value):
|
|
status[0] = state_value
|
|
return True
|
|
|
|
self._wait_state(status_func)
|
|
return status[0]
|
|
|
|
def _wait_state(self, status_func, wait_progress=False):
|
|
tries = 4
|
|
delay = 1
|
|
|
|
while tries > 0:
|
|
while True:
|
|
try:
|
|
stack_info = self._client.stacks.get(
|
|
stack_id=self._name)
|
|
status = stack_info.stack_status
|
|
tries = 4
|
|
delay = 1
|
|
except heat_exc.HTTPNotFound:
|
|
stack_info = None
|
|
status = 'NOT_FOUND'
|
|
except Exception:
|
|
tries -= 1
|
|
delay *= 2
|
|
if not tries:
|
|
raise
|
|
eventlet.sleep(delay)
|
|
break
|
|
|
|
if 'IN_PROGRESS' in status:
|
|
eventlet.sleep(2)
|
|
continue
|
|
|
|
last_stack_timestamps = self._last_stack_timestamps
|
|
self._last_stack_timestamps = (None, None) if not stack_info \
|
|
else(stack_info.creation_time, stack_info.updated_time)
|
|
|
|
if (wait_progress and last_stack_timestamps ==
|
|
self._last_stack_timestamps and
|
|
last_stack_timestamps != (None, None)):
|
|
eventlet.sleep(2)
|
|
continue
|
|
|
|
if not status_func(status):
|
|
reason = ': {0}'.format(
|
|
stack_info.stack_status_reason) if stack_info else ''
|
|
raise EnvironmentError(
|
|
"Unexpected stack state {0}{1}".format(status, reason))
|
|
|
|
try:
|
|
return dict([(t['output_key'], t['output_value'])
|
|
for t in stack_info.outputs])
|
|
except Exception:
|
|
return {}
|
|
return {}
|
|
|
|
def output(self):
|
|
return self._wait_state(lambda status: True)
|
|
|
|
def push(self):
|
|
if self._applied or self._template is None:
|
|
return
|
|
|
|
self._tags = ','.join(CONF.heat.stack_tags)
|
|
if 'heat_template_version' not in self._template:
|
|
self._template['heat_template_version'] = HEAT_TEMPLATE_VERSION
|
|
|
|
if 'description' not in self._template and self._description:
|
|
self._template['description'] = self._description
|
|
|
|
template = copy.deepcopy(self._template)
|
|
LOG.debug('Pushing: {template}'.format(template=template))
|
|
|
|
while True:
|
|
try:
|
|
current_status = self._get_status()
|
|
resources = template.get('Resources') or template.get(
|
|
'resources')
|
|
if current_status == 'NOT_FOUND':
|
|
if resources is not None:
|
|
token_client = self._get_token_client()
|
|
token_client.stacks.create(
|
|
stack_name=self._name,
|
|
parameters=self._parameters,
|
|
template=template,
|
|
files=self._files,
|
|
environment=self._hot_environment,
|
|
disable_rollback=True,
|
|
tags=self._tags)
|
|
|
|
self._wait_state(
|
|
lambda status: status == 'CREATE_COMPLETE')
|
|
else:
|
|
if resources is not None:
|
|
self._client.stacks.update(
|
|
stack_id=self._name,
|
|
parameters=self._parameters,
|
|
files=self._files,
|
|
environment=self._hot_environment,
|
|
template=template,
|
|
disable_rollback=True,
|
|
tags=self._tags)
|
|
self._wait_state(
|
|
lambda status: status == 'UPDATE_COMPLETE', True)
|
|
else:
|
|
self.delete()
|
|
except heat_exc.HTTPConflict as e:
|
|
LOG.warning(_LW('Conflicting operation: {msg}').format(msg=e))
|
|
eventlet.sleep(3)
|
|
else:
|
|
break
|
|
|
|
self._applied = self._template == template
|
|
|
|
def delete(self):
|
|
while True:
|
|
try:
|
|
if not self.current():
|
|
return
|
|
self._wait_state(lambda s: True)
|
|
self._client.stacks.delete(stack_id=self._name)
|
|
self._wait_state(
|
|
lambda status: status in ('DELETE_COMPLETE', 'NOT_FOUND'),
|
|
wait_progress=True)
|
|
except heat_exc.NotFound:
|
|
LOG.warning(_LW('Stack {stack_name} already deleted?')
|
|
.format(stack_name=self._name))
|
|
break
|
|
except heat_exc.HTTPConflict as e:
|
|
LOG.warning(_LW('Conflicting operation: {msg}').format(msg=e))
|
|
eventlet.sleep(3)
|
|
else:
|
|
break
|
|
|
|
self._template = {}
|
|
self._applied = True
|