diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 1d15cf6f3..d96ec4c70 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -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 diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index 6bafa0d70..d6bcea449 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -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 diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index d87d1a4f4..f9fe4a88a 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -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 ) diff --git a/netbox/ipam/migrations/0011_add_service_port.py b/netbox/dcim/migrations/0022_add_service_port.py similarity index 97% rename from netbox/ipam/migrations/0011_add_service_port.py rename to netbox/dcim/migrations/0022_add_service_port.py index ec9c29b52..046518257 100644 --- a/netbox/ipam/migrations/0011_add_service_port.py +++ b/netbox/dcim/migrations/0022_add_service_port.py @@ -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 = [ diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index 93cdb95d5..d9ccbf0f4 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -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, + ]) diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 38eafd70b..03c409fde 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -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) diff --git a/netbox/ipam/models.py b/netbox/ipam/models.py index 5fade85ad..973b706e4 100644 --- a/netbox/ipam/models.py +++ b/netbox/ipam/models.py @@ -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. diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index d28a10e86..14b188672 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -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 # diff --git a/netbox/templates/dcim/device.html b/netbox/templates/dcim/device.html index 417ca9ad5..a568a17f2 100644 --- a/netbox/templates/dcim/device.html +++ b/netbox/templates/dcim/device.html @@ -220,7 +220,7 @@ None found {% endif %} - {% if perms.ipam.add_ipaddress %} + {% if perms.dcim.add_serviceport %}
- {% if perms.ipam.change_ipaddress %} + {% if perms.dcim.change_serviceport %} Edit this Service Port {% endif %} - {% if perms.ipam.delete_ipaddress %} + {% if perms.dcim.delete_serviceport %} Delete this Service Port