diff --git a/tricircle/db/client.py b/tricircle/db/client.py index a6e91c6f..b4c3aeac 100644 --- a/tricircle/db/client.py +++ b/tricircle/db/client.py @@ -13,8 +13,10 @@ # License for the specific language governing permissions and limitations # under the License. +import collections import functools import inspect +import six import uuid from keystoneclient.auth.identity import v3 as auth_identity @@ -67,10 +69,35 @@ cfg.CONF.register_opts(client_opts, group=client_opt_group) LOG = logging.getLogger(__name__) +def _safe_operation(operation_name): + def handle_func(func): + @six.wraps(func) + def handle_args(*args, **kwargs): + instance, resource, context = args[:3] + if resource not in instance.operation_resources_map[ + operation_name]: + raise exception.ResourceNotSupported(resource, operation_name) + retries = 1 + for _ in xrange(retries + 1): + try: + service = instance.resource_service_map[resource] + instance._ensure_endpoint_set(context, service) + return func(*args, **kwargs) + except exception.EndpointNotAvailable as e: + if cfg.CONF.client.auto_refresh_endpoint: + LOG.warn(e.message + ', update endpoint and try again') + instance._update_endpoint_from_keystone(context, True) + else: + raise + return handle_args + return handle_func + + class Client(object): def __init__(self): self.auth_url = cfg.CONF.client.auth_url self.resource_service_map = {} + self.operation_resources_map = collections.defaultdict(set) self.service_handle_map = {} for _, handle_class in inspect.getmembers(resource_handle): if not inspect.isclass(handle_class): @@ -81,8 +108,16 @@ class Client(object): self.service_handle_map[handle_obj.service_type] = handle_obj for resource in handle_obj.support_resource: self.resource_service_map[resource] = handle_obj.service_type - setattr(self, 'list_%ss' % resource, - functools.partial(self.list_resources, resource)) + for operation, index in six.iteritems( + resource_handle.operation_index_map): + # add parentheses to emphasize we mean to do bitwise and + if (handle_obj.support_resource[resource] & index) == 0: + continue + self.operation_resources_map[operation].add(resource) + setattr(self, '%s_%ss' % (operation, resource), + functools.partial( + getattr(self, '%s_resources' % operation), + resource)) def _get_admin_token(self): auth = auth_identity.Password( @@ -233,6 +268,7 @@ class Client(object): """ self._update_endpoint_from_keystone(cxt, False) + @_safe_operation('list') def list_resources(self, resource, cxt, filters=None): """Query resource in site of top layer @@ -250,19 +286,49 @@ class Client(object): :return: list of dict containing resources information :raises: EndpointNotAvailable """ - if resource not in self.resource_service_map: - raise exception.ResourceNotSupported(resource, 'list') service = self.resource_service_map[resource] - self._ensure_endpoint_set(cxt, service) handle = self.service_handle_map[service] filters = filters or [] - try: - return handle.handle_list(cxt, resource, filters) - except exception.EndpointNotAvailable as e: - if cfg.CONF.client.auto_refresh_endpoint: - LOG.warn(e.message + ', update endpoint and try again') - self._update_endpoint_from_keystone(cxt, True) - self._ensure_endpoint_set(cxt, service) - return handle.handle_list(cxt, resource, filters) - else: - raise e + return handle.handle_list(cxt, resource, filters) + + @_safe_operation('create') + def create_resources(self, resource, cxt, *args, **kwargs): + """Create resource in site of top layer + + Directly invoke this method to create resources, or use + create_(resource)s (self, cxt, *args, **kwargs). These methods are + automatically generated according to the supported resources of each + ResourceHandle class. + + :param resource: resource type + :param cxt: context object + :param args, kwargs: passed according to resource type + -------------------------- + resource -> args -> kwargs + -------------------------- + aggregate -> name, availability_zone_name -> none + -------------------------- + :return: a dict containing resource information + :raises: EndpointNotAvailable + """ + service = self.resource_service_map[resource] + handle = self.service_handle_map[service] + return handle.handle_create(cxt, resource, *args, **kwargs) + + @_safe_operation('delete') + def delete_resources(self, resource, cxt, resource_id): + """Delete resource in site of top layer + + Directly invoke this method to delete resources, or use + delete_(resource)s (self, cxt, obj_id). These methods are + automatically generated according to the supported resources + of each ResourceHandle class. + :param resource: resource type + :param cxt: context object + :param resource_id: id of resource + :return: None + :raises: EndpointNotAvailable + """ + service = self.resource_service_map[resource] + handle = self.service_handle_map[service] + handle.handle_delete(cxt, resource, resource_id) diff --git a/tricircle/db/resource_handle.py b/tricircle/db/resource_handle.py index 3661fe1a..9335fb67 100644 --- a/tricircle/db/resource_handle.py +++ b/tricircle/db/resource_handle.py @@ -19,7 +19,9 @@ import glanceclient.exc as g_exceptions from neutronclient.common import exceptions as q_exceptions from neutronclient.neutron import client as q_client from novaclient import client as n_client +from novaclient import exceptions as n_exceptions from oslo_config import cfg +from oslo_log import log as logging from requests import exceptions as r_exceptions from tricircle.db import exception as exception @@ -38,6 +40,12 @@ client_opts = [ cfg.CONF.register_opts(client_opts, group='client') +LIST, CREATE, DELETE = 1, 2, 4 +operation_index_map = {'list': LIST, 'create': CREATE, 'delete': DELETE} + +LOG = logging.getLogger(__name__) + + def _transform_filters(filters): filter_dict = {} for query_filter in filters: @@ -64,7 +72,7 @@ class ResourceHandle(object): class GlanceResourceHandle(ResourceHandle): service_type = 'glance' - support_resource = ('image', ) + support_resource = {'image': LIST} def _get_client(self, cxt): return g_client.Client('1', @@ -74,8 +82,6 @@ class GlanceResourceHandle(ResourceHandle): timeout=cfg.CONF.client.glance_timeout) def handle_list(self, cxt, resource, filters): - if resource not in self.support_resource: - return [] try: client = self._get_client(cxt) collection = '%ss' % resource @@ -89,8 +95,12 @@ class GlanceResourceHandle(ResourceHandle): class NeutronResourceHandle(ResourceHandle): service_type = 'neutron' - support_resource = ('network', 'subnet', 'port', 'router', - 'security_group', 'security_group_rule') + support_resource = {'network': LIST, + 'subnet': LIST, + 'port': LIST, + 'router': LIST, + 'security_group': LIST, + 'security_group_rule': LIST} def _get_client(self, cxt): return q_client.Client('2.0', @@ -100,8 +110,6 @@ class NeutronResourceHandle(ResourceHandle): timeout=cfg.CONF.client.neutron_timeout) def handle_list(self, cxt, resource, filters): - if resource not in self.support_resource: - return [] try: client = self._get_client(cxt) collection = '%ss' % resource @@ -116,7 +124,9 @@ class NeutronResourceHandle(ResourceHandle): class NovaResourceHandle(ResourceHandle): service_type = 'nova' - support_resource = ('flavor', 'server') + support_resource = {'flavor': LIST, + 'server': LIST, + 'aggregate': LIST | CREATE | DELETE} def _get_client(self, cxt): cli = n_client.Client('2', @@ -128,8 +138,6 @@ class NovaResourceHandle(ResourceHandle): return cli def handle_list(self, cxt, resource, filters): - if resource not in self.support_resource: - return [] try: client = self._get_client(cxt) collection = '%ss' % resource @@ -145,3 +153,27 @@ class NovaResourceHandle(ResourceHandle): self.endpoint_url = None raise exception.EndpointNotAvailable('nova', client.client.management_url) + + def handle_create(self, cxt, resource, *args, **kwargs): + try: + client = self._get_client(cxt) + collection = '%ss' % resource + return getattr(client, collection).create( + *args, **kwargs).to_dict() + except r_exceptions.ConnectTimeout: + self.endpoint_url = None + raise exception.EndpointNotAvailable('nova', + client.client.management_url) + + def handle_delete(self, cxt, resource, resource_id): + try: + client = self._get_client(cxt) + collection = '%ss' % resource + return getattr(client, collection).delete(resource_id) + except r_exceptions.ConnectTimeout: + self.endpoint_url = None + raise exception.EndpointNotAvailable('nova', + client.client.management_url) + except n_exceptions.NotFound: + LOG.debug("Delete %(resource)s %(resource_id)s which not found", + {'resource': resource, 'resource_id': resource_id}) diff --git a/tricircle/tests/unit/db/test_client.py b/tricircle/tests/unit/db/test_client.py index 491b6ca8..99954363 100644 --- a/tricircle/tests/unit/db/test_client.py +++ b/tricircle/tests/unit/db/test_client.py @@ -35,6 +35,7 @@ FAKE_SERVICE_NAME = 'fake_service_name' FAKE_TYPE = 'fake_type' FAKE_URL = 'http://127.0.0.1:12345' FAKE_URL_INVALID = 'http://127.0.0.1:23456' +FAKE_RESOURCES = [{'name': 'res1'}, {'name': 'res2'}] class FakeException(Exception): @@ -44,18 +45,31 @@ class FakeException(Exception): class FakeClient(object): def __init__(self, url): self.endpoint = url - self.resources = [{'name': 'res1'}, {'name': 'res2'}] def list_fake_res(self, search_opts): # make sure endpoint is correctly set if self.endpoint != FAKE_URL: raise FakeException() if not search_opts: - return [res for res in self.resources] + return [res for res in FAKE_RESOURCES] else: - return [res for res in self.resources if ( + return [res for res in FAKE_RESOURCES if ( res['name'] == search_opts['name'])] + def create_fake_res(self, name): + if self.endpoint != FAKE_URL: + raise FakeException() + FAKE_RESOURCES.append({'name': name}) + return {'name': name} + + def delete_fake_res(self, name): + if self.endpoint != FAKE_URL: + raise FakeException() + try: + FAKE_RESOURCES.remove({'name': name}) + except ValueError: + pass + class FakeResHandle(resource_handle.ResourceHandle): def _get_client(self, cxt): @@ -70,6 +84,22 @@ class FakeResHandle(resource_handle.ResourceHandle): self.endpoint_url = None raise exception.EndpointNotAvailable(FAKE_TYPE, cli.endpoint) + def handle_create(self, cxt, resource, name): + try: + cli = self._get_client(cxt) + return cli.create_fake_res(name) + except FakeException: + self.endpoint_url = None + raise exception.EndpointNotAvailable(FAKE_TYPE, cli.endpoint) + + def handle_delete(self, cxt, resource, name): + try: + cli = self._get_client(cxt) + cli.delete_fake_res(name) + except FakeException: + self.endpoint_url = None + raise exception.EndpointNotAvailable(FAKE_TYPE, cli.endpoint) + class ClientTest(unittest.TestCase): def setUp(self): @@ -97,10 +127,16 @@ class ClientTest(unittest.TestCase): models.create_service_type(self.context, type_dict) models.create_site_service_configuration(self.context, config_dict) + global FAKE_RESOURCES + FAKE_RESOURCES = [{'name': 'res1'}, {'name': 'res2'}] + cfg.CONF.set_override(name='top_site_name', override=FAKE_SITE_NAME, group='client') self.client = client.Client() self.client.resource_service_map[FAKE_RESOURCE] = FAKE_TYPE + self.client.operation_resources_map['list'].add(FAKE_RESOURCE) + self.client.operation_resources_map['create'].add(FAKE_RESOURCE) + self.client.operation_resources_map['delete'].add(FAKE_RESOURCE) self.client.service_handle_map[FAKE_TYPE] = FakeResHandle(None) def test_list(self): @@ -115,6 +151,19 @@ class ClientTest(unittest.TestCase): 'value': 'res2'}]) self.assertEqual(resources, [{'name': 'res2'}]) + def test_create(self): + resource = self.client.create_resources(FAKE_RESOURCE, self.context, + 'res3') + self.assertEqual(resource, {'name': 'res3'}) + resources = self.client.list_resources(FAKE_RESOURCE, self.context) + self.assertEqual(resources, [{'name': 'res1'}, {'name': 'res2'}, + {'name': 'res3'}]) + + def test_delete(self): + self.client.delete_resources(FAKE_RESOURCE, self.context, 'res1') + resources = self.client.list_resources(FAKE_RESOURCE, self.context) + self.assertEqual(resources, [{'name': 'res2'}]) + def test_list_endpoint_not_found(self): cfg.CONF.set_override(name='auto_refresh_endpoint', override=False, group='client') @@ -125,6 +174,18 @@ class ClientTest(unittest.TestCase): self.client.list_resources, FAKE_RESOURCE, self.context, []) + def test_resource_not_supported(self): + # no such resource + self.assertRaises(exception.ResourceNotSupported, + self.client.list_resources, + 'no_such_resource', self.context, []) + # remove "create" entry for FAKE_RESOURCE + self.client.operation_resources_map['create'].remove(FAKE_RESOURCE) + # operation not supported + self.assertRaises(exception.ResourceNotSupported, + self.client.create_resources, + FAKE_RESOURCE, self.context, []) + def test_list_endpoint_not_found_retry(self): cfg.CONF.set_override(name='auto_refresh_endpoint', override=True, group='client')