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 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): class ModuleSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:module-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:module-detail')
device = NestedDeviceSerializer() device = NestedDeviceSerializer()

View File

@ -37,6 +37,7 @@ router.register('inventory-item-templates', views.InventoryItemTemplateViewSet)
router.register('device-roles', views.DeviceRoleViewSet) router.register('device-roles', views.DeviceRoleViewSet)
router.register('platforms', views.PlatformViewSet) router.register('platforms', views.PlatformViewSet)
router.register('devices', views.DeviceViewSet) router.register('devices', views.DeviceViewSet)
router.register('vdcs', views.VirtualDeviceContextViewSet)
router.register('modules', views.ModuleViewSet) router.register('modules', views.ModuleViewSet)
# Device components # Device components

View File

@ -538,6 +538,14 @@ class DeviceViewSet(ConfigContextQuerySetMixin, NetBoxModelViewSet):
return Response(response) 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): class ModuleViewSet(NetBoxModelViewSet):
queryset = Module.objects.prefetch_related( queryset = Module.objects.prefetch_related(
'device', 'module_bay', 'module_type__manufacturer', 'tags', 'device', 'module_bay', 'module_type__manufacturer', 'tags',

View File

@ -1399,3 +1399,74 @@ class PowerFeedPhaseChoices(ChoiceSet):
(PHASE_SINGLE, 'Single phase'), (PHASE_SINGLE, 'Single phase'),
(PHASE_3PHASE, 'Three-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', 'SiteFilterSet',
'SiteGroupFilterSet', 'SiteGroupFilterSet',
'VirtualChassisFilterSet', 'VirtualChassisFilterSet',
'VirtualDeviceContextFilterSet',
) )
@ -434,6 +435,9 @@ class DeviceTypeFilterSet(NetBoxModelFilterSet):
to_field_name='slug', to_field_name='slug',
label='Manufacturer (slug)', label='Manufacturer (slug)',
) )
vdc_type = django_filters.MultipleChoiceFilter(
choices=VirtualDeviceContextTypeChoices
)
has_front_image = django_filters.BooleanFilter( has_front_image = django_filters.BooleanFilter(
label='Has a front image', label='Has a front image',
method='_has_front_image' method='_has_front_image'
@ -482,7 +486,7 @@ class DeviceTypeFilterSet(NetBoxModelFilterSet):
class Meta: class Meta:
model = DeviceType model = DeviceType
fields = [ 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): def search(self, queryset, name, value):
@ -1009,6 +1013,30 @@ class DeviceFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilter
return queryset.exclude(devicebays__isnull=value) 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): class ModuleFilterSet(NetBoxModelFilterSet):
manufacturer_id = django_filters.ModelMultipleChoiceFilter( manufacturer_id = django_filters.ModelMultipleChoiceFilter(
field_name='module_type__manufacturer', field_name='module_type__manufacturer',

View File

@ -54,6 +54,7 @@ __all__ = (
'SiteBulkEditForm', 'SiteBulkEditForm',
'SiteGroupBulkEditForm', 'SiteGroupBulkEditForm',
'VirtualChassisBulkEditForm', 'VirtualChassisBulkEditForm',
'VirtualDeviceContextBulkEditForm'
) )
@ -1325,3 +1326,20 @@ class InventoryItemRoleBulkEditForm(NetBoxModelBulkEditForm):
(None, ('color', 'description')), (None, ('color', 'description')),
) )
nullable_fields = ('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', 'SiteCSVForm',
'SiteGroupCSVForm', 'SiteGroupCSVForm',
'VirtualChassisCSVForm', 'VirtualChassisCSVForm',
'VirtualDeviceContextCSVForm'
) )
@ -1083,3 +1084,25 @@ class PowerFeedCSVForm(NetBoxModelCSVForm):
f"location__{self.fields['location'].to_field_name}": data.get('location'), f"location__{self.fields['location'].to_field_name}": data.get('location'),
} }
self.fields['rack'].queryset = self.fields['rack'].queryset.filter(**params) 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', 'SiteFilterForm',
'SiteGroupFilterForm', 'SiteGroupFilterForm',
'VirtualChassisFilterForm', 'VirtualChassisFilterForm',
'VirtualDeviceContextFilterForm'
) )
@ -372,7 +373,7 @@ class DeviceTypeFilterForm(NetBoxModelFilterSetForm):
model = DeviceType model = DeviceType
fieldsets = ( fieldsets = (
(None, ('q', 'tag')), (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')), ('Images', ('has_front_image', 'has_rear_image')),
('Components', ( ('Components', (
'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces', 'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces',
@ -396,6 +397,10 @@ class DeviceTypeFilterForm(NetBoxModelFilterSetForm):
choices=add_blank_choice(DeviceAirflowChoices), choices=add_blank_choice(DeviceAirflowChoices),
required=False required=False
) )
vdc_type = MultipleChoiceField(
choices=add_blank_choice(VirtualDeviceContextTypeChoices),
required=False
)
has_front_image = forms.NullBooleanField( has_front_image = forms.NullBooleanField(
required=False, required=False,
label='Has a front image', label='Has a front image',
@ -728,6 +733,37 @@ class DeviceFilterForm(
tag = TagFilterField(model) 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): class ModuleFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, NetBoxModelFilterSetForm):
model = Module model = Module
fieldsets = ( fieldsets = (

View File

@ -62,6 +62,7 @@ __all__ = (
'SiteGroupForm', 'SiteGroupForm',
'VCMemberSelectForm', 'VCMemberSelectForm',
'VirtualChassisForm', 'VirtualChassisForm',
'VirtualDeviceContextForm'
) )
INTERFACE_MODE_HELP_TEXT = """ INTERFACE_MODE_HELP_TEXT = """
@ -386,7 +387,7 @@ class DeviceTypeForm(NetBoxModelForm):
'manufacturer', 'model', 'slug', 'part_number', 'tags', 'manufacturer', 'model', 'slug', 'part_number', 'tags',
)), )),
('Chassis', ( ('Chassis', (
'u_height', 'is_full_depth', 'subdevice_role', 'airflow', 'u_height', 'is_full_depth', 'subdevice_role', 'airflow', 'vdc_type'
)), )),
('Attributes', ('weight', 'weight_unit')), ('Attributes', ('weight', 'weight_unit')),
('Images', ('front_image', 'rear_image')), ('Images', ('front_image', 'rear_image')),
@ -396,7 +397,7 @@ class DeviceTypeForm(NetBoxModelForm):
model = DeviceType model = DeviceType
fields = [ fields = [
'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role', 'airflow', '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 = { widgets = {
'airflow': StaticSelect(), 'airflow': StaticSelect(),
@ -1374,6 +1375,14 @@ class PowerOutletForm(ModularDeviceComponentForm):
class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm): class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
vdc = DynamicModelMultipleChoiceField(
queryset=VirtualDeviceContext.objects.all(),
required=False,
label='Virtual Device Contexts',
query_params={
'device_id': '$device',
}
)
parent = DynamicModelChoiceField( parent = DynamicModelChoiceField(
queryset=Interface.objects.all(), queryset=Interface.objects.all(),
required=False, required=False,
@ -1448,7 +1457,7 @@ class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
) )
fieldsets = ( 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')), ('Addressing', ('vrf', 'mac_address', 'wwn')),
('Operation', ('mtu', 'tx_power', 'enabled', 'mgmt_only', 'mark_connected')), ('Operation', ('mtu', 'tx_power', 'enabled', 'mgmt_only', 'mark_connected')),
('Related Interfaces', ('parent', 'bridge', 'lag')), ('Related Interfaces', ('parent', 'bridge', 'lag')),
@ -1462,7 +1471,7 @@ class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
class Meta: class Meta:
model = Interface model = Interface
fields = [ 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', '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', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'wireless_lans',
'untagged_vlan', 'tagged_vlans', 'vrf', 'tags', 'untagged_vlan', 'tagged_vlans', 'vrf', 'tags',
@ -1486,6 +1495,13 @@ class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
'rf_channel_width': "Populated by selected channel (if set)", '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): class FrontPortForm(ModularDeviceComponentForm):
rear_port = DynamicModelChoiceField( rear_port = DynamicModelChoiceField(
@ -1632,3 +1648,73 @@ class InventoryItemRoleForm(NetBoxModelForm):
fields = [ fields = [
'name', 'slug', 'color', 'description', 'tags', '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, max_length=100,
blank=True blank=True
) )
vdc = models.ManyToManyField(
to='dcim.VirtualDeviceContext',
related_name='interfaces'
)
lag = models.ForeignKey( lag = models.ForeignKey(
to='self', to='self',
on_delete=models.SET_NULL, on_delete=models.SET_NULL,

View File

@ -34,6 +34,7 @@ __all__ = (
'ModuleType', 'ModuleType',
'Platform', 'Platform',
'VirtualChassis', 'VirtualChassis',
'VirtualDeviceContext',
) )
@ -123,6 +124,12 @@ class DeviceType(NetBoxModel, WeightMixin):
help_text='Parent devices house child devices in device bays. Leave blank ' help_text='Parent devices house child devices in device bays. Leave blank '
'if this device type is neither a parent nor a child.' '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( airflow = models.CharField(
max_length=50, max_length=50,
choices=DeviceAirflowChoices, choices=DeviceAirflowChoices,
@ -141,7 +148,7 @@ class DeviceType(NetBoxModel, WeightMixin):
) )
clone_fields = ( 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: class Meta:
@ -1129,3 +1136,72 @@ class VirtualChassis(NetBoxModel):
) )
return super().delete(*args, **kwargs) 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 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 django.dispatch import receiver
from .choices import CableEndChoices, LinkStatusChoices from .choices import CableEndChoices, LinkStatusChoices, VirtualDeviceContextTypeChoices
from .models import Cable, CablePath, CableTermination, Device, PathEndpoint, PowerPanel, Rack, Location, VirtualChassis from .models import Cable, CablePath, CableTermination, Device, PathEndpoint, PowerPanel, Rack, Location, \
VirtualChassis, VirtualDeviceContext, Interface
from .models.cables import trace_paths from .models.cables import trace_paths
from .utils import create_cablepath, rebuild_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): for cablepath in CablePath.objects.filter(_nodes__contains=instance.cable):
cablepath.retrace() 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, PowerPort,
RearPort, RearPort,
VirtualChassis, VirtualChassis,
VirtualDeviceContext,
) )
from django_tables2.utils import Accessor from django_tables2.utils import Accessor
from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
@ -52,6 +53,7 @@ __all__ = (
'PowerPortTable', 'PowerPortTable',
'RearPortTable', 'RearPortTable',
'VirtualChassisTable', 'VirtualChassisTable',
'VirtualDeviceContextTable'
) )
@ -896,3 +898,44 @@ class VirtualChassisTable(NetBoxTable):
model = VirtualChassis model = VirtualChassis
fields = ('pk', 'id', 'name', 'domain', 'master', 'member_count', 'tags', 'created', 'last_updated',) fields = ('pk', 'id', 'name', 'domain', 'master', 'member_count', 'tags', 'created', 'last_updated',)
default_columns = ('pk', 'name', 'domain', 'master', 'member_count') 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/delete/', views.DeviceBulkDeleteView.as_view(), name='device_bulk_delete'),
path('devices/<int:pk>/', include(get_model_urls('dcim', 'device'))), 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 # Modules
path('modules/', views.ModuleListView.as_view(), name='module_list'), path('modules/', views.ModuleListView.as_view(), name='module_list'),
path('modules/add/', views.ModuleEditView.as_view(), name='module_add'), path('modules/add/', views.ModuleEditView.as_view(), name='module_add'),

View File

@ -3561,3 +3561,66 @@ class PowerFeedBulkDeleteView(generic.BulkDeleteView):
# Trace view # Trace view
register_model_view(PowerFeed, 'trace', kwargs={'model': PowerFeed})(PathTraceView) 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', label='Devices',
items=( items=(
get_model_item('dcim', 'device', 'Devices'), get_model_item('dcim', 'device', 'Devices'),
get_model_item('dcim', 'virtualdevicecontext', 'Virtual Device Contexts'),
get_model_item('dcim', 'module', 'Modules'), get_model_item('dcim', 'module', 'Modules'),
get_model_item('dcim', 'devicerole', 'Device Roles'), get_model_item('dcim', 'devicerole', 'Device Roles'),
get_model_item('dcim', 'platform', 'Platforms'), 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 %}