Repository: incubator-ariatosca
Updated Branches:
  refs/heads/ARIA-126-update-node-statuses 288e06323 -> d74baa789 (forced 
update)


ARIA-126 Add node states

1. The states are described in section 3.3.1 of the TOSCA spec.

2. The state is changed if a standard lifecycle operation runs of the
node, as described in sections 5.7.4.1, 5.7.4.4.1, and 5.7.4.4.2 of the TOSCA 
spec.

3. We did not address the 'error' state yet.
This state is defined as part of the possible node states in the model, but 
currently no execution path leads to setting a node's state to 'error'.

4. No validation of state transiontions.
For example, we do not validate if a node goes from 'created'
to 'started' without going through the 'configured' state in between.


Project: http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/repo
Commit: 
http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/commit/d74baa78
Tree: http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/tree/d74baa78
Diff: http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/diff/d74baa78

Branch: refs/heads/ARIA-126-update-node-statuses
Commit: d74baa7895297bdcd190afdf823d3a70d2429e42
Parents: b3cf69a
Author: Avia Efrat <[email protected]>
Authored: Wed Mar 22 17:19:58 2017 +0200
Committer: Avia Efrat <[email protected]>
Committed: Sun Mar 26 14:07:17 2017 +0300

----------------------------------------------------------------------
 aria/modeling/service_instance.py               |  50 ++++++-
 aria/modeling/service_template.py               |   2 +-
 .../workflows/core/events_handler.py            |  24 ++++
 tests/mock/models.py                            |   4 +-
 tests/modeling/test_mixins.py                   |   2 +-
 tests/modeling/test_models.py                   |  12 +-
 .../orchestrator/workflows/core/test_events.py  | 130 +++++++++++++++++++
 7 files changed, 210 insertions(+), 14 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/d74baa78/aria/modeling/service_instance.py
----------------------------------------------------------------------
diff --git a/aria/modeling/service_instance.py 
b/aria/modeling/service_instance.py
index f120734..5cb3120 100644
--- a/aria/modeling/service_instance.py
+++ b/aria/modeling/service_instance.py
@@ -18,7 +18,9 @@
 from sqlalchemy import (
     Column,
     Text,
-    Integer
+    Integer,
+    Enum,
+    orm
 )
 from sqlalchemy import DateTime
 from sqlalchemy.ext.associationproxy import association_proxy
@@ -322,8 +324,8 @@ class NodeBase(InstanceModelMixin): # pylint: 
disable=too-many-public-methods
     :vartype runtime_properties: {}
     :ivar scaling_groups: ??
     :vartype scaling_groups: []
-    :ivar state: ??
-    :vartype state: basestring
+    :ivar state: The state of the node, according to to the TOSCA-defined node 
states
+    :vartype state: string
     :ivar version: Used by `aria.storage.instrumentation`
     :vartype version: int
 
@@ -347,6 +349,46 @@ class NodeBase(InstanceModelMixin): # pylint: 
disable=too-many-public-methods
                           'node_template_fk',
                           'service_name']
 
+    INITIAL = 'initial'
+    CREATING = 'creating'
+    CREATED = 'created'
+    CONFIGURING = 'configuring'
+    CONFIGURED = 'configured'
+    STARTING = 'starting'
+    STARTED = 'started'
+    STOPPING = 'stopping'
+    DELETING = 'deleting'
+    DELETED = 'deleted'
+    # TODO decide what happens to a node's state after its 'deleting' state, as
+    # this is not defined as part of the tosca spec.
+    ERROR = 'error'
+
+    STATES = {INITIAL, CREATING, CREATED, CONFIGURING, CONFIGURED, STARTING, 
STARTED, STOPPING,
+              DELETING, DELETED, ERROR}
+
+    _op_to_state = {'create': {'transitional': CREATING, 'finished': CREATED},
+                    'configure': {'transitional': CONFIGURING, 'finished': 
CONFIGURED},
+                    'start': {'transitional': STARTING, 'finished': STARTED},
+                    'stop': {'transitional': STOPPING, 'finished': CONFIGURED},
+                    'delete': {'transitional': DELETING, 'finished': DELETED}}
+
+    @classmethod
+    def determine_state(cls, op_name, transitional):
+        """ :returns the state the node should be in as a result of running the
+            operation on this node.
+
+            e.g. if we are running 
tosca.interfaces.node.lifecycle.Standard.create, then
+            the resulting state should either 'creating' (if the task just 
started) or 'created'
+            (if the task ended).
+
+            If the operation is not a standard tosca lifecycle operation, then 
we return None"""
+
+        state_type = 'transitional' if transitional else 'finished'
+        try:
+            return cls._op_to_state[op_name][state_type]
+        except KeyError:
+            return None
+
     @declared_attr
     def node_template(cls):
         return relationship.many_to_one(cls, 'node_template')
@@ -391,7 +433,7 @@ class NodeBase(InstanceModelMixin): # pylint: 
disable=too-many-public-methods
 
     runtime_properties = Column(modeling_types.Dict)
     scaling_groups = Column(modeling_types.List)
-    state = Column(Text, nullable=False)
+    state = Column(Enum(*STATES, name='node_state'), nullable=False, 
default=INITIAL)
     version = Column(Integer, default=1)
 
     __mapper_args__ = {'version_id_col': version} # Enable SQLAlchemy 
automatic version counting

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/d74baa78/aria/modeling/service_template.py
----------------------------------------------------------------------
diff --git a/aria/modeling/service_template.py 
b/aria/modeling/service_template.py
index 7246ff1..8b619bf 100644
--- a/aria/modeling/service_template.py
+++ b/aria/modeling/service_template.py
@@ -497,7 +497,7 @@ class NodeTemplateBase(TemplateModelMixin):
         node = models.Node(name=name,
                            type=self.type,
                            
description=deepcopy_with_locators(self.description),
-                           state='',
+                           state=models.Node.INITIAL,
                            node_template=self)
         utils.instantiate_dict(node, node.properties, self.properties)
         utils.instantiate_dict(node, node.interfaces, self.interface_templates)

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/d74baa78/aria/orchestrator/workflows/core/events_handler.py
----------------------------------------------------------------------
diff --git a/aria/orchestrator/workflows/core/events_handler.py 
b/aria/orchestrator/workflows/core/events_handler.py
index a420d2b..80d1266 100644
--- a/aria/orchestrator/workflows/core/events_handler.py
+++ b/aria/orchestrator/workflows/core/events_handler.py
@@ -20,14 +20,19 @@ Path: aria.events.storage_event_handler
 Implementation of storage handlers for workflow and operation events.
 """
 
+import re
 
 from datetime import (
     datetime,
     timedelta,
 )
 
+from aria.orchestrator.workflows.core.task import OperationTask
+
 from ... import events
 from ... import exceptions
+from ... context.operation import NodeOperationContext
+
 
 @events.sent_task_signal.connect
 def _task_sent(task, *args, **kwargs):
@@ -41,6 +46,10 @@ def _task_started(task, *args, **kwargs):
         task.started_at = datetime.utcnow()
         task.status = task.STARTED
 
+        # update node state if necessary
+        if type(task) is OperationTask and type(task.context) is 
NodeOperationContext:
+            _update_node_state(task, transitional=True)
+
 
 @events.on_failure_task_signal.connect
 def _task_failed(task, exception, *args, **kwargs):
@@ -73,6 +82,10 @@ def _task_succeeded(task, *args, **kwargs):
         task.ended_at = datetime.utcnow()
         task.status = task.SUCCESS
 
+        # update node state if necessary
+        if type(task) is OperationTask and type(task.context) is 
NodeOperationContext:
+            _update_node_state(task)
+
 
 @events.start_workflow_signal.connect
 def _workflow_started(workflow_context, *args, **kwargs):
@@ -118,3 +131,14 @@ def _workflow_cancelling(workflow_context, *args, 
**kwargs):
         return _workflow_cancelled(workflow_context=workflow_context)
     execution.status = execution.CANCELLING
     workflow_context.execution = execution
+
+
+def _update_node_state(task, transitional=False):
+        match = 
re.search('^(?:tosca.interfaces.node.lifecycle.Standard|Standard):(\S+)@node',
+                          task.context.name)
+        if match:
+            node = task.runs_on
+            state = node.determine_state(match.group(1), transitional)
+            if state:
+                node.state = state
+                task.context.model.node.update(node)

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/d74baa78/tests/mock/models.py
----------------------------------------------------------------------
diff --git a/tests/mock/models.py b/tests/mock/models.py
index a60b35e..9ae2815 100644
--- a/tests/mock/models.py
+++ b/tests/mock/models.py
@@ -121,7 +121,7 @@ def create_dependency_node(dependency_node_template, 
service):
         runtime_properties={'ip': '1.1.1.1'},
         version=None,
         node_template=dependency_node_template,
-        state='',
+        state=models.Node.INITIAL,
         scaling_groups=[],
         service=service
     )
@@ -136,7 +136,7 @@ def create_dependent_node(dependent_node_template, service):
         runtime_properties={},
         version=None,
         node_template=dependent_node_template,
-        state='',
+        state=models.Node.INITIAL,
         scaling_groups=[],
         service=service
     )

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/d74baa78/tests/modeling/test_mixins.py
----------------------------------------------------------------------
diff --git a/tests/modeling/test_mixins.py b/tests/modeling/test_mixins.py
index 7795b57..6a59102 100644
--- a/tests/modeling/test_mixins.py
+++ b/tests/modeling/test_mixins.py
@@ -127,7 +127,7 @@ def test_relationship_model_ordering(context):
         service=service,
         version=None,
         node_template=new_node_template,
-        state='',
+        state=models.Node.INITIAL,
         scaling_groups=[]
     )
 

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/d74baa78/tests/modeling/test_models.py
----------------------------------------------------------------------
diff --git a/tests/modeling/test_models.py b/tests/modeling/test_models.py
index c3b98c1..84200d5 100644
--- a/tests/modeling/test_models.py
+++ b/tests/modeling/test_models.py
@@ -554,11 +554,11 @@ class TestNode(object):
             (False, 'name', {}, [], m_cls, 1),
             (False, m_cls, {}, [], 'state', m_cls),
 
-            (True, 'name', {}, [], 'state', 1),
-            (True, None, {}, [], 'state', 1),
-            (True, 'name', None, [], 'state', 1),
-            (True, 'name', {}, None, 'state', 1),
-            (True, 'name', {}, [], 'state', None),
+            (True, 'name', {}, [], 'initial', 1),
+            (True, None, {}, [], 'initial', 1),
+            (True, 'name', None, [], 'initial', 1),
+            (True, 'name', {}, None, 'initial', 1),
+            (True, 'name', {}, [], 'initial', None),
         ]
     )
     def test_node_model_creation(self, node_template_storage, is_valid, name, 
runtime_properties,
@@ -645,7 +645,7 @@ class TestNodeIP(object):
             node_template=node,
             type=storage.type.list()[0],
             runtime_properties={},
-            state='',
+            state='initial',
             service=storage.service.list()[0]
         )
         if ip:

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/d74baa78/tests/orchestrator/workflows/core/test_events.py
----------------------------------------------------------------------
diff --git a/tests/orchestrator/workflows/core/test_events.py 
b/tests/orchestrator/workflows/core/test_events.py
new file mode 100644
index 0000000..5e717e7
--- /dev/null
+++ b/tests/orchestrator/workflows/core/test_events.py
@@ -0,0 +1,130 @@
+import pytest
+from tests import mock, storage
+from aria.modeling.service_instance import NodeBase
+from aria.orchestrator.decorators import operation, workflow
+from aria.orchestrator.workflows.core import engine
+from aria.orchestrator.workflows.executor.thread import ThreadExecutor
+from aria.orchestrator.workflows import api
+
+global_test_dict = {}  # used the capture transitional node state changes
+
+
[email protected]
+def ctx(tmpdir):
+    context = mock.context.simple(str(tmpdir))
+    yield context
+    storage.release_sqlite_storage(context.model)
+
+# TODO another possible approach of writing these tests:
+# Don't create a ctx for every test.
+# Problem is, that if for every test we create a workflow that contains just 
one standard
+# lifecycle operation, then by the time we try to run the second test, the 
workflow failes since
+# the execution tries to go from 'terminated' to 'pending'.
+# And if we write a workflow that contains all the lifecycle operations, then 
first we need to
+# change the api of `mock.models.create_interface`, which a lot of other tests 
use, and second how
+# do we check all the state transition during the workflow execution in a 
convenient way.
+
+TYPE_URI_NAME = 'tosca.interfaces.node.lifecycle.Standard'
+SHORTHAND_NAME = 'Standard'
+
+def test_node_state_changes_as_a_result_of_standard_lifecycle_create(ctx):
+    node = run_operation_on_node(ctx, interface_name=TYPE_URI_NAME, 
op_name='create')
+    
_assert_node_state_changed_as_a_result_of_standard_lifecycle_operation(node, 
'create')
+
+
+def test_node_state_changes_as_a_result_of_standard_lifecycle_configure(ctx):
+    node = run_operation_on_node(ctx, interface_name=TYPE_URI_NAME, 
op_name='configure')
+    
_assert_node_state_changed_as_a_result_of_standard_lifecycle_operation(node, 
'configure')
+
+
+def test_node_state_changes_as_a_result_of_standard_lifecycle_start(ctx):
+    node = run_operation_on_node(ctx, interface_name=TYPE_URI_NAME, 
op_name='start')
+    
_assert_node_state_changed_as_a_result_of_standard_lifecycle_operation(node, 
'start')
+
+
+def test_node_state_changes_as_a_result_of_standard_lifecycle_stop(ctx):
+    node = run_operation_on_node(ctx, interface_name=TYPE_URI_NAME, 
op_name='stop')
+    
_assert_node_state_changed_as_a_result_of_standard_lifecycle_operation(node, 
'stop')
+
+
+def test_node_state_changes_as_a_result_of_standard_lifecycle_delete(ctx):
+    node = run_operation_on_node(ctx, interface_name=TYPE_URI_NAME, 
op_name='delete')
+    
_assert_node_state_changed_as_a_result_of_standard_lifecycle_operation(node, 
'delete')
+
+
+def 
test_node_state_changes_as_a_result_of_standard_lifecycle_create_shorthand_name(ctx):
+    node = run_operation_on_node(ctx, interface_name=SHORTHAND_NAME, 
op_name='create')
+    
_assert_node_state_changed_as_a_result_of_standard_lifecycle_operation(node, 
'create')
+
+
+def 
test_node_state_changes_as_a_result_of_standard_lifecycle_configure_shorthand_name(ctx):
+    node = run_operation_on_node(ctx, interface_name=SHORTHAND_NAME, 
op_name='configure')
+    
_assert_node_state_changed_as_a_result_of_standard_lifecycle_operation(node, 
'configure')
+
+
+def 
test_node_state_changes_as_a_result_of_standard_lifecycle_start_shorthand_name(ctx):
+    node = run_operation_on_node(ctx, interface_name=SHORTHAND_NAME, 
op_name='start')
+    
_assert_node_state_changed_as_a_result_of_standard_lifecycle_operation(node, 
'start')
+
+
+def 
test_node_state_changes_as_a_result_of_standard_lifecycle_stop_shorthand_name(ctx):
+    node = run_operation_on_node(ctx, interface_name=SHORTHAND_NAME, 
op_name='stop')
+    
_assert_node_state_changed_as_a_result_of_standard_lifecycle_operation(node, 
'stop')
+
+
+def 
test_node_state_changes_as_a_result_of_standard_lifecycle_delete_shorthand_name(ctx):
+    node = run_operation_on_node(ctx, interface_name=SHORTHAND_NAME, 
op_name='delete')
+    
_assert_node_state_changed_as_a_result_of_standard_lifecycle_operation(node, 
'delete')
+
+
+def 
test_node_state_doesnt_change_as_a_result_of_an_operation_that_is_not_standard_lifecycle1(ctx):
+    node = run_operation_on_node(ctx, interface_name='interface_name', 
op_name='op_name')
+    assert node.state == node.INITIAL
+
+
+def 
test_node_state_doesnt_change_as_a_result_of_an_operation_that_is_not_standard_lifecycle2(ctx):
+    node = run_operation_on_node(ctx, interface_name='interface_name', 
op_name='create')
+    assert node.state == node.INITIAL
+
+
+def run_operation_on_node(ctx, op_name, interface_name):
+    node = ctx.model.node.get_by_name(mock.models.DEPENDENCY_NODE_NAME)
+    interface = mock.models.create_interface(
+        service=node.service,
+        interface_name=interface_name,
+        operation_name=op_name,
+        
operation_kwargs=dict(implementation='{name}.{func.__name__}'.format(name=__name__,
+                                                                             
func=func)))
+    node.interfaces[interface.name] = interface
+
+    eng = engine.Engine(executor=ThreadExecutor(),
+                        workflow_context=ctx,
+                        tasks_graph=single_operation_workflow(ctx=ctx,
+                                                              node=node,
+                                                              
interface_name=interface_name,
+                                                              op_name=op_name))
+    eng.execute()
+    return node
+
+
+def run_standard_lifecycle_operation_on_node(ctx, op_name):
+    return run_operation_on_node(ctx, 
interface_name='aria.interfaces.lifecycle.Standard',
+                                 op_name=op_name)
+
+
+def 
_assert_node_state_changed_as_a_result_of_standard_lifecycle_operation(node, 
op_name):
+    assert global_test_dict['transitional_state'] == 
NodeBase._op_to_state[op_name]['transitional']
+    assert node.state == NodeBase._op_to_state[op_name]['finished']
+
+
+@workflow
+def single_operation_workflow(ctx, graph, node, interface_name, op_name):
+    graph.add_tasks(api.task.OperationTask.for_node(
+        node=node,
+        interface_name=interface_name,
+        operation_name=op_name))
+
+
+@operation
+def func(ctx):
+    global_test_dict['transitional_state'] = ctx.node.state

Reply via email to