Merge "Add allowed address pair extension UI for neutron ports."
This commit is contained in:
commit
091cc15abd
@ -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