Refactor the current UpdatePort form to workflow version

This patch change UpdatePort form to workflow in order to
be able to add extra tabs like the setting of security groups.
Originally, this work is a part of the subsequent patch
"Support security groups association per port",
but the changes are big so it is divided into two patches.
This patch focuses on refactoring the existing implementation
from the form to the workflow.

Change-Id: Id42b311242b66ee25e8870ed86e45b5464e19c01
Co-Authored-By: Akihiro Motoki <amotoki@gmail.com>
Related-bug: #1637444
This commit is contained in:
Kenji Ishii 2017-03-30 01:46:08 +00:00 committed by Akihiro Motoki
parent d66c96269d
commit 5aab8bf508
13 changed files with 264 additions and 239 deletions

View File

@ -53,6 +53,15 @@ ROUTER_INTERFACE_OWNERS = (
'network:ha_router_replicated_interface'
)
VNIC_TYPES = [
('normal', _('Normal')),
('direct', _('Direct')),
('direct-physical', _('Direct Physical')),
('macvtap', _('MacVTap')),
('baremetal', _('Bare Metal')),
('virtio-forwarder', _('Virtio Forwarder')),
]
class NeutronAPIDictWrapper(base.APIDictWrapper):

View File

@ -28,12 +28,6 @@ from openstack_dashboard.dashboards.project.networks.ports \
LOG = logging.getLogger(__name__)
VNIC_TYPES = [('normal', _('Normal')),
('direct', _('Direct')),
('direct-physical', _('Direct Physical')),
('macvtap', _('MacVTap')),
('baremetal', _('Bare Metal')),
('virtio-forwarder', _('Virtio Forwarder'))]
class CreatePort(project_forms.CreatePort):
@ -56,10 +50,10 @@ class CreatePort(project_forms.CreatePort):
'supported_vnic_types', ['*'])
if supported_vnic_types:
if supported_vnic_types == ['*']:
vnic_type_choices = VNIC_TYPES
vnic_type_choices = api.neutron.VNIC_TYPES
else:
vnic_type_choices = [
vnic_type for vnic_type in VNIC_TYPES
vnic_type for vnic_type in api.neutron.VNIC_TYPES
if vnic_type[0] in supported_vnic_types
]
@ -137,63 +131,3 @@ class CreatePort(project_forms.CreatePort):
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)
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)
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)
mac_address = forms.MACAddressField(
label=_("MAC Address"),
required=False,
help_text=_("Specify a new MAC address for the port"))
failure_url = 'horizon:admin:networks:detail'
def handle(self, request, data):
port_id = self.initial['port_id']
try:
LOG.debug('params = %s', data)
extension_kwargs = {}
if 'binding__vnic_type' in data:
extension_kwargs['binding__vnic_type'] = \
data['binding__vnic_type']
if 'mac_state' in data:
extension_kwargs['mac_learning_enabled'] = data['mac_state']
if 'port_security_enabled' in data:
extension_kwargs['port_security_enabled'] = \
data['port_security_enabled']
port = api.neutron.port_update(request,
port_id,
name=data['name'],
admin_state_up=data['admin_state'],
device_id=data['device_id'],
device_owner=data['device_owner'],
binding__host_id=data
['binding__host_id'],
mac_address=data['mac_address'],
**extension_kwargs)
msg = _('Port %s was successfully updated.') % port_id
messages.success(request, msg)
return port
except Exception as e:
LOG.info('Failed to update port %(id)s: %(exc)s',
{'id': port_id, 'exc': e})
msg = _('Failed to update port %s') % port_id
redirect = reverse(self.failure_url,
args=[self.initial['network_id']])
exceptions.handle(request, msg, redirect=redirect)

View File

@ -18,6 +18,8 @@ from django import http
from mox3.mox import IsA
from horizon.workflows import views
from openstack_dashboard import api
from openstack_dashboard.test import helpers as test
@ -375,7 +377,7 @@ class NetworkPortTests(test.BaseAdminViewTests):
args=[port.network_id, port.id])
res = self.client.get(url)
self.assertTemplateUsed(res, 'admin/networks/ports/update.html')
self.assertTemplateUsed(res, views.WorkflowView.template_name)
@test.create_stubs({api.neutron: ('port_get',
'is_extension_supported',
@ -402,13 +404,13 @@ class NetworkPortTests(test.BaseAdminViewTests):
.AndReturn(port)
api.neutron.is_extension_supported(IsA(http.HttpRequest),
'binding')\
.AndReturn(binding)
.MultipleTimes().AndReturn(binding)
api.neutron.is_extension_supported(IsA(http.HttpRequest),
'mac-learning')\
.AndReturn(mac_learning)
.MultipleTimes().AndReturn(mac_learning)
api.neutron.is_extension_supported(IsA(http.HttpRequest),
'port-security')\
.AndReturn(port_security)
.MultipleTimes().AndReturn(port_security)
extension_kwargs = {}
if binding:
extension_kwargs['binding__vnic_type'] = port.binding__vnic_type
@ -475,13 +477,13 @@ class NetworkPortTests(test.BaseAdminViewTests):
.AndReturn(port)
api.neutron.is_extension_supported(IsA(http.HttpRequest),
'binding')\
.AndReturn(binding)
.MultipleTimes().AndReturn(binding)
api.neutron.is_extension_supported(IsA(http.HttpRequest),
'mac-learning')\
.AndReturn(mac_learning)
.MultipleTimes().AndReturn(mac_learning)
api.neutron.is_extension_supported(IsA(http.HttpRequest),
'port-security')\
.AndReturn(port_security)
.MultipleTimes().AndReturn(port_security)
extension_kwargs = {}
if binding:
extension_kwargs['binding__vnic_type'] = port.binding__vnic_type

View File

@ -27,6 +27,8 @@ from openstack_dashboard.dashboards.admin.networks.ports \
import tables as ports_tables
from openstack_dashboard.dashboards.admin.networks.ports \
import tabs as ports_tabs
from openstack_dashboard.dashboards.admin.networks.ports \
import workflows as admin_workflows
from openstack_dashboard.dashboards.project.networks.ports \
import views as project_views
@ -99,11 +101,8 @@ class DetailView(project_views.DetailView):
class UpdateView(project_views.UpdateView):
form_class = ports_forms.UpdatePort
template_name = 'admin/networks/ports/update.html'
context_object_name = 'port'
submit_url = "horizon:admin:networks:editport"
success_url = 'horizon:admin:networks:detail'
workflow_class = admin_workflows.UpdatePort
failure_url = 'horizon:admin:networks:detail'
def get_initial(self):
initial = super(UpdateView, self).get_initial()
@ -111,4 +110,5 @@ class UpdateView(project_views.UpdateView):
initial['binding__host_id'] = port['binding__host_id']
initial['device_id'] = port['device_id']
initial['device_owner'] = port['device_owner']
return initial

View File

@ -0,0 +1,75 @@
# Copyright 2016 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 openstack_dashboard.dashboards.project.networks.ports \
import workflows as project_workflow
LOG = logging.getLogger(__name__)
class UpdatePortInfoAction(project_workflow.UpdatePortInfoAction):
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)
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)
mac_address = forms.MACAddressField(
label=_("MAC Address"),
required=False,
help_text=_("MAC address for the port"))
class Meta(object):
name = _("Info")
class UpdatePortInfo(project_workflow.UpdatePortInfo):
action_class = UpdatePortInfoAction
contributes = ["name", "admin_state",
"binding__vnic_type", "mac_state", "port_security_enabled",
"device_id", "device_owner", "binding__host_id",
"mac_address"]
class UpdatePort(project_workflow.UpdatePort):
default_steps = (UpdatePortInfo, )
def get_success_url(self):
return reverse("horizon:admin:networks:detail",
args=(self.context['network_id'],))
def _construct_parameters(self, data):
params = super(UpdatePort, self)._construct_parameters(data)
params.update({'device_id': data['device_id'],
'device_owner': data['device_owner'],
'binding__host_id': data['binding__host_id'],
'mac_address': data['mac_address']})
return params

View File

@ -1,7 +0,0 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
{% block modal-body-right %}
<h3>{% trans "Description:" %}</h3>
<p>{% trans "You may update the editable properties of your port here." %}</p>
{% endblock %}

View File

@ -1,7 +0,0 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Update Port" %}{% endblock %}
{% block main %}
{% include 'admin/networks/ports/_update.html' %}
{% endblock %}

View File

@ -14,7 +14,6 @@
import logging
from django.conf import settings
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext_lazy as _
@ -26,12 +25,6 @@ from openstack_dashboard import api
LOG = logging.getLogger(__name__)
VNIC_TYPES = [('normal', _('Normal')),
('direct', _('Direct')),
('direct-physical', _('Direct Physical')),
('macvtap', _('MacVTap')),
('baremetal', _('Bare Metal')),
('virtio-forwarder', _('Virtio Forwarder'))]
class CreatePort(forms.SelfHandlingForm):
@ -163,88 +156,3 @@ class CreatePort(forms.SelfHandlingForm):
redirect = reverse(self.failure_url,
args=(self.initial['network_id'],))
exceptions.handle(request, msg, redirect=redirect)
class UpdatePort(forms.SelfHandlingForm):
name = forms.CharField(max_length=255,
label=_("Name"),
required=False)
admin_state = forms.BooleanField(label=_("Enable Admin State"),
required=False)
failure_url = 'horizon:project:networks:detail'
def __init__(self, request, *args, **kwargs):
super(UpdatePort, self).__init__(request, *args, **kwargs)
try:
if api.neutron.is_extension_supported(request, 'binding'):
neutron_settings = getattr(settings,
'OPENSTACK_NEUTRON_NETWORK', {})
supported_vnic_types = neutron_settings.get(
'supported_vnic_types', ['*'])
if supported_vnic_types:
if supported_vnic_types == ['*']:
vnic_type_choices = VNIC_TYPES
else:
vnic_type_choices = [
vnic_type for vnic_type in VNIC_TYPES
if vnic_type[0] in supported_vnic_types
]
self.fields['binding__vnic_type'] = forms.ChoiceField(
choices=vnic_type_choices,
label=_("Binding: VNIC Type"),
help_text=_(
"The VNIC type that is bound to the neutron port"),
required=False)
except Exception:
msg = _("Unable to verify the VNIC types extension in Neutron")
exceptions.handle(self.request, msg)
try:
if api.neutron.is_extension_supported(request, 'mac-learning'):
self.fields['mac_state'] = forms.BooleanField(
label=_("MAC Learning State"), initial=False,
required=False)
except Exception:
msg = _("Unable to retrieve MAC learning state")
exceptions.handle(self.request, msg)
try:
if api.neutron.is_extension_supported(request, 'port-security'):
self.fields['port_security_enabled'] = forms.BooleanField(
label=_("Port Security"),
help_text=_("Enable anti-spoofing rules for the port"),
required=False)
except Exception:
msg = _("Unable to retrieve port security state")
exceptions.handle(self.request, msg)
def handle(self, request, data):
port_id = self.initial['port_id']
try:
LOG.debug('params = %s', data)
extension_kwargs = {}
if 'binding__vnic_type' in data:
extension_kwargs['binding__vnic_type'] = \
data['binding__vnic_type']
if 'mac_state' in data:
extension_kwargs['mac_learning_enabled'] = data['mac_state']
if 'port_security_enabled' in data:
extension_kwargs['port_security_enabled'] = \
data['port_security_enabled']
port = api.neutron.port_update(request,
port_id,
name=data['name'],
admin_state_up=data['admin_state'],
**extension_kwargs)
msg = _('Port %s was successfully updated.') % port_id
messages.success(request, msg)
return port
except Exception as e:
LOG.info('Failed to update port %(id)s: %(exc)s',
{'id': port_id, 'exc': e})
msg = _('Failed to update port %s') % port_id
redirect = reverse(self.failure_url,
args=[self.initial['network_id']])
exceptions.handle(request, msg, redirect=redirect)

View File

@ -20,6 +20,8 @@ from django import http
from mox3.mox import IsA
from horizon.workflows import views
from openstack_dashboard import api
from openstack_dashboard.test import helpers as test
@ -91,22 +93,21 @@ class NetworkPortTests(test.TestCase):
def _test_port_update_get(self, mac_learning=False, binding=False):
port = self.ports.first()
api.neutron.port_get(IsA(http.HttpRequest),
port.id)\
.AndReturn(port)
api.neutron.port_get(IsA(http.HttpRequest), port.id) \
.MultipleTimes().AndReturn(port)
api.neutron.is_extension_supported(IsA(http.HttpRequest),
'binding')\
.AndReturn(binding)
.MultipleTimes().AndReturn(binding)
api.neutron.is_extension_supported(IsA(http.HttpRequest),
'mac-learning')\
.AndReturn(mac_learning)
.MultipleTimes().AndReturn(mac_learning)
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')
self.assertTemplateUsed(res, views.WorkflowView.template_name)
@test.create_stubs({api.neutron: ('port_get',
'is_extension_supported',
@ -133,13 +134,13 @@ class NetworkPortTests(test.TestCase):
.AndReturn(port)
api.neutron.is_extension_supported(IsA(http.HttpRequest),
'binding')\
.AndReturn(binding)
.MultipleTimes().AndReturn(binding)
api.neutron.is_extension_supported(IsA(http.HttpRequest),
'mac-learning')\
.AndReturn(mac_learning)
.MultipleTimes().AndReturn(mac_learning)
api.neutron.is_extension_supported(IsA(http.HttpRequest),
'port-security')\
.AndReturn(port_security)
.MultipleTimes().AndReturn(port_security)
extension_kwargs = {}
if binding:
extension_kwargs['binding__vnic_type'] = port.binding__vnic_type
@ -198,13 +199,13 @@ class NetworkPortTests(test.TestCase):
.AndReturn(port)
api.neutron.is_extension_supported(IsA(http.HttpRequest),
'binding')\
.AndReturn(binding)
.MultipleTimes().AndReturn(binding)
api.neutron.is_extension_supported(IsA(http.HttpRequest),
'mac-learning')\
.AndReturn(mac_learning)
.MultipleTimes().AndReturn(mac_learning)
api.neutron.is_extension_supported(IsA(http.HttpRequest),
'port-security')\
.AndReturn(port_security)
.MultipleTimes().AndReturn(port_security)
extension_kwargs = {}
if binding:
extension_kwargs['binding__vnic_type'] = port.binding__vnic_type

View File

@ -19,6 +19,7 @@ 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
@ -28,10 +29,13 @@ from openstack_dashboard.dashboards.project.networks.ports \
import tables as project_tables
from openstack_dashboard.dashboards.project.networks.ports \
import tabs as project_tabs
from openstack_dashboard.dashboards.project.networks.ports \
import workflows as project_workflows
STATE_DICT = dict(project_tables.DISPLAY_CHOICES)
STATUS_DICT = dict(project_tables.STATUS_DISPLAY_CHOICES)
VNIC_TYPES = dict(project_forms.VNIC_TYPES)
VNIC_TYPE_DICT = dict(api.neutron.VNIC_TYPES)
class CreateView(forms.ModalFormView):
@ -89,7 +93,7 @@ class DetailView(tabs.TabbedTableView):
port.status_label = STATUS_DICT.get(port.status,
port.status)
if port.get('binding__vnic_type'):
port.binding__vnic_type = VNIC_TYPES.get(
port.binding__vnic_type = VNIC_TYPE_DICT.get(
port.binding__vnic_type, port.binding__vnic_type)
except Exception:
port = []
@ -161,19 +165,9 @@ class DetailView(tabs.TabbedTableView):
return reverse('horizon:project:networks:index')
class UpdateView(forms.ModalFormView):
form_class = project_forms.UpdatePort
form_id = "update_port_form"
template_name = 'project/networks/ports/update.html'
context_object_name = 'port'
submit_label = _("Save Changes")
submit_url = "horizon:project:networks:editport"
success_url = 'horizon:project:networks:detail'
page_title = _("Edit Port")
def get_success_url(self):
return reverse(self.success_url,
args=(self.kwargs['network_id'],))
class UpdateView(workflows.WorkflowView):
workflow_class = project_workflows.UpdatePort
failure_url = "horizon:project:networks:detail"
@memoized.memoized_method
def _get_object(self, *args, **kwargs):
@ -181,21 +175,11 @@ class UpdateView(forms.ModalFormView):
try:
return api.neutron.port_get(self.request, port_id)
except Exception:
redirect = self.get_success_url()
redirect = reverse(self.failure_url,
args=(self.kwargs['network_id'],))
msg = _('Unable to retrieve port details')
exceptions.handle(self.request, msg, redirect=redirect)
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']
args = (self.kwargs['network_id'], self.kwargs['port_id'],)
context['submit_url'] = reverse(self.submit_url, args=args)
context['cancel_url'] = reverse(self.success_url,
args=(self.kwargs['network_id'],))
return context
def get_initial(self):
port = self._get_object()
initial = {'port_id': port['id'],
@ -213,4 +197,5 @@ class UpdateView(forms.ModalFormView):
pass
if 'port_security_enabled' in port:
initial['port_security_enabled'] = port['port_security_enabled']
return initial

View File

@ -0,0 +1,139 @@
# Copyright 2016 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 horizon import exceptions
from horizon import forms
from horizon import workflows
from openstack_dashboard import api
LOG = logging.getLogger(__name__)
class UpdatePortInfoAction(workflows.Action):
name = forms.CharField(max_length=255,
label=_("Name"),
required=False)
admin_state = forms.BooleanField(label=_("Enable Admin State"),
required=False)
def __init__(self, request, *args, **kwargs):
super(UpdatePortInfoAction, self).__init__(request, *args, **kwargs)
try:
if api.neutron.is_extension_supported(request, 'binding'):
neutron_settings = getattr(settings,
'OPENSTACK_NEUTRON_NETWORK', {})
supported_vnic_types = neutron_settings.get(
'supported_vnic_types', ['*'])
if supported_vnic_types:
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'] = forms.ChoiceField(
choices=vnic_type_choices,
label=_("Binding: VNIC Type"),
help_text=_(
"The VNIC type that is bound to the neutron port"),
required=False)
except Exception:
msg = _("Unable to verify the VNIC types extension in Neutron")
exceptions.handle(self.request, msg)
try:
if api.neutron.is_extension_supported(request, 'mac-learning'):
self.fields['mac_state'] = forms.BooleanField(
label=_("MAC Learning State"), initial=False,
required=False)
except Exception:
msg = _("Unable to retrieve MAC learning state")
exceptions.handle(self.request, msg)
try:
if api.neutron.is_extension_supported(request, 'port-security'):
self.fields['port_security_enabled'] = forms.BooleanField(
label=_("Port Security"),
help_text=_("Enable anti-spoofing rules for the port"),
required=False
)
except Exception:
msg = _("Unable to retrieve port security state")
exceptions.handle(self.request, msg)
class Meta(object):
name = _("Info")
class UpdatePortInfo(workflows.Step):
action_class = UpdatePortInfoAction
depends_on = ("network_id", "port_id")
contributes = ["name", "admin_state",
"binding__vnic_type", "mac_state", "port_security_enabled"]
help_text = _("You can update the editable properties of your port here.")
class UpdatePort(workflows.Workflow):
slug = "update_port"
name = _("Edit Port")
finalize_button_name = _("Update")
success_message = _('Port %s was successfully updated.')
failure_message = _('Failed to update port "%s".')
default_steps = (UpdatePortInfo,)
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['port_id']
return message % name
def handle(self, request, data):
port_id = self.context['port_id']
LOG.debug('params = %s', data)
params = self._construct_parameters(data)
try:
api.neutron.port_update(request, port_id, **params)
return True
except Exception as e:
LOG.info('Failed to update port %(port_id)s: %(exc)s',
{'port_id': port_id, 'exc': e})
return False
def _construct_parameters(self, data):
params = {
'name': data['name'],
'admin_state_up': data['admin_state'],
}
# If a field value is None, it means the field is not supported,
# If so, we skip sending such field.
if data['binding__vnic_type'] is not None:
params['binding__vnic_type'] = data['binding__vnic_type']
if data['mac_state'] is not None:
params['mac_learning_enabled'] = data['mac_state']
if data['port_security_enabled'] is not None:
params['port_security_enabled'] = data['port_security_enabled']
return params

View File

@ -1,7 +0,0 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
{% block modal-body-right %}
<h3>{% trans "Description:" %}</h3>
<p>{% trans "You may update the editable properties of your port here." %}</p>
{% endblock %}

View File

@ -1,7 +0,0 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Update Port" %}{% endblock %}
{% block main %}
{% include 'project/networks/ports/_update.html' %}
{% endblock %}