Compare commits

...

7 Commits

Author SHA1 Message Date
Jeremy Stretch
cb99199340 Initial POC for #21025 2026-03-03 08:17:55 -05:00
Jeremy Stretch
7ec656bc7c Introduce GitHub actions for Claude Code review (#21545) 2026-03-02 10:39:23 -06:00
Rob Duffy
06bbae0f84 Fixes #21527: UI Bug with Displaying Primary IP Address with NAT IP on a Device 2026-03-02 08:57:52 -05:00
Arthur Hanson
8ff9fd26d1 Closes #20787: Address warnings from generation of OpenAPI schema (#21521) 2026-03-02 14:38:39 +01:00
github-actions
a0e23ac3c9 Update source translation strings 2026-02-28 05:11:26 +00:00
Jeremy Stretch
071d4a63aa Fixes #21518: Ensure proper display of decimal custom fields with a zero value (#21523) 2026-02-27 09:13:53 -08:00
github-actions
7db2739465 Update source translation strings 2026-02-26 05:25:45 +00:00
19 changed files with 1445 additions and 163 deletions

View File

@@ -0,0 +1,44 @@
name: Claude Code Review
on:
pull_request:
types: [opened, synchronize, ready_for_review, reopened]
# Optional: Only run on specific file changes
# paths:
# - "src/**/*.ts"
# - "src/**/*.tsx"
# - "src/**/*.js"
# - "src/**/*.jsx"
jobs:
claude-review:
# Optional: Filter by PR author
# if: |
# github.event.pull_request.user.login == 'external-contributor' ||
# github.event.pull_request.user.login == 'new-developer' ||
# github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
issues: read
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Run Claude Code Review
id: claude-review
uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
plugin_marketplaces: 'https://github.com/anthropics/claude-code.git'
plugins: 'code-review@claude-code-plugins'
prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}'
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
# or https://code.claude.com/docs/en/cli-reference for available options

50
.github/workflows/claude.yml vendored Normal file
View File

@@ -0,0 +1,50 @@
name: Claude Code
on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
issues:
types: [opened, assigned]
pull_request_review:
types: [submitted]
jobs:
claude:
if: |
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
issues: read
id-token: write
actions: read # Required for Claude to read CI results on PRs
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Run Claude Code
id: claude
uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
# This is an optional setting that allows Claude to read CI results on PRs
additional_permissions: |
actions: read
# Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it.
# prompt: 'Update the pull request description to include a summary of changes.'
# Optional: Add claude_args to customize behavior and configuration
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
# or https://code.claude.com/docs/en/cli-reference for available options
# claude_args: '--allowed-tools Bash(gh pr:*)'

View File

@@ -2,6 +2,7 @@ import re
import typing
from collections import OrderedDict
from drf_spectacular.contrib.django_filters import DjangoFilterExtension
from drf_spectacular.extensions import OpenApiSerializerExtension, OpenApiSerializerFieldExtension, _SchemaType
from drf_spectacular.openapi import AutoSchema
from drf_spectacular.plumbing import (
@@ -9,6 +10,7 @@ from drf_spectacular.plumbing import (
build_choice_field,
build_media_type_object,
build_object_type,
follow_field_source,
get_doc,
)
from drf_spectacular.types import OpenApiTypes
@@ -23,6 +25,29 @@ BULK_ACTIONS = ("bulk_destroy", "bulk_partial_update", "bulk_update")
WRITABLE_ACTIONS = ("PATCH", "POST", "PUT")
class NetBoxDjangoFilterExtension(DjangoFilterExtension):
"""
Overrides drf-spectacular's DjangoFilterExtension to fix a regression in v0.29.0 where
_get_model_field() incorrectly double-appends to_field_name when field_name already ends
with that value (e.g. field_name='tags__slug', to_field_name='slug' produces the invalid
path ['tags', 'slug', 'slug']). This caused hundreds of spurious warnings during schema
generation for filters such as TagFilter, TenancyFilterSet.tenant, and OwnerFilterMixin.owner.
See: https://github.com/netbox-community/netbox/issues/20787
https://github.com/tfranzel/drf-spectacular/issues/1475
"""
priority = 1
def _get_model_field(self, filter_field, model):
if not filter_field.field_name:
return None
path = filter_field.field_name.split('__')
to_field_name = filter_field.extra.get('to_field_name')
if to_field_name is not None and path[-1] != to_field_name:
path.append(to_field_name)
return follow_field_source(model, path, emit_warnings=False)
class FixTimeZoneSerializerField(OpenApiSerializerFieldExtension):
target_class = 'timezone_field.rest_framework.TimeZoneSerializerField'

View File

@@ -12,7 +12,7 @@ from dcim import filtersets
from dcim.constants import CABLE_TRACE_SVG_DEFAULT_WIDTH
from dcim.models import *
from dcim.svg import CableTraceSVG
from extras.api.mixins import ConfigContextQuerySetMixin, RenderConfigMixin
from extras.api.mixins import RenderConfigMixin
from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
from netbox.api.metadata import ContentTypeMetadata
from netbox.api.pagination import StripCountAnnotationsPaginator
@@ -398,12 +398,7 @@ class PlatformViewSet(MPTTLockedMixin, NetBoxModelViewSet):
# Devices/modules
#
class DeviceViewSet(
SequentialBulkCreatesMixin,
ConfigContextQuerySetMixin,
RenderConfigMixin,
NetBoxModelViewSet
):
class DeviceViewSet(SequentialBulkCreatesMixin, RenderConfigMixin, NetBoxModelViewSet):
queryset = Device.objects.prefetch_related(
'parent_bay', # Referenced by DeviceSerializer.get_parent_device()
)

View File

@@ -0,0 +1,21 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dcim', '0226_modulebay_rebuild_tree'),
]
operations = [
migrations.AddField(
model_name='device',
name='config_context_data',
field=models.JSONField(blank=True, editable=False, null=True),
),
migrations.AddField(
model_name='module',
name='config_context_data',
field=models.JSONField(blank=True, editable=False, null=True),
),
]

View File

@@ -2683,7 +2683,7 @@ class DeviceInventoryView(DeviceComponentsView):
@register_model_view(Device, 'configcontext', path='config-context')
class DeviceConfigContextView(ObjectConfigContextView):
queryset = Device.objects.annotate_config_context_data()
queryset = Device.objects.all()
base_template = 'dcim/device/base.html'
tab = ViewTab(
label=_('Config Context'),

View File

@@ -10,34 +10,11 @@ from netbox.api.renderers import TextRenderer
from .serializers import ConfigTemplateSerializer
__all__ = (
'ConfigContextQuerySetMixin',
'ConfigTemplateRenderMixin',
'RenderConfigMixin',
)
class ConfigContextQuerySetMixin:
"""
Used by views that work with config context models (device and virtual machine).
Provides a get_queryset() method which deals with adding the config context
data annotation or not.
"""
def get_queryset(self):
"""
Build the proper queryset based on the request context
If the `brief` query param equates to True or the `exclude` query param
includes `config_context` as a value, return the base queryset.
Else, return the queryset annotated with config context data
"""
queryset = super().get_queryset()
request = self.get_serializer_context()['request']
if self.brief or 'config_context' in request.query_params.get('exclude', []):
return queryset
return queryset.annotate_config_context_data()
class ConfigTemplateRenderMixin:
"""
Provides a method to return a rendered ConfigTemplate as REST API data.

View File

@@ -22,19 +22,7 @@ if TYPE_CHECKING:
@strawberry.type
class ConfigContextMixin:
@classmethod
def get_queryset(cls, queryset, info: Info, **kwargs):
queryset = super().get_queryset(queryset, info, **kwargs)
# If `config_context` is requested, call annotate_config_context_data() on the queryset
selected = {f.name for f in info.selected_fields[0].selections}
if 'config_context' in selected and hasattr(queryset, 'annotate_config_context_data'):
return queryset.annotate_config_context_data()
return queryset
# Ensure `local_context_data` is fetched when `config_context` is requested
@strawberry_django.field(only=['local_context_data'])
@strawberry_django.field(only=['config_context_data', 'local_context_data'])
def config_context(self) -> strawberry.scalars.JSON:
return self.get_config_context()

View File

@@ -0,0 +1,40 @@
from django.core.management.base import BaseCommand
from django.db import connection
class Command(BaseCommand):
help = 'Rebuild pre-rendered config context data for all devices and/or virtual machines'
def add_arguments(self, parser):
parser.add_argument(
'--devices-only',
action='store_true',
help='Only rebuild config context data for devices',
)
parser.add_argument(
'--vms-only',
action='store_true',
help='Only rebuild config context data for virtual machines',
)
def handle(self, *args, **options):
devices_only = options['devices_only']
vms_only = options['vms_only']
with connection.cursor() as cursor:
if not vms_only:
self.stdout.write('Rebuilding config context data for devices...')
cursor.execute(
'UPDATE dcim_device SET config_context_data = compute_config_context_for_device(id)'
)
self.stdout.write(self.style.SUCCESS(f' Updated {cursor.rowcount} devices'))
if not devices_only:
self.stdout.write('Rebuilding config context data for virtual machines...')
cursor.execute(
'UPDATE virtualization_virtualmachine '
'SET config_context_data = compute_config_context_for_vm(id)'
)
self.stdout.write(self.style.SUCCESS(f' Updated {cursor.rowcount} virtual machines'))
self.stdout.write(self.style.SUCCESS('Done.'))

File diff suppressed because it is too large Load Diff

View File

@@ -225,6 +225,11 @@ class ConfigContextModel(models.Model):
"Local config context data takes precedence over source contexts in the final rendered config context"
)
)
config_context_data = models.JSONField(
blank=True,
null=True,
editable=False,
)
class Meta:
abstract = True
@@ -234,19 +239,21 @@ class ConfigContextModel(models.Model):
Compile all config data, overwriting lower-weight values with higher-weight values where a collision occurs.
Return the rendered configuration context for a device or VM.
"""
data = {}
# Use pre-rendered cached field if available
if self.config_context_data is not None:
return self.config_context_data
if not hasattr(self, 'config_context_data'):
# The annotation is not available, so we fall back to manually querying for the config context objects
config_context_data = ConfigContext.objects.get_for_object(self, aggregate_data=True) or []
# Fall back to annotation if queryset was annotated
data = {}
if hasattr(self, '_annotated_config_context_data'):
config_context_data = self._annotated_config_context_data or []
else:
# The attribute may exist, but the annotated value could be None if there is no config context data
config_context_data = self.config_context_data or []
# Last resort: compute on-the-fly
config_context_data = ConfigContext.objects.get_for_object(self, aggregate_data=True) or []
for context in config_context_data:
data = deepmerge(data, context)
# If the object has local config context data defined, merge it last
if self.local_context_data:
data = deepmerge(data, self.local_context_data)

View File

@@ -90,7 +90,7 @@ class ConfigContextModelQuerySet(RestrictedQuerySet):
"""
from extras.models import ConfigContext
return self.annotate(
config_context_data=Subquery(
_annotated_config_context_data=Subquery(
ConfigContext.objects.filter(
self._get_config_context_filters()
).annotate(

View File

@@ -206,6 +206,7 @@ class ConfigContextTest(TestCase):
"b": 456,
"c": 777
}
device.refresh_from_db()
self.assertEqual(device.get_config_context(), expected_data)
def test_name_ordering_after_weight(self):
@@ -235,6 +236,7 @@ class ConfigContextTest(TestCase):
"b": 456,
"c": 789
}
device.refresh_from_db()
self.assertEqual(device.get_config_context(), expected_data)
def test_schema_validation(self):
@@ -303,6 +305,7 @@ class ConfigContextTest(TestCase):
)
ConfigContext.objects.bulk_create([context1, context2, context3, context4])
device.refresh_from_db()
annotated_queryset = Device.objects.filter(name=device.name).annotate_config_context_data()
self.assertEqual(device.get_config_context(), annotated_queryset[0].get_config_context())
@@ -666,7 +669,7 @@ class ConfigContextTest(TestCase):
self.assertFalse(queryset.query.distinct)
# Check that tag subqueries DO use DISTINCT by inspecting the annotation
config_annotation = queryset.query.annotations.get('config_context_data')
config_annotation = queryset.query.annotations.get('_annotated_config_context_data')
self.assertIsNotNone(config_annotation)
def find_tag_subqueries(where_node):

View File

@@ -1,10 +1,12 @@
{% load i18n %}
<a href="{{ value.get_absolute_url }}"{% if name %} id="attr_{{ name }}"{% endif %}>{{ value.address.ip }}</a>
{% if value.nat_inside %}
({% trans "NAT for" %} <a href="{{ value.nat_inside.get_absolute_url }}">{{ value.nat_inside.address.ip }}</a>)
{% elif value.nat_outside.exists %}
({% trans "NAT" %}: {% for nat in value.nat_outside.all %}<a href="{{ nat.get_absolute_url }}">{{ nat.address.ip }}</a>{% if not forloop.last %}, {% endif %}{% endfor %})
{% endif %}
<span>
<a href="{{ value.get_absolute_url }}"{% if name %} id="attr_{{ name }}"{% endif %}>{{ value.address.ip }}</a>
{% if value.nat_inside %}
({% trans "NAT for" %} <a href="{{ value.nat_inside.get_absolute_url }}">{{ value.nat_inside.address.ip }}</a>)
{% elif value.nat_outside.exists %}
({% trans "NAT" %}: {% for nat in value.nat_outside.all %}<a href="{{ nat.get_absolute_url }}">{{ nat.address.ip }}</a>{% if not forloop.last %}, {% endif %}{% endfor %})
{% endif %}
</span>
<a class="btn btn-sm btn-primary copy-content" data-clipboard-target="#attr_{{ name }}" title="{% trans "Copy to clipboard" %}">
<i class="mdi mdi-content-copy"></i>
</a>

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-02-21 05:16+0000\n"
"POT-Creation-Date: 2026-02-28 05:11+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -41,9 +41,9 @@ msgstr ""
#: netbox/circuits/choices.py:21 netbox/dcim/choices.py:20
#: netbox/dcim/choices.py:102 netbox/dcim/choices.py:204
#: netbox/dcim/choices.py:257 netbox/dcim/choices.py:1929
#: netbox/dcim/choices.py:1987 netbox/dcim/choices.py:2054
#: netbox/dcim/choices.py:2076 netbox/virtualization/choices.py:20
#: netbox/dcim/choices.py:257 netbox/dcim/choices.py:1933
#: netbox/dcim/choices.py:1991 netbox/dcim/choices.py:2058
#: netbox/dcim/choices.py:2080 netbox/virtualization/choices.py:20
#: netbox/virtualization/choices.py:46 netbox/vpn/choices.py:18
#: netbox/vpn/choices.py:281
msgid "Planned"
@@ -57,8 +57,8 @@ msgstr ""
#: netbox/core/tables/tasks.py:23 netbox/dcim/choices.py:22
#: netbox/dcim/choices.py:103 netbox/dcim/choices.py:155
#: netbox/dcim/choices.py:203 netbox/dcim/choices.py:256
#: netbox/dcim/choices.py:1986 netbox/dcim/choices.py:2053
#: netbox/dcim/choices.py:2075 netbox/extras/tables/tables.py:642
#: netbox/dcim/choices.py:1990 netbox/dcim/choices.py:2057
#: netbox/dcim/choices.py:2079 netbox/extras/tables/tables.py:642
#: netbox/ipam/choices.py:31 netbox/ipam/choices.py:49
#: netbox/ipam/choices.py:69 netbox/ipam/choices.py:154
#: netbox/templates/extras/configcontext.html:29
@@ -70,8 +70,8 @@ msgid "Active"
msgstr ""
#: netbox/circuits/choices.py:24 netbox/dcim/choices.py:202
#: netbox/dcim/choices.py:255 netbox/dcim/choices.py:1985
#: netbox/dcim/choices.py:2055 netbox/dcim/choices.py:2074
#: netbox/dcim/choices.py:255 netbox/dcim/choices.py:1989
#: netbox/dcim/choices.py:2059 netbox/dcim/choices.py:2078
#: netbox/virtualization/choices.py:24 netbox/virtualization/choices.py:44
msgid "Offline"
msgstr ""
@@ -84,7 +84,7 @@ msgstr ""
msgid "Decommissioned"
msgstr ""
#: netbox/circuits/choices.py:90 netbox/dcim/choices.py:1998
#: netbox/circuits/choices.py:90 netbox/dcim/choices.py:2002
#: netbox/dcim/tables/devices.py:1208 netbox/templates/dcim/interface.html:148
#: netbox/tenancy/choices.py:17
msgid "Primary"
@@ -1995,7 +1995,7 @@ msgstr ""
#: netbox/core/choices.py:22 netbox/core/choices.py:59
#: netbox/core/constants.py:21 netbox/core/tables/tasks.py:35
#: netbox/dcim/choices.py:206 netbox/dcim/choices.py:259
#: netbox/dcim/choices.py:1988 netbox/dcim/choices.py:2078
#: netbox/dcim/choices.py:1992 netbox/dcim/choices.py:2082
#: netbox/virtualization/choices.py:48
msgid "Failed"
msgstr ""
@@ -2181,7 +2181,7 @@ msgid "User name"
msgstr ""
#: netbox/core/forms/bulk_edit.py:25 netbox/core/forms/filtersets.py:47
#: netbox/core/tables/data.py:28 netbox/dcim/choices.py:2036
#: netbox/core/tables/data.py:28 netbox/dcim/choices.py:2040
#: netbox/dcim/forms/bulk_edit.py:1105 netbox/dcim/forms/bulk_edit.py:1386
#: netbox/dcim/forms/filtersets.py:1619 netbox/dcim/forms/filtersets.py:1712
#: netbox/dcim/tables/devices.py:581 netbox/dcim/tables/devicetypes.py:233
@@ -2375,7 +2375,7 @@ msgstr ""
msgid "Rack Elevations"
msgstr ""
#: netbox/core/forms/model_forms.py:160 netbox/dcim/choices.py:1907
#: netbox/core/forms/model_forms.py:160 netbox/dcim/choices.py:1911
#: netbox/dcim/forms/bulk_edit.py:944 netbox/dcim/forms/bulk_edit.py:1340
#: netbox/dcim/forms/bulk_edit.py:1361 netbox/dcim/tables/racks.py:144
#: netbox/netbox/navigation/menu.py:316 netbox/netbox/navigation/menu.py:320
@@ -3041,8 +3041,8 @@ msgid "Staging"
msgstr ""
#: netbox/dcim/choices.py:23 netbox/dcim/choices.py:208
#: netbox/dcim/choices.py:260 netbox/dcim/choices.py:1930
#: netbox/dcim/choices.py:2079 netbox/virtualization/choices.py:23
#: netbox/dcim/choices.py:260 netbox/dcim/choices.py:1934
#: netbox/dcim/choices.py:2083 netbox/virtualization/choices.py:23
#: netbox/virtualization/choices.py:49 netbox/vpn/choices.py:282
msgid "Decommissioning"
msgstr ""
@@ -3108,7 +3108,7 @@ msgstr ""
msgid "Millimeters"
msgstr ""
#: netbox/dcim/choices.py:115 netbox/dcim/choices.py:1952
#: netbox/dcim/choices.py:115 netbox/dcim/choices.py:1956
msgid "Inches"
msgstr ""
@@ -3186,7 +3186,7 @@ msgid "Rear"
msgstr ""
#: netbox/dcim/choices.py:205 netbox/dcim/choices.py:258
#: netbox/dcim/choices.py:2077 netbox/virtualization/choices.py:47
#: netbox/dcim/choices.py:2081 netbox/virtualization/choices.py:47
msgid "Staged"
msgstr ""
@@ -3219,7 +3219,7 @@ msgid "Top to bottom"
msgstr ""
#: netbox/dcim/choices.py:235 netbox/dcim/choices.py:280
#: netbox/dcim/choices.py:1562
#: netbox/dcim/choices.py:1566
msgid "Passive"
msgstr ""
@@ -3248,8 +3248,8 @@ msgid "Proprietary"
msgstr ""
#: netbox/dcim/choices.py:606 netbox/dcim/choices.py:853
#: netbox/dcim/choices.py:1474 netbox/dcim/choices.py:1476
#: netbox/dcim/choices.py:1712 netbox/dcim/choices.py:1714
#: netbox/dcim/choices.py:1478 netbox/dcim/choices.py:1480
#: netbox/dcim/choices.py:1716 netbox/dcim/choices.py:1718
#: netbox/netbox/navigation/menu.py:212
msgid "Other"
msgstr ""
@@ -3262,11 +3262,11 @@ msgstr ""
msgid "Physical"
msgstr ""
#: netbox/dcim/choices.py:884 netbox/dcim/choices.py:1151
#: netbox/dcim/choices.py:884 netbox/dcim/choices.py:1153
msgid "Virtual"
msgstr ""
#: netbox/dcim/choices.py:885 netbox/dcim/choices.py:1351
#: netbox/dcim/choices.py:885 netbox/dcim/choices.py:1355
#: netbox/dcim/forms/bulk_edit.py:1546 netbox/dcim/forms/filtersets.py:1577
#: netbox/dcim/forms/filtersets.py:1703 netbox/dcim/forms/model_forms.py:1125
#: netbox/dcim/forms/model_forms.py:1589 netbox/netbox/navigation/menu.py:150
@@ -3275,11 +3275,11 @@ msgstr ""
msgid "Wireless"
msgstr ""
#: netbox/dcim/choices.py:1149
#: netbox/dcim/choices.py:1151
msgid "Virtual interfaces"
msgstr ""
#: netbox/dcim/choices.py:1152 netbox/dcim/forms/bulk_edit.py:1399
#: netbox/dcim/choices.py:1154 netbox/dcim/forms/bulk_edit.py:1399
#: netbox/dcim/forms/bulk_import.py:949 netbox/dcim/forms/model_forms.py:1107
#: netbox/dcim/tables/devices.py:741 netbox/templates/dcim/interface.html:112
#: netbox/virtualization/forms/bulk_edit.py:177
@@ -3288,67 +3288,67 @@ msgstr ""
msgid "Bridge"
msgstr ""
#: netbox/dcim/choices.py:1153
#: netbox/dcim/choices.py:1155
msgid "Link Aggregation Group (LAG)"
msgstr ""
#: netbox/dcim/choices.py:1157
#: netbox/dcim/choices.py:1159
msgid "FastEthernet (100 Mbps)"
msgstr ""
#: netbox/dcim/choices.py:1166
#: netbox/dcim/choices.py:1168
msgid "GigabitEthernet (1 Gbps)"
msgstr ""
#: netbox/dcim/choices.py:1184
#: netbox/dcim/choices.py:1186
msgid "2.5/5 Gbps Ethernet"
msgstr ""
#: netbox/dcim/choices.py:1191
#: netbox/dcim/choices.py:1193
msgid "10 Gbps Ethernet"
msgstr ""
#: netbox/dcim/choices.py:1206
#: netbox/dcim/choices.py:1209
msgid "25 Gbps Ethernet"
msgstr ""
#: netbox/dcim/choices.py:1216
#: netbox/dcim/choices.py:1219
msgid "40 Gbps Ethernet"
msgstr ""
#: netbox/dcim/choices.py:1226
#: netbox/dcim/choices.py:1230
msgid "50 Gbps Ethernet"
msgstr ""
#: netbox/dcim/choices.py:1236
#: netbox/dcim/choices.py:1240
msgid "100 Gbps Ethernet"
msgstr ""
#: netbox/dcim/choices.py:1257
#: netbox/dcim/choices.py:1261
msgid "200 Gbps Ethernet"
msgstr ""
#: netbox/dcim/choices.py:1271
#: netbox/dcim/choices.py:1275
msgid "400 Gbps Ethernet"
msgstr ""
#: netbox/dcim/choices.py:1289
#: netbox/dcim/choices.py:1293
msgid "800 Gbps Ethernet"
msgstr ""
#: netbox/dcim/choices.py:1298
#: netbox/dcim/choices.py:1302
msgid "Pluggable transceivers"
msgstr ""
#: netbox/dcim/choices.py:1335
#: netbox/dcim/choices.py:1339
msgid "Backplane Ethernet"
msgstr ""
#: netbox/dcim/choices.py:1367
#: netbox/dcim/choices.py:1371
msgid "Cellular"
msgstr ""
#: netbox/dcim/choices.py:1419 netbox/dcim/forms/filtersets.py:425
#: netbox/dcim/choices.py:1423 netbox/dcim/forms/filtersets.py:425
#: netbox/dcim/forms/filtersets.py:911 netbox/dcim/forms/filtersets.py:1112
#: netbox/dcim/forms/filtersets.py:1910
#: netbox/templates/dcim/inventoryitem.html:56
@@ -3356,255 +3356,255 @@ msgstr ""
msgid "Serial"
msgstr ""
#: netbox/dcim/choices.py:1434
#: netbox/dcim/choices.py:1438
msgid "Coaxial"
msgstr ""
#: netbox/dcim/choices.py:1455
#: netbox/dcim/choices.py:1459
msgid "Stacking"
msgstr ""
#: netbox/dcim/choices.py:1507
#: netbox/dcim/choices.py:1511
msgid "Half"
msgstr ""
#: netbox/dcim/choices.py:1508
#: netbox/dcim/choices.py:1512
msgid "Full"
msgstr ""
#: netbox/dcim/choices.py:1509 netbox/netbox/preferences.py:32
#: netbox/dcim/choices.py:1513 netbox/netbox/preferences.py:32
#: netbox/wireless/choices.py:480
msgid "Auto"
msgstr ""
#: netbox/dcim/choices.py:1521
#: netbox/dcim/choices.py:1525
msgid "Access"
msgstr ""
#: netbox/dcim/choices.py:1522 netbox/ipam/tables/vlans.py:150
#: netbox/dcim/choices.py:1526 netbox/ipam/tables/vlans.py:150
#: netbox/ipam/tables/vlans.py:210
#: netbox/templates/dcim/inc/interface_vlans_table.html:7
msgid "Tagged"
msgstr ""
#: netbox/dcim/choices.py:1523
#: netbox/dcim/choices.py:1527
msgid "Tagged (All)"
msgstr ""
#: netbox/dcim/choices.py:1524 netbox/templates/ipam/vlan_edit.html:26
#: netbox/dcim/choices.py:1528 netbox/templates/ipam/vlan_edit.html:26
msgid "Q-in-Q (802.1ad)"
msgstr ""
#: netbox/dcim/choices.py:1553
#: netbox/dcim/choices.py:1557
msgid "IEEE Standard"
msgstr ""
#: netbox/dcim/choices.py:1564
#: netbox/dcim/choices.py:1568
msgid "Passive 24V (2-pair)"
msgstr ""
#: netbox/dcim/choices.py:1565
#: netbox/dcim/choices.py:1569
msgid "Passive 24V (4-pair)"
msgstr ""
#: netbox/dcim/choices.py:1566
#: netbox/dcim/choices.py:1570
msgid "Passive 48V (2-pair)"
msgstr ""
#: netbox/dcim/choices.py:1567
#: netbox/dcim/choices.py:1571
msgid "Passive 48V (4-pair)"
msgstr ""
#: netbox/dcim/choices.py:1640
#: netbox/dcim/choices.py:1644
msgid "Copper"
msgstr ""
#: netbox/dcim/choices.py:1663
#: netbox/dcim/choices.py:1667
msgid "Fiber Optic"
msgstr ""
#: netbox/dcim/choices.py:1699 netbox/dcim/choices.py:1913
#: netbox/dcim/choices.py:1703 netbox/dcim/choices.py:1917
msgid "USB"
msgstr ""
#: netbox/dcim/choices.py:1755
#: netbox/dcim/choices.py:1759
msgid "Single"
msgstr ""
#: netbox/dcim/choices.py:1757
#: netbox/dcim/choices.py:1761
msgid "1C1P"
msgstr ""
#: netbox/dcim/choices.py:1758
#: netbox/dcim/choices.py:1762
msgid "1C2P"
msgstr ""
#: netbox/dcim/choices.py:1759
#: netbox/dcim/choices.py:1763
msgid "1C4P"
msgstr ""
#: netbox/dcim/choices.py:1760
#: netbox/dcim/choices.py:1764
msgid "1C6P"
msgstr ""
#: netbox/dcim/choices.py:1761
#: netbox/dcim/choices.py:1765
msgid "1C8P"
msgstr ""
#: netbox/dcim/choices.py:1762
#: netbox/dcim/choices.py:1766
msgid "1C12P"
msgstr ""
#: netbox/dcim/choices.py:1763
#: netbox/dcim/choices.py:1767
msgid "1C16P"
msgstr ""
#: netbox/dcim/choices.py:1767
#: netbox/dcim/choices.py:1771
msgid "Trunk"
msgstr ""
#: netbox/dcim/choices.py:1769
#: netbox/dcim/choices.py:1773
msgid "2C1P trunk"
msgstr ""
#: netbox/dcim/choices.py:1770
#: netbox/dcim/choices.py:1774
msgid "2C2P trunk"
msgstr ""
#: netbox/dcim/choices.py:1771
#: netbox/dcim/choices.py:1775
msgid "2C4P trunk"
msgstr ""
#: netbox/dcim/choices.py:1772
#: netbox/dcim/choices.py:1776
msgid "2C4P trunk (shuffle)"
msgstr ""
#: netbox/dcim/choices.py:1773
#: netbox/dcim/choices.py:1777
msgid "2C6P trunk"
msgstr ""
#: netbox/dcim/choices.py:1774
#: netbox/dcim/choices.py:1778
msgid "2C8P trunk"
msgstr ""
#: netbox/dcim/choices.py:1775
#: netbox/dcim/choices.py:1779
msgid "2C12P trunk"
msgstr ""
#: netbox/dcim/choices.py:1776
#: netbox/dcim/choices.py:1780
msgid "4C1P trunk"
msgstr ""
#: netbox/dcim/choices.py:1777
#: netbox/dcim/choices.py:1781
msgid "4C2P trunk"
msgstr ""
#: netbox/dcim/choices.py:1778
#: netbox/dcim/choices.py:1782
msgid "4C4P trunk"
msgstr ""
#: netbox/dcim/choices.py:1779
#: netbox/dcim/choices.py:1783
msgid "4C4P trunk (shuffle)"
msgstr ""
#: netbox/dcim/choices.py:1780
#: netbox/dcim/choices.py:1784
msgid "4C6P trunk"
msgstr ""
#: netbox/dcim/choices.py:1781
#: netbox/dcim/choices.py:1785
msgid "4C8P trunk"
msgstr ""
#: netbox/dcim/choices.py:1782
#: netbox/dcim/choices.py:1786
msgid "8C4P trunk"
msgstr ""
#: netbox/dcim/choices.py:1786
#: netbox/dcim/choices.py:1790
msgid "Breakout"
msgstr ""
#: netbox/dcim/choices.py:1788
#: netbox/dcim/choices.py:1792
msgid "1C4P:4C1P breakout"
msgstr ""
#: netbox/dcim/choices.py:1789
#: netbox/dcim/choices.py:1793
msgid "1C6P:6C1P breakout"
msgstr ""
#: netbox/dcim/choices.py:1790
#: netbox/dcim/choices.py:1794
msgid "2C4P:8C1P breakout (shuffle)"
msgstr ""
#: netbox/dcim/choices.py:1848
#: netbox/dcim/choices.py:1852
msgid "Copper - Twisted Pair (UTP/STP)"
msgstr ""
#: netbox/dcim/choices.py:1862
#: netbox/dcim/choices.py:1866
msgid "Copper - Twinax (DAC)"
msgstr ""
#: netbox/dcim/choices.py:1869
#: netbox/dcim/choices.py:1873
msgid "Copper - Coaxial"
msgstr ""
#: netbox/dcim/choices.py:1884
#: netbox/dcim/choices.py:1888
msgid "Fiber - Multimode"
msgstr ""
#: netbox/dcim/choices.py:1895
#: netbox/dcim/choices.py:1899
msgid "Fiber - Single-mode"
msgstr ""
#: netbox/dcim/choices.py:1903
#: netbox/dcim/choices.py:1907
msgid "Fiber - Other"
msgstr ""
#: netbox/dcim/choices.py:1928 netbox/dcim/forms/filtersets.py:1402
#: netbox/dcim/choices.py:1932 netbox/dcim/forms/filtersets.py:1402
msgid "Connected"
msgstr ""
#: netbox/dcim/choices.py:1947 netbox/netbox/choices.py:177
#: netbox/dcim/choices.py:1951 netbox/netbox/choices.py:177
msgid "Kilometers"
msgstr ""
#: netbox/dcim/choices.py:1948 netbox/netbox/choices.py:178
#: netbox/dcim/choices.py:1952 netbox/netbox/choices.py:178
#: netbox/templates/dcim/cable_trace.html:65
msgid "Meters"
msgstr ""
#: netbox/dcim/choices.py:1949
#: netbox/dcim/choices.py:1953
msgid "Centimeters"
msgstr ""
#: netbox/dcim/choices.py:1950 netbox/netbox/choices.py:179
#: netbox/dcim/choices.py:1954 netbox/netbox/choices.py:179
msgid "Miles"
msgstr ""
#: netbox/dcim/choices.py:1951 netbox/netbox/choices.py:180
#: netbox/dcim/choices.py:1955 netbox/netbox/choices.py:180
#: netbox/templates/dcim/cable_trace.html:66
msgid "Feet"
msgstr ""
#: netbox/dcim/choices.py:1999
#: netbox/dcim/choices.py:2003
msgid "Redundant"
msgstr ""
#: netbox/dcim/choices.py:2020
#: netbox/dcim/choices.py:2024
msgid "Single phase"
msgstr ""
#: netbox/dcim/choices.py:2021
#: netbox/dcim/choices.py:2025
msgid "Three-phase"
msgstr ""
#: netbox/dcim/choices.py:2037 netbox/extras/choices.py:53
#: netbox/dcim/choices.py:2041 netbox/extras/choices.py:53
#: netbox/netbox/preferences.py:45 netbox/netbox/preferences.py:70
#: netbox/templates/extras/customfield.html:78 netbox/vpn/choices.py:20
#: netbox/wireless/choices.py:27
msgid "Disabled"
msgstr ""
#: netbox/dcim/choices.py:2038
#: netbox/dcim/choices.py:2042
msgid "Faulty"
msgstr ""
@@ -16677,7 +16677,7 @@ msgstr ""
msgid "A column named {name} is already defined for table {table_name}"
msgstr ""
#: netbox/utilities/templates/builtins/customfield_value.html:30
#: netbox/utilities/templates/builtins/customfield_value.html:32
msgid "Not defined"
msgstr ""

View File

@@ -2,6 +2,8 @@
{% load i18n %}
{% if customfield.type == 'integer' and value is not None %}
{{ value }}
{% elif customfield.type == 'decimal' and value is not None %}
{{ value }}
{% elif customfield.type == 'longtext' and value %}
{{ value|markdown }}
{% elif customfield.type == 'boolean' and value == True %}

View File

@@ -1,7 +1,7 @@
from django.db.models import Sum
from rest_framework.routers import APIRootView
from extras.api.mixins import ConfigContextQuerySetMixin, RenderConfigMixin
from extras.api.mixins import RenderConfigMixin
from netbox.api.viewsets import NetBoxModelViewSet
from utilities.query_functions import CollateAsChar
from virtualization import filtersets
@@ -48,7 +48,7 @@ class ClusterViewSet(NetBoxModelViewSet):
# Virtual machines
#
class VirtualMachineViewSet(ConfigContextQuerySetMixin, RenderConfigMixin, NetBoxModelViewSet):
class VirtualMachineViewSet(RenderConfigMixin, NetBoxModelViewSet):
queryset = VirtualMachine.objects.all()
filterset_class = filtersets.VirtualMachineFilterSet

View File

@@ -0,0 +1,16 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('virtualization', '0052_gfk_indexes'),
]
operations = [
migrations.AddField(
model_name='virtualmachine',
name='config_context_data',
field=models.JSONField(blank=True, editable=False, null=True),
),
]

View File

@@ -487,7 +487,7 @@ class VirtualMachineVirtualDisksView(generic.ObjectChildrenView):
@register_model_view(VirtualMachine, 'configcontext', path='config-context')
class VirtualMachineConfigContextView(ObjectConfigContextView):
queryset = VirtualMachine.objects.annotate_config_context_data()
queryset = VirtualMachine.objects.all()
base_template = 'virtualization/virtualmachine.html'
tab = ViewTab(
label=_('Config Context'),