Add Port-Create in Project Dashboard

Gives end-users the ability to create and delete ports in their
networks. The functionality will be implemented into the project
network details table.

Following the discussions in the bug discussion.
This functionality will be enabled/disabled via policy.

Change-Id: I560b42b94acb6a2424fbc9b574b6e376c34ac9ee
Implements Blueprint: network-ports-tenant
Closes-Bug: #1399252
Co-Authored-By: kenji-i<ken-ishii@sx.jp.nec.com>
This commit is contained in:
Ankur Gupta 2016-05-23 22:19:03 -05:00 committed by Kenji Ishii
parent f716d559ad
commit b327515253
11 changed files with 472 additions and 130 deletions

View File

@ -32,78 +32,18 @@ VNIC_TYPES = [('normal', _('Normal')), ('direct', _('Direct')),
('macvtap', _('MacVTap'))]
class CreatePort(forms.SelfHandlingForm):
network_name = forms.CharField(label=_("Network Name"),
widget=forms.TextInput(
attrs={'readonly': 'readonly'}),
required=False)
network_id = forms.CharField(label=_("Network ID"),
widget=forms.TextInput(
attrs={'readonly': 'readonly'}))
name = forms.CharField(max_length=255,
label=_("Name"),
required=False)
admin_state = forms.ThemableChoiceField(choices=[('True', _('UP')),
('False', _('DOWN'))],
label=_("Admin State"))
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)
class CreatePort(project_forms.CreatePort):
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)
specify_ip = forms.ThemableChoiceField(
label=_("Specify IP address or subnet"),
help_text=_("To specify a subnet or a fixed IP, select any options."),
initial=False,
required=False,
choices=[('', _("Unspecified")),
('subnet_id', _("Subnet")),
('fixed_ip', _("Fixed IP Address"))],
widget=forms.Select(attrs={
'class': 'switchable',
'data-slug': 'specify_ip',
}))
subnet_id = forms.ThemableChoiceField(
label=_("Subnet"),
required=False,
widget=forms.Select(attrs={
'class': 'switched',
'data-switch-on': 'specify_ip',
'data-specify_ip-subnet_id': _('Subnet'),
}))
fixed_ip = forms.IPField(
label=_("Fixed IP Address"),
required=False,
help_text=_("Specify the subnet IP address for the new port"),
version=forms.IPv4 | forms.IPv6,
widget=forms.TextInput(attrs={
'class': 'switched',
'data-switch-on': 'specify_ip',
'data-specify_ip-fixed_ip': _('Fixed IP Address'),
}))
failure_url = 'horizon:admin:networks:detail'
def __init__(self, request, *args, **kwargs):
super(CreatePort, self).__init__(request, *args, **kwargs)
# prepare subnet choices and input area for each subnet
subnet_choices = self._get_subnet_choices(kwargs['initial'])
if subnet_choices:
subnet_choices.insert(0, ('', _("Select a subnet")))
self.fields['subnet_id'].choices = subnet_choices
else:
self.fields['specify_ip'].widget = forms.HiddenInput()
self.fields['subnet_id'].widget = forms.HiddenInput()
self.fields['fixed_ip'].widget = forms.HiddenInput()
try:
if api.neutron.is_extension_supported(request, 'binding'):
neutron_settings = getattr(settings,
@ -140,16 +80,6 @@ class CreatePort(forms.SelfHandlingForm):
msg = _("Unable to retrieve MAC learning state")
exceptions.handle(self.request, msg)
def _get_subnet_choices(self, kwargs):
try:
network_id = kwargs['network_id']
network = api.neutron.network_get(self.request, network_id)
except Exception:
return []
return [(subnet.id, '%s %s' % (subnet.name_or_id, subnet.cidr))
for subnet in network.subnets]
def handle(self, request, data):
try:
# We must specify tenant_id of the network which a subnet is

View File

@ -14,65 +14,24 @@
import logging
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ungettext_lazy
from horizon import exceptions
from horizon import tables
from openstack_dashboard import api
from openstack_dashboard.dashboards.project.networks.ports import \
tables as project_tables
from openstack_dashboard.dashboards.project.networks.ports.tabs \
import PortsTab as project_port_tab
from openstack_dashboard import policy
LOG = logging.getLogger(__name__)
class DeletePort(policy.PolicyTargetMixin, tables.DeleteAction):
@staticmethod
def action_present(count):
return ungettext_lazy(
u"Delete Port",
u"Delete Ports",
count
)
@staticmethod
def action_past(count):
return ungettext_lazy(
u"Deleted Port",
u"Deleted Ports",
count
)
policy_rules = (("network", "delete_port"),)
def delete(self, request, obj_id):
try:
api.neutron.port_delete(request, obj_id)
except Exception as e:
msg = _('Failed to delete port: %s') % e
LOG.info(msg)
network_id = self.table.kwargs['network_id']
redirect = reverse('horizon:admin:networks:detail',
args=[network_id])
exceptions.handle(request, msg, redirect=redirect)
class DeletePort(project_tables.DeletePort):
failure_url = "horizon:admin:networks:detail"
class CreatePort(tables.LinkAction):
name = "create"
verbose_name = _("Create Port")
class CreatePort(project_tables.CreatePort):
url = "horizon:admin:networks:addport"
classes = ("ajax-modal",)
icon = "plus"
policy_rules = (("network", "create_port"),)
def get_link_url(self, datum=None):
network_id = self.table.kwargs['network_id']
return reverse(self.url, args=(network_id,))
class UpdatePort(project_tables.UpdatePort):

View File

@ -89,12 +89,13 @@ class NetworkPortTests(test.BaseAdminViewTests):
api.neutron.network_get(IsA(http.HttpRequest),
network.id)\
.AndReturn(self.networks.first())
api.neutron.is_extension_supported(IsA(http.HttpRequest),
'binding')\
.AndReturn(binding)
api.neutron.is_extension_supported(IsA(http.HttpRequest),
'mac-learning')\
.AndReturn(mac_learning)
api.neutron.is_extension_supported(IsA(http.HttpRequest),
'binding')\
.AndReturn(binding)
self.mox.ReplayAll()
url = reverse('horizon:admin:networks:addport',
@ -127,12 +128,12 @@ class NetworkPortTests(test.BaseAdminViewTests):
api.neutron.network_get(IsA(http.HttpRequest),
network.id)\
.AndReturn(self.networks.first())
api.neutron.is_extension_supported(IsA(http.HttpRequest),
'binding')\
.AndReturn(binding)
api.neutron.is_extension_supported(IsA(http.HttpRequest),
'mac-learning')\
.AndReturn(mac_learning)
api.neutron.is_extension_supported(IsA(http.HttpRequest),
'binding') \
.AndReturn(binding)
extension_kwargs = {}
if binding:
extension_kwargs['binding__vnic_type'] = \
@ -185,9 +186,6 @@ class NetworkPortTests(test.BaseAdminViewTests):
api.neutron.network_get(IsA(http.HttpRequest),
network.id)\
.AndReturn(self.networks.first())
api.neutron.is_extension_supported(IsA(http.HttpRequest),
'binding')\
.AndReturn(True)
api.neutron.is_extension_supported(IsA(http.HttpRequest),
'mac-learning')\
.AndReturn(True)
@ -252,12 +250,12 @@ class NetworkPortTests(test.BaseAdminViewTests):
api.neutron.network_get(IsA(http.HttpRequest),
network.id)\
.AndReturn(self.networks.first())
api.neutron.is_extension_supported(IsA(http.HttpRequest),
'binding')\
.AndReturn(binding)
api.neutron.is_extension_supported(IsA(http.HttpRequest),
'mac-learning')\
.AndReturn(mac_learning)
api.neutron.is_extension_supported(IsA(http.HttpRequest),
'binding') \
.AndReturn(binding)
extension_kwargs = {}
if binding:
extension_kwargs['binding__vnic_type'] = port.binding__vnic_type
@ -311,7 +309,7 @@ class NetworkPortTests(test.BaseAdminViewTests):
port.id)\
.AndReturn(port)
api.neutron.is_extension_supported(IsA(http.HttpRequest),
'binding')\
'binding') \
.AndReturn(binding)
api.neutron.is_extension_supported(IsA(http.HttpRequest),
'mac-learning')\

View File

@ -30,6 +30,123 @@ VNIC_TYPES = [('normal', _('Normal')), ('direct', _('Direct')),
('macvtap', _('MacVTap'))]
class CreatePort(forms.SelfHandlingForm):
network_name = forms.CharField(label=_("Network Name"),
widget=forms.TextInput(
attrs={'readonly': 'readonly'}),
required=False)
network_id = forms.CharField(label=_("Network ID"),
widget=forms.TextInput(
attrs={'readonly': 'readonly'}))
name = forms.CharField(max_length=255,
label=_("Name"),
required=False)
admin_state = forms.ChoiceField(choices=[('True', _('UP')),
('False', _('DOWN'))],
label=_("Admin State"))
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=_("Owner of the device attached to the port"),
required=False)
specify_ip = forms.ThemableChoiceField(
label=_("Specify IP address or subnet"),
help_text=_("To specify a subnet or a fixed IP, select any options."),
initial=False,
required=False,
choices=[('', _("Unspecified")),
('subnet_id', _("Subnet")),
('fixed_ip', _("Fixed IP Address"))],
widget=forms.Select(attrs={
'class': 'switchable',
'data-slug': 'specify_ip',
}))
subnet_id = forms.ThemableChoiceField(
label=_("Subnet"),
required=False,
widget=forms.Select(attrs={
'class': 'switched',
'data-switch-on': 'specify_ip',
'data-specify_ip-subnet_id': _('Subnet'),
}))
fixed_ip = forms.IPField(
label=_("Fixed IP Address"),
required=False,
help_text=_("Specify the subnet IP address for the new port"),
version=forms.IPv4 | forms.IPv6,
widget=forms.TextInput(attrs={
'class': 'switched',
'data-switch-on': 'specify_ip',
'data-specify_ip-fixed_ip': _('Fixed IP Address'),
}))
failure_url = 'horizon:project:networks:detail'
def __init__(self, request, *args, **kwargs):
super(CreatePort, self).__init__(request, *args, **kwargs)
# prepare subnet choices and input area for each subnet
subnet_choices = self._get_subnet_choices(kwargs['initial'])
if subnet_choices:
subnet_choices.insert(0, ('', _("Select a subnet")))
self.fields['subnet_id'].choices = subnet_choices
else:
self.fields['specify_ip'].widget = forms.HiddenInput()
self.fields['subnet_id'].widget = forms.HiddenInput()
self.fields['fixed_ip'].widget = forms.HiddenInput()
if api.neutron.is_extension_supported(request, 'mac-learning'):
self.fields['mac_state'] = forms.BooleanField(
label=_("MAC Learning State"), initial=False, required=False)
def _get_subnet_choices(self, kwargs):
try:
network_id = kwargs['network_id']
network = api.neutron.network_get(self.request, network_id)
except Exception:
return []
return [(subnet.id, '%s %s' % (subnet.name_or_id, subnet.cidr))
for subnet in network.subnets]
def handle(self, request, data):
try:
params = {
'network_id': data['network_id'],
'admin_state_up': data['admin_state'] == 'True',
'name': data['name'],
'device_id': data['device_id'],
'device_owner': data['device_owner']
}
if data.get('specify_ip') == 'subnet_id':
if data.get('subnet_id'):
params['fixed_ips'] = [{"subnet_id": data['subnet_id']}]
elif data.get('specify_ip') == 'fixed_ip':
if data.get('fixed_ip'):
params['fixed_ips'] = [{"ip_address": data['fixed_ip']}]
if data.get('mac_state'):
params['mac_learning_enabled'] = data['mac_state']
port = api.neutron.port_create(request, **params)
if port['name']:
msg = _('Port %s was successfully created.') % port['name']
else:
msg = _('Port %s was successfully created.') % port['id']
LOG.debug(msg)
messages.success(request, msg)
return port
except Exception:
msg = _('Failed to create a port for network %s') \
% data['network_id']
LOG.info(msg)
redirect = reverse(self.failure_url,
args=(data['network_id'],))
exceptions.handle(request, msg, redirect=redirect)
class UpdatePort(forms.SelfHandlingForm):
network_id = forms.CharField(widget=forms.HiddenInput())
port_id = forms.CharField(label=_("ID"),

View File

@ -12,16 +12,22 @@
# License for the specific language governing permissions and limitations
# under the License.
import logging
from django.core.urlresolvers import reverse
from django import template
from django.utils.translation import pgettext_lazy
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ungettext_lazy
from horizon import exceptions
from horizon import tables
from openstack_dashboard import api
from openstack_dashboard import policy
LOG = logging.getLogger(__name__)
def get_fixed_ips(port):
template_name = 'project/networks/ports/_port_ips.html'
@ -64,6 +70,51 @@ STATUS_DISPLAY_CHOICES = (
)
class CreatePort(tables.LinkAction):
name = "create"
verbose_name = _("Create Port")
url = "horizon:project:networks:addport"
classes = ("ajax-modal",)
icon = "plus"
policy_rules = (("network", "create_port"),)
def get_link_url(self, datum=None):
network_id = self.table.kwargs['network_id']
return reverse(self.url, args=(network_id,))
class DeletePort(policy.PolicyTargetMixin, tables.DeleteAction):
@staticmethod
def action_present(count):
return ungettext_lazy(
u"Delete Port",
u"Delete Ports",
count
)
@staticmethod
def action_past(count):
return ungettext_lazy(
u"Deleted Port",
u"Deleted Ports",
count
)
policy_rules = (("network", "delete_port"),)
def delete(self, request, port_id):
failure_url = "horizon:project:networks:detail"
try:
api.neutron.port_delete(request, port_id)
except Exception:
msg = _('Failed to delete port: %s') % port_id
LOG.info(msg)
network_id = self.table.kwargs['network_id']
redirect = reverse(failure_url,
args=[network_id])
exceptions.handle(request, msg, redirect=redirect)
class PortsTable(tables.DataTable):
name = tables.WrappingColumn("name_or_id",
verbose_name=_("Name"),
@ -85,8 +136,8 @@ class PortsTable(tables.DataTable):
class Meta(object):
name = "ports"
verbose_name = _("Ports")
table_actions = (tables.FilterAction,)
row_actions = (UpdatePort,)
table_actions = (tables.FilterAction, CreatePort, DeletePort)
row_actions = (UpdatePort, DeletePort)
hidden_title = False
def __init__(self, request, data=None, needs_form_wrapper=None, **kwargs):

View File

@ -323,3 +323,222 @@ class NetworkPortTests(test.TestCase):
self.assertNoFormErrors(res)
self.assertRedirectsNoFollow(res, url)
self.assertMessageCount(success=1)
@test.create_stubs({api.neutron: ('network_get',
'is_extension_supported',)})
def test_port_create_get(self):
self._test_port_create_get()
@test.create_stubs({api.neutron: ('network_get',
'is_extension_supported',)})
def test_port_create_get_with_mac_learning(self):
self._test_port_create_get(mac_learning=True)
def _test_port_create_get(self, mac_learning=False, binding=False):
network = self.networks.first()
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)
self.mox.ReplayAll()
url = reverse('horizon:project:networks:addport',
args=[network.id])
res = self.client.get(url)
self.assertTemplateUsed(res, 'project/networks/ports/create.html')
@test.create_stubs({api.neutron: ('network_get',
'is_extension_supported',
'port_create',)})
def test_port_create_post(self):
self._test_port_create_post()
@test.create_stubs({api.neutron: ('network_get',
'is_extension_supported',
'port_create',)})
def test_port_create_post_with_mac_learning(self):
self._test_port_create_post(mac_learning=True, binding=False)
def _test_port_create_post(self, mac_learning=False, binding=False):
network = self.networks.first()
port = self.ports.first()
api.neutron.network_get(IsA(http.HttpRequest),
network.id) \
.MultipleTimes().AndReturn(self.networks.first())
api.neutron.is_extension_supported(IsA(http.HttpRequest),
'mac-learning') \
.AndReturn(mac_learning)
extension_kwargs = {}
if binding:
extension_kwargs['binding__vnic_type'] = \
port.binding__vnic_type
if mac_learning:
extension_kwargs['mac_learning_enabled'] = True
api.neutron.port_create(IsA(http.HttpRequest),
tenant_id=network.tenant_id,
network_id=network.id,
name=port.name,
admin_state_up=port.admin_state_up,
device_id=port.device_id,
device_owner=port.device_owner,
fixed_ips=port.fixed_ips,
**extension_kwargs) \
.AndReturn(port)
self.mox.ReplayAll()
form_data = {'network_id': port.network_id,
'network_name': network.name,
'name': port.name,
'admin_state': port.admin_state_up,
'device_id': port.device_id,
'device_owner': port.device_owner,
'specify_ip': 'fixed_ip',
'fixed_ip': port.fixed_ips[0]['ip_address'],
'subnet_id': port.fixed_ips[0]['subnet_id']}
if binding:
form_data['binding__vnic_type'] = port.binding__vnic_type
if mac_learning:
form_data['mac_state'] = True
url = reverse('horizon:project:networks:addport',
args=[port.network_id])
res = self.client.post(url, form_data)
self.assertNoFormErrors(res)
redir_url = reverse(NETWORKS_DETAIL_URL, args=[port.network_id])
self.assertRedirectsNoFollow(res, redir_url)
@test.create_stubs({api.neutron: ('network_get',
'port_create',
'is_extension_supported',)})
def test_port_create_post_exception(self):
self._test_port_create_post_exception()
@test.create_stubs({api.neutron: ('network_get',
'port_create',
'is_extension_supported',)})
def test_port_create_post_exception_with_mac_learning(self):
self._test_port_create_post_exception(mac_learning=True)
def _test_port_create_post_exception(self, mac_learning=False,
binding=False):
network = self.networks.first()
port = self.ports.first()
api.neutron.network_get(IsA(http.HttpRequest),
network.id) \
.MultipleTimes().AndReturn(self.networks.first())
api.neutron.is_extension_supported(IsA(http.HttpRequest),
'mac-learning') \
.AndReturn(mac_learning)
extension_kwargs = {}
if binding:
extension_kwargs['binding__vnic_type'] = port.binding__vnic_type
if mac_learning:
extension_kwargs['mac_learning_enabled'] = True
api.neutron.port_create(IsA(http.HttpRequest),
tenant_id=network.tenant_id,
network_id=network.id,
name=port.name,
admin_state_up=port.admin_state_up,
device_id=port.device_id,
device_owner=port.device_owner,
**extension_kwargs) \
.AndRaise(self.exceptions.neutron)
self.mox.ReplayAll()
form_data = {'network_id': port.network_id,
'network_name': network.name,
'name': port.name,
'admin_state': port.admin_state_up,
'mac_state': True,
'device_id': port.device_id,
'device_owner': port.device_owner,
'specify_ip': 'fixed_ip',
'fixed_ip': port.fixed_ips[0]['ip_address'],
'subnet_id': port.fixed_ips[0]['subnet_id']}
if binding:
form_data['binding__vnic_type'] = port.binding__vnic_type
if mac_learning:
form_data['mac_learning_enabled'] = True
url = reverse('horizon:project:networks:addport',
args=[port.network_id])
res = self.client.post(url, form_data)
self.assertNoFormErrors(res)
redir_url = reverse(NETWORKS_DETAIL_URL, args=[port.network_id])
self.assertRedirectsNoFollow(res, redir_url)
@test.create_stubs({api.neutron: ('port_delete',
'subnet_list',
'port_list',
'is_extension_supported',
'network_get',
'list_dhcp_agent_hosting_networks',)})
def test_port_delete(self):
self._test_port_delete()
@test.create_stubs({api.neutron: ('port_delete',
'subnet_list',
'port_list',
'network_get',
'is_extension_supported',)})
def test_port_delete_with_mac_learning(self):
self._test_port_delete(mac_learning=True)
def _test_port_delete(self, mac_learning=False):
port = self.ports.first()
network_id = port.network_id
api.neutron.port_delete(IsA(http.HttpRequest), port.id)
api.neutron.port_list(IsA(http.HttpRequest), network_id=network_id) \
.AndReturn([self.ports.first()])
api.neutron.is_extension_supported(IsA(http.HttpRequest),
'mac-learning') \
.AndReturn(mac_learning)
self.mox.ReplayAll()
form_data = {'action': 'ports__delete__%s' % port.id}
url = reverse(NETWORKS_DETAIL_URL, args=[network_id])
res = self.client.post(url, form_data)
self.assertRedirectsNoFollow(res, url)
@test.create_stubs({api.neutron: ('port_delete',
'subnet_list',
'port_list',
'is_extension_supported',
'network_get',)})
def test_port_delete_exception(self):
self._test_port_delete_exception()
@test.create_stubs({api.neutron: ('port_delete',
'subnet_list',
'port_list',
'is_extension_supported',
'network_get',)})
def test_port_delete_exception_with_mac_learning(self):
self._test_port_delete_exception(mac_learning=True)
def _test_port_delete_exception(self, mac_learning=False):
port = self.ports.first()
network_id = port.network_id
api.neutron.port_delete(IsA(http.HttpRequest), port.id) \
.AndRaise(self.exceptions.neutron)
api.neutron.port_list(IsA(http.HttpRequest), network_id=network_id) \
.AndReturn([self.ports.first()])
api.neutron.is_extension_supported(IsA(http.HttpRequest),
'mac-learning') \
.AndReturn(mac_learning)
self.mox.ReplayAll()
form_data = {'action': 'ports__delete__%s' % port.id}
url = reverse(NETWORKS_DETAIL_URL, args=[network_id])
res = self.client.post(url, form_data)
self.assertRedirectsNoFollow(res, url)

View File

@ -34,6 +34,45 @@ STATUS_DICT = dict(project_tables.STATUS_DISPLAY_CHOICES)
VNIC_TYPES = dict(project_forms.VNIC_TYPES)
class CreateView(forms.ModalFormView):
form_class = project_forms.CreatePort
form_id = "create_port_form"
modal_header = _("Create Port")
submit_label = _("Create Port")
submit_url = "horizon:project:networks:addport"
page_title = _("Create Port")
template_name = 'project/networks/ports/create.html'
url = 'horizon:project:networks:detail'
def get_success_url(self):
return reverse(self.url,
args=(self.kwargs['network_id'],))
@memoized.memoized_method
def get_network(self):
try:
network_id = self.kwargs["network_id"]
return api.neutron.network_get(self.request, network_id)
except Exception:
redirect = reverse(self.url,
args=(self.kwargs['network_id'],))
msg = _("Unable to retrieve network.")
exceptions.handle(self.request, msg, redirect=redirect)
def get_context_data(self, **kwargs):
context = super(CreateView, self).get_context_data(**kwargs)
context['network'] = self.get_network()
args = (self.kwargs['network_id'],)
context['submit_url'] = reverse(self.submit_url, args=args)
context['cancel_url'] = reverse(self.url, args=args)
return context
def get_initial(self):
network = self.get_network()
return {"network_id": self.kwargs['network_id'],
"network_name": network.name}
class DetailView(tabs.TabbedTableView):
tab_group_class = project_tabs.PortDetailTabs
template_name = 'horizon/common/_detail.html'

View File

@ -0,0 +1,11 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
{% block modal-body-right %}
<h3>{% trans "Description:" %}</h3>
<p>{% blocktrans %} You can create a port for the network.
If you specify device ID to be attached, the device specified will
be attached to the port created.
{% endblocktrans %}
</p>
{% endblock %}

View File

@ -0,0 +1,7 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Create Port" %}{% endblock %}
{% block main %}
{% include "project/networks/ports/_create.html" %}
{% endblock %}

View File

@ -42,6 +42,8 @@ urlpatterns = [
url(NETWORKS % 'update', views.UpdateView.as_view(), name='update'),
url(NETWORKS % 'subnets/create', subnet_views.CreateView.as_view(),
name='addsubnet'),
url(NETWORKS % 'ports/create',
port_views.CreateView.as_view(), name='addport'),
url(r'^(?P<network_id>[^/]+)/subnets/(?P<subnet_id>[^/]+)/update$',
subnet_views.UpdateView.as_view(), name='editsubnet'),
url(r'^(?P<network_id>[^/]+)/ports/(?P<port_id>[^/]+)/update$',

View File

@ -0,0 +1,9 @@
---
features:
- Gives end-users the ability to create and delete ports in their networks.
The functionality will be implemented into the project network
details table. Following the discussions in the bug discussion.
This functionality will be enabled/disabled via policy.
Blueprint can be found at
[`blueprint network-ports-tenant <https://blueprints.launchpad.net/horizon/+spec/network-ports-tenant>`_]
Bug can be found at [`bug 1399252 <https://bugs.launchpad.net/horizon/+bug/1399252>`_]