Skip to content

oob_ip - IPAddressFunctionAssignments - poc #13094

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
16 changes: 13 additions & 3 deletions netbox/dcim/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -699,7 +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', 'config_template',
'created', 'last_updated',
'ipaddressfunctions', 'created', 'last_updated',
]

@extend_schema_field(serializers.JSONField(allow_null=True))
Expand Down
8 changes: 8 additions & 0 deletions netbox/dcim/models/devices.py
Original file line number Diff line number Diff line change
Expand Up @@ -645,6 +645,9 @@ class Device(PrimaryModel, ConfigContextModel):
images = GenericRelation(
to='extras.ImageAttachment'
)
ipaddressfunctions = GenericRelation(
to='ipam.IPAddressFunctionAssignments'
)

objects = ConfigContextModelQuerySet.as_manager()

Expand Down Expand Up @@ -1231,6 +1234,11 @@ class VirtualDeviceContext(PrimaryModel):
blank=True
)

# Generic relation
ipaddressfunctions = GenericRelation(
to='ipam.IPAddressFunctionAssignments'
)

class Meta:
ordering = ['name']
constraints = (
Expand Down
13 changes: 13 additions & 0 deletions netbox/ipam/api/nested_serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
'NestedFHRPGroupSerializer',
'NestedFHRPGroupAssignmentSerializer',
'NestedIPAddressSerializer',
'NestedIPAddressFunctionAssignmentsSerializer',
'NestedIPRangeSerializer',
'NestedL2VPNSerializer',
'NestedL2VPNTerminationSerializer',
Expand Down Expand Up @@ -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
#
Expand Down
25 changes: 24 additions & 1 deletion netbox/ipam/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -446,6 +446,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
#
Expand Down
1 change: 1 addition & 0 deletions netbox/ipam/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
6 changes: 6 additions & 0 deletions netbox/ipam/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,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
Expand Down
13 changes: 13 additions & 0 deletions netbox/ipam/choices.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,19 @@ class IPAddressRoleChoices(ChoiceSet):
)


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
#
Expand Down
6 changes: 6 additions & 0 deletions netbox/ipam/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
#
Expand Down
46 changes: 46 additions & 0 deletions netbox/ipam/forms/model_forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
'IPAddressBulkAddForm',
'IPAddressForm',
'IPRangeForm',
'IPAddressFunctionAssignmentsForm',
'L2VPNForm',
'L2VPNTerminationForm',
'PrefixForm',
Expand Down Expand Up @@ -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
Expand Down
44 changes: 44 additions & 0 deletions netbox/ipam/migrations/0067_ipaddressfunction_and_more.py
Original file line number Diff line number Diff line change
@@ -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'),
),
]
Original file line number Diff line number Diff line change
@@ -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'},
),
]
1 change: 1 addition & 0 deletions netbox/ipam/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
'ASNRange',
'Aggregate',
'IPAddress',
'IPAddressFunctionAssignments',
'IPRange',
'FHRPGroup',
'FHRPGroupAssignment',
Expand Down
50 changes: 49 additions & 1 deletion netbox/ipam/models/ip.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,12 @@
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',
'IPAddress',
'IPAddressFunctionAssignments',
'IPRange',
'Prefix',
'RIR',
Expand Down Expand Up @@ -667,6 +668,53 @@ def utilization(self):
return int(float(child_count) / self.size * 100)


class IPAddressFunctionAssignments(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 Assignments'
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} - {self.function} - {self.assigned_ip}'
return super().__str__()

def get_absolute_url(self):
return reverse('ipam:ipaddressfunctionassignments', 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
Expand Down
Loading