mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
Merge branch 'develop' into api2
Conflicts: netbox/dcim/api/serializers.py netbox/dcim/api/views.py netbox/dcim/filters.py
This commit is contained in:
@ -24,7 +24,7 @@ Each group is assigned to a parent site for easy navigation. Hierarchical recurs
|
|||||||
|
|
||||||
### Rack Roles
|
### Rack Roles
|
||||||
|
|
||||||
Each rak can optionally be assigned to a functional role. For example, you might designate a rack for compute or storage resources, or to house colocated customer devices.
|
Each rack can optionally be assigned to a functional role. For example, you might designate a rack for compute or storage resources, or to house colocated customer devices.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -83,9 +83,11 @@ One IP address can be designated as the network address translation (NAT) IP add
|
|||||||
|
|
||||||
# VLANs
|
# VLANs
|
||||||
|
|
||||||
A VLAN represents an isolated layer two domain, identified by a name and a numeric ID (1-4094). Note that while it is good practice, neither VLAN names nor IDs must be unique within a site. This is to accommodate the fact that many real-world network use less-than-optimal VLAN allocations and may have overlapping VLAN ID assignments in practice.
|
A VLAN represents an isolated layer two domain, identified by a name and a numeric ID (1-4094) as defined in [IEEE 802.1Q](https://en.wikipedia.org/wiki/IEEE_802.1Q). VLANs may be assigned to a site and/or VLAN group. Like prefixes, each VLAN is assigned an operational status and (optionally) a functional role.
|
||||||
|
|
||||||
Like prefixes, each VLAN is assigned an operational status and (optionally) a functional role.
|
### VLAN Groups
|
||||||
|
|
||||||
|
VLAN groups can be employed for administrative organization within NetBox. Each VLAN within a group must have a unique ID and name. VLANs which are not assigned to a group may have overlapping names and IDs, including within a site.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.db.models import Count
|
from django.db.models import Count
|
||||||
|
|
||||||
from dcim.models import Site, Device, Interface, Rack, IFACE_FF_VIRTUAL
|
from dcim.models import Site, Device, Interface, Rack, VIRTUAL_IFACE_TYPES
|
||||||
from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
|
from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
|
||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
from utilities.forms import (
|
from utilities.forms import (
|
||||||
@ -227,14 +227,18 @@ class CircuitTerminationForm(BootstrapMixin, forms.ModelForm):
|
|||||||
|
|
||||||
# Limit interface choices
|
# Limit interface choices
|
||||||
if self.is_bound and self.data.get('device'):
|
if self.is_bound and self.data.get('device'):
|
||||||
interfaces = Interface.objects.filter(device=self.data['device'])\
|
interfaces = Interface.objects.filter(device=self.data['device']).exclude(
|
||||||
.exclude(form_factor=IFACE_FF_VIRTUAL).select_related('circuit_termination', 'connected_as_a',
|
form_factor__in=VIRTUAL_IFACE_TYPES
|
||||||
'connected_as_b')
|
).select_related(
|
||||||
|
'circuit_termination', 'connected_as_a', 'connected_as_b'
|
||||||
|
)
|
||||||
self.fields['interface'].widget.attrs['initial'] = self.data.get('interface')
|
self.fields['interface'].widget.attrs['initial'] = self.data.get('interface')
|
||||||
elif self.initial.get('device'):
|
elif self.initial.get('device'):
|
||||||
interfaces = Interface.objects.filter(device=self.initial['device'])\
|
interfaces = Interface.objects.filter(device=self.initial['device']).exclude(
|
||||||
.exclude(form_factor=IFACE_FF_VIRTUAL).select_related('circuit_termination', 'connected_as_a',
|
form_factor__in=VIRTUAL_IFACE_TYPES
|
||||||
'connected_as_b')
|
).select_related(
|
||||||
|
'circuit_termination', 'connected_as_a', 'connected_as_b'
|
||||||
|
)
|
||||||
self.fields['interface'].widget.attrs['initial'] = self.initial.get('interface')
|
self.fields['interface'].widget.attrs['initial'] = self.initial.get('interface')
|
||||||
else:
|
else:
|
||||||
interfaces = []
|
interfaces = []
|
||||||
|
@ -503,7 +503,6 @@ class WritablePowerPortSerializer(serializers.ModelSerializer):
|
|||||||
# Interfaces
|
# Interfaces
|
||||||
#
|
#
|
||||||
|
|
||||||
|
|
||||||
class InterfaceSerializer(serializers.ModelSerializer):
|
class InterfaceSerializer(serializers.ModelSerializer):
|
||||||
device = NestedDeviceSerializer()
|
device = NestedDeviceSerializer()
|
||||||
form_factor = ChoiceFieldSerializer(choices=IFACE_FF_CHOICES)
|
form_factor = ChoiceFieldSerializer(choices=IFACE_FF_CHOICES)
|
||||||
@ -513,7 +512,7 @@ class InterfaceSerializer(serializers.ModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Interface
|
model = Interface
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'device', 'name', 'form_factor', 'mac_address', 'mgmt_only', 'description', 'connection',
|
'id', 'device', 'name', 'form_factor', 'lag', 'mac_address', 'mgmt_only', 'description', 'connection',
|
||||||
'connected_interface',
|
'connected_interface',
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -541,7 +540,7 @@ class WritableInterfaceSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Interface
|
model = Interface
|
||||||
fields = ['id', 'device', 'name', 'form_factor', 'mac_address', 'mgmt_only', 'description']
|
fields = ['id', 'device', 'name', 'form_factor', 'lag', 'mac_address', 'mgmt_only', 'description']
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
@ -8,9 +8,9 @@ from tenancy.models import Tenant
|
|||||||
from utilities.filters import NullableModelMultipleChoiceFilter
|
from utilities.filters import NullableModelMultipleChoiceFilter
|
||||||
from .models import (
|
from .models import (
|
||||||
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||||
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer, Module,
|
DeviceBayTemplate, DeviceRole, DeviceType, IFACE_FF_LAG, Interface, InterfaceConnection, InterfaceTemplate,
|
||||||
Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation,
|
Manufacturer, Module, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
|
||||||
RackRole, Site,
|
RackReservation, RackRole, Site, VIRTUAL_IFACE_TYPES,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -391,11 +391,25 @@ class PowerOutletFilter(DeviceComponentFilterSet):
|
|||||||
|
|
||||||
|
|
||||||
class InterfaceFilter(DeviceComponentFilterSet):
|
class InterfaceFilter(DeviceComponentFilterSet):
|
||||||
|
type = django_filters.MethodFilter(
|
||||||
|
action='filter_type',
|
||||||
|
label='Interface type',
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Interface
|
model = Interface
|
||||||
fields = ['name']
|
fields = ['name']
|
||||||
|
|
||||||
|
def filter_type(self, queryset, value):
|
||||||
|
value = value.strip().lower()
|
||||||
|
if value == 'physical':
|
||||||
|
return queryset.exclude(form_factor__in=VIRTUAL_IFACE_TYPES)
|
||||||
|
elif value == 'virtual':
|
||||||
|
return queryset.filter(form_factor__in=VIRTUAL_IFACE_TYPES)
|
||||||
|
elif value == 'lag':
|
||||||
|
return queryset.filter(form_factor=IFACE_FF_LAG)
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
class DeviceBayFilter(DeviceComponentFilterSet):
|
class DeviceBayFilter(DeviceComponentFilterSet):
|
||||||
|
|
||||||
|
@ -18,9 +18,10 @@ from .formfields import MACAddressFormField
|
|||||||
from .models import (
|
from .models import (
|
||||||
DeviceBay, DeviceBayTemplate, CONNECTION_STATUS_CHOICES, CONNECTION_STATUS_PLANNED, CONNECTION_STATUS_CONNECTED,
|
DeviceBay, DeviceBayTemplate, CONNECTION_STATUS_CHOICES, CONNECTION_STATUS_PLANNED, CONNECTION_STATUS_CONNECTED,
|
||||||
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceRole, DeviceType,
|
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceRole, DeviceType,
|
||||||
Interface, IFACE_FF_CHOICES, IFACE_FF_VIRTUAL, IFACE_ORDERING_CHOICES, InterfaceConnection, InterfaceTemplate,
|
Interface, IFACE_FF_CHOICES, IFACE_FF_LAG, IFACE_ORDERING_CHOICES, InterfaceConnection, InterfaceTemplate,
|
||||||
Manufacturer, Module, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, RACK_TYPE_CHOICES,
|
Manufacturer, Module, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, RACK_TYPE_CHOICES,
|
||||||
RACK_WIDTH_CHOICES, Rack, RackGroup, RackReservation, RackRole, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHILD
|
RACK_WIDTH_CHOICES, Rack, RackGroup, RackReservation, RackRole, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHILD,
|
||||||
|
VIRTUAL_IFACE_TYPES
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -53,6 +54,15 @@ def validate_connection_status(value):
|
|||||||
raise ValidationError('Invalid connection status ({}); must be either "planned" or "connected".'.format(value))
|
raise ValidationError('Invalid connection status ({}); must be either "planned" or "connected".'.format(value))
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceComponentForm(BootstrapMixin, forms.Form):
|
||||||
|
"""
|
||||||
|
Allow inclusion of the parent device as context for limiting field choices.
|
||||||
|
"""
|
||||||
|
def __init__(self, device, *args, **kwargs):
|
||||||
|
self.device = device
|
||||||
|
super(DeviceComponentForm, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Sites
|
# Sites
|
||||||
#
|
#
|
||||||
@ -331,7 +341,7 @@ class ConsolePortTemplateForm(BootstrapMixin, forms.ModelForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class ConsolePortTemplateCreateForm(BootstrapMixin, forms.Form):
|
class ConsolePortTemplateCreateForm(DeviceComponentForm):
|
||||||
name_pattern = ExpandableNameField(label='Name')
|
name_pattern = ExpandableNameField(label='Name')
|
||||||
|
|
||||||
|
|
||||||
@ -345,7 +355,7 @@ class ConsoleServerPortTemplateForm(BootstrapMixin, forms.ModelForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class ConsoleServerPortTemplateCreateForm(BootstrapMixin, forms.Form):
|
class ConsoleServerPortTemplateCreateForm(DeviceComponentForm):
|
||||||
name_pattern = ExpandableNameField(label='Name')
|
name_pattern = ExpandableNameField(label='Name')
|
||||||
|
|
||||||
|
|
||||||
@ -359,7 +369,7 @@ class PowerPortTemplateForm(BootstrapMixin, forms.ModelForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class PowerPortTemplateCreateForm(BootstrapMixin, forms.Form):
|
class PowerPortTemplateCreateForm(DeviceComponentForm):
|
||||||
name_pattern = ExpandableNameField(label='Name')
|
name_pattern = ExpandableNameField(label='Name')
|
||||||
|
|
||||||
|
|
||||||
@ -373,7 +383,7 @@ class PowerOutletTemplateForm(BootstrapMixin, forms.ModelForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class PowerOutletTemplateCreateForm(BootstrapMixin, forms.Form):
|
class PowerOutletTemplateCreateForm(DeviceComponentForm):
|
||||||
name_pattern = ExpandableNameField(label='Name')
|
name_pattern = ExpandableNameField(label='Name')
|
||||||
|
|
||||||
|
|
||||||
@ -387,7 +397,7 @@ class InterfaceTemplateForm(BootstrapMixin, forms.ModelForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class InterfaceTemplateCreateForm(BootstrapMixin, forms.Form):
|
class InterfaceTemplateCreateForm(DeviceComponentForm):
|
||||||
name_pattern = ExpandableNameField(label='Name')
|
name_pattern = ExpandableNameField(label='Name')
|
||||||
form_factor = forms.ChoiceField(choices=IFACE_FF_CHOICES)
|
form_factor = forms.ChoiceField(choices=IFACE_FF_CHOICES)
|
||||||
mgmt_only = forms.BooleanField(required=False, label='OOB Management')
|
mgmt_only = forms.BooleanField(required=False, label='OOB Management')
|
||||||
@ -411,7 +421,7 @@ class DeviceBayTemplateForm(BootstrapMixin, forms.ModelForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class DeviceBayTemplateCreateForm(BootstrapMixin, forms.Form):
|
class DeviceBayTemplateCreateForm(DeviceComponentForm):
|
||||||
name_pattern = ExpandableNameField(label='Name')
|
name_pattern = ExpandableNameField(label='Name')
|
||||||
|
|
||||||
|
|
||||||
@ -743,7 +753,7 @@ class ConsolePortForm(BootstrapMixin, forms.ModelForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class ConsolePortCreateForm(BootstrapMixin, forms.Form):
|
class ConsolePortCreateForm(DeviceComponentForm):
|
||||||
name_pattern = ExpandableNameField(label='Name')
|
name_pattern = ExpandableNameField(label='Name')
|
||||||
|
|
||||||
|
|
||||||
@ -914,7 +924,7 @@ class ConsoleServerPortForm(BootstrapMixin, forms.ModelForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class ConsoleServerPortCreateForm(BootstrapMixin, forms.Form):
|
class ConsoleServerPortCreateForm(DeviceComponentForm):
|
||||||
name_pattern = ExpandableNameField(label='Name')
|
name_pattern = ExpandableNameField(label='Name')
|
||||||
|
|
||||||
|
|
||||||
@ -1012,7 +1022,7 @@ class PowerPortForm(BootstrapMixin, forms.ModelForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class PowerPortCreateForm(BootstrapMixin, forms.Form):
|
class PowerPortCreateForm(DeviceComponentForm):
|
||||||
name_pattern = ExpandableNameField(label='Name')
|
name_pattern = ExpandableNameField(label='Name')
|
||||||
|
|
||||||
|
|
||||||
@ -1181,7 +1191,7 @@ class PowerOutletForm(BootstrapMixin, forms.ModelForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class PowerOutletCreateForm(BootstrapMixin, forms.Form):
|
class PowerOutletCreateForm(DeviceComponentForm):
|
||||||
name_pattern = ExpandableNameField(label='Name')
|
name_pattern = ExpandableNameField(label='Name')
|
||||||
|
|
||||||
|
|
||||||
@ -1273,27 +1283,65 @@ class InterfaceForm(BootstrapMixin, forms.ModelForm):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Interface
|
model = Interface
|
||||||
fields = ['device', 'name', 'form_factor', 'mac_address', 'mgmt_only', 'description']
|
fields = ['device', 'name', 'form_factor', 'lag', 'mac_address', 'mgmt_only', 'description']
|
||||||
widgets = {
|
widgets = {
|
||||||
'device': forms.HiddenInput(),
|
'device': forms.HiddenInput(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(InterfaceForm, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
class InterfaceCreateForm(BootstrapMixin, forms.Form):
|
# Limit LAG choices to interfaces belonging to this device
|
||||||
|
if self.is_bound:
|
||||||
|
self.fields['lag'].queryset = Interface.objects.order_naturally().filter(
|
||||||
|
device_id=self.data['device'], form_factor=IFACE_FF_LAG
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.fields['lag'].queryset = Interface.objects.order_naturally().filter(
|
||||||
|
device=self.instance.device, form_factor=IFACE_FF_LAG
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class InterfaceCreateForm(DeviceComponentForm):
|
||||||
name_pattern = ExpandableNameField(label='Name')
|
name_pattern = ExpandableNameField(label='Name')
|
||||||
form_factor = forms.ChoiceField(choices=IFACE_FF_CHOICES)
|
form_factor = forms.ChoiceField(choices=IFACE_FF_CHOICES)
|
||||||
|
lag = forms.ModelChoiceField(queryset=Interface.objects.all(), required=False, label='Parent LAG')
|
||||||
mac_address = MACAddressFormField(required=False, label='MAC Address')
|
mac_address = MACAddressFormField(required=False, label='MAC Address')
|
||||||
mgmt_only = forms.BooleanField(required=False, label='OOB Management')
|
mgmt_only = forms.BooleanField(required=False, label='OOB Management')
|
||||||
description = forms.CharField(max_length=100, required=False)
|
description = forms.CharField(max_length=100, required=False)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(InterfaceCreateForm, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
# Limit LAG choices to interfaces belonging to this device
|
||||||
|
if self.device is not None:
|
||||||
|
self.fields['lag'].queryset = Interface.objects.order_naturally().filter(
|
||||||
|
device=self.device, form_factor=IFACE_FF_LAG
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.fields['lag'].queryset = Interface.objects.none()
|
||||||
|
|
||||||
|
|
||||||
class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
|
class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||||
pk = forms.ModelMultipleChoiceField(queryset=Interface.objects.all(), widget=forms.MultipleHiddenInput)
|
pk = forms.ModelMultipleChoiceField(queryset=Interface.objects.all(), widget=forms.MultipleHiddenInput)
|
||||||
|
device = forms.ModelChoiceField(queryset=Device.objects.all(), widget=forms.HiddenInput)
|
||||||
|
lag = forms.ModelChoiceField(queryset=Interface.objects.all(), required=False, label='Parent LAG')
|
||||||
form_factor = forms.ChoiceField(choices=add_blank_choice(IFACE_FF_CHOICES), required=False)
|
form_factor = forms.ChoiceField(choices=add_blank_choice(IFACE_FF_CHOICES), required=False)
|
||||||
description = forms.CharField(max_length=100, required=False)
|
description = forms.CharField(max_length=100, required=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
nullable_fields = ['description']
|
nullable_fields = ['lag', 'description']
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(InterfaceBulkEditForm, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
# Limit LAG choices to interfaces which belong to the parent device.
|
||||||
|
if self.initial.get('device'):
|
||||||
|
self.fields['lag'].queryset = Interface.objects.filter(
|
||||||
|
device=self.initial['device'], form_factor=IFACE_FF_LAG
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.fields['lag'].choices = []
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -1360,8 +1408,11 @@ class InterfaceConnectionForm(BootstrapMixin, forms.ModelForm):
|
|||||||
super(InterfaceConnectionForm, self).__init__(*args, **kwargs)
|
super(InterfaceConnectionForm, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
# Initialize interface A choices
|
# Initialize interface A choices
|
||||||
device_a_interfaces = Interface.objects.filter(device=device_a).exclude(form_factor=IFACE_FF_VIRTUAL)\
|
device_a_interfaces = Interface.objects.filter(device=device_a).exclude(
|
||||||
.select_related('circuit_termination', 'connected_as_a', 'connected_as_b')
|
form_factor__in=VIRTUAL_IFACE_TYPES
|
||||||
|
).select_related(
|
||||||
|
'circuit_termination', 'connected_as_a', 'connected_as_b'
|
||||||
|
)
|
||||||
self.fields['interface_a'].choices = [
|
self.fields['interface_a'].choices = [
|
||||||
(iface.id, {'label': iface.name, 'disabled': iface.is_connected}) for iface in device_a_interfaces
|
(iface.id, {'label': iface.name, 'disabled': iface.is_connected}) for iface in device_a_interfaces
|
||||||
]
|
]
|
||||||
@ -1388,13 +1439,17 @@ class InterfaceConnectionForm(BootstrapMixin, forms.ModelForm):
|
|||||||
|
|
||||||
# Initialize interface_b choices if device_b is set
|
# Initialize interface_b choices if device_b is set
|
||||||
if self.is_bound:
|
if self.is_bound:
|
||||||
device_b_interfaces = Interface.objects.filter(device=self.data['device_b'])\
|
device_b_interfaces = Interface.objects.filter(device=self.data['device_b']).exclude(
|
||||||
.exclude(form_factor=IFACE_FF_VIRTUAL)\
|
form_factor__in=VIRTUAL_IFACE_TYPES
|
||||||
.select_related('circuit_termination', 'connected_as_a', 'connected_as_b')
|
).select_related(
|
||||||
|
'circuit_termination', 'connected_as_a', 'connected_as_b'
|
||||||
|
)
|
||||||
elif self.initial.get('device_b'):
|
elif self.initial.get('device_b'):
|
||||||
device_b_interfaces = Interface.objects.filter(device=self.initial['device_b'])\
|
device_b_interfaces = Interface.objects.filter(device=self.initial['device_b']).exclude(
|
||||||
.exclude(form_factor=IFACE_FF_VIRTUAL)\
|
form_factor__in=VIRTUAL_IFACE_TYPES
|
||||||
.select_related('circuit_termination', 'connected_as_a', 'connected_as_b')
|
).select_related(
|
||||||
|
'circuit_termination', 'connected_as_a', 'connected_as_b'
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
device_b_interfaces = []
|
device_b_interfaces = []
|
||||||
self.fields['interface_b'].choices = [
|
self.fields['interface_b'].choices = [
|
||||||
@ -1512,7 +1567,7 @@ class DeviceBayForm(BootstrapMixin, forms.ModelForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class DeviceBayCreateForm(BootstrapMixin, forms.Form):
|
class DeviceBayCreateForm(DeviceComponentForm):
|
||||||
name_pattern = ExpandableNameField(label='Name')
|
name_pattern = ExpandableNameField(label='Name')
|
||||||
|
|
||||||
|
|
||||||
|
31
netbox/dcim/migrations/0030_interface_add_lag.py
Normal file
31
netbox/dcim/migrations/0030_interface_add_lag.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.4 on 2017-02-27 19:55
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('dcim', '0029_allow_rackless_devices'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='interface',
|
||||||
|
name='lag',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='member_interfaces', to='dcim.Interface', verbose_name=b'Parent LAG'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='interface',
|
||||||
|
name='form_factor',
|
||||||
|
field=models.PositiveSmallIntegerField(choices=[[b'Virtual interfaces', [[0, b'Virtual'], [200, b'Link Aggregation Group (LAG)']]], [b'Ethernet (fixed)', [[800, b'100BASE-TX (10/100ME)'], [1000, b'1000BASE-T (1GE)'], [1150, b'10GBASE-T (10GE)']]], [b'Ethernet (modular)', [[1050, b'GBIC (1GE)'], [1100, b'SFP (1GE)'], [1200, b'SFP+ (10GE)'], [1300, b'XFP (10GE)'], [1310, b'XENPAK (10GE)'], [1320, b'X2 (10GE)'], [1350, b'SFP28 (25GE)'], [1400, b'QSFP+ (40GE)'], [1500, b'CFP (100GE)'], [1600, b'QSFP28 (100GE)']]], [b'FibreChannel', [[3010, b'SFP (1GFC)'], [3020, b'SFP (2GFC)'], [3040, b'SFP (4GFC)'], [3080, b'SFP+ (8GFC)'], [3160, b'SFP+ (16GFC)']]], [b'Serial', [[4000, b'T1 (1.544 Mbps)'], [4010, b'E1 (2.048 Mbps)'], [4040, b'T3 (45 Mbps)'], [4050, b'E3 (34 Mbps)'], [4050, b'E3 (34 Mbps)']]], [b'Stacking', [[5000, b'Cisco StackWise'], [5050, b'Cisco StackWise Plus'], [5100, b'Cisco FlexStack'], [5150, b'Cisco FlexStack Plus']]], [b'Other', [[32767, b'Other']]]], default=1200),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='interfacetemplate',
|
||||||
|
name='form_factor',
|
||||||
|
field=models.PositiveSmallIntegerField(choices=[[b'Virtual interfaces', [[0, b'Virtual'], [200, b'Link Aggregation Group (LAG)']]], [b'Ethernet (fixed)', [[800, b'100BASE-TX (10/100ME)'], [1000, b'1000BASE-T (1GE)'], [1150, b'10GBASE-T (10GE)']]], [b'Ethernet (modular)', [[1050, b'GBIC (1GE)'], [1100, b'SFP (1GE)'], [1200, b'SFP+ (10GE)'], [1300, b'XFP (10GE)'], [1310, b'XENPAK (10GE)'], [1320, b'X2 (10GE)'], [1350, b'SFP28 (25GE)'], [1400, b'QSFP+ (40GE)'], [1500, b'CFP (100GE)'], [1600, b'QSFP28 (100GE)']]], [b'FibreChannel', [[3010, b'SFP (1GFC)'], [3020, b'SFP (2GFC)'], [3040, b'SFP (4GFC)'], [3080, b'SFP+ (8GFC)'], [3160, b'SFP+ (16GFC)']]], [b'Serial', [[4000, b'T1 (1.544 Mbps)'], [4010, b'E1 (2.048 Mbps)'], [4040, b'T3 (45 Mbps)'], [4050, b'E3 (34 Mbps)'], [4050, b'E3 (34 Mbps)']]], [b'Stacking', [[5000, b'Cisco StackWise'], [5050, b'Cisco StackWise Plus'], [5100, b'Cisco FlexStack'], [5150, b'Cisco FlexStack Plus']]], [b'Other', [[32767, b'Other']]]], default=1200),
|
||||||
|
),
|
||||||
|
]
|
@ -68,6 +68,7 @@ IFACE_ORDERING_CHOICES = [
|
|||||||
|
|
||||||
# Virtual
|
# Virtual
|
||||||
IFACE_FF_VIRTUAL = 0
|
IFACE_FF_VIRTUAL = 0
|
||||||
|
IFACE_FF_LAG = 200
|
||||||
# Ethernet
|
# Ethernet
|
||||||
IFACE_FF_100ME_FIXED = 800
|
IFACE_FF_100ME_FIXED = 800
|
||||||
IFACE_FF_1GE_FIXED = 1000
|
IFACE_FF_1GE_FIXED = 1000
|
||||||
@ -106,6 +107,7 @@ IFACE_FF_CHOICES = [
|
|||||||
'Virtual interfaces',
|
'Virtual interfaces',
|
||||||
[
|
[
|
||||||
[IFACE_FF_VIRTUAL, 'Virtual'],
|
[IFACE_FF_VIRTUAL, 'Virtual'],
|
||||||
|
[IFACE_FF_LAG, 'Link Aggregation Group (LAG)'],
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
@ -148,6 +150,7 @@ IFACE_FF_CHOICES = [
|
|||||||
[IFACE_FF_E1, 'E1 (2.048 Mbps)'],
|
[IFACE_FF_E1, 'E1 (2.048 Mbps)'],
|
||||||
[IFACE_FF_T3, 'T3 (45 Mbps)'],
|
[IFACE_FF_T3, 'T3 (45 Mbps)'],
|
||||||
[IFACE_FF_E3, 'E3 (34 Mbps)'],
|
[IFACE_FF_E3, 'E3 (34 Mbps)'],
|
||||||
|
[IFACE_FF_E3, 'E3 (34 Mbps)'],
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
@ -167,6 +170,11 @@ IFACE_FF_CHOICES = [
|
|||||||
],
|
],
|
||||||
]
|
]
|
||||||
|
|
||||||
|
VIRTUAL_IFACE_TYPES = [
|
||||||
|
IFACE_FF_VIRTUAL,
|
||||||
|
IFACE_FF_LAG,
|
||||||
|
]
|
||||||
|
|
||||||
STATUS_ACTIVE = True
|
STATUS_ACTIVE = True
|
||||||
STATUS_OFFLINE = False
|
STATUS_OFFLINE = False
|
||||||
STATUS_CHOICES = [
|
STATUS_CHOICES = [
|
||||||
@ -1062,6 +1070,10 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
return RPC_CLIENTS.get(self.platform.rpc_client)
|
return RPC_CLIENTS.get(self.platform.rpc_client)
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Console ports
|
||||||
|
#
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
class ConsolePort(models.Model):
|
class ConsolePort(models.Model):
|
||||||
"""
|
"""
|
||||||
@ -1091,6 +1103,10 @@ class ConsolePort(models.Model):
|
|||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Console server ports
|
||||||
|
#
|
||||||
|
|
||||||
class ConsoleServerPortManager(models.Manager):
|
class ConsoleServerPortManager(models.Manager):
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
@ -1123,6 +1139,10 @@ class ConsoleServerPort(models.Model):
|
|||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Power ports
|
||||||
|
#
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
class PowerPort(models.Model):
|
class PowerPort(models.Model):
|
||||||
"""
|
"""
|
||||||
@ -1152,6 +1172,10 @@ class PowerPort(models.Model):
|
|||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Power outlets
|
||||||
|
#
|
||||||
|
|
||||||
class PowerOutletManager(models.Manager):
|
class PowerOutletManager(models.Manager):
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
@ -1178,6 +1202,10 @@ class PowerOutlet(models.Model):
|
|||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Interfaces
|
||||||
|
#
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
class Interface(models.Model):
|
class Interface(models.Model):
|
||||||
"""
|
"""
|
||||||
@ -1185,6 +1213,8 @@ class Interface(models.Model):
|
|||||||
of an InterfaceConnection.
|
of an InterfaceConnection.
|
||||||
"""
|
"""
|
||||||
device = models.ForeignKey('Device', related_name='interfaces', on_delete=models.CASCADE)
|
device = models.ForeignKey('Device', related_name='interfaces', on_delete=models.CASCADE)
|
||||||
|
lag = models.ForeignKey('self', related_name='member_interfaces', null=True, blank=True, on_delete=models.SET_NULL,
|
||||||
|
verbose_name='Parent LAG')
|
||||||
name = models.CharField(max_length=30)
|
name = models.CharField(max_length=30)
|
||||||
form_factor = models.PositiveSmallIntegerField(choices=IFACE_FF_CHOICES, default=IFACE_FF_10GE_SFP_PLUS)
|
form_factor = models.PositiveSmallIntegerField(choices=IFACE_FF_CHOICES, default=IFACE_FF_10GE_SFP_PLUS)
|
||||||
mac_address = MACAddressField(null=True, blank=True, verbose_name='MAC Address')
|
mac_address = MACAddressField(null=True, blank=True, verbose_name='MAC Address')
|
||||||
@ -1203,15 +1233,42 @@ class Interface(models.Model):
|
|||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
|
||||||
if self.form_factor == IFACE_FF_VIRTUAL and self.is_connected:
|
# Virtual interfaces cannot be connected
|
||||||
|
if self.form_factor in VIRTUAL_IFACE_TYPES and self.is_connected:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'form_factor': "Virtual interfaces cannot be connected to another interface or circuit. Disconnect the "
|
'form_factor': "Virtual interfaces cannot be connected to another interface or circuit. Disconnect the "
|
||||||
"interface or choose a physical form factor."
|
"interface or choose a physical form factor."
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# An interface's LAG must belong to the same device
|
||||||
|
if self.lag and self.lag.device != self.device:
|
||||||
|
raise ValidationError({
|
||||||
|
'lag': u"The selected LAG interface ({}) belongs to a different device ({}).".format(
|
||||||
|
self.lag.name, self.lag.device.name
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
# A LAG interface cannot have a parent LAG
|
||||||
|
if self.form_factor == IFACE_FF_LAG and self.lag is not None:
|
||||||
|
raise ValidationError({
|
||||||
|
'lag': u"{} interfaces cannot have a parent LAG interface.".format(self.get_form_factor_display())
|
||||||
|
})
|
||||||
|
|
||||||
|
# Only a LAG can have LAG members
|
||||||
|
if self.form_factor != IFACE_FF_LAG and self.member_interfaces.exists():
|
||||||
|
raise ValidationError({
|
||||||
|
'form_factor': "Cannot change interface form factor; it has LAG members ({}).".format(
|
||||||
|
u", ".join([iface.name for iface in self.member_interfaces.all()])
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_physical(self):
|
def is_virtual(self):
|
||||||
return self.form_factor != IFACE_FF_VIRTUAL
|
return self.form_factor in VIRTUAL_IFACE_TYPES
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_lag(self):
|
||||||
|
return self.form_factor == IFACE_FF_LAG
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_connected(self):
|
def is_connected(self):
|
||||||
@ -1275,6 +1332,10 @@ class InterfaceConnection(models.Model):
|
|||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Device bays
|
||||||
|
#
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
class DeviceBay(models.Model):
|
class DeviceBay(models.Model):
|
||||||
"""
|
"""
|
||||||
@ -1305,6 +1366,10 @@ class DeviceBay(models.Model):
|
|||||||
raise ValidationError("Cannot install a device into itself.")
|
raise ValidationError("Cannot install a device into itself.")
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Modules
|
||||||
|
#
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
class Module(models.Model):
|
class Module(models.Model):
|
||||||
"""
|
"""
|
||||||
|
@ -561,6 +561,7 @@ class InterfaceTest(APITestCase):
|
|||||||
'device',
|
'device',
|
||||||
'name',
|
'name',
|
||||||
'form_factor',
|
'form_factor',
|
||||||
|
'lag',
|
||||||
'mac_address',
|
'mac_address',
|
||||||
'mgmt_only',
|
'mgmt_only',
|
||||||
'description',
|
'description',
|
||||||
@ -574,6 +575,7 @@ class InterfaceTest(APITestCase):
|
|||||||
'device',
|
'device',
|
||||||
'name',
|
'name',
|
||||||
'form_factor',
|
'form_factor',
|
||||||
|
'lag',
|
||||||
'mac_address',
|
'mac_address',
|
||||||
'mgmt_only',
|
'mgmt_only',
|
||||||
'description',
|
'description',
|
||||||
|
@ -66,11 +66,12 @@ class ComponentCreateView(View):
|
|||||||
def get(self, request, pk):
|
def get(self, request, pk):
|
||||||
|
|
||||||
parent = get_object_or_404(self.parent_model, pk=pk)
|
parent = get_object_or_404(self.parent_model, pk=pk)
|
||||||
|
form = self.form(parent, initial=request.GET)
|
||||||
|
|
||||||
return render(request, 'dcim/device_component_add.html', {
|
return render(request, 'dcim/device_component_add.html', {
|
||||||
'parent': parent,
|
'parent': parent,
|
||||||
'component_type': self.model._meta.verbose_name,
|
'component_type': self.model._meta.verbose_name,
|
||||||
'form': self.form(initial=request.GET),
|
'form': form,
|
||||||
'return_url': parent.get_absolute_url(),
|
'return_url': parent.get_absolute_url(),
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -78,7 +79,7 @@ class ComponentCreateView(View):
|
|||||||
|
|
||||||
parent = get_object_or_404(self.parent_model, pk=pk)
|
parent = get_object_or_404(self.parent_model, pk=pk)
|
||||||
|
|
||||||
form = self.form(request.POST)
|
form = self.form(parent, request.POST)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
|
|
||||||
new_components = []
|
new_components = []
|
||||||
|
@ -262,13 +262,13 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
|
|
||||||
|
|
||||||
class VLANGroupFilter(django_filters.FilterSet):
|
class VLANGroupFilter(django_filters.FilterSet):
|
||||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
site_id = NullableModelMultipleChoiceFilter(
|
||||||
name='site',
|
name='site',
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
label='Site (ID)',
|
label='Site (ID)',
|
||||||
)
|
)
|
||||||
site = django_filters.ModelMultipleChoiceFilter(
|
site = NullableModelMultipleChoiceFilter(
|
||||||
name='site__slug',
|
name='site',
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Site (slug)',
|
label='Site (slug)',
|
||||||
@ -283,13 +283,13 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
action='search',
|
action='search',
|
||||||
label='Search',
|
label='Search',
|
||||||
)
|
)
|
||||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
site_id = NullableModelMultipleChoiceFilter(
|
||||||
name='site',
|
name='site',
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
label='Site (ID)',
|
label='Site (ID)',
|
||||||
)
|
)
|
||||||
site = django_filters.ModelMultipleChoiceFilter(
|
site = NullableModelMultipleChoiceFilter(
|
||||||
name='site__slug',
|
name='site',
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Site (slug)',
|
label='Site (slug)',
|
||||||
|
@ -153,7 +153,7 @@ class RoleForm(BootstrapMixin, forms.ModelForm):
|
|||||||
|
|
||||||
class PrefixForm(BootstrapMixin, CustomFieldForm):
|
class PrefixForm(BootstrapMixin, CustomFieldForm):
|
||||||
site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False, label='Site',
|
site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False, label='Site',
|
||||||
widget=forms.Select(attrs={'filter-for': 'vlan'}))
|
widget=forms.Select(attrs={'filter-for': 'vlan', 'nullable': 'true'}))
|
||||||
vlan = forms.ModelChoiceField(queryset=VLAN.objects.all(), required=False, label='VLAN',
|
vlan = forms.ModelChoiceField(queryset=VLAN.objects.all(), required=False, label='VLAN',
|
||||||
widget=APISelect(api_url='/api/ipam/vlans/?site_id={{site}}',
|
widget=APISelect(api_url='/api/ipam/vlans/?site_id={{site}}',
|
||||||
display_field='display_name'))
|
display_field='display_name'))
|
||||||
@ -173,7 +173,7 @@ class PrefixForm(BootstrapMixin, CustomFieldForm):
|
|||||||
elif self.initial.get('site'):
|
elif self.initial.get('site'):
|
||||||
self.fields['vlan'].queryset = VLAN.objects.filter(site=self.initial['site'])
|
self.fields['vlan'].queryset = VLAN.objects.filter(site=self.initial['site'])
|
||||||
else:
|
else:
|
||||||
self.fields['vlan'].choices = []
|
self.fields['vlan'].queryset = VLAN.objects.filter(site=None)
|
||||||
|
|
||||||
|
|
||||||
class PrefixFromCSVForm(forms.ModelForm):
|
class PrefixFromCSVForm(forms.ModelForm):
|
||||||
@ -508,7 +508,11 @@ class VLANGroupForm(BootstrapMixin, forms.ModelForm):
|
|||||||
|
|
||||||
|
|
||||||
class VLANGroupFilterForm(BootstrapMixin, forms.Form):
|
class VLANGroupFilterForm(BootstrapMixin, forms.Form):
|
||||||
site = FilterChoiceField(queryset=Site.objects.annotate(filter_count=Count('vlan_groups')), to_field_name='slug')
|
site = FilterChoiceField(
|
||||||
|
queryset=Site.objects.annotate(filter_count=Count('vlan_groups')),
|
||||||
|
to_field_name='slug',
|
||||||
|
null_option=(0, 'Global')
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -524,7 +528,7 @@ class VLANForm(BootstrapMixin, CustomFieldForm):
|
|||||||
model = VLAN
|
model = VLAN
|
||||||
fields = ['site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description']
|
fields = ['site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description']
|
||||||
help_texts = {
|
help_texts = {
|
||||||
'site': "The site at which this VLAN exists",
|
'site': "Leave blank if this VLAN spans multiple sites",
|
||||||
'group': "VLAN group (optional)",
|
'group': "VLAN group (optional)",
|
||||||
'vid': "Configured VLAN ID",
|
'vid': "Configured VLAN ID",
|
||||||
'name': "Configured VLAN name",
|
'name': "Configured VLAN name",
|
||||||
@ -532,7 +536,7 @@ class VLANForm(BootstrapMixin, CustomFieldForm):
|
|||||||
'role': "The primary function of this VLAN",
|
'role': "The primary function of this VLAN",
|
||||||
}
|
}
|
||||||
widgets = {
|
widgets = {
|
||||||
'site': forms.Select(attrs={'filter-for': 'group'}),
|
'site': forms.Select(attrs={'filter-for': 'group', 'nullable': 'true'}),
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@ -545,11 +549,11 @@ class VLANForm(BootstrapMixin, CustomFieldForm):
|
|||||||
elif self.initial.get('site'):
|
elif self.initial.get('site'):
|
||||||
self.fields['group'].queryset = VLANGroup.objects.filter(site=self.initial['site'])
|
self.fields['group'].queryset = VLANGroup.objects.filter(site=self.initial['site'])
|
||||||
else:
|
else:
|
||||||
self.fields['group'].choices = []
|
self.fields['group'].queryset = VLANGroup.objects.filter(site=None)
|
||||||
|
|
||||||
|
|
||||||
class VLANFromCSVForm(forms.ModelForm):
|
class VLANFromCSVForm(forms.ModelForm):
|
||||||
site = forms.ModelChoiceField(queryset=Site.objects.all(), to_field_name='name',
|
site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False, to_field_name='name',
|
||||||
error_messages={'invalid_choice': 'Site not found.'})
|
error_messages={'invalid_choice': 'Site not found.'})
|
||||||
group = forms.ModelChoiceField(queryset=VLANGroup.objects.all(), required=False, to_field_name='name',
|
group = forms.ModelChoiceField(queryset=VLANGroup.objects.all(), required=False, to_field_name='name',
|
||||||
error_messages={'invalid_choice': 'VLAN group not found.'})
|
error_messages={'invalid_choice': 'VLAN group not found.'})
|
||||||
@ -599,7 +603,8 @@ def vlan_status_choices():
|
|||||||
class VLANFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
class VLANFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||||
model = VLAN
|
model = VLAN
|
||||||
q = forms.CharField(required=False, label='Search')
|
q = forms.CharField(required=False, label='Search')
|
||||||
site = FilterChoiceField(queryset=Site.objects.annotate(filter_count=Count('vlans')), to_field_name='slug')
|
site = FilterChoiceField(queryset=Site.objects.annotate(filter_count=Count('vlans')), to_field_name='slug',
|
||||||
|
null_option=(0, 'Global'))
|
||||||
group_id = FilterChoiceField(queryset=VLANGroup.objects.annotate(filter_count=Count('vlans')), label='VLAN group',
|
group_id = FilterChoiceField(queryset=VLANGroup.objects.annotate(filter_count=Count('vlans')), label='VLAN group',
|
||||||
null_option=(0, 'None'))
|
null_option=(0, 'None'))
|
||||||
tenant = FilterChoiceField(queryset=Tenant.objects.annotate(filter_count=Count('vlans')), to_field_name='slug',
|
tenant = FilterChoiceField(queryset=Tenant.objects.annotate(filter_count=Count('vlans')), to_field_name='slug',
|
||||||
|
26
netbox/ipam/migrations/0015_global_vlans.py
Normal file
26
netbox/ipam/migrations/0015_global_vlans.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.4 on 2017-02-21 18:45
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('ipam', '0014_ipaddress_status_add_deprecated'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='vlan',
|
||||||
|
name='site',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='vlans', to='dcim.Site'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='vlangroup',
|
||||||
|
name='site',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='vlan_groups', to='dcim.Site'),
|
||||||
|
),
|
||||||
|
]
|
@ -485,7 +485,7 @@ class VLANGroup(models.Model):
|
|||||||
"""
|
"""
|
||||||
name = models.CharField(max_length=50)
|
name = models.CharField(max_length=50)
|
||||||
slug = models.SlugField()
|
slug = models.SlugField()
|
||||||
site = models.ForeignKey('dcim.Site', related_name='vlan_groups')
|
site = models.ForeignKey('dcim.Site', related_name='vlan_groups', on_delete=models.PROTECT, blank=True, null=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['site', 'name']
|
ordering = ['site', 'name']
|
||||||
@ -497,6 +497,8 @@ class VLANGroup(models.Model):
|
|||||||
verbose_name_plural = 'VLAN groups'
|
verbose_name_plural = 'VLAN groups'
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
if self.site is None:
|
||||||
|
return self.name
|
||||||
return u'{} - {}'.format(self.site.name, self.name)
|
return u'{} - {}'.format(self.site.name, self.name)
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
@ -513,7 +515,7 @@ class VLAN(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
Like Prefixes, each VLAN is assigned an operational status and optionally a user-defined Role. A VLAN can have zero
|
Like Prefixes, each VLAN is assigned an operational status and optionally a user-defined Role. A VLAN can have zero
|
||||||
or more Prefixes assigned to it.
|
or more Prefixes assigned to it.
|
||||||
"""
|
"""
|
||||||
site = models.ForeignKey('dcim.Site', related_name='vlans', on_delete=models.PROTECT)
|
site = models.ForeignKey('dcim.Site', related_name='vlans', on_delete=models.PROTECT, blank=True, null=True)
|
||||||
group = models.ForeignKey('VLANGroup', related_name='vlans', blank=True, null=True, on_delete=models.PROTECT)
|
group = models.ForeignKey('VLANGroup', related_name='vlans', blank=True, null=True, on_delete=models.PROTECT)
|
||||||
vid = models.PositiveSmallIntegerField(verbose_name='ID', validators=[
|
vid = models.PositiveSmallIntegerField(verbose_name='ID', validators=[
|
||||||
MinValueValidator(1),
|
MinValueValidator(1),
|
||||||
@ -551,7 +553,7 @@ class VLAN(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
|
|
||||||
def to_csv(self):
|
def to_csv(self):
|
||||||
return csv_format([
|
return csv_format([
|
||||||
self.site.name,
|
self.site.name if self.site else None,
|
||||||
self.group.name if self.group else None,
|
self.group.name if self.group else None,
|
||||||
self.vid,
|
self.vid,
|
||||||
self.name,
|
self.name,
|
||||||
|
@ -297,9 +297,17 @@ def aggregate(request, pk):
|
|||||||
prefix_table.base_columns['pk'].visible = True
|
prefix_table.base_columns['pk'].visible = True
|
||||||
RequestConfig(request, paginate={'klass': EnhancedPaginator}).configure(prefix_table)
|
RequestConfig(request, paginate={'klass': EnhancedPaginator}).configure(prefix_table)
|
||||||
|
|
||||||
|
# Compile permissions list for rendering the object table
|
||||||
|
permissions = {
|
||||||
|
'add': request.user.has_perm('ipam.add_prefix'),
|
||||||
|
'change': request.user.has_perm('ipam.change_prefix'),
|
||||||
|
'delete': request.user.has_perm('ipam.delete_prefix'),
|
||||||
|
}
|
||||||
|
|
||||||
return render(request, 'ipam/aggregate.html', {
|
return render(request, 'ipam/aggregate.html', {
|
||||||
'aggregate': aggregate,
|
'aggregate': aggregate,
|
||||||
'prefix_table': prefix_table,
|
'prefix_table': prefix_table,
|
||||||
|
'permissions': permissions,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@ -425,6 +433,13 @@ def prefix(request, pk):
|
|||||||
child_prefix_table.base_columns['pk'].visible = True
|
child_prefix_table.base_columns['pk'].visible = True
|
||||||
RequestConfig(request, paginate={'klass': EnhancedPaginator}).configure(child_prefix_table)
|
RequestConfig(request, paginate={'klass': EnhancedPaginator}).configure(child_prefix_table)
|
||||||
|
|
||||||
|
# Compile permissions list for rendering the object table
|
||||||
|
permissions = {
|
||||||
|
'add': request.user.has_perm('ipam.add_prefix'),
|
||||||
|
'change': request.user.has_perm('ipam.change_prefix'),
|
||||||
|
'delete': request.user.has_perm('ipam.delete_prefix'),
|
||||||
|
}
|
||||||
|
|
||||||
return render(request, 'ipam/prefix.html', {
|
return render(request, 'ipam/prefix.html', {
|
||||||
'prefix': prefix,
|
'prefix': prefix,
|
||||||
'aggregate': aggregate,
|
'aggregate': aggregate,
|
||||||
@ -432,6 +447,7 @@ def prefix(request, pk):
|
|||||||
'parent_prefix_table': parent_prefix_table,
|
'parent_prefix_table': parent_prefix_table,
|
||||||
'child_prefix_table': child_prefix_table,
|
'child_prefix_table': child_prefix_table,
|
||||||
'duplicate_prefix_table': duplicate_prefix_table,
|
'duplicate_prefix_table': duplicate_prefix_table,
|
||||||
|
'permissions': permissions,
|
||||||
'return_url': prefix.get_absolute_url(),
|
'return_url': prefix.get_absolute_url(),
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -490,9 +506,17 @@ def prefix_ipaddresses(request, pk):
|
|||||||
ip_table.base_columns['pk'].visible = True
|
ip_table.base_columns['pk'].visible = True
|
||||||
RequestConfig(request, paginate={'klass': EnhancedPaginator}).configure(ip_table)
|
RequestConfig(request, paginate={'klass': EnhancedPaginator}).configure(ip_table)
|
||||||
|
|
||||||
|
# Compile permissions list for rendering the object table
|
||||||
|
permissions = {
|
||||||
|
'add': request.user.has_perm('ipam.add_ipaddress'),
|
||||||
|
'change': request.user.has_perm('ipam.change_ipaddress'),
|
||||||
|
'delete': request.user.has_perm('ipam.delete_ipaddress'),
|
||||||
|
}
|
||||||
|
|
||||||
return render(request, 'ipam/prefix_ipaddresses.html', {
|
return render(request, 'ipam/prefix_ipaddresses.html', {
|
||||||
'prefix': prefix,
|
'prefix': prefix,
|
||||||
'ip_table': ip_table,
|
'ip_table': ip_table,
|
||||||
|
'permissions': permissions,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@ -396,6 +396,7 @@
|
|||||||
{% if perms.dcim.delete_interface %}
|
{% if perms.dcim.delete_interface %}
|
||||||
<form method="post">
|
<form method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="device" value="{{ device.pk }}" />
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
{% block title %}Create {{ component_type }} ({{ parent }}){% endblock %}
|
{% block title %}Create {{ component_type }} ({{ parent }}){% endblock %}
|
||||||
|
|
||||||
{% block content %}{{ form.errors }}
|
{% block content %}
|
||||||
<form action="." method="post" class="form form-horizontal">
|
<form action="." method="post" class="form form-horizontal">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
@ -6,14 +6,20 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<td>
|
<td>
|
||||||
<i class="fa fa-fw fa-{{ icon|default:"exchange" }}"></i> <span title="{{ iface.get_form_factor_display }}">{{ iface.name }}</span>
|
<i class="fa fa-fw fa-{{ icon|default:"exchange" }}"></i> <span title="{{ iface.get_form_factor_display }}">{{ iface.name }}</span>
|
||||||
|
{% if iface.lag %}
|
||||||
|
<span class="label label-primary">{{ iface.lag.name }}</span>
|
||||||
|
{% endif %}
|
||||||
{% if iface.description %}
|
{% if iface.description %}
|
||||||
<i class="fa fa-fw fa-comment-o" title="{{ iface.description }}"></i>
|
<i class="fa fa-fw fa-comment-o" title="{{ iface.description }}"></i>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if iface.is_lag %}
|
||||||
|
<br /><small class="text-muted">{{ iface.member_interfaces.all|join:", "|default:"No members" }}</small>
|
||||||
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<small>{{ iface.mac_address|default:'' }}</small>
|
<small>{{ iface.mac_address|default:'' }}</small>
|
||||||
</td>
|
</td>
|
||||||
{% if not iface.is_physical %}
|
{% if iface.is_virtual %}
|
||||||
<td colspan="2" class="text-muted">Virtual interface</td>
|
<td colspan="2" class="text-muted">Virtual interface</td>
|
||||||
{% elif iface.connection %}
|
{% elif iface.connection %}
|
||||||
{% with iface.connected_interface as connected_iface %}
|
{% with iface.connected_interface as connected_iface %}
|
||||||
@ -48,7 +54,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.dcim.change_interface %}
|
{% if perms.dcim.change_interface %}
|
||||||
{% if iface.is_physical %}
|
{% if not iface.is_virtual %}
|
||||||
{% if iface.connection %}
|
{% if iface.connection %}
|
||||||
{% if iface.connection.connection_status %}
|
{% if iface.connection.connection_status %}
|
||||||
<a href="#" class="btn btn-warning btn-xs interface-toggle connected" data="{{ iface.connection.pk }}" title="Mark planned">
|
<a href="#" class="btn btn-warning btn-xs interface-toggle connected" data="{{ iface.connection.pk }}" title="Mark planned">
|
||||||
|
@ -8,9 +8,11 @@
|
|||||||
<div class="col-sm-8 col-md-9">
|
<div class="col-sm-8 col-md-9">
|
||||||
<ol class="breadcrumb">
|
<ol class="breadcrumb">
|
||||||
<li><a href="{% url 'ipam:vlan_list' %}">VLANs</a></li>
|
<li><a href="{% url 'ipam:vlan_list' %}">VLANs</a></li>
|
||||||
<li><a href="{% url 'ipam:vlan_list' %}?site={{ vlan.site.slug }}">{{ vlan.site }}</a></li>
|
{% if vlan.site %}
|
||||||
|
<li><a href="{% url 'ipam:vlan_list' %}?site={{ vlan.site.slug }}">{{ vlan.site }}</a></li>
|
||||||
|
{% endif %}
|
||||||
{% if vlan.group %}
|
{% if vlan.group %}
|
||||||
<li><a href="{% url 'ipam:vlan_list' %}?site={{ vlan.site.slug }}&group={{ vlan.group.slug }}">{{ vlan.group.name }}</a></li>
|
<li><a href="{% url 'ipam:vlan_list' %}?group={{ vlan.group.slug }}">{{ vlan.group.name }}</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li>{{ vlan.name }} ({{ vlan.vid }})</li>
|
<li>{{ vlan.name }} ({{ vlan.vid }})</li>
|
||||||
</ol>
|
</ol>
|
||||||
@ -53,7 +55,13 @@
|
|||||||
<table class="table table-hover panel-body attr-table">
|
<table class="table table-hover panel-body attr-table">
|
||||||
<tr>
|
<tr>
|
||||||
<td>Site</td>
|
<td>Site</td>
|
||||||
<td><a href="{% url 'dcim:site' slug=vlan.site.slug %}">{{ vlan.site }}</a></td>
|
<td>
|
||||||
|
{% if vlan.site %}
|
||||||
|
<a href="{% url 'dcim:site' slug=vlan.site.slug %}">{{ vlan.site }}</a>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">None</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Group</td>
|
<td>Group</td>
|
||||||
|
@ -307,11 +307,12 @@ class BulkAddView(View):
|
|||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
|
|
||||||
# The first field will be used as the pattern
|
# The first field will be used as the pattern
|
||||||
pattern_field = form.fields.keys()[0]
|
field_names = list(form.fields.keys())
|
||||||
|
pattern_field = field_names[0]
|
||||||
pattern = form.cleaned_data[pattern_field]
|
pattern = form.cleaned_data[pattern_field]
|
||||||
|
|
||||||
# All other fields will be copied as object attributes
|
# All other fields will be copied as object attributes
|
||||||
kwargs = {k: form.cleaned_data[k] for k in form.fields.keys()[1:]}
|
kwargs = {k: form.cleaned_data[k] for k in field_names[1:]}
|
||||||
|
|
||||||
new_objs = []
|
new_objs = []
|
||||||
try:
|
try:
|
||||||
@ -470,7 +471,9 @@ class BulkEditView(View):
|
|||||||
return redirect(return_url)
|
return redirect(return_url)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
form = self.form(self.cls, initial={'pk': pk_list})
|
initial_data = request.POST.copy()
|
||||||
|
initial_data['pk'] = pk_list
|
||||||
|
form = self.form(self.cls, initial=initial_data)
|
||||||
|
|
||||||
selected_objects = self.cls.objects.filter(pk__in=pk_list)
|
selected_objects = self.cls.objects.filter(pk__in=pk_list)
|
||||||
if not selected_objects:
|
if not selected_objects:
|
||||||
|
Reference in New Issue
Block a user