diff --git a/openstack_dashboard/api/neutron.py b/openstack_dashboard/api/neutron.py index c7ac0709d8..62895423c6 100644 --- a/openstack_dashboard/api/neutron.py +++ b/openstack_dashboard/api/neutron.py @@ -34,6 +34,7 @@ from horizon.utils.memoized import memoized # noqa from openstack_dashboard.api import base from openstack_dashboard.api import network_base from openstack_dashboard.api import nova +from openstack_dashboard import policy LOG = logging.getLogger(__name__) @@ -898,3 +899,31 @@ def is_port_profiles_supported(): profile_support = network_config.get('profile_support', None) if str(profile_support).lower() == 'cisco': return True + + +def get_dvr_permission(request, operation): + """Check if "distributed" field can be displayed. + + :param request: Request Object + :param operation: Operation type. The valid value is "get" or "create" + """ + network_config = getattr(settings, 'OPENSTACK_NEUTRON_NETWORK', {}) + if not network_config.get('enable_distributed_router', False): + return False + policy_check = getattr(settings, "POLICY_CHECK_FUNCTION", None) + if operation not in ("get", "create"): + raise ValueError(_("The 'operation' parameter for get_dvr_permission " + "is invalid. It should be 'get' or 'create'.")) + role = (("network", "%s_router:distributed" % operation),) + if policy_check: + has_permission = policy.check(role, request) + else: + has_permission = True + if not has_permission: + return False + try: + return is_extension_supported(request, 'dvr') + except Exception: + msg = _('Failed to check Neutron "dvr" extension is not supported') + LOG.info(msg) + return False diff --git a/openstack_dashboard/conf/neutron_policy.json b/openstack_dashboard/conf/neutron_policy.json index 3947312cee..d4c631fc60 100644 --- a/openstack_dashboard/conf/neutron_policy.json +++ b/openstack_dashboard/conf/neutron_policy.json @@ -145,6 +145,8 @@ "add_router_interface": "rule:admin_or_owner", "remove_router_interface": "rule:admin_or_owner", "delete_router": "rule:admin_or_owner", + "get_router:distributed": "rule:admin_only", + "create_router:distributed": "rule:admin_only", "create_floatingip": "rule:regular_user", "update_floatingip": "rule:admin_or_owner", diff --git a/openstack_dashboard/dashboards/admin/routers/tables.py b/openstack_dashboard/dashboards/admin/routers/tables.py index 0ca666396f..2b75717a55 100644 --- a/openstack_dashboard/dashboards/admin/routers/tables.py +++ b/openstack_dashboard/dashboards/admin/routers/tables.py @@ -12,7 +12,6 @@ # License for the specific language governing permissions and limitations # under the License. -from django.template.defaultfilters import title # noqa from django.utils.translation import ugettext_lazy as _ from horizon import tables @@ -51,20 +50,11 @@ class UpdateRow(tables.Row): return router -class RoutersTable(tables.DataTable): +class RoutersTable(r_tables.RoutersTable): tenant = tables.Column("tenant_name", verbose_name=_("Project")) name = tables.Column("name", verbose_name=_("Name"), link="horizon:admin:routers:detail") - status = tables.Column("status", - filters=(title,), - verbose_name=_("Status"), - status=True) - ext_net = tables.Column(r_tables.get_external_network, - verbose_name=_("External Network")) - - def get_object_display(self, obj): - return obj.name class Meta: name = "Routers" @@ -73,3 +63,4 @@ class RoutersTable(tables.DataTable): row_class = UpdateRow table_actions = (DeleteRouter,) row_actions = (DeleteRouter,) + Columns = ('tenant', 'name', 'status', 'distributed', 'ext_net') diff --git a/openstack_dashboard/dashboards/admin/routers/templates/routers/_detail_overview.html b/openstack_dashboard/dashboards/admin/routers/templates/routers/_detail_overview.html index db0dc9297c..20b9773917 100644 --- a/openstack_dashboard/dashboards/admin/routers/templates/routers/_detail_overview.html +++ b/openstack_dashboard/dashboards/admin/routers/templates/routers/_detail_overview.html @@ -12,6 +12,10 @@
{{ router.tenant_id }}
{% trans "Status" %}
{{ router.status|capfirst }}
+ {% if dvr_supported %} +
{% trans "Distributed" %}
+
{{ router.distributed|yesno|capfirst }}
+ {% endif %} {% if router.external_gateway_info %}
{% trans "External Gateway Information" %}
{% trans "Connected External Network" %}: diff --git a/openstack_dashboard/dashboards/project/routers/forms.py b/openstack_dashboard/dashboards/project/routers/forms.py index 2cebda0668..c1bbc77171 100644 --- a/openstack_dashboard/dashboards/project/routers/forms.py +++ b/openstack_dashboard/dashboards/project/routers/forms.py @@ -32,15 +32,28 @@ LOG = logging.getLogger(__name__) class CreateForm(forms.SelfHandlingForm): name = forms.CharField(max_length="255", label=_("Router Name")) + mode = forms.ChoiceField(label=_("Router Type")) failure_url = 'horizon:project:routers:index' def __init__(self, request, *args, **kwargs): super(CreateForm, self).__init__(request, *args, **kwargs) + self.dvr_enabled = api.neutron.get_dvr_permission(self.request, + "create") + if self.dvr_enabled: + mode_choices = [('server_default', _('Use Server Default')), + ('centralized', _('Centralized')), + ('distributed', _('Distributed'))] + self.fields['mode'].choices = mode_choices + else: + self.fields['mode'].widget = forms.HiddenInput() + self.fields['mode'].required = False def handle(self, request, data): try: - router = api.neutron.router_create(request, - name=data['name']) + params = {'name': data['name']} + if (self.dvr_enabled and data['mode'] != 'server_default'): + params['distributed'] = (data['mode'] == 'distributed') + router = api.neutron.router_create(request, **params) message = _('Router %s was successfully created.') % data['name'] messages.success(request, message) return router diff --git a/openstack_dashboard/dashboards/project/routers/tables.py b/openstack_dashboard/dashboards/project/routers/tables.py index 65fafc8c4b..12ee44ddea 100644 --- a/openstack_dashboard/dashboards/project/routers/tables.py +++ b/openstack_dashboard/dashboards/project/routers/tables.py @@ -15,7 +15,7 @@ import logging from django.core.urlresolvers import reverse -from django.template.defaultfilters import title # noqa +from django.template import defaultfilters as filters from django.utils.translation import ugettext_lazy as _ from neutronclient.common import exceptions as q_ext @@ -147,12 +147,24 @@ class RoutersTable(tables.DataTable): verbose_name=_("Name"), link="horizon:project:routers:detail") status = tables.Column("status", - filters=(title,), + filters=(filters.title,), verbose_name=_("Status"), status=True) + distributed = tables.Column("distributed", + filters=(filters.yesno, filters.capfirst), + verbose_name=_("Distributed")) ext_net = tables.Column(get_external_network, verbose_name=_("External Network")) + def __init__(self, request, data=None, needs_form_wrapper=None, **kwargs): + super(RoutersTable, self).__init__( + request, + data=data, + needs_form_wrapper=needs_form_wrapper, + **kwargs) + if not api.neutron.get_dvr_permission(request, "get"): + del self.columns["distributed"] + def get_object_display(self, obj): return obj.name diff --git a/openstack_dashboard/dashboards/project/routers/templates/routers/_detail_overview.html b/openstack_dashboard/dashboards/project/routers/templates/routers/_detail_overview.html index 226939f653..a40a21ad09 100644 --- a/openstack_dashboard/dashboards/project/routers/templates/routers/_detail_overview.html +++ b/openstack_dashboard/dashboards/project/routers/templates/routers/_detail_overview.html @@ -10,6 +10,10 @@
{{ router.id|default:_("None") }}
{% trans "Status" %}
{{ router.status|default:_("Unknown") }}
+ {% if dvr_supported %} +
{% trans "Distributed" %}
+
{{ router.distributed|yesno|capfirst }}
+ {% endif %} {% if router.external_gateway_info %}
{% trans "External Gateway Information" %}
{% trans "Connected External Network" %}: diff --git a/openstack_dashboard/dashboards/project/routers/tests.py b/openstack_dashboard/dashboards/project/routers/tests.py index 9234761514..9ac43998b1 100644 --- a/openstack_dashboard/dashboards/project/routers/tests.py +++ b/openstack_dashboard/dashboards/project/routers/tests.py @@ -109,13 +109,16 @@ class RouterActionTests(test.TestCase): INDEX_URL = reverse('horizon:%s:routers:index' % DASHBOARD) DETAIL_PATH = 'horizon:%s:routers:detail' % DASHBOARD - @test.create_stubs({api.neutron: ('router_create',)}) + @test.create_stubs({api.neutron: ('router_create', + 'get_dvr_permission',)}) def test_router_create_post(self): router = self.routers.first() + api.neutron.get_dvr_permission(IsA(http.HttpRequest), "create")\ + .AndReturn(False) api.neutron.router_create(IsA(http.HttpRequest), name=router.name)\ .AndReturn(router) - self.mox.ReplayAll() + self.mox.ReplayAll() form_data = {'name': router.name} url = reverse('horizon:%s:routers:create' % self.DASHBOARD) res = self.client.post(url, form_data) @@ -123,9 +126,50 @@ class RouterActionTests(test.TestCase): self.assertNoFormErrors(res) self.assertRedirectsNoFollow(res, self.INDEX_URL) - @test.create_stubs({api.neutron: ('router_create',)}) + @test.create_stubs({api.neutron: ('router_create', + 'get_dvr_permission',)}) + def test_router_create_post_mode_server_default(self): + router = self.routers.first() + api.neutron.get_dvr_permission(IsA(http.HttpRequest), "create")\ + .AndReturn(True) + api.neutron.router_create(IsA(http.HttpRequest), name=router.name)\ + .AndReturn(router) + + self.mox.ReplayAll() + form_data = {'name': router.name, + 'mode': 'server_default'} + url = reverse('horizon:%s:routers:create' % self.DASHBOARD) + res = self.client.post(url, form_data) + + self.assertNoFormErrors(res) + self.assertRedirectsNoFollow(res, self.INDEX_URL) + + @test.create_stubs({api.neutron: ('router_create', + 'get_dvr_permission',)}) + def test_dvr_router_create_post(self): + router = self.routers.first() + api.neutron.get_dvr_permission(IsA(http.HttpRequest), "create")\ + .MultipleTimes().AndReturn(True) + param = {'name': router.name, + 'distributed': True} + api.neutron.router_create(IsA(http.HttpRequest), **param)\ + .AndReturn(router) + + self.mox.ReplayAll() + form_data = {'name': router.name, + 'mode': 'distributed'} + url = reverse('horizon:%s:routers:create' % self.DASHBOARD) + res = self.client.post(url, form_data) + + self.assertNoFormErrors(res) + self.assertRedirectsNoFollow(res, self.INDEX_URL) + + @test.create_stubs({api.neutron: ('router_create', + 'get_dvr_permission',)}) def test_router_create_post_exception_error_case_409(self): router = self.routers.first() + api.neutron.get_dvr_permission(IsA(http.HttpRequest), "create")\ + .MultipleTimes().AndReturn(False) self.exceptions.neutron.status_code = 409 api.neutron.router_create(IsA(http.HttpRequest), name=router.name)\ .AndRaise(self.exceptions.neutron) @@ -138,9 +182,12 @@ class RouterActionTests(test.TestCase): self.assertNoFormErrors(res) self.assertRedirectsNoFollow(res, self.INDEX_URL) - @test.create_stubs({api.neutron: ('router_create',)}) + @test.create_stubs({api.neutron: ('router_create', + 'get_dvr_permission',)}) def test_router_create_post_exception_error_case_non_409(self): router = self.routers.first() + api.neutron.get_dvr_permission(IsA(http.HttpRequest), "create")\ + .MultipleTimes().AndReturn(False) self.exceptions.neutron.status_code = 999 api.neutron.router_create(IsA(http.HttpRequest), name=router.name)\ .AndRaise(self.exceptions.neutron) diff --git a/openstack_dashboard/dashboards/project/routers/views.py b/openstack_dashboard/dashboards/project/routers/views.py index 68977115d0..bbf1b2f68e 100644 --- a/openstack_dashboard/dashboards/project/routers/views.py +++ b/openstack_dashboard/dashboards/project/routers/views.py @@ -116,6 +116,8 @@ class DetailView(tabs.TabbedTableView): def get_context_data(self, **kwargs): context = super(DetailView, self).get_context_data(**kwargs) context["router"] = self._get_data() + context['dvr_supported'] = api.neutron.get_dvr_permission(self.request, + "get") return context def get(self, request, *args, **kwargs): diff --git a/openstack_dashboard/local/local_settings.py.example b/openstack_dashboard/local/local_settings.py.example index fffd8daee2..cf3959e003 100644 --- a/openstack_dashboard/local/local_settings.py.example +++ b/openstack_dashboard/local/local_settings.py.example @@ -183,6 +183,7 @@ OPENSTACK_NEUTRON_NETWORK = { 'enable_quotas': True, 'enable_vpn': False, 'enable_ipv6': True, + 'enable_distributed_router': True, # The profile_support option is used to detect if an external router can be # configured via the dashboard. When using specific plugins the # profile_support can be turned on if needed. diff --git a/openstack_dashboard/test/api_tests/neutron_tests.py b/openstack_dashboard/test/api_tests/neutron_tests.py index 9746d8cfea..59def8eb6f 100644 --- a/openstack_dashboard/test/api_tests/neutron_tests.py +++ b/openstack_dashboard/test/api_tests/neutron_tests.py @@ -12,7 +12,10 @@ # License for the specific language governing permissions and limitations # under the License. +from django.test.utils import override_settings + from openstack_dashboard import api +from openstack_dashboard import policy from openstack_dashboard.test import helpers as test @@ -279,3 +282,67 @@ class NeutronApiTests(test.APITestCase): api.neutron.is_extension_supported(self.request, 'quotas')) self.assertFalse( api.neutron.is_extension_supported(self.request, 'doesntexist')) + + @override_settings(OPENSTACK_NEUTRON_NETWORK={'enable_distributed_router': + True}, + POLICY_CHECK_FUNCTION=None) + def _test_get_dvr_permission_dvr_supported(self, dvr_enabled): + neutronclient = self.stub_neutronclient() + extensions = self.api_extensions.list() + if not dvr_enabled: + extensions = [ext for ext in extensions if ext['alias'] != 'dvr'] + neutronclient.list_extensions() \ + .AndReturn({'extensions': extensions}) + self.mox.ReplayAll() + self.assertEqual(dvr_enabled, + api.neutron.get_dvr_permission(self.request, 'get')) + + def test_get_dvr_permission_dvr_supported(self): + self._test_get_dvr_permission_dvr_supported(dvr_enabled=True, ) + + def test_get_dvr_permission_dvr_not_supported(self): + self._test_get_dvr_permission_dvr_supported(dvr_enabled=False) + + @override_settings(OPENSTACK_NEUTRON_NETWORK={'enable_distributed_router': + True}, + POLICY_CHECK_FUNCTION=policy.check) + def _test_get_dvr_permission_with_policy_check(self, policy_check_allowed, + operation): + self.mox.StubOutWithMock(policy, 'check') + if operation == "create": + role = (("network", "create_router:distributed"),) + elif operation == "get": + role = (("network", "get_router:distributed"),) + policy.check(role, self.request).AndReturn(policy_check_allowed) + if policy_check_allowed: + neutronclient = self.stub_neutronclient() + neutronclient.list_extensions() \ + .AndReturn({'extensions': self.api_extensions.list()}) + self.mox.ReplayAll() + self.assertEqual(policy_check_allowed, + api.neutron.get_dvr_permission(self.request, + operation)) + + def test_get_dvr_permission_with_policy_check_allowed(self): + self._test_get_dvr_permission_with_policy_check(True, "get") + + def test_get_dvr_permission_with_policy_check_disallowed(self): + self._test_get_dvr_permission_with_policy_check(False, "get") + + def test_get_dvr_permission_create_with_policy_check_allowed(self): + self._test_get_dvr_permission_with_policy_check(True, "create") + + def test_get_dvr_permission_create_with_policy_check_disallowed(self): + self._test_get_dvr_permission_with_policy_check(False, "create") + + @override_settings(OPENSTACK_NEUTRON_NETWORK={'enable_distributed_router': + False}) + def test_get_dvr_permission_dvr_disabled_by_config(self): + self.assertFalse(api.neutron.get_dvr_permission(self.request, 'get')) + + @override_settings(OPENSTACK_NEUTRON_NETWORK={'enable_distributed_router': + True}) + def test_get_dvr_permission_dvr_unsupported_operation(self): + self.assertRaises(ValueError, + api.neutron.get_dvr_permission, + self.request, 'unSupported') diff --git a/openstack_dashboard/test/settings.py b/openstack_dashboard/test/settings.py index bb33eb4d0e..4a7ad403d1 100644 --- a/openstack_dashboard/test/settings.py +++ b/openstack_dashboard/test/settings.py @@ -109,6 +109,7 @@ OPENSTACK_NEUTRON_NETWORK = { 'enable_quotas': False, # Enabled in specific tests only 'enable_vpn': True, 'profile_support': None, + 'enable_distributed_router': False, # 'profile_support': 'cisco' } diff --git a/openstack_dashboard/test/test_data/neutron_data.py b/openstack_dashboard/test/test_data/neutron_data.py index f3d900f37d..09865b2ffe 100644 --- a/openstack_dashboard/test/test_data/neutron_data.py +++ b/openstack_dashboard/test/test_data/neutron_data.py @@ -568,9 +568,14 @@ def data(TEST): extension_3 = {"name": "Provider network", "alias": "provider", "description": "Provider network extension"} + extension_4 = {"name": "Distributed Virtual Router", + "alias": "dvr", + "description": + "Enables configuration of Distributed Virtual Routers."} TEST.api_extensions.add(extension_1) TEST.api_extensions.add(extension_2) TEST.api_extensions.add(extension_3) + TEST.api_extensions.add(extension_4) # 1st agent. agent_dict = {"binary": "neutron-openvswitch-agent",