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).