diff --git a/etc/vitrage/policy.json b/etc/vitrage/policy.json index e1d8ec4b3..2b7ad9665 100644 --- a/etc/vitrage/policy.json +++ b/etc/vitrage/policy.json @@ -3,6 +3,7 @@ "get topology:all_tenants": "role:admin", "get resource": "", "list resources": "", + "list resources:all_tenants": "role:admin", "list alarms": "", "list alarms:all_tenants": "role:admin", "get rca": "", diff --git a/vitrage/api/controllers/v1/resource.py b/vitrage/api/controllers/v1/resource.py index 9931fdc92..e6942a0ee 100644 --- a/vitrage/api/controllers/v1/resource.py +++ b/vitrage/api/controllers/v1/resource.py @@ -9,62 +9,56 @@ # 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 pecan from oslo_log import log +from oslo_utils.strutils import bool_from_string from pecan.core import abort -from pecan import rest + +from vitrage.api.controllers.rest import RootRestController from vitrage.api.policy import enforce LOG = log.getLogger(__name__) -class ResourcesController(rest.RestController): - - @pecan.expose() - def _lookup(self, id_, *remainder): - LOG.info('got lookup %s', id_) - return ResourceController(id_), remainder - +class ResourcesController(RootRestController): @pecan.expose('json') - def index(self, resource_type=None): - enforce('list resources', pecan.request.headers, - pecan.request.enforcer, {}) + def get(self, **kwargs): + LOG.info('get list resource with args: %s', kwargs) - LOG.info('received list resources with filter %s', resource_type) + resource_type = kwargs.get('resource_type', None) + all_tenants = kwargs.get('all_tenants', False) + all_tenants = bool_from_string(all_tenants) + if all_tenants: + enforce('list resources:all_tenants', pecan.request.headers, + pecan.request.enforcer, {}) + else: + enforce('list resources', pecan.request.headers, + pecan.request.enforcer, {}) + + LOG.info('received resources list with filter %s', resource_type) try: - return self.get_resources(resource_type) + return self._get_resources(resource_type, all_tenants) except Exception as e: LOG.exception('failed to get resources %s', e) abort(404, str(e)) @staticmethod - def get_resources(resource_type): - # todo(eyalb1) need a mock for this - return [{'None': None}] - - -class ResourceController(rest.RestController): - - def __init__(self, id_): - self.id = id_ - - @pecan.expose('json') - def get(self): - enforce('get resource', pecan.request.headers, - pecan.request.enforcer, {}) - - LOG.info('received get resource with id %s', self.id) + def _get_resources(resource_type=None, all_tenants=False): + LOG.info('get_resources with type: %s, all_tenants: %s', + resource_type, all_tenants) try: - return self.get_resource(self.id) + resources_json = \ + pecan.request.client.call(pecan.request.context, + 'get_resources', + resource_type=resource_type, + all_tenants=all_tenants) + LOG.info(resources_json) + resources = json.loads(resources_json)['resources'] + return resources except Exception as e: - LOG.exception('failed to get resource %s', e) + LOG.exception('failed to get resources %s ', e) abort(404, str(e)) - - @staticmethod - def get_resource(id_): - # todo(eyalb1) need a mock for this - return {'None': None} diff --git a/vitrage/api_handler/apis/base.py b/vitrage/api_handler/apis/base.py index 4df244944..5ca168e69 100644 --- a/vitrage/api_handler/apis/base.py +++ b/vitrage/api_handler/apis/base.py @@ -80,6 +80,14 @@ ALARM_QUERY = { EDGE_QUERY = {'==': {EProps.IS_DELETED: False}} +RESOURCES_ALL_QUERY = { + 'and': [ + {'==': {VProps.CATEGORY: EntityCategory.RESOURCE}}, + {'==': {VProps.IS_DELETED: False}}, + {'==': {VProps.IS_PLACEHOLDER: False}} + ] +} + class EntityGraphApisBase(object): TENANT_PROPERTY = 'tenant' diff --git a/vitrage/api_handler/apis/resource.py b/vitrage/api_handler/apis/resource.py new file mode 100644 index 000000000..81fa51b4b --- /dev/null +++ b/vitrage/api_handler/apis/resource.py @@ -0,0 +1,55 @@ +# Copyright 2016 - ZTE, 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 copy +import json +from oslo_log import log + +from vitrage.api_handler.apis.base import EntityGraphApisBase +from vitrage.api_handler.apis.base import RESOURCES_ALL_QUERY +from vitrage.common.constants import EntityCategory +from vitrage.common.constants import VertexProperties as VProps + + +LOG = log.getLogger(__name__) + + +class ResourceApis(EntityGraphApisBase): + + def __init__(self, entity_graph, conf): + self.entity_graph = entity_graph + self.conf = conf + + def get_resources(self, ctx, resource_type=None, all_tenants=False): + LOG.debug('ResourceApis get_resources - resource_type: %s,' + 'all_tenants: %s', str(resource_type), all_tenants) + + project_id = ctx.get(self.TENANT_PROPERTY, None) + is_admin_project = ctx.get(self.IS_ADMIN_PROJECT_PROPERTY, False) + + if all_tenants: + resource_query = RESOURCES_ALL_QUERY + else: + resource_query = self._get_query_with_project( + EntityCategory.RESOURCE, + project_id, + is_admin_project) + query = copy.deepcopy(resource_query) + + if resource_type: + type_query = {'==': {VProps.TYPE: resource_type}} + query['and'].append(type_query) + + resources = self.entity_graph.get_vertices(query_dict=query) + return json.dumps({'resources': [resource.properties + for resource in resources]}) diff --git a/vitrage/api_handler/service.py b/vitrage/api_handler/service.py index c139cca48..18fc69090 100644 --- a/vitrage/api_handler/service.py +++ b/vitrage/api_handler/service.py @@ -20,6 +20,7 @@ from oslo_service import service as os_service from vitrage.api_handler.apis.alarm import AlarmApis from vitrage.api_handler.apis.event import EventApis from vitrage.api_handler.apis.rca import RcaApis +from vitrage.api_handler.apis.resource import ResourceApis from vitrage.api_handler.apis.template import TemplateApis from vitrage.api_handler.apis.topology import TopologyApis from vitrage import messaging @@ -52,7 +53,8 @@ class VitrageApiHandlerService(os_service.Service): AlarmApis(self.entity_graph, self.conf), RcaApis(self.entity_graph, self.conf), TemplateApis(self.scenario_repo.templates), - EventApis(self.conf)] + EventApis(self.conf), + ResourceApis(self.entity_graph, self.conf)] server = vitrage_rpc.get_server(target, endpoints, transport) diff --git a/vitrage/tests/functional/api_handler/test_apis.py b/vitrage/tests/functional/api_handler/test_apis.py index 9d4721fd4..23160fd4d 100644 --- a/vitrage/tests/functional/api_handler/test_apis.py +++ b/vitrage/tests/functional/api_handler/test_apis.py @@ -16,6 +16,7 @@ import json from vitrage.api_handler.apis.alarm import AlarmApis from vitrage.api_handler.apis.rca import RcaApis +from vitrage.api_handler.apis.resource import ResourceApis from vitrage.api_handler.apis.topology import TopologyApis from vitrage.common.constants import EntityCategory from vitrage.common.constants import VertexProperties as VProps @@ -193,6 +194,86 @@ class TestApis(TestEntityGraphUnitBase): # Test assertions self.assertEqual(12, len(graph_topology['nodes'])) + def test_resource_list_with_admin_project(self): + # Setup + graph = self._create_graph() + apis = ResourceApis(graph, None) + ctx = {'tenant': 'project_2', 'is_admin': True} + + # Action + resources = apis.get_resources( + ctx, + resource_type=None, + all_tenants=False) + resources = json.loads(resources)['resources'] + + # Test assertions + self.assertEqual(5, len(resources)) + + def test_resource_list_with_not_admin_project(self): + # Setup + graph = self._create_graph() + apis = ResourceApis(graph, None) + ctx = {'tenant': 'project_2', 'is_admin': False} + + # Action + resources = apis.get_resources( + ctx, + resource_type=None, + all_tenants=False) + resources = json.loads(resources)['resources'] + + # Test assertions + self.assertEqual(2, len(resources)) + + def test_resource_list_with_not_admin_project_and_no_existing_type(self): + # Setup + graph = self._create_graph() + apis = ResourceApis(graph, None) + ctx = {'tenant': 'project_2', 'is_admin': False} + + # Action + resources = apis.get_resources( + ctx, + resource_type=NOVA_HOST_DATASOURCE, + all_tenants=False) + resources = json.loads(resources)['resources'] + + # Test assertions + self.assertEqual(0, len(resources)) + + def test_resource_list_with_not_admin_project_and_existing_type(self): + # Setup + graph = self._create_graph() + apis = ResourceApis(graph, None) + ctx = {'tenant': 'project_2', 'is_admin': False} + + # Action + resources = apis.get_resources( + ctx, + resource_type=NOVA_INSTANCE_DATASOURCE, + all_tenants=False) + resources = json.loads(resources)['resources'] + + # Test assertions + self.assertEqual(2, len(resources)) + + def test_resource_list_with_all_tenants(self): + # Setup + graph = self._create_graph() + apis = ResourceApis(graph, None) + ctx = {'tenant': 'project_1', 'is_admin': False} + + # Action + resources = apis.get_resources( + ctx, + resource_type=None, + all_tenants=True) + resources = json.loads(resources)['resources'] + + # Test assertions + self.assertEqual(7, len(resources)) + def _check_projects_entities(self, alarms, project_id, diff --git a/vitrage_tempest_tests/tests/api/resources/__init__.py b/vitrage_tempest_tests/tests/api/resources/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/vitrage_tempest_tests/tests/api/resources/test_resources.py b/vitrage_tempest_tests/tests/api/resources/test_resources.py new file mode 100644 index 000000000..49f5d9e94 --- /dev/null +++ b/vitrage_tempest_tests/tests/api/resources/test_resources.py @@ -0,0 +1,154 @@ +# Copyright 2017 ZTE, 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 +import traceback + +from oslo_log import log as logging + +from vitrage.datasources import CINDER_VOLUME_DATASOURCE +from vitrage.datasources import NOVA_INSTANCE_DATASOURCE +from vitrage_tempest_tests.tests.api.base import BaseApiTest +import vitrage_tempest_tests.tests.utils as utils + + +LOG = logging.getLogger(__name__) + + +class TestResource(BaseApiTest): + """Test class for Vitrage resource API tests.""" + + @classmethod + def setUpClass(cls): + super(TestResource, cls).setUpClass() + + @utils.tempest_logger + def test_compare_cli_vs_api_resource_list(self): + """resource list """ + try: + instances = self._create_instances(num_instances=1) + self.assertNotEqual(len(instances), 0, + 'The instances list is empty') + api_resources = self.vitrage_client.resource.list() + cli_resources = utils.run_vitrage_command( + 'vitrage resource list -f json', self.conf) + + self._compare_resources(api_resources, cli_resources) + except Exception as e: + LOG.exception(e) + traceback.print_exc() + raise + finally: + self._delete_instances() + + @utils.tempest_logger + def test_default_resource_list(self): + """resource list with default query + + get the resources: cluster, zone, host and one instance + """ + try: + instances = self._create_instances(num_instances=1) + self.assertNotEqual(len(instances), 0, + 'The instances list is empty') + resources = self.vitrage_client.resource.list() + self.assertEqual(4, len(resources)) + except Exception as e: + LOG.exception(e) + traceback.print_exc() + raise + finally: + self._delete_instances() + + @utils.tempest_logger + def test_resource_list_with_all_tenants(self): + """resource list with all tenants + + get the resources: + cluster, zone, host and one instance(no other tenants) + """ + try: + instances = self._create_instances(num_instances=1) + self.assertNotEqual(len(instances), 0, + 'The instances list is empty') + resources = self.vitrage_client.resource.list(all_tenants=True) + self.assertEqual(4, len(resources)) + except Exception as e: + LOG.exception(e) + traceback.print_exc() + raise + finally: + self._delete_instances() + + @utils.tempest_logger + def test_resource_list_with_existing_type(self): + """resource list with existing type + + get the resource: one instance + """ + try: + instances = self._create_instances(num_instances=1) + self.assertNotEqual(len(instances), 0, + 'The instances list is empty') + resources = self.vitrage_client.resource.list( + resource_type=NOVA_INSTANCE_DATASOURCE, + all_tenants=False) + self.assertEqual(1, len(resources)) + except Exception as e: + LOG.exception(e) + traceback.print_exc() + raise + finally: + self._delete_instances() + + @utils.tempest_logger + def test_resource_list_with_no_existing_type(self): + """resource list with no existing type""" + try: + instances = self._create_instances(num_instances=1) + self.assertNotEqual(len(instances), 0, + 'The instances list is empty') + resources = self.vitrage_client.resource.list( + resource_type=CINDER_VOLUME_DATASOURCE, + all_tenants=False) + self.assertEqual(0, len(resources)) + except Exception as e: + LOG.exception(e) + traceback.print_exc() + raise + finally: + self._delete_instances() + + def _compare_resources(self, api_resources, cli_resources): + self.assertNotEqual(len(api_resources), 0, + 'The resources taken from rest api is empty') + self.assertNotEqual(len(cli_resources), 0, + 'The resources taken from terminal is empty') + + sorted_cli_resources = sorted( + json.loads(cli_resources), + key=lambda resource: resource["vitrage_id"]) + sorted_api_resources = sorted( + api_resources, + key=lambda resource: resource["vitrage_id"]) + + self.assertEqual(len(sorted_cli_resources), + len(sorted_api_resources)) + + properties = ('vitrage_id', 'type', 'id', 'state', + 'aggregated_state', 'operational_state') + for cli_resource, api_resource in \ + zip(sorted_cli_resources, sorted_api_resources): + for item in properties: + self.assertEqual(cli_resource.get(item), + api_resource.get(item))