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:
@@ -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>`_]
|
Reference in New Issue
Block a user