Compare commits

...

54 Commits

Author SHA1 Message Date
Jeremy Stretch
56d9725c39 Merge pull request #10570 from netbox-community/develop
Release v3.3.5
2022-10-05 10:10:44 -04:00
jeremystretch
1c69bfaf2c Release v3.3.5 2022-10-05 09:47:55 -04:00
jeremystretch
bdefd8ea8c Fixes #10562: Correct URL for contacts table tags column 2022-10-05 08:13:33 -04:00
jeremystretch
03946f2ca8 Fixes #10559: Permit the pinning of a VM to a particular device within a cluster which has no site assignment 2022-10-04 15:46:55 -04:00
jeremystretch
fec8d1bc2f Fixes #10423: Enforce object type validation when creating journal entries 2022-10-04 15:26:52 -04:00
jeremystretch
53f5f46037 #10460: Fix PowerFeed details 2022-10-04 14:36:14 -04:00
jeremystretch
eef5cefb5d Fixes #10460: Restore missing connection details for device components 2022-10-03 16:11:24 -04:00
jeremystretch
7712b81ab9 Fixes #10517: Automatically inherit site assignment from cluster when creating a virtual machine 2022-10-03 15:35:45 -04:00
jeremystretch
7feb86fe55 Changelog for #10352 2022-10-03 15:03:28 -04:00
PieterL75
d1efbf6620 Issue10352 removegetvariables (#10475)
* Add javascript to disable empty form fields

* add js cleanGetUrl

* use addEventListener submit

* use addEventListener

* update collectstatics

* Use FormData to remove empty fields

* optimeze ts-ignore

* update ts-ignore comment

* oneline of ts-ignore

* one line of ts-ingnore

* fix tsc errors by adding types (as per kkthxbye)

Co-authored-by: Pieter Lambrecht <pieter.lambrecht@sentia.com>
2022-10-03 14:32:01 -04:00
jeremystretch
aabee05a6a Changelog for #8424, #10491 2022-10-03 13:58:04 -04:00
jeremystretch
cf062b5b6a Closes #10346: Document how to access plugin config parameters 2022-10-03 13:56:46 -04:00
Arthur Hanson
0b6a3898fe 8424 device location (#10544)
* 8424 fix merge

* 8424 fix merge

* 8424 fix merge

* 8424 fix merge
2022-10-03 13:55:05 -04:00
Jeremy Stretch
517ebcfbcd Merge pull request #10525 from netbox-community/10491-delete-dependant
10491 improve error message for ProtectedError on contact assignment
2022-10-03 13:27:34 -04:00
jeremystretch
9ef24d3f43 Fixes #10513: Disable the reassignment of a module to a new device 2022-10-03 11:11:51 -04:00
Arthur
02ffc2ddee 10491 improve error message for ProtectedError on contact assignment 2022-09-30 09:09:21 -07:00
jeremystretch
62820ea2b8 Add workflow_dispatch event 2022-09-29 12:36:10 -04:00
jeremystretch
04738587e8 Move permissions block to root 2022-09-29 12:17:10 -04:00
jeremystretch
cbbfcd0e7b Bump stale to v6 2022-09-29 12:00:44 -04:00
jeremystretch
309a70df89 Tweak workflow permissions 2022-09-29 11:59:15 -04:00
Alex
4cb6984a65 GitHub Workflows security hardening (#10456)
* build: harden lock.yml permissions

Signed-off-by: Alex <aleksandrosansan@gmail.com>

* build: harden stale.yml permissions

Signed-off-by: Alex <aleksandrosansan@gmail.com>

* build: harden ci.yml permissions

Signed-off-by: Alex <aleksandrosansan@gmail.com>

Signed-off-by: Alex <aleksandrosansan@gmail.com>
2022-09-29 11:41:33 -04:00
jeremystretch
3c32c09a5a Fixes #10496: Use page.canonical_url to identify ReadTheDocs builds 2022-09-28 09:30:38 -04:00
jeremystretch
2d9852d6f1 Fixes #10408: Fix validation when attempting to add redundant contact assignments 2022-09-27 13:11:57 -04:00
jeremystretch
05542324fc Changelog for #10465, #10480 2022-09-27 11:53:11 -04:00
Patrick Hurrelmann
669e86f96e Fixes: #10465 Format all remaining displayed rackunits with floatformat (#10481)
* Fixes: #10465 Try to finish #10268 and format all remaining displayed rackunits with floatformat

* #10465: PEP8 fix

Co-authored-by: Patrick Hurrelmann <patrick.hurrelmann@nfon.com>
Co-authored-by: jeremystretch <jstretch@ns1.com>
2022-09-27 11:24:19 -04:00
Jeremy Stretch
cbf928f363 Merge pull request #10482 from phurrelmann/10480-fix-link-target-on-cable-svg
Fixes: #10480 Fix link-target on cable-trace svg
2022-09-27 11:07:14 -04:00
Patrick Hurrelmann
43b18c13e3 Fixes: #10480 Fix link-target on cable-trace svg to open link in the same window. 2022-09-27 13:23:51 +02:00
jeremystretch
dda193247a Fixes #10470: Omit read-only custom fields from CSV import forms 2022-09-26 16:47:34 -04:00
jeremystretch
2463e4efd3 Fixes #10461: Enable filtering by read-only custom fields in the UI 2022-09-26 16:42:11 -04:00
jeremystretch
a0b17887fd Fixes #10445: Avoid rounding virtual machine memory values 2022-09-26 15:45:58 -04:00
jeremystretch
96784640e3 Changelog for #10435, #10439 2022-09-26 10:27:35 -04:00
Jeremy Stretch
b75d12fe05 Merge pull request #10442 from netbox-community/10435-untagged-vlan
10435 check if vm.cluster in qs
2022-09-26 10:25:48 -04:00
Jeremy Stretch
5e389c32ed Merge pull request #10463 from netbox-community/revert-10410-10408-add-contact
Revert "10408 add error message if already exists"
2022-09-26 10:24:54 -04:00
Jeremy Stretch
fd89ef04b6 Revert "10408 add error message if already exists" 2022-09-26 10:24:40 -04:00
Jeremy Stretch
abcc10e938 Merge pull request #10410 from netbox-community/10408-add-contact
10408 add error message if already exists
2022-09-26 10:24:23 -04:00
jeremystretch
3ad337dd15 Filter VLANs and VLANGroups by site or cluster site for VM 2022-09-26 10:08:54 -04:00
Jeremy Stretch
a527767caa Merge pull request #10455 from miaow2/10439-airlow-widget
10439 Add widget for Airflow field in DeviceTypeForm
2022-09-26 09:20:48 -04:00
Arthur Hanson
39129ecedf 10407 fix documentation link to requests (#10409)
* 10407 fix documentation link to requests

* Append page heading to URL

Co-authored-by: jeremystretch <jstretch@ns1.com>
2022-09-26 09:17:02 -04:00
Artem I. Kotik
c97d2d4fe9 Add widget for Airflow field in DeviceTypeForm 2022-09-24 15:49:23 +04:00
Arthur
7735634649 10435 check if vm.cluster in qs 2022-09-22 10:34:37 -07:00
Jonathan Senecal
148c6a6c23 Merge pull request #10432 from netbox-community/10431-pylance-is-no-longer-working-by-default-in-vscode
Add [tool.pyright] to pyproject.toml and fix #10431
2022-09-21 15:33:25 -04:00
Jonathan Senecal
360172cad0 Add [tool.pyright] to pyproject.toml 2022-09-21 15:19:40 -04:00
Daniel Sheppard
75c91232b4 Update changelog for #9497 2022-09-20 09:49:46 -05:00
Daniel Sheppard
0190c0225e Merge pull request #10420 from netbox-community/9497-fix-site-location-nonracked-device-display
Fixes #9497 - Change non-racked filter for sites/locations
2022-09-20 09:48:09 -05:00
Daniel Sheppard
86d366be4d Fixes #9651 - Document Pre-Change process for scripts 2022-09-20 09:46:23 -05:00
Daniel Sheppard
71d71a6b1b Fixes #9497 - Change filter for sites/locations 2022-09-20 09:26:40 -05:00
Arthur
695ad47fe9 10408 add error message if already exists 2022-09-19 10:46:16 -07:00
jeremystretch
1b62c11db5 PRVB 2022-09-16 13:41:09 -04:00
Jeremy Stretch
83a66a672d Merge pull request #10391 from netbox-community/develop
Release v3.3.4
2022-09-16 13:39:06 -04:00
jeremystretch
30b9ddc251 Release v3.3.4 2022-09-16 13:14:14 -04:00
jeremystretch
4a9831bd23 Fixes #10383: Fix assignment of component templates to module types via web UI 2022-09-16 08:30:46 -04:00
jeremystretch
59388d89a0 Fixes #10387: Fix MultiValueDictKeyError exception when editing a device interface 2022-09-16 08:20:24 -04:00
jeremystretch
1d033bd286 Update lock threads action; lock closed PRs after 30 days 2022-09-15 16:08:43 -04:00
jeremystretch
935f008c16 PRVB 2022-09-15 14:29:43 -04:00
49 changed files with 428 additions and 545 deletions

View File

@@ -14,7 +14,7 @@ body:
attributes: attributes:
label: NetBox version label: NetBox version
description: What version of NetBox are you currently running? description: What version of NetBox are you currently running?
placeholder: v3.3.3 placeholder: v3.3.5
validations: validations:
required: true required: true
- type: dropdown - type: dropdown

View File

@@ -14,7 +14,7 @@ body:
attributes: attributes:
label: NetBox version label: NetBox version
description: What version of NetBox are you currently running? description: What version of NetBox are you currently running?
placeholder: v3.3.3 placeholder: v3.3.5
validations: validations:
required: true required: true
- type: dropdown - type: dropdown

View File

@@ -1,5 +1,7 @@
name: CI name: CI
on: [push, pull_request] on: [push, pull_request]
permissions:
contents: read
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest

View File

@@ -4,18 +4,18 @@ name: 'Lock threads'
on: on:
schedule: schedule:
- cron: '0 3 * * *' - cron: '0 3 * * *'
workflow_dispatch:
permissions:
issues: write
pull-requests: write
jobs: jobs:
lock: lock:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: dessant/lock-threads@v2 - uses: dessant/lock-threads@v3
with: with:
github-token: ${{ github.token }} issue-inactive-days: 90
issue-lock-inactive-days: '90' pr-inactive-days: 30
issue-exclude-created-before: ''
issue-exclude-labels: ''
issue-lock-labels: ''
issue-lock-comment: ''
issue-lock-reason: 'resolved' issue-lock-reason: 'resolved'
process-only: 'issues'

View File

@@ -1,14 +1,21 @@
# close-stale-issues (https://github.com/marketplace/actions/close-stale-issues) # close-stale-issues (https://github.com/marketplace/actions/close-stale-issues)
name: 'Close stale issues/PRs' name: 'Close stale issues/PRs'
on: on:
schedule: schedule:
- cron: '0 4 * * *' - cron: '0 4 * * *'
workflow_dispatch:
permissions:
issues: write
pull-requests: write
jobs: jobs:
stale: stale:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/stale@v5 - uses: actions/stale@v6
with: with:
close-issue-message: > close-issue-message: >
This issue has been automatically closed due to lack of activity. In an This issue has been automatically closed due to lack of activity. In an

View File

@@ -68,7 +68,7 @@ drf-yasg[validation]
# Django wrapper for Graphene (GraphQL support) # Django wrapper for Graphene (GraphQL support)
# https://github.com/graphql-python/graphene-django # https://github.com/graphql-python/graphene-django
graphene_django graphene_django<3.0
# WSGI HTTP server # WSGI HTTP server
# https://gunicorn.org/ # https://gunicorn.org/
@@ -80,7 +80,8 @@ Jinja2
# Simple markup language for rendering HTML # Simple markup language for rendering HTML
# https://github.com/Python-Markdown/markdown # https://github.com/Python-Markdown/markdown
Markdown # mkdocs currently requires Markdown v3.3
Markdown<3.4
# File inclusion plugin for Python-Markdown # File inclusion plugin for Python-Markdown
# https://github.com/cmacmackin/markdown-include # https://github.com/cmacmackin/markdown-include

View File

@@ -2,8 +2,8 @@
{% block site_meta %} {% block site_meta %}
{{ super() }} {{ super() }}
{# Disable search indexing unless we're building for ReadTheDocs #} {# Disable search indexing unless we're building for ReadTheDocs (see #10496) #}
{% if not config.extra.readthedocs %} {% if page.canonical_url != 'https://docs.netbox.dev/' %}
<meta name="robots" content="noindex"> <meta name="robots" content="noindex">
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View File

@@ -58,7 +58,7 @@ Email is sent from NetBox only for critical events or if configured for [logging
Default: None Default: None
A dictionary of HTTP proxies to use for outbound requests originating from NetBox (e.g. when sending webhook requests). Proxies should be specified by schema (HTTP and HTTPS) as per the [Python requests library documentation](https://2.python-requests.org/en/master/user/advanced/). For example: A dictionary of HTTP proxies to use for outbound requests originating from NetBox (e.g. when sending webhook requests). Proxies should be specified by schema (HTTP and HTTPS) as per the [Python requests library documentation](https://requests.readthedocs.io/en/latest/user/advanced/#proxies). For example:
```python ```python
HTTP_PROXIES = { HTTP_PROXIES = {

View File

@@ -129,6 +129,19 @@ The Script object provides a set of convenient functions for recording messages
Log messages are returned to the user upon execution of the script. Markdown rendering is supported for log messages. Log messages are returned to the user upon execution of the script. Markdown rendering is supported for log messages.
## Change Logging
To generate the correct change log data when editing an existing object, a snapshot of the object must be taken before making any changes to the object.
```python
if obj.pk and hasattr(obj, 'snapshot'):
obj.snapshot()
obj.property = "New Value"
obj.full_clean()
obj.save()
```
## Variable Reference ## Variable Reference
### Default Options ### Default Options

View File

@@ -112,6 +112,14 @@ NetBox looks for the `config` variable within a plugin's `__init__.py` to load i
All required settings must be configured by the user. If a configuration parameter is listed in both `required_settings` and `default_settings`, the default setting will be ignored. All required settings must be configured by the user. If a configuration parameter is listed in both `required_settings` and `default_settings`, the default setting will be ignored.
!!! tip "Accessing Config Parameters"
Plugin configuration parameters can be accessed in `settings.PLUGINS_CONFIG`, mapped by plugin name. For example:
```python
from django.conf import settings
settings.PLUGINS_CONFIG['myplugin']['verbose_name']
```
## Create setup.py ## Create setup.py
`setup.py` is the [setup script](https://docs.python.org/3.8/distutils/setupscript.html) used to package and install our plugin once it's finished. The primary function of this script is to call the setuptools library's `setup()` function to create a Python distribution package. We can pass a number of keyword arguments to control the package creation as well as to provide metadata about the plugin. An example `setup.py` is below: `setup.py` is the [setup script](https://docs.python.org/3.8/distutils/setupscript.html) used to package and install our plugin once it's finished. The primary function of this script is to call the setuptools library's `setup()` function to create a Python distribution package. We can pass a number of keyword arguments to control the package creation as well as to provide metadata about the plugin. An example `setup.py` is below:

View File

@@ -1,5 +1,42 @@
# NetBox v3.3 # NetBox v3.3
## v3.3.5 (2022-10-05)
### Enhancements
* [#8424](https://github.com/netbox-community/netbox/issues/8424) - Include rack elevation under device view
* [#10352](https://github.com/netbox-community/netbox/issues/10352) - Omit extraneous URL query attributes during search
* [#10465](https://github.com/netbox-community/netbox/issues/10465) - Improve formatting of device heights and rack positions
### Bug Fixes
* [#9497](https://github.com/netbox-community/netbox/issues/9497) - Adjust non-racked device filter on site and location detailed view
* [#10408](https://github.com/netbox-community/netbox/issues/10408) - Fix validation when attempting to add redundant contact assignments
* [#10423](https://github.com/netbox-community/netbox/issues/10423) - Enforce object type validation when creating journal entries
* [#10435](https://github.com/netbox-community/netbox/issues/10435) - Fix exception when filtering VLANs by virtual machine with no cluster assigned
* [#10439](https://github.com/netbox-community/netbox/issues/10439) - Fix form widget styling for DeviceType airflow field
* [#10445](https://github.com/netbox-community/netbox/issues/10445) - Avoid rounding virtual machine memory values
* [#10460](https://github.com/netbox-community/netbox/issues/10460) - Restore missing connection details for device components
* [#10461](https://github.com/netbox-community/netbox/issues/10461) - Enable filtering by read-only custom fields in the UI
* [#10470](https://github.com/netbox-community/netbox/issues/10470) - Omit read-only custom fields from CSV import forms
* [#10480](https://github.com/netbox-community/netbox/issues/10480) - Cable trace SVG links should not force a new window
* [#10491](https://github.com/netbox-community/netbox/issues/10491) - Clarify representation of blocking contact assignments during contact deletion
* [#10513](https://github.com/netbox-community/netbox/issues/10513) - Disable the reassignment of a module to a new device
* [#10517](https://github.com/netbox-community/netbox/issues/10517) - Automatically inherit site assignment from cluster when creating a virtual machine
* [#10559](https://github.com/netbox-community/netbox/issues/10559) - Permit the pinning of a VM to a particular device within a cluster which has no site assignment
* [#10562](https://github.com/netbox-community/netbox/issues/10562) - Correct URL for contacts table tags column
---
## v3.3.4 (2022-09-16)
### Bug Fixes
* [#10383](https://github.com/netbox-community/netbox/issues/10383) - Fix assignment of component templates to module types via web UI
* [#10387](https://github.com/netbox-community/netbox/issues/10387) - Fix `MultiValueDictKeyError` exception when editing a device interface
---
## v3.3.3 (2022-09-15) ## v3.3.3 (2022-09-15)
### Enhancements ### Enhancements

View File

@@ -38,7 +38,6 @@ plugins:
show_root_toc_entry: false show_root_toc_entry: false
show_source: false show_source: false
extra: extra:
readthedocs: !ENV READTHEDOCS
social: social:
- icon: fontawesome/brands/github - icon: fontawesome/brands/github
link: https://github.com/netbox-community/netbox link: https://github.com/netbox-community/netbox

View File

@@ -373,6 +373,7 @@ class DeviceTypeForm(NetBoxModelForm):
'front_image', 'rear_image', 'comments', 'tags', 'front_image', 'rear_image', 'comments', 'tags',
] ]
widgets = { widgets = {
'airflow': StaticSelect(),
'subdevice_role': StaticSelect(), 'subdevice_role': StaticSelect(),
'front_image': ClearableFileInput(attrs={ 'front_image': ClearableFileInput(attrs={
'accept': DEVICETYPE_IMAGE_FORMATS 'accept': DEVICETYPE_IMAGE_FORMATS
@@ -678,6 +679,7 @@ class ModuleForm(NetBoxModelForm):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
if self.instance.pk: if self.instance.pk:
self.fields['device'].disabled = True
self.fields['replicate_components'].initial = False self.fields['replicate_components'].initial = False
self.fields['replicate_components'].disabled = True self.fields['replicate_components'].disabled = True
self.fields['adopt_components'].initial = False self.fields['adopt_components'].initial = False
@@ -1000,11 +1002,22 @@ class ComponentTemplateForm(BootstrapMixin, forms.ModelForm):
class ModularComponentTemplateForm(ComponentTemplateForm): class ModularComponentTemplateForm(ComponentTemplateForm):
device_type = DynamicModelChoiceField(
queryset=DeviceType.objects.all().all(),
required=False
)
module_type = DynamicModelChoiceField( module_type = DynamicModelChoiceField(
queryset=ModuleType.objects.all(), queryset=ModuleType.objects.all(),
required=False required=False
) )
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Disable reassignment of ModuleType when editing an existing instance
if self.instance.pk:
self.fields['module_type'].disabled = True
class ConsolePortTemplateForm(ModularComponentTemplateForm): class ConsolePortTemplateForm(ModularComponentTemplateForm):
fieldsets = ( fieldsets = (
@@ -1429,16 +1442,6 @@ class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
'rf_channel_width': "Populated by selected channel (if set)", 'rf_channel_width': "Populated by selected channel (if set)",
} }
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Restrict LAG/bridge interface assignment by device/VC
device_id = self.data['device'] if self.is_bound else self.initial.get('device')
device = Device.objects.filter(pk=device_id).first()
if device and device.virtual_chassis and device.virtual_chassis.master:
self.fields['lag'].widget.add_query_param('device_id', device.virtual_chassis.master.pk)
self.fields['bridge'].widget.add_query_param('device_id', device.virtual_chassis.master.pk)
class FrontPortForm(ModularDeviceComponentForm): class FrontPortForm(ModularDeviceComponentForm):
rear_port = DynamicModelChoiceField( rear_port = DynamicModelChoiceField(

View File

@@ -987,6 +987,14 @@ class Module(NetBoxModel, ConfigContextModel):
def get_absolute_url(self): def get_absolute_url(self):
return reverse('dcim:module', args=[self.pk]) return reverse('dcim:module', args=[self.pk])
def clean(self):
super().clean()
if self.module_bay.device != self.device:
raise ValidationError(
f"Module must be installed within a module bay belonging to the assigned device ({self.device})."
)
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
is_new = self.pk is None is_new = self.pk is None

View File

@@ -35,7 +35,7 @@ class Node(Hyperlink):
""" """
def __init__(self, position, width, url, color, labels, radius=10, **extra): def __init__(self, position, width, url, color, labels, radius=10, **extra):
super(Node, self).__init__(href=url, target='_blank', **extra) super(Node, self).__init__(href=url, target='_parent', **extra)
x, y = position x, y = position

View File

@@ -9,6 +9,7 @@ from svgwrite.text import Text
from django.conf import settings from django.conf import settings
from django.core.exceptions import FieldError from django.core.exceptions import FieldError
from django.db.models import Q from django.db.models import Q
from django.template.defaultfilters import floatformat
from django.urls import reverse from django.urls import reverse
from django.utils.http import urlencode from django.utils.http import urlencode
@@ -41,7 +42,7 @@ def get_device_description(device):
device.device_role, device.device_role,
device.device_type.manufacturer.name, device.device_type.manufacturer.name,
device.device_type.model, device.device_type.model,
device.device_type.u_height, floatformat(device.device_type.u_height),
device.asset_tag or '', device.asset_tag or '',
device.serial or '' device.serial or ''
) )

View File

@@ -85,6 +85,9 @@ class DeviceTypeTable(NetBoxTable):
tags = columns.TagColumn( tags = columns.TagColumn(
url_name='dcim:devicetype_list' url_name='dcim:devicetype_list'
) )
u_height = columns.TemplateColumn(
template_code='{{ value|floatformat }}'
)
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = DeviceType model = DeviceType

View File

@@ -1778,10 +1778,12 @@ class ModuleTestCase(
ModuleBay(device=devices[0], name='Module Bay 2'), ModuleBay(device=devices[0], name='Module Bay 2'),
ModuleBay(device=devices[0], name='Module Bay 3'), ModuleBay(device=devices[0], name='Module Bay 3'),
ModuleBay(device=devices[0], name='Module Bay 4'), ModuleBay(device=devices[0], name='Module Bay 4'),
ModuleBay(device=devices[0], name='Module Bay 5'),
ModuleBay(device=devices[1], name='Module Bay 1'), ModuleBay(device=devices[1], name='Module Bay 1'),
ModuleBay(device=devices[1], name='Module Bay 2'), ModuleBay(device=devices[1], name='Module Bay 2'),
ModuleBay(device=devices[1], name='Module Bay 3'), ModuleBay(device=devices[1], name='Module Bay 3'),
ModuleBay(device=devices[1], name='Module Bay 4'), ModuleBay(device=devices[1], name='Module Bay 4'),
ModuleBay(device=devices[1], name='Module Bay 5'),
) )
ModuleBay.objects.bulk_create(module_bays) ModuleBay.objects.bulk_create(module_bays)
@@ -1795,7 +1797,7 @@ class ModuleTestCase(
tags = create_tags('Alpha', 'Bravo', 'Charlie') tags = create_tags('Alpha', 'Bravo', 'Charlie')
cls.form_data = { cls.form_data = {
'device': devices[1].pk, 'device': devices[0].pk,
'module_bay': module_bays[3].pk, 'module_bay': module_bays[3].pk,
'module_type': module_types[0].pk, 'module_type': module_types[0].pk,
'serial': 'A', 'serial': 'A',
@@ -1867,7 +1869,6 @@ class ModuleTestCase(
self.assertIsNone(interface.module) self.assertIsNone(interface.module)
# Create a module with adopted components # Create a module with adopted components
form_data['module_bay'] = ModuleBay.objects.filter(device=device).first()
form_data['module_type'] = module_type form_data['module_type'] = module_type
form_data['replicate_components'] = False form_data['replicate_components'] = False
form_data['adopt_components'] = True form_data['adopt_components'] = True

View File

@@ -355,7 +355,7 @@ class SiteView(generic.ObjectView):
nonracked_devices = Device.objects.filter( nonracked_devices = Device.objects.filter(
site=instance, site=instance,
position__isnull=True, rack__isnull=True,
parent_bay__isnull=True parent_bay__isnull=True
).prefetch_related('device_type__manufacturer', 'parent_bay', 'device_role') ).prefetch_related('device_type__manufacturer', 'parent_bay', 'device_role')
@@ -450,7 +450,7 @@ class LocationView(generic.ObjectView):
nonracked_devices = Device.objects.filter( nonracked_devices = Device.objects.filter(
location=instance, location=instance,
position__isnull=True, rack__isnull=True,
parent_bay__isnull=True parent_bay__isnull=True
).prefetch_related('device_type__manufacturer', 'parent_bay', 'device_role') ).prefetch_related('device_type__manufacturer', 'parent_bay', 'device_role')
@@ -1616,6 +1616,7 @@ class DeviceView(generic.ObjectView):
return { return {
'services': services, 'services': services,
'vc_members': vc_members, 'vc_members': vc_members,
'svg_extra': f'highlight=id:{instance.pk}'
} }

View File

@@ -34,7 +34,9 @@ class CustomFieldsMixin:
return ContentType.objects.get_for_model(self.model) return ContentType.objects.get_for_model(self.model)
def _get_custom_fields(self, content_type): def _get_custom_fields(self, content_type):
return CustomField.objects.filter(content_types=content_type) return CustomField.objects.filter(content_types=content_type).exclude(
ui_visibility=CustomFieldVisibilityChoices.VISIBILITY_HIDDEN
)
def _get_form_field(self, customfield): def _get_form_field(self, customfield):
return customfield.to_form_field() return customfield.to_form_field()
@@ -50,13 +52,6 @@ class CustomFieldsMixin:
field_name = f'cf_{customfield.name}' field_name = f'cf_{customfield.name}'
self.fields[field_name] = self._get_form_field(customfield) self.fields[field_name] = self._get_form_field(customfield)
if customfield.ui_visibility == CustomFieldVisibilityChoices.VISIBILITY_READ_ONLY:
self.fields[field_name].disabled = True
if self.fields[field_name].help_text:
self.fields[field_name].help_text += '<br />'
self.fields[field_name].help_text += '<i class="mdi mdi-alert-circle-outline"></i> ' \
'Field is set to read-only.'
# Annotate the field in the list of CustomField form fields # Annotate the field in the list of CustomField form fields
self.custom_fields[field_name] = customfield self.custom_fields[field_name] = customfield
if customfield.group_name not in self.custom_field_groups: if customfield.group_name not in self.custom_field_groups:

View File

@@ -297,12 +297,13 @@ class CustomField(CloningMixin, ExportTemplatesMixin, WebhooksMixin, ChangeLogge
return model.objects.filter(pk__in=value) return model.objects.filter(pk__in=value)
return value return value
def to_form_field(self, set_initial=True, enforce_required=True, for_csv_import=False): def to_form_field(self, set_initial=True, enforce_required=True, enforce_visibility=True, for_csv_import=False):
""" """
Return a form field suitable for setting a CustomField's value for an object. Return a form field suitable for setting a CustomField's value for an object.
set_initial: Set initial data for the field. This should be False when generating a field for bulk editing. set_initial: Set initial data for the field. This should be False when generating a field for bulk editing.
enforce_required: Honor the value of CustomField.required. Set to False for filtering/bulk editing. enforce_required: Honor the value of CustomField.required. Set to False for filtering/bulk editing.
enforce_visibility: Honor the value of CustomField.ui_visibility. Set to False for filtering.
for_csv_import: Return a form field suitable for bulk import of objects in CSV format. for_csv_import: Return a form field suitable for bulk import of objects in CSV format.
""" """
initial = self.default if set_initial else None initial = self.default if set_initial else None
@@ -398,6 +399,12 @@ class CustomField(CloningMixin, ExportTemplatesMixin, WebhooksMixin, ChangeLogge
if self.description: if self.description:
field.help_text = escape(self.description) field.help_text = escape(self.description)
# Annotate read-only fields
if enforce_visibility and self.ui_visibility == CustomFieldVisibilityChoices.VISIBILITY_READ_ONLY:
field.disabled = True
prepend = '<br />' if field.help_text else ''
field.help_text += f'{prepend}<i class="mdi mdi-alert-circle-outline"></i> Field is set to read-only.'
return field return field
def to_filter(self, lookup_expr=None): def to_filter(self, lookup_expr=None):

View File

@@ -463,6 +463,14 @@ class JournalEntry(CustomFieldsMixin, CustomLinksMixin, TagsMixin, WebhooksMixin
def get_absolute_url(self): def get_absolute_url(self):
return reverse('extras:journalentry', args=[self.pk]) return reverse('extras:journalentry', args=[self.pk])
def clean(self):
super().clean()
# Prevent the creation of journal entries on unsupported models
permitted_types = ContentType.objects.filter(FeatureQuery('journaling').get_query())
if self.assigned_object_type not in permitted_types:
raise ValidationError(f"Journaling is not supported for this object type ({self.assigned_object_type}).")
def get_kind_color(self): def get_kind_color(self):
return JournalEntryKindChoices.colors.get(self.kind) return JournalEntryKindChoices.colors.get(self.kind)

View File

@@ -81,29 +81,33 @@ class VLANQuerySet(RestrictedQuerySet):
# Find all relevant VLANGroups # Find all relevant VLANGroups
q = Q() q = Q()
if vm.cluster.site: site = vm.site or vm.cluster.site
if vm.cluster.site.region: if vm.cluster:
# Add VLANGroups scoped to the assigned cluster (or its group)
q |= Q( q |= Q(
scope_type=ContentType.objects.get_by_natural_key('dcim', 'region'), scope_type=ContentType.objects.get_by_natural_key('virtualization', 'cluster'),
scope_id__in=vm.cluster.site.region.get_ancestors(include_self=True) scope_id=vm.cluster_id
)
if vm.cluster.site.group:
q |= Q(
scope_type=ContentType.objects.get_by_natural_key('dcim', 'sitegroup'),
scope_id__in=vm.cluster.site.group.get_ancestors(include_self=True)
)
q |= Q(
scope_type=ContentType.objects.get_by_natural_key('dcim', 'site'),
scope_id=vm.cluster.site_id
) )
if vm.cluster.group: if vm.cluster.group:
q |= Q( q |= Q(
scope_type=ContentType.objects.get_by_natural_key('virtualization', 'clustergroup'), scope_type=ContentType.objects.get_by_natural_key('virtualization', 'clustergroup'),
scope_id=vm.cluster.group_id scope_id=vm.cluster.group_id
) )
if site:
# Add VLANGroups scoped to the assigned site (or its group or region)
q |= Q( q |= Q(
scope_type=ContentType.objects.get_by_natural_key('virtualization', 'cluster'), scope_type=ContentType.objects.get_by_natural_key('dcim', 'site'),
scope_id=vm.cluster_id scope_id=site.pk
)
if site.region:
q |= Q(
scope_type=ContentType.objects.get_by_natural_key('dcim', 'region'),
scope_id__in=site.region.get_ancestors(include_self=True)
)
if site.group:
q |= Q(
scope_type=ContentType.objects.get_by_natural_key('dcim', 'sitegroup'),
scope_id__in=site.group.get_ancestors(include_self=True)
) )
vlan_groups = VLANGroup.objects.filter(q) vlan_groups = VLANGroup.objects.filter(q)
@@ -113,7 +117,7 @@ class VLANQuerySet(RestrictedQuerySet):
Q(group__scope_id__isnull=True, site__isnull=True) | # Global group VLANs Q(group__scope_id__isnull=True, site__isnull=True) | # Global group VLANs
Q(group__isnull=True, site__isnull=True) # Global VLANs Q(group__isnull=True, site__isnull=True) # Global VLANs
) )
if vm.cluster.site: if site:
q |= Q(site=vm.cluster.site) q |= Q(site=site)
return self.filter(q) return self.filter(q)

View File

@@ -2,7 +2,7 @@ from django import forms
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.db.models import Q from django.db.models import Q
from extras.choices import CustomFieldFilterLogicChoices, CustomFieldTypeChoices from extras.choices import CustomFieldFilterLogicChoices, CustomFieldTypeChoices, CustomFieldVisibilityChoices
from extras.forms.customfields import CustomFieldsMixin from extras.forms.customfields import CustomFieldsMixin
from extras.models import CustomField, Tag from extras.models import CustomField, Tag
from utilities.forms import BootstrapMixin, CSVModelForm from utilities.forms import BootstrapMixin, CSVModelForm
@@ -63,6 +63,11 @@ class NetBoxModelCSVForm(CSVModelForm, NetBoxModelForm):
""" """
tags = None # Temporary fix in lieu of tag import support (see #9158) tags = None # Temporary fix in lieu of tag import support (see #9158)
def _get_custom_fields(self, content_type):
return CustomField.objects.filter(content_types=content_type).filter(
ui_visibility=CustomFieldVisibilityChoices.VISIBILITY_READ_WRITE
)
def _get_form_field(self, customfield): def _get_form_field(self, customfield):
return customfield.to_form_field(for_csv_import=True) return customfield.to_form_field(for_csv_import=True)
@@ -125,10 +130,10 @@ class NetBoxModelFilterSetForm(BootstrapMixin, CustomFieldsMixin, forms.Form):
) )
def _get_custom_fields(self, content_type): def _get_custom_fields(self, content_type):
return CustomField.objects.filter(content_types=content_type).exclude( return super()._get_custom_fields(content_type).exclude(
Q(filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED) | Q(filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED) |
Q(type=CustomFieldTypeChoices.TYPE_JSON) Q(type=CustomFieldTypeChoices.TYPE_JSON)
) )
def _get_form_field(self, customfield): def _get_form_field(self, customfield):
return customfield.to_form_field(set_initial=False, enforce_required=False) return customfield.to_form_field(set_initial=False, enforce_required=False, enforce_visibility=False)

View File

@@ -20,7 +20,6 @@ class NetBoxFeatureSet(
CustomLinksMixin, CustomLinksMixin,
CustomValidationMixin, CustomValidationMixin,
ExportTemplatesMixin, ExportTemplatesMixin,
JournalingMixin,
TagsMixin, TagsMixin,
WebhooksMixin WebhooksMixin
): ):
@@ -51,7 +50,7 @@ class ChangeLoggedModel(ChangeLoggingMixin, CustomValidationMixin, models.Model)
abstract = True abstract = True
class NetBoxModel(CloningMixin, NetBoxFeatureSet, models.Model): class NetBoxModel(CloningMixin, JournalingMixin, NetBoxFeatureSet, models.Model):
""" """
Primary models represent real objects within the infrastructure being modeled. Primary models represent real objects within the infrastructure being modeled.
""" """

View File

@@ -29,7 +29,7 @@ django.utils.encoding.force_text = force_str
# Environment setup # Environment setup
# #
VERSION = '3.3.3' VERSION = '3.3.5'
# Hostname # Hostname
HOSTNAME = platform.node() HOSTNAME = platform.node()

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -37,6 +37,20 @@ function initDocument(): void {
} }
function initWindow(): void { function initWindow(): void {
const documentForms = document.forms
for (var documentForm of documentForms) {
if (documentForm.method.toUpperCase() == 'GET') {
// @ts-ignore: Our version of typescript seems to be too old for FormDataEvent
documentForm.addEventListener('formdata', function(event: FormDataEvent) {
let formData: FormData = event.formData;
for (let [name, value] of Array.from(formData.entries())) {
if (value === '') formData.delete(name);
}
});
}
}
const contentContainer = document.querySelector<HTMLElement>('.content-container'); const contentContainer = document.querySelector<HTMLElement>('.content-container');
if (contentContainer !== null) { if (contentContainer !== null) {
// Focus the content container for accessible navigation. // Focus the content container for accessible navigation.

View File

@@ -55,52 +55,12 @@
</div> </div>
<div class="col col-md-6"> <div class="col col-md-6">
<div class="card"> <div class="card">
<h5 class="card-header"> <h5 class="card-header">Connection</h5>
Connection
</h5>
<div class="card-body"> <div class="card-body">
{% if object.mark_connected %} {% if object.mark_connected %}
<span class="text-success"><i class="mdi mdi-check-bold"></i></span> Marked as connected <span class="text-success"><i class="mdi mdi-check-bold"></i></span> Marked as connected
{% elif object.cable %} {% elif object.cable %}
<table class="table table-hover attr-table"> {% include 'dcim/inc/connection_endpoints.html' %}
<tr>
<th scope="row">Cable</th>
<td>
{{ object.cable|linkify }}
<a href="{% url 'dcim:consoleport_trace' pk=object.pk %}" class="btn btn-primary btn-sm lh-1" title="Trace">
<i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i>
</a>
</td>
</tr>
{% if object.connected_endpoint %}
<tr>
<th scope="row">Device</th>
<td>{{ object.connected_endpoint.device|linkify }}</td>
</tr>
<tr>
<th scope="row">Name</th>
<td>{{ object.connected_endpoint|linkify:"name" }}</td>
</tr>
<tr>
<th scope="row">Type</th>
<td>{{ object.connected_endpoint.get_type_display|placeholder }}</td>
</tr>
<tr>
<th scope="row">Description</th>
<td>{{ object.connected_endpoint.description|placeholder }}</td>
</tr>
<tr>
<th scope="row">Path Status</th>
<td>
{% if object.path.is_active %}
<span class="badge bg-success">Reachable</span>
{% else %}
<span class="badge bg-danger">Not Reachable</span>
{% endif %}
</td>
</tr>
{% endif %}
</table>
{% else %} {% else %}
<div class="text-muted"> <div class="text-muted">
Not Connected Not Connected

View File

@@ -55,54 +55,12 @@
</div> </div>
<div class="col col-md-6"> <div class="col col-md-6">
<div class="card"> <div class="card">
<h5 class="card-header"> <h5 class="card-header">Connection</h5>
Connection
</h5>
<div class="card-body"> <div class="card-body">
{% if object.mark_connected %} {% if object.mark_connected %}
<span class="text-success"><i class="mdi mdi-check-bold"></i></span> Marked as connected <span class="text-success"><i class="mdi mdi-check-bold"></i></span> Marked as connected
{% elif object.cable %} {% elif object.cable %}
<table class="table table-hover attr-table"> {% include 'dcim/inc/connection_endpoints.html' %}
<tr>
<th scope="row">Cable</th>
<td>
{{ object.cable|linkify }}
<a href="{% url 'dcim:consoleserverport_trace' pk=object.pk %}" class="btn btn-primary btn-sm lh-1" title="Trace">
<i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i>
</a>
</td>
</tr>
{% if object.connected_endpoint %}
<tr>
<th scope="row">Device</th>
<td>
{{ object.connected_endpoint.device|linkify }}
</td>
</tr>
<tr>
<th scope="row">Name</th>
<td>{{ object.connected_endpoint|linkify:"name" }}</td>
</tr>
<tr>
<th scope="row">Type</th>
<td>{{ object.connected_endpoint.get_type_display|placeholder }}</td>
</tr>
<tr>
<th scope="row">Description</th>
<td>{{ object.connected_endpoint.description|placeholder }}</td>
</tr>
<tr>
<th scope="row">Path Status</th>
<td>
{% if object.path.is_active %}
<span class="badge bg-success">Reachable</span>
{% else %}
<span class="badge bg-danger">Not Reachable</span>
{% endif %}
</td>
</tr>
{% endif %}
</table>
{% else %} {% else %}
<div class="text-muted"> <div class="text-muted">
Not Connected Not Connected

View File

@@ -7,7 +7,7 @@
{% block content %} {% block content %}
<div class="row"> <div class="row">
<div class="col col-md-6"> <div class="col col-12 col-xl-6">
<div class="card"> <div class="card">
<h5 class="card-header"> <h5 class="card-header">
Device Device
@@ -66,7 +66,7 @@
{% with object.parent_bay.device as parent %} {% with object.parent_bay.device as parent %}
{{ parent|linkify }} / {{ object.parent_bay }} {{ parent|linkify }} / {{ object.parent_bay }}
{% if parent.position %} {% if parent.position %}
(U{{ parent.position }} / {{ parent.get_face_display }}) (U{{ parent.position|floatformat }} / {{ parent.get_face_display }})
{% endif %} {% endif %}
{% endwith %} {% endwith %}
{% elif object.rack and object.position %} {% elif object.rack and object.position %}
@@ -90,7 +90,7 @@
<tr> <tr>
<th scope="row">Device Type</th> <th scope="row">Device Type</th>
<td> <td>
{{ object.device_type|linkify:"get_full_name" }} ({{ object.device_type.u_height }}U) {{ object.device_type|linkify:"get_full_name" }} ({{ object.device_type.u_height|floatformat }}U)
</td> </td>
</tr> </tr>
<tr> <tr>
@@ -153,7 +153,7 @@
{% include 'inc/panels/comments.html' %} {% include 'inc/panels/comments.html' %}
{% plugin_left_page object %} {% plugin_left_page object %}
</div> </div>
<div class="col col-md-6"> <div class="col col-12 col-xl-6">
<div class="card"> <div class="card">
<h5 class="card-header">Management</h5> <h5 class="card-header">Management</h5>
<div class="card-body"> <div class="card-body">
@@ -286,6 +286,22 @@
</div> </div>
{% include 'inc/panels/contacts.html' %} {% include 'inc/panels/contacts.html' %}
{% include 'inc/panels/image_attachments.html' %} {% include 'inc/panels/image_attachments.html' %}
{% if object.rack and object.position %}
<div class="row" style="margin-bottom: 20px">
<div class="col col-md-6 col-sm-6 col-xs-12 text-center">
<div style="margin-left: 30px">
<h4>Front</h4>
{% include 'dcim/inc/rack_elevation.html' with object=object.rack face='front' extra_params=svg_extra %}
</div>
</div>
<div class="col col-md-6 col-sm-6 col-xs-12 text-center">
<div style="margin-left: 30px">
<h4>Rear</h4>
{% include 'dcim/inc/rack_elevation.html' with object=object.rack face='rear' extra_params=svg_extra %}
</div>
</div>
</div>
{% endif %}
{% plugin_right_page object %} {% plugin_right_page object %}
</div> </div>
</div> </div>

View File

@@ -29,7 +29,7 @@
</tr> </tr>
<tr> <tr>
<td>Height (U)</td> <td>Height (U)</td>
<td>{{ object.u_height }}</td> <td>{{ object.u_height|floatformat }}</td>
</tr> </tr>
<tr> <tr>
<td>Full Depth</td> <td>Full Depth</td>

View File

@@ -1,14 +0,0 @@
<td>
{% if termination.parent_object.provider %}
<i class="mdi mdi-lightning-bolt" title="Circuit"></i>
<a href="{{ termination.parent_object.get_absolute_url }}">
{{ termination.parent_object.provider }}
{{ termination.parent_object }}
</a>
{% else %}
{{ termination.parent_object|linkify }}
{% endif %}
</td>
<td>
{{ termination|linkify }}
</td>

View File

@@ -0,0 +1,36 @@
<table class="table table-hover">
<tr>
<th scope="row">Cable</th>
<td>
{{ object.cable|linkify }}
<a href="{% url 'dcim:interface_trace' pk=object.pk %}" class="btn btn-primary btn-sm lh-1" title="Trace">
<i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i>
</a>
</td>
</tr>
<tr>
<th scope="row">Path Status</th>
<td>
{% if object.path.is_complete and object.path.is_active %}
<span class="badge bg-success">Reachable</span>
{% else %}
<span class="badge bg-danger">Not Reachable</span>
{% endif %}
</td>
</tr>
<tr>
<th scope="row">Path Endpoints</th>
<td>
{% for endpoint in object.connected_endpoints %}
{% if endpoint.parent_object %}
{{ endpoint.parent_object|linkify }}
<i class="mdi mdi-chevron-right"></i>
{% endif %}
{{ endpoint|linkify }}
{% if not forloop.last %}<br />{% endif %}
{% empty %}
{{ ''|placeholder }}
{% endfor %}
</td>
</tr>
</table>

View File

@@ -144,89 +144,7 @@
<span class="text-success"><i class="mdi mdi-check-bold"></i></span> Marked as Connected <span class="text-success"><i class="mdi mdi-check-bold"></i></span> Marked as Connected
</div> </div>
{% elif object.cable %} {% elif object.cable %}
<table class="table table-hover"> {% include 'dcim/inc/connection_endpoints.html' %}
{% if object.connected_endpoint.device %}
<tr>
<td colspan="2">
{% if object.connected_endpoint.enabled %}
<span class="badge bg-success">Enabled</span>
{% else %}
<span class="badge bg-danger">Disabled</span>
{% endif %}
</td>
</tr>
{% endif %}
<tr>
<th scope="row">Cable</th>
<td>
{{ object.cable|linkify }}
<a href="{% url 'dcim:interface_trace' pk=object.pk %}" class="btn btn-primary btn-sm lh-1" title="Trace">
<i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i>
</a>
</td>
</tr>
{% if object.connected_endpoint.device %}
{% with iface=object.connected_endpoint %}
<tr>
<th scope="row">Device</th>
<td>{{ iface.device|linkify }}</td>
</tr>
<tr>
<th scope="row">Name</th>
<td>{{ iface|linkify:"name" }}</td>
</tr>
<tr>
<th scope="row">Type</th>
<td>{{ iface.get_type_display }}</td>
</tr>
<tr>
<th scope="row">LAG</th>
<td>{{ iface.lag|linkify|placeholder }}</td>
</tr>
<tr>
<th scope="row">Description</th>
<td>{{ iface.description|placeholder }}</td>
</tr>
<tr>
<th scope="row">MTU</th>
<td>{{ iface.mtu|placeholder }}</td>
</tr>
<tr>
<th scope="row">MAC Address</th>
<td>{{ iface.mac_address|placeholder }}</td>
</tr>
<tr>
<th scope="row">802.1Q Mode</th>
<td>{{ iface.get_mode_display }}</td>
</tr>
{% endwith %}
{% elif object.connected_endpoint.circuit %}
{% with ct=object.connected_endpoint %}
<tr>
<th scope="row">Provider</th>
<td>{{ ct.circuit.provider|linkify }}</td>
</tr>
<tr>
<th scope="row">Circuit</th>
<td>{{ ct.circuit|linkify }}</td>
</tr>
<tr>
<th scope="row">Side</th>
<td>{{ ct.term_side }}</td>
</tr>
{% endwith %}
{% endif %}
<tr>
<th scope="row">Path Status</th>
<td>
{% if object.path.is_complete and object.path.is_active %}
<span class="badge bg-success">Reachable</span>
{% else %}
<span class="badge bg-danger">Not Reachable</span>
{% endif %}
</td>
</tr>
</table>
{% elif object.wireless_link %} {% elif object.wireless_link %}
<table class="table table-hover"> <table class="table table-hover">
<tr> <tr>
@@ -238,7 +156,7 @@
</a> </a>
</td> </td>
</tr> </tr>
{% with peer_interface=object.connected_endpoint %} {% with peer_interface=object.link_peers.0 %}
<tr> <tr>
<th scope="row">Device</th> <th scope="row">Device</th>
<td>{{ peer_interface.device|linkify }}</td> <td>{{ peer_interface.device|linkify }}</td>

View File

@@ -41,8 +41,8 @@
<tr> <tr>
<th scope="row">Connected Device</th> <th scope="row">Connected Device</th>
<td> <td>
{% if object.connected_endpoint %} {% if object.connected_endpoints %}
{{ object.connected_endpoint.device|linkify }} ({{ object.connected_endpoint }}) {{ object.connected_endpoints.0.device|linkify }} ({{ object.connected_endpoints.0|linkify:"name" }})
{% else %} {% else %}
{{ ''|placeholder }} {{ ''|placeholder }}
{% endif %} {% endif %}
@@ -50,7 +50,7 @@
</tr> </tr>
<tr> <tr>
<th scope="row">Utilization (Allocated)</th> <th scope="row">Utilization (Allocated)</th>
{% with utilization=object.connected_endpoint.get_power_draw %} {% with utilization=object.connected_endpoints.0.get_power_draw %}
{% if utilization %} {% if utilization %}
<td> <td>
{{ utilization.allocated }}VA / {{ object.available_power }}VA {{ utilization.allocated }}VA / {{ object.available_power }}VA
@@ -101,54 +101,14 @@
</div> </div>
<div class="col col-md-6"> <div class="col col-md-6">
<div class="card"> <div class="card">
<h5 class="card-header"> <h5 class="card-header">Connection</h5>
Connection
</h5>
<div class="card-body"> <div class="card-body">
{% if object.mark_connected %} {% if object.mark_connected %}
<div class="text-muted"> <div class="text-muted">
<span class="text-success"><i class="mdi mdi-check-bold"></i></span> Marked as connected <span class="text-success"><i class="mdi mdi-check-bold"></i></span> Marked as connected
</div> </div>
{% elif object.cable %} {% elif object.cable %}
<table class="table table-hover attr-table"> {% include 'dcim/inc/connection_endpoints.html' %}
<tr>
<th scope="row">Cable</th>
<td>
{{ object.cable|linkify }}
<a href="{% url 'dcim:powerfeed_trace' pk=object.pk %}" class="btn btn-primary btn-sm lh-1" title="Trace">
<i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i>
</a>
</td>
</tr>
{% if object.connected_endpoint %}
<tr>
<th scope="row">Device</th>
<td>{{ object.connected_endpoint.device|linkify }}</td>
</tr>
<tr>
<th scope="row">Name</th>
<td>{{ object.connected_endpoint|linkify:"name" }}</td>
</tr>
<tr>
<th scope="row">Type</th>
<td>{{ object.connected_endpoint.get_type_display|placeholder }}</td>
</tr>
<tr>
<th scope="row">Description</th>
<td>{{ object.connected_endpoint.description|placeholder }}</td>
</tr>
<tr>
<th scope="row">Path Status</th>
<td>
{% if object.path.is_active %}
<span class="badge bg-success">Reachable</span>
{% else %}
<span class="badge bg-danger">Not Reachable</span>
{% endif %}
</td>
</tr>
{% endif %}
</table>
{% else %} {% else %}
<div class="text-muted"> <div class="text-muted">
Not connected Not connected

View File

@@ -59,54 +59,14 @@
</div> </div>
<div class="col col-md-6"> <div class="col col-md-6">
<div class="card"> <div class="card">
<h5 class="card-header"> <h5 class="card-header">Connection</h5>
Connection
</h5>
<div class="card-body"> <div class="card-body">
{% if object.mark_connected %} {% if object.mark_connected %}
<div class="text-muted"> <div class="text-muted">
<span class="text-success"><i class="mdi mdi-check-bold"></i></span> Marked as Connected <span class="text-success"><i class="mdi mdi-check-bold"></i></span> Marked as Connected
</div> </div>
{% elif object.cable %} {% elif object.cable %}
<table class="table table-hover attr-table"> {% include 'dcim/inc/connection_endpoints.html' %}
<tr>
<th scope="row">Cable</th>
<td>
{{ object.cable|linkify }}
<a href="{% url 'dcim:poweroutlet_trace' pk=object.pk %}" class="btn btn-primary btn-sm lh-1" title="Trace">
<i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i>
</a>
</td>
</tr>
{% if object.connected_endpoint %}
<tr>
<th scope="row">Device</th>
<td>{{ object.connected_endpoint.device|linkify }}</td>
</tr>
<tr>
<th scope="row">Name</th>
<td>{{ object.connected_endpoint|linkify:"name" }}</td>
</tr>
<tr>
<th scope="row">Type</th>
<td>{{ object.connected_endpoint.get_type_display|placeholder }}</td>
</tr>
<tr>
<th scope="row">Description</th>
<td>{{ object.connected_endpoint.description|placeholder }}</td>
</tr>
<tr>
<th scope="row">Path Status</th>
<td>
{% if object.path.is_active %}
<span class="badge bg-success">Reachable</span>
{% else %}
<span class="badge bg-danger">Not Reachable</span>
{% endif %}
</td>
</tr>
{% endif %}
</table>
{% else %} {% else %}
<div class="text-muted"> <div class="text-muted">
Not Connected Not Connected

View File

@@ -59,54 +59,14 @@
</div> </div>
<div class="col col-md-6"> <div class="col col-md-6">
<div class="card"> <div class="card">
<h5 class="card-header"> <h5 class="card-header">Connection</h5>
Connection
</h5>
<div class="card-body"> <div class="card-body">
{% if object.mark_connected %} {% if object.mark_connected %}
<div class="text-muted"> <div class="text-muted">
<span class="text-success"><i class="mdi mdi-check-bold"></i></span> Marked as Connected <span class="text-success"><i class="mdi mdi-check-bold"></i></span> Marked as Connected
</div> </div>
{% elif object.cable %} {% elif object.cable %}
<table class="table table-hover attr-table"> {% include 'dcim/inc/connection_endpoints.html' %}
<tr>
<th scope="row">Cable</th>
<td>
{{ object.cable|linkify }}
<a href="{% url 'dcim:powerport_trace' pk=object.pk %}" class="btn btn-primary btn-sm lh-1" title="Trace">
<i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i>
</a>
</td>
</tr>
{% if object.connected_endpoint %}
<tr>
<th scope="row">Device</th>
<td>{{ object.connected_endpoint.device|linkify }}</td>
</tr>
<tr>
<th scope="row">Name</th>
<td>{{ object.connected_endpoint|linkify:"name" }}</td>
</tr>
<tr>
<th scope="row">Type</th>
<td>{{ object.connected_endpoint.get_type_display|placeholder }}</td>
</tr>
<tr>
<th scope="row">Description</th>
<td>{{ object.connected_endpoint.description|placeholder }}</td>
</tr>
<tr>
<th scope="row">Path Status</th>
<td>
{% if object.path.is_active %}
<span class="badge bg-success">Reachable</span>
{% else %}
<span class="badge bg-danger">Not Reachable</span>
{% endif %}
</td>
</tr>
{% endif %}
</table>
{% else %} {% else %}
<div class="text-muted"> <div class="text-muted">
Not Connected Not Connected

View File

@@ -3,6 +3,9 @@
{% load form_helpers %} {% load form_helpers %}
{% block form %} {% block form %}
{% for field in form.hidden_fields %}
{{ field }}
{% endfor %}
<div class="field-group my-5"> <div class="field-group my-5">
<div class="row mb-2"> <div class="row mb-2">
<h5 class="offset-sm-3">Contact Assignment</h5> <h5 class="offset-sm-3">Contact Assignment</h5>

View File

@@ -21,7 +21,7 @@
</tr> </tr>
<tr> <tr>
<th scope="row">Group</th> <th scope="row">Group</th>
<td>{{ object.group|linkify }}</td> <td>{{ object.group|linkify|placeholder }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Tenant</th> <th scope="row">Tenant</th>
@@ -34,7 +34,7 @@
</tr> </tr>
<tr> <tr>
<th scope="row">Site</th> <th scope="row">Site</th>
<td>{{ object.site|linkify }}</td> <td>{{ object.site|linkify|placeholder }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Virtual Machines</th> <th scope="row">Virtual Machines</th>

View File

@@ -119,8 +119,10 @@ class ContactAssignmentForm(BootstrapMixin, forms.ModelForm):
class Meta: class Meta:
model = ContactAssignment model = ContactAssignment
fields = ( fields = (
'group', 'contact', 'role', 'priority', 'content_type', 'object_id', 'group', 'contact', 'role', 'priority',
) )
widgets = { widgets = {
'content_type': forms.HiddenInput(),
'object_id': forms.HiddenInput(),
'priority': StaticSelect(), 'priority': StaticSelect(),
} }

View File

@@ -163,8 +163,8 @@ class ContactAssignment(WebhooksMixin, ChangeLoggedModel):
def __str__(self): def __str__(self):
if self.priority: if self.priority:
return f"{self.contact} ({self.get_priority_display()})" return f"{self.contact} ({self.get_priority_display()}) -> {self.object}"
return str(self.contact) return str(f"{self.contact} -> {self.object}")
def get_absolute_url(self): def get_absolute_url(self):
return reverse('tenancy:contact', args=[self.contact.pk]) return reverse('tenancy:contact', args=[self.contact.pk])

View File

@@ -42,7 +42,7 @@ class TenantTable(NetBoxTable):
linkify_item=True linkify_item=True
) )
tags = columns.TagColumn( tags = columns.TagColumn(
url_name='tenancy:tenant_list' url_name='tenancy:contact_list'
) )
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):

View File

@@ -73,9 +73,9 @@ def humanize_megabytes(mb):
""" """
if not mb: if not mb:
return '' return ''
if mb >= 1048576: if not mb % 1048576: # 1024^2
return f'{int(mb / 1048576)} TB' return f'{int(mb / 1048576)} TB'
if mb >= 1024: if not mb % 1024:
return f'{int(mb / 1024)} GB' return f'{int(mb / 1024)} GB'
return f'{mb} MB' return f'{mb} MB'

View File

@@ -347,14 +347,12 @@ class VirtualMachine(NetBoxModel, ConfigContextModel):
}) })
# Validate site for cluster & device # Validate site for cluster & device
if self.cluster and self.cluster.site != self.site: if self.cluster and self.site and self.cluster.site != self.site:
raise ValidationError({ raise ValidationError({
'cluster': f'The selected cluster ({self.cluster} is not assigned to this site ({self.site}).' 'cluster': f'The selected cluster ({self.cluster}) is not assigned to this site ({self.site}).'
})
if self.device and self.device.site != self.site:
raise ValidationError({
'device': f'The selected device ({self.device} is not assigned to this site ({self.site}).'
}) })
elif self.cluster:
self.site = self.cluster.site
# Validate assigned cluster device # Validate assigned cluster device
if self.device and not self.cluster: if self.device and not self.cluster:
@@ -363,7 +361,7 @@ class VirtualMachine(NetBoxModel, ConfigContextModel):
}) })
if self.device and self.device not in self.cluster.devices.all(): if self.device and self.device not in self.cluster.devices.all():
raise ValidationError({ raise ValidationError({
'device': f'The selected device ({self.device} is not assigned to this cluster ({self.cluster}).' 'device': f'The selected device ({self.device}) is not assigned to this cluster ({self.cluster}).'
}) })
# Validate primary IP addresses # Validate primary IP addresses

View File

@@ -68,6 +68,7 @@ class VirtualMachineTestCase(TestCase):
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
VirtualMachine(name='vm1', site=sites[0], cluster=clusters[1]).full_clean() VirtualMachine(name='vm1', site=sites[0], cluster=clusters[1]).full_clean()
# VM with cluster site but no direct site should fail # VM with cluster site but no direct site should have its site set automatically
with self.assertRaises(ValidationError): vm = VirtualMachine(name='vm1', site=None, cluster=clusters[0])
VirtualMachine(name='vm1', site=None, cluster=clusters[0]).full_clean() vm.full_clean()
self.assertEqual(vm.site, sites[0])

View File

@@ -11,3 +11,12 @@ profile = "black"
[tool.pylint] [tool.pylint]
max-line-length = 120 max-line-length = 120
[tool.pyright]
include = ["netbox"]
exclude = [
"**/node_modules",
"**/__pycache__",
]
reportMissingImports = true
reportMissingTypeStubs = false

View File

@@ -1,10 +1,10 @@
bleach==5.0.1 bleach==5.0.1
Django==4.0.7 Django==4.0.8
django-cors-headers==3.13.0 django-cors-headers==3.13.0
django-debug-toolbar==3.6.0 django-debug-toolbar==3.7.0
django-filter==22.1 django-filter==22.1
django-graphiql-debug-toolbar==0.2.0 django-graphiql-debug-toolbar==0.2.0
django-mptt==0.13.4 django-mptt==0.14
django-pglocks==1.0.4 django-pglocks==1.0.4
django-prometheus==2.2.0 django-prometheus==2.2.0
django-redis==5.2.0 django-redis==5.2.0
@@ -13,24 +13,24 @@ django-rq==2.5.1
django-tables2==2.4.1 django-tables2==2.4.1
django-taggit==3.0.0 django-taggit==3.0.0
django-timezone-field==5.0 django-timezone-field==5.0
djangorestframework==3.13.1 djangorestframework==3.14.0
drf-yasg[validation]==1.21.3 drf-yasg[validation]==1.21.4
graphene-django==2.15.0 graphene-django==2.15.0
gunicorn==20.1.0 gunicorn==20.1.0
Jinja2==3.1.2 Jinja2==3.1.2
Markdown==3.4.1 Markdown==3.3.7
mkdocs-material==8.5.1 mkdocs-material==8.5.6
mkdocstrings[python-legacy]==0.19.0 mkdocstrings[python-legacy]==0.19.0
netaddr==0.8.0 netaddr==0.8.0
Pillow==9.2.0 Pillow==9.2.0
psycopg2-binary==2.9.3 psycopg2-binary==2.9.3
PyYAML==6.0 PyYAML==6.0
sentry-sdk==1.9.8 sentry-sdk==1.9.10
social-auth-app-django==5.0.0 social-auth-app-django==5.0.0
social-auth-core==4.3.0 social-auth-core==4.3.0
svgwrite==1.4.3 svgwrite==1.4.3
tablib==3.2.1 tablib==3.2.1
tzdata==2022.2 tzdata==2022.4
# Workaround for #7401 # Workaround for #7401
jsonschema==3.2.0 jsonschema==3.2.0