Adds REST API endpoint for action execution

Deployment is replaced with a more generic concept called 'action'.
Action can be performed on any object of Object Model.
Actions are marked with 'Usage: Action' in MuranoPL code. They can have arguments.
List of available actions can be obtained from Object Model itself after initial deployment.
This commit adds ability to REST API invoke actions by providing its unique id (from OM) and parameters.
Also refactors API code to use tasks.

Change-Id: If21809340bb799af58a8d1a2d148e52565028970
Partially-Implements: blueprint application-actions
This commit is contained in:
Stan Lagun 2014-06-11 22:47:32 +04:00 committed by Ruslan Kamaldinov
parent b5960e407f
commit e47ef4e849
18 changed files with 679 additions and 90 deletions

73
murano/api/v1/actions.py Normal file
View File

@ -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 <ActionId: {0}>'.format(action_id))
unit = db_session.get_session()
environment = unit.query(models.Environment).get(environment_id)
if environment is None:
LOG.info(_('Environment <EnvId {0}> '
'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 <EnvId: {0}>,'
'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 <SessionId {0}> '
'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())

View File

@ -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

View File

@ -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,

View File

@ -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)

View File

@ -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()

View File

@ -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 ###

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

107
murano/services/actions.py Normal file
View File

@ -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

20
murano/services/state.py Normal file
View File

@ -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'
)

View File

@ -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'])

View File

@ -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)

View File

@ -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)

View File

@ -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')