mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-29 11:56:25 -06:00
All views/filtering working and documentation done; no unit tests yet
This commit is contained in:
parent
eed926ca73
commit
b8572dc33f
@ -10,6 +10,9 @@ Interfaces in NetBox represent network interfaces used to exchange data with con
|
||||
|
||||
## Fields
|
||||
|
||||
!!! note
|
||||
The MAC address of an interface (formerly a concrete database field) is available as a property, `mac_address`, which reflects the value of the primary linked [MAC address](./macaddress.md) object.
|
||||
|
||||
### Device
|
||||
|
||||
The device to which this interface belongs.
|
||||
@ -45,10 +48,6 @@ The operation duplex (full, half, or auto).
|
||||
|
||||
The [virtual routing and forwarding](../ipam/vrf.md) instance to which this interface is assigned.
|
||||
|
||||
### MAC Address
|
||||
|
||||
The 48-bit MAC address (for Ethernet interfaces).
|
||||
|
||||
### WWN
|
||||
|
||||
The 64-bit world-wide name (for Fibre Channel interfaces).
|
||||
|
11
docs/models/dcim/macaddress.md
Normal file
11
docs/models/dcim/macaddress.md
Normal file
@ -0,0 +1,11 @@
|
||||
# MAC Addresses
|
||||
|
||||
A MAC address object in NetBox comprises a single physical (hardware) address, and represents a MAC address as reported by or assigned to a network interface. MAC addresses can be assigned to [device](../dcim/device.md) and [virtual machine](../virtualization/virtualmachine.md) interfaces. A MAC address can be specified as the "primary" MAC address for a given interface or VM interface.
|
||||
|
||||
Most interfaces only have a single MAC address, hard-coded at the factory. However, on some devices (particularly virtual interfaces) it is possible to assign additional MAC addresses or change existing ones. For this reason NetBox allows multiple MACAddress objects to be assigned to a single interface. However, for convenience and backward compatibiility reasons, the value of the `mac_address` field of the primary (or single) MAC address on an interface is reflected as a simple property in the interface detail page.
|
||||
|
||||
## Fields
|
||||
|
||||
### MAC Address
|
||||
|
||||
The 48-bit MAC address, in colon-hexadecimal notation (e.g. `aa:bb:cc:11:22:33`).
|
@ -4,6 +4,9 @@
|
||||
|
||||
## Fields
|
||||
|
||||
!!! note
|
||||
The MAC address of an interface (formerly a concrete database field) is available as a property, `mac_address`, which reflects the value of the primary linked [MAC address](./macaddress.md) object.
|
||||
|
||||
### Virtual Machine
|
||||
|
||||
The [virtual machine](./virtualmachine.md) to which this interface is assigned.
|
||||
@ -27,10 +30,6 @@ An interface on the same VM with which this interface is bridged.
|
||||
|
||||
If not selected, this interface will be treated as disabled/inoperative.
|
||||
|
||||
### MAC Address
|
||||
|
||||
The 48-bit MAC address (for Ethernet interfaces).
|
||||
|
||||
### MTU
|
||||
|
||||
The interface's configured maximum transmissible unit (MTU).
|
||||
|
@ -6,7 +6,7 @@ from dcim.choices import *
|
||||
from dcim.constants import *
|
||||
from dcim.models import (
|
||||
ConsolePort, ConsoleServerPort, DeviceBay, FrontPort, Interface, InventoryItem, ModuleBay, PowerOutlet, PowerPort,
|
||||
RearPort, VirtualDeviceContext,
|
||||
RearPort, VirtualDeviceContext, MACAddress,
|
||||
)
|
||||
from ipam.api.serializers_.vlans import VLANSerializer
|
||||
from ipam.api.serializers_.vrfs import VRFSerializer
|
||||
@ -33,6 +33,7 @@ __all__ = (
|
||||
'FrontPortSerializer',
|
||||
'InterfaceSerializer',
|
||||
'InventoryItemSerializer',
|
||||
'MACAddressSerializer',
|
||||
'ModuleBaySerializer',
|
||||
'PowerOutletSerializer',
|
||||
'PowerPortSerializer',
|
||||
@ -363,3 +364,11 @@ class InventoryItemSerializer(NetBoxModelSerializer):
|
||||
serializer = get_serializer_for_model(obj.component)
|
||||
context = {'request': self.context['request']}
|
||||
return serializer(obj.component, nested=True, context=context).data
|
||||
|
||||
|
||||
class MACAddressSerializer(NetBoxModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = MACAddress
|
||||
fields = ['mac_address',]
|
||||
brief_fields = ('mac_address',)
|
||||
|
@ -123,3 +123,13 @@ COMPATIBLE_TERMINATION_TYPES = {
|
||||
'powerport': ['poweroutlet', 'powerfeed'],
|
||||
'rearport': ['consoleport', 'consoleserverport', 'interface', 'frontport', 'rearport', 'circuittermination'],
|
||||
}
|
||||
|
||||
|
||||
#
|
||||
# MAC addresses
|
||||
#
|
||||
|
||||
MACADDRESS_ASSIGNMENT_MODELS = Q(
|
||||
Q(app_label='dcim', model='interface') |
|
||||
Q(app_label='virtualization', model='vminterface')
|
||||
)
|
||||
|
@ -20,7 +20,7 @@ from utilities.filters import (
|
||||
ContentTypeFilter, MultiValueCharFilter, MultiValueMACAddressFilter, MultiValueNumberFilter, MultiValueWWNFilter,
|
||||
NumericArrayFilter, TreeNodeMultipleChoiceFilter,
|
||||
)
|
||||
from virtualization.models import Cluster, ClusterGroup
|
||||
from virtualization.models import Cluster, ClusterGroup, VMInterface
|
||||
from vpn.models import L2VPN
|
||||
from wireless.choices import WirelessRoleChoices, WirelessChannelChoices
|
||||
from wireless.models import WirelessLAN, WirelessLink
|
||||
@ -52,6 +52,7 @@ __all__ = (
|
||||
'InventoryItemRoleFilterSet',
|
||||
'InventoryItemTemplateFilterSet',
|
||||
'LocationFilterSet',
|
||||
'MACAddressFilterSet',
|
||||
'ManufacturerFilterSet',
|
||||
'ModuleBayFilterSet',
|
||||
'ModuleBayTemplateFilterSet',
|
||||
@ -1098,10 +1099,10 @@ class DeviceFilterSet(
|
||||
field_name='device_type__is_full_depth',
|
||||
label=_('Is full depth'),
|
||||
)
|
||||
mac_address = MultiValueMACAddressFilter(
|
||||
field_name='interfaces___mac_address',
|
||||
label=_('MAC address'),
|
||||
)
|
||||
# mac_address = MultiValueMACAddressFilter(
|
||||
# field_name='interfaces___mac_address',
|
||||
# label=_('MAC address'),
|
||||
# )
|
||||
serial = MultiValueCharFilter(
|
||||
lookup_expr='iexact'
|
||||
)
|
||||
@ -1598,6 +1599,154 @@ class PowerOutletFilterSet(
|
||||
)
|
||||
|
||||
|
||||
class MACAddressFilterSet(NetBoxModelFilterSet):
|
||||
mac_address = MultiValueMACAddressFilter()
|
||||
interface = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='interface__name',
|
||||
queryset=Interface.objects.all(),
|
||||
to_field_name='name',
|
||||
label=_('Interface (name)'),
|
||||
)
|
||||
interface_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='interface',
|
||||
queryset=Interface.objects.all(),
|
||||
label=_('Interface (ID)'),
|
||||
)
|
||||
vminterface = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='vminterface__name',
|
||||
queryset=VMInterface.objects.all(),
|
||||
to_field_name='name',
|
||||
label=_('VM interface (name)'),
|
||||
)
|
||||
vminterface_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='vminterface',
|
||||
queryset=VMInterface.objects.all(),
|
||||
label=_('VM interface (ID)'),
|
||||
)
|
||||
|
||||
# assigned_to_interface = django_filters.BooleanFilter(
|
||||
# method='_assigned_to_interface',
|
||||
# label=_('Is assigned to an interface'),
|
||||
# )
|
||||
# assigned = django_filters.BooleanFilter(
|
||||
# method='_assigned',
|
||||
# label=_('Is assigned'),
|
||||
# )
|
||||
|
||||
class Meta:
|
||||
model = MACAddress
|
||||
fields = ('id', 'description', 'interface', 'assigned_object_type', 'assigned_object_id')
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
qs_filter = (
|
||||
Q(mac_address__icontains=value) |
|
||||
Q(description__icontains=value)
|
||||
)
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
def search_by_parent(self, queryset, name, value):
|
||||
if not value:
|
||||
return queryset
|
||||
q = Q()
|
||||
for prefix in value:
|
||||
try:
|
||||
query = str(netaddr.IPNetwork(prefix.strip()).cidr)
|
||||
q |= Q(address__net_host_contained=query)
|
||||
except (AddrFormatError, ValueError):
|
||||
return queryset.none()
|
||||
return queryset.filter(q)
|
||||
|
||||
def parse_inet_addresses(self, value):
|
||||
'''
|
||||
Parse networks or IP addresses and cast to a format
|
||||
acceptable by the Postgres inet type.
|
||||
|
||||
Skips invalid values.
|
||||
'''
|
||||
parsed = []
|
||||
for addr in value:
|
||||
if netaddr.valid_ipv4(addr) or netaddr.valid_ipv6(addr):
|
||||
parsed.append(addr)
|
||||
continue
|
||||
try:
|
||||
network = netaddr.IPNetwork(addr)
|
||||
parsed.append(str(network))
|
||||
except (AddrFormatError, ValueError):
|
||||
continue
|
||||
return parsed
|
||||
|
||||
def filter_address(self, queryset, name, value):
|
||||
# Let's first parse the addresses passed
|
||||
# as argument. If they are all invalid,
|
||||
# we return an empty queryset
|
||||
value = self.parse_inet_addresses(value)
|
||||
if (len(value) == 0):
|
||||
return queryset.none()
|
||||
|
||||
try:
|
||||
return queryset.filter(address__net_in=value)
|
||||
except ValidationError:
|
||||
return queryset.none()
|
||||
|
||||
@extend_schema_field(OpenApiTypes.STR)
|
||||
def filter_present_in_vrf(self, queryset, name, vrf):
|
||||
if vrf is None:
|
||||
return queryset.none
|
||||
return queryset.filter(
|
||||
Q(vrf=vrf) |
|
||||
Q(vrf__export_targets__in=vrf.import_targets.all())
|
||||
).distinct()
|
||||
|
||||
def filter_device(self, queryset, name, value):
|
||||
devices = Device.objects.filter(**{'{}__in'.format(name): value})
|
||||
if not devices.exists():
|
||||
return queryset.none()
|
||||
interface_ids = []
|
||||
for device in devices:
|
||||
interface_ids.extend(device.vc_interfaces().values_list('id', flat=True))
|
||||
return queryset.filter(
|
||||
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):
|
||||
content_types = ContentType.objects.get_for_models(Interface, VMInterface).values()
|
||||
if value:
|
||||
return queryset.filter(
|
||||
assigned_object_type__in=content_types,
|
||||
assigned_object_id__isnull=False
|
||||
)
|
||||
else:
|
||||
return queryset.exclude(
|
||||
assigned_object_type__in=content_types,
|
||||
assigned_object_id__isnull=False
|
||||
)
|
||||
|
||||
def _assigned(self, queryset, name, value):
|
||||
if value:
|
||||
return queryset.exclude(
|
||||
assigned_object_type__isnull=True,
|
||||
assigned_object_id__isnull=True
|
||||
)
|
||||
else:
|
||||
return queryset.filter(
|
||||
assigned_object_type__isnull=True,
|
||||
assigned_object_id__isnull=True
|
||||
)
|
||||
|
||||
|
||||
class CommonInterfaceFilterSet(django_filters.FilterSet):
|
||||
vlan_id = django_filters.CharFilter(
|
||||
method='filter_vlan_id',
|
||||
|
@ -2,6 +2,7 @@ from django import forms
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from dcim.models import *
|
||||
from dcim.fields import MACAddressField
|
||||
from extras.models import Tag
|
||||
from netbox.forms.mixins import CustomFieldsMixin
|
||||
from utilities.forms import form_from_model
|
||||
@ -15,6 +16,7 @@ __all__ = (
|
||||
# 'FrontPortBulkCreateForm',
|
||||
'InterfaceBulkCreateForm',
|
||||
'InventoryItemBulkCreateForm',
|
||||
'MACAddressBulkCreateForm',
|
||||
'ModuleBayBulkCreateForm',
|
||||
'PowerOutletBulkCreateForm',
|
||||
'PowerPortBulkCreateForm',
|
||||
@ -76,6 +78,12 @@ class PowerOutletBulkCreateForm(
|
||||
field_order = ('name', 'label', 'type', 'feed_leg', 'description', 'tags')
|
||||
|
||||
|
||||
class MACAddressBulkCreateForm(forms.Form):
|
||||
pattern = MACAddressField(
|
||||
# label=_('Address pattern')
|
||||
)
|
||||
|
||||
|
||||
class InterfaceBulkCreateForm(
|
||||
form_from_model(Interface, [
|
||||
'type', 'enabled', 'speed', 'duplex', 'mtu', 'mgmt_only', 'mark_connected', 'poe_mode', 'poe_type', 'rf_role'
|
||||
|
@ -38,6 +38,7 @@ __all__ = (
|
||||
'InventoryItemRoleBulkEditForm',
|
||||
'InventoryItemTemplateBulkEditForm',
|
||||
'LocationBulkEditForm',
|
||||
'MACAddressBulkEditForm',
|
||||
'ManufacturerBulkEditForm',
|
||||
'ModuleBulkEditForm',
|
||||
'ModuleBayBulkEditForm',
|
||||
@ -1389,10 +1390,28 @@ class PowerOutletBulkEditForm(
|
||||
self.fields['power_port'].widget.attrs['disabled'] = True
|
||||
|
||||
|
||||
class MACAddressBulkEditForm(NetBoxModelBulkEditForm):
|
||||
description = forms.CharField(
|
||||
label=_('Description'),
|
||||
max_length=200,
|
||||
required=False
|
||||
)
|
||||
comments = CommentField()
|
||||
|
||||
model = MACAddress
|
||||
fieldsets = (
|
||||
FieldSet('description'),
|
||||
# FieldSet('vrf', 'mask_length', 'dns_name', name=_('Addressing')),
|
||||
)
|
||||
nullable_fields = (
|
||||
'description', 'comments',
|
||||
)
|
||||
|
||||
|
||||
class InterfaceBulkEditForm(
|
||||
ComponentBulkEditForm,
|
||||
form_from_model(Interface, [
|
||||
'label', 'type', 'parent', 'bridge', 'lag', 'speed', 'duplex', '_mac_address', 'wwn', 'mtu', 'mgmt_only',
|
||||
'label', 'type', 'parent', 'bridge', 'lag', 'speed', 'duplex', 'wwn', 'mtu', 'mgmt_only',
|
||||
'mark_connected', 'description', 'mode', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width',
|
||||
'tx_power', 'wireless_lans'
|
||||
])
|
||||
|
@ -17,7 +17,7 @@ from utilities.forms.fields import (
|
||||
CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, CSVModelMultipleChoiceField, CSVTypedChoiceField,
|
||||
SlugField,
|
||||
)
|
||||
from virtualization.models import Cluster
|
||||
from virtualization.models import Cluster, VMInterface, VirtualMachine
|
||||
from wireless.choices import WirelessRoleChoices
|
||||
from .common import ModuleCommonForm
|
||||
|
||||
@ -34,6 +34,7 @@ __all__ = (
|
||||
'InventoryItemImportForm',
|
||||
'InventoryItemRoleImportForm',
|
||||
'LocationImportForm',
|
||||
'MACAddressImportForm',
|
||||
'ManufacturerImportForm',
|
||||
'ModuleImportForm',
|
||||
'ModuleBayImportForm',
|
||||
@ -824,6 +825,98 @@ class PowerOutletImportForm(NetBoxModelImportForm):
|
||||
self.fields['power_port'].queryset = PowerPort.objects.none()
|
||||
|
||||
|
||||
class MACAddressImportForm(NetBoxModelImportForm):
|
||||
device = CSVModelChoiceField(
|
||||
label=_('Device'),
|
||||
queryset=Device.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text=_('Parent device of assigned interface (if any)')
|
||||
)
|
||||
virtual_machine = CSVModelChoiceField(
|
||||
label=_('Virtual machine'),
|
||||
queryset=VirtualMachine.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text=_('Parent VM of assigned interface (if any)')
|
||||
)
|
||||
interface = CSVModelChoiceField(
|
||||
label=_('Interface'),
|
||||
queryset=Interface.objects.none(), # Can also refer to VMInterface
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text=_('Assigned interface')
|
||||
)
|
||||
is_primary = forms.BooleanField(
|
||||
label=_('Is primary'),
|
||||
help_text=_('Make this the primary MAC for the assigned interface'),
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = MACAddress
|
||||
fields = [
|
||||
'mac_address', 'device', 'virtual_machine', 'interface', 'is_primary',
|
||||
'description', 'comments', 'tags',
|
||||
]
|
||||
|
||||
def __init__(self, data=None, *args, **kwargs):
|
||||
super().__init__(data, *args, **kwargs)
|
||||
|
||||
if data:
|
||||
|
||||
# Limit interface queryset by assigned device
|
||||
if data.get('device'):
|
||||
self.fields['interface'].queryset = Interface.objects.filter(
|
||||
**{f"device__{self.fields['device'].to_field_name}": data['device']}
|
||||
)
|
||||
|
||||
# Limit interface queryset by assigned device
|
||||
elif data.get('virtual_machine'):
|
||||
self.fields['interface'].queryset = VMInterface.objects.filter(
|
||||
**{f"virtual_machine__{self.fields['virtual_machine'].to_field_name}": data['virtual_machine']}
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
device = self.cleaned_data.get('device')
|
||||
virtual_machine = self.cleaned_data.get('virtual_machine')
|
||||
interface = self.cleaned_data.get('interface')
|
||||
is_primary = self.cleaned_data.get('is_primary')
|
||||
|
||||
# Validate is_primary
|
||||
# TODO: scope to interface rather than device/VM
|
||||
if is_primary and not device and not virtual_machine:
|
||||
raise forms.ValidationError({
|
||||
"is_primary": _("No device or virtual machine specified; cannot set as primary MAC")
|
||||
})
|
||||
if is_primary and not interface:
|
||||
raise forms.ValidationError({
|
||||
"is_primary": _("No interface specified; cannot set as primary MAC")
|
||||
})
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
||||
# Set interface assignment
|
||||
if self.cleaned_data.get('interface'):
|
||||
self.instance.assigned_object = self.cleaned_data['interface']
|
||||
|
||||
mac_address = super().save(*args, **kwargs)
|
||||
|
||||
# Set as primary for device/VM
|
||||
# TODO: set as primary for interface
|
||||
# if self.cleaned_data.get('is_primary'):
|
||||
# parent = self.cleaned_data.get('device') or self.cleaned_data.get('virtual_machine')
|
||||
# if self.instance.address.version == 4:
|
||||
# parent.primary_ip4 = ipaddress
|
||||
# elif self.instance.address.version == 6:
|
||||
# parent.primary_ip6 = ipaddress
|
||||
# parent.save()
|
||||
|
||||
return mac_address
|
||||
|
||||
|
||||
class InterfaceImportForm(NetBoxModelImportForm):
|
||||
device = CSVModelChoiceField(
|
||||
label=_('Device'),
|
||||
@ -906,7 +999,7 @@ class InterfaceImportForm(NetBoxModelImportForm):
|
||||
model = Interface
|
||||
fields = (
|
||||
'device', 'name', 'label', 'parent', 'bridge', 'lag', 'type', 'speed', 'duplex', 'enabled',
|
||||
'mark_connected', '_mac_address', 'wwn', 'vdcs', 'mtu', 'mgmt_only', 'description', 'poe_mode', 'poe_type', 'mode',
|
||||
'mark_connected', 'wwn', 'vdcs', 'mtu', 'mgmt_only', 'description', 'poe_mode', 'poe_type', 'mode',
|
||||
'vrf', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'tags'
|
||||
)
|
||||
|
||||
|
@ -34,6 +34,7 @@ __all__ = (
|
||||
'InventoryItemFilterForm',
|
||||
'InventoryItemRoleFilterForm',
|
||||
'LocationFilterForm',
|
||||
'MACAddressFilterForm',
|
||||
'ManufacturerFilterForm',
|
||||
'ModuleFilterForm',
|
||||
'ModuleBayFilterForm',
|
||||
@ -1324,6 +1325,20 @@ class PowerOutletFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
||||
)
|
||||
|
||||
|
||||
class MACAddressFilterForm(NetBoxModelFilterSetForm):
|
||||
model = MACAddress
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('mac_address', name=_('Addressing')),
|
||||
)
|
||||
selector_fields = ('filter_id', 'q', 'device_id')
|
||||
mac_address = forms.CharField(
|
||||
required=False,
|
||||
label=_('MAC address')
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
||||
model = Interface
|
||||
fieldsets = (
|
||||
|
@ -1,5 +1,6 @@
|
||||
from django import forms
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from timezone_field import TimeZoneFormField
|
||||
|
||||
@ -17,7 +18,7 @@ from utilities.forms.fields import (
|
||||
)
|
||||
from utilities.forms.rendering import FieldSet, InlineFields, TabbedGroups
|
||||
from utilities.forms.widgets import APISelect, ClearableFileInput, HTMXSelect, NumberWithOptions, SelectWithPK
|
||||
from virtualization.models import Cluster
|
||||
from virtualization.models import Cluster, VMInterface
|
||||
from wireless.models import WirelessLAN, WirelessLANGroup
|
||||
from .common import InterfaceCommonForm, ModuleCommonForm
|
||||
|
||||
@ -41,6 +42,8 @@ __all__ = (
|
||||
'InventoryItemRoleForm',
|
||||
'InventoryItemTemplateForm',
|
||||
'LocationForm',
|
||||
'MACAddressForm',
|
||||
'MACAddressBulkAddForm',
|
||||
'ManufacturerForm',
|
||||
'ModuleForm',
|
||||
'ModuleBayForm',
|
||||
@ -1298,6 +1301,120 @@ class PowerOutletForm(ModularDeviceComponentForm):
|
||||
]
|
||||
|
||||
|
||||
class MACAddressForm(NetBoxModelForm):
|
||||
mac_address = forms.CharField(
|
||||
required=True,
|
||||
label=_('MAC address')
|
||||
)
|
||||
interface = DynamicModelChoiceField(
|
||||
label=_('Interface'),
|
||||
queryset=Interface.objects.all(),
|
||||
required=False,
|
||||
)
|
||||
vminterface = DynamicModelChoiceField(
|
||||
label=_('VM Interface'),
|
||||
queryset=VMInterface.objects.all(),
|
||||
required=False,
|
||||
)
|
||||
is_primary = forms.BooleanField(
|
||||
required=False,
|
||||
label=_('Primary for interface'),
|
||||
)
|
||||
|
||||
fieldsets = (
|
||||
FieldSet(
|
||||
'mac_address', 'description', 'tags',
|
||||
),
|
||||
FieldSet(
|
||||
TabbedGroups(
|
||||
FieldSet('interface', name=_('Device')),
|
||||
FieldSet('vminterface', name=_('Virtual Machine')),
|
||||
),
|
||||
'is_primary', name=_('Assignment')
|
||||
),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = MACAddress
|
||||
fields = [
|
||||
'mac_address', 'interface', 'vminterface', 'is_primary', 'description', 'tags',
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
# Initialize helper selectors
|
||||
instance = kwargs.get('instance')
|
||||
initial = kwargs.get('initial', {}).copy()
|
||||
if instance:
|
||||
if type(instance.assigned_object) is Interface:
|
||||
initial['interface'] = instance.assigned_object
|
||||
elif type(instance.assigned_object) is VMInterface:
|
||||
initial['vminterface'] = instance.assigned_object
|
||||
kwargs['initial'] = initial
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Initialize primary_for_parent if IP address is already assigned
|
||||
if self.instance.pk and self.instance.assigned_object:
|
||||
# parent = getattr(self.instance.assigned_object, 'parent_object', None)
|
||||
# if parent and (
|
||||
# self.instance.address.version == 4 and parent.primary_ip4_id == self.instance.pk or
|
||||
# self.instance.address.version == 6 and parent.primary_ip6_id == self.instance.pk
|
||||
# ):
|
||||
# self.initial['primary_for_parent'] = True
|
||||
|
||||
if type(instance.assigned_object) is Interface:
|
||||
self.fields['interface'].widget.add_query_params({
|
||||
'device_id': instance.assigned_object.device.pk,
|
||||
})
|
||||
elif type(instance.assigned_object) is VMInterface:
|
||||
self.fields['vminterface'].widget.add_query_params({
|
||||
'virtual_machine_id': instance.assigned_object.virtual_machine.pk,
|
||||
})
|
||||
|
||||
# Disable object assignment fields if the IP address is designated as primary
|
||||
if self.initial.get('primary_for_parent'):
|
||||
self.fields['interface'].disabled = True
|
||||
self.fields['vminterface'].disabled = True
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Handle object assignment
|
||||
selected_objects = [
|
||||
field for field in ('interface', 'vminterface') if self.cleaned_data[field]
|
||||
]
|
||||
if len(selected_objects) > 1:
|
||||
raise forms.ValidationError({
|
||||
selected_objects[1]: _("A MAC address can only be assigned to a single object.")
|
||||
})
|
||||
elif selected_objects:
|
||||
assigned_object = self.cleaned_data[selected_objects[0]]
|
||||
if self.instance.pk and self.instance.assigned_object and self.cleaned_data['is_primary'] and assigned_object != self.instance.assigned_object:
|
||||
raise ValidationError(
|
||||
_("Cannot reassign MAC address while it is designated as the primary MAC for the interface")
|
||||
)
|
||||
self.instance.assigned_object = assigned_object
|
||||
else:
|
||||
self.instance.assigned_object = None
|
||||
|
||||
# Primary MAC assignment is only available if an interface has been assigned.
|
||||
interface = self.cleaned_data.get('interface') or self.cleaned_data.get('vminterface')
|
||||
if self.cleaned_data.get('is_primary') and not interface:
|
||||
self.add_error(
|
||||
'is_primary', _("Only IP addresses assigned to an interface can be designated as primary IPs.")
|
||||
)
|
||||
|
||||
|
||||
class MACAddressBulkAddForm(NetBoxModelForm):
|
||||
|
||||
class Meta:
|
||||
model = MACAddress
|
||||
fields = [
|
||||
'mac_address', 'description', 'tags',
|
||||
]
|
||||
|
||||
|
||||
class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
|
||||
vdcs = DynamicModelMultipleChoiceField(
|
||||
queryset=VirtualDeviceContext.objects.all(),
|
||||
|
@ -23,6 +23,7 @@ __all__ = (
|
||||
'InventoryItemCreateForm',
|
||||
'InventoryItemTemplateCreateForm',
|
||||
'ModuleBayCreateForm',
|
||||
# 'MACAddressCreateForm',
|
||||
'ModuleBayTemplateCreateForm',
|
||||
'PowerOutletCreateForm',
|
||||
'PowerOutletTemplateCreateForm',
|
||||
@ -238,6 +239,12 @@ class PowerOutletCreateForm(ComponentCreateForm, model_forms.PowerOutletForm):
|
||||
exclude = ('name', 'label')
|
||||
|
||||
|
||||
# class MACAddressCreateForm(ComponentCreateForm, model_forms.MACAddressForm):
|
||||
#
|
||||
# class Meta(model_forms.MACAddressForm.Meta):
|
||||
# exclude = ('name', 'label')
|
||||
|
||||
|
||||
class InterfaceCreateForm(ComponentCreateForm, model_forms.InterfaceForm):
|
||||
|
||||
class Meta(model_forms.InterfaceForm.Meta):
|
||||
|
@ -377,7 +377,7 @@ class FrontPortTemplateType(ModularComponentTemplateType):
|
||||
filters=InterfaceFilter
|
||||
)
|
||||
class InterfaceType(IPAddressesMixin, ModularComponentType, CabledObjectMixin, PathEndpointMixin):
|
||||
_mac_address: str | None
|
||||
# _mac_address: str | None
|
||||
wwn: str | None
|
||||
parent: Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')] | None
|
||||
bridge: Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')] | None
|
||||
|
@ -11,6 +11,9 @@ def populate_macaddress_objects(apps, schema_editor):
|
||||
Interface = apps.get_model('dcim', 'Interface')
|
||||
VMInterface = apps.get_model('virtualization', 'VMInterface')
|
||||
MACAddress = apps.get_model('dcim', 'MACAddress')
|
||||
ContentType = apps.get_model('contenttypes', 'ContentType')
|
||||
interface_ct = ContentType.objects.get_for_model(Interface)
|
||||
vminterface_ct = ContentType.objects.get_for_model(VMInterface)
|
||||
mac_addresses = []
|
||||
print()
|
||||
print('Converting MAC addresses...')
|
||||
@ -18,14 +21,18 @@ def populate_macaddress_objects(apps, schema_editor):
|
||||
mac_addresses.append(
|
||||
MACAddress(
|
||||
mac_address=interface._mac_address,
|
||||
interface=interface,
|
||||
assigned_object_type=interface_ct,
|
||||
assigned_object_id=interface.id,
|
||||
# interface=interface,
|
||||
)
|
||||
)
|
||||
for vm_interface in VMInterface.objects.filter(_mac_address__isnull=False):
|
||||
for vminterface in VMInterface.objects.filter(_mac_address__isnull=False):
|
||||
mac_addresses.append(
|
||||
MACAddress(
|
||||
mac_address=vm_interface._mac_address,
|
||||
vm_interface=vm_interface,
|
||||
mac_address=vminterface._mac_address,
|
||||
assigned_object_type=vminterface_ct,
|
||||
assigned_object_id=vminterface.id,
|
||||
# vm_interface=vm_interface,
|
||||
)
|
||||
)
|
||||
MACAddress.objects.bulk_create(mac_addresses)
|
||||
@ -57,9 +64,11 @@ class Migration(migrations.Migration):
|
||||
('comments', models.TextField(blank=True)),
|
||||
('mac_address', dcim.fields.MACAddressField(blank=True, null=True)),
|
||||
('is_primary', models.BooleanField(default=False)),
|
||||
('interface', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='dcim.interface')),
|
||||
# ('interface', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='dcim.interface')),
|
||||
('assigned_object_id', models.PositiveBigIntegerField(blank=True, null=True)),
|
||||
('assigned_object_type', 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')),
|
||||
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
|
||||
('vm_interface', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='virtualization.vminterface')),
|
||||
# ('vm_interface', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='virtualization.vminterface')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
|
17
netbox/dcim/migrations/0196_remove_interface__mac_address.py
Normal file
17
netbox/dcim/migrations/0196_remove_interface__mac_address.py
Normal file
@ -0,0 +1,17 @@
|
||||
# Generated by Django 5.1.2 on 2024-10-29 15:41
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0195_rename_mac_address_interface__mac_address_macaddress'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='interface',
|
||||
name='_mac_address',
|
||||
),
|
||||
]
|
@ -510,11 +510,6 @@ class BaseInterface(models.Model):
|
||||
verbose_name=_('enabled'),
|
||||
default=True
|
||||
)
|
||||
_mac_address = MACAddressField(
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name=_('MAC address')
|
||||
)
|
||||
mtu = models.PositiveIntegerField(
|
||||
blank=True,
|
||||
null=True,
|
||||
@ -578,7 +573,7 @@ class BaseInterface(models.Model):
|
||||
|
||||
@property
|
||||
def mac_address(self):
|
||||
if macaddress := self.macaddress_set.first():
|
||||
if macaddress := self.mac_addresses.order_by('-is_primary').first():
|
||||
return macaddress.mac_address
|
||||
return None
|
||||
|
||||
@ -725,6 +720,12 @@ class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEnd
|
||||
object_id_field='assigned_object_id',
|
||||
related_query_name='interface'
|
||||
)
|
||||
mac_addresses = GenericRelation(
|
||||
to='dcim.MACAddress',
|
||||
content_type_field='assigned_object_type',
|
||||
object_id_field='assigned_object_id',
|
||||
related_query_name='interface'
|
||||
)
|
||||
fhrp_group_assignments = GenericRelation(
|
||||
to='ipam.FHRPGroupAssignment',
|
||||
content_type_field='interface_type',
|
||||
@ -1338,21 +1339,44 @@ class MACAddress(PrimaryModel):
|
||||
blank=True,
|
||||
verbose_name=_('MAC address')
|
||||
)
|
||||
interface = models.ForeignKey(
|
||||
to='dcim.Interface',
|
||||
# interface = models.ForeignKey(
|
||||
# to='dcim.Interface',
|
||||
# on_delete=models.PROTECT,
|
||||
# null=True,
|
||||
# blank=True,
|
||||
# verbose_name=_('Interface')
|
||||
# )
|
||||
# vm_interface = models.ForeignKey(
|
||||
# to='virtualization.VMInterface',
|
||||
# on_delete=models.PROTECT,
|
||||
# null=True,
|
||||
# blank=True,
|
||||
# verbose_name=_('VM Interface')
|
||||
# )
|
||||
assigned_object_type = models.ForeignKey(
|
||||
to='contenttypes.ContentType',
|
||||
limit_choices_to=MACADDRESS_ASSIGNMENT_MODELS,
|
||||
on_delete=models.PROTECT,
|
||||
null=True,
|
||||
related_name='+',
|
||||
blank=True,
|
||||
verbose_name=_('Interface')
|
||||
null=True
|
||||
)
|
||||
vm_interface = models.ForeignKey(
|
||||
to='virtualization.VMInterface',
|
||||
on_delete=models.PROTECT,
|
||||
null=True,
|
||||
assigned_object_id = models.PositiveBigIntegerField(
|
||||
blank=True,
|
||||
verbose_name=_('VM Interface')
|
||||
null=True
|
||||
)
|
||||
assigned_object = GenericForeignKey(
|
||||
ct_field='assigned_object_type',
|
||||
fk_field='assigned_object_id'
|
||||
)
|
||||
is_primary = models.BooleanField(
|
||||
verbose_name=_('is primary for interface'),
|
||||
default=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('MAC address')
|
||||
verbose_name_plural = _('MAC addresses')
|
||||
|
||||
def __str__(self):
|
||||
return str(self.mac_address)
|
||||
|
@ -29,6 +29,7 @@ __all__ = (
|
||||
'InterfaceTable',
|
||||
'InventoryItemRoleTable',
|
||||
'InventoryItemTable',
|
||||
'MACAddressTable',
|
||||
'ModuleBayTable',
|
||||
'PlatformTable',
|
||||
'PowerOutletTable',
|
||||
@ -593,6 +594,40 @@ class BaseInterfaceTable(NetBoxTable):
|
||||
return ",".join([str(obj) for obj in value.all()])
|
||||
|
||||
|
||||
class MACAddressTable(NetBoxTable):
|
||||
mac_address = tables.Column(
|
||||
verbose_name=_('MAC Address'),
|
||||
linkify=True
|
||||
)
|
||||
assigned_object = tables.Column(
|
||||
linkify=True,
|
||||
orderable=False,
|
||||
verbose_name=_('Interface')
|
||||
)
|
||||
is_primary = columns.BooleanColumn(
|
||||
verbose_name=_('Primary MAC'),
|
||||
false_mark=None
|
||||
)
|
||||
# interface = tables.Column(
|
||||
# verbose_name=_('Interface'),
|
||||
# linkify=True
|
||||
# )
|
||||
# vm_interface = tables.Column(
|
||||
# verbose_name=_('VM Interface'),
|
||||
# linkify=True
|
||||
# )
|
||||
tags = columns.TagColumn(
|
||||
url_name='dcim:macaddress_list'
|
||||
)
|
||||
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = models.MACAddress
|
||||
fields = (
|
||||
'pk', 'id', 'mac_address', 'assigned_object', 'created', 'last_updated', 'is_primary'
|
||||
)
|
||||
default_columns = ('pk', 'mac_address', 'assigned_object', 'is_primary')
|
||||
|
||||
|
||||
class InterfaceTable(ModularDeviceComponentTable, BaseInterfaceTable, PathEndpointTable):
|
||||
device = tables.Column(
|
||||
verbose_name=_('Device'),
|
||||
|
@ -250,6 +250,16 @@ urlpatterns = [
|
||||
path('power-outlets/<int:pk>/', include(get_model_urls('dcim', 'poweroutlet'))),
|
||||
path('devices/power-outlets/add/', views.DeviceBulkAddPowerOutletView.as_view(), name='device_bulk_add_poweroutlet'),
|
||||
|
||||
# MAC addresses
|
||||
path('mac-addresses/', views.MACAddressListView.as_view(), name='macaddress_list'),
|
||||
path('mac-addresses/add/', views.MACAddressEditView.as_view(), name='macaddress_add'),
|
||||
path('mac-addresses/bulk-add/', views.MACAddressBulkCreateView.as_view(), name='macaddress_bulk_add'),
|
||||
path('mac-addresses/import/', views.MACAddressBulkImportView.as_view(), name='macaddress_import'),
|
||||
path('mac-addresses/edit/', views.MACAddressBulkEditView.as_view(), name='macaddress_bulk_edit'),
|
||||
path('mac-addresses/rename/', views.MACAddressBulkRenameView.as_view(), name='macaddress_bulk_rename'),
|
||||
path('mac-addresses/delete/', views.MACAddressBulkDeleteView.as_view(), name='macaddress_bulk_delete'),
|
||||
path('mac-addresses/<int:pk>/', include(get_model_urls('dcim', 'macaddress'))),
|
||||
|
||||
# Interfaces
|
||||
path('interfaces/', views.InterfaceListView.as_view(), name='interface_list'),
|
||||
path('interfaces/add/', views.InterfaceCreateView.as_view(), name='interface_add'),
|
||||
|
@ -2519,6 +2519,78 @@ class PowerOutletBulkDeleteView(generic.BulkDeleteView):
|
||||
register_model_view(PowerOutlet, 'trace', kwargs={'model': PowerOutlet})(PathTraceView)
|
||||
|
||||
|
||||
#
|
||||
# MAC addresses
|
||||
#
|
||||
|
||||
class MACAddressListView(generic.ObjectListView):
|
||||
queryset = MACAddress.objects.all()
|
||||
filterset = filtersets.MACAddressFilterSet
|
||||
filterset_form = forms.MACAddressFilterForm
|
||||
table = tables.MACAddressTable
|
||||
template_name = 'dcim/component_list.html'
|
||||
actions = {
|
||||
**DEFAULT_ACTION_PERMISSIONS,
|
||||
'bulk_rename': {'change'},
|
||||
}
|
||||
|
||||
|
||||
@register_model_view(MACAddress)
|
||||
class MACAddressView(generic.ObjectView):
|
||||
queryset = MACAddress.objects.all()
|
||||
|
||||
|
||||
# class MACAddressCreateView(generic.ComponentCreateView):
|
||||
# queryset = MACAddress.objects.all()
|
||||
# form = forms.MACAddressForm
|
||||
# model_form = forms.MACAddressForm
|
||||
|
||||
|
||||
@register_model_view(MACAddress, 'edit')
|
||||
class MACAddressEditView(generic.ObjectEditView):
|
||||
queryset = MACAddress.objects.all()
|
||||
form = forms.MACAddressForm
|
||||
|
||||
|
||||
@register_model_view(MACAddress, 'delete')
|
||||
class MACAddressDeleteView(generic.ObjectDeleteView):
|
||||
queryset = MACAddress.objects.all()
|
||||
|
||||
|
||||
class MACAddressBulkCreateView(generic.BulkCreateView):
|
||||
queryset = MACAddress.objects.all()
|
||||
form = forms.MACAddressBulkCreateForm
|
||||
model_form = forms.MACAddressBulkAddForm
|
||||
pattern_target = 'mac_address'
|
||||
template_name = 'dcim/macaddress_bulk_add.html'
|
||||
|
||||
|
||||
class MACAddressBulkImportView(generic.BulkImportView):
|
||||
queryset = MACAddress.objects.all()
|
||||
model_form = forms.MACAddressImportForm
|
||||
|
||||
|
||||
class MACAddressBulkEditView(generic.BulkEditView):
|
||||
queryset = MACAddress.objects.all()
|
||||
filterset = filtersets.MACAddressFilterSet
|
||||
table = tables.MACAddressTable
|
||||
form = forms.MACAddressBulkEditForm
|
||||
|
||||
|
||||
class MACAddressBulkRenameView(generic.BulkRenameView):
|
||||
queryset = MACAddress.objects.all()
|
||||
|
||||
|
||||
class MACAddressBulkDisconnectView(BulkDisconnectView):
|
||||
queryset = MACAddress.objects.all()
|
||||
|
||||
|
||||
class MACAddressBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = MACAddress.objects.all()
|
||||
filterset = filtersets.MACAddressFilterSet
|
||||
table = tables.MACAddressTable
|
||||
|
||||
|
||||
#
|
||||
# Interfaces
|
||||
#
|
||||
@ -2552,7 +2624,7 @@ class InterfaceView(generic.ObjectView):
|
||||
|
||||
# Get bridge interfaces
|
||||
bridge_interfaces = Interface.objects.restrict(request.user, 'view').filter(bridge=instance)
|
||||
bridge_interfaces_tables = tables.InterfaceTable(
|
||||
bridge_interfaces_table = tables.InterfaceTable(
|
||||
bridge_interfaces,
|
||||
exclude=('device', 'parent'),
|
||||
orderable=False
|
||||
@ -2560,12 +2632,19 @@ class InterfaceView(generic.ObjectView):
|
||||
|
||||
# Get child interfaces
|
||||
child_interfaces = Interface.objects.restrict(request.user, 'view').filter(parent=instance)
|
||||
child_interfaces_tables = tables.InterfaceTable(
|
||||
child_interfaces_table = tables.InterfaceTable(
|
||||
child_interfaces,
|
||||
exclude=('device', 'parent'),
|
||||
orderable=False
|
||||
)
|
||||
|
||||
# Get MAC addresses
|
||||
mac_addresses_table = tables.MACAddressTable(
|
||||
data=instance.mac_addresses,
|
||||
exclude=('assigned_object',),
|
||||
orderable=False
|
||||
)
|
||||
|
||||
# Get assigned VLANs and annotate whether each is tagged or untagged
|
||||
vlans = []
|
||||
if instance.untagged_vlan is not None:
|
||||
@ -2582,8 +2661,9 @@ class InterfaceView(generic.ObjectView):
|
||||
|
||||
return {
|
||||
'vdc_table': vdc_table,
|
||||
'bridge_interfaces_table': bridge_interfaces_tables,
|
||||
'child_interfaces_table': child_interfaces_tables,
|
||||
'bridge_interfaces_table': bridge_interfaces_table,
|
||||
'child_interfaces_table': child_interfaces_table,
|
||||
'mac_addresses_table': mac_addresses_table,
|
||||
'vlan_table': vlan_table,
|
||||
}
|
||||
|
||||
|
@ -91,6 +91,7 @@ DEVICES_MENU = Menu(
|
||||
MenuGroup(
|
||||
label=_('Device Components'),
|
||||
items=(
|
||||
get_model_item('dcim', 'macaddress', _('MAC Addresses')),
|
||||
get_model_item('dcim', 'interface', _('Interfaces')),
|
||||
get_model_item('dcim', 'frontport', _('Front Ports')),
|
||||
get_model_item('dcim', 'rearport', _('Rear Ports')),
|
||||
|
@ -346,7 +346,23 @@
|
||||
{% endif %}
|
||||
</h2>
|
||||
{% htmx_table 'ipam:ipaddress_list' interface_id=object.pk %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-12">
|
||||
<div class="card">
|
||||
<h2 class="card-header">
|
||||
{% trans "MAC Addresses" %}
|
||||
{% if perms.ipam.add_macaddress %}
|
||||
<div class="card-actions">
|
||||
<a href="{% url 'dcim:macaddress_add' %}?device={{ object.device.pk }}&interface={{ object.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-ghost-primary btn-sm">
|
||||
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> {% trans "Add MAC Address" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</h2>
|
||||
{% htmx_table 'dcim:macaddress_list' interface_id=object.pk %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
50
netbox/templates/dcim/macaddress.html
Normal file
50
netbox/templates/dcim/macaddress.html
Normal file
@ -0,0 +1,50 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col col-md-4">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "MAC Address" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Assignment" %}</th>
|
||||
<td>
|
||||
{% if object.assigned_object %}
|
||||
{% if object.assigned_object.parent_object %}
|
||||
{{ object.assigned_object.parent_object|linkify }} /
|
||||
{% endif %}
|
||||
{{ object.assigned_object|linkify }}
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Primary MAC for Interface" %}</th>
|
||||
<td>{% checkmark object.is_primary %}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-md-8">
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
30
netbox/templates/dcim/macaddress_bulk_add.html
Normal file
30
netbox/templates/dcim/macaddress_bulk_add.html
Normal file
@ -0,0 +1,30 @@
|
||||
{% extends 'generic/object_edit.html' %}
|
||||
{% load static %}
|
||||
{% load form_helpers %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}{% trans "Bulk Add MAC Addresses" %}{% endblock %}
|
||||
|
||||
{% block tabs %}
|
||||
{% include 'ipam/inc/ipaddress_edit_header.html' with active_tab='bulk_add' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block form %}
|
||||
<div class="field-group my-5">
|
||||
<div class="row">
|
||||
<h2 class="col-9 offset-3">{% trans "MAC Addresses" %}</h2>
|
||||
</div>
|
||||
{% render_field form.pattern %}
|
||||
{% render_field model_form.description %}
|
||||
{% render_field model_form.tags %}
|
||||
</div>
|
||||
|
||||
{% if model_form.custom_fields %}
|
||||
<div class="field-group my-5">
|
||||
<div class="row">
|
||||
<h2 class="col-9 offset-3">{% trans "Custom Fields" %}</h2>
|
||||
</div>
|
||||
{% render_custom_fields model_form %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
@ -95,6 +95,24 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-12">
|
||||
<div class="card">
|
||||
<h2 class="card-header">
|
||||
{% trans "MAC Addresses" %}
|
||||
{% if perms.ipam.add_macaddress %}
|
||||
<div class="card-actions">
|
||||
<a href="{% url 'dcim:macaddress_add' %}?virtual_machine={{ object.device.pk }}&vminterface={{ object.pk }}&return_url={{ object.get_absolute_url }}"
|
||||
class="btn btn-ghost-primary btn-sm">
|
||||
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> {% trans "Add MAC Address" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</h2>
|
||||
{% htmx_table 'dcim:macaddress_list' vminterface_id=object.pk %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-12">
|
||||
{% include 'inc/panel_table.html' with table=vlan_table heading="VLANs" %}
|
||||
|
@ -9,7 +9,7 @@ from extras.models import ConfigTemplate
|
||||
from ipam.filtersets import PrimaryIPFilterSet
|
||||
from netbox.filtersets import OrganizationalModelFilterSet, NetBoxModelFilterSet
|
||||
from tenancy.filtersets import TenancyFilterSet, ContactModelFilterSet
|
||||
from utilities.filters import MultiValueCharFilter, MultiValueMACAddressFilter, TreeNodeMultipleChoiceFilter
|
||||
from utilities.filters import MultiValueCharFilter, TreeNodeMultipleChoiceFilter
|
||||
from .choices import *
|
||||
from .models import *
|
||||
|
||||
@ -225,10 +225,10 @@ class VirtualMachineFilterSet(
|
||||
to_field_name='slug',
|
||||
label=_('Platform (slug)'),
|
||||
)
|
||||
mac_address = MultiValueMACAddressFilter(
|
||||
field_name='interfaces___mac_address',
|
||||
label=_('MAC address'),
|
||||
)
|
||||
# mac_address = MultiValueMACAddressFilter(
|
||||
# field_name='interfaces___mac_address',
|
||||
# label=_('MAC address'),
|
||||
# )
|
||||
has_primary_ip = django_filters.BooleanFilter(
|
||||
method='_has_primary_ip',
|
||||
label=_('Has a primary IP'),
|
||||
@ -297,9 +297,9 @@ class VMInterfaceFilterSet(NetBoxModelFilterSet, CommonInterfaceFilterSet):
|
||||
queryset=VMInterface.objects.all(),
|
||||
label=_('Bridged interface (ID)'),
|
||||
)
|
||||
_mac_address = MultiValueMACAddressFilter(
|
||||
label=_('MAC address'),
|
||||
)
|
||||
# _mac_address = MultiValueMACAddressFilter(
|
||||
# label=_('MAC address'),
|
||||
# )
|
||||
|
||||
class Meta:
|
||||
model = VMInterface
|
||||
|
@ -178,7 +178,7 @@ class VMInterfaceImportForm(NetBoxModelImportForm):
|
||||
class Meta:
|
||||
model = VMInterface
|
||||
fields = (
|
||||
'virtual_machine', 'name', 'parent', 'bridge', 'enabled', '_mac_address', 'mtu', 'description', 'mode',
|
||||
'virtual_machine', 'name', 'parent', 'bridge', 'enabled', 'mtu', 'description', 'mode',
|
||||
'vrf', 'tags'
|
||||
)
|
||||
|
||||
|
@ -95,7 +95,7 @@ class VirtualMachineType(ConfigContextMixin, ContactsMixin, NetBoxObjectType):
|
||||
filters=VMInterfaceFilter
|
||||
)
|
||||
class VMInterfaceType(IPAddressesMixin, ComponentType):
|
||||
_mac_address: str | None
|
||||
# _mac_address: str | None
|
||||
parent: Annotated["VMInterfaceType", strawberry.lazy('virtualization.graphql.types')] | None
|
||||
bridge: Annotated["VMInterfaceType", strawberry.lazy('virtualization.graphql.types')] | None
|
||||
untagged_vlan: Annotated["VLANType", strawberry.lazy('ipam.graphql.types')] | None
|
||||
|
@ -0,0 +1,17 @@
|
||||
# Generated by Django 5.1.2 on 2024-10-29 15:41
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('virtualization', '0042_rename_mac_address_vminterface__mac_address'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='vminterface',
|
||||
name='_mac_address',
|
||||
),
|
||||
]
|
@ -368,6 +368,12 @@ class VMInterface(ComponentModel, BaseInterface, TrackingModelMixin):
|
||||
object_id_field='assigned_object_id',
|
||||
related_query_name='vminterface',
|
||||
)
|
||||
mac_addresses = GenericRelation(
|
||||
to='dcim.MACAddress',
|
||||
content_type_field='assigned_object_type',
|
||||
object_id_field='assigned_object_id',
|
||||
related_query_name='vminterface'
|
||||
)
|
||||
|
||||
class Meta(ComponentModel.Meta):
|
||||
verbose_name = _('interface')
|
||||
|
Loading…
Reference in New Issue
Block a user