diff --git a/horizon/horizon/api/keystone.py b/horizon/horizon/api/keystone.py index 44772ae29d6..f420442729b 100644 --- a/horizon/horizon/api/keystone.py +++ b/horizon/horizon/api/keystone.py @@ -26,10 +26,12 @@ from keystoneclient.v2_0 import client as keystone_client from keystoneclient.v2_0 import tokens -from horizon.api.base import * +from horizon import exceptions +from horizon.api import APIResourceWrapper LOG = logging.getLogger(__name__) +DEFAULT_ROLE = None def _get_endpoint_url(request): @@ -47,11 +49,6 @@ class User(APIResourceWrapper): _attrs = ['email', 'enabled', 'id', 'tenantId', 'name'] -class Role(APIResourceWrapper): - """Wrapper around keystoneclient.roles.role""" - _attrs = ['id', 'name', 'description', 'service_id'] - - class Services(APIResourceWrapper): _attrs = ['disabled', 'host', 'id', 'last_update', 'stats', 'type', 'up', 'zone'] @@ -226,34 +223,41 @@ def user_update_tenant(request, user_id, tenant_id): .update_tenant(user_id, tenant_id)) -def _get_role(request, name): - roles = keystoneclient(request).roles.list() - for role in roles: - if role.name.lower() == name.lower(): - return role - - raise Exception(_('Role does not exist: %s') % name) +def role_list(request): + """ Returns a global list of available roles. """ + return keystoneclient(request).roles.list() -def _get_roleref(request, user_id, tenant_id, role): - rolerefs = keystoneclient(request).roles.get_user_role_refs(user_id) - for roleref in rolerefs: - if roleref.roleId == role.id and roleref.tenantId == tenant_id: - return roleref - raise Exception(_('Role "%s" does not exist for that user on this tenant.') - % role.name) +def add_tenant_user_role(request, tenant_id, user_id, role_id): + """ Adds a role for a user on a tenant. """ + return keystoneclient(request).roles.add_user_role(user_id, + role_id, + tenant_id) -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) +def remove_tenant_user(request, tenant_id, user_id): + """ Removes all roles from a user on a tenant, removing them from it. """ + client = keystoneclient(request) + roles = client.roles.roles_for_user(user_id, tenant_id) + for role in roles: + client.roles.remove_user_role(user_id, role.id, tenant_id) -def role_delete_for_tenant_user(request, tenant_id, user_id, role): - role = _get_role(request, role) - roleref = _get_roleref(request, user_id, tenant_id, role) - return keystoneclient(request).roles.remove_user_from_tenant(tenant_id, - user_id, - roleref.id) +def get_default_role(request): + """ + Gets the default role object from Keystone and saves it as a global + since this is configured in settings and should not change from request + to request. Supports lookup by name or id. + """ + global DEFAULT_ROLE + default = getattr(settings, "OPENSTACK_KEYSTONE_DEFAULT_ROLE", None) + if default and DEFAULT_ROLE is None: + try: + roles = keystoneclient(request).roles.list() + except: + exceptions.handle(request) + for role in roles: + if role.id == default or role.name == default: + DEFAULT_ROLE = role + break + return DEFAULT_ROLE diff --git a/horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/_add_user.html b/horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/_add_user.html index 020d70335e2..3ccae3aa066 100644 --- a/horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/_add_user.html +++ b/horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/_add_user.html @@ -1,10 +1,25 @@ +{% extends "horizon/common/_modal_form.html" %} {% load i18n %} -
- {% csrf_token %} - {% for hidden in form.hidden_fields %} - {{ hidden }} - {% endfor %} - - - -
+ +{% block form_id %}add_user_form{% endblock %} +{% block form_action %}{% url horizon:syspanel:tenants:add_user tenant_id user_id %}{% endblock %} + +{% block modal_id %}add_user_modal{% endblock %} +{% block modal-header %}{% trans "Add User To Tenant" %}{% endblock %} + +{% block modal-body %} +
+
+ {% include "horizon/common/_form_fields.html" %} +
+
+
+

{% trans "Description" %}:

+

{% trans "Select the user role for the tenant." %}

+
+{% endblock %} + +{% block modal-footer %} + + {% trans "Cancel" %} +{% endblock %} diff --git a/horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/_remove_user.html b/horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/_remove_user.html deleted file mode 100644 index 130f0a13b77..00000000000 --- a/horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/_remove_user.html +++ /dev/null @@ -1,10 +0,0 @@ -{% load i18n %} -
- {% csrf_token %} - {% for hidden in form.hidden_fields %} - {{ hidden }} - {% endfor %} - - - -
diff --git a/horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/add_user.html b/horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/add_user.html new file mode 100644 index 00000000000..781698d24c2 --- /dev/null +++ b/horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/add_user.html @@ -0,0 +1,11 @@ +{% extends 'syspanel/base.html' %} +{% load i18n %} +{% block title %}{% trans "Add User To Tenant" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Add User To Tenant") %} +{% endblock page_header %} + +{% block syspanel_main %} + {% include 'syspanel/tenants/_add_user.html' %} +{% endblock %} diff --git a/horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/users.html b/horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/users.html index 5e9efa14873..86014f195e5 100644 --- a/horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/users.html +++ b/horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/users.html @@ -4,64 +4,15 @@ {% block page_header %} {% endblock %} {% block syspanel_main %} -
- - {% if users %} - - - - - - - - - {% for user in users %} - - - - - - - {% endfor %} - -
{% trans "ID" %}{% trans "Name" %}{% trans "Email" %}{% trans "Actions" %}
{{ user.id }}{{ user.name }}{{ user.email }} -
    -
  • {% include "syspanel/tenants/_remove_user.html" with form=remove_user_form %}
  • -
-
- {% else %} -
-

{% trans "Info" %}

-

T{% trans "here are currently no users for this tenant" %}

-
- {% endif %} - {% if new_users %} -

{% trans "Add new users" %}

- - - - - - - - {% for user in new_users %} - - - - - - {% endfor %} - -
{% trans "ID" %}{% trans "Name" %}{% trans "Actions" %}
{{ user.id }}{{ user.name }} -
    -
  • {% include "syspanel/tenants/_add_user.html" with form=add_user_form %}
  • -
-
- {% endif %} - +
+ {{ tenant_users_table.render }} +
+
+ {{ add_users_table.render }} +
{% endblock %} diff --git a/horizon/horizon/dashboards/syspanel/tenants/forms.py b/horizon/horizon/dashboards/syspanel/tenants/forms.py index 0a40fd87ca9..640105c2942 100644 --- a/horizon/horizon/dashboards/syspanel/tenants/forms.py +++ b/horizon/horizon/dashboards/syspanel/tenants/forms.py @@ -21,7 +21,6 @@ import logging from django import shortcuts -from django.conf import settings from django.contrib import messages from django.utils.translation import ugettext as _ @@ -34,43 +33,27 @@ class AddUser(forms.SelfHandlingForm): - user = forms.CharField() - tenant = forms.CharField() + tenant_id = forms.CharField(widget=forms.widgets.HiddenInput()) + user_id = forms.CharField(widget=forms.widgets.HiddenInput()) + role_id = forms.ChoiceField(label=_("Role")) - def handle(self, request, data): - try: - api.role_add_for_tenant_user( - request, - data['tenant'], - data['user'], - settings.OPENSTACK_KEYSTONE_DEFAULT_ROLE) - messages.success(request, - _('%(user)s was successfully added to %(tenant)s.') - % {"user": data['user'], "tenant": data['tenant']}) - except: - exceptions.handle(request, _('Unable to add user to tenant.')) - return shortcuts.redirect('horizon:syspanel:tenants:users', - tenant_id=data['tenant']) - - -class RemoveUser(forms.SelfHandlingForm): - user = forms.CharField() - tenant = forms.CharField() + def __init__(self, *args, **kwargs): + roles = kwargs.pop('roles') + super(AddUser, self).__init__(*args, **kwargs) + role_choices = [(role.id, role.name) for role in roles] + self.fields['role_id'].choices = role_choices def handle(self, request, data): try: - api.role_delete_for_tenant_user( - request, - data['tenant'], - data['user'], - settings.OPENSTACK_KEYSTONE_DEFAULT_ROLE) - messages.success(request, - _('%(user)s was successfully removed from %(tenant)s.') - % {"user": data['user'], "tenant": data['tenant']}) + api.add_tenant_user_role(request, + data['tenant_id'], + data['user_id'], + data['role_id']) + messages.success(request, _('Successfully added user to tenant.')) except: - exceptions.handle(request, _('Unable to remove user from tenant.')) + exceptions.handle(request, _('Unable to add user to tenant.')) return shortcuts.redirect('horizon:syspanel:tenants:users', - tenant_id=data['tenant']) + tenant_id=data['tenant_id']) class CreateTenant(forms.SelfHandlingForm): diff --git a/horizon/horizon/dashboards/syspanel/tenants/tables.py b/horizon/horizon/dashboards/syspanel/tenants/tables.py index ea354c0cfbd..8fdd84b9d97 100644 --- a/horizon/horizon/dashboards/syspanel/tenants/tables.py +++ b/horizon/horizon/dashboards/syspanel/tenants/tables.py @@ -1,10 +1,13 @@ import logging +from django.core.urlresolvers import reverse from django.utils.translation import ugettext_lazy as _ from horizon import api from horizon import tables +from ..users.tables import UsersTable + LOG = logging.getLogger(__name__) @@ -18,7 +21,7 @@ class ModifyQuotasLink(tables.LinkAction): class ViewMembersLink(tables.LinkAction): name = "users" - verbose_name = _("View Members") + verbose_name = _("Modify Users") url = "horizon:syspanel:tenants:users" @@ -30,7 +33,7 @@ class UsageLink(tables.LinkAction): class EditLink(tables.LinkAction): name = "update" - verbose_name = _("Edit") + verbose_name = _("Edit Tenant") url = "horizon:syspanel:tenants:update" attrs = {"class": "ajax-modal"} @@ -77,3 +80,43 @@ class Meta: row_actions = (EditLink, UsageLink, ViewMembersLink, ModifyQuotasLink, DeleteTenantsAction) table_actions = (TenantFilterAction, CreateLink, DeleteTenantsAction) + + +class RemoveUserAction(tables.BatchAction): + name = "remove_user" + action_present = _("Remove") + action_past = _("Removed") + data_type_singular = _("User") + data_type_plural = _("Users") + classes = ('danger',) + + def action(self, request, user_id): + tenant_id = self.table.kwargs['tenant_id'] + api.keystone.remove_tenant_user(request, tenant_id, user_id) + + +class TenantUsersTable(UsersTable): + class Meta: + name = "tenant_users" + verbose_name = _("Users For Tenant") + table_actions = (RemoveUserAction,) + row_actions = (RemoveUserAction,) + + +class AddUserAction(tables.LinkAction): + name = "add_user" + verbose_name = _("Add To Tenant") + url = "horizon:syspanel:tenants:add_user" + classes = ('ajax-modal',) + + def get_link_url(self, user): + tenant_id = self.table.kwargs['tenant_id'] + return reverse(self.url, args=(tenant_id, user.id)) + + +class AddUsersTable(UsersTable): + class Meta: + name = "add_users" + verbose_name = _("Add New Users") + table_actions = () + row_actions = (AddUserAction,) diff --git a/horizon/horizon/dashboards/syspanel/tenants/urls.py b/horizon/horizon/dashboards/syspanel/tenants/urls.py index ef68c1388b9..18bf748079a 100644 --- a/horizon/horizon/dashboards/syspanel/tenants/urls.py +++ b/horizon/horizon/dashboards/syspanel/tenants/urls.py @@ -20,7 +20,8 @@ from django.conf.urls.defaults import patterns, url -from .views import IndexView, CreateView, UpdateView, QuotasView +from .views import (IndexView, CreateView, UpdateView, QuotasView, UsersView, + AddUserView) urlpatterns = patterns('horizon.dashboards.syspanel.tenants.views', @@ -28,8 +29,10 @@ url(r'^create$', CreateView.as_view(), name='create'), url(r'^(?P[^/]+)/update/$', UpdateView.as_view(), name='update'), - url(r'^(?P[^/]+)/users/$', 'users', name='users'), url(r'^(?P[^/]+)/quotas/$', QuotasView.as_view(), name='quotas'), - url(r'^(?P[^/]+)/usage/$', 'usage', name='usage') + url(r'^(?P[^/]+)/usage/$', 'usage', name='usage'), + url(r'^(?P[^/]+)/users/$', UsersView.as_view(), name='users'), + url(r'^(?P[^/]+)/users/(?P[^/]+)/add/$', + AddUserView.as_view(), name='add_user') ) diff --git a/horizon/horizon/dashboards/syspanel/tenants/views.py b/horizon/horizon/dashboards/syspanel/tenants/views.py index f56599ed8e1..5fc4ce27e6f 100644 --- a/horizon/horizon/dashboards/syspanel/tenants/views.py +++ b/horizon/horizon/dashboards/syspanel/tenants/views.py @@ -20,20 +20,21 @@ import datetime import logging +import operator from django import shortcuts from django import http -from django.conf import settings from django.contrib import messages +from django.core.urlresolvers import reverse from django.utils.translation import ugettext as _ from keystoneclient import exceptions as api_exceptions from horizon import api +from horizon import exceptions from horizon import forms from horizon import tables -from .forms import (AddUser, RemoveUser, CreateTenant, UpdateTenant, - UpdateQuotas) -from .tables import TenantsTable +from .forms import AddUser, CreateTenant, UpdateTenant, UpdateQuotas +from .tables import TenantsTable, TenantUsersTable, AddUsersTable from horizon.dashboards.syspanel.overview.views import GlobalSummary @@ -90,26 +91,70 @@ def get_initial(self): 'enabled': self.object.enabled} -def users(request, tenant_id): - for f in (AddUser, RemoveUser,): - form, handled = f.maybe_handle(request) - if handled: - return handled +class UsersView(tables.MultiTableView): + table_classes = (TenantUsersTable, AddUsersTable) + template_name = 'syspanel/tenants/users.html' - add_user_form = AddUser() - remove_user_form = RemoveUser() + def get_data(self, *args, **kwargs): + tenant_id = self.kwargs["tenant_id"] + try: + self.tenant = api.keystone.tenant_get(self.request, tenant_id) + self.all_users = api.keystone.user_list(self.request) + self.tenant_users = api.keystone.user_list(self.request, tenant_id) + except: + redirect = reverse("horizon:syspanel:tenants:index") + exceptions.handle(self.request, + _("Unable to retrieve users."), + redirect=redirect) + return super(UsersView, self).get_data(*args, **kwargs) + + def get_tenant_users_data(self): + return self.tenant_users + + def get_add_users_data(self): + tenant_user_ids = [user.id for user in self.tenant_users] + return [user for user in self.all_users if + user.id not in tenant_user_ids] + + def get_context_data(self, **kwargs): + context = super(UsersView, self).get_context_data(**kwargs) + context['tenant'] = self.tenant + return context + + +class AddUserView(forms.ModalFormView): + form_class = AddUser + template_name = 'syspanel/tenants/add_user.html' + context_object_name = 'tenant' + + def get_object(self, *args, **kwargs): + return api.keystone.tenant_get(self.request, kwargs["tenant_id"]) - users = api.user_list(request, tenant_id) - all_users = api.user_list(request) - user_ids = [u.id for u in users] - new_users = [u for u in all_users if not u.id in user_ids] - return shortcuts.render(request, - 'syspanel/tenants/users.html', { - 'add_user_form': add_user_form, - 'remove_user_form': remove_user_form, - 'tenant_id': tenant_id, - 'users': users, - 'new_users': new_users}) + def get_context_data(self, **kwargs): + context = super(AddUserView, self).get_context_data(**kwargs) + context['tenant_id'] = self.kwargs["tenant_id"] + context['user_id'] = self.kwargs["user_id"] + return context + + def get_form_kwargs(self): + kwargs = super(AddUserView, self).get_form_kwargs() + try: + roles = api.keystone.role_list(self.request) + except: + redirect = reverse("horizon:syspanel:tenants:users", + args=(self.kwargs["tenant_id"],)) + exceptions.handle(self.request, + _("Unable to retrieve roles."), + redirect=redirect) + roles.sort(key=operator.attrgetter("id")) + kwargs['roles'] = roles + return kwargs + + def get_initial(self): + default_role = api.keystone.get_default_role(self.request) + return {'tenant_id': self.kwargs['tenant_id'], + 'user_id': self.kwargs['user_id'], + 'role_id': getattr(default_role, "id", None)} class QuotasView(forms.ModalFormView): @@ -151,7 +196,6 @@ def usage(request, tenant_id): if date_start > GlobalSummary.current_month(): messages.error(request, _('No data for the selected period')) - date_end = date_start datetime_end = datetime_start usage = {} diff --git a/horizon/horizon/dashboards/syspanel/users/forms.py b/horizon/horizon/dashboards/syspanel/users/forms.py index ce424290a5d..08f9aa1b030 100644 --- a/horizon/horizon/dashboards/syspanel/users/forms.py +++ b/horizon/horizon/dashboards/syspanel/users/forms.py @@ -21,11 +21,11 @@ import logging from django import shortcuts -from django.conf import settings from django.contrib import messages from django.utils.translation import ugettext as _ from horizon import api +from horizon import exceptions from horizon import forms @@ -68,29 +68,18 @@ def handle(self, request, data): _('User "%s" was successfully created.') % data['name']) try: - api.role_add_for_tenant_user( - request, data['tenant_id'], new_user.id, - settings.OPENSTACK_KEYSTONE_DEFAULT_ROLE) - except Exception, e: - LOG.exception('Exception while assigning \ - role to new user: %s' % new_user.id) - if not hasattr(e, 'message'): - e.message = str(e) - messages.error(request, - _('Error assigning role to user: %s') - % e.message) - + default_role = api.keystone.get_default_role(request) + if default_role: + api.add_tenant_user_role(request, + data['tenant_id'], + new_user.id, + default_role.id) + except: + exceptions.handle(request, + _('Unable to add user to primary tenant.')) return shortcuts.redirect('horizon:syspanel:users:index') - - except Exception, e: - LOG.exception('Exception while creating user\n' - 'name: "%s", email: "%s", tenant_id: "%s"' % - (data['name'], data['email'], data['tenant_id'])) - if not hasattr(e, 'message'): - e.message = str(e) - messages.error(request, - _('Error creating user: %s') - % e.message) + except: + exceptions.handle(request, _('Unable to create user.')) return shortcuts.redirect('horizon:syspanel:users:index') diff --git a/horizon/horizon/tables/views.py b/horizon/horizon/tables/views.py index 0a3e19a647d..7d403f7884a 100644 --- a/horizon/horizon/tables/views.py +++ b/horizon/horizon/tables/views.py @@ -58,9 +58,9 @@ def get_tables(self): table_func = getattr(self, func_name, None) data = self.get_data()[table._meta.name] if table_func is None: - tbl = table(self.request, data) + tbl = table(self.request, data, **self.kwargs) else: - tbl = table_func(self, self.request, data) + tbl = table_func(self, self.request, data, **self.kwargs) self._tables[table._meta.name] = tbl return self._tables diff --git a/horizon/horizon/tests/api_tests/keystone.py b/horizon/horizon/tests/api_tests/keystone.py index a9759ce7272..285b78d9e3f 100644 --- a/horizon/horizon/tests/api_tests/keystone.py +++ b/horizon/horizon/tests/api_tests/keystone.py @@ -20,13 +20,11 @@ from __future__ import absolute_import -from django import http from django.conf import settings -from keystoneclient.v2_0 import tenants as keystoneclient_tenants -from mox import IsA +from keystoneclient.v2_0.roles import Role, RoleManager from horizon import api -from horizon.tests.api_tests.utils import (APITestCase, APIResource, +from horizon.tests.api_tests.utils import (APITestCase, TEST_RETURN, TEST_URL, TEST_USERNAME, TEST_TENANT_ID, TEST_TOKEN_ID, TEST_TENANT_NAME, TEST_PASSWORD, TEST_EMAIL) @@ -83,26 +81,47 @@ def test_token_create(self): class RoleAPITests(APITestCase): - def test_role_add_for_tenant_user(self): + def setUp(self): + super(RoleAPITests, self).setUp() + self.role = Role(RoleManager, + {'id': '2', + 'name': settings.OPENSTACK_KEYSTONE_DEFAULT_ROLE}) + self.roles = (self.role,) + + def test_remove_tenant_user(self): + """ + Tests api.keystone.remove_tenant_user. + + Verifies that remove_tenant_user is called with the right arguments + after iterating the user's roles. + + There are no assertions in this test because the checking is handled + by mox in the VerifyAll() call in tearDown(). + """ keystoneclient = self.stub_keystoneclient() - role = api.Role(APIResource.get_instance()) - role.id = TEST_RETURN - role.name = TEST_RETURN - keystoneclient.roles = self.mox.CreateMockAnything() - keystoneclient.roles.add_user_to_tenant(TEST_TENANT_ID, - TEST_USERNAME, - TEST_RETURN).AndReturn(role) - api.keystone._get_role = self.mox.CreateMockAnything() - api.keystone._get_role(IsA(http.HttpRequest), IsA(str)).AndReturn(role) + keystoneclient.roles.roles_for_user(TEST_USERNAME, TEST_TENANT_ID) \ + .AndReturn(self.roles) + keystoneclient.roles.remove_user_role(TEST_USERNAME, + self.role.id, + TEST_TENANT_ID) \ + .AndReturn(self.role) + self.mox.ReplayAll() + api.keystone.remove_tenant_user(self.request, + TEST_TENANT_ID, + TEST_USERNAME) + def test_get_default_role(self): + keystoneclient = self.stub_keystoneclient() + keystoneclient.roles = self.mox.CreateMockAnything() + keystoneclient.roles.list().AndReturn(self.roles) self.mox.ReplayAll() - ret_val = api.role_add_for_tenant_user(self.request, - TEST_TENANT_ID, - TEST_USERNAME, - TEST_RETURN) - self.assertEqual(ret_val, role) + role = api.keystone.get_default_role(self.request) + self.assertEqual(role, self.role) + # Verify that a second call doesn't hit the API again, + # (it would show up in mox as an unexpected method call) + role = api.keystone.get_default_role(self.request) class UserAPITests(APITestCase): diff --git a/horizon/horizon/tests/testsettings.py b/horizon/horizon/tests/testsettings.py index 6c03c4d7fef..1e3f9f91594 100644 --- a/horizon/horizon/tests/testsettings.py +++ b/horizon/horizon/tests/testsettings.py @@ -109,7 +109,7 @@ OPENSTACK_ADMIN_TOKEN = "openstack" OPENSTACK_KEYSTONE_URL = "http://%s:5000/v2.0" % OPENSTACK_ADDRESS OPENSTACK_KEYSTONE_ADMIN_URL = "http://%s:35357/v2.0" % OPENSTACK_ADDRESS -OPENSTACK_KEYSTONE_DEFAULT_ROLE_ID = "2" +OPENSTACK_KEYSTONE_DEFAULT_ROLE = "Member" # Silence logging output during tests. LOGGING = { diff --git a/run_tests.sh b/run_tests.sh index 4a3c33d7658..5884b720dba 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -6,7 +6,7 @@ set -o errexit # Increment me any time the environment should be rebuilt. # This includes dependncy changes, directory renames, etc. # Simple integer secuence: 1, 2, 3... -environment_version=7 +environment_version=8 #--------------------------------------------------------# function usage {