From 3c82920870ce592f240a4ec38f90585fa6dc900a Mon Sep 17 00:00:00 2001 From: Jamie Murphy Date: Wed, 5 Jul 2023 16:43:29 +0100 Subject: [PATCH 1/8] ipaddressfunction - poc --- netbox/ipam/choices.py | 12 ++++++++++ netbox/ipam/constants.py | 6 +++++ netbox/ipam/models/ip.py | 49 +++++++++++++++++++++++++++++++++++++++- 3 files changed, 66 insertions(+), 1 deletion(-) diff --git a/netbox/ipam/choices.py b/netbox/ipam/choices.py index 298baa64329..bc3dec1165b 100644 --- a/netbox/ipam/choices.py +++ b/netbox/ipam/choices.py @@ -94,6 +94,18 @@ class IPAddressRoleChoices(ChoiceSet): (ROLE_CARP, 'CARP', 'green'), ) +class IPAddressFunctionChoices(ChoiceSet): + + FUNC_OOB = 'Out Of Band' + # Future planning, depreciate primary_ip + # FUNC_PRIMARY_IP = 'Primary IP' + + CHOICES = ( + (FUNC_OOB, 'Out Of Band', 'gray'), + # Future planning, depreciate primary_ip + # (FUNC_PRIMARY_IP, 'Primary IP', 'blue'), + ) + # # FHRP diff --git a/netbox/ipam/constants.py b/netbox/ipam/constants.py index f26fce2b51b..503229f56ef 100644 --- a/netbox/ipam/constants.py +++ b/netbox/ipam/constants.py @@ -48,6 +48,12 @@ ) +IPADDRESS_FUNCTION_ASSIGNMENT_MODELS = Q( + Q(app_label='dcim', model='device') | + Q(app_label='dcim', model='virtualdevicecontext') | + Q(app_label='virtualization', model='VirtualMachine') +) + # # FHRP groups # diff --git a/netbox/ipam/models/ip.py b/netbox/ipam/models/ip.py index 015f9220ccf..0f470281ea8 100644 --- a/netbox/ipam/models/ip.py +++ b/netbox/ipam/models/ip.py @@ -15,7 +15,7 @@ from ipam.querysets import PrefixQuerySet from ipam.validators import DNSValidator from netbox.config import get_config -from netbox.models import OrganizationalModel, PrimaryModel +from netbox.models import OrganizationalModel, PrimaryModel, NetBoxModel __all__ = ( 'Aggregate', @@ -667,6 +667,53 @@ def utilization(self): return int(float(child_count) / self.size * 100) +class IPAddressFunction(NetBoxModel): + assigned_object_type = models.ForeignKey( + to=ContentType, + limit_choices_to=IPADDRESS_FUNCTION_ASSIGNMENT_MODELS, + on_delete=models.CASCADE, + related_name='+' + ) + assigned_object_id = models.PositiveBigIntegerField() + assigned_object = GenericForeignKey( + ct_field='assigned_object_type', + fk_field='assigned_object_id' + ) + assigned_ip = models.ForeignKey( + to='ipam.IPAddress', + on_delete=models.CASCADE, + related_name='+', + verbose_name='Assigned IP' + ) + function = models.CharField( + max_length=50, + choices=IPAddressFunctionChoices, + help_text=_('Function to assign to ip') + ) + + class Meta: + ordering = ('function',) + verbose_name = 'IP Address Function' + constraints = ( + models.UniqueConstraint( + fields=('assigned_object_type', 'assigned_object_id', 'function'), + name='ipam_ipfunction_assigned_object' + ), + models.UniqueConstraint( + fields=('assigned_ip'), + name='ipam_ipfunction_ip_single_use' + ), + ) + + def __str__(self): + if self.pk is not None: + return f'{self.assigned_object_type} - {self.function} - {self.assigned_ip}' + return super().__str__() + + def get_absolute_url(self): + return reverse('ipam:ipaddressfunction', args=[self.pk]) + + class IPAddress(PrimaryModel): """ An IPAddress represents an individual IPv4 or IPv6 address and its mask. The mask length should match what is From 1167a0473e1d0ce0e8d38ca57bb6b27662f1e5ae Mon Sep 17 00:00:00 2001 From: Jamie Murphy Date: Wed, 5 Jul 2023 16:58:11 +0100 Subject: [PATCH 2/8] add GenericRelation for ipam.IPAddressFunction --- netbox/dcim/models/devices.py | 8 ++++++++ netbox/virtualization/models/virtualmachines.py | 3 +++ 2 files changed, 11 insertions(+) diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py index 4cf330ffd98..9852f76e1dc 100644 --- a/netbox/dcim/models/devices.py +++ b/netbox/dcim/models/devices.py @@ -645,6 +645,9 @@ class Device(PrimaryModel, ConfigContextModel): images = GenericRelation( to='extras.ImageAttachment' ) + ipaddressfunctions = GenericRelation( + to='ipam.IPAddressFunction' + ) objects = ConfigContextModelQuerySet.as_manager() @@ -1231,6 +1234,11 @@ class VirtualDeviceContext(PrimaryModel): blank=True ) + # Generic relation + ipaddressfunctions = GenericRelation( + to='ipam.IPAddressFunction' + ) + class Meta: ordering = ['name'] constraints = ( diff --git a/netbox/virtualization/models/virtualmachines.py b/netbox/virtualization/models/virtualmachines.py index 6e9cc566493..5d6a4ea29bd 100644 --- a/netbox/virtualization/models/virtualmachines.py +++ b/netbox/virtualization/models/virtualmachines.py @@ -124,6 +124,9 @@ class VirtualMachine(PrimaryModel, ConfigContextModel): contacts = GenericRelation( to='tenancy.ContactAssignment' ) + ipaddressfunctions = GenericRelation( + to='ipam.IPAddressFunction' + ) objects = ConfigContextModelQuerySet.as_manager() From 63b946b33952e49ea4da064ed5ec624423042bdf Mon Sep 17 00:00:00 2001 From: Jamie Murphy Date: Wed, 5 Jul 2023 16:58:20 +0100 Subject: [PATCH 3/8] add IPAddressFunction to __all__ --- netbox/ipam/models/ip.py | 1 + 1 file changed, 1 insertion(+) diff --git a/netbox/ipam/models/ip.py b/netbox/ipam/models/ip.py index 0f470281ea8..25517f1f6c5 100644 --- a/netbox/ipam/models/ip.py +++ b/netbox/ipam/models/ip.py @@ -20,6 +20,7 @@ __all__ = ( 'Aggregate', 'IPAddress', + 'IPAddressFunction' 'IPRange', 'Prefix', 'RIR', From 557c7d5f2ed7a011d129e256024567e2c23115ae Mon Sep 17 00:00:00 2001 From: Jamie Murphy Date: Thu, 6 Jul 2023 21:09:08 +0100 Subject: [PATCH 4/8] IPAddressFunction: add migration, fix imports and constraints --- .../0067_ipaddressfunction_and_more.py | 44 +++++++++++++++++++ netbox/ipam/models/ip.py | 4 +- 2 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 netbox/ipam/migrations/0067_ipaddressfunction_and_more.py diff --git a/netbox/ipam/migrations/0067_ipaddressfunction_and_more.py b/netbox/ipam/migrations/0067_ipaddressfunction_and_more.py new file mode 100644 index 00000000000..3a18e36c24e --- /dev/null +++ b/netbox/ipam/migrations/0067_ipaddressfunction_and_more.py @@ -0,0 +1,44 @@ +# Generated by Django 4.1.9 on 2023-07-06 20:08 + +from django.db import migrations, models +import django.db.models.deletion +import taggit.managers +import utilities.json + + +class Migration(migrations.Migration): + + dependencies = [ + ('extras', '0092_delete_jobresult'), + ('contenttypes', '0002_remove_content_type_name'), + ('ipam', '0066_iprange_mark_utilized'), + ] + + operations = [ + migrations.CreateModel( + name='IPAddressFunction', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)), + ('created', models.DateTimeField(auto_now_add=True, null=True)), + ('last_updated', models.DateTimeField(auto_now=True, null=True)), + ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder)), + ('assigned_object_id', models.PositiveBigIntegerField()), + ('function', models.CharField(max_length=50)), + ('assigned_ip', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='ipam.ipaddress')), + ('assigned_object_type', models.ForeignKey(limit_choices_to=models.Q(models.Q(models.Q(('app_label', 'dcim'), ('model', 'device')), models.Q(('app_label', 'dcim'), ('model', 'virtualdevicecontext')), models.Q(('app_label', 'virtualization'), ('model', 'VirtualMachine')), _connector='OR')), on_delete=django.db.models.deletion.CASCADE, related_name='+', to='contenttypes.contenttype')), + ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')), + ], + options={ + 'verbose_name': 'IP Address Function', + 'ordering': ('function',), + }, + ), + migrations.AddConstraint( + model_name='ipaddressfunction', + constraint=models.UniqueConstraint(fields=('assigned_object_type', 'assigned_object_id', 'function'), name='ipam_ipfunction_assigned_object'), + ), + migrations.AddConstraint( + model_name='ipaddressfunction', + constraint=models.UniqueConstraint(fields=('assigned_ip',), name='ipam_ipfunction_ip_single_use'), + ), + ] diff --git a/netbox/ipam/models/ip.py b/netbox/ipam/models/ip.py index 25517f1f6c5..4fedf714c32 100644 --- a/netbox/ipam/models/ip.py +++ b/netbox/ipam/models/ip.py @@ -20,7 +20,7 @@ __all__ = ( 'Aggregate', 'IPAddress', - 'IPAddressFunction' + 'IPAddressFunction', 'IPRange', 'Prefix', 'RIR', @@ -701,7 +701,7 @@ class Meta: name='ipam_ipfunction_assigned_object' ), models.UniqueConstraint( - fields=('assigned_ip'), + fields=('assigned_ip',), name='ipam_ipfunction_ip_single_use' ), ) From 603f0214659910f1f653a8f00e623cafba50a006 Mon Sep 17 00:00:00 2001 From: Jamie Murphy Date: Thu, 6 Jul 2023 21:51:45 +0100 Subject: [PATCH 5/8] adjust model name to IPAddressFunctionAssignments --- netbox/dcim/models/devices.py | 4 ++-- ...n_ipaddressfunctionassignments_and_more.py | 23 +++++++++++++++++++ netbox/ipam/models/__init__.py | 1 + netbox/ipam/models/ip.py | 6 ++--- .../virtualization/models/virtualmachines.py | 2 +- 5 files changed, 30 insertions(+), 6 deletions(-) create mode 100644 netbox/ipam/migrations/0068_rename_ipaddressfunction_ipaddressfunctionassignments_and_more.py diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py index 9852f76e1dc..e48f8f57aa3 100644 --- a/netbox/dcim/models/devices.py +++ b/netbox/dcim/models/devices.py @@ -646,7 +646,7 @@ class Device(PrimaryModel, ConfigContextModel): to='extras.ImageAttachment' ) ipaddressfunctions = GenericRelation( - to='ipam.IPAddressFunction' + to='ipam.IPAddressFunctionAssignments' ) objects = ConfigContextModelQuerySet.as_manager() @@ -1236,7 +1236,7 @@ class VirtualDeviceContext(PrimaryModel): # Generic relation ipaddressfunctions = GenericRelation( - to='ipam.IPAddressFunction' + to='ipam.IPAddressFunctionAssignments' ) class Meta: diff --git a/netbox/ipam/migrations/0068_rename_ipaddressfunction_ipaddressfunctionassignments_and_more.py b/netbox/ipam/migrations/0068_rename_ipaddressfunction_ipaddressfunctionassignments_and_more.py new file mode 100644 index 00000000000..ed3529af679 --- /dev/null +++ b/netbox/ipam/migrations/0068_rename_ipaddressfunction_ipaddressfunctionassignments_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 4.1.9 on 2023-07-06 20:51 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('extras', '0092_delete_jobresult'), + ('contenttypes', '0002_remove_content_type_name'), + ('ipam', '0067_ipaddressfunction_and_more'), + ] + + operations = [ + migrations.RenameModel( + old_name='IPAddressFunction', + new_name='IPAddressFunctionAssignments', + ), + migrations.AlterModelOptions( + name='ipaddressfunctionassignments', + options={'ordering': ('function',), 'verbose_name': 'IP Address Function Assignments'}, + ), + ] diff --git a/netbox/ipam/models/__init__.py b/netbox/ipam/models/__init__.py index a00919ee0eb..2f969f1fdaf 100644 --- a/netbox/ipam/models/__init__.py +++ b/netbox/ipam/models/__init__.py @@ -12,6 +12,7 @@ 'ASNRange', 'Aggregate', 'IPAddress', + 'IPAddressFunctionAssignments', 'IPRange', 'FHRPGroup', 'FHRPGroupAssignment', diff --git a/netbox/ipam/models/ip.py b/netbox/ipam/models/ip.py index 4fedf714c32..d1eace08a0c 100644 --- a/netbox/ipam/models/ip.py +++ b/netbox/ipam/models/ip.py @@ -20,7 +20,7 @@ __all__ = ( 'Aggregate', 'IPAddress', - 'IPAddressFunction', + 'IPAddressFunctionAssignments', 'IPRange', 'Prefix', 'RIR', @@ -668,7 +668,7 @@ def utilization(self): return int(float(child_count) / self.size * 100) -class IPAddressFunction(NetBoxModel): +class IPAddressFunctionAssignments(NetBoxModel): assigned_object_type = models.ForeignKey( to=ContentType, limit_choices_to=IPADDRESS_FUNCTION_ASSIGNMENT_MODELS, @@ -694,7 +694,7 @@ class IPAddressFunction(NetBoxModel): class Meta: ordering = ('function',) - verbose_name = 'IP Address Function' + verbose_name = 'IP Address Function Assignments' constraints = ( models.UniqueConstraint( fields=('assigned_object_type', 'assigned_object_id', 'function'), diff --git a/netbox/virtualization/models/virtualmachines.py b/netbox/virtualization/models/virtualmachines.py index 5d6a4ea29bd..06fb6277c14 100644 --- a/netbox/virtualization/models/virtualmachines.py +++ b/netbox/virtualization/models/virtualmachines.py @@ -125,7 +125,7 @@ class VirtualMachine(PrimaryModel, ConfigContextModel): to='tenancy.ContactAssignment' ) ipaddressfunctions = GenericRelation( - to='ipam.IPAddressFunction' + to='ipam.IPAddressFunctionAssignments' ) objects = ConfigContextModelQuerySet.as_manager() From 4920539c522f931a527d421c796f55f8dddfb0f4 Mon Sep 17 00:00:00 2001 From: Jamie Murphy Date: Thu, 6 Jul 2023 23:53:47 +0100 Subject: [PATCH 6/8] IPAddressFunctionAssignments - api mvp --- netbox/ipam/api/serializers.py | 25 ++++++++++++++++++++++++- netbox/ipam/api/urls.py | 1 + netbox/ipam/api/views.py | 6 ++++++ netbox/ipam/choices.py | 5 +++-- 4 files changed, 34 insertions(+), 3 deletions(-) diff --git a/netbox/ipam/api/serializers.py b/netbox/ipam/api/serializers.py index 064452667a7..0b952f1df73 100644 --- a/netbox/ipam/api/serializers.py +++ b/netbox/ipam/api/serializers.py @@ -4,7 +4,7 @@ from dcim.api.nested_serializers import NestedDeviceSerializer, NestedSiteSerializer from ipam.choices import * -from ipam.constants import IPADDRESS_ASSIGNMENT_MODELS, VLANGROUP_SCOPE_TYPES +from ipam.constants import IPADDRESS_ASSIGNMENT_MODELS, VLANGROUP_SCOPE_TYPES, IPADDRESS_FUNCTION_ASSIGNMENT_MODELS from ipam.models import * from netbox.api.fields import ChoiceField, ContentTypeField, SerializedPKRelatedField from netbox.api.serializers import NetBoxModelSerializer @@ -445,6 +445,29 @@ def to_representation(self, instance): } +class IPAddressFunctionAssignmentsSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='ipam-api:ipaddressfunctionassignments-detail') + assigned_object_type = ContentTypeField( + queryset=ContentType.objects.filter(IPADDRESS_FUNCTION_ASSIGNMENT_MODELS), + ) + assigned_object = serializers.SerializerMethodField(read_only=True) + assigned_ip = NestedIPAddressSerializer() + + class Meta: + model = IPAddressFunctionAssignments + fields = [ + 'id', 'url', 'display', 'assigned_ip', 'function', + 'assigned_object_type', 'assigned_object_id', 'assigned_object', + 'tags', 'custom_fields', 'created', 'last_updated', + ] + + @extend_schema_field(serializers.JSONField(allow_null=True)) + def get_assigned_object(self, instance): + serializer = get_serializer_for_model(instance.assigned_object, prefix=NESTED_SERIALIZER_PREFIX) + context = {'request': self.context['request']} + return serializer(instance.assigned_object, context=context).data + + # # Services # diff --git a/netbox/ipam/api/urls.py b/netbox/ipam/api/urls.py index 442fd22403a..06cade5a79c 100644 --- a/netbox/ipam/api/urls.py +++ b/netbox/ipam/api/urls.py @@ -17,6 +17,7 @@ router.register('prefixes', views.PrefixViewSet) router.register('ip-ranges', views.IPRangeViewSet) router.register('ip-addresses', views.IPAddressViewSet) +router.register('ip-address-function-assignments', views.IPAddressFunctionAssignmentsViewSet) router.register('fhrp-groups', views.FHRPGroupViewSet) router.register('fhrp-group-assignments', views.FHRPGroupAssignmentViewSet) router.register('vlan-groups', views.VLANGroupViewSet) diff --git a/netbox/ipam/api/views.py b/netbox/ipam/api/views.py index f432e0e6b06..0bce6cc8769 100644 --- a/netbox/ipam/api/views.py +++ b/netbox/ipam/api/views.py @@ -131,6 +131,12 @@ def destroy(self, request, *args, **kwargs): return super().destroy(request, *args, **kwargs) +class IPAddressFunctionAssignmentsViewSet(NetBoxModelViewSet): + queryset = IPAddressFunctionAssignments.objects.prefetch_related('assigned_ip', 'assigned_object', 'tags') + serializer_class = serializers.IPAddressFunctionAssignmentsSerializer + # filterset_class = filtersets. # TODO add filterset + + class FHRPGroupViewSet(NetBoxModelViewSet): queryset = FHRPGroup.objects.prefetch_related('ip_addresses', 'tags') serializer_class = serializers.FHRPGroupSerializer diff --git a/netbox/ipam/choices.py b/netbox/ipam/choices.py index bc3dec1165b..9a96b510faa 100644 --- a/netbox/ipam/choices.py +++ b/netbox/ipam/choices.py @@ -94,16 +94,17 @@ class IPAddressRoleChoices(ChoiceSet): (ROLE_CARP, 'CARP', 'green'), ) + class IPAddressFunctionChoices(ChoiceSet): FUNC_OOB = 'Out Of Band' # Future planning, depreciate primary_ip - # FUNC_PRIMARY_IP = 'Primary IP' + # FUNC_PRIMARY_IP = 'Primary IP' CHOICES = ( (FUNC_OOB, 'Out Of Band', 'gray'), # Future planning, depreciate primary_ip - # (FUNC_PRIMARY_IP, 'Primary IP', 'blue'), + # (FUNC_PRIMARY_IP, 'Primary IP', 'blue'), ) From 432d37dd0943b43b7a7b3d18d69a743c0864aa63 Mon Sep 17 00:00:00 2001 From: Jamie Murphy Date: Fri, 7 Jul 2023 01:15:41 +0100 Subject: [PATCH 7/8] show ip address functions on device api view (rough first pass) --- netbox/dcim/api/serializers.py | 15 +++++++++++++-- netbox/ipam/api/nested_serializers.py | 13 +++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 2f854d3e4a3..947f8fb4a58 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -13,9 +13,9 @@ from extras.api.nested_serializers import NestedConfigTemplateSerializer from ipam.api.nested_serializers import ( NestedASNSerializer, NestedIPAddressSerializer, NestedL2VPNTerminationSerializer, NestedVLANSerializer, - NestedVRFSerializer, + NestedVRFSerializer, NestedIPAddressFunctionAssignmentsSerializer ) -from ipam.models import ASN, VLAN +from ipam.models import ASN, VLAN, IPAddressFunctionAssignments from netbox.api.fields import ChoiceField, ContentTypeField, SerializedPKRelatedField from netbox.api.serializers import ( GenericObjectSerializer, NestedGroupModelSerializer, NetBoxModelSerializer, ValidatedModelSerializer, @@ -668,6 +668,7 @@ class DeviceSerializer(NetBoxModelSerializer): virtual_chassis = NestedVirtualChassisSerializer(required=False, allow_null=True, default=None) vc_position = serializers.IntegerField(allow_null=True, max_value=255, min_value=0, default=None) config_template = NestedConfigTemplateSerializer(required=False, allow_null=True, default=None) + ipaddressfunctions = serializers.SerializerMethodField() class Meta: model = Device @@ -676,6 +677,7 @@ class Meta: 'site', 'location', 'rack', 'position', 'face', 'parent_device', 'status', 'airflow', 'primary_ip', 'primary_ip4', 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'description', 'comments', 'config_template', 'local_context_data', 'tags', 'custom_fields', 'created', 'last_updated', + 'ipaddressfunctions', ] @extend_schema_field(NestedDeviceSerializer) @@ -689,6 +691,14 @@ def get_parent_device(self, obj): data['device_bay'] = NestedDeviceBaySerializer(instance=device_bay, context=context).data return data + @extend_schema_field(NestedDeviceSerializer) + def get_ipaddressfunctions(self, obj): + ct = ContentType.objects.get_for_model(obj) + ipaddrfuncs = IPAddressFunctionAssignments.objects.filter(assigned_object_type=ct, assigned_object_id=obj.id) + serializer = NestedIPAddressFunctionAssignmentsSerializer + context = {'request': self.context['request']} + return serializer(ipaddrfuncs, context=context, many=True).data + class DeviceWithConfigContextSerializer(DeviceSerializer): config_context = serializers.SerializerMethodField(read_only=True) @@ -699,6 +709,7 @@ class Meta(DeviceSerializer.Meta): 'site', 'location', 'rack', 'position', 'face', 'parent_device', 'status', 'airflow', 'primary_ip', 'primary_ip4', 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'description', 'comments', 'local_context_data', 'tags', 'custom_fields', 'config_context', 'created', 'last_updated', + 'ipaddressfunctions', ] @extend_schema_field(serializers.JSONField(allow_null=True)) diff --git a/netbox/ipam/api/nested_serializers.py b/netbox/ipam/api/nested_serializers.py index 9e150e2cb09..4c8f37476d8 100644 --- a/netbox/ipam/api/nested_serializers.py +++ b/netbox/ipam/api/nested_serializers.py @@ -13,6 +13,7 @@ 'NestedFHRPGroupSerializer', 'NestedFHRPGroupAssignmentSerializer', 'NestedIPAddressSerializer', + 'NestedIPAddressFunctionAssignmentsSerializer', 'NestedIPRangeSerializer', 'NestedL2VPNSerializer', 'NestedL2VPNTerminationSerializer', @@ -205,6 +206,18 @@ class Meta: fields = ['id', 'url', 'display', 'family', 'address'] +class NestedIPAddressFunctionAssignmentsSerializer(WritableNestedSerializer): + url = serializers.HyperlinkedIdentityField(view_name='ipam-api:ipaddressfunctionassignments-detail') + # assigned_object = serializers.SerializerMethodField(read_only=True) + assigned_ip = NestedIPAddressSerializer() + + class Meta: + model = models.IPAddressFunctionAssignments + fields = [ + 'id', 'url', 'display', 'assigned_ip', 'function' + ] + + # # Services # From ca03c17326c715c6f1061c1ffca660ab819e28f2 Mon Sep 17 00:00:00 2001 From: Jamie Murphy Date: Tue, 18 Jul 2023 12:39:47 +0100 Subject: [PATCH 8/8] ipaddressfunctionassignments adding starting point for ui views / add / edit / delete --- netbox/ipam/forms/model_forms.py | 46 +++++++++++++++++ netbox/ipam/models/ip.py | 4 +- netbox/ipam/tables/ip.py | 20 ++++++++ netbox/ipam/urls.py | 7 +++ netbox/ipam/views.py | 51 +++++++++++++++++++ .../ipam/ipaddressfunctionassignments.html | 50 ++++++++++++++++++ 6 files changed, 176 insertions(+), 2 deletions(-) create mode 100644 netbox/templates/ipam/ipaddressfunctionassignments.html diff --git a/netbox/ipam/forms/model_forms.py b/netbox/ipam/forms/model_forms.py index a3c218fc9e7..e0259b84b37 100644 --- a/netbox/ipam/forms/model_forms.py +++ b/netbox/ipam/forms/model_forms.py @@ -29,6 +29,7 @@ 'IPAddressBulkAddForm', 'IPAddressForm', 'IPRangeForm', + 'IPAddressFunctionAssignmentsForm', 'L2VPNForm', 'L2VPNTerminationForm', 'PrefixForm', @@ -422,6 +423,51 @@ class IPAddressAssignForm(BootstrapMixin, forms.Form): ) +class IPAddressFunctionAssignmentsForm(NetBoxModelForm): + device_object = DynamicModelChoiceField( + queryset=Device.objects.all(), + required=False, + selector=True + ) + vm_object = DynamicModelChoiceField( + queryset=VirtualMachine.objects.all(), + required=False, + selector=True + ) + + class Meta: + model = IPAddressFunctionAssignments + fields = [ + 'assigned_ip', 'function', 'tags', + ] + + def __init__(self, *args, **kwargs): + instance = kwargs.get('instance') + initial = kwargs.get('initial', {}).copy() + + if instance: + if type(instance.assigned_object) is Device: + initial['device_object'] = instance.assigned_object + elif type(instance.assigned_object) is VirtualMachine: + initial['vm_object'] = instance.assigned_object + kwargs['initial'] = initial + + super().__init__(*args, **kwargs) + + def clean(self): + super().clean() + + device_object = self.cleaned_data.get('device_object') + vm_object = self.cleaned_data.get('vm_object') + + if not (device_object or vm_object): + raise ValidationError('An ip address function assignment must specify an device or virtualmachine.') + if len([x for x in (device_object, vm_object) if x]) > 1: + raise ValidationError('An ip address function assignment can only have one terminating object (a device or virtualmachine).') + + self.instance.assigned_object = device_object or vm_object + + class FHRPGroupForm(NetBoxModelForm): # Optionally create a new IPAddress along with the FHRPGroup diff --git a/netbox/ipam/models/ip.py b/netbox/ipam/models/ip.py index 7df1c349c4f..075600bbc5d 100644 --- a/netbox/ipam/models/ip.py +++ b/netbox/ipam/models/ip.py @@ -708,11 +708,11 @@ class Meta: def __str__(self): if self.pk is not None: - return f'{self.assigned_object_type} - {self.function} - {self.assigned_ip}' + return f'{self.assigned_object} - {self.function} - {self.assigned_ip}' return super().__str__() def get_absolute_url(self): - return reverse('ipam:ipaddressfunction', args=[self.pk]) + return reverse('ipam:ipaddressfunctionassignments', args=[self.pk]) class IPAddress(PrimaryModel): diff --git a/netbox/ipam/tables/ip.py b/netbox/ipam/tables/ip.py index aff090f3a39..9904c31460d 100644 --- a/netbox/ipam/tables/ip.py +++ b/netbox/ipam/tables/ip.py @@ -11,6 +11,7 @@ 'AssignedIPAddressesTable', 'IPAddressAssignTable', 'IPAddressTable', + 'IPAddressFunctionAssignmentsTable', 'IPRangeTable', 'PrefixTable', 'RIRTable', @@ -424,3 +425,22 @@ class Meta(NetBoxTable.Meta): model = IPAddress fields = ('address', 'vrf', 'status', 'role', 'tenant', 'description') exclude = ('id', ) + + +# +# IPAddresses +# + +class IPAddressFunctionAssignmentsTable(TenancyColumnsMixin, NetBoxTable): + + assigned_object = tables.Column(linkify=True) + assigned_ip = tables.Column(linkify=True) + + class Meta(NetBoxTable.Meta): + model = IPAddressFunctionAssignments + fields = ( + 'pk', 'id', 'function', 'assigned_ip', 'assigned_object', 'created', 'last_updated', + ) + default_columns = ( + 'pk', 'assigned_object', 'function', 'assigned_ip', + ) diff --git a/netbox/ipam/urls.py b/netbox/ipam/urls.py index 3bfe34b7bc7..c37b2aa1707 100644 --- a/netbox/ipam/urls.py +++ b/netbox/ipam/urls.py @@ -88,6 +88,13 @@ path('ip-addresses/assign/', views.IPAddressAssignView.as_view(), name='ipaddress_assign'), path('ip-addresses//', include(get_model_urls('ipam', 'ipaddress'))), + # IP address Function Assignments + path('ip-addresses-function-assignments/', views.IPAddressFunctionAssignmentsListView.as_view(), name='ipaddressfunctionassignments_list'), + path('ip-addresses-function-assignments/add/', views.IPAddressFunctionAssignmentsEditView.as_view(), name='ipaddressfunctionassignments_add'), + path('ip-addresses-function-assignments/edit/', views.IPAddressFunctionAssignmentsEditView.as_view(), name='IPAddressFunctionAssignments_edit'), + path('ip-addresses-function-assignments/delete/', views.IPAddressFunctionAssignmentsDeleteView.as_view(), name='IPAddressFunctionAssignments_delete'), + path('ip-addresses-function-assignments//', include(get_model_urls('ipam', 'ipaddressfunctionassignments'))), + # FHRP groups path('fhrp-groups/', views.FHRPGroupListView.as_view(), name='fhrpgroup_list'), path('fhrp-groups/add/', views.FHRPGroupEditView.as_view(), name='fhrpgroup_add'), diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index 32badd2d503..e0e89cfa227 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -878,10 +878,61 @@ def get_children(self, request, parent): return parent.get_related_ips().restrict(request.user, 'view') +# +# IP addresses +# + + +class IPAddressFunctionAssignmentsListView(generic.ObjectListView): + queryset = IPAddressFunctionAssignments.objects.all() + # filterset = filtersets.IPAddressFunctionAssignmentsFilterSet + # filterset_form = forms.IPAddressFunctionAssignmentsFilterForm + table = tables.IPAddressFunctionAssignmentsTable + + +@register_model_view(IPAddressFunctionAssignments) +class IPAddressFunctionAssignmentsView(generic.ObjectView): + queryset = IPAddressFunctionAssignments.objects.all() + + +@register_model_view(IPAddressFunctionAssignments, 'edit') +class IPAddressFunctionAssignmentsEditView(generic.ObjectEditView): + queryset = IPAddressFunctionAssignments.objects.all() + form = forms.IPAddressFunctionAssignmentsForm + # template_name = 'ipam/ipaddress_edit.html' + + # def alter_object(self, obj, request, url_args, url_kwargs): + + # if 'interface' in request.GET: + # try: + # obj.assigned_object = Interface.objects.get(pk=request.GET['interface']) + # except (ValueError, Interface.DoesNotExist): + # pass + + # elif 'vminterface' in request.GET: + # try: + # obj.assigned_object = VMInterface.objects.get(pk=request.GET['vminterface']) + # except (ValueError, VMInterface.DoesNotExist): + # pass + + # elif 'fhrpgroup' in request.GET: + # try: + # obj.assigned_object = FHRPGroup.objects.get(pk=request.GET['fhrpgroup']) + # except (ValueError, FHRPGroup.DoesNotExist): + # pass + + # return obj + + +@register_model_view(IPAddressFunctionAssignments, 'delete') +class IPAddressFunctionAssignmentsDeleteView(generic.ObjectDeleteView): + queryset = IPAddressFunctionAssignments.objects.all() + # # VLAN groups # + class VLANGroupListView(generic.ObjectListView): queryset = VLANGroup.objects.annotate_utilization().prefetch_related('tags') filterset = filtersets.VLANGroupFilterSet diff --git a/netbox/templates/ipam/ipaddressfunctionassignments.html b/netbox/templates/ipam/ipaddressfunctionassignments.html new file mode 100644 index 00000000000..8e71342d2da --- /dev/null +++ b/netbox/templates/ipam/ipaddressfunctionassignments.html @@ -0,0 +1,50 @@ +{% extends 'generic/object.html' %} +{% load helpers %} +{% load plugins %} +{% load render_table from django_tables2 %} + +{% block content %} +
+
+
+
+ IP Address Function Assignment +
+
+ + + + + + + + + + + + + +
Function{{ object.function }}
Assigned IP{{ object.assigned_ip|linkify }}
Assigned Object + {% if object.assigned_object %} + {{ object.assigned_object|linkify }} + {% else %} + {{ ''|placeholder }} + {% endif %} +
+
+
+ {% include 'inc/panels/tags.html' %} + {% include 'inc/panels/custom_fields.html' %} + {% include 'inc/panels/comments.html' %} + {% plugin_left_page object %} + + + {% plugin_right_page object %} +
+
+
+
+ {% plugin_full_width_page object %} +
+
+{% endblock %}