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 all commits
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
42 changes: 7 additions & 35 deletions netbox/dcim/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,12 @@
from tenancy.models import Tenant, TenantGroup
from utilities.forms import (
APISelect, APISelectMultiple, add_blank_choice, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect,
ColorSelect, CommentField, ConfirmationForm, CSVChoiceField, CSVModelChoiceField, CSVModelForm,
BulkRenameForm, ColorSelect, CommentField, ConfirmationForm, CSVChoiceField, CSVModelChoiceField, CSVModelForm,
DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableNameField, form_from_model, JSONField,
NumericArrayField, SelectWithPK, SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple, TagFilterField,
BOOLEAN_WITH_BLANK_CHOICES,
)
from virtualization.models import Cluster, ClusterGroup, VirtualMachine
from virtualization.models import Cluster, ClusterGroup
from .choices import *
from .constants import *
from .models import (
Expand Down Expand Up @@ -150,30 +150,6 @@ def clean(self):
}, code='label_pattern_mismatch')


class BulkRenameForm(forms.Form):
"""
An extendable form to be used for renaming device components in bulk.
"""
find = forms.CharField()
replace = forms.CharField()
use_regex = forms.BooleanField(
required=False,
initial=True,
label='Use regular expressions'
)

def clean(self):

# Validate regular expression in "find" field
if self.cleaned_data['use_regex']:
try:
re.compile(self.cleaned_data['find'])
except re.error:
raise forms.ValidationError({
'find': "Invalid regular expression"
})


#
# Fields
#
Expand Down Expand Up @@ -1816,18 +1792,20 @@ def __init__(self, *args, **kwargs):
ip_choices = [(None, '---------')]

# Gather PKs of all interfaces belonging to this Device or a peer VirtualChassis member
interface_ids = self.instance.vc_interfaces.values('pk')
interface_ids = self.instance.vc_interfaces.values_list('pk', flat=True)

# Collect interface IPs
interface_ips = IPAddress.objects.prefetch_related('interface').filter(
address__family=family, interface_id__in=interface_ids
address__family=family,
interface__in=interface_ids
)
if interface_ips:
ip_list = [(ip.id, '{} ({})'.format(ip.address, ip.interface)) for ip in interface_ips]
ip_choices.append(('Interface IPs', ip_list))
# Collect NAT IPs
nat_ips = IPAddress.objects.prefetch_related('nat_inside').filter(
address__family=family, nat_inside__interface__in=interface_ids
address__family=family,
nat_inside__interface__in=interface_ids
)
if nat_ips:
ip_list = [(ip.id, '{} ({})'.format(ip.address, ip.nat_inside.address)) for ip in nat_ips]
Expand Down Expand Up @@ -2961,12 +2939,6 @@ class InterfaceBulkDisconnectForm(ConfirmationForm):
class InterfaceCSVForm(CSVModelForm):
device = CSVModelChoiceField(
queryset=Device.objects.all(),
required=False,
to_field_name='name'
)
virtual_machine = CSVModelChoiceField(
queryset=VirtualMachine.objects.all(),
required=False,
to_field_name='name'
)
lag = CSVModelChoiceField(
Expand Down
18 changes: 18 additions & 0 deletions netbox/dcim/migrations/0109_interface_remove_vm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 3.0.6 on 2020-06-22 16:03

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
('dcim', '0108_add_tags'),
('virtualization', '0016_replicate_interfaces'),
]

operations = [
migrations.RemoveField(
model_name='interface',
name='virtual_machine',
),
]
5 changes: 3 additions & 2 deletions netbox/dcim/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,12 @@
PowerOutletTemplate, PowerPortTemplate, RearPortTemplate,
)
from .device_components import (
CableTermination, ConsolePort, ConsoleServerPort, DeviceBay, FrontPort, Interface, InventoryItem, PowerOutlet,
PowerPort, RearPort,
BaseInterface, CableTermination, ConsolePort, ConsoleServerPort, DeviceBay, FrontPort, Interface, InventoryItem,
PowerOutlet, PowerPort, RearPort,
)

__all__ = (
'BaseInterface',
'Cable',
'CableTermination',
'ConsolePort',
Expand Down
127 changes: 47 additions & 80 deletions netbox/dcim/models/device_components.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
from utilities.querysets import RestrictedQuerySet
from utilities.query_functions import CollateAsChar
from utilities.utils import serialize_object
from virtualization.choices import VMInterfaceTypeChoices


__all__ = (
Expand Down Expand Up @@ -53,18 +52,12 @@ def __str__(self):
return self.name

def to_objectchange(self, action):
# Annotate the parent Device/VM
try:
parent = getattr(self, 'device', None) or getattr(self, 'virtual_machine', None)
except ObjectDoesNotExist:
# The parent device/VM has already been deleted
parent = None

# Annotate the parent Device
return ObjectChange(
changed_object=self,
object_repr=str(self),
action=action,
related_object=parent,
related_object=self.device,
object_data=serialize_object(self)
)

Expand Down Expand Up @@ -592,11 +585,44 @@ def clean(self):
# Interfaces
#

class BaseInterface(models.Model):
name = models.CharField(
max_length=64
)
_name = NaturalOrderingField(
target_field='name',
naturalize_function=naturalize_interface,
max_length=100,
blank=True
)
enabled = models.BooleanField(
default=True
)
mac_address = MACAddressField(
null=True,
blank=True,
verbose_name='MAC Address'
)
mtu = models.PositiveIntegerField(
blank=True,
null=True,
validators=[MinValueValidator(1), MaxValueValidator(65536)],
verbose_name='MTU'
)
mode = models.CharField(
max_length=50,
choices=InterfaceModeChoices,
blank=True
)

class Meta:
abstract = True


@extras_features('graphs', 'export_templates', 'webhooks')
class Interface(CableTermination, ComponentModel):
class Interface(CableTermination, ComponentModel, BaseInterface):
"""
A network interface within a Device or VirtualMachine. A physical Interface can connect to exactly one other
Interface.
A network interface within a Device. A physical Interface can connect to exactly one other Interface.
"""
device = models.ForeignKey(
to='Device',
Expand All @@ -605,22 +631,6 @@ class Interface(CableTermination, ComponentModel):
null=True,
blank=True
)
virtual_machine = models.ForeignKey(
to='virtualization.VirtualMachine',
on_delete=models.CASCADE,
related_name='interfaces',
null=True,
blank=True
)
name = models.CharField(
max_length=64
)
_name = NaturalOrderingField(
target_field='name',
naturalize_function=naturalize_interface,
max_length=100,
blank=True
)
label = models.CharField(
max_length=64,
blank=True,
Expand Down Expand Up @@ -656,30 +666,11 @@ class Interface(CableTermination, ComponentModel):
max_length=50,
choices=InterfaceTypeChoices
)
enabled = models.BooleanField(
default=True
)
mac_address = MACAddressField(
null=True,
blank=True,
verbose_name='MAC Address'
)
mtu = models.PositiveIntegerField(
blank=True,
null=True,
validators=[MinValueValidator(1), MaxValueValidator(65536)],
verbose_name='MTU'
)
mgmt_only = models.BooleanField(
default=False,
verbose_name='OOB Management',
help_text='This interface is used only for out-of-band management'
)
mode = models.CharField(
max_length=50,
choices=InterfaceModeChoices,
blank=True
)
untagged_vlan = models.ForeignKey(
to='ipam.VLAN',
on_delete=models.SET_NULL,
Expand All @@ -694,15 +685,19 @@ class Interface(CableTermination, ComponentModel):
blank=True,
verbose_name='Tagged VLANs'
)
ip_addresses = GenericRelation(
to='ipam.IPAddress',
content_type_field='assigned_object_type',
object_id_field='assigned_object_id',
related_query_name='interface'
)
tags = TaggableManager(through=TaggedItem)

csv_headers = [
'device', 'virtual_machine', 'name', 'lag', 'type', 'enabled', 'mac_address', 'mtu', 'mgmt_only',
'description', 'mode',
'device', 'name', 'lag', 'type', 'enabled', 'mac_address', 'mtu', 'mgmt_only', 'description', 'mode',
]

class Meta:
# TODO: ordering and unique_together should include virtual_machine
ordering = ('device', CollateAsChar('_name'))
unique_together = ('device', 'name')

Expand All @@ -712,7 +707,6 @@ def get_absolute_url(self):
def to_csv(self):
return (
self.device.identifier if self.device else None,
self.virtual_machine.name if self.virtual_machine else None,
self.name,
self.lag.name if self.lag else None,
self.get_type_display(),
Expand All @@ -726,18 +720,6 @@ def to_csv(self):

def clean(self):

# An Interface must belong to a Device *or* to a VirtualMachine
if self.device and self.virtual_machine:
raise ValidationError("An interface cannot belong to both a device and a virtual machine.")
if not self.device and not self.virtual_machine:
raise ValidationError("An interface must belong to either a device or a virtual machine.")

# VM interfaces must be virtual
if self.virtual_machine and self.type not in VMInterfaceTypeChoices.values():
raise ValidationError({
'type': "Invalid interface type for a virtual machine: {}".format(self.type)
})

# Virtual interfaces cannot be connected
if self.type in NONCONNECTABLE_IFACE_TYPES and (
self.cable or getattr(self, 'circuit_termination', False)
Expand Down Expand Up @@ -773,7 +755,7 @@ def clean(self):
if self.untagged_vlan and self.untagged_vlan.site not in [self.parent.site, None]:
raise ValidationError({
'untagged_vlan': "The untagged VLAN ({}) must belong to the same site as the interface's parent "
"device/VM, or it must be global".format(self.untagged_vlan)
"device, or it must be global".format(self.untagged_vlan)
})

def save(self, *args, **kwargs):
Expand All @@ -788,21 +770,6 @@ def save(self, *args, **kwargs):

return super().save(*args, **kwargs)

def to_objectchange(self, action):
# Annotate the parent Device/VM
try:
parent_obj = self.device or self.virtual_machine
except ObjectDoesNotExist:
parent_obj = None

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

@property
def connected_endpoint(self):
"""
Expand Down Expand Up @@ -841,7 +808,7 @@ def connected_endpoint(self, value):

@property
def parent(self):
return self.device or self.virtual_machine
return self.device

@property
def is_connectable(self):
Expand Down
Loading