mirror of
https://github.com/netbox-community/netbox.git
synced 2025-08-25 00:36:11 -06:00
Work on #7854
This commit is contained in:
parent
0b24d3d892
commit
11471a22de
@ -672,6 +672,22 @@ class DeviceSerializer(NetBoxModelSerializer):
|
||||
return data
|
||||
|
||||
|
||||
class VirtualDeviceContextSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:device-detail')
|
||||
device = NestedDeviceSerializer()
|
||||
tenant = NestedTenantSerializer(required=False, allow_null=True, default=None)
|
||||
primary_ip = NestedIPAddressSerializer(read_only=True)
|
||||
primary_ip4 = NestedIPAddressSerializer(required=False, allow_null=True)
|
||||
primary_ip6 = NestedIPAddressSerializer(required=False, allow_null=True)
|
||||
|
||||
class Meta:
|
||||
model = VirtualDeviceContext
|
||||
fields = [
|
||||
'id', 'url', 'display', 'name', 'device', 'tenant', 'primary_ip', 'primary_ip4', 'primary_ip6', 'comments',
|
||||
'tags', 'custom_fields', 'created', 'last_updated',
|
||||
]
|
||||
|
||||
|
||||
class ModuleSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:module-detail')
|
||||
device = NestedDeviceSerializer()
|
||||
|
@ -37,6 +37,7 @@ router.register('inventory-item-templates', views.InventoryItemTemplateViewSet)
|
||||
router.register('device-roles', views.DeviceRoleViewSet)
|
||||
router.register('platforms', views.PlatformViewSet)
|
||||
router.register('devices', views.DeviceViewSet)
|
||||
router.register('vdcs', views.VirtualDeviceContextViewSet)
|
||||
router.register('modules', views.ModuleViewSet)
|
||||
|
||||
# Device components
|
||||
|
@ -538,6 +538,14 @@ class DeviceViewSet(ConfigContextQuerySetMixin, NetBoxModelViewSet):
|
||||
return Response(response)
|
||||
|
||||
|
||||
class VirtualDeviceContextViewSet(NetBoxModelViewSet):
|
||||
queryset = VirtualDeviceContext.objects.prefetch_related(
|
||||
'device__device_type', 'device', 'tenant', 'tags',
|
||||
)
|
||||
serializer_class = serializers.VirtualDeviceContextSerializer
|
||||
filterset_class = filtersets.VirtualDeviceContextFilterSet
|
||||
|
||||
|
||||
class ModuleViewSet(NetBoxModelViewSet):
|
||||
queryset = Module.objects.prefetch_related(
|
||||
'device', 'module_bay', 'module_type__manufacturer', 'tags',
|
||||
|
@ -1399,3 +1399,74 @@ class PowerFeedPhaseChoices(ChoiceSet):
|
||||
(PHASE_SINGLE, 'Single phase'),
|
||||
(PHASE_3PHASE, 'Three-phase'),
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# VDC
|
||||
#
|
||||
class VirtualDeviceContextStatusChoices(ChoiceSet):
|
||||
key = 'VirtualDeviceContext.status'
|
||||
|
||||
STATUS_PLANNED = 'planned'
|
||||
STATUS_ACTIVE = 'active'
|
||||
STATUS_OFFLINE = 'offline'
|
||||
|
||||
CHOICES = [
|
||||
(STATUS_PLANNED, 'Planned', 'cyan'),
|
||||
(STATUS_ACTIVE, 'Active', 'green'),
|
||||
(STATUS_OFFLINE, 'Offline', 'red'),
|
||||
]
|
||||
|
||||
|
||||
class VirtualDeviceContextTypeChoices(ChoiceSet):
|
||||
|
||||
CISCO_NEXUS_VDC = 'cisco-nexus-vdc'
|
||||
CISCO_ASA_CONTEXT = 'cisco-asa-context'
|
||||
CISCO_FTD_INSTANCE = 'cisico-ftd-instance'
|
||||
JUNIPER_VR = 'juniper-virtualrouter'
|
||||
FORTINET_VDOM = 'fortinet-virtualdomain'
|
||||
PALOALTO_VSYS = 'paloalto-virtualsystem'
|
||||
CHECKPOINT_VSYS = 'checkpoint-virtualsystem'
|
||||
|
||||
OTHER_VIRTUALCONTEXT = 'other-virtualcontext'
|
||||
|
||||
CHOICES = (
|
||||
(
|
||||
'Cisco',
|
||||
(
|
||||
(CISCO_NEXUS_VDC, 'Nexus VDC'),
|
||||
(CISCO_ASA_CONTEXT, 'ASA Context'),
|
||||
(CISCO_FTD_INSTANCE, 'FTD Instance'),
|
||||
)
|
||||
),
|
||||
(
|
||||
'Juniper',
|
||||
(
|
||||
(JUNIPER_VR, 'Virtual Router'),
|
||||
)
|
||||
),
|
||||
(
|
||||
'Fortinet',
|
||||
(
|
||||
(FORTINET_VDOM, 'Virtual Domain'),
|
||||
)
|
||||
),
|
||||
(
|
||||
'Palo Alto',
|
||||
(
|
||||
(PALOALTO_VSYS, 'Virtual System'),
|
||||
)
|
||||
),
|
||||
(
|
||||
'Checkpoint',
|
||||
(
|
||||
(CHECKPOINT_VSYS, 'Virtual System'),
|
||||
)
|
||||
),
|
||||
(
|
||||
'Other',
|
||||
(
|
||||
(OTHER_VIRTUALCONTEXT, 'Virtual Context'),
|
||||
)
|
||||
),
|
||||
)
|
||||
|
@ -65,6 +65,7 @@ __all__ = (
|
||||
'SiteFilterSet',
|
||||
'SiteGroupFilterSet',
|
||||
'VirtualChassisFilterSet',
|
||||
'VirtualDeviceContextFilterSet',
|
||||
)
|
||||
|
||||
|
||||
@ -434,6 +435,9 @@ class DeviceTypeFilterSet(NetBoxModelFilterSet):
|
||||
to_field_name='slug',
|
||||
label='Manufacturer (slug)',
|
||||
)
|
||||
vdc_type = django_filters.MultipleChoiceFilter(
|
||||
choices=VirtualDeviceContextTypeChoices
|
||||
)
|
||||
has_front_image = django_filters.BooleanFilter(
|
||||
label='Has a front image',
|
||||
method='_has_front_image'
|
||||
@ -482,7 +486,7 @@ class DeviceTypeFilterSet(NetBoxModelFilterSet):
|
||||
class Meta:
|
||||
model = DeviceType
|
||||
fields = [
|
||||
'id', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role', 'airflow', 'weight', 'weight_unit'
|
||||
'id', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role', 'airflow', 'weight', 'weight_unit', 'vdc_type',
|
||||
]
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
@ -1009,6 +1013,30 @@ class DeviceFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilter
|
||||
return queryset.exclude(devicebays__isnull=value)
|
||||
|
||||
|
||||
class VirtualDeviceContextFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
|
||||
device_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='device',
|
||||
queryset=Device.objects.all(),
|
||||
label='VDC (ID)',
|
||||
)
|
||||
device = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='device',
|
||||
queryset=Device.objects.all(),
|
||||
label='Device model',
|
||||
)
|
||||
status = django_filters.MultipleChoiceFilter(
|
||||
choices=VirtualDeviceContextStatusChoices
|
||||
)
|
||||
has_primary_ip = django_filters.BooleanFilter(
|
||||
method='_has_primary_ip',
|
||||
label='Has a primary IP',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = VirtualDeviceContext
|
||||
fields = ['id', 'device', 'name',]
|
||||
|
||||
|
||||
class ModuleFilterSet(NetBoxModelFilterSet):
|
||||
manufacturer_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='module_type__manufacturer',
|
||||
|
@ -54,6 +54,7 @@ __all__ = (
|
||||
'SiteBulkEditForm',
|
||||
'SiteGroupBulkEditForm',
|
||||
'VirtualChassisBulkEditForm',
|
||||
'VirtualDeviceContextBulkEditForm'
|
||||
)
|
||||
|
||||
|
||||
@ -1325,3 +1326,20 @@ class InventoryItemRoleBulkEditForm(NetBoxModelBulkEditForm):
|
||||
(None, ('color', 'description')),
|
||||
)
|
||||
nullable_fields = ('color', 'description')
|
||||
|
||||
|
||||
class VirtualDeviceContextBulkEditForm(NetBoxModelBulkEditForm):
|
||||
device = DynamicModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
required=False
|
||||
)
|
||||
status = forms.ChoiceField(
|
||||
required=False,
|
||||
choices=add_blank_choice(VirtualDeviceContextStatusChoices),
|
||||
widget=StaticSelect()
|
||||
)
|
||||
model = VirtualDeviceContext
|
||||
fieldsets = (
|
||||
(None, ('device',)),
|
||||
)
|
||||
nullable_fields = ('device', )
|
@ -43,6 +43,7 @@ __all__ = (
|
||||
'SiteCSVForm',
|
||||
'SiteGroupCSVForm',
|
||||
'VirtualChassisCSVForm',
|
||||
'VirtualDeviceContextCSVForm'
|
||||
)
|
||||
|
||||
|
||||
@ -1083,3 +1084,25 @@ class PowerFeedCSVForm(NetBoxModelCSVForm):
|
||||
f"location__{self.fields['location'].to_field_name}": data.get('location'),
|
||||
}
|
||||
self.fields['rack'].queryset = self.fields['rack'].queryset.filter(**params)
|
||||
|
||||
|
||||
class VirtualDeviceContextCSVForm(NetBoxModelCSVForm):
|
||||
|
||||
device = CSVModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
to_field_name='name',
|
||||
help_text='Assigned role'
|
||||
)
|
||||
tenant = CSVModelChoiceField(
|
||||
queryset=Tenant.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text='Assigned tenant'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
fields = [
|
||||
'name', 'device', 'status', 'tenant', 'identifier', 'comments',
|
||||
]
|
||||
model = VirtualDeviceContext
|
||||
help_texts = {}
|
||||
|
@ -50,6 +50,7 @@ __all__ = (
|
||||
'SiteFilterForm',
|
||||
'SiteGroupFilterForm',
|
||||
'VirtualChassisFilterForm',
|
||||
'VirtualDeviceContextFilterForm'
|
||||
)
|
||||
|
||||
|
||||
@ -372,7 +373,7 @@ class DeviceTypeFilterForm(NetBoxModelFilterSetForm):
|
||||
model = DeviceType
|
||||
fieldsets = (
|
||||
(None, ('q', 'tag')),
|
||||
('Hardware', ('manufacturer_id', 'part_number', 'subdevice_role', 'airflow')),
|
||||
('Hardware', ('manufacturer_id', 'part_number', 'subdevice_role', 'airflow', 'vdc_type')),
|
||||
('Images', ('has_front_image', 'has_rear_image')),
|
||||
('Components', (
|
||||
'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces',
|
||||
@ -396,6 +397,10 @@ class DeviceTypeFilterForm(NetBoxModelFilterSetForm):
|
||||
choices=add_blank_choice(DeviceAirflowChoices),
|
||||
required=False
|
||||
)
|
||||
vdc_type = MultipleChoiceField(
|
||||
choices=add_blank_choice(VirtualDeviceContextTypeChoices),
|
||||
required=False
|
||||
)
|
||||
has_front_image = forms.NullBooleanField(
|
||||
required=False,
|
||||
label='Has a front image',
|
||||
@ -728,6 +733,37 @@ class DeviceFilterForm(
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class VirtualDeviceContextFilterForm(
|
||||
TenancyFilterForm,
|
||||
NetBoxModelFilterSetForm
|
||||
):
|
||||
model = VirtualDeviceContext
|
||||
fieldsets = (
|
||||
(None, ('q', 'tag')),
|
||||
('Hardware', ('device',)),
|
||||
('Tenant', ('tenant_group_id', 'tenant_id')),
|
||||
('Miscellaneous', ('has_primary_ip',))
|
||||
)
|
||||
device = DynamicModelMultipleChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
required=False,
|
||||
label=_('Device'),
|
||||
fetch_trigger='open'
|
||||
)
|
||||
status = MultipleChoiceField(
|
||||
required=False,
|
||||
choices=add_blank_choice(VirtualDeviceContextStatusChoices)
|
||||
)
|
||||
has_primary_ip = forms.NullBooleanField(
|
||||
required=False,
|
||||
label='Has a primary IP',
|
||||
widget=StaticSelect(
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class ModuleFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, NetBoxModelFilterSetForm):
|
||||
model = Module
|
||||
fieldsets = (
|
||||
|
@ -62,6 +62,7 @@ __all__ = (
|
||||
'SiteGroupForm',
|
||||
'VCMemberSelectForm',
|
||||
'VirtualChassisForm',
|
||||
'VirtualDeviceContextForm'
|
||||
)
|
||||
|
||||
INTERFACE_MODE_HELP_TEXT = """
|
||||
@ -386,7 +387,7 @@ class DeviceTypeForm(NetBoxModelForm):
|
||||
'manufacturer', 'model', 'slug', 'part_number', 'tags',
|
||||
)),
|
||||
('Chassis', (
|
||||
'u_height', 'is_full_depth', 'subdevice_role', 'airflow',
|
||||
'u_height', 'is_full_depth', 'subdevice_role', 'airflow', 'vdc_type'
|
||||
)),
|
||||
('Attributes', ('weight', 'weight_unit')),
|
||||
('Images', ('front_image', 'rear_image')),
|
||||
@ -396,7 +397,7 @@ class DeviceTypeForm(NetBoxModelForm):
|
||||
model = DeviceType
|
||||
fields = [
|
||||
'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role', 'airflow',
|
||||
'weight', 'weight_unit', 'front_image', 'rear_image', 'comments', 'tags',
|
||||
'vdc_type','vdc_type','weight', 'weight_unit', 'front_image', 'rear_image', 'comments', 'tags',
|
||||
]
|
||||
widgets = {
|
||||
'airflow': StaticSelect(),
|
||||
@ -1374,6 +1375,14 @@ class PowerOutletForm(ModularDeviceComponentForm):
|
||||
|
||||
|
||||
class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
|
||||
vdc = DynamicModelMultipleChoiceField(
|
||||
queryset=VirtualDeviceContext.objects.all(),
|
||||
required=False,
|
||||
label='Virtual Device Contexts',
|
||||
query_params={
|
||||
'device_id': '$device',
|
||||
}
|
||||
)
|
||||
parent = DynamicModelChoiceField(
|
||||
queryset=Interface.objects.all(),
|
||||
required=False,
|
||||
@ -1448,7 +1457,7 @@ class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
|
||||
)
|
||||
|
||||
fieldsets = (
|
||||
('Interface', ('device', 'module', 'name', 'label', 'type', 'speed', 'duplex', 'description', 'tags')),
|
||||
('Interface', ('device', 'module', 'vdc', 'name', 'label', 'type', 'speed', 'duplex', 'description', 'tags')),
|
||||
('Addressing', ('vrf', 'mac_address', 'wwn')),
|
||||
('Operation', ('mtu', 'tx_power', 'enabled', 'mgmt_only', 'mark_connected')),
|
||||
('Related Interfaces', ('parent', 'bridge', 'lag')),
|
||||
@ -1462,7 +1471,7 @@ class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
|
||||
class Meta:
|
||||
model = Interface
|
||||
fields = [
|
||||
'device', 'module', 'name', 'label', 'type', 'speed', 'duplex', 'enabled', 'parent', 'bridge', 'lag',
|
||||
'device', 'module', 'vdc', 'name', 'label', 'type', 'speed', 'duplex', 'enabled', 'parent', 'bridge', 'lag',
|
||||
'mac_address', 'wwn', 'mtu', 'mgmt_only', 'mark_connected', 'description', 'poe_mode', 'poe_type', 'mode',
|
||||
'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'wireless_lans',
|
||||
'untagged_vlan', 'tagged_vlans', 'vrf', 'tags',
|
||||
@ -1486,6 +1495,13 @@ class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
|
||||
'rf_channel_width': "Populated by selected channel (if set)",
|
||||
}
|
||||
|
||||
def clean_vdc(self):
|
||||
device = self.cleaned_data.get('device')
|
||||
if device.device_type.vdc_type not in [VirtualDeviceContextTypeChoices.CISCO_ASA_CONTEXT, VirtualDeviceContextTypeChoices.CISCO_FTD_INSTANCE]\
|
||||
and len(self.cleaned_data.get('vdc')) > 1:
|
||||
raise forms.ValidationError(f"You cannot assign more then 1 VDC for {device.device_type}")
|
||||
return self.cleaned_data.get('vdc')
|
||||
|
||||
|
||||
class FrontPortForm(ModularDeviceComponentForm):
|
||||
rear_port = DynamicModelChoiceField(
|
||||
@ -1632,3 +1648,73 @@ class InventoryItemRoleForm(NetBoxModelForm):
|
||||
fields = [
|
||||
'name', 'slug', 'color', 'description', 'tags',
|
||||
]
|
||||
|
||||
|
||||
|
||||
class VirtualDeviceContextForm(TenancyForm, NetBoxModelForm):
|
||||
region = DynamicModelChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
initial_params={
|
||||
'sites': '$site'
|
||||
}
|
||||
)
|
||||
site_group = DynamicModelChoiceField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
required=False,
|
||||
initial_params={
|
||||
'sites': '$site'
|
||||
}
|
||||
)
|
||||
site = DynamicModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'region_id': '$region',
|
||||
'group_id': '$site_group',
|
||||
}
|
||||
)
|
||||
location = DynamicModelChoiceField(
|
||||
queryset=Location.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'site_id': '$site'
|
||||
},
|
||||
initial_params={
|
||||
'racks': '$rack'
|
||||
}
|
||||
)
|
||||
rack = DynamicModelChoiceField(
|
||||
queryset=Rack.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'site_id': '$site',
|
||||
'location_id': '$location',
|
||||
}
|
||||
)
|
||||
device = DynamicModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
query_params={
|
||||
'site_id': '$site',
|
||||
'location_id': '$location',
|
||||
'rack_id': '$rack',
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
fieldsets = (
|
||||
('Device', ('region', 'site_group', 'site', 'location', 'rack', 'device')),
|
||||
('Virtual Device Context', ('name', 'identifier', 'primary_ip4', 'primary_ip6', 'tenant_group', 'tenant')),
|
||||
(None, ('tags', ))
|
||||
)
|
||||
class Meta:
|
||||
model = VirtualDeviceContext
|
||||
fields = [
|
||||
'region', 'site_group', 'site', 'location', 'rack',
|
||||
'device', 'name', 'status', 'identifier', 'primary_ip4', 'primary_ip6', 'tenant_group', 'tenant', 'comments'
|
||||
]
|
||||
help_texts = {}
|
||||
widgets = {
|
||||
'primary_ip4': StaticSelect(),
|
||||
'primary_ip6': StaticSelect(),
|
||||
}
|
@ -531,6 +531,10 @@ class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEnd
|
||||
max_length=100,
|
||||
blank=True
|
||||
)
|
||||
vdc = models.ManyToManyField(
|
||||
to='dcim.VirtualDeviceContext',
|
||||
related_name='interfaces'
|
||||
)
|
||||
lag = models.ForeignKey(
|
||||
to='self',
|
||||
on_delete=models.SET_NULL,
|
||||
|
@ -34,6 +34,7 @@ __all__ = (
|
||||
'ModuleType',
|
||||
'Platform',
|
||||
'VirtualChassis',
|
||||
'VirtualDeviceContext',
|
||||
)
|
||||
|
||||
|
||||
@ -123,6 +124,12 @@ class DeviceType(NetBoxModel, WeightMixin):
|
||||
help_text='Parent devices house child devices in device bays. Leave blank '
|
||||
'if this device type is neither a parent nor a child.'
|
||||
)
|
||||
vdc_type = models.CharField(
|
||||
max_length=50,
|
||||
blank=True,
|
||||
choices=VirtualDeviceContextTypeChoices,
|
||||
verbose_name='VDC Type'
|
||||
)
|
||||
airflow = models.CharField(
|
||||
max_length=50,
|
||||
choices=DeviceAirflowChoices,
|
||||
@ -141,7 +148,7 @@ class DeviceType(NetBoxModel, WeightMixin):
|
||||
)
|
||||
|
||||
clone_fields = (
|
||||
'manufacturer', 'u_height', 'is_full_depth', 'subdevice_role', 'airflow', 'weight', 'weight_unit',
|
||||
'manufacturer', 'u_height', 'is_full_depth', 'subdevice_role', 'airflow', 'weight', 'weight_unit', 'vdc_type'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
@ -1129,3 +1136,72 @@ class VirtualChassis(NetBoxModel):
|
||||
)
|
||||
|
||||
return super().delete(*args, **kwargs)
|
||||
|
||||
class VirtualDeviceContext(NetBoxModel):
|
||||
device = models.ForeignKey(
|
||||
to='Device',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='vdcs',
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
name = models.CharField(
|
||||
max_length=64
|
||||
)
|
||||
status = models.CharField(
|
||||
max_length=50,
|
||||
blank=True,
|
||||
choices=VirtualDeviceContextStatusChoices,
|
||||
)
|
||||
identifier = models.PositiveSmallIntegerField(
|
||||
blank=True,
|
||||
null=True,
|
||||
validators=[MaxValueValidator(255)]
|
||||
)
|
||||
primary_ip4 = models.OneToOneField(
|
||||
to='ipam.IPAddress',
|
||||
on_delete=models.SET_NULL,
|
||||
related_name='+',
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name='Primary IPv4'
|
||||
)
|
||||
primary_ip6 = models.OneToOneField(
|
||||
to='ipam.IPAddress',
|
||||
on_delete=models.SET_NULL,
|
||||
related_name='+',
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name='Primary IPv6'
|
||||
)
|
||||
tenant = models.ForeignKey(
|
||||
to='tenancy.Tenant',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='vdcs',
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
comments = models.TextField(
|
||||
blank=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
constraints = (
|
||||
models.UniqueConstraint(
|
||||
fields=('device', 'identifier',),
|
||||
name='%(app_label)s_%(class)s_device_identifiers',
|
||||
violation_error_message="A VDC with this identifier already exists on this device."
|
||||
),
|
||||
models.UniqueConstraint(
|
||||
fields=('device', 'name',),
|
||||
name='%(app_label)s_%(class)s_name',
|
||||
violation_error_message="A VDC with this name already exists on this device."
|
||||
),
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:virtualdevicecontext', kwargs={'pk': self.pk})
|
||||
|
@ -1,10 +1,12 @@
|
||||
import logging
|
||||
|
||||
from django.db.models.signals import post_save, post_delete, pre_delete
|
||||
from django import forms
|
||||
from django.db.models.signals import post_save, post_delete, pre_delete, m2m_changed
|
||||
from django.dispatch import receiver
|
||||
|
||||
from .choices import CableEndChoices, LinkStatusChoices
|
||||
from .models import Cable, CablePath, CableTermination, Device, PathEndpoint, PowerPanel, Rack, Location, VirtualChassis
|
||||
from .choices import CableEndChoices, LinkStatusChoices, VirtualDeviceContextTypeChoices
|
||||
from .models import Cable, CablePath, CableTermination, Device, PathEndpoint, PowerPanel, Rack, Location, \
|
||||
VirtualChassis, VirtualDeviceContext, Interface
|
||||
from .models.cables import trace_paths
|
||||
from .utils import create_cablepath, rebuild_paths
|
||||
|
||||
@ -123,3 +125,15 @@ def nullify_connected_endpoints(instance, **kwargs):
|
||||
|
||||
for cablepath in CablePath.objects.filter(_nodes__contains=instance.cable):
|
||||
cablepath.retrace()
|
||||
|
||||
|
||||
@receiver(m2m_changed, sender=Interface.vdc.through)
|
||||
def enforce_vdc_type_restrictions(instance, **kwargs):
|
||||
if 'action' == 'post_add':
|
||||
device = instance.device
|
||||
if device.device_type.vdc_type not in [VirtualDeviceContextTypeChoices.CISCO_ASA_CONTEXT, VirtualDeviceContextTypeChoices.CISCO_FTD_INSTANCE] \
|
||||
and len(instance.vdc) > 1:
|
||||
print('Error')
|
||||
raise forms.ValidationError({
|
||||
'vdc': f"You cannot assign more then 1 VDC for {device.device_type}"
|
||||
})
|
||||
|
@ -15,6 +15,7 @@ from dcim.models import (
|
||||
PowerPort,
|
||||
RearPort,
|
||||
VirtualChassis,
|
||||
VirtualDeviceContext,
|
||||
)
|
||||
from django_tables2.utils import Accessor
|
||||
from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
|
||||
@ -52,6 +53,7 @@ __all__ = (
|
||||
'PowerPortTable',
|
||||
'RearPortTable',
|
||||
'VirtualChassisTable',
|
||||
'VirtualDeviceContextTable'
|
||||
)
|
||||
|
||||
|
||||
@ -896,3 +898,44 @@ class VirtualChassisTable(NetBoxTable):
|
||||
model = VirtualChassis
|
||||
fields = ('pk', 'id', 'name', 'domain', 'master', 'member_count', 'tags', 'created', 'last_updated',)
|
||||
default_columns = ('pk', 'name', 'domain', 'master', 'member_count')
|
||||
|
||||
|
||||
class VirtualDeviceContextTable(TenancyColumnsMixin, NetBoxTable):
|
||||
name = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
indentifier = tables.Column()
|
||||
device = tables.TemplateColumn(
|
||||
order_by=('_name',),
|
||||
template_code=DEVICE_LINK
|
||||
)
|
||||
status = tables.Column()
|
||||
primary_ip = tables.Column(
|
||||
linkify=True,
|
||||
order_by=('primary_ip4', 'primary_ip6'),
|
||||
verbose_name='IP Address'
|
||||
)
|
||||
primary_ip4 = tables.Column(
|
||||
linkify=True,
|
||||
verbose_name='IPv4 Address'
|
||||
)
|
||||
primary_ip6 = tables.Column(
|
||||
linkify=True,
|
||||
verbose_name='IPv6 Address'
|
||||
)
|
||||
|
||||
comments = columns.MarkdownColumn()
|
||||
|
||||
tags = columns.TagColumn(
|
||||
url_name='dcim:vdc_list'
|
||||
)
|
||||
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = VirtualDeviceContext
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'identifier', 'tenant', 'tenant_group',
|
||||
'primary_ip', 'primary_ip4', 'primary_ip6', 'comments', 'tags', 'created', 'last_updated',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'name', 'identifier', 'tenant', 'primary_ip',
|
||||
)
|
@ -183,6 +183,20 @@ urlpatterns = [
|
||||
path('devices/delete/', views.DeviceBulkDeleteView.as_view(), name='device_bulk_delete'),
|
||||
path('devices/<int:pk>/', include(get_model_urls('dcim', 'device'))),
|
||||
|
||||
# Virtual Device Context
|
||||
path('vdcs/', views.VirtualDeviceContextListView.as_view(), name='virtualdevicecontext_list'),
|
||||
path('vdcs/add/', views.VirtualDeviceContextEditView.as_view(), name='virtualdevicecontext_add'),
|
||||
path('vdcs/import/', views.VirtualDeviceContextBulkImportView.as_view(), name='virtualdevicecontext_import'),
|
||||
path('vdcs/edit/', views.VirtualDeviceContextBulkEditView.as_view(), name='virtualdevicecontext_bulk_edit'),
|
||||
path('vdcs/rename/', views.VirtualDeviceContextBulkRenameView.as_view(), name='virtualdevicecontext_bulk_rename'),
|
||||
path('vdcs/delete/', views.VirtualDeviceContextBulkDeleteView.as_view(), name='virtualdevicecontext_bulk_delete'),
|
||||
path('vdcs/<int:pk>/', views.VirtualDeviceContextView.as_view(), name='virtualdevicecontext'),
|
||||
path('vdcs/<int:pk>/edit/', views.VirtualDeviceContextEditView.as_view(), name='virtualdevicecontext_edit'),
|
||||
path('vdcs/<int:pk>/delete/', views.VirtualDeviceContextDeleteView.as_view(), name='virtualdevicecontext_delete'),
|
||||
path('vdcs/<int:pk>/interfaces/', views.VirtualDeviceContextInterfacesView.as_view(), name='virtualdevicecontext_interfaces'),
|
||||
path('vdcs/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='virtualdevicecontext_changelog', kwargs={'model': VirtualDeviceContext}),
|
||||
path('vdcs/<int:pk>/journal/', ObjectJournalView.as_view(), name='virtualdevicecontext_journal', kwargs={'model': VirtualDeviceContext}),
|
||||
|
||||
# Modules
|
||||
path('modules/', views.ModuleListView.as_view(), name='module_list'),
|
||||
path('modules/add/', views.ModuleEditView.as_view(), name='module_add'),
|
||||
|
@ -3561,3 +3561,66 @@ class PowerFeedBulkDeleteView(generic.BulkDeleteView):
|
||||
|
||||
# Trace view
|
||||
register_model_view(PowerFeed, 'trace', kwargs={'model': PowerFeed})(PathTraceView)
|
||||
|
||||
|
||||
# VDC
|
||||
class VirtualDeviceContextListView(generic.ObjectListView):
|
||||
queryset = VirtualDeviceContext.objects.all()
|
||||
filterset = filtersets.VirtualDeviceContextFilterSet
|
||||
filterset_form = forms.VirtualDeviceContextFilterForm
|
||||
table = tables.VirtualDeviceContextTable
|
||||
|
||||
|
||||
class VirtualDeviceContextView(generic.ObjectView):
|
||||
queryset = VirtualDeviceContext.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
interfaces_table = tables.InterfaceTable(instance.interfaces, user=request.user)
|
||||
interfaces_table.configure(request)
|
||||
|
||||
return {
|
||||
'interfaces_table': interfaces_table,
|
||||
'interface_count': instance.interfaces.count(),
|
||||
}
|
||||
|
||||
|
||||
class VirtualDeviceContextEditView(generic.ObjectEditView):
|
||||
queryset = VirtualDeviceContext.objects.all()
|
||||
form = forms.VirtualDeviceContextForm
|
||||
|
||||
|
||||
class VirtualDeviceContextDeleteView(generic.ObjectDeleteView):
|
||||
queryset = VirtualDeviceContext.objects.all()
|
||||
|
||||
|
||||
class VirtualDeviceContextBulkImportView(generic.BulkImportView):
|
||||
queryset = VirtualDeviceContext.objects.all()
|
||||
model_form = forms.VirtualDeviceContextCSVForm
|
||||
table = tables.VirtualDeviceContextTable
|
||||
|
||||
|
||||
class VirtualDeviceContextBulkRenameView(generic.BulkRenameView):
|
||||
queryset = VirtualDeviceContext.objects.all()
|
||||
filterset = filtersets.VirtualDeviceContextFilterSet
|
||||
table = tables.VirtualDeviceContextTable
|
||||
class VirtualDeviceContextBulkEditView(generic.BulkEditView):
|
||||
queryset = VirtualDeviceContext.objects.all()
|
||||
filterset = filtersets.VirtualDeviceContextFilterSet
|
||||
table = tables.VirtualDeviceContextTable
|
||||
form = forms.VirtualDeviceContextBulkEditForm
|
||||
|
||||
|
||||
class VirtualDeviceContextBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = VirtualDeviceContext.objects.all()
|
||||
filterset = filtersets.VirtualDeviceContextFilterSet
|
||||
table = tables.VirtualDeviceContextTable
|
||||
|
||||
|
||||
class VirtualDeviceContextInterfacesView(DeviceComponentsView):
|
||||
queryset = VirtualDeviceContext.objects.all()
|
||||
child_model = Interface
|
||||
table = tables.DeviceInterfaceTable
|
||||
filterset = filtersets.InterfaceFilterSet
|
||||
|
||||
def get_children(self, request, parent):
|
||||
return self.child_model.objects.restrict(request.user, 'view').filter(vdcs=parent)
|
||||
|
@ -58,6 +58,7 @@ DEVICES_MENU = Menu(
|
||||
label='Devices',
|
||||
items=(
|
||||
get_model_item('dcim', 'device', 'Devices'),
|
||||
get_model_item('dcim', 'virtualdevicecontext', 'Virtual Device Contexts'),
|
||||
get_model_item('dcim', 'module', 'Modules'),
|
||||
get_model_item('dcim', 'devicerole', 'Device Roles'),
|
||||
get_model_item('dcim', 'platform', 'Platforms'),
|
||||
|
75
netbox/templates/dcim/virtualdevicecontext.html
Normal file
75
netbox/templates/dcim/virtualdevicecontext.html
Normal file
@ -0,0 +1,75 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
{% load render_table from django_tables2 %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
<li class="breadcrumb-item"><a href="{% url 'dcim:devicerole_list' %}">Device Roles</a></li>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_controls %}
|
||||
{% if perms.dcim.add_interface %}
|
||||
<a href="{% url 'dcim:interface_add' %}?device={{ object.device.pk }}&vdc={{ object.pk }}" class="btn btn-sm btn-primary">
|
||||
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add Interface
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endblock extra_controls %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-6">
|
||||
<div class="card">
|
||||
<h5 class="card-header">
|
||||
Virtual Device Context
|
||||
</h5>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">Name</th>
|
||||
<td>{{ object.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Device</th>
|
||||
<td>{{ object.device|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Identifier</th>
|
||||
<td>{{ object.identifier|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Primary IPv4</th>
|
||||
<td>
|
||||
{{ object.primary_ip4|placeholder }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Primary IPv6</th>
|
||||
<td>
|
||||
{{ object.primary_ip6|placeholder }}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-12">
|
||||
<div class="card">
|
||||
<h5 class="card-header">Interfaces</h5>
|
||||
<div class="card-body table-responsive">
|
||||
{% render_table interfaces_table 'inc/table.html' %}
|
||||
{% include 'inc/paginator.html' with paginator=interfaces_table.paginator page=interfaces_table.page %}
|
||||
</div>
|
||||
</div>
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
Loading…
Reference in New Issue
Block a user