From eed092a5bb725e58ec45c0f469c2b8c73d59c46a Mon Sep 17 00:00:00 2001 From: Nachi Ueno Date: Thu, 20 Sep 2012 07:10:06 +0000 Subject: [PATCH] Support Quantum L3 function Implements bp quantum-l3-support Implemented basic CRD for router Add/remove interface support Support set gateway and clear gateway Change-Id: Ie4cac962eb8fadc021c80cf05e2aa63caab3c00a --- openstack_dashboard/api/quantum.py | 61 ++++- .../dashboards/admin/dashboard.py | 2 +- .../dashboards/admin/routers/__init__.py | 0 .../dashboards/admin/routers/forms.py | 71 ++++++ .../dashboards/admin/routers/panel.py | 29 +++ .../admin/routers/ports/__init__.py | 0 .../dashboards/admin/routers/ports/forms.py | 31 +++ .../dashboards/admin/routers/ports/tables.py | 64 +++++ .../dashboards/admin/routers/ports/tabs.py | 31 +++ .../dashboards/admin/routers/ports/urls.py | 24 ++ .../dashboards/admin/routers/ports/views.py | 43 ++++ .../dashboards/admin/routers/tables.py | 71 ++++++ .../dashboards/admin/routers/tabs.py | 28 +++ .../routers/templates/routers/_create.html | 21 ++ .../templates/routers/_detail_overview.html | 15 ++ .../routers/templates/routers/create.html | 11 + .../routers/templates/routers/detail.html | 15 ++ .../routers/templates/routers/index.html | 11 + .../dashboards/admin/routers/tests.py | 104 ++++++++ .../dashboards/admin/routers/urls.py | 34 +++ .../dashboards/admin/routers/views.py | 76 ++++++ .../dashboards/project/dashboard.py | 3 +- .../dashboards/project/routers/__init__.py | 0 .../dashboards/project/routers/forms.py | 41 +++ .../dashboards/project/routers/panel.py | 29 +++ .../project/routers/ports/__init__.py | 0 .../dashboards/project/routers/ports/forms.py | 132 ++++++++++ .../project/routers/ports/tables.py | 103 ++++++++ .../dashboards/project/routers/ports/tabs.py | 47 ++++ .../dashboards/project/routers/ports/urls.py | 24 ++ .../dashboards/project/routers/ports/views.py | 100 ++++++++ .../dashboards/project/routers/tables.py | 90 +++++++ .../dashboards/project/routers/tabs.py | 45 ++++ .../routers/templates/routers/_create.html | 21 ++ .../templates/routers/_detail_overview.html | 15 ++ .../routers/templates/routers/create.html | 11 + .../routers/templates/routers/detail.html | 15 ++ .../routers/templates/routers/index.html | 11 + .../templates/routers/ports/_create.html | 25 ++ .../templates/routers/ports/_setgateway.html | 25 ++ .../templates/routers/ports/create.html | 11 + .../templates/routers/ports/setgateway.html | 11 + .../dashboards/project/routers/tests.py | 235 ++++++++++++++++++ .../dashboards/project/routers/urls.py | 34 +++ .../dashboards/project/routers/views.py | 105 ++++++++ .../test/api_tests/quantum_tests.py | 67 +++++ .../test/test_data/quantum_data.py | 32 ++- 47 files changed, 1968 insertions(+), 6 deletions(-) create mode 100644 openstack_dashboard/dashboards/admin/routers/__init__.py create mode 100644 openstack_dashboard/dashboards/admin/routers/forms.py create mode 100644 openstack_dashboard/dashboards/admin/routers/panel.py create mode 100644 openstack_dashboard/dashboards/admin/routers/ports/__init__.py create mode 100644 openstack_dashboard/dashboards/admin/routers/ports/forms.py create mode 100644 openstack_dashboard/dashboards/admin/routers/ports/tables.py create mode 100644 openstack_dashboard/dashboards/admin/routers/ports/tabs.py create mode 100644 openstack_dashboard/dashboards/admin/routers/ports/urls.py create mode 100644 openstack_dashboard/dashboards/admin/routers/ports/views.py create mode 100644 openstack_dashboard/dashboards/admin/routers/tables.py create mode 100644 openstack_dashboard/dashboards/admin/routers/tabs.py create mode 100644 openstack_dashboard/dashboards/admin/routers/templates/routers/_create.html create mode 100644 openstack_dashboard/dashboards/admin/routers/templates/routers/_detail_overview.html create mode 100644 openstack_dashboard/dashboards/admin/routers/templates/routers/create.html create mode 100644 openstack_dashboard/dashboards/admin/routers/templates/routers/detail.html create mode 100644 openstack_dashboard/dashboards/admin/routers/templates/routers/index.html create mode 100644 openstack_dashboard/dashboards/admin/routers/tests.py create mode 100644 openstack_dashboard/dashboards/admin/routers/urls.py create mode 100644 openstack_dashboard/dashboards/admin/routers/views.py create mode 100644 openstack_dashboard/dashboards/project/routers/__init__.py create mode 100644 openstack_dashboard/dashboards/project/routers/forms.py create mode 100644 openstack_dashboard/dashboards/project/routers/panel.py create mode 100644 openstack_dashboard/dashboards/project/routers/ports/__init__.py create mode 100644 openstack_dashboard/dashboards/project/routers/ports/forms.py create mode 100644 openstack_dashboard/dashboards/project/routers/ports/tables.py create mode 100644 openstack_dashboard/dashboards/project/routers/ports/tabs.py create mode 100644 openstack_dashboard/dashboards/project/routers/ports/urls.py create mode 100644 openstack_dashboard/dashboards/project/routers/ports/views.py create mode 100644 openstack_dashboard/dashboards/project/routers/tables.py create mode 100644 openstack_dashboard/dashboards/project/routers/tabs.py create mode 100644 openstack_dashboard/dashboards/project/routers/templates/routers/_create.html create mode 100644 openstack_dashboard/dashboards/project/routers/templates/routers/_detail_overview.html create mode 100644 openstack_dashboard/dashboards/project/routers/templates/routers/create.html create mode 100644 openstack_dashboard/dashboards/project/routers/templates/routers/detail.html create mode 100644 openstack_dashboard/dashboards/project/routers/templates/routers/index.html create mode 100644 openstack_dashboard/dashboards/project/routers/templates/routers/ports/_create.html create mode 100644 openstack_dashboard/dashboards/project/routers/templates/routers/ports/_setgateway.html create mode 100644 openstack_dashboard/dashboards/project/routers/templates/routers/ports/create.html create mode 100644 openstack_dashboard/dashboards/project/routers/templates/routers/ports/setgateway.html create mode 100644 openstack_dashboard/dashboards/project/routers/tests.py create mode 100644 openstack_dashboard/dashboards/project/routers/urls.py create mode 100644 openstack_dashboard/dashboards/project/routers/views.py diff --git a/openstack_dashboard/api/quantum.py b/openstack_dashboard/api/quantum.py index 2c2f26e28d..c41309bfc4 100644 --- a/openstack_dashboard/api/quantum.py +++ b/openstack_dashboard/api/quantum.py @@ -78,6 +78,15 @@ class Port(QuantumAPIDictWrapper): super(Port, self).__init__(apiresource) +class Router(QuantumAPIDictWrapper): + """Wrapper for quantum routers""" + + def __init__(self, apiresource): + #apiresource['admin_state'] = \ + # 'UP' if apiresource['admin_state_up'] else 'DOWN' + super(Router, self).__init__(apiresource) + + IP_VERSION_DICT = {4: 'IPv4', 6: 'IPv6'} @@ -104,7 +113,7 @@ def network_list(request, **params): subnet_dict = SortedDict([(s['id'], s) for s in subnets]) # Expand subnet list from subnet_id to values. for n in networks: - n['subnets'] = [subnet_dict[s] for s in n['subnets']] + n['subnets'] = [subnet_dict.get(s) for s in n.get('subnets', [])] return [Network(n) for n in networks] @@ -256,3 +265,53 @@ def port_modify(request, port_id, **kwargs): body = {'port': kwargs} port = quantumclient(request).update_port(port_id, body=body).get('port') return Port(port) + + +def router_create(request, **kwargs): + LOG.debug("router_create():, kwargs=%s" % kwargs) + body = {'router': {}} + body['router'].update(kwargs) + router = quantumclient(request).create_router(body=body).get('router') + return Router(router) + + +def router_get(request, router_id, **params): + router = quantumclient(request).show_router(router_id, + **params).get('router') + return Router(router) + + +def router_list(request, **params): + routers = quantumclient(request).list_routers(**params).get('routers') + return [Router(r) for r in routers] + + +def router_delete(request, router_id): + quantumclient(request).delete_router(router_id) + + +def router_add_interface(request, router_id, subnet_id=None, port_id=None): + body = {} + if subnet_id: + body['subnet_id'] = subnet_id + if port_id: + body['port_id'] = port_id + quantumclient(request).add_interface_router(router_id, body) + + +def router_remove_interface(request, router_id, subnet_id=None, port_id=None): + body = {} + if subnet_id: + body['subnet_id'] = subnet_id + if port_id: + body['port_id'] = port_id + quantumclient(request).remove_interface_router(router_id, body) + + +def router_add_gateway(request, router_id, network_id): + body = {'network_id': network_id} + quantumclient(request).add_gateway_router(router_id, body) + + +def router_remove_gateway(request, router_id): + quantumclient(request).remove_gateway_router(router_id) diff --git a/openstack_dashboard/dashboards/admin/dashboard.py b/openstack_dashboard/dashboards/admin/dashboard.py index e556f97e3c..054c250e93 100644 --- a/openstack_dashboard/dashboards/admin/dashboard.py +++ b/openstack_dashboard/dashboards/admin/dashboard.py @@ -23,7 +23,7 @@ class SystemPanels(horizon.PanelGroup): slug = "admin" name = _("System Panel") panels = ('overview', 'instances', 'volumes', 'flavors', - 'images', 'projects', 'users', 'networks', 'info') + 'images', 'projects', 'users', 'networks', 'routers', 'info') class Admin(horizon.Dashboard): diff --git a/openstack_dashboard/dashboards/admin/routers/__init__.py b/openstack_dashboard/dashboards/admin/routers/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openstack_dashboard/dashboards/admin/routers/forms.py b/openstack_dashboard/dashboards/admin/routers/forms.py new file mode 100644 index 0000000000..dc83dd7b47 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/routers/forms.py @@ -0,0 +1,71 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012, Nachi Ueno, NTT MCL, Inc. +# +# 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. + +""" +Views for managing Quantum Routers. +""" +import logging + +from django.core.urlresolvers import reverse +from django.utils.translation import ugettext_lazy as _ + +from horizon import forms +from horizon import exceptions +from horizon import messages +from openstack_dashboard import api + +LOG = logging.getLogger(__name__) + + +class CreateForm(forms.SelfHandlingForm): + name = forms.CharField(max_length="255", + label=_("Router Name"), + required=False) + tenant_id = forms.ChoiceField(label=_("Project")) + failure_url = 'horizon:admin:routers:index' + + def __init__(self, request, *args, **kwargs): + super(CreateForm, self).__init__(request, *args, **kwargs) + tenant_choices = [('', _("Select a project"))] + try: + for tenant in api.keystone.tenant_list(request, admin=True): + if tenant.enabled: + tenant_choices.append((tenant.id, tenant.name)) + except: + msg = _('Failed to get tenants.') + LOG.warn(msg) + redirect = reverse(self.failure_url) + exceptions.handle(request, msg, redirect=redirect) + return False + + self.fields['tenant_id'].choices = tenant_choices + + def handle(self, request, data): + try: + params = {} + if data.get('tenant_id'): + params['tenant_id'] = data['tenant_id'] + router = api.quantum.router_create(request, + name=data['name'], **params) + message = 'Creating router "%s"' % data['name'] + messages.info(request, message) + return router + except: + msg = _('Failed to create router "%s".') % data['name'] + LOG.warn(msg) + redirect = reverse(self.failure_url) + exceptions.handle(request, msg, redirect=redirect) + return False diff --git a/openstack_dashboard/dashboards/admin/routers/panel.py b/openstack_dashboard/dashboards/admin/routers/panel.py new file mode 100644 index 0000000000..ccb9c996b4 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/routers/panel.py @@ -0,0 +1,29 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012, Nachi Ueno, NTT MCL, Inc. +# +# 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 _ + +import horizon + +from openstack_dashboard.dashboards.admin import dashboard + + +class Routers(horizon.Panel): + name = "Routers" + slug = 'routers' + permissions = ('openstack.services.network',) + +dashboard.Admin.register(Routers) diff --git a/openstack_dashboard/dashboards/admin/routers/ports/__init__.py b/openstack_dashboard/dashboards/admin/routers/ports/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openstack_dashboard/dashboards/admin/routers/ports/forms.py b/openstack_dashboard/dashboards/admin/routers/ports/forms.py new file mode 100644 index 0000000000..e25fa69c72 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/routers/ports/forms.py @@ -0,0 +1,31 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012, Nachi Ueno, NTT MCL, Inc. +# +# 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 openstack_dashboard.dashboards.project.routers.ports import ( + forms as p_forms) + + +LOG = logging.getLogger(__name__) + + +class AddInterface(p_forms.AddInterface): + failure_url = 'horizon:admin:routers:detail' + + +class SetGatewayForm(p_forms.SetGatewayForm): + failure_url = 'horizon:admin:routers:detail' diff --git a/openstack_dashboard/dashboards/admin/routers/ports/tables.py b/openstack_dashboard/dashboards/admin/routers/ports/tables.py new file mode 100644 index 0000000000..f064367f01 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/routers/ports/tables.py @@ -0,0 +1,64 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 NEC Corporation +# +# 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 tables +from openstack_dashboard.dashboards.project.networks.ports.tables import\ + get_fixed_ips, get_attached +from openstack_dashboard.dashboards.project.routers.ports import\ + tables as r_tables +from openstack_dashboard.dashboards.project.routers.ports.tables import\ + get_device_owner + + +LOG = logging.getLogger(__name__) + + +class SetGateway(r_tables.SetGateway): + url = "horizon:admin:routers:setgateway" + + +class AddInterface(r_tables.AddInterface): + url = "horizon:admin:routers:addinterface" + + +class RemoveInterface(r_tables.RemoveInterface): + failure_url = 'horizon:admin:routers:detail' + + +class PortsTable(tables.DataTable): + name = tables.Column("name", + verbose_name=_("Name"), + link="horizon:admin:networks:ports:detail") + fixed_ips = tables.Column(get_fixed_ips, verbose_name=_("Fixed IPs")) + attached = tables.Column(get_attached, verbose_name=_("Device Attached")) + status = tables.Column("status", verbose_name=_("Status")) + device_owner = tables.Column(get_device_owner, + verbose_name=_("Type")) + admin_state = tables.Column("admin_state", + verbose_name=_("Admin State")) + + def get_object_display(self, port): + return port.id + + class Meta: + name = "interfaces" + verbose_name = _("Interfaces") + table_actions = (AddInterface, SetGateway, RemoveInterface) + row_actions = (RemoveInterface, ) diff --git a/openstack_dashboard/dashboards/admin/routers/ports/tabs.py b/openstack_dashboard/dashboards/admin/routers/ports/tabs.py new file mode 100644 index 0000000000..16d63b85dd --- /dev/null +++ b/openstack_dashboard/dashboards/admin/routers/ports/tabs.py @@ -0,0 +1,31 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012, Nachi Ueno, NTT MCL, Inc. +# +# 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 horizon import tabs +from openstack_dashboard.dashboards.project.routers.ports import tabs as r_tabs + +LOG = logging.getLogger(__name__) + + +class OverviewTab(r_tabs.OverviewTab): + template_name = "admin/networks/ports/_detail_overview.html" + failure_url = "horizon:admin:routers:index" + + +class PortDetailTabs(tabs.TabGroup): + slug = "port_details" + tabs = (OverviewTab,) diff --git a/openstack_dashboard/dashboards/admin/routers/ports/urls.py b/openstack_dashboard/dashboards/admin/routers/ports/urls.py new file mode 100644 index 0000000000..1618bc14e7 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/routers/ports/urls.py @@ -0,0 +1,24 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 NTT MCL +# +# 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 DetailView + +PORTS = r'^(?P[^/]+)/%s$' + +urlpatterns = patterns('horizon.dashboards.admin.networks.ports.views', + url(PORTS % 'detail', DetailView.as_view(), name='detail')) diff --git a/openstack_dashboard/dashboards/admin/routers/ports/views.py b/openstack_dashboard/dashboards/admin/routers/ports/views.py new file mode 100644 index 0000000000..7ac47ffcb4 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/routers/ports/views.py @@ -0,0 +1,43 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012, Nachi Ueno, NTT MCL, Inc. +# +# 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 horizon import tabs +from .tabs import PortDetailTabs +from .forms import (AddInterface, SetGatewayForm) +from openstack_dashboard.dashboards.project.routers.ports import views + + +LOG = logging.getLogger(__name__) + + +class AddInterfaceView(views.AddInterfaceView): + form_class = AddInterface + template_name = 'admin/routers/ports/create.html' + success_url = 'horizon:admin:routers:detail' + failure_url = 'horizon:admin:routers:detail' + + +class SetGatewayView(views.SetGatewayView): + form_class = SetGatewayForm + success_url = 'horizon:admin:routers:detail' + failure_url = 'horizon:admin:routers:detail' + + +class DetailView(tabs.TabView): + tab_group_class = PortDetailTabs + template_name = 'admin/networks/ports/detail.html' diff --git a/openstack_dashboard/dashboards/admin/routers/tables.py b/openstack_dashboard/dashboards/admin/routers/tables.py new file mode 100644 index 0000000000..85162bec80 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/routers/tables.py @@ -0,0 +1,71 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012, Nachi Ueno, NTT MCL, Inc. +# +# 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.template.defaultfilters import title +from django.utils.translation import ugettext_lazy as _ + +from horizon import tables +from openstack_dashboard import api +from openstack_dashboard.dashboards.project.routers import tables as r_tables + + +LOG = logging.getLogger(__name__) + + +class DeleteRouter(r_tables.DeleteRouter): + redirect_url = "horizon:admin:routers:index" + + def allowed(self, request, router=None): + return True + + +class CreateRouter(tables.LinkAction): + name = "create" + verbose_name = _("Create Router") + url = "horizon:admin:routers:create" + classes = ("ajax-modal", "btn-create") + + +class UpdateRow(tables.Row): + ajax = True + + def get_data(self, request, router_id): + router = api.router_get(request, router_id) + return router + + +class RoutersTable(tables.DataTable): + 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) + + def get_object_display(self, obj): + return obj.name + + class Meta: + name = "Routers" + verbose_name = _("Routers") + status_columns = ["status"] + row_class = UpdateRow + table_actions = (CreateRouter, DeleteRouter) + row_actions = (DeleteRouter, ) diff --git a/openstack_dashboard/dashboards/admin/routers/tabs.py b/openstack_dashboard/dashboards/admin/routers/tabs.py new file mode 100644 index 0000000000..4fe386f2a5 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/routers/tabs.py @@ -0,0 +1,28 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012, Nachi Ueno, NTT MCL, Inc. +# +# 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 horizon import tabs +from openstack_dashboard.dashboards.project.routers import tabs as r_tabs + + +class OverviewTab(r_tabs.OverviewTab): + template_name = ("admin/routers/_detail_overview.html") + redirect_url = 'horizon:admin:routers:index' + + +class RouterDetailTabs(tabs.TabGroup): + slug = "router_details" + tabs = (OverviewTab,) diff --git a/openstack_dashboard/dashboards/admin/routers/templates/routers/_create.html b/openstack_dashboard/dashboards/admin/routers/templates/routers/_create.html new file mode 100644 index 0000000000..f929e410b3 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/routers/templates/routers/_create.html @@ -0,0 +1,21 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n horizon humanize %} + +{% block form_id %}{% endblock %} +{% block form_action %}{% url horizon:admin:routers:create %}?{{ request.GET.urlencode }}{% endblock %} + +{% block modal_id %}create_router_modal{% endblock %} +{% block modal-header %}{% trans "Create router" %}{% endblock %} + +{% block modal-body %} +
+
+ {% include "horizon/common/_form_fields.html" %} +
+
+{% endblock %} + +{% block modal-footer %} + + {% trans "Cancel" %} +{% endblock %} diff --git a/openstack_dashboard/dashboards/admin/routers/templates/routers/_detail_overview.html b/openstack_dashboard/dashboards/admin/routers/templates/routers/_detail_overview.html new file mode 100644 index 0000000000..29d86ed185 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/routers/templates/routers/_detail_overview.html @@ -0,0 +1,15 @@ +{% load i18n sizeformat parse_date %} + +

{% trans "Router Overview" %}: {{router.display_name }}

+ +
+
+
{% trans "Name" %}
+
{{ router.display_name }}
+
{% trans "ID" %}
+
{{ router.id }}
+
{% trans "Status" %}
+
{{ router.status|capfirst }}
+
+
+ diff --git a/openstack_dashboard/dashboards/admin/routers/templates/routers/create.html b/openstack_dashboard/dashboards/admin/routers/templates/routers/create.html new file mode 100644 index 0000000000..d9ca9ba0b5 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/routers/templates/routers/create.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}Create router{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Create a router") %} +{% endblock page_header %} + +{% block main %} + {% include 'admin/routers/_create.html' %} +{% endblock %} diff --git a/openstack_dashboard/dashboards/admin/routers/templates/routers/detail.html b/openstack_dashboard/dashboards/admin/routers/templates/routers/detail.html new file mode 100644 index 0000000000..15c46bce92 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/routers/templates/routers/detail.html @@ -0,0 +1,15 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Router Details" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Router Detail") %} +{% endblock page_header %} + +{% block main %} +{% include "admin/routers/_detail_overview.html" %} +
+
+ {{ interfaces_table.render }} +
+{% endblock %} diff --git a/openstack_dashboard/dashboards/admin/routers/templates/routers/index.html b/openstack_dashboard/dashboards/admin/routers/templates/routers/index.html new file mode 100644 index 0000000000..b169f34c27 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/routers/templates/routers/index.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "routers" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("routers") %} +{% endblock page_header %} + +{% block main %} + {{ table.render }} +{% endblock %} diff --git a/openstack_dashboard/dashboards/admin/routers/tests.py b/openstack_dashboard/dashboards/admin/routers/tests.py new file mode 100644 index 0000000000..786d2a3e64 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/routers/tests.py @@ -0,0 +1,104 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012, Nachi Ueno, NTT MCL, Inc. +# +# 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 mox import IsA +from django import http +from django.core.urlresolvers import reverse + +from openstack_dashboard import api +from openstack_dashboard.dashboards.project.routers import tests as r_test +from openstack_dashboard.test import helpers as test + + +class RouterTests(test.BaseAdminViewTests, r_test.RouterTests): + DASHBOARD = 'admin' + INDEX_URL = reverse('horizon:%s:routers:index' % DASHBOARD) + DETAIL_PATH = 'horizon:%s:routers:detail' % DASHBOARD + + @test.create_stubs({api.quantum: ('router_list',), + api.keystone: ('tenant_list',)}) + def test_index(self): + tenants = self.tenants.list() + api.quantum.router_list( + IsA(http.HttpRequest), + search_opts=None).AndReturn(self.routers.list()) + api.keystone.tenant_list(IsA(http.HttpRequest), admin=True)\ + .AndReturn(tenants) + + self.mox.ReplayAll() + + res = self.client.get(self.INDEX_URL) + + self.assertTemplateUsed(res, '%s/routers/index.html' % self.DASHBOARD) + routers = res.context['table'].data + self.assertItemsEqual(routers, self.routers.list()) + + @test.create_stubs({api.quantum: ('router_list',), + api.keystone: ('tenant_list',)}) + def test_index_router_list_exception(self): + tenants = self.tenants.list() + api.quantum.router_list( + IsA(http.HttpRequest), + search_opts=None).AndRaise(self.exceptions.quantum) + self.mox.ReplayAll() + + res = self.client.get(self.INDEX_URL) + + self.assertTemplateUsed(res, '%s/routers/index.html' % self.DASHBOARD) + self.assertEqual(len(res.context['table'].data), 0) + self.assertMessageCount(res, error=1) + + @test.create_stubs({api.quantum: ('router_list', 'router_create'), + api.keystone: ('tenant_list',)}) + def test_router_create_post(self): + router = self.routers.first() + tenants = self.tenants.list() + api.quantum.router_create( + IsA(http.HttpRequest), + name=router.name, + tenant_id=router.tenant_id).AndReturn(router) + api.keystone.tenant_list(IsA(http.HttpRequest), admin=True)\ + .AndReturn(tenants) + + self.mox.ReplayAll() + + form_data = {'name': router.name, + 'tenant_id': router.tenant_id} + 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.quantum: ('router_list', 'router_create'), + api.keystone: ('tenant_list',)}) + def test_router_create_post_exception(self): + router = self.routers.first() + tenants = self.tenants.list() + api.quantum.router_create( + IsA(http.HttpRequest), + name=router.name, + tenant_id=router.tenant_id).AndRaise(self.exceptions.quantum) + api.keystone.tenant_list(IsA(http.HttpRequest), admin=True)\ + .AndReturn(tenants) + self.mox.ReplayAll() + + form_data = {'name': router.name, + 'tenant_id': router.tenant_id} + url = reverse('horizon:%s:routers:create' % self.DASHBOARD) + res = self.client.post(url, form_data) + + self.assertNoFormErrors(res) + self.assertRedirectsNoFollow(res, self.INDEX_URL) diff --git a/openstack_dashboard/dashboards/admin/routers/urls.py b/openstack_dashboard/dashboards/admin/routers/urls.py new file mode 100644 index 0000000000..de299bbf80 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/routers/urls.py @@ -0,0 +1,34 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012, Nachi Ueno, NTT MCL, Inc. +# +# 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, CreateView, DetailView) +from .ports.views import (AddInterfaceView, SetGatewayView) + + +urlpatterns = patterns('horizon.dashboards.admin.routers.views', + url(r'^$', IndexView.as_view(), name='index'), + url(r'^create/$', CreateView.as_view(), name='create'), + url(r'^(?P[^/]+)/$', + DetailView.as_view(), + name='detail'), + url(r'^(?P[^/]+)/addinterface', AddInterfaceView.as_view(), + name='addinterface'), + url(r'^(?P[^/]+)/setgateway', + SetGatewayView.as_view(), + name='setgateway'), +) diff --git a/openstack_dashboard/dashboards/admin/routers/views.py b/openstack_dashboard/dashboards/admin/routers/views.py new file mode 100644 index 0000000000..47cba3bcb1 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/routers/views.py @@ -0,0 +1,76 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012, Nachi Ueno, NTT MCL, Inc. +# +# 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. + +""" +Views for managing Quantum Routers. +""" + +import logging + +from django.core.urlresolvers import reverse_lazy +from django.utils.translation import ugettext_lazy as _ + +from horizon import exceptions +from horizon import forms +from openstack_dashboard import api +from openstack_dashboard.dashboards.admin.networks import views +from openstack_dashboard.dashboards.project.routers import views as r_views + +from .ports.tables import PortsTable +from .forms import CreateForm +from .tables import RoutersTable + + +LOG = logging.getLogger(__name__) + + +class IndexView(views.IndexView): + table_class = RoutersTable + template_name = 'admin/routers/index.html' + + def _get_routers(self, search_opts=None): + try: + routers = api.quantum.router_list(self.request, + search_opts=search_opts) + except: + routers = [] + exceptions.handle(self.request, + _('Unable to retrieve router list.')) + if routers: + tenant_dict = self._get_tenant_list() + for r in routers: + # Set tenant name + tenant = tenant_dict.get(r.tenant_id, None) + r.tenant_name = getattr(tenant, 'name', None) + # If name is empty use UUID as name + r.set_id_as_name_if_empty() + return routers + + def get_data(self): + routers = self._get_routers() + return routers + + +class DetailView(r_views.DetailView): + table_classes = (PortsTable, ) + template_name = 'admin/routers/detail.html' + failure_url = reverse_lazy('horizon:admin:routers:index') + + +class CreateView(forms.ModalFormView): + form_class = CreateForm + template_name = 'admin/routers/create.html' + success_url = reverse_lazy("horizon:admin:routers:index") diff --git a/openstack_dashboard/dashboards/project/dashboard.py b/openstack_dashboard/dashboards/project/dashboard.py index 691d7fa41b..79df86a64e 100644 --- a/openstack_dashboard/dashboards/project/dashboard.py +++ b/openstack_dashboard/dashboards/project/dashboard.py @@ -27,7 +27,8 @@ class BasePanels(horizon.PanelGroup): 'volumes', 'images_and_snapshots', 'access_and_security', - 'networks') + 'networks', + 'routers') class ObjectStorePanels(horizon.PanelGroup): diff --git a/openstack_dashboard/dashboards/project/routers/__init__.py b/openstack_dashboard/dashboards/project/routers/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openstack_dashboard/dashboards/project/routers/forms.py b/openstack_dashboard/dashboards/project/routers/forms.py new file mode 100644 index 0000000000..ba45c57640 --- /dev/null +++ b/openstack_dashboard/dashboards/project/routers/forms.py @@ -0,0 +1,41 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012, Nachi Ueno, NTT MCL, Inc. +# All rights reserved. + +""" +Views for managing Quantum Routers. +""" +import logging + +from django.core.urlresolvers import reverse +from django.utils.translation import ugettext_lazy as _ + +from horizon import forms +from horizon import exceptions +from horizon import messages +from openstack_dashboard import api + +LOG = logging.getLogger(__name__) + + +class CreateForm(forms.SelfHandlingForm): + name = forms.CharField(max_length="255", label=_("Router Name")) + failure_url = 'horizon:project:routers:index' + + def __init__(self, request, *args, **kwargs): + super(CreateForm, self).__init__(request, *args, **kwargs) + + def handle(self, request, data): + try: + router = api.quantum.router_create(request, + name=data['name']) + message = 'Router created "%s"' % data['name'] + messages.success(request, message) + return router + except: + msg = _('Failed to create router "%s".') % data['name'] + LOG.info(msg) + redirect = reverse(self.failure_url) + exceptions.handle(request, msg, redirect=redirect) + return False diff --git a/openstack_dashboard/dashboards/project/routers/panel.py b/openstack_dashboard/dashboards/project/routers/panel.py new file mode 100644 index 0000000000..09218dd99b --- /dev/null +++ b/openstack_dashboard/dashboards/project/routers/panel.py @@ -0,0 +1,29 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012, Nachi Ueno, NTT MCL, Inc. +# +# 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 _ + +import horizon + +from openstack_dashboard.dashboards.project import dashboard + + +class Routers(horizon.Panel): + name = "Routers" + slug = 'routers' + permissions = ('openstack.services.network',) + +dashboard.Project.register(Routers) diff --git a/openstack_dashboard/dashboards/project/routers/ports/__init__.py b/openstack_dashboard/dashboards/project/routers/ports/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openstack_dashboard/dashboards/project/routers/ports/forms.py b/openstack_dashboard/dashboards/project/routers/ports/forms.py new file mode 100644 index 0000000000..7699f0b74c --- /dev/null +++ b/openstack_dashboard/dashboards/project/routers/ports/forms.py @@ -0,0 +1,132 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012, Nachi Ueno, NTT MCL, Inc. +# +# 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 forms +from horizon import messages +from horizon import exceptions +from openstack_dashboard import api + +LOG = logging.getLogger(__name__) + + +class AddInterface(forms.SelfHandlingForm): + subnet_id = forms.ChoiceField(label=_("Subnet ID"), required=False) + router_id = forms.CharField(label=_("Router ID"), + widget=forms.TextInput( + attrs={'readonly': 'readonly'})) + failure_url = 'horizon:project:routers:detail' + + def __init__(self, request, *args, **kwargs): + super(AddInterface, self).__init__(request, *args, **kwargs) + c = self.populate_subnet_id_choices(request) + self.fields['subnet_id'].choices = c + + def populate_subnet_id_choices(self, request): + tenant_id = self.request.user.tenant_id + networks = [] + try: + networks = api.quantum.network_list_for_tenant(request, tenant_id) + except Exception as e: + msg = _('Failed to get network list %s') % e.message + LOG.info(msg) + messages.error(request, msg) + redirect = reverse(self.failure_url, + args=[request.REQUEST['router_id']]) + exceptions.handle(request, msg, redirect=redirect) + return + + choices = [] + for n in networks: + net_name = n.name + ': ' if n.name else '' + choices += [(subnet.id, + '%s%s (%s)' % (net_name, subnet.cidr, + subnet.name or subnet.id)) + for subnet in n['subnets']] + if choices: + choices.insert(0, ("", _("Select Subnet"))) + else: + choices.insert(0, ("", _("No subnets available."))) + return choices + + def handle(self, request, data): + try: + api.quantum.router_add_interface(request, + data['router_id'], + subnet_id=data['subnet_id']) + msg = _('Interface added') + LOG.debug(msg) + messages.success(request, msg) + return True + except Exception as e: + msg = _('Failed to add_interface %s') % e.message + LOG.info(msg) + messages.error(request, msg) + redirect = reverse(self.failure_url, args=[data['router_id']]) + exceptions.handle(request, msg, redirect=redirect) + + +class SetGatewayForm(forms.SelfHandlingForm): + network_id = forms.ChoiceField(label=_("Network ID"), required=False) + router_id = forms.CharField(label=_("Router ID"), + widget=forms.TextInput( + attrs={'readonly': 'readonly'})) + failure_url = 'horizon:project:routers:detail' + + def __init__(self, request, *args, **kwargs): + super(SetGatewayForm, self).__init__(request, *args, **kwargs) + c = self.populate_network_id_choices(request) + self.fields['network_id'].choices = c + + def populate_network_id_choices(self, request): + search_opts = {'router:external': True} + try: + networks = api.quantum.network_list(request, **search_opts) + except Exception as e: + msg = _('Failed to get network list %s') % e.message + LOG.info(msg) + messages.error(request, msg) + redirect = reverse(self.failure_url, + args=[request.REQUEST['router_id']]) + exceptions.handle(request, msg, redirect=redirect) + return + choices = [(network.id, network.name or network.id) + for network in networks] + if choices: + choices.insert(0, ("", _("Select network"))) + else: + choices.insert(0, ("", _("No networks available."))) + return choices + + def handle(self, request, data): + try: + api.quantum.router_add_gateway(request, + data['router_id'], + data['network_id']) + msg = _('Gateway interface is added') + LOG.debug(msg) + messages.success(request, msg) + return True + except Exception as e: + msg = _('Failed to set gateway %s') % e.message + LOG.info(msg) + messages.error(request, msg) + redirect = reverse(self.failure_url, args=[data['router_id']]) + exceptions.handle(request, msg, redirect=redirect) diff --git a/openstack_dashboard/dashboards/project/routers/ports/tables.py b/openstack_dashboard/dashboards/project/routers/ports/tables.py new file mode 100644 index 0000000000..9f1b5c85e6 --- /dev/null +++ b/openstack_dashboard/dashboards/project/routers/ports/tables.py @@ -0,0 +1,103 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012, Nachi Ueno, NTT MCL, Inc. +# +# 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 tables +from openstack_dashboard import api +from openstack_dashboard.dashboards.project.networks.ports.tables import\ + get_fixed_ips, get_attached + +LOG = logging.getLogger(__name__) + + +def get_device_owner(port): + if port['device_owner'] == 'network:router_gateway': + return _('Gateway') + else: + return ' ' + + +class SetGateway(tables.LinkAction): + name = "setgateway" + verbose_name = _("Add Gateway Interface") + url = "horizon:project:routers:setgateway" + classes = ("ajax-modal", "btn-camera") + + def get_link_url(self, datum=None): + router_id = self.table.kwargs['router_id'] + return reverse(self.url, args=(router_id,)) + + +class AddInterface(tables.LinkAction): + name = "create" + verbose_name = _("Add Interface") + url = "horizon:project:routers:addinterface" + classes = ("ajax-modal", "btn-create") + + def get_link_url(self, datum=None): + router_id = self.table.kwargs['router_id'] + return reverse(self.url, args=(router_id,)) + + +class RemoveInterface(tables.DeleteAction): + data_type_singular = _("Interface") + data_type_plural = _("Interfaces") + failure_url = 'horizon:project:routers:detail' + + def delete(self, request, obj_id): + try: + router_id = self.table.kwargs['router_id'] + port = api.quantum.port_get(request, obj_id) + if port['device_owner'] == 'network:router_gateway': + api.quantum.router_remove_gateway(request, router_id) + else: + api.quantum.router_remove_interface(request, + router_id, + port_id=obj_id) + except: + msg = _('Failed to delete interface %s') % obj_id + LOG.info(msg) + router_id = self.table.kwargs['router_id'] + redirect = reverse(self.failure_url, + args=[router_id]) + exceptions.handle(request, msg, redirect=redirect) + + +class PortsTable(tables.DataTable): + name = tables.Column("name", + verbose_name=_("Name"), + link="horizon:project:networks:ports:detail") + fixed_ips = tables.Column(get_fixed_ips, verbose_name=_("Fixed IPs")) + attached = tables.Column(get_attached, verbose_name=_("Device Attached")) + status = tables.Column("status", verbose_name=_("Status")) + device_owner = tables.Column(get_device_owner, + verbose_name=_("Type")) + admin_state = tables.Column("admin_state", + verbose_name=_("Admin State")) + + def get_object_display(self, port): + return port.id + + class Meta: + name = "interfaces" + verbose_name = _("Interfaces") + table_actions = (AddInterface, SetGateway, RemoveInterface) + row_actions = (RemoveInterface, ) diff --git a/openstack_dashboard/dashboards/project/routers/ports/tabs.py b/openstack_dashboard/dashboards/project/routers/ports/tabs.py new file mode 100644 index 0000000000..4dc0568760 --- /dev/null +++ b/openstack_dashboard/dashboards/project/routers/ports/tabs.py @@ -0,0 +1,47 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012, Nachi Ueno, NTT MCL, Inc. +# +# 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 tabs +from openstack_dashboard import api + +LOG = logging.getLogger(__name__) + + +class OverviewTab(tabs.Tab): + name = _("Overview") + slug = "overview" + template_name = "project/networks/ports/_detail_overview.html" + failure_url = 'horizon:project:routers:index' + + def get_context_data(self, request): + port_id = self.tab_group.kwargs['port_id'] + try: + port = api.quantum.port_get(self.request, port_id) + except: + redirect = reverse(self.failure_url) + msg = _('Unable to retrieve port details.') + exceptions.handle(request, msg, redirect=redirect) + return {'port': port} + + +class PortDetailTabs(tabs.TabGroup): + slug = "port_details" + tabs = (OverviewTab,) diff --git a/openstack_dashboard/dashboards/project/routers/ports/urls.py b/openstack_dashboard/dashboards/project/routers/ports/urls.py new file mode 100644 index 0000000000..fa192c7dea --- /dev/null +++ b/openstack_dashboard/dashboards/project/routers/ports/urls.py @@ -0,0 +1,24 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012, Nachi Ueno, NTT MCL, Inc. +# +# 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 DetailView + +PORTS = r'^(?P[^/]+)/%s$' + +urlpatterns = patterns('horizon.dashboards.project.networks.ports.views', + url(PORTS % 'detail', DetailView.as_view(), name='detail')) diff --git a/openstack_dashboard/dashboards/project/routers/ports/views.py b/openstack_dashboard/dashboards/project/routers/ports/views.py new file mode 100644 index 0000000000..eab18efef0 --- /dev/null +++ b/openstack_dashboard/dashboards/project/routers/ports/views.py @@ -0,0 +1,100 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012, Nachi Ueno, NTT MCL, Inc. +# +# 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 horizon import tabs +from horizon import forms +from horizon import exceptions +from openstack_dashboard import api +from .tabs import PortDetailTabs +from .forms import (AddInterface, SetGatewayForm) + + +LOG = logging.getLogger(__name__) + + +class AddInterfaceView(forms.ModalFormView): + form_class = AddInterface + template_name = 'project/routers/ports/create.html' + success_url = 'horizon:project:routers:detail' + failure_url = 'horizon:project:routers:detail' + + def get_success_url(self): + return reverse(self.success_url, + args=(self.kwargs['router_id'],)) + + def get_object(self): + if not hasattr(self, "_object"): + try: + router_id = self.kwargs["router_id"] + self._object = api.quantum.router_get(self.request, + router_id) + except: + redirect = reverse(self.failure_url, args=[router_id]) + msg = _("Unable to retrieve router.") + exceptions.handle(self.request, msg, redirect=redirect) + return self._object + + def get_context_data(self, **kwargs): + context = super(AddInterfaceView, 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} + + +class SetGatewayView(forms.ModalFormView): + form_class = SetGatewayForm + template_name = 'project/routers/ports/setgateway.html' + success_url = 'horizon:project:routers:detail' + failure_url = 'horizon:project:routers:detail' + + def get_success_url(self): + return reverse(self.success_url, + args=(self.kwargs['router_id'],)) + + def get_object(self): + if not hasattr(self, "_object"): + try: + router_id = self.kwargs["router_id"] + self._object = api.quantum.router_get(self.request, + router_id) + except: + redirect = reverse(self.failure_url) + msg = _("Unable to set gateway.") + exceptions.handle(self.request, msg, redirect=redirect) + return self._object + + def get_context_data(self, **kwargs): + context = super(SetGatewayView, 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} + + +class DetailView(tabs.TabView): + tab_group_class = PortDetailTabs + template_name = 'project/networks/ports/detail.html' diff --git a/openstack_dashboard/dashboards/project/routers/tables.py b/openstack_dashboard/dashboards/project/routers/tables.py new file mode 100644 index 0000000000..e643a74a20 --- /dev/null +++ b/openstack_dashboard/dashboards/project/routers/tables.py @@ -0,0 +1,90 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012, Nachi Ueno, NTT MCL, Inc. +# +# 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.template.defaultfilters import title +from django.utils.translation import ugettext_lazy as _ + +from horizon import exceptions +from horizon import messages +from horizon import tables +from openstack_dashboard import api +from quantumclient.common import exceptions as q_ext + +LOG = logging.getLogger(__name__) + + +class DeleteRouter(tables.DeleteAction): + data_type_singular = _("Router") + data_type_plural = _("Routers") + redirect_url = "horizon:project:routers:index" + + def delete(self, request, obj_id): + obj = self.table.get_object_by_id(obj_id) + name = self.table.get_object_display(obj) + try: + api.router_delete(request, obj_id) + except q_ext.QuantumClientException as e: + msg = _('Unable to delete router "%s"') % e.message + LOG.info(msg) + messages.error(request, msg) + redirect = reverse(self.redirect_url) + raise exceptions.Http302(redirect, message=msg) + except Exception as e: + msg = _('Unable to delete router "%s"') % name + LOG.info(msg) + exceptions.handle(request, msg) + + def allowed(self, request, router=None): + return True + + +class CreateRouter(tables.LinkAction): + name = "create" + verbose_name = _("Create Router") + url = "horizon:project:routers:create" + classes = ("ajax-modal", "btn-create") + + +class UpdateRow(tables.Row): + ajax = True + + def get_data(self, request, router_id): + router = api.router_get(request, router_id) + return router + + +class RoutersTable(tables.DataTable): + name = tables.Column("name", + verbose_name=_("Name"), + link="horizon:project:routers:detail") + status = tables.Column("status", + filters=(title,), + verbose_name=_("Status"), + status=True) + + def get_object_display(self, obj): + return obj.name + + class Meta: + name = "Routers" + verbose_name = _("Routers") + status_columns = ["status"] + row_class = UpdateRow + table_actions = (CreateRouter, DeleteRouter) + row_actions = (DeleteRouter, ) diff --git a/openstack_dashboard/dashboards/project/routers/tabs.py b/openstack_dashboard/dashboards/project/routers/tabs.py new file mode 100644 index 0000000000..0cc7eb274a --- /dev/null +++ b/openstack_dashboard/dashboards/project/routers/tabs.py @@ -0,0 +1,45 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012, Nachi Ueno, NTT MCL, Inc. +# +# 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 tabs +from openstack_dashboard import api + + +class OverviewTab(tabs.Tab): + name = _("Overview") + slug = "overview" + template_name = ("project/routers/_detail_overview.html") + redirect_url = 'horizon:project:routers:index' + + def get_context_data(self, request): + router_id = self.tab_group.kwargs['router_id'] + try: + router = api.router_get(request, router_id) + except: + redirect = reverse(redirect_url) + exceptions.handle(self.request, + _('Unable to retrieve router details.'), + redirect=redirect) + return {'router': router} + + +class RouterDetailTabs(tabs.TabGroup): + slug = "router_details" + tabs = (OverviewTab,) diff --git a/openstack_dashboard/dashboards/project/routers/templates/routers/_create.html b/openstack_dashboard/dashboards/project/routers/templates/routers/_create.html new file mode 100644 index 0000000000..6ca89b23d0 --- /dev/null +++ b/openstack_dashboard/dashboards/project/routers/templates/routers/_create.html @@ -0,0 +1,21 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n horizon humanize %} + +{% block form_id %}{% endblock %} +{% block form_action %}{% url horizon:project:routers:create %}?{{ request.GET.urlencode }}{% endblock %} + +{% block modal_id %}create_router_modal{% endblock %} +{% block modal-header %}{% trans "Create router" %}{% endblock %} + +{% block modal-body %} +
+
+ {% include "horizon/common/_form_fields.html" %} +
+
+{% endblock %} + +{% block modal-footer %} + + {% trans "Cancel" %} +{% endblock %} diff --git a/openstack_dashboard/dashboards/project/routers/templates/routers/_detail_overview.html b/openstack_dashboard/dashboards/project/routers/templates/routers/_detail_overview.html new file mode 100644 index 0000000000..cbead2aa6e --- /dev/null +++ b/openstack_dashboard/dashboards/project/routers/templates/routers/_detail_overview.html @@ -0,0 +1,15 @@ +{% load i18n sizeformat parse_date %} + +

{% trans "Router Overview" %}: {{router.name|default:"None" }}

+ +
+
+
{% trans "Name" %}
+
{{ router.name|default:"None" }}
+
{% trans "ID" %}
+
{{ router.id|default:"None" }}
+
{% trans "Status" %}
+
{{ router.status|default:"Unknown" }}
+
+
+ diff --git a/openstack_dashboard/dashboards/project/routers/templates/routers/create.html b/openstack_dashboard/dashboards/project/routers/templates/routers/create.html new file mode 100644 index 0000000000..3dfff361b2 --- /dev/null +++ b/openstack_dashboard/dashboards/project/routers/templates/routers/create.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}Create router{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Create a router") %} +{% endblock page_header %} + +{% block main %} + {% include 'project/routers/_create.html' %} +{% endblock %} diff --git a/openstack_dashboard/dashboards/project/routers/templates/routers/detail.html b/openstack_dashboard/dashboards/project/routers/templates/routers/detail.html new file mode 100644 index 0000000000..ae7d159430 --- /dev/null +++ b/openstack_dashboard/dashboards/project/routers/templates/routers/detail.html @@ -0,0 +1,15 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Router Details" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Router Detail") %} +{% endblock page_header %} + +{% block main %} +{% include "project/routers/_detail_overview.html" %} +
+
+ {{ interfaces_table.render }} +
+{% endblock %} diff --git a/openstack_dashboard/dashboards/project/routers/templates/routers/index.html b/openstack_dashboard/dashboards/project/routers/templates/routers/index.html new file mode 100644 index 0000000000..b169f34c27 --- /dev/null +++ b/openstack_dashboard/dashboards/project/routers/templates/routers/index.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "routers" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("routers") %} +{% endblock page_header %} + +{% block main %} + {{ table.render }} +{% endblock %} diff --git a/openstack_dashboard/dashboards/project/routers/templates/routers/ports/_create.html b/openstack_dashboard/dashboards/project/routers/templates/routers/ports/_create.html new file mode 100644 index 0000000000..60b2269c47 --- /dev/null +++ b/openstack_dashboard/dashboards/project/routers/templates/routers/ports/_create.html @@ -0,0 +1,25 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} + +{% block form_id %}add_interface_form{% endblock %} +{% block form_action %}{% url horizon:project:routers:addinterface router.id %} +{% endblock %} + +{% block modal-header %}{% trans "Add interface" %}{% endblock %} + +{% block modal-body %} +
+
+ {% include "horizon/common/_form_fields.html" %} +
+
+
+

{% trans "Description" %}:

+

{% trans "You can add interface to the network with subnet_id." %}

+
+{% endblock %} + +{% block modal-footer %} + + {% trans "Cancel" %} +{% endblock %} diff --git a/openstack_dashboard/dashboards/project/routers/templates/routers/ports/_setgateway.html b/openstack_dashboard/dashboards/project/routers/templates/routers/ports/_setgateway.html new file mode 100644 index 0000000000..558507ba3e --- /dev/null +++ b/openstack_dashboard/dashboards/project/routers/templates/routers/ports/_setgateway.html @@ -0,0 +1,25 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} + +{% block form_id %}setgateway_form{% endblock %} +{% block form_action %}{% url horizon:project:routers:setgateway router.id %} +{% endblock %} + +{% block modal-header %}{% trans "Add Gateway Interface" %}{% endblock %} + +{% block modal-body %} +
+
+ {% include "horizon/common/_form_fields.html" %} +
+
+
+

{% trans "Description" %}:

+

{% trans "You can add inteface for Gateway. In this interface, NAT rule of floating IP will be set." %}

+
+{% endblock %} + +{% block modal-footer %} + + {% trans "Cancel" %} +{% endblock %} diff --git a/openstack_dashboard/dashboards/project/routers/templates/routers/ports/create.html b/openstack_dashboard/dashboards/project/routers/templates/routers/ports/create.html new file mode 100644 index 0000000000..300b8d152b --- /dev/null +++ b/openstack_dashboard/dashboards/project/routers/templates/routers/ports/create.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Add Interface" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Add Interface") %} +{% endblock page_header %} + +{% block main %} + {% include "project/routers/ports/_create.html" %} +{% endblock %} diff --git a/openstack_dashboard/dashboards/project/routers/templates/routers/ports/setgateway.html b/openstack_dashboard/dashboards/project/routers/templates/routers/ports/setgateway.html new file mode 100644 index 0000000000..5c966b88de --- /dev/null +++ b/openstack_dashboard/dashboards/project/routers/templates/routers/ports/setgateway.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Set Gateway" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Set Gateway") %} +{% endblock page_header %} + +{% block main %} + {% include "project/routers/ports/_setgateway.html" %} +{% endblock %} diff --git a/openstack_dashboard/dashboards/project/routers/tests.py b/openstack_dashboard/dashboards/project/routers/tests.py new file mode 100644 index 0000000000..75c17c3e63 --- /dev/null +++ b/openstack_dashboard/dashboards/project/routers/tests.py @@ -0,0 +1,235 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012, Nachi Ueno, NTT MCL, Inc. +# +# 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 mox import IsA +from django import http +from django.core.urlresolvers import reverse + +from openstack_dashboard import api +from openstack_dashboard.test import helpers as test + + +class RouterTests(test.TestCase): + DASHBOARD = 'project' + INDEX_URL = reverse('horizon:%s:routers:index' % DASHBOARD) + DETAIL_PATH = 'horizon:%s:routers:detail' % DASHBOARD + + @test.create_stubs({api.quantum: ('router_list',)}) + def test_index(self): + api.quantum.router_list( + IsA(http.HttpRequest), + tenant_id=self.tenant.id, + search_opts=None).AndReturn(self.routers.list()) + + self.mox.ReplayAll() + + res = self.client.get(self.INDEX_URL) + + self.assertTemplateUsed(res, '%s/routers/index.html' % self.DASHBOARD) + routers = res.context['table'].data + self.assertItemsEqual(routers, self.routers.list()) + + @test.create_stubs({api.quantum: ('router_list',)}) + def test_index_router_list_exception(self): + api.quantum.router_list( + IsA(http.HttpRequest), + tenant_id=self.tenant.id, + search_opts=None).AndRaise(self.exceptions.quantum) + + self.mox.ReplayAll() + + res = self.client.get(self.INDEX_URL) + + self.assertTemplateUsed(res, '%s/routers/index.html' % self.DASHBOARD) + self.assertEqual(len(res.context['table'].data), 0) + self.assertMessageCount(res, error=1) + + @test.create_stubs({api.quantum: ('router_get', 'port_list')}) + def test_router_detail(self): + router_id = self.routers.first().id + api.quantum.router_get(IsA(http.HttpRequest), router_id)\ + .AndReturn(self.routers.first()) + api.quantum.port_list(IsA(http.HttpRequest), + device_id=router_id)\ + .AndReturn([self.ports.first()]) + + self.mox.ReplayAll() + + res = self.client.get(reverse('horizon:%s' + ':routers:detail' % self.DASHBOARD, + args=[router_id])) + + self.assertTemplateUsed(res, '%s/routers/detail.html' % self.DASHBOARD) + ports = res.context['interfaces_table'].data + self.assertItemsEqual(ports, [self.ports.first()]) + + @test.create_stubs({api.quantum: ('router_get', 'port_list')}) + def test_router_detail_exception(self): + router_id = self.routers.first().id + api.quantum.router_get(IsA(http.HttpRequest), router_id)\ + .AndRaise(self.exceptions.quantum) + api.quantum.port_list(IsA(http.HttpRequest), + device_id=router_id)\ + .AndReturn([self.ports.first()]) + self.mox.ReplayAll() + + res = self.client.get(reverse('horizon:%s' + ':routers:detail' % self.DASHBOARD, + args=[router_id])) + self.assertRedirectsNoFollow(res, self.INDEX_URL) + + @test.create_stubs({api.quantum: ('router_create',)}) + def test_router_create_post(self): + router = self.routers.first() + api.quantum.router_create(IsA(http.HttpRequest), name=router.name)\ + .AndReturn(router) + self.mox.ReplayAll() + + form_data = {'name': router.name} + 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.quantum: ('router_create',)}) + def test_router_create_post_exception(self): + router = self.routers.first() + api.quantum.router_create(IsA(http.HttpRequest), name=router.name)\ + .AndRaise(self.exceptions.quantum) + self.mox.ReplayAll() + + form_data = {'name': router.name} + 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.quantum: ('router_get', + 'router_add_interface', + 'network_list')}) + def test_router_addinterface(self): + router = self.routers.first() + subnet = self.subnets.first() + api.quantum.router_add_interface( + IsA(http.HttpRequest), + router.id, + subnet_id=subnet.id).AndReturn(None) + api.quantum.router_get(IsA(http.HttpRequest), router.id)\ + .AndReturn(router) + api.quantum.network_list( + IsA(http.HttpRequest), + shared=False, + tenant_id=router['tenant_id']).AndReturn(self.networks.list()) + api.quantum.network_list( + IsA(http.HttpRequest), + shared=True).AndReturn([]) + self.mox.ReplayAll() + + form_data = {'router_id': router.id, + 'subnet_id': subnet.id} + + url = reverse('horizon:%s:routers:addinterface' % self.DASHBOARD, + args=[router.id]) + res = self.client.post(url, form_data) + self.assertNoFormErrors(res) + detail_url = reverse(self.DETAIL_PATH, args=[router.id]) + self.assertRedirectsNoFollow(res, detail_url) + + @test.create_stubs({api.quantum: ('router_get', + 'router_add_interface', + 'network_list')}) + def test_router_addinterface_exception(self): + router = self.routers.first() + subnet = self.subnets.first() + api.quantum.router_add_interface( + IsA(http.HttpRequest), + router.id, + subnet_id=subnet.id).AndRaise(self.exceptions.quantum) + api.quantum.router_get(IsA(http.HttpRequest), router.id)\ + .AndReturn(router) + api.quantum.network_list( + IsA(http.HttpRequest), + shared=False, + tenant_id=router['tenant_id']).AndReturn(self.networks.list()) + api.quantum.network_list( + IsA(http.HttpRequest), + shared=True).AndReturn([]) + self.mox.ReplayAll() + + form_data = {'router_id': router.id, + 'subnet_id': subnet.id} + + url = reverse('horizon:%s:routers:addinterface' % self.DASHBOARD, + args=[router.id]) + res = self.client.post(url, form_data) + self.assertNoFormErrors(res) + detail_url = reverse(self.DETAIL_PATH, args=[router.id]) + self.assertRedirectsNoFollow(res, detail_url) + + @test.create_stubs({api.quantum: ('router_get', + 'router_add_gateway', + 'network_list')}) + def test_router_add_gateway(self): + router = self.routers.first() + network = self.networks.first() + api.quantum.router_add_gateway( + IsA(http.HttpRequest), + router.id, + network.id).AndReturn(None) + api.quantum.router_get( + IsA(http.HttpRequest), router.id).AndReturn(router) + search_opts = {'router:external': True} + api.quantum.network_list( + IsA(http.HttpRequest), **search_opts).AndReturn([network]) + self.mox.ReplayAll() + + form_data = {'router_id': router.id, + 'network_id': network.id} + + url = reverse('horizon:%s:routers:setgateway' % self.DASHBOARD, + args=[router.id]) + res = self.client.post(url, form_data) + self.assertNoFormErrors(res) + detail_url = reverse(self.DETAIL_PATH, args=[router.id]) + self.assertRedirectsNoFollow(res, detail_url) + + @test.create_stubs({api.quantum: ('router_get', + 'router_add_gateway', + 'network_list')}) + def test_router_add_gateway_exception(self): + router = self.routers.first() + network = self.networks.first() + api.quantum.router_add_gateway( + IsA(http.HttpRequest), + router.id, + network.id).AndRaise(self.exceptions.quantum) + api.quantum.router_get( + IsA(http.HttpRequest), router.id).AndReturn(router) + search_opts = {'router:external': True} + api.quantum.network_list( + IsA(http.HttpRequest), **search_opts).AndReturn([network]) + self.mox.ReplayAll() + + form_data = {'router_id': router.id, + 'network_id': network.id} + + url = reverse('horizon:%s:routers:setgateway' % self.DASHBOARD, + args=[router.id]) + res = self.client.post(url, form_data) + self.assertNoFormErrors(res) + detail_url = reverse(self.DETAIL_PATH, args=[router.id]) + self.assertRedirectsNoFollow(res, detail_url) diff --git a/openstack_dashboard/dashboards/project/routers/urls.py b/openstack_dashboard/dashboards/project/routers/urls.py new file mode 100644 index 0000000000..ea1c156c1e --- /dev/null +++ b/openstack_dashboard/dashboards/project/routers/urls.py @@ -0,0 +1,34 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012, Nachi Ueno, NTT MCL, Inc. +# +# 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, CreateView, DetailView) +from .ports.views import (AddInterfaceView, SetGatewayView) + + +urlpatterns = patterns('horizon.dashboards.project.routers.views', + url(r'^$', IndexView.as_view(), name='index'), + url(r'^create/$', CreateView.as_view(), name='create'), + url(r'^(?P[^/]+)/$', + DetailView.as_view(), + name='detail'), + url(r'^(?P[^/]+)/addinterface', AddInterfaceView.as_view(), + name='addinterface'), + url(r'^(?P[^/]+)/setgateway', + SetGatewayView.as_view(), + name='setgateway'), +) diff --git a/openstack_dashboard/dashboards/project/routers/views.py b/openstack_dashboard/dashboards/project/routers/views.py new file mode 100644 index 0000000000..e051037fa2 --- /dev/null +++ b/openstack_dashboard/dashboards/project/routers/views.py @@ -0,0 +1,105 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012, Nachi Ueno, NTT MCL, Inc. +# +# 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. + +""" +Views for managing Quantum Routers. +""" + +import logging + +from django import shortcuts +from django.core.urlresolvers import reverse_lazy +from django.utils.translation import ugettext_lazy as _ +from django.utils.datastructures import SortedDict + +from horizon import exceptions +from horizon import forms +from horizon import tables +from horizon import tabs +from openstack_dashboard import api +from .ports.tables import PortsTable +from .forms import CreateForm +from .tables import RoutersTable +from .tabs import RouterDetailTabs + + +LOG = logging.getLogger(__name__) + + +class IndexView(tables.DataTableView): + table_class = RoutersTable + template_name = 'project/routers/index.html' + + def _get_routers(self, search_opts=None): + try: + tenant_id = self.request.user.tenant_id + routers = api.quantum.router_list(self.request, + tenant_id=tenant_id, + search_opts=search_opts) + except: + routers = [] + exceptions.handle(self.request, + _('Unable to retrieve router list.')) + for r in routers: + r.set_id_as_name_if_empty() + return routers + + def get_data(self): + routers = self._get_routers() + return routers + + +class DetailView(tables.MultiTableView): + table_classes = (PortsTable, ) + template_name = 'project/routers/detail.html' + failure_url = reverse_lazy('horizon:project:routers:index') + + def _get_data(self): + if not hasattr(self, "_router"): + try: + router_id = self.kwargs['router_id'] + router = api.quantum.router_get(self.request, router_id) + router.set_id_as_name_if_empty(length=0) + except: + msg = _('Unable to retrieve details for router "%s".') \ + % (router_id) + exceptions.handle(self.request, msg, redirect=self.failure_url) + self._router = router + return self._router + + def get_context_data(self, **kwargs): + context = super(DetailView, self).get_context_data(**kwargs) + context["router"] = self._get_data() + return context + + def get_interfaces_data(self): + try: + device_id = self.kwargs['router_id'] + ports = api.quantum.port_list(self.request, + device_id=device_id) + except: + ports = [] + msg = _('Port list can not be retrieved.') + exceptions.handle(self.request, msg) + for p in ports: + p.set_id_as_name_if_empty() + return ports + + +class CreateView(forms.ModalFormView): + form_class = CreateForm + template_name = 'project/routers/create.html' + success_url = reverse_lazy("horizon:project:routers:index") diff --git a/openstack_dashboard/test/api_tests/quantum_tests.py b/openstack_dashboard/test/api_tests/quantum_tests.py index 22bf459c88..49743a20a0 100644 --- a/openstack_dashboard/test/api_tests/quantum_tests.py +++ b/openstack_dashboard/test/api_tests/quantum_tests.py @@ -203,3 +203,70 @@ class QuantumApiTests(test.APITestCase): self.mox.ReplayAll() api.quantum.port_delete(self.request, port_id) + + def test_router_list(self): + routers = {'routers': self.api_routers.list()} + + quantumclient = self.stub_quantumclient() + quantumclient.list_routers().AndReturn(routers) + self.mox.ReplayAll() + + ret_val = api.quantum.router_list(self.request) + for n in ret_val: + self.assertIsInstance(n, api.quantum.Router) + + def test_router_get(self): + router = {'router': self.api_routers.first()} + router_id = self.api_routers.first()['id'] + + quantumclient = self.stub_quantumclient() + quantumclient.show_router(router_id).AndReturn(router) + self.mox.ReplayAll() + + ret_val = api.quantum.router_get(self.request, router_id) + self.assertIsInstance(ret_val, api.quantum.Router) + + def test_router_create(self): + router = {'router': self.api_routers.first()} + + quantumclient = self.stub_quantumclient() + form_data = {'router': {'name': 'router1'}} + quantumclient.create_router(body=form_data).AndReturn(router) + self.mox.ReplayAll() + + ret_val = api.quantum.router_create(self.request, name='router1') + self.assertIsInstance(ret_val, api.quantum.Router) + + def test_router_delete(self): + router_id = self.api_routers.first()['id'] + + quantumclient = self.stub_quantumclient() + quantumclient.delete_router(router_id) + self.mox.ReplayAll() + + api.quantum.router_delete(self.request, router_id) + + def test_router_add_interface(self): + subnet_id = self.api_subnets.first()['id'] + router_id = self.api_routers.first()['id'] + + quantumclient = self.stub_quantumclient() + form_data = {'subnet_id': subnet_id} + quantumclient.add_interface_router( + router_id, form_data).AndReturn(None) + self.mox.ReplayAll() + + api.quantum.router_add_interface( + self.request, router_id, subnet_id=subnet_id) + + def test_router_remove_interface(self): + router_id = self.api_routers.first()['id'] + fake_port = self.api_ports.first()['id'] + + quantumclient = self.stub_quantumclient() + quantumclient.remove_interface_router( + router_id, {'port_id': fake_port}) + self.mox.ReplayAll() + + api.quantum.router_remove_interface( + self.request, router_id, port_id=fake_port) diff --git a/openstack_dashboard/test/test_data/quantum_data.py b/openstack_dashboard/test/test_data/quantum_data.py index 23e138127d..7c60323fac 100644 --- a/openstack_dashboard/test/test_data/quantum_data.py +++ b/openstack_dashboard/test/test_data/quantum_data.py @@ -14,7 +14,7 @@ import copy -from openstack_dashboard.api.quantum import Network, Subnet, Port +from openstack_dashboard.api.quantum import Network, Subnet, Port, Router from .utils import TestDataContainer @@ -24,11 +24,13 @@ def data(TEST): TEST.networks = TestDataContainer() TEST.subnets = TestDataContainer() TEST.ports = TestDataContainer() + TEST.routers = TestDataContainer() # data return by quantumclient TEST.api_networks = TestDataContainer() TEST.api_subnets = TestDataContainer() TEST.api_ports = TestDataContainer() + TEST.api_routers = TestDataContainer() # 1st network network_dict = {'admin_state_up': True, @@ -62,7 +64,6 @@ def data(TEST): 'network_id': network_dict['id'], 'status': 'ACTIVE', 'tenant_id': network_dict['tenant_id']} - TEST.api_networks.add(network_dict) TEST.api_subnets.add(subnet_dict) TEST.api_ports.add(port_dict) @@ -109,7 +110,6 @@ def data(TEST): 'network_id': network_dict['id'], 'status': 'ACTIVE', 'tenant_id': network_dict['tenant_id']} - TEST.api_networks.add(network_dict) TEST.api_subnets.add(subnet_dict) TEST.api_ports.add(port_dict) @@ -120,3 +120,29 @@ def data(TEST): TEST.networks.add(Network(network)) TEST.subnets.add(subnet) TEST.ports.add(Port(port_dict)) + + # Set up router data + port_dict = {'admin_state_up': True, + 'device_id': '7180cede-bcd8-4334-b19f-f7ef2f331f53', + 'device_owner': 'network:router_gateway', + 'fixed_ips': [{'ip_address': '10.0.0.3', + 'subnet_id': subnet_dict['id']}], + 'id': '3ec7f3db-cb2f-4a34-ab6b-69a64d3f008c', + 'mac_address': 'fa:16:3e:9c:d5:7e', + 'name': '', + 'network_id': network_dict['id'], + 'status': 'ACTIVE', + 'tenant_id': '1'} + TEST.api_ports.add(port_dict) + TEST.ports.add(Port(port_dict)) + + router_dict = {'id': '279989f7-54bb-41d9-ba42-0d61f12fda61', + 'name': 'router1', + 'tenant_id': '1'} + TEST.api_routers.add(router_dict) + TEST.routers.add(Router(router_dict)) + router_dict = {'id': '279989f7-54bb-41d9-ba42-0d61f12fda61', + 'name': 'router1', + 'tenant_id': '1'} + TEST.api_routers.add(router_dict) + TEST.routers.add(Router(router_dict))