mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-29 03:46:25 -06:00
Move ServicePort mode definition to dcim
* add service port specific permissions
This commit is contained in:
parent
8360402e0a
commit
f2fef00ed8
@ -1,12 +1,12 @@
|
|||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from ipam.models import IPAddress, ServicePort
|
from ipam.models import IPAddress
|
||||||
from dcim.models import (
|
from dcim.models import (
|
||||||
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, DeviceType,
|
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, DeviceType,
|
||||||
DeviceRole, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer, Module, Platform, PowerOutlet,
|
DeviceRole, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer, Module, Platform, PowerOutlet,
|
||||||
PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackRole, RACK_FACE_FRONT, RACK_FACE_REAR, Site,
|
PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackRole, RACK_FACE_FRONT, RACK_FACE_REAR, Site,
|
||||||
SUBDEVICE_ROLE_CHILD, SUBDEVICE_ROLE_PARENT,
|
SUBDEVICE_ROLE_CHILD, SUBDEVICE_ROLE_PARENT,
|
||||||
)
|
ServicePort)
|
||||||
from extras.api.serializers import CustomFieldSerializer
|
from extras.api.serializers import CustomFieldSerializer
|
||||||
from tenancy.api.serializers import TenantNestedSerializer
|
from tenancy.api.serializers import TenantNestedSerializer
|
||||||
|
|
||||||
|
@ -12,11 +12,10 @@ from django.shortcuts import get_object_or_404
|
|||||||
from dcim.models import (
|
from dcim.models import (
|
||||||
ConsolePort, ConsoleServerPort, Device, DeviceBay, DeviceRole, DeviceType, IFACE_FF_VIRTUAL, Interface,
|
ConsolePort, ConsoleServerPort, Device, DeviceBay, DeviceRole, DeviceType, IFACE_FF_VIRTUAL, Interface,
|
||||||
InterfaceConnection, Manufacturer, Module, Platform, PowerOutlet, PowerPort, Rack, RackGroup, RackRole, Site,
|
InterfaceConnection, Manufacturer, Module, Platform, PowerOutlet, PowerPort, Rack, RackGroup, RackRole, Site,
|
||||||
)
|
ServicePort)
|
||||||
from dcim import filters
|
from dcim import filters
|
||||||
from extras.api.views import CustomFieldModelAPIView
|
from extras.api.views import CustomFieldModelAPIView
|
||||||
from extras.api.renderers import BINDZoneRenderer, FlatJSONRenderer
|
from extras.api.renderers import BINDZoneRenderer, FlatJSONRenderer
|
||||||
from ipam.models import ServicePort
|
|
||||||
from utilities.api import ServiceUnavailable
|
from utilities.api import ServiceUnavailable
|
||||||
from .exceptions import MissingFilterException
|
from .exceptions import MissingFilterException
|
||||||
from . import serializers
|
from . import serializers
|
||||||
|
@ -5,7 +5,7 @@ from django.core.exceptions import ValidationError
|
|||||||
from django.db.models import Count, Q
|
from django.db.models import Count, Q
|
||||||
|
|
||||||
from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
|
from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
|
||||||
from ipam.models import IPAddress, ServicePort
|
from ipam.models import IPAddress
|
||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
from utilities.forms import (
|
from utilities.forms import (
|
||||||
APISelect, add_blank_choice, BootstrapMixin, BulkEditForm, BulkImportForm, CommentField, CSVDataField,
|
APISelect, add_blank_choice, BootstrapMixin, BulkEditForm, BulkImportForm, CommentField, CSVDataField,
|
||||||
@ -18,7 +18,7 @@ from .models import (
|
|||||||
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceRole, DeviceType,
|
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceRole, DeviceType,
|
||||||
Interface, IFACE_FF_CHOICES, IFACE_FF_VIRTUAL, InterfaceConnection, InterfaceTemplate, Manufacturer, Module,
|
Interface, IFACE_FF_CHOICES, IFACE_FF_VIRTUAL, InterfaceConnection, InterfaceTemplate, Manufacturer, Module,
|
||||||
Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, RACK_TYPE_CHOICES, RACK_WIDTH_CHOICES,
|
Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, RACK_TYPE_CHOICES, RACK_WIDTH_CHOICES,
|
||||||
Rack, RackGroup, RackRole, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHILD
|
Rack, RackGroup, RackRole, Site, ServicePort, STATUS_CHOICES, SUBDEVICE_ROLE_CHILD
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Generated by Django 1.10 on 2016-11-22 22:05
|
# Generated by Django 1.10 on 2016-11-25 23:50
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
@ -9,8 +9,8 @@ import django.db.models.deletion
|
|||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('dcim', '0021_add_ff_flexstack'),
|
|
||||||
('ipam', '0010_ipaddress_help_texts'),
|
('ipam', '0010_ipaddress_help_texts'),
|
||||||
|
('dcim', '0021_add_ff_flexstack'),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
@ -203,6 +203,12 @@ RPC_CLIENT_CHOICES = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
SERVICE_PORT_CHOICES = (
|
||||||
|
(6, 'TCP'),
|
||||||
|
(17, 'UDP'),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def order_interfaces(queryset, sql_col, primary_ordering=tuple()):
|
def order_interfaces(queryset, sql_col, primary_ordering=tuple()):
|
||||||
"""
|
"""
|
||||||
Attempt to match interface names by their slot/position identifiers and order according. Matching is done using the
|
Attempt to match interface names by their slot/position identifiers and order according. Matching is done using the
|
||||||
@ -1267,3 +1273,67 @@ class Module(models.Model):
|
|||||||
|
|
||||||
def get_parent_url(self):
|
def get_parent_url(self):
|
||||||
return reverse('dcim:device_inventory', args=[self.device.pk])
|
return reverse('dcim:device_inventory', args=[self.device.pk])
|
||||||
|
|
||||||
|
|
||||||
|
class ServicePort(CreatedUpdatedModel):
|
||||||
|
"""
|
||||||
|
A ServicePort represents a port on a specific IPAddress on which a service is running.
|
||||||
|
The port can be one of 2 predefined protocols - TCP or UDP.
|
||||||
|
A ServicePort is always associated with a specific IPAddress on a Device.
|
||||||
|
|
||||||
|
The combination of IPAddress, Port Number and Port Protocol is always unique for ServicePort.
|
||||||
|
|
||||||
|
If a port number + port protocol combination is already assigned to no specific IPAddress
|
||||||
|
that means it is assigned on all IPs on the device
|
||||||
|
"""
|
||||||
|
|
||||||
|
device = models.ForeignKey('Device', related_name='service_ports', on_delete=models.CASCADE,
|
||||||
|
blank=False, null=False, verbose_name='device')
|
||||||
|
|
||||||
|
ip_address = models.ForeignKey('ipam.IPAddress', related_name='service_ports', on_delete=models.CASCADE,
|
||||||
|
blank=True, null=True, verbose_name='ip_address')
|
||||||
|
protocol = models.PositiveSmallIntegerField(choices=SERVICE_PORT_CHOICES, default=0)
|
||||||
|
|
||||||
|
port = models.PositiveIntegerField()
|
||||||
|
name = models.CharField(max_length=30, blank=False, null=False)
|
||||||
|
description = models.TextField(blank=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['device', 'ip_address', 'port']
|
||||||
|
verbose_name = 'Service Port'
|
||||||
|
verbose_name_plural = 'Service Ports'
|
||||||
|
unique_together = ['device', 'ip_address', 'port', 'protocol']
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
port_protocol = dict(SERVICE_PORT_CHOICES).get(self.protocol)
|
||||||
|
return u'{}/{}'.format(self.port, port_protocol)
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse('dcim:serviceport', args=[self.pk])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def short_description(self):
|
||||||
|
if self.description:
|
||||||
|
return self.description[:30]
|
||||||
|
return None
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
# if port is already assigned to no specific IPAddress
|
||||||
|
# that means it is assigned on all IPs on the device
|
||||||
|
port_assigned_on_all_ips = bool(ServicePort.objects.filter(
|
||||||
|
ip_address__address=None, port=self.port, protocol=self.protocol).exclude(pk=self.id))
|
||||||
|
if port_assigned_on_all_ips:
|
||||||
|
raise ValidationError(
|
||||||
|
'Port already assigned all IPAddresses for this device')
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
super(ServicePort, self).save(*args, **kwargs)
|
||||||
|
|
||||||
|
def to_csv(self):
|
||||||
|
return ','.join([
|
||||||
|
str(self.device_id),
|
||||||
|
self.ip_address,
|
||||||
|
self.port,
|
||||||
|
self.name,
|
||||||
|
self.description,
|
||||||
|
])
|
||||||
|
@ -8,14 +8,13 @@ from django.contrib.auth.decorators import permission_required
|
|||||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
from django.db import transaction
|
|
||||||
from django.db.models import Count
|
from django.db.models import Count
|
||||||
from django.http import HttpResponseRedirect
|
from django.http import HttpResponseRedirect
|
||||||
from django.shortcuts import get_object_or_404, redirect, render
|
from django.shortcuts import get_object_or_404, redirect, render
|
||||||
from django.utils.http import urlencode
|
from django.utils.http import urlencode
|
||||||
from django.views.generic import View
|
from django.views.generic import View
|
||||||
|
|
||||||
from ipam.models import Prefix, IPAddress, VLAN, ServicePort
|
from ipam.models import Prefix, IPAddress, VLAN
|
||||||
from circuits.models import Circuit
|
from circuits.models import Circuit
|
||||||
from extras.models import Graph, TopologyMap, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE
|
from extras.models import Graph, TopologyMap, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE
|
||||||
from utilities.forms import ConfirmationForm
|
from utilities.forms import ConfirmationForm
|
||||||
@ -28,7 +27,7 @@ from .models import (
|
|||||||
CONNECTION_STATUS_CONNECTED, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device,
|
CONNECTION_STATUS_CONNECTED, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device,
|
||||||
DeviceBay, DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate,
|
DeviceBay, DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate,
|
||||||
Manufacturer, Module, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
|
Manufacturer, Module, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
|
||||||
RackRole, Site,
|
RackRole, ServicePort, Site,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -1614,7 +1613,7 @@ def serviceport(request, pk):
|
|||||||
|
|
||||||
|
|
||||||
class ServicePortEditView(PermissionRequiredMixin, ObjectEditView):
|
class ServicePortEditView(PermissionRequiredMixin, ObjectEditView):
|
||||||
permission_required = 'ipam.change_ipaddress'
|
permission_required = 'dcim.change_serviceport'
|
||||||
model = ServicePort
|
model = ServicePort
|
||||||
form_class = forms.ServiceEditForm
|
form_class = forms.ServiceEditForm
|
||||||
fields_initial = ['ip_address', 'port' 'protocol', 'name', 'description']
|
fields_initial = ['ip_address', 'port' 'protocol', 'name', 'description']
|
||||||
@ -1630,7 +1629,7 @@ class ServicePortEditView(PermissionRequiredMixin, ObjectEditView):
|
|||||||
|
|
||||||
|
|
||||||
class ServicePortDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
class ServicePortDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||||
permission_required = 'ipam.delete_ipaddress'
|
permission_required = 'dcim.delete_serviceport'
|
||||||
model = ServicePort
|
model = ServicePort
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
@ -1640,7 +1639,7 @@ class ServicePortDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
|||||||
return super(ServicePortDeleteView, self).post(request, *args, **kwargs)
|
return super(ServicePortDeleteView, self).post(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
@permission_required('ipam.add_ipaddress')
|
@permission_required(['dcim.change_device', 'dcim.add_serviceport'])
|
||||||
def serviceport_add(request, pk):
|
def serviceport_add(request, pk):
|
||||||
device = get_object_or_404(Device, pk=pk)
|
device = get_object_or_404(Device, pk=pk)
|
||||||
|
|
||||||
|
@ -13,20 +13,13 @@ from extras.models import CustomFieldModel, CustomFieldValue
|
|||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
from utilities.models import CreatedUpdatedModel
|
from utilities.models import CreatedUpdatedModel
|
||||||
from utilities.sql import NullsFirstQuerySet
|
from utilities.sql import NullsFirstQuerySet
|
||||||
|
|
||||||
from .fields import IPNetworkField, IPAddressField
|
from .fields import IPNetworkField, IPAddressField
|
||||||
|
|
||||||
|
|
||||||
AF_CHOICES = (
|
AF_CHOICES = (
|
||||||
(4, 'IPv4'),
|
(4, 'IPv4'),
|
||||||
(6, 'IPv6'),
|
(6, 'IPv6'),
|
||||||
)
|
)
|
||||||
|
|
||||||
SERVICE_PORT_CHOICES = (
|
|
||||||
(6, 'TCP'),
|
|
||||||
(17, 'UDP'),
|
|
||||||
)
|
|
||||||
|
|
||||||
PREFIX_STATUS_CONTAINER = 0
|
PREFIX_STATUS_CONTAINER = 0
|
||||||
PREFIX_STATUS_ACTIVE = 1
|
PREFIX_STATUS_ACTIVE = 1
|
||||||
PREFIX_STATUS_RESERVED = 2
|
PREFIX_STATUS_RESERVED = 2
|
||||||
@ -441,76 +434,6 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
return STATUS_CHOICE_CLASSES[self.status]
|
return STATUS_CHOICE_CLASSES[self.status]
|
||||||
|
|
||||||
|
|
||||||
class ServicePort(CreatedUpdatedModel):
|
|
||||||
"""
|
|
||||||
A ServicePort represents a port on a specific IPAddress on which a service is running.
|
|
||||||
The port can be one of 2 predefined protocols - TCP or UDP.
|
|
||||||
A ServicePort is always associated with a specific IPAddress on a Device.
|
|
||||||
|
|
||||||
If an user wants to specify a service running on all IP Addresses on a device,
|
|
||||||
this can be done by assigning the port to the '0.0.0.0/32' or '::/128' IPAddress.
|
|
||||||
|
|
||||||
The combination of IPAddress, Port Number and Port Protocol is always unique for ServicePort.
|
|
||||||
|
|
||||||
If a port number + port protocol combination is assigned to '0.0.0.0/32' or '::/128' IPAddress,
|
|
||||||
it cannot be assigned to any other IPAddress on the same Device.
|
|
||||||
"""
|
|
||||||
|
|
||||||
device = models.ForeignKey('dcim.Device', related_name='service_ports', on_delete=models.CASCADE,
|
|
||||||
blank=False, null=False, verbose_name='device')
|
|
||||||
|
|
||||||
ip_address = models.ForeignKey('IPAddress', related_name='service_ports', on_delete=models.CASCADE,
|
|
||||||
blank=True, null=True, verbose_name='ip_address')
|
|
||||||
protocol = models.PositiveSmallIntegerField(choices=SERVICE_PORT_CHOICES, default=0)
|
|
||||||
|
|
||||||
port = models.PositiveIntegerField()
|
|
||||||
name = models.CharField(max_length=30, blank=False, null=False)
|
|
||||||
description = models.TextField(blank=True)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
ordering = ['device', 'ip_address', 'port']
|
|
||||||
verbose_name = 'Service Port'
|
|
||||||
verbose_name_plural = 'Service Ports'
|
|
||||||
unique_together = ['device', 'ip_address', 'port', 'protocol']
|
|
||||||
|
|
||||||
def __unicode__(self):
|
|
||||||
port_protocol = dict(SERVICE_PORT_CHOICES).get(self.protocol)
|
|
||||||
return u'{}/{}'.format(self.port, port_protocol)
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
|
||||||
return reverse('dcim:serviceport', args=[self.pk])
|
|
||||||
|
|
||||||
@property
|
|
||||||
def short_description(self):
|
|
||||||
if self.description:
|
|
||||||
return self.description[:30]
|
|
||||||
return None
|
|
||||||
|
|
||||||
def clean(self):
|
|
||||||
# if port is already assigned on '0.0.0.0/32' or '::/128'
|
|
||||||
# that means it is assigned on all IPs on the device
|
|
||||||
port_assigned_on_all_ips = bool(ServicePort.objects.filter(
|
|
||||||
ip_address__address='::/128', port=self.port, protocol=self.protocol).exclude(pk=self.id))
|
|
||||||
port_assigned_on_all_v4_ips = bool(ServicePort.objects.filter(
|
|
||||||
ip_address__address='0.0.0.0/32', port=self.port, protocol=self.protocol).exclude(pk=self.id))
|
|
||||||
if port_assigned_on_all_ips:
|
|
||||||
raise ValidationError('Port already assigned on address ::/128')
|
|
||||||
elif port_assigned_on_all_v4_ips and self.ip_address.family == 4:
|
|
||||||
raise ValidationError('Port already assigned on address 0.0.0.0/32')
|
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
|
||||||
super(ServicePort, self).save(*args, **kwargs)
|
|
||||||
|
|
||||||
def to_csv(self):
|
|
||||||
return ','.join([
|
|
||||||
str(self.device_id),
|
|
||||||
self.ip_address,
|
|
||||||
self.port,
|
|
||||||
self.name,
|
|
||||||
self.description,
|
|
||||||
])
|
|
||||||
|
|
||||||
|
|
||||||
class VLANGroup(models.Model):
|
class VLANGroup(models.Model):
|
||||||
"""
|
"""
|
||||||
A VLAN group is an arbitrary collection of VLANs within which VLAN IDs and names must be unique.
|
A VLAN group is an arbitrary collection of VLANs within which VLAN IDs and names must be unique.
|
||||||
|
@ -17,7 +17,7 @@ from utilities.views import (
|
|||||||
|
|
||||||
from . import filters, forms, tables
|
from . import filters, forms, tables
|
||||||
from .models import (Aggregate, IPAddress, PREFIX_STATUS_ACTIVE, PREFIX_STATUS_DEPRECATED, PREFIX_STATUS_RESERVED,
|
from .models import (Aggregate, IPAddress, PREFIX_STATUS_ACTIVE, PREFIX_STATUS_DEPRECATED, PREFIX_STATUS_RESERVED,
|
||||||
Prefix, RIR, Role, VLAN, VLANGroup, VRF, ServicePort)
|
Prefix, RIR, Role, VLAN, VLANGroup, VRF)
|
||||||
|
|
||||||
|
|
||||||
def add_available_prefixes(parent, prefix_list):
|
def add_available_prefixes(parent, prefix_list):
|
||||||
@ -645,6 +645,7 @@ class IPAddressBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
|||||||
cls = IPAddress
|
cls = IPAddress
|
||||||
default_redirect_url = 'ipam:ipaddress_list'
|
default_redirect_url = 'ipam:ipaddress_list'
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# VLAN groups
|
# VLAN groups
|
||||||
#
|
#
|
||||||
|
@ -220,7 +220,7 @@
|
|||||||
None found
|
None found
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.ipam.add_ipaddress %}
|
{% if perms.dcim.add_serviceport %}
|
||||||
<div class="panel-footer text-right">
|
<div class="panel-footer text-right">
|
||||||
<a href="{% url 'dcim:serviceport_add' pk=device.pk %}" class="btn btn-xs btn-primary">
|
<a href="{% url 'dcim:serviceport_add' pk=device.pk %}" class="btn btn-xs btn-primary">
|
||||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
|
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
{{ port.ip_address }}
|
{% if port.ip_address %}
|
||||||
|
{{ port.ip_address }}
|
||||||
|
{% else %}
|
||||||
|
All IP addresses
|
||||||
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="{% url 'dcim:serviceport' pk=port.pk %}"> {{ port }} </a>
|
<a href="{% url 'dcim:serviceport' pk=port.pk %}"> {{ port }} </a>
|
||||||
@ -15,12 +19,12 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td class="text-right">
|
<td class="text-right">
|
||||||
{% if perms.ipam.change_ipaddress %}
|
{% if perms.dcim.change_serviceport%}
|
||||||
<a href="{% url 'dcim:serviceport_edit' pk=port.pk %}" class="btn btn-info btn-xs" title="Edit service port">
|
<a href="{% url 'dcim:serviceport_edit' pk=port.pk %}" class="btn btn-info btn-xs" title="Edit service port">
|
||||||
<i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
|
<i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.ipam.delete_ipaddress %}
|
{% if perms.dcim.delete_serviceport %}
|
||||||
<a href="{% url 'dcim:serviceport_delete' pk=port.pk %}" class="btn btn-danger btn-xs">
|
<a href="{% url 'dcim:serviceport_delete' pk=port.pk %}" class="btn btn-danger btn-xs">
|
||||||
<i class="glyphicon glyphicon-trash" aria-hidden="true" title="Delete port"></i>
|
<i class="glyphicon glyphicon-trash" aria-hidden="true" title="Delete port"></i>
|
||||||
</a>
|
</a>
|
||||||
|
@ -13,13 +13,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="pull-right">
|
<div class="pull-right">
|
||||||
{% if perms.ipam.change_ipaddress %}
|
{% if perms.dcim.change_serviceport %}
|
||||||
<a href="{% url 'dcim:serviceport_edit' pk=service_port.pk %}" class="btn btn-warning">
|
<a href="{% url 'dcim:serviceport_edit' pk=service_port.pk %}" class="btn btn-warning">
|
||||||
<span class="fa fa-pencil" aria-hidden="true"></span>
|
<span class="fa fa-pencil" aria-hidden="true"></span>
|
||||||
Edit this Service Port
|
Edit this Service Port
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.ipam.delete_ipaddress %}
|
{% if perms.dcim.delete_serviceport %}
|
||||||
<a href="{% url 'dcim:serviceport_delete' pk=service_port.pk %}" class="btn btn-danger">
|
<a href="{% url 'dcim:serviceport_delete' pk=service_port.pk %}" class="btn btn-danger">
|
||||||
<span class="fa fa-trash" aria-hidden="true"></span>
|
<span class="fa fa-trash" aria-hidden="true"></span>
|
||||||
Delete this Service Port
|
Delete this Service Port
|
||||||
|
Loading…
Reference in New Issue
Block a user