From 88d78a489b6e83d8f113a6ada7a993edeeec837b Mon Sep 17 00:00:00 2001 From: "wei.ying" Date: Fri, 2 Feb 2018 17:35:34 +0800 Subject: [PATCH] Refactor the create port form to workflow The neutron API allows the users to specify security group(s) when creating a port [1], but at the moment the Horizon can only modify the security group of a port by 'Edit Port' row action. As with edit port workflow, refactoring the creation of port form to workflow is to support the creation of ports as well as the assignment of security groups. Since the change may be more, this patch only contains reconstruct the form, support the security groups will be submitted in another patch. [1] https://developer.openstack.org/api-ref/network/v2/index.html#create-port Change-Id: Ie7e5242cc885b6eb0316f695453b0d2844360d67 Related-bug: #1746985 --- .../dashboards/admin/networks/ports/forms.py | 82 ------- .../dashboards/admin/networks/ports/tests.py | 24 +- .../dashboards/admin/networks/ports/views.py | 47 +--- .../admin/networks/ports/workflows.py | 35 +++ .../templates/networks/ports/_create.html | 7 - .../templates/networks/ports/create.html | 7 - .../project/networks/ports/forms.py | 220 ------------------ .../project/networks/ports/tests.py | 3 +- .../project/networks/ports/views.py | 32 +-- .../project/networks/ports/workflows.py | 209 +++++++++++++++++ .../templates/networks/ports/_create.html | 11 - .../networks/ports/_create_port_help.html | 8 + .../templates/networks/ports/create.html | 7 - 13 files changed, 265 insertions(+), 427 deletions(-) delete mode 100644 openstack_dashboard/dashboards/admin/networks/ports/forms.py delete mode 100644 openstack_dashboard/dashboards/admin/networks/templates/networks/ports/_create.html delete mode 100644 openstack_dashboard/dashboards/admin/networks/templates/networks/ports/create.html delete mode 100644 openstack_dashboard/dashboards/project/networks/ports/forms.py delete mode 100644 openstack_dashboard/dashboards/project/networks/templates/networks/ports/_create.html create mode 100644 openstack_dashboard/dashboards/project/networks/templates/networks/ports/_create_port_help.html delete mode 100644 openstack_dashboard/dashboards/project/networks/templates/networks/ports/create.html diff --git a/openstack_dashboard/dashboards/admin/networks/ports/forms.py b/openstack_dashboard/dashboards/admin/networks/ports/forms.py deleted file mode 100644 index f1daea4c3e..0000000000 --- a/openstack_dashboard/dashboards/admin/networks/ports/forms.py +++ /dev/null @@ -1,82 +0,0 @@ -# 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 -from openstack_dashboard.dashboards.project.networks.ports \ - import forms as project_forms - - -LOG = logging.getLogger(__name__) - - -class CreatePort(project_forms.CreatePort): - binding__host_id = forms.CharField( - label=_("Binding: Host"), - help_text=_("The ID of the host where the port is allocated. In some " - "cases, different implementations can run on different " - "hosts."), - required=False) - failure_url = 'horizon:admin:networks:detail' - - def handle(self, request, data): - network_id = self.initial['network_id'] - 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.neutron.network_get(request, network_id) - params = { - 'tenant_id': network.tenant_id, - 'network_id': network_id, - 'admin_state_up': data['admin_state'], - 'name': data['name'], - 'device_id': data['device_id'], - 'device_owner': data['device_owner'], - 'binding__host_id': data['binding__host_id'] - } - - if data.get('specify_ip') == 'subnet_id': - if data.get('subnet_id'): - params['fixed_ips'] = [{"subnet_id": data['subnet_id']}] - elif data.get('specify_ip') == 'fixed_ip': - if data.get('fixed_ip'): - params['fixed_ips'] = [{"ip_address": data['fixed_ip']}] - - if data.get('binding__vnic_type'): - params['binding__vnic_type'] = data['binding__vnic_type'] - - if data.get('mac_state'): - params['mac_learning_enabled'] = data['mac_state'] - - if 'port_security_enabled' in data: - params['port_security_enabled'] = data['port_security_enabled'] - - port = api.neutron.port_create(request, **params) - msg = _('Port %s was successfully created.') % port['id'] - messages.success(request, msg) - return port - except Exception as e: - LOG.info('Failed to create a port for network %(id)s: %(exc)s', - {'id': network_id, 'exc': e}) - msg = _('Failed to create a port for network %s') % network_id - redirect = reverse(self.failure_url, args=(network_id,)) - exceptions.handle(request, msg, redirect=redirect) diff --git a/openstack_dashboard/dashboards/admin/networks/ports/tests.py b/openstack_dashboard/dashboards/admin/networks/ports/tests.py index 3c8d082f27..6789fa2a62 100644 --- a/openstack_dashboard/dashboards/admin/networks/ports/tests.py +++ b/openstack_dashboard/dashboards/admin/networks/ports/tests.py @@ -113,7 +113,7 @@ class NetworkPortTests(test.BaseAdminViewTests): args=[network.id]) res = self.client.get(url) - self.assertTemplateUsed(res, 'admin/networks/ports/create.html') + self.assertTemplateUsed(res, views.WorkflowView.template_name) @test.create_stubs({api.neutron: ('network_get', 'is_extension_supported', @@ -137,12 +137,6 @@ class NetworkPortTests(test.BaseAdminViewTests): port_security=False): network = self.networks.first() port = self.ports.first() - api.neutron.network_get(IsA(http.HttpRequest), - network.id)\ - .AndReturn(self.networks.first()) - api.neutron.network_get(IsA(http.HttpRequest), - network.id)\ - .AndReturn(self.networks.first()) api.neutron.network_get(IsA(http.HttpRequest), network.id)\ .AndReturn(self.networks.first()) @@ -205,14 +199,8 @@ class NetworkPortTests(test.BaseAdminViewTests): network = self.networks.first() port = self.ports.first() api.neutron.network_get(IsA(http.HttpRequest), - network.id)\ - .AndReturn(self.networks.first()) - api.neutron.network_get(IsA(http.HttpRequest), - network.id)\ - .AndReturn(self.networks.first()) - api.neutron.network_get(IsA(http.HttpRequest), - network.id)\ - .AndReturn(self.networks.first()) + network.id) \ + .MultipleTimes().AndReturn(self.networks.first()) api.neutron.is_extension_supported(IsA(http.HttpRequest), 'mac-learning')\ .AndReturn(True) @@ -280,12 +268,6 @@ class NetworkPortTests(test.BaseAdminViewTests): port_security=False): network = self.networks.first() port = self.ports.first() - api.neutron.network_get(IsA(http.HttpRequest), - network.id)\ - .AndReturn(self.networks.first()) - api.neutron.network_get(IsA(http.HttpRequest), - network.id)\ - .AndReturn(self.networks.first()) api.neutron.network_get(IsA(http.HttpRequest), network.id)\ .AndReturn(self.networks.first()) diff --git a/openstack_dashboard/dashboards/admin/networks/ports/views.py b/openstack_dashboard/dashboards/admin/networks/ports/views.py index 821931c76d..21a81bcbc5 100644 --- a/openstack_dashboard/dashboards/admin/networks/ports/views.py +++ b/openstack_dashboard/dashboards/admin/networks/ports/views.py @@ -15,14 +15,6 @@ from django.core.urlresolvers import reverse from django.utils.translation import ugettext_lazy as _ -from horizon import exceptions -from horizon import forms -from horizon.utils import memoized - -from openstack_dashboard import api - -from openstack_dashboard.dashboards.admin.networks.ports \ - import forms as ports_forms from openstack_dashboard.dashboards.admin.networks.ports \ import tables as ports_tables from openstack_dashboard.dashboards.admin.networks.ports \ @@ -33,42 +25,9 @@ from openstack_dashboard.dashboards.project.networks.ports \ import views as project_views -class CreateView(forms.ModalFormView): - form_class = ports_forms.CreatePort - form_id = "create_port_form" - submit_label = _("Create Port") - submit_url = "horizon:admin:networks:addport" - page_title = _("Create Port") - template_name = 'admin/networks/ports/create.html' - url = 'horizon:admin:networks:detail' - - def get_success_url(self): - return reverse(self.url, - args=(self.kwargs['network_id'],)) - - @memoized.memoized_method - def get_object(self): - try: - network_id = self.kwargs["network_id"] - return api.neutron.network_get(self.request, network_id) - except Exception: - redirect = reverse(self.url, - args=(self.kwargs['network_id'],)) - msg = _("Unable to retrieve network.") - exceptions.handle(self.request, msg, redirect=redirect) - - def get_context_data(self, **kwargs): - context = super(CreateView, self).get_context_data(**kwargs) - context['network'] = self.get_object() - args = (self.kwargs['network_id'],) - context['submit_url'] = reverse(self.submit_url, args=args) - context['cancel_url'] = reverse(self.url, args=args) - return context - - def get_initial(self): - network = self.get_object() - return {"network_id": self.kwargs['network_id'], - "network_name": network.name} +class CreateView(project_views.CreateView): + workflow_class = admin_workflows.CreatePort + failure_url = 'horizon:admin:networks:detail' class DetailView(project_views.DetailView): diff --git a/openstack_dashboard/dashboards/admin/networks/ports/workflows.py b/openstack_dashboard/dashboards/admin/networks/ports/workflows.py index dd70738a16..70beccd971 100644 --- a/openstack_dashboard/dashboards/admin/networks/ports/workflows.py +++ b/openstack_dashboard/dashboards/admin/networks/ports/workflows.py @@ -27,6 +27,41 @@ from openstack_dashboard.dashboards.project.networks.ports \ LOG = logging.getLogger(__name__) +class CreatePortInfoAction(project_workflow.CreatePortInfoAction): + binding__host_id = forms.CharField( + label=_("Binding: Host"), + help_text=_("The ID of the host where the port is allocated. In some " + "cases, different implementations can run on different " + "hosts."), + required=False) + + class Meta(object): + name = _("Info") + slug = 'create_info' + help_text_template = 'project/networks/ports/_create_port_help.html' + + +class CreatePortInfo(project_workflow.CreatePortInfo): + action_class = CreatePortInfoAction + depends_on = ("network_id", "target_tenant_id") + contributes = (project_workflow.CreatePortInfo.contributes + + ['binding__host_id']) + + +class CreatePort(project_workflow.CreatePort): + default_steps = (CreatePortInfo,) + + def get_success_url(self): + return reverse("horizon:admin:networks:detail", + args=(self.context['network_id'],)) + + def _construct_parameters(self, context): + params = super(CreatePort, self)._construct_parameters(context) + params.update({'tenant_id': context['target_tenant_id'], + 'binding__host_id': context['binding__host_id']}) + return params + + class UpdatePortInfoAction(project_workflow.UpdatePortInfoAction): device_id = forms.CharField( max_length=100, label=_("Device ID"), diff --git a/openstack_dashboard/dashboards/admin/networks/templates/networks/ports/_create.html b/openstack_dashboard/dashboards/admin/networks/templates/networks/ports/_create.html deleted file mode 100644 index 3260ae88a7..0000000000 --- a/openstack_dashboard/dashboards/admin/networks/templates/networks/ports/_create.html +++ /dev/null @@ -1,7 +0,0 @@ -{% extends "horizon/common/_modal_form.html" %} -{% load i18n %} - -{% block modal-body-right %} -

{% trans "Description:" %}

-

{% trans "You can create a port for the network. If you specify device ID to be attached, the device specified will be attached to the port created."%}

-{% endblock %} diff --git a/openstack_dashboard/dashboards/admin/networks/templates/networks/ports/create.html b/openstack_dashboard/dashboards/admin/networks/templates/networks/ports/create.html deleted file mode 100644 index 7dbc4e7c78..0000000000 --- a/openstack_dashboard/dashboards/admin/networks/templates/networks/ports/create.html +++ /dev/null @@ -1,7 +0,0 @@ -{% extends 'base.html' %} -{% load i18n %} -{% block title %}{% trans "Create Port" %}{% endblock %} - -{% block main %} - {% include "admin/networks/ports/_create.html" %} -{% endblock %} diff --git a/openstack_dashboard/dashboards/project/networks/ports/forms.py b/openstack_dashboard/dashboards/project/networks/ports/forms.py deleted file mode 100644 index e253e107cf..0000000000 --- a/openstack_dashboard/dashboards/project/networks/ports/forms.py +++ /dev/null @@ -1,220 +0,0 @@ -# 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.conf import settings -from django.core.urlresolvers import reverse -from django.utils.translation import ugettext_lazy as _ - -from neutronclient.common import exceptions as neutron_exc - -from horizon import exceptions -from horizon import forms -from horizon import messages - -from openstack_dashboard import api - - -LOG = logging.getLogger(__name__) - - -class CreatePort(forms.SelfHandlingForm): - name = forms.CharField(max_length=255, - label=_("Name"), - required=False) - admin_state = forms.BooleanField(label=_("Enable 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=_("Owner of the device attached to the port"), - required=False) - specify_ip = forms.ThemableChoiceField( - label=_("Specify IP address or subnet"), - help_text=_("To specify a subnet or a fixed IP, select any options."), - required=False, - choices=[('', _("Unspecified")), - ('subnet_id', _("Subnet")), - ('fixed_ip', _("Fixed IP Address"))], - widget=forms.ThemableSelectWidget(attrs={ - 'class': 'switchable', - 'data-slug': 'specify_ip', - })) - subnet_id = forms.ThemableChoiceField( - label=_("Subnet"), - required=False, - widget=forms.ThemableSelectWidget(attrs={ - 'class': 'switched', - 'data-switch-on': 'specify_ip', - 'data-specify_ip-subnet_id': _('Subnet'), - })) - fixed_ip = forms.IPField( - label=_("Fixed IP Address"), - required=False, - help_text=_("Specify the subnet IP address for the new port"), - version=forms.IPv4 | forms.IPv6, - widget=forms.TextInput(attrs={ - 'class': 'switched', - 'data-switch-on': 'specify_ip', - 'data-specify_ip-fixed_ip': _('Fixed IP Address'), - })) - mac_address = forms.MACAddressField( - label=_("MAC Address"), - required=False, - help_text=_("Specify the MAC address for the new port")) - mac_state = forms.BooleanField( - label=_("MAC Learning State"), initial=False, - required=False) - port_security_enabled = forms.BooleanField( - label=_("Port Security"), - help_text=_("Enable anti-spoofing rules for the port"), - initial=True, - required=False) - binding__vnic_type = forms.ThemableChoiceField( - label=_("VNIC Type"), - help_text=_("The VNIC type that is bound to the network port"), - required=False) - - failure_url = 'horizon:project:networks:detail' - - def __init__(self, request, *args, **kwargs): - super(CreatePort, self).__init__(request, *args, **kwargs) - - # prepare subnet choices and input area for each subnet - subnet_choices = self._get_subnet_choices(kwargs['initial']) - if subnet_choices: - subnet_choices.insert(0, ('', _("Select a subnet"))) - self.fields['subnet_id'].choices = subnet_choices - else: - self.fields['specify_ip'].widget = forms.HiddenInput() - self.fields['subnet_id'].widget = forms.HiddenInput() - self.fields['fixed_ip'].widget = forms.HiddenInput() - - self._hide_field_if_not_supported( - request, 'mac_state', 'mac-learning', - _("Unable to retrieve MAC learning state")) - self._hide_field_if_not_supported( - request, 'port_security_enabled', 'port-security', - _("Unable to retrieve port security state")) - - self._populate_vnic_type_choices(request) - - def _hide_field_if_not_supported(self, request, field, extension_alias, - failure_message): - is_supproted = False - try: - is_supproted = api.neutron.is_extension_supported( - request, extension_alias) - except Exception: - exceptions.handle(self.request, failure_message) - if not is_supproted: - del self.fields[field] - return is_supproted - - def _populate_vnic_type_choices(self, request): - neutron_settings = getattr(settings, 'OPENSTACK_NEUTRON_NETWORK', {}) - supported_vnic_types = neutron_settings.get('supported_vnic_types', - ['*']) - # When a list of VNIC types is empty, hide the corresponding field. - if not supported_vnic_types: - del self.fields['binding__vnic_type'] - return - - binding_supported = self._hide_field_if_not_supported( - request, 'binding__vnic_type', 'binding', - _("Unable to verify the VNIC types extension in Neutron")) - if not binding_supported: - # binding__vnic_type field is already deleted, so return here - return - - if supported_vnic_types == ['*']: - vnic_type_choices = api.neutron.VNIC_TYPES - else: - vnic_type_choices = [ - vnic_type for vnic_type in api.neutron.VNIC_TYPES - if vnic_type[0] in supported_vnic_types - ] - self.fields['binding__vnic_type'].choices = vnic_type_choices - - def _get_subnet_choices(self, kwargs): - try: - network_id = kwargs['network_id'] - network = api.neutron.network_get(self.request, network_id) - except Exception: - return [] - - # NOTE(amotoki): When a user cannot retrieve a subnet info, - # subnet ID is stored in network.subnets field. - # If so, we skip such subnet as subnet choices. - # This happens usually for external networks. - # TODO(amotoki): Ideally it is better to disable/hide - # Create Port button in the port table, but as of Pike - # the default neutron policy.json for "create_port" is empty - # and there seems no appropriate policy. This is a dirty hack. - return [(subnet.id, '%s %s' % (subnet.name_or_id, subnet.cidr)) - for subnet in network.subnets - if isinstance(subnet, api.neutron.Subnet)] - - def handle(self, request, data): - try: - params = { - 'network_id': self.initial['network_id'], - 'admin_state_up': data['admin_state'], - 'name': data['name'], - 'device_id': data['device_id'], - 'device_owner': data['device_owner'] - } - - if data.get('specify_ip') == 'subnet_id': - if data.get('subnet_id'): - params['fixed_ips'] = [{"subnet_id": data['subnet_id']}] - elif data.get('specify_ip') == 'fixed_ip': - if data.get('fixed_ip'): - params['fixed_ips'] = [{"ip_address": data['fixed_ip']}] - - if data.get('binding__vnic_type'): - params['binding__vnic_type'] = data['binding__vnic_type'] - if data.get('mac_state'): - params['mac_learning_enabled'] = data['mac_state'] - if 'port_security_enabled' in data: - params['port_security_enabled'] = data['port_security_enabled'] - - # Send mac_address only when it is specified. - if data['mac_address']: - params['mac_address'] = data['mac_address'] - - port = api.neutron.port_create(request, **params) - if port['name']: - msg = _('Port %s was successfully created.') % port['name'] - else: - msg = _('Port %s was successfully created.') % port['id'] - messages.success(request, msg) - return port - except Exception as e: - LOG.info('Failed to create a port for network %(id)s: %(exc)s', - {'id': self.initial['network_id'], 'exc': e}) - if isinstance(e, neutron_exc.Forbidden): - msg = (_('You are not allowed to create a port ' - 'for network %s.') - % self.initial['network_id']) - else: - msg = (_('Failed to create a port for network %s') - % self.initial['network_id']) - redirect = reverse(self.failure_url, - args=(self.initial['network_id'],)) - exceptions.handle(request, msg, redirect=redirect) diff --git a/openstack_dashboard/dashboards/project/networks/ports/tests.py b/openstack_dashboard/dashboards/project/networks/ports/tests.py index 8806db3ff0..06c764759b 100644 --- a/openstack_dashboard/dashboards/project/networks/ports/tests.py +++ b/openstack_dashboard/dashboards/project/networks/ports/tests.py @@ -392,7 +392,7 @@ class NetworkPortTests(test.TestCase): args=[network.id]) res = self.client.get(url) - self.assertTemplateUsed(res, 'project/networks/ports/create.html') + self.assertTemplateUsed(res, views.WorkflowView.template_name) @test.create_stubs({api.neutron: ('network_get', 'is_extension_supported', @@ -446,7 +446,6 @@ class NetworkPortTests(test.TestCase): **extension_kwargs) \ .AndReturn(port) self.mox.ReplayAll() - form_data = {'network_id': port.network_id, 'network_name': network.name, 'name': port.name, diff --git a/openstack_dashboard/dashboards/project/networks/ports/views.py b/openstack_dashboard/dashboards/project/networks/ports/views.py index 92943fe73b..d41d8a9fdc 100644 --- a/openstack_dashboard/dashboards/project/networks/ports/views.py +++ b/openstack_dashboard/dashboards/project/networks/ports/views.py @@ -16,15 +16,12 @@ 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 horizon.utils import memoized from horizon import workflows from openstack_dashboard import api -from openstack_dashboard.dashboards.project.networks.ports \ - import forms as project_forms from openstack_dashboard.dashboards.project.networks.ports \ import tables as project_tables from openstack_dashboard.dashboards.project.networks.ports \ @@ -38,19 +35,9 @@ STATUS_DICT = dict(project_tables.STATUS_DISPLAY_CHOICES) VNIC_TYPE_DICT = dict(api.neutron.VNIC_TYPES) -class CreateView(forms.ModalFormView): - form_class = project_forms.CreatePort - form_id = "create_port_form" - modal_header = _("Create Port") - submit_label = _("Create Port") - submit_url = "horizon:project:networks:addport" - page_title = _("Create Port") - template_name = 'project/networks/ports/create.html' - url = 'horizon:project:networks:detail' - - def get_success_url(self): - return reverse(self.url, - args=(self.kwargs['network_id'],)) +class CreateView(workflows.WorkflowView): + workflow_class = project_workflows.CreatePort + failure_url = 'horizon:project:networks:detail' @memoized.memoized_method def get_network(self): @@ -58,23 +45,16 @@ class CreateView(forms.ModalFormView): network_id = self.kwargs["network_id"] return api.neutron.network_get(self.request, network_id) except Exception: - redirect = reverse(self.url, + redirect = reverse(self.failure_url, args=(self.kwargs['network_id'],)) msg = _("Unable to retrieve network.") exceptions.handle(self.request, msg, redirect=redirect) - def get_context_data(self, **kwargs): - context = super(CreateView, self).get_context_data(**kwargs) - context['network'] = self.get_network() - args = (self.kwargs['network_id'],) - context['submit_url'] = reverse(self.submit_url, args=args) - context['cancel_url'] = reverse(self.url, args=args) - return context - def get_initial(self): network = self.get_network() return {"network_id": self.kwargs['network_id'], - "network_name": network.name} + "network_name": network.name, + "target_tenant_id": network.tenant_id} class DetailView(tabs.TabbedTableView): diff --git a/openstack_dashboard/dashboards/project/networks/ports/workflows.py b/openstack_dashboard/dashboards/project/networks/ports/workflows.py index 9d47e2c269..416bad940c 100644 --- a/openstack_dashboard/dashboards/project/networks/ports/workflows.py +++ b/openstack_dashboard/dashboards/project/networks/ports/workflows.py @@ -32,6 +32,215 @@ from openstack_dashboard.utils import filters LOG = logging.getLogger(__name__) +class CreatePortInfoAction(workflows.Action): + name = forms.CharField(max_length=255, + label=_("Name"), + required=False) + admin_state = forms.BooleanField(label=_("Enable 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=_("Owner of the device attached to the port"), + required=False) + specify_ip = forms.ThemableChoiceField( + label=_("Specify IP address or subnet"), + help_text=_("To specify a subnet or a fixed IP, select any options."), + required=False, + choices=[('', _("Unspecified")), + ('subnet_id', _("Subnet")), + ('fixed_ip', _("Fixed IP Address"))], + widget=forms.ThemableSelectWidget(attrs={ + 'class': 'switchable', + 'data-slug': 'specify_ip', + })) + subnet_id = forms.ThemableChoiceField( + label=_("Subnet"), + required=False, + widget=forms.ThemableSelectWidget(attrs={ + 'class': 'switched', + 'data-switch-on': 'specify_ip', + 'data-specify_ip-subnet_id': _('Subnet'), + })) + fixed_ip = forms.IPField( + label=_("Fixed IP Address"), + required=False, + help_text=_("Specify the subnet IP address for the new port"), + version=forms.IPv4 | forms.IPv6, + widget=forms.TextInput(attrs={ + 'class': 'switched', + 'data-switch-on': 'specify_ip', + 'data-specify_ip-fixed_ip': _('Fixed IP Address'), + })) + mac_address = forms.MACAddressField( + label=_("MAC Address"), + required=False, + help_text=_("Specify the MAC address for the new port")) + mac_state = forms.BooleanField( + label=_("MAC Learning State"), initial=False, + required=False) + port_security_enabled = forms.BooleanField( + label=_("Port Security"), + help_text=_("Enable anti-spoofing rules for the port"), + initial=True, + required=False) + binding__vnic_type = forms.ThemableChoiceField( + label=_("VNIC Type"), + help_text=_("The VNIC type that is bound to the network port"), + required=False) + + def __init__(self, request, context, *args, **kwargs): + super(CreatePortInfoAction, self).__init__( + request, context, *args, **kwargs) + + # prepare subnet choices and input area for each subnet + subnet_choices = self._get_subnet_choices(context) + if subnet_choices: + subnet_choices.insert(0, ('', _("Select a subnet"))) + self.fields['subnet_id'].choices = subnet_choices + else: + self.fields['specify_ip'].widget = forms.HiddenInput() + self.fields['subnet_id'].widget = forms.HiddenInput() + self.fields['fixed_ip'].widget = forms.HiddenInput() + + self._hide_field_if_not_supported( + request, 'mac_state', 'mac-learning', + _("Unable to retrieve MAC learning state")) + self._hide_field_if_not_supported( + request, 'port_security_enabled', 'port-security', + _("Unable to retrieve port security state")) + + self._populate_vnic_type_choices(request) + + def _hide_field_if_not_supported(self, request, field, extension_alias, + failure_message): + is_supproted = False + try: + is_supproted = api.neutron.is_extension_supported( + request, extension_alias) + except Exception: + exceptions.handle(self.request, failure_message) + if not is_supproted: + del self.fields[field] + return is_supproted + + def _populate_vnic_type_choices(self, request): + neutron_settings = getattr(settings, 'OPENSTACK_NEUTRON_NETWORK', {}) + supported_vnic_types = neutron_settings.get('supported_vnic_types', + ['*']) + # When a list of VNIC types is empty, hide the corresponding field. + if not supported_vnic_types: + del self.fields['binding__vnic_type'] + return + + binding_supported = self._hide_field_if_not_supported( + request, 'binding__vnic_type', 'binding', + _("Unable to verify the VNIC types extension in Neutron")) + if not binding_supported: + # binding__vnic_type field is already deleted, so return here + return + + if supported_vnic_types == ['*']: + vnic_type_choices = api.neutron.VNIC_TYPES + else: + vnic_type_choices = [ + vnic_type for vnic_type in api.neutron.VNIC_TYPES + if vnic_type[0] in supported_vnic_types + ] + self.fields['binding__vnic_type'].choices = vnic_type_choices + + def _get_subnet_choices(self, context): + try: + network_id = context['network_id'] + network = api.neutron.network_get(self.request, network_id) + except Exception: + return [] + + # NOTE(amotoki): When a user cannot retrieve a subnet info, + # subnet ID is stored in network.subnets field. + # If so, we skip such subnet as subnet choices. + # This happens usually for external networks. + # TODO(amotoki): Ideally it is better to disable/hide + # Create Port button in the port table, but as of Pike + # the default neutron policy.json for "create_port" is empty + # and there seems no appropriate policy. This is a dirty hack. + return [(subnet.id, '%s %s' % (subnet.name_or_id, subnet.cidr)) + for subnet in network.subnets + if isinstance(subnet, api.neutron.Subnet)] + + class Meta(object): + name = _("Info") + slug = 'create_info' + help_text_template = 'project/networks/ports/_create_port_help.html' + + +class CreatePortInfo(workflows.Step): + action_class = CreatePortInfoAction + depends_on = ("network_id",) + contributes = ["name", "admin_state", "device_id", "device_owner", + "specify_ip", "subnet_id", "fixed_ip", "mac_address", + "mac_state", "port_security_enabled", "binding__vnic_type"] + + +class CreatePort(workflows.Workflow): + slug = "create_port" + name = _("Create Port") + finalize_button_name = _("Create") + success_message = _('Port %s was successfully created.') + failure_message = _('Failed to create port "%s".') + default_steps = (CreatePortInfo,) + + def get_success_url(self): + return reverse("horizon:project:networks:detail", + args=(self.context['network_id'],)) + + def format_status_message(self, message): + name = self.context['name'] or self.context.get('port_id', '') + return message % name + + def handle(self, request, context): + try: + params = self._construct_parameters(context) + port = api.neutron.port_create(request, **params) + self.context['port_id'] = port.id + return True + except Exception as e: + LOG.info('Failed to create a port for network %(id)s: %(exc)s', + {'id': context['network_id'], 'exc': e}) + + def _construct_parameters(self, context): + params = { + 'network_id': context['network_id'], + 'admin_state_up': context['admin_state'], + 'name': context['name'], + 'device_id': context['device_id'], + 'device_owner': context['device_owner'] + } + + if context.get('specify_ip') == 'subnet_id': + if context.get('subnet_id'): + params['fixed_ips'] = [{"subnet_id": context['subnet_id']}] + elif context.get('specify_ip') == 'fixed_ip': + if context.get('fixed_ip'): + params['fixed_ips'] = [{"ip_address": context['fixed_ip']}] + + if context.get('binding__vnic_type'): + params['binding__vnic_type'] = context['binding__vnic_type'] + if context.get('mac_state'): + params['mac_learning_enabled'] = context['mac_state'] + if 'port_security_enabled' in context: + params['port_security_enabled'] = context['port_security_enabled'] + + # Send mac_address only when it is specified. + if context['mac_address']: + params['mac_address'] = context['mac_address'] + + return params + + class UpdatePortInfoAction(workflows.Action): name = forms.CharField(max_length=255, label=_("Name"), diff --git a/openstack_dashboard/dashboards/project/networks/templates/networks/ports/_create.html b/openstack_dashboard/dashboards/project/networks/templates/networks/ports/_create.html deleted file mode 100644 index e0d239051c..0000000000 --- a/openstack_dashboard/dashboards/project/networks/templates/networks/ports/_create.html +++ /dev/null @@ -1,11 +0,0 @@ -{% extends "horizon/common/_modal_form.html" %} -{% load i18n %} - -{% block modal-body-right %} -

{% trans "Description:" %}

-

{% blocktrans trimmed %} You can create a port for the network. - If you specify device ID to be attached, the device specified will - be attached to the port created. - {% endblocktrans %} -

-{% endblock %} diff --git a/openstack_dashboard/dashboards/project/networks/templates/networks/ports/_create_port_help.html b/openstack_dashboard/dashboards/project/networks/templates/networks/ports/_create_port_help.html new file mode 100644 index 0000000000..ac600c4d4e --- /dev/null +++ b/openstack_dashboard/dashboards/project/networks/templates/networks/ports/_create_port_help.html @@ -0,0 +1,8 @@ +{% load i18n %} + +

{% trans "Description:" %}

+

{% blocktrans trimmed %} You can create a port for the network. + If you specify device ID to be attached, the device specified will + be attached to the port created. + {% endblocktrans %} +

diff --git a/openstack_dashboard/dashboards/project/networks/templates/networks/ports/create.html b/openstack_dashboard/dashboards/project/networks/templates/networks/ports/create.html deleted file mode 100644 index 922625d1aa..0000000000 --- a/openstack_dashboard/dashboards/project/networks/templates/networks/ports/create.html +++ /dev/null @@ -1,7 +0,0 @@ -{% extends 'base.html' %} -{% load i18n %} -{% block title %}{% trans "Create Port" %}{% endblock %} - -{% block main %} - {% include "project/networks/ports/_create.html" %} -{% endblock %}