diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 8a8824138..d0e134b71 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -1,7 +1,5 @@ from __future__ import unicode_literals -from collections import OrderedDict - from rest_framework import serializers from rest_framework.validators import UniqueTogetherValidator from taggit.models import Tag @@ -63,9 +61,9 @@ class SiteSerializer(CustomFieldModelSerializer): model = Site fields = [ 'id', 'name', 'slug', 'status', 'region', 'tenant', 'facility', 'asn', 'time_zone', 'description', - 'physical_address', 'shipping_address', 'contact_name', 'contact_phone', 'contact_email', 'comments', - 'tags', 'custom_fields', 'created', 'last_updated', 'count_prefixes', 'count_vlans', 'count_racks', - 'count_devices', 'count_circuits', + 'physical_address', 'shipping_address', 'latitude', 'longitude', 'contact_name', 'contact_phone', + 'contact_email', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'count_prefixes', + 'count_vlans', 'count_racks', 'count_devices', 'count_circuits', ] diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 6460d5d2c..807caa5ce 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -115,8 +115,8 @@ class SiteForm(BootstrapMixin, TenancyForm, CustomFieldForm): model = Site fields = [ 'name', 'slug', 'status', 'region', 'tenant_group', 'tenant', 'facility', 'asn', 'time_zone', 'description', - 'physical_address', 'shipping_address', 'contact_name', 'contact_phone', 'contact_email', 'comments', - 'tags', + 'physical_address', 'shipping_address', 'latitude', 'longitude', 'contact_name', 'contact_phone', + 'contact_email', 'comments', 'tags', ] widgets = { 'physical_address': SmallTextarea(attrs={'rows': 3}), @@ -129,7 +129,9 @@ class SiteForm(BootstrapMixin, TenancyForm, CustomFieldForm): 'time_zone': "Local time zone", 'description': "Short description (will appear in sites list)", 'physical_address': "Physical location of the building (e.g. for GPS)", - 'shipping_address': "If different from the physical address" + 'shipping_address': "If different from the physical address", + 'latitude': "Latitude in decimal format (xx.yyyyyy)", + 'longitude': "Longitude in decimal format (xx.yyyyyy)" } diff --git a/netbox/dcim/migrations/0059_site_latitude_longitude.py b/netbox/dcim/migrations/0059_site_latitude_longitude.py new file mode 100644 index 000000000..15e666f35 --- /dev/null +++ b/netbox/dcim/migrations/0059_site_latitude_longitude.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.12 on 2018-06-21 18:45 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0058_relax_rack_naming_constraints'), + ] + + operations = [ + migrations.AddField( + model_name='site', + name='latitude', + field=models.DecimalField(blank=True, decimal_places=6, max_digits=8, null=True), + ), + migrations.AddField( + model_name='site', + name='longitude', + field=models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True), + ), + ] diff --git a/netbox/dcim/migrations/0059_change_logging.py b/netbox/dcim/migrations/0060_change_logging.py similarity index 98% rename from netbox/dcim/migrations/0059_change_logging.py rename to netbox/dcim/migrations/0060_change_logging.py index b64e2570b..8a40f4e4e 100644 --- a/netbox/dcim/migrations/0059_change_logging.py +++ b/netbox/dcim/migrations/0060_change_logging.py @@ -8,7 +8,7 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('dcim', '0058_relax_rack_naming_constraints'), + ('dcim', '0059_site_latitude_longitude'), ] operations = [ diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index f430eb095..5ffac3f5c 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -136,6 +136,18 @@ class Site(ChangeLoggedModel, CustomFieldModel): max_length=200, blank=True ) + latitude = models.DecimalField( + max_digits=8, + decimal_places=6, + blank=True, + null=True + ) + longitude = models.DecimalField( + max_digits=9, + decimal_places=6, + blank=True, + null=True + ) contact_name = models.CharField( max_length=50, blank=True @@ -166,7 +178,7 @@ class Site(ChangeLoggedModel, CustomFieldModel): serializer = 'dcim.api.serializers.SiteSerializer' csv_headers = [ 'name', 'slug', 'status', 'region', 'tenant', 'facility', 'asn', 'time_zone', 'description', 'physical_address', - 'shipping_address', 'contact_name', 'contact_phone', 'contact_email', 'comments', + 'shipping_address', 'latitude', 'longitude', 'contact_name', 'contact_phone', 'contact_email', 'comments', ] class Meta: @@ -191,6 +203,8 @@ class Site(ChangeLoggedModel, CustomFieldModel): self.description, self.physical_address, self.shipping_address, + self.latitude, + self.longitude, self.contact_name, self.contact_phone, self.contact_email, diff --git a/netbox/extras/constants.py b/netbox/extras/constants.py index 84c84dfcf..6807af9d9 100644 --- a/netbox/extras/constants.py +++ b/netbox/extras/constants.py @@ -3,11 +3,11 @@ from __future__ import unicode_literals # Models which support custom fields CUSTOMFIELD_MODELS = ( - 'provider', 'circuit', # Circuits - 'site', 'rack', 'devicetype', 'device', # DCIM - 'aggregate', 'prefix', 'ipaddress', 'vlan', 'vrf', # IPAM - 'tenant', # Tenancy - 'cluster', 'virtualmachine', # Virtualization + 'provider', 'circuit', # Circuits + 'site', 'rack', 'devicetype', 'device', # DCIM + 'aggregate', 'prefix', 'ipaddress', 'vlan', 'vrf', 'service', # IPAM + 'tenant', # Tenancy + 'cluster', 'virtualmachine', # Virtualization ) # Custom field types diff --git a/netbox/ipam/api/serializers.py b/netbox/ipam/api/serializers.py index f7969fbc3..bfbc66be3 100644 --- a/netbox/ipam/api/serializers.py +++ b/netbox/ipam/api/serializers.py @@ -301,7 +301,7 @@ class AvailableIPSerializer(serializers.Serializer): # Services # -class ServiceSerializer(ValidatedModelSerializer): +class ServiceSerializer(CustomFieldModelSerializer): device = NestedDeviceSerializer(required=False, allow_null=True) virtual_machine = NestedVirtualMachineSerializer(required=False, allow_null=True) protocol = ChoiceFieldSerializer(choices=IP_PROTOCOL_CHOICES) @@ -315,6 +315,6 @@ class ServiceSerializer(ValidatedModelSerializer): class Meta: model = Service fields = [ - 'id', 'device', 'virtual_machine', 'name', 'port', 'protocol', 'ipaddresses', 'description', 'created', - 'last_updated', + 'id', 'device', 'virtual_machine', 'name', 'port', 'protocol', 'ipaddresses', 'description', + 'custom_fields', 'created', 'last_updated', ] diff --git a/netbox/ipam/filters.py b/netbox/ipam/filters.py index db2806b77..487970b49 100644 --- a/netbox/ipam/filters.py +++ b/netbox/ipam/filters.py @@ -426,6 +426,10 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet): class ServiceFilter(django_filters.FilterSet): + q = django_filters.CharFilter( + method='search', + label='Search', + ) device_id = django_filters.ModelMultipleChoiceFilter( queryset=Device.objects.all(), label='Device (ID)', @@ -450,3 +454,9 @@ class ServiceFilter(django_filters.FilterSet): class Meta: model = Service fields = ['name', 'protocol', 'port'] + + def search(self, queryset, name, value): + if not value.strip(): + return queryset + qs_filter = Q(name__icontains=value) | Q(description__icontains=value) + return queryset.filter(qs_filter) diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index 82ebfe724..2b5f8ed7a 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals from django import forms from django.core.exceptions import MultipleObjectsReturned +from django.core.validators import MaxValueValidator, MinValueValidator from django.db.models import Count from taggit.forms import TagField @@ -15,7 +16,9 @@ from utilities.forms import ( SlugField, add_blank_choice, ) from virtualization.models import VirtualMachine -from .constants import IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, PREFIX_STATUS_CHOICES, VLAN_STATUS_CHOICES +from .constants import ( + IP_PROTOCOL_CHOICES, IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, PREFIX_STATUS_CHOICES, VLAN_STATUS_CHOICES, +) from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF IP_FAMILY_CHOICES = [ @@ -914,7 +917,7 @@ class VLANFilterForm(BootstrapMixin, CustomFieldFilterForm): # Services # -class ServiceForm(BootstrapMixin, forms.ModelForm): +class ServiceForm(BootstrapMixin, CustomFieldForm): class Meta: model = Service @@ -940,3 +943,28 @@ class ServiceForm(BootstrapMixin, forms.ModelForm): ) else: self.fields['ipaddresses'].choices = [] + + +class ServiceFilterForm(BootstrapMixin, CustomFieldFilterForm): + model = Service + q = forms.CharField( + required=False, + label='Search' + ) + protocol = forms.ChoiceField( + choices=add_blank_choice(IP_PROTOCOL_CHOICES), + required=False + ) + port = forms.IntegerField( + required=False + ) + + +class ServiceBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): + pk = forms.ModelMultipleChoiceField(queryset=Service.objects.all(), widget=forms.MultipleHiddenInput) + protocol = forms.ChoiceField(choices=add_blank_choice(IP_PROTOCOL_CHOICES), required=False) + port = forms.IntegerField(validators=[MinValueValidator(1), MaxValueValidator(65535)], required=False) + description = forms.CharField(max_length=100, required=False) + + class Meta: + nullable_fields = ['site', 'group', 'tenant', 'role', 'description'] diff --git a/netbox/ipam/models.py b/netbox/ipam/models.py index f1414bd27..e1bd93f97 100644 --- a/netbox/ipam/models.py +++ b/netbox/ipam/models.py @@ -831,7 +831,7 @@ class VLAN(ChangeLoggedModel, CustomFieldModel): @python_2_unicode_compatible -class Service(ChangeLoggedModel): +class Service(ChangeLoggedModel, CustomFieldModel): """ A Service represents a layer-four service (e.g. HTTP or SSH) running on a Device or VirtualMachine. A Service may optionally be tied to one or more specific IPAddresses belonging to its parent. @@ -871,6 +871,11 @@ class Service(ChangeLoggedModel): max_length=100, blank=True ) + custom_field_values = GenericRelation( + to='extras.CustomFieldValue', + content_type_field='obj_type', + object_id_field='obj_id' + ) serializer = 'ipam.api.serializers.ServiceSerializer' @@ -880,6 +885,9 @@ class Service(ChangeLoggedModel): def __str__(self): return '{} ({}/{})'.format(self.name, self.port, self.get_protocol_display()) + def get_absolute_url(self): + return reverse('ipam:service', args=[self.pk]) + @property def parent(self): return self.device or self.virtual_machine diff --git a/netbox/ipam/tables.py b/netbox/ipam/tables.py index e6310475f..2cb1c6606 100644 --- a/netbox/ipam/tables.py +++ b/netbox/ipam/tables.py @@ -6,7 +6,7 @@ from django_tables2.utils import Accessor from dcim.models import Interface from tenancy.tables import COL_TENANT from utilities.tables import BaseTable, ToggleColumn -from .models import Aggregate, IPAddress, Prefix, RIR, Role, VLAN, VLANGroup, VRF +from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF RIR_UTILIZATION = """
Name | +{{ service.name }} | +
Parent | ++ {{ service.parent }} + | +
Protocol | +{{ service.get_protocol_display }} | +
Port | +{{ service.port }} | +
IP Addresses | +
+ {% for ipaddress in service.ipaddresses.all %}
+ {{ ipaddress }} + {% empty %} + None + {% endfor %} + |
+
Description | ++ {% if service.description %} + {{ service.description }} + {% else %} + N/A + {% endif %} + | +