Closes #539: Implemented L4 services for devices

This commit is contained in:
Jeremy Stretch
2016-12-15 15:32:58 -05:00
parent 66fa877198
commit f02c222d4f
15 changed files with 268 additions and 12 deletions

View File

@@ -1,8 +1,8 @@
from rest_framework import serializers
from dcim.api.serializers import SiteNestedSerializer, InterfaceNestedSerializer
from dcim.api.serializers import DeviceNestedSerializer, InterfaceNestedSerializer, SiteNestedSerializer
from extras.api.serializers import CustomFieldSerializer
from ipam.models import VRF, Role, RIR, Aggregate, Prefix, IPAddress, VLAN, VLANGroup
from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
from tenancy.api.serializers import TenantNestedSerializer
@@ -170,3 +170,22 @@ class IPAddressNestedSerializer(IPAddressSerializer):
IPAddressSerializer._declared_fields['nat_inside'] = IPAddressNestedSerializer()
IPAddressSerializer._declared_fields['nat_outside'] = IPAddressNestedSerializer()
#
# Services
#
class ServiceSerializer(serializers.ModelSerializer):
device = DeviceNestedSerializer()
ipaddresses = IPAddressNestedSerializer(many=True)
class Meta:
model = Service
fields = ['id', 'device', 'name', 'port', 'protocol', 'ipaddresses', 'description']
class ServiceNestedSerializer(ServiceSerializer):
class Meta(ServiceSerializer.Meta):
fields = ['id', 'name', 'port', 'protocol']

View File

@@ -37,4 +37,8 @@ urlpatterns = [
url(r'^vlans/$', VLANListView.as_view(), name='vlan_list'),
url(r'^vlans/(?P<pk>\d+)/$', VLANDetailView.as_view(), name='vlan_detail'),
# Services
url(r'^services/$', ServiceListView.as_view(), name='service_list'),
url(r'^services/(?P<pk>\d+)/$', ServiceDetailView.as_view(), name='service_detail'),
]

View File

@@ -1,6 +1,6 @@
from rest_framework import generics
from ipam.models import VRF, Role, RIR, Aggregate, Prefix, IPAddress, VLAN, VLANGroup
from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
from ipam import filters
from extras.api.views import CustomFieldModelAPIView
@@ -177,3 +177,24 @@ class VLANDetailView(CustomFieldModelAPIView, generics.RetrieveAPIView):
queryset = VLAN.objects.select_related('site', 'group', 'tenant', 'role')\
.prefetch_related('custom_field_values__field')
serializer_class = serializers.VLANSerializer
#
# Services
#
class ServiceListView(generics.ListAPIView):
"""
List services (filterable)
"""
queryset = Service.objects.select_related('device').prefetch_related('ipaddresses')
serializer_class = serializers.ServiceSerializer
filter_class = filters.ServiceFilter
class ServiceDetailView(generics.RetrieveAPIView):
"""
Retrieve a single service
"""
queryset = Service.objects.select_related('device').prefetch_related('ipaddresses')
serializer_class = serializers.ServiceSerializer

View File

@@ -9,7 +9,7 @@ from extras.filters import CustomFieldFilterSet
from tenancy.models import Tenant
from utilities.filters import NullableModelMultipleChoiceFilter
from .models import RIR, Aggregate, VRF, Prefix, IPAddress, VLAN, VLANGroup, Role
from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet):
@@ -349,3 +349,10 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
except ValueError:
pass
return queryset.filter(qs_filter)
class ServiceFilter(django_filters.FilterSet):
class Meta:
model = Service
fields = ['device', 'name', 'protocol', 'port']

View File

@@ -9,8 +9,8 @@ from utilities.forms import (
)
from .models import (
Aggregate, IPAddress, IPADDRESS_STATUS_CHOICES, Prefix, PREFIX_STATUS_CHOICES, RIR, Role, VLAN, VLANGroup,
VLAN_STATUS_CHOICES, VRF,
Aggregate, IPAddress, IPADDRESS_STATUS_CHOICES, Prefix, PREFIX_STATUS_CHOICES, RIR, Role, Service, VLAN,
VLANGroup, VLAN_STATUS_CHOICES, VRF,
)
@@ -563,3 +563,25 @@ class VLANFilterForm(BootstrapMixin, CustomFieldFilterForm):
status = forms.MultipleChoiceField(choices=vlan_status_choices, required=False)
role = FilterChoiceField(queryset=Role.objects.annotate(filter_count=Count('vlans')), to_field_name='slug',
null_option=(0, 'None'))
#
# Services
#
class ServiceForm(forms.ModelForm, BootstrapMixin):
class Meta:
model = Service
fields = ['name', 'protocol', 'port', 'ipaddresses', 'description']
help_texts = {
'ipaddresses': "IP address assignment is optional. If no IPs are selected, the service is assumed to be "
"reachable via all IPs assigned to the device.",
}
def __init__(self, *args, **kwargs):
super(ServiceForm, self).__init__(*args, **kwargs)
# Limit IP address choices to those assigned to interfaces of the parent device
self.fields['ipaddresses'].queryset = IPAddress.objects.filter(interface__device=self.instance.device)

View File

@@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10 on 2016-12-15 20:22
from __future__ import unicode_literals
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('dcim', '0022_color_names_to_rgb'),
('ipam', '0011_rir_add_is_private'),
]
operations = [
migrations.CreateModel(
name='Service',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateField(auto_now_add=True)),
('last_updated', models.DateTimeField(auto_now=True)),
('name', models.CharField(max_length=30)),
('protocol', models.PositiveSmallIntegerField(choices=[(6, b'TCP'), (17, b'UDP')])),
('port', models.PositiveIntegerField(validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(65535)], verbose_name=b'Port number')),
('description', models.CharField(blank=True, max_length=100)),
('device', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='services', to='dcim.Device', verbose_name=b'device')),
('ipaddresses', models.ManyToManyField(blank=True, related_name='services', to='ipam.IPAddress', verbose_name=b'IP addresses')),
],
options={
'ordering': ['device', 'protocol', 'port'],
},
),
migrations.AlterUniqueTogether(
name='service',
unique_together=set([('device', 'protocol', 'port')]),
),
]

View File

@@ -61,6 +61,14 @@ STATUS_CHOICE_CLASSES = {
}
IP_PROTOCOL_TCP = 6
IP_PROTOCOL_UDP = 17
IP_PROTOCOL_CHOICES = (
(IP_PROTOCOL_TCP, 'TCP'),
(IP_PROTOCOL_UDP, 'UDP'),
)
class VRF(CreatedUpdatedModel, CustomFieldModel):
"""
A virtual routing and forwarding (VRF) table represents a discrete layer three forwarding domain (e.g. a routing
@@ -525,3 +533,28 @@ class VLAN(CreatedUpdatedModel, CustomFieldModel):
def get_status_class(self):
return STATUS_CHOICE_CLASSES[self.status]
class Service(CreatedUpdatedModel):
"""
A Service represents a layer-four service (e.g. HTTP or SSH) running on a Device. A Service may optionally be tied
to one or more specific IPAddresses belonging to the Device.
"""
device = models.ForeignKey('dcim.Device', related_name='services', on_delete=models.CASCADE, verbose_name='device')
name = models.CharField(max_length=30)
protocol = models.PositiveSmallIntegerField(choices=IP_PROTOCOL_CHOICES)
port = models.PositiveIntegerField(validators=[MinValueValidator(1), MaxValueValidator(65535)],
verbose_name='Port number')
ipaddresses = models.ManyToManyField('ipam.IPAddress', related_name='services', blank=True,
verbose_name='IP addresses')
description = models.CharField(max_length=100, blank=True)
class Meta:
ordering = ['device', 'protocol', 'port']
unique_together = ['device', 'protocol', 'port']
def __unicode__(self):
return u'{} ({}/{})'.format(self.name, self.port, self.get_protocol_display())
def get_parent_url(self):
return self.device.get_absolute_url()

View File

@@ -76,4 +76,8 @@ urlpatterns = [
url(r'^vlans/(?P<pk>\d+)/edit/$', views.VLANEditView.as_view(), name='vlan_edit'),
url(r'^vlans/(?P<pk>\d+)/delete/$', views.VLANDeleteView.as_view(), name='vlan_delete'),
# Services
url(r'^services/(?P<pk>\d+)/edit/$', views.ServiceEditView.as_view(), name='service_edit'),
url(r'^services/(?P<pk>\d+)/delete/$', views.ServiceDeleteView.as_view(), name='service_delete'),
]

View File

@@ -16,7 +16,10 @@ 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
from .models import (
Aggregate, IPAddress, PREFIX_STATUS_ACTIVE, PREFIX_STATUS_DEPRECATED, PREFIX_STATUS_RESERVED, Prefix, RIR, Role,
Service, VLAN, VLANGroup, VRF,
)
def add_available_prefixes(parent, prefix_list):
@@ -733,3 +736,24 @@ class VLANBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'ipam.delete_vlan'
cls = VLAN
default_redirect_url = 'ipam:vlan_list'
#
# Services
#
class ServiceEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.change_service'
model = Service
form_class = forms.ServiceForm
template_name = 'ipam/service_edit.html'
def alter_obj(self, obj, args, kwargs):
if 'device' in kwargs:
obj.device = get_object_or_404(Device, pk=kwargs['device'])
return obj
class ServiceDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'ipam.delete_service'
model = Service