mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-23 04:22:01 -06:00
Merge pull request #8176 from netbox-community/7846-inventoryitem-component
Closes #7846: Associate inventory items with device components
This commit is contained in:
commit
e9910d1fe2
@ -48,6 +48,7 @@ FIELD_CHOICES = {
|
||||
* [#7681](https://github.com/netbox-community/netbox/issues/7681) - Add `service_id` field for provider networks
|
||||
* [#7759](https://github.com/netbox-community/netbox/issues/7759) - Improved the user preferences form
|
||||
* [#7784](https://github.com/netbox-community/netbox/issues/7784) - Support cluster type assignment for config contexts
|
||||
* [#7846](https://github.com/netbox-community/netbox/issues/7846) - Enable associating inventory items with device components
|
||||
* [#8168](https://github.com/netbox-community/netbox/issues/8168) - Add `min_vid` and `max_vid` fields to VLAN group
|
||||
|
||||
### Other Changes
|
||||
@ -76,7 +77,8 @@ FIELD_CHOICES = {
|
||||
* dcim.Interface
|
||||
* Added `module` field
|
||||
* dcim.InventoryItem
|
||||
* Added `role` field
|
||||
* Added `component_type`, `component_id`, and `role` fields
|
||||
* Added read-only `component` field
|
||||
* dcim.PowerPort
|
||||
* Added `module` field
|
||||
* dcim.PowerOutlet
|
||||
|
@ -810,17 +810,32 @@ class InventoryItemSerializer(PrimaryModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitem-detail')
|
||||
device = NestedDeviceSerializer()
|
||||
parent = serializers.PrimaryKeyRelatedField(queryset=InventoryItem.objects.all(), allow_null=True, default=None)
|
||||
manufacturer = NestedManufacturerSerializer(required=False, allow_null=True, default=None)
|
||||
role = NestedInventoryItemRoleSerializer(required=False, allow_null=True)
|
||||
manufacturer = NestedManufacturerSerializer(required=False, allow_null=True, default=None)
|
||||
component_type = ContentTypeField(
|
||||
queryset=ContentType.objects.filter(MODULAR_COMPONENT_MODELS),
|
||||
required=False,
|
||||
allow_null=True
|
||||
)
|
||||
component = serializers.SerializerMethodField(read_only=True)
|
||||
_depth = serializers.IntegerField(source='level', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = InventoryItem
|
||||
fields = [
|
||||
'id', 'url', 'display', 'device', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial',
|
||||
'asset_tag', 'discovered', 'description', 'tags', 'custom_fields', 'created', 'last_updated', '_depth',
|
||||
'asset_tag', 'discovered', 'description', 'component_type', 'component_id', 'component', 'tags',
|
||||
'custom_fields', 'created', 'last_updated', '_depth',
|
||||
]
|
||||
|
||||
@swagger_serializer_method(serializer_or_field=serializers.DictField)
|
||||
def get_component(self, obj):
|
||||
if obj.component is None:
|
||||
return None
|
||||
serializer = get_serializer_for_model(obj.component, prefix='Nested')
|
||||
context = {'request': self.context['request']}
|
||||
return serializer(obj.component, context=context).data
|
||||
|
||||
|
||||
#
|
||||
# Device component roles
|
||||
|
@ -50,16 +50,31 @@ NONCONNECTABLE_IFACE_TYPES = VIRTUAL_IFACE_TYPES + WIRELESS_IFACE_TYPES
|
||||
|
||||
|
||||
#
|
||||
# PowerFeeds
|
||||
# Power feeds
|
||||
#
|
||||
|
||||
POWERFEED_VOLTAGE_DEFAULT = 120
|
||||
|
||||
POWERFEED_AMPERAGE_DEFAULT = 20
|
||||
|
||||
POWERFEED_MAX_UTILIZATION_DEFAULT = 80 # Percentage
|
||||
|
||||
|
||||
#
|
||||
# Device components
|
||||
#
|
||||
|
||||
MODULAR_COMPONENT_MODELS = Q(
|
||||
app_label='dcim',
|
||||
model__in=(
|
||||
'consoleport',
|
||||
'consoleserverport',
|
||||
'frontport',
|
||||
'interface',
|
||||
'poweroutlet',
|
||||
'powerport',
|
||||
'rearport',
|
||||
))
|
||||
|
||||
|
||||
#
|
||||
# Cabling and connections
|
||||
#
|
||||
|
@ -1294,6 +1294,8 @@ class InventoryItemFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet):
|
||||
to_field_name='slug',
|
||||
label='Role (slug)',
|
||||
)
|
||||
component_type = ContentTypeFilter()
|
||||
component_id = MultiValueNumberFilter()
|
||||
serial = django_filters.CharFilter(
|
||||
lookup_expr='iexact'
|
||||
)
|
||||
|
@ -4,7 +4,7 @@ from dcim.models import *
|
||||
from extras.forms import CustomFieldsMixin
|
||||
from extras.models import Tag
|
||||
from utilities.forms import DynamicModelMultipleChoiceField, form_from_model
|
||||
from .object_create import ComponentForm
|
||||
from .object_create import ComponentCreateForm
|
||||
|
||||
__all__ = (
|
||||
'ConsolePortBulkCreateForm',
|
||||
@ -24,7 +24,7 @@ __all__ = (
|
||||
# Device components
|
||||
#
|
||||
|
||||
class DeviceBulkAddComponentForm(CustomFieldsMixin, ComponentForm):
|
||||
class DeviceBulkAddComponentForm(CustomFieldsMixin, ComponentCreateForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
widget=forms.MultipleHiddenInput()
|
||||
|
@ -12,8 +12,8 @@ from extras.models import Tag
|
||||
from ipam.models import IPAddress, VLAN, VLANGroup, ASN
|
||||
from tenancy.forms import TenancyForm
|
||||
from utilities.forms import (
|
||||
APISelect, add_blank_choice, BootstrapMixin, ClearableFileInput, CommentField, DynamicModelChoiceField,
|
||||
DynamicModelMultipleChoiceField, JSONField, NumericArrayField, SelectWithPK, SmallTextarea,
|
||||
APISelect, add_blank_choice, BootstrapMixin, ClearableFileInput, CommentField, ContentTypeChoiceField,
|
||||
DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField, NumericArrayField, SelectWithPK, SmallTextarea,
|
||||
SlugField, StaticSelect,
|
||||
)
|
||||
from virtualization.models import Cluster, ClusterGroup
|
||||
@ -957,6 +957,7 @@ class ConsolePortTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||
widgets = {
|
||||
'device_type': forms.HiddenInput(),
|
||||
'module_type': forms.HiddenInput(),
|
||||
'type': StaticSelect,
|
||||
}
|
||||
|
||||
|
||||
@ -969,6 +970,7 @@ class ConsoleServerPortTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||
widgets = {
|
||||
'device_type': forms.HiddenInput(),
|
||||
'module_type': forms.HiddenInput(),
|
||||
'type': StaticSelect,
|
||||
}
|
||||
|
||||
|
||||
@ -981,10 +983,19 @@ class PowerPortTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||
widgets = {
|
||||
'device_type': forms.HiddenInput(),
|
||||
'module_type': forms.HiddenInput(),
|
||||
'type': StaticSelect(),
|
||||
}
|
||||
|
||||
|
||||
class PowerOutletTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||
power_port = DynamicModelChoiceField(
|
||||
queryset=PowerPortTemplate.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'devicetype_id': '$device_type',
|
||||
}
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = PowerOutletTemplate
|
||||
fields = [
|
||||
@ -993,18 +1004,10 @@ class PowerOutletTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||
widgets = {
|
||||
'device_type': forms.HiddenInput(),
|
||||
'module_type': forms.HiddenInput(),
|
||||
'type': StaticSelect(),
|
||||
'feed_leg': StaticSelect(),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Limit power_port choices to current DeviceType/ModuleType
|
||||
if self.instance.pk:
|
||||
self.fields['power_port'].queryset = PowerPortTemplate.objects.filter(
|
||||
device_type=self.instance.device_type,
|
||||
module_type=self.instance.module_type
|
||||
)
|
||||
|
||||
|
||||
class InterfaceTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||
class Meta:
|
||||
@ -1020,6 +1023,14 @@ class InterfaceTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||
|
||||
|
||||
class FrontPortTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||
rear_port = DynamicModelChoiceField(
|
||||
queryset=RearPortTemplate.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'devicetype_id': '$device_type',
|
||||
}
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = FrontPortTemplate
|
||||
fields = [
|
||||
@ -1029,19 +1040,9 @@ class FrontPortTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||
widgets = {
|
||||
'device_type': forms.HiddenInput(),
|
||||
'module_type': forms.HiddenInput(),
|
||||
'rear_port': StaticSelect(),
|
||||
'type': StaticSelect(),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Limit rear_port choices to current DeviceType/ModuleType
|
||||
if self.instance.pk:
|
||||
self.fields['rear_port'].queryset = RearPortTemplate.objects.filter(
|
||||
device_type=self.instance.device_type,
|
||||
module_type=self.instance.module_type
|
||||
)
|
||||
|
||||
|
||||
class RearPortTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||
class Meta:
|
||||
@ -1095,6 +1096,8 @@ class ConsolePortForm(CustomFieldModelForm):
|
||||
]
|
||||
widgets = {
|
||||
'device': forms.HiddenInput(),
|
||||
'type': StaticSelect(),
|
||||
'speed': StaticSelect(),
|
||||
}
|
||||
|
||||
|
||||
@ -1111,6 +1114,8 @@ class ConsoleServerPortForm(CustomFieldModelForm):
|
||||
]
|
||||
widgets = {
|
||||
'device': forms.HiddenInput(),
|
||||
'type': StaticSelect(),
|
||||
'speed': StaticSelect(),
|
||||
}
|
||||
|
||||
|
||||
@ -1128,13 +1133,17 @@ class PowerPortForm(CustomFieldModelForm):
|
||||
]
|
||||
widgets = {
|
||||
'device': forms.HiddenInput(),
|
||||
'type': StaticSelect(),
|
||||
}
|
||||
|
||||
|
||||
class PowerOutletForm(CustomFieldModelForm):
|
||||
power_port = forms.ModelChoiceField(
|
||||
power_port = DynamicModelChoiceField(
|
||||
queryset=PowerPort.objects.all(),
|
||||
required=False
|
||||
required=False,
|
||||
query_params={
|
||||
'device_id': '$device',
|
||||
}
|
||||
)
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
@ -1148,34 +1157,34 @@ class PowerOutletForm(CustomFieldModelForm):
|
||||
]
|
||||
widgets = {
|
||||
'device': forms.HiddenInput(),
|
||||
'type': StaticSelect(),
|
||||
'feed_leg': StaticSelect(),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Limit power_port choices to the local device
|
||||
if hasattr(self.instance, 'device'):
|
||||
self.fields['power_port'].queryset = PowerPort.objects.filter(
|
||||
device=self.instance.device
|
||||
)
|
||||
|
||||
|
||||
class InterfaceForm(InterfaceCommonForm, CustomFieldModelForm):
|
||||
parent = DynamicModelChoiceField(
|
||||
queryset=Interface.objects.all(),
|
||||
required=False,
|
||||
label='Parent interface'
|
||||
label='Parent interface',
|
||||
query_params={
|
||||
'device_id': '$device',
|
||||
}
|
||||
)
|
||||
bridge = DynamicModelChoiceField(
|
||||
queryset=Interface.objects.all(),
|
||||
required=False,
|
||||
label='Bridged interface'
|
||||
label='Bridged interface',
|
||||
query_params={
|
||||
'device_id': '$device',
|
||||
}
|
||||
)
|
||||
lag = DynamicModelChoiceField(
|
||||
queryset=Interface.objects.all(),
|
||||
required=False,
|
||||
label='LAG interface',
|
||||
query_params={
|
||||
'device_id': '$device',
|
||||
'type': 'lag',
|
||||
}
|
||||
)
|
||||
@ -1203,6 +1212,7 @@ class InterfaceForm(InterfaceCommonForm, CustomFieldModelForm):
|
||||
label='Untagged VLAN',
|
||||
query_params={
|
||||
'group_id': '$vlan_group',
|
||||
'available_on_device': '$device',
|
||||
}
|
||||
)
|
||||
tagged_vlans = DynamicModelMultipleChoiceField(
|
||||
@ -1211,6 +1221,7 @@ class InterfaceForm(InterfaceCommonForm, CustomFieldModelForm):
|
||||
label='Tagged VLANs',
|
||||
query_params={
|
||||
'group_id': '$vlan_group',
|
||||
'available_on_device': '$device',
|
||||
}
|
||||
)
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
@ -1225,6 +1236,17 @@ class InterfaceForm(InterfaceCommonForm, CustomFieldModelForm):
|
||||
'mgmt_only', 'mark_connected', 'description', 'mode', 'rf_role', 'rf_channel', 'rf_channel_frequency',
|
||||
'rf_channel_width', 'tx_power', 'wireless_lans', 'untagged_vlan', 'tagged_vlans', 'tags',
|
||||
]
|
||||
fieldsets = (
|
||||
('Interface', ('device', 'name', 'type', 'label', 'description', 'tags')),
|
||||
('Addressing', ('mac_address', 'wwn')),
|
||||
('Operation', ('mtu', 'tx_power', 'enabled', 'mgmt_only', 'mark_connected')),
|
||||
('Related Interfaces', ('parent', 'bridge', 'lag')),
|
||||
('802.1Q Switching', ('mode', 'vlan_group', 'untagged_vlan', 'tagged_vlans')),
|
||||
('Wireless', (
|
||||
'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'wireless_lan_group',
|
||||
'wireless_lans',
|
||||
)),
|
||||
)
|
||||
widgets = {
|
||||
'device': forms.HiddenInput(),
|
||||
'type': StaticSelect(),
|
||||
@ -1241,26 +1263,14 @@ class InterfaceForm(InterfaceCommonForm, CustomFieldModelForm):
|
||||
'rf_channel_width': "Populated by selected channel (if set)",
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
device = Device.objects.get(pk=self.data['device']) if self.is_bound else self.instance.device
|
||||
|
||||
# Restrict parent/bridge/LAG interface assignment by device/VC
|
||||
self.fields['parent'].widget.add_query_param('device_id', device.pk)
|
||||
self.fields['bridge'].widget.add_query_param('device_id', device.pk)
|
||||
self.fields['lag'].widget.add_query_param('device_id', device.pk)
|
||||
if device.virtual_chassis and device.virtual_chassis.master:
|
||||
self.fields['parent'].widget.add_query_param('device_id', device.virtual_chassis.master.pk)
|
||||
self.fields['bridge'].widget.add_query_param('device_id', device.virtual_chassis.master.pk)
|
||||
self.fields['lag'].widget.add_query_param('device_id', device.virtual_chassis.master.pk)
|
||||
|
||||
# Limit VLAN choices by device
|
||||
self.fields['untagged_vlan'].widget.add_query_param('available_on_device', device.pk)
|
||||
self.fields['tagged_vlans'].widget.add_query_param('available_on_device', device.pk)
|
||||
|
||||
|
||||
class FrontPortForm(CustomFieldModelForm):
|
||||
rear_port = DynamicModelChoiceField(
|
||||
queryset=RearPort.objects.all(),
|
||||
query_params={
|
||||
'device_id': '$device',
|
||||
}
|
||||
)
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
@ -1275,18 +1285,8 @@ class FrontPortForm(CustomFieldModelForm):
|
||||
widgets = {
|
||||
'device': forms.HiddenInput(),
|
||||
'type': StaticSelect(),
|
||||
'rear_port': StaticSelect(),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Limit RearPort choices to the local device
|
||||
if hasattr(self.instance, 'device'):
|
||||
self.fields['rear_port'].queryset = self.fields['rear_port'].queryset.filter(
|
||||
device=self.instance.device
|
||||
)
|
||||
|
||||
|
||||
class RearPortForm(CustomFieldModelForm):
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
@ -1358,9 +1358,6 @@ class PopulateDeviceBayForm(BootstrapMixin, forms.Form):
|
||||
|
||||
|
||||
class InventoryItemForm(CustomFieldModelForm):
|
||||
device = DynamicModelChoiceField(
|
||||
queryset=Device.objects.all()
|
||||
)
|
||||
parent = DynamicModelChoiceField(
|
||||
queryset=InventoryItem.objects.all(),
|
||||
required=False,
|
||||
@ -1376,6 +1373,15 @@ class InventoryItemForm(CustomFieldModelForm):
|
||||
queryset=Manufacturer.objects.all(),
|
||||
required=False
|
||||
)
|
||||
component_type = ContentTypeChoiceField(
|
||||
queryset=ContentType.objects.all(),
|
||||
limit_choices_to=MODULAR_COMPONENT_MODELS,
|
||||
required=False,
|
||||
widget=StaticSelect
|
||||
)
|
||||
component_id = forms.IntegerField(
|
||||
required=False
|
||||
)
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
@ -1385,8 +1391,16 @@ class InventoryItemForm(CustomFieldModelForm):
|
||||
model = InventoryItem
|
||||
fields = [
|
||||
'device', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag',
|
||||
'description', 'tags',
|
||||
'description', 'component_type', 'component_id', 'tags',
|
||||
]
|
||||
fieldsets = (
|
||||
('Inventory Item', ('device', 'parent', 'name', 'label', 'role', 'description', 'tags')),
|
||||
('Hardware', ('manufacturer', 'part_id', 'serial', 'asset_tag')),
|
||||
('Component', ('component_type', 'component_id')),
|
||||
)
|
||||
widgets = {
|
||||
'device': forms.HiddenInput(),
|
||||
}
|
||||
|
||||
|
||||
#
|
||||
|
@ -1,43 +1,21 @@
|
||||
from django import forms
|
||||
|
||||
from dcim.choices import *
|
||||
from dcim.constants import *
|
||||
from dcim.models import *
|
||||
from extras.forms import CustomFieldModelForm, CustomFieldsMixin
|
||||
from extras.forms import CustomFieldModelForm
|
||||
from extras.models import Tag
|
||||
from ipam.models import VLAN
|
||||
from utilities.forms import (
|
||||
add_blank_choice, BootstrapMixin, ColorField, DynamicModelChoiceField, DynamicModelMultipleChoiceField,
|
||||
ExpandableNameField, StaticSelect,
|
||||
BootstrapMixin, DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableNameField,
|
||||
)
|
||||
from wireless.choices import *
|
||||
from .common import InterfaceCommonForm
|
||||
|
||||
__all__ = (
|
||||
'ConsolePortCreateForm',
|
||||
'ConsolePortTemplateCreateForm',
|
||||
'ConsoleServerPortCreateForm',
|
||||
'ConsoleServerPortTemplateCreateForm',
|
||||
'DeviceBayCreateForm',
|
||||
'DeviceBayTemplateCreateForm',
|
||||
'ComponentCreateForm',
|
||||
'FrontPortCreateForm',
|
||||
'FrontPortTemplateCreateForm',
|
||||
'InterfaceCreateForm',
|
||||
'InterfaceTemplateCreateForm',
|
||||
'InventoryItemCreateForm',
|
||||
'ModuleBayCreateForm',
|
||||
'ModuleBayTemplateCreateForm',
|
||||
'PowerOutletCreateForm',
|
||||
'PowerOutletTemplateCreateForm',
|
||||
'PowerPortCreateForm',
|
||||
'PowerPortTemplateCreateForm',
|
||||
'RearPortCreateForm',
|
||||
'RearPortTemplateCreateForm',
|
||||
'VirtualChassisCreateForm',
|
||||
)
|
||||
|
||||
|
||||
class ComponentForm(BootstrapMixin, forms.Form):
|
||||
class ComponentCreateForm(BootstrapMixin, forms.Form):
|
||||
"""
|
||||
Subclass this form when facilitating the creation of one or more device component or component templates based on
|
||||
a name pattern.
|
||||
@ -65,6 +43,97 @@ class ComponentForm(BootstrapMixin, forms.Form):
|
||||
}, code='label_pattern_mismatch')
|
||||
|
||||
|
||||
class FrontPortTemplateCreateForm(ComponentCreateForm):
|
||||
rear_port_set = forms.MultipleChoiceField(
|
||||
choices=[],
|
||||
label='Rear ports',
|
||||
help_text='Select one rear port assignment for each front port being created.',
|
||||
)
|
||||
field_order = (
|
||||
'name_pattern', 'label_pattern', 'rear_port_set',
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
device_type = DeviceType.objects.get(
|
||||
pk=self.initial.get('device_type') or self.data.get('device_type')
|
||||
)
|
||||
|
||||
# Determine which rear port positions are occupied. These will be excluded from the list of available mappings.
|
||||
occupied_port_positions = [
|
||||
(front_port.rear_port_id, front_port.rear_port_position)
|
||||
for front_port in device_type.frontporttemplates.all()
|
||||
]
|
||||
|
||||
# Populate rear port choices
|
||||
choices = []
|
||||
rear_ports = RearPortTemplate.objects.filter(device_type=device_type)
|
||||
for rear_port in rear_ports:
|
||||
for i in range(1, rear_port.positions + 1):
|
||||
if (rear_port.pk, i) not in occupied_port_positions:
|
||||
choices.append(
|
||||
('{}:{}'.format(rear_port.pk, i), '{}:{}'.format(rear_port.name, i))
|
||||
)
|
||||
self.fields['rear_port_set'].choices = choices
|
||||
|
||||
def get_iterative_data(self, iteration):
|
||||
|
||||
# Assign rear port and position from selected set
|
||||
rear_port, position = self.cleaned_data['rear_port_set'][iteration].split(':')
|
||||
|
||||
return {
|
||||
'rear_port': int(rear_port),
|
||||
'rear_port_position': int(position),
|
||||
}
|
||||
|
||||
|
||||
class FrontPortCreateForm(ComponentCreateForm):
|
||||
rear_port_set = forms.MultipleChoiceField(
|
||||
choices=[],
|
||||
label='Rear ports',
|
||||
help_text='Select one rear port assignment for each front port being created.',
|
||||
)
|
||||
field_order = (
|
||||
'name_pattern', 'label_pattern', 'rear_port_set',
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
device = Device.objects.get(
|
||||
pk=self.initial.get('device') or self.data.get('device')
|
||||
)
|
||||
|
||||
# Determine which rear port positions are occupied. These will be excluded from the list of available
|
||||
# mappings.
|
||||
occupied_port_positions = [
|
||||
(front_port.rear_port_id, front_port.rear_port_position)
|
||||
for front_port in device.frontports.all()
|
||||
]
|
||||
|
||||
# Populate rear port choices
|
||||
choices = []
|
||||
rear_ports = RearPort.objects.filter(device=device)
|
||||
for rear_port in rear_ports:
|
||||
for i in range(1, rear_port.positions + 1):
|
||||
if (rear_port.pk, i) not in occupied_port_positions:
|
||||
choices.append(
|
||||
('{}:{}'.format(rear_port.pk, i), '{}:{}'.format(rear_port.name, i))
|
||||
)
|
||||
self.fields['rear_port_set'].choices = choices
|
||||
|
||||
def get_iterative_data(self, iteration):
|
||||
|
||||
# Assign rear port and position from selected set
|
||||
rear_port, position = self.cleaned_data['rear_port_set'][iteration].split(':')
|
||||
|
||||
return {
|
||||
'rear_port': int(rear_port),
|
||||
'rear_port_position': int(position),
|
||||
}
|
||||
|
||||
|
||||
class VirtualChassisCreateForm(CustomFieldModelForm):
|
||||
region = DynamicModelChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
@ -138,549 +207,3 @@ class VirtualChassisCreateForm(CustomFieldModelForm):
|
||||
member.save()
|
||||
|
||||
return instance
|
||||
|
||||
|
||||
#
|
||||
# Component templates
|
||||
#
|
||||
|
||||
class ComponentTemplateCreateForm(ComponentForm):
|
||||
"""
|
||||
Base form for the creation of device component templates (subclassed from ComponentTemplateModel).
|
||||
"""
|
||||
manufacturer = DynamicModelChoiceField(
|
||||
queryset=Manufacturer.objects.all(),
|
||||
required=False,
|
||||
initial_params={
|
||||
'device_types': 'device_type',
|
||||
'module_types': 'module_type',
|
||||
}
|
||||
)
|
||||
device_type = DynamicModelChoiceField(
|
||||
queryset=DeviceType.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'manufacturer_id': '$manufacturer'
|
||||
}
|
||||
)
|
||||
description = forms.CharField(
|
||||
required=False
|
||||
)
|
||||
|
||||
|
||||
class ModularComponentTemplateCreateForm(ComponentTemplateCreateForm):
|
||||
module_type = DynamicModelChoiceField(
|
||||
queryset=ModuleType.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'manufacturer_id': '$manufacturer'
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class ConsolePortTemplateCreateForm(ModularComponentTemplateCreateForm):
|
||||
type = forms.ChoiceField(
|
||||
choices=add_blank_choice(ConsolePortTypeChoices),
|
||||
widget=StaticSelect()
|
||||
)
|
||||
field_order = (
|
||||
'manufacturer', 'device_type', 'module_type', 'name_pattern', 'label_pattern', 'type', 'description',
|
||||
)
|
||||
|
||||
|
||||
class ConsoleServerPortTemplateCreateForm(ModularComponentTemplateCreateForm):
|
||||
type = forms.ChoiceField(
|
||||
choices=add_blank_choice(ConsolePortTypeChoices),
|
||||
widget=StaticSelect()
|
||||
)
|
||||
field_order = (
|
||||
'manufacturer', 'device_type', 'module_type', 'name_pattern', 'label_pattern', 'type', 'description',
|
||||
)
|
||||
|
||||
|
||||
class PowerPortTemplateCreateForm(ModularComponentTemplateCreateForm):
|
||||
type = forms.ChoiceField(
|
||||
choices=add_blank_choice(PowerPortTypeChoices),
|
||||
required=False
|
||||
)
|
||||
maximum_draw = forms.IntegerField(
|
||||
min_value=1,
|
||||
required=False,
|
||||
help_text="Maximum power draw (watts)"
|
||||
)
|
||||
allocated_draw = forms.IntegerField(
|
||||
min_value=1,
|
||||
required=False,
|
||||
help_text="Allocated power draw (watts)"
|
||||
)
|
||||
field_order = (
|
||||
'manufacturer', 'device_type', 'module_type', 'name_pattern', 'label_pattern', 'type', 'maximum_draw',
|
||||
'allocated_draw', 'description',
|
||||
)
|
||||
|
||||
|
||||
class PowerOutletTemplateCreateForm(ModularComponentTemplateCreateForm):
|
||||
type = forms.ChoiceField(
|
||||
choices=add_blank_choice(PowerOutletTypeChoices),
|
||||
required=False
|
||||
)
|
||||
power_port = DynamicModelChoiceField(
|
||||
queryset=PowerPortTemplate.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'devicetype_id': '$device_type',
|
||||
'moduletype_id': '$module_type',
|
||||
}
|
||||
)
|
||||
feed_leg = forms.ChoiceField(
|
||||
choices=add_blank_choice(PowerOutletFeedLegChoices),
|
||||
required=False,
|
||||
widget=StaticSelect()
|
||||
)
|
||||
field_order = (
|
||||
'manufacturer', 'device_type', 'module_type', 'name_pattern', 'label_pattern', 'type', 'power_port', 'feed_leg',
|
||||
'description',
|
||||
)
|
||||
|
||||
|
||||
class InterfaceTemplateCreateForm(ModularComponentTemplateCreateForm):
|
||||
type = forms.ChoiceField(
|
||||
choices=InterfaceTypeChoices,
|
||||
widget=StaticSelect()
|
||||
)
|
||||
mgmt_only = forms.BooleanField(
|
||||
required=False,
|
||||
label='Management only'
|
||||
)
|
||||
field_order = (
|
||||
'manufacturer', 'device_type', 'module_type', 'name_pattern', 'label_pattern', 'type', 'mgmt_only',
|
||||
'description',
|
||||
)
|
||||
|
||||
|
||||
class FrontPortTemplateCreateForm(ModularComponentTemplateCreateForm):
|
||||
type = forms.ChoiceField(
|
||||
choices=PortTypeChoices,
|
||||
widget=StaticSelect()
|
||||
)
|
||||
color = ColorField(
|
||||
required=False
|
||||
)
|
||||
rear_port_set = forms.MultipleChoiceField(
|
||||
choices=[],
|
||||
label='Rear ports',
|
||||
help_text='Select one rear port assignment for each front port being created.',
|
||||
)
|
||||
field_order = (
|
||||
'manufacturer', 'device_type', 'module_type', 'name_pattern', 'label_pattern', 'type', 'color', 'rear_port_set',
|
||||
'description',
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
device_type = DeviceType.objects.get(
|
||||
pk=self.initial.get('device_type') or self.data.get('device_type')
|
||||
)
|
||||
|
||||
# Determine which rear port positions are occupied. These will be excluded from the list of available mappings.
|
||||
occupied_port_positions = [
|
||||
(front_port.rear_port_id, front_port.rear_port_position)
|
||||
for front_port in device_type.frontporttemplates.all()
|
||||
]
|
||||
|
||||
# Populate rear port choices
|
||||
choices = []
|
||||
rear_ports = RearPortTemplate.objects.filter(device_type=device_type)
|
||||
for rear_port in rear_ports:
|
||||
for i in range(1, rear_port.positions + 1):
|
||||
if (rear_port.pk, i) not in occupied_port_positions:
|
||||
choices.append(
|
||||
('{}:{}'.format(rear_port.pk, i), '{}:{}'.format(rear_port.name, i))
|
||||
)
|
||||
self.fields['rear_port_set'].choices = choices
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Validate that the number of ports being created equals the number of selected (rear port, position) tuples
|
||||
front_port_count = len(self.cleaned_data['name_pattern'])
|
||||
rear_port_count = len(self.cleaned_data['rear_port_set'])
|
||||
if front_port_count != rear_port_count:
|
||||
raise forms.ValidationError({
|
||||
'rear_port_set': 'The provided name pattern will create {} ports, however {} rear port assignments '
|
||||
'were selected. These counts must match.'.format(front_port_count, rear_port_count)
|
||||
})
|
||||
|
||||
def get_iterative_data(self, iteration):
|
||||
|
||||
# Assign rear port and position from selected set
|
||||
rear_port, position = self.cleaned_data['rear_port_set'][iteration].split(':')
|
||||
|
||||
return {
|
||||
'rear_port': int(rear_port),
|
||||
'rear_port_position': int(position),
|
||||
}
|
||||
|
||||
|
||||
class RearPortTemplateCreateForm(ModularComponentTemplateCreateForm):
|
||||
type = forms.ChoiceField(
|
||||
choices=PortTypeChoices,
|
||||
widget=StaticSelect(),
|
||||
)
|
||||
color = ColorField(
|
||||
required=False
|
||||
)
|
||||
positions = forms.IntegerField(
|
||||
min_value=REARPORT_POSITIONS_MIN,
|
||||
max_value=REARPORT_POSITIONS_MAX,
|
||||
initial=1,
|
||||
help_text='The number of front ports which may be mapped to each rear port'
|
||||
)
|
||||
field_order = (
|
||||
'manufacturer', 'device_type', 'module_type', 'name_pattern', 'label_pattern', 'type', 'color', 'positions',
|
||||
'description',
|
||||
)
|
||||
|
||||
|
||||
class ModuleBayTemplateCreateForm(ComponentTemplateCreateForm):
|
||||
# TODO: Support patterned position assignment
|
||||
field_order = ('manufacturer', 'device_type', 'name_pattern', 'label_pattern', 'description')
|
||||
|
||||
|
||||
class DeviceBayTemplateCreateForm(ComponentTemplateCreateForm):
|
||||
field_order = ('manufacturer', 'device_type', 'name_pattern', 'label_pattern', 'description')
|
||||
|
||||
|
||||
#
|
||||
# Device components
|
||||
#
|
||||
|
||||
class ComponentCreateForm(CustomFieldsMixin, ComponentForm):
|
||||
"""
|
||||
Base form for the creation of device components (models subclassed from ComponentModel).
|
||||
"""
|
||||
device = DynamicModelChoiceField(
|
||||
queryset=Device.objects.all()
|
||||
)
|
||||
description = forms.CharField(
|
||||
max_length=200,
|
||||
required=False
|
||||
)
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
|
||||
class ConsolePortCreateForm(ComponentCreateForm):
|
||||
model = ConsolePort
|
||||
type = forms.ChoiceField(
|
||||
choices=add_blank_choice(ConsolePortTypeChoices),
|
||||
required=False,
|
||||
widget=StaticSelect()
|
||||
)
|
||||
speed = forms.ChoiceField(
|
||||
choices=add_blank_choice(ConsolePortSpeedChoices),
|
||||
required=False,
|
||||
widget=StaticSelect()
|
||||
)
|
||||
field_order = ('device', 'name_pattern', 'label_pattern', 'type', 'speed', 'mark_connected', 'description', 'tags')
|
||||
|
||||
|
||||
class ConsoleServerPortCreateForm(ComponentCreateForm):
|
||||
model = ConsoleServerPort
|
||||
type = forms.ChoiceField(
|
||||
choices=add_blank_choice(ConsolePortTypeChoices),
|
||||
required=False,
|
||||
widget=StaticSelect()
|
||||
)
|
||||
speed = forms.ChoiceField(
|
||||
choices=add_blank_choice(ConsolePortSpeedChoices),
|
||||
required=False,
|
||||
widget=StaticSelect()
|
||||
)
|
||||
field_order = ('device', 'name_pattern', 'label_pattern', 'type', 'speed', 'mark_connected', 'description', 'tags')
|
||||
|
||||
|
||||
class PowerPortCreateForm(ComponentCreateForm):
|
||||
model = PowerPort
|
||||
type = forms.ChoiceField(
|
||||
choices=add_blank_choice(PowerPortTypeChoices),
|
||||
required=False,
|
||||
widget=StaticSelect()
|
||||
)
|
||||
maximum_draw = forms.IntegerField(
|
||||
min_value=1,
|
||||
required=False,
|
||||
help_text="Maximum draw in watts"
|
||||
)
|
||||
allocated_draw = forms.IntegerField(
|
||||
min_value=1,
|
||||
required=False,
|
||||
help_text="Allocated draw in watts"
|
||||
)
|
||||
field_order = (
|
||||
'device', 'name_pattern', 'label_pattern', 'type', 'maximum_draw', 'allocated_draw', 'mark_connected',
|
||||
'description', 'tags',
|
||||
)
|
||||
|
||||
|
||||
class PowerOutletCreateForm(ComponentCreateForm):
|
||||
model = PowerOutlet
|
||||
type = forms.ChoiceField(
|
||||
choices=add_blank_choice(PowerOutletTypeChoices),
|
||||
required=False,
|
||||
widget=StaticSelect()
|
||||
)
|
||||
power_port = forms.ModelChoiceField(
|
||||
queryset=PowerPort.objects.all(),
|
||||
required=False
|
||||
)
|
||||
feed_leg = forms.ChoiceField(
|
||||
choices=add_blank_choice(PowerOutletFeedLegChoices),
|
||||
required=False
|
||||
)
|
||||
field_order = (
|
||||
'device', 'name_pattern', 'label_pattern', 'type', 'power_port', 'feed_leg', 'mark_connected', 'description',
|
||||
'tags',
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Limit power_port queryset to PowerPorts which belong to the parent Device
|
||||
device = Device.objects.get(
|
||||
pk=self.initial.get('device') or self.data.get('device')
|
||||
)
|
||||
self.fields['power_port'].queryset = PowerPort.objects.filter(device=device)
|
||||
|
||||
|
||||
class InterfaceCreateForm(ComponentCreateForm, InterfaceCommonForm):
|
||||
model = Interface
|
||||
type = forms.ChoiceField(
|
||||
choices=InterfaceTypeChoices,
|
||||
widget=StaticSelect(),
|
||||
)
|
||||
enabled = forms.BooleanField(
|
||||
required=False,
|
||||
initial=True
|
||||
)
|
||||
parent = DynamicModelChoiceField(
|
||||
queryset=Interface.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'device_id': '$device',
|
||||
}
|
||||
)
|
||||
bridge = DynamicModelChoiceField(
|
||||
queryset=Interface.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'device_id': '$device',
|
||||
}
|
||||
)
|
||||
lag = DynamicModelChoiceField(
|
||||
queryset=Interface.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'device_id': '$device',
|
||||
'type': 'lag',
|
||||
},
|
||||
label='LAG'
|
||||
)
|
||||
mac_address = forms.CharField(
|
||||
required=False,
|
||||
label='MAC Address'
|
||||
)
|
||||
wwn = forms.CharField(
|
||||
required=False,
|
||||
label='WWN'
|
||||
)
|
||||
mgmt_only = forms.BooleanField(
|
||||
required=False,
|
||||
label='Management only',
|
||||
help_text='This interface is used only for out-of-band management'
|
||||
)
|
||||
mode = forms.ChoiceField(
|
||||
choices=add_blank_choice(InterfaceModeChoices),
|
||||
required=False,
|
||||
widget=StaticSelect()
|
||||
)
|
||||
rf_role = forms.ChoiceField(
|
||||
choices=add_blank_choice(WirelessRoleChoices),
|
||||
required=False,
|
||||
widget=StaticSelect(),
|
||||
label='Wireless role'
|
||||
)
|
||||
rf_channel = forms.ChoiceField(
|
||||
choices=add_blank_choice(WirelessChannelChoices),
|
||||
required=False,
|
||||
widget=StaticSelect(),
|
||||
label='Wireless channel'
|
||||
)
|
||||
rf_channel_frequency = forms.DecimalField(
|
||||
required=False,
|
||||
label='Channel frequency (MHz)'
|
||||
)
|
||||
rf_channel_width = forms.DecimalField(
|
||||
required=False,
|
||||
label='Channel width (MHz)'
|
||||
)
|
||||
untagged_vlan = DynamicModelChoiceField(
|
||||
queryset=VLAN.objects.all(),
|
||||
required=False,
|
||||
label='Untagged VLAN'
|
||||
)
|
||||
tagged_vlans = DynamicModelMultipleChoiceField(
|
||||
queryset=VLAN.objects.all(),
|
||||
required=False,
|
||||
label='Tagged VLANs'
|
||||
)
|
||||
field_order = (
|
||||
'device', 'name_pattern', 'label_pattern', 'type', 'enabled', 'parent', 'bridge', 'lag', 'mtu', 'mac_address',
|
||||
'wwn', 'description', 'mgmt_only', 'mark_connected', 'rf_role', 'rf_channel', 'rf_channel_frequency',
|
||||
'rf_channel_width', 'mode', 'untagged_vlan', 'tagged_vlans', 'tags'
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Limit VLAN choices by device
|
||||
device_id = self.initial.get('device') or self.data.get('device')
|
||||
self.fields['untagged_vlan'].widget.add_query_param('available_on_device', device_id)
|
||||
self.fields['tagged_vlans'].widget.add_query_param('available_on_device', device_id)
|
||||
|
||||
|
||||
class FrontPortCreateForm(ComponentCreateForm):
|
||||
model = FrontPort
|
||||
type = forms.ChoiceField(
|
||||
choices=PortTypeChoices,
|
||||
widget=StaticSelect(),
|
||||
)
|
||||
color = ColorField(
|
||||
required=False
|
||||
)
|
||||
rear_port_set = forms.MultipleChoiceField(
|
||||
choices=[],
|
||||
label='Rear ports',
|
||||
help_text='Select one rear port assignment for each front port being created.',
|
||||
)
|
||||
field_order = (
|
||||
'device', 'name_pattern', 'label_pattern', 'type', 'color', 'rear_port_set', 'mark_connected', 'description',
|
||||
'tags',
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
device = Device.objects.get(
|
||||
pk=self.initial.get('device') or self.data.get('device')
|
||||
)
|
||||
|
||||
# Determine which rear port positions are occupied. These will be excluded from the list of available
|
||||
# mappings.
|
||||
occupied_port_positions = [
|
||||
(front_port.rear_port_id, front_port.rear_port_position)
|
||||
for front_port in device.frontports.all()
|
||||
]
|
||||
|
||||
# Populate rear port choices
|
||||
choices = []
|
||||
rear_ports = RearPort.objects.filter(device=device)
|
||||
for rear_port in rear_ports:
|
||||
for i in range(1, rear_port.positions + 1):
|
||||
if (rear_port.pk, i) not in occupied_port_positions:
|
||||
choices.append(
|
||||
('{}:{}'.format(rear_port.pk, i), '{}:{}'.format(rear_port.name, i))
|
||||
)
|
||||
self.fields['rear_port_set'].choices = choices
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Validate that the number of ports being created equals the number of selected (rear port, position) tuples
|
||||
front_port_count = len(self.cleaned_data['name_pattern'])
|
||||
rear_port_count = len(self.cleaned_data['rear_port_set'])
|
||||
if front_port_count != rear_port_count:
|
||||
raise forms.ValidationError({
|
||||
'rear_port_set': 'The provided name pattern will create {} ports, however {} rear port assignments '
|
||||
'were selected. These counts must match.'.format(front_port_count, rear_port_count)
|
||||
})
|
||||
|
||||
def get_iterative_data(self, iteration):
|
||||
|
||||
# Assign rear port and position from selected set
|
||||
rear_port, position = self.cleaned_data['rear_port_set'][iteration].split(':')
|
||||
|
||||
return {
|
||||
'rear_port': int(rear_port),
|
||||
'rear_port_position': int(position),
|
||||
}
|
||||
|
||||
|
||||
class RearPortCreateForm(ComponentCreateForm):
|
||||
model = RearPort
|
||||
type = forms.ChoiceField(
|
||||
choices=PortTypeChoices,
|
||||
widget=StaticSelect(),
|
||||
)
|
||||
color = ColorField(
|
||||
required=False
|
||||
)
|
||||
positions = forms.IntegerField(
|
||||
min_value=REARPORT_POSITIONS_MIN,
|
||||
max_value=REARPORT_POSITIONS_MAX,
|
||||
initial=1,
|
||||
help_text='The number of front ports which may be mapped to each rear port'
|
||||
)
|
||||
field_order = (
|
||||
'device', 'name_pattern', 'label_pattern', 'type', 'color', 'positions', 'mark_connected', 'description',
|
||||
'tags',
|
||||
)
|
||||
|
||||
|
||||
class ModuleBayCreateForm(ComponentCreateForm):
|
||||
model = ModuleBay
|
||||
field_order = ('device', 'name_pattern', 'label_pattern', 'description', 'tags')
|
||||
|
||||
|
||||
class DeviceBayCreateForm(ComponentCreateForm):
|
||||
model = DeviceBay
|
||||
field_order = ('device', 'name_pattern', 'label_pattern', 'description', 'tags')
|
||||
|
||||
|
||||
class InventoryItemCreateForm(ComponentCreateForm):
|
||||
model = InventoryItem
|
||||
parent = DynamicModelChoiceField(
|
||||
queryset=InventoryItem.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'device_id': '$device'
|
||||
}
|
||||
)
|
||||
role = DynamicModelChoiceField(
|
||||
queryset=InventoryItemRole.objects.all(),
|
||||
required=False
|
||||
)
|
||||
manufacturer = DynamicModelChoiceField(
|
||||
queryset=Manufacturer.objects.all(),
|
||||
required=False
|
||||
)
|
||||
part_id = forms.CharField(
|
||||
max_length=50,
|
||||
required=False,
|
||||
label='Part ID'
|
||||
)
|
||||
serial = forms.CharField(
|
||||
max_length=50,
|
||||
required=False,
|
||||
)
|
||||
asset_tag = forms.CharField(
|
||||
max_length=50,
|
||||
required=False,
|
||||
)
|
||||
field_order = (
|
||||
'device', 'parent', 'name_pattern', 'label_pattern', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag',
|
||||
'description', 'tags',
|
||||
)
|
||||
|
23
netbox/dcim/migrations/0147_inventoryitem_component.py
Normal file
23
netbox/dcim/migrations/0147_inventoryitem_component.py
Normal file
@ -0,0 +1,23 @@
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
('dcim', '0146_inventoryitemrole'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='inventoryitem',
|
||||
name='component_id',
|
||||
field=models.PositiveBigIntegerField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='inventoryitem',
|
||||
name='component_type',
|
||||
field=models.ForeignKey(blank=True, limit_choices_to=models.Q(('app_label', 'dcim'), ('model__in', ('consoleport', 'consoleserverport', 'frontport', 'interface', 'poweroutlet', 'powerport', 'rearport'))), null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.contenttype'),
|
||||
),
|
||||
]
|
@ -97,6 +97,12 @@ class ModularComponentModel(ComponentModel):
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
inventory_items = GenericRelation(
|
||||
to='dcim.InventoryItem',
|
||||
content_type_field='component_type',
|
||||
object_id_field='component_id',
|
||||
related_name='%(class)ss',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
@ -994,6 +1000,22 @@ class InventoryItem(MPTTModel, ComponentModel):
|
||||
null=True,
|
||||
db_index=True
|
||||
)
|
||||
component_type = models.ForeignKey(
|
||||
to=ContentType,
|
||||
limit_choices_to=MODULAR_COMPONENT_MODELS,
|
||||
on_delete=models.PROTECT,
|
||||
related_name='+',
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
component_id = models.PositiveBigIntegerField(
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
component = GenericForeignKey(
|
||||
ct_field='component_type',
|
||||
fk_field='component_id'
|
||||
)
|
||||
role = models.ForeignKey(
|
||||
to='dcim.InventoryItemRole',
|
||||
on_delete=models.PROTECT,
|
||||
|
@ -780,6 +780,9 @@ class InventoryItemTable(DeviceComponentTable):
|
||||
manufacturer = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
component = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
discovered = BooleanColumn()
|
||||
tags = TagColumn(
|
||||
url_name='dcim:inventoryitem_list'
|
||||
@ -790,9 +793,11 @@ class InventoryItemTable(DeviceComponentTable):
|
||||
model = InventoryItem
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'device', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag',
|
||||
'description', 'discovered', 'tags',
|
||||
'component', 'description', 'discovered', 'tags',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'name', 'device', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'device', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag')
|
||||
|
||||
|
||||
class DeviceInventoryItemTable(InventoryItemTable):
|
||||
@ -810,11 +815,11 @@ class DeviceInventoryItemTable(InventoryItemTable):
|
||||
class Meta(BaseTable.Meta):
|
||||
model = InventoryItem
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description',
|
||||
'discovered', 'tags', 'actions',
|
||||
'pk', 'id', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'component',
|
||||
'description', 'discovered', 'tags', 'actions',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description', 'actions',
|
||||
'pk', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'component', 'actions',
|
||||
)
|
||||
|
||||
|
||||
|
@ -1632,9 +1632,16 @@ class InventoryItemTest(APIViewTestCases.APIViewTestCase):
|
||||
)
|
||||
InventoryItemRole.objects.bulk_create(roles)
|
||||
|
||||
InventoryItem.objects.create(device=device, name='Inventory Item 1', role=roles[0], manufacturer=manufacturer)
|
||||
InventoryItem.objects.create(device=device, name='Inventory Item 2', role=roles[0], manufacturer=manufacturer)
|
||||
InventoryItem.objects.create(device=device, name='Inventory Item 3', role=roles[0], manufacturer=manufacturer)
|
||||
interfaces = (
|
||||
Interface(device=device, name='Interface 1'),
|
||||
Interface(device=device, name='Interface 2'),
|
||||
Interface(device=device, name='Interface 3'),
|
||||
)
|
||||
Interface.objects.bulk_create(interfaces)
|
||||
|
||||
InventoryItem.objects.create(device=device, name='Inventory Item 1', role=roles[0], manufacturer=manufacturer, component=interfaces[0])
|
||||
InventoryItem.objects.create(device=device, name='Inventory Item 2', role=roles[0], manufacturer=manufacturer, component=interfaces[1])
|
||||
InventoryItem.objects.create(device=device, name='Inventory Item 3', role=roles[0], manufacturer=manufacturer, component=interfaces[2])
|
||||
|
||||
cls.create_data = [
|
||||
{
|
||||
@ -1642,18 +1649,24 @@ class InventoryItemTest(APIViewTestCases.APIViewTestCase):
|
||||
'name': 'Inventory Item 4',
|
||||
'role': roles[1].pk,
|
||||
'manufacturer': manufacturer.pk,
|
||||
'component_type': 'dcim.interface',
|
||||
'component_id': interfaces[0].pk,
|
||||
},
|
||||
{
|
||||
'device': device.pk,
|
||||
'name': 'Inventory Item 5',
|
||||
'role': roles[1].pk,
|
||||
'manufacturer': manufacturer.pk,
|
||||
'component_type': 'dcim.interface',
|
||||
'component_id': interfaces[1].pk,
|
||||
},
|
||||
{
|
||||
'device': device.pk,
|
||||
'name': 'Inventory Item 6',
|
||||
'role': roles[1].pk,
|
||||
'manufacturer': manufacturer.pk,
|
||||
'component_type': 'dcim.interface',
|
||||
'component_id': interfaces[2].pk,
|
||||
},
|
||||
]
|
||||
|
||||
|
@ -3004,10 +3004,16 @@ class InventoryItemTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
)
|
||||
InventoryItemRole.objects.bulk_create(roles)
|
||||
|
||||
components = (
|
||||
Interface.objects.create(device=devices[0], name='Interface 1'),
|
||||
ConsolePort.objects.create(device=devices[1], name='Console Port 1'),
|
||||
ConsoleServerPort.objects.create(device=devices[2], name='Console Server Port 1'),
|
||||
)
|
||||
|
||||
inventory_items = (
|
||||
InventoryItem(device=devices[0], role=roles[0], manufacturer=manufacturers[0], name='Inventory Item 1', label='A', part_id='1001', serial='ABC', asset_tag='1001', discovered=True, description='First'),
|
||||
InventoryItem(device=devices[1], role=roles[1], manufacturer=manufacturers[1], name='Inventory Item 2', label='B', part_id='1002', serial='DEF', asset_tag='1002', discovered=True, description='Second'),
|
||||
InventoryItem(device=devices[2], role=roles[2], manufacturer=manufacturers[2], name='Inventory Item 3', label='C', part_id='1003', serial='GHI', asset_tag='1003', discovered=False, description='Third'),
|
||||
InventoryItem(device=devices[0], role=roles[0], manufacturer=manufacturers[0], name='Inventory Item 1', label='A', part_id='1001', serial='ABC', asset_tag='1001', discovered=True, description='First', component=components[0]),
|
||||
InventoryItem(device=devices[1], role=roles[1], manufacturer=manufacturers[1], name='Inventory Item 2', label='B', part_id='1002', serial='DEF', asset_tag='1002', discovered=True, description='Second', component=components[1]),
|
||||
InventoryItem(device=devices[2], role=roles[2], manufacturer=manufacturers[2], name='Inventory Item 3', label='C', part_id='1003', serial='GHI', asset_tag='1003', discovered=False, description='Third', component=components[2]),
|
||||
)
|
||||
for i in inventory_items:
|
||||
i.save()
|
||||
@ -3103,6 +3109,10 @@ class InventoryItemTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
params = {'serial': 'abc'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_component_type(self):
|
||||
params = {'component_type': 'dcim.interface'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
|
||||
class InventoryItemRoleTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = InventoryItemRole.objects.all()
|
||||
|
@ -118,41 +118,27 @@ class DeviceTestCase(TestCase):
|
||||
|
||||
class LabelTestCase(TestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
site = Site.objects.create(name='Site 2', slug='site-2')
|
||||
manufacturer = Manufacturer.objects.create(name='Manufacturer 2', slug='manufacturer-2')
|
||||
cls.device_type = DeviceType.objects.create(
|
||||
manufacturer=manufacturer, model='Device Type 2', slug='device-type-2', u_height=1
|
||||
)
|
||||
device_role = DeviceRole.objects.create(
|
||||
name='Device Role 2', slug='device-role-2', color='ffff00'
|
||||
)
|
||||
cls.device = Device.objects.create(
|
||||
name='Device 2', device_type=cls.device_type, device_role=device_role, site=site
|
||||
)
|
||||
|
||||
def test_interface_label_count_valid(self):
|
||||
"""Test that a `label` can be generated for each generated `name` from `name_pattern` on InterfaceCreateForm"""
|
||||
"""
|
||||
Test that generating an equal number of names and labels passes form validation.
|
||||
"""
|
||||
interface_data = {
|
||||
'device': self.device.pk,
|
||||
'name_pattern': 'eth[0-9]',
|
||||
'label_pattern': 'Interface[0-9]',
|
||||
'type': InterfaceTypeChoices.TYPE_100ME_FIXED,
|
||||
}
|
||||
form = InterfaceCreateForm(interface_data)
|
||||
form = ComponentCreateForm(interface_data)
|
||||
|
||||
self.assertTrue(form.is_valid())
|
||||
|
||||
def test_interface_label_count_mismatch(self):
|
||||
"""Test that a `label` cannot be generated for each generated `name` from `name_pattern` due to invalid `label_pattern` on InterfaceCreateForm"""
|
||||
"""
|
||||
Check that attempting to generate a differing number of names and labels results in a validation error.
|
||||
"""
|
||||
bad_interface_data = {
|
||||
'device': self.device.pk,
|
||||
'name_pattern': 'eth[0-9]',
|
||||
'label_pattern': 'Interface[0-1]',
|
||||
'type': InterfaceTypeChoices.TYPE_100ME_FIXED,
|
||||
}
|
||||
form = InterfaceCreateForm(bad_interface_data)
|
||||
form = ComponentCreateForm(bad_interface_data)
|
||||
|
||||
self.assertFalse(form.is_valid())
|
||||
self.assertIn('label_pattern', form.errors)
|
||||
|
@ -1054,7 +1054,6 @@ class ModuleTypeBulkDeleteView(generic.BulkDeleteView):
|
||||
|
||||
class ConsolePortTemplateCreateView(generic.ComponentCreateView):
|
||||
queryset = ConsolePortTemplate.objects.all()
|
||||
form = forms.ConsolePortTemplateCreateForm
|
||||
model_form = forms.ConsolePortTemplateForm
|
||||
|
||||
|
||||
@ -1088,7 +1087,6 @@ class ConsolePortTemplateBulkDeleteView(generic.BulkDeleteView):
|
||||
|
||||
class ConsoleServerPortTemplateCreateView(generic.ComponentCreateView):
|
||||
queryset = ConsoleServerPortTemplate.objects.all()
|
||||
form = forms.ConsoleServerPortTemplateCreateForm
|
||||
model_form = forms.ConsoleServerPortTemplateForm
|
||||
|
||||
|
||||
@ -1122,7 +1120,6 @@ class ConsoleServerPortTemplateBulkDeleteView(generic.BulkDeleteView):
|
||||
|
||||
class PowerPortTemplateCreateView(generic.ComponentCreateView):
|
||||
queryset = PowerPortTemplate.objects.all()
|
||||
form = forms.PowerPortTemplateCreateForm
|
||||
model_form = forms.PowerPortTemplateForm
|
||||
|
||||
|
||||
@ -1156,7 +1153,6 @@ class PowerPortTemplateBulkDeleteView(generic.BulkDeleteView):
|
||||
|
||||
class PowerOutletTemplateCreateView(generic.ComponentCreateView):
|
||||
queryset = PowerOutletTemplate.objects.all()
|
||||
form = forms.PowerOutletTemplateCreateForm
|
||||
model_form = forms.PowerOutletTemplateForm
|
||||
|
||||
|
||||
@ -1190,7 +1186,6 @@ class PowerOutletTemplateBulkDeleteView(generic.BulkDeleteView):
|
||||
|
||||
class InterfaceTemplateCreateView(generic.ComponentCreateView):
|
||||
queryset = InterfaceTemplate.objects.all()
|
||||
form = forms.InterfaceTemplateCreateForm
|
||||
model_form = forms.InterfaceTemplateForm
|
||||
|
||||
|
||||
@ -1227,6 +1222,14 @@ class FrontPortTemplateCreateView(generic.ComponentCreateView):
|
||||
form = forms.FrontPortTemplateCreateForm
|
||||
model_form = forms.FrontPortTemplateForm
|
||||
|
||||
def initialize_forms(self, request):
|
||||
form, model_form = super().initialize_forms(request)
|
||||
|
||||
model_form.fields.pop('rear_port')
|
||||
model_form.fields.pop('rear_port_position')
|
||||
|
||||
return form, model_form
|
||||
|
||||
|
||||
class FrontPortTemplateEditView(generic.ObjectEditView):
|
||||
queryset = FrontPortTemplate.objects.all()
|
||||
@ -1258,7 +1261,6 @@ class FrontPortTemplateBulkDeleteView(generic.BulkDeleteView):
|
||||
|
||||
class RearPortTemplateCreateView(generic.ComponentCreateView):
|
||||
queryset = RearPortTemplate.objects.all()
|
||||
form = forms.RearPortTemplateCreateForm
|
||||
model_form = forms.RearPortTemplateForm
|
||||
|
||||
|
||||
@ -1292,7 +1294,6 @@ class RearPortTemplateBulkDeleteView(generic.BulkDeleteView):
|
||||
|
||||
class ModuleBayTemplateCreateView(generic.ComponentCreateView):
|
||||
queryset = ModuleBayTemplate.objects.all()
|
||||
form = forms.ModuleBayTemplateCreateForm
|
||||
model_form = forms.ModuleBayTemplateForm
|
||||
|
||||
|
||||
@ -1326,7 +1327,6 @@ class ModuleBayTemplateBulkDeleteView(generic.BulkDeleteView):
|
||||
|
||||
class DeviceBayTemplateCreateView(generic.ComponentCreateView):
|
||||
queryset = DeviceBayTemplate.objects.all()
|
||||
form = forms.DeviceBayTemplateCreateForm
|
||||
model_form = forms.DeviceBayTemplateForm
|
||||
|
||||
|
||||
@ -1741,7 +1741,6 @@ class ConsolePortView(generic.ObjectView):
|
||||
|
||||
class ConsolePortCreateView(generic.ComponentCreateView):
|
||||
queryset = ConsolePort.objects.all()
|
||||
form = forms.ConsolePortCreateForm
|
||||
model_form = forms.ConsolePortForm
|
||||
|
||||
|
||||
@ -1800,7 +1799,6 @@ class ConsoleServerPortView(generic.ObjectView):
|
||||
|
||||
class ConsoleServerPortCreateView(generic.ComponentCreateView):
|
||||
queryset = ConsoleServerPort.objects.all()
|
||||
form = forms.ConsoleServerPortCreateForm
|
||||
model_form = forms.ConsoleServerPortForm
|
||||
|
||||
|
||||
@ -1859,7 +1857,6 @@ class PowerPortView(generic.ObjectView):
|
||||
|
||||
class PowerPortCreateView(generic.ComponentCreateView):
|
||||
queryset = PowerPort.objects.all()
|
||||
form = forms.PowerPortCreateForm
|
||||
model_form = forms.PowerPortForm
|
||||
|
||||
|
||||
@ -1918,7 +1915,6 @@ class PowerOutletView(generic.ObjectView):
|
||||
|
||||
class PowerOutletCreateView(generic.ComponentCreateView):
|
||||
queryset = PowerOutlet.objects.all()
|
||||
form = forms.PowerOutletCreateForm
|
||||
model_form = forms.PowerOutletForm
|
||||
|
||||
|
||||
@ -2012,35 +2008,35 @@ class InterfaceView(generic.ObjectView):
|
||||
|
||||
class InterfaceCreateView(generic.ComponentCreateView):
|
||||
queryset = Interface.objects.all()
|
||||
form = forms.InterfaceCreateForm
|
||||
model_form = forms.InterfaceForm
|
||||
template_name = 'dcim/interface_create.html'
|
||||
# template_name = 'dcim/interface_create.html'
|
||||
|
||||
def post(self, request):
|
||||
"""
|
||||
Override inherited post() method to handle request to assign newly created
|
||||
interface objects (first object) to an IP Address object.
|
||||
"""
|
||||
form = self.form(request.POST, initial=request.GET)
|
||||
new_objs = self.validate_form(request, form)
|
||||
|
||||
if form.is_valid() and not form.errors:
|
||||
if '_addanother' in request.POST:
|
||||
return redirect(request.get_full_path())
|
||||
elif new_objs is not None and '_assignip' in request.POST and len(new_objs) >= 1 and \
|
||||
request.user.has_perm('ipam.add_ipaddress'):
|
||||
first_obj = new_objs[0].pk
|
||||
return redirect(
|
||||
f'/ipam/ip-addresses/add/?interface={first_obj}&return_url={self.get_return_url(request)}'
|
||||
)
|
||||
else:
|
||||
return redirect(self.get_return_url(request))
|
||||
|
||||
return render(request, self.template_name, {
|
||||
'obj_type': self.queryset.model._meta.verbose_name,
|
||||
'form': form,
|
||||
'return_url': self.get_return_url(request),
|
||||
})
|
||||
# TODO: Figure out what to do with this
|
||||
# def post(self, request):
|
||||
# """
|
||||
# Override inherited post() method to handle request to assign newly created
|
||||
# interface objects (first object) to an IP Address object.
|
||||
# """
|
||||
# form = self.form(request.POST, initial=request.GET)
|
||||
# new_objs = self.validate_form(request, form)
|
||||
#
|
||||
# if form.is_valid() and not form.errors:
|
||||
# if '_addanother' in request.POST:
|
||||
# return redirect(request.get_full_path())
|
||||
# elif new_objs is not None and '_assignip' in request.POST and len(new_objs) >= 1 and \
|
||||
# request.user.has_perm('ipam.add_ipaddress'):
|
||||
# first_obj = new_objs[0].pk
|
||||
# return redirect(
|
||||
# f'/ipam/ip-addresses/add/?interface={first_obj}&return_url={self.get_return_url(request)}'
|
||||
# )
|
||||
# else:
|
||||
# return redirect(self.get_return_url(request))
|
||||
#
|
||||
# return render(request, self.template_name, {
|
||||
# 'obj_type': self.queryset.model._meta.verbose_name,
|
||||
# 'form': form,
|
||||
# 'return_url': self.get_return_url(request),
|
||||
# })
|
||||
|
||||
|
||||
class InterfaceEditView(generic.ObjectEditView):
|
||||
@ -2101,6 +2097,14 @@ class FrontPortCreateView(generic.ComponentCreateView):
|
||||
form = forms.FrontPortCreateForm
|
||||
model_form = forms.FrontPortForm
|
||||
|
||||
def initialize_forms(self, request):
|
||||
form, model_form = super().initialize_forms(request)
|
||||
|
||||
model_form.fields.pop('rear_port')
|
||||
model_form.fields.pop('rear_port_position')
|
||||
|
||||
return form, model_form
|
||||
|
||||
|
||||
class FrontPortEditView(generic.ObjectEditView):
|
||||
queryset = FrontPort.objects.all()
|
||||
@ -2157,7 +2161,6 @@ class RearPortView(generic.ObjectView):
|
||||
|
||||
class RearPortCreateView(generic.ComponentCreateView):
|
||||
queryset = RearPort.objects.all()
|
||||
form = forms.RearPortCreateForm
|
||||
model_form = forms.RearPortForm
|
||||
|
||||
|
||||
@ -2216,7 +2219,6 @@ class ModuleBayView(generic.ObjectView):
|
||||
|
||||
class ModuleBayCreateView(generic.ComponentCreateView):
|
||||
queryset = ModuleBay.objects.all()
|
||||
form = forms.ModuleBayCreateForm
|
||||
model_form = forms.ModuleBayForm
|
||||
|
||||
|
||||
@ -2271,7 +2273,6 @@ class DeviceBayView(generic.ObjectView):
|
||||
|
||||
class DeviceBayCreateView(generic.ComponentCreateView):
|
||||
queryset = DeviceBay.objects.all()
|
||||
form = forms.DeviceBayCreateForm
|
||||
model_form = forms.DeviceBayForm
|
||||
|
||||
|
||||
@ -2397,7 +2398,6 @@ class InventoryItemEditView(generic.ObjectEditView):
|
||||
|
||||
class InventoryItemCreateView(generic.ComponentCreateView):
|
||||
queryset = InventoryItem.objects.all()
|
||||
form = forms.InventoryItemCreateForm
|
||||
model_form = forms.InventoryItemForm
|
||||
|
||||
|
||||
|
@ -6,6 +6,7 @@ from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.db import transaction
|
||||
from django.db.models import ProtectedError
|
||||
from django.forms.widgets import HiddenInput
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.utils.html import escape
|
||||
@ -14,6 +15,7 @@ from django.utils.safestring import mark_safe
|
||||
from django.views.generic import View
|
||||
from django_tables2.export import TableExport
|
||||
|
||||
from dcim.forms.object_create import ComponentCreateForm
|
||||
from extras.models import ExportTemplate
|
||||
from extras.signals import clear_webhooks
|
||||
from utilities.error_handlers import handle_protectederror
|
||||
@ -674,33 +676,46 @@ class ObjectDeleteView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
||||
# Device/VirtualMachine components
|
||||
#
|
||||
|
||||
# TODO: Replace with BulkCreateView
|
||||
class ComponentCreateView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
||||
"""
|
||||
Add one or more components (e.g. interfaces, console ports, etc.) to a Device or VirtualMachine.
|
||||
"""
|
||||
queryset = None
|
||||
form = None
|
||||
form = ComponentCreateForm
|
||||
model_form = None
|
||||
template_name = 'generic/object_edit.html'
|
||||
template_name = 'dcim/component_create.html'
|
||||
patterned_fields = ('name', 'label')
|
||||
|
||||
def get_required_permission(self):
|
||||
return get_permission_for_model(self.queryset.model, 'add')
|
||||
|
||||
def get(self, request):
|
||||
def initialize_forms(self, request):
|
||||
data = request.POST if request.method == 'POST' else None
|
||||
initial_data = normalize_querydict(request.GET)
|
||||
|
||||
form = self.form(initial=request.GET)
|
||||
form = self.form(data=data, initial=request.GET)
|
||||
model_form = self.model_form(data=data, initial=initial_data)
|
||||
|
||||
# These fields will be set from the pattern values
|
||||
for field_name in self.patterned_fields:
|
||||
model_form.fields[field_name].widget = HiddenInput()
|
||||
|
||||
return form, model_form
|
||||
|
||||
def get(self, request):
|
||||
form, model_form = self.initialize_forms(request)
|
||||
|
||||
return render(request, self.template_name, {
|
||||
'obj': self.queryset.model(),
|
||||
'obj': self.queryset.model,
|
||||
'obj_type': self.queryset.model._meta.verbose_name,
|
||||
'form': form,
|
||||
'replication_form': form,
|
||||
'form': model_form,
|
||||
'return_url': self.get_return_url(request),
|
||||
})
|
||||
|
||||
def post(self, request):
|
||||
logger = logging.getLogger('netbox.views.ComponentCreateView')
|
||||
form = self.form(request.POST, initial=request.GET)
|
||||
form, model_form = self.initialize_forms(request)
|
||||
|
||||
self.validate_form(request, form)
|
||||
|
||||
if form.is_valid() and not form.errors:
|
||||
@ -710,8 +725,10 @@ class ComponentCreateView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View
|
||||
return redirect(self.get_return_url(request))
|
||||
|
||||
return render(request, self.template_name, {
|
||||
'obj': self.queryset.model,
|
||||
'obj_type': self.queryset.model._meta.verbose_name,
|
||||
'form': form,
|
||||
'replication_form': form,
|
||||
'form': model_form,
|
||||
'return_url': self.get_return_url(request),
|
||||
})
|
||||
|
||||
@ -720,7 +737,6 @@ class ComponentCreateView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View
|
||||
Validate form values and set errors on the form object as they are detected. If
|
||||
no errors are found, signal success messages.
|
||||
"""
|
||||
|
||||
logger = logging.getLogger('netbox.views.ComponentCreateView')
|
||||
if form.is_valid():
|
||||
new_components = []
|
||||
|
7
netbox/templates/dcim/component_create.html
Normal file
7
netbox/templates/dcim/component_create.html
Normal file
@ -0,0 +1,7 @@
|
||||
{% extends 'generic/object_edit.html' %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block form %}
|
||||
{% render_form replication_form %}
|
||||
{{ block.super }}
|
||||
{% endblock form %}
|
@ -58,91 +58,92 @@
|
||||
</h5>
|
||||
<div class="card-body">
|
||||
{% if object.mark_connected %}
|
||||
<span class="text-success"><i class="mdi mdi-check-bold"></i></span> Marked as connected
|
||||
<span class="text-success"><i class="mdi mdi-check-bold"></i></span> Marked as connected
|
||||
{% elif object.cable %}
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">Cable</th>
|
||||
<td>
|
||||
<a href="{{ object.cable.get_absolute_url }}">{{ object.cable }}</a>
|
||||
<a href="{% url 'dcim:consoleport_trace' pk=object.pk %}" class="btn btn-primary btn-sm lh-1" title="Trace">
|
||||
<i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% if object.connected_endpoint %}
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">Device</th>
|
||||
<th scope="row">Cable</th>
|
||||
<td>
|
||||
<a href="{{ object.connected_endpoint.device.get_absolute_url }}">{{ object.connected_endpoint.device }}</a>
|
||||
<a href="{{ object.cable.get_absolute_url }}">{{ object.cable }}</a>
|
||||
<a href="{% url 'dcim:consoleport_trace' pk=object.pk %}" class="btn btn-primary btn-sm lh-1" title="Trace">
|
||||
<i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Name</th>
|
||||
<td>
|
||||
<a href="{{ object.connected_endpoint.get_absolute_url }}">{{ object.connected_endpoint.name }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Type</th>
|
||||
<td>{{ object.connected_endpoint.get_type_display|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Description</th>
|
||||
<td>{{ object.connected_endpoint.description|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Path Status</th>
|
||||
<td>
|
||||
{% if object.path.is_active %}
|
||||
<span class="badge bg-success">Reachable</span>
|
||||
{% else %}
|
||||
<span class="badge bg-danger">Not Reachable</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="text-muted">
|
||||
Not Connected
|
||||
{% if perms.dcim.add_cable %}
|
||||
<div class="dropdown float-end">
|
||||
<button type="button" class="btn btn-primary btn-sm dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span> Connect
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
<li>
|
||||
<a
|
||||
class="dropdown-item"
|
||||
href="{% url 'dcim:consoleport_connect' termination_a_id=object.pk termination_b_type='console-server-port' %}?return_url={{ object.get_absolute_url }}"
|
||||
>
|
||||
Console Server Port
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
class="dropdown-item"
|
||||
href="{% url 'dcim:consoleport_connect' termination_a_id=object.pk termination_b_type='front-port' %}?return_url={{ object.get_absolute_url }}"
|
||||
>
|
||||
Front Port
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
class="dropdown-item"
|
||||
href="{% url 'dcim:consoleport_connect' termination_a_id=object.pk termination_b_type='rear-port' %}?return_url={{ object.get_absolute_url }}"
|
||||
>
|
||||
Rear Port
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if object.connected_endpoint %}
|
||||
<tr>
|
||||
<th scope="row">Device</th>
|
||||
<td>
|
||||
<a href="{{ object.connected_endpoint.device.get_absolute_url }}">{{ object.connected_endpoint.device }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Name</th>
|
||||
<td>
|
||||
<a href="{{ object.connected_endpoint.get_absolute_url }}">{{ object.connected_endpoint.name }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Type</th>
|
||||
<td>{{ object.connected_endpoint.get_type_display|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Description</th>
|
||||
<td>{{ object.connected_endpoint.description|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Path Status</th>
|
||||
<td>
|
||||
{% if object.path.is_active %}
|
||||
<span class="badge bg-success">Reachable</span>
|
||||
{% else %}
|
||||
<span class="badge bg-danger">Not Reachable</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="text-muted">
|
||||
Not Connected
|
||||
{% if perms.dcim.add_cable %}
|
||||
<div class="dropdown float-end">
|
||||
<button type="button" class="btn btn-primary btn-sm dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span> Connect
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
<li>
|
||||
<a
|
||||
class="dropdown-item"
|
||||
href="{% url 'dcim:consoleport_connect' termination_a_id=object.pk termination_b_type='console-server-port' %}?return_url={{ object.get_absolute_url }}"
|
||||
>
|
||||
Console Server Port
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
class="dropdown-item"
|
||||
href="{% url 'dcim:consoleport_connect' termination_a_id=object.pk termination_b_type='front-port' %}?return_url={{ object.get_absolute_url }}"
|
||||
>
|
||||
Front Port
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
class="dropdown-item"
|
||||
href="{% url 'dcim:consoleport_connect' termination_a_id=object.pk termination_b_type='rear-port' %}?return_url={{ object.get_absolute_url }}"
|
||||
>
|
||||
Rear Port
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% include 'dcim/inc/panels/inventory_items.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -143,6 +143,7 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% include 'dcim/inc/panels/inventory_items.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -129,6 +129,7 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% include 'dcim/inc/panels/inventory_items.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
|
59
netbox/templates/dcim/inc/panels/inventory_items.html
Normal file
59
netbox/templates/dcim/inc/panels/inventory_items.html
Normal file
@ -0,0 +1,59 @@
|
||||
{% load helpers %}
|
||||
|
||||
<div class="card">
|
||||
<h5 class="card-header">Inventory Items</h5>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover table-headings">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Label</th>
|
||||
<th>Role</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in object.inventory_items.all %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{{ item.get_absolute_url }}">{{ item.name }}</a>
|
||||
</td>
|
||||
<td>
|
||||
{{ item.label|placeholder }}
|
||||
</td>
|
||||
<td>
|
||||
{% if item.role %}
|
||||
<a href="{{ item.role.get_absolute_url }}">{{ item.role }}</a>
|
||||
{% else %}
|
||||
<span class="text-muted">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-end noprint">
|
||||
{% if perms.dcim.change_inventoryitem %}
|
||||
<a href="{% url 'dcim:inventoryitem_edit' pk=item.pk %}?return_url={{ object.get_absolute_url }}" class="btn btn-warning btn-sm lh-1" title="Edit">
|
||||
<i class="mdi mdi-pencil" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if perms.ipam.delete_inventoryitem %}
|
||||
<a href="{% url 'dcim:inventoryitem_delete' pk=item.pk %}?return_url={{ object.get_absolute_url }}" class="btn btn-danger btn-sm lh-1" title="Delete">
|
||||
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="5" class="text-muted">None</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="card-footer text-end noprint">
|
||||
{% if perms.dcim.add_inventoryitem %}
|
||||
<a href="{% url 'dcim:inventoryitem_add' %}?device={{ object.device.pk }}&component_type={{ object|content_type_id }}&component_id={{ object.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-sm btn-primary">
|
||||
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add Inventory Item
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
@ -448,6 +448,7 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
{% include 'ipam/inc/panels/fhrp_groups.html' %}
|
||||
{% include 'dcim/inc/panels/inventory_items.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,16 +0,0 @@
|
||||
{% extends 'generic/object_edit.html' %}
|
||||
|
||||
{% block buttons %}
|
||||
<a class="btn btn-outline-danger" href="{{ return_url }}">Cancel</a>
|
||||
{% if component_type == 'interface' and perms.ipam.add_ipaddress %}
|
||||
<button type="submit" name="_assignip" class="btn btn-outline-success">
|
||||
Create & Assign IP Address
|
||||
</button>
|
||||
{% endif %}
|
||||
<button type="submit" name="_addanother" class="btn btn-outline-primary">
|
||||
Create & Add Another
|
||||
</button>
|
||||
<button type="submit" name="_create" class="btn btn-primary">
|
||||
Create
|
||||
</button>
|
||||
{% endblock %}
|
@ -50,6 +50,16 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Component</th>
|
||||
<td>
|
||||
{% if object.component %}
|
||||
<a href="{{ object.component.get_absolute_url }}">{{ object.component }}</a>
|
||||
{% else %}
|
||||
<span class="text-muted">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Manufacturer</th>
|
||||
<td>
|
||||
|
@ -121,6 +121,7 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% include 'dcim/inc/panels/inventory_items.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -131,6 +131,7 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% include 'dcim/inc/panels/inventory_items.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -117,6 +117,7 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% include 'dcim/inc/panels/inventory_items.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -29,29 +29,35 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form action="" method="post" enctype="multipart/form-data" class="form-object-edit">
|
||||
<form action="" method="post" enctype="multipart/form-data" class="form-object-edit mt-5">
|
||||
{% csrf_token %}
|
||||
{% for field in form.hidden_fields %}
|
||||
{{ field }}
|
||||
{% endfor %}
|
||||
|
||||
{% block form %}
|
||||
{% if form.Meta.fieldsets %}
|
||||
|
||||
{# Render hidden fields #}
|
||||
{% for field in form.hidden_fields %}
|
||||
{{ field }}
|
||||
{% endfor %}
|
||||
|
||||
{# Render grouped fields according to Form #}
|
||||
{% for group, fields in form.Meta.fieldsets %}
|
||||
<div class="field-group my-5">
|
||||
<div class="field-group mb-5">
|
||||
<div class="row mb-2">
|
||||
<h5 class="offset-sm-3">{{ group }}</h5>
|
||||
</div>
|
||||
{% for name in fields %}
|
||||
{% render_field form|getfield:name %}
|
||||
{% with field=form|getfield:name %}
|
||||
{% if not field.field.widget.is_hidden %}
|
||||
{% render_field field %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% if form.custom_fields %}
|
||||
<div class="field-group my-5">
|
||||
<div class="field-group mb-5">
|
||||
<div class="row mb-2">
|
||||
<h5 class="offset-sm-3">Custom Fields</h5>
|
||||
</div>
|
||||
@ -60,7 +66,7 @@
|
||||
{% endif %}
|
||||
|
||||
{% if form.comments %}
|
||||
<div class="field-group my-5">
|
||||
<div class="field-group mb-5">
|
||||
<h5 class="text-center">Comments</h5>
|
||||
{% render_field form.comments %}
|
||||
</div>
|
||||
@ -68,7 +74,7 @@
|
||||
|
||||
{% else %}
|
||||
{# Render all fields in a single group #}
|
||||
<div class="field-group my-5">
|
||||
<div class="field-group mb-5">
|
||||
{% block form_fields %}{% render_form form %}{% endblock %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
@ -346,11 +346,7 @@ class ExpandableNameField(forms.CharField):
|
||||
if not self.help_text:
|
||||
self.help_text = """
|
||||
Alphanumeric ranges are supported for bulk creation. Mixed cases and types within a single range
|
||||
are not supported. Examples:
|
||||
<ul>
|
||||
<li><code>[ge,xe]-0/0/[0-9]</code></li>
|
||||
<li><code>e[0-3][a-d,f]</code></li>
|
||||
</ul>
|
||||
are not supported. Example: <code>[ge,xe]-0/0/[0-9]</code>
|
||||
"""
|
||||
|
||||
def to_python(self, value):
|
||||
|
@ -275,12 +275,18 @@ class VMInterfaceForm(InterfaceCommonForm, CustomFieldModelForm):
|
||||
parent = DynamicModelChoiceField(
|
||||
queryset=VMInterface.objects.all(),
|
||||
required=False,
|
||||
label='Parent interface'
|
||||
label='Parent interface',
|
||||
query_params={
|
||||
'virtual_machine_id': '$virtual_machine',
|
||||
}
|
||||
)
|
||||
bridge = DynamicModelChoiceField(
|
||||
queryset=VMInterface.objects.all(),
|
||||
required=False,
|
||||
label='Bridged interface'
|
||||
label='Bridged interface',
|
||||
query_params={
|
||||
'virtual_machine_id': '$virtual_machine',
|
||||
}
|
||||
)
|
||||
vlan_group = DynamicModelChoiceField(
|
||||
queryset=VLANGroup.objects.all(),
|
||||
@ -293,6 +299,7 @@ class VMInterfaceForm(InterfaceCommonForm, CustomFieldModelForm):
|
||||
label='Untagged VLAN',
|
||||
query_params={
|
||||
'group_id': '$vlan_group',
|
||||
'available_on_virtualmachine': '$virtual_machine',
|
||||
}
|
||||
)
|
||||
tagged_vlans = DynamicModelMultipleChoiceField(
|
||||
@ -301,6 +308,7 @@ class VMInterfaceForm(InterfaceCommonForm, CustomFieldModelForm):
|
||||
label='Tagged VLANs',
|
||||
query_params={
|
||||
'group_id': '$vlan_group',
|
||||
'available_on_virtualmachine': '$virtual_machine',
|
||||
}
|
||||
)
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
@ -324,15 +332,3 @@ class VMInterfaceForm(InterfaceCommonForm, CustomFieldModelForm):
|
||||
help_texts = {
|
||||
'mode': INTERFACE_MODE_HELP_TEXT,
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
vm_id = self.initial.get('virtual_machine') or self.data.get('virtual_machine')
|
||||
|
||||
# Restrict parent interface assignment by VM
|
||||
self.fields['parent'].widget.add_query_param('virtual_machine_id', vm_id)
|
||||
self.fields['bridge'].widget.add_query_param('virtual_machine_id', vm_id)
|
||||
|
||||
# Limit VLAN choices by virtual machine
|
||||
self.fields['untagged_vlan'].widget.add_query_param('available_on_virtualmachine', vm_id)
|
||||
self.fields['tagged_vlans'].widget.add_query_param('available_on_virtualmachine', vm_id)
|
||||
|
@ -1,81 +1,13 @@
|
||||
from django import forms
|
||||
|
||||
from dcim.choices import InterfaceModeChoices
|
||||
from dcim.forms.common import InterfaceCommonForm
|
||||
from extras.forms import CustomFieldsMixin
|
||||
from extras.models import Tag
|
||||
from ipam.models import VLAN
|
||||
from utilities.forms import (
|
||||
add_blank_choice, BootstrapMixin, DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableNameField,
|
||||
StaticSelect,
|
||||
)
|
||||
from virtualization.models import VMInterface, VirtualMachine
|
||||
from utilities.forms import BootstrapMixin, ExpandableNameField
|
||||
|
||||
__all__ = (
|
||||
'VMInterfaceCreateForm',
|
||||
)
|
||||
|
||||
|
||||
class VMInterfaceCreateForm(BootstrapMixin, CustomFieldsMixin, InterfaceCommonForm):
|
||||
model = VMInterface
|
||||
virtual_machine = DynamicModelChoiceField(
|
||||
queryset=VirtualMachine.objects.all()
|
||||
)
|
||||
class VMInterfaceCreateForm(BootstrapMixin, forms.Form):
|
||||
name_pattern = ExpandableNameField(
|
||||
label='Name'
|
||||
)
|
||||
enabled = forms.BooleanField(
|
||||
required=False,
|
||||
initial=True
|
||||
)
|
||||
parent = DynamicModelChoiceField(
|
||||
queryset=VMInterface.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'virtual_machine_id': '$virtual_machine',
|
||||
}
|
||||
)
|
||||
bridge = DynamicModelChoiceField(
|
||||
queryset=VMInterface.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'virtual_machine_id': '$virtual_machine',
|
||||
}
|
||||
)
|
||||
mac_address = forms.CharField(
|
||||
required=False,
|
||||
label='MAC Address'
|
||||
)
|
||||
description = forms.CharField(
|
||||
max_length=200,
|
||||
required=False
|
||||
)
|
||||
mode = forms.ChoiceField(
|
||||
choices=add_blank_choice(InterfaceModeChoices),
|
||||
required=False,
|
||||
widget=StaticSelect(),
|
||||
)
|
||||
untagged_vlan = DynamicModelChoiceField(
|
||||
queryset=VLAN.objects.all(),
|
||||
required=False
|
||||
)
|
||||
tagged_vlans = DynamicModelMultipleChoiceField(
|
||||
queryset=VLAN.objects.all(),
|
||||
required=False
|
||||
)
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
field_order = (
|
||||
'virtual_machine', 'name_pattern', 'enabled', 'parent', 'bridge', 'mtu', 'mac_address', 'description', 'mode',
|
||||
'untagged_vlan', 'tagged_vlans', 'tags'
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
vm_id = self.initial.get('virtual_machine') or self.data.get('virtual_machine')
|
||||
|
||||
# Limit VLAN choices by virtual machine
|
||||
self.fields['untagged_vlan'].widget.add_query_param('available_on_virtualmachine', vm_id)
|
||||
self.fields['tagged_vlans'].widget.add_query_param('available_on_virtualmachine', vm_id)
|
||||
|
@ -447,11 +447,11 @@ class VMInterfaceView(generic.ObjectView):
|
||||
}
|
||||
|
||||
|
||||
# TODO: This should not use ComponentCreateView
|
||||
class VMInterfaceCreateView(generic.ComponentCreateView):
|
||||
queryset = VMInterface.objects.all()
|
||||
form = forms.VMInterfaceCreateForm
|
||||
model_form = forms.VMInterfaceForm
|
||||
patterned_fields = ('name',)
|
||||
|
||||
|
||||
class VMInterfaceEditView(generic.ObjectEditView):
|
||||
|
Loading…
Reference in New Issue
Block a user