mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-14 01:41:22 -06:00
Merge branch 'develop' into feature
This commit is contained in:
commit
8d53b46e82
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)',
|
||||
|
@ -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(),
|
||||
|
@ -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',
|
||||
|
@ -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')
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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',
|
||||
|
@ -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(),
|
||||
|
@ -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 = (
|
||||
|
@ -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',
|
||||
|
@ -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',
|
||||
|
@ -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')
|
||||
|
||||
|
||||
|
@ -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',
|
||||
|
@ -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')
|
||||
|
@ -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,
|
||||
}
|
||||
|
||||
|
||||
|
@ -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(),
|
||||
|
@ -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',
|
||||
}
|
||||
|
||||
|
@ -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,8 +27,9 @@ from virtualization.models import Cluster, VirtualMachine
|
||||
from virtualization.tables import ClusterTable, VirtualMachineTable
|
||||
|
||||
SEARCH_MAX_RESULTS = 15
|
||||
SEARCH_TYPES = OrderedDict((
|
||||
# Circuits
|
||||
|
||||
CIRCUIT_TYPES = OrderedDict(
|
||||
(
|
||||
('provider', {
|
||||
'queryset': Provider.objects.annotate(
|
||||
count_circuits=count_related(Circuit, 'provider')
|
||||
@ -50,7 +52,12 @@ SEARCH_TYPES = OrderedDict((
|
||||
'table': ProviderNetworkTable,
|
||||
'url': 'circuits:providernetwork_list',
|
||||
}),
|
||||
# DCIM
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
DCIM_TYPES = OrderedDict(
|
||||
(
|
||||
('site', {
|
||||
'queryset': Site.objects.prefetch_related('region', 'tenant'),
|
||||
'filterset': SiteFilterSet,
|
||||
@ -123,25 +130,11 @@ SEARCH_TYPES = OrderedDict((
|
||||
'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
|
||||
)
|
||||
)
|
||||
|
||||
IPAM_TYPES = OrderedDict(
|
||||
(
|
||||
('vrf', {
|
||||
'queryset': VRF.objects.prefetch_related('tenant'),
|
||||
'filterset': VRFFilterSet,
|
||||
@ -178,7 +171,11 @@ SEARCH_TYPES = OrderedDict((
|
||||
'table': ASNTable,
|
||||
'url': 'ipam:asn_list',
|
||||
}),
|
||||
# Tenancy
|
||||
)
|
||||
)
|
||||
|
||||
TENANCY_TYPES = OrderedDict(
|
||||
(
|
||||
('tenant', {
|
||||
'queryset': Tenant.objects.prefetch_related('group'),
|
||||
'filterset': TenantFilterSet,
|
||||
@ -186,9 +183,56 @@ SEARCH_TYPES = OrderedDict((
|
||||
'url': 'tenancy:tenant_list',
|
||||
}),
|
||||
('contact', {
|
||||
'queryset': Contact.objects.prefetch_related('group', 'assignments').annotate(assignment_count=count_related(ContactAssignment, '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()
|
||||
|
@ -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():
|
||||
|
BIN
netbox/project-static/dist/netbox-dark.css
vendored
BIN
netbox/project-static/dist/netbox-dark.css
vendored
Binary file not shown.
@ -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;
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
62
netbox/templates/dcim/inc/nonracked_devices.html
Normal file
62
netbox/templates/dcim/inc/nonracked_devices.html
Normal 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">—</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>
|
@ -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>
|
||||
|
@ -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">—</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>
|
||||
|
@ -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>
|
||||
|
@ -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)',
|
||||
)
|
||||
|
@ -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,
|
||||
|
@ -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')
|
||||
)
|
||||
|
@ -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])
|
||||
|
@ -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')
|
||||
|
@ -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
|
||||
|
@ -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(),
|
||||
|
@ -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')
|
||||
|
@ -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')
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user