Add support for heat environments

Enable the user to choose one environment from all environment files
located in the package under /Resources/HotEnvironments to be deployed
with the heat template as part of a Murano environment.

Partially implements: blueprint add-support-for-heat-environments-and-files

Change-Id: Id14fea94854221ba92b3eb09189c211c3d10d82f
This commit is contained in:
Michal Gershenzon 2015-08-11 14:59:42 +00:00
parent 9ac9ad538d
commit e811406945
4 changed files with 238 additions and 142 deletions

View File

@ -40,6 +40,7 @@ class HeatStack(object):
self._template = None
self._parameters = {}
self._files = {}
self._hot_environment = ''
self._applied = True
self._description = description
self._clients = helpers.get_environment().clients
@ -88,6 +89,10 @@ class HeatStack(object):
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)
@ -186,6 +191,7 @@ class HeatStack(object):
parameters=self._parameters,
template=template,
files=self._files,
environment=self._hot_environment,
disable_rollback=True)
self._wait_state(lambda status: status == 'CREATE_COMPLETE')
@ -197,6 +203,7 @@ class HeatStack(object):
stack_id=self._name,
parameters=self._parameters,
files=self._files,
environment=self._hot_environment,
template=template,
disable_rollback=True)
self._wait_state(

View File

@ -23,8 +23,10 @@ from murano.dsl import yaql_expression
import murano.packages.application_package
from murano.packages import exceptions
YAQL = yaql_expression.YaqlExpression
RESOURCES_DIR_NAME = 'Resources/'
HOT_FILES_DIR_NAME = 'HotFiles/'
HOT_ENV_DIR_NAME = 'HotEnvironments/'
class Dumper(yaml.Dumper):
@ -34,6 +36,7 @@ class Dumper(yaml.Dumper):
def yaql_representer(dumper, data):
return dumper.represent_scalar(u'!yaql', str(data))
Dumper.add_representer(YAQL, yaql_representer)
@ -90,7 +93,17 @@ class HotPackage(murano.packages.application_package.ApplicationPackage):
'Extends': 'io.murano.Application'
}
parameters = HotPackage._translate_parameters(hot)
hot_envs_path = os.path.join(self._source_directory,
RESOURCES_DIR_NAME,
HOT_ENV_DIR_NAME)
# if using hot environments, doing parameter validation with contracts
# will overwrite the parameters in the hot environment.
# don't validate parameters if hot environments exist.
validate_hot_parameters = (not os.path.isdir(hot_envs_path) or
not os.listdir(hot_envs_path))
parameters = HotPackage._build_properties(hot, validate_hot_parameters)
parameters.update(HotPackage._translate_outputs(hot))
translated['Properties'] = parameters
@ -99,21 +112,44 @@ class HotPackage(murano.packages.application_package.ApplicationPackage):
self._translated_class = translated
@staticmethod
def _translate_parameters(hot):
def _build_properties(hot, validate_hot_parameters):
result = {
'generatedHeatStackName': {
'Contract': YAQL('$.string()'),
'Usage': 'Out'
},
'hotEnvironment': {
'Contract': YAQL('$.string()'),
'Usage': 'In'
},
'name': {
'Contract': YAQL('$.string().notNull()'),
'Usage': 'In',
}
}
for key, value in (hot.get('parameters') or {}).items():
result[key] = HotPackage._translate_parameter(value)
result['name'] = {'Usage': 'In',
'Contract': YAQL('$.string().notNull()')}
if validate_hot_parameters:
params_dict = {}
for key, value in (hot.get('parameters') or {}).items():
param_contract = HotPackage._translate_param_to_contract(value)
params_dict[key] = param_contract
result['templateParameters'] = {
'Contract': params_dict,
'Default': {},
'Usage': 'In'
}
else:
result['templateParameters'] = {
'Contract': {},
'Default': {},
'Usage': 'In'
}
return result
@staticmethod
def _translate_parameter(value):
def _translate_param_to_contract(value):
contract = '$'
parameter_type = value['type']
@ -132,12 +168,7 @@ class HotPackage(murano.packages.application_package.ApplicationPackage):
if translated:
contract += translated
result = {
'Contract': YAQL(contract),
"Usage": "In"
}
if 'default' in value:
result['Default'] = value['default']
result = YAQL(contract)
return result
@staticmethod
@ -152,20 +183,21 @@ class HotPackage(murano.packages.application_package.ApplicationPackage):
@staticmethod
def _translate_files(source_directory):
heat_files_dir = os.path.join(source_directory, 'Resources/HotFiles')
result = {}
if os.path.isdir(heat_files_dir):
result = HotPackage._build_heat_files_dict(heat_files_dir)
return result
hot_files_path = os.path.join(source_directory,
RESOURCES_DIR_NAME,
HOT_FILES_DIR_NAME)
return HotPackage._build_hot_resources_dict(hot_files_path)
@staticmethod
def _build_heat_files_dict(basedir):
def _build_hot_resources_dict(basedir):
result = []
for root, _, files in os.walk(os.path.abspath(basedir)):
for f in files:
full_path = os.path.join(root, f)
relative_path = os.path.relpath(full_path, basedir)
result.append(relative_path)
if os.path.isdir(basedir):
for root, _, files in os.walk(os.path.abspath(basedir)):
for f in files:
full_path = os.path.join(root, f)
relative_path = os.path.relpath(full_path, basedir)
result.append(relative_path)
return result
@staticmethod
@ -220,14 +252,14 @@ class HotPackage(murano.packages.application_package.ApplicationPackage):
@staticmethod
def _generate_workflow(hot, files):
template_parameters = {}
for key, value in (hot.get('parameters') or {}).items():
template_parameters[key] = YAQL("$." + key)
hot_files_map = {}
for f in files:
file_path = "$resources.string('HotFiles/%s')" % f
file_path = "$resources.string('{0}{1}')".format(
HOT_FILES_DIR_NAME, f)
hot_files_map[f] = YAQL(file_path)
hot_env = YAQL("$.hotEnvironment")
copy_outputs = []
for key, value in (hot.get('outputs') or {}).items():
copy_outputs.append({YAQL('$.' + key): YAQL('$outputs.' + key)})
@ -254,12 +286,24 @@ class HotPackage(murano.packages.application_package.ApplicationPackage):
"'Application deployment has started')"),
{YAQL('$resources'): YAQL("new('io.murano.system.Resources')")},
{YAQL('$template'): YAQL("$resources.yaml(type($this))")},
{YAQL('$parameters'): template_parameters},
{YAQL('$files'): hot_files_map},
YAQL('$stack.setTemplate($template)'),
{YAQL('$parameters'): YAQL("$.templateParameters")},
YAQL('$stack.setParameters($parameters)'),
{YAQL('$files'): hot_files_map},
YAQL('$stack.setFiles($files)'),
{YAQL('$hotEnv'): hot_env},
{
'If': YAQL("bool($hotEnv)"),
'Then': [
{YAQL('$envRelPath'): YAQL("'{0}' + $hotEnv".format(
HOT_ENV_DIR_NAME))},
{YAQL('$hotEnvContent'): YAQL("$resources.string("
"$envRelPath)")},
YAQL('$stack.setHotEnvironment($hotEnvContent)')
]
},
YAQL("$reporter.report($this, 'Stack creation has started')"),
{
@ -323,8 +367,8 @@ class HotPackage(murano.packages.application_package.ApplicationPackage):
'label': 'Application Name',
'required': True,
'description':
'Enter a desired name for the application.'
' Just A-Z, a-z, 0-9, and dash are allowed'
'Enter a desired name for the application.'
' Just A-Z, a-z, 0-9, and dash are allowed'
}
]
used_parameters = set()
@ -367,7 +411,7 @@ class HotPackage(murano.packages.application_package.ApplicationPackage):
translated['type'] = 'boolean'
else:
# string, json, and comma_delimited_list parameters are all
# displayed as strings in UI. Any unsuported parameter would also
# displayed as strings in UI. Any unsupported parameter would also
# be displayed as strings.
translated['type'] = 'string'
@ -452,8 +496,12 @@ class HotPackage(murano.packages.application_package.ApplicationPackage):
}
}
for i, record in enumerate(groups):
if i == 0:
section = app
else:
section = app.setdefault('templateParameters', {})
for property_name in record[1]:
app[property_name] = YAQL(
section[property_name] = YAQL(
'$.group{0}.{1}'.format(i, property_name))
app['name'] = YAQL('$.group0.name')

View File

@ -27,7 +27,7 @@ class TestHotPackage(test_base.MuranoTestCase):
)
load_utils.load_from_dir(package_dir)
result = murano.packages.hot_package.HotPackage._translate_files(
files = murano.packages.hot_package.HotPackage._translate_files(
package_dir)
expected_result = [
"testHeatFile",
@ -36,7 +36,7 @@ class TestHotPackage(test_base.MuranoTestCase):
"middle_file/inner_file2/testHeatFile"
]
msg = "hot files were not generated correctly"
self.assertEqual(expected_result, result, msg)
self.assertEqual(expected_result, files, msg)
def test_heat_files_generated_empty(self):
package_dir = os.path.abspath(
@ -45,7 +45,7 @@ class TestHotPackage(test_base.MuranoTestCase):
)
load_utils.load_from_dir(package_dir)
result = murano.packages.hot_package.HotPackage._translate_files(
package_dir)
files = murano.packages.hot_package.HotPackage \
._translate_files(package_dir)
msg = "heat files were not generated correctly. Expected empty dict"
self.assertEqual(result, {}, msg)
self.assertEqual(files, [], msg)

View File

@ -26,7 +26,6 @@ from murano.engine import environment
from murano.engine.system import heat_stack
from murano.tests.unit import base
MOD_NAME = 'murano.engine.system.heat_stack'
@ -48,107 +47,150 @@ class TestHeatStack(base.MuranoTestCase):
self.heat_client_mock
self.environment_mock.clients = client_manager_mock
def test_push_adds_version(self):
@mock.patch(MOD_NAME + '.HeatStack._wait_state')
@mock.patch(MOD_NAME + '.HeatStack._get_status')
def test_push_adds_version(self, status_get, wait_st):
"""Assert that if heat_template_version is omitted, it's added."""
# Note that the 'with x as y, a as b:' syntax was introduced in
# python 2.7, and contextlib.nested was deprecated in py2.7
with mock.patch(MOD_NAME + '.HeatStack._get_status') as status_get:
with mock.patch(MOD_NAME + '.HeatStack._wait_state') as wait_st:
status_get.return_value = 'NOT_FOUND'
wait_st.return_value = {}
status_get.return_value = 'NOT_FOUND'
wait_st.return_value = {}
context = {constants.CTX_ENVIRONMENT: self.environment_mock}
context = {constants.CTX_ENVIRONMENT: self.environment_mock}
with helpers.contextual(context):
hs = heat_stack.HeatStack(
'test-stack', 'Generated by TestHeatStack')
hs._template = {'resources': {'test': 1}}
hs._files = {}
hs._parameters = {}
hs._applied = False
hs.push()
with helpers.contextual(context):
hs = heat_stack.HeatStack(
'test-stack', 'Generated by TestHeatStack')
hs._template = {'resources': {'test': 1}}
hs._files = {}
hs._hot_environment = ''
hs._parameters = {}
hs._applied = False
hs.push()
expected_template = {
'heat_template_version': '2013-05-23',
'description': 'Generated by TestHeatStack',
'resources': {'test': 1}
}
self.heat_client_mock.stacks.create.assert_called_with(
stack_name='test-stack',
disable_rollback=True,
parameters={},
template=expected_template,
files={}
)
self.assertTrue(hs._applied)
with helpers.contextual(context):
hs = heat_stack.HeatStack(
'test-stack', 'Generated by TestHeatStack')
hs._template = {'resources': {'test': 1}}
hs._files = {}
hs._parameters = {}
hs._applied = False
hs.push()
def test_description_is_optional(self):
expected_template = {
'heat_template_version': '2013-05-23',
'description': 'Generated by TestHeatStack',
'resources': {'test': 1}
}
self.heat_client_mock.stacks.create.assert_called_with(
stack_name='test-stack',
disable_rollback=True,
parameters={},
template=expected_template,
files={},
environment=''
)
self.assertTrue(hs._applied)
@mock.patch(MOD_NAME + '.HeatStack._wait_state')
@mock.patch(MOD_NAME + '.HeatStack._get_status')
def test_description_is_optional(self, status_get, wait_st):
"""Assert that if heat_template_version is omitted, it's added."""
# Note that the 'with x as y, a as b:' syntax was introduced in
# python 2.7, and contextlib.nested was deprecated in py2.7
with mock.patch(MOD_NAME + '.HeatStack._get_status') as status_get:
with mock.patch(MOD_NAME + '.HeatStack._wait_state') as wait_st:
status_get.return_value = 'NOT_FOUND'
wait_st.return_value = {}
context = {constants.CTX_ENVIRONMENT: self.environment_mock}
status_get.return_value = 'NOT_FOUND'
wait_st.return_value = {}
context = {constants.CTX_ENVIRONMENT: self.environment_mock}
with helpers.contextual(context):
hs = heat_stack.HeatStack('test-stack', None)
hs._template = {'resources': {'test': 1}}
hs._files = {}
hs._parameters = {}
hs._applied = False
hs.push()
with helpers.contextual(context):
hs = heat_stack.HeatStack('test-stack', None)
hs._template = {'resources': {'test': 1}}
hs._files = {}
hs._hot_environment = ''
hs._parameters = {}
hs._applied = False
hs.push()
expected_template = {
'heat_template_version': '2013-05-23',
'resources': {'test': 1}
}
self.heat_client_mock.stacks.create.assert_called_with(
stack_name='test-stack',
disable_rollback=True,
parameters={},
template=expected_template,
files={}
)
self.assertTrue(hs._applied)
expected_template = {
'heat_template_version': '2013-05-23',
'resources': {'test': 1}
}
self.heat_client_mock.stacks.create.assert_called_with(
stack_name='test-stack',
disable_rollback=True,
parameters={},
template=expected_template,
files={},
environment=''
)
self.assertTrue(hs._applied)
def test_heat_files_are_sent(self):
@mock.patch(MOD_NAME + '.HeatStack._wait_state')
@mock.patch(MOD_NAME + '.HeatStack._get_status')
def test_heat_files_are_sent(self, status_get, wait_st):
"""Assert that if heat_template_version is omitted, it's added."""
# Note that the 'with x as y, a as b:' syntax was introduced in
# python 2.7, and contextlib.nested was deprecated in py2.7
with mock.patch(MOD_NAME + '.HeatStack._get_status') as status_get:
with mock.patch(MOD_NAME + '.HeatStack._wait_state') as wait_st:
status_get.return_value = 'NOT_FOUND'
wait_st.return_value = {}
context = {constants.CTX_ENVIRONMENT: self.environment_mock}
status_get.return_value = 'NOT_FOUND'
wait_st.return_value = {}
context = {constants.CTX_ENVIRONMENT: self.environment_mock}
with helpers.contextual(context):
hs = heat_stack.HeatStack('test-stack', None)
hs._description = None
hs._template = {'resources': {'test': 1}}
hs._files = {"heatFile": "file"}
hs._parameters = {}
hs._applied = False
hs.push()
with helpers.contextual(context):
hs = heat_stack.HeatStack('test-stack', None)
hs._description = None
hs._template = {'resources': {'test': 1}}
hs._files = {"heatFile": "file"}
hs._hot_environment = ''
hs._parameters = {}
hs._applied = False
hs.push()
expected_template = {
'heat_template_version': '2013-05-23',
'resources': {'test': 1}
}
self.heat_client_mock.stacks.create.assert_called_with(
stack_name='test-stack',
disable_rollback=True,
parameters={},
template=expected_template,
files={"heatFile": "file"}
)
self.assertTrue(hs._applied)
expected_template = {
'heat_template_version': '2013-05-23',
'resources': {'test': 1}
}
self.heat_client_mock.stacks.create.assert_called_with(
stack_name='test-stack',
disable_rollback=True,
parameters={},
template=expected_template,
files={"heatFile": "file"},
environment=''
)
self.assertTrue(hs._applied)
def test_update_wrong_template_version(self):
@mock.patch(MOD_NAME + '.HeatStack._wait_state')
@mock.patch(MOD_NAME + '.HeatStack._get_status')
def test_heat_environments_are_sent(self, status_get, wait_st):
"""Assert that if heat_template_version is omitted, it's added."""
status_get.return_value = 'NOT_FOUND'
wait_st.return_value = {}
context = {constants.CTX_ENVIRONMENT: self.environment_mock}
with helpers.contextual(context):
hs = heat_stack.HeatStack('test-stack', None)
hs._description = None
hs._template = {'resources': {'test': 1}}
hs._files = {"heatFile": "file"}
hs._hot_environment = 'environments'
hs._parameters = {}
hs._applied = False
hs.push()
expected_template = {
'heat_template_version': '2013-05-23',
'resources': {'test': 1}
}
self.heat_client_mock.stacks.create.assert_called_with(
stack_name='test-stack',
disable_rollback=True,
parameters={},
template=expected_template,
files={"heatFile": "file"},
environment='environments',
)
self.assertTrue(hs._applied)
@mock.patch(MOD_NAME + '.HeatStack.current')
def test_update_wrong_template_version(self, current):
"""Template version other than expected should cause error."""
context = {constants.CTX_ENVIRONMENT: self.environment_mock}
@ -161,22 +203,21 @@ class TestHeatStack(base.MuranoTestCase):
'heat_template_version': 'something else'
}
with mock.patch(MOD_NAME + '.HeatStack.current') as current:
current.return_value = {}
current.return_value = {}
e = self.assertRaises(heat_stack.HeatStackError,
hs.update_template,
invalid_template)
err_msg = "Currently only heat_template_version 2013-05-23 "\
"is supported."
self.assertEqual(err_msg, str(e))
e = self.assertRaises(heat_stack.HeatStackError,
hs.update_template,
invalid_template)
err_msg = "Currently only heat_template_version 2013-05-23 "\
"is supported."
self.assertEqual(err_msg, str(e))
# Check it's ok without a version
hs.update_template({})
expected = {'resources': {'test': 1}}
self.assertEqual(expected, hs._template)
# Check it's ok without a version
hs.update_template({})
expected = {'resources': {'test': 1}}
self.assertEqual(expected, hs._template)
# .. or with a good version
hs.update_template({'heat_template_version': '2013-05-23'})
expected['heat_template_version'] = '2013-05-23'
self.assertEqual(expected, hs._template)
# .. or with a good version
hs.update_template({'heat_template_version': '2013-05-23'})
expected['heat_template_version'] = '2013-05-23'
self.assertEqual(expected, hs._template)