Add allowed address pair extension UI for neutron ports.
Changed the port detail view to a TabbedTableView where the extra tab is enabled/disabled when the extension is active or not in neutron. Similar to how extensions are handled for routers. If the extension is not active the port detail screen should look the same as it does now. The extra tab has a table of the allowed address pairs (columns: IP address, MAC) with create and delete actions. Change-Id: I07edb1afae5c2004761d1c118a724fb94aaebe3e implements: blueprint port-allowed-address-pairs-extension
This commit is contained in:
parent
dda1eb2cb6
commit
c140149308
@ -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',
|
||||
|
@ -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'
|
@ -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'
|
@ -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)
|
||||
|
@ -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()
|
||||
|
@ -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<port_id>[^/]+)/%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'),
|
||||
]
|
||||
|
@ -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
|
@ -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)
|
@ -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', [])
|
@ -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']}
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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<port_id>[^/]+)/%s$'
|
||||
|
||||
urlpatterns = [
|
||||
url(PORTS % 'detail', views.DetailView.as_view(), name='detail'),
|
||||
url(PORTS % 'addallowedaddresspairs',
|
||||
addr_pairs_views.AddAllowedAddressPair.as_view(),
|
||||
name='addallowedaddresspairs')
|
||||
]
|
||||
|
@ -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 }}"
|
||||
|
@ -0,0 +1,9 @@
|
||||
{% extends "horizon/common/_modal_form.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block modal-body-right %}
|
||||
<h3>{% trans "Description:" %}</h3>
|
||||
<p>
|
||||
{% trans "Add an allowed address pair for this port. This will allow multiple MAC/IP address (range) pairs to pass through this port."%}
|
||||
</p>
|
||||
{% endblock %}
|
@ -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 %}
|
@ -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))
|
||||
|
@ -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.
|
Loading…
Reference in New Issue
Block a user