From c119343f6af5b7d9381cc0ee909938eab9598524 Mon Sep 17 00:00:00 2001 From: Lin Hua Cheng Date: Wed, 15 May 2013 14:43:49 -0700 Subject: [PATCH] Adding Domain CRUD in Admin Dashboard. Add basic support for CRUD on Domain for admin users. This feature is only exposed if the user explicitly set the OPENSTACK_KEYSTONE_MULTIDOMAIN_SUPPORT to True and keystone supports V3. Creating projects, users and groups on a specific domain will be covered separately on domain-context blueprint. Implements blueprint admin-domain-crud Change-Id: If2684331776166dd9579716deebca100770a5ce0 --- openstack_dashboard/api/keystone.py | 33 +++ .../dashboards/admin/dashboard.py | 3 +- .../dashboards/admin/domains/__init__.py | 0 .../dashboards/admin/domains/constants.py | 23 ++ .../dashboards/admin/domains/panel.py | 37 ++++ .../dashboards/admin/domains/tables.py | 100 +++++++++ .../domains/templates/domains/index.html | 11 + .../dashboards/admin/domains/tests.py | 196 ++++++++++++++++++ .../dashboards/admin/domains/urls.py | 27 +++ .../dashboards/admin/domains/views.py | 68 ++++++ .../dashboards/admin/domains/workflows.py | 127 ++++++++++++ .../local/local_settings.py.example | 3 +- openstack_dashboard/test/settings.py | 6 +- .../test/test_data/keystone_data.py | 15 ++ 14 files changed, 646 insertions(+), 3 deletions(-) create mode 100644 openstack_dashboard/dashboards/admin/domains/__init__.py create mode 100644 openstack_dashboard/dashboards/admin/domains/constants.py create mode 100644 openstack_dashboard/dashboards/admin/domains/panel.py create mode 100644 openstack_dashboard/dashboards/admin/domains/tables.py create mode 100644 openstack_dashboard/dashboards/admin/domains/templates/domains/index.html create mode 100644 openstack_dashboard/dashboards/admin/domains/tests.py create mode 100644 openstack_dashboard/dashboards/admin/domains/urls.py create mode 100644 openstack_dashboard/dashboards/admin/domains/views.py create mode 100644 openstack_dashboard/dashboards/admin/domains/workflows.py diff --git a/openstack_dashboard/api/keystone.py b/openstack_dashboard/api/keystone.py index ed22f24f0b..888f3fac9f 100644 --- a/openstack_dashboard/api/keystone.py +++ b/openstack_dashboard/api/keystone.py @@ -168,6 +168,34 @@ def keystoneclient(request, admin=False): return conn +def domain_create(request, name, description=None, enabled=None): + manager = keystoneclient(request, admin=True).domains + return manager.create(name, + description=description, + enabled=enabled) + + +def domain_get(request, domain_id): + manager = keystoneclient(request, admin=True).domains + return manager.get(domain_id) + + +def domain_delete(request, domain_id): + manager = keystoneclient(request, admin=True).domains + return manager.delete(domain_id) + + +def domain_list(request): + manager = keystoneclient(request, admin=True).domains + return manager.list() + + +def domain_update(request, domain_id, name=None, description=None, + enabled=None): + manager = keystoneclient(request, admin=True).domains + return manager.update(domain_id, name, description, enabled) + + def tenant_create(request, name, description=None, enabled=None, domain=None): manager = VERSIONS.get_project_manager(request, admin=True) if VERSIONS.active < 3: @@ -399,6 +427,11 @@ def get_user_ec2_credentials(request, user_id, access_token): return keystoneclient(request).ec2.get(user_id, access_token) +def keystone_can_edit_domain(): + backend_settings = getattr(settings, "OPENSTACK_KEYSTONE_BACKEND", {}) + return backend_settings.get('can_edit_domain', True) + + def keystone_can_edit_user(): backend_settings = getattr(settings, "OPENSTACK_KEYSTONE_BACKEND", {}) return backend_settings.get('can_edit_user', True) diff --git a/openstack_dashboard/dashboards/admin/dashboard.py b/openstack_dashboard/dashboards/admin/dashboard.py index 054c250e93..43597defa4 100644 --- a/openstack_dashboard/dashboards/admin/dashboard.py +++ b/openstack_dashboard/dashboards/admin/dashboard.py @@ -23,7 +23,8 @@ class SystemPanels(horizon.PanelGroup): slug = "admin" name = _("System Panel") panels = ('overview', 'instances', 'volumes', 'flavors', - 'images', 'projects', 'users', 'networks', 'routers', 'info') + 'images', 'domains', 'projects', 'users', + 'networks', 'routers', 'info') class Admin(horizon.Dashboard): diff --git a/openstack_dashboard/dashboards/admin/domains/__init__.py b/openstack_dashboard/dashboards/admin/domains/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openstack_dashboard/dashboards/admin/domains/constants.py b/openstack_dashboard/dashboards/admin/domains/constants.py new file mode 100644 index 0000000000..e871ace28b --- /dev/null +++ b/openstack_dashboard/dashboards/admin/domains/constants.py @@ -0,0 +1,23 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 Hewlett-Packard Development Company, L.P. +# +# 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. + +DOMAIN_INFO_FIELDS = ("name", + "description", + "enabled") +DOMAINS_INDEX_URL = 'horizon:admin:domains:index' +DOMAINS_INDEX_VIEW_TEMPLATE = 'admin/domains/index.html' +DOMAINS_CREATE_URL = 'horizon:admin:domains:create' +DOMAINS_UPDATE_URL = 'horizon:admin:domains:update' diff --git a/openstack_dashboard/dashboards/admin/domains/panel.py b/openstack_dashboard/dashboards/admin/domains/panel.py new file mode 100644 index 0000000000..e6e59166c2 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/domains/panel.py @@ -0,0 +1,37 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 Hewlett-Packard Development Company, L.P. +# +# 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. + +from django.conf import settings +from django.utils.translation import ugettext_lazy as _ + +import horizon + +from openstack_dashboard.api.keystone import VERSIONS as IDENTITY_VERSIONS +from openstack_dashboard.dashboards.admin import dashboard + + +class Domains(horizon.Panel): + name = _("Domains") + slug = 'domains' + + +MULTIDOMAIN_SUPPORT = getattr(settings, + 'OPENSTACK_KEYSTONE_MULTIDOMAIN_SUPPORT', + False) + + +if MULTIDOMAIN_SUPPORT and IDENTITY_VERSIONS.active >= 3: + dashboard.Admin.register(Domains) diff --git a/openstack_dashboard/dashboards/admin/domains/tables.py b/openstack_dashboard/dashboards/admin/domains/tables.py new file mode 100644 index 0000000000..787a045f8f --- /dev/null +++ b/openstack_dashboard/dashboards/admin/domains/tables.py @@ -0,0 +1,100 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 Hewlett-Packard Development Company, L.P. +# +# 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 logging + +from django.utils.translation import ugettext_lazy as _ + +from keystoneclient.exceptions import ClientException + +from horizon import messages +from horizon import tables + +from openstack_dashboard import api + +from .constants import DOMAINS_CREATE_URL, \ + DOMAINS_UPDATE_URL + + +LOG = logging.getLogger(__name__) + + +class CreateDomainLink(tables.LinkAction): + name = "create" + verbose_name = _("Create Domain") + url = DOMAINS_CREATE_URL + classes = ("ajax-modal", "btn-create") + + def allowed(self, request, domain): + return api.keystone.keystone_can_edit_domain() + + +class EditDomainLink(tables.LinkAction): + name = "edit" + verbose_name = _("Edit") + url = DOMAINS_UPDATE_URL + classes = ("ajax-modal", "btn-edit") + + def allowed(self, request, domain): + return api.keystone.keystone_can_edit_domain() + + +class DeleteDomainsAction(tables.DeleteAction): + name = "delete" + data_type_singular = _("Domain") + data_type_plural = _("Domains") + + def allowed(self, request, datum): + return api.keystone.keystone_can_edit_domain() + + def delete(self, request, obj_id): + domain = self.table.get_object_by_id(obj_id) + if domain.enabled: + msg = _('Domain "%s" must be disabled before it can be deleted.') \ + % domain.name + messages.error(request, msg) + raise ClientException(409, msg) + else: + LOG.info('Deleting domain "%s".' % obj_id) + api.keystone.domain_delete(request, obj_id) + + +class DomainFilterAction(tables.FilterAction): + def filter(self, table, domains, filter_string): + """ Naive case-insensitive search """ + q = filter_string.lower() + + def comp(domain): + if q in domain.name.lower(): + return True + return False + + return filter(comp, domains) + + +class DomainsTable(tables.DataTable): + name = tables.Column('name', verbose_name=_('Name')) + description = tables.Column(lambda obj: getattr(obj, 'description', None), + verbose_name=_('Description')) + id = tables.Column('id', verbose_name=_('Domain ID')) + enabled = tables.Column('enabled', verbose_name=_('Enabled'), status=True) + + class Meta: + name = "domains" + verbose_name = _("Domains") + row_actions = (EditDomainLink, DeleteDomainsAction) + table_actions = (DomainFilterAction, CreateDomainLink, + DeleteDomainsAction) diff --git a/openstack_dashboard/dashboards/admin/domains/templates/domains/index.html b/openstack_dashboard/dashboards/admin/domains/templates/domains/index.html new file mode 100644 index 0000000000..8bc5229423 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/domains/templates/domains/index.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Domains" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Domains") %} +{% endblock page_header %} + +{% block main %} + {{ table.render }} +{% endblock %} diff --git a/openstack_dashboard/dashboards/admin/domains/tests.py b/openstack_dashboard/dashboards/admin/domains/tests.py new file mode 100644 index 0000000000..3cebee7ffd --- /dev/null +++ b/openstack_dashboard/dashboards/admin/domains/tests.py @@ -0,0 +1,196 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 Hewlett-Packard Development Company, L.P. +# +# 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. + + +from django import http +from django.core.urlresolvers import reverse + +from mox import IgnoreArg, IsA + +from horizon.workflows.views import WorkflowView + +from openstack_dashboard import api +from openstack_dashboard.test import helpers as test + +from .constants import DOMAINS_INDEX_VIEW_TEMPLATE, \ + DOMAINS_INDEX_URL as index_url, \ + DOMAINS_CREATE_URL as create_url, \ + DOMAINS_UPDATE_URL as update_url +from .workflows import CreateDomain, UpdateDomain + + +DOMAINS_INDEX_URL = reverse(index_url) +DOMAIN_CREATE_URL = reverse(create_url) +DOMAIN_UPDATE_URL = reverse(update_url, args=[1]) + + +class DomainsViewTests(test.BaseAdminViewTests): + @test.create_stubs({api.keystone: ('domain_list',)}) + def test_index(self): + api.keystone.domain_list(IgnoreArg()).AndReturn(self.domains.list()) + + self.mox.ReplayAll() + + res = self.client.get(DOMAINS_INDEX_URL) + + self.assertTemplateUsed(res, DOMAINS_INDEX_VIEW_TEMPLATE) + self.assertItemsEqual(res.context['table'].data, self.domains.list()) + self.assertContains(res, 'Create Domain') + self.assertContains(res, 'Edit') + self.assertContains(res, 'Delete Domain') + + @test.create_stubs({api.keystone: ('domain_list', + 'keystone_can_edit_domain')}) + def test_index_with_keystone_can_edit_domain_false(self): + api.keystone.domain_list(IgnoreArg()).AndReturn(self.domains.list()) + api.keystone.keystone_can_edit_domain() \ + .MultipleTimes().AndReturn(False) + + self.mox.ReplayAll() + + res = self.client.get(DOMAINS_INDEX_URL) + + self.assertTemplateUsed(res, DOMAINS_INDEX_VIEW_TEMPLATE) + self.assertItemsEqual(res.context['table'].data, self.domains.list()) + self.assertNotContains(res, 'Create Domain') + self.assertNotContains(res, 'Edit') + self.assertNotContains(res, 'Delete Domain') + + @test.create_stubs({api.keystone: ('domain_list', + 'domain_delete')}) + def test_delete_domain(self): + domain = self.domains.get(id="2") + + api.keystone.domain_list(IgnoreArg()).AndReturn(self.domains.list()) + api.keystone.domain_delete(IgnoreArg(), domain.id) + + self.mox.ReplayAll() + + formData = {'action': 'domains__delete__%s' % domain.id} + res = self.client.post(DOMAINS_INDEX_URL, formData) + + self.assertRedirectsNoFollow(res, DOMAINS_INDEX_URL) + + @test.create_stubs({api.keystone: ('domain_list', )}) + def test_delete_with_enabled_domain(self): + domain = self.domains.get(id="1") + + api.keystone.domain_list(IgnoreArg()).AndReturn(self.domains.list()) + + self.mox.ReplayAll() + + formData = {'action': 'domains__delete__%s' % domain.id} + res = self.client.post(DOMAINS_INDEX_URL, formData) + + self.assertRedirectsNoFollow(res, DOMAINS_INDEX_URL) + self.assertMessageCount(error=2) + + +class CreateDomainWorkflowTests(test.BaseAdminViewTests): + def _get_domain_info(self, domain): + domain_info = {"name": domain.name, + "description": domain.description, + "enabled": domain.enabled} + return domain_info + + def _get_workflow_data(self, domain): + domain_info = self._get_domain_info(domain) + return domain_info + + def test_add_domain_get(self): + url = reverse('horizon:admin:domains:create') + res = self.client.get(url) + + self.assertTemplateUsed(res, WorkflowView.template_name) + + workflow = res.context['workflow'] + self.assertEqual(res.context['workflow'].name, CreateDomain.name) + + self.assertQuerysetEqual(workflow.steps, + ['', ]) + + @test.create_stubs({api.keystone: ('domain_create', )}) + def test_add_domain_post(self): + domain = self.domains.get(id="1") + + api.keystone.domain_create(IsA(http.HttpRequest), + description=domain.description, + enabled=domain.enabled, + name=domain.name).AndReturn(domain) + + self.mox.ReplayAll() + + workflow_data = self._get_workflow_data(domain) + + res = self.client.post(DOMAIN_CREATE_URL, workflow_data) + + self.assertNoFormErrors(res) + self.assertRedirectsNoFollow(res, DOMAINS_INDEX_URL) + + +class UpdateDomainWorkflowTests(test.BaseAdminViewTests): + def _get_domain_info(self, domain): + domain_info = {"domain_id": domain.id, + "name": domain.name, + "description": domain.description, + "enabled": domain.enabled} + return domain_info + + def _get_workflow_data(self, domain): + domain_info = self._get_domain_info(domain) + return domain_info + + @test.create_stubs({api.keystone: ('domain_get', )}) + def test_update_domain_get(self): + domain = self.domains.get(id="1") + + api.keystone.domain_get(IsA(http.HttpRequest), '1').AndReturn(domain) + + self.mox.ReplayAll() + + res = self.client.get(DOMAIN_UPDATE_URL) + + self.assertTemplateUsed(res, WorkflowView.template_name) + + workflow = res.context['workflow'] + self.assertEqual(res.context['workflow'].name, UpdateDomain.name) + + self.assertQuerysetEqual(workflow.steps, + ['', ]) + + @test.create_stubs({api.keystone: ('domain_get', + 'domain_update')}) + def test_update_domain_post(self): + domain = self.domains.get(id="1") + test_description = 'updated description' + + api.keystone.domain_get(IsA(http.HttpRequest), '1').AndReturn(domain) + api.keystone.domain_update(IsA(http.HttpRequest), + description=test_description, + domain_id=domain.id, + enabled=domain.enabled, + name=domain.name).AndReturn(None) + + self.mox.ReplayAll() + + workflow_data = self._get_workflow_data(domain) + workflow_data['description'] = test_description + + res = self.client.post(DOMAIN_UPDATE_URL, workflow_data) + + self.assertNoFormErrors(res) + self.assertMessageCount(success=1) + self.assertRedirectsNoFollow(res, DOMAINS_INDEX_URL) diff --git a/openstack_dashboard/dashboards/admin/domains/urls.py b/openstack_dashboard/dashboards/admin/domains/urls.py new file mode 100644 index 0000000000..e9eaef6d87 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/domains/urls.py @@ -0,0 +1,27 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 Hewlett-Packard Development Company, L.P. +# +# 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. + +from django.conf.urls.defaults import patterns, url + +from .views import IndexView, CreateDomainView, UpdateDomainView + + +urlpatterns = patterns('', + url(r'^$', IndexView.as_view(), name='index'), + url(r'^create$', CreateDomainView.as_view(), name='create'), + url(r'^(?P[^/]+)/update/$', + UpdateDomainView.as_view(), name='update') +) diff --git a/openstack_dashboard/dashboards/admin/domains/views.py b/openstack_dashboard/dashboards/admin/domains/views.py new file mode 100644 index 0000000000..58e925bff5 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/domains/views.py @@ -0,0 +1,68 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 Hewlett-Packard Development Company, L.P. +# +# 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. + +from django.core.urlresolvers import reverse +from django.utils.translation import ugettext_lazy as _ + +from horizon import exceptions +from horizon import tables +from horizon import workflows + +from openstack_dashboard import api +from .constants import DOMAIN_INFO_FIELDS, DOMAINS_INDEX_URL, \ + DOMAINS_INDEX_VIEW_TEMPLATE +from .tables import DomainsTable +from .workflows import CreateDomain, UpdateDomain + + +class IndexView(tables.DataTableView): + table_class = DomainsTable + template_name = DOMAINS_INDEX_VIEW_TEMPLATE + + def get_data(self): + domains = [] + try: + domains = api.keystone.domain_list(self.request) + except: + exceptions.handle(self.request, + _('Unable to retrieve domain list.')) + return domains + + +class CreateDomainView(workflows.WorkflowView): + workflow_class = CreateDomain + + +class UpdateDomainView(workflows.WorkflowView): + workflow_class = UpdateDomain + + def get_initial(self): + initial = super(UpdateDomainView, self).get_initial() + + domain_id = self.kwargs['domain_id'] + initial['domain_id'] = domain_id + + try: + # get initial domain info + domain_info = api.keystone.domain_get(self.request, + domain_id) + for field in DOMAIN_INFO_FIELDS: + initial[field] = getattr(domain_info, field, None) + except: + exceptions.handle(self.request, + _('Unable to retrieve domain details.'), + redirect=reverse(DOMAINS_INDEX_URL)) + return initial diff --git a/openstack_dashboard/dashboards/admin/domains/workflows.py b/openstack_dashboard/dashboards/admin/domains/workflows.py new file mode 100644 index 0000000000..2a9f358c64 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/domains/workflows.py @@ -0,0 +1,127 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 Hewlett-Packard Development Company, L.P. +# +# 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 logging + +from django.utils.translation import ugettext_lazy as _ + +from horizon import exceptions +from horizon import workflows +from horizon import forms +from horizon import messages + +from openstack_dashboard import api + +from .constants import DOMAINS_INDEX_URL + +LOG = logging.getLogger(__name__) + + +class CreateDomainInfoAction(workflows.Action): + name = forms.CharField(label=_("Name"), + required=True) + description = forms.CharField(widget=forms.widgets.Textarea(), + label=_("Description"), + required=False) + enabled = forms.BooleanField(label=_("Enabled"), + required=False, + initial=True) + + class Meta: + name = _("Domain Info") + slug = "create_domain" + help_text = _("From here you can create a new domain to organize " + "projects, groups and users.") + + +class CreateDomainInfo(workflows.Step): + action_class = CreateDomainInfoAction + contributes = ("domain_id", + "name", + "description", + "enabled") + + +class CreateDomain(workflows.Workflow): + slug = "create_domain" + name = _("Create Domain") + finalize_button_name = _("Create Domain") + success_message = _('Created new domain "%s".') + failure_message = _('Unable to create domain "%s".') + success_url = DOMAINS_INDEX_URL + default_steps = (CreateDomainInfo, ) + + def format_status_message(self, message): + return message % self.context.get('name', 'unknown domain') + + def handle(self, request, data): + # create the domain + try: + LOG.info('Creating domain with name "%s"' % data['name']) + desc = data['description'] + new_domain = api.keystone.domain_create(request, + name=data['name'], + description=desc, + enabled=data['enabled']) + except: + exceptions.handle(request, ignore=True) + return False + + return True + + +class UpdateDomainInfoAction(CreateDomainInfoAction): + + class Meta: + name = _("Domain Info") + slug = 'update_domain' + help_text = _("From here you can edit the domain details.") + + +class UpdateDomainInfo(workflows.Step): + action_class = UpdateDomainInfoAction + depends_on = ("domain_id",) + contributes = ("name", + "description", + "enabled") + + +class UpdateDomain(workflows.Workflow): + slug = "update_domain" + name = _("Edit Domain") + finalize_button_name = _("Save") + success_message = _('Modified domain "%s".') + failure_message = _('Unable to modify domain "%s".') + success_url = DOMAINS_INDEX_URL + default_steps = (UpdateDomainInfo, ) + + def format_status_message(self, message): + return message % self.context.get('name', 'unknown domain') + + def handle(self, request, data): + domain_id = data.pop('domain_id') + + try: + LOG.info('Updating domain with name "%s"' % data['name']) + api.keystone.domain_update(request, + domain_id=domain_id, + name=data['name'], + description=data['description'], + enabled=data['enabled']) + except: + exceptions.handle(request, ignore=True) + return False + return True diff --git a/openstack_dashboard/local/local_settings.py.example b/openstack_dashboard/local/local_settings.py.example index dc3bf150b4..dfe9c0a73a 100644 --- a/openstack_dashboard/local/local_settings.py.example +++ b/openstack_dashboard/local/local_settings.py.example @@ -136,7 +136,8 @@ OPENSTACK_KEYSTONE_DEFAULT_ROLE = "Member" OPENSTACK_KEYSTONE_BACKEND = { 'name': 'native', 'can_edit_user': True, - 'can_edit_project': True + 'can_edit_project': True, + 'can_edit_domain': True } OPENSTACK_HYPERVISOR_FEATURES = { diff --git a/openstack_dashboard/test/settings.py b/openstack_dashboard/test/settings.py index 90b60d0d10..1e02e5ce63 100644 --- a/openstack_dashboard/test/settings.py +++ b/openstack_dashboard/test/settings.py @@ -70,10 +70,14 @@ AVAILABLE_REGIONS = [ OPENSTACK_KEYSTONE_URL = "http://localhost:5000/v2.0" OPENSTACK_KEYSTONE_DEFAULT_ROLE = "Member" +OPENSTACK_KEYSTONE_MULTIDOMAIN_SUPPORT = True +OPENSTACK_KEYSTONE_DEFAULT_DOMAIN = 'test_domain' + OPENSTACK_KEYSTONE_BACKEND = { 'name': 'native', 'can_edit_user': True, - 'can_edit_project': True + 'can_edit_project': True, + 'can_edit_domain': True } OPENSTACK_QUANTUM_NETWORK = { diff --git a/openstack_dashboard/test/test_data/keystone_data.py b/openstack_dashboard/test/test_data/keystone_data.py index ea61f8188f..9832c7da51 100644 --- a/openstack_dashboard/test/test_data/keystone_data.py +++ b/openstack_dashboard/test/test_data/keystone_data.py @@ -18,6 +18,7 @@ from django.conf import settings from django.utils import datetime_safe from keystoneclient.v2_0 import users, tenants, tokens, roles, ec2 +from keystoneclient.v3 import domains from .utils import TestDataContainer @@ -88,6 +89,7 @@ SERVICE_CATALOG = [ def data(TEST): TEST.service_catalog = SERVICE_CATALOG TEST.tokens = TestDataContainer() + TEST.domains = TestDataContainer() TEST.users = TestDataContainer() TEST.tenants = TestDataContainer() TEST.roles = TestDataContainer() @@ -103,6 +105,19 @@ def data(TEST): TEST.roles.admin = admin_role TEST.roles.member = member_role + domain_dict = {'id': "1", + 'name': 'test_domain', + 'description': "a test domain.", + 'enabled': True} + domain_dict_2 = {'id': "2", + 'name': 'disabled_domain', + 'description': "a disabled test domain.", + 'enabled': False} + domain = domains.Domain(domains.DomainManager, domain_dict) + disabled_domain = domains.Domain(domains.DomainManager, domain_dict_2) + TEST.domains.add(domain, disabled_domain) + TEST.domain = domain # Your "current" domain + user_dict = {'id': "1", 'name': 'test_user', 'email': 'test@example.com',