Merge "Preliminary support for HOT packages"

This commit is contained in:
Jenkins 2014-06-03 09:57:34 +00:00 committed by Gerrit Code Review
commit 852a21ae4e
17 changed files with 811 additions and 197 deletions

View File

@ -29,8 +29,8 @@ from murano.openstack.common import exception
from murano.openstack.common.gettextutils import _ # noqa from murano.openstack.common.gettextutils import _ # noqa
from murano.openstack.common import log as logging from murano.openstack.common import log as logging
from murano.openstack.common import wsgi from murano.openstack.common import wsgi
from murano.packages import application_package as app_pkg
from murano.packages import exceptions as pkg_exc from murano.packages import exceptions as pkg_exc
from murano.packages import load_utils
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
CONF = cfg.CONF CONF = cfg.CONF
@ -201,14 +201,13 @@ class Controller(object):
tempf.write(content) tempf.write(content)
package_meta['archive'] = content package_meta['archive'] = content
try: try:
LOG.debug("Deleting package archive temporary file") pkg_to_upload = load_utils.load_from_file(
pkg_to_upload = app_pkg.load_from_file(tempf.name, tempf.name, target_dir=None, drop_dir=True)
target_dir=None,
drop_dir=True)
except pkg_exc.PackageLoadError as e: except pkg_exc.PackageLoadError as e:
LOG.exception(e) LOG.exception(e)
raise exc.HTTPBadRequest(e) raise exc.HTTPBadRequest(e)
finally: finally:
LOG.debug("Deleting package archive temporary file")
os.remove(tempf.name) os.remove(tempf.name)
# extend dictionary for update db # extend dictionary for update db

View File

@ -27,7 +27,7 @@ from murano.db.catalog import api as db_catalog_api
from murano.db import session as db_session from murano.db import session as db_session
from murano.openstack.common.db import exception as db_exception from murano.openstack.common.db import exception as db_exception
from murano.openstack.common import log as logging from murano.openstack.common import log as logging
from murano.packages import application_package from murano.packages import load_utils
CONF = cfg.CONF CONF = cfg.CONF
@ -50,7 +50,7 @@ class AdminContext(object):
def _do_import_package(_dir, categories, update=False): def _do_import_package(_dir, categories, update=False):
LOG.info("Going to import Murano package from {0}".format(_dir)) LOG.info("Going to import Murano package from {0}".format(_dir))
pkg = application_package.load_from_dir(_dir) pkg = load_utils.load_from_dir(_dir)
LOG.info("Checking for existing") LOG.info("Checking for existing")
existing = db_catalog_api.package_search( existing = db_catalog_api.package_search(

View File

@ -73,10 +73,11 @@ class TaskProcessingEndpoint(object):
# TODO(slagun) code below needs complete rewrite and redesign # TODO(slagun) code below needs complete rewrite and redesign
LOG.exception("Error during task execution for tenant %s", LOG.exception("Error during task execution for tenant %s",
env.tenant_id) env.tenant_id)
msg_env = Environment(task['model']['Objects']['?']['id']) if task['model']['Objects']:
reporter = status_reporter.StatusReporter() msg_env = Environment(task['model']['Objects']['?']['id'])
reporter.initialize(msg_env) reporter = status_reporter.StatusReporter()
reporter.report_error(msg_env, '{0}'.format(e)) reporter.initialize(msg_env)
reporter.report_error(msg_env, '{0}'.format(e))
rpc.api().process_result(task['model']) rpc.api().process_result(task['model'])

View File

@ -43,13 +43,12 @@ class ResultEndpoint(object):
LOG.debug('Got result from orchestration ' LOG.debug('Got result from orchestration '
'engine:\n{0}'.format(secure_result)) 'engine:\n{0}'.format(secure_result))
result_id = result['Objects']['?']['id'] if not result['Objects']:
LOG.debug('Ignoring result for deleted environment')
if 'deleted' in result:
LOG.debug('Result for environment {0} is dropped. Environment '
'is deleted'.format(result_id))
return return
result_id = result['Objects']['?']['id']
unit = session.get_session() unit = session.get_session()
environment = unit.query(models.Environment).get(result_id) environment = unit.query(models.Environment).get(result_id)

View File

@ -233,7 +233,7 @@ class MuranoDslExecutor(object):
if not isinstance(data, types.DictionaryType): if not isinstance(data, types.DictionaryType):
raise TypeError() raise TypeError()
self._attribute_store.load(data.get('Attributes') or []) self._attribute_store.load(data.get('Attributes') or [])
result = self._object_store.load(data.get('Objects') or {}, result = self._object_store.load(data.get('Objects'),
None, self._root_context) None, self._root_context)
self.cleanup(data) self.cleanup(data)
return result return result

View File

@ -129,6 +129,11 @@ def _sleep(seconds):
eventlet.sleep(seconds) eventlet.sleep(seconds)
@yaql.context.EvalArg('value', murano_object.MuranoObject)
def _type(value):
return value.type.name
def register(context): def register(context):
context.register_function(_resolve, '#resolve') context.register_function(_resolve, '#resolve')
context.register_function(_cast, 'cast') context.register_function(_cast, 'cast')
@ -140,3 +145,4 @@ def register(context):
context.register_function(_require, 'require') context.register_function(_require, 'require')
context.register_function(_get_container, 'find') context.register_function(_get_container, 'find')
context.register_function(_sleep, 'sleep') context.register_function(_sleep, 'sleep')
context.register_function(_type, 'type')

View File

@ -23,31 +23,17 @@ from keystoneclient.v2_0 import client as keystoneclient
from muranoclient.common import exceptions as muranoclient_exc from muranoclient.common import exceptions as muranoclient_exc
from muranoclient.v1 import client as muranoclient from muranoclient.v1 import client as muranoclient
import six import six
import yaml
from murano.common import config from murano.common import config
from murano.dsl import exceptions from murano.dsl import exceptions
from murano.dsl import yaql_expression from murano.engine import yaql_yaml_loader
from murano.openstack.common import log as logging from murano.openstack.common import log as logging
from murano.packages import application_package as app_pkg
from murano.packages import exceptions as pkg_exc from murano.packages import exceptions as pkg_exc
from murano.packages import load_utils
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
class YaqlYamlLoader(yaml.Loader):
pass
def yaql_constructor(loader, node):
value = loader.construct_scalar(node)
return yaql_expression.YaqlExpression(value)
yaml.add_constructor(u'!yaql', yaql_constructor, YaqlYamlLoader)
yaml.add_implicit_resolver(u'!yaql', yaql_expression.YaqlExpression,
Loader=YaqlYamlLoader)
class PackageLoader(six.with_metaclass(abc.ABCMeta)): class PackageLoader(six.with_metaclass(abc.ABCMeta)):
@abc.abstractmethod @abc.abstractmethod
def get_package(self, name): def get_package(self, name):
@ -149,34 +135,36 @@ class ApiPackageLoader(PackageLoader):
if os.path.exists(package_directory): if os.path.exists(package_directory):
try: try:
return app_pkg.load_from_dir(package_directory, preload=True, return load_utils.load_from_dir(
loader=YaqlYamlLoader) package_directory, preload=True,
loader=yaql_yaml_loader.YaqlYamlLoader)
except pkg_exc.PackageLoadError: except pkg_exc.PackageLoadError:
LOG.exception('Unable to load package from cache. Clean-up...') LOG.exception('Unable to load package from cache. Clean-up...')
shutil.rmtree(package_directory, ignore_errors=True) shutil.rmtree(package_directory, ignore_errors=True)
try: try:
package_data = self._client.packages.download(package_id) package_data = self._client.packages.download(package_id)
except muranoclient_exc.HTTPException: except muranoclient_exc.HTTPException:
LOG.exception('Unable to download ' LOG.exception('Unable to download '
'package with id {0}'.format(package_id)) 'package with id {0}'.format(package_id))
raise pkg_exc.PackageLoadError() raise pkg_exc.PackageLoadError()
package_file = None
try: try:
with tempfile.NamedTemporaryFile(delete=False) as package_file: with tempfile.NamedTemporaryFile(delete=False) as package_file:
package_file.write(package_data) package_file.write(package_data)
return app_pkg.load_from_file( return load_utils.load_from_file(
package_file.name, package_file.name,
target_dir=package_directory, target_dir=package_directory,
drop_dir=False, drop_dir=False,
loader=YaqlYamlLoader loader=yaql_yaml_loader.YaqlYamlLoader
) )
except IOError: except IOError:
LOG.exception('Unable to write package file') LOG.exception('Unable to write package file')
raise pkg_exc.PackageLoadError() raise pkg_exc.PackageLoadError()
finally: finally:
try: try:
os.remove(package_file.name) if package_file:
os.remove(package_file.name)
except OSError: except OSError:
pass pass
@ -213,8 +201,9 @@ class DirectoryPackageLoader(PackageLoader):
continue continue
try: try:
package = app_pkg.load_from_dir(folder, preload=True, package = load_utils.load_from_dir(
loader=YaqlYamlLoader) folder, preload=True,
loader=yaql_yaml_loader.YaqlYamlLoader)
except pkg_exc.PackageLoadError: except pkg_exc.PackageLoadError:
LOG.exception('Unable to load package from path: ' LOG.exception('Unable to load package from path: '
'{0}'.format(entry)) '{0}'.format(entry))

View File

@ -26,7 +26,7 @@ import murano.dsl.murano_class as murano_class
import murano.dsl.murano_object as murano_object import murano.dsl.murano_object as murano_object
import murano.openstack.common.log as logging import murano.openstack.common.log as logging
log = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@murano_class.classname('io.murano.system.HeatStack') @murano_class.classname('io.murano.system.HeatStack')
@ -103,6 +103,10 @@ class HeatStack(murano_object.MuranoObject):
self._parameters.clear() self._parameters.clear()
self._applied = False self._applied = False
def setParameters(self, parameters):
self._parameters = parameters
self._applied = False
def updateTemplate(self, template): def updateTemplate(self, template):
self.current() self.current()
self._template = helpers.merge_dicts(self._template, template) self._template = helpers.merge_dicts(self._template, template)
@ -161,13 +165,13 @@ class HeatStack(murano_object.MuranoObject):
if self._applied or self._template is None: if self._applied or self._template is None:
return return
log.info('Pushing: {0}'.format(self._template)) LOG.info('Pushing: {0}'.format(self._template))
current_status = self._get_status() current_status = self._get_status()
resources = self._template.get('Resources') or \
self._template.get('resources')
if current_status == 'NOT_FOUND': if current_status == 'NOT_FOUND':
# For now, allow older CFN style templates as well, but this if resources:
# should be removed to avoid mixing them
if 'resources' in self._template or 'Resources' in self._template:
self._heat_client.stacks.create( self._heat_client.stacks.create(
stack_name=self._name, stack_name=self._name,
parameters=self._parameters, parameters=self._parameters,
@ -177,9 +181,7 @@ class HeatStack(murano_object.MuranoObject):
self._wait_state( self._wait_state(
lambda status: status == 'CREATE_COMPLETE') lambda status: status == 'CREATE_COMPLETE')
else: else:
# For now, allow older CFN style templates as well, but this if resources:
# should be removed to avoid mixing them
if 'resources' in self._template or 'Resources' in self._template:
self._heat_client.stacks.update( self._heat_client.stacks.update(
stack_id=self._name, stack_id=self._name,
parameters=self._parameters, parameters=self._parameters,

View File

@ -18,6 +18,30 @@ import yaml as yamllib
import murano.dsl.murano_object as murano_object import murano.dsl.murano_object as murano_object
if hasattr(yamllib, 'CSafeLoader'):
yaml_loader = yamllib.CSafeLoader
else:
yaml_loader = yamllib.SafeLoader
if hasattr(yamllib, 'CSafeDumper'):
yaml_dumper = yamllib.CSafeDumper
else:
yaml_dumper = yamllib.SafeDumper
def _construct_yaml_str(self, node):
# Override the default string handling function
# to always return unicode objects
return self.construct_scalar(node)
yaml_loader.add_constructor(u'tag:yaml.org,2002:str', _construct_yaml_str)
# Unquoted dates like 2013-05-23 in yaml files get loaded as objects of type
# datetime.data which causes problems in API layer when being processed by
# openstack.common.jsonutils. Therefore, make unicode string out of timestamps
# until jsonutils can handle dates.
yaml_loader.add_constructor(u'tag:yaml.org,2002:timestamp',
_construct_yaml_str)
class ResourceManager(murano_object.MuranoObject): class ResourceManager(murano_object.MuranoObject):
def initialize(self, package_loader, _context, _class): def initialize(self, package_loader, _context, _class):

View File

@ -15,7 +15,10 @@
import base64 import base64
import collections import collections
import random
import re import re
import string
import time
import types import types
import jsonpatch import jsonpatch
@ -27,6 +30,9 @@ import murano.common.config as cfg
import murano.dsl.helpers as helpers import murano.dsl.helpers as helpers
_random_string_counter = None
def _transform_json(json, mappings): def _transform_json(json, mappings):
if isinstance(json, types.ListType): if isinstance(json, types.ListType):
return [_transform_json(t, mappings) for t in json] return [_transform_json(t, mappings) for t in json]
@ -204,6 +210,55 @@ def _patch(obj, patch):
return obj return obj
def _int2base(x, base):
"""Converts decimal integers into another number base
from base-2 to base-36.
:param x: decimal integer
:param base: number base, max value is 36
:return: integer converted to the specified base
"""
digs = string.digits + string.lowercase
if x < 0:
sign = -1
elif x == 0:
return '0'
else:
sign = 1
x *= sign
digits = []
while x:
digits.append(digs[x % base])
x /= base
if sign < 0:
digits.append('-')
digits.reverse()
return ''.join(digits)
def _random_name():
"""Replace '#' char in pattern with supplied number, if no pattern is
supplied generate short and unique name for the host.
:param pattern: hostname pattern
:param number: number to replace with in pattern
:return: hostname
"""
global _random_string_counter
counter = _random_string_counter or 1
# generate first 5 random chars
prefix = ''.join(random.choice(string.lowercase) for _ in range(5))
# convert timestamp to higher base to shorten hostname string
# (up to 8 chars)
timestamp = _int2base(int(time.time() * 1000), 36)[:8]
# third part of random name up to 2 chars
# (1295 is last 2-digit number in base-36, 1296 is first 3-digit number)
suffix = _int2base(counter, 36)
_random_string_counter = (counter + 1) % 1296
return prefix + timestamp + suffix
@yaql.context.EvalArg('self', dict) @yaql.context.EvalArg('self', dict)
def _values(self): def _values(self):
return self.values() return self.values()
@ -250,6 +305,7 @@ def register(context):
context.register_function(_str, 'str') context.register_function(_str, 'str')
context.register_function(_int, 'int') context.register_function(_int, 'int')
context.register_function(_patch, 'patch') context.register_function(_patch, 'patch')
context.register_function(_random_name, 'randomName')
# Temporary workaround as YAQL does not provide "where" function for # Temporary workaround as YAQL does not provide "where" function for
# dictionaries, and there is no easy way to implement it there. # dictionaries, and there is no easy way to implement it there.
context.register_function(yaql_builtin.dict_attribution, 'get') context.register_function(yaql_builtin.dict_attribution, 'get')

View File

@ -0,0 +1,37 @@
# 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 yaml
from murano.dsl import yaql_expression
class YaqlYamlLoader(yaml.Loader):
pass
# workaround for PyYAML bug: http://pyyaml.org/ticket/221
resolvers = {}
for k, v in yaml.Loader.yaml_implicit_resolvers.items():
resolvers[k] = v[:]
YaqlYamlLoader.yaml_implicit_resolvers = resolvers
def yaql_constructor(loader, node):
value = loader.construct_scalar(node)
return yaql_expression.YaqlExpression(value)
yaml.add_constructor(u'!yaql', yaql_constructor, YaqlYamlLoader)
yaml.add_implicit_resolver(u'!yaql', yaql_expression.YaqlExpression,
Loader=YaqlYamlLoader)

View File

@ -16,26 +16,10 @@
import imghdr import imghdr
import io import io
import os import os
import shutil
import sys import sys
import tempfile
import yaml
import zipfile import zipfile
import murano.packages.exceptions as e import murano.packages.exceptions as e
import murano.packages.versions.v1
class DummyLoader(yaml.Loader):
pass
def yaql_constructor(loader, node):
value = loader.construct_scalar(node)
return value
yaml.add_constructor(u'!yaql', yaql_constructor, DummyLoader)
class PackageTypes(object): class PackageTypes(object):
@ -45,7 +29,7 @@ class PackageTypes(object):
class ApplicationPackage(object): class ApplicationPackage(object):
def __init__(self, source_directory, manifest, loader=DummyLoader): def __init__(self, source_directory, manifest, loader):
self.yaml_loader = loader self.yaml_loader = loader
self._source_directory = source_directory self._source_directory = source_directory
self._full_name = None self._full_name = None
@ -54,14 +38,9 @@ class ApplicationPackage(object):
self._description = None self._description = None
self._author = None self._author = None
self._tags = None self._tags = None
self._classes = None
self._ui = None
self._logo = None self._logo = None
self._format = manifest.get('Format') self._format = manifest.get('Format')
self._ui_cache = None
self._raw_ui_cache = None
self._logo_cache = None self._logo_cache = None
self._classes_cache = {}
self._blob_cache = None self._blob_cache = None
@property @property
@ -88,22 +67,6 @@ class ApplicationPackage(object):
def tags(self): def tags(self):
return tuple(self._tags) return tuple(self._tags)
@property
def classes(self):
return tuple(self._classes.keys())
@property
def ui(self):
if not self._ui_cache:
self._load_ui(True)
return self._ui_cache
@property
def raw_ui(self):
if not self._raw_ui_cache:
self._load_ui(False)
return self._raw_ui_cache
@property @property
def logo(self): def logo(self):
if not self._logo_cache: if not self._logo_cache:
@ -116,42 +79,15 @@ class ApplicationPackage(object):
self._blob_cache = _pack_dir(self._source_directory) self._blob_cache = _pack_dir(self._source_directory)
return self._blob_cache return self._blob_cache
def get_class(self, name):
if name not in self._classes_cache:
self._load_class(name)
return self._classes_cache[name]
def get_resource(self, name): def get_resource(self, name):
return os.path.join(self._source_directory, 'Resources', name) resources_dir = os.path.join(self._source_directory, 'Resources')
if not os.path.exists(resources_dir):
os.makedirs(resources_dir)
return os.path.join(resources_dir, name)
def validate(self): def validate(self):
self._classes_cache.clear()
for class_name in self._classes:
self.get_class(class_name)
self._load_ui(True)
self._load_logo(True) self._load_logo(True)
# Private methods
def _load_ui(self, load_yaml=False):
if self._raw_ui_cache and load_yaml:
self._ui_cache = yaml.load(self._raw_ui_cache, self.yaml_loader)
else:
ui_file = self._ui
full_path = os.path.join(self._source_directory, 'UI', ui_file)
if not os.path.isfile(full_path):
self._raw_ui_cache = None
self._ui_cache = None
return
try:
with open(full_path) as stream:
self._raw_ui_cache = stream.read()
if load_yaml:
self._ui_cache = yaml.load(self._raw_ui_cache,
self.yaml_loader)
except Exception as ex:
trace = sys.exc_info()[2]
raise e.PackageUILoadError(str(ex)), None, trace
def _load_logo(self, validate=False): def _load_logo(self, validate=False):
logo_file = self._logo or 'logo.png' logo_file = self._logo or 'logo.png'
full_path = os.path.join(self._source_directory, logo_file) full_path = os.path.join(self._source_directory, logo_file)
@ -169,54 +105,6 @@ class ApplicationPackage(object):
raise e.PackageLoadError( raise e.PackageLoadError(
"Unable to load logo: " + str(ex)), None, trace "Unable to load logo: " + str(ex)), None, trace
def _load_class(self, name):
if name not in self._classes:
raise e.PackageClassLoadError(name, 'Class not defined '
'in this package')
def_file = self._classes[name]
full_path = os.path.join(self._source_directory, 'Classes', def_file)
if not os.path.isfile(full_path):
raise e.PackageClassLoadError(name, 'File with class '
'definition not found')
try:
with open(full_path) as stream:
self._classes_cache[name] = yaml.load(stream, self.yaml_loader)
except Exception as ex:
trace = sys.exc_info()[2]
msg = 'Unable to load class definition due to "{0}"'.format(
str(ex))
raise e.PackageClassLoadError(name, msg), None, trace
def load_from_dir(source_directory, filename='manifest.yaml', preload=False,
loader=DummyLoader):
formats = {'1.0': murano.packages.versions.v1}
if not os.path.isdir(source_directory) or not os.path.exists(
source_directory):
raise e.PackageLoadError('Invalid package directory')
full_path = os.path.join(source_directory, filename)
if not os.path.isfile(full_path):
raise e.PackageLoadError('Unable to find package manifest')
try:
with open(full_path) as stream:
content = yaml.load(stream, DummyLoader)
except Exception as ex:
trace = sys.exc_info()[2]
raise e.PackageLoadError(
"Unable to load due to '{0}'".format(str(ex))), None, trace
if content:
p_format = str(content.get('Format'))
if not p_format or p_format not in formats:
raise e.PackageFormatError(
'Unknown or missing format version')
package = ApplicationPackage(source_directory, content, loader)
formats[p_format].load(package, content)
if preload:
package.validate()
return package
def _zipdir(path, zipf): def _zipdir(path, zipf):
for root, dirs, files in os.walk(path): for root, dirs, files in os.walk(path):
@ -233,34 +121,3 @@ def _pack_dir(source_directory):
zipf.close() zipf.close()
return blob.getvalue() return blob.getvalue()
def load_from_file(archive_path, target_dir=None, drop_dir=False,
loader=DummyLoader):
if not os.path.isfile(archive_path):
raise e.PackageLoadError('Unable to find package file')
created = False
if not target_dir:
target_dir = tempfile.mkdtemp()
created = True
elif not os.path.exists(target_dir):
os.mkdir(target_dir)
created = True
else:
if os.listdir(target_dir):
raise e.PackageLoadError('Target directory is not empty')
try:
if not zipfile.is_zipfile(archive_path):
raise e.PackageFormatError("Uploading file should be a "
"zip' archive")
package = zipfile.ZipFile(archive_path)
package.extractall(path=target_dir)
return load_from_dir(target_dir, preload=True, loader=loader)
finally:
if drop_dir:
if created:
shutil.rmtree(target_dir)
else:
for f in os.listdir(target_dir):
os.unlink(os.path.join(target_dir, f))

View File

@ -0,0 +1,389 @@
# 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 types
import yaml
from murano.dsl import yaql_expression
import murano.packages.application_package
from murano.packages import exceptions
YAQL = yaql_expression.YaqlExpression
class Dumper(yaml.Dumper):
pass
def yaql_representer(dumper, data):
return dumper.represent_scalar(u'!yaql', str(data))
Dumper.add_representer(YAQL, yaql_representer)
class HotPackage(murano.packages.application_package.ApplicationPackage):
def __init__(self, source_directory, manifest, loader):
super(HotPackage, self).__init__(source_directory, manifest, loader)
self._translated_class = None
self._source_directory = source_directory
self._translated_ui = None
@property
def classes(self):
return self.full_name,
@property
def ui(self):
if not self._translated_ui:
self._translated_ui = self._translate_ui()
return self._translated_ui
@property
def raw_ui(self):
ui_obj = self.ui
result = yaml.dump(ui_obj, Dumper=Dumper, default_style='"')
return result
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
def validate(self):
self.get_class(self.full_name)
if not self._translated_ui:
self._translated_ui = self._translate_ui()
super(HotPackage, self).validate()
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'
}
parameters = HotPackage._translate_parameters(hot)
parameters.update(HotPackage._translate_outputs(hot))
translated['Properties'] = parameters
translated.update(HotPackage._generate_workflow(hot))
self._translated_class = translated
@staticmethod
def _translate_parameters(hot):
result = {
'generatedHeatStackName': {
'Contract': YAQL('$.string()'),
'Usage': 'Out'
}
}
for key, value in (hot.get('parameters') or {}).items():
result[key] = HotPackage._translate_parameter(value)
result['name'] = {'Usage': 'In',
'Contract': YAQL('$.string().notNull()')}
return result
@staticmethod
def _translate_parameter(value):
contract = '$'
parameter_type = value['type']
if parameter_type == 'string':
contract += '.string()'
elif parameter_type == 'number':
contract += '.int()'
elif parameter_type == 'json':
contract += '.object()'
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 = {
'Contract': YAQL(contract),
"Usage": "In"
}
if 'default' in value:
result['Default'] = value['default']
return result
@staticmethod
def _translate_outputs(hot):
result = {}
for key, value in (hot.get('outputs') or {}).items():
result[key] = {
"Contract": YAQL("$.string()"),
"Usage": "Out"
}
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, types.StringTypes):
return str("'" + value + "'")
return str(value)
@staticmethod
def _generate_workflow(hot):
template_parameters = {}
for key, value in (hot.get('parameters') or {}).items():
template_parameters[key] = YAQL("$." + key)
deploy = [
{
'If': YAQL('$.generatedHeatStackName = null'),
'Then': [
{YAQL('$.generatedHeatStackName'): YAQL('randomName()')}
]
},
{YAQL('$stack'): YAQL(
"new('io.murano.system.HeatStack', "
"name => $.generatedHeatStackName)")},
{YAQL('$resources'): YAQL("new('io.murano.system.Resources')")},
{YAQL('$template'): YAQL("$resources.yaml(type($this))")},
{YAQL('$parameters'): template_parameters},
YAQL('$stack.setTemplate($template)'),
YAQL('$stack.setParameters($parameters)'),
YAQL('$stack.push()'),
{YAQL('$outputs'): YAQL('$stack.output()')}
]
for key, value in (hot.get('outputs') or {}).items():
deploy.append({YAQL('$.' + key): YAQL(
'$outputs.' + key)})
destroy = [
{YAQL('$stack'): YAQL(
"new('io.murano.system.HeatStack', "
"name => $.generatedHeatStackName)")},
YAQL('$stack.delete()')
]
return {
'Workflow': {
'deploy': {
'Body': deploy
},
'destroy': {
'Body': destroy
}
}
}
@staticmethod
def _translate_ui_parameters(hot, title):
result = [
{
'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'
}
]
for key, value in (hot.get('parameters') or {}).items():
result.append(HotPackage._translate_ui_parameter(key, value))
return result
@staticmethod
def _translate_ui_parameter(name, parameter_spec):
translated = {
'name': name,
'label': name.title().replace('_', ' ')
}
parameter_type = parameter_spec['type']
if parameter_type == 'string':
translated['type'] = 'string'
elif parameter_type == 'number':
translated['type'] = 'integer'
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(hot, type_name):
app = {
'?': {
'type': type_name
}
}
for key in (hot.get('parameters') or {}).keys():
app[key] = YAQL('$.appConfiguration.' + key)
app['name'] = YAQL('$.appConfiguration.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)
translated = {
'Version': 2,
'Application': HotPackage._generate_application_ui(
hot, self.full_name),
'Forms': [
{
'appConfiguration': {
'fields': HotPackage._translate_ui_parameters(
hot, self.description)
}
}
]
}
return translated

View File

@ -0,0 +1,91 @@
# 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 tempfile
import yaml
import zipfile
from murano.engine import yaql_yaml_loader
import murano.packages.application_package
import murano.packages.exceptions as e
import murano.packages.versions.hot_v1
import murano.packages.versions.mpl_v1
def load_from_file(archive_path, target_dir=None, drop_dir=False,
loader=yaql_yaml_loader.YaqlYamlLoader):
if not os.path.isfile(archive_path):
raise e.PackageLoadError('Unable to find package file')
created = False
if not target_dir:
target_dir = tempfile.mkdtemp()
created = True
elif not os.path.exists(target_dir):
os.mkdir(target_dir)
created = True
else:
if os.listdir(target_dir):
raise e.PackageLoadError('Target directory is not empty')
try:
if not zipfile.is_zipfile(archive_path):
raise e.PackageFormatError("Uploading file should be a "
"zip' archive")
package = zipfile.ZipFile(archive_path)
package.extractall(path=target_dir)
return load_from_dir(target_dir, preload=True, loader=loader)
finally:
if drop_dir:
if created:
shutil.rmtree(target_dir)
else:
for f in os.listdir(target_dir):
os.unlink(os.path.join(target_dir, f))
def load_from_dir(source_directory, filename='manifest.yaml', preload=False,
loader=yaql_yaml_loader.YaqlYamlLoader):
formats = {
'1.0': murano.packages.versions.mpl_v1,
'MuranoPL/1.0': murano.packages.versions.mpl_v1,
'Heat.HOT/1.0': murano.packages.versions.hot_v1
}
if not os.path.isdir(source_directory) or not os.path.exists(
source_directory):
raise e.PackageLoadError('Invalid package directory')
full_path = os.path.join(source_directory, filename)
if not os.path.isfile(full_path):
raise e.PackageLoadError('Unable to find package manifest')
try:
with open(full_path) as stream:
content = yaml.safe_load(stream)
except Exception as ex:
trace = sys.exc_info()[2]
raise e.PackageLoadError(
"Unable to load due to '{0}'".format(str(ex))), None, trace
if content:
p_format = str(content.get('Format'))
if not p_format or p_format not in formats:
raise e.PackageFormatError(
'Unknown or missing format version')
package = formats[p_format].create(source_directory, content, loader)
formats[p_format].load(package, content)
if preload:
package.validate()
return package

View File

@ -0,0 +1,99 @@
# 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 sys
import yaml
import murano.packages.application_package
from murano.packages import exceptions
class MuranoPlPackage(murano.packages.application_package.ApplicationPackage):
def __init__(self, source_directory, manifest, loader):
super(MuranoPlPackage, self).__init__(
source_directory, manifest, loader)
self._classes = None
self._ui = None
self._ui_cache = None
self._raw_ui_cache = None
self._classes_cache = {}
@property
def classes(self):
return tuple(self._classes.keys())
@property
def ui(self):
if not self._ui_cache:
self._load_ui(True)
return self._ui_cache
@property
def raw_ui(self):
if not self._raw_ui_cache:
self._load_ui(False)
return self._raw_ui_cache
def get_class(self, name):
if name not in self._classes_cache:
self._load_class(name)
return self._classes_cache[name]
# Private methods
def _load_ui(self, load_yaml=False):
if self._raw_ui_cache and load_yaml:
self._ui_cache = yaml.load(self._raw_ui_cache, self.yaml_loader)
else:
ui_file = self._ui
full_path = os.path.join(self._source_directory, 'UI', ui_file)
if not os.path.isfile(full_path):
self._raw_ui_cache = None
self._ui_cache = None
return
try:
with open(full_path) as stream:
self._raw_ui_cache = stream.read()
if load_yaml:
self._ui_cache = yaml.load(self._raw_ui_cache,
self.yaml_loader)
except Exception as ex:
trace = sys.exc_info()[2]
raise exceptions.PackageUILoadError(str(ex)), None, trace
def _load_class(self, name):
if name not in self._classes:
raise exceptions.PackageClassLoadError(
name, 'Class not defined in this package')
def_file = self._classes[name]
full_path = os.path.join(self._source_directory, 'Classes', def_file)
if not os.path.isfile(full_path):
raise exceptions.PackageClassLoadError(
name, 'File with class definition not found')
try:
with open(full_path) as stream:
self._classes_cache[name] = yaml.load(stream, self.yaml_loader)
except Exception as ex:
trace = sys.exc_info()[2]
msg = 'Unable to load class definition due to "{0}"'.format(
str(ex))
raise exceptions.PackageClassLoadError(name, msg), None, trace
def validate(self):
self._classes_cache.clear()
for class_name in self._classes:
self.get_class(class_name)
self._load_ui(True)
super(MuranoPlPackage, self).validate()

View File

@ -0,0 +1,57 @@
# 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 re
import murano.packages.application_package
import murano.packages.exceptions as e
import murano.packages.hot_package
# noinspection PyProtectedMember
def load(package, yaml_content):
package._full_name = yaml_content.get('FullName')
if not package._full_name:
raise murano.packages.exceptions.PackageFormatError(
'FullName not specified')
_check_full_name(package._full_name)
package._package_type = yaml_content.get('Type')
if not package._package_type or package._package_type not in \
murano.packages.application_package.PackageTypes.ALL:
raise e.PackageFormatError('Invalid Package Type')
package._display_name = yaml_content.get('Name', package._full_name)
package._description = yaml_content.get('Description')
package._author = yaml_content.get('Author')
package._logo = yaml_content.get('Logo')
package._tags = yaml_content.get('Tags')
def create(source_directory, content, loader):
return murano.packages.hot_package.HotPackage(
source_directory, content, loader)
def _check_full_name(full_name):
error = murano.packages.exceptions.PackageFormatError(
'Invalid FullName')
if re.match(r'^[\w\.]+$', full_name):
if full_name.startswith('.') or full_name.endswith('.'):
raise error
if '..' in full_name:
raise error
else:
raise error

View File

@ -17,9 +17,12 @@ import re
import murano.packages.application_package import murano.packages.application_package
import murano.packages.exceptions as e import murano.packages.exceptions as e
import murano.packages.mpl_package
# noinspection PyProtectedMember # noinspection PyProtectedMember
def load(package, yaml_content): def load(package, yaml_content):
package._full_name = yaml_content.get('FullName') package._full_name = yaml_content.get('FullName')
if not package._full_name: if not package._full_name:
@ -39,6 +42,11 @@ def load(package, yaml_content):
package._tags = yaml_content.get('Tags') package._tags = yaml_content.get('Tags')
def create(source_directory, content, loader):
return murano.packages.mpl_package.MuranoPlPackage(
source_directory, content, loader)
def _check_full_name(full_name): def _check_full_name(full_name):
error = murano.packages.exceptions.PackageFormatError( error = murano.packages.exceptions.PackageFormatError(
'Invalid FullName') 'Invalid FullName')