diff --git a/murano/api/v1/actions.py b/murano/api/v1/actions.py new file mode 100644 index 00000000..1785cfd0 --- /dev/null +++ b/murano/api/v1/actions.py @@ -0,0 +1,73 @@ +# 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. + +from webob import exc + +from murano.common import policy +from murano.db import models +from murano.db.services import environments as envs +from murano.db.services import sessions +from murano.db import session as db_session + +from murano.openstack.common.gettextutils import _ # noqa +from murano.openstack.common import log as logging +from murano.openstack.common import wsgi +from murano.services import actions + + +LOG = logging.getLogger(__name__) + + +class Controller(object): + def execute(self, request, environment_id, action_id, body): + policy.check("execute_action", request.context, {}) + + LOG.debug('Action:Execute '.format(action_id)) + + unit = db_session.get_session() + environment = unit.query(models.Environment).get(environment_id) + + if environment is None: + LOG.info(_('Environment ' + 'is not found').format(environment_id)) + raise exc.HTTPNotFound + + if environment.tenant_id != request.context.tenant: + LOG.info(_('User is not authorized to access ' + 'this tenant resources.')) + raise exc.HTTPUnauthorized + + # no new session can be opened if environment has deploying status + env_status = envs.EnvironmentServices.get_status(environment_id) + if env_status in (envs.EnvironmentStatus.DEPLOYING, + envs.EnvironmentStatus.DELETING): + LOG.info(_('Could not open session for environment ,' + 'environment has deploying ' + 'status.').format(environment_id)) + raise exc.HTTPForbidden() + + user_id = request.context.user + session = sessions.SessionServices.create(environment_id, user_id) + + if not sessions.SessionServices.validate(session): + LOG.error(_('Session ' + 'is invalid').format(session.id)) + raise exc.HTTPForbidden() + + actions.ActionServices.execute(action_id, session, unit, + request.context.auth_token, body or {}) + + +def create_resource(): + return wsgi.Resource(Controller()) diff --git a/murano/api/v1/deployments.py b/murano/api/v1/deployments.py index 8f214772..ce9cb885 100644 --- a/murano/api/v1/deployments.py +++ b/murano/api/v1/deployments.py @@ -15,6 +15,7 @@ from sqlalchemy import desc from webob import exc from murano.api.v1 import request_statistics +from murano.common.helpers import token_sanitizer from murano.common import policy from murano.common import utils from murano.db import models @@ -37,10 +38,13 @@ class Controller(object): unit = db_session.get_session() verify_and_get_env(unit, environment_id, request) - query = unit.query(models.Deployment) \ + query = unit.query(models.Task) \ .filter_by(environment_id=environment_id) \ - .order_by(desc(models.Deployment.created)) + .order_by(desc(models.Task.created)) result = query.all() + # show only tasks with 'deploy' action + result = [task for task in result + if (task.action or {}).get('method', 'deploy') == 'deploy'] deployments = [set_dep_state(deployment, unit).to_dict() for deployment in result] return {'deployments': deployments} @@ -53,7 +57,7 @@ class Controller(object): unit = db_session.get_session() query = unit.query(models.Status) \ - .filter_by(deployment_id=deployment_id) \ + .filter_by(task_id=deployment_id) \ .order_by(models.Status.created) deployment = verify_and_get_deployment(unit, environment_id, deployment_id) @@ -88,12 +92,12 @@ def verify_and_get_env(db_session, environment_id, request): def _patch_description(description): - description['services'] = description.get('applications', []) - del description['applications'] + description['services'] = description.pop('applications', []) + return token_sanitizer.TokenSanitizer().sanitize(description) def verify_and_get_deployment(db_session, environment_id, deployment_id): - deployment = db_session.query(models.Deployment).get(deployment_id) + deployment = db_session.query(models.Task).get(deployment_id) if not deployment: LOG.info(_('Deployment with id {0} not found').format(deployment_id)) raise exc.HTTPNotFound @@ -103,7 +107,7 @@ def verify_and_get_deployment(db_session, environment_id, deployment_id): environment_id)) raise exc.HTTPBadRequest - _patch_description(deployment.description) + deployment.description = _patch_description(deployment.description) return deployment @@ -114,11 +118,11 @@ def create_resource(): def set_dep_state(deployment, unit): num_errors = unit.query(models.Status).filter_by( level='error', - deployment_id=deployment.id).count() + task_id=deployment.id).count() num_warnings = unit.query(models.Status).filter_by( level='warning', - deployment_id=deployment.id).count() + task_id=deployment.id).count() if deployment.finished: if num_errors: @@ -135,5 +139,5 @@ def set_dep_state(deployment, unit): else: deployment.state = 'running' - _patch_description(deployment.description) + deployment.description = _patch_description(deployment.description) return deployment diff --git a/murano/api/v1/router.py b/murano/api/v1/router.py index 6d287169..9982de64 100644 --- a/murano/api/v1/router.py +++ b/murano/api/v1/router.py @@ -13,6 +13,7 @@ # under the License. import routes +from murano.api.v1 import actions from murano.api.v1 import catalog from murano.api.v1 import deployments from murano.api.v1 import environments @@ -140,6 +141,12 @@ class API(wsgi.Router): action='get_aggregated', conditions={'method': ['GET']}) + actions_resource = actions.create_resource() + mapper.connect('/environments/{environment_id}/actions/{action_id}', + controller=actions_resource, + action='execute', + conditions={'method': ['POST']}) + catalog_resource = catalog.create_resource() mapper.connect('/catalog/packages/categories', controller=catalog_resource, diff --git a/murano/api/v1/sessions.py b/murano/api/v1/sessions.py index 2fa89b6a..665eb8ed 100644 --- a/murano/api/v1/sessions.py +++ b/murano/api/v1/sessions.py @@ -24,7 +24,6 @@ from murano.openstack.common.gettextutils import _ # noqa from murano.openstack.common import log as logging from murano.openstack.common import wsgi - LOG = logging.getLogger(__name__) API_NAME = 'Sessions' @@ -145,7 +144,7 @@ class Controller(object): LOG.error(msg) raise exc.HTTPForbidden(explanation=msg) - sessions.SessionServices.deploy(session, + envs.EnvironmentServices.deploy(session, unit, request.context.auth_token) diff --git a/murano/common/server.py b/murano/common/server.py index bbb3f276..f094407b 100644 --- a/murano/common/server.py +++ b/murano/common/server.py @@ -75,9 +75,9 @@ class ResultEndpoint(object): deployment.finished = timeutils.utcnow() num_errors = unit.query(models.Status)\ - .filter_by(level='error', deployment_id=deployment.id).count() + .filter_by(level='error', task_id=deployment.id).count() num_warnings = unit.query(models.Status)\ - .filter_by(level='warning', deployment_id=deployment.id).count() + .filter_by(level='warning', task_id=deployment.id).count() final_status_text = action_name + ' finished' if num_errors: @@ -87,7 +87,7 @@ class ResultEndpoint(object): final_status_text += " with warnings" status = models.Status() - status.deployment_id = deployment.id + status.task_id = deployment.id status.text = final_status_text status.level = 'info' deployment.statuses.append(status) @@ -170,9 +170,9 @@ def report_notification(report): def get_last_deployment(unit, env_id): - query = unit.query(models.Deployment)\ - .filter_by(environment_id=env_id)\ - .order_by(desc(models.Deployment.started)) + query = unit.query(models.Task) \ + .filter_by(environment_id=env_id) \ + .order_by(desc(models.Task.started)) return query.first() diff --git a/murano/db/migration/alembic_migrations/versions/003_add_action_entry.py b/murano/db/migration/alembic_migrations/versions/003_add_action_entry.py new file mode 100644 index 00000000..ac2f88b4 --- /dev/null +++ b/murano/db/migration/alembic_migrations/versions/003_add_action_entry.py @@ -0,0 +1,92 @@ +# 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. + +""" +Add action column to deployment table. + +Revision ID: 003 +Revises: table deployment +Create Date: 2014-07-30 16:11:33.244 + +""" + +# revision identifiers, used by Alembic. +revision = '003' +down_revision = '002' + +from alembic import op +import sqlalchemy as sa + +import murano.db.migration.helpers as helpers + +MYSQL_ENGINE = 'InnoDB' +MYSQL_CHARSET = 'utf8' + + +def upgrade(): + op.rename_table('deployment', 'task') + op.add_column( + 'task', + sa.Column('action', sa.types.Text()) + ) + op.create_table( + 'deployment', + sa.Column('id', sa.String(length=36), nullable=False)) + + helpers.transform_table( + 'status', {'deployment_id': 'task_id'}, {}, + sa.Column('created', sa.DateTime(), nullable=False), + sa.Column('updated', sa.DateTime(), nullable=False), + sa.Column('id', sa.String(length=36), nullable=False), + sa.Column('entity_id', sa.String(length=255), nullable=True), + sa.Column('entity', sa.String(length=10), nullable=True), + sa.Column('task_id', sa.String(length=36), nullable=True), + sa.Column('text', sa.Text(), nullable=False), + sa.Column('level', sa.String(length=32), nullable=False), + sa.Column('details', sa.Text(), nullable=True), + sa.ForeignKeyConstraint(['task_id'], ['task.id'], ), + sa.PrimaryKeyConstraint('id'), + mysql_engine=MYSQL_ENGINE, + mysql_charset=MYSQL_CHARSET + ) + + op.drop_table('deployment') + + +def downgrade(): + op.drop_column('task', 'action') + op.rename_table('task', 'deployment') + + op.create_table( + 'task', + sa.Column('id', sa.String(length=36), nullable=False)) + + helpers.transform_table( + 'status', {'task_id': 'deployment_id'}, {}, + sa.Column('created', sa.DateTime(), nullable=False), + sa.Column('updated', sa.DateTime(), nullable=False), + sa.Column('id', sa.String(length=36), nullable=False), + sa.Column('entity_id', sa.String(length=255), nullable=True), + sa.Column('entity', sa.String(length=10), nullable=True), + sa.Column('deployment_id', sa.String(length=36), nullable=True), + sa.Column('text', sa.Text(), nullable=False), + sa.Column('level', sa.String(length=32), nullable=False), + sa.Column('details', sa.Text(), nullable=True), + sa.ForeignKeyConstraint(['deployment_id'], ['deployment.id'], ), + sa.PrimaryKeyConstraint('id'), + mysql_engine=MYSQL_ENGINE, + mysql_charset=MYSQL_CHARSET + ) + + op.drop_table('task') + ### end Alembic commands ### diff --git a/murano/db/migration/helpers.py b/murano/db/migration/helpers.py new file mode 100644 index 00000000..5f6eecdd --- /dev/null +++ b/murano/db/migration/helpers.py @@ -0,0 +1,56 @@ +# 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. + +from alembic import op +import sqlalchemy as sa + + +def transform_table(name, renames, defaults, *columns, **kw): + def escape(val): + if isinstance(val, (str, unicode)): + return "'{0}'".format(val) + elif val is None: + return 'NULL' + else: + return val + + engine = op.get_bind() + meta = sa.MetaData(bind=engine) + meta.reflect() + new_name = name + '__tmp' + old_table = meta.tables[name] + mapping = dict( + (renames.get(col.name, col.name), col.name) for col in old_table.c + ) + + columns_to_select = [ + old_table.c[mapping[c.name]] + if c.name in mapping else escape(defaults.get(c.name)) + for c in columns if isinstance(c, sa.Column) + ] + select_as = [ + c.name for c in columns if isinstance(c, sa.Column) + ] + select = sa.sql.select(columns_to_select) + + op.create_table(new_name, *columns, **kw) + meta.reflect() + new_table = meta.tables[new_name] + insert = sa.sql.insert(new_table) + if engine.dialect.dialect_description == 'postgresql+psycopg2': + insert = insert.returning(next(iter(new_table.primary_key.columns))) + insert = insert.from_select(select_as, select) + engine.execute(insert) + op.drop_table(name) + op.rename_table(new_name, name) diff --git a/murano/db/models.py b/murano/db/models.py index 324146be..577a37de 100644 --- a/murano/db/models.py +++ b/murano/db/models.py @@ -68,8 +68,9 @@ class Environment(Base, TimestampMixin): sessions = sa_orm.relationship("Session", backref='environment', cascade='save-update, merge, delete') - deployments = sa_orm.relationship("Deployment", backref='environment', - cascade='save-update, merge, delete') + + tasks = sa_orm.relationship('Task', backref='environment', + cascade='save-update, merge, delete') def to_dict(self): dictionary = super(Environment, self).to_dict() @@ -99,22 +100,22 @@ class Session(Base, TimestampMixin): return dictionary -class Deployment(Base, TimestampMixin): - __tablename__ = 'deployment' +class Task(Base, TimestampMixin): + __tablename__ = 'task' - id = sa.Column(sa.String(36), - primary_key=True, + id = sa.Column(sa.String(36), primary_key=True, default=uuidutils.generate_uuid) started = sa.Column(sa.DateTime, default=timeutils.utcnow, nullable=False) finished = sa.Column(sa.DateTime, default=None, nullable=True) description = sa.Column(st.JsonBlob(), nullable=False) environment_id = sa.Column(sa.String(255), sa.ForeignKey('environment.id')) - statuses = sa_orm.relationship("Status", backref='deployment', + action = sa.Column(st.JsonBlob()) + + statuses = sa_orm.relationship("Status", backref='task', cascade='save-update, merge, delete') def to_dict(self): - dictionary = super(Deployment, self).to_dict() - # del dictionary["description"] + dictionary = super(Task, self).to_dict() if 'statuses' in dictionary: del dictionary['statuses'] if 'environment' in dictionary: @@ -130,8 +131,8 @@ class Status(Base, TimestampMixin): default=uuidutils.generate_uuid) entity_id = sa.Column(sa.String(255), nullable=True) entity = sa.Column(sa.String(10), nullable=True) - deployment_id = sa.Column(sa.String(36), sa.ForeignKey('deployment.id')) - text = sa.Column(sa.Text(), nullable=False) + task_id = sa.Column(sa.String(32), sa.ForeignKey('task.id')) + text = sa.Column(sa.String(), nullable=False) level = sa.Column(sa.String(32), nullable=False) details = sa.Column(sa.Text(), nullable=True) @@ -297,7 +298,7 @@ def register_models(engine): """ Creates database tables for all models with the given engine """ - models = (Environment, Status, Session, Deployment, + models = (Environment, Status, Session, Task, ApiStats, Package, Category, Class, Instance) for model in models: model.metadata.create_all(engine) @@ -307,7 +308,7 @@ def unregister_models(engine): """ Drops database tables for all models with the given engine """ - models = (Environment, Status, Session, Deployment, + models = (Environment, Status, Session, Task, ApiStats, Package, Category, Class) for model in models: model.metadata.drop_all(engine) diff --git a/murano/db/services/actions.py b/murano/db/services/actions.py new file mode 100644 index 00000000..b4a1f7e7 --- /dev/null +++ b/murano/db/services/actions.py @@ -0,0 +1,39 @@ +# 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 murano.common.helpers import token_sanitizer +from murano.db import models +from murano.services import state + + +def get_environment(session, unit): + environment = unit.query(models.Environment).get( + session.environment_id) + return environment + + +def update_task(action, session, task, unit): + session.state = state.SessionState.deploying + task_info = models.Task() + task_info.environment_id = session.environment_id + objects = session.description.get('Objects', None) + if objects: + task_info.description = token_sanitizer.TokenSanitizer().sanitize( + dict(session.description.get('Objects'))) + task_info.action = task['action'] + status = models.Status() + status.text = 'Action {0} is scheduled'.format(action) + status.level = 'info' + task_info.statuses.append(status) + with unit.begin(): + unit.add(session) + unit.add(task_info) diff --git a/murano/db/services/environments.py b/murano/db/services/environments.py index fdebd98e..8730d658 100644 --- a/murano/db/services/environments.py +++ b/murano/db/services/environments.py @@ -1,4 +1,4 @@ -# Copyright (c) 2013 Mirantis, Inc. +# Copyright (c) 2013 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 @@ -49,7 +49,7 @@ class EnvironmentServices(object): :return: Returns list of environments """ unit = db_session.get_session() - environments = unit.query(models.Environment).\ + environments = unit.query(models.Environment). \ filter_by(**filters).all() for env in environments: @@ -174,11 +174,11 @@ class EnvironmentServices(object): if session.state != sessions.SessionState.DEPLOYED: env_description = session.description else: - env = unit.query(models.Environment)\ + env = unit.query(models.Environment) \ .get(session.environment_id) env_description = env.description else: - env = unit.query(models.Environment)\ + env = unit.query(models.Environment) \ .get(session.environment_id) env_description = env.description else: @@ -229,3 +229,14 @@ class EnvironmentServices(object): 'flat': None } } + + @staticmethod + def deploy(session, unit, token): + environment = unit.query(models.Environment).get( + session.environment_id) + + if (session.description['Objects'] is None and + 'ObjectsCopy' not in session.description): + EnvironmentServices.remove(session.environment_id) + else: + sessions.SessionServices.deploy(session, environment, unit, token) diff --git a/murano/db/services/sessions.py b/murano/db/services/sessions.py index 8b91f937..97fe5f86 100644 --- a/murano/db/services/sessions.py +++ b/murano/db/services/sessions.py @@ -11,12 +11,12 @@ # 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 -from murano.common.helpers import token_sanitizer -from murano.common import rpc from murano.db import models from murano.db import session as db_session +from murano.services import actions SessionState = collections.namedtuple('SessionState', [ @@ -116,7 +116,7 @@ class SessionServices(object): return True @staticmethod - def deploy(session, unit, token): + def deploy(session, environment, unit, token): """ Prepares environment for deployment and send deployment command to orchestration engine @@ -127,47 +127,10 @@ class SessionServices(object): """ #Set X-Auth-Token for conductor - environment = unit.query(models.Environment).get( - session.environment_id) deleted = session.description['Objects'] is None - action = None - if not deleted: - action = { - 'object_id': environment.id, - 'method': 'deploy', - 'args': {} - } - - task = { - 'action': action, - 'model': session.description, - 'token': token, - 'tenant_id': environment.tenant_id, - 'id': environment.id - } - - if not deleted: - task['model']['Objects']['?']['id'] = environment.id - task['model']['Objects']['applications'] = \ - task['model']['Objects'].get('services', []) - - if 'services' in task['model']['Objects']: - del task['model']['Objects']['services'] - - session.state = SessionState.DELETING if deleted \ - else SessionState.DEPLOYING - deployment = models.Deployment() - deployment.environment_id = session.environment_id - deployment.description = token_sanitizer.TokenSanitizer().sanitize( - session.description.get('Objects')) - status = models.Status() - status.text = ('Delete' if deleted else 'Deployment') + ' scheduled' - status.level = 'info' - deployment.statuses.append(status) - - with unit.begin(): - unit.add(session) - unit.add(deployment) - - rpc.engine().handle_task(task) + action_name = None if deleted else 'deploy' + actions.ActionServices.submit_task( + action_name, environment.id, + {}, environment, session, + token, unit) diff --git a/murano/services/__init__.py b/murano/services/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/murano/services/actions.py b/murano/services/actions.py new file mode 100644 index 00000000..2350c7eb --- /dev/null +++ b/murano/services/actions.py @@ -0,0 +1,107 @@ +# 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 murano.common import rpc +from murano.db import models +from murano.db.services import actions as actions_db +from murano.services import state as states + + +class ActionServices(object): + @staticmethod + def create_action_task(action_name, target_obj, + args, environment, session, token): + action = None + if action_name and target_obj: + action = { + 'object_id': target_obj, + 'method': action_name, + 'args': args or {} + } + task = { + 'action': action, + 'model': session.description, + 'token': token, + 'tenant_id': environment.tenant_id, + 'id': environment.id + } + if session.description['Objects'] is not None: + task['model']['Objects']['?']['id'] = environment.id + task['model']['Objects']['applications'] = \ + task['model']['Objects'].pop('services', []) + + return task + + @staticmethod + def update_task(action, session, task, unit): + session.state = states.SessionState.deploying + task_info = models.Task() + task_info.environment_id = session.environment_id + task_info.description = dict(session.description.get('Objects')) + task_info.action = task['action'] + status = models.Status() + status.text = 'Action {0} is scheduled'.format(action[1]['name']) + status.level = 'info' + task_info.statuses.append(status) + with unit.begin(): + unit.add(session) + unit.add(task_info) + + @staticmethod + def submit_task(action_name, target_obj, + args, environment, session, token, unit): + task = ActionServices.create_action_task( + action_name, target_obj, args, + environment, session, token) + actions_db.update_task(action_name, session, task, unit) + rpc.engine().handle_task(task) + + @staticmethod + def execute(action_id, session, unit, token, args={}): + environment = actions_db.get_environment(session, unit) + action = ActionServices.find_action(session.description, action_id) + if action is None: + raise LookupError('Action is not found') + if not action[1].get('enabled', True): + raise ValueError('Cannot execute disabled action') + + ActionServices.submit_task(action[1]['name'], action[0], + args, environment, session, token, unit) + + @staticmethod + def find_action(model, action_id): + """ + Traverses object model looking for an object definition + containing specified action + + :param model: object model + :param action_id: ID of an action + :return: tuple (object id, {"name": "action_name_in_MuranoPL", + "enabled": True }) + """ + if isinstance(model, list): + for item in model: + result = ActionServices.find_action(item, action_id) + if result is not None: + return result + elif isinstance(model, dict): + if '?' in model and 'id' in model['?'] and \ + '_actions' in model['?'] and \ + action_id in model['?']['_actions']: + return model['?']['id'], model['?']['_actions'][action_id] + + for obj in model.values(): + result = ActionServices.find_action(obj, action_id) + if result is not None: + return result + else: + return None diff --git a/murano/services/state.py b/murano/services/state.py new file mode 100644 index 00000000..13aaafc7 --- /dev/null +++ b/murano/services/state.py @@ -0,0 +1,20 @@ +# Copyright (c) 2013 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 + +SessionState = collections.namedtuple('SessionState', [ + 'open', 'deploying', 'deployed' +])( + open='open', deploying='deploying', deployed='deployed' +) diff --git a/murano/tests/test_actions.py b/murano/tests/test_actions.py new file mode 100644 index 00000000..1afc8eb9 --- /dev/null +++ b/murano/tests/test_actions.py @@ -0,0 +1,135 @@ +# 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 mock + +from murano.dsl import murano_method +from murano.dsl import results_serializer +from murano.services import actions +from murano import tests + + +class TestActionsSerializer(tests.base.MuranoTestCase): + def setUp(self): + super(TestActionsSerializer, self).setUp() + + def test_old_actions_deletion(self): + old = { + 'action1': {'name': 'name1', 'enabled': True}, + 'action2': {'name': 'name2', 'enabled': True}, + 'action3': {'name': 'name3', 'enabled': True}, + } + new = { + 'action2': {'name': 'name2', 'enabled': False}, + 'action3': {'name': 'name3', 'enabled': True}, + } + + result = results_serializer._merge_actions(old, new) + + self.assertEqual(2, len(result)) + self.assertNotIn('action1', result) + + def test_actions_state_update(self): + old = { + 'action1': {'name': 'name1', 'enabled': True}, + 'action2': {'name': 'name2', 'enabled': True}, + } + new = { + 'action1': {'name': 'name2', 'enabled': False}, + 'action2': {'name': 'name3', 'enabled': True}, + } + + result = results_serializer._merge_actions(old, new) + + self.assertFalse(result['action1']['enabled']) + + def _get_mocked_obj(self): + method1 = mock.Mock() + method1.usage = murano_method.MethodUsages.Action + method2 = mock.Mock() + method2.usage = murano_method.MethodUsages.Runtime + method3 = mock.Mock() + method3.usage = murano_method.MethodUsages.Action + + obj2_type = mock.Mock() + obj2_type.parents = [] + obj2_type.methods = {'method3': method3} + + obj = mock.Mock() + obj.object_id = 'id1' + obj.type.parents = [obj2_type] + obj.type.methods = {'method1': method1, 'method2': method2} + + return obj + + def test_object_actions_serialization(self): + obj = self._get_mocked_obj() + + obj_actions = results_serializer._serialize_available_action(obj) + + expected_result = {'name': 'method1', 'enabled': True} + self.assertIn('id1_method1', obj_actions) + self.assertEqual(expected_result, obj_actions['id1_method1']) + + def test_that_only_actions_are_serialized(self): + obj = self._get_mocked_obj() + obj_actions = results_serializer._serialize_available_action(obj) + self.assertNotIn('id1_method2', obj_actions) + + def test_parent_actions_are_serialized(self): + obj = self._get_mocked_obj() + + obj_actions = results_serializer._serialize_available_action(obj) + + expected_result = {'name': 'method3', 'enabled': True} + self.assertIn('id1_method3', obj_actions) + self.assertEqual(expected_result, obj_actions['id1_method3']) + + +class TestActionFinder(tests.base.MuranoTestCase): + def setUp(self): + super(TestActionFinder, self).setUp() + + def test_simple_root_level_search(self): + model = { + '?': { + 'id': 'id1', + '_actions': { + 'ad_deploy': { + 'enabled': True, + 'name': 'deploy' + } + } + } + } + action = actions.ActionServices.find_action(model, 'ad_deploy') + self.assertEqual('deploy', action[1]['name']) + + def test_recursive_action_search(self): + model = { + '?': { + 'id': 'id1', + '_actions': {'ad_deploy': {'enabled': True, 'name': 'deploy'}} + }, + 'property': { + '?': { + 'id': 'id2', + '_actions': { + 'ad_scale': {'enabled': True, 'name': 'scale'} + } + }, + } + } + action = actions.ActionServices.find_action(model, 'ad_scale') + self.assertEqual('scale', action[1]['name']) diff --git a/murano/tests/unit/api/v1/test_actions.py b/murano/tests/unit/api/v1/test_actions.py new file mode 100644 index 00000000..c8cf94b3 --- /dev/null +++ b/murano/tests/unit/api/v1/test_actions.py @@ -0,0 +1,87 @@ +# Copyright (c) 2014 Hewlett-Packard Development Company, L.P. +# +# 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 murano.api.v1 import actions +from murano.common import policy +from murano.db import models +from murano.openstack.common import timeutils +import murano.tests.unit.api.base as tb +import murano.tests.unit.utils as test_utils + + +@mock.patch.object(policy, 'check') +class TestActionsApi(tb.ControllerTest, tb.MuranoApiTestCase): + + def setUp(self): + super(TestActionsApi, self).setUp() + self.controller = actions.Controller() + + def test_execute_action(self, mock_policy_check): + """Test that action execution results in the correct rpc call""" + self._set_policy_rules( + {'execute_action': '@'} + ) + + fake_now = timeutils.utcnow() + expected = dict( + id='12345', + name='my-env', + version=0, + networking={}, + created=fake_now, + updated=fake_now, + tenant_id=self.tenant, + description={ + 'Objects': { + '?': {'id': '12345', + '_actions': { + 'actionsID_action': { + 'enabled': True, + 'name': 'Testaction' + } + }} + }, + 'Attributes': {} + } + ) + e = models.Environment(**expected) + test_utils.save_models(e) + + rpc_task = { + 'tenant_id': self.tenant, + 'model': {'Objects': {'applications': [], '?': + { + '_actions': {'actionsID_action': { + 'name': 'Testaction', 'enabled': True}}, + 'id': '12345'}}, 'Attributes': {}}, + 'action': { + 'method': 'Testaction', + 'object_id': '12345', + 'args': '{}'}, + 'token': None, + 'id': '12345' + } + + req = self._post('/environments/12345/actions/actionID_action', '{}') + result = self.controller.execute(req, '12345', 'actionsID_action', + '{}') + + self.mock_engine_rpc.handle_task.assert_called_once_with(rpc_task) + + # Should this be expected behavior? + self.assertEqual(None, result) diff --git a/murano/tests/unit/api/v1/test_environments.py b/murano/tests/unit/api/v1/test_environments.py index 400847c9..31b6080f 100644 --- a/murano/tests/unit/api/v1/test_environments.py +++ b/murano/tests/unit/api/v1/test_environments.py @@ -186,19 +186,9 @@ class TestEnvironmentApi(tb.ControllerTest, tb.MuranoApiTestCase): e = models.Environment(**expected) test_utils.save_models(e) - rpc_task = { - 'id': '12345', - 'action': None, - 'tenant_id': self.tenant, - 'model': {'Attributes': {}, 'Objects': None}, - 'token': None - } - req = self._delete('/environments/12345') result = req.get_response(self.api) - self.mock_engine_rpc.handle_task.assert_called_once_with(rpc_task) - # Should this be expected behavior? self.assertEqual('', result.body) self.assertEqual(200, result.status_code) diff --git a/murano/tests/unit/db/migration/test_migrations.py b/murano/tests/unit/db/migration/test_migrations.py index 41b835aa..39d5914f 100644 --- a/murano/tests/unit/db/migration/test_migrations.py +++ b/murano/tests/unit/db/migration/test_migrations.py @@ -118,3 +118,8 @@ class TestMigrations(base.BaseWalkMigrationTestCase, base.CommonTestsMixIn): def _check_002(self, engine, data): self.assertEqual('002', migration.version(engine)) self.assertColumnExists(engine, 'package', 'supplier_logo') + + def _check_003(self, engine, data): + self.assertEqual('003', migration.version(engine)) + self.assertColumnExists(engine, 'task', 'action') + self.assertColumnExists(engine, 'status', 'task_id')