Skip to content
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

#4721: Move VM interfaces to a separate model (WIP) #4781

Merged
merged 30 commits into from
Jun 24, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
181bcd7
Fix schema migrations for device components
jeremystretch Jun 22, 2020
6cb31a2
Initial work on #4721 (WIP)
jeremystretch Jun 22, 2020
e76b1f1
Fix assigned_object field
jeremystretch Jun 22, 2020
2608b3f
Separate VM interface view and template
jeremystretch Jun 22, 2020
31bb70d
Fixed IPAM tests
jeremystretch Jun 22, 2020
f2b2628
Disable VM interface bulk creation testing
jeremystretch Jun 22, 2020
380a5cf
Fix IP choices for DeviceForm
jeremystretch Jun 22, 2020
37564d6
Misc test fixes
jeremystretch Jun 22, 2020
7b24984
Update IPAddressSerializer
jeremystretch Jun 22, 2020
490dee1
Merge branch 'develop-2.9' into 4721-virtualmachine-interface
jeremystretch Jun 22, 2020
40938f0
Retain ip_addresses name for related IPAddress objects
jeremystretch Jun 22, 2020
fc2d08c
Set related_query_name for GenericRelations to IPAddress
jeremystretch Jun 22, 2020
bb6be8e
Disable editing assigned interface under IPAddress form
jeremystretch Jun 22, 2020
d1bd010
Fix Interface tag replication in schema migration
jeremystretch Jun 23, 2020
75354a8
Rename Interface to VMInterface
jeremystretch Jun 23, 2020
25d6bbf
Update view and permission names for VMInterface
jeremystretch Jun 23, 2020
5ad5994
Update interface view templates
jeremystretch Jun 23, 2020
a1b816b
Remove 'parent' attribute from VMinterface
jeremystretch Jun 23, 2020
548127c
Rename VMInterface serializers
jeremystretch Jun 23, 2020
459e485
Restore interface assignment for IPAddress CSV import
jeremystretch Jun 23, 2020
e3820e9
Misc cleanup, renaming
jeremystretch Jun 23, 2020
fce19a3
Add VMInterface list view
jeremystretch Jun 23, 2020
603c804
Add VMInterface CSV import view
jeremystretch Jun 23, 2020
afda46d
Fix VMInterface bulk creation
jeremystretch Jun 23, 2020
d6386f7
Restore interface filtering for IPAddresses
jeremystretch Jun 24, 2020
6663844
Rename 'vm_interface' to 'vminterface'; misc cleanup
jeremystretch Jun 24, 2020
9a0bc16
Update device/VM interface templates
jeremystretch Jun 24, 2020
052555c
Add bulk renaming function for VM interfaces
jeremystretch Jun 24, 2020
99c72c7
Update VMInterface view names
jeremystretch Jun 24, 2020
4d2c75a
Restore ability to assign interface when editing an IPAddress
jeremystretch Jun 24, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Fixed IPAM tests
  • Loading branch information
jeremystretch committed Jun 22, 2020
commit 31bb70d9a251cfb39cbb77f03907bfd1be12a554
3 changes: 1 addition & 2 deletions netbox/ipam/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,8 +233,7 @@ def available_ips(self, request, pk=None):

class IPAddressViewSet(CustomFieldModelViewSet):
queryset = IPAddress.objects.prefetch_related(
'vrf__tenant', 'tenant', 'nat_inside', 'interface__device__device_type', 'interface__virtual_machine',
'nat_outside', 'tags',
'vrf__tenant', 'tenant', 'nat_inside', 'nat_outside', 'tags',
)
serializer_class = serializers.IPAddressSerializer
filterset_class = filters.IPAddressFilterSet
Expand Down
74 changes: 44 additions & 30 deletions netbox/ipam/filters.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import django_filters
import netaddr
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.db.models import Q
from netaddr.core import AddrFormatError
Expand All @@ -11,7 +12,7 @@
BaseFilterSet, MultiValueCharFilter, MultiValueNumberFilter, NameSlugSearchFilterSet, TagFilter,
TreeNodeMultipleChoiceFilter,
)
from virtualization.models import VirtualMachine
from virtualization.models import Interface as VMInterface, VirtualMachine
from .choices import *
from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF

Expand Down Expand Up @@ -299,27 +300,26 @@ class IPAddressFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldFilterSet,
to_field_name='rd',
label='VRF (RD)',
)
# device = MultiValueCharFilter(
# method='filter_device',
# field_name='name',
# label='Device (name)',
# )
# device_id = MultiValueNumberFilter(
# method='filter_device',
# field_name='pk',
# label='Device (ID)',
# )
# virtual_machine_id = django_filters.ModelMultipleChoiceFilter(
# field_name='interface__virtual_machine',
# queryset=VirtualMachine.objects.unrestricted(),
# label='Virtual machine (ID)',
# )
# virtual_machine = django_filters.ModelMultipleChoiceFilter(
# field_name='interface__virtual_machine__name',
# queryset=VirtualMachine.objects.unrestricted(),
# to_field_name='name',
# label='Virtual machine (name)',
# )
device = MultiValueCharFilter(
method='filter_device',
field_name='name',
label='Device (name)',
)
device_id = MultiValueNumberFilter(
method='filter_device',
field_name='pk',
label='Device (ID)',
)
virtual_machine = MultiValueCharFilter(
method='filter_virtual_machine',
field_name='name',
label='Virtual machine (name)',
)
virtual_machine_id = MultiValueNumberFilter(
method='filter_virtual_machine',
field_name='pk',
label='Virtual machine (ID)',
)
# interface = django_filters.ModelMultipleChoiceFilter(
# field_name='interface__name',
# queryset=Interface.objects.unrestricted(),
Expand Down Expand Up @@ -379,17 +379,31 @@ def filter_mask_length(self, queryset, name, value):
return queryset.filter(address__net_mask_length=value)

def filter_device(self, queryset, name, value):
try:
devices = Device.objects.prefetch_related('device_type').filter(**{'{}__in'.format(name): value})
vc_interface_ids = []
for device in devices:
vc_interface_ids.extend([i['id'] for i in device.vc_interfaces.values('id')])
return queryset.filter(interface_id__in=vc_interface_ids)
except Device.DoesNotExist:
devices = Device.objects.filter(**{'{}__in'.format(name): value})
if not devices.exists():
return queryset.none()
interface_ids = []
for device in devices:
interface_ids.extend(device.vc_interfaces.values_list('id', flat=True))
return queryset.filter(
assigned_object_type=ContentType.objects.get_for_model(Interface),
assigned_object_id__in=interface_ids
)

def filter_virtual_machine(self, queryset, name, value):
virtual_machines = VirtualMachine.objects.filter(**{'{}__in'.format(name): value})
if not virtual_machines.exists():
return queryset.none()
interface_ids = []
for vm in virtual_machines:
interface_ids.extend(vm.interfaces.values_list('id', flat=True))
return queryset.filter(
assigned_object_type=ContentType.objects.get_for_model(VMInterface),
assigned_object_id__in=interface_ids
)

def _assigned_to_interface(self, queryset, name, value):
return queryset.exclude(interface__isnull=value)
return queryset.exclude(assigned_object_id__isnull=value)


class VLANGroupFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
Expand Down
142 changes: 71 additions & 71 deletions netbox/ipam/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -523,10 +523,10 @@ class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm)
#

class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModelForm):
interface = forms.ModelChoiceField(
queryset=Interface.objects.all(),
required=False
)
# interface = forms.ModelChoiceField(
# queryset=Interface.objects.all(),
# required=False
# )
vrf = DynamicModelChoiceField(
queryset=VRF.objects.all(),
required=False,
Expand Down Expand Up @@ -598,8 +598,8 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModel
class Meta:
model = IPAddress
fields = [
'address', 'vrf', 'status', 'role', 'dns_name', 'description', 'interface', 'primary_for_parent',
'nat_site', 'nat_rack', 'nat_inside', 'tenant_group', 'tenant', 'tags',
'address', 'vrf', 'status', 'role', 'dns_name', 'description', 'primary_for_parent', 'nat_site', 'nat_rack',
'nat_inside', 'tenant_group', 'tenant', 'tags',
]
widgets = {
'status': StaticSelect2(),
Expand All @@ -621,27 +621,27 @@ def __init__(self, *args, **kwargs):

self.fields['vrf'].empty_label = 'Global'

# Limit interface selections to those belonging to the parent device/VM
if self.instance and self.instance.interface:
self.fields['interface'].queryset = Interface.objects.filter(
device=self.instance.interface.device, virtual_machine=self.instance.interface.virtual_machine
).prefetch_related(
'device__primary_ip4',
'device__primary_ip6',
'virtual_machine__primary_ip4',
'virtual_machine__primary_ip6',
) # We prefetch the primary address fields to ensure cache invalidation does not balk on the save()
else:
self.fields['interface'].choices = []

# Initialize primary_for_parent if IP address is already assigned
if self.instance.pk and self.instance.interface is not None:
parent = self.instance.interface.parent
if (
self.instance.address.version == 4 and parent.primary_ip4_id == self.instance.pk or
self.instance.address.version == 6 and parent.primary_ip6_id == self.instance.pk
):
self.initial['primary_for_parent'] = True
# # Limit interface selections to those belonging to the parent device/VM
# if self.instance and self.instance.interface:
# self.fields['interface'].queryset = Interface.objects.filter(
# device=self.instance.interface.device, virtual_machine=self.instance.interface.virtual_machine
# ).prefetch_related(
# 'device__primary_ip4',
# 'device__primary_ip6',
# 'virtual_machine__primary_ip4',
# 'virtual_machine__primary_ip6',
# ) # We prefetch the primary address fields to ensure cache invalidation does not balk on the save()
# else:
# self.fields['interface'].choices = []
#
# # Initialize primary_for_parent if IP address is already assigned
# if self.instance.pk and self.instance.interface is not None:
# parent = self.instance.interface.parent
# if (
# self.instance.address.version == 4 and parent.primary_ip4_id == self.instance.pk or
# self.instance.address.version == 6 and parent.primary_ip6_id == self.instance.pk
# ):
# self.initial['primary_for_parent'] = True

def clean(self):
super().clean()
Expand All @@ -664,14 +664,14 @@ def save(self, *args, **kwargs):
else:
parent.primary_ip6 = ipaddress
parent.save()
elif self.cleaned_data['interface']:
parent = self.cleaned_data['interface'].parent
if ipaddress.address.version == 4 and parent.primary_ip4 == ipaddress:
parent.primary_ip4 = None
parent.save()
elif ipaddress.address.version == 6 and parent.primary_ip6 == ipaddress:
parent.primary_ip6 = None
parent.save()
# elif self.cleaned_data['interface']:
# parent = self.cleaned_data['interface'].parent
# if ipaddress.address.version == 4 and parent.primary_ip4 == ipaddress:
# parent.primary_ip4 = None
# parent.save()
# elif ipaddress.address.version == 6 and parent.primary_ip6 == ipaddress:
# parent.primary_ip6 = None
# parent.save()

return ipaddress

Expand Down Expand Up @@ -730,24 +730,24 @@ class IPAddressCSVForm(CustomFieldModelCSVForm):
required=False,
help_text='Functional role'
)
device = CSVModelChoiceField(
queryset=Device.objects.all(),
required=False,
to_field_name='name',
help_text='Parent device of assigned interface (if any)'
)
virtual_machine = CSVModelChoiceField(
queryset=VirtualMachine.objects.all(),
required=False,
to_field_name='name',
help_text='Parent VM of assigned interface (if any)'
)
interface = CSVModelChoiceField(
queryset=Interface.objects.all(),
required=False,
to_field_name='name',
help_text='Assigned interface'
)
# device = CSVModelChoiceField(
# queryset=Device.objects.all(),
# required=False,
# to_field_name='name',
# help_text='Parent device of assigned interface (if any)'
# )
# virtual_machine = CSVModelChoiceField(
# queryset=VirtualMachine.objects.all(),
# required=False,
# to_field_name='name',
# help_text='Parent VM of assigned interface (if any)'
# )
# interface = CSVModelChoiceField(
# queryset=Interface.objects.all(),
# required=False,
# to_field_name='name',
# help_text='Assigned interface'
# )
is_primary = forms.BooleanField(
help_text='Make this the primary IP for the assigned device',
required=False
Expand All @@ -760,23 +760,23 @@ class Meta:
def __init__(self, data=None, *args, **kwargs):
super().__init__(data, *args, **kwargs)

if data:

# Limit interface queryset by assigned device or virtual machine
if data.get('device'):
params = {
f"device__{self.fields['device'].to_field_name}": data.get('device')
}
elif data.get('virtual_machine'):
params = {
f"virtual_machine__{self.fields['virtual_machine'].to_field_name}": data.get('virtual_machine')
}
else:
params = {
'device': None,
'virtual_machine': None,
}
self.fields['interface'].queryset = self.fields['interface'].queryset.filter(**params)
# if data:
#
# # Limit interface queryset by assigned device or virtual machine
# if data.get('device'):
# params = {
# f"device__{self.fields['device'].to_field_name}": data.get('device')
# }
# elif data.get('virtual_machine'):
# params = {
# f"virtual_machine__{self.fields['virtual_machine'].to_field_name}": data.get('virtual_machine')
# }
# else:
# params = {
# 'device': None,
# 'virtual_machine': None,
# }
# self.fields['interface'].queryset = self.fields['interface'].queryset.filter(**params)

def clean(self):
super().clean()
Expand Down Expand Up @@ -1197,7 +1197,7 @@ def __init__(self, *args, **kwargs):
if self.instance.device:
self.fields['ipaddresses'].queryset = IPAddress.objects.filter(
assigned_object_type=ContentType.objects.get_for_model(Interface),
assigned_object_id__in=self.instance.device.vc_interfaces.values('id', flat=True)
assigned_object_id__in=self.instance.device.vc_interfaces.values_list('id', flat=True)
)
elif self.instance.virtual_machine:
self.fields['ipaddresses'].queryset = IPAddress.objects.filter(
Expand Down
29 changes: 5 additions & 24 deletions netbox/ipam/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from django.core.exceptions import ValidationError, ObjectDoesNotExist
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from django.db.models import F, Q
from django.db.models import F
from django.urls import reverse
from taggit.managers import TaggableManager

Expand Down Expand Up @@ -653,7 +653,7 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel):
objects = IPAddressManager()

csv_headers = [
'address', 'vrf', 'tenant', 'status', 'role', 'device', 'virtual_machine', 'interface', 'is_primary',
'address', 'vrf', 'tenant', 'status', 'role', 'assigned_object_type', 'assigned_object_id', 'is_primary',
'dns_name', 'description',
]
clone_fields = [
Expand Down Expand Up @@ -753,17 +753,11 @@ def save(self, *args, **kwargs):
super().save(*args, **kwargs)

def to_objectchange(self, action):
# Annotate the assigned Interface (if any)
try:
parent_obj = self.interface
except ObjectDoesNotExist:
parent_obj = None

return ObjectChange(
changed_object=self,
object_repr=str(self),
action=action,
related_object=parent_obj,
related_object=self.assigned_object,
object_data=serialize_object(self)
)

Expand All @@ -783,9 +777,8 @@ def to_csv(self):
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.virtual_machine.name if self.virtual_machine else None,
self.interface.name if self.interface else None,
'{}.{}'.format(self.assigned_object_type.app_label, self.assigned_object_type.model) if self.assigned_object_type else None,
self.assigned_object_id,
is_primary,
self.dns_name,
self.description,
Expand All @@ -806,18 +799,6 @@ def _set_mask_length(self, value):
self.address.prefixlen = value
mask_length = property(fset=_set_mask_length)

@property
def device(self):
if self.interface:
return self.interface.device
return None

@property
def virtual_machine(self):
if self.interface:
return self.interface.virtual_machine
return None

def get_status_class(self):
return self.STATUS_CLASS_MAP.get(self.status)

Expand Down
4 changes: 2 additions & 2 deletions netbox/ipam/tables.py
Original file line number Diff line number Diff line change
Expand Up @@ -481,13 +481,13 @@ class IPAddressAssignTable(BaseTable):
template_code=IPADDRESS_PARENT,
orderable=False
)
interface = tables.Column(
assigned_object = tables.Column(
orderable=False
)

class Meta(BaseTable.Meta):
model = IPAddress
fields = ('address', 'dns_name', 'vrf', 'status', 'role', 'tenant', 'parent', 'interface', 'description')
fields = ('address', 'dns_name', 'vrf', 'status', 'role', 'tenant', 'parent', 'assigned_object', 'description')
orderable = False


Expand Down
Loading