mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-24 17:38:37 -06:00
Merge pull request #4781 from netbox-community/4721-virtualmachine-interface
#4721: Move VM interfaces to a separate model (WIP)
This commit is contained in:
commit
d60a2d3723
@ -23,12 +23,12 @@ from tenancy.forms import TenancyFilterForm, TenancyForm
|
|||||||
from tenancy.models import Tenant, TenantGroup
|
from tenancy.models import Tenant, TenantGroup
|
||||||
from utilities.forms import (
|
from utilities.forms import (
|
||||||
APISelect, APISelectMultiple, add_blank_choice, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect,
|
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,
|
DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableNameField, form_from_model, JSONField,
|
||||||
NumericArrayField, SelectWithPK, SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple, TagFilterField,
|
NumericArrayField, SelectWithPK, SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple, TagFilterField,
|
||||||
BOOLEAN_WITH_BLANK_CHOICES,
|
BOOLEAN_WITH_BLANK_CHOICES,
|
||||||
)
|
)
|
||||||
from virtualization.models import Cluster, ClusterGroup, VirtualMachine
|
from virtualization.models import Cluster, ClusterGroup
|
||||||
from .choices import *
|
from .choices import *
|
||||||
from .constants import *
|
from .constants import *
|
||||||
from .models import (
|
from .models import (
|
||||||
@ -150,30 +150,6 @@ class LabeledComponentForm(BootstrapMixin, forms.Form):
|
|||||||
}, code='label_pattern_mismatch')
|
}, 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
|
# Fields
|
||||||
#
|
#
|
||||||
@ -1816,18 +1792,20 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
|||||||
ip_choices = [(None, '---------')]
|
ip_choices = [(None, '---------')]
|
||||||
|
|
||||||
# Gather PKs of all interfaces belonging to this Device or a peer VirtualChassis member
|
# 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
|
# Collect interface IPs
|
||||||
interface_ips = IPAddress.objects.prefetch_related('interface').filter(
|
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:
|
if interface_ips:
|
||||||
ip_list = [(ip.id, '{} ({})'.format(ip.address, ip.interface)) for ip in interface_ips]
|
ip_list = [(ip.id, '{} ({})'.format(ip.address, ip.interface)) for ip in interface_ips]
|
||||||
ip_choices.append(('Interface IPs', ip_list))
|
ip_choices.append(('Interface IPs', ip_list))
|
||||||
# Collect NAT IPs
|
# Collect NAT IPs
|
||||||
nat_ips = IPAddress.objects.prefetch_related('nat_inside').filter(
|
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:
|
if nat_ips:
|
||||||
ip_list = [(ip.id, '{} ({})'.format(ip.address, ip.nat_inside.address)) for ip in nat_ips]
|
ip_list = [(ip.id, '{} ({})'.format(ip.address, ip.nat_inside.address)) for ip in nat_ips]
|
||||||
@ -2961,12 +2939,6 @@ class InterfaceBulkDisconnectForm(ConfirmationForm):
|
|||||||
class InterfaceCSVForm(CSVModelForm):
|
class InterfaceCSVForm(CSVModelForm):
|
||||||
device = CSVModelChoiceField(
|
device = CSVModelChoiceField(
|
||||||
queryset=Device.objects.all(),
|
queryset=Device.objects.all(),
|
||||||
required=False,
|
|
||||||
to_field_name='name'
|
|
||||||
)
|
|
||||||
virtual_machine = CSVModelChoiceField(
|
|
||||||
queryset=VirtualMachine.objects.all(),
|
|
||||||
required=False,
|
|
||||||
to_field_name='name'
|
to_field_name='name'
|
||||||
)
|
)
|
||||||
lag = CSVModelChoiceField(
|
lag = CSVModelChoiceField(
|
||||||
|
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,42 @@ 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 +666,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 +685,19 @@ class Interface(CableTermination, ComponentModel):
|
|||||||
blank=True,
|
blank=True,
|
||||||
verbose_name='Tagged VLANs'
|
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)
|
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 +707,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 +720,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 +755,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 +770,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 +808,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):
|
||||||
|
@ -598,17 +598,11 @@ class InterfaceImportTable(BaseTable):
|
|||||||
viewname='dcim:device',
|
viewname='dcim:device',
|
||||||
args=[Accessor('device.pk')]
|
args=[Accessor('device.pk')]
|
||||||
)
|
)
|
||||||
virtual_machine = tables.LinkColumn(
|
|
||||||
viewname='virtualization:virtualmachine',
|
|
||||||
args=[Accessor('virtual_machine.pk')],
|
|
||||||
verbose_name='Virtual Machine'
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = Interface
|
model = Interface
|
||||||
fields = (
|
fields = (
|
||||||
'device', 'virtual_machine', 'name', 'description', 'lag', 'type', 'enabled', 'mac_address', 'mtu',
|
'device', 'name', 'description', 'lag', 'type', 'enabled', 'mac_address', 'mtu', 'mgmt_only', 'mode',
|
||||||
'mgmt_only', 'mode',
|
|
||||||
)
|
)
|
||||||
empty_text = False
|
empty_text = False
|
||||||
|
|
||||||
@ -863,6 +857,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 +876,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 +890,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 +904,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 +918,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 +931,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 +948,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 +963,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 +977,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):
|
||||||
|
@ -1254,8 +1254,8 @@ class DeviceTestCase(TestCase):
|
|||||||
|
|
||||||
# Assign primary IPs for filtering
|
# Assign primary IPs for filtering
|
||||||
ipaddresses = (
|
ipaddresses = (
|
||||||
IPAddress(address='192.0.2.1/24', interface=interfaces[0]),
|
IPAddress(address='192.0.2.1/24', assigned_object=interfaces[0]),
|
||||||
IPAddress(address='192.0.2.2/24', interface=interfaces[1]),
|
IPAddress(address='192.0.2.2/24', assigned_object=interfaces[1]),
|
||||||
)
|
)
|
||||||
IPAddress.objects.bulk_create(ipaddresses)
|
IPAddress.objects.bulk_create(ipaddresses)
|
||||||
Device.objects.filter(pk=devices[0].pk).update(primary_ip4=ipaddresses[0])
|
Device.objects.filter(pk=devices[0].pk).update(primary_ip4=ipaddresses[0])
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
import re
|
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
@ -25,8 +24,9 @@ from utilities.paginator import EnhancedPaginator
|
|||||||
from utilities.permissions import get_permission_for_model
|
from utilities.permissions import get_permission_for_model
|
||||||
from utilities.utils import csv_format
|
from utilities.utils import csv_format
|
||||||
from utilities.views import (
|
from utilities.views import (
|
||||||
BulkComponentCreateView, BulkDeleteView, BulkEditView, BulkImportView, ComponentCreateView, GetReturnURLMixin,
|
BulkComponentCreateView, BulkDeleteView, BulkEditView, BulkImportView, BulkRenameView, ComponentCreateView,
|
||||||
ObjectView, ObjectImportView, ObjectDeleteView, ObjectEditView, ObjectListView, ObjectPermissionRequiredMixin,
|
GetReturnURLMixin, ObjectView, ObjectImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
|
||||||
|
ObjectPermissionRequiredMixin,
|
||||||
)
|
)
|
||||||
from virtualization.models import VirtualMachine
|
from virtualization.models import VirtualMachine
|
||||||
from . import filters, forms, tables
|
from . import filters, forms, tables
|
||||||
@ -41,58 +41,6 @@ from .models import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class BulkRenameView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
|
||||||
"""
|
|
||||||
An extendable view for renaming device components in bulk.
|
|
||||||
"""
|
|
||||||
queryset = None
|
|
||||||
form = None
|
|
||||||
template_name = 'dcim/bulk_rename.html'
|
|
||||||
|
|
||||||
def get_required_permission(self):
|
|
||||||
return get_permission_for_model(self.queryset.model, 'change')
|
|
||||||
|
|
||||||
def post(self, request):
|
|
||||||
|
|
||||||
if '_preview' in request.POST or '_apply' in request.POST:
|
|
||||||
form = self.form(request.POST, initial={'pk': request.POST.getlist('pk')})
|
|
||||||
selected_objects = self.queryset.filter(pk__in=form.initial['pk'])
|
|
||||||
|
|
||||||
if form.is_valid():
|
|
||||||
for obj in selected_objects:
|
|
||||||
find = form.cleaned_data['find']
|
|
||||||
replace = form.cleaned_data['replace']
|
|
||||||
if form.cleaned_data['use_regex']:
|
|
||||||
try:
|
|
||||||
obj.new_name = re.sub(find, replace, obj.name)
|
|
||||||
# Catch regex group reference errors
|
|
||||||
except re.error:
|
|
||||||
obj.new_name = obj.name
|
|
||||||
else:
|
|
||||||
obj.new_name = obj.name.replace(find, replace)
|
|
||||||
|
|
||||||
if '_apply' in request.POST:
|
|
||||||
for obj in selected_objects:
|
|
||||||
obj.name = obj.new_name
|
|
||||||
obj.save()
|
|
||||||
messages.success(request, "Renamed {} {}".format(
|
|
||||||
len(selected_objects),
|
|
||||||
self.queryset.model._meta.verbose_name_plural
|
|
||||||
))
|
|
||||||
return redirect(self.get_return_url(request))
|
|
||||||
|
|
||||||
else:
|
|
||||||
form = self.form(initial={'pk': request.POST.getlist('pk')})
|
|
||||||
selected_objects = self.queryset.filter(pk__in=form.initial['pk'])
|
|
||||||
|
|
||||||
return render(request, self.template_name, {
|
|
||||||
'form': form,
|
|
||||||
'obj_type_plural': self.queryset.model._meta.verbose_name_plural,
|
|
||||||
'selected_objects': selected_objects,
|
|
||||||
'return_url': self.get_return_url(request),
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
class BulkDisconnectView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
class BulkDisconnectView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
||||||
"""
|
"""
|
||||||
An extendable view for disconnection console/power/interface components in bulk.
|
An extendable view for disconnection console/power/interface components in bulk.
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from drf_yasg.utils import swagger_serializer_method
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.reverse import reverse
|
from rest_framework.reverse import reverse
|
||||||
from rest_framework.validators import UniqueTogetherValidator
|
from rest_framework.validators import UniqueTogetherValidator
|
||||||
@ -9,10 +11,12 @@ 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.choices import *
|
from ipam.choices import *
|
||||||
|
from ipam.constants import IPADDRESS_ASSIGNMENT_MODELS
|
||||||
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 tenancy.api.nested_serializers import NestedTenantSerializer
|
from tenancy.api.nested_serializers import NestedTenantSerializer
|
||||||
from utilities.api import (
|
from utilities.api import (
|
||||||
ChoiceField, SerializedPKRelatedField, ValidatedModelSerializer, WritableNestedSerializer,
|
ChoiceField, ContentTypeField, SerializedPKRelatedField, ValidatedModelSerializer, WritableNestedSerializer,
|
||||||
|
get_serializer_for_model,
|
||||||
)
|
)
|
||||||
from virtualization.api.nested_serializers import NestedVirtualMachineSerializer
|
from virtualization.api.nested_serializers import NestedVirtualMachineSerializer
|
||||||
from .nested_serializers import *
|
from .nested_serializers import *
|
||||||
@ -228,18 +232,31 @@ class IPAddressSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
|||||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||||
status = ChoiceField(choices=IPAddressStatusChoices, required=False)
|
status = ChoiceField(choices=IPAddressStatusChoices, required=False)
|
||||||
role = ChoiceField(choices=IPAddressRoleChoices, allow_blank=True, required=False)
|
role = ChoiceField(choices=IPAddressRoleChoices, allow_blank=True, required=False)
|
||||||
interface = IPAddressInterfaceSerializer(required=False, allow_null=True)
|
assigned_object_type = ContentTypeField(
|
||||||
|
queryset=ContentType.objects.filter(IPADDRESS_ASSIGNMENT_MODELS),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
assigned_object = serializers.SerializerMethodField(read_only=True)
|
||||||
nat_inside = NestedIPAddressSerializer(required=False, allow_null=True)
|
nat_inside = NestedIPAddressSerializer(required=False, allow_null=True)
|
||||||
nat_outside = NestedIPAddressSerializer(read_only=True)
|
nat_outside = NestedIPAddressSerializer(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = IPAddress
|
model = IPAddress
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'family', 'address', 'vrf', 'tenant', 'status', 'role', 'interface', 'nat_inside',
|
'id', 'family', 'address', 'vrf', 'tenant', 'status', 'role', 'assigned_object_type', 'assigned_object_id',
|
||||||
'nat_outside', 'dns_name', 'description', 'tags', 'custom_fields', 'created', 'last_updated',
|
'assigned_object', 'nat_inside', 'nat_outside', 'dns_name', 'description', 'tags', 'custom_fields',
|
||||||
|
'created', 'last_updated',
|
||||||
]
|
]
|
||||||
read_only_fields = ['family']
|
read_only_fields = ['family']
|
||||||
|
|
||||||
|
@swagger_serializer_method(serializer_or_field=serializers.DictField)
|
||||||
|
def get_assigned_object(self, obj):
|
||||||
|
if obj.assigned_object is None:
|
||||||
|
return None
|
||||||
|
serializer = get_serializer_for_model(obj.assigned_object, prefix='Nested')
|
||||||
|
context = {'request': self.context['request']}
|
||||||
|
return serializer(obj.assigned_object, context=context).data
|
||||||
|
|
||||||
|
|
||||||
class AvailableIPSerializer(serializers.Serializer):
|
class AvailableIPSerializer(serializers.Serializer):
|
||||||
"""
|
"""
|
||||||
|
@ -233,8 +233,7 @@ class PrefixViewSet(CustomFieldModelViewSet):
|
|||||||
|
|
||||||
class IPAddressViewSet(CustomFieldModelViewSet):
|
class IPAddressViewSet(CustomFieldModelViewSet):
|
||||||
queryset = IPAddress.objects.prefetch_related(
|
queryset = IPAddress.objects.prefetch_related(
|
||||||
'vrf__tenant', 'tenant', 'nat_inside', 'interface__device__device_type', 'interface__virtual_machine',
|
'vrf__tenant', 'tenant', 'nat_inside', 'nat_outside', 'tags',
|
||||||
'nat_outside', 'tags',
|
|
||||||
)
|
)
|
||||||
serializer_class = serializers.IPAddressSerializer
|
serializer_class = serializers.IPAddressSerializer
|
||||||
filterset_class = filters.IPAddressFilterSet
|
filterset_class = filters.IPAddressFilterSet
|
||||||
|
@ -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='vminterface')
|
||||||
|
)
|
||||||
|
|
||||||
IPADDRESS_MASK_LENGTH_MIN = 1
|
IPADDRESS_MASK_LENGTH_MIN = 1
|
||||||
IPADDRESS_MASK_LENGTH_MAX = 128 # IPv6
|
IPADDRESS_MASK_LENGTH_MAX = 128 # IPv6
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ from utilities.filters import (
|
|||||||
BaseFilterSet, MultiValueCharFilter, MultiValueNumberFilter, NameSlugSearchFilterSet, TagFilter,
|
BaseFilterSet, MultiValueCharFilter, MultiValueNumberFilter, NameSlugSearchFilterSet, TagFilter,
|
||||||
TreeNodeMultipleChoiceFilter,
|
TreeNodeMultipleChoiceFilter,
|
||||||
)
|
)
|
||||||
from virtualization.models import VirtualMachine
|
from virtualization.models import VirtualMachine, VMInterface
|
||||||
from .choices import *
|
from .choices 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
|
||||||
|
|
||||||
@ -309,27 +309,38 @@ class IPAddressFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldFilterSet,
|
|||||||
field_name='pk',
|
field_name='pk',
|
||||||
label='Device (ID)',
|
label='Device (ID)',
|
||||||
)
|
)
|
||||||
virtual_machine_id = django_filters.ModelMultipleChoiceFilter(
|
virtual_machine = MultiValueCharFilter(
|
||||||
field_name='interface__virtual_machine',
|
method='filter_virtual_machine',
|
||||||
queryset=VirtualMachine.objects.unrestricted(),
|
field_name='name',
|
||||||
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)',
|
label='Virtual machine (name)',
|
||||||
)
|
)
|
||||||
|
virtual_machine_id = MultiValueNumberFilter(
|
||||||
|
method='filter_virtual_machine',
|
||||||
|
field_name='pk',
|
||||||
|
label='Virtual machine (ID)',
|
||||||
|
)
|
||||||
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 (name)',
|
||||||
)
|
)
|
||||||
interface_id = django_filters.ModelMultipleChoiceFilter(
|
interface_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
field_name='interface',
|
||||||
queryset=Interface.objects.unrestricted(),
|
queryset=Interface.objects.unrestricted(),
|
||||||
label='Interface (ID)',
|
label='Interface (ID)',
|
||||||
)
|
)
|
||||||
|
vminterface = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
field_name='vminterface__name',
|
||||||
|
queryset=VMInterface.objects.unrestricted(),
|
||||||
|
to_field_name='name',
|
||||||
|
label='VM interface (name)',
|
||||||
|
)
|
||||||
|
vminterface_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
field_name='vminterface',
|
||||||
|
queryset=VMInterface.objects.unrestricted(),
|
||||||
|
label='VM 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',
|
||||||
@ -379,17 +390,29 @@ class IPAddressFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldFilterSet,
|
|||||||
return queryset.filter(address__net_mask_length=value)
|
return queryset.filter(address__net_mask_length=value)
|
||||||
|
|
||||||
def filter_device(self, queryset, name, value):
|
def filter_device(self, queryset, name, value):
|
||||||
try:
|
devices = Device.objects.filter(**{'{}__in'.format(name): value})
|
||||||
devices = Device.objects.prefetch_related('device_type').filter(**{'{}__in'.format(name): value})
|
if not devices.exists():
|
||||||
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:
|
|
||||||
return queryset.none()
|
return queryset.none()
|
||||||
|
interface_ids = []
|
||||||
|
for device in devices:
|
||||||
|
interface_ids.extend(device.vc_interfaces.values_list('id', flat=True))
|
||||||
|
return queryset.filter(
|
||||||
|
interface__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(
|
||||||
|
vminterface__in=interface_ids
|
||||||
|
)
|
||||||
|
|
||||||
def _assigned_to_interface(self, queryset, name, value):
|
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):
|
class VLANGroupFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
|
||||||
|
@ -14,7 +14,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 VirtualMachine, VMInterface
|
||||||
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
|
||||||
@ -522,10 +522,33 @@ class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm)
|
|||||||
#
|
#
|
||||||
|
|
||||||
class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModelForm):
|
class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModelForm):
|
||||||
interface = forms.ModelChoiceField(
|
device = DynamicModelChoiceField(
|
||||||
|
queryset=Device.objects.all(),
|
||||||
|
required=False,
|
||||||
|
widget=APISelect(
|
||||||
|
filter_for={
|
||||||
|
'interface': 'device_id'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
interface = DynamicModelChoiceField(
|
||||||
queryset=Interface.objects.all(),
|
queryset=Interface.objects.all(),
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
|
virtual_machine = DynamicModelChoiceField(
|
||||||
|
queryset=VirtualMachine.objects.all(),
|
||||||
|
required=False,
|
||||||
|
widget=APISelect(
|
||||||
|
filter_for={
|
||||||
|
'vminterface': 'virtual_machine_id'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
vminterface = DynamicModelChoiceField(
|
||||||
|
queryset=VMInterface.objects.all(),
|
||||||
|
required=False,
|
||||||
|
label='Interface'
|
||||||
|
)
|
||||||
vrf = DynamicModelChoiceField(
|
vrf = DynamicModelChoiceField(
|
||||||
queryset=VRF.objects.all(),
|
queryset=VRF.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
@ -597,8 +620,8 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModel
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = IPAddress
|
model = IPAddress
|
||||||
fields = [
|
fields = [
|
||||||
'address', 'vrf', 'status', 'role', 'dns_name', 'description', 'interface', 'primary_for_parent',
|
'address', 'vrf', 'status', 'role', 'dns_name', 'description', 'primary_for_parent', 'nat_site', 'nat_rack',
|
||||||
'nat_site', 'nat_rack', 'nat_inside', 'tenant_group', 'tenant', 'tags',
|
'nat_inside', 'tenant_group', 'tenant', 'tags',
|
||||||
]
|
]
|
||||||
widgets = {
|
widgets = {
|
||||||
'status': StaticSelect2(),
|
'status': StaticSelect2(),
|
||||||
@ -610,32 +633,26 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModel
|
|||||||
# Initialize helper selectors
|
# Initialize helper selectors
|
||||||
instance = kwargs.get('instance')
|
instance = kwargs.get('instance')
|
||||||
initial = kwargs.get('initial', {}).copy()
|
initial = kwargs.get('initial', {}).copy()
|
||||||
if instance and instance.nat_inside and instance.nat_inside.device is not None:
|
if instance:
|
||||||
initial['nat_site'] = instance.nat_inside.device.site
|
if type(instance.assigned_object) is Interface:
|
||||||
initial['nat_rack'] = instance.nat_inside.device.rack
|
initial['device'] = instance.assigned_object.device
|
||||||
initial['nat_device'] = instance.nat_inside.device
|
initial['interface'] = instance.assigned_object
|
||||||
|
elif type(instance.assigned_object) is VMInterface:
|
||||||
|
initial['virtual_machine'] = instance.assigned_object.virtual_machine
|
||||||
|
initial['vminterface'] = instance.assigned_object
|
||||||
|
if instance.nat_inside and instance.nat_inside.device is not None:
|
||||||
|
initial['nat_site'] = instance.nat_inside.device.site
|
||||||
|
initial['nat_rack'] = instance.nat_inside.device.rack
|
||||||
|
initial['nat_device'] = instance.nat_inside.device
|
||||||
kwargs['initial'] = initial
|
kwargs['initial'] = initial
|
||||||
|
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
self.fields['vrf'].empty_label = 'Global'
|
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
|
# Initialize primary_for_parent if IP address is already assigned
|
||||||
if self.instance.pk and self.instance.interface is not None:
|
if self.instance.pk and self.instance.assigned_object:
|
||||||
parent = self.instance.interface.parent
|
parent = self.instance.assigned_object.parent
|
||||||
if (
|
if (
|
||||||
self.instance.address.version == 4 and parent.primary_ip4_id == self.instance.pk or
|
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.instance.address.version == 6 and parent.primary_ip6_id == self.instance.pk
|
||||||
@ -645,32 +662,39 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModel
|
|||||||
def clean(self):
|
def clean(self):
|
||||||
super().clean()
|
super().clean()
|
||||||
|
|
||||||
|
# Cannot select both a device interface and a VM interface
|
||||||
|
if self.cleaned_data.get('interface') and self.cleaned_data.get('vminterface'):
|
||||||
|
raise forms.ValidationError("Cannot select both a device interface and a virtual machine interface")
|
||||||
|
|
||||||
# Primary IP assignment is only available if an interface has been assigned.
|
# Primary IP assignment is only available if an interface has been assigned.
|
||||||
if self.cleaned_data.get('primary_for_parent') and not self.cleaned_data.get('interface'):
|
interface = self.cleaned_data.get('interface') or self.cleaned_data.get('vminterface')
|
||||||
|
if self.cleaned_data.get('primary_for_parent') and not interface:
|
||||||
self.add_error(
|
self.add_error(
|
||||||
'primary_for_parent', "Only IP addresses assigned to an interface can be designated as primary IPs."
|
'primary_for_parent', "Only IP addresses assigned to an interface can be designated as primary IPs."
|
||||||
)
|
)
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
|
|
||||||
|
# Set assigned object
|
||||||
|
interface = self.cleaned_data.get('interface') or self.cleaned_data.get('vminterface')
|
||||||
|
if interface:
|
||||||
|
self.instance.assigned_object = interface
|
||||||
|
|
||||||
ipaddress = super().save(*args, **kwargs)
|
ipaddress = super().save(*args, **kwargs)
|
||||||
|
|
||||||
# Assign/clear this IPAddress as the primary for the associated Device/VirtualMachine.
|
# Assign/clear this IPAddress as the primary for the associated Device/VirtualMachine.
|
||||||
if self.cleaned_data['primary_for_parent']:
|
if interface and self.cleaned_data['primary_for_parent']:
|
||||||
parent = self.cleaned_data['interface'].parent
|
|
||||||
if ipaddress.address.version == 4:
|
if ipaddress.address.version == 4:
|
||||||
parent.primary_ip4 = ipaddress
|
interface.parent.primary_ip4 = ipaddress
|
||||||
else:
|
else:
|
||||||
parent.primary_ip6 = ipaddress
|
interface.primary_ip6 = ipaddress
|
||||||
parent.save()
|
interface.parent.save()
|
||||||
elif self.cleaned_data['interface']:
|
elif interface and ipaddress.address.version == 4 and interface.parent.primary_ip4 == ipaddress:
|
||||||
parent = self.cleaned_data['interface'].parent
|
interface.parent.primary_ip4 = None
|
||||||
if ipaddress.address.version == 4 and parent.primary_ip4 == ipaddress:
|
interface.parent.save()
|
||||||
parent.primary_ip4 = None
|
elif interface and ipaddress.address.version == 6 and interface.parent.primary_ip6 == ipaddress:
|
||||||
parent.save()
|
interface.parent.primary_ip4 = None
|
||||||
elif ipaddress.address.version == 6 and parent.primary_ip6 == ipaddress:
|
interface.parent.save()
|
||||||
parent.primary_ip6 = None
|
|
||||||
parent.save()
|
|
||||||
|
|
||||||
return ipaddress
|
return ipaddress
|
||||||
|
|
||||||
@ -742,7 +766,7 @@ class IPAddressCSVForm(CustomFieldModelCSVForm):
|
|||||||
help_text='Parent VM of assigned interface (if any)'
|
help_text='Parent VM of assigned interface (if any)'
|
||||||
)
|
)
|
||||||
interface = CSVModelChoiceField(
|
interface = CSVModelChoiceField(
|
||||||
queryset=Interface.objects.all(),
|
queryset=Interface.objects.none(), # Can also refer to VMInterface
|
||||||
required=False,
|
required=False,
|
||||||
to_field_name='name',
|
to_field_name='name',
|
||||||
help_text='Assigned interface'
|
help_text='Assigned interface'
|
||||||
@ -761,21 +785,17 @@ class IPAddressCSVForm(CustomFieldModelCSVForm):
|
|||||||
|
|
||||||
if data:
|
if data:
|
||||||
|
|
||||||
# Limit interface queryset by assigned device or virtual machine
|
# Limit interface queryset by assigned device
|
||||||
if data.get('device'):
|
if data.get('device'):
|
||||||
params = {
|
self.fields['interface'].queryset = Interface.objects.filter(
|
||||||
f"device__{self.fields['device'].to_field_name}": data.get('device')
|
**{f"device__{self.fields['device'].to_field_name}": data['device']}
|
||||||
}
|
)
|
||||||
|
|
||||||
|
# Limit interface queryset by assigned device
|
||||||
elif data.get('virtual_machine'):
|
elif data.get('virtual_machine'):
|
||||||
params = {
|
self.fields['interface'].queryset = VMInterface.objects.filter(
|
||||||
f"virtual_machine__{self.fields['virtual_machine'].to_field_name}": data.get('virtual_machine')
|
**{f"virtual_machine__{self.fields['virtual_machine'].to_field_name}": data['virtual_machine']}
|
||||||
}
|
)
|
||||||
else:
|
|
||||||
params = {
|
|
||||||
'device': None,
|
|
||||||
'virtual_machine': None,
|
|
||||||
}
|
|
||||||
self.fields['interface'].queryset = self.fields['interface'].queryset.filter(**params)
|
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
super().clean()
|
super().clean()
|
||||||
@ -790,6 +810,10 @@ class IPAddressCSVForm(CustomFieldModelCSVForm):
|
|||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
|
|
||||||
|
# Set interface assignment
|
||||||
|
if self.cleaned_data['interface']:
|
||||||
|
self.instance.assigned_object = self.cleaned_data['interface']
|
||||||
|
|
||||||
ipaddress = super().save(*args, **kwargs)
|
ipaddress = super().save(*args, **kwargs)
|
||||||
|
|
||||||
# Set as primary for device/VM
|
# Set as primary for device/VM
|
||||||
@ -1194,13 +1218,12 @@ 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
|
interface__in=self.instance.device.vc_interfaces.values_list('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
|
vminterface__in=self.instance.virtual_machine.interfaces.values_list('id', flat=True)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self.fields['ipaddresses'].choices = []
|
self.fields['ipaddresses'].choices = []
|
||||||
|
40
netbox/ipam/migrations/0037_ipaddress_assignment.py
Normal file
40
netbox/ipam/migrations/0037_ipaddress_assignment.py
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
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.AlterField(
|
||||||
|
model_name='ipaddress',
|
||||||
|
name='assigned_object_id',
|
||||||
|
field=models.PositiveIntegerField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='ipaddress',
|
||||||
|
name='assigned_object_type',
|
||||||
|
field=models.ForeignKey(blank=True, limit_choices_to=models.Q(models.Q(models.Q(('app_label', 'dcim'), ('model', 'interface')), models.Q(('app_label', 'virtualization'), ('model', 'vminterface')), _connector='OR')), null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.ContentType'),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.RunPython(
|
||||||
|
code=set_assigned_object_type
|
||||||
|
),
|
||||||
|
]
|
@ -1,10 +1,11 @@
|
|||||||
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.core.exceptions import ValidationError, ObjectDoesNotExist
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import F, Q
|
from django.db.models import F
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from taggit.managers import TaggableManager
|
from taggit.managers import TaggableManager
|
||||||
|
|
||||||
@ -14,7 +15,7 @@ from extras.utils import extras_features
|
|||||||
from utilities.models import ChangeLoggedModel
|
from utilities.models import ChangeLoggedModel
|
||||||
from utilities.querysets import RestrictedQuerySet
|
from utilities.querysets import RestrictedQuerySet
|
||||||
from utilities.utils import serialize_object
|
from utilities.utils import serialize_object
|
||||||
from virtualization.models import VirtualMachine
|
from virtualization.models import VirtualMachine, VMInterface
|
||||||
from .choices import *
|
from .choices import *
|
||||||
from .constants import *
|
from .constants import *
|
||||||
from .fields import IPNetworkField, IPAddressField
|
from .fields import IPNetworkField, IPAddressField
|
||||||
@ -606,13 +607,22 @@ 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='dcim.Interface',
|
to=ContentType,
|
||||||
on_delete=models.CASCADE,
|
limit_choices_to=IPADDRESS_ASSIGNMENT_MODELS,
|
||||||
related_name='ip_addresses',
|
on_delete=models.PROTECT,
|
||||||
|
related_name='+',
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True
|
null=True
|
||||||
)
|
)
|
||||||
|
assigned_object_id = models.PositiveIntegerField(
|
||||||
|
blank=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,
|
||||||
@ -643,11 +653,11 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel):
|
|||||||
objects = IPAddressManager()
|
objects = IPAddressManager()
|
||||||
|
|
||||||
csv_headers = [
|
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',
|
'dns_name', 'description',
|
||||||
]
|
]
|
||||||
clone_fields = [
|
clone_fields = [
|
||||||
'vrf', 'tenant', 'status', 'role', 'description', 'interface',
|
'vrf', 'tenant', 'status', 'role', 'description',
|
||||||
]
|
]
|
||||||
|
|
||||||
STATUS_CLASS_MAP = {
|
STATUS_CLASS_MAP = {
|
||||||
@ -707,32 +717,31 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel):
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
if self.pk:
|
# Check for primary IP assignment that doesn't match the assigned device/VM
|
||||||
|
if self.pk and type(self.assigned_object) is Interface:
|
||||||
# Check for primary IP assignment that doesn't match the assigned device/VM
|
|
||||||
device = Device.objects.filter(Q(primary_ip4=self) | Q(primary_ip6=self)).first()
|
device = Device.objects.filter(Q(primary_ip4=self) | Q(primary_ip6=self)).first()
|
||||||
if device:
|
if device:
|
||||||
if self.interface is None:
|
if self.assigned_object is None:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'interface': "IP address is primary for device {} but not assigned".format(device)
|
'interface': f"IP address is primary for device {device} but not assigned to an interface"
|
||||||
})
|
})
|
||||||
elif (device.primary_ip4 == self or device.primary_ip6 == self) and self.interface.device != device:
|
elif self.assigned_object.device != device:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'interface': "IP address is primary for device {} but assigned to {} ({})".format(
|
'interface': f"IP address is primary for device {device} but assigned to "
|
||||||
device, self.interface.device, self.interface
|
f"{self.assigned_object.device} ({self.assigned_object})"
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
elif self.pk and type(self.assigned_object) is VMInterface:
|
||||||
vm = VirtualMachine.objects.filter(Q(primary_ip4=self) | Q(primary_ip6=self)).first()
|
vm = VirtualMachine.objects.filter(Q(primary_ip4=self) | Q(primary_ip6=self)).first()
|
||||||
if vm:
|
if vm:
|
||||||
if self.interface is None:
|
if self.assigned_object is None:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'interface': "IP address is primary for virtual machine {} but not assigned".format(vm)
|
'vminterface': f"IP address is primary for virtual machine {vm} but not assigned to an "
|
||||||
|
f"interface"
|
||||||
})
|
})
|
||||||
elif (vm.primary_ip4 == self or vm.primary_ip6 == self) and self.interface.virtual_machine != vm:
|
elif self.interface.virtual_machine != vm:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'interface': "IP address is primary for virtual machine {} but assigned to {} ({})".format(
|
'vminterface': f"IP address is primary for virtual machine {vm} but assigned to "
|
||||||
vm, self.interface.virtual_machine, self.interface
|
f"{self.assigned_object.virtual_machine} ({self.assigned_object})"
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
@ -743,29 +752,27 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel):
|
|||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
def to_objectchange(self, action):
|
def to_objectchange(self, action):
|
||||||
# Annotate the assigned Interface (if any)
|
# Annotate the assigned object, if any
|
||||||
try:
|
|
||||||
parent_obj = self.interface
|
|
||||||
except ObjectDoesNotExist:
|
|
||||||
parent_obj = 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_obj,
|
related_object=self.assigned_object,
|
||||||
object_data=serialize_object(self)
|
object_data=serialize_object(self)
|
||||||
)
|
)
|
||||||
|
|
||||||
def to_csv(self):
|
def to_csv(self):
|
||||||
|
|
||||||
# Determine if this IP is primary for a Device
|
# Determine if this IP is primary for a Device
|
||||||
|
is_primary = False
|
||||||
if self.address.version == 4 and getattr(self, 'primary_ip4_for', False):
|
if self.address.version == 4 and getattr(self, 'primary_ip4_for', False):
|
||||||
is_primary = True
|
is_primary = True
|
||||||
elif self.address.version == 6 and getattr(self, 'primary_ip6_for', False):
|
elif self.address.version == 6 and getattr(self, 'primary_ip6_for', False):
|
||||||
is_primary = True
|
is_primary = True
|
||||||
else:
|
|
||||||
is_primary = False
|
obj_type = None
|
||||||
|
if self.assigned_object_type:
|
||||||
|
obj_type = f'{self.assigned_object_type.app_label}.{self.assigned_object_type.model}'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
self.address,
|
self.address,
|
||||||
@ -773,9 +780,8 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel):
|
|||||||
self.tenant.name if self.tenant else None,
|
self.tenant.name if self.tenant else None,
|
||||||
self.get_status_display(),
|
self.get_status_display(),
|
||||||
self.get_role_display(),
|
self.get_role_display(),
|
||||||
self.device.identifier if self.device else None,
|
obj_type,
|
||||||
self.virtual_machine.name if self.virtual_machine else None,
|
self.assigned_object_id,
|
||||||
self.interface.name if self.interface else None,
|
|
||||||
is_primary,
|
is_primary,
|
||||||
self.dns_name,
|
self.dns_name,
|
||||||
self.description,
|
self.description,
|
||||||
@ -796,18 +802,6 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel):
|
|||||||
self.address.prefixlen = value
|
self.address.prefixlen = value
|
||||||
mask_length = property(fset=_set_mask_length)
|
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):
|
def get_status_class(self):
|
||||||
return self.STATUS_CLASS_MAP.get(self.status)
|
return self.STATUS_CLASS_MAP.get(self.status)
|
||||||
|
|
||||||
|
@ -92,14 +92,6 @@ IPADDRESS_ASSIGN_LINK = """
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
IPADDRESS_PARENT = """
|
|
||||||
{% if record.interface %}
|
|
||||||
<a href="{{ record.interface.parent.get_absolute_url }}">{{ record.interface.parent }}</a>
|
|
||||||
{% else %}
|
|
||||||
—
|
|
||||||
{% endif %}
|
|
||||||
"""
|
|
||||||
|
|
||||||
VRF_LINK = """
|
VRF_LINK = """
|
||||||
{% if record.vrf %}
|
{% if record.vrf %}
|
||||||
<a href="{{ record.vrf.get_absolute_url }}">{{ record.vrf }}</a>
|
<a href="{{ record.vrf.get_absolute_url }}">{{ record.vrf }}</a>
|
||||||
@ -168,7 +160,7 @@ VLAN_MEMBER_UNTAGGED = """
|
|||||||
|
|
||||||
VLAN_MEMBER_ACTIONS = """
|
VLAN_MEMBER_ACTIONS = """
|
||||||
{% if perms.dcim.change_interface %}
|
{% if perms.dcim.change_interface %}
|
||||||
<a href="{% if record.device %}{% url 'dcim:interface_edit' pk=record.pk %}{% else %}{% url 'virtualization:interface_edit' pk=record.pk %}{% endif %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil"></i></a>
|
<a href="{% if record.device %}{% url 'dcim:interface_edit' pk=record.pk %}{% else %}{% url 'virtualization:vminterface_edit' pk=record.pk %}{% endif %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil"></i></a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -431,18 +423,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 +453,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',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -481,17 +469,13 @@ class IPAddressAssignTable(BaseTable):
|
|||||||
status = tables.TemplateColumn(
|
status = tables.TemplateColumn(
|
||||||
template_code=STATUS_LABEL
|
template_code=STATUS_LABEL
|
||||||
)
|
)
|
||||||
parent = tables.TemplateColumn(
|
assigned_object = tables.Column(
|
||||||
template_code=IPADDRESS_PARENT,
|
|
||||||
orderable=False
|
|
||||||
)
|
|
||||||
interface = tables.Column(
|
|
||||||
orderable=False
|
orderable=False
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = IPAddress
|
model = IPAddress
|
||||||
fields = ('address', 'dns_name', 'vrf', 'status', 'role', 'tenant', 'parent', 'interface', 'description')
|
fields = ('address', 'dns_name', 'vrf', 'status', 'role', 'tenant', 'assigned_object', 'description')
|
||||||
orderable = False
|
orderable = False
|
||||||
|
|
||||||
|
|
||||||
|
@ -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, VirtualMachine, VMInterface
|
||||||
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 = (
|
vminterfaces = (
|
||||||
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(vminterfaces)
|
||||||
|
|
||||||
tenant_groups = (
|
tenant_groups = (
|
||||||
TenantGroup(name='Tenant group 1', slug='tenant-group-1'),
|
TenantGroup(name='Tenant group 1', slug='tenant-group-1'),
|
||||||
@ -411,16 +415,16 @@ class IPAddressTestCase(TestCase):
|
|||||||
Tenant.objects.bulk_create(tenants)
|
Tenant.objects.bulk_create(tenants)
|
||||||
|
|
||||||
ipaddresses = (
|
ipaddresses = (
|
||||||
IPAddress(address='10.0.0.1/24', tenant=None, vrf=None, interface=None, status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-a'),
|
IPAddress(address='10.0.0.1/24', tenant=None, vrf=None, assigned_object=None, status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-a'),
|
||||||
IPAddress(address='10.0.0.2/24', tenant=tenants[0], vrf=vrfs[0], interface=interfaces[0], status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-b'),
|
IPAddress(address='10.0.0.2/24', tenant=tenants[0], vrf=vrfs[0], assigned_object=interfaces[0], status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-b'),
|
||||||
IPAddress(address='10.0.0.3/24', tenant=tenants[1], vrf=vrfs[1], interface=interfaces[1], status=IPAddressStatusChoices.STATUS_RESERVED, role=IPAddressRoleChoices.ROLE_VIP, dns_name='ipaddress-c'),
|
IPAddress(address='10.0.0.3/24', tenant=tenants[1], vrf=vrfs[1], assigned_object=interfaces[1], status=IPAddressStatusChoices.STATUS_RESERVED, role=IPAddressRoleChoices.ROLE_VIP, dns_name='ipaddress-c'),
|
||||||
IPAddress(address='10.0.0.4/24', tenant=tenants[2], vrf=vrfs[2], interface=interfaces[2], status=IPAddressStatusChoices.STATUS_DEPRECATED, role=IPAddressRoleChoices.ROLE_SECONDARY, dns_name='ipaddress-d'),
|
IPAddress(address='10.0.0.4/24', tenant=tenants[2], vrf=vrfs[2], assigned_object=interfaces[2], status=IPAddressStatusChoices.STATUS_DEPRECATED, role=IPAddressRoleChoices.ROLE_SECONDARY, dns_name='ipaddress-d'),
|
||||||
IPAddress(address='10.0.0.1/25', tenant=None, vrf=None, interface=None, status=IPAddressStatusChoices.STATUS_ACTIVE),
|
IPAddress(address='10.0.0.1/25', tenant=None, vrf=None, assigned_object=None, status=IPAddressStatusChoices.STATUS_ACTIVE),
|
||||||
IPAddress(address='2001:db8::1/64', tenant=None, vrf=None, interface=None, status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-a'),
|
IPAddress(address='2001:db8::1/64', tenant=None, vrf=None, assigned_object=None, status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-a'),
|
||||||
IPAddress(address='2001:db8::2/64', tenant=tenants[0], vrf=vrfs[0], interface=interfaces[3], status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-b'),
|
IPAddress(address='2001:db8::2/64', tenant=tenants[0], vrf=vrfs[0], assigned_object=vminterfaces[0], status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-b'),
|
||||||
IPAddress(address='2001:db8::3/64', tenant=tenants[1], vrf=vrfs[1], interface=interfaces[4], status=IPAddressStatusChoices.STATUS_RESERVED, role=IPAddressRoleChoices.ROLE_VIP, dns_name='ipaddress-c'),
|
IPAddress(address='2001:db8::3/64', tenant=tenants[1], vrf=vrfs[1], assigned_object=vminterfaces[1], status=IPAddressStatusChoices.STATUS_RESERVED, role=IPAddressRoleChoices.ROLE_VIP, dns_name='ipaddress-c'),
|
||||||
IPAddress(address='2001:db8::4/64', tenant=tenants[2], vrf=vrfs[2], interface=interfaces[5], status=IPAddressStatusChoices.STATUS_DEPRECATED, role=IPAddressRoleChoices.ROLE_SECONDARY, dns_name='ipaddress-d'),
|
IPAddress(address='2001:db8::4/64', tenant=tenants[2], vrf=vrfs[2], assigned_object=vminterfaces[2], status=IPAddressStatusChoices.STATUS_DEPRECATED, role=IPAddressRoleChoices.ROLE_SECONDARY, dns_name='ipaddress-d'),
|
||||||
IPAddress(address='2001:db8::1/65', tenant=None, vrf=None, interface=None, status=IPAddressStatusChoices.STATUS_ACTIVE),
|
IPAddress(address='2001:db8::1/65', tenant=None, vrf=None, assigned_object=None, status=IPAddressStatusChoices.STATUS_ACTIVE),
|
||||||
)
|
)
|
||||||
IPAddress.objects.bulk_create(ipaddresses)
|
IPAddress.objects.bulk_create(ipaddresses)
|
||||||
|
|
||||||
@ -487,7 +491,14 @@ class IPAddressTestCase(TestCase):
|
|||||||
params = {'interface_id': [interfaces[0].pk, interfaces[1].pk]}
|
params = {'interface_id': [interfaces[0].pk, interfaces[1].pk]}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
params = {'interface': ['Interface 1', 'Interface 2']}
|
params = {'interface': ['Interface 1', 'Interface 2']}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_vminterface(self):
|
||||||
|
vminterfaces = VMInterface.objects.all()[:2]
|
||||||
|
params = {'vminterface_id': [vminterfaces[0].pk, vminterfaces[1].pk]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
params = {'vminterface': ['Interface 1', 'Interface 2']}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
def test_assigned_to_interface(self):
|
def test_assigned_to_interface(self):
|
||||||
params = {'assigned_to_interface': 'true'}
|
params = {'assigned_to_interface': 'true'}
|
||||||
|
@ -236,7 +236,6 @@ class IPAddressTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||||||
'tenant': None,
|
'tenant': None,
|
||||||
'status': IPAddressStatusChoices.STATUS_RESERVED,
|
'status': IPAddressStatusChoices.STATUS_RESERVED,
|
||||||
'role': IPAddressRoleChoices.ROLE_ANYCAST,
|
'role': IPAddressRoleChoices.ROLE_ANYCAST,
|
||||||
'interface': None,
|
|
||||||
'nat_inside': None,
|
'nat_inside': None,
|
||||||
'dns_name': 'example',
|
'dns_name': 'example',
|
||||||
'description': 'A new IP address',
|
'description': 'A new IP address',
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import netaddr
|
import netaddr
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db.models import Count, Q
|
from django.db.models import Count
|
||||||
from django.db.models.expressions import RawSQL
|
from django.db.models.expressions import RawSQL
|
||||||
from django.shortcuts import get_object_or_404, redirect, render
|
from django.shortcuts import get_object_or_404, redirect, render
|
||||||
from django_tables2 import RequestConfig
|
from django_tables2 import RequestConfig
|
||||||
@ -11,7 +11,7 @@ from utilities.views import (
|
|||||||
BulkCreateView, BulkDeleteView, BulkEditView, BulkImportView, ObjectView, ObjectDeleteView, ObjectEditView,
|
BulkCreateView, BulkDeleteView, BulkEditView, BulkImportView, ObjectView, ObjectDeleteView, ObjectEditView,
|
||||||
ObjectListView,
|
ObjectListView,
|
||||||
)
|
)
|
||||||
from virtualization.models import VirtualMachine
|
from virtualization.models import VirtualMachine, VMInterface
|
||||||
from . import filters, forms, tables
|
from . import filters, forms, tables
|
||||||
from .choices import *
|
from .choices import *
|
||||||
from .constants import *
|
from .constants import *
|
||||||
@ -517,7 +517,7 @@ class PrefixIPAddressesView(ObjectView):
|
|||||||
|
|
||||||
# Find all IPAddresses belonging to this Prefix
|
# Find all IPAddresses belonging to this Prefix
|
||||||
ipaddresses = prefix.get_child_ips().restrict(request.user, 'view').prefetch_related(
|
ipaddresses = prefix.get_child_ips().restrict(request.user, 'view').prefetch_related(
|
||||||
'vrf', 'interface__device', 'primary_ip4_for', 'primary_ip6_for'
|
'vrf', 'primary_ip4_for', 'primary_ip6_for'
|
||||||
)
|
)
|
||||||
|
|
||||||
# Add available IP addresses to the table if requested
|
# Add available IP addresses to the table if requested
|
||||||
@ -593,7 +593,7 @@ class PrefixBulkDeleteView(BulkDeleteView):
|
|||||||
|
|
||||||
class IPAddressListView(ObjectListView):
|
class IPAddressListView(ObjectListView):
|
||||||
queryset = IPAddress.objects.prefetch_related(
|
queryset = IPAddress.objects.prefetch_related(
|
||||||
'vrf__tenant', 'tenant', 'nat_inside', 'interface__device', 'interface__virtual_machine'
|
'vrf__tenant', 'tenant', 'nat_inside'
|
||||||
)
|
)
|
||||||
filterset = filters.IPAddressFilterSet
|
filterset = filters.IPAddressFilterSet
|
||||||
filterset_form = forms.IPAddressFilterForm
|
filterset_form = forms.IPAddressFilterForm
|
||||||
@ -622,7 +622,7 @@ class IPAddressView(ObjectView):
|
|||||||
).exclude(
|
).exclude(
|
||||||
pk=ipaddress.pk
|
pk=ipaddress.pk
|
||||||
).prefetch_related(
|
).prefetch_related(
|
||||||
'nat_inside', 'interface__device'
|
'nat_inside'
|
||||||
)
|
)
|
||||||
# Exclude anycast IPs if this IP is anycast
|
# Exclude anycast IPs if this IP is anycast
|
||||||
if ipaddress.role == IPAddressRoleChoices.ROLE_ANYCAST:
|
if ipaddress.role == IPAddressRoleChoices.ROLE_ANYCAST:
|
||||||
@ -630,9 +630,7 @@ class IPAddressView(ObjectView):
|
|||||||
duplicate_ips_table = tables.IPAddressTable(list(duplicate_ips), orderable=False)
|
duplicate_ips_table = tables.IPAddressTable(list(duplicate_ips), orderable=False)
|
||||||
|
|
||||||
# Related IP table
|
# Related IP table
|
||||||
related_ips = IPAddress.objects.restrict(request.user, 'view').prefetch_related(
|
related_ips = IPAddress.objects.restrict(request.user, 'view').exclude(
|
||||||
'interface__device'
|
|
||||||
).exclude(
|
|
||||||
address=str(ipaddress.address)
|
address=str(ipaddress.address)
|
||||||
).filter(
|
).filter(
|
||||||
vrf=ipaddress.vrf, address__net_contained_or_equal=str(ipaddress.address)
|
vrf=ipaddress.vrf, address__net_contained_or_equal=str(ipaddress.address)
|
||||||
@ -661,13 +659,18 @@ class IPAddressEditView(ObjectEditView):
|
|||||||
|
|
||||||
def alter_obj(self, obj, request, url_args, url_kwargs):
|
def alter_obj(self, obj, request, url_args, url_kwargs):
|
||||||
|
|
||||||
interface_id = request.GET.get('interface')
|
if 'interface' in request.GET:
|
||||||
if interface_id:
|
|
||||||
try:
|
try:
|
||||||
obj.interface = Interface.objects.get(pk=interface_id)
|
obj.assigned_object = Interface.objects.get(pk=request.GET['interface'])
|
||||||
except (ValueError, Interface.DoesNotExist):
|
except (ValueError, Interface.DoesNotExist):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
elif 'vminterface' in request.GET:
|
||||||
|
try:
|
||||||
|
obj.assigned_object = VMInterface.objects.get(pk=request.GET['vminterface'])
|
||||||
|
except (ValueError, VMInterface.DoesNotExist):
|
||||||
|
pass
|
||||||
|
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
|
|
||||||
@ -699,9 +702,7 @@ class IPAddressAssignView(ObjectView):
|
|||||||
|
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
|
|
||||||
addresses = self.queryset.prefetch_related(
|
addresses = self.queryset.prefetch_related('vrf', 'tenant')
|
||||||
'vrf', 'tenant', 'interface__device', 'interface__virtual_machine'
|
|
||||||
)
|
|
||||||
# Limit to 100 results
|
# Limit to 100 results
|
||||||
addresses = filters.IPAddressFilterSet(request.POST, addresses).qs[:100]
|
addresses = filters.IPAddressFilterSet(request.POST, addresses).qs[:100]
|
||||||
table = tables.IPAddressAssignTable(addresses)
|
table = tables.IPAddressAssignTable(addresses)
|
||||||
@ -735,7 +736,7 @@ class IPAddressBulkImportView(BulkImportView):
|
|||||||
|
|
||||||
|
|
||||||
class IPAddressBulkEditView(BulkEditView):
|
class IPAddressBulkEditView(BulkEditView):
|
||||||
queryset = IPAddress.objects.prefetch_related('vrf__tenant', 'tenant').prefetch_related('interface__device')
|
queryset = IPAddress.objects.prefetch_related('vrf__tenant', 'tenant')
|
||||||
filterset = filters.IPAddressFilterSet
|
filterset = filters.IPAddressFilterSet
|
||||||
table = tables.IPAddressTable
|
table = tables.IPAddressTable
|
||||||
form = forms.IPAddressBulkEditForm
|
form = forms.IPAddressBulkEditForm
|
||||||
@ -743,7 +744,7 @@ class IPAddressBulkEditView(BulkEditView):
|
|||||||
|
|
||||||
|
|
||||||
class IPAddressBulkDeleteView(BulkDeleteView):
|
class IPAddressBulkDeleteView(BulkDeleteView):
|
||||||
queryset = IPAddress.objects.prefetch_related('vrf__tenant', 'tenant').prefetch_related('interface__device')
|
queryset = IPAddress.objects.prefetch_related('vrf__tenant', 'tenant')
|
||||||
filterset = filters.IPAddressFilterSet
|
filterset = filters.IPAddressFilterSet
|
||||||
table = tables.IPAddressTable
|
table = tables.IPAddressTable
|
||||||
default_return_url = 'ipam:ipaddress_list'
|
default_return_url = 'ipam:ipaddress_list'
|
||||||
|
@ -166,7 +166,7 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</span>
|
</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a href="{% if iface.device_id %}{% url 'dcim:interface_edit' pk=iface.pk %}{% else %}{% url 'virtualization:interface_edit' pk=iface.pk %}{% endif %}?return_url={{ device.get_absolute_url }}" class="btn btn-info btn-xs" title="Edit interface">
|
<a href="{% url 'dcim:interface_edit' pk=iface.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-info btn-xs" title="Edit interface">
|
||||||
<i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
|
<i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -176,7 +176,7 @@
|
|||||||
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
|
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="{% if iface.device_id %}{% url 'dcim:interface_delete' pk=iface.pk %}{% else %}{% url 'virtualization:interface_delete' pk=iface.pk %}{% endif %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs" title="Delete interface">
|
<a href="{% url 'dcim:interface_delete' pk=iface.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs" title="Delete interface">
|
||||||
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
|
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -5,29 +5,25 @@
|
|||||||
<div class="row noprint">
|
<div class="row noprint">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<ol class="breadcrumb">
|
<ol class="breadcrumb">
|
||||||
{% if interface.device %}
|
<li><a href="{% url 'dcim:device_list' %}">Devices</a></li>
|
||||||
<li><a href="{% url 'dcim:device_list' %}">Devices</a></li>
|
<li><a href="{{ interface.device.get_absolute_url }}">{{ interface.device }}</a></li>
|
||||||
{% else %}
|
|
||||||
<li><a href="{% url 'virtualization:virtualmachine_list' %}">Virtual Machines</a></li>
|
|
||||||
{% endif %}
|
|
||||||
<li><a href="{{ interface.parent.get_absolute_url }}">{{ interface.parent }}</a></li>
|
|
||||||
<li>{{ interface }}</li>
|
<li>{{ interface }}</li>
|
||||||
</ol>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="pull-right noprint">
|
<div class="pull-right noprint">
|
||||||
{% if perms.dcim.change_interface %}
|
{% if perms.dcim.change_interface %}
|
||||||
<a href="{% if interface.device %}{% url 'dcim:interface_edit' pk=interface.pk %}{% else %}{% url 'virtualization:interface_edit' pk=interface.pk %}{% endif %}" class="btn btn-warning">
|
<a href="{% url 'dcim:interface_edit' pk=interface.pk %}" class="btn btn-warning">
|
||||||
<span class="fa fa-pencil" aria-hidden="true"></span> Edit
|
<span class="fa fa-pencil" aria-hidden="true"></span> Edit
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.dcim.delete_interface %}
|
{% if perms.dcim.delete_interface %}
|
||||||
<a href="{% if interface.device %}{% url 'dcim:interface_delete' pk=interface.pk %}{% else %}{% url 'virtualization:interface_delete' pk=interface.pk %}{% endif %}" class="btn btn-danger">
|
<a href="{% url 'dcim:interface_delete' pk=interface.pk %}" class="btn btn-danger">
|
||||||
<span class="fa fa-trash" aria-hidden="true"></span> Delete
|
<span class="fa fa-trash" aria-hidden="true"></span> Delete
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<h1>{% block title %}{{ interface.parent }} / {{ interface.name }}{% endblock %}</h1>
|
<h1>{% block title %}{{ interface.device }} / {{ interface.name }}{% endblock %}</h1>
|
||||||
<ul class="nav nav-tabs">
|
<ul class="nav nav-tabs">
|
||||||
<li role="presentation"{% if not active_tab %} class="active"{% endif %}>
|
<li role="presentation"{% if not active_tab %} class="active"{% endif %}>
|
||||||
<a href="{{ interface.get_absolute_url }}">Interface</a>
|
<a href="{{ interface.get_absolute_url }}">Interface</a>
|
||||||
@ -49,9 +45,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<table class="table table-hover panel-body attr-table">
|
<table class="table table-hover panel-body attr-table">
|
||||||
<tr>
|
<tr>
|
||||||
<td>{% if interface.device %}Device{% else %}Virtual Machine{% endif %}</td>
|
<td>Device</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="{{ interface.parent.get_absolute_url }}">{{ interface.parent }}</a>
|
<a href="{{ interface.device.get_absolute_url }}">{{ interface.device }}</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
@ -96,7 +92,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>MAC Address</td>
|
<td>MAC Address</td>
|
||||||
<td>{{ interface.mac_address|placeholder }}</span></td>
|
<td><span class="text-monospace">{{ interface.mac_address|placeholder }}</span></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>802.1Q Mode</td>
|
<td>802.1Q Mode</td>
|
||||||
@ -118,7 +114,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td>Device</td>
|
<td>Device</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="{{ connected_interface.parent.get_absolute_url }}">{{ connected_interface.device }}</a>
|
<a href="{{ connected_interface.device.get_absolute_url }}">{{ connected_interface.device }}</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
@ -225,7 +221,7 @@
|
|||||||
{% for member in interface.member_interfaces.all %}
|
{% for member in interface.member_interfaces.all %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<a href="{{ member.parent.get_absolute_url }}">{{ member.parent }}</a>
|
<a href="{{ member.device.get_absolute_url }}">{{ member.device }}</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="{{ member.get_absolute_url }}">{{ member }}</a>
|
<a href="{{ member.get_absolute_url }}">{{ member }}</a>
|
||||||
|
@ -372,6 +372,14 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<a href="{% url 'virtualization:virtualmachine_list' %}">Virtual Machines</a>
|
<a href="{% url 'virtualization:virtualmachine_list' %}">Virtual Machines</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li{% if not perms.virtualization.view_vminterface%} class="disabled"{% endif %}>
|
||||||
|
{% if perms.virtualization.add_vminterface %}
|
||||||
|
<div class="buttons pull-right">
|
||||||
|
<a href="{% url 'virtualization:vminterface_import' %}" class="btn btn-xs btn-info" title="Import"><i class="fa fa-download"></i></a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<a href="{% url 'virtualization:vminterface_list' %}">Interfaces</a>
|
||||||
|
</li>
|
||||||
<li class="divider"></li>
|
<li class="divider"></li>
|
||||||
<li class="dropdown-header">Clusters</li>
|
<li class="dropdown-header">Clusters</li>
|
||||||
<li{% if not perms.virtualization.view_cluster %} class="disabled"{% endif %}>
|
<li{% if not perms.virtualization.view_cluster %} class="disabled"{% endif %}>
|
||||||
|
@ -120,8 +120,8 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td>Assignment</td>
|
<td>Assignment</td>
|
||||||
<td>
|
<td>
|
||||||
{% if ipaddress.interface %}
|
{% if ipaddress.assigned_object %}
|
||||||
<span><a href="{{ ipaddress.interface.parent.get_absolute_url }}">{{ ipaddress.interface.parent }}</a> ({{ ipaddress.interface }})</span>
|
<span><a href="{{ ipaddress.assigned_object.parent.get_absolute_url }}">{{ ipaddress.assigned_object.parent }}</a> ({{ ipaddress.assigned_object }})</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="text-muted">—</span>
|
<span class="text-muted">—</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -132,8 +132,8 @@
|
|||||||
<td>
|
<td>
|
||||||
{% if ipaddress.nat_inside %}
|
{% if ipaddress.nat_inside %}
|
||||||
<a href="{% url 'ipam:ipaddress' pk=ipaddress.nat_inside.pk %}">{{ ipaddress.nat_inside }}</a>
|
<a href="{% url 'ipam:ipaddress' pk=ipaddress.nat_inside.pk %}">{{ ipaddress.nat_inside }}</a>
|
||||||
{% if ipaddress.nat_inside.interface %}
|
{% if ipaddress.nat_inside.assigned_object %}
|
||||||
(<a href="{{ ipaddress.nat_inside.interface.parent.get_absolute_url }}">{{ ipaddress.nat_inside.interface.parent }}</a>)
|
(<a href="{{ ipaddress.nat_inside.assigned_object.parent.get_absolute_url }}">{{ ipaddress.nat_inside.assigned_object.parent }}</a>)
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="text-muted">None</span>
|
<span class="text-muted">None</span>
|
||||||
|
@ -28,25 +28,30 @@
|
|||||||
{% render_field form.tenant %}
|
{% render_field form.tenant %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% if obj.interface %}
|
<div class="panel panel-default">
|
||||||
<div class="panel panel-default">
|
<div class="panel-heading">
|
||||||
<div class="panel-heading">
|
<strong>Interface Assignment</strong>
|
||||||
<strong>Interface Assignment</strong>
|
</div>
|
||||||
</div>
|
<div class="panel-body">
|
||||||
<div class="panel-body">
|
{% with vm_tab_active=obj.vminterface.exists %}
|
||||||
<div class="form-group">
|
<ul class="nav nav-tabs" role="tablist">
|
||||||
<label class="col-md-3 control-label">{{ obj.interface.parent|meta:"verbose_name"|bettertitle }}</label>
|
<li role="presentation"{% if not vm_tab_active %} class="active"{% endif %}><a href="#device" role="tab" data-toggle="tab">Device</a></li>
|
||||||
<div class="col-md-9">
|
<li role="presentation"{% if vm_tab_active %} class="active"{% endif %}><a href="#virtualmachine" role="tab" data-toggle="tab">Virtual Machine</a></li>
|
||||||
<p class="form-control-static">
|
</ul>
|
||||||
<a href="{{ obj.interface.parent.get_absolute_url }}">{{ obj.interface.parent }}</a>
|
<div class="tab-content">
|
||||||
</p>
|
<div class="tab-pane{% if not vm_tab_active %} active{% endif %}" id="device">
|
||||||
|
{% render_field form.device %}
|
||||||
|
{% render_field form.interface %}
|
||||||
|
</div>
|
||||||
|
<div class="tab-pane{% if vm_tab_active %} active{% endif %}" id="virtualmachine">
|
||||||
|
{% render_field form.virtual_machine %}
|
||||||
|
{% render_field form.vminterface %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% render_field form.interface %}
|
{% endwith %}
|
||||||
{% render_field form.primary_for_parent %}
|
{% render_field form.primary_for_parent %}
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
</div>
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading"><strong>NAT IP (Inside)</strong></div>
|
<div class="panel-heading"><strong>NAT IP (Inside)</strong></div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
|
141
netbox/templates/virtualization/inc/vminterface.html
Normal file
141
netbox/templates/virtualization/inc/vminterface.html
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
{% load helpers %}
|
||||||
|
<tr class="interface{% if not iface.enabled %} danger{% endif %}" id="interface_{{ iface.name }}">
|
||||||
|
|
||||||
|
{# Checkbox #}
|
||||||
|
{% if perms.virtualization.change_interface or perms.virtualization.delete_interface %}
|
||||||
|
<td class="pk">
|
||||||
|
<input name="pk" type="checkbox" value="{{ iface.pk }}" />
|
||||||
|
</td>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{# Name #}
|
||||||
|
<td>
|
||||||
|
<a href="{{ iface.get_absolute_url }}">{{ iface }}</a>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
{# MAC address #}
|
||||||
|
<td class="text-monospace">
|
||||||
|
{{ iface.mac_address|default:"—" }}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
{# MTU #}
|
||||||
|
<td>{{ iface.mtu|default:"—" }}</td>
|
||||||
|
|
||||||
|
{# 802.1Q mode #}
|
||||||
|
<td>{{ iface.get_mode_display|default:"—" }}</td>
|
||||||
|
|
||||||
|
{# Description/tags #}
|
||||||
|
<td>
|
||||||
|
{% if iface.description %}
|
||||||
|
{{ iface.description }}<br/>
|
||||||
|
{% endif %}
|
||||||
|
{% for tag in iface.tags.all %}
|
||||||
|
{% tag tag %}
|
||||||
|
{% empty %}
|
||||||
|
{% if not iface.description %}—{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
{# Buttons #}
|
||||||
|
<td class="text-right text-nowrap noprint">
|
||||||
|
{% if show_interface_graphs %}
|
||||||
|
<button type="button" class="btn btn-primary btn-xs" data-toggle="modal" data-target="#graphs_modal" data-obj="{{ virtualmachine.name }} - {{ iface.name }}" data-url="{% url 'virtualization-api:vminterface-graphs' pk=iface.pk %}" title="Show graphs">
|
||||||
|
<i class="glyphicon glyphicon-signal" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
{% if perms.ipam.add_ipaddress %}
|
||||||
|
<a href="{% url 'ipam:ipaddress_add' %}?vminterface={{ iface.pk }}&return_url={{ virtualmachine.get_absolute_url }}" class="btn btn-xs btn-success" title="Add IP address">
|
||||||
|
<i class="glyphicon glyphicon-plus" aria-hidden="true"></i>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if perms.virtualization.change_interface %}
|
||||||
|
<a href="{% url 'virtualization:vminterface_edit' pk=iface.pk %}?return_url={{ virtualmachine.get_absolute_url }}" class="btn btn-info btn-xs" title="Edit interface">
|
||||||
|
<i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if perms.virtualization.delete_interface %}
|
||||||
|
<a href="{% url 'virtualization:vminterface_delete' pk=iface.pk %}?return_url={{ virtualmachine.get_absolute_url }}" class="btn btn-danger btn-xs" title="Delete interface">
|
||||||
|
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
{% with ipaddresses=iface.ip_addresses.all %}
|
||||||
|
{% if ipaddresses %}
|
||||||
|
<tr class="ipaddresses">
|
||||||
|
{# Placeholder #}
|
||||||
|
{% if perms.virtualization.change_interface or perms.virtualization.delete_interface %}
|
||||||
|
<td></td>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{# IP addresses table #}
|
||||||
|
<td colspan="9" style="padding: 0">
|
||||||
|
<table class="table table-condensed interface-ips">
|
||||||
|
<thead>
|
||||||
|
<tr class="text-muted">
|
||||||
|
<th class="col-md-3">IP Address</th>
|
||||||
|
<th class="col-md-2">Status/Role</th>
|
||||||
|
<th class="col-md-3">VRF</th>
|
||||||
|
<th class="col-md-3">Description</th>
|
||||||
|
<th class="col-md-1"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
{% for ip in iface.ip_addresses.all %}
|
||||||
|
<tr>
|
||||||
|
|
||||||
|
{# IP address #}
|
||||||
|
<td>
|
||||||
|
<a href="{% url 'ipam:ipaddress' pk=ip.pk %}">{{ ip }}</a>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
{# Primary/status/role #}
|
||||||
|
<td>
|
||||||
|
{% if virtualmachine.primary_ip4 == ip or virtualmachine.primary_ip6 == ip %}
|
||||||
|
<span class="label label-success">Primary</span>
|
||||||
|
{% endif %}
|
||||||
|
<span class="label label-{{ ip.get_status_class }}">{{ ip.get_status_display }}</span>
|
||||||
|
{% if ip.role %}
|
||||||
|
<span class="label label-{{ ip.get_role_class }}">{{ ip.get_role_display }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
{# VRF #}
|
||||||
|
<td>
|
||||||
|
{% if ip.vrf %}
|
||||||
|
<a href="{% url 'ipam:vrf' pk=ip.vrf.pk %}" title="{{ ip.vrf.rd }}">{{ ip.vrf.name }}</a>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">Global</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
{# Description #}
|
||||||
|
<td>
|
||||||
|
{% if ip.description %}
|
||||||
|
{{ ip.description }}
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">—</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
{# Buttons #}
|
||||||
|
<td class="text-right text-nowrap noprint">
|
||||||
|
{% if perms.ipam.change_ipaddress %}
|
||||||
|
<a href="{% url 'ipam:ipaddress_edit' pk=ip.pk %}?return_url={{ virtualmachine.get_absolute_url }}" class="btn btn-info btn-xs">
|
||||||
|
<i class="glyphicon glyphicon-pencil" aria-hidden="true" title="Edit IP address"></i>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if perms.ipam.delete_ipaddress %}
|
||||||
|
<a href="{% url 'ipam:ipaddress_delete' pk=ip.pk %}?return_url={{ virtualmachine.get_absolute_url }}" class="btn btn-danger btn-xs">
|
||||||
|
<i class="glyphicon glyphicon-trash" aria-hidden="true" title="Delete IP address"></i>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
@ -248,7 +248,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
{% if perms.dcim.change_interface or perms.dcim.delete_interface %}
|
{% if perms.virtualization.change_vminterface or perms.virtualization.delete_vminterface %}
|
||||||
<form method="post">
|
<form method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input type="hidden" name="virtual_machine" value="{{ virtualmachine.pk }}" />
|
<input type="hidden" name="virtual_machine" value="{{ virtualmachine.pk }}" />
|
||||||
@ -268,22 +268,20 @@
|
|||||||
<table id="interfaces_table" class="table table-hover table-headings panel-body component-list">
|
<table id="interfaces_table" class="table table-hover table-headings panel-body component-list">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
{% if perms.dcim.change_interface or perms.dcim.delete_interface %}
|
{% if perms.virtualization.change_vminterface or perms.virtualization.delete_vminterface %}
|
||||||
<th class="pk"><input type="checkbox" class="toggle" title="Toggle all" /></th>
|
<th class="pk"><input type="checkbox" class="toggle" title="Toggle all" /></th>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
<th>LAG</th>
|
<th>MAC Address</th>
|
||||||
<th>Description</th>
|
|
||||||
<th>MTU</th>
|
<th>MTU</th>
|
||||||
<th>Mode</th>
|
<th>Mode</th>
|
||||||
<th>Cable</th>
|
<th>Description</th>
|
||||||
<th colspan="2">Connection</th>
|
|
||||||
<th></th>
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for iface in interfaces %}
|
{% for iface in interfaces %}
|
||||||
{% include 'dcim/inc/interface.html' with device=virtualmachine %}
|
{% include 'virtualization/inc/vminterface.html' %}
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="8" class="text-center text-muted">— No interfaces defined —</td>
|
<td colspan="8" class="text-center text-muted">— No interfaces defined —</td>
|
||||||
@ -291,24 +289,24 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{% if perms.dcim.add_interface or perms.dcim.delete_interface %}
|
{% if perms.virtualization.add_vminterface or perms.virtualization.delete_vminterface %}
|
||||||
<div class="panel-footer noprint">
|
<div class="panel-footer noprint">
|
||||||
{% if interfaces and perms.dcim.change_interface %}
|
{% if interfaces and perms.virtualization.change_vminterface %}
|
||||||
<button type="submit" name="_rename" formaction="{% url 'dcim:interface_bulk_rename' %}?return_url={{ virtualmachine.get_absolute_url }}" class="btn btn-warning btn-xs">
|
<button type="submit" name="_rename" formaction="{% url 'virtualization:vminterface_bulk_rename' %}?return_url={{ virtualmachine.get_absolute_url }}" class="btn btn-warning btn-xs">
|
||||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
|
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
|
||||||
</button>
|
</button>
|
||||||
<button type="submit" name="_edit" formaction="{% url 'virtualization:interface_bulk_edit' %}?return_url={{ virtualmachine.get_absolute_url }}" class="btn btn-warning btn-xs">
|
<button type="submit" name="_edit" formaction="{% url 'virtualization:vminterface_bulk_edit' %}?return_url={{ virtualmachine.get_absolute_url }}" class="btn btn-warning btn-xs">
|
||||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
|
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if interfaces and perms.dcim.delete_interface %}
|
{% if interfaces and perms.virtualization.delete_vminterface %}
|
||||||
<button type="submit" name="_delete" formaction="{% url 'virtualization:interface_bulk_delete' %}?return_url={{ virtualmachine.get_absolute_url }}" class="btn btn-danger btn-xs">
|
<button type="submit" name="_delete" formaction="{% url 'virtualization:vminterface_bulk_delete' %}?return_url={{ virtualmachine.get_absolute_url }}" class="btn btn-danger btn-xs">
|
||||||
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
|
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.dcim.add_interface %}
|
{% if perms.virtualization.add_vminterface %}
|
||||||
<div class="pull-right">
|
<div class="pull-right">
|
||||||
<a href="{% url 'virtualization:interface_add' %}?virtual_machine={{ virtualmachine.pk }}&return_url={{ virtualmachine.get_absolute_url }}" class="btn btn-primary btn-xs">
|
<a href="{% url 'virtualization:vminterface_add' %}?virtual_machine={{ virtualmachine.pk }}&return_url={{ virtualmachine.get_absolute_url }}" class="btn btn-primary btn-xs">
|
||||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add interfaces
|
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add interfaces
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@ -317,7 +315,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% if perms.dcim.delete_interface %}
|
{% if perms.virtualization.delete_vminterface %}
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
@ -22,12 +22,6 @@
|
|||||||
<strong>{{ component_type|bettertitle }}</strong>
|
<strong>{{ component_type|bettertitle }}</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<div class="form-group">
|
|
||||||
<label class="col-md-3 control-label required">Virtual Machine</label>
|
|
||||||
<div class="col-md-9">
|
|
||||||
<p class="form-control-static">{{ parent }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% render_form form %}
|
{% render_form form %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add Components <span class="caret"></span>
|
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add Components <span class="caret"></span>
|
||||||
</button>
|
</button>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
{% if perms.dcim.add_interface %}<li><a href="{% url 'virtualization:virtualmachine_bulk_add_interface' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="formaction">Interfaces</a></li>{% endif %}
|
{% if perms.dcim.add_interface %}<li><a href="{% url 'virtualization:virtualmachine_bulk_add_vminterface' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="formaction">Interfaces</a></li>{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
100
netbox/templates/virtualization/vminterface.html
Normal file
100
netbox/templates/virtualization/vminterface.html
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load helpers %}
|
||||||
|
|
||||||
|
{% block header %}
|
||||||
|
<div class="row noprint">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<li><a href="{% url 'virtualization:virtualmachine_list' %}">Virtual Machines</a></li>
|
||||||
|
<li><a href="{{ vminterface.virtual_machine.get_absolute_url }}">{{ vminterface.virtual_machine }}</a></li>
|
||||||
|
<li>{{ vminterface }}</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="pull-right noprint">
|
||||||
|
{% if perms.virtualization.change_vminterface %}
|
||||||
|
<a href="{% url 'virtualization:vminterface_edit' pk=vminterface.pk %}" class="btn btn-warning">
|
||||||
|
<span class="fa fa-pencil" aria-hidden="true"></span> Edit
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if perms.virtualization.delete_vminterface %}
|
||||||
|
<a href="{% url 'virtualization:vminterface_delete' pk=vminterface.pk %}" class="btn btn-danger">
|
||||||
|
<span class="fa fa-trash" aria-hidden="true"></span> Delete
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<h1>{% block title %}{{ vminterface.virtual_machine }} / {{ vminterface.name }}{% endblock %}</h1>
|
||||||
|
<ul class="nav nav-tabs">
|
||||||
|
<li role="presentation"{% if not active_tab %} class="active"{% endif %}>
|
||||||
|
<a href="{{ vminterface.get_absolute_url }}">Interface</a>
|
||||||
|
</li>
|
||||||
|
{% if perms.extras.view_objectchange %}
|
||||||
|
<li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
|
||||||
|
<a href="{% url 'virtualization:vminterface_changelog' pk=vminterface.pk %}">Change Log</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<strong>Interface</strong>
|
||||||
|
</div>
|
||||||
|
<table class="table table-hover panel-body attr-table">
|
||||||
|
<tr>
|
||||||
|
<td>Virtual Machine</td>
|
||||||
|
<td>
|
||||||
|
<a href="{{ vminterface.virtual_machine.get_absolute_url }}">{{ vminterface.virtual_machine }}</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Name</td>
|
||||||
|
<td>{{ vminterface.name }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Enabled</td>
|
||||||
|
<td>
|
||||||
|
{% if vminterface.enabled %}
|
||||||
|
<span class="text-success"><i class="fa fa-check"></i></span>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-danger"><i class="fa fa-close"></i></span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Description</td>
|
||||||
|
<td>{{ vminterface.description|placeholder }} </td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>MTU</td>
|
||||||
|
<td>{{ vminterface.mtu|placeholder }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>MAC Address</td>
|
||||||
|
<td><span class="text-monospace">{{ vminterface.mac_address|placeholder }}</span></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>802.1Q Mode</td>
|
||||||
|
<td>{{ vminterface.get_mode_display }}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
{% include 'extras/inc/tags_panel.html' with tags=vminterface.tags.all %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
{% include 'panel_table.html' with table=ipaddress_table heading="IP Addresses" %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
{% include 'panel_table.html' with table=vlan_table heading="VLANs" %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
@ -21,7 +21,7 @@
|
|||||||
{% block buttons %}
|
{% block buttons %}
|
||||||
{% if obj.pk %}
|
{% if obj.pk %}
|
||||||
<button type="submit" name="_update" class="btn btn-primary">Update</button>
|
<button type="submit" name="_update" class="btn btn-primary">Update</button>
|
||||||
<button type="submit" formaction="?return_url={% url 'virtualization:interface_edit' pk=obj.pk %}" class="btn btn-primary">Update and Continue Editing</button>
|
<button type="submit" formaction="?return_url={% url 'virtualization:vminterface_edit' pk=obj.pk %}" class="btn btn-primary">Update and Continue Editing</button>
|
||||||
{% else %}
|
{% else %}
|
||||||
<button type="submit" name="_create" class="btn btn-primary">Create</button>
|
<button type="submit" name="_create" class="btn btn-primary">Create</button>
|
||||||
<button type="submit" name="_addanother" class="btn btn-primary">Create and Add Another</button>
|
<button type="submit" name="_addanother" class="btn btn-primary">Create and Add Another</button>
|
@ -5,14 +5,8 @@ from drf_yasg.utils import get_serializer_ref_name
|
|||||||
from rest_framework.fields import ChoiceField
|
from rest_framework.fields import ChoiceField
|
||||||
from rest_framework.relations import ManyRelatedField
|
from rest_framework.relations import ManyRelatedField
|
||||||
|
|
||||||
from dcim.api.serializers import InterfaceSerializer as DeviceInterfaceSerializer
|
|
||||||
from extras.api.customfields import CustomFieldsSerializer
|
from extras.api.customfields import CustomFieldsSerializer
|
||||||
from utilities.api import ChoiceField, SerializedPKRelatedField, WritableNestedSerializer
|
from utilities.api import ChoiceField, SerializedPKRelatedField, WritableNestedSerializer
|
||||||
from virtualization.api.serializers import InterfaceSerializer as VirtualMachineInterfaceSerializer
|
|
||||||
|
|
||||||
# this might be ugly, but it limits drf_yasg-specific code to this file
|
|
||||||
DeviceInterfaceSerializer.Meta.ref_name = 'DeviceInterface'
|
|
||||||
VirtualMachineInterfaceSerializer.Meta.ref_name = 'VirtualMachineInterface'
|
|
||||||
|
|
||||||
|
|
||||||
class NetBoxSwaggerAutoSchema(SwaggerAutoSchema):
|
class NetBoxSwaggerAutoSchema(SwaggerAutoSchema):
|
||||||
|
@ -733,6 +733,30 @@ class BulkEditForm(forms.Form):
|
|||||||
self.nullable_fields = self.Meta.nullable_fields
|
self.nullable_fields = self.Meta.nullable_fields
|
||||||
|
|
||||||
|
|
||||||
|
class BulkRenameForm(forms.Form):
|
||||||
|
"""
|
||||||
|
An extendable form to be used for renaming objects 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"
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
class CSVModelForm(forms.ModelForm):
|
class CSVModelForm(forms.ModelForm):
|
||||||
"""
|
"""
|
||||||
ModelForm used for the import of objects in CSV format.
|
ModelForm used for the import of objects in CSV format.
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import logging
|
import logging
|
||||||
|
import re
|
||||||
import sys
|
import sys
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
|
|
||||||
@ -963,6 +964,58 @@ class BulkEditView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
class BulkRenameView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
||||||
|
"""
|
||||||
|
An extendable view for renaming objects in bulk.
|
||||||
|
"""
|
||||||
|
queryset = None
|
||||||
|
form = None
|
||||||
|
template_name = 'utilities/obj_bulk_rename.html'
|
||||||
|
|
||||||
|
def get_required_permission(self):
|
||||||
|
return get_permission_for_model(self.queryset.model, 'change')
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
|
||||||
|
if '_preview' in request.POST or '_apply' in request.POST:
|
||||||
|
form = self.form(request.POST, initial={'pk': request.POST.getlist('pk')})
|
||||||
|
selected_objects = self.queryset.filter(pk__in=form.initial['pk'])
|
||||||
|
|
||||||
|
if form.is_valid():
|
||||||
|
for obj in selected_objects:
|
||||||
|
find = form.cleaned_data['find']
|
||||||
|
replace = form.cleaned_data['replace']
|
||||||
|
if form.cleaned_data['use_regex']:
|
||||||
|
try:
|
||||||
|
obj.new_name = re.sub(find, replace, obj.name)
|
||||||
|
# Catch regex group reference errors
|
||||||
|
except re.error:
|
||||||
|
obj.new_name = obj.name
|
||||||
|
else:
|
||||||
|
obj.new_name = obj.name.replace(find, replace)
|
||||||
|
|
||||||
|
if '_apply' in request.POST:
|
||||||
|
for obj in selected_objects:
|
||||||
|
obj.name = obj.new_name
|
||||||
|
obj.save()
|
||||||
|
messages.success(request, "Renamed {} {}".format(
|
||||||
|
len(selected_objects),
|
||||||
|
self.queryset.model._meta.verbose_name_plural
|
||||||
|
))
|
||||||
|
return redirect(self.get_return_url(request))
|
||||||
|
|
||||||
|
else:
|
||||||
|
form = self.form(initial={'pk': request.POST.getlist('pk')})
|
||||||
|
selected_objects = self.queryset.filter(pk__in=form.initial['pk'])
|
||||||
|
|
||||||
|
return render(request, self.template_name, {
|
||||||
|
'form': form,
|
||||||
|
'obj_type_plural': self.queryset.model._meta.verbose_name_plural,
|
||||||
|
'selected_objects': selected_objects,
|
||||||
|
'return_url': self.get_return_url(request),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
class BulkDeleteView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
class BulkDeleteView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
||||||
"""
|
"""
|
||||||
Delete objects in bulk.
|
Delete objects in bulk.
|
||||||
|
@ -8,7 +8,7 @@ __all__ = [
|
|||||||
'NestedClusterGroupSerializer',
|
'NestedClusterGroupSerializer',
|
||||||
'NestedClusterSerializer',
|
'NestedClusterSerializer',
|
||||||
'NestedClusterTypeSerializer',
|
'NestedClusterTypeSerializer',
|
||||||
'NestedInterfaceSerializer',
|
'NestedVMInterfaceSerializer',
|
||||||
'NestedVirtualMachineSerializer',
|
'NestedVirtualMachineSerializer',
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -56,8 +56,8 @@ class NestedVirtualMachineSerializer(WritableNestedSerializer):
|
|||||||
fields = ['id', 'url', 'name']
|
fields = ['id', 'url', 'name']
|
||||||
|
|
||||||
|
|
||||||
class NestedInterfaceSerializer(WritableNestedSerializer):
|
class NestedVMInterfaceSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:interface-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:vminterface-detail')
|
||||||
virtual_machine = NestedVirtualMachineSerializer(read_only=True)
|
virtual_machine = NestedVirtualMachineSerializer(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -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, VirtualMachine, VMInterface
|
||||||
from .nested_serializers import *
|
from .nested_serializers import *
|
||||||
|
|
||||||
|
|
||||||
@ -95,9 +94,8 @@ class VirtualMachineWithConfigContextSerializer(VirtualMachineSerializer):
|
|||||||
# VM interfaces
|
# VM interfaces
|
||||||
#
|
#
|
||||||
|
|
||||||
class InterfaceSerializer(TaggedObjectSerializer, ValidatedModelSerializer):
|
class VMInterfaceSerializer(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(
|
||||||
@ -108,8 +106,8 @@ class InterfaceSerializer(TaggedObjectSerializer, ValidatedModelSerializer):
|
|||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Interface
|
model = VMInterface
|
||||||
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',
|
||||||
]
|
]
|
||||||
|
@ -21,7 +21,7 @@ router.register('clusters', views.ClusterViewSet)
|
|||||||
|
|
||||||
# VirtualMachines
|
# VirtualMachines
|
||||||
router.register('virtual-machines', views.VirtualMachineViewSet)
|
router.register('virtual-machines', views.VirtualMachineViewSet)
|
||||||
router.register('interfaces', views.InterfaceViewSet)
|
router.register('interfaces', views.VMInterfaceViewSet)
|
||||||
|
|
||||||
app_name = 'virtualization-api'
|
app_name = 'virtualization-api'
|
||||||
urlpatterns = router.urls
|
urlpatterns = router.urls
|
||||||
|
@ -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, VirtualMachine, VMInterface
|
||||||
from . import serializers
|
from . import serializers
|
||||||
|
|
||||||
|
|
||||||
@ -71,18 +71,11 @@ class VirtualMachineViewSet(CustomFieldModelViewSet):
|
|||||||
return serializers.VirtualMachineWithConfigContextSerializer
|
return serializers.VirtualMachineWithConfigContextSerializer
|
||||||
|
|
||||||
|
|
||||||
class InterfaceViewSet(ModelViewSet):
|
class VMInterfaceViewSet(ModelViewSet):
|
||||||
queryset = Interface.objects.filter(
|
queryset = VMInterface.objects.filter(
|
||||||
virtual_machine__isnull=False
|
virtual_machine__isnull=False
|
||||||
).prefetch_related(
|
).prefetch_related(
|
||||||
'virtual_machine', 'tags'
|
'virtual_machine', 'tags'
|
||||||
)
|
)
|
||||||
serializer_class = serializers.InterfaceSerializer
|
serializer_class = serializers.VMInterfaceSerializer
|
||||||
filterset_class = filters.InterfaceFilterSet
|
filterset_class = filters.VMInterfaceFilterSet
|
||||||
|
|
||||||
def get_serializer_class(self):
|
|
||||||
request = self.get_serializer_context()['request']
|
|
||||||
if request.query_params.get('brief', False):
|
|
||||||
# Override get_serializer_for_model(), which will return the DCIM NestedInterfaceSerializer
|
|
||||||
return serializers.NestedInterfaceSerializer
|
|
||||||
return serializers.InterfaceSerializer
|
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
from dcim.choices import InterfaceTypeChoices
|
|
||||||
from utilities.choices import ChoiceSet
|
from utilities.choices import ChoiceSet
|
||||||
|
|
||||||
|
|
||||||
@ -29,16 +28,3 @@ class VirtualMachineStatusChoices(ChoiceSet):
|
|||||||
STATUS_ACTIVE: 1,
|
STATUS_ACTIVE: 1,
|
||||||
STATUS_STAGED: 3,
|
STATUS_STAGED: 3,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#
|
|
||||||
# Interface types (for VirtualMachines)
|
|
||||||
#
|
|
||||||
|
|
||||||
class VMInterfaceTypeChoices(ChoiceSet):
|
|
||||||
|
|
||||||
TYPE_VIRTUAL = InterfaceTypeChoices.TYPE_VIRTUAL
|
|
||||||
|
|
||||||
CHOICES = (
|
|
||||||
(TYPE_VIRTUAL, 'Virtual'),
|
|
||||||
)
|
|
||||||
|
@ -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,14 +9,14 @@ 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, VirtualMachine, VMInterface
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'ClusterFilterSet',
|
'ClusterFilterSet',
|
||||||
'ClusterGroupFilterSet',
|
'ClusterGroupFilterSet',
|
||||||
'ClusterTypeFilterSet',
|
'ClusterTypeFilterSet',
|
||||||
'InterfaceFilterSet',
|
|
||||||
'VirtualMachineFilterSet',
|
'VirtualMachineFilterSet',
|
||||||
|
'VMInterfaceFilterSet',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -201,7 +201,7 @@ class VirtualMachineFilterSet(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class InterfaceFilterSet(BaseFilterSet):
|
class VMInterfaceFilterSet(BaseFilterSet):
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
method='search',
|
method='search',
|
||||||
label='Search',
|
label='Search',
|
||||||
@ -222,7 +222,7 @@ class InterfaceFilterSet(BaseFilterSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Interface
|
model = VMInterface
|
||||||
fields = ['id', 'name', 'enabled', 'mtu']
|
fields = ['id', 'name', 'enabled', 'mtu']
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
|
@ -4,7 +4,7 @@ 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,
|
||||||
)
|
)
|
||||||
@ -14,12 +14,12 @@ from tenancy.forms import TenancyFilterForm, TenancyForm
|
|||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
from utilities.forms import (
|
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,
|
BulkRenameForm, CommentField, ConfirmationForm, CSVChoiceField, CSVModelChoiceField, CSVModelForm,
|
||||||
DynamicModelMultipleChoiceField, ExpandableNameField, form_from_model, JSONField, SlugField, SmallTextarea,
|
DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableNameField, form_from_model, JSONField,
|
||||||
StaticSelect2, StaticSelect2Multiple, TagFilterField,
|
SlugField, SmallTextarea, 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, VirtualMachine, VMInterface
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -356,7 +356,8 @@ class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
|||||||
ip_choices = [(None, '---------')]
|
ip_choices = [(None, '---------')]
|
||||||
# Collect interface IPs
|
# Collect interface IPs
|
||||||
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,
|
||||||
|
vminterface__in=self.instance.interfaces.values_list('id', flat=True)
|
||||||
)
|
)
|
||||||
if interface_ips:
|
if interface_ips:
|
||||||
ip_choices.append(
|
ip_choices.append(
|
||||||
@ -366,7 +367,8 @@ class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
|||||||
)
|
)
|
||||||
# Collect NAT IPs
|
# Collect NAT IPs
|
||||||
nat_ips = IPAddress.objects.prefetch_related('nat_inside').filter(
|
nat_ips = IPAddress.objects.prefetch_related('nat_inside').filter(
|
||||||
address__family=family, nat_inside__interface__virtual_machine=self.instance
|
address__family=family,
|
||||||
|
nat_inside__vminterface__in=self.instance.interfaces.values_list('id', flat=True)
|
||||||
)
|
)
|
||||||
if nat_ips:
|
if nat_ips:
|
||||||
ip_choices.append(
|
ip_choices.append(
|
||||||
@ -569,7 +571,7 @@ class VirtualMachineFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFil
|
|||||||
# VM interfaces
|
# VM interfaces
|
||||||
#
|
#
|
||||||
|
|
||||||
class InterfaceForm(BootstrapMixin, forms.ModelForm):
|
class VMInterfaceForm(BootstrapMixin, forms.ModelForm):
|
||||||
untagged_vlan = DynamicModelChoiceField(
|
untagged_vlan = DynamicModelChoiceField(
|
||||||
queryset=VLAN.objects.all(),
|
queryset=VLAN.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
@ -598,14 +600,13 @@ class InterfaceForm(BootstrapMixin, forms.ModelForm):
|
|||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Interface
|
model = VMInterface
|
||||||
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 = {
|
||||||
@ -618,10 +619,13 @@ class InterfaceForm(BootstrapMixin, forms.ModelForm):
|
|||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
virtual_machine = VirtualMachine.objects.get(
|
||||||
|
pk=self.initial.get('virtual_machine') or self.data.get('virtual_machine')
|
||||||
|
)
|
||||||
|
|
||||||
# Add current site to VLANs query params
|
# Add current site to VLANs query params
|
||||||
site = getattr(self.instance.parent, 'site', None)
|
site = virtual_machine.site
|
||||||
if site is not None:
|
if site:
|
||||||
# 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)
|
||||||
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)
|
||||||
|
|
||||||
@ -642,19 +646,13 @@ class InterfaceForm(BootstrapMixin, forms.ModelForm):
|
|||||||
self.cleaned_data['tagged_vlans'] = []
|
self.cleaned_data['tagged_vlans'] = []
|
||||||
|
|
||||||
|
|
||||||
class InterfaceCreateForm(BootstrapMixin, forms.Form):
|
class VMInterfaceCreateForm(BootstrapMixin, forms.Form):
|
||||||
virtual_machine = forms.ModelChoiceField(
|
virtual_machine = DynamicModelChoiceField(
|
||||||
queryset=VirtualMachine.objects.all(),
|
queryset=VirtualMachine.objects.all()
|
||||||
widget=forms.HiddenInput()
|
|
||||||
)
|
)
|
||||||
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
|
||||||
@ -712,16 +710,39 @@ class InterfaceCreateForm(BootstrapMixin, forms.Form):
|
|||||||
pk=self.initial.get('virtual_machine') or self.data.get('virtual_machine')
|
pk=self.initial.get('virtual_machine') or self.data.get('virtual_machine')
|
||||||
)
|
)
|
||||||
|
|
||||||
site = getattr(virtual_machine.cluster, 'site', None)
|
# Add current site to VLANs query params
|
||||||
if site is not None:
|
site = virtual_machine.site
|
||||||
# Add current site to VLANs query params
|
if site:
|
||||||
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)
|
||||||
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 InterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
|
class VMInterfaceCSVForm(CSVModelForm):
|
||||||
|
virtual_machine = CSVModelChoiceField(
|
||||||
|
queryset=VirtualMachine.objects.all(),
|
||||||
|
to_field_name='name'
|
||||||
|
)
|
||||||
|
mode = CSVChoiceField(
|
||||||
|
choices=InterfaceModeChoices,
|
||||||
|
required=False,
|
||||||
|
help_text='IEEE 802.1Q operational mode (for L2 interfaces)'
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = VMInterface
|
||||||
|
fields = VMInterface.csv_headers
|
||||||
|
|
||||||
|
def clean_enabled(self):
|
||||||
|
# Make sure enabled is True when it's not included in the uploaded data
|
||||||
|
if 'enabled' not in self.data:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return self.cleaned_data['enabled']
|
||||||
|
|
||||||
|
|
||||||
|
class VMInterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||||
pk = forms.ModelMultipleChoiceField(
|
pk = forms.ModelMultipleChoiceField(
|
||||||
queryset=Interface.objects.all(),
|
queryset=VMInterface.objects.all(),
|
||||||
widget=forms.MultipleHiddenInput()
|
widget=forms.MultipleHiddenInput()
|
||||||
)
|
)
|
||||||
virtual_machine = forms.ModelChoiceField(
|
virtual_machine = forms.ModelChoiceField(
|
||||||
@ -789,6 +810,24 @@ 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 VMInterfaceBulkRenameForm(BulkRenameForm):
|
||||||
|
pk = forms.ModelMultipleChoiceField(
|
||||||
|
queryset=VMInterface.objects.all(),
|
||||||
|
widget=forms.MultipleHiddenInput()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class VMInterfaceFilterForm(forms.Form):
|
||||||
|
model = VMInterface
|
||||||
|
enabled = forms.NullBooleanField(
|
||||||
|
required=False,
|
||||||
|
widget=StaticSelect2(
|
||||||
|
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||||
|
)
|
||||||
|
)
|
||||||
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Bulk VirtualMachine component creation
|
# Bulk VirtualMachine component creation
|
||||||
#
|
#
|
||||||
@ -808,12 +847,8 @@ class VirtualMachineBulkAddComponentForm(BootstrapMixin, forms.Form):
|
|||||||
return ','.join(self.cleaned_data.get('tags'))
|
return ','.join(self.cleaned_data.get('tags'))
|
||||||
|
|
||||||
|
|
||||||
class InterfaceBulkCreateForm(
|
class VMInterfaceBulkCreateForm(
|
||||||
form_from_model(Interface, ['enabled', 'mtu', 'description', 'tags']),
|
form_from_model(VMInterface, ['enabled', 'mtu', 'description', 'tags']),
|
||||||
VirtualMachineBulkAddComponentForm
|
VirtualMachineBulkAddComponentForm
|
||||||
):
|
):
|
||||||
type = forms.ChoiceField(
|
pass
|
||||||
choices=VMInterfaceTypeChoices,
|
|
||||||
initial=VMInterfaceTypeChoices.TYPE_VIRTUAL,
|
|
||||||
widget=forms.HiddenInput()
|
|
||||||
)
|
|
||||||
|
44
netbox/virtualization/migrations/0015_vminterface.py
Normal file
44
netbox/virtualization/migrations/0015_vminterface.py
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
# 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='VMInterface',
|
||||||
|
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='vminterfaces_as_tagged', to='ipam.VLAN')),
|
||||||
|
('tags', taggit.managers.TaggableManager(related_name='vminterface', through='extras.TaggedItem', to='extras.Tag')),
|
||||||
|
('untagged_vlan', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='vminterfaces_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')},
|
||||||
|
'verbose_name': 'interface',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
@ -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('extras', 'TaggedItem')
|
||||||
|
Interface = apps.get_model('dcim', 'Interface')
|
||||||
|
IPAddress = apps.get_model('ipam', 'IPAddress')
|
||||||
|
VMInterface = apps.get_model('virtualization', 'VMInterface')
|
||||||
|
|
||||||
|
interface_ct = ContentType.objects.get_for_model(Interface)
|
||||||
|
vminterface_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:
|
||||||
|
vminterface = 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,
|
||||||
|
)
|
||||||
|
vminterface.save()
|
||||||
|
|
||||||
|
# Copy tagged VLANs
|
||||||
|
vminterface.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=vminterface_ct, object_id=vminterface.pk
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update any assigned IPAddresses
|
||||||
|
IPAddress.objects.filter(assigned_object_id=interface.pk).update(
|
||||||
|
assigned_object_type=vminterface_ct,
|
||||||
|
assigned_object_id=vminterface.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_vminterface'),
|
||||||
|
]
|
||||||
|
|
||||||
|
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 *
|
||||||
|
|
||||||
|
|
||||||
@ -18,6 +21,7 @@ __all__ = (
|
|||||||
'ClusterGroup',
|
'ClusterGroup',
|
||||||
'ClusterType',
|
'ClusterType',
|
||||||
'VirtualMachine',
|
'VirtualMachine',
|
||||||
|
'VMInterface',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -370,3 +374,111 @@ 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 VMInterface(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='vminterfaces_as_untagged',
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name='Untagged VLAN'
|
||||||
|
)
|
||||||
|
tagged_vlans = models.ManyToManyField(
|
||||||
|
to='ipam.VLAN',
|
||||||
|
related_name='vminterfaces_as_tagged',
|
||||||
|
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='vminterface'
|
||||||
|
)
|
||||||
|
tags = TaggableManager(
|
||||||
|
through=TaggedItem,
|
||||||
|
related_name='vminterface'
|
||||||
|
)
|
||||||
|
|
||||||
|
objects = RestrictedQuerySet.as_manager()
|
||||||
|
|
||||||
|
csv_headers = [
|
||||||
|
'virtual_machine', 'name', 'enabled', 'mac_address', 'mtu', 'description', 'mode',
|
||||||
|
]
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = 'interface'
|
||||||
|
ordering = ('virtual_machine', CollateAsChar('_name'))
|
||||||
|
unique_together = ('virtual_machine', 'name')
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse('virtualization:vminterface', 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.virtual_machine.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, VirtualMachine, VMInterface
|
||||||
|
|
||||||
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">
|
||||||
@ -173,8 +172,12 @@ class VirtualMachineDetailTable(VirtualMachineTable):
|
|||||||
# VM components
|
# VM components
|
||||||
#
|
#
|
||||||
|
|
||||||
class InterfaceTable(BaseTable):
|
class VMInterfaceTable(BaseTable):
|
||||||
|
virtual_machine = tables.LinkColumn()
|
||||||
|
name = tables.Column(
|
||||||
|
linkify=True
|
||||||
|
)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = Interface
|
model = VMInterface
|
||||||
fields = ('name', 'enabled', 'description')
|
fields = ('virtual_machine', 'name', 'enabled', 'mac_address', 'mtu', 'description')
|
||||||
|
@ -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, VirtualMachine, VMInterface
|
||||||
from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine
|
|
||||||
|
|
||||||
|
|
||||||
class AppTest(APITestCase):
|
class AppTest(APITestCase):
|
||||||
@ -196,7 +194,7 @@ class VirtualMachineTest(APIViewTestCases.APIViewTestCase):
|
|||||||
|
|
||||||
|
|
||||||
# TODO: Standardize InterfaceTest (pending #4721)
|
# TODO: Standardize InterfaceTest (pending #4721)
|
||||||
class InterfaceTest(APITestCase):
|
class VMInterfaceTest(APITestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
|
||||||
@ -205,20 +203,17 @@ class InterfaceTest(APITestCase):
|
|||||||
clustertype = ClusterType.objects.create(name='Test Cluster Type 1', slug='test-cluster-type-1')
|
clustertype = ClusterType.objects.create(name='Test Cluster Type 1', slug='test-cluster-type-1')
|
||||||
cluster = Cluster.objects.create(name='Test Cluster 1', type=clustertype)
|
cluster = Cluster.objects.create(name='Test Cluster 1', type=clustertype)
|
||||||
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 = VMInterface.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 = VMInterface.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 = VMInterface.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)
|
||||||
@ -226,22 +221,22 @@ class InterfaceTest(APITestCase):
|
|||||||
self.vlan3 = VLAN.objects.create(name="Test VLAN 3", vid=3)
|
self.vlan3 = VLAN.objects.create(name="Test VLAN 3", vid=3)
|
||||||
|
|
||||||
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:vminterface-detail', kwargs={'pk': self.interface1.pk})
|
||||||
self.add_permissions('dcim.view_interface')
|
self.add_permissions('virtualization.view_vminterface')
|
||||||
|
|
||||||
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:vminterface-list')
|
||||||
self.add_permissions('dcim.view_interface')
|
self.add_permissions('virtualization.view_vminterface')
|
||||||
|
|
||||||
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:vminterface-list')
|
||||||
self.add_permissions('dcim.view_interface')
|
self.add_permissions('virtualization.view_vminterface')
|
||||||
|
|
||||||
response = self.client.get('{}?brief=1'.format(url), **self.header)
|
response = self.client.get('{}?brief=1'.format(url), **self.header)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
@ -254,13 +249,13 @@ class InterfaceTest(APITestCase):
|
|||||||
'virtual_machine': self.virtualmachine.pk,
|
'virtual_machine': self.virtualmachine.pk,
|
||||||
'name': 'Test Interface 4',
|
'name': 'Test Interface 4',
|
||||||
}
|
}
|
||||||
url = reverse('virtualization-api:interface-list')
|
url = reverse('virtualization-api:vminterface-list')
|
||||||
self.add_permissions('dcim.add_interface')
|
self.add_permissions('virtualization.add_vminterface')
|
||||||
|
|
||||||
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)
|
||||||
self.assertEqual(Interface.objects.count(), 4)
|
self.assertEqual(VMInterface.objects.count(), 4)
|
||||||
interface4 = Interface.objects.get(pk=response.data['id'])
|
interface4 = VMInterface.objects.get(pk=response.data['id'])
|
||||||
self.assertEqual(interface4.virtual_machine_id, data['virtual_machine'])
|
self.assertEqual(interface4.virtual_machine_id, data['virtual_machine'])
|
||||||
self.assertEqual(interface4.name, data['name'])
|
self.assertEqual(interface4.name, data['name'])
|
||||||
|
|
||||||
@ -272,12 +267,12 @@ class InterfaceTest(APITestCase):
|
|||||||
'untagged_vlan': self.vlan3.id,
|
'untagged_vlan': self.vlan3.id,
|
||||||
'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:vminterface-list')
|
||||||
self.add_permissions('dcim.add_interface')
|
self.add_permissions('virtualization.add_vminterface')
|
||||||
|
|
||||||
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)
|
||||||
self.assertEqual(Interface.objects.count(), 4)
|
self.assertEqual(VMInterface.objects.count(), 4)
|
||||||
self.assertEqual(response.data['virtual_machine']['id'], data['virtual_machine'])
|
self.assertEqual(response.data['virtual_machine']['id'], data['virtual_machine'])
|
||||||
self.assertEqual(response.data['name'], data['name'])
|
self.assertEqual(response.data['name'], data['name'])
|
||||||
self.assertEqual(response.data['untagged_vlan']['id'], data['untagged_vlan'])
|
self.assertEqual(response.data['untagged_vlan']['id'], data['untagged_vlan'])
|
||||||
@ -298,12 +293,12 @@ class InterfaceTest(APITestCase):
|
|||||||
'name': 'Test Interface 6',
|
'name': 'Test Interface 6',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
url = reverse('virtualization-api:interface-list')
|
url = reverse('virtualization-api:vminterface-list')
|
||||||
self.add_permissions('dcim.add_interface')
|
self.add_permissions('virtualization.add_vminterface')
|
||||||
|
|
||||||
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)
|
||||||
self.assertEqual(Interface.objects.count(), 6)
|
self.assertEqual(VMInterface.objects.count(), 6)
|
||||||
self.assertEqual(response.data[0]['name'], data[0]['name'])
|
self.assertEqual(response.data[0]['name'], data[0]['name'])
|
||||||
self.assertEqual(response.data[1]['name'], data[1]['name'])
|
self.assertEqual(response.data[1]['name'], data[1]['name'])
|
||||||
self.assertEqual(response.data[2]['name'], data[2]['name'])
|
self.assertEqual(response.data[2]['name'], data[2]['name'])
|
||||||
@ -332,12 +327,12 @@ class InterfaceTest(APITestCase):
|
|||||||
'tagged_vlans': [self.vlan1.id],
|
'tagged_vlans': [self.vlan1.id],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
url = reverse('virtualization-api:interface-list')
|
url = reverse('virtualization-api:vminterface-list')
|
||||||
self.add_permissions('dcim.add_interface')
|
self.add_permissions('virtualization.add_vminterface')
|
||||||
|
|
||||||
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)
|
||||||
self.assertEqual(Interface.objects.count(), 6)
|
self.assertEqual(VMInterface.objects.count(), 6)
|
||||||
for i in range(0, 3):
|
for i in range(0, 3):
|
||||||
self.assertEqual(response.data[i]['name'], data[i]['name'])
|
self.assertEqual(response.data[i]['name'], data[i]['name'])
|
||||||
self.assertEqual([v['id'] for v in response.data[i]['tagged_vlans']], data[i]['tagged_vlans'])
|
self.assertEqual([v['id'] for v in response.data[i]['tagged_vlans']], data[i]['tagged_vlans'])
|
||||||
@ -348,19 +343,19 @@ class InterfaceTest(APITestCase):
|
|||||||
'virtual_machine': self.virtualmachine.pk,
|
'virtual_machine': self.virtualmachine.pk,
|
||||||
'name': 'Test Interface X',
|
'name': 'Test Interface X',
|
||||||
}
|
}
|
||||||
url = reverse('virtualization-api:interface-detail', kwargs={'pk': self.interface1.pk})
|
url = reverse('virtualization-api:vminterface-detail', kwargs={'pk': self.interface1.pk})
|
||||||
self.add_permissions('dcim.change_interface')
|
self.add_permissions('virtualization.change_vminterface')
|
||||||
|
|
||||||
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)
|
||||||
self.assertEqual(Interface.objects.count(), 3)
|
self.assertEqual(VMInterface.objects.count(), 3)
|
||||||
interface1 = Interface.objects.get(pk=response.data['id'])
|
interface1 = VMInterface.objects.get(pk=response.data['id'])
|
||||||
self.assertEqual(interface1.name, data['name'])
|
self.assertEqual(interface1.name, data['name'])
|
||||||
|
|
||||||
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:vminterface-detail', kwargs={'pk': self.interface1.pk})
|
||||||
self.add_permissions('dcim.delete_interface')
|
self.add_permissions('virtualization.delete_vminterface')
|
||||||
|
|
||||||
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)
|
||||||
self.assertEqual(Interface.objects.count(), 2)
|
self.assertEqual(VMInterface.objects.count(), 2)
|
||||||
|
@ -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, VirtualMachine, VMInterface
|
||||||
|
|
||||||
|
|
||||||
class ClusterTypeTestCase(TestCase):
|
class ClusterTypeTestCase(TestCase):
|
||||||
@ -260,11 +260,11 @@ class VirtualMachineTestCase(TestCase):
|
|||||||
VirtualMachine.objects.bulk_create(vms)
|
VirtualMachine.objects.bulk_create(vms)
|
||||||
|
|
||||||
interfaces = (
|
interfaces = (
|
||||||
Interface(virtual_machine=vms[0], name='Interface 1', mac_address='00-00-00-00-00-01'),
|
VMInterface(virtual_machine=vms[0], name='Interface 1', mac_address='00-00-00-00-00-01'),
|
||||||
Interface(virtual_machine=vms[1], name='Interface 2', mac_address='00-00-00-00-00-02'),
|
VMInterface(virtual_machine=vms[1], name='Interface 2', mac_address='00-00-00-00-00-02'),
|
||||||
Interface(virtual_machine=vms[2], name='Interface 3', mac_address='00-00-00-00-00-03'),
|
VMInterface(virtual_machine=vms[2], name='Interface 3', mac_address='00-00-00-00-00-03'),
|
||||||
)
|
)
|
||||||
Interface.objects.bulk_create(interfaces)
|
VMInterface.objects.bulk_create(interfaces)
|
||||||
|
|
||||||
def test_id(self):
|
def test_id(self):
|
||||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||||
@ -365,9 +365,9 @@ class VirtualMachineTestCase(TestCase):
|
|||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
|
||||||
class InterfaceTestCase(TestCase):
|
class VMInterfaceTestCase(TestCase):
|
||||||
queryset = Interface.objects.all()
|
queryset = VMInterface.objects.all()
|
||||||
filterset = InterfaceFilterSet
|
filterset = VMInterfaceFilterSet
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
@ -394,11 +394,11 @@ class InterfaceTestCase(TestCase):
|
|||||||
VirtualMachine.objects.bulk_create(vms)
|
VirtualMachine.objects.bulk_create(vms)
|
||||||
|
|
||||||
interfaces = (
|
interfaces = (
|
||||||
Interface(virtual_machine=vms[0], name='Interface 1', enabled=True, mtu=100, mac_address='00-00-00-00-00-01'),
|
VMInterface(virtual_machine=vms[0], name='Interface 1', enabled=True, mtu=100, mac_address='00-00-00-00-00-01'),
|
||||||
Interface(virtual_machine=vms[1], name='Interface 2', enabled=True, mtu=200, mac_address='00-00-00-00-00-02'),
|
VMInterface(virtual_machine=vms[1], name='Interface 2', enabled=True, mtu=200, mac_address='00-00-00-00-00-02'),
|
||||||
Interface(virtual_machine=vms[2], name='Interface 3', enabled=False, mtu=300, mac_address='00-00-00-00-00-03'),
|
VMInterface(virtual_machine=vms[2], name='Interface 3', enabled=False, mtu=300, mac_address='00-00-00-00-00-03'),
|
||||||
)
|
)
|
||||||
Interface.objects.bulk_create(interfaces)
|
VMInterface.objects.bulk_create(interfaces)
|
||||||
|
|
||||||
def test_id(self):
|
def test_id(self):
|
||||||
id_list = self.queryset.values_list('id', flat=True)[:2]
|
id_list = self.queryset.values_list('id', flat=True)[:2]
|
||||||
|
@ -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, VirtualMachine, VMInterface
|
||||||
|
|
||||||
|
|
||||||
class ClusterGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
class ClusterGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
||||||
@ -189,21 +189,11 @@ class VirtualMachineTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# TODO: Update base class to DeviceComponentViewTestCase
|
class VMInterfaceTestCase(
|
||||||
# Blocked by #4721
|
|
||||||
class InterfaceTestCase(
|
|
||||||
ViewTestCases.GetObjectViewTestCase,
|
ViewTestCases.GetObjectViewTestCase,
|
||||||
ViewTestCases.EditObjectViewTestCase,
|
ViewTestCases.DeviceComponentViewTestCase,
|
||||||
ViewTestCases.DeleteObjectViewTestCase,
|
|
||||||
ViewTestCases.BulkCreateObjectsViewTestCase,
|
|
||||||
ViewTestCases.BulkEditObjectsViewTestCase,
|
|
||||||
ViewTestCases.BulkDeleteObjectsViewTestCase,
|
|
||||||
):
|
):
|
||||||
model = Interface
|
model = VMInterface
|
||||||
|
|
||||||
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):
|
||||||
@ -218,10 +208,10 @@ class InterfaceTestCase(
|
|||||||
)
|
)
|
||||||
VirtualMachine.objects.bulk_create(virtualmachines)
|
VirtualMachine.objects.bulk_create(virtualmachines)
|
||||||
|
|
||||||
Interface.objects.bulk_create([
|
VMInterface.objects.bulk_create([
|
||||||
Interface(virtual_machine=virtualmachines[0], name='Interface 1', type=InterfaceTypeChoices.TYPE_VIRTUAL),
|
VMInterface(virtual_machine=virtualmachines[0], name='Interface 1'),
|
||||||
Interface(virtual_machine=virtualmachines[0], name='Interface 2', type=InterfaceTypeChoices.TYPE_VIRTUAL),
|
VMInterface(virtual_machine=virtualmachines[0], name='Interface 2'),
|
||||||
Interface(virtual_machine=virtualmachines[0], name='Interface 3', type=InterfaceTypeChoices.TYPE_VIRTUAL),
|
VMInterface(virtual_machine=virtualmachines[0], name='Interface 3'),
|
||||||
])
|
])
|
||||||
|
|
||||||
vlans = (
|
vlans = (
|
||||||
@ -237,9 +227,7 @@ 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,
|
|
||||||
'mac_address': EUI('01-02-03-04-05-06'),
|
'mac_address': EUI('01-02-03-04-05-06'),
|
||||||
'mtu': 2000,
|
'mtu': 2000,
|
||||||
'description': 'New description',
|
'description': 'New description',
|
||||||
@ -252,9 +240,7 @@ 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,
|
|
||||||
'mac_address': EUI('01-02-03-04-05-06'),
|
'mac_address': EUI('01-02-03-04-05-06'),
|
||||||
'mtu': 2000,
|
'mtu': 2000,
|
||||||
'description': 'New description',
|
'description': 'New description',
|
||||||
@ -264,19 +250,19 @@ class InterfaceTestCase(
|
|||||||
'tags': [t.pk for t in tags],
|
'tags': [t.pk for t in tags],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cls.csv_data = (
|
||||||
|
"virtual_machine,name",
|
||||||
|
"Virtual Machine 2,Interface 4",
|
||||||
|
"Virtual Machine 2,Interface 5",
|
||||||
|
"Virtual Machine 2,Interface 6",
|
||||||
|
)
|
||||||
|
|
||||||
cls.bulk_edit_data = {
|
cls.bulk_edit_data = {
|
||||||
'virtual_machine': virtualmachines[1].pk,
|
'virtual_machine': virtualmachines[1].pk,
|
||||||
'enabled': False,
|
'enabled': False,
|
||||||
'mtu': 2000,
|
'mtu': 2000,
|
||||||
'description': 'New description',
|
'description': 'New description',
|
||||||
'mode': InterfaceModeChoices.MODE_TAGGED,
|
'mode': InterfaceModeChoices.MODE_TAGGED,
|
||||||
# 'untagged_vlan': vlans[0].pk,
|
'untagged_vlan': vlans[0].pk,
|
||||||
# 'tagged_vlans': [v.pk for v in vlans[1:4]],
|
'tagged_vlans': [v.pk for v in vlans[1:4]],
|
||||||
}
|
}
|
||||||
|
|
||||||
cls.csv_data = (
|
|
||||||
"device,name,type",
|
|
||||||
"Device 1,Interface 4,1000BASE-T (1GE)",
|
|
||||||
"Device 1,Interface 5,1000BASE-T (1GE)",
|
|
||||||
"Device 1,Interface 6,1000BASE-T (1GE)",
|
|
||||||
)
|
|
||||||
|
@ -3,7 +3,7 @@ from django.urls import path
|
|||||||
from extras.views import ObjectChangeLogView
|
from extras.views import ObjectChangeLogView
|
||||||
from ipam.views import ServiceEditView
|
from ipam.views import ServiceEditView
|
||||||
from . import views
|
from . import views
|
||||||
from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine
|
from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
|
||||||
|
|
||||||
app_name = 'virtualization'
|
app_name = 'virtualization'
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
@ -51,11 +51,16 @@ urlpatterns = [
|
|||||||
path('virtual-machines/<int:virtualmachine>/services/assign/', ServiceEditView.as_view(), name='virtualmachine_service_assign'),
|
path('virtual-machines/<int:virtualmachine>/services/assign/', ServiceEditView.as_view(), name='virtualmachine_service_assign'),
|
||||||
|
|
||||||
# VM interfaces
|
# VM interfaces
|
||||||
path('interfaces/add/', views.InterfaceCreateView.as_view(), name='interface_add'),
|
path('interfaces/', views.VMInterfaceListView.as_view(), name='vminterface_list'),
|
||||||
path('interfaces/edit/', views.InterfaceBulkEditView.as_view(), name='interface_bulk_edit'),
|
path('interfaces/add/', views.VMInterfaceCreateView.as_view(), name='vminterface_add'),
|
||||||
path('interfaces/delete/', views.InterfaceBulkDeleteView.as_view(), name='interface_bulk_delete'),
|
path('interfaces/import/', views.VMInterfaceBulkImportView.as_view(), name='vminterface_import'),
|
||||||
path('interfaces/<int:pk>/edit/', views.InterfaceEditView.as_view(), name='interface_edit'),
|
path('interfaces/edit/', views.VMInterfaceBulkEditView.as_view(), name='vminterface_bulk_edit'),
|
||||||
path('interfaces/<int:pk>/delete/', views.InterfaceDeleteView.as_view(), name='interface_delete'),
|
path('interfaces/rename/', views.VMInterfaceBulkRenameView.as_view(), name='vminterface_bulk_rename'),
|
||||||
path('virtual-machines/interfaces/add/', views.VirtualMachineBulkAddInterfaceView.as_view(), name='virtualmachine_bulk_add_interface'),
|
path('interfaces/delete/', views.VMInterfaceBulkDeleteView.as_view(), name='vminterface_bulk_delete'),
|
||||||
|
path('interfaces/<int:pk>/', views.VMInterfaceView.as_view(), name='vminterface'),
|
||||||
|
path('interfaces/<int:pk>/edit/', views.VMInterfaceEditView.as_view(), name='vminterface_edit'),
|
||||||
|
path('interfaces/<int:pk>/delete/', views.VMInterfaceDeleteView.as_view(), name='vminterface_delete'),
|
||||||
|
path('interfaces/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='vminterface_changelog', kwargs={'model': VMInterface}),
|
||||||
|
path('virtual-machines/interfaces/add/', views.VirtualMachineBulkAddInterfaceView.as_view(), name='virtualmachine_bulk_add_vminterface'),
|
||||||
|
|
||||||
]
|
]
|
||||||
|
@ -4,16 +4,17 @@ 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.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
|
||||||
|
from ipam.tables import InterfaceIPAddressTable, InterfaceVLANTable
|
||||||
from utilities.views import (
|
from utilities.views import (
|
||||||
BulkComponentCreateView, BulkDeleteView, BulkEditView, BulkImportView, ComponentCreateView, ObjectView,
|
BulkComponentCreateView, BulkDeleteView, BulkEditView, BulkImportView, BulkRenameView, ComponentCreateView,
|
||||||
ObjectDeleteView, ObjectEditView, ObjectListView,
|
ObjectView, 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, VirtualMachine, VMInterface
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -235,7 +236,7 @@ class VirtualMachineView(ObjectView):
|
|||||||
def get(self, request, pk):
|
def get(self, request, pk):
|
||||||
|
|
||||||
virtualmachine = get_object_or_404(self.queryset, pk=pk)
|
virtualmachine = get_object_or_404(self.queryset, pk=pk)
|
||||||
interfaces = Interface.objects.restrict(request.user, 'view').filter(virtual_machine=virtualmachine)
|
interfaces = VMInterface.objects.restrict(request.user, 'view').filter(virtual_machine=virtualmachine)
|
||||||
services = Service.objects.restrict(request.user, 'view').filter(virtual_machine=virtualmachine)
|
services = Service.objects.restrict(request.user, 'view').filter(virtual_machine=virtualmachine)
|
||||||
|
|
||||||
return render(request, 'virtualization/virtualmachine.html', {
|
return render(request, 'virtualization/virtualmachine.html', {
|
||||||
@ -288,32 +289,87 @@ class VirtualMachineBulkDeleteView(BulkDeleteView):
|
|||||||
# VM interfaces
|
# VM interfaces
|
||||||
#
|
#
|
||||||
|
|
||||||
class InterfaceCreateView(ComponentCreateView):
|
class VMInterfaceListView(ObjectListView):
|
||||||
queryset = Interface.objects.all()
|
queryset = VMInterface.objects.prefetch_related('virtual_machine')
|
||||||
form = forms.InterfaceCreateForm
|
filterset = filters.VMInterfaceFilterSet
|
||||||
model_form = forms.InterfaceForm
|
filterset_form = forms.VMInterfaceFilterForm
|
||||||
|
table = tables.VMInterfaceTable
|
||||||
|
action_buttons = ('export',)
|
||||||
|
|
||||||
|
|
||||||
|
class VMInterfaceView(ObjectView):
|
||||||
|
queryset = VMInterface.objects.all()
|
||||||
|
|
||||||
|
def get(self, request, pk):
|
||||||
|
|
||||||
|
vminterface = get_object_or_404(self.queryset, pk=pk)
|
||||||
|
|
||||||
|
# Get assigned IP addresses
|
||||||
|
ipaddress_table = InterfaceIPAddressTable(
|
||||||
|
data=vminterface.ip_addresses.restrict(request.user, 'view').prefetch_related('vrf', 'tenant'),
|
||||||
|
orderable=False
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get assigned VLANs and annotate whether each is tagged or untagged
|
||||||
|
vlans = []
|
||||||
|
if vminterface.untagged_vlan is not None:
|
||||||
|
vlans.append(vminterface.untagged_vlan)
|
||||||
|
vlans[0].tagged = False
|
||||||
|
for vlan in vminterface.tagged_vlans.prefetch_related('site', 'group', 'tenant', 'role'):
|
||||||
|
vlan.tagged = True
|
||||||
|
vlans.append(vlan)
|
||||||
|
vlan_table = InterfaceVLANTable(
|
||||||
|
interface=vminterface,
|
||||||
|
data=vlans,
|
||||||
|
orderable=False
|
||||||
|
)
|
||||||
|
|
||||||
|
return render(request, 'virtualization/vminterface.html', {
|
||||||
|
'vminterface': vminterface,
|
||||||
|
'ipaddress_table': ipaddress_table,
|
||||||
|
'vlan_table': vlan_table,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: This should not use ComponentCreateView
|
||||||
|
class VMInterfaceCreateView(ComponentCreateView):
|
||||||
|
queryset = VMInterface.objects.all()
|
||||||
|
form = forms.VMInterfaceCreateForm
|
||||||
|
model_form = forms.VMInterfaceForm
|
||||||
template_name = 'virtualization/virtualmachine_component_add.html'
|
template_name = 'virtualization/virtualmachine_component_add.html'
|
||||||
|
|
||||||
|
|
||||||
class InterfaceEditView(ObjectEditView):
|
class VMInterfaceEditView(ObjectEditView):
|
||||||
queryset = Interface.objects.all()
|
queryset = VMInterface.objects.all()
|
||||||
model_form = forms.InterfaceForm
|
model_form = forms.VMInterfaceForm
|
||||||
template_name = 'virtualization/interface_edit.html'
|
template_name = 'virtualization/vminterface_edit.html'
|
||||||
|
|
||||||
|
|
||||||
class InterfaceDeleteView(ObjectDeleteView):
|
class VMInterfaceDeleteView(ObjectDeleteView):
|
||||||
queryset = Interface.objects.all()
|
queryset = VMInterface.objects.all()
|
||||||
|
|
||||||
|
|
||||||
class InterfaceBulkEditView(BulkEditView):
|
class VMInterfaceBulkImportView(BulkImportView):
|
||||||
queryset = Interface.objects.all()
|
queryset = VMInterface.objects.all()
|
||||||
table = tables.InterfaceTable
|
model_form = forms.VMInterfaceCSVForm
|
||||||
form = forms.InterfaceBulkEditForm
|
table = tables.VMInterfaceTable
|
||||||
|
default_return_url = 'virtualization:vminterface_list'
|
||||||
|
|
||||||
|
|
||||||
class InterfaceBulkDeleteView(BulkDeleteView):
|
class VMInterfaceBulkEditView(BulkEditView):
|
||||||
queryset = Interface.objects.all()
|
queryset = VMInterface.objects.all()
|
||||||
table = tables.InterfaceTable
|
table = tables.VMInterfaceTable
|
||||||
|
form = forms.VMInterfaceBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
|
class VMInterfaceBulkRenameView(BulkRenameView):
|
||||||
|
queryset = VMInterface.objects.all()
|
||||||
|
form = forms.VMInterfaceBulkRenameForm
|
||||||
|
|
||||||
|
|
||||||
|
class VMInterfaceBulkDeleteView(BulkDeleteView):
|
||||||
|
queryset = VMInterface.objects.all()
|
||||||
|
table = tables.VMInterfaceTable
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -323,9 +379,9 @@ class InterfaceBulkDeleteView(BulkDeleteView):
|
|||||||
class VirtualMachineBulkAddInterfaceView(BulkComponentCreateView):
|
class VirtualMachineBulkAddInterfaceView(BulkComponentCreateView):
|
||||||
parent_model = VirtualMachine
|
parent_model = VirtualMachine
|
||||||
parent_field = 'virtual_machine'
|
parent_field = 'virtual_machine'
|
||||||
form = forms.InterfaceBulkCreateForm
|
form = forms.VMInterfaceBulkCreateForm
|
||||||
queryset = Interface.objects.all()
|
queryset = VMInterface.objects.all()
|
||||||
model_form = forms.InterfaceForm
|
model_form = forms.VMInterfaceForm
|
||||||
filterset = filters.VirtualMachineFilterSet
|
filterset = filters.VirtualMachineFilterSet
|
||||||
table = tables.VirtualMachineTable
|
table = tables.VirtualMachineTable
|
||||||
default_return_url = 'virtualization:virtualmachine_list'
|
default_return_url = 'virtualization:virtualmachine_list'
|
||||||
|
Loading…
Reference in New Issue
Block a user