diff --git a/nailgun/nailgun/api/v1/handlers/base.py b/nailgun/nailgun/api/v1/handlers/base.py index 99fd1b8bf1..33c933c9e3 100644 --- a/nailgun/nailgun/api/v1/handlers/base.py +++ b/nailgun/nailgun/api/v1/handlers/base.py @@ -37,6 +37,7 @@ from nailgun import objects from nailgun.objects.serializers.base import BasicSerializer from nailgun.orchestrator import orchestrator_graph from nailgun.settings import settings +from nailgun import transactions from nailgun import utils @@ -394,7 +395,6 @@ class SingleHandler(BaseHandler): validator = BasicValidator @handle_errors - @validate @serialize def GET(self, obj_id): """:returns: JSONized REST object. @@ -701,3 +701,22 @@ class OrchestratorDeploymentTasksHandler(SingleHandler): :http: * 405 (method not supported) """ 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) diff --git a/nailgun/nailgun/api/v1/handlers/deployment_graph.py b/nailgun/nailgun/api/v1/handlers/deployment_graph.py index 98046bfd1f..571b433861 100644 --- a/nailgun/nailgun/api/v1/handlers/deployment_graph.py +++ b/nailgun/nailgun/api/v1/handlers/deployment_graph.py @@ -14,7 +14,7 @@ # License for the specific language governing permissions and limitations # under the License. -from nailgun.api.v1.handlers.base import BaseHandler +from nailgun.api.v1.handlers.base import TransactionExecutorHandler import web 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.validators import deployment_graph as validators -from nailgun import errors from nailgun import objects from nailgun.objects.serializers.deployment_graph import \ DeploymentGraphSerializer -from nailgun.transactions import TransactionsManager from nailgun import utils @@ -300,31 +298,23 @@ class DeploymentGraphCollectionHandler(CollectionHandler): return self.collection.to_list(result) -class GraphsExecutorHandler(BaseHandler): +class GraphsExecutorHandler(TransactionExecutorHandler): + """Handler to execute sequence of deployment graphs.""" validator = validators.GraphExecuteParamsValidator @handle_errors - @validate def POST(self): - """:returns: JSONized Task object. + """Execute graph(s) as single transaction. + + :returns: JSONized Task object :http: * 200 (task successfully executed) * 202 (task scheduled for execution) * 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) """ - data = self.checked_data(self.validator.validate_params) - cluster_id = self.get_object_or_404( - objects.Cluster, data.pop('cluster')).id - - 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) + data = self.checked_data() + cluster = self.get_object_or_404(objects.Cluster, data.pop('cluster')) + return self.start_transaction(cluster, data) diff --git a/nailgun/nailgun/api/v1/handlers/deployment_sequence.py b/nailgun/nailgun/api/v1/handlers/deployment_sequence.py new file mode 100644 index 0000000000..6733c74665 --- /dev/null +++ b/nailgun/nailgun/api/v1/handlers/deployment_sequence.py @@ -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) diff --git a/nailgun/nailgun/api/v1/urls.py b/nailgun/nailgun/api/v1/urls.py index 403703f07e..c5d2f9d76d 100644 --- a/nailgun/nailgun/api/v1/urls.py +++ b/nailgun/nailgun/api/v1/urls.py @@ -142,6 +142,11 @@ from nailgun.api.v1.handlers.deployment_graph import \ DeploymentGraphHandler 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 urls = ( @@ -239,6 +244,13 @@ urls = ( r'/graphs/execute/?$', GraphsExecutorHandler, + r'/sequences/?$', + SequenceCollectionHandler, + r'/sequences/(?P\d+)/?$', + SequenceHandler, + r'/sequences/(?P\d+)/execute/?$', + SequenceExecutorHandler, + r'/clusters/(?P\d+)/assignment/?$', NodeAssignmentHandler, r'/clusters/(?P\d+)/unassignment/?$', diff --git a/nailgun/nailgun/api/v1/validators/deployment_graph.py b/nailgun/nailgun/api/v1/validators/deployment_graph.py index 2e9c649b45..3b0b3bf4f4 100644 --- a/nailgun/nailgun/api/v1/validators/deployment_graph.py +++ b/nailgun/nailgun/api/v1/validators/deployment_graph.py @@ -39,7 +39,7 @@ class GraphExecuteParamsValidator(BasicValidator): single_schema = schema.GRAPH_EXECUTE_PARAMS_SCHEMA @classmethod - def validate_params(cls, data): + def validate(cls, data): parsed = cls.validate_json(data) cls.validate_schema(parsed, cls.single_schema) diff --git a/nailgun/nailgun/api/v1/validators/deployment_sequence.py b/nailgun/nailgun/api/v1/validators/deployment_sequence.py new file mode 100644 index 0000000000..fd61742509 --- /dev/null +++ b/nailgun/nailgun/api/v1/validators/deployment_sequence.py @@ -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 diff --git a/nailgun/nailgun/api/v1/validators/json_schema/deployment_graph.py b/nailgun/nailgun/api/v1/validators/json_schema/deployment_graph.py index e7ff88fa9d..9dfa8e260a 100644 --- a/nailgun/nailgun/api/v1/validators/json_schema/deployment_graph.py +++ b/nailgun/nailgun/api/v1/validators/json_schema/deployment_graph.py @@ -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 = { "$schema": "http://json-schema.org/draft-04/schema#", "type": "object", @@ -71,27 +95,7 @@ GRAPH_EXECUTE_PARAMS_SCHEMA = { "cluster": { "type": "integer", }, - "graphs": { - "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"} - } - } - } - }, + "graphs": SEQUENCE_OF_GRAPHS_SCHEMA, "dry_run": { "type": "boolean" }, diff --git a/nailgun/nailgun/api/v1/validators/json_schema/deployment_sequence.py b/nailgun/nailgun/api/v1/validators/json_schema/deployment_sequence.py new file mode 100644 index 0000000000..27029088c1 --- /dev/null +++ b/nailgun/nailgun/api/v1/validators/json_schema/deployment_sequence.py @@ -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"} + } +} diff --git a/nailgun/nailgun/db/migration/alembic_migrations/versions/fuel_9_1.py b/nailgun/nailgun/db/migration/alembic_migrations/versions/fuel_9_1.py index e46d70eae9..3c560db3c1 100644 --- a/nailgun/nailgun/db/migration/alembic_migrations/versions/fuel_9_1.py +++ b/nailgun/nailgun/db/migration/alembic_migrations/versions/fuel_9_1.py @@ -60,9 +60,11 @@ def upgrade(): upgrade_deployment_history_summary() upgrade_add_task_start_end_time() fix_deployment_history_constraint() + upgrade_deployment_sequences() def downgrade(): + downgrade_deployment_sequences() downgrade_add_task_start_end_time() downgrade_cluster_attributes() downgrade_deployment_history_summary() @@ -345,3 +347,20 @@ def fix_deployment_history_constraint(): "deployment_history", "tasks", ["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') diff --git a/nailgun/nailgun/db/sqlalchemy/models/__init__.py b/nailgun/nailgun/db/sqlalchemy/models/__init__.py index 608569eae2..78d1ce8593 100644 --- a/nailgun/nailgun/db/sqlalchemy/models/__init__.py +++ b/nailgun/nailgun/db/sqlalchemy/models/__init__.py @@ -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.deployment_history import DeploymentHistory +from nailgun.db.sqlalchemy.models.deployment_sequence import DeploymentSequence from nailgun.db.sqlalchemy.models.master_node_settings \ import MasterNodeSettings diff --git a/nailgun/nailgun/db/sqlalchemy/models/deployment_history.py b/nailgun/nailgun/db/sqlalchemy/models/deployment_history.py index acf1906f95..00d4311b33 100644 --- a/nailgun/nailgun/db/sqlalchemy/models/deployment_history.py +++ b/nailgun/nailgun/db/sqlalchemy/models/deployment_history.py @@ -16,13 +16,13 @@ import sqlalchemy as sa -from sqlalchemy.ext.mutable import MutableDict from sqlalchemy.orm import deferred 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 class DeploymentHistory(Base): diff --git a/nailgun/nailgun/db/sqlalchemy/models/deployment_sequence.py b/nailgun/nailgun/db/sqlalchemy/models/deployment_sequence.py new file mode 100644 index 0000000000..0ec03961a7 --- /dev/null +++ b/nailgun/nailgun/db/sqlalchemy/models/deployment_sequence.py @@ -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) diff --git a/nailgun/nailgun/extensions/network_manager/tests/test_network_group_handler.py b/nailgun/nailgun/extensions/network_manager/tests/test_network_group_handler.py index abe00b6a41..b0b452a1b7 100644 --- a/nailgun/nailgun/extensions/network_manager/tests/test_network_group_handler.py +++ b/nailgun/nailgun/extensions/network_manager/tests/test_network_group_handler.py @@ -67,10 +67,7 @@ class TestHandlers(BaseIntegrationTest): expect_errors=True ) self.assertEqual(400, resp.status_code) - self.assertIn( - "Failed validating 'enum' in " - "schema['properties']['meta']['properties']['notation']", - resp.json_body["message"]) + self.assertIn("'new' is not one of", resp.json_body["message"]) resp = self.env._create_network_group( meta={"notation": consts.NETWORK_NOTATION.ip_ranges}, diff --git a/nailgun/nailgun/objects/__init__.py b/nailgun/nailgun/objects/__init__.py index d3284f17b2..659001342a 100644 --- a/nailgun/nailgun/objects/__init__.py +++ b/nailgun/nailgun/objects/__init__.py @@ -45,6 +45,9 @@ from nailgun.objects.transaction import TransactionCollection from nailgun.objects.deployment_history import DeploymentHistory 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 NotificationCollection diff --git a/nailgun/nailgun/objects/deployment_sequence.py b/nailgun/nailgun/objects/deployment_sequence.py new file mode 100644 index 0000000000..13d6c34887 --- /dev/null +++ b/nailgun/nailgun/objects/deployment_sequence.py @@ -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 diff --git a/nailgun/nailgun/objects/serializers/deployment_sequence.py b/nailgun/nailgun/objects/serializers/deployment_sequence.py new file mode 100644 index 0000000000..c1c83ebeff --- /dev/null +++ b/nailgun/nailgun/objects/serializers/deployment_sequence.py @@ -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" + ) diff --git a/nailgun/nailgun/test/integration/test_deployment_sequences_handler.py b/nailgun/nailgun/test/integration/test_deployment_sequences_handler.py new file mode 100644 index 0000000000..ab7f690bd3 --- /dev/null +++ b/nailgun/nailgun/test/integration/test_deployment_sequences_handler.py @@ -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) diff --git a/nailgun/nailgun/test/unit/test_downgrade_fuel_9_1.py b/nailgun/nailgun/test/unit/test_downgrade_fuel_9_1.py index edceaeec83..95f869e288 100644 --- a/nailgun/nailgun/test/unit/test_downgrade_fuel_9_1.py +++ b/nailgun/nailgun/test/unit/test_downgrade_fuel_9_1.py @@ -174,3 +174,8 @@ class TestClusterAttributesDowngrade(base.BaseAlembicMigrationTest): sa.select([clusters_table.c.replaced_deployment_info]) ).fetchone()[0] self.assertEqual('[]', deployment_info) + + +class TestDeploymentSequencesDowngrade(base.BaseAlembicMigrationTest): + def test_deployment_sequences_table_removed(self): + self.assertNotIn('deployment_sequences', self.meta.tables) diff --git a/nailgun/nailgun/test/unit/test_graph_executor_handler.py b/nailgun/nailgun/test/unit/test_graph_executor_handler.py index 4b132112dd..96db4d4dca 100644 --- a/nailgun/nailgun/test/unit/test_graph_executor_handler.py +++ b/nailgun/nailgun/test/unit/test_graph_executor_handler.py @@ -25,7 +25,7 @@ from nailgun.test import base 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): def setUp(self): diff --git a/nailgun/nailgun/test/unit/test_migration_fuel_9_1.py b/nailgun/nailgun/test/unit/test_migration_fuel_9_1.py index 351e5670ce..a10a8c97d1 100644 --- a/nailgun/nailgun/test/unit/test_migration_fuel_9_1.py +++ b/nailgun/nailgun/test/unit/test_migration_fuel_9_1.py @@ -17,6 +17,7 @@ import datetime import alembic from oslo_serialization import jsonutils import sqlalchemy as sa +import sqlalchemy.exc as sa_exc from nailgun.db import db from nailgun.db import dropdb @@ -253,3 +254,32 @@ class TestClusterAttributesMigration(base.BaseAlembicMigrationTest): sa.select([clusters_table.c.replaced_deployment_info]) ).fetchone()[0] 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"]', + }] + )