Merge "Itroduced deployment sequences"

This commit is contained in:
Jenkins 2016-09-08 10:25:13 +00:00 committed by Gerrit Code Review
commit 80cc133047
20 changed files with 683 additions and 49 deletions

View File

@ -37,6 +37,7 @@ from nailgun import objects
from nailgun.objects.serializers.base import BasicSerializer from nailgun.objects.serializers.base import BasicSerializer
from nailgun.orchestrator import orchestrator_graph from nailgun.orchestrator import orchestrator_graph
from nailgun.settings import settings from nailgun.settings import settings
from nailgun import transactions
from nailgun import utils from nailgun import utils
@ -394,7 +395,6 @@ class SingleHandler(BaseHandler):
validator = BasicValidator validator = BasicValidator
@handle_errors @handle_errors
@validate
@serialize @serialize
def GET(self, obj_id): def GET(self, obj_id):
""":returns: JSONized REST object. """:returns: JSONized REST object.
@ -701,3 +701,22 @@ class OrchestratorDeploymentTasksHandler(SingleHandler):
:http: * 405 (method not supported) :http: * 405 (method not supported)
""" """
raise self.http(405, 'Delete not supported for this entity') raise self.http(405, 'Delete not supported for this entity')
class TransactionExecutorHandler(BaseHandler):
def start_transaction(self, cluster, options):
"""Starts new transaction.
:param cluster: the cluster object
:param options: the transaction parameters
:return: JSONized task object
"""
try:
manager = transactions.TransactionsManager(cluster.id)
self.raise_task(manager.execute(**options))
except errors.ObjectNotFound as e:
raise self.http(404, e.message)
except errors.DeploymentAlreadyStarted as e:
raise self.http(409, e.message)
except errors.InvalidData as e:
raise self.http(400, e.message)

View File

@ -14,7 +14,7 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
from nailgun.api.v1.handlers.base import BaseHandler from nailgun.api.v1.handlers.base import TransactionExecutorHandler
import web import web
from nailgun.api.v1.handlers.base import CollectionHandler from nailgun.api.v1.handlers.base import CollectionHandler
@ -24,11 +24,9 @@ from nailgun.api.v1.handlers.base import SingleHandler
from nailgun.api.v1.handlers.base import validate from nailgun.api.v1.handlers.base import validate
from nailgun.api.v1.validators import deployment_graph as validators from nailgun.api.v1.validators import deployment_graph as validators
from nailgun import errors
from nailgun import objects from nailgun import objects
from nailgun.objects.serializers.deployment_graph import \ from nailgun.objects.serializers.deployment_graph import \
DeploymentGraphSerializer DeploymentGraphSerializer
from nailgun.transactions import TransactionsManager
from nailgun import utils from nailgun import utils
@ -300,31 +298,23 @@ class DeploymentGraphCollectionHandler(CollectionHandler):
return self.collection.to_list(result) return self.collection.to_list(result)
class GraphsExecutorHandler(BaseHandler): class GraphsExecutorHandler(TransactionExecutorHandler):
"""Handler to execute sequence of deployment graphs."""
validator = validators.GraphExecuteParamsValidator validator = validators.GraphExecuteParamsValidator
@handle_errors @handle_errors
@validate
def POST(self): def POST(self):
""":returns: JSONized Task object. """Execute graph(s) as single transaction.
:returns: JSONized Task object
:http: * 200 (task successfully executed) :http: * 200 (task successfully executed)
* 202 (task scheduled for execution) * 202 (task scheduled for execution)
* 400 (data validation failed) * 400 (data validation failed)
* 404 (cluster or nodes not found in db) * 404 (cluster or sequence not found in db)
* 409 (graph execution is in progress) * 409 (graph execution is in progress)
""" """
data = self.checked_data(self.validator.validate_params) data = self.checked_data()
cluster_id = self.get_object_or_404( cluster = self.get_object_or_404(objects.Cluster, data.pop('cluster'))
objects.Cluster, data.pop('cluster')).id return self.start_transaction(cluster, data)
try:
manager = TransactionsManager(cluster_id)
self.raise_task(manager.execute(**data))
except errors.ObjectNotFound as e:
raise self.http(404, e.message)
except errors.DeploymentAlreadyStarted as e:
raise self.http(409, e.message)
except errors.InvalidData as e:
raise self.http(400, e.message)

View File

@ -0,0 +1,98 @@
# -*- coding: utf-8 -*-
# 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.
from nailgun.api.v1.handlers.base import TransactionExecutorHandler
from nailgun.api.v1.handlers.base import CollectionHandler
from nailgun.api.v1.handlers.base import handle_errors
from nailgun.api.v1.handlers.base import serialize
from nailgun.api.v1.handlers.base import SingleHandler
from nailgun.api.v1.validators import deployment_sequence as validators
from nailgun import objects
class SequenceHandler(SingleHandler):
"""Handler for deployment graph related to model."""
validator = validators.SequenceValidator
single = objects.DeploymentSequence
@handle_errors
@serialize
def PUT(self, obj_id):
""":returns: JSONized REST object.
:http: * 200 (OK)
* 404 (object not found in db)
"""
obj = self.get_object_or_404(self.single, obj_id)
data = self.checked_data(
self.validator.validate_update,
instance=obj
)
self.single.update(obj, data)
return self.single.to_dict(obj)
def PATCH(self, obj_id):
"""Update deployment sequence.
:param obj_id: the deployment sequence id
:returns: updated object
:http: * 200 (OK)
* 400 (invalid data specified)
* 404 (object not found in db)
"""
return self.PUT(obj_id)
class SequenceCollectionHandler(CollectionHandler):
"""Handler for deployment graphs related to the models collection."""
validator = validators.SequenceValidator
collection = objects.DeploymentSequenceCollection
class SequenceExecutorHandler(TransactionExecutorHandler):
"""Handler to execute deployment sequence."""
validator = validators.SequenceExecutorValidator
@handle_errors
def POST(self, obj_id):
"""Execute sequence as single transaction.
:returns: JSONized Task object
:http: * 200 (task successfully executed)
* 202 (task scheduled for execution)
* 400 (data validation failed)
* 404 (cluster or sequence not found in db)
* 409 (graph execution is in progress)
"""
data = self.checked_data()
seq = self.get_object_or_404(objects.DeploymentSequence, id=obj_id)
cluster = self.get_object_or_404(objects.Cluster, data.pop('cluster'))
if cluster.release_id != seq.release_id:
raise self.http(
404,
"Sequence '{0}' is not found for cluster {1}"
.format(seq.name, cluster.name)
)
data['graphs'] = seq.graphs
return self.start_transaction(cluster, data)

View File

@ -142,6 +142,11 @@ from nailgun.api.v1.handlers.deployment_graph import \
DeploymentGraphHandler DeploymentGraphHandler
from nailgun.api.v1.handlers.deployment_graph import GraphsExecutorHandler from nailgun.api.v1.handlers.deployment_graph import GraphsExecutorHandler
from nailgun.api.v1.handlers.deployment_sequence import \
SequenceCollectionHandler
from nailgun.api.v1.handlers.deployment_sequence import SequenceExecutorHandler
from nailgun.api.v1.handlers.deployment_sequence import SequenceHandler
from nailgun.settings import settings from nailgun.settings import settings
urls = ( urls = (
@ -239,6 +244,13 @@ urls = (
r'/graphs/execute/?$', r'/graphs/execute/?$',
GraphsExecutorHandler, GraphsExecutorHandler,
r'/sequences/?$',
SequenceCollectionHandler,
r'/sequences/(?P<obj_id>\d+)/?$',
SequenceHandler,
r'/sequences/(?P<obj_id>\d+)/execute/?$',
SequenceExecutorHandler,
r'/clusters/(?P<cluster_id>\d+)/assignment/?$', r'/clusters/(?P<cluster_id>\d+)/assignment/?$',
NodeAssignmentHandler, NodeAssignmentHandler,
r'/clusters/(?P<cluster_id>\d+)/unassignment/?$', r'/clusters/(?P<cluster_id>\d+)/unassignment/?$',

View File

@ -39,7 +39,7 @@ class GraphExecuteParamsValidator(BasicValidator):
single_schema = schema.GRAPH_EXECUTE_PARAMS_SCHEMA single_schema = schema.GRAPH_EXECUTE_PARAMS_SCHEMA
@classmethod @classmethod
def validate_params(cls, data): def validate(cls, data):
parsed = cls.validate_json(data) parsed = cls.validate_json(data)
cls.validate_schema(parsed, cls.single_schema) cls.validate_schema(parsed, cls.single_schema)

View File

@ -0,0 +1,65 @@
# -*- coding: utf-8 -*-
# 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.
from nailgun.api.v1.validators.base import BasicValidator
from nailgun.api.v1.validators.json_schema import deployment_sequence as schema
from nailgun import errors
from nailgun import objects
class SequenceValidator(BasicValidator):
single_schema = schema.CREATE_SEQUENCE_SCHEMA
update_schema = schema.UPDATE_SEQUENCE_SCHEMA
@classmethod
def validate(cls, data):
parsed = cls.validate_json(data)
cls.validate_schema(
parsed,
cls.single_schema
)
release = objects.Release.get_by_uid(
parsed.pop('release'), fail_if_not_found=True
)
parsed['release_id'] = release.id
if objects.DeploymentSequence.get_by_name_for_release(
release, parsed['name']):
raise errors.AlreadyExists(
'Sequence with name "{0}" already exist for release {1}.'
.format(parsed['name'], release.id)
)
return parsed
@classmethod
def validate_update(cls, data, instance):
parsed = cls.validate_json(data)
cls.validate_schema(parsed, cls.update_schema)
return parsed
@classmethod
def validate_delete(cls, *args, **kwargs):
pass
class SequenceExecutorValidator(BasicValidator):
single_schema = schema.SEQUENCE_EXECUTION_PARAMS
@classmethod
def validate(cls, data):
parsed = cls.validate_json(data)
cls.validate_schema(parsed, cls.single_schema)
return parsed

View File

@ -60,6 +60,30 @@ DEPLOYMENT_GRAPHS_SCHEMA = {
} }
SEQUENCE_OF_GRAPHS_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"required": ["type"],
"parameters": {
"type": {
"type": "string"
},
"nodes": {
"type": "array",
"items": {"type": "integer"}
},
"tasks": {
"type": "array",
"items": {"type": "string"}
}
}
}
}
GRAPH_EXECUTE_PARAMS_SCHEMA = { GRAPH_EXECUTE_PARAMS_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#", "$schema": "http://json-schema.org/draft-04/schema#",
"type": "object", "type": "object",
@ -71,27 +95,7 @@ GRAPH_EXECUTE_PARAMS_SCHEMA = {
"cluster": { "cluster": {
"type": "integer", "type": "integer",
}, },
"graphs": { "graphs": SEQUENCE_OF_GRAPHS_SCHEMA,
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"required": ["type"],
"parameters": {
"type": {
"type": "string"
},
"nodes": {
"type": "array",
"items": {"type": "integer"}
},
"tasks": {
"type": "array",
"items": {"type": "string"}
}
}
}
},
"dry_run": { "dry_run": {
"type": "boolean" "type": "boolean"
}, },

View File

@ -0,0 +1,61 @@
# -*- coding: utf-8 -*-
# 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.
from nailgun.api.v1.validators.json_schema import deployment_graph
CREATE_SEQUENCE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"title": "Deployment Sequence structure",
"description": "The structure of deployment sequence object",
"required": ["release", "name", "graphs"],
"additionalProperties": False,
"properties": {
"name": {"type": "string"},
"release": {"type": "integer"},
"graphs": deployment_graph.SEQUENCE_OF_GRAPHS_SCHEMA
}
}
UPDATE_SEQUENCE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"title": "Deployment Sequence structure",
"description": "The structure of deployment sequence object",
"additionalProperties": False,
"properties": {
"name": {"type": "string"},
"graphs": deployment_graph.SEQUENCE_OF_GRAPHS_SCHEMA
}
}
SEQUENCE_EXECUTION_PARAMS = {
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"title": "Sequence execution parameters",
"description": "The parameters of api method to execute sequence",
"required": ["cluster"],
"additionalProperties": False,
"properties": {
"cluster": {"type": "integer"},
"dry_run": {"type": "boolean"},
"noop_run": {"type": "boolean"},
"force": {"type": "boolean"},
"debug": {"type": "boolean"}
}
}

View File

@ -60,9 +60,11 @@ def upgrade():
upgrade_deployment_history_summary() upgrade_deployment_history_summary()
upgrade_add_task_start_end_time() upgrade_add_task_start_end_time()
fix_deployment_history_constraint() fix_deployment_history_constraint()
upgrade_deployment_sequences()
def downgrade(): def downgrade():
downgrade_deployment_sequences()
downgrade_add_task_start_end_time() downgrade_add_task_start_end_time()
downgrade_cluster_attributes() downgrade_cluster_attributes()
downgrade_deployment_history_summary() downgrade_deployment_history_summary()
@ -345,3 +347,20 @@ def fix_deployment_history_constraint():
"deployment_history", "tasks", "deployment_history", "tasks",
["task_id"], ["id"], ondelete="CASCADE" ["task_id"], ["id"], ondelete="CASCADE"
) )
def upgrade_deployment_sequences():
op.create_table(
'deployment_sequences',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('release_id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(255), nullable=False),
sa.Column('graphs', fields.JSON(), nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.ForeignKeyConstraint(['release_id'], ['releases.id']),
sa.UniqueConstraint('release_id', 'name')
)
def downgrade_deployment_sequences():
op.drop_table('deployment_sequences')

View File

@ -67,6 +67,7 @@ from nailgun.db.sqlalchemy.models.plugin_link import PluginLink
from nailgun.db.sqlalchemy.models.task import Task from nailgun.db.sqlalchemy.models.task import Task
from nailgun.db.sqlalchemy.models.deployment_history import DeploymentHistory from nailgun.db.sqlalchemy.models.deployment_history import DeploymentHistory
from nailgun.db.sqlalchemy.models.deployment_sequence import DeploymentSequence
from nailgun.db.sqlalchemy.models.master_node_settings \ from nailgun.db.sqlalchemy.models.master_node_settings \
import MasterNodeSettings import MasterNodeSettings

View File

@ -16,13 +16,13 @@
import sqlalchemy as sa import sqlalchemy as sa
from sqlalchemy.ext.mutable import MutableDict
from sqlalchemy.orm import deferred from sqlalchemy.orm import deferred
from nailgun import consts from nailgun import consts
from nailgun.db.sqlalchemy.models.base import Base from nailgun.db.sqlalchemy.models.base import Base
from nailgun.db.sqlalchemy.models.fields import JSON from nailgun.db.sqlalchemy.models.fields import JSON
from nailgun.db.sqlalchemy.models.mutable import MutableDict
class DeploymentHistory(Base): class DeploymentHistory(Base):

View File

@ -0,0 +1,33 @@
# 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 sqlalchemy as sa
from nailgun.db.sqlalchemy.models.base import Base
from nailgun.db.sqlalchemy.models.fields import JSON
from nailgun.db.sqlalchemy.models.mutable import MutableList
class DeploymentSequence(Base):
__tablename__ = 'deployment_sequences'
__table_args__ = (
sa.UniqueConstraint('release_id', 'name', name='_name_uc'),
)
id = sa.Column(sa.Integer, primary_key=True)
release_id = sa.Column(
sa.Integer, sa.ForeignKey('releases.id'), nullable=False
)
name = sa.Column(sa.String(255), nullable=False)
# contains graphs to execute
graphs = sa.Column(MutableList.as_mutable(JSON), nullable=False)

View File

@ -67,10 +67,7 @@ class TestHandlers(BaseIntegrationTest):
expect_errors=True expect_errors=True
) )
self.assertEqual(400, resp.status_code) self.assertEqual(400, resp.status_code)
self.assertIn( self.assertIn("'new' is not one of", resp.json_body["message"])
"Failed validating 'enum' in "
"schema['properties']['meta']['properties']['notation']",
resp.json_body["message"])
resp = self.env._create_network_group( resp = self.env._create_network_group(
meta={"notation": consts.NETWORK_NOTATION.ip_ranges}, meta={"notation": consts.NETWORK_NOTATION.ip_ranges},

View File

@ -45,6 +45,9 @@ from nailgun.objects.transaction import TransactionCollection
from nailgun.objects.deployment_history import DeploymentHistory from nailgun.objects.deployment_history import DeploymentHistory
from nailgun.objects.deployment_history import DeploymentHistoryCollection from nailgun.objects.deployment_history import DeploymentHistoryCollection
from nailgun.objects.deployment_sequence import DeploymentSequence
from nailgun.objects.deployment_sequence import DeploymentSequenceCollection
from nailgun.objects.notification import Notification from nailgun.objects.notification import Notification
from nailgun.objects.notification import NotificationCollection from nailgun.objects.notification import NotificationCollection

View File

@ -0,0 +1,61 @@
# -*- coding: utf-8 -*-
# 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.
from nailgun.db import db
from nailgun.db.sqlalchemy import models
from nailgun import errors
from nailgun.objects import NailgunCollection
from nailgun.objects import NailgunObject
from nailgun.objects.serializers import deployment_sequence as serializer
class DeploymentSequence(NailgunObject):
model = models.DeploymentSequence
serializer = serializer.DeploymentSequenceSerializer
@classmethod
def get_by_name_for_release(cls, release, name,
fail_if_not_found=False,
lock_for_update=False):
"""Get sequence by name.
:param release: the release object
:param name: the name of sequence
:param fail_if_not_found: True means raising of exception
in case if object is not found
:param lock_for_update: True means acquiring exclusive access
for object
:return: deployment sequence object
"""
q = db().query(cls.model).filter_by(release_id=release.id, name=name)
if lock_for_update:
q = q.order_by('id')
q = q.with_lockmode('update')
res = q.first()
if not res and fail_if_not_found:
raise errors.ObjectNotFound(
"Sequence with name='{0}' is not found for release {1}"
.format(name, release.id)
)
return res
class DeploymentSequenceCollection(NailgunCollection):
single = DeploymentSequence

View File

@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
# 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.
from nailgun.objects.serializers.base import BasicSerializer
class DeploymentSequenceSerializer(BasicSerializer):
fields = (
"id",
"name",
"graphs",
"release_id"
)

View File

@ -0,0 +1,209 @@
# -*- coding: utf-8 -*-
# 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 mock
from oslo_serialization import jsonutils
from nailgun import consts
from nailgun import objects
from nailgun.test import base
from nailgun.utils import reverse
class TestSequencesHandler(base.BaseIntegrationTest):
def setUp(self):
super(TestSequencesHandler, self).setUp()
self.release = self.env.create_release(
version='mitaka-9.0', operating_system=consts.RELEASE_OS.ubuntu
)
def _create_sequence(self, name, expect_errors=False, release_id=None):
return self.app.post(
reverse('SequenceCollectionHandler'),
params=jsonutils.dumps(
{
"name": name,
'release': release_id or self.release.id,
"graphs": [{'type': 'test_graph'}],
}
),
headers=self.default_headers,
expect_errors=expect_errors
)
def test_create_new_sequence(self):
self._create_sequence('test')
sequence = objects.DeploymentSequence.get_by_name_for_release(
self.release, 'test'
)
self.assertIsNotNone(sequence)
self.assertEqual([{'type': 'test_graph'}], sequence.graphs)
def test_create_sequence_with_same_name(self):
self._create_sequence('test')
resp = self._create_sequence('test', True)
self.assertEqual(409, resp.status_code)
release2 = self.env.create_release(
version='newton-10.0', operating_system=consts.RELEASE_OS.ubuntu
)
self._create_sequence('test', release_id=release2.id)
def test_update_sequence(self):
resp = self._create_sequence('test')
resp = self.app.put(
reverse('SequenceHandler', {'obj_id': resp.json_body['id']}),
params=jsonutils.dumps(
{
"graphs": [{'type': 'test_graph2'}],
}
),
headers=self.default_headers
)
self.assertEqual([{'type': 'test_graph2'}], resp.json_body['graphs'])
sequence = objects.DeploymentSequence.get_by_name_for_release(
self.release, 'test'
)
self.assertEqual(resp.json_body['graphs'], sequence.graphs)
def test_delete_sequence(self):
resp = self._create_sequence('test')
self.app.delete(
reverse('SequenceHandler', {'obj_id': resp.json_body['id']}),
headers=self.default_headers
)
sequence = objects.DeploymentSequence.get_by_name_for_release(
self.release, 'test'
)
self.assertIsNone(sequence)
class TestSequenceExecutorHandler(base.BaseIntegrationTest):
def setUp(self):
super(TestSequenceExecutorHandler, self).setUp()
self.cluster = self.env.create(
cluster_kwargs={},
nodes_kwargs=[
{"status": consts.NODE_STATUSES.discover},
],
release_kwargs={
'version': 'mitaka-9.0',
'operating_system': consts.RELEASE_OS.ubuntu
}
)
objects.DeploymentGraph.create_for_model(
{
'tasks': [
{
'id': 'test_task',
'type': consts.ORCHESTRATOR_TASK_TYPES.puppet,
'roles': ['/.*/']
},
],
'name': 'test_graph',
},
instance=self.cluster,
graph_type='test_graph'
)
self.sequence = objects.DeploymentSequence.create(
{'name': 'test', 'release_id': self.cluster.release_id,
'graphs': [{'type': 'test_graph'}]}
)
self.expected_metadata = {
'fault_tolerance_groups': [],
'node_statuses_transitions': {
'successful': {'status': consts.NODE_STATUSES.ready},
'failed': {'status': consts.NODE_STATUSES.error},
'stopped': {'status': consts.NODE_STATUSES.stopped}}
}
@mock.patch('nailgun.transactions.manager.rpc')
def test_execute_for_cluster(self, rpc_mock):
resp = self.app.post(
reverse('SequenceExecutorHandler',
kwargs={'obj_id': self.sequence.id}),
params=jsonutils.dumps(
{
"cluster": self.cluster.id,
"debug": True,
"noop_run": True,
"dry_run": True,
}
),
headers=self.default_headers
)
self.assertEqual(202, resp.status_code)
task = objects.Task.get_by_uid(resp.json_body['id'])
sub_task = task.subtasks[0]
rpc_mock.cast.assert_called_once_with(
'naily',
[{
'args': {
'tasks_metadata': self.expected_metadata,
'task_uuid': sub_task.uuid,
'tasks_graph': {
None: [],
self.cluster.nodes[0].uid: [
{
'id': 'test_task',
'type': 'puppet',
'fail_on_error': True,
'parameters': {'cwd': '/'}
},
]
},
'tasks_directory': {},
'dry_run': True,
'noop_run': True,
'debug': True
},
'respond_to': 'transaction_resp',
'method': 'task_deploy',
'api_version': '1'
}]
)
def test_execute_for_different_cluster(self):
cluster2 = self.env.create(
cluster_kwargs={},
nodes_kwargs=[
{"status": consts.NODE_STATUSES.discover},
],
release_kwargs={
'version': 'newton-10.0',
'operating_system': consts.RELEASE_OS.ubuntu
}
)
resp = self.app.post(
reverse('SequenceExecutorHandler',
kwargs={'obj_id': self.sequence.id}),
params=jsonutils.dumps(
{
"cluster": cluster2.id,
"debug": True,
"noop_run": True,
"dry_run": True,
}
),
headers=self.default_headers,
expect_errors=True
)
self.assertEqual(404, resp.status_code)

View File

@ -174,3 +174,8 @@ class TestClusterAttributesDowngrade(base.BaseAlembicMigrationTest):
sa.select([clusters_table.c.replaced_deployment_info]) sa.select([clusters_table.c.replaced_deployment_info])
).fetchone()[0] ).fetchone()[0]
self.assertEqual('[]', deployment_info) self.assertEqual('[]', deployment_info)
class TestDeploymentSequencesDowngrade(base.BaseAlembicMigrationTest):
def test_deployment_sequences_table_removed(self):
self.assertNotIn('deployment_sequences', self.meta.tables)

View File

@ -25,7 +25,7 @@ from nailgun.test import base
from nailgun.utils import reverse from nailgun.utils import reverse
@mock.patch("nailgun.api.v1.handlers.deployment_graph.TransactionsManager") @mock.patch("nailgun.api.v1.handlers.base.transactions.TransactionsManager")
class TestGraphExecutorHandler(base.BaseTestCase): class TestGraphExecutorHandler(base.BaseTestCase):
def setUp(self): def setUp(self):

View File

@ -17,6 +17,7 @@ import datetime
import alembic import alembic
from oslo_serialization import jsonutils from oslo_serialization import jsonutils
import sqlalchemy as sa import sqlalchemy as sa
import sqlalchemy.exc as sa_exc
from nailgun.db import db from nailgun.db import db
from nailgun.db import dropdb from nailgun.db import dropdb
@ -253,3 +254,32 @@ class TestClusterAttributesMigration(base.BaseAlembicMigrationTest):
sa.select([clusters_table.c.replaced_deployment_info]) sa.select([clusters_table.c.replaced_deployment_info])
).fetchone()[0] ).fetchone()[0]
self.assertEqual('{}', deployment_info) self.assertEqual('{}', deployment_info)
class TestDeploymentSequencesMigration(base.BaseAlembicMigrationTest):
def test_deployment_sequences_table_exists(self):
deployment_sequences = self.meta.tables['deployment_sequences']
release_id = db.execute(
sa.select([self.meta.tables['releases'].c.id])
).fetchone()[0]
db.execute(
deployment_sequences.insert(),
[{
'release_id': release_id,
'name': 'test',
'graphs': '["test_graph"]',
}]
)
result = db.execute(sa.select([
deployment_sequences.c.name, deployment_sequences.c.graphs
]).where(deployment_sequences.c.name == 'test')).fetchone()
self.assertEqual('test', result[0])
self.assertEqual('["test_graph"]', result[1])
with self.assertRaises(sa_exc.IntegrityError):
db.execute(
deployment_sequences.insert(),
[{
'name': 'test',
'graphs': '["test_graph2"]',
}]
)