Implementing multi-region support.

* Multiple regions are optionally setup in local_settings.py
  * fixes bug 920761

Change-Id: I7a5ef182a0fd00369414491a88446f7dd94ae679
This commit is contained in:
jakedahn 2012-01-16 11:30:19 -08:00
parent fe0a70ead2
commit ff7c1ceb85
11 changed files with 106 additions and 12 deletions

View File

@ -34,6 +34,11 @@ from horizon.api.deprecated import check_openstackx
LOG = logging.getLogger(__name__)
def _get_endpoint_url(request):
return request.session.get('region_endpoint',
getattr(settings, 'OPENSTACK_KEYSTONE_URL'))
class Tenant(APIResourceWrapper):
"""Simple wrapper around keystoneclient.tenants.Tenant"""
_attrs = ['id', 'description', 'enabled', 'name']
@ -93,7 +98,7 @@ def keystoneclient(request, username=None, password=None, tenant_id=None,
password=password,
tenant_id=tenant_id or user.tenant_id,
token=token_id or user.token,
auth_url=settings.OPENSTACK_KEYSTONE_URL,
auth_url=_get_endpoint_url(request),
endpoint=endpoint)
request._keystone = conn
@ -110,7 +115,7 @@ def keystoneclient(request, username=None, password=None, tenant_id=None,
endpoint = catalog.url_for(service_type='identity',
endpoint_type='publicURL')
else:
endpoint = settings.OPENSTACK_KEYSTONE_URL
endpoint = _get_endpoint_url(request)
conn.management_url = endpoint
return conn
@ -144,7 +149,7 @@ def tenant_update(request, tenant_id, tenant_name, description, enabled):
def tenant_list_for_token(request, token, endpoint_type=None):
c = keystoneclient(request,
token_id=token,
endpoint=settings.OPENSTACK_KEYSTONE_URL,
endpoint=_get_endpoint_url(request),
endpoint_type=endpoint_type)
return [Tenant(t) for t in c.tenants.list()]
@ -160,7 +165,7 @@ def token_create(request, tenant, username, password):
username=username,
password=password,
tenant_id=tenant,
endpoint=settings.OPENSTACK_KEYSTONE_URL)
endpoint=_get_endpoint_url(request))
token = c.tokens.authenticate(username=username,
password=password,
tenant_id=tenant)
@ -174,8 +179,10 @@ def token_create_scoped(request, tenant, token):
'''
if hasattr(request, '_keystone'):
del request._keystone
c = keystoneclient(request, tenant_id=tenant, token_id=token,
endpoint=settings.OPENSTACK_KEYSTONE_URL)
c = keystoneclient(request,
tenant_id=tenant,
token_id=token,
endpoint=_get_endpoint_url(request))
raw_token = c.tokens.authenticate(tenant_id=tenant,
token=token,
return_raw=True)
@ -247,8 +254,8 @@ def _get_roleref(request, user_id, tenant_id, role):
def role_add_for_tenant_user(request, tenant_id, user_id, role):
role = _get_role(request, role)
return keystoneclient(request).roles.add_user_to_tenant(tenant_id,
user_id,
role.id)
user_id,
role.id)
def role_delete_for_tenant_user(request, tenant_id, user_id, role):

View File

@ -74,4 +74,11 @@ def horizon(request):
# supporting keystone integration.
context['network_configured'] = getattr(settings, 'QUANTUM_ENABLED', None)
# Region context/support
available_regions = getattr(settings, 'AVAILABLE_REGIONS', None)
regions = {'support': available_regions > 1,
'endpoint': request.session.get('region_endpoint'),
'name': request.session.get('region_name')}
context['region'] = regions
return context

View File

@ -142,6 +142,7 @@ class TestCase(django_test.TestCase):
service_catalog=self.TEST_SERVICE_CATALOG,
authorized_tenants=tenants)
self.request = http.HttpRequest()
self.request.session = self.client._session()
middleware.HorizonMiddleware().process_request(self.request)
def tearDown(self):

View File

@ -55,6 +55,7 @@ class AuthViewTests(test.BaseViewTests):
TOKEN_ID = 1
form_data = {'method': 'Login',
'region': 'http://localhost:5000/v2.0,local',
'password': self.PASSWORD,
'username': self.TEST_USER}
@ -94,6 +95,7 @@ class AuthViewTests(test.BaseViewTests):
TOKEN_ID = 1
form_data = {'method': 'Login',
'region': 'http://localhost:5000/v2.0,local',
'password': self.PASSWORD,
'username': self.TEST_USER}
@ -125,9 +127,7 @@ class AuthViewTests(test.BaseViewTests):
aToken.id).AndReturn(bToken)
self.mox.ReplayAll()
res = self.client.post(reverse('horizon:auth_login'), form_data)
self.assertRedirectsNoFollow(res, DASH_INDEX_URL)
def test_login_invalid_credentials(self):
@ -139,6 +139,7 @@ class AuthViewTests(test.BaseViewTests):
self.mox.ReplayAll()
form_data = {'method': 'Login',
'region': 'http://localhost:5000/v2.0,local',
'password': self.PASSWORD,
'username': self.TEST_USER}
res = self.client.post(reverse('horizon:auth_login'),
@ -158,6 +159,7 @@ class AuthViewTests(test.BaseViewTests):
self.mox.ReplayAll()
form_data = {'method': 'Login',
'region': 'http://localhost:5000/v2.0,local',
'password': self.PASSWORD,
'username': self.TEST_USER}
res = self.client.post(reverse('horizon:auth_login'), form_data)
@ -205,6 +207,7 @@ class AuthViewTests(test.BaseViewTests):
self.mox.ReplayAll()
form_data = {'method': 'LoginWithTenant',
'region': 'http://localhost:5000/v2.0,local',
'password': self.PASSWORD,
'tenant': NEW_TENANT_ID,
'username': self.TEST_USER}

View File

@ -100,6 +100,11 @@ SWIFT_USER = 'tester'
SWIFT_PASS = 'testing'
SWIFT_AUTHURL = 'http://swift/swiftapi/v1.0'
AVAILABLE_REGIONS = [
('local', 'http://localhost:5000/v2.0'),
('remote', 'http://remote:5000/v2.0'),
]
OPENSTACK_ADDRESS = "localhost"
OPENSTACK_ADMIN_TOKEN = "openstack"
OPENSTACK_KEYSTONE_URL = "http://%s:5000/v2.0" % OPENSTACK_ADDRESS

View File

@ -21,6 +21,7 @@
import logging
from django import shortcuts
from django.conf import settings
from django.utils.translation import ugettext as _
import horizon

View File

@ -25,6 +25,7 @@ Forms used for Horizon's auth mechanisms.
import logging
from django import shortcuts
from django.conf import settings
from django.contrib import messages
from django.utils.translation import ugettext as _
from keystoneclient import exceptions as keystone_exceptions
@ -49,6 +50,16 @@ def _set_session_data(request, token):
request.session['roles'] = token.user['roles']
def _regions_supported():
if len(getattr(settings, 'AVAILABLE_REGIONS', [])) > 1:
return True
region_field = forms.ChoiceField(widget=forms.Select,
choices=[('%s,%s' % (region[1], region[0]), region[0])
for region in getattr(settings, 'AVAILABLE_REGIONS', [])])
class Login(forms.SelfHandlingForm):
""" Form used for logging in a user.
@ -58,11 +69,18 @@ class Login(forms.SelfHandlingForm):
Subclass of :class:`~horizon.forms.SelfHandlingForm`.
"""
if _regions_supported():
region = region_field
username = forms.CharField(max_length="20", label=_("User Name"))
password = forms.CharField(max_length="20", label=_("Password"),
widget=forms.PasswordInput(render_value=False))
def handle(self, request, data):
region = data.get('region', '').split(',')
if len(region) > 1:
request.session['region_endpoint'] = region[0]
request.session['region_name'] = region[1]
if data.get('tenant', None):
try:
token = api.token_create(request,
@ -137,7 +155,7 @@ class Login(forms.SelfHandlingForm):
_set_session_data(request, token)
user = users.get_user_from_request(request)
return shortcuts.redirect(base.Horizon.get_user_home(user))
return shortcuts.redirect(base.Horizon.get_user_home(user))
class LoginWithTenant(Login):
@ -145,6 +163,8 @@ class LoginWithTenant(Login):
Exactly like :class:`.Login` but includes the tenant id as a field
so that the process of choosing a default tenant is bypassed.
"""
if _regions_supported():
region = region_field
username = forms.CharField(max_length="20",
widget=forms.TextInput(attrs={'readonly': 'readonly'}))
tenant = forms.CharField(widget=forms.HiddenInput())

View File

@ -276,6 +276,7 @@ small {
padding-right: 25px;
padding-top: 25px;
color: #888;
right: 145px;
}
#user_info a {
@ -901,7 +902,41 @@ a.view_full {
border-bottom: solid 1px #E1E1E1;
background-image: url(/static/dashboard/images/drop_arrow.png);
}
form div.clearfix.error {
width: 330px;
padding: 10px 10px;
}
/* Region selector in header */
#region_selector {
position: absolute;
z-index: 9999;
right: 0;
top: 24px;
}
#region_selector a { margin-left: 0; }
#region_selector ul{
float: left;
margin-left: 5px;
padding-right: 21px;
width: 125px;
}
#region_selector ul:hover a { display: block; }
#region_selector li a{
padding: 3px 3px 3px 5px;
display: none;
background: #E1E1E1;
margin-top: -10px;
}
#region_selector li:first-child p{
background: #EDEDED url(/static/dashboard/images/drop_arrow.png) no-repeat 106px 9px !important;
display: block;
border: 1px solid #e1e1e1;
padding: 5px;
}

View File

@ -37,7 +37,14 @@
<a href="{% url horizon:settings:user:index %}">{% trans "Settings" %}</a>
<a href="{% url horizon:auth_logout %}">{% trans "Sign Out" %}</a>
</p>
</div>
<div id="region_selector">
{% if region.support %}
<ul>
<li class="odd"><p>{{ region.name }}</p></li>
<li><a href="{% url horizon:auth_logout %}">Change Regions</a></li>
</ul>
{% endif %}
</div>
{% endblock %}
{% block content %}

View File

@ -36,6 +36,14 @@ HORIZON_CONFIG = {
'user_home': 'dashboard.views.user_home',
}
# For multiple regions uncomment this configuration, and add (title, endpoint).
# AVAILABLE_REGIONS = [
# ('cluster1', 'http://cluster1.com:5000/v2.0'),
# ('cluster2', 'http://cluster2.com:5000/v2.0'),
# ]
# FIXME: This is only here so quantum still works. It must be refactored to
# work with multiple regions.
OPENSTACK_HOST = "127.0.0.1"
OPENSTACK_KEYSTONE_URL = "http://%s:5000/v2.0" % OPENSTACK_HOST
# FIXME: this is only needed until keystone fixes its GET /tenants call