diff --git a/openstack_dashboard/api/neutron.py b/openstack_dashboard/api/neutron.py index f819a83f6..bf2836857 100644 --- a/openstack_dashboard/api/neutron.py +++ b/openstack_dashboard/api/neutron.py @@ -20,6 +20,7 @@ from __future__ import absolute_import import collections +import copy import logging import netaddr @@ -120,9 +121,23 @@ class Port(NeutronAPIDictWrapper): if 'mac_learning_enabled' in apidict: apidict['mac_state'] = \ ON_STATE if apidict['mac_learning_enabled'] else OFF_STATE + pairs = apidict.get('allowed_address_pairs') + if pairs: + apidict = copy.deepcopy(apidict) + wrapped_pairs = [PortAllowedAddressPair(pair) for pair in pairs] + apidict['allowed_address_pairs'] = wrapped_pairs super(Port, self).__init__(apidict) +class PortAllowedAddressPair(NeutronAPIDictWrapper): + """Wrapper for neutron port allowed address pairs.""" + + def __init__(self, addr_pair): + super(PortAllowedAddressPair, self).__init__(addr_pair) + # Horizon references id property for table operations + self.id = addr_pair['ip_address'] + + class Profile(NeutronAPIDictWrapper): """Wrapper for neutron profiles.""" _attrs = ['profile_id', 'name', 'segment_type', 'segment_range', diff --git a/openstack_dashboard/dashboards/admin/networks/ports/extensions/__init__.py b/openstack_dashboard/dashboards/admin/networks/ports/extensions/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openstack_dashboard/dashboards/admin/networks/ports/extensions/allowed_address_pairs/__init__.py b/openstack_dashboard/dashboards/admin/networks/ports/extensions/allowed_address_pairs/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openstack_dashboard/dashboards/admin/networks/ports/extensions/allowed_address_pairs/forms.py b/openstack_dashboard/dashboards/admin/networks/ports/extensions/allowed_address_pairs/forms.py new file mode 100644 index 000000000..28fde85c1 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/networks/ports/extensions/allowed_address_pairs/forms.py @@ -0,0 +1,21 @@ +# Copyright 2015, Alcatel-Lucent USA Inc. +# All Rights Reserved. +# +# 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. + +from openstack_dashboard.dashboards.project.networks.ports.extensions.\ + allowed_address_pairs import forms as project_forms + + +class AddAllowedAddressPairForm(project_forms.AddAllowedAddressPairForm): + failure_url = 'horizon:admin:networks:ports:detail' diff --git a/openstack_dashboard/dashboards/admin/networks/ports/extensions/allowed_address_pairs/views.py b/openstack_dashboard/dashboards/admin/networks/ports/extensions/allowed_address_pairs/views.py new file mode 100644 index 000000000..8d8fef331 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/networks/ports/extensions/allowed_address_pairs/views.py @@ -0,0 +1,25 @@ +# Copyright 2015, Alcatel-Lucent USA Inc. +# All Rights Reserved. +# +# 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. + +from openstack_dashboard.dashboards.project.networks.ports.extensions.\ + allowed_address_pairs import views as project_views +from openstack_dashboard.dashboards.admin.networks.ports.extensions.\ + allowed_address_pairs import forms as admin_forms + + +class AddAllowedAddressPair(project_views.AddAllowedAddressPair): + form_class = admin_forms.AddAllowedAddressPairForm + submit_url = "horizon:admin:networks:ports:addallowedaddresspairs" + success_url = 'horizon:admin:networks:ports:detail' diff --git a/openstack_dashboard/dashboards/admin/networks/ports/tabs.py b/openstack_dashboard/dashboards/admin/networks/ports/tabs.py index 80a0f194c..1e171643f 100644 --- a/openstack_dashboard/dashboards/admin/networks/ports/tabs.py +++ b/openstack_dashboard/dashboards/admin/networks/ports/tabs.py @@ -14,6 +14,8 @@ from openstack_dashboard.dashboards.project.networks.ports \ import tabs as project_tabs +from openstack_dashboard.dashboards.project.networks.ports.extensions. \ + allowed_address_pairs import tabs as addr_pairs_tabs class OverviewTab(project_tabs.OverviewTab): @@ -21,4 +23,4 @@ class OverviewTab(project_tabs.OverviewTab): class PortDetailTabs(project_tabs.PortDetailTabs): - tabs = (OverviewTab,) + tabs = (OverviewTab, addr_pairs_tabs.AllowedAddressPairsTab) diff --git a/openstack_dashboard/dashboards/admin/networks/ports/tests.py b/openstack_dashboard/dashboards/admin/networks/ports/tests.py index a36ee3f80..e4d185a15 100644 --- a/openstack_dashboard/dashboards/admin/networks/ports/tests.py +++ b/openstack_dashboard/dashboards/admin/networks/ports/tests.py @@ -49,6 +49,9 @@ class NetworkPortTests(test.BaseAdminViewTests): api.neutron.is_extension_supported(IsA(http.HttpRequest), 'mac-learning')\ .MultipleTimes().AndReturn(mac_learning) + api.neutron.is_extension_supported(IsA(http.HttpRequest), + 'allowed-address-pairs') \ + .MultipleTimes().AndReturn(False) api.neutron.network_get(IsA(http.HttpRequest), network_id)\ .AndReturn(self.networks.first()) self.mox.ReplayAll() diff --git a/openstack_dashboard/dashboards/admin/networks/ports/urls.py b/openstack_dashboard/dashboards/admin/networks/ports/urls.py index 93952b859..9ca905600 100644 --- a/openstack_dashboard/dashboards/admin/networks/ports/urls.py +++ b/openstack_dashboard/dashboards/admin/networks/ports/urls.py @@ -15,10 +15,15 @@ from django.conf.urls import url from openstack_dashboard.dashboards.admin.networks.ports import views +from openstack_dashboard.dashboards.admin.networks.ports.extensions. \ + allowed_address_pairs import views as addr_pairs_views PORTS = r'^(?P[^/]+)/%s$' urlpatterns = [ - url(PORTS % 'detail', views.DetailView.as_view(), name='detail') + url(PORTS % 'detail', views.DetailView.as_view(), name='detail'), + url(PORTS % 'addallowedaddresspairs', + addr_pairs_views.AddAllowedAddressPair.as_view(), + name='addallowedaddresspairs'), ] diff --git a/openstack_dashboard/dashboards/project/networks/ports/extensions/__init__.py b/openstack_dashboard/dashboards/project/networks/ports/extensions/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openstack_dashboard/dashboards/project/networks/ports/extensions/allowed_address_pairs/__init__.py b/openstack_dashboard/dashboards/project/networks/ports/extensions/allowed_address_pairs/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openstack_dashboard/dashboards/project/networks/ports/extensions/allowed_address_pairs/forms.py b/openstack_dashboard/dashboards/project/networks/ports/extensions/allowed_address_pairs/forms.py new file mode 100644 index 000000000..5494d11c1 --- /dev/null +++ b/openstack_dashboard/dashboards/project/networks/ports/extensions/allowed_address_pairs/forms.py @@ -0,0 +1,75 @@ +# Copyright 2015, Alcatel-Lucent USA Inc. +# All Rights Reserved. +# +# 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.core import validators +from django.utils.translation import ugettext_lazy as _ + +from horizon import exceptions +from horizon import forms +from horizon import messages + +from openstack_dashboard import api + +LOG = logging.getLogger(__name__) + +validate_mac = validators.RegexValidator(r'([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}', + _("Invalid MAC Address format"), + code="invalid_mac") + + +class AddAllowedAddressPairForm(forms.SelfHandlingForm): + ip = forms.IPField(label=_("IP Address or CIDR"), + help_text=_("A single IP Address or CIDR"), + version=forms.IPv4 | forms.IPv6, + mask=True) + mac = forms.CharField(label=_("MAC Address"), + help_text=_("A valid MAC Address"), + validators=[validate_mac], + required=False) + failure_url = 'horizon:project:networks:ports:detail' + + def clean(self): + cleaned_data = super(AddAllowedAddressPairForm, self).clean() + if '/' not in self.data['ip']: + cleaned_data['ip'] = self.data['ip'] + return cleaned_data + + def handle(self, request, data): + port_id = self.initial['port_id'] + try: + port = api.neutron.port_get(request, port_id) + + current = port.get('allowed_address_pairs', []) + current = [pair.to_dict() for pair in current] + pair = {'ip_address': data['ip']} + if data['mac']: + pair['mac_address'] = data['mac'] + current.append(pair) + port = api.neutron.port_update(request, port_id, + allowed_address_pairs=current) + msg = _('Port %s was successfully updated.') % port_id + messages.success(request, msg) + return port + except Exception as e: + LOG.error('Failed to update port %(port_id)s: %(reason)s', + {'port_id': port_id, 'reason': e}) + msg = _('Failed to update port "%s".') % port_id + args = (self.initial.get('port_id'),) + redirect = reverse(self.failure_url, args=args) + exceptions.handle(request, msg, redirect=redirect) + return False diff --git a/openstack_dashboard/dashboards/project/networks/ports/extensions/allowed_address_pairs/tables.py b/openstack_dashboard/dashboards/project/networks/ports/extensions/allowed_address_pairs/tables.py new file mode 100644 index 000000000..461f97772 --- /dev/null +++ b/openstack_dashboard/dashboards/project/networks/ports/extensions/allowed_address_pairs/tables.py @@ -0,0 +1,96 @@ +# Copyright 2015, Alcatel-Lucent USA Inc. +# All Rights Reserved. +# +# 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 django.utils.translation import ungettext_lazy + +from openstack_dashboard import api +from openstack_dashboard import policy + + +from horizon import exceptions +from horizon import tables + + +LOG = logging.getLogger(__name__) + + +class AddAllowedAddressPair(policy.PolicyTargetMixin, tables.LinkAction): + name = "AddAllowedAddressPair" + verbose_name = _("Add Allowed Address Pair") + url = "horizon:project:networks:ports:addallowedaddresspairs" + classes = ("ajax-modal",) + icon = "plus" + policy_rules = (("network", "update_port"),) + + def get_link_url(self, port=None): + if port: + return reverse(self.url, args=(port.id,)) + else: + return reverse(self.url, args=(self.table.kwargs.get('port_id'),)) + + +class DeleteAllowedAddressPair(tables.DeleteAction): + @staticmethod + def action_present(count): + return ungettext_lazy( + u"Delete", + u"Delete", + count + ) + + @staticmethod + def action_past(count): + return ungettext_lazy( + u"Deleted address pair", + u"Deleted address pairs", + count + ) + + def delete(self, request, ip_address): + try: + port_id = self.table.kwargs['port_id'] + port = api.neutron.port_get(request, port_id) + pairs = port.get('allowed_address_pairs', []) + pairs = [pair for pair in pairs + if pair['ip_address'] != ip_address] + pairs = [pair.to_dict() for pair in pairs] + api.neutron.port_update(request, port_id, + allowed_address_pairs=pairs) + except Exception as e: + LOG.error('Failed to update port %(port_id)s: %(reason)s', + {'port_id': port_id, 'reason': e}) + redirect = reverse("horizon:project:networks:ports:detail", + args=(port_id,)) + exceptions.handle(request, _('Failed to update port %s') % port_id, + redirect=redirect) + + +class AllowedAddressPairsTable(tables.DataTable): + IP = tables.Column("ip_address", + verbose_name=_("IP Address or CIDR")) + mac = tables.Column('mac_address', verbose_name=_("MAC Address")) + + def get_object_display(self, address_pair): + return address_pair['ip_address'] + + class Meta(object): + name = "allowed_address_pairs" + verbose_name = _("Allowed Address Pairs") + row_actions = (DeleteAllowedAddressPair,) + table_actions = (AddAllowedAddressPair, DeleteAllowedAddressPair) diff --git a/openstack_dashboard/dashboards/project/networks/ports/extensions/allowed_address_pairs/tabs.py b/openstack_dashboard/dashboards/project/networks/ports/extensions/allowed_address_pairs/tabs.py new file mode 100644 index 000000000..1114b5cf0 --- /dev/null +++ b/openstack_dashboard/dashboards/project/networks/ports/extensions/allowed_address_pairs/tabs.py @@ -0,0 +1,51 @@ +# Copyright 2015, Alcatel-Lucent USA Inc. +# All Rights Reserved. +# +# 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.utils.translation import ugettext_lazy as _ + +from horizon import tabs + +from openstack_dashboard import api +from openstack_dashboard.dashboards.project.networks.ports.extensions.\ + allowed_address_pairs import tables as addr_pairs_tables + + +LOG = logging.getLogger(__name__) + + +class AllowedAddressPairsTab(tabs.TableTab): + table_classes = (addr_pairs_tables.AllowedAddressPairsTable,) + name = _("Allowed Address Pairs") + slug = "allowed_address_pairs" + template_name = "horizon/common/_detail_table.html" + + def allowed(self, request): + port = self.tab_group.kwargs['port'] + if not port or not port.get('port_security_enabled', True): + return False + + try: + return api.neutron.is_extension_supported(request, + "allowed-address-pairs") + except Exception as e: + LOG.error("Failed to check if Neutron allowed-address-pairs " + "extension is supported: %s", e) + return False + + def get_allowed_address_pairs_data(self): + port = self.tab_group.kwargs['port'] + return port.get('allowed_address_pairs', []) diff --git a/openstack_dashboard/dashboards/project/networks/ports/extensions/allowed_address_pairs/views.py b/openstack_dashboard/dashboards/project/networks/ports/extensions/allowed_address_pairs/views.py new file mode 100644 index 000000000..fff4ec7a6 --- /dev/null +++ b/openstack_dashboard/dashboards/project/networks/ports/extensions/allowed_address_pairs/views.py @@ -0,0 +1,51 @@ +# Copyright 2015, Alcatel-Lucent USA Inc. +# All Rights Reserved. +# +# 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.extensions.\ + allowed_address_pairs import forms as addr_pairs_forms + +LOG = logging.getLogger(__name__) + + +class AddAllowedAddressPair(forms.ModalFormView): + form_class = addr_pairs_forms.AddAllowedAddressPairForm + form_id = "addallowedaddresspair_form" + modal_header = _("Add allowed address pair") + template_name = 'project/networks/ports/add_addresspair.html' + context_object_name = 'port' + submit_label = _("Submit") + submit_url = "horizon:project:networks:ports:addallowedaddresspairs" + success_url = 'horizon:project:networks:ports:detail' + page_title = _("Add allowed address pair") + + def get_success_url(self): + return reverse(self.success_url, args=(self.kwargs['port_id'],)) + + def get_context_data(self, **kwargs): + context = super(AddAllowedAddressPair, self).get_context_data(**kwargs) + context["port_id"] = self.kwargs['port_id'] + context['submit_url'] = reverse(self.submit_url, + args=(self.kwargs['port_id'],)) + return context + + def get_initial(self): + return {'port_id': self.kwargs['port_id']} diff --git a/openstack_dashboard/dashboards/project/networks/ports/tabs.py b/openstack_dashboard/dashboards/project/networks/ports/tabs.py index 5e12e0d1a..faf0811bc 100644 --- a/openstack_dashboard/dashboards/project/networks/ports/tabs.py +++ b/openstack_dashboard/dashboards/project/networks/ports/tabs.py @@ -16,6 +16,9 @@ from django.utils.translation import ugettext_lazy as _ from horizon import tabs +from openstack_dashboard.dashboards.project.networks.ports.extensions. \ + allowed_address_pairs import tabs as addr_pairs_tabs + class OverviewTab(tabs.Tab): name = _("Overview") @@ -29,4 +32,5 @@ class OverviewTab(tabs.Tab): class PortDetailTabs(tabs.TabGroup): slug = "port_details" - tabs = (OverviewTab,) + tabs = (OverviewTab, addr_pairs_tabs.AllowedAddressPairsTab) + sticky = True diff --git a/openstack_dashboard/dashboards/project/networks/ports/tests.py b/openstack_dashboard/dashboards/project/networks/ports/tests.py index 8d7bc048b..1dd2bb54d 100644 --- a/openstack_dashboard/dashboards/project/networks/ports/tests.py +++ b/openstack_dashboard/dashboards/project/networks/ports/tests.py @@ -13,6 +13,8 @@ # License for the specific language governing permissions and limitations # under the License. +import copy + from django.core.urlresolvers import reverse from django import http @@ -48,12 +50,12 @@ class NetworkPortTests(test.TestCase): .AndReturn(self.ports.first()) api.neutron.is_extension_supported(IsA(http.HttpRequest), 'mac-learning')\ - .AndReturn(mac_learning) + .MultipleTimes().AndReturn(mac_learning) api.neutron.network_get(IsA(http.HttpRequest), network_id)\ .AndReturn(self.networks.first()) api.neutron.is_extension_supported(IsA(http.HttpRequest), - 'mac-learning')\ - .AndReturn(mac_learning) + 'allowed-address-pairs')\ + .MultipleTimes().AndReturn(False) self.mox.ReplayAll() res = self.client.get(reverse(DETAIL_URL, args=[port.id])) @@ -201,3 +203,123 @@ 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', 'network_get', + 'is_extension_supported',)}) + def test_allowed_address_pair_detail(self): + port = self.ports.first() + network = self.networks.first() + api.neutron.port_get(IsA(http.HttpRequest), port.id) \ + .AndReturn(self.ports.first()) + api.neutron.is_extension_supported(IsA(http.HttpRequest), + 'allowed-address-pairs') \ + .MultipleTimes().AndReturn(True) + api.neutron.is_extension_supported(IsA(http.HttpRequest), + 'mac-learning') \ + .MultipleTimes().AndReturn(False) + api.neutron.network_get(IsA(http.HttpRequest), network.id)\ + .AndReturn(network) + self.mox.ReplayAll() + + res = self.client.get(reverse('horizon:project:networks:ports:detail', + args=[port.id])) + + self.assertTemplateUsed(res, 'horizon/common/_detail.html') + self.assertEqual(res.context['port'].id, port.id) + address_pairs = res.context['allowed_address_pairs_table'].data + self.assertItemsEqual(port.allowed_address_pairs, address_pairs) + + @test.create_stubs({api.neutron: ('port_get', 'port_update')}) + def test_port_add_allowed_address_pair(self): + detail_path = 'horizon:project:networks:ports:detail' + + pre_port = self.ports.first() + post_port = copy.deepcopy(pre_port) + pair = {'ip_address': '179.0.0.201', + 'mac_address': 'fa:16:4e:7a:7b:18'} + post_port['allowed_address_pairs'].insert( + 1, api.neutron.PortAllowedAddressPair(pair)) + + api.neutron.port_get(IsA(http.HttpRequest), pre_port.id) \ + .MultipleTimes().AndReturn(pre_port) + + update_pairs = post_port['allowed_address_pairs'] + update_pairs = [p.to_dict() for p in update_pairs] + params = {'allowed_address_pairs': update_pairs} + port_update = api.neutron.port_update(IsA(http.HttpRequest), + pre_port.id, **params) + port_update.AndReturn({'port': post_port}) + self.mox.ReplayAll() + + form_data = {'ip': pair['ip_address'], 'mac': pair['mac_address'], + 'port_id': pre_port.id} + url = reverse('horizon:project:networks:ports:addallowedaddresspairs', + args=[pre_port.id]) + res = self.client.post(url, form_data) + self.assertNoFormErrors(res) + detail_url = reverse(detail_path, args=[pre_port.id]) + self.assertRedirectsNoFollow(res, detail_url) + self.assertMessageCount(success=1) + + def test_port_add_allowed_address_pair_incorrect_mac(self): + pre_port = self.ports.first() + pair = {'ip_address': '179.0.0.201', + 'mac_address': 'incorrect'} + form_data = {'ip': pair['ip_address'], 'mac': pair['mac_address'], + 'port_id': pre_port.id} + url = reverse('horizon:project:networks:ports:addallowedaddresspairs', + args=[pre_port.id]) + res = self.client.post(url, form_data) + self.assertFormErrors(res, 1) + self.assertContains(res, "Invalid MAC Address format") + + def test_port_add_allowed_address_pair_incorrect_ip(self): + pre_port = self.ports.first() + pair = {'ip_address': 'incorrect', + 'mac_address': 'fa:16:4e:7a:7b:18'} + form_data = {'ip': pair['ip_address'], 'mac': pair['mac_address'], + 'port_id': pre_port.id} + url = reverse('horizon:project:networks:ports:addallowedaddresspairs', + args=[pre_port.id]) + res = self.client.post(url, form_data) + self.assertFormErrors(res, 1) + self.assertContains(res, "Incorrect format for IP address") + + @test.create_stubs({api.neutron: ('port_get', 'port_update', + 'is_extension_supported',)}) + def test_port_remove_allowed_address_pair(self): + detail_path = 'horizon:project:networks:ports:detail' + + pre_port = self.ports.first() + post_port = copy.deepcopy(pre_port) + pair = post_port['allowed_address_pairs'].pop() + + # Update will do get and update + api.neutron.port_get(IsA(http.HttpRequest), pre_port.id) \ + .AndReturn(pre_port) + + params = {'allowed_address_pairs': post_port['allowed_address_pairs']} + api.neutron.port_update(IsA(http.HttpRequest), + pre_port.id, **params) \ + .AndReturn({'port': post_port}) + + # After update the detail page is loaded + api.neutron.is_extension_supported(IsA(http.HttpRequest), + 'mac-learning') \ + .MultipleTimes().AndReturn(False) + api.neutron.is_extension_supported(IsA(http.HttpRequest), + 'allowed-address-pairs') \ + .MultipleTimes().AndReturn(True) + api.neutron.port_get(IsA(http.HttpRequest), pre_port.id) \ + .AndReturn(post_port) + + self.mox.ReplayAll() + + pair_ip = pair['ip_address'] + form_data = {'action': 'allowed_address_pairs__delete__%s' % pair_ip} + url = reverse(detail_path, args=[pre_port.id]) + + res = self.client.post(url, form_data) + self.assertNoFormErrors(res) + self.assertRedirectsNoFollow(res, url) + self.assertMessageCount(success=1) diff --git a/openstack_dashboard/dashboards/project/networks/ports/urls.py b/openstack_dashboard/dashboards/project/networks/ports/urls.py index c02db8c2f..77f019e77 100644 --- a/openstack_dashboard/dashboards/project/networks/ports/urls.py +++ b/openstack_dashboard/dashboards/project/networks/ports/urls.py @@ -15,10 +15,15 @@ from django.conf.urls import url from openstack_dashboard.dashboards.project.networks.ports import views +from openstack_dashboard.dashboards.project.networks.ports.extensions. \ + allowed_address_pairs import views as addr_pairs_views PORTS = r'^(?P[^/]+)/%s$' urlpatterns = [ url(PORTS % 'detail', views.DetailView.as_view(), name='detail'), + url(PORTS % 'addallowedaddresspairs', + addr_pairs_views.AddAllowedAddressPair.as_view(), + name='addallowedaddresspairs') ] diff --git a/openstack_dashboard/dashboards/project/networks/ports/views.py b/openstack_dashboard/dashboards/project/networks/ports/views.py index 61107bed1..1a079dd74 100644 --- a/openstack_dashboard/dashboards/project/networks/ports/views.py +++ b/openstack_dashboard/dashboards/project/networks/ports/views.py @@ -34,7 +34,7 @@ STATUS_DICT = dict(project_tables.STATUS_DISPLAY_CHOICES) VNIC_TYPES = dict(project_forms.VNIC_TYPES) -class DetailView(tabs.TabView): +class DetailView(tabs.TabbedTableView): tab_group_class = project_tabs.PortDetailTabs template_name = 'horizon/common/_detail.html' page_title = "{{ port.name|default:port.id }}" diff --git a/openstack_dashboard/dashboards/project/networks/templates/networks/ports/_add_addresspair.html b/openstack_dashboard/dashboards/project/networks/templates/networks/ports/_add_addresspair.html new file mode 100644 index 000000000..47a4c9750 --- /dev/null +++ b/openstack_dashboard/dashboards/project/networks/templates/networks/ports/_add_addresspair.html @@ -0,0 +1,9 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} + +{% block modal-body-right %} +

{% trans "Description:" %}

+

+ {% trans "Add an allowed address pair for this port. This will allow multiple MAC/IP address (range) pairs to pass through this port."%} +

+{% endblock %} diff --git a/openstack_dashboard/dashboards/project/networks/templates/networks/ports/add_addresspair.html b/openstack_dashboard/dashboards/project/networks/templates/networks/ports/add_addresspair.html new file mode 100644 index 000000000..8862a7f72 --- /dev/null +++ b/openstack_dashboard/dashboards/project/networks/templates/networks/ports/add_addresspair.html @@ -0,0 +1,7 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Add allowed address pair" %}{% endblock %} + +{% block main %} + {% include 'project/networks/ports/_add_addresspair.html' %} +{% endblock %} diff --git a/openstack_dashboard/test/test_data/neutron_data.py b/openstack_dashboard/test/test_data/neutron_data.py index 56926e15b..743d4db0c 100644 --- a/openstack_dashboard/test/test_data/neutron_data.py +++ b/openstack_dashboard/test/test_data/neutron_data.py @@ -169,7 +169,10 @@ def data(TEST): 'status': 'ACTIVE', 'tenant_id': network_dict['tenant_id'], 'binding:vnic_type': 'normal', - 'binding:host_id': 'host'} + 'binding:host_id': 'host', + 'allowed_address_pairs': [{'ip_address': '174.0.0.201', + 'mac_address': 'fa:16:3e:7a:7b:18'}] + } TEST.api_ports.add(port_dict) TEST.ports.add(neutron.Port(port_dict)) diff --git a/releasenotes/notes/bp-port-allowed-address-pairs-extension-a05c3a864f494b0c.yaml b/releasenotes/notes/bp-port-allowed-address-pairs-extension-a05c3a864f494b0c.yaml new file mode 100644 index 000000000..1f76b90c8 --- /dev/null +++ b/releasenotes/notes/bp-port-allowed-address-pairs-extension-a05c3a864f494b0c.yaml @@ -0,0 +1,6 @@ +--- +features: + - The port-details page has a new tab for managing Allowed Address Pairs. + This tab and its features will only be available when this extension is + active in Neutron. The Allowed Address Pairs tab will enable creating, + deleting, and listing address pairs for the current port. \ No newline at end of file