Allow YAQL expressions in task's dependencies

Since Fuel 9.0 we have a task-based deployment turned on by default.
That means our tasks are executing simultaneously on several nodes, and
old static approach for specifying dependencies doesn't work anymore.

In order to avoid dependency loops and make possible to rely on
different tasks based on some condition, we've got to be able to
calculate `cross-depends` and `cross-depended-by` attributes on fly.

Fortunately, we already have traversal mechanism that evaluates YAQL
expressions almost everywhere. So all we need to do is to remove
validation limitation and allow to upload deployment tasks with YAQL
expressions in those fields.

Change-Id: I393dfb06f9e25ecd3ca79fddc84104a8cf993094
Partial-Bug: #1541309
This commit is contained in:
Igor Kalnitsky 2016-05-25 12:19:22 +03:00
parent a056929141
commit 886ab3087c
No known key found for this signature in database
GPG Key ID: F05067E18910196E
3 changed files with 220 additions and 21 deletions

View File

@ -18,22 +18,6 @@ from nailgun.consts import NODE_RESOLVE_POLICY
from nailgun.consts import ORCHESTRATOR_TASK_TYPES
RELATION_SCHEMA = {
'$schema': 'http://json-schema.org/draft-04/schema#',
'type': 'object',
'required': ['name'],
'properties': {
'name': {'type': 'string'},
'role': {
'oneOf': [
{'type': 'string'},
{'type': 'array'},
]
},
'policy': {'type': 'string', 'enum': list(NODE_RESOLVE_POLICY)},
}
}
YAQL_EXP = {
'$schema': 'http://json-schema.org/draft-04/schema#',
'type': 'object',
@ -43,6 +27,25 @@ YAQL_EXP = {
}
}
RELATION_SCHEMA = {
'$schema': 'http://json-schema.org/draft-04/schema#',
'type': 'object',
'required': ['name'],
'properties': {
'name': {
'oneOf': [
{'type': 'string'}, YAQL_EXP],
},
'role': {
'oneOf': [
{'type': 'string'}, {'type': 'array'}, YAQL_EXP]
},
'policy': {'type': 'string', 'enum': list(NODE_RESOLVE_POLICY)},
}
}
TASK_STRATEGY = {
'$schema': 'http://json-schema.org/draft-04/schema#',
'type': 'object',
@ -79,8 +82,14 @@ TASK_SCHEMA = {
'parameters': TASK_PARAMETERS,
'required_for': {'type': 'array'},
'requires': {'type': 'array'},
'cross-depends': {'type': 'array', 'items': RELATION_SCHEMA},
'cross-depended-by': {'type': 'array', 'items': RELATION_SCHEMA}
'cross-depends': {
'oneOf': [
{'type': 'array', 'items': RELATION_SCHEMA}, YAQL_EXP]
},
'cross-depended-by': {
'oneOf': [
{'type': 'array', 'items': RELATION_SCHEMA}, YAQL_EXP]
},
}
}

View File

@ -18,7 +18,6 @@ from nailgun import consts
from nailgun.db.sqlalchemy.models.base import Base
from nailgun.db.sqlalchemy.models.fields import JSON
from nailgun.db.sqlalchemy.models.mutable import MutableDict
from nailgun.db.sqlalchemy.models.mutable import MutableList
class DeploymentGraph(Base):
@ -110,12 +109,12 @@ class DeploymentGraphTask(Base):
nullable=False)
# cross-depended-by with hypen is deprecated notation
cross_depended_by = sa.Column(
MutableList.as_mutable(JSON),
JSON,
default=[],
server_default='[]')
# cross-depends with hypen is deprecated notation
cross_depends = sa.Column(
MutableList.as_mutable(JSON),
JSON,
default=[],
server_default='[]')
parameters = sa.Column(

View File

@ -0,0 +1,191 @@
# Copyright 2016 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 textwrap
import jsonschema
from nailgun.api.v1.validators import orchestrator_graph
from nailgun.test import base
class TestGraphSolverTasksValidator(base.BaseUnitTest):
validator = orchestrator_graph.GraphSolverTasksValidator
def test_task_requires_list(self):
self.assertNotRaises(
jsonschema.exceptions.ValidationError,
self.validator.validate_update,
textwrap.dedent('''
[{
"id": "netconfig",
"type": "puppet",
"requires": ["tools"]
}]
'''),
instance=None)
def test_task_required_for_list(self):
self.assertNotRaises(
jsonschema.exceptions.ValidationError,
self.validator.validate_update,
textwrap.dedent('''
[{
"id": "netconfig",
"type": "puppet",
"required_for": ["tools"]
}]
'''),
instance=None)
def test_task_cross_depends_list(self):
self.assertNotRaises(
jsonschema.exceptions.ValidationError,
self.validator.validate_update,
textwrap.dedent('''
[{
"id": "netconfig",
"type": "puppet",
"cross-depends": [{
"name": "something"
}]
}]
'''),
instance=None)
def test_task_cross_depends_yaql(self):
self.assertNotRaises(
jsonschema.exceptions.ValidationError,
self.validator.validate_update,
textwrap.dedent('''
[{
"id": "netconfig",
"type": "puppet",
"cross-depends": {
"yaql_exp": "$.do_something()"
}
}]
'''),
instance=None)
def test_task_cross_depends_yaql_inside(self):
self.assertNotRaises(
jsonschema.exceptions.ValidationError,
self.validator.validate_update,
textwrap.dedent('''
[{
"id": "netconfig",
"type": "puppet",
"cross-depends": [{
"name": {
"yaql_exp": "$.do_something()"
},
"role": {
"yaql_exp": "$.do_something()"
}
}]
}]
'''),
instance=None)
def test_task_cross_depended_by_list(self):
self.assertNotRaises(
jsonschema.exceptions.ValidationError,
self.validator.validate_update,
textwrap.dedent('''
[{
"id": "netconfig",
"type": "puppet",
"cross-depended-by": [{
"name": "something",
"role": "something"
}]
}]
'''),
instance=None)
def test_task_cross_depended_by_yaql(self):
self.assertNotRaises(
jsonschema.exceptions.ValidationError,
self.validator.validate_update,
textwrap.dedent('''
[{
"id": "netconfig",
"type": "puppet",
"cross-depended-by": {
"yaql_exp": "$.do_something()"
}
}]
'''),
instance=None)
def test_task_cross_depended_by_yaql_inside(self):
self.assertNotRaises(
jsonschema.exceptions.ValidationError,
self.validator.validate_update,
textwrap.dedent('''
[{
"id": "netconfig",
"type": "puppet",
"cross-depended-by": [{
"name": {
"yaql_exp": "$.do_something()"
},
"role": {
"yaql_exp": "$.do_something()"
}
}]
}]
'''),
instance=None)
def test_task_strategy_int(self):
self.assertNotRaises(
jsonschema.exceptions.ValidationError,
self.validator.validate_update,
textwrap.dedent('''
[{
"id": "netconfig",
"type": "puppet",
"parameters": {
"strategy": {
"type": "parallel",
"amount": 42
}
}
}]
'''),
instance=None)
def test_task_strategy_yaql(self):
self.assertNotRaises(
jsonschema.exceptions.ValidationError,
self.validator.validate_update,
textwrap.dedent('''
[{
"id": "netconfig",
"type": "puppet",
"parameters": {
"strategy": {
"type": "parallel",
"amount": {
"yaql_exp": "$.do_something()"
}
}
}
}]
'''),
instance=None)