1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

merge develop

This commit is contained in:
John Anderson
2020-02-18 00:39:30 -05:00
80 changed files with 354 additions and 1138 deletions

View File

@ -22,6 +22,10 @@ django-filter
# https://github.com/django-mptt/django-mptt # https://github.com/django-mptt/django-mptt
django-mptt django-mptt
# Context managers for PostgreSQL advisory locks
# https://github.com/Xof/django-pglocks
django-pglocks
# Prometheus metrics library for Django # Prometheus metrics library for Django
# https://github.com/korfuri/django-prometheus # https://github.com/korfuri/django-prometheus
django-prometheus django-prometheus

View File

@ -80,11 +80,11 @@ REDIS = {
} }
``` ```
!!! note: !!! note
If you are upgrading from a version prior to v2.7, please note that the Redis connection configuration settings have If you are upgrading from a version prior to v2.7, please note that the Redis connection configuration settings have
changed. Manual modification to bring the `REDIS` section inline with the above specification is necessary changed. Manual modification to bring the `REDIS` section inline with the above specification is necessary
!!! warning: !!! note
It is highly recommended to keep the webhook and cache databases separate. Using the same database number on the It is highly recommended to keep the webhook and cache databases separate. Using the same database number on the
same Redis instance for both may result in webhook processing data being lost during cache flushing events. same Redis instance for both may result in webhook processing data being lost during cache flushing events.
@ -124,7 +124,7 @@ REDIS = {
} }
``` ```
!!! note: !!! note
It is possible to have only one or the other Redis configurations to use Sentinel functionality. It is possible It is possible to have only one or the other Redis configurations to use Sentinel functionality. It is possible
for example to have the webhook use sentinel via `HOST`/`PORT` and for caching to use Sentinel via for example to have the webhook use sentinel via `HOST`/`PORT` and for caching to use Sentinel via
`SENTINELS`/`SENTINEL_SERVICE`. `SENTINELS`/`SENTINEL_SERVICE`.

View File

@ -1,3 +1,26 @@
# v2.7.7 (FUTURE)
## Enhancements
* [#3840](https://github.com/netbox-community/netbox/issues/3840) - Enhance search function when selecting VLANs for interface assignment
* [#4170](https://github.com/netbox-community/netbox/issues/4170) - Improve color contrast in rack elevation drawings
## Bug Fixes
* [#2519](https://github.com/netbox-community/netbox/issues/2519) - Avoid race condition when provisioning "next available" IPs/prefixes via the API
* [#4168](https://github.com/netbox-community/netbox/issues/4168) - Role is not required when creating a virtual machine
* [#4175](https://github.com/netbox-community/netbox/issues/4175) - Fix potential exception when bulk editing objects from a filtered list
---
# v2.7.6 (2020-02-13)
## Bug Fixes
* [#4166](https://github.com/netbox-community/netbox/issues/4166) - Fix schema migrations to enforce maximum character length for naturalized fields
---
# v2.7.5 (2020-02-13) # v2.7.5 (2020-02-13)
**Note:** This release includes several database schema migrations that calculate and store copies of names for certain objects to improve natural ordering performance (see [#3799](https://github.com/netbox-community/netbox/issues/3799)). These migrations may take a few minutes to run if you have a very large number of objects defined in NetBox. **Note:** This release includes several database schema migrations that calculate and store copies of names for certain objects to improve natural ordering performance (see [#3799](https://github.com/netbox-community/netbox/issues/3799)). These migrations may take a few minutes to run if you have a very large number of objects defined in NetBox.

View File

@ -29,7 +29,6 @@ class ProviderListView(PermissionRequiredMixin, ObjectListView):
filterset = filters.ProviderFilterSet filterset = filters.ProviderFilterSet
filterset_form = forms.ProviderFilterForm filterset_form = forms.ProviderFilterForm
table = tables.ProviderDetailTable table = tables.ProviderDetailTable
template_name = 'circuits/provider_list.html'
class ProviderView(PermissionRequiredMixin, View): class ProviderView(PermissionRequiredMixin, View):
@ -107,7 +106,6 @@ class CircuitTypeListView(PermissionRequiredMixin, ObjectListView):
permission_required = 'circuits.view_circuittype' permission_required = 'circuits.view_circuittype'
queryset = CircuitType.objects.annotate(circuit_count=Count('circuits')) queryset = CircuitType.objects.annotate(circuit_count=Count('circuits'))
table = tables.CircuitTypeTable table = tables.CircuitTypeTable
template_name = 'circuits/circuittype_list.html'
class CircuitTypeCreateView(PermissionRequiredMixin, ObjectEditView): class CircuitTypeCreateView(PermissionRequiredMixin, ObjectEditView):
@ -151,7 +149,6 @@ class CircuitListView(PermissionRequiredMixin, ObjectListView):
filterset = filters.CircuitFilterSet filterset = filters.CircuitFilterSet
filterset_form = forms.CircuitFilterForm filterset_form = forms.CircuitFilterForm
table = tables.CircuitTable table = tables.CircuitTable
template_name = 'circuits/circuit_list.html'
class CircuitView(PermissionRequiredMixin, View): class CircuitView(PermissionRequiredMixin, View):

View File

@ -2832,7 +2832,10 @@ class InterfaceForm(InterfaceCommonForm, BootstrapMixin, forms.ModelForm):
widget=APISelect( widget=APISelect(
api_url="/api/ipam/vlans/", api_url="/api/ipam/vlans/",
display_field='display_name', display_field='display_name',
full=True full=True,
additional_query_params={
'site_id': 'null',
},
) )
) )
tagged_vlans = DynamicModelMultipleChoiceField( tagged_vlans = DynamicModelMultipleChoiceField(
@ -2842,7 +2845,10 @@ class InterfaceForm(InterfaceCommonForm, BootstrapMixin, forms.ModelForm):
widget=APISelectMultiple( widget=APISelectMultiple(
api_url="/api/ipam/vlans/", api_url="/api/ipam/vlans/",
display_field='display_name', display_field='display_name',
full=True full=True,
additional_query_params={
'site_id': 'null',
},
) )
) )
tags = TagField( tags = TagField(
@ -2871,18 +2877,20 @@ class InterfaceForm(InterfaceCommonForm, BootstrapMixin, forms.ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
# Limit LAG choices to interfaces belonging to this device (or VC master)
if self.is_bound: if self.is_bound:
device = Device.objects.get(pk=self.data['device']) device = Device.objects.get(pk=self.data['device'])
self.fields['lag'].queryset = Interface.objects.filter(
device__in=[device, device.get_vc_master()],
type=InterfaceTypeChoices.TYPE_LAG
)
else: else:
self.fields['lag'].queryset = Interface.objects.filter( device = self.instance.device
device__in=[self.instance.device, self.instance.device.get_vc_master()],
type=InterfaceTypeChoices.TYPE_LAG # Limit LAG choices to interfaces belonging to this device (or VC master)
) self.fields['lag'].queryset = Interface.objects.filter(
device__in=[device, device.get_vc_master()],
type=InterfaceTypeChoices.TYPE_LAG
)
# Add current site to VLANs query params
self.fields['untagged_vlan'].widget.add_additional_query_param('site_id', device.site.pk)
self.fields['tagged_vlans'].widget.add_additional_query_param('site_id', device.site.pk)
class InterfaceCreateForm(BootstrapMixin, InterfaceCommonForm, forms.Form): class InterfaceCreateForm(BootstrapMixin, InterfaceCommonForm, forms.Form):
@ -2942,7 +2950,10 @@ class InterfaceCreateForm(BootstrapMixin, InterfaceCommonForm, forms.Form):
widget=APISelect( widget=APISelect(
api_url="/api/ipam/vlans/", api_url="/api/ipam/vlans/",
display_field='display_name', display_field='display_name',
full=True full=True,
additional_query_params={
'site_id': 'null',
},
) )
) )
tagged_vlans = DynamicModelMultipleChoiceField( tagged_vlans = DynamicModelMultipleChoiceField(
@ -2951,7 +2962,10 @@ class InterfaceCreateForm(BootstrapMixin, InterfaceCommonForm, forms.Form):
widget=APISelectMultiple( widget=APISelectMultiple(
api_url="/api/ipam/vlans/", api_url="/api/ipam/vlans/",
display_field='display_name', display_field='display_name',
full=True full=True,
additional_query_params={
'site_id': 'null',
},
) )
) )
@ -2967,6 +2981,10 @@ class InterfaceCreateForm(BootstrapMixin, InterfaceCommonForm, forms.Form):
type=InterfaceTypeChoices.TYPE_LAG type=InterfaceTypeChoices.TYPE_LAG
) )
# Add current site to VLANs query params
self.fields['untagged_vlan'].widget.add_additional_query_param('site_id', device.site.pk)
self.fields['tagged_vlans'].widget.add_additional_query_param('site_id', device.site.pk)
class InterfaceCSVForm(forms.ModelForm): class InterfaceCSVForm(forms.ModelForm):
device = FlexibleModelChoiceField( device = FlexibleModelChoiceField(
@ -3090,7 +3108,10 @@ class InterfaceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
widget=APISelect( widget=APISelect(
api_url="/api/ipam/vlans/", api_url="/api/ipam/vlans/",
display_field='display_name', display_field='display_name',
full=True full=True,
additional_query_params={
'site_id': 'null',
},
) )
) )
tagged_vlans = DynamicModelMultipleChoiceField( tagged_vlans = DynamicModelMultipleChoiceField(
@ -3099,7 +3120,10 @@ class InterfaceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
widget=APISelectMultiple( widget=APISelectMultiple(
api_url="/api/ipam/vlans/", api_url="/api/ipam/vlans/",
display_field='display_name', display_field='display_name',
full=True full=True,
additional_query_params={
'site_id': 'null',
},
) )
) )
@ -3118,6 +3142,10 @@ class InterfaceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
device__in=[device, device.get_vc_master()], device__in=[device, device.get_vc_master()],
type=InterfaceTypeChoices.TYPE_LAG type=InterfaceTypeChoices.TYPE_LAG
) )
# Add current site to VLANs query params
self.fields['untagged_vlan'].widget.add_additional_query_param('site_id', device.site.pk)
self.fields['tagged_vlans'].widget.add_additional_query_param('site_id', device.site.pk)
else: else:
self.fields['lag'].choices = () self.fields['lag'].choices = ()
self.fields['lag'].widget.attrs['disabled'] = True self.fields['lag'].widget.attrs['disabled'] = True

View File

@ -6,7 +6,7 @@ import utilities.ordering
def _update_model_names(model): def _update_model_names(model):
# Update each unique field value in bulk # Update each unique field value in bulk
for name in model.objects.values_list('name', flat=True).order_by('name').distinct(): for name in model.objects.values_list('name', flat=True).order_by('name').distinct():
model.objects.filter(name=name).update(_name=utilities.ordering.naturalize(name)) model.objects.filter(name=name).update(_name=utilities.ordering.naturalize(name, max_length=100))
def naturalize_consoleports(apps, schema_editor): def naturalize_consoleports(apps, schema_editor):

View File

@ -6,7 +6,7 @@ import utilities.ordering
def _update_model_names(model): def _update_model_names(model):
# Update each unique field value in bulk # Update each unique field value in bulk
for name in model.objects.values_list('name', flat=True).order_by('name').distinct(): for name in model.objects.values_list('name', flat=True).order_by('name').distinct():
model.objects.filter(name=name).update(_name=utilities.ordering.naturalize(name)) model.objects.filter(name=name).update(_name=utilities.ordering.naturalize(name, max_length=100))
def naturalize_consoleporttemplates(apps, schema_editor): def naturalize_consoleporttemplates(apps, schema_editor):

View File

@ -6,7 +6,7 @@ import utilities.ordering
def _update_model_names(model): def _update_model_names(model):
# Update each unique field value in bulk # Update each unique field value in bulk
for name in model.objects.values_list('name', flat=True).order_by('name').distinct(): for name in model.objects.values_list('name', flat=True).order_by('name').distinct():
model.objects.filter(name=name).update(_name=utilities.ordering.naturalize(name)) model.objects.filter(name=name).update(_name=utilities.ordering.naturalize(name, max_length=100))
def naturalize_sites(apps, schema_editor): def naturalize_sites(apps, schema_editor):

View File

@ -6,7 +6,7 @@ import utilities.ordering
def _update_model_names(model): def _update_model_names(model):
# Update each unique field value in bulk # Update each unique field value in bulk
for name in model.objects.values_list('name', flat=True).order_by('name').distinct(): for name in model.objects.values_list('name', flat=True).order_by('name').distinct():
model.objects.filter(name=name).update(_name=utilities.ordering.naturalize_interface(name)) model.objects.filter(name=name).update(_name=utilities.ordering.naturalize_interface(name, max_length=100))
def naturalize_interfacetemplates(apps, schema_editor): def naturalize_interfacetemplates(apps, schema_editor):

View File

@ -382,8 +382,8 @@ class RackElevationHelperMixin:
# add gradients # add gradients
RackElevationHelperMixin._add_gradient(drawing, 'reserved', '#c7c7ff') RackElevationHelperMixin._add_gradient(drawing, 'reserved', '#c7c7ff')
RackElevationHelperMixin._add_gradient(drawing, 'occupied', '#f0f0f0') RackElevationHelperMixin._add_gradient(drawing, 'occupied', '#d7d7d7')
RackElevationHelperMixin._add_gradient(drawing, 'blocked', '#ffc7c7') RackElevationHelperMixin._add_gradient(drawing, 'blocked', '#ffc0c0')
return drawing return drawing

View File

@ -152,7 +152,6 @@ class RegionListView(PermissionRequiredMixin, ObjectListView):
filterset = filters.RegionFilterSet filterset = filters.RegionFilterSet
filterset_form = forms.RegionFilterForm filterset_form = forms.RegionFilterForm
table = tables.RegionTable table = tables.RegionTable
template_name = 'dcim/region_list.html'
class RegionCreateView(PermissionRequiredMixin, ObjectEditView): class RegionCreateView(PermissionRequiredMixin, ObjectEditView):
@ -191,7 +190,6 @@ class SiteListView(PermissionRequiredMixin, ObjectListView):
filterset = filters.SiteFilterSet filterset = filters.SiteFilterSet
filterset_form = forms.SiteFilterForm filterset_form = forms.SiteFilterForm
table = tables.SiteTable table = tables.SiteTable
template_name = 'dcim/site_list.html'
class SiteView(PermissionRequiredMixin, View): class SiteView(PermissionRequiredMixin, View):
@ -271,7 +269,6 @@ class RackGroupListView(PermissionRequiredMixin, ObjectListView):
filterset = filters.RackGroupFilterSet filterset = filters.RackGroupFilterSet
filterset_form = forms.RackGroupFilterForm filterset_form = forms.RackGroupFilterForm
table = tables.RackGroupTable table = tables.RackGroupTable
template_name = 'dcim/rackgroup_list.html'
class RackGroupCreateView(PermissionRequiredMixin, ObjectEditView): class RackGroupCreateView(PermissionRequiredMixin, ObjectEditView):
@ -308,7 +305,6 @@ class RackRoleListView(PermissionRequiredMixin, ObjectListView):
permission_required = 'dcim.view_rackrole' permission_required = 'dcim.view_rackrole'
queryset = RackRole.objects.annotate(rack_count=Count('racks')) queryset = RackRole.objects.annotate(rack_count=Count('racks'))
table = tables.RackRoleTable table = tables.RackRoleTable
template_name = 'dcim/rackrole_list.html'
class RackRoleCreateView(PermissionRequiredMixin, ObjectEditView): class RackRoleCreateView(PermissionRequiredMixin, ObjectEditView):
@ -350,7 +346,6 @@ class RackListView(PermissionRequiredMixin, ObjectListView):
filterset = filters.RackFilterSet filterset = filters.RackFilterSet
filterset_form = forms.RackFilterForm filterset_form = forms.RackFilterForm
table = tables.RackDetailTable table = tables.RackDetailTable
template_name = 'dcim/rack_list.html'
class RackElevationListView(PermissionRequiredMixin, View): class RackElevationListView(PermissionRequiredMixin, View):
@ -474,7 +469,7 @@ class RackReservationListView(PermissionRequiredMixin, ObjectListView):
filterset = filters.RackReservationFilterSet filterset = filters.RackReservationFilterSet
filterset_form = forms.RackReservationFilterForm filterset_form = forms.RackReservationFilterForm
table = tables.RackReservationTable table = tables.RackReservationTable
template_name = 'dcim/rackreservation_list.html' action_buttons = ()
class RackReservationCreateView(PermissionRequiredMixin, ObjectEditView): class RackReservationCreateView(PermissionRequiredMixin, ObjectEditView):
@ -533,7 +528,6 @@ class ManufacturerListView(PermissionRequiredMixin, ObjectListView):
platform_count=Count('platforms', distinct=True), platform_count=Count('platforms', distinct=True),
) )
table = tables.ManufacturerTable table = tables.ManufacturerTable
template_name = 'dcim/manufacturer_list.html'
class ManufacturerCreateView(PermissionRequiredMixin, ObjectEditView): class ManufacturerCreateView(PermissionRequiredMixin, ObjectEditView):
@ -571,7 +565,6 @@ class DeviceTypeListView(PermissionRequiredMixin, ObjectListView):
filterset = filters.DeviceTypeFilterSet filterset = filters.DeviceTypeFilterSet
filterset_form = forms.DeviceTypeFilterForm filterset_form = forms.DeviceTypeFilterForm
table = tables.DeviceTypeTable table = tables.DeviceTypeTable
template_name = 'dcim/devicetype_list.html'
class DeviceTypeView(PermissionRequiredMixin, View): class DeviceTypeView(PermissionRequiredMixin, View):
@ -995,7 +988,6 @@ class DeviceRoleListView(PermissionRequiredMixin, ObjectListView):
permission_required = 'dcim.view_devicerole' permission_required = 'dcim.view_devicerole'
queryset = DeviceRole.objects.all() queryset = DeviceRole.objects.all()
table = tables.DeviceRoleTable table = tables.DeviceRoleTable
template_name = 'dcim/devicerole_list.html'
class DeviceRoleCreateView(PermissionRequiredMixin, ObjectEditView): class DeviceRoleCreateView(PermissionRequiredMixin, ObjectEditView):
@ -1031,7 +1023,6 @@ class PlatformListView(PermissionRequiredMixin, ObjectListView):
permission_required = 'dcim.view_platform' permission_required = 'dcim.view_platform'
queryset = Platform.objects.all() queryset = Platform.objects.all()
table = tables.PlatformTable table = tables.PlatformTable
template_name = 'dcim/platform_list.html'
class PlatformCreateView(PermissionRequiredMixin, ObjectEditView): class PlatformCreateView(PermissionRequiredMixin, ObjectEditView):
@ -1292,7 +1283,7 @@ class ConsolePortListView(PermissionRequiredMixin, ObjectListView):
filterset = filters.ConsolePortFilterSet filterset = filters.ConsolePortFilterSet
filterset_form = forms.ConsolePortFilterForm filterset_form = forms.ConsolePortFilterForm
table = tables.ConsolePortDetailTable table = tables.ConsolePortDetailTable
template_name = 'dcim/consoleport_list.html' action_buttons = ('import', 'export')
class ConsolePortCreateView(PermissionRequiredMixin, ComponentCreateView): class ConsolePortCreateView(PermissionRequiredMixin, ComponentCreateView):
@ -1345,7 +1336,7 @@ class ConsoleServerPortListView(PermissionRequiredMixin, ObjectListView):
filterset = filters.ConsoleServerPortFilterSet filterset = filters.ConsoleServerPortFilterSet
filterset_form = forms.ConsoleServerPortFilterForm filterset_form = forms.ConsoleServerPortFilterForm
table = tables.ConsoleServerPortDetailTable table = tables.ConsoleServerPortDetailTable
template_name = 'dcim/consoleserverport_list.html' action_buttons = ('import', 'export')
class ConsoleServerPortCreateView(PermissionRequiredMixin, ComponentCreateView): class ConsoleServerPortCreateView(PermissionRequiredMixin, ComponentCreateView):
@ -1410,7 +1401,7 @@ class PowerPortListView(PermissionRequiredMixin, ObjectListView):
filterset = filters.PowerPortFilterSet filterset = filters.PowerPortFilterSet
filterset_form = forms.PowerPortFilterForm filterset_form = forms.PowerPortFilterForm
table = tables.PowerPortDetailTable table = tables.PowerPortDetailTable
template_name = 'dcim/powerport_list.html' action_buttons = ('import', 'export')
class PowerPortCreateView(PermissionRequiredMixin, ComponentCreateView): class PowerPortCreateView(PermissionRequiredMixin, ComponentCreateView):
@ -1463,7 +1454,7 @@ class PowerOutletListView(PermissionRequiredMixin, ObjectListView):
filterset = filters.PowerOutletFilterSet filterset = filters.PowerOutletFilterSet
filterset_form = forms.PowerOutletFilterForm filterset_form = forms.PowerOutletFilterForm
table = tables.PowerOutletDetailTable table = tables.PowerOutletDetailTable
template_name = 'dcim/poweroutlet_list.html' action_buttons = ('import', 'export')
class PowerOutletCreateView(PermissionRequiredMixin, ComponentCreateView): class PowerOutletCreateView(PermissionRequiredMixin, ComponentCreateView):
@ -1528,7 +1519,7 @@ class InterfaceListView(PermissionRequiredMixin, ObjectListView):
filterset = filters.InterfaceFilterSet filterset = filters.InterfaceFilterSet
filterset_form = forms.InterfaceFilterForm filterset_form = forms.InterfaceFilterForm
table = tables.InterfaceDetailTable table = tables.InterfaceDetailTable
template_name = 'dcim/interface_list.html' action_buttons = ('import', 'export')
class InterfaceView(PermissionRequiredMixin, View): class InterfaceView(PermissionRequiredMixin, View):
@ -1630,7 +1621,7 @@ class FrontPortListView(PermissionRequiredMixin, ObjectListView):
filterset = filters.FrontPortFilterSet filterset = filters.FrontPortFilterSet
filterset_form = forms.FrontPortFilterForm filterset_form = forms.FrontPortFilterForm
table = tables.FrontPortDetailTable table = tables.FrontPortDetailTable
template_name = 'dcim/frontport_list.html' action_buttons = ('import', 'export')
class FrontPortCreateView(PermissionRequiredMixin, ComponentCreateView): class FrontPortCreateView(PermissionRequiredMixin, ComponentCreateView):
@ -1695,7 +1686,7 @@ class RearPortListView(PermissionRequiredMixin, ObjectListView):
filterset = filters.RearPortFilterSet filterset = filters.RearPortFilterSet
filterset_form = forms.RearPortFilterForm filterset_form = forms.RearPortFilterForm
table = tables.RearPortDetailTable table = tables.RearPortDetailTable
template_name = 'dcim/rearport_list.html' action_buttons = ('import', 'export')
class RearPortCreateView(PermissionRequiredMixin, ComponentCreateView): class RearPortCreateView(PermissionRequiredMixin, ComponentCreateView):
@ -1762,7 +1753,7 @@ class DeviceBayListView(PermissionRequiredMixin, ObjectListView):
filterset = filters.DeviceBayFilterSet filterset = filters.DeviceBayFilterSet
filterset_form = forms.DeviceBayFilterForm filterset_form = forms.DeviceBayFilterForm
table = tables.DeviceBayDetailTable table = tables.DeviceBayDetailTable
template_name = 'dcim/devicebay_list.html' action_buttons = ('import', 'export')
class DeviceBayCreateView(PermissionRequiredMixin, ComponentCreateView): class DeviceBayCreateView(PermissionRequiredMixin, ComponentCreateView):
@ -1961,7 +1952,7 @@ class CableListView(PermissionRequiredMixin, ObjectListView):
filterset = filters.CableFilterSet filterset = filters.CableFilterSet
filterset_form = forms.CableFilterForm filterset_form = forms.CableFilterForm
table = tables.CableTable table = tables.CableTable
template_name = 'dcim/cable_list.html' action_buttons = ('import', 'export')
class CableView(PermissionRequiredMixin, View): class CableView(PermissionRequiredMixin, View):
@ -2233,7 +2224,7 @@ class InventoryItemListView(PermissionRequiredMixin, ObjectListView):
filterset = filters.InventoryItemFilterSet filterset = filters.InventoryItemFilterSet
filterset_form = forms.InventoryItemFilterForm filterset_form = forms.InventoryItemFilterForm
table = tables.InventoryItemTable table = tables.InventoryItemTable
template_name = 'dcim/inventoryitem_list.html' action_buttons = ('import', 'export')
class InventoryItemEditView(PermissionRequiredMixin, ObjectEditView): class InventoryItemEditView(PermissionRequiredMixin, ObjectEditView):
@ -2289,7 +2280,7 @@ class VirtualChassisListView(PermissionRequiredMixin, ObjectListView):
table = tables.VirtualChassisTable table = tables.VirtualChassisTable
filterset = filters.VirtualChassisFilterSet filterset = filters.VirtualChassisFilterSet
filterset_form = forms.VirtualChassisFilterForm filterset_form = forms.VirtualChassisFilterForm
template_name = 'dcim/virtualchassis_list.html' action_buttons = ('export',)
class VirtualChassisCreateView(PermissionRequiredMixin, View): class VirtualChassisCreateView(PermissionRequiredMixin, View):
@ -2533,7 +2524,6 @@ class PowerPanelListView(PermissionRequiredMixin, ObjectListView):
filterset = filters.PowerPanelFilterSet filterset = filters.PowerPanelFilterSet
filterset_form = forms.PowerPanelFilterForm filterset_form = forms.PowerPanelFilterForm
table = tables.PowerPanelTable table = tables.PowerPanelTable
template_name = 'dcim/powerpanel_list.html'
class PowerPanelView(PermissionRequiredMixin, View): class PowerPanelView(PermissionRequiredMixin, View):
@ -2602,7 +2592,6 @@ class PowerFeedListView(PermissionRequiredMixin, ObjectListView):
filterset = filters.PowerFeedFilterSet filterset = filters.PowerFeedFilterSet
filterset_form = forms.PowerFeedFilterForm filterset_form = forms.PowerFeedFilterForm
table = tables.PowerFeedTable table = tables.PowerFeedTable
template_name = 'dcim/powerfeed_list.html'
class PowerFeedView(PermissionRequiredMixin, View): class PowerFeedView(PermissionRequiredMixin, View):

View File

@ -1,28 +1,8 @@
from django.apps import AppConfig from django.apps import AppConfig
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
import redis
class ExtrasConfig(AppConfig): class ExtrasConfig(AppConfig):
name = "extras" name = "extras"
def ready(self): def ready(self):
import extras.signals import extras.signals
# Check that we can connect to the configured Redis database.
try:
rs = redis.Redis(
host=settings.WEBHOOKS_REDIS_HOST,
port=settings.WEBHOOKS_REDIS_PORT,
db=settings.WEBHOOKS_REDIS_DATABASE,
password=settings.WEBHOOKS_REDIS_PASSWORD or None,
ssl=settings.WEBHOOKS_REDIS_SSL,
)
rs.ping()
except redis.exceptions.ConnectionError:
raise ImproperlyConfigured(
"Unable to connect to the Redis database. Check that the Redis configuration has been defined in "
"configuration.py."
)

View File

@ -86,7 +86,7 @@ class Command(BaseCommand):
# Find all unique values for the field # Find all unique values for the field
queryset = model.objects.values_list(target_field, flat=True).order_by(target_field).distinct() queryset = model.objects.values_list(target_field, flat=True).order_by(target_field).distinct()
for value in queryset: for value in queryset:
naturalized_value = naturalize(value) naturalized_value = naturalize(value, max_length=field.max_length)
if options['verbosity'] >= 2: if options['verbosity'] >= 2:
self.stdout.write(" {} -> {}".format(value, naturalized_value), ending='') self.stdout.write(" {} -> {}".format(value, naturalized_value), ending='')

View File

@ -34,7 +34,7 @@ class TagListView(PermissionRequiredMixin, ObjectListView):
filterset = filters.TagFilterSet filterset = filters.TagFilterSet
filterset_form = forms.TagFilterForm filterset_form = forms.TagFilterForm
table = TagTable table = TagTable
template_name = 'extras/tag_list.html' action_buttons = ()
class TagView(PermissionRequiredMixin, View): class TagView(PermissionRequiredMixin, View):
@ -111,7 +111,7 @@ class ConfigContextListView(PermissionRequiredMixin, ObjectListView):
filterset = filters.ConfigContextFilterSet filterset = filters.ConfigContextFilterSet
filterset_form = forms.ConfigContextFilterForm filterset_form = forms.ConfigContextFilterForm
table = ConfigContextTable table = ConfigContextTable
template_name = 'extras/configcontext_list.html' action_buttons = ('add',)
class ConfigContextView(PermissionRequiredMixin, View): class ConfigContextView(PermissionRequiredMixin, View):
@ -191,6 +191,7 @@ class ObjectChangeListView(PermissionRequiredMixin, ObjectListView):
filterset_form = forms.ObjectChangeFilterForm filterset_form = forms.ObjectChangeFilterForm
table = ObjectChangeTable table = ObjectChangeTable
template_name = 'extras/objectchange_list.html' template_name = 'extras/objectchange_list.html'
action_buttons = ('export',)
class ObjectChangeView(PermissionRequiredMixin, View): class ObjectChangeView(PermissionRequiredMixin, View):

View File

@ -1,6 +1,7 @@
from django.conf import settings from django.conf import settings
from django.db.models import Count from django.db.models import Count
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django_pglocks import advisory_lock
from rest_framework import status from rest_framework import status
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.exceptions import PermissionDenied from rest_framework.exceptions import PermissionDenied
@ -10,6 +11,7 @@ from extras.api.views import CustomFieldModelViewSet
from ipam import filters from ipam import filters
from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
from utilities.api import FieldChoicesViewSet, ModelViewSet from utilities.api import FieldChoicesViewSet, ModelViewSet
from utilities.constants import ADVISORY_LOCK_KEYS
from utilities.utils import get_subquery from utilities.utils import get_subquery
from . import serializers from . import serializers
@ -86,9 +88,13 @@ class PrefixViewSet(CustomFieldModelViewSet):
filterset_class = filters.PrefixFilterSet filterset_class = filters.PrefixFilterSet
@action(detail=True, url_path='available-prefixes', methods=['get', 'post']) @action(detail=True, url_path='available-prefixes', methods=['get', 'post'])
@advisory_lock(ADVISORY_LOCK_KEYS['available-prefixes'])
def available_prefixes(self, request, pk=None): def available_prefixes(self, request, pk=None):
""" """
A convenience method for returning available child prefixes within a parent. A convenience method for returning available child prefixes within a parent.
The advisory lock decorator uses a PostgreSQL advisory lock to prevent this API from being
invoked in parallel, which results in a race condition where multiple insertions can occur.
""" """
prefix = get_object_or_404(Prefix, pk=pk) prefix = get_object_or_404(Prefix, pk=pk)
available_prefixes = prefix.get_available_prefixes() available_prefixes = prefix.get_available_prefixes()
@ -180,11 +186,15 @@ class PrefixViewSet(CustomFieldModelViewSet):
return Response(serializer.data) return Response(serializer.data)
@action(detail=True, url_path='available-ips', methods=['get', 'post']) @action(detail=True, url_path='available-ips', methods=['get', 'post'])
@advisory_lock(ADVISORY_LOCK_KEYS['available-ips'])
def available_ips(self, request, pk=None): def available_ips(self, request, pk=None):
""" """
A convenience method for returning available IP addresses within a prefix. By default, the number of IPs A convenience method for returning available IP addresses within a prefix. By default, the number of IPs
returned will be equivalent to PAGINATE_COUNT. An arbitrary limit (up to MAX_PAGE_SIZE, if set) may be passed, returned will be equivalent to PAGINATE_COUNT. An arbitrary limit (up to MAX_PAGE_SIZE, if set) may be passed,
however results will not be paginated. however results will not be paginated.
The advisory lock decorator uses a PostgreSQL advisory lock to prevent this API from being
invoked in parallel, which results in a race condition where multiple insertions can occur.
""" """
prefix = get_object_or_404(Prefix, pk=pk) prefix = get_object_or_404(Prefix, pk=pk)

View File

@ -118,7 +118,6 @@ class VRFListView(PermissionRequiredMixin, ObjectListView):
filterset = filters.VRFFilterSet filterset = filters.VRFFilterSet
filterset_form = forms.VRFFilterForm filterset_form = forms.VRFFilterForm
table = tables.VRFTable table = tables.VRFTable
template_name = 'ipam/vrf_list.html'
class VRFView(PermissionRequiredMixin, View): class VRFView(PermissionRequiredMixin, View):
@ -293,7 +292,6 @@ class AggregateListView(PermissionRequiredMixin, ObjectListView):
queryset = Aggregate.objects.prefetch_related('rir').annotate( queryset = Aggregate.objects.prefetch_related('rir').annotate(
child_count=RawSQL('SELECT COUNT(*) FROM ipam_prefix WHERE ipam_prefix.prefix <<= ipam_aggregate.prefix', ()) child_count=RawSQL('SELECT COUNT(*) FROM ipam_prefix WHERE ipam_prefix.prefix <<= ipam_aggregate.prefix', ())
) )
filterset = filters.AggregateFilterSet filterset = filters.AggregateFilterSet
filterset_form = forms.AggregateFilterForm filterset_form = forms.AggregateFilterForm
table = tables.AggregateDetailTable table = tables.AggregateDetailTable
@ -411,7 +409,6 @@ class RoleListView(PermissionRequiredMixin, ObjectListView):
permission_required = 'ipam.view_role' permission_required = 'ipam.view_role'
queryset = Role.objects.all() queryset = Role.objects.all()
table = tables.RoleTable table = tables.RoleTable
template_name = 'ipam/role_list.html'
class RoleCreateView(PermissionRequiredMixin, ObjectEditView): class RoleCreateView(PermissionRequiredMixin, ObjectEditView):
@ -644,7 +641,6 @@ class IPAddressListView(PermissionRequiredMixin, ObjectListView):
filterset = filters.IPAddressFilterSet filterset = filters.IPAddressFilterSet
filterset_form = forms.IPAddressFilterForm filterset_form = forms.IPAddressFilterForm
table = tables.IPAddressDetailTable table = tables.IPAddressDetailTable
template_name = 'ipam/ipaddress_list.html'
class IPAddressView(PermissionRequiredMixin, View): class IPAddressView(PermissionRequiredMixin, View):
@ -817,7 +813,6 @@ class VLANGroupListView(PermissionRequiredMixin, ObjectListView):
filterset = filters.VLANGroupFilterSet filterset = filters.VLANGroupFilterSet
filterset_form = forms.VLANGroupFilterForm filterset_form = forms.VLANGroupFilterForm
table = tables.VLANGroupTable table = tables.VLANGroupTable
template_name = 'ipam/vlangroup_list.html'
class VLANGroupCreateView(PermissionRequiredMixin, ObjectEditView): class VLANGroupCreateView(PermissionRequiredMixin, ObjectEditView):
@ -893,7 +888,6 @@ class VLANListView(PermissionRequiredMixin, ObjectListView):
filterset = filters.VLANFilterSet filterset = filters.VLANFilterSet
filterset_form = forms.VLANFilterForm filterset_form = forms.VLANFilterForm
table = tables.VLANDetailTable table = tables.VLANDetailTable
template_name = 'ipam/vlan_list.html'
class VLANView(PermissionRequiredMixin, View): class VLANView(PermissionRequiredMixin, View):
@ -989,7 +983,7 @@ class ServiceListView(PermissionRequiredMixin, ObjectListView):
filterset = filters.ServiceFilterSet filterset = filters.ServiceFilterSet
filterset_form = forms.ServiceFilterForm filterset_form = forms.ServiceFilterForm
table = tables.ServiceTable table = tables.ServiceTable
template_name = 'ipam/service_list.html' action_buttons = ('export',)
class ServiceView(PermissionRequiredMixin, View): class ServiceView(PermissionRequiredMixin, View):

View File

@ -12,7 +12,7 @@ from django.core.exceptions import ImproperlyConfigured
# Environment setup # Environment setup
# #
VERSION = '2.7.6-dev' VERSION = '2.7.7-dev'
# Hostname # Hostname
HOSTNAME = platform.node() HOSTNAME = platform.node()

View File

@ -190,15 +190,18 @@ $(document).ready(function() {
$.each(element.attributes, function(index, attr){ $.each(element.attributes, function(index, attr){
if (attr.name.includes("data-additional-query-param-")){ if (attr.name.includes("data-additional-query-param-")){
var param_name = attr.name.split("data-additional-query-param-")[1]; var param_name = attr.name.split("data-additional-query-param-")[1];
if (param_name in parameters) {
if (Array.isArray(parameters[param_name])) { $.each($.parseJSON(attr.value), function(index, value) {
parameters[param_name].push(attr.value) if (param_name in parameters) {
if (Array.isArray(parameters[param_name])) {
parameters[param_name].push(value);
} else {
parameters[param_name] = [parameters[param_name], value];
}
} else { } else {
parameters[param_name] = [parameters[param_name], attr.value] parameters[param_name] = value;
} }
} else { });
parameters[param_name] = attr.value;
}
} }
}); });

View File

@ -35,7 +35,6 @@ class SecretRoleListView(PermissionRequiredMixin, ObjectListView):
permission_required = 'secrets.view_secretrole' permission_required = 'secrets.view_secretrole'
queryset = SecretRole.objects.annotate(secret_count=Count('secrets')) queryset = SecretRole.objects.annotate(secret_count=Count('secrets'))
table = tables.SecretRoleTable table = tables.SecretRoleTable
template_name = 'secrets/secretrole_list.html'
class SecretRoleCreateView(PermissionRequiredMixin, ObjectEditView): class SecretRoleCreateView(PermissionRequiredMixin, ObjectEditView):
@ -73,7 +72,7 @@ class SecretListView(PermissionRequiredMixin, ObjectListView):
filterset = filters.SecretFilterSet filterset = filters.SecretFilterSet
filterset_form = forms.SecretFilterForm filterset_form = forms.SecretFilterForm
table = tables.SecretTable table = tables.SecretTable
template_name = 'secrets/secret_list.html' action_buttons = ('import', 'export')
class SecretView(PermissionRequiredMixin, View): class SecretView(PermissionRequiredMixin, View):

View File

@ -1,21 +0,0 @@
{% extends '_base.html' %}
{% load buttons %}
{% block content %}
<div class="pull-right noprint">
{% if perms.circuits.add_circuit %}
{% add_button 'circuits:circuit_add' %}
{% import_button 'circuits:circuit_import' %}
{% endif %}
{% export_button content_type %}
</div>
<h1>{% block title %}Circuits{% endblock %}</h1>
<div class="row">
<div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_edit_url='circuits:circuit_bulk_edit' bulk_delete_url='circuits:circuit_bulk_delete' %}
</div>
<div class="col-md-3 noprint">
{% include 'inc/search_panel.html' %}
</div>
</div>
{% endblock %}

View File

@ -1,18 +0,0 @@
{% extends '_base.html' %}
{% load buttons %}
{% block content %}
<div class="pull-right noprint">
{% if perms.circuits.add_circuittype %}
{% add_button 'circuits:circuittype_add' %}
{% import_button 'circuits:circuittype_import' %}
{% endif %}
{% export_button content_type %}
</div>
<h1>{% block title %}Circuit Types{% endblock %}</h1>
<div class="row">
<div class="col-md-12">
{% include 'utilities/obj_table.html' with bulk_delete_url='circuits:circuittype_bulk_delete' %}
</div>
</div>
{% endblock %}

View File

@ -1,21 +0,0 @@
{% extends '_base.html' %}
{% load buttons %}
{% block content %}
<div class="pull-right noprint">
{% if perms.circuits.add_provider %}
{% add_button 'circuits:provider_add' %}
{% import_button 'circuits:provider_import' %}
{% endif %}
{% export_button content_type %}
</div>
<h1>{% block title %}Providers{% endblock %}</h1>
<div class="row">
<div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_edit_url='circuits:provider_bulk_edit' bulk_delete_url='circuits:provider_bulk_delete' %}
</div>
<div class="col-md-3 noprint">
{% include 'inc/search_panel.html' %}
</div>
</div>
{% endblock %}

View File

@ -1,20 +0,0 @@
{% extends '_base.html' %}
{% load buttons %}
{% block content %}
<div class="pull-right noprint">
{% if perms.dcim.add_cable %}
{% import_button 'dcim:cable_import' %}
{% endif %}
{% export_button content_type %}
</div>
<h1>{% block title %}Cables{% endblock %}</h1>
<div class="row">
<div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_edit_url='dcim:cable_bulk_edit' bulk_delete_url='dcim:cable_bulk_delete' %}
</div>
<div class="col-md-3 noprint">
{% include 'inc/search_panel.html' %}
</div>
</div>
{% endblock %}

View File

@ -1,17 +0,0 @@
{% extends '_base.html' %}
{% load buttons %}
{% block content %}
<div class="pull-right noprint">
{% export_button content_type %}
</div>
<h1>{% block title %}Console Ports{% endblock %}</h1>
<div class="row">
<div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_edit_url='dcim:consoleport_bulk_edit' bulk_delete_url='dcim:consoleport_bulk_delete' %}
</div>
<div class="col-md-3 noprint">
{% include 'inc/search_panel.html' %}
</div>
</div>
{% endblock %}

View File

@ -1,17 +0,0 @@
{% extends '_base.html' %}
{% load buttons %}
{% block content %}
<div class="pull-right noprint">
{% export_button content_type %}
</div>
<h1>{% block title %}Console Server Ports{% endblock %}</h1>
<div class="row">
<div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_edit_url='dcim:consoleserverport_bulk_edit' bulk_delete_url='dcim:consoleserverport_bulk_delete' %}
</div>
<div class="col-md-3 noprint">
{% include 'inc/search_panel.html' %}
</div>
</div>
{% endblock %}

View File

@ -1,21 +1,24 @@
{% extends '_base.html' %} {% extends 'utilities/obj_list.html' %}
{% load buttons %}
{% block content %} {% block bulk_buttons %}
<div class="pull-right noprint"> {% if perms.dcim.change_device %}
{% if perms.dcim.add_device %} <div class="btn-group">
{% add_button 'dcim:device_add' %} <button type="button" class="btn btn-sm btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{% import_button 'dcim:device_import' %} <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add Components <span class="caret"></span>
</button>
<ul class="dropdown-menu">
{% if perms.dcim.add_consoleport %}<li><a href="{% url 'dcim:device_bulk_add_consoleport' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="formaction">Console Ports</a></li>{% endif %}
{% if perms.dcim.add_consoleserverport %}<li><a href="{% url 'dcim:device_bulk_add_consoleserverport' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="formaction">Console Server Ports</a></li>{% endif %}
{% if perms.dcim.add_powerport %}<li><a href="{% url 'dcim:device_bulk_add_powerport' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="formaction">Power Ports</a></li>{% endif %}
{% if perms.dcim.add_poweroutlet %}<li><a href="{% url 'dcim:device_bulk_add_poweroutlet' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="formaction">Power Outlets</a></li>{% endif %}
{% if perms.dcim.add_interface %}<li><a href="{% url 'dcim:device_bulk_add_interface' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="formaction">Interfaces</a></li>{% endif %}
{% if perms.dcim.add_devicebay %}<li><a href="{% url 'dcim:device_bulk_add_devicebay' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="formaction">Device Bays</a></li>{% endif %}
</ul>
</div>
{% endif %}
{% if perms.dcim.add_virtualchassis %}
<button type="submit" name="_edit" formaction="{% url 'dcim:virtualchassis_add' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-primary btn-sm">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Create Virtual Chassis
</button>
{% endif %} {% endif %}
{% export_button content_type %}
</div>
<h1>{% block title %}Devices{% endblock %}</h1>
<div class="row">
<div class="col-md-9">
{% include 'dcim/inc/device_table.html' with bulk_edit_url='dcim:device_bulk_edit' bulk_delete_url='dcim:device_bulk_delete' %}
</div>
<div class="col-md-3 noprint">
{% include 'inc/search_panel.html' %}
</div>
</div>
{% endblock %} {% endblock %}

View File

@ -1,17 +0,0 @@
{% extends '_base.html' %}
{% load buttons %}
{% block content %}
<div class="pull-right noprint">
{% export_button content_type %}
</div>
<h1>{% block title %}Device Bays{% endblock %}</h1>
<div class="row">
<div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_delete_url='dcim:devicebay_bulk_delete' %}
</div>
<div class="col-md-3 noprint">
{% include 'inc/search_panel.html' %}
</div>
</div>
{% endblock %}

View File

@ -1,18 +0,0 @@
{% extends '_base.html' %}
{% load buttons %}
{% block content %}
<div class="pull-right noprint">
{% if perms.dcim.add_devicerole %}
{% add_button 'dcim:devicerole_add' %}
{% import_button 'dcim:devicerole_import' %}
{% endif %}
{% export_button content_type %}
</div>
<h1>{% block title %}Device Roles{% endblock %}</h1>
<div class="row">
<div class="col-md-12">
{% include 'utilities/obj_table.html' with bulk_delete_url='dcim:devicerole_bulk_delete' %}
</div>
</div>
{% endblock %}

View File

@ -1,21 +0,0 @@
{% extends '_base.html' %}
{% load buttons %}
{% block content %}
<div class="pull-right noprint">
{% if perms.dcim.add_devicetype %}
{% add_button 'dcim:devicetype_add' %}
{% import_button 'dcim:devicetype_import' %}
{% endif %}
{% export_button content_type %}
</div>
<h1>{% block title %}Device Types{% endblock %}</h1>
<div class="row">
<div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_edit_url='dcim:devicetype_bulk_edit' bulk_delete_url='dcim:devicetype_bulk_delete' %}
</div>
<div class="col-md-3 noprint">
{% include 'inc/search_panel.html' %}
</div>
</div>
{% endblock %}

View File

@ -1,17 +0,0 @@
{% extends '_base.html' %}
{% load buttons %}
{% block content %}
<div class="pull-right noprint">
{% export_button content_type %}
</div>
<h1>{% block title %}Front Ports{% endblock %}</h1>
<div class="row">
<div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_edit_url='dcim:frontport_bulk_edit' bulk_delete_url='dcim:frontport_bulk_delete' %}
</div>
<div class="col-md-3 noprint">
{% include 'inc/search_panel.html' %}
</div>
</div>
{% endblock %}

View File

@ -1,24 +0,0 @@
{% extends 'utilities/obj_table.html' %}
{% block extra_actions %}
{% if perms.dcim.change_device %}
<div class="btn-group">
<button type="button" class="btn btn-sm btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add Components <span class="caret"></span>
</button>
<ul class="dropdown-menu">
{% if perms.dcim.add_consoleport %}<li><a href="{% url 'dcim:device_bulk_add_consoleport' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="formaction">Console Ports</a></li>{% endif %}
{% if perms.dcim.add_consoleserverport %}<li><a href="{% url 'dcim:device_bulk_add_consoleserverport' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="formaction">Console Server Ports</a></li>{% endif %}
{% if perms.dcim.add_powerport %}<li><a href="{% url 'dcim:device_bulk_add_powerport' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="formaction">Power Ports</a></li>{% endif %}
{% if perms.dcim.add_poweroutlet %}<li><a href="{% url 'dcim:device_bulk_add_poweroutlet' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="formaction">Power Outlets</a></li>{% endif %}
{% if perms.dcim.add_interface %}<li><a href="{% url 'dcim:device_bulk_add_interface' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="formaction">Interfaces</a></li>{% endif %}
{% if perms.dcim.add_devicebay %}<li><a href="{% url 'dcim:device_bulk_add_devicebay' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="formaction">Device Bays</a></li>{% endif %}
</ul>
</div>
{% endif %}
{% if perms.dcim.add_virtualchassis %}
<button type="submit" name="_edit" formaction="{% url 'dcim:virtualchassis_add' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-primary btn-sm">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Create Virtual Chassis
</button>
{% endif %}
{% endblock %}

View File

@ -1,17 +0,0 @@
{% extends '_base.html' %}
{% load buttons %}
{% block content %}
<div class="pull-right noprint">
{% export_button content_type %}
</div>
<h1>{% block title %}Interfaces{% endblock %}</h1>
<div class="row">
<div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_edit_url='dcim:interface_bulk_edit' bulk_delete_url='dcim:interface_bulk_delete' %}
</div>
<div class="col-md-3 noprint">
{% include 'inc/search_panel.html' %}
</div>
</div>
{% endblock %}

View File

@ -1,21 +0,0 @@
{% extends '_base.html' %}
{% load buttons %}
{% load helpers %}
{% block content %}
<div class="pull-right noprint">
{% if perms.dcim.add_devicetype %}
{% import_button 'dcim:inventoryitem_import' %}
{% endif %}
{% export_button content_type %}
</div>
<h1>{% block title %}Inventory Items{% endblock %}</h1>
<div class="row">
<div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_edit_url='dcim:inventoryitem_bulk_edit' bulk_delete_url='dcim:inventoryitem_bulk_delete' %}
</div>
<div class="col-md-3 noprint">
{% include 'inc/search_panel.html' %}
</div>
</div>
{% endblock %}

View File

@ -1,18 +0,0 @@
{% extends '_base.html' %}
{% load buttons %}
{% block content %}
<div class="pull-right noprint">
{% if perms.dcim.add_manufacturer %}
{% add_button 'dcim:manufacturer_add' %}
{% import_button 'dcim:manufacturer_import' %}
{% endif %}
{% export_button content_type %}
</div>
<h1>{% block title %}Manufacturers{% endblock %}</h1>
<div class="row">
<div class="col-md-12">
{% include 'utilities/obj_table.html' with bulk_delete_url='dcim:manufacturer_bulk_delete' %}
</div>
</div>
{% endblock %}

View File

@ -1,18 +0,0 @@
{% extends '_base.html' %}
{% load buttons %}
{% block content %}
<div class="pull-right noprint">
{% if perms.dcim.add_platform %}
{% add_button 'dcim:platform_add' %}
{% import_button 'dcim:platform_import' %}
{% endif %}
{% export_button content_type %}
</div>
<h1>{% block title %}Platforms{% endblock %}</h1>
<div class="row">
<div class="col-md-12">
{% include 'utilities/obj_table.html' with bulk_delete_url='dcim:platform_bulk_delete' %}
</div>
</div>
{% endblock %}

View File

@ -1,21 +0,0 @@
{% extends '_base.html' %}
{% load buttons %}
{% block content %}
<div class="pull-right noprint">
{% if perms.dcim.add_powerfeed %}
{% add_button 'dcim:powerfeed_add' %}
{% import_button 'dcim:powerfeed_import' %}
{% endif %}
{% export_button content_type %}
</div>
<h1>{% block title %}Power Feeds{% endblock %}</h1>
<div class="row">
<div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_edit_url='dcim:powerfeed_bulk_edit' bulk_delete_url='dcim:powerfeed_bulk_delete' %}
</div>
<div class="col-md-3 noprint">
{% include 'inc/search_panel.html' %}
</div>
</div>
{% endblock %}

View File

@ -1,17 +0,0 @@
{% extends '_base.html' %}
{% load buttons %}
{% block content %}
<div class="pull-right noprint">
{% export_button content_type %}
</div>
<h1>{% block title %}Power Outlets{% endblock %}</h1>
<div class="row">
<div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_edit_url='dcim:poweroutlet_bulk_edit' bulk_delete_url='dcim:poweroutlet_bulk_delete' %}
</div>
<div class="col-md-3 noprint">
{% include 'inc/search_panel.html' %}
</div>
</div>
{% endblock %}

View File

@ -1,21 +0,0 @@
{% extends '_base.html' %}
{% load buttons %}
{% block content %}
<div class="pull-right noprint">
{% if perms.dcim.add_powerpanel %}
{% add_button 'dcim:powerpanel_add' %}
{% import_button 'dcim:powerpanel_import' %}
{% endif %}
{% export_button content_type %}
</div>
<h1>{% block title %}Power Panels{% endblock %}</h1>
<div class="row">
<div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_delete_url='dcim:powerpanel_bulk_delete' %}
</div>
<div class="col-md-3">
{% include 'inc/search_panel.html' %}
</div>
</div>
{% endblock %}

View File

@ -1,17 +0,0 @@
{% extends '_base.html' %}
{% load buttons %}
{% block content %}
<div class="pull-right noprint">
{% export_button content_type %}
</div>
<h1>{% block title %}Power Ports{% endblock %}</h1>
<div class="row">
<div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_edit_url='dcim:powerport_bulk_edit' bulk_delete_url='dcim:powerport_bulk_delete' %}
</div>
<div class="col-md-3 noprint">
{% include 'inc/search_panel.html' %}
</div>
</div>
{% endblock %}

View File

@ -1,21 +0,0 @@
{% extends '_base.html' %}
{% load buttons %}
{% block content %}
<div class="pull-right noprint">
{% if perms.dcim.add_rack %}
{% add_button 'dcim:rack_add' %}
{% import_button 'dcim:rack_import' %}
{% endif %}
{% export_button content_type %}
</div>
<h1>{% block title %}Racks{% endblock %}</h1>
<div class="row">
<div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_edit_url='dcim:rack_bulk_edit' bulk_delete_url='dcim:rack_bulk_delete' %}
</div>
<div class="col-md-3 noprint">
{% include 'inc/search_panel.html' %}
</div>
</div>
{% endblock %}

View File

@ -1,21 +0,0 @@
{% extends '_base.html' %}
{% load buttons %}
{% block content %}
<div class="pull-right noprint">
{% if perms.dcim.add_rackgroup %}
{% add_button 'dcim:rackgroup_add' %}
{% import_button 'dcim:rackgroup_import' %}
{% endif %}
{% export_button content_type %}
</div>
<h1>{% block title %}Rack Groups{% endblock %}</h1>
<div class="row">
<div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_delete_url='dcim:rackgroup_bulk_delete' %}
</div>
<div class="col-md-3 noprint">
{% include 'inc/search_panel.html' %}
</div>
</div>
{% endblock %}

View File

@ -1,14 +0,0 @@
{% extends '_base.html' %}
{% load helpers %}
{% block content %}
<h1>{% block title %}Rack Reservations{% endblock %}</h1>
<div class="row">
<div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_edit_url='dcim:rackreservation_bulk_edit' bulk_delete_url='dcim:rackreservation_bulk_delete' %}
</div>
<div class="col-md-3 noprint">
{% include 'inc/search_panel.html' %}
</div>
</div>
{% endblock %}

View File

@ -1,18 +0,0 @@
{% extends '_base.html' %}
{% load buttons %}
{% block content %}
<div class="pull-right noprint">
{% if perms.dcim.add_rackrole %}
{% add_button 'dcim:rackrole_add' %}
{% import_button 'dcim:rackrole_import' %}
{% endif %}
{% export_button content_type %}
</div>
<h1>{% block title %}Rack Roles{% endblock %}</h1>
<div class="row">
<div class="col-md-12">
{% include 'utilities/obj_table.html' with bulk_delete_url='dcim:rackrole_bulk_delete' %}
</div>
</div>
{% endblock %}

View File

@ -1,17 +0,0 @@
{% extends '_base.html' %}
{% load buttons %}
{% block content %}
<div class="pull-right noprint">
{% export_button content_type %}
</div>
<h1>{% block title %}Rear Ports{% endblock %}</h1>
<div class="row">
<div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_edit_url='dcim:rearport_bulk_edit' bulk_delete_url='dcim:rearport_bulk_delete' %}
</div>
<div class="col-md-3 noprint">
{% include 'inc/search_panel.html' %}
</div>
</div>
{% endblock %}

View File

@ -1,21 +0,0 @@
{% extends '_base.html' %}
{% load buttons %}
{% block content %}
<div class="pull-right noprint">
{% if perms.dcim.add_region %}
{% add_button 'dcim:region_add' %}
{% import_button 'dcim:region_import' %}
{% endif %}
{% export_button content_type %}
</div>
<h1>{% block title %}Regions{% endblock %}</h1>
<div class="row">
<div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_delete_url='dcim:region_bulk_delete' %}
</div>
<div class="col-md-3 noprint">
{% include 'inc/search_panel.html' %}
</div>
</div>
{% endblock %}

View File

@ -1,21 +0,0 @@
{% extends '_base.html' %}
{% load buttons %}
{% block content %}
<div class="pull-right noprint">
{% if perms.dcim.add_site %}
{% add_button 'dcim:site_add' %}
{% import_button 'dcim:site_import' %}
{% endif %}
{% export_button content_type %}
</div>
<h1>{% block title %}Sites{% endblock %}</h1>
<div class="row">
<div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_edit_url='dcim:site_bulk_edit' bulk_delete_url='dcim:site_bulk_delete' %}
</div>
<div class="col-md-3 noprint">
{% include 'inc/search_panel.html' %}
</div>
</div>
{% endblock %}

View File

@ -1,18 +0,0 @@
{% extends '_base.html' %}
{% load buttons %}
{% load helpers %}
{% block content %}
<div class="pull-right noprint">
{% export_button content_type %}
</div>
<h1>{% block title %}Virtual Chassis{% endblock %}</h1>
<div class="row">
<div class="col-md-9">
{% include 'utilities/obj_table.html' %}
</div>
<div class="col-md-3 noprint">
{% include 'inc/search_panel.html' %}
</div>
</div>
{% endblock %}

View File

@ -1,19 +0,0 @@
{% extends '_base.html' %}
{% load buttons %}
{% block content %}
<div class="pull-right noprint">
{% if perms.extras.add_configcontext %}
{% add_button 'extras:configcontext_add' %}
{% endif %}
</div>
<h1>{% block title %}Config Contexts{% endblock %}</h1>
<div class="row">
<div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_edit_url='extras:configcontext_bulk_edit' bulk_delete_url='extras:configcontext_bulk_delete' %}
</div>
<div class="col-md-3 noprint">
{% include 'inc/search_panel.html' %}
</div>
</div>
{% endblock %}

View File

@ -1,20 +1,9 @@
{% extends '_base.html' %} {% extends 'utilities/obj_list.html' %}
{% load buttons %}
{% block content %} {% block title %}Change Log{% endblock %}
<div class="pull-right noprint">
{% export_button content_type %} {% block sidebar %}
</div> <div class="text-muted">
<h1>{% block title %}Changelog{% endblock %}</h1> Change log retention: {% if settings.CHANGELOG_RETENTION %}{{ settings.CHANGELOG_RETENTION }} days{% else %}Indefinite{% endif %}
<div class="row">
<div class="col-md-9">
{% include 'utilities/obj_table.html' %}
<div class="text-muted text-right">
Changelog retention: {% if settings.CHANGELOG_RETENTION %}{{ settings.CHANGELOG_RETENTION }} days{% else %}Indefinite{% endif %}
</div>
</div> </div>
<div class="col-md-3 noprint">
{% include 'inc/search_panel.html' %}
</div>
</div>
{% endblock %} {% endblock %}

View File

@ -1,14 +0,0 @@
{% extends '_base.html' %}
{% load buttons %}
{% block content %}
<h1>{% block title %}Tags{% endblock %}</h1>
<div class="row">
<div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_edit_url='extras:tag_bulk_edit' bulk_delete_url='extras:tag_bulk_delete' %}
</div>
<div class="col-md-3">
{% include 'inc/search_panel.html' %}
</div>
</div>
{% endblock %}

View File

@ -473,7 +473,7 @@
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li class="dropdown-header">Logging</li> <li class="dropdown-header">Logging</li>
<li{% if not perms.extras.view_objectchange %} class="disabled"{% endif %}> <li{% if not perms.extras.view_objectchange %} class="disabled"{% endif %}>
<a href="{% url 'extras:objectchange_list' %}">Changelog</a> <a href="{% url 'extras:objectchange_list' %}">Change Log</a>
</li> </li>
<li class="divider"></li> <li class="divider"></li>
<li class="dropdown-header">Miscellaneous</li> <li class="dropdown-header">Miscellaneous</li>

View File

@ -1,31 +1,14 @@
{% extends '_base.html' %} {% extends 'utilities/obj_list.html' %}
{% load buttons %}
{% load humanize %} {% load humanize %}
{% block content %} {% block sidebar %}
<div class="pull-right noprint"> <div class="panel panel-default">
{% if perms.ipam.add_aggregate %} <div class="panel-heading">
{% add_button 'ipam:aggregate_add' %} <strong><i class="fa fa-bar-chart"></i> Statistics</strong>
{% import_button 'ipam:aggregate_import' %}
{% endif %}
{% export_button content_type %}
</div>
<h1>{% block title %}Aggregates{% endblock %}</h1>
<div class="row">
<div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_edit_url='ipam:aggregate_bulk_edit' bulk_delete_url='ipam:aggregate_bulk_delete' %}
</div>
<div class="col-md-3 noprint">
{% include 'inc/search_panel.html' %}
<div class="panel panel-default">
<div class="panel-heading">
<strong><i class="fa fa-bar-chart"></i> Statistics</strong>
</div>
<ul class="list-group">
<li class="list-group-item">Total IPv4 IPs <span class="badge">{{ ipv4_total|intcomma }}</span></li>
<li class="list-group-item">Total IPv6 /64s <span class="badge">{{ ipv6_total|intcomma }}</span></li>
</ul>
</div> </div>
</div> <ul class="list-group">
</div> <li class="list-group-item">Total IPv4 IPs <span class="badge">{{ ipv4_total|intcomma }}</span></li>
<li class="list-group-item">Total IPv6 /64s <span class="badge">{{ ipv6_total|intcomma }}</span></li>
</ul>
</div>
{% endblock %} {% endblock %}

View File

@ -1,21 +0,0 @@
{% extends '_base.html' %}
{% load buttons %}
{% block content %}
<div class="pull-right noprint">
{% if perms.ipam.add_ipaddress %}
{% add_button 'ipam:ipaddress_add' %}
{% import_button 'ipam:ipaddress_import' %}
{% endif %}
{% export_button content_type %}
</div>
<h1>{% block title %}IP Addresses{% endblock %}</h1>
<div class="row">
<div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_edit_url='ipam:ipaddress_bulk_edit' bulk_delete_url='ipam:ipaddress_bulk_delete' %}
</div>
<div class="col-md-3 noprint">
{% include 'inc/search_panel.html' %}
</div>
</div>
{% endblock %}

View File

@ -1,26 +1,9 @@
{% extends '_base.html' %} {% extends 'utilities/obj_list.html' %}
{% load buttons %}
{% load helpers %} {% load helpers %}
{% block content %} {% block buttons %}
<div class="pull-right noprint">
<div class="btn-group" role="group"> <div class="btn-group" role="group">
<a href="{% url 'ipam:prefix_list' %}{% querystring request expand=None page=1 %}" class="btn btn-default{% if not request.GET.expand %} active{% endif %}">Collapse</a> <a href="{% url 'ipam:prefix_list' %}{% querystring request expand=None page=1 %}" class="btn btn-default{% if not request.GET.expand %} active{% endif %}">Collapse</a>
<a href="{% url 'ipam:prefix_list' %}{% querystring request expand='on' page=1 %}" class="btn btn-default{% if request.GET.expand %} active{% endif %}">Expand</a> <a href="{% url 'ipam:prefix_list' %}{% querystring request expand='on' page=1 %}" class="btn btn-default{% if request.GET.expand %} active{% endif %}">Expand</a>
</div> </div>
{% if perms.ipam.add_prefix %}
{% add_button 'ipam:prefix_add' %}
{% import_button 'ipam:prefix_import' %}
{% endif %}
{% export_button content_type %}
</div>
<h1>{% block title %}Prefixes{% endblock %}</h1>
<div class="row">
<div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_edit_url='ipam:prefix_bulk_edit' bulk_delete_url='ipam:prefix_bulk_delete' %}
</div>
<div class="col-md-3 noprint">
{% include 'inc/search_panel.html' %}
</div>
</div>
{% endblock %} {% endblock %}

View File

@ -1,9 +1,6 @@
{% extends '_base.html' %} {% extends 'utilities/obj_list.html' %}
{% load buttons %}
{% load humanize %}
{% block content %} {% block buttons %}
<div class="pull-right noprint">
{% if request.GET.family == '6' %} {% if request.GET.family == '6' %}
<a href="{% url 'ipam:rir_list' %}" class="btn btn-default"> <a href="{% url 'ipam:rir_list' %}" class="btn btn-default">
<span class="fa fa-table" aria-hidden="true"></span> <span class="fa fa-table" aria-hidden="true"></span>
@ -15,22 +12,12 @@
IPv6 Stats IPv6 Stats
</a> </a>
{% endif %} {% endif %}
{% if perms.ipam.add_rir %} {% endblock %}
{% add_button 'ipam:rir_add' %}
{% import_button 'ipam:rir_import' %} {% block sidebar %}
{% endif %} {% if request.GET.family == '6' %}
{% export_button content_type %} <div class="alert alert-info">
</div> <i class="fa fa-info-circle"></i> Numbers shown indicate /64 prefixes.
<h1>{% block title %}RIRs{% endblock %}</h1> </div>
<div class="row"> {% endif %}
<div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_delete_url='ipam:rir_bulk_delete' %}
{% if request.GET.family == '6' %}
<div class="alert alert-info pull-right"><strong>Note:</strong> Numbers shown indicate /64 prefixes.</div>
{% endif %}
</div>
<div class="col-md-3 noprint">
{% include 'inc/search_panel.html' %}
</div>
</div>
{% endblock %} {% endblock %}

View File

@ -1,18 +0,0 @@
{% extends '_base.html' %}
{% load buttons %}
{% block content %}
<div class="pull-right noprint">
{% if perms.ipam.add_role %}
{% add_button 'ipam:role_add' %}
{% import_button 'ipam:role_import' %}
{% endif %}
{% export_button content_type %}
</div>
<h1>{% block title %}Prefix/VLAN Roles{% endblock %}</h1>
<div class="row">
<div class="col-md-12">
{% include 'utilities/obj_table.html' with bulk_delete_url='ipam:role_bulk_delete' %}
</div>
</div>
{% endblock %}

View File

@ -1,17 +0,0 @@
{% extends '_base.html' %}
{% load buttons %}
{% block content %}
<div class="pull-right noprint">
{% export_button content_type %}
</div>
<h1>{% block title %}Services{% endblock %}</h1>
<div class="row">
<div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_edit_url='ipam:service_bulk_edit' bulk_delete_url='ipam:service_bulk_delete' %}
</div>
<div class="col-md-3 noprint">
{% include 'inc/search_panel.html' %}
</div>
</div>
{% endblock %}

View File

@ -1,21 +0,0 @@
{% extends '_base.html' %}
{% load buttons %}
{% block content %}
<div class="pull-right noprint">
{% if perms.ipam.add_vlan %}
{% add_button 'ipam:vlan_add' %}
{% import_button 'ipam:vlan_import' %}
{% endif %}
{% export_button content_type %}
</div>
<h1>{% block title %}VLANs{% endblock %}</h1>
<div class="row">
<div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_edit_url='ipam:vlan_bulk_edit' bulk_delete_url='ipam:vlan_bulk_delete' %}
</div>
<div class="col-md-3 noprint">
{% include 'inc/search_panel.html' %}
</div>
</div>
{% endblock %}

View File

@ -1,21 +0,0 @@
{% extends '_base.html' %}
{% load buttons %}
{% block content %}
<div class="pull-right noprint">
{% if perms.ipam.add_vlangroup %}
{% add_button 'ipam:vlangroup_add' %}
{% import_button 'ipam:vlangroup_import' %}
{% endif %}
{% export_button content_type %}
</div>
<h1>{% block title %}VLAN Groups{% endblock %}</h1>
<div class="row">
<div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_delete_url='ipam:vlangroup_bulk_delete' %}
</div>
<div class="col-md-3 noprint">
{% include 'inc/search_panel.html' %}
</div>
</div>
{% endblock %}

View File

@ -1,21 +0,0 @@
{% extends '_base.html' %}
{% load buttons %}
{% block content %}
<div class="pull-right noprint">
{% if perms.ipam.add_vrf %}
{% add_button 'ipam:vrf_add' %}
{% import_button 'ipam:vrf_import' %}
{% endif %}
{% export_button content_type %}
</div>
<h1>{% block title %}VRFs{% endblock %}</h1>
<div class="row">
<div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_edit_url='ipam:vrf_bulk_edit' bulk_delete_url='ipam:vrf_bulk_delete' %}
</div>
<div class="col-md-3 noprint">
{% include 'inc/search_panel.html' %}
</div>
</div>
{% endblock %}

View File

@ -1,20 +0,0 @@
{% extends '_base.html' %}
{% load buttons %}
{% block content %}
<div class="pull-right noprint">
{% if perms.secrets.add_secret %}
{% import_button 'secrets:secret_import' %}
{% endif %}
{% export_button content_type %}
</div>
<h1>{% block title %}Secrets{% endblock %}</h1>
<div class="row">
<div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_edit_url='secrets:secret_bulk_edit' bulk_delete_url='secrets:secret_bulk_delete' %}
</div>
<div class="col-md-3 noprint">
{% include 'inc/search_panel.html' %}
</div>
</div>
{% endblock %}

View File

@ -1,18 +0,0 @@
{% extends '_base.html' %}
{% load buttons %}
{% block content %}
<div class="pull-right noprint">
{% if perms.secrets.add_secretrole %}
{% add_button 'secrets:secretrole_add' %}
{% import_button 'secrets:secretrole_import' %}
{% endif %}
{% export_button content_type %}
</div>
<h1>{% block title %}Secret Roles{% endblock %}</h1>
<div class="row">
<div class="col-md-12">
{% include 'utilities/obj_table.html' with bulk_delete_url='secrets:secretrole_bulk_delete' %}
</div>
</div>
{% endblock %}

View File

@ -1,21 +0,0 @@
{% extends '_base.html' %}
{% load buttons %}
{% block content %}
<div class="pull-right noprint">
{% if perms.tenancy.add_tenant %}
{% add_button 'tenancy:tenant_add' %}
{% import_button 'tenancy:tenant_import' %}
{% endif %}
{% export_button content_type %}
</div>
<h1>{% block title %}Tenants{% endblock %}</h1>
<div class="row">
<div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_edit_url='tenancy:tenant_bulk_edit' bulk_delete_url='tenancy:tenant_bulk_delete' %}
</div>
<div class="col-md-3 noprint">
{% include 'inc/search_panel.html' %}
</div>
</div>
{% endblock %}

View File

@ -1,18 +0,0 @@
{% extends '_base.html' %}
{% load buttons %}
{% block content %}
<div class="pull-right noprint">
{% if perms.tenancy.add_tenantgroup %}
{% add_button 'tenancy:tenantgroup_add' %}
{% import_button 'tenancy:tenantgroup_import' %}
{% endif %}
{% export_button content_type %}
</div>
<h1>{% block title %}Tenant Groups{% endblock %}</h1>
<div class="row">
<div class="col-md-12">
{% include 'utilities/obj_table.html' with bulk_delete_url='tenancy:tenantgroup_bulk_delete' %}
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,79 @@
{% extends '_base.html' %}
{% load buttons %}
{% load helpers %}
{% block content %}
<div class="pull-right noprint">
{% block buttons %}{% endblock %}
{% if permissions.add and 'add' in action_buttons %}
{% add_button content_type.model_class|url_name:"add" %}
{% endif %}
{% if permissions.add and 'import' in action_buttons %}
{% import_button content_type.model_class|url_name:"import" %}
{% endif %}
{% if 'export' in action_buttons %}
{% export_button content_type %}
{% endif %}
</div>
<h1>{% block title %}{{ content_type.model_class|model_name_plural|bettertitle }}{% endblock %}</h1>
<div class="row">
<div class="col-md-9">
{% with bulk_edit_url=content_type.model_class|url_name:"bulk_edit" bulk_delete_url=content_type.model_class|url_name:"bulk_delete" %}
{% if permissions.change or permissions.delete %}
<form method="post" class="form form-horizontal">
{% csrf_token %}
<input type="hidden" name="return_url" value="{% if return_url %}{{ return_url }}{% else %}{{ request.path }}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}{% endif %}" />
{% if table.paginator.num_pages > 1 %}
<div id="select_all_box" class="hidden panel panel-default noprint">
<div class="panel-body">
<div class="checkbox-inline">
<label for="select_all">
<input type="checkbox" id="select_all" name="_all" />
Select <strong>all {{ table.rows|length }} {{ table.data.verbose_name_plural }}</strong> matching query
</label>
</div>
<div class="pull-right">
{% if bulk_edit_url and permissions.change %}
<button type="submit" name="_edit" formaction="{% url bulk_edit_url %}{% if bulk_querystring %}?{{ bulk_querystring }}{% elif request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-warning btn-sm" disabled="disabled">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit All
</button>
{% endif %}
{% if bulk_delete_url and permissions.delete %}
<button type="submit" name="_delete" formaction="{% url bulk_delete_url %}{% if bulk_querystring %}?{{ bulk_querystring }}{% elif request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-danger btn-sm" disabled="disabled">
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete All
</button>
{% endif %}
</div>
</div>
</div>
{% endif %}
{% include table_template|default:'responsive_table.html' %}
<div class="pull-left noprint">
{% block bulk_buttons %}{% endblock %}
{% if bulk_edit_url and permissions.change %}
<button type="submit" name="_edit" formaction="{% url bulk_edit_url %}" class="btn btn-warning btn-sm">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit Selected
</button>
{% endif %}
{% if bulk_delete_url and permissions.delete %}
<button type="submit" name="_delete" formaction="{% url bulk_delete_url %}" class="btn btn-danger btn-sm">
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete Selected
</button>
{% endif %}
</div>
</form>
{% else %}
{% include table_template|default:'responsive_table.html' %}
{% endif %}
{% endwith %}
{% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
<div class="clearfix"></div>
</div>
<div class="col-md-3 noprint">
{% if filter_form %}
{% include 'inc/search_panel.html' %}
{% endif %}
{% block sidebar %}{% endblock %}
</div>
</div>
{% endblock %}

View File

@ -1,21 +0,0 @@
{% extends '_base.html' %}
{% load buttons %}
{% block content %}
<div class="pull-right noprint">
{% if perms.virtualization.add_cluster %}
{% add_button 'virtualization:cluster_add' %}
{% import_button 'virtualization:cluster_import' %}
{% endif %}
{% export_button content_type %}
</div>
<h1>{% block title %}Clusters{% endblock %}</h1>
<div class="row">
<div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_edit_url='virtualization:cluster_bulk_edit' bulk_delete_url='virtualization:cluster_bulk_delete' %}
</div>
<div class="col-md-3 noprint">
{% include 'inc/search_panel.html' %}
</div>
</div>
{% endblock %}

View File

@ -1,18 +0,0 @@
{% extends '_base.html' %}
{% load buttons %}
{% block content %}
<div class="pull-right noprint">
{% if perms.virtualization.add_clustergroup %}
{% add_button 'virtualization:clustergroup_add' %}
{% import_button 'virtualization:clustergroup_import' %}
{% endif %}
{% export_button content_type %}
</div>
<h1>{% block title %}Cluster Groups{% endblock %}</h1>
<div class="row">
<div class="col-md-12">
{% include 'utilities/obj_table.html' with bulk_delete_url='virtualization:clustergroup_bulk_delete' %}
</div>
</div>
{% endblock %}

View File

@ -1,18 +0,0 @@
{% extends '_base.html' %}
{% load buttons %}
{% block content %}
<div class="pull-right noprint">
{% if perms.virtualization.add_clustertype %}
{% add_button 'virtualization:clustertype_add' %}
{% import_button 'virtualization:clustertype_import' %}
{% endif %}
{% export_button content_type %}
</div>
<h1>{% block title %}Cluster Types{% endblock %}</h1>
<div class="row">
<div class="col-md-12">
{% include 'utilities/obj_table.html' with bulk_delete_url='virtualization:clustertype_bulk_delete' %}
</div>
</div>
{% endblock %}

View File

@ -1,14 +0,0 @@
{% extends 'utilities/obj_table.html' %}
{% block extra_actions %}
{% if perms.virtualization.change_virtualmachine %}
<div class="btn-group">
<button type="button" class="btn btn-sm btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add Components <span class="caret"></span>
</button>
<ul class="dropdown-menu">
{% if perms.dcim.add_interface %}<li><a href="{% url 'virtualization:virtualmachine_bulk_add_interface' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="formaction">Interfaces</a></li>{% endif %}
</ul>
</div>
{% endif %}
{% endblock %}

View File

@ -1,21 +1,14 @@
{% extends '_base.html' %} {% extends 'utilities/obj_list.html' %}
{% load buttons %}
{% block content %} {% block bulk_buttons %}
<div class="pull-right noprint"> {% if perms.virtualization.change_virtualmachine %}
{% if perms.virtualization.add_virtualmachine %} <div class="btn-group">
{% add_button 'virtualization:virtualmachine_add' %} <button type="button" class="btn btn-sm btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{% import_button 'virtualization:virtualmachine_import' %} <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add Components <span class="caret"></span>
</button>
<ul class="dropdown-menu">
{% if perms.dcim.add_interface %}<li><a href="{% url 'virtualization:virtualmachine_bulk_add_interface' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="formaction">Interfaces</a></li>{% endif %}
</ul>
</div>
{% endif %} {% endif %}
{% export_button content_type %}
</div>
<h1>{% block title %}Virtual Machines{% endblock %}</h1>
<div class="row">
<div class="col-md-9">
{% include 'virtualization/inc/virtualmachine_table.html' with bulk_edit_url='virtualization:virtualmachine_bulk_edit' bulk_delete_url='virtualization:virtualmachine_bulk_delete' %}
</div>
<div class="col-md-3 noprint">
{% include 'inc/search_panel.html' %}
</div>
</div>
{% endblock %} {% endblock %}

View File

@ -22,7 +22,6 @@ class TenantGroupListView(PermissionRequiredMixin, ObjectListView):
permission_required = 'tenancy.view_tenantgroup' permission_required = 'tenancy.view_tenantgroup'
queryset = TenantGroup.objects.annotate(tenant_count=Count('tenants')) queryset = TenantGroup.objects.annotate(tenant_count=Count('tenants'))
table = tables.TenantGroupTable table = tables.TenantGroupTable
template_name = 'tenancy/tenantgroup_list.html'
class TenantGroupCreateView(PermissionRequiredMixin, ObjectEditView): class TenantGroupCreateView(PermissionRequiredMixin, ObjectEditView):
@ -60,7 +59,6 @@ class TenantListView(PermissionRequiredMixin, ObjectListView):
filterset = filters.TenantFilterSet filterset = filters.TenantFilterSet
filterset_form = forms.TenantFilterForm filterset_form = forms.TenantFilterForm
table = tables.TenantTable table = tables.TenantTable
template_name = 'tenancy/tenant_list.html'
class TenantView(PermissionRequiredMixin, View): class TenantView(PermissionRequiredMixin, View):

View File

@ -69,3 +69,16 @@ FILTER_LOOKUP_HELP_TEXT_MAP = dict(
gte='greater than or equal', gte='greater than or equal',
n='negated' n='negated'
) )
# Keys for PostgreSQL advisory locks. These are arbitrary bigints used by
# the advisory_lock contextmanager. When a lock is acquired,
# one of these keys will be used to identify said lock.
#
# When adding a new key, pick something arbitrary and unique so
# that it is easily searchable in query logs.
ADVISORY_LOCK_KEYS = {
'available-prefixes': 100100,
'available-ips': 100200,
}

View File

@ -309,12 +309,17 @@ class APISelect(SelectWithDisabled):
def add_additional_query_param(self, name, value): def add_additional_query_param(self, name, value):
""" """
Add details for an additional query param in the form of a data-* attribute. Add details for an additional query param in the form of a data-* JSON-encoded list attribute.
:param name: The name of the query param :param name: The name of the query param
:param value: The value of the query param :param value: The value of the query param
""" """
self.attrs['data-additional-query-param-{}'.format(name)] = value key = 'data-additional-query-param-{}'.format(name)
values = json.loads(self.attrs.get(key, '[]'))
values.append(value)
self.attrs[key] = json.dumps(values)
def add_conditional_query_param(self, condition, value): def add_conditional_query_param(self, condition, value):
""" """

View File

@ -10,7 +10,7 @@ INTERFACE_NAME_REGEX = r'(^(?P<type>[^\d\.:]+)?)' \
r'(.(?P<vc>\d+)$)?' r'(.(?P<vc>\d+)$)?'
def naturalize(value, max_length=None, integer_places=8): def naturalize(value, max_length, integer_places=8):
""" """
Take an alphanumeric string and prepend all integers to `integer_places` places to ensure the strings Take an alphanumeric string and prepend all integers to `integer_places` places to ensure the strings
are ordered naturally. For example: are ordered naturally. For example:
@ -39,10 +39,10 @@ def naturalize(value, max_length=None, integer_places=8):
output.append(segment) output.append(segment)
ret = ''.join(output) ret = ''.join(output)
return ret[:max_length] if max_length else ret return ret[:max_length]
def naturalize_interface(value, max_length=None): def naturalize_interface(value, max_length):
""" """
Similar in nature to naturalize(), but takes into account a particular naming format adapted from the old Similar in nature to naturalize(), but takes into account a particular naming format adapted from the old
InterfaceManager. InterfaceManager.
@ -77,4 +77,4 @@ def naturalize_interface(value, max_length=None):
output.append('000000') output.append('000000')
ret = ''.join(output) ret = ''.join(output)
return ret[:max_length] if max_length else ret return ret[:max_length]

View File

@ -1,9 +1,10 @@
import datetime import datetime
import json import json
import re import re
import yaml
import yaml
from django import template from django import template
from django.urls import NoReverseMatch, reverse
from django.utils.html import strip_tags from django.utils.html import strip_tags
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from markdown import markdown from markdown import markdown
@ -11,7 +12,6 @@ from markdown import markdown
from utilities.choices import unpack_grouped_choices from utilities.choices import unpack_grouped_choices
from utilities.utils import foreground_color from utilities.utils import foreground_color
register = template.Library() register = template.Library()
@ -101,6 +101,21 @@ def model_name_plural(obj):
return obj._meta.verbose_name_plural return obj._meta.verbose_name_plural
@register.filter()
def url_name(model, action):
"""
Return the URL name for the given model and action, or None if invalid.
"""
url_name = '{}:{}_{}'.format(model._meta.app_label, model._meta.model_name, action)
try:
# Validate and return the URL name. We don't return the actual URL yet because many of the templates
# are written to pass a name to {% url %}.
reverse(url_name)
return url_name
except NoReverseMatch:
return None
@register.filter() @register.filter()
def contains(value, arg): def contains(value, arg):
""" """

View File

@ -21,7 +21,10 @@ class NaturalizationTestCase(TestCase):
) )
for origin, naturalized in data: for origin, naturalized in data:
self.assertEqual(naturalize(origin), naturalized) self.assertEqual(naturalize(origin, max_length=50), naturalized)
def test_naturalize_max_length(self):
self.assertEqual(naturalize('abc123def456', max_length=10), 'abc0000012')
def test_naturalize_interface(self): def test_naturalize_interface(self):
@ -40,4 +43,7 @@ class NaturalizationTestCase(TestCase):
) )
for origin, naturalized in data: for origin, naturalized in data:
self.assertEqual(naturalize_interface(origin), naturalized) self.assertEqual(naturalize_interface(origin, max_length=50), naturalized)
def test_naturalize_interface_max_length(self):
self.assertEqual(naturalize_interface('Gi1/2/3', max_length=20), '0001000299999999Gi00')

View File

@ -71,7 +71,8 @@ class ObjectListView(View):
filterset = None filterset = None
filterset_form = None filterset_form = None
table = None table = None
template_name = None template_name = 'utilities/obj_list.html'
action_buttons = ('add', 'import', 'export')
def queryset_to_yaml(self): def queryset_to_yaml(self):
""" """
@ -156,9 +157,11 @@ class ObjectListView(View):
# Provide a hook to tweak the queryset based on the request immediately prior to rendering the object list # Provide a hook to tweak the queryset based on the request immediately prior to rendering the object list
self.queryset = self.alter_queryset(request) self.queryset = self.alter_queryset(request)
# Compile user model permissions for access from within the template # Compile a dictionary indicating which permissions are available to the current user for this model
perm_base_name = '{}.{{}}_{}'.format(model._meta.app_label, model._meta.model_name) permissions = {}
permissions = {p: request.user.has_perm(perm_base_name.format(p)) for p in ['add', 'change', 'delete']} for action in ('add', 'change', 'delete', 'view'):
perm_name = '{}.{}_{}'.format(model._meta.app_label, action, model._meta.model_name)
permissions[action] = request.user.has_perm(perm_name)
# Construct the table based on the user's permissions # Construct the table based on the user's permissions
table = self.table(self.queryset) table = self.table(self.queryset)
@ -176,6 +179,7 @@ class ObjectListView(View):
'content_type': content_type, 'content_type': content_type,
'table': table, 'table': table,
'permissions': permissions, 'permissions': permissions,
'action_buttons': self.action_buttons,
'filter_form': self.filterset_form(request.GET, label_suffix='') if self.filterset_form else None, 'filter_form': self.filterset_form(request.GET, label_suffix='') if self.filterset_form else None,
} }
context.update(self.extra_context()) context.update(self.extra_context())
@ -630,7 +634,7 @@ class BulkEditView(GetReturnURLMixin, View):
post_data['pk'] = [obj.pk for obj in self.filterset(request.GET, model.objects.only('pk')).qs] post_data['pk'] = [obj.pk for obj in self.filterset(request.GET, model.objects.only('pk')).qs]
if '_apply' in request.POST: if '_apply' in request.POST:
form = self.form(model, request.POST, initial=request.GET) form = self.form(model, request.POST)
if form.is_valid(): if form.is_valid():
custom_fields = form.custom_fields if hasattr(form, 'custom_fields') else [] custom_fields = form.custom_fields if hasattr(form, 'custom_fields') else []
@ -714,10 +718,6 @@ class BulkEditView(GetReturnURLMixin, View):
else: else:
# Pass the PK list as initial data to avoid binding the form # Pass the PK list as initial data to avoid binding the form
initial_data = querydict_to_dict(post_data) initial_data = querydict_to_dict(post_data)
# Append any normal initial data (passed as GET parameters)
initial_data.update(request.GET)
form = self.form(model, initial=initial_data) form = self.form(model, initial=initial_data)
# Retrieve objects being edited # Retrieve objects being edited

View File

@ -351,6 +351,7 @@ class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
) )
role = DynamicModelChoiceField( role = DynamicModelChoiceField(
queryset=DeviceRole.objects.all(), queryset=DeviceRole.objects.all(),
required=False,
widget=APISelect( widget=APISelect(
api_url="/api/dcim/device-roles/", api_url="/api/dcim/device-roles/",
additional_query_params={ additional_query_params={
@ -658,7 +659,10 @@ class InterfaceForm(BootstrapMixin, forms.ModelForm):
widget=APISelect( widget=APISelect(
api_url="/api/ipam/vlans/", api_url="/api/ipam/vlans/",
display_field='display_name', display_field='display_name',
full=True full=True,
additional_query_params={
'site_id': 'null',
},
) )
) )
tagged_vlans = DynamicModelMultipleChoiceField( tagged_vlans = DynamicModelMultipleChoiceField(
@ -667,7 +671,10 @@ class InterfaceForm(BootstrapMixin, forms.ModelForm):
widget=APISelectMultiple( widget=APISelectMultiple(
api_url="/api/ipam/vlans/", api_url="/api/ipam/vlans/",
display_field='display_name', display_field='display_name',
full=True full=True,
additional_query_params={
'site_id': 'null',
},
) )
) )
tags = TagField( tags = TagField(
@ -695,35 +702,12 @@ class InterfaceForm(BootstrapMixin, forms.ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
# Limit VLan choices to those in: global vlans, global groups, the current site's group, the current site # Add current site to VLANs query params
vlan_choices = []
global_vlans = VLAN.objects.filter(site=None, group=None)
vlan_choices.append(
('Global', [(vlan.pk, vlan) for vlan in global_vlans])
)
for group in VLANGroup.objects.filter(site=None):
global_group_vlans = VLAN.objects.filter(group=group)
vlan_choices.append(
(group.name, [(vlan.pk, vlan) for vlan in global_group_vlans])
)
site = getattr(self.instance.parent, 'site', None) site = getattr(self.instance.parent, 'site', None)
if site is not None: if site is not None:
# Add current site to VLANs query params
# Add non-grouped site VLANs self.fields['untagged_vlan'].widget.add_additional_query_param('site_id', site.pk)
site_vlans = VLAN.objects.filter(site=site, group=None) self.fields['tagged_vlans'].widget.add_additional_query_param('site_id', site.pk)
vlan_choices.append((site.name, [(vlan.pk, vlan) for vlan in site_vlans]))
# Add grouped site VLANs
for group in VLANGroup.objects.filter(site=site):
site_group_vlans = VLAN.objects.filter(group=group)
vlan_choices.append((
'{} / {}'.format(group.site.name, group.name),
[(vlan.pk, vlan) for vlan in site_group_vlans]
))
self.fields['untagged_vlan'].choices = [(None, '---------')] + vlan_choices
self.fields['tagged_vlans'].choices = vlan_choices
def clean(self): def clean(self):
super().clean() super().clean()
@ -784,7 +768,10 @@ class InterfaceCreateForm(BootstrapMixin, forms.Form):
widget=APISelect( widget=APISelect(
api_url="/api/ipam/vlans/", api_url="/api/ipam/vlans/",
display_field='display_name', display_field='display_name',
full=True full=True,
additional_query_params={
'site_id': 'null',
},
) )
) )
tagged_vlans = DynamicModelMultipleChoiceField( tagged_vlans = DynamicModelMultipleChoiceField(
@ -793,7 +780,10 @@ class InterfaceCreateForm(BootstrapMixin, forms.Form):
widget=APISelectMultiple( widget=APISelectMultiple(
api_url="/api/ipam/vlans/", api_url="/api/ipam/vlans/",
display_field='display_name', display_field='display_name',
full=True full=True,
additional_query_params={
'site_id': 'null',
},
) )
) )
tags = TagField( tags = TagField(
@ -807,35 +797,11 @@ class InterfaceCreateForm(BootstrapMixin, forms.Form):
pk=self.initial.get('virtual_machine') or self.data.get('virtual_machine') pk=self.initial.get('virtual_machine') or self.data.get('virtual_machine')
) )
# Limit VLAN choices to those in: global vlans, global groups, the current site's group, the current site
vlan_choices = []
global_vlans = VLAN.objects.filter(site=None, group=None)
vlan_choices.append(
('Global', [(vlan.pk, vlan) for vlan in global_vlans])
)
for group in VLANGroup.objects.filter(site=None):
global_group_vlans = VLAN.objects.filter(group=group)
vlan_choices.append(
(group.name, [(vlan.pk, vlan) for vlan in global_group_vlans])
)
site = getattr(virtual_machine.cluster, 'site', None) site = getattr(virtual_machine.cluster, 'site', None)
if site is not None: if site is not None:
# Add current site to VLANs query params
# Add non-grouped site VLANs self.fields['untagged_vlan'].widget.add_additional_query_param('site_id', site.pk)
site_vlans = VLAN.objects.filter(site=site, group=None) self.fields['tagged_vlans'].widget.add_additional_query_param('site_id', site.pk)
vlan_choices.append((site.name, [(vlan.pk, vlan) for vlan in site_vlans]))
# Add grouped site VLANs
for group in VLANGroup.objects.filter(site=site):
site_group_vlans = VLAN.objects.filter(group=group)
vlan_choices.append((
'{} / {}'.format(group.site.name, group.name),
[(vlan.pk, vlan) for vlan in site_group_vlans]
))
self.fields['untagged_vlan'].choices = [(None, '---------')] + vlan_choices
self.fields['tagged_vlans'].choices = vlan_choices
class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm): class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
@ -872,7 +838,10 @@ class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
widget=APISelect( widget=APISelect(
api_url="/api/ipam/vlans/", api_url="/api/ipam/vlans/",
display_field='display_name', display_field='display_name',
full=True full=True,
additional_query_params={
'site_id': 'null',
},
) )
) )
tagged_vlans = DynamicModelMultipleChoiceField( tagged_vlans = DynamicModelMultipleChoiceField(
@ -881,7 +850,10 @@ class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
widget=APISelectMultiple( widget=APISelectMultiple(
api_url="/api/ipam/vlans/", api_url="/api/ipam/vlans/",
display_field='display_name', display_field='display_name',
full=True full=True,
additional_query_params={
'site_id': 'null',
},
) )
) )
@ -897,35 +869,11 @@ class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
if 'virtual_machine' in self.initial: if 'virtual_machine' in self.initial:
parent_obj = VirtualMachine.objects.filter(pk=self.initial['virtual_machine']).first() parent_obj = VirtualMachine.objects.filter(pk=self.initial['virtual_machine']).first()
# Limit VLAN choices to global VLANs, VLANs in global groups, the current site's group, the current site site = getattr(parent_obj.cluster, 'site', None)
vlan_choices = [] if site is not None:
global_vlans = VLAN.objects.filter(site=None, group=None) # Add current site to VLANs query params
vlan_choices.append( self.fields['untagged_vlan'].widget.add_additional_query_param('site_id', site.pk)
('Global', [(vlan.pk, vlan) for vlan in global_vlans]) self.fields['tagged_vlans'].widget.add_additional_query_param('site_id', site.pk)
)
for group in VLANGroup.objects.filter(site=None):
global_group_vlans = VLAN.objects.filter(group=group)
vlan_choices.append(
(group.name, [(vlan.pk, vlan) for vlan in global_group_vlans])
)
if parent_obj.cluster is not None:
site = getattr(parent_obj.cluster, 'site', None)
if site is not None:
# Add non-grouped site VLANs
site_vlans = VLAN.objects.filter(site=site, group=None)
vlan_choices.append((site.name, [(vlan.pk, vlan) for vlan in site_vlans]))
# Add grouped site VLANs
for group in VLANGroup.objects.filter(site=site):
site_group_vlans = VLAN.objects.filter(group=group)
vlan_choices.append((
'{} / {}'.format(group.site.name, group.name),
[(vlan.pk, vlan) for vlan in site_group_vlans]
))
self.fields['untagged_vlan'].choices = [(None, '---------')] + vlan_choices
self.fields['tagged_vlans'].choices = vlan_choices
# #

View File

@ -26,7 +26,6 @@ class ClusterTypeListView(PermissionRequiredMixin, ObjectListView):
permission_required = 'virtualization.view_clustertype' permission_required = 'virtualization.view_clustertype'
queryset = ClusterType.objects.annotate(cluster_count=Count('clusters')) queryset = ClusterType.objects.annotate(cluster_count=Count('clusters'))
table = tables.ClusterTypeTable table = tables.ClusterTypeTable
template_name = 'virtualization/clustertype_list.html'
class ClusterTypeCreateView(PermissionRequiredMixin, ObjectEditView): class ClusterTypeCreateView(PermissionRequiredMixin, ObjectEditView):
@ -62,7 +61,6 @@ class ClusterGroupListView(PermissionRequiredMixin, ObjectListView):
permission_required = 'virtualization.view_clustergroup' permission_required = 'virtualization.view_clustergroup'
queryset = ClusterGroup.objects.annotate(cluster_count=Count('clusters')) queryset = ClusterGroup.objects.annotate(cluster_count=Count('clusters'))
table = tables.ClusterGroupTable table = tables.ClusterGroupTable
template_name = 'virtualization/clustergroup_list.html'
class ClusterGroupCreateView(PermissionRequiredMixin, ObjectEditView): class ClusterGroupCreateView(PermissionRequiredMixin, ObjectEditView):
@ -100,7 +98,6 @@ class ClusterListView(PermissionRequiredMixin, ObjectListView):
table = tables.ClusterTable table = tables.ClusterTable
filterset = filters.ClusterFilterSet filterset = filters.ClusterFilterSet
filterset_form = forms.ClusterFilterForm filterset_form = forms.ClusterFilterForm
template_name = 'virtualization/cluster_list.html'
class ClusterView(PermissionRequiredMixin, View): class ClusterView(PermissionRequiredMixin, View):

View File

@ -4,6 +4,7 @@ django-cors-headers==3.2.1
django-debug-toolbar==2.1 django-debug-toolbar==2.1
django-filter==2.2.0 django-filter==2.2.0
django-mptt==0.9.1 django-mptt==0.9.1
django-pglocks==1.0.4
django-prometheus==1.1.0 django-prometheus==1.1.0
django-rq==2.2.0 django-rq==2.2.0
django-tables2==2.2.1 django-tables2==2.2.1