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 ipam.models import IPAddress, ServicePort
from ipam.models import IPAddress
from dcim.models import (
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, DeviceType,
DeviceRole, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer, Module, Platform, PowerOutlet,
PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackRole, RACK_FACE_FRONT, RACK_FACE_REAR, Site,
SUBDEVICE_ROLE_CHILD, SUBDEVICE_ROLE_PARENT,
)
ServicePort)
from extras.api.serializers import CustomFieldSerializer
from tenancy.api.serializers import TenantNestedSerializer

View File

@ -12,11 +12,10 @@ from django.shortcuts import get_object_or_404
from dcim.models import (
ConsolePort, ConsoleServerPort, Device, DeviceBay, DeviceRole, DeviceType, IFACE_FF_VIRTUAL, Interface,
InterfaceConnection, Manufacturer, Module, Platform, PowerOutlet, PowerPort, Rack, RackGroup, RackRole, Site,
)
ServicePort)
from dcim import filters
from extras.api.views import CustomFieldModelAPIView
from extras.api.renderers import BINDZoneRenderer, FlatJSONRenderer
from ipam.models import ServicePort
from utilities.api import ServiceUnavailable
from .exceptions import MissingFilterException
from . import serializers

View File

@ -5,7 +5,7 @@ from django.core.exceptions import ValidationError
from django.db.models import Count, Q
from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
from ipam.models import IPAddress, ServicePort
from ipam.models import IPAddress
from tenancy.models import Tenant
from utilities.forms import (
APISelect, add_blank_choice, BootstrapMixin, BulkEditForm, BulkImportForm, CommentField, CSVDataField,
@ -18,7 +18,7 @@ from .models import (
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceRole, DeviceType,
Interface, IFACE_FF_CHOICES, IFACE_FF_VIRTUAL, InterfaceConnection, InterfaceTemplate, Manufacturer, Module,
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 -*-
# 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 django.db import migrations, models
@ -9,8 +9,8 @@ import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('dcim', '0021_add_ff_flexstack'),
('ipam', '0010_ipaddress_help_texts'),
('dcim', '0021_add_ff_flexstack'),
]
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()):
"""
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):
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.core.exceptions import ValidationError
from django.core.urlresolvers import reverse
from django.db import transaction
from django.db.models import Count
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, redirect, render
from django.utils.http import urlencode
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 extras.models import Graph, TopologyMap, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE
from utilities.forms import ConfirmationForm
@ -28,7 +27,7 @@ from .models import (
CONNECTION_STATUS_CONNECTED, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device,
DeviceBay, DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate,
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):
permission_required = 'ipam.change_ipaddress'
permission_required = 'dcim.change_serviceport'
model = ServicePort
form_class = forms.ServiceEditForm
fields_initial = ['ip_address', 'port' 'protocol', 'name', 'description']
@ -1630,7 +1629,7 @@ class ServicePortEditView(PermissionRequiredMixin, ObjectEditView):
class ServicePortDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'ipam.delete_ipaddress'
permission_required = 'dcim.delete_serviceport'
model = ServicePort
def post(self, request, *args, **kwargs):
@ -1640,7 +1639,7 @@ class ServicePortDeleteView(PermissionRequiredMixin, ObjectDeleteView):
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):
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 utilities.models import CreatedUpdatedModel
from utilities.sql import NullsFirstQuerySet
from .fields import IPNetworkField, IPAddressField
AF_CHOICES = (
(4, 'IPv4'),
(6, 'IPv6'),
)
SERVICE_PORT_CHOICES = (
(6, 'TCP'),
(17, 'UDP'),
)
PREFIX_STATUS_CONTAINER = 0
PREFIX_STATUS_ACTIVE = 1
PREFIX_STATUS_RESERVED = 2
@ -441,76 +434,6 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel):
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):
"""
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 .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):
@ -645,6 +645,7 @@ class IPAddressBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
cls = IPAddress
default_redirect_url = 'ipam:ipaddress_list'
#
# VLAN groups
#

View File

@ -220,7 +220,7 @@
None found
</div>
{% endif %}
{% if perms.ipam.add_ipaddress %}
{% if perms.dcim.add_serviceport %}
<div class="panel-footer text-right">
<a href="{% url 'dcim:serviceport_add' pk=device.pk %}" class="btn btn-xs btn-primary">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>

View File

@ -1,6 +1,10 @@
<tr>
<td>
{{ port.ip_address }}
{% if port.ip_address %}
{{ port.ip_address }}
{% else %}
All IP addresses
{% endif %}
</td>
<td>
<a href="{% url 'dcim:serviceport' pk=port.pk %}"> {{ port }} </a>
@ -15,12 +19,12 @@
{% endif %}
</td>
<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">
<i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
</a>
{% 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">
<i class="glyphicon glyphicon-trash" aria-hidden="true" title="Delete port"></i>
</a>

View File

@ -13,13 +13,13 @@
</div>
</div>
<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">
<span class="fa fa-pencil" aria-hidden="true"></span>
Edit this Service Port
</a>
{% 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">
<span class="fa fa-trash" aria-hidden="true"></span>
Delete this Service Port