mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-16 12:12:53 -06:00
commit
8ba50d0cf2
23
.github/lock.yml
vendored
23
.github/lock.yml
vendored
@ -1,23 +0,0 @@
|
|||||||
# Configuration for Lock (https://github.com/apps/lock)
|
|
||||||
|
|
||||||
# Number of days of inactivity before a closed issue or pull request is locked
|
|
||||||
daysUntilLock: 90
|
|
||||||
|
|
||||||
# Skip issues and pull requests created before a given timestamp. Timestamp must
|
|
||||||
# follow ISO 8601 (`YYYY-MM-DD`). Set to `false` to disable
|
|
||||||
skipCreatedBefore: false
|
|
||||||
|
|
||||||
# Issues and pull requests with these labels will be ignored. Set to `[]` to disable
|
|
||||||
exemptLabels: []
|
|
||||||
|
|
||||||
# Label to add before locking, such as `outdated`. Set to `false` to disable
|
|
||||||
lockLabel: false
|
|
||||||
|
|
||||||
# Comment to post before locking. Set to `false` to disable
|
|
||||||
lockComment: false
|
|
||||||
|
|
||||||
# Assign `resolved` as the reason for locking. Set to `false` to disable
|
|
||||||
setLockReason: true
|
|
||||||
|
|
||||||
# Limit to only `issues` or `pulls`
|
|
||||||
# only: issues
|
|
21
.github/workflows/lock.yml
vendored
Normal file
21
.github/workflows/lock.yml
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# lock-threads (https://github.com/marketplace/actions/lock-threads)
|
||||||
|
name: 'Lock threads'
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: '0 3 * * *'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lock:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: dessant/lock-threads@v2
|
||||||
|
with:
|
||||||
|
github-token: ${{ github.token }}
|
||||||
|
issue-lock-inactive-days: '90'
|
||||||
|
issue-exclude-created-before: ''
|
||||||
|
issue-exclude-labels: ''
|
||||||
|
issue-lock-labels: ''
|
||||||
|
issue-lock-comment: ''
|
||||||
|
issue-lock-reason: 'resolved'
|
||||||
|
process-only: 'issues'
|
@ -73,8 +73,9 @@ tar -xf netbox_media.tar.gz
|
|||||||
|
|
||||||
## Cache Invalidation
|
## Cache Invalidation
|
||||||
|
|
||||||
If you are migrating your instance of NetBox to a different machine, be sure to first invalidate the cache by performing this command:
|
If you are migrating your instance of NetBox to a different machine, be sure to first invalidate the cache on the original instance by issuing the `invalidate all` management command (within the Python virtual environment):
|
||||||
|
|
||||||
```no-highlight
|
```no-highlight
|
||||||
python3 manage.py invalidate all
|
# source /opt/netbox/venv/bin/activate
|
||||||
|
(venv) # python3 manage.py invalidate all
|
||||||
```
|
```
|
||||||
|
@ -1,5 +1,25 @@
|
|||||||
# NetBox v2.9
|
# NetBox v2.9
|
||||||
|
|
||||||
|
## v2.9.9 (2020-11-09)
|
||||||
|
|
||||||
|
### Enhancements
|
||||||
|
|
||||||
|
* [#5304](https://github.com/netbox-community/netbox/issues/5304) - Return server error messages as JSON when handling REST API requests
|
||||||
|
* [#5310](https://github.com/netbox-community/netbox/issues/5310) - Link to rack groups within rack list table
|
||||||
|
* [#5327](https://github.com/netbox-community/netbox/issues/5327) - Be more strict when capturing anticipated ImportError exceptions
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* [#5271](https://github.com/netbox-community/netbox/issues/5271) - Fix auto-population of region field when editing a device
|
||||||
|
* [#5314](https://github.com/netbox-community/netbox/issues/5314) - Fix config context rendering when multiple tags are assigned to an object
|
||||||
|
* [#5316](https://github.com/netbox-community/netbox/issues/5316) - Dry running scripts should not trigger webhooks
|
||||||
|
* [#5324](https://github.com/netbox-community/netbox/issues/5324) - Add missing template extension tags for plugins for VM interface view
|
||||||
|
* [#5328](https://github.com/netbox-community/netbox/issues/5328) - Fix CreatedUpdatedFilterTest when running in non-UTC timezone
|
||||||
|
* [#5331](https://github.com/netbox-community/netbox/issues/5331) - Fix filtering of sites by null region
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## v2.9.8 (2020-10-30)
|
## v2.9.8 (2020-10-30)
|
||||||
|
|
||||||
### Enhancements
|
### Enhancements
|
||||||
|
@ -303,14 +303,24 @@ class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm
|
|||||||
#
|
#
|
||||||
|
|
||||||
class CircuitTerminationForm(BootstrapMixin, forms.ModelForm):
|
class CircuitTerminationForm(BootstrapMixin, forms.ModelForm):
|
||||||
|
region = DynamicModelChoiceField(
|
||||||
|
queryset=Region.objects.all(),
|
||||||
|
required=False,
|
||||||
|
initial_params={
|
||||||
|
'sites': '$site'
|
||||||
|
}
|
||||||
|
)
|
||||||
site = DynamicModelChoiceField(
|
site = DynamicModelChoiceField(
|
||||||
queryset=Site.objects.all()
|
queryset=Site.objects.all(),
|
||||||
|
query_params={
|
||||||
|
'region_id': '$region'
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CircuitTermination
|
model = CircuitTermination
|
||||||
fields = [
|
fields = [
|
||||||
'term_side', 'site', 'port_speed', 'upstream_speed', 'xconnect_id', 'pp_info', 'description',
|
'term_side', 'region', 'site', 'port_speed', 'upstream_speed', 'xconnect_id', 'pp_info', 'description',
|
||||||
]
|
]
|
||||||
help_texts = {
|
help_texts = {
|
||||||
'port_speed': "Physical circuit speed",
|
'port_speed': "Physical circuit speed",
|
||||||
|
@ -396,9 +396,7 @@ class DeviceViewSet(CustomFieldModelViewSet, ConfigContextQuerySetMixin):
|
|||||||
if device.platform is None:
|
if device.platform is None:
|
||||||
raise ServiceUnavailable("No platform is configured for this device.")
|
raise ServiceUnavailable("No platform is configured for this device.")
|
||||||
if not device.platform.napalm_driver:
|
if not device.platform.napalm_driver:
|
||||||
raise ServiceUnavailable("No NAPALM driver is configured for this device's platform {}.".format(
|
raise ServiceUnavailable(f"No NAPALM driver is configured for this device's platform: {device.platform}.")
|
||||||
device.platform
|
|
||||||
))
|
|
||||||
|
|
||||||
# Check for primary IP address from NetBox object
|
# Check for primary IP address from NetBox object
|
||||||
if device.primary_ip:
|
if device.primary_ip:
|
||||||
@ -407,21 +405,25 @@ class DeviceViewSet(CustomFieldModelViewSet, ConfigContextQuerySetMixin):
|
|||||||
# Raise exception for no IP address and no Name if device.name does not exist
|
# Raise exception for no IP address and no Name if device.name does not exist
|
||||||
if not device.name:
|
if not device.name:
|
||||||
raise ServiceUnavailable(
|
raise ServiceUnavailable(
|
||||||
"This device does not have a primary IP address or device name to lookup configured.")
|
"This device does not have a primary IP address or device name to lookup configured."
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
# Attempt to complete a DNS name resolution if no primary_ip is set
|
# Attempt to complete a DNS name resolution if no primary_ip is set
|
||||||
host = socket.gethostbyname(device.name)
|
host = socket.gethostbyname(device.name)
|
||||||
except socket.gaierror:
|
except socket.gaierror:
|
||||||
# Name lookup failure
|
# Name lookup failure
|
||||||
raise ServiceUnavailable(
|
raise ServiceUnavailable(
|
||||||
f"Name lookup failure, unable to resolve IP address for {device.name}. Please set Primary IP or setup name resolution.")
|
f"Name lookup failure, unable to resolve IP address for {device.name}. Please set Primary IP or "
|
||||||
|
f"setup name resolution.")
|
||||||
|
|
||||||
# Check that NAPALM is installed
|
# Check that NAPALM is installed
|
||||||
try:
|
try:
|
||||||
import napalm
|
import napalm
|
||||||
from napalm.base.exceptions import ModuleImportError
|
from napalm.base.exceptions import ModuleImportError
|
||||||
except ImportError:
|
except ModuleNotFoundError as e:
|
||||||
raise ServiceUnavailable("NAPALM is not installed. Please see the documentation for instructions.")
|
if getattr(e, 'name') == 'napalm':
|
||||||
|
raise ServiceUnavailable("NAPALM is not installed. Please see the documentation for instructions.")
|
||||||
|
raise e
|
||||||
|
|
||||||
# Validate the configured driver
|
# Validate the configured driver
|
||||||
try:
|
try:
|
||||||
|
@ -352,8 +352,18 @@ class SiteFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class RackGroupForm(BootstrapMixin, forms.ModelForm):
|
class RackGroupForm(BootstrapMixin, forms.ModelForm):
|
||||||
|
region = DynamicModelChoiceField(
|
||||||
|
queryset=Region.objects.all(),
|
||||||
|
required=False,
|
||||||
|
initial_params={
|
||||||
|
'sites': '$site'
|
||||||
|
}
|
||||||
|
)
|
||||||
site = DynamicModelChoiceField(
|
site = DynamicModelChoiceField(
|
||||||
queryset=Site.objects.all()
|
queryset=Site.objects.all(),
|
||||||
|
query_params={
|
||||||
|
'region_id': '$region'
|
||||||
|
}
|
||||||
)
|
)
|
||||||
parent = DynamicModelChoiceField(
|
parent = DynamicModelChoiceField(
|
||||||
queryset=RackGroup.objects.all(),
|
queryset=RackGroup.objects.all(),
|
||||||
@ -367,7 +377,7 @@ class RackGroupForm(BootstrapMixin, forms.ModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = RackGroup
|
model = RackGroup
|
||||||
fields = (
|
fields = (
|
||||||
'site', 'parent', 'name', 'slug', 'description',
|
'region', 'site', 'parent', 'name', 'slug', 'description',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -447,14 +457,17 @@ class RackRoleCSVForm(CSVModelForm):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class RackForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
class RackForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||||
site = DynamicModelChoiceField(
|
region = DynamicModelChoiceField(
|
||||||
queryset=Site.objects.all()
|
queryset=Region.objects.all(),
|
||||||
)
|
|
||||||
group = DynamicModelChoiceField(
|
|
||||||
queryset=RackGroup.objects.all(),
|
|
||||||
required=False,
|
required=False,
|
||||||
|
initial_params={
|
||||||
|
'sites': '$site'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
site = DynamicModelChoiceField(
|
||||||
|
queryset=Site.objects.all(),
|
||||||
query_params={
|
query_params={
|
||||||
'site_id': '$site'
|
'region_id': '$region'
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
role = DynamicModelChoiceField(
|
role = DynamicModelChoiceField(
|
||||||
@ -470,8 +483,9 @@ class RackForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Rack
|
model = Rack
|
||||||
fields = [
|
fields = [
|
||||||
'site', 'group', 'name', 'facility_id', 'tenant_group', 'tenant', 'status', 'role', 'serial', 'asset_tag',
|
'region', 'site', 'group', 'name', 'facility_id', 'tenant_group', 'tenant', 'status', 'role', 'serial',
|
||||||
'type', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'comments', 'tags',
|
'asset_tag', 'type', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit',
|
||||||
|
'comments', 'tags',
|
||||||
]
|
]
|
||||||
help_texts = {
|
help_texts = {
|
||||||
'site': "The site at which the rack exists",
|
'site': "The site at which the rack exists",
|
||||||
@ -548,9 +562,19 @@ class RackBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor
|
|||||||
queryset=Rack.objects.all(),
|
queryset=Rack.objects.all(),
|
||||||
widget=forms.MultipleHiddenInput
|
widget=forms.MultipleHiddenInput
|
||||||
)
|
)
|
||||||
|
region = DynamicModelChoiceField(
|
||||||
|
queryset=Region.objects.all(),
|
||||||
|
required=False,
|
||||||
|
initial_params={
|
||||||
|
'sites': '$site'
|
||||||
|
}
|
||||||
|
)
|
||||||
site = DynamicModelChoiceField(
|
site = DynamicModelChoiceField(
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
required=False
|
required=False,
|
||||||
|
query_params={
|
||||||
|
'region_id': '$region'
|
||||||
|
}
|
||||||
)
|
)
|
||||||
group = DynamicModelChoiceField(
|
group = DynamicModelChoiceField(
|
||||||
queryset=RackGroup.objects.all(),
|
queryset=RackGroup.objects.all(),
|
||||||
@ -691,9 +715,19 @@ class RackElevationFilterForm(RackFilterForm):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class RackReservationForm(BootstrapMixin, TenancyForm, forms.ModelForm):
|
class RackReservationForm(BootstrapMixin, TenancyForm, forms.ModelForm):
|
||||||
|
region = DynamicModelChoiceField(
|
||||||
|
queryset=Region.objects.all(),
|
||||||
|
required=False,
|
||||||
|
initial_params={
|
||||||
|
'sites': '$site'
|
||||||
|
}
|
||||||
|
)
|
||||||
site = DynamicModelChoiceField(
|
site = DynamicModelChoiceField(
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
required=False
|
required=False,
|
||||||
|
query_params={
|
||||||
|
'region_id': '$region'
|
||||||
|
}
|
||||||
)
|
)
|
||||||
rack_group = DynamicModelChoiceField(
|
rack_group = DynamicModelChoiceField(
|
||||||
queryset=RackGroup.objects.all(),
|
queryset=RackGroup.objects.all(),
|
||||||
@ -707,7 +741,7 @@ class RackReservationForm(BootstrapMixin, TenancyForm, forms.ModelForm):
|
|||||||
display_field='display_name',
|
display_field='display_name',
|
||||||
query_params={
|
query_params={
|
||||||
'site_id': '$site',
|
'site_id': '$site',
|
||||||
'group_id': 'rack',
|
'group_id': '$rack',
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
units = NumericArrayField(
|
units = NumericArrayField(
|
||||||
@ -809,15 +843,23 @@ class RackReservationBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditFor
|
|||||||
|
|
||||||
class RackReservationFilterForm(BootstrapMixin, TenancyFilterForm):
|
class RackReservationFilterForm(BootstrapMixin, TenancyFilterForm):
|
||||||
model = RackReservation
|
model = RackReservation
|
||||||
field_order = ['q', 'site', 'group_id', 'tenant_group', 'tenant']
|
field_order = ['q', 'region', 'site', 'group_id', 'tenant_group', 'tenant']
|
||||||
q = forms.CharField(
|
q = forms.CharField(
|
||||||
required=False,
|
required=False,
|
||||||
label='Search'
|
label='Search'
|
||||||
)
|
)
|
||||||
|
region = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=Region.objects.all(),
|
||||||
|
to_field_name='slug',
|
||||||
|
required=False
|
||||||
|
)
|
||||||
site = DynamicModelMultipleChoiceField(
|
site = DynamicModelMultipleChoiceField(
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
required=False
|
required=False,
|
||||||
|
query_params={
|
||||||
|
'region': '$region'
|
||||||
|
}
|
||||||
)
|
)
|
||||||
group_id = DynamicModelMultipleChoiceField(
|
group_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=RackGroup.objects.prefetch_related('site'),
|
queryset=RackGroup.objects.prefetch_related('site'),
|
||||||
@ -1672,7 +1714,10 @@ class PlatformCSVForm(CSVModelForm):
|
|||||||
class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||||
region = DynamicModelChoiceField(
|
region = DynamicModelChoiceField(
|
||||||
queryset=Region.objects.all(),
|
queryset=Region.objects.all(),
|
||||||
required=False
|
required=False,
|
||||||
|
initial_params={
|
||||||
|
'sites': '$site'
|
||||||
|
}
|
||||||
)
|
)
|
||||||
site = DynamicModelChoiceField(
|
site = DynamicModelChoiceField(
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
@ -1686,6 +1731,9 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
|||||||
display_field='display_name',
|
display_field='display_name',
|
||||||
query_params={
|
query_params={
|
||||||
'site_id': '$site'
|
'site_id': '$site'
|
||||||
|
},
|
||||||
|
initial_params={
|
||||||
|
'racks': '$rack'
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
rack = DynamicModelChoiceField(
|
rack = DynamicModelChoiceField(
|
||||||
@ -1711,7 +1759,10 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
|||||||
)
|
)
|
||||||
manufacturer = DynamicModelChoiceField(
|
manufacturer = DynamicModelChoiceField(
|
||||||
queryset=Manufacturer.objects.all(),
|
queryset=Manufacturer.objects.all(),
|
||||||
required=False
|
required=False,
|
||||||
|
initial_params={
|
||||||
|
'device_types': '$device_type'
|
||||||
|
}
|
||||||
)
|
)
|
||||||
device_type = DynamicModelChoiceField(
|
device_type = DynamicModelChoiceField(
|
||||||
queryset=DeviceType.objects.all(),
|
queryset=DeviceType.objects.all(),
|
||||||
@ -1733,7 +1784,10 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
|||||||
cluster_group = DynamicModelChoiceField(
|
cluster_group = DynamicModelChoiceField(
|
||||||
queryset=ClusterGroup.objects.all(),
|
queryset=ClusterGroup.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
null_option='None'
|
null_option='None',
|
||||||
|
initial_params={
|
||||||
|
'clusters': '$cluster'
|
||||||
|
}
|
||||||
)
|
)
|
||||||
cluster = DynamicModelChoiceField(
|
cluster = DynamicModelChoiceField(
|
||||||
queryset=Cluster.objects.all(),
|
queryset=Cluster.objects.all(),
|
||||||
@ -1772,27 +1826,6 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
|
||||||
# Initialize helper selectors
|
|
||||||
instance = kwargs.get('instance')
|
|
||||||
if 'initial' not in kwargs:
|
|
||||||
kwargs['initial'] = {}
|
|
||||||
# Using hasattr() instead of "is not None" to avoid RelatedObjectDoesNotExist on required field
|
|
||||||
if instance and hasattr(instance, 'device_type'):
|
|
||||||
kwargs['initial']['manufacturer'] = instance.device_type.manufacturer
|
|
||||||
if instance and instance.cluster is not None:
|
|
||||||
kwargs['initial']['cluster_group'] = instance.cluster.group
|
|
||||||
|
|
||||||
if 'device_type' in kwargs['initial'] and 'manufacturer' not in kwargs['initial']:
|
|
||||||
device_type_id = kwargs['initial']['device_type']
|
|
||||||
manufacturer_id = DeviceType.objects.filter(pk=device_type_id).values_list('manufacturer__pk', flat=True).first()
|
|
||||||
kwargs['initial']['manufacturer'] = manufacturer_id
|
|
||||||
|
|
||||||
if 'cluster' in kwargs['initial'] and 'cluster_group' not in kwargs['initial']:
|
|
||||||
cluster_id = kwargs['initial']['cluster']
|
|
||||||
cluster_group_id = Cluster.objects.filter(pk=cluster_id).values_list('group__pk', flat=True).first()
|
|
||||||
kwargs['initial']['cluster_group'] = cluster_group_id
|
|
||||||
|
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
if self.instance.pk:
|
if self.instance.pk:
|
||||||
@ -3426,10 +3459,18 @@ class ConnectCableToDeviceForm(BootstrapMixin, forms.ModelForm):
|
|||||||
"""
|
"""
|
||||||
Base form for connecting a Cable to a Device component
|
Base form for connecting a Cable to a Device component
|
||||||
"""
|
"""
|
||||||
|
termination_b_region = DynamicModelChoiceField(
|
||||||
|
queryset=Region.objects.all(),
|
||||||
|
label='Region',
|
||||||
|
required=False
|
||||||
|
)
|
||||||
termination_b_site = DynamicModelChoiceField(
|
termination_b_site = DynamicModelChoiceField(
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
label='Site',
|
label='Site',
|
||||||
required=False
|
required=False,
|
||||||
|
query_params={
|
||||||
|
'region_id': '$termination_b_region'
|
||||||
|
}
|
||||||
)
|
)
|
||||||
termination_b_rack = DynamicModelChoiceField(
|
termination_b_rack = DynamicModelChoiceField(
|
||||||
queryset=Rack.objects.all(),
|
queryset=Rack.objects.all(),
|
||||||
@ -3455,8 +3496,8 @@ class ConnectCableToDeviceForm(BootstrapMixin, forms.ModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Cable
|
model = Cable
|
||||||
fields = [
|
fields = [
|
||||||
'termination_b_site', 'termination_b_rack', 'termination_b_device', 'termination_b_id', 'type', 'status',
|
'termination_b_region', 'termination_b_site', 'termination_b_rack', 'termination_b_device',
|
||||||
'label', 'color', 'length', 'length_unit',
|
'termination_b_id', 'type', 'status', 'label', 'color', 'length', 'length_unit',
|
||||||
]
|
]
|
||||||
widgets = {
|
widgets = {
|
||||||
'status': StaticSelect2,
|
'status': StaticSelect2,
|
||||||
@ -3553,10 +3594,18 @@ class ConnectCableToCircuitTerminationForm(BootstrapMixin, forms.ModelForm):
|
|||||||
label='Provider',
|
label='Provider',
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
|
termination_b_region = DynamicModelChoiceField(
|
||||||
|
queryset=Region.objects.all(),
|
||||||
|
label='Region',
|
||||||
|
required=False
|
||||||
|
)
|
||||||
termination_b_site = DynamicModelChoiceField(
|
termination_b_site = DynamicModelChoiceField(
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
label='Site',
|
label='Site',
|
||||||
required=False
|
required=False,
|
||||||
|
query_params={
|
||||||
|
'region_id': '$termination_b_region'
|
||||||
|
}
|
||||||
)
|
)
|
||||||
termination_b_circuit = DynamicModelChoiceField(
|
termination_b_circuit = DynamicModelChoiceField(
|
||||||
queryset=Circuit.objects.all(),
|
queryset=Circuit.objects.all(),
|
||||||
@ -3580,8 +3629,8 @@ class ConnectCableToCircuitTerminationForm(BootstrapMixin, forms.ModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Cable
|
model = Cable
|
||||||
fields = [
|
fields = [
|
||||||
'termination_b_provider', 'termination_b_site', 'termination_b_circuit', 'termination_b_id', 'type',
|
'termination_b_provider', 'termination_b_region', 'termination_b_site', 'termination_b_circuit',
|
||||||
'status', 'label', 'color', 'length', 'length_unit',
|
'termination_b_id', 'type', 'status', 'label', 'color', 'length', 'length_unit',
|
||||||
]
|
]
|
||||||
|
|
||||||
def clean_termination_b_id(self):
|
def clean_termination_b_id(self):
|
||||||
@ -3590,11 +3639,18 @@ class ConnectCableToCircuitTerminationForm(BootstrapMixin, forms.ModelForm):
|
|||||||
|
|
||||||
|
|
||||||
class ConnectCableToPowerFeedForm(BootstrapMixin, forms.ModelForm):
|
class ConnectCableToPowerFeedForm(BootstrapMixin, forms.ModelForm):
|
||||||
|
termination_b_region = DynamicModelChoiceField(
|
||||||
|
queryset=Region.objects.all(),
|
||||||
|
label='Region',
|
||||||
|
required=False
|
||||||
|
)
|
||||||
termination_b_site = DynamicModelChoiceField(
|
termination_b_site = DynamicModelChoiceField(
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
label='Site',
|
label='Site',
|
||||||
required=False,
|
required=False,
|
||||||
display_field='cid'
|
query_params={
|
||||||
|
'region_id': '$termination_b_region'
|
||||||
|
}
|
||||||
)
|
)
|
||||||
termination_b_rackgroup = DynamicModelChoiceField(
|
termination_b_rackgroup = DynamicModelChoiceField(
|
||||||
queryset=RackGroup.objects.all(),
|
queryset=RackGroup.objects.all(),
|
||||||
@ -3836,10 +3892,18 @@ class CableFilterForm(BootstrapMixin, forms.Form):
|
|||||||
required=False,
|
required=False,
|
||||||
label='Search'
|
label='Search'
|
||||||
)
|
)
|
||||||
|
region = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=Region.objects.all(),
|
||||||
|
to_field_name='slug',
|
||||||
|
required=False
|
||||||
|
)
|
||||||
site = DynamicModelMultipleChoiceField(
|
site = DynamicModelMultipleChoiceField(
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
required=False
|
required=False,
|
||||||
|
query_params={
|
||||||
|
'region': '$region'
|
||||||
|
}
|
||||||
)
|
)
|
||||||
tenant = DynamicModelMultipleChoiceField(
|
tenant = DynamicModelMultipleChoiceField(
|
||||||
queryset=Tenant.objects.all(),
|
queryset=Tenant.objects.all(),
|
||||||
@ -3888,10 +3952,18 @@ class CableFilterForm(BootstrapMixin, forms.Form):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class ConsoleConnectionFilterForm(BootstrapMixin, forms.Form):
|
class ConsoleConnectionFilterForm(BootstrapMixin, forms.Form):
|
||||||
|
region = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=Region.objects.all(),
|
||||||
|
to_field_name='slug',
|
||||||
|
required=False
|
||||||
|
)
|
||||||
site = DynamicModelMultipleChoiceField(
|
site = DynamicModelMultipleChoiceField(
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
required=False
|
required=False,
|
||||||
|
query_params={
|
||||||
|
'region': '$region'
|
||||||
|
}
|
||||||
)
|
)
|
||||||
device_id = DynamicModelMultipleChoiceField(
|
device_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=Device.objects.all(),
|
queryset=Device.objects.all(),
|
||||||
@ -3904,10 +3976,18 @@ class ConsoleConnectionFilterForm(BootstrapMixin, forms.Form):
|
|||||||
|
|
||||||
|
|
||||||
class PowerConnectionFilterForm(BootstrapMixin, forms.Form):
|
class PowerConnectionFilterForm(BootstrapMixin, forms.Form):
|
||||||
|
region = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=Region.objects.all(),
|
||||||
|
to_field_name='slug',
|
||||||
|
required=False
|
||||||
|
)
|
||||||
site = DynamicModelMultipleChoiceField(
|
site = DynamicModelMultipleChoiceField(
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
required=False
|
required=False,
|
||||||
|
query_params={
|
||||||
|
'region': '$region'
|
||||||
|
}
|
||||||
)
|
)
|
||||||
device_id = DynamicModelMultipleChoiceField(
|
device_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=Device.objects.all(),
|
queryset=Device.objects.all(),
|
||||||
@ -3920,10 +4000,18 @@ class PowerConnectionFilterForm(BootstrapMixin, forms.Form):
|
|||||||
|
|
||||||
|
|
||||||
class InterfaceConnectionFilterForm(BootstrapMixin, forms.Form):
|
class InterfaceConnectionFilterForm(BootstrapMixin, forms.Form):
|
||||||
|
region = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=Region.objects.all(),
|
||||||
|
to_field_name='slug',
|
||||||
|
required=False
|
||||||
|
)
|
||||||
site = DynamicModelMultipleChoiceField(
|
site = DynamicModelMultipleChoiceField(
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
required=False
|
required=False,
|
||||||
|
query_params={
|
||||||
|
'region': '$region'
|
||||||
|
}
|
||||||
)
|
)
|
||||||
device_id = DynamicModelMultipleChoiceField(
|
device_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=Device.objects.all(),
|
queryset=Device.objects.all(),
|
||||||
@ -3947,9 +4035,19 @@ class DeviceSelectionForm(forms.Form):
|
|||||||
|
|
||||||
|
|
||||||
class VirtualChassisCreateForm(BootstrapMixin, forms.ModelForm):
|
class VirtualChassisCreateForm(BootstrapMixin, forms.ModelForm):
|
||||||
|
region = DynamicModelChoiceField(
|
||||||
|
queryset=Region.objects.all(),
|
||||||
|
required=False,
|
||||||
|
initial_params={
|
||||||
|
'sites': '$site'
|
||||||
|
}
|
||||||
|
)
|
||||||
site = DynamicModelChoiceField(
|
site = DynamicModelChoiceField(
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
required=False
|
required=False,
|
||||||
|
query_params={
|
||||||
|
'region_id': '$region'
|
||||||
|
}
|
||||||
)
|
)
|
||||||
rack = DynamicModelChoiceField(
|
rack = DynamicModelChoiceField(
|
||||||
queryset=Rack.objects.all(),
|
queryset=Rack.objects.all(),
|
||||||
@ -3982,7 +4080,7 @@ class VirtualChassisCreateForm(BootstrapMixin, forms.ModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = VirtualChassis
|
model = VirtualChassis
|
||||||
fields = [
|
fields = [
|
||||||
'name', 'domain', 'site', 'rack', 'members', 'initial_position', 'tags',
|
'name', 'domain', 'region', 'site', 'rack', 'members', 'initial_position', 'tags',
|
||||||
]
|
]
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
@ -4079,9 +4177,19 @@ class DeviceVCMembershipForm(forms.ModelForm):
|
|||||||
|
|
||||||
|
|
||||||
class VCMemberSelectForm(BootstrapMixin, forms.Form):
|
class VCMemberSelectForm(BootstrapMixin, forms.Form):
|
||||||
|
region = DynamicModelChoiceField(
|
||||||
|
queryset=Region.objects.all(),
|
||||||
|
required=False,
|
||||||
|
initial_params={
|
||||||
|
'sites': '$site'
|
||||||
|
}
|
||||||
|
)
|
||||||
site = DynamicModelChoiceField(
|
site = DynamicModelChoiceField(
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
required=False
|
required=False,
|
||||||
|
query_params={
|
||||||
|
'region_id': '$region'
|
||||||
|
}
|
||||||
)
|
)
|
||||||
rack = DynamicModelChoiceField(
|
rack = DynamicModelChoiceField(
|
||||||
queryset=Rack.objects.all(),
|
queryset=Rack.objects.all(),
|
||||||
@ -4180,8 +4288,18 @@ class VirtualChassisFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class PowerPanelForm(BootstrapMixin, forms.ModelForm):
|
class PowerPanelForm(BootstrapMixin, forms.ModelForm):
|
||||||
|
region = DynamicModelChoiceField(
|
||||||
|
queryset=Region.objects.all(),
|
||||||
|
required=False,
|
||||||
|
initial_params={
|
||||||
|
'sites': '$site'
|
||||||
|
}
|
||||||
|
)
|
||||||
site = DynamicModelChoiceField(
|
site = DynamicModelChoiceField(
|
||||||
queryset=Site.objects.all()
|
queryset=Site.objects.all(),
|
||||||
|
query_params={
|
||||||
|
'region_id': '$region'
|
||||||
|
}
|
||||||
)
|
)
|
||||||
rack_group = DynamicModelChoiceField(
|
rack_group = DynamicModelChoiceField(
|
||||||
queryset=RackGroup.objects.all(),
|
queryset=RackGroup.objects.all(),
|
||||||
@ -4198,7 +4316,7 @@ class PowerPanelForm(BootstrapMixin, forms.ModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = PowerPanel
|
model = PowerPanel
|
||||||
fields = [
|
fields = [
|
||||||
'site', 'rack_group', 'name', 'tags',
|
'region', 'site', 'rack_group', 'name', 'tags',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -4233,9 +4351,19 @@ class PowerPanelBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
|
|||||||
queryset=PowerPanel.objects.all(),
|
queryset=PowerPanel.objects.all(),
|
||||||
widget=forms.MultipleHiddenInput
|
widget=forms.MultipleHiddenInput
|
||||||
)
|
)
|
||||||
|
region = DynamicModelChoiceField(
|
||||||
|
queryset=Region.objects.all(),
|
||||||
|
required=False,
|
||||||
|
initial_params={
|
||||||
|
'sites': '$site'
|
||||||
|
}
|
||||||
|
)
|
||||||
site = DynamicModelChoiceField(
|
site = DynamicModelChoiceField(
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
required=False
|
required=False,
|
||||||
|
query_params={
|
||||||
|
'region_id': '$region'
|
||||||
|
}
|
||||||
)
|
)
|
||||||
rack_group = DynamicModelChoiceField(
|
rack_group = DynamicModelChoiceField(
|
||||||
queryset=RackGroup.objects.all(),
|
queryset=RackGroup.objects.all(),
|
||||||
@ -4287,9 +4415,22 @@ class PowerPanelFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class PowerFeedForm(BootstrapMixin, CustomFieldModelForm):
|
class PowerFeedForm(BootstrapMixin, CustomFieldModelForm):
|
||||||
|
region = DynamicModelChoiceField(
|
||||||
|
queryset=Region.objects.all(),
|
||||||
|
required=False,
|
||||||
|
initial_params={
|
||||||
|
'sites__powerpanel': '$power_panel'
|
||||||
|
}
|
||||||
|
)
|
||||||
site = DynamicModelChoiceField(
|
site = DynamicModelChoiceField(
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
required=False
|
required=False,
|
||||||
|
initial_params={
|
||||||
|
'powerpanel': '$power_panel'
|
||||||
|
},
|
||||||
|
query_params={
|
||||||
|
'region_id': '$region'
|
||||||
|
}
|
||||||
)
|
)
|
||||||
power_panel = DynamicModelChoiceField(
|
power_panel = DynamicModelChoiceField(
|
||||||
queryset=PowerPanel.objects.all(),
|
queryset=PowerPanel.objects.all(),
|
||||||
@ -4314,7 +4455,7 @@ class PowerFeedForm(BootstrapMixin, CustomFieldModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = PowerFeed
|
model = PowerFeed
|
||||||
fields = [
|
fields = [
|
||||||
'site', 'power_panel', 'rack', 'name', 'status', 'type', 'supply', 'phase', 'voltage', 'amperage',
|
'region', 'site', 'power_panel', 'rack', 'name', 'status', 'type', 'supply', 'phase', 'voltage', 'amperage',
|
||||||
'max_utilization', 'comments', 'tags',
|
'max_utilization', 'comments', 'tags',
|
||||||
]
|
]
|
||||||
widgets = {
|
widgets = {
|
||||||
@ -4324,14 +4465,6 @@ class PowerFeedForm(BootstrapMixin, CustomFieldModelForm):
|
|||||||
'phase': StaticSelect2(),
|
'phase': StaticSelect2(),
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
# Initialize site field
|
|
||||||
if self.instance and hasattr(self.instance, 'power_panel'):
|
|
||||||
self.initial['site'] = self.instance.power_panel.site
|
|
||||||
|
|
||||||
|
|
||||||
class PowerFeedCSVForm(CustomFieldModelCSVForm):
|
class PowerFeedCSVForm(CustomFieldModelCSVForm):
|
||||||
site = CSVModelChoiceField(
|
site = CSVModelChoiceField(
|
||||||
|
@ -262,12 +262,15 @@ class RackRoleTable(BaseTable):
|
|||||||
|
|
||||||
class RackTable(BaseTable):
|
class RackTable(BaseTable):
|
||||||
pk = ToggleColumn()
|
pk = ToggleColumn()
|
||||||
name = tables.LinkColumn(
|
name = tables.Column(
|
||||||
order_by=('_name',)
|
order_by=('_name',),
|
||||||
|
linkify=True
|
||||||
)
|
)
|
||||||
site = tables.LinkColumn(
|
group = tables.Column(
|
||||||
viewname='dcim:site',
|
linkify=True
|
||||||
args=[Accessor('site__slug')]
|
)
|
||||||
|
site = tables.Column(
|
||||||
|
linkify=True
|
||||||
)
|
)
|
||||||
tenant = tables.TemplateColumn(
|
tenant = tables.TemplateColumn(
|
||||||
template_code=COL_TENANT
|
template_code=COL_TENANT
|
||||||
|
@ -100,23 +100,6 @@ class DeviceTestCase(TestCase):
|
|||||||
self.assertIn('face', form.errors)
|
self.assertIn('face', form.errors)
|
||||||
self.assertIn('position', form.errors)
|
self.assertIn('position', form.errors)
|
||||||
|
|
||||||
def test_initial_data_population(self):
|
|
||||||
device_type = DeviceType.objects.first()
|
|
||||||
cluster = Cluster.objects.first()
|
|
||||||
test = DeviceForm(initial={
|
|
||||||
'device_type': device_type.pk,
|
|
||||||
'device_role': DeviceRole.objects.first().pk,
|
|
||||||
'status': DeviceStatusChoices.STATUS_ACTIVE,
|
|
||||||
'site': Site.objects.first().pk,
|
|
||||||
'cluster': cluster.pk,
|
|
||||||
})
|
|
||||||
|
|
||||||
# Check that the initial value for the manufacturer is set automatically when assigning the device type
|
|
||||||
self.assertEqual(test.initial['manufacturer'], device_type.manufacturer.pk)
|
|
||||||
|
|
||||||
# Check that the initial value for the cluster group is set automatically when assigning the cluster
|
|
||||||
self.assertEqual(test.initial['cluster_group'], cluster.group.pk)
|
|
||||||
|
|
||||||
|
|
||||||
class LabelTestCase(TestCase):
|
class LabelTestCase(TestCase):
|
||||||
|
|
||||||
|
@ -2043,8 +2043,11 @@ class CableCreateView(ObjectEditView):
|
|||||||
initial_data = {k: request.GET[k] for k in request.GET}
|
initial_data = {k: request.GET[k] for k in request.GET}
|
||||||
|
|
||||||
# Set initial site and rack based on side A termination (if not already set)
|
# Set initial site and rack based on side A termination (if not already set)
|
||||||
|
termination_a_site = getattr(obj.termination_a.parent, 'site', None)
|
||||||
|
if termination_a_site and 'termination_b_region' not in initial_data:
|
||||||
|
initial_data['termination_b_region'] = termination_a_site.region
|
||||||
if 'termination_b_site' not in initial_data:
|
if 'termination_b_site' not in initial_data:
|
||||||
initial_data['termination_b_site'] = getattr(obj.termination_a.parent, 'site', None)
|
initial_data['termination_b_site'] = termination_a_site
|
||||||
if 'termination_b_rack' not in initial_data:
|
if 'termination_b_rack' not in initial_data:
|
||||||
initial_data['termination_b_rack'] = getattr(obj.termination_a.parent, 'rack', None)
|
initial_data['termination_b_rack'] = getattr(obj.termination_a.parent, 'rack', None)
|
||||||
|
|
||||||
|
@ -60,7 +60,7 @@ class ConfigContextQuerySet(RestrictedQuerySet):
|
|||||||
Q(tenants=obj.tenant) | Q(tenants=None),
|
Q(tenants=obj.tenant) | Q(tenants=None),
|
||||||
Q(tags__slug__in=obj.tags.slugs()) | Q(tags=None),
|
Q(tags__slug__in=obj.tags.slugs()) | Q(tags=None),
|
||||||
is_active=True,
|
is_active=True,
|
||||||
).order_by('weight', 'name')
|
).order_by('weight', 'name').distinct()
|
||||||
|
|
||||||
if aggregate_data:
|
if aggregate_data:
|
||||||
return queryset.aggregate(
|
return queryset.aggregate(
|
||||||
@ -95,7 +95,7 @@ class ConfigContextModelQuerySet(RestrictedQuerySet):
|
|||||||
_data=EmptyGroupByJSONBAgg('data', ordering=['weight', 'name'])
|
_data=EmptyGroupByJSONBAgg('data', ordering=['weight', 'name'])
|
||||||
).values("_data")
|
).values("_data")
|
||||||
)
|
)
|
||||||
)
|
).distinct()
|
||||||
|
|
||||||
def _get_config_context_filters(self):
|
def _get_config_context_filters(self):
|
||||||
# Construct the set of Q objects for the specific object types
|
# Construct the set of Q objects for the specific object types
|
||||||
|
@ -441,8 +441,11 @@ def run_script(data, request, commit=True, *args, **kwargs):
|
|||||||
f"with NetBox v2.10."
|
f"with NetBox v2.10."
|
||||||
)
|
)
|
||||||
|
|
||||||
with change_logging(request):
|
def _run_script():
|
||||||
|
"""
|
||||||
|
Core script execution task. We capture this within a subfunction to allow for conditionally wrapping it with
|
||||||
|
the change_logging context manager (which is bypassed if commit == False).
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
script.output = script.run(**kwargs)
|
script.output = script.run(**kwargs)
|
||||||
@ -469,6 +472,14 @@ def run_script(data, request, commit=True, *args, **kwargs):
|
|||||||
|
|
||||||
logger.info(f"Script completed in {job_result.duration}")
|
logger.info(f"Script completed in {job_result.duration}")
|
||||||
|
|
||||||
|
# Execute the script. If commit is True, wrap it with the change_logging context manager to ensure we process
|
||||||
|
# change logging, webhooks, etc.
|
||||||
|
if commit:
|
||||||
|
with change_logging(request):
|
||||||
|
_run_script()
|
||||||
|
else:
|
||||||
|
_run_script()
|
||||||
|
|
||||||
# Delete any previous terminal state results
|
# Delete any previous terminal state results
|
||||||
JobResult.objects.filter(
|
JobResult.objects.filter(
|
||||||
obj_type=job_result.obj_type,
|
obj_type=job_result.obj_type,
|
||||||
|
@ -16,7 +16,7 @@ GROUP_BUTTON = '<div class="btn-group">\n' \
|
|||||||
'{} <span class="caret"></span>\n' \
|
'{} <span class="caret"></span>\n' \
|
||||||
'</button>\n' \
|
'</button>\n' \
|
||||||
'<ul class="dropdown-menu pull-right">\n' \
|
'<ul class="dropdown-menu pull-right">\n' \
|
||||||
'{}</ul></div>'
|
'{}</ul></div>\n'
|
||||||
GROUP_LINK = '<li><a href="{}"{}>{}</a></li>\n'
|
GROUP_LINK = '<li><a href="{}"{}>{}</a></li>\n'
|
||||||
|
|
||||||
|
|
||||||
|
@ -13,7 +13,7 @@ class SiteContent(PluginTemplateExtension):
|
|||||||
def full_width_page(self):
|
def full_width_page(self):
|
||||||
return "SITE CONTENT - FULL WIDTH PAGE"
|
return "SITE CONTENT - FULL WIDTH PAGE"
|
||||||
|
|
||||||
def full_buttons(self):
|
def buttons(self):
|
||||||
return "SITE CONTENT - BUTTONS"
|
return "SITE CONTENT - BUTTONS"
|
||||||
|
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ from unittest import skipIf
|
|||||||
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils import timezone
|
from django.utils.timezone import make_aware
|
||||||
from django_rq.queues import get_connection
|
from django_rq.queues import get_connection
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rq import Worker
|
from rq import Worker
|
||||||
@ -369,8 +369,8 @@ class CreatedUpdatedFilterTest(APITestCase):
|
|||||||
|
|
||||||
# change the created and last_updated of one
|
# change the created and last_updated of one
|
||||||
Rack.objects.filter(pk=self.rack2.pk).update(
|
Rack.objects.filter(pk=self.rack2.pk).update(
|
||||||
last_updated=datetime.datetime(2001, 2, 3, 1, 2, 3, 4, tzinfo=timezone.utc),
|
last_updated=make_aware(datetime.datetime(2001, 2, 3, 1, 2, 3, 4)),
|
||||||
created=datetime.datetime(2001, 2, 3)
|
created=make_aware(datetime.datetime(2001, 2, 3))
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_get_rack_created(self):
|
def test_get_rack_created(self):
|
||||||
|
@ -75,6 +75,7 @@ class ConfigContextTest(TestCase):
|
|||||||
self.tenantgroup = TenantGroup.objects.create(name="Tenant Group")
|
self.tenantgroup = TenantGroup.objects.create(name="Tenant Group")
|
||||||
self.tenant = Tenant.objects.create(name="Tenant", group=self.tenantgroup)
|
self.tenant = Tenant.objects.create(name="Tenant", group=self.tenantgroup)
|
||||||
self.tag = Tag.objects.create(name="Tag", slug="tag")
|
self.tag = Tag.objects.create(name="Tag", slug="tag")
|
||||||
|
self.tag2 = Tag.objects.create(name="Tag2", slug="tag2")
|
||||||
|
|
||||||
self.device = Device.objects.create(
|
self.device = Device.objects.create(
|
||||||
name='Device 1',
|
name='Device 1',
|
||||||
@ -328,3 +329,37 @@ class ConfigContextTest(TestCase):
|
|||||||
|
|
||||||
annotated_queryset = VirtualMachine.objects.filter(name=virtual_machine.name).annotate_config_context_data()
|
annotated_queryset = VirtualMachine.objects.filter(name=virtual_machine.name).annotate_config_context_data()
|
||||||
self.assertEqual(virtual_machine.get_config_context(), annotated_queryset[0].get_config_context())
|
self.assertEqual(virtual_machine.get_config_context(), annotated_queryset[0].get_config_context())
|
||||||
|
|
||||||
|
def test_multiple_tags_return_distinct_objects(self):
|
||||||
|
"""
|
||||||
|
Tagged items use a generic relationship, which results in duplicate rows being returned when queried.
|
||||||
|
This is combatted by by appending distinct() to the config context querysets. This test creates a config
|
||||||
|
context assigned to two tags and ensures objects related by those same two tags result in only a single
|
||||||
|
config context record being returned.
|
||||||
|
|
||||||
|
See https://github.com/netbox-community/netbox/issues/5314
|
||||||
|
"""
|
||||||
|
tag_context = ConfigContext.objects.create(
|
||||||
|
name="tag",
|
||||||
|
weight=100,
|
||||||
|
data={
|
||||||
|
"tag": 1
|
||||||
|
}
|
||||||
|
)
|
||||||
|
tag_context.tags.add(self.tag)
|
||||||
|
tag_context.tags.add(self.tag2)
|
||||||
|
|
||||||
|
device = Device.objects.create(
|
||||||
|
name="Device 3",
|
||||||
|
site=self.site,
|
||||||
|
tenant=self.tenant,
|
||||||
|
platform=self.platform,
|
||||||
|
device_role=self.devicerole,
|
||||||
|
device_type=self.devicetype
|
||||||
|
)
|
||||||
|
device.tags.add(self.tag)
|
||||||
|
device.tags.add(self.tag2)
|
||||||
|
|
||||||
|
annotated_queryset = Device.objects.filter(name=device.name).annotate_config_context_data()
|
||||||
|
self.assertEqual(ConfigContext.objects.get_for_object(device).count(), 1)
|
||||||
|
self.assertEqual(device.get_config_context(), annotated_queryset[0].get_config_context())
|
||||||
|
@ -253,10 +253,20 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
|||||||
label='VRF',
|
label='VRF',
|
||||||
display_field='display_name'
|
display_field='display_name'
|
||||||
)
|
)
|
||||||
|
region = DynamicModelChoiceField(
|
||||||
|
queryset=Region.objects.all(),
|
||||||
|
required=False,
|
||||||
|
initial_params={
|
||||||
|
'sites': '$site'
|
||||||
|
}
|
||||||
|
)
|
||||||
site = DynamicModelChoiceField(
|
site = DynamicModelChoiceField(
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
null_option='None'
|
null_option='None',
|
||||||
|
query_params={
|
||||||
|
'region_id': '$region'
|
||||||
|
}
|
||||||
)
|
)
|
||||||
vlan_group = DynamicModelChoiceField(
|
vlan_group = DynamicModelChoiceField(
|
||||||
queryset=VLANGroup.objects.all(),
|
queryset=VLANGroup.objects.all(),
|
||||||
@ -265,6 +275,9 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
|||||||
null_option='None',
|
null_option='None',
|
||||||
query_params={
|
query_params={
|
||||||
'site_id': '$site'
|
'site_id': '$site'
|
||||||
|
},
|
||||||
|
initial_params={
|
||||||
|
'vlans': '$vlan'
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
vlan = DynamicModelChoiceField(
|
vlan = DynamicModelChoiceField(
|
||||||
@ -297,14 +310,6 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
|
||||||
# Initialize helper selectors
|
|
||||||
instance = kwargs.get('instance')
|
|
||||||
initial = kwargs.get('initial', {}).copy()
|
|
||||||
if instance and instance.vlan is not None:
|
|
||||||
initial['vlan_group'] = instance.vlan.group
|
|
||||||
kwargs['initial'] = initial
|
|
||||||
|
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
self.fields['vrf'].empty_label = 'Global'
|
self.fields['vrf'].empty_label = 'Global'
|
||||||
@ -374,9 +379,17 @@ class PrefixBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF
|
|||||||
queryset=Prefix.objects.all(),
|
queryset=Prefix.objects.all(),
|
||||||
widget=forms.MultipleHiddenInput()
|
widget=forms.MultipleHiddenInput()
|
||||||
)
|
)
|
||||||
|
region = DynamicModelChoiceField(
|
||||||
|
queryset=Region.objects.all(),
|
||||||
|
required=False,
|
||||||
|
to_field_name='slug'
|
||||||
|
)
|
||||||
site = DynamicModelChoiceField(
|
site = DynamicModelChoiceField(
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
required=False
|
required=False,
|
||||||
|
query_params={
|
||||||
|
'region': '$region'
|
||||||
|
}
|
||||||
)
|
)
|
||||||
vrf = DynamicModelChoiceField(
|
vrf = DynamicModelChoiceField(
|
||||||
queryset=VRF.objects.all(),
|
queryset=VRF.objects.all(),
|
||||||
@ -501,7 +514,10 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModel
|
|||||||
device = DynamicModelChoiceField(
|
device = DynamicModelChoiceField(
|
||||||
queryset=Device.objects.all(),
|
queryset=Device.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
display_field='display_name'
|
display_field='display_name',
|
||||||
|
initial_params={
|
||||||
|
'interfaces': '$interface'
|
||||||
|
}
|
||||||
)
|
)
|
||||||
interface = DynamicModelChoiceField(
|
interface = DynamicModelChoiceField(
|
||||||
queryset=Interface.objects.all(),
|
queryset=Interface.objects.all(),
|
||||||
@ -512,7 +528,10 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModel
|
|||||||
)
|
)
|
||||||
virtual_machine = DynamicModelChoiceField(
|
virtual_machine = DynamicModelChoiceField(
|
||||||
queryset=VirtualMachine.objects.all(),
|
queryset=VirtualMachine.objects.all(),
|
||||||
required=False
|
required=False,
|
||||||
|
initial_params={
|
||||||
|
'interfaces': '$vminterface'
|
||||||
|
}
|
||||||
)
|
)
|
||||||
vminterface = DynamicModelChoiceField(
|
vminterface = DynamicModelChoiceField(
|
||||||
queryset=VMInterface.objects.all(),
|
queryset=VMInterface.objects.all(),
|
||||||
@ -528,10 +547,21 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModel
|
|||||||
label='VRF',
|
label='VRF',
|
||||||
display_field='display_name'
|
display_field='display_name'
|
||||||
)
|
)
|
||||||
|
nat_region = DynamicModelChoiceField(
|
||||||
|
queryset=Region.objects.all(),
|
||||||
|
required=False,
|
||||||
|
label='Region',
|
||||||
|
initial_params={
|
||||||
|
'sites': '$nat_site'
|
||||||
|
}
|
||||||
|
)
|
||||||
nat_site = DynamicModelChoiceField(
|
nat_site = DynamicModelChoiceField(
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
label='Site'
|
label='Site',
|
||||||
|
query_params={
|
||||||
|
'region_id': '$nat_region'
|
||||||
|
}
|
||||||
)
|
)
|
||||||
nat_rack = DynamicModelChoiceField(
|
nat_rack = DynamicModelChoiceField(
|
||||||
queryset=Rack.objects.all(),
|
queryset=Rack.objects.all(),
|
||||||
@ -611,10 +641,8 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModel
|
|||||||
initial = kwargs.get('initial', {}).copy()
|
initial = kwargs.get('initial', {}).copy()
|
||||||
if instance:
|
if instance:
|
||||||
if type(instance.assigned_object) is Interface:
|
if type(instance.assigned_object) is Interface:
|
||||||
initial['device'] = instance.assigned_object.device
|
|
||||||
initial['interface'] = instance.assigned_object
|
initial['interface'] = instance.assigned_object
|
||||||
elif type(instance.assigned_object) is VMInterface:
|
elif type(instance.assigned_object) is VMInterface:
|
||||||
initial['virtual_machine'] = instance.assigned_object.virtual_machine
|
|
||||||
initial['vminterface'] = instance.assigned_object
|
initial['vminterface'] = instance.assigned_object
|
||||||
if instance.nat_inside:
|
if instance.nat_inside:
|
||||||
nat_inside_parent = instance.nat_inside.assigned_object
|
nat_inside_parent = instance.nat_inside.assigned_object
|
||||||
@ -925,16 +953,26 @@ class IPAddressFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterFo
|
|||||||
#
|
#
|
||||||
|
|
||||||
class VLANGroupForm(BootstrapMixin, forms.ModelForm):
|
class VLANGroupForm(BootstrapMixin, forms.ModelForm):
|
||||||
|
region = DynamicModelChoiceField(
|
||||||
|
queryset=Region.objects.all(),
|
||||||
|
required=False,
|
||||||
|
initial_params={
|
||||||
|
'sites': '$site'
|
||||||
|
}
|
||||||
|
)
|
||||||
site = DynamicModelChoiceField(
|
site = DynamicModelChoiceField(
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
required=False
|
required=False,
|
||||||
|
query_params={
|
||||||
|
'region_id': '$region'
|
||||||
|
}
|
||||||
)
|
)
|
||||||
slug = SlugField()
|
slug = SlugField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = VLANGroup
|
model = VLANGroup
|
||||||
fields = [
|
fields = [
|
||||||
'site', 'name', 'slug', 'description',
|
'region', 'site', 'name', 'slug', 'description',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -974,10 +1012,20 @@ class VLANGroupFilterForm(BootstrapMixin, forms.Form):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class VLANForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
class VLANForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||||
|
region = DynamicModelChoiceField(
|
||||||
|
queryset=Region.objects.all(),
|
||||||
|
required=False,
|
||||||
|
initial_params={
|
||||||
|
'sites': '$site'
|
||||||
|
}
|
||||||
|
)
|
||||||
site = DynamicModelChoiceField(
|
site = DynamicModelChoiceField(
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
null_option='None'
|
null_option='None',
|
||||||
|
query_params={
|
||||||
|
'region_id': '$region'
|
||||||
|
}
|
||||||
)
|
)
|
||||||
group = DynamicModelChoiceField(
|
group = DynamicModelChoiceField(
|
||||||
queryset=VLANGroup.objects.all(),
|
queryset=VLANGroup.objects.all(),
|
||||||
@ -1066,9 +1114,17 @@ class VLANBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor
|
|||||||
queryset=VLAN.objects.all(),
|
queryset=VLAN.objects.all(),
|
||||||
widget=forms.MultipleHiddenInput()
|
widget=forms.MultipleHiddenInput()
|
||||||
)
|
)
|
||||||
|
region = DynamicModelChoiceField(
|
||||||
|
queryset=Region.objects.all(),
|
||||||
|
required=False,
|
||||||
|
to_field_name='slug'
|
||||||
|
)
|
||||||
site = DynamicModelChoiceField(
|
site = DynamicModelChoiceField(
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
required=False
|
required=False,
|
||||||
|
query_params={
|
||||||
|
'region': '$region'
|
||||||
|
}
|
||||||
)
|
)
|
||||||
group = DynamicModelChoiceField(
|
group = DynamicModelChoiceField(
|
||||||
queryset=VLANGroup.objects.all(),
|
queryset=VLANGroup.objects.all(),
|
||||||
|
@ -137,19 +137,24 @@ class LDAPBackend:
|
|||||||
|
|
||||||
def __new__(cls, *args, **kwargs):
|
def __new__(cls, *args, **kwargs):
|
||||||
try:
|
try:
|
||||||
import ldap
|
|
||||||
from django_auth_ldap.backend import LDAPBackend as LDAPBackend_, LDAPSettings
|
from django_auth_ldap.backend import LDAPBackend as LDAPBackend_, LDAPSettings
|
||||||
except ImportError:
|
import ldap
|
||||||
raise ImproperlyConfigured(
|
except ModuleNotFoundError as e:
|
||||||
"LDAP authentication has been configured, but django-auth-ldap is not installed."
|
if getattr(e, 'name') == 'django_auth_ldap':
|
||||||
)
|
raise ImproperlyConfigured(
|
||||||
|
"LDAP authentication has been configured, but django-auth-ldap is not installed."
|
||||||
|
)
|
||||||
|
raise e
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from netbox import ldap_config
|
from netbox import ldap_config
|
||||||
except ImportError:
|
except ModuleNotFoundError as e:
|
||||||
raise ImproperlyConfigured(
|
if getattr(e, 'name') == 'ldap_config':
|
||||||
"ldap_config.py does not exist"
|
raise ImproperlyConfigured(
|
||||||
)
|
"LDAP configuration file not found: Check that ldap_config.py has been created alongside "
|
||||||
|
"configuration.py."
|
||||||
|
)
|
||||||
|
raise e
|
||||||
|
|
||||||
try:
|
try:
|
||||||
getattr(ldap_config, 'AUTH_LDAP_SERVER_URI')
|
getattr(ldap_config, 'AUTH_LDAP_SERVER_URI')
|
||||||
|
@ -16,7 +16,7 @@ from django.core.validators import URLValidator
|
|||||||
# Environment setup
|
# Environment setup
|
||||||
#
|
#
|
||||||
|
|
||||||
VERSION = '2.9.8'
|
VERSION = '2.9.9'
|
||||||
|
|
||||||
# Hostname
|
# Hostname
|
||||||
HOSTNAME = platform.node()
|
HOSTNAME = platform.node()
|
||||||
@ -38,10 +38,12 @@ if platform.python_version_tuple() < ('3', '6'):
|
|||||||
# Import configuration parameters
|
# Import configuration parameters
|
||||||
try:
|
try:
|
||||||
from netbox import configuration
|
from netbox import configuration
|
||||||
except ImportError:
|
except ModuleNotFoundError as e:
|
||||||
raise ImproperlyConfigured(
|
if getattr(e, 'name') == 'configuration':
|
||||||
"Configuration file is not present. Please define netbox/netbox/configuration.py per the documentation."
|
raise ImproperlyConfigured(
|
||||||
)
|
"Configuration file is not present. Please define netbox/netbox/configuration.py per the documentation."
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
|
||||||
# Enforce required configuration parameters
|
# Enforce required configuration parameters
|
||||||
for parameter in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY', 'REDIS']:
|
for parameter in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY', 'REDIS']:
|
||||||
@ -183,11 +185,13 @@ if STORAGE_BACKEND is not None:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
import storages.utils
|
import storages.utils
|
||||||
except ImportError:
|
except ModuleNotFoundError as e:
|
||||||
raise ImproperlyConfigured(
|
if getattr(e, 'name') == 'storages':
|
||||||
"STORAGE_BACKEND is set to {} but django-storages is not present. It can be installed by running 'pip "
|
raise ImproperlyConfigured(
|
||||||
"install django-storages'.".format(STORAGE_BACKEND)
|
f"STORAGE_BACKEND is set to {STORAGE_BACKEND} but django-storages is not present. It can be "
|
||||||
)
|
f"installed by running 'pip install django-storages'."
|
||||||
|
)
|
||||||
|
raise e
|
||||||
|
|
||||||
# Monkey-patch django-storages to fetch settings from STORAGE_CONFIG
|
# Monkey-patch django-storages to fetch settings from STORAGE_CONFIG
|
||||||
def _setting(name, default=None):
|
def _setting(name, default=None):
|
||||||
@ -596,11 +600,13 @@ for plugin_name in PLUGINS:
|
|||||||
# Import plugin module
|
# Import plugin module
|
||||||
try:
|
try:
|
||||||
plugin = importlib.import_module(plugin_name)
|
plugin = importlib.import_module(plugin_name)
|
||||||
except ImportError:
|
except ModuleNotFoundError as e:
|
||||||
raise ImproperlyConfigured(
|
if getattr(e, 'name') == plugin_name:
|
||||||
"Unable to import plugin {}: Module not found. Check that the plugin module has been installed within the "
|
raise ImproperlyConfigured(
|
||||||
"correct Python environment.".format(plugin_name)
|
"Unable to import plugin {}: Module not found. Check that the plugin module has been installed within the "
|
||||||
)
|
"correct Python environment.".format(plugin_name)
|
||||||
|
)
|
||||||
|
raise e
|
||||||
|
|
||||||
# Determine plugin config and add to INSTALLED_APPS.
|
# Determine plugin config and add to INSTALLED_APPS.
|
||||||
try:
|
try:
|
||||||
|
@ -40,6 +40,7 @@
|
|||||||
<p class="form-control-static">{{ form.term_side.value }}</p>
|
<p class="form-control-static">{{ form.term_side.value }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% render_field form.region %}
|
||||||
{% render_field form.site %}
|
{% render_field form.site %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -32,6 +32,12 @@
|
|||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
{% if termination_a.device %}
|
{% if termination_a.device %}
|
||||||
{# Device component #}
|
{# Device component #}
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-md-3 control-label required">Region</label>
|
||||||
|
<div class="col-md-9">
|
||||||
|
<p class="form-control-static">{{ termination_a.device.site.region }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="col-md-3 control-label required">Site</label>
|
<label class="col-md-3 control-label required">Site</label>
|
||||||
<div class="col-md-9">
|
<div class="col-md-9">
|
||||||
@ -111,6 +117,9 @@
|
|||||||
{% if 'termination_b_provider' in form.fields %}
|
{% if 'termination_b_provider' in form.fields %}
|
||||||
{% render_field form.termination_b_provider %}
|
{% render_field form.termination_b_provider %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if 'termination_b_region' in form.fields %}
|
||||||
|
{% render_field form.termination_b_region %}
|
||||||
|
{% endif %}
|
||||||
{% if 'termination_b_site' in form.fields %}
|
{% if 'termination_b_site' in form.fields %}
|
||||||
{% render_field form.termination_b_site %}
|
{% render_field form.termination_b_site %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -3,10 +3,16 @@
|
|||||||
|
|
||||||
{% block form %}
|
{% block form %}
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading"><strong>Power Feed</strong></div>
|
<div class="panel-heading"><strong>Power Panel</strong></div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
|
{% render_field form.region %}
|
||||||
{% render_field form.site %}
|
{% render_field form.site %}
|
||||||
{% render_field form.power_panel %}
|
{% render_field form.power_panel %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading"><strong>Power Feed</strong></div>
|
||||||
|
<div class="panel-body">
|
||||||
{% render_field form.rack %}
|
{% render_field form.rack %}
|
||||||
{% render_field form.name %}
|
{% render_field form.name %}
|
||||||
{% render_field form.status %}
|
{% render_field form.status %}
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading"><strong>Rack</strong></div>
|
<div class="panel-heading"><strong>Rack</strong></div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
|
{% render_field form.region %}
|
||||||
{% render_field form.site %}
|
{% render_field form.site %}
|
||||||
{% render_field form.name %}
|
{% render_field form.name %}
|
||||||
{% render_field form.facility_id %}
|
{% render_field form.facility_id %}
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading"><strong>Rack Reservation</strong></div>
|
<div class="panel-heading"><strong>Rack Reservation</strong></div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
|
{% render_field form.region %}
|
||||||
{% render_field form.site %}
|
{% render_field form.site %}
|
||||||
{% render_field form.rack_group %}
|
{% render_field form.rack_group %}
|
||||||
{% render_field form.rack %}
|
{% render_field form.rack %}
|
||||||
|
@ -13,6 +13,7 @@
|
|||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading"><strong>Member Devices</strong></div>
|
<div class="panel-heading"><strong>Member Devices</strong></div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
|
{% render_field form.region %}
|
||||||
{% render_field form.site %}
|
{% render_field form.site %}
|
||||||
{% render_field form.rack %}
|
{% render_field form.rack %}
|
||||||
{% render_field form.members %}
|
{% render_field form.members %}
|
||||||
|
@ -62,6 +62,7 @@
|
|||||||
</ul>
|
</ul>
|
||||||
<div class="tab-content">
|
<div class="tab-content">
|
||||||
<div class="tab-pane active" id="by_device">
|
<div class="tab-pane active" id="by_device">
|
||||||
|
{% render_field form.nat_region %}
|
||||||
{% render_field form.nat_site %}
|
{% render_field form.nat_site %}
|
||||||
{% render_field form.nat_rack %}
|
{% render_field form.nat_rack %}
|
||||||
{% render_field form.nat_device %}
|
{% render_field form.nat_device %}
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading"><strong>Site/VLAN Assignment</strong></div>
|
<div class="panel-heading"><strong>Site/VLAN Assignment</strong></div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
|
{% render_field form.region %}
|
||||||
{% render_field form.site %}
|
{% render_field form.site %}
|
||||||
{% render_field form.vlan_group %}
|
{% render_field form.vlan_group %}
|
||||||
{% render_field form.vlan %}
|
{% render_field form.vlan %}
|
||||||
|
@ -8,12 +8,18 @@
|
|||||||
{% render_field form.vid %}
|
{% render_field form.vid %}
|
||||||
{% render_field form.name %}
|
{% render_field form.name %}
|
||||||
{% render_field form.status %}
|
{% render_field form.status %}
|
||||||
{% render_field form.site %}
|
|
||||||
{% render_field form.group %}
|
|
||||||
{% render_field form.role %}
|
{% render_field form.role %}
|
||||||
{% render_field form.description %}
|
{% render_field form.description %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading"><strong>Assignment</strong></div>
|
||||||
|
<div class="panel-body">
|
||||||
|
{% render_field form.region %}
|
||||||
|
{% render_field form.site %}
|
||||||
|
{% render_field form.group %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading"><strong>Tenancy</strong></div>
|
<div class="panel-heading"><strong>Tenancy</strong></div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
{% render_field form.name %}
|
{% render_field form.name %}
|
||||||
{% render_field form.type %}
|
{% render_field form.type %}
|
||||||
{% render_field form.group %}
|
{% render_field form.group %}
|
||||||
|
{% render_field form.region %}
|
||||||
{% render_field form.site %}
|
{% render_field form.site %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
{% load helpers %}
|
{% load helpers %}
|
||||||
|
{% load plugins %}
|
||||||
|
|
||||||
{% block header %}
|
{% block header %}
|
||||||
<div class="row noprint">
|
<div class="row noprint">
|
||||||
@ -12,6 +13,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="pull-right noprint">
|
<div class="pull-right noprint">
|
||||||
|
{% plugin_buttons vminterface %}
|
||||||
{% if perms.virtualization.change_vminterface %}
|
{% if perms.virtualization.change_vminterface %}
|
||||||
<a href="{% url 'virtualization:vminterface_edit' pk=vminterface.pk %}" class="btn btn-warning">
|
<a href="{% url 'virtualization:vminterface_edit' pk=vminterface.pk %}" class="btn btn-warning">
|
||||||
<span class="fa fa-pencil" aria-hidden="true"></span> Edit
|
<span class="fa fa-pencil" aria-hidden="true"></span> Edit
|
||||||
@ -82,9 +84,11 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
{% plugin_left_page vminterface %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
{% include 'extras/inc/tags_panel.html' with tags=vminterface.tags.all %}
|
{% include 'extras/inc/tags_panel.html' with tags=vminterface.tags.all %}
|
||||||
|
{% plugin_right_page vminterface %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@ -97,4 +101,9 @@
|
|||||||
{% include 'panel_table.html' with table=vlan_table heading="VLANs" %}
|
{% include 'panel_table.html' with table=vlan_table heading="VLANs" %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
{% plugin_full_width_page vminterface %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -119,7 +119,10 @@ class TenancyForm(forms.Form):
|
|||||||
tenant_group = DynamicModelChoiceField(
|
tenant_group = DynamicModelChoiceField(
|
||||||
queryset=TenantGroup.objects.all(),
|
queryset=TenantGroup.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
null_option='None'
|
null_option='None',
|
||||||
|
initial_params={
|
||||||
|
'tenants': '$tenant'
|
||||||
|
}
|
||||||
)
|
)
|
||||||
tenant = DynamicModelChoiceField(
|
tenant = DynamicModelChoiceField(
|
||||||
queryset=Tenant.objects.all(),
|
queryset=Tenant.objects.all(),
|
||||||
@ -129,17 +132,6 @@ class TenancyForm(forms.Form):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
|
|
||||||
# Initialize helper selector
|
|
||||||
instance = kwargs.get('instance')
|
|
||||||
if instance and instance.tenant is not None:
|
|
||||||
initial = kwargs.get('initial', {}).copy()
|
|
||||||
initial['tenant_group'] = instance.tenant.group
|
|
||||||
kwargs['initial'] = initial
|
|
||||||
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class TenancyFilterForm(forms.Form):
|
class TenancyFilterForm(forms.Form):
|
||||||
tenant_group = DynamicModelMultipleChoiceField(
|
tenant_group = DynamicModelMultipleChoiceField(
|
||||||
|
@ -68,11 +68,10 @@ class TreeNodeMultipleChoiceFilter(django_filters.ModelMultipleChoiceFilter):
|
|||||||
"""
|
"""
|
||||||
Filters for a set of Models, including all descendant models within a Tree. Example: [<Region: R1>,<Region: R2>]
|
Filters for a set of Models, including all descendant models within a Tree. Example: [<Region: R1>,<Region: R2>]
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def get_filter_predicate(self, v):
|
def get_filter_predicate(self, v):
|
||||||
# null value filtering
|
# Null value filtering
|
||||||
if v is None:
|
if v is None:
|
||||||
return {self.field_name.replace('in', 'isnull'): True}
|
return {f"{self.field_name}__isnull": True}
|
||||||
return super().get_filter_predicate(v)
|
return super().get_filter_predicate(v)
|
||||||
|
|
||||||
def filter(self, qs, value):
|
def filter(self, qs, value):
|
||||||
|
@ -248,6 +248,7 @@ class DynamicModelChoiceMixin:
|
|||||||
"""
|
"""
|
||||||
:param display_field: The name of the attribute of an API response object to display in the selection list
|
:param display_field: The name of the attribute of an API response object to display in the selection list
|
||||||
:param query_params: A dictionary of additional key/value pairs to attach to the API request
|
:param query_params: A dictionary of additional key/value pairs to attach to the API request
|
||||||
|
:param initial_params: A dictionary of child field references to use for selecting a parent field's initial value
|
||||||
:param null_option: The string used to represent a null selection (if any)
|
:param null_option: The string used to represent a null selection (if any)
|
||||||
:param disabled_indicator: The name of the field which, if populated, will disable selection of the
|
:param disabled_indicator: The name of the field which, if populated, will disable selection of the
|
||||||
choice (optional)
|
choice (optional)
|
||||||
@ -256,10 +257,11 @@ class DynamicModelChoiceMixin:
|
|||||||
filter = django_filters.ModelChoiceFilter
|
filter = django_filters.ModelChoiceFilter
|
||||||
widget = widgets.APISelect
|
widget = widgets.APISelect
|
||||||
|
|
||||||
def __init__(self, display_field='name', query_params=None, null_option=None, disabled_indicator=None,
|
def __init__(self, display_field='name', query_params=None, initial_params=None, null_option=None,
|
||||||
brief_mode=True, *args, **kwargs):
|
disabled_indicator=None, brief_mode=True, *args, **kwargs):
|
||||||
self.display_field = display_field
|
self.display_field = display_field
|
||||||
self.query_params = query_params or {}
|
self.query_params = query_params or {}
|
||||||
|
self.initial_params = initial_params or {}
|
||||||
self.null_option = null_option
|
self.null_option = null_option
|
||||||
self.disabled_indicator = disabled_indicator
|
self.disabled_indicator = disabled_indicator
|
||||||
self.brief_mode = brief_mode
|
self.brief_mode = brief_mode
|
||||||
@ -300,6 +302,16 @@ class DynamicModelChoiceMixin:
|
|||||||
def get_bound_field(self, form, field_name):
|
def get_bound_field(self, form, field_name):
|
||||||
bound_field = BoundField(form, self, field_name)
|
bound_field = BoundField(form, self, field_name)
|
||||||
|
|
||||||
|
# Set initial value based on prescribed child fields (if not already set)
|
||||||
|
if not self.initial and self.initial_params:
|
||||||
|
filter_kwargs = {}
|
||||||
|
for kwarg, child_field in self.initial_params.items():
|
||||||
|
value = form.initial.get(child_field.lstrip('$'))
|
||||||
|
if value:
|
||||||
|
filter_kwargs[kwarg] = value
|
||||||
|
if filter_kwargs:
|
||||||
|
self.initial = self.queryset.filter(**filter_kwargs).first()
|
||||||
|
|
||||||
# Modify the QuerySet of the field before we return it. Limit choices to any data already bound: Options
|
# Modify the QuerySet of the field before we return it. Limit choices to any data already bound: Options
|
||||||
# will be populated on-demand via the APISelect widget.
|
# will be populated on-demand via the APISelect widget.
|
||||||
data = bound_field.value()
|
data = bound_field.value()
|
||||||
|
@ -7,7 +7,7 @@ from django.http import Http404, HttpResponseRedirect
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from .api import is_api_request
|
from .api import is_api_request
|
||||||
from .views import server_error
|
from .views import server_error, rest_api_server_error
|
||||||
|
|
||||||
|
|
||||||
class LoginRequiredMiddleware(object):
|
class LoginRequiredMiddleware(object):
|
||||||
@ -86,6 +86,10 @@ class ExceptionHandlingMiddleware(object):
|
|||||||
if isinstance(exception, Http404):
|
if isinstance(exception, Http404):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Handle exceptions that occur from REST API requests
|
||||||
|
if is_api_request(request):
|
||||||
|
return rest_api_server_error(request)
|
||||||
|
|
||||||
# Determine the type of exception. If it's a common issue, return a custom error page with instructions.
|
# Determine the type of exception. If it's a common issue, return a custom error page with instructions.
|
||||||
custom_template = None
|
custom_template = None
|
||||||
if isinstance(exception, ProgrammingError):
|
if isinstance(exception, ProgrammingError):
|
||||||
|
@ -23,7 +23,8 @@ class TreeNodeMultipleChoiceFilterTest(TestCase):
|
|||||||
class SiteFilterSet(django_filters.FilterSet):
|
class SiteFilterSet(django_filters.FilterSet):
|
||||||
region = TreeNodeMultipleChoiceFilter(
|
region = TreeNodeMultipleChoiceFilter(
|
||||||
queryset=Region.objects.all(),
|
queryset=Region.objects.all(),
|
||||||
field_name='region__in',
|
field_name='region',
|
||||||
|
lookup_expr='in',
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -13,7 +13,7 @@ from django.core.exceptions import FieldDoesNotExist, ImproperlyConfigured, Obje
|
|||||||
from django.db import transaction, IntegrityError
|
from django.db import transaction, IntegrityError
|
||||||
from django.db.models import ManyToManyField, ProtectedError
|
from django.db.models import ManyToManyField, ProtectedError
|
||||||
from django.forms import Form, ModelMultipleChoiceField, MultipleHiddenInput, Textarea
|
from django.forms import Form, ModelMultipleChoiceField, MultipleHiddenInput, Textarea
|
||||||
from django.http import HttpResponse, HttpResponseServerError
|
from django.http import HttpResponse, HttpResponseServerError, JsonResponse
|
||||||
from django.shortcuts import get_object_or_404, redirect, render
|
from django.shortcuts import get_object_or_404, redirect, render
|
||||||
from django.template import loader
|
from django.template import loader
|
||||||
from django.template.exceptions import TemplateDoesNotExist
|
from django.template.exceptions import TemplateDoesNotExist
|
||||||
@ -27,6 +27,7 @@ from django.views.decorators.csrf import requires_csrf_token
|
|||||||
from django.views.defaults import ERROR_500_TEMPLATE_NAME
|
from django.views.defaults import ERROR_500_TEMPLATE_NAME
|
||||||
from django.views.generic import View
|
from django.views.generic import View
|
||||||
from django_tables2 import RequestConfig
|
from django_tables2 import RequestConfig
|
||||||
|
from rest_framework import status
|
||||||
|
|
||||||
from extras.models import CustomField, CustomFieldValue, ExportTemplate
|
from extras.models import CustomField, CustomFieldValue, ExportTemplate
|
||||||
from extras.querysets import CustomFieldQueryset
|
from extras.querysets import CustomFieldQueryset
|
||||||
@ -1423,8 +1424,22 @@ def server_error(request, template_name=ERROR_500_TEMPLATE_NAME):
|
|||||||
type_, error, traceback = sys.exc_info()
|
type_, error, traceback = sys.exc_info()
|
||||||
|
|
||||||
return HttpResponseServerError(template.render({
|
return HttpResponseServerError(template.render({
|
||||||
'python_version': platform.python_version(),
|
|
||||||
'netbox_version': settings.VERSION,
|
|
||||||
'exception': str(type_),
|
|
||||||
'error': error,
|
'error': error,
|
||||||
|
'exception': str(type_),
|
||||||
|
'netbox_version': settings.VERSION,
|
||||||
|
'python_version': platform.python_version(),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
|
||||||
|
def rest_api_server_error(request, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Handle exceptions and return a useful error message for REST API requests.
|
||||||
|
"""
|
||||||
|
type_, error, traceback = sys.exc_info()
|
||||||
|
data = {
|
||||||
|
'error': str(error),
|
||||||
|
'exception': type_.__name__,
|
||||||
|
'netbox_version': settings.VERSION,
|
||||||
|
'python_version': platform.python_version(),
|
||||||
|
}
|
||||||
|
return JsonResponse(data, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
@ -79,9 +79,19 @@ class ClusterForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
|||||||
queryset=ClusterGroup.objects.all(),
|
queryset=ClusterGroup.objects.all(),
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
|
region = DynamicModelChoiceField(
|
||||||
|
queryset=Region.objects.all(),
|
||||||
|
required=False,
|
||||||
|
initial_params={
|
||||||
|
'sites': '$site'
|
||||||
|
}
|
||||||
|
)
|
||||||
site = DynamicModelChoiceField(
|
site = DynamicModelChoiceField(
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
required=False
|
required=False,
|
||||||
|
query_params={
|
||||||
|
'region_id': '$region'
|
||||||
|
}
|
||||||
)
|
)
|
||||||
comments = CommentField()
|
comments = CommentField()
|
||||||
tags = DynamicModelMultipleChoiceField(
|
tags = DynamicModelMultipleChoiceField(
|
||||||
@ -92,7 +102,7 @@ class ClusterForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Cluster
|
model = Cluster
|
||||||
fields = (
|
fields = (
|
||||||
'name', 'type', 'group', 'tenant', 'site', 'comments', 'tags',
|
'name', 'type', 'group', 'tenant', 'region', 'site', 'comments', 'tags',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -143,9 +153,17 @@ class ClusterBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEdit
|
|||||||
queryset=Tenant.objects.all(),
|
queryset=Tenant.objects.all(),
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
|
region = DynamicModelChoiceField(
|
||||||
|
queryset=Region.objects.all(),
|
||||||
|
required=False,
|
||||||
|
to_field_name='slug'
|
||||||
|
)
|
||||||
site = DynamicModelChoiceField(
|
site = DynamicModelChoiceField(
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
required=False
|
required=False,
|
||||||
|
query_params={
|
||||||
|
'region': '$region'
|
||||||
|
}
|
||||||
)
|
)
|
||||||
comments = CommentField(
|
comments = CommentField(
|
||||||
widget=SmallTextarea,
|
widget=SmallTextarea,
|
||||||
@ -266,7 +284,10 @@ class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
|||||||
cluster_group = DynamicModelChoiceField(
|
cluster_group = DynamicModelChoiceField(
|
||||||
queryset=ClusterGroup.objects.all(),
|
queryset=ClusterGroup.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
null_option='None'
|
null_option='None',
|
||||||
|
initial_params={
|
||||||
|
'clusters': '$cluster'
|
||||||
|
}
|
||||||
)
|
)
|
||||||
cluster = DynamicModelChoiceField(
|
cluster = DynamicModelChoiceField(
|
||||||
queryset=Cluster.objects.all(),
|
queryset=Cluster.objects.all(),
|
||||||
@ -311,14 +332,6 @@ class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
|
||||||
# Initialize helper selector
|
|
||||||
instance = kwargs.get('instance')
|
|
||||||
if instance.pk and instance.cluster is not None:
|
|
||||||
initial = kwargs.get('initial', {}).copy()
|
|
||||||
initial['cluster_group'] = instance.cluster.group
|
|
||||||
kwargs['initial'] = initial
|
|
||||||
|
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
if self.instance.pk:
|
if self.instance.pk:
|
||||||
|
Loading…
Reference in New Issue
Block a user