# 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 eventlet
from unittest import mock

from senlin.common import consts
from senlin.common import scaleutils
from senlin.engine.actions import base as base_action
from senlin.engine.actions import node_action
from senlin.engine import cluster as cluster_mod
from senlin.engine import event as EVENT
from senlin.engine import node as node_mod
from senlin.engine import senlin_lock as lock
from senlin.objects import node as node_obj
from senlin.policies import base as policy_mod
from senlin.tests.unit.common import base
from senlin.tests.unit.common import utils


@mock.patch.object(node_mod.Node, 'load')
class NodeActionTest(base.SenlinTestCase):

    def setUp(self):
        super(NodeActionTest, self).setUp()
        self.ctx = utils.dummy_context()

    def test_do_create_okay(self, mock_load):
        node = mock.Mock(id='NID')
        node.do_create = mock.Mock(return_value=[True, ''])
        mock_load.return_value = node
        action = node_action.NodeAction(node.id, 'ACTION', self.ctx)

        res_code, res_msg = action.do_create()

        self.assertEqual(action.RES_OK, res_code)
        self.assertEqual('Node created successfully.', res_msg)
        node.do_create.assert_called_once_with(action.context)

    def test_do_create_failed(self, mock_load):
        node = mock.Mock(id='NID')
        node.do_create = mock.Mock(return_value=[False,
                                                 'custom error message'])
        mock_load.return_value = node
        action = node_action.NodeAction(node.id, 'ACTION', self.ctx)

        # Test node creation failure path
        res_code, res_msg = action.do_create()

        self.assertEqual(action.RES_ERROR, res_code)
        self.assertEqual('custom error message', res_msg)
        node.do_create.assert_called_once_with(action.context)

    @mock.patch.object(scaleutils, 'check_size_params')
    @mock.patch.object(node_obj.Node, 'count_by_cluster')
    @mock.patch.object(cluster_mod.Cluster, 'load')
    def test_do_create_with_cluster_id_success(self, mock_c_load, mock_count,
                                               mock_check, mock_load):
        cluster = mock.Mock(id='CID')
        mock_c_load.return_value = cluster
        node = mock.Mock(id='NID', cluster_id='CID')
        node.do_create = mock.Mock(return_value=[True, ''])
        mock_load.return_value = node
        mock_count.return_value = 11
        mock_check.return_value = None
        action = node_action.NodeAction(node.id, 'ACTION', self.ctx,
                                        cause=consts.CAUSE_RPC)

        # do it
        res_code, res_msg = action.do_create()

        # assertions
        self.assertEqual(action.RES_OK, res_code)
        mock_c_load.assert_called_once_with(action.context, 'CID')
        mock_count.assert_called_once_with(action.context, 'CID')
        mock_check.assert_called_once_with(cluster, 11, None, None, True)
        node.do_create.assert_called_once_with(action.context)
        cluster.eval_status.assert_called_once_with(
            action.context, consts.NODE_CREATE, desired_capacity=11)

    @mock.patch.object(node_obj.Node, 'update')
    @mock.patch.object(scaleutils, 'check_size_params')
    @mock.patch.object(node_obj.Node, 'count_by_cluster')
    @mock.patch.object(cluster_mod.Cluster, 'load')
    def test_do_create_with_cluster_id_failed_checking(
            self, mock_c_load, mock_count, mock_check, mock_update, mock_load):

        cluster = mock.Mock(id='CID')
        mock_c_load.return_value = cluster
        node = mock.Mock(id='NID', cluster_id='CID')
        node.do_create = mock.Mock(return_value=[True, ''])
        mock_load.return_value = node
        mock_count.return_value = 11
        mock_check.return_value = 'overflow'
        action = node_action.NodeAction(node.id, 'ACTION', self.ctx,
                                        cause=consts.CAUSE_RPC)

        # do it
        res_code, res_msg = action.do_create()

        # assertions
        self.assertEqual(action.RES_ERROR, res_code)
        self.assertEqual('overflow', res_msg)
        mock_c_load.assert_called_once_with(action.context, 'CID')
        mock_count.assert_called_once_with(action.context, 'CID')
        mock_check.assert_called_once_with(cluster, 11, None, None, True)
        mock_update.assert_called_once_with(action.context, 'NID',
                                            {'cluster_id': '',
                                             'status': consts.NS_ERROR})
        self.assertEqual(0, node.do_create.call_count)
        self.assertEqual(0, cluster.eval_status.call_count)

    @mock.patch.object(scaleutils, 'check_size_params')
    @mock.patch.object(node_obj.Node, 'count_by_cluster')
    @mock.patch.object(cluster_mod.Cluster, 'load')
    def test_do_create_with_cluster_id_failed_creation(
            self, mock_c_load, mock_count, mock_check, mock_load):

        cluster = mock.Mock(id='CID')
        mock_c_load.return_value = cluster
        node = mock.Mock(id='NID', cluster_id='CID')
        node.do_create = mock.Mock(return_value=[False,
                                                 'custom error message'])
        mock_load.return_value = node
        mock_count.return_value = 11
        mock_check.return_value = ''
        action = node_action.NodeAction(node.id, 'ACTION', self.ctx,
                                        cause=consts.CAUSE_RPC)

        # do it
        res_code, res_msg = action.do_create()

        # assertions
        self.assertEqual(action.RES_ERROR, res_code)
        self.assertEqual('custom error message', res_msg)
        mock_c_load.assert_called_once_with(action.context, 'CID')
        mock_count.assert_called_once_with(action.context, 'CID')
        mock_check.assert_called_once_with(cluster, 11, None, None, True)
        node.do_create.assert_called_once_with(action.context)
        cluster.eval_status.assert_called_once_with(
            action.context, consts.NODE_CREATE, desired_capacity=11)

    def test_do_delete_okay(self, mock_load):
        node = mock.Mock(id='NID')
        node.do_delete = mock.Mock(return_value=True)
        mock_load.return_value = node
        action = node_action.NodeAction('ID', 'ACTION', self.ctx)

        # do it
        res_code, res_msg = action.do_delete()

        # assertions
        self.assertEqual(action.RES_OK, res_code)
        self.assertEqual('Node deleted successfully.', res_msg)
        node.do_delete.assert_called_once_with(action.context)

    def test_do_delete_failed(self, mock_load):
        node = mock.Mock(id='NID')
        node.do_delete = mock.Mock(return_value=False)
        mock_load.return_value = node
        action = node_action.NodeAction('ID', 'ACTION', self.ctx)

        # Test failed node deletion path
        res_code, res_msg = action.do_delete()

        self.assertEqual(action.RES_ERROR, res_code)
        self.assertEqual('Node deletion failed.', res_msg)
        node.do_delete.assert_called_once_with(action.context)

    @mock.patch.object(scaleutils, 'check_size_params')
    @mock.patch.object(node_obj.Node, 'count_by_cluster')
    @mock.patch.object(cluster_mod.Cluster, 'load')
    def test_do_delete_with_cluster_id_success(self, mock_c_load, mock_count,
                                               mock_check, mock_load):
        cluster = mock.Mock(id='CID')
        mock_c_load.return_value = cluster
        node = mock.Mock(id='NID', cluster_id='CID')
        node.do_delete.return_value = True
        mock_load.return_value = node
        mock_count.return_value = 2
        mock_check.return_value = None
        action = node_action.NodeAction(node.id, 'ACTION', self.ctx,
                                        cause=consts.CAUSE_RPC)

        # do it
        res_code, res_msg = action.do_delete()

        # assertion
        self.assertEqual(action.RES_OK, res_code)
        self.assertEqual('Node deleted successfully.', res_msg)
        mock_c_load.assert_called_once_with(action.context, 'CID')
        mock_count.assert_called_once_with(action.context, 'CID')
        mock_check.assert_called_once_with(cluster, 1, None, None, True)
        cluster.eval_status.assert_called_once_with(
            action.context, consts.NODE_DELETE, desired_capacity=1)

    @mock.patch.object(scaleutils, 'check_size_params')
    @mock.patch.object(node_obj.Node, 'count_by_cluster')
    @mock.patch.object(cluster_mod.Cluster, 'load')
    def test_do_delete_with_cluster_id_failed_checking(
            self, mock_c_load, mock_count, mock_check, mock_load):

        cluster = mock.Mock(id='CID')
        mock_c_load.return_value = cluster
        node = mock.Mock(id='NID', cluster_id='CID')
        node.do_delete.return_value = True
        mock_load.return_value = node
        mock_count.return_value = 2
        mock_check.return_value = 'underflow'
        action = node_action.NodeAction(node.id, 'ACTION', self.ctx,
                                        cause=consts.CAUSE_RPC)

        res_code, res_msg = action.do_delete()

        self.assertEqual(action.RES_ERROR, res_code)
        self.assertEqual('underflow', res_msg)

        mock_load.assert_called_once_with(action.context, node_id='NID')
        mock_c_load.assert_called_once_with(action.context, 'CID')
        mock_count.assert_called_once_with(action.context, 'CID')
        mock_check.assert_called_once_with(cluster, 1, None, None, True)
        self.assertEqual(0, node.do_delete.call_count)
        self.assertEqual(0, cluster.eval_status.call_count)

    @mock.patch.object(scaleutils, 'check_size_params')
    @mock.patch.object(node_obj.Node, 'count_by_cluster')
    @mock.patch.object(cluster_mod.Cluster, 'load')
    def test_do_delete_with_cluster_id_failed_deletion(
            self, mock_c_load, mock_count, mock_check, mock_load):

        cluster = mock.Mock(id='CID')
        mock_c_load.return_value = cluster
        node = mock.Mock(id='NID', cluster_id='CID')
        node.do_delete.return_value = False
        mock_load.return_value = node
        mock_count.return_value = 2
        mock_check.return_value = None
        action = node_action.NodeAction(node.id, 'ACTION', self.ctx,
                                        cause=consts.CAUSE_RPC)

        res_code, res_msg = action.do_delete()

        self.assertEqual(action.RES_ERROR, res_code)
        self.assertEqual('Node deletion failed.', res_msg)
        mock_load.assert_called_once_with(action.context, node_id='NID')
        mock_c_load.assert_called_once_with(action.context, 'CID')
        mock_count.assert_called_once_with(action.context, 'CID')
        mock_check.assert_called_once_with(cluster, 1, None, None, True)
        node.do_delete.assert_called_once_with(action.context)
        cluster.eval_status.assert_called_once_with(
            action.context, consts.NODE_DELETE)

    @mock.patch.object(eventlet, 'sleep')
    @mock.patch.object(scaleutils, 'check_size_params')
    @mock.patch.object(node_obj.Node, 'count_by_cluster')
    @mock.patch.object(cluster_mod.Cluster, 'load')
    def test_do_delete_with_cluster_id_and_grace_period(
            self, mock_c_load, mock_count, mock_check, mock_sleep, mock_load):

        cluster = mock.Mock(id='CID')
        mock_c_load.return_value = cluster
        node = mock.Mock(id='NID', cluster_id='CID')
        node.do_delete.return_value = True
        mock_load.return_value = node
        mock_count.return_value = 2
        mock_check.return_value = None
        action = node_action.NodeAction(
            node.id, 'ACTION', self.ctx, cause=consts.CAUSE_RPC,
            data={'deletion': {'grace_period': 10}})

        # do it
        res_code, res_msg = action.do_delete()

        # assertions
        self.assertEqual(action.RES_OK, res_code)
        self.assertEqual('Node deleted successfully.', res_msg)
        mock_load.assert_called_once_with(action.context, node_id='NID')
        mock_c_load.assert_called_once_with(action.context, 'CID')
        mock_count.assert_called_once_with(action.context, 'CID')
        mock_check.assert_called_once_with(cluster, 1, None, None, True)
        mock_sleep.assert_called_once_with(10)
        node.do_delete.assert_called_once_with(action.context)
        cluster.eval_status.assert_called_once_with(
            action.context, consts.NODE_DELETE, desired_capacity=1)

    @mock.patch.object(eventlet, 'sleep')
    @mock.patch.object(scaleutils, 'check_size_params')
    @mock.patch.object(node_obj.Node, 'count_by_cluster')
    @mock.patch.object(cluster_mod.Cluster, 'load')
    def test_do_delete_with_cluster_id_and_forced_reduce(
            self, mock_c_load, mock_count, mock_check, mock_sleep, mock_load):
        cluster = mock.Mock(id='CID')
        mock_c_load.return_value = cluster
        node = mock.Mock(id='NID', cluster_id='CID')
        node.do_delete.return_value = True
        mock_load.return_value = node
        mock_count.return_value = 2
        mock_check.return_value = None
        action = node_action.NodeAction(
            'NID', 'ACTION', self.ctx,
            cause=consts.CAUSE_RPC,
            data={'deletion': {'reduce_desired_capacity': True}})

        # do it
        res_code, res_msg = action.do_delete()

        self.assertEqual(action.RES_OK, res_code)
        self.assertEqual('Node deleted successfully.', res_msg)
        mock_load.assert_called_once_with(action.context, node_id='NID')
        mock_c_load.assert_called_once_with(action.context, 'CID')
        mock_count.assert_called_once_with(action.context, 'CID')
        mock_check.assert_called_once_with(cluster, 1, None, None, True)
        node.do_delete.assert_called_once_with(action.context)
        cluster.eval_status.assert_called_once_with(
            action.context, consts.NODE_DELETE, desired_capacity=1)

    @mock.patch.object(eventlet, 'sleep')
    @mock.patch.object(scaleutils, 'check_size_params')
    @mock.patch.object(node_obj.Node, 'count_by_cluster')
    @mock.patch.object(cluster_mod.Cluster, 'load')
    def test_do_delete_with_cluster_id_and_forced_no_reduce(
            self, mock_c_load, mock_count, mock_check, mock_sleep, mock_load):
        cluster = mock.Mock(id='CID')
        mock_c_load.return_value = cluster
        node = mock.Mock(id='NID', cluster_id='CID')
        node.do_delete.return_value = True
        mock_load.return_value = node
        mock_count.return_value = 2
        mock_check.return_value = None
        action = node_action.NodeAction(
            'NID', 'ACTION', self.ctx,
            cause=consts.CAUSE_RPC,
            data={'deletion': {'reduce_desired_capacity': False}})

        # do it
        res_code, res_msg = action.do_delete()

        self.assertEqual(action.RES_OK, res_code)
        self.assertEqual('Node deleted successfully.', res_msg)
        mock_load.assert_called_once_with(action.context, node_id='NID')
        mock_c_load.assert_called_once_with(action.context, 'CID')
        mock_count.assert_called_once_with(action.context, 'CID')
        mock_check.assert_called_once_with(cluster, 1, None, None, True)
        node.do_delete.assert_called_once_with(action.context)
        cluster.eval_status.assert_called_once_with(
            action.context, consts.NODE_DELETE)

    def test_do_delete_derived_success(self, mock_load):

        node = mock.Mock(id='NID', cluster_id='CLUSTER_ID')
        node.do_delete.return_value = True
        mock_load.return_value = node
        action = node_action.NodeAction(node.id, 'ACTION', self.ctx,
                                        cause=consts.CAUSE_DERIVED)

        res_code, res_msg = action.do_delete()

        self.assertEqual(action.RES_OK, res_code)
        self.assertEqual('Node deleted successfully.', res_msg)
        mock_load.assert_called_once_with(action.context, node_id='NID')

    def test_do_delete_derived_failed_deletion(self, mock_load):

        node = mock.Mock(id='NID', cluster_id='CLUSTER_ID')
        node.do_delete.return_value = False
        mock_load.return_value = node
        action = node_action.NodeAction(node.id, 'ACTION', self.ctx,
                                        cause=consts.CAUSE_DERIVED)

        res_code, res_msg = action.do_delete()

        self.assertEqual(action.RES_ERROR, res_code)
        self.assertEqual('Node deletion failed.', res_msg)
        mock_load.assert_called_once_with(action.context, node_id='NID')

    def test_do_update(self, mock_load):
        node = mock.Mock()
        node.id = 'NID'
        mock_load.return_value = node
        inputs = {"new_profile_id": "FAKE_PROFILE_ID"}
        action = node_action.NodeAction(node.id, 'ACTION', self.ctx,
                                        inputs=inputs)

        # Test failed node update path
        node.do_update = mock.Mock(return_value=None)
        res_code, res_msg = action.do_update()
        self.assertEqual(action.RES_ERROR, res_code)
        self.assertEqual('Node update failed.', res_msg)
        node.do_update.assert_called_once_with(action.context, inputs)
        node.reset_mock()

        # Test node update success path
        node.do_update = mock.Mock(return_value=mock.Mock())
        res_code, res_msg = action.do_update()
        self.assertEqual(action.RES_OK, res_code)
        self.assertEqual('Node updated successfully.', res_msg)
        node.do_update.assert_called_once_with(action.context, inputs)

    def test_do_update_no_need_update(self, mock_load):
        node = mock.Mock()
        node.id = 'NID'
        node.profile_id = 'PROFILE_ID'
        mock_load.return_value = node
        inputs = {"new_profile_id": "PROFILE_ID"}
        action = node_action.NodeAction(node.id, 'ACTION', self.ctx,
                                        inputs=inputs)

        # Test node update success path
        node.do_update = mock.Mock(return_value=mock.Mock())
        res_code, res_msg = action.do_update()
        self.assertEqual(action.RES_OK, res_code)
        self.assertEqual('No property to update.', res_msg)
        self.assertFalse(node.do_update.called)

    def test_do_join_success(self, mock_load):
        node = mock.Mock(id='NID')
        mock_load.return_value = node
        inputs = {"cluster_id": "FAKE_ID"}
        action = node_action.NodeAction(node.id, 'NODE_JOIN', self.ctx,
                                        inputs=inputs)
        node.do_join = mock.Mock(return_value=True)

        # Test failed node join path
        res_code, res_msg = action.do_join()

        self.assertEqual(action.RES_OK, res_code)
        self.assertEqual('Node successfully joined cluster.', res_msg)
        node.do_join.assert_called_once_with(action.context, 'FAKE_ID')

    def test_do_join_failed_do_join(self, mock_load):
        node = mock.Mock(id='NID')
        mock_load.return_value = node
        inputs = {"cluster_id": "FAKE_ID"}
        action = node_action.NodeAction(node.id, 'NODE_JOIN', self.ctx,
                                        inputs=inputs)
        node.do_join = mock.Mock(return_value=False)

        # Test failed node join path
        res_code, res_msg = action.do_join()

        self.assertEqual(action.RES_ERROR, res_code)
        self.assertEqual('Node failed in joining cluster.', res_msg)
        node.do_join.assert_called_once_with(action.context, 'FAKE_ID')

    def test_do_leave_success(self, mock_load):
        node = mock.Mock(id='NID', cluster_id='CID')
        mock_load.return_value = node
        action = node_action.NodeAction(node.id, 'NODE_LEAVE', self.ctx)
        node.do_leave = mock.Mock(return_value=True)

        # Test failed node join path
        res_code, res_msg = action.do_leave()

        self.assertEqual(action.RES_OK, res_code)
        self.assertEqual('Node successfully left cluster.', res_msg)
        node.do_leave.assert_called_once_with(action.context)

    def test_do_leave_failed_leave(self, mock_load):
        node = mock.Mock(id='NID', cluster_id='CID')
        mock_load.return_value = node
        action = node_action.NodeAction(node.id, 'NODE_LEAVE', self.ctx)
        node.do_leave = mock.Mock(return_value=False)

        # Test failed node join path
        res_code, res_msg = action.do_leave()

        self.assertEqual(action.RES_ERROR, res_code)
        self.assertEqual('Node failed in leaving cluster.', res_msg)
        node.do_leave.assert_called_once_with(action.context)

    def test_do_check_success(self, mock_load):
        node = mock.Mock(id='NID')
        mock_load.return_value = node
        action = node_action.NodeAction(node.id, 'ACTION', self.ctx)
        node.do_check = mock.Mock(return_value=True)

        res_code, res_msg = action.do_check()

        self.assertEqual(action.RES_OK, res_code)
        self.assertEqual('Node check succeeded.', res_msg)
        node.do_check.assert_called_once_with(action.context)

    def test_do_check_failed(self, mock_load):
        node = mock.Mock(id='NID')
        mock_load.return_value = node
        action = node_action.NodeAction(node.id, 'ACTION', self.ctx)
        node.do_check = mock.Mock(return_value=False)

        res_code, res_msg = action.do_check()

        self.assertEqual(action.RES_ERROR, res_code)
        self.assertEqual('Node check failed.', res_msg)
        node.do_check.assert_called_once_with(action.context)

    def test_do_recover_success(self, mock_load):
        node = mock.Mock(id='NID')
        mock_load.return_value = node
        action = node_action.NodeAction(node.id, 'ACTION', self.ctx)
        action.inputs = {'operation': ['SWIM', 'DANCE']}
        node.do_recover = mock.Mock(return_value=True)

        res_code, res_msg = action.do_recover()

        self.assertEqual(action.RES_OK, res_code)
        self.assertEqual('Node recovered successfully.', res_msg)
        node.do_recover.assert_called_once_with(action.context, action)

    def test_do_recover_failed(self, mock_load):
        node = mock.Mock(id='NID')
        mock_load.return_value = node
        action = node_action.NodeAction(node.id, 'ACTION', self.ctx)
        action.inputs = {'operation': ['SWIM', 'DANCE']}

        # Test node recover failure path
        node.do_recover = mock.Mock(return_value=False)
        res_code, res_msg = action.do_recover()
        self.assertEqual(action.RES_ERROR, res_code)
        self.assertEqual('Node recover failed.', res_msg)
        node.do_recover.assert_called_once_with(action.context, action)

    def test_do_operation_success(self, mock_load):
        node = mock.Mock(id='NID')
        mock_load.return_value = node
        action = node_action.NodeAction(node.id, 'ACTION', self.ctx)
        action.inputs = {'operation': 'dance', 'params': {}}
        node.do_operation = mock.Mock(return_value=True)

        res_code, res_msg = action.do_operation()

        self.assertEqual(action.RES_OK, res_code)
        self.assertEqual("Node operation 'dance' succeeded.", res_msg)
        node.do_operation.assert_called_once_with(action.context,
                                                  operation='dance',
                                                  params={})

    def test_do_operation_failed(self, mock_load):
        node = mock.Mock(id='NID')
        mock_load.return_value = node
        action = node_action.NodeAction(node.id, 'ACTION', self.ctx)
        action.inputs = {'operation': 'dance', 'params': {}}
        node.do_operation = mock.Mock(return_value=False)

        res_code, res_msg = action.do_operation()

        self.assertEqual(action.RES_ERROR, res_code)
        self.assertEqual("Node operation 'dance' failed.", res_msg)
        node.do_operation.assert_called_once_with(action.context,
                                                  operation='dance',
                                                  params={})

    def test_execute(self, mock_load):
        node = mock.Mock()
        node.id = 'NID'
        mock_load.return_value = node
        action = node_action.NodeAction(node.id, 'NODE_SING', self.ctx)
        action.do_sing = mock.Mock(return_value=(action.RES_OK, 'GOOD'))

        res_code, res_msg = action._execute()

        self.assertEqual(action.RES_OK, res_code)
        self.assertEqual('GOOD', res_msg)
        action.do_sing.assert_called_once_with()

    @mock.patch.object(EVENT, 'error')
    def test_execute_bad_action(self, mock_error, mock_load):
        node = mock.Mock()
        node.id = 'NID'
        mock_load.return_value = node
        action = node_action.NodeAction(node.id, 'NODE_DANCE', self.ctx)

        res_code, res_msg = action._execute()

        self.assertEqual(action.RES_ERROR, res_code)
        reason = 'Unsupported action: NODE_DANCE'
        self.assertEqual(reason, res_msg)
        mock_error.assert_called_once_with(action, 'error', reason)

    @mock.patch.object(lock, 'cluster_lock_acquire')
    def test_execute_failed_lock_cluster(self, mock_acquire, mock_load):
        node = mock.Mock()
        node.cluster_id = 'FAKE_CLUSTER'
        node.id = 'NID'
        mock_load.return_value = node
        action = node_action.NodeAction('NODE_ID', 'NODE_FLY', self.ctx,
                                        cause='RPC Request')
        action.id = 'ACTION_ID'
        mock_acquire.return_value = None

        res_code, res_msg = action.execute()

        reason = 'Failed in locking cluster'
        self.assertEqual(action.RES_RETRY, res_code)
        self.assertEqual(reason, res_msg)
        mock_load.assert_called_once_with(action.context, node_id='NODE_ID')
        mock_acquire.assert_called_once_with(self.ctx, 'FAKE_CLUSTER',
                                             'ACTION_ID', None,
                                             lock.NODE_SCOPE, False)

    @mock.patch.object(lock, 'cluster_lock_acquire')
    @mock.patch.object(lock, 'cluster_lock_release')
    @mock.patch.object(base_action.Action, 'policy_check')
    def test_execute_failed_policy_check(self, mock_check, mock_release,
                                         mock_acquire, mock_load):
        node = mock.Mock()
        node.id = 'NID'
        node.cluster_id = 'FAKE_CLUSTER'
        mock_load.return_value = node

        action = node_action.NodeAction('NODE_ID', 'NODE_FLY', self.ctx,
                                        cause='RPC Request')
        action.id = 'ACTION_ID'
        action.data = {
            'status': policy_mod.CHECK_ERROR,
            'reason': 'Failed policy checking'
        }
        mock_acquire.return_value = action.id

        res_code, res_msg = action.execute()

        reason = 'Policy check: Failed policy checking'
        self.assertEqual(action.RES_ERROR, res_code)
        self.assertEqual(reason, res_msg)
        mock_load.assert_called_once_with(action.context, node_id='NODE_ID')
        mock_acquire.assert_called_once_with(self.ctx, 'FAKE_CLUSTER',
                                             'ACTION_ID', None,
                                             lock.NODE_SCOPE, False)
        mock_release.assert_called_once_with('FAKE_CLUSTER', 'ACTION_ID',
                                             lock.NODE_SCOPE)
        mock_check.assert_called_once_with('FAKE_CLUSTER', 'BEFORE')

    @mock.patch.object(lock, 'cluster_lock_acquire')
    @mock.patch.object(lock, 'cluster_lock_release')
    @mock.patch.object(base_action.Action, 'policy_check')
    def test_execute_policy_check_exception(self, mock_check, mock_release,
                                            mock_acquire, mock_load):
        node = mock.Mock()
        node.id = 'NID'
        node.cluster_id = 'FAKE_CLUSTER'
        mock_load.return_value = node

        action = node_action.NodeAction('NODE_ID', 'NODE_FLY', self.ctx,
                                        cause='RPC Request')
        action.id = 'ACTION_ID'
        action.data = {
            'status': policy_mod.CHECK_NONE,
            'reason': ''
        }
        mock_acquire.return_value = action.id

        mock_check.side_effect = Exception('error')

        res_code, res_msg = action.execute()

        reason = 'Policy check: '
        self.assertEqual(action.RES_ERROR, res_code)
        self.assertEqual(reason, res_msg)
        mock_load.assert_called_once_with(action.context, node_id='NODE_ID')
        mock_acquire.assert_called_once_with(self.ctx, 'FAKE_CLUSTER',
                                             'ACTION_ID', None,
                                             lock.NODE_SCOPE, False)
        mock_release.assert_called_once_with('FAKE_CLUSTER', 'ACTION_ID',
                                             lock.NODE_SCOPE)
        mock_check.assert_called_once_with('FAKE_CLUSTER', 'BEFORE')

    @mock.patch.object(lock, 'cluster_lock_acquire')
    @mock.patch.object(lock, 'cluster_lock_release')
    @mock.patch.object(lock, 'node_lock_acquire')
    @mock.patch.object(lock, 'node_lock_release')
    @mock.patch.object(base_action.Action, 'policy_check')
    def test_execute_no_policy_check(self, mock_check,
                                     mock_nl_release, mock_nl_acquire,
                                     mock_cl_release, mock_cl_acquire,
                                     mock_load):
        node_id = 'NODE_ID'
        node = mock.Mock(id=node_id, cluster_id='FAKE_CLUSTER')
        mock_load.return_value = node
        action = node_action.NodeAction(node_id, 'NODE_FLY', self.ctx,
                                        cause=consts.CAUSE_DERIVED)
        action.id = 'ACTION_ID'
        action.owner = 'OWNER'
        mock_exec = self.patchobject(action, '_execute',
                                     return_value=(action.RES_OK, 'Good'))
        mock_nl_acquire.return_value = action.id

        res_code, res_msg = action.execute()

        self.assertEqual(action.RES_OK, res_code)
        self.assertEqual('Good', res_msg)
        mock_load.assert_called_once_with(action.context, node_id=node_id)
        self.assertEqual(0, mock_cl_acquire.call_count)
        self.assertEqual(0, mock_cl_release.call_count)
        mock_nl_acquire.assert_called_once_with(self.ctx, node_id,
                                                action.id, action.owner,
                                                False)
        mock_nl_release.assert_called_once_with(node_id, action.id)
        mock_exec.assert_called_once_with()
        self.assertEqual(0, mock_check.call_count)

    @mock.patch.object(lock, 'cluster_lock_acquire')
    @mock.patch.object(lock, 'cluster_lock_release')
    @mock.patch.object(base_action.Action, 'policy_check')
    @mock.patch.object(lock, 'node_lock_acquire')
    @mock.patch.object(lock, 'node_lock_release')
    def test_execute_failed_locking_node(self, mock_release_node,
                                         mock_acquire_node, mock_check,
                                         mock_release, mock_acquire,
                                         mock_load):
        node = mock.Mock()
        node.cluster_id = 'FAKE_CLUSTER'
        node.id = 'NODE_ID'
        mock_load.return_value = node

        action = node_action.NodeAction('NODE_ID', 'NODE_FLY', self.ctx,
                                        cause='RPC Request')
        action.id = 'ACTION_ID'
        action.data = {
            'status': policy_mod.CHECK_OK,
            'reason': 'Policy checking passed'
        }
        mock_acquire.return_value = 'ACTION_ID'
        mock_acquire_node.return_value = None

        res_code, res_msg = action.execute()

        reason = 'Failed in locking node'
        self.assertEqual(action.RES_RETRY, res_code)
        self.assertEqual(reason, res_msg)
        mock_load.assert_called_once_with(action.context, node_id='NODE_ID')
        mock_acquire.assert_called_once_with(self.ctx, 'FAKE_CLUSTER',
                                             'ACTION_ID', None,
                                             lock.NODE_SCOPE, False)
        mock_release.assert_called_once_with('FAKE_CLUSTER', 'ACTION_ID',
                                             lock.NODE_SCOPE)
        mock_check.assert_called_once_with('FAKE_CLUSTER', 'BEFORE')
        mock_acquire_node.assert_called_once_with(self.ctx, 'NODE_ID',
                                                  'ACTION_ID', None, False)
        mock_release_node.assert_called_once_with('NODE_ID', 'ACTION_ID')

    @mock.patch.object(lock, 'cluster_lock_acquire')
    @mock.patch.object(lock, 'cluster_lock_release')
    @mock.patch.object(base_action.Action, 'policy_check')
    @mock.patch.object(lock, 'node_lock_acquire')
    @mock.patch.object(lock, 'node_lock_release')
    def test_execute_success_stealing_node_lock(self, mock_release_node,
                                                mock_acquire_node, mock_check,
                                                mock_release, mock_acquire,
                                                mock_load):
        node = mock.Mock()
        node.cluster_id = 'FAKE_CLUSTER'
        node.id = 'NODE_ID'
        mock_load.return_value = node

        action = node_action.NodeAction('NODE_ID', 'NODE_OPERATION', self.ctx,
                                        cause='RPC Request')
        action.id = 'ACTION_ID'
        action.data = {
            'status': policy_mod.CHECK_OK,
            'reason': 'Policy checking passed'
        }
        action.inputs = {'operation': 'stop', 'params': {}}

        mock_acquire.return_value = 'ACTION_ID'
        mock_acquire_node.return_value = True

        res_code, res_msg = action.execute()

        reason = "Node operation 'stop' succeeded."
        self.assertEqual(action.RES_OK, res_code)
        self.assertEqual(reason, res_msg)
        mock_load.assert_called_once_with(action.context, node_id='NODE_ID')
        mock_acquire.assert_called_once_with(self.ctx, 'FAKE_CLUSTER',
                                             'ACTION_ID', None,
                                             lock.NODE_SCOPE, False)
        policy_calls = [
            mock.call('FAKE_CLUSTER', 'BEFORE'),
            mock.call('FAKE_CLUSTER', 'AFTER')
        ]
        mock_release.assert_called_once_with('FAKE_CLUSTER', 'ACTION_ID',
                                             lock.NODE_SCOPE)
        mock_check.assert_has_calls(policy_calls)
        mock_acquire_node.assert_called_once_with(self.ctx, 'NODE_ID',
                                                  'ACTION_ID', None, True)
        mock_release_node.assert_called_once_with('NODE_ID', 'ACTION_ID')

    @mock.patch.object(lock, 'cluster_lock_acquire')
    @mock.patch.object(lock, 'cluster_lock_release')
    @mock.patch.object(base_action.Action, 'policy_check')
    @mock.patch.object(lock, 'node_lock_acquire')
    @mock.patch.object(lock, 'node_lock_release')
    def test_execute_success(self, mock_release_node, mock_acquire_node,
                             mock_check, mock_release, mock_acquire,
                             mock_load):
        def fake_execute():
            node.cluster_id = ''
            return (action.RES_OK, 'Execution ok')

        node = mock.Mock()
        node.cluster_id = 'FAKE_CLUSTER'
        node.id = 'NODE_ID'
        mock_load.return_value = node

        action = node_action.NodeAction(node.id, 'NODE_FLY', self.ctx,
                                        cause='RPC Request')
        action.id = 'ACTION_ID'
        # check result
        action.data = {
            'status': policy_mod.CHECK_OK,
            'reason': 'Policy checking passed'
        }
        self.patchobject(action, '_execute', side_effect=fake_execute)
        mock_acquire.return_value = 'ACTION_ID'
        mock_acquire_node.return_value = 'ACTION_ID'

        res_code, res_msg = action.execute()

        reason = 'Execution ok'
        self.assertEqual(action.RES_OK, res_code)
        self.assertEqual(reason, res_msg)
        mock_load.assert_called_once_with(action.context, node_id='NODE_ID')
        mock_acquire.assert_called_once_with(self.ctx, 'FAKE_CLUSTER',
                                             'ACTION_ID', None,
                                             lock.NODE_SCOPE, False)
        mock_release.assert_called_once_with('FAKE_CLUSTER', 'ACTION_ID',
                                             lock.NODE_SCOPE)
        mock_acquire_node.assert_called_once_with(self.ctx, 'NODE_ID',
                                                  'ACTION_ID', None, False)
        mock_release_node.assert_called_once_with('NODE_ID', 'ACTION_ID')
        check_calls = [
            mock.call('FAKE_CLUSTER', 'BEFORE'),
            mock.call('FAKE_CLUSTER', 'AFTER')
        ]
        mock_check.assert_has_calls(check_calls)

    @mock.patch.object(lock, 'cluster_lock_acquire')
    @mock.patch.object(lock, 'cluster_lock_release')
    @mock.patch.object(base_action.Action, 'policy_check')
    @mock.patch.object(lock, 'node_lock_acquire')
    @mock.patch.object(lock, 'node_lock_release')
    def test_execute_failed_execute(self, mock_release_node, mock_acquire_node,
                                    mock_check, mock_release, mock_acquire,
                                    mock_load):
        node = mock.Mock()
        node.cluster_id = 'FAKE_CLUSTER'
        node.id = 'NODE_ID'
        mock_load.return_value = node

        action = node_action.NodeAction(node.id, 'NODE_FLY', self.ctx,
                                        cause='RPC Request')
        action.id = 'ACTION_ID'
        # check result
        action.data = {
            'status': policy_mod.CHECK_OK,
            'reason': 'Policy checking passed'
        }
        self.patchobject(action, '_execute',
                         return_value=(action.RES_ERROR, 'Execution Failed'))
        mock_acquire.return_value = 'ACTION_ID'
        mock_acquire_node.return_value = 'ACTION_ID'

        res_code, res_msg = action.execute()

        reason = 'Execution Failed'
        self.assertEqual(action.RES_ERROR, res_code)
        self.assertEqual(reason, res_msg)
        mock_load.assert_called_once_with(action.context, node_id='NODE_ID')
        mock_acquire.assert_called_once_with(self.ctx, 'FAKE_CLUSTER',
                                             'ACTION_ID', None,
                                             lock.NODE_SCOPE, False)
        mock_release.assert_called_once_with('FAKE_CLUSTER', 'ACTION_ID',
                                             lock.NODE_SCOPE)
        mock_acquire_node.assert_called_once_with(self.ctx, 'NODE_ID',
                                                  'ACTION_ID', None, False)
        mock_release_node.assert_called_once_with('NODE_ID', 'ACTION_ID')
        check_calls = [
            mock.call('FAKE_CLUSTER', 'BEFORE'),
            mock.call('FAKE_CLUSTER', 'AFTER')
        ]
        mock_check.assert_has_calls(check_calls)

    @mock.patch.object(lock, 'cluster_lock_acquire')
    @mock.patch.object(lock, 'cluster_lock_release')
    @mock.patch.object(lock, 'node_lock_acquire')
    @mock.patch.object(lock, 'node_lock_release')
    def test_execute_failed_post_check(self, mock_release_node,
                                       mock_acquire_node,
                                       mock_release, mock_acquire,
                                       mock_load):

        def fake_check(cluster_id, target):
            if target == 'BEFORE':
                action.data = {
                    'status': policy_mod.CHECK_OK,
                    'reason': 'Policy checking passed'
                }
            else:
                action.data = {
                    'status': policy_mod.CHECK_ERROR,
                    'reason': 'Policy checking failed'
                }

        node = mock.Mock()
        node.cluster_id = 'FAKE_CLUSTER'
        node.id = 'NODE_ID'
        mock_load.return_value = node

        action = node_action.NodeAction('NODE_ID', 'NODE_FLY', self.ctx,
                                        cause='RPC Request')
        action.id = 'ACTION_ID'
        mock_check = self.patchobject(action, 'policy_check',
                                      side_effect=fake_check)
        # check result
        self.patchobject(action, '_execute',
                         return_value=(action.RES_OK, 'Ignored'))
        mock_acquire.return_value = 'ACTION_ID'
        mock_acquire_node.return_value = 'ACTION_ID'

        res_code, res_msg = action.execute()

        reason = 'Policy check: Policy checking failed'
        self.assertEqual(action.RES_ERROR, res_code)
        self.assertEqual(reason, res_msg)
        mock_load.assert_called_once_with(action.context, node_id='NODE_ID')
        mock_acquire.assert_called_once_with(self.ctx, 'FAKE_CLUSTER',
                                             'ACTION_ID', None,
                                             lock.NODE_SCOPE, False)
        mock_release.assert_called_once_with('FAKE_CLUSTER', 'ACTION_ID',
                                             lock.NODE_SCOPE)
        check_calls = [
            mock.call('FAKE_CLUSTER', 'BEFORE'),
            mock.call('FAKE_CLUSTER', 'AFTER')
        ]
        mock_check.assert_has_calls(check_calls)
        mock_acquire_node.assert_called_once_with(self.ctx, 'NODE_ID',
                                                  'ACTION_ID', None, False)
        mock_release_node.assert_called_once_with('NODE_ID', 'ACTION_ID')
