Browse Source

Closes #819: Implemented IP address functional roles

Jeremy Stretch 7 years ago
parent
commit
f427c00d94

+ 8 - 4
netbox/ipam/api/serializers.py

@@ -6,8 +6,8 @@ from rest_framework.validators import UniqueTogetherValidator
 from dcim.api.serializers import NestedDeviceSerializer, InterfaceSerializer, NestedSiteSerializer
 from extras.api.customfields import CustomFieldModelSerializer
 from ipam.models import (
-    Aggregate, IPAddress, IPADDRESS_STATUS_CHOICES, IP_PROTOCOL_CHOICES, Prefix, PREFIX_STATUS_CHOICES, RIR, Role,
-    Service, VLAN, VLAN_STATUS_CHOICES, VLANGroup, VRF,
+    Aggregate, IPAddress, IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, IP_PROTOCOL_CHOICES, Prefix,
+    PREFIX_STATUS_CHOICES, RIR, Role, Service, VLAN, VLAN_STATUS_CHOICES, VLANGroup, VRF,
 )
 from tenancy.api.serializers import NestedTenantSerializer
 from utilities.api import ChoiceFieldSerializer
@@ -236,12 +236,13 @@ class IPAddressSerializer(CustomFieldModelSerializer):
     vrf = NestedVRFSerializer()
     tenant = NestedTenantSerializer()
     status = ChoiceFieldSerializer(choices=IPADDRESS_STATUS_CHOICES)
+    role = ChoiceFieldSerializer(choices=IPADDRESS_ROLE_CHOICES)
     interface = InterfaceSerializer()
 
     class Meta:
         model = IPAddress
         fields = [
-            'id', 'family', 'address', 'vrf', 'tenant', 'status', 'interface', 'description', 'nat_inside',
+            'id', 'family', 'address', 'vrf', 'tenant', 'status', 'role', 'interface', 'description', 'nat_inside',
             'nat_outside', 'custom_fields',
         ]
 
@@ -261,7 +262,10 @@ class WritableIPAddressSerializer(CustomFieldModelSerializer):
 
     class Meta:
         model = IPAddress
-        fields = ['id', 'address', 'vrf', 'tenant', 'status', 'interface', 'description', 'nat_inside', 'custom_fields']
+        fields = [
+            'id', 'address', 'vrf', 'tenant', 'status', 'role', 'interface', 'description', 'nat_inside',
+            'custom_fields',
+        ]
 
 
 #

+ 5 - 2
netbox/ipam/filters.py

@@ -11,8 +11,8 @@ from extras.filters import CustomFieldFilterSet
 from tenancy.models import Tenant
 from utilities.filters import NullableModelMultipleChoiceFilter, NumericInFilter
 from .models import (
-    Aggregate, IPAddress, IPADDRESS_STATUS_CHOICES, Prefix, PREFIX_STATUS_CHOICES, RIR, Role, Service, VLAN,
-    VLAN_STATUS_CHOICES, VLANGroup, VRF,
+    Aggregate, IPAddress, IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, Prefix, PREFIX_STATUS_CHOICES, RIR, Role,
+    Service, VLAN, VLAN_STATUS_CHOICES, VLANGroup, VRF,
 )
 
 
@@ -247,6 +247,9 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
     status = django_filters.MultipleChoiceFilter(
         choices=IPADDRESS_STATUS_CHOICES
     )
+    role = django_filters.MultipleChoiceFilter(
+        choices=IPADDRESS_ROLE_CHOICES
+    )
 
     class Meta:
         model = IPAddress

+ 20 - 6
netbox/ipam/forms.py

@@ -13,8 +13,8 @@ from utilities.forms import (
     add_blank_choice,
 )
 from .models import (
-    Aggregate, IPAddress, IPADDRESS_STATUS_CHOICES, Prefix, PREFIX_STATUS_CHOICES, RIR, Role, Service, VLAN,
-    VLANGroup, VLAN_STATUS_CHOICES, VRF,
+    Aggregate, IPAddress, IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, Prefix, PREFIX_STATUS_CHOICES, RIR, Role,
+    Service, VLAN, VLANGroup, VLAN_STATUS_CHOICES, VRF,
 )
 
 
@@ -477,7 +477,7 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm)
     class Meta:
         model = IPAddress
         fields = [
-            'address', 'vrf', 'status', 'description', 'interface', 'primary_for_device', 'nat_site', 'nat_rack',
+            'address', 'vrf', 'status', 'role', 'description', 'interface', 'primary_for_device', 'nat_site', 'nat_rack',
             'nat_inside', 'tenant_group', 'tenant',
         ]
 
@@ -555,7 +555,7 @@ class IPAddressBulkAddForm(BootstrapMixin, TenancyForm, CustomFieldForm):
 
     class Meta:
         model = IPAddress
-        fields = ['address', 'status', 'vrf', 'description', 'tenant_group', 'tenant']
+        fields = ['address', 'vrf', 'status', 'role', 'description', 'tenant_group', 'tenant']
 
     def __init__(self, *args, **kwargs):
         super(IPAddressBulkAddForm, self).__init__(*args, **kwargs)
@@ -585,6 +585,11 @@ class IPAddressCSVForm(forms.ModelForm):
         choices=PREFIX_STATUS_CHOICES,
         help_text='Operational status'
     )
+    role = CSVChoiceField(
+        choices=IPADDRESS_ROLE_CHOICES,
+        required=False,
+        help_text='Functional role'
+    )
     device = FlexibleModelChoiceField(
         queryset=Device.objects.all(),
         required=False,
@@ -605,7 +610,7 @@ class IPAddressCSVForm(forms.ModelForm):
 
     class Meta:
         model = IPAddress
-        fields = ['address', 'vrf', 'tenant', 'status', 'device', 'interface_name', 'is_primary', 'description']
+        fields = ['address', 'vrf', 'tenant', 'status', 'role', 'device', 'interface_name', 'is_primary', 'description']
 
     def clean(self):
 
@@ -651,10 +656,11 @@ class IPAddressBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
     vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, label='VRF')
     tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
     status = forms.ChoiceField(choices=add_blank_choice(IPADDRESS_STATUS_CHOICES), required=False)
+    role = forms.ChoiceField(choices=add_blank_choice(IPADDRESS_ROLE_CHOICES), required=False)
     description = forms.CharField(max_length=100, required=False)
 
     class Meta:
-        nullable_fields = ['vrf', 'tenant', 'description']
+        nullable_fields = ['vrf', 'role', 'tenant', 'description']
 
 
 def ipaddress_status_choices():
@@ -664,6 +670,13 @@ def ipaddress_status_choices():
     return [(s[0], '{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in IPADDRESS_STATUS_CHOICES]
 
 
+def ipaddress_role_choices():
+    role_counts = {}
+    for role in IPAddress.objects.values('role').annotate(count=Count('role')).order_by('role'):
+        role_counts[role['role']] = role['count']
+    return [(r[0], '{} ({})'.format(r[1], role_counts.get(r[0], 0))) for r in IPADDRESS_ROLE_CHOICES]
+
+
 class IPAddressFilterForm(BootstrapMixin, CustomFieldFilterForm):
     model = IPAddress
     q = forms.CharField(required=False, label='Search')
@@ -684,6 +697,7 @@ class IPAddressFilterForm(BootstrapMixin, CustomFieldFilterForm):
         null_option=(0, 'None')
     )
     status = forms.MultipleChoiceField(choices=ipaddress_status_choices, required=False)
+    role = forms.MultipleChoiceField(choices=ipaddress_role_choices, required=False)
 
 
 #

+ 25 - 0
netbox/ipam/migrations/0017_ipaddress_roles.py

@@ -0,0 +1,25 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.1 on 2017-06-14 19:52
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('ipam', '0016_unicode_literals'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='ipaddress',
+            name='role',
+            field=models.PositiveSmallIntegerField(blank=True, choices=[(10, 'Loopback'), (20, 'Secondary'), (30, 'Anycast'), (40, 'Virtual'), (41, 'VRRP'), (42, 'HSRP'), (43, 'GLBP')], help_text='The functional role of this IP', null=True, verbose_name='Role'),
+        ),
+        migrations.AlterField(
+            model_name='ipaddress',
+            name='status',
+            field=models.PositiveSmallIntegerField(choices=[(1, 'Active'), (2, 'Reserved'), (3, 'Deprecated'), (5, 'DHCP')], default=1, help_text='The operational status of this IP', verbose_name='Status'),
+        ),
+    ]

+ 28 - 3
netbox/ipam/models.py

@@ -47,6 +47,23 @@ IPADDRESS_STATUS_CHOICES = (
     (IPADDRESS_STATUS_DHCP, 'DHCP')
 )
 
+IPADDRESS_ROLE_LOOPBACK = 10
+IPADDRESS_ROLE_SECONDARY = 20
+IPADDRESS_ROLE_ANYCAST = 30
+IPADDRESS_ROLE_VIRTUAL = 40
+IPADDRESS_ROLE_VRRP = 41
+IPADDRESS_ROLE_HSRP = 42
+IPADDRESS_ROLE_GLBP = 43
+IPADDRESS_ROLE_CHOICES = (
+    (IPADDRESS_ROLE_LOOPBACK, 'Loopback'),
+    (IPADDRESS_ROLE_SECONDARY, 'Secondary'),
+    (IPADDRESS_ROLE_ANYCAST, 'Anycast'),
+    (IPADDRESS_ROLE_VIRTUAL, 'Virtual'),
+    (IPADDRESS_ROLE_VRRP, 'VRRP'),
+    (IPADDRESS_ROLE_HSRP, 'HSRP'),
+    (IPADDRESS_ROLE_GLBP, 'GLBP'),
+)
+
 VLAN_STATUS_ACTIVE = 1
 VLAN_STATUS_RESERVED = 2
 VLAN_STATUS_DEPRECATED = 3
@@ -65,7 +82,6 @@ STATUS_CHOICE_CLASSES = {
     5: 'success',
 }
 
-
 IP_PROTOCOL_TCP = 6
 IP_PROTOCOL_UDP = 17
 IP_PROTOCOL_CHOICES = (
@@ -427,7 +443,13 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel):
     vrf = models.ForeignKey('VRF', related_name='ip_addresses', on_delete=models.PROTECT, blank=True, null=True,
                             verbose_name='VRF')
     tenant = models.ForeignKey(Tenant, related_name='ip_addresses', blank=True, null=True, on_delete=models.PROTECT)
-    status = models.PositiveSmallIntegerField('Status', choices=IPADDRESS_STATUS_CHOICES, default=1)
+    status = models.PositiveSmallIntegerField(
+        'Status', choices=IPADDRESS_STATUS_CHOICES, default=IPADDRESS_STATUS_ACTIVE,
+        help_text='The operational status of this IP'
+    )
+    role = models.PositiveSmallIntegerField(
+        'Role', choices=IPADDRESS_ROLE_CHOICES, blank=True, null=True, help_text='The functional role of this IP'
+    )
     interface = models.ForeignKey(Interface, related_name='ip_addresses', on_delete=models.CASCADE, blank=True,
                                   null=True)
     nat_inside = models.OneToOneField('self', related_name='nat_outside', on_delete=models.SET_NULL, blank=True,
@@ -438,7 +460,9 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel):
 
     objects = IPAddressManager()
 
-    csv_headers = ['address', 'vrf', 'tenant', 'status', 'device', 'interface_name', 'is_primary', 'description']
+    csv_headers = [
+        'address', 'vrf', 'tenant', 'status', 'role', 'device', 'interface_name', 'is_primary', 'description',
+    ]
 
     class Meta:
         ordering = ['family', 'address']
@@ -490,6 +514,7 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel):
             self.vrf.rd if self.vrf else None,
             self.tenant.name if self.tenant else None,
             self.get_status_display(),
+            self.get_role_display(),
             self.device.identifier if self.device else None,
             self.interface.name if self.interface else None,
             is_primary,

+ 2 - 2
netbox/ipam/tables.py

@@ -299,7 +299,7 @@ class IPAddressTable(BaseTable):
 
     class Meta(BaseTable.Meta):
         model = IPAddress
-        fields = ('pk', 'address', 'status', 'vrf', 'tenant', 'nat_inside', 'device', 'description')
+        fields = ('pk', 'address', 'vrf', 'status', 'role', 'tenant', 'nat_inside', 'device', 'description')
         row_attrs = {
             'class': lambda record: 'success' if not isinstance(record, IPAddress) else '',
         }
@@ -328,7 +328,7 @@ class IPAddressSearchTable(SearchTable):
 
     class Meta(SearchTable.Meta):
         model = IPAddress
-        fields = ('address', 'status', 'vrf', 'tenant', 'device', 'interface', 'description')
+        fields = ('address', 'vrf', 'status', 'role', 'tenant', 'device', 'interface', 'description')
 
 
 #

+ 6 - 0
netbox/templates/ipam/ipaddress.html

@@ -83,6 +83,12 @@
                     </td>
                 </tr>
                 <tr>
+                    <td>Role</td>
+                    <td>
+                        <a href="{% url 'ipam:ipaddress_list' %}?role={{ ipaddress.role }}">{{ ipaddress.get_role_display }}</a>
+                    </td>
+                </tr>
+                <tr>
                     <td>Description</td>
                     <td>
                         {% if ipaddress.description %}

+ 1 - 0
netbox/templates/ipam/ipaddress_bulk_add.html

@@ -14,6 +14,7 @@
         <div class="panel-body">
             {% render_field pattern_form.pattern %}
             {% render_field model_form.status %}
+            {% render_field model_form.role %}
             {% render_field model_form.vrf %}
             {% render_field model_form.description %}
         </div>

+ 1 - 0
netbox/templates/ipam/ipaddress_edit.html

@@ -14,6 +14,7 @@
         <div class="panel-body">
             {% render_field form.address %}
             {% render_field form.status %}
+            {% render_field form.role %}
             {% render_field form.vrf %}
             {% render_field form.description %}
         </div>