Skip to content

Commit

Permalink
Service Port functionality #539 (#590)
Browse files Browse the repository at this point in the history
* Add ServicePort model

* Add ServicePort panel in Device page

* Add "Add ServicePort" functionality

* Add "Edit" and "Delete" Service port functionality

* Add ServicePort API serializers

* Rename ServicePort.type to ServicePort.protocol

* Add IPv6 support for assigning a ServicePort to all device's IPs

* Make ServicePort bound to Device and make IPAddres optional

* Move ServicePort related urls and views from `ipam` to `dcim`

* Move serviceport templates to `dcim` folder

* Move ServicePort api serialization from `ipam` to `dcim`

* Rename `serviceport_assign` to `serviceport_add`

...to keep consistency with the naming convention along the rest of device related components

* Move ServicePort mode definition to `dcim`

* add service port specific permissions
  • Loading branch information
if-fi authored and jeremystretch committed Dec 15, 2016
1 parent 6a9f26a commit b821cd2
Show file tree
Hide file tree
Showing 16 changed files with 506 additions and 8 deletions.
31 changes: 30 additions & 1 deletion netbox/dcim/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
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

Expand Down Expand Up @@ -442,3 +442,32 @@ class InterfaceConnectionSerializer(serializers.ModelSerializer):
class Meta:
model = InterfaceConnection
fields = ['id', 'interface_a', 'interface_b', 'connection_status']


#
# Service Ports
#

class ServicePortSerializer(serializers.ModelSerializer):
device = DeviceNestedSerializer()
ip_address = DeviceIPAddressNestedSerializer()

class Meta:
model = ServicePort
fields = ['id', 'device', 'ip_address', 'port', 'protocol', 'name', 'description']


class ServicePortNestedSerializer(ServicePortSerializer):
device = DeviceNestedSerializer()
ip_address = DeviceIPAddressNestedSerializer()

class Meta(ServicePortSerializer.Meta):
fields = ['id', 'device', 'ip_address', 'port', 'protocol']


class ServicePortDetailSerializer(ServicePortSerializer):
device = DeviceNestedSerializer()
ip_address = DeviceIPAddressNestedSerializer()

class Meta(ServicePortSerializer.Meta):
fields = ['id', 'device', 'ip_address', 'port', 'protocol', 'name', 'description']
4 changes: 4 additions & 0 deletions netbox/dcim/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@
url(r'^interface-connections/$', InterfaceConnectionListView.as_view(), name='interfaceconnection_list'),
url(r'^interface-connections/(?P<pk>\d+)/$', InterfaceConnectionView.as_view(), name='interfaceconnection_detail'),

# Service ports
url(r'^service-ports/$', ServicePortListView.as_view(), name='serviceport_list'),
url(r'^service-ports/(?P<pk>\d+)/$', ServicePortDetailView.as_view(), name='serviceport_detail'),

# Miscellaneous
url(r'^related-connections/$', RelatedConnectionsView.as_view(), name='related_connections'),
url(r'^topology-maps/(?P<slug>[\w-]+)/$', TopologyMapView.as_view(), name='topology_map'),
Expand Down
22 changes: 21 additions & 1 deletion netbox/dcim/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
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
Expand Down Expand Up @@ -422,6 +422,26 @@ def get(self, request, pk):
return Response(lldp_neighbors)


#
# Service Port
#

class ServicePortListView(generics.ListAPIView):
"""
List IP addresses (filterable)
"""
queryset = ServicePort.objects.select_related('device', 'ip_address', 'port', 'protocol', 'name', 'description')
serializer_class = serializers.ServicePortSerializer


class ServicePortDetailView(generics.RetrieveAPIView):
"""
Retrieve a single IP address
"""
queryset = ServicePort.objects.select_related('device', 'ip_address', 'port', 'protocol', 'name', 'description')
serializer_class = serializers.ServicePortSerializer


#
# Miscellaneous
#
Expand Down
53 changes: 52 additions & 1 deletion netbox/dcim/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
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
)


Expand Down Expand Up @@ -1275,6 +1275,57 @@ def __init__(self, device, *args, **kwargs):
self.fields['set_as_primary'].initial = True


#
# Service Port
#

class ServicePortForm(forms.ModelForm, BootstrapMixin):

class Meta:
model = ServicePort
fields = ['device', 'ip_address', 'protocol', 'port', 'name', 'description']
widgets = {
'device': forms.HiddenInput(),
}


class ServicePortCreateForm(forms.ModelForm, BootstrapMixin):

class Meta:
model = ServicePort
fields = ['ip_address', 'protocol', 'port', 'name', 'description']
help_texts = {
'port': '0-65535',
'name': 'Service running on this port',
'description': 'Service description'
}
labels = {
'ip_address': "IP Address",
}

def __init__(self, device, *args, **kwargs):
super(ServicePortCreateForm, self).__init__(*args, **kwargs)
self.fields['ip_address'].queryset = IPAddress.objects.filter(interface__device=device)


class ServiceEditForm(forms.ModelForm, BootstrapMixin):
class Meta:
model = ServicePort
fields = ['ip_address', 'protocol', 'port', 'name', 'description']
help_texts = {
'port': '0-65535',
'name': 'Service running on this port',
'description': 'Service description'
}
labels = {
'ip_address': "IP Address",
}

def __init__(self, *args, **kwargs):
super(ServiceEditForm, self).__init__(*args, **kwargs)
self.fields['ip_address'].queryset = IPAddress.objects.filter(interface__device=kwargs['instance'].device)


#
# Modules
#
Expand Down
40 changes: 40 additions & 0 deletions netbox/dcim/migrations/0022_add_service_port.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10 on 2016-11-25 23:50
from __future__ import unicode_literals

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('ipam', '0010_ipaddress_help_texts'),
('dcim', '0021_add_ff_flexstack'),
]

operations = [
migrations.CreateModel(
name='ServicePort',
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)),
('protocol', models.PositiveSmallIntegerField(choices=[(6, b'TCP'), (17, b'UDP')], default=0)),
('port', models.PositiveIntegerField()),
('name', models.CharField(max_length=30)),
('description', models.TextField(blank=True)),
('device', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='service_ports', to='dcim.Device', verbose_name=b'device')),
('ip_address', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='service_ports', to='ipam.IPAddress', verbose_name=b'ip_address')),
],
options={
'ordering': ['device', 'ip_address', 'port'],
'verbose_name': 'Service Port',
'verbose_name_plural': 'Service Ports',
},
),
migrations.AlterUniqueTogether(
name='serviceport',
unique_together={('device', 'ip_address', 'port', 'protocol')},
),
]
70 changes: 70 additions & 0 deletions netbox/dcim/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,12 @@
]


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
Expand Down Expand Up @@ -1246,3 +1252,67 @@ def __unicode__(self):

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,
])
6 changes: 6 additions & 0 deletions netbox/dcim/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,4 +168,10 @@
url(r'^modules/(?P<pk>\d+)/edit/$', views.ModuleEditView.as_view(), name='module_edit'),
url(r'^modules/(?P<pk>\d+)/delete/$', views.ModuleDeleteView.as_view(), name='module_delete'),

# ServicePorts
url(r'^devices/(?P<pk>\d+)/service-ports/add/$', views.serviceport_add, name='serviceport_add'),
url(r'^service-ports/(?P<pk>\d+)/$', views.serviceport, name='serviceport'),
url(r'^service-ports/(?P<pk>\d+)/edit/$', views.ServicePortEditView.as_view(), name='serviceport_edit'),
url(r'^service-ports/(?P<pk>\d+)/delete/$', views.ServicePortDeleteView.as_view(), name='serviceport_delete'),

]
79 changes: 78 additions & 1 deletion netbox/dcim/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
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,
)


Expand Down Expand Up @@ -575,6 +575,7 @@ def device(request, pk):
# Find all IP addresses assigned to this device
ip_addresses = IPAddress.objects.filter(interface__device=device).select_related('interface', 'vrf')\
.order_by('address')
service_ports = ServicePort.objects.filter(device=device).order_by('ip_address', 'port')

# Find any related devices for convenient linking in the UI
related_devices = []
Expand Down Expand Up @@ -604,6 +605,7 @@ def device(request, pk):
'mgmt_interfaces': mgmt_interfaces,
'device_bays': device_bays,
'ip_addresses': ip_addresses,
'service_ports': service_ports,
'secrets': secrets,
'related_devices': related_devices,
'show_graphs': show_graphs,
Expand Down Expand Up @@ -1600,6 +1602,81 @@ def ipaddress_assign(request, pk):
})


#
# Service Ports
#

def serviceport(request, pk):
service_port = get_object_or_404(ServicePort.objects.select_related('device'), pk=pk)

return render(request, 'dcim/serviceport.html', {
'service_port': service_port,
})


class ServicePortEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.change_serviceport'
model = ServicePort
form_class = forms.ServiceEditForm
fields_initial = ['ip_address', 'port' 'protocol', 'name', 'description']
template_name = 'dcim/serviceport_edit.html'

def post(self, request, *args, **kwargs):
service_port = self.get_object(kwargs)
device_url = reverse('dcim:device', kwargs={'pk': service_port.device.pk})
self.success_url = device_url
self.cancel_url = device_url

return super(ServicePortEditView, self).post(request, *args, **kwargs)


class ServicePortDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'dcim.delete_serviceport'
model = ServicePort

def post(self, request, *args, **kwargs):
service_port = self.get_object(kwargs)
self.redirect_url = reverse('dcim:device', kwargs={'pk': service_port.device.pk})

return super(ServicePortDeleteView, self).post(request, *args, **kwargs)


@permission_required(['dcim.change_device', 'dcim.add_serviceport'])
def serviceport_add(request, pk):
device = get_object_or_404(Device, pk=pk)

if request.method == 'POST':
form = forms.ServicePortCreateForm(device, request.POST)
if form.is_valid():
serv_form = forms.ServicePortForm({
'device': device.pk,
'ip_address': form.cleaned_data['ip_address'].pk if form.cleaned_data['ip_address'] else None,
'protocol': form.cleaned_data['protocol'],
'port': form.cleaned_data['port'],
'name': form.cleaned_data['name'],
'description': form.cleaned_data['description'],
})
if serv_form.is_valid():
service_port = serv_form.save(commit=False)
service_port.save()
messages.success(request, "Added new port {0} for device {1}".format(service_port,
service_port.device))

if '_addanother' in request.POST:
return redirect('dcim:serviceport_add', pk=device.pk)
else:
return redirect('dcim:device', pk=device.pk)

else:
form = forms.ServicePortCreateForm(device)

return render(request, 'dcim/serviceport_add.html', {
'device': device,
'form': form,
'cancel_url': reverse('dcim:device', kwargs={'pk': device.pk}),
})


#
# Modules
#
Expand Down
Loading

0 comments on commit b821cd2

Please sign in to comment.