diff --git a/openstack_dashboard/api/neutron.py b/openstack_dashboard/api/neutron.py
index a57350041e..4d331cae2b 100644
--- a/openstack_dashboard/api/neutron.py
+++ b/openstack_dashboard/api/neutron.py
@@ -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."""
diff --git a/openstack_dashboard/dashboards/admin/networks/ports/tests.py b/openstack_dashboard/dashboards/admin/networks/ports/tests.py
index 248e2aba88..3c8d082f27 100644
--- a/openstack_dashboard/dashboards/admin/networks/ports/tests.py
+++ b/openstack_dashboard/dashboards/admin/networks/ports/tests.py
@@ -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
diff --git a/openstack_dashboard/dashboards/admin/networks/ports/workflows.py b/openstack_dashboard/dashboards/admin/networks/ports/workflows.py
index c86b13aedf..dd70738a16 100644
--- a/openstack_dashboard/dashboards/admin/networks/ports/workflows.py
+++ b/openstack_dashboard/dashboards/admin/networks/ports/workflows.py
@@ -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",
diff --git a/openstack_dashboard/dashboards/admin/networks/templates/networks/ports/_edit_port_help.html b/openstack_dashboard/dashboards/admin/networks/templates/networks/ports/_edit_port_help.html
new file mode 100644
index 0000000000..6387c09b6f
--- /dev/null
+++ b/openstack_dashboard/dashboards/admin/networks/templates/networks/ports/_edit_port_help.html
@@ -0,0 +1,18 @@
+{% extends 'project/networks/ports/_edit_port_help.html' %}
+{% load i18n %}
+
+{% block admin_fields %}
+
{% trans "Device ID" %}
+ {% blocktrans trimmed %}Device ID attached to the port.
+ {% endblocktrans %}
+ {% trans "Device Owner" %}
+ {% blocktrans trimmed %}Device owner attached to the port.
+ {% endblocktrans %}
+ {% trans "Binding: Host" %}
+ {% blocktrans trimmed %}The ID of the host where the port is allocated.
+ In some cases, different implementations can run on different hosts.
+ {% endblocktrans %}
+ {% trans "MAC Address" %}
+ {% blocktrans trimmed %}MAC address for the port.
+ {% endblocktrans %}
+{% endblock admin_fields %}
diff --git a/openstack_dashboard/dashboards/project/instances/tests.py b/openstack_dashboard/dashboards/project/instances/tests.py
index 9b57ef6b1b..eb16d90256 100644
--- a/openstack_dashboard/dashboards/project/instances/tests.py
+++ b/openstack_dashboard/dashboards/project/instances/tests.py
@@ -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)
diff --git a/openstack_dashboard/dashboards/project/instances/workflows/update_instance.py b/openstack_dashboard/dashboards/project/instances/workflows/update_instance.py
index 7e748b906d..048b2e8f97 100644
--- a/openstack_dashboard/dashboards/project/instances/workflows/update_instance.py
+++ b/openstack_dashboard/dashboards/project/instances/workflows/update_instance.py
@@ -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"),
diff --git a/openstack_dashboard/dashboards/project/networks/ports/tables.py b/openstack_dashboard/dashboards/project/networks/ports/tables.py
index aa13d464c2..7c6453e860 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.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 = (
diff --git a/openstack_dashboard/dashboards/project/networks/ports/tests.py b/openstack_dashboard/dashboards/project/networks/ports/tests.py
index 198b9f2be9..8806db3ff0 100644
--- a/openstack_dashboard/dashboards/project/networks/ports/tests.py
+++ b/openstack_dashboard/dashboards/project/networks/ports/tests.py
@@ -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
diff --git a/openstack_dashboard/dashboards/project/networks/ports/views.py b/openstack_dashboard/dashboards/project/networks/ports/views.py
index 603290f285..3000fbdfdb 100644
--- a/openstack_dashboard/dashboards/project/networks/ports/views.py
+++ b/openstack_dashboard/dashboards/project/networks/ports/views.py
@@ -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:
diff --git a/openstack_dashboard/dashboards/project/networks/ports/workflows.py b/openstack_dashboard/dashboards/project/networks/ports/workflows.py
index 133939a8a2..4966b922e9 100644
--- a/openstack_dashboard/dashboards/project/networks/ports/workflows.py
+++ b/openstack_dashboard/dashboards/project/networks/ports/workflows.py
@@ -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
diff --git a/openstack_dashboard/dashboards/project/networks/templates/networks/ports/_edit_port_help.html b/openstack_dashboard/dashboards/project/networks/templates/networks/ports/_edit_port_help.html
new file mode 100644
index 0000000000..1b249f8e30
--- /dev/null
+++ b/openstack_dashboard/dashboards/project/networks/templates/networks/ports/_edit_port_help.html
@@ -0,0 +1,25 @@
+{% load i18n %}
+
+{% trans "You can edit the properties of your port here." %}
+
+ - {% trans "Enable Admin State" %}
+ - {% 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 %}
+ {% block admin_fields %}{% endblock %}
+ - {% trans "Binding: VNIC Type" %}
+ - {% blocktrans trimmed %}It specified the VNIC type bound to the
+ networking port.{% endblocktrans %}
+ - {% trans "Port Security" %}
+ - {% 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 %}
+ - {% trans "Security Groups" %}
+ - {% 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 %}
+
diff --git a/openstack_dashboard/test/test_data/neutron_data.py b/openstack_dashboard/test/test_data/neutron_data.py
index 25928b55f9..876b01a6cc 100644
--- a/openstack_dashboard/test/test_data/neutron_data.py
+++ b/openstack_dashboard/test/test_data/neutron_data.py
@@ -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',
diff --git a/releasenotes/notes/security-group-associate-per-port-c81ca7beb7dca409.yaml b/releasenotes/notes/security-group-associate-per-port-c81ca7beb7dca409.yaml
new file mode 100644
index 0000000000..c4f939846f
--- /dev/null
+++ b/releasenotes/notes/security-group-associate-per-port-c81ca7beb7dca409.yaml
@@ -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).