mirror of
https://github.com/netbox-community/netbox.git
synced 2026-03-06 22:40:05 +01:00
Compare commits
12 Commits
20123-expo
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
67defb3228 | ||
|
|
cca4cc61b6 | ||
|
|
758b230403 | ||
|
|
8ea33df148 | ||
|
|
685c1afdcf | ||
|
|
d62a0d7d8d | ||
|
|
1c527366c9 | ||
|
|
e1684fb645 | ||
|
|
969ae81574 | ||
|
|
baec71fcaf | ||
|
|
44abeeff5a | ||
|
|
93e01d5b07 |
@@ -84,6 +84,8 @@ intake policy](https://github.com/netbox-community/netbox/wiki/Issue-Intake-Poli
|
||||
|
||||
* It's very important that you not submit a pull request until a relevant issue has been opened **and** assigned to you. Otherwise, you risk wasting time on work that may ultimately not be needed.
|
||||
|
||||
* Community members are limited to a maximum of **three open PRs** at any time. This is to avoid the accumulation of too much parallel work and maintain focus on already PRs under review. If you already have three NetBox PRs open, please wait for at least one of them to be merged (or closed) before opening another.
|
||||
|
||||
* New pull requests should generally be based off of the `main` branch. This branch, in keeping with the [trunk-based development](https://trunkbaseddevelopment.com/) approach, is used for ongoing development and bug fixes and always represents the newest stable code, from which releases are periodically branched. (If you're developing for an upcoming minor release, use `feature` instead.)
|
||||
|
||||
* In most cases, it is not necessary to add a changelog entry: A maintainer will take care of this when the PR is merged. (This helps avoid merge conflicts resulting from multiple PRs being submitted simultaneously.)
|
||||
@@ -96,10 +98,10 @@ intake policy](https://github.com/netbox-community/netbox/wiki/Issue-Intake-Poli
|
||||
greater than 80 characters in length
|
||||
|
||||
> [!CAUTION]
|
||||
> Any contributions which include AI-generated or reproduced content will be rejected.
|
||||
> Any contributions which include solely AI-generated or reproduced content will be rejected. All PRs must be submitted by a human.
|
||||
|
||||
* Some other tips to keep in mind:
|
||||
* If you'd like to volunteer for someone else's issue, please post a comment on that issue letting us know. (This will allow the maintainers to assign it to you.)
|
||||
* If you'd like to volunteer for someone else's issue, please post a comment on that issue letting us know. (GitHub allows only people who have commented on an issue to be assigned as its owner.)
|
||||
* Check out our [developer docs](https://docs.netbox.dev/en/stable/development/getting-started/) for tips on setting up your development environment.
|
||||
* All new functionality must include relevant tests where applicable.
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ colorama
|
||||
|
||||
# The Python web framework on which NetBox is built
|
||||
# https://docs.djangoproject.com/en/stable/releases/
|
||||
Django==6.0.*
|
||||
Django==5.2.*
|
||||
|
||||
# Django middleware which permits cross-domain API requests
|
||||
# https://github.com/adamchainz/django-cors-headers/blob/main/CHANGELOG.rst
|
||||
@@ -35,9 +35,7 @@ django-pglocks
|
||||
|
||||
# Prometheus metrics library for Django
|
||||
# https://github.com/korfuri/django-prometheus/blob/master/CHANGELOG.md
|
||||
# TODO: 2.4.1 is incompatible with Django>=6.0, but a fixed release is expected
|
||||
# https://github.com/django-commons/django-prometheus/issues/494
|
||||
django-prometheus>=2.4.0,<2.5.0,!=2.4.1
|
||||
django-prometheus
|
||||
|
||||
# Django caching backend using Redis
|
||||
# https://github.com/jazzband/django-redis/blob/master/CHANGELOG.rst
|
||||
|
||||
@@ -23,19 +23,14 @@ For example, you might create a NetBox webhook to [trigger a Slack message](http
|
||||
|
||||
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.
|
||||
* `event` - The type of event which triggered the webhook: `created`, `updated`, or `deleted`.
|
||||
* `timestamp` - The time at which the event occurred (in [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) format).
|
||||
* `object_type` - The NetBox model which triggered the change in the form `app_label.model_name`.
|
||||
* `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.
|
||||
|
||||
!!! warning "Deprecation of legacy fields"
|
||||
The "request_id" and "username" fields in the webhook payload above are deprecated and should no longer be used. Support for them will be removed in NetBox v4.7.0.
|
||||
|
||||
Use `request.user.username` and `request.request_id` from the `request` object included in the callback context instead.
|
||||
|
||||
### 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:
|
||||
@@ -43,18 +38,20 @@ If no body template is specified, the request body will be populated with a JSON
|
||||
```json
|
||||
{
|
||||
"event": "created",
|
||||
"timestamp": "2021-03-09 17:55:33.968016+00:00",
|
||||
"model": "site",
|
||||
"timestamp": "2026-03-06T15:11:23.503186+00:00",
|
||||
"object_type": "dcim.site",
|
||||
"username": "jstretch",
|
||||
"request_id": "fdbca812-3142-4783-b364-2e2bd5c16c6a",
|
||||
"request_id": "17af32f0-852a-46ca-a7d4-33ecd0c13de6",
|
||||
"data": {
|
||||
"id": 19,
|
||||
"id": 4,
|
||||
"url": "/api/dcim/sites/4/",
|
||||
"display_url": "/dcim/sites/4/",
|
||||
"display": "Site 1",
|
||||
"name": "Site 1",
|
||||
"slug": "site-1",
|
||||
"status":
|
||||
"status": {
|
||||
"value": "active",
|
||||
"label": "Active",
|
||||
"id": 1
|
||||
"label": "Active"
|
||||
},
|
||||
"region": null,
|
||||
...
|
||||
@@ -62,8 +59,10 @@ If no body template is specified, the request body will be populated with a JSON
|
||||
"snapshots": {
|
||||
"prechange": null,
|
||||
"postchange": {
|
||||
"created": "2021-03-09",
|
||||
"last_updated": "2021-03-09T17:55:33.851Z",
|
||||
"created": "2026-03-06T15:11:23.484Z",
|
||||
"owner": null,
|
||||
"description": "",
|
||||
"comments": "",
|
||||
"name": "Site 1",
|
||||
"slug": "site-1",
|
||||
"status": "active",
|
||||
|
||||
@@ -77,19 +77,14 @@ The file path to a particular certificate authority (CA) file to use when valida
|
||||
|
||||
## Context Data
|
||||
|
||||
The following context variables are available in to the text and link templates.
|
||||
The following context variables are available 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 |
|
||||
|
||||
!!! warning "Deprecation of legacy fields"
|
||||
The "request_id" and "username" fields in the webhook payload above are deprecated and should no longer be used. Support for them will be removed in NetBox v4.7.0.
|
||||
|
||||
Use `request.user.username` and `request.request_id` from the `request` object included in the callback context instead.
|
||||
| Variable | Description |
|
||||
|---------------|------------------------------------------------------|
|
||||
| `event` | The event type (`created`, `updated`, or `deleted`) |
|
||||
| `timestamp` | The time at which the event occurred |
|
||||
| `object_type` | The type of object impacted (`app_label.model_name`) |
|
||||
| `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 |
|
||||
|
||||
@@ -43,11 +43,6 @@ The resulting webhook payload will look like the following:
|
||||
}
|
||||
```
|
||||
|
||||
!!! warning "Deprecation of legacy fields"
|
||||
The "request_id" and "username" fields in the webhook payload above are deprecated and should no longer be used. Support for them will be removed in NetBox v4.7.0.
|
||||
|
||||
Use `request.user.username` and `request.request_id` from the `request` object included in the callback context instead.
|
||||
|
||||
!!! note "Consider namespacing webhook data"
|
||||
The data returned from all webhook callbacks will be compiled into a single `context` dictionary. Any existing keys within this dictionary will be overwritten by subsequent callbacks which include those keys. To avoid collisions with webhook data provided by other plugins, consider namespacing your plugin's data within a nested dictionary as such:
|
||||
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import django_tables2 as tables
|
||||
from django.utils.html import conditional_escape
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from core.constants import JOB_LOG_ENTRY_LEVELS
|
||||
@@ -84,9 +82,3 @@ class JobLogEntryTable(BaseTable):
|
||||
class Meta(BaseTable.Meta):
|
||||
empty_text = _('No log entries')
|
||||
fields = ('timestamp', 'level', 'message')
|
||||
|
||||
def render_message(self, record, value):
|
||||
if record.get('level') == 'error' and '\n' in value:
|
||||
value = conditional_escape(value)
|
||||
return mark_safe(f'<pre class="p-0">{value}</pre>')
|
||||
return value
|
||||
|
||||
@@ -6,7 +6,7 @@ from drf_spectacular.utils import extend_schema_field
|
||||
from rest_framework import serializers
|
||||
|
||||
from dcim.choices import *
|
||||
from dcim.constants import MACADDRESS_ASSIGNMENT_MODELS, MODULE_TOKEN
|
||||
from dcim.constants import MACADDRESS_ASSIGNMENT_MODELS
|
||||
from dcim.models import Device, DeviceBay, MACAddress, Module, VirtualDeviceContext
|
||||
from extras.api.serializers_.configtemplates import ConfigTemplateSerializer
|
||||
from ipam.api.serializers_.ip import IPAddressSerializer
|
||||
@@ -150,144 +150,15 @@ class ModuleSerializer(PrimaryModelSerializer):
|
||||
module_bay = NestedModuleBaySerializer()
|
||||
module_type = ModuleTypeSerializer(nested=True)
|
||||
status = ChoiceField(choices=ModuleStatusChoices, required=False)
|
||||
replicate_components = serializers.BooleanField(
|
||||
required=False,
|
||||
default=True,
|
||||
write_only=True,
|
||||
label=_('Replicate components'),
|
||||
help_text=_('Automatically populate components associated with this module type (default: true)')
|
||||
)
|
||||
adopt_components = serializers.BooleanField(
|
||||
required=False,
|
||||
default=False,
|
||||
write_only=True,
|
||||
label=_('Adopt components'),
|
||||
help_text=_('Adopt already existing components')
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Module
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'device', 'module_bay', 'module_type', 'status', 'serial',
|
||||
'asset_tag', 'description', 'owner', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||
'replicate_components', 'adopt_components',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'device', 'module_bay', 'module_type', 'description')
|
||||
|
||||
def validate(self, data):
|
||||
# When used as a nested serializer (e.g. as the `module` field on device component
|
||||
# serializers), `data` is already a resolved Module instance — skip our custom logic.
|
||||
if self.nested:
|
||||
return super().validate(data)
|
||||
|
||||
# Pop write-only transient fields before ValidatedModelSerializer tries to
|
||||
# construct a Module instance for full_clean(); restore them afterwards.
|
||||
replicate_components = data.pop('replicate_components', True)
|
||||
adopt_components = data.pop('adopt_components', False)
|
||||
data = super().validate(data)
|
||||
|
||||
# For updates these fields are not meaningful; omit them from validated_data so that
|
||||
# ModelSerializer.update() does not set unexpected attributes on the instance.
|
||||
if self.instance:
|
||||
return data
|
||||
|
||||
# Always pass the flags to create() so it can set the correct private attributes.
|
||||
data['replicate_components'] = replicate_components
|
||||
data['adopt_components'] = adopt_components
|
||||
|
||||
# Skip conflict checks when no component operations are requested.
|
||||
if not replicate_components and not adopt_components:
|
||||
return data
|
||||
|
||||
device = data.get('device')
|
||||
module_type = data.get('module_type')
|
||||
module_bay = data.get('module_bay')
|
||||
|
||||
# Required-field validation fires separately; skip here if any are missing.
|
||||
if not all([device, module_type, module_bay]):
|
||||
return data
|
||||
|
||||
# Build module bay tree for MODULE_TOKEN placeholder resolution (outermost to innermost)
|
||||
module_bays = []
|
||||
current_bay = module_bay
|
||||
while current_bay:
|
||||
module_bays.append(current_bay)
|
||||
current_bay = current_bay.module.module_bay if current_bay.module else None
|
||||
module_bays.reverse()
|
||||
|
||||
for templates_attr, component_attr in [
|
||||
('consoleporttemplates', 'consoleports'),
|
||||
('consoleserverporttemplates', 'consoleserverports'),
|
||||
('interfacetemplates', 'interfaces'),
|
||||
('powerporttemplates', 'powerports'),
|
||||
('poweroutlettemplates', 'poweroutlets'),
|
||||
('rearporttemplates', 'rearports'),
|
||||
('frontporttemplates', 'frontports'),
|
||||
]:
|
||||
installed_components = {
|
||||
component.name: component
|
||||
for component in getattr(device, component_attr).all()
|
||||
}
|
||||
|
||||
for template in getattr(module_type, templates_attr).all():
|
||||
resolved_name = template.name
|
||||
if MODULE_TOKEN in template.name:
|
||||
if not module_bay.position:
|
||||
raise serializers.ValidationError(
|
||||
_("Cannot install module with placeholder values in a module bay with no position defined.")
|
||||
)
|
||||
if template.name.count(MODULE_TOKEN) != len(module_bays):
|
||||
raise serializers.ValidationError(
|
||||
_(
|
||||
"Cannot install module with {tokens} placeholder(s) in a module bay at depth {level}."
|
||||
).format(
|
||||
level=len(module_bays), tokens=template.name.count(MODULE_TOKEN)
|
||||
)
|
||||
)
|
||||
for bay in module_bays:
|
||||
resolved_name = resolved_name.replace(MODULE_TOKEN, bay.position, 1)
|
||||
|
||||
existing_item = installed_components.get(resolved_name)
|
||||
|
||||
if adopt_components and existing_item and existing_item.module:
|
||||
raise serializers.ValidationError(
|
||||
_("Cannot adopt {model} {name} as it already belongs to a module").format(
|
||||
model=template.component_model.__name__,
|
||||
name=resolved_name
|
||||
)
|
||||
)
|
||||
|
||||
if not adopt_components and replicate_components and resolved_name in installed_components:
|
||||
raise serializers.ValidationError(
|
||||
_("A {model} named {name} already exists").format(
|
||||
model=template.component_model.__name__,
|
||||
name=resolved_name
|
||||
)
|
||||
)
|
||||
|
||||
return data
|
||||
|
||||
def create(self, validated_data):
|
||||
replicate_components = validated_data.pop('replicate_components', True)
|
||||
adopt_components = validated_data.pop('adopt_components', False)
|
||||
|
||||
# Tags are handled after save; pop them here to pass to _save_tags()
|
||||
tags = validated_data.pop('tags', None)
|
||||
|
||||
# _adopt_components and _disable_replication must be set on the instance before
|
||||
# save() is called, so we cannot delegate to super().create() here.
|
||||
instance = self.Meta.model(**validated_data)
|
||||
if adopt_components:
|
||||
instance._adopt_components = True
|
||||
if not replicate_components:
|
||||
instance._disable_replication = True
|
||||
instance.save()
|
||||
|
||||
if tags is not None:
|
||||
self._save_tags(instance, tags)
|
||||
|
||||
return instance
|
||||
|
||||
|
||||
class MACAddressSerializer(PrimaryModelSerializer):
|
||||
assigned_object_type = ContentTypeField(
|
||||
|
||||
@@ -306,12 +306,9 @@ class LocationFilterSet(TenancyFilterSet, ContactModelFilterSet, NestedGroupMode
|
||||
fields = ('id', 'name', 'slug', 'facility', 'description')
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
# extended in order to include querying on Location.facility
|
||||
queryset = super().search(queryset, name, value)
|
||||
|
||||
# Extend `search()` to include querying on Location.facility
|
||||
if value.strip():
|
||||
queryset = queryset | queryset.model.objects.filter(facility__icontains=value)
|
||||
|
||||
return super().search(queryset, name, value) | queryset.filter(facility__icontains=value)
|
||||
return queryset
|
||||
|
||||
|
||||
|
||||
@@ -267,32 +267,32 @@ class DeviceFilter(
|
||||
longitude: Annotated['FloatLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
console_ports: Annotated['ConsolePortFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
consoleports: Annotated['ConsolePortFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field(name='console_ports')
|
||||
)
|
||||
console_server_ports: Annotated['ConsoleServerPortFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
consoleserverports: Annotated['ConsoleServerPortFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field(name='console_server_ports')
|
||||
)
|
||||
power_outlets: Annotated['PowerOutletFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
poweroutlets: Annotated['PowerOutletFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field(name='power_outlets')
|
||||
)
|
||||
power_ports: Annotated['PowerPortFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
powerports: Annotated['PowerPortFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field(name='power_ports')
|
||||
)
|
||||
interfaces: Annotated['InterfaceFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
front_ports: Annotated['FrontPortFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
frontports: Annotated['FrontPortFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field(name='front_ports')
|
||||
)
|
||||
rear_ports: Annotated['RearPortFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
rearports: Annotated['RearPortFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field(name='rear_ports')
|
||||
)
|
||||
device_bays: Annotated['DeviceBayFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
devicebays: Annotated['DeviceBayFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field(name='device_bays')
|
||||
)
|
||||
module_bays: Annotated['ModuleBayFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
modulebays: Annotated['ModuleBayFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field(name='module_bays')
|
||||
)
|
||||
modules: Annotated['ModuleFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
@@ -383,36 +383,36 @@ class DeviceTypeFilter(ImageAttachmentFilterMixin, WeightFilterMixin, PrimaryMod
|
||||
rear_image: Annotated['ImageAttachmentFilter', strawberry.lazy('extras.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
console_port_templates: (
|
||||
Annotated['ConsolePortTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None
|
||||
) = strawberry_django.filter_field()
|
||||
console_server_port_templates: (
|
||||
consoleporttemplates: Annotated['ConsolePortTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field(name='console_port_templates')
|
||||
)
|
||||
consoleserverporttemplates: (
|
||||
Annotated['ConsoleServerPortTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None
|
||||
) = strawberry_django.filter_field()
|
||||
power_port_templates: (
|
||||
Annotated['PowerPortTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None
|
||||
) = strawberry_django.filter_field()
|
||||
power_outlet_templates: (
|
||||
Annotated['PowerOutletTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None
|
||||
) = strawberry_django.filter_field()
|
||||
interface_templates: (
|
||||
Annotated['InterfaceTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None
|
||||
) = strawberry_django.filter_field()
|
||||
front_port_templates: (
|
||||
Annotated['FrontPortTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None
|
||||
) = strawberry_django.filter_field()
|
||||
rear_port_templates: (
|
||||
Annotated['RearPortTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None
|
||||
) = strawberry_django.filter_field()
|
||||
device_bay_templates: (
|
||||
Annotated['DeviceBayTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None
|
||||
) = strawberry_django.filter_field()
|
||||
module_bay_templates: (
|
||||
Annotated['ModuleBayTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None
|
||||
) = strawberry_django.filter_field()
|
||||
inventory_item_templates: (
|
||||
Annotated['InventoryItemTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None
|
||||
) = strawberry_django.filter_field()
|
||||
) = strawberry_django.filter_field(name='console_server_port_templates')
|
||||
powerporttemplates: Annotated['PowerPortTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field(name='power_port_templates')
|
||||
)
|
||||
poweroutlettemplates: Annotated['PowerOutletTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field(name='power_outlet_templates')
|
||||
)
|
||||
interfacetemplates: Annotated['InterfaceTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field(name='interface_templates')
|
||||
)
|
||||
frontporttemplates: Annotated['FrontPortTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field(name='front_port_templates')
|
||||
)
|
||||
rearporttemplates: Annotated['RearPortTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field(name='rear_port_templates')
|
||||
)
|
||||
devicebaytemplates: Annotated['DeviceBayTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field(name='device_bay_templates')
|
||||
)
|
||||
modulebaytemplates: Annotated['ModuleBayTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field(name='module_bay_templates')
|
||||
)
|
||||
inventoryitemtemplates: Annotated['InventoryItemTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field(name='inventory_item_templates')
|
||||
)
|
||||
console_port_template_count: FilterLookup[int] | None = strawberry_django.filter_field()
|
||||
console_server_port_template_count: FilterLookup[int] | None = strawberry_django.filter_field()
|
||||
power_port_template_count: FilterLookup[int] | None = strawberry_django.filter_field()
|
||||
@@ -696,32 +696,32 @@ class ModuleFilter(ConfigContextFilterMixin, PrimaryModelFilter):
|
||||
)
|
||||
serial: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
asset_tag: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
console_ports: Annotated['ConsolePortFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
consoleports: Annotated['ConsolePortFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field(name='console_ports')
|
||||
)
|
||||
console_server_ports: Annotated['ConsoleServerPortFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
consoleserverports: Annotated['ConsoleServerPortFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field(name='console_server_ports')
|
||||
)
|
||||
power_outlets: Annotated['PowerOutletFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
poweroutlets: Annotated['PowerOutletFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field(name='power_outlets')
|
||||
)
|
||||
power_ports: Annotated['PowerPortFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
powerports: Annotated['PowerPortFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field(name='power_ports')
|
||||
)
|
||||
interfaces: Annotated['InterfaceFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
front_ports: Annotated['FrontPortFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
frontports: Annotated['FrontPortFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field(name='front_ports')
|
||||
)
|
||||
rear_ports: Annotated['RearPortFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
rearports: Annotated['RearPortFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field(name='rear_ports')
|
||||
)
|
||||
device_bays: Annotated['DeviceBayFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
devicebays: Annotated['DeviceBayFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field(name='device_bays')
|
||||
)
|
||||
module_bays: Annotated['ModuleBayFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
modulebays: Annotated['ModuleBayFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field(name='module_bays')
|
||||
)
|
||||
modules: Annotated['ModuleFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
@@ -765,36 +765,33 @@ class ModuleTypeFilter(ImageAttachmentFilterMixin, WeightFilterMixin, PrimaryMod
|
||||
airflow: BaseFilterLookup[Annotated['ModuleAirflowEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
console_port_templates: (
|
||||
Annotated['ConsolePortTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None
|
||||
) = strawberry_django.filter_field()
|
||||
console_server_port_templates: (
|
||||
consoleporttemplates: Annotated['ConsolePortTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field(name='console_port_templates')
|
||||
)
|
||||
consoleserverporttemplates: (
|
||||
Annotated['ConsoleServerPortTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None
|
||||
) = strawberry_django.filter_field()
|
||||
power_port_templates: (
|
||||
Annotated['PowerPortTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None
|
||||
) = strawberry_django.filter_field()
|
||||
power_outlet_templates: (
|
||||
Annotated['PowerOutletTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None
|
||||
) = strawberry_django.filter_field()
|
||||
interface_templates: (
|
||||
Annotated['InterfaceTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None
|
||||
) = strawberry_django.filter_field()
|
||||
front_port_templates: (
|
||||
Annotated['FrontPortTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None
|
||||
) = strawberry_django.filter_field()
|
||||
rear_port_templates: (
|
||||
Annotated['RearPortTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None
|
||||
) = strawberry_django.filter_field()
|
||||
device_bay_templates: (
|
||||
Annotated['DeviceBayTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None
|
||||
) = strawberry_django.filter_field()
|
||||
module_bay_templates: (
|
||||
Annotated['ModuleBayTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None
|
||||
) = strawberry_django.filter_field()
|
||||
inventory_item_templates: (
|
||||
Annotated['InventoryItemTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None
|
||||
) = strawberry_django.filter_field()
|
||||
) = strawberry_django.filter_field(name='console_server_port_templates')
|
||||
powerporttemplates: Annotated['PowerPortTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field(name='power_port_templates')
|
||||
)
|
||||
poweroutlettemplates: Annotated['PowerOutletTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field(name='power_outlet_templates')
|
||||
)
|
||||
interfacetemplates: Annotated['InterfaceTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field(name='interface_templates')
|
||||
)
|
||||
frontporttemplates: Annotated['FrontPortTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field(name='front_port_templates')
|
||||
)
|
||||
rearporttemplates: Annotated['RearPortTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field(name='rear_port_templates')
|
||||
)
|
||||
devicebaytemplates: Annotated['DeviceBayTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field(name='device_bay_templates')
|
||||
)
|
||||
modulebaytemplates: Annotated['ModuleBayTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field(name='module_bay_templates')
|
||||
)
|
||||
module_count: ComparisonFilterLookup[int] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
|
||||
@@ -1699,189 +1699,6 @@ class ModuleTest(APIViewTestCases.APIViewTestCase):
|
||||
},
|
||||
]
|
||||
|
||||
def test_replicate_components(self):
|
||||
"""
|
||||
Installing a module with replicate_components=True (the default) should create
|
||||
components from the module type's templates on the parent device.
|
||||
"""
|
||||
self.add_permissions('dcim.add_module')
|
||||
manufacturer = Manufacturer.objects.get(name='Generic')
|
||||
device = create_test_device('Device for Replication Test')
|
||||
module_type = ModuleType.objects.create(manufacturer=manufacturer, model='Replication Test Module Type')
|
||||
InterfaceTemplate.objects.create(module_type=module_type, name='eth0', type='1000base-t')
|
||||
module_bay = ModuleBay.objects.create(device=device, name='Replication Bay')
|
||||
|
||||
url = reverse('dcim-api:module-list')
|
||||
data = {
|
||||
'device': device.pk,
|
||||
'module_bay': module_bay.pk,
|
||||
'module_type': module_type.pk,
|
||||
'replicate_components': True,
|
||||
}
|
||||
response = self.client.post(url, data, format='json', **self.header)
|
||||
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||
self.assertTrue(device.interfaces.filter(name='eth0').exists())
|
||||
|
||||
def test_no_replicate_components(self):
|
||||
"""
|
||||
Installing a module with replicate_components=False should NOT create components
|
||||
from the module type's templates.
|
||||
"""
|
||||
self.add_permissions('dcim.add_module')
|
||||
manufacturer = Manufacturer.objects.get(name='Generic')
|
||||
device = create_test_device('Device for No Replication Test')
|
||||
module_type = ModuleType.objects.create(manufacturer=manufacturer, model='No Replication Test Module Type')
|
||||
InterfaceTemplate.objects.create(module_type=module_type, name='eth0', type='1000base-t')
|
||||
module_bay = ModuleBay.objects.create(device=device, name='No Replication Bay')
|
||||
|
||||
url = reverse('dcim-api:module-list')
|
||||
data = {
|
||||
'device': device.pk,
|
||||
'module_bay': module_bay.pk,
|
||||
'module_type': module_type.pk,
|
||||
'replicate_components': False,
|
||||
}
|
||||
response = self.client.post(url, data, format='json', **self.header)
|
||||
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||
self.assertFalse(device.interfaces.filter(name='eth0').exists())
|
||||
|
||||
def test_adopt_components(self):
|
||||
"""
|
||||
Installing a module with adopt_components=True should assign existing unattached
|
||||
device components to the new module.
|
||||
"""
|
||||
self.add_permissions('dcim.add_module')
|
||||
manufacturer = Manufacturer.objects.get(name='Generic')
|
||||
device = create_test_device('Device for Adopt Test')
|
||||
module_type = ModuleType.objects.create(manufacturer=manufacturer, model='Adopt Test Module Type')
|
||||
InterfaceTemplate.objects.create(module_type=module_type, name='eth0', type='1000base-t')
|
||||
module_bay = ModuleBay.objects.create(device=device, name='Adopt Bay')
|
||||
existing_iface = Interface.objects.create(device=device, name='eth0', type='1000base-t')
|
||||
|
||||
url = reverse('dcim-api:module-list')
|
||||
data = {
|
||||
'device': device.pk,
|
||||
'module_bay': module_bay.pk,
|
||||
'module_type': module_type.pk,
|
||||
'adopt_components': True,
|
||||
'replicate_components': False,
|
||||
}
|
||||
response = self.client.post(url, data, format='json', **self.header)
|
||||
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||
existing_iface.refresh_from_db()
|
||||
self.assertIsNotNone(existing_iface.module)
|
||||
|
||||
def test_replicate_components_conflict(self):
|
||||
"""
|
||||
Installing a module with replicate_components=True when a component with the same name
|
||||
already exists should return a validation error.
|
||||
"""
|
||||
self.add_permissions('dcim.add_module')
|
||||
manufacturer = Manufacturer.objects.get(name='Generic')
|
||||
device = create_test_device('Device for Conflict Test')
|
||||
module_type = ModuleType.objects.create(manufacturer=manufacturer, model='Conflict Test Module Type')
|
||||
InterfaceTemplate.objects.create(module_type=module_type, name='eth0', type='1000base-t')
|
||||
module_bay = ModuleBay.objects.create(device=device, name='Conflict Bay')
|
||||
Interface.objects.create(device=device, name='eth0', type='1000base-t')
|
||||
|
||||
url = reverse('dcim-api:module-list')
|
||||
data = {
|
||||
'device': device.pk,
|
||||
'module_bay': module_bay.pk,
|
||||
'module_type': module_type.pk,
|
||||
'replicate_components': True,
|
||||
}
|
||||
response = self.client.post(url, data, format='json', **self.header)
|
||||
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def test_adopt_components_already_owned(self):
|
||||
"""
|
||||
Installing a module with adopt_components=True when an existing component already
|
||||
belongs to another module should return a validation error.
|
||||
"""
|
||||
self.add_permissions('dcim.add_module')
|
||||
manufacturer = Manufacturer.objects.get(name='Generic')
|
||||
device = create_test_device('Device for Adopt Owned Test')
|
||||
owner_module_type = ModuleType.objects.create(manufacturer=manufacturer, model='Owner Module Type')
|
||||
module_type = ModuleType.objects.create(manufacturer=manufacturer, model='Adopt Owned Test Module Type')
|
||||
InterfaceTemplate.objects.create(module_type=module_type, name='eth0', type='1000base-t')
|
||||
owner_bay = ModuleBay.objects.create(device=device, name='Owner Bay')
|
||||
target_bay = ModuleBay.objects.create(device=device, name='Adopt Owned Bay')
|
||||
|
||||
# Install a module that owns the interface
|
||||
owner_module = Module.objects.create(device=device, module_bay=owner_bay, module_type=owner_module_type)
|
||||
Interface.objects.create(device=device, name='eth0', type='1000base-t', module=owner_module)
|
||||
|
||||
url = reverse('dcim-api:module-list')
|
||||
data = {
|
||||
'device': device.pk,
|
||||
'module_bay': target_bay.pk,
|
||||
'module_type': module_type.pk,
|
||||
'adopt_components': True,
|
||||
}
|
||||
response = self.client.post(url, data, format='json', **self.header)
|
||||
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def test_patch_ignores_replicate_and_adopt(self):
|
||||
"""
|
||||
PATCH requests that include replicate_components or adopt_components should not
|
||||
trigger component replication or adoption (these fields are create-only).
|
||||
"""
|
||||
self.add_permissions('dcim.change_module')
|
||||
manufacturer = Manufacturer.objects.get(name='Generic')
|
||||
device = create_test_device('Device for PATCH Test')
|
||||
module_type = ModuleType.objects.create(manufacturer=manufacturer, model='PATCH Test Module Type')
|
||||
InterfaceTemplate.objects.create(module_type=module_type, name='eth0', type='1000base-t')
|
||||
module_bay = ModuleBay.objects.create(device=device, name='PATCH Bay')
|
||||
# Create the module without replication so we can verify PATCH doesn't trigger it
|
||||
module = Module(device=device, module_bay=module_bay, module_type=module_type)
|
||||
module._disable_replication = True
|
||||
module.save()
|
||||
|
||||
url = reverse('dcim-api:module-detail', kwargs={'pk': module.pk})
|
||||
data = {
|
||||
'replicate_components': True,
|
||||
'adopt_components': True,
|
||||
'serial': 'PATCHED',
|
||||
}
|
||||
response = self.client.patch(url, data, format='json', **self.header)
|
||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data['serial'], 'PATCHED')
|
||||
# No interfaces should have been created by the PATCH
|
||||
self.assertFalse(device.interfaces.exists())
|
||||
|
||||
def test_adopt_and_replicate_components(self):
|
||||
"""
|
||||
Installing a module with both adopt_components=True and replicate_components=True
|
||||
should adopt existing unowned components and create new components for templates
|
||||
that have no matching existing component.
|
||||
"""
|
||||
self.add_permissions('dcim.add_module')
|
||||
manufacturer = Manufacturer.objects.get(name='Generic')
|
||||
device = create_test_device('Device for Adopt+Replicate Test')
|
||||
module_type = ModuleType.objects.create(manufacturer=manufacturer, model='Adopt+Replicate Test Module Type')
|
||||
InterfaceTemplate.objects.create(module_type=module_type, name='eth0', type='1000base-t')
|
||||
InterfaceTemplate.objects.create(module_type=module_type, name='eth1', type='1000base-t')
|
||||
module_bay = ModuleBay.objects.create(device=device, name='Adopt+Replicate Bay')
|
||||
# eth0 already exists (unowned); eth1 does not
|
||||
existing_iface = Interface.objects.create(device=device, name='eth0', type='1000base-t')
|
||||
|
||||
url = reverse('dcim-api:module-list')
|
||||
data = {
|
||||
'device': device.pk,
|
||||
'module_bay': module_bay.pk,
|
||||
'module_type': module_type.pk,
|
||||
'adopt_components': True,
|
||||
'replicate_components': True,
|
||||
}
|
||||
response = self.client.post(url, data, format='json', **self.header)
|
||||
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||
# eth0 should have been adopted (now owned by the new module)
|
||||
existing_iface.refresh_from_db()
|
||||
self.assertIsNotNone(existing_iface.module)
|
||||
# eth1 should have been created
|
||||
self.assertTrue(device.interfaces.filter(name='eth1').exists())
|
||||
|
||||
|
||||
class ConsolePortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase):
|
||||
model = ConsolePort
|
||||
|
||||
@@ -16,7 +16,7 @@ from circuits.models import Circuit, CircuitTermination
|
||||
from extras.ui.panels import CustomFieldsPanel, ImageAttachmentsPanel, TagsPanel
|
||||
from extras.views import ObjectConfigContextView, ObjectRenderConfigView
|
||||
from ipam.models import ASN, VLAN, IPAddress, Prefix, VLANGroup
|
||||
from ipam.tables import InterfaceVLANTable, VLANTranslationRuleTable
|
||||
from ipam.tables import VLANTranslationRuleTable
|
||||
from netbox.object_actions import *
|
||||
from netbox.ui import actions, layout
|
||||
from netbox.ui.panels import (
|
||||
@@ -389,7 +389,7 @@ class SiteGroupView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
title=_('Child Groups'),
|
||||
filters={'parent_id': lambda ctx: ctx['object'].pk},
|
||||
actions=[
|
||||
actions.AddObject('dcim.Region', url_params={'parent': lambda ctx: ctx['object'].pk}),
|
||||
actions.AddObject('dcim.SiteGroup', url_params={'parent': lambda ctx: ctx['object'].pk}),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -3230,21 +3230,6 @@ class InterfaceView(generic.ObjectView):
|
||||
)
|
||||
lag_interfaces_table.configure(request)
|
||||
|
||||
# Get assigned VLANs and annotate whether each is tagged or untagged
|
||||
vlans = []
|
||||
if instance.untagged_vlan is not None:
|
||||
vlans.append(instance.untagged_vlan)
|
||||
vlans[0].tagged = False
|
||||
for vlan in instance.tagged_vlans.restrict(request.user).prefetch_related('site', 'group', 'tenant', 'role'):
|
||||
vlan.tagged = True
|
||||
vlans.append(vlan)
|
||||
vlan_table = InterfaceVLANTable(
|
||||
interface=instance,
|
||||
data=vlans,
|
||||
orderable=False
|
||||
)
|
||||
vlan_table.configure(request)
|
||||
|
||||
# Get VLAN translation rules
|
||||
vlan_translation_table = None
|
||||
if instance.vlan_translation_policy:
|
||||
@@ -3260,7 +3245,6 @@ class InterfaceView(generic.ObjectView):
|
||||
'bridge_interfaces_table': bridge_interfaces_table,
|
||||
'child_interfaces_table': child_interfaces_table,
|
||||
'lag_interfaces_table': lag_interfaces_table,
|
||||
'vlan_table': vlan_table,
|
||||
'vlan_translation_table': vlan_translation_table,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import warnings
|
||||
from datetime import timedelta
|
||||
from importlib import import_module
|
||||
|
||||
@@ -18,12 +17,11 @@ class Command(BaseCommand):
|
||||
help = "Perform nightly housekeeping tasks [DEPRECATED]"
|
||||
|
||||
def handle(self, *args, **options):
|
||||
warnings.warn(
|
||||
"\n\nDEPRECATION WARNING\n"
|
||||
self.stdout.write(
|
||||
"Running this command is no longer necessary: All housekeeping tasks\n"
|
||||
"are addressed automatically via NetBox's built-in job scheduler. It\n"
|
||||
"will be removed in a future release.\n",
|
||||
category=FutureWarning,
|
||||
"will be removed in a future release.",
|
||||
self.style.WARNING
|
||||
)
|
||||
|
||||
config = Config()
|
||||
|
||||
@@ -81,7 +81,7 @@ class Command(BaseCommand):
|
||||
logger.error(f'\t{field}: {error.get("message")}')
|
||||
raise CommandError()
|
||||
|
||||
# Remove extra fields from ScriptForm before passng data to script
|
||||
# Remove extra fields from ScriptForm before passing data to script
|
||||
form.cleaned_data.pop('_schedule_at')
|
||||
form.cleaned_data.pop('_interval')
|
||||
form.cleaned_data.pop('_commit')
|
||||
@@ -94,10 +94,12 @@ class Command(BaseCommand):
|
||||
data=form.cleaned_data,
|
||||
request=NetBoxFakeRequest({
|
||||
'META': {},
|
||||
'COOKIES': {},
|
||||
'POST': data,
|
||||
'GET': {},
|
||||
'FILES': {},
|
||||
'user': user,
|
||||
'method': 'POST',
|
||||
'path': '',
|
||||
'id': uuid.uuid4()
|
||||
}),
|
||||
|
||||
@@ -677,19 +677,15 @@ class ConfigContextTest(TestCase):
|
||||
if hasattr(node, 'children'):
|
||||
for child in node.children:
|
||||
try:
|
||||
# In Django 6.0+, rhs is a Query directly; older Django wraps it in Subquery
|
||||
rhs_query = getattr(child.rhs, 'query', child.rhs)
|
||||
if rhs_query.model is TaggedItem:
|
||||
subqueries.append(rhs_query)
|
||||
if child.rhs.query.model is TaggedItem:
|
||||
subqueries.append(child.rhs.query)
|
||||
except AttributeError:
|
||||
traverse(child)
|
||||
traverse(where_node)
|
||||
return subqueries
|
||||
|
||||
# In Django 6.0+, the annotation is a Query directly; older Django wraps it in Subquery
|
||||
annotation_query = getattr(config_annotation, 'query', config_annotation)
|
||||
# Find subqueries in the WHERE clause that should have DISTINCT
|
||||
tag_subqueries = find_tag_subqueries(annotation_query.where)
|
||||
tag_subqueries = find_tag_subqueries(config_annotation.query.where)
|
||||
distinct_subqueries = [sq for sq in tag_subqueries if sq.distinct]
|
||||
|
||||
# Verify we found at least one DISTINCT subquery for tags
|
||||
|
||||
@@ -94,11 +94,9 @@ class NetHost(Lookup):
|
||||
rhs, rhs_params = self.process_rhs(qn, connection)
|
||||
# Query parameters are automatically converted to IPNetwork objects, which are then turned to strings. We need
|
||||
# to omit the mask portion of the object's string representation to match PostgreSQL's HOST() function.
|
||||
# Note: params may be tuples (Django 6.0+) or lists (older Django), so convert before mutating.
|
||||
rhs_params = list(rhs_params)
|
||||
if rhs_params:
|
||||
rhs_params[0] = rhs_params[0].split('/')[0]
|
||||
params = list(lhs_params) + rhs_params
|
||||
params = lhs_params + rhs_params
|
||||
return f'HOST({lhs}) = {rhs}', params
|
||||
|
||||
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
import django_tables2 as tables
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_tables2.utils import Accessor
|
||||
|
||||
from dcim.models import Interface
|
||||
from dcim.tables.template_code import INTERFACE_LINKTERMINATION, LINKTERMINATION
|
||||
from ipam.models import *
|
||||
from netbox.tables import NetBoxTable, OrganizationalModelTable, PrimaryModelTable, columns
|
||||
from tenancy.tables import TenancyColumnsMixin, TenantColumn
|
||||
from tenancy.tables import TenancyColumnsMixin
|
||||
from virtualization.models import VMInterface
|
||||
|
||||
from .template_code import *
|
||||
|
||||
__all__ = (
|
||||
'InterfaceVLANTable',
|
||||
'VLANDevicesTable',
|
||||
'VLANGroupTable',
|
||||
'VLANMembersTable',
|
||||
@@ -198,47 +196,6 @@ class VLANVirtualMachinesTable(VLANMembersTable):
|
||||
exclude = ('id', )
|
||||
|
||||
|
||||
class InterfaceVLANTable(NetBoxTable):
|
||||
"""
|
||||
List VLANs assigned to a specific Interface.
|
||||
"""
|
||||
vid = tables.Column(
|
||||
linkify=True,
|
||||
verbose_name=_('VID')
|
||||
)
|
||||
tagged = columns.BooleanColumn(
|
||||
verbose_name=_('Tagged'),
|
||||
false_mark=None
|
||||
)
|
||||
site = tables.Column(
|
||||
verbose_name=_('Site'),
|
||||
linkify=True
|
||||
)
|
||||
group = tables.Column(
|
||||
accessor=Accessor('group__name'),
|
||||
verbose_name=_('Group')
|
||||
)
|
||||
tenant = TenantColumn(
|
||||
verbose_name=_('Tenant'),
|
||||
)
|
||||
status = columns.ChoiceFieldColumn(
|
||||
verbose_name=_('Status'),
|
||||
)
|
||||
role = tables.Column(
|
||||
verbose_name=_('Role'),
|
||||
linkify=True
|
||||
)
|
||||
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = VLAN
|
||||
fields = ('vid', 'tagged', 'site', 'group', 'name', 'tenant', 'status', 'role', 'description')
|
||||
exclude = ('id', )
|
||||
|
||||
def __init__(self, interface, *args, **kwargs):
|
||||
self.interface = interface
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
#
|
||||
# VLAN Translation
|
||||
#
|
||||
|
||||
@@ -13,7 +13,7 @@ from rest_framework.viewsets import GenericViewSet
|
||||
from netbox.api.serializers.features import ChangeLogMessageSerializer
|
||||
from netbox.constants import ADVISORY_LOCK_KEYS
|
||||
from utilities.api import get_annotations_for_serializer, get_prefetches_for_serializer
|
||||
from utilities.exceptions import AbortRequest, PreconditionFailed
|
||||
from utilities.exceptions import AbortRequest
|
||||
from utilities.query import reapply_model_ordering
|
||||
|
||||
from . import mixins
|
||||
@@ -34,50 +34,6 @@ HTTP_ACTIONS = {
|
||||
}
|
||||
|
||||
|
||||
class ETagMixin:
|
||||
"""
|
||||
Adds ETag header support to ViewSets. Generates weak ETags (W/ prefix per
|
||||
RFC 7232 §2.1) from `last_updated` (or `created` if unavailable). Weak ETags
|
||||
are appropriate here because the tag is derived from a modification timestamp
|
||||
rather than a hash of the serialized payload.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def _get_etag(obj):
|
||||
"""Return a weak ETag string for the given object, or None."""
|
||||
if ts := getattr(obj, 'last_updated', None) or getattr(obj, 'created', None):
|
||||
return f'W/"{ts.isoformat()}"'
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _get_if_match(request):
|
||||
"""Return the list of If-Match header values (if specified)."""
|
||||
if (if_match := request.META.get('HTTP_IF_MATCH')) and if_match != '*':
|
||||
return [e.strip() for e in if_match.split(',')]
|
||||
return []
|
||||
|
||||
def _validate_etag(self, request, instance):
|
||||
"""Validate the request's ETag"""
|
||||
if provided := self._get_if_match(request):
|
||||
current_etag = self._get_etag(instance)
|
||||
if current_etag and current_etag not in provided:
|
||||
raise PreconditionFailed(etag=current_etag)
|
||||
|
||||
def handle_exception(self, exc):
|
||||
response = super().handle_exception(exc)
|
||||
if isinstance(exc, PreconditionFailed) and exc.etag:
|
||||
response['ETag'] = exc.etag
|
||||
return response
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
instance = self.get_object()
|
||||
serializer = self.get_serializer(instance)
|
||||
response = Response(serializer.data)
|
||||
if etag := self._get_etag(instance):
|
||||
response['ETag'] = etag
|
||||
return response
|
||||
|
||||
|
||||
class BaseViewSet(GenericViewSet):
|
||||
"""
|
||||
Base class for all API ViewSets. This is responsible for the enforcement of object-based permissions.
|
||||
@@ -139,7 +95,6 @@ class BaseViewSet(GenericViewSet):
|
||||
|
||||
|
||||
class NetBoxReadOnlyModelViewSet(
|
||||
ETagMixin,
|
||||
mixins.CustomFieldsMixin,
|
||||
mixins.ExportTemplatesMixin,
|
||||
drf_mixins.RetrieveModelMixin,
|
||||
@@ -150,7 +105,6 @@ class NetBoxReadOnlyModelViewSet(
|
||||
|
||||
|
||||
class NetBoxModelViewSet(
|
||||
ETagMixin,
|
||||
mixins.BulkUpdateModelMixin,
|
||||
mixins.BulkDestroyModelMixin,
|
||||
mixins.ObjectValidationMixin,
|
||||
@@ -237,14 +191,7 @@ class NetBoxModelViewSet(
|
||||
serializer = self.get_serializer(qs, many=bulk_create)
|
||||
|
||||
headers = self.get_success_headers(serializer.data)
|
||||
response = Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
|
||||
|
||||
# Add ETag for single-object creation only (bulk returns a list, no single ETag)
|
||||
if not bulk_create:
|
||||
if etag := self._get_etag(qs):
|
||||
response['ETag'] = etag
|
||||
|
||||
return response
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
|
||||
|
||||
def perform_create(self, serializer):
|
||||
model = self.queryset.model
|
||||
@@ -264,10 +211,6 @@ class NetBoxModelViewSet(
|
||||
def update(self, request, *args, **kwargs):
|
||||
partial = kwargs.pop('partial', False)
|
||||
instance = self.get_object_with_snapshot()
|
||||
|
||||
# Enforce If-Match precondition (RFC 9110 §13.1.1)
|
||||
self._validate_etag(self.request, instance)
|
||||
|
||||
serializer = self.get_serializer(instance, data=request.data, partial=partial)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
self.perform_update(serializer)
|
||||
@@ -278,12 +221,8 @@ class NetBoxModelViewSet(
|
||||
|
||||
# Re-serialize the instance(s) with prefetched data
|
||||
serializer = self.get_serializer(qs)
|
||||
response = Response(serializer.data)
|
||||
|
||||
if etag := self._get_etag(qs):
|
||||
response['ETag'] = etag
|
||||
|
||||
return response
|
||||
return Response(serializer.data)
|
||||
|
||||
def perform_update(self, serializer):
|
||||
model = self.queryset.model
|
||||
@@ -293,11 +232,6 @@ class NetBoxModelViewSet(
|
||||
# Enforce object-level permissions on save()
|
||||
try:
|
||||
with transaction.atomic(using=router.db_for_write(model)):
|
||||
# Re-check the If-Match ETag under a row-level lock to close the TOCTOU window
|
||||
# between the initial check in update() and the actual write.
|
||||
if self._get_if_match(self.request):
|
||||
locked = model.objects.select_for_update().get(pk=serializer.instance.pk)
|
||||
self._validate_etag(self.request, locked)
|
||||
instance = serializer.save()
|
||||
self._validate_objects(instance)
|
||||
except ObjectDoesNotExist:
|
||||
@@ -308,9 +242,6 @@ class NetBoxModelViewSet(
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
instance = self.get_object_with_snapshot()
|
||||
|
||||
# Enforce If-Match precondition (RFC 9110 §13.1.1)
|
||||
self._validate_etag(request, instance)
|
||||
|
||||
# Attach changelog message (if any)
|
||||
serializer = ChangeLogMessageSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
@@ -325,16 +256,7 @@ class NetBoxModelViewSet(
|
||||
logger = logging.getLogger(f'netbox.api.views.{self.__class__.__name__}')
|
||||
logger.info(f"Deleting {model._meta.verbose_name} {instance} (PK: {instance.pk})")
|
||||
|
||||
try:
|
||||
with transaction.atomic(using=router.db_for_write(model)):
|
||||
# Re-check the If-Match ETag under a row-level lock to close the TOCTOU window
|
||||
# between the initial check in destroy() and the actual delete.
|
||||
if self._get_if_match(self.request):
|
||||
locked = model.objects.select_for_update().get(pk=instance.pk)
|
||||
self._validate_etag(self.request, locked)
|
||||
super().perform_destroy(instance)
|
||||
except ObjectDoesNotExist:
|
||||
raise PermissionDenied()
|
||||
return super().perform_destroy(instance)
|
||||
|
||||
|
||||
class MPTTLockedMixin:
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import logging
|
||||
import os
|
||||
import traceback
|
||||
from abc import ABC, abstractmethod
|
||||
from datetime import timedelta
|
||||
from pathlib import Path
|
||||
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.utils import timezone
|
||||
@@ -24,11 +21,6 @@ __all__ = (
|
||||
'system_job',
|
||||
)
|
||||
|
||||
# The installation root, e.g. "/opt/netbox/". Used to strip absolute path
|
||||
# prefixes from traceback file paths before recording them in the job log.
|
||||
# jobs.py lives at <root>/netbox/netbox/jobs.py, so parents[2] is the root.
|
||||
_INSTALL_ROOT = str(Path(__file__).resolve().parents[2]) + os.sep
|
||||
|
||||
|
||||
def system_job(interval):
|
||||
"""
|
||||
@@ -115,13 +107,6 @@ class JobRunner(ABC):
|
||||
job.terminate(status=JobStatusChoices.STATUS_FAILED)
|
||||
|
||||
except Exception as e:
|
||||
tb_str = traceback.format_exc().replace(_INSTALL_ROOT, '')
|
||||
tb_record = logging.makeLogRecord({
|
||||
'levelno': logging.ERROR,
|
||||
'levelname': 'ERROR',
|
||||
'msg': tb_str,
|
||||
})
|
||||
job.log(tb_record)
|
||||
job.terminate(status=JobStatusChoices.STATUS_ERRORED, error=repr(e))
|
||||
if type(e) is JobTimeoutException:
|
||||
logger.error(e)
|
||||
|
||||
@@ -40,15 +40,24 @@ class CoreMiddleware:
|
||||
with apply_request_processors(request):
|
||||
response = self.get_response(request)
|
||||
|
||||
# Check if language cookie should be renewed
|
||||
if request.user.is_authenticated and settings.SESSION_SAVE_EVERY_REQUEST:
|
||||
if language := request.user.config.get('locale.language'):
|
||||
response.set_cookie(
|
||||
key=settings.LANGUAGE_COOKIE_NAME,
|
||||
value=language,
|
||||
max_age=request.session.get_expiry_age(),
|
||||
secure=settings.SESSION_COOKIE_SECURE,
|
||||
)
|
||||
# Set or renew the language cookie based on the user's preference. This handles two cases:
|
||||
# 1. The user just logged in (via any auth backend): the user_logged_in signal stores the preferred language on
|
||||
# the request so we set the cookie here on the login response.
|
||||
# 2. SESSION_SAVE_EVERY_REQUEST is enabled: renew the language cookie on every request to keep it in sync with
|
||||
# the session expiry.
|
||||
if hasattr(request, '_language_cookie'):
|
||||
language = request._language_cookie
|
||||
elif request.user.is_authenticated and settings.SESSION_SAVE_EVERY_REQUEST:
|
||||
language = request.user.config.get('locale.language')
|
||||
else:
|
||||
language = None
|
||||
if language:
|
||||
response.set_cookie(
|
||||
key=settings.LANGUAGE_COOKIE_NAME,
|
||||
value=language,
|
||||
max_age=request.session.get_expiry_age(),
|
||||
secure=settings.SESSION_COOKIE_SECURE,
|
||||
)
|
||||
|
||||
# Attach the unique request ID as an HTTP header.
|
||||
response['X-Request-ID'] = request.id
|
||||
|
||||
@@ -435,7 +435,6 @@ INSTALLED_APPS = [
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'django.contrib.humanize',
|
||||
'django.contrib.postgres',
|
||||
'django.forms',
|
||||
'corsheaders',
|
||||
'debug_toolbar',
|
||||
|
||||
@@ -10,7 +10,6 @@ from core.models import DataSource, Job
|
||||
from utilities.testing import disable_warnings
|
||||
|
||||
from ..jobs import *
|
||||
from ..jobs import _INSTALL_ROOT
|
||||
|
||||
|
||||
class TestJobRunner(JobRunner):
|
||||
@@ -84,12 +83,6 @@ class JobRunnerTest(JobRunnerTestCase):
|
||||
|
||||
self.assertEqual(job.status, JobStatusChoices.STATUS_ERRORED)
|
||||
self.assertEqual(job.error, repr(ErroredJobRunner.EXP))
|
||||
self.assertEqual(len(job.log_entries), 1)
|
||||
self.assertEqual(job.log_entries[0]['level'], 'error')
|
||||
tb_message = job.log_entries[0]['message']
|
||||
self.assertIn('Traceback', tb_message)
|
||||
self.assertIn('Test error', tb_message)
|
||||
self.assertNotIn(_INSTALL_ROOT, tb_message)
|
||||
|
||||
|
||||
class EnqueueTest(JobRunnerTestCase):
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card table-responsive">
|
||||
{% render_table table %}
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
||||
@@ -86,6 +86,11 @@
|
||||
<th scope="row">{% trans "Q-in-Q SVLAN" %}</th>
|
||||
<td>{{ object.qinq_svlan|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
{% elif object.mode %}
|
||||
<tr>
|
||||
<th scope="row">{% trans "Untagged VLAN" %}</th>
|
||||
<td>{{ object.untagged_vlan|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<th scope="row">{% trans "Transmit power (dBm)" %}</th>
|
||||
@@ -411,7 +416,10 @@
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-12">
|
||||
{% include 'inc/panel_table.html' with table=vlan_table heading="VLANs" %}
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "VLANs" %}</h2>
|
||||
{% htmx_table 'ipam:vlan_list' interface_id=object.pk %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if object.is_lag %}
|
||||
|
||||
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-03-04 05:17+0000\n"
|
||||
"POT-Creation-Date: 2026-03-06 05:17+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"
|
||||
@@ -473,8 +473,7 @@ msgstr ""
|
||||
#: netbox/extras/forms/bulk_edit.py:306 netbox/extras/tables/tables.py:552
|
||||
#: netbox/netbox/ui/attrs.py:193 netbox/templates/circuits/circuittype.html:30
|
||||
#: netbox/templates/circuits/virtualcircuittype.html:30
|
||||
#: netbox/templates/dcim/cable.html:44 netbox/templates/dcim/devicerole.html:38
|
||||
#: netbox/templates/dcim/frontport.html:40
|
||||
#: netbox/templates/dcim/cable.html:44 netbox/templates/dcim/frontport.html:40
|
||||
#: netbox/templates/dcim/inventoryitemrole.html:26
|
||||
#: netbox/templates/dcim/poweroutlet.html:48
|
||||
#: netbox/templates/dcim/rearport.html:40 netbox/templates/extras/tag.html:26
|
||||
@@ -601,7 +600,7 @@ msgstr ""
|
||||
#: netbox/templates/core/rq_task.html:81 netbox/templates/core/system.html:19
|
||||
#: netbox/templates/dcim/cable.html:19
|
||||
#: netbox/templates/dcim/inventoryitem.html:36
|
||||
#: netbox/templates/dcim/module.html:69 netbox/templates/dcim/powerfeed.html:36
|
||||
#: netbox/templates/dcim/powerfeed.html:36
|
||||
#: netbox/templates/dcim/poweroutlet.html:40
|
||||
#: netbox/templates/extras/inc/script_list_content.html:35
|
||||
#: netbox/templates/ipam/ipaddress.html:37
|
||||
@@ -771,17 +770,17 @@ msgstr ""
|
||||
#: netbox/dcim/forms/filtersets.py:1864 netbox/dcim/forms/filtersets.py:1879
|
||||
#: netbox/dcim/forms/filtersets.py:1890 netbox/dcim/forms/filtersets.py:1936
|
||||
#: netbox/dcim/forms/filtersets.py:1972 netbox/dcim/tables/modules.py:25
|
||||
#: netbox/extras/forms/bulk_edit.py:94 netbox/extras/forms/filtersets.py:48
|
||||
#: netbox/extras/forms/filtersets.py:147 netbox/extras/forms/filtersets.py:226
|
||||
#: netbox/extras/forms/filtersets.py:243 netbox/extras/forms/filtersets.py:275
|
||||
#: netbox/extras/forms/filtersets.py:306 netbox/extras/forms/filtersets.py:329
|
||||
#: netbox/extras/forms/filtersets.py:361 netbox/extras/forms/filtersets.py:560
|
||||
#: netbox/ipam/forms/filtersets.py:108 netbox/ipam/forms/filtersets.py:296
|
||||
#: netbox/ipam/forms/filtersets.py:346 netbox/ipam/forms/filtersets.py:423
|
||||
#: netbox/ipam/forms/filtersets.py:511 netbox/ipam/forms/filtersets.py:525
|
||||
#: netbox/ipam/forms/filtersets.py:550 netbox/ipam/forms/filtersets.py:622
|
||||
#: netbox/ipam/forms/filtersets.py:641 netbox/netbox/tables/tables.py:355
|
||||
#: netbox/templates/dcim/moduletype.html:68
|
||||
#: netbox/dcim/views.py:1679 netbox/extras/forms/bulk_edit.py:94
|
||||
#: netbox/extras/forms/filtersets.py:48 netbox/extras/forms/filtersets.py:147
|
||||
#: netbox/extras/forms/filtersets.py:226 netbox/extras/forms/filtersets.py:243
|
||||
#: netbox/extras/forms/filtersets.py:275 netbox/extras/forms/filtersets.py:306
|
||||
#: netbox/extras/forms/filtersets.py:329 netbox/extras/forms/filtersets.py:361
|
||||
#: netbox/extras/forms/filtersets.py:560 netbox/ipam/forms/filtersets.py:108
|
||||
#: netbox/ipam/forms/filtersets.py:296 netbox/ipam/forms/filtersets.py:346
|
||||
#: netbox/ipam/forms/filtersets.py:423 netbox/ipam/forms/filtersets.py:511
|
||||
#: netbox/ipam/forms/filtersets.py:525 netbox/ipam/forms/filtersets.py:550
|
||||
#: netbox/ipam/forms/filtersets.py:622 netbox/ipam/forms/filtersets.py:641
|
||||
#: netbox/netbox/tables/tables.py:355
|
||||
#: netbox/virtualization/forms/filtersets.py:52
|
||||
#: netbox/virtualization/forms/filtersets.py:116
|
||||
#: netbox/virtualization/forms/filtersets.py:217
|
||||
@@ -834,7 +833,7 @@ msgstr ""
|
||||
#: netbox/extras/tables/tables.py:97 netbox/ipam/tables/vlans.py:257
|
||||
#: netbox/ipam/tables/vlans.py:284 netbox/netbox/forms/bulk_edit.py:79
|
||||
#: netbox/netbox/forms/bulk_edit.py:91 netbox/netbox/forms/bulk_edit.py:103
|
||||
#: netbox/netbox/ui/panels.py:196 netbox/netbox/ui/panels.py:205
|
||||
#: netbox/netbox/ui/panels.py:199 netbox/netbox/ui/panels.py:208
|
||||
#: netbox/templates/circuits/circuit.html:69
|
||||
#: netbox/templates/circuits/circuitgroup.html:32
|
||||
#: netbox/templates/circuits/circuittype.html:26
|
||||
@@ -850,15 +849,12 @@ msgstr ""
|
||||
#: netbox/templates/dcim/consoleport.html:44
|
||||
#: netbox/templates/dcim/consoleserverport.html:44
|
||||
#: netbox/templates/dcim/devicebay.html:32
|
||||
#: netbox/templates/dcim/devicerole.html:30
|
||||
#: netbox/templates/dcim/frontport.html:54
|
||||
#: netbox/templates/dcim/interface.html:69
|
||||
#: netbox/templates/dcim/inventoryitem.html:64
|
||||
#: netbox/templates/dcim/inventoryitemrole.html:22
|
||||
#: netbox/templates/dcim/macaddress.html:21
|
||||
#: netbox/templates/dcim/module.html:73 netbox/templates/dcim/modulebay.html:42
|
||||
#: netbox/templates/dcim/moduletype.html:43
|
||||
#: netbox/templates/dcim/platform.html:33
|
||||
#: netbox/templates/dcim/modulebay.html:42
|
||||
#: netbox/templates/dcim/powerfeed.html:40
|
||||
#: netbox/templates/dcim/poweroutlet.html:44
|
||||
#: netbox/templates/dcim/powerpanel.html:30
|
||||
@@ -1703,7 +1699,7 @@ msgstr ""
|
||||
#: netbox/ipam/tables/vlans.py:35 netbox/ipam/tables/vlans.py:88
|
||||
#: netbox/ipam/tables/vlans.py:248 netbox/ipam/tables/vrfs.py:26
|
||||
#: netbox/ipam/tables/vrfs.py:65 netbox/netbox/tables/tables.py:325
|
||||
#: netbox/netbox/ui/panels.py:195 netbox/netbox/ui/panels.py:204
|
||||
#: netbox/netbox/ui/panels.py:198 netbox/netbox/ui/panels.py:207
|
||||
#: netbox/templates/circuits/circuitgroup.html:28
|
||||
#: netbox/templates/circuits/circuittype.html:22
|
||||
#: netbox/templates/circuits/provideraccount.html:28
|
||||
@@ -1714,7 +1710,6 @@ msgstr ""
|
||||
#: netbox/templates/dcim/consoleport.html:28
|
||||
#: netbox/templates/dcim/consoleserverport.html:28
|
||||
#: netbox/templates/dcim/devicebay.html:24
|
||||
#: netbox/templates/dcim/devicerole.html:26
|
||||
#: netbox/templates/dcim/frontport.html:28
|
||||
#: netbox/templates/dcim/inc/interface_vlans_table.html:5
|
||||
#: netbox/templates/dcim/inc/panels/inventory_items.html:18
|
||||
@@ -1723,7 +1718,6 @@ msgstr ""
|
||||
#: netbox/templates/dcim/inventoryitem.html:28
|
||||
#: netbox/templates/dcim/inventoryitemrole.html:18
|
||||
#: netbox/templates/dcim/modulebay.html:30
|
||||
#: netbox/templates/dcim/platform.html:29
|
||||
#: netbox/templates/dcim/poweroutlet.html:28
|
||||
#: netbox/templates/dcim/powerport.html:28
|
||||
#: netbox/templates/dcim/rearport.html:28
|
||||
@@ -1917,7 +1911,7 @@ msgstr ""
|
||||
#: netbox/templates/dcim/interface.html:30
|
||||
#: netbox/templates/dcim/interface.html:231
|
||||
#: netbox/templates/dcim/inventoryitem.html:20
|
||||
#: netbox/templates/dcim/module.html:57 netbox/templates/dcim/modulebay.html:20
|
||||
#: netbox/templates/dcim/modulebay.html:20
|
||||
#: netbox/templates/dcim/panels/virtual_chassis_members.html:8
|
||||
#: netbox/templates/dcim/poweroutlet.html:20
|
||||
#: netbox/templates/dcim/powerport.html:20
|
||||
@@ -3144,10 +3138,9 @@ msgstr ""
|
||||
#: netbox/dcim/tables/devices.py:1205 netbox/ipam/forms/bulk_import.py:582
|
||||
#: netbox/ipam/forms/model_forms.py:758 netbox/ipam/tables/fhrp.py:56
|
||||
#: netbox/ipam/tables/ip.py:329 netbox/ipam/tables/services.py:42
|
||||
#: netbox/netbox/tables/tables.py:329 netbox/netbox/ui/panels.py:203
|
||||
#: netbox/templates/dcim/devicerole.html:34
|
||||
#: netbox/netbox/tables/tables.py:329 netbox/netbox/ui/panels.py:206
|
||||
#: netbox/templates/dcim/interface.html:108
|
||||
#: netbox/templates/dcim/platform.html:37 netbox/templates/ipam/service.html:30
|
||||
#: netbox/templates/ipam/service.html:30
|
||||
#: netbox/templates/tenancy/contactgroup.html:29
|
||||
#: netbox/templates/tenancy/tenantgroup.html:37
|
||||
#: netbox/templates/wireless/wirelesslangroup.html:37
|
||||
@@ -4301,9 +4294,8 @@ msgstr ""
|
||||
#: netbox/dcim/tables/modules.py:47 netbox/dcim/tables/modules.py:90
|
||||
#: netbox/dcim/tables/racks.py:51 netbox/dcim/tables/racks.py:121
|
||||
#: netbox/templates/dcim/inventoryitem.html:48
|
||||
#: netbox/templates/dcim/module.html:95 netbox/templates/dcim/modulebay.html:62
|
||||
#: netbox/templates/dcim/moduletype.html:31
|
||||
#: netbox/templates/dcim/platform.html:41
|
||||
#: netbox/templates/dcim/modulebay.html:62
|
||||
#: netbox/templates/dcim/panels/module_type.html:7
|
||||
msgid "Manufacturer"
|
||||
msgstr ""
|
||||
|
||||
@@ -4364,13 +4356,13 @@ msgstr ""
|
||||
#: netbox/dcim/forms/model_forms.py:237 netbox/dcim/forms/model_forms.py:318
|
||||
#: netbox/dcim/tables/devicetypes.py:110 netbox/dcim/tables/modules.py:55
|
||||
#: netbox/dcim/tables/racks.py:71 netbox/dcim/tables/racks.py:162
|
||||
#: netbox/dcim/views.py:890 netbox/dcim/views.py:1018
|
||||
#: netbox/dcim/views.py:891 netbox/dcim/views.py:1019
|
||||
#: netbox/extras/forms/bulk_edit.py:57 netbox/extras/forms/bulk_edit.py:137
|
||||
#: netbox/extras/forms/bulk_edit.py:191 netbox/extras/forms/bulk_edit.py:219
|
||||
#: netbox/extras/forms/bulk_edit.py:315 netbox/extras/forms/bulk_edit.py:341
|
||||
#: netbox/extras/forms/filtersets.py:74 netbox/extras/forms/filtersets.py:170
|
||||
#: netbox/extras/forms/filtersets.py:266 netbox/extras/forms/filtersets.py:297
|
||||
#: netbox/ipam/forms/bulk_edit.py:162 netbox/templates/dcim/moduletype.html:51
|
||||
#: netbox/ipam/forms/bulk_edit.py:162
|
||||
#: netbox/templates/extras/configcontext.html:17
|
||||
#: netbox/templates/extras/customlink.html:25
|
||||
#: netbox/templates/extras/savedfilter.html:33
|
||||
@@ -4405,7 +4397,7 @@ msgstr ""
|
||||
|
||||
#: netbox/dcim/forms/bulk_edit.py:295 netbox/dcim/forms/model_forms.py:238
|
||||
#: netbox/dcim/forms/model_forms.py:319 netbox/dcim/ui/panels.py:135
|
||||
#: netbox/dcim/views.py:884 netbox/dcim/views.py:1016
|
||||
#: netbox/dcim/views.py:885 netbox/dcim/views.py:1017
|
||||
#: netbox/extras/tables/tables.py:278
|
||||
#: netbox/templates/dcim/inc/panels/racktype_dimensions.html:3
|
||||
#: netbox/templates/extras/imageattachment.html:40
|
||||
@@ -4414,7 +4406,7 @@ msgstr ""
|
||||
|
||||
#: netbox/dcim/forms/bulk_edit.py:297 netbox/dcim/forms/filtersets.py:336
|
||||
#: netbox/dcim/forms/filtersets.py:361 netbox/dcim/forms/model_forms.py:240
|
||||
#: netbox/dcim/views.py:889 netbox/dcim/views.py:1017
|
||||
#: netbox/dcim/views.py:890 netbox/dcim/views.py:1018
|
||||
#: netbox/templates/dcim/inc/panels/racktype_numbering.html:3
|
||||
msgid "Numbering"
|
||||
msgstr ""
|
||||
@@ -4425,8 +4417,7 @@ msgid "Rack type"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/forms/bulk_edit.py:363 netbox/dcim/forms/bulk_edit.py:708
|
||||
#: netbox/dcim/forms/bulk_edit.py:763 netbox/templates/dcim/module.html:77
|
||||
#: netbox/templates/dcim/modulebay.html:70
|
||||
#: netbox/dcim/forms/bulk_edit.py:763 netbox/templates/dcim/modulebay.html:70
|
||||
msgid "Serial Number"
|
||||
msgstr ""
|
||||
|
||||
@@ -4441,7 +4432,7 @@ msgstr ""
|
||||
#: netbox/dcim/forms/bulk_import.py:306 netbox/dcim/forms/bulk_import.py:471
|
||||
#: netbox/dcim/forms/bulk_import.py:680 netbox/dcim/forms/filtersets.py:420
|
||||
#: netbox/dcim/forms/filtersets.py:568 netbox/dcim/forms/filtersets.py:750
|
||||
#: netbox/dcim/forms/filtersets.py:906 netbox/templates/dcim/moduletype.html:47
|
||||
#: netbox/dcim/forms/filtersets.py:906
|
||||
msgid "Airflow"
|
||||
msgstr ""
|
||||
|
||||
@@ -4499,12 +4490,12 @@ msgstr ""
|
||||
#: netbox/dcim/forms/model_forms.py:1140 netbox/dcim/forms/model_forms.py:1180
|
||||
#: netbox/dcim/forms/model_forms.py:1198 netbox/dcim/forms/object_create.py:119
|
||||
#: netbox/dcim/tables/devicetypes.py:84 netbox/dcim/ui/panels.py:125
|
||||
#: netbox/templates/dcim/devicebay.html:52 netbox/templates/dcim/module.html:61
|
||||
#: netbox/templates/dcim/devicebay.html:52
|
||||
msgid "Device Type"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/forms/bulk_edit.py:543 netbox/dcim/forms/model_forms.py:412
|
||||
#: netbox/dcim/views.py:1589 netbox/extras/forms/model_forms.py:601
|
||||
#: netbox/dcim/views.py:1590 netbox/extras/forms/model_forms.py:601
|
||||
msgid "Schema"
|
||||
msgstr ""
|
||||
|
||||
@@ -4515,7 +4506,7 @@ msgstr ""
|
||||
#: netbox/dcim/forms/model_forms.py:431 netbox/dcim/tables/modules.py:43
|
||||
#: netbox/extras/forms/filtersets.py:413 netbox/extras/forms/model_forms.py:626
|
||||
#: netbox/extras/tables/tables.py:627 netbox/templates/account/base.html:7
|
||||
#: netbox/templates/dcim/cable.html:23 netbox/templates/dcim/moduletype.html:27
|
||||
#: netbox/templates/dcim/cable.html:23
|
||||
#: netbox/templates/extras/configcontext.html:21
|
||||
#: netbox/templates/inc/user_menu.html:38 netbox/vpn/forms/bulk_edit.py:213
|
||||
#: netbox/vpn/forms/filtersets.py:203 netbox/vpn/forms/model_forms.py:378
|
||||
@@ -4528,9 +4519,8 @@ msgstr ""
|
||||
#: netbox/dcim/forms/model_forms.py:1120 netbox/dcim/forms/model_forms.py:1141
|
||||
#: netbox/dcim/forms/model_forms.py:1181 netbox/dcim/forms/model_forms.py:1199
|
||||
#: netbox/dcim/forms/object_create.py:120 netbox/dcim/tables/modules.py:52
|
||||
#: netbox/dcim/tables/modules.py:95 netbox/templates/dcim/module.html:92
|
||||
#: netbox/dcim/tables/modules.py:95 netbox/dcim/views.py:2848
|
||||
#: netbox/templates/dcim/modulebay.html:66
|
||||
#: netbox/templates/dcim/moduletype.html:24
|
||||
msgid "Module Type"
|
||||
msgstr ""
|
||||
|
||||
@@ -4539,7 +4529,7 @@ msgid "Chassis"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/forms/bulk_edit.py:611 netbox/dcim/models/devices.py:390
|
||||
#: netbox/dcim/tables/devices.py:76
|
||||
#: netbox/dcim/tables/devices.py:76 netbox/dcim/ui/panels.py:142
|
||||
msgid "VM role"
|
||||
msgstr ""
|
||||
|
||||
@@ -4575,7 +4565,6 @@ msgstr ""
|
||||
#: netbox/dcim/forms/filtersets.py:789 netbox/dcim/forms/filtersets.py:898
|
||||
#: netbox/dcim/forms/model_forms.py:557 netbox/dcim/forms/model_forms.py:629
|
||||
#: netbox/dcim/tables/devices.py:191 netbox/extras/filtersets.py:745
|
||||
#: netbox/templates/dcim/platform.html:26
|
||||
#: netbox/virtualization/forms/bulk_edit.py:131
|
||||
#: netbox/virtualization/forms/bulk_import.py:135
|
||||
#: netbox/virtualization/forms/filtersets.py:187
|
||||
@@ -4747,7 +4736,7 @@ msgstr ""
|
||||
#: netbox/templates/dcim/consoleport.html:24
|
||||
#: netbox/templates/dcim/consoleserverport.html:24
|
||||
#: netbox/templates/dcim/frontport.html:24
|
||||
#: netbox/templates/dcim/interface.html:34 netbox/templates/dcim/module.html:54
|
||||
#: netbox/templates/dcim/interface.html:34
|
||||
#: netbox/templates/dcim/modulebay.html:26
|
||||
#: netbox/templates/dcim/modulebay.html:58
|
||||
#: netbox/templates/dcim/poweroutlet.html:24
|
||||
@@ -5540,7 +5529,7 @@ msgid "Function"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/forms/filtersets.py:461 netbox/dcim/forms/model_forms.py:341
|
||||
#: netbox/dcim/tables/racks.py:189 netbox/dcim/views.py:1163
|
||||
#: netbox/dcim/tables/racks.py:189 netbox/dcim/views.py:1164
|
||||
msgid "Reservation"
|
||||
msgstr ""
|
||||
|
||||
@@ -5568,12 +5557,11 @@ msgid "Module count"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/forms/filtersets.py:769 netbox/dcim/forms/model_forms.py:522
|
||||
#: netbox/templates/dcim/devicerole.html:23
|
||||
msgid "Device Role"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/forms/filtersets.py:892 netbox/dcim/tables/racks.py:47
|
||||
#: netbox/templates/dcim/module.html:99
|
||||
#: netbox/templates/dcim/panels/module_type.html:11
|
||||
msgid "Model"
|
||||
msgstr ""
|
||||
|
||||
@@ -7629,8 +7617,6 @@ msgstr ""
|
||||
|
||||
#: netbox/dcim/tables/devices.py:105 netbox/dcim/tables/devices.py:225
|
||||
#: netbox/extras/forms/model_forms.py:754
|
||||
#: netbox/templates/dcim/devicerole.html:48
|
||||
#: netbox/templates/dcim/platform.html:45
|
||||
#: netbox/templates/extras/configtemplate.html:10
|
||||
#: netbox/templates/extras/object_render_config.html:12
|
||||
#: netbox/templates/extras/object_render_config.html:15
|
||||
@@ -7695,8 +7681,8 @@ msgid "Power outlets"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/tables/devices.py:254 netbox/dcim/tables/devices.py:1174
|
||||
#: netbox/dcim/tables/devicetypes.py:132 netbox/dcim/views.py:1423
|
||||
#: netbox/dcim/views.py:1760 netbox/dcim/views.py:2590
|
||||
#: netbox/dcim/tables/devicetypes.py:132 netbox/dcim/views.py:1424
|
||||
#: netbox/dcim/views.py:1777 netbox/dcim/views.py:2649
|
||||
#: netbox/netbox/navigation/menu.py:98 netbox/netbox/navigation/menu.py:262
|
||||
#: netbox/templates/dcim/buttons/bulk_add_components.html:38
|
||||
#: netbox/templates/dcim/device/base.html:37
|
||||
@@ -7737,13 +7723,13 @@ msgid "Device Site"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/tables/devices.py:322 netbox/dcim/tables/modules.py:86
|
||||
#: netbox/templates/dcim/module.html:65 netbox/templates/dcim/modulebay.html:17
|
||||
#: netbox/templates/dcim/modulebay.html:17
|
||||
msgid "Module Bay"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/tables/devices.py:335 netbox/dcim/tables/devicetypes.py:53
|
||||
#: netbox/dcim/tables/devicetypes.py:147 netbox/dcim/views.py:1498
|
||||
#: netbox/dcim/views.py:2676 netbox/netbox/navigation/menu.py:107
|
||||
#: netbox/dcim/tables/devicetypes.py:147 netbox/dcim/views.py:1499
|
||||
#: netbox/dcim/views.py:2735 netbox/netbox/navigation/menu.py:107
|
||||
#: netbox/templates/dcim/buttons/bulk_add_components.html:66
|
||||
#: netbox/templates/dcim/device/base.html:52
|
||||
#: netbox/templates/dcim/devicetype/base.html:49
|
||||
@@ -7884,7 +7870,7 @@ msgstr ""
|
||||
msgid "Device Types"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/tables/devicetypes.py:48 netbox/dcim/views.py:1595
|
||||
#: netbox/dcim/tables/devicetypes.py:48 netbox/dcim/views.py:1596
|
||||
#: netbox/netbox/navigation/menu.py:90
|
||||
msgid "Module Types"
|
||||
msgstr ""
|
||||
@@ -7907,8 +7893,8 @@ msgstr ""
|
||||
msgid "Device Count"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/tables/devicetypes.py:120 netbox/dcim/views.py:1363
|
||||
#: netbox/dcim/views.py:1700 netbox/dcim/views.py:2525
|
||||
#: netbox/dcim/tables/devicetypes.py:120 netbox/dcim/views.py:1364
|
||||
#: netbox/dcim/views.py:1717 netbox/dcim/views.py:2584
|
||||
#: netbox/netbox/navigation/menu.py:101
|
||||
#: netbox/templates/dcim/buttons/bulk_add_components.html:10
|
||||
#: netbox/templates/dcim/device/base.html:25
|
||||
@@ -7918,8 +7904,8 @@ msgstr ""
|
||||
msgid "Console Ports"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/tables/devicetypes.py:123 netbox/dcim/views.py:1378
|
||||
#: netbox/dcim/views.py:1715 netbox/dcim/views.py:2541
|
||||
#: netbox/dcim/tables/devicetypes.py:123 netbox/dcim/views.py:1379
|
||||
#: netbox/dcim/views.py:1732 netbox/dcim/views.py:2600
|
||||
#: netbox/netbox/navigation/menu.py:102
|
||||
#: netbox/templates/dcim/buttons/bulk_add_components.html:17
|
||||
#: netbox/templates/dcim/device/base.html:28
|
||||
@@ -7929,8 +7915,8 @@ msgstr ""
|
||||
msgid "Console Server Ports"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/tables/devicetypes.py:126 netbox/dcim/views.py:1393
|
||||
#: netbox/dcim/views.py:1730 netbox/dcim/views.py:2557
|
||||
#: netbox/dcim/tables/devicetypes.py:126 netbox/dcim/views.py:1394
|
||||
#: netbox/dcim/views.py:1747 netbox/dcim/views.py:2616
|
||||
#: netbox/netbox/navigation/menu.py:103
|
||||
#: netbox/templates/dcim/buttons/bulk_add_components.html:24
|
||||
#: netbox/templates/dcim/device/base.html:31
|
||||
@@ -7940,8 +7926,8 @@ msgstr ""
|
||||
msgid "Power Ports"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/tables/devicetypes.py:129 netbox/dcim/views.py:1408
|
||||
#: netbox/dcim/views.py:1745 netbox/dcim/views.py:2573
|
||||
#: netbox/dcim/tables/devicetypes.py:129 netbox/dcim/views.py:1409
|
||||
#: netbox/dcim/views.py:1762 netbox/dcim/views.py:2632
|
||||
#: netbox/netbox/navigation/menu.py:104
|
||||
#: netbox/templates/dcim/buttons/bulk_add_components.html:31
|
||||
#: netbox/templates/dcim/device/base.html:34
|
||||
@@ -7951,8 +7937,8 @@ msgstr ""
|
||||
msgid "Power Outlets"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/tables/devicetypes.py:135 netbox/dcim/views.py:1438
|
||||
#: netbox/dcim/views.py:1775 netbox/dcim/views.py:2612
|
||||
#: netbox/dcim/tables/devicetypes.py:135 netbox/dcim/views.py:1439
|
||||
#: netbox/dcim/views.py:1792 netbox/dcim/views.py:2671
|
||||
#: netbox/netbox/navigation/menu.py:99
|
||||
#: netbox/templates/dcim/device/base.html:40
|
||||
#: netbox/templates/dcim/devicetype/base.html:37
|
||||
@@ -7961,8 +7947,8 @@ msgstr ""
|
||||
msgid "Front Ports"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/tables/devicetypes.py:138 netbox/dcim/views.py:1453
|
||||
#: netbox/dcim/views.py:1790 netbox/dcim/views.py:2628
|
||||
#: netbox/dcim/tables/devicetypes.py:138 netbox/dcim/views.py:1454
|
||||
#: netbox/dcim/views.py:1807 netbox/dcim/views.py:2687
|
||||
#: netbox/netbox/navigation/menu.py:100
|
||||
#: netbox/templates/dcim/buttons/bulk_add_components.html:45
|
||||
#: netbox/templates/dcim/device/base.html:43
|
||||
@@ -7972,16 +7958,16 @@ msgstr ""
|
||||
msgid "Rear Ports"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/tables/devicetypes.py:141 netbox/dcim/views.py:1483
|
||||
#: netbox/dcim/views.py:2660 netbox/netbox/navigation/menu.py:106
|
||||
#: netbox/dcim/tables/devicetypes.py:141 netbox/dcim/views.py:1484
|
||||
#: netbox/dcim/views.py:2719 netbox/netbox/navigation/menu.py:106
|
||||
#: netbox/templates/dcim/buttons/bulk_add_components.html:52
|
||||
#: netbox/templates/dcim/device/base.html:49
|
||||
#: netbox/templates/dcim/devicetype/base.html:46
|
||||
msgid "Device Bays"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/tables/devicetypes.py:144 netbox/dcim/views.py:1468
|
||||
#: netbox/dcim/views.py:1805 netbox/dcim/views.py:2644
|
||||
#: netbox/dcim/tables/devicetypes.py:144 netbox/dcim/views.py:1469
|
||||
#: netbox/dcim/views.py:1822 netbox/dcim/views.py:2703
|
||||
#: netbox/netbox/navigation/menu.py:105
|
||||
#: netbox/templates/dcim/buttons/bulk_add_components.html:59
|
||||
#: netbox/templates/dcim/device/base.html:46
|
||||
@@ -8068,7 +8054,7 @@ msgid "{} millimeters"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/ui/panels.py:53 netbox/dcim/ui/panels.py:95
|
||||
#: netbox/virtualization/forms/filtersets.py:202
|
||||
#: netbox/dcim/ui/panels.py:168 netbox/virtualization/forms/filtersets.py:202
|
||||
#: netbox/virtualization/ui/panels.py:23
|
||||
msgid "Serial number"
|
||||
msgstr ""
|
||||
@@ -8086,51 +8072,63 @@ msgstr ""
|
||||
msgid "Out-of-band IP"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/ui/panels.py:150
|
||||
#: netbox/dcim/ui/panels.py:156
|
||||
msgid "Parent/child"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/ui/panels.py:166
|
||||
#: netbox/dcim/ui/panels.py:180
|
||||
msgid "Model name"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/ui/panels.py:197
|
||||
msgid "Virtual Chassis Members"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/ui/panels.py:185
|
||||
#: netbox/dcim/ui/panels.py:216
|
||||
msgid "Power Utilization"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/views.py:148
|
||||
#: netbox/dcim/views.py:149
|
||||
#, python-brace-format
|
||||
msgid "Disconnected {count} {type}"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/views.py:256
|
||||
#: netbox/dcim/views.py:257
|
||||
msgid "Child Regions"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/views.py:388 netbox/templates/tenancy/contactgroup.html:47
|
||||
#: netbox/dcim/views.py:389 netbox/templates/tenancy/contactgroup.html:47
|
||||
#: netbox/templates/tenancy/tenantgroup.html:56
|
||||
#: netbox/templates/wireless/wirelesslangroup.html:56
|
||||
msgid "Child Groups"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/views.py:546 netbox/dcim/views.py:686 netbox/dcim/views.py:1093
|
||||
#: netbox/dcim/views.py:547 netbox/dcim/views.py:687 netbox/dcim/views.py:1094
|
||||
msgid "Non-Racked Devices"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/views.py:672
|
||||
#: netbox/dcim/views.py:673
|
||||
msgid "Child Locations"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/views.py:1074 netbox/netbox/navigation/menu.py:54
|
||||
#: netbox/dcim/views.py:1075 netbox/netbox/navigation/menu.py:54
|
||||
msgid "Reservations"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/views.py:2470 netbox/netbox/navigation/menu.py:216
|
||||
#: netbox/dcim/views.py:2339
|
||||
msgid "Child Device Roles"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/views.py:2439
|
||||
msgid "Child Platforms"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/views.py:2529 netbox/netbox/navigation/menu.py:216
|
||||
#: netbox/templates/ipam/ipaddress.html:118 netbox/virtualization/views.py:419
|
||||
msgid "Application Services"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/views.py:2689 netbox/extras/forms/filtersets.py:402
|
||||
#: netbox/dcim/views.py:2748 netbox/extras/forms/filtersets.py:402
|
||||
#: netbox/extras/forms/model_forms.py:701
|
||||
#: netbox/templates/extras/configcontext.html:10
|
||||
#: netbox/virtualization/forms/model_forms.py:225
|
||||
@@ -8138,41 +8136,41 @@ msgstr ""
|
||||
msgid "Config Context"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/views.py:2700 netbox/virtualization/views.py:504
|
||||
#: netbox/dcim/views.py:2759 netbox/virtualization/views.py:504
|
||||
msgid "Render Config"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/views.py:2713 netbox/extras/tables/tables.py:725
|
||||
#: netbox/dcim/views.py:2772 netbox/extras/tables/tables.py:725
|
||||
#: netbox/netbox/navigation/menu.py:259 netbox/netbox/navigation/menu.py:261
|
||||
#: netbox/virtualization/views.py:278
|
||||
msgid "Virtual Machines"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/views.py:3532
|
||||
#: netbox/dcim/views.py:3606
|
||||
#, python-brace-format
|
||||
msgid "Installed device {device} in bay {device_bay}."
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/views.py:3573
|
||||
#: netbox/dcim/views.py:3647
|
||||
#, python-brace-format
|
||||
msgid "Removed device {device} from bay {device_bay}."
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/views.py:3686 netbox/ipam/tables/ip.py:179
|
||||
#: netbox/dcim/views.py:3760 netbox/ipam/tables/ip.py:179
|
||||
msgid "Children"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/views.py:4147
|
||||
#: netbox/dcim/views.py:4221
|
||||
#, python-brace-format
|
||||
msgid "Added member <a href=\"{url}\">{device}</a>"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/views.py:4192
|
||||
#: netbox/dcim/views.py:4266
|
||||
#, python-brace-format
|
||||
msgid "Unable to remove master device {device} from the virtual chassis."
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/views.py:4203
|
||||
#: netbox/dcim/views.py:4277
|
||||
#, python-brace-format
|
||||
msgid "Removed {device} from virtual chassis {chassis}"
|
||||
msgstr ""
|
||||
@@ -8822,7 +8820,7 @@ msgstr ""
|
||||
|
||||
#: netbox/extras/forms/bulk_import.py:305 netbox/extras/tables/tables.py:758
|
||||
#: netbox/netbox/tables/tables.py:295 netbox/netbox/tables/tables.py:310
|
||||
#: netbox/netbox/tables/tables.py:333 netbox/netbox/ui/panels.py:216
|
||||
#: netbox/netbox/tables/tables.py:333 netbox/netbox/ui/panels.py:219
|
||||
#: netbox/templates/dcim/htmx/cable_edit.html:99
|
||||
#: netbox/templates/generic/bulk_edit.html:99
|
||||
#: netbox/templates/inc/panels/comments.html:5
|
||||
@@ -12815,7 +12813,7 @@ msgstr ""
|
||||
msgid "GPS coordinates"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/netbox/ui/panels.py:263
|
||||
#: netbox/netbox/ui/panels.py:266
|
||||
#: netbox/templates/inc/panels/related_objects.html:5
|
||||
msgid "Related Objects"
|
||||
msgstr ""
|
||||
@@ -13115,7 +13113,7 @@ msgstr ""
|
||||
#: netbox/templates/dcim/inc/panels/inventory_items.html:45
|
||||
#: netbox/templates/dcim/interface.html:366
|
||||
#: netbox/templates/dcim/modulebay.html:80
|
||||
#: netbox/templates/dcim/moduletype.html:90
|
||||
#: netbox/templates/dcim/panels/module_type_attributes.html:26
|
||||
#: netbox/templates/extras/configcontext.html:46
|
||||
#: netbox/templates/extras/configtemplate.html:81
|
||||
#: netbox/templates/extras/eventrule.html:66
|
||||
@@ -13917,18 +13915,6 @@ msgstr ""
|
||||
msgid "Add Device"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/templates/dcim/devicerole.html:44
|
||||
msgid "VM Role"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/templates/dcim/devicerole.html:67
|
||||
msgid "Child Device Roles"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/templates/dcim/devicerole.html:71
|
||||
msgid "Add a Device Role"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/templates/dcim/frontport.html:50
|
||||
#: netbox/templates/dcim/rearport.html:50
|
||||
msgid "Positions"
|
||||
@@ -14118,7 +14104,7 @@ msgid "Part ID"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/templates/dcim/inventoryitem.html:60
|
||||
#: netbox/templates/dcim/module.html:81 netbox/templates/dcim/modulebay.html:74
|
||||
#: netbox/templates/dcim/modulebay.html:74
|
||||
msgid "Asset Tag"
|
||||
msgstr ""
|
||||
|
||||
@@ -14134,15 +14120,7 @@ msgstr ""
|
||||
msgid "Add Module Type"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/templates/dcim/moduletype.html:35
|
||||
msgid "Model Name"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/templates/dcim/moduletype.html:39
|
||||
msgid "Part Number"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/templates/dcim/moduletype.html:71
|
||||
#: netbox/templates/dcim/panels/module_type_attributes.html:7
|
||||
msgid "No profile assigned"
|
||||
msgstr ""
|
||||
|
||||
@@ -14185,14 +14163,6 @@ msgstr ""
|
||||
msgid "Labels only"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/templates/dcim/platform.html:64
|
||||
msgid "Child Platforms"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/templates/dcim/platform.html:68
|
||||
msgid "Add a Platform"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/templates/dcim/powerfeed.html:53
|
||||
msgid "Connected Device"
|
||||
msgstr ""
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import logging
|
||||
|
||||
from django.contrib.auth.signals import user_login_failed
|
||||
from django.contrib.auth.signals import user_logged_in, user_login_failed
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
|
||||
@@ -23,6 +23,18 @@ def log_user_login_failed(sender, credentials, request, **kwargs):
|
||||
logger.info(f"Failed login attempt for username: {username}")
|
||||
|
||||
|
||||
@receiver(user_logged_in)
|
||||
def set_language_on_login(sender, user, request, **kwargs):
|
||||
"""
|
||||
Store the user's preferred language on the request so that middleware can set the language cookie. This ensures the
|
||||
language preference is applied even when logging in via an external auth provider (e.g. social-app-django) that
|
||||
does not go through NetBox's LoginView.
|
||||
"""
|
||||
if hasattr(user, 'config'):
|
||||
if language := user.config.get('locale.language'):
|
||||
request._language_cookie = language
|
||||
|
||||
|
||||
@receiver(post_save, sender=User)
|
||||
def create_userconfig(instance, created, raw=False, **kwargs):
|
||||
"""
|
||||
|
||||
@@ -6,7 +6,6 @@ __all__ = (
|
||||
'AbortScript',
|
||||
'AbortTransaction',
|
||||
'PermissionsViolation',
|
||||
'PreconditionFailed',
|
||||
'RQWorkerNotRunningException',
|
||||
)
|
||||
|
||||
@@ -41,20 +40,6 @@ class PermissionsViolation(Exception):
|
||||
message = "Operation failed due to object-level permissions violation"
|
||||
|
||||
|
||||
class PreconditionFailed(APIException):
|
||||
"""
|
||||
Raised when an If-Match precondition is not satisfied (HTTP 412).
|
||||
Optionally carries the current ETag so it can be included in the response.
|
||||
"""
|
||||
status_code = status.HTTP_412_PRECONDITION_FAILED
|
||||
default_detail = 'Precondition failed.'
|
||||
default_code = 'precondition_failed'
|
||||
|
||||
def __init__(self, detail=None, code=None, etag=None):
|
||||
super().__init__(detail=detail, code=code)
|
||||
self.etag = etag
|
||||
|
||||
|
||||
class RQWorkerNotRunningException(APIException):
|
||||
"""
|
||||
Indicates the temporary inability to enqueue a new task (e.g. custom script execution) because no RQ worker
|
||||
|
||||
@@ -114,12 +114,7 @@ class APIViewTestCases:
|
||||
|
||||
# Try GET to permitted object
|
||||
url = self._get_detail_url(instance1)
|
||||
response = self.client.get(url, **self.header)
|
||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||
|
||||
# Verify ETag header is present for objects with timestamps
|
||||
if issubclass(self.model, ChangeLoggingMixin):
|
||||
self.assertIn('ETag', response, "ETag header missing from detail response")
|
||||
self.assertHttpStatus(self.client.get(url, **self.header), status.HTTP_200_OK)
|
||||
|
||||
# Try GET to non-permitted object
|
||||
url = self._get_detail_url(instance2)
|
||||
@@ -372,46 +367,6 @@ class APIViewTestCases:
|
||||
self.assertEqual(objectchange.action, ObjectChangeActionChoices.ACTION_UPDATE)
|
||||
self.assertEqual(objectchange.message, data['changelog_message'])
|
||||
|
||||
def test_update_object_with_etag(self):
|
||||
"""
|
||||
PATCH an object using a valid If-Match ETag → expect 200.
|
||||
PATCH again with the now-stale ETag → expect 412.
|
||||
"""
|
||||
if not issubclass(self.model, ChangeLoggingMixin):
|
||||
self.skipTest("Model does not support ETags")
|
||||
|
||||
self.add_permissions(
|
||||
f'{self.model._meta.app_label}.view_{self.model._meta.model_name}',
|
||||
f'{self.model._meta.app_label}.change_{self.model._meta.model_name}',
|
||||
)
|
||||
instance = self._get_queryset().first()
|
||||
url = self._get_detail_url(instance)
|
||||
update_data = self.update_data or getattr(self, 'create_data')[0]
|
||||
|
||||
# Fetch current ETag
|
||||
get_response = self.client.get(url, **self.header)
|
||||
self.assertHttpStatus(get_response, status.HTTP_200_OK)
|
||||
etag = get_response.get('ETag')
|
||||
self.assertIsNotNone(etag, "No ETag returned by GET")
|
||||
|
||||
# PATCH with correct ETag → 200
|
||||
response = self.client.patch(
|
||||
url, update_data, format='json',
|
||||
**{**self.header, 'HTTP_IF_MATCH': etag}
|
||||
)
|
||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||
new_etag = response.get('ETag')
|
||||
self.assertIsNotNone(new_etag)
|
||||
self.assertNotEqual(etag, new_etag) # ETag must change after update
|
||||
|
||||
# PATCH with the old (stale) ETag → 412
|
||||
with disable_warnings('django.request'):
|
||||
response = self.client.patch(
|
||||
url, update_data, format='json',
|
||||
**{**self.header, 'HTTP_IF_MATCH': etag}
|
||||
)
|
||||
self.assertHttpStatus(response, status.HTTP_412_PRECONDITION_FAILED)
|
||||
|
||||
def test_bulk_update_objects(self):
|
||||
"""
|
||||
PATCH a set of objects in a single request.
|
||||
|
||||
@@ -13,7 +13,7 @@ from dcim.tables import DeviceTable
|
||||
from extras.ui.panels import CustomFieldsPanel, ImageAttachmentsPanel, TagsPanel
|
||||
from extras.views import ObjectConfigContextView, ObjectRenderConfigView
|
||||
from ipam.models import IPAddress, VLANGroup
|
||||
from ipam.tables import InterfaceVLANTable, VLANTranslationRuleTable
|
||||
from ipam.tables import VLANTranslationRuleTable
|
||||
from ipam.ui.panels import FHRPGroupAssignmentsPanel
|
||||
from netbox.object_actions import (
|
||||
AddObject,
|
||||
@@ -594,7 +594,11 @@ class VMInterfaceView(generic.ObjectView):
|
||||
),
|
||||
],
|
||||
),
|
||||
ContextTablePanel('vlan_table', title=_('Assigned VLANs')),
|
||||
ObjectsTablePanel(
|
||||
model='ipam.VLAN',
|
||||
title=_('Assigned VLANs'),
|
||||
filters={'vminterface_id': lambda ctx: ctx['object'].pk},
|
||||
),
|
||||
ContextTablePanel('vlan_translation_table', title=_('VLAN Translation')),
|
||||
ContextTablePanel('child_interfaces_table', title=_('Child Interfaces')),
|
||||
],
|
||||
@@ -620,24 +624,8 @@ class VMInterfaceView(generic.ObjectView):
|
||||
)
|
||||
vlan_translation_table.configure(request)
|
||||
|
||||
# Get assigned VLANs and annotate whether each is tagged or untagged
|
||||
vlans = []
|
||||
if instance.untagged_vlan is not None:
|
||||
vlans.append(instance.untagged_vlan)
|
||||
vlans[0].tagged = False
|
||||
for vlan in instance.tagged_vlans.restrict(request.user).prefetch_related('site', 'group', 'tenant', 'role'):
|
||||
vlan.tagged = True
|
||||
vlans.append(vlan)
|
||||
vlan_table = InterfaceVLANTable(
|
||||
interface=instance,
|
||||
data=vlans,
|
||||
orderable=False
|
||||
)
|
||||
vlan_table.configure(request)
|
||||
|
||||
return {
|
||||
'child_interfaces_table': child_interfaces_tables,
|
||||
'vlan_table': vlan_table,
|
||||
'vlan_translation_table': vlan_translation_table,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
colorama==0.4.6
|
||||
Django==6.0.3
|
||||
Django==5.2.11
|
||||
django-cors-headers==4.9.0
|
||||
django-debug-toolbar==6.2.0
|
||||
django-filter==25.2
|
||||
@@ -7,7 +7,7 @@ django-graphiql-debug-toolbar==0.2.0
|
||||
django-htmx==1.27.0
|
||||
django-mptt==0.18.0
|
||||
django-pglocks==1.0.4
|
||||
django-prometheus==2.4.0
|
||||
django-prometheus==2.4.1
|
||||
django-redis==6.0.0
|
||||
django-rich==2.2.0
|
||||
django-rq==3.2.2
|
||||
|
||||
Reference in New Issue
Block a user