Support security groups association per port

This patch support operation for operators and project users to
associate security groups to a port. The feature is mentioned at
the neutron user feedback session in Barcelona summit [1].

This function UI is same as the function of security groups
association per instance. To realize this, the way of implementation
for 'Edit port' is changed, which move from a single modal to a
workflow base.

[1] https://etherpad.openstack.org/p/ocata-neutron-end-user-operator-feedback (L.35+)

Also we need to display how security groups is associated at a port.
At the moment, there is not way to be able to see it (only this function).
It should be done as an another patch.

Change-Id: I96e0fafdffbf05b8167ec1b85f7430176fdaab90
Closes-Bug: #1637444
Co-Authored-By: Akihiro Motoki <amotoki@gmail.com>
This commit is contained in:
Kenji Ishii 2017-03-30 02:43:49 +00:00 committed by Akihiro Motoki
parent 0ff122d830
commit e9db12382e
13 changed files with 207 additions and 93 deletions

View File

@ -331,8 +331,12 @@ class SecurityGroupManager(object):
:returns: List of SecurityGroup objects
"""
# This is to ensure tenant_id key is not populated
# if tenant_id=None is specified.
tenant_id = params.pop('tenant_id', self.request.user.tenant_id)
return self._list(tenant_id=tenant_id, **params)
if tenant_id:
params['tenant_id'] = tenant_id
return self._list(**params)
def _sg_name_dict(self, sg_id, rules):
"""Create a mapping dict from secgroup id to its name."""

View File

@ -341,21 +341,18 @@ class NetworkPortTests(test.BaseAdminViewTests):
redir_url = reverse(NETWORKS_DETAIL_URL, args=[port.network_id])
self.assertRedirectsNoFollow(res, redir_url)
@test.create_stubs({api.neutron: ('port_get',
'is_extension_supported',)})
def test_port_update_get(self):
self._test_port_update_get()
@test.create_stubs({api.neutron: ('port_get',
'is_extension_supported',)})
def test_port_update_get_with_mac_learning(self):
self._test_port_update_get(mac_learning=True)
@test.create_stubs({api.neutron: ('port_get',
'is_extension_supported',)})
def test_port_update_get_with_port_security(self):
self._test_port_update_get(port_security=True)
@test.create_stubs({api.neutron: ('port_get',
'security_group_list',
'is_extension_supported',)})
def _test_port_update_get(self, mac_learning=False, binding=False,
port_security=False):
port = self.ports.first()
@ -371,6 +368,9 @@ class NetworkPortTests(test.BaseAdminViewTests):
api.neutron.is_extension_supported(IsA(http.HttpRequest),
'port-security')\
.AndReturn(port_security)
api.neutron.security_group_list(IsA(http.HttpRequest),
tenant_id=None)\
.AndReturn(self.security_groups.list())
self.mox.ReplayAll()
url = reverse('horizon:admin:networks:editport',
@ -379,24 +379,19 @@ class NetworkPortTests(test.BaseAdminViewTests):
self.assertTemplateUsed(res, views.WorkflowView.template_name)
@test.create_stubs({api.neutron: ('port_get',
'is_extension_supported',
'port_update')})
def test_port_update_post(self):
self._test_port_update_post()
@test.create_stubs({api.neutron: ('port_get',
'is_extension_supported',
'port_update')})
def test_port_update_post_with_mac_learning(self):
self._test_port_update_post(mac_learning=True)
@test.create_stubs({api.neutron: ('port_get',
'is_extension_supported',
'port_update')})
def test_port_update_post_with_port_security(self):
self._test_port_update_post(port_security=True)
@test.create_stubs({api.neutron: ('port_get',
'is_extension_supported',
'security_group_list',
'port_update')})
def _test_port_update_post(self, mac_learning=False, binding=False,
port_security=False):
port = self.ports.first()
@ -411,6 +406,9 @@ class NetworkPortTests(test.BaseAdminViewTests):
api.neutron.is_extension_supported(IsA(http.HttpRequest),
'port-security')\
.MultipleTimes().AndReturn(port_security)
api.neutron.security_group_list(IsA(http.HttpRequest),
tenant_id=None)\
.AndReturn(self.security_groups.list())
extension_kwargs = {}
if binding:
extension_kwargs['binding__vnic_type'] = port.binding__vnic_type
@ -451,24 +449,19 @@ class NetworkPortTests(test.BaseAdminViewTests):
redir_url = reverse(NETWORKS_DETAIL_URL, args=[port.network_id])
self.assertRedirectsNoFollow(res, redir_url)
@test.create_stubs({api.neutron: ('port_get',
'is_extension_supported',
'port_update')})
def test_port_update_post_exception(self):
self._test_port_update_post_exception()
@test.create_stubs({api.neutron: ('port_get',
'is_extension_supported',
'port_update')})
def test_port_update_post_exception_with_mac_learning(self):
self._test_port_update_post_exception(mac_learning=True, binding=False)
@test.create_stubs({api.neutron: ('port_get',
'is_extension_supported',
'port_update')})
def test_port_update_post_exception_with_port_security(self):
self._test_port_update_post_exception(port_security=True)
@test.create_stubs({api.neutron: ('port_get',
'is_extension_supported',
'security_group_list',
'port_update')})
def _test_port_update_post_exception(self, mac_learning=False,
binding=False,
port_security=False):
@ -484,6 +477,9 @@ class NetworkPortTests(test.BaseAdminViewTests):
api.neutron.is_extension_supported(IsA(http.HttpRequest),
'port-security')\
.MultipleTimes().AndReturn(port_security)
api.neutron.security_group_list(IsA(http.HttpRequest),
tenant_id=None)\
.AndReturn(self.security_groups.list())
extension_kwargs = {}
if binding:
extension_kwargs['binding__vnic_type'] = port.binding__vnic_type

View File

@ -30,25 +30,20 @@ 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"))
required=False)
class Meta(object):
name = _("Info")
help_text_template = 'admin/networks/ports/_edit_port_help.html'
class UpdatePortInfo(project_workflow.UpdatePortInfo):
@ -60,7 +55,7 @@ class UpdatePortInfo(project_workflow.UpdatePortInfo):
class UpdatePort(project_workflow.UpdatePort):
default_steps = (UpdatePortInfo, )
default_steps = (UpdatePortInfo, project_workflow.UpdatePortSecurityGroup)
def get_success_url(self):
return reverse("horizon:admin:networks:detail",

View File

@ -0,0 +1,18 @@
{% extends 'project/networks/ports/_edit_port_help.html' %}
{% load i18n %}
{% block admin_fields %}
<dt>{% trans "Device ID" %}</dt>
<dd>{% blocktrans trimmed %}Device ID attached to the port.
{% endblocktrans %}</dd>
<dt>{% trans "Device Owner" %}</dt>
<dd>{% blocktrans trimmed %}Device owner attached to the port.
{% endblocktrans %}</dd>
<dt>{% trans "Binding: Host" %}</dt>
<dd>{% blocktrans trimmed %}The ID of the host where the port is allocated.
In some cases, different implementations can run on different hosts.
{% endblocktrans %}</dd>
<dt>{% trans "MAC Address" %}</dt>
<dd>{% blocktrans trimmed %}MAC address for the port.
{% endblocktrans %}</dd>
{% endblock admin_fields %}

View File

@ -1509,7 +1509,8 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase):
server = self.servers.first()
api.nova.server_get(IsA(http.HttpRequest), server.id).AndReturn(server)
api.neutron.security_group_list(IsA(http.HttpRequest)) \
api.neutron.security_group_list(IsA(http.HttpRequest),
tenant_id=None) \
.AndReturn([])
api.neutron.server_security_groups(IsA(http.HttpRequest),
server.id).AndReturn([])
@ -1561,7 +1562,8 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase):
wanted_groups = [secgroups[1].id, secgroups[2].id]
api.nova.server_get(IsA(http.HttpRequest), server.id).AndReturn(server)
api.neutron.security_group_list(IsA(http.HttpRequest)) \
api.neutron.security_group_list(IsA(http.HttpRequest),
tenant_id=None) \
.AndReturn(secgroups)
api.neutron.server_security_groups(IsA(http.HttpRequest),
server.id).AndReturn(server_groups)

View File

@ -31,15 +31,14 @@ ADD_USER_URL = "horizon:projects:instances:create_user"
INSTANCE_SEC_GROUP_SLUG = "update_security_groups"
class UpdateInstanceSecurityGroupsAction(workflows.MembershipAction):
class BaseSecurityGroupsAction(workflows.MembershipAction):
def __init__(self, request, *args, **kwargs):
super(UpdateInstanceSecurityGroupsAction, self).__init__(request,
*args,
**kwargs)
super(BaseSecurityGroupsAction, self).__init__(request,
*args,
**kwargs)
err_msg = _('Unable to retrieve security group list. '
'Please try again later.')
context = args[0]
instance_id = context.get('instance_id', '')
default_role_name = self.get_default_role_field_name()
self.fields[default_role_name] = forms.CharField(required=False)
@ -48,22 +47,55 @@ class UpdateInstanceSecurityGroupsAction(workflows.MembershipAction):
# Get list of available security groups
all_groups = []
try:
all_groups = api.neutron.security_group_list(request)
# target_tenant_id is required when the form is used as admin.
# Owner of security group and port should match.
tenant_id = context.get('target_tenant_id')
all_groups = api.neutron.security_group_list(request,
tenant_id=tenant_id)
except Exception:
exceptions.handle(request, err_msg)
groups_list = [(group.id, group.name) for group in all_groups]
instance_groups = []
try:
instance_groups = api.neutron.server_security_groups(request,
instance_id)
except Exception:
exceptions.handle(request, err_msg)
field_name = self.get_member_field_name('member')
self.fields[field_name] = forms.MultipleChoiceField(required=False)
self.fields[field_name].choices = groups_list
self.fields[field_name].initial = [group.id
for group in instance_groups]
sec_groups = []
try:
sec_groups = self._get_initial_security_groups(context)
except Exception:
exceptions.handle(request, err_msg)
self.fields[field_name].initial = sec_groups
def _get_initial_security_groups(self, context):
# This depends on each cases
pass
def handle(self, request, data):
# This depends on each cases
pass
class BaseSecurityGroups(workflows.UpdateMembersStep):
available_list_title = _("All Security Groups")
no_available_text = _("No security groups found.")
no_members_text = _("No security groups enabled.")
show_roles = False
contributes = ("wanted_groups",)
def contribute(self, data, context):
request = self.workflow.request
if data:
field_name = self.get_member_field_name('member')
context["wanted_groups"] = request.POST.getlist(field_name)
return context
class UpdateInstanceSecurityGroupsAction(BaseSecurityGroupsAction):
def _get_initial_security_groups(self, context):
instance_id = context.get('instance_id', '')
sec_groups = api.neutron.server_security_groups(self.request,
instance_id)
return [group.id for group in sec_groups]
def handle(self, request, data):
instance_id = data['instance_id']
@ -81,28 +113,16 @@ class UpdateInstanceSecurityGroupsAction(workflows.MembershipAction):
slug = INSTANCE_SEC_GROUP_SLUG
class UpdateInstanceSecurityGroups(workflows.UpdateMembersStep):
class UpdateInstanceSecurityGroups(BaseSecurityGroups):
action_class = UpdateInstanceSecurityGroupsAction
members_list_title = _("Instance Security Groups")
help_text = _("Add and remove security groups to this instance "
"from the list of available security groups.")
available_list_title = _("All Security Groups")
members_list_title = _("Instance Security Groups")
no_available_text = _("No security groups found.")
no_members_text = _("No security groups enabled.")
show_roles = False
depends_on = ("instance_id",)
contributes = ("wanted_groups",)
def allowed(self, request):
return api.base.is_service_enabled(request, 'network')
def contribute(self, data, context):
request = self.workflow.request
if data:
field_name = self.get_member_field_name('member')
context["wanted_groups"] = request.POST.getlist(field_name)
return context
class UpdateInstanceInfoAction(workflows.Action):
name = forms.CharField(label=_("Name"),

View File

@ -16,6 +16,7 @@ import logging
from django.core.urlresolvers import reverse
from django import template
from django.utils.http import urlencode
from django.utils.translation import pgettext_lazy
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ungettext_lazy
@ -55,7 +56,10 @@ class UpdatePort(policy.PolicyTargetMixin, tables.LinkAction):
def get_link_url(self, port):
network_id = self.table.kwargs['network_id']
return reverse(self.url, args=(network_id, port.id))
base_url = reverse(self.url, args=(network_id, port.id))
params = {'step': 'update_info'}
param = urlencode(params)
return '?'.join([base_url, param])
DISPLAY_CHOICES = (

View File

@ -81,16 +81,15 @@ class NetworkPortTests(test.TestCase):
self.assertRedirectsNoFollow(res, NETWORKS_INDEX_URL)
@test.create_stubs({api.neutron: ('port_get',
'is_extension_supported',)})
def test_port_update_get(self):
self._test_port_update_get()
@test.create_stubs({api.neutron: ('port_get',
'is_extension_supported',)})
def test_port_update_get_with_mac_learning(self):
self._test_port_update_get(mac_learning=True)
@test.create_stubs({api.neutron: ('port_get',
'security_group_list',
'is_extension_supported',)})
def _test_port_update_get(self, mac_learning=False, binding=False):
port = self.ports.first()
api.neutron.port_get(IsA(http.HttpRequest), port.id) \
@ -101,6 +100,9 @@ class NetworkPortTests(test.TestCase):
api.neutron.is_extension_supported(IsA(http.HttpRequest),
'mac-learning')\
.MultipleTimes().AndReturn(mac_learning)
api.neutron.security_group_list(IsA(http.HttpRequest),
tenant_id=None)\
.AndReturn(self.security_groups.list())
self.mox.ReplayAll()
url = reverse('horizon:project:networks:editport',
@ -109,27 +111,23 @@ class NetworkPortTests(test.TestCase):
self.assertTemplateUsed(res, views.WorkflowView.template_name)
@test.create_stubs({api.neutron: ('port_get',
'is_extension_supported',
'port_update')})
def test_port_update_post(self):
self._test_port_update_post()
@test.create_stubs({api.neutron: ('port_get',
'is_extension_supported',
'port_update')})
def test_port_update_post_with_mac_learning(self):
self._test_port_update_post(mac_learning=True)
@test.create_stubs({api.neutron: ('port_get',
'is_extension_supported',
'port_update')})
def test_port_update_post_with_port_security(self):
self._test_port_update_post(port_security=True)
@test.create_stubs({api.neutron: ('port_get',
'is_extension_supported',
'security_group_list',
'port_update')})
def _test_port_update_post(self, mac_learning=False, binding=False,
port_security=False):
port = self.ports.first()
security_groups = self.security_groups.list()
api.neutron.port_get(IsA(http.HttpRequest), port.id)\
.AndReturn(port)
api.neutron.is_extension_supported(IsA(http.HttpRequest),
@ -141,6 +139,9 @@ class NetworkPortTests(test.TestCase):
api.neutron.is_extension_supported(IsA(http.HttpRequest),
'port-security')\
.MultipleTimes().AndReturn(port_security)
api.neutron.security_group_list(IsA(http.HttpRequest),
tenant_id=None)\
.AndReturn(self.security_groups.list())
extension_kwargs = {}
if binding:
extension_kwargs['binding__vnic_type'] = port.binding__vnic_type
@ -148,6 +149,7 @@ class NetworkPortTests(test.TestCase):
extension_kwargs['mac_learning_enabled'] = True
if port_security:
extension_kwargs['port_security_enabled'] = True
extension_kwargs['wanted_groups'] = security_groups
api.neutron.port_update(IsA(http.HttpRequest), port.id,
name=port.name,
admin_state_up=port.admin_state_up,
@ -165,6 +167,7 @@ class NetworkPortTests(test.TestCase):
form_data['mac_state'] = True
if port_security:
form_data['port_security_enabled'] = True
form_data['wanted_groups'] = security_groups
url = reverse('horizon:project:networks:editport',
args=[port.network_id, port.id])
res = self.client.post(url, form_data)
@ -172,24 +175,19 @@ class NetworkPortTests(test.TestCase):
redir_url = reverse(NETWORKS_DETAIL_URL, args=[port.network_id])
self.assertRedirectsNoFollow(res, redir_url)
@test.create_stubs({api.neutron: ('port_get',
'is_extension_supported',
'port_update')})
def test_port_update_post_exception(self):
self._test_port_update_post_exception()
@test.create_stubs({api.neutron: ('port_get',
'is_extension_supported',
'port_update')})
def test_port_update_post_exception_with_mac_learning(self):
self._test_port_update_post_exception(mac_learning=True)
@test.create_stubs({api.neutron: ('port_get',
'is_extension_supported',
'port_update')})
def test_port_update_post_exception_with_port_security(self):
self._test_port_update_post_exception(port_security=True)
@test.create_stubs({api.neutron: ('port_get',
'is_extension_supported',
'security_group_list',
'port_update')})
def _test_port_update_post_exception(self, mac_learning=False,
binding=False,
port_security=False):
@ -206,6 +204,9 @@ class NetworkPortTests(test.TestCase):
api.neutron.is_extension_supported(IsA(http.HttpRequest),
'port-security')\
.MultipleTimes().AndReturn(port_security)
api.neutron.security_group_list(IsA(http.HttpRequest),
tenant_id=None)\
.AndReturn(self.security_groups.list())
extension_kwargs = {}
if binding:
extension_kwargs['binding__vnic_type'] = port.binding__vnic_type

View File

@ -187,7 +187,8 @@ class UpdateView(workflows.WorkflowView):
'tenant_id': port['tenant_id'],
'name': port['name'],
'admin_state': port['admin_state_up'],
'mac_address': port['mac_address']}
'mac_address': port['mac_address'],
'target_tenant_id': port['tenant_id']}
if port.get('binding__vnic_type'):
initial['binding__vnic_type'] = port['binding__vnic_type']
try:

View File

@ -24,6 +24,9 @@ from horizon import forms
from horizon import workflows
from openstack_dashboard import api
from openstack_dashboard.dashboards.project.instances.workflows import \
update_instance as base_sec_group
from openstack_dashboard.utils import filters
LOG = logging.getLogger(__name__)
@ -55,8 +58,6 @@ class UpdatePortInfoAction(workflows.Action):
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")
@ -75,8 +76,13 @@ class UpdatePortInfoAction(workflows.Action):
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
required=False,
widget=forms.CheckboxInput(attrs={
'class': 'switchable',
'data-slug': 'port_security_enabled',
'data-hide-tab': 'update_port__update_security_groups',
'data-hide-on-checked': 'false'
})
)
except Exception:
msg = _("Unable to retrieve port security state")
@ -84,6 +90,8 @@ class UpdatePortInfoAction(workflows.Action):
class Meta(object):
name = _("Info")
slug = 'update_info'
help_text_template = 'project/networks/ports/_edit_port_help.html'
class UpdatePortInfo(workflows.Step):
@ -91,7 +99,25 @@ class UpdatePortInfo(workflows.Step):
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 UpdatePortSecurityGroupAction(base_sec_group.BaseSecurityGroupsAction):
def _get_initial_security_groups(self, context):
port_id = context.get('port_id', '')
port = api.neutron.port_get(self.request, port_id)
return port.security_groups
class Meta(object):
name = _("Security Groups")
slug = "update_security_groups"
class UpdatePortSecurityGroup(base_sec_group.BaseSecurityGroups):
action_class = UpdatePortSecurityGroupAction
members_list_title = _("Port Security Groups")
help_text = _("Add or remove security groups to this port "
"from the list of available security groups.")
depends_on = ("port_id", 'target_tenant_id')
class UpdatePort(workflows.Workflow):
@ -100,7 +126,7 @@ class UpdatePort(workflows.Workflow):
finalize_button_name = _("Update")
success_message = _('Port %s was successfully updated.')
failure_message = _('Failed to update port "%s".')
default_steps = (UpdatePortInfo,)
default_steps = (UpdatePortInfo, UpdatePortSecurityGroup)
def get_success_url(self):
return reverse("horizon:project:networks:detail",
@ -136,4 +162,18 @@ class UpdatePort(workflows.Workflow):
if data['port_security_enabled'] is not None:
params['port_security_enabled'] = data['port_security_enabled']
# If port_security_enabled is set to False, security groups on the port
# must be cleared. We will clear the current security groups
# in this case.
if ('port_security_enabled' in params
and not params['port_security_enabled']):
params['security_groups'] = []
# In case of UpdatePortSecurityGroup registered, 'wanted_groups'
# exists in data.
elif 'wanted_groups' in data:
# If data has that key, we need to set its value
# even if its value is empty to clear sec group setting.
groups = map(filters.get_int_or_uuid, data['wanted_groups'])
params['security_groups'] = groups
return params

View File

@ -0,0 +1,25 @@
{% load i18n %}
<p>{% trans "You can edit the properties of your port here." %}</p>
<dl>
<dt>{% trans "Enable Admin State" %}</dt>
<dd>{% blocktrans trimmed %}When the admin state of the port is enabled, the
networking service forward packets on the port. Otherwise, it does not
forward any packets on the port.{% endblocktrans %}</dd>
{% block admin_fields %}{% endblock %}
<dt>{% trans "Binding: VNIC Type" %}</dt>
<dd>{% blocktrans trimmed %}It specified the VNIC type bound to the
networking port.{% endblocktrans %}</dd>
<dt>{% trans "Port Security" %}</dt>
<dd>{% blocktrans trimmed %}
Enables anti-spoofing rules for the port if enabled. In addition,
if port security is disabled, security groups
on the port will be automatically cleared. When you enable port security
of the port, you may want to associate some security groups on
the port.{% endblocktrans %}</dd>
<dt>{% trans "Security Groups" %}</dt>
<dd>{% blocktrans trimmed %}You can add or remove security groups
associated with the port in the next tab (if the port security is
enabled for the port).
{% endblocktrans %}</dd>
</dl>

View File

@ -120,6 +120,7 @@ def data(TEST):
{'ip_address': '174.0.0.201',
'mac_address': 'fa:16:3e:7a:7b:18'}
],
'port_security_enabled': True,
'security_groups': [],
}
@ -140,6 +141,7 @@ def data(TEST):
'tenant_id': network_dict['tenant_id'],
'binding:vnic_type': 'normal',
'binding:host_id': 'host',
'port_security_enabled': True,
'security_groups': [
# sec_group_1 ID below
'faad7c80-3b62-4440-967c-13808c37131d',

View File

@ -0,0 +1,6 @@
---
features:
- Support security groups association per network port for operators
and users. Note that the current implementation only supports to edit
security groups of neutron port from the port tables in the network
detail page (Further improvement is planned).