Skip to content

Commit

Permalink
Closes: #7854 - Add VDC/Instances/etc (#10787)
Browse files Browse the repository at this point in the history
* Work on #7854

* Move to new URL scheme.

* Fix PEP8 errors

* Fix PEP8 errors

* Add GraphQL and fix primary_ip missing

* Fix PEP8 on GQL Type

* Fix missing NestedSerializer.

* Fix missing NestedSerializer & rename VDC to VDCs

* Fix migration

* Change Validation for identifier

* Fix missing migration

* Rebase to feature

* Post-review changes

* Remove VDC Type
* Remove M2M Enforcement logic

* Interface related changes

* Add filter fields to filterset for Interface filter
* Add form field to filterset form for Interface filter
* Add VDC display to interface detail template

* Remove VirtualDeviceContextTypeChoices

* Accommodate recent changes in feature branch

* Add tests
Add missing search()

* Update tests, and fix model form

* Update test_api

* Update test_api.InterfaceTest create_data

* Fix issue with tests

* Update interface serializer

* Update serializer and tests

* Update status to be required

* Remove error message for constraint

* Remove extraneous import

* Re-ordered devices menu to place VDC below virtual chassis

* Add helptext for `identifier` field

* Fix breadcrumb link

* Remove add interface link

* Add missing tenant and status fields

* Changes to tests as per Jeremy

* Change for #9623

Co-authored-by: Jeremy Stretch <[email protected]>

* Update filterset form for status field

* Remove Rename View

* Change tabs to spaces

* Update netbox/dcim/tables/devices.py

Co-authored-by: Jeremy Stretch <[email protected]>

* Update netbox/dcim/tables/devices.py

Co-authored-by: Jeremy Stretch <[email protected]>

* Fix tenant in bulk_edit

* Apply suggestions from code review

Co-authored-by: Jeremy Stretch <[email protected]>

* Add status field to table.

* Re-order table fields.

Co-authored-by: Jeremy Stretch <[email protected]>
  • Loading branch information
DanSheps and jeremystretch authored Nov 11, 2022
1 parent 653acbf commit b374351
Show file tree
Hide file tree
Showing 27 changed files with 890 additions and 14 deletions.
10 changes: 10 additions & 0 deletions netbox/dcim/api/nested_serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
'NestedSiteSerializer',
'NestedSiteGroupSerializer',
'NestedVirtualChassisSerializer',
'NestedVirtualDeviceContextSerializer',
]


Expand Down Expand Up @@ -466,3 +467,12 @@ class NestedPowerFeedSerializer(WritableNestedSerializer):
class Meta:
model = models.PowerFeed
fields = ['id', 'url', 'display', 'name', 'cable', '_occupied']


class NestedVirtualDeviceContextSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualdevicecontext-detail')
device = NestedDeviceSerializer()

class Meta:
model = models.VirtualDeviceContext
fields = ['id', 'url', 'display', 'name', 'identifier', 'device']
36 changes: 29 additions & 7 deletions netbox/dcim/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -671,6 +671,22 @@ def get_parent_device(self, obj):
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', 'identifier', 'tenant', 'primary_ip', 'primary_ip4',
'primary_ip6', 'status', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
]


class ModuleSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:module-detail')
device = NestedDeviceSerializer()
Expand Down Expand Up @@ -823,6 +839,12 @@ class Meta:
class InterfaceSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail')
device = NestedDeviceSerializer()
vdcs = SerializedPKRelatedField(
queryset=VirtualDeviceContext.objects.all(),
serializer=NestedVirtualDeviceContextSerializer,
required=False,
many=True
)
module = ComponentNestedModuleSerializer(
required=False,
allow_null=True
Expand Down Expand Up @@ -859,13 +881,13 @@ class InterfaceSerializer(NetBoxModelSerializer, CabledObjectSerializer, Connect
class Meta:
model = Interface
fields = [
'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'enabled', 'parent', 'bridge', 'lag',
'mtu', 'mac_address', 'speed', 'duplex', 'wwn', 'mgmt_only', 'description', 'mode', 'rf_role', 'rf_channel',
'poe_mode', 'poe_type', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'untagged_vlan',
'tagged_vlans', 'mark_connected', 'cable', 'cable_end', 'wireless_link', 'link_peers', 'link_peers_type',
'wireless_lans', 'vrf', 'l2vpn_termination', 'connected_endpoints', 'connected_endpoints_type',
'connected_endpoints_reachable', 'tags', 'custom_fields', 'created', 'last_updated', 'count_ipaddresses',
'count_fhrp_groups', '_occupied',
'id', 'url', 'display', 'device', 'vdcs', 'module', 'name', 'label', 'type', 'enabled', 'parent', 'bridge',
'lag', 'mtu', 'mac_address', 'speed', 'duplex', 'wwn', 'mgmt_only', 'description', 'mode', 'rf_role',
'rf_channel', 'poe_mode', 'poe_type', 'rf_channel_frequency', 'rf_channel_width', 'tx_power',
'untagged_vlan', 'tagged_vlans', 'mark_connected', 'cable', 'cable_end', 'wireless_link', 'link_peers',
'link_peers_type', 'wireless_lans', 'vrf', 'l2vpn_termination', 'connected_endpoints',
'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields', 'created',
'last_updated', 'count_ipaddresses', 'count_fhrp_groups', '_occupied',
]

def validate(self, data):
Expand Down
1 change: 1 addition & 0 deletions netbox/dcim/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
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
Expand Down
8 changes: 8 additions & 0 deletions netbox/dcim/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,14 @@ def napalm(self, request, pk):
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',
Expand Down
17 changes: 17 additions & 0 deletions netbox/dcim/choices.py
Original file line number Diff line number Diff line change
Expand Up @@ -1399,3 +1399,20 @@ 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'),
]
58 changes: 57 additions & 1 deletion netbox/dcim/filtersets.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
'SiteFilterSet',
'SiteGroupFilterSet',
'VirtualChassisFilterSet',
'VirtualDeviceContextFilterSet',
)


Expand Down Expand Up @@ -482,7 +483,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',
]

def search(self, queryset, name, value):
Expand Down Expand Up @@ -1009,6 +1010,44 @@ def _device_bays(self, queryset, name, 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', ]

def search(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(
Q(name__icontains=value) |
Q(identifier=value.strip())
).distinct()

def _has_primary_ip(self, queryset, name, value):
params = Q(primary_ip4__isnull=False) | Q(primary_ip6__isnull=False)
if value:
return queryset.filter(params)
return queryset.exclude(params)


class ModuleFilterSet(NetBoxModelFilterSet):
manufacturer_id = django_filters.ModelMultipleChoiceFilter(
field_name='module_type__manufacturer',
Expand Down Expand Up @@ -1342,6 +1381,23 @@ class InterfaceFilterSet(
to_field_name='rd',
label='VRF (RD)',
)
vdc_id = django_filters.ModelMultipleChoiceFilter(
field_name='vdcs',
queryset=VirtualDeviceContext.objects.all(),
label='Virtual Device Context',
)
vdc_identifier = django_filters.ModelMultipleChoiceFilter(
field_name='vdcs__identifier',
queryset=VirtualDeviceContext.objects.all(),
to_field_name='identifier',
label='Virtual Device Context (Identifier)',
)
vdc = django_filters.ModelMultipleChoiceFilter(
field_name='vdcs__name',
queryset=VirtualDeviceContext.objects.all(),
to_field_name='name',
label='Virtual Device Context',
)

class Meta:
model = Interface
Expand Down
22 changes: 22 additions & 0 deletions netbox/dcim/forms/bulk_edit.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
'SiteBulkEditForm',
'SiteGroupBulkEditForm',
'VirtualChassisBulkEditForm',
'VirtualDeviceContextBulkEditForm'
)


Expand Down Expand Up @@ -1398,3 +1399,24 @@ 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()
)
tenant = DynamicModelChoiceField(
queryset=Tenant.objects.all(),
required=False
)
model = VirtualDeviceContext
fieldsets = (
(None, ('device', 'status', 'tenant')),
)
nullable_fields = ('device', 'tenant', )
23 changes: 23 additions & 0 deletions netbox/dcim/forms/bulk_import.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
'SiteCSVForm',
'SiteGroupCSVForm',
'VirtualChassisCSVForm',
'VirtualDeviceContextCSVForm'
)


Expand Down Expand Up @@ -1084,3 +1085,25 @@ def __init__(self, data=None, *args, **kwargs):
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 = {}
43 changes: 42 additions & 1 deletion netbox/dcim/forms/filtersets.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
'SiteFilterForm',
'SiteGroupFilterForm',
'VirtualChassisFilterForm',
'VirtualDeviceContextFilterForm'
)


Expand Down Expand Up @@ -728,6 +729,37 @@ class DeviceFilterForm(
tag = TagFilterField(model)


class VirtualDeviceContextFilterForm(
TenancyFilterForm,
NetBoxModelFilterSetForm
):
model = VirtualDeviceContext
fieldsets = (
(None, ('q', 'filter', 'tag')),
('Hardware', ('device', 'status', )),
('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 = (
Expand Down Expand Up @@ -1075,9 +1107,18 @@ class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
('Addressing', ('vrf_id', 'mac_address', 'wwn')),
('PoE', ('poe_mode', 'poe_type')),
('Wireless', ('rf_role', 'rf_channel', 'rf_channel_width', 'tx_power')),
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id',
'device_id', 'vdc_id')),
('Connection', ('cabled', 'connected', 'occupied')),
)
vdc_id = DynamicModelMultipleChoiceField(
queryset=VirtualDeviceContext.objects.all(),
required=False,
query_params={
'device_id': '$device_id',
},
label=_('Virtual Device Context')
)
kind = MultipleChoiceField(
choices=InterfaceKindChoices,
required=False
Expand Down
Loading

0 comments on commit b374351

Please sign in to comment.