deb-murano/murano/dsl/helpers.py
Alexander Tivelkov df95ad397e Added the support of Glance Artifact Repository
Murano's application packages can now be stored either in database
(using murano-api) or in Glance Artifact repository (so-called Glance
V3 api). This patch adds an experimental support of the latter
approach to murano engine.

As all the difference between these two storages is incapsulated in
the murano client, engine just needs to configure it properly by
passing an instance of glance v3 client on creation. This is
controlled by a 'packages_service' parameter of 'packages_opts'
configuration group. It is set to 'murano' by default and indicates
the usage of old, database-backed storage. If set to 'glance', the
murano client will encapsulate glance v3 connector and thus the
packages will be accessed from Glance Artifact Repository.

The settings of Glance client are also added to the configuration, as
well as a client factory to generate the client. As these settings may
now conflict with the settings "demo plugin", the appropriate
configuration section is renamed in the latter.

This patch also contains a couple of utility functions to transform
partial semver version specs into non-partial ones and - further - to
a set of Glance query parameters needed to filter the artifacts based
on that spec.

Change-Id: I690467e43b6b63850ebecef756635241e623554c
Implements-blueprint: artifact-repository-support
2015-09-09 19:15:25 +00:00

418 lines
13 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 collections
import contextlib
import functools
import re
import string
import sys
import types
import uuid
import eventlet.greenpool
import eventlet.greenthread
import semantic_version
import yaql.language.exceptions
import yaql.language.expressions
from yaql.language import utils as yaqlutils
from murano.common import utils
from murano.dsl import constants
from murano.dsl import dsl_types
from murano.dsl import exceptions
KEYWORD_REGEX = re.compile(r'(?!__)\b[^\W\d]\w*\b')
_threads_sequencer = 0
def evaluate(value, context):
if isinstance(value, (dsl_types.YaqlExpression,
yaql.language.expressions.Statement)):
return value(context)
elif isinstance(value, yaqlutils.MappingType):
return yaqlutils.FrozenDict(
(evaluate(d_key, context),
evaluate(d_value, context))
for d_key, d_value in value.iteritems())
elif yaqlutils.is_sequence(value):
return tuple(evaluate(t, context) for t in value)
elif isinstance(value, yaqlutils.SetType):
return frozenset(evaluate(t, context) for t in value)
elif yaqlutils.is_iterable(value):
return tuple(
evaluate(t, context)
for t in yaqlutils.limit_iterable(
value, constants.ITERATORS_LIMIT))
elif isinstance(value, dsl_types.MuranoObjectInterface):
return value.object
else:
return value
def merge_lists(list1, list2):
result = []
for item in list1 + list2:
exists = False
for old_item in result:
if not utils.is_different(item, old_item):
exists = True
break
if not exists:
result.append(item)
return result
def merge_dicts(dict1, dict2, max_levels=0):
result = {}
for key, value1 in dict1.items():
result[key] = value1
if key in dict2:
value2 = dict2[key]
if type(value2) != type(value1):
if (isinstance(value1, types.StringTypes) and
isinstance(value2, types.StringTypes)):
continue
raise TypeError()
if max_levels != 1 and isinstance(value2, types.DictionaryType):
result[key] = merge_dicts(
value1, value2,
0 if max_levels == 0 else max_levels - 1)
elif max_levels != 1 and isinstance(value2, types.ListType):
result[key] = merge_lists(value1, value2)
else:
result[key] = value2
for key, value1 in dict2.items():
if key not in result:
result[key] = value1
return result
def generate_id():
return uuid.uuid4().hex
def parallel_select(collection, func, limit=1000):
# workaround for eventlet issue 232
# https://github.com/eventlet/eventlet/issues/232
def wrapper(element):
try:
with contextual(get_context()):
return func(element), False, None
except Exception as e:
return e, True, sys.exc_info()[2]
gpool = eventlet.greenpool.GreenPool(limit)
result = list(gpool.imap(wrapper, collection))
try:
exception = next(t for t in result if t[1])
except StopIteration:
return map(lambda t: t[0], result)
else:
raise exception[0], None, exception[2]
def enum(**enums):
return type('Enum', (), enums)
def get_context():
current_thread = eventlet.greenthread.getcurrent()
return getattr(current_thread, '__murano_context', None)
def get_executor(context=None):
context = context or get_context()
result = context[constants.CTX_EXECUTOR]
return None if not result else result()
def get_type(context=None):
context = context or get_context()
return context[constants.CTX_TYPE]
def get_environment(context=None):
context = context or get_context()
return context[constants.CTX_ENVIRONMENT]
def get_object_store(context=None):
context = context or get_context()
return context[constants.CTX_THIS].object_store
def get_package_loader(context=None):
context = context or get_context()
result = context[constants.CTX_PACKAGE_LOADER]
return None if not result else result()
def get_this(context=None):
context = context or get_context()
return context[constants.CTX_THIS]
def get_caller_context(context=None):
context = context or get_context()
return context[constants.CTX_CALLER_CONTEXT]
def get_attribute_store(context=None):
context = context or get_context()
store = context[constants.CTX_ATTRIBUTE_STORE]
return None if not store else store()
def get_current_instruction(context=None):
context = context or get_context()
return context[constants.CTX_CURRENT_INSTRUCTION]
def get_current_method(context=None):
context = context or get_context()
return context[constants.CTX_CURRENT_METHOD]
def get_yaql_engine(context=None):
context = context or get_context()
return context[constants.CTX_YAQL_ENGINE]
def get_current_exception(context=None):
context = context or get_context()
return context[constants.CTX_CURRENT_EXCEPTION]
def are_property_modifications_allowed(context=None):
context = context or get_context()
return context[constants.CTX_ALLOW_PROPERTY_WRITES] or False
def get_class(name, context=None):
context = context or get_context()
murano_class = get_type(context)
return murano_class.package.find_class(name)
def is_keyword(text):
return KEYWORD_REGEX.match(text) is not None
def get_current_thread_id():
global _threads_sequencer
current_thread = eventlet.greenthread.getcurrent()
thread_id = getattr(current_thread, '__thread_id', None)
if thread_id is None:
thread_id = 'T' + str(_threads_sequencer)
_threads_sequencer += 1
setattr(current_thread, '__thread_id', thread_id)
return thread_id
@contextlib.contextmanager
def contextual(ctx):
current_thread = eventlet.greenthread.getcurrent()
current_context = getattr(current_thread, '__murano_context', None)
if ctx:
setattr(current_thread, '__murano_context', ctx)
try:
yield
finally:
if current_context:
setattr(current_thread, '__murano_context', current_context)
elif hasattr(current_thread, '__murano_context'):
delattr(current_thread, '__murano_context')
def parse_version_spec(version_spec):
if isinstance(version_spec, semantic_version.Spec):
return normalize_version_spec(version_spec)
if isinstance(version_spec, semantic_version.Version):
return normalize_version_spec(
semantic_version.Spec('==' + str(version_spec)))
if not version_spec:
version_spec = '0'
version_spec = str(version_spec).translate(None, string.whitespace)
if version_spec[0].isdigit():
version_spec = '==' + str(version_spec)
version_spec = semantic_version.Spec(version_spec)
return normalize_version_spec(version_spec)
def parse_version(version):
if isinstance(version, semantic_version.Version):
return version
if not version:
version = '0'
return semantic_version.Version.coerce(str(version))
def traverse(seed, producer=None, track_visited=True):
if not yaqlutils.is_iterable(seed):
seed = [seed]
visited = None if not track_visited else set()
queue = collections.deque(seed)
while queue:
item = queue.popleft()
if track_visited:
if item in visited:
continue
visited.add(item)
produced = (yield item)
if produced is None and producer:
produced = producer(item)
if produced:
queue.extend(produced)
def cast(obj, murano_class, pov_or_version_spec=None):
if isinstance(obj, dsl_types.MuranoObjectInterface):
obj = obj.object
if isinstance(pov_or_version_spec, dsl_types.MuranoClass):
pov_or_version_spec = pov_or_version_spec.package
elif isinstance(pov_or_version_spec, types.StringTypes):
pov_or_version_spec = parse_version_spec(pov_or_version_spec)
if isinstance(murano_class, dsl_types.MuranoClass):
if pov_or_version_spec is None:
pov_or_version_spec = parse_version_spec(murano_class.version)
murano_class = murano_class.name
candidates = []
for cls in obj.type.ancestors():
if cls.name != murano_class:
continue
elif isinstance(pov_or_version_spec, semantic_version.Version):
if cls.version != pov_or_version_spec:
continue
elif isinstance(pov_or_version_spec, semantic_version.Spec):
if cls.version not in pov_or_version_spec:
continue
elif isinstance(pov_or_version_spec, dsl_types.MuranoPackage):
requirement = pov_or_version_spec.requirements.get(
cls.package.name)
if requirement is None:
raise exceptions.NoClassFound(murano_class)
if cls.version not in requirement:
continue
elif pov_or_version_spec is not None:
raise ValueError('pov_or_version_spec of unsupported '
'type {0}'.format(type(pov_or_version_spec)))
candidates.append(cls)
if not candidates:
raise exceptions.NoClassFound(murano_class)
elif len(candidates) > 1:
raise exceptions.AmbiguousClassName(murano_class)
return obj.cast(candidates[0])
def is_instance_of(obj, class_name, pov_or_version_spec=None):
try:
cast(obj, class_name, pov_or_version_spec)
return True
except (exceptions.NoClassFound, exceptions.AmbiguousClassName):
return False
def filter_parameters_dict(parameters):
parameters = parameters.copy()
for name in parameters.keys():
if not is_keyword(name):
del parameters[name]
return parameters
def memoize(func):
cache = {}
@functools.wraps(func)
def wrap(*args):
if args not in cache:
result = func(*args)
cache[args] = result
return result
else:
return cache[args]
return wrap
def normalize_version_spec(version_spec):
def coerce(v):
return semantic_version.Version('{0}.{1}.{2}'.format(
v.major, v.minor or 0, v.patch or 0
))
def increment(v):
# NOTE(ativelkov): replace these implementations with next_minor() and
# next_major() calls when the semantic_version is updated in global
# requirements.
if v.minor is None:
return semantic_version.Version(
'.'.join(str(x) for x in [v.major + 1, 0, 0]))
else:
return semantic_version.Version(
'.'.join(str(x) for x in [v.major, v.minor + 1, 0]))
def extend(v):
return semantic_version.Version(str(v) + '-0')
transformations = {
'>': [('>=', (increment, extend))],
'>=': [('>=', (coerce,))],
'<': [('<', (coerce, extend))],
'<=': [('<', (increment, extend))],
'!=': [('>=', (increment, extend))],
'==': [('>=', (coerce,)), ('<', (increment, coerce, extend))]
}
new_parts = []
for item in version_spec.specs:
if item.kind == '*':
continue
elif item.spec.patch is not None:
new_parts.append(str(item))
else:
for op, funcs in transformations[item.kind]:
new_parts.append('{0}{1}'.format(
op,
reduce(lambda v, f: f(v), funcs, item.spec)
))
if not new_parts:
return semantic_version.Spec('*')
return semantic_version.Spec(*new_parts)
semver_to_api_map = {
'>': 'gt',
'>=': 'ge',
'<': 'lt',
'<=': 'le',
'!=': 'ne',
'==': 'eq'
}
def breakdown_spec_to_query(normalized_spec):
res = []
for item in normalized_spec.specs:
if item.kind == '*':
continue
else:
res.append("%s:%s" % (semver_to_api_map[item.kind],
item.spec))
return res