mirror of
https://github.com/netbox-community/netbox.git
synced 2025-08-26 09:16:10 -06:00
Merge branch 'feature' into 9608-drf-spectacular2
This commit is contained in:
commit
b097ee25fc
12
README.md
12
README.md
@ -67,15 +67,17 @@ complete list of requirements, see `requirements.txt`. The code is available
|
||||
<div align="center">
|
||||
<h3>Thank you to our sponsors!</h3>
|
||||
|
||||
[](https://netboxlabs.com)
|
||||
|
||||
[](https://try.digitalocean.com/developer-cloud)
|
||||
|
||||
[](https://metal.equinix.com/)
|
||||
|
||||
[](https://ns1.com/)
|
||||
[](https://ns1.com)
|
||||
<br />
|
||||
[](https://sentry.io/)
|
||||
[](https://sentry.io)
|
||||
|
||||
[](https://stellar.tech/)
|
||||
[](https://metal.equinix.com)
|
||||
|
||||
[](https://stellar.tech)
|
||||
|
||||
</div>
|
||||
|
||||
|
@ -24,7 +24,7 @@ If you believe you've uncovered a security vulnerability and wish to report it c
|
||||
|
||||
Please note that we **DO NOT** accept reports generated by automated tooling which merely suggest that a file or file(s) _may_ be vulnerable under certain conditions, as these are most often innocuous.
|
||||
|
||||
If you believe that you've found a vulnerability which meets all of these conditions, please email a brief description of the suspected bug and instructions for reproduction to **security@netbox.dev**. For any security concerns regarding NetBox deployed via Docker, please see the [netbox-docker](https://github.com/netbox-community/netbox-docker) project.
|
||||
If you believe that you've found a vulnerability which meets all of these conditions, please [submit a draft security advisory](https://github.com/netbox-community/netbox/security/advisories/new) on GitHub, or email a brief description of the suspected bug and instructions for reproduction to **security@netbox.dev**. For any security concerns regarding NetBox deployed via Docker, please see the [netbox-docker](https://github.com/netbox-community/netbox-docker) project.
|
||||
|
||||
### Bug Bounties
|
||||
|
||||
|
@ -272,7 +272,10 @@ See the [housekeeping documentation](../administration/housekeeping.md) for furt
|
||||
|
||||
## Test the Application
|
||||
|
||||
At this point, we should be able to run NetBox's development server for testing. We can check by starting a development instance:
|
||||
At this point, we should be able to run NetBox's development server for testing. We can check by starting a development instance locally.
|
||||
|
||||
!!! tip
|
||||
Check that the Python virtual environment is still active before attempting to run the server.
|
||||
|
||||
```no-highlight
|
||||
python3 manage.py runserver 0.0.0.0:8000 --insecure
|
||||
|
@ -14,7 +14,10 @@ While the provided configuration should suffice for most initial installations,
|
||||
|
||||
## systemd Setup
|
||||
|
||||
We'll use systemd to control both gunicorn and NetBox's background worker process. First, copy `contrib/netbox.service` and `contrib/netbox-rq.service` to the `/etc/systemd/system/` directory and reload the systemd daemon:
|
||||
We'll use systemd to control both gunicorn and NetBox's background worker process. First, copy `contrib/netbox.service` and `contrib/netbox-rq.service` to the `/etc/systemd/system/` directory and reload the systemd daemon.
|
||||
|
||||
!!! warning "Check user & group assignment"
|
||||
The stock service configuration files packaged with NetBox assume that the service will run with the `netbox` user and group names. If these differ on your installation, be sure to update the service files accordingly.
|
||||
|
||||
```no-highlight
|
||||
sudo cp -v /opt/netbox/contrib/*.service /etc/systemd/system/
|
||||
|
@ -2,6 +2,19 @@
|
||||
|
||||
## v3.4.4 (FUTURE)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#10762](https://github.com/netbox-community/netbox/issues/10762) - Permit selection custom fields to have only one choice
|
||||
* [#11585](https://github.com/netbox-community/netbox/issues/11585) - Add IP address filters for services
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#11487](https://github.com/netbox-community/netbox/issues/11487) - Remove "set null" option from non-writable custom fields during bulk edit
|
||||
* [#11491](https://github.com/netbox-community/netbox/issues/11491) - Show edit/delete buttons in user tokens table
|
||||
* [#11528](https://github.com/netbox-community/netbox/issues/11528) - Permit import of devices using uploaded file
|
||||
* [#11555](https://github.com/netbox-community/netbox/issues/11555) - Avoid inadvertent interpretation of search query as regular expression under global search (previously [#11516](https://github.com/netbox-community/netbox/issues/11516))
|
||||
* [#11562](https://github.com/netbox-community/netbox/issues/11562) - Correct ordering of virtual chassis interfaces with duplicate names
|
||||
|
||||
---
|
||||
|
||||
## v3.4.3 (2023-01-20)
|
||||
@ -34,8 +47,9 @@
|
||||
* [#11483](https://github.com/netbox-community/netbox/issues/11483) - Apply configured formatting to custom date fields
|
||||
* [#11488](https://github.com/netbox-community/netbox/issues/11488) - Add missing `description` fields to several REST API serializers
|
||||
* [#11497](https://github.com/netbox-community/netbox/issues/11497) - Enforce `run_script` permission when executing scripts via REST API
|
||||
* [#11516](https://github.com/netbox-community/netbox/issues/11516) - Prevent text highlight utility from interpreting match as regex
|
||||
* ~[#11516](https://github.com/netbox-community/netbox/issues/11516) - Prevent text highlight utility from interpreting match as regex~
|
||||
* [#11522](https://github.com/netbox-community/netbox/issues/11522) - Correct tag links under contact & tenant list views
|
||||
* [#11537](https://github.com/netbox-community/netbox/issues/11537) - Remove obsolete "Connection" column from power feeds table
|
||||
* [#11544](https://github.com/netbox-community/netbox/issues/11544) - Catch ValidationError exception when filtering by invalid MAC address
|
||||
|
||||
---
|
||||
|
@ -4,6 +4,8 @@
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#11517](https://github.com/netbox-community/netbox/issues/11517) - Standardize the inclusion of related objects across the entire UI
|
||||
* [#11584](https://github.com/netbox-community/netbox/issues/11584) - Add a list view for contact assignments
|
||||
* [#11254](https://github.com/netbox-community/netbox/issues/11254) - Introduce the `X-Request-ID` HTTP header to annotate the unique ID of each request for change logging
|
||||
* [#11440](https://github.com/netbox-community/netbox/issues/11440) - Add an `enabled` field for device type interfaces
|
||||
|
||||
@ -11,3 +13,4 @@
|
||||
|
||||
* [#10604](https://github.com/netbox-community/netbox/issues/10604) - Remove unused `extra_tabs` block from `object.html` generic template
|
||||
* [#10923](https://github.com/netbox-community/netbox/issues/10923) - Remove unused `NetBoxModelCSVForm` class (replaced by `NetBoxModelImportForm`)
|
||||
* [#11611](https://github.com/netbox-community/netbox/issues/11611) - Refactor API viewset classes and introduce NetBoxReadOnlyModelViewSet
|
||||
|
@ -28,7 +28,9 @@ class CircuitTypeTable(NetBoxTable):
|
||||
tags = columns.TagColumn(
|
||||
url_name='circuits:circuittype_list'
|
||||
)
|
||||
circuit_count = tables.Column(
|
||||
circuit_count = columns.LinkedCountColumn(
|
||||
viewname='circuits:circuit_list',
|
||||
url_params={'type_id': 'pk'},
|
||||
verbose_name='Circuits'
|
||||
)
|
||||
|
||||
|
@ -29,6 +29,15 @@ class ProviderListView(generic.ObjectListView):
|
||||
class ProviderView(generic.ObjectView):
|
||||
queryset = Provider.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
related_models = (
|
||||
(Circuit.objects.restrict(request.user, 'view').filter(provider=instance), 'provider_id'),
|
||||
)
|
||||
|
||||
return {
|
||||
'related_models': related_models,
|
||||
}
|
||||
|
||||
|
||||
@register_model_view(Provider, 'edit')
|
||||
class ProviderEditView(generic.ObjectEditView):
|
||||
@ -79,6 +88,18 @@ class ProviderNetworkListView(generic.ObjectListView):
|
||||
class ProviderNetworkView(generic.ObjectView):
|
||||
queryset = ProviderNetwork.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
related_models = (
|
||||
(
|
||||
Circuit.objects.restrict(request.user, 'view').filter(terminations__provider_network=instance),
|
||||
'providernetwork_id',
|
||||
),
|
||||
)
|
||||
|
||||
return {
|
||||
'related_models': related_models,
|
||||
}
|
||||
|
||||
|
||||
@register_model_view(ProviderNetwork, 'edit')
|
||||
class ProviderNetworkEditView(generic.ObjectEditView):
|
||||
@ -127,6 +148,15 @@ class CircuitTypeListView(generic.ObjectListView):
|
||||
class CircuitTypeView(generic.ObjectView):
|
||||
queryset = CircuitType.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
related_models = (
|
||||
(Circuit.objects.restrict(request.user, 'view').filter(type=instance), 'type_id'),
|
||||
)
|
||||
|
||||
return {
|
||||
'related_models': related_models,
|
||||
}
|
||||
|
||||
|
||||
@register_model_view(CircuitType, 'edit')
|
||||
class CircuitTypeEditView(generic.ObjectEditView):
|
||||
|
@ -18,7 +18,6 @@ from .common import ModuleCommonForm
|
||||
|
||||
__all__ = (
|
||||
'CableImportForm',
|
||||
'ChildDeviceImportForm',
|
||||
'ConsolePortImportForm',
|
||||
'ConsoleServerPortImportForm',
|
||||
'DeviceBayImportForm',
|
||||
@ -413,6 +412,18 @@ class DeviceImportForm(BaseDeviceImportForm):
|
||||
required=False,
|
||||
help_text=_('Mounted rack face')
|
||||
)
|
||||
parent = CSVModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
to_field_name='name',
|
||||
required=False,
|
||||
help_text=_('Parent device (for child devices)')
|
||||
)
|
||||
device_bay = CSVModelChoiceField(
|
||||
queryset=DeviceBay.objects.all(),
|
||||
to_field_name='name',
|
||||
required=False,
|
||||
help_text=_('Device bay in which this device is installed (for child devices)')
|
||||
)
|
||||
airflow = CSVChoiceField(
|
||||
choices=DeviceAirflowChoices,
|
||||
required=False,
|
||||
@ -422,8 +433,8 @@ class DeviceImportForm(BaseDeviceImportForm):
|
||||
class Meta(BaseDeviceImportForm.Meta):
|
||||
fields = [
|
||||
'name', 'device_role', 'tenant', 'manufacturer', 'device_type', 'platform', 'serial', 'asset_tag', 'status',
|
||||
'site', 'location', 'rack', 'position', 'face', 'airflow', 'virtual_chassis', 'vc_position', 'vc_priority',
|
||||
'cluster', 'description', 'comments', 'tags',
|
||||
'site', 'location', 'rack', 'position', 'face', 'parent', 'device_bay', 'airflow', 'virtual_chassis',
|
||||
'vc_position', 'vc_priority', 'cluster', 'description', 'comments', 'tags',
|
||||
]
|
||||
|
||||
def __init__(self, data=None, *args, **kwargs):
|
||||
@ -434,6 +445,7 @@ class DeviceImportForm(BaseDeviceImportForm):
|
||||
# Limit location queryset by assigned site
|
||||
params = {f"site__{self.fields['site'].to_field_name}": data.get('site')}
|
||||
self.fields['location'].queryset = self.fields['location'].queryset.filter(**params)
|
||||
self.fields['parent'].queryset = self.fields['parent'].queryset.filter(**params)
|
||||
|
||||
# Limit rack queryset by assigned site and group
|
||||
params = {
|
||||
@ -442,6 +454,23 @@ class DeviceImportForm(BaseDeviceImportForm):
|
||||
}
|
||||
self.fields['rack'].queryset = self.fields['rack'].queryset.filter(**params)
|
||||
|
||||
# Limit device bay queryset by parent device
|
||||
if parent := data.get('parent'):
|
||||
params = {f"device__{self.fields['parent'].to_field_name}": parent}
|
||||
self.fields['device_bay'].queryset = self.fields['device_bay'].queryset.filter(**params)
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Inherit site and rack from parent device
|
||||
if parent := self.cleaned_data.get('parent'):
|
||||
self.instance.site = parent.site
|
||||
self.instance.rack = parent.rack
|
||||
|
||||
# Set parent_bay reverse relationship
|
||||
if device_bay := self.cleaned_data.get('device_bay'):
|
||||
self.instance.parent_bay = device_bay
|
||||
|
||||
|
||||
class ModuleImportForm(ModuleCommonForm, NetBoxModelImportForm):
|
||||
device = CSVModelChoiceField(
|
||||
@ -495,48 +524,6 @@ class ModuleImportForm(ModuleCommonForm, NetBoxModelImportForm):
|
||||
return self.cleaned_data['replicate_components']
|
||||
|
||||
|
||||
class ChildDeviceImportForm(BaseDeviceImportForm):
|
||||
parent = CSVModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
to_field_name='name',
|
||||
help_text=_('Parent device')
|
||||
)
|
||||
device_bay = CSVModelChoiceField(
|
||||
queryset=DeviceBay.objects.all(),
|
||||
to_field_name='name',
|
||||
help_text=_('Device bay in which this device is installed')
|
||||
)
|
||||
|
||||
class Meta(BaseDeviceImportForm.Meta):
|
||||
fields = [
|
||||
'name', 'device_role', 'tenant', 'manufacturer', 'device_type', 'platform', 'serial', 'asset_tag', 'status',
|
||||
'parent', 'device_bay', 'virtual_chassis', 'vc_position', 'vc_priority', 'cluster', 'comments', 'tags'
|
||||
]
|
||||
|
||||
def __init__(self, data=None, *args, **kwargs):
|
||||
super().__init__(data, *args, **kwargs)
|
||||
|
||||
if data:
|
||||
|
||||
# Limit device bay queryset by parent device
|
||||
params = {f"device__{self.fields['parent'].to_field_name}": data.get('parent')}
|
||||
self.fields['device_bay'].queryset = self.fields['device_bay'].queryset.filter(**params)
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Set parent_bay reverse relationship
|
||||
device_bay = self.cleaned_data.get('device_bay')
|
||||
if device_bay:
|
||||
self.instance.parent_bay = device_bay
|
||||
|
||||
# Inherit site and rack from parent device
|
||||
parent = self.cleaned_data.get('parent')
|
||||
if parent:
|
||||
self.instance.site = parent.site
|
||||
self.instance.rack = parent.rack
|
||||
|
||||
|
||||
#
|
||||
# Device components
|
||||
#
|
||||
|
@ -107,6 +107,9 @@ class PlatformTable(NetBoxTable):
|
||||
name = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
manufacturer = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
device_count = columns.LinkedCountColumn(
|
||||
viewname='dcim:device_list',
|
||||
url_params={'platform_id': 'pk'},
|
||||
@ -580,7 +583,6 @@ class DeviceInterfaceTable(InterfaceTable):
|
||||
'wireless_lans', 'link_peer', 'connection', 'tags', 'vdcs', 'vrf', 'l2vpn', 'ip_addresses', 'fhrp_groups',
|
||||
'untagged_vlan', 'tagged_vlans', 'actions',
|
||||
)
|
||||
order_by = ('name',)
|
||||
default_columns = (
|
||||
'pk', 'name', 'label', 'enabled', 'type', 'parent', 'lag', 'mtu', 'mode', 'description', 'ip_addresses',
|
||||
'cable', 'connection',
|
||||
|
@ -34,13 +34,21 @@ class ManufacturerTable(ContactsColumnMixin, NetBoxTable):
|
||||
url_params={'manufacturer_id': 'pk'},
|
||||
verbose_name='Device Types'
|
||||
)
|
||||
inventoryitem_count = tables.Column(
|
||||
moduletype_count = columns.LinkedCountColumn(
|
||||
viewname='dcim:moduletype_list',
|
||||
url_params={'manufacturer_id': 'pk'},
|
||||
verbose_name='Module Types'
|
||||
)
|
||||
inventoryitem_count = columns.LinkedCountColumn(
|
||||
viewname='dcim:inventoryitem_list',
|
||||
url_params={'manufacturer_id': 'pk'},
|
||||
verbose_name='Inventory Items'
|
||||
)
|
||||
platform_count = tables.Column(
|
||||
platform_count = columns.LinkedCountColumn(
|
||||
viewname='dcim:platform_list',
|
||||
url_params={'manufacturer_id': 'pk'},
|
||||
verbose_name='Platforms'
|
||||
)
|
||||
slug = tables.Column()
|
||||
tags = columns.TagColumn(
|
||||
url_name='dcim:manufacturer_list'
|
||||
)
|
||||
@ -48,11 +56,12 @@ class ManufacturerTable(ContactsColumnMixin, NetBoxTable):
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = models.Manufacturer
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'devicetype_count', 'inventoryitem_count', 'platform_count', 'description', 'slug',
|
||||
'tags', 'contacts', 'actions', 'created', 'last_updated',
|
||||
'pk', 'id', 'name', 'devicetype_count', 'moduletype_count', 'inventoryitem_count', 'platform_count',
|
||||
'description', 'slug', 'tags', 'contacts', 'actions', 'created', 'last_updated',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'name', 'devicetype_count', 'inventoryitem_count', 'platform_count', 'description', 'slug',
|
||||
'pk', 'name', 'devicetype_count', 'moduletype_count', 'inventoryitem_count', 'platform_count',
|
||||
'description', 'slug',
|
||||
)
|
||||
|
||||
|
||||
|
@ -19,7 +19,11 @@ __all__ = (
|
||||
|
||||
class RackRoleTable(NetBoxTable):
|
||||
name = tables.Column(linkify=True)
|
||||
rack_count = tables.Column(verbose_name='Racks')
|
||||
rack_count = columns.LinkedCountColumn(
|
||||
viewname='dcim:rack_list',
|
||||
url_params={'role_id': 'pk'},
|
||||
verbose_name='Racks'
|
||||
)
|
||||
color = columns.ColorColumn()
|
||||
tags = columns.TagColumn(
|
||||
url_name='dcim:rackrole_list'
|
||||
|
@ -177,7 +177,6 @@ urlpatterns = [
|
||||
path('devices/', views.DeviceListView.as_view(), name='device_list'),
|
||||
path('devices/add/', views.DeviceEditView.as_view(), name='device_add'),
|
||||
path('devices/import/', views.DeviceBulkImportView.as_view(), name='device_import'),
|
||||
path('devices/import/child-devices/', views.ChildDeviceBulkImportView.as_view(), name='device_import_child'),
|
||||
path('devices/edit/', views.DeviceBulkEditView.as_view(), name='device_bulk_edit'),
|
||||
path('devices/rename/', views.DeviceBulkRenameView.as_view(), name='device_bulk_rename'),
|
||||
path('devices/delete/', views.DeviceBulkDeleteView.as_view(), name='device_bulk_delete'),
|
||||
|
@ -21,9 +21,7 @@ from utilities.paginator import EnhancedPaginator, get_paginate_count
|
||||
from utilities.permissions import get_permission_for_model
|
||||
from utilities.utils import count_related
|
||||
from utilities.views import GetReturnURLMixin, ObjectPermissionRequiredMixin, ViewTab, register_model_view
|
||||
from virtualization.filtersets import VirtualMachineFilterSet
|
||||
from virtualization.models import VirtualMachine
|
||||
from virtualization.tables import VirtualMachineTable
|
||||
from . import filtersets, forms, tables
|
||||
from .choices import DeviceFaceChoices
|
||||
from .constants import NONCONNECTABLE_IFACE_TYPES
|
||||
@ -212,6 +210,18 @@ class RegionListView(generic.ObjectListView):
|
||||
class RegionView(generic.ObjectView):
|
||||
queryset = Region.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
regions = instance.get_descendants(include_self=True)
|
||||
related_models = (
|
||||
(Site.objects.restrict(request.user, 'view').filter(region__in=regions), 'region_id'),
|
||||
(Location.objects.restrict(request.user, 'view').filter(site__region__in=regions), 'region_id'),
|
||||
(Rack.objects.restrict(request.user, 'view').filter(site__region__in=regions), 'region_id'),
|
||||
)
|
||||
|
||||
return {
|
||||
'related_models': related_models,
|
||||
}
|
||||
|
||||
|
||||
@register_model_view(Region, 'edit')
|
||||
class RegionEditView(generic.ObjectEditView):
|
||||
@ -276,6 +286,18 @@ class SiteGroupListView(generic.ObjectListView):
|
||||
class SiteGroupView(generic.ObjectView):
|
||||
queryset = SiteGroup.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
groups = instance.get_descendants(include_self=True)
|
||||
related_models = (
|
||||
(Site.objects.restrict(request.user, 'view').filter(group__in=groups), 'group_id'),
|
||||
(Location.objects.restrict(request.user, 'view').filter(site__group__in=groups), 'site_group_id'),
|
||||
(Rack.objects.restrict(request.user, 'view').filter(site__group__in=groups), 'site_group_id'),
|
||||
)
|
||||
|
||||
return {
|
||||
'related_models': related_models,
|
||||
}
|
||||
|
||||
|
||||
@register_model_view(SiteGroup, 'edit')
|
||||
class SiteGroupEditView(generic.ObjectEditView):
|
||||
@ -335,19 +357,25 @@ class SiteView(generic.ObjectView):
|
||||
queryset = Site.objects.prefetch_related('tenant__group')
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
stats = {
|
||||
'location_count': Location.objects.restrict(request.user, 'view').filter(site=instance).count(),
|
||||
'rack_count': Rack.objects.restrict(request.user, 'view').filter(site=instance).count(),
|
||||
'device_count': Device.objects.restrict(request.user, 'view').filter(site=instance).count(),
|
||||
'prefix_count': Prefix.objects.restrict(request.user, 'view').filter(site=instance).count(),
|
||||
'vlangroup_count': VLANGroup.objects.restrict(request.user, 'view').filter(
|
||||
related_models = (
|
||||
# DCIM
|
||||
(Location.objects.restrict(request.user, 'view').filter(site=instance), 'site_id'),
|
||||
(Rack.objects.restrict(request.user, 'view').filter(site=instance), 'site_id'),
|
||||
(Device.objects.restrict(request.user, 'view').filter(site=instance), 'site_id'),
|
||||
# Virtualization
|
||||
(VirtualMachine.objects.restrict(request.user, 'view').filter(cluster__site=instance), 'site_id'),
|
||||
# IPAM
|
||||
(Prefix.objects.restrict(request.user, 'view').filter(site=instance), 'site_id'),
|
||||
(ASN.objects.restrict(request.user, 'view').filter(sites=instance), 'site_id'),
|
||||
(VLANGroup.objects.restrict(request.user, 'view').filter(
|
||||
scope_type=ContentType.objects.get_for_model(Site),
|
||||
scope_id=instance.pk
|
||||
).count(),
|
||||
'vlan_count': VLAN.objects.restrict(request.user, 'view').filter(site=instance).count(),
|
||||
'circuit_count': Circuit.objects.restrict(request.user, 'view').filter(terminations__site=instance).distinct().count(),
|
||||
'vm_count': VirtualMachine.objects.restrict(request.user, 'view').filter(cluster__site=instance).count(),
|
||||
}
|
||||
), 'site_id'),
|
||||
(VLAN.objects.restrict(request.user, 'view').filter(site=instance), 'site_id'),
|
||||
# Circuits
|
||||
(Circuit.objects.restrict(request.user, 'view').filter(terminations__site=instance).distinct(), 'site_id'),
|
||||
)
|
||||
|
||||
locations = Location.objects.add_related_count(
|
||||
Location.objects.all(),
|
||||
Rack,
|
||||
@ -369,15 +397,9 @@ class SiteView(generic.ObjectView):
|
||||
parent_bay__isnull=True
|
||||
).prefetch_related('device_type__manufacturer', 'parent_bay', 'device_role')
|
||||
|
||||
asns = ASN.objects.restrict(request.user, 'view').filter(sites=instance)
|
||||
asn_count = asns.count()
|
||||
|
||||
stats.update({'asn_count': asn_count})
|
||||
|
||||
return {
|
||||
'stats': stats,
|
||||
'related_models': related_models,
|
||||
'locations': locations,
|
||||
'asns': asns,
|
||||
'nonracked_devices': nonracked_devices.order_by('-pk')[:10],
|
||||
'total_nonracked_devices_count': nonracked_devices.count(),
|
||||
}
|
||||
@ -441,9 +463,11 @@ class LocationView(generic.ObjectView):
|
||||
queryset = Location.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
location_ids = instance.get_descendants(include_self=True).values_list('pk', flat=True)
|
||||
rack_count = Rack.objects.filter(location__in=location_ids).count()
|
||||
device_count = Device.objects.filter(location__in=location_ids).count()
|
||||
locations = instance.get_descendants(include_self=True)
|
||||
related_models = (
|
||||
(Rack.objects.restrict(request.user, 'view').filter(location__in=locations), 'location_id'),
|
||||
(Device.objects.restrict(request.user, 'view').filter(location__in=locations), 'location_id'),
|
||||
)
|
||||
|
||||
nonracked_devices = Device.objects.filter(
|
||||
location=instance,
|
||||
@ -452,8 +476,7 @@ class LocationView(generic.ObjectView):
|
||||
).prefetch_related('device_type__manufacturer', 'parent_bay', 'device_role')
|
||||
|
||||
return {
|
||||
'rack_count': rack_count,
|
||||
'device_count': device_count,
|
||||
'related_models': related_models,
|
||||
'nonracked_devices': nonracked_devices.order_by('-pk')[:10],
|
||||
'total_nonracked_devices_count': nonracked_devices.count(),
|
||||
}
|
||||
@ -518,6 +541,15 @@ class RackRoleListView(generic.ObjectListView):
|
||||
class RackRoleView(generic.ObjectView):
|
||||
queryset = RackRole.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
related_models = (
|
||||
(Rack.objects.restrict(request.user, 'view').filter(role=instance), 'role_id'),
|
||||
)
|
||||
|
||||
return {
|
||||
'related_models': related_models,
|
||||
}
|
||||
|
||||
|
||||
@register_model_view(RackRole, 'edit')
|
||||
class RackRoleEditView(generic.ObjectEditView):
|
||||
@ -623,6 +655,11 @@ class RackView(generic.ObjectView):
|
||||
queryset = Rack.objects.prefetch_related('site__region', 'tenant__group', 'location', 'role')
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
related_models = (
|
||||
(Device.objects.restrict(request.user, 'view').filter(rack=instance), 'rack_id'),
|
||||
(PowerFeed.objects.restrict(request.user).filter(rack=instance), 'rack_id'),
|
||||
)
|
||||
|
||||
# Get 0U devices located within the rack
|
||||
nonracked_devices = Device.objects.filter(
|
||||
rack=instance,
|
||||
@ -639,22 +676,13 @@ class RackView(generic.ObjectView):
|
||||
next_rack = peer_racks.filter(_name__gt=instance._name).first()
|
||||
prev_rack = peer_racks.filter(_name__lt=instance._name).reverse().first()
|
||||
|
||||
reservations = RackReservation.objects.restrict(request.user, 'view').filter(rack=instance)
|
||||
power_feeds = PowerFeed.objects.restrict(request.user, 'view').filter(rack=instance).prefetch_related(
|
||||
'power_panel'
|
||||
)
|
||||
|
||||
device_count = Device.objects.restrict(request.user, 'view').filter(rack=instance).count()
|
||||
|
||||
# Determine any additional parameters to pass when embedding the rack elevations
|
||||
svg_extra = '&'.join([
|
||||
f'highlight=id:{pk}' for pk in request.GET.getlist('device')
|
||||
])
|
||||
|
||||
return {
|
||||
'device_count': device_count,
|
||||
'reservations': reservations,
|
||||
'power_feeds': power_feeds,
|
||||
'related_models': related_models,
|
||||
'nonracked_devices': nonracked_devices,
|
||||
'next_rack': next_rack,
|
||||
'prev_rack': prev_rack,
|
||||
@ -662,6 +690,25 @@ class RackView(generic.ObjectView):
|
||||
}
|
||||
|
||||
|
||||
@register_model_view(Rack, 'reservations')
|
||||
class RackRackReservationsView(generic.ObjectChildrenView):
|
||||
queryset = Rack.objects.all()
|
||||
child_model = RackReservation
|
||||
table = tables.RackReservationTable
|
||||
filterset = filtersets.RackReservationFilterSet
|
||||
template_name = 'dcim/rack/reservations.html'
|
||||
tab = ViewTab(
|
||||
label=_('Reservations'),
|
||||
badge=lambda obj: obj.reservations.count(),
|
||||
permission='dcim.view_rackreservation',
|
||||
weight=510,
|
||||
hide_if_empty=True
|
||||
)
|
||||
|
||||
def get_children(self, request, parent):
|
||||
return parent.reservations.restrict(request.user, 'view')
|
||||
|
||||
|
||||
@register_model_view(Rack, 'edit')
|
||||
class RackEditView(generic.ObjectEditView):
|
||||
queryset = Rack.objects.all()
|
||||
@ -763,6 +810,7 @@ class RackReservationBulkDeleteView(generic.BulkDeleteView):
|
||||
class ManufacturerListView(generic.ObjectListView):
|
||||
queryset = Manufacturer.objects.annotate(
|
||||
devicetype_count=count_related(DeviceType, 'manufacturer'),
|
||||
moduletype_count=count_related(ModuleType, 'manufacturer'),
|
||||
inventoryitem_count=count_related(InventoryItem, 'manufacturer'),
|
||||
platform_count=count_related(Platform, 'manufacturer')
|
||||
)
|
||||
@ -776,20 +824,15 @@ class ManufacturerView(generic.ObjectView):
|
||||
queryset = Manufacturer.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
device_types = DeviceType.objects.restrict(request.user, 'view').filter(
|
||||
manufacturer=instance
|
||||
)
|
||||
module_types = ModuleType.objects.restrict(request.user, 'view').filter(
|
||||
manufacturer=instance
|
||||
)
|
||||
inventory_items = InventoryItem.objects.restrict(request.user, 'view').filter(
|
||||
manufacturer=instance
|
||||
related_models = (
|
||||
(DeviceType.objects.restrict(request.user, 'view').filter(manufacturer=instance), 'manufacturer_id'),
|
||||
(ModuleType.objects.restrict(request.user, 'view').filter(manufacturer=instance), 'manufacturer_id'),
|
||||
(InventoryItem.objects.restrict(request.user, 'view').filter(manufacturer=instance), 'manufacturer_id'),
|
||||
(Platform.objects.restrict(request.user, 'view').filter(manufacturer=instance), 'manufacturer_id'),
|
||||
)
|
||||
|
||||
return {
|
||||
'devicetype_count': device_types.count(),
|
||||
'inventoryitem_count': inventory_items.count(),
|
||||
'moduletype_count': module_types.count(),
|
||||
'related_models': related_models,
|
||||
}
|
||||
|
||||
|
||||
@ -812,7 +855,10 @@ class ManufacturerBulkImportView(generic.BulkImportView):
|
||||
|
||||
class ManufacturerBulkEditView(generic.BulkEditView):
|
||||
queryset = Manufacturer.objects.annotate(
|
||||
devicetype_count=count_related(DeviceType, 'manufacturer')
|
||||
devicetype_count=count_related(DeviceType, 'manufacturer'),
|
||||
moduletype_count=count_related(ModuleType, 'manufacturer'),
|
||||
inventoryitem_count=count_related(InventoryItem, 'manufacturer'),
|
||||
platform_count=count_related(Platform, 'manufacturer')
|
||||
)
|
||||
filterset = filtersets.ManufacturerFilterSet
|
||||
table = tables.ManufacturerTable
|
||||
@ -821,7 +867,10 @@ class ManufacturerBulkEditView(generic.BulkEditView):
|
||||
|
||||
class ManufacturerBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = Manufacturer.objects.annotate(
|
||||
devicetype_count=count_related(DeviceType, 'manufacturer')
|
||||
devicetype_count=count_related(DeviceType, 'manufacturer'),
|
||||
moduletype_count=count_related(ModuleType, 'manufacturer'),
|
||||
inventoryitem_count=count_related(InventoryItem, 'manufacturer'),
|
||||
platform_count=count_related(Platform, 'manufacturer')
|
||||
)
|
||||
table = tables.ManufacturerTable
|
||||
|
||||
@ -844,10 +893,12 @@ class DeviceTypeView(generic.ObjectView):
|
||||
queryset = DeviceType.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
instance_count = Device.objects.restrict(request.user).filter(device_type=instance).count()
|
||||
related_models = (
|
||||
(Device.objects.restrict(request.user).filter(device_type=instance), 'device_type_id'),
|
||||
)
|
||||
|
||||
return {
|
||||
'instance_count': instance_count,
|
||||
'related_models': related_models,
|
||||
}
|
||||
|
||||
|
||||
@ -1082,10 +1133,12 @@ class ModuleTypeView(generic.ObjectView):
|
||||
queryset = ModuleType.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
instance_count = Module.objects.restrict(request.user).filter(module_type=instance).count()
|
||||
related_models = (
|
||||
(Module.objects.restrict(request.user).filter(module_type=instance), 'module_type_id'),
|
||||
)
|
||||
|
||||
return {
|
||||
'instance_count': instance_count,
|
||||
'related_models': related_models,
|
||||
}
|
||||
|
||||
|
||||
@ -1640,41 +1693,15 @@ class DeviceRoleListView(generic.ObjectListView):
|
||||
class DeviceRoleView(generic.ObjectView):
|
||||
queryset = DeviceRole.objects.all()
|
||||
|
||||
|
||||
@register_model_view(DeviceRole, 'devices', path='devices')
|
||||
class DeviceRoleDevicesView(generic.ObjectChildrenView):
|
||||
queryset = DeviceRole.objects.all()
|
||||
child_model = Device
|
||||
table = tables.DeviceTable
|
||||
filterset = filtersets.DeviceFilterSet
|
||||
template_name = 'dcim/devicerole/devices.html'
|
||||
tab = ViewTab(
|
||||
label=_('Devices'),
|
||||
badge=lambda obj: obj.devices.count(),
|
||||
permission='dcim.view_device',
|
||||
weight=400
|
||||
def get_extra_context(self, request, instance):
|
||||
related_models = (
|
||||
(Device.objects.restrict(request.user, 'view').filter(device_role=instance), 'role_id'),
|
||||
(VirtualMachine.objects.restrict(request.user, 'view').filter(role=instance), 'role_id'),
|
||||
)
|
||||
|
||||
def get_children(self, request, parent):
|
||||
return Device.objects.restrict(request.user, 'view').filter(device_role=parent)
|
||||
|
||||
|
||||
@register_model_view(DeviceRole, 'virtual_machines', path='virtual-machines')
|
||||
class DeviceRoleVirtualMachinesView(generic.ObjectChildrenView):
|
||||
queryset = DeviceRole.objects.all()
|
||||
child_model = VirtualMachine
|
||||
table = VirtualMachineTable
|
||||
filterset = VirtualMachineFilterSet
|
||||
template_name = 'dcim/devicerole/virtual_machines.html'
|
||||
tab = ViewTab(
|
||||
label=_('Virtual machines'),
|
||||
badge=lambda obj: obj.virtual_machines.count(),
|
||||
permission='virtualization.view_virtualmachine',
|
||||
weight=500
|
||||
)
|
||||
|
||||
def get_children(self, request, parent):
|
||||
return VirtualMachine.objects.restrict(request.user, 'view').filter(role=parent)
|
||||
return {
|
||||
'related_models': related_models,
|
||||
}
|
||||
|
||||
|
||||
@register_model_view(DeviceRole, 'edit')
|
||||
@ -1731,16 +1758,13 @@ class PlatformView(generic.ObjectView):
|
||||
queryset = Platform.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
devices = Device.objects.restrict(request.user, 'view').filter(
|
||||
platform=instance
|
||||
)
|
||||
virtual_machines = VirtualMachine.objects.restrict(request.user, 'view').filter(
|
||||
platform=instance
|
||||
related_models = (
|
||||
(Device.objects.restrict(request.user, 'view').filter(platform=instance), 'platform_id'),
|
||||
(VirtualMachine.objects.restrict(request.user, 'view').filter(platform=instance), 'platform_id'),
|
||||
)
|
||||
|
||||
return {
|
||||
'device_count': devices.count(),
|
||||
'virtualmachine_count': virtual_machines.count()
|
||||
'related_models': related_models,
|
||||
}
|
||||
|
||||
|
||||
@ -1798,14 +1822,7 @@ class DeviceView(generic.ObjectView):
|
||||
else:
|
||||
vc_members = []
|
||||
|
||||
services = Service.objects.restrict(request.user, 'view').filter(device=instance)
|
||||
vdcs = VirtualDeviceContext.objects.restrict(request.user, 'view').filter(device=instance).prefetch_related(
|
||||
'tenant'
|
||||
)
|
||||
|
||||
return {
|
||||
'services': services,
|
||||
'vdcs': vdcs,
|
||||
'vc_members': vc_members,
|
||||
'svg_extra': f'highlight=id:{instance.pk}'
|
||||
}
|
||||
@ -1994,19 +2011,12 @@ class DeviceBulkImportView(generic.BulkImportView):
|
||||
queryset = Device.objects.all()
|
||||
model_form = forms.DeviceImportForm
|
||||
table = tables.DeviceImportTable
|
||||
template_name = 'dcim/device_import.html'
|
||||
|
||||
|
||||
class ChildDeviceBulkImportView(generic.BulkImportView):
|
||||
queryset = Device.objects.all()
|
||||
model_form = forms.ChildDeviceImportForm
|
||||
table = tables.DeviceImportTable
|
||||
template_name = 'dcim/device_import_child.html'
|
||||
|
||||
def save_object(self, object_form, request):
|
||||
obj = object_form.save()
|
||||
|
||||
# Save the reverse relation to the parent device bay
|
||||
# For child devices, save the reverse relation to the parent device bay
|
||||
if getattr(obj, 'parent_bay', None):
|
||||
device_bay = obj.parent_bay
|
||||
device_bay.installed_device = obj
|
||||
device_bay.save()
|
||||
@ -2113,6 +2123,21 @@ class ModuleListView(generic.ObjectListView):
|
||||
class ModuleView(generic.ObjectView):
|
||||
queryset = Module.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
related_models = (
|
||||
(Interface.objects.restrict(request.user, 'view').filter(module=instance), 'module_id'),
|
||||
(ConsolePort.objects.restrict(request.user, 'view').filter(module=instance), 'module_id'),
|
||||
(ConsoleServerPort.objects.restrict(request.user, 'view').filter(module=instance), 'module_id'),
|
||||
(PowerPort.objects.restrict(request.user, 'view').filter(module=instance), 'module_id'),
|
||||
(PowerOutlet.objects.restrict(request.user, 'view').filter(module=instance), 'module_id'),
|
||||
(FrontPort.objects.restrict(request.user, 'view').filter(module=instance), 'module_id'),
|
||||
(RearPort.objects.restrict(request.user, 'view').filter(module=instance), 'module_id'),
|
||||
)
|
||||
|
||||
return {
|
||||
'related_models': related_models,
|
||||
}
|
||||
|
||||
|
||||
@register_model_view(Module, 'edit')
|
||||
class ModuleEditView(generic.ObjectEditView):
|
||||
@ -3435,6 +3460,15 @@ class PowerPanelListView(generic.ObjectListView):
|
||||
class PowerPanelView(generic.ObjectView):
|
||||
queryset = PowerPanel.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
related_models = (
|
||||
(PowerFeed.objects.restrict(request.user).filter(power_panel=instance), 'power_panel_id'),
|
||||
)
|
||||
|
||||
return {
|
||||
'related_models': related_models,
|
||||
}
|
||||
|
||||
|
||||
@register_model_view(PowerPanel, 'edit')
|
||||
class PowerPanelEditView(generic.ObjectEditView):
|
||||
@ -3536,6 +3570,15 @@ class VirtualDeviceContextListView(generic.ObjectListView):
|
||||
class VirtualDeviceContextView(generic.ObjectView):
|
||||
queryset = VirtualDeviceContext.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
related_models = (
|
||||
(Interface.objects.restrict(request.user, 'view').filter(vdcs__in=[instance]), 'vdc_id'),
|
||||
)
|
||||
|
||||
return {
|
||||
'related_models': related_models,
|
||||
}
|
||||
|
||||
|
||||
@register_model_view(VirtualDeviceContext, 'edit')
|
||||
class VirtualDeviceContextEditView(generic.ObjectEditView):
|
||||
|
@ -273,10 +273,13 @@ class CustomField(CloningMixin, ExportTemplatesMixin, WebhooksMixin, ChangeLogge
|
||||
'choices': "Choices may be set only for custom selection fields."
|
||||
})
|
||||
|
||||
# A selection field must have at least two choices defined
|
||||
if self.type == CustomFieldTypeChoices.TYPE_SELECT and self.choices and len(self.choices) < 2:
|
||||
# Selection fields must have at least one choice defined
|
||||
if self.type in (
|
||||
CustomFieldTypeChoices.TYPE_SELECT,
|
||||
CustomFieldTypeChoices.TYPE_MULTISELECT
|
||||
) and not self.choices:
|
||||
raise ValidationError({
|
||||
'choices': "Selection fields must specify at least two choices."
|
||||
'choices': "Selection fields must specify at least one choice."
|
||||
})
|
||||
|
||||
# A selection field's default (if any) must be present in its available choices
|
||||
|
@ -101,6 +101,7 @@ class CustomFieldTest(APIViewTestCases.APIViewTestCase):
|
||||
'content_types': ['dcim.site'],
|
||||
'name': 'cf6',
|
||||
'type': 'select',
|
||||
'choices': ['A', 'B', 'C']
|
||||
},
|
||||
]
|
||||
bulk_update_data = {
|
||||
|
@ -923,6 +923,18 @@ class ServiceFilterSet(NetBoxModelFilterSet):
|
||||
to_field_name='name',
|
||||
label=_('Virtual machine (name)'),
|
||||
)
|
||||
ipaddress_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='ipaddresses',
|
||||
queryset=IPAddress.objects.all(),
|
||||
label=_('IP address (ID)'),
|
||||
)
|
||||
ipaddress = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='ipaddresses__address',
|
||||
queryset=IPAddress.objects.all(),
|
||||
to_field_name='address',
|
||||
label=_('IP address'),
|
||||
)
|
||||
|
||||
port = NumericArrayFilter(
|
||||
field_name='ports',
|
||||
lookup_expr='contains'
|
||||
|
@ -1420,6 +1420,19 @@ class ServiceTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
)
|
||||
Device.objects.bulk_create(devices)
|
||||
|
||||
interface = Interface.objects.create(
|
||||
device=devices[0],
|
||||
name='eth0',
|
||||
type=InterfaceTypeChoices.TYPE_VIRTUAL
|
||||
)
|
||||
interface_ct = ContentType.objects.get_for_model(Interface).pk
|
||||
ip_addresses = (
|
||||
IPAddress(address='192.0.2.1/24', assigned_object_type_id=interface_ct, assigned_object_id=interface.pk),
|
||||
IPAddress(address='192.0.2.2/24', assigned_object_type_id=interface_ct, assigned_object_id=interface.pk),
|
||||
IPAddress(address='192.0.2.3/24', assigned_object_type_id=interface_ct, assigned_object_id=interface.pk),
|
||||
)
|
||||
IPAddress.objects.bulk_create(ip_addresses)
|
||||
|
||||
clustertype = ClusterType.objects.create(name='Cluster Type 1', slug='cluster-type-1')
|
||||
cluster = Cluster.objects.create(type=clustertype, name='Cluster 1')
|
||||
|
||||
@ -1439,6 +1452,9 @@ class ServiceTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
Service(virtual_machine=virtual_machines[2], name='Service 6', protocol=ServiceProtocolChoices.PROTOCOL_UDP, ports=[2003]),
|
||||
)
|
||||
Service.objects.bulk_create(services)
|
||||
services[0].ipaddresses.add(ip_addresses[0])
|
||||
services[1].ipaddresses.add(ip_addresses[1])
|
||||
services[2].ipaddresses.add(ip_addresses[2])
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Service 1', 'Service 2']}
|
||||
@ -1470,6 +1486,13 @@ class ServiceTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
params = {'virtual_machine': [vms[0].name, vms[1].name]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_ipaddress(self):
|
||||
ips = IPAddress.objects.all()[:2]
|
||||
params = {'ipaddress_id': [ips[0].pk, ips[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'ipaddress': [str(ips[0].address), str(ips[1].address)]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
class L2VPNTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = L2VPN.objects.all()
|
||||
|
@ -37,8 +37,10 @@ class VRFView(generic.ObjectView):
|
||||
queryset = VRF.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
prefix_count = Prefix.objects.restrict(request.user, 'view').filter(vrf=instance).count()
|
||||
ipaddress_count = IPAddress.objects.restrict(request.user, 'view').filter(vrf=instance).count()
|
||||
related_models = (
|
||||
(Prefix.objects.restrict(request.user, 'view').filter(vrf=instance), 'vrf_id'),
|
||||
(IPAddress.objects.restrict(request.user, 'view').filter(vrf=instance), 'vrf_id'),
|
||||
)
|
||||
|
||||
import_targets_table = tables.RouteTargetTable(
|
||||
instance.import_targets.all(),
|
||||
@ -50,8 +52,7 @@ class VRFView(generic.ObjectView):
|
||||
)
|
||||
|
||||
return {
|
||||
'prefix_count': prefix_count,
|
||||
'ipaddress_count': ipaddress_count,
|
||||
'related_models': related_models,
|
||||
'import_targets_table': import_targets_table,
|
||||
'export_targets_table': export_targets_table,
|
||||
}
|
||||
@ -102,21 +103,6 @@ class RouteTargetListView(generic.ObjectListView):
|
||||
class RouteTargetView(generic.ObjectView):
|
||||
queryset = RouteTarget.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
importing_vrfs_table = tables.VRFTable(
|
||||
instance.importing_vrfs.all(),
|
||||
orderable=False
|
||||
)
|
||||
exporting_vrfs_table = tables.VRFTable(
|
||||
instance.exporting_vrfs.all(),
|
||||
orderable=False
|
||||
)
|
||||
|
||||
return {
|
||||
'importing_vrfs_table': importing_vrfs_table,
|
||||
'exporting_vrfs_table': exporting_vrfs_table,
|
||||
}
|
||||
|
||||
|
||||
@register_model_view(RouteTarget, 'edit')
|
||||
class RouteTargetEditView(generic.ObjectEditView):
|
||||
@ -165,6 +151,15 @@ class RIRListView(generic.ObjectListView):
|
||||
class RIRView(generic.ObjectView):
|
||||
queryset = RIR.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
related_models = (
|
||||
(Aggregate.objects.restrict(request.user, 'view').filter(rir=instance), 'rir_id'),
|
||||
)
|
||||
|
||||
return {
|
||||
'related_models': related_models,
|
||||
}
|
||||
|
||||
|
||||
@register_model_view(RIR, 'edit')
|
||||
class RIREditView(generic.ObjectEditView):
|
||||
@ -219,12 +214,13 @@ class ASNView(generic.ObjectView):
|
||||
queryset = ASN.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
sites = instance.sites.restrict(request.user, 'view')
|
||||
providers = instance.providers.restrict(request.user, 'view')
|
||||
related_models = (
|
||||
(Site.objects.restrict(request.user, 'view').filter(asns__in=[instance]), 'asn_id'),
|
||||
(Provider.objects.restrict(request.user, 'view').filter(asns__in=[instance]), 'asn_id'),
|
||||
)
|
||||
|
||||
return {
|
||||
'sites_count': sites.count(),
|
||||
'providers_count': providers.count(),
|
||||
'related_models': related_models,
|
||||
}
|
||||
|
||||
|
||||
@ -368,6 +364,17 @@ class RoleListView(generic.ObjectListView):
|
||||
class RoleView(generic.ObjectView):
|
||||
queryset = Role.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
related_models = (
|
||||
(Prefix.objects.restrict(request.user, 'view').filter(role=instance), 'role_id'),
|
||||
(IPRange.objects.restrict(request.user, 'view').filter(role=instance), 'role_id'),
|
||||
(VLAN.objects.restrict(request.user, 'view').filter(role=instance), 'role_id'),
|
||||
)
|
||||
|
||||
return {
|
||||
'related_models': related_models,
|
||||
}
|
||||
|
||||
|
||||
@register_model_view(Role, 'edit')
|
||||
class RoleEditView(generic.ObjectEditView):
|
||||
@ -694,28 +701,10 @@ class IPAddressView(generic.ObjectView):
|
||||
related_ips_table = tables.IPAddressTable(related_ips, orderable=False)
|
||||
related_ips_table.configure(request)
|
||||
|
||||
# Find services belonging to the IP
|
||||
service_filter = Q(ipaddresses=instance)
|
||||
|
||||
# Find services listening on all IPs on the assigned device/vm
|
||||
try:
|
||||
if instance.assigned_object and instance.assigned_object.parent_object:
|
||||
parent_object = instance.assigned_object.parent_object
|
||||
|
||||
if isinstance(parent_object, VirtualMachine):
|
||||
service_filter |= (Q(virtual_machine=parent_object) & Q(ipaddresses=None))
|
||||
elif isinstance(parent_object, Device):
|
||||
service_filter |= (Q(device=parent_object) & Q(ipaddresses=None))
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
services = Service.objects.restrict(request.user, 'view').filter(service_filter)
|
||||
|
||||
return {
|
||||
'parent_prefixes_table': parent_prefixes_table,
|
||||
'duplicate_ips_table': duplicate_ips_table,
|
||||
'related_ips_table': related_ips_table,
|
||||
'services': services,
|
||||
}
|
||||
|
||||
|
||||
@ -839,11 +828,15 @@ class VLANGroupView(generic.ObjectView):
|
||||
queryset = VLANGroup.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
related_models = (
|
||||
(VLAN.objects.restrict(request.user, 'view').filter(group=instance), 'group_id'),
|
||||
)
|
||||
|
||||
# TODO: Replace with embedded table
|
||||
vlans = VLAN.objects.restrict(request.user, 'view').filter(group=instance).prefetch_related(
|
||||
Prefetch('prefixes', queryset=Prefix.objects.restrict(request.user)),
|
||||
'tenant', 'site', 'role',
|
||||
).order_by('vid')
|
||||
vlans_count = vlans.count()
|
||||
vlans = add_available_vlans(vlans, vlan_group=instance)
|
||||
|
||||
vlans_table = tables.VLANTable(vlans, user=request.user, exclude=('group',))
|
||||
@ -852,7 +845,7 @@ class VLANGroupView(generic.ObjectView):
|
||||
vlans_table.configure(request)
|
||||
|
||||
return {
|
||||
'vlans_count': vlans_count,
|
||||
'related_models': related_models,
|
||||
'vlans_table': vlans_table,
|
||||
}
|
||||
|
||||
|
@ -1,21 +1,17 @@
|
||||
import logging
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
|
||||
from django.db import transaction
|
||||
from django.db.models import ProtectedError
|
||||
from django.http import Http404
|
||||
from rest_framework import mixins as drf_mixins
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
from rest_framework.viewsets import GenericViewSet
|
||||
|
||||
from extras.models import ExportTemplate
|
||||
from netbox.api.exceptions import SerializerNotFound
|
||||
from netbox.constants import NESTED_SERIALIZER_PREFIX
|
||||
from utilities.api import get_serializer_for_model
|
||||
from utilities.exceptions import AbortRequest
|
||||
from .mixins import *
|
||||
from . import mixins
|
||||
|
||||
__all__ = (
|
||||
'NetBoxReadOnlyModelViewSet',
|
||||
'NetBoxModelViewSet',
|
||||
)
|
||||
|
||||
@ -30,13 +26,47 @@ HTTP_ACTIONS = {
|
||||
}
|
||||
|
||||
|
||||
class NetBoxModelViewSet(BulkUpdateModelMixin, BulkDestroyModelMixin, ObjectValidationMixin, ModelViewSet):
|
||||
class BaseViewSet(GenericViewSet):
|
||||
"""
|
||||
Base class for all API ViewSets. This is responsible for the enforcement of object-based permissions.
|
||||
"""
|
||||
def initial(self, request, *args, **kwargs):
|
||||
super().initial(request, *args, **kwargs)
|
||||
|
||||
# Restrict the view's QuerySet to allow only the permitted objects
|
||||
if request.user.is_authenticated:
|
||||
if action := HTTP_ACTIONS[request.method]:
|
||||
self.queryset = self.queryset.restrict(request.user, action)
|
||||
|
||||
|
||||
class NetBoxReadOnlyModelViewSet(
|
||||
mixins.BriefModeMixin,
|
||||
mixins.CustomFieldsMixin,
|
||||
mixins.ExportTemplatesMixin,
|
||||
drf_mixins.RetrieveModelMixin,
|
||||
drf_mixins.ListModelMixin,
|
||||
BaseViewSet
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
class NetBoxModelViewSet(
|
||||
mixins.BulkUpdateModelMixin,
|
||||
mixins.BulkDestroyModelMixin,
|
||||
mixins.ObjectValidationMixin,
|
||||
mixins.BriefModeMixin,
|
||||
mixins.CustomFieldsMixin,
|
||||
mixins.ExportTemplatesMixin,
|
||||
drf_mixins.CreateModelMixin,
|
||||
drf_mixins.RetrieveModelMixin,
|
||||
drf_mixins.UpdateModelMixin,
|
||||
drf_mixins.DestroyModelMixin,
|
||||
drf_mixins.ListModelMixin,
|
||||
BaseViewSet
|
||||
):
|
||||
"""
|
||||
Extend DRF's ModelViewSet to support bulk update and delete functions.
|
||||
"""
|
||||
brief = False
|
||||
brief_prefetch_fields = []
|
||||
|
||||
def get_object_with_snapshot(self):
|
||||
"""
|
||||
Save a pre-change snapshot of the object immediately after retrieving it. This snapshot will be used to
|
||||
@ -48,71 +78,14 @@ class NetBoxModelViewSet(BulkUpdateModelMixin, BulkDestroyModelMixin, ObjectVali
|
||||
return obj
|
||||
|
||||
def get_serializer(self, *args, **kwargs):
|
||||
|
||||
# If a list of objects has been provided, initialize the serializer with many=True
|
||||
if isinstance(kwargs.get('data', {}), list):
|
||||
kwargs['many'] = True
|
||||
|
||||
return super().get_serializer(*args, **kwargs)
|
||||
|
||||
def get_serializer_class(self):
|
||||
logger = logging.getLogger('netbox.api.views.ModelViewSet')
|
||||
|
||||
# If using 'brief' mode, find and return the nested serializer for this model, if one exists
|
||||
if self.brief:
|
||||
logger.debug("Request is for 'brief' format; initializing nested serializer")
|
||||
try:
|
||||
serializer = get_serializer_for_model(self.queryset.model, prefix=NESTED_SERIALIZER_PREFIX)
|
||||
logger.debug(f"Using serializer {serializer}")
|
||||
return serializer
|
||||
except SerializerNotFound:
|
||||
logger.debug(f"Nested serializer for {self.queryset.model} not found!")
|
||||
|
||||
# Fall back to the hard-coded serializer class
|
||||
logger.debug(f"Using serializer {self.serializer_class}")
|
||||
return self.serializer_class
|
||||
|
||||
def get_serializer_context(self):
|
||||
"""
|
||||
For models which support custom fields, populate the `custom_fields` context.
|
||||
"""
|
||||
context = super().get_serializer_context()
|
||||
|
||||
if hasattr(self.queryset.model, 'custom_fields'):
|
||||
content_type = ContentType.objects.get_for_model(self.queryset.model)
|
||||
context.update({
|
||||
'custom_fields': content_type.custom_fields.all(),
|
||||
})
|
||||
|
||||
return context
|
||||
|
||||
def get_queryset(self):
|
||||
# If using brief mode, clear all prefetches from the queryset and append only brief_prefetch_fields (if any)
|
||||
if self.brief:
|
||||
return super().get_queryset().prefetch_related(None).prefetch_related(*self.brief_prefetch_fields)
|
||||
|
||||
return super().get_queryset()
|
||||
|
||||
def initialize_request(self, request, *args, **kwargs):
|
||||
# Check if brief=True has been passed
|
||||
if request.method == 'GET' and request.GET.get('brief'):
|
||||
self.brief = True
|
||||
|
||||
return super().initialize_request(request, *args, **kwargs)
|
||||
|
||||
def initial(self, request, *args, **kwargs):
|
||||
super().initial(request, *args, **kwargs)
|
||||
|
||||
if not request.user.is_authenticated:
|
||||
return
|
||||
|
||||
# Restrict the view's QuerySet to allow only the permitted objects
|
||||
action = HTTP_ACTIONS[request.method]
|
||||
if action:
|
||||
self.queryset = self.queryset.restrict(request.user, action)
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
logger = logging.getLogger('netbox.api.views.ModelViewSet')
|
||||
logger = logging.getLogger(f'netbox.api.views.{self.__class__.__name__}')
|
||||
|
||||
try:
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
@ -136,21 +109,11 @@ class NetBoxModelViewSet(BulkUpdateModelMixin, BulkDestroyModelMixin, ObjectVali
|
||||
**kwargs
|
||||
)
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
# Overrides ListModelMixin to allow processing ExportTemplates.
|
||||
if 'export' in request.GET:
|
||||
content_type = ContentType.objects.get_for_model(self.get_serializer_class().Meta.model)
|
||||
et = ExportTemplate.objects.filter(content_types=content_type, name=request.GET['export']).first()
|
||||
if et is None:
|
||||
raise Http404
|
||||
queryset = self.filter_queryset(self.get_queryset())
|
||||
return et.render_to_response(queryset)
|
||||
|
||||
return super().list(request, *args, **kwargs)
|
||||
# Creates
|
||||
|
||||
def perform_create(self, serializer):
|
||||
model = self.queryset.model
|
||||
logger = logging.getLogger('netbox.api.views.ModelViewSet')
|
||||
logger = logging.getLogger(f'netbox.api.views.{self.__class__.__name__}')
|
||||
logger.info(f"Creating new {model._meta.verbose_name}")
|
||||
|
||||
# Enforce object-level permissions on save()
|
||||
@ -161,6 +124,8 @@ class NetBoxModelViewSet(BulkUpdateModelMixin, BulkDestroyModelMixin, ObjectVali
|
||||
except ObjectDoesNotExist:
|
||||
raise PermissionDenied()
|
||||
|
||||
# Updates
|
||||
|
||||
def update(self, request, *args, **kwargs):
|
||||
# Hotwire get_object() to ensure we save a pre-change snapshot
|
||||
self.get_object = self.get_object_with_snapshot
|
||||
@ -168,7 +133,7 @@ class NetBoxModelViewSet(BulkUpdateModelMixin, BulkDestroyModelMixin, ObjectVali
|
||||
|
||||
def perform_update(self, serializer):
|
||||
model = self.queryset.model
|
||||
logger = logging.getLogger('netbox.api.views.ModelViewSet')
|
||||
logger = logging.getLogger(f'netbox.api.views.{self.__class__.__name__}')
|
||||
logger.info(f"Updating {model._meta.verbose_name} {serializer.instance} (PK: {serializer.instance.pk})")
|
||||
|
||||
# Enforce object-level permissions on save()
|
||||
@ -179,6 +144,8 @@ class NetBoxModelViewSet(BulkUpdateModelMixin, BulkDestroyModelMixin, ObjectVali
|
||||
except ObjectDoesNotExist:
|
||||
raise PermissionDenied()
|
||||
|
||||
# Deletes
|
||||
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
# Hotwire get_object() to ensure we save a pre-change snapshot
|
||||
self.get_object = self.get_object_with_snapshot
|
||||
@ -186,7 +153,7 @@ class NetBoxModelViewSet(BulkUpdateModelMixin, BulkDestroyModelMixin, ObjectVali
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
model = self.queryset.model
|
||||
logger = logging.getLogger('netbox.api.views.ModelViewSet')
|
||||
logger = logging.getLogger(f'netbox.api.views.{self.__class__.__name__}')
|
||||
logger.info(f"Deleting {model._meta.verbose_name} {instance} (PK: {instance.pk})")
|
||||
|
||||
return super().perform_destroy(instance)
|
||||
|
@ -1,17 +1,99 @@
|
||||
import logging
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.db import transaction
|
||||
from django.http import Http404
|
||||
from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
|
||||
from extras.models import ExportTemplate
|
||||
from netbox.api.exceptions import SerializerNotFound
|
||||
from netbox.api.serializers import BulkOperationSerializer
|
||||
from netbox.constants import NESTED_SERIALIZER_PREFIX
|
||||
from utilities.api import get_serializer_for_model
|
||||
|
||||
__all__ = (
|
||||
'BriefModeMixin',
|
||||
'BulkUpdateModelMixin',
|
||||
'CustomFieldsMixin',
|
||||
'ExportTemplatesMixin',
|
||||
'BulkDestroyModelMixin',
|
||||
'ObjectValidationMixin',
|
||||
)
|
||||
|
||||
|
||||
class BriefModeMixin:
|
||||
"""
|
||||
Enables brief mode support, so that the client can invoke a model's nested serializer by passing e.g.
|
||||
GET /api/dcim/sites/?brief=True
|
||||
"""
|
||||
brief = False
|
||||
brief_prefetch_fields = []
|
||||
|
||||
def initialize_request(self, request, *args, **kwargs):
|
||||
# Annotate whether brief mode is active
|
||||
self.brief = request.method == 'GET' and request.GET.get('brief')
|
||||
|
||||
return super().initialize_request(request, *args, **kwargs)
|
||||
|
||||
def get_serializer_class(self):
|
||||
logger = logging.getLogger(f'netbox.api.views.{self.__class__.__name__}')
|
||||
|
||||
# If using 'brief' mode, find and return the nested serializer for this model, if one exists
|
||||
if self.brief:
|
||||
logger.debug("Request is for 'brief' format; initializing nested serializer")
|
||||
try:
|
||||
return get_serializer_for_model(self.queryset.model, prefix=NESTED_SERIALIZER_PREFIX)
|
||||
except SerializerNotFound:
|
||||
logger.debug(
|
||||
f"Nested serializer for {self.queryset.model} not found! Using serializer {self.serializer_class}"
|
||||
)
|
||||
|
||||
return self.serializer_class
|
||||
|
||||
def get_queryset(self):
|
||||
qs = super().get_queryset()
|
||||
|
||||
# If using brief mode, clear all prefetches from the queryset and append only brief_prefetch_fields (if any)
|
||||
if self.brief:
|
||||
return qs.prefetch_related(None).prefetch_related(*self.brief_prefetch_fields)
|
||||
|
||||
return qs
|
||||
|
||||
|
||||
class CustomFieldsMixin:
|
||||
"""
|
||||
For models which support custom fields, populate the `custom_fields` context.
|
||||
"""
|
||||
def get_serializer_context(self):
|
||||
context = super().get_serializer_context()
|
||||
|
||||
if hasattr(self.queryset.model, 'custom_fields'):
|
||||
content_type = ContentType.objects.get_for_model(self.queryset.model)
|
||||
context.update({
|
||||
'custom_fields': content_type.custom_fields.all(),
|
||||
})
|
||||
|
||||
return context
|
||||
|
||||
|
||||
class ExportTemplatesMixin:
|
||||
"""
|
||||
Enable ExportTemplate support for list views.
|
||||
"""
|
||||
def list(self, request, *args, **kwargs):
|
||||
if 'export' in request.GET:
|
||||
content_type = ContentType.objects.get_for_model(self.get_serializer_class().Meta.model)
|
||||
et = ExportTemplate.objects.filter(content_types=content_type, name=request.GET['export']).first()
|
||||
if et is None:
|
||||
raise Http404
|
||||
queryset = self.filter_queryset(self.get_queryset())
|
||||
return et.render_to_response(queryset)
|
||||
|
||||
return super().list(request, *args, **kwargs)
|
||||
|
||||
|
||||
class BulkUpdateModelMixin:
|
||||
"""
|
||||
Support bulk modification of objects using the list endpoint for a model. Accepts a PATCH action with a list of one
|
||||
|
@ -122,7 +122,7 @@ class NetBoxModelBulkEditForm(BootstrapMixin, CustomFieldsMixin, forms.Form):
|
||||
|
||||
def _extend_nullable_fields(self):
|
||||
nullable_custom_fields = [
|
||||
name for name, customfield in self.custom_fields.items() if not customfield.required
|
||||
name for name, customfield in self.custom_fields.items() if (not customfield.required and customfield.ui_visibility == CustomFieldVisibilityChoices.VISIBILITY_READ_WRITE)
|
||||
]
|
||||
self.nullable_fields = (*self.nullable_fields, *nullable_custom_fields)
|
||||
|
||||
|
@ -46,6 +46,7 @@ ORGANIZATION_MENU = Menu(
|
||||
get_model_item('tenancy', 'contact', _('Contacts')),
|
||||
get_model_item('tenancy', 'contactgroup', _('Contact Groups')),
|
||||
get_model_item('tenancy', 'contactrole', _('Contact Roles')),
|
||||
get_model_item('tenancy', 'contactassignment', _('Contact Assignments'), actions=[]),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -28,12 +28,6 @@
|
||||
<th scope="row">Description</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Circuits</th>
|
||||
<td>
|
||||
<a href="{% url 'circuits:circuit_list' %}?type_id={{ object.pk }}">{{ object.circuits.count }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@ -41,19 +35,13 @@
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
{% include 'inc/panels/related_objects.html' %}
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-12">
|
||||
<div class="card">
|
||||
<h5 class="card-header">Circuits</h5>
|
||||
<div class="card-body htmx-container table-responsive"
|
||||
hx-get="{% url 'circuits:circuit_list' %}?type_id={{ object.pk }}"
|
||||
hx-trigger="load"
|
||||
></div>
|
||||
</div>
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -37,21 +37,16 @@
|
||||
<th scope="row">Description</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Circuits</th>
|
||||
<td>
|
||||
<a href="{% url 'circuits:circuit_list' %}?provider={{ object.slug }}">{{ object.circuits.count }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
{% include 'inc/panels/related_objects.html' %}
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% include 'inc/panels/contacts.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
|
@ -37,12 +37,13 @@
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% include 'inc/panels/related_objects.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -157,28 +157,10 @@
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
<div class="card">
|
||||
<h5 class="card-header">Virtual Device Contexts</h5>
|
||||
<div class="card-body">
|
||||
{% if vdcs %}
|
||||
<table class="table table-hover">
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Status</th>
|
||||
<th>Identifier</th>
|
||||
<th>Tenant</th>
|
||||
</tr>
|
||||
{% for vdc in vdcs %}
|
||||
<tr>
|
||||
<td>{{ vdc|linkify }}</td>
|
||||
<td>{% badge vdc.get_status_display bg_color=vdc.get_status_color %}</td>
|
||||
<td>{{ vdc.identifier|placeholder }}</td>
|
||||
<td>{{ vdc.tenant|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="text-muted">None</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card-body htmx-container table-responsive"
|
||||
hx-get="{% url 'dcim:virtualdevicecontext_list' %}?device_id={{ object.pk }}"
|
||||
hx-trigger="load"
|
||||
></div>
|
||||
{% if perms.dcim.add_virtualdevicecontext %}
|
||||
<div class="card-footer text-end noprint">
|
||||
<a href="{% url 'dcim:virtualdevicecontext_add' %}?device={{ object.pk }}" class="btn btn-sm btn-primary">
|
||||
@ -300,7 +282,20 @@
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% include 'inc/panels/services.html' %}
|
||||
<div class="card">
|
||||
<h5 class="card-header">Services</h5>
|
||||
<div class="card-body htmx-container table-responsive"
|
||||
hx-get="{% url 'ipam:service_list' %}?device_id={{ object.pk }}"
|
||||
hx-trigger="load"
|
||||
></div>
|
||||
{% if perms.ipam.add_service %}
|
||||
<div class="card-footer text-end noprint">
|
||||
<a href="{% url 'ipam:service_add' %}?device={{ object.pk }}" class="btn btn-sm btn-primary">
|
||||
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add a service
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% include 'inc/panels/contacts.html' %}
|
||||
{% include 'inc/panels/image_attachments.html' %}
|
||||
{% if object.rack and object.position %}
|
||||
|
@ -1,5 +0,0 @@
|
||||
{% extends 'generic/bulk_import.html' %}
|
||||
|
||||
{% block tabs %}
|
||||
{% include 'dcim/inc/device_import_header.html' %}
|
||||
{% endblock %}
|
@ -1,5 +0,0 @@
|
||||
{% extends 'generic/bulk_import.html' %}
|
||||
|
||||
{% block tabs %}
|
||||
{% include 'dcim/inc/device_import_header.html' with active_tab='child_import' %}
|
||||
{% endblock %}
|
@ -42,22 +42,6 @@
|
||||
<th scope="row">VM Role</th>
|
||||
<td>{% checkmark object.vm_role %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Devices</th>
|
||||
<td>
|
||||
<a href="{% url 'dcim:device_list' %}?role_id={{ object.pk }}">{{ object.devices.count }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Virtual Machines</th>
|
||||
<td>
|
||||
{% if object.vm_role %}
|
||||
<a href="{% url 'virtualization:virtualmachine_list' %}?role_id={{ object.pk }}">{{ object.virtual_machines.count }}</a>
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@ -65,6 +49,7 @@
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
{% include 'inc/panels/related_objects.html' %}
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
|
@ -1,21 +0,0 @@
|
||||
{% extends 'dcim/devicerole.html' %}
|
||||
{% load helpers %}
|
||||
{% load render_table from django_tables2 %}
|
||||
|
||||
{% block content %}
|
||||
{% include 'inc/table_controls_htmx.html' with table_modal='DeviceTable_config' %}
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<div class="card">
|
||||
<div class="card-body htmx-container table-responsive"
|
||||
hx-get="{% url 'dcim:device_list' %}?role_id={{ object.pk }}"
|
||||
hx-trigger="load"
|
||||
></div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock content %}
|
||||
|
||||
{% block modals %}
|
||||
{{ block.super }}
|
||||
{% table_config_form table %}
|
||||
{% endblock modals %}
|
@ -1,21 +0,0 @@
|
||||
{% extends 'dcim/devicerole.html' %}
|
||||
{% load helpers %}
|
||||
{% load render_table from django_tables2 %}
|
||||
|
||||
{% block content %}
|
||||
{% include 'inc/table_controls_htmx.html' with table_modal='VirtualMachineTable_config' %}
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<div class="card">
|
||||
<div class="card-body htmx-container table-responsive"
|
||||
hx-get="{% url 'virtualization:virtualmachine_list' %}?role_id={{ object.pk }}"
|
||||
hx-trigger="load"
|
||||
></div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock content %}
|
||||
|
||||
{% block modals %}
|
||||
{{ block.super }}
|
||||
{% table_config_form table %}
|
||||
{% endblock modals %}
|
@ -85,18 +85,15 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Instances</td>
|
||||
<td><a href="{% url 'dcim:device_list' %}?device_type_id={{ object.pk }}">{{ instance_count }}</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
{% include 'inc/panels/related_objects.html' %}
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
|
@ -1,8 +0,0 @@
|
||||
<ul class="nav nav-tabs px-3">
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class ="nav-link{% if not active_tab %} active{% endif %}" href="{% url 'dcim:device_import' %}">Racked Devices</a>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link{% if active_tab == 'child_import' %} active{% endif %}" href="{% url 'dcim:device_import_child' %}">Child Devices</a>
|
||||
</li>
|
||||
</ul>
|
@ -56,33 +56,15 @@
|
||||
{{ object.tenant|linkify|placeholder }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Racks</th>
|
||||
<td class="position-relative">
|
||||
{% if rack_count %}
|
||||
<div class="position-absolute top-50 end-0 translate-middle-y noprint">
|
||||
<a href="{% url 'dcim:rack_elevation_list' %}?location_id={{ object.pk }}" class="btn btn-sm btn-primary" title="View elevations">
|
||||
<i class="mdi mdi-server"></i>
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<a href="{% url 'dcim:rack_list' %}?location_id={{ object.pk }}">{{ rack_count }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Devices</th>
|
||||
<td>
|
||||
<a href="{% url 'dcim:device_list' %}?location_id={{ object.pk }}">{{ device_count }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/related_objects.html' %}
|
||||
{% include 'inc/panels/contacts.html' %}
|
||||
{% include 'dcim/inc/nonracked_devices.html' %}
|
||||
{% include 'inc/panels/image_attachments.html' %}
|
||||
|
@ -42,24 +42,6 @@
|
||||
<th scope="row">Description</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Device types</th>
|
||||
<td>
|
||||
<a href="{% url 'dcim:devicetype_list' %}?manufacturer_id={{ object.pk }}">{{ devicetype_count }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Module types</th>
|
||||
<td>
|
||||
<a href="{% url 'dcim:moduletype_list' %}?manufacturer_id={{ object.pk }}">{{ moduletype_count }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Inventory Items</th>
|
||||
<td>
|
||||
<a href="{% url 'dcim:inventoryitem_list' %}?manufacturer_id={{ object.pk }}">{{ inventoryitem_count }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@ -67,6 +49,7 @@
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
{% include 'inc/panels/related_objects.html' %}
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/contacts.html' %}
|
||||
{% plugin_right_page object %}
|
||||
@ -74,13 +57,6 @@
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-12">
|
||||
<div class="card">
|
||||
<h5 class="card-header">Device Types</h5>
|
||||
<div class="card-body htmx-container table-responsive"
|
||||
hx-get="{% url 'dcim:devicetype_list' %}?manufacturer_id={{ object.pk }}"
|
||||
hx-trigger="load"
|
||||
></div>
|
||||
</div>
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -81,103 +81,13 @@
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
<div class="card">
|
||||
<h5 class="card-header">Components</h5>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">Interfaces</th>
|
||||
<td>
|
||||
{% with component_count=object.interfaces.count %}
|
||||
{% if component_count %}
|
||||
<a href="{% url 'dcim:interface_list' %}?module_id={{ object.pk }}">{{ component_count }}</a>
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Console Ports</th>
|
||||
<td>
|
||||
{% with component_count=object.consoleports.count %}
|
||||
{% if component_count %}
|
||||
<a href="{% url 'dcim:consoleport_list' %}?module_id={{ object.pk }}">{{ component_count }}</a>
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Console Server Ports</th>
|
||||
<td>
|
||||
{% with component_count=object.consoleserverports.count %}
|
||||
{% if component_count %}
|
||||
<a href="{% url 'dcim:consoleserverport_list' %}?module_id={{ object.pk }}">{{ component_count }}</a>
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Power Ports</th>
|
||||
<td>
|
||||
{% with component_count=object.powerports.count %}
|
||||
{% if component_count %}
|
||||
<a href="{% url 'dcim:powerport_list' %}?module_id={{ object.pk }}">{{ component_count }}</a>
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Power Outlets</th>
|
||||
<td>
|
||||
{% with component_count=object.poweroutlets.count %}
|
||||
{% if component_count %}
|
||||
<a href="{% url 'dcim:poweroutlet_list' %}?module_id={{ object.pk }}">{{ component_count }}</a>
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Front Ports</th>
|
||||
<td>
|
||||
{% with component_count=object.frontports.count %}
|
||||
{% if component_count %}
|
||||
<a href="{% url 'dcim:frontport_list' %}?module_id={{ object.pk }}">{{ component_count }}</a>
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Rear Ports</th>
|
||||
<td>
|
||||
{% with component_count=object.rearports.count %}
|
||||
{% if component_count %}
|
||||
<a href="{% url 'dcim:rearport_list' %}?module_id={{ object.pk }}">{{ component_count }}</a>
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'inc/panels/related_objects.html' %}
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -36,19 +36,16 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Instances</td>
|
||||
<td><a href="{% url 'dcim:module_list' %}?module_type_id={{ object.pk }}">{{ instance_count }}</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% include 'inc/panels/related_objects.html' %}
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/image_attachments.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
|
@ -43,46 +43,26 @@
|
||||
<th scope="row">NAPALM Driver</th>
|
||||
<td>{{ object.napalm_driver|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Devices</th>
|
||||
<td>
|
||||
<a href="{% url 'dcim:device_list' %}?platform_id={{ object.pk }}">{{ device_count }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Virtual Machines</th>
|
||||
<td>
|
||||
<a href="{% url 'virtualization:virtualmachine_list' %}?platform_id={{ object.pk }}">{{ virtualmachine_count }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
<div class="card">
|
||||
<h5 class="card-header">
|
||||
NAPALM Arguments
|
||||
</h5>
|
||||
<h5 class="card-header">NAPALM Arguments</h5>
|
||||
<div class="card-body">
|
||||
<pre>{{ object.napalm_args|json }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
{% include 'inc/panels/related_objects.html' %}
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-12">
|
||||
<div class="card">
|
||||
<h5 class="card-header">Devices</h5>
|
||||
<div class="card-body htmx-container table-responsive"
|
||||
hx-get="{% url 'dcim:device_list' %}?platform_id={{ object.pk }}"
|
||||
hx-trigger="load"
|
||||
></div>
|
||||
</div>
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -38,6 +38,7 @@
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
{% include 'inc/panels/related_objects.html' %}
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/contacts.html' %}
|
||||
{% include 'inc/panels/image_attachments.html' %}
|
||||
|
@ -1,31 +1,9 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% extends 'dcim/rack/base.html' %}
|
||||
{% load buttons %}
|
||||
{% load helpers %}
|
||||
{% load static %}
|
||||
{% load plugins %}
|
||||
|
||||
{% block title %}Rack {{ object }}{% endblock %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
{{ block.super }}
|
||||
<li class="breadcrumb-item"><a href="{% url 'dcim:rack_list' %}?site_id={{ object.site.pk }}">{{ object.site }}</a></li>
|
||||
{% if object.location %}
|
||||
{% for location in object.location.get_ancestors %}
|
||||
<li class="breadcrumb-item"><a href="{% url 'dcim:rack_list' %}?location_id={{ location.pk }}">{{ location }}</a></li>
|
||||
{% endfor %}
|
||||
<li class="breadcrumb-item"><a href="{% url 'dcim:rack_list' %}?location_id={{ object.location.pk }}">{{ object.location }}</a></li>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_controls %}
|
||||
<a {% if prev_rack %}href="{% url 'dcim:rack' pk=prev_rack.pk %}{% endif %}" class="btn btn-sm btn-primary{% if not prev_rack %} disabled{% endif %}">
|
||||
<i class="mdi mdi-chevron-left" aria-hidden="true"></i> Previous
|
||||
</a>
|
||||
<a {% if next_rack %}href="{% url 'dcim:rack' pk=next_rack.pk %}{% endif %}" class="btn btn-sm btn-primary{% if not next_rack %} disabled{% endif %}">
|
||||
<i class="mdi mdi-chevron-right" aria-hidden="true"></i> Next
|
||||
</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col col-12 col-xl-5">
|
||||
@ -90,12 +68,6 @@
|
||||
<th scope="row">Asset Tag</th>
|
||||
<td class="font-monospace">{{ object.asset_tag|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Devices</th>
|
||||
<td>
|
||||
<a href="{% url 'dcim:device_list' %}?rack_id={{ object.id }}">{{ device_count }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Space Utilization</th>
|
||||
<td>{% utilization_graph object.get_utilization %}</td>
|
||||
@ -192,90 +164,7 @@
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% if power_feeds %}
|
||||
<div class="card">
|
||||
<h5 class="card-header">
|
||||
Power Feeds
|
||||
</h5>
|
||||
<div class="card-body">
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th>Panel</th>
|
||||
<th>Feed</th>
|
||||
<th>Status</th>
|
||||
<th>Type</th>
|
||||
<th>Utilization</th>
|
||||
</tr>
|
||||
{% for powerfeed in power_feeds %}
|
||||
<tr>
|
||||
<td>{{ powerfeed.power_panel|linkify }}</td>
|
||||
<td>{{ powerfeed|linkify }}</td>
|
||||
<td>{% badge powerfeed.get_status_display bg_color=powerfeed.get_status_color %}</td>
|
||||
<td>{% badge powerfeed.get_type_display bg_color=powerfeed.get_type_color %}</td>
|
||||
{% with power_port=powerfeed.connected_endpoints.0 %}
|
||||
{% if power_port %}
|
||||
<td>{% utilization_graph power_port.get_power_draw.allocated|percentage:powerfeed.available_power %}</td>
|
||||
{% else %}
|
||||
<td class="text-muted">N/A</td>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% include 'inc/panels/image_attachments.html' %}
|
||||
<div class="card">
|
||||
<h5 class="card-header">
|
||||
Reservations
|
||||
</h5>
|
||||
<div class="card-body">
|
||||
{% if reservations %}
|
||||
<table class="table table-hover">
|
||||
<tr>
|
||||
<th>Units</th>
|
||||
<th>Tenant</th>
|
||||
<th>Description</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
{% for resv in reservations %}
|
||||
<tr>
|
||||
<td>{{ resv|linkify:"unit_list" }}</td>
|
||||
<td>{{ resv.tenant|linkify|placeholder }}</td>
|
||||
<td>
|
||||
{{ resv.description }}<br />
|
||||
<small>{{ resv.user }} · {{ resv.created|annotated_date }}</small>
|
||||
</td>
|
||||
<td class="text-end noprint">
|
||||
{% if perms.dcim.change_rackreservation %}
|
||||
<a href="{% url 'dcim:rackreservation_edit' pk=resv.pk %}?return_url={{ object.get_absolute_url }}" class="btn btn-warning btn-sm" title="Edit Reservation">
|
||||
<i class="mdi mdi-pencil" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if perms.dcim.delete_rackreservation %}
|
||||
<a href="{% url 'dcim:rackreservation_delete' pk=resv.pk %}?return_url={{ object.get_absolute_url }}" class="btn btn-danger btn-sm" title="Delete Reservation">
|
||||
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="text-muted">None</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if perms.dcim.add_rackreservation %}
|
||||
<div class="card-footer text-end noprint">
|
||||
<a href="{% url 'dcim:rackreservation_add' %}?rack={{ object.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-primary btn-sm">
|
||||
<i class="mdi mdi-plus-thick" aria-hidden="true"></i>
|
||||
Add a Reservation
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-12 col-xl-7">
|
||||
@ -300,6 +189,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'inc/panels/related_objects.html' %}
|
||||
{% include 'dcim/inc/nonracked_devices.html' %}
|
||||
{% include 'inc/panels/contacts.html' %}
|
||||
{% plugin_right_page object %}
|
||||
|
23
netbox/templates/dcim/rack/base.html
Normal file
23
netbox/templates/dcim/rack/base.html
Normal file
@ -0,0 +1,23 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
|
||||
{% block title %}Rack {{ object }}{% endblock %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
{{ block.super }}
|
||||
<li class="breadcrumb-item"><a href="{% url 'dcim:rack_list' %}?site_id={{ object.site.pk }}">{{ object.site }}</a></li>
|
||||
{% if object.location %}
|
||||
{% for location in object.location.get_ancestors %}
|
||||
<li class="breadcrumb-item"><a href="{% url 'dcim:rack_list' %}?location_id={{ location.pk }}">{{ location }}</a></li>
|
||||
{% endfor %}
|
||||
<li class="breadcrumb-item"><a href="{% url 'dcim:rack_list' %}?location_id={{ object.location.pk }}">{{ object.location }}</a></li>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_controls %}
|
||||
<a {% if prev_rack %}href="{% url 'dcim:rack' pk=prev_rack.pk %}{% endif %}" class="btn btn-sm btn-primary{% if not prev_rack %} disabled{% endif %}">
|
||||
<i class="mdi mdi-chevron-left" aria-hidden="true"></i> Previous
|
||||
</a>
|
||||
<a {% if next_rack %}href="{% url 'dcim:rack' pk=next_rack.pk %}{% endif %}" class="btn btn-sm btn-primary{% if not next_rack %} disabled{% endif %}">
|
||||
<i class="mdi mdi-chevron-right" aria-hidden="true"></i> Next
|
||||
</a>
|
||||
{% endblock %}
|
43
netbox/templates/dcim/rack/reservations.html
Normal file
43
netbox/templates/dcim/rack/reservations.html
Normal file
@ -0,0 +1,43 @@
|
||||
{% extends 'dcim/rack/base.html' %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block content %}
|
||||
{% include 'inc/table_controls_htmx.html' with table_modal="RackReservationTable_config" %}
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body htmx-container table-responsive" id="object_list">
|
||||
{% include 'htmx/table.html' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="noprint bulk-buttons">
|
||||
<div class="bulk-button-group">
|
||||
{% if 'bulk_edit' in actions %}
|
||||
<button type="submit" name="_edit" formaction="{% url 'dcim:rackreservation_bulk_edit' %}?return_url={% url 'dcim:rack_reservations' pk=object.pk %}" class="btn btn-warning btn-sm">
|
||||
<i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if 'bulk_delete' in actions %}
|
||||
<button type="submit" formaction="{% url 'dcim:rackreservation_bulk_delete' %}?return_url={% url 'dcim:rack_reservations' pk=object.pk %}" class="btn btn-danger btn-sm">
|
||||
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if perms.dcim.add_rackreservation %}
|
||||
<div class="bulk-button-group">
|
||||
<a href="{% url 'dcim:rackreservation_add' %}?rack={{ object.pk }}&return_url={% url 'dcim:rack_reservations' pk=object.pk %}" class="btn btn-primary btn-sm">
|
||||
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add reservation
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
{% block modals %}
|
||||
{{ block.super }}
|
||||
{% table_config_form table %}
|
||||
{% endblock modals %}
|
@ -34,12 +34,6 @@
|
||||
<span class="badge color-label" style="background-color: #{{ object.color }}"> </span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Racks</th>
|
||||
<td>
|
||||
<a href="{% url 'dcim:rack_list' %}?role_id={{ object.pk }}">{{ object.racks.count }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@ -47,19 +41,13 @@
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
{% include 'inc/panels/related_objects.html' %}
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-12">
|
||||
<div class="card">
|
||||
<h5 class="card-header">Racks</h5>
|
||||
<div class="card-body htmx-container table-responsive"
|
||||
hx-get="{% url 'dcim:rack_list' %}?role_id={{ object.pk }}"
|
||||
hx-trigger="load"
|
||||
></div>
|
||||
</div>
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -37,21 +37,21 @@
|
||||
<th scope="row">Parent</th>
|
||||
<td>{{ object.parent|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Sites</th>
|
||||
<td>
|
||||
<a href="{% url 'dcim:site_list' %}?region_id={{ object.pk }}">{{ object.sites.count }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/contacts.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
{% include 'inc/panels/related_objects.html' %}
|
||||
{% include 'inc/panels/contacts.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-12">
|
||||
<div class="card">
|
||||
<h5 class="card-header">Child Regions</h5>
|
||||
<div class="card-body htmx-container table-responsive"
|
||||
@ -66,18 +66,6 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-12">
|
||||
<div class="card">
|
||||
<h5 class="card-header">Sites</h5>
|
||||
<div class="card-body htmx-container table-responsive"
|
||||
hx-get="{% url 'dcim:site_list' %}?region_id={{ object.pk }}"
|
||||
hx-trigger="load"
|
||||
></div>
|
||||
</div>
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -126,112 +126,7 @@
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
<div class="card">
|
||||
<h5 class="card-header">Related Objects</h5>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">Locations</th>
|
||||
<td class="text-end">
|
||||
{% if stats.location_count %}
|
||||
<a href="{% url 'dcim:location_list' %}?site_id={{ object.pk }}">{{ stats.location_count }}</a>
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Racks</th>
|
||||
<td class="text-end">
|
||||
{% if stats.rack_count %}
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-sm btn-light dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
{{ stats.rack_count }}
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a class="dropdown-item" href="{% url 'dcim:rack_list' %}?site_id={{ object.pk }}">View Racks</a></li>
|
||||
<li><a class="dropdown-item" href="{% url 'dcim:rack_elevation_list' %}?site_id={{ object.pk }}">View Elevations</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Devices</th>
|
||||
<td class="text-end">
|
||||
{% if stats.device_count %}
|
||||
<a href="{% url 'dcim:device_list' %}?site_id={{ object.pk }}">{{ stats.device_count }}</a>
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Virtual Machines</th>
|
||||
<td class="text-end">
|
||||
{% if stats.vm_count %}
|
||||
<a href="{% url 'virtualization:virtualmachine_list' %}?site_id={{ object.pk }}">{{ stats.vm_count }}</a>
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Prefixes</th>
|
||||
<td class="text-end">
|
||||
{% if stats.prefix_count %}
|
||||
<a href="{% url 'ipam:prefix_list' %}?site_id={{ object.pk }}">{{ stats.prefix_count }}</a>
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">VLAN Groups</th>
|
||||
<td class="text-end">
|
||||
{% if stats.vlangroup_count %}
|
||||
<a href="{% url 'ipam:vlangroup_list' %}?site={{ object.pk }}">{{ stats.vlangroup_count }}</a>
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">VLANs</th>
|
||||
<td class="text-end">
|
||||
{% if stats.vlan_count %}
|
||||
<a href="{% url 'ipam:vlan_list' %}?site_id={{ object.pk }}">{{ stats.vlan_count }}</a>
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">ASNs</th>
|
||||
<td class="text-end">
|
||||
{% if stats.asn_count %}
|
||||
<a href="{% url 'ipam:asn_list' %}?site_id={{ object.pk }}">{{ stats.asn_count }}</a>
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Circuits</th>
|
||||
<td class="text-end">
|
||||
{% if stats.circuit_count %}
|
||||
<a href="{% url 'circuits:circuit_list' %}?site_id={{ object.pk }}">{{ stats.circuit_count }}</a>
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'dcim/inc/nonracked_devices.html' %}
|
||||
{% include 'inc/panels/related_objects.html' with filter_name='site_id' %}
|
||||
{% include 'inc/panels/contacts.html' %}
|
||||
<div class="card">
|
||||
<h5 class="card-header">Locations</h5>
|
||||
@ -276,40 +171,13 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card">
|
||||
<h5 class="card-header">ASNs</h5>
|
||||
<div class='card-body'>
|
||||
{% if asns %}
|
||||
<table class="table table-hover">
|
||||
<tr>
|
||||
<th>ASN</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
{% for asn in asns %}
|
||||
<tr>
|
||||
<td>{{ asn|linkify }}</td>
|
||||
<td>{{ asn.description|placeholder }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% else %}
|
||||
<span class="text-muted">None</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if perms.ipam.add_asn %}
|
||||
<div class="card-footer text-end noprint">
|
||||
<a href="{% url 'ipam:asn_add' %}?sites={{ object.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-primary btn-sm">
|
||||
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add an ASN
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% include 'inc/panels/image_attachments.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
{% include 'dcim/inc/nonracked_devices.html' %}
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -37,12 +37,6 @@
|
||||
<th scope="row">Parent</th>
|
||||
<td>{{ object.parent|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Sites</th>
|
||||
<td>
|
||||
<a href="{% url 'dcim:site_list' %}?group_id={{ object.pk }}">{{ object.sites.count }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@ -52,6 +46,12 @@
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
{% include 'inc/panels/related_objects.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-12">
|
||||
<div class="card">
|
||||
<h5 class="card-header">Child Groups</h5>
|
||||
<div class="card-body htmx-container table-responsive"
|
||||
@ -66,18 +66,6 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-12">
|
||||
<div class="card">
|
||||
<h5 class="card-header">Sites</h5>
|
||||
<div class="card-body htmx-container table-responsive"
|
||||
hx-get="{% url 'dcim:site_list' %}?group_id={{ object.pk }}"
|
||||
hx-trigger="load"
|
||||
></div>
|
||||
</div>
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -59,10 +59,11 @@
|
||||
</div>
|
||||
</div>
|
||||
{% plugin_left_page object %}
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
{% include 'inc/panels/related_objects.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
|
21
netbox/templates/inc/panels/related_objects.html
Normal file
21
netbox/templates/inc/panels/related_objects.html
Normal file
@ -0,0 +1,21 @@
|
||||
{% load helpers %}
|
||||
|
||||
<div class="card">
|
||||
<h5 class="card-header">Related Objects</h5>
|
||||
<ul class="list-group list-group-flush">
|
||||
{% for qs, filter_param in related_models %}
|
||||
{% with viewname=qs.model|viewname:"list" %}
|
||||
<a href="{% url viewname %}?{{ filter_param }}={{ object.pk }}" class="list-group-item list-group-item-action d-flex justify-content-between">
|
||||
{{ qs.model|meta:"verbose_name_plural"|bettertitle }}
|
||||
{% with count=qs.count %}
|
||||
{% if count %}
|
||||
<span class="badge bg-primary rounded-pill">{{ count }}</span>
|
||||
{% else %}
|
||||
<span class="badge bg-light rounded-pill">—</span>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</a>
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
@ -1,50 +0,0 @@
|
||||
<div class="card">
|
||||
<h5 class="card-header">Services</h5>
|
||||
<div class="card-body">
|
||||
{% if services %}
|
||||
<table class="table table-hover">
|
||||
{% for service in services %}
|
||||
<tr>
|
||||
<td>{{ service|linkify:"name" }}</td>
|
||||
<td>{{ service.get_protocol_display }}</td>
|
||||
<td>{{ service.port_list }}</td>
|
||||
<td>
|
||||
{% for ip in service.ipaddresses.all %}
|
||||
<a href="{{ ip.get_absolute_url }}">{{ ip.address.ip }}</a><br />
|
||||
{% empty %}
|
||||
<span class="text-muted">All IPs</span>
|
||||
{% endfor %}
|
||||
</td>
|
||||
<td>{{ service.description }}</td>
|
||||
<td class="text-end noprint">
|
||||
<a href="{% url 'ipam:service_changelog' pk=service.pk %}" class="btn btn-sm btn-outline-secondary" title="Change Log">
|
||||
<i class="mdi mdi-history"></i>
|
||||
</a>
|
||||
{% if perms.ipam.change_service %}
|
||||
<a href="{% url 'ipam:service_edit' pk=service.pk %}?return_url={{ service.parent.get_absolute_url }}" class="btn btn-warning btn-sm" title="Edit Service">
|
||||
<i class="mdi mdi-pencil"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if perms.ipam.delete_service %}
|
||||
<a href="{% url 'ipam:service_delete' pk=service.pk %}?return_url={{ service.parent.get_absolute_url }}" class="btn btn-danger btn-sm">
|
||||
<i class="mdi mdi-trash-can-outline" title="Delete Service"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="text-muted">None</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if perms.ipam.add_service %}
|
||||
{% with object|meta:"model_name" as object_type %}
|
||||
<div class="card-footer text-end noprint">
|
||||
<a href="{% url 'ipam:service_add' %}{% if object_type == "device" %}?device={{ object.pk }}{% elif object_type == "virtualmachine" %}?virtual_machine={{ object.pk }}{% endif %}" class="btn btn-sm btn-primary">
|
||||
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add Service
|
||||
</a>
|
||||
</div>
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
</div>
|
@ -39,54 +39,21 @@
|
||||
<td>Description</td>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Sites</td>
|
||||
<td>
|
||||
{% if sites_count %}
|
||||
<a href="{% url 'dcim:site_list' %}?asn_id={{ object.pk }}">{{ sites_count }}</a>
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Providers</td>
|
||||
<td>
|
||||
{% if providers_count %}
|
||||
<a href="{% url 'circuits:provider_list' %}?asn_id={{ object.pk }}">{{ providers_count }}</a>
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% plugin_left_page object %}
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
{% include 'inc/panels/related_objects.html' %}
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/tags.html' with tags=object.tags.all url='ipam:asn_list' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
<div class="card">
|
||||
<h5 class="card-header">Sites</h5>
|
||||
<div class="card-body htmx-container table-responsive"
|
||||
hx-get="{% url 'dcim:site_list' %}?asn_id={{ object.pk }}"
|
||||
hx-trigger="load"
|
||||
></div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h5 class="card-header">Providers</h5>
|
||||
<div class="card-body htmx-container table-responsive"
|
||||
hx-get="{% url 'circuits:provider_list' %}?asn_id={{ object.pk }}"
|
||||
hx-trigger="load"
|
||||
></div>
|
||||
</div>
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -117,11 +117,16 @@
|
||||
{% include 'inc/panel_table.html' with table=duplicate_ips_table heading='Duplicate IPs' panel_class='danger' %}
|
||||
{% endif %}
|
||||
{% include 'inc/panel_table.html' with table=related_ips_table heading='Related IPs' %}
|
||||
{% include 'inc/panels/services.html' %}
|
||||
<div class="card">
|
||||
<h5 class="card-header">Services</h5>
|
||||
<div class="card-body htmx-container table-responsive"
|
||||
hx-get="{% url 'ipam:service_list' %}?ipaddress_id={{ object.pk }}"
|
||||
hx-trigger="load"
|
||||
></div>
|
||||
</div>
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
{% plugin_full_width_page object %}
|
||||
|
@ -32,32 +32,20 @@
|
||||
<th scope="row">Private</th>
|
||||
<td>{% checkmark object.is_private %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Aggregates</th>
|
||||
<td>
|
||||
<a href="{% url 'ipam:aggregate_list' %}?rir_id={{ object.pk }}">{{ object.aggregates.count }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% include 'inc/panels/related_objects.html' %}
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-12">
|
||||
<div class="card">
|
||||
<h5 class="card-header">Aggregates</h5>
|
||||
<div class="card-body htmx-container table-responsive"
|
||||
hx-get="{% url 'ipam:aggregate_list' %}?rir_id={{ object.pk }}"
|
||||
hx-trigger="load"
|
||||
></div>
|
||||
</div>
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -32,44 +32,20 @@
|
||||
<th scope="row">Weight</th>
|
||||
<td>{{ object.weight }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Prefixes</th>
|
||||
<td>
|
||||
<a href="{% url 'ipam:prefix_list' %}?role_id={{ object.pk }}">{{ object.prefixes.count }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">IP Ranges</th>
|
||||
<td>
|
||||
<a href="{% url 'ipam:iprange_list' %}?role_id={{ object.pk }}">{{ object.ip_ranges.count }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">VLANs</th>
|
||||
<td>
|
||||
<a href="{% url 'ipam:vlan_list' %}?role_id={{ object.pk }}">{{ object.vlans.count }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% include 'inc/panels/related_objects.html' %}
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-12">
|
||||
<div class="card">
|
||||
<h5 class="card-header">Prefixes</h5>
|
||||
<div class="card-body htmx-container table-responsive"
|
||||
hx-get="{% url 'ipam:prefix_list' %}?role_id={{ object.pk }}"
|
||||
hx-trigger="load"
|
||||
></div>
|
||||
</div>
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -25,18 +25,54 @@
|
||||
</div>
|
||||
</div>
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
<div class="mb-4">
|
||||
{% include 'inc/panel_table.html' with table=importing_vrfs_table heading="Importing VRFs" %}
|
||||
</div>
|
||||
{% include 'inc/panel_table.html' with table=exporting_vrfs_table heading="Exporting VRFs" %}
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-6">
|
||||
<div class="card">
|
||||
<h5 class="card-header">Importing VRFs</h5>
|
||||
<div class="card-body htmx-container table-responsive"
|
||||
hx-get="{% url 'ipam:vrf_list' %}?import_target_id={{ object.pk }}"
|
||||
hx-trigger="load"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
<div class="card">
|
||||
<h5 class="card-header">Exporting VRFs</h5>
|
||||
<div class="card-body htmx-container table-responsive"
|
||||
hx-get="{% url 'ipam:vrf_list' %}?export_target_id={{ object.pk }}"
|
||||
hx-trigger="load"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-6">
|
||||
<div class="card">
|
||||
<h5 class="card-header">Importing L2VPNs</h5>
|
||||
<div class="card-body htmx-container table-responsive"
|
||||
hx-get="{% url 'ipam:l2vpn_list' %}?import_target_id={{ object.pk }}"
|
||||
hx-trigger="load"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
<div class="card">
|
||||
<h5 class="card-header">Exporting L2VPNs</h5>
|
||||
<div class="card-body htmx-container table-responsive"
|
||||
hx-get="{% url 'ipam:l2vpn_list' %}?export_target_id={{ object.pk }}"
|
||||
hx-trigger="load"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
{% plugin_full_width_page object %}
|
||||
|
@ -42,12 +42,6 @@
|
||||
<th scope="row">Permitted VIDs</th>
|
||||
<td>{{ object.min_vid }} - {{ object.max_vid }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">VLANs</th>
|
||||
<td>
|
||||
<a href="{% url 'ipam:vlan_list' %}?group_id={{ object.pk }}">{{ vlans_count }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@ -55,6 +49,7 @@
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
{% include 'inc/panels/related_objects.html' %}
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
|
@ -35,25 +35,14 @@
|
||||
<th scope="row">Description</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Prefixes</th>
|
||||
<td>
|
||||
<a href="{% url 'ipam:prefix_list' %}?vrf_id={{ object.pk }}">{{ prefix_count }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">IP Addresses</th>
|
||||
<td>
|
||||
<a href="{% url 'ipam:ipaddress_list' %}?vrf_id={{ object.pk }}">{{ ipaddress_count }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% include 'inc/panels/related_objects.html' %}
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% plugin_right_page object %}
|
||||
|
@ -67,19 +67,15 @@
|
||||
<th scope="row">Description</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Assignments</th>
|
||||
<td>{{ assignment_count }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-md-5">
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
@ -87,10 +83,10 @@
|
||||
<div class="col col-md-12">
|
||||
<div class="card">
|
||||
<h5 class="card-header">Assignments</h5>
|
||||
<div class="card-body table-responsive">
|
||||
{% render_table assignments_table 'inc/table.html' %}
|
||||
{% include 'inc/paginator.html' with paginator=assignments_table.paginator page=assignments_table.page %}
|
||||
</div>
|
||||
<div class="card-body htmx-container table-responsive"
|
||||
hx-get="{% url 'tenancy:contactassignment_list' %}?contact_id={{ object.pk }}"
|
||||
hx-trigger="load"
|
||||
></div>
|
||||
</div>
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
|
@ -31,12 +31,6 @@
|
||||
<th scope="row">Parent</th>
|
||||
<td>{{ object.parent|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Contacts</th>
|
||||
<td>
|
||||
<a href="{% url 'tenancy:contact_list' %}?group_id={{ object.pk }}">{{ object.contacts.count }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@ -44,7 +38,12 @@
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
{% include 'inc/panels/related_objects.html' %}
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col col-md-12">
|
||||
<div class="card">
|
||||
<h5 class="card-header">Child Groups</h5>
|
||||
<div class="card-body htmx-container table-responsive"
|
||||
@ -59,17 +58,6 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col col-md-12">
|
||||
<div class="card">
|
||||
<h5 class="card-header">Contacts</h5>
|
||||
<div class="card-body htmx-container table-responsive"
|
||||
hx-get="{% url 'tenancy:contact_list' %}?group_id={{ object.pk }}"
|
||||
hx-trigger="load"
|
||||
></div>
|
||||
</div>
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -22,12 +22,6 @@
|
||||
<th scope="row">Description</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Assignments</th>
|
||||
<td>
|
||||
{{ assignment_count }}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@ -35,19 +29,13 @@
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
{% include 'inc/panels/related_objects.html' %}
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-12">
|
||||
<div class="card">
|
||||
<h5 class="card-header">Assigned Contacts</h5>
|
||||
<div class="card-body table-responsive">
|
||||
{% render_table contacts_table 'inc/table.html' %}
|
||||
{% include 'inc/paginator.html' with paginator=contacts_table.paginator page=contacts_table.page %}
|
||||
</div>
|
||||
</div>
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -13,9 +13,7 @@
|
||||
<div class="row">
|
||||
<div class="col col-md-7">
|
||||
<div class="card">
|
||||
<h5 class="card-header">
|
||||
Tenant
|
||||
</h5>
|
||||
<h5 class="card-header">Tenant</h5>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
@ -36,95 +34,7 @@
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-md-5">
|
||||
<div class="card">
|
||||
<h5 class="card-header">
|
||||
Stats
|
||||
</h5>
|
||||
<div class="row card-body">
|
||||
<div class="col col-md-4 text-center">
|
||||
<h2><a href="{% url 'dcim:site_list' %}?tenant_id={{ object.pk }}" class="stat-btn btn {% if stats.site_count %}btn-primary{% else %}btn-outline-dark{% endif %} btn-lg">{{ stats.site_count }}</a></h2>
|
||||
<p>Sites</p>
|
||||
</div>
|
||||
<div class="col col-md-4 text-center">
|
||||
<h2><a href="{% url 'dcim:rack_list' %}?tenant_id={{ object.pk }}" class="stat-btn btn {% if stats.rack_count %}btn-primary{% else %}btn-outline-dark{% endif %} btn-lg">{{ stats.rack_count }}</a></h2>
|
||||
<p>Racks</p>
|
||||
</div>
|
||||
<div class="col col-md-4 text-center">
|
||||
<h2><a href="{% url 'dcim:rackreservation_list' %}?tenant_id={{ object.pk }}" class="stat-btn btn {% if stats.rackreservation_count %}btn-primary{% else %}btn-outline-dark{% endif %} btn-lg">{{ stats.rackreservation_count }}</a></h2>
|
||||
<p>Rack reservations</p>
|
||||
</div>
|
||||
<div class="col col-md-4 text-center">
|
||||
<h2><a href="{% url 'dcim:location_list' %}?tenant_id={{ object.pk }}" class="stat-btn btn {% if stats.location_count %}btn-primary{% else %}btn-outline-dark{% endif %} btn-lg">{{ stats.location_count }}</a></h2>
|
||||
<p>Locations</p>
|
||||
</div>
|
||||
<div class="col col-md-4 text-center">
|
||||
<h2><a href="{% url 'dcim:device_list' %}?tenant_id={{ object.pk }}" class="stat-btn btn {% if stats.device_count %}btn-primary{% else %}btn-outline-dark{% endif %} btn-lg">{{ stats.device_count }}</a></h2>
|
||||
<p>Devices</p>
|
||||
</div>
|
||||
<div class="col col-md-4 text-center">
|
||||
<h2><a href="{% url 'dcim:virtualdevicecontext_list' %}?tenant_id={{ object.pk }}" class="stat-btn btn {% if stats.vdc_count %}btn-primary{% else %}btn-outline-dark{% endif %} btn-lg">{{ stats.vdc_count }}</a></h2>
|
||||
<p>Virtual Device Contexts</p>
|
||||
</div>
|
||||
<div class="col col-md-4 text-center">
|
||||
<h2><a href="{% url 'dcim:cable_list' %}?tenant_id={{ object.pk }}" class="stat-btn btn {% if stats.cable_count %}btn-primary{% else %}btn-outline-dark{% endif %} btn-lg">{{ stats.cable_count }}</a></h2>
|
||||
<p>Cables</p>
|
||||
</div>
|
||||
<div class="col col-md-4 text-center">
|
||||
<h2><a href="{% url 'ipam:vrf_list' %}?tenant_id={{ object.pk }}" class="stat-btn btn {% if stats.vrf_count %}btn-primary{% else %}btn-outline-dark{% endif %} btn-lg">{{ stats.vrf_count }}</a></h2>
|
||||
<p>VRFs</p>
|
||||
</div>
|
||||
<div class="col col-md-4 text-center">
|
||||
<h2><a href="{% url 'ipam:aggregate_list' %}?tenant_id={{ object.pk }}" class="stat-btn btn {% if stats.aggregate_count %}btn-primary{% else %}btn-outline-dark{% endif %} btn-lg">{{ stats.aggregate_count }}</a></h2>
|
||||
<p>Aggregates</p>
|
||||
</div>
|
||||
<div class="col col-md-4 text-center">
|
||||
<h2><a href="{% url 'ipam:asn_list' %}?tenant_id={{ object.pk }}" class="stat-btn btn {% if stats.asn_count %}btn-primary{% else %}btn-outline-dark{% endif %} btn-lg">{{ stats.asn_count }}</a></h2>
|
||||
<p>ASNs</p>
|
||||
</div>
|
||||
<div class="col col-md-4 text-center">
|
||||
<h2><a href="{% url 'ipam:prefix_list' %}?tenant_id={{ object.pk }}" class="stat-btn btn {% if stats.prefix_count %}btn-primary{% else %}btn-outline-dark{% endif %} btn-lg">{{ stats.prefix_count }}</a></h2>
|
||||
<p>Prefixes</p>
|
||||
</div>
|
||||
<div class="col col-md-4 text-center">
|
||||
<h2><a href="{% url 'ipam:iprange_list' %}?tenant_id={{ object.pk }}" class="stat-btn btn {% if stats.iprange_count %}btn-primary{% else %}btn-outline-dark{% endif %} btn-lg">{{ stats.iprange_count }}</a></h2>
|
||||
<p>IP Ranges</p>
|
||||
</div>
|
||||
<div class="col col-md-4 text-center">
|
||||
<h2><a href="{% url 'ipam:ipaddress_list' %}?tenant_id={{ object.pk }}" class="stat-btn btn {% if stats.ipaddress_count %}btn-primary{% else %}btn-outline-dark{% endif %} btn-lg">{{ stats.ipaddress_count }}</a></h2>
|
||||
<p>IP addresses</p>
|
||||
</div>
|
||||
<div class="col col-md-4 text-center">
|
||||
<h2><a href="{% url 'ipam:vlan_list' %}?tenant_id={{ object.pk }}" class="stat-btn btn {% if stats.vlan_count %}btn-primary{% else %}btn-outline-dark{% endif %} btn-lg">{{ stats.vlan_count }}</a></h2>
|
||||
<p>VLANs</p>
|
||||
</div>
|
||||
|
||||
<div class="col col-md-4 text-center">
|
||||
<h2><a href="{% url 'ipam:l2vpn_list' %}?tenant_id={{ object.pk }}" class="stat-btn btn {% if stats.l2vpn_count %}btn-primary{% else %}btn-outline-dark{% endif %} btn-lg">{{ stats.l2vpn_count }}</a></h2>
|
||||
<p>L2VPNs</p>
|
||||
</div>
|
||||
|
||||
<div class="col col-md-4 text-center">
|
||||
<h2><a href="{% url 'circuits:circuit_list' %}?tenant_id={{ object.pk }}" class="stat-btn btn {% if stats.circuit_count %}btn-primary{% else %}btn-outline-dark{% endif %} btn-lg">{{ stats.circuit_count }}</a></h2>
|
||||
<p>Circuits</p>
|
||||
</div>
|
||||
<div class="col col-md-4 text-center">
|
||||
<h2><a href="{% url 'virtualization:virtualmachine_list' %}?tenant_id={{ object.pk }}" class="stat-btn btn {% if stats.virtualmachine_count %}btn-primary{% else %}btn-outline-dark{% endif %} btn-lg">{{ stats.virtualmachine_count }}</a></h2>
|
||||
<p>Virtual machines</p>
|
||||
</div>
|
||||
<div class="col col-md-4 text-center">
|
||||
<h2><a href="{% url 'virtualization:cluster_list' %}?tenant_id={{ object.pk }}" class="stat-btn btn {% if stats.cluster_count %}btn-primary{% else %}btn-outline-dark{% endif %} btn-lg">{{ stats.cluster_count }}</a></h2>
|
||||
<p>Clusters</p>
|
||||
</div>
|
||||
<div class="col col-md-4 text-center">
|
||||
<h2><a href="{% url 'wireless:wirelesslan_list' %}?tenant_id={{ object.pk }}" class="stat-btn btn {% if stats.wirelesslan_count %}btn-primary{% else %}btn-outline-dark{% endif %} btn-lg">{{ stats.wirelesslan_count }}</a></h2>
|
||||
<p>Wireless LANs</p>
|
||||
</div>
|
||||
<div class="col col-md-4 text-center">
|
||||
<h2><a href="{% url 'wireless:wirelesslink_list' %}?tenant_id={{ object.pk }}" class="stat-btn btn {% if stats.wirelesslink_count %}btn-primary{% else %}btn-outline-dark{% endif %} btn-lg">{{ stats.wirelesslink_count }}</a></h2>
|
||||
<p>Wireless Links</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'inc/panels/related_objects.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -39,12 +39,6 @@
|
||||
<th scope="row">Parent</th>
|
||||
<td>{{ object.parent|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Tenants</th>
|
||||
<td>
|
||||
<a href="{% url 'tenancy:tenant_list' %}?group_id={{ object.pk }}">{{ object.tenants.count }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@ -52,7 +46,13 @@
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
{% include 'inc/panels/related_objects.html' %}
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-12">
|
||||
<div class="card">
|
||||
<h5 class="card-header">Child Groups</h5>
|
||||
<div class="card-body htmx-container table-responsive"
|
||||
@ -67,18 +67,6 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-12">
|
||||
<div class="card">
|
||||
<h5 class="card-header">Tenants</h5>
|
||||
<div class="card-body htmx-container table-responsive"
|
||||
hx-get="{% url 'tenancy:tenant_list' %}?group_id={{ object.pk }}"
|
||||
hx-trigger="load"
|
||||
></div>
|
||||
</div>
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -44,10 +44,6 @@
|
||||
<th scope="row">Site</th>
|
||||
<td>{{ object.site|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Virtual Machines</th>
|
||||
<td><a href="{% url 'virtualization:virtualmachine_list' %}?cluster_id={{ object.pk }}">{{ object.virtual_machines.count }}</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -28,12 +28,6 @@
|
||||
<th scope="row">Description</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Clusters</th>
|
||||
<td>
|
||||
<a href="{% url 'virtualization:cluster_list' %}?group_id={{ object.pk }}">{{ object.clusters.count }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@ -41,6 +35,7 @@
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
{% include 'inc/panels/related_objects.html' %}
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/contacts.html' %}
|
||||
{% plugin_right_page object %}
|
||||
@ -48,13 +43,6 @@
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
<div class="card">
|
||||
<h5 class="card-header">Clusters</h5>
|
||||
<div class="card-body htmx-container table-responsive"
|
||||
hx-get="{% url 'virtualization:cluster_list' %}?group_id={{ object.pk }}"
|
||||
hx-trigger="load"
|
||||
></div>
|
||||
</div>
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -41,19 +41,13 @@
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
{% include 'inc/panels/related_objects.html' %}
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
<div class="card">
|
||||
<h5 class="card-header">Clusters</h5>
|
||||
<div class="card-body htmx-container table-responsive"
|
||||
hx-get="{% url 'virtualization:cluster_list' %}?type_id={{ object.pk }}"
|
||||
hx-trigger="load"
|
||||
></div>
|
||||
</div>
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -144,7 +144,20 @@
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'inc/panels/services.html' %}
|
||||
<div class="card">
|
||||
<h5 class="card-header">Services</h5>
|
||||
<div class="card-body htmx-container table-responsive"
|
||||
hx-get="{% url 'ipam:service_list' %}?virtual_machine_id={{ object.pk }}"
|
||||
hx-trigger="load"
|
||||
></div>
|
||||
{% if perms.ipam.add_service %}
|
||||
<div class="card-footer text-end noprint">
|
||||
<a href="{% url 'ipam:service_add' %}?virtual_machine={{ object.pk }}" class="btn btn-sm btn-primary">
|
||||
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add a service
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% include 'inc/panels/contacts.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
|
@ -37,12 +37,6 @@
|
||||
<th scope="row">Parent</th>
|
||||
<td>{{ object.parent|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Wireless LANs</th>
|
||||
<td>
|
||||
<a href="{% url 'wireless:wirelesslan_list' %}?group_id={{ object.pk }}">{{ object.wirelesslans.count }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@ -50,7 +44,13 @@
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
{% include 'inc/panels/related_objects.html' %}
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-12">
|
||||
<div class="card">
|
||||
<h5 class="card-header">Child Groups</h5>
|
||||
<div class="card-body htmx-container table-responsive"
|
||||
@ -65,18 +65,6 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-12">
|
||||
<div class="card">
|
||||
<h5 class="card-header">Wireless LANs</h5>
|
||||
<div class="card-body htmx-container table-responsive"
|
||||
hx-get="{% url 'wireless:wirelesslan_list' %}?group_id={{ object.pk }}"
|
||||
hx-trigger="load"
|
||||
></div>
|
||||
</div>
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,11 +1,17 @@
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from extras.utils import FeatureQuery
|
||||
from netbox.forms import NetBoxModelFilterSetForm
|
||||
from tenancy.choices import *
|
||||
from tenancy.models import *
|
||||
from tenancy.forms import ContactModelFilterForm
|
||||
from utilities.forms import DynamicModelMultipleChoiceField, TagFilterField
|
||||
from utilities.forms.fields import (
|
||||
ContentTypeMultipleChoiceField, DynamicModelMultipleChoiceField, MultipleChoiceField, TagFilterField,
|
||||
)
|
||||
|
||||
__all__ = (
|
||||
'ContactAssignmentFilterForm',
|
||||
'ContactFilterForm',
|
||||
'ContactGroupFilterForm',
|
||||
'ContactRoleFilterForm',
|
||||
@ -71,3 +77,36 @@ class ContactFilterForm(NetBoxModelFilterSetForm):
|
||||
label=_('Group')
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class ContactAssignmentFilterForm(NetBoxModelFilterSetForm):
|
||||
model = ContactAssignment
|
||||
fieldsets = (
|
||||
(None, ('q', 'filter_id')),
|
||||
('Assignment', ('content_type_id', 'group_id', 'contact_id', 'role_id', 'priority')),
|
||||
)
|
||||
content_type_id = ContentTypeMultipleChoiceField(
|
||||
queryset=ContentType.objects.all(),
|
||||
limit_choices_to=FeatureQuery('custom_fields'),
|
||||
required=False,
|
||||
label=_('Object type')
|
||||
)
|
||||
group_id = DynamicModelMultipleChoiceField(
|
||||
queryset=ContactGroup.objects.all(),
|
||||
required=False,
|
||||
label=_('Group')
|
||||
)
|
||||
contact_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Contact.objects.all(),
|
||||
required=False,
|
||||
label=_('Contact')
|
||||
)
|
||||
role_id = DynamicModelMultipleChoiceField(
|
||||
queryset=ContactRole.objects.all(),
|
||||
required=False,
|
||||
label=_('Role')
|
||||
)
|
||||
priority = MultipleChoiceField(
|
||||
choices=ContactPriorityChoices,
|
||||
required=False
|
||||
)
|
||||
|
@ -47,6 +47,7 @@ urlpatterns = [
|
||||
path('contacts/<int:pk>/', include(get_model_urls('tenancy', 'contact'))),
|
||||
|
||||
# Contact assignments
|
||||
path('contact-assignments/', views.ContactAssignmentListView.as_view(), name='contactassignment_list'),
|
||||
path('contact-assignments/add/', views.ContactAssignmentEditView.as_view(), name='contactassignment_add'),
|
||||
path('contact-assignments/<int:pk>/', include(get_model_urls('tenancy', 'contactassignment'))),
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from circuits.models import Circuit
|
||||
from dcim.models import Cable, Device, Location, Rack, RackReservation, Site, VirtualDeviceContext
|
||||
@ -34,6 +35,16 @@ class TenantGroupListView(generic.ObjectListView):
|
||||
class TenantGroupView(generic.ObjectView):
|
||||
queryset = TenantGroup.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
groups = instance.get_descendants(include_self=True)
|
||||
related_models = (
|
||||
(Tenant.objects.restrict(request.user, 'view').filter(group__in=groups), 'group_id'),
|
||||
)
|
||||
|
||||
return {
|
||||
'related_models': related_models,
|
||||
}
|
||||
|
||||
|
||||
@register_model_view(TenantGroup, 'edit')
|
||||
class TenantGroupEditView(generic.ObjectEditView):
|
||||
@ -92,31 +103,36 @@ class TenantView(generic.ObjectView):
|
||||
queryset = Tenant.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
stats = {
|
||||
'site_count': Site.objects.restrict(request.user, 'view').filter(tenant=instance).count(),
|
||||
'rack_count': Rack.objects.restrict(request.user, 'view').filter(tenant=instance).count(),
|
||||
'rackreservation_count': RackReservation.objects.restrict(request.user, 'view').filter(tenant=instance).count(),
|
||||
'location_count': Location.objects.restrict(request.user, 'view').filter(tenant=instance).count(),
|
||||
'device_count': Device.objects.restrict(request.user, 'view').filter(tenant=instance).count(),
|
||||
'vdc_count': VirtualDeviceContext.objects.restrict(request.user, 'view').filter(tenant=instance).count(),
|
||||
'vrf_count': VRF.objects.restrict(request.user, 'view').filter(tenant=instance).count(),
|
||||
'aggregate_count': Aggregate.objects.restrict(request.user, 'view').filter(tenant=instance).count(),
|
||||
'prefix_count': Prefix.objects.restrict(request.user, 'view').filter(tenant=instance).count(),
|
||||
'iprange_count': IPRange.objects.restrict(request.user, 'view').filter(tenant=instance).count(),
|
||||
'ipaddress_count': IPAddress.objects.restrict(request.user, 'view').filter(tenant=instance).count(),
|
||||
'vlan_count': VLAN.objects.restrict(request.user, 'view').filter(tenant=instance).count(),
|
||||
'l2vpn_count': L2VPN.objects.restrict(request.user, 'view').filter(tenant=instance).count(),
|
||||
'circuit_count': Circuit.objects.restrict(request.user, 'view').filter(tenant=instance).count(),
|
||||
'virtualmachine_count': VirtualMachine.objects.restrict(request.user, 'view').filter(tenant=instance).count(),
|
||||
'cluster_count': Cluster.objects.restrict(request.user, 'view').filter(tenant=instance).count(),
|
||||
'cable_count': Cable.objects.restrict(request.user, 'view').filter(tenant=instance).count(),
|
||||
'asn_count': ASN.objects.restrict(request.user, 'view').filter(tenant=instance).count(),
|
||||
'wirelesslan_count': WirelessLAN.objects.restrict(request.user, 'view').filter(tenant=instance).count(),
|
||||
'wirelesslink_count': WirelessLink.objects.restrict(request.user, 'view').filter(tenant=instance).count(),
|
||||
}
|
||||
related_models = [
|
||||
# DCIM
|
||||
(Site.objects.restrict(request.user, 'view').filter(tenant=instance), 'tenant_id'),
|
||||
(Rack.objects.restrict(request.user, 'view').filter(tenant=instance), 'tenant_id'),
|
||||
(RackReservation.objects.restrict(request.user, 'view').filter(tenant=instance), 'tenant_id'),
|
||||
(Location.objects.restrict(request.user, 'view').filter(tenant=instance), 'tenant_id'),
|
||||
(Device.objects.restrict(request.user, 'view').filter(tenant=instance), 'tenant_id'),
|
||||
(VirtualDeviceContext.objects.restrict(request.user, 'view').filter(tenant=instance), 'tenant_id'),
|
||||
(Cable.objects.restrict(request.user, 'view').filter(tenant=instance), 'tenant_id'),
|
||||
# IPAM
|
||||
(VRF.objects.restrict(request.user, 'view').filter(tenant=instance), 'tenant_id'),
|
||||
(Aggregate.objects.restrict(request.user, 'view').filter(tenant=instance), 'tenant_id'),
|
||||
(Prefix.objects.restrict(request.user, 'view').filter(tenant=instance), 'tenant_id'),
|
||||
(IPRange.objects.restrict(request.user, 'view').filter(tenant=instance), 'tenant_id'),
|
||||
(IPAddress.objects.restrict(request.user, 'view').filter(tenant=instance), 'tenant_id'),
|
||||
(ASN.objects.restrict(request.user, 'view').filter(tenant=instance), 'tenant_id'),
|
||||
(VLAN.objects.restrict(request.user, 'view').filter(tenant=instance), 'tenant_id'),
|
||||
(L2VPN.objects.restrict(request.user, 'view').filter(tenant=instance), 'tenant_id'),
|
||||
# Circuits
|
||||
(Circuit.objects.restrict(request.user, 'view').filter(tenant=instance), 'tenant_id'),
|
||||
# Virtualization
|
||||
(VirtualMachine.objects.restrict(request.user, 'view').filter(tenant=instance), 'tenant_id'),
|
||||
(Cluster.objects.restrict(request.user, 'view').filter(tenant=instance), 'tenant_id'),
|
||||
# Wireless
|
||||
(WirelessLAN.objects.restrict(request.user, 'view').filter(tenant=instance), 'tenant_id'),
|
||||
(WirelessLink.objects.restrict(request.user, 'view').filter(tenant=instance), 'tenant_id'),
|
||||
]
|
||||
|
||||
return {
|
||||
'stats': stats,
|
||||
'related_models': related_models,
|
||||
}
|
||||
|
||||
|
||||
@ -171,6 +187,16 @@ class ContactGroupListView(generic.ObjectListView):
|
||||
class ContactGroupView(generic.ObjectView):
|
||||
queryset = ContactGroup.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
groups = instance.get_descendants(include_self=True)
|
||||
related_models = (
|
||||
(Contact.objects.restrict(request.user, 'view').filter(group__in=groups), 'group_id'),
|
||||
)
|
||||
|
||||
return {
|
||||
'related_models': related_models,
|
||||
}
|
||||
|
||||
|
||||
@register_model_view(ContactGroup, 'edit')
|
||||
class ContactGroupEditView(generic.ObjectEditView):
|
||||
@ -229,16 +255,12 @@ class ContactRoleView(generic.ObjectView):
|
||||
queryset = ContactRole.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
contact_assignments = ContactAssignment.objects.restrict(request.user, 'view').filter(
|
||||
role=instance
|
||||
related_models = (
|
||||
(ContactAssignment.objects.restrict(request.user, 'view').filter(role=instance), 'role_id'),
|
||||
)
|
||||
contacts_table = tables.ContactAssignmentTable(contact_assignments, user=request.user)
|
||||
contacts_table.columns.hide('role')
|
||||
contacts_table.configure(request)
|
||||
|
||||
return {
|
||||
'contacts_table': contacts_table,
|
||||
'assignment_count': ContactAssignment.objects.filter(role=instance).count(),
|
||||
'related_models': related_models,
|
||||
}
|
||||
|
||||
|
||||
@ -288,19 +310,6 @@ class ContactListView(generic.ObjectListView):
|
||||
class ContactView(generic.ObjectView):
|
||||
queryset = Contact.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
contact_assignments = ContactAssignment.objects.restrict(request.user, 'view').filter(
|
||||
contact=instance
|
||||
)
|
||||
assignments_table = tables.ContactAssignmentTable(contact_assignments, user=request.user)
|
||||
assignments_table.columns.hide('contact')
|
||||
assignments_table.configure(request)
|
||||
|
||||
return {
|
||||
'assignments_table': assignments_table,
|
||||
'assignment_count': ContactAssignment.objects.filter(contact=instance).count(),
|
||||
}
|
||||
|
||||
|
||||
@register_model_view(Contact, 'edit')
|
||||
class ContactEditView(generic.ObjectEditView):
|
||||
@ -340,6 +349,13 @@ class ContactBulkDeleteView(generic.BulkDeleteView):
|
||||
# Contact assignments
|
||||
#
|
||||
|
||||
class ContactAssignmentListView(generic.ObjectListView):
|
||||
queryset = ContactAssignment.objects.all()
|
||||
filterset = filtersets.ContactAssignmentFilterSet
|
||||
filterset_form = forms.ContactAssignmentFilterForm
|
||||
table = tables.ContactAssignmentTable
|
||||
|
||||
|
||||
@register_model_view(ContactAssignment, 'edit')
|
||||
class ContactAssignmentEditView(generic.ObjectEditView):
|
||||
queryset = ContactAssignment.objects.all()
|
||||
|
@ -19,6 +19,14 @@ COPY_BUTTON = """
|
||||
"""
|
||||
|
||||
|
||||
class TokenActionsColumn(columns.ActionsColumn):
|
||||
# Subclass ActionsColumn to disregard permissions for edit & delete buttons
|
||||
actions = {
|
||||
'edit': columns.ActionsItem('Edit', 'pencil', None, 'warning'),
|
||||
'delete': columns.ActionsItem('Delete', 'trash-can-outline', None, 'danger'),
|
||||
}
|
||||
|
||||
|
||||
class TokenTable(NetBoxTable):
|
||||
key = columns.TemplateColumn(
|
||||
template_code=TOKEN
|
||||
@ -32,7 +40,7 @@ class TokenTable(NetBoxTable):
|
||||
allowed_ips = columns.TemplateColumn(
|
||||
template_code=ALLOWED_IPS
|
||||
)
|
||||
actions = columns.ActionsColumn(
|
||||
actions = TokenActionsColumn(
|
||||
actions=('edit', 'delete'),
|
||||
extra_buttons=COPY_BUTTON
|
||||
)
|
||||
|
@ -527,6 +527,7 @@ def highlight_string(value, highlight, trim_pre=None, trim_post=None, trim_place
|
||||
if type(highlight) is re.Pattern:
|
||||
pre, match, post = highlight.split(value, maxsplit=1)
|
||||
else:
|
||||
highlight = re.escape(highlight)
|
||||
pre, match, post = re.split(fr'({highlight})', value, maxsplit=1, flags=re.IGNORECASE)
|
||||
except ValueError as e:
|
||||
# Match not found
|
||||
|
@ -10,7 +10,7 @@ from dcim.models import Device
|
||||
from dcim.tables import DeviceTable
|
||||
from extras.views import ObjectConfigContextView
|
||||
from ipam.models import IPAddress, Service
|
||||
from ipam.tables import AssignedIPAddressesTable, InterfaceVLANTable
|
||||
from ipam.tables import InterfaceVLANTable
|
||||
from netbox.views import generic
|
||||
from utilities.utils import count_related
|
||||
from utilities.views import ViewTab, register_model_view
|
||||
@ -36,17 +36,12 @@ class ClusterTypeView(generic.ObjectView):
|
||||
queryset = ClusterType.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
clusters = Cluster.objects.restrict(request.user, 'view').filter(
|
||||
type=instance
|
||||
).annotate(
|
||||
device_count=count_related(Device, 'cluster'),
|
||||
vm_count=count_related(VirtualMachine, 'cluster')
|
||||
related_models = (
|
||||
(Cluster.objects.restrict(request.user, 'view').filter(type=instance), 'type_id'),
|
||||
)
|
||||
clusters_table = tables.ClusterTable(clusters, user=request.user, exclude=('type',))
|
||||
clusters_table.configure(request)
|
||||
|
||||
return {
|
||||
'clusters_table': clusters_table,
|
||||
'related_models': related_models,
|
||||
}
|
||||
|
||||
|
||||
@ -100,6 +95,15 @@ class ClusterGroupListView(generic.ObjectListView):
|
||||
class ClusterGroupView(generic.ObjectView):
|
||||
queryset = ClusterGroup.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
related_models = (
|
||||
(Cluster.objects.restrict(request.user, 'view').filter(group=instance), 'group_id'),
|
||||
)
|
||||
|
||||
return {
|
||||
'related_models': related_models,
|
||||
}
|
||||
|
||||
|
||||
@register_model_view(ClusterGroup, 'edit')
|
||||
class ClusterGroupEditView(generic.ObjectEditView):
|
||||
@ -323,32 +327,7 @@ class VirtualMachineListView(generic.ObjectListView):
|
||||
|
||||
@register_model_view(VirtualMachine)
|
||||
class VirtualMachineView(generic.ObjectView):
|
||||
queryset = VirtualMachine.objects.prefetch_related('tenant__group')
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
# Interfaces
|
||||
vminterfaces = VMInterface.objects.restrict(request.user, 'view').filter(
|
||||
virtual_machine=instance
|
||||
).prefetch_related(
|
||||
Prefetch('ip_addresses', queryset=IPAddress.objects.restrict(request.user))
|
||||
)
|
||||
vminterface_table = tables.VirtualMachineVMInterfaceTable(vminterfaces, user=request.user, orderable=False)
|
||||
if request.user.has_perm('virtualization.change_vminterface') or \
|
||||
request.user.has_perm('virtualization.delete_vminterface'):
|
||||
vminterface_table.columns.show('pk')
|
||||
|
||||
# Services
|
||||
services = Service.objects.restrict(request.user, 'view').filter(
|
||||
virtual_machine=instance
|
||||
).prefetch_related(
|
||||
Prefetch('ipaddresses', queryset=IPAddress.objects.restrict(request.user)),
|
||||
'virtual_machine'
|
||||
)
|
||||
|
||||
return {
|
||||
'vminterface_table': vminterface_table,
|
||||
'services': services,
|
||||
}
|
||||
queryset = VirtualMachine.objects.all()
|
||||
|
||||
|
||||
@register_model_view(VirtualMachine, 'interfaces')
|
||||
|
@ -27,6 +27,16 @@ class WirelessLANGroupListView(generic.ObjectListView):
|
||||
class WirelessLANGroupView(generic.ObjectView):
|
||||
queryset = WirelessLANGroup.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
groups = instance.get_descendants(include_self=True)
|
||||
related_models = (
|
||||
(WirelessLAN.objects.restrict(request.user, 'view').filter(group__in=groups), 'group_id'),
|
||||
)
|
||||
|
||||
return {
|
||||
'related_models': related_models,
|
||||
}
|
||||
|
||||
|
||||
@register_model_view(WirelessLANGroup, 'edit')
|
||||
class WirelessLANGroupEditView(generic.ObjectEditView):
|
||||
|
Loading…
Reference in New Issue
Block a user