mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
Initial work on #4721 (WIP)
This commit is contained in:
18
netbox/dcim/migrations/0109_interface_remove_vm.py
Normal file
18
netbox/dcim/migrations/0109_interface_remove_vm.py
Normal file
@ -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',
|
||||||
|
),
|
||||||
|
]
|
@ -35,11 +35,12 @@ from .device_component_templates import (
|
|||||||
PowerOutletTemplate, PowerPortTemplate, RearPortTemplate,
|
PowerOutletTemplate, PowerPortTemplate, RearPortTemplate,
|
||||||
)
|
)
|
||||||
from .device_components import (
|
from .device_components import (
|
||||||
CableTermination, ConsolePort, ConsoleServerPort, DeviceBay, FrontPort, Interface, InventoryItem, PowerOutlet,
|
BaseInterface, CableTermination, ConsolePort, ConsoleServerPort, DeviceBay, FrontPort, Interface, InventoryItem,
|
||||||
PowerPort, RearPort,
|
PowerOutlet, PowerPort, RearPort,
|
||||||
)
|
)
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
|
'BaseInterface',
|
||||||
'Cable',
|
'Cable',
|
||||||
'CableTermination',
|
'CableTermination',
|
||||||
'ConsolePort',
|
'ConsolePort',
|
||||||
|
@ -19,7 +19,6 @@ from utilities.ordering import naturalize_interface
|
|||||||
from utilities.querysets import RestrictedQuerySet
|
from utilities.querysets import RestrictedQuerySet
|
||||||
from utilities.query_functions import CollateAsChar
|
from utilities.query_functions import CollateAsChar
|
||||||
from utilities.utils import serialize_object
|
from utilities.utils import serialize_object
|
||||||
from virtualization.choices import VMInterfaceTypeChoices
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
@ -53,18 +52,12 @@ class ComponentModel(models.Model):
|
|||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
def to_objectchange(self, action):
|
def to_objectchange(self, action):
|
||||||
# Annotate the parent Device/VM
|
# Annotate the parent Device
|
||||||
try:
|
|
||||||
parent = getattr(self, 'device', None) or getattr(self, 'virtual_machine', None)
|
|
||||||
except ObjectDoesNotExist:
|
|
||||||
# The parent device/VM has already been deleted
|
|
||||||
parent = None
|
|
||||||
|
|
||||||
return ObjectChange(
|
return ObjectChange(
|
||||||
changed_object=self,
|
changed_object=self,
|
||||||
object_repr=str(self),
|
object_repr=str(self),
|
||||||
action=action,
|
action=action,
|
||||||
related_object=parent,
|
related_object=self.device,
|
||||||
object_data=serialize_object(self)
|
object_data=serialize_object(self)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -592,26 +585,7 @@ class PowerOutlet(CableTermination, ComponentModel):
|
|||||||
# Interfaces
|
# Interfaces
|
||||||
#
|
#
|
||||||
|
|
||||||
@extras_features('graphs', 'export_templates', 'webhooks')
|
class BaseInterface(models.Model):
|
||||||
class Interface(CableTermination, ComponentModel):
|
|
||||||
"""
|
|
||||||
A network interface within a Device or VirtualMachine. A physical Interface can connect to exactly one other
|
|
||||||
Interface.
|
|
||||||
"""
|
|
||||||
device = models.ForeignKey(
|
|
||||||
to='Device',
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
related_name='interfaces',
|
|
||||||
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(
|
name = models.CharField(
|
||||||
max_length=64
|
max_length=64
|
||||||
)
|
)
|
||||||
@ -621,6 +595,43 @@ class Interface(CableTermination, ComponentModel):
|
|||||||
max_length=100,
|
max_length=100,
|
||||||
blank=True
|
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, BaseInterface):
|
||||||
|
"""
|
||||||
|
A network interface within a Device. A physical Interface can connect to exactly one other
|
||||||
|
Interface.
|
||||||
|
"""
|
||||||
|
device = models.ForeignKey(
|
||||||
|
to='Device',
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='interfaces',
|
||||||
|
null=True,
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
label = models.CharField(
|
label = models.CharField(
|
||||||
max_length=64,
|
max_length=64,
|
||||||
blank=True,
|
blank=True,
|
||||||
@ -656,30 +667,11 @@ class Interface(CableTermination, ComponentModel):
|
|||||||
max_length=50,
|
max_length=50,
|
||||||
choices=InterfaceTypeChoices
|
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(
|
mgmt_only = models.BooleanField(
|
||||||
default=False,
|
default=False,
|
||||||
verbose_name='OOB Management',
|
verbose_name='OOB Management',
|
||||||
help_text='This interface is used only for out-of-band 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(
|
untagged_vlan = models.ForeignKey(
|
||||||
to='ipam.VLAN',
|
to='ipam.VLAN',
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
@ -694,15 +686,19 @@ class Interface(CableTermination, ComponentModel):
|
|||||||
blank=True,
|
blank=True,
|
||||||
verbose_name='Tagged VLANs'
|
verbose_name='Tagged VLANs'
|
||||||
)
|
)
|
||||||
|
ipaddresses = GenericRelation(
|
||||||
|
to='ipam.IPAddress',
|
||||||
|
content_type_field='assigned_object_type',
|
||||||
|
object_id_field='assigned_object_id'
|
||||||
|
)
|
||||||
tags = TaggableManager(through=TaggedItem)
|
tags = TaggableManager(through=TaggedItem)
|
||||||
|
|
||||||
csv_headers = [
|
csv_headers = [
|
||||||
'device', 'virtual_machine', 'name', 'lag', 'type', 'enabled', 'mac_address', 'mtu', 'mgmt_only',
|
'device', 'name', 'lag', 'type', 'enabled', 'mac_address', 'mtu', 'mgmt_only',
|
||||||
'description', 'mode',
|
'description', 'mode',
|
||||||
]
|
]
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
# TODO: ordering and unique_together should include virtual_machine
|
|
||||||
ordering = ('device', CollateAsChar('_name'))
|
ordering = ('device', CollateAsChar('_name'))
|
||||||
unique_together = ('device', 'name')
|
unique_together = ('device', 'name')
|
||||||
|
|
||||||
@ -712,7 +708,6 @@ class Interface(CableTermination, ComponentModel):
|
|||||||
def to_csv(self):
|
def to_csv(self):
|
||||||
return (
|
return (
|
||||||
self.device.identifier if self.device else None,
|
self.device.identifier if self.device else None,
|
||||||
self.virtual_machine.name if self.virtual_machine else None,
|
|
||||||
self.name,
|
self.name,
|
||||||
self.lag.name if self.lag else None,
|
self.lag.name if self.lag else None,
|
||||||
self.get_type_display(),
|
self.get_type_display(),
|
||||||
@ -726,18 +721,6 @@ class Interface(CableTermination, ComponentModel):
|
|||||||
|
|
||||||
def clean(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
|
# Virtual interfaces cannot be connected
|
||||||
if self.type in NONCONNECTABLE_IFACE_TYPES and (
|
if self.type in NONCONNECTABLE_IFACE_TYPES and (
|
||||||
self.cable or getattr(self, 'circuit_termination', False)
|
self.cable or getattr(self, 'circuit_termination', False)
|
||||||
@ -773,7 +756,7 @@ class Interface(CableTermination, ComponentModel):
|
|||||||
if self.untagged_vlan and self.untagged_vlan.site not in [self.parent.site, None]:
|
if self.untagged_vlan and self.untagged_vlan.site not in [self.parent.site, None]:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'untagged_vlan': "The untagged VLAN ({}) must belong to the same site as the interface's parent "
|
'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):
|
def save(self, *args, **kwargs):
|
||||||
@ -788,21 +771,6 @@ class Interface(CableTermination, ComponentModel):
|
|||||||
|
|
||||||
return super().save(*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
|
@property
|
||||||
def connected_endpoint(self):
|
def connected_endpoint(self):
|
||||||
"""
|
"""
|
||||||
@ -841,7 +809,7 @@ class Interface(CableTermination, ComponentModel):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def parent(self):
|
def parent(self):
|
||||||
return self.device or self.virtual_machine
|
return self.device
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_connectable(self):
|
def is_connectable(self):
|
||||||
|
@ -863,6 +863,7 @@ class DeviceImportTable(BaseTable):
|
|||||||
|
|
||||||
class DeviceComponentDetailTable(BaseTable):
|
class DeviceComponentDetailTable(BaseTable):
|
||||||
pk = ToggleColumn()
|
pk = ToggleColumn()
|
||||||
|
device = tables.LinkColumn()
|
||||||
name = tables.Column(order_by=('_name',))
|
name = tables.Column(order_by=('_name',))
|
||||||
cable = tables.LinkColumn()
|
cable = tables.LinkColumn()
|
||||||
|
|
||||||
@ -881,7 +882,6 @@ class ConsolePortTable(BaseTable):
|
|||||||
|
|
||||||
|
|
||||||
class ConsolePortDetailTable(DeviceComponentDetailTable):
|
class ConsolePortDetailTable(DeviceComponentDetailTable):
|
||||||
device = tables.LinkColumn()
|
|
||||||
|
|
||||||
class Meta(DeviceComponentDetailTable.Meta, ConsolePortTable.Meta):
|
class Meta(DeviceComponentDetailTable.Meta, ConsolePortTable.Meta):
|
||||||
pass
|
pass
|
||||||
@ -896,7 +896,6 @@ class ConsoleServerPortTable(BaseTable):
|
|||||||
|
|
||||||
|
|
||||||
class ConsoleServerPortDetailTable(DeviceComponentDetailTable):
|
class ConsoleServerPortDetailTable(DeviceComponentDetailTable):
|
||||||
device = tables.LinkColumn()
|
|
||||||
|
|
||||||
class Meta(DeviceComponentDetailTable.Meta, ConsoleServerPortTable.Meta):
|
class Meta(DeviceComponentDetailTable.Meta, ConsoleServerPortTable.Meta):
|
||||||
pass
|
pass
|
||||||
@ -911,7 +910,6 @@ class PowerPortTable(BaseTable):
|
|||||||
|
|
||||||
|
|
||||||
class PowerPortDetailTable(DeviceComponentDetailTable):
|
class PowerPortDetailTable(DeviceComponentDetailTable):
|
||||||
device = tables.LinkColumn()
|
|
||||||
|
|
||||||
class Meta(DeviceComponentDetailTable.Meta, PowerPortTable.Meta):
|
class Meta(DeviceComponentDetailTable.Meta, PowerPortTable.Meta):
|
||||||
pass
|
pass
|
||||||
@ -926,7 +924,6 @@ class PowerOutletTable(BaseTable):
|
|||||||
|
|
||||||
|
|
||||||
class PowerOutletDetailTable(DeviceComponentDetailTable):
|
class PowerOutletDetailTable(DeviceComponentDetailTable):
|
||||||
device = tables.LinkColumn()
|
|
||||||
|
|
||||||
class Meta(DeviceComponentDetailTable.Meta, PowerOutletTable.Meta):
|
class Meta(DeviceComponentDetailTable.Meta, PowerOutletTable.Meta):
|
||||||
pass
|
pass
|
||||||
@ -940,14 +937,11 @@ class InterfaceTable(BaseTable):
|
|||||||
|
|
||||||
|
|
||||||
class InterfaceDetailTable(DeviceComponentDetailTable):
|
class InterfaceDetailTable(DeviceComponentDetailTable):
|
||||||
parent = tables.LinkColumn(order_by=('device', 'virtual_machine'))
|
|
||||||
name = tables.LinkColumn()
|
|
||||||
enabled = BooleanColumn()
|
enabled = BooleanColumn()
|
||||||
|
|
||||||
class Meta(InterfaceTable.Meta):
|
class Meta(DeviceComponentDetailTable.Meta, InterfaceTable.Meta):
|
||||||
order_by = ('parent', 'name')
|
fields = ('pk', 'device', 'name', 'label', 'enabled', 'type', 'description', 'cable')
|
||||||
fields = ('pk', 'parent', 'name', 'label', 'enabled', 'type', 'description', 'cable')
|
sequence = ('pk', 'device', 'name', 'label', 'enabled', 'type', 'description', 'cable')
|
||||||
sequence = ('pk', 'parent', 'name', 'label', 'enabled', 'type', 'description', 'cable')
|
|
||||||
|
|
||||||
|
|
||||||
class FrontPortTable(BaseTable):
|
class FrontPortTable(BaseTable):
|
||||||
@ -960,7 +954,6 @@ class FrontPortTable(BaseTable):
|
|||||||
|
|
||||||
|
|
||||||
class FrontPortDetailTable(DeviceComponentDetailTable):
|
class FrontPortDetailTable(DeviceComponentDetailTable):
|
||||||
device = tables.LinkColumn()
|
|
||||||
|
|
||||||
class Meta(DeviceComponentDetailTable.Meta, FrontPortTable.Meta):
|
class Meta(DeviceComponentDetailTable.Meta, FrontPortTable.Meta):
|
||||||
pass
|
pass
|
||||||
@ -976,7 +969,6 @@ class RearPortTable(BaseTable):
|
|||||||
|
|
||||||
|
|
||||||
class RearPortDetailTable(DeviceComponentDetailTable):
|
class RearPortDetailTable(DeviceComponentDetailTable):
|
||||||
device = tables.LinkColumn()
|
|
||||||
|
|
||||||
class Meta(DeviceComponentDetailTable.Meta, RearPortTable.Meta):
|
class Meta(DeviceComponentDetailTable.Meta, RearPortTable.Meta):
|
||||||
pass
|
pass
|
||||||
@ -991,7 +983,6 @@ class DeviceBayTable(BaseTable):
|
|||||||
|
|
||||||
|
|
||||||
class DeviceBayDetailTable(DeviceComponentDetailTable):
|
class DeviceBayDetailTable(DeviceComponentDetailTable):
|
||||||
device = tables.LinkColumn()
|
|
||||||
installed_device = tables.LinkColumn()
|
installed_device = tables.LinkColumn()
|
||||||
|
|
||||||
class Meta(DeviceBayTable.Meta):
|
class Meta(DeviceBayTable.Meta):
|
||||||
|
@ -1442,7 +1442,7 @@ class InterfaceView(ObjectView):
|
|||||||
|
|
||||||
# Get assigned IP addresses
|
# Get assigned IP addresses
|
||||||
ipaddress_table = InterfaceIPAddressTable(
|
ipaddress_table = InterfaceIPAddressTable(
|
||||||
data=interface.ip_addresses.restrict(request.user, 'view').prefetch_related('vrf', 'tenant'),
|
data=interface.ipaddresses.restrict(request.user, 'view').prefetch_related('vrf', 'tenant'),
|
||||||
orderable=False
|
orderable=False
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from django.db.models import Q
|
||||||
|
|
||||||
from .choices import IPAddressRoleChoices
|
from .choices import IPAddressRoleChoices
|
||||||
|
|
||||||
# BGP ASN bounds
|
# BGP ASN bounds
|
||||||
@ -29,6 +31,11 @@ PREFIX_LENGTH_MAX = 127 # IPv6
|
|||||||
# IPAddresses
|
# IPAddresses
|
||||||
#
|
#
|
||||||
|
|
||||||
|
IPADDRESS_ASSIGNMENT_MODELS = Q(
|
||||||
|
Q(app_label='dcim', model='interface') |
|
||||||
|
Q(app_label='virtualization', model='interface')
|
||||||
|
)
|
||||||
|
|
||||||
IPADDRESS_MASK_LENGTH_MIN = 1
|
IPADDRESS_MASK_LENGTH_MIN = 1
|
||||||
IPADDRESS_MASK_LENGTH_MAX = 128 # IPv6
|
IPADDRESS_MASK_LENGTH_MAX = 128 # IPv6
|
||||||
|
|
||||||
|
@ -299,37 +299,37 @@ class IPAddressFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldFilterSet,
|
|||||||
to_field_name='rd',
|
to_field_name='rd',
|
||||||
label='VRF (RD)',
|
label='VRF (RD)',
|
||||||
)
|
)
|
||||||
device = MultiValueCharFilter(
|
# device = MultiValueCharFilter(
|
||||||
method='filter_device',
|
# method='filter_device',
|
||||||
field_name='name',
|
# field_name='name',
|
||||||
label='Device (name)',
|
# label='Device (name)',
|
||||||
)
|
# )
|
||||||
device_id = MultiValueNumberFilter(
|
# device_id = MultiValueNumberFilter(
|
||||||
method='filter_device',
|
# method='filter_device',
|
||||||
field_name='pk',
|
# field_name='pk',
|
||||||
label='Device (ID)',
|
# label='Device (ID)',
|
||||||
)
|
# )
|
||||||
virtual_machine_id = django_filters.ModelMultipleChoiceFilter(
|
# virtual_machine_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
field_name='interface__virtual_machine',
|
# field_name='interface__virtual_machine',
|
||||||
queryset=VirtualMachine.objects.unrestricted(),
|
# queryset=VirtualMachine.objects.unrestricted(),
|
||||||
label='Virtual machine (ID)',
|
# label='Virtual machine (ID)',
|
||||||
)
|
# )
|
||||||
virtual_machine = django_filters.ModelMultipleChoiceFilter(
|
# virtual_machine = django_filters.ModelMultipleChoiceFilter(
|
||||||
field_name='interface__virtual_machine__name',
|
# field_name='interface__virtual_machine__name',
|
||||||
queryset=VirtualMachine.objects.unrestricted(),
|
# queryset=VirtualMachine.objects.unrestricted(),
|
||||||
to_field_name='name',
|
# to_field_name='name',
|
||||||
label='Virtual machine (name)',
|
# label='Virtual machine (name)',
|
||||||
)
|
# )
|
||||||
interface = django_filters.ModelMultipleChoiceFilter(
|
# interface = django_filters.ModelMultipleChoiceFilter(
|
||||||
field_name='interface__name',
|
# field_name='interface__name',
|
||||||
queryset=Interface.objects.unrestricted(),
|
# queryset=Interface.objects.unrestricted(),
|
||||||
to_field_name='name',
|
# to_field_name='name',
|
||||||
label='Interface (ID)',
|
# label='Interface (ID)',
|
||||||
)
|
# )
|
||||||
interface_id = django_filters.ModelMultipleChoiceFilter(
|
# interface_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=Interface.objects.unrestricted(),
|
# queryset=Interface.objects.unrestricted(),
|
||||||
label='Interface (ID)',
|
# label='Interface (ID)',
|
||||||
)
|
# )
|
||||||
assigned_to_interface = django_filters.BooleanFilter(
|
assigned_to_interface = django_filters.BooleanFilter(
|
||||||
method='_assigned_to_interface',
|
method='_assigned_to_interface',
|
||||||
label='Is assigned to an interface',
|
label='Is assigned to an interface',
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||||
|
|
||||||
from dcim.models import Device, Interface, Rack, Region, Site
|
from dcim.models import Device, Interface, Rack, Region, Site
|
||||||
@ -14,7 +15,7 @@ from utilities.forms import (
|
|||||||
ExpandableIPAddressField, ReturnURLForm, SlugField, StaticSelect2, StaticSelect2Multiple, TagFilterField,
|
ExpandableIPAddressField, ReturnURLForm, SlugField, StaticSelect2, StaticSelect2Multiple, TagFilterField,
|
||||||
BOOLEAN_WITH_BLANK_CHOICES,
|
BOOLEAN_WITH_BLANK_CHOICES,
|
||||||
)
|
)
|
||||||
from virtualization.models import VirtualMachine
|
from virtualization.models import Interface as VMInterface, VirtualMachine
|
||||||
from .choices import *
|
from .choices import *
|
||||||
from .constants import *
|
from .constants import *
|
||||||
from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
|
from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
|
||||||
@ -1194,13 +1195,14 @@ class ServiceForm(BootstrapMixin, CustomFieldModelForm):
|
|||||||
|
|
||||||
# Limit IP address choices to those assigned to interfaces of the parent device/VM
|
# Limit IP address choices to those assigned to interfaces of the parent device/VM
|
||||||
if self.instance.device:
|
if self.instance.device:
|
||||||
vc_interface_ids = [i['id'] for i in self.instance.device.vc_interfaces.values('id')]
|
|
||||||
self.fields['ipaddresses'].queryset = IPAddress.objects.filter(
|
self.fields['ipaddresses'].queryset = IPAddress.objects.filter(
|
||||||
interface_id__in=vc_interface_ids
|
assigned_object_type=ContentType.objects.get_for_model(Interface),
|
||||||
|
assigned_object_id__in=self.instance.device.vc_interfaces.values('id', flat=True)
|
||||||
)
|
)
|
||||||
elif self.instance.virtual_machine:
|
elif self.instance.virtual_machine:
|
||||||
self.fields['ipaddresses'].queryset = IPAddress.objects.filter(
|
self.fields['ipaddresses'].queryset = IPAddress.objects.filter(
|
||||||
interface__virtual_machine=self.instance.virtual_machine
|
assigned_object_type=ContentType.objects.get_for_model(VMInterface),
|
||||||
|
assigned_object_id__in=self.instance.virtual_machine.interfaces.values_list('id', flat=True)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self.fields['ipaddresses'].choices = []
|
self.fields['ipaddresses'].choices = []
|
||||||
|
35
netbox/ipam/migrations/0037_ipaddress_assignment.py
Normal file
35
netbox/ipam/migrations/0037_ipaddress_assignment.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
def set_assigned_object_type(apps, schema_editor):
|
||||||
|
ContentType = apps.get_model('contenttypes', 'ContentType')
|
||||||
|
IPAddress = apps.get_model('ipam', 'IPAddress')
|
||||||
|
|
||||||
|
device_ct = ContentType.objects.get(app_label='dcim', model='interface').pk
|
||||||
|
IPAddress.objects.update(assigned_object_type=device_ct)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('contenttypes', '0002_remove_content_type_name'),
|
||||||
|
('ipam', '0036_standardize_description'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='ipaddress',
|
||||||
|
old_name='interface',
|
||||||
|
new_name='assigned_object_id',
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='ipaddress',
|
||||||
|
name='assigned_object_type',
|
||||||
|
field=models.ForeignKey(limit_choices_to=models.Q(models.Q(models.Q(('app_label', 'dcim'), ('model', 'interface')), models.Q(('app_label', 'virtualization'), ('model', 'interface')), _connector='OR')), on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.ContentType', blank=True, null=True),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.RunPython(
|
||||||
|
code=set_assigned_object_type
|
||||||
|
),
|
||||||
|
]
|
@ -1,6 +1,7 @@
|
|||||||
import netaddr
|
import netaddr
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.contenttypes.fields import GenericRelation
|
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.core.exceptions import ValidationError, ObjectDoesNotExist
|
from django.core.exceptions import ValidationError, ObjectDoesNotExist
|
||||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
@ -606,13 +607,25 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel):
|
|||||||
blank=True,
|
blank=True,
|
||||||
help_text='The functional role of this IP'
|
help_text='The functional role of this IP'
|
||||||
)
|
)
|
||||||
interface = models.ForeignKey(
|
assigned_object_type = models.ForeignKey(
|
||||||
|
to=ContentType,
|
||||||
|
limit_choices_to=IPADDRESS_ASSIGNMENT_MODELS,
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
related_name='+',
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
assigned_object_id = models.ForeignKey(
|
||||||
to='dcim.Interface',
|
to='dcim.Interface',
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
related_name='ip_addresses',
|
related_name='ip_addresses',
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True
|
null=True
|
||||||
)
|
)
|
||||||
|
assigned_object = GenericForeignKey(
|
||||||
|
ct_field='assigned_object_type',
|
||||||
|
fk_field='assigned_object_id'
|
||||||
|
)
|
||||||
nat_inside = models.OneToOneField(
|
nat_inside = models.OneToOneField(
|
||||||
to='self',
|
to='self',
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
|
@ -431,18 +431,14 @@ class IPAddressTable(BaseTable):
|
|||||||
tenant = tables.TemplateColumn(
|
tenant = tables.TemplateColumn(
|
||||||
template_code=TENANT_LINK
|
template_code=TENANT_LINK
|
||||||
)
|
)
|
||||||
parent = tables.TemplateColumn(
|
assigned = tables.BooleanColumn(
|
||||||
template_code=IPADDRESS_PARENT,
|
accessor='assigned_object_id'
|
||||||
orderable=False
|
|
||||||
)
|
|
||||||
interface = tables.Column(
|
|
||||||
orderable=False
|
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = IPAddress
|
model = IPAddress
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'address', 'vrf', 'status', 'role', 'tenant', 'parent', 'interface', 'dns_name', 'description',
|
'pk', 'address', 'vrf', 'status', 'role', 'tenant', 'assigned', 'dns_name', 'description',
|
||||||
)
|
)
|
||||||
row_attrs = {
|
row_attrs = {
|
||||||
'class': lambda record: 'success' if not isinstance(record, IPAddress) else '',
|
'class': lambda record: 'success' if not isinstance(record, IPAddress) else '',
|
||||||
@ -465,11 +461,11 @@ class IPAddressDetailTable(IPAddressTable):
|
|||||||
|
|
||||||
class Meta(IPAddressTable.Meta):
|
class Meta(IPAddressTable.Meta):
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'address', 'vrf', 'status', 'role', 'tenant', 'nat_inside', 'parent', 'interface', 'dns_name',
|
'pk', 'address', 'vrf', 'status', 'role', 'tenant', 'nat_inside', 'assigned', 'dns_name',
|
||||||
'description', 'tags',
|
'description', 'tags',
|
||||||
)
|
)
|
||||||
default_columns = (
|
default_columns = (
|
||||||
'pk', 'address', 'vrf', 'status', 'role', 'tenant', 'parent', 'interface', 'dns_name', 'description',
|
'pk', 'address', 'vrf', 'status', 'role', 'tenant', 'assigned', 'dns_name', 'description',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ from dcim.models import Device, DeviceRole, DeviceType, Interface, Manufacturer,
|
|||||||
from ipam.choices import *
|
from ipam.choices import *
|
||||||
from ipam.filters import *
|
from ipam.filters import *
|
||||||
from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
|
from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
|
||||||
from virtualization.models import Cluster, ClusterType, VirtualMachine
|
from virtualization.models import Cluster, ClusterType, Interfaces as VMInterface, VirtualMachine
|
||||||
from tenancy.models import Tenant, TenantGroup
|
from tenancy.models import Tenant, TenantGroup
|
||||||
|
|
||||||
|
|
||||||
@ -375,6 +375,13 @@ class IPAddressTestCase(TestCase):
|
|||||||
)
|
)
|
||||||
Device.objects.bulk_create(devices)
|
Device.objects.bulk_create(devices)
|
||||||
|
|
||||||
|
interfaces = (
|
||||||
|
Interface(device=devices[0], name='Interface 1'),
|
||||||
|
Interface(device=devices[1], name='Interface 2'),
|
||||||
|
Interface(device=devices[2], name='Interface 3'),
|
||||||
|
)
|
||||||
|
Interface.objects.bulk_create(interfaces)
|
||||||
|
|
||||||
clustertype = ClusterType.objects.create(name='Cluster Type 1', slug='cluster-type-1')
|
clustertype = ClusterType.objects.create(name='Cluster Type 1', slug='cluster-type-1')
|
||||||
cluster = Cluster.objects.create(type=clustertype, name='Cluster 1')
|
cluster = Cluster.objects.create(type=clustertype, name='Cluster 1')
|
||||||
|
|
||||||
@ -385,15 +392,12 @@ class IPAddressTestCase(TestCase):
|
|||||||
)
|
)
|
||||||
VirtualMachine.objects.bulk_create(virtual_machines)
|
VirtualMachine.objects.bulk_create(virtual_machines)
|
||||||
|
|
||||||
interfaces = (
|
vm_interfaces = (
|
||||||
Interface(device=devices[0], name='Interface 1'),
|
VMInterface(virtual_machine=virtual_machines[0], name='Interface 1'),
|
||||||
Interface(device=devices[1], name='Interface 2'),
|
VMInterface(virtual_machine=virtual_machines[1], name='Interface 2'),
|
||||||
Interface(device=devices[2], name='Interface 3'),
|
VMInterface(virtual_machine=virtual_machines[2], name='Interface 3'),
|
||||||
Interface(virtual_machine=virtual_machines[0], name='Interface 1'),
|
|
||||||
Interface(virtual_machine=virtual_machines[1], name='Interface 2'),
|
|
||||||
Interface(virtual_machine=virtual_machines[2], name='Interface 3'),
|
|
||||||
)
|
)
|
||||||
Interface.objects.bulk_create(interfaces)
|
VMInterface.objects.bulk_create(vm_interfaces)
|
||||||
|
|
||||||
tenant_groups = (
|
tenant_groups = (
|
||||||
TenantGroup(name='Tenant group 1', slug='tenant-group-1'),
|
TenantGroup(name='Tenant group 1', slug='tenant-group-1'),
|
||||||
|
@ -256,6 +256,10 @@ class BaseFilterSet(django_filters.FilterSet):
|
|||||||
except django_filters.exceptions.FieldLookupError:
|
except django_filters.exceptions.FieldLookupError:
|
||||||
# The filter could not be created because the lookup expression is not supported on the field
|
# The filter could not be created because the lookup expression is not supported on the field
|
||||||
continue
|
continue
|
||||||
|
except Exception as e:
|
||||||
|
print(existing_filter_name, existing_filter)
|
||||||
|
print(f'field: {field}, lookup_expr: {lookup_expr}')
|
||||||
|
raise e
|
||||||
|
|
||||||
if lookup_name.startswith('n'):
|
if lookup_name.startswith('n'):
|
||||||
# This is a negation filter which requires a queryset.exclude() clause
|
# This is a negation filter which requires a queryset.exclude() clause
|
||||||
|
@ -3,7 +3,6 @@ from rest_framework import serializers
|
|||||||
|
|
||||||
from dcim.api.nested_serializers import NestedDeviceRoleSerializer, NestedPlatformSerializer, NestedSiteSerializer
|
from dcim.api.nested_serializers import NestedDeviceRoleSerializer, NestedPlatformSerializer, NestedSiteSerializer
|
||||||
from dcim.choices import InterfaceModeChoices
|
from dcim.choices import InterfaceModeChoices
|
||||||
from dcim.models import Interface
|
|
||||||
from extras.api.customfields import CustomFieldModelSerializer
|
from extras.api.customfields import CustomFieldModelSerializer
|
||||||
from extras.api.serializers import TaggedObjectSerializer
|
from extras.api.serializers import TaggedObjectSerializer
|
||||||
from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedVLANSerializer
|
from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedVLANSerializer
|
||||||
@ -11,7 +10,7 @@ from ipam.models import VLAN
|
|||||||
from tenancy.api.nested_serializers import NestedTenantSerializer
|
from tenancy.api.nested_serializers import NestedTenantSerializer
|
||||||
from utilities.api import ChoiceField, SerializedPKRelatedField, ValidatedModelSerializer
|
from utilities.api import ChoiceField, SerializedPKRelatedField, ValidatedModelSerializer
|
||||||
from virtualization.choices import *
|
from virtualization.choices import *
|
||||||
from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine
|
from virtualization.models import Cluster, ClusterGroup, ClusterType, Interface, VirtualMachine
|
||||||
from .nested_serializers import *
|
from .nested_serializers import *
|
||||||
|
|
||||||
|
|
||||||
@ -97,7 +96,6 @@ class VirtualMachineWithConfigContextSerializer(VirtualMachineSerializer):
|
|||||||
|
|
||||||
class InterfaceSerializer(TaggedObjectSerializer, ValidatedModelSerializer):
|
class InterfaceSerializer(TaggedObjectSerializer, ValidatedModelSerializer):
|
||||||
virtual_machine = NestedVirtualMachineSerializer()
|
virtual_machine = NestedVirtualMachineSerializer()
|
||||||
type = ChoiceField(choices=VMInterfaceTypeChoices, default=VMInterfaceTypeChoices.TYPE_VIRTUAL, required=False)
|
|
||||||
mode = ChoiceField(choices=InterfaceModeChoices, allow_blank=True, required=False)
|
mode = ChoiceField(choices=InterfaceModeChoices, allow_blank=True, required=False)
|
||||||
untagged_vlan = NestedVLANSerializer(required=False, allow_null=True)
|
untagged_vlan = NestedVLANSerializer(required=False, allow_null=True)
|
||||||
tagged_vlans = SerializedPKRelatedField(
|
tagged_vlans = SerializedPKRelatedField(
|
||||||
@ -110,6 +108,6 @@ class InterfaceSerializer(TaggedObjectSerializer, ValidatedModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Interface
|
model = Interface
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'virtual_machine', 'name', 'type', 'enabled', 'mtu', 'mac_address', 'description', 'mode',
|
'id', 'virtual_machine', 'name', 'enabled', 'mtu', 'mac_address', 'description', 'mode', 'untagged_vlan',
|
||||||
'untagged_vlan', 'tagged_vlans', 'tags',
|
'tagged_vlans', 'tags',
|
||||||
]
|
]
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
from django.db.models import Count
|
from django.db.models import Count
|
||||||
|
|
||||||
from dcim.models import Device, Interface
|
from dcim.models import Device
|
||||||
from extras.api.views import CustomFieldModelViewSet
|
from extras.api.views import CustomFieldModelViewSet
|
||||||
from utilities.api import ModelViewSet
|
from utilities.api import ModelViewSet
|
||||||
from utilities.utils import get_subquery
|
from utilities.utils import get_subquery
|
||||||
from virtualization import filters
|
from virtualization import filters
|
||||||
from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine
|
from virtualization.models import Cluster, ClusterGroup, ClusterType, Interface, VirtualMachine
|
||||||
from . import serializers
|
from . import serializers
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import django_filters
|
import django_filters
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
|
||||||
from dcim.models import DeviceRole, Interface, Platform, Region, Site
|
from dcim.models import DeviceRole, Platform, Region, Site
|
||||||
from extras.filters import CustomFieldFilterSet, CreatedUpdatedFilterSet, LocalConfigContextFilterSet
|
from extras.filters import CustomFieldFilterSet, CreatedUpdatedFilterSet, LocalConfigContextFilterSet
|
||||||
from tenancy.filters import TenancyFilterSet
|
from tenancy.filters import TenancyFilterSet
|
||||||
from utilities.filters import (
|
from utilities.filters import (
|
||||||
@ -9,7 +9,7 @@ from utilities.filters import (
|
|||||||
TreeNodeMultipleChoiceFilter,
|
TreeNodeMultipleChoiceFilter,
|
||||||
)
|
)
|
||||||
from .choices import *
|
from .choices import *
|
||||||
from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine
|
from .models import Cluster, ClusterGroup, ClusterType, Interface, VirtualMachine
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'ClusterFilterSet',
|
'ClusterFilterSet',
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
|
|
||||||
from dcim.choices import InterfaceModeChoices
|
from dcim.choices import InterfaceModeChoices
|
||||||
from dcim.constants import INTERFACE_MTU_MAX, INTERFACE_MTU_MIN
|
from dcim.constants import INTERFACE_MTU_MAX, INTERFACE_MTU_MIN
|
||||||
from dcim.forms import INTERFACE_MODE_HELP_TEXT
|
from dcim.forms import INTERFACE_MODE_HELP_TEXT
|
||||||
from dcim.models import Device, DeviceRole, Interface, Platform, Rack, Region, Site
|
from dcim.models import Device, DeviceRole, Platform, Rack, Region, Site
|
||||||
from extras.forms import (
|
from extras.forms import (
|
||||||
AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldModelCSVForm, CustomFieldModelForm, CustomFieldFilterForm,
|
AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldModelCSVForm, CustomFieldModelForm, CustomFieldFilterForm,
|
||||||
)
|
)
|
||||||
@ -16,10 +17,10 @@ from utilities.forms import (
|
|||||||
add_blank_choice, APISelect, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect,
|
add_blank_choice, APISelect, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect,
|
||||||
CommentField, ConfirmationForm, CSVChoiceField, CSVModelChoiceField, CSVModelForm, DynamicModelChoiceField,
|
CommentField, ConfirmationForm, CSVChoiceField, CSVModelChoiceField, CSVModelForm, DynamicModelChoiceField,
|
||||||
DynamicModelMultipleChoiceField, ExpandableNameField, form_from_model, JSONField, SlugField, SmallTextarea,
|
DynamicModelMultipleChoiceField, ExpandableNameField, form_from_model, JSONField, SlugField, SmallTextarea,
|
||||||
StaticSelect2, StaticSelect2Multiple, TagFilterField,
|
StaticSelect2, StaticSelect2Multiple, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES,
|
||||||
)
|
)
|
||||||
from .choices import *
|
from .choices import *
|
||||||
from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine
|
from .models import Cluster, ClusterGroup, ClusterType, Interface, VirtualMachine
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -355,8 +356,11 @@ class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
|||||||
for family in [4, 6]:
|
for family in [4, 6]:
|
||||||
ip_choices = [(None, '---------')]
|
ip_choices = [(None, '---------')]
|
||||||
# Collect interface IPs
|
# Collect interface IPs
|
||||||
|
interface_pks = self.instance.interfaces.values_list('id', flat=True)
|
||||||
interface_ips = IPAddress.objects.prefetch_related('interface').filter(
|
interface_ips = IPAddress.objects.prefetch_related('interface').filter(
|
||||||
address__family=family, interface__virtual_machine=self.instance
|
address__family=family,
|
||||||
|
assigned_object_type=ContentType.objects.get_for_model(Interface),
|
||||||
|
assigned_object_id__in=interface_pks
|
||||||
)
|
)
|
||||||
if interface_ips:
|
if interface_ips:
|
||||||
ip_choices.append(
|
ip_choices.append(
|
||||||
@ -600,12 +604,11 @@ class InterfaceForm(BootstrapMixin, forms.ModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Interface
|
model = Interface
|
||||||
fields = [
|
fields = [
|
||||||
'virtual_machine', 'name', 'type', 'enabled', 'mac_address', 'mtu', 'description', 'mode', 'tags',
|
'virtual_machine', 'name', 'enabled', 'mac_address', 'mtu', 'description', 'mode', 'tags', 'untagged_vlan',
|
||||||
'untagged_vlan', 'tagged_vlans',
|
'tagged_vlans',
|
||||||
]
|
]
|
||||||
widgets = {
|
widgets = {
|
||||||
'virtual_machine': forms.HiddenInput(),
|
'virtual_machine': forms.HiddenInput(),
|
||||||
'type': forms.HiddenInput(),
|
|
||||||
'mode': StaticSelect2()
|
'mode': StaticSelect2()
|
||||||
}
|
}
|
||||||
labels = {
|
labels = {
|
||||||
@ -619,7 +622,7 @@ class InterfaceForm(BootstrapMixin, forms.ModelForm):
|
|||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
# Add current site to VLANs query params
|
# Add current site to VLANs query params
|
||||||
site = getattr(self.instance.parent, 'site', None)
|
site = getattr(self.instance.virtual_machine, 'site', None)
|
||||||
if site is not None:
|
if site is not None:
|
||||||
# Add current site to VLANs query params
|
# Add current site to VLANs query params
|
||||||
self.fields['untagged_vlan'].widget.add_additional_query_param('site_id', site.pk)
|
self.fields['untagged_vlan'].widget.add_additional_query_param('site_id', site.pk)
|
||||||
@ -650,11 +653,6 @@ class InterfaceCreateForm(BootstrapMixin, forms.Form):
|
|||||||
name_pattern = ExpandableNameField(
|
name_pattern = ExpandableNameField(
|
||||||
label='Name'
|
label='Name'
|
||||||
)
|
)
|
||||||
type = forms.ChoiceField(
|
|
||||||
choices=VMInterfaceTypeChoices,
|
|
||||||
initial=VMInterfaceTypeChoices.TYPE_VIRTUAL,
|
|
||||||
widget=forms.HiddenInput()
|
|
||||||
)
|
|
||||||
enabled = forms.BooleanField(
|
enabled = forms.BooleanField(
|
||||||
required=False,
|
required=False,
|
||||||
initial=True
|
initial=True
|
||||||
@ -789,6 +787,17 @@ class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
|
|||||||
self.fields['tagged_vlans'].widget.add_additional_query_param('site_id', site.pk)
|
self.fields['tagged_vlans'].widget.add_additional_query_param('site_id', site.pk)
|
||||||
|
|
||||||
|
|
||||||
|
class InterfaceFilterForm(forms.Form):
|
||||||
|
model = Interface
|
||||||
|
enabled = forms.NullBooleanField(
|
||||||
|
required=False,
|
||||||
|
widget=StaticSelect2(
|
||||||
|
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||||
|
)
|
||||||
|
)
|
||||||
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Bulk VirtualMachine component creation
|
# Bulk VirtualMachine component creation
|
||||||
#
|
#
|
||||||
@ -812,8 +821,4 @@ class InterfaceBulkCreateForm(
|
|||||||
form_from_model(Interface, ['enabled', 'mtu', 'description', 'tags']),
|
form_from_model(Interface, ['enabled', 'mtu', 'description', 'tags']),
|
||||||
VirtualMachineBulkAddComponentForm
|
VirtualMachineBulkAddComponentForm
|
||||||
):
|
):
|
||||||
type = forms.ChoiceField(
|
pass
|
||||||
choices=VMInterfaceTypeChoices,
|
|
||||||
initial=VMInterfaceTypeChoices.TYPE_VIRTUAL,
|
|
||||||
widget=forms.HiddenInput()
|
|
||||||
)
|
|
||||||
|
43
netbox/virtualization/migrations/0015_interface.py
Normal file
43
netbox/virtualization/migrations/0015_interface.py
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
# Generated by Django 3.0.6 on 2020-06-18 20:21
|
||||||
|
|
||||||
|
import dcim.fields
|
||||||
|
import django.core.validators
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import taggit.managers
|
||||||
|
import utilities.fields
|
||||||
|
import utilities.ordering
|
||||||
|
import utilities.query_functions
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('ipam', '0036_standardize_description'),
|
||||||
|
('extras', '0042_customfield_manager'),
|
||||||
|
('virtualization', '0014_standardize_description'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Interface',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||||
|
('name', models.CharField(max_length=64)),
|
||||||
|
('_name', utilities.fields.NaturalOrderingField('name', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize_interface)),
|
||||||
|
('enabled', models.BooleanField(default=True)),
|
||||||
|
('mac_address', dcim.fields.MACAddressField(blank=True, null=True)),
|
||||||
|
('mtu', models.PositiveIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(65536)])),
|
||||||
|
('mode', models.CharField(blank=True, max_length=50)),
|
||||||
|
('description', models.CharField(blank=True, max_length=200)),
|
||||||
|
('tagged_vlans', models.ManyToManyField(blank=True, related_name='vm_interfaces_as_tagged', to='ipam.VLAN')),
|
||||||
|
('tags', taggit.managers.TaggableManager(related_name='vm_interface', through='extras.TaggedItem', to='extras.Tag')),
|
||||||
|
('untagged_vlan', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='vm_interfaces_as_untagged', to='ipam.VLAN')),
|
||||||
|
('virtual_machine', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='interfaces', to='virtualization.VirtualMachine')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'ordering': ('virtual_machine', utilities.query_functions.CollateAsChar('_name')),
|
||||||
|
'unique_together': {('virtual_machine', 'name')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,69 @@
|
|||||||
|
import sys
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
def replicate_interfaces(apps, schema_editor):
|
||||||
|
ContentType = apps.get_model('contenttypes', 'ContentType')
|
||||||
|
TaggedItem = apps.get_model('taggit', 'TaggedItem')
|
||||||
|
Interface = apps.get_model('dcim', 'Interface')
|
||||||
|
IPAddress = apps.get_model('ipam', 'IPAddress')
|
||||||
|
VMInterface = apps.get_model('virtualization', 'Interface')
|
||||||
|
|
||||||
|
interface_ct = ContentType.objects.get_for_model(Interface)
|
||||||
|
vm_interface_ct = ContentType.objects.get_for_model(VMInterface)
|
||||||
|
|
||||||
|
# Replicate dcim.Interface instances assigned to VirtualMachines
|
||||||
|
original_interfaces = Interface.objects.filter(virtual_machine__isnull=False)
|
||||||
|
for interface in original_interfaces:
|
||||||
|
vm_interface = VMInterface(
|
||||||
|
virtual_machine=interface.virtual_machine,
|
||||||
|
name=interface.name,
|
||||||
|
enabled=interface.enabled,
|
||||||
|
mac_address=interface.mac_address,
|
||||||
|
mtu=interface.mtu,
|
||||||
|
mode=interface.mode,
|
||||||
|
description=interface.description,
|
||||||
|
untagged_vlan=interface.untagged_vlan,
|
||||||
|
)
|
||||||
|
vm_interface.save()
|
||||||
|
|
||||||
|
# Copy tagged VLANs
|
||||||
|
vm_interface.tagged_vlans.set(interface.tagged_vlans.all())
|
||||||
|
|
||||||
|
# Reassign tags to the new instance
|
||||||
|
TaggedItem.objects.filter(
|
||||||
|
content_type=interface_ct, object_id=interface.pk
|
||||||
|
).update(
|
||||||
|
content_type=vm_interface_ct, object_id=vm_interface.pk
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update any assigned IPAddresses
|
||||||
|
IPAddress.objects.filter(assigned_object_id=interface.pk).update(
|
||||||
|
assigned_object_type=vm_interface_ct,
|
||||||
|
assigned_object_id=vm_interface.pk
|
||||||
|
)
|
||||||
|
|
||||||
|
replicated_count = VMInterface.objects.count()
|
||||||
|
if 'test' not in sys.argv:
|
||||||
|
print(f"\n Replicated {replicated_count} interfaces ", end='', flush=True)
|
||||||
|
|
||||||
|
# Verify that all interfaces have been replicated
|
||||||
|
assert replicated_count == original_interfaces.count(), "Replicated interfaces count does not match original count!"
|
||||||
|
|
||||||
|
# Delete original VM interfaces
|
||||||
|
original_interfaces.delete()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('ipam', '0037_ipaddress_assignment'),
|
||||||
|
('virtualization', '0015_interface'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(
|
||||||
|
code=replicate_interfaces
|
||||||
|
),
|
||||||
|
]
|
@ -5,11 +5,14 @@ from django.db import models
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from taggit.managers import TaggableManager
|
from taggit.managers import TaggableManager
|
||||||
|
|
||||||
from dcim.models import Device
|
from dcim.choices import InterfaceModeChoices
|
||||||
from extras.models import ConfigContextModel, CustomFieldModel, TaggedItem
|
from dcim.models import BaseInterface, Device
|
||||||
|
from extras.models import ConfigContextModel, CustomFieldModel, ObjectChange, TaggedItem
|
||||||
from extras.utils import extras_features
|
from extras.utils import extras_features
|
||||||
from utilities.models import ChangeLoggedModel
|
from utilities.models import ChangeLoggedModel
|
||||||
|
from utilities.query_functions import CollateAsChar
|
||||||
from utilities.querysets import RestrictedQuerySet
|
from utilities.querysets import RestrictedQuerySet
|
||||||
|
from utilities.utils import serialize_object
|
||||||
from .choices import *
|
from .choices import *
|
||||||
|
|
||||||
|
|
||||||
@ -17,6 +20,7 @@ __all__ = (
|
|||||||
'Cluster',
|
'Cluster',
|
||||||
'ClusterGroup',
|
'ClusterGroup',
|
||||||
'ClusterType',
|
'ClusterType',
|
||||||
|
'Interface',
|
||||||
'VirtualMachine',
|
'VirtualMachine',
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -370,3 +374,109 @@ class VirtualMachine(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
|
|||||||
@property
|
@property
|
||||||
def site(self):
|
def site(self):
|
||||||
return self.cluster.site
|
return self.cluster.site
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Interfaces
|
||||||
|
#
|
||||||
|
|
||||||
|
@extras_features('graphs', 'export_templates', 'webhooks')
|
||||||
|
class Interface(BaseInterface):
|
||||||
|
virtual_machine = models.ForeignKey(
|
||||||
|
to='virtualization.VirtualMachine',
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='interfaces'
|
||||||
|
)
|
||||||
|
description = models.CharField(
|
||||||
|
max_length=200,
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
|
untagged_vlan = models.ForeignKey(
|
||||||
|
to='ipam.VLAN',
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
related_name='vm_interfaces_as_untagged',
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name='Untagged VLAN'
|
||||||
|
)
|
||||||
|
tagged_vlans = models.ManyToManyField(
|
||||||
|
to='ipam.VLAN',
|
||||||
|
related_name='vm_interfaces_as_tagged',
|
||||||
|
blank=True,
|
||||||
|
verbose_name='Tagged VLANs'
|
||||||
|
)
|
||||||
|
ipaddresses = GenericRelation(
|
||||||
|
to='ipam.IPAddress',
|
||||||
|
content_type_field='assigned_object_type',
|
||||||
|
object_id_field='assigned_object_id'
|
||||||
|
)
|
||||||
|
tags = TaggableManager(
|
||||||
|
through=TaggedItem,
|
||||||
|
related_name='vm_interface'
|
||||||
|
)
|
||||||
|
|
||||||
|
objects = RestrictedQuerySet.as_manager()
|
||||||
|
|
||||||
|
csv_headers = [
|
||||||
|
'virtual_machine', 'name', 'enabled', 'mac_address', 'mtu', 'description', 'mode',
|
||||||
|
]
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ('virtual_machine', CollateAsChar('_name'))
|
||||||
|
unique_together = ('virtual_machine', 'name')
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse('virtualization:interface', kwargs={'pk': self.pk})
|
||||||
|
|
||||||
|
def to_csv(self):
|
||||||
|
return (
|
||||||
|
self.virtual_machine.name,
|
||||||
|
self.name,
|
||||||
|
self.enabled,
|
||||||
|
self.mac_address,
|
||||||
|
self.mtu,
|
||||||
|
self.description,
|
||||||
|
self.get_mode_display(),
|
||||||
|
)
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
|
||||||
|
# Validate untagged VLAN
|
||||||
|
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 "
|
||||||
|
"virtual machine, or it must be global".format(self.untagged_vlan)
|
||||||
|
})
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
|
||||||
|
# Remove untagged VLAN assignment for non-802.1Q interfaces
|
||||||
|
if self.mode is None:
|
||||||
|
self.untagged_vlan = None
|
||||||
|
|
||||||
|
# Only "tagged" interfaces may have tagged VLANs assigned. ("tagged all" implies all VLANs are assigned.)
|
||||||
|
if self.pk and self.mode != InterfaceModeChoices.MODE_TAGGED:
|
||||||
|
self.tagged_vlans.clear()
|
||||||
|
|
||||||
|
return super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
def to_objectchange(self, action):
|
||||||
|
# Annotate the parent VirtualMachine
|
||||||
|
return ObjectChange(
|
||||||
|
changed_object=self,
|
||||||
|
object_repr=str(self),
|
||||||
|
action=action,
|
||||||
|
related_object=self.virtual_machine,
|
||||||
|
object_data=serialize_object(self)
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def parent(self):
|
||||||
|
return self.virtual_machine
|
||||||
|
|
||||||
|
@property
|
||||||
|
def count_ipaddresses(self):
|
||||||
|
return self.ip_addresses.count()
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
import django_tables2 as tables
|
import django_tables2 as tables
|
||||||
from django_tables2.utils import Accessor
|
from django_tables2.utils import Accessor
|
||||||
|
|
||||||
from dcim.models import Interface
|
|
||||||
from tenancy.tables import COL_TENANT
|
from tenancy.tables import COL_TENANT
|
||||||
from utilities.tables import BaseTable, ColoredLabelColumn, TagColumn, ToggleColumn
|
from utilities.tables import BaseTable, ColoredLabelColumn, TagColumn, ToggleColumn
|
||||||
from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine
|
from .models import Cluster, ClusterGroup, ClusterType, Interface, VirtualMachine
|
||||||
|
|
||||||
CLUSTERTYPE_ACTIONS = """
|
CLUSTERTYPE_ACTIONS = """
|
||||||
<a href="{% url 'virtualization:clustertype_changelog' slug=record.slug %}" class="btn btn-default btn-xs" title="Change log">
|
<a href="{% url 'virtualization:clustertype_changelog' slug=record.slug %}" class="btn btn-default btn-xs" title="Change log">
|
||||||
|
@ -2,11 +2,9 @@ from django.urls import reverse
|
|||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
|
||||||
from dcim.choices import InterfaceModeChoices
|
from dcim.choices import InterfaceModeChoices
|
||||||
from dcim.models import Interface
|
|
||||||
from ipam.models import VLAN
|
from ipam.models import VLAN
|
||||||
from utilities.testing import APITestCase, APIViewTestCases
|
from utilities.testing import APITestCase, APIViewTestCases
|
||||||
from virtualization.choices import *
|
from virtualization.models import Cluster, ClusterGroup, ClusterType, Interface, VirtualMachine
|
||||||
from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine
|
|
||||||
|
|
||||||
|
|
||||||
class AppTest(APITestCase):
|
class AppTest(APITestCase):
|
||||||
@ -207,18 +205,15 @@ class InterfaceTest(APITestCase):
|
|||||||
self.virtualmachine = VirtualMachine.objects.create(cluster=cluster, name='Test VM 1')
|
self.virtualmachine = VirtualMachine.objects.create(cluster=cluster, name='Test VM 1')
|
||||||
self.interface1 = Interface.objects.create(
|
self.interface1 = Interface.objects.create(
|
||||||
virtual_machine=self.virtualmachine,
|
virtual_machine=self.virtualmachine,
|
||||||
name='Test Interface 1',
|
name='Test Interface 1'
|
||||||
type=InterfaceTypeChoices.TYPE_VIRTUAL
|
|
||||||
)
|
)
|
||||||
self.interface2 = Interface.objects.create(
|
self.interface2 = Interface.objects.create(
|
||||||
virtual_machine=self.virtualmachine,
|
virtual_machine=self.virtualmachine,
|
||||||
name='Test Interface 2',
|
name='Test Interface 2'
|
||||||
type=InterfaceTypeChoices.TYPE_VIRTUAL
|
|
||||||
)
|
)
|
||||||
self.interface3 = Interface.objects.create(
|
self.interface3 = Interface.objects.create(
|
||||||
virtual_machine=self.virtualmachine,
|
virtual_machine=self.virtualmachine,
|
||||||
name='Test Interface 3',
|
name='Test Interface 3'
|
||||||
type=InterfaceTypeChoices.TYPE_VIRTUAL
|
|
||||||
)
|
)
|
||||||
|
|
||||||
self.vlan1 = VLAN.objects.create(name="Test VLAN 1", vid=1)
|
self.vlan1 = VLAN.objects.create(name="Test VLAN 1", vid=1)
|
||||||
@ -227,21 +222,21 @@ class InterfaceTest(APITestCase):
|
|||||||
|
|
||||||
def test_get_interface(self):
|
def test_get_interface(self):
|
||||||
url = reverse('virtualization-api:interface-detail', kwargs={'pk': self.interface1.pk})
|
url = reverse('virtualization-api:interface-detail', kwargs={'pk': self.interface1.pk})
|
||||||
self.add_permissions('dcim.view_interface')
|
self.add_permissions('virtualization.view_interface')
|
||||||
|
|
||||||
response = self.client.get(url, **self.header)
|
response = self.client.get(url, **self.header)
|
||||||
self.assertEqual(response.data['name'], self.interface1.name)
|
self.assertEqual(response.data['name'], self.interface1.name)
|
||||||
|
|
||||||
def test_list_interfaces(self):
|
def test_list_interfaces(self):
|
||||||
url = reverse('virtualization-api:interface-list')
|
url = reverse('virtualization-api:interface-list')
|
||||||
self.add_permissions('dcim.view_interface')
|
self.add_permissions('virtualization.view_interface')
|
||||||
|
|
||||||
response = self.client.get(url, **self.header)
|
response = self.client.get(url, **self.header)
|
||||||
self.assertEqual(response.data['count'], 3)
|
self.assertEqual(response.data['count'], 3)
|
||||||
|
|
||||||
def test_list_interfaces_brief(self):
|
def test_list_interfaces_brief(self):
|
||||||
url = reverse('virtualization-api:interface-list')
|
url = reverse('virtualization-api:interface-list')
|
||||||
self.add_permissions('dcim.view_interface')
|
self.add_permissions('virtualization.view_interface')
|
||||||
|
|
||||||
response = self.client.get('{}?brief=1'.format(url), **self.header)
|
response = self.client.get('{}?brief=1'.format(url), **self.header)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
@ -255,7 +250,7 @@ class InterfaceTest(APITestCase):
|
|||||||
'name': 'Test Interface 4',
|
'name': 'Test Interface 4',
|
||||||
}
|
}
|
||||||
url = reverse('virtualization-api:interface-list')
|
url = reverse('virtualization-api:interface-list')
|
||||||
self.add_permissions('dcim.add_interface')
|
self.add_permissions('virtualization.add_interface')
|
||||||
|
|
||||||
response = self.client.post(url, data, format='json', **self.header)
|
response = self.client.post(url, data, format='json', **self.header)
|
||||||
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||||
@ -273,7 +268,7 @@ class InterfaceTest(APITestCase):
|
|||||||
'tagged_vlans': [self.vlan1.id, self.vlan2.id],
|
'tagged_vlans': [self.vlan1.id, self.vlan2.id],
|
||||||
}
|
}
|
||||||
url = reverse('virtualization-api:interface-list')
|
url = reverse('virtualization-api:interface-list')
|
||||||
self.add_permissions('dcim.add_interface')
|
self.add_permissions('virtualization.add_interface')
|
||||||
|
|
||||||
response = self.client.post(url, data, format='json', **self.header)
|
response = self.client.post(url, data, format='json', **self.header)
|
||||||
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||||
@ -299,7 +294,7 @@ class InterfaceTest(APITestCase):
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
url = reverse('virtualization-api:interface-list')
|
url = reverse('virtualization-api:interface-list')
|
||||||
self.add_permissions('dcim.add_interface')
|
self.add_permissions('virtualization.add_interface')
|
||||||
|
|
||||||
response = self.client.post(url, data, format='json', **self.header)
|
response = self.client.post(url, data, format='json', **self.header)
|
||||||
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||||
@ -333,7 +328,7 @@ class InterfaceTest(APITestCase):
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
url = reverse('virtualization-api:interface-list')
|
url = reverse('virtualization-api:interface-list')
|
||||||
self.add_permissions('dcim.add_interface')
|
self.add_permissions('virtualization.add_interface')
|
||||||
|
|
||||||
response = self.client.post(url, data, format='json', **self.header)
|
response = self.client.post(url, data, format='json', **self.header)
|
||||||
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||||
@ -349,7 +344,7 @@ class InterfaceTest(APITestCase):
|
|||||||
'name': 'Test Interface X',
|
'name': 'Test Interface X',
|
||||||
}
|
}
|
||||||
url = reverse('virtualization-api:interface-detail', kwargs={'pk': self.interface1.pk})
|
url = reverse('virtualization-api:interface-detail', kwargs={'pk': self.interface1.pk})
|
||||||
self.add_permissions('dcim.change_interface')
|
self.add_permissions('virtualization.change_interface')
|
||||||
|
|
||||||
response = self.client.put(url, data, format='json', **self.header)
|
response = self.client.put(url, data, format='json', **self.header)
|
||||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||||
@ -359,7 +354,7 @@ class InterfaceTest(APITestCase):
|
|||||||
|
|
||||||
def test_delete_interface(self):
|
def test_delete_interface(self):
|
||||||
url = reverse('virtualization-api:interface-detail', kwargs={'pk': self.interface1.pk})
|
url = reverse('virtualization-api:interface-detail', kwargs={'pk': self.interface1.pk})
|
||||||
self.add_permissions('dcim.delete_interface')
|
self.add_permissions('virtualization.delete_interface')
|
||||||
|
|
||||||
response = self.client.delete(url, **self.header)
|
response = self.client.delete(url, **self.header)
|
||||||
self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
|
self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from dcim.models import DeviceRole, Interface, Platform, Region, Site
|
from dcim.models import DeviceRole, Platform, Region, Site
|
||||||
from tenancy.models import Tenant, TenantGroup
|
from tenancy.models import Tenant, TenantGroup
|
||||||
from virtualization.choices import *
|
from virtualization.choices import *
|
||||||
from virtualization.filters import *
|
from virtualization.filters import *
|
||||||
from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine
|
from virtualization.models import Cluster, ClusterGroup, ClusterType, Interface, VirtualMachine
|
||||||
|
|
||||||
|
|
||||||
class ClusterTypeTestCase(TestCase):
|
class ClusterTypeTestCase(TestCase):
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
from netaddr import EUI
|
from netaddr import EUI
|
||||||
|
|
||||||
from dcim.choices import InterfaceModeChoices
|
from dcim.choices import InterfaceModeChoices
|
||||||
from dcim.models import DeviceRole, Interface, Platform, Site
|
from dcim.models import DeviceRole, Platform, Site
|
||||||
from ipam.models import VLAN
|
from ipam.models import VLAN
|
||||||
from utilities.testing import ViewTestCases
|
from utilities.testing import ViewTestCases
|
||||||
from virtualization.choices import *
|
from virtualization.choices import *
|
||||||
from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine
|
from virtualization.models import Cluster, ClusterGroup, ClusterType, Interface, VirtualMachine
|
||||||
|
|
||||||
|
|
||||||
class ClusterGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
class ClusterGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
||||||
@ -201,10 +201,6 @@ class InterfaceTestCase(
|
|||||||
):
|
):
|
||||||
model = Interface
|
model = Interface
|
||||||
|
|
||||||
def _get_base_url(self):
|
|
||||||
# Interface belongs to the DCIM app, so we have to override the base URL
|
|
||||||
return 'virtualization:interface_{}'
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
|
|
||||||
@ -219,9 +215,9 @@ class InterfaceTestCase(
|
|||||||
VirtualMachine.objects.bulk_create(virtualmachines)
|
VirtualMachine.objects.bulk_create(virtualmachines)
|
||||||
|
|
||||||
Interface.objects.bulk_create([
|
Interface.objects.bulk_create([
|
||||||
Interface(virtual_machine=virtualmachines[0], name='Interface 1', type=InterfaceTypeChoices.TYPE_VIRTUAL),
|
Interface(virtual_machine=virtualmachines[0], name='Interface 1'),
|
||||||
Interface(virtual_machine=virtualmachines[0], name='Interface 2', type=InterfaceTypeChoices.TYPE_VIRTUAL),
|
Interface(virtual_machine=virtualmachines[0], name='Interface 2'),
|
||||||
Interface(virtual_machine=virtualmachines[0], name='Interface 3', type=InterfaceTypeChoices.TYPE_VIRTUAL),
|
Interface(virtual_machine=virtualmachines[0], name='Interface 3'),
|
||||||
])
|
])
|
||||||
|
|
||||||
vlans = (
|
vlans = (
|
||||||
@ -237,7 +233,6 @@ class InterfaceTestCase(
|
|||||||
cls.form_data = {
|
cls.form_data = {
|
||||||
'virtual_machine': virtualmachines[1].pk,
|
'virtual_machine': virtualmachines[1].pk,
|
||||||
'name': 'Interface X',
|
'name': 'Interface X',
|
||||||
'type': InterfaceTypeChoices.TYPE_VIRTUAL,
|
|
||||||
'enabled': False,
|
'enabled': False,
|
||||||
'mgmt_only': False,
|
'mgmt_only': False,
|
||||||
'mac_address': EUI('01-02-03-04-05-06'),
|
'mac_address': EUI('01-02-03-04-05-06'),
|
||||||
@ -252,7 +247,6 @@ class InterfaceTestCase(
|
|||||||
cls.bulk_create_data = {
|
cls.bulk_create_data = {
|
||||||
'virtual_machine': virtualmachines[1].pk,
|
'virtual_machine': virtualmachines[1].pk,
|
||||||
'name_pattern': 'Interface [4-6]',
|
'name_pattern': 'Interface [4-6]',
|
||||||
'type': InterfaceTypeChoices.TYPE_VIRTUAL,
|
|
||||||
'enabled': False,
|
'enabled': False,
|
||||||
'mgmt_only': False,
|
'mgmt_only': False,
|
||||||
'mac_address': EUI('01-02-03-04-05-06'),
|
'mac_address': EUI('01-02-03-04-05-06'),
|
||||||
|
@ -54,6 +54,7 @@ urlpatterns = [
|
|||||||
path('interfaces/add/', views.InterfaceCreateView.as_view(), name='interface_add'),
|
path('interfaces/add/', views.InterfaceCreateView.as_view(), name='interface_add'),
|
||||||
path('interfaces/edit/', views.InterfaceBulkEditView.as_view(), name='interface_bulk_edit'),
|
path('interfaces/edit/', views.InterfaceBulkEditView.as_view(), name='interface_bulk_edit'),
|
||||||
path('interfaces/delete/', views.InterfaceBulkDeleteView.as_view(), name='interface_bulk_delete'),
|
path('interfaces/delete/', views.InterfaceBulkDeleteView.as_view(), name='interface_bulk_delete'),
|
||||||
|
path('interfaces/<int:pk>/', views.InterfaceView.as_view(), name='interface'),
|
||||||
path('interfaces/<int:pk>/edit/', views.InterfaceEditView.as_view(), name='interface_edit'),
|
path('interfaces/<int:pk>/edit/', views.InterfaceEditView.as_view(), name='interface_edit'),
|
||||||
path('interfaces/<int:pk>/delete/', views.InterfaceDeleteView.as_view(), name='interface_delete'),
|
path('interfaces/<int:pk>/delete/', views.InterfaceDeleteView.as_view(), name='interface_delete'),
|
||||||
path('virtual-machines/interfaces/add/', views.VirtualMachineBulkAddInterfaceView.as_view(), name='virtualmachine_bulk_add_interface'),
|
path('virtual-machines/interfaces/add/', views.VirtualMachineBulkAddInterfaceView.as_view(), name='virtualmachine_bulk_add_interface'),
|
||||||
|
@ -4,7 +4,8 @@ from django.db.models import Count
|
|||||||
from django.shortcuts import get_object_or_404, redirect, render
|
from django.shortcuts import get_object_or_404, redirect, render
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from dcim.models import Device, Interface
|
from dcim.models import Device
|
||||||
|
from dcim.views import InterfaceView as DeviceInterfaceView
|
||||||
from dcim.tables import DeviceTable
|
from dcim.tables import DeviceTable
|
||||||
from extras.views import ObjectConfigContextView
|
from extras.views import ObjectConfigContextView
|
||||||
from ipam.models import Service
|
from ipam.models import Service
|
||||||
@ -13,7 +14,7 @@ from utilities.views import (
|
|||||||
ObjectDeleteView, ObjectEditView, ObjectListView,
|
ObjectDeleteView, ObjectEditView, ObjectListView,
|
||||||
)
|
)
|
||||||
from . import filters, forms, tables
|
from . import filters, forms, tables
|
||||||
from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine
|
from .models import Cluster, ClusterGroup, ClusterType, Interface, VirtualMachine
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -288,6 +289,18 @@ class VirtualMachineBulkDeleteView(BulkDeleteView):
|
|||||||
# VM interfaces
|
# VM interfaces
|
||||||
#
|
#
|
||||||
|
|
||||||
|
class InterfaceListView(ObjectListView):
|
||||||
|
queryset = Interface.objects.prefetch_related('virtual_machine', 'virtual_machine__tenant', 'cable')
|
||||||
|
filterset = filters.InterfaceFilterSet
|
||||||
|
filterset_form = forms.InterfaceFilterForm
|
||||||
|
table = tables.InterfaceTable
|
||||||
|
action_buttons = ('import', 'export')
|
||||||
|
|
||||||
|
|
||||||
|
class InterfaceView(DeviceInterfaceView):
|
||||||
|
queryset = Interface.objects.all()
|
||||||
|
|
||||||
|
|
||||||
class InterfaceCreateView(ComponentCreateView):
|
class InterfaceCreateView(ComponentCreateView):
|
||||||
queryset = Interface.objects.all()
|
queryset = Interface.objects.all()
|
||||||
form = forms.InterfaceCreateForm
|
form = forms.InterfaceCreateForm
|
||||||
|
Reference in New Issue
Block a user