Repository: incubator-ariatosca
Updated Branches:
  refs/heads/ARIA-132-Models-cascading-deletion-raises-constraint-errors 
3220d4701 -> 6fb690006 (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/0e107933
Tree: http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/tree/0e107933
Diff: http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/diff/0e107933

Branch: refs/heads/ARIA-132-Models-cascading-deletion-raises-constraint-errors
Commit: 0e107933e68ec8a6fbc690a02d59e2fca900a540
Parents: b3cf69a
Author: Avia Efrat <a...@gigaspaces.com>
Authored: Wed Mar 22 17:19:58 2017 +0200
Committer: Avia Efrat <a...@gigaspaces.com>
Committed: Mon Mar 27 16:38:52 2017 +0300

----------------------------------------------------------------------
 aria/modeling/service_instance.py               |  52 ++++++-
 aria/modeling/service_template.py               |   2 +-
 .../workflows/core/events_handler.py            |  18 ++-
 tests/mock/models.py                            |   4 +-
 tests/modeling/test_mixins.py                   |   2 +-
 tests/modeling/test_models.py                   |  12 +-
 .../orchestrator/workflows/core/test_events.py  | 147 +++++++++++++++++++
 7 files changed, 222 insertions(+), 15 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/0e107933/aria/modeling/service_instance.py
----------------------------------------------------------------------
diff --git a/aria/modeling/service_instance.py 
b/aria/modeling/service_instance.py
index f120734..1e18db0 100644
--- a/aria/modeling/service_instance.py
+++ b/aria/modeling/service_instance.py
@@ -18,7 +18,8 @@
 from sqlalchemy import (
     Column,
     Text,
-    Integer
+    Integer,
+    Enum,
 )
 from sqlalchemy import DateTime
 from sqlalchemy.ext.associationproxy import association_proxy
@@ -322,8 +323,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 +348,49 @@ 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' isn't actually part of the tosca spec, since according the 
description of the
+    # 'deleting' state: "Node is transitioning from its current state to one 
where it is deleted and
+    #  its state is no longer tracked by the instance model."
+    # However, we prefer to be able to retrieve information about deleted 
nodes, so we chose to add
+    # this 'deleted' state to enable us to do so.
+    DELETED = 'deleted'
+    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, is_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 is_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 +435,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/0e107933/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/0e107933/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..4ac4b64 100644
--- a/aria/orchestrator/workflows/core/events_handler.py
+++ b/aria/orchestrator/workflows/core/events_handler.py
@@ -20,7 +20,7 @@ Path: aria.events.storage_event_handler
 Implementation of storage handlers for workflow and operation events.
 """
 
-
+import re
 from datetime import (
     datetime,
     timedelta,
@@ -29,6 +29,7 @@ from datetime import (
 from ... import events
 from ... import exceptions
 
+
 @events.sent_task_signal.connect
 def _task_sent(task, *args, **kwargs):
     with task._update():
@@ -41,6 +42,8 @@ def _task_started(task, *args, **kwargs):
         task.started_at = datetime.utcnow()
         task.status = task.STARTED
 
+        _update_node_state_if_necessary(task, is_transitional=True)
+
 
 @events.on_failure_task_signal.connect
 def _task_failed(task, exception, *args, **kwargs):
@@ -73,6 +76,8 @@ def _task_succeeded(task, *args, **kwargs):
         task.ended_at = datetime.utcnow()
         task.status = task.SUCCESS
 
+        _update_node_state_if_necessary(task)
+
 
 @events.start_workflow_signal.connect
 def _workflow_started(workflow_context, *args, **kwargs):
@@ -118,3 +123,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_if_necessary(task, is_transitional=False):
+    match = 
re.search(r'^(?:tosca.interfaces.node.lifecycle.Standard|Standard):(\S+)@node',
+                      task.name)
+    if match:
+        node = task.runs_on
+        state = node.determine_state(op_name=match.group(1), 
is_transitional=is_transitional)
+        if state:
+            node.state = state
+            task.context.model.node.update(node)

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/0e107933/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/0e107933/tests/modeling/test_mixins.py
----------------------------------------------------------------------
diff --git a/tests/modeling/test_mixins.py b/tests/modeling/test_mixins.py
index 7795b57..651f53f 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=modeling.models.Node.INITIAL,
         scaling_groups=[]
     )
 

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/0e107933/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/0e107933/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..b9bff77
--- /dev/null
+++ b/tests/orchestrator/workflows/core/test_events.py
@@ -0,0 +1,147 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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 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 to capture transitional node state changes
+
+
+@pytest.fixture
+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,  # 
pylint: disable=no-value-for-parameter
+                                                              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(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