Merge branch 'develop' into feature

This commit is contained in:
jeremystretch 2022-03-18 13:17:11 -04:00
commit 8d53b46e82
38 changed files with 609 additions and 388 deletions

View File

@ -58,7 +58,7 @@ jobs:
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip
pip install -r requirements.txt pip install -r requirements.txt
pip install pycodestyle coverage pip install pycodestyle coverage tblib
- name: Build documentation - name: Build documentation
run: mkdocs build run: mkdocs build

View File

@ -87,7 +87,7 @@ mkdocs-material
mkdocstrings mkdocstrings
# Library for manipulating IP prefixes and addresses # Library for manipulating IP prefixes and addresses
# https://github.com/drkjam/netaddr # https://github.com/netaddr/netaddr
netaddr netaddr
# Fork of PIL (Python Imaging Library) for image processing # Fork of PIL (Python Imaging Library) for image processing

View File

@ -2,6 +2,18 @@
## v3.1.10 (FUTURE) ## v3.1.10 (FUTURE)
### Enhancements
* [#8457](https://github.com/netbox-community/netbox/issues/8457) - Enable adding non-racked devices from site & location views
* [#8553](https://github.com/netbox-community/netbox/issues/8553) - Add missing object types to global search form
* [#8575](https://github.com/netbox-community/netbox/issues/8575) - Add rack columns to cables list
* [#8645](https://github.com/netbox-community/netbox/issues/8645) - Enable filtering objects by assigned contacts & contact roles
### Bug Fixes
* [#8820](https://github.com/netbox-community/netbox/issues/8820) - Fix navbar background color in dark mode
* [#8850](https://github.com/netbox-community/netbox/issues/8850) - Show airflow field on device REST API serializer when config context data is included
--- ---
## v3.1.9 (2022-03-07) ## v3.1.9 (2022-03-07)

View File

@ -3,8 +3,8 @@ from django.db.models import Q
from dcim.filtersets import CableTerminationFilterSet from dcim.filtersets import CableTerminationFilterSet
from dcim.models import Region, Site, SiteGroup from dcim.models import Region, Site, SiteGroup
from netbox.filtersets import ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, NetBoxModelFilterSet from netbox.filtersets import ChangeLoggedModelFilterSet, NetBoxModelFilterSet, OrganizationalModelFilterSet
from tenancy.filtersets import TenancyFilterSet from tenancy.filtersets import ContactModelFilterSet, TenancyFilterSet
from utilities.filters import TreeNodeMultipleChoiceFilter from utilities.filters import TreeNodeMultipleChoiceFilter
from .choices import * from .choices import *
from .models import * from .models import *
@ -18,7 +18,7 @@ __all__ = (
) )
class ProviderFilterSet(NetBoxModelFilterSet): class ProviderFilterSet(NetBoxModelFilterSet, ContactModelFilterSet):
region_id = TreeNodeMultipleChoiceFilter( region_id = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(), queryset=Region.objects.all(),
field_name='circuits__terminations__site__region', field_name='circuits__terminations__site__region',
@ -107,7 +107,7 @@ class CircuitTypeFilterSet(OrganizationalModelFilterSet):
fields = ['id', 'name', 'slug', 'description'] fields = ['id', 'name', 'slug', 'description']
class CircuitFilterSet(NetBoxModelFilterSet, TenancyFilterSet): class CircuitFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
provider_id = django_filters.ModelMultipleChoiceFilter( provider_id = django_filters.ModelMultipleChoiceFilter(
queryset=Provider.objects.all(), queryset=Provider.objects.all(),
label='Provider (ID)', label='Provider (ID)',

View File

@ -5,7 +5,7 @@ from circuits.choices import CircuitStatusChoices
from circuits.models import * from circuits.models import *
from dcim.models import Region, Site, SiteGroup from dcim.models import Region, Site, SiteGroup
from netbox.forms import NetBoxModelFilterSetForm from netbox.forms import NetBoxModelFilterSetForm
from tenancy.forms import TenancyFilterForm from tenancy.forms import TenancyFilterForm, ContactModelFilterForm
from utilities.forms import DynamicModelMultipleChoiceField, StaticSelectMultiple, TagFilterField from utilities.forms import DynamicModelMultipleChoiceField, StaticSelectMultiple, TagFilterField
__all__ = ( __all__ = (
@ -16,12 +16,13 @@ __all__ = (
) )
class ProviderFilterForm(NetBoxModelFilterSetForm): class ProviderFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
model = Provider model = Provider
fieldsets = ( fieldsets = (
(None, ('q', 'tag')), (None, ('q', 'tag')),
('Location', ('region_id', 'site_group_id', 'site_id')), ('Location', ('region_id', 'site_group_id', 'site_id')),
('ASN', ('asn',)), ('ASN', ('asn',)),
('Contacts', ('contact', 'contact_role')),
) )
region_id = DynamicModelMultipleChoiceField( region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(), queryset=Region.objects.all(),
@ -72,7 +73,7 @@ class CircuitTypeFilterForm(NetBoxModelFilterSetForm):
tag = TagFilterField(model) tag = TagFilterField(model)
class CircuitFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm):
model = Circuit model = Circuit
fieldsets = ( fieldsets = (
(None, ('q', 'tag')), (None, ('q', 'tag')),
@ -80,6 +81,7 @@ class CircuitFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
('Attributes', ('type_id', 'status', 'commit_rate')), ('Attributes', ('type_id', 'status', 'commit_rate')),
('Location', ('region_id', 'site_group_id', 'site_id')), ('Location', ('region_id', 'site_group_id', 'site_id')),
('Tenant', ('tenant_group_id', 'tenant_id')), ('Tenant', ('tenant_group_id', 'tenant_id')),
('Contacts', ('contact', 'contact_role')),
) )
type_id = DynamicModelMultipleChoiceField( type_id = DynamicModelMultipleChoiceField(
queryset=CircuitType.objects.all(), queryset=CircuitType.objects.all(),

View File

@ -59,6 +59,9 @@ class CircuitTable(NetBoxTable):
) )
commit_rate = CommitRateColumn() commit_rate = CommitRateColumn()
comments = columns.MarkdownColumn() comments = columns.MarkdownColumn()
contacts = tables.ManyToManyColumn(
linkify_item=True
)
tags = columns.TagColumn( tags = columns.TagColumn(
url_name='circuits:circuit_list' url_name='circuits:circuit_list'
) )
@ -67,7 +70,7 @@ class CircuitTable(NetBoxTable):
model = Circuit model = Circuit
fields = ( fields = (
'pk', 'id', 'cid', 'provider', 'type', 'status', 'tenant', 'termination_a', 'termination_z', 'install_date', 'pk', 'id', 'cid', 'provider', 'type', 'status', 'tenant', 'termination_a', 'termination_z', 'install_date',
'commit_rate', 'description', 'comments', 'tags', 'created', 'last_updated', 'commit_rate', 'description', 'comments', 'contacts', 'tags', 'created', 'last_updated',
) )
default_columns = ( default_columns = (
'pk', 'cid', 'provider', 'type', 'status', 'tenant', 'termination_a', 'termination_z', 'description', 'pk', 'cid', 'provider', 'type', 'status', 'tenant', 'termination_a', 'termination_z', 'description',

View File

@ -19,6 +19,9 @@ class ProviderTable(NetBoxTable):
verbose_name='Circuits' verbose_name='Circuits'
) )
comments = columns.MarkdownColumn() comments = columns.MarkdownColumn()
contacts = tables.ManyToManyColumn(
linkify_item=True
)
tags = columns.TagColumn( tags = columns.TagColumn(
url_name='circuits:provider_list' url_name='circuits:provider_list'
) )
@ -27,7 +30,7 @@ class ProviderTable(NetBoxTable):
model = Provider model = Provider
fields = ( fields = (
'pk', 'id', 'name', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'circuit_count', 'pk', 'id', 'name', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'circuit_count',
'comments', 'tags', 'created', 'last_updated', 'comments', 'contacts', 'tags', 'created', 'last_updated',
) )
default_columns = ('pk', 'name', 'asn', 'account', 'circuit_count') default_columns = ('pk', 'name', 'asn', 'account', 'circuit_count')

View File

@ -576,9 +576,9 @@ class DeviceWithConfigContextSerializer(DeviceSerializer):
class Meta(DeviceSerializer.Meta): class Meta(DeviceSerializer.Meta):
fields = [ fields = [
'id', 'url', 'display', 'name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag', 'id', 'url', 'display', 'name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag',
'site', 'location', 'rack', 'position', 'face', 'parent_device', 'status', 'primary_ip', 'primary_ip4', 'site', 'location', 'rack', 'position', 'face', 'parent_device', 'status', 'airflow', 'primary_ip',
'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'local_context_data', 'primary_ip4', 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments',
'tags', 'custom_fields', 'config_context', 'created', 'last_updated', 'local_context_data', 'tags', 'custom_fields', 'config_context', 'created', 'last_updated',
] ]
@swagger_serializer_method(serializer_or_field=serializers.DictField) @swagger_serializer_method(serializer_or_field=serializers.DictField)

View File

@ -6,8 +6,8 @@ from ipam.models import ASN, VRF
from netbox.filtersets import ( from netbox.filtersets import (
BaseFilterSet, ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, NetBoxModelFilterSet, BaseFilterSet, ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, NetBoxModelFilterSet,
) )
from tenancy.filtersets import TenancyFilterSet from tenancy.filtersets import TenancyFilterSet, ContactModelFilterSet
from tenancy.models import Tenant from tenancy.models import *
from utilities.choices import ColorChoices from utilities.choices import ColorChoices
from utilities.filters import ( from utilities.filters import (
ContentTypeFilter, MultiValueCharFilter, MultiValueMACAddressFilter, MultiValueNumberFilter, MultiValueWWNFilter, ContentTypeFilter, MultiValueCharFilter, MultiValueMACAddressFilter, MultiValueNumberFilter, MultiValueWWNFilter,
@ -67,7 +67,7 @@ __all__ = (
) )
class RegionFilterSet(OrganizationalModelFilterSet): class RegionFilterSet(OrganizationalModelFilterSet, ContactModelFilterSet):
parent_id = django_filters.ModelMultipleChoiceFilter( parent_id = django_filters.ModelMultipleChoiceFilter(
queryset=Region.objects.all(), queryset=Region.objects.all(),
label='Parent region (ID)', label='Parent region (ID)',
@ -84,7 +84,7 @@ class RegionFilterSet(OrganizationalModelFilterSet):
fields = ['id', 'name', 'slug', 'description'] fields = ['id', 'name', 'slug', 'description']
class SiteGroupFilterSet(OrganizationalModelFilterSet): class SiteGroupFilterSet(OrganizationalModelFilterSet, ContactModelFilterSet):
parent_id = django_filters.ModelMultipleChoiceFilter( parent_id = django_filters.ModelMultipleChoiceFilter(
queryset=SiteGroup.objects.all(), queryset=SiteGroup.objects.all(),
label='Parent site group (ID)', label='Parent site group (ID)',
@ -101,7 +101,7 @@ class SiteGroupFilterSet(OrganizationalModelFilterSet):
fields = ['id', 'name', 'slug', 'description'] fields = ['id', 'name', 'slug', 'description']
class SiteFilterSet(NetBoxModelFilterSet, TenancyFilterSet): class SiteFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
status = django_filters.MultipleChoiceFilter( status = django_filters.MultipleChoiceFilter(
choices=SiteStatusChoices, choices=SiteStatusChoices,
null_value=None null_value=None
@ -166,7 +166,7 @@ class SiteFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
return queryset.filter(qs_filter) return queryset.filter(qs_filter)
class LocationFilterSet(TenancyFilterSet, OrganizationalModelFilterSet): class LocationFilterSet(TenancyFilterSet, ContactModelFilterSet, OrganizationalModelFilterSet):
region_id = TreeNodeMultipleChoiceFilter( region_id = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(), queryset=Region.objects.all(),
field_name='site__region', field_name='site__region',
@ -237,7 +237,7 @@ class RackRoleFilterSet(OrganizationalModelFilterSet):
fields = ['id', 'name', 'slug', 'color', 'description'] fields = ['id', 'name', 'slug', 'color', 'description']
class RackFilterSet(NetBoxModelFilterSet, TenancyFilterSet): class RackFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
region_id = TreeNodeMultipleChoiceFilter( region_id = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(), queryset=Region.objects.all(),
field_name='site__region', field_name='site__region',
@ -385,7 +385,7 @@ class RackReservationFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
) )
class ManufacturerFilterSet(OrganizationalModelFilterSet): class ManufacturerFilterSet(OrganizationalModelFilterSet, ContactModelFilterSet):
class Meta: class Meta:
model = Manufacturer model = Manufacturer
@ -724,7 +724,7 @@ class PlatformFilterSet(OrganizationalModelFilterSet):
fields = ['id', 'name', 'slug', 'napalm_driver', 'description'] fields = ['id', 'name', 'slug', 'napalm_driver', 'description']
class DeviceFilterSet(NetBoxModelFilterSet, TenancyFilterSet, LocalConfigContextFilterSet): class DeviceFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSet, LocalConfigContextFilterSet):
manufacturer_id = django_filters.ModelMultipleChoiceFilter( manufacturer_id = django_filters.ModelMultipleChoiceFilter(
field_name='device_type__manufacturer', field_name='device_type__manufacturer',
queryset=Manufacturer.objects.all(), queryset=Manufacturer.objects.all(),
@ -1514,7 +1514,7 @@ class CableFilterSet(TenancyFilterSet, NetBoxModelFilterSet):
return queryset return queryset
class PowerPanelFilterSet(NetBoxModelFilterSet): class PowerPanelFilterSet(NetBoxModelFilterSet, ContactModelFilterSet):
region_id = TreeNodeMultipleChoiceFilter( region_id = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(), queryset=Region.objects.all(),
field_name='site__region', field_name='site__region',

View File

@ -8,7 +8,7 @@ from dcim.models import *
from extras.forms import LocalConfigContextFilterForm from extras.forms import LocalConfigContextFilterForm
from ipam.models import ASN, VRF from ipam.models import ASN, VRF
from netbox.forms import NetBoxModelFilterSetForm from netbox.forms import NetBoxModelFilterSetForm
from tenancy.forms import TenancyFilterForm from tenancy.forms import ContactModelFilterForm, TenancyFilterForm
from utilities.forms import ( from utilities.forms import (
APISelectMultiple, add_blank_choice, ColorField, DynamicModelMultipleChoiceField, FilterForm, StaticSelect, APISelectMultiple, add_blank_choice, ColorField, DynamicModelMultipleChoiceField, FilterForm, StaticSelect,
StaticSelectMultiple, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES, SelectSpeedWidget, StaticSelectMultiple, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES, SelectSpeedWidget,
@ -104,8 +104,12 @@ class DeviceComponentFilterForm(NetBoxModelFilterSetForm):
) )
class RegionFilterForm(NetBoxModelFilterSetForm): class RegionFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
model = Region model = Region
fieldsets = (
(None, ('q', 'tag', 'parent_id')),
('Contacts', ('contact', 'contact_role'))
)
parent_id = DynamicModelMultipleChoiceField( parent_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(), queryset=Region.objects.all(),
required=False, required=False,
@ -114,8 +118,12 @@ class RegionFilterForm(NetBoxModelFilterSetForm):
tag = TagFilterField(model) tag = TagFilterField(model)
class SiteGroupFilterForm(NetBoxModelFilterSetForm): class SiteGroupFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
model = SiteGroup model = SiteGroup
fieldsets = (
(None, ('q', 'tag', 'parent_id')),
('Contacts', ('contact', 'contact_role'))
)
parent_id = DynamicModelMultipleChoiceField( parent_id = DynamicModelMultipleChoiceField(
queryset=SiteGroup.objects.all(), queryset=SiteGroup.objects.all(),
required=False, required=False,
@ -124,12 +132,13 @@ class SiteGroupFilterForm(NetBoxModelFilterSetForm):
tag = TagFilterField(model) tag = TagFilterField(model)
class SiteFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): class SiteFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm):
model = Site model = Site
fieldsets = ( fieldsets = (
(None, ('q', 'tag')), (None, ('q', 'tag')),
('Attributes', ('status', 'region_id', 'group_id', 'asn_id')), ('Attributes', ('status', 'region_id', 'group_id', 'asn_id')),
('Tenant', ('tenant_group_id', 'tenant_id')), ('Tenant', ('tenant_group_id', 'tenant_id')),
('Contacts', ('contact', 'contact_role')),
) )
status = forms.MultipleChoiceField( status = forms.MultipleChoiceField(
choices=SiteStatusChoices, choices=SiteStatusChoices,
@ -154,12 +163,13 @@ class SiteFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
tag = TagFilterField(model) tag = TagFilterField(model)
class LocationFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): class LocationFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm):
model = Location model = Location
fieldsets = ( fieldsets = (
(None, ('q', 'tag')), (None, ('q', 'tag')),
('Parent', ('region_id', 'site_group_id', 'site_id', 'parent_id')), ('Parent', ('region_id', 'site_group_id', 'site_id', 'parent_id')),
('Tenant', ('tenant_group_id', 'tenant_id')), ('Tenant', ('tenant_group_id', 'tenant_id')),
('Contacts', ('contact', 'contact_role')),
) )
region_id = DynamicModelMultipleChoiceField( region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(), queryset=Region.objects.all(),
@ -197,7 +207,7 @@ class RackRoleFilterForm(NetBoxModelFilterSetForm):
tag = TagFilterField(model) tag = TagFilterField(model)
class RackFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm):
model = Rack model = Rack
fieldsets = ( fieldsets = (
(None, ('q', 'tag')), (None, ('q', 'tag')),
@ -205,6 +215,7 @@ class RackFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
('Function', ('status', 'role_id')), ('Function', ('status', 'role_id')),
('Hardware', ('type', 'width', 'serial', 'asset_tag')), ('Hardware', ('type', 'width', 'serial', 'asset_tag')),
('Tenant', ('tenant_group_id', 'tenant_id')), ('Tenant', ('tenant_group_id', 'tenant_id')),
('Contacts', ('contact', 'contact_role')),
) )
region_id = DynamicModelMultipleChoiceField( region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(), queryset=Region.objects.all(),
@ -308,8 +319,12 @@ class RackReservationFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
tag = TagFilterField(model) tag = TagFilterField(model)
class ManufacturerFilterForm(NetBoxModelFilterSetForm): class ManufacturerFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
model = Manufacturer model = Manufacturer
fieldsets = (
(None, ('q', 'tag')),
('Contacts', ('contact', 'contact_role'))
)
tag = TagFilterField(model) tag = TagFilterField(model)
@ -465,7 +480,12 @@ class PlatformFilterForm(NetBoxModelFilterSetForm):
tag = TagFilterField(model) tag = TagFilterField(model)
class DeviceFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, NetBoxModelFilterSetForm): class DeviceFilterForm(
LocalConfigContextFilterForm,
TenancyFilterForm,
ContactModelFilterForm,
NetBoxModelFilterSetForm
):
model = Device model = Device
fieldsets = ( fieldsets = (
(None, ('q', 'tag')), (None, ('q', 'tag')),
@ -473,6 +493,7 @@ class DeviceFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, NetBoxMo
('Operation', ('status', 'role_id', 'airflow', 'serial', 'asset_tag', 'mac_address')), ('Operation', ('status', 'role_id', 'airflow', 'serial', 'asset_tag', 'mac_address')),
('Hardware', ('manufacturer_id', 'device_type_id', 'platform_id')), ('Hardware', ('manufacturer_id', 'device_type_id', 'platform_id')),
('Tenant', ('tenant_group_id', 'tenant_id')), ('Tenant', ('tenant_group_id', 'tenant_id')),
('Contacts', ('contact', 'contact_role')),
('Components', ( ('Components', (
'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces', 'pass_through_ports', 'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces', 'pass_through_ports',
)), )),
@ -741,11 +762,12 @@ class CableFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
tag = TagFilterField(model) tag = TagFilterField(model)
class PowerPanelFilterForm(NetBoxModelFilterSetForm): class PowerPanelFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
model = PowerPanel model = PowerPanel
fieldsets = ( fieldsets = (
(None, ('q', 'tag')), (None, ('q', 'tag')),
('Location', ('region_id', 'site_group_id', 'site_id', 'location_id')) ('Location', ('region_id', 'site_group_id', 'site_id', 'location_id')),
('Contacts', ('contact', 'contact_role')),
) )
region_id = DynamicModelMultipleChoiceField( region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(), queryset=Region.objects.all(),

View File

@ -22,6 +22,12 @@ class CableTable(NetBoxTable):
orderable=False, orderable=False,
verbose_name='Side A' verbose_name='Side A'
) )
rack_a = tables.Column(
accessor=Accessor('termination_a__device__rack'),
orderable=False,
linkify=True,
verbose_name='Rack A'
)
termination_a = tables.Column( termination_a = tables.Column(
accessor=Accessor('termination_a'), accessor=Accessor('termination_a'),
orderable=False, orderable=False,
@ -34,6 +40,12 @@ class CableTable(NetBoxTable):
orderable=False, orderable=False,
verbose_name='Side B' verbose_name='Side B'
) )
rack_b = tables.Column(
accessor=Accessor('termination_b__device__rack'),
orderable=False,
linkify=True,
verbose_name='Rack B'
)
termination_b = tables.Column( termination_b = tables.Column(
accessor=Accessor('termination_b'), accessor=Accessor('termination_b'),
orderable=False, orderable=False,
@ -54,7 +66,7 @@ class CableTable(NetBoxTable):
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = Cable model = Cable
fields = ( fields = (
'pk', 'id', 'label', 'termination_a_parent', 'termination_a', 'termination_b_parent', 'termination_b', 'pk', 'id', 'label', 'termination_a_parent', 'rack_a', 'termination_a', 'termination_b_parent', 'rack_b', 'termination_b',
'status', 'type', 'tenant', 'color', 'length', 'tags', 'created', 'last_updated', 'status', 'type', 'tenant', 'color', 'length', 'tags', 'created', 'last_updated',
) )
default_columns = ( default_columns = (

View File

@ -190,6 +190,9 @@ class DeviceTable(NetBoxTable):
verbose_name='VC Priority' verbose_name='VC Priority'
) )
comments = columns.MarkdownColumn() comments = columns.MarkdownColumn()
contacts = tables.ManyToManyColumn(
linkify_item=True
)
tags = columns.TagColumn( tags = columns.TagColumn(
url_name='dcim:device_list' url_name='dcim:device_list'
) )
@ -199,8 +202,8 @@ class DeviceTable(NetBoxTable):
fields = ( fields = (
'pk', 'id', 'name', 'status', 'tenant', 'device_role', 'manufacturer', 'device_type', 'platform', 'serial', 'pk', 'id', 'name', 'status', 'tenant', 'device_role', 'manufacturer', 'device_type', 'platform', 'serial',
'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'primary_ip', 'airflow', 'primary_ip4', 'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'primary_ip', 'airflow', 'primary_ip4',
'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'tags', 'created', 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'contacts', 'tags',
'last_updated', 'created', 'last_updated',
) )
default_columns = ( default_columns = (
'pk', 'name', 'status', 'tenant', 'site', 'location', 'rack', 'device_role', 'manufacturer', 'device_type', 'pk', 'name', 'status', 'tenant', 'site', 'location', 'rack', 'device_role', 'manufacturer', 'device_type',

View File

@ -41,6 +41,9 @@ class ManufacturerTable(NetBoxTable):
verbose_name='Platforms' verbose_name='Platforms'
) )
slug = tables.Column() slug = tables.Column()
contacts = tables.ManyToManyColumn(
linkify_item=True
)
tags = columns.TagColumn( tags = columns.TagColumn(
url_name='dcim:manufacturer_list' url_name='dcim:manufacturer_list'
) )
@ -49,7 +52,7 @@ class ManufacturerTable(NetBoxTable):
model = Manufacturer model = Manufacturer
fields = ( fields = (
'pk', 'id', 'name', 'devicetype_count', 'inventoryitem_count', 'platform_count', 'description', 'slug', 'pk', 'id', 'name', 'devicetype_count', 'inventoryitem_count', 'platform_count', 'description', 'slug',
'actions', 'created', 'last_updated', 'contacts', 'actions', 'created', 'last_updated',
) )
default_columns = ( default_columns = (
'pk', 'name', 'devicetype_count', 'inventoryitem_count', 'platform_count', 'description', 'slug', 'pk', 'name', 'devicetype_count', 'inventoryitem_count', 'platform_count', 'description', 'slug',

View File

@ -26,13 +26,16 @@ class PowerPanelTable(NetBoxTable):
url_params={'power_panel_id': 'pk'}, url_params={'power_panel_id': 'pk'},
verbose_name='Feeds' verbose_name='Feeds'
) )
contacts = tables.ManyToManyColumn(
linkify_item=True
)
tags = columns.TagColumn( tags = columns.TagColumn(
url_name='dcim:powerpanel_list' url_name='dcim:powerpanel_list'
) )
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = PowerPanel model = PowerPanel
fields = ('pk', 'id', 'name', 'site', 'location', 'powerfeed_count', 'tags', 'created', 'last_updated',) fields = ('pk', 'id', 'name', 'site', 'location', 'powerfeed_count', 'contacts', 'tags', 'created', 'last_updated',)
default_columns = ('pk', 'name', 'site', 'location', 'powerfeed_count') default_columns = ('pk', 'name', 'site', 'location', 'powerfeed_count')

View File

@ -69,6 +69,9 @@ class RackTable(NetBoxTable):
orderable=False, orderable=False,
verbose_name='Power' verbose_name='Power'
) )
contacts = tables.ManyToManyColumn(
linkify_item=True
)
tags = columns.TagColumn( tags = columns.TagColumn(
url_name='dcim:rack_list' url_name='dcim:rack_list'
) )
@ -86,7 +89,7 @@ class RackTable(NetBoxTable):
fields = ( fields = (
'pk', 'id', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'role', 'serial', 'asset_tag', 'pk', 'id', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'role', 'serial', 'asset_tag',
'type', 'width', 'outer_width', 'outer_depth', 'u_height', 'comments', 'device_count', 'get_utilization', 'type', 'width', 'outer_width', 'outer_depth', 'u_height', 'comments', 'device_count', 'get_utilization',
'get_power_utilization', 'tags', 'created', 'last_updated', 'get_power_utilization', 'contacts', 'tags', 'created', 'last_updated',
) )
default_columns = ( default_columns = (
'pk', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'role', 'u_height', 'device_count', 'pk', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'role', 'u_height', 'device_count',

View File

@ -26,6 +26,9 @@ class RegionTable(NetBoxTable):
url_params={'region_id': 'pk'}, url_params={'region_id': 'pk'},
verbose_name='Sites' verbose_name='Sites'
) )
contacts = tables.ManyToManyColumn(
linkify_item=True
)
tags = columns.TagColumn( tags = columns.TagColumn(
url_name='dcim:region_list' url_name='dcim:region_list'
) )
@ -33,7 +36,8 @@ class RegionTable(NetBoxTable):
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = Region model = Region
fields = ( fields = (
'pk', 'id', 'name', 'slug', 'site_count', 'description', 'tags', 'created', 'last_updated', 'actions', 'pk', 'id', 'name', 'slug', 'site_count', 'description', 'contacts', 'tags', 'created', 'last_updated',
'actions',
) )
default_columns = ('pk', 'name', 'site_count', 'description') default_columns = ('pk', 'name', 'site_count', 'description')
@ -51,6 +55,9 @@ class SiteGroupTable(NetBoxTable):
url_params={'group_id': 'pk'}, url_params={'group_id': 'pk'},
verbose_name='Sites' verbose_name='Sites'
) )
contacts = tables.ManyToManyColumn(
linkify_item=True
)
tags = columns.TagColumn( tags = columns.TagColumn(
url_name='dcim:sitegroup_list' url_name='dcim:sitegroup_list'
) )
@ -58,7 +65,8 @@ class SiteGroupTable(NetBoxTable):
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = SiteGroup model = SiteGroup
fields = ( fields = (
'pk', 'id', 'name', 'slug', 'site_count', 'description', 'tags', 'created', 'last_updated', 'actions', 'pk', 'id', 'name', 'slug', 'site_count', 'description', 'contacts', 'tags', 'created', 'last_updated',
'actions',
) )
default_columns = ('pk', 'name', 'site_count', 'description') default_columns = ('pk', 'name', 'site_count', 'description')
@ -90,6 +98,9 @@ class SiteTable(NetBoxTable):
) )
tenant = TenantColumn() tenant = TenantColumn()
comments = columns.MarkdownColumn() comments = columns.MarkdownColumn()
contacts = tables.ManyToManyColumn(
linkify_item=True
)
tags = columns.TagColumn( tags = columns.TagColumn(
url_name='dcim:site_list' url_name='dcim:site_list'
) )
@ -99,7 +110,7 @@ class SiteTable(NetBoxTable):
fields = ( fields = (
'pk', 'id', 'name', 'slug', 'status', 'facility', 'region', 'group', 'tenant', 'asns', 'asn_count', 'pk', 'id', 'name', 'slug', 'status', 'facility', 'region', 'group', 'tenant', 'asns', 'asn_count',
'time_zone', 'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'comments', 'time_zone', 'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'comments',
'tags', 'created', 'last_updated', 'actions', 'contacts', 'tags', 'created', 'last_updated', 'actions',
) )
default_columns = ('pk', 'name', 'status', 'facility', 'region', 'group', 'tenant', 'description') default_columns = ('pk', 'name', 'status', 'facility', 'region', 'group', 'tenant', 'description')
@ -126,6 +137,9 @@ class LocationTable(NetBoxTable):
url_params={'location_id': 'pk'}, url_params={'location_id': 'pk'},
verbose_name='Devices' verbose_name='Devices'
) )
contacts = tables.ManyToManyColumn(
linkify_item=True
)
tags = columns.TagColumn( tags = columns.TagColumn(
url_name='dcim:location_list' url_name='dcim:location_list'
) )
@ -136,7 +150,7 @@ class LocationTable(NetBoxTable):
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = Location model = Location
fields = ( fields = (
'pk', 'id', 'name', 'site', 'tenant', 'rack_count', 'device_count', 'description', 'slug', 'tags', 'pk', 'id', 'name', 'site', 'tenant', 'rack_count', 'device_count', 'description', 'slug', 'contacts',
'actions', 'created', 'last_updated', 'tags', 'actions', 'created', 'last_updated',
) )
default_columns = ('pk', 'name', 'site', 'tenant', 'rack_count', 'device_count', 'description') default_columns = ('pk', 'name', 'site', 'tenant', 'rack_count', 'device_count', 'description')

View File

@ -338,6 +338,11 @@ class SiteView(generic.ObjectView):
'device_count', 'device_count',
cumulative=True cumulative=True
).restrict(request.user, 'view').filter(site=instance) ).restrict(request.user, 'view').filter(site=instance)
nonracked_devices = Device.objects.filter(
site=instance,
position__isnull=True,
parent_bay__isnull=True
).prefetch_related('device_type__manufacturer')
asns = ASN.objects.restrict(request.user, 'view').filter(sites=instance) asns = ASN.objects.restrict(request.user, 'view').filter(sites=instance)
asn_count = asns.count() asn_count = asns.count()
@ -348,6 +353,7 @@ class SiteView(generic.ObjectView):
'stats': stats, 'stats': stats,
'locations': locations, 'locations': locations,
'asns': asns, 'asns': asns,
'nonracked_devices': nonracked_devices,
} }
@ -425,11 +431,17 @@ class LocationView(generic.ObjectView):
).filter(pk__in=location_ids).exclude(pk=instance.pk) ).filter(pk__in=location_ids).exclude(pk=instance.pk)
child_locations_table = tables.LocationTable(child_locations) child_locations_table = tables.LocationTable(child_locations)
child_locations_table.configure(request) child_locations_table.configure(request)
nonracked_devices = Device.objects.filter(
location=instance,
position__isnull=True,
parent_bay__isnull=True
).prefetch_related('device_type__manufacturer')
return { return {
'rack_count': rack_count, 'rack_count': rack_count,
'device_count': device_count, 'device_count': device_count,
'child_locations_table': child_locations_table, 'child_locations_table': child_locations_table,
'nonracked_devices': nonracked_devices,
} }

View File

@ -309,7 +309,7 @@ class PrefixFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
) )
vlan_vid = django_filters.NumberFilter( vlan_vid = django_filters.NumberFilter(
field_name='vlan__vid', field_name='vlan__vid',
label='VLAN number (1-4095)', label='VLAN number (1-4094)',
) )
role_id = django_filters.ModelMultipleChoiceFilter( role_id = django_filters.ModelMultipleChoiceFilter(
queryset=Role.objects.all(), queryset=Role.objects.all(),

View File

@ -388,7 +388,7 @@ class VLANCSVForm(NetBoxModelCSVForm):
model = VLAN model = VLAN
fields = ('site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description') fields = ('site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description')
help_texts = { help_texts = {
'vid': 'Numeric VLAN ID (1-4095)', 'vid': 'Numeric VLAN ID (1-4094)',
'name': 'VLAN name', 'name': 'VLAN name',
} }

View File

@ -1,4 +1,5 @@
from collections import OrderedDict from collections import OrderedDict
from typing import Dict
from circuits.filtersets import CircuitFilterSet, ProviderFilterSet, ProviderNetworkFilterSet from circuits.filtersets import CircuitFilterSet, ProviderFilterSet, ProviderNetworkFilterSet
from circuits.models import Circuit, ProviderNetwork, Provider from circuits.models import Circuit, ProviderNetwork, Provider
@ -26,169 +27,212 @@ from virtualization.models import Cluster, VirtualMachine
from virtualization.tables import ClusterTable, VirtualMachineTable from virtualization.tables import ClusterTable, VirtualMachineTable
SEARCH_MAX_RESULTS = 15 SEARCH_MAX_RESULTS = 15
SEARCH_TYPES = OrderedDict((
# Circuits CIRCUIT_TYPES = OrderedDict(
('provider', { (
'queryset': Provider.objects.annotate( ('provider', {
count_circuits=count_related(Circuit, 'provider') 'queryset': Provider.objects.annotate(
), count_circuits=count_related(Circuit, 'provider')
'filterset': ProviderFilterSet,
'table': ProviderTable,
'url': 'circuits:provider_list',
}),
('circuit', {
'queryset': Circuit.objects.prefetch_related(
'type', 'provider', 'tenant', 'terminations__site'
),
'filterset': CircuitFilterSet,
'table': CircuitTable,
'url': 'circuits:circuit_list',
}),
('providernetwork', {
'queryset': ProviderNetwork.objects.prefetch_related('provider'),
'filterset': ProviderNetworkFilterSet,
'table': ProviderNetworkTable,
'url': 'circuits:providernetwork_list',
}),
# DCIM
('site', {
'queryset': Site.objects.prefetch_related('region', 'tenant'),
'filterset': SiteFilterSet,
'table': SiteTable,
'url': 'dcim:site_list',
}),
('rack', {
'queryset': Rack.objects.prefetch_related('site', 'location', 'tenant', 'role'),
'filterset': RackFilterSet,
'table': RackTable,
'url': 'dcim:rack_list',
}),
('rackreservation', {
'queryset': RackReservation.objects.prefetch_related('site', 'rack', 'user'),
'filterset': RackReservationFilterSet,
'table': RackReservationTable,
'url': 'dcim:rackreservation_list',
}),
('location', {
'queryset': Location.objects.add_related_count(
Location.objects.add_related_count(
Location.objects.all(),
Device,
'location',
'device_count',
cumulative=True
), ),
Rack, 'filterset': ProviderFilterSet,
'location', 'table': ProviderTable,
'rack_count', 'url': 'circuits:provider_list',
cumulative=True }),
).prefetch_related('site'), ('circuit', {
'filterset': LocationFilterSet, 'queryset': Circuit.objects.prefetch_related(
'table': LocationTable, 'type', 'provider', 'tenant', 'terminations__site'
'url': 'dcim:location_list', ),
}), 'filterset': CircuitFilterSet,
('devicetype', { 'table': CircuitTable,
'queryset': DeviceType.objects.prefetch_related('manufacturer').annotate( 'url': 'circuits:circuit_list',
instance_count=count_related(Device, 'device_type') }),
), ('providernetwork', {
'filterset': DeviceTypeFilterSet, 'queryset': ProviderNetwork.objects.prefetch_related('provider'),
'table': DeviceTypeTable, 'filterset': ProviderNetworkFilterSet,
'url': 'dcim:devicetype_list', 'table': ProviderNetworkTable,
}), 'url': 'circuits:providernetwork_list',
('device', { }),
'queryset': Device.objects.prefetch_related( )
'device_type__manufacturer', 'device_role', 'tenant', 'site', 'rack', 'primary_ip4', 'primary_ip6', )
),
'filterset': DeviceFilterSet,
'table': DeviceTable, DCIM_TYPES = OrderedDict(
'url': 'dcim:device_list', (
}), ('site', {
('virtualchassis', { 'queryset': Site.objects.prefetch_related('region', 'tenant'),
'queryset': VirtualChassis.objects.prefetch_related('master').annotate( 'filterset': SiteFilterSet,
member_count=count_related(Device, 'virtual_chassis') 'table': SiteTable,
), 'url': 'dcim:site_list',
'filterset': VirtualChassisFilterSet, }),
'table': VirtualChassisTable, ('rack', {
'url': 'dcim:virtualchassis_list', 'queryset': Rack.objects.prefetch_related('site', 'location', 'tenant', 'role'),
}), 'filterset': RackFilterSet,
('cable', { 'table': RackTable,
'queryset': Cable.objects.all(), 'url': 'dcim:rack_list',
'filterset': CableFilterSet, }),
'table': CableTable, ('rackreservation', {
'url': 'dcim:cable_list', 'queryset': RackReservation.objects.prefetch_related('site', 'rack', 'user'),
}), 'filterset': RackReservationFilterSet,
('powerfeed', { 'table': RackReservationTable,
'queryset': PowerFeed.objects.all(), 'url': 'dcim:rackreservation_list',
'filterset': PowerFeedFilterSet, }),
'table': PowerFeedTable, ('location', {
'url': 'dcim:powerfeed_list', 'queryset': Location.objects.add_related_count(
}), Location.objects.add_related_count(
# Virtualization Location.objects.all(),
('cluster', { Device,
'queryset': Cluster.objects.prefetch_related('type', 'group').annotate( 'location',
device_count=count_related(Device, 'cluster'), 'device_count',
vm_count=count_related(VirtualMachine, 'cluster') cumulative=True
), ),
'filterset': ClusterFilterSet, Rack,
'table': ClusterTable, 'location',
'url': 'virtualization:cluster_list', 'rack_count',
}), cumulative=True
('virtualmachine', { ).prefetch_related('site'),
'queryset': VirtualMachine.objects.prefetch_related( 'filterset': LocationFilterSet,
'cluster', 'tenant', 'platform', 'primary_ip4', 'primary_ip6', 'table': LocationTable,
), 'url': 'dcim:location_list',
'filterset': VirtualMachineFilterSet, }),
'table': VirtualMachineTable, ('devicetype', {
'url': 'virtualization:virtualmachine_list', 'queryset': DeviceType.objects.prefetch_related('manufacturer').annotate(
}), instance_count=count_related(Device, 'device_type')
# IPAM ),
('vrf', { 'filterset': DeviceTypeFilterSet,
'queryset': VRF.objects.prefetch_related('tenant'), 'table': DeviceTypeTable,
'filterset': VRFFilterSet, 'url': 'dcim:devicetype_list',
'table': VRFTable, }),
'url': 'ipam:vrf_list', ('device', {
}), 'queryset': Device.objects.prefetch_related(
('aggregate', { 'device_type__manufacturer', 'device_role', 'tenant', 'site', 'rack', 'primary_ip4', 'primary_ip6',
'queryset': Aggregate.objects.prefetch_related('rir'), ),
'filterset': AggregateFilterSet, 'filterset': DeviceFilterSet,
'table': AggregateTable, 'table': DeviceTable,
'url': 'ipam:aggregate_list', 'url': 'dcim:device_list',
}), }),
('prefix', { ('virtualchassis', {
'queryset': Prefix.objects.prefetch_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role'), 'queryset': VirtualChassis.objects.prefetch_related('master').annotate(
'filterset': PrefixFilterSet, member_count=count_related(Device, 'virtual_chassis')
'table': PrefixTable, ),
'url': 'ipam:prefix_list', 'filterset': VirtualChassisFilterSet,
}), 'table': VirtualChassisTable,
('ipaddress', { 'url': 'dcim:virtualchassis_list',
'queryset': IPAddress.objects.prefetch_related('vrf__tenant', 'tenant'), }),
'filterset': IPAddressFilterSet, ('cable', {
'table': IPAddressTable, 'queryset': Cable.objects.all(),
'url': 'ipam:ipaddress_list', 'filterset': CableFilterSet,
}), 'table': CableTable,
('vlan', { 'url': 'dcim:cable_list',
'queryset': VLAN.objects.prefetch_related('site', 'group', 'tenant', 'role'), }),
'filterset': VLANFilterSet, ('powerfeed', {
'table': VLANTable, 'queryset': PowerFeed.objects.all(),
'url': 'ipam:vlan_list', 'filterset': PowerFeedFilterSet,
}), 'table': PowerFeedTable,
('asn', { 'url': 'dcim:powerfeed_list',
'queryset': ASN.objects.prefetch_related('rir', 'tenant'), }),
'filterset': ASNFilterSet, )
'table': ASNTable, )
'url': 'ipam:asn_list',
}), IPAM_TYPES = OrderedDict(
# Tenancy (
('tenant', { ('vrf', {
'queryset': Tenant.objects.prefetch_related('group'), 'queryset': VRF.objects.prefetch_related('tenant'),
'filterset': TenantFilterSet, 'filterset': VRFFilterSet,
'table': TenantTable, 'table': VRFTable,
'url': 'tenancy:tenant_list', 'url': 'ipam:vrf_list',
}), }),
('contact', { ('aggregate', {
'queryset': Contact.objects.prefetch_related('group', 'assignments').annotate(assignment_count=count_related(ContactAssignment, 'contact')), 'queryset': Aggregate.objects.prefetch_related('rir'),
'filterset': ContactFilterSet, 'filterset': AggregateFilterSet,
'table': ContactTable, 'table': AggregateTable,
'url': 'tenancy:contact_list', 'url': 'ipam:aggregate_list',
}), }),
)) ('prefix', {
'queryset': Prefix.objects.prefetch_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role'),
'filterset': PrefixFilterSet,
'table': PrefixTable,
'url': 'ipam:prefix_list',
}),
('ipaddress', {
'queryset': IPAddress.objects.prefetch_related('vrf__tenant', 'tenant'),
'filterset': IPAddressFilterSet,
'table': IPAddressTable,
'url': 'ipam:ipaddress_list',
}),
('vlan', {
'queryset': VLAN.objects.prefetch_related('site', 'group', 'tenant', 'role'),
'filterset': VLANFilterSet,
'table': VLANTable,
'url': 'ipam:vlan_list',
}),
('asn', {
'queryset': ASN.objects.prefetch_related('rir', 'tenant'),
'filterset': ASNFilterSet,
'table': ASNTable,
'url': 'ipam:asn_list',
}),
)
)
TENANCY_TYPES = OrderedDict(
(
('tenant', {
'queryset': Tenant.objects.prefetch_related('group'),
'filterset': TenantFilterSet,
'table': TenantTable,
'url': 'tenancy:tenant_list',
}),
('contact', {
'queryset': Contact.objects.prefetch_related('group', 'assignments').annotate(
assignment_count=count_related(ContactAssignment, 'contact')),
'filterset': ContactFilterSet,
'table': ContactTable,
'url': 'tenancy:contact_list',
}),
)
)
VIRTUALIZATION_TYPES = OrderedDict(
(
('cluster', {
'queryset': Cluster.objects.prefetch_related('type', 'group').annotate(
device_count=count_related(Device, 'cluster'),
vm_count=count_related(VirtualMachine, 'cluster')
),
'filterset': ClusterFilterSet,
'table': ClusterTable,
'url': 'virtualization:cluster_list',
}),
('virtualmachine', {
'queryset': VirtualMachine.objects.prefetch_related(
'cluster', 'tenant', 'platform', 'primary_ip4', 'primary_ip6',
),
'filterset': VirtualMachineFilterSet,
'table': VirtualMachineTable,
'url': 'virtualization:virtualmachine_list',
}),
)
)
SEARCH_TYPE_HIERARCHY = OrderedDict(
(
("Circuits", CIRCUIT_TYPES),
("DCIM", DCIM_TYPES),
("IPAM", IPAM_TYPES),
("Tenancy", TENANCY_TYPES),
("Virtualization", VIRTUALIZATION_TYPES),
)
)
def build_search_types() -> Dict[str, Dict]:
result = dict()
for app_types in SEARCH_TYPE_HIERARCHY.values():
for name, items in app_types.items():
result[name] = items
return result
SEARCH_TYPES = build_search_types()

View File

@ -1,40 +1,25 @@
from django import forms from django import forms
from netbox.constants import SEARCH_TYPE_HIERARCHY
from utilities.forms import BootstrapMixin from utilities.forms import BootstrapMixin
from .base import * from .base import *
OBJ_TYPE_CHOICES = (
('', 'All Objects'), def build_search_choices():
('Circuits', ( result = list()
('provider', 'Providers'), result.append(('', 'All Objects'))
('circuit', 'Circuits'), for category, items in SEARCH_TYPE_HIERARCHY.items():
)), subcategories = list()
('DCIM', ( for slug, obj in items.items():
('site', 'Sites'), name = obj['queryset'].model._meta.verbose_name_plural
('rack', 'Racks'), name = name[0].upper() + name[1:]
('rackreservation', 'Rack reservations'), subcategories.append((slug, name))
('location', 'Locations'), result.append((category, tuple(subcategories)))
('devicetype', 'Device Types'),
('device', 'Devices'), return tuple(result)
('virtualchassis', 'Virtual chassis'),
('cable', 'Cables'),
('powerfeed', 'Power feeds'), OBJ_TYPE_CHOICES = build_search_choices()
)),
('IPAM', (
('vrf', 'VRFs'),
('aggregate', 'Aggregates'),
('prefix', 'Prefixes'),
('ipaddress', 'IP Addresses'),
('vlan', 'VLANs'),
)),
('Tenancy', (
('tenant', 'Tenants'),
)),
('Virtualization', (
('cluster', 'Clusters'),
('virtualmachine', 'Virtual Machines'),
)),
)
def build_options(): def build_options():

Binary file not shown.

View File

@ -145,9 +145,9 @@ $nav-tabs-link-active-border-color: $gray-800 $gray-800 $nav-tabs-link-active-bg
$nav-pills-link-active-color: $component-active-color; $nav-pills-link-active-color: $component-active-color;
$nav-pills-link-active-bg: $component-active-bg; $nav-pills-link-active-bg: $component-active-bg;
$navbar-light-color: $navbar-dark-color; $navbar-light-color: $darker;
$navbar-light-toggler-icon-bg: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'><path stroke='#{$navbar-light-color}' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/></svg>");
$navbar-light-toggler-border-color: $gray-700; $navbar-light-toggler-border-color: $gray-700;
$navbar-light-toggler-icon-bg: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'><path stroke='#{$navbar-light-toggler-border-color}' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/></svg>");
// Dropdowns // Dropdowns
$dropdown-color: $body-color; $dropdown-color: $body-color;

View File

@ -37,9 +37,7 @@
</tr> </tr>
<tr> <tr>
<th scope="row">Serial Number</th> <th scope="row">Serial Number</th>
<td> <td id="serial_number" class="text-monospace"></td>
<code id="serial_number"></code>
</td>
</tr> </tr>
<tr> <tr>
<th scope="row">OS Version</th> <th scope="row">OS Version</th>

View File

@ -8,6 +8,22 @@
<a href="{{ termination.device.get_absolute_url }}">{{ termination.device }}</a> <a href="{{ termination.device.get_absolute_url }}">{{ termination.device }}</a>
</td> </td>
</tr> </tr>
{% if termination.device.site %}
<tr>
<td>Site</td>
<td>
<a href="{{ termination.device.site.get_absolute_url }}">{{ termination.device.site }}</a>
</td>
</tr>
{% endif %}
{% if termination.device.rack %}
<tr>
<td>Rack</td>
<td>
<a href="{{ termination.device.rack.get_absolute_url }}">{{ termination.device.rack }}</a>
</td>
</tr>
{% endif %}
<tr> <tr>
<td>Type</td> <td>Type</td>
<td> <td>

View File

@ -0,0 +1,62 @@
{% load helpers %}
<div class="card">
<h5 class="card-header">
Non-Racked Devices
</h5>
<div class="card-body">
{% if nonracked_devices %}
<table class="table table-hover">
<tr>
<th>Name</th>
<th>Role</th>
<th>Type</th>
<th colspan="2">Parent Device</th>
</tr>
{% for device in nonracked_devices %}
<tr{% if device.device_type.u_height %} class="warning"{% endif %}>
<td>
<a href="{% url 'dcim:device' pk=device.pk %}">{{ device }}</a>
</td>
<td>{{ device.device_role }}</td>
<td>{{ device.device_type }}</td>
{% if device.parent_bay %}
<td><a href="{{ device.parent_bay.device.get_absolute_url }}">{{ device.parent_bay.device }}</a></td>
<td>{{ device.parent_bay }}</td>
{% else %}
<td colspan="2" class="text-muted">&mdash;</td>
{% endif %}
</tr>
{% endfor %}
</table>
{% else %}
<div class="text-muted">
None
</div>
{% endif %}
</div>
{% if perms.dcim.add_device %}
{% if object|meta:'verbose_name' == 'rack' %}
<div class="card-footer text-end noprint">
<a href="{% url 'dcim:device_add' %}?site={{ object.site.pk }}&rack={{ object.pk }}" class="btn btn-primary btn-sm">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i>
Add a Non-Racked Device
</a>
</div>
{% elif object|meta:'verbose_name' == 'site' %}
<div class="card-footer text-end noprint">
<a href="{% url 'dcim:device_add' %}?site={{ object.pk }}" class="btn btn-primary btn-sm">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i>
Add a Non-Racked Device
</a>
</div>
{% elif object|meta:'verbose_name' == 'location' %}
<div class="card-footer text-end noprint">
<a href="{% url 'dcim:device_add' %}?site={{ object.site.pk }}&location={{ object.pk }}" class="btn btn-primary btn-sm">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i>
Add a Non-Racked Device
</a>
</div>
{% endif %}
{% endif %}
</div>

View File

@ -90,6 +90,7 @@
<div class="col col-md-6"> <div class="col col-md-6">
{% include 'inc/panels/custom_fields.html' %} {% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/contacts.html' %} {% include 'inc/panels/contacts.html' %}
{% include 'dcim/inc/nonracked_devices.html' %}
{% include 'inc/panels/image_attachments.html' %} {% include 'inc/panels/image_attachments.html' %}
{% plugin_right_page object %} {% plugin_right_page object %}
</div> </div>

View File

@ -286,50 +286,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="card"> {% include 'dcim/inc/nonracked_devices.html' %}
<h5 class="card-header">
Non-Racked Devices
</h5>
<div class="card-body">
{% if nonracked_devices %}
<table class="table table-hover">
<tr>
<th>Name</th>
<th>Role</th>
<th>Type</th>
<th colspan="2">Parent Device</th>
</tr>
{% for device in nonracked_devices %}
<tr{% if device.device_type.u_height %} class="warning"{% endif %}>
<td>
<a href="{% url 'dcim:device' pk=device.pk %}">{{ device }}</a>
</td>
<td>{{ device.device_role }}</td>
<td>{{ device.device_type }}</td>
{% if device.parent_bay %}
<td><a href="{{ device.parent_bay.device.get_absolute_url }}">{{ device.parent_bay.device }}</a></td>
<td>{{ device.parent_bay }}</td>
{% else %}
<td colspan="2" class="text-muted">&mdash;</td>
{% endif %}
</tr>
{% endfor %}
</table>
{% else %}
<div class="text-muted">
None
</div>
{% endif %}
</div>
{% if perms.dcim.add_device %}
<div class="card-footer text-end noprint">
<a href="{% url 'dcim:device_add' %}?site={{ object.site.pk }}&rack={{ object.pk }}" class="btn btn-primary btn-sm">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i>
Add a Non-Racked Device
</a>
</div>
{% endif %}
</div>
{% include 'inc/panels/contacts.html' %} {% include 'inc/panels/contacts.html' %}
{% plugin_right_page object %} {% plugin_right_page object %}
</div> </div>

View File

@ -225,6 +225,7 @@
</table> </table>
</div> </div>
</div> </div>
{% include 'dcim/inc/nonracked_devices.html' %}
{% include 'inc/panels/contacts.html' %} {% include 'inc/panels/contacts.html' %}
<div class="card"> <div class="card">
<h5 class="card-header">Locations</h5> <h5 class="card-header">Locations</h5>

View File

@ -10,6 +10,7 @@ __all__ = (
'ContactAssignmentFilterSet', 'ContactAssignmentFilterSet',
'ContactFilterSet', 'ContactFilterSet',
'ContactGroupFilterSet', 'ContactGroupFilterSet',
'ContactModelFilterSet',
'ContactRoleFilterSet', 'ContactRoleFilterSet',
'TenancyFilterSet', 'TenancyFilterSet',
'TenantFilterSet', 'TenantFilterSet',
@ -17,86 +18,6 @@ __all__ = (
) )
#
# Tenancy
#
class TenantGroupFilterSet(OrganizationalModelFilterSet):
parent_id = django_filters.ModelMultipleChoiceFilter(
queryset=TenantGroup.objects.all(),
label='Tenant group (ID)',
)
parent = django_filters.ModelMultipleChoiceFilter(
field_name='parent__slug',
queryset=TenantGroup.objects.all(),
to_field_name='slug',
label='Tenant group (slug)',
)
class Meta:
model = TenantGroup
fields = ['id', 'name', 'slug', 'description']
class TenantFilterSet(NetBoxModelFilterSet):
group_id = TreeNodeMultipleChoiceFilter(
queryset=TenantGroup.objects.all(),
field_name='group',
lookup_expr='in',
label='Tenant group (ID)',
)
group = TreeNodeMultipleChoiceFilter(
queryset=TenantGroup.objects.all(),
field_name='group',
lookup_expr='in',
to_field_name='slug',
label='Tenant group (slug)',
)
class Meta:
model = Tenant
fields = ['id', 'name', 'slug', 'description']
def search(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(
Q(name__icontains=value) |
Q(slug__icontains=value) |
Q(description__icontains=value) |
Q(comments__icontains=value)
)
class TenancyFilterSet(django_filters.FilterSet):
"""
An inheritable FilterSet for models which support Tenant assignment.
"""
tenant_group_id = TreeNodeMultipleChoiceFilter(
queryset=TenantGroup.objects.all(),
field_name='tenant__group',
lookup_expr='in',
label='Tenant Group (ID)',
)
tenant_group = TreeNodeMultipleChoiceFilter(
queryset=TenantGroup.objects.all(),
field_name='tenant__group',
to_field_name='slug',
lookup_expr='in',
label='Tenant Group (slug)',
)
tenant_id = django_filters.ModelMultipleChoiceFilter(
queryset=Tenant.objects.all(),
label='Tenant (ID)',
)
tenant = django_filters.ModelMultipleChoiceFilter(
queryset=Tenant.objects.all(),
field_name='tenant__slug',
to_field_name='slug',
label='Tenant (slug)',
)
# #
# Contacts # Contacts
# #
@ -177,3 +98,96 @@ class ContactAssignmentFilterSet(ChangeLoggedModelFilterSet):
class Meta: class Meta:
model = ContactAssignment model = ContactAssignment
fields = ['id', 'content_type_id', 'object_id', 'priority'] fields = ['id', 'content_type_id', 'object_id', 'priority']
class ContactModelFilterSet(django_filters.FilterSet):
contact = django_filters.ModelMultipleChoiceFilter(
field_name='contacts__contact',
queryset=Contact.objects.all(),
label='Contact',
)
contact_role = django_filters.ModelMultipleChoiceFilter(
field_name='contacts__role',
queryset=ContactRole.objects.all(),
label='Contact Role'
)
#
# Tenancy
#
class TenantGroupFilterSet(OrganizationalModelFilterSet):
parent_id = django_filters.ModelMultipleChoiceFilter(
queryset=TenantGroup.objects.all(),
label='Tenant group (ID)',
)
parent = django_filters.ModelMultipleChoiceFilter(
field_name='parent__slug',
queryset=TenantGroup.objects.all(),
to_field_name='slug',
label='Tenant group (slug)',
)
class Meta:
model = TenantGroup
fields = ['id', 'name', 'slug', 'description']
class TenantFilterSet(NetBoxModelFilterSet, ContactModelFilterSet):
group_id = TreeNodeMultipleChoiceFilter(
queryset=TenantGroup.objects.all(),
field_name='group',
lookup_expr='in',
label='Tenant group (ID)',
)
group = TreeNodeMultipleChoiceFilter(
queryset=TenantGroup.objects.all(),
field_name='group',
lookup_expr='in',
to_field_name='slug',
label='Tenant group (slug)',
)
class Meta:
model = Tenant
fields = ['id', 'name', 'slug', 'description']
def search(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(
Q(name__icontains=value) |
Q(slug__icontains=value) |
Q(description__icontains=value) |
Q(comments__icontains=value)
)
class TenancyFilterSet(django_filters.FilterSet):
"""
An inheritable FilterSet for models which support Tenant assignment.
"""
tenant_group_id = TreeNodeMultipleChoiceFilter(
queryset=TenantGroup.objects.all(),
field_name='tenant__group',
lookup_expr='in',
label='Tenant Group (ID)',
)
tenant_group = TreeNodeMultipleChoiceFilter(
queryset=TenantGroup.objects.all(),
field_name='tenant__group',
to_field_name='slug',
lookup_expr='in',
label='Tenant Group (slug)',
)
tenant_id = django_filters.ModelMultipleChoiceFilter(
queryset=Tenant.objects.all(),
label='Tenant (ID)',
)
tenant = django_filters.ModelMultipleChoiceFilter(
queryset=Tenant.objects.all(),
field_name='tenant__slug',
to_field_name='slug',
label='Tenant (slug)',
)

View File

@ -2,6 +2,7 @@ from django.utils.translation import gettext as _
from netbox.forms import NetBoxModelFilterSetForm from netbox.forms import NetBoxModelFilterSetForm
from tenancy.models import * from tenancy.models import *
from tenancy.forms import ContactModelFilterForm
from utilities.forms import DynamicModelMultipleChoiceField, TagFilterField from utilities.forms import DynamicModelMultipleChoiceField, TagFilterField
__all__ = ( __all__ = (
@ -27,8 +28,12 @@ class TenantGroupFilterForm(NetBoxModelFilterSetForm):
tag = TagFilterField(model) tag = TagFilterField(model)
class TenantFilterForm(NetBoxModelFilterSetForm): class TenantFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
model = Tenant model = Tenant
fieldsets = (
(None, ('q', 'tag', 'group_id')),
('Contacts', ('contact', 'contact_role'))
)
group_id = DynamicModelMultipleChoiceField( group_id = DynamicModelMultipleChoiceField(
queryset=TenantGroup.objects.all(), queryset=TenantGroup.objects.all(),
required=False, required=False,

View File

@ -1,10 +1,11 @@
from django import forms from django import forms
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from tenancy.models import Tenant, TenantGroup from tenancy.models import *
from utilities.forms import DynamicModelChoiceField, DynamicModelMultipleChoiceField from utilities.forms import DynamicModelChoiceField, DynamicModelMultipleChoiceField
__all__ = ( __all__ = (
'ContactModelFilterForm',
'TenancyForm', 'TenancyForm',
'TenancyFilterForm', 'TenancyFilterForm',
) )
@ -44,3 +45,16 @@ class TenancyFilterForm(forms.Form):
}, },
label=_('Tenant') label=_('Tenant')
) )
class ContactModelFilterForm(forms.Form):
contact = DynamicModelMultipleChoiceField(
queryset=Contact.objects.all(),
required=False,
label=_('Contact')
)
contact_role = DynamicModelMultipleChoiceField(
queryset=ContactRole.objects.all(),
required=False,
label=_('Contact Role')
)

View File

@ -162,3 +162,6 @@ class ContactAssignment(WebhooksMixin, ChangeLoggedModel):
if self.priority: if self.priority:
return f"{self.contact} ({self.get_priority_display()})" return f"{self.contact} ({self.get_priority_display()})"
return str(self.contact) return str(self.contact)
def get_absolute_url(self):
return reverse('tenancy:contact', args=[self.contact.pk])

View File

@ -38,11 +38,17 @@ class TenantTable(NetBoxTable):
linkify=True linkify=True
) )
comments = columns.MarkdownColumn() comments = columns.MarkdownColumn()
contacts = tables.ManyToManyColumn(
linkify_item=True
)
tags = columns.TagColumn( tags = columns.TagColumn(
url_name='tenancy:tenant_list' url_name='tenancy:tenant_list'
) )
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = Tenant model = Tenant
fields = ('pk', 'id', 'name', 'slug', 'group', 'description', 'comments', 'tags', 'created', 'last_updated',) fields = (
'pk', 'id', 'name', 'slug', 'group', 'description', 'comments', 'contacts', 'tags', 'created',
'last_updated',
)
default_columns = ('pk', 'name', 'group', 'description') default_columns = ('pk', 'name', 'group', 'description')

View File

@ -5,7 +5,7 @@ from dcim.models import DeviceRole, Platform, Region, Site, SiteGroup
from extras.filtersets import LocalConfigContextFilterSet from extras.filtersets import LocalConfigContextFilterSet
from ipam.models import VRF from ipam.models import VRF
from netbox.filtersets import OrganizationalModelFilterSet, NetBoxModelFilterSet from netbox.filtersets import OrganizationalModelFilterSet, NetBoxModelFilterSet
from tenancy.filtersets import TenancyFilterSet from tenancy.filtersets import TenancyFilterSet, ContactModelFilterSet
from utilities.filters import MultiValueMACAddressFilter, TreeNodeMultipleChoiceFilter from utilities.filters import MultiValueMACAddressFilter, TreeNodeMultipleChoiceFilter
from .choices import * from .choices import *
from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
@ -26,14 +26,14 @@ class ClusterTypeFilterSet(OrganizationalModelFilterSet):
fields = ['id', 'name', 'slug', 'description'] fields = ['id', 'name', 'slug', 'description']
class ClusterGroupFilterSet(OrganizationalModelFilterSet): class ClusterGroupFilterSet(OrganizationalModelFilterSet, ContactModelFilterSet):
class Meta: class Meta:
model = ClusterGroup model = ClusterGroup
fields = ['id', 'name', 'slug', 'description'] fields = ['id', 'name', 'slug', 'description']
class ClusterFilterSet(NetBoxModelFilterSet, TenancyFilterSet): class ClusterFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
region_id = TreeNodeMultipleChoiceFilter( region_id = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(), queryset=Region.objects.all(),
field_name='site__region', field_name='site__region',
@ -104,7 +104,12 @@ class ClusterFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
) )
class VirtualMachineFilterSet(NetBoxModelFilterSet, TenancyFilterSet, LocalConfigContextFilterSet): class VirtualMachineFilterSet(
NetBoxModelFilterSet,
TenancyFilterSet,
ContactModelFilterSet,
LocalConfigContextFilterSet
):
status = django_filters.MultipleChoiceFilter( status = django_filters.MultipleChoiceFilter(
choices=VirtualMachineStatusChoices, choices=VirtualMachineStatusChoices,
null_value=None null_value=None

View File

@ -5,7 +5,7 @@ from dcim.models import DeviceRole, Platform, Region, Site, SiteGroup
from extras.forms import LocalConfigContextFilterForm from extras.forms import LocalConfigContextFilterForm
from ipam.models import VRF from ipam.models import VRF
from netbox.forms import NetBoxModelFilterSetForm from netbox.forms import NetBoxModelFilterSetForm
from tenancy.forms import TenancyFilterForm from tenancy.forms import ContactModelFilterForm, TenancyFilterForm
from utilities.forms import ( from utilities.forms import (
DynamicModelMultipleChoiceField, StaticSelect, StaticSelectMultiple, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES, DynamicModelMultipleChoiceField, StaticSelect, StaticSelectMultiple, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES,
) )
@ -26,18 +26,19 @@ class ClusterTypeFilterForm(NetBoxModelFilterSetForm):
tag = TagFilterField(model) tag = TagFilterField(model)
class ClusterGroupFilterForm(NetBoxModelFilterSetForm): class ClusterGroupFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
model = ClusterGroup model = ClusterGroup
tag = TagFilterField(model) tag = TagFilterField(model)
class ClusterFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): class ClusterFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm):
model = Cluster model = Cluster
fieldsets = ( fieldsets = (
(None, ('q', 'tag')), (None, ('q', 'tag')),
('Attributes', ('group_id', 'type_id')), ('Attributes', ('group_id', 'type_id')),
('Location', ('region_id', 'site_group_id', 'site_id')), ('Location', ('region_id', 'site_group_id', 'site_id')),
('Tenant', ('tenant_group_id', 'tenant_id')), ('Tenant', ('tenant_group_id', 'tenant_id')),
('Contacts', ('contact', 'contact_role')),
) )
type_id = DynamicModelMultipleChoiceField( type_id = DynamicModelMultipleChoiceField(
queryset=ClusterType.objects.all(), queryset=ClusterType.objects.all(),
@ -73,7 +74,12 @@ class ClusterFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
tag = TagFilterField(model) tag = TagFilterField(model)
class VirtualMachineFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, NetBoxModelFilterSetForm): class VirtualMachineFilterForm(
LocalConfigContextFilterForm,
TenancyFilterForm,
ContactModelFilterForm,
NetBoxModelFilterSetForm
):
model = VirtualMachine model = VirtualMachine
fieldsets = ( fieldsets = (
(None, ('q', 'tag')), (None, ('q', 'tag')),
@ -81,6 +87,7 @@ class VirtualMachineFilterForm(LocalConfigContextFilterForm, TenancyFilterForm,
('Location', ('region_id', 'site_group_id', 'site_id')), ('Location', ('region_id', 'site_group_id', 'site_id')),
('Attriubtes', ('status', 'role_id', 'platform_id', 'mac_address', 'has_primary_ip', 'local_context_data')), ('Attriubtes', ('status', 'role_id', 'platform_id', 'mac_address', 'has_primary_ip', 'local_context_data')),
('Tenant', ('tenant_group_id', 'tenant_id')), ('Tenant', ('tenant_group_id', 'tenant_id')),
('Contacts', ('contact', 'contact_role')),
) )
cluster_group_id = DynamicModelMultipleChoiceField( cluster_group_id = DynamicModelMultipleChoiceField(
queryset=ClusterGroup.objects.all(), queryset=ClusterGroup.objects.all(),

View File

@ -36,6 +36,9 @@ class ClusterGroupTable(NetBoxTable):
cluster_count = tables.Column( cluster_count = tables.Column(
verbose_name='Clusters' verbose_name='Clusters'
) )
contacts = tables.ManyToManyColumn(
linkify_item=True
)
tags = columns.TagColumn( tags = columns.TagColumn(
url_name='virtualization:clustergroup_list' url_name='virtualization:clustergroup_list'
) )
@ -43,7 +46,8 @@ class ClusterGroupTable(NetBoxTable):
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = ClusterGroup model = ClusterGroup
fields = ( fields = (
'pk', 'id', 'name', 'slug', 'cluster_count', 'description', 'tags', 'created', 'last_updated', 'actions', 'pk', 'id', 'name', 'slug', 'cluster_count', 'description', 'contacts', 'tags', 'created', 'last_updated',
'actions',
) )
default_columns = ('pk', 'name', 'cluster_count', 'description') default_columns = ('pk', 'name', 'cluster_count', 'description')
@ -75,6 +79,9 @@ class ClusterTable(NetBoxTable):
verbose_name='VMs' verbose_name='VMs'
) )
comments = columns.MarkdownColumn() comments = columns.MarkdownColumn()
contacts = tables.ManyToManyColumn(
linkify_item=True
)
tags = columns.TagColumn( tags = columns.TagColumn(
url_name='virtualization:cluster_list' url_name='virtualization:cluster_list'
) )
@ -82,7 +89,7 @@ class ClusterTable(NetBoxTable):
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = Cluster model = Cluster
fields = ( fields = (
'pk', 'id', 'name', 'type', 'group', 'tenant', 'site', 'comments', 'device_count', 'vm_count', 'tags', 'pk', 'id', 'name', 'type', 'group', 'tenant', 'site', 'comments', 'device_count', 'vm_count', 'contacts',
'created', 'last_updated', 'tags', 'created', 'last_updated',
) )
default_columns = ('pk', 'name', 'type', 'group', 'tenant', 'site', 'device_count', 'vm_count') default_columns = ('pk', 'name', 'type', 'group', 'tenant', 'site', 'device_count', 'vm_count')

View File

@ -78,6 +78,9 @@ class VMInterfaceTable(BaseInterfaceTable):
vrf = tables.Column( vrf = tables.Column(
linkify=True linkify=True
) )
contacts = tables.ManyToManyColumn(
linkify_item=True
)
tags = columns.TagColumn( tags = columns.TagColumn(
url_name='virtualization:vminterface_list' url_name='virtualization:vminterface_list'
) )
@ -86,7 +89,8 @@ class VMInterfaceTable(BaseInterfaceTable):
model = VMInterface model = VMInterface
fields = ( fields = (
'pk', 'id', 'name', 'virtual_machine', 'enabled', 'mac_address', 'mtu', 'mode', 'description', 'tags', 'pk', 'id', 'name', 'virtual_machine', 'enabled', 'mac_address', 'mtu', 'mode', 'description', 'tags',
'vrf', 'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'created', 'last_updated', 'vrf', 'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'contacts', 'created',
'last_updated',
) )
default_columns = ('pk', 'name', 'virtual_machine', 'enabled', 'description') default_columns = ('pk', 'name', 'virtual_machine', 'enabled', 'description')