This commit is contained in:
Daniel Sheppard 2022-10-28 10:50:19 -05:00
parent 0b24d3d892
commit 11471a22de
17 changed files with 587 additions and 10 deletions

View File

@ -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()

View File

@ -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

View File

@ -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',

View File

@ -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'),
)
),
)

View File

@ -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',

View File

@ -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', )

View File

@ -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 = {}

View File

@ -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 = (

View File

@ -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(),
}

View File

@ -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,

View File

@ -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})

View File

@ -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}"
})

View File

@ -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',
)

View File

@ -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'),

View File

@ -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)

View File

@ -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'),

View 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 %}