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:
parent
b5960e407f
commit
e47ef4e849
73
murano/api/v1/actions.py
Normal file
73
murano/api/v1/actions.py
Normal 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())
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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()
|
||||
|
||||
|
||||
|
@ -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 ###
|
56
murano/db/migration/helpers.py
Normal file
56
murano/db/migration/helpers.py
Normal 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)
|
@ -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)
|
||||
|
39
murano/db/services/actions.py
Normal file
39
murano/db/services/actions.py
Normal 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)
|
@ -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)
|
||||
|
@ -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)
|
||||
|
0
murano/services/__init__.py
Normal file
0
murano/services/__init__.py
Normal file
107
murano/services/actions.py
Normal file
107
murano/services/actions.py
Normal 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
20
murano/services/state.py
Normal 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'
|
||||
)
|
135
murano/tests/test_actions.py
Normal file
135
murano/tests/test_actions.py
Normal 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'])
|
87
murano/tests/unit/api/v1/test_actions.py
Normal file
87
murano/tests/unit/api/v1/test_actions.py
Normal 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)
|
@ -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)
|
||||
|
@ -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')
|
||||
|
Loading…
Reference in New Issue
Block a user