From b3275152534e2c450cb00fcfcea393bbca44efde Mon Sep 17 00:00:00 2001 From: Ankur Gupta Date: Mon, 23 May 2016 22:19:03 -0500 Subject: [PATCH] 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 --- .../dashboards/admin/networks/ports/forms.py | 72 +----- .../dashboards/admin/networks/ports/tables.py | 47 +--- .../dashboards/admin/networks/ports/tests.py | 24 +- .../project/networks/ports/forms.py | 117 ++++++++++ .../project/networks/ports/tables.py | 55 ++++- .../project/networks/ports/tests.py | 219 ++++++++++++++++++ .../project/networks/ports/views.py | 39 ++++ .../templates/networks/ports/_create.html | 11 + .../templates/networks/ports/create.html | 7 + .../dashboards/project/networks/urls.py | 2 + ...network-ports-tenant-58a9d5ba925f1d3d.yaml | 9 + 11 files changed, 472 insertions(+), 130 deletions(-) create mode 100644 openstack_dashboard/dashboards/project/networks/templates/networks/ports/_create.html create mode 100644 openstack_dashboard/dashboards/project/networks/templates/networks/ports/create.html create mode 100644 releasenotes/notes/bp-network-ports-tenant-58a9d5ba925f1d3d.yaml diff --git a/openstack_dashboard/dashboards/admin/networks/ports/forms.py b/openstack_dashboard/dashboards/admin/networks/ports/forms.py index 521f37a965..22c849571b 100644 --- a/openstack_dashboard/dashboards/admin/networks/ports/forms.py +++ b/openstack_dashboard/dashboards/admin/networks/ports/forms.py @@ -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 diff --git a/openstack_dashboard/dashboards/admin/networks/ports/tables.py b/openstack_dashboard/dashboards/admin/networks/ports/tables.py index 016b077c7b..28c469ad96 100644 --- a/openstack_dashboard/dashboards/admin/networks/ports/tables.py +++ b/openstack_dashboard/dashboards/admin/networks/ports/tables.py @@ -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): diff --git a/openstack_dashboard/dashboards/admin/networks/ports/tests.py b/openstack_dashboard/dashboards/admin/networks/ports/tests.py index c4d5d650df..73fe1f5f03 100644 --- a/openstack_dashboard/dashboards/admin/networks/ports/tests.py +++ b/openstack_dashboard/dashboards/admin/networks/ports/tests.py @@ -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')\ diff --git a/openstack_dashboard/dashboards/project/networks/ports/forms.py b/openstack_dashboard/dashboards/project/networks/ports/forms.py index f373ab4a2f..9085981c8b 100644 --- a/openstack_dashboard/dashboards/project/networks/ports/forms.py +++ b/openstack_dashboard/dashboards/project/networks/ports/forms.py @@ -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"), diff --git a/openstack_dashboard/dashboards/project/networks/ports/tables.py b/openstack_dashboard/dashboards/project/networks/ports/tables.py index 35b8c7b625..5ba3a873b8 100644 --- a/openstack_dashboard/dashboards/project/networks/ports/tables.py +++ b/openstack_dashboard/dashboards/project/networks/ports/tables.py @@ -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): diff --git a/openstack_dashboard/dashboards/project/networks/ports/tests.py b/openstack_dashboard/dashboards/project/networks/ports/tests.py index 1dd2bb54db..e20e4b2c34 100644 --- a/openstack_dashboard/dashboards/project/networks/ports/tests.py +++ b/openstack_dashboard/dashboards/project/networks/ports/tests.py @@ -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) diff --git a/openstack_dashboard/dashboards/project/networks/ports/views.py b/openstack_dashboard/dashboards/project/networks/ports/views.py index aad4d773ec..4fc16c9981 100644 --- a/openstack_dashboard/dashboards/project/networks/ports/views.py +++ b/openstack_dashboard/dashboards/project/networks/ports/views.py @@ -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' diff --git a/openstack_dashboard/dashboards/project/networks/templates/networks/ports/_create.html b/openstack_dashboard/dashboards/project/networks/templates/networks/ports/_create.html new file mode 100644 index 0000000000..bb77d1fea8 --- /dev/null +++ b/openstack_dashboard/dashboards/project/networks/templates/networks/ports/_create.html @@ -0,0 +1,11 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} + +{% block modal-body-right %} +

{% trans "Description:" %}

+

{% 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 %} +

+{% endblock %} diff --git a/openstack_dashboard/dashboards/project/networks/templates/networks/ports/create.html b/openstack_dashboard/dashboards/project/networks/templates/networks/ports/create.html new file mode 100644 index 0000000000..922625d1aa --- /dev/null +++ b/openstack_dashboard/dashboards/project/networks/templates/networks/ports/create.html @@ -0,0 +1,7 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Create Port" %}{% endblock %} + +{% block main %} + {% include "project/networks/ports/_create.html" %} +{% endblock %} diff --git a/openstack_dashboard/dashboards/project/networks/urls.py b/openstack_dashboard/dashboards/project/networks/urls.py index 9ded89eaa9..1006ebc650 100644 --- a/openstack_dashboard/dashboards/project/networks/urls.py +++ b/openstack_dashboard/dashboards/project/networks/urls.py @@ -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[^/]+)/subnets/(?P[^/]+)/update$', subnet_views.UpdateView.as_view(), name='editsubnet'), url(r'^(?P[^/]+)/ports/(?P[^/]+)/update$', diff --git a/releasenotes/notes/bp-network-ports-tenant-58a9d5ba925f1d3d.yaml b/releasenotes/notes/bp-network-ports-tenant-58a9d5ba925f1d3d.yaml new file mode 100644 index 0000000000..898e0320ec --- /dev/null +++ b/releasenotes/notes/bp-network-ports-tenant-58a9d5ba925f1d3d.yaml @@ -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 `_] + Bug can be found at [`bug 1399252 `_]