mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@@ -14,7 +14,7 @@ body:
|
||||
attributes:
|
||||
label: NetBox version
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v3.3.4
|
||||
placeholder: v3.3.5
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
@@ -14,7 +14,7 @@ body:
|
||||
attributes:
|
||||
label: NetBox version
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v3.3.4
|
||||
placeholder: v3.3.5
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -1,5 +1,7 @@
|
||||
name: CI
|
||||
on: [push, pull_request]
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
6
.github/workflows/lock.yml
vendored
6
.github/workflows/lock.yml
vendored
@@ -4,6 +4,11 @@ name: 'Lock threads'
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 3 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
lock:
|
||||
@@ -11,7 +16,6 @@ jobs:
|
||||
steps:
|
||||
- uses: dessant/lock-threads@v3
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
issue-inactive-days: 90
|
||||
pr-inactive-days: 30
|
||||
issue-lock-reason: 'resolved'
|
||||
|
9
.github/workflows/stale.yml
vendored
9
.github/workflows/stale.yml
vendored
@@ -1,14 +1,21 @@
|
||||
# close-stale-issues (https://github.com/marketplace/actions/close-stale-issues)
|
||||
name: 'Close stale issues/PRs'
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 4 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v5
|
||||
- uses: actions/stale@v6
|
||||
with:
|
||||
close-issue-message: >
|
||||
This issue has been automatically closed due to lack of activity. In an
|
||||
|
@@ -68,7 +68,7 @@ drf-yasg[validation]
|
||||
|
||||
# Django wrapper for Graphene (GraphQL support)
|
||||
# https://github.com/graphql-python/graphene-django
|
||||
graphene_django
|
||||
graphene_django<3.0
|
||||
|
||||
# WSGI HTTP server
|
||||
# https://gunicorn.org/
|
||||
@@ -80,7 +80,8 @@ Jinja2
|
||||
|
||||
# Simple markup language for rendering HTML
|
||||
# https://github.com/Python-Markdown/markdown
|
||||
Markdown
|
||||
# mkdocs currently requires Markdown v3.3
|
||||
Markdown<3.4
|
||||
|
||||
# File inclusion plugin for Python-Markdown
|
||||
# https://github.com/cmacmackin/markdown-include
|
||||
|
4
docs/_theme/main.html
vendored
4
docs/_theme/main.html
vendored
@@ -2,8 +2,8 @@
|
||||
|
||||
{% block site_meta %}
|
||||
{{ super() }}
|
||||
{# Disable search indexing unless we're building for ReadTheDocs #}
|
||||
{% if not config.extra.readthedocs %}
|
||||
{# Disable search indexing unless we're building for ReadTheDocs (see #10496) #}
|
||||
{% if page.canonical_url != 'https://docs.netbox.dev/' %}
|
||||
<meta name="robots" content="noindex">
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
@@ -58,7 +58,7 @@ Email is sent from NetBox only for critical events or if configured for [logging
|
||||
|
||||
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
|
||||
HTTP_PROXIES = {
|
||||
|
@@ -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.
|
||||
|
||||
## 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
|
||||
|
||||
### Default Options
|
||||
|
@@ -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.
|
||||
|
||||
!!! 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
|
||||
|
||||
`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:
|
||||
|
@@ -1,5 +1,33 @@
|
||||
# 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
|
||||
|
@@ -38,7 +38,6 @@ plugins:
|
||||
show_root_toc_entry: false
|
||||
show_source: false
|
||||
extra:
|
||||
readthedocs: !ENV READTHEDOCS
|
||||
social:
|
||||
- icon: fontawesome/brands/github
|
||||
link: https://github.com/netbox-community/netbox
|
||||
|
@@ -373,6 +373,7 @@ class DeviceTypeForm(NetBoxModelForm):
|
||||
'front_image', 'rear_image', 'comments', 'tags',
|
||||
]
|
||||
widgets = {
|
||||
'airflow': StaticSelect(),
|
||||
'subdevice_role': StaticSelect(),
|
||||
'front_image': ClearableFileInput(attrs={
|
||||
'accept': DEVICETYPE_IMAGE_FORMATS
|
||||
@@ -678,6 +679,7 @@ class ModuleForm(NetBoxModelForm):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if self.instance.pk:
|
||||
self.fields['device'].disabled = True
|
||||
self.fields['replicate_components'].initial = False
|
||||
self.fields['replicate_components'].disabled = True
|
||||
self.fields['adopt_components'].initial = False
|
||||
|
@@ -987,6 +987,14 @@ class Module(NetBoxModel, ConfigContextModel):
|
||||
def get_absolute_url(self):
|
||||
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):
|
||||
is_new = self.pk is None
|
||||
|
||||
|
@@ -35,7 +35,7 @@ class Node(Hyperlink):
|
||||
"""
|
||||
|
||||
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
|
||||
|
||||
|
@@ -9,6 +9,7 @@ from svgwrite.text import Text
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import FieldError
|
||||
from django.db.models import Q
|
||||
from django.template.defaultfilters import floatformat
|
||||
from django.urls import reverse
|
||||
from django.utils.http import urlencode
|
||||
|
||||
@@ -41,7 +42,7 @@ def get_device_description(device):
|
||||
device.device_role,
|
||||
device.device_type.manufacturer.name,
|
||||
device.device_type.model,
|
||||
device.device_type.u_height,
|
||||
floatformat(device.device_type.u_height),
|
||||
device.asset_tag or '',
|
||||
device.serial or ''
|
||||
)
|
||||
|
@@ -85,6 +85,9 @@ class DeviceTypeTable(NetBoxTable):
|
||||
tags = columns.TagColumn(
|
||||
url_name='dcim:devicetype_list'
|
||||
)
|
||||
u_height = columns.TemplateColumn(
|
||||
template_code='{{ value|floatformat }}'
|
||||
)
|
||||
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = DeviceType
|
||||
|
@@ -1778,10 +1778,12 @@ class ModuleTestCase(
|
||||
ModuleBay(device=devices[0], name='Module Bay 2'),
|
||||
ModuleBay(device=devices[0], name='Module Bay 3'),
|
||||
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 2'),
|
||||
ModuleBay(device=devices[1], name='Module Bay 3'),
|
||||
ModuleBay(device=devices[1], name='Module Bay 4'),
|
||||
ModuleBay(device=devices[1], name='Module Bay 5'),
|
||||
)
|
||||
ModuleBay.objects.bulk_create(module_bays)
|
||||
|
||||
@@ -1795,7 +1797,7 @@ class ModuleTestCase(
|
||||
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||
|
||||
cls.form_data = {
|
||||
'device': devices[1].pk,
|
||||
'device': devices[0].pk,
|
||||
'module_bay': module_bays[3].pk,
|
||||
'module_type': module_types[0].pk,
|
||||
'serial': 'A',
|
||||
@@ -1867,7 +1869,6 @@ class ModuleTestCase(
|
||||
self.assertIsNone(interface.module)
|
||||
|
||||
# Create a module with adopted components
|
||||
form_data['module_bay'] = ModuleBay.objects.filter(device=device).first()
|
||||
form_data['module_type'] = module_type
|
||||
form_data['replicate_components'] = False
|
||||
form_data['adopt_components'] = True
|
||||
|
@@ -355,7 +355,7 @@ class SiteView(generic.ObjectView):
|
||||
|
||||
nonracked_devices = Device.objects.filter(
|
||||
site=instance,
|
||||
position__isnull=True,
|
||||
rack__isnull=True,
|
||||
parent_bay__isnull=True
|
||||
).prefetch_related('device_type__manufacturer', 'parent_bay', 'device_role')
|
||||
|
||||
@@ -450,7 +450,7 @@ class LocationView(generic.ObjectView):
|
||||
|
||||
nonracked_devices = Device.objects.filter(
|
||||
location=instance,
|
||||
position__isnull=True,
|
||||
rack__isnull=True,
|
||||
parent_bay__isnull=True
|
||||
).prefetch_related('device_type__manufacturer', 'parent_bay', 'device_role')
|
||||
|
||||
@@ -1616,6 +1616,7 @@ class DeviceView(generic.ObjectView):
|
||||
return {
|
||||
'services': services,
|
||||
'vc_members': vc_members,
|
||||
'svg_extra': f'highlight=id:{instance.pk}'
|
||||
}
|
||||
|
||||
|
||||
|
@@ -34,7 +34,9 @@ class CustomFieldsMixin:
|
||||
return ContentType.objects.get_for_model(self.model)
|
||||
|
||||
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):
|
||||
return customfield.to_form_field()
|
||||
@@ -50,13 +52,6 @@ class CustomFieldsMixin:
|
||||
field_name = f'cf_{customfield.name}'
|
||||
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
|
||||
self.custom_fields[field_name] = customfield
|
||||
if customfield.group_name not in self.custom_field_groups:
|
||||
|
@@ -297,12 +297,13 @@ class CustomField(CloningMixin, ExportTemplatesMixin, WebhooksMixin, ChangeLogge
|
||||
return model.objects.filter(pk__in=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.
|
||||
|
||||
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_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.
|
||||
"""
|
||||
initial = self.default if set_initial else None
|
||||
@@ -398,6 +399,12 @@ class CustomField(CloningMixin, ExportTemplatesMixin, WebhooksMixin, ChangeLogge
|
||||
if 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
|
||||
|
||||
def to_filter(self, lookup_expr=None):
|
||||
|
@@ -463,6 +463,14 @@ class JournalEntry(CustomFieldsMixin, CustomLinksMixin, TagsMixin, WebhooksMixin
|
||||
def get_absolute_url(self):
|
||||
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):
|
||||
return JournalEntryKindChoices.colors.get(self.kind)
|
||||
|
||||
|
@@ -81,30 +81,34 @@ class VLANQuerySet(RestrictedQuerySet):
|
||||
|
||||
# Find all relevant VLANGroups
|
||||
q = Q()
|
||||
if vm.cluster.site:
|
||||
if vm.cluster.site.region:
|
||||
site = vm.site or vm.cluster.site
|
||||
if vm.cluster:
|
||||
# Add VLANGroups scoped to the assigned cluster (or its group)
|
||||
q |= Q(
|
||||
scope_type=ContentType.objects.get_by_natural_key('virtualization', 'cluster'),
|
||||
scope_id=vm.cluster_id
|
||||
)
|
||||
if vm.cluster.group:
|
||||
q |= Q(
|
||||
scope_type=ContentType.objects.get_by_natural_key('dcim', 'region'),
|
||||
scope_id__in=vm.cluster.site.region.get_ancestors(include_self=True)
|
||||
)
|
||||
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)
|
||||
scope_type=ContentType.objects.get_by_natural_key('virtualization', 'clustergroup'),
|
||||
scope_id=vm.cluster.group_id
|
||||
)
|
||||
if site:
|
||||
# Add VLANGroups scoped to the assigned site (or its group or region)
|
||||
q |= Q(
|
||||
scope_type=ContentType.objects.get_by_natural_key('dcim', 'site'),
|
||||
scope_id=vm.cluster.site_id
|
||||
scope_id=site.pk
|
||||
)
|
||||
if vm.cluster.group:
|
||||
q |= Q(
|
||||
scope_type=ContentType.objects.get_by_natural_key('virtualization', 'clustergroup'),
|
||||
scope_id=vm.cluster.group_id
|
||||
)
|
||||
q |= Q(
|
||||
scope_type=ContentType.objects.get_by_natural_key('virtualization', 'cluster'),
|
||||
scope_id=vm.cluster_id
|
||||
)
|
||||
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)
|
||||
|
||||
# Return all applicable VLANs
|
||||
@@ -113,7 +117,7 @@ class VLANQuerySet(RestrictedQuerySet):
|
||||
Q(group__scope_id__isnull=True, site__isnull=True) | # Global group VLANs
|
||||
Q(group__isnull=True, site__isnull=True) # Global VLANs
|
||||
)
|
||||
if vm.cluster.site:
|
||||
q |= Q(site=vm.cluster.site)
|
||||
if site:
|
||||
q |= Q(site=site)
|
||||
|
||||
return self.filter(q)
|
||||
|
@@ -2,7 +2,7 @@ from django import forms
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
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.models import CustomField, Tag
|
||||
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)
|
||||
|
||||
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):
|
||||
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):
|
||||
return CustomField.objects.filter(content_types=content_type).exclude(
|
||||
return super()._get_custom_fields(content_type).exclude(
|
||||
Q(filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED) |
|
||||
Q(type=CustomFieldTypeChoices.TYPE_JSON)
|
||||
)
|
||||
|
||||
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)
|
||||
|
@@ -20,7 +20,6 @@ class NetBoxFeatureSet(
|
||||
CustomLinksMixin,
|
||||
CustomValidationMixin,
|
||||
ExportTemplatesMixin,
|
||||
JournalingMixin,
|
||||
TagsMixin,
|
||||
WebhooksMixin
|
||||
):
|
||||
@@ -51,7 +50,7 @@ class ChangeLoggedModel(ChangeLoggingMixin, CustomValidationMixin, models.Model)
|
||||
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.
|
||||
"""
|
||||
|
@@ -29,7 +29,7 @@ django.utils.encoding.force_text = force_str
|
||||
# Environment setup
|
||||
#
|
||||
|
||||
VERSION = '3.3.4'
|
||||
VERSION = '3.3.5'
|
||||
|
||||
# Hostname
|
||||
HOSTNAME = platform.node()
|
||||
|
2
netbox/project-static/dist/netbox.js
vendored
2
netbox/project-static/dist/netbox.js
vendored
File diff suppressed because one or more lines are too long
2
netbox/project-static/dist/netbox.js.map
vendored
2
netbox/project-static/dist/netbox.js.map
vendored
File diff suppressed because one or more lines are too long
@@ -37,6 +37,20 @@ function initDocument(): 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');
|
||||
if (contentContainer !== null) {
|
||||
// Focus the content container for accessible navigation.
|
||||
|
@@ -54,80 +54,40 @@
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
<div class="card">
|
||||
<h5 class="card-header">
|
||||
Connection
|
||||
</h5>
|
||||
<div class="card-body">
|
||||
{% if object.mark_connected %}
|
||||
<span class="text-success"><i class="mdi mdi-check-bold"></i></span> Marked as connected
|
||||
{% elif object.cable %}
|
||||
<table class="table table-hover attr-table">
|
||||
<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 %}
|
||||
<div class="text-muted">
|
||||
Not Connected
|
||||
{% if perms.dcim.add_cable %}
|
||||
<div class="dropdown float-end">
|
||||
<button type="button" class="btn btn-primary btn-sm dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span> Connect
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
<li>
|
||||
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleport&a_terminations={{ object.pk }}&b_terminations_type=dcim.consoleserverport&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}" class="dropdown-item">Console Server Port</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleport&a_terminations={{ object.pk }}&b_terminations_type=dcim.frontport&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}" class="dropdown-item">Front Port</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleport&a_terminations={{ object.pk }}&b_terminations_type=dcim.rearport&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}" class="dropdown-item">Rear Port</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="card">
|
||||
<h5 class="card-header">Connection</h5>
|
||||
<div class="card-body">
|
||||
{% if object.mark_connected %}
|
||||
<span class="text-success"><i class="mdi mdi-check-bold"></i></span> Marked as connected
|
||||
{% elif object.cable %}
|
||||
{% include 'dcim/inc/connection_endpoints.html' %}
|
||||
{% else %}
|
||||
<div class="text-muted">
|
||||
Not Connected
|
||||
{% if perms.dcim.add_cable %}
|
||||
<div class="dropdown float-end">
|
||||
<button type="button" class="btn btn-primary btn-sm dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span> Connect
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
<li>
|
||||
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleport&a_terminations={{ object.pk }}&b_terminations_type=dcim.consoleserverport&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}" class="dropdown-item">Console Server Port</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleport&a_terminations={{ object.pk }}&b_terminations_type=dcim.frontport&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}" class="dropdown-item">Front Port</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleport&a_terminations={{ object.pk }}&b_terminations_type=dcim.rearport&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}" class="dropdown-item">Rear Port</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% include 'dcim/inc/panels/inventory_items.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
{% include 'dcim/inc/panels/inventory_items.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
|
@@ -54,82 +54,40 @@
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
<div class="card">
|
||||
<h5 class="card-header">
|
||||
Connection
|
||||
</h5>
|
||||
<div class="card-body">
|
||||
{% if object.mark_connected %}
|
||||
<span class="text-success"><i class="mdi mdi-check-bold"></i></span> Marked as connected
|
||||
{% elif object.cable %}
|
||||
<table class="table table-hover attr-table">
|
||||
<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 %}
|
||||
<div class="text-muted">
|
||||
Not Connected
|
||||
{% if perms.dcim.add_cable %}
|
||||
<div class="dropdown float-end">
|
||||
<button type="button" class="btn btn-primary btn-sm dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span> Connect
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
<li>
|
||||
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleserverport&a_terminations={{ object.pk }}&b_terminations_type=dcim.consoleport&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}" class="dropdown-item">Console Port</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleserverport&a_terminations={{ object.pk }}&b_terminations_type=dcim.frontport&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}" class="dropdown-item">Front Port</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleserverport&a_terminations={{ object.pk }}&b_terminations_type=dcim.rearport&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}" class="dropdown-item">Rear Port</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="card">
|
||||
<h5 class="card-header">Connection</h5>
|
||||
<div class="card-body">
|
||||
{% if object.mark_connected %}
|
||||
<span class="text-success"><i class="mdi mdi-check-bold"></i></span> Marked as connected
|
||||
{% elif object.cable %}
|
||||
{% include 'dcim/inc/connection_endpoints.html' %}
|
||||
{% else %}
|
||||
<div class="text-muted">
|
||||
Not Connected
|
||||
{% if perms.dcim.add_cable %}
|
||||
<div class="dropdown float-end">
|
||||
<button type="button" class="btn btn-primary btn-sm dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span> Connect
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
<li>
|
||||
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleserverport&a_terminations={{ object.pk }}&b_terminations_type=dcim.consoleport&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}" class="dropdown-item">Console Port</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleserverport&a_terminations={{ object.pk }}&b_terminations_type=dcim.frontport&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}" class="dropdown-item">Front Port</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleserverport&a_terminations={{ object.pk }}&b_terminations_type=dcim.rearport&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}" class="dropdown-item">Rear Port</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% include 'dcim/inc/panels/inventory_items.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
{% include 'dcim/inc/panels/inventory_items.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
|
@@ -7,7 +7,7 @@
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col col-md-6">
|
||||
<div class="col col-12 col-xl-6">
|
||||
<div class="card">
|
||||
<h5 class="card-header">
|
||||
Device
|
||||
@@ -66,7 +66,7 @@
|
||||
{% with object.parent_bay.device as parent %}
|
||||
{{ parent|linkify }} / {{ object.parent_bay }}
|
||||
{% if parent.position %}
|
||||
(U{{ parent.position }} / {{ parent.get_face_display }})
|
||||
(U{{ parent.position|floatformat }} / {{ parent.get_face_display }})
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% elif object.rack and object.position %}
|
||||
@@ -90,7 +90,7 @@
|
||||
<tr>
|
||||
<th scope="row">Device Type</th>
|
||||
<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>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -153,7 +153,7 @@
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
<div class="col col-12 col-xl-6">
|
||||
<div class="card">
|
||||
<h5 class="card-header">Management</h5>
|
||||
<div class="card-body">
|
||||
@@ -286,6 +286,22 @@
|
||||
</div>
|
||||
{% include 'inc/panels/contacts.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 %}
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -29,7 +29,7 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Height (U)</td>
|
||||
<td>{{ object.u_height }}</td>
|
||||
<td>{{ object.u_height|floatformat }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Full Depth</td>
|
||||
|
@@ -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>
|
36
netbox/templates/dcim/inc/connection_endpoints.html
Normal file
36
netbox/templates/dcim/inc/connection_endpoints.html
Normal 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>
|
@@ -144,89 +144,7 @@
|
||||
<span class="text-success"><i class="mdi mdi-check-bold"></i></span> Marked as Connected
|
||||
</div>
|
||||
{% elif object.cable %}
|
||||
<table class="table table-hover">
|
||||
{% 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>
|
||||
{% include 'dcim/inc/connection_endpoints.html' %}
|
||||
{% elif object.wireless_link %}
|
||||
<table class="table table-hover">
|
||||
<tr>
|
||||
@@ -238,7 +156,7 @@
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% with peer_interface=object.connected_endpoint %}
|
||||
{% with peer_interface=object.link_peers.0 %}
|
||||
<tr>
|
||||
<th scope="row">Device</th>
|
||||
<td>{{ peer_interface.device|linkify }}</td>
|
||||
|
@@ -41,8 +41,8 @@
|
||||
<tr>
|
||||
<th scope="row">Connected Device</th>
|
||||
<td>
|
||||
{% if object.connected_endpoint %}
|
||||
{{ object.connected_endpoint.device|linkify }} ({{ object.connected_endpoint }})
|
||||
{% if object.connected_endpoints %}
|
||||
{{ object.connected_endpoints.0.device|linkify }} ({{ object.connected_endpoints.0|linkify:"name" }})
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
@@ -50,7 +50,7 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<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 %}
|
||||
<td>
|
||||
{{ utilization.allocated }}VA / {{ object.available_power }}VA
|
||||
@@ -100,73 +100,33 @@
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
<div class="card">
|
||||
<h5 class="card-header">
|
||||
Connection
|
||||
</h5>
|
||||
<div class="card-body">
|
||||
{% if object.mark_connected %}
|
||||
<div class="text-muted">
|
||||
<span class="text-success"><i class="mdi mdi-check-bold"></i></span> Marked as connected
|
||||
</div>
|
||||
{% elif object.cable %}
|
||||
<table class="table table-hover attr-table">
|
||||
<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 %}
|
||||
<div class="text-muted">
|
||||
Not connected
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="card">
|
||||
<h5 class="card-header">Connection</h5>
|
||||
<div class="card-body">
|
||||
{% if object.mark_connected %}
|
||||
<div class="text-muted">
|
||||
<span class="text-success"><i class="mdi mdi-check-bold"></i></span> Marked as connected
|
||||
</div>
|
||||
{% if not object.mark_connected and not object.cable %}
|
||||
<div class="card-footer">
|
||||
{% if perms.dcim.add_cable %}
|
||||
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.powerfeed&a_terminations={{ object.pk }}&b_terminations_type=dcim.powerport&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-primary btn-sm float-end">
|
||||
<i class="mdi mdi-ethernet-cable" aria-hidden="true"></i> Connect
|
||||
</a>
|
||||
{% endif %}
|
||||
{% elif object.cable %}
|
||||
{% include 'dcim/inc/connection_endpoints.html' %}
|
||||
{% else %}
|
||||
<div class="text-muted">
|
||||
Not connected
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% plugin_right_page object %}
|
||||
{% if not object.mark_connected and not object.cable %}
|
||||
<div class="card-footer">
|
||||
{% if perms.dcim.add_cable %}
|
||||
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.powerfeed&a_terminations={{ object.pk }}&b_terminations_type=dcim.powerport&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-primary btn-sm float-end">
|
||||
<i class="mdi mdi-ethernet-cable" aria-hidden="true"></i> Connect
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
|
@@ -58,69 +58,29 @@
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
<div class="card">
|
||||
<h5 class="card-header">
|
||||
Connection
|
||||
</h5>
|
||||
<div class="card-body">
|
||||
{% if object.mark_connected %}
|
||||
<div class="text-muted">
|
||||
<span class="text-success"><i class="mdi mdi-check-bold"></i></span> Marked as Connected
|
||||
</div>
|
||||
{% elif object.cable %}
|
||||
<table class="table table-hover attr-table">
|
||||
<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 %}
|
||||
<div class="text-muted">
|
||||
Not Connected
|
||||
{% if perms.dcim.add_cable %}
|
||||
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.poweroutlet&a_terminations={{ object.pk }}&b_terminations_type=dcim.powerport&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}" title="Connect" class="btn btn-primary btn-sm float-end">
|
||||
<i class="mdi mdi-ethernet-cable" aria-hidden="true"></i> Connect
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="card">
|
||||
<h5 class="card-header">Connection</h5>
|
||||
<div class="card-body">
|
||||
{% if object.mark_connected %}
|
||||
<div class="text-muted">
|
||||
<span class="text-success"><i class="mdi mdi-check-bold"></i></span> Marked as Connected
|
||||
</div>
|
||||
{% elif object.cable %}
|
||||
{% include 'dcim/inc/connection_endpoints.html' %}
|
||||
{% else %}
|
||||
<div class="text-muted">
|
||||
Not Connected
|
||||
{% if perms.dcim.add_cable %}
|
||||
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.poweroutlet&a_terminations={{ object.pk }}&b_terminations_type=dcim.powerport&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}" title="Connect" class="btn btn-primary btn-sm float-end">
|
||||
<i class="mdi mdi-ethernet-cable" aria-hidden="true"></i> Connect
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% include 'dcim/inc/panels/inventory_items.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
{% include 'dcim/inc/panels/inventory_items.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
|
@@ -58,79 +58,39 @@
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
<div class="card">
|
||||
<h5 class="card-header">
|
||||
Connection
|
||||
</h5>
|
||||
<div class="card-body">
|
||||
{% if object.mark_connected %}
|
||||
<div class="text-muted">
|
||||
<span class="text-success"><i class="mdi mdi-check-bold"></i></span> Marked as Connected
|
||||
</div>
|
||||
{% elif object.cable %}
|
||||
<table class="table table-hover attr-table">
|
||||
<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 %}
|
||||
<div class="text-muted">
|
||||
Not Connected
|
||||
{% if perms.dcim.add_cable %}
|
||||
<span class="dropdown float-end">
|
||||
<button type="button" class="btn btn-primary btn-sm dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span> Connect
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
<li>
|
||||
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.powerportport&a_terminations={{ object.pk }}&termination_b_type=dcim.poweroutlet&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}" class="dropdown-link">Power Outlet</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.powerportport&a_terminations={{ object.pk }}&termination_b_type=dcim.powerfeed&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}" class="dropdown-link">Power Feed</a>
|
||||
</li>
|
||||
</ul>
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="card">
|
||||
<h5 class="card-header">Connection</h5>
|
||||
<div class="card-body">
|
||||
{% if object.mark_connected %}
|
||||
<div class="text-muted">
|
||||
<span class="text-success"><i class="mdi mdi-check-bold"></i></span> Marked as Connected
|
||||
</div>
|
||||
{% elif object.cable %}
|
||||
{% include 'dcim/inc/connection_endpoints.html' %}
|
||||
{% else %}
|
||||
<div class="text-muted">
|
||||
Not Connected
|
||||
{% if perms.dcim.add_cable %}
|
||||
<span class="dropdown float-end">
|
||||
<button type="button" class="btn btn-primary btn-sm dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span> Connect
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
<li>
|
||||
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.powerportport&a_terminations={{ object.pk }}&termination_b_type=dcim.poweroutlet&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}" class="dropdown-link">Power Outlet</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.powerportport&a_terminations={{ object.pk }}&termination_b_type=dcim.powerfeed&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}" class="dropdown-link">Power Feed</a>
|
||||
</li>
|
||||
</ul>
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% include 'dcim/inc/panels/inventory_items.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
{% include 'dcim/inc/panels/inventory_items.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
|
@@ -3,6 +3,9 @@
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block form %}
|
||||
{% for field in form.hidden_fields %}
|
||||
{{ field }}
|
||||
{% endfor %}
|
||||
<div class="field-group my-5">
|
||||
<div class="row mb-2">
|
||||
<h5 class="offset-sm-3">Contact Assignment</h5>
|
||||
|
@@ -21,7 +21,7 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Group</th>
|
||||
<td>{{ object.group|linkify }}</td>
|
||||
<td>{{ object.group|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Tenant</th>
|
||||
@@ -34,7 +34,7 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Site</th>
|
||||
<td>{{ object.site|linkify }}</td>
|
||||
<td>{{ object.site|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Virtual Machines</th>
|
||||
|
@@ -119,8 +119,10 @@ class ContactAssignmentForm(BootstrapMixin, forms.ModelForm):
|
||||
class Meta:
|
||||
model = ContactAssignment
|
||||
fields = (
|
||||
'group', 'contact', 'role', 'priority',
|
||||
'content_type', 'object_id', 'group', 'contact', 'role', 'priority',
|
||||
)
|
||||
widgets = {
|
||||
'content_type': forms.HiddenInput(),
|
||||
'object_id': forms.HiddenInput(),
|
||||
'priority': StaticSelect(),
|
||||
}
|
||||
|
@@ -163,8 +163,8 @@ class ContactAssignment(WebhooksMixin, ChangeLoggedModel):
|
||||
|
||||
def __str__(self):
|
||||
if self.priority:
|
||||
return f"{self.contact} ({self.get_priority_display()})"
|
||||
return str(self.contact)
|
||||
return f"{self.contact} ({self.get_priority_display()}) -> {self.object}"
|
||||
return str(f"{self.contact} -> {self.object}")
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('tenancy:contact', args=[self.contact.pk])
|
||||
|
@@ -42,7 +42,7 @@ class TenantTable(NetBoxTable):
|
||||
linkify_item=True
|
||||
)
|
||||
tags = columns.TagColumn(
|
||||
url_name='tenancy:tenant_list'
|
||||
url_name='tenancy:contact_list'
|
||||
)
|
||||
|
||||
class Meta(NetBoxTable.Meta):
|
||||
|
@@ -73,9 +73,9 @@ def humanize_megabytes(mb):
|
||||
"""
|
||||
if not mb:
|
||||
return ''
|
||||
if mb >= 1048576:
|
||||
if not mb % 1048576: # 1024^2
|
||||
return f'{int(mb / 1048576)} TB'
|
||||
if mb >= 1024:
|
||||
if not mb % 1024:
|
||||
return f'{int(mb / 1024)} GB'
|
||||
return f'{mb} MB'
|
||||
|
||||
|
@@ -347,14 +347,12 @@ class VirtualMachine(NetBoxModel, ConfigContextModel):
|
||||
})
|
||||
|
||||
# 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({
|
||||
'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}).'
|
||||
'cluster': f'The selected cluster ({self.cluster}) is not assigned to this site ({self.site}).'
|
||||
})
|
||||
elif self.cluster:
|
||||
self.site = self.cluster.site
|
||||
|
||||
# Validate assigned cluster device
|
||||
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():
|
||||
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
|
||||
|
@@ -68,6 +68,7 @@ class VirtualMachineTestCase(TestCase):
|
||||
with self.assertRaises(ValidationError):
|
||||
VirtualMachine(name='vm1', site=sites[0], cluster=clusters[1]).full_clean()
|
||||
|
||||
# VM with cluster site but no direct site should fail
|
||||
with self.assertRaises(ValidationError):
|
||||
VirtualMachine(name='vm1', site=None, cluster=clusters[0]).full_clean()
|
||||
# VM with cluster site but no direct site should have its site set automatically
|
||||
vm = VirtualMachine(name='vm1', site=None, cluster=clusters[0])
|
||||
vm.full_clean()
|
||||
self.assertEqual(vm.site, sites[0])
|
||||
|
@@ -11,3 +11,12 @@ profile = "black"
|
||||
|
||||
[tool.pylint]
|
||||
max-line-length = 120
|
||||
|
||||
[tool.pyright]
|
||||
include = ["netbox"]
|
||||
exclude = [
|
||||
"**/node_modules",
|
||||
"**/__pycache__",
|
||||
]
|
||||
reportMissingImports = true
|
||||
reportMissingTypeStubs = false
|
||||
|
@@ -1,10 +1,10 @@
|
||||
bleach==5.0.1
|
||||
Django==4.0.7
|
||||
Django==4.0.8
|
||||
django-cors-headers==3.13.0
|
||||
django-debug-toolbar==3.6.0
|
||||
django-debug-toolbar==3.7.0
|
||||
django-filter==22.1
|
||||
django-graphiql-debug-toolbar==0.2.0
|
||||
django-mptt==0.13.4
|
||||
django-mptt==0.14
|
||||
django-pglocks==1.0.4
|
||||
django-prometheus==2.2.0
|
||||
django-redis==5.2.0
|
||||
@@ -13,24 +13,24 @@ django-rq==2.5.1
|
||||
django-tables2==2.4.1
|
||||
django-taggit==3.0.0
|
||||
django-timezone-field==5.0
|
||||
djangorestframework==3.13.1
|
||||
drf-yasg[validation]==1.21.3
|
||||
djangorestframework==3.14.0
|
||||
drf-yasg[validation]==1.21.4
|
||||
graphene-django==2.15.0
|
||||
gunicorn==20.1.0
|
||||
Jinja2==3.1.2
|
||||
Markdown==3.4.1
|
||||
mkdocs-material==8.5.1
|
||||
Markdown==3.3.7
|
||||
mkdocs-material==8.5.6
|
||||
mkdocstrings[python-legacy]==0.19.0
|
||||
netaddr==0.8.0
|
||||
Pillow==9.2.0
|
||||
psycopg2-binary==2.9.3
|
||||
PyYAML==6.0
|
||||
sentry-sdk==1.9.8
|
||||
sentry-sdk==1.9.10
|
||||
social-auth-app-django==5.0.0
|
||||
social-auth-core==4.3.0
|
||||
svgwrite==1.4.3
|
||||
tablib==3.2.1
|
||||
tzdata==2022.2
|
||||
tzdata==2022.4
|
||||
|
||||
# Workaround for #7401
|
||||
jsonschema==3.2.0
|
||||
|
Reference in New Issue
Block a user