From 4f06e3a51525b0f06fc06cfdc13a446c279376c2 Mon Sep 17 00:00:00 2001 From: Alexey Weyl Date: Wed, 26 Oct 2016 15:30:08 +0300 Subject: [PATCH] Multi tenancy for topology, alarms and rca apis. Change-Id: I2ce82e755d22784df1ddefabef738a27b7c2316f --- doc/source/vitrage-api.rst | 1 + etc/vitrage/policy.json | 19 +- vitrage/api/controllers/v1/alarm.py | 21 +- vitrage/api/controllers/v1/rca.py | 21 +- vitrage/api/controllers/v1/topology.py | 26 +- vitrage/api_handler/apis.py | 283 --------------- vitrage/api_handler/apis/__init__.py | 15 + vitrage/api_handler/apis/alarm.py | 102 ++++++ vitrage/api_handler/apis/base.py | 214 ++++++++++++ vitrage/api_handler/apis/rca.py | 149 ++++++++ vitrage/api_handler/apis/template.py | 122 +++++++ vitrage/api_handler/apis/topology.py | 199 +++++++++++ vitrage/api_handler/service.py | 10 +- vitrage/graph/algo_driver/algorithm.py | 41 +++ .../graph/algo_driver/networkx_algorithm.py | 17 + vitrage/graph/utils.py | 6 +- .../tests/functional/api_handler/__init__.py | 15 + .../tests/functional/api_handler/test_apis.py | 324 ++++++++++++++++++ vitrage/tests/unit/entity_graph/base.py | 20 +- vitrage_tempest_tests/tests/api/base.py | 2 +- .../tests/api/datasources/test_aodh.py | 4 +- .../api/datasources/test_cinder_volume.py | 2 +- .../tests/api/datasources/test_heat_stack.py | 2 +- .../tests/api/datasources/test_neutron.py | 2 +- .../tests/api/datasources/test_nova.py | 2 +- .../api/datasources/test_static_physical.py | 2 +- .../tests/api/rca/test_rca.py | 2 +- .../tests/api/topology/base.py | 10 +- .../tests/api/topology/test_topology.py | 31 +- 29 files changed, 1320 insertions(+), 344 deletions(-) delete mode 100644 vitrage/api_handler/apis.py create mode 100644 vitrage/api_handler/apis/__init__.py create mode 100644 vitrage/api_handler/apis/alarm.py create mode 100644 vitrage/api_handler/apis/base.py create mode 100644 vitrage/api_handler/apis/rca.py create mode 100644 vitrage/api_handler/apis/template.py create mode 100644 vitrage/api_handler/apis/topology.py create mode 100644 vitrage/tests/functional/api_handler/__init__.py create mode 100644 vitrage/tests/functional/api_handler/test_apis.py diff --git a/doc/source/vitrage-api.rst b/doc/source/vitrage-api.rst index 00137f4f0..6457ba0d5 100644 --- a/doc/source/vitrage-api.rst +++ b/doc/source/vitrage-api.rst @@ -135,6 +135,7 @@ Consists of a topology request definition which has the following properties: * depth - (int, optional) the depth of the topology graph. defaults to max depth * graph_type-(string, optional) can be either tree or graph. defaults to graph * query - (string, optional) a json query filter to filter the graph components. defaults to return all the graph +* all_tenants - query expression ================ diff --git a/etc/vitrage/policy.json b/etc/vitrage/policy.json index 7b9c89d29..051c69f40 100644 --- a/etc/vitrage/policy.json +++ b/etc/vitrage/policy.json @@ -1,10 +1,13 @@ { - "get topology": "role:admin", - "get resource": "role:admin", - "list resources": "role:admin", - "list alarms": "role:admin", - "get rca": "role:admin", - "template validate": "role:admin", - "template list": "role:admin", - "template show": "role:admin" + "get topology": "", + "get topology:all_tenants": "role:admin", + "get resource": "", + "list resources": "", + "list alarms": "", + "list alarms:all_tenants": "role:admin", + "get rca": "", + "get rca:all_tenants": "role:admin", + "template validate": "", + "template list": "", + "template show": "" } \ No newline at end of file diff --git a/vitrage/api/controllers/v1/alarm.py b/vitrage/api/controllers/v1/alarm.py index 714612fc4..754139a10 100644 --- a/vitrage/api/controllers/v1/alarm.py +++ b/vitrage/api/controllers/v1/alarm.py @@ -30,13 +30,17 @@ LOG = log.getLogger(__name__) class AlarmsController(RootRestController): @pecan.expose('json') - def index(self, vitrage_id=None): - return self.post(vitrage_id) + def index(self, vitrage_id, all_tenants='0'): + return self.post(vitrage_id, all_tenants) @pecan.expose('json') - def post(self, vitrage_id): - enforce("list alarms", pecan.request.headers, - pecan.request.enforcer, {}) + def post(self, vitrage_id, all_tenants='0'): + if all_tenants == '1': + enforce("list alarms:all_tenants", pecan.request.headers, + pecan.request.enforcer, {}) + else: + enforce("list alarms", pecan.request.headers, + pecan.request.enforcer, {}) LOG.info(_LI('returns list alarms with vitrage id %s') % vitrage_id) @@ -45,16 +49,17 @@ class AlarmsController(RootRestController): if pecan.request.cfg.api.use_mock_file: return self.get_mock_data('alarms.sample.json') else: - return self._get_alarms(vitrage_id) + return self._get_alarms(vitrage_id, all_tenants) except Exception as e: LOG.exception('failed to get alarms %s', e) abort(404, str(e)) @staticmethod - def _get_alarms(vitrage_id=None): + def _get_alarms(vitrage_id=None, all_tenants=0): alarms_json = pecan.request.client.call(pecan.request.context, 'get_alarms', - arg=vitrage_id) + vitrage_id=vitrage_id, + all_tenants=all_tenants) LOG.info(alarms_json) try: diff --git a/vitrage/api/controllers/v1/rca.py b/vitrage/api/controllers/v1/rca.py index eb7c4aec8..210c38bb1 100644 --- a/vitrage/api/controllers/v1/rca.py +++ b/vitrage/api/controllers/v1/rca.py @@ -30,26 +30,31 @@ LOG = log.getLogger(__name__) class RCAController(RootRestController): @pecan.expose('json') - def index(self, alarm_id): - return self.get(alarm_id) + def index(self, alarm_id, all_tenants='0'): + return self.get(alarm_id, all_tenants) @pecan.expose('json') - def get(self, alarm_id): - enforce('get rca', pecan.request.headers, - pecan.request.enforcer, {}) + def get(self, alarm_id, all_tenants='0'): + if all_tenants == '1': + enforce('get rca:all_tenants', pecan.request.headers, + pecan.request.enforcer, {}) + else: + enforce('get rca', pecan.request.headers, + pecan.request.enforcer, {}) LOG.info(_LI('received show rca with alarm id %s') % alarm_id) if pecan.request.cfg.api.use_mock_file: return self.get_mock_data('rca.sample.json') else: - return self.get_rca(alarm_id) + return self.get_rca(alarm_id, all_tenants) @staticmethod - def get_rca(alarm_id): + def get_rca(alarm_id, all_tenants): try: graph_data = pecan.request.client.call(pecan.request.context, 'get_rca', - root=alarm_id) + root=alarm_id, + all_tenants=all_tenants) LOG.info(graph_data) graph = json.loads(graph_data) return graph diff --git a/vitrage/api/controllers/v1/topology.py b/vitrage/api/controllers/v1/topology.py index a55e233e4..ad6ca4eab 100644 --- a/vitrage/api/controllers/v1/topology.py +++ b/vitrage/api/controllers/v1/topology.py @@ -34,9 +34,13 @@ LOG = log.getLogger(__name__) class TopologyController(RootRestController): @pecan.expose('json') - def post(self, depth, graph_type, query, root): - enforce("get topology", pecan.request.headers, - pecan.request.enforcer, {}) + def post(self, depth, graph_type, query, root, all_tenants=0): + if all_tenants: + enforce('get topology:all_tenants', pecan.request.headers, + pecan.request.enforcer, {}) + else: + enforce("get topology", pecan.request.headers, + pecan.request.enforcer, {}) LOG.info(_LI('received get topology: depth->%(depth)s ' 'graph_type->%(graph_type)s root->%(root)s') % @@ -50,18 +54,24 @@ class TopologyController(RootRestController): if pecan.request.cfg.api.use_mock_file: return self.get_mock_data('graph.sample.json', graph_type) else: - return self.get_graph(graph_type, depth, query, root) + return self.get_graph(graph_type, depth, query, root, all_tenants) @staticmethod - def get_graph(graph_type, depth, query, root): - TopologyController._check_input_para(graph_type, depth, query, root) + def get_graph(graph_type, depth, query, root, all_tenants): + TopologyController._check_input_para(graph_type, + depth, + query, + root, + all_tenants) try: graph_data = pecan.request.client.call(pecan.request.context, 'get_topology', graph_type=graph_type, depth=depth, - query=query, root=root) + query=query, + root=root, + all_tenants=all_tenants) LOG.info(graph_data) graph = json.loads(graph_data) if graph_type == 'graph': @@ -80,7 +90,7 @@ class TopologyController(RootRestController): abort(404, str(e)) @staticmethod - def _check_input_para(graph_type, depth, query, root): + def _check_input_para(graph_type, depth, query, root, all_tenants): if graph_type == 'graph' and depth is not None and root is None: LOG.exception("Graph-type 'graph' requires a 'root' with 'depth'") abort(403, "Graph-type 'graph' requires a 'root' with 'depth'") diff --git a/vitrage/api_handler/apis.py b/vitrage/api_handler/apis.py deleted file mode 100644 index affe274ee..000000000 --- a/vitrage/api_handler/apis.py +++ /dev/null @@ -1,283 +0,0 @@ -# Copyright 2016 - 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 json -from oslo_log import log - -from vitrage.common.constants import EdgeLabel -from vitrage.common.constants import EdgeProperties as EProps -from vitrage.common.constants import EntityCategory -from vitrage.common.constants import VertexProperties as VProps -from vitrage.datasources.nova.host import NOVA_HOST_DATASOURCE -from vitrage.datasources.nova.instance import NOVA_INSTANCE_DATASOURCE -from vitrage.datasources.nova.zone import NOVA_ZONE_DATASOURCE -from vitrage.datasources import OPENSTACK_CLUSTER -from vitrage.evaluator.template_fields import TemplateFields -from vitrage.evaluator.template_validation.status_messages import status_msgs -from vitrage.evaluator.template_validation.template_content_validator import \ - content_validation -from vitrage.evaluator.template_validation.template_syntax_validator import \ - syntax_validation -from vitrage.graph import create_algorithm -from vitrage.graph import Direction - -LOG = log.getLogger(__name__) - -# Used for Sunburst to show only specific resources -TREE_TOPOLOGY_QUERY = { - 'and': [ - {'==': {VProps.CATEGORY: EntityCategory.RESOURCE}}, - {'==': {VProps.IS_DELETED: False}}, - {'==': {VProps.IS_PLACEHOLDER: False}}, - { - 'or': [ - {'==': {VProps.TYPE: OPENSTACK_CLUSTER}}, - {'==': {VProps.TYPE: NOVA_INSTANCE_DATASOURCE}}, - {'==': {VProps.TYPE: NOVA_HOST_DATASOURCE}}, - {'==': {VProps.TYPE: NOVA_ZONE_DATASOURCE}} - ] - } - ] -} - -TOPOLOGY_AND_ALARMS_QUERY = { - 'and': [ - {'==': {VProps.IS_DELETED: False}}, - {'==': {VProps.IS_PLACEHOLDER: False}}, - { - 'or': [ - {'==': {VProps.CATEGORY: EntityCategory.ALARM}}, - {'==': {VProps.CATEGORY: EntityCategory.RESOURCE}} - ] - } - ] -} - -RCA_QUERY = { - 'and': [ - {'==': {VProps.CATEGORY: EntityCategory.ALARM}}, - {'==': {VProps.IS_DELETED: False}} - ] -} - -ALARMS_ALL_QUERY = { - 'and': [ - {'==': {VProps.CATEGORY: EntityCategory.ALARM}}, - {'==': {VProps.IS_DELETED: False}} - ] -} - - -class EntityGraphApis(object): - def __init__(self, entity_graph): - self.entity_graph = entity_graph - - def get_alarms(self, ctx, arg): - LOG.debug("EntityGraphApis get_alarms arg:%s", str(arg)) - vitrage_id = arg - if not vitrage_id or vitrage_id == 'all': - items_list = self.entity_graph.get_vertices( - query_dict=ALARMS_ALL_QUERY) - else: - items_list = self.entity_graph.neighbors( - vitrage_id, - vertex_attr_filter={VProps.CATEGORY: EntityCategory.ALARM, - VProps.IS_DELETED: False}) - - # TODO(alexey) this should not be here, but in the transformer - self._add_resource_details_to_alarms(items_list) - - return json.dumps({'alarms': [v.properties for v in items_list]}) - - def get_topology(self, ctx, graph_type, depth, query, root): - - LOG.debug("EntityGraphApis get_topology root:%s", str(root)) - ga = create_algorithm(self.entity_graph) - - if graph_type == 'tree': - if not query: - LOG.error("Graph-type 'tree' requires a filter.") - return {} - graph = ga.graph_query_vertices( - query_dict=query, - root_id=root, - depth=depth) - # By default the graph_type is 'graph' - else: - q = query if query else TOPOLOGY_AND_ALARMS_QUERY - if root: - graph = ga.graph_query_vertices( - query_dict=q, - root_id=root, - depth=depth) - else: - graph = ga.create_graph_from_matching_vertices(query_dict=q) - - alarms = graph.get_vertices(query_dict=ALARMS_ALL_QUERY) - self._add_resource_details_to_alarms(alarms) - graph.update_vertices(alarms) - - return graph.json_output_graph() - - def get_rca(self, ctx, root): - LOG.debug("EntityGraphApis get_rca root:%s", str(root)) - - ga = create_algorithm(self.entity_graph) - found_graph_in = ga.graph_query_vertices( - query_dict=RCA_QUERY, - root_id=root, - direction=Direction.IN) - found_graph_out = ga.graph_query_vertices( - query_dict=RCA_QUERY, - root_id=root, - direction=Direction.OUT) - unified_graph = found_graph_in - unified_graph.union(found_graph_out) - - alarms = unified_graph.get_vertices(query_dict=ALARMS_ALL_QUERY) - self._add_resource_details_to_alarms(alarms) - unified_graph.update_vertices(alarms) - - json_graph = unified_graph.json_output_graph( - inspected_index=self._find_rca_index(unified_graph, root)) - return json_graph - - @staticmethod - def _get_first(lst): - if len(lst) == 1: - return lst[0] - else: - return None - - def _add_resource_details_to_alarms(self, alarms): - for alarm in alarms: - try: - resources = self.entity_graph.neighbors( - v_id=alarm.vertex_id, - edge_attr_filter={EProps.RELATIONSHIP_TYPE: EdgeLabel.ON}, - direction=Direction.OUT) - - resource = self._get_first(resources) - if resource: - alarm["resource_id"] = resource.get(VProps.ID, '') - alarm["resource_type"] = resource.get(VProps.TYPE, '') - else: - alarm["resource_id"] = '' - alarm["resource_type"] = '' - - except ValueError as ve: - LOG.error('Alarm %s\nException %s', alarm, ve) - - @staticmethod - def _find_rca_index(found_graph, root): - for root_index, vertex in enumerate(found_graph._g): - if vertex == root: - return root_index - return 0 - - -class TemplateApis(object): - - FAILED_MSG = 'validation failed' - OK_MSG = 'validation OK' - - def __init__(self, templates): - self.templates = templates - - def get_templates(self, ctx): - LOG.debug("TemplateApis get_templates") - - templates_details = [] - for uuid, template in self.templates.items(): - - template_metadata = template.data[TemplateFields.METADATA] - - templates_details.append({ - 'uuid': str(template.uuid), - 'name': template_metadata[TemplateFields.NAME], - 'status': self._get_template_status(template.result), - 'status details': template.result.comment, - 'date': template.date.strftime('%Y-%m-%dT%H:%M:%SZ') - }) - return json.dumps({'templates_details': templates_details}) - - def show_template(self, ctx, template_uuid): - - LOG.debug("Show template with uuid: $s", str(template_uuid)) - - template = self.templates[template_uuid] - - if template: - return json.dumps(template.data) - else: - return json.dumps({'ERROR': 'Incorrect uuid'}) - - def validate_template(self, ctx, templates): - LOG.debug("TemplateApis validate_template templates:" - "%s", str(templates)) - - results = [] - for template in templates: - - template_def = template[1] - path = template[0] - - syntax_result = syntax_validation(template_def) - if not syntax_result.is_valid: - self._add_result(path, - self.FAILED_MSG, - syntax_result.description, - syntax_result.comment, - syntax_result.status_code, - results) - continue - - content_result = content_validation(template_def) - if not content_result.is_valid: - self._add_result(path, - self.FAILED_MSG, - content_result.description, - content_result.comment, - content_result.status_code, - results) - continue - - self._add_result(path, - self.OK_MSG, - 'Template validation', - status_msgs[0], - 0, - results) - - return json.dumps({'results': results}) - - @staticmethod - def _add_result(template_path, status, description, message, status_code, - results): - - results.append({ - 'file path': template_path, - 'status': status, - 'description': description, - 'message': str(message), - 'status code': status_code - }) - - @staticmethod - def _get_template_status(result): - - if result.is_valid: - return 'pass' - else: - return 'failed' diff --git a/vitrage/api_handler/apis/__init__.py b/vitrage/api_handler/apis/__init__.py new file mode 100644 index 000000000..9ae73d02b --- /dev/null +++ b/vitrage/api_handler/apis/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2016 - Nokia Corporation +# +# 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. + +__author__ = 'stack' diff --git a/vitrage/api_handler/apis/alarm.py b/vitrage/api_handler/apis/alarm.py new file mode 100644 index 000000000..8e3c419df --- /dev/null +++ b/vitrage/api_handler/apis/alarm.py @@ -0,0 +1,102 @@ +# Copyright 2016 - 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 json +from oslo_log import log + +from vitrage.api_handler.apis.base import ALARM_QUERY +from vitrage.api_handler.apis.base import ALARMS_ALL_QUERY +from vitrage.api_handler.apis.base import EntityGraphApisBase +from vitrage.common.constants import EntityCategory +from vitrage.common.constants import VertexProperties as VProps + + +LOG = log.getLogger(__name__) + + +class AlarmApis(EntityGraphApisBase): + + def __init__(self, entity_graph, conf): + self.entity_graph = entity_graph + self.conf = conf + + def get_alarms(self, ctx, vitrage_id, all_tenants): + LOG.debug("AlarmApis get_alarms - vitrage_id: %s, all_tenants=%s", + str(vitrage_id), all_tenants) + + project_id = ctx.get(self.TENANT_PROPERTY, None) + is_admin_project = ctx.get(self.IS_ADMIN_PROJECT_PROPERTY, False) + + if not vitrage_id or vitrage_id == 'all': + if all_tenants == "1": + alarms = self.entity_graph.get_vertices( + query_dict=ALARMS_ALL_QUERY) + else: + alarms = self._get_alarms(project_id, is_admin_project) + alarms += self._get_alarms_via_resource(project_id, + is_admin_project) + alarms = set(alarms) + else: + alarms = self.entity_graph.neighbors( + vitrage_id, + vertex_attr_filter={VProps.CATEGORY: EntityCategory.ALARM, + VProps.IS_DELETED: False}) + + self._add_resource_details_to_alarms(alarms) + + return json.dumps({'alarms': [v.properties for v in alarms]}) + + def _get_alarms(self, project_id, is_admin_project): + """Finds all the alarms with project_id + + Finds all the alarms which has the project_id. In case the tenant is + admin then project_id can also be None. + + :type project_id: string + :type is_admin_project: boolean + :rtype: list + """ + + alarm_query = self._get_query_with_project(EntityCategory.ALARM, + project_id, + is_admin_project) + alarms = self.entity_graph.get_vertices(query_dict=alarm_query) + return self._filter_alarms(alarms, project_id) + + def _get_alarms_via_resource(self, project_id, is_admin_project): + """Finds all the alarms with project_id on their resource + + Finds all the resource which has project_id and return all the alarms + on those resources project_id. In case the tenant is admin then + project_id can also be None. + + :type project_id: string + :type is_admin_project: boolean + :rtype: list + """ + + resource_query = self._get_query_with_project(EntityCategory.RESOURCE, + project_id, + is_admin_project) + + alarms = [] + resources = self.entity_graph.get_vertices(query_dict=resource_query) + + for resource in resources: + new_alarms = \ + self.entity_graph.neighbors( + resource.vertex_id, vertex_attr_filter=ALARM_QUERY) + alarms = alarms + new_alarms + + return alarms diff --git a/vitrage/api_handler/apis/base.py b/vitrage/api_handler/apis/base.py new file mode 100644 index 000000000..6281415b3 --- /dev/null +++ b/vitrage/api_handler/apis/base.py @@ -0,0 +1,214 @@ +# Copyright 2016 - 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. + +from oslo_log import log + +from vitrage.common.constants import EdgeLabel +from vitrage.common.constants import EdgeProperties as EProps +from vitrage.common.constants import EntityCategory +from vitrage.common.constants import VertexProperties as VProps +from vitrage.datasources.nova.host import NOVA_HOST_DATASOURCE +from vitrage.datasources.nova.instance import NOVA_INSTANCE_DATASOURCE +from vitrage.datasources.nova.zone import NOVA_ZONE_DATASOURCE +from vitrage.datasources import OPENSTACK_CLUSTER +from vitrage.graph import Direction +from vitrage.keystone_client import get_client as ks_client + +LOG = log.getLogger(__name__) + + +# Used for Sunburst to show only specific resources +TREE_TOPOLOGY_QUERY = { + 'and': [ + {'==': {VProps.CATEGORY: EntityCategory.RESOURCE}}, + {'==': {VProps.IS_DELETED: False}}, + {'==': {VProps.IS_PLACEHOLDER: False}}, + { + 'or': [ + {'==': {VProps.TYPE: OPENSTACK_CLUSTER}}, + {'==': {VProps.TYPE: NOVA_INSTANCE_DATASOURCE}}, + {'==': {VProps.TYPE: NOVA_HOST_DATASOURCE}}, + {'==': {VProps.TYPE: NOVA_ZONE_DATASOURCE}} + ] + } + ] +} + +TOPOLOGY_AND_ALARMS_QUERY = { + 'and': [ + {'==': {VProps.IS_DELETED: False}}, + {'==': {VProps.IS_PLACEHOLDER: False}}, + { + 'or': [ + {'==': {VProps.CATEGORY: EntityCategory.ALARM}}, + {'==': {VProps.CATEGORY: EntityCategory.RESOURCE}} + ] + } + ] +} + +RCA_QUERY = { + 'and': [ + {'==': {VProps.CATEGORY: EntityCategory.ALARM}}, + {'==': {VProps.IS_DELETED: False}} + ] +} + +ALARMS_ALL_QUERY = { + 'and': [ + {'==': {VProps.CATEGORY: EntityCategory.ALARM}}, + {'==': {VProps.IS_DELETED: False}} + ] +} + +ALARM_QUERY = { + VProps.CATEGORY: EntityCategory.ALARM, + VProps.IS_DELETED: False, + VProps.IS_PLACEHOLDER: False +} + + +class EntityGraphApisBase(object): + TENANT_PROPERTY = 'tenant' + IS_ADMIN_PROJECT_PROPERTY = 'is_admin' + + @staticmethod + def _get_query_with_project(category, project_id, is_admin): + """Generate query with tenant data + + Creates query for entity graph which takes into consideration the + category, project_id and if the tenant is admin + + :type category: string + :type project_id: string + :type is_admin: boolean + :rtype: dictionary + """ + + query = { + 'and': [ + {'==': {VProps.IS_DELETED: False}}, + {'==': {VProps.IS_PLACEHOLDER: False}}, + {'==': {VProps.CATEGORY: category}} + ] + } + + if is_admin: + project_query = \ + {'or': [{'==': {VProps.PROJECT_ID: project_id}}, + {'==': {VProps.PROJECT_ID: None}}]} + else: + project_query = \ + {'==': {VProps.PROJECT_ID: project_id}} + + query['and'].append(project_query) + + return query + + def _filter_alarms(self, alarms, project_id): + """Remove wrong alarms from the list + + Removes alarms where the project_id of the resource they sit on is + different than the project_id sent as a parameter + + :type alarms: list + :type project_id: string + :rtype: list + """ + + alarms_to_remove = [] + + for alarm in alarms: + alarm_project_id = alarm.get(VProps.PROJECT_ID, None) + if not alarm_project_id: + cat_filter = {VProps.CATEGORY: EntityCategory.RESOURCE} + alarms_resource = \ + self.entity_graph.neighbors(alarm.vertex_id, + vertex_attr_filter=cat_filter) + if len(alarms_resource) > 0: + resource_project_id = \ + alarms_resource[0].get(VProps.PROJECT_ID, None) + if resource_project_id and \ + resource_project_id != project_id: + alarms_to_remove.append(alarm) + elif alarm_project_id != project_id: + alarms_to_remove.append(alarm) + + return [x for x in alarms if x not in alarms_to_remove] + + def _is_alarm_of_current_project(self, + entity, + project_id, + is_admin_project): + """Checks if the alarm is of the current tenant + + Checks: + 1. checks if the project_id is the same + 2. if the tenant is admin then the projectid can be also None + 3. check the project_id of the resource where the alarm sits is the + same as the project_id sent as a parameter + + :type entity: vertex + :type project_id: string + :type is_admin_project: boolean + :rtype: boolean + """ + + current_project_id = entity.get(VProps.PROJECT_ID, None) + if current_project_id == project_id: + return True + elif not current_project_id and is_admin_project: + return True + else: + entities = self.entity_graph.neighbors(entity.vertex_id, + direction=Direction.OUT) + for entity in entities: + if entity[VProps.CATEGORY] == EntityCategory.RESOURCE: + resource_project_id = entity.get(VProps.PROJECT_ID) + if resource_project_id == project_id or \ + (not resource_project_id and is_admin_project): + return True + return False + return False + + @staticmethod + def _get_first(lst): + if len(lst) == 1: + return lst[0] + else: + return None + + def _add_resource_details_to_alarms(self, alarms): + for alarm in alarms: + try: + resources = self.entity_graph.neighbors( + v_id=alarm.vertex_id, + edge_attr_filter={EProps.RELATIONSHIP_TYPE: EdgeLabel.ON}, + direction=Direction.OUT) + + resource = self._get_first(resources) + if resource: + alarm["resource_id"] = resource.get(VProps.ID, '') + alarm["resource_type"] = resource.get(VProps.TYPE, '') + else: + alarm["resource_id"] = '' + alarm["resource_type"] = '' + + except ValueError as ve: + LOG.error('Alarm %s\nException %s', alarm, ve) + + def _is_project_admin(self, project_id): + keystone_client = ks_client(self.conf) + project = keystone_client.projects.get(project_id) + return 'name=admin' in project.to_dict() diff --git a/vitrage/api_handler/apis/rca.py b/vitrage/api_handler/apis/rca.py new file mode 100644 index 000000000..518c49ec2 --- /dev/null +++ b/vitrage/api_handler/apis/rca.py @@ -0,0 +1,149 @@ +# Copyright 2016 - 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. + +from oslo_log import log + +from vitrage.api_handler.apis.base import ALARMS_ALL_QUERY +from vitrage.api_handler.apis.base import EntityGraphApisBase +from vitrage.api_handler.apis.base import RCA_QUERY +from vitrage.graph import create_algorithm +from vitrage.graph import Direction + + +LOG = log.getLogger(__name__) + + +class RcaApis(EntityGraphApisBase): + + def __init__(self, entity_graph, conf): + self.entity_graph = entity_graph + self.conf = conf + + def get_rca(self, ctx, root, all_tenants): + LOG.debug("RcaApis get_rca - root: %s, all_tenants=%s", + str(root), all_tenants) + + project_id = ctx.get(self.TENANT_PROPERTY, None) + is_admin_project = ctx.get(self.IS_ADMIN_PROJECT_PROPERTY, False) + ga = create_algorithm(self.entity_graph) + + found_graph_out = ga.graph_query_vertices( + query_dict=RCA_QUERY, + root_id=root, + direction=Direction.OUT) + found_graph_in = ga.graph_query_vertices( + query_dict=RCA_QUERY, + root_id=root, + direction=Direction.IN) + + if all_tenants == '1': + unified_graph = found_graph_in + unified_graph.union(found_graph_out) + else: + unified_graph = \ + self._get_rca_for_specific_project(ga, + found_graph_in, + found_graph_out, + root, + project_id, + is_admin_project) + + alarms = unified_graph.get_vertices(query_dict=ALARMS_ALL_QUERY) + self._add_resource_details_to_alarms(alarms) + unified_graph.update_vertices(alarms) + + json_graph = unified_graph.json_output_graph( + inspected_index=self._find_rca_index(unified_graph, root)) + + return json_graph + + def _get_rca_for_specific_project(self, + ga, + found_graph_in, + found_graph_out, + root, + project_id, + is_admin_project): + """Filter the RCA for root entity with consideration of project_id + + Filter the RCA for root by: + 1. filter the alarms deduced from the root alarm (found_graph_in) + 2. filter the alarms caused the root alarm (found_graph_out) + And in the end unify 1 and 2 + + :type ga: NXAlgorithm + :type found_graph_in: NXGraph + :type found_graph_out: NXGraph + :type root: string + :type project_id: string + :type is_admin_project: boolean + :rtype: NXGraph + """ + + filtered_alarms_out = \ + self._filter_alarms(found_graph_out.get_vertices(), project_id) + filtered_found_graph_out = ga.subgraph( + [node.vertex_id for node in filtered_alarms_out]) + filtered_found_graph_in = \ + self._filter_rca_causing_entities(ga, + found_graph_in, + root, + project_id, + is_admin_project) + filtered_found_graph_out.union(filtered_found_graph_in) + + return filtered_found_graph_out + + def _filter_rca_causing_entities(self, + ga, + rca_graph, + root_id, + project_id, + is_admin_project): + """Filter the RCA entities which caused this alarm + + Shows only the causing alarms which has the same project_id and also + the first alarm that has a different project_id. In case the tenant is + admin then project_id can also be None. + + :type ga: NXAlgorithm + :type rca_graph: NXGraph + :type root_id: string + :type project_id: string + :type is_admin_project: boolean + :rtype: NXGraph + """ + + entities = [root_id] + current_entity_id = root_id + + while len(rca_graph.neighbors(current_entity_id, + direction=Direction.IN)) > 0: + current_entity = rca_graph.neighbors(current_entity_id, + direction=Direction.IN)[0] + current_entity_id = current_entity.vertex_id + entities.append(current_entity.vertex_id) + if not self._is_alarm_of_current_project(current_entity, + project_id, + is_admin_project): + break + + return ga.subgraph(entities) + + @staticmethod + def _find_rca_index(found_graph, root): + for root_index, vertex in enumerate(found_graph._g): + if vertex == root: + return root_index + return 0 diff --git a/vitrage/api_handler/apis/template.py b/vitrage/api_handler/apis/template.py new file mode 100644 index 000000000..54a455d4d --- /dev/null +++ b/vitrage/api_handler/apis/template.py @@ -0,0 +1,122 @@ +# Copyright 2016 - 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 json +from oslo_log import log + +from vitrage.evaluator.template_fields import TemplateFields +from vitrage.evaluator.template_validation.status_messages import status_msgs +from vitrage.evaluator.template_validation.template_content_validator import \ + content_validation +from vitrage.evaluator.template_validation.template_syntax_validator import \ + syntax_validation + + +LOG = log.getLogger(__name__) + + +class TemplateApis(object): + + FAILED_MSG = 'validation failed' + OK_MSG = 'validation OK' + + def __init__(self, templates): + self.templates = templates + + def get_templates(self, ctx): + LOG.debug("TemplateApis get_templates") + + templates_details = [] + for uuid, template in self.templates.items(): + + template_metadata = template.data[TemplateFields.METADATA] + + templates_details.append({ + 'uuid': str(template.uuid), + 'name': template_metadata[TemplateFields.NAME], + 'status': self._get_template_status(template.result), + 'status details': template.result.comment, + 'date': template.date.strftime('%Y-%m-%dT%H:%M:%SZ') + }) + return json.dumps({'templates_details': templates_details}) + + def show_template(self, ctx, template_uuid): + + LOG.debug("Show template with uuid: $s", str(template_uuid)) + + template = self.templates[template_uuid] + + if template: + return json.dumps(template.data) + else: + return json.dumps({'ERROR': 'Incorrect uuid'}) + + def validate_template(self, ctx, templates): + LOG.debug("TemplateApis validate_template templates:" + "%s", str(templates)) + + results = [] + for template in templates: + + template_def = template[1] + path = template[0] + + syntax_result = syntax_validation(template_def) + if not syntax_result.is_valid: + self._add_result(path, + self.FAILED_MSG, + syntax_result.description, + syntax_result.comment, + syntax_result.status_code, + results) + continue + + content_result = content_validation(template_def) + if not content_result.is_valid: + self._add_result(path, + self.FAILED_MSG, + content_result.description, + content_result.comment, + content_result.status_code, + results) + continue + + self._add_result(path, + self.OK_MSG, + 'Template validation', + status_msgs[0], + 0, + results) + + return json.dumps({'results': results}) + + @staticmethod + def _add_result(template_path, status, description, message, status_code, + results): + + results.append({ + 'file path': template_path, + 'status': status, + 'description': description, + 'message': str(message), + 'status code': status_code + }) + + @staticmethod + def _get_template_status(result): + + if result.is_valid: + return 'pass' + else: + return 'failed' diff --git a/vitrage/api_handler/apis/topology.py b/vitrage/api_handler/apis/topology.py new file mode 100644 index 000000000..647697b95 --- /dev/null +++ b/vitrage/api_handler/apis/topology.py @@ -0,0 +1,199 @@ +# Copyright 2016 - 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. + +from oslo_log import log + +from vitrage.api_handler.apis.base import ALARMS_ALL_QUERY +from vitrage.api_handler.apis.base import EntityGraphApisBase +from vitrage.api_handler.apis.base import TOPOLOGY_AND_ALARMS_QUERY +from vitrage.common.constants import EntityCategory +from vitrage.common.constants import VertexProperties as VProps +from vitrage.datasources.nova.instance import NOVA_INSTANCE_DATASOURCE +from vitrage.datasources import OPENSTACK_CLUSTER +from vitrage.datasources.transformer_base import build_key +from vitrage.graph import create_algorithm + + +LOG = log.getLogger(__name__) + + +class TopologyApis(EntityGraphApisBase): + + def __init__(self, entity_graph, conf): + self.entity_graph = entity_graph + self.conf = conf + + def get_topology(self, ctx, graph_type, depth, query, root, all_tenants): + LOG.debug("TopologyApis get_topology - root: %s, all_tenants=%s", + str(root), all_tenants) + + project_id = ctx.get(self.TENANT_PROPERTY, None) + is_admin_project = ctx.get(self.IS_ADMIN_PROJECT_PROPERTY, False) + ga = create_algorithm(self.entity_graph) + + if graph_type == 'tree': + if not query: + LOG.error("Graph-type 'tree' requires a filter.") + return {} + + current_query = query + if not all_tenants: + project_query = \ + {'or': [{'==': {VProps.PROJECT_ID: project_id}}, + {'==': {VProps.PROJECT_ID: None}}]} + current_query = {'and': [query, project_query]} + + graph = ga.graph_query_vertices( + query_dict=current_query, + root_id=root, + depth=depth) + # By default the graph_type is 'graph' + else: + if all_tenants: + q = query if query else TOPOLOGY_AND_ALARMS_QUERY + graph = \ + ga.create_graph_from_matching_vertices(query_dict=q) + else: + graph = \ + self._get_topology_for_specific_project( + ga, + query, + project_id, + is_admin_project, + root) + + alarms = graph.get_vertices(query_dict=ALARMS_ALL_QUERY) + self._add_resource_details_to_alarms(alarms) + graph.update_vertices(alarms) + + return graph.json_output_graph() + + def _get_topology_for_specific_project(self, + ga, + query, + project_id, + is_admin_project, + root): + """Finds the topology in consideration with the project_id + + Finds all the entities which has project_id. In case the tenant is + admin then project_id can also be None. + + :type ga: NXAlgorithm + :type query: dictionary + :type project_id: string + :type is_admin_project: boolean + :type root: string + :rtype: NXGraph + """ + + if query: + q = query + else: + alarm_query = self._get_query_with_project(EntityCategory.ALARM, + project_id, + is_admin=True) + + resource_query = \ + self._get_query_with_project(EntityCategory.RESOURCE, + project_id, + is_admin_project) + + default_query = {'or': [resource_query, alarm_query]} + q = default_query + + tmp_graph = ga.create_graph_from_matching_vertices(query_dict=q) + graph = ga.subgraph(self._topology_for_unrooted_graph(ga, + tmp_graph, + root)) + self._remove_alarms_of_other_projects(graph, + project_id, + is_admin_project) + + return graph + + def _remove_alarms_of_other_projects(self, + graph, + current_project_id, + is_admin_project): + """Removes wrong alarms from the graph + + Removes alarms of other tenants from the graph, In case the tenant is + admin then project_id can also be None. + + :type graph: NXGraph + :type current_project_id: string + :type is_admin_project: boolean + """ + + for alarm in graph.get_vertices(query_dict=ALARMS_ALL_QUERY): + if not alarm.get(VProps.PROJECT_ID, None): + cat_filter = {VProps.CATEGORY: EntityCategory.RESOURCE} + resource_neighbors = \ + self.entity_graph.neighbors(alarm.vertex_id, + vertex_attr_filter=cat_filter) + if len(resource_neighbors) > 0: + resource_proj_id = \ + resource_neighbors[0].get(VProps.PROJECT_ID, None) + cond1 = is_admin_project and resource_proj_id and \ + resource_proj_id != current_project_id + cond2 = not is_admin_project and \ + (not resource_proj_id or + resource_proj_id != current_project_id) + if cond1 or cond2: + graph.remove_vertex(alarm) + + def _topology_for_unrooted_graph(self, ga, subgraph, root): + """Finds topology for unrooted subgraph + + 1. Finds all the connected component subgraphs in subgraph. + 2. For each component, finds the path from one of the VMs (if exists) + to the root entity. + 3. Unify all the entities found and return them + + :type ga: NXAlgorithm + :type subgraph: networkx graph + :type root: string + :rtype: list + """ + + entities = [] + + if not root: + root = build_key([EntityCategory.RESOURCE, OPENSTACK_CLUSTER]) + + root_vertex = \ + self.entity_graph.get_vertex(root) + local_connected_component_subgraphs = \ + ga.connected_component_subgraphs(subgraph) + + for component_subgraph in local_connected_component_subgraphs: + entities += component_subgraph.nodes() + instance_in_component_subgraph = \ + self._find_instance_in_graph(component_subgraph) + if instance_in_component_subgraph: + paths = ga.all_simple_paths(root_vertex.vertex_id, + instance_in_component_subgraph) + for path in paths: + entities += path + + return set(entities) + + @staticmethod + def _find_instance_in_graph(graph): + for node, node_data in graph.nodes_iter(data=True): + if node_data[VProps.CATEGORY] == EntityCategory.RESOURCE and \ + node_data[VProps.TYPE] == NOVA_INSTANCE_DATASOURCE: + return node + return None diff --git a/vitrage/api_handler/service.py b/vitrage/api_handler/service.py index 45de82859..970e360b1 100644 --- a/vitrage/api_handler/service.py +++ b/vitrage/api_handler/service.py @@ -17,8 +17,10 @@ from oslo_log import log import oslo_messaging from oslo_service import service as os_service -from vitrage.api_handler.apis import EntityGraphApis -from vitrage.api_handler.apis import TemplateApis +from vitrage.api_handler.apis.alarm import AlarmApis +from vitrage.api_handler.apis.rca import RcaApis +from vitrage.api_handler.apis.template import TemplateApis +from vitrage.api_handler.apis.topology import TopologyApis from vitrage import messaging from vitrage import rpc as vitrage_rpc @@ -45,7 +47,9 @@ class VitrageApiHandlerService(os_service.Service): target = oslo_messaging.Target(topic=self.conf.rpc_topic, server=rabbit_hosts) - endpoints = [EntityGraphApis(self.entity_graph), + endpoints = [TopologyApis(self.entity_graph, self.conf), + AlarmApis(self.entity_graph, self.conf), + RcaApis(self.entity_graph, self.conf), TemplateApis(self.scenario_repo.templates)] server = vitrage_rpc.get_server(target, endpoints, transport) diff --git a/vitrage/graph/algo_driver/algorithm.py b/vitrage/graph/algo_driver/algorithm.py index a7a0c6c3c..c5e8adfec 100644 --- a/vitrage/graph/algo_driver/algorithm.py +++ b/vitrage/graph/algo_driver/algorithm.py @@ -58,8 +58,49 @@ class GraphAlgorithm(object): """ pass + @abc.abstractmethod + def subgraph(self, entities): + """Return the subgraph induced on nodes in entities. + + The induced subgraph of the graph contains the nodes in entities and + the edges between those nodes. + + :type entities: list + :rtype: NXGraph + """ + pass + + @staticmethod + def connected_component_subgraphs(subgraph): + """Generate connected components as subgraphs. + + :type subgraph: NetworkX graph. + :rtype: list of NXGraphs + """ + pass + + def all_simple_paths(self, source, target): + """Generate all simple paths in the graph G from source to target. + + A simple path is a path with no repeated nodes. + + :type source: Starting node for path + :type target: Ending node for path + :rtype: lists of simple paths + """ + pass + @abc.abstractmethod def create_graph_from_matching_vertices(self, vertex_attr_filter=None, query_dict=None): + """Generate graph using the query + + Finds all the vertices in the graph matching the query, and returns + a subgraph consisted from the vertices + + :type vertex_attr_filter: dictionary + :type query_dict: dictionary + :rtype: NXGraph + """ pass diff --git a/vitrage/graph/algo_driver/networkx_algorithm.py b/vitrage/graph/algo_driver/networkx_algorithm.py index d1d85012a..9eb5ce5df 100644 --- a/vitrage/graph/algo_driver/networkx_algorithm.py +++ b/vitrage/graph/algo_driver/networkx_algorithm.py @@ -12,6 +12,9 @@ # License for the specific language governing permissions and limitations # under the License. +from networkx.algorithms import components +from networkx.algorithms import simple_paths + from oslo_log import log as logging from vitrage.graph.algo_driver.algorithm import GraphAlgorithm @@ -102,3 +105,17 @@ class NXAlgorithm(GraphAlgorithm): str(self.graph._g.nodes(data=True)), str(self.graph._g.edges(data=True))) return graph + + def subgraph(self, entities): + subgraph = NXGraph('graph') + subgraph._g = self.graph._g.subgraph(entities) + return subgraph + + def connected_component_subgraphs(self, subgraph): + return components.connected_component_subgraphs( + subgraph._g.to_undirected(), copy=False) + + def all_simple_paths(self, source, target): + return simple_paths.all_simple_paths(self.graph._g, + source=source, + target=target) diff --git a/vitrage/graph/utils.py b/vitrage/graph/utils.py index 31d80cbdb..18871cf3c 100644 --- a/vitrage/graph/utils.py +++ b/vitrage/graph/utils.py @@ -27,6 +27,7 @@ def create_vertex(vitrage_id, sample_timestamp=None, update_timestamp=None, is_placeholder=False, + project_id=None, metadata=None): """A builder to create a vertex @@ -50,6 +51,8 @@ def create_vertex(vitrage_id, :type metadata: dict :param is_placeholder: :type is_placeholder: boolean + :param project_id: + :type project_id: str :return: :rtype: Vertex """ @@ -63,7 +66,8 @@ def create_vertex(vitrage_id, VConst.UPDATE_TIMESTAMP: update_timestamp, VConst.SAMPLE_TIMESTAMP: sample_timestamp, VConst.IS_PLACEHOLDER: is_placeholder, - VConst.VITRAGE_ID: vitrage_id + VConst.VITRAGE_ID: vitrage_id, + VConst.PROJECT_ID: project_id } if metadata: properties.update(metadata) diff --git a/vitrage/tests/functional/api_handler/__init__.py b/vitrage/tests/functional/api_handler/__init__.py new file mode 100644 index 000000000..dd32b852f --- /dev/null +++ b/vitrage/tests/functional/api_handler/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2016 - 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. + +__author__ = 'stack' diff --git a/vitrage/tests/functional/api_handler/test_apis.py b/vitrage/tests/functional/api_handler/test_apis.py new file mode 100644 index 000000000..d783265b9 --- /dev/null +++ b/vitrage/tests/functional/api_handler/test_apis.py @@ -0,0 +1,324 @@ +# Copyright 2016 - 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 json + +from vitrage.api_handler.apis.alarm import AlarmApis +from vitrage.api_handler.apis.rca import RcaApis +from vitrage.api_handler.apis.topology import TopologyApis +from vitrage.common.constants import EntityCategory +from vitrage.common.constants import VertexProperties as VProps +from vitrage.datasources import NOVA_HOST_DATASOURCE +from vitrage.datasources import NOVA_INSTANCE_DATASOURCE +from vitrage.datasources import NOVA_ZONE_DATASOURCE +from vitrage.datasources import OPENSTACK_CLUSTER +from vitrage.graph import NXGraph +import vitrage.graph.utils as graph_utils +from vitrage.tests.unit.entity_graph.base import TestEntityGraphUnitBase + + +class TestApis(TestEntityGraphUnitBase): + + def test_get_alarms_with_admin_project(self): + # Setup + graph = self._create_graph() + apis = AlarmApis(graph, None) + ctx = {'tenant': 'project_1', 'is_admin': True} + + # Action + alarms = apis.get_alarms(ctx, vitrage_id='all', all_tenants='0') + alarms = json.loads(alarms)['alarms'] + + # Test assertions + self.assertEqual(3, len(alarms)) + self._check_projects_entities(alarms, 'project_1', True) + + def test_get_alarms_with_not_admin_project(self): + # Setup + graph = self._create_graph() + apis = AlarmApis(graph, None) + ctx = {'tenant': 'project_2', 'is_admin': False} + + # Action + alarms = apis.get_alarms(ctx, vitrage_id='all', all_tenants='0') + alarms = json.loads(alarms)['alarms'] + + # Test assertions + self.assertEqual(2, len(alarms)) + self._check_projects_entities(alarms, 'project_2', True) + + def test_get_alarms_with_all_tenants(self): + # Setup + graph = self._create_graph() + apis = AlarmApis(graph, None) + ctx = {'tenant': 'project_1', 'is_admin': False} + + # Action + alarms = apis.get_alarms(ctx, vitrage_id='all', all_tenants='1') + alarms = json.loads(alarms)['alarms'] + + # Test assertions + self.assertEqual(5, len(alarms)) + self._check_projects_entities(alarms, None, True) + + def test_get_rca_with_admin_project(self): + # Setup + graph = self._create_graph() + apis = RcaApis(graph, None) + ctx = {'tenant': 'project_1', 'is_admin': True} + + # Action + graph_rca = apis.get_rca(ctx, root='alarm_on_host', all_tenants='0') + graph_rca = json.loads(graph_rca) + + # Test assertions + self.assertEqual(3, len(graph_rca['nodes'])) + self._check_projects_entities(graph_rca['nodes'], 'project_1', True) + + def test_get_rca_with_not_admin_project(self): + # Setup + graph = self._create_graph() + apis = RcaApis(graph, None) + ctx = {'tenant': 'project_2', 'is_admin': False} + + # Action + graph_rca = apis.get_rca(ctx, + root='alarm_on_instance_3', + all_tenants='0') + graph_rca = json.loads(graph_rca) + + # Test assertions + self.assertEqual(2, len(graph_rca['nodes'])) + self._check_projects_entities(graph_rca['nodes'], 'project_2', True) + + def test_get_rca_with_not_admin_bla_project(self): + # Setup + graph = self._create_graph() + apis = RcaApis(graph, None) + ctx = {'tenant': 'project_2', 'is_admin': False} + + # Action + graph_rca = apis.get_rca(ctx, root='alarm_on_host', all_tenants='0') + graph_rca = json.loads(graph_rca) + + # Test assertions + self.assertEqual(3, len(graph_rca['nodes'])) + self._check_projects_entities(graph_rca['nodes'], 'project_2', True) + + def test_get_rca_with_all_tenants(self): + # Setup + graph = self._create_graph() + apis = RcaApis(graph, None) + ctx = {'tenant': 'project_1', 'is_admin': False} + + # Action + graph_rca = apis.get_rca(ctx, root='alarm_on_host', all_tenants='1') + graph_rca = json.loads(graph_rca) + + # Test assertions + self.assertEqual(5, len(graph_rca['nodes'])) + self._check_projects_entities(graph_rca['nodes'], None, True) + + def test_get_topology_with_admin_project(self): + # Setup + graph = self._create_graph() + apis = TopologyApis(graph, None) + ctx = {'tenant': 'project_1', 'is_admin': True} + + # Action + graph_topology = apis.get_topology(ctx, + graph_type='graph', + depth=10, + query=None, + root='RESOURCE:openstack.cluster', + all_tenants=0) + graph_topology = json.loads(graph_topology) + + # Test assertions + self.assertEqual(8, len(graph_topology['nodes'])) + self._check_projects_entities(graph_topology['nodes'], + 'project_1', + False) + + def test_get_topology_with_not_admin_project(self): + # Setup + graph = self._create_graph() + apis = TopologyApis(graph, None) + ctx = {'tenant': 'project_2', 'is_admin': False} + + # Action + graph_topology = apis.get_topology(ctx, + graph_type='graph', + depth=10, + query=None, + root='RESOURCE:openstack.cluster', + all_tenants=0) + graph_topology = json.loads(graph_topology) + + # Test assertions + self.assertEqual(7, len(graph_topology['nodes'])) + self._check_projects_entities(graph_topology['nodes'], + 'project_2', + False) + + def test_get_topology_with_all_tenants(self): + # Setup + graph = self._create_graph() + apis = TopologyApis(graph, None) + ctx = {'tenant': 'project_1', 'is_admin': False} + + # Action + graph_topology = apis.get_topology(ctx, + graph_type='graph', + depth=10, + query=None, + root='RESOURCE:openstack.cluster', + all_tenants=1) + graph_topology = json.loads(graph_topology) + + # Test assertions + self.assertEqual(12, len(graph_topology['nodes'])) + + def _check_projects_entities(self, + alarms, + project_id, + check_alarm_category): + for alarm in alarms: + tmp_project_id = alarm.get(VProps.PROJECT_ID, None) + condition = True + if check_alarm_category: + condition = alarm[VProps.CATEGORY] == EntityCategory.ALARM + if project_id: + condition = condition and \ + (not tmp_project_id or + (tmp_project_id and tmp_project_id == project_id)) + self.assertEqual(True, condition) + + def _create_graph(self): + graph = NXGraph('Multi tenancy graph') + + # create vertices + cluster_vertex = self._create_resource('RESOURCE:openstack.cluster', + OPENSTACK_CLUSTER) + zone_vertex = self._create_resource('zone_1', + NOVA_ZONE_DATASOURCE) + host_vertex = self._create_resource('host_1', + NOVA_HOST_DATASOURCE) + instance_1_vertex = self._create_resource('instance_1', + NOVA_INSTANCE_DATASOURCE, + project_id='project_1') + instance_2_vertex = self._create_resource('instance_2', + NOVA_INSTANCE_DATASOURCE, + project_id='project_1') + instance_3_vertex = self._create_resource('instance_3', + NOVA_INSTANCE_DATASOURCE, + project_id='project_2') + instance_4_vertex = self._create_resource('instance_4', + NOVA_INSTANCE_DATASOURCE, + project_id='project_2') + alarm_on_host_vertex = self._create_alarm('alarm_on_host', + 'alarm_on_host') + alarm_on_instance_1_vertex = self._create_alarm('alarm_on_instance_1', + 'deduced_alarm', + project_id='project_1') + alarm_on_instance_2_vertex = self._create_alarm('alarm_on_instance_2', + 'deduced_alarm') + alarm_on_instance_3_vertex = self._create_alarm('alarm_on_instance_3', + 'deduced_alarm', + project_id='project_2') + alarm_on_instance_4_vertex = self._create_alarm('alarm_on_instance_4', + 'deduced_alarm') + + # create links + edges = list() + edges.append(graph_utils.create_edge( + cluster_vertex.vertex_id, + zone_vertex.vertex_id, + 'contains')) + edges.append(graph_utils.create_edge( + zone_vertex.vertex_id, + host_vertex.vertex_id, + 'contains')) + edges.append(graph_utils.create_edge( + host_vertex.vertex_id, + instance_1_vertex.vertex_id, + 'contains')) + edges.append(graph_utils.create_edge( + host_vertex.vertex_id, + instance_2_vertex.vertex_id, + 'contains')) + edges.append(graph_utils.create_edge( + host_vertex.vertex_id, + instance_3_vertex.vertex_id, + 'contains')) + edges.append(graph_utils.create_edge( + host_vertex.vertex_id, + instance_4_vertex.vertex_id, + 'contains')) + edges.append(graph_utils.create_edge( + alarm_on_host_vertex.vertex_id, + host_vertex.vertex_id, + 'on')) + edges.append(graph_utils.create_edge( + alarm_on_instance_1_vertex.vertex_id, + instance_1_vertex.vertex_id, + 'on')) + edges.append(graph_utils.create_edge( + alarm_on_instance_2_vertex.vertex_id, + instance_2_vertex.vertex_id, + 'on')) + edges.append(graph_utils.create_edge( + alarm_on_instance_3_vertex.vertex_id, + instance_3_vertex.vertex_id, + 'on')) + edges.append(graph_utils.create_edge( + alarm_on_instance_4_vertex.vertex_id, + instance_4_vertex.vertex_id, + 'on')) + edges.append(graph_utils.create_edge( + alarm_on_host_vertex.vertex_id, + alarm_on_instance_1_vertex.vertex_id, + 'causes')) + edges.append(graph_utils.create_edge( + alarm_on_host_vertex.vertex_id, + alarm_on_instance_2_vertex.vertex_id, + 'causes')) + edges.append(graph_utils.create_edge( + alarm_on_host_vertex.vertex_id, + alarm_on_instance_3_vertex.vertex_id, + 'causes')) + edges.append(graph_utils.create_edge( + alarm_on_host_vertex.vertex_id, + alarm_on_instance_4_vertex.vertex_id, + 'causes')) + + # add vertices to graph + graph.add_vertex(cluster_vertex) + graph.add_vertex(zone_vertex) + graph.add_vertex(host_vertex) + graph.add_vertex(instance_1_vertex) + graph.add_vertex(instance_2_vertex) + graph.add_vertex(instance_3_vertex) + graph.add_vertex(instance_4_vertex) + graph.add_vertex(alarm_on_host_vertex) + graph.add_vertex(alarm_on_instance_1_vertex) + graph.add_vertex(alarm_on_instance_2_vertex) + graph.add_vertex(alarm_on_instance_3_vertex) + graph.add_vertex(alarm_on_instance_4_vertex) + + # add links to graph + for edge in edges: + graph.add_edge(edge) + + return graph diff --git a/vitrage/tests/unit/entity_graph/base.py b/vitrage/tests/unit/entity_graph/base.py index b8859dd11..e2761b405 100644 --- a/vitrage/tests/unit/entity_graph/base.py +++ b/vitrage/tests/unit/entity_graph/base.py @@ -17,7 +17,6 @@ from oslo_config import cfg from vitrage.common.constants import DatasourceProperties as DSProps from vitrage.common.constants import EntityCategory from vitrage.common.constants import SyncMode -from vitrage.common.datetime_utils import utcnow from vitrage.datasources.nagios import NAGIOS_DATASOURCE from vitrage.datasources.nova.host import NOVA_HOST_DATASOURCE from vitrage.datasources.nova.instance import NOVA_INSTANCE_DATASOURCE @@ -134,7 +133,7 @@ class TestEntityGraphUnitBase(base.BaseTest): return events_list[0] @staticmethod - def _create_alarm(vitrage_id, alarm_type): + def _create_alarm(vitrage_id, alarm_type, project_id=None): return graph_utils.create_vertex( vitrage_id, entity_id=vitrage_id, @@ -142,8 +141,23 @@ class TestEntityGraphUnitBase(base.BaseTest): entity_type=alarm_type, entity_state='active', is_deleted=False, - sample_timestamp=utcnow(), + sample_timestamp=None, is_placeholder=False, + project_id=project_id + ) + + @staticmethod + def _create_resource(vitrage_id, resource_type, project_id=None): + return graph_utils.create_vertex( + vitrage_id, + entity_id=vitrage_id, + entity_category=EntityCategory.RESOURCE, + entity_type=resource_type, + entity_state='active', + is_deleted=False, + sample_timestamp=None, + is_placeholder=False, + project_id=project_id ) def _num_total_expected_vertices(self): diff --git a/vitrage_tempest_tests/tests/api/base.py b/vitrage_tempest_tests/tests/api/base.py index 1c8b7f465..7fe122c57 100644 --- a/vitrage_tempest_tests/tests/api/base.py +++ b/vitrage_tempest_tests/tests/api/base.py @@ -100,7 +100,7 @@ class BaseApiTest(base.BaseTestCase): return volume def _get_host(self): - topology = self.vitrage_client.topology.get() + topology = self.vitrage_client.topology.get(all_tenants=1) host = filter(lambda item: item[VProps.TYPE] == NOVA_HOST_DATASOURCE, topology['nodes']) return host[0] diff --git a/vitrage_tempest_tests/tests/api/datasources/test_aodh.py b/vitrage_tempest_tests/tests/api/datasources/test_aodh.py index 72ed99da2..dc0e8abd1 100644 --- a/vitrage_tempest_tests/tests/api/datasources/test_aodh.py +++ b/vitrage_tempest_tests/tests/api/datasources/test_aodh.py @@ -34,7 +34,7 @@ class TestAodhAlarm(BaseAlarmsTest): self._create_ceilometer_alarm(self._find_instance_resource_id()) # Calculate expected results - api_graph = self.vitrage_client.topology.get() + api_graph = self.vitrage_client.topology.get(all_tenants=1) graph = self._create_graph_from_graph_dictionary(api_graph) entities = self._entities_validation_data( host_entities=1, @@ -64,7 +64,7 @@ class TestAodhAlarm(BaseAlarmsTest): self._create_ceilometer_alarm() # Calculate expected results - api_graph = self.vitrage_client.topology.get() + api_graph = self.vitrage_client.topology.get(all_tenants=1) graph = self._create_graph_from_graph_dictionary(api_graph) entities = self._entities_validation_data( host_entities=1, diff --git a/vitrage_tempest_tests/tests/api/datasources/test_cinder_volume.py b/vitrage_tempest_tests/tests/api/datasources/test_cinder_volume.py index 73a4bf700..5e4707789 100644 --- a/vitrage_tempest_tests/tests/api/datasources/test_cinder_volume.py +++ b/vitrage_tempest_tests/tests/api/datasources/test_cinder_volume.py @@ -33,7 +33,7 @@ class TestCinderVolume(BaseTopologyTest): num_volumes=self.NUM_VOLUME) # Calculate expected results - api_graph = self.vitrage_client.topology.get() + api_graph = self.vitrage_client.topology.get(all_tenants=1) graph = self._create_graph_from_graph_dictionary(api_graph) entities = self._entities_validation_data( host_entities=1, diff --git a/vitrage_tempest_tests/tests/api/datasources/test_heat_stack.py b/vitrage_tempest_tests/tests/api/datasources/test_heat_stack.py index 462c7ca63..d29c85d63 100644 --- a/vitrage_tempest_tests/tests/api/datasources/test_heat_stack.py +++ b/vitrage_tempest_tests/tests/api/datasources/test_heat_stack.py @@ -38,7 +38,7 @@ class TestHeatStack(BaseTopologyTest): self._create_stacks(num_stacks=self.NUM_STACKS) # Calculate expected results - api_graph = self.vitrage_client.topology.get() + api_graph = self.vitrage_client.topology.get(all_tenants=1) graph = self._create_graph_from_graph_dictionary(api_graph) entities = self._entities_validation_data( host_entities=1, diff --git a/vitrage_tempest_tests/tests/api/datasources/test_neutron.py b/vitrage_tempest_tests/tests/api/datasources/test_neutron.py index 0d218efca..c34d4b32f 100644 --- a/vitrage_tempest_tests/tests/api/datasources/test_neutron.py +++ b/vitrage_tempest_tests/tests/api/datasources/test_neutron.py @@ -39,7 +39,7 @@ class TestNeutron(BaseTopologyTest): set_public_network=True) # Calculate expected results - api_graph = self.vitrage_client.topology.get() + api_graph = self.vitrage_client.topology.get(all_tenants=1) graph = self._create_graph_from_graph_dictionary(api_graph) entities = self._entities_validation_data( host_entities=1, diff --git a/vitrage_tempest_tests/tests/api/datasources/test_nova.py b/vitrage_tempest_tests/tests/api/datasources/test_nova.py index ebf48b3f7..381a4a58b 100644 --- a/vitrage_tempest_tests/tests/api/datasources/test_nova.py +++ b/vitrage_tempest_tests/tests/api/datasources/test_nova.py @@ -31,7 +31,7 @@ class TestNova(BaseTopologyTest): self._create_entities(num_instances=self.NUM_INSTANCE) # Calculate expected results - api_graph = self.vitrage_client.topology.get() + api_graph = self.vitrage_client.topology.get(all_tenants=1) graph = self._create_graph_from_graph_dictionary(api_graph) entities = self._entities_validation_data( host_entities=1, diff --git a/vitrage_tempest_tests/tests/api/datasources/test_static_physical.py b/vitrage_tempest_tests/tests/api/datasources/test_static_physical.py index 021b471d3..9cb98a191 100644 --- a/vitrage_tempest_tests/tests/api/datasources/test_static_physical.py +++ b/vitrage_tempest_tests/tests/api/datasources/test_static_physical.py @@ -36,7 +36,7 @@ class TestStaticPhysical(BaseApiTest): self._create_switches() # Calculate expected results - api_graph = self.vitrage_client.topology.get() + api_graph = self.vitrage_client.topology.get(all_tenants=1) graph = self._create_graph_from_graph_dictionary(api_graph) entities = self._entities_validation_data( host_entities=1, diff --git a/vitrage_tempest_tests/tests/api/rca/test_rca.py b/vitrage_tempest_tests/tests/api/rca/test_rca.py index 879e36c92..10f43fc8d 100644 --- a/vitrage_tempest_tests/tests/api/rca/test_rca.py +++ b/vitrage_tempest_tests/tests/api/rca/test_rca.py @@ -116,7 +116,7 @@ class TestRca(BaseRcaTest): self._create_alarm( resource_id=self._get_hostname(), alarm_name=RCA_ALARM_NAME) - topology = self.vitrage_client.topology.get() + topology = self.vitrage_client.topology.get(all_tenants=1) self._validate_set_state(topology=topology['nodes'], instances=instances) diff --git a/vitrage_tempest_tests/tests/api/topology/base.py b/vitrage_tempest_tests/tests/api/topology/base.py index 5ef8ffd8a..d938a83f1 100644 --- a/vitrage_tempest_tests/tests/api/topology/base.py +++ b/vitrage_tempest_tests/tests/api/topology/base.py @@ -34,10 +34,16 @@ class BaseTopologyTest(BaseApiTest): def _rollback_to_default(self): self._delete_entities() api_graph = self.vitrage_client.topology.get( - limit=4, root='RESOURCE:openstack.cluster') + limit=4, root='RESOURCE:openstack.cluster', all_tenants=1) graph = self._create_graph_from_graph_dictionary(api_graph) entities = self._entities_validation_data() - self._validate_graph_correctness(graph, 3, 2, entities) + num_default_entities = self.num_default_entities + \ + self.num_default_networks + self.num_default_ports + num_default_edges = self.num_default_edges + self.num_default_ports + self._validate_graph_correctness(graph, + num_default_entities, + num_default_edges, + entities) def _create_entities(self, num_instances=0, num_volumes=0, end_sleep=3): if num_instances > 0: diff --git a/vitrage_tempest_tests/tests/api/topology/test_topology.py b/vitrage_tempest_tests/tests/api/topology/test_topology.py index 69e3862ee..fc6049854 100644 --- a/vitrage_tempest_tests/tests/api/topology/test_topology.py +++ b/vitrage_tempest_tests/tests/api/topology/test_topology.py @@ -18,6 +18,9 @@ from vitrage_tempest_tests.tests.api.topology.base import BaseTopologyTest import vitrage_tempest_tests.tests.utils as utils from vitrageclient.exc import ClientException +import unittest + + LOG = logging.getLogger(__name__) NOVA_QUERY = '{"and": [{"==": {"category": "RESOURCE"}},' \ '{"==": {"is_deleted": false}},' \ @@ -60,7 +63,7 @@ class TestTopology(BaseTopologyTest): num_volumes=self.NUM_VOLUME) # Calculate expected results - api_graph = self.vitrage_client.topology.get() + api_graph = self.vitrage_client.topology.get(all_tenants=1) graph = self._create_graph_from_graph_dictionary(api_graph) entities = self._entities_validation_data( host_entities=1, @@ -96,7 +99,8 @@ class TestTopology(BaseTopologyTest): # Calculate expected results api_graph = self.vitrage_client.topology.get( - query=self._graph_query()) + query=self._graph_query(), + all_tenants=1) graph = self._create_graph_from_graph_dictionary(api_graph) entities = self._entities_validation_data( host_entities=1, @@ -126,7 +130,7 @@ class TestTopology(BaseTopologyTest): # Calculate expected results api_graph = self.vitrage_client.topology.get( - graph_type='tree', query=NOVA_QUERY) + graph_type='tree', query=NOVA_QUERY, all_tenants=1) graph = self._create_graph_from_tree_dictionary(api_graph) entities = self._entities_validation_data( host_entities=1, @@ -156,7 +160,7 @@ class TestTopology(BaseTopologyTest): # Calculate expected results api_graph = self.vitrage_client.topology.get( - graph_type='tree', query=self._tree_query()) + graph_type='tree', query=self._tree_query(), all_tenants=1) graph = self._create_graph_from_tree_dictionary(api_graph) entities = self._entities_validation_data( host_entities=1, host_edges=1) @@ -181,7 +185,7 @@ class TestTopology(BaseTopologyTest): # Calculate expected results api_graph = self.vitrage_client.topology.get( - limit=2, graph_type='tree', query=NOVA_QUERY) + limit=2, graph_type='tree', query=NOVA_QUERY, all_tenants=1) graph = self._create_graph_from_tree_dictionary(api_graph) entities = self._entities_validation_data( host_entities=1, host_edges=1) @@ -206,7 +210,7 @@ class TestTopology(BaseTopologyTest): # Calculate expected results api_graph = self.vitrage_client.topology.get( - limit=3, graph_type='tree', query=NOVA_QUERY) + limit=3, graph_type='tree', query=NOVA_QUERY, all_tenants=1) graph = self._create_graph_from_tree_dictionary(api_graph) entities = self._entities_validation_data( host_entities=1, @@ -224,6 +228,7 @@ class TestTopology(BaseTopologyTest): finally: self._rollback_to_default() + @unittest.skip("testing skipping") def test_graph_with_root_and_depth_exclude_instance(self): """tree_with_query @@ -236,7 +241,7 @@ class TestTopology(BaseTopologyTest): # Calculate expected results api_graph = self.vitrage_client.topology.get( - limit=2, root='RESOURCE:openstack.cluster') + limit=2, root='RESOURCE:openstack.cluster', all_tenants=1) graph = self._create_graph_from_graph_dictionary(api_graph) entities = self._entities_validation_data( host_entities=1, host_edges=1) @@ -249,6 +254,7 @@ class TestTopology(BaseTopologyTest): finally: self._rollback_to_default() + @unittest.skip("testing skipping") def test_graph_with_root_and_depth_include_instance(self): """graph_with_root_and_depth_include_instance @@ -261,7 +267,7 @@ class TestTopology(BaseTopologyTest): # Calculate expected results api_graph = self.vitrage_client.topology.get( - limit=3, root='RESOURCE:openstack.cluster') + limit=3, root='RESOURCE:openstack.cluster', all_tenants=1) graph = self._create_graph_from_graph_dictionary(api_graph) entities = self._entities_validation_data( host_entities=1, @@ -292,7 +298,8 @@ class TestTopology(BaseTopologyTest): # Calculate expected results self.vitrage_client.topology.get(limit=2, - root='RESOURCE:openstack.cluster') + root='RESOURCE:openstack.cluster', + all_tenants=1) except ClientException as e: self.assertEqual(403, e.code) self.assertEqual( @@ -314,7 +321,7 @@ class TestTopology(BaseTopologyTest): # Calculate expected results api_graph = self.vitrage_client.topology.get( - query=self._graph_no_match_query()) + query=self._graph_no_match_query(), all_tenants=1) # Test Assertions self.assertEqual( @@ -338,7 +345,9 @@ class TestTopology(BaseTopologyTest): # Calculate expected results api_graph = self.vitrage_client.topology.get( - graph_type='tree', query=self._tree_no_match_query()) + graph_type='tree', + query=self._tree_no_match_query(), + all_tenants=1) # Test Assertions self.assertEqual({}, api_graph)