From 440bd19040ad30d1b6a0da2c2b5ebf658cfa8040 Mon Sep 17 00:00:00 2001 From: CHARDON Gerome Date: Wed, 19 Nov 2014 17:49:32 +0100 Subject: [PATCH] Add Extra Routes to Router Tabs View Add support to neutron extra routes to routers views. Change-Id: Id5b5c7494d903fff0fdb5cba9297dbed5cf3d62e Closes-Bug: #1396616 --- openstack_dashboard/api/neutron.py | 42 +++++++ .../admin/routers/extensions/__init__.py | 0 .../extensions/extraroutes/__init__.py | 0 .../routers/extensions/extraroutes/tables.py | 27 +++++ .../dashboards/admin/routers/tabs.py | 10 +- .../dashboards/admin/routers/tests.py | 6 + .../extensions/extraroutes/__init__.py | 0 .../routers/extensions/extraroutes/forms.py | 60 ++++++++++ .../routers/extensions/extraroutes/tables.py | 77 +++++++++++++ .../routers/extensions/extraroutes/tabs.py | 49 ++++++++ .../routers/extensions/extraroutes/views.py | 59 ++++++++++ .../dashboards/project/routers/tabs.py | 6 +- .../extensions/routerroutes/_create.html | 29 +++++ .../extensions/routerroutes/create.html | 11 ++ .../dashboards/project/routers/tests.py | 106 +++++++++++++++++- .../dashboards/project/routers/urls.py | 5 + .../test/api_tests/neutron_tests.py | 45 ++++++++ .../test/test_data/neutron_data.py | 16 +++ 18 files changed, 541 insertions(+), 7 deletions(-) create mode 100644 openstack_dashboard/dashboards/admin/routers/extensions/__init__.py create mode 100644 openstack_dashboard/dashboards/admin/routers/extensions/extraroutes/__init__.py create mode 100644 openstack_dashboard/dashboards/admin/routers/extensions/extraroutes/tables.py create mode 100644 openstack_dashboard/dashboards/project/routers/extensions/extraroutes/__init__.py create mode 100644 openstack_dashboard/dashboards/project/routers/extensions/extraroutes/forms.py create mode 100644 openstack_dashboard/dashboards/project/routers/extensions/extraroutes/tables.py create mode 100644 openstack_dashboard/dashboards/project/routers/extensions/extraroutes/tabs.py create mode 100644 openstack_dashboard/dashboards/project/routers/extensions/extraroutes/views.py create mode 100644 openstack_dashboard/dashboards/project/routers/templates/routers/extensions/routerroutes/_create.html create mode 100644 openstack_dashboard/dashboards/project/routers/templates/routers/extensions/routerroutes/create.html diff --git a/openstack_dashboard/api/neutron.py b/openstack_dashboard/api/neutron.py index 2ff31e7caf..7e89aba7ad 100644 --- a/openstack_dashboard/api/neutron.py +++ b/openstack_dashboard/api/neutron.py @@ -142,6 +142,15 @@ class Router(NeutronAPIDictWrapper): super(Router, self).__init__(apiresource) +class RouterStaticRoute(NeutronAPIDictWrapper): + """Wrapper for neutron routes extra route.""" + + def __init__(self, route): + super(RouterStaticRoute, self).__init__(route) + # Horizon references id property for table operations + self.id = route['nexthop'] + ":" + route['destination'] + + class SecurityGroup(NeutronAPIDictWrapper): # Required attributes: id, name, description, tenant_id, rules @@ -896,6 +905,39 @@ def router_remove_gateway(request, router_id): neutronclient(request).remove_gateway_router(router_id) +def router_static_route_list(request, router_id=None): + router = router_get(request, router_id) + try: + routes = [RouterStaticRoute(r) for r in router.routes] + except AttributeError: + LOG.debug("router_static_route_list(): router_id=%s, " + "router=%s", (router_id, router)) + return [] + return routes + + +def router_static_route_remove(request, router_id, route_ids): + currentroutes = router_static_route_list(request, router_id=router_id) + newroutes = [] + for oldroute in currentroutes: + if oldroute.id not in route_ids: + newroutes.append({'nexthop': oldroute.nexthop, + 'destination': oldroute.destination}) + body = {'routes': newroutes} + new = router_update(request, router_id, **body) + return new + + +def router_static_route_add(request, router_id, newroute): + body = {} + currentroutes = router_static_route_list(request, router_id=router_id) + body['routes'] = [newroute] + [{'nexthop': r.nexthop, + 'destination': r.destination} + for r in currentroutes] + new = router_update(request, router_id, **body) + return new + + def tenant_quota_get(request, tenant_id): return base.QuotaSet(neutronclient(request).show_quota(tenant_id)['quota']) diff --git a/openstack_dashboard/dashboards/admin/routers/extensions/__init__.py b/openstack_dashboard/dashboards/admin/routers/extensions/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openstack_dashboard/dashboards/admin/routers/extensions/extraroutes/__init__.py b/openstack_dashboard/dashboards/admin/routers/extensions/extraroutes/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openstack_dashboard/dashboards/admin/routers/extensions/extraroutes/tables.py b/openstack_dashboard/dashboards/admin/routers/extensions/extraroutes/tables.py new file mode 100644 index 0000000000..936f7a6bcc --- /dev/null +++ b/openstack_dashboard/dashboards/admin/routers/extensions/extraroutes/tables.py @@ -0,0 +1,27 @@ +# Copyright 2015, Thales Services SAS +# All Rights Reserved. +# +# 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.utils.translation import ugettext_lazy as _ + +from openstack_dashboard.dashboards.project.routers.extensions.extraroutes\ + import tables as routes_table + + +class AdminRouterRoutesTable(routes_table.ExtraRoutesTable): + + class Meta(object): + # Redifine Meta class to disable action (admin) + name = "extra_routes" + verbose_name = _("Static Routes") diff --git a/openstack_dashboard/dashboards/admin/routers/tabs.py b/openstack_dashboard/dashboards/admin/routers/tabs.py index a60389fc8a..2a94bcf069 100644 --- a/openstack_dashboard/dashboards/admin/routers/tabs.py +++ b/openstack_dashboard/dashboards/admin/routers/tabs.py @@ -12,7 +12,11 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack_dashboard.dashboards.admin.routers.extensions.extraroutes\ + import tables as ertbl from openstack_dashboard.dashboards.admin.routers.ports import tables as ptbl +from openstack_dashboard.dashboards.project.routers.extensions.extraroutes\ + import tabs as er_tabs from openstack_dashboard.dashboards.project.routers.extensions.routerrules\ import tabs as rr_tabs from openstack_dashboard.dashboards.project.routers import tabs as r_tabs @@ -22,11 +26,15 @@ class OverviewTab(r_tabs.OverviewTab): template_name = "project/routers/_detail_overview.html" +class ExtraRoutesTab(er_tabs.ExtraRoutesTab): + table_classes = (ertbl.AdminRouterRoutesTable,) + + class InterfacesTab(r_tabs.InterfacesTab): table_classes = (ptbl.PortsTable,) class RouterDetailTabs(r_tabs.RouterDetailTabs): - tabs = (OverviewTab, InterfacesTab, rr_tabs.RulesGridTab, + tabs = (OverviewTab, InterfacesTab, ExtraRoutesTab, rr_tabs.RulesGridTab, rr_tabs.RouterRulesTab) sticky = True diff --git a/openstack_dashboard/dashboards/admin/routers/tests.py b/openstack_dashboard/dashboards/admin/routers/tests.py index eeb1559a3b..22eea0b9fd 100644 --- a/openstack_dashboard/dashboards/admin/routers/tests.py +++ b/openstack_dashboard/dashboards/admin/routers/tests.py @@ -162,3 +162,9 @@ class RouterTests(test.BaseAdminViewTests, r_test.RouterTests): self.assertNoFormErrors(res) self.assertMessageCount(response=res, success=1) self.assertIn('Deleted Router: ' + router.name, res.content) + + +class RouterRouteTest(test.BaseAdminViewTests, r_test.RouterRouteTests): + DASHBOARD = 'admin' + INDEX_URL = reverse('horizon:%s:routers:index' % DASHBOARD) + DETAIL_PATH = 'horizon:%s:routers:detail' % DASHBOARD diff --git a/openstack_dashboard/dashboards/project/routers/extensions/extraroutes/__init__.py b/openstack_dashboard/dashboards/project/routers/extensions/extraroutes/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openstack_dashboard/dashboards/project/routers/extensions/extraroutes/forms.py b/openstack_dashboard/dashboards/project/routers/extensions/extraroutes/forms.py new file mode 100644 index 0000000000..ae6ab70f5d --- /dev/null +++ b/openstack_dashboard/dashboards/project/routers/extensions/extraroutes/forms.py @@ -0,0 +1,60 @@ +# Copyright 2015, Thales Services SAS +# All Rights Reserved. +# +# 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.core.urlresolvers import reverse +from django.utils.translation import ugettext_lazy as _ + +from horizon import exceptions +from horizon import forms +from horizon import messages + +from neutronclient.common import exceptions as neutron_exc + +from openstack_dashboard.api import neutron as api + +LOG = logging.getLogger(__name__) + + +class AddRouterRoute(forms.SelfHandlingForm): + destination = forms.IPField(label=_("Destination CIDR"), mask=True) + nexthop = forms.IPField(label=_("Next Hop")) + failure_url = 'horizon:project:routers:detail' + + def handle(self, request, data, **kwargs): + router_id = self.initial['router_id'] + try: + route = {'nexthop': data['nexthop'], + 'destination': data['destination']} + api.router_static_route_add(request, + router_id, + route) + msg = _('Static route added') + LOG.debug(msg) + messages.success(request, msg) + return True + except neutron_exc.BadRequest as e: + msg = (_('Invalid format for routes : %s') % e) + LOG.info(msg) + messages.error(request, msg) + redirect = reverse(self.failure_url, args=[router_id]) + exceptions.handle(request, msg, redirect=redirect) + except Exception as e: + msg = (_('Failed to add route : %s') % e) + LOG.info(msg) + messages.error(request, msg) + redirect = reverse(self.failure_url, args=[router_id]) + exceptions.handle(request, msg, redirect=redirect) diff --git a/openstack_dashboard/dashboards/project/routers/extensions/extraroutes/tables.py b/openstack_dashboard/dashboards/project/routers/extensions/extraroutes/tables.py new file mode 100644 index 0000000000..10e1cdbf4a --- /dev/null +++ b/openstack_dashboard/dashboards/project/routers/extensions/extraroutes/tables.py @@ -0,0 +1,77 @@ +# Copyright 2015, Thales Services SAS +# All Rights Reserved. +# +# 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 django.utils.translation import ungettext_lazy + +from openstack_dashboard.api import neutron as api +from openstack_dashboard import policy + +from horizon import tables + + +class AddRouterRoute(policy.PolicyTargetMixin, tables.LinkAction): + name = "create" + verbose_name = _("Add Static Route") + url = "horizon:project:routers:addrouterroute" + classes = ("ajax-modal",) + icon = "plus" + policy_rules = (("network", "update_router"),) + + def get_link_url(self, datum=None): + router_id = self.table.kwargs['router_id'] + return reverse(self.url, args=(router_id,)) + + +class RemoveRouterRoute(policy.PolicyTargetMixin, tables.DeleteAction): + @staticmethod + def action_present(count): + return ungettext_lazy( + u"Delete Static Route", + u"Delete Static Routes", + count + ) + + @staticmethod + def action_past(count): + return ungettext_lazy( + u"Deleted Static Route", + u"Deleted Static Routes", + count + ) + failure_url = 'horizon:project:routers:detail' + policy_rules = (("network", "update_router"),) + + def delete(self, request, obj_id): + router_id = self.table.kwargs['router_id'] + api.router_static_route_remove(request, router_id, [obj_id]) + + +class ExtraRoutesTable(tables.DataTable): + destination = tables.Column("destination", + verbose_name=_("Destination CIDR")) + nexthop = tables.Column("nexthop", verbose_name=_("Next Hop")) + + def get_object_display(self, datum): + """Display ExtraRoutes when deleted.""" + return (super(ExtraRoutesTable, self).get_object_display(datum) + or datum.destination + " -> " + datum.nexthop) + + class Meta(object): + name = "extra_routes" + verbose_name = _("Static Routes") + table_actions = (AddRouterRoute, RemoveRouterRoute) + row_actions = (RemoveRouterRoute, ) diff --git a/openstack_dashboard/dashboards/project/routers/extensions/extraroutes/tabs.py b/openstack_dashboard/dashboards/project/routers/extensions/extraroutes/tabs.py new file mode 100644 index 0000000000..cc0f01f953 --- /dev/null +++ b/openstack_dashboard/dashboards/project/routers/extensions/extraroutes/tabs.py @@ -0,0 +1,49 @@ +# Copyright 2015, Thales Services SAS +# All Rights Reserved. +# +# 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 tabs + +from openstack_dashboard.api import neutron as api +from openstack_dashboard.dashboards.project.routers.extensions.extraroutes\ + import tables as ertbl + + +LOG = logging.getLogger(__name__) + + +class ExtraRoutesTab(tabs.TableTab): + table_classes = (ertbl.ExtraRoutesTable,) + name = _("Static Routes") + slug = "extraroutes" + template_name = "horizon/common/_detail_table.html" + + def allowed(self, request): + try: + return api.is_extension_supported(request, 'extraroute') + except Exception: + LOG.info(_("Failed to check if Neutron extraroute extension is " + "supported")) + return False + + def get_extra_routes_data(self): + try: + extraroutes = getattr(self.tab_group.kwargs['router'], 'routes') + except AttributeError: + extraroutes = [] + return [api.RouterStaticRoute(r) for r in extraroutes] diff --git a/openstack_dashboard/dashboards/project/routers/extensions/extraroutes/views.py b/openstack_dashboard/dashboards/project/routers/extensions/extraroutes/views.py new file mode 100644 index 0000000000..d52719974b --- /dev/null +++ b/openstack_dashboard/dashboards/project/routers/extensions/extraroutes/views.py @@ -0,0 +1,59 @@ +# Copyright 2015, Thales Services SAS +# All Rights Reserved. +# +# 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.core.urlresolvers import reverse +from django.utils.translation import ugettext_lazy as _ + +from horizon import exceptions +from horizon import forms +from horizon.utils import memoized + +from openstack_dashboard import api +from openstack_dashboard.dashboards.project.routers.extensions.extraroutes\ + import forms as erforms + +LOG = logging.getLogger(__name__) + + +class AddRouterRouteView(forms.ModalFormView): + form_class = erforms.AddRouterRoute + template_name = 'project/routers/extensions/routerroutes/create.html' + url = 'horizon:project:routers:detail' + + def get_success_url(self): + return reverse(self.url, + args=(self.kwargs['router_id'],)) + + @memoized.memoized_method + def get_object(self): + try: + router_id = self.kwargs["router_id"] + return api.neutron.router_get(self.request, router_id) + except Exception: + redirect = reverse(self.url, args=[router_id]) + msg = _("Unable to retrieve router.") + exceptions.handle(self.request, msg, redirect=redirect) + + def get_context_data(self, **kwargs): + context = super(AddRouterRouteView, self).get_context_data(**kwargs) + context['router'] = self.get_object() + return context + + def get_initial(self): + router = self.get_object() + return {"router_id": self.kwargs['router_id'], + "router_name": router.name} diff --git a/openstack_dashboard/dashboards/project/routers/tabs.py b/openstack_dashboard/dashboards/project/routers/tabs.py index 1dbf1ab4a1..04046eb75c 100644 --- a/openstack_dashboard/dashboards/project/routers/tabs.py +++ b/openstack_dashboard/dashboards/project/routers/tabs.py @@ -16,6 +16,8 @@ from django.utils.translation import ugettext_lazy as _ from horizon import tabs +from openstack_dashboard.dashboards.project.routers.extensions.extraroutes\ + import tabs as er_tabs from openstack_dashboard.dashboards.project.routers.extensions.routerrules\ import tabs as rr_tabs from openstack_dashboard.dashboards.project.routers.ports import tables as ptbl @@ -42,6 +44,6 @@ class InterfacesTab(tabs.TableTab): class RouterDetailTabs(tabs.TabGroup): slug = "router_details" - tabs = (OverviewTab, InterfacesTab, rr_tabs.RulesGridTab, - rr_tabs.RouterRulesTab) + tabs = (OverviewTab, InterfacesTab, er_tabs.ExtraRoutesTab, + rr_tabs.RulesGridTab, rr_tabs.RouterRulesTab) sticky = True diff --git a/openstack_dashboard/dashboards/project/routers/templates/routers/extensions/routerroutes/_create.html b/openstack_dashboard/dashboards/project/routers/templates/routers/extensions/routerroutes/_create.html new file mode 100644 index 0000000000..c7b2ecb899 --- /dev/null +++ b/openstack_dashboard/dashboards/project/routers/templates/routers/extensions/routerroutes/_create.html @@ -0,0 +1,29 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} +{% load url from future %} + +{% block form_id %}add_routerroute_form{% endblock %} +{% block form_action %}{% url 'horizon:project:routers:addrouterroute' router.id %} +{% endblock %} + +{% block modal-header %}{% trans "Add Static Route" %}{% endblock %} + +{% block modal-body %} +
+
+ {% include "horizon/common/_form_fields.html" %} +
+
+
+

{% trans "Description:" %}

+

+ {% trans "Add static route to the router." %}
+ {% trans "Next Hop IP must be a part of one of the subnets to which the router interfaces are connected." %} +

+
+{% endblock %} + +{% block modal-footer %} + + {% trans "Cancel" %} +{% endblock %} diff --git a/openstack_dashboard/dashboards/project/routers/templates/routers/extensions/routerroutes/create.html b/openstack_dashboard/dashboards/project/routers/templates/routers/extensions/routerroutes/create.html new file mode 100644 index 0000000000..261eac858c --- /dev/null +++ b/openstack_dashboard/dashboards/project/routers/templates/routers/extensions/routerroutes/create.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Add Router Route" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Add Router Route") %} +{% endblock page_header %} + +{% block main %} + {% include "project/routers/extensions/routerroutes/_create.html" %} +{% endblock %} diff --git a/openstack_dashboard/dashboards/project/routers/tests.py b/openstack_dashboard/dashboards/project/routers/tests.py index cbfcfb8618..840c472b67 100644 --- a/openstack_dashboard/dashboards/project/routers/tests.py +++ b/openstack_dashboard/dashboards/project/routers/tests.py @@ -30,9 +30,11 @@ from openstack_dashboard.usage import quotas class RouterMixin(object): @test.create_stubs({ api.neutron: ('router_get', 'port_list', - 'network_get'), + 'network_get', 'is_extension_supported'), }) - def _get_detail(self, router): + def _get_detail(self, router, extraroute=True): + api.neutron.is_extension_supported(IsA(http.HttpRequest), 'extraroute')\ + .MultipleTimes().AndReturn(extraroute) api.neutron.router_get(IsA(http.HttpRequest), router.id)\ .AndReturn(router) api.neutron.port_list(IsA(http.HttpRequest), @@ -750,11 +752,14 @@ class RouterRuleTests(RouterMixin, test.TestCase): self._test_router_addrouterrule(raise_error=True) @test.create_stubs({api.neutron: ('router_get', 'router_update', - 'port_list', 'network_get')}) + 'port_list', 'network_get', + 'is_extension_supported')}) def test_router_removerouterrule(self): pre_router = self.routers_with_rules.first() post_router = copy.deepcopy(pre_router) rule = post_router['router_rules'].pop() + api.neutron.is_extension_supported(IsA(http.HttpRequest), 'extraroute')\ + .AndReturn(False) api.neutron.router_get(IsA(http.HttpRequest), pre_router.id).AndReturn(pre_router) params = {} @@ -779,7 +784,8 @@ class RouterRuleTests(RouterMixin, test.TestCase): @test.create_stubs({api.neutron: ('router_get', 'router_update', 'network_list', 'port_list', - 'network_get')}) + 'network_get', + 'is_extension_supported')}) def test_router_resetrouterrules(self): pre_router = self.routers_with_rules.first() post_router = copy.deepcopy(pre_router) @@ -787,6 +793,8 @@ class RouterRuleTests(RouterMixin, test.TestCase): 'action': 'permit', 'nexthops': [], 'id': '2'}] del post_router['router_rules'][:] post_router['router_rules'].extend(default_rules) + api.neutron.is_extension_supported(IsA(http.HttpRequest), 'extraroute')\ + .AndReturn(False) api.neutron.router_get(IsA(http.HttpRequest), pre_router.id).AndReturn(post_router) params = {} @@ -810,6 +818,96 @@ class RouterRuleTests(RouterMixin, test.TestCase): self.assertNoFormErrors(res) +class RouterRouteTests(RouterMixin, test.TestCase): + DASHBOARD = 'project' + INDEX_URL = reverse('horizon:%s:routers:index' % DASHBOARD) + DETAIL_PATH = 'horizon:%s:routers:detail' % DASHBOARD + + def test_extension_hides_without_routes(self): + router = self.routers_with_routes.first() + res = self._get_detail(router, extraroute=False) + + self.assertTemplateUsed(res, '%s/routers/detail.html' % self.DASHBOARD) + self.assertNotIn('extra_routes_table', res.context) + + def test_routerroute_detail(self): + router = self.routers_with_routes.first() + res = self._get_detail(router, extraroute=True) + + self.assertTemplateUsed(res, '%s/routers/detail.html' % self.DASHBOARD) + routes = res.context['extra_routes_table'].data + routes_dict = [r._apidict for r in routes] + self.assertItemsEqual(routes_dict, router['routes']) + + @test.create_stubs({api.neutron: ('router_get', 'router_update')}) + def _test_router_addrouterroute(self, raise_error=False): + pre_router = self.routers_with_routes.first() + post_router = copy.deepcopy(pre_router) + route = {'nexthop': '10.0.0.5', 'destination': '40.0.1.0/24'} + post_router['routes'].insert(0, route) + api.neutron.router_get(IsA(http.HttpRequest), pre_router.id)\ + .MultipleTimes().AndReturn(pre_router) + params = {} + params['routes'] = post_router['routes'] + router_update = api.neutron.router_update(IsA(http.HttpRequest), + pre_router.id, **params) + if raise_error: + router_update.AndRaise(self.exceptions.neutron) + else: + router_update.AndReturn({'router': post_router}) + self.mox.ReplayAll() + + form_data = copy.deepcopy(route) + form_data['router_id'] = pre_router.id + url = reverse('horizon:%s:routers:addrouterroute' % self.DASHBOARD, + args=[pre_router.id]) + res = self.client.post(url, form_data) + self.assertNoFormErrors(res) + detail_url = reverse(self.DETAIL_PATH, args=[pre_router.id]) + self.assertRedirectsNoFollow(res, detail_url) + + def test_router_addrouterroute(self): + if self.DASHBOARD == 'project': + self._test_router_addrouterroute() + self.assertMessageCount(success=1) + + def test_router_addrouterroute_exception(self): + if self.DASHBOARD == 'project': + self._test_router_addrouterroute(raise_error=True) + self.assertMessageCount(error=1) + + @test.create_stubs({api.neutron: ('router_get', 'router_update', + 'network_get', 'port_list', + 'is_extension_supported')}) + def test_router_removeroute(self): + if self.DASHBOARD == 'admin': + return + pre_router = self.routers_with_routes.first() + post_router = copy.deepcopy(pre_router) + route = post_router['routes'].pop() + api.neutron.is_extension_supported(IsA(http.HttpRequest), 'extraroute')\ + .MultipleTimes().AndReturn(True) + api.neutron.router_get(IsA(http.HttpRequest), + pre_router.id).AndReturn(pre_router) + params = {} + params['routes'] = post_router['routes'] + api.neutron.router_get(IsA(http.HttpRequest), + pre_router.id).AndReturn(pre_router) + router_update = api.neutron.router_update(IsA(http.HttpRequest), + pre_router.id, **params) + router_update.AndReturn({'router': post_router}) + api.neutron.port_list(IsA(http.HttpRequest), + device_id=pre_router.id)\ + .AndReturn([self.ports.first()]) + self._mock_external_network_get(pre_router) + self.mox.ReplayAll() + form_route_id = route['nexthop'] + ":" + route['destination'] + form_data = {'action': 'extra_routes__delete__%s' % form_route_id} + url = reverse(self.DETAIL_PATH, args=[pre_router.id]) + res = self.client.post(url, form_data) + self.assertNoFormErrors(res) + + class RouterViewTests(RouterMixin, test.TestCase): DASHBOARD = 'project' INDEX_URL = reverse('horizon:%s:routers:index' % DASHBOARD) diff --git a/openstack_dashboard/dashboards/project/routers/urls.py b/openstack_dashboard/dashboards/project/routers/urls.py index 36529c1c90..cd700e6aa5 100644 --- a/openstack_dashboard/dashboards/project/routers/urls.py +++ b/openstack_dashboard/dashboards/project/routers/urls.py @@ -15,6 +15,8 @@ from django.conf.urls import patterns from django.conf.urls import url +from openstack_dashboard.dashboards.project.routers.extensions.extraroutes\ + import views as er_views from openstack_dashboard.dashboards.project.routers.extensions.routerrules\ import views as rr_views from openstack_dashboard.dashboards.project.routers.ports \ @@ -41,6 +43,9 @@ urlpatterns = patterns( url(ROUTER_URL % 'addrouterrule', rr_views.AddRouterRuleView.as_view(), name='addrouterrule'), + url(ROUTER_URL % 'addrouterroute', + er_views.AddRouterRouteView.as_view(), + name='addrouterroute'), url(ROUTER_URL % 'setgateway', port_views.SetGatewayView.as_view(), name='setgateway'), diff --git a/openstack_dashboard/test/api_tests/neutron_tests.py b/openstack_dashboard/test/api_tests/neutron_tests.py index 5f9940ad62..238b691a72 100644 --- a/openstack_dashboard/test/api_tests/neutron_tests.py +++ b/openstack_dashboard/test/api_tests/neutron_tests.py @@ -11,6 +11,7 @@ # 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 copy import uuid @@ -289,6 +290,50 @@ class NeutronApiTests(test.APITestCase): self.assertFalse( api.neutron.is_extension_supported(self.request, 'doesntexist')) + def test_router_static_route_list(self): + router = {'router': self.api_routers_with_routes.first()} + router_id = self.api_routers_with_routes.first()['id'] + + neutronclient = self.stub_neutronclient() + neutronclient.show_router(router_id).AndReturn(router) + self.mox.ReplayAll() + + ret_val = api.neutron.router_static_route_list(self.request, router_id) + self.assertIsInstance(ret_val[0], api.neutron.RouterStaticRoute) + + def test_router_static_route_remove(self): + router = {'router': self.api_routers_with_routes.first()} + router_id = self.api_routers_with_routes.first()['id'] + post_router = copy.deepcopy(router) + route = api.neutron.RouterStaticRoute(post_router['router'] + ['routes'].pop()) + + neutronclient = self.stub_neutronclient() + neutronclient.show_router(router_id).AndReturn(router) + body = {'router': {'routes': post_router['router']['routes']}} + neutronclient.update_router(router_id, body=body)\ + .AndReturn(post_router) + self.mox.ReplayAll() + + api.neutron.router_static_route_remove(self.request, + router_id, route.id) + + def test_router_static_route_add(self): + router = {'router': self.api_routers_with_routes.first()} + router_id = self.api_routers_with_routes.first()['id'] + post_router = copy.deepcopy(router) + route = {'nexthop': '10.0.0.5', 'destination': '40.0.1.0/24'} + post_router['router']['routes'].insert(0, route) + body = {'router': {'routes': post_router['router']['routes']}} + + neutronclient = self.stub_neutronclient() + neutronclient.show_router(router_id).AndReturn(router) + neutronclient.update_router(router_id, body=body)\ + .AndReturn(post_router) + self.mox.ReplayAll() + + api.neutron.router_static_route_add(self.request, router_id, route) + # NOTE(amotoki): "dvr" permission tests check most of # get_feature_permission features. # These tests are not specific to "dvr" extension. diff --git a/openstack_dashboard/test/test_data/neutron_data.py b/openstack_dashboard/test/test_data/neutron_data.py index 0bc646e60e..6e25725443 100644 --- a/openstack_dashboard/test/test_data/neutron_data.py +++ b/openstack_dashboard/test/test_data/neutron_data.py @@ -31,6 +31,7 @@ def data(TEST): TEST.ports = utils.TestDataContainer() TEST.routers = utils.TestDataContainer() TEST.routers_with_rules = utils.TestDataContainer() + TEST.routers_with_routes = utils.TestDataContainer() TEST.q_floating_ips = utils.TestDataContainer() TEST.q_secgroups = utils.TestDataContainer() TEST.q_secgroup_rules = utils.TestDataContainer() @@ -58,6 +59,7 @@ def data(TEST): TEST.api_subnets = utils.TestDataContainer() TEST.api_ports = utils.TestDataContainer() TEST.api_routers = utils.TestDataContainer() + TEST.api_routers_with_routes = utils.TestDataContainer() TEST.api_q_floating_ips = utils.TestDataContainer() TEST.api_q_secgroups = utils.TestDataContainer() TEST.api_q_secgroup_rules = utils.TestDataContainer() @@ -404,6 +406,20 @@ def data(TEST): 'nexthops': ['1.0.0.2', '1.0.0.1']}]} TEST.api_routers.add(router_dict) TEST.routers_with_rules.add(neutron.Router(router_dict)) + router_dict_with_route = {'id': '725c24c9-061b-416b-b9d4-012392b32fd9', + 'name': 'routerouter', + 'status': 'ACTIVE', + 'admin_state_up': True, + 'distributed': False, + 'external_gateway_info': + {'network_id': ext_net['id']}, + 'tenant_id': '1', + 'routes': [{'nexthop': '10.0.0.1', + 'destination': '172.0.0.0/24'}, + {'nexthop': '10.0.0.2', + 'destination': '172.1.0.0/24'}]} + TEST.api_routers_with_routes.add(router_dict_with_route) + TEST.routers_with_routes.add(neutron.Router(router_dict_with_route)) # Floating IP. # Unassociated.