Move ServicePort mode definition to dcim

* add service port specific permissions
This commit is contained in:
Iva Kaneva 2016-11-28 00:49:21 +02:00
parent 8360402e0a
commit f2fef00ed8
11 changed files with 94 additions and 98 deletions

View File

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

View File

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

View File

@ -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
) )

View File

@ -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 = [

View File

@ -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,
])

View File

@ -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)

View File

@ -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.

View File

@ -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
# #

View File

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

View File

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

View File

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