From 917591428ec195370116ab8186ac175e9a4ee746 Mon Sep 17 00:00:00 2001 From: David Lyle Date: Wed, 8 May 2013 14:15:27 -0600 Subject: [PATCH] Region selector enabling multi-region support. This patch adds a region selector dropdown at the top of both the Project and Admin dashboards if more than one region is available in the user's service catalog. The user is allowed to choose from any region available in the service catalog. By selecting a region, the user is limited to accessing endpoints in that region only as long as the go through api.base.url_for If there are more than one endpoint for a service in a region the first in the catalog is returned. Further work on the blueprint will handle that complexity. Supporting Keystone v2.0 and v3 catalog formats. Partially implements blueprint multiple-service-endpoints Change-Id: I1ab6539c7c5f4b1ae4b1716059370e86b6ca4d2e --- .../templates/horizon/common/_sidebar.html | 18 +++++++ openstack_dashboard/api/base.py | 48 ++++++++++++------- openstack_dashboard/api/keystone.py | 13 +++-- .../dashboards/admin/info/tables.py | 5 +- .../dashboards/admin/info/tabs.py | 3 +- .../access_and_security/api_access/tables.py | 6 +-- .../project/access_and_security/tabs.py | 4 +- .../test/api_tests/base_tests.py | 22 +++++++++ .../test/api_tests/keystone_tests.py | 20 +++++++- .../test/test_data/keystone_data.py | 10 +++- 10 files changed, 118 insertions(+), 31 deletions(-) diff --git a/horizon/templates/horizon/common/_sidebar.html b/horizon/templates/horizon/common/_sidebar.html index 1f7a9a23e..b5367674f 100644 --- a/horizon/templates/horizon/common/_sidebar.html +++ b/horizon/templates/horizon/common/_sidebar.html @@ -32,5 +32,23 @@ {% endif %} + {% with num_of_regions=request.user.available_services_regions|length %} + {% if num_of_regions > 1 %} + + {% endif %} + {% endwith %} + {% horizon_dashboard_nav %} diff --git a/openstack_dashboard/api/base.py b/openstack_dashboard/api/base.py index ff227f6f1..ce16ba1e9 100644 --- a/openstack_dashboard/api/base.py +++ b/openstack_dashboard/api/base.py @@ -97,7 +97,7 @@ class APIDictWrapper(object): dictionary, in addition to attribute accesses. Attribute access is the preferred method of access, to be - consistent with api resource objects from novclient. + consistent with api resource objects from novaclient. """ def __init__(self, apidict): self._apidict = apidict @@ -198,18 +198,21 @@ ENDPOINT_TYPE_TO_INTERFACE = { } -def get_url_for_service(service, endpoint_type): +def get_url_for_service(service, region, endpoint_type): identity_version = get_version_from_service(service) for endpoint in service['endpoints']: - try: - if identity_version < 3: - return endpoint[endpoint_type] - else: - interface = ENDPOINT_TYPE_TO_INTERFACE.get(endpoint_type, '') - if endpoint['interface'] == interface: - return endpoint['url'] - except (IndexError, KeyError): - pass + # ignore region for identity + if service['type'] == 'identity' or region == endpoint['region']: + try: + if identity_version < 3: + return endpoint[endpoint_type] + else: + interface = \ + ENDPOINT_TYPE_TO_INTERFACE.get(endpoint_type, '') + if endpoint['interface'] == interface: + return endpoint['url'] + except (IndexError, KeyError): + return None return None @@ -222,9 +225,13 @@ def url_for(request, service_type, endpoint_type=None): catalog = request.user.service_catalog service = get_service_from_catalog(catalog, service_type) if service: - url = get_url_for_service(service, endpoint_type) + url = get_url_for_service(service, + request.user.services_region, + endpoint_type) if not url and fallback_endpoint_type: - url = get_url_for_service(service, fallback_endpoint_type) + url = get_url_for_service(service, + request.user.services_region, + fallback_endpoint_type) if url: return url raise exceptions.ServiceCatalogException(service_type) @@ -233,7 +240,14 @@ def url_for(request, service_type, endpoint_type=None): def is_service_enabled(request, service_type, service_name=None): service = get_service_from_catalog(request.user.service_catalog, service_type) - if service and service_name: - return service['name'] == service_name - else: - return service is not None + if service: + region = request.user.services_region + for endpoint in service['endpoints']: + # ignore region for identity + if service['type'] == 'identity' or \ + endpoint['region'] == region: + if service_name: + return service['name'] == service_name + else: + return True + return False diff --git a/openstack_dashboard/api/keystone.py b/openstack_dashboard/api/keystone.py index 458d1c38f..feae7ae02 100644 --- a/openstack_dashboard/api/keystone.py +++ b/openstack_dashboard/api/keystone.py @@ -77,12 +77,17 @@ class Service(base.APIDictWrapper): """ Wrapper for a dict based on the service data from keystone. """ _attrs = ['id', 'type', 'name'] - def __init__(self, service, *args, **kwargs): + def __init__(self, service, region, *args, **kwargs): super(Service, self).__init__(service, *args, **kwargs) - self.url = service['endpoints'][0]['internalURL'] - self.host = urlparse.urlparse(self.url).hostname - self.region = service['endpoints'][0]['region'] + self.public_url = base.get_url_for_service(service, region, + 'publicURL') + self.url = base.get_url_for_service(service, region, 'internalURL') + if self.url: + self.host = urlparse.urlparse(self.url).hostname + else: + self.host = None self.disabled = None + self.region = region def __unicode__(self): if(self.type == "identity"): diff --git a/openstack_dashboard/dashboards/admin/info/tables.py b/openstack_dashboard/dashboards/admin/info/tables.py index 7e4ed3217..bfe4f2dd5 100644 --- a/openstack_dashboard/dashboards/admin/info/tables.py +++ b/openstack_dashboard/dashboards/admin/info/tables.py @@ -63,7 +63,10 @@ def get_enabled(service, reverse=False): options = ["Enabled", "Disabled"] if reverse: options.reverse() - return options[0] if not service.disabled else options[1] + # if not configured in this region, neither option makes sense + if service.host: + return options[0] if not service.disabled else options[1] + return None class ServicesTable(tables.DataTable): diff --git a/openstack_dashboard/dashboards/admin/info/tabs.py b/openstack_dashboard/dashboards/admin/info/tabs.py index c99ea1853..28740419b 100644 --- a/openstack_dashboard/dashboards/admin/info/tabs.py +++ b/openstack_dashboard/dashboards/admin/info/tabs.py @@ -54,7 +54,8 @@ class ServicesTab(tabs.TableTab): services = [] for i, service in enumerate(request.user.service_catalog): service['id'] = i - services.append(keystone.Service(service)) + services.append( + keystone.Service(service, request.user.services_region)) return services diff --git a/openstack_dashboard/dashboards/project/access_and_security/api_access/tables.py b/openstack_dashboard/dashboards/project/access_and_security/api_access/tables.py index dc520cba2..88f171a7a 100644 --- a/openstack_dashboard/dashboards/project/access_and_security/api_access/tables.py +++ b/openstack_dashboard/dashboards/project/access_and_security/api_access/tables.py @@ -20,10 +20,6 @@ from django.utils.translation import ugettext_lazy as _ from horizon import tables -def get_endpoint(service): - return service.endpoints[0]['publicURL'] - - def pretty_service_names(name): name = name.replace('-', ' ') if name in ['ec2', 's3']: @@ -53,7 +49,7 @@ class EndpointsTable(tables.DataTable): api_name = tables.Column('type', verbose_name=_("Service"), filters=(pretty_service_names,)) - api_endpoint = tables.Column(get_endpoint, + api_endpoint = tables.Column('public_url', verbose_name=_("Service Endpoint")) class Meta: diff --git a/openstack_dashboard/dashboards/project/access_and_security/tabs.py b/openstack_dashboard/dashboards/project/access_and_security/tabs.py index 90420bac2..66dd6300e 100644 --- a/openstack_dashboard/dashboards/project/access_and_security/tabs.py +++ b/openstack_dashboard/dashboards/project/access_and_security/tabs.py @@ -117,7 +117,9 @@ class APIAccessTab(tabs.TableTab): services = [] for i, service in enumerate(self.request.user.service_catalog): service['id'] = i - services.append(keystone.Service(service)) + services.append( + keystone.Service(service, self.request.user.services_region)) + return services diff --git a/openstack_dashboard/test/api_tests/base_tests.py b/openstack_dashboard/test/api_tests/base_tests.py index 13493f9c8..c6aa0460d 100644 --- a/openstack_dashboard/test/api_tests/base_tests.py +++ b/openstack_dashboard/test/api_tests/base_tests.py @@ -140,3 +140,25 @@ class ApiHelperTests(test.TestCase): 'Select a new nonexistent service catalog key') with self.assertRaises(exceptions.ServiceCatalogException): url = api_base.url_for(self.request, 'notAnApi') + + self.request.user.services_region = "RegionTwo" + url = api_base.url_for(self.request, 'compute') + self.assertEqual(url, 'http://public.nova2.example.com:8774/v2') + + self.request.user.services_region = "RegionTwo" + url = api_base.url_for(self.request, 'compute', + endpoint_type='adminURL') + self.assertEqual(url, 'http://admin.nova2.example.com:8774/v2') + + self.request.user.services_region = "RegionTwo" + with self.assertRaises(exceptions.ServiceCatalogException): + url = api_base.url_for(self.request, 'image') + + self.request.user.services_region = "bogus_value" + url = api_base.url_for(self.request, 'identity', + endpoint_type='adminURL') + self.assertEqual(url, 'http://admin.keystone.example.com:35357/v2.0') + + self.request.user.services_region = "bogus_value" + with self.assertRaises(exceptions.ServiceCatalogException): + url = api_base.url_for(self.request, 'image') diff --git a/openstack_dashboard/test/api_tests/keystone_tests.py b/openstack_dashboard/test/api_tests/keystone_tests.py index de8aafe93..02223d454 100644 --- a/openstack_dashboard/test/api_tests/keystone_tests.py +++ b/openstack_dashboard/test/api_tests/keystone_tests.py @@ -91,10 +91,28 @@ class ServiceAPITests(test.APITestCase): catalog = self.service_catalog identity_data = api.base.get_service_from_catalog(catalog, "identity") identity_data['id'] = 1 - service = api.keystone.Service(identity_data) + region = identity_data["endpoints"][0]["region"] + service = api.keystone.Service(identity_data, region) self.assertEqual(unicode(service), u"identity (native backend)") self.assertEqual(service.region, identity_data["endpoints"][0]["region"]) self.assertEqual(service.url, "http://int.keystone.example.com:5000/v2.0") + self.assertEqual(service.public_url, + "http://public.keystone.example.com:5000/v2.0") self.assertEqual(service.host, "int.keystone.example.com") + + def test_service_wrapper_service_in_region(self): + catalog = self.service_catalog + compute_data = api.base.get_service_from_catalog(catalog, "compute") + compute_data['id'] = 1 + region = compute_data["endpoints"][1]["region"] + service = api.keystone.Service(compute_data, region) + self.assertEqual(unicode(service), u"compute") + self.assertEqual(service.region, + compute_data["endpoints"][1]["region"]) + self.assertEqual(service.url, + "http://int.nova2.example.com:8774/v2") + self.assertEqual(service.public_url, + "http://public.nova2.example.com:8774/v2") + self.assertEqual(service.host, "int.nova2.example.com") diff --git a/openstack_dashboard/test/test_data/keystone_data.py b/openstack_dashboard/test/test_data/keystone_data.py index 38f3f67a8..d7340fce6 100644 --- a/openstack_dashboard/test/test_data/keystone_data.py +++ b/openstack_dashboard/test/test_data/keystone_data.py @@ -40,12 +40,20 @@ SERVICE_CATALOG = [ {"region": "RegionOne", "adminURL": "http://admin.nova.example.com:8774/v2", "internalURL": "http://int.nova.example.com:8774/v2", - "publicURL": "http://public.nova.example.com:8774/v2"}]}, + "publicURL": "http://public.nova.example.com:8774/v2"}, + {"region": "RegionTwo", + "adminURL": "http://admin.nova2.example.com:8774/v2", + "internalURL": "http://int.nova2.example.com:8774/v2", + "publicURL": "http://public.nova2.example.com:8774/v2"}]}, {"type": "volume", "name": "nova", "endpoints_links": [], "endpoints": [ {"region": "RegionOne", + "adminURL": "http://admin.nova.example.com:8776/v1", + "internalURL": "http://int.nova.example.com:8776/v1", + "publicURL": "http://public.nova.example.com:8776/v1"}, + {"region": "RegionTwo", "adminURL": "http://admin.nova.example.com:8776/v1", "internalURL": "http://int.nova.example.com:8776/v1", "publicURL": "http://public.nova.example.com:8776/v1"}]},