Merge "Vertex Properties refactoring"
This commit is contained in:
commit
12d9e56bcb
vitrage_tempest_tests/tests/api
@ -72,7 +72,7 @@ class TestAlarms(BaseAlarmsTest):
|
||||
cli_items = cli_alarms.splitlines()
|
||||
|
||||
api_by_type = self._filter_list_by_pairs_parameters(
|
||||
api_alarms, [VProps.TYPE], [resource_type])
|
||||
api_alarms, [VProps.VITRAGE_TYPE], [resource_type])
|
||||
cli_by_type = cli_alarms.count(' ' + resource_type + ' ')
|
||||
|
||||
api_by_id = self._filter_list_by_pairs_parameters(
|
||||
|
@ -121,8 +121,9 @@ class BaseApiTest(base.BaseTestCase):
|
||||
|
||||
def _get_host(self):
|
||||
topology = self.vitrage_client.topology.get(all_tenants=True)
|
||||
host = filter(lambda item: item[VProps.TYPE] == NOVA_HOST_DATASOURCE,
|
||||
topology['nodes'])
|
||||
host = filter(
|
||||
lambda item: item[VProps.VITRAGE_TYPE] == NOVA_HOST_DATASOURCE,
|
||||
topology['nodes'])
|
||||
return host[0]
|
||||
|
||||
def _create_instances(self, num_instances, set_public_network=False):
|
||||
@ -247,29 +248,29 @@ class BaseApiTest(base.BaseTestCase):
|
||||
validation_data = []
|
||||
|
||||
# openstack.cluster
|
||||
props = {VProps.CATEGORY: EntityCategory.RESOURCE,
|
||||
VProps.TYPE: OPENSTACK_CLUSTER,
|
||||
props = {VProps.VITRAGE_CATEGORY: EntityCategory.RESOURCE,
|
||||
VProps.VITRAGE_TYPE: OPENSTACK_CLUSTER,
|
||||
self.NUM_VERTICES_PER_TYPE: kwargs.get('cluster_entities', 1),
|
||||
self.NUM_EDGES_PER_TYPE: kwargs.get('cluster_edges', 1)}
|
||||
validation_data.append(props)
|
||||
|
||||
# nova.zone
|
||||
props = {VProps.CATEGORY: EntityCategory.RESOURCE,
|
||||
VProps.TYPE: NOVA_ZONE_DATASOURCE,
|
||||
props = {VProps.VITRAGE_CATEGORY: EntityCategory.RESOURCE,
|
||||
VProps.VITRAGE_TYPE: NOVA_ZONE_DATASOURCE,
|
||||
self.NUM_VERTICES_PER_TYPE: kwargs.get('zone_entities', 1),
|
||||
self.NUM_EDGES_PER_TYPE: kwargs.get('zone_edges', 2)}
|
||||
validation_data.append(props)
|
||||
|
||||
# nova.host
|
||||
props = {VProps.CATEGORY: EntityCategory.RESOURCE,
|
||||
VProps.TYPE: NOVA_HOST_DATASOURCE,
|
||||
props = {VProps.VITRAGE_CATEGORY: EntityCategory.RESOURCE,
|
||||
VProps.VITRAGE_TYPE: NOVA_HOST_DATASOURCE,
|
||||
self.NUM_VERTICES_PER_TYPE: kwargs.get('host_entities', 1),
|
||||
self.NUM_EDGES_PER_TYPE: kwargs.get('host_edges', 1)}
|
||||
validation_data.append(props)
|
||||
|
||||
# nova.instance
|
||||
props = {VProps.CATEGORY: EntityCategory.RESOURCE,
|
||||
VProps.TYPE: NOVA_INSTANCE_DATASOURCE,
|
||||
props = {VProps.VITRAGE_CATEGORY: EntityCategory.RESOURCE,
|
||||
VProps.VITRAGE_TYPE: NOVA_INSTANCE_DATASOURCE,
|
||||
self.NUM_VERTICES_PER_TYPE: kwargs.get(
|
||||
'instance_entities', 0),
|
||||
self.NUM_EDGES_PER_TYPE: kwargs.get(
|
||||
@ -277,8 +278,8 @@ class BaseApiTest(base.BaseTestCase):
|
||||
validation_data.append(props)
|
||||
|
||||
# cinder.volume
|
||||
props = {VProps.CATEGORY: EntityCategory.RESOURCE,
|
||||
VProps.TYPE: CINDER_VOLUME_DATASOURCE,
|
||||
props = {VProps.VITRAGE_CATEGORY: EntityCategory.RESOURCE,
|
||||
VProps.VITRAGE_TYPE: CINDER_VOLUME_DATASOURCE,
|
||||
self.NUM_VERTICES_PER_TYPE: kwargs.get(
|
||||
'volume_entities', 0),
|
||||
self.NUM_EDGES_PER_TYPE: kwargs.get(
|
||||
@ -286,8 +287,8 @@ class BaseApiTest(base.BaseTestCase):
|
||||
validation_data.append(props)
|
||||
|
||||
# switch
|
||||
props = {VProps.CATEGORY: EntityCategory.RESOURCE,
|
||||
VProps.TYPE: SWITCH,
|
||||
props = {VProps.VITRAGE_CATEGORY: EntityCategory.RESOURCE,
|
||||
VProps.VITRAGE_TYPE: SWITCH,
|
||||
self.NUM_VERTICES_PER_TYPE: kwargs.get(
|
||||
'switch_entities', 0),
|
||||
self.NUM_EDGES_PER_TYPE: kwargs.get(
|
||||
@ -295,8 +296,8 @@ class BaseApiTest(base.BaseTestCase):
|
||||
validation_data.append(props)
|
||||
|
||||
# aodh
|
||||
props = {VProps.CATEGORY: EntityCategory.ALARM,
|
||||
VProps.TYPE: AODH_DATASOURCE,
|
||||
props = {VProps.VITRAGE_CATEGORY: EntityCategory.ALARM,
|
||||
VProps.VITRAGE_TYPE: AODH_DATASOURCE,
|
||||
self.NUM_VERTICES_PER_TYPE: kwargs.get(
|
||||
'aodh_entities', 0),
|
||||
self.NUM_EDGES_PER_TYPE: kwargs.get(
|
||||
@ -305,8 +306,8 @@ class BaseApiTest(base.BaseTestCase):
|
||||
|
||||
# neutron.network
|
||||
if kwargs.get('network_entities') is not None:
|
||||
props = {VProps.CATEGORY: EntityCategory.RESOURCE,
|
||||
VProps.TYPE: NEUTRON_NETWORK_DATASOURCE,
|
||||
props = {VProps.VITRAGE_CATEGORY: EntityCategory.RESOURCE,
|
||||
VProps.VITRAGE_TYPE: NEUTRON_NETWORK_DATASOURCE,
|
||||
self.NUM_VERTICES_PER_TYPE: kwargs.get(
|
||||
'network_entities', 0),
|
||||
self.NUM_EDGES_PER_TYPE: kwargs.get(
|
||||
@ -315,8 +316,8 @@ class BaseApiTest(base.BaseTestCase):
|
||||
|
||||
# neutron.port
|
||||
if kwargs.get('port_entities') is not None:
|
||||
props = {VProps.CATEGORY: EntityCategory.RESOURCE,
|
||||
VProps.TYPE: NEUTRON_PORT_DATASOURCE,
|
||||
props = {VProps.VITRAGE_CATEGORY: EntityCategory.RESOURCE,
|
||||
VProps.VITRAGE_TYPE: NEUTRON_PORT_DATASOURCE,
|
||||
self.NUM_VERTICES_PER_TYPE: kwargs.get(
|
||||
'port_entities', 0),
|
||||
self.NUM_EDGES_PER_TYPE: kwargs.get(
|
||||
@ -324,8 +325,8 @@ class BaseApiTest(base.BaseTestCase):
|
||||
validation_data.append(props)
|
||||
|
||||
# heat.stack
|
||||
props = {VProps.CATEGORY: EntityCategory.RESOURCE,
|
||||
VProps.TYPE: HEAT_STACK_DATASOURCE,
|
||||
props = {VProps.VITRAGE_CATEGORY: EntityCategory.RESOURCE,
|
||||
VProps.VITRAGE_TYPE: HEAT_STACK_DATASOURCE,
|
||||
self.NUM_VERTICES_PER_TYPE: kwargs.get(
|
||||
'stack_entities', 0),
|
||||
self.NUM_EDGES_PER_TYPE: kwargs.get(
|
||||
@ -344,23 +345,23 @@ class BaseApiTest(base.BaseTestCase):
|
||||
|
||||
for entity in entities:
|
||||
query = {
|
||||
VProps.CATEGORY: entity[VProps.CATEGORY],
|
||||
VProps.TYPE: entity[VProps.TYPE],
|
||||
VProps.IS_DELETED: False,
|
||||
VProps.IS_PLACEHOLDER: False
|
||||
VProps.VITRAGE_CATEGORY: entity[VProps.VITRAGE_CATEGORY],
|
||||
VProps.VITRAGE_TYPE: entity[VProps.VITRAGE_TYPE],
|
||||
VProps.VITRAGE_IS_DELETED: False,
|
||||
VProps.VITRAGE_IS_PLACEHOLDER: False
|
||||
}
|
||||
vertices = graph.get_vertices(vertex_attr_filter=query)
|
||||
self.assertEqual(entity[self.NUM_VERTICES_PER_TYPE],
|
||||
len(vertices),
|
||||
'%s%s' % ('Num vertices is incorrect for: ',
|
||||
entity[VProps.TYPE]))
|
||||
entity[VProps.VITRAGE_TYPE]))
|
||||
|
||||
entity_num_edges = sum([len(graph.get_edges(vertex.vertex_id))
|
||||
for vertex in vertices])
|
||||
self.assertEqual(entity[self.NUM_EDGES_PER_TYPE],
|
||||
entity_num_edges,
|
||||
'%s%s' % ('Num edges is incorrect for: ',
|
||||
entity[VProps.TYPE]))
|
||||
entity[VProps.VITRAGE_TYPE]))
|
||||
|
||||
self.assertEqual(num_entities, graph.num_vertices())
|
||||
self.assertEqual(num_edges, graph.num_edges())
|
||||
|
@ -94,12 +94,12 @@ class TestEvents(base.BaseTestCase):
|
||||
return False, api_alarms
|
||||
|
||||
def _check_alarm(self, alarm, event_time, event_type, details):
|
||||
self.assertEqual(EntityCategory.ALARM, alarm[VProps.CATEGORY])
|
||||
self.assertEqual(EntityCategory.ALARM, alarm[VProps.VITRAGE_CATEGORY])
|
||||
self.assertEqual(event_type, alarm[VProps.NAME])
|
||||
self.assertEqual(event_time, alarm[EventProps.TIME])
|
||||
self.assertEqual(details['status'], alarm['status'])
|
||||
self.assertFalse(alarm[VProps.IS_DELETED])
|
||||
self.assertFalse(alarm[VProps.IS_PLACEHOLDER])
|
||||
self.assertFalse(alarm[VProps.VITRAGE_IS_DELETED])
|
||||
self.assertFalse(alarm[VProps.VITRAGE_IS_PLACEHOLDER])
|
||||
|
||||
@staticmethod
|
||||
def _wait_for_status(max_waiting, func, **kwargs):
|
||||
|
@ -25,7 +25,7 @@ from vitrage.entity_graph.mappings.operational_alarm_severity \
|
||||
from vitrage.entity_graph.mappings.operational_resource_state \
|
||||
import OperationalResourceState
|
||||
from vitrage.evaluator.actions.evaluator_event_transformer \
|
||||
import VITRAGE_TYPE
|
||||
import VITRAGE_DATASOURCE
|
||||
from vitrage_tempest_tests.tests.api.alarms.base import BaseAlarmsTest
|
||||
import vitrage_tempest_tests.tests.utils as utils
|
||||
|
||||
@ -50,7 +50,7 @@ class BaseRcaTest(BaseAlarmsTest):
|
||||
|
||||
list_alarms = self.vitrage_client.alarm.list(vitrage_id=None)
|
||||
expected_alarm = self._filter_list_by_pairs_parameters(
|
||||
list_alarms, ['resource_id', VProps.TYPE],
|
||||
list_alarms, ['resource_id', VProps.VITRAGE_TYPE],
|
||||
[resource_id, AODH_DATASOURCE])
|
||||
if not expected_alarm:
|
||||
return None
|
||||
@ -73,12 +73,12 @@ class BaseRcaTest(BaseAlarmsTest):
|
||||
LOG.info("The rca alarms list is : " + str(json.dumps(rca)))
|
||||
|
||||
resource_alarm = self._filter_list_by_pairs_parameters(
|
||||
rca, [VProps.TYPE, VProps.NAME],
|
||||
rca, [VProps.VITRAGE_TYPE, VProps.NAME],
|
||||
[AODH_DATASOURCE, RCA_ALARM_NAME])
|
||||
|
||||
deduce_alarms = self._filter_list_by_pairs_parameters(
|
||||
rca, [VProps.TYPE, VProps.NAME, VProps.SEVERITY],
|
||||
[VITRAGE_TYPE, VITRAGE_ALARM_NAME,
|
||||
rca, [VProps.VITRAGE_TYPE, VProps.NAME, VProps.SEVERITY],
|
||||
[VITRAGE_DATASOURCE, VITRAGE_ALARM_NAME,
|
||||
OperationalAlarmSeverity.WARNING])
|
||||
|
||||
self.assertEqual(len(rca), 3)
|
||||
@ -92,15 +92,15 @@ class BaseRcaTest(BaseAlarmsTest):
|
||||
|
||||
deduce_alarms_1 = self._filter_list_by_pairs_parameters(
|
||||
alarms,
|
||||
[VProps.TYPE, VProps.NAME, 'resource_type', 'resource_id'],
|
||||
[VITRAGE_TYPE, VITRAGE_ALARM_NAME,
|
||||
[VProps.VITRAGE_TYPE, VProps.NAME, 'resource_type', 'resource_id'],
|
||||
[VITRAGE_DATASOURCE, VITRAGE_ALARM_NAME,
|
||||
NOVA_INSTANCE_DATASOURCE,
|
||||
utils.uni2str(instances[0].id)])
|
||||
|
||||
deduce_alarms_2 = self._filter_list_by_pairs_parameters(
|
||||
alarms,
|
||||
[VProps.TYPE, VProps.NAME, 'resource_type', 'resource_id'],
|
||||
[VITRAGE_TYPE, VITRAGE_ALARM_NAME,
|
||||
[VProps.VITRAGE_TYPE, VProps.NAME, 'resource_type', 'resource_id'],
|
||||
[VITRAGE_DATASOURCE, VITRAGE_ALARM_NAME,
|
||||
NOVA_INSTANCE_DATASOURCE,
|
||||
utils.uni2str(instances[1].id)])
|
||||
|
||||
@ -132,8 +132,8 @@ class BaseRcaTest(BaseAlarmsTest):
|
||||
|
||||
host = self._filter_list_by_pairs_parameters(
|
||||
topology,
|
||||
[VProps.TYPE, VProps.ID, VProps.VITRAGE_STATE,
|
||||
VProps.AGGREGATED_STATE],
|
||||
[VProps.VITRAGE_TYPE, VProps.ID, VProps.VITRAGE_STATE,
|
||||
VProps.VITRAGE_AGGREGATED_STATE],
|
||||
[NOVA_HOST_DATASOURCE,
|
||||
self._get_hostname(),
|
||||
OperationalResourceState.ERROR,
|
||||
@ -141,8 +141,8 @@ class BaseRcaTest(BaseAlarmsTest):
|
||||
|
||||
vm1 = self._filter_list_by_pairs_parameters(
|
||||
topology,
|
||||
[VProps.TYPE, VProps.ID, VProps.VITRAGE_STATE,
|
||||
VProps.AGGREGATED_STATE],
|
||||
[VProps.VITRAGE_TYPE, VProps.ID, VProps.VITRAGE_STATE,
|
||||
VProps.VITRAGE_AGGREGATED_STATE],
|
||||
[NOVA_INSTANCE_DATASOURCE,
|
||||
utils.uni2str(instances[0].id),
|
||||
OperationalResourceState.SUBOPTIMAL,
|
||||
@ -150,8 +150,8 @@ class BaseRcaTest(BaseAlarmsTest):
|
||||
|
||||
vm2 = self._filter_list_by_pairs_parameters(
|
||||
topology,
|
||||
[VProps.TYPE, VProps.ID, VProps.VITRAGE_STATE,
|
||||
VProps.AGGREGATED_STATE],
|
||||
[VProps.VITRAGE_TYPE, VProps.ID, VProps.VITRAGE_STATE,
|
||||
VProps.VITRAGE_AGGREGATED_STATE],
|
||||
[NOVA_INSTANCE_DATASOURCE,
|
||||
utils.uni2str(instances[1].id),
|
||||
OperationalResourceState.SUBOPTIMAL,
|
||||
@ -189,7 +189,7 @@ class BaseRcaTest(BaseAlarmsTest):
|
||||
@staticmethod
|
||||
def _clean_timestamps(alist):
|
||||
try:
|
||||
del alist[5][1][0][VProps.SAMPLE_TIMESTAMP]
|
||||
del alist[5][1][0][VProps.VITRAGE_SAMPLE_TIMESTAMP]
|
||||
del alist[5][1][0][VProps.UPDATE_TIMESTAMP]
|
||||
except Exception:
|
||||
pass
|
||||
|
@ -11,11 +11,13 @@
|
||||
# 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 json
|
||||
import traceback
|
||||
|
||||
from oslo_log import log as logging
|
||||
|
||||
from vitrage.common.constants import VertexProperties as VProps
|
||||
from vitrage.datasources import CINDER_VOLUME_DATASOURCE
|
||||
from vitrage.datasources import NOVA_INSTANCE_DATASOURCE
|
||||
from vitrage_tempest_tests.tests.api.base import BaseApiTest
|
||||
@ -28,7 +30,11 @@ LOG = logging.getLogger(__name__)
|
||||
class TestResource(BaseApiTest):
|
||||
"""Test class for Vitrage resource API tests."""
|
||||
|
||||
properties = ('vitrage_id', 'type', 'id', 'state', 'aggregated_state')
|
||||
properties = (VProps.VITRAGE_ID,
|
||||
VProps.VITRAGE_TYPE,
|
||||
VProps.ID,
|
||||
VProps.STATE,
|
||||
VProps.VITRAGE_AGGREGATED_STATE)
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
|
@ -82,47 +82,47 @@ class BaseTopologyTest(BaseApiTest):
|
||||
item.pop(VProps.UPDATE_TIMESTAMP, None)
|
||||
|
||||
for item in sorted_cli_graph[4][1]:
|
||||
item.pop(VProps.SAMPLE_TIMESTAMP, None)
|
||||
item.pop(VProps.VITRAGE_SAMPLE_TIMESTAMP, None)
|
||||
|
||||
for item in sorted_api_graph[4][1]:
|
||||
item.pop(VProps.SAMPLE_TIMESTAMP, None)
|
||||
item.pop(VProps.VITRAGE_SAMPLE_TIMESTAMP, None)
|
||||
|
||||
self.assertEqual(sorted_cli_graph, sorted_api_graph)
|
||||
|
||||
@staticmethod
|
||||
def _graph_query():
|
||||
return '{"and": [{"==": {"category": "RESOURCE"}},' \
|
||||
'{"==": {"is_deleted": false}},' \
|
||||
'{"==": {"is_placeholder": false}},' \
|
||||
'{"or": [{"==": {"type": "openstack.cluster"}},' \
|
||||
'{"==": {"type": "nova.instance"}},' \
|
||||
'{"==": {"type": "nova.host"}},' \
|
||||
'{"==": {"type": "nova.zone"}}]}]}'
|
||||
return '{"and": [{"==": {"vitrage_category": "RESOURCE"}},' \
|
||||
'{"==": {"vitrage_is_deleted": false}},' \
|
||||
'{"==": {"vitrage_is_placeholder": false}},' \
|
||||
'{"or": [{"==": {"vitrage_type": "openstack.cluster"}},' \
|
||||
'{"==": {"vitrage_type": "nova.instance"}},' \
|
||||
'{"==": {"vitrage_type": "nova.host"}},' \
|
||||
'{"==": {"vitrage_type": "nova.zone"}}]}]}'
|
||||
|
||||
@staticmethod
|
||||
def _tree_query():
|
||||
return '{"and": [{"==": {"category": "RESOURCE"}},' \
|
||||
'{"==": {"is_deleted": false}},' \
|
||||
'{"==": {"is_placeholder": false}},' \
|
||||
'{"or": [{"==": {"type": "openstack.cluster"}},' \
|
||||
'{"==": {"type": "nova.host"}},' \
|
||||
'{"==": {"type": "nova.zone"}}]}]}'
|
||||
return '{"and": [{"==": {"vitrage_category": "RESOURCE"}},' \
|
||||
'{"==": {"vitrage_is_deleted": false}},' \
|
||||
'{"==": {"vitrage_is_placeholder": false}},' \
|
||||
'{"or": [{"==": {"vitrage_type": "openstack.cluster"}},' \
|
||||
'{"==": {"vitrage_type": "nova.host"}},' \
|
||||
'{"==": {"vitrage_type": "nova.zone"}}]}]}'
|
||||
|
||||
@staticmethod
|
||||
def _graph_no_match_query():
|
||||
return '{"and": [{"==": {"category": "test"}},' \
|
||||
'{"==": {"is_deleted": false}},' \
|
||||
'{"==": {"is_placeholder": false}},' \
|
||||
'{"or": [{"==": {"type": "openstack.cluster"}},' \
|
||||
'{"==": {"type": "nova.instance"}},' \
|
||||
'{"==": {"type": "nova.host"}},' \
|
||||
'{"==": {"type": "nova.zone"}}]}]}'
|
||||
return '{"and": [{"==": {"vitrage_category": "test"}},' \
|
||||
'{"==": {"vitrage_is_deleted": false}},' \
|
||||
'{"==": {"vitrage_is_placeholder": false}},' \
|
||||
'{"or": [{"==": {"vitrage_type": "openstack.cluster"}},' \
|
||||
'{"==": {"vitrage_type": "nova.instance"}},' \
|
||||
'{"==": {"vitrage_type": "nova.host"}},' \
|
||||
'{"==": {"vitrage_type": "nova.zone"}}]}]}'
|
||||
|
||||
@staticmethod
|
||||
def _tree_no_match_query():
|
||||
return '{"and": [{"==": {"category": "test"}},' \
|
||||
'{"==": {"is_deleted": false}},' \
|
||||
'{"==": {"is_placeholder": false}},' \
|
||||
'{"or": [{"==": {"type": "openstack.cluster"}},' \
|
||||
'{"==": {"type": "nova.host"}},' \
|
||||
'{"==": {"type": "nova.zone"}}]}]}'
|
||||
return '{"and": [{"==": {"vitrage_category": "test"}},' \
|
||||
'{"==": {"vitrage_is_deleted": false}},' \
|
||||
'{"==": {"vitrage_is_placeholder": false}},' \
|
||||
'{"or": [{"==": {"vitrage_type": "openstack.cluster"}},' \
|
||||
'{"==": {"vitrage_type": "nova.host"}},' \
|
||||
'{"==": {"vitrage_type": "nova.zone"}}]}]}'
|
||||
|
@ -24,13 +24,13 @@ import unittest
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
NOVA_QUERY = '{"and": [{"==": {"category": "RESOURCE"}},' \
|
||||
'{"==": {"is_deleted": false}},' \
|
||||
'{"==": {"is_placeholder": false}},' \
|
||||
'{"or": [{"==": {"type": "openstack.cluster"}},' \
|
||||
'{"==": {"type": "nova.instance"}},' \
|
||||
'{"==": {"type": "nova.host"}},' \
|
||||
'{"==": {"type": "nova.zone"}}]}]}'
|
||||
NOVA_QUERY = '{"and": [{"==": {"vitrage_category": "RESOURCE"}},' \
|
||||
'{"==": {"vitrage_is_deleted": false}},' \
|
||||
'{"==": {"vitrage_is_placeholder": false}},' \
|
||||
'{"or": [{"==": {"vitrage_type": "openstack.cluster"}},' \
|
||||
'{"==": {"vitrage_type": "nova.instance"}},' \
|
||||
'{"==": {"vitrage_type": "nova.host"}},' \
|
||||
'{"==": {"vitrage_type": "nova.zone"}}]}]}'
|
||||
CLUSTER_VERTEX_ID = 'RESOURCE:openstack.cluster:OpenStack Cluster'
|
||||
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user