Merge "Fix serialization of structures that might contain YAQL types"
This commit is contained in:
@@ -16,7 +16,6 @@
|
|||||||
from email import header
|
from email import header
|
||||||
from email.mime import multipart
|
from email.mime import multipart
|
||||||
from email.mime import text
|
from email.mime import text
|
||||||
import json as json_lib
|
|
||||||
import smtplib
|
import smtplib
|
||||||
import time
|
import time
|
||||||
|
|
||||||
@@ -25,6 +24,7 @@ import requests
|
|||||||
import six
|
import six
|
||||||
|
|
||||||
from mistral import exceptions as exc
|
from mistral import exceptions as exc
|
||||||
|
from mistral import utils
|
||||||
from mistral.utils import javascript
|
from mistral.utils import javascript
|
||||||
from mistral.utils import ssh_utils
|
from mistral.utils import ssh_utils
|
||||||
from mistral_lib import actions
|
from mistral_lib import actions
|
||||||
@@ -182,7 +182,7 @@ class HTTPAction(actions.Action):
|
|||||||
self.url = url
|
self.url = url
|
||||||
self.method = method
|
self.method = method
|
||||||
self.params = params
|
self.params = params
|
||||||
self.body = json_lib.dumps(body) if isinstance(body, dict) else body
|
self.body = utils.to_json_str(body) if isinstance(body, dict) else body
|
||||||
self.json = json
|
self.json = json
|
||||||
self.headers = headers
|
self.headers = headers
|
||||||
self.cookies = cookies
|
self.cookies = cookies
|
||||||
@@ -456,7 +456,7 @@ class SSHAction(actions.Action):
|
|||||||
return raise_exc(parent_exc=e)
|
return raise_exc(parent_exc=e)
|
||||||
|
|
||||||
def test(self, context):
|
def test(self, context):
|
||||||
return json_lib.dumps(self.params)
|
return utils.to_json_str(self.params)
|
||||||
|
|
||||||
|
|
||||||
class SSHProxiedAction(SSHAction):
|
class SSHProxiedAction(SSHAction):
|
||||||
|
|||||||
@@ -16,11 +16,12 @@
|
|||||||
# expressed by json-strings
|
# expressed by json-strings
|
||||||
#
|
#
|
||||||
|
|
||||||
from oslo_serialization import jsonutils
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
from sqlalchemy.dialects import mysql
|
from sqlalchemy.dialects import mysql
|
||||||
from sqlalchemy.ext import mutable
|
from sqlalchemy.ext import mutable
|
||||||
|
|
||||||
|
from mistral import utils
|
||||||
|
|
||||||
|
|
||||||
class JsonEncoded(sa.TypeDecorator):
|
class JsonEncoded(sa.TypeDecorator):
|
||||||
"""Represents an immutable structure as a json-encoded string."""
|
"""Represents an immutable structure as a json-encoded string."""
|
||||||
@@ -28,22 +29,10 @@ class JsonEncoded(sa.TypeDecorator):
|
|||||||
impl = sa.Text
|
impl = sa.Text
|
||||||
|
|
||||||
def process_bind_param(self, value, dialect):
|
def process_bind_param(self, value, dialect):
|
||||||
if value is not None:
|
return utils.to_json_str(value)
|
||||||
# We need to convert the root of the given object graph into
|
|
||||||
# a primitive by hand so that we also enable conversion of
|
|
||||||
# object of custom classes into primitives. Otherwise, they are
|
|
||||||
# ignored by the "json" lib.
|
|
||||||
value = jsonutils.dumps(
|
|
||||||
jsonutils.to_primitive(value, convert_instances=True)
|
|
||||||
)
|
|
||||||
|
|
||||||
return value
|
|
||||||
|
|
||||||
def process_result_value(self, value, dialect):
|
def process_result_value(self, value, dialect):
|
||||||
if value is not None:
|
return utils.from_json_str(value)
|
||||||
value = jsonutils.loads(value)
|
|
||||||
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
class MutableList(mutable.Mutable, list):
|
class MutableList(mutable.Mutable, list):
|
||||||
|
|||||||
@@ -135,7 +135,10 @@ def _sanitize_yaql_result(result):
|
|||||||
if isinstance(result, yaql_utils.FrozenDict):
|
if isinstance(result, yaql_utils.FrozenDict):
|
||||||
return result._d
|
return result._d
|
||||||
|
|
||||||
return result if not inspect.isgenerator(result) else list(result)
|
if inspect.isgenerator(result):
|
||||||
|
return list(result)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
class YAQLEvaluator(base.Evaluator):
|
class YAQLEvaluator(base.Evaluator):
|
||||||
|
|||||||
@@ -14,7 +14,6 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
from oslo_serialization import jsonutils
|
|
||||||
|
|
||||||
from mistral.db.v2 import api as db_api
|
from mistral.db.v2 import api as db_api
|
||||||
from mistral.db.v2.sqlalchemy import models
|
from mistral.db.v2.sqlalchemy import models
|
||||||
@@ -24,6 +23,7 @@ from mistral.services import workbooks as wb_service
|
|||||||
from mistral.services import workflows as wf_service
|
from mistral.services import workflows as wf_service
|
||||||
from mistral.tests.unit import base as test_base
|
from mistral.tests.unit import base as test_base
|
||||||
from mistral.tests.unit.engine import base as engine_test_base
|
from mistral.tests.unit.engine import base as engine_test_base
|
||||||
|
from mistral import utils
|
||||||
from mistral.workflow import data_flow
|
from mistral.workflow import data_flow
|
||||||
from mistral.workflow import states
|
from mistral.workflow import states
|
||||||
|
|
||||||
@@ -1444,9 +1444,7 @@ class DataFlowTest(test_base.BaseTest):
|
|||||||
{'k2': 'v2'},
|
{'k2': 'v2'},
|
||||||
)
|
)
|
||||||
|
|
||||||
json_str = jsonutils.dumps(
|
json_str = utils.to_json_str(ctx)
|
||||||
jsonutils.to_primitive(ctx, convert_instances=True)
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertIsNotNone(json_str)
|
self.assertIsNotNone(json_str)
|
||||||
self.assertNotEqual('{}', json_str)
|
self.assertNotEqual('{}', json_str)
|
||||||
@@ -1464,9 +1462,7 @@ class DataFlowTest(test_base.BaseTest):
|
|||||||
|
|
||||||
d = {'root': ctx}
|
d = {'root': ctx}
|
||||||
|
|
||||||
json_str = jsonutils.dumps(
|
json_str = utils.to_json_str(d)
|
||||||
jsonutils.to_primitive(d, convert_instances=True)
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertIsNotNone(json_str)
|
self.assertIsNotNone(json_str)
|
||||||
self.assertNotEqual('{"root": {}}', json_str)
|
self.assertNotEqual('{"root": {}}', json_str)
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ from mistral.expressions import yaql_expression as expr
|
|||||||
from mistral.tests.unit import base
|
from mistral.tests.unit import base
|
||||||
from mistral_lib import utils
|
from mistral_lib import utils
|
||||||
|
|
||||||
|
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
|
|
||||||
DATA = {
|
DATA = {
|
||||||
@@ -96,6 +97,7 @@ class YaqlEvaluatorTest(base.BaseTest):
|
|||||||
'$.servers.where($.name = ubuntu)',
|
'$.servers.where($.name = ubuntu)',
|
||||||
SERVERS
|
SERVERS
|
||||||
)
|
)
|
||||||
|
|
||||||
item = list(res)[0]
|
item = list(res)[0]
|
||||||
|
|
||||||
self.assertEqual({'name': 'ubuntu'}, item)
|
self.assertEqual({'name': 'ubuntu'}, item)
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
# Copyright 2013 - Mirantis, Inc.
|
||||||
|
# Copyright 2015 - StackStorm, Inc.
|
||||||
|
# Copyright 2016 - Brocade Communications Systems, 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.
|
||||||
|
|
||||||
|
from yaql.language import utils as yaql_utils
|
||||||
|
|
||||||
|
from mistral.tests.unit import base
|
||||||
|
from mistral import utils
|
||||||
|
|
||||||
|
|
||||||
|
class YaqlJsonSerializationTest(base.BaseTest):
|
||||||
|
def test_serialize_frozen_dict(self):
|
||||||
|
data = yaql_utils.FrozenDict(a=1, b=2, c=iter([1, 2, 3]))
|
||||||
|
|
||||||
|
json_str = utils.to_json_str(data)
|
||||||
|
|
||||||
|
self.assertIsNotNone(json_str)
|
||||||
|
|
||||||
|
self.assertIn('"a": 1', json_str)
|
||||||
|
self.assertIn('"b": 2', json_str)
|
||||||
|
self.assertIn('"c": [1, 2, 3]', json_str)
|
||||||
|
|
||||||
|
def test_serialize_generator(self):
|
||||||
|
def _list_stream(_list):
|
||||||
|
for i in _list:
|
||||||
|
yield i
|
||||||
|
|
||||||
|
gen = _list_stream(
|
||||||
|
[1, yaql_utils.FrozenDict(a=1), _list_stream([12, 15])]
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual('[1, {"a": 1}, [12, 15]]', utils.to_json_str(gen))
|
||||||
|
|
||||||
|
def test_serialize_dict_of_generators(self):
|
||||||
|
def _f(cnt):
|
||||||
|
for i in range(1, cnt + 1):
|
||||||
|
yield i
|
||||||
|
|
||||||
|
data = {'numbers': _f(3)}
|
||||||
|
|
||||||
|
self.assertEqual('{"numbers": [1, 2, 3]}', utils.to_json_str(data))
|
||||||
|
|
||||||
|
def test_serialize_range(self):
|
||||||
|
self.assertEqual("[1, 2, 3, 4]", utils.to_json_str(range(1, 5)))
|
||||||
|
|
||||||
|
def test_serialize_iterator_of_frozen_dicts(self):
|
||||||
|
data = iter(
|
||||||
|
[
|
||||||
|
yaql_utils.FrozenDict(a=1, b=2, c=iter([1, 2, 3])),
|
||||||
|
yaql_utils.FrozenDict(
|
||||||
|
a=11,
|
||||||
|
b=yaql_utils.FrozenDict(b='222'),
|
||||||
|
c=iter(
|
||||||
|
[
|
||||||
|
1,
|
||||||
|
yaql_utils.FrozenDict(
|
||||||
|
a=iter([4, yaql_utils.FrozenDict(a=99)])
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
json_str = utils.to_json_str(data)
|
||||||
|
|
||||||
|
self.assertIsNotNone(json_str)
|
||||||
|
|
||||||
|
# Checking the first item.
|
||||||
|
self.assertIn('"a": 1', json_str)
|
||||||
|
self.assertIn('"b": 2', json_str)
|
||||||
|
self.assertIn('"c": [1, 2, 3]', json_str)
|
||||||
|
|
||||||
|
# Checking the first item.
|
||||||
|
self.assertIn('"a": 11', json_str)
|
||||||
|
self.assertIn('"b": {"b": "222"}', json_str)
|
||||||
|
self.assertIn('"c": [1, {"a": [4, {"a": 99}]}]', json_str)
|
||||||
@@ -15,12 +15,14 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
import contextlib
|
import contextlib
|
||||||
|
import inspect
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import tempfile
|
import tempfile
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
from oslo_concurrency import processutils
|
from oslo_concurrency import processutils
|
||||||
|
from oslo_serialization import jsonutils
|
||||||
|
|
||||||
from mistral import exceptions as exc
|
from mistral import exceptions as exc
|
||||||
|
|
||||||
@@ -101,3 +103,52 @@ def generate_key_pair(key_length=2048):
|
|||||||
public_key = open(public_key_path).read()
|
public_key = open(public_key_path).read()
|
||||||
|
|
||||||
return private_key, public_key
|
return private_key, public_key
|
||||||
|
|
||||||
|
|
||||||
|
def to_json_str(obj):
|
||||||
|
"""Serializes an object into a JSON string.
|
||||||
|
|
||||||
|
:param obj: Object to serialize.
|
||||||
|
:return: JSON string.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if obj is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _fallback(value):
|
||||||
|
if inspect.isgenerator(value):
|
||||||
|
result = list(value)
|
||||||
|
|
||||||
|
# The result of the generator call may be again not primitive
|
||||||
|
# so we need to call "to_primitive" again with the same fallback
|
||||||
|
# function. Note that the endless recursion here is not a problem
|
||||||
|
# because "to_primitive" limits the depth for custom classes,
|
||||||
|
# if they are present in the object graph being traversed.
|
||||||
|
return jsonutils.to_primitive(
|
||||||
|
result,
|
||||||
|
convert_instances=True,
|
||||||
|
fallback=_fallback
|
||||||
|
)
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
# We need to convert the root of the given object graph into
|
||||||
|
# a primitive by hand so that we also enable conversion of
|
||||||
|
# object of custom classes into primitives. Otherwise, they are
|
||||||
|
# ignored by the "json" lib.
|
||||||
|
return jsonutils.dumps(
|
||||||
|
jsonutils.to_primitive(obj, convert_instances=True, fallback=_fallback)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def from_json_str(json_str):
|
||||||
|
"""Reconstructs an object from a JSON string.
|
||||||
|
|
||||||
|
:param json_str: A JSON string.
|
||||||
|
:return: Deserialized object.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if json_str is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return jsonutils.loads(json_str)
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ import abc
|
|||||||
|
|
||||||
from mistral import config as cfg
|
from mistral import config as cfg
|
||||||
from mistral import exceptions as exc
|
from mistral import exceptions as exc
|
||||||
|
from mistral import utils
|
||||||
|
|
||||||
from oslo_serialization import jsonutils
|
|
||||||
from oslo_utils import importutils
|
from oslo_utils import importutils
|
||||||
from stevedore import driver
|
from stevedore import driver
|
||||||
from stevedore import extension
|
from stevedore import extension
|
||||||
@@ -55,13 +55,7 @@ class PyV8Evaluator(JSEvaluator):
|
|||||||
|
|
||||||
with _PYV8.JSContext() as js_ctx:
|
with _PYV8.JSContext() as js_ctx:
|
||||||
# Prepare data context and way for interaction with it.
|
# Prepare data context and way for interaction with it.
|
||||||
# NOTE: it's important to enable conversion of custom types
|
js_ctx.eval('$ = %s' % utils.to_json_str(ctx))
|
||||||
# into JSON to account for classes like ContextView.
|
|
||||||
ctx_str = jsonutils.dumps(
|
|
||||||
jsonutils.to_primitive(ctx, convert_instances=True)
|
|
||||||
)
|
|
||||||
|
|
||||||
js_ctx.eval('$ = %s' % ctx_str)
|
|
||||||
|
|
||||||
result = js_ctx.eval(script)
|
result = js_ctx.eval(script)
|
||||||
|
|
||||||
@@ -78,11 +72,7 @@ class V8EvalEvaluator(JSEvaluator):
|
|||||||
|
|
||||||
v8 = _V8EVAL.V8()
|
v8 = _V8EVAL.V8()
|
||||||
|
|
||||||
# NOTE: it's important to enable conversion of custom types
|
ctx_str = utils.to_json_str(ctx)
|
||||||
# into JSON to account for classes like ContextView.
|
|
||||||
ctx_str = jsonutils.dumps(
|
|
||||||
jsonutils.to_primitive(ctx, convert_instances=True)
|
|
||||||
)
|
|
||||||
|
|
||||||
return v8.eval(
|
return v8.eval(
|
||||||
('$ = %s; %s' % (ctx_str, script)).encode(encoding='UTF-8')
|
('$ = %s; %s' % (ctx_str, script)).encode(encoding='UTF-8')
|
||||||
@@ -100,14 +90,10 @@ class PyMiniRacerEvaluator(JSEvaluator):
|
|||||||
|
|
||||||
js_ctx = _PY_MINI_RACER.MiniRacer()
|
js_ctx = _PY_MINI_RACER.MiniRacer()
|
||||||
|
|
||||||
# NOTE: it's important to enable conversion of custom types
|
return js_ctx.eval(
|
||||||
# into JSON to account for classes like ContextView.
|
'$ = {}; {}'.format(utils.to_json_str(ctx), script)
|
||||||
ctx_str = jsonutils.dumps(
|
|
||||||
jsonutils.to_primitive(ctx, convert_instances=True)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return js_ctx.eval(('$ = {}; {}'.format(ctx_str, script)))
|
|
||||||
|
|
||||||
|
|
||||||
_mgr = extension.ExtensionManager(
|
_mgr = extension.ExtensionManager(
|
||||||
namespace='mistral.expression.evaluators',
|
namespace='mistral.expression.evaluators',
|
||||||
|
|||||||
Reference in New Issue
Block a user