Merge "Add Port-Create in Project Dashboard"

This commit is contained in:
Jenkins 2017-02-27 14:36:15 +00:00 committed by Gerrit Code Review
commit 5679634fed
11 changed files with 472 additions and 130 deletions
openstack_dashboard/dashboards
admin/networks/ports
project/networks
releasenotes/notes

@ -32,78 +32,18 @@ VNIC_TYPES = [('normal', _('Normal')), ('direct', _('Direct')),
('macvtap', _('MacVTap'))] ('macvtap', _('MacVTap'))]
class CreatePort(forms.SelfHandlingForm): class CreatePort(project_forms.CreatePort):
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)
binding__host_id = forms.CharField( binding__host_id = forms.CharField(
label=_("Binding: Host"), label=_("Binding: Host"),
help_text=_("The ID of the host where the port is allocated. In some " help_text=_("The ID of the host where the port is allocated. In some "
"cases, different implementations can run on different " "cases, different implementations can run on different "
"hosts."), "hosts."),
required=False) 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' failure_url = 'horizon:admin:networks:detail'
def __init__(self, request, *args, **kwargs): def __init__(self, request, *args, **kwargs):
super(CreatePort, self).__init__(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: try:
if api.neutron.is_extension_supported(request, 'binding'): if api.neutron.is_extension_supported(request, 'binding'):
neutron_settings = getattr(settings, neutron_settings = getattr(settings,
@ -140,16 +80,6 @@ class CreatePort(forms.SelfHandlingForm):
msg = _("Unable to retrieve MAC learning state") msg = _("Unable to retrieve MAC learning state")
exceptions.handle(self.request, msg) 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): def handle(self, request, data):
try: try:
# We must specify tenant_id of the network which a subnet is # We must specify tenant_id of the network which a subnet is

@ -14,65 +14,24 @@
import logging import logging
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ungettext_lazy
from horizon import exceptions
from horizon import tables from horizon import tables
from openstack_dashboard import api
from openstack_dashboard.dashboards.project.networks.ports import \ from openstack_dashboard.dashboards.project.networks.ports import \
tables as project_tables tables as project_tables
from openstack_dashboard.dashboards.project.networks.ports.tabs \ from openstack_dashboard.dashboards.project.networks.ports.tabs \
import PortsTab as project_port_tab import PortsTab as project_port_tab
from openstack_dashboard import policy
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
class DeletePort(policy.PolicyTargetMixin, tables.DeleteAction): class DeletePort(project_tables.DeletePort):
@staticmethod failure_url = "horizon:admin:networks:detail"
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 CreatePort(tables.LinkAction): class CreatePort(project_tables.CreatePort):
name = "create"
verbose_name = _("Create Port")
url = "horizon:admin:networks:addport" 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): class UpdatePort(project_tables.UpdatePort):

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

@ -30,6 +30,123 @@ VNIC_TYPES = [('normal', _('Normal')), ('direct', _('Direct')),
('macvtap', _('MacVTap'))] ('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): class UpdatePort(forms.SelfHandlingForm):
network_id = forms.CharField(widget=forms.HiddenInput()) network_id = forms.CharField(widget=forms.HiddenInput())
port_id = forms.CharField(label=_("ID"), port_id = forms.CharField(label=_("ID"),

@ -12,16 +12,22 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import logging
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django import template from django import template
from django.utils.translation import pgettext_lazy from django.utils.translation import pgettext_lazy
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ungettext_lazy
from horizon import exceptions
from horizon import tables from horizon import tables
from openstack_dashboard import api from openstack_dashboard import api
from openstack_dashboard import policy from openstack_dashboard import policy
LOG = logging.getLogger(__name__)
def get_fixed_ips(port): def get_fixed_ips(port):
template_name = 'project/networks/ports/_port_ips.html' 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): class PortsTable(tables.DataTable):
name = tables.WrappingColumn("name_or_id", name = tables.WrappingColumn("name_or_id",
verbose_name=_("Name"), verbose_name=_("Name"),
@ -85,8 +136,8 @@ class PortsTable(tables.DataTable):
class Meta(object): class Meta(object):
name = "ports" name = "ports"
verbose_name = _("Ports") verbose_name = _("Ports")
table_actions = (tables.FilterAction,) table_actions = (tables.FilterAction, CreatePort, DeletePort)
row_actions = (UpdatePort,) row_actions = (UpdatePort, DeletePort)
hidden_title = False hidden_title = False
def __init__(self, request, data=None, needs_form_wrapper=None, **kwargs): def __init__(self, request, data=None, needs_form_wrapper=None, **kwargs):

@ -323,3 +323,222 @@ class NetworkPortTests(test.TestCase):
self.assertNoFormErrors(res) self.assertNoFormErrors(res)
self.assertRedirectsNoFollow(res, url) self.assertRedirectsNoFollow(res, url)
self.assertMessageCount(success=1) 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)

@ -34,6 +34,45 @@ STATUS_DICT = dict(project_tables.STATUS_DISPLAY_CHOICES)
VNIC_TYPES = dict(project_forms.VNIC_TYPES) 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): class DetailView(tabs.TabbedTableView):
tab_group_class = project_tabs.PortDetailTabs tab_group_class = project_tabs.PortDetailTabs
template_name = 'horizon/common/_detail.html' template_name = 'horizon/common/_detail.html'

@ -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 %}

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

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

@ -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>`_]