From 31d55e503d578f3af86fed15a37127aff9871ecd Mon Sep 17 00:00:00 2001 From: Akihiro MOTOKI Date: Fri, 4 Jan 2013 18:33:03 +0900 Subject: [PATCH] Improve Quantum panels to Folsom advanced features Implements blueprint improve-quantum-summary-table * Improve displayed columns in network related tables * Use workflows in subnet create/update panels * Operations * admin_state control for network and port * router:external support in network creation and update * No gateway support in subnet creation and update * enable_dhcp, allocation_pools, dns_nameservers and host_routes support in subnet creattion and update * Setting device_owner is supported in admin port panel * Detail panels * router:external and provider network information in admin network detail * enable_dhcp, host_routes and dns_nameservers in subnet detail * device_owner in port detail * Behavior changes * Remove created network when subnet creation failed in "Create Network". Before this commit a created network remains even when an associated subnet failed to be created, but it is a little confusing since an unintended network without subnet is created. This commit deletes such networks and display a message indicating it. Change-Id: I1325c415acc6afc664879540c66957874d1c95c3 --- openstack_dashboard/api/quantum.py | 11 +- .../dashboards/admin/networks/forms.py | 23 +- .../dashboards/admin/networks/ports/forms.py | 28 +- .../dashboards/admin/networks/ports/views.py | 39 +- .../admin/networks/subnets/forms.py | 53 -- .../admin/networks/subnets/views.py | 77 +- .../admin/networks/subnets/workflows.py | 60 ++ .../templates/networks/subnets/_create.html | 25 - .../templates/networks/subnets/_update.html | 33 - .../templates/networks/subnets/create.html | 2 +- .../templates/networks/subnets/update.html | 2 +- .../dashboards/admin/networks/tests.py | 191 +++-- .../dashboards/admin/networks/views.py | 4 +- .../dashboards/project/networks/forms.py | 5 +- .../project/networks/ports/forms.py | 56 ++ .../project/networks/ports/tables.py | 22 +- .../project/networks/ports/views.py | 48 ++ .../project/networks/subnets/forms.py | 139 ---- .../project/networks/subnets/views.py | 66 +- .../project/networks/subnets/workflows.py | 198 +++++ .../templates/networks/_detail_overview.html | 8 + .../networks/ports/_detail_overview.html | 10 +- .../networks/{subnets => ports}/_update.html | 14 +- .../templates/networks/ports/update.html | 11 + .../templates/networks/subnets/_create.html | 25 - .../networks/subnets/_detail_overview.html | 30 +- .../templates/networks/subnets/create.html | 2 +- .../templates/networks/subnets/update.html | 2 +- .../dashboards/project/networks/tests.py | 777 +++++++++++++++--- .../dashboards/project/networks/urls.py | 3 + .../dashboards/project/networks/views.py | 3 +- .../dashboards/project/networks/workflows.py | 276 ++++++- .../test/test_data/quantum_data.py | 13 +- 33 files changed, 1559 insertions(+), 697 deletions(-) delete mode 100644 openstack_dashboard/dashboards/admin/networks/subnets/forms.py create mode 100644 openstack_dashboard/dashboards/admin/networks/subnets/workflows.py delete mode 100644 openstack_dashboard/dashboards/admin/networks/templates/networks/subnets/_create.html delete mode 100644 openstack_dashboard/dashboards/admin/networks/templates/networks/subnets/_update.html create mode 100644 openstack_dashboard/dashboards/project/networks/ports/forms.py delete mode 100644 openstack_dashboard/dashboards/project/networks/subnets/forms.py create mode 100644 openstack_dashboard/dashboards/project/networks/subnets/workflows.py rename openstack_dashboard/dashboards/project/networks/templates/networks/{subnets => ports}/_update.html (61%) create mode 100644 openstack_dashboard/dashboards/project/networks/templates/networks/ports/update.html delete mode 100644 openstack_dashboard/dashboards/project/networks/templates/networks/subnets/_create.html diff --git a/openstack_dashboard/api/quantum.py b/openstack_dashboard/api/quantum.py index 22c86271b..2c2f26e28 100644 --- a/openstack_dashboard/api/quantum.py +++ b/openstack_dashboard/api/quantum.py @@ -50,19 +50,19 @@ class QuantumAPIDictWrapper(APIDictWrapper): class Network(QuantumAPIDictWrapper): """Wrapper for quantum Networks""" - _attrs = ['name', 'id', 'subnets', 'tenant_id', 'status', - 'admin_state_up', 'shared'] def __init__(self, apiresource): apiresource['admin_state'] = \ 'UP' if apiresource['admin_state_up'] else 'DOWN' + # Django cannot handle a key name with a colon, so remap another key + for key in apiresource.keys(): + if key.find(':'): + apiresource['__'.join(key.split(':'))] = apiresource[key] super(Network, self).__init__(apiresource) class Subnet(QuantumAPIDictWrapper): """Wrapper for quantum subnets""" - _attrs = ['name', 'id', 'cidr', 'network_id', 'tenant_id', - 'ip_version', 'ipver_str'] def __init__(self, apiresource): apiresource['ipver_str'] = get_ipver_str(apiresource['ip_version']) @@ -71,9 +71,6 @@ class Subnet(QuantumAPIDictWrapper): class Port(QuantumAPIDictWrapper): """Wrapper for quantum ports""" - _attrs = ['name', 'id', 'network_id', 'tenant_id', - 'admin_state_up', 'status', 'mac_address', - 'fixed_ips', 'host_routes', 'device_id'] def __init__(self, apiresource): apiresource['admin_state'] = \ diff --git a/openstack_dashboard/dashboards/admin/networks/forms.py b/openstack_dashboard/dashboards/admin/networks/forms.py index 213f00ea5..13f177e14 100644 --- a/openstack_dashboard/dashboards/admin/networks/forms.py +++ b/openstack_dashboard/dashboards/admin/networks/forms.py @@ -34,8 +34,12 @@ class CreateNetwork(forms.SelfHandlingForm): label=_("Name"), required=False) tenant_id = forms.ChoiceField(label=_("Project")) + admin_state = forms.BooleanField(label=_("Admin State"), + initial=True, required=False) shared = forms.BooleanField(label=_("Shared"), initial=False, required=False) + external = forms.BooleanField(label=_("External Network"), + initial=False, required=False) @classmethod def _instantiate(cls, request, *args, **kwargs): @@ -51,10 +55,12 @@ class CreateNetwork(forms.SelfHandlingForm): def handle(self, request, data): try: - network = api.quantum.network_create(request, - name=data['name'], - tenant_id=data['tenant_id'], - shared=data['shared']) + params = {'name': data['name'], + 'tenant_id': data['tenant_id'], + 'admin_state_up': data['admin_state'], + 'shared': data['shared'], + 'router:external': data['external']} + network = api.quantum.network_create(request, **params) msg = _('Network %s was successfully created.') % data['name'] LOG.debug(msg) messages.success(request, msg) @@ -71,14 +77,19 @@ class UpdateNetwork(forms.SelfHandlingForm): network_id = forms.CharField(label=_("ID"), widget=forms.TextInput( attrs={'readonly': 'readonly'})) + admin_state = forms.BooleanField(label=_("Admin State"), required=False) shared = forms.BooleanField(label=_("Shared"), required=False) + external = forms.BooleanField(label=_("External Network"), required=False) failure_url = 'horizon:admin:networks:index' def handle(self, request, data): try: + params = {'name': data['name'], + 'admin_state_up': data['admin_state'], + 'shared': data['shared'], + 'router:external': data['external']} network = api.quantum.network_modify(request, data['network_id'], - name=data['name'], - shared=data['shared']) + **params) msg = _('Network %s was successfully updated.') % data['name'] LOG.debug(msg) messages.success(request, msg) diff --git a/openstack_dashboard/dashboards/admin/networks/ports/forms.py b/openstack_dashboard/dashboards/admin/networks/ports/forms.py index 19d0838cd..7a1f45d43 100644 --- a/openstack_dashboard/dashboards/admin/networks/ports/forms.py +++ b/openstack_dashboard/dashboards/admin/networks/ports/forms.py @@ -24,6 +24,8 @@ from horizon import forms from horizon import messages from openstack_dashboard import api +from openstack_dashboard.dashboards.project.networks.ports \ + import forms as project_forms LOG = logging.getLogger(__name__) @@ -39,9 +41,14 @@ class CreatePort(forms.SelfHandlingForm): name = forms.CharField(max_length=255, label=_("Name"), required=False) + admin_state = forms.BooleanField(label=_("Admin State"), + initial=True, required=False) device_id = forms.CharField(max_length=100, label=_("Device ID"), help_text='Device ID attached to the port', required=False) + device_owner = forms.CharField(max_length=100, label=_("Device Owner"), + help_text='Device owner attached to the port', + required=False) def handle(self, request, data): try: @@ -49,6 +56,8 @@ class CreatePort(forms.SelfHandlingForm): # created for if admin user does not belong to the tenant. network = api.quantum.network_get(request, data['network_id']) data['tenant_id'] = network.tenant_id + data['admin_state_up'] = data['admin_state'] + del data['admin_state'] port = api.quantum.port_create(request, **data) msg = _('Port %s was successfully created.') % port['id'] @@ -64,23 +73,24 @@ class CreatePort(forms.SelfHandlingForm): exceptions.handle(request, msg, redirect=redirect) -class UpdatePort(forms.SelfHandlingForm): - network_id = forms.CharField(widget=forms.HiddenInput()) - tenant_id = forms.CharField(widget=forms.HiddenInput()) - port_id = forms.CharField(widget=forms.HiddenInput()) - name = forms.CharField(max_length=255, - label=_("Name"), - required=False) +class UpdatePort(project_forms.UpdatePort): + #tenant_id = forms.CharField(widget=forms.HiddenInput()) device_id = forms.CharField(max_length=100, label=_("Device ID"), help_text='Device ID attached to the port', required=False) + device_owner = forms.CharField(max_length=100, label=_("Device Owner"), + help_text='Device owner attached to the port', + required=False) + failure_url = 'horizon:admin:networks:detail' def handle(self, request, data): try: LOG.debug('params = %s' % data) port = api.quantum.port_modify(request, data['port_id'], name=data['name'], - device_id=data['device_id']) + admin_state_up=data['admin_state'], + device_id=data['device_id'], + device_owner=data['device_owner']) msg = _('Port %s was successfully updated.') % data['port_id'] LOG.debug(msg) messages.success(request, msg) @@ -88,6 +98,6 @@ class UpdatePort(forms.SelfHandlingForm): except Exception: msg = _('Failed to update port %s') % data['port_id'] LOG.info(msg) - redirect = reverse('horizon:admin:networks:detail', + redirect = reverse(self.failure_url, args=[data['network_id']]) exceptions.handle(request, msg, redirect=redirect) diff --git a/openstack_dashboard/dashboards/admin/networks/ports/views.py b/openstack_dashboard/dashboards/admin/networks/ports/views.py index 43fad6390..384670d36 100644 --- a/openstack_dashboard/dashboards/admin/networks/ports/views.py +++ b/openstack_dashboard/dashboards/admin/networks/ports/views.py @@ -23,6 +23,9 @@ from horizon import exceptions from horizon import forms from openstack_dashboard import api +from openstack_dashboard.dashboards.project.networks.ports \ + import views as project_views + from .forms import CreatePort, UpdatePort LOG = logging.getLogger(__name__) @@ -32,6 +35,7 @@ class CreateView(forms.ModalFormView): form_class = CreatePort template_name = 'admin/networks/ports/create.html' success_url = 'horizon:admin:networks:detail' + failure_url = 'horizon:admin:networks:detail' def get_success_url(self): return reverse(self.success_url, @@ -44,7 +48,7 @@ class CreateView(forms.ModalFormView): self._object = api.quantum.network_get(self.request, network_id) except: - redirect = reverse("horizon:admin:networks:detail", + redirect = reverse(self.failure_url, args=(self.kwargs['network_id'],)) msg = _("Unable to retrieve network.") exceptions.handle(self.request, msg, redirect=redirect) @@ -61,39 +65,8 @@ class CreateView(forms.ModalFormView): "network_name": network.name} -class UpdateView(forms.ModalFormView): +class UpdateView(project_views.UpdateView): form_class = UpdatePort template_name = 'admin/networks/ports/update.html' context_object_name = 'port' success_url = 'horizon:admin:networks:detail' - - def get_success_url(self): - return reverse(self.success_url, - args=(self.kwargs['network_id'],)) - - def _get_object(self, *args, **kwargs): - if not hasattr(self, "_object"): - port_id = self.kwargs['port_id'] - try: - self._object = api.quantum.port_get(self.request, port_id) - except: - redirect = reverse("horizon:admin:networks:detail", - args=(self.kwargs['network_id'],)) - msg = _('Unable to retrieve port details') - exceptions.handle(self.request, msg, redirect=redirect) - return self._object - - def get_context_data(self, **kwargs): - context = super(UpdateView, self).get_context_data(**kwargs) - port = self._get_object() - context['port_id'] = port['id'] - context['network_id'] = port['network_id'] - return context - - def get_initial(self): - port = self._get_object() - return {'port_id': port['id'], - 'network_id': port['network_id'], - 'tenant_id': port['tenant_id'], - 'name': port['name'], - 'device_id': port['device_id']} diff --git a/openstack_dashboard/dashboards/admin/networks/subnets/forms.py b/openstack_dashboard/dashboards/admin/networks/subnets/forms.py deleted file mode 100644 index 2a4ab362d..000000000 --- a/openstack_dashboard/dashboards/admin/networks/subnets/forms.py +++ /dev/null @@ -1,53 +0,0 @@ -# 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.core.urlresolvers import reverse -from django.utils.translation import ugettext_lazy as _ - -from horizon import forms -from horizon import exceptions - -from openstack_dashboard import api -from openstack_dashboard.dashboards.project.networks.subnets import \ - forms as user_forms - - -LOG = logging.getLogger(__name__) - - -class CreateSubnet(user_forms.CreateSubnet): - failure_url = 'horizon:admin:networks:detail' - - def handle(self, request, data): - try: - # We must specify tenant_id of the network which a subnet is - # created for if admin user does not belong to the tenant. - network = api.quantum.network_get(request, data['network_id']) - data['tenant_id'] = network.tenant_id - except: - msg = _('Failed to retrieve network %s for a subnet') \ - % data['network_id'] - LOG.info(msg) - redirect = reverse(self.failure_url, args=[data['network_id']]) - exceptions.handle(request, msg, redirect=redirect) - return super(CreateSubnet, self).handle(request, data) - - -class UpdateSubnet(user_forms.UpdateSubnet): - tenant_id = forms.CharField(widget=forms.HiddenInput()) - failure_url = 'horizon:admin:networks:detail' diff --git a/openstack_dashboard/dashboards/admin/networks/subnets/views.py b/openstack_dashboard/dashboards/admin/networks/subnets/views.py index 86837cfac..5e54022ca 100644 --- a/openstack_dashboard/dashboards/admin/networks/subnets/views.py +++ b/openstack_dashboard/dashboards/admin/networks/subnets/views.py @@ -23,81 +23,20 @@ from horizon import exceptions from horizon import forms from openstack_dashboard import api -from .forms import CreateSubnet, UpdateSubnet +from openstack_dashboard.dashboards.project.networks.subnets \ + import views as project_views + +from .workflows import CreateSubnet, UpdateSubnet LOG = logging.getLogger(__name__) -class CreateView(forms.ModalFormView): - form_class = CreateSubnet +class CreateView(project_views.CreateView): + workflow_class = CreateSubnet template_name = 'admin/networks/subnets/create.html' - success_url = 'horizon:admin:networks:detail' - - def get_success_url(self): - return reverse(self.success_url, - args=(self.kwargs['network_id'],)) - - def get_object(self): - if not hasattr(self, "_object"): - try: - network_id = self.kwargs["network_id"] - self._object = api.quantum.network_get(self.request, - network_id) - except: - redirect = reverse('horizon:project:networks:index') - msg = _("Unable to retrieve network.") - exceptions.handle(self.request, msg, redirect=redirect) - return self._object - - def get_context_data(self, **kwargs): - context = super(CreateView, self).get_context_data(**kwargs) - context['network'] = self.get_object() - return context - - def get_initial(self): - network = self.get_object() - return {"network_id": self.kwargs['network_id'], - "network_name": network.name} -class UpdateView(forms.ModalFormView): - form_class = UpdateSubnet +class UpdateView(project_views.UpdateView): + workflow_class = UpdateSubnet template_name = 'admin/networks/subnets/update.html' - context_object_name = 'subnet' - success_url = 'horizon:admin:networks:detail' - - def get_success_url(self): - return reverse(self.success_url, - args=(self.kwargs['network_id'],)) - - def _get_object(self, *args, **kwargs): - if not hasattr(self, "_object"): - subnet_id = self.kwargs['subnet_id'] - try: - self._object = api.quantum.subnet_get(self.request, subnet_id) - except: - redirect = reverse("horizon:admin:networks:detail", - args=(self.kwargs['network_id'],)) - msg = _('Unable to retrieve subnet details') - exceptions.handle(self.request, msg, redirect=redirect) - return self._object - - def get_context_data(self, **kwargs): - context = super(UpdateView, self).get_context_data(**kwargs) - subnet = self._get_object() - context['subnet_id'] = subnet['id'] - context['network_id'] = subnet['network_id'] - context['cidr'] = subnet['cidr'] - context['ip_version'] = {4: 'IPv4', 6: 'IPv6'}[subnet['ip_version']] - return context - - def get_initial(self): - subnet = self._get_object() - return {'network_id': self.kwargs['network_id'], - 'subnet_id': subnet['id'], - 'tenant_id': subnet['tenant_id'], - 'cidr': subnet['cidr'], - 'ip_version': subnet['ip_version'], - 'name': subnet['name'], - 'gateway_ip': subnet['gateway_ip']} diff --git a/openstack_dashboard/dashboards/admin/networks/subnets/workflows.py b/openstack_dashboard/dashboards/admin/networks/subnets/workflows.py new file mode 100644 index 000000000..0b479ca37 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/networks/subnets/workflows.py @@ -0,0 +1,60 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 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.core.urlresolvers import reverse +from django.utils.translation import ugettext as _ + +from horizon import exceptions + +from openstack_dashboard import api +from openstack_dashboard.dashboards.project.networks.subnets \ + import workflows as project_workflows + + +LOG = logging.getLogger(__name__) + + +class CreateSubnet(project_workflows.CreateSubnet): + def get_success_url(self): + return reverse("horizon:admin:networks:detail", + args=(self.context.get('network_id'),)) + + def get_failure_url(self): + return reverse("horizon:admin:networks:detail", + args=(self.context.get('network_id'),)) + + def handle(self, request, data): + try: + # We must specify tenant_id of the network which a subnet is + # created for if admin user does not belong to the tenant. + network = api.quantum.network_get(request, + self.context['network_id']) + except: + msg = (_('Failed to retrieve network %s for a subnet') % + data['network_id']) + LOG.info(msg) + redirect = self.get_failure_url() + exceptions.handle(request, msg, redirect=redirect) + subnet = self._create_subnet(request, data, + tenant_id=network.tenant_id) + return True if subnet else False + + +class UpdateSubnet(project_workflows.UpdateSubnet): + success_url = "horizon:admin:networks:detail" + failure_url = "horizon:admin:networks:detail" diff --git a/openstack_dashboard/dashboards/admin/networks/templates/networks/subnets/_create.html b/openstack_dashboard/dashboards/admin/networks/templates/networks/subnets/_create.html deleted file mode 100644 index c7367b4c8..000000000 --- a/openstack_dashboard/dashboards/admin/networks/templates/networks/subnets/_create.html +++ /dev/null @@ -1,25 +0,0 @@ -{% extends "horizon/common/_modal_form.html" %} -{% load i18n %} - -{% block form_id %}create_subnet_form{% endblock %} -{% block form_action %}{% url horizon:admin:networks:addsubnet network.id %} -{% endblock %} - -{% block modal-header %}{% trans "Create Subnet" %}{% endblock %} - -{% block modal-body %} -
-
- {% include "horizon/common/_form_fields.html" %} -
-
-
-

{% trans "Description" %}:

-

{% trans "You can create a subnet for the network. Any network address can be specified unless the network address does not overlap other subnets in the network." %}

-
-{% endblock %} - -{% block modal-footer %} - - {% trans "Cancel" %} -{% endblock %} diff --git a/openstack_dashboard/dashboards/admin/networks/templates/networks/subnets/_update.html b/openstack_dashboard/dashboards/admin/networks/templates/networks/subnets/_update.html deleted file mode 100644 index 45a125ea6..000000000 --- a/openstack_dashboard/dashboards/admin/networks/templates/networks/subnets/_update.html +++ /dev/null @@ -1,33 +0,0 @@ -{% extends "horizon/common/_modal_form.html" %} -{% load i18n %} - -{% block form_id %}update_subnet_form{% endblock %} -{% block form_action %}{% url horizon:admin:networks:editsubnet network_id subnet_id %}{% endblock %} - -{% block modal-header %}{% trans "Edit Subnet" %}{% endblock %} - -{% block modal-body %} -
-
-
{% trans "ID" %}
-
{{ subnet_id }}
-
{% trans "Network Address" %}
-
{{ cidr }}
-
{% trans "IP version" %}
-
{{ ip_version }}
-
-
-
- {% include "horizon/common/_form_fields.html" %} -
-
-
-

{% trans "Description:" %}

-

{% trans "You may update the editable properties of your subnet here." %}

-
-{% endblock %} - -{% block modal-footer %} - - {% trans "Cancel" %} -{% endblock %} diff --git a/openstack_dashboard/dashboards/admin/networks/templates/networks/subnets/create.html b/openstack_dashboard/dashboards/admin/networks/templates/networks/subnets/create.html index 84624cc13..e026151c1 100644 --- a/openstack_dashboard/dashboards/admin/networks/templates/networks/subnets/create.html +++ b/openstack_dashboard/dashboards/admin/networks/templates/networks/subnets/create.html @@ -7,5 +7,5 @@ {% endblock page_header %} {% block main %} - {% include "admin/networks/subnets/_create.html" %} + {% include "horizon/common/_workflow.html" %} {% endblock %} diff --git a/openstack_dashboard/dashboards/admin/networks/templates/networks/subnets/update.html b/openstack_dashboard/dashboards/admin/networks/templates/networks/subnets/update.html index b152cdf4e..6d7c62311 100644 --- a/openstack_dashboard/dashboards/admin/networks/templates/networks/subnets/update.html +++ b/openstack_dashboard/dashboards/admin/networks/templates/networks/subnets/update.html @@ -7,5 +7,5 @@ {% endblock page_header %} {% block main %} - {% include 'admin/networks/subnets/_update.html' %} + {% include "horizon/common/_workflow.html" %} {% endblock %} diff --git a/openstack_dashboard/dashboards/admin/networks/tests.py b/openstack_dashboard/dashboards/admin/networks/tests.py index 4ea208cae..1c4e45d13 100644 --- a/openstack_dashboard/dashboards/admin/networks/tests.py +++ b/openstack_dashboard/dashboards/admin/networks/tests.py @@ -21,6 +21,8 @@ from mox import IsA from openstack_dashboard import api from openstack_dashboard.test import helpers as test +from openstack_dashboard.dashboards.project.networks.tests \ + import form_data_subnet INDEX_URL = reverse('horizon:admin:networks:index') @@ -166,13 +168,19 @@ class NetworkTests(test.BaseAdminViewTests): network = self.networks.first() api.keystone.tenant_list(IsA(http.HttpRequest), admin=True)\ .AndReturn(tenants) - api.quantum.network_create(IsA(http.HttpRequest), name=network.name, - tenant_id=tenant_id, shared=True)\ + params = {'name': network.name, + 'tenant_id': tenant_id, + 'admin_state_up': network.admin_state_up, + 'router:external': True, + 'shared': True} + api.quantum.network_create(IsA(http.HttpRequest), **params)\ .AndReturn(network) self.mox.ReplayAll() form_data = {'tenant_id': tenant_id, 'name': network.name, + 'admin_state': network.admin_state_up, + 'external': True, 'shared': True} url = reverse('horizon:admin:networks:create') res = self.client.post(url, form_data) @@ -188,13 +196,19 @@ class NetworkTests(test.BaseAdminViewTests): network = self.networks.first() api.keystone.tenant_list(IsA(http.HttpRequest), admin=True)\ .AndReturn(tenants) - api.quantum.network_create(IsA(http.HttpRequest), name=network.name, - tenant_id=tenant_id, shared=False)\ + params = {'name': network.name, + 'tenant_id': tenant_id, + 'admin_state_up': network.admin_state_up, + 'router:external': True, + 'shared': False} + api.quantum.network_create(IsA(http.HttpRequest), **params)\ .AndRaise(self.exceptions.quantum) self.mox.ReplayAll() form_data = {'tenant_id': tenant_id, 'name': network.name, + 'admin_state': network.admin_state_up, + 'external': True, 'shared': False} url = reverse('horizon:admin:networks:create') res = self.client.post(url, form_data) @@ -233,19 +247,25 @@ class NetworkTests(test.BaseAdminViewTests): 'network_get',)}) def test_network_update_post(self): network = self.networks.first() + params = {'name': network.name, + 'shared': True, + 'admin_state_up': network.admin_state_up, + 'router:external': True} api.quantum.network_modify(IsA(http.HttpRequest), network.id, - name=network.name, shared=True)\ + **params)\ .AndReturn(network) api.quantum.network_get(IsA(http.HttpRequest), network.id)\ .AndReturn(network) self.mox.ReplayAll() - formData = {'network_id': network.id, - 'name': network.name, - 'tenant_id': network.tenant_id, - 'shared': True} + form_data = {'network_id': network.id, + 'name': network.name, + 'tenant_id': network.tenant_id, + 'admin_state': network.admin_state_up, + 'shared': True, + 'external': True} url = reverse('horizon:admin:networks:update', args=[network.id]) - res = self.client.post(url, formData) + res = self.client.post(url, form_data) self.assertRedirectsNoFollow(res, INDEX_URL) @@ -253,8 +273,12 @@ class NetworkTests(test.BaseAdminViewTests): 'network_get',)}) def test_network_update_post_exception(self): network = self.networks.first() + params = {'name': network.name, + 'shared': False, + 'admin_state_up': network.admin_state_up, + 'router:external': False} api.quantum.network_modify(IsA(http.HttpRequest), network.id, - name=network.name, shared=False)\ + **params)\ .AndRaise(self.exceptions.quantum) api.quantum.network_get(IsA(http.HttpRequest), network.id)\ .AndReturn(network) @@ -263,7 +287,9 @@ class NetworkTests(test.BaseAdminViewTests): form_data = {'network_id': network.id, 'name': network.name, 'tenant_id': network.tenant_id, - 'shared': False} + 'admin_state': network.admin_state_up, + 'shared': False, + 'external': False} url = reverse('horizon:admin:networks:update', args=[network.id]) res = self.client.post(url, form_data) @@ -308,6 +334,9 @@ class NetworkTests(test.BaseAdminViewTests): self.assertRedirectsNoFollow(res, INDEX_URL) + +class NetworkSubnetTests(test.BaseAdminViewTests): + @test.create_stubs({api.quantum: ('subnet_get',)}) def test_subnet_detail(self): subnet = self.subnets.first() @@ -367,21 +396,17 @@ class NetworkTests(test.BaseAdminViewTests): .AndReturn(self.networks.first()) api.quantum.subnet_create(IsA(http.HttpRequest), network_id=network.id, - network_name=network.name, name=subnet.name, cidr=subnet.cidr, ip_version=subnet.ip_version, gateway_ip=subnet.gateway_ip, + enable_dhcp=subnet.enable_dhcp, + allocation_pools=subnet.allocation_pools, tenant_id=subnet.tenant_id)\ .AndReturn(subnet) self.mox.ReplayAll() - form_data = {'network_id': subnet.network_id, - 'network_name': network.name, - 'name': subnet.name, - 'cidr': subnet.cidr, - 'ip_version': subnet.ip_version, - 'gateway_ip': subnet.gateway_ip} + form_data = form_data_subnet(subnet) url = reverse('horizon:admin:networks:addsubnet', args=[subnet.network_id]) res = self.client.post(url, form_data) @@ -401,12 +426,7 @@ class NetworkTests(test.BaseAdminViewTests): .AndRaise(self.exceptions.quantum) self.mox.ReplayAll() - form_data = {'network_id': subnet.network_id, - 'network_name': network.name, - 'name': subnet.name, - 'cidr': subnet.cidr, - 'ip_version': subnet.ip_version, - 'gateway_ip': subnet.gateway_ip} + form_data = form_data_subnet(subnet, allocation_pools=[]) url = reverse('horizon:admin:networks:addsubnet', args=[subnet.network_id]) res = self.client.post(url, form_data) @@ -430,21 +450,16 @@ class NetworkTests(test.BaseAdminViewTests): .AndReturn(self.networks.first()) api.quantum.subnet_create(IsA(http.HttpRequest), network_id=network.id, - network_name=network.name, name=subnet.name, cidr=subnet.cidr, ip_version=subnet.ip_version, gateway_ip=subnet.gateway_ip, + enable_dhcp=subnet.enable_dhcp, tenant_id=subnet.tenant_id)\ .AndRaise(self.exceptions.quantum) self.mox.ReplayAll() - form_data = {'network_id': subnet.network_id, - 'network_name': network.name, - 'name': subnet.name, - 'cidr': subnet.cidr, - 'ip_version': subnet.ip_version, - 'gateway_ip': subnet.gateway_ip} + form_data = form_data_subnet(subnet, allocation_pools=[]) url = reverse('horizon:admin:networks:addsubnet', args=[subnet.network_id]) res = self.client.post(url, form_data) @@ -464,12 +479,7 @@ class NetworkTests(test.BaseAdminViewTests): # dummy IPv6 address cidr = '2001:0DB8:0:CD30:123:4567:89AB:CDEF/60' - form_data = {'network_id': subnet.network_id, - 'network_name': network.name, - 'name': subnet.name, - 'cidr': cidr, - 'ip_version': subnet.ip_version, - 'gateway_ip': subnet.gateway_ip} + form_data = form_data_subnet(subnet, cidr=cidr, allocation_pools=[]) url = reverse('horizon:admin:networks:addsubnet', args=[subnet.network_id]) res = self.client.post(url, form_data) @@ -488,12 +498,8 @@ class NetworkTests(test.BaseAdminViewTests): # dummy IPv6 address gateway_ip = '2001:0DB8:0:CD30:123:4567:89AB:CDEF' - form_data = {'network_id': subnet.network_id, - 'network_name': network.name, - 'name': subnet.name, - 'cidr': subnet.cidr, - 'ip_version': subnet.ip_version, - 'gateway_ip': gateway_ip} + form_data = form_data_subnet(subnet, gateway_ip=gateway_ip, + allocation_pools=[]) url = reverse('horizon:admin:networks:addsubnet', args=[subnet.network_id]) res = self.client.post(url, form_data) @@ -508,20 +514,17 @@ class NetworkTests(test.BaseAdminViewTests): .AndReturn(subnet) api.quantum.subnet_modify(IsA(http.HttpRequest), subnet.id, name=subnet.name, - gateway_ip=subnet.gateway_ip)\ + gateway_ip=subnet.gateway_ip, + enable_dhcp=subnet.enable_dhcp, + dns_nameservers=[], + host_routes=[])\ .AndReturn(subnet) self.mox.ReplayAll() - formData = {'network_id': subnet.network_id, - 'tenant_id': subnet.tenant_id, - 'subnet_id': subnet.id, - 'name': subnet.name, - 'cidr': subnet.cidr, - 'ip_version': subnet.ip_version, - 'gateway_ip': subnet.gateway_ip} + form_data = form_data_subnet(subnet, allocation_pools=[]) url = reverse('horizon:admin:networks:editsubnet', args=[subnet.network_id, subnet.id]) - res = self.client.post(url, formData) + res = self.client.post(url, form_data) redir_url = reverse('horizon:admin:networks:detail', args=[subnet.network_id]) @@ -537,16 +540,11 @@ class NetworkTests(test.BaseAdminViewTests): # dummy IPv6 address gateway_ip = '2001:0DB8:0:CD30:123:4567:89AB:CDEF' - formData = {'network_id': subnet.network_id, - 'tenant_id': subnet.tenant_id, - 'subnet_id': subnet.id, - 'name': subnet.name, - 'cidr': subnet.cidr, - 'ip_version': subnet.ip_version, - 'gateway_ip': gateway_ip} + form_data = form_data_subnet(subnet, gateway_ip=gateway_ip, + allocation_pools=[]) url = reverse('horizon:admin:networks:editsubnet', args=[subnet.network_id, subnet.id]) - res = self.client.post(url, formData) + res = self.client.post(url, form_data) self.assertContains(res, 'Gateway IP and IP version are inconsistent.') @@ -563,10 +561,10 @@ class NetworkTests(test.BaseAdminViewTests): .AndReturn([self.ports.first()]) self.mox.ReplayAll() - formData = {'action': 'subnets__delete__%s' % subnet.id} + form_data = {'action': 'subnets__delete__%s' % subnet.id} url = reverse('horizon:admin:networks:detail', args=[network_id]) - res = self.client.post(url, formData) + res = self.client.post(url, form_data) self.assertRedirectsNoFollow(res, url) @@ -584,13 +582,16 @@ class NetworkTests(test.BaseAdminViewTests): .AndReturn([self.ports.first()]) self.mox.ReplayAll() - formData = {'action': 'subnets__delete__%s' % subnet.id} + form_data = {'action': 'subnets__delete__%s' % subnet.id} url = reverse('horizon:admin:networks:detail', args=[network_id]) - res = self.client.post(url, formData) + res = self.client.post(url, form_data) self.assertRedirectsNoFollow(res, url) + +class NetworkPortTests(test.BaseAdminViewTests): + @test.create_stubs({api.quantum: ('port_get',)}) def test_port_detail(self): port = self.ports.first() @@ -651,14 +652,18 @@ class NetworkTests(test.BaseAdminViewTests): network_id=network.id, network_name=network.name, name=port.name, - device_id=port.device_id)\ + admin_state_up=port.admin_state_up, + device_id=port.device_id, + device_owner=port.device_owner)\ .AndReturn(port) self.mox.ReplayAll() form_data = {'network_id': port.network_id, 'network_name': network.name, 'name': port.name, - 'device_id': port.device_id} + 'admin_state': port.admin_state_up, + 'device_id': port.device_id, + 'device_owner': port.device_owner} url = reverse('horizon:admin:networks:addport', args=[port.network_id]) res = self.client.post(url, form_data) @@ -684,14 +689,18 @@ class NetworkTests(test.BaseAdminViewTests): network_id=network.id, network_name=network.name, name=port.name, - device_id=port.device_id)\ + admin_state_up=port.admin_state_up, + device_id=port.device_id, + device_owner=port.device_owner)\ .AndRaise(self.exceptions.quantum) self.mox.ReplayAll() form_data = {'network_id': port.network_id, 'network_name': network.name, 'name': port.name, - 'device_id': port.device_id} + 'admin_state': port.admin_state_up, + 'device_id': port.device_id, + 'device_owner': port.device_owner} url = reverse('horizon:admin:networks:addport', args=[port.network_id]) res = self.client.post(url, form_data) @@ -722,18 +731,22 @@ class NetworkTests(test.BaseAdminViewTests): api.quantum.port_get(IsA(http.HttpRequest), port.id)\ .AndReturn(port) api.quantum.port_modify(IsA(http.HttpRequest), port.id, - name=port.name, device_id=port.device_id)\ + name=port.name, + admin_state_up=port.admin_state_up, + device_id=port.device_id, + device_owner=port.device_owner)\ .AndReturn(port) self.mox.ReplayAll() - formData = {'tenant_id': port.tenant_id, - 'network_id': port.network_id, - 'port_id': port.id, - 'name': port.name, - 'device_id': port.device_id} + form_data = {'network_id': port.network_id, + 'port_id': port.id, + 'name': port.name, + 'admin_state': port.admin_state_up, + 'device_id': port.device_id, + 'device_owner': port.device_owner} url = reverse('horizon:admin:networks:editport', args=[port.network_id, port.id]) - res = self.client.post(url, formData) + res = self.client.post(url, form_data) redir_url = reverse('horizon:admin:networks:detail', args=[port.network_id]) @@ -746,18 +759,22 @@ class NetworkTests(test.BaseAdminViewTests): api.quantum.port_get(IsA(http.HttpRequest), port.id)\ .AndReturn(port) api.quantum.port_modify(IsA(http.HttpRequest), port.id, - name=port.name, device_id=port.device_id)\ + name=port.name, + admin_state_up=port.admin_state_up, + device_id=port.device_id, + device_owner=port.device_owner)\ .AndRaise(self.exceptions.quantum) self.mox.ReplayAll() - formData = {'tenant_id': port.tenant_id, - 'network_id': port.network_id, - 'port_id': port.id, - 'name': port.name, - 'device_id': port.device_id} + form_data = {'network_id': port.network_id, + 'port_id': port.id, + 'name': port.name, + 'admin_state': port.admin_state_up, + 'device_id': port.device_id, + 'device_owner': port.device_owner} url = reverse('horizon:admin:networks:editport', args=[port.network_id, port.id]) - res = self.client.post(url, formData) + res = self.client.post(url, form_data) redir_url = reverse('horizon:admin:networks:detail', args=[port.network_id]) @@ -776,10 +793,10 @@ class NetworkTests(test.BaseAdminViewTests): .AndReturn([self.ports.first()]) self.mox.ReplayAll() - formData = {'action': 'ports__delete__%s' % port.id} + form_data = {'action': 'ports__delete__%s' % port.id} url = reverse('horizon:admin:networks:detail', args=[network_id]) - res = self.client.post(url, formData) + res = self.client.post(url, form_data) self.assertRedirectsNoFollow(res, url) @@ -797,9 +814,9 @@ class NetworkTests(test.BaseAdminViewTests): .AndReturn([self.ports.first()]) self.mox.ReplayAll() - formData = {'action': 'ports__delete__%s' % port.id} + form_data = {'action': 'ports__delete__%s' % port.id} url = reverse('horizon:admin:networks:detail', args=[network_id]) - res = self.client.post(url, formData) + res = self.client.post(url, form_data) self.assertRedirectsNoFollow(res, url) diff --git a/openstack_dashboard/dashboards/admin/networks/views.py b/openstack_dashboard/dashboards/admin/networks/views.py index 2c38adb8e..32f08b14c 100644 --- a/openstack_dashboard/dashboards/admin/networks/views.py +++ b/openstack_dashboard/dashboards/admin/networks/views.py @@ -137,4 +137,6 @@ class UpdateView(user_views.UpdateView): return {'network_id': network['id'], 'tenant_id': network['tenant_id'], 'name': network['name'], - 'shared': network['shared']} + 'admin_state': network['admin_state_up'], + 'shared': network['shared'], + 'external': network['router__external']} diff --git a/openstack_dashboard/dashboards/project/networks/forms.py b/openstack_dashboard/dashboards/project/networks/forms.py index 8ba73a6ec..edc9e69e1 100644 --- a/openstack_dashboard/dashboards/project/networks/forms.py +++ b/openstack_dashboard/dashboards/project/networks/forms.py @@ -39,12 +39,15 @@ class UpdateNetwork(forms.SelfHandlingForm): network_id = forms.CharField(label=_("ID"), widget=forms.TextInput( attrs={'readonly': 'readonly'})) + admin_state = forms.BooleanField(label=_("Admin State"), required=False) failure_url = 'horizon:project:networks:index' def handle(self, request, data): try: + params = {'admin_state_up': data['admin_state'], + 'name': data['name']} network = api.quantum.network_modify(request, data['network_id'], - name=data['name']) + **params) msg = _('Network %s was successfully updated.') % data['name'] LOG.debug(msg) messages.success(request, msg) diff --git a/openstack_dashboard/dashboards/project/networks/ports/forms.py b/openstack_dashboard/dashboards/project/networks/ports/forms.py new file mode 100644 index 000000000..4e25404f0 --- /dev/null +++ b/openstack_dashboard/dashboards/project/networks/ports/forms.py @@ -0,0 +1,56 @@ +# 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.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 openstack_dashboard import api + + +LOG = logging.getLogger(__name__) + + +class UpdatePort(forms.SelfHandlingForm): + network_id = forms.CharField(widget=forms.HiddenInput()) + port_id = forms.CharField(widget=forms.HiddenInput()) + name = forms.CharField(max_length=255, + label=_("Name"), + required=False) + admin_state = forms.BooleanField(label=_("Admin State"), required=False) + failure_url = 'horizon:project:networks:detail' + + def handle(self, request, data): + try: + LOG.debug('params = %s' % data) + port = api.quantum.port_modify(request, data['port_id'], + name=data['name'], + admin_state_up=data['admin_state']) + msg = _('Port %s was successfully updated.') % data['port_id'] + LOG.debug(msg) + messages.success(request, msg) + return port + except Exception: + msg = _('Failed to update port %s') % data['port_id'] + LOG.info(msg) + redirect = reverse(self.failure_url, + args=[data['network_id']]) + exceptions.handle(request, msg, redirect=redirect) diff --git a/openstack_dashboard/dashboards/project/networks/ports/tables.py b/openstack_dashboard/dashboards/project/networks/ports/tables.py index 1cb6243b7..0397bbd40 100644 --- a/openstack_dashboard/dashboards/project/networks/ports/tables.py +++ b/openstack_dashboard/dashboards/project/networks/ports/tables.py @@ -16,6 +16,7 @@ import logging +from django.core.urlresolvers import reverse from django import template from django.utils.translation import ugettext_lazy as _ @@ -32,7 +33,23 @@ def get_fixed_ips(port): def get_attached(port): - return _('Attached') if port['device_id'] else _('Detached') + if port['device_owner']: + return port['device_owner'] + elif port['device_id']: + return _('Attached') + else: + return _('Detached') + + +class UpdatePort(tables.LinkAction): + name = "update" + verbose_name = _("Edit Port") + url = "horizon:project:networks:editport" + classes = ("ajax-modal", "btn-edit") + + def get_link_url(self, port): + network_id = self.table.kwargs['network_id'] + return reverse(self.url, args=(network_id, port.id)) class PortsTable(tables.DataTable): @@ -40,7 +57,7 @@ class PortsTable(tables.DataTable): 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")) + attached = tables.Column(get_attached, verbose_name=_("Attached Device")) status = tables.Column("status", verbose_name=_("Status")) admin_state = tables.Column("admin_state", verbose_name=_("Admin State")) @@ -51,3 +68,4 @@ class PortsTable(tables.DataTable): class Meta: name = "ports" verbose_name = _("Ports") + row_actions = (UpdatePort,) diff --git a/openstack_dashboard/dashboards/project/networks/ports/views.py b/openstack_dashboard/dashboards/project/networks/ports/views.py index 14f361440..581b8aebd 100644 --- a/openstack_dashboard/dashboards/project/networks/ports/views.py +++ b/openstack_dashboard/dashboards/project/networks/ports/views.py @@ -14,11 +14,59 @@ # 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 forms from horizon import tabs +from openstack_dashboard import api + +from .forms import UpdatePort from .tabs import PortDetailTabs class DetailView(tabs.TabView): tab_group_class = PortDetailTabs template_name = 'project/networks/ports/detail.html' + + +class UpdateView(forms.ModalFormView): + form_class = UpdatePort + template_name = 'project/networks/ports/update.html' + context_object_name = 'port' + success_url = 'horizon:project:networks:detail' + + def get_success_url(self): + return reverse(self.success_url, + args=(self.kwargs['network_id'],)) + + def _get_object(self, *args, **kwargs): + if not hasattr(self, "_object"): + port_id = self.kwargs['port_id'] + try: + self._object = api.quantum.port_get(self.request, port_id) + except: + redirect = reverse("horizon:project:networks:detail", + args=(self.kwargs['network_id'],)) + msg = _('Unable to retrieve port details') + exceptions.handle(self.request, msg, redirect=redirect) + return self._object + + def get_context_data(self, **kwargs): + context = super(UpdateView, self).get_context_data(**kwargs) + port = self._get_object() + context['port_id'] = port['id'] + context['network_id'] = port['network_id'] + return context + + def get_initial(self): + port = self._get_object() + return {'port_id': port['id'], + 'network_id': port['network_id'], + 'tenant_id': port['tenant_id'], + 'name': port['name'], + 'admin_state': port['admin_state_up'], + 'device_id': port['device_id'], + 'device_owner': port['device_owner']} diff --git a/openstack_dashboard/dashboards/project/networks/subnets/forms.py b/openstack_dashboard/dashboards/project/networks/subnets/forms.py deleted file mode 100644 index c03febb59..000000000 --- a/openstack_dashboard/dashboards/project/networks/subnets/forms.py +++ /dev/null @@ -1,139 +0,0 @@ -# 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 -import netaddr - -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 horizon.utils import fields - -from openstack_dashboard import api - - -LOG = logging.getLogger(__name__) - - -class CreateSubnet(forms.SelfHandlingForm): - network_name = forms.CharField(label=_("Network Name"), - required=False, - widget=forms.TextInput( - attrs={'readonly': 'readonly'})) - network_id = forms.CharField(label=_("Network ID"), - widget=forms.TextInput( - attrs={'readonly': 'readonly'})) - name = forms.CharField(max_length=255, - label=_("Name"), - required=False) - cidr = fields.IPField(label=_("Network Address"), - required=True, - initial="", - help_text=_("Network address in CIDR format " - "(e.g. 192.168.0.0/24)"), - version=fields.IPv4 | fields.IPv6, - mask=True) - ip_version = forms.ChoiceField(choices=[(4, 'IPv4'), (6, 'IPv6')], - label=_("IP Version")) - gateway_ip = fields.IPField(label=_("Gateway IP"), - required=False, - initial="", - help_text=_("IP address of Gateway " - "(e.g. 192.168.0.1)"), - version=fields.IPv4 | fields.IPv6, - mask=False) - failure_url = 'horizon:project:networks:detail' - - def clean(self): - cleaned_data = super(CreateSubnet, self).clean() - cidr = cleaned_data.get('cidr') - ip_version = int(cleaned_data.get('ip_version')) - gateway_ip = cleaned_data.get('gateway_ip') - if cidr: - if netaddr.IPNetwork(cidr).version is not ip_version: - msg = _('Network Address and IP version are inconsistent.') - raise forms.ValidationError(msg) - if gateway_ip: - if netaddr.IPAddress(gateway_ip).version is not ip_version: - msg = _('Gateway IP and IP version are inconsistent.') - raise forms.ValidationError(msg) - return cleaned_data - - def handle(self, request, data): - try: - LOG.debug('params = %s' % data) - data['ip_version'] = int(data['ip_version']) - if not data['gateway_ip']: - del data['gateway_ip'] - subnet = api.quantum.subnet_create(request, **data) - msg = _('Subnet %s was successfully created.') % data['cidr'] - LOG.debug(msg) - messages.success(request, msg) - return subnet - except Exception: - msg = _('Failed to create subnet %s') % data['cidr'] - LOG.info(msg) - redirect = reverse(self.failure_url, args=[data['network_id']]) - exceptions.handle(request, msg, redirect=redirect) - - -class UpdateSubnet(forms.SelfHandlingForm): - network_id = forms.CharField(widget=forms.HiddenInput()) - subnet_id = forms.CharField(widget=forms.HiddenInput()) - cidr = forms.CharField(widget=forms.HiddenInput()) - ip_version = forms.CharField(widget=forms.HiddenInput()) - name = forms.CharField(max_length=255, - label=_("Name"), - required=False) - gateway_ip = fields.IPField(label=_("Gateway IP"), - required=True, - initial="", - help_text=_("IP address of Gateway " - "(e.g. 192.168.0.1)"), - version=fields.IPv4 | fields.IPv6, - mask=False) - failure_url = 'horizon:project:networks:detail' - - def clean(self): - cleaned_data = super(UpdateSubnet, self).clean() - ip_version = int(cleaned_data.get('ip_version')) - gateway_ip = cleaned_data.get('gateway_ip') - if gateway_ip: - if netaddr.IPAddress(gateway_ip).version is not ip_version: - msg = _('Gateway IP and IP version are inconsistent.') - raise forms.ValidationError(msg) - return cleaned_data - - def handle(self, request, data): - try: - LOG.debug('params = %s' % data) - params = {'name': data['name']} - params['gateway_ip'] = data['gateway_ip'] - subnet = api.quantum.subnet_modify(request, data['subnet_id'], - name=data['name'], - gateway_ip=data['gateway_ip']) - msg = _('Subnet %s was successfully updated.') % data['cidr'] - LOG.debug(msg) - messages.success(request, msg) - return subnet - except Exception: - msg = _('Failed to update subnet %s') % data['cidr'] - LOG.info(msg) - redirect = reverse(self.failure_url, args=[data['network_id']]) - exceptions.handle(request, msg, redirect=redirect) diff --git a/openstack_dashboard/dashboards/project/networks/subnets/views.py b/openstack_dashboard/dashboards/project/networks/subnets/views.py index 52d308023..580f5f59e 100644 --- a/openstack_dashboard/dashboards/project/networks/subnets/views.py +++ b/openstack_dashboard/dashboards/project/networks/subnets/views.py @@ -22,26 +22,21 @@ import logging from django.core.urlresolvers import reverse_lazy, reverse from django.utils.translation import ugettext_lazy as _ -from horizon import forms from horizon import exceptions from horizon import tabs +from horizon import workflows from openstack_dashboard import api -from .forms import CreateSubnet, UpdateSubnet from .tabs import SubnetDetailTabs +from .workflows import CreateSubnet, UpdateSubnet LOG = logging.getLogger(__name__) -class CreateView(forms.ModalFormView): - form_class = CreateSubnet +class CreateView(workflows.WorkflowView): + workflow_class = CreateSubnet template_name = 'project/networks/subnets/create.html' - success_url = 'horizon:project:networks:detail' - - def get_success_url(self): - return reverse(self.success_url, - args=(self.kwargs['network_id'],)) def get_object(self): if not hasattr(self, "_object"): @@ -49,32 +44,22 @@ class CreateView(forms.ModalFormView): network_id = self.kwargs["network_id"] self._object = api.quantum.network_get(self.request, network_id) + self._object.set_id_as_name_if_empty() except: redirect = reverse('horizon:project:networks:index') msg = _("Unable to retrieve network.") exceptions.handle(self.request, msg, redirect=redirect) return self._object - def get_context_data(self, **kwargs): - context = super(CreateView, self).get_context_data(**kwargs) - context['network'] = self.get_object() - return context - def get_initial(self): network = self.get_object() return {"network_id": self.kwargs['network_id'], "network_name": network.name} -class UpdateView(forms.ModalFormView): - form_class = UpdateSubnet +class UpdateView(workflows.WorkflowView): + workflow_class = UpdateSubnet template_name = 'project/networks/subnets/update.html' - context_object_name = 'subnet' - success_url = reverse_lazy('horizon:project:networks:detail') - - def get_success_url(self): - return reverse('horizon:project:networks:detail', - args=(self.kwargs['network_id'],)) def _get_object(self, *args, **kwargs): if not hasattr(self, "_object"): @@ -87,23 +72,30 @@ class UpdateView(forms.ModalFormView): exceptions.handle(self.request, msg, redirect=redirect) return self._object - def get_context_data(self, **kwargs): - context = super(UpdateView, self).get_context_data(**kwargs) - subnet = self._get_object() - context['subnet_id'] = subnet.id - context['network_id'] = subnet.network_id - context['cidr'] = subnet.cidr - context['ip_version'] = subnet.ipver_str - return context - def get_initial(self): + initial = super(UpdateView, self).get_initial() + subnet = self._get_object() - return {'network_id': self.kwargs['network_id'], - 'subnet_id': subnet['id'], - 'cidr': subnet['cidr'], - 'ip_version': subnet['ip_version'], - 'name': subnet['name'], - 'gateway_ip': subnet['gateway_ip']} + + initial['network_id'] = self.kwargs['network_id'] + initial['subnet_id'] = subnet['id'] + initial['subnet_name'] = subnet['name'] + + for key in ('cidr', 'ip_version', 'enable_dhcp'): + initial[key] = subnet[key] + + initial['gateway_ip'] = subnet['gateway_ip'] or '' + initial['no_gateway'] = (subnet['gateway_ip'] is None) + + initial['dns_nameservers'] = '\n'.join(subnet['dns_nameservers']) + pools = ['%s,%s' % (p['start'], p['end']) + for p in subnet['allocation_pools']] + initial['allocation_pools'] = '\n'.join(pools) + routes = ['%s,%s' % (r['destination'], r['nexthop']) + for r in subnet['host_routes']] + initial['host_routes'] = '\n'.join(routes) + + return initial class DetailView(tabs.TabView): diff --git a/openstack_dashboard/dashboards/project/networks/subnets/workflows.py b/openstack_dashboard/dashboards/project/networks/subnets/workflows.py new file mode 100644 index 000000000..469c609d2 --- /dev/null +++ b/openstack_dashboard/dashboards/project/networks/subnets/workflows.py @@ -0,0 +1,198 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 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 + +import netaddr + +from django.core.urlresolvers import reverse +from django.utils.translation import ugettext as _ + +from horizon import exceptions +from horizon import forms +from horizon.utils import fields +from horizon import workflows + +from openstack_dashboard import api +from openstack_dashboard.dashboards.project.networks import workflows \ + as network_workflows + + +LOG = logging.getLogger(__name__) + + +class CreateSubnetInfoAction(network_workflows.CreateSubnetInfoAction): + with_subnet = forms.BooleanField(initial=True, required=False, + widget=forms.HiddenInput()) + + class Meta: + name = ("Subnet") + help_text = _('You can create a subnet associated with the ' + 'network. Advanced configuration are available ' + 'at "Subnet Detail" tab.') + + def clean(self): + cleaned_data = workflows.Action.clean(self) + self._check_subnet_data(cleaned_data) + return cleaned_data + + +class CreateSubnetInfo(network_workflows.CreateSubnetInfo): + action_class = CreateSubnetInfoAction + depends_on = ("network_id",) + + +class CreateSubnet(network_workflows.CreateNetwork): + slug = "create_subnet" + name = _("Create Subnet") + finalize_button_name = _("Create") + success_message = _('Created subnet "%s".') + failure_message = _('Unable to create subnet "%s".') + default_steps = (CreateSubnetInfo, + network_workflows.CreateSubnetDetail) + + def format_status_message(self, message): + name = self.context.get('subnet_name') or self.context.get('subnet_id') + return message % name + + def get_success_url(self): + return reverse("horizon:project:networks:detail", + args=(self.context.get('network_id'),)) + + def get_failure_url(self): + return reverse("horizon:project:networks:detail", + args=(self.context.get('network_id'),)) + + def handle(self, request, data): + subnet = self._create_subnet(request, data) + return True if subnet else False + + +class UpdateSubnetInfoAction(CreateSubnetInfoAction): + cidr = fields.IPField(label=_("Network Address"), + required=False, + initial="", + widget=forms.TextInput( + attrs={'readonly': 'readonly'}), + help_text=_("Network address in CIDR format " + "(e.g. 192.168.0.0/24)"), + version=fields.IPv4 | fields.IPv6, + mask=True) + # NOTE(amotoki): When 'disabled' attribute is set for the ChoiceField + # and ValidationError is raised for POST request, the initial value of + # the ip_version ChoiceField is not set in the re-displayed form + # As a result, 'IPv4' is displayed even when IPv6 is used if + # ValidationError is detected. In addition 'required=True' check complains + # when re-POST since the value of the ChoiceField is not set. + # Thus now I use HiddenInput for the ip_version ChoiceField as a work + # around. + ip_version = forms.ChoiceField(choices=[(4, 'IPv4'), (6, 'IPv6')], + #widget=forms.Select( + # attrs={'disabled': 'disabled'}), + widget=forms.HiddenInput(), + label=_("IP Version")) + + gateway_ip = fields.IPField( + label=_("Gateway IP (optional)"), + required=False, + initial="", + help_text=_("IP address of Gateway (e.g. 192.168.0.254). " + "You need to specify an explicit address " + "to set the gateway. " + "If you want to use no gateway, " + "check 'Disable Gateway' below."), + version=fields.IPv4 | fields.IPv6, + mask=False) + no_gateway = forms.BooleanField(label=_("Disable Gateway"), + initial=False, required=False) + + class Meta: + name = ("Subnet") + help_text = _('You can update a subnet associated with the ' + 'network. Advanced configuration are available ' + 'at "Subnet Detail" tab.') + + def clean(self): + cleaned_data = workflows.Action.clean(self) + self._check_subnet_data(cleaned_data, is_create=False) + return cleaned_data + + +class UpdateSubnetInfo(CreateSubnetInfo): + action_class = UpdateSubnetInfoAction + depends_on = ("network_id", "subnet_id") + + +class UpdateSubnetDetailAction(network_workflows.CreateSubnetDetailAction): + allocation_pools = forms.CharField(widget=forms.HiddenInput(), + required=False) + + class Meta: + name = ("Subnet Detail") + help_text = _('You can specify additional attributes for the subnet.') + + +class UpdateSubnetDetail(network_workflows.CreateSubnetDetail): + action_class = UpdateSubnetDetailAction + + +class UpdateSubnet(network_workflows.CreateNetwork): + slug = "update_subnet" + name = _("Update Subnet") + finalize_button_name = _("Update") + success_message = _('Updated subnet "%s".') + failure_message = _('Unable to update subnet "%s".') + success_url = "horizon:project:networks:detail" + failure_url = "horizon:project:networks:detail" + default_steps = (UpdateSubnetInfo, + UpdateSubnetDetail) + + def format_status_message(self, message): + name = self.context.get('subnet_name') or self.context.get('subnet_id') + return message % name + + def get_success_url(self): + return reverse(self.success_url, + args=(self.context.get('network_id'),)) + + def _update_subnet(self, request, data): + network_id = self.context.get('network_id') + try: + subnet_id = self.context.get('subnet_id') + params = {} + params['name'] = data['subnet_name'] + if data['no_gateway']: + params['gateway_ip'] = None + elif data['gateway_ip']: + params['gateway_ip'] = data['gateway_ip'] + + self._setup_subnet_parameters(params, data, is_create=False) + + subnet = api.quantum.subnet_modify(request, subnet_id, **params) + msg = _('Subnet "%s" was successfully updated.') % data['cidr'] + LOG.debug(msg) + return subnet + except Exception as e: + msg = (_('Failed to update subnet "%(sub)s": ' + ' %(reason)s') % + {"sub": data['cidr'], "reason": e}) + redirect = reverse(self.failure_url, args=(network_id,)) + exceptions.handle(request, msg, redirect=redirect) + return False + + def handle(self, request, data): + subnet = self._update_subnet(request, data) + return True if subnet else False diff --git a/openstack_dashboard/dashboards/project/networks/templates/networks/_detail_overview.html b/openstack_dashboard/dashboards/project/networks/templates/networks/_detail_overview.html index b64341fb5..b8c87a7a2 100644 --- a/openstack_dashboard/dashboards/project/networks/templates/networks/_detail_overview.html +++ b/openstack_dashboard/dashboards/project/networks/templates/networks/_detail_overview.html @@ -16,5 +16,13 @@
{{ network.admin_state|default:"Unknown" }}
{% trans "Shared" %}
{{ network.shared|yesno|capfirst }}
+
{% trans "External Network" %}
+
{{ network.router__external|yesno|capfirst }}
+ {% if network.provider__network_type %} +
{% trans "Provider Network" %}
+
{% trans "Network Type" %}: {{ network.provider__network_type|default:"Unknown" }}
+
{% trans "Physical Network" %}: {{ network.provider__physical_network|default:"-" }}
+
{% trans "Segmentation ID" %}: {{ network.provider__segmentation_id|default:"-" }}
+ {% endif %} diff --git a/openstack_dashboard/dashboards/project/networks/templates/networks/ports/_detail_overview.html b/openstack_dashboard/dashboards/project/networks/templates/networks/ports/_detail_overview.html index 401a5db69..166fcd12d 100644 --- a/openstack_dashboard/dashboards/project/networks/templates/networks/ports/_detail_overview.html +++ b/openstack_dashboard/dashboards/project/networks/templates/networks/ports/_detail_overview.html @@ -10,8 +10,9 @@
{{ port.name|default:"None" }}
{% trans "ID" %}
{{ port.id|default:"None" }}
+ {% url horizon:project:networks:detail port.network_id as network_url %}
{% trans "Network ID" %}
-
{{ port.network_id|default:"None" }}
+
{{ port.network_id|default:"None" }}
{% trans "Project ID" %}
{{ port.tenant_id|default:"-" }}
{% trans "Fixed IP" %}
@@ -31,9 +32,10 @@
{{ port.status|default:"None" }}
{% trans "Admin State" %}
{{ port.admin_state|default:"None" }}
-
{% trans "Device ID" %}
- {% if port.device_id|length > 1 %} -
{{ port.device_id }}
+
{% trans "Attached Device" %}
+ {% if port.device_id|length > 1 or port.device_owner %} +
{% trans "Device Owner" %}: {{ port.device_owner|default:"None" }}
+
{% trans "Device ID" %}: {{ port.device_id|default:"-" }}
{% else %}
No attached device
{% endif %} diff --git a/openstack_dashboard/dashboards/project/networks/templates/networks/subnets/_update.html b/openstack_dashboard/dashboards/project/networks/templates/networks/ports/_update.html similarity index 61% rename from openstack_dashboard/dashboards/project/networks/templates/networks/subnets/_update.html rename to openstack_dashboard/dashboards/project/networks/templates/networks/ports/_update.html index f74b5e280..e490f5a19 100644 --- a/openstack_dashboard/dashboards/project/networks/templates/networks/subnets/_update.html +++ b/openstack_dashboard/dashboards/project/networks/templates/networks/ports/_update.html @@ -1,20 +1,16 @@ {% extends "horizon/common/_modal_form.html" %} {% load i18n %} -{% block form_id %}update_subnet_form{% endblock %} -{% block form_action %}{% url horizon:project:networks:editsubnet network_id subnet_id %}{% endblock %} +{% block form_id %}update_port_form{% endblock %} +{% block form_action %}{% url horizon:project:networks:editport network_id port_id %}{% endblock %} -{% block modal-header %}{% trans "Edit Subnet" %}{% endblock %} +{% block modal-header %}{% trans "Edit Port" %}{% endblock %} {% block modal-body %}
{% trans "ID" %}
-
{{ subnet_id }}
-
{% trans "Network Address" %}
-
{{ cidr }}
-
{% trans "IP version" %}
-
{{ ip_version }}
+
{{ port_id }}

@@ -23,7 +19,7 @@

{% trans "Description:" %}

-

{% trans "You may update the editable properties of your subnet here." %}

+

{% trans "You may update the editable properties of your port here." %}

{% endblock %} diff --git a/openstack_dashboard/dashboards/project/networks/templates/networks/ports/update.html b/openstack_dashboard/dashboards/project/networks/templates/networks/ports/update.html new file mode 100644 index 000000000..f6b932294 --- /dev/null +++ b/openstack_dashboard/dashboards/project/networks/templates/networks/ports/update.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Update Port" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Update Port") %} +{% endblock page_header %} + +{% block main %} + {% include 'project/networks/ports/_update.html' %} +{% endblock %} diff --git a/openstack_dashboard/dashboards/project/networks/templates/networks/subnets/_create.html b/openstack_dashboard/dashboards/project/networks/templates/networks/subnets/_create.html deleted file mode 100644 index cd4ebbc1c..000000000 --- a/openstack_dashboard/dashboards/project/networks/templates/networks/subnets/_create.html +++ /dev/null @@ -1,25 +0,0 @@ -{% extends "horizon/common/_modal_form.html" %} -{% load i18n %} - -{% block form_id %}create_subnet_form{% endblock %} -{% block form_action %}{% url horizon:project:networks:addsubnet network.id %} -{% endblock %} - -{% block modal-header %}{% trans "Create Subnet" %}{% endblock %} - -{% block modal-body %} -
-
- {% include "horizon/common/_form_fields.html" %} -
-
-
-

{% trans "Description" %}:

-

{% trans "You can create a subnet for the network. Any network address can be specified unless the network address does not overlap other subnets in the network." %}

-
-{% endblock %} - -{% block modal-footer %} - - {% trans "Cancel" %} -{% endblock %} diff --git a/openstack_dashboard/dashboards/project/networks/templates/networks/subnets/_detail_overview.html b/openstack_dashboard/dashboards/project/networks/templates/networks/subnets/_detail_overview.html index 4c09fde09..5aa9c1ad6 100644 --- a/openstack_dashboard/dashboards/project/networks/templates/networks/subnets/_detail_overview.html +++ b/openstack_dashboard/dashboards/project/networks/templates/networks/subnets/_detail_overview.html @@ -10,14 +10,13 @@
{{ subnet.name|default:"None" }}
{% trans "ID" %}
{{ subnet.id|default:"None" }}
+ {% url horizon:project:networks:detail subnet.network_id as network_url %}
{% trans "Network ID" %}
-
{{ subnet.network_id|default:"None" }}
-
{% trans "CIDR" %}
-
{{ subnet.cidr|default:"None" }}
+
{{ subnet.network_id|default:"None" }}
{% trans "IP version" %}
{{ subnet.ipver_str|default:"-" }}
-
{% trans "Gateway IP" %}
-
{{ subnet.gateway_ip|default:"-" }}
+
{% trans "CIDR" %}
+
{{ subnet.cidr|default:"None" }}
{% trans "IP allocation pool" %}
{% for pool in subnet.allocation_pools %} @@ -25,5 +24,26 @@ {% trans " - End" %} {{ pool.end }}
{% endfor %}
+
{% trans "DHCP Enable" %}
+
{{ subnet.enable_dhcp|yesno|capfirst }}
+
{% trans "Gateway IP" %}
+
{{ subnet.gateway_ip|default:"-" }}
+
{% trans "Additional routes" %}
+
+ {% for route in subnet.host_routes %} + {% trans "Destination" %} {{ route.destination }} + {% trans " : Next hop" %} {{ route.nexthop }}
+ {% empty %} + {% trans "None" %} + {% endfor %} +
+
{% trans "DNS name server" %}
+
+ {% for dns in subnet.dns_nameservers %} + {{ dns }} + {% empty %} + {% trans "None" %} + {% endfor %} +
diff --git a/openstack_dashboard/dashboards/project/networks/templates/networks/subnets/create.html b/openstack_dashboard/dashboards/project/networks/templates/networks/subnets/create.html index 7a614b1b8..e026151c1 100644 --- a/openstack_dashboard/dashboards/project/networks/templates/networks/subnets/create.html +++ b/openstack_dashboard/dashboards/project/networks/templates/networks/subnets/create.html @@ -7,5 +7,5 @@ {% endblock page_header %} {% block main %} - {% include "project/networks/subnets/_create.html" %} + {% include "horizon/common/_workflow.html" %} {% endblock %} diff --git a/openstack_dashboard/dashboards/project/networks/templates/networks/subnets/update.html b/openstack_dashboard/dashboards/project/networks/templates/networks/subnets/update.html index 1d74569d6..6d7c62311 100644 --- a/openstack_dashboard/dashboards/project/networks/templates/networks/subnets/update.html +++ b/openstack_dashboard/dashboards/project/networks/templates/networks/subnets/update.html @@ -7,5 +7,5 @@ {% endblock page_header %} {% block main %} - {% include 'project/networks/subnets/_update.html' %} + {% include "horizon/common/_workflow.html" %} {% endblock %} diff --git a/openstack_dashboard/dashboards/project/networks/tests.py b/openstack_dashboard/dashboards/project/networks/tests.py index a9e246929..19038e776 100644 --- a/openstack_dashboard/dashboards/project/networks/tests.py +++ b/openstack_dashboard/dashboards/project/networks/tests.py @@ -1,5 +1,5 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 - +# # Copyright 2012 NEC Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -19,6 +19,7 @@ from django.core.urlresolvers import reverse from django.utils.html import escape from mox import IsA +import netaddr from openstack_dashboard import api from openstack_dashboard.test import helpers as test @@ -28,7 +29,70 @@ from .workflows import CreateNetwork INDEX_URL = reverse('horizon:project:networks:index') +def form_data_subnet(subnet, + name=None, cidr=None, ip_version=None, + gateway_ip='', enable_dhcp=None, + allocation_pools=None, + dns_nameservers=None, + host_routes=None): + def get_value(value, default): + return default if value is None else value + + data = {} + data['subnet_name'] = get_value(name, subnet.name) + data['cidr'] = get_value(cidr, subnet.cidr) + data['ip_version'] = get_value(ip_version, subnet.ip_version) + + gateway_ip = subnet.gateway_ip if gateway_ip == '' else gateway_ip + data['gateway_ip'] = gateway_ip or '' + data['no_gateway'] = (gateway_ip is None) + + data['enable_dhcp'] = get_value(enable_dhcp, subnet.enable_dhcp) + + pools = get_value(allocation_pools, subnet.allocation_pools) + data['allocation_pools'] = _str_allocation_pools(pools) + nameservers = get_value(dns_nameservers, subnet.dns_nameservers) + data['dns_nameservers'] = _str_dns_nameservers(nameservers) + routes = get_value(host_routes, subnet.host_routes) + data['host_routes'] = _str_host_routes(routes) + + return data + + +def form_data_no_subnet(): + return {'subnet_name': '', + 'cidr': '', + 'ip_version': 4, + 'gateway_ip': '', + 'no_gateway': False, + 'enable_dhcp': True, + 'allocation_pools': '', + 'dns_nameservers': '', + 'host_routes': ''} + + +def _str_allocation_pools(allocation_pools): + if isinstance(allocation_pools, str): + return allocation_pools + return '\n'.join(['%s,%s' % (pool['start'], pool['end']) + for pool in allocation_pools]) + + +def _str_dns_nameservers(dns_nameservers): + if isinstance(dns_nameservers, str): + return dns_nameservers + return '\n'.join(dns_nameservers) + + +def _str_host_routes(host_routes): + if isinstance(host_routes, str): + return host_routes + return '\n'.join(['%s,%s' % (route['destination'], route['nexthop']) + for route in host_routes]) + + class NetworkTests(test.TestCase): + @test.create_stubs({api.quantum: ('network_list',)}) def test_index(self): api.quantum.network_list( @@ -164,22 +228,23 @@ class NetworkTests(test.TestCase): self.assertTemplateUsed(res, 'project/networks/create.html') self.assertEqual(workflow.name, CreateNetwork.name) expected_objs = ['', - ''] + '', + ''] self.assertQuerysetEqual(workflow.steps, expected_objs) @test.create_stubs({api.quantum: ('network_create',)}) def test_network_create_post(self): network = self.networks.first() - api.quantum.network_create(IsA(http.HttpRequest), name=network.name)\ + api.quantum.network_create(IsA(http.HttpRequest), name=network.name, + admin_state_up=network.admin_state_up)\ .AndReturn(network) self.mox.ReplayAll() form_data = {'net_name': network.name, - 'with_subnet': False, - 'subnet_name': '', - 'cidr': '', - 'ip_version': 4, - 'gateway_ip': ''} + 'admin_state': network.admin_state_up, + # subnet + 'with_subnet': False} + form_data.update(form_data_no_subnet()) url = reverse('horizon:project:networks:create') res = self.client.post(url, form_data) @@ -191,23 +256,23 @@ class NetworkTests(test.TestCase): def test_network_create_post_with_subnet(self): network = self.networks.first() subnet = self.subnets.first() - api.quantum.network_create(IsA(http.HttpRequest), name=network.name)\ + api.quantum.network_create(IsA(http.HttpRequest), name=network.name, + admin_state_up=network.admin_state_up)\ .AndReturn(network) api.quantum.subnet_create(IsA(http.HttpRequest), network_id=network.id, name=subnet.name, cidr=subnet.cidr, ip_version=subnet.ip_version, - gateway_ip=subnet.gateway_ip)\ + gateway_ip=subnet.gateway_ip, + enable_dhcp=subnet.enable_dhcp)\ .AndReturn(subnet) self.mox.ReplayAll() form_data = {'net_name': network.name, - 'with_subnet': True, - 'subnet_name': subnet.name, - 'cidr': subnet.cidr, - 'ip_version': subnet.ip_version, - 'gateway_ip': subnet.gateway_ip} + 'admin_state': network.admin_state_up, + 'with_subnet': True} + form_data.update(form_data_subnet(subnet, allocation_pools=[])) url = reverse('horizon:project:networks:create') res = self.client.post(url, form_data) @@ -217,16 +282,16 @@ class NetworkTests(test.TestCase): @test.create_stubs({api.quantum: ('network_create',)}) def test_network_create_post_network_exception(self): network = self.networks.first() - api.quantum.network_create(IsA(http.HttpRequest), name=network.name)\ + api.quantum.network_create(IsA(http.HttpRequest), name=network.name, + admin_state_up=network.admin_state_up)\ .AndRaise(self.exceptions.quantum) self.mox.ReplayAll() form_data = {'net_name': network.name, - 'with_subnet': False, - 'subnet_name': '', - 'cidr': '', - 'ip_version': 4, - 'gateway_ip': ''} + 'admin_state': network.admin_state_up, + # subnet + 'with_subnet': False} + form_data.update(form_data_no_subnet()) url = reverse('horizon:project:networks:create') res = self.client.post(url, form_data) @@ -237,16 +302,15 @@ class NetworkTests(test.TestCase): def test_network_create_post_with_subnet_network_exception(self): network = self.networks.first() subnet = self.subnets.first() - api.quantum.network_create(IsA(http.HttpRequest), name=network.name)\ + api.quantum.network_create(IsA(http.HttpRequest), name=network.name, + admin_state_up=network.admin_state_up)\ .AndRaise(self.exceptions.quantum) self.mox.ReplayAll() form_data = {'net_name': network.name, - 'with_subnet': True, - 'subnet_name': subnet.name, - 'cidr': subnet.cidr, - 'ip_version': subnet.ip_version, - 'gateway_ip': subnet.gateway_ip} + 'admin_state': network.admin_state_up, + 'with_subnet': True} + form_data.update(form_data_subnet(subnet, allocation_pools=[])) url = reverse('horizon:project:networks:create') res = self.client.post(url, form_data) @@ -254,27 +318,30 @@ class NetworkTests(test.TestCase): self.assertRedirectsNoFollow(res, INDEX_URL) @test.create_stubs({api.quantum: ('network_create', + 'network_delete', 'subnet_create',)}) def test_network_create_post_with_subnet_subnet_exception(self): network = self.networks.first() subnet = self.subnets.first() - api.quantum.network_create(IsA(http.HttpRequest), name=network.name)\ + api.quantum.network_create(IsA(http.HttpRequest), name=network.name, + admin_state_up=network.admin_state_up)\ .AndReturn(network) api.quantum.subnet_create(IsA(http.HttpRequest), network_id=network.id, name=subnet.name, cidr=subnet.cidr, ip_version=subnet.ip_version, - gateway_ip=subnet.gateway_ip)\ + gateway_ip=subnet.gateway_ip, + enable_dhcp=subnet.enable_dhcp)\ .AndRaise(self.exceptions.quantum) + api.quantum.network_delete(IsA(http.HttpRequest), + network.id) self.mox.ReplayAll() form_data = {'net_name': network.name, - 'with_subnet': True, - 'subnet_name': subnet.name, - 'cidr': subnet.cidr, - 'ip_version': subnet.ip_version, - 'gateway_ip': subnet.gateway_ip} + 'admin_state': network.admin_state_up, + 'with_subnet': True} + form_data.update(form_data_subnet(subnet, allocation_pools=[])) url = reverse('horizon:project:networks:create') res = self.client.post(url, form_data) @@ -287,11 +354,10 @@ class NetworkTests(test.TestCase): self.mox.ReplayAll() form_data = {'net_name': network.name, - 'with_subnet': True, - 'subnet_name': subnet.name, - 'cidr': '', - 'ip_version': subnet.ip_version, - 'gateway_ip': subnet.gateway_ip} + 'admin_state': network.admin_state_up, + 'with_subnet': True} + form_data.update(form_data_subnet(subnet, cidr='', + allocation_pools=[])) url = reverse('horizon:project:networks:create') res = self.client.post(url, form_data) @@ -306,11 +372,10 @@ class NetworkTests(test.TestCase): # dummy IPv6 address cidr = '2001:0DB8:0:CD30:123:4567:89AB:CDEF/60' form_data = {'net_name': network.name, - 'with_subnet': True, - 'subnet_name': subnet.name, - 'cidr': cidr, - 'ip_version': subnet.ip_version, - 'gateway_ip': subnet.gateway_ip} + 'admin_state': network.admin_state_up, + 'with_subnet': True} + form_data.update(form_data_subnet(subnet, cidr=cidr, + allocation_pools=[])) url = reverse('horizon:project:networks:create') res = self.client.post(url, form_data) @@ -325,11 +390,10 @@ class NetworkTests(test.TestCase): # dummy IPv6 address gateway_ip = '2001:0DB8:0:CD30:123:4567:89AB:CDEF' form_data = {'net_name': network.name, - 'with_subnet': True, - 'subnet_name': subnet.name, - 'cidr': subnet.cidr, - 'ip_version': subnet.ip_version, - 'gateway_ip': gateway_ip} + 'admin_state': network.admin_state_up, + 'with_subnet': True} + form_data.update(form_data_subnet(subnet, gateway_ip=gateway_ip, + allocation_pools=[])) url = reverse('horizon:project:networks:create') res = self.client.post(url, form_data) @@ -367,17 +431,19 @@ class NetworkTests(test.TestCase): def test_network_update_post(self): network = self.networks.first() api.quantum.network_modify(IsA(http.HttpRequest), network.id, - name=network.name)\ + name=network.name, + admin_state_up=network.admin_state_up)\ .AndReturn(network) api.quantum.network_get(IsA(http.HttpRequest), network.id)\ .AndReturn(network) self.mox.ReplayAll() - formData = {'network_id': network.id, - 'name': network.name, - 'tenant_id': network.tenant_id} + form_data = {'network_id': network.id, + 'name': network.name, + 'admin_state': network.admin_state_up, + 'tenant_id': network.tenant_id} url = reverse('horizon:project:networks:update', args=[network.id]) - res = self.client.post(url, formData) + res = self.client.post(url, form_data) self.assertRedirectsNoFollow(res, INDEX_URL) @@ -386,7 +452,8 @@ class NetworkTests(test.TestCase): def test_network_update_post_exception(self): network = self.networks.first() api.quantum.network_modify(IsA(http.HttpRequest), network.id, - name=network.name)\ + name=network.name, + admin_state_up=network.admin_state_up)\ .AndRaise(self.exceptions.quantum) api.quantum.network_get(IsA(http.HttpRequest), network.id)\ .AndReturn(network) @@ -394,6 +461,7 @@ class NetworkTests(test.TestCase): form_data = {'network_id': network.id, 'name': network.name, + 'admin_state': network.admin_state_up, 'tenant_id': network.tenant_id} url = reverse('horizon:project:networks:update', args=[network.id]) res = self.client.post(url, form_data) @@ -475,6 +543,9 @@ class NetworkTests(test.TestCase): self.assertRedirectsNoFollow(res, INDEX_URL) + +class NetworkSubnetTests(test.TestCase): + @test.create_stubs({api.quantum: ('subnet_get',)}) def test_subnet_detail(self): subnet = self.subnets.first() @@ -528,20 +599,76 @@ class NetworkTests(test.TestCase): .AndReturn(self.networks.first()) api.quantum.subnet_create(IsA(http.HttpRequest), network_id=network.id, - network_name=network.name, name=subnet.name, cidr=subnet.cidr, ip_version=subnet.ip_version, - gateway_ip=subnet.gateway_ip)\ + gateway_ip=subnet.gateway_ip, + enable_dhcp=subnet.enable_dhcp, + allocation_pools=subnet.allocation_pools)\ .AndReturn(subnet) self.mox.ReplayAll() - form_data = {'network_id': subnet.network_id, - 'network_name': network.name, - 'name': subnet.name, - 'cidr': subnet.cidr, - 'ip_version': subnet.ip_version, - 'gateway_ip': subnet.gateway_ip} + form_data = form_data_subnet(subnet) + url = reverse('horizon:project:networks:addsubnet', + args=[subnet.network_id]) + res = self.client.post(url, form_data) + + self.assertNoFormErrors(res) + redir_url = reverse('horizon:project:networks:detail', + args=[subnet.network_id]) + self.assertRedirectsNoFollow(res, redir_url) + + @test.create_stubs({api.quantum: ('network_get', + 'subnet_create',)}) + def test_subnet_create_post_with_additional_attributes(self): + network = self.networks.list()[1] + subnet = self.subnets.list()[1] + api.quantum.network_get(IsA(http.HttpRequest), + network.id)\ + .AndReturn(self.networks.first()) + api.quantum.subnet_create(IsA(http.HttpRequest), + network_id=network.id, + name=subnet.name, + cidr=subnet.cidr, + ip_version=subnet.ip_version, + gateway_ip=subnet.gateway_ip, + enable_dhcp=subnet.enable_dhcp, + allocation_pools=subnet.allocation_pools, + dns_nameservers=subnet.dns_nameservers, + host_routes=subnet.host_routes)\ + .AndReturn(subnet) + self.mox.ReplayAll() + + form_data = form_data_subnet(subnet) + url = reverse('horizon:project:networks:addsubnet', + args=[subnet.network_id]) + res = self.client.post(url, form_data) + + self.assertNoFormErrors(res) + redir_url = reverse('horizon:project:networks:detail', + args=[subnet.network_id]) + self.assertRedirectsNoFollow(res, redir_url) + + @test.create_stubs({api.quantum: ('network_get', + 'subnet_create',)}) + def test_subnet_create_post_with_additional_attributes(self): + network = self.networks.first() + subnet = self.subnets.first() + api.quantum.network_get(IsA(http.HttpRequest), + network.id)\ + .AndReturn(self.networks.first()) + api.quantum.subnet_create(IsA(http.HttpRequest), + network_id=network.id, + name=subnet.name, + cidr=subnet.cidr, + ip_version=subnet.ip_version, + gateway_ip=None, + enable_dhcp=subnet.enable_dhcp, + allocation_pools=subnet.allocation_pools)\ + .AndReturn(subnet) + self.mox.ReplayAll() + + form_data = form_data_subnet(subnet, gateway_ip=None) url = reverse('horizon:project:networks:addsubnet', args=[subnet.network_id]) res = self.client.post(url, form_data) @@ -561,12 +688,8 @@ class NetworkTests(test.TestCase): .AndRaise(self.exceptions.quantum) self.mox.ReplayAll() - form_data = {'network_id': subnet.network_id, - 'network_name': network.name, - 'name': subnet.name, - 'cidr': subnet.cidr, - 'ip_version': subnet.ip_version, - 'gateway_ip': subnet.gateway_ip} + form_data = form_data_subnet(subnet, + allocation_pools=[]) url = reverse('horizon:project:networks:addsubnet', args=[subnet.network_id]) res = self.client.post(url, form_data) @@ -584,20 +707,16 @@ class NetworkTests(test.TestCase): .AndReturn(self.networks.first()) api.quantum.subnet_create(IsA(http.HttpRequest), network_id=network.id, - network_name=network.name, name=subnet.name, cidr=subnet.cidr, ip_version=subnet.ip_version, - gateway_ip=subnet.gateway_ip)\ + gateway_ip=subnet.gateway_ip, + enable_dhcp=subnet.enable_dhcp)\ .AndRaise(self.exceptions.quantum) self.mox.ReplayAll() - form_data = {'network_id': subnet.network_id, - 'network_name': network.name, - 'name': subnet.name, - 'cidr': subnet.cidr, - 'ip_version': subnet.ip_version, - 'gateway_ip': subnet.gateway_ip} + form_data = form_data_subnet(subnet, + allocation_pools=[]) url = reverse('horizon:project:networks:addsubnet', args=[subnet.network_id]) res = self.client.post(url, form_data) @@ -617,18 +736,16 @@ class NetworkTests(test.TestCase): # dummy IPv6 address cidr = '2001:0DB8:0:CD30:123:4567:89AB:CDEF/60' - form_data = {'network_id': subnet.network_id, - 'network_name': network.name, - 'name': subnet.name, - 'cidr': cidr, - 'ip_version': subnet.ip_version, - 'gateway_ip': subnet.gateway_ip} + form_data = form_data_subnet(subnet, cidr=cidr, + allocation_pools=[]) url = reverse('horizon:project:networks:addsubnet', args=[subnet.network_id]) res = self.client.post(url, form_data) expected_msg = 'Network Address and IP version are inconsistent.' - self.assertContains(res, expected_msg) + self.assertFormErrors(res, 1, expected_msg) + self.assertTemplateUsed(res, + 'project/networks/subnets/create.html') @test.create_stubs({api.quantum: ('network_get',)}) def test_subnet_create_post_gw_inconsistent(self): @@ -641,18 +758,220 @@ class NetworkTests(test.TestCase): # dummy IPv6 address gateway_ip = '2001:0DB8:0:CD30:123:4567:89AB:CDEF' - form_data = {'network_id': subnet.network_id, - 'network_name': network.name, - 'name': subnet.name, - 'cidr': subnet.cidr, - 'ip_version': subnet.ip_version, - 'gateway_ip': gateway_ip} + form_data = form_data_subnet(subnet, gateway_ip=gateway_ip, + allocation_pools=[]) url = reverse('horizon:project:networks:addsubnet', args=[subnet.network_id]) res = self.client.post(url, form_data) self.assertContains(res, 'Gateway IP and IP version are inconsistent.') + @test.create_stubs({api.quantum: ('network_get',)}) + def test_subnet_create_post_invalid_pools_start_only(self): + network = self.networks.first() + subnet = self.subnets.first() + api.quantum.network_get(IsA(http.HttpRequest), + network.id).AndReturn(network) + self.mox.ReplayAll() + + # Start only allocation_pools + allocation_pools = '10.0.0.2' + form_data = form_data_subnet(subnet, + allocation_pools=allocation_pools) + url = reverse('horizon:project:networks:addsubnet', + args=[subnet.network_id]) + res = self.client.post(url, form_data) + + self.assertContains(res, + 'Start and end addresses must be specified ' + '(value=%s)' % allocation_pools) + + @test.create_stubs({api.quantum: ('network_get',)}) + def test_subnet_create_post_invalid_pools_three_entries(self): + network = self.networks.first() + subnet = self.subnets.first() + api.quantum.network_get(IsA(http.HttpRequest), + network.id).AndReturn(network) + self.mox.ReplayAll() + + # pool with three entries + allocation_pools = '10.0.0.2,10.0.0.3,10.0.0.4' + form_data = form_data_subnet(subnet, + allocation_pools=allocation_pools) + url = reverse('horizon:project:networks:addsubnet', + args=[subnet.network_id]) + res = self.client.post(url, form_data) + + self.assertContains(res, + 'Start and end addresses must be specified ' + '(value=%s)' % allocation_pools) + + @test.create_stubs({api.quantum: ('network_get',)}) + def test_subnet_create_post_invalid_pools_invalid_address(self): + network = self.networks.first() + subnet = self.subnets.first() + api.quantum.network_get(IsA(http.HttpRequest), + network.id).AndReturn(network) + self.mox.ReplayAll() + + # end address is not a valid IP address + allocation_pools = '10.0.0.2,invalid_address' + form_data = form_data_subnet(subnet, + allocation_pools=allocation_pools) + url = reverse('horizon:project:networks:addsubnet', + args=[subnet.network_id]) + res = self.client.post(url, form_data) + + self.assertContains(res, + 'allocation_pools: Invalid IP address ' + '(value=%s)' % allocation_pools.split(',')[1]) + + @test.create_stubs({api.quantum: ('network_get',)}) + def test_subnet_create_post_invalid_pools_ip_network(self): + network = self.networks.first() + subnet = self.subnets.first() + api.quantum.network_get(IsA(http.HttpRequest), + network.id).AndReturn(network) + self.mox.ReplayAll() + + # start address is CIDR + allocation_pools = '10.0.0.2/24,10.0.0.5' + form_data = form_data_subnet(subnet, + allocation_pools=allocation_pools) + url = reverse('horizon:project:networks:addsubnet', + args=[subnet.network_id]) + res = self.client.post(url, form_data) + + self.assertContains(res, + 'allocation_pools: Invalid IP address ' + '(value=%s)' % allocation_pools.split(',')[0]) + + @test.create_stubs({api.quantum: ('network_get',)}) + def test_subnet_create_post_invalid_pools_start_larger_than_end(self): + network = self.networks.first() + subnet = self.subnets.first() + api.quantum.network_get(IsA(http.HttpRequest), + network.id).AndReturn(network) + self.mox.ReplayAll() + + # start address is larger than end address + allocation_pools = '10.0.0.254,10.0.0.2' + form_data = form_data_subnet(subnet, + allocation_pools=allocation_pools) + url = reverse('horizon:project:networks:addsubnet', + args=[subnet.network_id]) + res = self.client.post(url, form_data) + + self.assertContains(res, + 'Start address is larger than end address ' + '(value=%s)' % allocation_pools) + + @test.create_stubs({api.quantum: ('network_get',)}) + def test_subnet_create_post_invalid_nameservers(self): + network = self.networks.first() + subnet = self.subnets.first() + api.quantum.network_get(IsA(http.HttpRequest), + network.id).AndReturn(network) + self.mox.ReplayAll() + + # invalid DNS server address + dns_nameservers = ['192.168.0.2', 'invalid_address'] + form_data = form_data_subnet(subnet, dns_nameservers=dns_nameservers, + allocation_pools=[]) + url = reverse('horizon:project:networks:addsubnet', + args=[subnet.network_id]) + res = self.client.post(url, form_data) + + self.assertContains(res, + 'dns_nameservers: Invalid IP address ' + '(value=%s)' % dns_nameservers[1]) + + @test.create_stubs({api.quantum: ('network_get',)}) + def test_subnet_create_post_invalid_routes_destination_only(self): + network = self.networks.first() + subnet = self.subnets.first() + api.quantum.network_get(IsA(http.HttpRequest), + network.id).AndReturn(network) + self.mox.ReplayAll() + + # Start only host_route + host_routes = '192.168.0.0/24' + form_data = form_data_subnet(subnet, + allocation_pools=[], + host_routes=host_routes) + url = reverse('horizon:project:networks:addsubnet', + args=[subnet.network_id]) + res = self.client.post(url, form_data) + + self.assertContains(res, + 'Host Routes format error: ' + 'Destination CIDR and nexthop must be specified ' + '(value=%s)' % host_routes) + + @test.create_stubs({api.quantum: ('network_get',)}) + def test_subnet_create_post_invalid_routes_three_entries(self): + network = self.networks.first() + subnet = self.subnets.first() + api.quantum.network_get(IsA(http.HttpRequest), + network.id).AndReturn(network) + self.mox.ReplayAll() + + # host_route with three entries + host_routes = 'aaaa,bbbb,cccc' + form_data = form_data_subnet(subnet, + allocation_pools=[], + host_routes=host_routes) + url = reverse('horizon:project:networks:addsubnet', + args=[subnet.network_id]) + res = self.client.post(url, form_data) + + self.assertContains(res, + 'Host Routes format error: ' + 'Destination CIDR and nexthop must be specified ' + '(value=%s)' % host_routes) + + @test.create_stubs({api.quantum: ('network_get',)}) + def test_subnet_create_post_invalid_routes_invalid_destination(self): + network = self.networks.first() + subnet = self.subnets.first() + api.quantum.network_get(IsA(http.HttpRequest), + network.id).AndReturn(network) + self.mox.ReplayAll() + + # invalid destination network + host_routes = '172.16.0.0/64,10.0.0.253' + form_data = form_data_subnet(subnet, + host_routes=host_routes, + allocation_pools=[]) + url = reverse('horizon:project:networks:addsubnet', + args=[subnet.network_id]) + res = self.client.post(url, form_data) + + self.assertContains(res, + 'host_routes: Invalid IP address ' + '(value=%s)' % host_routes.split(',')[0]) + + @test.create_stubs({api.quantum: ('network_get',)}) + def test_subnet_create_post_invalid_routes_nexthop_ip_network(self): + network = self.networks.first() + subnet = self.subnets.first() + api.quantum.network_get(IsA(http.HttpRequest), + network.id).AndReturn(network) + self.mox.ReplayAll() + + # nexthop is not an IP address + host_routes = '172.16.0.0/24,10.0.0.253/24' + form_data = form_data_subnet(subnet, + host_routes=host_routes, + allocation_pools=[]) + url = reverse('horizon:project:networks:addsubnet', + args=[subnet.network_id]) + res = self.client.post(url, form_data) + + self.assertContains(res, + 'host_routes: Invalid IP address ' + '(value=%s)' % host_routes.split(',')[1]) + @test.create_stubs({api.quantum: ('subnet_modify', 'subnet_get',)}) def test_subnet_update_post(self): @@ -661,19 +980,92 @@ class NetworkTests(test.TestCase): .AndReturn(subnet) api.quantum.subnet_modify(IsA(http.HttpRequest), subnet.id, name=subnet.name, - gateway_ip=subnet.gateway_ip)\ + gateway_ip=subnet.gateway_ip, + enable_dhcp=subnet.enable_dhcp)\ .AndReturn(subnet) self.mox.ReplayAll() - formData = {'network_id': subnet.network_id, - 'subnet_id': subnet.id, - 'name': subnet.name, - 'cidr': subnet.cidr, - 'ip_version': subnet.ip_version, - 'gateway_ip': subnet.gateway_ip} + form_data = form_data_subnet(subnet, + allocation_pools=[]) url = reverse('horizon:project:networks:editsubnet', args=[subnet.network_id, subnet.id]) - res = self.client.post(url, formData) + res = self.client.post(url, form_data) + + redir_url = reverse('horizon:project:networks:detail', + args=[subnet.network_id]) + self.assertRedirectsNoFollow(res, redir_url) + + @test.create_stubs({api.quantum: ('subnet_modify', + 'subnet_get',)}) + def test_subnet_update_post(self): + subnet = self.subnets.first() + api.quantum.subnet_get(IsA(http.HttpRequest), subnet.id)\ + .AndReturn(subnet) + api.quantum.subnet_modify(IsA(http.HttpRequest), subnet.id, + name=subnet.name, + gateway_ip=subnet.gateway_ip, + enable_dhcp=subnet.enable_dhcp, + dns_nameservers=[], + host_routes=[])\ + .AndReturn(subnet) + self.mox.ReplayAll() + + form_data = form_data_subnet(subnet, + allocation_pools=[]) + url = reverse('horizon:project:networks:editsubnet', + args=[subnet.network_id, subnet.id]) + res = self.client.post(url, form_data) + + redir_url = reverse('horizon:project:networks:detail', + args=[subnet.network_id]) + self.assertRedirectsNoFollow(res, redir_url) + + @test.create_stubs({api.quantum: ('subnet_modify', + 'subnet_get',)}) + def test_subnet_update_post_no_gateway(self): + subnet = self.subnets.first() + api.quantum.subnet_get(IsA(http.HttpRequest), subnet.id)\ + .AndReturn(subnet) + api.quantum.subnet_modify(IsA(http.HttpRequest), subnet.id, + name=subnet.name, + gateway_ip=None, + enable_dhcp=subnet.enable_dhcp, + dns_nameservers=[], + host_routes=[])\ + .AndReturn(subnet) + self.mox.ReplayAll() + + form_data = form_data_subnet(subnet, + gateway_ip=None, + allocation_pools=[]) + url = reverse('horizon:project:networks:editsubnet', + args=[subnet.network_id, subnet.id]) + res = self.client.post(url, form_data) + + redir_url = reverse('horizon:project:networks:detail', + args=[subnet.network_id]) + self.assertRedirectsNoFollow(res, redir_url) + + @test.create_stubs({api.quantum: ('subnet_modify', + 'subnet_get',)}) + def test_subnet_update_post_with_additional_attributes(self): + subnet = self.subnets.list()[1] + api.quantum.subnet_get(IsA(http.HttpRequest), subnet.id)\ + .AndReturn(subnet) + api.quantum.subnet_modify(IsA(http.HttpRequest), subnet.id, + name=subnet.name, + gateway_ip=subnet.gateway_ip, + enable_dhcp=False, + dns_nameservers=subnet.dns_nameservers, + host_routes=subnet.host_routes)\ + .AndReturn(subnet) + self.mox.ReplayAll() + + form_data = form_data_subnet(subnet, + enable_dhcp=False) + url = reverse('horizon:project:networks:editsubnet', + args=[subnet.network_id, subnet.id]) + res = self.client.post(url, form_data) redir_url = reverse('horizon:project:networks:detail', args=[subnet.network_id]) @@ -689,18 +1081,120 @@ class NetworkTests(test.TestCase): # dummy IPv6 address gateway_ip = '2001:0DB8:0:CD30:123:4567:89AB:CDEF' - formData = {'network_id': subnet.network_id, - 'subnet_id': subnet.id, - 'name': subnet.name, - 'cidr': subnet.cidr, - 'ip_version': subnet.ip_version, - 'gateway_ip': gateway_ip} + form_data = form_data_subnet(subnet, gateway_ip=gateway_ip, + allocation_pools=[]) url = reverse('horizon:project:networks:editsubnet', args=[subnet.network_id, subnet.id]) - res = self.client.post(url, formData) + res = self.client.post(url, form_data) self.assertContains(res, 'Gateway IP and IP version are inconsistent.') + @test.create_stubs({api.quantum: ('subnet_modify', + 'subnet_get',)}) + def test_subnet_update_post_invalid_nameservers(self): + subnet = self.subnets.first() + api.quantum.subnet_get(IsA(http.HttpRequest), subnet.id)\ + .AndReturn(subnet) + self.mox.ReplayAll() + + # invalid DNS server address + dns_nameservers = ['192.168.0.2', 'invalid_address'] + form_data = form_data_subnet(subnet, dns_nameservers=dns_nameservers, + allocation_pools=[]) + url = reverse('horizon:project:networks:editsubnet', + args=[subnet.network_id, subnet.id]) + res = self.client.post(url, form_data) + + self.assertContains(res, + 'dns_nameservers: Invalid IP address ' + '(value=%s)' % dns_nameservers[1]) + + @test.create_stubs({api.quantum: ('subnet_modify', + 'subnet_get',)}) + def test_subnet_update_post_invalid_routes_destination_only(self): + subnet = self.subnets.first() + api.quantum.subnet_get(IsA(http.HttpRequest), subnet.id)\ + .AndReturn(subnet) + self.mox.ReplayAll() + + # Start only host_route + host_routes = '192.168.0.0/24' + form_data = form_data_subnet(subnet, + allocation_pools=[], + host_routes=host_routes) + url = reverse('horizon:project:networks:editsubnet', + args=[subnet.network_id, subnet.id]) + res = self.client.post(url, form_data) + + self.assertContains(res, + 'Host Routes format error: ' + 'Destination CIDR and nexthop must be specified ' + '(value=%s)' % host_routes) + + @test.create_stubs({api.quantum: ('subnet_modify', + 'subnet_get',)}) + def test_subnet_update_post_invalid_routes_three_entries(self): + subnet = self.subnets.first() + api.quantum.subnet_get(IsA(http.HttpRequest), subnet.id)\ + .AndReturn(subnet) + self.mox.ReplayAll() + + # host_route with three entries + host_routes = 'aaaa,bbbb,cccc' + form_data = form_data_subnet(subnet, + allocation_pools=[], + host_routes=host_routes) + url = reverse('horizon:project:networks:editsubnet', + args=[subnet.network_id, subnet.id]) + res = self.client.post(url, form_data) + + self.assertContains(res, + 'Host Routes format error: ' + 'Destination CIDR and nexthop must be specified ' + '(value=%s)' % host_routes) + + @test.create_stubs({api.quantum: ('subnet_modify', + 'subnet_get',)}) + def test_subnet_update_post_invalid_routes_invalid_destination(self): + subnet = self.subnets.first() + api.quantum.subnet_get(IsA(http.HttpRequest), subnet.id)\ + .AndReturn(subnet) + self.mox.ReplayAll() + + # invalid destination network + host_routes = '172.16.0.0/64,10.0.0.253' + form_data = form_data_subnet(subnet, + host_routes=host_routes, + allocation_pools=[]) + url = reverse('horizon:project:networks:editsubnet', + args=[subnet.network_id, subnet.id]) + res = self.client.post(url, form_data) + + self.assertContains(res, + 'host_routes: Invalid IP address ' + '(value=%s)' % host_routes.split(',')[0]) + + @test.create_stubs({api.quantum: ('subnet_modify', + 'subnet_get',)}) + def test_subnet_update_post_invalid_routes_nexthop_ip_network(self): + subnet = self.subnets.first() + api.quantum.subnet_get(IsA(http.HttpRequest), subnet.id)\ + .AndReturn(subnet) + self.mox.ReplayAll() + + # nexthop is not an IP address + host_routes = '172.16.0.0/24,10.0.0.253/24' + form_data = form_data_subnet(subnet, + host_routes=host_routes, + allocation_pools=[]) + url = reverse('horizon:project:networks:editsubnet', + args=[subnet.network_id, subnet.id]) + res = self.client.post(url, form_data) + + self.assertContains(res, + 'host_routes: Invalid IP address ' + '(value=%s)' % host_routes.split(',')[1]) + @test.create_stubs({api.quantum: ('subnet_delete', 'subnet_list', 'network_get', @@ -720,10 +1214,10 @@ class NetworkTests(test.TestCase): .AndReturn(self.networks.first()) self.mox.ReplayAll() - formData = {'action': 'subnets__delete__%s' % subnet.id} + form_data = {'action': 'subnets__delete__%s' % subnet.id} url = reverse('horizon:project:networks:detail', args=[network_id]) - res = self.client.post(url, formData) + res = self.client.post(url, form_data) self.assertRedirectsNoFollow(res, url) @@ -747,13 +1241,16 @@ class NetworkTests(test.TestCase): .AndReturn(self.networks.first()) self.mox.ReplayAll() - formData = {'action': 'subnets__delete__%s' % subnet.id} + form_data = {'action': 'subnets__delete__%s' % subnet.id} url = reverse('horizon:project:networks:detail', args=[network_id]) - res = self.client.post(url, formData) + res = self.client.post(url, form_data) self.assertRedirectsNoFollow(res, url) + +class NetworkPortTests(test.TestCase): + @test.create_stubs({api.quantum: ('port_get',)}) def test_port_detail(self): port = self.ports.first() @@ -780,3 +1277,65 @@ class NetworkTests(test.TestCase): args=[port.id])) self.assertRedirectsNoFollow(res, INDEX_URL) + + @test.create_stubs({api.quantum: ('port_get',)}) + def test_port_update_get(self): + port = self.ports.first() + api.quantum.port_get(IsA(http.HttpRequest), + port.id)\ + .AndReturn(port) + self.mox.ReplayAll() + + url = reverse('horizon:project:networks:editport', + args=[port.network_id, port.id]) + res = self.client.get(url) + + self.assertTemplateUsed(res, 'project/networks/ports/update.html') + + @test.create_stubs({api.quantum: ('port_get', + 'port_modify')}) + def test_port_update_post(self): + port = self.ports.first() + api.quantum.port_get(IsA(http.HttpRequest), port.id)\ + .AndReturn(port) + api.quantum.port_modify(IsA(http.HttpRequest), port.id, + name=port.name, + admin_state_up=port.admin_state_up)\ + .AndReturn(port) + self.mox.ReplayAll() + + form_data = {'network_id': port.network_id, + 'port_id': port.id, + 'name': port.name, + 'admin_state': port.admin_state_up} + url = reverse('horizon:project:networks:editport', + args=[port.network_id, port.id]) + res = self.client.post(url, form_data) + + redir_url = reverse('horizon:project:networks:detail', + args=[port.network_id]) + self.assertRedirectsNoFollow(res, redir_url) + + @test.create_stubs({api.quantum: ('port_get', + 'port_modify')}) + def test_port_update_post_exception(self): + port = self.ports.first() + api.quantum.port_get(IsA(http.HttpRequest), port.id)\ + .AndReturn(port) + api.quantum.port_modify(IsA(http.HttpRequest), port.id, + name=port.name, + admin_state_up=port.admin_state_up)\ + .AndRaise(self.exceptions.quantum) + self.mox.ReplayAll() + + form_data = {'network_id': port.network_id, + 'port_id': port.id, + 'name': port.name, + 'admin_state': port.admin_state_up} + url = reverse('horizon:project:networks:editport', + args=[port.network_id, port.id]) + res = self.client.post(url, form_data) + + redir_url = reverse('horizon:project:networks:detail', + args=[port.network_id]) + self.assertRedirectsNoFollow(res, redir_url) diff --git a/openstack_dashboard/dashboards/project/networks/urls.py b/openstack_dashboard/dashboards/project/networks/urls.py index ee856cb37..06f53b79e 100644 --- a/openstack_dashboard/dashboards/project/networks/urls.py +++ b/openstack_dashboard/dashboards/project/networks/urls.py @@ -20,6 +20,7 @@ from .views import IndexView, CreateView, DetailView, UpdateView from .subnets.views import CreateView as AddSubnetView from .subnets.views import UpdateView as EditSubnetView from .subnets import urls as subnet_urls +from .ports.views import UpdateView as EditPortView from .ports import urls as port_urls @@ -35,5 +36,7 @@ urlpatterns = patterns('', name='addsubnet'), url(r'^(?P[^/]+)/subnets/(?P[^/]+)/update$', EditSubnetView.as_view(), name='editsubnet'), + url(r'^(?P[^/]+)/ports/(?P[^/]+)/update$', + EditPortView.as_view(), name='editport'), url(r'^subnets/', include(subnet_urls, namespace='subnets')), url(r'^ports/', include(port_urls, namespace='ports'))) diff --git a/openstack_dashboard/dashboards/project/networks/views.py b/openstack_dashboard/dashboards/project/networks/views.py index dbc5d18fa..a4be61841 100644 --- a/openstack_dashboard/dashboards/project/networks/views.py +++ b/openstack_dashboard/dashboards/project/networks/views.py @@ -91,7 +91,8 @@ class UpdateView(forms.ModalFormView): network = self._get_object() return {'network_id': network['id'], 'tenant_id': network['tenant_id'], - 'name': network['name']} + 'name': network['name'], + 'admin_state': network['admin_state_up']} class DetailView(tables.MultiTableView): diff --git a/openstack_dashboard/dashboards/project/networks/workflows.py b/openstack_dashboard/dashboards/project/networks/workflows.py index 54d4a9bf9..41283ece9 100644 --- a/openstack_dashboard/dashboards/project/networks/workflows.py +++ b/openstack_dashboard/dashboards/project/networks/workflows.py @@ -23,6 +23,7 @@ from django.utils.translation import ugettext as _ from horizon import exceptions from horizon import forms +from horizon import messages from horizon import workflows from horizon.utils import fields @@ -34,8 +35,12 @@ LOG = logging.getLogger(__name__) class CreateNetworkInfoAction(workflows.Action): net_name = forms.CharField(max_length=255, - label=_("Network Name (optional)"), + label=_("Network Name"), + help_text=_("Network Name. This field is " + "optional."), required=False) + admin_state = forms.BooleanField(label=_("Admin State"), + initial=True, required=False) class Meta: name = ("Network") @@ -46,14 +51,16 @@ class CreateNetworkInfoAction(workflows.Action): class CreateNetworkInfo(workflows.Step): action_class = CreateNetworkInfoAction - contributes = ("net_name",) + contributes = ("net_name", "admin_state") class CreateSubnetInfoAction(workflows.Action): with_subnet = forms.BooleanField(label=_("Create Subnet"), initial=True, required=False) subnet_name = forms.CharField(max_length=255, - label=_("Subnet Name (optional)"), + label=_("Subnet Name"), + help_text=_("Subnet Name. This field is " + "optional."), required=False) cidr = fields.IPField(label=_("Network Address"), required=False, @@ -64,13 +71,21 @@ class CreateSubnetInfoAction(workflows.Action): mask=True) ip_version = forms.ChoiceField(choices=[(4, 'IPv4'), (6, 'IPv6')], label=_("IP Version")) - gateway_ip = fields.IPField(label=_("Gateway IP (optional)"), - required=False, - initial="", - help_text=_("IP address of Gateway " - "(e.g. 192.168.0.1)"), - version=fields.IPv4 | fields.IPv6, - mask=False) + gateway_ip = fields.IPField( + label=_("Gateway IP (optional)"), + required=False, + initial="", + help_text=_("IP address of Gateway (e.g. 192.168.0.254) " + "The default value is the first IP of the " + "network address (e.g. 192.168.0.1 for " + "192.168.0.0/24). " + "If you use the default, leave blank. " + "If you want to use no gateway, " + "check 'Disable Gateway' below."), + version=fields.IPv4 | fields.IPv6, + mask=False) + no_gateway = forms.BooleanField(label=_("Disable Gateway"), + initial=False, required=False) class Meta: name = ("Subnet") @@ -79,13 +94,12 @@ class CreateSubnetInfoAction(workflows.Action): 'specified. If you wish to create a network WITHOUT a ' 'subnet, uncheck the "Create Subnet" checkbox.') - def clean(self): - cleaned_data = super(CreateSubnetInfoAction, self).clean() - with_subnet = cleaned_data.get('with_subnet') + def _check_subnet_data(self, cleaned_data, is_create=True): cidr = cleaned_data.get('cidr') ip_version = int(cleaned_data.get('ip_version')) gateway_ip = cleaned_data.get('gateway_ip') - if with_subnet and not cidr: + no_gateway = cleaned_data.get('no_gateway') + if not cidr: msg = _('Specify "Network Address" or ' 'clear "Create Subnet" checkbox.') raise forms.ValidationError(msg) @@ -93,17 +107,126 @@ class CreateSubnetInfoAction(workflows.Action): if netaddr.IPNetwork(cidr).version is not ip_version: msg = _('Network Address and IP version are inconsistent.') raise forms.ValidationError(msg) - if gateway_ip: + if not no_gateway and gateway_ip: if netaddr.IPAddress(gateway_ip).version is not ip_version: msg = _('Gateway IP and IP version are inconsistent.') raise forms.ValidationError(msg) + if not is_create and not no_gateway and not gateway_ip: + msg = _('Specify IP address of gateway or ' + 'check "Disable Gateway".') + raise forms.ValidationError(msg) + + def clean(self): + cleaned_data = super(CreateSubnetInfoAction, self).clean() + with_subnet = cleaned_data.get('with_subnet') + if not with_subnet: + return cleaned_data + self._check_subnet_data(cleaned_data) return cleaned_data class CreateSubnetInfo(workflows.Step): action_class = CreateSubnetInfoAction contributes = ("with_subnet", "subnet_name", "cidr", - "ip_version", "gateway_ip") + "ip_version", "gateway_ip", "no_gateway") + + +class CreateSubnetDetailAction(workflows.Action): + enable_dhcp = forms.BooleanField(label=_("Enable DHCP"), + initial=True, required=False) + allocation_pools = forms.CharField( + widget=forms.Textarea(), + label=_("Allocation Pools"), + help_text=_("IP address allocation pools. Each entry is " + "<start_ip_address>,<end_ip_address> " + "(e.g., 192.168.1.100,192.168.1.120) " + "and one entry per line."), + required=False) + dns_nameservers = forms.CharField( + widget=forms.widgets.Textarea(), + label=_("DNS Name Servers"), + help_text=_("IP address list of DNS name servers for this subnet. " + "One entry per line."), + required=False) + host_routes = forms.CharField( + widget=forms.widgets.Textarea(), + label=_("Host Routes"), + help_text=_("Additional routes announced to the hosts. " + "Each entry is <destination_cidr>,<nexthop> " + "(e.g., 192.168.200.0/24,10.56.1.254)" + "and one entry per line."), + required=False) + + class Meta: + name = ("Subnet Detail") + help_text = _('You can specify additional attributes for the subnet.') + + def _convert_ip_address(self, ip, field_name): + try: + return netaddr.IPAddress(ip) + except (netaddr.AddrFormatError, ValueError): + msg = _('%(field_name)s: Invalid IP address ' + '(value=%(ip)s)') % locals() + raise forms.ValidationError(msg) + + def _convert_ip_network(self, network, field_name): + try: + return netaddr.IPNetwork(network) + except (netaddr.AddrFormatError, ValueError): + msg = _('%(field_name)s: Invalid IP address ' + '(value=%(network)s)') % locals() + raise forms.ValidationError(msg) + + def _check_allocation_pools(self, allocation_pools): + for p in allocation_pools.split('\n'): + p = p.strip() + if not p: + continue + pool = p.split(',') + if len(pool) != 2: + msg = _('Start and end addresses must be specified ' + '(value=%s)') % p + raise forms.ValidationError(msg) + start, end = [self._convert_ip_address(ip, "allocation_pools") + for ip in pool] + if start > end: + msg = _('Start address is larger than end address ' + '(value=%s)') % p + raise forms.ValidationError(msg) + + def _check_dns_nameservers(self, dns_nameservers): + for ns in dns_nameservers.split('\n'): + ns = ns.strip() + if not ns: + continue + self._convert_ip_address(ns, "dns_nameservers") + + def _check_host_routes(self, host_routes): + for r in host_routes.split('\n'): + r = r.strip() + if not r: + continue + route = r.split(',') + if len(route) != 2: + msg = _('Host Routes format error: ' + 'Destination CIDR and nexthop must be specified ' + '(value=%s)') % r + raise forms.ValidationError(msg) + dest = self._convert_ip_network(route[0], "host_routes") + nexthop = self._convert_ip_address(route[1], "host_routes") + + def clean(self): + cleaned_data = super(CreateSubnetDetailAction, self).clean() + self._check_allocation_pools(cleaned_data.get('allocation_pools')) + self._check_host_routes(cleaned_data.get('host_routes')) + self._check_dns_nameservers(cleaned_data.get('dns_nameservers')) + return cleaned_data + + +class CreateSubnetDetail(workflows.Step): + action_class = CreateSubnetDetailAction + contributes = ("enable_dhcp", "allocation_pools", + "dns_nameservers", "host_routes") class CreateNetwork(workflows.Workflow): @@ -112,51 +235,130 @@ class CreateNetwork(workflows.Workflow): finalize_button_name = _("Create") success_message = _('Created network "%s".') failure_message = _('Unable to create network "%s".') - success_url = "horizon:project:networks:index" default_steps = (CreateNetworkInfo, - CreateSubnetInfo) + CreateSubnetInfo, + CreateSubnetDetail) + + def get_success_url(self): + return reverse("horizon:project:networks:index") + + def get_failure_url(self): + return reverse("horizon:project:networks:index") def format_status_message(self, message): name = self.context.get('net_name') or self.context.get('net_id', '') return message % name - def handle(self, request, data): - # create the network + def _create_network(self, request, data): try: - network = api.quantum.network_create(request, - name=data['net_name']) + params = {'name': data['net_name'], + 'admin_state_up': data['admin_state']} + network = api.quantum.network_create(request, **params) network.set_id_as_name_if_empty() self.context['net_id'] = network.id msg = _('Network "%s" was successfully created.') % network.name LOG.debug(msg) - except: - msg = _('Failed to create network "%s".') % data['net_name'] + return network + except Exception as e: + msg = (_('Failed to create network "%(network)s": %(reason)s') % + {"network": data['net_name'], "reason": e}) LOG.info(msg) - redirect = reverse('horizon:project:networks:index') + redirect = self.get_failure_url() exceptions.handle(request, msg, redirect=redirect) return False - # If we do not need to create a subnet, return here. - if not data['with_subnet']: - return True + def _setup_subnet_parameters(self, params, data, is_create=True): + """Setup subnet parameters - # Create the subnet. + This methods setups subnet parameters which are available + in both create and update. + """ + is_update = not is_create + params['enable_dhcp'] = data['enable_dhcp'] + if is_create and data['allocation_pools']: + pools = [dict(zip(['start', 'end'], pool.strip().split(','))) + for pool in data['allocation_pools'].split('\n') + if pool.strip()] + params['allocation_pools'] = pools + if data['host_routes'] or is_update: + routes = [dict(zip(['destination', 'nexthop'], + route.strip().split(','))) + for route in data['host_routes'].split('\n') + if route.strip()] + params['host_routes'] = routes + if data['dns_nameservers'] or is_update: + nameservers = [ns.strip() + for ns in data['dns_nameservers'].split('\n') + if ns.strip()] + params['dns_nameservers'] = nameservers + + def _create_subnet(self, request, data, network=None, tenant_id=None, + no_redirect=False): + if network: + network_id = network.id + network_name = network.name + else: + network_id = self.context.get('network_id') + network_name = self.context.get('network_name') try: - params = {'network_id': network.id, + params = {'network_id': network_id, 'name': data['subnet_name'], 'cidr': data['cidr'], 'ip_version': int(data['ip_version'])} - if data['gateway_ip']: + if tenant_id: + params['tenant_id'] = tenant_id + if data['no_gateway']: + params['gateway_ip'] = None + elif data['gateway_ip']: params['gateway_ip'] = data['gateway_ip'] - api.quantum.subnet_create(request, **params) + + self._setup_subnet_parameters(params, data) + + subnet = api.quantum.subnet_create(request, **params) + self.context['subnet_id'] = subnet.id msg = _('Subnet "%s" was successfully created.') % data['cidr'] LOG.debug(msg) - except Exception: - msg = _('Failed to create subnet "%(sub)s" for network "%(net)s".') - redirect = reverse('horizon:project:networks:index') + return subnet + except Exception as e: + msg = _('Failed to create subnet "%(sub)s" for network "%(net)s": ' + ' %(reason)s') + if no_redirect: + redirect = None + else: + redirect = self.get_failure_url() exceptions.handle(request, - msg % {"sub": data['cidr'], "net": network.id}, + msg % {"sub": data['cidr'], "net": network_name, + "reason": e}, redirect=redirect) return False - return True + def _delete_network(self, request, network): + """Delete the created network when subnet creation failed""" + try: + api.quantum.network_delete(request, network.id) + msg = _('Delete the created network "%s" ' + 'due to subnet creation failure.') % network.name + LOG.debug(msg) + redirect = self.get_failure_url() + messages.info(request, msg) + raise exceptions.Http302(redirect) + #return exceptions.RecoverableError + except: + msg = _('Failed to delete network "%s"') % network.name + LOG.info(msg) + redirect = self.get_failure_url() + exceptions.handle(request, msg, redirect=redirect) + + def handle(self, request, data): + network = self._create_network(request, data) + if not network: + return False + # If we do not need to create a subnet, return here. + if not data['with_subnet']: + return True + subnet = self._create_subnet(request, data, network, no_redirect=True) + if subnet: + return True + else: + self._delete_network(request, network) + return False diff --git a/openstack_dashboard/test/test_data/quantum_data.py b/openstack_dashboard/test/test_data/quantum_data.py index 23b896fd9..23e138127 100644 --- a/openstack_dashboard/test/test_data/quantum_data.py +++ b/openstack_dashboard/test/test_data/quantum_data.py @@ -37,9 +37,12 @@ def data(TEST): 'status': 'ACTIVE', 'subnets': ['e8abc972-eb0c-41f1-9edd-4bc6e3bcd8c9'], 'tenant_id': '1', + 'router:external': False, 'shared': False} subnet_dict = {'allocation_pools': [{'end': '10.0.0.254', 'start': '10.0.0.2'}], + 'dns_nameservers': [], + 'host_routes': [], 'cidr': '10.0.0.0/24', 'enable_dhcp': True, 'gateway_ip': '10.0.0.1', @@ -50,6 +53,7 @@ def data(TEST): 'tenant_id': network_dict['tenant_id']} port_dict = {'admin_state_up': True, 'device_id': 'af75c8e5-a1cc-4567-8d04-44fcd6922890', + 'device_owner': 'network:dhcp', 'fixed_ips': [{'ip_address': '10.0.0.3', 'subnet_id': subnet_dict['id']}], 'id': '3ec7f3db-cb2f-4a34-ab6b-69a64d3f008c', @@ -77,9 +81,15 @@ def data(TEST): 'status': 'ACTIVE', 'subnets': ['3f7c5d79-ee55-47b0-9213-8e669fb03009'], 'tenant_id': '2', + 'router:external': True, 'shared': True} subnet_dict = {'allocation_pools': [{'end': '172.16.88.254', - 'start': '172.16.88.2'}], + 'start': '172.16.88.2'}], + 'dns_nameservers': ['10.56.1.20', '10.56.1.21'], + 'host_routes': [{'destination': '192.168.20.0/24', + 'nexthop': '172.16.88.253'}, + {'destination': '192.168.21.0/24', + 'nexthop': '172.16.88.252'}], 'cidr': '172.16.88.0/24', 'enable_dhcp': True, 'gateway_ip': '172.16.88.1', @@ -90,6 +100,7 @@ def data(TEST): 'tenant_id': network_dict['tenant_id']} port_dict = {'admin_state_up': True, 'device_id': '40e536b1-b9fd-4eb7-82d6-84db5d65a2ac', + 'device_owner': 'compute:nova', 'fixed_ips': [{'ip_address': '172.16.88.3', 'subnet_id': subnet_dict['id']}], 'id': '7e6ce62c-7ea2-44f8-b6b4-769af90a8406',