From 03535ce50b7063414dea71f753305d27506a646a Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 8 Apr 2022 16:00:33 -0400 Subject: [PATCH 001/593] Closes #8995: Enable arbitrary ordering of REST API results --- docs/release-notes/version-3.3.md | 7 ++++ docs/rest-api/filtering.md | 20 +++++++++++ netbox/netbox/settings.py | 3 +- netbox/utilities/tests/test_api.py | 58 ++++++++++++++++++++++++++++++ 4 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 docs/release-notes/version-3.3.md diff --git a/docs/release-notes/version-3.3.md b/docs/release-notes/version-3.3.md new file mode 100644 index 000000000..de59d0327 --- /dev/null +++ b/docs/release-notes/version-3.3.md @@ -0,0 +1,7 @@ +# NetBox v3.3 + +## v3.3.0 (FUTURE) + +### Enhancements + +* [#8995](https://github.com/netbox-community/netbox/issues/8995) - Enable arbitrary ordering of REST API results diff --git a/docs/rest-api/filtering.md b/docs/rest-api/filtering.md index 45dfcfa36..7ddda6f3c 100644 --- a/docs/rest-api/filtering.md +++ b/docs/rest-api/filtering.md @@ -106,3 +106,23 @@ expression: `n`. Here is an example of a lookup expression on a foreign key, it ```no-highlight GET /api/ipam/vlans/?group_id__n=3203 ``` + +## Ordering Objects + +To order results by a particular field, include the `ordering` query parameter. For example, order the list of sites according to their facility values: + +```no-highlight +GET /api/dcim/sites/?ordering=facility +``` + +To invert the ordering, prepend a hyphen to the field name: + +```no-highlight +GET /api/dcim/sites/?ordering=-facility +``` + +Multiple fields can be specified by separating the field names with a comma. For example: + +```no-highlight +GET /api/dcim/sites/?ordering=facility,-name +``` diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 04c0e9c3d..0db41373f 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -26,7 +26,7 @@ django.utils.encoding.force_text = force_str # Environment setup # -VERSION = '3.2.1-dev' +VERSION = '3.3.0-dev' # Hostname HOSTNAME = platform.node() @@ -469,6 +469,7 @@ REST_FRAMEWORK = { ), 'DEFAULT_FILTER_BACKENDS': ( 'django_filters.rest_framework.DjangoFilterBackend', + 'rest_framework.filters.OrderingFilter', ), 'DEFAULT_METADATA_CLASS': 'netbox.api.metadata.BulkOperationMetadata', 'DEFAULT_PAGINATION_CLASS': 'netbox.api.pagination.OptionalLimitOffsetPagination', diff --git a/netbox/utilities/tests/test_api.py b/netbox/utilities/tests/test_api.py index 1171bd496..e341442be 100644 --- a/netbox/utilities/tests/test_api.py +++ b/netbox/utilities/tests/test_api.py @@ -176,6 +176,64 @@ class APIPaginationTestCase(APITestCase): self.assertEqual(len(response.data['results']), 100) +class APIOrderingTestCase(APITestCase): + user_permissions = ('dcim.view_site',) + + @classmethod + def setUpTestData(cls): + cls.url = reverse('dcim-api:site-list') + + sites = ( + Site(name='Site 1', slug='site-1', facility='C', description='Z'), + Site(name='Site 2', slug='site-2', facility='C', description='Y'), + Site(name='Site 3', slug='site-3', facility='B', description='X'), + Site(name='Site 4', slug='site-4', facility='B', description='W'), + Site(name='Site 5', slug='site-5', facility='A', description='V'), + Site(name='Site 6', slug='site-6', facility='A', description='U'), + ) + Site.objects.bulk_create(sites) + + def test_default_order(self): + response = self.client.get(self.url, format='json', **self.header) + + self.assertHttpStatus(response, status.HTTP_200_OK) + self.assertEqual(response.data['count'], 6) + self.assertListEqual( + [s['name'] for s in response.data['results']], + ['Site 1', 'Site 2', 'Site 3', 'Site 4', 'Site 5', 'Site 6'] + ) + + def test_order_single_field(self): + response = self.client.get(f'{self.url}?ordering=description', format='json', **self.header) + + self.assertHttpStatus(response, status.HTTP_200_OK) + self.assertEqual(response.data['count'], 6) + self.assertListEqual( + [s['name'] for s in response.data['results']], + ['Site 6', 'Site 5', 'Site 4', 'Site 3', 'Site 2', 'Site 1'] + ) + + def test_order_reversed(self): + response = self.client.get(f'{self.url}?ordering=-name', format='json', **self.header) + + self.assertHttpStatus(response, status.HTTP_200_OK) + self.assertEqual(response.data['count'], 6) + self.assertListEqual( + [s['name'] for s in response.data['results']], + ['Site 6', 'Site 5', 'Site 4', 'Site 3', 'Site 2', 'Site 1'] + ) + + def test_order_multiple_fields(self): + response = self.client.get(f'{self.url}?ordering=facility,name', format='json', **self.header) + + self.assertHttpStatus(response, status.HTTP_200_OK) + self.assertEqual(response.data['count'], 6) + self.assertListEqual( + [s['name'] for s in response.data['results']], + ['Site 5', 'Site 6', 'Site 3', 'Site 4', 'Site 1', 'Site 2'] + ) + + class APIDocsTestCase(TestCase): def setUp(self): From 17df8a5c4394785cb2e176788e468bd508c5a169 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 15 Apr 2022 14:45:28 -0400 Subject: [PATCH 002/593] Closes #8495: Enable custom field grouping --- docs/release-notes/version-3.3.md | 6 ++ netbox/extras/api/serializers.py | 4 +- netbox/extras/filtersets.py | 3 +- netbox/extras/forms/bulk_edit.py | 5 +- netbox/extras/forms/bulk_import.py | 4 +- netbox/extras/forms/filtersets.py | 5 +- netbox/extras/forms/models.py | 4 +- .../migrations/0074_customfield_group_name.py | 22 +++++ netbox/extras/models/customfields.py | 7 +- netbox/extras/tables/tables.py | 4 +- netbox/netbox/models/features.py | 12 +++ netbox/templates/extras/customfield.html | 4 + .../templates/inc/panels/custom_fields.html | 95 ++++++++++--------- 13 files changed, 119 insertions(+), 56 deletions(-) create mode 100644 netbox/extras/migrations/0074_customfield_group_name.py diff --git a/docs/release-notes/version-3.3.md b/docs/release-notes/version-3.3.md index de59d0327..1dd19a5c0 100644 --- a/docs/release-notes/version-3.3.md +++ b/docs/release-notes/version-3.3.md @@ -4,4 +4,10 @@ ### Enhancements +* [#8495](https://github.com/netbox-community/netbox/issues/8495) - Enable custom field grouping * [#8995](https://github.com/netbox-community/netbox/issues/8995) - Enable arbitrary ordering of REST API results + +### REST API Changes + +* extras.CustomField + * Added `group_name` field diff --git a/netbox/extras/api/serializers.py b/netbox/extras/api/serializers.py index e05d4083c..eed7f7603 100644 --- a/netbox/extras/api/serializers.py +++ b/netbox/extras/api/serializers.py @@ -88,8 +88,8 @@ class CustomFieldSerializer(ValidatedModelSerializer): class Meta: model = CustomField fields = [ - 'id', 'url', 'display', 'content_types', 'type', 'object_type', 'data_type', 'name', 'label', 'description', - 'required', 'filter_logic', 'default', 'weight', 'validation_minimum', 'validation_maximum', + 'id', 'url', 'display', 'content_types', 'type', 'object_type', 'data_type', 'name', 'label', 'group_name', + 'description', 'required', 'filter_logic', 'default', 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex', 'choices', 'created', 'last_updated', ] diff --git a/netbox/extras/filtersets.py b/netbox/extras/filtersets.py index 25477fbda..467ae23af 100644 --- a/netbox/extras/filtersets.py +++ b/netbox/extras/filtersets.py @@ -62,7 +62,7 @@ class CustomFieldFilterSet(BaseFilterSet): class Meta: model = CustomField - fields = ['id', 'content_types', 'name', 'required', 'filter_logic', 'weight', 'description'] + fields = ['id', 'content_types', 'name', 'group_name', 'required', 'filter_logic', 'weight', 'description'] def search(self, queryset, name, value): if not value.strip(): @@ -70,6 +70,7 @@ class CustomFieldFilterSet(BaseFilterSet): return queryset.filter( Q(name__icontains=value) | Q(label__icontains=value) | + Q(group_name__icontains=value) | Q(description__icontains=value) ) diff --git a/netbox/extras/forms/bulk_edit.py b/netbox/extras/forms/bulk_edit.py index e16f8aeac..b722bd751 100644 --- a/netbox/extras/forms/bulk_edit.py +++ b/netbox/extras/forms/bulk_edit.py @@ -24,6 +24,9 @@ class CustomFieldBulkEditForm(BulkEditForm): queryset=CustomField.objects.all(), widget=forms.MultipleHiddenInput ) + group_name = forms.CharField( + required=False + ) description = forms.CharField( required=False ) @@ -35,7 +38,7 @@ class CustomFieldBulkEditForm(BulkEditForm): required=False ) - nullable_fields = ('description',) + nullable_fields = ('group_name', 'description',) class CustomLinkBulkEditForm(BulkEditForm): diff --git a/netbox/extras/forms/bulk_import.py b/netbox/extras/forms/bulk_import.py index fa6d8af55..dabf2f811 100644 --- a/netbox/extras/forms/bulk_import.py +++ b/netbox/extras/forms/bulk_import.py @@ -36,8 +36,8 @@ class CustomFieldCSVForm(CSVModelForm): class Meta: model = CustomField fields = ( - 'name', 'label', 'type', 'content_types', 'required', 'description', 'weight', 'filter_logic', 'default', - 'choices', 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex', + 'name', 'label', 'group_name', 'type', 'content_types', 'required', 'description', 'weight', 'filter_logic', + 'default', 'choices', 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex', ) diff --git a/netbox/extras/forms/filtersets.py b/netbox/extras/forms/filtersets.py index 5d66c8be8..1710ecb89 100644 --- a/netbox/extras/forms/filtersets.py +++ b/netbox/extras/forms/filtersets.py @@ -32,7 +32,7 @@ __all__ = ( class CustomFieldFilterForm(FilterForm): fieldsets = ( (None, ('q',)), - ('Attributes', ('type', 'content_types', 'weight', 'required')), + ('Attributes', ('content_types', 'type', 'group_name', 'weight', 'required')), ) content_types = ContentTypeMultipleChoiceField( queryset=ContentType.objects.all(), @@ -44,6 +44,9 @@ class CustomFieldFilterForm(FilterForm): required=False, label=_('Field type') ) + group_name = forms.CharField( + required=False + ) weight = forms.IntegerField( required=False ) diff --git a/netbox/extras/forms/models.py b/netbox/extras/forms/models.py index 112911f42..b07853f86 100644 --- a/netbox/extras/forms/models.py +++ b/netbox/extras/forms/models.py @@ -40,7 +40,9 @@ class CustomFieldForm(BootstrapMixin, forms.ModelForm): ) fieldsets = ( - ('Custom Field', ('content_types', 'name', 'label', 'type', 'object_type', 'weight', 'required', 'description')), + ('Custom Field', ( + 'content_types', 'name', 'label', 'group_name', 'type', 'object_type', 'weight', 'required', 'description', + )), ('Behavior', ('filter_logic',)), ('Values', ('default', 'choices')), ('Validation', ('validation_minimum', 'validation_maximum', 'validation_regex')), diff --git a/netbox/extras/migrations/0074_customfield_group_name.py b/netbox/extras/migrations/0074_customfield_group_name.py new file mode 100644 index 000000000..e1be76b1f --- /dev/null +++ b/netbox/extras/migrations/0074_customfield_group_name.py @@ -0,0 +1,22 @@ +# Generated by Django 4.0.4 on 2022-04-15 17:13 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('extras', '0073_journalentry_tags_custom_fields'), + ] + + operations = [ + migrations.AlterModelOptions( + name='customfield', + options={'ordering': ['group_name', 'weight', 'name']}, + ), + migrations.AddField( + model_name='customfield', + name='group_name', + field=models.CharField(blank=True, max_length=50), + ), + ] diff --git a/netbox/extras/models/customfields.py b/netbox/extras/models/customfields.py index 49afe1bba..55caa4a70 100644 --- a/netbox/extras/models/customfields.py +++ b/netbox/extras/models/customfields.py @@ -79,6 +79,11 @@ class CustomField(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel): help_text='Name of the field as displayed to users (if not provided, ' 'the field\'s name will be used)' ) + group_name = models.CharField( + max_length=50, + blank=True, + help_text="Custom fields within the same group will be displayed together" + ) description = models.CharField( max_length=200, blank=True @@ -134,7 +139,7 @@ class CustomField(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel): objects = CustomFieldManager() class Meta: - ordering = ['weight', 'name'] + ordering = ['group_name', 'weight', 'name'] def __str__(self): return self.label or self.name.replace('_', ' ').capitalize() diff --git a/netbox/extras/tables/tables.py b/netbox/extras/tables/tables.py index a13054d56..1a0f5d58a 100644 --- a/netbox/extras/tables/tables.py +++ b/netbox/extras/tables/tables.py @@ -32,10 +32,10 @@ class CustomFieldTable(NetBoxTable): class Meta(NetBoxTable.Meta): model = CustomField fields = ( - 'pk', 'id', 'name', 'content_types', 'label', 'type', 'required', 'weight', 'default', + 'pk', 'id', 'name', 'content_types', 'label', 'type', 'group_name', 'required', 'weight', 'default', 'description', 'filter_logic', 'choices', 'created', 'last_updated', ) - default_columns = ('pk', 'name', 'content_types', 'label', 'type', 'required', 'description') + default_columns = ('pk', 'name', 'content_types', 'label', 'group_name', 'type', 'required', 'description') # diff --git a/netbox/netbox/models/features.py b/netbox/netbox/models/features.py index e443dde5f..4bd1b0e9c 100644 --- a/netbox/netbox/models/features.py +++ b/netbox/netbox/models/features.py @@ -1,3 +1,5 @@ +from collections import defaultdict + from django.contrib.contenttypes.fields import GenericRelation from django.db.models.signals import class_prepared from django.dispatch import receiver @@ -117,6 +119,16 @@ class CustomFieldsMixin(models.Model): return data + def get_custom_fields_by_group(self): + """ + Return a dictionary of custom field/value mappings organized by group. + """ + grouped_custom_fields = defaultdict(dict) + for cf, value in self.get_custom_fields().items(): + grouped_custom_fields[cf.group_name][cf] = value + + return dict(grouped_custom_fields) + def clean(self): super().clean() from extras.models import CustomField diff --git a/netbox/templates/extras/customfield.html b/netbox/templates/extras/customfield.html index 9be7a485a..0d9856938 100644 --- a/netbox/templates/extras/customfield.html +++ b/netbox/templates/extras/customfield.html @@ -19,6 +19,10 @@ Label {{ object.label|placeholder }} + + Group Name + {{ object.group_name|placeholder }} + Type {{ object.get_type_display }} diff --git a/netbox/templates/inc/panels/custom_fields.html b/netbox/templates/inc/panels/custom_fields.html index 32e586d3a..b18d44030 100644 --- a/netbox/templates/inc/panels/custom_fields.html +++ b/netbox/templates/inc/panels/custom_fields.html @@ -1,49 +1,54 @@ {% load helpers %} -{% with custom_fields=object.get_custom_fields %} - {% if custom_fields %} -
-
Custom Fields
-
- - {% for field, value in custom_fields.items %} - - - - +{% with custom_fields=object.get_custom_fields_by_group %} + {% if custom_fields %} +
+
Custom Fields
+
+ {% for group_name, fields in custom_fields.items %} + {% if group_name %} +
{{ group_name }}
+ {% endif %} +
- {{ field }} - - {% if field.type == 'integer' and value is not None %} - {{ value }} - {% elif field.type == 'longtext' and value %} - {{ value|markdown }} - {% elif field.type == 'boolean' and value == True %} - {% checkmark value true="True" %} - {% elif field.type == 'boolean' and value == False %} - {% checkmark value false="False" %} - {% elif field.type == 'url' and value %} - {{ value|truncatechars:70 }} - {% elif field.type == 'json' and value %} -
{{ value|json }}
- {% elif field.type == 'multiselect' and value %} - {{ value|join:", " }} - {% elif field.type == 'object' and value %} - {{ value|linkify }} - {% elif field.type == 'multiobject' and value %} - {% for obj in value %} - {{ obj|linkify }}{% if not forloop.last %}
{% endif %} - {% endfor %} - {% elif value %} - {{ value }} - {% elif field.required %} - Not defined - {% else %} - - {% endif %} -
+ {% for field, value in fields.items %} + + +
+ {{ field }} + + {% if field.type == 'integer' and value is not None %} + {{ value }} + {% elif field.type == 'longtext' and value %} + {{ value|markdown }} + {% elif field.type == 'boolean' and value == True %} + {% checkmark value true="True" %} + {% elif field.type == 'boolean' and value == False %} + {% checkmark value false="False" %} + {% elif field.type == 'url' and value %} + {{ value|truncatechars:70 }} + {% elif field.type == 'json' and value %} +
{{ value|json }}
+ {% elif field.type == 'multiselect' and value %} + {{ value|join:", " }} + {% elif field.type == 'object' and value %} + {{ value|linkify }} + {% elif field.type == 'multiobject' and value %} + {% for obj in value %} + {{ obj|linkify }}{% if not forloop.last %}
{% endif %} {% endfor %} -
-
-
- {% endif %} + {% elif value %} + {{ value }} + {% elif field.required %} + Not defined + {% else %} + {{ ''|placeholder }} + {% endif %} + + + {% endfor %} + + {% endfor %} + + + {% endif %} {% endwith %} From 25877202983182fd12a7b6a31dedca7edf5a589c Mon Sep 17 00:00:00 2001 From: Pieter Lambrecht Date: Tue, 19 Apr 2022 14:44:35 +0200 Subject: [PATCH 003/593] Fix 8878: Restrict API key usage by Source IP --- docs/release-notes/version-3.3.md | 1 + netbox/netbox/api/authentication.py | 26 ++++++++++++++++++ netbox/templates/users/api_tokens.html | 15 ++++++++--- netbox/users/admin/__init__.py | 6 ++++- netbox/users/admin/forms.py | 2 +- netbox/users/api/serializers.py | 2 +- netbox/users/forms.py | 10 ++++++- .../migrations/0003_token_allowed_ips.py | 20 ++++++++++++++ netbox/users/models.py | 27 +++++++++++++++++++ 9 files changed, 101 insertions(+), 8 deletions(-) create mode 100644 netbox/users/migrations/0003_token_allowed_ips.py diff --git a/docs/release-notes/version-3.3.md b/docs/release-notes/version-3.3.md index 1dd19a5c0..09dcfcf22 100644 --- a/docs/release-notes/version-3.3.md +++ b/docs/release-notes/version-3.3.md @@ -6,6 +6,7 @@ * [#8495](https://github.com/netbox-community/netbox/issues/8495) - Enable custom field grouping * [#8995](https://github.com/netbox-community/netbox/issues/8995) - Enable arbitrary ordering of REST API results +* [#8878](https://github.com/netbox-community/netbox/issues/8878) - Restrict API key usage by source IP ### REST API Changes diff --git a/netbox/netbox/api/authentication.py b/netbox/netbox/api/authentication.py index 5e177bfcb..2f86a1da2 100644 --- a/netbox/netbox/api/authentication.py +++ b/netbox/netbox/api/authentication.py @@ -1,4 +1,5 @@ from django.conf import settings +from django.core.exceptions import ValidationError from rest_framework import authentication, exceptions from rest_framework.permissions import BasePermission, DjangoObjectPermissions, SAFE_METHODS @@ -11,6 +12,31 @@ class TokenAuthentication(authentication.TokenAuthentication): """ model = Token + def authenticate(self, request): + authenticationresult = super().authenticate(request) + if authenticationresult: + token_user, token = authenticationresult + + # Verify source IP is allowed + if token.allowed_ips: + # Replace 'HTTP_X_REAL_IP' with the settings variable choosen in #8867 + if 'HTTP_X_REAL_IP' in request.META: + clientip = request.META['HTTP_X_REAL_IP'].split(",")[0].strip() + http_header = 'HTTP_X_REAL_IP' + elif 'REMOTE_ADDR' in request.META: + clientip = request.META['REMOTE_ADDR'] + http_header = 'REMOTE_ADDR' + else: + raise exceptions.AuthenticationFailed(f"A HTTP header containing the SourceIP (HTTP_X_REAL_IP, REMOTE_ADDR) is missing from the request.") + + try: + if not token.validate_client_ip(clientip): + raise exceptions.AuthenticationFailed(f"Source IP {clientip} is not allowed to use this token.") + except ValidationError as ValidationErrorInfo: + raise exceptions.ValidationError(f"The value in the HTTP Header {http_header} has a ValidationError: {ValidationErrorInfo.message}") + + return authenticationresult + def authenticate_credentials(self, key): model = self.get_model() try: diff --git a/netbox/templates/users/api_tokens.html b/netbox/templates/users/api_tokens.html index 01ffec23a..360e65a67 100644 --- a/netbox/templates/users/api_tokens.html +++ b/netbox/templates/users/api_tokens.html @@ -22,11 +22,11 @@
-
+
Created
{{ token.created|annotated_date }}
-
+
Expires
{% if token.expires %} {{ token.expires|annotated_date }} @@ -34,7 +34,7 @@ Never {% endif %}
-
+
Create/Edit/Delete Operations
{% if token.write_enabled %} Enabled @@ -42,7 +42,14 @@ Disabled {% endif %}
-
+
+ Allowed Source IPs
+ {% if token.allowed_ips %} + {{ token.allowed_ips|join:', ' }} + {% else %} + Any + {% endif %} +
{% if token.description %}
{{ token.description }} {% endif %} diff --git a/netbox/users/admin/__init__.py b/netbox/users/admin/__init__.py index 1b163ed06..ede26cd1b 100644 --- a/netbox/users/admin/__init__.py +++ b/netbox/users/admin/__init__.py @@ -58,9 +58,13 @@ class UserAdmin(UserAdmin_): class TokenAdmin(admin.ModelAdmin): form = forms.TokenAdminForm list_display = [ - 'key', 'user', 'created', 'expires', 'write_enabled', 'description' + 'key', 'user', 'created', 'expires', 'write_enabled', 'description', 'list_allowed_ips' ] + def list_allowed_ips(self, obj): + return obj.allowed_ips or 'Any' + list_allowed_ips.short_description = "Allowed IPs" + # # Permissions diff --git a/netbox/users/admin/forms.py b/netbox/users/admin/forms.py index 7d0212441..bc3d44862 100644 --- a/netbox/users/admin/forms.py +++ b/netbox/users/admin/forms.py @@ -51,7 +51,7 @@ class TokenAdminForm(forms.ModelForm): class Meta: fields = [ - 'user', 'key', 'write_enabled', 'expires', 'description' + 'user', 'key', 'write_enabled', 'expires', 'description', 'allowed_ips' ] model = Token diff --git a/netbox/users/api/serializers.py b/netbox/users/api/serializers.py index d490e8fe9..4b1f5bff3 100644 --- a/netbox/users/api/serializers.py +++ b/netbox/users/api/serializers.py @@ -62,7 +62,7 @@ class TokenSerializer(ValidatedModelSerializer): class Meta: model = Token - fields = ('id', 'url', 'display', 'user', 'created', 'expires', 'key', 'write_enabled', 'description') + fields = ('id', 'url', 'display', 'user', 'created', 'expires', 'key', 'write_enabled', 'description', 'allowed_ips') def to_internal_value(self, data): if 'key' not in data: diff --git a/netbox/users/forms.py b/netbox/users/forms.py index d5e6218e5..9720f92b7 100644 --- a/netbox/users/forms.py +++ b/netbox/users/forms.py @@ -1,7 +1,9 @@ from django import forms from django.contrib.auth.forms import AuthenticationForm, PasswordChangeForm as DjangoPasswordChangeForm +from django.contrib.postgres.forms import SimpleArrayField from django.utils.html import mark_safe +from ipam.formfields import IPNetworkFormField from netbox.preferences import PREFERENCES from utilities.forms import BootstrapMixin, DateTimePicker, StaticSelect from utilities.utils import flatten_dict @@ -100,10 +102,16 @@ class TokenForm(BootstrapMixin, forms.ModelForm): help_text="If no key is provided, one will be generated automatically." ) + allowed_ips = SimpleArrayField( + base_field=IPNetworkFormField(), + required=False, + help_text='Allowed IPv4/IPv6 networks from where the token can be used. Leave blank for no restrictions. Ex: "10.1.1.0/24, 192.168.10.16/32, 2001:DB8:1::/64"', + ) + class Meta: model = Token fields = [ - 'key', 'write_enabled', 'expires', 'description', + 'key', 'write_enabled', 'expires', 'description', 'allowed_ips', ] widgets = { 'expires': DateTimePicker(), diff --git a/netbox/users/migrations/0003_token_allowed_ips.py b/netbox/users/migrations/0003_token_allowed_ips.py new file mode 100644 index 000000000..f4eaa9f96 --- /dev/null +++ b/netbox/users/migrations/0003_token_allowed_ips.py @@ -0,0 +1,20 @@ +# Generated by Django 3.2.12 on 2022-04-19 12:37 + +import django.contrib.postgres.fields +from django.db import migrations +import ipam.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0002_standardize_id_fields'), + ] + + operations = [ + migrations.AddField( + model_name='token', + name='allowed_ips', + field=django.contrib.postgres.fields.ArrayField(base_field=ipam.fields.IPNetworkField(), blank=True, null=True, size=None), + ), + ] diff --git a/netbox/users/models.py b/netbox/users/models.py index 722ec5ba6..40ff78b98 100644 --- a/netbox/users/models.py +++ b/netbox/users/models.py @@ -4,17 +4,20 @@ import os from django.contrib.auth.models import Group, User from django.contrib.contenttypes.models import ContentType from django.contrib.postgres.fields import ArrayField +from django.core.exceptions import ValidationError from django.core.validators import MinLengthValidator from django.db import models from django.db.models.signals import post_save from django.dispatch import receiver from django.utils import timezone +from ipam.fields import IPNetworkField from netbox.config import get_config from utilities.querysets import RestrictedQuerySet from utilities.utils import flatten_dict from .constants import * +import ipaddress __all__ = ( 'ObjectPermission', @@ -216,6 +219,12 @@ class Token(models.Model): max_length=200, blank=True ) + allowed_ips = ArrayField( + base_field=IPNetworkField(), + blank=True, + null=True, + help_text='Allowed IPv4/IPv6 networks from where the token can be used. Leave blank for no restrictions. Ex: "10.1.1.0/24, 192.168.10.16/32, 2001:DB8:1::/64"', + ) class Meta: pass @@ -240,6 +249,24 @@ class Token(models.Model): return False return True + def validate_client_ip(self, raw_ip_address): + """ + Checks that an IP address falls within the allowed IPs. + """ + if not self.allowed_ips: + return True + + try: + ip_address = ipaddress.ip_address(raw_ip_address) + except ValueError as e: + raise ValidationError(str(e)) + + for ip_network in self.allowed_ips: + if ip_address in ipaddress.ip_network(ip_network): + return True + + return False + # # Permissions From 086e34f728c4fb873b7e63561bc901e9954a5ec3 Mon Sep 17 00:00:00 2001 From: Pieter Lambrecht Date: Tue, 19 Apr 2022 21:33:29 +0200 Subject: [PATCH 004/593] Updated docs relnotes to refer to 8233 --- docs/release-notes/version-3.3.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/release-notes/version-3.3.md b/docs/release-notes/version-3.3.md index 09dcfcf22..415e61963 100644 --- a/docs/release-notes/version-3.3.md +++ b/docs/release-notes/version-3.3.md @@ -6,7 +6,7 @@ * [#8495](https://github.com/netbox-community/netbox/issues/8495) - Enable custom field grouping * [#8995](https://github.com/netbox-community/netbox/issues/8995) - Enable arbitrary ordering of REST API results -* [#8878](https://github.com/netbox-community/netbox/issues/8878) - Restrict API key usage by source IP +* [#8233](https://github.com/netbox-community/netbox/issues/8233) - Restrict API key usage by source IP ### REST API Changes From fa4807be8ccf93aed93c42dbcb5231e7657c8e54 Mon Sep 17 00:00:00 2001 From: Pieter Lambrecht Date: Tue, 19 Apr 2022 21:55:39 +0200 Subject: [PATCH 005/593] Update releasenotes --- docs/release-notes/version-3.3.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/release-notes/version-3.3.md b/docs/release-notes/version-3.3.md index 415e61963..294f8f4d7 100644 --- a/docs/release-notes/version-3.3.md +++ b/docs/release-notes/version-3.3.md @@ -6,7 +6,7 @@ * [#8495](https://github.com/netbox-community/netbox/issues/8495) - Enable custom field grouping * [#8995](https://github.com/netbox-community/netbox/issues/8995) - Enable arbitrary ordering of REST API results -* [#8233](https://github.com/netbox-community/netbox/issues/8233) - Restrict API key usage by source IP +* [#8233](https://github.com/netbox-community/netbox/issues/8233) - Restrict API key access by source IP ### REST API Changes From 61d756c7c48d67417255d3ca277c09abfe147bcc Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 29 Apr 2022 13:09:39 -0400 Subject: [PATCH 006/593] Closes #9261: NetBoxTable no longer automatically clears pre-existing calls to prefetch_related() on its queryset --- docs/release-notes/version-3.3.md | 4 ++++ netbox/netbox/tables/tables.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/version-3.3.md b/docs/release-notes/version-3.3.md index 1dd19a5c0..9b061b7d6 100644 --- a/docs/release-notes/version-3.3.md +++ b/docs/release-notes/version-3.3.md @@ -7,6 +7,10 @@ * [#8495](https://github.com/netbox-community/netbox/issues/8495) - Enable custom field grouping * [#8995](https://github.com/netbox-community/netbox/issues/8995) - Enable arbitrary ordering of REST API results +### Other Changes + +* [#9261](https://github.com/netbox-community/netbox/issues/9261) - `NetBoxTable` no longer automatically clears pre-existing calls to `prefetch_related()` on its queryset + ### REST API Changes * extras.CustomField diff --git a/netbox/netbox/tables/tables.py b/netbox/netbox/tables/tables.py index 8c5fb039c..5ebb78865 100644 --- a/netbox/netbox/tables/tables.py +++ b/netbox/netbox/tables/tables.py @@ -97,7 +97,7 @@ class BaseTable(tables.Table): break if prefetch_path: prefetch_fields.append('__'.join(prefetch_path)) - self.data.data = self.data.data.prefetch_related(None).prefetch_related(*prefetch_fields) + self.data.data = self.data.data.prefetch_related(*prefetch_fields) def _get_columns(self, visible=True): columns = [] From 2979a64ce3c0983f2aee7c7a49141bb5c092e1b1 Mon Sep 17 00:00:00 2001 From: CroogQT Date: Thu, 5 May 2022 12:11:02 -0700 Subject: [PATCH 007/593] add file, skeleton from "select all" --- netbox/project-static/src/buttons/selectMultiple.ts | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 netbox/project-static/src/buttons/selectMultiple.ts diff --git a/netbox/project-static/src/buttons/selectMultiple.ts b/netbox/project-static/src/buttons/selectMultiple.ts new file mode 100644 index 000000000..465edc2f3 --- /dev/null +++ b/netbox/project-static/src/buttons/selectMultiple.ts @@ -0,0 +1,5 @@ +import { getElement, getElements, findFirstAdjacent } from '../util'; + +export function initSelectMultiple(): void { +} + From 2e38e621017e8d6de0057c8c767b60aee9423063 Mon Sep 17 00:00:00 2001 From: CroogQT Date: Thu, 5 May 2022 12:13:02 -0700 Subject: [PATCH 008/593] create store to store previously checked element --- netbox/project-static/src/stores/previousPkCheck.ts | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 netbox/project-static/src/stores/previousPkCheck.ts diff --git a/netbox/project-static/src/stores/previousPkCheck.ts b/netbox/project-static/src/stores/previousPkCheck.ts new file mode 100644 index 000000000..7fba2faba --- /dev/null +++ b/netbox/project-static/src/stores/previousPkCheck.ts @@ -0,0 +1,7 @@ +import { createState } from '../state'; + +export const previousPKCheckState = createState<{ hidden: boolean }>( + { hidden: false }, + { persist: false }, +); + From ae7ddecaa65f7ccff69352fb9d75bf775b6c0b6c Mon Sep 17 00:00:00 2001 From: CroogQT Date: Thu, 5 May 2022 12:14:15 -0700 Subject: [PATCH 009/593] now exports previousPkCheck.ts --- netbox/project-static/src/stores/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/netbox/project-static/src/stores/index.ts b/netbox/project-static/src/stores/index.ts index 42d4aa0b5..5e53410ad 100644 --- a/netbox/project-static/src/stores/index.ts +++ b/netbox/project-static/src/stores/index.ts @@ -1,2 +1,3 @@ export * from './objectDepth'; export * from './rackImages'; +export * from './previousPkCheck'; \ No newline at end of file From c536944a101d87bd38f1c33d1c14a0788e453251 Mon Sep 17 00:00:00 2001 From: CroogQT Date: Thu, 5 May 2022 12:36:17 -0700 Subject: [PATCH 010/593] now exports multiselect function --- netbox/project-static/src/buttons/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/netbox/project-static/src/buttons/index.ts b/netbox/project-static/src/buttons/index.ts index 6a9001cd1..e677ff599 100644 --- a/netbox/project-static/src/buttons/index.ts +++ b/netbox/project-static/src/buttons/index.ts @@ -3,6 +3,7 @@ import { initDepthToggle } from './depthToggle'; import { initMoveButtons } from './moveOptions'; import { initReslug } from './reslug'; import { initSelectAll } from './selectAll'; +import { initSelectMultiple } from './selectMultiple'; export function initButtons(): void { for (const func of [ @@ -10,6 +11,7 @@ export function initButtons(): void { initConnectionToggle, initReslug, initSelectAll, + initSelectMultiple, initMoveButtons, ]) { func(); From db142061ffc77cd16bcc89df199f070697873694 Mon Sep 17 00:00:00 2001 From: CroogQT Date: Thu, 5 May 2022 12:37:28 -0700 Subject: [PATCH 011/593] clicking a PkCheckbox updates state --- netbox/project-static/dist/netbox.js | Bin 375393 -> 375642 bytes netbox/project-static/dist/netbox.js.map | Bin 344719 -> 345022 bytes .../src/buttons/selectMultiple.ts | 20 ++++++++++++++++-- .../src/stores/previousPkCheck.ts | 6 +++--- 4 files changed, 21 insertions(+), 5 deletions(-) diff --git a/netbox/project-static/dist/netbox.js b/netbox/project-static/dist/netbox.js index acd1abbf28c867420280fb8364a550d7f54c9ec4..3d6bb9d1a610d4977fd9c887d49450d097f78f7d 100644 GIT binary patch delta 27130 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 }; + +function updatePreviousPkCheckState(eventTargetElement: HTMLInputElement, state: StateManager): void { + console.log(state) + state.set('element', eventTargetElement); } + +export function initSelectMultiple(): void { + const checkboxElements = getElements('input[type="checkbox"][name="pk"]'); + for (const element of checkboxElements) { + element.addEventListener('click', (event) => { + event.stopPropagation(); + updatePreviousPkCheckState(event.target as HTMLInputElement, previousPkCheckState); + }); + } +} diff --git a/netbox/project-static/src/stores/previousPkCheck.ts b/netbox/project-static/src/stores/previousPkCheck.ts index 7fba2faba..a5d06ceee 100644 --- a/netbox/project-static/src/stores/previousPkCheck.ts +++ b/netbox/project-static/src/stores/previousPkCheck.ts @@ -1,7 +1,7 @@ import { createState } from '../state'; -export const previousPKCheckState = createState<{ hidden: boolean }>( - { hidden: false }, - { persist: false }, +export const previousPkCheckState = createState<{ element: Nullable }>( + { element: null}, + { persist: false } ); From ea9258d36cabaf5de25c45f65fd92bfc5959bbe0 Mon Sep 17 00:00:00 2001 From: CroogQT Date: Thu, 5 May 2022 13:23:43 -0700 Subject: [PATCH 012/593] added main multi-select function --- .../src/buttons/selectMultiple.ts | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/netbox/project-static/src/buttons/selectMultiple.ts b/netbox/project-static/src/buttons/selectMultiple.ts index 08b5165e2..68cd57032 100644 --- a/netbox/project-static/src/buttons/selectMultiple.ts +++ b/netbox/project-static/src/buttons/selectMultiple.ts @@ -9,6 +9,43 @@ function updatePreviousPkCheckState(eventTargetElement: HTMLInputElement, state: state.set('element', eventTargetElement); } +function handlePkCheck(event: _MouseEvent, state: StateManager): void { + const eventTargetElement = event.target as HTMLInputElement; + const previousStateElement = state.get('element'); + updatePreviousPkCheckState(eventTargetElement, state); + //Stop if user is not holding shift key + if(event.shiftKey === false){ + return + } + //If no previous state, store event target element as previous state and return + if (previousStateElement === null) { + return updatePreviousPkCheckState(eventTargetElement, state); + } + const checkboxList = getElements('input[type="checkbox"][name="pk"]'); + let changePkCheckboxState = false; + for(const element of checkboxList){ + //The previously clicked checkbox was above the shift clicked checkbox + if(element === previousStateElement){ + if(changePkCheckboxState === true){ + changePkCheckboxState = false; + return + } + changePkCheckboxState = true; + } + //Change loop's current checkbox state to eventTargetElement checkbox state + if(changePkCheckboxState === true){ + element.checked = eventTargetElement.checked; + } + //The previously clicked checkbox was below the shift clicked checkbox + if(element === eventTargetElement){ + if(changePkCheckboxState === true){ + changePkCheckboxState = false + return + } + changePkCheckboxState = true; + } + } +} export function initSelectMultiple(): void { const checkboxElements = getElements('input[type="checkbox"][name="pk"]'); From 1493c920fd1a49f8aac53584abcb61c24a49078f Mon Sep 17 00:00:00 2001 From: CroogQT Date: Thu, 5 May 2022 13:24:12 -0700 Subject: [PATCH 013/593] silly text highlight workaround... --- netbox/project-static/src/buttons/selectMultiple.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/netbox/project-static/src/buttons/selectMultiple.ts b/netbox/project-static/src/buttons/selectMultiple.ts index 68cd57032..62e66ed0a 100644 --- a/netbox/project-static/src/buttons/selectMultiple.ts +++ b/netbox/project-static/src/buttons/selectMultiple.ts @@ -4,6 +4,10 @@ import { previousPkCheckState } from '../stores'; type PreviousPkCheckState = { element: Nullable }; +function preventTextHighlight(): void { + return +} + function updatePreviousPkCheckState(eventTargetElement: HTMLInputElement, state: StateManager): void { console.log(state) state.set('element', eventTargetElement); From 3effa37fa77a5123c65dd85e6f87599b99b3b133 Mon Sep 17 00:00:00 2001 From: CroogQT Date: Thu, 5 May 2022 13:24:50 -0700 Subject: [PATCH 014/593] click event calls multiselect function --- netbox/project-static/dist/netbox.js | Bin 375642 -> 376041 bytes netbox/project-static/dist/netbox.js.map | Bin 345022 -> 345446 bytes .../src/buttons/selectMultiple.ts | 9 +++++++-- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/netbox/project-static/dist/netbox.js b/netbox/project-static/dist/netbox.js index 3d6bb9d1a610d4977fd9c887d49450d097f78f7d..b7095fa78873efc9370ca09754b6e9cf3b8641cb 100644 GIT binary patch delta 22477 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 O55LBxFYi`+XZ=4EDc3Im delta 22089 zcma)kd3+ni_4sFYrQCOXu@mMvMfCEo-^w`EJK`|w2wZ6T$Q z5SC;hgm5%aDCGzNmbTo`au;Z6%TXxMavyJI9+h@1D1M z_y0;>`cKJhtyahG9L=~B<-S9+tEmCkp^D<^A%BvIx!6lCFFtf&mWqkwALkr$ED;~L zU42NZK$$}?)*_Esj6SA3F7ZjUO01$vrw_QaDS?TICd!C-F+*`^SiGBRMrM(w?IwP@%YM0r&DW}=hBUj>uL$LbSv0u7-U-H z!7Xw}as0?mdHPE6fg?o}=fn?wPM3&hADJzFexw<3Vm)5FhAcWV$n+Ez<(+IBELz($ z2|q6$z*>2x?o+?TrD!D?)HTRNV-p7wim`DwKxzeqh^H?zAgfq@c~YLdbC5|E`k9<; zhZiPK#tH{#67Rmewwlb?JIJ^TJ#7jYjK#?gk`eC75f-uhXdSYNdyZOVpju9yJlcWe z$3}6+u~qU@kH{RmY8jcfd60<}rX6y!8K4}dO?>m%syVHIU}yh8Z1>n$o4DZ0qOw+J z9-|{}nf}@bnP6dThgg5*xu`?@-IeX~#C?NIrtoqwe?}RRJ60@~Uo}It9iPDre)pVu zlymYM8Bbwg+{u~=?KFw|j+-|(ITb36JGFjbBSDxvV+Ep=kr^(Gt#`5pra}DTc;DW7 z=lTYxPM?Y@)TQ0DwJ_48tST58M`2`T9uFf+<%FWc_a`=Z`-#4n_nJ}1k^wDn$HRTVEDymk}S=M*2gcJ&Uj*QAj#73T5V*~$>W zm9onsixKQ>1EFWrBt2L7?I530cJaAObVv|wKR=@prm@8Gsvwb{A!KKVU?dZ@vmFF$ zNjuv`2*+(_n@D@ut{kBk`jfgXme(1ZYb`MlbqRZ&Emjjq3*J;HDgb#A>KY9N;E7Gz z^W?_5?I7fJj8`l_v8E(Unn^cZ!kJsEFH_w9I>+_1lK_YL$!mGIF8xtuyKiMX-Rp+t`8{m1pTx| zd3WRD>pxp59=pC+=Ielwi5KSX_Og`(+Zpl6>s=@#uK&eGS*-btOnsq~$F4|~VK@?k z`lcA`5zqZa-PU2dwkf95<%LR@!omYarm`^93^2^Ix1%>XI>8p!vb1 zyziLUc*Ck@LS+#n(_5IuVP`wZ!r43^Ihc*$)@f&Xf{|XYLK`jl-Pi$8vMMLucEj4G zyq(S3_1%8=kj))qI^9e>$@IDzt6e;FX|brjaVcf8;|Euua&gyy9fVadHfGv4q9s+hDfVSl^+ZbaVgy>uCY-3Chvojs?lV1o#6J9?vXlL{M?MR8+ zeswpph#&t-Q`V8EYC)fjrlB7gAl`{*c2G3jtXpKW>qgx|T}tTp3%glCs2LK6Zob;o z3db-IO6m#&+wu+L6Q|mwoJ_{ZG#9oLx3e~P3M>wPC=8O_eoOs^CVNfXt|Qzrfrp%e z2al0yFAS^)W*{oRL44qr4XOsawqE@7mdY9Az=w@YpLokH%T(?h(=D>MR(Dp)BvPB_ zO+naUs2d?0T@Ora(A9L4=c#ft5s?vZ2BMElXzs4O6zR0HIfB?;@%~%4ujo?bfm>iZ z?b@iI5Br5+Y5-)6!1juZZ+o_u0OT?4O}dw}V&vxCH} z2t~k#DH0dlH3zkbbtl(=c=n%c1WAAM$@Q}0c8IT>yr2T+Pe$?ru-VQw6XI^Svn_-c z`s{4GUEF`Wqqtd?Pkr_RO4~t_iyz;SfC!txR|6%~GAoJosI@7#*KV%wc1HdxesLK&|OY&G#x;{ zpHNlhQ!1cSBXSQ7d8J`03?R}C+OWWQ1qKGnER+|iskD65RwEOWp^lMtOT^m_tHisd zzU5>`CL}P$h5B0y3ExIAR9v z06b;DS(7oC7MM+NC<1F04fiZXCb9V*9T@(pdny-#I~EiABf@Ut&1D2|$Zou+9o~I; z&l>n!aqkxR+kNjN^u`1E~CP@DMHeHE0&fKQgFmg1L7RI3(7l>?0%8D7Q$ zf2!mDnUsvSda$bsm}wFt51!f7WXRLOkRsX|jEtqwr9RI<4f%tqG$>BW2+*tnG*-;- z1>ahutF@55tejP8*vW1&j*V5+i`yUCibllfL!8W(BSxmJFl&oJkw%T;6Ax{2w2(7q z8Vv;r)S&>O2hB}O*RFUX=xTvUICZJkC zpO$!eAUOIA;)M@apl0!whd0P>aTnM@g?0R#;<^n^&gbVu`VlQp4zbh7bQQX$IK}u9 zIJScjZ&O5(tqD#R(@cnCu7PNKZVb-pwk=EQp*HBxEtF&{0byqvi3u`^_)(IVO(TcsttY&0^Bg{cCZ5+`V!L`X$| z)8dL#s0h!QuUa7f<}rtl4C?_UTo~r#6q!E2DclwVWD!Ur&`yxj9ARUeR%0={+B)n` zJ6JtIj3)`0=zy> zeC??vqVz6==CsZz6G_9fC;v#s9DK6TKv2^TYCbq& zE@YL%08Nm~L*dLrkqEL*PMr4CCMw5?wNI^}vYgoaRJAM<`ix9e+0+g@$9iBhdP^S4 zwh4SOT>iXyAW5oy7PQ2@Bm#jNdamHKdI+W*` zHcmAB;q=TQ4lE-OW}K*frcU0b5NABfHfObRY?v6{9e~GYN|p`g_u>NxNOPQ1)MpbX z-v3M&1x4wzX{v)eb>P{}6g9|+kNojJOX8ds`Ay`suGSw;`pa86@sEGnA+MK+ zGY(}vs-6?|&zDgRoY?yOGG8SSZcv{D>v%+dDp=uKS5Q~$2o_A822RT*67FF}BNrN` zz}AY#Jz5E91@o5~B_v(r3Nk5o0%og$^`3fu9W}z8`tS2)NQNd9XPgQ&8?!-0;C1II z*a@slD7cGLjIK^jab_FBT8HxSSGr z@g@k+wu^7S*tyr%uoU9Ft3C|hGg5DV1g<)P^jyN)Dp6w*`8sJ%u9AGreYWg+n1N?f;ZVlt(#jE~Sx@3T7JN!X&E~br! z*TX-}x=MoS^n}YeV;Y<0wO(HQ>Tenf#+1LZoEqRo_LY@;`LxyuVoLDUz|%&Ek|aVA z^0=v>Pn#5;9f&jW0(3E6F-s$P=z;)15M&A&%?t(=P?KRc#_MD5MAC18fK7&HH=NbK zzIu0^p}!}$djsR)!Tf&l75!chUss^|d{88>ZG*fvLQYGko3Oc)VMT%M+_}>LK4ga1 zr~Jtj2~X4+x7HX9;Of^T!+D#;s-QR$XDSPzo8df&f9J}B?%-!Am~IFXbgf>hs8dk& zE?(FEznFfB+uE`5P;FyY5Ui{*X%cx3T zy!0PlYJ?|Y5vqw77yNV0g6=6pbfaCoX#VH#sUcpR`)1AFL0%si2a;gAc-EgO2;EV? z;P!<5HQhWDa3@nO{^6PioS*Mei7RVxR_!NH1gsLZ|-#V@_Jd?vn*W5jEyz&{Vn`%xl-V| zHidK@aVAk%phpotOmlvL_WXil#aniAiCYX?e++P8a#H-#uT|?-{8E4XkK|+2|!paI6 z*mTIM_(f5u6M2=ivQg6R79V@(VH6cFeD`dKu+Dz37r90MdzRva6+BU$Zj`8%v;fo+ z6xfjX@O!&aQmlCY4tZ(2_|p47TS=DY;!LElbl9rk+m~U(r_TMrr?`(im;i+?Uh%&l zCcpt7`bfElv5L2Rw6r&ERcJn7Riv0XBfHFu&#Kr216I~e6iv*^wvs(WtZWA<<7tIA zn$k*KAZTTSWN#U(VkddT^3zEa5HCG#LO$`u(`Onvi0@7WZo$2-h^MvCR99iFaR|ya zgRI-Z6J%LKouP(175KOxp#kxsPXcg1sQcH2#SyF4LtJ&@bjw#w(AgtC@vn-tF{{=g zFr3iu1{ZQPHQ-Lwa8P%+%&pE2i`h|=5WpQt_dGFPX+=fD6HX{P%VUUoQP@1NgR<&;ATv_p1Nwh9Ktnf11n9RxN}V zqH@5DamW?L=mDJ&^3~YIxBpWHp@3e#*$%d8Mq^`R#n-=$G%v% zfVb)up#gKYL|KbSeYp*Br)s{ui;`({Nc`+;W^bDnv~yJUL&}xg8Bo%#R#1FrCfAR_ z|H=(Pri0Y`tJm)~y>T$Uqg+AMqh9)ap(5CfPmKcQ5 zKzT46^|-@ezeGLa)!*n*gZRuhH8U!$Y`t}CtU+A(?MlN49NhPuBc|KRj#vw~RZN#v ziI+wow%IA3^=&z-6!(5xO7&Xt+!-JSzW%lzbsgS?oG6ZG%|x4_sGEs;*TR*vNvH<$ zg!^WAfZ$LkiM3S6g$a!v6zV#y`2Lwl17jixt(h4Yc0!l2u{d5Z3$4KqAaoAmFh`+% z&|~{-v=)jzv(f$)K+bJ~J`heP2UsV09-pui%3JV38l6DBc(V$fOEp??`y8aFdaQWg z98^)#XeH9Fs;b+X=Q48b+lE)E(N@%hyVU5)O|4c&1Ca+}Xh8IC5Kuwv2*PBAXf8lF z*<9BIo0yA=Q3u{J4Xs?&V$B;For)-J1Dl{Acbctu;XG7|4^BhVhuVLLDEhuf6n@hW z6Ge)&Paz6Hu1_I~glUo}LKsa)^;Fa(KKsoXGm|DZWEvZTn(>n54#sWzUm^&bShs1C z2)w3AB1lg>hXkf4J*kxo0xfA0&-u0l1@N31D1e%Aat7j%7vDMK`!rJZ|3xEdeBVs8 z`UgqmI)ysixO6ru|1OEZn|Ei!@;=-)2W>zD_?$UNQ|dD*#~(ILIsSGl{`DNR1^S$x zgRb1z_nq@k0TzDX{DsNpx&*#&E-KmKpVBmvsaX%sZP%m_37GO!k@Cm}LMzx>o(Wqk zh8N63+m=L3lR}~)e;Ej3B8Xk{&?=O{d*`7g$b&DN2b&#MPSmWNXiJ72h7*MvoG9*_ zk7gkk_RmKK@#4j3BO1aN6r+{cyBMu2;-Zsq>AhjUJHcAEMa>`(dG*6%eFkUGBCMnD?;UK;@^Zo=0rL>WtyNvi|J97^h4E)w~7 zx!6${Gf*S=v*jE_eB3eesO8~M8>3(>$sTC_?f9gytH-lTQ8j8H#nR~j2_f$zrKl2* zm!gGe1RpF#Oc6hI7Ycft@HLA8z4fNr@4O^?Y4~0n*-Pd3+Q?o;ux1I`gj#Ud62#cM zWzHgZ))jpj0>#)^r`(jkpRO><5*7UGM!9)x%psR`c2kYVG$C63g2HXR__-x$^Q!(2KEVDcZ!i$T)I$HZcg6hI?@6Rg;N^eC38o^mO5UOVJ4G#9u8%m7o$T zmZ7^)48O1pJ-(6$vuq8jg_*-PCGg?3$u>@{!&C5R!ZxiJhsw~38Jr?` zdhkVMXelz|Uz7n(%=oD?bOXta7zpI=^mvbA#JZHp7^{bM+4M>np>`mk?`(B+wzb(i zoMw}&m*qVqY>6LUg_^1Sv!)yYxxnt*n;2UGMu$zAHk+R|XJfO4X>F4LJ8Wb^l*4zH zqej_7?rUO(3e!0A)7bOVa5mPCD=JVC>N}S>C2ZN~|!{16EqA zhc%JXWK-_ z8?r%ulC8D_SFc8^XItbWBtZcl_pe4*Bdhe)YP5j@;losc5o5!*RG~eH$Me^sB?|{_ zinkH>uukwQY6tQ5wWvXcJb{0-7J*k_lix7)`6Fon$Vu)%lTC?L)=N*UMbi+f$Iofe zvmm#w)uE%P3A5`^nLJMj?^y?j-GE2dp_=Utwmj$LFO6%A1+UT-khM~@p6M>2x-w5y zBlv@L=w*;8kF7`DAXCa2v}7AOUw1vzS?JxEr|KS?Vk!i}(P)Bcwvn{Egg+K`gMw(c z;V6SvQGGUiF@rqo`fL;Y6NT(XR$*Vbl>-De+T;)}1QXkB5Z1>ls?n!lp)Js(3jlvt z=+Vk09!sq~0RBfJ zR0ReW{%;HDSq^Wtq9%En?s}%$C#`5YS>{jeXgd^hI>0dFq}C49f}ncTfjm%r z?Lg~cIh_+~ap^)Q`i#6hw+l5u-^aVq+w+=>`!a{zTy(C@+lQ~^6+ z(n3Ec-7$cUs-V#xK|6~eEj0%Ax)yR!C$Ps!*F*?=aQL|>S_Z`jQN)&lAL{^tN7N$; zu0uX+T>>sarCl-9ML@xEU=2W0ty9oLFe3sT;G$F?N?HU+brDjm2V4kFdNqztLAEM+ zryJ#@%aiCKveKCuWQF3!4B7yhvs&G_2OJ6I7F0W-0JL!!6373^pfjMWAqy&&lg`Sb zd&y>2jDf?#;kq%f$vEs911ZYkACDopy_IemL)%pg0Jc$Z;{ljp(4Ww9_&?{MqyN(? zUA+(G5Nw2%4FOQSaPblW&s|r`K}Z0=lF`LUXZ#2?lZAhLKB|M_k@L|e`2;%anfT-h z{{jeU&Kkm&ef11KOiW9DxS$3aEtA@XybK*crOkw6fbR2FYg7>GdN8>a~;m9w+Y!jqozl1bvczw~$Xf8s2e8ny3DwWgB^3u** z(VrK<0Tn%fc2eMXXgdVP3j3Kj@$y2`Xr8mDQ0q6f z%k2rjFX7IzEdkPr*iwF1{t04?el{i7Qqf>A>}R8R8lt{f&}RmW^#X#qto zSD~=POr!3ZT^fSMyeu6jo~v14!-BRMf4h)+1J3Y`MN||ax71NYRZTXy#gLY2~LGTSJ2w^AkeAWton9^Ou!oTct8v`Wxwrckf+XESvf z0`pO5r0zwH64OMD&I3?y=%R#~fXC83R38mgnDA0}Q0-<(>!UtbQGI5qB1FA98*xX{ z)O(B4E+*hAIG$L#!+n4k=@zOIf#(I4rP61&QfE`MT`tBS2*53LjYB&7 zcIs9dl$ic5>MkhWyo=h6vJx**$0#UY`!&^t=$wlcZ#`a$+kQ*^2s!cdzoni5ljy`f z)CV8|4&6)Dsz9*C@w4|*+d)`?H4OP|0^Hl$0|D935UNf2{IUXt+a-W!E}HEufMUl@ z_fw59;l=k;b)ci3y`L(DuL|%x4^YRU_525^4ye8KAay6`gNq*mBIfX^hbSG?UU`Tj zqOtU0Y7}bX!&EzXd@%nyY5pVBDjF&q9;LM4{z2OcDfK8dms)6YF~rv)5q()v+wdii zQ|HgKxky?c#P|u_TJYK@0HYRs{u9*su=xBZWytJLQu6^a$@LWV2O14Y`ey;s@*x+o zk!#{nZOE(T6GT9PYA$TJ4f~%1e6>j(f21mj+ROfxs#s36DaSD?wzKjDvPY@$1*(U})z4DX@%1lLP2efQX4c}f-=U^UEB{9AB)aI#SEwaR zT3y-*EKRa?h{7Thd)+CJ`xm}KJqHt=^(u9cXz-8zPW6y^Un6MlkX)}*dIUbFjG|w@ zL8+;EgRb#OF&M}9{*!tW%(;nW=iwLLq?%!d)o)QnXkOFQ&h_#%*!?zjH<*LV-l2Bj zYu}`{N*BIEZ6$y{{4TWv#qWBLT2CNh-lr;{Xnmi$vm`F`+aXTeCl`?O8zKrfj+cHw ztyaTRbzI^;pjJ>|L;}Do@#sg?McDKAwIPhoAm0 z#Q=YO@n32g0l4@JiY33u05ly+9P zE3dA)@H<~o+h%o5)H@~J*Fd=-0aOYZ!>CyrnL(?emvqid+Kg)2 zUF?Sbv-G>yY3tZC_4;+XGxcNobsLyIXeadzjNid-sHr*A2hm<8<0WZ;oR2j!Dg5m$ z+K2>foK2Sj^|#HYJE#)Qw|@Qj9&($bPltTwj7+;6zI!g+hZ1=4JbIXDzJbIG)d~Mjv z?$K9{h8^R37%#k$a~-SQv593A#TFW*Gnmv0!58$#Y8xn4Qy zC3D3l7mUDT!0m(Ym4f(%mGoBR#`DYQ-=i4*OBrA=fM=|t*Ub((bhQw|Az@v7jhil^ zBMu$@cJ9b753LuUqJt0=Hwla@34>RM18$%A=_R<%1woCY6||OeIqVQ1 zbQ5w&ts44H+L2Y3^STq!bkZNzFqH{;!7N!Y?)%PyIp_-!zG&9!55NgHC#oHx-~iO^ z4t9&dz;L*$if#eAxVDPEd9T&Mj`rsqyK7uYrr!x=AUe*N0OVviqKKD zTIkp@(FVMipKuVGV$l%MaZ}cgPqwCG`JPSC2If^zStP#+XbVT(%4C(JaUX1~0ouG_ zt4vg|(|l_sAZ`-j15u!9_$VU-lr;ixWW;Eg?kSzBi3?IK3yCg#m6l$zwDW)73ZP~* zOfP;`OP3b4!0Y@@s{Mp}iFEl=ORrtm18*QMt(>+zuZC&o+H&6=|dE<p#H4yjsh!h=o;TF0ZjDpY>Itg;|l`TZp znDF~s>9eUu2YzlFy$t_k8;Bae^z1g8rWbTMYPucd$~OR)M2?zX=^r)pg+!UecF^B~ zNs42;=nqv54%ncZqt`B}cQ9Jw@(}h0hoTF7I=Fq*3ixV{z7)jbmU;j?gm>4|8``2- zpy+{UwsO>w1Ug9Ot3c0k9Srk512X-~)v!|a`6u3cp}`xL$p$)9jwbNm>S<;z=LW)w zhQsby(hmxw3)nLo@}&kq$ic}B_!T}3NE>*1m_`BV#s<0-QPC{kcMH8jDsG}zplO^N z@_7W@z7G!f>|d#8;Z6(v0La&pX8J??c_Z{&)k60w{lZiG9kbHcpfKKP1DO}X>Q+!Y zX{n`^W>E>uSp%O5c*E{w(i%#p^lqOIoay6s`fY@~(s}LlAyn-H5J9l{Am&fFOT*NM zWJ%-80)n!j4yTAV9-AC=?cRYbyOD`ySrW0%=NRp&9L;&h$0|pyZt|CRkMDsy@&Y>R zLoiuz3OeP63^XXGAy+;5Dqvf#4?{hVZMoh8;)vAoHx9b8$qa8n8WG-yx0Rz=qCIjU zU{p9?4da4skv*kj8pe@-mN_+>ugEe&16Lt~PI}iOC$#DXcgEvRR6_w$6W;^vz#LyY zfwXzNyb}s5KBE&@!Gw2{0&X@sfx<2L1Sy8_qn&h2)s|&P@xkMCg>+^YokrA97N0x- zqJHlQ(3fpld|-rLCN1cJ0qRT^R4}1|#Zq%G?V-Tfzqz0O7uBKMc|J(?9?$OaL)<;O+!HQalI< zjzoR#aJ9$n9U>>AVdBzT33?p`cJRV9&48)5GflTpty%oQ26`pFAx*CbQ~CKcJ%m8) z?8?$b)0zlT*5ke`98CkhB1`iNDuE6Zo*f}NYXpCkrMH0nH9(J;M@QgL30Wa`EMAze~*jHVD(Df9m_{J@*^G%SwO z_kqpZaW?%Y1Usubhpq$LGH?#fP*gW`d;t#o#&hWl=vgok7%@BHx& zX&F9jy9`cf0AF+&eLiqR@#S=5Q94KNPvr}#`e+oaA3^KGvCHWa=m$j+zT$Fv6h(09 zQCbIDfICXxNx|69kI`Kyh+D6uOG<({z}Yb1oG5abBNYNR$>5PI={0csd-at-%fr$i zucRLVHpBQTz<^8o>?%5jN+UT&ajE#I5C{d8BzDFjja@_2$igKj2-^yT9ZiGbk=X!EQe>@OK+jLBtCf` zU4{Mk(k9^lU)~E!j+b`aM?Zl;vQB@1evm>L>G6l?7tuUB>l)_f?iJ9 zrsBsWz4|Eq4pLik%uo)GOW<|Peu91y_5~ny;p!*p_gA&SjKGpfZvu>pWDEpLn_Je& zZ3MPfyzME#Xa|lwMep1^2uvfJseppY(KgV#Oq}qXJT~9Z1Puz;!u!h6R{Zy;=+$NQ zId*NqYJ^pQLgYc3-D{bK9De;JV5Kcj)APaNGCxgMZX5xqrkDxcQ>#F%Bw`7+543_P zfj|`eeuQ+z(?IYb*Gr$FPpP_caIx`+XJ`&+W5KiZuP6}i_x=$auKC?La72}eDWuzs z;P79;Wo^Ls{*~^5#Y*nvGtRW|hGhGv37*-n(J?fEK5F}dCc^}P6kdgbV} z7P5vPAAgB1tK@-p6ne12lVXje;3p`i;7-7=y+rS%5>8o^;Y}~oXOxDfb}8U9ihuMn z-A;Ap@agO6rTFyA^a%(GT>3Y_Ya?Fp3Q%D)1_@Y&hhL$+5S)Ofa{Tox^fJ`0NJRas z^u0h_AG`|IWE9{0cluqZ;n(O4xb*X0r%M*5obWRaaOp6C(U7#|br9#OJ~$LS^ahQW zCjl~o(v8ODaA80LTR;NOdJ8O#2+q7ke}Q~B{x-db8gSx6 zZ_yg*v$yFMTJ3f+9w#n83~D2c7k&U5^n&;4CCE#jEAS)llW4;GbT8ic0ev=zo|7NY zmoMN#%DsMSgzZRZI|gv{Bf5BDD|}-{GWfX&8-k3)+E#qdhqS(#i;OcQ>y><$R|6Gg zXoo>bUmP^BQIfuxOv{6qxboGf{3V(-QXUe@2Msz1sJYW@2mbGeaENid{v*2A(4Akc zYvRklP#An^rV~E+BOgxn8rV+w@-85^Li$@b0Y*br^x_vjqE}H}Q$7T&@YR!T zw#NJyHC&LjL&A4$qd4bH4Nje=7i{8!uo>_?LO}?*2J?G~=D(Rp@=98*n`bR#T$CRh zOY&GcO|PtS^UP>kNV)_5nnG^%a6ToyKFBta{=+=};xx@wWO>#K!05dL?gW=&a`^=Q zk)ZNHx|7FeeoQYfw?oFd^63pHKf#R~V4wl=i@V{|8|Z~UrpuPuAs>43vzqSUMD9(F z$IpBW7~y#Q>BqE#%JR7B6ME~yjEsynNcUBcF@z6(0-VR=Uw%S6AXNR$C-hHdEbbxM zc`45r@CF!??(hbtSvv9siJmp%>%Ia^{pm>a%}EDzoA=ZLBumE)Om#U z9RB$?pvvO-op0!J2+J~A0iW|F5aot%>Fv{~b`uV%RS%MqnWj2U^_lQf(^N}l_-*XQ zEe43=eK1Y68Vn`8V20`@VETJzsCJW1%V(Mw2x#g?xoooa&k#<;E&ypX40AJq^*DKIV zx4waVa|brwx>YLeqL>A~2OfYwEgpsQILWt4^&Gjt+FGId6zsiq8r5rapq#xyWt;&e zyG>=Tgz`uIs^82917zorsvj`hZt&giJD2fY+@gv39n^fuGUhkr$ z>IpEjds3gs*sJKA&B5ZNjg(?r; z{WH}L=m*b(*m9Ao7JqSp%8S4HnQ8|9<%O!v`0}5ruE#&V2o8z|SZh5A_~@n9>rVQ? ztb$AipQgt8gYDJsa9C4g`rbPYSb85{q1!pRkLvQ6)G!8fRQUyHK-ond4=jY7_ZJ_s!PClefyZ| ze7Gi$Ua8s#F^NyER9%BQv3MN#u3q}*aaGwg=(^)N)hevHP6dgIX0auU8?wF%AVhILb}4}0;8zffHOp!;r6vGZGG zVx35C=*73)pwgfg=}$MPp3#&5Z##s3kFdKo1D8rdav&6d>r3hGCsmi37Eiv+e}%~j z__BrS)%d&x>buEX1LUjh7?}7al3EV8FH|qy2!a8A083zlLTy~w(NK6D7uK)W0T(w2 z{c*T<%+EOh=|cF(LiGSp%eM>FJK;AFcEh*Z&JAQbPIz?}p zSRJx$!t(E+04Oj$YzD^ZLq6~((~ueDtEwXTIPl|dAWJwYT%vAU40GJ8Qv(}u`1N&a zO%q%QM`Pr^FG=nrAxTN*RdOdiu@ln|?L2BW$L zRvI*_uR%0Sgxx7>8P;x6uK_A*-K1`Z;_6LmNM4s7+@$`)Tojcq=G3!D0>Tw$^}P^k zmwF@W^C9*ny)LN#Ldv^i>Jfyc6OhOJ&JJ~n)Rj;#rs0?e)9N$ePVPinU8{!bYa?o2 zDb`@isCqtJiS>@EFNda2N7W{%Z68x_1xX(rQ~w;SS12~%`f>Fs=pV|h&jc@mFupsc_*{YZT+ z8SrCu>*BzWPBvJHB=o~iR}o1FW0vCi=c(u7KmS-=26O%U$7+79Z)gfj4h+dzdvG*x z!=8lTiXI|xa+)mO{S$TBTr){B$=?7>o$$T$)SFgApdguw#z?kJ=c}tBD)G?y>Z+}=Ay#80LHlwWWCCiK2-HpP#8AHKf?sEe_(IiT(#4Yw zd#cF=p#zugS1$)QqGrDu^8fJAe)UH1KaTHLcf#KPymB(1pQ@{Ljvsml117OKG1a^a)ORn*3^AUe-8#P@r*0J_+c19h z0`+CEPbgO7qd!ys0{}kzLiO>1NrYA$~iLm_o;38fO@^oZickX zfIA(Qv)ttwD#K(3{<||+rKb<5RjA1EL%VtNpt=?mZpFpwA~=z)7pv=`7`|BD4?(JY z<9ipYw?pIVLux-|GUF8))fRmJA$2iwN`F42z6UO@q)S9KT=1LlWtXali)=px@tVWx zO^Y4h#||9w&|$THarOsZ{OYjUh%HxL15MzSlK=n! delta 104 zcmaFXCAzO&w4sHug{g(Pg=GtCs>1X_1y%{AKu5r#5S9LpjwVhzzRqE$F5CAju`X8v05p^xJ^%m! diff --git a/netbox/project-static/src/buttons/selectMultiple.ts b/netbox/project-static/src/buttons/selectMultiple.ts index 62e66ed0a..dc33e4fc5 100644 --- a/netbox/project-static/src/buttons/selectMultiple.ts +++ b/netbox/project-static/src/buttons/selectMultiple.ts @@ -9,7 +9,6 @@ function preventTextHighlight(): void { } function updatePreviousPkCheckState(eventTargetElement: HTMLInputElement, state: StateManager): void { - console.log(state) state.set('element', eventTargetElement); } @@ -55,8 +54,14 @@ export function initSelectMultiple(): void { const checkboxElements = getElements('input[type="checkbox"][name="pk"]'); for (const element of checkboxElements) { element.addEventListener('click', (event) => { + //Prevents shift+click from selecting table text + document.addEventListener('selectstart', preventTextHighlight) + //Stop propogation to avoid event firing multiple times event.stopPropagation(); - updatePreviousPkCheckState(event.target as HTMLInputElement, previousPkCheckState); + //Main logic for multi select + handlePkCheck(event, previousPkCheckState); + //Re-enables user's ability to select table text + document.removeEventListener('selectstart', preventTextHighlight) }); } } From ef29bffb723c86b51f1ae00ae65b59786ae5298c Mon Sep 17 00:00:00 2001 From: CroogQT Date: Thu, 5 May 2022 13:27:09 -0700 Subject: [PATCH 015/593] is this supposed to be ignored? --- netbox/project-static/dist/netbox.js.map | Bin 345446 -> 345447 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/netbox/project-static/dist/netbox.js.map b/netbox/project-static/dist/netbox.js.map index 8538f4c2a701fe35487ac56132d7e08be5a42f1f..61469e070456ceeedee6d1878b86d8a63390bc29 100644 GIT binary patch delta 92 zcmaFXCHlNew4sHug{g(Pg=Gutas}ReCmnZ3$4nO;XGdql?VA-?_cJoOPA^tsRZ@5L r14$)=2!9< Date: Thu, 5 May 2022 15:01:40 -0700 Subject: [PATCH 016/593] fixed text deselection and refactor --- netbox/project-static/dist/netbox.js | Bin 376041 -> 376078 bytes netbox/project-static/dist/netbox.js.map | Bin 345447 -> 345520 bytes .../src/buttons/selectMultiple.ts | 52 +++++++++--------- 3 files changed, 27 insertions(+), 25 deletions(-) diff --git a/netbox/project-static/dist/netbox.js b/netbox/project-static/dist/netbox.js index b7095fa78873efc9370ca09754b6e9cf3b8641cb..ce2e0efd2ede7b7f41e1b1837cc1373283e0cee2 100644 GIT binary patch delta 1655 zcmaizdr(wW9LMLoyGLn!G?gw+BC*oQu5&YSv>By!1Vuy^L|9*#@p6}iyL&IY?5>ZM z)D%GxMGwnCAS^SbNRh63H7B3VsF{{&Qa&J`X}-&5vYDFBBG&jGg;(D}fF9oUo(4|O&LP)Kh!t%0n}+xR?CqLja% z1?18zANb&;GdEG>D|a>p0lV_<<_5q)BeyUV)7@KI;G%h3y(pmVTV?oZQgbx&6tP*< zVu8}$GDm|%I%>NQ*|cSQF*0dHYZ>M#n_3fr`Sj+FN3bfF+LD2I8l!B1g`QHrzyxLW z&KMw7*}1C|y^>>vVpnMU#V)>mJ>3!1(LL(h5S8#fZXlhm+?&Ix{<4>Zs6P074yT&7 z?^6wN#|l;#TNjD`wBP<^WKyg!r8{if&oEI*KA_id#5so+Uu?TcQT>q< zoaU7ysYp_Wf0?J{IO)-Sm`tY}i{>;-jx`}mdGWXj#O4;Hg02?Iw6Ds*xn4NYN+h=+ zKlw{Hf8EIeFFzSXq4M&nsUWgj$m^~s`=&}qO58$bcPyN}2a@X+tlja*a}65g({F!p z!l#&jT*~Rw1LqfV_+u_m4*%4JMz|F5;s9{^%P(E!+#MYOaNyTEWn?SXpWXl`UU}Ki ziEFNWq~*k$uh+p!<8Rbp3O#q@7{=3$KmUe!h2ET~<@C?rUd!nh-C4rv)7y6sBU#yg zFO%p*w-8U;?&+0*k5&*}x?2#tqJ57?>yV-R{+BllMQTwG)b)g0)%L~OzDQDO1U>;~ zvayj!LzY?#9+WSc+^${&JdKS%VKa*gIty~6n;3>_Mxx=KGiH>b|2l0 z1RLz{B|1)}v6(sRhwNR_*oZ$1g{DpaJ~R zsDTD;`_!<)h2{*8F~h;+d)of0d{=uIaH%8iYc2fjtBW6MGlqm&6G9rlJ*3f>-w)TR z_rpn+>p6d}C(0|u8NKY`IfM@~naz^V>0GqPD-HA5T(aL5?{sG5N(HtOqlqP#kui$} zNiqeTHgA+iG8g?1xPnazgbLxvb-X;{9)mM_}onz?27r%8}Q z0^2iUQ{aiA$L95Uq|#6@oa>&}JS~soR;9c}mxK3z9ZE|}?LH~rTgcZ}z!sH}HzN#u zAtjDNyEnzQ!1SbB95+l%G@1l)_iF1iYNfJB~?{8w;R*+&tS61SMUb#5kBNyiu zTGO>b;KsIZt zAghqaOe@JC_|>G9WHEXtibA$H$7GX?UWtj7EC&HL-UwDU)^3sqBXFx@79tucD9-fV&-bXFrFEz zNd$XX%{}AQ@7IxKTDBob!kHW-L)oPuw{H!SVO-Y{B$K!#rj{&;fQ2=GMjpbV)+w$Qbl&CibSz%yNCs;YV2;Zr^i2ocWmzf delta 1606 zcmZXTdr(wW9LMLoyGNPPXfj=xKmu`)b#4xrHlxlu0`+a}k-<;1m zck2`F?Z?`V`SnPU7u3HB#M4#VlaVC1Z(ju1=>zJ*o7B>fiAZ|3p&v_Vo!f@#6pbkp z$_j*-Je_`_D<+SIw@#(yjgeSRk2M}d8WmYAQsrLe2dtz6O~(*Jn|5SiDgA3l4_3&X zJJW!-s8>rc5~A7+517@+ zmIvBat58bk9x8%?wjHt|gZj5uAX(nsZUENN;f_Zrk*{<{15tE_yblrdocsl1<@dT~ z0E^|W?jcNw3m0sKp5xmJ`SG>1!>yq`${Iw=K0S88Ot&1#4fjqm+b zMGC@<NY?fGKqH(M`l2E-V`hJC8C%%F%sV6-S(TycA`0@c1Cd+1;OsRH{93hSR)$ zA`UTf*q7OAj+373#R|IYR4}LMJk^RsdD3YeNP4&+<~|)Er+!rd&h^rnb|TpY>u8ql z{d$N4-h9>#hy2R9M38*DkUiE>@lCCUSnWdcSS)-$0^+m_DP!@Ri_I!n=(j&wkR$7V z+Q{kCzQHmMfBq%P;h($I0-J2U>BsZ0Fm))+YX14*$ARRzGB*m$^7T2ay5g0D#g{T z9!T_ZlIk(M zr9g`qXf*{|MV*T)EF$l5jPALN_lN0Rqxw;sa!(Le&ZT4JkPzN9D<{vGV>YhUi_f+? z;i;GSKd(ui;m?_sv(PGHV?)vK3v(JR#vI8hWjUm2g2A8@xT90D+7cXATUKtCly4P7 zbwZ&zR1^ghFEj4~O@R0FUF=8|NqH&Pte+N}pPMHI##z@IrwPR@pqeZ~Hp{3cVJKyF z)nqmd>_jzr7sV`o3z>>s#j=I0hi|D_Fql{CjAE!H>dH+;Vvc8SUVX z4VljRYRC<4xaU36s#<4e;T>cOo3&9hj;*OB;mBZhwIuB2WV7xWYstfU)jH)uEeTe! zh+3j$6YI!4q_8J-#Ghr?5ylL4WGd6vbLS}KNj=%5MhWY4b7g~@%w*+m;=_J$lh@c$ zPtLcIyv}v8jbwvABG}o_$U{UZeKOGkvFu_O>EcUGH+dV2*>E>0#9}4wFzFfhFCOe! AJpcdz diff --git a/netbox/project-static/dist/netbox.js.map b/netbox/project-static/dist/netbox.js.map index 61469e070456ceeedee6d1878b86d8a63390bc29..9814304071db1bcd00884cef28580bc2cee1a997 100644 GIT binary patch delta 312 zcmX9(yH3ME5EPI50E!eKglr4CZ7jqi6`EZ;Ckr{oK`2U1Lxdof!b_&1a78}gK7p1Z zh?XCr<~O*#G^5q*?9A-nd+&46d+XM^^=_lvT$-EcU?wCOHbNDuoW?*!QwA>R5_n{~ zW!RQo6Pfhvn8^ZjQG5>Q8G$?$$`n+1S|xJC-VHUCz*yOdSCuF3D>kWnjm_zvyacP7 zYT!zb@7Ry_DVSRK8ynv4GfhH;)!O4U)3nwtBa`n65Y(kFDkGm)47||*bdKC@#iqY= spQ-aKB&~4HzKv}&7bj-4FNdOHsH)UQWvJt|LPe#dS;OByiTOzW0i(87L;wH) delta 239 zcmdncE&9Amw4sHug{g(Pg=GtCh5|>ivyQ8yXNmLlUIkV|;Zi3ZcSpy3Fq!G1Jy?_A)d|b zf^;}Lx@LeCxKCGAWOZcnH=O<#Xxa2$MOI6uGN }; -function preventTextHighlight(): void { - return +function removeTextSelection(): void{ + window.getSelection()?.removeAllRanges(); } function updatePreviousPkCheckState(eventTargetElement: HTMLInputElement, state: StateManager): void { state.set('element', eventTargetElement); } -function handlePkCheck(event: _MouseEvent, state: StateManager): void { - const eventTargetElement = event.target as HTMLInputElement; - const previousStateElement = state.get('element'); - updatePreviousPkCheckState(eventTargetElement, state); - //Stop if user is not holding shift key - if(event.shiftKey === false){ - return - } - //If no previous state, store event target element as previous state and return - if (previousStateElement === null) { - return updatePreviousPkCheckState(eventTargetElement, state); - } - const checkboxList = getElements('input[type="checkbox"][name="pk"]'); - let changePkCheckboxState = false; - for(const element of checkboxList){ +function toggleCheckboxRange(eventTargetElement: HTMLInputElement, previousStateElement: HTMLInputElement, elementList: Generator): void{ + let changePkCheckboxState = false + for(let element of elementList){ + //Change loop's current checkbox state to eventTargetElement checkbox state + if(changePkCheckboxState === true){ + element.checked = eventTargetElement.checked; + } //The previously clicked checkbox was above the shift clicked checkbox if(element === previousStateElement){ if(changePkCheckboxState === true){ @@ -34,9 +26,6 @@ function handlePkCheck(event: _MouseEvent, state: StateManager): void { + const eventTargetElement = event.target as HTMLInputElement; + const previousStateElement = state.get('element'); + updatePreviousPkCheckState(eventTargetElement, state); + //Stop if user is not holding shift key + if(!event.shiftKey){ + return + } + removeTextSelection(); + //If no previous state, store event target element as previous state and return + if (previousStateElement === null) { + return updatePreviousPkCheckState(eventTargetElement, state); + } + const checkboxList = getElements('input[type="checkbox"][name="pk"]'); + toggleCheckboxRange(eventTargetElement, previousStateElement, checkboxList) +} + export function initSelectMultiple(): void { const checkboxElements = getElements('input[type="checkbox"][name="pk"]'); for (const element of checkboxElements) { element.addEventListener('click', (event) => { - //Prevents shift+click from selecting table text - document.addEventListener('selectstart', preventTextHighlight) + removeTextSelection() //Stop propogation to avoid event firing multiple times event.stopPropagation(); - //Main logic for multi select handlePkCheck(event, previousPkCheckState); - //Re-enables user's ability to select table text - document.removeEventListener('selectstart', preventTextHighlight) }); } } From 90d8395a2c9438b99be7ac0213e08f38cbc2225c Mon Sep 17 00:00:00 2001 From: CroogQT Date: Thu, 5 May 2022 15:24:16 -0700 Subject: [PATCH 017/593] Fixed variable type issue...i think. --- netbox/project-static/src/buttons/selectMultiple.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/netbox/project-static/src/buttons/selectMultiple.ts b/netbox/project-static/src/buttons/selectMultiple.ts index 0ec19672c..8d75fb866 100644 --- a/netbox/project-static/src/buttons/selectMultiple.ts +++ b/netbox/project-static/src/buttons/selectMultiple.ts @@ -14,10 +14,11 @@ function updatePreviousPkCheckState(eventTargetElement: HTMLInputElement, state: function toggleCheckboxRange(eventTargetElement: HTMLInputElement, previousStateElement: HTMLInputElement, elementList: Generator): void{ let changePkCheckboxState = false - for(let element of elementList){ + for(const element of elementList){ + const typedElement = element as HTMLInputElement //Change loop's current checkbox state to eventTargetElement checkbox state if(changePkCheckboxState === true){ - element.checked = eventTargetElement.checked; + typedElement.checked = eventTargetElement.checked; } //The previously clicked checkbox was above the shift clicked checkbox if(element === previousStateElement){ @@ -26,7 +27,7 @@ function toggleCheckboxRange(eventTargetElement: HTMLInputElement, previousState return } changePkCheckboxState = true; - element.checked = eventTargetElement.checked; + typedElement.checked = eventTargetElement.checked; } //The previously clicked checkbox was below the shift clicked checkbox if(element === eventTargetElement){ From 491a4e7d787a75c0560136d597d491a3c2266bf0 Mon Sep 17 00:00:00 2001 From: CroogQT Date: Fri, 6 May 2022 11:33:00 -0700 Subject: [PATCH 018/593] various punctuation and spacing fixes --- .../src/buttons/selectMultiple.ts | 49 +++++++++++-------- netbox/project-static/src/stores/index.ts | 2 +- .../src/stores/previousPkCheck.ts | 5 +- 3 files changed, 31 insertions(+), 25 deletions(-) diff --git a/netbox/project-static/src/buttons/selectMultiple.ts b/netbox/project-static/src/buttons/selectMultiple.ts index 8d75fb866..8a5d2aabb 100644 --- a/netbox/project-static/src/buttons/selectMultiple.ts +++ b/netbox/project-static/src/buttons/selectMultiple.ts @@ -4,36 +4,43 @@ import { previousPkCheckState } from '../stores'; type PreviousPkCheckState = { element: Nullable }; -function removeTextSelection(): void{ +function removeTextSelection(): void { window.getSelection()?.removeAllRanges(); } -function updatePreviousPkCheckState(eventTargetElement: HTMLInputElement, state: StateManager): void { +function updatePreviousPkCheckState( + eventTargetElement: HTMLInputElement, + state: StateManager, +): void { state.set('element', eventTargetElement); } -function toggleCheckboxRange(eventTargetElement: HTMLInputElement, previousStateElement: HTMLInputElement, elementList: Generator): void{ - let changePkCheckboxState = false - for(const element of elementList){ - const typedElement = element as HTMLInputElement +function toggleCheckboxRange( + eventTargetElement: HTMLInputElement, + previousStateElement: HTMLInputElement, + elementList: Generator, +): void { + let changePkCheckboxState = false; + for (const element of elementList) { + const typedElement = element as HTMLInputElement; //Change loop's current checkbox state to eventTargetElement checkbox state - if(changePkCheckboxState === true){ + if (changePkCheckboxState === true) { typedElement.checked = eventTargetElement.checked; } - //The previously clicked checkbox was above the shift clicked checkbox - if(element === previousStateElement){ - if(changePkCheckboxState === true){ + //The previously clicked checkbox was above the shift clicked checkbox + if (element === previousStateElement) { + if (changePkCheckboxState === true) { changePkCheckboxState = false; - return + return; } changePkCheckboxState = true; typedElement.checked = eventTargetElement.checked; } - //The previously clicked checkbox was below the shift clicked checkbox - if(element === eventTargetElement){ - if(changePkCheckboxState === true){ - changePkCheckboxState = false - return + //The previously clicked checkbox was below the shift clicked checkbox + if (element === eventTargetElement) { + if (changePkCheckboxState === true) { + changePkCheckboxState = false; + return; } changePkCheckboxState = true; } @@ -45,8 +52,8 @@ function handlePkCheck(event: MouseEvent, state: StateManager('input[type="checkbox"][name="pk"]'); - toggleCheckboxRange(eventTargetElement, previousStateElement, checkboxList) + toggleCheckboxRange(eventTargetElement, previousStateElement, checkboxList); } export function initSelectMultiple(): void { const checkboxElements = getElements('input[type="checkbox"][name="pk"]'); for (const element of checkboxElements) { - element.addEventListener('click', (event) => { - removeTextSelection() + element.addEventListener('click', event => { + removeTextSelection(); //Stop propogation to avoid event firing multiple times event.stopPropagation(); handlePkCheck(event, previousPkCheckState); diff --git a/netbox/project-static/src/stores/index.ts b/netbox/project-static/src/stores/index.ts index 5e53410ad..d4644e619 100644 --- a/netbox/project-static/src/stores/index.ts +++ b/netbox/project-static/src/stores/index.ts @@ -1,3 +1,3 @@ export * from './objectDepth'; export * from './rackImages'; -export * from './previousPkCheck'; \ No newline at end of file +export * from './previousPkCheck'; diff --git a/netbox/project-static/src/stores/previousPkCheck.ts b/netbox/project-static/src/stores/previousPkCheck.ts index a5d06ceee..19b244ec7 100644 --- a/netbox/project-static/src/stores/previousPkCheck.ts +++ b/netbox/project-static/src/stores/previousPkCheck.ts @@ -1,7 +1,6 @@ import { createState } from '../state'; export const previousPkCheckState = createState<{ element: Nullable }>( - { element: null}, - { persist: false } + { element: null }, + { persist: false }, ); - From 9c5355a300e4a2139d12f3ff9a60e5415c00e38f Mon Sep 17 00:00:00 2001 From: CroogQT Date: Fri, 6 May 2022 11:43:18 -0700 Subject: [PATCH 019/593] added JSDoc comments --- .../src/buttons/selectMultiple.ts | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/netbox/project-static/src/buttons/selectMultiple.ts b/netbox/project-static/src/buttons/selectMultiple.ts index 8a5d2aabb..d05c21716 100644 --- a/netbox/project-static/src/buttons/selectMultiple.ts +++ b/netbox/project-static/src/buttons/selectMultiple.ts @@ -4,10 +4,20 @@ import { previousPkCheckState } from '../stores'; type PreviousPkCheckState = { element: Nullable }; +/** + * If there is a text selection, removes it. + */ function removeTextSelection(): void { window.getSelection()?.removeAllRanges(); } +/** + * Sets the state object passed in to the eventTargetElement object passed in. + * + * @param eventTargetElement HTML Input Element, retrieved from getting the target of the + * event passed in from handlePkCheck() + * @param state PreviousPkCheckState object. + */ function updatePreviousPkCheckState( eventTargetElement: HTMLInputElement, state: StateManager, @@ -15,6 +25,14 @@ function updatePreviousPkCheckState( state.set('element', eventTargetElement); } +/** + * For all checkboxes between eventTargetElement and previousStateElement in elementList, toggle + * "checked" value to eventTargetElement.checked + * + * @param eventTargetElement HTML Input Element, retrieved from getting the target of the + * event passed in from handlePkCheck() + * @param state PreviousPkCheckState object. + */ function toggleCheckboxRange( eventTargetElement: HTMLInputElement, previousStateElement: HTMLInputElement, @@ -47,6 +65,14 @@ function toggleCheckboxRange( } } + +/** + * IF the shift key is pressed and there is state is not null, toggleCheckboxRange between the + * event target element and the state element. + * + * @param event Mouse event. + * @param state PreviousPkCheckState object. + */ function handlePkCheck(event: MouseEvent, state: StateManager): void { const eventTargetElement = event.target as HTMLInputElement; const previousStateElement = state.get('element'); @@ -64,6 +90,9 @@ function handlePkCheck(event: MouseEvent, state: StateManager('input[type="checkbox"][name="pk"]'); for (const element of checkboxElements) { From fbd933b56a82c774853e347627909fe6788f2848 Mon Sep 17 00:00:00 2001 From: CroogQT Date: Fri, 6 May 2022 11:44:34 -0700 Subject: [PATCH 020/593] prettier fixes --- .../project-static/src/buttons/selectMultiple.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/netbox/project-static/src/buttons/selectMultiple.ts b/netbox/project-static/src/buttons/selectMultiple.ts index d05c21716..d8bad3105 100644 --- a/netbox/project-static/src/buttons/selectMultiple.ts +++ b/netbox/project-static/src/buttons/selectMultiple.ts @@ -13,9 +13,9 @@ function removeTextSelection(): void { /** * Sets the state object passed in to the eventTargetElement object passed in. - * + * * @param eventTargetElement HTML Input Element, retrieved from getting the target of the - * event passed in from handlePkCheck() + * event passed in from handlePkCheck() * @param state PreviousPkCheckState object. */ function updatePreviousPkCheckState( @@ -27,10 +27,10 @@ function updatePreviousPkCheckState( /** * For all checkboxes between eventTargetElement and previousStateElement in elementList, toggle - * "checked" value to eventTargetElement.checked - * + * "checked" value to eventTargetElement.checked + * * @param eventTargetElement HTML Input Element, retrieved from getting the target of the - * event passed in from handlePkCheck() + * event passed in from handlePkCheck() * @param state PreviousPkCheckState object. */ function toggleCheckboxRange( @@ -65,11 +65,10 @@ function toggleCheckboxRange( } } - /** - * IF the shift key is pressed and there is state is not null, toggleCheckboxRange between the + * IF the shift key is pressed and there is state is not null, toggleCheckboxRange between the * event target element and the state element. - * + * * @param event Mouse event. * @param state PreviousPkCheckState object. */ From 124e93f73726d99a0383768e797d3ad774a2e6ef Mon Sep 17 00:00:00 2001 From: CroogQT Date: Fri, 6 May 2022 12:16:45 -0700 Subject: [PATCH 021/593] yarn bundle. --- netbox/project-static/dist/netbox.js | Bin 376078 -> 376088 bytes netbox/project-static/dist/netbox.js.map | Bin 345520 -> 345522 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/netbox/project-static/dist/netbox.js b/netbox/project-static/dist/netbox.js index ce2e0efd2ede7b7f41e1b1837cc1373283e0cee2..ce02d4bbb227926941d2adc3ae38e721d48ce21b 100644 GIT binary patch delta 57 zcmeDCB{t)iSVIeA3sVbo3(FSPhpiGhsU-@DdA9j^)|qJX&Oa&wzjs425M>=`O|;4vf44`Z?|Y;eP9Lv DuRjqe diff --git a/netbox/project-static/dist/netbox.js.map b/netbox/project-static/dist/netbox.js.map index 9814304071db1bcd00884cef28580bc2cee1a997..e21571e0c84f1680154cb438612199fef77a0f4e 100644 GIT binary patch delta 377 zcmY+AJxc>Y5QfReBG_3(un}_>HdBa_BB0>AOLCsaCWhoV4ne%aLXD^qja;L}bpgR@ zOKUq3!GGXi@ozYDeuOm3?#%N(Gwkc6_BN?K}r=eNR zA^Vd=D$uDB$_jPNH#-t^FQ5=u*Ibnp_fqNsk)Y?A7xM4cj39|ruC<6R3(;S&aB_9% z!n*RdXW~8hjVW9xE(*nPTIJa@ils`3#C_J)!NR8e6Mon3!wlSprw&WjJ$Y)^J#Q&h T<-wWz^3-y<+5VVYRWIH*)~sdF delta 390 zcmY+Ay-EW?6opB2iZlU>6e37=ZEv9j6$^_q6Pc{bx~{T>MF`p%qp0`?+r(nKJiz!0 zb}E8TVe32i7T!BSLYm=n?)lC=%*UklHfcRqwW?k%REwYLv^~DCrJ*z6+K|D)jiD6j z9`%8gh8j4eW8gv4NW+$z3U9Q)ogsdQU4~5AlR2RXh$sj8JlRKY-qNy~nx8zv3M2QC z=n+Nkr>&ZUjFKsag15=GQ2Ul*U|n;V>Bx(WG9VZ5N^>pCW!X`PaO+bq{e^#y Date: Wed, 11 May 2022 16:13:35 -0400 Subject: [PATCH 022/593] Remove erroneous field from prefetch --- netbox/ipam/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index 79804aabd..078848b3e 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -585,7 +585,7 @@ class IPRangeIPAddressesView(generic.ObjectChildrenView): def get_children(self, request, parent): return parent.get_child_ips().restrict(request.user, 'view').prefetch_related( - 'vrf', 'role', 'tenant', + 'vrf', 'tenant', ) def get_extra_context(self, request, instance): From 01d2ede097b8f908f8a57302d868ae0e50ef4dcb Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 11 May 2022 16:22:07 -0400 Subject: [PATCH 023/593] Closes #1202: Support overlapping assignment of NAT IP addresses --- docs/release-notes/version-3.3.md | 8 ++++++++ netbox/ipam/api/serializers.py | 3 +-- .../0058_ipaddress_nat_inside_nonunique.py | 17 +++++++++++++++++ netbox/ipam/models/ip.py | 2 +- netbox/templates/ipam/ipaddress.html | 10 ++++++++-- 5 files changed, 35 insertions(+), 5 deletions(-) create mode 100644 netbox/ipam/migrations/0058_ipaddress_nat_inside_nonunique.py diff --git a/docs/release-notes/version-3.3.md b/docs/release-notes/version-3.3.md index 9b061b7d6..c46fceea5 100644 --- a/docs/release-notes/version-3.3.md +++ b/docs/release-notes/version-3.3.md @@ -2,8 +2,13 @@ ## v3.3.0 (FUTURE) +### Breaking Changes + +* The `nat_outside` relation on the IP address model now returns a list of zero or more related IP addresses, rather than a single instance (or None). + ### Enhancements +* [#1202](https://github.com/netbox-community/netbox/issues/1202) - Support overlapping assignment of NAT IP addresses * [#8495](https://github.com/netbox-community/netbox/issues/8495) - Enable custom field grouping * [#8995](https://github.com/netbox-community/netbox/issues/8995) - Enable arbitrary ordering of REST API results @@ -15,3 +20,6 @@ * extras.CustomField * Added `group_name` field +* ipam.IPAddress + * The `nat_inside` field no longer requires a unique value + * The `nat_outside` field has changed from a single IP address instance to a list of multiple IP addresses diff --git a/netbox/ipam/api/serializers.py b/netbox/ipam/api/serializers.py index 3fa1bcc7e..ea5c37f91 100644 --- a/netbox/ipam/api/serializers.py +++ b/netbox/ipam/api/serializers.py @@ -360,7 +360,7 @@ class IPAddressSerializer(NetBoxModelSerializer): ) assigned_object = serializers.SerializerMethodField(read_only=True) nat_inside = NestedIPAddressSerializer(required=False, allow_null=True) - nat_outside = NestedIPAddressSerializer(required=False, read_only=True) + nat_outside = NestedIPAddressSerializer(many=True, read_only=True) class Meta: model = IPAddress @@ -369,7 +369,6 @@ class IPAddressSerializer(NetBoxModelSerializer): 'assigned_object_id', 'assigned_object', 'nat_inside', 'nat_outside', 'dns_name', 'description', 'tags', 'custom_fields', 'created', 'last_updated', ] - read_only_fields = ['family', 'nat_outside'] @swagger_serializer_method(serializer_or_field=serializers.DictField) def get_assigned_object(self, obj): diff --git a/netbox/ipam/migrations/0058_ipaddress_nat_inside_nonunique.py b/netbox/ipam/migrations/0058_ipaddress_nat_inside_nonunique.py new file mode 100644 index 000000000..63e93d137 --- /dev/null +++ b/netbox/ipam/migrations/0058_ipaddress_nat_inside_nonunique.py @@ -0,0 +1,17 @@ +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('ipam', '0057_created_datetimefield'), + ] + + operations = [ + migrations.AlterField( + model_name='ipaddress', + name='nat_inside', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='nat_outside', to='ipam.ipaddress'), + ), + ] diff --git a/netbox/ipam/models/ip.py b/netbox/ipam/models/ip.py index a3b8fb2c1..db662f49c 100644 --- a/netbox/ipam/models/ip.py +++ b/netbox/ipam/models/ip.py @@ -813,7 +813,7 @@ class IPAddress(NetBoxModel): ct_field='assigned_object_type', fk_field='assigned_object_id' ) - nat_inside = models.OneToOneField( + nat_inside = models.ForeignKey( to='self', on_delete=models.SET_NULL, related_name='nat_outside', diff --git a/netbox/templates/ipam/ipaddress.html b/netbox/templates/ipam/ipaddress.html index 7867e829b..96a76cf8c 100644 --- a/netbox/templates/ipam/ipaddress.html +++ b/netbox/templates/ipam/ipaddress.html @@ -91,8 +91,14 @@ - NAT (outside) - {{ object.nat_outside|linkify|placeholder }} + Outside NAT IPs + + {% for ip in object.nat_outside.all %} + {{ ip|linkify }}
+ {% empty %} + {{ ''|placeholder }} + {% endfor %} +
From c48c8cc353efc21a20a7c67fa435c19619ef6359 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 25 Apr 2022 14:05:44 -0400 Subject: [PATCH 024/593] Remove termination IDs from cable creation view paths --- netbox/circuits/urls.py | 2 +- netbox/dcim/forms/connections.py | 3 +- netbox/dcim/tables/template_code.py | 50 +++++++++---------- netbox/dcim/urls.py | 16 +++--- netbox/dcim/views.py | 26 +++++----- .../circuits/inc/circuit_termination.html | 8 +-- netbox/templates/dcim/consoleport.html | 6 +-- netbox/templates/dcim/consoleserverport.html | 6 +-- netbox/templates/dcim/frontport.html | 12 ++--- netbox/templates/dcim/interface.html | 8 +-- netbox/templates/dcim/powerfeed.html | 2 +- netbox/templates/dcim/poweroutlet.html | 2 +- netbox/templates/dcim/powerport.html | 4 +- netbox/templates/dcim/rearport.html | 8 +-- 14 files changed, 76 insertions(+), 77 deletions(-) diff --git a/netbox/circuits/urls.py b/netbox/circuits/urls.py index f3ee64cf0..894be27f3 100644 --- a/netbox/circuits/urls.py +++ b/netbox/circuits/urls.py @@ -60,7 +60,7 @@ urlpatterns = [ path('circuit-terminations/add/', views.CircuitTerminationEditView.as_view(), name='circuittermination_add'), path('circuit-terminations//edit/', views.CircuitTerminationEditView.as_view(), name='circuittermination_edit'), path('circuit-terminations//delete/', views.CircuitTerminationDeleteView.as_view(), name='circuittermination_delete'), - path('circuit-terminations//connect//', CableCreateView.as_view(), name='circuittermination_connect', kwargs={'termination_a_type': CircuitTermination}), path('circuit-terminations//trace/', PathTraceView.as_view(), name='circuittermination_trace', kwargs={'model': CircuitTermination}), + path('circuit-terminations/connect/', CableCreateView.as_view(), name='circuittermination_connect', kwargs={'termination_a_type': CircuitTermination}), ] diff --git a/netbox/dcim/forms/connections.py b/netbox/dcim/forms/connections.py index 1ba7adf84..13ab0ae09 100644 --- a/netbox/dcim/forms/connections.py +++ b/netbox/dcim/forms/connections.py @@ -1,9 +1,8 @@ from circuits.models import Circuit, CircuitTermination, Provider from dcim.models import * -from extras.models import Tag from netbox.forms import NetBoxModelForm from tenancy.forms import TenancyForm -from utilities.forms import DynamicModelChoiceField, DynamicModelMultipleChoiceField, StaticSelect +from utilities.forms import DynamicModelChoiceField, StaticSelect __all__ = ( 'ConnectCableToCircuitTerminationForm', diff --git a/netbox/dcim/tables/template_code.py b/netbox/dcim/tables/template_code.py index 92739c6ed..0c1e0ed9e 100644 --- a/netbox/dcim/tables/template_code.py +++ b/netbox/dcim/tables/template_code.py @@ -133,9 +133,9 @@ CONSOLEPORT_BUTTONS = """ {% else %} @@ -165,9 +165,9 @@ CONSOLESERVERPORT_BUTTONS = """ {% else %} @@ -197,8 +197,8 @@ POWERPORT_BUTTONS = """ {% else %} @@ -224,7 +224,7 @@ POWEROUTLET_BUTTONS = """ {% if not record.mark_connected %} - + {% else %} @@ -274,10 +274,10 @@ INTERFACE_BUTTONS = """ {% else %} @@ -313,12 +313,12 @@ FRONTPORT_BUTTONS = """ {% else %} @@ -350,12 +350,12 @@ REARPORT_BUTTONS = """ {% else %} diff --git a/netbox/dcim/urls.py b/netbox/dcim/urls.py index c5cd0fa65..0c04c734e 100644 --- a/netbox/dcim/urls.py +++ b/netbox/dcim/urls.py @@ -294,7 +294,7 @@ urlpatterns = [ path('console-ports//delete/', views.ConsolePortDeleteView.as_view(), name='consoleport_delete'), path('console-ports//changelog/', ObjectChangeLogView.as_view(), name='consoleport_changelog', kwargs={'model': ConsolePort}), path('console-ports//trace/', views.PathTraceView.as_view(), name='consoleport_trace', kwargs={'model': ConsolePort}), - path('console-ports//connect//', views.CableCreateView.as_view(), name='consoleport_connect', kwargs={'termination_a_type': ConsolePort}), + path('console-ports/connect/', views.CableCreateView.as_view(), name='consoleport_connect', kwargs={'termination_a_type': ConsolePort}), path('devices/console-ports/add/', views.DeviceBulkAddConsolePortView.as_view(), name='device_bulk_add_consoleport'), # Console server ports @@ -310,7 +310,7 @@ urlpatterns = [ path('console-server-ports//delete/', views.ConsoleServerPortDeleteView.as_view(), name='consoleserverport_delete'), path('console-server-ports//changelog/', ObjectChangeLogView.as_view(), name='consoleserverport_changelog', kwargs={'model': ConsoleServerPort}), path('console-server-ports//trace/', views.PathTraceView.as_view(), name='consoleserverport_trace', kwargs={'model': ConsoleServerPort}), - path('console-server-ports//connect//', views.CableCreateView.as_view(), name='consoleserverport_connect', kwargs={'termination_a_type': ConsoleServerPort}), + path('console-server-ports/connect/', views.CableCreateView.as_view(), name='consoleserverport_connect', kwargs={'termination_a_type': ConsoleServerPort}), path('devices/console-server-ports/add/', views.DeviceBulkAddConsoleServerPortView.as_view(), name='device_bulk_add_consoleserverport'), # Power ports @@ -326,7 +326,7 @@ urlpatterns = [ path('power-ports//delete/', views.PowerPortDeleteView.as_view(), name='powerport_delete'), path('power-ports//changelog/', ObjectChangeLogView.as_view(), name='powerport_changelog', kwargs={'model': PowerPort}), path('power-ports//trace/', views.PathTraceView.as_view(), name='powerport_trace', kwargs={'model': PowerPort}), - path('power-ports//connect//', views.CableCreateView.as_view(), name='powerport_connect', kwargs={'termination_a_type': PowerPort}), + path('power-ports/connect/', views.CableCreateView.as_view(), name='powerport_connect', kwargs={'termination_a_type': PowerPort}), path('devices/power-ports/add/', views.DeviceBulkAddPowerPortView.as_view(), name='device_bulk_add_powerport'), # Power outlets @@ -342,7 +342,7 @@ urlpatterns = [ path('power-outlets//delete/', views.PowerOutletDeleteView.as_view(), name='poweroutlet_delete'), path('power-outlets//changelog/', ObjectChangeLogView.as_view(), name='poweroutlet_changelog', kwargs={'model': PowerOutlet}), path('power-outlets//trace/', views.PathTraceView.as_view(), name='poweroutlet_trace', kwargs={'model': PowerOutlet}), - path('power-outlets//connect//', views.CableCreateView.as_view(), name='poweroutlet_connect', kwargs={'termination_a_type': PowerOutlet}), + path('power-outlets/connect/', views.CableCreateView.as_view(), name='poweroutlet_connect', kwargs={'termination_a_type': PowerOutlet}), path('devices/power-outlets/add/', views.DeviceBulkAddPowerOutletView.as_view(), name='device_bulk_add_poweroutlet'), # Interfaces @@ -358,7 +358,7 @@ urlpatterns = [ path('interfaces//delete/', views.InterfaceDeleteView.as_view(), name='interface_delete'), path('interfaces//changelog/', ObjectChangeLogView.as_view(), name='interface_changelog', kwargs={'model': Interface}), path('interfaces//trace/', views.PathTraceView.as_view(), name='interface_trace', kwargs={'model': Interface}), - path('interfaces//connect//', views.CableCreateView.as_view(), name='interface_connect', kwargs={'termination_a_type': Interface}), + path('interfaces/connect/', views.CableCreateView.as_view(), name='interface_connect', kwargs={'termination_a_type': Interface}), path('devices/interfaces/add/', views.DeviceBulkAddInterfaceView.as_view(), name='device_bulk_add_interface'), # Front ports @@ -374,7 +374,7 @@ urlpatterns = [ path('front-ports//delete/', views.FrontPortDeleteView.as_view(), name='frontport_delete'), path('front-ports//changelog/', ObjectChangeLogView.as_view(), name='frontport_changelog', kwargs={'model': FrontPort}), path('front-ports//trace/', views.PathTraceView.as_view(), name='frontport_trace', kwargs={'model': FrontPort}), - path('front-ports//connect//', views.CableCreateView.as_view(), name='frontport_connect', kwargs={'termination_a_type': FrontPort}), + path('front-ports/connect/', views.CableCreateView.as_view(), name='frontport_connect', kwargs={'termination_a_type': FrontPort}), # path('devices/front-ports/add/', views.DeviceBulkAddFrontPortView.as_view(), name='device_bulk_add_frontport'), # Rear ports @@ -390,7 +390,7 @@ urlpatterns = [ path('rear-ports//delete/', views.RearPortDeleteView.as_view(), name='rearport_delete'), path('rear-ports//changelog/', ObjectChangeLogView.as_view(), name='rearport_changelog', kwargs={'model': RearPort}), path('rear-ports//trace/', views.PathTraceView.as_view(), name='rearport_trace', kwargs={'model': RearPort}), - path('rear-ports//connect//', views.CableCreateView.as_view(), name='rearport_connect', kwargs={'termination_a_type': RearPort}), + path('rear-ports/connect/', views.CableCreateView.as_view(), name='rearport_connect', kwargs={'termination_a_type': RearPort}), path('devices/rear-ports/add/', views.DeviceBulkAddRearPortView.as_view(), name='device_bulk_add_rearport'), # Module bays @@ -500,6 +500,6 @@ urlpatterns = [ path('power-feeds//trace/', views.PathTraceView.as_view(), name='powerfeed_trace', kwargs={'model': PowerFeed}), path('power-feeds//changelog/', ObjectChangeLogView.as_view(), name='powerfeed_changelog', kwargs={'model': PowerFeed}), path('power-feeds//journal/', ObjectJournalView.as_view(), name='powerfeed_journal', kwargs={'model': PowerFeed}), - path('power-feeds//connect//', views.CableCreateView.as_view(), name='powerfeed_connect', kwargs={'termination_a_type': PowerFeed}), + path('power-feeds/connect/', views.CableCreateView.as_view(), name='powerfeed_connect', kwargs={'termination_a_type': PowerFeed}), ] diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 57e8b1c79..b18b6d4b3 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -2812,16 +2812,16 @@ class CableCreateView(generic.ObjectEditView): # Set the form class based on the type of component being connected self.form = { - 'console-port': forms.ConnectCableToConsolePortForm, - 'console-server-port': forms.ConnectCableToConsoleServerPortForm, - 'power-port': forms.ConnectCableToPowerPortForm, - 'power-outlet': forms.ConnectCableToPowerOutletForm, - 'interface': forms.ConnectCableToInterfaceForm, - 'front-port': forms.ConnectCableToFrontPortForm, - 'rear-port': forms.ConnectCableToRearPortForm, - 'power-feed': forms.ConnectCableToPowerFeedForm, - 'circuit-termination': forms.ConnectCableToCircuitTerminationForm, - }[kwargs.get('termination_b_type')] + 'dcim.consoleport': forms.ConnectCableToConsolePortForm, + 'dcim.consoleserverport': forms.ConnectCableToConsoleServerPortForm, + 'dcim.powerport': forms.ConnectCableToPowerPortForm, + 'dcim.poweroutlet': forms.ConnectCableToPowerOutletForm, + 'dcim.interface': forms.ConnectCableToInterfaceForm, + 'dcim.frontport': forms.ConnectCableToFrontPortForm, + 'dcim.rearport': forms.ConnectCableToRearPortForm, + 'dcim.powerfeed': forms.ConnectCableToPowerFeedForm, + 'circuits.circuittermination': forms.ConnectCableToCircuitTerminationForm, + }[request.GET.get('termination_b_type')] return super().dispatch(request, *args, **kwargs) @@ -2831,9 +2831,9 @@ class CableCreateView(generic.ObjectEditView): def alter_object(self, obj, request, url_args, url_kwargs): termination_a_type = url_kwargs.get('termination_a_type') - termination_a_id = url_kwargs.get('termination_a_id') - termination_b_type_name = url_kwargs.get('termination_b_type') - self.termination_b_type = ContentType.objects.get(model=termination_b_type_name.replace('-', '')) + termination_a_id = request.GET.get('termination_a_id') + app_label, model = request.GET.get('termination_b_type').split('.') + self.termination_b_type = ContentType.objects.get(app_label=app_label, model=model) # Initialize Cable termination attributes obj.termination_a = termination_a_type.objects.get(pk=termination_a_id) diff --git a/netbox/templates/circuits/inc/circuit_termination.html b/netbox/templates/circuits/inc/circuit_termination.html index fdb01e803..12fb85d57 100644 --- a/netbox/templates/circuits/inc/circuit_termination.html +++ b/netbox/templates/circuits/inc/circuit_termination.html @@ -70,10 +70,10 @@ Connect
{% endif %} diff --git a/netbox/templates/dcim/consoleport.html b/netbox/templates/dcim/consoleport.html index ce2c1655d..f2ebc16d9 100644 --- a/netbox/templates/dcim/consoleport.html +++ b/netbox/templates/dcim/consoleport.html @@ -113,7 +113,7 @@
  • Console Server Port @@ -121,7 +121,7 @@
  • Front Port @@ -129,7 +129,7 @@
  • Rear Port diff --git a/netbox/templates/dcim/consoleserverport.html b/netbox/templates/dcim/consoleserverport.html index 52b1a3229..47b784ede 100644 --- a/netbox/templates/dcim/consoleserverport.html +++ b/netbox/templates/dcim/consoleserverport.html @@ -115,7 +115,7 @@
  • Console Port @@ -123,7 +123,7 @@
  • Front Port @@ -131,7 +131,7 @@
  • Rear Port diff --git a/netbox/templates/dcim/frontport.html b/netbox/templates/dcim/frontport.html index 891f217ee..75402e2e7 100644 --- a/netbox/templates/dcim/frontport.html +++ b/netbox/templates/dcim/frontport.html @@ -105,22 +105,22 @@
  • diff --git a/netbox/templates/dcim/interface.html b/netbox/templates/dcim/interface.html index 358922730..7181d2554 100644 --- a/netbox/templates/dcim/interface.html +++ b/netbox/templates/dcim/interface.html @@ -251,22 +251,22 @@ diff --git a/netbox/utilities/templatetags/navigation.py b/netbox/utilities/templatetags/navigation.py index ede8792fa..ef0657446 100644 --- a/netbox/utilities/templatetags/navigation.py +++ b/netbox/utilities/templatetags/navigation.py @@ -13,7 +13,26 @@ def nav(context: Context) -> Dict: """ Render the navigation menu. """ + user = context['request'].user + nav_items = [] + + # Construct the navigation menu based upon the current user's permissions + for menu in MENUS: + groups = [] + for group in menu.groups: + items = [] + for item in group.items: + if user.has_perms(item.permissions): + buttons = [ + button for button in item.buttons if user.has_perms(button.permissions) + ] + items.append((item, buttons)) + if items: + groups.append((group, items)) + if groups: + nav_items.append((menu, groups)) + return { - "nav_items": MENUS, + "nav_items": nav_items, "request": context["request"] } From 3be9f6c4f3a74ab74f35f6cdc4de77593583c409 Mon Sep 17 00:00:00 2001 From: Daniel Sheppard Date: Wed, 29 Jun 2022 16:01:20 -0500 Subject: [PATCH 186/593] #8157 - Final work on L2VPN model --- netbox/dcim/api/serializers.py | 4 +- netbox/dcim/models/device_components.py | 7 +- netbox/ipam/api/nested_serializers.py | 8 +- netbox/ipam/api/serializers.py | 5 +- netbox/ipam/api/views.py | 2 +- netbox/ipam/filtersets.py | 51 ++++++++- netbox/ipam/forms/bulk_edit.py | 5 + netbox/ipam/graphql/schema.py | 6 ++ netbox/ipam/graphql/types.py | 16 +++ netbox/ipam/models/vlans.py | 7 +- netbox/ipam/tests/test_api.py | 38 +++---- netbox/ipam/tests/test_filtersets.py | 62 +++++------ netbox/ipam/tests/test_models.py | 81 ++++++++++++-- netbox/ipam/tests/test_views.py | 138 ++++++++++++++++++++++-- netbox/ipam/urls.py | 1 + netbox/ipam/views.py | 21 ++-- netbox/templates/dcim/interface.html | 4 + netbox/templates/ipam/vlan.html | 4 + 18 files changed, 376 insertions(+), 84 deletions(-) diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 8ac2aa738..32709000b 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -10,6 +10,7 @@ from dcim.constants import * from dcim.models import * from ipam.api.nested_serializers import ( NestedASNSerializer, NestedIPAddressSerializer, NestedVLANSerializer, NestedVRFSerializer, + NestedL2VPNTerminationSerializer, ) from ipam.models import ASN, VLAN from netbox.api import ChoiceField, ContentTypeField, SerializedPKRelatedField @@ -823,6 +824,7 @@ class InterfaceSerializer(NetBoxModelSerializer, LinkTerminationSerializer, Conn many=True ) vrf = NestedVRFSerializer(required=False, allow_null=True) + l2vpn_termination = NestedL2VPNTerminationSerializer(read_only=True) cable = NestedCableSerializer(read_only=True) wireless_link = NestedWirelessLinkSerializer(read_only=True) wireless_lans = SerializedPKRelatedField( @@ -841,7 +843,7 @@ class InterfaceSerializer(NetBoxModelSerializer, LinkTerminationSerializer, Conn 'mtu', 'mac_address', 'speed', 'duplex', 'wwn', 'mgmt_only', 'description', 'mode', 'rf_role', 'rf_channel', 'poe_mode', 'poe_type', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'untagged_vlan', 'tagged_vlans', 'mark_connected', 'cable', 'wireless_link', 'link_peer', 'link_peer_type', 'wireless_lans', - 'vrf', 'connected_endpoint', 'connected_endpoint_type', 'connected_endpoint_reachable', 'tags', + 'vrf', 'l2vpn_termination', 'connected_endpoint', 'connected_endpoint_type', 'connected_endpoint_reachable', 'tags', 'custom_fields', 'created', 'last_updated', 'count_ipaddresses', 'count_fhrp_groups', '_occupied', ] diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index 4d19a2d8d..70c21c165 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -649,10 +649,11 @@ class Interface(ModularComponentModel, BaseInterface, LinkTermination, PathEndpo object_id_field='interface_id', related_query_name='+' ) - l2vpn = GenericRelation( + l2vpn_terminations = GenericRelation( to='ipam.L2VPNTermination', content_type_field='assigned_object_type', object_id_field='assigned_object_id', + related_query_name='interface', ) clone_fields = ['device', 'parent', 'bridge', 'lag', 'type', 'mgmt_only', 'poe_mode', 'poe_type'] @@ -828,6 +829,10 @@ class Interface(ModularComponentModel, BaseInterface, LinkTermination, PathEndpo def link(self): return self.cable or self.wireless_link + @property + def l2vpn_termination(self): + return self.l2vpn_terminations.first() + # # Pass-through ports diff --git a/netbox/ipam/api/nested_serializers.py b/netbox/ipam/api/nested_serializers.py index 8316cb992..39305a017 100644 --- a/netbox/ipam/api/nested_serializers.py +++ b/netbox/ipam/api/nested_serializers.py @@ -11,6 +11,8 @@ __all__ = [ 'NestedFHRPGroupAssignmentSerializer', 'NestedIPAddressSerializer', 'NestedIPRangeSerializer', + 'NestedL2VPNSerializer', + 'NestedL2VPNTerminationSerializer', 'NestedPrefixSerializer', 'NestedRIRSerializer', 'NestedRoleSerializer', @@ -203,17 +205,17 @@ class NestedL2VPNSerializer(WritableNestedSerializer): class Meta: model = L2VPN fields = [ - 'id', 'url', 'display', 'name', 'type' + 'id', 'url', 'display', 'identifier', 'name', 'slug', 'type' ] class NestedL2VPNTerminationSerializer(WritableNestedSerializer): - url = serializers.HyperlinkedIdentityField(view_name='ipam-api:l2vpn_termination-detail') + url = serializers.HyperlinkedIdentityField(view_name='ipam-api:l2vpntermination-detail') l2vpn = NestedL2VPNSerializer() class Meta: model = L2VPNTermination fields = [ - 'id', 'url', 'display', 'l2vpn', 'assigned_object' + 'id', 'url', 'display', 'l2vpn' ] diff --git a/netbox/ipam/api/serializers.py b/netbox/ipam/api/serializers.py index a51043e27..36102f853 100644 --- a/netbox/ipam/api/serializers.py +++ b/netbox/ipam/api/serializers.py @@ -207,13 +207,14 @@ class VLANSerializer(NetBoxModelSerializer): tenant = NestedTenantSerializer(required=False, allow_null=True) status = ChoiceField(choices=VLANStatusChoices, required=False) role = NestedRoleSerializer(required=False, allow_null=True) + l2vpn_termination = NestedL2VPNTerminationSerializer(read_only=True) prefix_count = serializers.IntegerField(read_only=True) class Meta: model = VLAN fields = [ - 'id', 'url', 'display', 'site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description', 'tags', - 'custom_fields', 'created', 'last_updated', 'prefix_count', + 'id', 'url', 'display', 'site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description', + 'l2vpn_termination', 'tags', 'custom_fields', 'created', 'last_updated', 'prefix_count', ] diff --git a/netbox/ipam/api/views.py b/netbox/ipam/api/views.py index 36a6f02b6..f5a61c031 100644 --- a/netbox/ipam/api/views.py +++ b/netbox/ipam/api/views.py @@ -165,7 +165,7 @@ class L2VPNViewSet(NetBoxModelViewSet): class L2VPNTerminationViewSet(NetBoxModelViewSet): - queryset = L2VPNTermination.objects + queryset = L2VPNTermination.objects.prefetch_related('assigned_object') serializer_class = serializers.L2VPNTerminationSerializer filterset_class = filtersets.L2VPNTerminationFilterSet diff --git a/netbox/ipam/filtersets.py b/netbox/ipam/filtersets.py index 03189a7cb..f682009ee 100644 --- a/netbox/ipam/filtersets.py +++ b/netbox/ipam/filtersets.py @@ -957,7 +957,7 @@ class L2VPNFilterSet(NetBoxModelFilterSet, TenancyFilterSet): class Meta: model = L2VPN - fields = ['identifier', 'name', 'type', 'description'] + fields = ['id', 'identifier', 'name', 'type', 'description'] def search(self, queryset, name, value): if not value.strip(): @@ -977,13 +977,60 @@ class L2VPNTerminationFilterSet(NetBoxModelFilterSet): to_field_name='name', label='L2VPN (name)', ) + device = MultiValueCharFilter( + method='filter_device', + field_name='name', + label='Device (name)', + ) + device_id = MultiValueNumberFilter( + method='filter_device', + field_name='pk', + label='Device (ID)', + ) + interface = django_filters.ModelMultipleChoiceFilter( + field_name='interface__name', + queryset=Interface.objects.all(), + to_field_name='name', + label='Interface (name)', + ) + interface_id = django_filters.ModelMultipleChoiceFilter( + field_name='interface', + queryset=Interface.objects.all(), + label='Interface (ID)', + ) + vlan = django_filters.ModelMultipleChoiceFilter( + field_name='vlan__name', + queryset=VLAN.objects.all(), + to_field_name='name', + label='VLAN (name)', + ) + vlan_vid = django_filters.NumberFilter( + field_name='vlan__vid', + label='VLAN number (1-4094)', + ) + vlan_id = django_filters.ModelMultipleChoiceFilter( + field_name='vlan', + queryset=VLAN.objects.all(), + label='VLAN (ID)', + ) class Meta: model = L2VPNTermination - fields = ['l2vpn'] + fields = ['id', ] def search(self, queryset, name, value): if not value.strip(): return queryset qs_filter = Q(l2vpn__name__icontains=value) return queryset.filter(qs_filter) + + def filter_device(self, queryset, name, value): + devices = Device.objects.filter(**{'{}__in'.format(name): value}) + if not devices.exists(): + return queryset.none() + interface_ids = [] + for device in devices: + interface_ids.extend(device.vc_interfaces().values_list('id', flat=True)) + return queryset.filter( + interface__in=interface_ids + ) diff --git a/netbox/ipam/forms/bulk_edit.py b/netbox/ipam/forms/bulk_edit.py index bbfa5bf9f..50fc51522 100644 --- a/netbox/ipam/forms/bulk_edit.py +++ b/netbox/ipam/forms/bulk_edit.py @@ -19,6 +19,7 @@ __all__ = ( 'IPAddressBulkEditForm', 'IPRangeBulkEditForm', 'L2VPNBulkEditForm', + 'L2VPNTerminationBulkEditForm', 'PrefixBulkEditForm', 'RIRBulkEditForm', 'RoleBulkEditForm', @@ -458,3 +459,7 @@ class L2VPNBulkEditForm(NetBoxModelBulkEditForm): (None, ('tenant', 'description')), ) nullable_fields = ('tenant', 'description',) + + +class L2VPNTerminationBulkEditForm(NetBoxModelBulkEditForm): + model = L2VPN diff --git a/netbox/ipam/graphql/schema.py b/netbox/ipam/graphql/schema.py index f466c1857..5cd5e030e 100644 --- a/netbox/ipam/graphql/schema.py +++ b/netbox/ipam/graphql/schema.py @@ -17,6 +17,12 @@ class IPAMQuery(graphene.ObjectType): ip_range = ObjectField(IPRangeType) ip_range_list = ObjectListField(IPRangeType) + l2vpn = ObjectField(L2VPNType) + l2vpn_list = ObjectListField(L2VPNType) + + l2vpn_termination = ObjectField(L2VPNTerminationType) + l2vpn_termination_list = ObjectListField(L2VPNTerminationType) + prefix = ObjectField(PrefixType) prefix_list = ObjectListField(PrefixType) diff --git a/netbox/ipam/graphql/types.py b/netbox/ipam/graphql/types.py index ca206b4b8..5af2ca72a 100644 --- a/netbox/ipam/graphql/types.py +++ b/netbox/ipam/graphql/types.py @@ -11,6 +11,8 @@ __all__ = ( 'FHRPGroupAssignmentType', 'IPAddressType', 'IPRangeType', + 'L2VPNType', + 'L2VPNTerminationType', 'PrefixType', 'RIRType', 'RoleType', @@ -151,3 +153,17 @@ class VRFType(NetBoxObjectType): model = models.VRF fields = '__all__' filterset_class = filtersets.VRFFilterSet + + +class L2VPNType(NetBoxObjectType): + class Meta: + model = models.L2VPN + fields = '__all__' + filtersets_class = filtersets.L2VPNFilterSet + + +class L2VPNTerminationType(NetBoxObjectType): + class Meta: + model = models.L2VPNTermination + fields = '__all__' + filtersets_class = filtersets.L2VPNTerminationFilterSet diff --git a/netbox/ipam/models/vlans.py b/netbox/ipam/models/vlans.py index 3a7969405..f0e062721 100644 --- a/netbox/ipam/models/vlans.py +++ b/netbox/ipam/models/vlans.py @@ -174,10 +174,11 @@ class VLAN(NetBoxModel): blank=True ) - l2vpn = GenericRelation( + l2vpn_terminations = GenericRelation( to='ipam.L2VPNTermination', content_type_field='assigned_object_type', object_id_field='assigned_object_id', + related_query_name='vlan' ) objects = VLANQuerySet.as_manager() @@ -234,3 +235,7 @@ class VLAN(NetBoxModel): Q(untagged_vlan_id=self.pk) | Q(tagged_vlans=self.pk) ).distinct() + + @property + def l2vpn_termination(self): + return self.l2vpn_terminations.first() diff --git a/netbox/ipam/tests/test_api.py b/netbox/ipam/tests/test_api.py index 0e93bd43e..a5ebef2c7 100644 --- a/netbox/ipam/tests/test_api.py +++ b/netbox/ipam/tests/test_api.py @@ -947,28 +947,28 @@ class L2VPNTest(APIViewTestCases.APIViewTestCase): def setUpTestData(cls): l2vpns = ( - L2VPN(name='L2VPN 1', type='vxlan', identifier=650001), - L2VPN(name='L2VPN 2', type='vpws', identifier=650002), - L2VPN(name='L2VPN 3', type='vpls'), # No RD + L2VPN(name='L2VPN 1', slug='l2vpn-1', type='vxlan', identifier=650001), + L2VPN(name='L2VPN 2', slug='l2vpn-2', type='vpws', identifier=650002), + L2VPN(name='L2VPN 3', slug='l2vpn-3', type='vpls'), # No RD ) L2VPN.objects.bulk_create(l2vpns) class L2VPNTerminationTest(APIViewTestCases.APIViewTestCase): model = L2VPNTermination - brief_fields = ['display', 'id', 'l2vpn', 'assigned_object', 'assigned_object_id', 'assigned_object_type', 'url'] + brief_fields = ['display', 'id', 'l2vpn', 'url'] @classmethod def setUpTestData(cls): vlans = ( - VLAN(name='VLAN 1', vid=650001), - VLAN(name='VLAN 2', vid=650002), - VLAN(name='VLAN 3', vid=650003), - VLAN(name='VLAN 4', vid=650004), - VLAN(name='VLAN 5', vid=650005), - VLAN(name='VLAN 6', vid=650006), - VLAN(name='VLAN 7', vid=650007) + VLAN(name='VLAN 1', vid=651), + VLAN(name='VLAN 2', vid=652), + VLAN(name='VLAN 3', vid=653), + VLAN(name='VLAN 4', vid=654), + VLAN(name='VLAN 5', vid=655), + VLAN(name='VLAN 6', vid=656), + VLAN(name='VLAN 7', vid=657) ) VLAN.objects.bulk_create(vlans) @@ -986,24 +986,26 @@ class L2VPNTerminationTest(APIViewTestCases.APIViewTestCase): L2VPNTermination(l2vpn=l2vpns[0], assigned_object=vlans[2]) ) + L2VPNTermination.objects.bulk_create(l2vpnterminations) + cls.create_data = [ { - 'l2vpn': l2vpns[0], + 'l2vpn': l2vpns[0].pk, 'assigned_object_type': 'ipam.vlan', - 'assigned_object_id': vlans[3], + 'assigned_object_id': vlans[3].pk, }, { - 'l2vpn': l2vpns[0], + 'l2vpn': l2vpns[0].pk, 'assigned_object_type': 'ipam.vlan', - 'assigned_object_id': vlans[4], + 'assigned_object_id': vlans[4].pk, }, { - 'l2vpn': l2vpns[0], + 'l2vpn': l2vpns[0].pk, 'assigned_object_type': 'ipam.vlan', - 'assigned_object_id': vlans[5], + 'assigned_object_id': vlans[5].pk, }, ] cls.bulk_update_data = { - 'l2vpn': l2vpns[2] + 'l2vpn': l2vpns[2].pk } diff --git a/netbox/ipam/tests/test_filtersets.py b/netbox/ipam/tests/test_filtersets.py index c5cffc7dc..2b5fb0759 100644 --- a/netbox/ipam/tests/test_filtersets.py +++ b/netbox/ipam/tests/test_filtersets.py @@ -1465,8 +1465,7 @@ class ServiceTestCase(TestCase, ChangeLoggedFilterSetTests): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) -class L2VPNTest(TestCase, ChangeLoggedFilterSetTests): - # TODO: L2VPN Tests +class L2VPNTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = L2VPN.objects.all() filterset = L2VPNFilterSet @@ -1480,20 +1479,8 @@ class L2VPNTest(TestCase, ChangeLoggedFilterSetTests): ) L2VPN.objects.bulk_create(l2vpns) - def test_created(self): - from datetime import date, date - pk_list = self.queryset.values_list('pk', flat=True)[:2] - print(pk_list) - self.queryset.filter(pk__in=pk_list).update(created=datetime(2021, 1, 1, 0, 0, 0, tzinfo=timezone.utc)) - params = {'created': '2021-01-01T00:00:00'} - fs = self.filterset({}, self.queryset).qs.all() - for res in fs: - print(f'{res.name}:{res.created}') - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) - -class L2VPNTerminationTest(TestCase, ChangeLoggedFilterSetTests): - # TODO: L2VPN Termination Tests +class L2VPNTerminationTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = L2VPNTermination.objects.all() filterset = L2VPNTerminationFilterSet @@ -1511,22 +1498,24 @@ class L2VPNTerminationTest(TestCase, ChangeLoggedFilterSetTests): device_role=device_role, status='active' ) - interfaces = Interface.objects.bulk_create( - Interface(name='GigabitEthernet1/0/1', device=device, type='1000baset'), - Interface(name='GigabitEthernet1/0/2', device=device, type='1000baset'), - Interface(name='GigabitEthernet1/0/3', device=device, type='1000baset'), - Interface(name='GigabitEthernet1/0/4', device=device, type='1000baset'), - Interface(name='GigabitEthernet1/0/5', device=device, type='1000baset'), + + interfaces = ( + Interface(name='Interface 1', device=device, type='1000baset'), + Interface(name='Interface 2', device=device, type='1000baset'), + Interface(name='Interface 3', device=device, type='1000baset'), + Interface(name='Interface 4', device=device, type='1000baset'), + Interface(name='Interface 5', device=device, type='1000baset'), + Interface(name='Interface 6', device=device, type='1000baset') ) + Interface.objects.bulk_create(interfaces) + vlans = ( - VLAN(name='VLAN 1', vid=650001), - VLAN(name='VLAN 2', vid=650002), - VLAN(name='VLAN 3', vid=650003), - VLAN(name='VLAN 4', vid=650004), - VLAN(name='VLAN 5', vid=650005), - VLAN(name='VLAN 6', vid=650006), - VLAN(name='VLAN 7', vid=650007) + VLAN(name='VLAN 1', vid=651), + VLAN(name='VLAN 2', vid=652), + VLAN(name='VLAN 3', vid=653), + VLAN(name='VLAN 4', vid=654), + VLAN(name='VLAN 5', vid=655) ) VLAN.objects.bulk_create(vlans) @@ -1534,26 +1523,33 @@ class L2VPNTerminationTest(TestCase, ChangeLoggedFilterSetTests): l2vpns = ( L2VPN(name='L2VPN 1', type='vxlan', identifier=650001), L2VPN(name='L2VPN 2', type='vpws', identifier=650002), - L2VPN(name='L2VPN 3', type='vpls'), # No RD + L2VPN(name='L2VPN 3', type='vpls'), # No RD, ) L2VPN.objects.bulk_create(l2vpns) l2vpnterminations = ( L2VPNTermination(l2vpn=l2vpns[0], assigned_object=vlans[0]), - L2VPNTermination(l2vpn=l2vpns[0], assigned_object=vlans[1]), - L2VPNTermination(l2vpn=l2vpns[0], assigned_object=vlans[2]) + L2VPNTermination(l2vpn=l2vpns[1], assigned_object=vlans[1]), + L2VPNTermination(l2vpn=l2vpns[2], assigned_object=vlans[2]), + L2VPNTermination(l2vpn=l2vpns[0], assigned_object=interfaces[0]), + L2VPNTermination(l2vpn=l2vpns[1], assigned_object=interfaces[1]), + L2VPNTermination(l2vpn=l2vpns[2], assigned_object=interfaces[2]), ) + L2VPNTermination.objects.bulk_create(l2vpnterminations) + def test_l2vpns(self): l2vpns = L2VPN.objects.all()[:2] params = {'l2vpn_id': [l2vpns[0].pk, l2vpns[1].pk]} - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) params = {'l2vpn': ['L2VPN 1', 'L2VPN 2']} - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) def test_interfaces(self): interfaces = Interface.objects.all()[:2] params = {'interface_id': [interfaces[0].pk, interfaces[1].pk]} + qs = self.filterset(params, self.queryset).qs + results = qs.all() self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) params = {'interface': ['Interface 1', 'Interface 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) diff --git a/netbox/ipam/tests/test_models.py b/netbox/ipam/tests/test_models.py index ce4643516..1b5fbadc3 100644 --- a/netbox/ipam/tests/test_models.py +++ b/netbox/ipam/tests/test_models.py @@ -2,8 +2,9 @@ from netaddr import IPNetwork, IPSet from django.core.exceptions import ValidationError from django.test import TestCase, override_settings +from dcim.models import Interface, Device, DeviceRole, DeviceType, Manufacturer, Site from ipam.choices import IPAddressRoleChoices, PrefixStatusChoices -from ipam.models import Aggregate, IPAddress, IPRange, Prefix, RIR, VLAN, VLANGroup, VRF +from ipam.models import Aggregate, IPAddress, IPRange, Prefix, RIR, VLAN, VLANGroup, VRF, L2VPN, L2VPNTermination class TestAggregate(TestCase): @@ -540,11 +541,75 @@ class TestVLANGroup(TestCase): self.assertEqual(vlangroup.get_next_available_vid(), 105) -class TestL2VPN(TestCase): - # TODO: L2VPN Tests - pass - - class TestL2VPNTermination(TestCase): - # TODO: L2VPN Termination Tests - pass + + @classmethod + def setUpTestData(cls): + + site = Site.objects.create(name='Site 1') + manufacturer = Manufacturer.objects.create(name='Manufacturer 1') + device_type = DeviceType.objects.create(model='Device Type 1', manufacturer=manufacturer) + device_role = DeviceRole.objects.create(name='Switch') + device = Device.objects.create( + name='Device 1', + site=site, + device_type=device_type, + device_role=device_role, + status='active' + ) + + interfaces = ( + Interface(name='Interface 1', device=device, type='1000baset'), + Interface(name='Interface 2', device=device, type='1000baset'), + Interface(name='Interface 3', device=device, type='1000baset'), + Interface(name='Interface 4', device=device, type='1000baset'), + Interface(name='Interface 5', device=device, type='1000baset'), + ) + + Interface.objects.bulk_create(interfaces) + + vlans = ( + VLAN(name='VLAN 1', vid=651), + VLAN(name='VLAN 2', vid=652), + VLAN(name='VLAN 3', vid=653), + VLAN(name='VLAN 4', vid=654), + VLAN(name='VLAN 5', vid=655), + VLAN(name='VLAN 6', vid=656), + VLAN(name='VLAN 7', vid=657) + ) + + VLAN.objects.bulk_create(vlans) + + l2vpns = ( + L2VPN(name='L2VPN 1', type='vxlan', identifier=650001), + L2VPN(name='L2VPN 2', type='vpws', identifier=650002), + L2VPN(name='L2VPN 3', type='vpls'), # No RD + ) + L2VPN.objects.bulk_create(l2vpns) + + l2vpnterminations = ( + L2VPNTermination(l2vpn=l2vpns[0], assigned_object=vlans[0]), + L2VPNTermination(l2vpn=l2vpns[0], assigned_object=vlans[1]), + L2VPNTermination(l2vpn=l2vpns[0], assigned_object=vlans[2]) + ) + + L2VPNTermination.objects.bulk_create(l2vpnterminations) + + def test_duplicate_interface_terminations(self): + device = Device.objects.first() + interface = Interface.objects.filter(device=device).first() + l2vpn = L2VPN.objects.first() + + L2VPNTermination.objects.create(l2vpn=l2vpn, assigned_object=interface) + duplicate = L2VPNTermination(l2vpn=l2vpn, assigned_object=interface) + + self.assertRaises(ValidationError, duplicate.clean) + + def test_duplicate_vlan_terminations(self): + vlan = Interface.objects.first() + l2vpn = L2VPN.objects.first() + + L2VPNTermination.objects.create(l2vpn=l2vpn, assigned_object=vlan) + duplicate = L2VPNTermination(l2vpn=l2vpn, assigned_object=vlan) + self.assertRaises(ValidationError, duplicate.clean) + diff --git a/netbox/ipam/tests/test_views.py b/netbox/ipam/tests/test_views.py index 8d1b9bd1b..dd3733d4d 100644 --- a/netbox/ipam/tests/test_views.py +++ b/netbox/ipam/tests/test_views.py @@ -1,14 +1,18 @@ import datetime +from django.contrib.contenttypes.models import ContentType from django.test import override_settings from django.urls import reverse from netaddr import IPNetwork -from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site +from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site, Interface +from extras.choices import ObjectChangeActionChoices +from extras.models import ObjectChange from ipam.choices import * from ipam.models import * from tenancy.models import Tenant -from utilities.testing import ViewTestCases, create_tags +from users.models import ObjectPermission +from utilities.testing import ViewTestCases, create_tags, post_data class ASNTestCase(ViewTestCases.PrimaryObjectViewTestCase): @@ -749,10 +753,130 @@ class ServiceTestCase(ViewTestCases.PrimaryObjectViewTestCase): class L2VPNTestCase(ViewTestCases.PrimaryObjectViewTestCase): - # TODO: L2VPN Tests - pass + model = L2VPN + csv_data = ( + 'name,slug,type,identifier', + 'L2VPN 5,l2vpn-5,vxlan,456', + 'L2VPN 6,l2vpn-6,vxlan,444', + ) + bulk_edit_data = { + 'description': 'New Description', + } + + @classmethod + def setUpTestData(cls): + rts = ( + RouteTarget(name='64534:123'), + RouteTarget(name='64534:321') + ) + RouteTarget.objects.bulk_create(rts) + + l2vpns = ( + L2VPN(name='L2VPN 1', slug='l2vpn-1', type='vxlan', identifier='650001'), + L2VPN(name='L2VPN 2', slug='l2vpn-2', type='vxlan', identifier='650002'), + L2VPN(name='L2VPN 3', slug='l2vpn-3', type='vxlan', identifier='650003') + ) + + L2VPN.objects.bulk_create(l2vpns) + + cls.form_data = { + 'name': 'L2VPN 8', + 'slug': 'l2vpn-8', + 'type': 'vxlan', + 'identifier': 123, + 'description': 'Description', + 'import_targets': [rts[0].pk], + 'export_targets': [rts[1].pk] + } + + print(cls.form_data) -class L2VPNTerminationTestCase(ViewTestCases.PrimaryObjectViewTestCase): - # TODO: L2VPN Termination Tests - pass +class L2VPNTerminationTestCase( + ViewTestCases.GetObjectViewTestCase, + ViewTestCases.GetObjectChangelogViewTestCase, + ViewTestCases.CreateObjectViewTestCase, + ViewTestCases.EditObjectViewTestCase, + ViewTestCases.DeleteObjectViewTestCase, + ViewTestCases.ListObjectsViewTestCase, + ViewTestCases.BulkImportObjectsViewTestCase, + ViewTestCases.BulkDeleteObjectsViewTestCase, +): + + model = L2VPNTermination + + @classmethod + def setUpTestData(cls): + site = Site.objects.create(name='Site 1') + manufacturer = Manufacturer.objects.create(name='Manufacturer 1') + device_type = DeviceType.objects.create(model='Device Type 1', manufacturer=manufacturer) + device_role = DeviceRole.objects.create(name='Switch') + device = Device.objects.create( + name='Device 1', + site=site, + device_type=device_type, + device_role=device_role, + status='active' + ) + + interface = Interface.objects.create(name='Interface 1', device=device, type='1000baset') + l2vpn = L2VPN.objects.create(name='L2VPN 1', type='vxlan', identifier=650001) + l2vpn_vlans = L2VPN.objects.create(name='L2VPN 2', type='vxlan', identifier=650002) + + vlans = ( + VLAN(name='Vlan 1', vid=1001), + VLAN(name='Vlan 2', vid=1002), + VLAN(name='Vlan 3', vid=1003), + VLAN(name='Vlan 4', vid=1004), + VLAN(name='Vlan 5', vid=1005), + VLAN(name='Vlan 6', vid=1006) + ) + VLAN.objects.bulk_create(vlans) + + terminations = ( + L2VPNTermination(l2vpn=l2vpn, assigned_object=vlans[0]), + L2VPNTermination(l2vpn=l2vpn, assigned_object=vlans[1]), + L2VPNTermination(l2vpn=l2vpn, assigned_object=vlans[2]) + ) + L2VPNTermination.objects.bulk_create(terminations) + + cls.form_data = { + 'l2vpn': l2vpn.pk, + 'device': device.pk, + 'interface': interface.pk, + } + + cls.csv_data = ( + "l2vpn,vlan", + "L2VPN 2,Vlan 4", + "L2VPN 2,Vlan 5", + "L2VPN 2,Vlan 6", + ) + + cls.bulk_edit_data = {} + + # + # Custom assertions + # + + def assertInstanceEqual(self, instance, data, exclude=None, api=False): + """ + Override parent + """ + if exclude is None: + exclude = [] + + fields = [k for k in data.keys() if k not in exclude] + model_dict = self.model_to_dict(instance, fields=fields, api=api) + + # Omit any dictionary keys which are not instance attributes or have been excluded + relevant_data = { + k: v for k, v in data.items() if hasattr(instance, k) and k not in exclude + } + + # Handle relations on the model + for k, v in model_dict.items(): + if isinstance(v, object) and hasattr(v, 'first'): + model_dict[k] = v.first().pk + + self.assertDictEqual(model_dict, relevant_data) diff --git a/netbox/ipam/urls.py b/netbox/ipam/urls.py index 65a6b55ad..e00b0365f 100644 --- a/netbox/ipam/urls.py +++ b/netbox/ipam/urls.py @@ -201,6 +201,7 @@ urlpatterns = [ path('l2vpn-termination/', views.L2VPNTerminationListView.as_view(), name='l2vpntermination_list'), path('l2vpn-termination/add/', views.L2VPNTerminationEditView.as_view(), name='l2vpntermination_add'), path('l2vpn-termination/import/', views.L2VPNTerminationBulkImportView.as_view(), name='l2vpntermination_import'), + path('l2vpn-termination/edit/', views.L2VPNTerminationBulkEditView.as_view(), name='l2vpntermination_bulk_edit'), path('l2vpn-termination/delete/', views.L2VPNTerminationBulkDeleteView.as_view(), name='l2vpntermination_bulk_delete'), path('l2vpn-termination//', views.L2VPNTerminationView.as_view(), name='l2vpntermination'), path('l2vpn-termination//edit/', views.L2VPNTerminationEditView.as_view(), name='l2vpntermination_edit'), diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index 77539434c..35103be48 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -1141,6 +1141,13 @@ class ServiceBulkEditView(generic.BulkEditView): queryset = Service.objects.prefetch_related('device', 'virtual_machine') filterset = filtersets.ServiceFilterSet table = tables.ServiceTable + form = forms.ServiceBulkEditForm + + +class ServiceBulkDeleteView(generic.BulkDeleteView): + queryset = Service.objects.prefetch_related('device', 'virtual_machine') + filterset = filtersets.ServiceFilterSet + table = tables.ServiceTable # L2VPN @@ -1232,14 +1239,14 @@ class L2VPNTerminationBulkImportView(generic.BulkImportView): table = tables.L2VPNTerminationTable +class L2VPNTerminationBulkEditView(generic.BulkEditView): + queryset = L2VPNTermination.objects.all() + filterset = filtersets.L2VPNTerminationFilterSet + table = tables.L2VPNTerminationTable + form = forms.L2VPNTerminationBulkEditForm + + class L2VPNTerminationBulkDeleteView(generic.BulkDeleteView): queryset = L2VPNTermination.objects.all() filterset = filtersets.L2VPNTerminationFilterSet table = tables.L2VPNTerminationTable - form = forms.ServiceBulkEditForm - - -class ServiceBulkDeleteView(generic.BulkDeleteView): - queryset = Service.objects.prefetch_related('device', 'virtual_machine') - filterset = filtersets.ServiceFilterSet - table = tables.ServiceTable diff --git a/netbox/templates/dcim/interface.html b/netbox/templates/dcim/interface.html index e98750518..247592e14 100644 --- a/netbox/templates/dcim/interface.html +++ b/netbox/templates/dcim/interface.html @@ -104,6 +104,10 @@ LAG {{ object.lag|linkify|placeholder }} + + L2VPN + {{ object.l2vpn_termination.l2vpn|linkify|placeholder }} + diff --git a/netbox/templates/ipam/vlan.html b/netbox/templates/ipam/vlan.html index fd0ba36a3..53bb75b8f 100644 --- a/netbox/templates/ipam/vlan.html +++ b/netbox/templates/ipam/vlan.html @@ -64,6 +64,10 @@ Description {{ object.description|placeholder }} + + L2VPN + {{ object.l2vpn_termination.l2vpn|linkify|placeholder }} + From 6e983d154264b6af6586db01ef93586a7276261f Mon Sep 17 00:00:00 2001 From: Daniel Sheppard Date: Wed, 29 Jun 2022 16:14:30 -0500 Subject: [PATCH 187/593] Fix up some PEP errors --- netbox/ipam/api/nested_serializers.py | 1 - netbox/ipam/choices.py | 31 +++++++++++++-------------- netbox/ipam/tests/test_models.py | 1 - 3 files changed, 15 insertions(+), 18 deletions(-) diff --git a/netbox/ipam/api/nested_serializers.py b/netbox/ipam/api/nested_serializers.py index 39305a017..07a7c9598 100644 --- a/netbox/ipam/api/nested_serializers.py +++ b/netbox/ipam/api/nested_serializers.py @@ -218,4 +218,3 @@ class NestedL2VPNTerminationSerializer(WritableNestedSerializer): fields = [ 'id', 'url', 'display', 'l2vpn' ] - diff --git a/netbox/ipam/choices.py b/netbox/ipam/choices.py index a867b05bc..72cd4ff73 100644 --- a/netbox/ipam/choices.py +++ b/netbox/ipam/choices.py @@ -192,26 +192,25 @@ class L2VPNTypeChoices(ChoiceSet): (TYPE_VPLS, 'VPLS'), )), ('E-Line', ( - (TYPE_EPL, 'EPL'), - (TYPE_EVPL, 'EVPL'), - )), + (TYPE_EPL, 'EPL'), + (TYPE_EVPL, 'EVPL'), + )), ('E-LAN', ( - (TYPE_EPLAN, 'Ethernet Private LAN'), - (TYPE_EVPLAN, 'Ethernet Virtual Private LAN'), - )), + (TYPE_EPLAN, 'Ethernet Private LAN'), + (TYPE_EVPLAN, 'Ethernet Virtual Private LAN'), + )), ('E-Tree', ( - (TYPE_EPTREE, 'Ethernet Private Tree'), - (TYPE_EVPTREE, 'Ethernet Virtual Private Tree'), - )), + (TYPE_EPTREE, 'Ethernet Private Tree'), + (TYPE_EVPTREE, 'Ethernet Virtual Private Tree'), + )), ('VXLAN', ( - (TYPE_VXLAN, 'VXLAN'), - (TYPE_VXLAN_EVPN, 'VXLAN-EVPN'), - )), + (TYPE_VXLAN, 'VXLAN'), + (TYPE_VXLAN_EVPN, 'VXLAN-EVPN'), + )), ('L2VPN E-VPN', ( - (TYPE_MPLS_EVPN, 'MPLS EVPN'), - (TYPE_PBB_EVPN, 'PBB EVPN'), - )) - + (TYPE_MPLS_EVPN, 'MPLS EVPN'), + (TYPE_PBB_EVPN, 'PBB EVPN'), + )) ) P2P = ( diff --git a/netbox/ipam/tests/test_models.py b/netbox/ipam/tests/test_models.py index 1b5fbadc3..3bd7e8ccb 100644 --- a/netbox/ipam/tests/test_models.py +++ b/netbox/ipam/tests/test_models.py @@ -612,4 +612,3 @@ class TestL2VPNTermination(TestCase): L2VPNTermination.objects.create(l2vpn=l2vpn, assigned_object=vlan) duplicate = L2VPNTermination(l2vpn=l2vpn, assigned_object=vlan) self.assertRaises(ValidationError, duplicate.clean) - From dd6bfed565fc25f841f58bc3f70e339da37f69ba Mon Sep 17 00:00:00 2001 From: Daniel Sheppard Date: Wed, 29 Jun 2022 18:37:31 -0500 Subject: [PATCH 188/593] Add migration file --- .../0059_l2vpn_l2vpntermination_and_more.py | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 netbox/ipam/migrations/0059_l2vpn_l2vpntermination_and_more.py diff --git a/netbox/ipam/migrations/0059_l2vpn_l2vpntermination_and_more.py b/netbox/ipam/migrations/0059_l2vpn_l2vpntermination_and_more.py new file mode 100644 index 000000000..a8e5ace25 --- /dev/null +++ b/netbox/ipam/migrations/0059_l2vpn_l2vpntermination_and_more.py @@ -0,0 +1,62 @@ +# Generated by Django 4.0.5 on 2022-06-28 04:57 + +import django.core.serializers.json +from django.db import migrations, models +import django.db.models.deletion +import taggit.managers + + +class Migration(migrations.Migration): + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + ('tenancy', '0007_contact_link'), + ('extras', '0076_configcontext_locations'), + ('ipam', '0058_ipaddress_nat_inside_nonunique'), + ] + + operations = [ + migrations.CreateModel( + name='L2VPN', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)), + ('created', models.DateTimeField(auto_now_add=True, null=True)), + ('last_updated', models.DateTimeField(auto_now=True, null=True)), + ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)), + ('name', models.CharField(max_length=100, unique=True)), + ('slug', models.SlugField()), + ('type', models.CharField(max_length=50)), + ('identifier', models.BigIntegerField(blank=True, null=True, unique=True)), + ('description', models.TextField(blank=True, null=True)), + ('export_targets', models.ManyToManyField(blank=True, related_name='exporting_l2vpns', to='ipam.routetarget')), + ('import_targets', models.ManyToManyField(blank=True, related_name='importing_l2vpns', to='ipam.routetarget')), + ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')), + ('tenant', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='l2vpns', to='tenancy.tenant')), + ], + options={ + 'verbose_name': 'L2VPN', + 'ordering': ('identifier', 'name'), + }, + ), + migrations.CreateModel( + name='L2VPNTermination', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)), + ('created', models.DateTimeField(auto_now_add=True, null=True)), + ('last_updated', models.DateTimeField(auto_now=True, null=True)), + ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)), + ('assigned_object_id', models.PositiveBigIntegerField(blank=True, null=True)), + ('assigned_object_type', models.ForeignKey(blank=True, limit_choices_to=models.Q(models.Q(models.Q(('app_label', 'dcim'), ('model', 'interface')), models.Q(('app_label', 'ipam'), ('model', 'vlan')), models.Q(('app_label', 'virtualization'), ('model', 'vminterface')), _connector='OR')), null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.contenttype')), + ('l2vpn', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='terminations', to='ipam.l2vpn')), + ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')), + ], + options={ + 'verbose_name': 'L2VPN Termination', + 'ordering': ('l2vpn',), + }, + ), + migrations.AddConstraint( + model_name='l2vpntermination', + constraint=models.UniqueConstraint(fields=('assigned_object_type', 'assigned_object_id'), name='ipam_l2vpntermination_assigned_object'), + ), + ] From fa014fcbf0aa03d7cdd822ff3aa38e2824773d48 Mon Sep 17 00:00:00 2001 From: Craig Pund Date: Thu, 30 Jun 2022 01:38:38 -0400 Subject: [PATCH 189/593] add device bulk rename view and url --- netbox/dcim/urls.py | 1 + netbox/dcim/views.py | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/netbox/dcim/urls.py b/netbox/dcim/urls.py index c5cd0fa65..f00bd73e1 100644 --- a/netbox/dcim/urls.py +++ b/netbox/dcim/urls.py @@ -248,6 +248,7 @@ urlpatterns = [ path('devices/import/', views.DeviceBulkImportView.as_view(), name='device_import'), path('devices/import/child-devices/', views.ChildDeviceBulkImportView.as_view(), name='device_import_child'), path('devices/edit/', views.DeviceBulkEditView.as_view(), name='device_bulk_edit'), + path('devices/rename/', views.DeviceBulkRenameView.as_view(), name='device_bulk_rename'), path('devices/delete/', views.DeviceBulkDeleteView.as_view(), name='device_bulk_delete'), path('devices//', views.DeviceView.as_view(), name='device'), path('devices//edit/', views.DeviceEditView.as_view(), name='device_edit'), diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 35a1056b2..28325bcfc 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -1786,6 +1786,12 @@ class DeviceBulkDeleteView(generic.BulkDeleteView): table = tables.DeviceTable +class DeviceBulkRenameView(generic.BulkRenameView): + queryset = Device.objects.prefetch_related('tenant', 'site', 'rack', 'device_role', 'device_type__manufacturer') + filterset = filtersets.DeviceFilterSet + table = tables.DeviceTable + + # # Devices # From 5dff7433e854a09cb876602ce8220f8f5479a80a Mon Sep 17 00:00:00 2001 From: Craig Pund Date: Thu, 30 Jun 2022 01:38:53 -0400 Subject: [PATCH 190/593] add bulk device rename button to device_list --- netbox/templates/dcim/device_list.html | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/netbox/templates/dcim/device_list.html b/netbox/templates/dcim/device_list.html index 60efc842e..e5dbe0d9c 100644 --- a/netbox/templates/dcim/device_list.html +++ b/netbox/templates/dcim/device_list.html @@ -2,6 +2,10 @@ {% block bulk_buttons %} {% if perms.dcim.change_device %} + + diff --git a/netbox/templates/generic/bulk_rename.html b/netbox/templates/generic/bulk_rename.html index 134d3df5a..ef6b18cae 100644 --- a/netbox/templates/generic/bulk_rename.html +++ b/netbox/templates/generic/bulk_rename.html @@ -34,11 +34,11 @@
    - Cancel {% if '_preview' in request.POST and not form.errors %} {% endif %} + Cancel
    diff --git a/netbox/templates/generic/confirmation_form.html b/netbox/templates/generic/confirmation_form.html index c4c15f7e7..e9d3d01aa 100644 --- a/netbox/templates/generic/confirmation_form.html +++ b/netbox/templates/generic/confirmation_form.html @@ -2,33 +2,24 @@ {% load form_helpers %} {% block content %} -
    - -
    - -
    - {% csrf_token %} - {% for field in form.hidden_fields %} - {{ field }} - {% endfor %} - -
    -
    {% block confirmation_title %}{% endblock %}
    - -
    - {% block message %}

    Are you sure?

    {% endblock %} -
    - - - -
    - -
    - +
    +
    +
    + {% csrf_token %} + {% for field in form.hidden_fields %} + {{ field }} + {% endfor %} +
    +
    {% block confirmation_title %}{% endblock %}
    +
    + {% block message %}

    Are you sure?

    {% endblock %} +
    +
    - +
    +
    {% endblock %} diff --git a/netbox/templates/generic/object_edit.html b/netbox/templates/generic/object_edit.html index 06308e9ef..892c7d2b1 100644 --- a/netbox/templates/generic/object_edit.html +++ b/netbox/templates/generic/object_edit.html @@ -94,19 +94,19 @@ Context:
    {% block buttons %} - Cancel {% if object.pk %} {% else %} - + {% endif %} + Cancel {% endblock buttons %}
    diff --git a/netbox/templates/generic/object_import.html b/netbox/templates/generic/object_import.html index ffa16b4c2..4d54fde61 100644 --- a/netbox/templates/generic/object_import.html +++ b/netbox/templates/generic/object_import.html @@ -5,19 +5,19 @@ {% block title %}{{ obj_type|bettertitle }} Import{% endblock %} {% block content %} -
    -
    -
    - {% csrf_token %} - {% render_form form %} -
    - {% if return_url %} - Cancel - {% endif %} - - -
    -
    -
    -
    +
    +
    +
    + {% csrf_token %} + {% render_form form %} +
    + + + {% if return_url %} + Cancel + {% endif %} +
    +
    +
    +
    {% endblock content %} diff --git a/netbox/templates/virtualization/vminterface_edit.html b/netbox/templates/virtualization/vminterface_edit.html index 496960a64..316900865 100644 --- a/netbox/templates/virtualization/vminterface_edit.html +++ b/netbox/templates/virtualization/vminterface_edit.html @@ -55,14 +55,3 @@
    {% endif %} {% endblock %} - -{% block buttons %} - Cancel - {% if object.pk %} - - - {% else %} - - - {% endif %} -{% endblock %} From 1c9db2d9f8dffd3ac1f2f28564e054ca73d54e78 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 19 Jul 2022 16:21:32 -0400 Subject: [PATCH 288/593] Fixes #9499: Fix filtered bulk deletion of VM Interfaces --- docs/release-notes/version-3.2.md | 1 + netbox/virtualization/views.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/docs/release-notes/version-3.2.md b/docs/release-notes/version-3.2.md index 7b9adb374..806b0fa91 100644 --- a/docs/release-notes/version-3.2.md +++ b/docs/release-notes/version-3.2.md @@ -10,6 +10,7 @@ ### Bug Fixes * [#9437](https://github.com/netbox-community/netbox/issues/9437) - Standardize form submission buttons and behavior when using enter key +* [#9499](https://github.com/netbox-community/netbox/issues/9499) - Fix filtered bulk deletion of VM Interfaces * [#9634](https://github.com/netbox-community/netbox/issues/9634) - Fix image URLs in rack elevations when using external storage * [#9715](https://github.com/netbox-community/netbox/issues/9715) - Fix `SOCIAL_AUTH_PIPELINE` config parameter not taking effect * [#9754](https://github.com/netbox-community/netbox/issues/9754) - Fix regression introduced by #9632 diff --git a/netbox/virtualization/views.py b/netbox/virtualization/views.py index 0b593289b..4cd7da30d 100644 --- a/netbox/virtualization/views.py +++ b/netbox/virtualization/views.py @@ -471,6 +471,7 @@ class VMInterfaceBulkImportView(generic.BulkImportView): class VMInterfaceBulkEditView(generic.BulkEditView): queryset = VMInterface.objects.all() + filterset = filtersets.VMInterfaceFilterSet table = tables.VMInterfaceTable form = forms.VMInterfaceBulkEditForm @@ -482,6 +483,7 @@ class VMInterfaceBulkRenameView(generic.BulkRenameView): class VMInterfaceBulkDeleteView(generic.BulkDeleteView): queryset = VMInterface.objects.all() + filterset = filtersets.VMInterfaceFilterSet table = tables.VMInterfaceTable From e92b7f8bb95ebf2018a0d57dad2fe88a69cecc85 Mon Sep 17 00:00:00 2001 From: Marek Zbroch Date: Wed, 20 Jul 2022 13:02:10 +0200 Subject: [PATCH 289/593] Typo fix in REARPORT_BUTTONS template code --- netbox/dcim/tables/template_code.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/dcim/tables/template_code.py b/netbox/dcim/tables/template_code.py index 90befe0a4..082df56df 100644 --- a/netbox/dcim/tables/template_code.py +++ b/netbox/dcim/tables/template_code.py @@ -343,7 +343,7 @@ REARPORT_BUTTONS = """
  • Console Port
  • Front Port
  • Rear Port
  • -
  • Circuit Termination
  • +
  • Circuit Termination
  • {% else %} From 17e00ac0402a10fd823dc7f223b4db6b1c4ef9e1 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 20 Jul 2022 10:39:36 -0400 Subject: [PATCH 290/593] Fixes #9705: Support filter expressions for the serial field on racks, devices, and inventory items --- docs/release-notes/version-3.2.md | 1 + netbox/dcim/filtersets.py | 9 ++++++--- netbox/dcim/tests/test_filtersets.py | 20 +++++++++++--------- netbox/netbox/filtersets.py | 2 +- 4 files changed, 19 insertions(+), 13 deletions(-) diff --git a/docs/release-notes/version-3.2.md b/docs/release-notes/version-3.2.md index 806b0fa91..1e422547e 100644 --- a/docs/release-notes/version-3.2.md +++ b/docs/release-notes/version-3.2.md @@ -4,6 +4,7 @@ ### Enhancements +* [#9705](https://github.com/netbox-community/netbox/issues/9705) - Support filter expressions for the `serial` field on racks, devices, and inventory items * [#9741](https://github.com/netbox-community/netbox/issues/9741) - Check for UserConfig instance during user login * [#9745](https://github.com/netbox-community/netbox/issues/9745) - Add wireless LANs and links to global search diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py index 4b4201578..f5342106e 100644 --- a/netbox/dcim/filtersets.py +++ b/netbox/dcim/filtersets.py @@ -307,7 +307,7 @@ class RackFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSe to_field_name='slug', label='Role (slug)', ) - serial = django_filters.CharFilter( + serial = MultiValueCharFilter( lookup_expr='iexact' ) @@ -1002,10 +1002,13 @@ class ModuleFilterSet(NetBoxModelFilterSet): queryset=Device.objects.all(), label='Device (ID)', ) + serial = MultiValueCharFilter( + lookup_expr='iexact' + ) class Meta: model = Module - fields = ['id', 'serial', 'asset_tag'] + fields = ['id', 'asset_tag'] def search(self, queryset, name, value): if not value.strip(): @@ -1400,7 +1403,7 @@ class InventoryItemFilterSet(DeviceComponentFilterSet, NetBoxModelFilterSet): ) component_type = ContentTypeFilter() component_id = MultiValueNumberFilter() - serial = django_filters.CharFilter( + serial = MultiValueCharFilter( lookup_expr='iexact' ) diff --git a/netbox/dcim/tests/test_filtersets.py b/netbox/dcim/tests/test_filtersets.py index 47aa9368c..cf0f397df 100644 --- a/netbox/dcim/tests/test_filtersets.py +++ b/netbox/dcim/tests/test_filtersets.py @@ -494,10 +494,10 @@ class RackTestCase(TestCase, ChangeLoggedFilterSetTests): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_serial(self): - params = {'serial': 'ABC'} - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) - params = {'serial': 'abc'} - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + params = {'serial': ['ABC', 'DEF']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'serial': ['abc', 'def']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_tenant(self): tenants = Tenant.objects.all()[:2] @@ -1860,7 +1860,9 @@ class ModuleTestCase(TestCase, ChangeLoggedFilterSetTests): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6) def test_serial(self): - params = {'asset_tag': ['A', 'B']} + params = {'serial': ['A', 'B']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'serial': ['a', 'b']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_asset_tag(self): @@ -3413,10 +3415,10 @@ class InventoryItemTestCase(TestCase, ChangeLoggedFilterSetTests): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_serial(self): - params = {'serial': 'ABC'} - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) - params = {'serial': 'abc'} - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + params = {'serial': ['ABC', 'DEF']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'serial': ['abc', 'def']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_component_type(self): params = {'component_type': 'dcim.interface'} diff --git a/netbox/netbox/filtersets.py b/netbox/netbox/filtersets.py index 1a72d8159..f509afa5b 100644 --- a/netbox/netbox/filtersets.py +++ b/netbox/netbox/filtersets.py @@ -125,7 +125,7 @@ class BaseFilterSet(django_filters.FilterSet): return {} # Skip nonstandard lookup expressions - if existing_filter.method is not None or existing_filter.lookup_expr not in ['exact', 'in']: + if existing_filter.method is not None or existing_filter.lookup_expr not in ['exact', 'iexact', 'in']: return {} # Choose the lookup expression map based on the filter type From 9835d6b2dfd05d4ba1eb4d39dd6e6828948239db Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 20 Jul 2022 10:57:11 -0400 Subject: [PATCH 291/593] Release NetBox v3.2.7 --- .github/ISSUE_TEMPLATE/bug_report.yaml | 2 +- .github/ISSUE_TEMPLATE/feature_request.yaml | 2 +- docs/release-notes/version-3.2.md | 2 +- netbox/netbox/settings.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index 78231890b..332a0ad75 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -14,7 +14,7 @@ body: attributes: label: NetBox version description: What version of NetBox are you currently running? - placeholder: v3.2.6 + placeholder: v3.2.7 validations: required: true - type: dropdown diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml index 71d45092c..ff9b5e358 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yaml +++ b/.github/ISSUE_TEMPLATE/feature_request.yaml @@ -14,7 +14,7 @@ body: attributes: label: NetBox version description: What version of NetBox are you currently running? - placeholder: v3.2.6 + placeholder: v3.2.7 validations: required: true - type: dropdown diff --git a/docs/release-notes/version-3.2.md b/docs/release-notes/version-3.2.md index 1e422547e..35e9b9a22 100644 --- a/docs/release-notes/version-3.2.md +++ b/docs/release-notes/version-3.2.md @@ -1,6 +1,6 @@ # NetBox v3.2 -## v3.2.7 (FUTURE) +## v3.2.7 (2022-07-20) ### Enhancements diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index b17d70e31..094771581 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -29,7 +29,7 @@ django.utils.encoding.force_text = force_str # Environment setup # -VERSION = '3.2.7-dev' +VERSION = '3.2.7' # Hostname HOSTNAME = platform.node() From 383918d83ba039e0e1217afa353312d972c181c3 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 20 Jul 2022 11:15:02 -0400 Subject: [PATCH 292/593] PRVB --- docs/release-notes/version-3.2.md | 4 ++++ netbox/netbox/settings.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/version-3.2.md b/docs/release-notes/version-3.2.md index 35e9b9a22..c36344912 100644 --- a/docs/release-notes/version-3.2.md +++ b/docs/release-notes/version-3.2.md @@ -1,5 +1,9 @@ # NetBox v3.2 +## v3.2.8 (FUTURE) + +--- + ## v3.2.7 (2022-07-20) ### Enhancements diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 094771581..c7e49c1be 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -29,7 +29,7 @@ django.utils.encoding.force_text = force_str # Environment setup # -VERSION = '3.2.7' +VERSION = '3.2.8-dev' # Hostname HOSTNAME = platform.node() From e2580ea469e0394d7e00a65d91aca51968a97ad1 Mon Sep 17 00:00:00 2001 From: Juho Ylikorpi Date: Thu, 21 Jul 2022 16:54:23 +0300 Subject: [PATCH 293/593] fixes #9818 --- netbox/dcim/forms/connections.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/dcim/forms/connections.py b/netbox/dcim/forms/connections.py index 62247e3e3..7552c0c87 100644 --- a/netbox/dcim/forms/connections.py +++ b/netbox/dcim/forms/connections.py @@ -138,7 +138,7 @@ def get_cable_form(a_type, b_type): label='Side', disabled_indicator='_occupied', query_params={ - 'circuit_id': f'termination_{cable_end}_circuit', + 'circuit_id': f'$termination_{cable_end}_circuit', } ) From 451a0067c70f13741cb52e5b9ee3dad5cb1d9d58 Mon Sep 17 00:00:00 2001 From: Gabor SOMOGYVARI Date: Fri, 22 Jul 2022 10:42:20 +0200 Subject: [PATCH 294/593] Closes #9825: Add Contacts to VM table view --- netbox/virtualization/tables/virtualmachines.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/netbox/virtualization/tables/virtualmachines.py b/netbox/virtualization/tables/virtualmachines.py index 8cff96227..d2be07876 100644 --- a/netbox/virtualization/tables/virtualmachines.py +++ b/netbox/virtualization/tables/virtualmachines.py @@ -48,6 +48,9 @@ class VirtualMachineTable(TenancyColumnsMixin, NetBoxTable): order_by=('primary_ip4', 'primary_ip6'), verbose_name='IP Address' ) + contacts = columns.ManyToManyColumn( + linkify_item=True + ) tags = columns.TagColumn( url_name='virtualization:virtualmachine_list' ) @@ -56,7 +59,7 @@ class VirtualMachineTable(TenancyColumnsMixin, NetBoxTable): model = VirtualMachine fields = ( 'pk', 'id', 'name', 'status', 'cluster', 'role', 'tenant', 'tenant_group', 'platform', 'vcpus', 'memory', 'disk', - 'primary_ip4', 'primary_ip6', 'primary_ip', 'comments', 'tags', 'created', 'last_updated', + 'primary_ip4', 'primary_ip6', 'primary_ip', 'comments', 'contacts', 'tags', 'created', 'last_updated', ) default_columns = ( 'pk', 'name', 'status', 'cluster', 'role', 'tenant', 'vcpus', 'memory', 'disk', 'primary_ip', From 7d6882bec29f2ffe70a278cee99f0abc9216d498 Mon Sep 17 00:00:00 2001 From: Renato Almeida de Oliveira Date: Sat, 23 Jul 2022 20:24:33 +0000 Subject: [PATCH 295/593] Change display to Modal --- netbox/templates/ipam/prefix.html | 84 +++++++++++++++++++++---------- 1 file changed, 58 insertions(+), 26 deletions(-) diff --git a/netbox/templates/ipam/prefix.html b/netbox/templates/ipam/prefix.html index caad547c4..ed2c3339b 100644 --- a/netbox/templates/ipam/prefix.html +++ b/netbox/templates/ipam/prefix.html @@ -143,34 +143,16 @@ {% endwith %} - {% if object.prefix.version == 4 %} - - - Network Mask - - - {{ object.prefix.netmask }} - - - - - Broadcast Address - - - {{ object.prefix.broadcast }} - - - - - Wildcard Mask - - - {{ object.prefix.hostmask }} - - - {% endif %}
    + {% if object.prefix.version == 4 %} + + {% endif %} {% include 'inc/panels/custom_fields.html' %} {% include 'inc/panels/tags.html' %} @@ -186,4 +168,54 @@ {% plugin_full_width_page object %} +{% if object.prefix.version == 4 %} + +{% endif %} {% endblock %} From 12476036cd3dc5990d2a6c9973ed9ccc758c377d Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 25 Jul 2022 09:55:20 -0400 Subject: [PATCH 296/593] Changelog for #9794, #9818 --- docs/release-notes/version-3.3.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/release-notes/version-3.3.md b/docs/release-notes/version-3.3.md index 4b3f1dcb2..ad9ce1a0e 100644 --- a/docs/release-notes/version-3.3.md +++ b/docs/release-notes/version-3.3.md @@ -103,6 +103,8 @@ Custom field UI visibility has no impact on API operation. * [#9730](https://github.com/netbox-community/netbox/issues/9730) - Fix validation error when creating a new cable via UI form * [#9733](https://github.com/netbox-community/netbox/issues/9733) - Handle split paths during trace when fanning out to front ports with differing cables * [#9765](https://github.com/netbox-community/netbox/issues/9765) - Report correct segment count under cable trace UI view +* [#9794](https://github.com/netbox-community/netbox/issues/9794) - Fix link to connect a rear port to a circuit termination +* [#9818](https://github.com/netbox-community/netbox/issues/9818) - Fix circuit side selection when connecting a cable to a circuit termination ### Plugins API From 2583abc39d4eafdca645bc18110140f1e350c7df Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 25 Jul 2022 11:34:16 -0400 Subject: [PATCH 297/593] Fix null cable termination representation --- .../templates/dcim/inc/cable_termination.html | 118 +++++++++--------- 1 file changed, 61 insertions(+), 57 deletions(-) diff --git a/netbox/templates/dcim/inc/cable_termination.html b/netbox/templates/dcim/inc/cable_termination.html index 9d1c43bdd..ced9bda50 100644 --- a/netbox/templates/dcim/inc/cable_termination.html +++ b/netbox/templates/dcim/inc/cable_termination.html @@ -1,58 +1,62 @@ {% load helpers %} - - {% if terminations.0.device %} - {# Device component #} - - - - - - - - - - - - - - - - - {% elif terminations.0.power_panel %} - {# Power feed #} - - - - - - - - - - - - - {% else %} - {# Circuit termination #} - - - - - - - - - {% endif %} -
    Site{{ terminations.0.device.site|linkify }}
    Rack{{ terminations.0.device.rack|linkify|placeholder }}
    Device{{ terminations.0.device|linkify }}
    {{ terminations.0|meta:"verbose_name"|capfirst }} - {% for term in terminations %} - {{ term|linkify }}{% if not forloop.last %},{% endif %} - {% endfor %} -
    Site{{ terminations.0.power_panel.site|linkify }}
    Power Panel{{ terminations.0.power_panel|linkify }}
    {{ terminations.0|meta:"verbose_name"|capfirst }} - {% for term in terminations %} - {{ term|linkify }}{% if not forloop.last %},{% endif %} - {% endfor %} -
    Provider{{ terminations.0.circuit.provider|linkify }}
    Circuit - {% for term in terminations %} - {{ term.circuit|linkify }} ({{ term }}){% if not forloop.last %},{% endif %} - {% endfor %} -
    +{% if terminations.0 %} + + {% if terminations.0.device %} + {# Device component #} + + + + + + + + + + + + + + + + + {% elif terminations.0.power_panel %} + {# Power feed #} + + + + + + + + + + + + + {% elif terminations.0.circuit %} + {# Circuit termination #} + + + + + + + + + {% endif %} +
    Site{{ terminations.0.device.site|linkify }}
    Rack{{ terminations.0.device.rack|linkify|placeholder }}
    Device{{ terminations.0.device|linkify }}
    {{ terminations.0|meta:"verbose_name"|capfirst }} + {% for term in terminations %} + {{ term|linkify }}{% if not forloop.last %},{% endif %} + {% endfor %} +
    Site{{ terminations.0.power_panel.site|linkify }}
    Power Panel{{ terminations.0.power_panel|linkify }}
    {{ terminations.0|meta:"verbose_name"|capfirst }} + {% for term in terminations %} + {{ term|linkify }}{% if not forloop.last %},{% endif %} + {% endfor %} +
    Provider{{ terminations.0.circuit.provider|linkify }}
    Circuit + {% for term in terminations %} + {{ term.circuit|linkify }} ({{ term }}){% if not forloop.last %},{% endif %} + {% endfor %} +
    +{% else %} + No termination +{% endif %} From 6f7289f93217c5d3499264f2cfe6e540ba17d8e9 Mon Sep 17 00:00:00 2001 From: Daniel Sheppard Date: Tue, 26 Jul 2022 07:22:21 -0500 Subject: [PATCH 298/593] Fixes #9844 - Add dedicated `device_vlan` form field --- netbox/ipam/forms/models.py | 10 ++++++++-- netbox/templates/ipam/l2vpntermination_edit.html | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/netbox/ipam/forms/models.py b/netbox/ipam/forms/models.py index 415c952be..0a22cbc21 100644 --- a/netbox/ipam/forms/models.py +++ b/netbox/ipam/forms/models.py @@ -906,8 +906,9 @@ class L2VPNTerminationForm(NetBoxModelForm): label='L2VPN', fetch_trigger='open' ) - device = DynamicModelChoiceField( + device_vlan = DynamicModelChoiceField( queryset=Device.objects.all(), + label="Available on Device", required=False, query_params={} ) @@ -915,10 +916,15 @@ class L2VPNTerminationForm(NetBoxModelForm): queryset=VLAN.objects.all(), required=False, query_params={ - 'available_on_device': '$device' + 'available_on_device': '$device_vlan' }, label='VLAN' ) + device = DynamicModelChoiceField( + queryset=Device.objects.all(), + required=False, + query_params={} + ) interface = DynamicModelChoiceField( queryset=Interface.objects.all(), required=False, diff --git a/netbox/templates/ipam/l2vpntermination_edit.html b/netbox/templates/ipam/l2vpntermination_edit.html index 7b4a9f50a..c66b8a3d1 100644 --- a/netbox/templates/ipam/l2vpntermination_edit.html +++ b/netbox/templates/ipam/l2vpntermination_edit.html @@ -32,7 +32,7 @@
    - {% render_field form.device %} + {% render_field form.device_vlan %} {% render_field form.vlan %}
    From 6d30c07dd0608c80e4f0a161d7f5ed129b9baa0e Mon Sep 17 00:00:00 2001 From: Daniel Sheppard Date: Tue, 26 Jul 2022 07:29:18 -0500 Subject: [PATCH 299/593] Update changelog for #9844 --- docs/release-notes/version-3.3.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes/version-3.3.md b/docs/release-notes/version-3.3.md index 4b3f1dcb2..85a2e1599 100644 --- a/docs/release-notes/version-3.3.md +++ b/docs/release-notes/version-3.3.md @@ -103,6 +103,7 @@ Custom field UI visibility has no impact on API operation. * [#9730](https://github.com/netbox-community/netbox/issues/9730) - Fix validation error when creating a new cable via UI form * [#9733](https://github.com/netbox-community/netbox/issues/9733) - Handle split paths during trace when fanning out to front ports with differing cables * [#9765](https://github.com/netbox-community/netbox/issues/9765) - Report correct segment count under cable trace UI view +* [#9844](https://github.com/netbox-community/netbox/issues/9844) - Fix interface api request when creating/editing L2VPN termination ### Plugins API From d442f8fd60c88ffabcd6f75e438baa8ecca15f98 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 26 Jul 2022 11:09:51 -0400 Subject: [PATCH 300/593] Fixes #9843: Fix rendering of custom field values (regression from #9647) --- docs/release-notes/version-3.3.md | 1 + .../templates/builtins/customfield_value.html | 20 +++++++++---------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/docs/release-notes/version-3.3.md b/docs/release-notes/version-3.3.md index 8aa91f8f9..51d595123 100644 --- a/docs/release-notes/version-3.3.md +++ b/docs/release-notes/version-3.3.md @@ -105,6 +105,7 @@ Custom field UI visibility has no impact on API operation. * [#9765](https://github.com/netbox-community/netbox/issues/9765) - Report correct segment count under cable trace UI view * [#9794](https://github.com/netbox-community/netbox/issues/9794) - Fix link to connect a rear port to a circuit termination * [#9818](https://github.com/netbox-community/netbox/issues/9818) - Fix circuit side selection when connecting a cable to a circuit termination +* [#9843](https://github.com/netbox-community/netbox/issues/9843) - Fix rendering of custom field values (regression from #9647) * [#9844](https://github.com/netbox-community/netbox/issues/9844) - Fix interface api request when creating/editing L2VPN termination ### Plugins API diff --git a/netbox/utilities/templates/builtins/customfield_value.html b/netbox/utilities/templates/builtins/customfield_value.html index 8fedb03d5..ff93a5168 100644 --- a/netbox/utilities/templates/builtins/customfield_value.html +++ b/netbox/utilities/templates/builtins/customfield_value.html @@ -1,26 +1,26 @@ -{% if field.type == 'integer' and value is not None %} +{% if customfield.type == 'integer' and value is not None %} {{ value }} -{% elif field.type == 'longtext' and value %} +{% elif customfield.type == 'longtext' and value %} {{ value|markdown }} -{% elif field.type == 'boolean' and value == True %} +{% elif customfield.type == 'boolean' and value == True %} {% checkmark value true="True" %} -{% elif field.type == 'boolean' and value == False %} +{% elif customfield.type == 'boolean' and value == False %} {% checkmark value false="False" %} -{% elif field.type == 'url' and value %} +{% elif customfield.type == 'url' and value %} {{ value|truncatechars:70 }} -{% elif field.type == 'json' and value %} +{% elif customfield.type == 'json' and value %}
    {{ value|json }}
    -{% elif field.type == 'multiselect' and value %} +{% elif customfield.type == 'multiselect' and value %} {{ value|join:", " }} -{% elif field.type == 'object' and value %} +{% elif customfield.type == 'object' and value %} {{ value|linkify }} -{% elif field.type == 'multiobject' and value %} +{% elif customfield.type == 'multiobject' and value %} {% for object in value %} {{ object|linkify }}{% if not forloop.last %}
    {% endif %} {% endfor %} {% elif value %} {{ value }} -{% elif field.required %} +{% elif customfield.required %} Not defined {% else %} {{ ''|placeholder }} From 466931d2fbb5e9db6b3eb90d385119ae44b34886 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 26 Jul 2022 12:41:51 -0400 Subject: [PATCH 301/593] Fixes #9829: Arrange custom fields by group when editing objects --- docs/release-notes/version-3.3.md | 1 + netbox/extras/forms/customfields.py | 4 +++ netbox/netbox/forms/base.py | 27 ++++++------------- .../templates/inc/panels/custom_fields.html | 2 +- .../form_helpers/render_custom_fields.html | 13 ++++++--- 5 files changed, 23 insertions(+), 24 deletions(-) diff --git a/docs/release-notes/version-3.3.md b/docs/release-notes/version-3.3.md index 51d595123..7e8fd50e3 100644 --- a/docs/release-notes/version-3.3.md +++ b/docs/release-notes/version-3.3.md @@ -105,6 +105,7 @@ Custom field UI visibility has no impact on API operation. * [#9765](https://github.com/netbox-community/netbox/issues/9765) - Report correct segment count under cable trace UI view * [#9794](https://github.com/netbox-community/netbox/issues/9794) - Fix link to connect a rear port to a circuit termination * [#9818](https://github.com/netbox-community/netbox/issues/9818) - Fix circuit side selection when connecting a cable to a circuit termination +* [#9829](https://github.com/netbox-community/netbox/issues/9829) - Arrange custom fields by group when editing objects * [#9843](https://github.com/netbox-community/netbox/issues/9843) - Fix rendering of custom field values (regression from #9647) * [#9844](https://github.com/netbox-community/netbox/issues/9844) - Fix interface api request when creating/editing L2VPN termination diff --git a/netbox/extras/forms/customfields.py b/netbox/extras/forms/customfields.py index 4cf8b5e0a..7574f4f2b 100644 --- a/netbox/extras/forms/customfields.py +++ b/netbox/extras/forms/customfields.py @@ -19,6 +19,7 @@ class CustomFieldsMixin: def __init__(self, *args, **kwargs): self.custom_fields = {} + self.custom_field_groups = {} super().__init__(*args, **kwargs) @@ -58,3 +59,6 @@ class CustomFieldsMixin: # 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: + self.custom_field_groups[customfield.group_name] = [] + self.custom_field_groups[customfield.group_name].append(field_name) diff --git a/netbox/netbox/forms/base.py b/netbox/netbox/forms/base.py index 0e232af1d..2676e4cde 100644 --- a/netbox/netbox/forms/base.py +++ b/netbox/netbox/forms/base.py @@ -94,30 +94,19 @@ class NetBoxModelBulkEditForm(BootstrapMixin, CustomFieldsMixin, forms.Form): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + self.fields['pk'].queryset = self.model.objects.all() + self._extend_nullable_fields() + def _get_form_field(self, customfield): return customfield.to_form_field(set_initial=False, enforce_required=False) - def _append_customfield_fields(self): - """ - Append form fields for all CustomFields assigned to this object type. - """ - nullable_custom_fields = [] - for customfield in self._get_custom_fields(self._get_content_type()): - field_name = f'cf_{customfield.name}' - self.fields[field_name] = self._get_form_field(customfield) - - # Record non-required custom fields as nullable - if not customfield.required: - nullable_custom_fields.append(field_name) - - # Annotate the field in the list of CustomField form fields - self.custom_fields[field_name] = customfield - - # Annotate nullable custom fields (if any) on the form instance - if nullable_custom_fields: - self.nullable_fields = (*self.nullable_fields, *nullable_custom_fields) + def _extend_nullable_fields(self): + nullable_custom_fields = [ + name for name, customfield in self.custom_fields.items() if not customfield.required + ] + self.nullable_fields = (*self.nullable_fields, *nullable_custom_fields) class NetBoxModelFilterSetForm(BootstrapMixin, CustomFieldsMixin, forms.Form): diff --git a/netbox/templates/inc/panels/custom_fields.html b/netbox/templates/inc/panels/custom_fields.html index 90059447f..616b1c712 100644 --- a/netbox/templates/inc/panels/custom_fields.html +++ b/netbox/templates/inc/panels/custom_fields.html @@ -7,7 +7,7 @@
    {% for group_name, fields in custom_fields.items %} {% if group_name %} -
    {{ group_name }}
    +
    {{ group_name }}
    {% endif %} {% for field, value in fields.items %} diff --git a/netbox/utilities/templates/form_helpers/render_custom_fields.html b/netbox/utilities/templates/form_helpers/render_custom_fields.html index f3e5bffa9..6b0b2840b 100644 --- a/netbox/utilities/templates/form_helpers/render_custom_fields.html +++ b/netbox/utilities/templates/form_helpers/render_custom_fields.html @@ -1,7 +1,12 @@ {% load form_helpers %} -{% for field in form %} - {% if field.name in form.custom_fields %} - {% render_field field %} - {% endif %} +{% for group, fields in form.custom_field_groups.items %} + {% if group %} +
    +
    {{ group }}
    +
    + {% endif %} + {% for name in fields %} + {% render_field form|getfield:name %} + {% endfor %} {% endfor %} From a6be8dccf55726ddd1c9a8319f4a162c43023fa0 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 26 Jul 2022 15:45:47 -0400 Subject: [PATCH 302/593] Fixes #9847: Respect desc_units when ordering rack units --- docs/release-notes/version-3.3.md | 1 + netbox/dcim/models/racks.py | 5 ++--- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/release-notes/version-3.3.md b/docs/release-notes/version-3.3.md index 7e8fd50e3..87cb00730 100644 --- a/docs/release-notes/version-3.3.md +++ b/docs/release-notes/version-3.3.md @@ -108,6 +108,7 @@ Custom field UI visibility has no impact on API operation. * [#9829](https://github.com/netbox-community/netbox/issues/9829) - Arrange custom fields by group when editing objects * [#9843](https://github.com/netbox-community/netbox/issues/9843) - Fix rendering of custom field values (regression from #9647) * [#9844](https://github.com/netbox-community/netbox/issues/9844) - Fix interface api request when creating/editing L2VPN termination +* [#9847](https://github.com/netbox-community/netbox/issues/9847) - Respect `desc_units` when ordering rack units ### Plugins API diff --git a/netbox/dcim/models/racks.py b/netbox/dcim/models/racks.py index 2039def09..4dcfcde28 100644 --- a/netbox/dcim/models/racks.py +++ b/netbox/dcim/models/racks.py @@ -244,10 +244,9 @@ class Rack(NetBoxModel): """ Return a list of unit numbers, top to bottom. """ - max_position = self.u_height + decimal.Decimal(0.5) if self.desc_units: - drange(0.5, max_position, 0.5) - return drange(max_position, 0.5, -0.5) + return drange(decimal.Decimal(1.0), self.u_height + 1, 0.5) + return drange(self.u_height + decimal.Decimal(0.5), 0.5, -0.5) def get_status_color(self): return RackStatusChoices.colors.get(self.status) From bbf4b906e4083dbeadbaa802af5aac18e1896ba3 Mon Sep 17 00:00:00 2001 From: sleepinggenius2 Date: Tue, 26 Jul 2022 17:16:03 -0400 Subject: [PATCH 303/593] Adds patterned_fields support for bulk components --- netbox/dcim/views.py | 1 + netbox/netbox/views/generic/bulk_views.py | 17 +++++++++-------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index ec3e9152e..3bec02f5c 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -2707,6 +2707,7 @@ class DeviceBulkAddModuleBayView(generic.BulkComponentCreateView): filterset = filtersets.DeviceFilterSet table = tables.DeviceTable default_return_url = 'dcim:device_list' + patterned_fields = ('name', 'label', 'position') class DeviceBulkAddDeviceBayView(generic.BulkComponentCreateView): diff --git a/netbox/netbox/views/generic/bulk_views.py b/netbox/netbox/views/generic/bulk_views.py index 5bdf5cbc9..7e07c57d0 100644 --- a/netbox/netbox/views/generic/bulk_views.py +++ b/netbox/netbox/views/generic/bulk_views.py @@ -795,6 +795,7 @@ class BulkComponentCreateView(GetReturnURLMixin, BaseMultiObjectView): model_form = None filterset = None table = None + patterned_fields = ('name', 'label') def get_required_permission(self): return f'dcim.add_{self.queryset.model._meta.model_name}' @@ -830,16 +831,16 @@ class BulkComponentCreateView(GetReturnURLMixin, BaseMultiObjectView): for obj in data['pk']: - names = data['name_pattern'] - labels = data['label_pattern'] if 'label_pattern' in data else None - for i, name in enumerate(names): - label = labels[i] if labels else None - + pattern_count = len(data[f'{self.patterned_fields[0]}_pattern']) + for i in range(pattern_count): component_data = { - self.parent_field: obj.pk, - 'name': name, - 'label': label + self.parent_field: obj.pk } + + for field_name in self.patterned_fields: + if data.get(f'{field_name}_pattern'): + component_data[field_name] = data[f'{field_name}_pattern'][i] + component_data.update(data) component_form = self.model_form(component_data) if component_form.is_valid(): From 6cee12b1530597bf308ba39c661e456bd1802835 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 27 Jul 2022 15:40:25 -0400 Subject: [PATCH 304/593] Fix formatting of webhook conditions form field --- netbox/extras/forms/models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/netbox/extras/forms/models.py b/netbox/extras/forms/models.py index 112911f42..82575de21 100644 --- a/netbox/extras/forms/models.py +++ b/netbox/extras/forms/models.py @@ -133,6 +133,7 @@ class WebhookForm(BootstrapMixin, forms.ModelForm): 'http_method': StaticSelect(), 'additional_headers': forms.Textarea(attrs={'class': 'font-monospace'}), 'body_template': forms.Textarea(attrs={'class': 'font-monospace'}), + 'conditions': forms.Textarea(attrs={'class': 'font-monospace'}), } From 498b655cb726b5bb1be442739923ae3746a8da41 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 27 Jul 2022 16:50:31 -0400 Subject: [PATCH 305/593] Changelog and cleanup for #9825 --- docs/release-notes/version-3.2.md | 4 ++++ netbox/virtualization/tables/virtualmachines.py | 11 ++++------- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/docs/release-notes/version-3.2.md b/docs/release-notes/version-3.2.md index c36344912..14d73d1eb 100644 --- a/docs/release-notes/version-3.2.md +++ b/docs/release-notes/version-3.2.md @@ -2,6 +2,10 @@ ## v3.2.8 (FUTURE) +### Enhancements + +* [#9825](https://github.com/netbox-community/netbox/issues/9825) - Add contacts column to virtual machines table + --- ## v3.2.7 (2022-07-20) diff --git a/netbox/virtualization/tables/virtualmachines.py b/netbox/virtualization/tables/virtualmachines.py index d2be07876..cace51ccc 100644 --- a/netbox/virtualization/tables/virtualmachines.py +++ b/netbox/virtualization/tables/virtualmachines.py @@ -58,8 +58,9 @@ class VirtualMachineTable(TenancyColumnsMixin, NetBoxTable): class Meta(NetBoxTable.Meta): model = VirtualMachine fields = ( - 'pk', 'id', 'name', 'status', 'cluster', 'role', 'tenant', 'tenant_group', 'platform', 'vcpus', 'memory', 'disk', - 'primary_ip4', 'primary_ip6', 'primary_ip', 'comments', 'contacts', 'tags', 'created', 'last_updated', + 'pk', 'id', 'name', 'status', 'cluster', 'role', 'tenant', 'tenant_group', 'platform', 'vcpus', 'memory', + 'disk', 'primary_ip4', 'primary_ip6', 'primary_ip', 'comments', 'contacts', 'tags', 'created', + 'last_updated', ) default_columns = ( 'pk', 'name', 'status', 'cluster', 'role', 'tenant', 'vcpus', 'memory', 'disk', 'primary_ip', @@ -80,9 +81,6 @@ class VMInterfaceTable(BaseInterfaceTable): vrf = tables.Column( linkify=True ) - contacts = columns.ManyToManyColumn( - linkify_item=True - ) tags = columns.TagColumn( url_name='virtualization:vminterface_list' ) @@ -91,8 +89,7 @@ class VMInterfaceTable(BaseInterfaceTable): model = VMInterface fields = ( 'pk', 'id', 'name', 'virtual_machine', 'enabled', 'mac_address', 'mtu', 'mode', 'description', 'tags', - 'vrf', 'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'contacts', 'created', - 'last_updated', + 'vrf', 'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'created', 'last_updated', ) default_columns = ('pk', 'name', 'virtual_machine', 'enabled', 'description') From 62d1510c55f4afb59b8fd0a09c38405b71a29899 Mon Sep 17 00:00:00 2001 From: atownson <52260120+atownson@users.noreply.github.com> Date: Thu, 28 Jul 2022 10:23:04 -0500 Subject: [PATCH 306/593] Closes netbox-community#9762 Added nat_outside to IPAddressTable class --- netbox/ipam/tables/ip.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/netbox/ipam/tables/ip.py b/netbox/ipam/tables/ip.py index bec05eeff..087d0de73 100644 --- a/netbox/ipam/tables/ip.py +++ b/netbox/ipam/tables/ip.py @@ -369,6 +369,11 @@ class IPAddressTable(TenancyColumnsMixin, NetBoxTable): orderable=False, verbose_name='NAT (Inside)' ) + nat_outside = tables.Column( + linkify=True, + orderable=False, + verbose_name='NAT (Outside)' + ) assigned = columns.BooleanColumn( accessor='assigned_object_id', linkify=True, @@ -381,7 +386,7 @@ class IPAddressTable(TenancyColumnsMixin, NetBoxTable): class Meta(NetBoxTable.Meta): model = IPAddress fields = ( - 'pk', 'id', 'address', 'vrf', 'status', 'role', 'tenant', 'tenant_group', 'nat_inside', 'assigned', 'dns_name', 'description', + 'pk', 'id', 'address', 'vrf', 'status', 'role', 'tenant', 'tenant_group', 'nat_inside', 'nat_outside', 'assigned', 'dns_name', 'description', 'tags', 'created', 'last_updated', ) default_columns = ( From 07620db027823fe55e49155f6adb4d2c80cace76 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Thu, 28 Jul 2022 12:45:27 -0400 Subject: [PATCH 307/593] Changelog for #9762 --- docs/release-notes/version-3.2.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes/version-3.2.md b/docs/release-notes/version-3.2.md index 14d73d1eb..c0c99bb73 100644 --- a/docs/release-notes/version-3.2.md +++ b/docs/release-notes/version-3.2.md @@ -4,6 +4,7 @@ ### Enhancements +* [#9762](https://github.com/netbox-community/netbox/issues/9762) - Add `nat_outside` column to the IPAddress table * [#9825](https://github.com/netbox-community/netbox/issues/9825) - Add contacts column to virtual machines table --- From c5fb7b72f0eb2b1e3f53b3397090b0470398f725 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Thu, 28 Jul 2022 14:36:20 -0400 Subject: [PATCH 308/593] Closes #9391: Remove 500-character limit for custom link text & URL fields --- docs/release-notes/version-3.3.md | 1 + .../0077_customlink_extend_text_and_url.py | 21 +++++++++++++++++++ netbox/extras/models/models.py | 6 ++---- 3 files changed, 24 insertions(+), 4 deletions(-) create mode 100644 netbox/extras/migrations/0077_customlink_extend_text_and_url.py diff --git a/docs/release-notes/version-3.3.md b/docs/release-notes/version-3.3.md index 87cb00730..211f264f0 100644 --- a/docs/release-notes/version-3.3.md +++ b/docs/release-notes/version-3.3.md @@ -93,6 +93,7 @@ Custom field UI visibility has no impact on API operation. * [#8995](https://github.com/netbox-community/netbox/issues/8995) - Enable arbitrary ordering of REST API results * [#9070](https://github.com/netbox-community/netbox/issues/9070) - Hide navigation menu items based on user permissions * [#9177](https://github.com/netbox-community/netbox/issues/9177) - Add tenant assignment for wireless LANs & links +* [#9391](https://github.com/netbox-community/netbox/issues/9391) - Remove 500-character limit for custom link text & URL fields * [#9536](https://github.com/netbox-community/netbox/issues/9536) - Track API token usage times * [#9582](https://github.com/netbox-community/netbox/issues/9582) - Enable assigning config contexts based on device location diff --git a/netbox/extras/migrations/0077_customlink_extend_text_and_url.py b/netbox/extras/migrations/0077_customlink_extend_text_and_url.py new file mode 100644 index 000000000..c08948aa6 --- /dev/null +++ b/netbox/extras/migrations/0077_customlink_extend_text_and_url.py @@ -0,0 +1,21 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('extras', '0076_tag_slug_unicode'), + ] + + operations = [ + migrations.AlterField( + model_name='customlink', + name='link_text', + field=models.TextField(), + ), + migrations.AlterField( + model_name='customlink', + name='link_url', + field=models.TextField(), + ), + ] diff --git a/netbox/extras/models/models.py b/netbox/extras/models/models.py index e614a1258..4873a1f9e 100644 --- a/netbox/extras/models/models.py +++ b/netbox/extras/models/models.py @@ -204,12 +204,10 @@ class CustomLink(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel): enabled = models.BooleanField( default=True ) - link_text = models.CharField( - max_length=500, + link_text = models.TextField( help_text="Jinja2 template code for link text" ) - link_url = models.CharField( - max_length=500, + link_url = models.TextField( verbose_name='Link URL', help_text="Jinja2 template code for link URL" ) From 2c43c8d077c34402720b86658674c46371da321d Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Thu, 28 Jul 2022 15:03:24 -0400 Subject: [PATCH 309/593] Closes #9793: Add PoE attributes to interface templates --- docs/models/dcim/interfacetemplate.md | 2 +- docs/release-notes/version-3.3.md | 4 +++- netbox/dcim/api/serializers.py | 12 +++++++++++- netbox/dcim/filtersets.py | 6 ++++++ netbox/dcim/forms/bulk_edit.py | 16 +++++++++++++++- netbox/dcim/forms/filtersets.py | 6 ++++-- netbox/dcim/forms/models.py | 4 +++- netbox/dcim/forms/object_import.py | 14 ++++++++++++-- netbox/dcim/graphql/types.py | 6 ++++++ .../migrations/0155_interface_poe_mode_type.py | 10 ++++++++++ netbox/dcim/models/device_component_templates.py | 16 +++++++++++++++- netbox/dcim/models/devices.py | 2 ++ netbox/dcim/tables/devicetypes.py | 2 +- netbox/dcim/tests/test_filtersets.py | 12 ++++++++++-- 14 files changed, 99 insertions(+), 13 deletions(-) diff --git a/docs/models/dcim/interfacetemplate.md b/docs/models/dcim/interfacetemplate.md index d9b30dd87..e11abcce4 100644 --- a/docs/models/dcim/interfacetemplate.md +++ b/docs/models/dcim/interfacetemplate.md @@ -1,3 +1,3 @@ ## Interface Templates -A template for a network interface that will be created on all instantiations of the parent device type. Each interface may be assigned a physical or virtual type, and may be designated as "management-only." +A template for a network interface that will be created on all instantiations of the parent device type. Each interface may be assigned a physical or virtual type, and may be designated as "management-only." Power over Ethernet (PoE) mode and type may also be assigned to interface templates. diff --git a/docs/release-notes/version-3.3.md b/docs/release-notes/version-3.3.md index 211f264f0..68cff0547 100644 --- a/docs/release-notes/version-3.3.md +++ b/docs/release-notes/version-3.3.md @@ -97,7 +97,7 @@ Custom field UI visibility has no impact on API operation. * [#9536](https://github.com/netbox-community/netbox/issues/9536) - Track API token usage times * [#9582](https://github.com/netbox-community/netbox/issues/9582) - Enable assigning config contexts based on device location -### Bug Fixes +### Bug Fixes (from Beta1) * [#9728](https://github.com/netbox-community/netbox/issues/9728) - Fix validation when assigning a virtual machine to a device * [#9729](https://github.com/netbox-community/netbox/issues/9729) - Fix ordering of content type creation to ensure compatability with demo data @@ -177,6 +177,8 @@ Custom field UI visibility has no impact on API operation. * `connected_endpoint_reachable` has been renamed to `connected_endpoints_reachable` * Added the optional `poe_mode` and `poe_type` fields * Added the `l2vpn_termination` read-only field +* dcim.InterfaceTemplate + * Added the optional `poe_mode` and `poe_type` fields * dcim.Location * Added required `status` field (default value: `active`) * dcim.PowerOutlet diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 5f30b7385..249a3f167 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -469,12 +469,22 @@ class InterfaceTemplateSerializer(ValidatedModelSerializer): default=None ) type = ChoiceField(choices=InterfaceTypeChoices) + poe_mode = ChoiceField( + choices=InterfacePoEModeChoices, + required=False, + allow_blank=True + ) + poe_type = ChoiceField( + choices=InterfacePoETypeChoices, + required=False, + allow_blank=True + ) class Meta: model = InterfaceTemplate fields = [ 'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'mgmt_only', 'description', - 'created', 'last_updated', + 'poe_mode', 'poe_type', 'created', 'last_updated', ] diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py index 4bdc525a5..874d08ba5 100644 --- a/netbox/dcim/filtersets.py +++ b/netbox/dcim/filtersets.py @@ -652,6 +652,12 @@ class InterfaceTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeCo choices=InterfaceTypeChoices, null_value=None ) + poe_mode = django_filters.MultipleChoiceFilter( + choices=InterfacePoEModeChoices + ) + poe_type = django_filters.MultipleChoiceFilter( + choices=InterfacePoETypeChoices + ) class Meta: model = InterfaceTemplate diff --git a/netbox/dcim/forms/bulk_edit.py b/netbox/dcim/forms/bulk_edit.py index 6d51302d3..8f765ae9b 100644 --- a/netbox/dcim/forms/bulk_edit.py +++ b/netbox/dcim/forms/bulk_edit.py @@ -818,8 +818,22 @@ class InterfaceTemplateBulkEditForm(BulkEditForm): description = forms.CharField( required=False ) + poe_mode = forms.ChoiceField( + choices=add_blank_choice(InterfacePoEModeChoices), + required=False, + initial='', + widget=StaticSelect(), + label='PoE mode' + ) + poe_type = forms.ChoiceField( + choices=add_blank_choice(InterfacePoETypeChoices), + required=False, + initial='', + widget=StaticSelect(), + label='PoE type' + ) - nullable_fields = ('label', 'description') + nullable_fields = ('label', 'description', 'poe_mode', 'poe_type') class FrontPortTemplateBulkEditForm(BulkEditForm): diff --git a/netbox/dcim/forms/filtersets.py b/netbox/dcim/forms/filtersets.py index 8d2e24c9c..c5474a2b1 100644 --- a/netbox/dcim/forms/filtersets.py +++ b/netbox/dcim/forms/filtersets.py @@ -1027,11 +1027,13 @@ class InterfaceFilterForm(DeviceComponentFilterForm): ) poe_mode = MultipleChoiceField( choices=InterfacePoEModeChoices, - required=False + required=False, + label='PoE mode' ) poe_type = MultipleChoiceField( choices=InterfacePoEModeChoices, - required=False + required=False, + label='PoE type' ) rf_role = MultipleChoiceField( choices=WirelessRoleChoices, diff --git a/netbox/dcim/forms/models.py b/netbox/dcim/forms/models.py index aa573a4df..f3ab6f3a9 100644 --- a/netbox/dcim/forms/models.py +++ b/netbox/dcim/forms/models.py @@ -1052,12 +1052,14 @@ class InterfaceTemplateForm(BootstrapMixin, forms.ModelForm): class Meta: model = InterfaceTemplate fields = [ - 'device_type', 'module_type', 'name', 'label', 'type', 'mgmt_only', 'description', + 'device_type', 'module_type', 'name', 'label', 'type', 'mgmt_only', 'description', 'poe_mode', 'poe_type', ] widgets = { 'device_type': forms.HiddenInput(), 'module_type': forms.HiddenInput(), 'type': StaticSelect(), + 'poe_mode': StaticSelect(), + 'poe_type': StaticSelect(), } diff --git a/netbox/dcim/forms/object_import.py b/netbox/dcim/forms/object_import.py index afbcd6543..a51f48c5b 100644 --- a/netbox/dcim/forms/object_import.py +++ b/netbox/dcim/forms/object_import.py @@ -1,6 +1,6 @@ from django import forms -from dcim.choices import InterfaceTypeChoices, PortTypeChoices +from dcim.choices import InterfacePoEModeChoices, InterfacePoETypeChoices, InterfaceTypeChoices, PortTypeChoices from dcim.models import * from utilities.forms import BootstrapMixin @@ -112,11 +112,21 @@ class InterfaceTemplateImportForm(ComponentTemplateImportForm): type = forms.ChoiceField( choices=InterfaceTypeChoices.CHOICES ) + poe_mode = forms.ChoiceField( + choices=InterfacePoEModeChoices, + required=False, + label='PoE mode' + ) + poe_type = forms.ChoiceField( + choices=InterfacePoETypeChoices, + required=False, + label='PoE type' + ) class Meta: model = InterfaceTemplate fields = [ - 'device_type', 'module_type', 'name', 'label', 'type', 'mgmt_only', 'description', + 'device_type', 'module_type', 'name', 'label', 'type', 'mgmt_only', 'description', 'poe_mode', 'poe_type', ] diff --git a/netbox/dcim/graphql/types.py b/netbox/dcim/graphql/types.py index a43b293a4..52a98278a 100644 --- a/netbox/dcim/graphql/types.py +++ b/netbox/dcim/graphql/types.py @@ -258,6 +258,12 @@ class InterfaceTemplateType(ComponentTemplateObjectType): fields = '__all__' filterset_class = filtersets.InterfaceTemplateFilterSet + def resolve_poe_mode(self, info): + return self.poe_mode or None + + def resolve_poe_type(self, info): + return self.poe_type or None + class InventoryItemType(ComponentObjectType): diff --git a/netbox/dcim/migrations/0155_interface_poe_mode_type.py b/netbox/dcim/migrations/0155_interface_poe_mode_type.py index 0615d5d7e..13f2ddfc0 100644 --- a/netbox/dcim/migrations/0155_interface_poe_mode_type.py +++ b/netbox/dcim/migrations/0155_interface_poe_mode_type.py @@ -20,4 +20,14 @@ class Migration(migrations.Migration): name='poe_type', field=models.CharField(blank=True, max_length=50), ), + migrations.AddField( + model_name='interfacetemplate', + name='poe_mode', + field=models.CharField(blank=True, max_length=50), + ), + migrations.AddField( + model_name='interfacetemplate', + name='poe_type', + field=models.CharField(blank=True, max_length=50), + ), ] diff --git a/netbox/dcim/models/device_component_templates.py b/netbox/dcim/models/device_component_templates.py index 92658d310..4a66bc457 100644 --- a/netbox/dcim/models/device_component_templates.py +++ b/netbox/dcim/models/device_component_templates.py @@ -1,6 +1,6 @@ from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType -from django.core.exceptions import ObjectDoesNotExist, ValidationError +from django.core.exceptions import ValidationError from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models from mptt.models import MPTTModel, TreeForeignKey @@ -318,6 +318,18 @@ class InterfaceTemplate(ModularComponentTemplateModel): default=False, verbose_name='Management only' ) + poe_mode = models.CharField( + max_length=50, + choices=InterfacePoEModeChoices, + blank=True, + verbose_name='PoE mode' + ) + poe_type = models.CharField( + max_length=50, + choices=InterfacePoETypeChoices, + blank=True, + verbose_name='PoE type' + ) component_model = Interface @@ -334,6 +346,8 @@ class InterfaceTemplate(ModularComponentTemplateModel): label=self.resolve_label(kwargs.get('module')), type=self.type, mgmt_only=self.mgmt_only, + poe_mode=self.poe_mode, + poe_type=self.poe_type, **kwargs ) diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py index 6075cb5a0..f8a28eb58 100644 --- a/netbox/dcim/models/devices.py +++ b/netbox/dcim/models/devices.py @@ -229,6 +229,8 @@ class DeviceType(NetBoxModel): 'mgmt_only': c.mgmt_only, 'label': c.label, 'description': c.description, + 'poe_mode': c.poe_mode, + 'poe_type': c.poe_type, } for c in self.interfacetemplates.all() ] diff --git a/netbox/dcim/tables/devicetypes.py b/netbox/dcim/tables/devicetypes.py index 2da9daee7..3ed4d8c08 100644 --- a/netbox/dcim/tables/devicetypes.py +++ b/netbox/dcim/tables/devicetypes.py @@ -172,7 +172,7 @@ class InterfaceTemplateTable(ComponentTemplateTable): class Meta(ComponentTemplateTable.Meta): model = InterfaceTemplate - fields = ('pk', 'name', 'label', 'mgmt_only', 'type', 'description', 'actions') + fields = ('pk', 'name', 'label', 'mgmt_only', 'type', 'description', 'poe_mode', 'poe_type', 'actions') empty_text = "None" diff --git a/netbox/dcim/tests/test_filtersets.py b/netbox/dcim/tests/test_filtersets.py index 21daa32c1..fbc0addb8 100644 --- a/netbox/dcim/tests/test_filtersets.py +++ b/netbox/dcim/tests/test_filtersets.py @@ -1089,8 +1089,8 @@ class InterfaceTemplateTestCase(TestCase, ChangeLoggedFilterSetTests): DeviceType.objects.bulk_create(device_types) InterfaceTemplate.objects.bulk_create(( - InterfaceTemplate(device_type=device_types[0], name='Interface 1', type=InterfaceTypeChoices.TYPE_1GE_FIXED, mgmt_only=True), - InterfaceTemplate(device_type=device_types[1], name='Interface 2', type=InterfaceTypeChoices.TYPE_1GE_GBIC, mgmt_only=False), + InterfaceTemplate(device_type=device_types[0], name='Interface 1', type=InterfaceTypeChoices.TYPE_1GE_FIXED, mgmt_only=True, poe_mode=InterfacePoEModeChoices.MODE_PD, poe_type=InterfacePoETypeChoices.TYPE_1_8023AF), + InterfaceTemplate(device_type=device_types[1], name='Interface 2', type=InterfaceTypeChoices.TYPE_1GE_GBIC, mgmt_only=False, poe_mode=InterfacePoEModeChoices.MODE_PSE, poe_type=InterfacePoETypeChoices.TYPE_2_8023AT), InterfaceTemplate(device_type=device_types[2], name='Interface 3', type=InterfaceTypeChoices.TYPE_1GE_SFP, mgmt_only=False), )) @@ -1113,6 +1113,14 @@ class InterfaceTemplateTestCase(TestCase, ChangeLoggedFilterSetTests): params = {'mgmt_only': 'false'} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_poe_mode(self): + params = {'poe_mode': [InterfacePoEModeChoices.MODE_PD, InterfacePoEModeChoices.MODE_PSE]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_poe_type(self): + params = {'poe_type': [InterfacePoETypeChoices.TYPE_1_8023AF, InterfacePoETypeChoices.TYPE_2_8023AT]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + class FrontPortTemplateTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = FrontPortTemplate.objects.all() From 04fb0bd51c5a3fd0d13fddea155de311b0f4edd9 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Thu, 28 Jul 2022 15:41:10 -0400 Subject: [PATCH 310/593] Closes #9858: ChangeLoggedModelFilterSet cleanup --- netbox/netbox/filtersets.py | 23 +++++------------------ netbox/utilities/testing/filtersets.py | 4 ++-- 2 files changed, 7 insertions(+), 20 deletions(-) diff --git a/netbox/netbox/filtersets.py b/netbox/netbox/filtersets.py index f509afa5b..3a0434592 100644 --- a/netbox/netbox/filtersets.py +++ b/netbox/netbox/filtersets.py @@ -197,24 +197,11 @@ class BaseFilterSet(django_filters.FilterSet): class ChangeLoggedModelFilterSet(BaseFilterSet): - created = django_filters.DateTimeFilter() - created__gte = django_filters.DateTimeFilter( - field_name='created', - lookup_expr='gte' - ) - created__lte = django_filters.DateTimeFilter( - field_name='created', - lookup_expr='lte' - ) - last_updated = django_filters.DateTimeFilter() - last_updated__gte = django_filters.DateTimeFilter( - field_name='last_updated', - lookup_expr='gte' - ) - last_updated__lte = django_filters.DateTimeFilter( - field_name='last_updated', - lookup_expr='lte' - ) + """ + Base FilterSet for ChangeLoggedModel classes. + """ + created = filters.MultiValueDateTimeFilter() + last_updated = filters.MultiValueDateTimeFilter() class NetBoxModelFilterSet(ChangeLoggedModelFilterSet): diff --git a/netbox/utilities/testing/filtersets.py b/netbox/utilities/testing/filtersets.py index 9c90f5530..00f3d9745 100644 --- a/netbox/utilities/testing/filtersets.py +++ b/netbox/utilities/testing/filtersets.py @@ -25,11 +25,11 @@ class ChangeLoggedFilterSetTests(BaseFilterSetTests): def test_created(self): pk_list = self.queryset.values_list('pk', flat=True)[:2] self.queryset.filter(pk__in=pk_list).update(created=datetime(2021, 1, 1, 0, 0, 0, tzinfo=timezone.utc)) - params = {'created': '2021-01-01T00:00:00'} + params = {'created': ['2021-01-01T00:00:00']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_last_updated(self): pk_list = self.queryset.values_list('pk', flat=True)[:2] self.queryset.filter(pk__in=pk_list).update(last_updated=datetime(2021, 1, 2, 0, 0, 0, tzinfo=timezone.utc)) - params = {'last_updated': '2021-01-02T00:00:00'} + params = {'last_updated': ['2021-01-02T00:00:00']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) From c582d7459f2bd5db72ba407b360bf381694b1767 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 29 Jul 2022 10:06:26 -0400 Subject: [PATCH 311/593] Rearrange introductory content --- docs/index.md | 81 ++++++++++++++++++-------------------------- docs/introduction.md | 79 ++++++++++++++++++++++++++++++++++++++++++ mkdocs.yml | 2 +- 3 files changed, 113 insertions(+), 49 deletions(-) create mode 100644 docs/introduction.md diff --git a/docs/index.md b/docs/index.md index 81c899387..a6bbcecff 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,63 +1,48 @@ ![NetBox](netbox_logo.svg "NetBox logo"){style="height: 100px; margin-bottom: 3em"} -# What is NetBox? +# The Premiere Network Source of Truth -NetBox is an infrastructure resource modeling (IRM) application designed to empower network automation. Initially conceived by the network engineering team at [DigitalOcean](https://www.digitalocean.com/), NetBox was developed specifically to address the needs of network and infrastructure engineers. NetBox is made available as open source under the Apache 2 license. It encompasses the following aspects of network management: +NetBox is the leading solution for modeling modern networks. By combining the traditional disciplines of IP address management (IPAM) and datacenter infrastructure management (DCIM) with powerful APIs and extensions, NetBox provides the ideal "source of truth" to power network automation. -* **IP address management (IPAM)** - IP networks and addresses, VRFs, and VLANs -* **Equipment racks** - Organized by group and site -* **Devices** - Types of devices and where they are installed -* **Connections** - Network, console, and power connections among devices -* **Virtualization** - Virtual machines and clusters -* **Data circuits** - Long-haul communications circuits and providers +## Built for Networks -## What NetBox Is Not +Unlike general-purpose CMDBs, NetBox has curated a data model which caters specifically to the needs of network engineers and operators. It delivers a wide assortment of object types carefully crafted to best serve the needs of network engineers and operators. These cover all facets of network design, from IP address managements to cabling to overlays and more: -While NetBox strives to cover many areas of network management, the scope of its feature set is necessarily limited. This ensures that development focuses on core functionality and that scope creep is reasonably contained. To that end, it might help to provide some examples of functionality that NetBox **does not** provide: +* Hierarchical regions, site groups, sites, and locations +* Racks, devices, and device components +* Cables and wireless connections +* Power distribution +* Data circuits and providers +* Virtual machines and clusters +* IP prefixes, ranges, and addresses +* VRFs and route targets +* FHRP groups (VRRP, HSRP, etc.) +* AS numbers +* VLANs and scoped VLAN groups +* L2VPN overlays +* Organizational tenants and contacts -* Network monitoring -* DNS server -* RADIUS server -* Configuration management -* Facilities management +## Customizable & Extensible -That said, NetBox _can_ be used to great effect in populating external tools with the data they need to perform these functions. +In addition to its expansive and robust data model, NetBox offers myriad mechanisms through it can be customized and extended. -## Design Philosophy +* Custom fields +* Custom model validation +* Export templates +* Webhooks +* Plugins +* REST & GraphQL APIs -NetBox was designed with the following tenets foremost in mind. +## Always Open -### Replicate the Real World - -Careful consideration has been given to the data model to ensure that it can accurately reflect a real-world network. For instance, IP addresses are assigned not to devices, but to specific interfaces attached to a device, and an interface may have multiple IP addresses assigned to it. - -### Serve as a "Source of Truth" - -NetBox intends to represent the _desired_ state of a network versus its _operational_ state. As such, automated import of live network state is strongly discouraged. All data created in NetBox should first be vetted by a human to ensure its integrity. NetBox can then be used to populate monitoring and provisioning systems with a high degree of confidence. - -### Keep it Simple - -When given a choice between a relatively simple [80% solution](https://en.wikipedia.org/wiki/Pareto_principle) and a much more complex complete solution, the former will typically be favored. This ensures a lean codebase with a low learning curve. - -## Application Stack - -NetBox is built on the [Django](https://djangoproject.com/) Python framework and utilizes a [PostgreSQL](https://www.postgresql.org/) database. It runs as a WSGI service behind your choice of HTTP server. - -| Function | Component | -|--------------------|-------------------| -| HTTP service | nginx or Apache | -| WSGI service | gunicorn or uWSGI | -| Application | Django/Python | -| Database | PostgreSQL 10+ | -| Task queuing | Redis/django-rq | -| Live device access | NAPALM (optional) | - -## Supported Python Versions - -NetBox supports Python 3.8, 3.9, and 3.10 environments. +Because NetBox is an open source application licensed under [Apache 2](https://www.apache.org/licenses/LICENSE-2.0.html), its entire code base is completely accessible to the end user, and there's never a risk of vendor lock-out. Additionally, NetBox development is an entirely public, community-driven process to which everyone can provide input. ## Getting Started -Minor NetBox releases (e.g. v3.1) are published three times a year; in April, August, and December. These typically introduce major new features and may contain breaking API changes. Patch releases are published roughly every one to two weeks to resolve bugs and fulfill minor feature requests. These are backward-compatible with previous releases unless otherwise noted. The NetBox maintainers strongly recommend running the latest stable release whenever possible. +* Public Demo +* Installation Guide +* Docker install +* NetBox Cloud -Please see the [official installation guide](installation/index.md) for detailed instructions on obtaining and installing NetBox. +!!! tip "NetBox Development" + Interested in contributing to NetBox? Check out our [GitHub repository](https://github.com/netbox-community/netbox) to get started! diff --git a/docs/introduction.md b/docs/introduction.md new file mode 100644 index 000000000..cffcb37dd --- /dev/null +++ b/docs/introduction.md @@ -0,0 +1,79 @@ +# Introduction to NetBox + +## Origin Story + +NetBox was originally developed by its lead maintainer, [Jeremy Stretch](https://github.com/jeremystretch), while he was working as a network engineer at [DigitalOcean](https://www.digitalocean.com/) in 2015 as part of an effort to automate their network provisioning. Recognizing the new tool's potential, DigitalOcean agreed to release it as an open source project in June 2016. + +Since then, thousands of organizations around the world have embraced NetBox as their central network source of truth to empower both network operators and automation. + +## Key Features + +NetBox was built specifically to serve the needs of network engineers and operators. Below is a very brief overview of the core features it provides. + +* IP address management (IPAM) with full IPv4/IPv6 parity +* Automatic provisioning of next available prefix/IP +* VRFs with import & export route targets +* VLANs with variably-scoped groups +* AS number (ASN) management +* Rack elevations with SVG rendering +* Device modeling using pre-defined types +* Network, power, and console cabling with SVG traces +* Power distribution modeling +* Data circuit and provider tracking +* Wireless LAN and point-to-point links +* L2 VPN overlays +* FHRP groups (VRRP, HSRP, etc.) +* Application service bindings +* Virtual machines & clusters +* Flexible hierarchy for sites and locations +* Tenant ownership assignment +* Device & VM configuration contexts for advanced configuration rendering +* Custom fields for data model extension +* Support for custom validation rules +* Custom reports & scripts executable directly within the UI +* Extensive plugin framework for adding custom functionality +* Single sign-on (SSO) authentication +* Robust object-based permissions +* Detailed, automatic change logging +* NAPALM integration + +## What NetBox Is Not + +While NetBox strives to cover many areas of network management, the scope of its feature set is necessarily limited. This ensures that development focuses on core functionality and that scope creep is reasonably contained. To that end, it might help to provide some examples of functionality that NetBox **does not** provide: + +* Network monitoring +* DNS server +* RADIUS server +* Configuration management +* Facilities management + +That said, NetBox _can_ be used to great effect in populating external tools with the data they need to perform these functions. + +## Design Philosophy + +NetBox was designed with the following tenets foremost in mind. + +### Replicate the Real World + +Careful consideration has been given to the data model to ensure that it can accurately reflect a real-world network. For instance, IP addresses are assigned not to devices, but to specific interfaces attached to a device, and an interface may have multiple IP addresses assigned to it. + +### Serve as a "Source of Truth" + +NetBox intends to represent the _desired_ state of a network versus its _operational_ state. As such, automated import of live network state is strongly discouraged. All data created in NetBox should first be vetted by a human to ensure its integrity. NetBox can then be used to populate monitoring and provisioning systems with a high degree of confidence. + +### Keep it Simple + +When given a choice between a relatively simple [80% solution](https://en.wikipedia.org/wiki/Pareto_principle) and a much more complex complete solution, the former will typically be favored. This ensures a lean codebase with a low learning curve. + +## Application Stack + +NetBox is built on the [Django](https://djangoproject.com/) Python framework and utilizes a [PostgreSQL](https://www.postgresql.org/) database. It runs as a WSGI service behind your choice of HTTP server. + +| Function | Component | +|--------------------|-------------------| +| HTTP service | nginx or Apache | +| WSGI service | gunicorn or uWSGI | +| Application | Django/Python | +| Database | PostgreSQL 10+ | +| Task queuing | Redis/django-rq | +| Live device access | NAPALM (optional) | diff --git a/mkdocs.yml b/mkdocs.yml index 34c65ed01..992859d66 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -57,7 +57,7 @@ markdown_extensions: - pymdownx.tabbed: alternate_style: true nav: - - Introduction: 'index.md' + - Introduction: 'introduction.md' - Installation: - Installing NetBox: 'installation/index.md' - 1. PostgreSQL: 'installation/1-postgresql.md' From 18acac18e0301ad0e3355f9742177062aad403c3 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 29 Jul 2022 10:30:47 -0400 Subject: [PATCH 312/593] Move data model definitions to separate hierarchy --- docs/core-functionality/circuits.md | 10 -- docs/core-functionality/contacts.md | 5 - docs/core-functionality/device-types.md | 41 --------- docs/core-functionality/devices.md | 40 -------- docs/core-functionality/ipam.md | 33 ------- docs/core-functionality/modules.md | 4 - docs/core-functionality/power.md | 8 -- docs/core-functionality/services.md | 4 - docs/core-functionality/sites-and-racks.md | 12 --- docs/core-functionality/tenancy.md | 4 - docs/core-functionality/virtualization.md | 10 -- docs/core-functionality/vlans.md | 4 - docs/core-functionality/wireless.md | 8 -- docs/installation/migrating-to-systemd.md | 55 ----------- mkdocs.yml | 101 +++++++++++++++++---- 15 files changed, 85 insertions(+), 254 deletions(-) delete mode 100644 docs/core-functionality/circuits.md delete mode 100644 docs/core-functionality/contacts.md delete mode 100644 docs/core-functionality/device-types.md delete mode 100644 docs/core-functionality/devices.md delete mode 100644 docs/core-functionality/ipam.md delete mode 100644 docs/core-functionality/modules.md delete mode 100644 docs/core-functionality/power.md delete mode 100644 docs/core-functionality/services.md delete mode 100644 docs/core-functionality/sites-and-racks.md delete mode 100644 docs/core-functionality/tenancy.md delete mode 100644 docs/core-functionality/virtualization.md delete mode 100644 docs/core-functionality/vlans.md delete mode 100644 docs/core-functionality/wireless.md delete mode 100644 docs/installation/migrating-to-systemd.md diff --git a/docs/core-functionality/circuits.md b/docs/core-functionality/circuits.md deleted file mode 100644 index b1b02e300..000000000 --- a/docs/core-functionality/circuits.md +++ /dev/null @@ -1,10 +0,0 @@ -# Circuits - -{!models/circuits/provider.md!} -{!models/circuits/providernetwork.md!} - ---- - -{!models/circuits/circuit.md!} -{!models/circuits/circuittype.md!} -{!models/circuits/circuittermination.md!} diff --git a/docs/core-functionality/contacts.md b/docs/core-functionality/contacts.md deleted file mode 100644 index 76a005fc0..000000000 --- a/docs/core-functionality/contacts.md +++ /dev/null @@ -1,5 +0,0 @@ -# Contacts - -{!models/tenancy/contact.md!} -{!models/tenancy/contactgroup.md!} -{!models/tenancy/contactrole.md!} diff --git a/docs/core-functionality/device-types.md b/docs/core-functionality/device-types.md deleted file mode 100644 index ec5cbacdb..000000000 --- a/docs/core-functionality/device-types.md +++ /dev/null @@ -1,41 +0,0 @@ -# Device Types - -{!models/dcim/devicetype.md!} -{!models/dcim/manufacturer.md!} - ---- - -## Device Component Templates - -Each device type is assigned a number of component templates which define the physical components within a device. These are: - -* Console ports -* Console server ports -* Power ports -* Power outlets -* Network interfaces -* Front ports -* Rear ports -* Device bays (which house child devices) - -Whenever a new device is created, its components are automatically created per the templates assigned to its device type. For example, a Juniper EX4300-48T device type might have the following component templates defined: - -* One template for a console port ("Console") -* Two templates for power ports ("PSU0" and "PSU1") -* 48 templates for 1GE interfaces ("ge-0/0/0" through "ge-0/0/47") -* Four templates for 10GE interfaces ("xe-0/2/0" through "xe-0/2/3") - -Once component templates have been created, every new device that you create as an instance of this type will automatically be assigned each of the components listed above. - -!!! note - Assignment of components from templates occurs only at the time of device creation. If you modify the templates of a device type, it will not affect devices which have already been created. However, you always have the option of adding, modifying, or deleting components on existing devices. - -{!models/dcim/consoleporttemplate.md!} -{!models/dcim/consoleserverporttemplate.md!} -{!models/dcim/powerporttemplate.md!} -{!models/dcim/poweroutlettemplate.md!} -{!models/dcim/interfacetemplate.md!} -{!models/dcim/frontporttemplate.md!} -{!models/dcim/rearporttemplate.md!} -{!models/dcim/modulebaytemplate.md!} -{!models/dcim/devicebaytemplate.md!} diff --git a/docs/core-functionality/devices.md b/docs/core-functionality/devices.md deleted file mode 100644 index 35c978210..000000000 --- a/docs/core-functionality/devices.md +++ /dev/null @@ -1,40 +0,0 @@ -# Devices and Cabling - -{!models/dcim/device.md!} -{!models/dcim/devicerole.md!} -{!models/dcim/platform.md!} - ---- - -## Device Components - -Device components represent discrete objects within a device which are used to terminate cables, house child devices, or track resources. - -{!models/dcim/consoleport.md!} -{!models/dcim/consoleserverport.md!} -{!models/dcim/powerport.md!} -{!models/dcim/poweroutlet.md!} -{!models/dcim/interface.md!} -{!models/dcim/frontport.md!} -{!models/dcim/rearport.md!} -{!models/dcim/modulebay.md!} -{!models/dcim/devicebay.md!} -{!models/dcim/inventoryitem.md!} - ---- - -{!models/dcim/virtualchassis.md!} - ---- - -{!models/dcim/cable.md!} - -In the example below, three individual cables comprise a path between devices A and D: - -![Cable path](../media/models/dcim_cable_trace.png) - -Traced from Interface 1 on Device A, NetBox will show the following path: - -* Cable 1: Interface 1 to Front Port 1 -* Cable 2: Rear Port 1 to Rear Port 2 -* Cable 3: Front Port 2 to Interface 2 diff --git a/docs/core-functionality/ipam.md b/docs/core-functionality/ipam.md deleted file mode 100644 index c86819380..000000000 --- a/docs/core-functionality/ipam.md +++ /dev/null @@ -1,33 +0,0 @@ -# IP Address Management - -{!models/ipam/aggregate.md!} -{!models/ipam/rir.md!} - ---- - -{!models/ipam/prefix.md!} -{!models/ipam/role.md!} - ---- - -{!models/ipam/iprange.md!} -{!models/ipam/ipaddress.md!} - ---- - -{!models/ipam/vrf.md!} -{!models/ipam/routetarget.md!} - ---- - -{!models/ipam/fhrpgroup.md!} -{!models/ipam/fhrpgroupassignment.md!} - ---- - -{!models/ipam/asn.md!} - ---- - -{!models/ipam/l2vpn.md!} -{!models/ipam/l2vpntermination.md!} diff --git a/docs/core-functionality/modules.md b/docs/core-functionality/modules.md deleted file mode 100644 index 4d32fe18c..000000000 --- a/docs/core-functionality/modules.md +++ /dev/null @@ -1,4 +0,0 @@ -# Modules - -{!models/dcim/moduletype.md!} -{!models/dcim/module.md!} diff --git a/docs/core-functionality/power.md b/docs/core-functionality/power.md deleted file mode 100644 index 4d7d5f0ab..000000000 --- a/docs/core-functionality/power.md +++ /dev/null @@ -1,8 +0,0 @@ -# Power Tracking - -{!models/dcim/powerpanel.md!} -{!models/dcim/powerfeed.md!} - -# Example Power Topology - -![Power distribution model](../media/power_distribution.png) diff --git a/docs/core-functionality/services.md b/docs/core-functionality/services.md deleted file mode 100644 index 316c7fe00..000000000 --- a/docs/core-functionality/services.md +++ /dev/null @@ -1,4 +0,0 @@ -# Service Mapping - -{!models/ipam/servicetemplate.md!} -{!models/ipam/service.md!} diff --git a/docs/core-functionality/sites-and-racks.md b/docs/core-functionality/sites-and-racks.md deleted file mode 100644 index c78f2120a..000000000 --- a/docs/core-functionality/sites-and-racks.md +++ /dev/null @@ -1,12 +0,0 @@ -# Sites and Racks - -{!models/dcim/region.md!} -{!models/dcim/sitegroup.md!} -{!models/dcim/site.md!} -{!models/dcim/location.md!} - ---- - -{!models/dcim/rack.md!} -{!models/dcim/rackrole.md!} -{!models/dcim/rackreservation.md!} diff --git a/docs/core-functionality/tenancy.md b/docs/core-functionality/tenancy.md deleted file mode 100644 index fbe1ea8b9..000000000 --- a/docs/core-functionality/tenancy.md +++ /dev/null @@ -1,4 +0,0 @@ -# Tenancy Assignment - -{!models/tenancy/tenant.md!} -{!models/tenancy/tenantgroup.md!} diff --git a/docs/core-functionality/virtualization.md b/docs/core-functionality/virtualization.md deleted file mode 100644 index 220030ab2..000000000 --- a/docs/core-functionality/virtualization.md +++ /dev/null @@ -1,10 +0,0 @@ -# Virtualization - -{!models/virtualization/cluster.md!} -{!models/virtualization/clustertype.md!} -{!models/virtualization/clustergroup.md!} - ---- - -{!models/virtualization/virtualmachine.md!} -{!models/virtualization/vminterface.md!} diff --git a/docs/core-functionality/vlans.md b/docs/core-functionality/vlans.md deleted file mode 100644 index d69128765..000000000 --- a/docs/core-functionality/vlans.md +++ /dev/null @@ -1,4 +0,0 @@ -# VLAN Management - -{!models/ipam/vlan.md!} -{!models/ipam/vlangroup.md!} diff --git a/docs/core-functionality/wireless.md b/docs/core-functionality/wireless.md deleted file mode 100644 index 57133f756..000000000 --- a/docs/core-functionality/wireless.md +++ /dev/null @@ -1,8 +0,0 @@ -# Wireless Networks - -{!models/wireless/wirelesslan.md!} -{!models/wireless/wirelesslangroup.md!} - ---- - -{!models/wireless/wirelesslink.md!} diff --git a/docs/installation/migrating-to-systemd.md b/docs/installation/migrating-to-systemd.md deleted file mode 100644 index a71b748fd..000000000 --- a/docs/installation/migrating-to-systemd.md +++ /dev/null @@ -1,55 +0,0 @@ -# Migrating to systemd - -This document contains instructions for migrating from a legacy NetBox deployment using [supervisor](http://supervisord.org/) to a systemd-based approach. - -## Ubuntu - -### Uninstall supervisord - -```no-highlight -# apt-get remove -y supervisor -``` - -### Configure systemd - -!!! note - These instructions assume the presence of a Python virtual environment at `/opt/netbox/venv`. If you have not created this environment, please refer to the [installation instructions](3-netbox.md#set-up-python-environment) for direction. - -We'll use systemd to control the daemonization of NetBox services. First, copy `contrib/netbox.service` and `contrib/netbox-rq.service` to the `/etc/systemd/system/` directory: - -```no-highlight -# cp contrib/*.service /etc/systemd/system/ -``` - -!!! note - You may need to modify the user that the systemd service runs as. Please verify the user for httpd on your specific release and edit both files to match your httpd service under user and group. The username could be "nobody", "nginx", "apache", "www-data", or something else. - -Then, start the `netbox` and `netbox-rq` services and enable them to initiate at boot time: - -```no-highlight -# systemctl daemon-reload -# systemctl start netbox netbox-rq -# systemctl enable netbox netbox-rq -``` - -You can use the command `systemctl status netbox` to verify that the WSGI service is running: - -``` -# systemctl status netbox.service -● netbox.service - NetBox WSGI Service - Loaded: loaded (/etc/systemd/system/netbox.service; enabled; vendor preset: enabled) - Active: active (running) since Sat 2020-10-24 19:23:40 UTC; 25s ago - Docs: https://docs.netbox.dev/ - Main PID: 11993 (gunicorn) - Tasks: 6 (limit: 2362) - CGroup: /system.slice/netbox.service - ├─11993 /opt/netbox/venv/bin/python3 /opt/netbox/venv/bin/gunicorn --pid /var/tmp/netbox.pid --pythonpath /opt/netbox/... - ├─12015 /opt/netbox/venv/bin/python3 /opt/netbox/venv/bin/gunicorn --pid /var/tmp/netbox.pid --pythonpath /opt/netbox/... - ├─12016 /opt/netbox/venv/bin/python3 /opt/netbox/venv/bin/gunicorn --pid /var/tmp/netbox.pid --pythonpath /opt/netbox/... -... -``` - -At this point, you should be able to connect to the HTTP service at the server name or IP address you provided. If you are unable to connect, check that the nginx service is running and properly configured. If you receive a 502 (bad gateway) error, this indicates that gunicorn is misconfigured or not running. Issue the command `journalctl -xe` to see why the services were unable to start. - -!!! info - Please keep in mind that the configurations provided here are bare minimums required to get NetBox up and running. You may want to make adjustments to better suit your production environment. diff --git a/mkdocs.yml b/mkdocs.yml index 992859d66..6471783e0 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -58,7 +58,7 @@ markdown_extensions: alternate_style: true nav: - Introduction: 'introduction.md' - - Installation: + - Installation & Upgrade: - Installing NetBox: 'installation/index.md' - 1. PostgreSQL: 'installation/1-postgresql.md' - 2. Redis: 'installation/2-redis.md' @@ -67,7 +67,6 @@ nav: - 5. HTTP Server: 'installation/5-http-server.md' - 6. LDAP (Optional): 'installation/6-ldap.md' - Upgrading NetBox: 'installation/upgrading.md' - - Migrating to systemd: 'installation/migrating-to-systemd.md' - Configuration: - Configuring NetBox: 'configuration/index.md' - Required Settings: 'configuration/required-settings.md' @@ -75,20 +74,6 @@ nav: - Dynamic Settings: 'configuration/dynamic-settings.md' - Error Reporting: 'configuration/error-reporting.md' - Remote Authentication: 'configuration/remote-authentication.md' - - Core Functionality: - - IP Address Management: 'core-functionality/ipam.md' - - VLAN Management: 'core-functionality/vlans.md' - - Sites and Racks: 'core-functionality/sites-and-racks.md' - - Devices and Cabling: 'core-functionality/devices.md' - - Device Types: 'core-functionality/device-types.md' - - Modules: 'core-functionality/modules.md' - - Virtualization: 'core-functionality/virtualization.md' - - Service Mapping: 'core-functionality/services.md' - - Circuits: 'core-functionality/circuits.md' - - Wireless: 'core-functionality/wireless.md' - - Power Tracking: 'core-functionality/power.md' - - Tenancy: 'core-functionality/tenancy.md' - - Contacts: 'core-functionality/contacts.md' - Customization: - Custom Fields: 'customization/custom-fields.md' - Custom Validation: 'customization/custom-validation.md' @@ -135,6 +120,90 @@ nav: - Authentication: 'rest-api/authentication.md' - GraphQL API: - Overview: 'graphql-api/overview.md' + - Data Model: + - Circuits: + - Circuit: 'models/circuits/circuit.md' + - Circuit Termination: 'models/circuits/circuittermination.md' + - Circuit Type: 'models/circuits/circuittype.md' + - Provider: 'models/circuits/provider.md' + - Provider Network: 'models/circuits/providernetwork.md' + - DCIM: + - Cable: 'models/dcim/cable.md' + - CablePath: 'models/dcim/cablepath.md' + - CableTermination: 'models/dcim/cabletermination.md' + - ConsolePort: 'models/dcim/consoleport.md' + - ConsolePortTemplate: 'models/dcim/consoleporttemplate.md' + - ConsoleServerPort: 'models/dcim/consoleserverport.md' + - ConsoleServerPortTemplate: 'models/dcim/consoleserverporttemplate.md' + - Device: 'models/dcim/device.md' + - DeviceBay: 'models/dcim/devicebay.md' + - DeviceBayTemplate: 'models/dcim/devicebaytemplate.md' + - DeviceRole: 'models/dcim/devicerole.md' + - DeviceType: 'models/dcim/devicetype.md' + - FrontPort: 'models/dcim/frontport.md' + - FrontPortTemplate: 'models/dcim/frontporttemplate.md' + - Interface: 'models/dcim/interface.md' + - InterfaceTemplate: 'models/dcim/interfacetemplate.md' + - InventoryItem: 'models/dcim/inventoryitem.md' + - InventoryItemRole: 'models/dcim/inventoryitemrole.md' + - InventoryItemTemplate: 'models/dcim/inventoryitemtemplate.md' + - Location: 'models/dcim/location.md' + - Manufacturer: 'models/dcim/manufacturer.md' + - Module: 'models/dcim/module.md' + - ModuleBay: 'models/dcim/modulebay.md' + - ModuleBayTemplate: 'models/dcim/modulebaytemplate.md' + - ModuleType: 'models/dcim/moduletype.md' + - Platform: 'models/dcim/platform.md' + - PowerFeed: 'models/dcim/powerfeed.md' + - PowerOutlet: 'models/dcim/poweroutlet.md' + - PowerOutletTemplate: 'models/dcim/poweroutlettemplate.md' + - PowerPanel: 'models/dcim/powerpanel.md' + - PowerPort: 'models/dcim/powerport.md' + - PowerPortTemplate: 'models/dcim/powerporttemplate.md' + - Rack: 'models/dcim/rack.md' + - RackReservation: 'models/dcim/rackreservation.md' + - RackRole: 'models/dcim/rackrole.md' + - RearPort: 'models/dcim/rearport.md' + - RearPortTemplate: 'models/dcim/rearporttemplate.md' + - Region: 'models/dcim/region.md' + - Site: 'models/dcim/site.md' + - SiteGroup: 'models/dcim/sitegroup.md' + - VirtualChassis: 'models/dcim/virtualchassis.md' + - IPAM: + - ASN: 'models/ipam/asn.md' + - Aggregate: 'models/ipam/aggregate.md' + - FHRPGroup: 'models/ipam/fhrpgroup.md' + - FHRPGroupAssignment: 'models/ipam/fhrpgroupassignment.md' + - IPAddress: 'models/ipam/ipaddress.md' + - IPRange: 'models/ipam/iprange.md' + - L2VPN: 'models/ipam/l2vpn.md' + - L2VPNTermination: 'models/ipam/l2vpntermination.md' + - Prefix: 'models/ipam/prefix.md' + - RIR: 'models/ipam/rir.md' + - Role: 'models/ipam/role.md' + - RouteTarget: 'models/ipam/routetarget.md' + - Service: 'models/ipam/service.md' + - ServiceTemplate: 'models/ipam/servicetemplate.md' + - VLAN: 'models/ipam/vlan.md' + - VLANGroup: 'models/ipam/vlangroup.md' + - VRF: 'models/ipam/vrf.md' + - Tenancy: + - Contact: 'models/tenancy/contact.md' + - ContactAssignment: 'models/tenancy/contactassignment.md' + - ContactGroup: 'models/tenancy/contactgroup.md' + - ContactRole: 'models/tenancy/contactrole.md' + - Tenant: 'models/tenancy/tenant.md' + - TenantGroup: 'models/tenancy/tenantgroup.md' + - Virtualization: + - Cluster: 'models/virtualization/cluster.md' + - ClusterGroup: 'models/virtualization/clustergroup.md' + - ClusterType: 'models/virtualization/clustertype.md' + - VMInterface: 'models/virtualization/vminterface.md' + - VirtualMachine: 'models/virtualization/virtualmachine.md' + - Wireless: + - WirelessLAN: 'models/wireless/wirelesslan.md' + - WirelessLANGroup: 'models/wireless/wirelesslangroup.md' + - WirelessLink: 'models/wireless/wirelesslink.md' - Reference: - Conditions: 'reference/conditions.md' - Markdown: 'reference/markdown.md' From 890efa5400b0325fc32ccbd4946a554a78566447 Mon Sep 17 00:00:00 2001 From: Daniel Sheppard Date: Fri, 29 Jul 2022 11:55:26 -0500 Subject: [PATCH 313/593] Fixes #9062 - Add/edit {module} substitution to help text for component template name --- docs/release-notes/version-3.2.md | 1 + netbox/dcim/forms/object_create.py | 8 ++++++++ netbox/dcim/models/device_component_templates.py | 5 ++++- 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/version-3.2.md b/docs/release-notes/version-3.2.md index c0c99bb73..ce8552b0b 100644 --- a/docs/release-notes/version-3.2.md +++ b/docs/release-notes/version-3.2.md @@ -4,6 +4,7 @@ ### Enhancements +* [#9062](https://github.com/netbox-community/netbox/issues/9062) - Add/edit {module} substitution to help text for component template name * [#9762](https://github.com/netbox-community/netbox/issues/9762) - Add `nat_outside` column to the IPAddress table * [#9825](https://github.com/netbox-community/netbox/issues/9825) - Add contacts column to virtual machines table diff --git a/netbox/dcim/forms/object_create.py b/netbox/dcim/forms/object_create.py index 8c9ddab19..d2c941b34 100644 --- a/netbox/dcim/forms/object_create.py +++ b/netbox/dcim/forms/object_create.py @@ -64,6 +64,14 @@ class ModularComponentTemplateCreateForm(ComponentCreateForm): """ Creation form for component templates that can be assigned to either a DeviceType *or* a ModuleType. """ + name_pattern = ExpandableNameField( + label='Name', + help_text=""" + Alphanumeric ranges are supported for bulk creation. Mixed cases and types within a single range + are not supported. Example: [ge,xe]-0/0/[0-9]. {module} is accepted as a substitution for + the module bay position. + """ + ) device_type = DynamicModelChoiceField( queryset=DeviceType.objects.all(), required=False diff --git a/netbox/dcim/models/device_component_templates.py b/netbox/dcim/models/device_component_templates.py index 92658d310..ac0738b3f 100644 --- a/netbox/dcim/models/device_component_templates.py +++ b/netbox/dcim/models/device_component_templates.py @@ -39,7 +39,10 @@ class ComponentTemplateModel(WebhooksMixin, ChangeLoggedModel): related_name='%(class)ss' ) name = models.CharField( - max_length=64 + max_length=64, + help_text=""" + {module} is accepted as a substitution for the module bay position when attached to a module type. + """ ) _name = NaturalOrderingField( target_field='name', From b1ce8bd222a7b39bcacbe1d66ce5ab69f11dfe8e Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 29 Jul 2022 13:45:59 -0400 Subject: [PATCH 314/593] Started "getting started" guide --- docs/getting-started/planning.md | 87 +++++++++++++++++++++++++ docs/getting-started/populating-data.md | 42 ++++++++++++ mkdocs.yml | 3 + 3 files changed, 132 insertions(+) create mode 100644 docs/getting-started/planning.md create mode 100644 docs/getting-started/populating-data.md diff --git a/docs/getting-started/planning.md b/docs/getting-started/planning.md new file mode 100644 index 000000000..00640ca44 --- /dev/null +++ b/docs/getting-started/planning.md @@ -0,0 +1,87 @@ +# Planning Your Move + +This guide outlines the steps necessary for planning a successful migration to NetBox. Although it is written under the context of a completely new installation, the general approach outlined here works just as well for adding new data to existing NetBox deployments. + +## Identify Current Sources of Truth + +Before beginning to use NetBox for your own data, it's crucial to first understand where your existing sources of truth reside. A "source of truth" is really just any repository of data that is authoritative for a given domain. For example, you may have a spreadsheet which tracks all IP prefixes in use on your network. So long as everyone involved agrees that this spreadsheet is _authoritative_ for the entire network, it is your source of truth for IP prefixes. + +Anything can be a source of truth, provided it meets two conditions: + +1. It is agreed upon by all relevant parties that this source of data is correct. +2. The domain to which it applies is well-defined. + + + +Dedicate some time to take stock of your own sources of truth for your infrastructure. Upon attempting to catalog these, you're very likely to encounter some challenges, such as: + +* **Multiple conflicting sources** for a given domain. For example, there may be multiple versions of a spreadsheet circulating, each of which asserts a conflicting set of data. +* **Sources with no domain defined.** You may encounter that different teams within your organization use different tools for the same purpose, with no normal definition of when either should be used. +* **Inaccessible data formatting.** Some tools are better suited for programmatic usage than others. For example, spreadsheets are generally very easy to parse and export, however free-form notes on wiki or similar application are much more difficult to consume. +* **There is no source of truth.** Sometimes you'll find that a source of truth simply doesn't exist for a domain. For example, when assigning IP addresses, operators may be just using any (presumed) available IP from a subnet without ever recording its usage. + +See if you can identify each domain of infrastructure data for your organization, and the source of truth for each. Once you have these compiled, you'll need to determine what belongs in NetBox. + +## Determine What to Move + +The general rule when determining what data to put into NetBox is this: If there's a model for it, it belongs in NetBox. For instance, NetBox has purpose-built models for racks, devices, cables, IP prefixes, VLANs, and so on. These are very straightforward to use. However, you'll inevitably reach the limits of NetBox's data model and question what additional data might make sense to record in NetBox. For example, you might wonder whether NetBox should function as the source of truth for infrastructure DNS records or DHCP scopes. + +NetBox provides two core mechanisms for extending its data model. The first is custom fields: Most models in NetBox support the addition of custom fields to hold additional data for which a built-in field does not exist. For example, you might wish to add an "inventory ID" field to the device model. The second mechanism is plugins. Users can create their own plugins to introduce entirely new models, views, and API endpoints in NetBox. This can be incredibly powerful, as it enables rapid development and tight integration with core models. + +That said, it doesn't always make sense to migrate a domain of data to NetBox. For example, many organizations opt to use only the IPAM components or only the DCIM components of NetBox, and integrate with other sources of truth for different domains. This is an entirely valid approach (so long as everyone involved agrees which tool is authoritative for each domain). Ultimately, you'll need to weigh the value of having non-native data models in NetBox against the effort required to define and maintain those models. + +Consider also that NetBox is under constant development. Although the current release might not support a particular type of object, there may be plans to add support for it in a future release. (And if there aren't, consider submitting a feature request citing your use case.) + +## Validate Existing Data + +The last step before migrating data to NetBox is the most crucial: **validation**. The GIGO (garbage in, garbage out) principle is in full effect: Your source of truth is only as good as the data it holds. While NetBox has very powerful data validation tools (including support for custom validation rules), ultimately the onus falls to a human operator to assert what is correct and what is not. For example, NetBox can validate the connection of a cable between two interfaces, but it cannot say whether the cable _should_ be there. + +Here are some tips to help ensure you're only importing valid data into NetBox: + +* Ensure you're starting with complete, well-formatted data. JSON or CSV is highly recommended for the best portability. +* Consider defining custom validation rules in NetBox prior to import. (For example, to enforce device naming schemes.) +* Use custom scripts to automatically populate patterned data. (For example, to automatically create a set of standard VLANs for each site.) + +There are several methods available to import data into NetBox, which we'll cover in the next section. + +## Order of Operations + +When starting with a completely empty database, it might not be immediately clear where to begin. Many models in NetBox rely on the advance creation of other types. For example, you cannot create a device type until after you have created its manufacturer. + +Below is the (rough) recommended order in which NetBox objects should be created or imported. While it is not required to follow this exact order, doing so will help ensure the smoothest workflow. + + + +1. Tenant groups +2. Tenants +3. Regions and/or site groups +4. Sites +5. Locations +6. Rack roles +7. Racks +8. Platforms +9. Manufacturers +10. Device types +11. Module types +12. Device roles +13. Devices +14. Providers +15. Provider networks +16. Circuit types +17. Circuits +18. Wireless LAN groups +19. Wireless LANs & links +20. Route targets +21. VRFs +22. RIRs +23. Aggregates +24. IP/VLAN roles +25. Prefixes +26. IP ranges & addresses +27. VLAN groups +28. VLANs +29. Services +30. Clusters +31. Virtual machines +32. VM interfaces +33. L2 VPNs diff --git a/docs/getting-started/populating-data.md b/docs/getting-started/populating-data.md new file mode 100644 index 000000000..e182a9d52 --- /dev/null +++ b/docs/getting-started/populating-data.md @@ -0,0 +1,42 @@ +# Populating Data + +This section covers the mechanisms which are available to populate data in NetBox. + +## Manual Object Creation + +The simplest and most direct way of populating data in NetBox is to use the object creation forms in the user interface. + +!!! warning "Not Ideal for Large Imports" + While convenient and readily accessible to even novice users, creating objects one at a time by manually completing these forms obviously does not scale well. For large imports, you're generally best served by using one of the other methods discussed in this section. + +To create a new object in NetBox, find the object type in the navigation menu and click the green "Add" button. + +!!! info "Missing Button?" + If you don't see an "add" button for certain object types, it's likely that your account does not have sufficient permission to create these types. Ask your NetBox administrator to grant the required permissions. + + Also note that some object types, such as device components, cannot be created directly from the navigation menu. These must be created within the context of a parent object (such as a parent device). + + + +## Bulk Import (CSV/YAML) + +NetBox supports the bulk import of new objects using CSV-formatted data. This method can be ideal for importing spreadsheet data, which is very easy to convert to CSV data. CSV data can be imported either as raw text using the form field, or by uploading a properly formatted CSV file. + +When viewing the CSV import form for an object type, you'll notice that the headers for the required columns have been pre-populated. Each form has a table beneath it titled "CSV Field Options," which lists _all_ supported columns for your reference. (Generally, these map to the fields you see in the corresponding creation form for individual objects.) + + + +Note that some models (namely device types and module types) do not support CSV import. Instead, they accept YAML-formatted data to facilitate the import of both the parent object as well as child components. + +## Scripting + +Sometimes you'll find that data you need to populate in NetBox can be easily reduced to a pattern. For example, suppose you have one hundred branch sites and each site gets five VLANs, numbered 101 through 105. While it's certainly possible to explicitly define each of these 500 VLANs in a CSV file for import, it may be quicker to draft a simple custom script to automatically create these VLANs according to the pattern. This ensures a high degree of confidence in the validity of the data, since it's impossible for a script to "miss" a VLAN here or there. + +!!! tip "Reconstruct Existing Data with Scripts" + Sometimes, you might want to write a script to populate objects even if you have the necessary data ready for import. This is because using a script eliminates the need to manually verify existing data prior to import. + +## REST API + +You can also use the REST API to facilitate the population of data in NetBox. The REST API offers full programmatic control over the creation of objects, subject to the same validation rules enforced by the UI forms. Additionally, the REST API supports the bulk creation of multiple objects using a single request. + +For more information about this option, see the [REST API documentation](../rest-api/overview.md). diff --git a/mkdocs.yml b/mkdocs.yml index 6471783e0..2203b8934 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -67,6 +67,9 @@ nav: - 5. HTTP Server: 'installation/5-http-server.md' - 6. LDAP (Optional): 'installation/6-ldap.md' - Upgrading NetBox: 'installation/upgrading.md' + - Getting Started: + - Planning: 'getting-started/planning.md' + - Populating Data: 'getting-started/populating-data.md' - Configuration: - Configuring NetBox: 'configuration/index.md' - Required Settings: 'configuration/required-settings.md' From a6c431f3ba236760ab753f92b8b7aec16c07c568 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 29 Jul 2022 15:10:50 -0400 Subject: [PATCH 315/593] Reorganize configuration docs --- docs/additional-features/napalm.md | 2 +- docs/administration/housekeeping.md | 4 +- docs/configuration/data-validation.md | 86 +++ docs/configuration/date-time.md | 20 + docs/configuration/default-values.md | 77 +++ docs/configuration/development.md | 21 + docs/configuration/dynamic-settings.md | 232 --------- docs/configuration/index.md | 44 +- docs/configuration/miscellaneous.md | 159 ++++++ docs/configuration/napalm.md | 51 ++ docs/configuration/optional-settings.md | 489 ------------------ docs/configuration/plugins.md | 35 ++ docs/configuration/remote-authentication.md | 32 +- ...red-settings.md => required-parameters.md} | 0 docs/configuration/security.md | 144 ++++++ docs/configuration/system.md | 178 +++++++ docs/customization/custom-validation.md | 2 +- docs/customization/export-templates.md | 2 +- docs/customization/reports.md | 2 +- docs/graphql-api/overview.md | 2 +- docs/installation/3-netbox.md | 6 +- docs/installation/6-ldap.md | 2 +- docs/plugins/development/models.md | 2 +- docs/release-notes/version-3.0.md | 2 +- docs/release-notes/version-3.1.md | 2 - docs/release-notes/version-3.2.md | 4 +- docs/rest-api/authentication.md | 2 +- docs/rest-api/overview.md | 4 +- mkdocs.yml | 15 +- 29 files changed, 851 insertions(+), 770 deletions(-) create mode 100644 docs/configuration/data-validation.md create mode 100644 docs/configuration/date-time.md create mode 100644 docs/configuration/default-values.md create mode 100644 docs/configuration/development.md delete mode 100644 docs/configuration/dynamic-settings.md create mode 100644 docs/configuration/miscellaneous.md create mode 100644 docs/configuration/napalm.md delete mode 100644 docs/configuration/optional-settings.md create mode 100644 docs/configuration/plugins.md rename docs/configuration/{required-settings.md => required-parameters.md} (100%) create mode 100644 docs/configuration/security.md create mode 100644 docs/configuration/system.md diff --git a/docs/additional-features/napalm.md b/docs/additional-features/napalm.md index 2387bc8b7..60d8014e2 100644 --- a/docs/additional-features/napalm.md +++ b/docs/additional-features/napalm.md @@ -29,7 +29,7 @@ GET /api/dcim/devices/1/napalm/?method=get_environment ## Authentication -By default, the [`NAPALM_USERNAME`](../configuration/dynamic-settings.md#napalm_username) and [`NAPALM_PASSWORD`](../configuration/dynamic-settings.md#napalm_password) configuration parameters are used for NAPALM authentication. They can be overridden for an individual API call by specifying the `X-NAPALM-Username` and `X-NAPALM-Password` headers. +By default, the [`NAPALM_USERNAME`](../configuration/napalm.md#napalm_username) and [`NAPALM_PASSWORD`](../configuration/napalm.md#napalm_password) configuration parameters are used for NAPALM authentication. They can be overridden for an individual API call by specifying the `X-NAPALM-Username` and `X-NAPALM-Password` headers. ``` $ curl "http://localhost/api/dcim/devices/1/napalm/?method=get_environment" \ diff --git a/docs/administration/housekeeping.md b/docs/administration/housekeeping.md index 1989e41c0..da1a5443b 100644 --- a/docs/administration/housekeeping.md +++ b/docs/administration/housekeeping.md @@ -3,8 +3,8 @@ NetBox includes a `housekeeping` management command that should be run nightly. This command handles: * Clearing expired authentication sessions from the database -* Deleting changelog records older than the configured [retention time](../configuration/dynamic-settings.md#changelog_retention) -* Deleting job result records older than the configured [retention time](../configuration/dynamic-settings.md#jobresult_retention) +* Deleting changelog records older than the configured [retention time](../configuration/miscellaneous.md#changelog_retention) +* Deleting job result records older than the configured [retention time](../configuration/miscellaneous.md#jobresult_retention) This command can be invoked directly, or by using the shell script provided at `/opt/netbox/contrib/netbox-housekeeping.sh`. This script can be linked from your cron scheduler's daily jobs directory (e.g. `/etc/cron.daily`) or referenced directly within the cron configuration file. diff --git a/docs/configuration/data-validation.md b/docs/configuration/data-validation.md new file mode 100644 index 000000000..e4eb4baff --- /dev/null +++ b/docs/configuration/data-validation.md @@ -0,0 +1,86 @@ +# Data & Validation Parameters + +## CUSTOM_VALIDATORS + +!!! tip "Dynamic Configuration Parameter" + +This is a mapping of models to [custom validators](../customization/custom-validation.md) that have been defined locally to enforce custom validation logic. An example is provided below: + +```python +CUSTOM_VALIDATORS = { + "dcim.site": [ + { + "name": { + "min_length": 5, + "max_length": 30 + } + }, + "my_plugin.validators.Validator1" + ], + "dim.device": [ + "my_plugin.validators.Validator1" + ] +} +``` + +--- + +## FIELD_CHOICES + +Some static choice fields on models can be configured with custom values. This is done by defining `FIELD_CHOICES` as a dictionary mapping model fields to their choices. Each choice in the list must have a database value and a human-friendly label, and may optionally specify a color. (A list of available colors is provided below.) + +The choices provided can either replace the stock choices provided by NetBox, or append to them. To _replace_ the available choices, specify the app, model, and field name separated by dots. For example, the site model would be referenced as `dcim.Site.status`. To _extend_ the available choices, append a plus sign to the end of this string (e.g. `dcim.Site.status+`). + +For example, the following configuration would replace the default site status choices with the options Foo, Bar, and Baz: + +```python +FIELD_CHOICES = { + 'dcim.Site.status': ( + ('foo', 'Foo', 'red'), + ('bar', 'Bar', 'green'), + ('baz', 'Baz', 'blue'), + ) +} +``` + +Appending a plus sign to the field identifier would instead _add_ these choices to the ones already offered: + +```python +FIELD_CHOICES = { + 'dcim.Site.status+': ( + ... + ) +} +``` + +The following model fields support configurable choices: + +* `circuits.Circuit.status` +* `dcim.Device.status` +* `dcim.Location.status` +* `dcim.PowerFeed.status` +* `dcim.Rack.status` +* `dcim.Site.status` +* `extras.JournalEntry.kind` +* `ipam.IPAddress.status` +* `ipam.IPRange.status` +* `ipam.Prefix.status` +* `ipam.VLAN.status` +* `virtualization.Cluster.status` +* `virtualization.VirtualMachine.status` + +The following colors are supported: + +* `blue` +* `indigo` +* `purple` +* `pink` +* `red` +* `orange` +* `yellow` +* `green` +* `teal` +* `cyan` +* `gray` +* `black` +* `white` diff --git a/docs/configuration/date-time.md b/docs/configuration/date-time.md new file mode 100644 index 000000000..ab8b5ad13 --- /dev/null +++ b/docs/configuration/date-time.md @@ -0,0 +1,20 @@ +# Date & Time Parameters + +## TIME_ZONE + +Default: UTC + +The time zone NetBox will use when dealing with dates and times. It is recommended to use UTC time unless you have a specific need to use a local time zone. Please see the [list of available time zones](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones). + +## Date and Time Formatting + +You may define custom formatting for date and times. For detailed instructions on writing format strings, please see [the Django documentation](https://docs.djangoproject.com/en/stable/ref/templates/builtins/#date). Default formats are listed below. + +```python +DATE_FORMAT = 'N j, Y' # June 26, 2016 +SHORT_DATE_FORMAT = 'Y-m-d' # 2016-06-26 +TIME_FORMAT = 'g:i a' # 1:23 p.m. +SHORT_TIME_FORMAT = 'H:i:s' # 13:23:00 +DATETIME_FORMAT = 'N j, Y g:i a' # June 26, 2016 1:23 p.m. +SHORT_DATETIME_FORMAT = 'Y-m-d H:i' # 2016-06-26 13:23 +``` diff --git a/docs/configuration/default-values.md b/docs/configuration/default-values.md new file mode 100644 index 000000000..6d92858eb --- /dev/null +++ b/docs/configuration/default-values.md @@ -0,0 +1,77 @@ +# Default Value Parameters + +## DEFAULT_USER_PREFERENCES + +!!! tip "Dynamic Configuration Parameter" + +This is a dictionary defining the default preferences to be set for newly-created user accounts. For example, to set the default page size for all users to 100, define the following: + +```python +DEFAULT_USER_PREFERENCES = { + "pagination": { + "per_page": 100 + } +} +``` + +For a complete list of available preferences, log into NetBox and navigate to `/user/preferences/`. A period in a preference name indicates a level of nesting in the JSON data. The example above maps to `pagination.per_page`. + +--- + +## PAGINATE_COUNT + +!!! tip "Dynamic Configuration Parameter" + +Default: 50 + +The default maximum number of objects to display per page within each list of objects. + +--- + +## POWERFEED_DEFAULT_AMPERAGE + +!!! tip "Dynamic Configuration Parameter" + +Default: 15 + +The default value for the `amperage` field when creating new power feeds. + +--- + +## POWERFEED_DEFAULT_MAX_UTILIZATION + +!!! tip "Dynamic Configuration Parameter" + +Default: 80 + +The default value (percentage) for the `max_utilization` field when creating new power feeds. + +--- + +## POWERFEED_DEFAULT_VOLTAGE + +!!! tip "Dynamic Configuration Parameter" + +Default: 120 + +The default value for the `voltage` field when creating new power feeds. + +--- + +## RACK_ELEVATION_DEFAULT_UNIT_HEIGHT + +!!! tip "Dynamic Configuration Parameter" + +Default: 22 + +Default height (in pixels) of a unit within a rack elevation. For best results, this should be approximately one tenth of `RACK_ELEVATION_DEFAULT_UNIT_WIDTH`. + +--- + +## RACK_ELEVATION_DEFAULT_UNIT_WIDTH + +!!! tip "Dynamic Configuration Parameter" + +Default: 220 + +Default width (in pixels) of a unit within a rack elevation. diff --git a/docs/configuration/development.md b/docs/configuration/development.md new file mode 100644 index 000000000..3af56b0e3 --- /dev/null +++ b/docs/configuration/development.md @@ -0,0 +1,21 @@ +# Development Parameters + +## DEBUG + +Default: False + +This setting enables debugging. Debugging should be enabled only during development or troubleshooting. Note that only +clients which access NetBox from a recognized [internal IP address](#internal_ips) will see debugging tools in the user +interface. + +!!! warning + Never enable debugging on a production system, as it can expose sensitive data to unauthenticated users and impose a + substantial performance penalty. + +--- + +## DEVELOPER + +Default: False + +This parameter serves as a safeguard to prevent some potentially dangerous behavior, such as generating new database schema migrations. Set this to `True` **only** if you are actively developing the NetBox code base. diff --git a/docs/configuration/dynamic-settings.md b/docs/configuration/dynamic-settings.md deleted file mode 100644 index d376dc5c4..000000000 --- a/docs/configuration/dynamic-settings.md +++ /dev/null @@ -1,232 +0,0 @@ -# Dynamic Configuration Settings - -These configuration parameters are primarily controlled via NetBox's admin interface (under Admin > Extras > Configuration Revisions). These setting may also be overridden in `configuration.py`; this will prevent them from being modified via the UI. - ---- - -## ALLOWED_URL_SCHEMES - -Default: `('file', 'ftp', 'ftps', 'http', 'https', 'irc', 'mailto', 'sftp', 'ssh', 'tel', 'telnet', 'tftp', 'vnc', 'xmpp')` - -A list of permitted URL schemes referenced when rendering links within NetBox. Note that only the schemes specified in this list will be accepted: If adding your own, be sure to replicate all of the default values as well (excluding those schemes which are not desirable). - ---- - -## BANNER_TOP - -## BANNER_BOTTOM - -Setting these variables will display custom content in a banner at the top and/or bottom of the page, respectively. HTML is allowed. To replicate the content of the top banner in the bottom banner, set: - -```python -BANNER_TOP = 'Your banner text' -BANNER_BOTTOM = BANNER_TOP -``` - ---- - -## BANNER_LOGIN - -This defines custom content to be displayed on the login page above the login form. HTML is allowed. - ---- - -## CHANGELOG_RETENTION - -Default: 90 - -The number of days to retain logged changes (object creations, updates, and deletions). Set this to `0` to retain -changes in the database indefinitely. - -!!! warning - If enabling indefinite changelog retention, it is recommended to periodically delete old entries. Otherwise, the database may eventually exceed capacity. - ---- - -## CUSTOM_VALIDATORS - -This is a mapping of models to [custom validators](../customization/custom-validation.md) that have been defined locally to enforce custom validation logic. An example is provided below: - -```python -CUSTOM_VALIDATORS = { - "dcim.site": [ - { - "name": { - "min_length": 5, - "max_length": 30 - } - }, - "my_plugin.validators.Validator1" - ], - "dim.device": [ - "my_plugin.validators.Validator1" - ] -} -``` - ---- - -## DEFAULT_USER_PREFERENCES - -This is a dictionary defining the default preferences to be set for newly-created user accounts. For example, to set the default page size for all users to 100, define the following: - -```python -DEFAULT_USER_PREFERENCES = { - "pagination": { - "per_page": 100 - } -} -``` - -For a complete list of available preferences, log into NetBox and navigate to `/user/preferences/`. A period in a preference name indicates a level of nesting in the JSON data. The example above maps to `pagination.per_page`. - ---- - -## ENFORCE_GLOBAL_UNIQUE - -Default: False - -By default, NetBox will permit users to create duplicate prefixes and IP addresses in the global table (that is, those which are not assigned to any VRF). This behavior can be disabled by setting `ENFORCE_GLOBAL_UNIQUE` to True. - ---- - -## GRAPHQL_ENABLED - -Default: True - -Setting this to False will disable the GraphQL API. - ---- - -## JOBRESULT_RETENTION - -Default: 90 - -The number of days to retain job results (scripts and reports). Set this to `0` to retain -job results in the database indefinitely. - -!!! warning - If enabling indefinite job results retention, it is recommended to periodically delete old entries. Otherwise, the database may eventually exceed capacity. - ---- - -## MAINTENANCE_MODE - -Default: False - -Setting this to True will display a "maintenance mode" banner at the top of every page. Additionally, NetBox will no longer update a user's "last active" time upon login. This is to allow new logins when the database is in a read-only state. Recording of login times will resume when maintenance mode is disabled. - ---- - -## MAPS_URL - -Default: `https://maps.google.com/?q=` (Google Maps) - -This specifies the URL to use when presenting a map of a physical location by street address or GPS coordinates. The URL must accept either a free-form street address or a comma-separated pair of numeric coordinates appended to it. - ---- - -## MAX_PAGE_SIZE - -Default: 1000 - -A web user or API consumer can request an arbitrary number of objects by appending the "limit" parameter to the URL (e.g. `?limit=1000`). This parameter defines the maximum acceptable limit. Setting this to `0` or `None` will allow a client to retrieve _all_ matching objects at once with no limit by specifying `?limit=0`. - ---- - -## NAPALM_USERNAME - -## NAPALM_PASSWORD - -NetBox will use these credentials when authenticating to remote devices via the supported [NAPALM integration](../additional-features/napalm.md), if installed. Both parameters are optional. - -!!! note - If SSH public key authentication has been set up on the remote device(s) for the system account under which NetBox runs, these parameters are not needed. - ---- - -## NAPALM_ARGS - -A dictionary of optional arguments to pass to NAPALM when instantiating a network driver. See the NAPALM documentation for a [complete list of optional arguments](https://napalm.readthedocs.io/en/latest/support/#optional-arguments). An example: - -```python -NAPALM_ARGS = { - 'api_key': '472071a93b60a1bd1fafb401d9f8ef41', - 'port': 2222, -} -``` - -Some platforms (e.g. Cisco IOS) require an argument named `secret` to be passed in addition to the normal password. If desired, you can use the configured `NAPALM_PASSWORD` as the value for this argument: - -```python -NAPALM_USERNAME = 'username' -NAPALM_PASSWORD = 'MySecretPassword' -NAPALM_ARGS = { - 'secret': NAPALM_PASSWORD, - # Include any additional args here -} -``` - ---- - -## NAPALM_TIMEOUT - -Default: 30 seconds - -The amount of time (in seconds) to wait for NAPALM to connect to a device. - ---- - -## PAGINATE_COUNT - -Default: 50 - -The default maximum number of objects to display per page within each list of objects. - ---- - -## POWERFEED_DEFAULT_AMPERAGE - -Default: 15 - -The default value for the `amperage` field when creating new power feeds. - ---- - -## POWERFEED_DEFAULT_MAX_UTILIZATION - -Default: 80 - -The default value (percentage) for the `max_utilization` field when creating new power feeds. - ---- - -## POWERFEED_DEFAULT_VOLTAGE - -Default: 120 - -The default value for the `voltage` field when creating new power feeds. - ---- - -## PREFER_IPV4 - -Default: False - -When determining the primary IP address for a device, IPv6 is preferred over IPv4 by default. Set this to True to prefer IPv4 instead. - ---- - -## RACK_ELEVATION_DEFAULT_UNIT_HEIGHT - -Default: 22 - -Default height (in pixels) of a unit within a rack elevation. For best results, this should be approximately one tenth of `RACK_ELEVATION_DEFAULT_UNIT_WIDTH`. - ---- - -## RACK_ELEVATION_DEFAULT_UNIT_WIDTH - -Default: 220 - -Default width (in pixels) of a unit within a rack elevation. diff --git a/docs/configuration/index.md b/docs/configuration/index.md index a863ef3dc..42d254027 100644 --- a/docs/configuration/index.md +++ b/docs/configuration/index.md @@ -1,24 +1,50 @@ # NetBox Configuration -NetBox's local configuration is stored in `$INSTALL_ROOT/netbox/netbox/configuration.py` by default. An example configuration is provided as `configuration_example.py`. You may copy or rename the example configuration and make changes as appropriate. NetBox will not run without a configuration file. While NetBox has many configuration settings, only a few of them must be defined at the time of installation: these are defined under "required settings" below. +## Configuration File + +NetBox's configuration file contains all the important parameters which control how NetBox functions: database settings, security controls, user preferences, and so on. While the default configuration suffices out of the box for most use cases, there are a few [required parameters](./required-parameters.md) which **must** be defined during installation. + +The configuration file is loaded from `$INSTALL_ROOT/netbox/netbox/configuration.py` by default. An example configuration is provided at `configuration_example.py`, which you may copy to use as your default config. Note that a configuration file must be defined; NetBox will not run without one. !!! info "Customizing the Configuration Module" A custom configuration module may be specified by setting the `NETBOX_CONFIGURATION` environment variable. This must be a dotted path to the desired Python module. For example, a file named `my_config.py` in the same directory as `settings.py` would be referenced as `netbox.my_config`. - For the sake of brevity, the NetBox documentation refers to the configuration file simply as `configuration.py`. + To keep things simple, the NetBox documentation refers to the configuration file simply as `configuration.py`. Some configuration parameters may alternatively be defined either in `configuration.py` or within the administrative section of the user interface. Settings which are "hard-coded" in the configuration file take precedence over those defined via the UI. -## Configuration Parameters +## Dynamic Configuration Parameters -* [Required settings](required-settings.md) -* [Optional settings](optional-settings.md) -* [Dynamic settings](dynamic-settings.md) -* [Remote authentication settings](remote-authentication.md) +Some configuration parameters are primarily controlled via NetBox's admin interface (under Admin > Extras > Configuration Revisions). These are noted where applicable in the documentation. These settings may also be overridden in `configuration.py` to prevent them from being modified via the UI. A complete list of supported parameters is provided below: -## Changing the Configuration +* [`ALLOWED_URL_SCHEMES`](./security.md#allowed_url_schemes) +* [`BANNER_BOTTOM`](./miscellaneous.md#banner_bottom) +* [`BANNER_LOGIN`](./miscellaneous.md#banner_login) +* [`BANNER_TOP`](./miscellaneous.md#banner_top) +* [`CHANGELOG_RETENTION`](./miscellaneous.md#changelog_retention) +* [`CUSTOM_VALIDATORS`](./data-validation.md#custom_validators) +* [`DEFAULT_USER_PREFERENCES`](./default-values.md#default_user_preferences) +* [`ENFORCE_GLOBAL_UNIQUE`](./miscellaneous.md#enforce_global_unique) +* [`GRAPHQL_ENABLED`](./miscellaneous.md#graphql_enabled) +* [`JOBRESULT_RETENTION`](./miscellaneous.md#jobresult_retention) +* [`MAINTENANCE_MODE`](./miscellaneous.md#maintenance_mode) +* [`MAPS_URL`](./miscellaneous.md#maps_url) +* [`MAX_PAGE_SIZE`](./miscellaneous.md#max_page_size) +* [`NAPALM_ARGS`](./napalm.md#napalm_args) +* [`NAPALM_PASSWORD`](./napalm.md#napalm_password) +* [`NAPALM_TIMEOUT`](./napalm.md#napalm_timeout) +* [`NAPALM_USERNAME`](./napalm.md#napalm_username) +* [`PAGINATE_COUNT`](./default-values.md#paginate_count) +* [`POWERFEED_DEFAULT_AMPERAGE`](./default-values.md#powerfeed_default_amperage) +* [`POWERFEED_DEFAULT_MAX_UTILIZATION`](./default-values.md#powerfeed_default_max_utilization) +* [`POWERFEED_DEFAULT_VOLTAGE`](./default-values.md#powerfeed_default_voltage) +* [`PREFER_IPV4`](./miscellaneous.md#prefer_ipv4) +* [`RACK_ELEVATION_DEFAULT_UNIT_HEIGHT`](./default-values.md#rack_elevation_default_unit_height) +* [`RACK_ELEVATION_DEFAULT_UNIT_WIDTH`](./default-values.md#rack_elevation_default_unit_width) -The configuration file may be modified at any time. However, the WSGI service (e.g. Gunicorn) must be restarted before the changes will take effect: +## Modifying the Configuration + +The configuration file may be modified at any time. However, the WSGI service (e.g. Gunicorn) must be restarted before these changes will take effect: ```no-highlight $ sudo systemctl restart netbox diff --git a/docs/configuration/miscellaneous.md b/docs/configuration/miscellaneous.md new file mode 100644 index 000000000..2aa21b7e5 --- /dev/null +++ b/docs/configuration/miscellaneous.md @@ -0,0 +1,159 @@ +# Miscellaneous Parameters + +## ADMINS + +NetBox will email details about critical errors to the administrators listed here. This should be a list of (name, email) tuples. For example: + +```python +ADMINS = [ + ['Hank Hill', 'hhill@example.com'], + ['Dale Gribble', 'dgribble@example.com'], +] +``` + +--- + +## BANNER_BOTTOM + +!!! tip "Dynamic Configuration Parameter" + +Sets content for the bottom banner in the user interface. + +--- + +## BANNER_LOGIN + +!!! tip "Dynamic Configuration Parameter" + +This defines custom content to be displayed on the login page above the login form. HTML is allowed. + +--- + +## BANNER_TOP + +!!! tip "Dynamic Configuration Parameter" + +Sets content for the top banner in the user interface. + +!!! tip + If you'd like the top and bottom banners to match, set the following: + + ```python + BANNER_TOP = 'Your banner text' + BANNER_BOTTOM = BANNER_TOP + ``` + +--- + +## CHANGELOG_RETENTION + +!!! tip "Dynamic Configuration Parameter" + +Default: 90 + +The number of days to retain logged changes (object creations, updates, and deletions). Set this to `0` to retain +changes in the database indefinitely. + +!!! warning + If enabling indefinite changelog retention, it is recommended to periodically delete old entries. Otherwise, the database may eventually exceed capacity. + +--- + +## ENFORCE_GLOBAL_UNIQUE + +!!! tip "Dynamic Configuration Parameter" + +Default: False + +By default, NetBox will permit users to create duplicate prefixes and IP addresses in the global table (that is, those which are not assigned to any VRF). This behavior can be disabled by setting `ENFORCE_GLOBAL_UNIQUE` to True. + +--- + +## GRAPHQL_ENABLED + +!!! tip "Dynamic Configuration Parameter" + +Default: True + +Setting this to False will disable the GraphQL API. + +--- + +## JOBRESULT_RETENTION + +!!! tip "Dynamic Configuration Parameter" + +Default: 90 + +The number of days to retain job results (scripts and reports). Set this to `0` to retain +job results in the database indefinitely. + +!!! warning + If enabling indefinite job results retention, it is recommended to periodically delete old entries. Otherwise, the database may eventually exceed capacity. + +--- + +## MAINTENANCE_MODE + +!!! tip "Dynamic Configuration Parameter" + +Default: False + +Setting this to True will display a "maintenance mode" banner at the top of every page. Additionally, NetBox will no longer update a user's "last active" time upon login. This is to allow new logins when the database is in a read-only state. Recording of login times will resume when maintenance mode is disabled. + +--- + +## MAPS_URL + +!!! tip "Dynamic Configuration Parameter" + +Default: `https://maps.google.com/?q=` (Google Maps) + +This specifies the URL to use when presenting a map of a physical location by street address or GPS coordinates. The URL must accept either a free-form street address or a comma-separated pair of numeric coordinates appended to it. + +--- + +## MAX_PAGE_SIZE + +!!! tip "Dynamic Configuration Parameter" + +Default: 1000 + +A web user or API consumer can request an arbitrary number of objects by appending the "limit" parameter to the URL (e.g. `?limit=1000`). This parameter defines the maximum acceptable limit. Setting this to `0` or `None` will allow a client to retrieve _all_ matching objects at once with no limit by specifying `?limit=0`. + +--- + +## METRICS_ENABLED + +Default: False + +Toggle the availability Prometheus-compatible metrics at `/metrics`. See the [Prometheus Metrics](../additional-features/prometheus-metrics.md) documentation for more details. + +--- + +## PREFER_IPV4 + +!!! tip "Dynamic Configuration Parameter" + +Default: False + +When determining the primary IP address for a device, IPv6 is preferred over IPv4 by default. Set this to True to prefer IPv4 instead. + +--- + +## RELEASE_CHECK_URL + +Default: None (disabled) + +This parameter defines the URL of the repository that will be checked for new NetBox releases. When a new release is detected, a message will be displayed to administrative users on the home page. This can be set to the official repository (`'https://api.github.com/repos/netbox-community/netbox/releases'`) or a custom fork. Set this to `None` to disable automatic update checks. + +!!! note + The URL provided **must** be compatible with the [GitHub REST API](https://docs.github.com/en/rest). + +--- + +## RQ_DEFAULT_TIMEOUT + +Default: `300` + +The maximum execution time of a background task (such as running a custom script), in seconds. diff --git a/docs/configuration/napalm.md b/docs/configuration/napalm.md new file mode 100644 index 000000000..925ec17e6 --- /dev/null +++ b/docs/configuration/napalm.md @@ -0,0 +1,51 @@ +# NAPALM Parameters + +## NAPALM_USERNAME + +## NAPALM_PASSWORD + +!!! tip "Dynamic Configuration Parameter" + +NetBox will use these credentials when authenticating to remote devices via the supported [NAPALM integration](../additional-features/napalm.md), if installed. Both parameters are optional. + +!!! note + If SSH public key authentication has been set up on the remote device(s) for the system account under which NetBox runs, these parameters are not needed. + +--- + +## NAPALM_ARGS + +!!! tip "Dynamic Configuration Parameter" + +A dictionary of optional arguments to pass to NAPALM when instantiating a network driver. See the NAPALM documentation for a [complete list of optional arguments](https://napalm.readthedocs.io/en/latest/support/#optional-arguments). An example: + +```python +NAPALM_ARGS = { + 'api_key': '472071a93b60a1bd1fafb401d9f8ef41', + 'port': 2222, +} +``` + +Some platforms (e.g. Cisco IOS) require an argument named `secret` to be passed in addition to the normal password. If desired, you can use the configured `NAPALM_PASSWORD` as the value for this argument: + +```python +NAPALM_USERNAME = 'username' +NAPALM_PASSWORD = 'MySecretPassword' +NAPALM_ARGS = { + 'secret': NAPALM_PASSWORD, + # Include any additional args here +} +``` + +--- + +## NAPALM_TIMEOUT + +!!! tip "Dynamic Configuration Parameter" + +Default: 30 seconds + +The amount of time (in seconds) to wait for NAPALM to connect to a device. + +--- + diff --git a/docs/configuration/optional-settings.md b/docs/configuration/optional-settings.md deleted file mode 100644 index 8e5664a95..000000000 --- a/docs/configuration/optional-settings.md +++ /dev/null @@ -1,489 +0,0 @@ -# Optional Configuration Settings - -## ADMINS - -NetBox will email details about critical errors to the administrators listed here. This should be a list of (name, email) tuples. For example: - -```python -ADMINS = [ - ['Hank Hill', 'hhill@example.com'], - ['Dale Gribble', 'dgribble@example.com'], -] -``` - ---- - -## AUTH_PASSWORD_VALIDATORS - -This parameter acts as a pass-through for configuring Django's built-in password validators for local user accounts. If configured, these will be applied whenever a user's password is updated to ensure that it meets minimum criteria such as length or complexity. An example is provided below. For more detail on the available options, please see [the Django documentation](https://docs.djangoproject.com/en/stable/topics/auth/passwords/#password-validation). - -```python -AUTH_PASSWORD_VALIDATORS = [ - { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', - 'OPTIONS': { - 'min_length': 10, - } - }, -] -``` - ---- - -## BASE_PATH - -Default: None - -The base URL path to use when accessing NetBox. Do not include the scheme or domain name. For example, if installed at https://example.com/netbox/, set: - -```python -BASE_PATH = 'netbox/' -``` - ---- - -## CORS_ORIGIN_ALLOW_ALL - -Default: False - -If True, cross-origin resource sharing (CORS) requests will be accepted from all origins. If False, a whitelist will be used (see below). - ---- - -## CORS_ORIGIN_WHITELIST - -## CORS_ORIGIN_REGEX_WHITELIST - -These settings specify a list of origins that are authorized to make cross-site API requests. Use -`CORS_ORIGIN_WHITELIST` to define a list of exact hostnames, or `CORS_ORIGIN_REGEX_WHITELIST` to define a set of regular -expressions. (These settings have no effect if `CORS_ORIGIN_ALLOW_ALL` is True.) For example: - -```python -CORS_ORIGIN_WHITELIST = [ - 'https://example.com', -] -``` - ---- - -## CSRF_COOKIE_NAME - -Default: `csrftoken` - -The name of the cookie to use for the cross-site request forgery (CSRF) authentication token. See the [Django documentation](https://docs.djangoproject.com/en/stable/ref/settings/#csrf-cookie-name) for more detail. - ---- - -## CSRF_TRUSTED_ORIGINS - -Default: `[]` - -Defines a list of trusted origins for unsafe (e.g. `POST`) requests. This is a pass-through to Django's [`CSRF_TRUSTED_ORIGINS`](https://docs.djangoproject.com/en/4.0/ref/settings/#std:setting-CSRF_TRUSTED_ORIGINS) setting. Note that each host listed must specify a scheme (e.g. `http://` or `https://). - -```python -CSRF_TRUSTED_ORIGINS = ( - 'http://netbox.local', - 'https://netbox.local', -) -``` - ---- - -## DEBUG - -Default: False - -This setting enables debugging. Debugging should be enabled only during development or troubleshooting. Note that only -clients which access NetBox from a recognized [internal IP address](#internal_ips) will see debugging tools in the user -interface. - -!!! warning - Never enable debugging on a production system, as it can expose sensitive data to unauthenticated users and impose a - substantial performance penalty. - ---- - -## DEVELOPER - -Default: False - -This parameter serves as a safeguard to prevent some potentially dangerous behavior, such as generating new database schema migrations. Set this to `True` **only** if you are actively developing the NetBox code base. - ---- - -## DOCS_ROOT - -Default: `$INSTALL_ROOT/docs/` - -The filesystem path to NetBox's documentation. This is used when presenting context-sensitive documentation in the web UI. By default, this will be the `docs/` directory within the root NetBox installation path. (Set this to `None` to disable the embedded documentation.) - ---- - -## EMAIL - -In order to send email, NetBox needs an email server configured. The following items can be defined within the `EMAIL` configuration parameter: - -* `SERVER` - Hostname or IP address of the email server (use `localhost` if running locally) -* `PORT` - TCP port to use for the connection (default: `25`) -* `USERNAME` - Username with which to authenticate -* `PASSSWORD` - Password with which to authenticate -* `USE_SSL` - Use SSL when connecting to the server (default: `False`) -* `USE_TLS` - Use TLS when connecting to the server (default: `False`) -* `SSL_CERTFILE` - Path to the PEM-formatted SSL certificate file (optional) -* `SSL_KEYFILE` - Path to the PEM-formatted SSL private key file (optional) -* `TIMEOUT` - Amount of time to wait for a connection, in seconds (default: `10`) -* `FROM_EMAIL` - Sender address for emails sent by NetBox - -!!! note - The `USE_SSL` and `USE_TLS` parameters are mutually exclusive. - -Email is sent from NetBox only for critical events or if configured for [logging](#logging). If you would like to test the email server configuration, Django provides a convenient [send_mail()](https://docs.djangoproject.com/en/stable/topics/email/#send-mail) function accessible within the NetBox shell: - -```no-highlight -# python ./manage.py nbshell ->>> from django.core.mail import send_mail ->>> send_mail( - 'Test Email Subject', - 'Test Email Body', - 'noreply-netbox@example.com', - ['users@example.com'], - fail_silently=False -) -``` - ---- - -## EXEMPT_VIEW_PERMISSIONS - -Default: Empty list - -A list of NetBox models to exempt from the enforcement of view permissions. Models listed here will be viewable by all users, both authenticated and anonymous. - -List models in the form `.`. For example: - -```python -EXEMPT_VIEW_PERMISSIONS = [ - 'dcim.site', - 'dcim.region', - 'ipam.prefix', -] -``` - -To exempt _all_ models from view permission enforcement, set the following. (Note that `EXEMPT_VIEW_PERMISSIONS` must be an iterable.) - -```python -EXEMPT_VIEW_PERMISSIONS = ['*'] -``` - -!!! note - Using a wildcard will not affect certain potentially sensitive models, such as user permissions. If there is a need to exempt these models, they must be specified individually. - ---- - -## FIELD_CHOICES - -Some static choice fields on models can be configured with custom values. This is done by defining `FIELD_CHOICES` as a dictionary mapping model fields to their choices. Each choice in the list must have a database value and a human-friendly label, and may optionally specify a color. (A list of available colors is provided below.) - -The choices provided can either replace the stock choices provided by NetBox, or append to them. To _replace_ the available choices, specify the app, model, and field name separated by dots. For example, the site model would be referenced as `dcim.Site.status`. To _extend_ the available choices, append a plus sign to the end of this string (e.g. `dcim.Site.status+`). - -For example, the following configuration would replace the default site status choices with the options Foo, Bar, and Baz: - -```python -FIELD_CHOICES = { - 'dcim.Site.status': ( - ('foo', 'Foo', 'red'), - ('bar', 'Bar', 'green'), - ('baz', 'Baz', 'blue'), - ) -} -``` - -Appending a plus sign to the field identifier would instead _add_ these choices to the ones already offered: - -```python -FIELD_CHOICES = { - 'dcim.Site.status+': ( - ... - ) -} -``` - -The following model fields support configurable choices: - -* `circuits.Circuit.status` -* `dcim.Device.status` -* `dcim.Location.status` -* `dcim.PowerFeed.status` -* `dcim.Rack.status` -* `dcim.Site.status` -* `extras.JournalEntry.kind` -* `ipam.IPAddress.status` -* `ipam.IPRange.status` -* `ipam.Prefix.status` -* `ipam.VLAN.status` -* `virtualization.Cluster.status` -* `virtualization.VirtualMachine.status` - -The following colors are supported: - -* `blue` -* `indigo` -* `purple` -* `pink` -* `red` -* `orange` -* `yellow` -* `green` -* `teal` -* `cyan` -* `gray` -* `black` -* `white` - ---- - -## HTTP_PROXIES - -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: - -```python -HTTP_PROXIES = { - 'http': 'http://10.10.1.10:3128', - 'https': 'http://10.10.1.10:1080', -} -``` - ---- - -## JINJA2_FILTERS - -Default: `{}` - -A dictionary of custom jinja2 filters with the key being the filter name and the value being a callable. For more information see the [Jinja2 documentation](https://jinja.palletsprojects.com/en/3.1.x/api/#custom-filters). For example: - -```python -def uppercase(x): - return str(x).upper() - -JINJA2_FILTERS = { - 'uppercase': uppercase, -} -``` - ---- - -## INTERNAL_IPS - -Default: `('127.0.0.1', '::1')` - -A list of IP addresses recognized as internal to the system, used to control the display of debugging output. For -example, the debugging toolbar will be viewable only when a client is accessing NetBox from one of the listed IP -addresses (and [`DEBUG`](#debug) is true). - ---- - -## LOGGING - -By default, all messages of INFO severity or higher will be logged to the console. Additionally, if [`DEBUG`](#debug) is False and email access has been configured, ERROR and CRITICAL messages will be emailed to the users defined in [`ADMINS`](#admins). - -The Django framework on which NetBox runs allows for the customization of logging format and destination. Please consult the [Django logging documentation](https://docs.djangoproject.com/en/stable/topics/logging/) for more information on configuring this setting. Below is an example which will write all INFO and higher messages to a local file: - -```python -LOGGING = { - 'version': 1, - 'disable_existing_loggers': False, - 'handlers': { - 'file': { - 'level': 'INFO', - 'class': 'logging.FileHandler', - 'filename': '/var/log/netbox.log', - }, - }, - 'loggers': { - 'django': { - 'handlers': ['file'], - 'level': 'INFO', - }, - }, -} -``` - -### Available Loggers - -* `netbox..` - Generic form for model-specific log messages -* `netbox.auth.*` - Authentication events -* `netbox.api.views.*` - Views which handle business logic for the REST API -* `netbox.reports.*` - Report execution (`module.name`) -* `netbox.scripts.*` - Custom script execution (`module.name`) -* `netbox.views.*` - Views which handle business logic for the web UI - ---- - -## LOGIN_PERSISTENCE - -Default: False - -If true, the lifetime of a user's authentication session will be automatically reset upon each valid request. For example, if [`LOGIN_TIMEOUT`](#login_timeout) is configured to 14 days (the default), and a user whose session is due to expire in five days makes a NetBox request (with a valid session cookie), the session's lifetime will be reset to 14 days. - -Note that enabling this setting causes NetBox to update a user's session in the database (or file, as configured per [`SESSION_FILE_PATH`](#session_file_path)) with each request, which may introduce significant overhead in very active environments. It also permits an active user to remain authenticated to NetBox indefinitely. - ---- - -## LOGIN_REQUIRED - -Default: False - -Setting this to True will permit only authenticated users to access any part of NetBox. By default, anonymous users are permitted to access most data in NetBox but not make any changes. - ---- - -## LOGIN_TIMEOUT - -Default: 1209600 seconds (14 days) - -The lifetime (in seconds) of the authentication cookie issued to a NetBox user upon login. - ---- - -## MEDIA_ROOT - -Default: $INSTALL_ROOT/netbox/media/ - -The file path to the location where media files (such as image attachments) are stored. By default, this is the `netbox/media/` directory within the base NetBox installation path. - ---- - -## METRICS_ENABLED - -Default: False - -Toggle the availability Prometheus-compatible metrics at `/metrics`. See the [Prometheus Metrics](../additional-features/prometheus-metrics.md) documentation for more details. - ---- - -## PLUGINS - -Default: Empty - -A list of installed [NetBox plugins](../../plugins/) to enable. Plugins will not take effect unless they are listed here. - -!!! warning - Plugins extend NetBox by allowing external code to run with the same access and privileges as NetBox itself. Only install plugins from trusted sources. The NetBox maintainers make absolutely no guarantees about the integrity or security of your installation with plugins enabled. - ---- - -## PLUGINS_CONFIG - -Default: Empty - -This parameter holds configuration settings for individual NetBox plugins. It is defined as a dictionary, with each key using the name of an installed plugin. The specific parameters supported are unique to each plugin: Reference the plugin's documentation to determine the supported parameters. An example configuration is shown below: - -```python -PLUGINS_CONFIG = { - 'plugin1': { - 'foo': 123, - 'bar': True - }, - 'plugin2': { - 'foo': 456, - }, -} -``` - -Note that a plugin must be listed in `PLUGINS` for its configuration to take effect. - ---- - -## RELEASE_CHECK_URL - -Default: None (disabled) - -This parameter defines the URL of the repository that will be checked for new NetBox releases. When a new release is detected, a message will be displayed to administrative users on the home page. This can be set to the official repository (`'https://api.github.com/repos/netbox-community/netbox/releases'`) or a custom fork. Set this to `None` to disable automatic update checks. - -!!! note - The URL provided **must** be compatible with the [GitHub REST API](https://docs.github.com/en/rest). - ---- - -## REPORTS_ROOT - -Default: `$INSTALL_ROOT/netbox/reports/` - -The file path to the location where [custom reports](../customization/reports.md) will be kept. By default, this is the `netbox/reports/` directory within the base NetBox installation path. - ---- - -## RQ_DEFAULT_TIMEOUT - -Default: `300` - -The maximum execution time of a background task (such as running a custom script), in seconds. - ---- - -## SCRIPTS_ROOT - -Default: `$INSTALL_ROOT/netbox/scripts/` - -The file path to the location where [custom scripts](../customization/custom-scripts.md) will be kept. By default, this is the `netbox/scripts/` directory within the base NetBox installation path. - ---- - -## SESSION_COOKIE_NAME - -Default: `sessionid` - -The name used for the session cookie. See the [Django documentation](https://docs.djangoproject.com/en/stable/ref/settings/#session-cookie-name) for more detail. - ---- - -## SESSION_FILE_PATH - -Default: None - -HTTP session data is used to track authenticated users when they access NetBox. By default, NetBox stores session data in its PostgreSQL database. However, this inhibits authentication to a standby instance of NetBox without write access to the database. Alternatively, a local file path may be specified here and NetBox will store session data as files instead of using the database. Note that the NetBox system user must have read and write permissions to this path. - ---- - -## STORAGE_BACKEND - -Default: None (local storage) - -The backend storage engine for handling uploaded files (e.g. image attachments). NetBox supports integration with the [`django-storages`](https://django-storages.readthedocs.io/en/stable/) package, which provides backends for several popular file storage services. If not configured, local filesystem storage will be used. - -The configuration parameters for the specified storage backend are defined under the `STORAGE_CONFIG` setting. - ---- - -## STORAGE_CONFIG - -Default: Empty - -A dictionary of configuration parameters for the storage backend configured as `STORAGE_BACKEND`. The specific parameters to be used here are specific to each backend; see the [`django-storages` documentation](https://django-storages.readthedocs.io/en/stable/) for more detail. - -If `STORAGE_BACKEND` is not defined, this setting will be ignored. - ---- - -## TIME_ZONE - -Default: UTC - -The time zone NetBox will use when dealing with dates and times. It is recommended to use UTC time unless you have a specific need to use a local time zone. Please see the [list of available time zones](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones). - ---- - -## Date and Time Formatting - -You may define custom formatting for date and times. For detailed instructions on writing format strings, please see [the Django documentation](https://docs.djangoproject.com/en/stable/ref/templates/builtins/#date). Default formats are listed below. - -```python -DATE_FORMAT = 'N j, Y' # June 26, 2016 -SHORT_DATE_FORMAT = 'Y-m-d' # 2016-06-26 -TIME_FORMAT = 'g:i a' # 1:23 p.m. -SHORT_TIME_FORMAT = 'H:i:s' # 13:23:00 -DATETIME_FORMAT = 'N j, Y g:i a' # June 26, 2016 1:23 p.m. -SHORT_DATETIME_FORMAT = 'Y-m-d H:i' # 2016-06-26 13:23 -``` diff --git a/docs/configuration/plugins.md b/docs/configuration/plugins.md new file mode 100644 index 000000000..aea60f389 --- /dev/null +++ b/docs/configuration/plugins.md @@ -0,0 +1,35 @@ +# Plugin Parameters + +## PLUGINS + +Default: Empty + +A list of installed [NetBox plugins](../../plugins/) to enable. Plugins will not take effect unless they are listed here. + +!!! warning + Plugins extend NetBox by allowing external code to run with the same access and privileges as NetBox itself. Only install plugins from trusted sources. The NetBox maintainers make absolutely no guarantees about the integrity or security of your installation with plugins enabled. + +--- + +## PLUGINS_CONFIG + +Default: Empty + +This parameter holds configuration settings for individual NetBox plugins. It is defined as a dictionary, with each key using the name of an installed plugin. The specific parameters supported are unique to each plugin: Reference the plugin's documentation to determine the supported parameters. An example configuration is shown below: + +```python +PLUGINS_CONFIG = { + 'plugin1': { + 'foo': 123, + 'bar': True + }, + 'plugin2': { + 'foo': 456, + }, +} +``` + +Note that a plugin must be listed in `PLUGINS` for its configuration to take effect. + +--- + diff --git a/docs/configuration/remote-authentication.md b/docs/configuration/remote-authentication.md index 2c3a7002f..07adf5c6a 100644 --- a/docs/configuration/remote-authentication.md +++ b/docs/configuration/remote-authentication.md @@ -47,6 +47,22 @@ NetBox can be configured to support remote user authentication by inferring user --- +## REMOTE_AUTH_GROUP_HEADER + +Default: `'HTTP_REMOTE_USER_GROUP'` + +When remote user authentication is in use, this is the name of the HTTP header which informs NetBox of the currently authenticated user. For example, to use the request header `X-Remote-User-Groups` it needs to be set to `HTTP_X_REMOTE_USER_GROUPS`. (Requires `REMOTE_AUTH_ENABLED` and `REMOTE_AUTH_GROUP_SYNC_ENABLED` ) + +--- + +## REMOTE_AUTH_GROUP_SEPARATOR + +Default: `|` (Pipe) + +The Seperator upon which `REMOTE_AUTH_GROUP_HEADER` gets split into individual Groups. This needs to be coordinated with your authentication Proxy. (Requires `REMOTE_AUTH_ENABLED` and `REMOTE_AUTH_GROUP_SYNC_ENABLED` ) + +--- + ## REMOTE_AUTH_GROUP_SYNC_ENABLED Default: `False` @@ -63,14 +79,6 @@ When remote user authentication is in use, this is the name of the HTTP header w --- -## REMOTE_AUTH_GROUP_HEADER - -Default: `'HTTP_REMOTE_USER_GROUP'` - -When remote user authentication is in use, this is the name of the HTTP header which informs NetBox of the currently authenticated user. For example, to use the request header `X-Remote-User-Groups` it needs to be set to `HTTP_X_REMOTE_USER_GROUPS`. (Requires `REMOTE_AUTH_ENABLED` and `REMOTE_AUTH_GROUP_SYNC_ENABLED` ) - ---- - ## REMOTE_AUTH_SUPERUSER_GROUPS Default: `[]` (Empty list) @@ -100,11 +108,3 @@ The list of groups that promote an remote User to Staff on Login. If group isn't Default: `[]` (Empty list) The list of users that get promoted to Staff on Login. If user isn't present in list on next Login, the Role gets revoked. (Requires `REMOTE_AUTH_ENABLED` and `REMOTE_AUTH_GROUP_SYNC_ENABLED` ) - ---- - -## REMOTE_AUTH_GROUP_SEPARATOR - -Default: `|` (Pipe) - -The Seperator upon which `REMOTE_AUTH_GROUP_HEADER` gets split into individual Groups. This needs to be coordinated with your authentication Proxy. (Requires `REMOTE_AUTH_ENABLED` and `REMOTE_AUTH_GROUP_SYNC_ENABLED` ) diff --git a/docs/configuration/required-settings.md b/docs/configuration/required-parameters.md similarity index 100% rename from docs/configuration/required-settings.md rename to docs/configuration/required-parameters.md diff --git a/docs/configuration/security.md b/docs/configuration/security.md new file mode 100644 index 000000000..6aa363b1a --- /dev/null +++ b/docs/configuration/security.md @@ -0,0 +1,144 @@ +# Security & Authentication Parameters + +## ALLOWED_URL_SCHEMES + +!!! tip "Dynamic Configuration Parameter" + +Default: `('file', 'ftp', 'ftps', 'http', 'https', 'irc', 'mailto', 'sftp', 'ssh', 'tel', 'telnet', 'tftp', 'vnc', 'xmpp')` + +A list of permitted URL schemes referenced when rendering links within NetBox. Note that only the schemes specified in this list will be accepted: If adding your own, be sure to replicate all the default values as well (excluding those schemes which are not desirable). + +--- + +## AUTH_PASSWORD_VALIDATORS + +This parameter acts as a pass-through for configuring Django's built-in password validators for local user accounts. If configured, these will be applied whenever a user's password is updated to ensure that it meets minimum criteria such as length or complexity. An example is provided below. For more detail on the available options, please see [the Django documentation](https://docs.djangoproject.com/en/stable/topics/auth/passwords/#password-validation). + +```python +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + 'OPTIONS': { + 'min_length': 10, + } + }, +] +``` + +--- + +## CORS_ORIGIN_ALLOW_ALL + +Default: False + +If True, cross-origin resource sharing (CORS) requests will be accepted from all origins. If False, a whitelist will be used (see below). + +--- + +## CORS_ORIGIN_WHITELIST + +## CORS_ORIGIN_REGEX_WHITELIST + +These settings specify a list of origins that are authorized to make cross-site API requests. Use +`CORS_ORIGIN_WHITELIST` to define a list of exact hostnames, or `CORS_ORIGIN_REGEX_WHITELIST` to define a set of regular +expressions. (These settings have no effect if `CORS_ORIGIN_ALLOW_ALL` is True.) For example: + +```python +CORS_ORIGIN_WHITELIST = [ + 'https://example.com', +] +``` + +--- + +## CSRF_COOKIE_NAME + +Default: `csrftoken` + +The name of the cookie to use for the cross-site request forgery (CSRF) authentication token. See the [Django documentation](https://docs.djangoproject.com/en/stable/ref/settings/#csrf-cookie-name) for more detail. + +--- + +--- + +## CSRF_TRUSTED_ORIGINS + +Default: `[]` + +Defines a list of trusted origins for unsafe (e.g. `POST`) requests. This is a pass-through to Django's [`CSRF_TRUSTED_ORIGINS`](https://docs.djangoproject.com/en/4.0/ref/settings/#std:setting-CSRF_TRUSTED_ORIGINS) setting. Note that each host listed must specify a scheme (e.g. `http://` or `https://). + +```python +CSRF_TRUSTED_ORIGINS = ( + 'http://netbox.local', + 'https://netbox.local', +) +``` + +--- + +## EXEMPT_VIEW_PERMISSIONS + +Default: Empty list + +A list of NetBox models to exempt from the enforcement of view permissions. Models listed here will be viewable by all users, both authenticated and anonymous. + +List models in the form `.`. For example: + +```python +EXEMPT_VIEW_PERMISSIONS = [ + 'dcim.site', + 'dcim.region', + 'ipam.prefix', +] +``` + +To exempt _all_ models from view permission enforcement, set the following. (Note that `EXEMPT_VIEW_PERMISSIONS` must be an iterable.) + +```python +EXEMPT_VIEW_PERMISSIONS = ['*'] +``` + +!!! note + Using a wildcard will not affect certain potentially sensitive models, such as user permissions. If there is a need to exempt these models, they must be specified individually. + +--- + +## LOGIN_PERSISTENCE + +Default: False + +If true, the lifetime of a user's authentication session will be automatically reset upon each valid request. For example, if [`LOGIN_TIMEOUT`](#login_timeout) is configured to 14 days (the default), and a user whose session is due to expire in five days makes a NetBox request (with a valid session cookie), the session's lifetime will be reset to 14 days. + +Note that enabling this setting causes NetBox to update a user's session in the database (or file, as configured per [`SESSION_FILE_PATH`](#session_file_path)) with each request, which may introduce significant overhead in very active environments. It also permits an active user to remain authenticated to NetBox indefinitely. + +--- + +## LOGIN_REQUIRED + +Default: False + +Setting this to True will permit only authenticated users to access any part of NetBox. By default, anonymous users are permitted to access most data in NetBox but not make any changes. + +--- + +## LOGIN_TIMEOUT + +Default: 1209600 seconds (14 days) + +The lifetime (in seconds) of the authentication cookie issued to a NetBox user upon login. + +--- + +## SESSION_COOKIE_NAME + +Default: `sessionid` + +The name used for the session cookie. See the [Django documentation](https://docs.djangoproject.com/en/stable/ref/settings/#session-cookie-name) for more detail. + +--- + +## SESSION_FILE_PATH + +Default: None + +HTTP session data is used to track authenticated users when they access NetBox. By default, NetBox stores session data in its PostgreSQL database. However, this inhibits authentication to a standby instance of NetBox without write access to the database. Alternatively, a local file path may be specified here and NetBox will store session data as files instead of using the database. Note that the NetBox system user must have read and write permissions to this path. diff --git a/docs/configuration/system.md b/docs/configuration/system.md new file mode 100644 index 000000000..21607e566 --- /dev/null +++ b/docs/configuration/system.md @@ -0,0 +1,178 @@ +# System Parameters + +## BASE_PATH + +Default: None + +The base URL path to use when accessing NetBox. Do not include the scheme or domain name. For example, if installed at https://example.com/netbox/, set: + +```python +BASE_PATH = 'netbox/' +``` + +--- + +## DOCS_ROOT + +Default: `$INSTALL_ROOT/docs/` + +The filesystem path to NetBox's documentation. This is used when presenting context-sensitive documentation in the web UI. By default, this will be the `docs/` directory within the root NetBox installation path. (Set this to `None` to disable the embedded documentation.) + +--- + +## EMAIL + +In order to send email, NetBox needs an email server configured. The following items can be defined within the `EMAIL` configuration parameter: + +* `SERVER` - Hostname or IP address of the email server (use `localhost` if running locally) +* `PORT` - TCP port to use for the connection (default: `25`) +* `USERNAME` - Username with which to authenticate +* `PASSSWORD` - Password with which to authenticate +* `USE_SSL` - Use SSL when connecting to the server (default: `False`) +* `USE_TLS` - Use TLS when connecting to the server (default: `False`) +* `SSL_CERTFILE` - Path to the PEM-formatted SSL certificate file (optional) +* `SSL_KEYFILE` - Path to the PEM-formatted SSL private key file (optional) +* `TIMEOUT` - Amount of time to wait for a connection, in seconds (default: `10`) +* `FROM_EMAIL` - Sender address for emails sent by NetBox + +!!! note + The `USE_SSL` and `USE_TLS` parameters are mutually exclusive. + +Email is sent from NetBox only for critical events or if configured for [logging](#logging). If you would like to test the email server configuration, Django provides a convenient [send_mail()](https://docs.djangoproject.com/en/stable/topics/email/#send-mail) function accessible within the NetBox shell: + +```no-highlight +# python ./manage.py nbshell +>>> from django.core.mail import send_mail +>>> send_mail( + 'Test Email Subject', + 'Test Email Body', + 'noreply-netbox@example.com', + ['users@example.com'], + fail_silently=False +) +``` + +--- + +## HTTP_PROXIES + +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: + +```python +HTTP_PROXIES = { + 'http': 'http://10.10.1.10:3128', + 'https': 'http://10.10.1.10:1080', +} +``` + +--- + +## INTERNAL_IPS + +Default: `('127.0.0.1', '::1')` + +A list of IP addresses recognized as internal to the system, used to control the display of debugging output. For +example, the debugging toolbar will be viewable only when a client is accessing NetBox from one of the listed IP +addresses (and [`DEBUG`](#debug) is true). + +--- + +## JINJA2_FILTERS + +Default: `{}` + +A dictionary of custom jinja2 filters with the key being the filter name and the value being a callable. For more information see the [Jinja2 documentation](https://jinja.palletsprojects.com/en/3.1.x/api/#custom-filters). For example: + +```python +def uppercase(x): + return str(x).upper() + +JINJA2_FILTERS = { + 'uppercase': uppercase, +} +``` + +--- + +## LOGGING + +By default, all messages of INFO severity or higher will be logged to the console. Additionally, if [`DEBUG`](#debug) is False and email access has been configured, ERROR and CRITICAL messages will be emailed to the users defined in [`ADMINS`](#admins). + +The Django framework on which NetBox runs allows for the customization of logging format and destination. Please consult the [Django logging documentation](https://docs.djangoproject.com/en/stable/topics/logging/) for more information on configuring this setting. Below is an example which will write all INFO and higher messages to a local file: + +```python +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'handlers': { + 'file': { + 'level': 'INFO', + 'class': 'logging.FileHandler', + 'filename': '/var/log/netbox.log', + }, + }, + 'loggers': { + 'django': { + 'handlers': ['file'], + 'level': 'INFO', + }, + }, +} +``` + +### Available Loggers + +* `netbox..` - Generic form for model-specific log messages +* `netbox.auth.*` - Authentication events +* `netbox.api.views.*` - Views which handle business logic for the REST API +* `netbox.reports.*` - Report execution (`module.name`) +* `netbox.scripts.*` - Custom script execution (`module.name`) +* `netbox.views.*` - Views which handle business logic for the web UI + +--- + +## MEDIA_ROOT + +Default: $INSTALL_ROOT/netbox/media/ + +The file path to the location where media files (such as image attachments) are stored. By default, this is the `netbox/media/` directory within the base NetBox installation path. + +--- + +## REPORTS_ROOT + +Default: `$INSTALL_ROOT/netbox/reports/` + +The file path to the location where [custom reports](../customization/reports.md) will be kept. By default, this is the `netbox/reports/` directory within the base NetBox installation path. + +--- + +## SCRIPTS_ROOT + +Default: `$INSTALL_ROOT/netbox/scripts/` + +The file path to the location where [custom scripts](../customization/custom-scripts.md) will be kept. By default, this is the `netbox/scripts/` directory within the base NetBox installation path. + +--- + +## STORAGE_BACKEND + +Default: None (local storage) + +The backend storage engine for handling uploaded files (e.g. image attachments). NetBox supports integration with the [`django-storages`](https://django-storages.readthedocs.io/en/stable/) package, which provides backends for several popular file storage services. If not configured, local filesystem storage will be used. + +The configuration parameters for the specified storage backend are defined under the `STORAGE_CONFIG` setting. + +--- + +## STORAGE_CONFIG + +Default: Empty + +A dictionary of configuration parameters for the storage backend configured as `STORAGE_BACKEND`. The specific parameters to be used here are specific to each backend; see the [`django-storages` documentation](https://django-storages.readthedocs.io/en/stable/) for more detail. + +If `STORAGE_BACKEND` is not defined, this setting will be ignored. + +--- diff --git a/docs/customization/custom-validation.md b/docs/customization/custom-validation.md index f88cd309b..30198117f 100644 --- a/docs/customization/custom-validation.md +++ b/docs/customization/custom-validation.md @@ -50,7 +50,7 @@ The `fail()` method may optionally specify a field with which to associate the s ## Assigning Custom Validators -Custom validators are associated with specific NetBox models under the [CUSTOM_VALIDATORS](../configuration/dynamic-settings.md#custom_validators) configuration parameter. There are three manners by which custom validation rules can be defined: +Custom validators are associated with specific NetBox models under the [CUSTOM_VALIDATORS](../configuration/data-validation.md#custom_validators) configuration parameter. There are three manners by which custom validation rules can be defined: 1. Plain JSON mapping (no custom logic) 2. Dotted path to a custom validator class diff --git a/docs/customization/export-templates.md b/docs/customization/export-templates.md index affd39aae..3c7ff7d20 100644 --- a/docs/customization/export-templates.md +++ b/docs/customization/export-templates.md @@ -2,7 +2,7 @@ ## REST API Integration -When it is necessary to provide authentication credentials (such as when [`LOGIN_REQUIRED`](../configuration/optional-settings.md#login_required) has been enabled), it is recommended to render export templates via the REST API. This allows the client to specify an authentication token. To render an export template via the REST API, make a `GET` request to the model's list endpoint and append the `export` parameter specifying the export template name. For example: +When it is necessary to provide authentication credentials (such as when [`LOGIN_REQUIRED`](../configuration/security.md#login_required) has been enabled), it is recommended to render export templates via the REST API. This allows the client to specify an authentication token. To render an export template via the REST API, make a `GET` request to the model's list endpoint and append the `export` parameter specifying the export template name. For example: ``` GET /api/dcim/sites/?export=MyTemplateName diff --git a/docs/customization/reports.md b/docs/customization/reports.md index ae4ceb9aa..150c32f40 100644 --- a/docs/customization/reports.md +++ b/docs/customization/reports.md @@ -12,7 +12,7 @@ A NetBox report is a mechanism for validating the integrity of data within NetBo ## Writing Reports -Reports must be saved as files in the [`REPORTS_ROOT`](../configuration/optional-settings.md#reports_root) path (which defaults to `netbox/reports/`). Each file created within this path is considered a separate module. Each module holds one or more reports (Python classes), each of which performs a certain function. The logic of each report is broken into discrete test methods, each of which applies a small portion of the logic comprising the overall test. +Reports must be saved as files in the [`REPORTS_ROOT`](../configuration/system.md#reports_root) path (which defaults to `netbox/reports/`). Each file created within this path is considered a separate module. Each module holds one or more reports (Python classes), each of which performs a certain function. The logic of each report is broken into discrete test methods, each of which applies a small portion of the logic comprising the overall test. !!! warning The reports path includes a file named `__init__.py`, which registers the path as a Python module. Do not delete this file. diff --git a/docs/graphql-api/overview.md b/docs/graphql-api/overview.md index 57dfb22bd..4fc6d2dd8 100644 --- a/docs/graphql-api/overview.md +++ b/docs/graphql-api/overview.md @@ -67,4 +67,4 @@ Authorization: Token $TOKEN ## Disabling the GraphQL API -If not needed, the GraphQL API can be disabled by setting the [`GRAPHQL_ENABLED`](../configuration/dynamic-settings.md#graphql_enabled) configuration parameter to False and restarting NetBox. +If not needed, the GraphQL API can be disabled by setting the [`GRAPHQL_ENABLED`](../configuration/miscellaneous.md#graphql_enabled) configuration parameter to False and restarting NetBox. diff --git a/docs/installation/3-netbox.md b/docs/installation/3-netbox.md index 50b350d3a..7c4a60500 100644 --- a/docs/installation/3-netbox.md +++ b/docs/installation/3-netbox.md @@ -142,7 +142,7 @@ ALLOWED_HOSTS = ['*'] ### DATABASE -This parameter holds the database configuration details. You must define the username and password used when you configured PostgreSQL. If the service is running on a remote host, update the `HOST` and `PORT` parameters accordingly. See the [configuration documentation](../configuration/required-settings.md#database) for more detail on individual parameters. +This parameter holds the database configuration details. You must define the username and password used when you configured PostgreSQL. If the service is running on a remote host, update the `HOST` and `PORT` parameters accordingly. See the [configuration documentation](../configuration/required-parameters.md#database) for more detail on individual parameters. ```python DATABASE = { @@ -157,7 +157,7 @@ DATABASE = { ### REDIS -Redis is a in-memory key-value store used by NetBox for caching and background task queuing. Redis typically requires minimal configuration; the values below should suffice for most installations. See the [configuration documentation](../configuration/required-settings.md#redis) for more detail on individual parameters. +Redis is a in-memory key-value store used by NetBox for caching and background task queuing. Redis typically requires minimal configuration; the values below should suffice for most installations. See the [configuration documentation](../configuration/required-parameters.md#redis) for more detail on individual parameters. Note that NetBox requires the specification of two separate Redis databases: `tasks` and `caching`. These may both be provided by the same Redis service, however each should have a unique numeric database ID. @@ -209,7 +209,7 @@ sudo sh -c "echo 'napalm' >> /opt/netbox/local_requirements.txt" ### Remote File Storage -By default, NetBox will use the local filesystem to store uploaded files. To use a remote filesystem, install the [`django-storages`](https://django-storages.readthedocs.io/en/stable/) library and configure your [desired storage backend](../configuration/optional-settings.md#storage_backend) in `configuration.py`. +By default, NetBox will use the local filesystem to store uploaded files. To use a remote filesystem, install the [`django-storages`](https://django-storages.readthedocs.io/en/stable/) library and configure your [desired storage backend](../configuration/system.md#storage_backend) in `configuration.py`. ```no-highlight sudo sh -c "echo 'django-storages' >> /opt/netbox/local_requirements.txt" diff --git a/docs/installation/6-ldap.md b/docs/installation/6-ldap.md index 281554f75..163ace70d 100644 --- a/docs/installation/6-ldap.md +++ b/docs/installation/6-ldap.md @@ -142,7 +142,7 @@ AUTH_LDAP_CACHE_TIMEOUT = 3600 `systemctl restart netbox` restarts the NetBox service, and initiates any changes made to `ldap_config.py`. If there are syntax errors present, the NetBox process will not spawn an instance, and errors should be logged to `/var/log/messages`. -For troubleshooting LDAP user/group queries, add or merge the following [logging](../configuration/optional-settings.md#logging) configuration to `configuration.py`: +For troubleshooting LDAP user/group queries, add or merge the following [logging](../configuration/system.md#logging) configuration to `configuration.py`: ```python LOGGING = { diff --git a/docs/plugins/development/models.md b/docs/plugins/development/models.md index 6d7075b80..c58621b81 100644 --- a/docs/plugins/development/models.md +++ b/docs/plugins/development/models.md @@ -156,7 +156,7 @@ class StatusChoices(ChoiceSet): key = 'MyModel.status' ``` -To extend or replace the default values for this choice set, a NetBox administrator can then reference it under the [`FIELD_CHOICES`](../../configuration/optional-settings.md#field_choices) configuration parameter. For example, the `status` field on `MyModel` in `my_plugin` would be referenced as: +To extend or replace the default values for this choice set, a NetBox administrator can then reference it under the [`FIELD_CHOICES`](../../configuration/data-validation.md#field_choices) configuration parameter. For example, the `status` field on `MyModel` in `my_plugin` would be referenced as: ```python FIELD_CHOICES = { diff --git a/docs/release-notes/version-3.0.md b/docs/release-notes/version-3.0.md index 7d6341f44..93ff33d95 100644 --- a/docs/release-notes/version-3.0.md +++ b/docs/release-notes/version-3.0.md @@ -367,7 +367,7 @@ More information about IP ranges is available [in the documentation](../models/i #### Custom Model Validation ([#5963](https://github.com/netbox-community/netbox/issues/5963)) -This release introduces the [`CUSTOM_VALIDATORS`](../configuration/dynamic-settings.md#custom_validators) configuration parameter, which allows administrators to map NetBox models to custom validator classes to enforce custom validation logic. For example, the following configuration requires every site to have a name of at least ten characters and a description: +This release introduces the [`CUSTOM_VALIDATORS`](../configuration/data-validation.md#custom_validators) configuration parameter, which allows administrators to map NetBox models to custom validator classes to enforce custom validation logic. For example, the following configuration requires every site to have a name of at least ten characters and a description: ```python from extras.validators import CustomValidator diff --git a/docs/release-notes/version-3.1.md b/docs/release-notes/version-3.1.md index 27ba4e69e..9dce1dfd4 100644 --- a/docs/release-notes/version-3.1.md +++ b/docs/release-notes/version-3.1.md @@ -313,8 +313,6 @@ Some parameters of NetBox's configuration are now accessible via the admin UI. T Dynamic configuration parameters may also still be defined within `configuration.py`, and the settings defined here take precedence over those defined via the user interface. -For a complete list of supported parameters, please see the [dynamic configuration documentation](../configuration/dynamic-settings.md). - #### First Hop Redundancy Protocol (FHRP) Groups ([#6235](https://github.com/netbox-community/netbox/issues/6235)) A new FHRP group model has been introduced to aid in modeling the configurations of protocols such as HSRP, VRRP, and GLBP. Each FHRP group may be assigned one or more virtual IP addresses, as well as an authentication type and key. Member device and VM interfaces may be associated with one or more FHRP groups, with each assignment receiving a numeric priority designation. diff --git a/docs/release-notes/version-3.2.md b/docs/release-notes/version-3.2.md index c36344912..10ccaeb4d 100644 --- a/docs/release-notes/version-3.2.md +++ b/docs/release-notes/version-3.2.md @@ -267,7 +267,7 @@ Custom field object assignment is fully supported in the REST API, and functions #### Custom Status Choices ([#8054](https://github.com/netbox-community/netbox/issues/8054)) -Custom choices can be now added to most object status fields in NetBox. This is done by defining the [`FIELD_CHOICES`](../configuration/optional-settings.md#field_choices) configuration parameter to map field identifiers to an iterable of custom choices an (optionally) colors. These choices are populated automatically when NetBox initializes. For example, the following configuration will add three custom choices for the site status field, each with a designated color: +Custom choices can be now added to most object status fields in NetBox. This is done by defining the [`FIELD_CHOICES`](../configuration/data-validation.md#field_choices) configuration parameter to map field identifiers to an iterable of custom choices an (optionally) colors. These choices are populated automatically when NetBox initializes. For example, the following configuration will add three custom choices for the site status field, each with a designated color: ```python FIELD_CHOICES = { @@ -291,7 +291,7 @@ FIELD_CHOICES = { #### Improved User Preferences ([#7759](https://github.com/netbox-community/netbox/issues/7759)) -A robust new mechanism for managing user preferences is included in this release. The user preferences form has been improved for better usability, and administrators can now define default preferences for all users with the [`DEFAULT_USER_PREFERENCES`](../configuration/dynamic-settings.md##default_user_preferences) configuration parameter. For example, this can be used to define the columns which appear by default in a table: +A robust new mechanism for managing user preferences is included in this release. The user preferences form has been improved for better usability, and administrators can now define default preferences for all users with the [`DEFAULT_USER_PREFERENCES`](../configuration/default-values.md#default_user_preferences) configuration parameter. For example, this can be used to define the columns which appear by default in a table: ```python DEFAULT_USER_PREFERENCES = { diff --git a/docs/rest-api/authentication.md b/docs/rest-api/authentication.md index 18b6bc4f8..411063338 100644 --- a/docs/rest-api/authentication.md +++ b/docs/rest-api/authentication.md @@ -20,7 +20,7 @@ https://netbox/api/dcim/sites/ } ``` -A token is not required for read-only operations which have been exempted from permissions enforcement (using the [`EXEMPT_VIEW_PERMISSIONS`](../configuration/optional-settings.md#exempt_view_permissions) configuration parameter). However, if a token _is_ required but not present in a request, the API will return a 403 (Forbidden) response: +A token is not required for read-only operations which have been exempted from permissions enforcement (using the [`EXEMPT_VIEW_PERMISSIONS`](../configuration/security.md#exempt_view_permissions) configuration parameter). However, if a token _is_ required but not present in a request, the API will return a 403 (Forbidden) response: ``` $ curl https://netbox/api/dcim/sites/ diff --git a/docs/rest-api/overview.md b/docs/rest-api/overview.md index 27a9b6a7e..5fc4f18bb 100644 --- a/docs/rest-api/overview.md +++ b/docs/rest-api/overview.md @@ -308,7 +308,7 @@ Vary: Accept } ``` -The default page is determined by the [`PAGINATE_COUNT`](../configuration/dynamic-settings.md#paginate_count) configuration parameter, which defaults to 50. However, this can be overridden per request by specifying the desired `offset` and `limit` query parameters. For example, if you wish to retrieve a hundred devices at a time, you would make a request for: +The default page is determined by the [`PAGINATE_COUNT`](../configuration/default-values.md#paginate_count) configuration parameter, which defaults to 50. However, this can be overridden per request by specifying the desired `offset` and `limit` query parameters. For example, if you wish to retrieve a hundred devices at a time, you would make a request for: ``` http://netbox/api/dcim/devices/?limit=100 @@ -325,7 +325,7 @@ The response will return devices 1 through 100. The URL provided in the `next` a } ``` -The maximum number of objects that can be returned is limited by the [`MAX_PAGE_SIZE`](../configuration/dynamic-settings.md#max_page_size) configuration parameter, which is 1000 by default. Setting this to `0` or `None` will remove the maximum limit. An API consumer can then pass `?limit=0` to retrieve _all_ matching objects with a single request. +The maximum number of objects that can be returned is limited by the [`MAX_PAGE_SIZE`](../configuration/miscellaneous.md#max_page_size) configuration parameter, which is 1000 by default. Setting this to `0` or `None` will remove the maximum limit. An API consumer can then pass `?limit=0` to retrieve _all_ matching objects with a single request. !!! warning Disabling the page size limit introduces a potential for very resource-intensive requests, since one API request can effectively retrieve an entire table from the database. diff --git a/mkdocs.yml b/mkdocs.yml index 2203b8934..3acb157aa 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -72,11 +72,18 @@ nav: - Populating Data: 'getting-started/populating-data.md' - Configuration: - Configuring NetBox: 'configuration/index.md' - - Required Settings: 'configuration/required-settings.md' - - Optional Settings: 'configuration/optional-settings.md' - - Dynamic Settings: 'configuration/dynamic-settings.md' - - Error Reporting: 'configuration/error-reporting.md' + - Required Parameters: 'configuration/required-parameters.md' + - System: 'configuration/system.md' + - Security: 'configuration/security.md' - Remote Authentication: 'configuration/remote-authentication.md' + - Data & Validation: 'configuration/data-validation.md' + - Default Values: 'configuration/default-values.md' + - Error Reporting: 'configuration/error-reporting.md' + - Plugins: 'configuration/plugins.md' + - NAPALM: 'configuration/napalm.md' + - Date & Time: 'configuration/date-time.md' + - Miscellaneous: 'configuration/miscellaneous.md' + - Development: 'configuration/development.md' - Customization: - Custom Fields: 'customization/custom-fields.md' - Custom Validation: 'customization/custom-validation.md' From 1cd407548813bc959f4959f7009b3d800fe71311 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 29 Jul 2022 16:23:45 -0400 Subject: [PATCH 316/593] Add mermaid flowcharts showing rough data model dependencies --- docs/getting-started/planning.md | 116 ++++++++++++++++++++++--------- mkdocs.yml | 6 +- 2 files changed, 87 insertions(+), 35 deletions(-) diff --git a/docs/getting-started/planning.md b/docs/getting-started/planning.md index 00640ca44..5c431a4d2 100644 --- a/docs/getting-started/planning.md +++ b/docs/getting-started/planning.md @@ -50,38 +50,86 @@ When starting with a completely empty database, it might not be immediately clea Below is the (rough) recommended order in which NetBox objects should be created or imported. While it is not required to follow this exact order, doing so will help ensure the smoothest workflow. - +1. Tenant groups and tenants +2. Regions, site groups, sites, and locations +3. Rack roles and racks +4. Manufacturers, device types, and module types +5. Platforms and device roles +6. Devices and modules +7. Providers and provider networks +8. Circuit types and circuits +9. Wireless LAN groups and wireless LANs +10. Route targets and VRFs +11. RIRs and aggregates +12. IP/VLAN roles +13. Prefixes, IP ranges, and IP addresses +14. VLAN groups and VLANs +15. Cluster types, cluster groups, and clusters +16. Virtual machines and VM interfaces -1. Tenant groups -2. Tenants -3. Regions and/or site groups -4. Sites -5. Locations -6. Rack roles -7. Racks -8. Platforms -9. Manufacturers -10. Device types -11. Module types -12. Device roles -13. Devices -14. Providers -15. Provider networks -16. Circuit types -17. Circuits -18. Wireless LAN groups -19. Wireless LANs & links -20. Route targets -21. VRFs -22. RIRs -23. Aggregates -24. IP/VLAN roles -25. Prefixes -26. IP ranges & addresses -27. VLAN groups -28. VLANs -29. Services -30. Clusters -31. Virtual machines -32. VM interfaces -33. L2 VPNs +This is not a comprehensive list, but should suffice for the initial data imports. Beyond these, it the order in which objects are added doesn't have much if any impact. + +The graphs below illustrate some of the core dependencies among different models in NetBox for your reference. + +!!! note "Self-Nesting Models" + Each model in the graphs below which show a looping arrow pointing back to itself can be nested in a recursive hierarchy. For example, you can have regions representing both countries and cities, with the latter nested underneath the former. + +### Tenancy + +```mermaid +flowchart TD + TenantGroup --> TenantGroup + TenantGroup --> Tenant + Tenant --> Site & Device & Prefix & VLAN & ... +``` + +### Sites, Racks, and Devices + +```mermaid +flowchart TD + Region --> Region + SiteGroup --> SiteGroup + DeviceRole & Platform --> Device + Region & SiteGroup --> Site + Site --> Location & Device + Location --> Location + Location --> Rack & Device + Rack --> Device + Manufacturer --> DeviceType & ModuleType + DeviceType --> Device + Device & ModuleType ---> Module + Device & Module --> Interface +``` + +### VRFs, Prefixes, IP Addresses, and VLANs + +```mermaid +flowchart TD + VLANGroup --> VLAN + Role --> VLAN & IPRange & Prefix + RIR --> Aggregate + RouteTarget --> VRF + Aggregate & VRF --> Prefix + VRF --> IPRange & IPAddress + Prefix --> VLAN & IPRange & IPAddress +``` + +### Circuits + +```mermaid +flowchart TD + Provider & CircuitType --> Circuit + Provider --> ProviderNetwork + Circuit --> CircuitTermination +``` + +### Clusters and Virtual Machines + +```mermaid +flowchart TD + ClusterGroup & ClusterType --> Cluster + Cluster --> VirtualMachine + Site --> Cluster & VirtualMachine + Device & Platform --> VirtualMachine + VirtualMachine --> VMInterface +``` diff --git a/mkdocs.yml b/mkdocs.yml index 3acb157aa..4c5279127 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -53,7 +53,11 @@ markdown_extensions: - pymdownx.emoji: emoji_index: !!python/name:materialx.emoji.twemoji emoji_generator: !!python/name:materialx.emoji.to_svg - - pymdownx.superfences + - pymdownx.superfences: + custom_fences: + - name: mermaid + class: mermaid + format: !!python/name:pymdownx.superfences.fence_code_format - pymdownx.tabbed: alternate_style: true nav: From 6904666e2a6e9be451be3acbdd669af012aec45b Mon Sep 17 00:00:00 2001 From: Kim Johansson Date: Sat, 30 Jul 2022 01:18:30 +0200 Subject: [PATCH 317/593] Remove deprecated usage of prefetch_related Fixes #9699 --- netbox/circuits/views.py | 15 ++++-- netbox/dcim/views.py | 90 +++++++++++++++++----------------- netbox/extras/views.py | 4 +- netbox/ipam/views.py | 61 +++++++++++------------ netbox/tenancy/views.py | 10 ++-- netbox/virtualization/views.py | 15 +++--- 6 files changed, 101 insertions(+), 94 deletions(-) diff --git a/netbox/circuits/views.py b/netbox/circuits/views.py index 11f211b27..423bd67d6 100644 --- a/netbox/circuits/views.py +++ b/netbox/circuits/views.py @@ -30,7 +30,8 @@ class ProviderView(generic.ObjectView): circuits = Circuit.objects.restrict(request.user, 'view').filter( provider=instance ).prefetch_related( - 'type', 'tenant', 'tenant__group', 'terminations__site' + 'tenant__group', 'termination_a__site', 'termination_z__site', + 'termination_a__provider_network', 'termination_z__provider_network', ) circuits_table = tables.CircuitTable(circuits, user=request.user, exclude=('provider',)) circuits_table.configure(request) @@ -91,7 +92,8 @@ class ProviderNetworkView(generic.ObjectView): Q(termination_a__provider_network=instance.pk) | Q(termination_z__provider_network=instance.pk) ).prefetch_related( - 'type', 'tenant', 'tenant__group', 'terminations__site' + 'tenant__group', 'termination_a__site', 'termination_z__site', + 'termination_a__provider_network', 'termination_z__provider_network', ) circuits_table = tables.CircuitTable(circuits, user=request.user) circuits_table.configure(request) @@ -192,7 +194,8 @@ class CircuitTypeBulkDeleteView(generic.BulkDeleteView): class CircuitListView(generic.ObjectListView): queryset = Circuit.objects.prefetch_related( - 'provider', 'type', 'tenant', 'tenant__group', 'termination_a', 'termination_z' + 'tenant__group', 'termination_a__site', 'termination_z__site', + 'termination_a__provider_network', 'termination_z__provider_network', ) filterset = filtersets.CircuitFilterSet filterset_form = forms.CircuitFilterForm @@ -220,7 +223,8 @@ class CircuitBulkImportView(generic.BulkImportView): class CircuitBulkEditView(generic.BulkEditView): queryset = Circuit.objects.prefetch_related( - 'provider', 'type', 'tenant', 'terminations' + 'termination_a__site', 'termination_z__site', + 'termination_a__provider_network', 'termination_z__provider_network', ) filterset = filtersets.CircuitFilterSet table = tables.CircuitTable @@ -229,7 +233,8 @@ class CircuitBulkEditView(generic.BulkEditView): class CircuitBulkDeleteView(generic.BulkDeleteView): queryset = Circuit.objects.prefetch_related( - 'provider', 'type', 'tenant', 'terminations' + 'termination_a__site', 'termination_z__site', + 'termination_a__provider_network', 'termination_z__provider_network', ) filterset = filtersets.CircuitFilterSet table = tables.CircuitTable diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 12e070e70..6daecb3a6 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -324,7 +324,7 @@ class SiteListView(generic.ObjectListView): class SiteView(generic.ObjectView): - queryset = Site.objects.prefetch_related('region', 'tenant__group') + queryset = Site.objects.prefetch_related('tenant__group') def get_extra_context(self, request, instance): stats = { @@ -359,7 +359,7 @@ class SiteView(generic.ObjectView): site=instance, position__isnull=True, parent_bay__isnull=True - ).prefetch_related('device_type__manufacturer') + ).prefetch_related('device_type__manufacturer', 'parent_bay', 'device_role') asns = ASN.objects.restrict(request.user, 'view').filter(sites=instance) asn_count = asns.count() @@ -391,14 +391,14 @@ class SiteBulkImportView(generic.BulkImportView): class SiteBulkEditView(generic.BulkEditView): - queryset = Site.objects.prefetch_related('region', 'tenant') + queryset = Site.objects.all() filterset = filtersets.SiteFilterSet table = tables.SiteTable form = forms.SiteBulkEditForm class SiteBulkDeleteView(generic.BulkDeleteView): - queryset = Site.objects.prefetch_related('region', 'tenant') + queryset = Site.objects.all() filterset = filtersets.SiteFilterSet table = tables.SiteTable @@ -454,7 +454,7 @@ class LocationView(generic.ObjectView): location=instance, position__isnull=True, parent_bay__isnull=True - ).prefetch_related('device_type__manufacturer') + ).prefetch_related('device_type__manufacturer', 'parent_bay', 'device_role') return { 'rack_count': rack_count, @@ -572,7 +572,7 @@ class RackRoleBulkDeleteView(generic.BulkDeleteView): # class RackListView(generic.ObjectListView): - queryset = Rack.objects.prefetch_related('devices__device_type').annotate( + queryset = Rack.objects.annotate( device_count=count_related(Device, 'rack') ) filterset = filtersets.RackFilterSet @@ -631,7 +631,7 @@ class RackView(generic.ObjectView): rack=instance, position__isnull=True, parent_bay__isnull=True - ).prefetch_related('device_type__manufacturer') + ).prefetch_related('device_type__manufacturer', 'parent_bay', 'device_role') peer_racks = Rack.objects.restrict(request.user, 'view').filter(site=instance.site) @@ -682,14 +682,14 @@ class RackBulkImportView(generic.BulkImportView): class RackBulkEditView(generic.BulkEditView): - queryset = Rack.objects.prefetch_related('site', 'location', 'tenant', 'role') + queryset = Rack.objects.all() filterset = filtersets.RackFilterSet table = tables.RackTable form = forms.RackBulkEditForm class RackBulkDeleteView(generic.BulkDeleteView): - queryset = Rack.objects.prefetch_related('site', 'location', 'tenant', 'role') + queryset = Rack.objects.all() filterset = filtersets.RackFilterSet table = tables.RackTable @@ -706,7 +706,7 @@ class RackReservationListView(generic.ObjectListView): class RackReservationView(generic.ObjectView): - queryset = RackReservation.objects.prefetch_related('rack') + queryset = RackReservation.objects.all() class RackReservationEditView(generic.ObjectEditView): @@ -742,14 +742,14 @@ class RackReservationImportView(generic.BulkImportView): class RackReservationBulkEditView(generic.BulkEditView): - queryset = RackReservation.objects.prefetch_related('rack', 'user') + queryset = RackReservation.objects.all() filterset = filtersets.RackReservationFilterSet table = tables.RackReservationTable form = forms.RackReservationBulkEditForm class RackReservationBulkDeleteView(generic.BulkDeleteView): - queryset = RackReservation.objects.prefetch_related('rack', 'user') + queryset = RackReservation.objects.all() filterset = filtersets.RackReservationFilterSet table = tables.RackReservationTable @@ -831,7 +831,7 @@ class ManufacturerBulkDeleteView(generic.BulkDeleteView): # class DeviceTypeListView(generic.ObjectListView): - queryset = DeviceType.objects.prefetch_related('manufacturer').annotate( + queryset = DeviceType.objects.annotate( instance_count=count_related(Device, 'device_type') ) filterset = filtersets.DeviceTypeFilterSet @@ -840,7 +840,7 @@ class DeviceTypeListView(generic.ObjectListView): class DeviceTypeView(generic.ObjectView): - queryset = DeviceType.objects.prefetch_related('manufacturer') + queryset = DeviceType.objects.all() def get_extra_context(self, request, instance): instance_count = Device.objects.restrict(request.user).filter(device_type=instance).count() @@ -964,7 +964,7 @@ class DeviceTypeImportView(generic.ObjectImportView): class DeviceTypeBulkEditView(generic.BulkEditView): - queryset = DeviceType.objects.prefetch_related('manufacturer').annotate( + queryset = DeviceType.objects.annotate( instance_count=count_related(Device, 'device_type') ) filterset = filtersets.DeviceTypeFilterSet @@ -973,7 +973,7 @@ class DeviceTypeBulkEditView(generic.BulkEditView): class DeviceTypeBulkDeleteView(generic.BulkDeleteView): - queryset = DeviceType.objects.prefetch_related('manufacturer').annotate( + queryset = DeviceType.objects.annotate( instance_count=count_related(Device, 'device_type') ) filterset = filtersets.DeviceTypeFilterSet @@ -985,7 +985,7 @@ class DeviceTypeBulkDeleteView(generic.BulkDeleteView): # class ModuleTypeListView(generic.ObjectListView): - queryset = ModuleType.objects.prefetch_related('manufacturer').annotate( + queryset = ModuleType.objects.annotate( instance_count=count_related(Module, 'module_type') ) filterset = filtersets.ModuleTypeFilterSet @@ -994,7 +994,7 @@ class ModuleTypeListView(generic.ObjectListView): class ModuleTypeView(generic.ObjectView): - queryset = ModuleType.objects.prefetch_related('manufacturer') + queryset = ModuleType.objects.all() def get_extra_context(self, request, instance): instance_count = Module.objects.restrict(request.user).filter(module_type=instance).count() @@ -1091,7 +1091,7 @@ class ModuleTypeImportView(generic.ObjectImportView): class ModuleTypeBulkEditView(generic.BulkEditView): - queryset = ModuleType.objects.prefetch_related('manufacturer').annotate( + queryset = ModuleType.objects.annotate( instance_count=count_related(Module, 'module_type') ) filterset = filtersets.ModuleTypeFilterSet @@ -1100,7 +1100,7 @@ class ModuleTypeBulkEditView(generic.BulkEditView): class ModuleTypeBulkDeleteView(generic.BulkDeleteView): - queryset = ModuleType.objects.prefetch_related('manufacturer').annotate( + queryset = ModuleType.objects.annotate( instance_count=count_related(Module, 'module_type') ) filterset = filtersets.ModuleTypeFilterSet @@ -1611,9 +1611,7 @@ class DeviceListView(generic.ObjectListView): class DeviceView(generic.ObjectView): - queryset = Device.objects.prefetch_related( - 'site__region', 'location', 'rack', 'tenant__group', 'device_role', 'platform', 'primary_ip4', 'primary_ip6' - ) + queryset = Device.objects.all() def get_extra_context(self, request, instance): # VirtualChassis members @@ -1790,14 +1788,14 @@ class ChildDeviceBulkImportView(generic.BulkImportView): class DeviceBulkEditView(generic.BulkEditView): - queryset = Device.objects.prefetch_related('tenant', 'site', 'rack', 'device_role', 'device_type__manufacturer') + queryset = Device.objects.prefetch_related('device_type__manufacturer') filterset = filtersets.DeviceFilterSet table = tables.DeviceTable form = forms.DeviceBulkEditForm class DeviceBulkDeleteView(generic.BulkDeleteView): - queryset = Device.objects.prefetch_related('tenant', 'site', 'rack', 'device_role', 'device_type__manufacturer') + queryset = Device.objects.prefetch_related('device_type__manufacturer') filterset = filtersets.DeviceFilterSet table = tables.DeviceTable @@ -1807,7 +1805,7 @@ class DeviceBulkDeleteView(generic.BulkDeleteView): # class ModuleListView(generic.ObjectListView): - queryset = Module.objects.prefetch_related('device', 'module_type__manufacturer') + queryset = Module.objects.prefetch_related('module_type__manufacturer') filterset = filtersets.ModuleFilterSet filterset_form = forms.ModuleFilterForm table = tables.ModuleTable @@ -1833,14 +1831,14 @@ class ModuleBulkImportView(generic.BulkImportView): class ModuleBulkEditView(generic.BulkEditView): - queryset = Module.objects.prefetch_related('device', 'module_type__manufacturer') + queryset = Module.objects.prefetch_related('module_type__manufacturer') filterset = filtersets.ModuleFilterSet table = tables.ModuleTable form = forms.ModuleBulkEditForm class ModuleBulkDeleteView(generic.BulkDeleteView): - queryset = Module.objects.prefetch_related('device', 'module_type__manufacturer') + queryset = Module.objects.prefetch_related('module_type__manufacturer') filterset = filtersets.ModuleFilterSet table = tables.ModuleTable @@ -2566,7 +2564,7 @@ class InventoryItemBulkImportView(generic.BulkImportView): class InventoryItemBulkEditView(generic.BulkEditView): - queryset = InventoryItem.objects.prefetch_related('device', 'manufacturer', 'role') + queryset = InventoryItem.objects.all() filterset = filtersets.InventoryItemFilterSet table = tables.InventoryItemTable form = forms.InventoryItemBulkEditForm @@ -2577,7 +2575,7 @@ class InventoryItemBulkRenameView(generic.BulkRenameView): class InventoryItemBulkDeleteView(generic.BulkDeleteView): - queryset = InventoryItem.objects.prefetch_related('device', 'manufacturer', 'role') + queryset = InventoryItem.objects.all() table = tables.InventoryItemTable template_name = 'dcim/inventoryitem_bulk_delete.html' @@ -2867,14 +2865,20 @@ class CableBulkImportView(generic.BulkImportView): class CableBulkEditView(generic.BulkEditView): - queryset = Cable.objects.prefetch_related('terminations') + queryset = Cable.objects.prefetch_related( + 'terminations__termination', 'terminations___device', 'terminations___rack', 'terminations___location', + 'terminations___site', + ) filterset = filtersets.CableFilterSet table = tables.CableTable form = forms.CableBulkEditForm class CableBulkDeleteView(generic.BulkDeleteView): - queryset = Cable.objects.prefetch_related('terminations') + queryset = Cable.objects.prefetch_related( + 'terminations__termination', 'terminations___device', 'terminations___rack', 'terminations___location', + 'terminations___site', + ) filterset = filtersets.CableFilterSet table = tables.CableTable @@ -2930,7 +2934,7 @@ class InterfaceConnectionsListView(generic.ObjectListView): # class VirtualChassisListView(generic.ObjectListView): - queryset = VirtualChassis.objects.prefetch_related('master').annotate( + queryset = VirtualChassis.objects.annotate( member_count=count_related(Device, 'virtual_chassis') ) table = tables.VirtualChassisTable @@ -3158,9 +3162,7 @@ class VirtualChassisBulkDeleteView(generic.BulkDeleteView): # class PowerPanelListView(generic.ObjectListView): - queryset = PowerPanel.objects.prefetch_related( - 'site', 'location' - ).annotate( + queryset = PowerPanel.objects.annotate( powerfeed_count=count_related(PowerFeed, 'power_panel') ) filterset = filtersets.PowerPanelFilterSet @@ -3169,10 +3171,10 @@ class PowerPanelListView(generic.ObjectListView): class PowerPanelView(generic.ObjectView): - queryset = PowerPanel.objects.prefetch_related('site', 'location') + queryset = PowerPanel.objects.all() def get_extra_context(self, request, instance): - power_feeds = PowerFeed.objects.restrict(request.user).filter(power_panel=instance).prefetch_related('rack') + power_feeds = PowerFeed.objects.restrict(request.user).filter(power_panel=instance) powerfeed_table = tables.PowerFeedTable( data=power_feeds, orderable=False @@ -3202,16 +3204,14 @@ class PowerPanelBulkImportView(generic.BulkImportView): class PowerPanelBulkEditView(generic.BulkEditView): - queryset = PowerPanel.objects.prefetch_related('site', 'location') + queryset = PowerPanel.objects.all() filterset = filtersets.PowerPanelFilterSet table = tables.PowerPanelTable form = forms.PowerPanelBulkEditForm class PowerPanelBulkDeleteView(generic.BulkDeleteView): - queryset = PowerPanel.objects.prefetch_related( - 'site', 'location' - ).annotate( + queryset = PowerPanel.objects.annotate( powerfeed_count=count_related(PowerFeed, 'power_panel') ) filterset = filtersets.PowerPanelFilterSet @@ -3230,7 +3230,7 @@ class PowerFeedListView(generic.ObjectListView): class PowerFeedView(generic.ObjectView): - queryset = PowerFeed.objects.prefetch_related('power_panel', 'rack') + queryset = PowerFeed.objects.all() class PowerFeedEditView(generic.ObjectEditView): @@ -3249,7 +3249,7 @@ class PowerFeedBulkImportView(generic.BulkImportView): class PowerFeedBulkEditView(generic.BulkEditView): - queryset = PowerFeed.objects.prefetch_related('power_panel', 'rack') + queryset = PowerFeed.objects.all() filterset = filtersets.PowerFeedFilterSet table = tables.PowerFeedTable form = forms.PowerFeedBulkEditForm @@ -3260,6 +3260,6 @@ class PowerFeedBulkDisconnectView(BulkDisconnectView): class PowerFeedBulkDeleteView(generic.BulkDeleteView): - queryset = PowerFeed.objects.prefetch_related('power_panel', 'rack') + queryset = PowerFeed.objects.all() filterset = filtersets.PowerFeedFilterSet table = tables.PowerFeedTable diff --git a/netbox/extras/views.py b/netbox/extras/views.py index bb99536c3..5b589c181 100644 --- a/netbox/extras/views.py +++ b/netbox/extras/views.py @@ -492,14 +492,14 @@ class JournalEntryDeleteView(generic.ObjectDeleteView): class JournalEntryBulkEditView(generic.BulkEditView): - queryset = JournalEntry.objects.prefetch_related('created_by') + queryset = JournalEntry.objects.all() filterset = filtersets.JournalEntryFilterSet table = tables.JournalEntryTable form = forms.JournalEntryBulkEditForm class JournalEntryBulkDeleteView(generic.BulkDeleteView): - queryset = JournalEntry.objects.prefetch_related('created_by') + queryset = JournalEntry.objects.all() filterset = filtersets.JournalEntryFilterSet table = tables.JournalEntryTable diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index 72b223b55..880ddb83b 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -40,11 +40,11 @@ class VRFView(generic.ObjectView): ipaddress_count = IPAddress.objects.restrict(request.user, 'view').filter(vrf=instance).count() import_targets_table = tables.RouteTargetTable( - instance.import_targets.prefetch_related('tenant'), + instance.import_targets.all(), orderable=False ) export_targets_table = tables.RouteTargetTable( - instance.export_targets.prefetch_related('tenant'), + instance.export_targets.all(), orderable=False ) @@ -72,14 +72,14 @@ class VRFBulkImportView(generic.BulkImportView): class VRFBulkEditView(generic.BulkEditView): - queryset = VRF.objects.prefetch_related('tenant') + queryset = VRF.objects.all() filterset = filtersets.VRFFilterSet table = tables.VRFTable form = forms.VRFBulkEditForm class VRFBulkDeleteView(generic.BulkDeleteView): - queryset = VRF.objects.prefetch_related('tenant') + queryset = VRF.objects.all() filterset = filtersets.VRFFilterSet table = tables.VRFTable @@ -100,11 +100,11 @@ class RouteTargetView(generic.ObjectView): def get_extra_context(self, request, instance): importing_vrfs_table = tables.VRFTable( - instance.importing_vrfs.prefetch_related('tenant'), + instance.importing_vrfs.all(), orderable=False ) exporting_vrfs_table = tables.VRFTable( - instance.exporting_vrfs.prefetch_related('tenant'), + instance.exporting_vrfs.all(), orderable=False ) @@ -130,14 +130,14 @@ class RouteTargetBulkImportView(generic.BulkImportView): class RouteTargetBulkEditView(generic.BulkEditView): - queryset = RouteTarget.objects.prefetch_related('tenant') + queryset = RouteTarget.objects.all() filterset = filtersets.RouteTargetFilterSet table = tables.RouteTargetTable form = forms.RouteTargetBulkEditForm class RouteTargetBulkDeleteView(generic.BulkDeleteView): - queryset = RouteTarget.objects.prefetch_related('tenant') + queryset = RouteTarget.objects.all() filterset = filtersets.RouteTargetFilterSet table = tables.RouteTargetTable @@ -334,14 +334,14 @@ class AggregateBulkImportView(generic.BulkImportView): class AggregateBulkEditView(generic.BulkEditView): - queryset = Aggregate.objects.prefetch_related('rir') + queryset = Aggregate.objects.all() filterset = filtersets.AggregateFilterSet table = tables.AggregateTable form = forms.AggregateBulkEditForm class AggregateBulkDeleteView(generic.BulkDeleteView): - queryset = Aggregate.objects.prefetch_related('rir') + queryset = Aggregate.objects.all() filterset = filtersets.AggregateFilterSet table = tables.AggregateTable @@ -417,7 +417,7 @@ class PrefixListView(generic.ObjectListView): class PrefixView(generic.ObjectView): - queryset = Prefix.objects.prefetch_related('vrf', 'site__region', 'tenant__group', 'vlan__group', 'role') + queryset = Prefix.objects.all() def get_extra_context(self, request, instance): try: @@ -433,7 +433,7 @@ class PrefixView(generic.ObjectView): ).filter( prefix__net_contains=str(instance.prefix) ).prefetch_related( - 'site', 'role', 'tenant' + 'site', 'role', 'tenant', 'vlan', ) parent_prefix_table = tables.PrefixTable( list(parent_prefixes), @@ -447,7 +447,7 @@ class PrefixView(generic.ObjectView): ).exclude( pk=instance.pk ).prefetch_related( - 'site', 'role' + 'site', 'role', 'tenant', 'vlan', ) duplicate_prefix_table = tables.PrefixTable( list(duplicate_prefixes), @@ -500,7 +500,7 @@ class PrefixIPRangesView(generic.ObjectChildrenView): def get_children(self, request, parent): return parent.get_child_ranges().restrict(request.user, 'view').prefetch_related( - 'vrf', 'role', 'tenant', 'tenant__group', + 'tenant__group', ) def get_extra_context(self, request, instance): @@ -519,7 +519,7 @@ class PrefixIPAddressesView(generic.ObjectChildrenView): template_name = 'ipam/prefix/ip_addresses.html' def get_children(self, request, parent): - return parent.get_child_ips().restrict(request.user, 'view').prefetch_related('vrf', 'tenant') + return parent.get_child_ips().restrict(request.user, 'view').prefetch_related('vrf', 'tenant', 'tenant__group') def prep_table_data(self, request, queryset, parent): show_available = bool(request.GET.get('show_available', 'true') == 'true') @@ -552,14 +552,14 @@ class PrefixBulkImportView(generic.BulkImportView): class PrefixBulkEditView(generic.BulkEditView): - queryset = Prefix.objects.prefetch_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role') + queryset = Prefix.objects.prefetch_related('vrf__tenant') filterset = filtersets.PrefixFilterSet table = tables.PrefixTable form = forms.PrefixBulkEditForm class PrefixBulkDeleteView(generic.BulkDeleteView): - queryset = Prefix.objects.prefetch_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role') + queryset = Prefix.objects.prefetch_related('vrf__tenant') filterset = filtersets.PrefixFilterSet table = tables.PrefixTable @@ -611,14 +611,14 @@ class IPRangeBulkImportView(generic.BulkImportView): class IPRangeBulkEditView(generic.BulkEditView): - queryset = IPRange.objects.prefetch_related('vrf', 'tenant') + queryset = IPRange.objects.all() filterset = filtersets.IPRangeFilterSet table = tables.IPRangeTable form = forms.IPRangeBulkEditForm class IPRangeBulkDeleteView(generic.BulkDeleteView): - queryset = IPRange.objects.prefetch_related('vrf', 'tenant') + queryset = IPRange.objects.all() filterset = filtersets.IPRangeFilterSet table = tables.IPRangeTable @@ -789,14 +789,14 @@ class IPAddressBulkImportView(generic.BulkImportView): class IPAddressBulkEditView(generic.BulkEditView): - queryset = IPAddress.objects.prefetch_related('vrf__tenant', 'tenant') + queryset = IPAddress.objects.prefetch_related('vrf__tenant') filterset = filtersets.IPAddressFilterSet table = tables.IPAddressTable form = forms.IPAddressBulkEditForm class IPAddressBulkDeleteView(generic.BulkDeleteView): - queryset = IPAddress.objects.prefetch_related('vrf__tenant', 'tenant') + queryset = IPAddress.objects.prefetch_related('vrf__tenant') filterset = filtersets.IPAddressFilterSet table = tables.IPAddressTable @@ -819,7 +819,8 @@ class VLANGroupView(generic.ObjectView): def get_extra_context(self, request, instance): vlans = VLAN.objects.restrict(request.user, 'view').filter(group=instance).prefetch_related( - Prefetch('prefixes', queryset=Prefix.objects.restrict(request.user)) + Prefetch('prefixes', queryset=Prefix.objects.restrict(request.user)), + 'tenant', 'site', 'role', ).order_by('vid') vlans_count = vlans.count() vlans = add_available_vlans(vlans, vlan_group=instance) @@ -894,7 +895,7 @@ class FHRPGroupView(generic.ObjectView): def get_extra_context(self, request, instance): # Get assigned IP addresses ipaddress_table = tables.AssignedIPAddressesTable( - data=instance.ip_addresses.restrict(request.user, 'view').prefetch_related('vrf', 'tenant'), + data=instance.ip_addresses.restrict(request.user, 'view'), orderable=False ) @@ -984,11 +985,11 @@ class VLANListView(generic.ObjectListView): class VLANView(generic.ObjectView): - queryset = VLAN.objects.prefetch_related('site__region', 'tenant__group', 'role') + queryset = VLAN.objects.all() def get_extra_context(self, request, instance): prefixes = Prefix.objects.restrict(request.user, 'view').filter(vlan=instance).prefetch_related( - 'vrf', 'site', 'role' + 'vrf', 'site', 'role', 'tenant' ) prefix_table = tables.PrefixTable(list(prefixes), exclude=('vlan', 'utilization'), orderable=False) @@ -1046,14 +1047,14 @@ class VLANBulkImportView(generic.BulkImportView): class VLANBulkEditView(generic.BulkEditView): - queryset = VLAN.objects.prefetch_related('site', 'group', 'tenant', 'role') + queryset = VLAN.objects.all() filterset = filtersets.VLANFilterSet table = tables.VLANTable form = forms.VLANBulkEditForm class VLANBulkDeleteView(generic.BulkDeleteView): - queryset = VLAN.objects.prefetch_related('site', 'group', 'tenant', 'role') + queryset = VLAN.objects.all() filterset = filtersets.VLANFilterSet table = tables.VLANTable @@ -1106,14 +1107,14 @@ class ServiceTemplateBulkDeleteView(generic.BulkDeleteView): # class ServiceListView(generic.ObjectListView): - queryset = Service.objects.all() + queryset = Service.objects.prefetch_related('device', 'virtual_machine') filterset = filtersets.ServiceFilterSet filterset_form = forms.ServiceFilterForm table = tables.ServiceTable class ServiceView(generic.ObjectView): - queryset = Service.objects.prefetch_related('ipaddresses') + queryset = Service.objects.all() class ServiceCreateView(generic.ObjectEditView): @@ -1123,7 +1124,7 @@ class ServiceCreateView(generic.ObjectEditView): class ServiceEditView(generic.ObjectEditView): - queryset = Service.objects.prefetch_related('ipaddresses') + queryset = Service.objects.all() form = forms.ServiceForm template_name = 'ipam/service_edit.html' diff --git a/netbox/tenancy/views.py b/netbox/tenancy/views.py index 07a25b5a4..9a2fe6ab9 100644 --- a/netbox/tenancy/views.py +++ b/netbox/tenancy/views.py @@ -95,7 +95,7 @@ class TenantListView(generic.ObjectListView): class TenantView(generic.ObjectView): - queryset = Tenant.objects.prefetch_related('group') + queryset = Tenant.objects.all() def get_extra_context(self, request, instance): stats = { @@ -140,14 +140,14 @@ class TenantBulkImportView(generic.BulkImportView): class TenantBulkEditView(generic.BulkEditView): - queryset = Tenant.objects.prefetch_related('group') + queryset = Tenant.objects.all() filterset = filtersets.TenantFilterSet table = tables.TenantTable form = forms.TenantBulkEditForm class TenantBulkDeleteView(generic.BulkDeleteView): - queryset = Tenant.objects.prefetch_related('group') + queryset = Tenant.objects.all() filterset = filtersets.TenantFilterSet table = tables.TenantTable @@ -337,14 +337,14 @@ class ContactBulkImportView(generic.BulkImportView): class ContactBulkEditView(generic.BulkEditView): - queryset = Contact.objects.prefetch_related('group') + queryset = Contact.objects.all() filterset = filtersets.ContactFilterSet table = tables.ContactTable form = forms.ContactBulkEditForm class ContactBulkDeleteView(generic.BulkDeleteView): - queryset = Contact.objects.prefetch_related('group') + queryset = Contact.objects.all() filterset = filtersets.ContactFilterSet table = tables.ContactTable diff --git a/netbox/virtualization/views.py b/netbox/virtualization/views.py index 4cd7da30d..5b26f8503 100644 --- a/netbox/virtualization/views.py +++ b/netbox/virtualization/views.py @@ -209,14 +209,14 @@ class ClusterBulkImportView(generic.BulkImportView): class ClusterBulkEditView(generic.BulkEditView): - queryset = Cluster.objects.prefetch_related('type', 'group', 'site') + queryset = Cluster.objects.all() filterset = filtersets.ClusterFilterSet table = tables.ClusterTable form = forms.ClusterBulkEditForm class ClusterBulkDeleteView(generic.BulkDeleteView): - queryset = Cluster.objects.prefetch_related('type', 'group', 'site') + queryset = Cluster.objects.all() filterset = filtersets.ClusterFilterSet table = tables.ClusterTable @@ -308,7 +308,7 @@ class ClusterRemoveDevicesView(generic.ObjectEditView): # class VirtualMachineListView(generic.ObjectListView): - queryset = VirtualMachine.objects.all() + queryset = VirtualMachine.objects.prefetch_related('primary_ip4', 'primary_ip6') filterset = filtersets.VirtualMachineFilterSet filterset_form = forms.VirtualMachineFilterForm table = tables.VirtualMachineTable @@ -334,7 +334,8 @@ class VirtualMachineView(generic.ObjectView): services = Service.objects.restrict(request.user, 'view').filter( virtual_machine=instance ).prefetch_related( - Prefetch('ipaddresses', queryset=IPAddress.objects.restrict(request.user)) + Prefetch('ipaddresses', queryset=IPAddress.objects.restrict(request.user)), + 'virtual_machine' ) return { @@ -383,14 +384,14 @@ class VirtualMachineBulkImportView(generic.BulkImportView): class VirtualMachineBulkEditView(generic.BulkEditView): - queryset = VirtualMachine.objects.prefetch_related('cluster', 'tenant', 'role') + queryset = VirtualMachine.objects.prefetch_related('primary_ip4', 'primary_ip6') filterset = filtersets.VirtualMachineFilterSet table = tables.VirtualMachineTable form = forms.VirtualMachineBulkEditForm class VirtualMachineBulkDeleteView(generic.BulkDeleteView): - queryset = VirtualMachine.objects.prefetch_related('cluster', 'tenant', 'role') + queryset = VirtualMachine.objects.prefetch_related('primary_ip4', 'primary_ip6') filterset = filtersets.VirtualMachineFilterSet table = tables.VirtualMachineTable @@ -413,7 +414,7 @@ class VMInterfaceView(generic.ObjectView): def get_extra_context(self, request, instance): # Get assigned IP addresses ipaddress_table = AssignedIPAddressesTable( - data=instance.ip_addresses.restrict(request.user, 'view').prefetch_related('vrf', 'tenant'), + data=instance.ip_addresses.restrict(request.user, 'view'), orderable=False ) From 5ab03b7e926506a7ae949982bb252d62133fb7eb Mon Sep 17 00:00:00 2001 From: atownson <52260120+atownson@users.noreply.github.com> Date: Mon, 1 Aug 2022 08:01:18 -0500 Subject: [PATCH 318/593] Closes #9637: Add existing fields to the Rack Reservation user interface pages (#9870) * Closes netbox-community#9637 Added site_group to RackReservationFrom class fieldsets Added location to RackReservationTable class --- netbox/dcim/forms/filtersets.py | 2 +- netbox/dcim/forms/models.py | 2 +- netbox/dcim/tables/racks.py | 6 +++++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/netbox/dcim/forms/filtersets.py b/netbox/dcim/forms/filtersets.py index 38221b371..9a9f91ffb 100644 --- a/netbox/dcim/forms/filtersets.py +++ b/netbox/dcim/forms/filtersets.py @@ -287,7 +287,7 @@ class RackReservationFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): fieldsets = ( (None, ('q', 'tag')), ('User', ('user_id',)), - ('Rack', ('region_id', 'site_group_id', 'site_id', 'location_id')), + ('Location', ('region_id', 'site_group_id', 'site_id', 'location_id')), ('Tenant', ('tenant_group_id', 'tenant_id')), ) region_id = DynamicModelMultipleChoiceField( diff --git a/netbox/dcim/forms/models.py b/netbox/dcim/forms/models.py index 043af751d..fb09b9871 100644 --- a/netbox/dcim/forms/models.py +++ b/netbox/dcim/forms/models.py @@ -321,7 +321,7 @@ class RackReservationForm(TenancyForm, NetBoxModelForm): ) fieldsets = ( - ('Reservation', ('region', 'site', 'location', 'rack', 'units', 'user', 'description', 'tags')), + ('Reservation', ('region', 'site_group', 'site', 'location', 'rack', 'units', 'user', 'description', 'tags')), ('Tenancy', ('tenant_group', 'tenant')), ) diff --git a/netbox/dcim/tables/racks.py b/netbox/dcim/tables/racks.py index 5412e2297..d83f25a5f 100644 --- a/netbox/dcim/tables/racks.py +++ b/netbox/dcim/tables/racks.py @@ -109,6 +109,10 @@ class RackReservationTable(TenancyColumnsMixin, NetBoxTable): accessor=Accessor('rack__site'), linkify=True ) + location = tables.Column( + accessor=Accessor('rack__location'), + linkify=True + ) rack = tables.Column( linkify=True ) @@ -123,7 +127,7 @@ class RackReservationTable(TenancyColumnsMixin, NetBoxTable): class Meta(NetBoxTable.Meta): model = RackReservation fields = ( - 'pk', 'id', 'reservation', 'site', 'rack', 'unit_list', 'user', 'created', 'tenant', 'tenant_group', 'description', 'tags', + 'pk', 'id', 'reservation', 'site', 'location', 'rack', 'unit_list', 'user', 'created', 'tenant', 'tenant_group', 'description', 'tags', 'actions', 'created', 'last_updated', ) default_columns = ('pk', 'reservation', 'site', 'rack', 'unit_list', 'user', 'description') From 728ad5162466fa805a428553a621bd78134ba61b Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 1 Aug 2022 09:12:15 -0400 Subject: [PATCH 319/593] Changelog & cleanup for #9637 --- docs/release-notes/version-3.2.md | 1 + netbox/dcim/forms/filtersets.py | 35 +++++++++++++++++++++---------- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/docs/release-notes/version-3.2.md b/docs/release-notes/version-3.2.md index ce8552b0b..57a6dffe1 100644 --- a/docs/release-notes/version-3.2.md +++ b/docs/release-notes/version-3.2.md @@ -5,6 +5,7 @@ ### Enhancements * [#9062](https://github.com/netbox-community/netbox/issues/9062) - Add/edit {module} substitution to help text for component template name +* [#9637](https://github.com/netbox-community/netbox/issues/9637) - Add site group field to rack reservation form * [#9762](https://github.com/netbox-community/netbox/issues/9762) - Add `nat_outside` column to the IPAddress table * [#9825](https://github.com/netbox-community/netbox/issues/9825) - Add contacts column to virtual machines table diff --git a/netbox/dcim/forms/filtersets.py b/netbox/dcim/forms/filtersets.py index 9a9f91ffb..12905aec9 100644 --- a/netbox/dcim/forms/filtersets.py +++ b/netbox/dcim/forms/filtersets.py @@ -287,7 +287,7 @@ class RackReservationFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): fieldsets = ( (None, ('q', 'tag')), ('User', ('user_id',)), - ('Location', ('region_id', 'site_group_id', 'site_id', 'location_id')), + ('Rack', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')), ('Tenant', ('tenant_group_id', 'tenant_id')), ) region_id = DynamicModelMultipleChoiceField( @@ -295,25 +295,38 @@ class RackReservationFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): required=False, label=_('Region') ) - site_id = DynamicModelMultipleChoiceField( - queryset=Site.objects.all(), - required=False, - query_params={ - 'region_id': '$region_id' - }, - label=_('Site') - ) site_group_id = DynamicModelMultipleChoiceField( queryset=SiteGroup.objects.all(), required=False, label=_('Site group') ) - location_id = DynamicModelMultipleChoiceField( - queryset=Location.objects.prefetch_related('site'), + site_id = DynamicModelMultipleChoiceField( + queryset=Site.objects.all(), required=False, + query_params={ + 'region_id': '$region_id', + 'group_id': '$site_group_id', + }, + label=_('Site') + ) + location_id = DynamicModelMultipleChoiceField( + queryset=Location.objects.all(), + required=False, + query_params={ + 'site_id': '$site_id', + }, label=_('Location'), null_option='None' ) + rack_id = DynamicModelMultipleChoiceField( + queryset=Rack.objects.all(), + required=False, + query_params={ + 'site_id': '$site_id', + 'location_id': '$location_id', + }, + label=_('Rack') + ) user_id = DynamicModelMultipleChoiceField( queryset=User.objects.all(), required=False, From 84f2225f42f16ca64777172135eba2689db958f3 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 1 Aug 2022 09:16:58 -0400 Subject: [PATCH 320/593] PEP8 cleanup --- netbox/dcim/tests/test_models.py | 4 ++-- netbox/extras/models/customfields.py | 2 +- netbox/extras/tests/test_customfields.py | 2 +- netbox/extras/tests/test_registry.py | 2 +- netbox/ipam/forms/models.py | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/netbox/dcim/tests/test_models.py b/netbox/dcim/tests/test_models.py index 8566f969b..03438a441 100644 --- a/netbox/dcim/tests/test_models.py +++ b/netbox/dcim/tests/test_models.py @@ -194,14 +194,14 @@ class RackTestCase(TestCase): # Validate inventory (front face) rack1_inventory_front = self.rack.get_rack_units(face=DeviceFaceChoices.FACE_FRONT) self.assertEqual(rack1_inventory_front[-10]['device'], device1) - del(rack1_inventory_front[-10]) + del rack1_inventory_front[-10] for u in rack1_inventory_front: self.assertIsNone(u['device']) # Validate inventory (rear face) rack1_inventory_rear = self.rack.get_rack_units(face=DeviceFaceChoices.FACE_REAR) self.assertEqual(rack1_inventory_rear[-10]['device'], device1) - del(rack1_inventory_rear[-10]) + del rack1_inventory_rear[-10] for u in rack1_inventory_rear: self.assertIsNone(u['device']) diff --git a/netbox/extras/models/customfields.py b/netbox/extras/models/customfields.py index 6a8c1dacf..b7d77e550 100644 --- a/netbox/extras/models/customfields.py +++ b/netbox/extras/models/customfields.py @@ -169,7 +169,7 @@ class CustomField(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel): model = ct.model_class() instances = model.objects.filter(**{f'custom_field_data__{self.name}__isnull': False}) for instance in instances: - del(instance.custom_field_data[self.name]) + del instance.custom_field_data[self.name] model.objects.bulk_update(instances, ['custom_field_data'], batch_size=100) def rename_object_data(self, old_name, new_name): diff --git a/netbox/extras/tests/test_customfields.py b/netbox/extras/tests/test_customfields.py index 8dcb53b09..946999bc2 100644 --- a/netbox/extras/tests/test_customfields.py +++ b/netbox/extras/tests/test_customfields.py @@ -992,7 +992,7 @@ class CustomFieldModelTest(TestCase): with self.assertRaises(ValidationError): site.clean() - del(site.cf['bar']) + del site.cf['bar'] site.clean() def test_missing_required_field(self): diff --git a/netbox/extras/tests/test_registry.py b/netbox/extras/tests/test_registry.py index 53ba6584a..38a6b9f83 100644 --- a/netbox/extras/tests/test_registry.py +++ b/netbox/extras/tests/test_registry.py @@ -30,4 +30,4 @@ class RegistryTest(TestCase): reg['foo'] = 123 with self.assertRaises(TypeError): - del(reg['foo']) + del reg['foo'] diff --git a/netbox/ipam/forms/models.py b/netbox/ipam/forms/models.py index e86abc672..d3421f22b 100644 --- a/netbox/ipam/forms/models.py +++ b/netbox/ipam/forms/models.py @@ -848,7 +848,7 @@ class ServiceCreateForm(ServiceForm): # Fields which may be populated from a ServiceTemplate are not required for field in ('name', 'protocol', 'ports'): self.fields[field].required = False - del(self.fields[field].widget.attrs['required']) + del self.fields[field].widget.attrs['required'] def clean(self): if self.cleaned_data['service_template']: From 9fe5f09742b8d9ed0a0ddc8ea57a7ad21884726b Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 1 Aug 2022 09:32:52 -0400 Subject: [PATCH 321/593] Fixes #9891: Ensure consistent ordering for tags during object serialization --- docs/release-notes/version-3.2.md | 6 +++++- netbox/utilities/utils.py | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/release-notes/version-3.2.md b/docs/release-notes/version-3.2.md index 57a6dffe1..ed08ad891 100644 --- a/docs/release-notes/version-3.2.md +++ b/docs/release-notes/version-3.2.md @@ -4,11 +4,15 @@ ### Enhancements -* [#9062](https://github.com/netbox-community/netbox/issues/9062) - Add/edit {module} substitution to help text for component template name +* [#9062](https://github.com/netbox-community/netbox/issues/9062) - Add/edit {module} substitution to help text for component template name * [#9637](https://github.com/netbox-community/netbox/issues/9637) - Add site group field to rack reservation form * [#9762](https://github.com/netbox-community/netbox/issues/9762) - Add `nat_outside` column to the IPAddress table * [#9825](https://github.com/netbox-community/netbox/issues/9825) - Add contacts column to virtual machines table +### Bug Fixes + +* [#9891](https://github.com/netbox-community/netbox/issues/9891) - Ensure consistent ordering for tags during object serialization + --- ## v3.2.7 (2022-07-20) diff --git a/netbox/utilities/utils.py b/netbox/utilities/utils.py index 2b939471c..da7cdde94 100644 --- a/netbox/utilities/utils.py +++ b/netbox/utilities/utils.py @@ -148,7 +148,7 @@ def serialize_object(obj, extra=None): # Include any tags. Check for tags cached on the instance; fall back to using the manager. if is_taggable(obj): tags = getattr(obj, '_tags', None) or obj.tags.all() - data['tags'] = [tag.name for tag in tags] + data['tags'] = sorted([tag.name for tag in tags]) # Append any extra data if extra is not None: From 8a075bcff9b3a75ae86cd21eeb3921108594802f Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 1 Aug 2022 09:47:18 -0400 Subject: [PATCH 322/593] Fixes #9884: Prevent querying assigned VRF on prefix object init --- docs/release-notes/version-3.2.md | 1 + netbox/ipam/models/ip.py | 2 +- netbox/ipam/signals.py | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/release-notes/version-3.2.md b/docs/release-notes/version-3.2.md index ed08ad891..caaf079b2 100644 --- a/docs/release-notes/version-3.2.md +++ b/docs/release-notes/version-3.2.md @@ -11,6 +11,7 @@ ### Bug Fixes +* [#9884](https://github.com/netbox-community/netbox/issues/9884) - Prevent querying assigned VRF on prefix object init * [#9891](https://github.com/netbox-community/netbox/issues/9891) - Ensure consistent ordering for tags during object serialization --- diff --git a/netbox/ipam/models/ip.py b/netbox/ipam/models/ip.py index a3b8fb2c1..d1538953a 100644 --- a/netbox/ipam/models/ip.py +++ b/netbox/ipam/models/ip.py @@ -373,7 +373,7 @@ class Prefix(GetAvailablePrefixesMixin, NetBoxModel): # Cache the original prefix and VRF so we can check if they have changed on post_save self._prefix = self.prefix - self._vrf = self.vrf + self._vrf_id = self.vrf_id def __str__(self): return str(self.prefix) diff --git a/netbox/ipam/signals.py b/netbox/ipam/signals.py index 3e8b86050..8555f5e67 100644 --- a/netbox/ipam/signals.py +++ b/netbox/ipam/signals.py @@ -30,14 +30,14 @@ def update_children_depth(prefix): def handle_prefix_saved(instance, created, **kwargs): # Prefix has changed (or new instance has been created) - if created or instance.vrf != instance._vrf or instance.prefix != instance._prefix: + if created or instance.vrf_id != instance._vrf_id or instance.prefix != instance._prefix: update_parents_children(instance) update_children_depth(instance) # If this is not a new prefix, clean up parent/children of previous prefix if not created: - old_prefix = Prefix(vrf=instance._vrf, prefix=instance._prefix) + old_prefix = Prefix(vrf_id=instance._vrf_id, prefix=instance._prefix) update_parents_children(old_prefix) update_children_depth(old_prefix) From 1bbf5d214b3c58d97c0c600ca2cf4c929f588e5e Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 1 Aug 2022 10:23:18 -0400 Subject: [PATCH 323/593] Closes #9881: Increase granularity in utilization graph values --- docs/release-notes/version-3.2.md | 1 + netbox/utilities/templates/helpers/utilization_graph.html | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/release-notes/version-3.2.md b/docs/release-notes/version-3.2.md index caaf079b2..b366d7c8f 100644 --- a/docs/release-notes/version-3.2.md +++ b/docs/release-notes/version-3.2.md @@ -8,6 +8,7 @@ * [#9637](https://github.com/netbox-community/netbox/issues/9637) - Add site group field to rack reservation form * [#9762](https://github.com/netbox-community/netbox/issues/9762) - Add `nat_outside` column to the IPAddress table * [#9825](https://github.com/netbox-community/netbox/issues/9825) - Add contacts column to virtual machines table +* [#9881](https://github.com/netbox-community/netbox/issues/9881) - Increase granularity in utilization graph values ### Bug Fixes diff --git a/netbox/utilities/templates/helpers/utilization_graph.html b/netbox/utilities/templates/helpers/utilization_graph.html index e6829befc..08e4e6f8a 100644 --- a/netbox/utilities/templates/helpers/utilization_graph.html +++ b/netbox/utilities/templates/helpers/utilization_graph.html @@ -12,10 +12,10 @@ class="progress-bar {{ bar_class }}" style="width: {{ utilization }}%;" > - {% if utilization >= 25 %}{{ utilization|floatformat:0 }}%{% endif %} + {% if utilization >= 25 %}{{ utilization|floatformat }}%{% endif %} {% if utilization < 25 %} - {{ utilization|floatformat:0 }}% + {{ utilization|floatformat }}% {% endif %} {% endif %} From 9646f88384360855fdc000156920eb3365d68d81 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 1 Aug 2022 10:28:21 -0400 Subject: [PATCH 324/593] Fixes #9885: Fix child prefix counts when editing/deleting aggregates in bulk --- docs/release-notes/version-3.2.md | 1 + netbox/ipam/views.py | 8 ++++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/release-notes/version-3.2.md b/docs/release-notes/version-3.2.md index b366d7c8f..8a57f4644 100644 --- a/docs/release-notes/version-3.2.md +++ b/docs/release-notes/version-3.2.md @@ -13,6 +13,7 @@ ### Bug Fixes * [#9884](https://github.com/netbox-community/netbox/issues/9884) - Prevent querying assigned VRF on prefix object init +* [#9885](https://github.com/netbox-community/netbox/issues/9885) - Fix child prefix counts when editing/deleting aggregates in bulk * [#9891](https://github.com/netbox-community/netbox/issues/9891) - Ensure consistent ordering for tags during object serialization --- diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index 706670cad..9ae7cd4d7 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -333,14 +333,18 @@ class AggregateBulkImportView(generic.BulkImportView): class AggregateBulkEditView(generic.BulkEditView): - queryset = Aggregate.objects.prefetch_related('rir') + queryset = Aggregate.objects.annotate( + child_count=RawSQL('SELECT COUNT(*) FROM ipam_prefix WHERE ipam_prefix.prefix <<= ipam_aggregate.prefix', ()) + ) filterset = filtersets.AggregateFilterSet table = tables.AggregateTable form = forms.AggregateBulkEditForm class AggregateBulkDeleteView(generic.BulkDeleteView): - queryset = Aggregate.objects.prefetch_related('rir') + queryset = Aggregate.objects.annotate( + child_count=RawSQL('SELECT COUNT(*) FROM ipam_prefix WHERE ipam_prefix.prefix <<= ipam_aggregate.prefix', ()) + ) filterset = filtersets.AggregateFilterSet table = tables.AggregateTable From 3af989763e41fefdfbee1806c7e4c163e148db9b Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 1 Aug 2022 10:32:05 -0400 Subject: [PATCH 325/593] Closes #9883: Linkify location column in power panels table --- docs/release-notes/version-3.2.md | 1 + netbox/dcim/tables/power.py | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/version-3.2.md b/docs/release-notes/version-3.2.md index 8a57f4644..e4c7d9268 100644 --- a/docs/release-notes/version-3.2.md +++ b/docs/release-notes/version-3.2.md @@ -9,6 +9,7 @@ * [#9762](https://github.com/netbox-community/netbox/issues/9762) - Add `nat_outside` column to the IPAddress table * [#9825](https://github.com/netbox-community/netbox/issues/9825) - Add contacts column to virtual machines table * [#9881](https://github.com/netbox-community/netbox/issues/9881) - Increase granularity in utilization graph values +* [#9883](https://github.com/netbox-community/netbox/issues/9883) - Linkify location column in power panels table ### Bug Fixes diff --git a/netbox/dcim/tables/power.py b/netbox/dcim/tables/power.py index 92c4bb0aa..6696d516a 100644 --- a/netbox/dcim/tables/power.py +++ b/netbox/dcim/tables/power.py @@ -21,6 +21,9 @@ class PowerPanelTable(NetBoxTable): site = tables.Column( linkify=True ) + location = tables.Column( + linkify=True + ) powerfeed_count = columns.LinkedCountColumn( viewname='dcim:powerfeed_list', url_params={'power_panel_id': 'pk'}, @@ -35,7 +38,9 @@ class PowerPanelTable(NetBoxTable): class Meta(NetBoxTable.Meta): model = PowerPanel - fields = ('pk', 'id', 'name', 'site', 'location', 'powerfeed_count', 'contacts', 'tags', 'created', 'last_updated',) + fields = ( + 'pk', 'id', 'name', 'site', 'location', 'powerfeed_count', 'contacts', 'tags', 'created', 'last_updated', + ) default_columns = ('pk', 'name', 'site', 'location', 'powerfeed_count') From efa449faff99205a351553770e7e5186b487b754 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 1 Aug 2022 10:36:53 -0400 Subject: [PATCH 326/593] Closes #9882: Add manufacturer column to modules table --- docs/release-notes/version-3.2.md | 1 + netbox/dcim/tables/modules.py | 12 ++++++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/docs/release-notes/version-3.2.md b/docs/release-notes/version-3.2.md index e4c7d9268..64e229129 100644 --- a/docs/release-notes/version-3.2.md +++ b/docs/release-notes/version-3.2.md @@ -9,6 +9,7 @@ * [#9762](https://github.com/netbox-community/netbox/issues/9762) - Add `nat_outside` column to the IPAddress table * [#9825](https://github.com/netbox-community/netbox/issues/9825) - Add contacts column to virtual machines table * [#9881](https://github.com/netbox-community/netbox/issues/9881) - Increase granularity in utilization graph values +* [#9882](https://github.com/netbox-community/netbox/issues/9882) - Add manufacturer column to modules table * [#9883](https://github.com/netbox-community/netbox/issues/9883) - Linkify location column in power panels table ### Bug Fixes diff --git a/netbox/dcim/tables/modules.py b/netbox/dcim/tables/modules.py index 5b009e42e..e40d7bd80 100644 --- a/netbox/dcim/tables/modules.py +++ b/netbox/dcim/tables/modules.py @@ -14,6 +14,9 @@ class ModuleTypeTable(NetBoxTable): linkify=True, verbose_name='Module Type' ) + manufacturer = tables.Column( + linkify=True + ) instance_count = columns.LinkedCountColumn( viewname='dcim:module_list', url_params={'module_type_id': 'pk'}, @@ -41,6 +44,10 @@ class ModuleTable(NetBoxTable): module_bay = tables.Column( linkify=True ) + manufacturer = tables.Column( + accessor=tables.A('module_type__manufacturer'), + linkify=True + ) module_type = tables.Column( linkify=True ) @@ -52,8 +59,9 @@ class ModuleTable(NetBoxTable): class Meta(NetBoxTable.Meta): model = Module fields = ( - 'pk', 'id', 'device', 'module_bay', 'module_type', 'serial', 'asset_tag', 'comments', 'tags', + 'pk', 'id', 'device', 'module_bay', 'manufacturer', 'module_type', 'serial', 'asset_tag', 'comments', + 'tags', ) default_columns = ( - 'pk', 'id', 'device', 'module_bay', 'module_type', 'serial', 'asset_tag', + 'pk', 'id', 'device', 'module_bay', 'manufacturer', 'module_type', 'serial', 'asset_tag', ) From 984d15d7fb637f7e5a73fa2138637d11a76f524b Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 1 Aug 2022 11:39:07 -0400 Subject: [PATCH 327/593] Closes #9893: Move YAML serialization logic for component templates under the individual models --- .../dcim/models/device_component_templates.py | 79 ++++++++ netbox/dcim/models/devices.py | 179 ++++-------------- 2 files changed, 112 insertions(+), 146 deletions(-) diff --git a/netbox/dcim/models/device_component_templates.py b/netbox/dcim/models/device_component_templates.py index ac0738b3f..f596cf28b 100644 --- a/netbox/dcim/models/device_component_templates.py +++ b/netbox/dcim/models/device_component_templates.py @@ -160,6 +160,14 @@ class ConsolePortTemplate(ModularComponentTemplateModel): **kwargs ) + def to_yaml(self): + return { + 'name': self.name, + 'type': self.type, + 'label': self.label, + 'description': self.description, + } + class ConsoleServerPortTemplate(ModularComponentTemplateModel): """ @@ -188,6 +196,14 @@ class ConsoleServerPortTemplate(ModularComponentTemplateModel): **kwargs ) + def to_yaml(self): + return { + 'name': self.name, + 'type': self.type, + 'label': self.label, + 'description': self.description, + } + class PowerPortTemplate(ModularComponentTemplateModel): """ @@ -239,6 +255,16 @@ class PowerPortTemplate(ModularComponentTemplateModel): 'allocated_draw': f"Allocated draw cannot exceed the maximum draw ({self.maximum_draw}W)." }) + def to_yaml(self): + return { + 'name': self.name, + 'type': self.type, + 'maximum_draw': self.maximum_draw, + 'allocated_draw': self.allocated_draw, + 'label': self.label, + 'description': self.description, + } + class PowerOutletTemplate(ModularComponentTemplateModel): """ @@ -301,6 +327,16 @@ class PowerOutletTemplate(ModularComponentTemplateModel): **kwargs ) + def to_yaml(self): + return { + 'name': self.name, + 'type': self.type, + 'power_port': self.power_port.name if self.power_port else None, + 'feed_leg': self.feed_leg, + 'label': self.label, + 'description': self.description, + } + class InterfaceTemplate(ModularComponentTemplateModel): """ @@ -340,6 +376,15 @@ class InterfaceTemplate(ModularComponentTemplateModel): **kwargs ) + def to_yaml(self): + return { + 'name': self.name, + 'type': self.type, + 'mgmt_only': self.mgmt_only, + 'label': self.label, + 'description': self.description, + } + class FrontPortTemplate(ModularComponentTemplateModel): """ @@ -413,6 +458,16 @@ class FrontPortTemplate(ModularComponentTemplateModel): **kwargs ) + def to_yaml(self): + return { + 'name': self.name, + 'type': self.type, + 'rear_port': self.rear_port.name, + 'rear_port_position': self.rear_port_position, + 'label': self.label, + 'description': self.description, + } + class RearPortTemplate(ModularComponentTemplateModel): """ @@ -452,6 +507,15 @@ class RearPortTemplate(ModularComponentTemplateModel): **kwargs ) + def to_yaml(self): + return { + 'name': self.name, + 'type': self.type, + 'positions': self.positions, + 'label': self.label, + 'description': self.description, + } + class ModuleBayTemplate(ComponentTemplateModel): """ @@ -477,6 +541,14 @@ class ModuleBayTemplate(ComponentTemplateModel): position=self.position ) + def to_yaml(self): + return { + 'name': self.name, + 'label': self.label, + 'position': self.position, + 'description': self.description, + } + class DeviceBayTemplate(ComponentTemplateModel): """ @@ -501,6 +573,13 @@ class DeviceBayTemplate(ComponentTemplateModel): f"Subdevice role of device type ({self.device_type}) must be set to \"parent\" to allow device bays." ) + def to_yaml(self): + return { + 'name': self.name, + 'label': self.label, + 'description': self.description, + } + class InventoryItemTemplate(MPTTModel, ComponentTemplateModel): """ diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py index e88af2d05..91227f1cf 100644 --- a/netbox/dcim/models/devices.py +++ b/netbox/dcim/models/devices.py @@ -1,5 +1,3 @@ -from collections import OrderedDict - import yaml from django.contrib.contenttypes.fields import GenericRelation from django.core.exceptions import ValidationError @@ -161,115 +159,54 @@ class DeviceType(NetBoxModel): return reverse('dcim:devicetype', args=[self.pk]) def to_yaml(self): - data = OrderedDict(( - ('manufacturer', self.manufacturer.name), - ('model', self.model), - ('slug', self.slug), - ('part_number', self.part_number), - ('u_height', self.u_height), - ('is_full_depth', self.is_full_depth), - ('subdevice_role', self.subdevice_role), - ('airflow', self.airflow), - ('comments', self.comments), - )) + data = { + 'manufacturer': self.manufacturer.name, + 'model': self.model, + 'slug': self.slug, + 'part_number': self.part_number, + 'u_height': self.u_height, + 'is_full_depth': self.is_full_depth, + 'subdevice_role': self.subdevice_role, + 'airflow': self.airflow, + 'comments': self.comments, + } # Component templates if self.consoleporttemplates.exists(): data['console-ports'] = [ - { - 'name': c.name, - 'type': c.type, - 'label': c.label, - 'description': c.description, - } - for c in self.consoleporttemplates.all() + c.to_yaml() for c in self.consoleporttemplates.all() ] if self.consoleserverporttemplates.exists(): data['console-server-ports'] = [ - { - 'name': c.name, - 'type': c.type, - 'label': c.label, - 'description': c.description, - } - for c in self.consoleserverporttemplates.all() + c.to_yaml() for c in self.consoleserverporttemplates.all() ] if self.powerporttemplates.exists(): data['power-ports'] = [ - { - 'name': c.name, - 'type': c.type, - 'maximum_draw': c.maximum_draw, - 'allocated_draw': c.allocated_draw, - 'label': c.label, - 'description': c.description, - } - for c in self.powerporttemplates.all() + c.to_yaml() for c in self.powerporttemplates.all() ] if self.poweroutlettemplates.exists(): data['power-outlets'] = [ - { - 'name': c.name, - 'type': c.type, - 'power_port': c.power_port.name if c.power_port else None, - 'feed_leg': c.feed_leg, - 'label': c.label, - 'description': c.description, - } - for c in self.poweroutlettemplates.all() + c.to_yaml() for c in self.poweroutlettemplates.all() ] if self.interfacetemplates.exists(): data['interfaces'] = [ - { - 'name': c.name, - 'type': c.type, - 'mgmt_only': c.mgmt_only, - 'label': c.label, - 'description': c.description, - } - for c in self.interfacetemplates.all() + c.to_yaml() for c in self.interfacetemplates.all() ] if self.frontporttemplates.exists(): data['front-ports'] = [ - { - 'name': c.name, - 'type': c.type, - 'rear_port': c.rear_port.name, - 'rear_port_position': c.rear_port_position, - 'label': c.label, - 'description': c.description, - } - for c in self.frontporttemplates.all() + c.to_yaml() for c in self.frontporttemplates.all() ] if self.rearporttemplates.exists(): data['rear-ports'] = [ - { - 'name': c.name, - 'type': c.type, - 'positions': c.positions, - 'label': c.label, - 'description': c.description, - } - for c in self.rearporttemplates.all() + c.to_yaml() for c in self.rearporttemplates.all() ] if self.modulebaytemplates.exists(): data['module-bays'] = [ - { - 'name': c.name, - 'label': c.label, - 'position': c.position, - 'description': c.description, - } - for c in self.modulebaytemplates.all() + c.to_yaml() for c in self.modulebaytemplates.all() ] if self.devicebaytemplates.exists(): data['device-bays'] = [ - { - 'name': c.name, - 'label': c.label, - 'description': c.description, - } - for c in self.devicebaytemplates.all() + c.to_yaml() for c in self.devicebaytemplates.all() ] return yaml.dump(dict(data), sort_keys=False) @@ -395,91 +332,41 @@ class ModuleType(NetBoxModel): return reverse('dcim:moduletype', args=[self.pk]) def to_yaml(self): - data = OrderedDict(( - ('manufacturer', self.manufacturer.name), - ('model', self.model), - ('part_number', self.part_number), - ('comments', self.comments), - )) + data = { + 'manufacturer': self.manufacturer.name, + 'model': self.model, + 'part_number': self.part_number, + 'comments': self.comments, + } # Component templates if self.consoleporttemplates.exists(): data['console-ports'] = [ - { - 'name': c.name, - 'type': c.type, - 'label': c.label, - 'description': c.description, - } - for c in self.consoleporttemplates.all() + c.to_yaml() for c in self.consoleporttemplates.all() ] if self.consoleserverporttemplates.exists(): data['console-server-ports'] = [ - { - 'name': c.name, - 'type': c.type, - 'label': c.label, - 'description': c.description, - } - for c in self.consoleserverporttemplates.all() + c.to_yaml() for c in self.consoleserverporttemplates.all() ] if self.powerporttemplates.exists(): data['power-ports'] = [ - { - 'name': c.name, - 'type': c.type, - 'maximum_draw': c.maximum_draw, - 'allocated_draw': c.allocated_draw, - 'label': c.label, - 'description': c.description, - } - for c in self.powerporttemplates.all() + c.to_yaml() for c in self.powerporttemplates.all() ] if self.poweroutlettemplates.exists(): data['power-outlets'] = [ - { - 'name': c.name, - 'type': c.type, - 'power_port': c.power_port.name if c.power_port else None, - 'feed_leg': c.feed_leg, - 'label': c.label, - 'description': c.description, - } - for c in self.poweroutlettemplates.all() + c.to_yaml() for c in self.poweroutlettemplates.all() ] if self.interfacetemplates.exists(): data['interfaces'] = [ - { - 'name': c.name, - 'type': c.type, - 'mgmt_only': c.mgmt_only, - 'label': c.label, - 'description': c.description, - } - for c in self.interfacetemplates.all() + c.to_yaml() for c in self.interfacetemplates.all() ] if self.frontporttemplates.exists(): data['front-ports'] = [ - { - 'name': c.name, - 'type': c.type, - 'rear_port': c.rear_port.name, - 'rear_port_position': c.rear_port_position, - 'label': c.label, - 'description': c.description, - } - for c in self.frontporttemplates.all() + c.to_yaml() for c in self.frontporttemplates.all() ] if self.rearporttemplates.exists(): data['rear-ports'] = [ - { - 'name': c.name, - 'type': c.type, - 'positions': c.positions, - 'label': c.label, - 'description': c.description, - } - for c in self.rearporttemplates.all() + c.to_yaml() for c in self.rearporttemplates.all() ] return yaml.dump(dict(data), sort_keys=False) From d4d73674fced54570db111574dae0a080f89ce77 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 1 Aug 2022 11:54:39 -0400 Subject: [PATCH 328/593] Fixes #9871: Fix utilization graph value alignments --- docs/release-notes/version-3.2.md | 1 + .../templates/helpers/utilization_graph.html | 34 ++++++++----------- 2 files changed, 15 insertions(+), 20 deletions(-) diff --git a/docs/release-notes/version-3.2.md b/docs/release-notes/version-3.2.md index 64e229129..5e538890c 100644 --- a/docs/release-notes/version-3.2.md +++ b/docs/release-notes/version-3.2.md @@ -14,6 +14,7 @@ ### Bug Fixes +* [#9871](https://github.com/netbox-community/netbox/issues/9871) - Fix utilization graph value alignments * [#9884](https://github.com/netbox-community/netbox/issues/9884) - Prevent querying assigned VRF on prefix object init * [#9885](https://github.com/netbox-community/netbox/issues/9885) - Fix child prefix counts when editing/deleting aggregates in bulk * [#9891](https://github.com/netbox-community/netbox/issues/9891) - Ensure consistent ordering for tags during object serialization diff --git a/netbox/utilities/templates/helpers/utilization_graph.html b/netbox/utilities/templates/helpers/utilization_graph.html index 08e4e6f8a..2a1856abf 100644 --- a/netbox/utilities/templates/helpers/utilization_graph.html +++ b/netbox/utilities/templates/helpers/utilization_graph.html @@ -1,21 +1,15 @@ -{% if utilization == 0 %} -
    - {{ utilization }}% +
    +
    + {% if utilization >= 25 %}{{ utilization|floatformat:1 }}%{% endif %}
    -{% else %} -
    -
    - {% if utilization >= 25 %}{{ utilization|floatformat }}%{% endif %} -
    - {% if utilization < 25 %} - {{ utilization|floatformat }}% - {% endif %} -
    -{% endif %} + {% if utilization < 25 %} + {{ utilization|floatformat:1 }}% + {% endif %} +
    From ff3fcb8134c229033453afadcc1e315c52f3fd20 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 1 Aug 2022 12:38:12 -0400 Subject: [PATCH 329/593] #9871: Tweak display of utilization graph value --- netbox/utilities/templates/helpers/utilization_graph.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/netbox/utilities/templates/helpers/utilization_graph.html b/netbox/utilities/templates/helpers/utilization_graph.html index 2a1856abf..967ac8a87 100644 --- a/netbox/utilities/templates/helpers/utilization_graph.html +++ b/netbox/utilities/templates/helpers/utilization_graph.html @@ -7,9 +7,9 @@ class="progress-bar {{ bar_class }}" style="width: {{ utilization }}%;" > - {% if utilization >= 25 %}{{ utilization|floatformat:1 }}%{% endif %} + {% if utilization >= 35 %}{{ utilization|floatformat:1 }}%{% endif %}
    - {% if utilization < 25 %} + {% if utilization < 35 %} {{ utilization|floatformat:1 }}% {% endif %} From 262a0cf397e1bf2cee4d11567b747c163161a838 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 1 Aug 2022 13:29:39 -0400 Subject: [PATCH 330/593] Fixes #9789: Fix rendering of cable traces ending at provider networks --- docs/release-notes/version-3.3.md | 1 + netbox/dcim/api/views.py | 18 +++++++++--------- netbox/dcim/models/device_components.py | 11 +++++++---- netbox/dcim/svg/cables.py | 6 ++++-- 4 files changed, 21 insertions(+), 15 deletions(-) diff --git a/docs/release-notes/version-3.3.md b/docs/release-notes/version-3.3.md index 68cff0547..e30ff011c 100644 --- a/docs/release-notes/version-3.3.md +++ b/docs/release-notes/version-3.3.md @@ -104,6 +104,7 @@ Custom field UI visibility has no impact on API operation. * [#9730](https://github.com/netbox-community/netbox/issues/9730) - Fix validation error when creating a new cable via UI form * [#9733](https://github.com/netbox-community/netbox/issues/9733) - Handle split paths during trace when fanning out to front ports with differing cables * [#9765](https://github.com/netbox-community/netbox/issues/9765) - Report correct segment count under cable trace UI view +* [#9789](https://github.com/netbox-community/netbox/issues/9789) - Fix rendering of cable traces ending at provider networks * [#9794](https://github.com/netbox-community/netbox/issues/9794) - Fix link to connect a rear port to a circuit termination * [#9818](https://github.com/netbox-community/netbox/issues/9818) - Fix circuit side selection when connecting a cable to a circuit termination * [#9829](https://github.com/netbox-community/netbox/issues/9829) - Arrange custom fields by group when editing objects diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index 59445d97b..32cc3dbba 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -64,20 +64,20 @@ class PathEndpointMixin(object): return HttpResponse(drawing.render().tostring(), content_type='image/svg+xml') # Serialize path objects, iterating over each three-tuple in the path - for near_end, cable, far_end in obj.trace(): - if near_end is not None: - serializer_a = get_serializer_for_model(near_end[0], prefix=NESTED_SERIALIZER_PREFIX) - near_end = serializer_a(near_end, many=True, context={'request': request}).data + for near_ends, cable, far_ends in obj.trace(): + if near_ends: + serializer_a = get_serializer_for_model(near_ends[0], prefix=NESTED_SERIALIZER_PREFIX) + near_ends = serializer_a(near_ends, many=True, context={'request': request}).data else: # Path is split; stop here break - if cable is not None: + if cable: cable = serializers.TracedCableSerializer(cable[0], context={'request': request}).data - if far_end is not None: - serializer_b = get_serializer_for_model(far_end[0], prefix=NESTED_SERIALIZER_PREFIX) - far_end = serializer_b(far_end, many=True, context={'request': request}).data + if far_ends: + serializer_b = get_serializer_for_model(far_ends[0], prefix=NESTED_SERIALIZER_PREFIX) + far_ends = serializer_b(far_ends, many=True, context={'request': request}).data - path.append((near_end, cable, far_end)) + path.append((near_ends, cable, far_ends)) return Response(path) diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index 8f62b0626..5e2fc348e 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -212,10 +212,13 @@ class PathEndpoint(models.Model): break path.extend(origin._path.path_objects) - while (len(path)) % 3: - # Pad to ensure we have complete three-tuples (e.g. for paths that end at a non-connected FrontPort) - # by inserting empty entries immediately prior to the path's destination node(s) - path.append([]) + + # If the path ends at a non-connected pass-through port, pad out the link and far-end terminations + if len(path) % 3 == 1: + path.extend(([], [])) + # If the path ends at a site or provider network, inject a null "link" to render an attachment + elif len(path) % 3 == 2: + path.insert(-1, []) # Check for a bridged relationship to continue the trace destinations = origin._path.destinations diff --git a/netbox/dcim/svg/cables.py b/netbox/dcim/svg/cables.py index f9c614b67..3b259eca2 100644 --- a/netbox/dcim/svg/cables.py +++ b/netbox/dcim/svg/cables.py @@ -369,14 +369,16 @@ class CableTraceSVG: parent_objects = set(end.parent_object for end in far_ends) self.draw_parent_objects(parent_objects) + # Render a far-end object not connected via a link (e.g. a ProviderNetwork or Site associated with + # a CircuitTermination) elif far_ends: # Attachment attachment = self.draw_attachment() self.connectors.append(attachment) - # ProviderNetwork - self.draw_parent_objects(set(end.parent_object for end in far_ends)) + # Object + self.draw_parent_objects(far_ends) # Determine drawing size self.drawing = svgwrite.Drawing( From 29a611c7293741100f3e5351243dffbae8e5b481 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 1 Aug 2022 16:51:44 -0400 Subject: [PATCH 331/593] Closes #9896: Discontinue arbitrary use of OrderedDict --- netbox/dcim/api/views.py | 3 +- netbox/dcim/views.py | 44 +++++++++++----------- netbox/extras/reports.py | 17 ++++----- netbox/extras/scripts.py | 5 +-- netbox/extras/templatetags/custom_links.py | 4 +- netbox/ipam/api/serializers.py | 32 ++++++++-------- netbox/netbox/api/fields.py | 10 ++--- netbox/netbox/api/views.py | 25 ++++++------ netbox/utilities/utils.py | 3 +- 9 files changed, 65 insertions(+), 78 deletions(-) diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index 32cc3dbba..c18eab01f 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -1,5 +1,4 @@ import socket -from collections import OrderedDict from django.http import Http404, HttpResponse, HttpResponseForbidden from django.shortcuts import get_object_or_404 @@ -484,7 +483,7 @@ class DeviceViewSet(ConfigContextQuerySetMixin, NetBoxModelViewSet): return HttpResponseForbidden() napalm_methods = request.GET.getlist('method') - response = OrderedDict([(m, None) for m in napalm_methods]) + response = {m: None for m in napalm_methods} config = get_config() username = config.NAPALM_USERNAME diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 6daecb3a6..4480bee6e 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -1,5 +1,3 @@ -from collections import OrderedDict - from django.contrib import messages from django.contrib.contenttypes.models import ContentType from django.core.paginator import EmptyPage, PageNotAnInteger @@ -945,18 +943,18 @@ class DeviceTypeImportView(generic.ObjectImportView): ] queryset = DeviceType.objects.all() model_form = forms.DeviceTypeImportForm - related_object_forms = OrderedDict(( - ('console-ports', forms.ConsolePortTemplateImportForm), - ('console-server-ports', forms.ConsoleServerPortTemplateImportForm), - ('power-ports', forms.PowerPortTemplateImportForm), - ('power-outlets', forms.PowerOutletTemplateImportForm), - ('interfaces', forms.InterfaceTemplateImportForm), - ('rear-ports', forms.RearPortTemplateImportForm), - ('front-ports', forms.FrontPortTemplateImportForm), - ('module-bays', forms.ModuleBayTemplateImportForm), - ('device-bays', forms.DeviceBayTemplateImportForm), - ('inventory-items', forms.InventoryItemTemplateImportForm), - )) + related_object_forms = { + 'console-ports': forms.ConsolePortTemplateImportForm, + 'console-server-ports': forms.ConsoleServerPortTemplateImportForm, + 'power-ports': forms.PowerPortTemplateImportForm, + 'power-outlets': forms.PowerOutletTemplateImportForm, + 'interfaces': forms.InterfaceTemplateImportForm, + 'rear-ports': forms.RearPortTemplateImportForm, + 'front-ports': forms.FrontPortTemplateImportForm, + 'module-bays': forms.ModuleBayTemplateImportForm, + 'device-bays': forms.DeviceBayTemplateImportForm, + 'inventory-items': forms.InventoryItemTemplateImportForm, + } def prep_related_object_data(self, parent, data): data.update({'device_type': parent}) @@ -1075,15 +1073,15 @@ class ModuleTypeImportView(generic.ObjectImportView): ] queryset = ModuleType.objects.all() model_form = forms.ModuleTypeImportForm - related_object_forms = OrderedDict(( - ('console-ports', forms.ConsolePortTemplateImportForm), - ('console-server-ports', forms.ConsoleServerPortTemplateImportForm), - ('power-ports', forms.PowerPortTemplateImportForm), - ('power-outlets', forms.PowerOutletTemplateImportForm), - ('interfaces', forms.InterfaceTemplateImportForm), - ('rear-ports', forms.RearPortTemplateImportForm), - ('front-ports', forms.FrontPortTemplateImportForm), - )) + related_object_forms = { + 'console-ports': forms.ConsolePortTemplateImportForm, + 'console-server-ports': forms.ConsoleServerPortTemplateImportForm, + 'power-ports': forms.PowerPortTemplateImportForm, + 'power-outlets': forms.PowerOutletTemplateImportForm, + 'interfaces': forms.InterfaceTemplateImportForm, + 'rear-ports': forms.RearPortTemplateImportForm, + 'front-ports': forms.FrontPortTemplateImportForm, + } def prep_related_object_data(self, parent, data): data.update({'module_type': parent}) diff --git a/netbox/extras/reports.py b/netbox/extras/reports.py index 0a8a8d89b..43d916aff 100644 --- a/netbox/extras/reports.py +++ b/netbox/extras/reports.py @@ -3,7 +3,6 @@ import inspect import logging import pkgutil import traceback -from collections import OrderedDict from django.conf import settings from django.utils import timezone @@ -114,7 +113,7 @@ class Report(object): def __init__(self): - self._results = OrderedDict() + self._results = {} self.active_test = None self.failed = False @@ -125,13 +124,13 @@ class Report(object): for method in dir(self): if method.startswith('test_') and callable(getattr(self, method)): test_methods.append(method) - self._results[method] = OrderedDict([ - ('success', 0), - ('info', 0), - ('warning', 0), - ('failure', 0), - ('log', []), - ]) + self._results[method] = { + 'success': 0, + 'info': 0, + 'warning': 0, + 'failure': 0, + 'log': [], + } if not test_methods: raise Exception("A report must contain at least one test method.") self.test_methods = test_methods diff --git a/netbox/extras/scripts.py b/netbox/extras/scripts.py index cee264878..6e4478304 100644 --- a/netbox/extras/scripts.py +++ b/netbox/extras/scripts.py @@ -6,7 +6,6 @@ import pkgutil import sys import traceback import threading -from collections import OrderedDict import yaml from django import forms @@ -496,7 +495,7 @@ def get_scripts(use_names=False): Return a dict of dicts mapping all scripts to their modules. Set use_names to True to use each module's human- defined name in place of the actual module name. """ - scripts = OrderedDict() + scripts = {} # Iterate through all modules within the scripts path. These are the user-created files in which reports are # defined. for importer, module_name, _ in pkgutil.iter_modules([settings.SCRIPTS_ROOT]): @@ -510,7 +509,7 @@ def get_scripts(use_names=False): if use_names and hasattr(module, 'name'): module_name = module.name - module_scripts = OrderedDict() + module_scripts = {} script_order = getattr(module, "script_order", ()) ordered_scripts = [cls for cls in script_order if is_script(cls)] unordered_scripts = [cls for _, cls in inspect.getmembers(module, is_script) if cls not in script_order] diff --git a/netbox/extras/templatetags/custom_links.py b/netbox/extras/templatetags/custom_links.py index d963bd25a..a73eb3fb4 100644 --- a/netbox/extras/templatetags/custom_links.py +++ b/netbox/extras/templatetags/custom_links.py @@ -1,5 +1,3 @@ -from collections import OrderedDict - from django import template from django.contrib.contenttypes.models import ContentType from django.utils.safestring import mark_safe @@ -50,7 +48,7 @@ def custom_links(context, obj): 'perms': context['perms'], # django.contrib.auth.context_processors.auth } template_code = '' - group_names = OrderedDict() + group_names = {} for cl in custom_links: diff --git a/netbox/ipam/api/serializers.py b/netbox/ipam/api/serializers.py index b3a3589fd..91a81d3b2 100644 --- a/netbox/ipam/api/serializers.py +++ b/netbox/ipam/api/serializers.py @@ -1,5 +1,3 @@ -from collections import OrderedDict - from django.contrib.contenttypes.models import ContentType from drf_yasg.utils import swagger_serializer_method from rest_framework import serializers @@ -227,13 +225,13 @@ class AvailableVLANSerializer(serializers.Serializer): group = NestedVLANGroupSerializer(read_only=True) def to_representation(self, instance): - return OrderedDict([ - ('vid', instance), - ('group', NestedVLANGroupSerializer( + return { + 'vid': instance, + 'group': NestedVLANGroupSerializer( self.context['group'], context={'request': self.context['request']} - ).data), - ]) + ).data, + } class CreateAvailableVLANSerializer(NetBoxModelSerializer): @@ -318,11 +316,11 @@ class AvailablePrefixSerializer(serializers.Serializer): vrf = NestedVRFSerializer(self.context['vrf'], context={'request': self.context['request']}).data else: vrf = None - return OrderedDict([ - ('family', instance.version), - ('prefix', str(instance)), - ('vrf', vrf), - ]) + return { + 'family': instance.version, + 'prefix': str(instance), + 'vrf': vrf, + } # @@ -397,11 +395,11 @@ class AvailableIPSerializer(serializers.Serializer): vrf = NestedVRFSerializer(self.context['vrf'], context={'request': self.context['request']}).data else: vrf = None - return OrderedDict([ - ('family', self.context['parent'].family), - ('address', f"{instance}/{self.context['parent'].mask_length}"), - ('vrf', vrf), - ]) + return { + 'family': self.context['parent'].family, + 'address': f"{instance}/{self.context['parent'].mask_length}", + 'vrf': vrf, + } # diff --git a/netbox/netbox/api/fields.py b/netbox/netbox/api/fields.py index 1f3c40dc2..52343c2f6 100644 --- a/netbox/netbox/api/fields.py +++ b/netbox/netbox/api/fields.py @@ -1,5 +1,3 @@ -from collections import OrderedDict - from django.core.exceptions import ObjectDoesNotExist from netaddr import IPNetwork from rest_framework import serializers @@ -48,10 +46,10 @@ class ChoiceField(serializers.Field): def to_representation(self, obj): if obj == '': return None - return OrderedDict([ - ('value', obj), - ('label', self._choices[obj]) - ]) + return { + 'value': obj, + 'label': self._choices[obj], + } def to_internal_value(self, data): if data == '': diff --git a/netbox/netbox/api/views.py b/netbox/netbox/api/views.py index 835ebc6a9..6c6083959 100644 --- a/netbox/netbox/api/views.py +++ b/netbox/netbox/api/views.py @@ -1,5 +1,4 @@ import platform -from collections import OrderedDict from django import __version__ as DJANGO_VERSION from django.apps import apps @@ -26,18 +25,18 @@ class APIRootView(APIView): def get(self, request, format=None): - return Response(OrderedDict(( - ('circuits', reverse('circuits-api:api-root', request=request, format=format)), - ('dcim', reverse('dcim-api:api-root', request=request, format=format)), - ('extras', reverse('extras-api:api-root', request=request, format=format)), - ('ipam', reverse('ipam-api:api-root', request=request, format=format)), - ('plugins', reverse('plugins-api:api-root', request=request, format=format)), - ('status', reverse('api-status', request=request, format=format)), - ('tenancy', reverse('tenancy-api:api-root', request=request, format=format)), - ('users', reverse('users-api:api-root', request=request, format=format)), - ('virtualization', reverse('virtualization-api:api-root', request=request, format=format)), - ('wireless', reverse('wireless-api:api-root', request=request, format=format)), - ))) + return Response({ + 'circuits': reverse('circuits-api:api-root', request=request, format=format), + 'dcim': reverse('dcim-api:api-root', request=request, format=format), + 'extras': reverse('extras-api:api-root', request=request, format=format), + 'ipam': reverse('ipam-api:api-root', request=request, format=format), + 'plugins': reverse('plugins-api:api-root', request=request, format=format), + 'status': reverse('api-status', request=request, format=format), + 'tenancy': reverse('tenancy-api:api-root', request=request, format=format), + 'users': reverse('users-api:api-root', request=request, format=format), + 'virtualization': reverse('virtualization-api:api-root', request=request, format=format), + 'wireless': reverse('wireless-api:api-root', request=request, format=format), + }) class StatusView(APIView): diff --git a/netbox/utilities/utils.py b/netbox/utilities/utils.py index fa0534ec0..1dece76c8 100644 --- a/netbox/utilities/utils.py +++ b/netbox/utilities/utils.py @@ -1,7 +1,6 @@ import datetime import decimal import json -from collections import OrderedDict from decimal import Decimal from itertools import count, groupby @@ -218,7 +217,7 @@ def deepmerge(original, new): """ Deep merge two dictionaries (new into original) and return a new dict """ - merged = OrderedDict(original) + merged = dict(original) for key, val in new.items(): if key in original and isinstance(original[key], dict) and val and isinstance(val, dict): merged[key] = deepmerge(original[key], val) From e96620260a6c1b5cf8cff2112d40d061984a7b2c Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 2 Aug 2022 13:49:34 -0400 Subject: [PATCH 332/593] Closes #9903: Implement a mechanism for automatically updating denormalized fields --- docs/release-notes/version-3.3.md | 1 + netbox/extras/registry.py | 1 + netbox/netbox/denormalized.py | 54 +++++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+) create mode 100644 netbox/netbox/denormalized.py diff --git a/docs/release-notes/version-3.3.md b/docs/release-notes/version-3.3.md index e30ff011c..49d6891e2 100644 --- a/docs/release-notes/version-3.3.md +++ b/docs/release-notes/version-3.3.md @@ -124,6 +124,7 @@ Custom field UI visibility has no impact on API operation. * [#9261](https://github.com/netbox-community/netbox/issues/9261) - `NetBoxTable` no longer automatically clears pre-existing calls to `prefetch_related()` on its queryset * [#9434](https://github.com/netbox-community/netbox/issues/9434) - Enabled `django-rich` test runner for more user-friendly output +* [#9903](https://github.com/netbox-community/netbox/issues/9903) - Implement a mechanism for automatically updating denormalized fields ### REST API Changes diff --git a/netbox/extras/registry.py b/netbox/extras/registry.py index 07fd4cc24..e1437c00e 100644 --- a/netbox/extras/registry.py +++ b/netbox/extras/registry.py @@ -28,3 +28,4 @@ registry = Registry() registry['model_features'] = { feature: collections.defaultdict(set) for feature in EXTRAS_FEATURES } +registry['denormalized_fields'] = collections.defaultdict(list) diff --git a/netbox/netbox/denormalized.py b/netbox/netbox/denormalized.py new file mode 100644 index 000000000..5808acddc --- /dev/null +++ b/netbox/netbox/denormalized.py @@ -0,0 +1,54 @@ +import logging + +from django.db.models.signals import post_save +from django.dispatch import receiver + +from extras.registry import registry + + +logger = logging.getLogger('netbox.denormalized') + + +def register(model, field_name, mappings): + """ + Register a denormalized model field to ensure that it is kept up-to-date with the related object. + + Args: + model: The class being updated + field_name: The name of the field related to the triggering instance + mappings: Dictionary mapping of local to remote fields + """ + logger.debug(f'Registering denormalized field {model}.{field_name}') + + field = model._meta.get_field(field_name) + rel_model = field.related_model + + registry['denormalized_fields'][rel_model].append( + (model, field_name, mappings) + ) + + +@receiver(post_save) +def update_denormalized_fields(sender, instance, created, raw, **kwargs): + """ + Check if the sender has denormalized fields registered, and update them as necessary. + """ + # Skip for new objects or those being populated from raw data + if created or raw: + return + + # Look up any denormalized fields referencing this model from the application registry + for model, field_name, mappings in registry['denormalized_fields'].get(sender, []): + logger.debug(f'Updating denormalized values for {model}.{field_name}') + filter_params = { + field_name: instance.pk, + } + update_params = { + # Map the denormalized field names to the instance's values + denorm: getattr(instance, origin) for denorm, origin in mappings.items() + } + + # TODO: Improve efficiency here by placing conditions on the query? + # Update all the denormalized fields with the triggering object's new values + count = model.objects.filter(**filter_params).update(**update_params) + logger.debug(f'Updated {count} rows') From 5b3ef045506100c6fdf6ae9a15d7f717a1fd0f15 Mon Sep 17 00:00:00 2001 From: Daniel Sheppard Date: Tue, 2 Aug 2022 12:38:16 -0500 Subject: [PATCH 333/593] #9888 - Add filter and columns for device and site --- netbox/ipam/filtersets.py | 83 +++++++++++++++++++++++++++------ netbox/ipam/forms/filtersets.py | 44 ++++++++++++++++- netbox/ipam/models/l2vpn.py | 15 ++++++ netbox/ipam/tables/l2vpn.py | 11 ++++- 4 files changed, 136 insertions(+), 17 deletions(-) diff --git a/netbox/ipam/filtersets.py b/netbox/ipam/filtersets.py index edd1867ed..132094325 100644 --- a/netbox/ipam/filtersets.py +++ b/netbox/ipam/filtersets.py @@ -980,21 +980,65 @@ class L2VPNTerminationFilterSet(NetBoxModelFilterSet): to_field_name='slug', label='L2VPN (slug)', ) - device = MultiValueCharFilter( - method='filter_device', - field_name='name', - label='Device (name)', + region = MultiValueCharFilter( + method='filter_region', + field_name='slug', + label='Region (slug)', ) - device_id = MultiValueNumberFilter( - method='filter_device', + region_id = MultiValueNumberFilter( + method='filter_region', + field_name='pk', + label='Region (ID)', + ) + site = MultiValueCharFilter( + method='filter_site', + field_name='slug', + label='Device (slug)', + ) + site_id = MultiValueNumberFilter( + method='filter_site', field_name='pk', label='Device (ID)', ) + device = django_filters.ModelMultipleChoiceFilter( + field_name='interface__device__name', + queryset=Device.objects.all(), + to_field_name='name', + label='Device (name)', + ) + device_id = django_filters.ModelMultipleChoiceFilter( + field_name='interface__device', + queryset=Device.objects.all(), + label='Device (ID)', + ) + virtual_machine = django_filters.ModelMultipleChoiceFilter( + field_name='vminterface__virtual_machine__name', + queryset=VirtualMachine.objects.all(), + to_field_name='name', + label='Virtual machine (name)', + ) + virtual_machine_id = django_filters.ModelMultipleChoiceFilter( + field_name='vminterface__virtual_machine', + queryset=VirtualMachine.objects.all(), + label='Virtual machine (ID)', + ) + interface = django_filters.ModelMultipleChoiceFilter( + field_name='interface__name', + queryset=Interface.objects.all(), + to_field_name='name', + label='Interface (name)', + ) interface_id = django_filters.ModelMultipleChoiceFilter( field_name='interface', queryset=Interface.objects.all(), label='Interface (ID)', ) + vminterface = django_filters.ModelMultipleChoiceFilter( + field_name='vminterface__name', + queryset=VMInterface.objects.all(), + to_field_name='name', + label='VM interface (name)', + ) vminterface_id = django_filters.ModelMultipleChoiceFilter( field_name='vminterface', queryset=VMInterface.objects.all(), @@ -1027,13 +1071,22 @@ class L2VPNTerminationFilterSet(NetBoxModelFilterSet): qs_filter = Q(l2vpn__name__icontains=value) return queryset.filter(qs_filter) - def filter_device(self, queryset, name, value): - devices = Device.objects.filter(**{'{}__in'.format(name): value}) - if not devices.exists(): - return queryset.none() - interface_ids = [] - for device in devices: - interface_ids.extend(device.vc_interfaces().values_list('id', flat=True)) - return queryset.filter( - interface__in=interface_ids + def filter_site(self, queryset, name, value): + qs = queryset.filter( + Q( + Q(**{'vlan__site__{}__in'.format(name): value}) | + Q(**{'interface__device__site__{}__in'.format(name): value}) | + Q(**{'vminterface__virtual_machine__site__{}__in'.format(name): value}) + ) ) + return qs + + def filter_region(self, queryset, name, value): + qs = queryset.filter( + Q( + Q(**{'vlan__site__region__{}__in'.format(name): value}) | + Q(**{'interface__device__site__region__{}__in'.format(name): value}) | + Q(**{'vminterface__virtual_machine__site__region__{}__in'.format(name): value}) + ) + ) + return qs diff --git a/netbox/ipam/forms/filtersets.py b/netbox/ipam/forms/filtersets.py index 384a4da33..d93bd16d0 100644 --- a/netbox/ipam/forms/filtersets.py +++ b/netbox/ipam/forms/filtersets.py @@ -508,7 +508,8 @@ class L2VPNFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): class L2VPNTerminationFilterForm(NetBoxModelFilterSetForm): model = L2VPNTermination fieldsets = ( - (None, ('l2vpn_id', 'assigned_object_type_id')), + (None, ('l2vpn_id', 'assigned_object_type_id', )), + ('Assigned Object', ('region_id', 'site_id', 'device_id', 'virtual_machine_id', 'vlan_id')), ) l2vpn_id = DynamicModelChoiceField( queryset=L2VPN.objects.all(), @@ -520,3 +521,44 @@ class L2VPNTerminationFilterForm(NetBoxModelFilterSetForm): required=False, label='Object type' ) + region_id = DynamicModelMultipleChoiceField( + queryset=Region.objects.all(), + required=False, + label=_('Region') + ) + site_id = DynamicModelMultipleChoiceField( + queryset=Site.objects.all(), + required=False, + null_option='None', + query_params={ + 'region_id': '$region_id' + }, + label=_('Site') + ) + device_id = DynamicModelMultipleChoiceField( + queryset=Device.objects.all(), + required=False, + null_option='None', + query_params={ + 'site_id': '$site_id' + }, + label=_('Device') + ) + vlan_id = DynamicModelMultipleChoiceField( + queryset=VLAN.objects.all(), + required=False, + null_option='None', + query_params={ + 'site_id': '$site_id' + }, + label=_('VLAN') + ) + virtual_machine_id = DynamicModelMultipleChoiceField( + queryset=VirtualMachine.objects.all(), + required=False, + null_option='None', + query_params={ + 'site_id': '$site_id' + }, + label=_('Virtual Machine') + ) diff --git a/netbox/ipam/models/l2vpn.py b/netbox/ipam/models/l2vpn.py index 5d85fe915..5adf5e05d 100644 --- a/netbox/ipam/models/l2vpn.py +++ b/netbox/ipam/models/l2vpn.py @@ -113,3 +113,18 @@ class L2VPNTermination(NetBoxModel): f'{l2vpn_type} L2VPNs cannot have more than two terminations; found {terminations_count} already ' f'defined.' ) + + @property + def assigned_object_parent(self): + obj_type = ContentType.objects.get_for_model(self.assigned_object) + if obj_type.model == 'vminterface': + return self.assigned_object.virtual_machine + elif obj_type.model == 'interface': + return self.assigned_object.device + elif obj_type.model == 'vminterface': + return self.assigned_object.virtual_machine + return None + + @property + def assigned_object_site(self): + return self.assigned_object_parent.site diff --git a/netbox/ipam/tables/l2vpn.py b/netbox/ipam/tables/l2vpn.py index 5be525343..e2eae7a32 100644 --- a/netbox/ipam/tables/l2vpn.py +++ b/netbox/ipam/tables/l2vpn.py @@ -53,8 +53,17 @@ class L2VPNTerminationTable(NetBoxTable): linkify=True, orderable=False ) + assigned_object_parent = tables.Column( + linkify=True, + orderable=False + ) + assigned_object_site = tables.Column( + linkify=True, + orderable=False + ) class Meta(NetBoxTable.Meta): model = L2VPNTermination - fields = ('pk', 'l2vpn', 'assigned_object_type', 'assigned_object', 'actions') + fields = ('pk', 'l2vpn', 'assigned_object_type', 'assigned_object', 'assigned_object_parent', + 'assigned_object_site', 'actions') default_columns = ('pk', 'l2vpn', 'assigned_object_type', 'assigned_object', 'actions') From d3a567a2f565097d7409d888dacabda2c59f42f6 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 2 Aug 2022 13:56:52 -0400 Subject: [PATCH 334/593] Fixes #9788: Ensure denormalized fields on CableTermination are kept in sync with related objects --- docs/release-notes/version-3.3.md | 1 + netbox/dcim/apps.py | 18 +++++++++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/version-3.3.md b/docs/release-notes/version-3.3.md index 49d6891e2..5d043a777 100644 --- a/docs/release-notes/version-3.3.md +++ b/docs/release-notes/version-3.3.md @@ -104,6 +104,7 @@ Custom field UI visibility has no impact on API operation. * [#9730](https://github.com/netbox-community/netbox/issues/9730) - Fix validation error when creating a new cable via UI form * [#9733](https://github.com/netbox-community/netbox/issues/9733) - Handle split paths during trace when fanning out to front ports with differing cables * [#9765](https://github.com/netbox-community/netbox/issues/9765) - Report correct segment count under cable trace UI view +* [#9788](https://github.com/netbox-community/netbox/issues/9788) - Ensure denormalized fields on CableTermination are kept in sync with related objects * [#9789](https://github.com/netbox-community/netbox/issues/9789) - Fix rendering of cable traces ending at provider networks * [#9794](https://github.com/netbox-community/netbox/issues/9794) - Fix link to connect a rear port to a circuit termination * [#9818](https://github.com/netbox-community/netbox/issues/9818) - Fix circuit side selection when connecting a cable to a circuit termination diff --git a/netbox/dcim/apps.py b/netbox/dcim/apps.py index 78a243f84..4be2df659 100644 --- a/netbox/dcim/apps.py +++ b/netbox/dcim/apps.py @@ -1,10 +1,26 @@ from django.apps import AppConfig +from netbox import denormalized + class DCIMConfig(AppConfig): name = "dcim" verbose_name = "DCIM" def ready(self): - import dcim.signals + from .models import CableTermination + + # Register denormalized fields + denormalized.register(CableTermination, '_device', { + '_rack': 'rack', + '_location': 'location', + '_site': 'site', + }) + denormalized.register(CableTermination, '_rack', { + '_location': 'location', + '_site': 'site', + }) + denormalized.register(CableTermination, '_location', { + '_site': 'site', + }) From 37c4f1a7d3509f137a5a81d1b570d10a7533ccae Mon Sep 17 00:00:00 2001 From: Daniel Sheppard Date: Tue, 2 Aug 2022 13:38:17 -0500 Subject: [PATCH 335/593] Fix up a few minor mistakes. Add tests. --- netbox/ipam/filtersets.py | 4 ++-- netbox/ipam/forms/filtersets.py | 11 ++++++----- netbox/ipam/tests/test_filtersets.py | 21 +++++++++++++++++++++ 3 files changed, 29 insertions(+), 7 deletions(-) diff --git a/netbox/ipam/filtersets.py b/netbox/ipam/filtersets.py index 132094325..49ec15fc1 100644 --- a/netbox/ipam/filtersets.py +++ b/netbox/ipam/filtersets.py @@ -993,12 +993,12 @@ class L2VPNTerminationFilterSet(NetBoxModelFilterSet): site = MultiValueCharFilter( method='filter_site', field_name='slug', - label='Device (slug)', + label='Site (slug)', ) site_id = MultiValueNumberFilter( method='filter_site', field_name='pk', - label='Device (ID)', + label='Site (ID)', ) device = django_filters.ModelMultipleChoiceFilter( field_name='interface__device__name', diff --git a/netbox/ipam/forms/filtersets.py b/netbox/ipam/forms/filtersets.py index d93bd16d0..ecf63b49f 100644 --- a/netbox/ipam/forms/filtersets.py +++ b/netbox/ipam/forms/filtersets.py @@ -11,7 +11,7 @@ from netbox.forms import NetBoxModelFilterSetForm from tenancy.forms import TenancyFilterForm from utilities.forms import ( add_blank_choice, ContentTypeMultipleChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, - MultipleChoiceField, StaticSelect, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES, + MultipleChoiceField, StaticSelect, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES, APISelectMultiple, ) from virtualization.models import VirtualMachine @@ -508,8 +508,8 @@ class L2VPNFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): class L2VPNTerminationFilterForm(NetBoxModelFilterSetForm): model = L2VPNTermination fieldsets = ( - (None, ('l2vpn_id', 'assigned_object_type_id', )), - ('Assigned Object', ('region_id', 'site_id', 'device_id', 'virtual_machine_id', 'vlan_id')), + (None, ('l2vpn_id', )), + ('Assigned Object', ('assigned_object_type_id', 'region_id', 'site_id', 'device_id', 'virtual_machine_id', 'vlan_id')), ) l2vpn_id = DynamicModelChoiceField( queryset=L2VPN.objects.all(), @@ -517,9 +517,10 @@ class L2VPNTerminationFilterForm(NetBoxModelFilterSetForm): label='L2VPN' ) assigned_object_type_id = ContentTypeMultipleChoiceField( - queryset=ContentType.objects.all(), + queryset=ContentType.objects.filter(L2VPN_ASSIGNMENT_MODELS), required=False, - label='Object type' + label=_('Assigned Object Type'), + limit_choices_to=L2VPN_ASSIGNMENT_MODELS ) region_id = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), diff --git a/netbox/ipam/tests/test_filtersets.py b/netbox/ipam/tests/test_filtersets.py index 9106a4965..081f6e11d 100644 --- a/netbox/ipam/tests/test_filtersets.py +++ b/netbox/ipam/tests/test_filtersets.py @@ -1600,3 +1600,24 @@ class L2VPNTerminationTestCase(TestCase, ChangeLoggedFilterSetTests): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) params = {'vlan': ['VLAN 1', 'VLAN 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_site(self): + site = Site.objects.all().first() + params = {'site_id': [site.pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) + params = {'site': ['site-1']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) + + def test_device(self): + device = Device.objects.all().first() + params = {'device_id': [device.pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) + params = {'device': ['Device 1']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) + + def test_virtual_machine(self): + virtual_machine = VirtualMachine.objects.all().first() + params = {'virtual_machine_id': [virtual_machine.pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) + params = {'virtual_machine': ['Virtual Machine 1']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) From c6e25f068d7aeb77f18add68ccab9ced6ff68d9a Mon Sep 17 00:00:00 2001 From: Jason Lavoie Date: Tue, 2 Aug 2022 20:39:56 -0400 Subject: [PATCH 336/593] import/export color field on front- and rear-ports for module-types and device-types Closes: #9906 - Adds `color` field to front and rearport template import forms - Adds `color` field to `to_yaml` export for front and rearport templates --- netbox/dcim/forms/object_import.py | 4 ++-- netbox/dcim/models/device_component_templates.py | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/netbox/dcim/forms/object_import.py b/netbox/dcim/forms/object_import.py index afbcd6543..606333e83 100644 --- a/netbox/dcim/forms/object_import.py +++ b/netbox/dcim/forms/object_import.py @@ -146,7 +146,7 @@ class FrontPortTemplateImportForm(ComponentTemplateImportForm): class Meta: model = FrontPortTemplate fields = [ - 'device_type', 'module_type', 'name', 'type', 'rear_port', 'rear_port_position', 'label', 'description', + 'device_type', 'module_type', 'name', 'type', 'color', 'rear_port', 'rear_port_position', 'label', 'description', ] @@ -158,7 +158,7 @@ class RearPortTemplateImportForm(ComponentTemplateImportForm): class Meta: model = RearPortTemplate fields = [ - 'device_type', 'module_type', 'name', 'type', 'positions', 'label', 'description', + 'device_type', 'module_type', 'name', 'type', 'color', 'positions', 'label', 'description', ] diff --git a/netbox/dcim/models/device_component_templates.py b/netbox/dcim/models/device_component_templates.py index f596cf28b..74252e480 100644 --- a/netbox/dcim/models/device_component_templates.py +++ b/netbox/dcim/models/device_component_templates.py @@ -462,6 +462,7 @@ class FrontPortTemplate(ModularComponentTemplateModel): return { 'name': self.name, 'type': self.type, + 'color': self.color, 'rear_port': self.rear_port.name, 'rear_port_position': self.rear_port_position, 'label': self.label, @@ -511,6 +512,7 @@ class RearPortTemplate(ModularComponentTemplateModel): return { 'name': self.name, 'type': self.type, + 'color': self.color, 'positions': self.positions, 'label': self.label, 'description': self.description, From b9678c7c0e50c9b2f24aec95a0d8c13c1b20f3dd Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 3 Aug 2022 10:50:35 -0400 Subject: [PATCH 337/593] Closes #9853: Show full object in cable A/B termination lists --- netbox/netbox/api/serializers/generic.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/netbox/netbox/api/serializers/generic.py b/netbox/netbox/api/serializers/generic.py index 8b4069c98..5016bdaab 100644 --- a/netbox/netbox/api/serializers/generic.py +++ b/netbox/netbox/api/serializers/generic.py @@ -1,7 +1,10 @@ from django.contrib.contenttypes.models import ContentType +from drf_yasg.utils import swagger_serializer_method from rest_framework import serializers from netbox.api.fields import ContentTypeField +from netbox.constants import NESTED_SERIALIZER_PREFIX +from utilities.api import get_serializer_for_model from utilities.utils import content_type_identifier __all__ = ( @@ -17,6 +20,7 @@ class GenericObjectSerializer(serializers.Serializer): queryset=ContentType.objects.all() ) object_id = serializers.IntegerField() + object = serializers.SerializerMethodField(read_only=True) def to_internal_value(self, data): data = super().to_internal_value(data) @@ -25,7 +29,17 @@ class GenericObjectSerializer(serializers.Serializer): def to_representation(self, instance): ct = ContentType.objects.get_for_model(instance) - return { + data = { 'object_type': content_type_identifier(ct), 'object_id': instance.pk, } + if 'request' in self.context: + data['object'] = self.get_object(instance) + + return data + + @swagger_serializer_method(serializer_or_field=serializers.DictField) + def get_object(self, obj): + serializer = get_serializer_for_model(obj, prefix=NESTED_SERIALIZER_PREFIX) + # context = {'request': self.context['request']} + return serializer(obj, context=self.context).data From 367bf25618d1be55c10b7e707101f2759711e855 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 3 Aug 2022 12:46:16 -0400 Subject: [PATCH 338/593] Fixes #9778: Fix exception during cable deletion after deleting a connected termination --- docs/release-notes/version-3.3.md | 1 + netbox/dcim/models/cables.py | 24 +++++++++++++----------- netbox/dcim/signals.py | 5 ++++- netbox/dcim/svg/cables.py | 5 ++++- netbox/dcim/utils.py | 5 +++-- netbox/templates/dcim/interface.html | 2 +- 6 files changed, 26 insertions(+), 16 deletions(-) diff --git a/docs/release-notes/version-3.3.md b/docs/release-notes/version-3.3.md index 5d043a777..6099e8a61 100644 --- a/docs/release-notes/version-3.3.md +++ b/docs/release-notes/version-3.3.md @@ -104,6 +104,7 @@ Custom field UI visibility has no impact on API operation. * [#9730](https://github.com/netbox-community/netbox/issues/9730) - Fix validation error when creating a new cable via UI form * [#9733](https://github.com/netbox-community/netbox/issues/9733) - Handle split paths during trace when fanning out to front ports with differing cables * [#9765](https://github.com/netbox-community/netbox/issues/9765) - Report correct segment count under cable trace UI view +* [#9778](https://github.com/netbox-community/netbox/issues/9778) - Fix exception during cable deletion after deleting a connected termination * [#9788](https://github.com/netbox-community/netbox/issues/9788) - Ensure denormalized fields on CableTermination are kept in sync with related objects * [#9789](https://github.com/netbox-community/netbox/issues/9789) - Fix rendering of cable traces ending at provider networks * [#9794](https://github.com/netbox-community/netbox/issues/9794) - Fix link to connect a rear port to a circuit termination diff --git a/netbox/dcim/models/cables.py b/netbox/dcim/models/cables.py index e0a489f5b..321d808ff 100644 --- a/netbox/dcim/models/cables.py +++ b/netbox/dcim/models/cables.py @@ -431,11 +431,7 @@ class CablePath(models.Model): """ Return the list of originating objects. """ - if hasattr(self, '_path_objects'): - return self.path_objects[0] - return [ - path_node_to_object(node) for node in self.path[0] - ] + return self.path_objects[0] @property def destinations(self): @@ -444,11 +440,7 @@ class CablePath(models.Model): """ if not self.is_complete: return [] - if hasattr(self, '_path_objects'): - return self.path_objects[-1] - return [ - path_node_to_object(node) for node in self.path[-1] - ] + return self.path_objects[-1] @property def segment_count(self): @@ -463,6 +455,9 @@ class CablePath(models.Model): """ from circuits.models import CircuitTermination + if not terminations: + return None + # Ensure all originating terminations are attached to the same link if len(terminations) > 1: assert all(t.link == terminations[0].link for t in terminations[1:]) @@ -529,6 +524,9 @@ class CablePath(models.Model): ]) # Step 6: Determine the "next hop" terminations, if applicable + if not remote_terminations: + break + if isinstance(remote_terminations[0], FrontPort): # Follow FrontPorts to their corresponding RearPorts rear_ports = RearPort.objects.filter( @@ -640,7 +638,11 @@ class CablePath(models.Model): nodes = [] for node in step: ct_id, object_id = decompile_path_node(node) - nodes.append(prefetched[ct_id][object_id]) + try: + nodes.append(prefetched[ct_id][object_id]) + except KeyError: + # Ignore stale (deleted) object IDs + pass path.append(nodes) return path diff --git a/netbox/dcim/signals.py b/netbox/dcim/signals.py index 2293f8840..b990daf1a 100644 --- a/netbox/dcim/signals.py +++ b/netbox/dcim/signals.py @@ -116,7 +116,10 @@ def retrace_cable_paths(instance, **kwargs): @receiver(post_delete, sender=CableTermination) def nullify_connected_endpoints(instance, **kwargs): """ - Disassociate the Cable from the termination object. + Disassociate the Cable from the termination object, and retrace any affected CablePaths. """ model = instance.termination_type.model_class() model.objects.filter(pk=instance.termination_id).update(cable=None, cable_end='') + + for cablepath in CablePath.objects.filter(_nodes__contains=instance.cable): + cablepath.retrace() diff --git a/netbox/dcim/svg/cables.py b/netbox/dcim/svg/cables.py index 3b259eca2..26d16fafe 100644 --- a/netbox/dcim/svg/cables.py +++ b/netbox/dcim/svg/cables.py @@ -362,8 +362,11 @@ class CableTraceSVG: terminations = self.draw_terminations(far_ends) for term in terminations: self.draw_fanout(term, cable) - else: + elif far_ends: self.draw_terminations(far_ends) + else: + # Link is not connected to anything + break # Far end parent parent_objects = set(end.parent_object for end in far_ends) diff --git a/netbox/dcim/utils.py b/netbox/dcim/utils.py index 26b6e2e25..eadd2da96 100644 --- a/netbox/dcim/utils.py +++ b/netbox/dcim/utils.py @@ -24,11 +24,12 @@ def object_to_path_node(obj): def path_node_to_object(repr): """ - Given the string representation of a path node, return the corresponding instance. + Given the string representation of a path node, return the corresponding instance. If the object no longer + exists, return None. """ ct_id, object_id = decompile_path_node(repr) ct = ContentType.objects.get_for_id(ct_id) - return ct.model_class().objects.get(pk=object_id) + return ct.model_class().objects.filter(pk=object_id).first() def create_cablepath(terminations): diff --git a/netbox/templates/dcim/interface.html b/netbox/templates/dcim/interface.html index 3a7fe986a..11e776872 100644 --- a/netbox/templates/dcim/interface.html +++ b/netbox/templates/dcim/interface.html @@ -219,7 +219,7 @@
    + + {% for contact in contacts %} @@ -17,6 +19,20 @@ + +
    Path Status - {% if object.path.is_active %} + {% if object.path.is_complete and object.path.is_active %} Reachable {% else %} Not Reachable From f11a6f0135fc40bb2b695f38b5668a09ec46e3be Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 3 Aug 2022 14:41:50 -0400 Subject: [PATCH 339/593] Release v3.3-beta2 --- .github/ISSUE_TEMPLATE/bug_report.yaml | 2 +- .github/ISSUE_TEMPLATE/feature_request.yaml | 2 +- docs/release-notes/version-3.3.md | 2 +- netbox/netbox/settings.py | 2 +- requirements.txt | 10 +++++----- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index 88fbb1df9..dc8dd8275 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -14,7 +14,7 @@ body: attributes: label: NetBox version description: What version of NetBox are you currently running? - placeholder: v3.3-beta1 + placeholder: v3.3-beta2 validations: required: true - type: dropdown diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml index 1035c02fb..d9e5a26fd 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yaml +++ b/.github/ISSUE_TEMPLATE/feature_request.yaml @@ -14,7 +14,7 @@ body: attributes: label: NetBox version description: What version of NetBox are you currently running? - placeholder: v3.3-beta1 + placeholder: v3.3-beta2 validations: required: true - type: dropdown diff --git a/docs/release-notes/version-3.3.md b/docs/release-notes/version-3.3.md index 6099e8a61..2a3935e5e 100644 --- a/docs/release-notes/version-3.3.md +++ b/docs/release-notes/version-3.3.md @@ -1,6 +1,6 @@ # NetBox v3.3 -## v3.3.0 (FUTURE) +## v3.3-beta2 (2022-08-03) ### Breaking Changes diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index e0ec8e1ec..0dcde9cf6 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -29,7 +29,7 @@ django.utils.encoding.force_text = force_str # Environment setup # -VERSION = '3.3-beta1' +VERSION = '3.3-beta2' # Hostname HOSTNAME = platform.node() diff --git a/requirements.txt b/requirements.txt index 4c8e5e5ce..8a7dd79d4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ bleach==5.0.1 -Django==4.0.6 +Django==4.0.7 django-cors-headers==3.13.0 django-debug-toolbar==3.5.0 django-filter==22.1 @@ -14,19 +14,19 @@ django-tables2==2.4.1 django-taggit==3.0.0 django-timezone-field==5.0 djangorestframework==3.13.1 -drf-yasg[validation]==1.20.0 +drf-yasg[validation]==1.21.3 graphene-django==2.15.0 gunicorn==20.1.0 Jinja2==3.1.2 -Markdown==3.3.7 -markdown-include==0.6.0 +Markdown==3.4.1 +markdown-include==0.7.0 mkdocs-material==8.3.9 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.7.1 +sentry-sdk==1.9.0 social-auth-app-django==5.0.0 social-auth-core==4.3.0 svgwrite==1.4.3 From a2e84dd279549c7284184ff98da4a1ce23c646c9 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 3 Aug 2022 15:22:51 -0400 Subject: [PATCH 340/593] Changelog for #9827, #9906 --- docs/release-notes/version-3.2.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/release-notes/version-3.2.md b/docs/release-notes/version-3.2.md index 5e538890c..1c7eb79e8 100644 --- a/docs/release-notes/version-3.2.md +++ b/docs/release-notes/version-3.2.md @@ -11,9 +11,11 @@ * [#9881](https://github.com/netbox-community/netbox/issues/9881) - Increase granularity in utilization graph values * [#9882](https://github.com/netbox-community/netbox/issues/9882) - Add manufacturer column to modules table * [#9883](https://github.com/netbox-community/netbox/issues/9883) - Linkify location column in power panels table +* [#9906](https://github.com/netbox-community/netbox/issues/9906) - Include `color` attribute in front & rear port YAML import/export ### Bug Fixes +* [#9827](https://github.com/netbox-community/netbox/issues/9827) - Fix assignment of module bay position during bulk creation * [#9871](https://github.com/netbox-community/netbox/issues/9871) - Fix utilization graph value alignments * [#9884](https://github.com/netbox-community/netbox/issues/9884) - Prevent querying assigned VRF on prefix object init * [#9885](https://github.com/netbox-community/netbox/issues/9885) - Fix child prefix counts when editing/deleting aggregates in bulk From 56433cf85520edee9cf0d440bfeb82524eac7fc7 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Thu, 4 Aug 2022 10:42:44 -0400 Subject: [PATCH 341/593] Fix denormalization logic to be compatible with loading fixture data --- netbox/netbox/denormalized.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/netbox/netbox/denormalized.py b/netbox/netbox/denormalized.py index 5808acddc..cd4a869d2 100644 --- a/netbox/netbox/denormalized.py +++ b/netbox/netbox/denormalized.py @@ -33,6 +33,10 @@ def update_denormalized_fields(sender, instance, created, raw, **kwargs): """ Check if the sender has denormalized fields registered, and update them as necessary. """ + def _get_field_value(instance, field_name): + field = instance._meta.get_field(field_name) + return field.value_from_object(instance) + # Skip for new objects or those being populated from raw data if created or raw: return @@ -45,7 +49,7 @@ def update_denormalized_fields(sender, instance, created, raw, **kwargs): } update_params = { # Map the denormalized field names to the instance's values - denorm: getattr(instance, origin) for denorm, origin in mappings.items() + denorm: _get_field_value(instance, origin) for denorm, origin in mappings.items() } # TODO: Improve efficiency here by placing conditions on the query? From f874e9932d06653e330da65d93c2fe0fd7720968 Mon Sep 17 00:00:00 2001 From: Osamu-kj Date: Thu, 4 Aug 2022 18:52:25 +0200 Subject: [PATCH 342/593] Added HTML Sanitization to the custom fields --- netbox/netbox/tables/columns.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/netbox/netbox/tables/columns.py b/netbox/netbox/tables/columns.py index 7da241566..7774a495f 100644 --- a/netbox/netbox/tables/columns.py +++ b/netbox/netbox/tables/columns.py @@ -1,4 +1,5 @@ from dataclasses import dataclass +from glob import escape from typing import Optional import django_tables2 as tables @@ -433,21 +434,21 @@ class CustomFieldColumn(tables.Column): def render(self, value): if self.customfield.type == CustomFieldTypeChoices.TYPE_BOOLEAN and value is True: - return mark_safe('') + return escape('') if self.customfield.type == CustomFieldTypeChoices.TYPE_BOOLEAN and value is False: - return mark_safe('') + return escape('') if self.customfield.type == CustomFieldTypeChoices.TYPE_URL: - return mark_safe(f'{value}') + return escape(f'{value}') if self.customfield.type == CustomFieldTypeChoices.TYPE_MULTISELECT: return ', '.join(v for v in value) if self.customfield.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT: - return mark_safe(', '.join([ + return escape(', '.join([ self._likify_item(obj) for obj in self.customfield.deserialize(value) ])) if value is not None: obj = self.customfield.deserialize(value) - return mark_safe(self._likify_item(obj)) - return self.default + return escape(self._likify_item(obj)) + return escape(self.default) def value(self, value): if isinstance(value, list): From 5da3cab4de0469c8e8f19726600286abc590c7c3 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Thu, 4 Aug 2022 14:11:52 -0400 Subject: [PATCH 343/593] Reorganize documentation --- docs/additional-features/webhooks.md | 57 ----------- docs/administration/permissions.md | 71 ++++++++++++- docs/configuration/miscellaneous.md | 2 +- docs/configuration/napalm.md | 2 +- docs/customization/custom-fields.md | 75 +++++++++++++- .../custom-links.md} | 0 docs/customization/export-templates.md | 39 +++++++- docs/development/models.md | 8 +- .../change-logging.md | 0 docs/features/circuits.md | 3 + docs/features/contacts.md | 3 + .../context-data.md} | 0 docs/features/customization.md | 3 + docs/features/devices-cabling.md | 3 + docs/features/facilities.md | 3 + docs/features/ipam.md | 3 + .../journaling.md | 0 docs/features/l2vpn-overlay.md | 3 + docs/features/permissions.md | 3 + docs/features/power-tracking.md | 3 + docs/features/services.md | 3 + docs/features/sso.md | 3 + .../extras/tag.md => features/tags.md} | 0 docs/features/tenancy.md | 3 + docs/features/vlan-management.md | 3 + .../webhook.md => features/webhooks.md} | 56 +++++++++++ docs/features/wireless.md | 3 + docs/getting-started/populating-data.md | 2 +- docs/installation/3-netbox.md | 2 +- .../graphql-api.md} | 0 .../napalm.md | 0 .../prometheus-metrics.md | 0 .../overview.md => integrations/rest-api.md} | 99 ++++++++++++++++++- docs/models/extras/customfield.md | 73 -------------- docs/models/extras/exporttemplate.md | 37 ------- docs/models/extras/imageattachment.md | 3 - docs/models/users/objectpermission.md | 69 ------------- docs/models/users/token.md | 19 ---- docs/{rest-api => reference}/filtering.md | 0 docs/release-notes/version-3.0.md | 2 +- docs/rest-api/authentication.md | 73 -------------- mkdocs.yml | 47 +++++---- 42 files changed, 412 insertions(+), 366 deletions(-) delete mode 100644 docs/additional-features/webhooks.md rename docs/{models/extras/customlink.md => customization/custom-links.md} (100%) rename docs/{additional-features => features}/change-logging.md (100%) create mode 100644 docs/features/circuits.md create mode 100644 docs/features/contacts.md rename docs/{models/extras/configcontext.md => features/context-data.md} (100%) create mode 100644 docs/features/customization.md create mode 100644 docs/features/devices-cabling.md create mode 100644 docs/features/facilities.md create mode 100644 docs/features/ipam.md rename docs/{additional-features => features}/journaling.md (100%) create mode 100644 docs/features/l2vpn-overlay.md create mode 100644 docs/features/permissions.md create mode 100644 docs/features/power-tracking.md create mode 100644 docs/features/services.md create mode 100644 docs/features/sso.md rename docs/{models/extras/tag.md => features/tags.md} (100%) create mode 100644 docs/features/tenancy.md create mode 100644 docs/features/vlan-management.md rename docs/{models/extras/webhook.md => features/webhooks.md} (67%) create mode 100644 docs/features/wireless.md rename docs/{graphql-api/overview.md => integrations/graphql-api.md} (100%) rename docs/{additional-features => integrations}/napalm.md (100%) rename docs/{additional-features => integrations}/prometheus-metrics.md (100%) rename docs/{rest-api/overview.md => integrations/rest-api.md} (78%) delete mode 100644 docs/models/extras/customfield.md delete mode 100644 docs/models/extras/exporttemplate.md delete mode 100644 docs/models/extras/imageattachment.md delete mode 100644 docs/models/users/objectpermission.md delete mode 100644 docs/models/users/token.md rename docs/{rest-api => reference}/filtering.md (100%) delete mode 100644 docs/rest-api/authentication.md diff --git a/docs/additional-features/webhooks.md b/docs/additional-features/webhooks.md deleted file mode 100644 index 5077f3a68..000000000 --- a/docs/additional-features/webhooks.md +++ /dev/null @@ -1,57 +0,0 @@ -{!models/extras/webhook.md!} - -## Conditional Webhooks - -A webhook may include a set of conditional logic expressed in JSON used to control whether a webhook triggers for a specific object. For example, you may wish to trigger a webhook for devices only when the `status` field of an object is "active": - -```json -{ - "and": [ - { - "attr": "status.value", - "value": "active" - } - ] -} -``` - -For more detail, see the reference documentation for NetBox's [conditional logic](../reference/conditions.md). - -## Webhook Processing - -When a change is detected, any resulting webhooks are placed into a Redis queue for processing. This allows the user's request to complete without needing to wait for the outgoing webhook(s) to be processed. The webhooks are then extracted from the queue by the `rqworker` process and HTTP requests are sent to their respective destinations. The current webhook queue and any failed webhooks can be inspected in the admin UI under System > Background Tasks. - -A request is considered successful if the response has a 2XX status code; otherwise, the request is marked as having failed. Failed requests may be retried manually via the admin UI. - -## Troubleshooting - -To assist with verifying that the content of outgoing webhooks is rendered correctly, NetBox provides a simple HTTP listener that can be run locally to receive and display webhook requests. First, modify the target URL of the desired webhook to `http://localhost:9000/`. This will instruct NetBox to send the request to the local server on TCP port 9000. Then, start the webhook receiver service from the NetBox root directory: - -```no-highlight -$ python netbox/manage.py webhook_receiver -Listening on port http://localhost:9000. Stop with CONTROL-C. -``` - -You can test the receiver itself by sending any HTTP request to it. For example: - -```no-highlight -$ curl -X POST http://localhost:9000 --data '{"foo": "bar"}' -``` - -The server will print output similar to the following: - -```no-highlight -[1] Tue, 07 Apr 2020 17:44:02 GMT 127.0.0.1 "POST / HTTP/1.1" 200 - -Host: localhost:9000 -User-Agent: curl/7.58.0 -Accept: */* -Content-Length: 14 -Content-Type: application/x-www-form-urlencoded - -{"foo": "bar"} ------------- -``` - -Note that `webhook_receiver` does not actually _do_ anything with the information received: It merely prints the request headers and body for inspection. - -Now, when the NetBox webhook is triggered and processed, you should see its headers and content appear in the terminal where the webhook receiver is listening. If you don't, check that the `rqworker` process is running and that webhook events are being placed into the queue (visible under the NetBox admin UI). diff --git a/docs/administration/permissions.md b/docs/administration/permissions.md index 60717c28a..21f259979 100644 --- a/docs/administration/permissions.md +++ b/docs/administration/permissions.md @@ -1,8 +1,75 @@ -# Permissions +# Object-Based Permissions NetBox v2.9 introduced a new object-based permissions framework, which replaces Django's built-in permissions model. Object-based permissions enable an administrator to grant users or groups the ability to perform an action on arbitrary subsets of objects in NetBox, rather than all objects of a certain type. For example, it is possible to grant a user permission to view only sites within a particular region, or to modify only VLANs with a numeric ID within a certain range. -{!models/users/objectpermission.md!} +A permission in NetBox represents a relationship shared by several components: + +* Object type(s) - One or more types of object in NetBox +* User(s)/Group(s) - One or more users or groups of users +* Action(s) - The action(s) that can be performed on an object +* Constraints - An arbitrary filter used to limit the granted action(s) to a specific subset of objects + +At a minimum, a permission assignment must specify one object type, one user or group, and one action. The specification of constraints is optional: A permission without any constraints specified will apply to all instances of the selected model(s). + +## Actions + +There are four core actions that can be permitted for each type of object within NetBox, roughly analogous to the CRUD convention (create, read, update, and delete): + +* **View** - Retrieve an object from the database +* **Add** - Create a new object +* **Change** - Modify an existing object +* **Delete** - Delete an existing object + +In addition to these, permissions can also grant custom actions that may be required by a specific model or plugin. For example, the `napalm_read` permission on the device model allows a user to execute NAPALM queries on a device via NetBox's REST API. These can be specified when granting a permission in the "additional actions" field. + +!!! note + Internally, all actions granted by a permission (both built-in and custom) are stored as strings in an array field named `actions`. + +## Constraints + +Constraints are expressed as a JSON object or list representing a [Django query filter](https://docs.djangoproject.com/en/stable/ref/models/querysets/#field-lookups). This is the same syntax that you would pass to the QuerySet `filter()` method when performing a query using the Django ORM. As with query filters, double underscores can be used to traverse related objects or invoke lookup expressions. Some example queries and their corresponding definitions are shown below. + +All attributes defined within a single JSON object are applied with a logical AND. For example, suppose you assign a permission for the site model with the following constraints. + +```json +{ + "status": "active", + "region__name": "Americas" +} +``` + +The permission will grant access only to sites which have a status of "active" **and** which are assigned to the "Americas" region. + +To achieve a logical OR with a different set of constraints, define multiple objects within a list. For example, if you want to constrain the permission to VLANs with an ID between 100 and 199 _or_ a status of "reserved," do the following: + +```json +[ + { + "vid__gte": 100, + "vid__lt": 200 + }, + { + "status": "reserved" + } +] +``` + +Additionally, where multiple permissions have been assigned for an object type, their collective constraints will be merged using a logical "OR" operation. + +### User Token + +!!! info "This feature was introduced in NetBox v3.3" + +When defining a permission constraint, administrators may use the special token `$user` to reference the current user at the time of evaluation. This can be helpful to restrict users to editing only their own journal entries, for example. Such a constraint might be defined as: + +```json +{ + "created_by": "$user" +} +``` + +The `$user` token can be used only as a constraint value, or as an item within a list of values. It cannot be modified or extended to reference specific user attributes. + #### Example Constraint Definitions diff --git a/docs/configuration/miscellaneous.md b/docs/configuration/miscellaneous.md index 2aa21b7e5..614e90eac 100644 --- a/docs/configuration/miscellaneous.md +++ b/docs/configuration/miscellaneous.md @@ -127,7 +127,7 @@ A web user or API consumer can request an arbitrary number of objects by appendi Default: False -Toggle the availability Prometheus-compatible metrics at `/metrics`. See the [Prometheus Metrics](../additional-features/prometheus-metrics.md) documentation for more details. +Toggle the availability Prometheus-compatible metrics at `/metrics`. See the [Prometheus Metrics](../integrations/prometheus-metrics.md) documentation for more details. --- diff --git a/docs/configuration/napalm.md b/docs/configuration/napalm.md index 925ec17e6..253bea297 100644 --- a/docs/configuration/napalm.md +++ b/docs/configuration/napalm.md @@ -6,7 +6,7 @@ !!! tip "Dynamic Configuration Parameter" -NetBox will use these credentials when authenticating to remote devices via the supported [NAPALM integration](../additional-features/napalm.md), if installed. Both parameters are optional. +NetBox will use these credentials when authenticating to remote devices via the supported [NAPALM integration](../integrations/napalm.md), if installed. Both parameters are optional. !!! note If SSH public key authentication has been set up on the remote device(s) for the system account under which NetBox runs, these parameters are not needed. diff --git a/docs/customization/custom-fields.md b/docs/customization/custom-fields.md index 757416626..bfe412edc 100644 --- a/docs/customization/custom-fields.md +++ b/docs/customization/custom-fields.md @@ -1,4 +1,77 @@ -{!models/extras/customfield.md!} +# Custom Fields + +Each model in NetBox is represented in the database as a discrete table, and each attribute of a model exists as a column within its table. For example, sites are stored in the `dcim_site` table, which has columns named `name`, `facility`, `physical_address`, and so on. As new attributes are added to objects throughout the development of NetBox, tables are expanded to include new rows. + +However, some users might want to store additional object attributes that are somewhat esoteric in nature, and that would not make sense to include in the core NetBox database schema. For instance, suppose your organization needs to associate each device with a ticket number correlating it with an internal support system record. This is certainly a legitimate use for NetBox, but it's not a common enough need to warrant including a field for _every_ NetBox installation. Instead, you can create a custom field to hold this data. + +Within the database, custom fields are stored as JSON data directly alongside each object. This alleviates the need for complex queries when retrieving objects. + +## Creating Custom Fields + +Custom fields may be created by navigating to Customization > Custom Fields. NetBox supports six types of custom field: + +* Text: Free-form text (intended for single-line use) +* Long text: Free-form of any length; supports Markdown rendering +* Integer: A whole number (positive or negative) +* Boolean: True or false +* Date: A date in ISO 8601 format (YYYY-MM-DD) +* URL: This will be presented as a link in the web UI +* JSON: Arbitrary data stored in JSON format +* Selection: A selection of one of several pre-defined custom choices +* Multiple selection: A selection field which supports the assignment of multiple values +* Object: A single NetBox object of the type defined by `object_type` +* Multiple object: One or more NetBox objects of the type defined by `object_type` + +Each custom field must have a name. This should be a simple database-friendly string (e.g. `tps_report`) and may contain only alphanumeric characters and underscores. You may also assign a corresponding human-friendly label (e.g. "TPS report"); the label will be displayed on web forms. A weight is also required: Higher-weight fields will be ordered lower within a form. (The default weight is 100.) If a description is provided, it will appear beneath the field in a form. + +Marking a field as required will force the user to provide a value for the field when creating a new object or when saving an existing object. A default value for the field may also be provided. Use "true" or "false" for boolean fields, or the exact value of a choice for selection fields. + +A custom field must be assigned to one or more object types, or models, in NetBox. Once created, custom fields will automatically appear as part of these models in the web UI and REST API. Note that not all models support custom fields. + +### Filtering + +The filter logic controls how values are matched when filtering objects by the custom field. Loose filtering (the default) matches on a partial value, whereas exact matching requires a complete match of the given string to a field's value. For example, exact filtering with the string "red" will only match the exact value "red", whereas loose filtering will match on the values "red", "red-orange", or "bored". Setting the filter logic to "disabled" disables filtering by the field entirely. + +### Grouping + +!!! note + This feature was introduced in NetBox v3.3. + +Related custom fields can be grouped together within the UI by assigning each the same group name. When at least one custom field for an object type has a group defined, it will appear under the group heading within the custom fields panel under the object view. All custom fields with the same group name will appear under that heading. (Note that the group names must match exactly, or each will appear as a separate heading.) + +This parameter has no effect on the API representation of custom field data. + +### Visibility + +!!! note + This feature was introduced in NetBox v3.3. + +When creating a custom field, there are three options for UI visibility. These control how and whether the custom field is displayed within the NetBox UI. + +* **Read/write** (default): The custom field is included when viewing and editing objects. +* **Read-only**: The custom field is displayed when viewing an object, but it cannot be edited via the UI. (It will appear in the form as a read-only field.) +* **Hidden**: The custom field will never be displayed within the UI. This option is recommended for fields which are not intended for use by human users. + +Note that this setting has no impact on the REST or GraphQL APIs: Custom field data will always be available via either API. + +### Validation + +NetBox supports limited custom validation for custom field values. Following are the types of validation enforced for each field type: + +* Text: Regular expression (optional) +* Integer: Minimum and/or maximum value (optional) +* Selection: Must exactly match one of the prescribed choices + +### Custom Selection Fields + +Each custom selection field must have at least two choices. These are specified as a comma-separated list. Choices appear in forms in the order they are listed. Note that choice values are saved exactly as they appear, so it's best to avoid superfluous punctuation or symbols where possible. + +If a default value is specified for a selection field, it must exactly match one of the provided choices. The value of a multiple selection field will always return a list, even if only one value is selected. + +### Custom Object Fields + +An object or multi-object custom field can be used to refer to a particular NetBox object or objects as the "value" for a custom field. These custom fields must define an `object_type`, which determines the type of object to which custom field instances point. + ## Custom Fields in Templates diff --git a/docs/models/extras/customlink.md b/docs/customization/custom-links.md similarity index 100% rename from docs/models/extras/customlink.md rename to docs/customization/custom-links.md diff --git a/docs/customization/export-templates.md b/docs/customization/export-templates.md index 3c7ff7d20..640a97531 100644 --- a/docs/customization/export-templates.md +++ b/docs/customization/export-templates.md @@ -1,4 +1,41 @@ -{!models/extras/exporttemplate.md!} +# Export Templates + +NetBox allows users to define custom templates that can be used when exporting objects. To create an export template, navigate to Customization > Export Templates. + +Each export template is associated with a certain type of object. For instance, if you create an export template for VLANs, your custom template will appear under the "Export" button on the VLANs list. Each export template must have a name, and may optionally designate a specific export [MIME type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types) and/or file extension. + +Export templates must be written in [Jinja2](https://jinja.palletsprojects.com/). + +!!! note + The name `table` is reserved for internal use. + +!!! warning + Export templates are rendered using user-submitted code, which may pose security risks under certain conditions. Only grant permission to create or modify export templates to trusted users. + +The list of objects returned from the database when rendering an export template is stored in the `queryset` variable, which you'll typically want to iterate through using a `for` loop. Object properties can be access by name. For example: + +```jinja2 +{% for rack in queryset %} +Rack: {{ rack.name }} +Site: {{ rack.site.name }} +Height: {{ rack.u_height }}U +{% endfor %} +``` + +To access custom fields of an object within a template, use the `cf` attribute. For example, `{{ obj.cf.color }}` will return the value (if any) for a custom field named `color` on `obj`. + +If you need to use the config context data in an export template, you'll should use the function `get_config_context` to get all the config context data. For example: +``` +{% for server in queryset %} +{% set data = server.get_config_context() %} +{{ data.syslog }} +{% endfor %} +``` + +The `as_attachment` attribute of an export template controls its behavior when rendered. If true, the rendered content will be returned to the user as a downloadable file. If false, it will be displayed within the browser. (This may be handy e.g. for generating HTML content.) + +A MIME type and file extension can optionally be defined for each export template. The default MIME type is `text/plain`. + ## REST API Integration diff --git a/docs/development/models.md b/docs/development/models.md index b6b2e4da2..be3608a30 100644 --- a/docs/development/models.md +++ b/docs/development/models.md @@ -8,12 +8,12 @@ The Django [content types](https://docs.djangoproject.com/en/stable/ref/contrib/ ### Features Matrix -* [Change logging](../additional-features/change-logging.md) - Changes to these objects are automatically recorded in the change log -* [Webhooks](../additional-features/webhooks.md) - NetBox is capable of generating outgoing webhooks for these objects +* [Change logging](../features/change-logging.md) - Changes to these objects are automatically recorded in the change log +* [Webhooks](../features/webhooks.md) - NetBox is capable of generating outgoing webhooks for these objects * [Custom fields](../customization/custom-fields.md) - These models support the addition of user-defined fields * [Export templates](../customization/export-templates.md) - Users can create custom export templates for these models -* [Tagging](../models/extras/tag.md) - The models can be tagged with user-defined tags -* [Journaling](../additional-features/journaling.md) - These models support persistent historical commentary +* [Tagging](../features/tags.md) - The models can be tagged with user-defined tags +* [Journaling](../features/journaling.md) - These models support persistent historical commentary * Nesting - These models can be nested recursively to create a hierarchy | Type | Change Logging | Webhooks | Custom Fields | Export Templates | Tags | Journaling | Nesting | diff --git a/docs/additional-features/change-logging.md b/docs/features/change-logging.md similarity index 100% rename from docs/additional-features/change-logging.md rename to docs/features/change-logging.md diff --git a/docs/features/circuits.md b/docs/features/circuits.md new file mode 100644 index 000000000..63c8bc970 --- /dev/null +++ b/docs/features/circuits.md @@ -0,0 +1,3 @@ +# Circuits + +TODO diff --git a/docs/features/contacts.md b/docs/features/contacts.md new file mode 100644 index 000000000..fb23e53da --- /dev/null +++ b/docs/features/contacts.md @@ -0,0 +1,3 @@ +# Contacts + +TODO diff --git a/docs/models/extras/configcontext.md b/docs/features/context-data.md similarity index 100% rename from docs/models/extras/configcontext.md rename to docs/features/context-data.md diff --git a/docs/features/customization.md b/docs/features/customization.md new file mode 100644 index 000000000..ae775312e --- /dev/null +++ b/docs/features/customization.md @@ -0,0 +1,3 @@ +# Customization + +TODO diff --git a/docs/features/devices-cabling.md b/docs/features/devices-cabling.md new file mode 100644 index 000000000..ab09b4443 --- /dev/null +++ b/docs/features/devices-cabling.md @@ -0,0 +1,3 @@ +# Devices & Cabling + +TODO diff --git a/docs/features/facilities.md b/docs/features/facilities.md new file mode 100644 index 000000000..5ace5b18b --- /dev/null +++ b/docs/features/facilities.md @@ -0,0 +1,3 @@ +# Facilities + +TODO \ No newline at end of file diff --git a/docs/features/ipam.md b/docs/features/ipam.md new file mode 100644 index 000000000..f263f3ab3 --- /dev/null +++ b/docs/features/ipam.md @@ -0,0 +1,3 @@ +# IP Address Management + +TODO diff --git a/docs/additional-features/journaling.md b/docs/features/journaling.md similarity index 100% rename from docs/additional-features/journaling.md rename to docs/features/journaling.md diff --git a/docs/features/l2vpn-overlay.md b/docs/features/l2vpn-overlay.md new file mode 100644 index 000000000..002c483f9 --- /dev/null +++ b/docs/features/l2vpn-overlay.md @@ -0,0 +1,3 @@ +# L2VPN & Overlay + +TODO diff --git a/docs/features/permissions.md b/docs/features/permissions.md new file mode 100644 index 000000000..a422ca7b3 --- /dev/null +++ b/docs/features/permissions.md @@ -0,0 +1,3 @@ +# Object-Based Permissions + +TODO diff --git a/docs/features/power-tracking.md b/docs/features/power-tracking.md new file mode 100644 index 000000000..1267af1e6 --- /dev/null +++ b/docs/features/power-tracking.md @@ -0,0 +1,3 @@ +# Power Tracking + +TODO diff --git a/docs/features/services.md b/docs/features/services.md new file mode 100644 index 000000000..338fc349a --- /dev/null +++ b/docs/features/services.md @@ -0,0 +1,3 @@ +# Services + +TODO diff --git a/docs/features/sso.md b/docs/features/sso.md new file mode 100644 index 000000000..b4f9782c2 --- /dev/null +++ b/docs/features/sso.md @@ -0,0 +1,3 @@ +# Single Sign-On (SSO) + +TODO diff --git a/docs/models/extras/tag.md b/docs/features/tags.md similarity index 100% rename from docs/models/extras/tag.md rename to docs/features/tags.md diff --git a/docs/features/tenancy.md b/docs/features/tenancy.md new file mode 100644 index 000000000..20534b13c --- /dev/null +++ b/docs/features/tenancy.md @@ -0,0 +1,3 @@ +# Tenancy + +TODO diff --git a/docs/features/vlan-management.md b/docs/features/vlan-management.md new file mode 100644 index 000000000..3d4fd4d1f --- /dev/null +++ b/docs/features/vlan-management.md @@ -0,0 +1,3 @@ +# VLAN Management + +TODO diff --git a/docs/models/extras/webhook.md b/docs/features/webhooks.md similarity index 67% rename from docs/models/extras/webhook.md rename to docs/features/webhooks.md index 9f64401ae..4705243d1 100644 --- a/docs/models/extras/webhook.md +++ b/docs/features/webhooks.md @@ -81,3 +81,59 @@ If no body template is specified, the request body will be populated with a JSON } } ``` + +## Conditional Webhooks + +A webhook may include a set of conditional logic expressed in JSON used to control whether a webhook triggers for a specific object. For example, you may wish to trigger a webhook for devices only when the `status` field of an object is "active": + +```json +{ + "and": [ + { + "attr": "status.value", + "value": "active" + } + ] +} +``` + +For more detail, see the reference documentation for NetBox's [conditional logic](../reference/conditions.md). + +## Webhook Processing + +When a change is detected, any resulting webhooks are placed into a Redis queue for processing. This allows the user's request to complete without needing to wait for the outgoing webhook(s) to be processed. The webhooks are then extracted from the queue by the `rqworker` process and HTTP requests are sent to their respective destinations. The current webhook queue and any failed webhooks can be inspected in the admin UI under System > Background Tasks. + +A request is considered successful if the response has a 2XX status code; otherwise, the request is marked as having failed. Failed requests may be retried manually via the admin UI. + +## Troubleshooting + +To assist with verifying that the content of outgoing webhooks is rendered correctly, NetBox provides a simple HTTP listener that can be run locally to receive and display webhook requests. First, modify the target URL of the desired webhook to `http://localhost:9000/`. This will instruct NetBox to send the request to the local server on TCP port 9000. Then, start the webhook receiver service from the NetBox root directory: + +```no-highlight +$ python netbox/manage.py webhook_receiver +Listening on port http://localhost:9000. Stop with CONTROL-C. +``` + +You can test the receiver itself by sending any HTTP request to it. For example: + +```no-highlight +$ curl -X POST http://localhost:9000 --data '{"foo": "bar"}' +``` + +The server will print output similar to the following: + +```no-highlight +[1] Tue, 07 Apr 2020 17:44:02 GMT 127.0.0.1 "POST / HTTP/1.1" 200 - +Host: localhost:9000 +User-Agent: curl/7.58.0 +Accept: */* +Content-Length: 14 +Content-Type: application/x-www-form-urlencoded + +{"foo": "bar"} +------------ +``` + +Note that `webhook_receiver` does not actually _do_ anything with the information received: It merely prints the request headers and body for inspection. + +Now, when the NetBox webhook is triggered and processed, you should see its headers and content appear in the terminal where the webhook receiver is listening. If you don't, check that the `rqworker` process is running and that webhook events are being placed into the queue (visible under the NetBox admin UI). diff --git a/docs/features/wireless.md b/docs/features/wireless.md new file mode 100644 index 000000000..ff3d5d10b --- /dev/null +++ b/docs/features/wireless.md @@ -0,0 +1,3 @@ +# Wireless + +TODO diff --git a/docs/getting-started/populating-data.md b/docs/getting-started/populating-data.md index e182a9d52..bb0e8e17f 100644 --- a/docs/getting-started/populating-data.md +++ b/docs/getting-started/populating-data.md @@ -39,4 +39,4 @@ Sometimes you'll find that data you need to populate in NetBox can be easily red You can also use the REST API to facilitate the population of data in NetBox. The REST API offers full programmatic control over the creation of objects, subject to the same validation rules enforced by the UI forms. Additionally, the REST API supports the bulk creation of multiple objects using a single request. -For more information about this option, see the [REST API documentation](../rest-api/overview.md). +For more information about this option, see the [REST API documentation](../integrations/rest-api.md). diff --git a/docs/installation/3-netbox.md b/docs/installation/3-netbox.md index 7c4a60500..eeb5e6f20 100644 --- a/docs/installation/3-netbox.md +++ b/docs/installation/3-netbox.md @@ -201,7 +201,7 @@ All Python packages required by NetBox are listed in `requirements.txt` and will ### NAPALM -Integration with the [NAPALM automation](../additional-features/napalm.md) library allows NetBox to fetch live data from devices and return it to a requester via its REST API. The `NAPALM_USERNAME` and `NAPALM_PASSWORD` configuration parameters define the credentials to be used when connecting to a device. +Integration with the [NAPALM automation](../integrations/napalm.md) library allows NetBox to fetch live data from devices and return it to a requester via its REST API. The `NAPALM_USERNAME` and `NAPALM_PASSWORD` configuration parameters define the credentials to be used when connecting to a device. ```no-highlight sudo sh -c "echo 'napalm' >> /opt/netbox/local_requirements.txt" diff --git a/docs/graphql-api/overview.md b/docs/integrations/graphql-api.md similarity index 100% rename from docs/graphql-api/overview.md rename to docs/integrations/graphql-api.md diff --git a/docs/additional-features/napalm.md b/docs/integrations/napalm.md similarity index 100% rename from docs/additional-features/napalm.md rename to docs/integrations/napalm.md diff --git a/docs/additional-features/prometheus-metrics.md b/docs/integrations/prometheus-metrics.md similarity index 100% rename from docs/additional-features/prometheus-metrics.md rename to docs/integrations/prometheus-metrics.md diff --git a/docs/rest-api/overview.md b/docs/integrations/rest-api.md similarity index 78% rename from docs/rest-api/overview.md rename to docs/integrations/rest-api.md index 5fc4f18bb..3a5aed055 100644 --- a/docs/rest-api/overview.md +++ b/docs/integrations/rest-api.md @@ -91,7 +91,7 @@ Lists of objects can be filtered using a set of query parameters. For example, t GET /api/dcim/interfaces/?device_id=123 ``` -See the [filtering documentation](filtering.md) for more details. +See the [filtering documentation](../reference/filtering.md) for more details. ## Serialization @@ -269,7 +269,7 @@ The brief format is supported for both lists and individual objects. ### Excluding Config Contexts -When retrieving devices and virtual machines via the REST API, each will included its rendered [configuration context data](../models/extras/configcontext.md) by default. Users with large amounts of context data will likely observe suboptimal performance when returning multiple objects, particularly with very high page sizes. To combat this, context data may be excluded from the response data by attaching the query parameter `?exclude=config_context` to the request. This parameter works for both list and detail views. +When retrieving devices and virtual machines via the REST API, each will include its rendered [configuration context data](../features/context-data.md) by default. Users with large amounts of context data will likely observe suboptimal performance when returning multiple objects, particularly with very high page sizes. To combat this, context data may be excluded from the response data by attaching the query parameter `?exclude=config_context` to the request. This parameter works for both list and detail views. ## Pagination @@ -387,7 +387,7 @@ curl -s -X GET http://netbox/api/ipam/ip-addresses/5618/ | jq '.' ### Creating a New Object -To create a new object, make a `POST` request to the model's _list_ endpoint with JSON data pertaining to the object being created. Note that a REST API token is required for all write operations; see the [authentication documentation](authentication.md) for more information. Also be sure to set the `Content-Type` HTTP header to `application/json`. +To create a new object, make a `POST` request to the model's _list_ endpoint with JSON data pertaining to the object being created. Note that a REST API token is required for all write operations; see the [authentication section](#authenticating-to-the-api) for more information. Also be sure to set the `Content-Type` HTTP header to `application/json`. ```no-highlight curl -s -X POST \ @@ -561,3 +561,96 @@ http://netbox/api/dcim/sites/ \ !!! note The bulk deletion of objects is an all-or-none operation, meaning that if NetBox fails to delete any of the specified objects (e.g. due a dependency by a related object), the entire operation will be aborted and none of the objects will be deleted. + +## Authentication + +The NetBox REST API primarily employs token-based authentication. For convenience, cookie-based authentication can also be used when navigating the browsable API. + +### Tokens + +A token is a unique identifier mapped to a NetBox user account. Each user may have one or more tokens which he or she can use for authentication when making REST API requests. To create a token, navigate to the API tokens page under your user profile. + +!!! note + All users can create and manage REST API tokens under the user control panel in the UI. The ability to view, add, change, or delete tokens via the REST API itself is controlled by the relevant model permissions, assigned to users and/or groups in the admin UI. These permissions should be used with great care to avoid accidentally permitting a user to create tokens for other user accounts. + +Each token contains a 160-bit key represented as 40 hexadecimal characters. When creating a token, you'll typically leave the key field blank so that a random key will be automatically generated. However, NetBox allows you to specify a key in case you need to restore a previously deleted token to operation. + +By default, a token can be used to perform all actions via the API that a user would be permitted to do via the web UI. Deselecting the "write enabled" option will restrict API requests made with the token to read operations (e.g. GET) only. + +Additionally, a token can be set to expire at a specific time. This can be useful if an external client needs to be granted temporary access to NetBox. + +#### Client IP Restriction + +!!! note + This feature was introduced in NetBox v3.3. + +Each API token can optionally be restricted by client IP address. If one or more allowed IP prefixes/addresses is defined for a token, authentication will fail for any client connecting from an IP address outside the defined range(s). This enables restricting the use a token to a specific client. (By default, any client IP address is permitted.) + + +### Authenticating to the API + +An authentication token is attached to a request by setting the `Authorization` header to the string `Token` followed by a space and the user's token: + +``` +$ curl -H "Authorization: Token $TOKEN" \ +-H "Accept: application/json; indent=4" \ +https://netbox/api/dcim/sites/ +{ + "count": 10, + "next": null, + "previous": null, + "results": [...] +} +``` + +A token is not required for read-only operations which have been exempted from permissions enforcement (using the [`EXEMPT_VIEW_PERMISSIONS`](../configuration/security.md#exempt_view_permissions) configuration parameter). However, if a token _is_ required but not present in a request, the API will return a 403 (Forbidden) response: + +``` +$ curl https://netbox/api/dcim/sites/ +{ + "detail": "Authentication credentials were not provided." +} +``` + +When a token is used to authenticate a request, its `last_updated` time updated to the current time if its last use was recorded more than 60 seconds ago (or was never recorded). This allows users to determine which tokens have been active recently. + +!!! note + The "last used" time for tokens will not be updated while maintenance mode is enabled. + +### Initial Token Provisioning + +Ideally, each user should provision his or her own REST API token(s) via the web UI. However, you may encounter where a token must be created by a user via the REST API itself. NetBox provides a special endpoint to provision tokens using a valid username and password combination. + +To provision a token via the REST API, make a `POST` request to the `/api/users/tokens/provision/` endpoint: + +``` +$ curl -X POST \ +-H "Content-Type: application/json" \ +-H "Accept: application/json; indent=4" \ +https://netbox/api/users/tokens/provision/ \ +--data '{ + "username": "hankhill", + "password": "I<3C3H8", +}' +``` + +Note that we are _not_ passing an existing REST API token with this request. If the supplied credentials are valid, a new REST API token will be automatically created for the user. Note that the key will be automatically generated, and write ability will be enabled. + +```json +{ + "id": 6, + "url": "https://netbox/api/users/tokens/6/", + "display": "3c9cb9 (hankhill)", + "user": { + "id": 2, + "url": "https://netbox/api/users/users/2/", + "display": "hankhill", + "username": "hankhill" + }, + "created": "2021-06-11T20:09:13.339367Z", + "expires": null, + "key": "9fc9b897abec9ada2da6aec9dbc34596293c9cb9", + "write_enabled": true, + "description": "" +} +``` diff --git a/docs/models/extras/customfield.md b/docs/models/extras/customfield.md deleted file mode 100644 index 3f6860758..000000000 --- a/docs/models/extras/customfield.md +++ /dev/null @@ -1,73 +0,0 @@ -# Custom Fields - -Each model in NetBox is represented in the database as a discrete table, and each attribute of a model exists as a column within its table. For example, sites are stored in the `dcim_site` table, which has columns named `name`, `facility`, `physical_address`, and so on. As new attributes are added to objects throughout the development of NetBox, tables are expanded to include new rows. - -However, some users might want to store additional object attributes that are somewhat esoteric in nature, and that would not make sense to include in the core NetBox database schema. For instance, suppose your organization needs to associate each device with a ticket number correlating it with an internal support system record. This is certainly a legitimate use for NetBox, but it's not a common enough need to warrant including a field for _every_ NetBox installation. Instead, you can create a custom field to hold this data. - -Within the database, custom fields are stored as JSON data directly alongside each object. This alleviates the need for complex queries when retrieving objects. - -## Creating Custom Fields - -Custom fields may be created by navigating to Customization > Custom Fields. NetBox supports six types of custom field: - -* Text: Free-form text (intended for single-line use) -* Long text: Free-form of any length; supports Markdown rendering -* Integer: A whole number (positive or negative) -* Boolean: True or false -* Date: A date in ISO 8601 format (YYYY-MM-DD) -* URL: This will be presented as a link in the web UI -* JSON: Arbitrary data stored in JSON format -* Selection: A selection of one of several pre-defined custom choices -* Multiple selection: A selection field which supports the assignment of multiple values -* Object: A single NetBox object of the type defined by `object_type` -* Multiple object: One or more NetBox objects of the type defined by `object_type` - -Each custom field must have a name. This should be a simple database-friendly string (e.g. `tps_report`) and may contain only alphanumeric characters and underscores. You may also assign a corresponding human-friendly label (e.g. "TPS report"); the label will be displayed on web forms. A weight is also required: Higher-weight fields will be ordered lower within a form. (The default weight is 100.) If a description is provided, it will appear beneath the field in a form. - -Marking a field as required will force the user to provide a value for the field when creating a new object or when saving an existing object. A default value for the field may also be provided. Use "true" or "false" for boolean fields, or the exact value of a choice for selection fields. - -A custom field must be assigned to one or more object types, or models, in NetBox. Once created, custom fields will automatically appear as part of these models in the web UI and REST API. Note that not all models support custom fields. - -### Filtering - -The filter logic controls how values are matched when filtering objects by the custom field. Loose filtering (the default) matches on a partial value, whereas exact matching requires a complete match of the given string to a field's value. For example, exact filtering with the string "red" will only match the exact value "red", whereas loose filtering will match on the values "red", "red-orange", or "bored". Setting the filter logic to "disabled" disables filtering by the field entirely. - -### Grouping - -!!! note - This feature was introduced in NetBox v3.3. - -Related custom fields can be grouped together within the UI by assigning each the same group name. When at least one custom field for an object type has a group defined, it will appear under the group heading within the custom fields panel under the object view. All custom fields with the same group name will appear under that heading. (Note that the group names must match exactly, or each will appear as a separate heading.) - -This parameter has no effect on the API representation of custom field data. - -### Visibility - -!!! note - This feature was introduced in NetBox v3.3. - -When creating a custom field, there are three options for UI visibility. These control how and whether the custom field is displayed within the NetBox UI. - -* **Read/write** (default): The custom field is included when viewing and editing objects. -* **Read-only**: The custom field is displayed when viewing an object, but it cannot be edited via the UI. (It will appear in the form as a read-only field.) -* **Hidden**: The custom field will never be displayed within the UI. This option is recommended for fields which are not intended for use by human users. - -Note that this setting has no impact on the REST or GraphQL APIs: Custom field data will always be available via either API. - -### Validation - -NetBox supports limited custom validation for custom field values. Following are the types of validation enforced for each field type: - -* Text: Regular expression (optional) -* Integer: Minimum and/or maximum value (optional) -* Selection: Must exactly match one of the prescribed choices - -### Custom Selection Fields - -Each custom selection field must have at least two choices. These are specified as a comma-separated list. Choices appear in forms in the order they are listed. Note that choice values are saved exactly as they appear, so it's best to avoid superfluous punctuation or symbols where possible. - -If a default value is specified for a selection field, it must exactly match one of the provided choices. The value of a multiple selection field will always return a list, even if only one value is selected. - -### Custom Object Fields - -An object or multi-object custom field can be used to refer to a particular NetBox object or objects as the "value" for a custom field. These custom fields must define an `object_type`, which determines the type of object to which custom field instances point. diff --git a/docs/models/extras/exporttemplate.md b/docs/models/extras/exporttemplate.md deleted file mode 100644 index e76a3ad47..000000000 --- a/docs/models/extras/exporttemplate.md +++ /dev/null @@ -1,37 +0,0 @@ -# Export Templates - -NetBox allows users to define custom templates that can be used when exporting objects. To create an export template, navigate to Customization > Export Templates. - -Each export template is associated with a certain type of object. For instance, if you create an export template for VLANs, your custom template will appear under the "Export" button on the VLANs list. Each export template must have a name, and may optionally designate a specific export [MIME type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types) and/or file extension. - -Export templates must be written in [Jinja2](https://jinja.palletsprojects.com/). - -!!! note - The name `table` is reserved for internal use. - -!!! warning - Export templates are rendered using user-submitted code, which may pose security risks under certain conditions. Only grant permission to create or modify export templates to trusted users. - -The list of objects returned from the database when rendering an export template is stored in the `queryset` variable, which you'll typically want to iterate through using a `for` loop. Object properties can be access by name. For example: - -```jinja2 -{% for rack in queryset %} -Rack: {{ rack.name }} -Site: {{ rack.site.name }} -Height: {{ rack.u_height }}U -{% endfor %} -``` - -To access custom fields of an object within a template, use the `cf` attribute. For example, `{{ obj.cf.color }}` will return the value (if any) for a custom field named `color` on `obj`. - -If you need to use the config context data in an export template, you'll should use the function `get_config_context` to get all the config context data. For example: -``` -{% for server in queryset %} -{% set data = server.get_config_context() %} -{{ data.syslog }} -{% endfor %} -``` - -The `as_attachment` attribute of an export template controls its behavior when rendered. If true, the rendered content will be returned to the user as a downloadable file. If false, it will be displayed within the browser. (This may be handy e.g. for generating HTML content.) - -A MIME type and file extension can optionally be defined for each export template. The default MIME type is `text/plain`. diff --git a/docs/models/extras/imageattachment.md b/docs/models/extras/imageattachment.md deleted file mode 100644 index da15462ab..000000000 --- a/docs/models/extras/imageattachment.md +++ /dev/null @@ -1,3 +0,0 @@ -# Image Attachments - -Certain objects in NetBox support the attachment of uploaded images. These will be saved to the NetBox server and made available whenever the object is viewed. diff --git a/docs/models/users/objectpermission.md b/docs/models/users/objectpermission.md deleted file mode 100644 index 82dbc955a..000000000 --- a/docs/models/users/objectpermission.md +++ /dev/null @@ -1,69 +0,0 @@ -# Object Permissions - -A permission in NetBox represents a relationship shared by several components: - -* Object type(s) - One or more types of object in NetBox -* User(s)/Group(s) - One or more users or groups of users -* Action(s) - The action(s) that can be performed on an object -* Constraints - An arbitrary filter used to limit the granted action(s) to a specific subset of objects - -At a minimum, a permission assignment must specify one object type, one user or group, and one action. The specification of constraints is optional: A permission without any constraints specified will apply to all instances of the selected model(s). - -## Actions - -There are four core actions that can be permitted for each type of object within NetBox, roughly analogous to the CRUD convention (create, read, update, and delete): - -* **View** - Retrieve an object from the database -* **Add** - Create a new object -* **Change** - Modify an existing object -* **Delete** - Delete an existing object - -In addition to these, permissions can also grant custom actions that may be required by a specific model or plugin. For example, the `napalm_read` permission on the device model allows a user to execute NAPALM queries on a device via NetBox's REST API. These can be specified when granting a permission in the "additional actions" field. - -!!! note - Internally, all actions granted by a permission (both built-in and custom) are stored as strings in an array field named `actions`. - -## Constraints - -Constraints are expressed as a JSON object or list representing a [Django query filter](https://docs.djangoproject.com/en/stable/ref/models/querysets/#field-lookups). This is the same syntax that you would pass to the QuerySet `filter()` method when performing a query using the Django ORM. As with query filters, double underscores can be used to traverse related objects or invoke lookup expressions. Some example queries and their corresponding definitions are shown below. - -All attributes defined within a single JSON object are applied with a logical AND. For example, suppose you assign a permission for the site model with the following constraints. - -```json -{ - "status": "active", - "region__name": "Americas" -} -``` - -The permission will grant access only to sites which have a status of "active" **and** which are assigned to the "Americas" region. - -To achieve a logical OR with a different set of constraints, define multiple objects within a list. For example, if you want to constrain the permission to VLANs with an ID between 100 and 199 _or_ a status of "reserved," do the following: - -```json -[ - { - "vid__gte": 100, - "vid__lt": 200 - }, - { - "status": "reserved" - } -] -``` - -Additionally, where multiple permissions have been assigned for an object type, their collective constraints will be merged using a logical "OR" operation. - -### User Token - -!!! info "This feature was introduced in NetBox v3.3" - -When defining a permission constraint, administrators may use the special token `$user` to reference the current user at the time of evaluation. This can be helpful to restrict users to editing only their own journal entries, for example. Such a constraint might be defined as: - -```json -{ - "created_by": "$user" -} -``` - -The `$user` token can be used only as a constraint value, or as an item within a list of values. It cannot be modified or extended to reference specific user attributes. diff --git a/docs/models/users/token.md b/docs/models/users/token.md deleted file mode 100644 index f6c5bfe80..000000000 --- a/docs/models/users/token.md +++ /dev/null @@ -1,19 +0,0 @@ -## Tokens - -A token is a unique identifier mapped to a NetBox user account. Each user may have one or more tokens which he or she can use for authentication when making REST API requests. To create a token, navigate to the API tokens page under your user profile. - -!!! note - All users can create and manage REST API tokens under the user control panel in the UI. The ability to view, add, change, or delete tokens via the REST API itself is controlled by the relevant model permissions, assigned to users and/or groups in the admin UI. These permissions should be used with great care to avoid accidentally permitting a user to create tokens for other user accounts. - -Each token contains a 160-bit key represented as 40 hexadecimal characters. When creating a token, you'll typically leave the key field blank so that a random key will be automatically generated. However, NetBox allows you to specify a key in case you need to restore a previously deleted token to operation. - -By default, a token can be used to perform all actions via the API that a user would be permitted to do via the web UI. Deselecting the "write enabled" option will restrict API requests made with the token to read operations (e.g. GET) only. - -Additionally, a token can be set to expire at a specific time. This can be useful if an external client needs to be granted temporary access to NetBox. - -### Client IP Restriction - -!!! note - This feature was introduced in NetBox v3.3. - -Each API token can optionally be restricted by client IP address. If one or more allowed IP prefixes/addresses is defined for a token, authentication will fail for any client connecting from an IP address outside the defined range(s). This enables restricting the use a token to a specific client. (By default, any client IP address is permitted.) diff --git a/docs/rest-api/filtering.md b/docs/reference/filtering.md similarity index 100% rename from docs/rest-api/filtering.md rename to docs/reference/filtering.md diff --git a/docs/release-notes/version-3.0.md b/docs/release-notes/version-3.0.md index 93ff33d95..06b889c22 100644 --- a/docs/release-notes/version-3.0.md +++ b/docs/release-notes/version-3.0.md @@ -357,7 +357,7 @@ And the response: ... ``` -All GraphQL requests are made at the `/graphql` URL (which also serves the GraphiQL UI). The API is currently read-only, however users who wish to disable it until needed can do so by setting the `GRAPHQL_ENABLED` configuration parameter to False. For more detail on NetBox's GraphQL implementation, see [the GraphQL API documentation](../graphql-api/overview.md). +All GraphQL requests are made at the `/graphql` URL (which also serves the GraphiQL UI). The API is currently read-only, however users who wish to disable it until needed can do so by setting the `GRAPHQL_ENABLED` configuration parameter to False. For more detail on NetBox's GraphQL implementation, see [the GraphQL API documentation](../integrations/graphql-api.md). #### IP Ranges ([#834](https://github.com/netbox-community/netbox/issues/834)) diff --git a/docs/rest-api/authentication.md b/docs/rest-api/authentication.md deleted file mode 100644 index 411063338..000000000 --- a/docs/rest-api/authentication.md +++ /dev/null @@ -1,73 +0,0 @@ -# REST API Authentication - -The NetBox REST API primarily employs token-based authentication. For convenience, cookie-based authentication can also be used when navigating the browsable API. - -{!models/users/token.md!} - -## Authenticating to the API - -An authentication token is attached to a request by setting the `Authorization` header to the string `Token` followed by a space and the user's token: - -``` -$ curl -H "Authorization: Token $TOKEN" \ --H "Accept: application/json; indent=4" \ -https://netbox/api/dcim/sites/ -{ - "count": 10, - "next": null, - "previous": null, - "results": [...] -} -``` - -A token is not required for read-only operations which have been exempted from permissions enforcement (using the [`EXEMPT_VIEW_PERMISSIONS`](../configuration/security.md#exempt_view_permissions) configuration parameter). However, if a token _is_ required but not present in a request, the API will return a 403 (Forbidden) response: - -``` -$ curl https://netbox/api/dcim/sites/ -{ - "detail": "Authentication credentials were not provided." -} -``` - -When a token is used to authenticate a request, its `last_updated` time updated to the current time if its last use was recorded more than 60 seconds ago (or was never recorded). This allows users to determine which tokens have been active recently. - -!!! note - The "last used" time for tokens will not be updated while maintenance mode is enabled. - -## Initial Token Provisioning - -Ideally, each user should provision his or her own REST API token(s) via the web UI. However, you may encounter where a token must be created by a user via the REST API itself. NetBox provides a special endpoint to provision tokens using a valid username and password combination. - -To provision a token via the REST API, make a `POST` request to the `/api/users/tokens/provision/` endpoint: - -``` -$ curl -X POST \ --H "Content-Type: application/json" \ --H "Accept: application/json; indent=4" \ -https://netbox/api/users/tokens/provision/ \ ---data '{ - "username": "hankhill", - "password": "I<3C3H8", -}' -``` - -Note that we are _not_ passing an existing REST API token with this request. If the supplied credentials are valid, a new REST API token will be automatically created for the user. Note that the key will be automatically generated, and write ability will be enabled. - -```json -{ - "id": 6, - "url": "https://netbox/api/users/tokens/6/", - "display": "3c9cb9 (hankhill)", - "user": { - "id": 2, - "url": "https://netbox/api/users/users/2/", - "display": "hankhill", - "username": "hankhill" - }, - "created": "2021-06-11T20:09:13.339367Z", - "expires": null, - "key": "9fc9b897abec9ada2da6aec9dbc34596293c9cb9", - "write_enabled": true, - "description": "" -} -``` diff --git a/mkdocs.yml b/mkdocs.yml index 4c5279127..f76a91484 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -62,6 +62,26 @@ markdown_extensions: alternate_style: true nav: - Introduction: 'introduction.md' + - Features: + - Facilities: 'features/facilities.md' + - Tenancy: 'features/tenancy.md' + - Contacts: 'features/contacts.md' + - Devices & Cabling: 'features/devices-cabling.md' + - Power Tracking: 'features/power-tracking.md' + - IPAM: 'features/ipam.md' + - VLAN Management: 'features/vlan-management.md' + - Service Mapping: 'features/services.md' + - L2VPN & Overlay: 'features/l2vpn-overlay.md' + - Circuits: 'features/circuits.md' + - Wireless: 'features/wireless.md' + - Context Data: 'features/context-data.md' + - Change Logging: 'features/change-logging.md' + - Journaling: 'features/journaling.md' + - Webhooks: 'features/webhooks.md' + - Tags: 'features/tags.md' + - Customization: 'features/customization.md' + - Object-Based Permissions: 'features/permissions.md' + - Single Sign-On (SSO): 'features/sso.md' - Installation & Upgrade: - Installing NetBox: 'installation/index.md' - 1. PostgreSQL: 'installation/1-postgresql.md' @@ -90,19 +110,16 @@ nav: - Development: 'configuration/development.md' - Customization: - Custom Fields: 'customization/custom-fields.md' + - Custom Links: 'customization/custom-links.md' - Custom Validation: 'customization/custom-validation.md' - - Custom Links: 'models/extras/customlink.md' - Export Templates: 'customization/export-templates.md' - - Custom Scripts: 'customization/custom-scripts.md' - Reports: 'customization/reports.md' - - Additional Features: - - Change Logging: 'additional-features/change-logging.md' - - Context Data: 'models/extras/configcontext.md' - - Journaling: 'additional-features/journaling.md' - - NAPALM: 'additional-features/napalm.md' - - Prometheus Metrics: 'additional-features/prometheus-metrics.md' - - Tags: 'models/extras/tag.md' - - Webhooks: 'additional-features/webhooks.md' + - Custom Scripts: 'customization/custom-scripts.md' + - Integrations: + - REST API: 'integrations/rest-api.md' + - GraphQL API: 'integrations/graphql-api.md' + - NAPALM: 'integrations/napalm.md' + - Prometheus Metrics: 'integrations/prometheus-metrics.md' - Plugins: - Using Plugins: 'plugins/index.md' - Developing Plugins: @@ -128,12 +145,6 @@ nav: - Housekeeping: 'administration/housekeeping.md' - Replicating NetBox: 'administration/replicating-netbox.md' - NetBox Shell: 'administration/netbox-shell.md' - - REST API: - - Overview: 'rest-api/overview.md' - - Filtering & Ordering: 'rest-api/filtering.md' - - Authentication: 'rest-api/authentication.md' - - GraphQL API: - - Overview: 'graphql-api/overview.md' - Data Model: - Circuits: - Circuit: 'models/circuits/circuit.md' @@ -143,8 +154,6 @@ nav: - Provider Network: 'models/circuits/providernetwork.md' - DCIM: - Cable: 'models/dcim/cable.md' - - CablePath: 'models/dcim/cablepath.md' - - CableTermination: 'models/dcim/cabletermination.md' - ConsolePort: 'models/dcim/consoleport.md' - ConsolePortTemplate: 'models/dcim/consoleporttemplate.md' - ConsoleServerPort: 'models/dcim/consoleserverport.md' @@ -203,7 +212,6 @@ nav: - VRF: 'models/ipam/vrf.md' - Tenancy: - Contact: 'models/tenancy/contact.md' - - ContactAssignment: 'models/tenancy/contactassignment.md' - ContactGroup: 'models/tenancy/contactgroup.md' - ContactRole: 'models/tenancy/contactrole.md' - Tenant: 'models/tenancy/tenant.md' @@ -219,6 +227,7 @@ nav: - WirelessLANGroup: 'models/wireless/wirelesslangroup.md' - WirelessLink: 'models/wireless/wirelesslink.md' - Reference: + - Filtering: 'reference/filtering.md' - Conditions: 'reference/conditions.md' - Markdown: 'reference/markdown.md' - Development: From 4c899f151cce9fae6ae1596c481f2fa92e6a6cf9 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Thu, 4 Aug 2022 14:12:51 -0400 Subject: [PATCH 344/593] Drop markdown-include --- mkdocs.yml | 3 --- requirements.txt | 1 - 2 files changed, 4 deletions(-) diff --git a/mkdocs.yml b/mkdocs.yml index f76a91484..78ca5031c 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -47,9 +47,6 @@ extra_css: markdown_extensions: - admonition - attr_list - - markdown_include.include: - base_path: 'docs/' - headingOffset: 1 - pymdownx.emoji: emoji_index: !!python/name:materialx.emoji.twemoji emoji_generator: !!python/name:materialx.emoji.to_svg diff --git a/requirements.txt b/requirements.txt index 8a7dd79d4..facf925f4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,7 +19,6 @@ graphene-django==2.15.0 gunicorn==20.1.0 Jinja2==3.1.2 Markdown==3.4.1 -markdown-include==0.7.0 mkdocs-material==8.3.9 mkdocstrings[python-legacy]==0.19.0 netaddr==0.8.0 From 8c0ef1a0a2e324a8b46af2591a9f1b087fcecb4d Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Thu, 4 Aug 2022 16:35:32 -0400 Subject: [PATCH 345/593] Started on feature docs --- docs/features/contacts.md | 23 +++++++++- docs/features/devices-cabling.md | 78 +++++++++++++++++++++++++++++++- docs/features/facilities.md | 57 ++++++++++++++++++++++- docs/features/tenancy.md | 18 +++++++- 4 files changed, 172 insertions(+), 4 deletions(-) diff --git a/docs/features/contacts.md b/docs/features/contacts.md index fb23e53da..7d717b2de 100644 --- a/docs/features/contacts.md +++ b/docs/features/contacts.md @@ -1,3 +1,24 @@ # Contacts -TODO +Much like [tenancy](./tenancy.md), contact assignment enables you to track ownership of resources modeled in NetBox. A contact represents an individual responsible for a resource within the context of its assigned role. + +```mermaid +flowchart TD + ContactGroup --> ContactGroup & Contact + ContactRole & Contact --> assignment([Assignment]) + assignment --> Object +``` + +## Contact Groups + +Contacts can be grouped arbitrarily into a recursive hierarchy, and a contact can be assigned to a group at any level within the hierarchy. + +## Contact Roles + +A contact role defines the relationship of a contact to an assigned object. For example, you might define roles for administrative, operational, and emergency contacts. + +## Contacts + +A contact should represent an individual or permanent point of contact. Each contact must define a name, and may optionally include a title, phone number, email address, and related details. + +Contacts are reused for assignments, so each unique contact must be created only once and can be assigned to any number of NetBox objects, and there is no limit to the number of assigned contacts an object may have. Most core objects in NetBox can have contacts assigned to them. diff --git a/docs/features/devices-cabling.md b/docs/features/devices-cabling.md index ab09b4443..7297f79ac 100644 --- a/docs/features/devices-cabling.md +++ b/docs/features/devices-cabling.md @@ -1,3 +1,79 @@ # Devices & Cabling -TODO +At its heart, NetBox is a tool for modeling your network infrastructure, and the device object is pivotal to that function. A device can be any piece of physical hardware installed within your network, such as server, router, or switch, and may optionally be mounted within a rack. Within each device, resources such as network interfaces and console ports are modeled as discrete components, which may optionally be grouped into modules. + +NetBox uses device types to represent unique real-world device models. This allows a user to define a device type and all its components once, and easily replicate an unlimited number of device instances from it. + +```mermaid +flowchart TD + Manufacturer -.-> Platform & DeviceType & ModuleType + Manufacturer --> DeviceType & ModuleType + DeviceRole & Platform & DeviceType --> Device + Device & ModuleType ---> Module + Device & Module --> Interface & ConsolePort & PowerPort & ... +``` + +## Manufacturers + +A manufacturer generally represents an organization which produces hardware devices. These can be defined by users, however they should represent an actual entity rather than some abstract idea. + +## Device Types + +A device type represents a unique combination of manufacturer and hardware model which maps to discrete make and model of device which exists in the real world. Each device type typically has a number of components created on it, representing network interfaces, device bays, and so on. New devices of this type can then be created in NetBox, and any associated components will be automatically replicated from the device type. This avoids needing to tediously recreate components for each device as it is added in NetBox. + +!!! tip "The Device Type Library" + While users are always free to create their own device types in NetBox, many find it convenient to draw from our [community library](https://github.com/netbox-community/devicetype-library) of pre-defined device types. This is possible because a particular make and model of device is applicable universally and never changes. + +All the following can be modeled as components: + +* Interfaces +* Console ports +* Console server ports +* Power ports +* Power outlets +* Pass-through ports (front and rear) +* Module bays (which house modules) +* Device bays (which house child devices) + +For example, a Juniper EX4300-48T device type might have the following component templates defined: + +* One template for a console port ("Console") +* Two templates for power ports ("PSU0" and "PSU1") +* 48 templates for 1GE interfaces ("ge-0/0/0" through "ge-0/0/47") +* Four templates for 10GE interfaces ("xe-0/2/0" through "xe-0/2/3") + +Once component templates have been created, every new device that you create as an instance of this type will automatically be assigned each of the components listed above. + +!!! note "Component Instantiation is not Retroactive" + The instantiation of components from a device type definition occurs only at the time of device creation. If you modify the components assigned to a device type, it will not affect devices which have already been created. This guards against any inadvertent changes to existing devices. However, you always have the option of adding, modifying, or deleting components on existing devices. (These changes can easily be applied to multiple devices at once using the bulk operations available in the UI.) + +## Devices + +Whereas a device type defines the make and model of a device, a device itself represents an actual piece of installed hardware somewhere in the real world. A device can be installed at a particular position within an equipment rack, or simply associated with a site (and optionally with a location within that site). + +Each device can have an operational status, functional role, and software platform assigned. Device components are instantiated automatically from the assigned device type upon creation. + +### Virtual Chassis + +Sometimes it is necessary to model a set of physical devices as sharing a single management plane. Perhaps the most common example of such a scenario is stackable switches. These can be modeled as virtual chassis in NetBox, with one device acting as the chassis master and the rest as members. All components of member devices will appear on the master. + +## Module Types & Modules + +Much like device types and devices, module types can instantiate discrete modules, which are hardware components installed within devices. Modules often have their own child components, which become available to the parent device. For example, when modeling a chassis-based switch with multiple line cards in NetBox, the chassis would be created (from a device type) as a device, and each of its line cards would be instantiated from a module type as a module installed in one of the device's module bays. + +!!! tip "Device Bays vs. Module Bays" + What's the difference between device bays and module bays? Device bays are appropriate when the installed hardware has its own management plane, isolated from the parent device. A common example is a blade server chassis in which the blades share power but operate independently. In contrast, a module bay holds a module which does _not_ operate independently of its parent device, as with the chassis switch line card example mentioned above. + +One especially nice feature of modules is that templated components can be automatically renamed according to the module bay into which the parent module is installed. For example, if we create a module type with interfaces named `Gi{module}/0/1-48` and install a module of this type into module bay 7 of a device, NetBox will create interfaces named `Gi7/0/1-48`. + +## Cables + +NetBox models cables as connections among certain types of device components and other objects. Each cable can be assigned a type, color, length, and label. NetBox will enforce basic sanity checks to prevent invalid connections. (For example, a network interface cannot be connected to a power outlet.) + +Either end of a cable may terminate to multiple objects of the same type. For example, a network interface can be connected via a fiber optic cable to two discrete ports on a patch panel (each port attaching to an individual fiber strand in the patch cable). + +```mermaid +flowchart LR + Interface --> Cable + Cable --> fp1[Front Port] & fp2[Front Port] +``` diff --git a/docs/features/facilities.md b/docs/features/facilities.md index 5ace5b18b..0fb65aae7 100644 --- a/docs/features/facilities.md +++ b/docs/features/facilities.md @@ -1,3 +1,58 @@ # Facilities -TODO \ No newline at end of file +From global regions down to individual equipment racks, NetBox allows you to model your network's entire presence. This is accomplished through the use of several purpose-built models. The graph below illustrates these models and their relationships. + +```mermaid +flowchart TD + Region --> Region + SiteGroup --> SiteGroup + Region & SiteGroup --> Site + Site --> Location & Device + Location --> Location + Location --> Rack & Device + Rack --> Device + Site --> Rack + RackRole --> Rack +``` + +## Regions + +Regions represent geographic domains in which your network or its customers have a presence. These are typically used to model countries, states, and cities, although NetBox does not prescribe any precise uses and your needs may differ. + +Regions are self-nesting, so you can define child regions within a parent, and grandchildren within each child. For example, you might create a hierarchy like this: + +* Europe + * France + * Germany + * Spain +* North America + * Canada + * United States + * California + * New York + * Texas + +Regions will always be listed alphabetically by name within each parent, and there is no maximum depth for the hierarchy. + +## Site Groups + +Like regions, site groups can be arranged in a recursive hierarchy for grouping sites. However, whereas regions are intended for geographic organization, site groups may be used for functional grouping. For example, you might classify sites as corporate, branch, or customer sites in addition to where they are physically located. + +The use of both regions and site groups affords to independent but complementary dimensions across which sites can be organized. + +## Site + +A site typically represents a building within a region and/or site group. Each site is assigned an operational status (e.g. active or planned), and can have a discrete mailing address and GPS coordinates assigned to it. + +## Location + +A location can be any logical subdivision within a building, such as a floor or room. Like regions and site groups, locations can be nested into a self-recursive hierarchy for maximum flexibility. And like sites, each location has an operational status assigned to it. + +## Rack + +Finally, NetBox models each equipment rack as a discrete object within a site and location. These are physical objects into which devices are installed. Each rack can be assigned an operational status, type, facility ID, and other attributes related to inventory tracking. Each rack also must define a height (in rack units) and width, and may optionally specify its physical dimensions. + +Each rack must be associated to a site, but the assignment to a location within that site is optional. Users can also create custom roles to which racks can be assigned. + +!!! tip "Devices" + You'll notice in the diagram above that a device can be installed within a site, location, or rack. This approach affords plenty of flexibility as not all sites need to define child locations, and not all devices reside in racks. diff --git a/docs/features/tenancy.md b/docs/features/tenancy.md index 20534b13c..fe6d8e5a8 100644 --- a/docs/features/tenancy.md +++ b/docs/features/tenancy.md @@ -1,3 +1,19 @@ # Tenancy -TODO +Most core objects within NetBox's data model support _tenancy_. This is the association of an object with a particular tenant to convey ownership or dependency. For example, an enterprise might represent its internal business units as tenants, whereas a managed services provider might create a tenant in NetBox to represent each of its customers. + +```mermaid +flowchart TD + TenantGroup --> TenantGroup & Tenant + Tenant --> Site & Device & Prefix & Circuit & ... +``` + +## Tenant Groups + +Tenants can be grouped by any logic that your use case demands, and groups can nested recursively for maximum flexibility. For example, You might define a parent "Customers" group with child groups "Current" and "Past" within it. A tenant can be assigned to a group at any level within the hierarchy. + +## Tenants + +Typically, the tenant model is used to represent a customer or internal organization, however it can be used for whatever purpose meets your needs. + +Most core objects within NetBox can be assigned to particular tenant, so this model provides a very convenient way to correlate ownership across object types. For example, each of your customers might have its own racks, devices, IP addresses, circuits and so on: These can all be easily tracked via tenant assignment. From db38ed4f19e5943bf60434f4cfc08d232dbb5e6e Mon Sep 17 00:00:00 2001 From: Osamu-kj Date: Sat, 6 Aug 2022 15:10:31 +0200 Subject: [PATCH 346/593] Fixed the XSS protection code inside custom fields --- netbox/netbox/tables/columns.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/netbox/netbox/tables/columns.py b/netbox/netbox/tables/columns.py index 7774a495f..277482512 100644 --- a/netbox/netbox/tables/columns.py +++ b/netbox/netbox/tables/columns.py @@ -1,5 +1,4 @@ from dataclasses import dataclass -from glob import escape from typing import Optional import django_tables2 as tables @@ -8,6 +7,7 @@ from django.contrib.auth.models import AnonymousUser from django.db.models import DateField, DateTimeField from django.template import Context, Template from django.urls import reverse +from django.utils.html import escape from django.utils.formats import date_format from django.utils.safestring import mark_safe from django_tables2.columns import library @@ -430,25 +430,28 @@ class CustomFieldColumn(tables.Column): def _likify_item(item): if hasattr(item, 'get_absolute_url'): return f'{item}' - return item + return escape(item) def render(self, value): if self.customfield.type == CustomFieldTypeChoices.TYPE_BOOLEAN and value is True: - return escape('') + return mark_safe('') if self.customfield.type == CustomFieldTypeChoices.TYPE_BOOLEAN and value is False: - return escape('') + return mark_safe('') if self.customfield.type == CustomFieldTypeChoices.TYPE_URL: - return escape(f'{value}') + return mark_safe(f'{escape(value)}') if self.customfield.type == CustomFieldTypeChoices.TYPE_MULTISELECT: return ', '.join(v for v in value) if self.customfield.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT: - return escape(', '.join([ + print (mark_safe(', '.join([ + self._likify_item(obj) for obj in self.customfield.deserialize(value) + ]))) + return mark_safe(', '.join([ self._likify_item(obj) for obj in self.customfield.deserialize(value) ])) if value is not None: obj = self.customfield.deserialize(value) - return escape(self._likify_item(obj)) - return escape(self.default) + return mark_safe(self._likify_item(obj)) + return self.default def value(self, value): if isinstance(value, list): From 7141fc8eb03eeba0d751e7af374f7d6a92ea60cb Mon Sep 17 00:00:00 2001 From: Osamu-kj Date: Sat, 6 Aug 2022 17:17:43 +0200 Subject: [PATCH 347/593] Custom fields - removed the debug lines --- netbox/netbox/tables/columns.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/netbox/netbox/tables/columns.py b/netbox/netbox/tables/columns.py index 277482512..573ac058c 100644 --- a/netbox/netbox/tables/columns.py +++ b/netbox/netbox/tables/columns.py @@ -429,7 +429,7 @@ class CustomFieldColumn(tables.Column): @staticmethod def _likify_item(item): if hasattr(item, 'get_absolute_url'): - return f'{item}' + return f'{escape(item)}' return escape(item) def render(self, value): @@ -442,9 +442,6 @@ class CustomFieldColumn(tables.Column): if self.customfield.type == CustomFieldTypeChoices.TYPE_MULTISELECT: return ', '.join(v for v in value) if self.customfield.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT: - print (mark_safe(', '.join([ - self._likify_item(obj) for obj in self.customfield.deserialize(value) - ]))) return mark_safe(', '.join([ self._likify_item(obj) for obj in self.customfield.deserialize(value) ])) From 0e1947bc4bceaf01d519bc7cc2e9fc09768b0409 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 8 Aug 2022 09:58:58 -0400 Subject: [PATCH 348/593] PEP8 fix --- netbox/netbox/tables/columns.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/netbox/tables/columns.py b/netbox/netbox/tables/columns.py index 573ac058c..e176b9af7 100644 --- a/netbox/netbox/tables/columns.py +++ b/netbox/netbox/tables/columns.py @@ -7,7 +7,7 @@ from django.contrib.auth.models import AnonymousUser from django.db.models import DateField, DateTimeField from django.template import Context, Template from django.urls import reverse -from django.utils.html import escape +from django.utils.html import escape from django.utils.formats import date_format from django.utils.safestring import mark_safe from django_tables2.columns import library From 135543683db21d2856f5b12311b348e7061ff743 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 8 Aug 2022 10:24:49 -0400 Subject: [PATCH 349/593] Changelog for #9919 --- docs/release-notes/version-3.2.md | 1 + netbox/netbox/tables/columns.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/release-notes/version-3.2.md b/docs/release-notes/version-3.2.md index 1c7eb79e8..d13e8db75 100644 --- a/docs/release-notes/version-3.2.md +++ b/docs/release-notes/version-3.2.md @@ -20,6 +20,7 @@ * [#9884](https://github.com/netbox-community/netbox/issues/9884) - Prevent querying assigned VRF on prefix object init * [#9885](https://github.com/netbox-community/netbox/issues/9885) - Fix child prefix counts when editing/deleting aggregates in bulk * [#9891](https://github.com/netbox-community/netbox/issues/9891) - Ensure consistent ordering for tags during object serialization +* [#9919](https://github.com/netbox-community/netbox/issues/9919) - Fix potential XSS avenue via linked objects in tables --- diff --git a/netbox/netbox/tables/columns.py b/netbox/netbox/tables/columns.py index e176b9af7..f78b9f37c 100644 --- a/netbox/netbox/tables/columns.py +++ b/netbox/netbox/tables/columns.py @@ -442,9 +442,9 @@ class CustomFieldColumn(tables.Column): if self.customfield.type == CustomFieldTypeChoices.TYPE_MULTISELECT: return ', '.join(v for v in value) if self.customfield.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT: - return mark_safe(', '.join([ + return mark_safe(', '.join( self._likify_item(obj) for obj in self.customfield.deserialize(value) - ])) + )) if value is not None: obj = self.customfield.deserialize(value) return mark_safe(self._likify_item(obj)) From 90317adae77c7aeab79d798c1e6f3a983d18fd8a Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 8 Aug 2022 10:47:07 -0400 Subject: [PATCH 350/593] Clean up usages of mark_safe() --- netbox/dcim/views.py | 5 ++--- netbox/netbox/views/generic/object_views.py | 6 +++--- netbox/utilities/templatetags/builtins/filters.py | 4 ++-- netbox/utilities/templatetags/helpers.py | 4 +--- 4 files changed, 8 insertions(+), 11 deletions(-) diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 3bec02f5c..0bdca686d 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -3083,7 +3083,7 @@ class VirtualChassisAddMemberView(ObjectPermissionRequiredMixin, GetReturnURLMix if membership_form.is_valid(): membership_form.save() - msg = 'Added member {}'.format(device.get_absolute_url(), escape(device)) + msg = f'Added member {escape(device)}' messages.success(request, mark_safe(msg)) if '_addanother' in request.POST: @@ -3128,8 +3128,7 @@ class VirtualChassisRemoveMemberView(ObjectPermissionRequiredMixin, GetReturnURL # Protect master device from being removed virtual_chassis = VirtualChassis.objects.filter(master=device).first() if virtual_chassis is not None: - msg = 'Unable to remove master device {} from the virtual chassis.'.format(escape(device)) - messages.error(request, mark_safe(msg)) + messages.error(request, f'Unable to remove master device {device} from the virtual chassis.') return redirect(device.get_absolute_url()) if form.is_valid(): diff --git a/netbox/netbox/views/generic/object_views.py b/netbox/netbox/views/generic/object_views.py index 4ebfe71cc..88e078ae3 100644 --- a/netbox/netbox/views/generic/object_views.py +++ b/netbox/netbox/views/generic/object_views.py @@ -386,10 +386,10 @@ class ObjectEditView(GetReturnURLMixin, BaseObjectView): ) logger.info(f"{msg} {obj} (PK: {obj.pk})") if hasattr(obj, 'get_absolute_url'): - msg = '{} {}'.format(msg, obj.get_absolute_url(), escape(obj)) + msg = mark_safe(f'{msg} {escape(obj)}') else: - msg = '{} {}'.format(msg, escape(obj)) - messages.success(request, mark_safe(msg)) + msg = f'{msg} {obj}' + messages.success(request, msg) if '_addanother' in request.POST: redirect_url = request.path diff --git a/netbox/utilities/templatetags/builtins/filters.py b/netbox/utilities/templatetags/builtins/filters.py index 5a6841286..bc395e438 100644 --- a/netbox/utilities/templatetags/builtins/filters.py +++ b/netbox/utilities/templatetags/builtins/filters.py @@ -86,8 +86,8 @@ def placeholder(value): """ if value not in ('', None): return value - placeholder = '' - return mark_safe(placeholder) + + return mark_safe('') @register.filter() diff --git a/netbox/utilities/templatetags/helpers.py b/netbox/utilities/templatetags/helpers.py index db4d14c24..67ed553b2 100644 --- a/netbox/utilities/templatetags/helpers.py +++ b/netbox/utilities/templatetags/helpers.py @@ -109,9 +109,7 @@ def annotated_date(date_value): long_ts = date(date_value, 'DATETIME_FORMAT') short_ts = date(date_value, 'SHORT_DATETIME_FORMAT') - span = f'{short_ts}' - - return mark_safe(span) + return mark_safe(f'{short_ts}') @register.simple_tag From 36ac83a319f1dbd1df3752c9ab8f129eefb23f9b Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 8 Aug 2022 11:43:27 -0400 Subject: [PATCH 351/593] Fixes #9949: Fix KeyError exception resulting from invalid API token provisioning request --- docs/release-notes/version-3.2.md | 1 + netbox/users/api/views.py | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/release-notes/version-3.2.md b/docs/release-notes/version-3.2.md index d13e8db75..0ee235a82 100644 --- a/docs/release-notes/version-3.2.md +++ b/docs/release-notes/version-3.2.md @@ -21,6 +21,7 @@ * [#9885](https://github.com/netbox-community/netbox/issues/9885) - Fix child prefix counts when editing/deleting aggregates in bulk * [#9891](https://github.com/netbox-community/netbox/issues/9891) - Ensure consistent ordering for tags during object serialization * [#9919](https://github.com/netbox-community/netbox/issues/9919) - Fix potential XSS avenue via linked objects in tables +* [#9949](https://github.com/netbox-community/netbox/issues/9949) - Fix KeyError exception resulting from invalid API token provisioning request --- diff --git a/netbox/users/api/views.py b/netbox/users/api/views.py index c3495afdf..e5c2bc8ab 100644 --- a/netbox/users/api/views.py +++ b/netbox/users/api/views.py @@ -74,11 +74,11 @@ class TokenProvisionView(APIView): serializer.is_valid() # Authenticate the user account based on the provided credentials - user = authenticate( - request=request, - username=serializer.data['username'], - password=serializer.data['password'] - ) + username = serializer.data.get('username') + password = serializer.data.get('password') + if not username or not password: + raise AuthenticationFailed("Username and password must be provided to provision a token.") + user = authenticate(request=request, username=username, password=password) if user is None: raise AuthenticationFailed("Invalid username/password") From 876251c1cf6da80894780d3856412a2918da9d0e Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 8 Aug 2022 11:48:43 -0400 Subject: [PATCH 352/593] Fixes #9948: Fix TypeError exception when requesting API tokens list as non-authenticated user --- docs/release-notes/version-3.2.md | 1 + netbox/users/api/views.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/docs/release-notes/version-3.2.md b/docs/release-notes/version-3.2.md index 0ee235a82..745ace110 100644 --- a/docs/release-notes/version-3.2.md +++ b/docs/release-notes/version-3.2.md @@ -21,6 +21,7 @@ * [#9885](https://github.com/netbox-community/netbox/issues/9885) - Fix child prefix counts when editing/deleting aggregates in bulk * [#9891](https://github.com/netbox-community/netbox/issues/9891) - Ensure consistent ordering for tags during object serialization * [#9919](https://github.com/netbox-community/netbox/issues/9919) - Fix potential XSS avenue via linked objects in tables +* [#9948](https://github.com/netbox-community/netbox/issues/9948) - Fix TypeError exception when requesting API tokens list as non-authenticated user * [#9949](https://github.com/netbox-community/netbox/issues/9949) - Fix KeyError exception resulting from invalid API token provisioning request --- diff --git a/netbox/users/api/views.py b/netbox/users/api/views.py index e5c2bc8ab..66ef92ab7 100644 --- a/netbox/users/api/views.py +++ b/netbox/users/api/views.py @@ -58,6 +58,8 @@ class TokenViewSet(NetBoxModelViewSet): # Workaround for schema generation (drf_yasg) if getattr(self, 'swagger_fake_view', False): return queryset.none() + if not self.request.user.is_authenticated: + return queryset.none() if self.request.user.is_superuser: return queryset return queryset.filter(user=self.request.user) From 8721ad987cf178cc5421f64c28b1772bdabaa967 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 8 Aug 2022 12:22:22 -0400 Subject: [PATCH 353/593] Fixes #9952: Prevent InvalidMove when attempting to assign a nested child object as parent --- docs/release-notes/version-3.2.md | 1 + netbox/netbox/models/__init__.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/release-notes/version-3.2.md b/docs/release-notes/version-3.2.md index 745ace110..f231b6e58 100644 --- a/docs/release-notes/version-3.2.md +++ b/docs/release-notes/version-3.2.md @@ -23,6 +23,7 @@ * [#9919](https://github.com/netbox-community/netbox/issues/9919) - Fix potential XSS avenue via linked objects in tables * [#9948](https://github.com/netbox-community/netbox/issues/9948) - Fix TypeError exception when requesting API tokens list as non-authenticated user * [#9949](https://github.com/netbox-community/netbox/issues/9949) - Fix KeyError exception resulting from invalid API token provisioning request +* [#9952](https://github.com/netbox-community/netbox/issues/9952) - Prevent InvalidMove when attempting to assign a nested child object as parent --- diff --git a/netbox/netbox/models/__init__.py b/netbox/netbox/models/__init__.py index b3bfe06c0..ea2feb8de 100644 --- a/netbox/netbox/models/__init__.py +++ b/netbox/netbox/models/__init__.py @@ -89,9 +89,9 @@ class NestedGroupModel(NetBoxFeatureSet, MPTTModel): super().clean() # An MPTT model cannot be its own parent - if self.pk and self.parent_id == self.pk: + if self.pk and self.parent and self.parent in self.get_descendants(include_self=True): raise ValidationError({ - "parent": "Cannot assign self as parent." + "parent": f"Cannot assign self or child {self._meta.verbose_name} as parent." }) From caca074161977fcbd7d3d0cd0e02b42efb3e8184 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 8 Aug 2022 14:21:42 -0400 Subject: [PATCH 354/593] Fixes #9950: Prevent redirection to arbitrary URLs via 'next' parameter on login URL --- docs/release-notes/version-3.2.md | 1 + netbox/users/views.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/version-3.2.md b/docs/release-notes/version-3.2.md index f231b6e58..46718837e 100644 --- a/docs/release-notes/version-3.2.md +++ b/docs/release-notes/version-3.2.md @@ -23,6 +23,7 @@ * [#9919](https://github.com/netbox-community/netbox/issues/9919) - Fix potential XSS avenue via linked objects in tables * [#9948](https://github.com/netbox-community/netbox/issues/9948) - Fix TypeError exception when requesting API tokens list as non-authenticated user * [#9949](https://github.com/netbox-community/netbox/issues/9949) - Fix KeyError exception resulting from invalid API token provisioning request +* [#9950](https://github.com/netbox-community/netbox/issues/9950) - Prevent redirection to arbitrary URLs via `next` parameter on login URL * [#9952](https://github.com/netbox-community/netbox/issues/9952) - Prevent InvalidMove when attempting to assign a nested child object as parent --- diff --git a/netbox/users/views.py b/netbox/users/views.py index 344f375fc..f08cac844 100644 --- a/netbox/users/views.py +++ b/netbox/users/views.py @@ -10,6 +10,7 @@ from django.http import HttpResponseRedirect from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse from django.utils.decorators import method_decorator +from django.utils.http import url_has_allowed_host_and_scheme from django.views.decorators.debug import sensitive_post_parameters from django.views.generic import View from social_core.backends.utils import load_backends @@ -91,7 +92,7 @@ class LoginView(View): data = request.POST if request.method == "POST" else request.GET redirect_url = data.get('next', settings.LOGIN_REDIRECT_URL) - if redirect_url and redirect_url.startswith('/'): + if redirect_url and url_has_allowed_host_and_scheme(redirect_url, allowed_hosts=None): logger.debug(f"Redirecting user to {redirect_url}") else: if redirect_url: From ce7fb8ab17406a8eba02055c22ec0b6cbe8ef425 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 8 Aug 2022 15:17:36 -0400 Subject: [PATCH 355/593] Release v3.2.8 --- .github/ISSUE_TEMPLATE/bug_report.yaml | 2 +- .github/ISSUE_TEMPLATE/feature_request.yaml | 2 +- base_requirements.txt | 2 +- docs/release-notes/version-3.2.md | 2 +- netbox/netbox/settings.py | 2 +- requirements.txt | 12 ++++++------ 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index 332a0ad75..c26584f32 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -14,7 +14,7 @@ body: attributes: label: NetBox version description: What version of NetBox are you currently running? - placeholder: v3.2.7 + placeholder: v3.2.8 validations: required: true - type: dropdown diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml index ff9b5e358..e6be95e49 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yaml +++ b/.github/ISSUE_TEMPLATE/feature_request.yaml @@ -14,7 +14,7 @@ body: attributes: label: NetBox version description: What version of NetBox are you currently running? - placeholder: v3.2.7 + placeholder: v3.2.8 validations: required: true - type: dropdown diff --git a/base_requirements.txt b/base_requirements.txt index 9dc85231b..672ce402c 100644 --- a/base_requirements.txt +++ b/base_requirements.txt @@ -4,7 +4,7 @@ bleach # The Python web framework on which NetBox is built # https://github.com/django/django -Django +Django<4.1 # Django middleware which permits cross-domain API requests # https://github.com/OttoYiu/django-cors-headers diff --git a/docs/release-notes/version-3.2.md b/docs/release-notes/version-3.2.md index 46718837e..bf6f2f848 100644 --- a/docs/release-notes/version-3.2.md +++ b/docs/release-notes/version-3.2.md @@ -1,6 +1,6 @@ # NetBox v3.2 -## v3.2.8 (FUTURE) +## v3.2.8 (2022-08-08) ### Enhancements diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index c7e49c1be..12ab44399 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -29,7 +29,7 @@ django.utils.encoding.force_text = force_str # Environment setup # -VERSION = '3.2.8-dev' +VERSION = '3.2.8' # Hostname HOSTNAME = platform.node() diff --git a/requirements.txt b/requirements.txt index f987ad7ba..59bd1e8cd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ bleach==5.0.1 -Django==4.0.6 +Django==4.0.7 django-cors-headers==3.13.0 django-debug-toolbar==3.5.0 django-filter==22.1 @@ -13,22 +13,22 @@ django-tables2==2.4.1 django-taggit==2.1.0 django-timezone-field==5.0 djangorestframework==3.13.1 -drf-yasg[validation]==1.20.0 +drf-yasg[validation]==1.21.3 graphene-django==2.15.0 gunicorn==20.1.0 Jinja2==3.1.2 -Markdown==3.3.7 -markdown-include==0.6.0 +Markdown==3.4.1 +markdown-include==0.7.0 mkdocs-material==8.3.9 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.7.0 +sentry-sdk==1.9.2 social-auth-app-django==5.0.0 social-auth-core==4.3.0 -svgwrite==1.4.2 +svgwrite==1.4.3 tablib==3.2.1 tzdata==2022.1 From 064d7f3bd0a803ed55198455e54a29c8f7c0adc2 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 8 Aug 2022 15:34:13 -0400 Subject: [PATCH 356/593] PRVB --- docs/release-notes/version-3.2.md | 4 ++++ netbox/netbox/settings.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/version-3.2.md b/docs/release-notes/version-3.2.md index bf6f2f848..56a35cc02 100644 --- a/docs/release-notes/version-3.2.md +++ b/docs/release-notes/version-3.2.md @@ -1,5 +1,9 @@ # NetBox v3.2 +## v3.2.9 (FUTURE) + +--- + ## v3.2.8 (2022-08-08) ### Enhancements diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 12ab44399..d38289d43 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -29,7 +29,7 @@ django.utils.encoding.force_text = force_str # Environment setup # -VERSION = '3.2.8' +VERSION = '3.2.9-dev' # Hostname HOSTNAME = platform.node() From c7faca948016caa8ce30b3571c8a4e71dc33f70e Mon Sep 17 00:00:00 2001 From: gildarov Date: Tue, 9 Aug 2022 11:56:19 +0300 Subject: [PATCH 357/593] fix typo in virtualization/forms/filtersets.py --- netbox/virtualization/forms/filtersets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/virtualization/forms/filtersets.py b/netbox/virtualization/forms/filtersets.py index 88aa1a6c2..2f52850bd 100644 --- a/netbox/virtualization/forms/filtersets.py +++ b/netbox/virtualization/forms/filtersets.py @@ -89,7 +89,7 @@ class VirtualMachineFilterForm( (None, ('q', 'tag')), ('Cluster', ('cluster_group_id', 'cluster_type_id', 'cluster_id')), ('Location', ('region_id', 'site_group_id', 'site_id')), - ('Attriubtes', ('status', 'role_id', 'platform_id', 'mac_address', 'has_primary_ip', 'local_context_data')), + ('Attributes', ('status', 'role_id', 'platform_id', 'mac_address', 'has_primary_ip', 'local_context_data')), ('Tenant', ('tenant_group_id', 'tenant_id')), ('Contacts', ('contact', 'contact_role', 'contact_group')), ) From 602cf8c5fa371d767af70c6d0d9333c5732e9c7f Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 9 Aug 2022 11:29:42 -0400 Subject: [PATCH 358/593] Fixes #9939: Fix list of next nodes for split paths under trace view --- docs/release-notes/version-3.3.md | 17 ++--------------- netbox/dcim/models/cables.py | 4 ++-- 2 files changed, 4 insertions(+), 17 deletions(-) diff --git a/docs/release-notes/version-3.3.md b/docs/release-notes/version-3.3.md index 2a3935e5e..74ed7f10c 100644 --- a/docs/release-notes/version-3.3.md +++ b/docs/release-notes/version-3.3.md @@ -97,22 +97,9 @@ Custom field UI visibility has no impact on API operation. * [#9536](https://github.com/netbox-community/netbox/issues/9536) - Track API token usage times * [#9582](https://github.com/netbox-community/netbox/issues/9582) - Enable assigning config contexts based on device location -### Bug Fixes (from Beta1) +### Bug Fixes (from Beta2) -* [#9728](https://github.com/netbox-community/netbox/issues/9728) - Fix validation when assigning a virtual machine to a device -* [#9729](https://github.com/netbox-community/netbox/issues/9729) - Fix ordering of content type creation to ensure compatability with demo data -* [#9730](https://github.com/netbox-community/netbox/issues/9730) - Fix validation error when creating a new cable via UI form -* [#9733](https://github.com/netbox-community/netbox/issues/9733) - Handle split paths during trace when fanning out to front ports with differing cables -* [#9765](https://github.com/netbox-community/netbox/issues/9765) - Report correct segment count under cable trace UI view -* [#9778](https://github.com/netbox-community/netbox/issues/9778) - Fix exception during cable deletion after deleting a connected termination -* [#9788](https://github.com/netbox-community/netbox/issues/9788) - Ensure denormalized fields on CableTermination are kept in sync with related objects -* [#9789](https://github.com/netbox-community/netbox/issues/9789) - Fix rendering of cable traces ending at provider networks -* [#9794](https://github.com/netbox-community/netbox/issues/9794) - Fix link to connect a rear port to a circuit termination -* [#9818](https://github.com/netbox-community/netbox/issues/9818) - Fix circuit side selection when connecting a cable to a circuit termination -* [#9829](https://github.com/netbox-community/netbox/issues/9829) - Arrange custom fields by group when editing objects -* [#9843](https://github.com/netbox-community/netbox/issues/9843) - Fix rendering of custom field values (regression from #9647) -* [#9844](https://github.com/netbox-community/netbox/issues/9844) - Fix interface api request when creating/editing L2VPN termination -* [#9847](https://github.com/netbox-community/netbox/issues/9847) - Respect `desc_units` when ordering rack units +* [#9939](https://github.com/netbox-community/netbox/issues/9939) - Fix list of next nodes for split paths under trace view ### Plugins API diff --git a/netbox/dcim/models/cables.py b/netbox/dcim/models/cables.py index 321d808ff..2be64451f 100644 --- a/netbox/dcim/models/cables.py +++ b/netbox/dcim/models/cables.py @@ -677,6 +677,6 @@ class CablePath(models.Model): """ Return all available next segments in a split cable path. """ - rearport = path_node_to_object(self._nodes[-1]) + rearports = self.path_objects[-1] - return FrontPort.objects.filter(rear_port=rearport) + return FrontPort.objects.filter(rear_port__in=rearports) From 7dc2e02e226445cffc84deae682c8c9cdeed8dc3 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 9 Aug 2022 11:39:04 -0400 Subject: [PATCH 359/593] Fixes #9938: Exclude virtual interfaces from terminations list when connecting a cable --- docs/release-notes/version-3.3.md | 1 + netbox/dcim/forms/connections.py | 1 + 2 files changed, 2 insertions(+) diff --git a/docs/release-notes/version-3.3.md b/docs/release-notes/version-3.3.md index 74ed7f10c..c7d1a30ff 100644 --- a/docs/release-notes/version-3.3.md +++ b/docs/release-notes/version-3.3.md @@ -99,6 +99,7 @@ Custom field UI visibility has no impact on API operation. ### Bug Fixes (from Beta2) +* [#9938](https://github.com/netbox-community/netbox/issues/9938) - Exclude virtual interfaces from terminations list when connecting a cable * [#9939](https://github.com/netbox-community/netbox/issues/9939) - Fix list of next nodes for split paths under trace view ### Plugins API diff --git a/netbox/dcim/forms/connections.py b/netbox/dcim/forms/connections.py index 7552c0c87..cc5cf362f 100644 --- a/netbox/dcim/forms/connections.py +++ b/netbox/dcim/forms/connections.py @@ -84,6 +84,7 @@ def get_cable_form(a_type, b_type): disabled_indicator='_occupied', query_params={ 'device_id': f'$termination_{cable_end}_device', + 'kind': 'physical', # Exclude virtual interfaces } ) From c1d9cace117fb593fd8e332c370e05096017dac5 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 9 Aug 2022 14:21:27 -0400 Subject: [PATCH 360/593] Fixes #9900: Pre-populate site & rack fields for cable connection form --- docs/release-notes/version-3.3.md | 1 + netbox/dcim/tables/template_code.py | 50 ++++++++++---------- netbox/templates/dcim/consoleport.html | 6 +-- netbox/templates/dcim/consoleserverport.html | 6 +-- netbox/templates/dcim/frontport.html | 12 ++--- netbox/templates/dcim/interface.html | 8 ++-- netbox/templates/dcim/powerfeed.html | 2 +- netbox/templates/dcim/poweroutlet.html | 2 +- netbox/templates/dcim/powerport.html | 4 +- netbox/templates/dcim/rearport.html | 8 ++-- 10 files changed, 50 insertions(+), 49 deletions(-) diff --git a/docs/release-notes/version-3.3.md b/docs/release-notes/version-3.3.md index c7d1a30ff..9ec4eb9b5 100644 --- a/docs/release-notes/version-3.3.md +++ b/docs/release-notes/version-3.3.md @@ -99,6 +99,7 @@ Custom field UI visibility has no impact on API operation. ### Bug Fixes (from Beta2) +* [#9900](https://github.com/netbox-community/netbox/issues/9900) - Pre-populate site & rack fields for cable connection form * [#9938](https://github.com/netbox-community/netbox/issues/9938) - Exclude virtual interfaces from terminations list when connecting a cable * [#9939](https://github.com/netbox-community/netbox/issues/9939) - Fix list of next nodes for split paths under trace view diff --git a/netbox/dcim/tables/template_code.py b/netbox/dcim/tables/template_code.py index 082df56df..04ef74192 100644 --- a/netbox/dcim/tables/template_code.py +++ b/netbox/dcim/tables/template_code.py @@ -121,9 +121,9 @@ CONSOLEPORT_BUTTONS = """ {% else %} @@ -153,9 +153,9 @@ CONSOLESERVERPORT_BUTTONS = """ {% else %} @@ -185,8 +185,8 @@ POWERPORT_BUTTONS = """ {% else %} @@ -212,7 +212,7 @@ POWEROUTLET_BUTTONS = """ {% if not record.mark_connected %} - + {% else %} @@ -262,10 +262,10 @@ INTERFACE_BUTTONS = """ {% else %} @@ -301,12 +301,12 @@ FRONTPORT_BUTTONS = """ {% else %} @@ -338,12 +338,12 @@ REARPORT_BUTTONS = """ {% else %} diff --git a/netbox/templates/dcim/consoleport.html b/netbox/templates/dcim/consoleport.html index f132a4ed8..39ffbf552 100644 --- a/netbox/templates/dcim/consoleport.html +++ b/netbox/templates/dcim/consoleport.html @@ -111,13 +111,13 @@ diff --git a/netbox/templates/dcim/consoleserverport.html b/netbox/templates/dcim/consoleserverport.html index f4da080e8..642e758a3 100644 --- a/netbox/templates/dcim/consoleserverport.html +++ b/netbox/templates/dcim/consoleserverport.html @@ -113,13 +113,13 @@ diff --git a/netbox/templates/dcim/frontport.html b/netbox/templates/dcim/frontport.html index e5f1df5ae..2ef955fe9 100644 --- a/netbox/templates/dcim/frontport.html +++ b/netbox/templates/dcim/frontport.html @@ -109,22 +109,22 @@ diff --git a/netbox/templates/dcim/interface.html b/netbox/templates/dcim/interface.html index 11e776872..7503e1be2 100644 --- a/netbox/templates/dcim/interface.html +++ b/netbox/templates/dcim/interface.html @@ -263,16 +263,16 @@ diff --git a/netbox/templates/dcim/powerfeed.html b/netbox/templates/dcim/powerfeed.html index 3972b30f3..584454df8 100644 --- a/netbox/templates/dcim/powerfeed.html +++ b/netbox/templates/dcim/powerfeed.html @@ -158,7 +158,7 @@ {% if not object.mark_connected and not object.cable %} Name Role PriorityPhoneEmail
    {{ contact.contact|linkify }} {{ contact.role|placeholder }} {{ contact.get_priority_display|placeholder }} + {% if contact.contact.phone %} + {{ contact.contact.phone }} + {% else %} + {{ ''|placeholder }} + {% endif %} + + {% if contact.contact.email %} + {{ contact.contact.email }} + {% else %} + {{ ''|placeholder }} + {% endif %} + {% if perms.tenancy.change_contactassignment %} From 1c7ef73d1fcff334ff34dba2fff073c934a06c99 Mon Sep 17 00:00:00 2001 From: Dorian Bourgeoisat Date: Tue, 9 Aug 2022 14:20:42 +0200 Subject: [PATCH 363/593] Closes #8595: Added new PON interface types --- netbox/dcim/choices.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/netbox/dcim/choices.py b/netbox/dcim/choices.py index 2e96f9c67..c91faf3da 100644 --- a/netbox/dcim/choices.py +++ b/netbox/dcim/choices.py @@ -814,6 +814,14 @@ class InterfaceTypeChoices(ChoiceSet): # ATM/DSL TYPE_XDSL = 'xdsl' + # PON + TYPE_GPON = 'gpon' + TYPE_XG_PON = 'xg-pon' + TYPE_XGS_PON = 'xgs-pon' + TYPE_TWDM_PON = 'twdm-pon' + TYPE_EPON = 'epon' + TYPE_10G_EPON = '10g-epon' + # Stacking TYPE_STACKWISE = 'cisco-stackwise' TYPE_STACKWISE_PLUS = 'cisco-stackwise-plus' @@ -950,6 +958,17 @@ class InterfaceTypeChoices(ChoiceSet): (TYPE_XDSL, 'xDSL'), ) ), + ( + 'PON', + ( + (TYPE_GPON, 'GPON (2.5 Gbps / 1.25 Gps)'), + (TYPE_XG_PON, 'XG-PON (10 Gbps / 2.5 Gbps)'), + (TYPE_XGS_PON, 'XGS-PON (10 Gbps)'), + (TYPE_TWDM_PON, 'TWDM-PON (NG-PON2) (4x10 Gbps)'), + (TYPE_EPON, 'EPON (1 Gbps)'), + (TYPE_10G_EPON, '10G-EPON (10 Gbps)'), + ) + ), ( 'Stacking', ( From e69be8f9a631e255caf08b838951d2041efbf4b1 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 10 Aug 2022 12:51:21 -0400 Subject: [PATCH 364/593] Continued work on feature docs --- docs/features/circuits.md | 26 +++++++++++++++++++++++++- docs/features/ipam.md | 6 ++++++ docs/features/l2vpn-overlay.md | 4 +++- docs/features/services.md | 3 --- docs/features/vlan-management.md | 19 ++++++++++++++++++- docs/features/wireless.md | 25 ++++++++++++++++++++++++- mkdocs.yml | 1 - 7 files changed, 76 insertions(+), 8 deletions(-) delete mode 100644 docs/features/services.md diff --git a/docs/features/circuits.md b/docs/features/circuits.md index 63c8bc970..f0c5832c4 100644 --- a/docs/features/circuits.md +++ b/docs/features/circuits.md @@ -1,3 +1,27 @@ # Circuits -TODO +NetBox is ideal for managing your network's transit and peering providers and circuits. It provides all the flexibility needed to model physical circuits in both data center and enterprise environments, and allows for "connecting" circuits directly to device interfaces via cables. + +```mermaid +flowchart TD + ASN --> Provider + Provider --> ProviderNetwork & Circuit + CircuitType --> Circuit +``` + +## Providers + +A provider is any organization which provides Internet or private connectivity. Typically, these are large carriers, however they might also include regional providers or even internal services. Each provider can be assigned account and contact details, and may have one or more AS numbers assigned to it. + +Sometimes you'll need to model provider networks into which you don't have full visibility; these are typically represented on topology diagrams with cloud icons. NetBox facilitates this through its provider network model: A provider network represents a "black box" network to which your circuits can connect. A common example is a provider MPLS network connecting multiple sites. + +## Circuits + +A circuit is a physical connection between two points, which is installed and maintained by an external provider. For example, an Internet connection delivered as a fiber optic cable would be modeled as a circuit in NetBox. + +Each circuit is associated with a provider and assigned a circuit ID, which must be unique to that provider. A circuit is also assigned a user-defined type, operational status, and various other operating characteristics. + +Each circuit may have up to two terminations (A and Z) defined. Each termination can be associated with a particular site or provider network. In the case of the former, a cable can be connected between the circuit termination and a device component to map its physical connectivity. + +!!! warning "Physical vs. Virtual Circuits" + The circuit model in NetBox represents **physical** connections. Don't confuse these with _virtual_ circuits which may be offered by providers overlaid on physical infrastructure. (For example, a VLAN-tagged subinterface would be a virtual circuit.) A good rule of thumb: If you can't point to it, it's not a physical circuit. diff --git a/docs/features/ipam.md b/docs/features/ipam.md index 7832fab44..45a6a0221 100644 --- a/docs/features/ipam.md +++ b/docs/features/ipam.md @@ -54,3 +54,9 @@ VRF modeling in NetBox very closely follows what you find in real-world network ## AS Numbers An often overlooked component of IPAM, NetBox also tracks autonomous system (AS) numbers and their assignment to sites. Both 16- and 32-bit AS numbers are supported, and like aggregates each ASN is assigned to an authoritative RIR. + +## Service Mapping + +NetBox models network applications as discrete service objects associated with devices and/or virtual machines, and optionally with specific IP addresses attached to those parent objects. These can be used to catalog the applications running on your network for reference by other objects or integrated tools. + +To model services in NetBox, begin by creating a service template defining the name, protocol, and port number(s) on which the service listens. This template can then be easily instantiated to "attach" new services to a device or virtual machine. It's also possible to create new services by hand, without a template, however this approach can be tedious. diff --git a/docs/features/l2vpn-overlay.md b/docs/features/l2vpn-overlay.md index 002c483f9..51fbf2a78 100644 --- a/docs/features/l2vpn-overlay.md +++ b/docs/features/l2vpn-overlay.md @@ -1,3 +1,5 @@ # L2VPN & Overlay -TODO +L2VPN and overlay networks, such as VXLAN and EVPN, can be defined in NetBox and tied to interfaces and VLANs. This allows for easy tracking of overlay assets and their relationships with underlay resources. + +Each L2VPN instance has a type and optional unique identifier. Like VRFs, L2VPNs can also have import and export route targets assigned to them. Terminations can then be created to assign VLANs and/or device and virtual machine interfaces to the overlay. diff --git a/docs/features/services.md b/docs/features/services.md deleted file mode 100644 index 338fc349a..000000000 --- a/docs/features/services.md +++ /dev/null @@ -1,3 +0,0 @@ -# Services - -TODO diff --git a/docs/features/vlan-management.md b/docs/features/vlan-management.md index 3d4fd4d1f..4af05dea3 100644 --- a/docs/features/vlan-management.md +++ b/docs/features/vlan-management.md @@ -1,3 +1,20 @@ # VLAN Management -TODO +Complementing its IPAM capabilities, NetBox also tracks VLAN information to assist with layer two network configurations. VLANs are defined per IEEE 802.1Q and related standards, and can be assigned to groups and functional roles. + +```mermaid +flowchart TD + VLANGroup & Role --> VLAN +``` + +## VLAN Groups + +A VLAN group is a collection of VLANs defined within a particular scope. Each VLAN group can be associated with a particular site, location, rack, or similar object to indicate its domain, and designates a minimum and maximum VLAN ID within the group. (By default, these are the standard minimum and maximum values of 1 and 4094, respectively.) + +Within a group, each VLAN must have a unique ID and name. There is no limit to how many groups can be created per scope. + +## VLANs + +NetBox models VLANs according to their definition under IEEE 802.1Q, with a 12-bit VLAN ID and a name. Each VLAN also has an operational status, and may be assigned a function role, just like prefixes. Each VLAN can be assigned to a VLAN group or site to convey the domain in which the VLAN exists. + +Once defined, VLANs can be associated with device and virtual machine interfaces. Each interface can be assigned an 802.1Q mode (access or tagged), and the relevant VLANs can be applied as tagged or untagged. diff --git a/docs/features/wireless.md b/docs/features/wireless.md index ff3d5d10b..215d1a682 100644 --- a/docs/features/wireless.md +++ b/docs/features/wireless.md @@ -1,3 +1,26 @@ # Wireless -TODO +Just as NetBox provides robust modeling for physical cable plants, it also supports modeling wireless LANs and point-to-point links. + +## Wireless LANs + +```mermaid +flowchart TD + WirelessLANGroup --> WirelessLANGroup & WirelessLAN +``` + +A wireless LAN is a multi-access network shared by multiple wireless clients, identified by a common service set identifier (SSID) and authentication parameters. Wireless LANs can be organized into self-nesting groups, and each wireless LAN may optionally be bound to a particular VLAN. This allows easily mapping wireless networks to their wired counterparts. + +Authentication attributes for wireless LANs include: + +* **Type** - Open, WEP, WPA, etc. +* **Cipher** - Auto, TKIP, or AES +* **Pre-shared key (PSK)** - The secret key configured on all participating clients + +The definition of authentication parameters is optional. + +## Wireless Links + +Whereas a wireless LAN represents a physical multi-access segment with any number of clients, a wireless link is a point-to-point connection between exactly two stations. These links behave much like cables, but more accurately model the nature of wireless communications. + +Like wireless LANs, wireless links also have an SSID and (optional) authentication attributes. diff --git a/mkdocs.yml b/mkdocs.yml index 78ca5031c..784e4498e 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -67,7 +67,6 @@ nav: - Power Tracking: 'features/power-tracking.md' - IPAM: 'features/ipam.md' - VLAN Management: 'features/vlan-management.md' - - Service Mapping: 'features/services.md' - L2VPN & Overlay: 'features/l2vpn-overlay.md' - Circuits: 'features/circuits.md' - Wireless: 'features/wireless.md' From 9733cee3d208d017382c44d966e2ab8f80a1f31f Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 10 Aug 2022 14:07:51 -0400 Subject: [PATCH 365/593] Continued work on feature docs --- docs/development/models.md | 2 +- docs/features/context-data.md | 79 ++++++--------------- docs/features/customization.md | 38 ++++++++++ docs/features/integrations.md | 41 +++++++++++ docs/features/tags.md | 17 ----- docs/{features => integrations}/webhooks.md | 0 docs/models/extras/configcontext.md | 69 ++++++++++++++++++ docs/models/extras/tag.md | 17 +++++ mkdocs.yml | 6 +- 9 files changed, 193 insertions(+), 76 deletions(-) create mode 100644 docs/features/integrations.md rename docs/{features => integrations}/webhooks.md (100%) create mode 100644 docs/models/extras/configcontext.md create mode 100644 docs/models/extras/tag.md diff --git a/docs/development/models.md b/docs/development/models.md index be3608a30..3c7aaaa78 100644 --- a/docs/development/models.md +++ b/docs/development/models.md @@ -9,7 +9,7 @@ The Django [content types](https://docs.djangoproject.com/en/stable/ref/contrib/ ### Features Matrix * [Change logging](../features/change-logging.md) - Changes to these objects are automatically recorded in the change log -* [Webhooks](../features/webhooks.md) - NetBox is capable of generating outgoing webhooks for these objects +* [Webhooks](../integrations/webhooks.md) - NetBox is capable of generating outgoing webhooks for these objects * [Custom fields](../customization/custom-fields.md) - These models support the addition of user-defined fields * [Export templates](../customization/export-templates.md) - Users can create custom export templates for these models * [Tagging](../features/tags.md) - The models can be tagged with user-defined tags diff --git a/docs/features/context-data.md b/docs/features/context-data.md index 08b5f4fd5..d202482da 100644 --- a/docs/features/context-data.md +++ b/docs/features/context-data.md @@ -1,69 +1,34 @@ -# Configuration Contexts +# Context Data -Sometimes it is desirable to associate additional data with a group of devices or virtual machines to aid in automated configuration. For example, you might want to associate a set of syslog servers for all devices within a particular region. Context data enables the association of extra user-defined data with devices and virtual machines grouped by one or more of the following assignments: - -* Region -* Site group -* Site -* Location (devices only) -* Device type (devices only) -* Role -* Platform -* Cluster type (VMs only) -* Cluster group (VMs only) -* Cluster (VMs only) -* Tenant group -* Tenant -* Tag - -## Hierarchical Rendering - -Context data is arranged hierarchically, so that data with a higher weight can be entered to override lower-weight data. Multiple instances of data are automatically merged by NetBox to present a single dictionary for each object. - -For example, suppose we want to specify a set of syslog and NTP servers for all devices within a region. We could create a config context instance with a weight of 1000 assigned to the region, with the following JSON data: - -```json -{ - "ntp-servers": [ - "172.16.10.22", - "172.16.10.33" - ], - "syslog-servers": [ - "172.16.9.100", - "172.16.9.101" - ] -} -``` - -But suppose there's a problem at one particular site within this region preventing traffic from reaching the regional syslog server. Devices there need to use a local syslog server instead of the two defined above. We'll create a second config context assigned only to that site with a weight of 2000 and the following data: +Configuration context data (or "config contexts" for short) is a powerful feature that enables users to define arbitrary data that applies to device and virtual machines based on certain characteristics. For example, suppose you want to define syslog servers for devices assigned to sites within a particular region. In NetBox, you can create a config context instance containing this data and apply it to the desired region. All devices within this region will now include this data when fetched via an API. ```json { "syslog-servers": [ - "192.168.43.107" + "192.168.43.107", + "192.168.48.112" ] } ``` -When the context data for a device at this site is rendered, the second, higher-weight data overwrite the first, resulting in the following: +While this is handy on its own, the real power of context data stems from its ability to be merged and overridden using multiple instances. For example, perhaps you need to define _different_ syslog servers within the region for a particular device role. You can create a second config context with the appropriate data and a higher weight, and apply it to the desired role. This will override the lower-weight data that applies to the entire region. As you can imagine, this flexibility can cater to many complex use cases. -```json -{ - "ntp-servers": [ - "172.16.10.22", - "172.16.10.33" - ], - "syslog-servers": [ - "192.168.43.107" - ] -} -``` +There are no restrictions around what data can be stored in a configuration context, so long as it can be expressed in JSON. Additionally, each device and VM may have local context data defined: This data is stored directly on the assigned object, and applies to it only. This is a convenient way for "attaching" miscellaneous data to a single device or VM as needed. -Data from the higher-weight context overwrites conflicting data from the lower-weight context, while the non-conflicting portion of the lower-weight context (the list of NTP servers) is preserved. +Config contexts can be computed for objects based on the following criteria: -## Local Context Data - -Devices and virtual machines may also have a local config context defined. This local context will _always_ take precedence over any separate config context objects which apply to the device/VM. This is useful in situations where we need to call out a specific deviation in the data for a particular object. - -!!! warning - If you find that you're routinely defining local context data for many individual devices or virtual machines, custom fields may offer a more effective solution. +| Type | Devices | Virtual Machines | +|---------------|------------------|------------------| +| Region | :material-check: | :material-check: | +| Site group | :material-check: | :material-check: | +| Site | :material-check: | :material-check: | +| Location | :material-check: | | +| Device type | :material-check: | | +| Role | :material-check: | :material-check: | +| Platform | :material-check: | :material-check: | +| Cluster type | | :material-check: | +| Cluster group | | :material-check: | +| Cluster | | :material-check: | +| Tenant group | :material-check: | :material-check: | +| Tenant | :material-check: | :material-check: | +| Tag | :material-check: | :material-check: | diff --git a/docs/features/customization.md b/docs/features/customization.md index ae775312e..5b42d5e43 100644 --- a/docs/features/customization.md +++ b/docs/features/customization.md @@ -1,3 +1,41 @@ # Customization +While NetBox strives to meet the needs of every network, the needs of users to cater to their own unique environments cannot be ignored. NetBox was built with this in mind, and can be customized in many ways to better suit your particular needs. + +## Custom Fields + +While NetBox provides a rather extensive data model out of the box, the need may arise to store certain additional data associated with NetBox objects. For example, you might need to record the invoice ID alongside an installed device, or record an approving authority when creating a new IP prefix. NetBox administrators can create custom fields on built-in objects to meet these needs. + +NetBox supports many types of custom field, from basic data types like strings and integers, to complex structures like selection lists or raw JSON. It's even possible to add a custom field which references other NetBox objects. Custom field data is stored directly alongside the object to which it is applied in the database, which ensures minimal performance impact. And custom field data can be written and read via the REST API, just like built-in fields. + +To learn more about this feature, check out the [custom field documentation](../customization/custom-fields.md). + +## Custom Links + TODO + +To learn more about this feature, check out the [custom link documentation](../customization/custom-links.md). + +## Custom Validation + +TODO + +To learn more about this feature, check out the [custom validation documentation](../customization/custom-validation.md). + +## Export Templates + +TODO + +To learn more about this feature, check out the [export template documentation](../customization/export-templates.md). + +## Reports + +TODO + +To learn more about this feature, check out the [documentation for reports](../customization/reports.md). + +## Custom Scripts + +TODO + +To learn more about this feature, check out the [documentation for custom scripts](../customization/custom-scripts.md). diff --git a/docs/features/integrations.md b/docs/features/integrations.md new file mode 100644 index 000000000..c2f68c5c8 --- /dev/null +++ b/docs/features/integrations.md @@ -0,0 +1,41 @@ +# Integrations + +NetBox includes a slew of features which enable integration with other tools and resources powering your network. + +## REST API + +NetBox's REST API, powered by the [Django REST Framework](https://www.django-rest-framework.org/), provides a robust yet accessible interface for creating, modifying, and deleting objects. Employing HTTP for transfer and JSON for data encapsulation, the REST API is easily consumed by clients on any platform and extremely well suited for automation tasks. + +```no-highlight +curl -s -X POST \ +-H "Authorization: Token $TOKEN" \ +-H "Content-Type: application/json" \ +http://netbox/api/ipam/prefixes/ \ +--data '{"prefix": "192.0.2.0/24", "site": {"name": "Branch 12"}}' +``` + +The REST API employs token-based authentication, which maps API clients to user accounts and their assigned permissions. The API endpoints are fully documented using OpenAPI, and NetBox even includes a convenient browser-based version of the API for exploration. The open source [pynetbox](https://github.com/netbox-community/pynetbox) and [go-netbox](https://github.com/netbox-community/go-netbox) API client libraries are also available for Python and Go, respectively. + +To learn more about this feature, check out the [REST API documentation](../integrations/rest-api.md). + +## GraphQL API + +NetBox also provides a [GraphQL](https://graphql.org/) API to complement its REST API. GraphQL enables complex queries for arbitrary objects and fields, enabling the client to retrieve only the specific data it needs from NetBox. This is a special-purpose read-only API intended for efficient queries. Like the REST API, the GraphQL API employs token-based authentication. + +To learn more about this feature, check out the [GraphQL API documentation](../integrations/graphql-api.md). + +## Webhooks + +A webhook is a mechanism for conveying to some external system a change that took place in NetBox. For example, you may want to notify a monitoring system whenever the status of a device is updated in NetBox. This can be done by creating a webhook for the device model in NetBox and identifying the webhook receiver. When NetBox detects a change to a device, an HTTP request containing the details of the change and who made it be sent to the specified receiver. Webhooks are an excellent mechanism for building event-based automation processes. + +To learn more about this feature, check out the [webhooks documentation](../integrations/webhooks.md). + +## NAPALM + +[NAPALM](https://github.com/napalm-automation/napalm) is a Python library which enables direct interaction with network devices of various platforms. When configured, NetBox supports fetching live operational and status data directly from network devices to be compared to what has been defined in NetBox. This allows for easily validating the device's operational state against its desired state. Additionally, NetBox's REST API can act as a sort of proxy for NAPALM commands, allowing external clients to interact with network devices by sending HTTP requests to the appropriate API endpoint. + +To learn more about this feature, check out the [NAPALM documentation](../integrations/napalm.md). + +## Prometheus Metrics + +NetBox includes a special `/metrics` view which exposes metrics for a [Prometheus](https://prometheus.io/) scraper, powered by the open source [django-prometheus](https://github.com/korfuri/django-prometheus) library. To learn more about this feature, check out the [Prometheus metrics documentation](../integrations/prometheus-metrics.md). diff --git a/docs/features/tags.md b/docs/features/tags.md index fe6a1ef36..e69de29bb 100644 --- a/docs/features/tags.md +++ b/docs/features/tags.md @@ -1,17 +0,0 @@ -# Tags - -Tags are user-defined labels which can be applied to a variety of objects within NetBox. They can be used to establish dimensions of organization beyond the relationships built into NetBox. For example, you might create a tag to identify a particular ownership or condition across several types of objects. - -Each tag has a label, color, and a URL-friendly slug. For example, the slug for a tag named "Dunder Mifflin, Inc." would be `dunder-mifflin-inc`. The slug is generated automatically and makes tags easier to work with as URL parameters. Each tag can also be assigned a description indicating its purpose. - -Objects can be filtered by the tags they have applied. For example, the following API request will retrieve all devices tagged as "monitored": - -```no-highlight -GET /api/dcim/devices/?tag=monitored -``` - -The `tag` filter can be specified multiple times to match only objects which have _all_ of the specified tags assigned: - -```no-highlight -GET /api/dcim/devices/?tag=monitored&tag=deprecated -``` diff --git a/docs/features/webhooks.md b/docs/integrations/webhooks.md similarity index 100% rename from docs/features/webhooks.md rename to docs/integrations/webhooks.md diff --git a/docs/models/extras/configcontext.md b/docs/models/extras/configcontext.md new file mode 100644 index 000000000..08b5f4fd5 --- /dev/null +++ b/docs/models/extras/configcontext.md @@ -0,0 +1,69 @@ +# Configuration Contexts + +Sometimes it is desirable to associate additional data with a group of devices or virtual machines to aid in automated configuration. For example, you might want to associate a set of syslog servers for all devices within a particular region. Context data enables the association of extra user-defined data with devices and virtual machines grouped by one or more of the following assignments: + +* Region +* Site group +* Site +* Location (devices only) +* Device type (devices only) +* Role +* Platform +* Cluster type (VMs only) +* Cluster group (VMs only) +* Cluster (VMs only) +* Tenant group +* Tenant +* Tag + +## Hierarchical Rendering + +Context data is arranged hierarchically, so that data with a higher weight can be entered to override lower-weight data. Multiple instances of data are automatically merged by NetBox to present a single dictionary for each object. + +For example, suppose we want to specify a set of syslog and NTP servers for all devices within a region. We could create a config context instance with a weight of 1000 assigned to the region, with the following JSON data: + +```json +{ + "ntp-servers": [ + "172.16.10.22", + "172.16.10.33" + ], + "syslog-servers": [ + "172.16.9.100", + "172.16.9.101" + ] +} +``` + +But suppose there's a problem at one particular site within this region preventing traffic from reaching the regional syslog server. Devices there need to use a local syslog server instead of the two defined above. We'll create a second config context assigned only to that site with a weight of 2000 and the following data: + +```json +{ + "syslog-servers": [ + "192.168.43.107" + ] +} +``` + +When the context data for a device at this site is rendered, the second, higher-weight data overwrite the first, resulting in the following: + +```json +{ + "ntp-servers": [ + "172.16.10.22", + "172.16.10.33" + ], + "syslog-servers": [ + "192.168.43.107" + ] +} +``` + +Data from the higher-weight context overwrites conflicting data from the lower-weight context, while the non-conflicting portion of the lower-weight context (the list of NTP servers) is preserved. + +## Local Context Data + +Devices and virtual machines may also have a local config context defined. This local context will _always_ take precedence over any separate config context objects which apply to the device/VM. This is useful in situations where we need to call out a specific deviation in the data for a particular object. + +!!! warning + If you find that you're routinely defining local context data for many individual devices or virtual machines, custom fields may offer a more effective solution. diff --git a/docs/models/extras/tag.md b/docs/models/extras/tag.md new file mode 100644 index 000000000..fe6a1ef36 --- /dev/null +++ b/docs/models/extras/tag.md @@ -0,0 +1,17 @@ +# Tags + +Tags are user-defined labels which can be applied to a variety of objects within NetBox. They can be used to establish dimensions of organization beyond the relationships built into NetBox. For example, you might create a tag to identify a particular ownership or condition across several types of objects. + +Each tag has a label, color, and a URL-friendly slug. For example, the slug for a tag named "Dunder Mifflin, Inc." would be `dunder-mifflin-inc`. The slug is generated automatically and makes tags easier to work with as URL parameters. Each tag can also be assigned a description indicating its purpose. + +Objects can be filtered by the tags they have applied. For example, the following API request will retrieve all devices tagged as "monitored": + +```no-highlight +GET /api/dcim/devices/?tag=monitored +``` + +The `tag` filter can be specified multiple times to match only objects which have _all_ of the specified tags assigned: + +```no-highlight +GET /api/dcim/devices/?tag=monitored&tag=deprecated +``` diff --git a/mkdocs.yml b/mkdocs.yml index 784e4498e..95672cede 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -73,8 +73,8 @@ nav: - Context Data: 'features/context-data.md' - Change Logging: 'features/change-logging.md' - Journaling: 'features/journaling.md' - - Webhooks: 'features/webhooks.md' - Tags: 'features/tags.md' + - Integrations: 'features/integrations.md' - Customization: 'features/customization.md' - Object-Based Permissions: 'features/permissions.md' - Single Sign-On (SSO): 'features/sso.md' @@ -114,6 +114,7 @@ nav: - Integrations: - REST API: 'integrations/rest-api.md' - GraphQL API: 'integrations/graphql-api.md' + - Webhooks: 'integrations/webhooks.md' - NAPALM: 'integrations/napalm.md' - Prometheus Metrics: 'integrations/prometheus-metrics.md' - Plugins: @@ -188,6 +189,9 @@ nav: - Site: 'models/dcim/site.md' - SiteGroup: 'models/dcim/sitegroup.md' - VirtualChassis: 'models/dcim/virtualchassis.md' + - Extras: + - ConfigContext: 'models/extras/configcontext.md' + - Tag: 'models/extras/tag.md' - IPAM: - ASN: 'models/ipam/asn.md' - Aggregate: 'models/ipam/aggregate.md' From 8f1e70f01d761f65382bb01d568e5f60ae47aa68 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 10 Aug 2022 15:24:45 -0400 Subject: [PATCH 366/593] Fixes #9961: Correct typo --- docs/models/dcim/frontport.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/models/dcim/frontport.md b/docs/models/dcim/frontport.md index 0b753c012..6f12e8cbf 100644 --- a/docs/models/dcim/frontport.md +++ b/docs/models/dcim/frontport.md @@ -1,3 +1,3 @@ ## Front Ports -Front ports are pass-through ports used to represent physical cable connections that comprise part of a longer path. For example, the ports on the front face of a UTP patch panel would be modeled in NetBox as front ports. Each port is assigned a physical type, and must be mapped to a specific rear port on the same device. A single rear port may be mapped to multiple rear ports, using numeric positions to annotate the specific alignment of each. +Front ports are pass-through ports used to represent physical cable connections that comprise part of a longer path. For example, the ports on the front face of a UTP patch panel would be modeled in NetBox as front ports. Each port is assigned a physical type, and must be mapped to a specific rear port on the same device. A single rear port may be mapped to multiple front ports, using numeric positions to annotate the specific alignment of each. From a9aaa8939c2bc15ff59f82d547ae4daa45234561 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 10 Aug 2022 16:08:52 -0400 Subject: [PATCH 367/593] Closes #9161: Pretty print JSON custom field data when editing --- docs/release-notes/version-3.2.md | 2 ++ netbox/extras/models/customfields.py | 4 ++-- netbox/project-static/dist/netbox-dark.css | Bin 374410 -> 374221 bytes netbox/project-static/dist/netbox-light.css | Bin 232175 -> 232088 bytes netbox/project-static/dist/netbox-print.css | Bin 727867 -> 727384 bytes netbox/project-static/styles/netbox.scss | 5 +---- netbox/utilities/forms/fields/fields.py | 1 + netbox/utilities/forms/forms.py | 2 +- 8 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/release-notes/version-3.2.md b/docs/release-notes/version-3.2.md index 56a35cc02..a5bd05cde 100644 --- a/docs/release-notes/version-3.2.md +++ b/docs/release-notes/version-3.2.md @@ -2,6 +2,8 @@ ## v3.2.9 (FUTURE) +* [#9161](https://github.com/netbox-community/netbox/issues/9161) - Pretty print JSON custom field data when editing + --- ## v3.2.8 (2022-08-08) diff --git a/netbox/extras/models/customfields.py b/netbox/extras/models/customfields.py index b7d77e550..55b7a9f03 100644 --- a/netbox/extras/models/customfields.py +++ b/netbox/extras/models/customfields.py @@ -18,7 +18,7 @@ from netbox.models.features import ExportTemplatesMixin, WebhooksMixin from utilities import filters from utilities.forms import ( CSVChoiceField, CSVMultipleChoiceField, DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField, - LaxURLField, StaticSelectMultiple, StaticSelect, add_blank_choice, + JSONField, LaxURLField, StaticSelectMultiple, StaticSelect, add_blank_choice, ) from utilities.querysets import RestrictedQuerySet from utilities.validators import validate_regex @@ -343,7 +343,7 @@ class CustomField(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel): # JSON elif self.type == CustomFieldTypeChoices.TYPE_JSON: - field = forms.JSONField(required=required, initial=initial) + field = JSONField(required=required, initial=initial) # Object elif self.type == CustomFieldTypeChoices.TYPE_OBJECT: diff --git a/netbox/project-static/dist/netbox-dark.css b/netbox/project-static/dist/netbox-dark.css index b929f176a6c67c2067c4f29044f724d50ccbd60b..c6e69db89fe9d6f02b4d9600fc1d70b2ca3c7bab 100644 GIT binary patch delta 44 zcmeBrD|Ys^SVIeA3sVbo3(FQ(>*>>Lrn8z%zQ2i=GdeFZH`O+|xNN)A3|0?00G8Pi A9{>OV delta 108 zcmX^6TCD4>SVIeA3sVbo3(FQ(>*-?3nJMu(`N@en@yYplC8-r9@hOQViIWdXs!Yy* z#xdPyI;$}+NKHX$Qch-ae0J*e##Ba?$$8I2r`Jqp<$@{{;fl^n%uTfg>D}Hsoz+7Q E0PR&NdH?_b diff --git a/netbox/project-static/dist/netbox-light.css b/netbox/project-static/dist/netbox-light.css index 341369adff9033ea697bf89ab50ae0c592c00e73..c3f8b80a69ea3e2cd756bc65c9a32709a7af0d4f 100644 GIT binary patch delta 40 wcmaDqm2bvWzJ?aY7N#xCyGuFsaubWPQ}WC6CKq;!O&2I*=Gtyu#=J!Y07M%O+5i9m delta 109 zcmbO+mGAvjzJ?aY7N#xCyG!krGgIPo@{<#D;*<09N>VFI;!_e!5_LeV#G=$hz1+m2 y?3DcSJh%u*LqTa$PG)j^c53D1!cMX2w@R70Kn$+vyu{p8TaW?UAD1$36#)QFI4eQ` diff --git a/netbox/project-static/dist/netbox-print.css b/netbox/project-static/dist/netbox-print.css index 61bcedf9c0ec303c353a88390cdb00e18b3f5aaf..3b10f5c27c1c4d3fea800feadc6b56b2fb229115 100644 GIT binary patch delta 101 zcmV-r0Gj{1wk+7PEP#XogaU*Egam{Iga(8Mgb0KQvuC6iz(2b1tR2bb{M3N(|@EeMw&-3kbox!ejgm#F7M2#)7Pc1l7LFFqEnLmF#FaBs;&bwo6LaE|^YcnlD@x*1 z5=#%n@fJW`fV=V=^FwWdAOqCj@~}+HrFm&09QI|KL7v# diff --git a/netbox/project-static/styles/netbox.scss b/netbox/project-static/styles/netbox.scss index a54b6c324..8ef280397 100644 --- a/netbox/project-static/styles/netbox.scss +++ b/netbox/project-static/styles/netbox.scss @@ -714,11 +714,8 @@ textarea.form-control[rows='10'] { height: 18rem; } -textarea#id_local_context_data, textarea.markdown, -textarea#id_public_key, -textarea.form-control[name='csv'], -textarea.form-control[name='data'] { +textarea.form-control[name='csv'] { font-family: $font-family-monospace; } diff --git a/netbox/utilities/forms/fields/fields.py b/netbox/utilities/forms/fields/fields.py index 9168189a1..df69339e5 100644 --- a/netbox/utilities/forms/fields/fields.py +++ b/netbox/utilities/forms/fields/fields.py @@ -99,6 +99,7 @@ class JSONField(_JSONField): if not self.help_text: self.help_text = 'Enter context data in JSON format.' self.widget.attrs['placeholder'] = '' + self.widget.attrs['class'] = 'font-monospace' def prepare_value(self, value): if isinstance(value, InvalidJSONInput): diff --git a/netbox/utilities/forms/forms.py b/netbox/utilities/forms/forms.py index 3b5cd8308..8ad6f103b 100644 --- a/netbox/utilities/forms/forms.py +++ b/netbox/utilities/forms/forms.py @@ -136,7 +136,7 @@ class ImportForm(BootstrapMixin, forms.Form): Generic form for creating an object from JSON/YAML data """ data = forms.CharField( - widget=forms.Textarea, + widget=forms.Textarea(attrs={'class': 'font-monospace'}), help_text="Enter object data in JSON or YAML format. Note: Only a single object/document is supported." ) format = forms.ChoiceField( From aabe8f7c5b4b61ca87623b496d127266aa443a52 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 10 Aug 2022 16:18:30 -0400 Subject: [PATCH 368/593] Changelog for #9625 --- docs/release-notes/version-3.2.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/release-notes/version-3.2.md b/docs/release-notes/version-3.2.md index a5bd05cde..598ee7874 100644 --- a/docs/release-notes/version-3.2.md +++ b/docs/release-notes/version-3.2.md @@ -2,7 +2,10 @@ ## v3.2.9 (FUTURE) +### Enhancements + * [#9161](https://github.com/netbox-community/netbox/issues/9161) - Pretty print JSON custom field data when editing +* [#9625](https://github.com/netbox-community/netbox/issues/9625) - Add phone & email details to contacts panel --- From 9a80a491c90bcdc3341a7c75f2f8bae93e32fba9 Mon Sep 17 00:00:00 2001 From: Christoph Schneider Date: Thu, 11 Aug 2022 14:11:41 +0200 Subject: [PATCH 369/593] re-enable markdown in custom columns --- netbox/netbox/tables/columns.py | 25 +++++++++++++++++++++++++ netbox/netbox/tables/tables.py | 3 ++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/netbox/netbox/tables/columns.py b/netbox/netbox/tables/columns.py index f78b9f37c..cc20bdd0c 100644 --- a/netbox/netbox/tables/columns.py +++ b/netbox/netbox/tables/columns.py @@ -550,3 +550,28 @@ class MarkdownColumn(tables.TemplateColumn): def value(self, value): return value + + +class CustomFieldMarkdownColumn(tables.TemplateColumn): + """ + Render a Markdown string in a longtext custom column. + """ + template_code = """ + {% if value %} + {{ value|markdown }} + {% else %} + — + {% endif %} + """ + + def __init__(self, customfield, *args, **kwargs): + self.customfield = customfield + kwargs['accessor'] = Accessor(f'custom_field_data__{customfield.name}') + kwargs['template_code'] = self.template_code + if 'verbose_name' not in kwargs: + kwargs['verbose_name'] = customfield.label or customfield.name + + super().__init__(*args, **kwargs) + + def value(self, value): + return value diff --git a/netbox/netbox/tables/tables.py b/netbox/netbox/tables/tables.py index 8c5fb039c..d55038c4d 100644 --- a/netbox/netbox/tables/tables.py +++ b/netbox/netbox/tables/tables.py @@ -7,6 +7,7 @@ from django.db.models.fields.related import RelatedField from django_tables2.data import TableQuerysetData from extras.models import CustomField, CustomLink +from extras.choices import CustomFieldTypeChoices from netbox.tables import columns from utilities.paginator import EnhancedPaginator, get_paginate_count @@ -180,7 +181,7 @@ class NetBoxTable(BaseTable): content_type = ContentType.objects.get_for_model(self._meta.model) custom_fields = CustomField.objects.filter(content_types=content_type) extra_columns.extend([ - (f'cf_{cf.name}', columns.CustomFieldColumn(cf)) for cf in custom_fields + (f'cf_{cf.name}', columns.CustomFieldMarkdownColumn(cf) if cf.type == CustomFieldTypeChoices.TYPE_LONGTEXT else columns.CustomFieldColumn(cf)) for cf in custom_fields ]) custom_links = CustomLink.objects.filter(content_type=content_type, enabled=True) extra_columns.extend([ From f74b7aa7acb6ee9f84e9d4ff237c33bac92cc0d8 Mon Sep 17 00:00:00 2001 From: Jonathan Senecal Date: Tue, 9 Aug 2022 17:32:20 -0400 Subject: [PATCH 370/593] Add a "clear" button for quick search Fixes #9857 --- netbox/project-static/dist/netbox-dark.css | Bin 374221 -> 375157 bytes netbox/project-static/dist/netbox-light.css | Bin 232088 -> 232694 bytes netbox/project-static/dist/netbox-print.css | Bin 727384 -> 729266 bytes netbox/project-static/dist/netbox.js | Bin 375907 -> 376463 bytes netbox/project-static/dist/netbox.js.map | Bin 345348 -> 345789 bytes netbox/project-static/src/search.ts | 44 ++++++ netbox/project-static/styles/netbox.scss | 21 +++ netbox/project-static/styles/overrides.scss | 8 + netbox/project-static/styles/theme-dark.scss | 4 + netbox/project-static/styles/theme-light.scss | 3 +- netbox/project-static/styles/utilities.scss | 6 + netbox/templates/dcim/device/interfaces.html | 145 +++++++++--------- netbox/templates/inc/table_controls_htmx.html | 34 ++-- 13 files changed, 169 insertions(+), 96 deletions(-) diff --git a/netbox/project-static/dist/netbox-dark.css b/netbox/project-static/dist/netbox-dark.css index c6e69db89fe9d6f02b4d9600fc1d70b2ca3c7bab..94718ac407f4e5b8c9acba2d7c97c9936fc79765 100644 GIT binary patch delta 597 zcmb7=y-EW?0E9{IqJn~j_=8%U8Vk?tkwmoEpwFPCh~Q>#^_E~Y+5P~z3FHi+O4B!&X1yYpj>QO9d z|Dk~q!^8*8-G#OFKg0!*p|4~=zx9v&HyDfb&6PQ&#e$jDZDu4|&mFTJcN~wp1o%iQ zfIaHF798lPL?z8rpQ;M%#azPl4oZMl@1=Tu#5oNqE@W5s4GZfKQGU1RL3)yhMuu7YZ?zI|r1xqzp6Z^pML#|?q zxUB?YlJ?GKZ~$nfWr_y~_gF0_E3Nkdr1wUR)!m;=7W8*t%0)<@zwOs_p=$Ft(1P8a9>IA<6lW+wFx4Z2G Vh>n+>egzDdLUsZYw*!6!ayxcD8N>hp diff --git a/netbox/project-static/dist/netbox-light.css b/netbox/project-static/dist/netbox-light.css index c3f8b80a69ea3e2cd756bc65c9a32709a7af0d4f..bf0ba2f62028e5344fcee1c4c872304e047234d6 100644 GIT binary patch delta 560 zcma)2y-EW?7$kS3iJHP65UUjto9uZP5oAv-ZEXdy*xOsUk6d;yw|mhTBntWjBCEZR zpj_z-*yIuP39M|RG3qH6ru)8OW|*1ltLp1b_4K@SXZeF3h$Pedx|8rN#c9f-148g2 z4;rBH2*4p{GNl@%sExs9R4^XnAk%sb!6u7v%2gI?HLj#kT+UA~RTiO{$W&9Ib26~< z(uNa3$Z<@S#w>z3M5MU8ZBP(E47x+!X(1QBOeh<^?){5>OOeycfHg+V(cijVbJj1- znFr6BEPpV~_OrE6$7}WEs7T@;V-O)fBA)C1KxL+55S$1pxM}U$Gv;xpG-XyjYoe!9 zDijPyCPhZ%s@i8hF7M2#)7Pc1l7LF~PB~8;6^*DtmE4j!|SJ2}WQL@r4Pfg0s zEYU4aO)N^z&`nOvOHR$vO)4!Z$@np0AYp->!$!pVj%dN8YP z!3;5Ul@P0^H~KMZffe8~im(cxWg3$eSlM9~n!`D`EEQbMqK)5Df+{Ajb5Wi?t$|B^ zdciw3*6meIoI0$6dKsB1sk%9d#U;AQ8JRgL3ey|)Sv4kq;N;-PB4MD5uG|WtwYn^` zI5R0TC$pr|3aBh4HE()j8mCPiG%9ov7 zkidZulbSx!i$!$$0bX{l>GQkTM4>W;+MI?6rWQy~B-k8C0#SgZjOyIPqV&u>-JI04 z5-VL}qk;;>%-n+fqLReC66-46%)FG;3M)ec10>P>(vqCayi}{a{Jd0{@brXhED|6m zaBGzmCFT`pmSpDVSs^^CU{I`+Uyzs#^s|DVQLzHZfW+yB(u`ulm~K*-Zupl;3#)|S mVJ7VWEIO<*^2<_-s-ad`nHs2C6LyCok~>7Vzh2ChTM7Wc!7`fw delta 68 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 delta 19478 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 NWqw?$eUNj`_&>7kHAesd diff --git a/netbox/project-static/dist/netbox.js.map b/netbox/project-static/dist/netbox.js.map index 60656bc6d6f3450acca6ca6d4369da29b4749527..9b92d1489c63f744f04a6085c653d32bd45b6331 100644 GIT binary patch delta 495 zcmZ8e!AiqG5JioMc<|JlpD>{4p@^rq)1-v3)DUW#V;Xv>RIB2__7V>{37+#2f+wLT zKfvGcOPtvRQoJlPZ)e|o^R{~HeZTi!tDS1M+WYc8@^XA(CCV7kZ=**eU_?uUc)|5& z?iFQx8&K{Hi4{@ltRyNm17>pYvooAh2R<6`n*X~J*^K5!2#xwmlyBwM><=0sfs%>JYv=S42XU*m3KR!e7Ag%jYLgCCHKy { + const search = new Event('search'); + quicksearch.value = ''; + await new Promise(f => setTimeout(f, 100)); + quicksearch.dispatchEvent(search); + }, { + passive: true + }) + } + } +} + export function initSearch(): void { for (const func of [initSearchBar]) { func(); } + initQuickSearch(); } diff --git a/netbox/project-static/styles/netbox.scss b/netbox/project-static/styles/netbox.scss index 8ef280397..d78e9e9b9 100644 --- a/netbox/project-static/styles/netbox.scss +++ b/netbox/project-static/styles/netbox.scss @@ -416,6 +416,27 @@ nav.search { } } +// Styles for the quicksearch and its clear button; +// Overrides input-group styles and adds transition effects +.quicksearch { + input[type="search"] { + border-radius: $border-radius !important; + } + + button { + margin-left: -32px !important; + z-index: 100 !important; + outline: none !important; + border-radius: $border-radius !important; + transition: visibility 0s, opacity 0.2s linear; + } + + button :hover { + opacity: 50%; + transition: visibility 0s, opacity 0.1s linear; + } +} + main.layout { display: flex; flex-wrap: nowrap; diff --git a/netbox/project-static/styles/overrides.scss b/netbox/project-static/styles/overrides.scss index 03c72c6e6..7fa366df8 100644 --- a/netbox/project-static/styles/overrides.scss +++ b/netbox/project-static/styles/overrides.scss @@ -34,3 +34,11 @@ a[type='button'] { .badge { font-size: $font-size-xs; } + +/* clears the 'X' in search inputs from webkit browsers */ +input[type='search']::-webkit-search-decoration, +input[type='search']::-webkit-search-cancel-button, +input[type='search']::-webkit-search-results-button, +input[type='search']::-webkit-search-results-decoration { + -webkit-appearance: none !important; +} diff --git a/netbox/project-static/styles/theme-dark.scss b/netbox/project-static/styles/theme-dark.scss index c0933e991..4bbe5cea5 100644 --- a/netbox/project-static/styles/theme-dark.scss +++ b/netbox/project-static/styles/theme-dark.scss @@ -92,6 +92,10 @@ $input-focus-color: $input-color; $input-placeholder-color: $gray-700; $input-plaintext-color: $body-color; +input { + color-scheme: dark; +} + $form-check-input-active-filter: brightness(90%); $form-check-input-bg: $input-bg; $form-check-input-border: 1px solid rgba(255, 255, 255, 0.25); diff --git a/netbox/project-static/styles/theme-light.scss b/netbox/project-static/styles/theme-light.scss index d417e1bf6..c9478f1cc 100644 --- a/netbox/project-static/styles/theme-light.scss +++ b/netbox/project-static/styles/theme-light.scss @@ -22,7 +22,6 @@ $theme-colors: ( 'danger': $danger, 'light': $light, 'dark': $dark, - // General-purpose palette 'blue': $blue-500, 'indigo': $indigo-500, @@ -36,7 +35,7 @@ $theme-colors: ( 'cyan': $cyan-500, 'gray': $gray-500, 'black': $black, - 'white': $white, + 'white': $white ); $light: $gray-200; diff --git a/netbox/project-static/styles/utilities.scss b/netbox/project-static/styles/utilities.scss index cd8ccc46b..a5a4bf038 100644 --- a/netbox/project-static/styles/utilities.scss +++ b/netbox/project-static/styles/utilities.scss @@ -42,3 +42,9 @@ table td { visibility: visible !important; } } + +// Hides the last child of an element +.hide-last-child :last-child { + visibility: hidden; + opacity: 0; +} diff --git a/netbox/templates/dcim/device/interfaces.html b/netbox/templates/dcim/device/interfaces.html index 22f6d8be5..79a9d6b6f 100644 --- a/netbox/templates/dcim/device/interfaces.html +++ b/netbox/templates/dcim/device/interfaces.html @@ -4,84 +4,85 @@ {% load static %} {% block content %} -
    -
    -
    - -
    +
    +
    +
    + +
    -
    -
    - {% if request.user.is_authenticated %} - - {% endif %} - - -
    +
    +
    +
    + {% if request.user.is_authenticated %} + + {% endif %} + + +
    +
    +
    + +
    + {% csrf_token %} + + +
    +
    + {% include 'htmx/table.html' %}
    - - {% csrf_token %} - - -
    -
    - {% include 'htmx/table.html' %} -
    +
    +
    + {% if perms.dcim.change_interface %} + + + + {% endif %} + {% if perms.dcim.delete_interface %} + + {% endif %}
    - -
    -
    - {% if perms.dcim.change_interface %} - - - - {% endif %} - {% if perms.dcim.delete_interface %} - - {% endif %} -
    - {% if perms.dcim.add_interface %} - - {% endif %} + {% if perms.dcim.add_interface %} + - + {% endif %} +
    + {% endblock %} {% block modals %} - {{ block.super }} - {% table_config_form table %} -{% endblock modals %} +{{ block.super }} +{% table_config_form table %} +{% endblock modals %} \ No newline at end of file diff --git a/netbox/templates/inc/table_controls_htmx.html b/netbox/templates/inc/table_controls_htmx.html index ab8167bc0..099ad537e 100644 --- a/netbox/templates/inc/table_controls_htmx.html +++ b/netbox/templates/inc/table_controls_htmx.html @@ -2,31 +2,21 @@
    -
    - +
    + +
    {% if request.user.is_authenticated and table_modal %} -
    - -
    +
    + +
    {% endif %}
    -
    +
    \ No newline at end of file From 8cd5a24409c993cc3533a1bfa03a95cf2a2f42b6 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Thu, 11 Aug 2022 09:58:37 -0400 Subject: [PATCH 371/593] #9414: Clean up clone_fields on all models --- netbox/circuits/models/circuits.py | 4 ++-- netbox/circuits/models/providers.py | 4 ++-- netbox/dcim/models/device_components.py | 23 +++++++++++++---------- netbox/dcim/models/devices.py | 11 ++++++----- netbox/dcim/models/power.py | 6 +++--- netbox/dcim/models/racks.py | 4 ++-- netbox/dcim/models/sites.py | 10 +++++----- netbox/ipam/models/fhrp.py | 2 +- netbox/ipam/models/ip.py | 18 +++++++++--------- netbox/ipam/models/vrfs.py | 4 ++-- netbox/tenancy/models/contacts.py | 8 ++++---- netbox/tenancy/models/tenants.py | 4 ++-- netbox/virtualization/models.py | 10 +++++----- netbox/wireless/models.py | 2 ++ 14 files changed, 58 insertions(+), 52 deletions(-) diff --git a/netbox/circuits/models/circuits.py b/netbox/circuits/models/circuits.py index d82878cde..c78ea81c7 100644 --- a/netbox/circuits/models/circuits.py +++ b/netbox/circuits/models/circuits.py @@ -125,9 +125,9 @@ class Circuit(NetBoxModel): null=True ) - clone_fields = [ + clone_fields = ( 'provider', 'type', 'status', 'tenant', 'install_date', 'termination_date', 'commit_rate', 'description', - ] + ) class Meta: ordering = ['provider', 'cid'] diff --git a/netbox/circuits/models/providers.py b/netbox/circuits/models/providers.py index 4211a54a6..e136e13ea 100644 --- a/netbox/circuits/models/providers.py +++ b/netbox/circuits/models/providers.py @@ -61,9 +61,9 @@ class Provider(NetBoxModel): to='tenancy.ContactAssignment' ) - clone_fields = [ + clone_fields = ( 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', - ] + ) class Meta: ordering = ['name'] diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index 5e2fc348e..838336e21 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -263,7 +263,7 @@ class ConsolePort(ModularComponentModel, CabledObjectModel, PathEndpoint): help_text='Port speed in bits per second' ) - clone_fields = ['device', 'type', 'speed'] + clone_fields = ('device', 'module', 'type', 'speed') class Meta: ordering = ('device', '_name') @@ -290,7 +290,7 @@ class ConsoleServerPort(ModularComponentModel, CabledObjectModel, PathEndpoint): help_text='Port speed in bits per second' ) - clone_fields = ['device', 'type', 'speed'] + clone_fields = ('device', 'module', 'type', 'speed') class Meta: ordering = ('device', '_name') @@ -327,7 +327,7 @@ class PowerPort(ModularComponentModel, CabledObjectModel, PathEndpoint): help_text="Allocated power draw (watts)" ) - clone_fields = ['device', 'maximum_draw', 'allocated_draw'] + clone_fields = ('device', 'module', 'maximum_draw', 'allocated_draw') class Meta: ordering = ('device', '_name') @@ -441,7 +441,7 @@ class PowerOutlet(ModularComponentModel, CabledObjectModel, PathEndpoint): help_text="Phase (for three-phase feeds)" ) - clone_fields = ['device', 'type', 'power_port', 'feed_leg'] + clone_fields = ('device', 'module', 'type', 'power_port', 'feed_leg') class Meta: ordering = ('device', '_name') @@ -672,7 +672,10 @@ class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEnd related_query_name='interface', ) - clone_fields = ['device', 'parent', 'bridge', 'lag', 'type', 'mgmt_only', 'poe_mode', 'poe_type'] + clone_fields = ( + 'device', 'module', 'parent', 'bridge', 'lag', 'type', 'mgmt_only', 'mtu', 'mode', 'speed', 'duplex', 'rf_role', + 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'poe_mode', 'poe_type', 'vrf', + ) class Meta: ordering = ('device', CollateAsChar('_name')) @@ -890,7 +893,7 @@ class FrontPort(ModularComponentModel, CabledObjectModel): ] ) - clone_fields = ['device', 'type'] + clone_fields = ('device', 'type', 'color') class Meta: ordering = ('device', '_name') @@ -937,7 +940,7 @@ class RearPort(ModularComponentModel, CabledObjectModel): MaxValueValidator(REARPORT_POSITIONS_MAX) ] ) - clone_fields = ['device', 'type', 'positions'] + clone_fields = ('device', 'type', 'color', 'positions') class Meta: ordering = ('device', '_name') @@ -972,7 +975,7 @@ class ModuleBay(ComponentModel): help_text='Identifier to reference when renaming installed components' ) - clone_fields = ['device'] + clone_fields = ('device',) class Meta: ordering = ('device', '_name') @@ -994,7 +997,7 @@ class DeviceBay(ComponentModel): null=True ) - clone_fields = ['device'] + clone_fields = ('device',) class Meta: ordering = ('device', '_name') @@ -1131,7 +1134,7 @@ class InventoryItem(MPTTModel, ComponentModel): objects = TreeManager() - clone_fields = ['device', 'parent', 'role', 'manufacturer', 'part_id'] + clone_fields = ('device', 'parent', 'role', 'manufacturer', 'part_id',) class Meta: ordering = ('device__id', 'parent__id', '_name') diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py index f21176d8d..136fcf6cf 100644 --- a/netbox/dcim/models/devices.py +++ b/netbox/dcim/models/devices.py @@ -135,9 +135,9 @@ class DeviceType(NetBoxModel): blank=True ) - clone_fields = [ + clone_fields = ( 'manufacturer', 'u_height', 'is_full_depth', 'subdevice_role', 'airflow', - ] + ) class Meta: ordering = ['manufacturer', 'model'] @@ -630,9 +630,10 @@ class Device(NetBoxModel, ConfigContextModel): objects = ConfigContextModelQuerySet.as_manager() - clone_fields = [ - 'device_type', 'device_role', 'tenant', 'platform', 'site', 'location', 'rack', 'status', 'airflow', 'cluster', - ] + clone_fields = ( + 'device_type', 'device_role', 'tenant', 'platform', 'site', 'location', 'rack', 'face', 'status', 'airflow', + 'cluster', 'virtual_chassis', + ) class Meta: ordering = ('_name', 'pk') # Name may be null diff --git a/netbox/dcim/models/power.py b/netbox/dcim/models/power.py index 94767c6c4..c275691c0 100644 --- a/netbox/dcim/models/power.py +++ b/netbox/dcim/models/power.py @@ -126,10 +126,10 @@ class PowerFeed(NetBoxModel, PathEndpoint, CabledObjectModel): blank=True ) - clone_fields = [ + clone_fields = ( 'power_panel', 'rack', 'status', 'type', 'mark_connected', 'supply', 'phase', 'voltage', 'amperage', - 'max_utilization', 'available_power', - ] + 'max_utilization', + ) class Meta: ordering = ['power_panel', 'name'] diff --git a/netbox/dcim/models/racks.py b/netbox/dcim/models/racks.py index 4dcfcde28..50c91b52e 100644 --- a/netbox/dcim/models/racks.py +++ b/netbox/dcim/models/racks.py @@ -183,10 +183,10 @@ class Rack(NetBoxModel): to='extras.ImageAttachment' ) - clone_fields = [ + clone_fields = ( 'site', 'location', 'tenant', 'status', 'role', 'type', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', - ] + ) class Meta: ordering = ('site', 'location', '_name', 'pk') # (site, location, name) may be non-unique diff --git a/netbox/dcim/models/sites.py b/netbox/dcim/models/sites.py index 9b7ffdcf4..67bcc6e4c 100644 --- a/netbox/dcim/models/sites.py +++ b/netbox/dcim/models/sites.py @@ -295,10 +295,10 @@ class Site(NetBoxModel): to='extras.ImageAttachment' ) - clone_fields = [ - 'status', 'region', 'group', 'tenant', 'facility', 'time_zone', 'description', 'physical_address', - 'shipping_address', 'latitude', 'longitude', - ] + clone_fields = ( + 'status', 'region', 'group', 'tenant', 'facility', 'time_zone', 'physical_address', 'shipping_address', + 'latitude', 'longitude', 'description', + ) class Meta: ordering = ('_name',) @@ -372,7 +372,7 @@ class Location(NestedGroupModel): to='extras.ImageAttachment' ) - clone_fields = ['site', 'parent', 'status', 'tenant', 'description'] + clone_fields = ('site', 'parent', 'status', 'tenant', 'description') class Meta: ordering = ['site', 'name'] diff --git a/netbox/ipam/models/fhrp.py b/netbox/ipam/models/fhrp.py index 2a8d1bdcd..286251444 100644 --- a/netbox/ipam/models/fhrp.py +++ b/netbox/ipam/models/fhrp.py @@ -48,7 +48,7 @@ class FHRPGroup(NetBoxModel): related_query_name='fhrpgroup' ) - clone_fields = ('protocol', 'auth_type', 'auth_key') + clone_fields = ('protocol', 'auth_type', 'auth_key', 'description') class Meta: ordering = ['protocol', 'group_id', 'pk'] diff --git a/netbox/ipam/models/ip.py b/netbox/ipam/models/ip.py index 9ad763920..26cee8100 100644 --- a/netbox/ipam/models/ip.py +++ b/netbox/ipam/models/ip.py @@ -175,9 +175,9 @@ class Aggregate(GetAvailablePrefixesMixin, NetBoxModel): blank=True ) - clone_fields = [ + clone_fields = ( 'rir', 'tenant', 'date_added', 'description', - ] + ) class Meta: ordering = ('prefix', 'pk') # prefix may be non-unique @@ -360,9 +360,9 @@ class Prefix(GetAvailablePrefixesMixin, NetBoxModel): objects = PrefixQuerySet.as_manager() - clone_fields = [ + clone_fields = ( 'site', 'vrf', 'tenant', 'vlan', 'status', 'role', 'is_pool', 'mark_utilized', 'description', - ] + ) class Meta: ordering = (F('vrf').asc(nulls_first=True), 'prefix', 'pk') # (vrf, prefix) may be non-unique @@ -608,9 +608,9 @@ class IPRange(NetBoxModel): blank=True ) - clone_fields = [ + clone_fields = ( 'vrf', 'tenant', 'status', 'role', 'description', - ] + ) class Meta: ordering = (F('vrf').asc(nulls_first=True), 'start_address', 'pk') # (vrf, start_address) may be non-unique @@ -836,9 +836,9 @@ class IPAddress(NetBoxModel): objects = IPAddressManager() - clone_fields = [ - 'vrf', 'tenant', 'status', 'role', 'description', - ] + clone_fields = ( + 'vrf', 'tenant', 'status', 'role', 'dns_name', 'description', + ) class Meta: ordering = ('address', 'pk') # address may be non-unique diff --git a/netbox/ipam/models/vrfs.py b/netbox/ipam/models/vrfs.py index fc34b5488..a926bec3e 100644 --- a/netbox/ipam/models/vrfs.py +++ b/netbox/ipam/models/vrfs.py @@ -55,9 +55,9 @@ class VRF(NetBoxModel): blank=True ) - clone_fields = [ + clone_fields = ( 'tenant', 'enforce_unique', 'description', - ] + ) class Meta: ordering = ('name', 'rd', 'pk') # (name, rd) may be non-unique diff --git a/netbox/tenancy/models/contacts.py b/netbox/tenancy/models/contacts.py index 75ec9f69c..41881f853 100644 --- a/netbox/tenancy/models/contacts.py +++ b/netbox/tenancy/models/contacts.py @@ -112,9 +112,9 @@ class Contact(NetBoxModel): blank=True ) - clone_fields = [ - 'group', - ] + clone_fields = ( + 'group', 'name', 'title', 'phone', 'email', 'address', 'link', + ) class Meta: ordering = ['name'] @@ -155,7 +155,7 @@ class ContactAssignment(WebhooksMixin, ChangeLoggedModel): blank=True ) - clone_fields = ('content_type', 'object_id') + clone_fields = ('content_type', 'object_id', 'role', 'priority') class Meta: ordering = ('priority', 'contact') diff --git a/netbox/tenancy/models/tenants.py b/netbox/tenancy/models/tenants.py index 88d8d52f1..b0ccd1cb2 100644 --- a/netbox/tenancy/models/tenants.py +++ b/netbox/tenancy/models/tenants.py @@ -76,9 +76,9 @@ class Tenant(NetBoxModel): to='tenancy.ContactAssignment' ) - clone_fields = [ + clone_fields = ( 'group', 'description', - ] + ) class Meta: ordering = ['name'] diff --git a/netbox/virtualization/models.py b/netbox/virtualization/models.py index f07b176e7..21bc799be 100644 --- a/netbox/virtualization/models.py +++ b/netbox/virtualization/models.py @@ -153,9 +153,9 @@ class Cluster(NetBoxModel): to='tenancy.ContactAssignment' ) - clone_fields = [ - 'type', 'group', 'tenant', 'site', - ] + clone_fields = ( + 'type', 'group', 'status', 'tenant', 'site', + ) class Meta: ordering = ['name'] @@ -299,9 +299,9 @@ class VirtualMachine(NetBoxModel, ConfigContextModel): objects = ConfigContextModelQuerySet.as_manager() - clone_fields = [ + clone_fields = ( 'site', 'cluster', 'device', 'tenant', 'platform', 'status', 'role', 'vcpus', 'memory', 'disk', - ] + ) class Meta: ordering = ('_name', 'pk') # Name may be non-unique diff --git a/netbox/wireless/models.py b/netbox/wireless/models.py index dd3945d50..0540e9c45 100644 --- a/netbox/wireless/models.py +++ b/netbox/wireless/models.py @@ -113,6 +113,8 @@ class WirelessLAN(WirelessAuthenticationBase, NetBoxModel): blank=True ) + clone_fields = ('ssid', 'group', 'tenant', 'description') + class Meta: ordering = ('ssid', 'pk') verbose_name = 'Wireless LAN' From 5f15f550c9fdf3603b5fd17816c929434f42b78e Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Thu, 11 Aug 2022 11:37:07 -0400 Subject: [PATCH 372/593] Restore model documentation for all models under extras --- docs/customization/custom-fields.md | 109 ------------------- docs/customization/custom-links.md | 66 ------------ docs/customization/export-templates.md | 81 -------------- docs/integrations/webhooks.md | 139 ------------------------- docs/models/extras/customfield.md | 109 +++++++++++++++++++ docs/models/extras/customlink.md | 66 ++++++++++++ docs/models/extras/exporttemplate.md | 81 ++++++++++++++ docs/models/extras/imageattachment.md | 3 + docs/models/extras/webhook.md | 139 +++++++++++++++++++++++++ mkdocs.yml | 5 + 10 files changed, 403 insertions(+), 395 deletions(-) create mode 100644 docs/models/extras/customfield.md create mode 100644 docs/models/extras/customlink.md create mode 100644 docs/models/extras/exporttemplate.md create mode 100644 docs/models/extras/imageattachment.md create mode 100644 docs/models/extras/webhook.md diff --git a/docs/customization/custom-fields.md b/docs/customization/custom-fields.md index bfe412edc..e69de29bb 100644 --- a/docs/customization/custom-fields.md +++ b/docs/customization/custom-fields.md @@ -1,109 +0,0 @@ -# Custom Fields - -Each model in NetBox is represented in the database as a discrete table, and each attribute of a model exists as a column within its table. For example, sites are stored in the `dcim_site` table, which has columns named `name`, `facility`, `physical_address`, and so on. As new attributes are added to objects throughout the development of NetBox, tables are expanded to include new rows. - -However, some users might want to store additional object attributes that are somewhat esoteric in nature, and that would not make sense to include in the core NetBox database schema. For instance, suppose your organization needs to associate each device with a ticket number correlating it with an internal support system record. This is certainly a legitimate use for NetBox, but it's not a common enough need to warrant including a field for _every_ NetBox installation. Instead, you can create a custom field to hold this data. - -Within the database, custom fields are stored as JSON data directly alongside each object. This alleviates the need for complex queries when retrieving objects. - -## Creating Custom Fields - -Custom fields may be created by navigating to Customization > Custom Fields. NetBox supports six types of custom field: - -* Text: Free-form text (intended for single-line use) -* Long text: Free-form of any length; supports Markdown rendering -* Integer: A whole number (positive or negative) -* Boolean: True or false -* Date: A date in ISO 8601 format (YYYY-MM-DD) -* URL: This will be presented as a link in the web UI -* JSON: Arbitrary data stored in JSON format -* Selection: A selection of one of several pre-defined custom choices -* Multiple selection: A selection field which supports the assignment of multiple values -* Object: A single NetBox object of the type defined by `object_type` -* Multiple object: One or more NetBox objects of the type defined by `object_type` - -Each custom field must have a name. This should be a simple database-friendly string (e.g. `tps_report`) and may contain only alphanumeric characters and underscores. You may also assign a corresponding human-friendly label (e.g. "TPS report"); the label will be displayed on web forms. A weight is also required: Higher-weight fields will be ordered lower within a form. (The default weight is 100.) If a description is provided, it will appear beneath the field in a form. - -Marking a field as required will force the user to provide a value for the field when creating a new object or when saving an existing object. A default value for the field may also be provided. Use "true" or "false" for boolean fields, or the exact value of a choice for selection fields. - -A custom field must be assigned to one or more object types, or models, in NetBox. Once created, custom fields will automatically appear as part of these models in the web UI and REST API. Note that not all models support custom fields. - -### Filtering - -The filter logic controls how values are matched when filtering objects by the custom field. Loose filtering (the default) matches on a partial value, whereas exact matching requires a complete match of the given string to a field's value. For example, exact filtering with the string "red" will only match the exact value "red", whereas loose filtering will match on the values "red", "red-orange", or "bored". Setting the filter logic to "disabled" disables filtering by the field entirely. - -### Grouping - -!!! note - This feature was introduced in NetBox v3.3. - -Related custom fields can be grouped together within the UI by assigning each the same group name. When at least one custom field for an object type has a group defined, it will appear under the group heading within the custom fields panel under the object view. All custom fields with the same group name will appear under that heading. (Note that the group names must match exactly, or each will appear as a separate heading.) - -This parameter has no effect on the API representation of custom field data. - -### Visibility - -!!! note - This feature was introduced in NetBox v3.3. - -When creating a custom field, there are three options for UI visibility. These control how and whether the custom field is displayed within the NetBox UI. - -* **Read/write** (default): The custom field is included when viewing and editing objects. -* **Read-only**: The custom field is displayed when viewing an object, but it cannot be edited via the UI. (It will appear in the form as a read-only field.) -* **Hidden**: The custom field will never be displayed within the UI. This option is recommended for fields which are not intended for use by human users. - -Note that this setting has no impact on the REST or GraphQL APIs: Custom field data will always be available via either API. - -### Validation - -NetBox supports limited custom validation for custom field values. Following are the types of validation enforced for each field type: - -* Text: Regular expression (optional) -* Integer: Minimum and/or maximum value (optional) -* Selection: Must exactly match one of the prescribed choices - -### Custom Selection Fields - -Each custom selection field must have at least two choices. These are specified as a comma-separated list. Choices appear in forms in the order they are listed. Note that choice values are saved exactly as they appear, so it's best to avoid superfluous punctuation or symbols where possible. - -If a default value is specified for a selection field, it must exactly match one of the provided choices. The value of a multiple selection field will always return a list, even if only one value is selected. - -### Custom Object Fields - -An object or multi-object custom field can be used to refer to a particular NetBox object or objects as the "value" for a custom field. These custom fields must define an `object_type`, which determines the type of object to which custom field instances point. - - -## Custom Fields in Templates - -Several features within NetBox, such as export templates and webhooks, utilize Jinja2 templating. For convenience, objects which support custom field assignment expose custom field data through the `cf` property. This is a bit cleaner than accessing custom field data through the actual field (`custom_field_data`). - -For example, a custom field named `foo123` on the Site model is accessible on an instance as `{{ site.cf.foo123 }}`. - -## Custom Fields and the REST API - -When retrieving an object via the REST API, all of its custom data will be included within the `custom_fields` attribute. For example, below is the partial output of a site with two custom fields defined: - -```json -{ - "id": 123, - "url": "http://localhost:8000/api/dcim/sites/123/", - "name": "Raleigh 42", - ... - "custom_fields": { - "deployed": "2018-06-19", - "site_code": "US-NC-RAL42" - }, - ... -``` - -To set or change these values, simply include nested JSON data. For example: - -```json -{ - "name": "New Site", - "slug": "new-site", - "custom_fields": { - "deployed": "2019-03-24" - } -} -``` diff --git a/docs/customization/custom-links.md b/docs/customization/custom-links.md index 16ba9d2af..e69de29bb 100644 --- a/docs/customization/custom-links.md +++ b/docs/customization/custom-links.md @@ -1,66 +0,0 @@ -# Custom Links - -Custom links allow users to display arbitrary hyperlinks to external content within NetBox object views. These are helpful for cross-referencing related records in systems outside NetBox. For example, you might create a custom link on the device view which links to the current device in a Network Monitoring System (NMS). - -Custom links are created by navigating to Customization > Custom Links. Each link is associated with a particular NetBox object type (site, device, prefix, etc.) and will be displayed on relevant views. Each link has display text and a URL, and data from the NetBox item being viewed can be included in the link using [Jinja2 template code](https://jinja2docs.readthedocs.io/en/stable/) through the variable `obj`, and custom fields through `obj.cf`. - -For example, you might define a link like this: - -* Text: `View NMS` -* URL: `https://nms.example.com/nodes/?name={{ obj.name }}` - -When viewing a device named Router4, this link would render as: - -```no-highlight -View NMS -``` - -Custom links appear as buttons in the top right corner of the page. Numeric weighting can be used to influence the ordering of links, and each link can be enabled or disabled individually. - -!!! warning - Custom links rely on user-created code to generate arbitrary HTML output, which may be dangerous. Only grant permission to create or modify custom links to trusted users. - -## Context Data - -The following context data is available within the template when rendering a custom link's text or URL. - -| Variable | Description | -|-----------|-------------------------------------------------------------------------------------------------------------------| -| `object` | The NetBox object being displayed | -| `obj` | Same as `object`; maintained for backward compatability until NetBox v3.5 | -| `debug` | A boolean indicating whether debugging is enabled | -| `request` | The current WSGI request | -| `user` | The current user (if authenticated) | -| `perms` | The [permissions](https://docs.djangoproject.com/en/stable/topics/auth/default/#permissions) assigned to the user | - -While most of the context variables listed above will have consistent attributes, the object will be an instance of the specific object being viewed when the link is rendered. Different models have different fields and properties, so you may need to some research to determine the attributes available for use within your template for a specific object type. - -Checking the REST API representation of an object is generally a convenient way to determine what attributes are available. You can also reference the NetBox source code directly for a comprehensive list. - -## Conditional Rendering - -Only links which render with non-empty text are included on the page. You can employ conditional Jinja2 logic to control the conditions under which a link gets rendered. - -For example, if you only want to display a link for active devices, you could set the link text to - -```jinja2 -{% if obj.status == 'active' %}View NMS{% endif %} -``` - -The link will not appear when viewing a device with any status other than "active." - -As another example, if you wanted to show only devices belonging to a certain manufacturer, you could do something like this: - -```jinja2 -{% if obj.device_type.manufacturer.name == 'Cisco' %}View NMS{% endif %} -``` - -The link will only appear when viewing a device with a manufacturer name of "Cisco." - -## Link Groups - -Group names can be specified to organize links into groups. Links with the same group name will render as a dropdown menu beneath a single button bearing the name of the group. - -## Table Columns - -Custom links can also be included in object tables by selecting the desired links from the table configuration form. When displayed, each link will render as a hyperlink for its corresponding object. When exported (e.g. as CSV data), each link render only its URL. diff --git a/docs/customization/export-templates.md b/docs/customization/export-templates.md index 640a97531..e69de29bb 100644 --- a/docs/customization/export-templates.md +++ b/docs/customization/export-templates.md @@ -1,81 +0,0 @@ -# Export Templates - -NetBox allows users to define custom templates that can be used when exporting objects. To create an export template, navigate to Customization > Export Templates. - -Each export template is associated with a certain type of object. For instance, if you create an export template for VLANs, your custom template will appear under the "Export" button on the VLANs list. Each export template must have a name, and may optionally designate a specific export [MIME type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types) and/or file extension. - -Export templates must be written in [Jinja2](https://jinja.palletsprojects.com/). - -!!! note - The name `table` is reserved for internal use. - -!!! warning - Export templates are rendered using user-submitted code, which may pose security risks under certain conditions. Only grant permission to create or modify export templates to trusted users. - -The list of objects returned from the database when rendering an export template is stored in the `queryset` variable, which you'll typically want to iterate through using a `for` loop. Object properties can be access by name. For example: - -```jinja2 -{% for rack in queryset %} -Rack: {{ rack.name }} -Site: {{ rack.site.name }} -Height: {{ rack.u_height }}U -{% endfor %} -``` - -To access custom fields of an object within a template, use the `cf` attribute. For example, `{{ obj.cf.color }}` will return the value (if any) for a custom field named `color` on `obj`. - -If you need to use the config context data in an export template, you'll should use the function `get_config_context` to get all the config context data. For example: -``` -{% for server in queryset %} -{% set data = server.get_config_context() %} -{{ data.syslog }} -{% endfor %} -``` - -The `as_attachment` attribute of an export template controls its behavior when rendered. If true, the rendered content will be returned to the user as a downloadable file. If false, it will be displayed within the browser. (This may be handy e.g. for generating HTML content.) - -A MIME type and file extension can optionally be defined for each export template. The default MIME type is `text/plain`. - - -## REST API Integration - -When it is necessary to provide authentication credentials (such as when [`LOGIN_REQUIRED`](../configuration/security.md#login_required) has been enabled), it is recommended to render export templates via the REST API. This allows the client to specify an authentication token. To render an export template via the REST API, make a `GET` request to the model's list endpoint and append the `export` parameter specifying the export template name. For example: - -``` -GET /api/dcim/sites/?export=MyTemplateName -``` - -Note that the body of the response will contain only the rendered export template content, as opposed to a JSON object or list. - -## Example - -Here's an example device export template that will generate a simple Nagios configuration from a list of devices. - -``` -{% for device in queryset %}{% if device.status and device.primary_ip %}define host{ - use generic-switch - host_name {{ device.name }} - address {{ device.primary_ip.address.ip }} -} -{% endif %}{% endfor %} -``` - -The generated output will look something like this: - -``` -define host{ - use generic-switch - host_name switch1 - address 192.0.2.1 -} -define host{ - use generic-switch - host_name switch2 - address 192.0.2.2 -} -define host{ - use generic-switch - host_name switch3 - address 192.0.2.3 -} -``` diff --git a/docs/integrations/webhooks.md b/docs/integrations/webhooks.md index 4705243d1..e69de29bb 100644 --- a/docs/integrations/webhooks.md +++ b/docs/integrations/webhooks.md @@ -1,139 +0,0 @@ -# Webhooks - -A webhook is a mechanism for conveying to some external system a change that took place in NetBox. For example, you may want to notify a monitoring system whenever the status of a device is updated in NetBox. This can be done by creating a webhook for the device model in NetBox and identifying the webhook receiver. When NetBox detects a change to a device, an HTTP request containing the details of the change and who made it be sent to the specified receiver. Webhooks are managed under Logging > Webhooks. - -!!! warning - Webhooks support the inclusion of user-submitted code to generate URL, custom headers and payloads, which may pose security risks under certain conditions. Only grant permission to create or modify webhooks to trusted users. - -## Configuration - -* **Name** - A unique name for the webhook. The name is not included with outbound messages. -* **Object type(s)** - The type or types of NetBox object that will trigger the webhook. -* **Enabled** - If unchecked, the webhook will be inactive. -* **Events** - A webhook may trigger on any combination of create, update, and delete events. At least one event type must be selected. -* **HTTP method** - The type of HTTP request to send. Options include `GET`, `POST`, `PUT`, `PATCH`, and `DELETE`. -* **URL** - The fully-qualified URL of the request to be sent. This may specify a destination port number if needed. Jinja2 templating is supported for this field. -* **HTTP content type** - The value of the request's `Content-Type` header. (Defaults to `application/json`) -* **Additional headers** - Any additional headers to include with the request (optional). Add one header per line in the format `Name: Value`. Jinja2 templating is supported for this field (see below). -* **Body template** - The content of the request being sent (optional). Jinja2 templating is supported for this field (see below). If blank, NetBox will populate the request body with a raw dump of the webhook context. (If the HTTP cotent type is set to `application/json`, this will be formatted as a JSON object.) -* **Secret** - A secret string used to prove authenticity of the request (optional). This will append a `X-Hook-Signature` header to the request, consisting of a HMAC (SHA-512) hex digest of the request body using the secret as the key. -* **Conditions** - An optional set of conditions evaluated to determine whether the webhook fires for a given object. -* **SSL verification** - Uncheck this option to disable validation of the receiver's SSL certificate. (Disable with caution!) -* **CA file path** - The file path to a particular certificate authority (CA) file to use when validating the receiver's SSL certificate (optional). - -## Jinja2 Template Support - -[Jinja2 templating](https://jinja.palletsprojects.com/) is supported for the `URL`, `additional_headers` and `body_template` fields. This enables the user to convey object data in the request headers as well as to craft a customized request body. Request content can be crafted to enable the direct interaction with external systems by ensuring the outgoing message is in a format the receiver expects and understands. - -For example, you might create a NetBox webhook to [trigger a Slack message](https://api.slack.com/messaging/webhooks) any time an IP address is created. You can accomplish this using the following configuration: - -* Object type: IPAM > IP address -* HTTP method: `POST` -* URL: Slack incoming webhook URL -* HTTP content type: `application/json` -* Body template: `{"text": "IP address {{ data['address'] }} was created by {{ username }}!"}` - -### Available Context - -The following data is available as context for Jinja2 templates: - -* `event` - The type of event which triggered the webhook: created, updated, or deleted. -* `model` - The NetBox model which triggered the change. -* `timestamp` - The time at which the event occurred (in [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) format). -* `username` - The name of the user account associated with the change. -* `request_id` - The unique request ID. This may be used to correlate multiple changes associated with a single request. -* `data` - A detailed representation of the object in its current state. This is typically equivalent to the model's representation in NetBox's REST API. -* `snapshots` - Minimal "snapshots" of the object state both before and after the change was made; provided as a dictionary with keys named `prechange` and `postchange`. These are not as extensive as the fully serialized representation, but contain enough information to convey what has changed. - -### Default Request Body - -If no body template is specified, the request body will be populated with a JSON object containing the context data. For example, a newly created site might appear as follows: - -```json -{ - "event": "created", - "timestamp": "2021-03-09 17:55:33.968016+00:00", - "model": "site", - "username": "jstretch", - "request_id": "fdbca812-3142-4783-b364-2e2bd5c16c6a", - "data": { - "id": 19, - "name": "Site 1", - "slug": "site-1", - "status": - "value": "active", - "label": "Active", - "id": 1 - }, - "region": null, - ... - }, - "snapshots": { - "prechange": null, - "postchange": { - "created": "2021-03-09", - "last_updated": "2021-03-09T17:55:33.851Z", - "name": "Site 1", - "slug": "site-1", - "status": "active", - ... - } - } -} -``` - -## Conditional Webhooks - -A webhook may include a set of conditional logic expressed in JSON used to control whether a webhook triggers for a specific object. For example, you may wish to trigger a webhook for devices only when the `status` field of an object is "active": - -```json -{ - "and": [ - { - "attr": "status.value", - "value": "active" - } - ] -} -``` - -For more detail, see the reference documentation for NetBox's [conditional logic](../reference/conditions.md). - -## Webhook Processing - -When a change is detected, any resulting webhooks are placed into a Redis queue for processing. This allows the user's request to complete without needing to wait for the outgoing webhook(s) to be processed. The webhooks are then extracted from the queue by the `rqworker` process and HTTP requests are sent to their respective destinations. The current webhook queue and any failed webhooks can be inspected in the admin UI under System > Background Tasks. - -A request is considered successful if the response has a 2XX status code; otherwise, the request is marked as having failed. Failed requests may be retried manually via the admin UI. - -## Troubleshooting - -To assist with verifying that the content of outgoing webhooks is rendered correctly, NetBox provides a simple HTTP listener that can be run locally to receive and display webhook requests. First, modify the target URL of the desired webhook to `http://localhost:9000/`. This will instruct NetBox to send the request to the local server on TCP port 9000. Then, start the webhook receiver service from the NetBox root directory: - -```no-highlight -$ python netbox/manage.py webhook_receiver -Listening on port http://localhost:9000. Stop with CONTROL-C. -``` - -You can test the receiver itself by sending any HTTP request to it. For example: - -```no-highlight -$ curl -X POST http://localhost:9000 --data '{"foo": "bar"}' -``` - -The server will print output similar to the following: - -```no-highlight -[1] Tue, 07 Apr 2020 17:44:02 GMT 127.0.0.1 "POST / HTTP/1.1" 200 - -Host: localhost:9000 -User-Agent: curl/7.58.0 -Accept: */* -Content-Length: 14 -Content-Type: application/x-www-form-urlencoded - -{"foo": "bar"} ------------- -``` - -Note that `webhook_receiver` does not actually _do_ anything with the information received: It merely prints the request headers and body for inspection. - -Now, when the NetBox webhook is triggered and processed, you should see its headers and content appear in the terminal where the webhook receiver is listening. If you don't, check that the `rqworker` process is running and that webhook events are being placed into the queue (visible under the NetBox admin UI). diff --git a/docs/models/extras/customfield.md b/docs/models/extras/customfield.md new file mode 100644 index 000000000..bfe412edc --- /dev/null +++ b/docs/models/extras/customfield.md @@ -0,0 +1,109 @@ +# Custom Fields + +Each model in NetBox is represented in the database as a discrete table, and each attribute of a model exists as a column within its table. For example, sites are stored in the `dcim_site` table, which has columns named `name`, `facility`, `physical_address`, and so on. As new attributes are added to objects throughout the development of NetBox, tables are expanded to include new rows. + +However, some users might want to store additional object attributes that are somewhat esoteric in nature, and that would not make sense to include in the core NetBox database schema. For instance, suppose your organization needs to associate each device with a ticket number correlating it with an internal support system record. This is certainly a legitimate use for NetBox, but it's not a common enough need to warrant including a field for _every_ NetBox installation. Instead, you can create a custom field to hold this data. + +Within the database, custom fields are stored as JSON data directly alongside each object. This alleviates the need for complex queries when retrieving objects. + +## Creating Custom Fields + +Custom fields may be created by navigating to Customization > Custom Fields. NetBox supports six types of custom field: + +* Text: Free-form text (intended for single-line use) +* Long text: Free-form of any length; supports Markdown rendering +* Integer: A whole number (positive or negative) +* Boolean: True or false +* Date: A date in ISO 8601 format (YYYY-MM-DD) +* URL: This will be presented as a link in the web UI +* JSON: Arbitrary data stored in JSON format +* Selection: A selection of one of several pre-defined custom choices +* Multiple selection: A selection field which supports the assignment of multiple values +* Object: A single NetBox object of the type defined by `object_type` +* Multiple object: One or more NetBox objects of the type defined by `object_type` + +Each custom field must have a name. This should be a simple database-friendly string (e.g. `tps_report`) and may contain only alphanumeric characters and underscores. You may also assign a corresponding human-friendly label (e.g. "TPS report"); the label will be displayed on web forms. A weight is also required: Higher-weight fields will be ordered lower within a form. (The default weight is 100.) If a description is provided, it will appear beneath the field in a form. + +Marking a field as required will force the user to provide a value for the field when creating a new object or when saving an existing object. A default value for the field may also be provided. Use "true" or "false" for boolean fields, or the exact value of a choice for selection fields. + +A custom field must be assigned to one or more object types, or models, in NetBox. Once created, custom fields will automatically appear as part of these models in the web UI and REST API. Note that not all models support custom fields. + +### Filtering + +The filter logic controls how values are matched when filtering objects by the custom field. Loose filtering (the default) matches on a partial value, whereas exact matching requires a complete match of the given string to a field's value. For example, exact filtering with the string "red" will only match the exact value "red", whereas loose filtering will match on the values "red", "red-orange", or "bored". Setting the filter logic to "disabled" disables filtering by the field entirely. + +### Grouping + +!!! note + This feature was introduced in NetBox v3.3. + +Related custom fields can be grouped together within the UI by assigning each the same group name. When at least one custom field for an object type has a group defined, it will appear under the group heading within the custom fields panel under the object view. All custom fields with the same group name will appear under that heading. (Note that the group names must match exactly, or each will appear as a separate heading.) + +This parameter has no effect on the API representation of custom field data. + +### Visibility + +!!! note + This feature was introduced in NetBox v3.3. + +When creating a custom field, there are three options for UI visibility. These control how and whether the custom field is displayed within the NetBox UI. + +* **Read/write** (default): The custom field is included when viewing and editing objects. +* **Read-only**: The custom field is displayed when viewing an object, but it cannot be edited via the UI. (It will appear in the form as a read-only field.) +* **Hidden**: The custom field will never be displayed within the UI. This option is recommended for fields which are not intended for use by human users. + +Note that this setting has no impact on the REST or GraphQL APIs: Custom field data will always be available via either API. + +### Validation + +NetBox supports limited custom validation for custom field values. Following are the types of validation enforced for each field type: + +* Text: Regular expression (optional) +* Integer: Minimum and/or maximum value (optional) +* Selection: Must exactly match one of the prescribed choices + +### Custom Selection Fields + +Each custom selection field must have at least two choices. These are specified as a comma-separated list. Choices appear in forms in the order they are listed. Note that choice values are saved exactly as they appear, so it's best to avoid superfluous punctuation or symbols where possible. + +If a default value is specified for a selection field, it must exactly match one of the provided choices. The value of a multiple selection field will always return a list, even if only one value is selected. + +### Custom Object Fields + +An object or multi-object custom field can be used to refer to a particular NetBox object or objects as the "value" for a custom field. These custom fields must define an `object_type`, which determines the type of object to which custom field instances point. + + +## Custom Fields in Templates + +Several features within NetBox, such as export templates and webhooks, utilize Jinja2 templating. For convenience, objects which support custom field assignment expose custom field data through the `cf` property. This is a bit cleaner than accessing custom field data through the actual field (`custom_field_data`). + +For example, a custom field named `foo123` on the Site model is accessible on an instance as `{{ site.cf.foo123 }}`. + +## Custom Fields and the REST API + +When retrieving an object via the REST API, all of its custom data will be included within the `custom_fields` attribute. For example, below is the partial output of a site with two custom fields defined: + +```json +{ + "id": 123, + "url": "http://localhost:8000/api/dcim/sites/123/", + "name": "Raleigh 42", + ... + "custom_fields": { + "deployed": "2018-06-19", + "site_code": "US-NC-RAL42" + }, + ... +``` + +To set or change these values, simply include nested JSON data. For example: + +```json +{ + "name": "New Site", + "slug": "new-site", + "custom_fields": { + "deployed": "2019-03-24" + } +} +``` diff --git a/docs/models/extras/customlink.md b/docs/models/extras/customlink.md new file mode 100644 index 000000000..16ba9d2af --- /dev/null +++ b/docs/models/extras/customlink.md @@ -0,0 +1,66 @@ +# Custom Links + +Custom links allow users to display arbitrary hyperlinks to external content within NetBox object views. These are helpful for cross-referencing related records in systems outside NetBox. For example, you might create a custom link on the device view which links to the current device in a Network Monitoring System (NMS). + +Custom links are created by navigating to Customization > Custom Links. Each link is associated with a particular NetBox object type (site, device, prefix, etc.) and will be displayed on relevant views. Each link has display text and a URL, and data from the NetBox item being viewed can be included in the link using [Jinja2 template code](https://jinja2docs.readthedocs.io/en/stable/) through the variable `obj`, and custom fields through `obj.cf`. + +For example, you might define a link like this: + +* Text: `View NMS` +* URL: `https://nms.example.com/nodes/?name={{ obj.name }}` + +When viewing a device named Router4, this link would render as: + +```no-highlight +View NMS +``` + +Custom links appear as buttons in the top right corner of the page. Numeric weighting can be used to influence the ordering of links, and each link can be enabled or disabled individually. + +!!! warning + Custom links rely on user-created code to generate arbitrary HTML output, which may be dangerous. Only grant permission to create or modify custom links to trusted users. + +## Context Data + +The following context data is available within the template when rendering a custom link's text or URL. + +| Variable | Description | +|-----------|-------------------------------------------------------------------------------------------------------------------| +| `object` | The NetBox object being displayed | +| `obj` | Same as `object`; maintained for backward compatability until NetBox v3.5 | +| `debug` | A boolean indicating whether debugging is enabled | +| `request` | The current WSGI request | +| `user` | The current user (if authenticated) | +| `perms` | The [permissions](https://docs.djangoproject.com/en/stable/topics/auth/default/#permissions) assigned to the user | + +While most of the context variables listed above will have consistent attributes, the object will be an instance of the specific object being viewed when the link is rendered. Different models have different fields and properties, so you may need to some research to determine the attributes available for use within your template for a specific object type. + +Checking the REST API representation of an object is generally a convenient way to determine what attributes are available. You can also reference the NetBox source code directly for a comprehensive list. + +## Conditional Rendering + +Only links which render with non-empty text are included on the page. You can employ conditional Jinja2 logic to control the conditions under which a link gets rendered. + +For example, if you only want to display a link for active devices, you could set the link text to + +```jinja2 +{% if obj.status == 'active' %}View NMS{% endif %} +``` + +The link will not appear when viewing a device with any status other than "active." + +As another example, if you wanted to show only devices belonging to a certain manufacturer, you could do something like this: + +```jinja2 +{% if obj.device_type.manufacturer.name == 'Cisco' %}View NMS{% endif %} +``` + +The link will only appear when viewing a device with a manufacturer name of "Cisco." + +## Link Groups + +Group names can be specified to organize links into groups. Links with the same group name will render as a dropdown menu beneath a single button bearing the name of the group. + +## Table Columns + +Custom links can also be included in object tables by selecting the desired links from the table configuration form. When displayed, each link will render as a hyperlink for its corresponding object. When exported (e.g. as CSV data), each link render only its URL. diff --git a/docs/models/extras/exporttemplate.md b/docs/models/extras/exporttemplate.md new file mode 100644 index 000000000..b4b69108a --- /dev/null +++ b/docs/models/extras/exporttemplate.md @@ -0,0 +1,81 @@ +# Export Templates + +NetBox allows users to define custom templates that can be used when exporting objects. To create an export template, navigate to Customization > Export Templates. + +Each export template is associated with a certain type of object. For instance, if you create an export template for VLANs, your custom template will appear under the "Export" button on the VLANs list. Each export template must have a name, and may optionally designate a specific export [MIME type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types) and/or file extension. + +Export templates must be written in [Jinja2](https://jinja.palletsprojects.com/). + +!!! note + The name `table` is reserved for internal use. + +!!! warning + Export templates are rendered using user-submitted code, which may pose security risks under certain conditions. Only grant permission to create or modify export templates to trusted users. + +The list of objects returned from the database when rendering an export template is stored in the `queryset` variable, which you'll typically want to iterate through using a `for` loop. Object properties can be access by name. For example: + +```jinja2 +{% for rack in queryset %} +Rack: {{ rack.name }} +Site: {{ rack.site.name }} +Height: {{ rack.u_height }}U +{% endfor %} +``` + +To access custom fields of an object within a template, use the `cf` attribute. For example, `{{ obj.cf.color }}` will return the value (if any) for a custom field named `color` on `obj`. + +If you need to use the config context data in an export template, you'll should use the function `get_config_context` to get all the config context data. For example: +``` +{% for server in queryset %} +{% set data = server.get_config_context() %} +{{ data.syslog }} +{% endfor %} +``` + +The `as_attachment` attribute of an export template controls its behavior when rendered. If true, the rendered content will be returned to the user as a downloadable file. If false, it will be displayed within the browser. (This may be handy e.g. for generating HTML content.) + +A MIME type and file extension can optionally be defined for each export template. The default MIME type is `text/plain`. + + +## REST API Integration + +When it is necessary to provide authentication credentials (such as when [`LOGIN_REQUIRED`](../../configuration/security.md#login_required) has been enabled), it is recommended to render export templates via the REST API. This allows the client to specify an authentication token. To render an export template via the REST API, make a `GET` request to the model's list endpoint and append the `export` parameter specifying the export template name. For example: + +``` +GET /api/dcim/sites/?export=MyTemplateName +``` + +Note that the body of the response will contain only the rendered export template content, as opposed to a JSON object or list. + +## Example + +Here's an example device export template that will generate a simple Nagios configuration from a list of devices. + +``` +{% for device in queryset %}{% if device.status and device.primary_ip %}define host{ + use generic-switch + host_name {{ device.name }} + address {{ device.primary_ip.address.ip }} +} +{% endif %}{% endfor %} +``` + +The generated output will look something like this: + +``` +define host{ + use generic-switch + host_name switch1 + address 192.0.2.1 +} +define host{ + use generic-switch + host_name switch2 + address 192.0.2.2 +} +define host{ + use generic-switch + host_name switch3 + address 192.0.2.3 +} +``` diff --git a/docs/models/extras/imageattachment.md b/docs/models/extras/imageattachment.md new file mode 100644 index 000000000..da15462ab --- /dev/null +++ b/docs/models/extras/imageattachment.md @@ -0,0 +1,3 @@ +# Image Attachments + +Certain objects in NetBox support the attachment of uploaded images. These will be saved to the NetBox server and made available whenever the object is viewed. diff --git a/docs/models/extras/webhook.md b/docs/models/extras/webhook.md new file mode 100644 index 000000000..427ae3dd1 --- /dev/null +++ b/docs/models/extras/webhook.md @@ -0,0 +1,139 @@ +# Webhooks + +A webhook is a mechanism for conveying to some external system a change that took place in NetBox. For example, you may want to notify a monitoring system whenever the status of a device is updated in NetBox. This can be done by creating a webhook for the device model in NetBox and identifying the webhook receiver. When NetBox detects a change to a device, an HTTP request containing the details of the change and who made it be sent to the specified receiver. Webhooks are managed under Logging > Webhooks. + +!!! warning + Webhooks support the inclusion of user-submitted code to generate URL, custom headers and payloads, which may pose security risks under certain conditions. Only grant permission to create or modify webhooks to trusted users. + +## Configuration + +* **Name** - A unique name for the webhook. The name is not included with outbound messages. +* **Object type(s)** - The type or types of NetBox object that will trigger the webhook. +* **Enabled** - If unchecked, the webhook will be inactive. +* **Events** - A webhook may trigger on any combination of create, update, and delete events. At least one event type must be selected. +* **HTTP method** - The type of HTTP request to send. Options include `GET`, `POST`, `PUT`, `PATCH`, and `DELETE`. +* **URL** - The fully-qualified URL of the request to be sent. This may specify a destination port number if needed. Jinja2 templating is supported for this field. +* **HTTP content type** - The value of the request's `Content-Type` header. (Defaults to `application/json`) +* **Additional headers** - Any additional headers to include with the request (optional). Add one header per line in the format `Name: Value`. Jinja2 templating is supported for this field (see below). +* **Body template** - The content of the request being sent (optional). Jinja2 templating is supported for this field (see below). If blank, NetBox will populate the request body with a raw dump of the webhook context. (If the HTTP cotent type is set to `application/json`, this will be formatted as a JSON object.) +* **Secret** - A secret string used to prove authenticity of the request (optional). This will append a `X-Hook-Signature` header to the request, consisting of a HMAC (SHA-512) hex digest of the request body using the secret as the key. +* **Conditions** - An optional set of conditions evaluated to determine whether the webhook fires for a given object. +* **SSL verification** - Uncheck this option to disable validation of the receiver's SSL certificate. (Disable with caution!) +* **CA file path** - The file path to a particular certificate authority (CA) file to use when validating the receiver's SSL certificate (optional). + +## Jinja2 Template Support + +[Jinja2 templating](https://jinja.palletsprojects.com/) is supported for the `URL`, `additional_headers` and `body_template` fields. This enables the user to convey object data in the request headers as well as to craft a customized request body. Request content can be crafted to enable the direct interaction with external systems by ensuring the outgoing message is in a format the receiver expects and understands. + +For example, you might create a NetBox webhook to [trigger a Slack message](https://api.slack.com/messaging/webhooks) any time an IP address is created. You can accomplish this using the following configuration: + +* Object type: IPAM > IP address +* HTTP method: `POST` +* URL: Slack incoming webhook URL +* HTTP content type: `application/json` +* Body template: `{"text": "IP address {{ data['address'] }} was created by {{ username }}!"}` + +### Available Context + +The following data is available as context for Jinja2 templates: + +* `event` - The type of event which triggered the webhook: created, updated, or deleted. +* `model` - The NetBox model which triggered the change. +* `timestamp` - The time at which the event occurred (in [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) format). +* `username` - The name of the user account associated with the change. +* `request_id` - The unique request ID. This may be used to correlate multiple changes associated with a single request. +* `data` - A detailed representation of the object in its current state. This is typically equivalent to the model's representation in NetBox's REST API. +* `snapshots` - Minimal "snapshots" of the object state both before and after the change was made; provided as a dictionary with keys named `prechange` and `postchange`. These are not as extensive as the fully serialized representation, but contain enough information to convey what has changed. + +### Default Request Body + +If no body template is specified, the request body will be populated with a JSON object containing the context data. For example, a newly created site might appear as follows: + +```json +{ + "event": "created", + "timestamp": "2021-03-09 17:55:33.968016+00:00", + "model": "site", + "username": "jstretch", + "request_id": "fdbca812-3142-4783-b364-2e2bd5c16c6a", + "data": { + "id": 19, + "name": "Site 1", + "slug": "site-1", + "status": + "value": "active", + "label": "Active", + "id": 1 + }, + "region": null, + ... + }, + "snapshots": { + "prechange": null, + "postchange": { + "created": "2021-03-09", + "last_updated": "2021-03-09T17:55:33.851Z", + "name": "Site 1", + "slug": "site-1", + "status": "active", + ... + } + } +} +``` + +## Conditional Webhooks + +A webhook may include a set of conditional logic expressed in JSON used to control whether a webhook triggers for a specific object. For example, you may wish to trigger a webhook for devices only when the `status` field of an object is "active": + +```json +{ + "and": [ + { + "attr": "status.value", + "value": "active" + } + ] +} +``` + +For more detail, see the reference documentation for NetBox's [conditional logic](../../reference/conditions.md). + +## Webhook Processing + +When a change is detected, any resulting webhooks are placed into a Redis queue for processing. This allows the user's request to complete without needing to wait for the outgoing webhook(s) to be processed. The webhooks are then extracted from the queue by the `rqworker` process and HTTP requests are sent to their respective destinations. The current webhook queue and any failed webhooks can be inspected in the admin UI under System > Background Tasks. + +A request is considered successful if the response has a 2XX status code; otherwise, the request is marked as having failed. Failed requests may be retried manually via the admin UI. + +## Troubleshooting + +To assist with verifying that the content of outgoing webhooks is rendered correctly, NetBox provides a simple HTTP listener that can be run locally to receive and display webhook requests. First, modify the target URL of the desired webhook to `http://localhost:9000/`. This will instruct NetBox to send the request to the local server on TCP port 9000. Then, start the webhook receiver service from the NetBox root directory: + +```no-highlight +$ python netbox/manage.py webhook_receiver +Listening on port http://localhost:9000. Stop with CONTROL-C. +``` + +You can test the receiver itself by sending any HTTP request to it. For example: + +```no-highlight +$ curl -X POST http://localhost:9000 --data '{"foo": "bar"}' +``` + +The server will print output similar to the following: + +```no-highlight +[1] Tue, 07 Apr 2020 17:44:02 GMT 127.0.0.1 "POST / HTTP/1.1" 200 - +Host: localhost:9000 +User-Agent: curl/7.58.0 +Accept: */* +Content-Length: 14 +Content-Type: application/x-www-form-urlencoded + +{"foo": "bar"} +------------ +``` + +Note that `webhook_receiver` does not actually _do_ anything with the information received: It merely prints the request headers and body for inspection. + +Now, when the NetBox webhook is triggered and processed, you should see its headers and content appear in the terminal where the webhook receiver is listening. If you don't, check that the `rqworker` process is running and that webhook events are being placed into the queue (visible under the NetBox admin UI). diff --git a/mkdocs.yml b/mkdocs.yml index 95672cede..a5eb36d81 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -191,7 +191,12 @@ nav: - VirtualChassis: 'models/dcim/virtualchassis.md' - Extras: - ConfigContext: 'models/extras/configcontext.md' + - CustomField: 'models/extras/customfield.md' + - CustomLink: 'models/extras/customlink.md' + - ExportTemplate: 'models/extras/exporttemplate.md' + - ImageAttachment: 'models/extras/imageattachment.md' - Tag: 'models/extras/tag.md' + - Webhook: 'models/extras/webhook.md' - IPAM: - ASN: 'models/ipam/asn.md' - Aggregate: 'models/ipam/aggregate.md' From a7b78565a199c6c5d1459decdd0d0019a743e974 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Thu, 11 Aug 2022 11:55:45 -0400 Subject: [PATCH 373/593] Split webhooks documentation --- docs/integrations/webhooks.md | 127 ++++++++++++++++++++++++++++++++++ docs/models/extras/webhook.md | 122 +------------------------------- 2 files changed, 128 insertions(+), 121 deletions(-) diff --git a/docs/integrations/webhooks.md b/docs/integrations/webhooks.md index e69de29bb..3ef28fb11 100644 --- a/docs/integrations/webhooks.md +++ b/docs/integrations/webhooks.md @@ -0,0 +1,127 @@ +# Webhooks + +NetBox can be configured to transmit outgoing webhooks to remote systems in response to internal object changes. The receiver can act on the data in these webhook messages to perform related tasks. + +For example, suppose you want to automatically configure a monitoring system to start monitoring a device when its operational status is changed to active, and remove it from monitoring for any other status. You can create a webhook in NetBox for the device model and craft its content and destination URL to effect the desired change on the receiving system. Webhooks will be sent automatically by NetBox whenever the configured constraints are met. + +Each webhook must be associated with at least one NetBox object type and at least one event (create, update, or delete). Users can specify the receiver URL, HTTP request type (`GET`, `POST`, etc.), content type, and headers. A request body can also be specified; if left blank, this will default to a serialized representation of the affected object. + +!!! warning "Security Notice" + Webhooks support the inclusion of user-submitted code to generate the URL, custom headers, and payloads, which may pose security risks under certain conditions. Only grant permission to create or modify webhooks to trusted users. + +## Jinja2 Template Support + +[Jinja2 templating](https://jinja.palletsprojects.com/) is supported for the `URL`, `additional_headers` and `body_template` fields. This enables the user to convey object data in the request headers as well as to craft a customized request body. Request content can be crafted to enable the direct interaction with external systems by ensuring the outgoing message is in a format the receiver expects and understands. + +For example, you might create a NetBox webhook to [trigger a Slack message](https://api.slack.com/messaging/webhooks) any time an IP address is created. You can accomplish this using the following configuration: + +* Object type: IPAM > IP address +* HTTP method: `POST` +* URL: Slack incoming webhook URL +* HTTP content type: `application/json` +* Body template: `{"text": "IP address {{ data['address'] }} was created by {{ username }}!"}` + +### Available Context + +The following data is available as context for Jinja2 templates: + +* `event` - The type of event which triggered the webhook: created, updated, or deleted. +* `model` - The NetBox model which triggered the change. +* `timestamp` - The time at which the event occurred (in [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) format). +* `username` - The name of the user account associated with the change. +* `request_id` - The unique request ID. This may be used to correlate multiple changes associated with a single request. +* `data` - A detailed representation of the object in its current state. This is typically equivalent to the model's representation in NetBox's REST API. +* `snapshots` - Minimal "snapshots" of the object state both before and after the change was made; provided as a dictionary with keys named `prechange` and `postchange`. These are not as extensive as the fully serialized representation, but contain enough information to convey what has changed. + +### Default Request Body + +If no body template is specified, the request body will be populated with a JSON object containing the context data. For example, a newly created site might appear as follows: + +```json +{ + "event": "created", + "timestamp": "2021-03-09 17:55:33.968016+00:00", + "model": "site", + "username": "jstretch", + "request_id": "fdbca812-3142-4783-b364-2e2bd5c16c6a", + "data": { + "id": 19, + "name": "Site 1", + "slug": "site-1", + "status": + "value": "active", + "label": "Active", + "id": 1 + }, + "region": null, + ... + }, + "snapshots": { + "prechange": null, + "postchange": { + "created": "2021-03-09", + "last_updated": "2021-03-09T17:55:33.851Z", + "name": "Site 1", + "slug": "site-1", + "status": "active", + ... + } + } +} +``` + +## Conditional Webhooks + +A webhook may include a set of conditional logic expressed in JSON used to control whether a webhook triggers for a specific object. For example, you may wish to trigger a webhook for devices only when the `status` field of an object is "active": + +```json +{ + "and": [ + { + "attr": "status.value", + "value": "active" + } + ] +} +``` + +For more detail, see the reference documentation for NetBox's [conditional logic](../../reference/conditions.md). + +## Webhook Processing + +When a change is detected, any resulting webhooks are placed into a Redis queue for processing. This allows the user's request to complete without needing to wait for the outgoing webhook(s) to be processed. The webhooks are then extracted from the queue by the `rqworker` process and HTTP requests are sent to their respective destinations. The current webhook queue and any failed webhooks can be inspected in the admin UI under System > Background Tasks. + +A request is considered successful if the response has a 2XX status code; otherwise, the request is marked as having failed. Failed requests may be retried manually via the admin UI. + +## Troubleshooting + +To assist with verifying that the content of outgoing webhooks is rendered correctly, NetBox provides a simple HTTP listener that can be run locally to receive and display webhook requests. First, modify the target URL of the desired webhook to `http://localhost:9000/`. This will instruct NetBox to send the request to the local server on TCP port 9000. Then, start the webhook receiver service from the NetBox root directory: + +```no-highlight +$ python netbox/manage.py webhook_receiver +Listening on port http://localhost:9000. Stop with CONTROL-C. +``` + +You can test the receiver itself by sending any HTTP request to it. For example: + +```no-highlight +$ curl -X POST http://localhost:9000 --data '{"foo": "bar"}' +``` + +The server will print output similar to the following: + +```no-highlight +[1] Tue, 07 Apr 2020 17:44:02 GMT 127.0.0.1 "POST / HTTP/1.1" 200 - +Host: localhost:9000 +User-Agent: curl/7.58.0 +Accept: */* +Content-Length: 14 +Content-Type: application/x-www-form-urlencoded + +{"foo": "bar"} +------------ +``` + +Note that `webhook_receiver` does not actually _do_ anything with the information received: It merely prints the request headers and body for inspection. + +Now, when the NetBox webhook is triggered and processed, you should see its headers and content appear in the terminal where the webhook receiver is listening. If you don't, check that the `rqworker` process is running and that webhook events are being placed into the queue (visible under the NetBox admin UI). diff --git a/docs/models/extras/webhook.md b/docs/models/extras/webhook.md index 427ae3dd1..2d598f8ae 100644 --- a/docs/models/extras/webhook.md +++ b/docs/models/extras/webhook.md @@ -2,10 +2,7 @@ A webhook is a mechanism for conveying to some external system a change that took place in NetBox. For example, you may want to notify a monitoring system whenever the status of a device is updated in NetBox. This can be done by creating a webhook for the device model in NetBox and identifying the webhook receiver. When NetBox detects a change to a device, an HTTP request containing the details of the change and who made it be sent to the specified receiver. Webhooks are managed under Logging > Webhooks. -!!! warning - Webhooks support the inclusion of user-submitted code to generate URL, custom headers and payloads, which may pose security risks under certain conditions. Only grant permission to create or modify webhooks to trusted users. - -## Configuration +## Model Fields * **Name** - A unique name for the webhook. The name is not included with outbound messages. * **Object type(s)** - The type or types of NetBox object that will trigger the webhook. @@ -20,120 +17,3 @@ A webhook is a mechanism for conveying to some external system a change that too * **Conditions** - An optional set of conditions evaluated to determine whether the webhook fires for a given object. * **SSL verification** - Uncheck this option to disable validation of the receiver's SSL certificate. (Disable with caution!) * **CA file path** - The file path to a particular certificate authority (CA) file to use when validating the receiver's SSL certificate (optional). - -## Jinja2 Template Support - -[Jinja2 templating](https://jinja.palletsprojects.com/) is supported for the `URL`, `additional_headers` and `body_template` fields. This enables the user to convey object data in the request headers as well as to craft a customized request body. Request content can be crafted to enable the direct interaction with external systems by ensuring the outgoing message is in a format the receiver expects and understands. - -For example, you might create a NetBox webhook to [trigger a Slack message](https://api.slack.com/messaging/webhooks) any time an IP address is created. You can accomplish this using the following configuration: - -* Object type: IPAM > IP address -* HTTP method: `POST` -* URL: Slack incoming webhook URL -* HTTP content type: `application/json` -* Body template: `{"text": "IP address {{ data['address'] }} was created by {{ username }}!"}` - -### Available Context - -The following data is available as context for Jinja2 templates: - -* `event` - The type of event which triggered the webhook: created, updated, or deleted. -* `model` - The NetBox model which triggered the change. -* `timestamp` - The time at which the event occurred (in [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) format). -* `username` - The name of the user account associated with the change. -* `request_id` - The unique request ID. This may be used to correlate multiple changes associated with a single request. -* `data` - A detailed representation of the object in its current state. This is typically equivalent to the model's representation in NetBox's REST API. -* `snapshots` - Minimal "snapshots" of the object state both before and after the change was made; provided as a dictionary with keys named `prechange` and `postchange`. These are not as extensive as the fully serialized representation, but contain enough information to convey what has changed. - -### Default Request Body - -If no body template is specified, the request body will be populated with a JSON object containing the context data. For example, a newly created site might appear as follows: - -```json -{ - "event": "created", - "timestamp": "2021-03-09 17:55:33.968016+00:00", - "model": "site", - "username": "jstretch", - "request_id": "fdbca812-3142-4783-b364-2e2bd5c16c6a", - "data": { - "id": 19, - "name": "Site 1", - "slug": "site-1", - "status": - "value": "active", - "label": "Active", - "id": 1 - }, - "region": null, - ... - }, - "snapshots": { - "prechange": null, - "postchange": { - "created": "2021-03-09", - "last_updated": "2021-03-09T17:55:33.851Z", - "name": "Site 1", - "slug": "site-1", - "status": "active", - ... - } - } -} -``` - -## Conditional Webhooks - -A webhook may include a set of conditional logic expressed in JSON used to control whether a webhook triggers for a specific object. For example, you may wish to trigger a webhook for devices only when the `status` field of an object is "active": - -```json -{ - "and": [ - { - "attr": "status.value", - "value": "active" - } - ] -} -``` - -For more detail, see the reference documentation for NetBox's [conditional logic](../../reference/conditions.md). - -## Webhook Processing - -When a change is detected, any resulting webhooks are placed into a Redis queue for processing. This allows the user's request to complete without needing to wait for the outgoing webhook(s) to be processed. The webhooks are then extracted from the queue by the `rqworker` process and HTTP requests are sent to their respective destinations. The current webhook queue and any failed webhooks can be inspected in the admin UI under System > Background Tasks. - -A request is considered successful if the response has a 2XX status code; otherwise, the request is marked as having failed. Failed requests may be retried manually via the admin UI. - -## Troubleshooting - -To assist with verifying that the content of outgoing webhooks is rendered correctly, NetBox provides a simple HTTP listener that can be run locally to receive and display webhook requests. First, modify the target URL of the desired webhook to `http://localhost:9000/`. This will instruct NetBox to send the request to the local server on TCP port 9000. Then, start the webhook receiver service from the NetBox root directory: - -```no-highlight -$ python netbox/manage.py webhook_receiver -Listening on port http://localhost:9000. Stop with CONTROL-C. -``` - -You can test the receiver itself by sending any HTTP request to it. For example: - -```no-highlight -$ curl -X POST http://localhost:9000 --data '{"foo": "bar"}' -``` - -The server will print output similar to the following: - -```no-highlight -[1] Tue, 07 Apr 2020 17:44:02 GMT 127.0.0.1 "POST / HTTP/1.1" 200 - -Host: localhost:9000 -User-Agent: curl/7.58.0 -Accept: */* -Content-Length: 14 -Content-Type: application/x-www-form-urlencoded - -{"foo": "bar"} ------------- -``` - -Note that `webhook_receiver` does not actually _do_ anything with the information received: It merely prints the request headers and body for inspection. - -Now, when the NetBox webhook is triggered and processed, you should see its headers and content appear in the terminal where the webhook receiver is listening. If you don't, check that the `rqworker` process is running and that webhook events are being placed into the queue (visible under the NetBox admin UI). From e2d53139403218ce47b46b8808580b174f1b3dc6 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Thu, 11 Aug 2022 13:02:37 -0400 Subject: [PATCH 374/593] Changelog for #9857 --- docs/release-notes/version-3.2.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes/version-3.2.md b/docs/release-notes/version-3.2.md index 598ee7874..e57cea3b3 100644 --- a/docs/release-notes/version-3.2.md +++ b/docs/release-notes/version-3.2.md @@ -6,6 +6,7 @@ * [#9161](https://github.com/netbox-community/netbox/issues/9161) - Pretty print JSON custom field data when editing * [#9625](https://github.com/netbox-community/netbox/issues/9625) - Add phone & email details to contacts panel +* [#9857](https://github.com/netbox-community/netbox/issues/9857) - Add clear button to quick search fields --- From 6a687a9ed1027e2819391076195138c7babf9400 Mon Sep 17 00:00:00 2001 From: Craig Pund Date: Thu, 11 Aug 2022 15:16:01 -0400 Subject: [PATCH 375/593] not necessary to prefetch --- netbox/dcim/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 28325bcfc..bd693e392 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -1787,7 +1787,7 @@ class DeviceBulkDeleteView(generic.BulkDeleteView): class DeviceBulkRenameView(generic.BulkRenameView): - queryset = Device.objects.prefetch_related('tenant', 'site', 'rack', 'device_role', 'device_type__manufacturer') + queryset = Device.objects.all() filterset = filtersets.DeviceFilterSet table = tables.DeviceTable From 5873ad95dc16db127298037b1af5093543583eff Mon Sep 17 00:00:00 2001 From: Craig Pund Date: Thu, 11 Aug 2022 15:16:42 -0400 Subject: [PATCH 376/593] handle objects without names --- netbox/netbox/views/generic/bulk_views.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/netbox/netbox/views/generic/bulk_views.py b/netbox/netbox/views/generic/bulk_views.py index f17bc179d..e1f73b225 100644 --- a/netbox/netbox/views/generic/bulk_views.py +++ b/netbox/netbox/views/generic/bulk_views.py @@ -632,7 +632,7 @@ class BulkRenameView(GetReturnURLMixin, BaseMultiObjectView): replace = form.cleaned_data['replace'] if form.cleaned_data['use_regex']: try: - obj.new_name = re.sub(find, replace, obj.name) + obj.new_name = re.sub(find, replace, obj.name or '') # Catch regex group reference errors except re.error: obj.new_name = obj.name @@ -676,9 +676,6 @@ class BulkRenameView(GetReturnURLMixin, BaseMultiObjectView): else: form = self.form(initial={'pk': request.POST.getlist('pk')}) selected_objects = self.queryset.filter(pk__in=form.initial['pk']) - for object in selected_objects: - # Do something to raise error message to user - pass return render(request, self.template_name, { 'form': form, From f9fb2cc69827559d7934718b3eb84806c278510b Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Thu, 11 Aug 2022 15:29:26 -0400 Subject: [PATCH 377/593] Finish features documentation --- docs/development/models.md | 2 +- .../{integrations.md => api-integration.md} | 2 +- docs/features/authentication-permissions.md | 49 +++++++++++++++++++ docs/features/customization.md | 41 ++++++++++++++-- docs/features/permissions.md | 3 -- docs/features/sso.md | 3 -- docs/features/tags.md | 0 docs/integrations/webhooks.md | 2 +- mkdocs.yml | 10 ++-- 9 files changed, 92 insertions(+), 20 deletions(-) rename docs/features/{integrations.md => api-integration.md} (99%) create mode 100644 docs/features/authentication-permissions.md delete mode 100644 docs/features/permissions.md delete mode 100644 docs/features/sso.md delete mode 100644 docs/features/tags.md diff --git a/docs/development/models.md b/docs/development/models.md index 3c7aaaa78..3b03c8935 100644 --- a/docs/development/models.md +++ b/docs/development/models.md @@ -12,7 +12,7 @@ The Django [content types](https://docs.djangoproject.com/en/stable/ref/contrib/ * [Webhooks](../integrations/webhooks.md) - NetBox is capable of generating outgoing webhooks for these objects * [Custom fields](../customization/custom-fields.md) - These models support the addition of user-defined fields * [Export templates](../customization/export-templates.md) - Users can create custom export templates for these models -* [Tagging](../features/tags.md) - The models can be tagged with user-defined tags +* [Tagging](../models/extras/tag.md) - The models can be tagged with user-defined tags * [Journaling](../features/journaling.md) - These models support persistent historical commentary * Nesting - These models can be nested recursively to create a hierarchy diff --git a/docs/features/integrations.md b/docs/features/api-integration.md similarity index 99% rename from docs/features/integrations.md rename to docs/features/api-integration.md index c2f68c5c8..50c31ec4f 100644 --- a/docs/features/integrations.md +++ b/docs/features/api-integration.md @@ -1,4 +1,4 @@ -# Integrations +# API & Integration NetBox includes a slew of features which enable integration with other tools and resources powering your network. diff --git a/docs/features/authentication-permissions.md b/docs/features/authentication-permissions.md new file mode 100644 index 000000000..14e13d5cd --- /dev/null +++ b/docs/features/authentication-permissions.md @@ -0,0 +1,49 @@ +# Authentication & Permissions + +## Object-Based Permissions + +NetBox boasts a very robust permissions system which extends well beyond the model-based permissions of the underlying Django framework. Assigning permissions in NetBox involves several dimensions: + +* The type(s) of object to which the permission applies +* The users and/or groups being granted the permissions +* The action(s) permitted by the permission (e.g. view, add, change, etc.) +* Any constraints limiting application of the permission to a particular subset of objects + +The implementation of constrains is what enables NetBox administrators to assign per-object permissions: Users can be limited to viewing or interacting with arbitrary subsets of objects based on the objects' attributes. For example, you might restrict a particular user to viewing only those prefixes or IP addresses within a particular VRF. Or you might restrict a group to modifying devices within a particular region. + +Permission constraints are declared in JSON format when creating a permission, and operate very similarly to Django ORM queries. For instance, here's a constraint that matches reserved VLANs with a VLAN ID between 100 and 199: + +```json +[ + { + "vid__gte": 100, + "vid__lt": 200 + }, + { + "status": "reserved" + } +] +``` + +Check out the [permissions documentation](../administration/permissions.md) for more information about permission constraints. + +## LDAP Authentication + +NetBox includes a built-in authentication backend for authenticating users against a remote LDAP server. The [installation documentation](../installation/6-ldap.md) provides more detail on this capability. + +## Single Sign-On (SSO) + +NetBox integrates with the open source [python-social-auth](https://github.com/python-social-auth) library to provide [myriad options](https://python-social-auth.readthedocs.io/en/latest/backends/index.html#supported-backends) for single sign-on (SSO) authentication. These include: + +* Cognito +* GitHub & GitHub Enterprise +* GitLab +* Google +* Hashicorp Vault +* Keycloak +* Microsoft Azure AD +* Microsoft Graph +* Okta +* OIDC + +...and many others. It's also possible to build your own custom backends as needed using python-social-auth's base OAuth, OpenID, and SAML classes. You can find some examples of configuring SSO in NetBox' [authentication documentation](../administration/authentication/overview.md). diff --git a/docs/features/customization.md b/docs/features/customization.md index 5b42d5e43..d7f2cf40a 100644 --- a/docs/features/customization.md +++ b/docs/features/customization.md @@ -2,6 +2,10 @@ While NetBox strives to meet the needs of every network, the needs of users to cater to their own unique environments cannot be ignored. NetBox was built with this in mind, and can be customized in many ways to better suit your particular needs. +## Tags + +Most objects in NetBox can be assigned user-created tags to aid with organization and filtering. Tag values are completely arbitrary: They may be used to store data in key-value pairs, or they may be employed simply as labels against which objects can be filtered. Each tag can also be assigned a color for quicker differentiation in the user interface. + ## Custom Fields While NetBox provides a rather extensive data model out of the box, the need may arise to store certain additional data associated with NetBox objects. For example, you might need to record the invoice ID alongside an installed device, or record an approving authority when creating a new IP prefix. NetBox administrators can create custom fields on built-in objects to meet these needs. @@ -12,30 +16,57 @@ To learn more about this feature, check out the [custom field documentation](../ ## Custom Links -TODO +Custom links allow you to conveniently reference external resources related to NetBox objects from within the NetBox UI. For example, you might wish to link each virtual machine modeled in NetBox to its corresponding view in some orchestration application. You can do this by creating a templatized custom link for the virtual machine model, specifying something like the following for the link URL: + +```no-highlight +http://server.local/vms/?name={{ object.name }} +``` + +Now, when viewing a virtual machine in NetBox, a user will see a handy button with the chosen title and link (complete with the name of the VM being viewed). Both the text and URL of custom links can be templatized in this manner, and custom links can be grouped together into dropdowns for more efficient display. To learn more about this feature, check out the [custom link documentation](../customization/custom-links.md). ## Custom Validation -TODO +While NetBox employs robust built-in object validation to ensure the integrity of its database, you might wish to enforce additional rules governing the creation and modification of certain objects. For example, perhaps you require that every device defined in NetBox adheres to a particular naming scheme and includes an asset tag. You can configure a custom validation rule in NetBox to enforce these requirements for the device model: + +```python +CUSTOM_VALIDATORS = { + "dcim.device": [ + { + "name": { + "regex": "[a-z]+\d{3}" + }, + "asset_tag": { + "required": True + } + } + ] +} +``` To learn more about this feature, check out the [custom validation documentation](../customization/custom-validation.md). ## Export Templates -TODO +Most NetBox objects can be exported in bulk in two built-in CSV formats: The current view (what the user currently sees in the objects list), or all available data. NetBox also provides the capability to define your own custom data export formats via export templates. An export template is essentially [Jinja2](https://jinja.palletsprojects.com/) template code associated with a particular object type. From the objects list in the NetBox UI, a user can select any of the created export templates to export the objects according to the template logic. + +An export template doesn't have to render CSV data: Its output can be in any character-based format. For example, you might want to render data using tabs as delimiters, or even create DNS address records directly from the IP addresses list. Export templates are a great way to get the data you need in the format you need quickly. To learn more about this feature, check out the [export template documentation](../customization/export-templates.md). ## Reports -TODO +NetBox administrators can install custom Python scripts, known as _reports_, which run within NetBox and can be executed and analyzed within the NetBox UI. Reports are a great way to evaluate NetBox objects against a set of arbitrary rules. For example, you could write a report to check that every router has a loopback interface with an IP address assigned, or that every site has a minimum set of VLANs defined. + +When a report runs, its logs messages pertaining to the operations being performed, and will ultimately result in either a pass or fail. Reports can be executed via the UI, REST API, or CLI (as a management command). To learn more about this feature, check out the [documentation for reports](../customization/reports.md). ## Custom Scripts -TODO +Custom scripts are similar to reports, but more powerful. A custom script can prompt the user for input via a form (or API data), and is built to do much more than just reporting. Custom scripts are generally used to automate tasks, such as the population of new objects in NetBox, or exchanging data with external systems. + +The complete Python environment is available to a custom script, including all of NetBox's internal mechanisms: There are no artificial restrictions on what a script can do. As such, custom scripting is considered an advanced feature and requires sufficient familiarity with Python and NetBox's data model. To learn more about this feature, check out the [documentation for custom scripts](../customization/custom-scripts.md). diff --git a/docs/features/permissions.md b/docs/features/permissions.md deleted file mode 100644 index a422ca7b3..000000000 --- a/docs/features/permissions.md +++ /dev/null @@ -1,3 +0,0 @@ -# Object-Based Permissions - -TODO diff --git a/docs/features/sso.md b/docs/features/sso.md deleted file mode 100644 index b4f9782c2..000000000 --- a/docs/features/sso.md +++ /dev/null @@ -1,3 +0,0 @@ -# Single Sign-On (SSO) - -TODO diff --git a/docs/features/tags.md b/docs/features/tags.md deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/integrations/webhooks.md b/docs/integrations/webhooks.md index 3ef28fb11..9a1094988 100644 --- a/docs/integrations/webhooks.md +++ b/docs/integrations/webhooks.md @@ -85,7 +85,7 @@ A webhook may include a set of conditional logic expressed in JSON used to contr } ``` -For more detail, see the reference documentation for NetBox's [conditional logic](../../reference/conditions.md). +For more detail, see the reference documentation for NetBox's [conditional logic](../reference/conditions.md). ## Webhook Processing diff --git a/mkdocs.yml b/mkdocs.yml index a5eb36d81..8bcf6cebf 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -61,8 +61,6 @@ nav: - Introduction: 'introduction.md' - Features: - Facilities: 'features/facilities.md' - - Tenancy: 'features/tenancy.md' - - Contacts: 'features/contacts.md' - Devices & Cabling: 'features/devices-cabling.md' - Power Tracking: 'features/power-tracking.md' - IPAM: 'features/ipam.md' @@ -70,14 +68,14 @@ nav: - L2VPN & Overlay: 'features/l2vpn-overlay.md' - Circuits: 'features/circuits.md' - Wireless: 'features/wireless.md' + - Tenancy: 'features/tenancy.md' + - Contacts: 'features/contacts.md' - Context Data: 'features/context-data.md' - Change Logging: 'features/change-logging.md' - Journaling: 'features/journaling.md' - - Tags: 'features/tags.md' - - Integrations: 'features/integrations.md' + - Auth & Permissions: 'features/authentication-permissions.md' + - API & Integration: 'features/api-integration.md' - Customization: 'features/customization.md' - - Object-Based Permissions: 'features/permissions.md' - - Single Sign-On (SSO): 'features/sso.md' - Installation & Upgrade: - Installing NetBox: 'installation/index.md' - 1. PostgreSQL: 'installation/1-postgresql.md' From 0750e27351db01161b74fa7417410ae9bdead423 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Thu, 11 Aug 2022 16:14:06 -0400 Subject: [PATCH 378/593] Finish customization docs --- docs/customization/custom-fields.md | 108 +++++++++++++++++++++ docs/customization/custom-links.md | 66 +++++++++++++ docs/customization/export-templates.md | 81 ++++++++++++++++ docs/models/extras/customfield.md | 128 +++++++++++-------------- docs/models/extras/customlink.md | 94 ++++++++---------- docs/models/extras/exporttemplate.md | 80 +++------------- 6 files changed, 365 insertions(+), 192 deletions(-) diff --git a/docs/customization/custom-fields.md b/docs/customization/custom-fields.md index e69de29bb..c443fa9f6 100644 --- a/docs/customization/custom-fields.md +++ b/docs/customization/custom-fields.md @@ -0,0 +1,108 @@ +# Custom Fields + +Each model in NetBox is represented in the database as a discrete table, and each attribute of a model exists as a column within its table. For example, sites are stored in the `dcim_site` table, which has columns named `name`, `facility`, `physical_address`, and so on. As new attributes are added to objects throughout the development of NetBox, tables are expanded to include new rows. + +However, some users might want to store additional object attributes that are somewhat esoteric in nature, and that would not make sense to include in the core NetBox database schema. For instance, suppose your organization needs to associate each device with a ticket number correlating it with an internal support system record. This is certainly a legitimate use for NetBox, but it's not a common enough need to warrant including a field for _every_ NetBox installation. Instead, you can create a custom field to hold this data. + +Within the database, custom fields are stored as JSON data directly alongside each object. This alleviates the need for complex queries when retrieving objects. + +## Creating Custom Fields + +Custom fields may be created by navigating to Customization > Custom Fields. NetBox supports many types of custom field: + +* Text: Free-form text (intended for single-line use) +* Long text: Free-form of any length; supports Markdown rendering +* Integer: A whole number (positive or negative) +* Boolean: True or false +* Date: A date in ISO 8601 format (YYYY-MM-DD) +* URL: This will be presented as a link in the web UI +* JSON: Arbitrary data stored in JSON format +* Selection: A selection of one of several pre-defined custom choices +* Multiple selection: A selection field which supports the assignment of multiple values +* Object: A single NetBox object of the type defined by `object_type` +* Multiple object: One or more NetBox objects of the type defined by `object_type` + +Each custom field must have a name. This should be a simple database-friendly string (e.g. `tps_report`) and may contain only alphanumeric characters and underscores. You may also assign a corresponding human-friendly label (e.g. "TPS report"); the label will be displayed on web forms. A weight is also required: Higher-weight fields will be ordered lower within a form. (The default weight is 100.) If a description is provided, it will appear beneath the field in a form. + +Marking a field as required will force the user to provide a value for the field when creating a new object or when saving an existing object. A default value for the field may also be provided. Use "true" or "false" for boolean fields, or the exact value of a choice for selection fields. + +A custom field must be assigned to one or more object types, or models, in NetBox. Once created, custom fields will automatically appear as part of these models in the web UI and REST API. Note that not all models support custom fields. + +### Filtering + +The filter logic controls how values are matched when filtering objects by the custom field. Loose filtering (the default) matches on a partial value, whereas exact matching requires a complete match of the given string to a field's value. For example, exact filtering with the string "red" will only match the exact value "red", whereas loose filtering will match on the values "red", "red-orange", or "bored". Setting the filter logic to "disabled" disables filtering by the field entirely. + +### Grouping + +!!! note + This feature was introduced in NetBox v3.3. + +Related custom fields can be grouped together within the UI by assigning each the same group name. When at least one custom field for an object type has a group defined, it will appear under the group heading within the custom fields panel under the object view. All custom fields with the same group name will appear under that heading. (Note that the group names must match exactly, or each will appear as a separate heading.) + +This parameter has no effect on the API representation of custom field data. + +### Visibility + +!!! note + This feature was introduced in NetBox v3.3. + +When creating a custom field, there are three options for UI visibility. These control how and whether the custom field is displayed within the NetBox UI. + +* **Read/write** (default): The custom field is included when viewing and editing objects. +* **Read-only**: The custom field is displayed when viewing an object, but it cannot be edited via the UI. (It will appear in the form as a read-only field.) +* **Hidden**: The custom field will never be displayed within the UI. This option is recommended for fields which are not intended for use by human users. + +Note that this setting has no impact on the REST or GraphQL APIs: Custom field data will always be available via either API. + +### Validation + +NetBox supports limited custom validation for custom field values. Following are the types of validation enforced for each field type: + +* Text: Regular expression (optional) +* Integer: Minimum and/or maximum value (optional) +* Selection: Must exactly match one of the prescribed choices + +### Custom Selection Fields + +Each custom selection field must have at least two choices. These are specified as a comma-separated list. Choices appear in forms in the order they are listed. Note that choice values are saved exactly as they appear, so it's best to avoid superfluous punctuation or symbols where possible. + +If a default value is specified for a selection field, it must exactly match one of the provided choices. The value of a multiple selection field will always return a list, even if only one value is selected. + +### Custom Object Fields + +An object or multi-object custom field can be used to refer to a particular NetBox object or objects as the "value" for a custom field. These custom fields must define an `object_type`, which determines the type of object to which custom field instances point. + +## Custom Fields in Templates + +Several features within NetBox, such as export templates and webhooks, utilize Jinja2 templating. For convenience, objects which support custom field assignment expose custom field data through the `cf` property. This is a bit cleaner than accessing custom field data through the actual field (`custom_field_data`). + +For example, a custom field named `foo123` on the Site model is accessible on an instance as `{{ site.cf.foo123 }}`. + +## Custom Fields and the REST API + +When retrieving an object via the REST API, all of its custom data will be included within the `custom_fields` attribute. For example, below is the partial output of a site with two custom fields defined: + +```json +{ + "id": 123, + "url": "http://localhost:8000/api/dcim/sites/123/", + "name": "Raleigh 42", + ... + "custom_fields": { + "deployed": "2018-06-19", + "site_code": "US-NC-RAL42" + }, + ... +``` + +To set or change these values, simply include nested JSON data. For example: + +```json +{ + "name": "New Site", + "slug": "new-site", + "custom_fields": { + "deployed": "2019-03-24" + } +} +``` diff --git a/docs/customization/custom-links.md b/docs/customization/custom-links.md index e69de29bb..16ba9d2af 100644 --- a/docs/customization/custom-links.md +++ b/docs/customization/custom-links.md @@ -0,0 +1,66 @@ +# Custom Links + +Custom links allow users to display arbitrary hyperlinks to external content within NetBox object views. These are helpful for cross-referencing related records in systems outside NetBox. For example, you might create a custom link on the device view which links to the current device in a Network Monitoring System (NMS). + +Custom links are created by navigating to Customization > Custom Links. Each link is associated with a particular NetBox object type (site, device, prefix, etc.) and will be displayed on relevant views. Each link has display text and a URL, and data from the NetBox item being viewed can be included in the link using [Jinja2 template code](https://jinja2docs.readthedocs.io/en/stable/) through the variable `obj`, and custom fields through `obj.cf`. + +For example, you might define a link like this: + +* Text: `View NMS` +* URL: `https://nms.example.com/nodes/?name={{ obj.name }}` + +When viewing a device named Router4, this link would render as: + +```no-highlight +View NMS +``` + +Custom links appear as buttons in the top right corner of the page. Numeric weighting can be used to influence the ordering of links, and each link can be enabled or disabled individually. + +!!! warning + Custom links rely on user-created code to generate arbitrary HTML output, which may be dangerous. Only grant permission to create or modify custom links to trusted users. + +## Context Data + +The following context data is available within the template when rendering a custom link's text or URL. + +| Variable | Description | +|-----------|-------------------------------------------------------------------------------------------------------------------| +| `object` | The NetBox object being displayed | +| `obj` | Same as `object`; maintained for backward compatability until NetBox v3.5 | +| `debug` | A boolean indicating whether debugging is enabled | +| `request` | The current WSGI request | +| `user` | The current user (if authenticated) | +| `perms` | The [permissions](https://docs.djangoproject.com/en/stable/topics/auth/default/#permissions) assigned to the user | + +While most of the context variables listed above will have consistent attributes, the object will be an instance of the specific object being viewed when the link is rendered. Different models have different fields and properties, so you may need to some research to determine the attributes available for use within your template for a specific object type. + +Checking the REST API representation of an object is generally a convenient way to determine what attributes are available. You can also reference the NetBox source code directly for a comprehensive list. + +## Conditional Rendering + +Only links which render with non-empty text are included on the page. You can employ conditional Jinja2 logic to control the conditions under which a link gets rendered. + +For example, if you only want to display a link for active devices, you could set the link text to + +```jinja2 +{% if obj.status == 'active' %}View NMS{% endif %} +``` + +The link will not appear when viewing a device with any status other than "active." + +As another example, if you wanted to show only devices belonging to a certain manufacturer, you could do something like this: + +```jinja2 +{% if obj.device_type.manufacturer.name == 'Cisco' %}View NMS{% endif %} +``` + +The link will only appear when viewing a device with a manufacturer name of "Cisco." + +## Link Groups + +Group names can be specified to organize links into groups. Links with the same group name will render as a dropdown menu beneath a single button bearing the name of the group. + +## Table Columns + +Custom links can also be included in object tables by selecting the desired links from the table configuration form. When displayed, each link will render as a hyperlink for its corresponding object. When exported (e.g. as CSV data), each link render only its URL. diff --git a/docs/customization/export-templates.md b/docs/customization/export-templates.md index e69de29bb..640a97531 100644 --- a/docs/customization/export-templates.md +++ b/docs/customization/export-templates.md @@ -0,0 +1,81 @@ +# Export Templates + +NetBox allows users to define custom templates that can be used when exporting objects. To create an export template, navigate to Customization > Export Templates. + +Each export template is associated with a certain type of object. For instance, if you create an export template for VLANs, your custom template will appear under the "Export" button on the VLANs list. Each export template must have a name, and may optionally designate a specific export [MIME type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types) and/or file extension. + +Export templates must be written in [Jinja2](https://jinja.palletsprojects.com/). + +!!! note + The name `table` is reserved for internal use. + +!!! warning + Export templates are rendered using user-submitted code, which may pose security risks under certain conditions. Only grant permission to create or modify export templates to trusted users. + +The list of objects returned from the database when rendering an export template is stored in the `queryset` variable, which you'll typically want to iterate through using a `for` loop. Object properties can be access by name. For example: + +```jinja2 +{% for rack in queryset %} +Rack: {{ rack.name }} +Site: {{ rack.site.name }} +Height: {{ rack.u_height }}U +{% endfor %} +``` + +To access custom fields of an object within a template, use the `cf` attribute. For example, `{{ obj.cf.color }}` will return the value (if any) for a custom field named `color` on `obj`. + +If you need to use the config context data in an export template, you'll should use the function `get_config_context` to get all the config context data. For example: +``` +{% for server in queryset %} +{% set data = server.get_config_context() %} +{{ data.syslog }} +{% endfor %} +``` + +The `as_attachment` attribute of an export template controls its behavior when rendered. If true, the rendered content will be returned to the user as a downloadable file. If false, it will be displayed within the browser. (This may be handy e.g. for generating HTML content.) + +A MIME type and file extension can optionally be defined for each export template. The default MIME type is `text/plain`. + + +## REST API Integration + +When it is necessary to provide authentication credentials (such as when [`LOGIN_REQUIRED`](../configuration/security.md#login_required) has been enabled), it is recommended to render export templates via the REST API. This allows the client to specify an authentication token. To render an export template via the REST API, make a `GET` request to the model's list endpoint and append the `export` parameter specifying the export template name. For example: + +``` +GET /api/dcim/sites/?export=MyTemplateName +``` + +Note that the body of the response will contain only the rendered export template content, as opposed to a JSON object or list. + +## Example + +Here's an example device export template that will generate a simple Nagios configuration from a list of devices. + +``` +{% for device in queryset %}{% if device.status and device.primary_ip %}define host{ + use generic-switch + host_name {{ device.name }} + address {{ device.primary_ip.address.ip }} +} +{% endif %}{% endfor %} +``` + +The generated output will look something like this: + +``` +define host{ + use generic-switch + host_name switch1 + address 192.0.2.1 +} +define host{ + use generic-switch + host_name switch2 + address 192.0.2.2 +} +define host{ + use generic-switch + host_name switch3 + address 192.0.2.3 +} +``` diff --git a/docs/models/extras/customfield.md b/docs/models/extras/customfield.md index bfe412edc..0b2bc29f9 100644 --- a/docs/models/extras/customfield.md +++ b/docs/models/extras/customfield.md @@ -1,109 +1,93 @@ # Custom Fields -Each model in NetBox is represented in the database as a discrete table, and each attribute of a model exists as a column within its table. For example, sites are stored in the `dcim_site` table, which has columns named `name`, `facility`, `physical_address`, and so on. As new attributes are added to objects throughout the development of NetBox, tables are expanded to include new rows. +## Fields -However, some users might want to store additional object attributes that are somewhat esoteric in nature, and that would not make sense to include in the core NetBox database schema. For instance, suppose your organization needs to associate each device with a ticket number correlating it with an internal support system record. This is certainly a legitimate use for NetBox, but it's not a common enough need to warrant including a field for _every_ NetBox installation. Instead, you can create a custom field to hold this data. +### Model(s) -Within the database, custom fields are stored as JSON data directly alongside each object. This alleviates the need for complex queries when retrieving objects. +Select the NetBox object type or types to which this custom field applies. -## Creating Custom Fields +### Name -Custom fields may be created by navigating to Customization > Custom Fields. NetBox supports six types of custom field: +The raw field name. This will be used in the database and API, and should consist only of alphanumeric characters and underscores. (Use the `label` field to designate a human-friendly name for the custom field.) -* Text: Free-form text (intended for single-line use) -* Long text: Free-form of any length; supports Markdown rendering -* Integer: A whole number (positive or negative) -* Boolean: True or false -* Date: A date in ISO 8601 format (YYYY-MM-DD) -* URL: This will be presented as a link in the web UI -* JSON: Arbitrary data stored in JSON format -* Selection: A selection of one of several pre-defined custom choices -* Multiple selection: A selection field which supports the assignment of multiple values -* Object: A single NetBox object of the type defined by `object_type` -* Multiple object: One or more NetBox objects of the type defined by `object_type` +### Label -Each custom field must have a name. This should be a simple database-friendly string (e.g. `tps_report`) and may contain only alphanumeric characters and underscores. You may also assign a corresponding human-friendly label (e.g. "TPS report"); the label will be displayed on web forms. A weight is also required: Higher-weight fields will be ordered lower within a form. (The default weight is 100.) If a description is provided, it will appear beneath the field in a form. +An optional human-friendly name for the custom field. If not defined, the field's `name` attribute will be used. -Marking a field as required will force the user to provide a value for the field when creating a new object or when saving an existing object. A default value for the field may also be provided. Use "true" or "false" for boolean fields, or the exact value of a choice for selection fields. +### Group Name -A custom field must be assigned to one or more object types, or models, in NetBox. Once created, custom fields will automatically appear as part of these models in the web UI and REST API. Note that not all models support custom fields. +If this custom field should be grouped with others, specify the name of the group here. Custom fields with no group defined will be ordered only by weight and name. -### Filtering +### Type -The filter logic controls how values are matched when filtering objects by the custom field. Loose filtering (the default) matches on a partial value, whereas exact matching requires a complete match of the given string to a field's value. For example, exact filtering with the string "red" will only match the exact value "red", whereas loose filtering will match on the values "red", "red-orange", or "bored". Setting the filter logic to "disabled" disables filtering by the field entirely. +The type of data this field holds. This must be one of the following: -### Grouping +| Type | Description | +|--------------------|--------------------------------------------------------------------| +| Text | Free-form text (intended for single-line use) | +| Long text | Free-form of any length; supports Markdown rendering | +| Integer | A whole number (positive or negative) | +| Boolean | True or false | +| Date | A date in ISO 8601 format (YYYY-MM-DD) | +| URL | This will be presented as a link in the web UI | +| JSON | Arbitrary data stored in JSON format | +| Selection | A selection of one of several pre-defined custom choices | +| Multiple selection | A selection field which supports the assignment of multiple values | +| Object | A single NetBox object of the type defined by `object_type` | +| Multiple object | One or more NetBox objects of the type defined by `object_type` | -!!! note - This feature was introduced in NetBox v3.3. +### Object Type -Related custom fields can be grouped together within the UI by assigning each the same group name. When at least one custom field for an object type has a group defined, it will appear under the group heading within the custom fields panel under the object view. All custom fields with the same group name will appear under that heading. (Note that the group names must match exactly, or each will appear as a separate heading.) +For object and multiple-object fields only. Designates the type of NetBox object being referenced. -This parameter has no effect on the API representation of custom field data. +### Weight -### Visibility +A numeric weight used to override alphabetic ordering of fields by name. Custom fields with a lower weight will be listed before those with a higher weight. (Note that weight applies within the context of a custom field group, if defined.) -!!! note - This feature was introduced in NetBox v3.3. +### Required -When creating a custom field, there are three options for UI visibility. These control how and whether the custom field is displayed within the NetBox UI. +If checked, this custom field must be populated with a valid value for the object to pass validation. -* **Read/write** (default): The custom field is included when viewing and editing objects. -* **Read-only**: The custom field is displayed when viewing an object, but it cannot be edited via the UI. (It will appear in the form as a read-only field.) -* **Hidden**: The custom field will never be displayed within the UI. This option is recommended for fields which are not intended for use by human users. +### Description -Note that this setting has no impact on the REST or GraphQL APIs: Custom field data will always be available via either API. +A brief description of the field's purpose (optional). -### Validation +### Filter Logic -NetBox supports limited custom validation for custom field values. Following are the types of validation enforced for each field type: +Defines how filters are evaluated against custom field values. -* Text: Regular expression (optional) -* Integer: Minimum and/or maximum value (optional) -* Selection: Must exactly match one of the prescribed choices +| Option | Description | +|----------|-------------------------------------| +| Disabled | Filtering disabled | +| Loose | Match any occurrence of the value | +| Exact | Match only the complete field value | -### Custom Selection Fields +### UI Visibility -Each custom selection field must have at least two choices. These are specified as a comma-separated list. Choices appear in forms in the order they are listed. Note that choice values are saved exactly as they appear, so it's best to avoid superfluous punctuation or symbols where possible. +Controls how and whether the custom field is displayed within the NetBox user interface. -If a default value is specified for a selection field, it must exactly match one of the provided choices. The value of a multiple selection field will always return a list, even if only one value is selected. +| Option | Description | +|------------|--------------------------------------| +| Read/write | Display and permit editing (default) | +| Read-only | Display field but disallow editing | +| Hidden | Do not display field in the UI | -### Custom Object Fields +### Default -An object or multi-object custom field can be used to refer to a particular NetBox object or objects as the "value" for a custom field. These custom fields must define an `object_type`, which determines the type of object to which custom field instances point. +The default value to populate for the custom field when creating new objects (optional). This value must be expressed as JSON. If this is a choice or multi-choice field, this must be one of the available choices. +### Choices -## Custom Fields in Templates +For choice and multi-choice custom fields only. A comma-delimited list of the available choices. -Several features within NetBox, such as export templates and webhooks, utilize Jinja2 templating. For convenience, objects which support custom field assignment expose custom field data through the `cf` property. This is a bit cleaner than accessing custom field data through the actual field (`custom_field_data`). +### Minimum Value -For example, a custom field named `foo123` on the Site model is accessible on an instance as `{{ site.cf.foo123 }}`. +For numeric custom fields only. The minimum valid value (optional). -## Custom Fields and the REST API +### Maximum Value -When retrieving an object via the REST API, all of its custom data will be included within the `custom_fields` attribute. For example, below is the partial output of a site with two custom fields defined: +For numeric custom fields only. The maximum valid value (optional). -```json -{ - "id": 123, - "url": "http://localhost:8000/api/dcim/sites/123/", - "name": "Raleigh 42", - ... - "custom_fields": { - "deployed": "2018-06-19", - "site_code": "US-NC-RAL42" - }, - ... -``` +### Validation Regex -To set or change these values, simply include nested JSON data. For example: - -```json -{ - "name": "New Site", - "slug": "new-site", - "custom_fields": { - "deployed": "2019-03-24" - } -} -``` +For string-based custom fields only. A regular expression used to validate the field's value (optional). diff --git a/docs/models/extras/customlink.md b/docs/models/extras/customlink.md index 16ba9d2af..458007ab1 100644 --- a/docs/models/extras/customlink.md +++ b/docs/models/extras/customlink.md @@ -1,66 +1,54 @@ # Custom Links -Custom links allow users to display arbitrary hyperlinks to external content within NetBox object views. These are helpful for cross-referencing related records in systems outside NetBox. For example, you might create a custom link on the device view which links to the current device in a Network Monitoring System (NMS). +## Fields -Custom links are created by navigating to Customization > Custom Links. Each link is associated with a particular NetBox object type (site, device, prefix, etc.) and will be displayed on relevant views. Each link has display text and a URL, and data from the NetBox item being viewed can be included in the link using [Jinja2 template code](https://jinja2docs.readthedocs.io/en/stable/) through the variable `obj`, and custom fields through `obj.cf`. +### Name -For example, you might define a link like this: +The name of the custom link. This is used primarily for administrative purposes only, although custom links of the same weight are ordered alphabetically by name when being rendered in the UI. -* Text: `View NMS` -* URL: `https://nms.example.com/nodes/?name={{ obj.name }}` +### Content Type -When viewing a device named Router4, this link would render as: +The type of NetBox object to which this custom link applies. -```no-highlight -View NMS -``` +### Weight -Custom links appear as buttons in the top right corner of the page. Numeric weighting can be used to influence the ordering of links, and each link can be enabled or disabled individually. +A numeric weight used to override alphabetic ordering of links by name. Custom fields with a lower weight will be listed before those with a higher weight. (Note that weight applies within the context of a custom link group, if defined.) -!!! warning - Custom links rely on user-created code to generate arbitrary HTML output, which may be dangerous. Only grant permission to create or modify custom links to trusted users. +### Group Name + +If this custom link should be grouped with others, specify the name of the group here. Grouped custom links will be listed in a dropdown menu attached to a single button bearing the group name. + +### Button Class + +The color of the UI button. + +### Enabled + +If not selected, the custom link will not be rendered. This can be useful for temporarily disabling a custom link. + +### New Window + +If selected, this will force the link to open in a new browser tab or window. + +### Link Text + +Jinja2 template code for rendering the button text. (Note that this does not _need_ to contain any template variables.) See below for available context data. + +!!! note + Custom links which render an empty text value will not be displayed in the UI. This can be used to toggle the inclusion of a link based on object attributes. + +### Link URL + +Jinja2 template code for rendering the hyperlink. See below for available context data. ## Context Data -The following context data is available within the template when rendering a custom link's text or URL. +The following context variables are available in to the text and link templates. -| Variable | Description | -|-----------|-------------------------------------------------------------------------------------------------------------------| -| `object` | The NetBox object being displayed | -| `obj` | Same as `object`; maintained for backward compatability until NetBox v3.5 | -| `debug` | A boolean indicating whether debugging is enabled | -| `request` | The current WSGI request | -| `user` | The current user (if authenticated) | -| `perms` | The [permissions](https://docs.djangoproject.com/en/stable/topics/auth/default/#permissions) assigned to the user | - -While most of the context variables listed above will have consistent attributes, the object will be an instance of the specific object being viewed when the link is rendered. Different models have different fields and properties, so you may need to some research to determine the attributes available for use within your template for a specific object type. - -Checking the REST API representation of an object is generally a convenient way to determine what attributes are available. You can also reference the NetBox source code directly for a comprehensive list. - -## Conditional Rendering - -Only links which render with non-empty text are included on the page. You can employ conditional Jinja2 logic to control the conditions under which a link gets rendered. - -For example, if you only want to display a link for active devices, you could set the link text to - -```jinja2 -{% if obj.status == 'active' %}View NMS{% endif %} -``` - -The link will not appear when viewing a device with any status other than "active." - -As another example, if you wanted to show only devices belonging to a certain manufacturer, you could do something like this: - -```jinja2 -{% if obj.device_type.manufacturer.name == 'Cisco' %}View NMS{% endif %} -``` - -The link will only appear when viewing a device with a manufacturer name of "Cisco." - -## Link Groups - -Group names can be specified to organize links into groups. Links with the same group name will render as a dropdown menu beneath a single button bearing the name of the group. - -## Table Columns - -Custom links can also be included in object tables by selecting the desired links from the table configuration form. When displayed, each link will render as a hyperlink for its corresponding object. When exported (e.g. as CSV data), each link render only its URL. +| Variable | Description | +|-----------|-----------------------------------------------------------------------------| +| `object` | The NetBox object being displayed | +| `debug` | A boolean indicating whether debugging is enabled | +| `request` | The current WSGI request | +| `user` | The current user (if authenticated) | +| `perms` | The [permissions](../../administration/permissions.md) assigned to the user | diff --git a/docs/models/extras/exporttemplate.md b/docs/models/extras/exporttemplate.md index b4b69108a..347184324 100644 --- a/docs/models/extras/exporttemplate.md +++ b/docs/models/extras/exporttemplate.md @@ -1,81 +1,27 @@ # Export Templates -NetBox allows users to define custom templates that can be used when exporting objects. To create an export template, navigate to Customization > Export Templates. +## Fields -Each export template is associated with a certain type of object. For instance, if you create an export template for VLANs, your custom template will appear under the "Export" button on the VLANs list. Each export template must have a name, and may optionally designate a specific export [MIME type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types) and/or file extension. +### Name -Export templates must be written in [Jinja2](https://jinja.palletsprojects.com/). +The name of the export template. This will appear in the "export" dropdown list in the NetBox UI. -!!! note - The name `table` is reserved for internal use. +### Content Type -!!! warning - Export templates are rendered using user-submitted code, which may pose security risks under certain conditions. Only grant permission to create or modify export templates to trusted users. +The type of NetBox object to which the export template applies. -The list of objects returned from the database when rendering an export template is stored in the `queryset` variable, which you'll typically want to iterate through using a `for` loop. Object properties can be access by name. For example: +### Template Code -```jinja2 -{% for rack in queryset %} -Rack: {{ rack.name }} -Site: {{ rack.site.name }} -Height: {{ rack.u_height }}U -{% endfor %} -``` +Jinja2 template code for rendering the exported data. -To access custom fields of an object within a template, use the `cf` attribute. For example, `{{ obj.cf.color }}` will return the value (if any) for a custom field named `color` on `obj`. +### MIME Type -If you need to use the config context data in an export template, you'll should use the function `get_config_context` to get all the config context data. For example: -``` -{% for server in queryset %} -{% set data = server.get_config_context() %} -{{ data.syslog }} -{% endfor %} -``` +The MIME type to indicate in the response when rendering the export template (optional). Defaults to `text/plain`. -The `as_attachment` attribute of an export template controls its behavior when rendered. If true, the rendered content will be returned to the user as a downloadable file. If false, it will be displayed within the browser. (This may be handy e.g. for generating HTML content.) +### File Extension -A MIME type and file extension can optionally be defined for each export template. The default MIME type is `text/plain`. +The file extension to append to the file name in the response (optional). +### As Attachment -## REST API Integration - -When it is necessary to provide authentication credentials (such as when [`LOGIN_REQUIRED`](../../configuration/security.md#login_required) has been enabled), it is recommended to render export templates via the REST API. This allows the client to specify an authentication token. To render an export template via the REST API, make a `GET` request to the model's list endpoint and append the `export` parameter specifying the export template name. For example: - -``` -GET /api/dcim/sites/?export=MyTemplateName -``` - -Note that the body of the response will contain only the rendered export template content, as opposed to a JSON object or list. - -## Example - -Here's an example device export template that will generate a simple Nagios configuration from a list of devices. - -``` -{% for device in queryset %}{% if device.status and device.primary_ip %}define host{ - use generic-switch - host_name {{ device.name }} - address {{ device.primary_ip.address.ip }} -} -{% endif %}{% endfor %} -``` - -The generated output will look something like this: - -``` -define host{ - use generic-switch - host_name switch1 - address 192.0.2.1 -} -define host{ - use generic-switch - host_name switch2 - address 192.0.2.2 -} -define host{ - use generic-switch - host_name switch3 - address 192.0.2.3 -} -``` +If selected, the rendered content will be returned as a file attachment, rather than displayed directly in-browser (where supported). From 7e3050cf83e729ed43a9e591e7dd937c7a98febb Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Thu, 11 Aug 2022 16:34:29 -0400 Subject: [PATCH 379/593] Add "getting started" links --- docs/index.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/index.md b/docs/index.md index a6bbcecff..d412d1a49 100644 --- a/docs/index.md +++ b/docs/index.md @@ -24,7 +24,7 @@ Unlike general-purpose CMDBs, NetBox has curated a data model which caters speci ## Customizable & Extensible -In addition to its expansive and robust data model, NetBox offers myriad mechanisms through it can be customized and extended. +In addition to its expansive and robust data model, NetBox offers myriad mechanisms through which it can be customized and extended. * Custom fields * Custom model validation @@ -39,10 +39,10 @@ Because NetBox is an open source application licensed under [Apache 2](https://w ## Getting Started -* Public Demo -* Installation Guide -* Docker install -* NetBox Cloud +* Try out our [public demo](https://demo.netbox.dev/) if you want to jump right in +* The [installation guide](./installation/index.md) will help you get your own deployment up and running +* Or try the community [Docker image](https://github.com/netbox-community/netbox-docker) for a low-touch approach +* [NetBox Cloud](https://www.getnetbox.io/) is a hosted solution offered by NS1 !!! tip "NetBox Development" Interested in contributing to NetBox? Check out our [GitHub repository](https://github.com/netbox-community/netbox) to get started! From 693ad700e83a49f8f9852bc4a952ed57811c10d4 Mon Sep 17 00:00:00 2001 From: Dorian Bourgeoisat <33571477+DorianXGH@users.noreply.github.com> Date: Fri, 12 Aug 2022 00:49:13 +0200 Subject: [PATCH 380/593] Swapping NG-PON2 as main name instead of TWDM-PON --- netbox/dcim/choices.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/netbox/dcim/choices.py b/netbox/dcim/choices.py index c91faf3da..1831ee8ae 100644 --- a/netbox/dcim/choices.py +++ b/netbox/dcim/choices.py @@ -818,7 +818,7 @@ class InterfaceTypeChoices(ChoiceSet): TYPE_GPON = 'gpon' TYPE_XG_PON = 'xg-pon' TYPE_XGS_PON = 'xgs-pon' - TYPE_TWDM_PON = 'twdm-pon' + TYPE_NG_PON2 = 'ng-pon2' TYPE_EPON = 'epon' TYPE_10G_EPON = '10g-epon' @@ -964,7 +964,7 @@ class InterfaceTypeChoices(ChoiceSet): (TYPE_GPON, 'GPON (2.5 Gbps / 1.25 Gps)'), (TYPE_XG_PON, 'XG-PON (10 Gbps / 2.5 Gbps)'), (TYPE_XGS_PON, 'XGS-PON (10 Gbps)'), - (TYPE_TWDM_PON, 'TWDM-PON (NG-PON2) (4x10 Gbps)'), + (TYPE_NG_PON2, 'NG-PON2 (TWDM-PON) (4x10 Gbps)'), (TYPE_EPON, 'EPON (1 Gbps)'), (TYPE_10G_EPON, '10G-EPON (10 Gbps)'), ) From c635f09e058877a462c7f8e8a44aa1fad1f0997e Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 12 Aug 2022 09:21:28 -0400 Subject: [PATCH 381/593] Closes #9994: Disable search indexing for local documentation builds --- docs/_theme/main.html | 9 +++++++++ mkdocs.yml | 2 ++ 2 files changed, 11 insertions(+) create mode 100644 docs/_theme/main.html diff --git a/docs/_theme/main.html b/docs/_theme/main.html new file mode 100644 index 000000000..4dfc4e14e --- /dev/null +++ b/docs/_theme/main.html @@ -0,0 +1,9 @@ +{% extends "base.html" %} + +{% block site_meta %} + {{ super() }} + {# Disable search indexing unless we're building for ReadTheDocs #} + {% if not config.extra.readthedocs %} + + {% endif %} +{% endblock %} diff --git a/mkdocs.yml b/mkdocs.yml index 34c65ed01..b13ab6c49 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -5,6 +5,7 @@ repo_name: netbox-community/netbox repo_url: https://github.com/netbox-community/netbox theme: name: material + custom_dir: docs/_theme/ icon: repo: fontawesome/brands/github palette: @@ -37,6 +38,7 @@ 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 From 41ad9b242c468fd3b868037320df7f4fd6123e0b Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 12 Aug 2022 10:12:01 -0400 Subject: [PATCH 382/593] Fixes #9986: Workaround for upstream timezone data bug --- docs/release-notes/version-3.2.md | 4 ++++ requirements.txt | 3 +++ 2 files changed, 7 insertions(+) diff --git a/docs/release-notes/version-3.2.md b/docs/release-notes/version-3.2.md index e57cea3b3..c572328bc 100644 --- a/docs/release-notes/version-3.2.md +++ b/docs/release-notes/version-3.2.md @@ -8,6 +8,10 @@ * [#9625](https://github.com/netbox-community/netbox/issues/9625) - Add phone & email details to contacts panel * [#9857](https://github.com/netbox-community/netbox/issues/9857) - Add clear button to quick search fields +### Bug Fixes + +* [#9986](https://github.com/netbox-community/netbox/issues/9986) - Workaround for upstream timezone data bug + --- ## v3.2.8 (2022-08-08) diff --git a/requirements.txt b/requirements.txt index 59bd1e8cd..a227971fa 100644 --- a/requirements.txt +++ b/requirements.txt @@ -34,3 +34,6 @@ tzdata==2022.1 # Workaround for #7401 jsonschema==3.2.0 + +# Workaround for #9986 +pytz==2022.1 From e4fa8af47f7ae449bc490d927e5ac329a419e79b Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 12 Aug 2022 10:48:16 -0400 Subject: [PATCH 383/593] Changelog for #8595 --- docs/release-notes/version-3.2.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes/version-3.2.md b/docs/release-notes/version-3.2.md index c572328bc..824e99778 100644 --- a/docs/release-notes/version-3.2.md +++ b/docs/release-notes/version-3.2.md @@ -4,6 +4,7 @@ ### Enhancements +* [#8595](https://github.com/netbox-community/netbox/issues/8595) - Add PON interface types * [#9161](https://github.com/netbox-community/netbox/issues/9161) - Pretty print JSON custom field data when editing * [#9625](https://github.com/netbox-community/netbox/issues/9625) - Add phone & email details to contacts panel * [#9857](https://github.com/netbox-community/netbox/issues/9857) - Add clear button to quick search fields From ca0b21bef5723aedfffaacd90df32a41e965a1cb Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 12 Aug 2022 11:25:03 -0400 Subject: [PATCH 384/593] Closes #9980: Use standard table controls template for device interfaces list --- .../device/inc/interface_table_controls.html | 11 +++++++ netbox/templates/dcim/device/interfaces.html | 29 +------------------ netbox/templates/inc/table_controls_htmx.html | 26 ++++++++--------- 3 files changed, 25 insertions(+), 41 deletions(-) create mode 100644 netbox/templates/dcim/device/inc/interface_table_controls.html diff --git a/netbox/templates/dcim/device/inc/interface_table_controls.html b/netbox/templates/dcim/device/inc/interface_table_controls.html new file mode 100644 index 000000000..14e552439 --- /dev/null +++ b/netbox/templates/dcim/device/inc/interface_table_controls.html @@ -0,0 +1,11 @@ +{% extends 'inc/table_controls_htmx.html' %} + +{% block extra_table_controls %} + + +{% endblock extra_table_controls %} diff --git a/netbox/templates/dcim/device/interfaces.html b/netbox/templates/dcim/device/interfaces.html index 79a9d6b6f..94e38dbf3 100644 --- a/netbox/templates/dcim/device/interfaces.html +++ b/netbox/templates/dcim/device/interfaces.html @@ -4,34 +4,7 @@ {% load static %} {% block content %} -
    -
    -
    - - -
    -
    -
    -
    - {% if request.user.is_authenticated %} - - {% endif %} - - -
    -
    -
    + {% include 'dcim/device/inc/interface_table_controls.html' with table_modal="DeviceInterfaceTable_config" %}
    {% csrf_token %} diff --git a/netbox/templates/inc/table_controls_htmx.html b/netbox/templates/inc/table_controls_htmx.html index 099ad537e..6d6926d5d 100644 --- a/netbox/templates/inc/table_controls_htmx.html +++ b/netbox/templates/inc/table_controls_htmx.html @@ -1,22 +1,22 @@ {% load helpers %} -
    -
    -
    +
    +
    +
    - +
    + {% block extra_table_controls %}{% endblock %}
    -
    +
    {% if request.user.is_authenticated and table_modal %} -
    - -
    +
    + +
    {% endif %}
    -
    \ No newline at end of file +
    From 642fe422c458d4e5f3409c827100c2f2ac60c920 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 12 Aug 2022 13:39:07 -0400 Subject: [PATCH 385/593] Touch up intro page --- docs/index.md | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/docs/index.md b/docs/index.md index d412d1a49..c233dedb7 100644 --- a/docs/index.md +++ b/docs/index.md @@ -2,16 +2,16 @@ # The Premiere Network Source of Truth -NetBox is the leading solution for modeling modern networks. By combining the traditional disciplines of IP address management (IPAM) and datacenter infrastructure management (DCIM) with powerful APIs and extensions, NetBox provides the ideal "source of truth" to power network automation. +NetBox is the leading solution for modeling and documenting modern networks. By combining the traditional disciplines of IP address management (IPAM) and datacenter infrastructure management (DCIM) with powerful APIs and extensions, NetBox provides the ideal "source of truth" to power network automation. Read on to discover why thousands of organizations worldwide put NetBox at the heart of their infrastructure. -## Built for Networks +## :material-server-network: Built for Networks -Unlike general-purpose CMDBs, NetBox has curated a data model which caters specifically to the needs of network engineers and operators. It delivers a wide assortment of object types carefully crafted to best serve the needs of network engineers and operators. These cover all facets of network design, from IP address managements to cabling to overlays and more: +Unlike general-purpose CMDBs, NetBox has curated a data model which caters specifically to the needs of network engineers and operators. It delivers a wide assortment of object types carefully crafted to best serve the needs of infrastructure design and documentation. These cover all facets of network technology, from IP address managements to cabling to overlays and more: -* Hierarchical regions, site groups, sites, and locations +* Hierarchical regions, sites, and locations * Racks, devices, and device components * Cables and wireless connections -* Power distribution +* Power distribution tracking * Data circuits and providers * Virtual machines and clusters * IP prefixes, ranges, and addresses @@ -20,11 +20,12 @@ Unlike general-purpose CMDBs, NetBox has curated a data model which caters speci * AS numbers * VLANs and scoped VLAN groups * L2VPN overlays -* Organizational tenants and contacts +* Tenancy assignments +* Contact management -## Customizable & Extensible +## :material-hammer-wrench: Customizable & Extensible -In addition to its expansive and robust data model, NetBox offers myriad mechanisms through which it can be customized and extended. +In addition to its expansive and robust data model, NetBox offers myriad mechanisms through which it can be customized and extended. Its powerful plugins architecture enables users to extend the application to meet their needs with minimal development effort. * Custom fields * Custom model validation @@ -33,16 +34,20 @@ In addition to its expansive and robust data model, NetBox offers myriad mechani * Plugins * REST & GraphQL APIs -## Always Open +## :material-lock-open: Always Open -Because NetBox is an open source application licensed under [Apache 2](https://www.apache.org/licenses/LICENSE-2.0.html), its entire code base is completely accessible to the end user, and there's never a risk of vendor lock-out. Additionally, NetBox development is an entirely public, community-driven process to which everyone can provide input. +Because NetBox is an open source application licensed under [Apache 2](https://www.apache.org/licenses/LICENSE-2.0.html), its entire code base is completely accessible to the end user, and there's never a risk of vendor lock-in. Additionally, NetBox development is an entirely public, community-driven process to which everyone can provide input. -## Getting Started +!!! tip "NetBox Development" + Interested in contributing to NetBox? Check out our [GitHub repository](https://github.com/netbox-community/netbox) to get started! + +## :material-language-python: Powered by Python + +NetBox is built on the enormously popular [Django](http://www.djangoproject.com/) framework for the Python programming language, already a favorite among network engineers. Users can leverage their existing skills coding Python tools to extend NetBox's already vast functionality via custom scripts and plugins. + +## :material-flag: Getting Started * Try out our [public demo](https://demo.netbox.dev/) if you want to jump right in * The [installation guide](./installation/index.md) will help you get your own deployment up and running * Or try the community [Docker image](https://github.com/netbox-community/netbox-docker) for a low-touch approach * [NetBox Cloud](https://www.getnetbox.io/) is a hosted solution offered by NS1 - -!!! tip "NetBox Development" - Interested in contributing to NetBox? Check out our [GitHub repository](https://github.com/netbox-community/netbox) to get started! From 8c5779c8642f5984fd101508d11b4d4e80a7d9f9 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 12 Aug 2022 14:11:58 -0400 Subject: [PATCH 386/593] Add feature docs for virtualization --- docs/features/virtualization.md | 26 ++++++++++++++++++++++++++ mkdocs.yml | 1 + 2 files changed, 27 insertions(+) create mode 100644 docs/features/virtualization.md diff --git a/docs/features/virtualization.md b/docs/features/virtualization.md new file mode 100644 index 000000000..8e1bf35ef --- /dev/null +++ b/docs/features/virtualization.md @@ -0,0 +1,26 @@ +# Virtualization + +Virtual machines and clusters can be modeled in NetBox alongside physical infrastructure. IP addresses and other resources are assigned to these objects just like physical objects, providing a seamless integration between physical and virtual networks. + +```mermaid +flowchart TD + ClusterGroup & ClusterType --> Cluster + Cluster --> VirtualMachine + Platform --> VirtualMachine + VirtualMachine --> VMInterface + +click Cluster "../../models/virtualization/cluster/" +click ClusterGroup "../../models/virtualization/clustergroup/" +click ClusterType "../../models/virtualization/clustertype/" +click Platform "../../models/dcim/platform/" +click VirtualMachine "../../models/virtualization/virtualmachine/" +click VMInterface "../../models/virtualization/vminterface/" +``` + +## Clusters + +A cluster is one or more physical host devices on which virtual machines can run. Each cluster must have a type and operational status, and may be assigned to a group. (Both types and groups are user-defined.) Each cluster may designate one or more devices as hosts, however this is optional. + +## Virtual Machines + +A virtual machine is a virtualized compute instance. These behave in NetBox very similarly to device objects, but without any physical attributes. For example, a VM may have interfaces assigned to it with IP addresses and VLANs, however its interfaces cannot be connected via cables (because they are virtual). Each VM may also define its compute, memory, and storage resources as well. diff --git a/mkdocs.yml b/mkdocs.yml index 9eea20397..be9c31651 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -70,6 +70,7 @@ nav: - L2VPN & Overlay: 'features/l2vpn-overlay.md' - Circuits: 'features/circuits.md' - Wireless: 'features/wireless.md' + - Virtualization: 'features/virtualization.md' - Tenancy: 'features/tenancy.md' - Contacts: 'features/contacts.md' - Context Data: 'features/context-data.md' From 9c667bb3af7fb8339acfd04ee586f98526ccf71e Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 12 Aug 2022 14:14:56 -0400 Subject: [PATCH 387/593] Add hyperlinks to Mermaid graphs --- docs/features/circuits.md | 6 ++++ docs/features/contacts.md | 4 +++ docs/features/devices-cabling.md | 8 ++++++ docs/features/facilities.md | 8 ++++++ docs/features/ipam.md | 9 +++++- docs/features/tenancy.md | 3 ++ docs/features/vlan-management.md | 4 +++ docs/features/wireless.md | 3 ++ docs/getting-started/planning.md | 49 ++++++++++++++++++++++++++++++-- 9 files changed, 91 insertions(+), 3 deletions(-) diff --git a/docs/features/circuits.md b/docs/features/circuits.md index f0c5832c4..7739efb4c 100644 --- a/docs/features/circuits.md +++ b/docs/features/circuits.md @@ -7,6 +7,12 @@ flowchart TD ASN --> Provider Provider --> ProviderNetwork & Circuit CircuitType --> Circuit + +click ASN "../../models/circuits/asn/" +click Circuit "../../models/circuits/circuit/" +click CircuitType "../../models/circuits/circuittype/" +click Provider "../../models/circuits/provider/" +click ProviderNetwork "../../models/circuits/providernetwork/" ``` ## Providers diff --git a/docs/features/contacts.md b/docs/features/contacts.md index 7d717b2de..6bbd217fc 100644 --- a/docs/features/contacts.md +++ b/docs/features/contacts.md @@ -7,6 +7,10 @@ flowchart TD ContactGroup --> ContactGroup & Contact ContactRole & Contact --> assignment([Assignment]) assignment --> Object + +click Contact "../../models/tenancy/contact/" +click ContactGroup "../../models/tenancy/contactgroup/" +click ContactRole "../../models/tenancy/contactrole/" ``` ## Contact Groups diff --git a/docs/features/devices-cabling.md b/docs/features/devices-cabling.md index 7297f79ac..bec3e56de 100644 --- a/docs/features/devices-cabling.md +++ b/docs/features/devices-cabling.md @@ -11,6 +11,14 @@ flowchart TD DeviceRole & Platform & DeviceType --> Device Device & ModuleType ---> Module Device & Module --> Interface & ConsolePort & PowerPort & ... + +click Device "../../models/dcim/device/" +click DeviceRole "../../models/dcim/devicerole/" +click DeviceType "../../models/dcim/devicetype/" +click Manufacturer "../../models/dcim/manufacturer/" +click Module "../../models/dcim/module/" +click ModuleType "../../models/dcim/moduletype/" +click Platform "../../models/dcim/platform/" ``` ## Manufacturers diff --git a/docs/features/facilities.md b/docs/features/facilities.md index f105bd4c5..84c7c5733 100644 --- a/docs/features/facilities.md +++ b/docs/features/facilities.md @@ -13,6 +13,14 @@ flowchart TD Rack --> Device Site --> Rack RackRole --> Rack + +click Device "../../models/dcim/device/" +click Location "../../models/dcim/location/" +click Rack "../../models/dcim/rack/" +click RackRole "../../models/dcim/rackrole/" +click Region "../../models/dcim/region/" +click Site "../../models/dcim/site/" +click SiteGroup "../../models/dcim/sitegroup/" ``` ## Regions diff --git a/docs/features/ipam.md b/docs/features/ipam.md index 45a6a0221..d67645b17 100644 --- a/docs/features/ipam.md +++ b/docs/features/ipam.md @@ -17,8 +17,15 @@ flowchart TD Aggregate & Role --> Prefix Prefix --> Prefix Prefix --> IPRange & IPAddress + +click Aggregate "../../models/ipam/aggregate/" +click IPAddress "../../models/ipam/ipaddress/" +click IPRange "../../models/ipam/iprange/" +click Prefix "../../models/ipam/prefix/" +click RIR "../../models/ipam/rir/" +click Role "../../models/ipam/role/" ``` - + !!! tip "Automatic Hierarchies" IP objects in NetBox never need to be manually assigned to the parent objects. The construction of hierarchies is handled automatically by the application according to the inherent rules of IP addressing. diff --git a/docs/features/tenancy.md b/docs/features/tenancy.md index fe6d8e5a8..a278dc4c2 100644 --- a/docs/features/tenancy.md +++ b/docs/features/tenancy.md @@ -6,6 +6,9 @@ Most core objects within NetBox's data model support _tenancy_. This is the asso flowchart TD TenantGroup --> TenantGroup & Tenant Tenant --> Site & Device & Prefix & Circuit & ... + +click Tenant "../../models/tenancy/tenant/" +click TenantGroup "../../models/tenancy/tenantgroup/" ``` ## Tenant Groups diff --git a/docs/features/vlan-management.md b/docs/features/vlan-management.md index 4af05dea3..c74c9015c 100644 --- a/docs/features/vlan-management.md +++ b/docs/features/vlan-management.md @@ -5,6 +5,10 @@ Complementing its IPAM capabilities, NetBox also tracks VLAN information to assi ```mermaid flowchart TD VLANGroup & Role --> VLAN + +click Role "../../models/ipam/role/" +click VLAN "../../models/ipam/vlan/" +click VLANGroup "../../models/ipam/vlangroup/" ``` ## VLAN Groups diff --git a/docs/features/wireless.md b/docs/features/wireless.md index 215d1a682..c78387efb 100644 --- a/docs/features/wireless.md +++ b/docs/features/wireless.md @@ -7,6 +7,9 @@ Just as NetBox provides robust modeling for physical cable plants, it also suppo ```mermaid flowchart TD WirelessLANGroup --> WirelessLANGroup & WirelessLAN + +click WirelessLAN "../../models/wireless/wirelesslan/" +click WirelessLANGroup "../../models/wireless/wirelesslangroup/" ``` A wireless LAN is a multi-access network shared by multiple wireless clients, identified by a common service set identifier (SSID) and authentication parameters. Wireless LANs can be organized into self-nesting groups, and each wireless LAN may optionally be bound to a particular VLAN. This allows easily mapping wireless networks to their wired counterparts. diff --git a/docs/getting-started/planning.md b/docs/getting-started/planning.md index 5c431a4d2..5dbe6e54e 100644 --- a/docs/getting-started/planning.md +++ b/docs/getting-started/planning.md @@ -78,9 +78,15 @@ The graphs below illustrate some of the core dependencies among different models ```mermaid flowchart TD - TenantGroup --> TenantGroup - TenantGroup --> Tenant + TenantGroup --> TenantGroup & Tenant Tenant --> Site & Device & Prefix & VLAN & ... + +click Device "../../models/dcim/device/" +click Prefix "../../models/ipam/prefix/" +click Site "../../models/dcim/site/" +click Tenant "../../models/tenancy/tenant/" +click TenantGroup "../../models/tenancy/tenantgroup/" +click VLAN "../../models/ipam/vlan/" ``` ### Sites, Racks, and Devices @@ -99,6 +105,21 @@ flowchart TD DeviceType --> Device Device & ModuleType ---> Module Device & Module --> Interface + +click Device "../../models/dcim/device/" +click DeviceRole "../../models/dcim/devicerole/" +click DeviceType "../../models/dcim/devicetype/" +click Interface "../../models/dcim/interface/" +click Location "../../models/dcim/location/" +click Manufacturer "../../models/dcim/manufacturer/" +click Module "../../models/dcim/module/" +click ModuleType "../../models/dcim/moduletype/" +click Platform "../../models/dcim/platform/" +click Rack "../../models/dcim/rack/" +click RackRole "../../models/dcim/rackrole/" +click Region "../../models/dcim/region/" +click Site "../../models/dcim/site/" +click SiteGroup "../../models/dcim/sitegroup/" ``` ### VRFs, Prefixes, IP Addresses, and VLANs @@ -112,6 +133,16 @@ flowchart TD Aggregate & VRF --> Prefix VRF --> IPRange & IPAddress Prefix --> VLAN & IPRange & IPAddress + +click Aggregate "../../models/ipam/aggregate/" +click IPAddress "../../models/ipam/ipaddress/" +click IPRange "../../models/ipam/iprange/" +click Prefix "../../models/ipam/prefix/" +click RIR "../../models/ipam/rir/" +click Role "../../models/ipam/role/" +click VLAN "../../models/ipam/vlan/" +click VLANGroup "../../models/ipam/vlangroup/" +click VRF "../../models/ipam/vrf/" ``` ### Circuits @@ -121,6 +152,12 @@ flowchart TD Provider & CircuitType --> Circuit Provider --> ProviderNetwork Circuit --> CircuitTermination + +click Circuit "../../models/circuits/circuit/" +click CircuitTermination "../../models/circuits/circuittermination/" +click CircuitType "../../models/circuits/circuittype/" +click Provider "../../models/circuits/provider/" +click ProviderNetwork "../../models/circuits/providernetwork/" ``` ### Clusters and Virtual Machines @@ -132,4 +169,12 @@ flowchart TD Site --> Cluster & VirtualMachine Device & Platform --> VirtualMachine VirtualMachine --> VMInterface + +click Cluster "../../models/virtualization/cluster/" +click ClusterGroup "../../models/virtualization/clustergroup/" +click ClusterType "../../models/virtualization/clustertype/" +click Device "../../models/dcim/device/" +click Platform "../../models/dcim/platform/" +click VirtualMachine "../../models/virtualization/virtualmachine/" +click VMInterface "../../models/virtualization/vminterface/" ``` From fafc464d932bed1a03aaba959286f1fe66ada931 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 12 Aug 2022 15:00:45 -0400 Subject: [PATCH 388/593] Refreshed circuits model documentation --- docs/models/circuits/circuit.md | 54 +++++++++++++++++----- docs/models/circuits/circuittermination.md | 42 +++++++++++++++-- docs/models/circuits/circuittype.md | 12 ++++- docs/models/circuits/provider.md | 39 +++++++++++++++- docs/models/circuits/providernetwork.md | 14 +++++- 5 files changed, 142 insertions(+), 19 deletions(-) diff --git a/docs/models/circuits/circuit.md b/docs/models/circuits/circuit.md index 3aaa4e99f..50637ab4e 100644 --- a/docs/models/circuits/circuit.md +++ b/docs/models/circuits/circuit.md @@ -1,19 +1,49 @@ # Circuits -A communications circuit represents a single _physical_ link connecting exactly two endpoints, commonly referred to as its A and Z terminations. A circuit in NetBox may have zero, one, or two terminations defined. It is common to have only one termination defined when you don't necessarily care about the details of the provider side of the circuit, e.g. for Internet access circuits. Both terminations would likely be modeled for circuits which connect one customer site to another. +A circuit represents a physical point-to-point data connection, typically used to interconnect sites across considerable distances (e.g. to deliver Internet connectivity). -Each circuit is associated with a provider and a user-defined type. For example, you might have Internet access circuits delivered to each site by one provider, and private MPLS circuits delivered by another. Each circuit must be assigned a circuit ID, each of which must be unique per provider. +## Fields -Each circuit is also assigned one of the following operational statuses: +### Provider -* Planned -* Provisioning -* Active -* Offline -* Deprovisioning -* Decommissioned +The [provider](./provider.md) to which this circuit belongs. -Circuits also have optional fields for annotating their installation and termination dates and commit rate, and may be assigned to NetBox tenants. +### Circuit ID -!!! note - NetBox currently models only physical circuits: those which have exactly two endpoints. It is common to layer virtualized constructs (_virtual circuits_) such as MPLS or EVPN tunnels on top of these, however NetBox does not yet support virtual circuit modeling. +An identifier for this circuit. This must be unique to the assigned provider. (Circuits assigned to different providers may have the same circuit ID.) + +### Circuit Type + +Each circuit is classified by a user-defined [circuit type](./circuittype.md). Generally this is something like "Internet access," "MPLS/VPN," etc. + +### Status + +The operational status of the circuit. By default, the following statuses are available: + +| Name | +|----------------| +| Planned | +| Provisioning | +| Active | +| Offline | +| Deprovisioning | +| Decommissioned | + +!!! tip "Custom circuit statuses" + Additional circuit statuses may be defined by setting `Circuit.status` under the [`FIELD_CHOICES`](../../configuration/data-validation.md#field_choices) configuration parameter. + +### Description + +A brief description of the circuit. + +### Installation Date + +The date on which the circuit was installed. + +### Termination Date + +The date on which the circuit is scheduled to be disconnected. + +### Commit Rate + +The committed rate (throughput) of the circuit, in kilobits per second. diff --git a/docs/models/circuits/circuittermination.md b/docs/models/circuits/circuittermination.md index beea2f85a..c6aa966d0 100644 --- a/docs/models/circuits/circuittermination.md +++ b/docs/models/circuits/circuittermination.md @@ -1,10 +1,46 @@ # Circuit Terminations -The association of a circuit with a particular site and/or device is modeled separately as a circuit termination. A circuit may have up to two terminations, labeled A and Z. A single-termination circuit can be used when you don't know (or care) about the far end of a circuit (for example, an Internet access circuit which connects to a transit provider). A dual-termination circuit is useful for tracking circuits which connect two sites. - -Each circuit termination is attached to either a site or to a provider network. Site terminations may optionally be connected via a cable to a specific device interface or port within that site. Each termination must be assigned a port speed, and can optionally be assigned an upstream speed if it differs from the downstream speed (a common scenario with e.g. DOCSIS cable modems). Fields are also available to track cross-connect and patch panel details. +Each circuit may have up to two terminations, designated A and Z. At either termination, a circuit may connect to a site, device interface (via a cable), or to a provider network. In adherence with NetBox's philosophy of closely modeling the real world, a circuit may be connected only to a physical interface. For example, circuits may not terminate to LAG interfaces, which are virtual in nature. In such cases, a separate physical circuit is associated with each LAG member interface and each needs to be modeled discretely. !!! note A circuit in NetBox represents a physical link, and cannot have more than two endpoints. When modeling a multi-point topology, each leg of the topology must be defined as a discrete circuit, with one end terminating within the provider's infrastructure. The provider network model is ideal for representing these networks. + +## Fields + +### Circuit + +The [circuit](./circuit.md) to which this termination belongs. + +### Termination Side + +Designates the termination as forming either the A or Z end of the circuit. + +### Mark Connected + +If selected, the circuit termination will be considered "connected" even if no cable has been connected to it in NetBox. + +### Site + +The [site](../dcim/site.md) with which this circuit termination is associated. Once created, a cable can be connected between the circuit termination and a device interface (or similar component). + +### Provider Network + +Circuits which do not connect to a site modeled by NetBox can instead be terminated to a [provider network](./providernetwork.md) representing an unknown network operated by a [provider](./provider.md). + +### Port Speed + +The operating speed of the terminated interface, in kilobits per second. This is useful for documenting the speed of a circuit when the actual interface to which it terminates is not being modeled in NetBox. + +### Upstream Speed + +The upstream speed of the terminated interface (in kilobits per second), if different from the downstream speed (a common scenario with e.g. DOCSIS cable modems). + +### Cross-connect ID + +In a data center environment, circuits are often delivered via a local cross-connect. While it may not be appropriate to model the cross-connect itself in NetBox, it's a good idea to record its ID for reference where applicable. + +### Patch Panel & Port(s) + +Similar to the cross-connect ID, this field can be used to track physical connection details which may be outside the scope of what is being modeled in NetBox. diff --git a/docs/models/circuits/circuittype.md b/docs/models/circuits/circuittype.md index aa8258e04..d0baa0f89 100644 --- a/docs/models/circuits/circuittype.md +++ b/docs/models/circuits/circuittype.md @@ -1,8 +1,18 @@ # Circuit Types -Circuits are classified by functional type. These types are completely customizable, and are typically used to convey the type of service being delivered over a circuit. For example, you might define circuit types for: +[Circuits](./circuit.md) are classified by functional type. These types are completely customizable, and are typically used to convey the type of service being delivered over a circuit. For example, you might define circuit types for: * Internet transit * Out-of-band connectivity * Peering * Private backhaul + +## Fields + +### Name + +A unique human-friendly name. + +### Slug + +A unique URL-friendly identifier. (This value can be used for filtering.) diff --git a/docs/models/circuits/provider.md b/docs/models/circuits/provider.md index e0847b72f..a4835199e 100644 --- a/docs/models/circuits/provider.md +++ b/docs/models/circuits/provider.md @@ -1,5 +1,40 @@ # Providers -A circuit provider is any entity which provides some form of connectivity of among sites or organizations within a site. While this obviously includes carriers which offer Internet and private transit service, it might also include Internet exchange (IX) points and even organizations with whom you peer directly. Each circuit within NetBox must be assigned a provider and a circuit ID which is unique to that provider. +A provider is any entity which provides some form of connectivity of among sites or organizations within a site. While this obviously includes carriers which offer Internet and private transit service, it might also include Internet exchange (IX) points and even organizations with whom you peer directly. Each [circuit](./circuit.md) within NetBox must be assigned a provider and a circuit ID which is unique to that provider. -Each provider may be assigned an autonomous system number (ASN), an account number, and contact information. +## Fields + +### Name + +A unique human-friendly name. + +### Slug + +A unique URL-friendly identifier. (This value can be used for filtering.) + +### ASN + +The AS number assigned to this provider. + +!!! warning "Legacy field" + This field is being removed in NetBox v3.4. Users are highly encouraged to use the [ASN model](../ipam/asn.md) to track AS number assignment for providers. + +### ASNs + +The [AS numbers](../ipam/asn.md) assigned to this provider (optional). + +### Account Number + +The administrative account identifier tied to this provider for your organization. + +### Portal URL + +The URL for the provider's customer service portal. + +### NOC Contact + +Contact details for the provider's network operations center (NOC). + +### Admin Contact + +Administrative contact details for the provider. diff --git a/docs/models/circuits/providernetwork.md b/docs/models/circuits/providernetwork.md index 42c46e13c..90db02573 100644 --- a/docs/models/circuits/providernetwork.md +++ b/docs/models/circuits/providernetwork.md @@ -2,4 +2,16 @@ This model can be used to represent the boundary of a provider network, the details of which are unknown or unimportant to the NetBox user. For example, it might represent a provider's regional MPLS network to which multiple circuits provide connectivity. -Each provider network must be assigned to a provider, and may optionally be assigned an arbitrary service ID. A circuit may terminate to either a provider network or to a site. +## Fields + +### Provider + +The [provider](./provider.md) responsible for the operation of this network. + +### Name + +A human-friendly name, unique to the provider. + +### Service ID + +An arbitrary identifier used as an alternate reference for the type of connectivity or service being delivered. From cff6b81f1fdfbc888e60e97be570262182d30fed Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 12 Aug 2022 16:00:04 -0400 Subject: [PATCH 389/593] Refreshed extras model documentation --- docs/features/context-data.md | 58 ++++++++++++++-- docs/features/customization.md | 12 ++++ docs/models/extras/configcontext.md | 68 ++++--------------- docs/models/extras/customfield.md | 2 + docs/models/extras/customlink.md | 2 + docs/models/extras/exporttemplate.md | 2 + docs/models/extras/imageattachment.md | 10 +++ docs/models/extras/tag.md | 18 ++--- docs/models/extras/webhook.md | 95 ++++++++++++++++++++++----- 9 files changed, 184 insertions(+), 83 deletions(-) diff --git a/docs/features/context-data.md b/docs/features/context-data.md index d202482da..04e795fd5 100644 --- a/docs/features/context-data.md +++ b/docs/features/context-data.md @@ -11,10 +11,6 @@ Configuration context data (or "config contexts" for short) is a powerful featur } ``` -While this is handy on its own, the real power of context data stems from its ability to be merged and overridden using multiple instances. For example, perhaps you need to define _different_ syslog servers within the region for a particular device role. You can create a second config context with the appropriate data and a higher weight, and apply it to the desired role. This will override the lower-weight data that applies to the entire region. As you can imagine, this flexibility can cater to many complex use cases. - -There are no restrictions around what data can be stored in a configuration context, so long as it can be expressed in JSON. Additionally, each device and VM may have local context data defined: This data is stored directly on the assigned object, and applies to it only. This is a convenient way for "attaching" miscellaneous data to a single device or VM as needed. - Config contexts can be computed for objects based on the following criteria: | Type | Devices | Virtual Machines | @@ -32,3 +28,57 @@ Config contexts can be computed for objects based on the following criteria: | Tenant group | :material-check: | :material-check: | | Tenant | :material-check: | :material-check: | | Tag | :material-check: | :material-check: | + +There are no restrictions around what data can be stored in a configuration context, so long as it can be expressed in JSON. + +## Hierarchical Rendering + +While this is handy on its own, the real power of context data stems from its ability to be merged and overridden using multiple instances. For example, perhaps you need to define _different_ syslog servers within the region for a particular device role. You can create a second config context with the appropriate data and a higher weight, and apply it to the desired role. This will override the lower-weight data that applies to the entire region. As you can imagine, this flexibility can cater to many complex use cases. + +For example, suppose we want to specify a set of syslog and NTP servers for all devices within a region. We could create a config context instance with a weight of 1000 assigned to the region, with the following JSON data: + +```json +{ + "ntp-servers": [ + "172.16.10.22", + "172.16.10.33" + ], + "syslog-servers": [ + "172.16.9.100", + "172.16.9.101" + ] +} +``` + +But suppose there's a problem at one particular site within this region preventing traffic from reaching the regional syslog server. Devices there need to use a local syslog server instead of the two defined above. We'll create a second config context assigned only to that site with a weight of 2000 and the following data: + +```json +{ + "syslog-servers": [ + "192.168.43.107" + ] +} +``` + +When the context data for a device at this site is rendered, the second, higher-weight data overwrite the first, resulting in the following: + +```json +{ + "ntp-servers": [ + "172.16.10.22", + "172.16.10.33" + ], + "syslog-servers": [ + "192.168.43.107" + ] +} +``` + +Data from the higher-weight context overwrites conflicting data from the lower-weight context, while the non-conflicting portion of the lower-weight context (the list of NTP servers) is preserved. + +## Local Context Data + +Devices and virtual machines may also have a local context data defined. This local context will _always_ take precedence over any separate config context objects which apply to the device/VM. This is useful in situations where we need to call out a specific deviation in the data for a particular object. + +!!! warning + If you find that you're routinely defining local context data for many individual devices or virtual machines, [custom fields](./customization.md#custom-fields) may offer a more effective solution. diff --git a/docs/features/customization.md b/docs/features/customization.md index d7f2cf40a..813914ae2 100644 --- a/docs/features/customization.md +++ b/docs/features/customization.md @@ -6,6 +6,18 @@ While NetBox strives to meet the needs of every network, the needs of users to c Most objects in NetBox can be assigned user-created tags to aid with organization and filtering. Tag values are completely arbitrary: They may be used to store data in key-value pairs, or they may be employed simply as labels against which objects can be filtered. Each tag can also be assigned a color for quicker differentiation in the user interface. +Objects can be filtered by the tags they have applied. For example, the following API request will retrieve all devices tagged as "monitored": + +```no-highlight +GET /api/dcim/devices/?tag=monitored +``` + +The `tag` filter can be specified multiple times to match only objects which have _all_ the specified tags assigned: + +```no-highlight +GET /api/dcim/devices/?tag=monitored&tag=deprecated +``` + ## Custom Fields While NetBox provides a rather extensive data model out of the box, the need may arise to store certain additional data associated with NetBox objects. For example, you might need to record the invoice ID alongside an installed device, or record an approving authority when creating a new IP prefix. NetBox administrators can create custom fields on built-in objects to meet these needs. diff --git a/docs/models/extras/configcontext.md b/docs/models/extras/configcontext.md index 08b5f4fd5..156b2d784 100644 --- a/docs/models/extras/configcontext.md +++ b/docs/models/extras/configcontext.md @@ -1,69 +1,27 @@ # Configuration Contexts -Sometimes it is desirable to associate additional data with a group of devices or virtual machines to aid in automated configuration. For example, you might want to associate a set of syslog servers for all devices within a particular region. Context data enables the association of extra user-defined data with devices and virtual machines grouped by one or more of the following assignments: +Context data is made available to [devices](../dcim/device.md) and/or [virtual machines](../virtualization/virtualmachine.md) based on their relationships to other objects in NetBox. For example, context data can be associated only with devices assigned to a particular site, or only to virtual machines in a certain cluster. -* Region -* Site group -* Site -* Location (devices only) -* Device type (devices only) -* Role -* Platform -* Cluster type (VMs only) -* Cluster group (VMs only) -* Cluster (VMs only) -* Tenant group -* Tenant -* Tag +See the [context data documentation](../../features/context-data.md) for more information. -## Hierarchical Rendering +## Fields -Context data is arranged hierarchically, so that data with a higher weight can be entered to override lower-weight data. Multiple instances of data are automatically merged by NetBox to present a single dictionary for each object. +### Name -For example, suppose we want to specify a set of syslog and NTP servers for all devices within a region. We could create a config context instance with a weight of 1000 assigned to the region, with the following JSON data: +A unique human-friendly name. -```json -{ - "ntp-servers": [ - "172.16.10.22", - "172.16.10.33" - ], - "syslog-servers": [ - "172.16.9.100", - "172.16.9.101" - ] -} -``` +### Weight -But suppose there's a problem at one particular site within this region preventing traffic from reaching the regional syslog server. Devices there need to use a local syslog server instead of the two defined above. We'll create a second config context assigned only to that site with a weight of 2000 and the following data: +A numeric value which influences the order in which context data is merged. Contexts with a lower weight are merged before those with a higher weight. -```json -{ - "syslog-servers": [ - "192.168.43.107" - ] -} -``` +### Data -When the context data for a device at this site is rendered, the second, higher-weight data overwrite the first, resulting in the following: +The context data expressed in JSON format. -```json -{ - "ntp-servers": [ - "172.16.10.22", - "172.16.10.33" - ], - "syslog-servers": [ - "192.168.43.107" - ] -} -``` +### Is Active -Data from the higher-weight context overwrites conflicting data from the lower-weight context, while the non-conflicting portion of the lower-weight context (the list of NTP servers) is preserved. +If not selected, this config context will be excluded from rendering. This can be convenient to temporarily disable a config context. -## Local Context Data +### Object Assignment -Devices and virtual machines may also have a local config context defined. This local context will _always_ take precedence over any separate config context objects which apply to the device/VM. This is useful in situations where we need to call out a specific deviation in the data for a particular object. - -!!! warning - If you find that you're routinely defining local context data for many individual devices or virtual machines, custom fields may offer a more effective solution. +Each configuration context may be assigned with any number of objects of the supported types. If no related objects are selected, it will be considered a "global" config context and apply to all devices and virtual machines. diff --git a/docs/models/extras/customfield.md b/docs/models/extras/customfield.md index 0b2bc29f9..ee1fde2b7 100644 --- a/docs/models/extras/customfield.md +++ b/docs/models/extras/customfield.md @@ -1,5 +1,7 @@ # Custom Fields +NetBox administrators can extend NetBox's built-in data model by adding custom fields to most object types. See the [custom fields documentation](../../customization/custom-fields.md) for more information. + ## Fields ### Model(s) diff --git a/docs/models/extras/customlink.md b/docs/models/extras/customlink.md index 458007ab1..dae9978aa 100644 --- a/docs/models/extras/customlink.md +++ b/docs/models/extras/customlink.md @@ -1,5 +1,7 @@ # Custom Links +Users can add custom links to object views in NetBox to reference external resources. For example, you might create a custom link for devices pointing to a monitoring system. See the [custom links documentation](../../customization/custom-links.md) for more information. + ## Fields ### Name diff --git a/docs/models/extras/exporttemplate.md b/docs/models/extras/exporttemplate.md index 347184324..3215201b3 100644 --- a/docs/models/extras/exporttemplate.md +++ b/docs/models/extras/exporttemplate.md @@ -1,5 +1,7 @@ # Export Templates +Export templates are used to render arbitrary data from a set of NetBox objects. For example, you might want to automatically generate a network monitoring service configuration from a list of device objects. See the [export templates documentation](../../customization/export-templates.md) for more information. + ## Fields ### Name diff --git a/docs/models/extras/imageattachment.md b/docs/models/extras/imageattachment.md index da15462ab..b8c48f055 100644 --- a/docs/models/extras/imageattachment.md +++ b/docs/models/extras/imageattachment.md @@ -1,3 +1,13 @@ # Image Attachments Certain objects in NetBox support the attachment of uploaded images. These will be saved to the NetBox server and made available whenever the object is viewed. + +## Fields + +### Name + +The name of the image being attached. If not defined, this will be inferred from the name of the uploaded file. + +### Image + +The image file to upload. Note that the uploaded file **must** be a supported image type, or validation will fail. diff --git a/docs/models/extras/tag.md b/docs/models/extras/tag.md index fe6a1ef36..97ebd9d72 100644 --- a/docs/models/extras/tag.md +++ b/docs/models/extras/tag.md @@ -2,16 +2,16 @@ Tags are user-defined labels which can be applied to a variety of objects within NetBox. They can be used to establish dimensions of organization beyond the relationships built into NetBox. For example, you might create a tag to identify a particular ownership or condition across several types of objects. -Each tag has a label, color, and a URL-friendly slug. For example, the slug for a tag named "Dunder Mifflin, Inc." would be `dunder-mifflin-inc`. The slug is generated automatically and makes tags easier to work with as URL parameters. Each tag can also be assigned a description indicating its purpose. +## Fields -Objects can be filtered by the tags they have applied. For example, the following API request will retrieve all devices tagged as "monitored": +### Name -```no-highlight -GET /api/dcim/devices/?tag=monitored -``` +A unique human-friendly label for the tag. -The `tag` filter can be specified multiple times to match only objects which have _all_ of the specified tags assigned: +### Slug -```no-highlight -GET /api/dcim/devices/?tag=monitored&tag=deprecated -``` +A unique URL-friendly identifier. (This value will be used for filtering.) This is automatically generated from the tag's name, but can be altered as needed. + +### Color + +The color to use when displaying the tag in the NetBox UI. diff --git a/docs/models/extras/webhook.md b/docs/models/extras/webhook.md index 2d598f8ae..1ca6ec191 100644 --- a/docs/models/extras/webhook.md +++ b/docs/models/extras/webhook.md @@ -1,19 +1,84 @@ # Webhooks -A webhook is a mechanism for conveying to some external system a change that took place in NetBox. For example, you may want to notify a monitoring system whenever the status of a device is updated in NetBox. This can be done by creating a webhook for the device model in NetBox and identifying the webhook receiver. When NetBox detects a change to a device, an HTTP request containing the details of the change and who made it be sent to the specified receiver. Webhooks are managed under Logging > Webhooks. +A webhook is a mechanism for conveying to some external system a change that took place in NetBox. For example, you may want to notify a monitoring system whenever the status of a device is updated in NetBox. This can be done by creating a webhook for the device model in NetBox and identifying the webhook receiver. When NetBox detects a change to a device, an HTTP request containing the details of the change and who made it be sent to the specified receiver. -## Model Fields +See the [webhooks documentation](../../integrations/webhooks.md) for more information. -* **Name** - A unique name for the webhook. The name is not included with outbound messages. -* **Object type(s)** - The type or types of NetBox object that will trigger the webhook. -* **Enabled** - If unchecked, the webhook will be inactive. -* **Events** - A webhook may trigger on any combination of create, update, and delete events. At least one event type must be selected. -* **HTTP method** - The type of HTTP request to send. Options include `GET`, `POST`, `PUT`, `PATCH`, and `DELETE`. -* **URL** - The fully-qualified URL of the request to be sent. This may specify a destination port number if needed. Jinja2 templating is supported for this field. -* **HTTP content type** - The value of the request's `Content-Type` header. (Defaults to `application/json`) -* **Additional headers** - Any additional headers to include with the request (optional). Add one header per line in the format `Name: Value`. Jinja2 templating is supported for this field (see below). -* **Body template** - The content of the request being sent (optional). Jinja2 templating is supported for this field (see below). If blank, NetBox will populate the request body with a raw dump of the webhook context. (If the HTTP cotent type is set to `application/json`, this will be formatted as a JSON object.) -* **Secret** - A secret string used to prove authenticity of the request (optional). This will append a `X-Hook-Signature` header to the request, consisting of a HMAC (SHA-512) hex digest of the request body using the secret as the key. -* **Conditions** - An optional set of conditions evaluated to determine whether the webhook fires for a given object. -* **SSL verification** - Uncheck this option to disable validation of the receiver's SSL certificate. (Disable with caution!) -* **CA file path** - The file path to a particular certificate authority (CA) file to use when validating the receiver's SSL certificate (optional). +## Fields + +### Name + +A unique human-friendly name. + +### Content Types + +The type(s) of object in NetBox that will trigger the webhook. + +### Enabled + +If not selected, the webhook will be inactive. + +### Events + +The events which will trigger the webhook. At least one event type must be selected. + +| Name | Description | +|-----------|--------------------------------------| +| Creations | A new object has been created | +| Updates | An existing object has been modified | +| Deletions | An object has been deleted | + +### URL + +The URL to which the webhook HTTP request will be made. + +### HTTP Method + +The type of HTTP request to send. Options are: + +* `GET` +* `POST` +* `PUT` +* `PATCH` +* `DELETE` + +### HTTP Content Type + +The content type to indicate in the outgoing HTTP request header. See [this list](https://www.iana.org/assignments/media-types/media-types.xhtml) of known types for reference. + +### Additional Headers + +Any additional header to include with the outgoing HTTP request. These should be defined in the format `Name: Value`, with each header on a separate line. Jinja2 templating is supported for this field. + +### Body Template + +Jinja2 template for a custom request body, if desired. If not defined, NetBox will populate the request body with a raw dump of the webhook context. + +### Secret + +A secret string used to prove authenticity of the request (optional). This will append a `X-Hook-Signature` header to the request, consisting of a HMAC (SHA-512) hex digest of the request body using the secret as the key. + +### SSL Verification + +Controls whether validation of the receiver's SSL certificate is enforced when HTTPS is used. + +!!! warning + Disabling this can expose your webhooks to man-in-the-middle attacks. + +### CA File Path + +The file path to a particular certificate authority (CA) file to use when validating the receiver's SSL certificate (if not using the system defaults). + +## Context Data + +The following context variables are available in to the text and link templates. + +| Variable | Description | +|--------------|----------------------------------------------------| +| `event` | The event type (`create`, `update`, or `delete`) | +| `timestamp` | The time at which the event occured | +| `model` | The type of object impacted | +| `username` | The name of the user associated with the change | +| `request_id` | The unique request ID | +| `data` | A complete serialized representation of the object | +| `snapshots` | Pre- and post-change snapshots of the object | From 040feddcf1ee29c65c1a0515d23b3412cf89b217 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 12 Aug 2022 16:19:34 -0400 Subject: [PATCH 390/593] Refreshed tenancy model documentation --- docs/features/contacts.md | 17 ++++++++++ docs/features/tenancy.md | 18 ++++++++++- docs/models/tenancy/contact.md | 48 +++++++++++++++-------------- docs/models/tenancy/contactgroup.md | 16 +++++++++- docs/models/tenancy/contactrole.md | 12 +++++++- docs/models/tenancy/tenant.md | 26 ++++++++-------- docs/models/tenancy/tenantgroup.md | 16 +++++++++- 7 files changed, 113 insertions(+), 40 deletions(-) diff --git a/docs/features/contacts.md b/docs/features/contacts.md index 6bbd217fc..40e8dd12c 100644 --- a/docs/features/contacts.md +++ b/docs/features/contacts.md @@ -26,3 +26,20 @@ A contact role defines the relationship of a contact to an assigned object. For A contact should represent an individual or permanent point of contact. Each contact must define a name, and may optionally include a title, phone number, email address, and related details. Contacts are reused for assignments, so each unique contact must be created only once and can be assigned to any number of NetBox objects, and there is no limit to the number of assigned contacts an object may have. Most core objects in NetBox can have contacts assigned to them. + +The following models support the assignment of contacts: + +* circuits.Circuit +* circuits.Provider +* dcim.Device +* dcim.Location +* dcim.Manufacturer +* dcim.PowerPanel +* dcim.Rack +* dcim.Region +* dcim.Site +* dcim.SiteGroup +* tenancy.Tenant +* virtualization.Cluster +* virtualization.ClusterGroup +* virtualization.VirtualMachine diff --git a/docs/features/tenancy.md b/docs/features/tenancy.md index a278dc4c2..470905f20 100644 --- a/docs/features/tenancy.md +++ b/docs/features/tenancy.md @@ -13,10 +13,26 @@ click TenantGroup "../../models/tenancy/tenantgroup/" ## Tenant Groups -Tenants can be grouped by any logic that your use case demands, and groups can nested recursively for maximum flexibility. For example, You might define a parent "Customers" group with child groups "Current" and "Past" within it. A tenant can be assigned to a group at any level within the hierarchy. +Tenants can be grouped by any logic that your use case demands, and groups can be nested recursively for maximum flexibility. For example, You might define a parent "Customers" group with child groups "Current" and "Past" within it. A tenant can be assigned to a group at any level within the hierarchy. ## Tenants Typically, the tenant model is used to represent a customer or internal organization, however it can be used for whatever purpose meets your needs. Most core objects within NetBox can be assigned to particular tenant, so this model provides a very convenient way to correlate ownership across object types. For example, each of your customers might have its own racks, devices, IP addresses, circuits and so on: These can all be easily tracked via tenant assignment. + +The following objects can be assigned to tenants: + +* Sites +* Racks +* Rack reservations +* Devices +* VRFs +* Prefixes +* IP addresses +* VLANs +* Circuits +* Clusters +* Virtual machines + +Tenant assignment is used to signify the ownership of an object in NetBox. As such, each object may only be owned by a single tenant. For example, if you have a firewall dedicated to a particular customer, you would assign it to the tenant which represents that customer. However, if the firewall serves multiple customers, it doesn't *belong* to any particular customer, so tenant assignment would not be appropriate. diff --git a/docs/models/tenancy/contact.md b/docs/models/tenancy/contact.md index 9d81e2d85..eac630180 100644 --- a/docs/models/tenancy/contact.md +++ b/docs/models/tenancy/contact.md @@ -1,31 +1,33 @@ # Contacts -A contact represent an individual or group that has been associated with an object in NetBox for administrative reasons. For example, you might assign one or more operational contacts to each site. Contacts can be arranged within nested contact groups. +A contact represents an individual or group that has been associated with an object in NetBox for administrative reasons. For example, you might assign one or more operational contacts to each site. -Each contact must include a name, which is unique to its parent group (if any). The following optional descriptors are also available: +## Fields -* Title -* Phone -* Email -* Address +### Group -## Contact Assignment +The [contact group](./contactgroup.md) to which this contact is assigned (if any). -Each contact can be assigned to one or more objects, allowing for the efficient reuse of contact information. When assigning a contact to an object, the user may optionally specify a role and/or priority (primary, secondary, tertiary, or inactive) to better convey the nature of the contact's relationship to the assigned object. +### Name -The following models support the assignment of contacts: +The name of the contact. This may be an individual or a team/department. (This is the only required contact detail; all others are optional.) -* circuits.Circuit -* circuits.Provider -* dcim.Device -* dcim.Location -* dcim.Manufacturer -* dcim.PowerPanel -* dcim.Rack -* dcim.Region -* dcim.Site -* dcim.SiteGroup -* tenancy.Tenant -* virtualization.Cluster -* virtualization.ClusterGroup -* virtualization.VirtualMachine +### Title + +The contact's title or role. + +### Phone + +The contact's phone number. (Note that NetBox does _not_ enforce a particular numbering format.) + +### Email + +The contact's email address. + +### Address + +The contact's physical or mailing address. + +### Link + +A URL to reach the contact via some other means. diff --git a/docs/models/tenancy/contactgroup.md b/docs/models/tenancy/contactgroup.md index ea566c58a..49efb7c18 100644 --- a/docs/models/tenancy/contactgroup.md +++ b/docs/models/tenancy/contactgroup.md @@ -1,3 +1,17 @@ # Contact Groups -Contacts can be organized into arbitrary groups. These groups can be recursively nested for convenience. Each contact within a group must have a unique name, but other attributes can be repeated. +[Contacts](./contact.md) can be organized into arbitrary groups. These groups can be recursively nested for convenience. Each contact within a group must have a unique name, but other attributes can be repeated. + +## Fields + +### Parent + +The parent contact group (if any). + +### Name + +A unique human-friendly name. + +### Slug + +A unique URL-friendly identifier. (This value can be used for filtering.) diff --git a/docs/models/tenancy/contactrole.md b/docs/models/tenancy/contactrole.md index 23642ca03..bc8e1c802 100644 --- a/docs/models/tenancy/contactrole.md +++ b/docs/models/tenancy/contactrole.md @@ -1,3 +1,13 @@ # Contact Roles -Contacts can be organized by functional roles, which are fully customizable by the user. For example, you might create roles for administrative, operational, or emergency contacts. +[Contacts](./contact.md) can be organized by functional roles, which are fully customizable by the user. For example, you might create roles for administrative, operational, or emergency contacts. + +## Fields + +### Name + +A unique human-friendly name. + +### Slug + +A unique URL-friendly identifier. (This value can be used for filtering.) diff --git a/docs/models/tenancy/tenant.md b/docs/models/tenancy/tenant.md index 60a160b9e..7df6992d1 100644 --- a/docs/models/tenancy/tenant.md +++ b/docs/models/tenancy/tenant.md @@ -1,17 +1,17 @@ # Tenants -A tenant represents a discrete grouping of resources used for administrative purposes. Typically, tenants are used to represent individual customers or internal departments within an organization. The following objects can be assigned to tenants: +A tenant represents a discrete grouping of resources used for administrative purposes. Typically, tenants are used to represent individual customers or internal departments within an organization. -* Sites -* Racks -* Rack reservations -* Devices -* VRFs -* Prefixes -* IP addresses -* VLANs -* Circuits -* Clusters -* Virtual machines +## Fields -Tenant assignment is used to signify the ownership of an object in NetBox. As such, each object may only be owned by a single tenant. For example, if you have a firewall dedicated to a particular customer, you would assign it to the tenant which represents that customer. However, if the firewall serves multiple customers, it doesn't *belong* to any particular customer, so tenant assignment would not be appropriate. +### Name + +A unique human-friendly name. + +### Slug + +A unique URL-friendly identifier. (This value can be used for filtering.) + +### Group + +The [tenant group](./tenantgroup.md) to which this tenant belongs (if any). diff --git a/docs/models/tenancy/tenantgroup.md b/docs/models/tenancy/tenantgroup.md index 078a71a72..f9929ff66 100644 --- a/docs/models/tenancy/tenantgroup.md +++ b/docs/models/tenancy/tenantgroup.md @@ -1,5 +1,19 @@ # Tenant Groups -Tenants can be organized by custom groups. For instance, you might create one group called "Customers" and one called "Departments." The assignment of a tenant to a group is optional. +[Tenants](./tenant.md) can be organized by custom groups. For instance, you might create one group called "Customers" and one called "Departments." The assignment of a tenant to a group is optional. Tenant groups may be nested recursively to achieve a multi-level hierarchy. For example, you might have a group called "Customers" containing subgroups of individual tenants grouped by product or account team. + +## Fields + +### Parent + +The parent tenant group (if any). + +### Name + +A unique human-friendly name. + +### Slug + +A unique URL-friendly identifier. (This value can be used for filtering.) From d4f976ac8dd2cd39f4e41520c003f77a7eac6210 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 12 Aug 2022 16:37:47 -0400 Subject: [PATCH 391/593] Refreshed wireless model documentation --- docs/models/wireless/wirelesslan.md | 39 +++++++++++++++++---- docs/models/wireless/wirelesslangroup.md | 16 ++++++++- docs/models/wireless/wirelesslink.md | 43 +++++++++++++++++++++--- 3 files changed, 86 insertions(+), 12 deletions(-) diff --git a/docs/models/wireless/wirelesslan.md b/docs/models/wireless/wirelesslan.md index cb478664c..5bb3dbd65 100644 --- a/docs/models/wireless/wirelesslan.md +++ b/docs/models/wireless/wirelesslan.md @@ -1,11 +1,38 @@ # Wireless LANs -A wireless LAN is a set of interfaces connected via a common wireless channel. Each instance must have an SSID, and may optionally be correlated to a VLAN. Wireless LANs can be arranged into hierarchical groups, and each may be associated with a particular tenant. +A wireless LAN is a set of interfaces connected via a common wireless channel, identified by its SSID and authentication parameters. Wireless [interfaces](../dcim/interface.md) can be associated with wireless LANs to model multi-acess wireless segments. -An interface may be attached to multiple wireless LANs, provided they are all operating on the same channel. Only wireless interfaces may be attached to wireless LANs. +## Fields -Each wireless LAN may have authentication attributes associated with it, including: +### SSID -* Authentication type -* Cipher -* Pre-shared key +The service set identifier (SSID) for the wireless network. + +### Group + +The [wireless LAN group](./wirelesslangroup.md) to which this wireless LAN is assigned (if any). + +### VLAN + +Each wireless LAN can optionally be mapped to a [VLAN](../ipam/vlan.md), to model a bridge between wired and wireless segments. + +### Authentication Type + +The type of wireless authentication in use. Options include: + +* Open +* WEP +* WPA Personal (PSK) +* WPA Enterprise + +### Authentication Cipher + +The security cipher used to apply wireless authentication. Options include: + +* Auto (automatic) +* TKIP +* AES + +### Pre-Shared Key + +The security key configured on each client to grant access to the secured wireless LAN. This applies only to certain authentication types. diff --git a/docs/models/wireless/wirelesslangroup.md b/docs/models/wireless/wirelesslangroup.md index e477abd0b..37871f208 100644 --- a/docs/models/wireless/wirelesslangroup.md +++ b/docs/models/wireless/wirelesslangroup.md @@ -1,3 +1,17 @@ # Wireless LAN Groups -Wireless LAN groups can be used to organize and classify wireless LANs. These groups are hierarchical: groups can be nested within parent groups. However, each wireless LAN may assigned only to one group. +Wireless LAN groups can be used to organize and classify [wireless LANs](./wirelesslan.md). These groups are hierarchical: groups can be nested within parent groups. However, each wireless LAN may be assigned only to one group. + +## Fields + +### Parent + +The parent wireless LAN group (if any). + +### Name + +A unique human-friendly name. + +### Slug + +A unique URL-friendly identifier. (This value can be used for filtering.) diff --git a/docs/models/wireless/wirelesslink.md b/docs/models/wireless/wirelesslink.md index f52dc7191..c9b331570 100644 --- a/docs/models/wireless/wirelesslink.md +++ b/docs/models/wireless/wirelesslink.md @@ -1,9 +1,42 @@ # Wireless Links -A wireless link represents a connection between exactly two wireless interfaces. It may optionally be assigned an SSID and a description. It may also have a status assigned to it, similar to the cable model. Each wireless link may also be assigned to a particular tenant. +A wireless link represents a connection between exactly two wireless interfaces. Unlike a [wireless LAN](./wirelesslan.md), which permit an arbitrary number of client associations, wireless links are used to model point-to-point wireless connections. -Each wireless link may have authentication attributes associated with it, including: +## Fields -* Authentication type -* Cipher -* Pre-shared key +### Interfaces + +Select two interfaces: One for side A and one for side B. (Both must be wireless interfaces.) + +### Status + +The operational status of the link. Options include: + +* Connected +* Planned +* Decommissioning + +### SSID + +The service set identifier (SSID) for the wireless link (optional). + +### Authentication Type + +The type of wireless authentication in use. Options include: + +* Open +* WEP +* WPA Personal (PSK) +* WPA Enterprise + +### Authentication Cipher + +The security cipher used to apply wireless authentication. Options include: + +* Auto (automatic) +* TKIP +* AES + +### Pre-Shared Key + +The security key configured on each client to grant access to the secured wireless LAN. This applies only to certain authentication types. From f942216f3f4931c0d1e835948478e976172713ab Mon Sep 17 00:00:00 2001 From: Christoph Schneider Date: Sat, 13 Aug 2022 13:54:38 +0200 Subject: [PATCH 392/593] re-enable markup in longtext custom columns --- netbox/netbox/tables/columns.py | 10 ++++++++++ netbox/netbox/tables/tables.py | 3 ++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/netbox/netbox/tables/columns.py b/netbox/netbox/tables/columns.py index cc20bdd0c..8b961219f 100644 --- a/netbox/netbox/tables/columns.py +++ b/netbox/netbox/tables/columns.py @@ -418,6 +418,14 @@ class CustomFieldColumn(tables.Column): """ Display custom fields in the appropriate format. """ + template_code = """ + {% if value %} + {{ value|markdown }} + {% else %} + — + {% endif %} + """ + def __init__(self, customfield, *args, **kwargs): self.customfield = customfield kwargs['accessor'] = Accessor(f'custom_field_data__{customfield.name}') @@ -445,6 +453,8 @@ class CustomFieldColumn(tables.Column): return mark_safe(', '.join( self._likify_item(obj) for obj in self.customfield.deserialize(value) )) + if self.customfield.type == CustomFieldTypeChoices.TYPE_LONGTEXT: + return Template(self.template_code).render(Context({"value": value})) if value is not None: obj = self.customfield.deserialize(value) return mark_safe(self._likify_item(obj)) diff --git a/netbox/netbox/tables/tables.py b/netbox/netbox/tables/tables.py index d55038c4d..deb0d8b90 100644 --- a/netbox/netbox/tables/tables.py +++ b/netbox/netbox/tables/tables.py @@ -181,7 +181,8 @@ class NetBoxTable(BaseTable): content_type = ContentType.objects.get_for_model(self._meta.model) custom_fields = CustomField.objects.filter(content_types=content_type) extra_columns.extend([ - (f'cf_{cf.name}', columns.CustomFieldMarkdownColumn(cf) if cf.type == CustomFieldTypeChoices.TYPE_LONGTEXT else columns.CustomFieldColumn(cf)) for cf in custom_fields + # (f'cf_{cf.name}', columns.CustomFieldMarkdownColumn(cf) if cf.type == CustomFieldTypeChoices.TYPE_LONGTEXT else columns.CustomFieldColumn(cf)) for cf in custom_fields + (f'cf_{cf.name}', columns.CustomFieldColumn(cf)) for cf in custom_fields ]) custom_links = CustomLink.objects.filter(content_type=content_type, enabled=True) extra_columns.extend([ From 6f09d94e2a99330ecd09e7d3ee0600fba8386716 Mon Sep 17 00:00:00 2001 From: Christoph Schneider Date: Sat, 13 Aug 2022 13:56:51 +0200 Subject: [PATCH 393/593] remove commented line --- netbox/netbox/tables/tables.py | 1 - 1 file changed, 1 deletion(-) diff --git a/netbox/netbox/tables/tables.py b/netbox/netbox/tables/tables.py index deb0d8b90..63aae1d19 100644 --- a/netbox/netbox/tables/tables.py +++ b/netbox/netbox/tables/tables.py @@ -181,7 +181,6 @@ class NetBoxTable(BaseTable): content_type = ContentType.objects.get_for_model(self._meta.model) custom_fields = CustomField.objects.filter(content_types=content_type) extra_columns.extend([ - # (f'cf_{cf.name}', columns.CustomFieldMarkdownColumn(cf) if cf.type == CustomFieldTypeChoices.TYPE_LONGTEXT else columns.CustomFieldColumn(cf)) for cf in custom_fields (f'cf_{cf.name}', columns.CustomFieldColumn(cf)) for cf in custom_fields ]) custom_links = CustomLink.objects.filter(content_type=content_type, enabled=True) From ac540b6183bd5bf8db57b1941de24aafc7b04f3c Mon Sep 17 00:00:00 2001 From: Christoph Schneider Date: Sat, 13 Aug 2022 13:59:19 +0200 Subject: [PATCH 394/593] remove import --- netbox/netbox/tables/tables.py | 1 - 1 file changed, 1 deletion(-) diff --git a/netbox/netbox/tables/tables.py b/netbox/netbox/tables/tables.py index 63aae1d19..8c5fb039c 100644 --- a/netbox/netbox/tables/tables.py +++ b/netbox/netbox/tables/tables.py @@ -7,7 +7,6 @@ from django.db.models.fields.related import RelatedField from django_tables2.data import TableQuerysetData from extras.models import CustomField, CustomLink -from extras.choices import CustomFieldTypeChoices from netbox.tables import columns from utilities.paginator import EnhancedPaginator, get_paginate_count From 36491b13d86b26a0e75d84b8b5db64403f3fbe89 Mon Sep 17 00:00:00 2001 From: Christoph Schneider Date: Sat, 13 Aug 2022 14:01:07 +0200 Subject: [PATCH 395/593] remove class definition --- netbox/netbox/tables/columns.py | 27 +-------------------------- 1 file changed, 1 insertion(+), 26 deletions(-) diff --git a/netbox/netbox/tables/columns.py b/netbox/netbox/tables/columns.py index 8b961219f..4918dd3b9 100644 --- a/netbox/netbox/tables/columns.py +++ b/netbox/netbox/tables/columns.py @@ -559,29 +559,4 @@ class MarkdownColumn(tables.TemplateColumn): ) def value(self, value): - return value - - -class CustomFieldMarkdownColumn(tables.TemplateColumn): - """ - Render a Markdown string in a longtext custom column. - """ - template_code = """ - {% if value %} - {{ value|markdown }} - {% else %} - — - {% endif %} - """ - - def __init__(self, customfield, *args, **kwargs): - self.customfield = customfield - kwargs['accessor'] = Accessor(f'custom_field_data__{customfield.name}') - kwargs['template_code'] = self.template_code - if 'verbose_name' not in kwargs: - kwargs['verbose_name'] = customfield.label or customfield.name - - super().__init__(*args, **kwargs) - - def value(self, value): - return value + return value \ No newline at end of file From 15f4b1fd5daa5c8dd4338352d37049b47e4bf7de Mon Sep 17 00:00:00 2001 From: Christoph Schneider Date: Sat, 13 Aug 2022 14:02:26 +0200 Subject: [PATCH 396/593] add newline --- netbox/netbox/tables/columns.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/netbox/tables/columns.py b/netbox/netbox/tables/columns.py index 4918dd3b9..113c8416b 100644 --- a/netbox/netbox/tables/columns.py +++ b/netbox/netbox/tables/columns.py @@ -559,4 +559,4 @@ class MarkdownColumn(tables.TemplateColumn): ) def value(self, value): - return value \ No newline at end of file + return value From f76ce172e04c1f8561d48416083e72f99a992c37 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 15 Aug 2022 11:57:38 -0400 Subject: [PATCH 397/593] Update model docs for device components --- docs/models/dcim/consoleport.md | 37 ++++- docs/models/dcim/consoleporttemplate.md | 4 +- docs/models/dcim/consoleserverport.md | 37 ++++- docs/models/dcim/consoleserverporttemplate.md | 4 +- docs/models/dcim/devicebay.md | 18 ++- docs/models/dcim/devicebaytemplate.md | 4 +- docs/models/dcim/frontport.md | 41 ++++- docs/models/dcim/frontporttemplate.md | 4 +- docs/models/dcim/interface.md | 140 +++++++++++++++--- docs/models/dcim/interfacetemplate.md | 4 +- docs/models/dcim/inventoryitem.md | 40 ++++- docs/models/dcim/inventoryitemtemplate.md | 2 +- docs/models/dcim/modulebay.md | 25 +++- docs/models/dcim/modulebaytemplate.md | 4 +- docs/models/dcim/poweroutlet.md | 38 ++++- docs/models/dcim/poweroutlettemplate.md | 4 +- docs/models/dcim/powerport.md | 40 ++++- docs/models/dcim/powerporttemplate.md | 4 +- docs/models/dcim/rearport.md | 38 ++++- docs/models/dcim/rearporttemplate.md | 4 +- 20 files changed, 432 insertions(+), 60 deletions(-) diff --git a/docs/models/dcim/consoleport.md b/docs/models/dcim/consoleport.md index 1a0782f25..e9cd09836 100644 --- a/docs/models/dcim/consoleport.md +++ b/docs/models/dcim/consoleport.md @@ -1,5 +1,36 @@ -## Console Ports +# Console Ports -A console port provides connectivity to the physical console of a device. These are typically used for temporary access by someone who is physically near the device, or for remote out-of-band access provided via a networked console server. Each console port may be assigned a physical type. +A console port provides connectivity to the physical console of a device. These are typically used for temporary access by someone who is physically near the device, or for remote out-of-band access provided via a networked console server. -Cables can connect console ports to console server ports or pass-through ports. +!!! tip + Like most device components, console ports are instantiated automatically from [console port templates](./consoleporttemplate.md) assigned to the selected device type when a device is created. + +## Fields + +### Device + +The device to which this console port belongs. + +### Module + +The installed module within the assigned device to which this console port belongs (optional). + +### Name + +The name of the console port. Must be unique to the parent device. + +### Label + +An alternative physical label identifying the console port. + +### Type + +The type of console port. + +### Speed + +Operating speed, in bits per second (bps). + +### Mark Connected + +If selected, this component will be treated as if a cable has been connected. diff --git a/docs/models/dcim/consoleporttemplate.md b/docs/models/dcim/consoleporttemplate.md index 3462ff253..67d91de68 100644 --- a/docs/models/dcim/consoleporttemplate.md +++ b/docs/models/dcim/consoleporttemplate.md @@ -1,3 +1,3 @@ -## Console Port Templates +# Console Port Templates -A template for a console port that will be created on all instantiations of the parent device type. Each console port can be assigned a physical type. +A template for a console port that will be created on all instantiations of the parent device type. See the [console port](./consoleport.md) documentation for more detail. diff --git a/docs/models/dcim/consoleserverport.md b/docs/models/dcim/consoleserverport.md index da1ee8986..1fbb653a8 100644 --- a/docs/models/dcim/consoleserverport.md +++ b/docs/models/dcim/consoleserverport.md @@ -1,5 +1,36 @@ -## Console Server Ports +# Console Server Ports -A console server is a device which provides remote access to the local consoles of connected devices. They are typically used to provide remote out-of-band access to network devices. Each console server port may be assigned a physical type. +A console server is a device which provides remote access to the local consoles of connected devices. They are typically used to provide remote out-of-band access to network devices, and generally connect to [console ports](./consoleport.md). -Cables can connect console server ports to console ports or pass-through ports. +!!! tip + Like most device components, console server ports are instantiated automatically from [console server port templates](./consoleserverporttemplate.md) assigned to the selected device type when a device is created. + +## Fields + +### Device + +The device to which this console server port belongs. + +### Module + +The installed module within the assigned device to which this console server port belongs (optional). + +### Name + +The name of the console server port. Must be unique to the parent device. + +### Label + +An alternative physical label identifying the console server port. + +### Type + +The type of console server port. + +### Speed + +Operating speed, in bits per second (bps). + +### Mark Connected + +If selected, this component will be treated as if a cable has been connected. \ No newline at end of file diff --git a/docs/models/dcim/consoleserverporttemplate.md b/docs/models/dcim/consoleserverporttemplate.md index cc4e8bcd3..c599e738a 100644 --- a/docs/models/dcim/consoleserverporttemplate.md +++ b/docs/models/dcim/consoleserverporttemplate.md @@ -1,3 +1,3 @@ -## Console Server Port Templates +# Console Server Port Templates -A template for a console server port that will be created on all instantiations of the parent device type. Each console server port can be assigned a physical type. +A template for a console server port that will be created on all instantiations of the parent device type. See the [console server port](./consoleserverport.md) documentation for more detail. diff --git a/docs/models/dcim/devicebay.md b/docs/models/dcim/devicebay.md index e79c426dc..f4f807c4a 100644 --- a/docs/models/dcim/devicebay.md +++ b/docs/models/dcim/devicebay.md @@ -1,8 +1,22 @@ -## Device Bays +# Device Bays Device bays represent a space or slot within a parent device in which a child device may be installed. For example, a 2U parent chassis might house four individual blade servers. The chassis would appear in the rack elevation as a 2U device with four device bays, and each server within it would be defined as a 0U device installed in one of the device bays. Child devices do not appear within rack elevations or count as consuming rack units. Child devices are first-class Devices in their own right: That is, they are fully independent managed entities which don't share any control plane with the parent. Just like normal devices, child devices have their own platform (OS), role, tags, and components. LAG interfaces may not group interfaces belonging to different child devices. !!! note - Device bays are **not** suitable for modeling line cards (such as those commonly found in chassis-based routers and switches), as these components depend on the control plane of the parent device to operate. Instead, these should be modeled as modules installed within module bays. + Device bays are **not** suitable for modeling line cards (such as those commonly found in chassis-based routers and switches), as these components depend on the control plane of the parent device to operate. Instead, these should be modeled as [modules](./module.md) installed within [module bays](./modulebay.md). + +## Fields + +### Device + +The device to which this device bay belongs. + +### Name + +The device bay's name. Must be unique to the parent device. + +### Label + +An alternative physical label identifying the device bay. diff --git a/docs/models/dcim/devicebaytemplate.md b/docs/models/dcim/devicebaytemplate.md index a4c50067a..060b799c0 100644 --- a/docs/models/dcim/devicebaytemplate.md +++ b/docs/models/dcim/devicebaytemplate.md @@ -1,3 +1,3 @@ -## Device Bay Templates +# Device Bay Templates -A template for a device bay that will be created on all instantiations of the parent device type. Device bays hold child devices, such as blade servers. +A template for a device bay that will be created on all instantiations of the parent device type. See the [device bay](./devicebay.md) documentation for more detail. diff --git a/docs/models/dcim/frontport.md b/docs/models/dcim/frontport.md index 6f12e8cbf..0b291c9ed 100644 --- a/docs/models/dcim/frontport.md +++ b/docs/models/dcim/frontport.md @@ -1,3 +1,40 @@ -## Front Ports +# Front Ports -Front ports are pass-through ports used to represent physical cable connections that comprise part of a longer path. For example, the ports on the front face of a UTP patch panel would be modeled in NetBox as front ports. Each port is assigned a physical type, and must be mapped to a specific rear port on the same device. A single rear port may be mapped to multiple front ports, using numeric positions to annotate the specific alignment of each. +Front ports are pass-through ports which represent physical cable connections that comprise part of a longer path. For example, the ports on the front face of a UTP patch panel would be modeled in NetBox as front ports. Each port is assigned a physical type, and must be mapped to a specific [rear port](./rearport.md) on the same device. A single rear port may be mapped to multiple front ports, using numeric positions to annotate the specific alignment of each. + +## Fields + +### Device + +The device to which this port belongs. + +### Module + +The installed module within the assigned device to which this port belongs (optional). + +### Name + +The port's name. Must be unique to the parent device. + +### Label + +An alternative physical label identifying the port. + +### Type + +The port's termination type. + +### Rear Ports + +The rear port and position to which this front port maps. + +!!! tip + When creating multiple front ports using a patterned name (e.g. `Port [1-12]`), you may select the equivalent number of rear port-position mappings from the list. + +### Color + +The port's color (optional). + +### Mark Connected + +If selected, this component will be treated as if a cable has been connected. diff --git a/docs/models/dcim/frontporttemplate.md b/docs/models/dcim/frontporttemplate.md index 03de0eae4..09535e3b6 100644 --- a/docs/models/dcim/frontporttemplate.md +++ b/docs/models/dcim/frontporttemplate.md @@ -1,3 +1,3 @@ -## Front Port Templates +# Front Port Templates -A template for a front-facing pass-through port that will be created on all instantiations of the parent device type. Front ports may have a physical type assigned, and must be associated with a corresponding rear port and position. This association will be automatically replicated when the device type is instantiated. +A template for a front-facing pass-through port that will be created on all instantiations of the parent device type. See the [front port](./frontport.md) documentation for more detail. diff --git a/docs/models/dcim/interface.md b/docs/models/dcim/interface.md index 87a1411b9..b69f7c6b1 100644 --- a/docs/models/dcim/interface.md +++ b/docs/models/dcim/interface.md @@ -1,34 +1,138 @@ -## Interfaces +# Interfaces -Interfaces in NetBox represent network interfaces used to exchange data with connected devices. On modern networks, these are most commonly Ethernet, but other types are supported as well. Each interface must be assigned a type, and may optionally be assigned a MAC address, MTU, and IEEE 802.1Q mode (tagged or access). Each interface can also be enabled or disabled, and optionally designated as management-only (for out-of-band management). Additionally, each interface may optionally be assigned to a VRF. +Interfaces in NetBox represent network interfaces used to exchange data with connected devices. On modern networks, these are most commonly Ethernet, but other types are supported as well. IP addresses and VLANs can be assigned to interfaces. !!! note - Although devices and virtual machines both can have interfaces, a separate model is used for each. Thus, device interfaces have some properties that are not present on virtual machine interfaces and vice versa. + Although both devices and virtual machines both can have interfaces assigned, a separate model is used for each. Thus, device interfaces have some properties that are not present on virtual machine interfaces and vice versa. -### Interface Types +## Fields -Interfaces may be physical or virtual in nature, but only physical interfaces may be connected via cables. Cables can connect interfaces to pass-through ports, circuit terminations, or other interfaces. Virtual interfaces, such as 802.1Q-tagged subinterfaces, may be assigned to physical parent interfaces. +### Device -Physical interfaces may be arranged into a link aggregation group (LAG) and associated with a parent LAG (virtual) interface. LAG interfaces can be recursively nested to model bonding of trunk groups. Like all virtual interfaces, LAG interfaces cannot be connected physically. +The device to which this interface belongs. -### Power over Ethernet (PoE) +### Module + +The installed module within the assigned device to which this interface belongs (optional). + +### Name + +The name of the interface, as reported by the device's operating system. Must be unique to the parent device. + +### Label + +An alternative physical label identifying the interface. + +### Type + +The type of interface. Interfaces may be physical or virtual in nature, but only physical interfaces may be connected via cables. !!! note - This feature was added in NetBox v3.3. + The interface type refers to the physical termination or port on the device. Interfaces which employ a removable optic or similar transceiver should be defined to represent the type of transceiver in use, irrespective of the physical termination to that transceiver. -Physical interfaces can be assigned a PoE mode to indicate PoE capability: power supplying equipment (PSE) or powered device (PD). Additionally, a PoE mode may be specified. This can be one of the listed IEEE 802.3 standards, or a passive setting (24 or 48 volts across two or four pairs). +### Speed -### Wireless Interfaces +The operating speed, in kilobits per second (kbps). -Wireless interfaces may additionally track the following attributes: +### Duplex -* **Role** - AP or station -* **Channel** - One of several standard wireless channels -* **Channel Frequency** - The transmit frequency -* **Channel Width** - Channel bandwidth +The operation duplex (full, half, or auto). -If a predefined channel is selected, the frequency and width attributes will be assigned automatically. If no channel is selected, these attributes may be defined manually. +### VRF -### IP Address Assignment +The [virtual routing and forwarding](../ipam/vrf.md) instance to which this interface is assigned. -IP addresses can be assigned to interfaces. VLANs can also be assigned to each interface as either tagged or untagged. (An interface may have only one untagged VLAN.) +### MAC Address + +The 48-bit MAC address (for Ethernet interfaces). + +### WWN + +The 64-bit world-wide name (for Fibre Channel interfaces). + +### MTU + +The interface's configured maximum transmissible unit (MTU). + +### Transmit Power + +The interface's configured output power, in dBm (for optical interfaces). + +### Enabled + +If not selected, this interface will be treated as disabled/inoperative. + +### Management Only + +Designates the interface as handling management traffic only (e.g. for out-of-band management connections). + +### Mark Connected + +If selected, this component will be treated as if a cable has been connected. + +### Parent Interface + +Virtual interfaces can be bound to a physical parent interface. This is helpful for modeling virtual interfaces which employ encapsulation on a physical interface, such as an 802.1Q VLAN-tagged subinterface. + +### Bridged Interface + +Interfaces can be bridged to other interfaces on a device in two manners: symmetric or grouped. + +* **Symmetric:** For example, eth0 is bridged to eth1, and eth1 is bridged to eth0. This effects a point-to-point bridge between the two interfaces, which NetBox will follow when tracing cable paths. +* **Grouped:** Multiple interfaces are each bridged to a common virtual bridge interface, effecting a multiaccess bridged segment. NetBox cannot follow these relationships when tracing cable paths, because no forwarding information is available. + +### LAG Interface + +Physical interfaces may be arranged into link aggregation groups (LAGs, also known as "trunks") and associated with a parent LAG (virtual) interface. LAG interfaces can be recursively nested to model bonding of trunk groups. Like all virtual interfaces, LAG interfaces cannot be connected physically. + +### PoE Mode + +The power over Ethernet (PoE) mode for this interface. (This field must be left empty for interfaces which do not support PoE.) Choices include: + +* Powered device (PD) +* Power-supplying equipment (PSE) + +### PoE Type + +The classification of PoE transmission supported, for PoE-enabled interfaces. This can be one of the listed IEEE 802.3 standards, or a passive setting (24 or 48 volts across two or four pairs). + +### 802.1Q Mode + +For switched Ethernet interfaces, this identifies the 802.1Q encapsulation strategy in effect. Options include: + +* **Access:** All traffic is assigned to a single VLAN, with no tagging. +* **Tagged:** One untagged "native" VLAN is allowed, as well as any number of tagged VLANs. +* **Tagged (all):** Implies that all VLANs are carried by the interface. One untagged VLAN may be designated. + +This field must be left blank for routed interfaces which do employ 802.1Q encapsulation. + +### Untagged VLAN + +The "native" (untagged) VLAN for the interface. Valid only when one of the above 802.1Q mode is selected. + +### Tagged VLANs + +The tagged VLANs which are configured to be carried by this interface. Valid only for the "tagged" 802.1Q mode above. + +### Wireless Role + +Indicates the configured role for wireless interfaces (access point or station). + +### Wireless Channel + +The configured channel for wireless interfaces. + +!!! tip + Selecting one of the pre-defined wireless channels will automatically populate the channel frequency and width upon saving the interface. + +### Channel Frequency + +The configured operation frequency of a wireless interface, in MHz. This is typically inferred by the configured channel above, but may be set manually e.g. to identify a licensed channel not available for general use. + +### Channel Width + +The configured channel width of a wireless interface, in MHz. This is typically inferred by the configured channel above, but may be set manually e.g. to identify a licensed channel not available for general use. + +### Wireless LANs + +The [wireless LANs](../wireless/wirelesslan.md) for which this interface carries traffic. (Valid for wireless interfaces only.) diff --git a/docs/models/dcim/interfacetemplate.md b/docs/models/dcim/interfacetemplate.md index e11abcce4..898c4843f 100644 --- a/docs/models/dcim/interfacetemplate.md +++ b/docs/models/dcim/interfacetemplate.md @@ -1,3 +1,3 @@ -## Interface Templates +# Interface Templates -A template for a network interface that will be created on all instantiations of the parent device type. Each interface may be assigned a physical or virtual type, and may be designated as "management-only." Power over Ethernet (PoE) mode and type may also be assigned to interface templates. +A template for a network interface that will be created on all instantiations of the parent device type. See the [interface](./interface.md) documentation for more detail. diff --git a/docs/models/dcim/inventoryitem.md b/docs/models/dcim/inventoryitem.md index fbd3172bb..1c2e894ed 100644 --- a/docs/models/dcim/inventoryitem.md +++ b/docs/models/dcim/inventoryitem.md @@ -2,6 +2,42 @@ Inventory items represent hardware components installed within a device, such as a power supply or CPU or line card. They are intended to be used primarily for inventory purposes. -Each inventory item can be assigned a functional role, manufacturer, part ID, serial number, and asset tag (all optional). A boolean toggle is also provided to indicate whether each item was entered manually or discovered automatically (by some process outside NetBox). - Inventory items are hierarchical in nature, such that any individual item may be designated as the parent for other items. For example, an inventory item might be created to represent a line card which houses several SFP optics, each of which exists as a child item within the device. An inventory item may also be associated with a specific component within the same device. For example, you may wish to associate a transceiver with an interface. + +## Fields + +### Device + +The device in which the inventory item is installed. + +### Parent + +The parent inventory item to which this item is assigned (optional). + +### Name + +The inventory item's name. Must be unique to the parent device. + +### Label + +An alternative physical label identifying the inventory item. + +### Role + +The functional [role](./inventoryitemrole.md) assigned to this inventory item. + +### Manufacturer + +The [manufacturer](./manufacturer.md) that produced the item. + +### Part ID + +The part identification or model number assigned by the manufacturer. + +### Serial Number + +The serial number assigned by the manufacturer. + +### Asset Tag + +A unique, locally-administered label used to identify hardware resources. diff --git a/docs/models/dcim/inventoryitemtemplate.md b/docs/models/dcim/inventoryitemtemplate.md index 3167ed4ab..02fde5995 100644 --- a/docs/models/dcim/inventoryitemtemplate.md +++ b/docs/models/dcim/inventoryitemtemplate.md @@ -1,3 +1,3 @@ # Inventory Item Templates -A template for an inventory item that will be automatically created when instantiating a new device. All attributes of this object will be copied to the new inventory item, including the associations with a parent item and assigned component, if any. +A template for an inventory item that will be automatically created when instantiating a new device. All attributes of this object will be copied to the new inventory item, including the associations with a parent item and assigned component, if any. See the [inventory item](./inventoryitem.md) documentation for more detail. diff --git a/docs/models/dcim/modulebay.md b/docs/models/dcim/modulebay.md index 6c6f94598..9291ff554 100644 --- a/docs/models/dcim/modulebay.md +++ b/docs/models/dcim/modulebay.md @@ -1,3 +1,24 @@ -## Module Bays +# Module Bays -Module bays represent a space or slot within a device in which a field-replaceable module may be installed. A common example is that of a chassis-based switch such as the Cisco Nexus 9000 or Juniper EX9200. Modules in turn hold additional components that become available to the parent device. +Module bays represent a space or slot within a device in which a field-replaceable [module](./module.md) may be installed. A common example is that of a chassis-based switch such as the Cisco Nexus 9000 or Juniper EX9200. Modules in turn hold additional components that become available to the parent device. + +!!! note + If you need to model child devices rather than modules, use a [device bay](./devicebay.md) instead. + +## Fields + +### Device + +The device to which this module bay belongs. + +### Name + +The module bay's name. Must be unique to the parent device. + +### Label + +An alternative physical label identifying the module bay. + +### Position + +The numeric position in which this module bay is situated. For example, this would be the number assigned to a slot within a chassis-based switch. diff --git a/docs/models/dcim/modulebaytemplate.md b/docs/models/dcim/modulebaytemplate.md index 463789305..3d5845d2e 100644 --- a/docs/models/dcim/modulebaytemplate.md +++ b/docs/models/dcim/modulebaytemplate.md @@ -1,3 +1,3 @@ -## Module Bay Templates +# Module Bay Templates -A template for a module bay that will be created on all instantiations of the parent device type. Module bays hold installed modules that do not have an independent management plane, such as line cards. +A template for a module bay that will be created on all instantiations of the parent device type. See the [module bay](./modulebay.md) documentation for more detail. diff --git a/docs/models/dcim/poweroutlet.md b/docs/models/dcim/poweroutlet.md index e9ef307bd..f6ca2c143 100644 --- a/docs/models/dcim/poweroutlet.md +++ b/docs/models/dcim/poweroutlet.md @@ -1,7 +1,39 @@ -## Power Outlets +# Power Outlets -Power outlets represent the outlets on a power distribution unit (PDU) or other device that supply power to dependent devices. Each power port may be assigned a physical type, and may be associated with a specific feed leg (where three-phase power is used) and/or a specific upstream power port. This association can be used to model the distribution of power within a device. +Power outlets represent the outlets on a power distribution unit (PDU) or other device that supplies power to dependent devices. Each power port may be assigned a physical type, and may be associated with a specific feed leg (where three-phase power is used) and/or a specific upstream power port. This association can be used to model the distribution of power within a device. For example, imagine a PDU with one power port which draws from a three-phase feed and 48 power outlets arranged into three banks of 16 outlets each. Outlets 1-16 would be associated with leg A on the port, and outlets 17-32 and 33-48 would be associated with legs B and C, respectively. -Cables can connect power outlets only to downstream power ports. (Pass-through ports cannot be used to model power distribution.) +## Fields + +### Device + +The device to which this power outlet belongs. + +### Module + +The installed module within the assigned device to which this power outlet belongs (optional). + +### Name + +The name of the power outlet. Must be unique to the parent device. + +### Label + +An alternative physical label identifying the power outlet. + +### Type + +The type of power outlet. + +### Power Port + +When modeling a device which redistributes power from an upstream supply, such as a power distribution unit (PDU), each power outlet should be mapped to the respective [power port](./powerport.md) on the device which supplies power. For example, a 24-outlet PDU may two power ports, each distributing power to 12 of its outlets. + +### Feed Leg + +This field is used to indicate to which leg of three-phase power circuit the outlet is bound. (This should be left blank for single-phase applications.) + +### Mark Connected + +If selected, this component will be treated as if a cable has been connected. diff --git a/docs/models/dcim/poweroutlettemplate.md b/docs/models/dcim/poweroutlettemplate.md index 6f81891f1..0803ede97 100644 --- a/docs/models/dcim/poweroutlettemplate.md +++ b/docs/models/dcim/poweroutlettemplate.md @@ -1,3 +1,3 @@ -## Power Outlet Templates +# Power Outlet Templates -A template for a power outlet that will be created on all instantiations of the parent device type. Each power outlet can be assigned a physical type, and its power source may be mapped to a specific feed leg and power port template. This association will be automatically replicated when the device type is instantiated. +A template for a power outlet that will be created on all instantiations of the parent device type. See the [power outlet](./poweroutlet.md) documentation for more detail. diff --git a/docs/models/dcim/powerport.md b/docs/models/dcim/powerport.md index 1948920d0..cfe513fbd 100644 --- a/docs/models/dcim/powerport.md +++ b/docs/models/dcim/powerport.md @@ -1,8 +1,40 @@ -## Power Ports +# Power Ports -A power port represents the inlet of a device where it draws its power, i.e. the connection port(s) on a device's power supply. Each power port may be assigned a physical type, as well as allocated and maximum draw values (in watts). These values can be used to calculate the overall utilization of an upstream power feed. +A power port is a device component which draws power from some external source (e.g. an upstream [power outlet](./poweroutlet.md)), and generally represents a power supply internal to a device. + +## Fields + +### Device + +The device to which this power port belongs. + +### Module + +The installed module within the assigned device to which this power port belongs (optional). + +### Name + +The name of the power port. Must be unique to the parent device. + +### Label + +An alternative physical label identifying the power port. + +### Type + +The type of power port. + +### Maximum Draw + +The maximum amount of power this port consumes (in watts). !!! info - When creating a power port on a device which supplies power to downstream devices, the allocated and maximum draw numbers should be left blank. Utilization will be calculated by taking the sum of all power ports of devices connected downstream. + When creating a power port on a device which is mapped to outlets and supplies power to downstream devices, the maximum and allocated draw numbers should be left blank. Utilization will be calculated by taking the sum of all power ports of devices connected downstream. -Cables can connect power ports only to power outlets or power feeds. (Pass-through ports cannot be used to model power distribution.) +### Allocated Draw + +The budgeted amount of power this port consumes (in watts). + +### Mark Connected + +If selected, this component will be treated as if a cable has been connected. diff --git a/docs/models/dcim/powerporttemplate.md b/docs/models/dcim/powerporttemplate.md index 947f146ae..32579f4d5 100644 --- a/docs/models/dcim/powerporttemplate.md +++ b/docs/models/dcim/powerporttemplate.md @@ -1,3 +1,3 @@ -## Power Port Templates +# Power Port Templates -A template for a power port that will be created on all instantiations of the parent device type. Each power port can be assigned a physical type, as well as a maximum and allocated draw in watts. +A template for a power port that will be created on all instantiations of the parent device type. See the [power port](./powerport.md) documentation for more detail. diff --git a/docs/models/dcim/rearport.md b/docs/models/dcim/rearport.md index 41c5b3037..3d161f65d 100644 --- a/docs/models/dcim/rearport.md +++ b/docs/models/dcim/rearport.md @@ -1,6 +1,40 @@ -## Rear Ports +# Rear Ports -Like front ports, rear ports are pass-through ports which represent the continuation of a path from one cable to the next. Each rear port is defined with its physical type and a number of positions: Rear ports with more than one position can be mapped to multiple front ports. This can be useful for modeling instances where multiple paths share a common cable (for example, six discrete two-strand fiber connections sharing a 12-strand MPO cable). +Like [front ports](./frontport.md), rear ports are pass-through ports which represent the continuation of a path from one cable to the next. Each rear port is defined with its physical type and a number of positions: Rear ports with more than one position can be mapped to multiple front ports. This can be useful for modeling instances where multiple paths share a common cable (for example, six discrete two-strand fiber connections sharing a 12-strand MPO cable). !!! note Front and rear ports need not necessarily reside on the actual front or rear device face. This terminology is used primarily to distinguish between the two components in a pass-through port pairing. + +## Fields + +### Device + +The device to which this port belongs. + +### Module + +The installed module within the assigned device to which this port belongs (optional). + +### Name + +The port's name. Must be unique to the parent device. + +### Label + +An alternative physical label identifying the port. + +### Type + +The port's termination type. + +### Color + +The port's color (optional). + +### Positions + +The number of [front ports](./frontport.md) to which this rear port can be mapped. For example, an MPO fiber termination cassette might have a single 12-strand rear port mapped to 12 discrete front ports, each terminating a single fiber strand. (For rear ports which map directly to a single front port, set this to `1`.) + +### Mark Connected + +If selected, this component will be treated as if a cable has been connected. diff --git a/docs/models/dcim/rearporttemplate.md b/docs/models/dcim/rearporttemplate.md index 01ba02ac0..00cdfa5d2 100644 --- a/docs/models/dcim/rearporttemplate.md +++ b/docs/models/dcim/rearporttemplate.md @@ -1,3 +1,3 @@ -## Rear Port Templates +# Rear Port Templates -A template for a rear-facing pass-through port that will be created on all instantiations of the parent device type. Each rear port may have a physical type and one or more front port templates assigned to it. The number of positions associated with a rear port determines how many front ports can be assigned to it (the maximum is 1024). +A template for a rear-facing pass-through port that will be created on all instantiations of the parent device type. See the [rear port](./rearport.md) documentation for more detail. From 4307f078edf8f94137b451150eeacf932d98f8b6 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 15 Aug 2022 15:16:02 -0400 Subject: [PATCH 398/593] Finish refreshing DCIM models documentation --- docs/models/dcim/cable.md | 36 +++++++++--- docs/models/dcim/consoleserverport.md | 2 +- docs/models/dcim/device.md | 82 ++++++++++++++++++++++++++- docs/models/dcim/devicebay.md | 3 + docs/models/dcim/devicerole.md | 18 ++++++ docs/models/dcim/devicetype.md | 44 +++++++++++--- docs/models/dcim/frontport.md | 3 + docs/models/dcim/interface.md | 3 + docs/models/dcim/inventoryitem.md | 3 + docs/models/dcim/inventoryitemrole.md | 14 +++++ docs/models/dcim/location.md | 27 ++++++++- docs/models/dcim/manufacturer.md | 12 +++- docs/models/dcim/module.md | 32 ++++++++++- docs/models/dcim/modulebay.md | 3 + docs/models/dcim/moduletype.md | 18 +++++- docs/models/dcim/platform.md | 28 ++++++++- docs/models/dcim/powerfeed.md | 58 +++++++++++++++---- docs/models/dcim/poweroutlet.md | 3 + docs/models/dcim/powerpanel.md | 18 +++++- docs/models/dcim/powerport.md | 3 + docs/models/dcim/rack.md | 63 +++++++++++++++++--- docs/models/dcim/rackreservation.md | 20 ++++++- docs/models/dcim/rackrole.md | 16 +++++- docs/models/dcim/rearport.md | 3 + docs/models/dcim/region.md | 16 +++++- docs/models/dcim/site.md | 52 ++++++++++++++--- docs/models/dcim/sitegroup.md | 16 +++++- docs/models/dcim/virtualchassis.md | 21 +++++-- 28 files changed, 547 insertions(+), 70 deletions(-) diff --git a/docs/models/dcim/cable.md b/docs/models/dcim/cable.md index 43c0abfab..20f6c03c7 100644 --- a/docs/models/dcim/cable.md +++ b/docs/models/dcim/cable.md @@ -1,24 +1,42 @@ # Cables -All connections between device components in NetBox are represented using cables. A cable represents a direct physical connection between two termination points, such as between a console port and a patch panel port, or between two network interfaces. +All connections between device components in NetBox are represented using cables. A cable represents a direct physical connection between two sets of endpoints (A and B), such as a console port and a patch panel port, or between two network interfaces. Cables may be connected to the following objects: -Each cable must have two endpoints defined. These endpoints are sometimes referenced as A and B for clarity, however cables are direction-agnostic and the order in which terminations are made has no meaning. Cables may be connected to the following objects: - -* Circuit terminations +* Network interfaces * Console ports * Console server ports -* Interfaces * Pass-through ports (front and rear) -* Power feeds -* Power outlets +* Circuit terminations * Power ports +* Power outlets +* Power feeds -Each cable may be assigned a type, label, length, and color. Each cable is also assigned one of three operational statuses: +## Fields + +### Status + +The cable's operational status. Choices include: * Active (default) * Planned * Decommissioning +### Type + +The cable's physical medium or classification. + +### Label + +An arbitrary label used to identify the cable. + +### Color + +The color of the cable. + +### Length + +The numeric length of the cable, including a unit designation (e.g. 100 meters or 25 feet). + ## Tracing Cables -A cable may be traced from either of its endpoints by clicking the "trace" button. (A REST API endpoint also provides this functionality.) NetBox will follow the path of connected cables from this termination across the directly connected cable to the far-end termination. If the cable connects to a pass-through port, and the peer port has another cable connected, NetBox will continue following the cable path until it encounters a non-pass-through or unconnected termination point. The entire path will be displayed to the user. +A cable may be traced from any of its endpoints by clicking the "trace" button. (A REST API endpoint also provides this functionality.) NetBox will follow the path of connected cables from this termination across the directly connected cable to the far-end termination. If the cable connects to a pass-through port, and the peer port has another cable connected, NetBox will continue following the cable path until it encounters a non-pass-through or unconnected termination point. The entire path will be displayed to the user. diff --git a/docs/models/dcim/consoleserverport.md b/docs/models/dcim/consoleserverport.md index 1fbb653a8..5c45451db 100644 --- a/docs/models/dcim/consoleserverport.md +++ b/docs/models/dcim/consoleserverport.md @@ -33,4 +33,4 @@ Operating speed, in bits per second (bps). ### Mark Connected -If selected, this component will be treated as if a cable has been connected. \ No newline at end of file +If selected, this component will be treated as if a cable has been connected. diff --git a/docs/models/dcim/device.md b/docs/models/dcim/device.md index a99078472..ddf1bfb09 100644 --- a/docs/models/dcim/device.md +++ b/docs/models/dcim/device.md @@ -8,8 +8,86 @@ A device is said to be full-depth if its installation on one rack face prevents Each device must be instantiated from a pre-created device type, and its default components (console ports, power ports, interfaces, etc.) will be created automatically. (The device type associated with a device may be changed after its creation, however its components will not be updated retroactively.) -Each device must be assigned a site, device role, and operational status, and may optionally be assigned to a specific location and/or rack within a site. A platform, serial number, and asset tag may optionally be assigned to each device. - Device names must be unique within a site, unless the device has been assigned to a tenant. Devices may also be unnamed. When a device has one or more interfaces with IP addresses assigned, a primary IP for the device can be designated, for both IPv4 and IPv6. + +## Fields + +### Name + +The device's configured name. This field is optional; devices can be unnamed. However, if set, the name must be unique to the assigned site and tenant. + +### Device Role + +The functional [role](./devicerole.md) assigned to this device. + +### Device Type + +The hardware [device type](./devicetype.md) which defines the device's make & model. Upon creating, all templated components assigned to the device type will be replicated on the new device. + +### Airflow + +The direction in which air circulates through the device chassis for cooling. + +### Serial Number + +The unique physical serial number assigned to this device by its manufacturer. + +### Asset Tag + +A unique, locally-administered label used to identify hardware resources. + +### Site + +The [site](./site.md) in which this device is located. + +### Location + +A specific [location](./location.md) where this device resides within the assigned site (optional). + +### Rack + +The [rack](./rack.md) within which this device is installed (optional). + +### Rack Face + +If installed in a rack, this field denotes the primary face on which the device is mounted. + +### Position + +If installed in a rack, this field indicates the base rack unit in which the device is mounted. + +!!! tip + Devices with a height of more than one rack unit should be set to the lowest-numbered rack unit that they occupy. + +### Status + +The device's operational status. + +!!! tip + Additional statuses may be defined by setting `Device.status` under the [`FIELD_CHOICES`](../../configuration/data-validation.md#field_choices) configuration parameter. + +### Platform + +A device may be associated with a particular [platform](./platform.md) to indicate its operating system. Note that only platforms assigned to the associated manufacturer (or to no manufacturer) will be available for selection. + +### Cluster + +If this device will serve as a host for a virtualization [cluster](../virtualization/cluster.md), it can be assigned here. (Host devices can also be assigned by editing the cluster.) + +### Virtual Chassis + +The [virtual chassis](./virtualchassis.md) of which this device is a member, if any. + +### VC Position + +If assigned to a [virtual chassis](./virtualchassis.md), this field indicates the device's member position. + +### VC Priority + +If assigned to a [virtual chassis](./virtualchassis.md), this field indicates the device's priority for master election. + +### Local Config Context Data + +Any unique [context data](../../features/context-data.md) to be associated with the device. diff --git a/docs/models/dcim/devicebay.md b/docs/models/dcim/devicebay.md index f4f807c4a..5bbb125f8 100644 --- a/docs/models/dcim/devicebay.md +++ b/docs/models/dcim/devicebay.md @@ -7,6 +7,9 @@ Child devices are first-class Devices in their own right: That is, they are full !!! note Device bays are **not** suitable for modeling line cards (such as those commonly found in chassis-based routers and switches), as these components depend on the control plane of the parent device to operate. Instead, these should be modeled as [modules](./module.md) installed within [module bays](./modulebay.md). +!!! tip + Like most device components, device bays are instantiated automatically from [device bay templates](./devicebaytemplate.md) assigned to the selected device type when a device is created. + ## Fields ### Device diff --git a/docs/models/dcim/devicerole.md b/docs/models/dcim/devicerole.md index 13b8f021e..e9bdc0fa6 100644 --- a/docs/models/dcim/devicerole.md +++ b/docs/models/dcim/devicerole.md @@ -1,3 +1,21 @@ # Device Roles Devices can be organized by functional roles, which are fully customizable by the user. For example, you might create roles for core switches, distribution switches, and access switches within your network. + +## Fields + +### Name + +A unique human-friendly name. + +### Slug + +A unique URL-friendly identifier. (This value can be used for filtering.) + +### Color + +The color used when displaying the role in the NetBox UI. + +### VM Role + +If selected, this role may be assigned to [virtual machines](../virtualization/virtualmachine.md) diff --git a/docs/models/dcim/devicetype.md b/docs/models/dcim/devicetype.md index cf42185f4..050f93244 100644 --- a/docs/models/dcim/devicetype.md +++ b/docs/models/dcim/devicetype.md @@ -4,13 +4,43 @@ A device type represents a particular make and model of hardware that exists in Device types are instantiated as devices installed within sites and/or equipment racks. For example, you might define a device type to represent a Juniper EX4300-48T network switch with 48 Ethernet interfaces. You can then create multiple _instances_ of this type named "switch1," "switch2," and so on. Each device will automatically inherit the components (such as interfaces) of its device type at the time of creation. However, changes made to a device type will **not** apply to instances of that device type retroactively. -Some devices house child devices which share physical resources, like space and power, but which function independently. A common example of this is blade server chassis. Each device type is designated as one of the following: - -* A parent device (which has device bays) -* A child device (which must be installed within a device bay) -* Neither - !!! note This parent/child relationship is **not** suitable for modeling chassis-based devices, wherein child members share a common control plane. Instead, line cards and similarly non-autonomous hardware should be modeled as modules or inventory items within a device. -A device type may optionally specify an airflow direction, such as front-to-rear, rear-to-front, or passive. Airflow direction may also be set separately per device. If it is not defined for a device at the time of its creation, it will inherit the airflow setting of its device type. +## Fields + +### Manufacturer + +The [manufacturer](./manufacturer.md) which produces this type of device. + +### Model + +The model number assigned to this device type by its manufacturer. Must be unique to the manufacturer. + +### Slug + +A unique URL-friendly representation of the model identifier. (This value can be used for filtering.) + +### Part Number + +An alternative part number to uniquely identify the device type. + +### Height + +The height of the physical device in rack units. (For device types that are not rack-mountable, this should be `0`.) + +### Is Full Depth + +If selected, this device type is considered to occupy both the front and rear faces of a rack, regardless of which face it is assigned. + +### Parent/Child Status + +Indicates whether this is a parent type (capable of housing child devices), a child type (which must be installed within a device bay), or neither. + +### Airflow + +The default direction in which airflow circulates within the device chassis. This may be configured differently for instantiated devices (e.g. because of different fan modules). + +### Front & Rear Images + +Users can upload illustrations of the device's front and rear panels. If present, these will be used to render the device in [rack](./rack.md) elevation diagrams. diff --git a/docs/models/dcim/frontport.md b/docs/models/dcim/frontport.md index 0b291c9ed..7ea617250 100644 --- a/docs/models/dcim/frontport.md +++ b/docs/models/dcim/frontport.md @@ -2,6 +2,9 @@ Front ports are pass-through ports which represent physical cable connections that comprise part of a longer path. For example, the ports on the front face of a UTP patch panel would be modeled in NetBox as front ports. Each port is assigned a physical type, and must be mapped to a specific [rear port](./rearport.md) on the same device. A single rear port may be mapped to multiple front ports, using numeric positions to annotate the specific alignment of each. +!!! tip + Like most device components, front ports are instantiated automatically from [front port templates](./frontporttemplate.md) assigned to the selected device type when a device is created. + ## Fields ### Device diff --git a/docs/models/dcim/interface.md b/docs/models/dcim/interface.md index b69f7c6b1..42b570964 100644 --- a/docs/models/dcim/interface.md +++ b/docs/models/dcim/interface.md @@ -2,6 +2,9 @@ Interfaces in NetBox represent network interfaces used to exchange data with connected devices. On modern networks, these are most commonly Ethernet, but other types are supported as well. IP addresses and VLANs can be assigned to interfaces. +!!! tip + Like most device components, interfaces are instantiated automatically from [interface templates](./interfacetemplate.md) assigned to the selected device type when a device is created. + !!! note Although both devices and virtual machines both can have interfaces assigned, a separate model is used for each. Thus, device interfaces have some properties that are not present on virtual machine interfaces and vice versa. diff --git a/docs/models/dcim/inventoryitem.md b/docs/models/dcim/inventoryitem.md index 1c2e894ed..f61586eda 100644 --- a/docs/models/dcim/inventoryitem.md +++ b/docs/models/dcim/inventoryitem.md @@ -4,6 +4,9 @@ Inventory items represent hardware components installed within a device, such as Inventory items are hierarchical in nature, such that any individual item may be designated as the parent for other items. For example, an inventory item might be created to represent a line card which houses several SFP optics, each of which exists as a child item within the device. An inventory item may also be associated with a specific component within the same device. For example, you may wish to associate a transceiver with an interface. +!!! tip + Like most device components, inventory items can be instantiated automatically from [templates](./inventoryitemtemplate.md) assigned to the selected device type when a device is created. + ## Fields ### Device diff --git a/docs/models/dcim/inventoryitemrole.md b/docs/models/dcim/inventoryitemrole.md index 8ed31481a..50eb61abd 100644 --- a/docs/models/dcim/inventoryitemrole.md +++ b/docs/models/dcim/inventoryitemrole.md @@ -1,3 +1,17 @@ # Inventory Item Roles Inventory items can be organized by functional roles, which are fully customizable by the user. For example, you might create roles for power supplies, fans, interface optics, etc. + +## Fields + +### Name + +A unique human-friendly name. + +### Slug + +A unique URL-friendly identifier. (This value can be used for filtering.) + +### Color + +The color used when displaying the role in the NetBox UI. diff --git a/docs/models/dcim/location.md b/docs/models/dcim/location.md index fb72c218d..96ab13039 100644 --- a/docs/models/dcim/location.md +++ b/docs/models/dcim/location.md @@ -1,5 +1,28 @@ # Locations -Racks and devices can be grouped by location within a site. A location may represent a floor, room, cage, or similar organizational unit. Locations can be nested to form a hierarchy. For example, you may have floors within a site, and rooms within a floor. +[Racks](./rack.md) and [devices](./device.md) can be grouped by location within a [site](./site.md). A location may represent a floor, room, cage, or similar organizational unit. Locations can be nested to form a hierarchy. For example, you may have floors within a site, and rooms within a floor. -Each location must have a name that is unique within its parent site and location, if any, and must be assigned an operational status. (The set of available statuses is configurable.) +## Fields + +### Site + +The parent [site](./site.md) to which this location belongs. + +### Parent + +The parent location of which this location is a child (optional). + +### Name + +A unique human-friendly name. + +### Slug + +A unique URL-friendly identifier. (This value can be used for filtering.) + +### Status + +The location's operational status. + +!!! tip + Additional statuses may be defined by setting `Location.status` under the [`FIELD_CHOICES`](../../configuration/data-validation.md#field_choices) configuration parameter. diff --git a/docs/models/dcim/manufacturer.md b/docs/models/dcim/manufacturer.md index df227ee17..6beee5a90 100644 --- a/docs/models/dcim/manufacturer.md +++ b/docs/models/dcim/manufacturer.md @@ -1,3 +1,13 @@ # Manufacturers -A manufacturer represents the "make" of a device; e.g. Cisco or Dell. Each device type must be assigned to a manufacturer. (Inventory items and platforms may also be associated with manufacturers.) Each manufacturer must have a unique name and may have a description assigned to it. +A manufacturer represents the "make" of a device; e.g. Cisco or Dell. Each [device type](./devicetype.md) must be assigned to a manufacturer. ([Inventory items](./inventoryitem.md) and [platforms](./platform.md) may also be associated with manufacturers.) + +## Fields + +### Name + +A unique human-friendly name. + +### Slug + +A unique URL-friendly identifier. (This value can be used for filtering.) diff --git a/docs/models/dcim/module.md b/docs/models/dcim/module.md index bc9753ecc..c90430faa 100644 --- a/docs/models/dcim/module.md +++ b/docs/models/dcim/module.md @@ -2,4 +2,34 @@ A module is a field-replaceable hardware component installed within a device which houses its own child components. The most common example is a chassis-based router or switch. -Similar to devices, modules are instantiated from module types, and any components associated with the module type are automatically instantiated on the new model. Each module must be installed within a module bay on a device, and each module bay may have only one module installed in it. A module may optionally be assigned a serial number and asset tag. +Similar to devices, modules are instantiated from [module types](./moduletype.md), and any components associated with the module type are automatically instantiated on the new model. Each module must be installed within a [module bay](./modulebay.md) on a [device](./device.md), and each module bay may have only one module installed in it. + +## Fields + +### Device + +The parent [device](./device.md) into which the module is installed. + +### Module Bay + +The [module bay](./modulebay.md) into which the module is installed. + +### Module Type + +The [module type](./moduletype.md) which represents the physical make & model of hardware. By default, module components will be instantiated automatically from the module type when creating a new module. + +### Serial Number + +The unique physical serial number assigned to this module by its manufacturer. + +### Asset Tag + +A unique, locally-administered label used to identify hardware resources. + +### Replicate Components + +Controls whether templates module type components are automatically added when creating a new module. + +### Adopt Components + +Controls whether pre-existing components assigned to the device with the same names as components that would be created automatically will be assigned to the new module. diff --git a/docs/models/dcim/modulebay.md b/docs/models/dcim/modulebay.md index 9291ff554..c77909511 100644 --- a/docs/models/dcim/modulebay.md +++ b/docs/models/dcim/modulebay.md @@ -5,6 +5,9 @@ Module bays represent a space or slot within a device in which a field-replaceab !!! note If you need to model child devices rather than modules, use a [device bay](./devicebay.md) instead. +!!! tip + Like most device components, module bays are instantiated automatically from [module bay templates](./modulebaytemplate.md) assigned to the selected device type when a device is created. + ## Fields ### Device diff --git a/docs/models/dcim/moduletype.md b/docs/models/dcim/moduletype.md index c1c8c5079..b8ec0ac6e 100644 --- a/docs/models/dcim/moduletype.md +++ b/docs/models/dcim/moduletype.md @@ -1,8 +1,8 @@ # Module Types -A module type represent a specific make and model of hardware component which is installable within a device and has its own child components. For example, consider a chassis-based switch or router with a number of field-replaceable line cards. Each line card has its own model number and includes a certain set of components such as interfaces. Each module type may have a manufacturer, model number, and part number assigned to it. +A module type represents a specific make and model of hardware component which is installable within a device's [module bay](./modulebay.md) and has its own child components. For example, consider a chassis-based switch or router with a number of field-replaceable line cards. Each line card has its own model number and includes a certain set of components such as interfaces. Each module type may have a manufacturer, model number, and part number assigned to it. -Similar to device types, each module type can have any of the following component templates associated with it: +Similar to [device types](./devicetype.md), each module type can have any of the following component templates associated with it: * Interfaces * Console ports @@ -21,3 +21,17 @@ When adding component templates to a module type, the string `{module}` can be u For example, you can create a module type with interface templates named `Gi{module}/0/[1-48]`. When a new module of this type is "installed" to a module bay with a position of "3", NetBox will automatically name these interfaces `Gi3/0/[1-48]`. Automatic renaming is supported for all modular component types (those listed above). + +## Fields + +### Manufacturer + +The [manufacturer](./manufacturer.md) which produces this type of module. + +### Model + +The model number assigned to this module type by its manufacturer. Must be unique to the manufacturer. + +### Part Number + +An alternative part number to uniquely identify the module type. diff --git a/docs/models/dcim/platform.md b/docs/models/dcim/platform.md index 347abc5b8..d080f74a4 100644 --- a/docs/models/dcim/platform.md +++ b/docs/models/dcim/platform.md @@ -1,9 +1,31 @@ # Platforms -A platform defines the type of software running on a device or virtual machine. This can be helpful to model when it is necessary to distinguish between different versions or feature sets. Note that two devices of the same type may be assigned different platforms: For example, one Juniper MX240 might run Junos 14 while another runs Junos 15. +A platform defines the type of software running on a [device](./device.md) or [virtual machine](../virtualization/virtualmachine.md). This can be helpful to model when it is necessary to distinguish between different versions or feature sets. Note that two devices of the same type may be assigned different platforms: For example, one Juniper MX240 might run Junos 14 while another runs Junos 15. -Platforms may optionally be limited by manufacturer: If a platform is assigned to a particular manufacturer, it can only be assigned to devices with a type belonging to that manufacturer. +Platforms may optionally be limited by [manufacturer](./manufacturer.md): If a platform is assigned to a particular manufacturer, it can only be assigned to devices with a type belonging to that manufacturer. -The platform model is also used to indicate which NAPALM driver (if any) and any associated arguments NetBox should use when connecting to a remote device. The name of the driver along with optional parameters are stored with the platform. +The platform model is also used to indicate which [NAPALM driver](../../integrations/napalm.md) (if any) and any associated arguments NetBox should use when connecting to a remote device. The name of the driver along with optional parameters are stored with the platform. The assignment of platforms to devices is an optional feature, and may be disregarded if not desired. + +## Fields + +### Name + +A unique human-friendly name. + +### Slug + +A unique URL-friendly identifier. (This value can be used for filtering.) + +### Manufacturer + +If designated, this platform will be available for use only to devices assigned to this [manufacturer](./manufacturer.md). This can be handy e.g. for limiting network operating systems to use on hardware produced by the relevant vendor. However, it should not be used when defining general-purpose software platforms. + +### NAPALM Driver + +The [NAPALM driver](https://napalm.readthedocs.io/en/latest/support/index.html) associated with this platform. + +### NAPALM Arguments + +Any additional arguments to send when invoking the NAPALM driver assigned to this platform. diff --git a/docs/models/dcim/powerfeed.md b/docs/models/dcim/powerfeed.md index bac7214f1..f98c758ff 100644 --- a/docs/models/dcim/powerfeed.md +++ b/docs/models/dcim/powerfeed.md @@ -1,21 +1,55 @@ # Power Feed -A power feed represents the distribution of power from a power panel to a particular device, typically a power distribution unit (PDU). The power port (inlet) on a device can be connected via a cable to a power feed. A power feed may optionally be assigned to a rack to allow more easily tracking the distribution of power among racks. +A power feed represents the distribution of power from a [power panel](./powerpanel.md) to a particular [device](./device.md), typically a power distribution unit (PDU). The [power port](./powerport.md) (inlet) on a device can be connected via a cable to a power feed. A power feed may optionally be assigned to a rack to allow more easily tracking the distribution of power among racks. -Each power feed is assigned an operational type (primary or redundant) and one of the following statuses: +## Fields -* Offline -* Active -* Planned -* Failed +### Power Panel -Each power feed also defines the electrical characteristics of the circuit which it represents. These include the following: +The [power panel](./powerpanel.md) which supplies upstream power to this feed. -* Supply type (AC or DC) -* Phase (single or three-phase) -* Voltage -* Amperage -* Maximum utilization (percentage) +### Rack + +The [rack](./rack.md) within which this feed delivers power (optional). + +### Name + +The feed's name or other identifier. Must be unique to the assigned power panel. + +### Status + +The feed's operational status. + +!!! tip + Additional statuses may be defined by setting `PowerFeed.status` under the [`FIELD_CHOICES`](../../configuration/data-validation.md#field_choices) configuration parameter. + +### Type + +In redundant environments, each power feed can be designated as providing either primary or redundant power. (In environment with only one power source, all power feeds should be designated as primary.) + +### Mark Connected + +If selected, the power feed will be treated as if a cable has been connected. + +### Supply + +Electrical current type (AC or DC). + +### Voltage + +Operating circuit voltage, in volts. + +### Amperage + +Operation circuit amperage, in amperes. + +### Phase + +Indicates whether the circuit provides single- or three-phase power. + +### Max Utilization + +The maximum safe utilization of the feed, expressed as a percentage of the total available power. (Typically this will be set to around 80%, to avoid tripping a breaker during heaving spikes in current draw.) !!! info The power utilization of a rack is calculated when one or more power feeds are assigned to the rack and connected to devices that draw power. diff --git a/docs/models/dcim/poweroutlet.md b/docs/models/dcim/poweroutlet.md index f6ca2c143..5c8bd6ff0 100644 --- a/docs/models/dcim/poweroutlet.md +++ b/docs/models/dcim/poweroutlet.md @@ -4,6 +4,9 @@ Power outlets represent the outlets on a power distribution unit (PDU) or other For example, imagine a PDU with one power port which draws from a three-phase feed and 48 power outlets arranged into three banks of 16 outlets each. Outlets 1-16 would be associated with leg A on the port, and outlets 17-32 and 33-48 would be associated with legs B and C, respectively. +!!! tip + Like most device components, power outlets are instantiated automatically from [power outlet templates](./poweroutlettemplate.md) assigned to the selected device type when a device is created. + ## Fields ### Device diff --git a/docs/models/dcim/powerpanel.md b/docs/models/dcim/powerpanel.md index 813321179..dd02a57f0 100644 --- a/docs/models/dcim/powerpanel.md +++ b/docs/models/dcim/powerpanel.md @@ -1,8 +1,20 @@ # Power Panel -A power panel represents the origin point in NetBox for electrical power being disseminated by one or more power feeds. In a data center environment, one power panel often serves a group of racks, with an individual power feed extending to each rack, though this is not always the case. It is common to have two sets of panels and feeds arranged in parallel to provide redundant power to each rack. - -Each power panel must be assigned to a site, and may optionally be assigned to a particular location within that site. +A power panel represents the origin point in NetBox for electrical power being disseminated by one or more [power feeds](./powerfeed.md). In a data center environment, one power panel often serves a group of racks, with an individual power feed extending to each rack, though this is not always the case. It is common to have two sets of panels and feeds arranged in parallel to provide redundant power to each rack. !!! note NetBox does not model the mechanism by which power is delivered to a power panel. Power panels define the root level of the power distribution hierarchy in NetBox. + +## Fields + +### Site + +The [site](./site.md) in which the power panel resides. + +### Location + +A specific [location](./location.md) within the assigned site where the power panel is installed. + +### Name + +The power panel's name. Must be unique to the assigned site. diff --git a/docs/models/dcim/powerport.md b/docs/models/dcim/powerport.md index cfe513fbd..7cc1dd60a 100644 --- a/docs/models/dcim/powerport.md +++ b/docs/models/dcim/powerport.md @@ -2,6 +2,9 @@ A power port is a device component which draws power from some external source (e.g. an upstream [power outlet](./poweroutlet.md)), and generally represents a power supply internal to a device. +!!! tip + Like most device components, power ports are instantiated automatically from [power port templates](./powerporttemplate.md) assigned to the selected device type when a device is created. + ## Fields ### Device diff --git a/docs/models/dcim/rack.md b/docs/models/dcim/rack.md index 9465a828c..57e7bec98 100644 --- a/docs/models/dcim/rack.md +++ b/docs/models/dcim/rack.md @@ -1,12 +1,51 @@ # Racks -The rack model represents a physical two- or four-post equipment rack in which devices can be installed. Each rack must be assigned to a site, and may optionally be assigned to a location and/or tenant. Racks can also be organized by user-defined functional roles. The name and facility ID of each rack within a location must be unique. +The rack model represents a physical two- or four-post equipment rack in which [devices](./device.md) can be installed. Each rack must be assigned to a [site](./site.md), and may optionally be assigned to a [location](./location.md) within that site. Racks can also be organized by user-defined functional roles. The name and facility ID of each rack within a location must be unique. Rack height is measured in *rack units* (U); racks are commonly between 42U and 48U tall, but NetBox allows you to define racks of arbitrary height. A toggle is provided to indicate whether rack units are in ascending (from the ground up) or descending order. Each rack is assigned a name and (optionally) a separate facility ID. This is helpful when leasing space in a data center your organization does not own: The facility will often assign a seemingly arbitrary ID to a rack (for example, "M204.313") whereas internally you refer to is simply as "R113." A unique serial number and asset tag may also be associated with each rack. -A rack must be designated as one of the following types: +## Fields + +### Site + +The [site](./site.md) to which the rack is assigned. + +### Location + +The [location](./location.md) within a site where the rack has been installed (optional). + +### Name + +The rack's name or identifier. Must be unique to the rack's location, if assigned. + +### Status + +Operational status. + +!!! tip + Additional statuses may be defined by setting `Rack.status` under the [`FIELD_CHOICES`](../../configuration/data-validation.md#field_choices) configuration parameter. + +### Role + +The functional [role](./rackrole.md) fulfilled by the rack. + +### Facility ID + +An alternative identifier assigned to the rack e.g. by the facility operator. This is helpful for tracking datacenter rack designations in a colocation facility. + +### Serial Number + +The unique physical serial number assigned to this rack. + +### Asset Tag + +A unique, locally-administered label used to identify hardware resources. + +### Type + +A rack can be designated as one of the following types: * 2-post frame * 4-post frame @@ -14,12 +53,18 @@ A rack must be designated as one of the following types: * Wall-mounted frame * Wall-mounted cabinet -Similarly, each rack must be assigned an operational status, which is one of the following: +### Width -* Reserved -* Available -* Planned -* Active -* Deprecated +The canonical distance between the two vertical rails on a face. (This is typically 19 inches, however other standard widths exist.) -Each rack has two faces (front and rear) on which devices can be mounted. Rail-to-rail width may be 10, 19, 21, or 23 inches. The outer width and depth of a rack or cabinet can also be annotated in millimeters or inches. +### Height + +The height of the rack, measured in units. + +### Outer Dimensions + +The external width and depth of the rack can be tracked to aid in floorplan calculations. These measurements must be designated in either millimeters or inches. + +### Descending Units + +If selected, the rack's elevation will display unit 1 at the top of the rack. (Most racks use asceneding numbering, with unit 1 assigned to the bottommost position.) diff --git a/docs/models/dcim/rackreservation.md b/docs/models/dcim/rackreservation.md index 0ed9651a0..32d52c9d7 100644 --- a/docs/models/dcim/rackreservation.md +++ b/docs/models/dcim/rackreservation.md @@ -1,3 +1,21 @@ # Rack Reservations -Users can reserve specific units within a rack for future use. An arbitrary set of units within a rack can be associated with a single reservation, but reservations cannot span multiple racks. A description is required for each reservation, reservations may optionally be associated with a specific tenant. +Users can reserve specific units within a [rack](./rackreservation.md) for future use. An arbitrary set of units within a rack can be associated with a single reservation, but reservations cannot span multiple racks. A description is required for each reservation, reservations may optionally be associated with a specific tenant. + +## Fields + +### Rack + +The [rack](./rack.md) being reserved. + +### Units + +The rack unit or units being reserved. Multiple units can be expressed using commas and/or hyphens. For example, `1,3,5-7` specifies units 1, 3, 5, 6, and 7. + +### User + +The NetBox user account associated with the reservation. Note that users with sufficient permission can make rack reservations for other users. + +### Description + +Every rack reservation must include a description of its purpose. diff --git a/docs/models/dcim/rackrole.md b/docs/models/dcim/rackrole.md index 1375ce692..88f171af8 100644 --- a/docs/models/dcim/rackrole.md +++ b/docs/models/dcim/rackrole.md @@ -1,3 +1,17 @@ # Rack Roles -Each rack can optionally be assigned a user-defined functional role. For example, you might designate a rack for compute or storage resources, or to house colocated customer devices. Rack roles are fully customizable and may be color-coded. +Each rack can optionally be assigned a user-defined functional role. For example, you might designate a rack for compute or storage resources, or to house colocated customer devices. + +## Fields + +### Name + +A unique human-friendly name. + +### Slug + +A unique URL-friendly identifier. (This value can be used for filtering.) + +### Color + +The color used when displaying the role in the NetBox UI. diff --git a/docs/models/dcim/rearport.md b/docs/models/dcim/rearport.md index 3d161f65d..b23d4a29b 100644 --- a/docs/models/dcim/rearport.md +++ b/docs/models/dcim/rearport.md @@ -5,6 +5,9 @@ Like [front ports](./frontport.md), rear ports are pass-through ports which repr !!! note Front and rear ports need not necessarily reside on the actual front or rear device face. This terminology is used primarily to distinguish between the two components in a pass-through port pairing. +!!! tip + Like most device components, rear ports are instantiated automatically from [rear port templates](./rearporttemplate.md) assigned to the selected device type when a device is created. + ## Fields ### Device diff --git a/docs/models/dcim/region.md b/docs/models/dcim/region.md index bac186264..27673b2df 100644 --- a/docs/models/dcim/region.md +++ b/docs/models/dcim/region.md @@ -1,5 +1,17 @@ # Regions -Sites can be arranged geographically using regions. A region might represent a continent, country, city, campus, or other area depending on your use case. Regions can be nested recursively to construct a hierarchy. For example, you might define several country regions, and within each of those several state or city regions to which sites are assigned. +[Sites](./site.md) can be arranged geographically using regions. A region might represent a continent, country, city, campus, or other area depending on your use case. Regions can be nested recursively to construct a hierarchy. For example, you might define several country regions, and within each of those several state or city regions to which sites are assigned. -Each region must have a name that is unique within its parent region, if any. +## Fields + +### Parent + +The parent region, if any. + +### Name + +The region's name. Must be unique to the parent region, if one is assigned. + +### Slug + +A unique URL-friendly identifier. (This value can be used for filtering.) diff --git a/docs/models/dcim/site.md b/docs/models/dcim/site.md index 6617b950c..c74c209e1 100644 --- a/docs/models/dcim/site.md +++ b/docs/models/dcim/site.md @@ -2,14 +2,50 @@ How you choose to employ sites when modeling your network may vary depending on the nature of your organization, but generally a site will equate to a building or campus. For example, a chain of banks might create a site to represent each of its branches, a site for its corporate headquarters, and two additional sites for its presence in two colocation facilities. -Each site must be assigned a unique name and may optionally be assigned to a region and/or tenant. The following operational statuses are available: +## Fields -* Planned -* Staging -* Active -* Decommissioning -* Retired +### Name -The site model also provides a facility ID field which can be used to annotate a facility ID (such as a datacenter name) associated with the site. Each site may also have an autonomous system (AS) number and time zone associated with it. (Time zones are provided by the [pytz](https://pypi.org/project/pytz/) package.) +The site's unique name. -The site model also includes several fields for storing contact and address information as well as geolocation data (GPS coordinates). +### Slug + +A unique URL-friendly identifier. (This value can be used for filtering.) + +### Status + +The site's operational status. + +!!! tip + Additional statuses may be defined by setting `Site.status` under the [`FIELD_CHOICES`](../../configuration/data-validation.md#field_choices) configuration parameter. + +### Region + +The parent [region](./region.md) to which the site belongs, if any. + +### Facility + +Data center or facility designation for identifying the site. + +### ASNs + +Each site can have multiple [AS numbers](../ipam/asn.md) assigned to it. + +### Time Zone + +The site's local time zone. (Time zones are provided by the [pytz](https://pypi.org/project/pytz/) package.) + +### Physical Address + +The site's physical address, used for mapping. + +### Shipping Address + +The address to use for deliveries destined for the site. + +!!! tip + You can also designate [points of contact](../../features/contacts.md) for each site to provide additional contact details. + +### Latitude & Longitude + +GPS coordinates of the site for geolocation. diff --git a/docs/models/dcim/sitegroup.md b/docs/models/dcim/sitegroup.md index 04ebcc1a5..e1c5215d9 100644 --- a/docs/models/dcim/sitegroup.md +++ b/docs/models/dcim/sitegroup.md @@ -1,5 +1,17 @@ # Site Groups -Like regions, site groups can be used to organize sites. Whereas regions are intended to provide geographic organization, site groups can be used to classify sites by role or function. Also like regions, site groups can be nested to form a hierarchy. Sites which belong to a child group are also considered to be members of any of its parent groups. +Like [regions](./region.md), site groups can be used to organize [sites](./site.md). Whereas regions are intended to provide geographic organization, site groups can be used to classify sites by role or function. Also like regions, site groups can be nested to form a hierarchy. Sites which belong to a child group are also considered to be members of all its parent groups. -Each site group must have a name that is unique within its parent group, if any. +## Fields + +### Parent + +The parent site group, if any. + +### Name + +The site group's name. Must be unique to the parent group, if one is assigned. + +### Slug + +A unique URL-friendly identifier. (This value can be used for filtering.) diff --git a/docs/models/dcim/virtualchassis.md b/docs/models/dcim/virtualchassis.md index 2466b065d..e68b2fda6 100644 --- a/docs/models/dcim/virtualchassis.md +++ b/docs/models/dcim/virtualchassis.md @@ -1,9 +1,22 @@ # Virtual Chassis -A virtual chassis represents a set of devices which share a common control plane. A common example of this is a stack of switches which are connected and configured to operate as a single device. A virtual chassis must be assigned a name and may be assigned a domain. +A virtual chassis represents a set of devices which share a common control plane. A common example of this is a stack of switches which are connected and configured to operate as a single managed device. Each device in the virtual chassis is referred to as a VC member, and assigned a position and (optionally) a priority. VC member devices commonly reside within the same rack, though this is not a requirement. -Each device in the virtual chassis is referred to as a VC member, and assigned a position and (optionally) a priority. VC member devices commonly reside within the same rack, though this is not a requirement. One of the devices may be designated as the VC master: This device will typically be assigned a name, services, virtual interfaces, and other attributes related to managing the VC. -If a VC master is defined, interfaces from all VC members are displayed when navigating to its device interfaces view. This does not include other members interfaces declared as management-only. +One of the member devices may be designated as the VC master: This device will typically be assigned a name, services, virtual interfaces, and other attributes related to managing the VC. If a VC master is defined, interfaces from all VC members are displayed when navigating to its device interfaces view. This does not include management-only interfaces belonging to other members. !!! note - It's important to recognize the distinction between a virtual chassis and a chassis-based device. A virtual chassis is **not** suitable for modeling a chassis-based switch with removable line cards (such as the Juniper EX9208), as its line cards are _not_ physically autonomous devices. + It's important to recognize the distinction between a virtual chassis and a chassis-based device. A virtual chassis is **not** suitable for modeling a chassis-based switch with removable line cards (such as the Juniper EX9208), as its line cards are _not_ physically autonomous devices. Instead, use [modules](./module.md) for these. + +## Fields + +### Name + +The virtual chassis' name. + +### Domain + +The domain assigned for VC member devices. + +### Master + +The member device which has been designated as the chassis master (optional). From 30ab1e5a5e1f12c3e88b145c511741f75b5d460b Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 16 Aug 2022 09:14:19 -0400 Subject: [PATCH 399/593] Changelog for #8723, #9505, #9979 --- docs/release-notes/version-3.2.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/release-notes/version-3.2.md b/docs/release-notes/version-3.2.md index 824e99778..f515e4d35 100644 --- a/docs/release-notes/version-3.2.md +++ b/docs/release-notes/version-3.2.md @@ -5,12 +5,15 @@ ### Enhancements * [#8595](https://github.com/netbox-community/netbox/issues/8595) - Add PON interface types +* [#8723](https://github.com/netbox-community/netbox/issues/8723) - Enable bulk renaming of devices * [#9161](https://github.com/netbox-community/netbox/issues/9161) - Pretty print JSON custom field data when editing +* [#9505](https://github.com/netbox-community/netbox/issues/9505) - Display extra addressing details for IPv4 prefixes * [#9625](https://github.com/netbox-community/netbox/issues/9625) - Add phone & email details to contacts panel * [#9857](https://github.com/netbox-community/netbox/issues/9857) - Add clear button to quick search fields ### Bug Fixes +* [#9979](https://github.com/netbox-community/netbox/issues/9979) - Fix Markdown rendering for custom fields in table columns * [#9986](https://github.com/netbox-community/netbox/issues/9986) - Workaround for upstream timezone data bug --- From 0ef1bc8490dc2f6d6623a3b209406d57bc278efd Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 16 Aug 2022 09:49:51 -0400 Subject: [PATCH 400/593] Clean up bulk edit buttons --- .../templates/dcim/device/consoleports.html | 54 ++++++++++--------- .../dcim/device/consoleserverports.html | 54 ++++++++++--------- netbox/templates/dcim/device/devicebays.html | 44 +++++++-------- netbox/templates/dcim/device/frontports.html | 54 ++++++++++--------- netbox/templates/dcim/device/interfaces.html | 51 ++++++++++-------- netbox/templates/dcim/device/inventory.html | 44 +++++++-------- netbox/templates/dcim/device/modulebays.html | 44 +++++++-------- .../templates/dcim/device/poweroutlets.html | 54 ++++++++++--------- netbox/templates/dcim/device/powerports.html | 54 ++++++++++--------- netbox/templates/dcim/device/rearports.html | 54 ++++++++++--------- netbox/templates/dcim/device_list.html | 16 ++++-- .../virtualmachine/interfaces.html | 43 +++++++-------- 12 files changed, 311 insertions(+), 255 deletions(-) diff --git a/netbox/templates/dcim/device/consoleports.html b/netbox/templates/dcim/device/consoleports.html index afc306bd4..6f8b383c3 100644 --- a/netbox/templates/dcim/device/consoleports.html +++ b/netbox/templates/dcim/device/consoleports.html @@ -16,31 +16,37 @@
    -
    - {% if perms.dcim.change_consoleport %} - - - - {% endif %} - {% if perms.dcim.delete_consoleport %} - - {% endif %} -
    - {% if perms.dcim.add_consoleport %} - +
    + {% if perms.dcim.change_consoleport %} +
    + + +
    {% endif %} +
    + {% if perms.dcim.delete_consoleport %} + + {% endif %} + {% if perms.dcim.change_consoleport %} + + {% endif %} +
    +
    + {% if perms.dcim.add_consoleport %} + + {% endif %}
    {% endblock %} diff --git a/netbox/templates/dcim/device/consoleserverports.html b/netbox/templates/dcim/device/consoleserverports.html index 5f244cdc7..f246d4a82 100644 --- a/netbox/templates/dcim/device/consoleserverports.html +++ b/netbox/templates/dcim/device/consoleserverports.html @@ -16,31 +16,37 @@
    -
    - {% if perms.dcim.change_consoleserverport %} - - - - {% endif %} - {% if perms.dcim.delete_consoleserverport %} - - {% endif %} -
    - {% if perms.dcim.add_consoleserverport %} - +
    + {% if perms.dcim.change_consoleserverport %} +
    + + +
    {% endif %} +
    + {% if perms.dcim.delete_consoleserverport %} + + {% endif %} + {% if perms.dcim.change_consoleserverport %} + + {% endif %} +
    +
    + {% if perms.dcim.add_consoleserverport %} + + {% endif %}
    {% endblock %} diff --git a/netbox/templates/dcim/device/devicebays.html b/netbox/templates/dcim/device/devicebays.html index 5e33bdae0..d84408962 100644 --- a/netbox/templates/dcim/device/devicebays.html +++ b/netbox/templates/dcim/device/devicebays.html @@ -16,28 +16,30 @@
    -
    - {% if perms.dcim.change_devicebay %} - - - {% endif %} - {% if perms.dcim.delete_devicebay %} - - {% endif %} -
    - {% if perms.dcim.add_devicebay %} - +
    + {% if perms.dcim.change_devicebay %} +
    + + +
    {% endif %} + {% if perms.dcim.delete_devicebay %} + + {% endif %} +
    + {% if perms.dcim.add_devicebay %} + + {% endif %}
    {% endblock %} diff --git a/netbox/templates/dcim/device/frontports.html b/netbox/templates/dcim/device/frontports.html index 0d0f9577c..513d02090 100644 --- a/netbox/templates/dcim/device/frontports.html +++ b/netbox/templates/dcim/device/frontports.html @@ -16,31 +16,37 @@
    -
    - {% if perms.dcim.change_frontport %} - - - - {% endif %} - {% if perms.dcim.delete_frontport %} - - {% endif %} -
    - {% if perms.dcim.add_frontport %} - +
    + {% if perms.dcim.change_frontport %} +
    + + +
    {% endif %} +
    + {% if perms.dcim.delete_frontport %} + + {% endif %} + {% if perms.dcim.change_frontport %} + + {% endif %} +
    +
    + {% if perms.dcim.add_frontport %} + + {% endif %}
    {% endblock %} diff --git a/netbox/templates/dcim/device/interfaces.html b/netbox/templates/dcim/device/interfaces.html index 94e38dbf3..2019d9135 100644 --- a/netbox/templates/dcim/device/interfaces.html +++ b/netbox/templates/dcim/device/interfaces.html @@ -9,7 +9,6 @@
    {% csrf_token %} -
    {% include 'htmx/table.html' %} @@ -19,29 +18,35 @@
    {% if perms.dcim.change_interface %} - - - - {% endif %} - {% if perms.dcim.delete_interface %} - +
    + + +
    {% endif %} +
    + {% if perms.dcim.delete_interface %} + + {% endif %} + {% if perms.dcim.change_interface %} + + {% endif %} +
    {% if perms.dcim.add_interface %}
    diff --git a/netbox/templates/dcim/device/inventory.html b/netbox/templates/dcim/device/inventory.html index 18a0712f3..8b74acaae 100644 --- a/netbox/templates/dcim/device/inventory.html +++ b/netbox/templates/dcim/device/inventory.html @@ -16,28 +16,30 @@
    -
    - {% if perms.dcim.change_inventoryitem %} - - - {% endif %} - {% if perms.dcim.delete_inventoryitem %} - - {% endif %} -
    - {% if perms.dcim.add_inventoryitem %} - +
    + {% if perms.dcim.change_inventoryitem %} +
    + + +
    {% endif %} + {% if perms.dcim.delete_inventoryitem %} + + {% endif %} +
    + {% if perms.dcim.add_inventoryitem %} + + {% endif %}
    {% endblock %} diff --git a/netbox/templates/dcim/device/modulebays.html b/netbox/templates/dcim/device/modulebays.html index fc1c9a60d..67b9d88d4 100644 --- a/netbox/templates/dcim/device/modulebays.html +++ b/netbox/templates/dcim/device/modulebays.html @@ -16,28 +16,30 @@
    -
    - {% if perms.dcim.change_modulebay %} - - - {% endif %} - {% if perms.dcim.delete_modulebay %} - - {% endif %} -
    - {% if perms.dcim.add_modulebay %} - +
    + {% if perms.dcim.change_modulebay %} +
    + + +
    {% endif %} + {% if perms.dcim.delete_modulebay %} + + {% endif %} +
    + {% if perms.dcim.add_modulebay %} + + {% endif %}
    {% table_config_form table %} diff --git a/netbox/templates/dcim/device/poweroutlets.html b/netbox/templates/dcim/device/poweroutlets.html index d312fbbd0..61c2b61f4 100644 --- a/netbox/templates/dcim/device/poweroutlets.html +++ b/netbox/templates/dcim/device/poweroutlets.html @@ -16,31 +16,37 @@
    -
    - {% if perms.dcim.change_powerport %} - - - - {% endif %} - {% if perms.dcim.delete_poweroutlet %} - - {% endif %} -
    - {% if perms.dcim.add_poweroutlet %} - +
    + {% if perms.dcim.change_powerport %} +
    + + +
    {% endif %} +
    + {% if perms.dcim.delete_poweroutlet %} + + {% endif %} + {% if perms.dcim.change_powerport %} + + {% endif %} +
    +
    + {% if perms.dcim.add_poweroutlet %} + + {% endif %}
    {% endblock %} diff --git a/netbox/templates/dcim/device/powerports.html b/netbox/templates/dcim/device/powerports.html index cf71e81ba..cd8597e63 100644 --- a/netbox/templates/dcim/device/powerports.html +++ b/netbox/templates/dcim/device/powerports.html @@ -16,31 +16,37 @@
    -
    - {% if perms.dcim.change_powerport %} - - - - {% endif %} - {% if perms.dcim.delete_powerport %} - - {% endif %} -
    - {% if perms.dcim.add_powerport %} - +
    + {% if perms.dcim.change_powerport %} +
    + + +
    {% endif %} +
    + {% if perms.dcim.delete_powerport %} + + {% endif %} + {% if perms.dcim.change_powerport %} + + {% endif %} +
    +
    + {% if perms.dcim.add_powerport %} + + {% endif %}
    {% endblock %} diff --git a/netbox/templates/dcim/device/rearports.html b/netbox/templates/dcim/device/rearports.html index 73341990f..b370de189 100644 --- a/netbox/templates/dcim/device/rearports.html +++ b/netbox/templates/dcim/device/rearports.html @@ -16,31 +16,37 @@
    -
    - {% if perms.dcim.change_rearport %} - - - - {% endif %} - {% if perms.dcim.delete_rearport %} - - {% endif %} -
    - {% if perms.dcim.add_rearport %} - +
    + {% if perms.dcim.change_rearport %} +
    + + +
    {% endif %} +
    + {% if perms.dcim.delete_rearport %} + + {% endif %} + {% if perms.dcim.change_rearport %} + + {% endif %} +
    +
    + {% if perms.dcim.add_rearport %} + + {% endif %}
    {% endblock %} diff --git a/netbox/templates/dcim/device_list.html b/netbox/templates/dcim/device_list.html index a576a1f9e..50af50525 100644 --- a/netbox/templates/dcim/device_list.html +++ b/netbox/templates/dcim/device_list.html @@ -1,10 +1,8 @@ {% extends 'generic/object_list.html' %} +{% load buttons %} {% block bulk_buttons %} {% if perms.dcim.change_device %} - {% endif %} - {{ block.super }} + {% if 'bulk_edit' in actions %} +
    + {% bulk_edit_button model query_params=request.GET %} + +
    + {% endif %} + {% if 'bulk_delete' in actions %} + {% bulk_delete_button model query_params=request.GET %} + {% endif %} {% endblock %} diff --git a/netbox/templates/virtualization/virtualmachine/interfaces.html b/netbox/templates/virtualization/virtualmachine/interfaces.html index e3ffb84d4..eff98cdd6 100644 --- a/netbox/templates/virtualization/virtualmachine/interfaces.html +++ b/netbox/templates/virtualization/virtualmachine/interfaces.html @@ -15,27 +15,28 @@
    - {% if perms.virtualization.change_vminterface %} - - - {% endif %} - {% if perms.virtualization.delete_vminterface %} - - {% endif %} - {% if perms.virtualization.add_vminterface %} - - {% endif %} -
    + {% if perms.virtualization.change_vminterface %} +
    + + +
    + {% endif %} + {% if perms.virtualization.delete_vminterface %} + + {% endif %} + {% if perms.virtualization.add_vminterface %} + + {% endif %}
    {% endblock content %} From dedee0f9d9d3abded2a6665aea43283be7cc1a8b Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 16 Aug 2022 09:53:13 -0400 Subject: [PATCH 401/593] #9979: Fix fallback to default value --- netbox/netbox/tables/columns.py | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/netbox/netbox/tables/columns.py b/netbox/netbox/tables/columns.py index 113c8416b..6ab50d4c2 100644 --- a/netbox/netbox/tables/columns.py +++ b/netbox/netbox/tables/columns.py @@ -14,6 +14,7 @@ from django_tables2.columns import library from django_tables2.utils import Accessor from extras.choices import CustomFieldTypeChoices +from utilities.templatetags.builtins.filters import render_markdown from utilities.utils import content_type_identifier, content_type_name, get_viewname __all__ = ( @@ -418,14 +419,6 @@ class CustomFieldColumn(tables.Column): """ Display custom fields in the appropriate format. """ - template_code = """ - {% if value %} - {{ value|markdown }} - {% else %} - — - {% endif %} - """ - def __init__(self, customfield, *args, **kwargs): self.customfield = customfield kwargs['accessor'] = Accessor(f'custom_field_data__{customfield.name}') @@ -435,7 +428,7 @@ class CustomFieldColumn(tables.Column): super().__init__(*args, **kwargs) @staticmethod - def _likify_item(item): + def _linkify_item(item): if hasattr(item, 'get_absolute_url'): return f'{escape(item)}' return escape(item) @@ -451,13 +444,13 @@ class CustomFieldColumn(tables.Column): return ', '.join(v for v in value) if self.customfield.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT: return mark_safe(', '.join( - self._likify_item(obj) for obj in self.customfield.deserialize(value) + self._linkify_item(obj) for obj in self.customfield.deserialize(value) )) - if self.customfield.type == CustomFieldTypeChoices.TYPE_LONGTEXT: - return Template(self.template_code).render(Context({"value": value})) + if self.customfield.type == CustomFieldTypeChoices.TYPE_LONGTEXT and value: + return render_markdown(value) if value is not None: obj = self.customfield.deserialize(value) - return mark_safe(self._likify_item(obj)) + return mark_safe(self._linkify_item(obj)) return self.default def value(self, value): From 6d328a82e9dac52821bba08841caa2e1188c310c Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 16 Aug 2022 10:04:47 -0400 Subject: [PATCH 402/593] Cleanup for #9505 --- netbox/templates/ipam/prefix.html | 95 +++++++++++++------------------ 1 file changed, 40 insertions(+), 55 deletions(-) diff --git a/netbox/templates/ipam/prefix.html b/netbox/templates/ipam/prefix.html index ed2c3339b..b15aa60bb 100644 --- a/netbox/templates/ipam/prefix.html +++ b/netbox/templates/ipam/prefix.html @@ -144,15 +144,14 @@
    + {% if object.prefix.version == 4 %} + + {% endif %}
    - {% if object.prefix.version == 4 %} - - {% endif %}
    {% include 'inc/panels/custom_fields.html' %} {% include 'inc/panels/tags.html' %} @@ -168,54 +167,40 @@ {% plugin_full_width_page object %}
    -{% if object.prefix.version == 4 %} -