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 %} +
+ {% 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." %}
+