add tempest tests
add overlapping templates tests for all actions add reset-server-state action test add set-state action on instance test Change-Id: I442f15e66767e5cb4cd0d0c3f6c06f5d02793eb5
This commit is contained in:
parent
1ea07a9014
commit
1245df7121
@ -76,21 +76,6 @@ class BaseVitrageTempest(base.BaseTestCase):
|
|||||||
filtered_list.append(item)
|
filtered_list.append(item)
|
||||||
return filtered_list
|
return filtered_list
|
||||||
|
|
||||||
def _check_num_instances(self, num_instances=0, state=''):
|
|
||||||
if len(TempestClients.nova().servers.list()) != num_instances:
|
|
||||||
return False
|
|
||||||
|
|
||||||
return all(instance.__dict__['status'].upper() == state.upper()
|
|
||||||
for instance in TempestClients.nova().servers.list())
|
|
||||||
|
|
||||||
def _check_num_volumes(self, num_volumes=0, state=''):
|
|
||||||
if len(TempestClients.cinder().volumes.list()) != num_volumes:
|
|
||||||
return False
|
|
||||||
|
|
||||||
return all(volume.__dict__['status'].upper() == state.upper() and
|
|
||||||
len(volume.__dict__['attachments']) == 1
|
|
||||||
for volume in TempestClients.cinder().volumes.list())
|
|
||||||
|
|
||||||
def _create_graph_from_graph_dictionary(self, api_graph):
|
def _create_graph_from_graph_dictionary(self, api_graph):
|
||||||
self.assertIsNotNone(api_graph)
|
self.assertIsNotNone(api_graph)
|
||||||
graph = NXGraph()
|
graph = NXGraph()
|
||||||
|
@ -14,17 +14,32 @@
|
|||||||
import six
|
import six
|
||||||
|
|
||||||
|
|
||||||
def get_first_match(list_of_dicts, subset_dict):
|
def get_first_match(list_of_dicts, **kwargs):
|
||||||
|
subset_dict = _subset_dict(**kwargs)
|
||||||
for d in list_of_dicts:
|
for d in list_of_dicts:
|
||||||
if is_subset(subset_dict, d):
|
if is_subset(subset_dict, d):
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
|
||||||
def get_all_matchs(list_of_dicts, subset_dict):
|
def get_all_matches(list_of_dicts, **kwargs):
|
||||||
# TODO(idan_hefetz) this method can replace the notorious
|
# TODO(idan_hefetz) this method can replace the notorious
|
||||||
# TODO(idan_hefetz) '_filter_list_by_pairs_parameters'
|
# TODO(idan_hefetz) '_filter_list_by_pairs_parameters'
|
||||||
|
subset_dict = _subset_dict(**kwargs)
|
||||||
return [d for d in list_of_dicts if is_subset(subset_dict, d)]
|
return [d for d in list_of_dicts if is_subset(subset_dict, d)]
|
||||||
|
|
||||||
|
|
||||||
def is_subset(subset, full):
|
def is_subset(subset, full):
|
||||||
return six.viewitems(subset) <= six.viewitems(full)
|
if not subset:
|
||||||
|
return True
|
||||||
|
full_dict = full
|
||||||
|
if type(full) is not dict:
|
||||||
|
full_dict = full.__dict__
|
||||||
|
return six.viewitems(subset) <= six.viewitems(full_dict)
|
||||||
|
|
||||||
|
|
||||||
|
def _subset_dict(**kwargs):
|
||||||
|
subset_dict_final = dict()
|
||||||
|
for keyword, arg in kwargs.items():
|
||||||
|
if arg is not None:
|
||||||
|
subset_dict_final[keyword] = arg
|
||||||
|
return subset_dict_final
|
||||||
|
@ -13,13 +13,14 @@
|
|||||||
# under the License.
|
# under the License.
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
from vitrage_tempest_tests.tests.common import general_utils as g_utils
|
||||||
from vitrage_tempest_tests.tests.common import glance_utils
|
from vitrage_tempest_tests.tests.common import glance_utils
|
||||||
from vitrage_tempest_tests.tests.common import neutron_utils
|
from vitrage_tempest_tests.tests.common import neutron_utils
|
||||||
from vitrage_tempest_tests.tests.common.tempest_clients import TempestClients
|
from vitrage_tempest_tests.tests.common.tempest_clients import TempestClients
|
||||||
from vitrage_tempest_tests.tests.utils import wait_for_status
|
from vitrage_tempest_tests.tests.utils import wait_for_status
|
||||||
|
|
||||||
|
|
||||||
def create_instances(num_instances, set_public_network=False, name='vm'):
|
def create_instances(num_instances=1, set_public_network=False, name='vm'):
|
||||||
nics = []
|
nics = []
|
||||||
flavor = get_first_flavor()
|
flavor = get_first_flavor()
|
||||||
image = glance_utils.get_first_image()
|
image = glance_utils.get_first_image()
|
||||||
@ -39,14 +40,18 @@ def create_instances(num_instances, set_public_network=False, name='vm'):
|
|||||||
return resources
|
return resources
|
||||||
|
|
||||||
|
|
||||||
def delete_all_instances():
|
def delete_all_instances(**kwargs):
|
||||||
instances = TempestClients.nova().servers.list()
|
instances = TempestClients.nova().servers.list()
|
||||||
for instance in instances:
|
instances_to_delete = g_utils.get_all_matches(instances, **kwargs)
|
||||||
|
for item in instances_to_delete:
|
||||||
try:
|
try:
|
||||||
TempestClients.nova().servers.delete(instance)
|
TempestClients.nova().servers.delete(item)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
wait_for_status(30, _check_num_instances, num_instances=0)
|
wait_for_status(
|
||||||
|
30,
|
||||||
|
_check_num_instances,
|
||||||
|
num_instances=len(instances) - len(instances_to_delete))
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
|
|
||||||
|
|
||||||
|
@ -13,11 +13,11 @@
|
|||||||
# under the License.
|
# under the License.
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from vitrage.common.constants import VertexProperties as VProps
|
|
||||||
from vitrage.datasources import NOVA_HOST_DATASOURCE
|
from vitrage.datasources import NOVA_HOST_DATASOURCE
|
||||||
|
from vitrage.datasources import NOVA_INSTANCE_DATASOURCE
|
||||||
from vitrage_tempest_tests.tests.api.event.base import DOWN
|
from vitrage_tempest_tests.tests.api.event.base import DOWN
|
||||||
from vitrage_tempest_tests.tests.api.event.base import UP
|
from vitrage_tempest_tests.tests.api.event.base import UP
|
||||||
from vitrage_tempest_tests.tests.common.general_utils import get_first_match
|
from vitrage_tempest_tests.tests.common import general_utils as g_utils
|
||||||
from vitrage_tempest_tests.tests.common.tempest_clients import TempestClients
|
from vitrage_tempest_tests.tests.common.tempest_clients import TempestClients
|
||||||
|
|
||||||
|
|
||||||
@ -36,6 +36,13 @@ def generate_fake_host_alarm(hostname, event_type, enabled=True):
|
|||||||
TempestClients.vitrage().event.post(event_time_iso, event_type, details)
|
TempestClients.vitrage().event.post(event_time_iso, event_type, details)
|
||||||
|
|
||||||
|
|
||||||
def get_first_host():
|
def get_first_host(**kwargs):
|
||||||
nodes = TempestClients.vitrage().topology.get(all_tenants=True)['nodes']
|
hosts = TempestClients.vitrage().resource.list(
|
||||||
return get_first_match(nodes, {VProps.VITRAGE_TYPE: NOVA_HOST_DATASOURCE})
|
NOVA_HOST_DATASOURCE, all_tenants=True)
|
||||||
|
return g_utils.get_first_match(hosts, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def get_first_instance(**kwargs):
|
||||||
|
instances = TempestClients.vitrage().resource.list(
|
||||||
|
NOVA_INSTANCE_DATASOURCE, all_tenants=True)
|
||||||
|
return g_utils.get_first_match(instances, **kwargs)
|
||||||
|
76
vitrage_tempest_tests/tests/e2e/test_actions_base.py
Normal file
76
vitrage_tempest_tests/tests/e2e/test_actions_base.py
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
# Copyright 2017 - Nokia
|
||||||
|
#
|
||||||
|
# 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 time
|
||||||
|
|
||||||
|
from oslo_log import log as logging
|
||||||
|
|
||||||
|
from vitrage.common.constants import VertexProperties as VProps
|
||||||
|
from vitrage_tempest_tests.tests.base import BaseVitrageTempest
|
||||||
|
from vitrage_tempest_tests.tests.common import general_utils as g_utils
|
||||||
|
from vitrage_tempest_tests.tests.common.tempest_clients import TempestClients
|
||||||
|
from vitrage_tempest_tests.tests.common import vitrage_utils
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class TestActionsBase(BaseVitrageTempest):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
super(TestActionsBase, cls).setUpClass()
|
||||||
|
host = vitrage_utils.get_first_host()
|
||||||
|
if not host:
|
||||||
|
raise Exception("No host found")
|
||||||
|
if not host.get(VProps.VITRAGE_AGGREGATED_STATE) == 'AVAILABLE':
|
||||||
|
raise Exception("Host is not running %s", str(host))
|
||||||
|
cls.orig_host = host
|
||||||
|
|
||||||
|
def _trigger_do_action(self, trigger_name):
|
||||||
|
vitrage_utils.generate_fake_host_alarm(
|
||||||
|
self.orig_host.get('name'),
|
||||||
|
enabled=True,
|
||||||
|
event_type=trigger_name
|
||||||
|
)
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
def _trigger_undo_action(self, trigger_name):
|
||||||
|
vitrage_utils.generate_fake_host_alarm(
|
||||||
|
self.orig_host.get('name'),
|
||||||
|
enabled=False,
|
||||||
|
event_type=trigger_name
|
||||||
|
)
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
def _check_deduced(self, deduced_count, deduced_props, resource_id):
|
||||||
|
alarms = TempestClients.vitrage().alarm.list(
|
||||||
|
vitrage_id=resource_id,
|
||||||
|
all_tenants=True)
|
||||||
|
deduces = g_utils.get_all_matches(alarms, **deduced_props)
|
||||||
|
self.assertEqual(
|
||||||
|
deduced_count,
|
||||||
|
len(deduces),
|
||||||
|
'Expected %s deduces\n - \n%s\n - \n%s' %
|
||||||
|
(str(deduced_count), str(alarms), str(deduces)))
|
||||||
|
|
||||||
|
def _check_rca(self, rca, expected_alarms, inspected):
|
||||||
|
self.assertEqual(len(expected_alarms), len(rca['nodes']))
|
||||||
|
for expected_alarm in expected_alarms:
|
||||||
|
self.assertIsNotNone(
|
||||||
|
g_utils.get_first_match(rca['nodes'], **expected_alarm),
|
||||||
|
'expected_alarm is not in the rca %s' % str(expected_alarm))
|
||||||
|
rca_inspected = rca['nodes'][rca['inspected_index']]
|
||||||
|
self.assertEqual(
|
||||||
|
True,
|
||||||
|
g_utils.is_subset(inspected, rca_inspected),
|
||||||
|
'Invalid inspected item \n%s\n%s' %
|
||||||
|
(str(rca_inspected), str(inspected)))
|
@ -20,16 +20,19 @@ from vitrage.common.constants import VertexProperties as VProps
|
|||||||
from vitrage.datasources.doctor import DOCTOR_DATASOURCE
|
from vitrage.datasources.doctor import DOCTOR_DATASOURCE
|
||||||
from vitrage.evaluator.actions.evaluator_event_transformer import \
|
from vitrage.evaluator.actions.evaluator_event_transformer import \
|
||||||
VITRAGE_DATASOURCE
|
VITRAGE_DATASOURCE
|
||||||
from vitrage_tempest_tests.tests.base import BaseVitrageTempest
|
|
||||||
from vitrage_tempest_tests.tests.common import general_utils as g_utils
|
from vitrage_tempest_tests.tests.common import general_utils as g_utils
|
||||||
|
from vitrage_tempest_tests.tests.common import nova_utils
|
||||||
from vitrage_tempest_tests.tests.common.tempest_clients import TempestClients
|
from vitrage_tempest_tests.tests.common.tempest_clients import TempestClients
|
||||||
from vitrage_tempest_tests.tests.common import vitrage_utils
|
from vitrage_tempest_tests.tests.common import vitrage_utils
|
||||||
|
from vitrage_tempest_tests.tests.e2e.test_actions_base import TestActionsBase
|
||||||
from vitrage_tempest_tests.tests import utils
|
from vitrage_tempest_tests.tests import utils
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
TRIGGER_ALARM_1 = 'e2e.test_basic_actions.trigger.alarm1'
|
TRIGGER_ALARM_1 = 'e2e.test_basic_actions.trigger.alarm1'
|
||||||
TRIGGER_ALARM_2 = 'e2e.test_basic_actions.trigger.alarm2'
|
TRIGGER_ALARM_2 = 'e2e.test_basic_actions.trigger.alarm2'
|
||||||
|
TRIGGER_ALARM_3 = 'e2e.test_basic_actions.trigger.alarm3'
|
||||||
|
TRIGGER_ALARM_4 = 'e2e.test_basic_actions.trigger.alarm4'
|
||||||
DEDUCED = 'e2e.test_basic_actions.deduced.alarm'
|
DEDUCED = 'e2e.test_basic_actions.deduced.alarm'
|
||||||
|
|
||||||
TRIGGER_ALARM_2_PROPS = {
|
TRIGGER_ALARM_2_PROPS = {
|
||||||
@ -45,35 +48,9 @@ DEDUCED_PROPS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class TestBasicActions(BaseVitrageTempest):
|
class TestBasicActions(TestActionsBase):
|
||||||
@classmethod
|
|
||||||
def setUpClass(cls):
|
|
||||||
super(TestBasicActions, cls).setUpClass()
|
|
||||||
host = vitrage_utils.get_first_host()
|
|
||||||
if not host:
|
|
||||||
raise Exception("No host found")
|
|
||||||
if not host.get(VProps.VITRAGE_AGGREGATED_STATE) == 'AVAILABLE':
|
|
||||||
raise Exception("Host is not running %s", str(host))
|
|
||||||
cls.orig_host = host
|
|
||||||
|
|
||||||
def _trigger_do_action(self, trigger_name):
|
|
||||||
vitrage_utils.generate_fake_host_alarm(
|
|
||||||
self.orig_host.get('name'),
|
|
||||||
enabled=True,
|
|
||||||
event_type=trigger_name
|
|
||||||
)
|
|
||||||
time.sleep(2)
|
|
||||||
|
|
||||||
def _trigger_undo_action(self, trigger_name):
|
|
||||||
vitrage_utils.generate_fake_host_alarm(
|
|
||||||
self.orig_host.get('name'),
|
|
||||||
enabled=False,
|
|
||||||
event_type=trigger_name
|
|
||||||
)
|
|
||||||
time.sleep(2)
|
|
||||||
|
|
||||||
@utils.tempest_logger
|
@utils.tempest_logger
|
||||||
def test_action_set_state(self):
|
def test_action_set_state_host(self):
|
||||||
try:
|
try:
|
||||||
|
|
||||||
# Do
|
# Do
|
||||||
@ -90,26 +67,56 @@ class TestBasicActions(BaseVitrageTempest):
|
|||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self.orig_host.get(VProps.VITRAGE_AGGREGATED_STATE),
|
self.orig_host.get(VProps.VITRAGE_AGGREGATED_STATE),
|
||||||
curr_host.get(VProps.VITRAGE_AGGREGATED_STATE),
|
curr_host.get(VProps.VITRAGE_AGGREGATED_STATE),
|
||||||
'state should change after set_state action')
|
'state should change after undo set_state action')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self._handle_exception(e)
|
self._handle_exception(e)
|
||||||
raise
|
raise
|
||||||
finally:
|
finally:
|
||||||
self._trigger_undo_action(TRIGGER_ALARM_1)
|
self._trigger_undo_action(TRIGGER_ALARM_1)
|
||||||
|
|
||||||
|
@utils.tempest_logger
|
||||||
|
def test_action_set_state_instance(self):
|
||||||
|
|
||||||
|
vm_id = ""
|
||||||
|
try:
|
||||||
|
vm_id = nova_utils.create_instances(set_public_network=True)[0].id
|
||||||
|
|
||||||
|
# Do
|
||||||
|
orig_instance = vitrage_utils.get_first_instance(id=vm_id)
|
||||||
|
self._trigger_do_action(TRIGGER_ALARM_3)
|
||||||
|
curr_instance = vitrage_utils.get_first_instance(id=vm_id)
|
||||||
|
self.assertEqual(
|
||||||
|
'ERROR',
|
||||||
|
curr_instance.get(VProps.VITRAGE_AGGREGATED_STATE),
|
||||||
|
'state should change after set_state action')
|
||||||
|
|
||||||
|
# Undo
|
||||||
|
self._trigger_undo_action(TRIGGER_ALARM_3)
|
||||||
|
curr_instance = vitrage_utils.get_first_instance(id=vm_id)
|
||||||
|
self.assertEqual(
|
||||||
|
orig_instance.get(VProps.VITRAGE_AGGREGATED_STATE),
|
||||||
|
curr_instance.get(VProps.VITRAGE_AGGREGATED_STATE),
|
||||||
|
'state should change after undo set_state action')
|
||||||
|
except Exception as e:
|
||||||
|
self._handle_exception(e)
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
self._trigger_undo_action(TRIGGER_ALARM_3)
|
||||||
|
nova_utils.delete_all_instances(id=vm_id)
|
||||||
|
|
||||||
@utils.tempest_logger
|
@utils.tempest_logger
|
||||||
def test_action_mark_down_host(self):
|
def test_action_mark_down_host(self):
|
||||||
try:
|
try:
|
||||||
host_name = self.orig_host.get(VProps.NAME)
|
host_name = self.orig_host.get(VProps.NAME)
|
||||||
|
|
||||||
# Do
|
# Do
|
||||||
self._trigger_do_action(TRIGGER_ALARM_1)
|
self._trigger_do_action(TRIGGER_ALARM_4)
|
||||||
nova_service = TempestClients.nova().services.list(
|
nova_service = TempestClients.nova().services.list(
|
||||||
host=host_name, binary='nova-compute')[0]
|
host=host_name, binary='nova-compute')[0]
|
||||||
self.assertEqual("down", str(nova_service.state))
|
self.assertEqual("down", str(nova_service.state))
|
||||||
|
|
||||||
# Undo
|
# Undo
|
||||||
self._trigger_undo_action(TRIGGER_ALARM_1)
|
self._trigger_undo_action(TRIGGER_ALARM_4)
|
||||||
nova_service = TempestClients.nova().services.list(
|
nova_service = TempestClients.nova().services.list(
|
||||||
host=host_name, binary='nova-compute')[0]
|
host=host_name, binary='nova-compute')[0]
|
||||||
self.assertEqual("up", str(nova_service.state))
|
self.assertEqual("up", str(nova_service.state))
|
||||||
@ -117,7 +124,31 @@ class TestBasicActions(BaseVitrageTempest):
|
|||||||
self._handle_exception(e)
|
self._handle_exception(e)
|
||||||
raise
|
raise
|
||||||
finally:
|
finally:
|
||||||
self._trigger_undo_action(TRIGGER_ALARM_1)
|
self._trigger_undo_action(TRIGGER_ALARM_4)
|
||||||
|
# nova.host datasource may take up to snapshot_intreval to update
|
||||||
|
time.sleep(130)
|
||||||
|
|
||||||
|
@utils.tempest_logger
|
||||||
|
def test_action_mark_down_instance(self):
|
||||||
|
vm_id = ""
|
||||||
|
try:
|
||||||
|
vm_id = nova_utils.create_instances(set_public_network=True)[0].id
|
||||||
|
# Do
|
||||||
|
self._trigger_do_action(TRIGGER_ALARM_3)
|
||||||
|
nova_instance = TempestClients.nova().servers.get(vm_id)
|
||||||
|
self.assertEqual("ERROR", str(nova_instance.status))
|
||||||
|
|
||||||
|
# Undo
|
||||||
|
self._trigger_undo_action(TRIGGER_ALARM_3)
|
||||||
|
nova_instance = TempestClients.nova().servers.get(vm_id)
|
||||||
|
self.assertEqual("ACTIVE", str(nova_instance.status))
|
||||||
|
except Exception as e:
|
||||||
|
self._handle_exception(e)
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
pass
|
||||||
|
self._trigger_undo_action(TRIGGER_ALARM_3)
|
||||||
|
nova_utils.delete_all_instances(id=vm_id)
|
||||||
|
|
||||||
@utils.tempest_logger
|
@utils.tempest_logger
|
||||||
def test_action_deduce_alarm(self):
|
def test_action_deduce_alarm(self):
|
||||||
@ -137,17 +168,6 @@ class TestBasicActions(BaseVitrageTempest):
|
|||||||
finally:
|
finally:
|
||||||
self._trigger_undo_action(TRIGGER_ALARM_2)
|
self._trigger_undo_action(TRIGGER_ALARM_2)
|
||||||
|
|
||||||
def _check_deduced(self, deduced_count, deduced_props, resource_id):
|
|
||||||
alarms = TempestClients.vitrage().alarm.list(
|
|
||||||
vitrage_id=resource_id,
|
|
||||||
all_tenants=True)
|
|
||||||
deduces = g_utils.get_all_matchs(alarms, deduced_props)
|
|
||||||
self.assertEqual(
|
|
||||||
deduced_count,
|
|
||||||
len(deduces),
|
|
||||||
'Expected %s deduces\n - \n%s\n - \n%s' %
|
|
||||||
(str(deduced_count), str(alarms), str(deduces)))
|
|
||||||
|
|
||||||
@utils.tempest_logger
|
@utils.tempest_logger
|
||||||
def test_action_add_causal_relationship(self):
|
def test_action_add_causal_relationship(self):
|
||||||
try:
|
try:
|
||||||
@ -156,9 +176,10 @@ class TestBasicActions(BaseVitrageTempest):
|
|||||||
alarms = TempestClients.vitrage().alarm.list(
|
alarms = TempestClients.vitrage().alarm.list(
|
||||||
vitrage_id=self.orig_host.get(VProps.VITRAGE_ID),
|
vitrage_id=self.orig_host.get(VProps.VITRAGE_ID),
|
||||||
all_tenants=True)
|
all_tenants=True)
|
||||||
|
self.assertEqual(True, len(alarms) >= 2, 'alarms %s' % str(alarms))
|
||||||
|
|
||||||
deduced = g_utils.get_all_matchs(alarms, DEDUCED_PROPS)[0]
|
deduced = g_utils.get_first_match(alarms, **DEDUCED_PROPS)
|
||||||
trigger = g_utils.get_all_matchs(alarms, TRIGGER_ALARM_2_PROPS)[0]
|
trigger = g_utils.get_first_match(alarms, **TRIGGER_ALARM_2_PROPS)
|
||||||
|
|
||||||
# Get Rca for the deduced
|
# Get Rca for the deduced
|
||||||
rca = TempestClients.vitrage().rca.get(
|
rca = TempestClients.vitrage().rca.get(
|
||||||
@ -174,16 +195,3 @@ class TestBasicActions(BaseVitrageTempest):
|
|||||||
raise
|
raise
|
||||||
finally:
|
finally:
|
||||||
self._trigger_undo_action(TRIGGER_ALARM_2)
|
self._trigger_undo_action(TRIGGER_ALARM_2)
|
||||||
|
|
||||||
def _check_rca(self, rca, expected_alarms, inspected):
|
|
||||||
self.assertEqual(len(expected_alarms), len(rca['nodes']))
|
|
||||||
for expected_alarm in expected_alarms:
|
|
||||||
self.assertIsNotNone(
|
|
||||||
g_utils.get_first_match(rca['nodes'], expected_alarm),
|
|
||||||
'expected_alarm is not in the rca %s' % str(expected_alarm))
|
|
||||||
rca_inspected = rca['nodes'][rca['inspected_index']]
|
|
||||||
self.assertEqual(
|
|
||||||
True,
|
|
||||||
g_utils.is_subset(inspected, rca_inspected),
|
|
||||||
'Invalid inspected item \n%s\n%s' %
|
|
||||||
(str(rca_inspected), str(inspected)))
|
|
||||||
|
234
vitrage_tempest_tests/tests/e2e/test_overlapping_actions.py
Normal file
234
vitrage_tempest_tests/tests/e2e/test_overlapping_actions.py
Normal file
@ -0,0 +1,234 @@
|
|||||||
|
# Copyright 2017 - Nokia
|
||||||
|
#
|
||||||
|
# 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 time
|
||||||
|
|
||||||
|
from oslo_log import log as logging
|
||||||
|
|
||||||
|
from vitrage.common.constants import EntityCategory
|
||||||
|
from vitrage.common.constants import VertexProperties as VProps
|
||||||
|
from vitrage.datasources.doctor import DOCTOR_DATASOURCE
|
||||||
|
from vitrage.evaluator.actions.evaluator_event_transformer import \
|
||||||
|
VITRAGE_DATASOURCE
|
||||||
|
from vitrage_tempest_tests.tests.common import general_utils as g_utils
|
||||||
|
from vitrage_tempest_tests.tests.common.tempest_clients import TempestClients
|
||||||
|
from vitrage_tempest_tests.tests.common import vitrage_utils
|
||||||
|
from vitrage_tempest_tests.tests.e2e.test_actions_base import TestActionsBase
|
||||||
|
from vitrage_tempest_tests.tests import utils
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
TRIGGER_ALARM_1 = 'e2e.test_overlapping_actions.trigger.alarm1'
|
||||||
|
TRIGGER_ALARM_2 = 'e2e.test_overlapping_actions.trigger.alarm2'
|
||||||
|
TRIGGER_ALARM_3 = 'e2e.test_overlapping_actions.trigger.alarm3'
|
||||||
|
TRIGGER_ALARM_4 = 'e2e.test_overlapping_actions.trigger.alarm4'
|
||||||
|
DEDUCED = 'e2e.test_overlapping_actions.deduced.alarm'
|
||||||
|
|
||||||
|
TRIGGER_ALARM_1_PROPS = {
|
||||||
|
VProps.NAME: TRIGGER_ALARM_1,
|
||||||
|
VProps.VITRAGE_CATEGORY: EntityCategory.ALARM,
|
||||||
|
VProps.VITRAGE_TYPE: DOCTOR_DATASOURCE,
|
||||||
|
}
|
||||||
|
|
||||||
|
TRIGGER_ALARM_2_PROPS = {
|
||||||
|
VProps.NAME: TRIGGER_ALARM_2,
|
||||||
|
VProps.VITRAGE_CATEGORY: EntityCategory.ALARM,
|
||||||
|
VProps.VITRAGE_TYPE: DOCTOR_DATASOURCE,
|
||||||
|
}
|
||||||
|
|
||||||
|
DEDUCED_PROPS = {
|
||||||
|
VProps.NAME: DEDUCED,
|
||||||
|
VProps.VITRAGE_CATEGORY: EntityCategory.ALARM,
|
||||||
|
VProps.VITRAGE_TYPE: VITRAGE_DATASOURCE,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class TestOverlappingcActions(TestActionsBase):
|
||||||
|
@utils.tempest_logger
|
||||||
|
def test_overlapping_action_set_state(self):
|
||||||
|
try:
|
||||||
|
# Do - first
|
||||||
|
self._trigger_do_action(TRIGGER_ALARM_1)
|
||||||
|
curr_host = vitrage_utils.get_first_host()
|
||||||
|
self.assertEqual(
|
||||||
|
'ERROR',
|
||||||
|
curr_host.get(VProps.VITRAGE_AGGREGATED_STATE),
|
||||||
|
'state should change after set_state action')
|
||||||
|
|
||||||
|
# Do - second
|
||||||
|
self._trigger_do_action(TRIGGER_ALARM_2)
|
||||||
|
curr_host = vitrage_utils.get_first_host()
|
||||||
|
self.assertEqual(
|
||||||
|
'ERROR',
|
||||||
|
curr_host.get(VProps.VITRAGE_AGGREGATED_STATE),
|
||||||
|
'state should remain unchanged')
|
||||||
|
|
||||||
|
# Undo - first
|
||||||
|
self._trigger_undo_action(TRIGGER_ALARM_1)
|
||||||
|
curr_host = vitrage_utils.get_first_host()
|
||||||
|
self.assertEqual(
|
||||||
|
'ERROR',
|
||||||
|
curr_host.get(VProps.VITRAGE_AGGREGATED_STATE),
|
||||||
|
'state should remain unchanged')
|
||||||
|
|
||||||
|
# Undo - second
|
||||||
|
self._trigger_undo_action(TRIGGER_ALARM_2)
|
||||||
|
curr_host = vitrage_utils.get_first_host()
|
||||||
|
self.assertEqual(
|
||||||
|
self.orig_host.get(VProps.VITRAGE_AGGREGATED_STATE),
|
||||||
|
curr_host.get(VProps.VITRAGE_AGGREGATED_STATE),
|
||||||
|
'state should change after undo set_state action')
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self._handle_exception(e)
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
self._trigger_undo_action(TRIGGER_ALARM_1)
|
||||||
|
self._trigger_undo_action(TRIGGER_ALARM_2)
|
||||||
|
|
||||||
|
@utils.tempest_logger
|
||||||
|
def test_overlapping_action_mark_down(self):
|
||||||
|
try:
|
||||||
|
host_name = self.orig_host.get(VProps.NAME)
|
||||||
|
|
||||||
|
# Do - first
|
||||||
|
self._trigger_do_action(TRIGGER_ALARM_3)
|
||||||
|
nova_service = TempestClients.nova().services.list(
|
||||||
|
host=host_name, binary='nova-compute')[0]
|
||||||
|
self.assertEqual("down", str(nova_service.state))
|
||||||
|
|
||||||
|
# Do - second
|
||||||
|
self._trigger_do_action(TRIGGER_ALARM_4)
|
||||||
|
nova_service = TempestClients.nova().services.list(
|
||||||
|
host=host_name, binary='nova-compute')[0]
|
||||||
|
self.assertEqual("down", str(nova_service.state))
|
||||||
|
|
||||||
|
# Undo - first
|
||||||
|
self._trigger_undo_action(TRIGGER_ALARM_3)
|
||||||
|
nova_service = TempestClients.nova().services.list(
|
||||||
|
host=host_name, binary='nova-compute')[0]
|
||||||
|
self.assertEqual("down", str(nova_service.state))
|
||||||
|
|
||||||
|
# Undo - second
|
||||||
|
self._trigger_undo_action(TRIGGER_ALARM_4)
|
||||||
|
nova_service = TempestClients.nova().services.list(
|
||||||
|
host=host_name, binary='nova-compute')[0]
|
||||||
|
self.assertEqual("up", str(nova_service.state))
|
||||||
|
except Exception as e:
|
||||||
|
self._handle_exception(e)
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
self._trigger_undo_action(TRIGGER_ALARM_3)
|
||||||
|
self._trigger_undo_action(TRIGGER_ALARM_4)
|
||||||
|
# nova.host datasource may take up to snapshot_intreval to update
|
||||||
|
time.sleep(130)
|
||||||
|
|
||||||
|
@utils.tempest_logger
|
||||||
|
def test_overlapping_action_deduce_alarm(self):
|
||||||
|
try:
|
||||||
|
host_id = self.orig_host.get(VProps.VITRAGE_ID)
|
||||||
|
|
||||||
|
# Do - first
|
||||||
|
self._trigger_do_action(TRIGGER_ALARM_1)
|
||||||
|
self._check_deduced(1, DEDUCED_PROPS, host_id)
|
||||||
|
|
||||||
|
# Do - second
|
||||||
|
self._trigger_do_action(TRIGGER_ALARM_2)
|
||||||
|
self._check_deduced(1, DEDUCED_PROPS, host_id)
|
||||||
|
|
||||||
|
# Undo - first
|
||||||
|
self._trigger_undo_action(TRIGGER_ALARM_1)
|
||||||
|
self._check_deduced(1, DEDUCED_PROPS, host_id)
|
||||||
|
|
||||||
|
# Undo - second
|
||||||
|
self._trigger_undo_action(TRIGGER_ALARM_2)
|
||||||
|
self._check_deduced(0, DEDUCED_PROPS, host_id)
|
||||||
|
except Exception as e:
|
||||||
|
self._handle_exception(e)
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
self._trigger_undo_action(TRIGGER_ALARM_1)
|
||||||
|
self._trigger_undo_action(TRIGGER_ALARM_2)
|
||||||
|
|
||||||
|
@utils.tempest_logger
|
||||||
|
def test_overlapping_action_add_causal_relationship(self):
|
||||||
|
try:
|
||||||
|
# ---- Do first & second ----
|
||||||
|
self._trigger_do_action(TRIGGER_ALARM_1)
|
||||||
|
self._trigger_do_action(TRIGGER_ALARM_2)
|
||||||
|
alarms = TempestClients.vitrage().alarm.list(
|
||||||
|
vitrage_id=self.orig_host.get(VProps.VITRAGE_ID),
|
||||||
|
all_tenants=True)
|
||||||
|
|
||||||
|
deduced = g_utils.get_first_match(alarms, **DEDUCED_PROPS)
|
||||||
|
trigger1 = g_utils.get_first_match(alarms, **TRIGGER_ALARM_1_PROPS)
|
||||||
|
trigger2 = g_utils.get_first_match(alarms, **TRIGGER_ALARM_2_PROPS)
|
||||||
|
|
||||||
|
# Get Rca for the deduced
|
||||||
|
rca = TempestClients.vitrage().rca.get(
|
||||||
|
deduced[VProps.VITRAGE_ID], all_tenants=True)
|
||||||
|
self._check_rca(rca, [deduced, trigger1, trigger2], DEDUCED_PROPS)
|
||||||
|
|
||||||
|
# Get Rca for trigger 1
|
||||||
|
rca = TempestClients.vitrage().rca.get(
|
||||||
|
trigger1[VProps.VITRAGE_ID], all_tenants=True)
|
||||||
|
self._check_rca(rca, [deduced, trigger1], TRIGGER_ALARM_1_PROPS)
|
||||||
|
|
||||||
|
# Get Rca for trigger 2
|
||||||
|
rca = TempestClients.vitrage().rca.get(
|
||||||
|
trigger2[VProps.VITRAGE_ID], all_tenants=True)
|
||||||
|
self._check_rca(rca, [deduced, trigger2], TRIGGER_ALARM_2_PROPS)
|
||||||
|
|
||||||
|
# ---- Undo - first ----
|
||||||
|
self._trigger_undo_action(TRIGGER_ALARM_1)
|
||||||
|
alarms = TempestClients.vitrage().alarm.list(
|
||||||
|
vitrage_id=self.orig_host.get(VProps.VITRAGE_ID),
|
||||||
|
all_tenants=True)
|
||||||
|
|
||||||
|
deduced = g_utils.get_first_match(alarms, **DEDUCED_PROPS)
|
||||||
|
trigger2 = g_utils.get_first_match(alarms, **TRIGGER_ALARM_2_PROPS)
|
||||||
|
|
||||||
|
# Get Rca for the deduced
|
||||||
|
rca = TempestClients.vitrage().rca.get(
|
||||||
|
deduced[VProps.VITRAGE_ID], all_tenants=True)
|
||||||
|
self._check_rca(rca, [deduced, trigger2], DEDUCED_PROPS)
|
||||||
|
|
||||||
|
# Get Rca for trigger 2
|
||||||
|
rca = TempestClients.vitrage().rca.get(
|
||||||
|
trigger2[VProps.VITRAGE_ID], all_tenants=True)
|
||||||
|
self._check_rca(rca, [deduced, trigger2], TRIGGER_ALARM_2_PROPS)
|
||||||
|
|
||||||
|
# ---- Undo - second ----
|
||||||
|
self._trigger_undo_action(TRIGGER_ALARM_2)
|
||||||
|
alarms = TempestClients.vitrage().alarm.list(
|
||||||
|
vitrage_id=self.orig_host.get(VProps.VITRAGE_ID),
|
||||||
|
all_tenants=True)
|
||||||
|
self.assertEqual(
|
||||||
|
0,
|
||||||
|
len(g_utils.get_all_matches(alarms, **TRIGGER_ALARM_1_PROPS)),
|
||||||
|
'trigger alarm 1 should have been removed')
|
||||||
|
self.assertEqual(
|
||||||
|
0,
|
||||||
|
len(g_utils.get_all_matches(alarms, **TRIGGER_ALARM_2_PROPS)),
|
||||||
|
'trigger alarm 2 should have been removed')
|
||||||
|
self.assertEqual(
|
||||||
|
0,
|
||||||
|
len(g_utils.get_all_matches(alarms, **DEDUCED_PROPS)),
|
||||||
|
'deduced alarm should have been removed')
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self._handle_exception(e)
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
self._trigger_undo_action(TRIGGER_ALARM_1)
|
||||||
|
self._trigger_undo_action(TRIGGER_ALARM_2)
|
@ -11,6 +11,14 @@ definitions:
|
|||||||
category: ALARM
|
category: ALARM
|
||||||
name: e2e.test_basic_actions.trigger.alarm2
|
name: e2e.test_basic_actions.trigger.alarm2
|
||||||
template_id: trigger_alarm_2
|
template_id: trigger_alarm_2
|
||||||
|
- entity:
|
||||||
|
category: ALARM
|
||||||
|
name: e2e.test_basic_actions.trigger.alarm3
|
||||||
|
template_id: trigger_alarm_3
|
||||||
|
- entity:
|
||||||
|
category: ALARM
|
||||||
|
name: e2e.test_basic_actions.trigger.alarm4
|
||||||
|
template_id: trigger_alarm_4
|
||||||
- entity:
|
- entity:
|
||||||
category: ALARM
|
category: ALARM
|
||||||
type: vitrage
|
type: vitrage
|
||||||
@ -20,6 +28,10 @@ definitions:
|
|||||||
category: RESOURCE
|
category: RESOURCE
|
||||||
type: nova.host
|
type: nova.host
|
||||||
template_id: host
|
template_id: host
|
||||||
|
- entity:
|
||||||
|
category: RESOURCE
|
||||||
|
type: nova.instance
|
||||||
|
template_id: instance
|
||||||
relationships:
|
relationships:
|
||||||
- relationship:
|
- relationship:
|
||||||
source: trigger_alarm_1
|
source: trigger_alarm_1
|
||||||
@ -31,11 +43,26 @@ definitions:
|
|||||||
relationship_type: on
|
relationship_type: on
|
||||||
target: host
|
target: host
|
||||||
template_id : trigger_alarm_2_on_host
|
template_id : trigger_alarm_2_on_host
|
||||||
|
- relationship:
|
||||||
|
source: trigger_alarm_3
|
||||||
|
relationship_type: on
|
||||||
|
target: host
|
||||||
|
template_id : trigger_alarm_3_on_host
|
||||||
|
- relationship:
|
||||||
|
source: trigger_alarm_4
|
||||||
|
relationship_type: on
|
||||||
|
target: host
|
||||||
|
template_id : trigger_alarm_4_on_host
|
||||||
- relationship:
|
- relationship:
|
||||||
source: deduced_alarm
|
source: deduced_alarm
|
||||||
relationship_type: on
|
relationship_type: on
|
||||||
target: host
|
target: host
|
||||||
template_id : deduced_alarm_on_host
|
template_id : deduced_alarm_on_host
|
||||||
|
- relationship:
|
||||||
|
source: host
|
||||||
|
target: instance
|
||||||
|
relationship_type: contains
|
||||||
|
template_id: host_contains_instance
|
||||||
scenarios:
|
scenarios:
|
||||||
- scenario:
|
- scenario:
|
||||||
condition: trigger_alarm_1_on_host
|
condition: trigger_alarm_1_on_host
|
||||||
@ -46,6 +73,9 @@ scenarios:
|
|||||||
target: host
|
target: host
|
||||||
properties:
|
properties:
|
||||||
state: ERROR
|
state: ERROR
|
||||||
|
- scenario:
|
||||||
|
condition: trigger_alarm_4_on_host
|
||||||
|
actions:
|
||||||
- action:
|
- action:
|
||||||
action_type: mark_down
|
action_type: mark_down
|
||||||
action_target:
|
action_target:
|
||||||
@ -68,3 +98,16 @@ scenarios:
|
|||||||
action_target:
|
action_target:
|
||||||
source: trigger_alarm_2
|
source: trigger_alarm_2
|
||||||
target: deduced_alarm
|
target: deduced_alarm
|
||||||
|
- scenario:
|
||||||
|
condition: trigger_alarm_3_on_host and host_contains_instance
|
||||||
|
actions:
|
||||||
|
- action:
|
||||||
|
action_type: set_state
|
||||||
|
action_target:
|
||||||
|
target: instance
|
||||||
|
properties:
|
||||||
|
state: ERROR
|
||||||
|
- action:
|
||||||
|
action_type: mark_down
|
||||||
|
action_target:
|
||||||
|
target: instance
|
||||||
|
@ -0,0 +1,131 @@
|
|||||||
|
metadata:
|
||||||
|
name: e2e_test_overlapping_actions
|
||||||
|
description: this template includes vitrage basic actions
|
||||||
|
definitions:
|
||||||
|
entities:
|
||||||
|
- entity:
|
||||||
|
category: ALARM
|
||||||
|
name: e2e.test_overlapping_actions.trigger.alarm1
|
||||||
|
template_id: trigger_alarm_1
|
||||||
|
- entity:
|
||||||
|
category: ALARM
|
||||||
|
name: e2e.test_overlapping_actions.trigger.alarm2
|
||||||
|
template_id: trigger_alarm_2
|
||||||
|
- entity:
|
||||||
|
category: ALARM
|
||||||
|
name: e2e.test_overlapping_actions.trigger.alarm3
|
||||||
|
template_id: trigger_alarm_3
|
||||||
|
- entity:
|
||||||
|
category: ALARM
|
||||||
|
name: e2e.test_overlapping_actions.trigger.alarm4
|
||||||
|
template_id: trigger_alarm_4
|
||||||
|
- entity:
|
||||||
|
category: ALARM
|
||||||
|
type: vitrage
|
||||||
|
name: e2e.test_overlapping_actions.deduced.alarm
|
||||||
|
template_id: deduced_alarm
|
||||||
|
- entity:
|
||||||
|
category: RESOURCE
|
||||||
|
type: nova.host
|
||||||
|
template_id: host
|
||||||
|
relationships:
|
||||||
|
- relationship:
|
||||||
|
source: trigger_alarm_1
|
||||||
|
relationship_type: on
|
||||||
|
target: host
|
||||||
|
template_id : trigger_alarm_1_on_host
|
||||||
|
- relationship:
|
||||||
|
source: trigger_alarm_2
|
||||||
|
relationship_type: on
|
||||||
|
target: host
|
||||||
|
template_id : trigger_alarm_2_on_host
|
||||||
|
- relationship:
|
||||||
|
source: trigger_alarm_3
|
||||||
|
relationship_type: on
|
||||||
|
target: host
|
||||||
|
template_id : trigger_alarm_3_on_host
|
||||||
|
- relationship:
|
||||||
|
source: trigger_alarm_4
|
||||||
|
relationship_type: on
|
||||||
|
target: host
|
||||||
|
template_id : trigger_alarm_4_on_host
|
||||||
|
- relationship:
|
||||||
|
source: deduced_alarm
|
||||||
|
relationship_type: on
|
||||||
|
target: host
|
||||||
|
template_id : deduced_alarm_on_host
|
||||||
|
scenarios:
|
||||||
|
|
||||||
|
# First part - trigger_alarm_1_on_host or trigger_alarm_3_on_host:
|
||||||
|
|
||||||
|
- scenario:
|
||||||
|
condition: trigger_alarm_1_on_host
|
||||||
|
actions:
|
||||||
|
- action:
|
||||||
|
action_type: set_state
|
||||||
|
action_target:
|
||||||
|
target: host
|
||||||
|
properties:
|
||||||
|
state: ERROR
|
||||||
|
- scenario:
|
||||||
|
condition: trigger_alarm_3_on_host
|
||||||
|
actions:
|
||||||
|
- action:
|
||||||
|
action_type: mark_down
|
||||||
|
action_target:
|
||||||
|
target: host
|
||||||
|
- scenario:
|
||||||
|
condition: trigger_alarm_1_on_host
|
||||||
|
actions:
|
||||||
|
- action:
|
||||||
|
action_type: raise_alarm
|
||||||
|
action_target:
|
||||||
|
target: host
|
||||||
|
properties:
|
||||||
|
alarm_name: e2e.test_overlapping_actions.deduced.alarm
|
||||||
|
severity: WARNING
|
||||||
|
- scenario:
|
||||||
|
condition: trigger_alarm_1_on_host and deduced_alarm_on_host
|
||||||
|
actions:
|
||||||
|
- action:
|
||||||
|
action_type: add_causal_relationship
|
||||||
|
action_target:
|
||||||
|
source: trigger_alarm_1
|
||||||
|
target: deduced_alarm
|
||||||
|
|
||||||
|
# Second part - trigger_alarm_2_on_host or trigger_alarm_4_on_host:
|
||||||
|
|
||||||
|
- scenario:
|
||||||
|
condition: trigger_alarm_2_on_host
|
||||||
|
actions:
|
||||||
|
- action:
|
||||||
|
action_type: set_state
|
||||||
|
action_target:
|
||||||
|
target: host
|
||||||
|
properties:
|
||||||
|
state: ERROR
|
||||||
|
- scenario:
|
||||||
|
condition: trigger_alarm_4_on_host
|
||||||
|
actions:
|
||||||
|
- action:
|
||||||
|
action_type: mark_down
|
||||||
|
action_target:
|
||||||
|
target: host
|
||||||
|
- scenario:
|
||||||
|
condition: trigger_alarm_2_on_host
|
||||||
|
actions:
|
||||||
|
- action:
|
||||||
|
action_type: raise_alarm
|
||||||
|
action_target:
|
||||||
|
target: host
|
||||||
|
properties:
|
||||||
|
alarm_name: e2e.test_overlapping_actions.deduced.alarm
|
||||||
|
severity: WARNING
|
||||||
|
- scenario:
|
||||||
|
condition: trigger_alarm_2_on_host and deduced_alarm_on_host
|
||||||
|
actions:
|
||||||
|
- action:
|
||||||
|
action_type: add_causal_relationship
|
||||||
|
action_target:
|
||||||
|
source: trigger_alarm_2
|
||||||
|
target: deduced_alarm
|
Loading…
Reference in New Issue
Block a user