deb-murano/murano/packages/hot_package.py
Alexander Tivelkov afa85b3c21 Added package references to generated UIs
Generated UIs of the non-muranoPL packages (HOT, cloudify_tosca and
CSAR) reference the type of object model's object by class FQNs.
However, when these packages are uploaded to glare, their class
contents are not indexed since the client which uploads them has no
knowledge of the generated classes.

To workaround this issue it is proposed to reference objects not just
by class FQNs but to include the package FQN and the version, so the
engine does not have to rely on Glare's indexes.

Change-Id: I6175e89b68bcdfc29d33ae3616d6ecba662f2509
Partial-bug: #1565805
2016-05-24 21:35:46 +03:00

541 lines
19 KiB
Python

# Copyright (c) 2014 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 os
import shutil
import sys
import six
import yaml
from murano.packages import exceptions
from murano.packages import package_base
RESOURCES_DIR_NAME = 'Resources/'
HOT_FILES_DIR_NAME = 'HotFiles/'
HOT_ENV_DIR_NAME = 'HotEnvironments/'
class YAQL(object):
def __init__(self, expr):
self.expr = expr
class Dumper(yaml.SafeDumper):
pass
def yaql_representer(dumper, data):
return dumper.represent_scalar(u'!yaql', data.expr)
Dumper.add_representer(YAQL, yaql_representer)
class HotPackage(package_base.PackageBase):
def __init__(self, format_name, runtime_version, source_directory,
manifest):
super(HotPackage, self).__init__(
format_name, runtime_version, source_directory, manifest)
self._translated_class = None
self._source_directory = source_directory
self._translated_ui = None
@property
def classes(self):
return self.full_name,
@property
def requirements(self):
return {}
@property
def ui(self):
if not self._translated_ui:
self._translated_ui = self._translate_ui()
return self._translated_ui
def get_class(self, name):
if name != self.full_name:
raise exceptions.PackageClassLoadError(
name, 'Class not defined in this package')
if not self._translated_class:
self._translate_class()
return self._translated_class, '<generated code>'
def _translate_class(self):
template_file = os.path.join(self._source_directory, 'template.yaml')
shutil.copy(template_file, self.get_resource(self.full_name))
if not os.path.isfile(template_file):
raise exceptions.PackageClassLoadError(
self.full_name, 'File with class definition not found')
with open(template_file) as stream:
hot = yaml.safe_load(stream)
if 'resources' not in hot:
raise exceptions.PackageFormatError('Not a HOT template')
translated = {
'Name': self.full_name,
'Extends': 'io.murano.Application'
}
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
files = HotPackage._translate_files(self._source_directory)
translated.update(HotPackage._generate_workflow(hot, files))
# use default_style with double quote mark because by default PyYAML
# doesn't put any quote marks ans as a result strings with e.g. dashes
# may be interpreted as YAQL expressions upon load
self._translated_class = yaml.dump(
translated, Dumper=Dumper, default_style='"')
@staticmethod
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',
}
}
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_param_to_contract(value):
contract = '$'
parameter_type = value['type']
if parameter_type in ('string', 'comma_delimited_list', 'json'):
contract += '.string()'
elif parameter_type == 'number':
contract += '.int()'
elif parameter_type == 'boolean':
contract += '.bool()'
else:
raise ValueError('Unsupported parameter type ' + parameter_type)
constraints = value.get('constraints') or []
for constraint in constraints:
translated = HotPackage._translate_constraint(constraint)
if translated:
contract += translated
result = YAQL(contract)
return result
@staticmethod
def _translate_outputs(hot):
contract = {}
for key in six.iterkeys(hot.get('outputs') or {}):
contract[key] = YAQL("$.string()")
return {
'templateOutputs': {
'Contract': contract,
'Default': {},
'Usage': 'Out'
}
}
@staticmethod
def _translate_files(source_directory):
hot_files_path = os.path.join(source_directory,
RESOURCES_DIR_NAME,
HOT_FILES_DIR_NAME)
return HotPackage._build_hot_resources(hot_files_path)
@staticmethod
def _build_hot_resources(basedir):
result = []
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
def _translate_constraint(constraint):
if 'allowed_values' in constraint:
return HotPackage._translate_allowed_values_constraint(
constraint['allowed_values'])
elif 'length' in constraint:
return HotPackage._translate_length_constraint(
constraint['length'])
elif 'range' in constraint:
return HotPackage._translate_range_constraint(
constraint['range'])
elif 'allowed_pattern' in constraint:
return HotPackage._translate_allowed_pattern_constraint(
constraint['allowed_pattern'])
@staticmethod
def _translate_allowed_pattern_constraint(value):
return ".check(matches($, '{0}'))".format(value)
@staticmethod
def _translate_allowed_values_constraint(values):
return '.check($ in list({0}))'.format(
', '.join([HotPackage._format_value(v) for v in values]))
@staticmethod
def _translate_length_constraint(value):
if 'min' in value and 'max' in value:
return '.check(len($) >= {0} and len($) <= {1})'.format(
int(value['min']), int(value['max']))
elif 'min' in value:
return '.check(len($) >= {0})'.format(int(value['min']))
elif 'max' in value:
return '.check(len($) <= {0})'.format(int(value['max']))
@staticmethod
def _translate_range_constraint(value):
if 'min' in value and 'max' in value:
return '.check($ >= {0} and $ <= {1})'.format(
int(value['min']), int(value['max']))
elif 'min' in value:
return '.check($ >= {0})'.format(int(value['min']))
elif 'max' in value:
return '.check($ <= {0})'.format(int(value['max']))
@staticmethod
def _format_value(value):
if isinstance(value, six.string_types):
return str("'" + value + "'")
return str(value)
@staticmethod
def _generate_workflow(hot, files):
hot_files_map = {}
for f in files:
file_path = "$resources.string('{0}{1}')".format(
HOT_FILES_DIR_NAME, f)
hot_files_map[f] = YAQL(file_path)
hot_env = YAQL("$.hotEnvironment")
deploy = [
{YAQL('$environment'): YAQL(
"$.find('io.murano.Environment').require()"
)},
{YAQL('$reporter'): YAQL(
"new('io.murano.system.StatusReporter', "
"environment => $environment)")},
{
'If': YAQL('$.getAttr(generatedHeatStackName) = null'),
'Then': [
YAQL("$.setAttr(generatedHeatStackName, "
"'{0}_{1}'.format(randomName(), id($environment)))")
]
},
{YAQL('$stack'): YAQL(
"new('io.murano.system.HeatStack', $environment, "
"name => $.getAttr(generatedHeatStackName))")},
YAQL("$reporter.report($this, "
"'Application deployment has started')"),
{YAQL('$resources'): YAQL("new('io.murano.system.Resources')")},
{YAQL('$template'): YAQL("$resources.yaml(type($this))")},
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')"),
{
'Try': [YAQL('$stack.push()')],
'Catch': [
{
'As': 'e',
'Do': [
YAQL("$reporter.report_error($this, $e.message)"),
{'Rethrow': None}
]
}
],
'Else': [
{YAQL('$.templateOutputs'): YAQL('$stack.output()')},
YAQL("$reporter.report($this, "
"'Stack was successfully created')"),
YAQL("$reporter.report($this, "
"'Application deployment has finished')"),
]
}
]
destroy = [
{YAQL('$environment'): YAQL(
"$.find('io.murano.Environment').require()"
)},
{YAQL('$stack'): YAQL(
"new('io.murano.system.HeatStack', $environment, "
"name => $.getAttr(generatedHeatStackName))")},
YAQL('$stack.delete()')
]
return {
'Methods': {
'deploy': {
'Body': deploy
},
'destroy': {
'Body': destroy
}
}
}
@staticmethod
def _translate_ui_parameters(hot, title):
groups = hot.get('parameter_groups', [])
result_groups = []
predefined_fields = [
{
'name': 'title',
'type': 'string',
'required': False,
'hidden': True,
'description': title
},
{
'name': 'name',
'type': 'string',
'label': 'Application Name',
'required': True,
'description':
'Enter a desired name for the application.'
' Just A-Z, a-z, 0-9, and dash are allowed'
}
]
used_parameters = set()
hot_parameters = hot.get('parameters') or {}
for group in groups:
fields = []
properties = []
for parameter in group.get('parameters', []):
parameter_value = hot_parameters.get(parameter)
if parameter_value:
fields.append(HotPackage._translate_ui_parameter(
parameter, parameter_value))
used_parameters.add(parameter)
properties.append(parameter)
result_groups.append((fields, properties))
rest_group = []
properties = []
for key, value in six.iteritems(hot_parameters):
if key not in used_parameters:
rest_group.append(HotPackage._translate_ui_parameter(
key, value))
properties.append(key)
if rest_group:
result_groups.append((rest_group, properties))
result_groups.insert(0, (predefined_fields, ['name']))
return result_groups
@staticmethod
def _translate_ui_parameter(name, parameter_spec):
translated = {
'name': name,
'label': name.title().replace('_', ' ')
}
parameter_type = parameter_spec['type']
if parameter_type == 'number':
translated['type'] = 'integer'
elif parameter_type == 'boolean':
translated['type'] = 'boolean'
else:
# string, json, and comma_delimited_list parameters are all
# displayed as strings in UI. Any unsupported parameter would also
# be displayed as strings.
translated['type'] = 'string'
label = parameter_spec.get('label')
if label:
translated['label'] = label
if 'description' in parameter_spec:
translated['description'] = parameter_spec['description']
if 'default' in parameter_spec:
translated['initial'] = parameter_spec['default']
translated['required'] = False
else:
translated['required'] = True
constraints = parameter_spec.get('constraints') or []
translated_constraints = []
for constraint in constraints:
if 'length' in constraint:
spec = constraint['length']
if 'min' in spec:
translated['minLength'] = max(
translated.get('minLength', -sys.maxint - 1),
int(spec['min']))
if 'max' in spec:
translated['maxLength'] = min(
translated.get('maxLength', sys.maxint),
int(spec['max']))
elif 'range' in constraint:
spec = constraint['range']
if 'min' in spec and 'max' in spec:
ui_constraint = {
'expr': YAQL('$ >= {0} and $ <= {1}'.format(
spec['min'], spec['max']))
}
elif 'min' in spec:
ui_constraint = {
'expr': YAQL('$ >= {0}'.format(spec['min']))
}
else:
ui_constraint = {
'expr': YAQL('$ <= {0}'.format(spec['max']))
}
if 'description' in constraint:
ui_constraint['message'] = constraint['description']
translated_constraints.append(ui_constraint)
elif 'allowed_values' in constraint:
values = constraint['allowed_values']
ui_constraint = {
'expr': YAQL('$ in list({0})'.format(', '.join(
[HotPackage._format_value(v) for v in values])))
}
if 'description' in constraint:
ui_constraint['message'] = constraint['description']
translated_constraints.append(ui_constraint)
elif 'allowed_pattern' in constraint:
pattern = constraint['allowed_pattern']
ui_constraint = {
'expr': {
'regexpValidator': pattern
}
}
if 'description' in constraint:
ui_constraint['message'] = constraint['description']
translated_constraints.append(ui_constraint)
if translated_constraints:
translated['validators'] = translated_constraints
return translated
@staticmethod
def _generate_application_ui(groups, type_name,
package_name=None, package_version=None):
app = {
'?': {
'type': type_name
}
}
if package_name:
app['?']['package'] = package_name
if package_version:
app['?']['classVersion'] = package_version
for i, record in enumerate(groups):
if i == 0:
section = app
else:
section = app.setdefault('templateParameters', {})
for property_name in record[1]:
section[property_name] = YAQL(
'$.group{0}.{1}'.format(i, property_name))
app['name'] = YAQL('$.group0.name')
return app
def _translate_ui(self):
template_file = os.path.join(self._source_directory, 'template.yaml')
if not os.path.isfile(template_file):
raise exceptions.PackageClassLoadError(
self.full_name, 'File with class definition not found')
with open(template_file) as stream:
hot = yaml.safe_load(stream)
groups = HotPackage._translate_ui_parameters(hot, self.description)
forms = []
for i, record in enumerate(groups):
forms.append({'group{0}'.format(i): {'fields': record[0]}})
translated = {
'Version': 2,
'Application': HotPackage._generate_application_ui(
groups, self.full_name, self.full_name, str(self.version)),
'Forms': forms
}
# see comment above about default_style
return yaml.dump(translated, Dumper=Dumper, default_style='"')