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: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install pycodestyle coverage
pip install pycodestyle coverage tblib
- name: Build documentation
run: mkdocs build

View File

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

View File

@ -2,6 +2,18 @@
## 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)

View File

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

View File

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

View File

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

View File

@ -19,6 +19,9 @@ class ProviderTable(NetBoxTable):
verbose_name='Circuits'
)
comments = columns.MarkdownColumn()
contacts = tables.ManyToManyColumn(
linkify_item=True
)
tags = columns.TagColumn(
url_name='circuits:provider_list'
)
@ -27,7 +30,7 @@ class ProviderTable(NetBoxTable):
model = Provider
fields = (
'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')

View File

@ -576,9 +576,9 @@ class DeviceWithConfigContextSerializer(DeviceSerializer):
class Meta(DeviceSerializer.Meta):
fields = [
'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',
'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'local_context_data',
'tags', 'custom_fields', 'config_context', 'created', 'last_updated',
'site', 'location', 'rack', 'position', 'face', 'parent_device', 'status', 'airflow', 'primary_ip',
'primary_ip4', 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments',
'local_context_data', 'tags', 'custom_fields', 'config_context', 'created', 'last_updated',
]
@swagger_serializer_method(serializer_or_field=serializers.DictField)

View File

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

View File

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

View File

@ -22,6 +22,12 @@ class CableTable(NetBoxTable):
orderable=False,
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(
accessor=Accessor('termination_a'),
orderable=False,
@ -34,6 +40,12 @@ class CableTable(NetBoxTable):
orderable=False,
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(
accessor=Accessor('termination_b'),
orderable=False,
@ -54,7 +66,7 @@ class CableTable(NetBoxTable):
class Meta(NetBoxTable.Meta):
model = Cable
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',
)
default_columns = (

View File

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

View File

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

View File

@ -69,6 +69,9 @@ class RackTable(NetBoxTable):
orderable=False,
verbose_name='Power'
)
contacts = tables.ManyToManyColumn(
linkify_item=True
)
tags = columns.TagColumn(
url_name='dcim:rack_list'
)
@ -86,7 +89,7 @@ class RackTable(NetBoxTable):
fields = (
'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',
'get_power_utilization', 'tags', 'created', 'last_updated',
'get_power_utilization', 'contacts', 'tags', 'created', 'last_updated',
)
default_columns = (
'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'},
verbose_name='Sites'
)
contacts = tables.ManyToManyColumn(
linkify_item=True
)
tags = columns.TagColumn(
url_name='dcim:region_list'
)
@ -33,7 +36,8 @@ class RegionTable(NetBoxTable):
class Meta(NetBoxTable.Meta):
model = Region
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')
@ -51,6 +55,9 @@ class SiteGroupTable(NetBoxTable):
url_params={'group_id': 'pk'},
verbose_name='Sites'
)
contacts = tables.ManyToManyColumn(
linkify_item=True
)
tags = columns.TagColumn(
url_name='dcim:sitegroup_list'
)
@ -58,7 +65,8 @@ class SiteGroupTable(NetBoxTable):
class Meta(NetBoxTable.Meta):
model = SiteGroup
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')
@ -90,6 +98,9 @@ class SiteTable(NetBoxTable):
)
tenant = TenantColumn()
comments = columns.MarkdownColumn()
contacts = tables.ManyToManyColumn(
linkify_item=True
)
tags = columns.TagColumn(
url_name='dcim:site_list'
)
@ -99,7 +110,7 @@ class SiteTable(NetBoxTable):
fields = (
'pk', 'id', 'name', 'slug', 'status', 'facility', 'region', 'group', 'tenant', 'asns', 'asn_count',
'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')
@ -126,6 +137,9 @@ class LocationTable(NetBoxTable):
url_params={'location_id': 'pk'},
verbose_name='Devices'
)
contacts = tables.ManyToManyColumn(
linkify_item=True
)
tags = columns.TagColumn(
url_name='dcim:location_list'
)
@ -136,7 +150,7 @@ class LocationTable(NetBoxTable):
class Meta(NetBoxTable.Meta):
model = Location
fields = (
'pk', 'id', 'name', 'site', 'tenant', 'rack_count', 'device_count', 'description', 'slug', 'tags',
'actions', 'created', 'last_updated',
'pk', 'id', 'name', 'site', 'tenant', 'rack_count', 'device_count', 'description', 'slug', 'contacts',
'tags', 'actions', 'created', 'last_updated',
)
default_columns = ('pk', 'name', 'site', 'tenant', 'rack_count', 'device_count', 'description')

View File

@ -338,6 +338,11 @@ class SiteView(generic.ObjectView):
'device_count',
cumulative=True
).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)
asn_count = asns.count()
@ -348,6 +353,7 @@ class SiteView(generic.ObjectView):
'stats': stats,
'locations': locations,
'asns': asns,
'nonracked_devices': nonracked_devices,
}
@ -425,11 +431,17 @@ class LocationView(generic.ObjectView):
).filter(pk__in=location_ids).exclude(pk=instance.pk)
child_locations_table = tables.LocationTable(child_locations)
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 {
'rack_count': rack_count,
'device_count': device_count,
'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(
field_name='vlan__vid',
label='VLAN number (1-4095)',
label='VLAN number (1-4094)',
)
role_id = django_filters.ModelMultipleChoiceFilter(
queryset=Role.objects.all(),

View File

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

View File

@ -1,4 +1,5 @@
from collections import OrderedDict
from typing import Dict
from circuits.filtersets import CircuitFilterSet, ProviderFilterSet, ProviderNetworkFilterSet
from circuits.models import Circuit, ProviderNetwork, Provider
@ -26,169 +27,212 @@ from virtualization.models import Cluster, VirtualMachine
from virtualization.tables import ClusterTable, VirtualMachineTable
SEARCH_MAX_RESULTS = 15
SEARCH_TYPES = OrderedDict((
# Circuits
('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
CIRCUIT_TYPES = OrderedDict(
(
('provider', {
'queryset': Provider.objects.annotate(
count_circuits=count_related(Circuit, 'provider')
),
Rack,
'location',
'rack_count',
cumulative=True
).prefetch_related('site'),
'filterset': LocationFilterSet,
'table': LocationTable,
'url': 'dcim:location_list',
}),
('devicetype', {
'queryset': DeviceType.objects.prefetch_related('manufacturer').annotate(
instance_count=count_related(Device, 'device_type')
),
'filterset': DeviceTypeFilterSet,
'table': DeviceTypeTable,
'url': 'dcim:devicetype_list',
}),
('device', {
'queryset': Device.objects.prefetch_related(
'device_type__manufacturer', 'device_role', 'tenant', 'site', 'rack', 'primary_ip4', 'primary_ip6',
),
'filterset': DeviceFilterSet,
'table': DeviceTable,
'url': 'dcim:device_list',
}),
('virtualchassis', {
'queryset': VirtualChassis.objects.prefetch_related('master').annotate(
member_count=count_related(Device, 'virtual_chassis')
),
'filterset': VirtualChassisFilterSet,
'table': VirtualChassisTable,
'url': 'dcim:virtualchassis_list',
}),
('cable', {
'queryset': Cable.objects.all(),
'filterset': CableFilterSet,
'table': CableTable,
'url': 'dcim:cable_list',
}),
('powerfeed', {
'queryset': PowerFeed.objects.all(),
'filterset': PowerFeedFilterSet,
'table': PowerFeedTable,
'url': 'dcim:powerfeed_list',
}),
# Virtualization
('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',
}),
# IPAM
('vrf', {
'queryset': VRF.objects.prefetch_related('tenant'),
'filterset': VRFFilterSet,
'table': VRFTable,
'url': 'ipam:vrf_list',
}),
('aggregate', {
'queryset': Aggregate.objects.prefetch_related('rir'),
'filterset': AggregateFilterSet,
'table': AggregateTable,
'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
('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',
}),
))
'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_TYPES = OrderedDict(
(
('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,
'location',
'rack_count',
cumulative=True
).prefetch_related('site'),
'filterset': LocationFilterSet,
'table': LocationTable,
'url': 'dcim:location_list',
}),
('devicetype', {
'queryset': DeviceType.objects.prefetch_related('manufacturer').annotate(
instance_count=count_related(Device, 'device_type')
),
'filterset': DeviceTypeFilterSet,
'table': DeviceTypeTable,
'url': 'dcim:devicetype_list',
}),
('device', {
'queryset': Device.objects.prefetch_related(
'device_type__manufacturer', 'device_role', 'tenant', 'site', 'rack', 'primary_ip4', 'primary_ip6',
),
'filterset': DeviceFilterSet,
'table': DeviceTable,
'url': 'dcim:device_list',
}),
('virtualchassis', {
'queryset': VirtualChassis.objects.prefetch_related('master').annotate(
member_count=count_related(Device, 'virtual_chassis')
),
'filterset': VirtualChassisFilterSet,
'table': VirtualChassisTable,
'url': 'dcim:virtualchassis_list',
}),
('cable', {
'queryset': Cable.objects.all(),
'filterset': CableFilterSet,
'table': CableTable,
'url': 'dcim:cable_list',
}),
('powerfeed', {
'queryset': PowerFeed.objects.all(),
'filterset': PowerFeedFilterSet,
'table': PowerFeedTable,
'url': 'dcim:powerfeed_list',
}),
)
)
IPAM_TYPES = OrderedDict(
(
('vrf', {
'queryset': VRF.objects.prefetch_related('tenant'),
'filterset': VRFFilterSet,
'table': VRFTable,
'url': 'ipam:vrf_list',
}),
('aggregate', {
'queryset': Aggregate.objects.prefetch_related('rir'),
'filterset': AggregateFilterSet,
'table': AggregateTable,
'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 netbox.constants import SEARCH_TYPE_HIERARCHY
from utilities.forms import BootstrapMixin
from .base import *
OBJ_TYPE_CHOICES = (
('', 'All Objects'),
('Circuits', (
('provider', 'Providers'),
('circuit', 'Circuits'),
)),
('DCIM', (
('site', 'Sites'),
('rack', 'Racks'),
('rackreservation', 'Rack reservations'),
('location', 'Locations'),
('devicetype', 'Device Types'),
('device', 'Devices'),
('virtualchassis', 'Virtual chassis'),
('cable', 'Cables'),
('powerfeed', 'Power feeds'),
)),
('IPAM', (
('vrf', 'VRFs'),
('aggregate', 'Aggregates'),
('prefix', 'Prefixes'),
('ipaddress', 'IP Addresses'),
('vlan', 'VLANs'),
)),
('Tenancy', (
('tenant', 'Tenants'),
)),
('Virtualization', (
('cluster', 'Clusters'),
('virtualmachine', 'Virtual Machines'),
)),
)
def build_search_choices():
result = list()
result.append(('', 'All Objects'))
for category, items in SEARCH_TYPE_HIERARCHY.items():
subcategories = list()
for slug, obj in items.items():
name = obj['queryset'].model._meta.verbose_name_plural
name = name[0].upper() + name[1:]
subcategories.append((slug, name))
result.append((category, tuple(subcategories)))
return tuple(result)
OBJ_TYPE_CHOICES = build_search_choices()
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-bg: $component-active-bg;
$navbar-light-color: $navbar-dark-color;
$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-color: $darker;
$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
$dropdown-color: $body-color;

View File

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

View File

@ -8,6 +8,22 @@
<a href="{{ termination.device.get_absolute_url }}">{{ termination.device }}</a>
</td>
</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>
<td>Type</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">
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/contacts.html' %}
{% include 'dcim/inc/nonracked_devices.html' %}
{% include 'inc/panels/image_attachments.html' %}
{% plugin_right_page object %}
</div>

View File

@ -286,50 +286,7 @@
</div>
</div>
</div>
<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 %}
<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 'dcim/inc/nonracked_devices.html' %}
{% include 'inc/panels/contacts.html' %}
{% plugin_right_page object %}
</div>

View File

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

View File

@ -10,6 +10,7 @@ __all__ = (
'ContactAssignmentFilterSet',
'ContactFilterSet',
'ContactGroupFilterSet',
'ContactModelFilterSet',
'ContactRoleFilterSet',
'TenancyFilterSet',
'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
#
@ -177,3 +98,96 @@ class ContactAssignmentFilterSet(ChangeLoggedModelFilterSet):
class Meta:
model = ContactAssignment
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 tenancy.models import *
from tenancy.forms import ContactModelFilterForm
from utilities.forms import DynamicModelMultipleChoiceField, TagFilterField
__all__ = (
@ -27,8 +28,12 @@ class TenantGroupFilterForm(NetBoxModelFilterSetForm):
tag = TagFilterField(model)
class TenantFilterForm(NetBoxModelFilterSetForm):
class TenantFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
model = Tenant
fieldsets = (
(None, ('q', 'tag', 'group_id')),
('Contacts', ('contact', 'contact_role'))
)
group_id = DynamicModelMultipleChoiceField(
queryset=TenantGroup.objects.all(),
required=False,

View File

@ -1,10 +1,11 @@
from django import forms
from django.utils.translation import gettext as _
from tenancy.models import Tenant, TenantGroup
from tenancy.models import *
from utilities.forms import DynamicModelChoiceField, DynamicModelMultipleChoiceField
__all__ = (
'ContactModelFilterForm',
'TenancyForm',
'TenancyFilterForm',
)
@ -44,3 +45,16 @@ class TenancyFilterForm(forms.Form):
},
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:
return f"{self.contact} ({self.get_priority_display()})"
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
)
comments = columns.MarkdownColumn()
contacts = tables.ManyToManyColumn(
linkify_item=True
)
tags = columns.TagColumn(
url_name='tenancy:tenant_list'
)
class Meta(NetBoxTable.Meta):
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')

View File

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

View File

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

View File

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

View File

@ -78,6 +78,9 @@ class VMInterfaceTable(BaseInterfaceTable):
vrf = tables.Column(
linkify=True
)
contacts = tables.ManyToManyColumn(
linkify_item=True
)
tags = columns.TagColumn(
url_name='virtualization:vminterface_list'
)
@ -86,7 +89,8 @@ class VMInterfaceTable(BaseInterfaceTable):
model = VMInterface
fields = (
'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')