mirror of
https://github.com/netbox-community/netbox.git
synced 2026-02-19 15:27:44 +01:00
Compare commits
41 Commits
v4.5.2
...
21478-grap
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e84b062393 | ||
|
|
ef52ac4203 | ||
|
|
b22e490847 | ||
|
|
945e7ade0a | ||
|
|
7300104cea | ||
|
|
2900429769 | ||
|
|
278c82dd88 | ||
|
|
c029782cf5 | ||
|
|
bdd23f3d17 | ||
|
|
af6e18b7d4 | ||
|
|
816c5d4bea | ||
|
|
f4c3c90bab | ||
|
|
862593f2dd | ||
|
|
f4c27fd494 | ||
|
|
ae736ef407 | ||
|
|
d95b1186fb | ||
|
|
d6b9d30086 | ||
|
|
9be5aa188c | ||
|
|
f113557e81 | ||
|
|
de812a5a85 | ||
|
|
0b7375136d | ||
|
|
1190adde2b | ||
|
|
2330874a8c | ||
|
|
dc738c7102 | ||
|
|
76fd3e3c61 | ||
|
|
4ee64a7731 | ||
|
|
0bb22dee0c | ||
|
|
6c383f293c | ||
|
|
5bf516c63d | ||
|
|
7df062d590 | ||
|
|
4b22be03a0 | ||
|
|
24769ce127 | ||
|
|
164e9db98d | ||
|
|
23f1c86e9c | ||
|
|
02ffdd9d5d | ||
|
|
5013297326 | ||
|
|
584e0a9b8c | ||
|
|
3ac9d0b8bf | ||
|
|
b387ea5f58 | ||
|
|
ba9f6bf359 | ||
|
|
ee6cbdcefe |
@@ -15,7 +15,7 @@ body:
|
||||
attributes:
|
||||
label: NetBox version
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v4.5.2
|
||||
placeholder: v4.5.3
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/02-bug_report.yaml
vendored
2
.github/ISSUE_TEMPLATE/02-bug_report.yaml
vendored
@@ -27,7 +27,7 @@ body:
|
||||
attributes:
|
||||
label: NetBox Version
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v4.5.2
|
||||
placeholder: v4.5.3
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/03-performance.yaml
vendored
2
.github/ISSUE_TEMPLATE/03-performance.yaml
vendored
@@ -8,7 +8,7 @@ body:
|
||||
attributes:
|
||||
label: NetBox Version
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v4.5.2
|
||||
placeholder: v4.5.3
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
||||
@@ -27,9 +27,7 @@ django-graphiql-debug-toolbar
|
||||
django-htmx
|
||||
|
||||
# Modified Preorder Tree Traversal (recursive nesting of objects)
|
||||
# https://github.com/django-mptt/django-mptt/blob/main/CHANGELOG.rst
|
||||
# v0.18.0 introduces errant migrations which need to be resolved
|
||||
django-mptt==0.17.0
|
||||
django-mptt
|
||||
|
||||
# Context managers for PostgreSQL advisory locks
|
||||
# https://github.com/Xof/django-pglocks/blob/master/CHANGES.txt
|
||||
@@ -159,7 +157,8 @@ strawberry-graphql
|
||||
|
||||
# Strawberry GraphQL Django extension
|
||||
# https://github.com/strawberry-graphql/strawberry-django/releases
|
||||
strawberry-graphql-django
|
||||
# Blocked by #21450
|
||||
strawberry-graphql-django==0.75.0
|
||||
|
||||
# SVG image rendering (used for rack elevations)
|
||||
# https://github.com/mozman/svgwrite/blob/master/NEWS.rst
|
||||
|
||||
1152
contrib/openapi.json
1152
contrib/openapi.json
File diff suppressed because it is too large
Load Diff
@@ -200,6 +200,48 @@ REDIS = {
|
||||
!!! note
|
||||
It is permissible to use Sentinel for only one database and not the other.
|
||||
|
||||
### SSL Configuration
|
||||
|
||||
If you need to configure SSL/TLS for Redis beyond the basic `SSL`, `CA_CERT_PATH`, and `INSECURE_SKIP_TLS_VERIFY` options (for example, client certificates, a specific TLS version, or custom ciphers), you can pass additional parameters via the `KWARGS` key in either the `tasks` or `caching` subsection.
|
||||
|
||||
NetBox already maps `CA_CERT_PATH` to `ssl_ca_certs` and (for caching) `INSECURE_SKIP_TLS_VERIFY` to `ssl_cert_reqs`; only add `KWARGS` when you need to override or extend those settings (for example, to supply client certificates or restrict TLS version or ciphers).
|
||||
|
||||
* `KWARGS` - Optional dictionary of additional SSL/TLS (or other) parameters passed to the Redis client. These are passed directly to the underlying Redis client: for `tasks` to [redis-py](https://redis-py.readthedocs.io/en/stable/connections.html), and for `caching` to the [django-redis](https://github.com/jazzband/django-redis#configure-as-cache-backend) connection pool.
|
||||
|
||||
Example:
|
||||
|
||||
```python
|
||||
REDIS = {
|
||||
'tasks': {
|
||||
'HOST': 'redis.example.com',
|
||||
'PORT': 1234,
|
||||
'SSL': True,
|
||||
'CA_CERT_PATH': '/etc/ssl/certs/ca.crt',
|
||||
'KWARGS': {
|
||||
'ssl_certfile': '/path/to/client-cert.pem',
|
||||
'ssl_keyfile': '/path/to/client-key.pem',
|
||||
'ssl_min_version': ssl.TLSVersion.TLSv1_2,
|
||||
'ssl_ciphers': 'HIGH:!aNULL',
|
||||
},
|
||||
},
|
||||
'caching': {
|
||||
'HOST': 'redis.example.com',
|
||||
'PORT': 1234,
|
||||
'SSL': True,
|
||||
'CA_CERT_PATH': '/etc/ssl/certs/ca.crt',
|
||||
'KWARGS': {
|
||||
'ssl_certfile': '/path/to/client-cert.pem',
|
||||
'ssl_keyfile': '/path/to/client-key.pem',
|
||||
'ssl_min_version': ssl.TLSVersion.TLSv1_2,
|
||||
'ssl_ciphers': 'HIGH:!aNULL',
|
||||
},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
!!! note
|
||||
If you use `ssl.TLSVersion` in your configuration (e.g. `ssl_min_version`), add `import ssl` at the top of your configuration file.
|
||||
|
||||
---
|
||||
|
||||
## SECRET_KEY
|
||||
|
||||
@@ -18,7 +18,8 @@ They can also be used as a mechanism for validating the integrity of data within
|
||||
Custom scripts are Python code which exists outside the NetBox code base, so they can be updated and changed without interfering with the core NetBox installation. And because they're completely custom, there is no inherent limitation on what a script can accomplish.
|
||||
|
||||
!!! danger "Only install trusted scripts"
|
||||
Custom scripts have unrestricted access to change anything in the databse and are inherently unsafe and should only be installed and run from trusted sources. You should also review and set permissions for who can run scripts if the script can modify any data.
|
||||
Custom scripts have unrestricted access to change anything in the database and are inherently unsafe and should only be installed and run from trusted sources. You should also review and set permissions for who can run scripts if the script can modify any data.
|
||||
|
||||
|
||||
## Writing Custom Scripts
|
||||
|
||||
|
||||
@@ -1,5 +1,42 @@
|
||||
# NetBox v4.5
|
||||
|
||||
## v4.5.3 (2026-02-17)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#19129](https://github.com/netbox-community/netbox/issues/19129) - Improve display of multiple MAC addresses within interfaces table
|
||||
* [#20981](https://github.com/netbox-community/netbox/issues/20981) - Enhance JSON rendering for custom validators and protection rules in config revision view
|
||||
* [#21240](https://github.com/netbox-community/netbox/issues/21240) - Add support for configuring Redis `KWARGS` parameters
|
||||
* [#21257](https://github.com/netbox-community/netbox/issues/21257) - `ContentTypeFilter` now accepts multiple values
|
||||
* [#21266](https://github.com/netbox-community/netbox/issues/21266) - Add table columns representing installed devices to the device bays table
|
||||
* [#21267](https://github.com/netbox-community/netbox/issues/21267) - Normalize device height formatting in rack units (display "0U")
|
||||
* [#21268](https://github.com/netbox-community/netbox/issues/21268) - Add device type details panel to device view
|
||||
* [#21337](https://github.com/netbox-community/netbox/issues/21337) - Show the assigned platform's parent on the virtual machine UI view
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* [#20211](https://github.com/netbox-community/netbox/issues/20211) - Use thumbnails for image attachment hover previews to improve page load performance
|
||||
* [#21016](https://github.com/netbox-community/netbox/issues/21016) - Restore missing SQL indexes for MPTT fields
|
||||
* [#21196](https://github.com/netbox-community/netbox/issues/21196) - `q` filter should match on primary IP only for IP address values when filtering devices/VMs
|
||||
* [#21420](https://github.com/netbox-community/netbox/issues/21420) - Improve query performance of `ContentTypeFilter`
|
||||
* [#21421](https://github.com/netbox-community/netbox/issues/21421) - Eliminate extraneous application of `DISTINCT` to queries for `MultipleChoiceFilter`
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#20435](https://github.com/netbox-community/netbox/issues/20435) - Fix navigation menu margin issue when scrollbar appears
|
||||
* [#21127](https://github.com/netbox-community/netbox/issues/21127) - Ensure assigned cable paths are cleared when removing terminations from a cable
|
||||
* [#21277](https://github.com/netbox-community/netbox/issues/21277) - Record pre-change snapshot when adding cluster members via UI
|
||||
* [#21320](https://github.com/netbox-community/netbox/issues/21320) - Avoid validation failures when site or optional fields are missing during rack import
|
||||
* [#21354](https://github.com/netbox-community/netbox/issues/21354) - Fix base URL in Swagger when `BASE_PATH` is set
|
||||
* [#21358](https://github.com/netbox-community/netbox/issues/21358) - Token list in UI cannot be ordered by token value
|
||||
* [#21371](https://github.com/netbox-community/netbox/issues/21371) - Fix `KeyError` exception when triggering a webhook from an event rule
|
||||
* [#21375](https://github.com/netbox-community/netbox/issues/21375) - Address failure condition in `ipam.0070_vlangroup_vlan_id_ranges` migration
|
||||
* [#21390](https://github.com/netbox-community/netbox/issues/21390) - Avoid creating "empty" changelog records for related objects when processing manyo-to-many relations
|
||||
* [#21397](https://github.com/netbox-community/netbox/issues/21397) - Correct rendering of owner field in CircuitType edit form
|
||||
* [#21412](https://github.com/netbox-community/netbox/issues/21412) - Avoid `AttributeError` exception on initialization when a plugin has local imports in `__init__.py`
|
||||
|
||||
---
|
||||
|
||||
## v4.5.2 (2026-02-03)
|
||||
|
||||
### Enhancements
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from django.urls import include, path
|
||||
|
||||
from utilities.urls import get_model_urls
|
||||
|
||||
from . import views
|
||||
|
||||
app_name = 'account'
|
||||
|
||||
@@ -2,14 +2,15 @@ import logging
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth import login as auth_login, logout as auth_logout, update_session_auth_hash
|
||||
from django.contrib.auth import login as auth_login
|
||||
from django.contrib.auth import logout as auth_logout
|
||||
from django.contrib.auth import update_session_auth_hash
|
||||
from django.contrib.auth.forms import AuthenticationForm, PasswordChangeForm
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.contrib.auth.models import update_last_login
|
||||
from django.contrib.auth.signals import user_logged_in
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.shortcuts import render, resolve_url
|
||||
from django.shortcuts import get_object_or_404, redirect, render, resolve_url
|
||||
from django.urls import reverse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils.http import urlencode
|
||||
@@ -35,11 +36,11 @@ from utilities.request import safe_for_redirect
|
||||
from utilities.string import remove_linebreaks
|
||||
from utilities.views import register_model_view
|
||||
|
||||
|
||||
#
|
||||
# Login/logout
|
||||
#
|
||||
|
||||
|
||||
class LoginView(View):
|
||||
"""
|
||||
Perform user authentication via the web UI.
|
||||
@@ -139,9 +140,8 @@ class LoginView(View):
|
||||
|
||||
return response
|
||||
|
||||
else:
|
||||
username = form['username'].value()
|
||||
logger.debug(f"Login form validation failed for username: {remove_linebreaks(username)}")
|
||||
username = form['username'].value()
|
||||
logger.debug(f"Login form validation failed for username: {remove_linebreaks(username)}")
|
||||
|
||||
return render(request, self.template_name, {
|
||||
'form': form,
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
from .serializers_.providers import *
|
||||
from .serializers_.circuits import *
|
||||
from .serializers_.providers import *
|
||||
|
||||
@@ -4,18 +4,28 @@ from rest_framework import serializers
|
||||
from circuits.choices import CircuitPriorityChoices, CircuitStatusChoices, VirtualCircuitTerminationRoleChoices
|
||||
from circuits.constants import CIRCUIT_GROUP_ASSIGNMENT_MEMBER_MODELS, CIRCUIT_TERMINATION_TERMINATION_TYPES
|
||||
from circuits.models import (
|
||||
Circuit, CircuitGroup, CircuitGroupAssignment, CircuitTermination, CircuitType, VirtualCircuit,
|
||||
VirtualCircuitTermination, VirtualCircuitType,
|
||||
Circuit,
|
||||
CircuitGroup,
|
||||
CircuitGroupAssignment,
|
||||
CircuitTermination,
|
||||
CircuitType,
|
||||
VirtualCircuit,
|
||||
VirtualCircuitTermination,
|
||||
VirtualCircuitType,
|
||||
)
|
||||
from dcim.api.serializers_.device_components import InterfaceSerializer
|
||||
from dcim.api.serializers_.cables import CabledObjectSerializer
|
||||
from dcim.api.serializers_.device_components import InterfaceSerializer
|
||||
from netbox.api.fields import ChoiceField, ContentTypeField, RelatedObjectCountField
|
||||
from netbox.api.gfk_fields import GFKSerializerField
|
||||
from netbox.api.serializers import (
|
||||
NetBoxModelSerializer, OrganizationalModelSerializer, PrimaryModelSerializer, WritableNestedSerializer,
|
||||
NetBoxModelSerializer,
|
||||
OrganizationalModelSerializer,
|
||||
PrimaryModelSerializer,
|
||||
WritableNestedSerializer,
|
||||
)
|
||||
from netbox.choices import DistanceUnitChoices
|
||||
from tenancy.api.serializers_.tenants import TenantSerializer
|
||||
|
||||
from .providers import ProviderAccountSerializer, ProviderNetworkSerializer, ProviderSerializer
|
||||
|
||||
__all__ = (
|
||||
|
||||
@@ -5,6 +5,7 @@ from ipam.api.serializers_.asns import ASNSerializer
|
||||
from ipam.models import ASN
|
||||
from netbox.api.fields import RelatedObjectCountField, SerializedPKRelatedField
|
||||
from netbox.api.serializers import PrimaryModelSerializer
|
||||
|
||||
from .nested import NestedProviderAccountSerializer
|
||||
|
||||
__all__ = (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from netbox.api.routers import NetBoxRouter
|
||||
from . import views
|
||||
|
||||
from . import views
|
||||
|
||||
router = NetBoxRouter()
|
||||
router.APIRootView = views.CircuitsRootView
|
||||
|
||||
@@ -4,6 +4,7 @@ from circuits import filtersets
|
||||
from circuits.models import *
|
||||
from dcim.api.views import PassThroughPortMixin
|
||||
from netbox.api.viewsets import NetBoxModelViewSet
|
||||
|
||||
from . import serializers
|
||||
|
||||
|
||||
|
||||
@@ -9,7 +9,8 @@ class CircuitsConfig(AppConfig):
|
||||
|
||||
def ready(self):
|
||||
from netbox.models.features import register_models
|
||||
from . import signals, search # noqa: F401
|
||||
|
||||
from . import search, signals # noqa: F401
|
||||
from .models import CircuitTermination
|
||||
|
||||
# Register models
|
||||
|
||||
@@ -2,11 +2,11 @@ from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from utilities.choices import ChoiceSet
|
||||
|
||||
|
||||
#
|
||||
# Circuits
|
||||
#
|
||||
|
||||
|
||||
class CircuitStatusChoices(ChoiceSet):
|
||||
key = 'Circuit.status'
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from django.db.models import Q
|
||||
|
||||
|
||||
# models values for ContentTypes which may be CircuitTermination termination types
|
||||
CIRCUIT_TERMINATION_TERMINATION_TYPES = (
|
||||
'region', 'sitegroup', 'site', 'location', 'providernetwork',
|
||||
|
||||
@@ -9,9 +9,13 @@ from ipam.models import ASN
|
||||
from netbox.filtersets import NetBoxModelFilterSet, OrganizationalModelFilterSet, PrimaryModelFilterSet
|
||||
from tenancy.filtersets import ContactModelFilterSet, TenancyFilterSet
|
||||
from utilities.filters import (
|
||||
ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter, TreeNodeMultipleChoiceFilter,
|
||||
MultiValueCharFilter,
|
||||
MultiValueContentTypeFilter,
|
||||
MultiValueNumberFilter,
|
||||
TreeNodeMultipleChoiceFilter,
|
||||
)
|
||||
from utilities.filtersets import register_filterset
|
||||
|
||||
from .choices import *
|
||||
from .models import *
|
||||
|
||||
@@ -99,11 +103,13 @@ class ProviderFilterSet(PrimaryModelFilterSet, ContactModelFilterSet):
|
||||
class ProviderAccountFilterSet(PrimaryModelFilterSet, ContactModelFilterSet):
|
||||
provider_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Provider.objects.all(),
|
||||
distinct=False,
|
||||
label=_('Provider (ID)'),
|
||||
)
|
||||
provider = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='provider__slug',
|
||||
queryset=Provider.objects.all(),
|
||||
distinct=False,
|
||||
to_field_name='slug',
|
||||
label=_('Provider (slug)'),
|
||||
)
|
||||
@@ -127,11 +133,13 @@ class ProviderAccountFilterSet(PrimaryModelFilterSet, ContactModelFilterSet):
|
||||
class ProviderNetworkFilterSet(PrimaryModelFilterSet):
|
||||
provider_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Provider.objects.all(),
|
||||
distinct=False,
|
||||
label=_('Provider (ID)'),
|
||||
)
|
||||
provider = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='provider__slug',
|
||||
queryset=Provider.objects.all(),
|
||||
distinct=False,
|
||||
to_field_name='slug',
|
||||
label=_('Provider (slug)'),
|
||||
)
|
||||
@@ -163,22 +171,26 @@ class CircuitTypeFilterSet(OrganizationalModelFilterSet):
|
||||
class CircuitFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
|
||||
provider_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Provider.objects.all(),
|
||||
distinct=False,
|
||||
label=_('Provider (ID)'),
|
||||
)
|
||||
provider = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='provider__slug',
|
||||
queryset=Provider.objects.all(),
|
||||
distinct=False,
|
||||
to_field_name='slug',
|
||||
label=_('Provider (slug)'),
|
||||
)
|
||||
provider_account_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='provider_account',
|
||||
queryset=ProviderAccount.objects.all(),
|
||||
distinct=False,
|
||||
label=_('Provider account (ID)'),
|
||||
)
|
||||
provider_account = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='provider_account__account',
|
||||
queryset=Provider.objects.all(),
|
||||
distinct=False,
|
||||
to_field_name='account',
|
||||
label=_('Provider account (account)'),
|
||||
)
|
||||
@@ -189,16 +201,19 @@ class CircuitFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilt
|
||||
)
|
||||
type_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=CircuitType.objects.all(),
|
||||
distinct=False,
|
||||
label=_('Circuit type (ID)'),
|
||||
)
|
||||
type = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='type__slug',
|
||||
queryset=CircuitType.objects.all(),
|
||||
distinct=False,
|
||||
to_field_name='slug',
|
||||
label=_('Circuit type (slug)'),
|
||||
)
|
||||
status = django_filters.MultipleChoiceFilter(
|
||||
choices=CircuitStatusChoices,
|
||||
distinct=False,
|
||||
null_value=None
|
||||
)
|
||||
region_id = TreeNodeMultipleChoiceFilter(
|
||||
@@ -245,10 +260,12 @@ class CircuitFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilt
|
||||
)
|
||||
termination_a_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=CircuitTermination.objects.all(),
|
||||
distinct=False,
|
||||
label=_('Termination A (ID)'),
|
||||
)
|
||||
termination_z_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=CircuitTermination.objects.all(),
|
||||
distinct=False,
|
||||
label=_('Termination A (ID)'),
|
||||
)
|
||||
|
||||
@@ -279,9 +296,10 @@ class CircuitTerminationFilterSet(NetBoxModelFilterSet, CabledObjectFilterSet):
|
||||
)
|
||||
circuit_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Circuit.objects.all(),
|
||||
distinct=False,
|
||||
label=_('Circuit'),
|
||||
)
|
||||
termination_type = ContentTypeFilter()
|
||||
termination_type = MultiValueContentTypeFilter()
|
||||
region_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='_region',
|
||||
@@ -310,12 +328,14 @@ class CircuitTerminationFilterSet(NetBoxModelFilterSet, CabledObjectFilterSet):
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Site.objects.all(),
|
||||
distinct=False,
|
||||
field_name='_site',
|
||||
label=_('Site (ID)'),
|
||||
)
|
||||
site = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='_site__slug',
|
||||
queryset=Site.objects.all(),
|
||||
distinct=False,
|
||||
to_field_name='slug',
|
||||
label=_('Site (slug)'),
|
||||
)
|
||||
@@ -334,17 +354,20 @@ class CircuitTerminationFilterSet(NetBoxModelFilterSet, CabledObjectFilterSet):
|
||||
)
|
||||
provider_network_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=ProviderNetwork.objects.all(),
|
||||
distinct=False,
|
||||
field_name='_provider_network',
|
||||
label=_('ProviderNetwork (ID)'),
|
||||
)
|
||||
provider_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='circuit__provider_id',
|
||||
queryset=Provider.objects.all(),
|
||||
distinct=False,
|
||||
label=_('Provider (ID)'),
|
||||
)
|
||||
provider = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='circuit__provider__slug',
|
||||
queryset=Provider.objects.all(),
|
||||
distinct=False,
|
||||
to_field_name='slug',
|
||||
label=_('Provider (slug)'),
|
||||
)
|
||||
@@ -381,7 +404,7 @@ class CircuitGroupAssignmentFilterSet(NetBoxModelFilterSet):
|
||||
method='search',
|
||||
label=_('Search'),
|
||||
)
|
||||
member_type = ContentTypeFilter()
|
||||
member_type = MultiValueContentTypeFilter()
|
||||
circuit = MultiValueCharFilter(
|
||||
method='filter_circuit',
|
||||
field_name='cid',
|
||||
@@ -414,11 +437,13 @@ class CircuitGroupAssignmentFilterSet(NetBoxModelFilterSet):
|
||||
)
|
||||
group_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=CircuitGroup.objects.all(),
|
||||
distinct=False,
|
||||
label=_('Circuit group (ID)'),
|
||||
)
|
||||
group = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='group__slug',
|
||||
queryset=CircuitGroup.objects.all(),
|
||||
distinct=False,
|
||||
to_field_name='slug',
|
||||
label=_('Circuit group (slug)'),
|
||||
)
|
||||
@@ -488,41 +513,49 @@ class VirtualCircuitFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
||||
provider_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='provider_network__provider',
|
||||
queryset=Provider.objects.all(),
|
||||
distinct=False,
|
||||
label=_('Provider (ID)'),
|
||||
)
|
||||
provider = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='provider_network__provider__slug',
|
||||
queryset=Provider.objects.all(),
|
||||
distinct=False,
|
||||
to_field_name='slug',
|
||||
label=_('Provider (slug)'),
|
||||
)
|
||||
provider_account_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='provider_account',
|
||||
queryset=ProviderAccount.objects.all(),
|
||||
distinct=False,
|
||||
label=_('Provider account (ID)'),
|
||||
)
|
||||
provider_account = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='provider_account__account',
|
||||
queryset=Provider.objects.all(),
|
||||
distinct=False,
|
||||
to_field_name='account',
|
||||
label=_('Provider account (account)'),
|
||||
)
|
||||
provider_network_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=ProviderNetwork.objects.all(),
|
||||
distinct=False,
|
||||
label=_('Provider network (ID)'),
|
||||
)
|
||||
type_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=VirtualCircuitType.objects.all(),
|
||||
distinct=False,
|
||||
label=_('Virtual circuit type (ID)'),
|
||||
)
|
||||
type = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='type__slug',
|
||||
queryset=VirtualCircuitType.objects.all(),
|
||||
distinct=False,
|
||||
to_field_name='slug',
|
||||
label=_('Virtual circuit type (slug)'),
|
||||
)
|
||||
status = django_filters.MultipleChoiceFilter(
|
||||
choices=CircuitStatusChoices,
|
||||
distinct=False,
|
||||
null_value=None
|
||||
)
|
||||
|
||||
@@ -548,41 +581,49 @@ class VirtualCircuitTerminationFilterSet(NetBoxModelFilterSet):
|
||||
)
|
||||
virtual_circuit_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=VirtualCircuit.objects.all(),
|
||||
distinct=False,
|
||||
label=_('Virtual circuit'),
|
||||
)
|
||||
role = django_filters.MultipleChoiceFilter(
|
||||
choices=VirtualCircuitTerminationRoleChoices,
|
||||
distinct=False,
|
||||
null_value=None
|
||||
)
|
||||
provider_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='virtual_circuit__provider_network__provider',
|
||||
queryset=Provider.objects.all(),
|
||||
distinct=False,
|
||||
label=_('Provider (ID)'),
|
||||
)
|
||||
provider = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='virtual_circuit__provider_network__provider__slug',
|
||||
queryset=Provider.objects.all(),
|
||||
distinct=False,
|
||||
to_field_name='slug',
|
||||
label=_('Provider (slug)'),
|
||||
)
|
||||
provider_account_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='virtual_circuit__provider_account',
|
||||
queryset=ProviderAccount.objects.all(),
|
||||
distinct=False,
|
||||
label=_('Provider account (ID)'),
|
||||
)
|
||||
provider_account = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='virtual_circuit__provider_account__account',
|
||||
queryset=ProviderAccount.objects.all(),
|
||||
distinct=False,
|
||||
to_field_name='account',
|
||||
label=_('Provider account (account)'),
|
||||
)
|
||||
provider_network_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=ProviderNetwork.objects.all(),
|
||||
distinct=False,
|
||||
field_name='virtual_circuit__provider_network',
|
||||
label=_('Provider network (ID)'),
|
||||
)
|
||||
interface_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Interface.objects.all(),
|
||||
distinct=False,
|
||||
field_name='interface',
|
||||
label=_('Interface (ID)'),
|
||||
)
|
||||
|
||||
@@ -4,7 +4,10 @@ from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from circuits.choices import (
|
||||
CircuitCommitRateChoices, CircuitPriorityChoices, CircuitStatusChoices, VirtualCircuitTerminationRoleChoices,
|
||||
CircuitCommitRateChoices,
|
||||
CircuitPriorityChoices,
|
||||
CircuitStatusChoices,
|
||||
VirtualCircuitTerminationRoleChoices,
|
||||
)
|
||||
from circuits.constants import CIRCUIT_TERMINATION_TERMINATION_TYPES
|
||||
from circuits.models import *
|
||||
@@ -15,7 +18,10 @@ from netbox.forms import NetBoxModelBulkEditForm, OrganizationalModelBulkEditFor
|
||||
from tenancy.models import Tenant
|
||||
from utilities.forms import add_blank_choice, get_field_value
|
||||
from utilities.forms.fields import (
|
||||
ColorField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField,
|
||||
ColorField,
|
||||
ContentTypeChoiceField,
|
||||
DynamicModelChoiceField,
|
||||
DynamicModelMultipleChoiceField,
|
||||
)
|
||||
from utilities.forms.rendering import FieldSet
|
||||
from utilities.forms.widgets import BulkEditNullBooleanSelect, DatePicker, HTMXSelect, NumberWithOptions
|
||||
|
||||
@@ -2,7 +2,10 @@ from django import forms
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from circuits.choices import (
|
||||
CircuitCommitRateChoices, CircuitPriorityChoices, CircuitStatusChoices, CircuitTerminationSideChoices,
|
||||
CircuitCommitRateChoices,
|
||||
CircuitPriorityChoices,
|
||||
CircuitStatusChoices,
|
||||
CircuitTerminationSideChoices,
|
||||
VirtualCircuitTerminationRoleChoices,
|
||||
)
|
||||
from circuits.models import *
|
||||
@@ -10,7 +13,7 @@ from dcim.models import Location, Region, Site, SiteGroup
|
||||
from ipam.models import ASN
|
||||
from netbox.choices import DistanceUnitChoices
|
||||
from netbox.forms import NetBoxModelFilterSetForm, OrganizationalModelFilterSetForm, PrimaryModelFilterSetForm
|
||||
from tenancy.forms import TenancyFilterForm, ContactModelFilterForm
|
||||
from tenancy.forms import ContactModelFilterForm, TenancyFilterForm
|
||||
from utilities.forms import add_blank_choice
|
||||
from utilities.forms.fields import ColorField, DynamicModelMultipleChoiceField, TagFilterField
|
||||
from utilities.forms.rendering import FieldSet
|
||||
|
||||
@@ -4,7 +4,9 @@ from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from circuits.choices import (
|
||||
CircuitCommitRateChoices, CircuitTerminationPortSpeedChoices, VirtualCircuitTerminationRoleChoices,
|
||||
CircuitCommitRateChoices,
|
||||
CircuitTerminationPortSpeedChoices,
|
||||
VirtualCircuitTerminationRoleChoices,
|
||||
)
|
||||
from circuits.constants import *
|
||||
from circuits.models import *
|
||||
@@ -14,7 +16,10 @@ from netbox.forms import NetBoxModelForm, OrganizationalModelForm, PrimaryModelF
|
||||
from tenancy.forms import TenancyForm
|
||||
from utilities.forms import get_field_value
|
||||
from utilities.forms.fields import (
|
||||
ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField,
|
||||
ContentTypeChoiceField,
|
||||
DynamicModelChoiceField,
|
||||
DynamicModelMultipleChoiceField,
|
||||
SlugField,
|
||||
)
|
||||
from utilities.forms.mixins import DistanceValidationMixin
|
||||
from utilities.forms.rendering import FieldSet, InlineFields
|
||||
@@ -91,13 +96,13 @@ class ProviderNetworkForm(PrimaryModelForm):
|
||||
|
||||
class CircuitTypeForm(OrganizationalModelForm):
|
||||
fieldsets = (
|
||||
FieldSet('name', 'slug', 'color', 'description', 'owner', 'tags'),
|
||||
FieldSet('name', 'slug', 'color', 'description', 'tags'),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = CircuitType
|
||||
fields = [
|
||||
'name', 'slug', 'color', 'description', 'comments', 'tags',
|
||||
'name', 'slug', 'color', 'description', 'owner', 'comments', 'tags',
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Annotated, TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Annotated
|
||||
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
from datetime import date
|
||||
from typing import Annotated, TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Annotated
|
||||
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
from strawberry.scalars import ID
|
||||
from strawberry_django import BaseFilterLookup, FilterLookup, DateFilterLookup
|
||||
from strawberry_django import BaseFilterLookup, DateFilterLookup, FilterLookup
|
||||
|
||||
from circuits import models
|
||||
from circuits.graphql.filter_mixins import CircuitTypeFilterMixin
|
||||
@@ -19,6 +19,7 @@ if TYPE_CHECKING:
|
||||
from dcim.graphql.filters import InterfaceFilter, LocationFilter, RegionFilter, SiteFilter, SiteGroupFilter
|
||||
from ipam.graphql.filters import ASNFilter
|
||||
from netbox.graphql.filter_lookups import IntegerLookup
|
||||
|
||||
from .enums import *
|
||||
|
||||
__all__ = (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from typing import Annotated, List, TYPE_CHECKING, Union
|
||||
from typing import TYPE_CHECKING, Annotated, List, Union
|
||||
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
@@ -8,6 +8,7 @@ from dcim.graphql.mixins import CabledObjectMixin
|
||||
from extras.graphql.mixins import ContactsMixin, CustomFieldsMixin, TagsMixin
|
||||
from netbox.graphql.types import BaseObjectType, ObjectType, OrganizationalObjectType, PrimaryObjectType
|
||||
from tenancy.graphql.types import TenantType
|
||||
|
||||
from .filters import *
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
import ipam.fields
|
||||
from utilities.json import CustomFieldJSONEncoder
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import taggit.managers
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# Generated by Django 4.2.5 on 2023-10-20 21:25
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
import utilities.fields
|
||||
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import django.db.models.deletion
|
||||
import taggit.managers
|
||||
import utilities.json
|
||||
from django.db import migrations, models
|
||||
|
||||
import utilities.json
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
|
||||
@@ -8,10 +8,16 @@ from django.utils.translation import gettext_lazy as _
|
||||
from circuits.choices import *
|
||||
from dcim.models import CabledObjectModel
|
||||
from netbox.models import ChangeLoggedModel, OrganizationalModel, PrimaryModel
|
||||
from netbox.models.mixins import DistanceMixin
|
||||
from netbox.models.features import (
|
||||
ContactsMixin, CustomFieldsMixin, CustomLinksMixin, ExportTemplatesMixin, ImageAttachmentsMixin, TagsMixin,
|
||||
ContactsMixin,
|
||||
CustomFieldsMixin,
|
||||
CustomLinksMixin,
|
||||
ExportTemplatesMixin,
|
||||
ImageAttachmentsMixin,
|
||||
TagsMixin,
|
||||
)
|
||||
from netbox.models.mixins import DistanceMixin
|
||||
|
||||
from .base import BaseCircuitType
|
||||
|
||||
__all__ = (
|
||||
|
||||
@@ -9,6 +9,7 @@ from django.utils.translation import gettext_lazy as _
|
||||
from circuits.choices import *
|
||||
from netbox.models import ChangeLoggedModel, PrimaryModel
|
||||
from netbox.models.features import CustomFieldsMixin, CustomLinksMixin, ExportTemplatesMixin, TagsMixin
|
||||
|
||||
from .base import BaseCircuitType
|
||||
|
||||
__all__ = (
|
||||
@@ -184,6 +185,8 @@ class VirtualCircuitTermination(
|
||||
return self.virtual_circuit.terminations.filter(
|
||||
role=VirtualCircuitTerminationRoleChoices.ROLE_HUB
|
||||
)
|
||||
# Fallback for unexpected roles
|
||||
return self.virtual_circuit.terminations.none()
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from netbox.search import SearchIndex, register_search
|
||||
|
||||
from . import models
|
||||
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ from django.db.models.signals import post_delete, post_save
|
||||
from django.dispatch import receiver
|
||||
|
||||
from dcim.signals import rebuild_paths
|
||||
|
||||
from .models import CircuitTermination
|
||||
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ from django.utils.translation import gettext_lazy as _
|
||||
from circuits.models import *
|
||||
from netbox.tables import NetBoxTable, OrganizationalModelTable, PrimaryModelTable, columns
|
||||
from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
|
||||
|
||||
from .columns import CommitRateColumn
|
||||
|
||||
__all__ = (
|
||||
|
||||
@@ -5,7 +5,16 @@ from circuits.filtersets import *
|
||||
from circuits.models import *
|
||||
from dcim.choices import InterfaceTypeChoices, LocationStatusChoices
|
||||
from dcim.models import (
|
||||
Cable, Device, DeviceRole, DeviceType, Interface, Location, Manufacturer, Region, Site, SiteGroup
|
||||
Cable,
|
||||
Device,
|
||||
DeviceRole,
|
||||
DeviceType,
|
||||
Interface,
|
||||
Location,
|
||||
Manufacturer,
|
||||
Region,
|
||||
Site,
|
||||
SiteGroup,
|
||||
)
|
||||
from ipam.models import ASN, RIR
|
||||
from netbox.choices import DistanceUnitChoices
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from django.test import RequestFactory, tag, TestCase
|
||||
from django.test import RequestFactory, TestCase, tag
|
||||
|
||||
from circuits.models import CircuitTermination
|
||||
from circuits.tables import CircuitTerminationTable
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from django.urls import include, path
|
||||
|
||||
from utilities.urls import get_model_urls
|
||||
|
||||
from . import views
|
||||
|
||||
app_name = 'circuits'
|
||||
|
||||
@@ -5,14 +5,15 @@ from netbox.object_actions import AddObject, BulkDelete, BulkEdit, BulkExport, B
|
||||
from netbox.views import generic
|
||||
from utilities.query import count_related
|
||||
from utilities.views import GetRelatedModelsMixin, register_model_view
|
||||
|
||||
from . import filtersets, forms, tables
|
||||
from .models import *
|
||||
|
||||
|
||||
#
|
||||
# Providers
|
||||
#
|
||||
|
||||
|
||||
@register_model_view(Provider, 'list', path='', detail=False)
|
||||
class ProviderListView(generic.ObjectListView):
|
||||
queryset = Provider.objects.annotate(
|
||||
|
||||
@@ -2,10 +2,14 @@ import re
|
||||
import typing
|
||||
from collections import OrderedDict
|
||||
|
||||
from drf_spectacular.extensions import OpenApiSerializerFieldExtension, OpenApiSerializerExtension, _SchemaType
|
||||
from drf_spectacular.extensions import OpenApiSerializerExtension, OpenApiSerializerFieldExtension, _SchemaType
|
||||
from drf_spectacular.openapi import AutoSchema
|
||||
from drf_spectacular.plumbing import (
|
||||
build_basic_type, build_choice_field, build_media_type_object, build_object_type, get_doc,
|
||||
build_basic_type,
|
||||
build_choice_field,
|
||||
build_media_type_object,
|
||||
build_object_type,
|
||||
get_doc,
|
||||
)
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import Direction
|
||||
@@ -35,7 +39,7 @@ class ChoiceFieldFix(OpenApiSerializerFieldExtension):
|
||||
if direction == 'request':
|
||||
return build_cf
|
||||
|
||||
elif direction == "response":
|
||||
if direction == "response":
|
||||
value = build_cf
|
||||
label = {
|
||||
**build_basic_type(OpenApiTypes.STR),
|
||||
@@ -49,6 +53,10 @@ class ChoiceFieldFix(OpenApiSerializerFieldExtension):
|
||||
}
|
||||
)
|
||||
|
||||
# TODO: This function should never implicitly/explicitly return `None`
|
||||
# The fallback should be well-defined (drf-spectacular expects request/response naming).
|
||||
return None
|
||||
|
||||
|
||||
def viewset_handles_bulk_create(view):
|
||||
"""Check if view automatically provides list-based bulk create"""
|
||||
@@ -71,8 +79,7 @@ class NetBoxAutoSchema(AutoSchema):
|
||||
def is_bulk_action(self):
|
||||
if hasattr(self.view, "action") and self.view.action in BULK_ACTIONS:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
return False
|
||||
|
||||
def get_operation_id(self):
|
||||
"""
|
||||
@@ -312,8 +319,7 @@ class FixSerializedPKRelatedField(OpenApiSerializerFieldExtension):
|
||||
if direction == "response":
|
||||
component = auto_schema.resolve_serializer(self.target.serializer, direction)
|
||||
return component.ref if component else None
|
||||
else:
|
||||
return build_basic_type(OpenApiTypes.INT)
|
||||
return build_basic_type(OpenApiTypes.INT)
|
||||
|
||||
|
||||
class FixIntegerRangeSerializerSchema(OpenApiSerializerExtension):
|
||||
|
||||
@@ -34,14 +34,14 @@ class ObjectTypeSerializer(BaseModelSerializer):
|
||||
@extend_schema_field(OpenApiTypes.STR)
|
||||
def get_rest_api_endpoint(self, obj):
|
||||
if not (model := obj.model_class()):
|
||||
return
|
||||
return None
|
||||
try:
|
||||
return get_action_url(model, action='list', rest_api=True)
|
||||
except NoReverseMatch:
|
||||
return
|
||||
return None
|
||||
|
||||
@extend_schema_field(OpenApiTypes.STR)
|
||||
def get_description(self, obj):
|
||||
if not (model := obj.model_class()):
|
||||
return
|
||||
return None
|
||||
return inspect.getdoc(model)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from netbox.api.routers import NetBoxRouter
|
||||
|
||||
from . import views
|
||||
|
||||
app_name = 'core-api'
|
||||
|
||||
@@ -23,6 +23,7 @@ from netbox.api.metadata import ContentTypeMetadata
|
||||
from netbox.api.pagination import LimitOffsetListPagination
|
||||
from netbox.api.viewsets import NetBoxModelViewSet, NetBoxReadOnlyModelViewSet
|
||||
from utilities.api import IsSuperuser
|
||||
|
||||
from . import serializers
|
||||
|
||||
|
||||
@@ -284,5 +285,4 @@ class BackgroundTaskViewSet(BaseRQViewSet):
|
||||
stopped_jobs = stop_rq_job(id)
|
||||
if len(stopped_jobs) == 1:
|
||||
return HttpResponse(status=200)
|
||||
else:
|
||||
return HttpResponse(status=204)
|
||||
return HttpResponse(status=204)
|
||||
|
||||
@@ -6,7 +6,7 @@ from django.db.migrations.operations import AlterModelOptions
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from core.events import *
|
||||
from netbox.events import EventType, EVENT_TYPE_KIND_DANGER, EVENT_TYPE_KIND_SUCCESS, EVENT_TYPE_KIND_WARNING
|
||||
from netbox.events import EVENT_TYPE_KIND_DANGER, EVENT_TYPE_KIND_SUCCESS, EVENT_TYPE_KIND_WARNING, EventType
|
||||
from utilities.migration import custom_deconstruct
|
||||
|
||||
# Ignore verbose_name & verbose_name_plural Meta options when calculating model migrations
|
||||
@@ -23,9 +23,10 @@ class CoreConfig(AppConfig):
|
||||
def ready(self):
|
||||
from core.api import schema # noqa: F401
|
||||
from core.checks import check_duplicate_indexes # noqa: F401
|
||||
from netbox.models.features import register_models
|
||||
from . import data_backends, events, search # noqa: F401
|
||||
from netbox import context_managers # noqa: F401
|
||||
from netbox.models.features import register_models
|
||||
|
||||
from . import data_backends, events, search # noqa: F401
|
||||
|
||||
# Register models
|
||||
register_models(*self.get_models())
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from django.core.checks import Error, register, Tags
|
||||
from django.db.models import Index, UniqueConstraint
|
||||
from django.apps import apps
|
||||
from django.core.checks import Error, Tags, register
|
||||
from django.db.models import Index, UniqueConstraint
|
||||
|
||||
__all__ = (
|
||||
'check_duplicate_indexes',
|
||||
|
||||
@@ -2,11 +2,11 @@ from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from utilities.choices import ChoiceSet
|
||||
|
||||
|
||||
#
|
||||
# Data sources
|
||||
#
|
||||
|
||||
|
||||
class DataSourceStatusChoices(ChoiceSet):
|
||||
NEW = 'new'
|
||||
QUEUED = 'queued'
|
||||
|
||||
@@ -15,6 +15,7 @@ from netbox.utils import register_data_backend
|
||||
from utilities.constants import HTTP_PROXY_SUPPORTED_SCHEMAS, HTTP_PROXY_SUPPORTED_SOCK_SCHEMAS
|
||||
from utilities.proxy import resolve_proxies
|
||||
from utilities.socks import ProxyPoolManager
|
||||
|
||||
from .exceptions import SyncError
|
||||
|
||||
__all__ = (
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import logging
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
@@ -6,8 +6,9 @@ from django.utils.translation import gettext as _
|
||||
from netbox.filtersets import BaseFilterSet, ChangeLoggedModelFilterSet, PrimaryModelFilterSet
|
||||
from netbox.utils import get_data_backend_choices
|
||||
from users.models import User
|
||||
from utilities.filters import ContentTypeFilter
|
||||
from utilities.filters import MultiValueContentTypeFilter
|
||||
from utilities.filtersets import register_filterset
|
||||
|
||||
from .choices import *
|
||||
from .models import *
|
||||
|
||||
@@ -25,14 +26,17 @@ __all__ = (
|
||||
class DataSourceFilterSet(PrimaryModelFilterSet):
|
||||
type = django_filters.MultipleChoiceFilter(
|
||||
choices=get_data_backend_choices,
|
||||
distinct=False,
|
||||
null_value=None
|
||||
)
|
||||
status = django_filters.MultipleChoiceFilter(
|
||||
choices=DataSourceStatusChoices,
|
||||
distinct=False,
|
||||
null_value=None
|
||||
)
|
||||
sync_interval = django_filters.MultipleChoiceFilter(
|
||||
choices=JobIntervalChoices,
|
||||
distinct=False,
|
||||
null_value=None
|
||||
)
|
||||
|
||||
@@ -57,11 +61,13 @@ class DataFileFilterSet(ChangeLoggedModelFilterSet):
|
||||
)
|
||||
source_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=DataSource.objects.all(),
|
||||
distinct=False,
|
||||
label=_('Data source (ID)'),
|
||||
)
|
||||
source = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='source__name',
|
||||
queryset=DataSource.objects.all(),
|
||||
distinct=False,
|
||||
to_field_name='name',
|
||||
label=_('Data source (name)'),
|
||||
)
|
||||
@@ -86,9 +92,10 @@ class JobFilterSet(BaseFilterSet):
|
||||
)
|
||||
object_type_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=ObjectType.objects.with_feature('jobs'),
|
||||
distinct=False,
|
||||
field_name='object_type_id',
|
||||
)
|
||||
object_type = ContentTypeFilter()
|
||||
object_type = MultiValueContentTypeFilter()
|
||||
created = django_filters.DateTimeFilter()
|
||||
created__before = django_filters.DateTimeFilter(
|
||||
field_name='created',
|
||||
@@ -127,6 +134,7 @@ class JobFilterSet(BaseFilterSet):
|
||||
)
|
||||
status = django_filters.MultipleChoiceFilter(
|
||||
choices=JobStatusChoices,
|
||||
distinct=False,
|
||||
null_value=None
|
||||
)
|
||||
queue_name = django_filters.CharFilter()
|
||||
@@ -180,18 +188,21 @@ class ObjectChangeFilterSet(BaseFilterSet):
|
||||
label=_('Search'),
|
||||
)
|
||||
time = django_filters.DateTimeFromToRangeFilter()
|
||||
changed_object_type = ContentTypeFilter()
|
||||
changed_object_type = MultiValueContentTypeFilter()
|
||||
changed_object_type_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=ContentType.objects.all()
|
||||
queryset=ContentType.objects.all(),
|
||||
distinct=False,
|
||||
)
|
||||
related_object_type = ContentTypeFilter()
|
||||
related_object_type = MultiValueContentTypeFilter()
|
||||
user_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=User.objects.all(),
|
||||
distinct=False,
|
||||
label=_('User (ID)'),
|
||||
)
|
||||
user = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='user__username',
|
||||
queryset=User.objects.all(),
|
||||
distinct=False,
|
||||
to_field_name='username',
|
||||
label=_('User name'),
|
||||
)
|
||||
|
||||
@@ -9,7 +9,10 @@ from netbox.utils import get_data_backend_choices
|
||||
from users.models import User
|
||||
from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm, add_blank_choice
|
||||
from utilities.forms.fields import (
|
||||
ContentTypeChoiceField, ContentTypeMultipleChoiceField, DynamicModelMultipleChoiceField, TagFilterField,
|
||||
ContentTypeChoiceField,
|
||||
ContentTypeMultipleChoiceField,
|
||||
DynamicModelMultipleChoiceField,
|
||||
TagFilterField,
|
||||
)
|
||||
from utilities.forms.rendering import FieldSet
|
||||
from utilities.forms.widgets import DateTimePicker
|
||||
|
||||
@@ -8,7 +8,7 @@ from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from core.forms.mixins import SyncedDataMixin
|
||||
from core.models import *
|
||||
from netbox.config import get_config, PARAMS
|
||||
from netbox.config import PARAMS, get_config
|
||||
from netbox.forms import NetBoxModelForm, PrimaryModelForm
|
||||
from netbox.registry import registry
|
||||
from netbox.utils import get_data_backend_choices
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import Annotated, TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Annotated
|
||||
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from datetime import datetime
|
||||
from typing import Annotated, TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Annotated
|
||||
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
@@ -9,6 +9,7 @@ from strawberry_django import BaseFilterLookup, DatetimeFilterLookup, FilterLook
|
||||
|
||||
from core import models
|
||||
from netbox.graphql.filters import BaseModelFilter, PrimaryModelFilter
|
||||
|
||||
from .enums import *
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from typing import Annotated, List, TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Annotated, List
|
||||
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
|
||||
@@ -6,6 +6,7 @@ from django.contrib.contenttypes.models import ContentType as DjangoContentType
|
||||
|
||||
from core import models
|
||||
from netbox.graphql.types import BaseObjectType, PrimaryObjectType
|
||||
|
||||
from .filters import *
|
||||
|
||||
__all__ = (
|
||||
|
||||
@@ -13,6 +13,7 @@ from netbox.config import Config
|
||||
from netbox.jobs import JobRunner, system_job
|
||||
from netbox.search.backends import search_backend
|
||||
from utilities.proxy import resolve_proxies
|
||||
|
||||
from .choices import DataSourceStatusChoices, JobIntervalChoices
|
||||
from .models import DataSource
|
||||
|
||||
|
||||
@@ -144,7 +144,7 @@ class Command(BaseCommand):
|
||||
# If Python code has been passed, execute it and exit.
|
||||
if options['command']:
|
||||
exec(options['command'], namespace)
|
||||
return
|
||||
return None
|
||||
|
||||
# Try to enable tab-complete
|
||||
try:
|
||||
|
||||
@@ -4,7 +4,6 @@ from django_rq.management.commands.rqworker import Command as _Command
|
||||
|
||||
from netbox.registry import registry
|
||||
|
||||
|
||||
DEFAULT_QUEUES = ('high', 'default', 'low')
|
||||
|
||||
logger = logging.getLogger('netbox.rqworker')
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import core.models.object_types
|
||||
from django.db import migrations
|
||||
|
||||
import core.models.object_types
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from .object_types import *
|
||||
from .object_types import * # isort: split
|
||||
|
||||
from .change_logging import *
|
||||
from .config import *
|
||||
from .data import *
|
||||
|
||||
@@ -10,8 +10,7 @@ from mptt.models import MPTTModel
|
||||
|
||||
from core.choices import ObjectChangeActionChoices
|
||||
from core.querysets import ObjectChangeQuerySet
|
||||
from netbox.models.features import ChangeLoggingMixin
|
||||
from netbox.models.features import has_feature
|
||||
from netbox.models.features import ChangeLoggingMixin, has_feature
|
||||
from utilities.data import shallow_compare_dict
|
||||
|
||||
__all__ = (
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
from django.core.cache import cache
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext, gettext_lazy as _
|
||||
from django.utils.translation import gettext
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ from netbox.models import PrimaryModel
|
||||
from netbox.models.features import JobsMixin
|
||||
from netbox.registry import registry
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
|
||||
from ..choices import *
|
||||
from ..exceptions import SyncError
|
||||
|
||||
@@ -97,6 +98,7 @@ class DataSource(JobsMixin, PrimaryModel):
|
||||
def get_type_display(self):
|
||||
if backend := registry['data_backends'].get(self.type):
|
||||
return backend.label
|
||||
return None
|
||||
|
||||
def get_status_color(self):
|
||||
return DataSourceStatusChoices.colors.get(self.status)
|
||||
|
||||
@@ -4,15 +4,16 @@ from functools import cached_property
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.core.files.storage import storages
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from ..choices import ManagedFileRootPathChoices
|
||||
from extras.storage import ScriptFileSystemStorage
|
||||
from netbox.models.features import SyncedDataMixin
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
|
||||
from ..choices import ManagedFileRootPathChoices
|
||||
|
||||
__all__ = (
|
||||
'ManagedFile',
|
||||
)
|
||||
@@ -78,8 +79,7 @@ class ManagedFile(SyncedDataMixin, models.Model):
|
||||
'scripts': settings.SCRIPTS_ROOT,
|
||||
'reports': settings.REPORTS_ROOT,
|
||||
}[self.file_root]
|
||||
else:
|
||||
return ""
|
||||
return ""
|
||||
|
||||
def sync_data(self):
|
||||
if self.data_file:
|
||||
@@ -89,6 +89,7 @@ class ManagedFile(SyncedDataMixin, models.Model):
|
||||
|
||||
with storage.open(self.full_path, 'wb+') as new_file:
|
||||
new_file.write(self.data_file.data)
|
||||
sync_data.alters_data = True
|
||||
|
||||
@cached_property
|
||||
def storage(self):
|
||||
|
||||
@@ -146,7 +146,7 @@ class Job(models.Model):
|
||||
if self.object_type:
|
||||
if self.object_type.model == 'reportmodule':
|
||||
return reverse('extras:report_result', kwargs={'job_pk': self.pk})
|
||||
elif self.object_type.model == 'scriptmodule':
|
||||
if self.object_type.model == 'scriptmodule':
|
||||
return reverse('extras:script_result', kwargs={'job_pk': self.pk})
|
||||
return reverse('core:job', args=[self.pk])
|
||||
|
||||
@@ -216,6 +216,7 @@ class Job(models.Model):
|
||||
|
||||
# Send signal
|
||||
job_start.send(self)
|
||||
start.alters_data = True
|
||||
|
||||
def terminate(self, status=JobStatusChoices.STATUS_COMPLETED, error=None):
|
||||
"""
|
||||
@@ -245,6 +246,7 @@ class Job(models.Model):
|
||||
|
||||
# Send signal
|
||||
job_end.send(self)
|
||||
terminate.alters_data = True
|
||||
|
||||
def log(self, record: logging.LogRecord):
|
||||
"""
|
||||
|
||||
@@ -218,19 +218,22 @@ class ObjectType(ContentType):
|
||||
def app_verbose_name(self):
|
||||
if model := self.model_class():
|
||||
return model._meta.app_config.verbose_name
|
||||
return None
|
||||
|
||||
@property
|
||||
def model_verbose_name(self):
|
||||
if model := self.model_class():
|
||||
return model._meta.verbose_name
|
||||
return None
|
||||
|
||||
@property
|
||||
def model_verbose_name_plural(self):
|
||||
if model := self.model_class():
|
||||
return model._meta.verbose_name_plural
|
||||
return None
|
||||
|
||||
@property
|
||||
def is_plugin_model(self):
|
||||
if not (model := self.model_class()):
|
||||
return # Return null if model class is invalid
|
||||
return None # Return null if model class is invalid
|
||||
return isinstance(model._meta.app_config, PluginConfig)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from netbox.search import SearchIndex, register_search
|
||||
|
||||
from . import models
|
||||
|
||||
|
||||
|
||||
@@ -3,11 +3,11 @@ from threading import local
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||
from django.core.signals import request_finished
|
||||
from django.db.models import CASCADE, RESTRICT
|
||||
from django.db.models.fields.reverse_related import ManyToManyRel, ManyToOneRel
|
||||
from django.db.models.signals import m2m_changed, post_migrate, post_save, pre_delete
|
||||
from django.dispatch import receiver, Signal
|
||||
from django.core.signals import request_finished
|
||||
from django.dispatch import Signal, receiver
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_prometheus.models import model_deletes, model_inserts, model_updates
|
||||
|
||||
@@ -18,10 +18,11 @@ from extras.events import enqueue_event
|
||||
from extras.models import Tag
|
||||
from extras.utils import run_validators
|
||||
from netbox.config import get_config
|
||||
from utilities.data import get_config_value_ci
|
||||
from netbox.context import current_request, events_queue
|
||||
from netbox.models.features import ChangeLoggingMixin, get_model_features, model_is_public
|
||||
from utilities.data import get_config_value_ci
|
||||
from utilities.exceptions import AbortRequest
|
||||
|
||||
from .models import ConfigRevision, DataSource, ObjectChange
|
||||
|
||||
__all__ = (
|
||||
@@ -209,22 +210,28 @@ def handle_deleted_object(sender, instance, **kwargs):
|
||||
# for the forward direction of the relationship, ensuring that the change is recorded.
|
||||
# Similarly, for many-to-one relationships, we set the value on the related object to None
|
||||
# and save it to trigger a change record on that object.
|
||||
for relation in instance._meta.related_objects:
|
||||
if type(relation) not in [ManyToManyRel, ManyToOneRel]:
|
||||
continue
|
||||
related_model = relation.related_model
|
||||
related_field_name = relation.remote_field.name
|
||||
if not issubclass(related_model, ChangeLoggingMixin):
|
||||
# We only care about triggering the m2m_changed signal for models which support
|
||||
# change logging
|
||||
continue
|
||||
for obj in related_model.objects.filter(**{related_field_name: instance.pk}):
|
||||
obj.snapshot() # Ensure the change record includes the "before" state
|
||||
if type(relation) is ManyToManyRel:
|
||||
getattr(obj, related_field_name).remove(instance)
|
||||
elif type(relation) is ManyToOneRel and relation.null and relation.on_delete not in (CASCADE, RESTRICT):
|
||||
setattr(obj, related_field_name, None)
|
||||
obj.save()
|
||||
#
|
||||
# Skip this for private models (e.g. CablePath) whose lifecycle is an internal
|
||||
# implementation detail. Django's on_delete handlers (e.g. SET_NULL) already take
|
||||
# care of the database integrity; recording changelog entries for the related
|
||||
# objects would be spurious. (Ref: #21390)
|
||||
if not getattr(instance, '_netbox_private', False):
|
||||
for relation in instance._meta.related_objects:
|
||||
if type(relation) not in [ManyToManyRel, ManyToOneRel]:
|
||||
continue
|
||||
related_model = relation.related_model
|
||||
related_field_name = relation.remote_field.name
|
||||
if not issubclass(related_model, ChangeLoggingMixin):
|
||||
# We only care about triggering the m2m_changed signal for models which support
|
||||
# change logging
|
||||
continue
|
||||
for obj in related_model.objects.filter(**{related_field_name: instance.pk}):
|
||||
obj.snapshot() # Ensure the change record includes the "before" state
|
||||
if type(relation) is ManyToManyRel:
|
||||
getattr(obj, related_field_name).remove(instance)
|
||||
elif type(relation) is ManyToOneRel and relation.null and relation.on_delete not in (CASCADE, RESTRICT):
|
||||
setattr(obj, related_field_name, None)
|
||||
obj.save()
|
||||
|
||||
# Enqueue the object for event processing
|
||||
queue = events_queue.get()
|
||||
|
||||
@@ -2,5 +2,5 @@ from .change_logging import *
|
||||
from .config import *
|
||||
from .data import *
|
||||
from .jobs import *
|
||||
from .tasks import *
|
||||
from .plugins import *
|
||||
from .tasks import *
|
||||
|
||||
@@ -3,6 +3,7 @@ from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from core.models import ObjectChange
|
||||
from netbox.tables import NetBoxTable, columns
|
||||
|
||||
from .template_code import *
|
||||
|
||||
__all__ = (
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
import django_tables2 as tables
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from core.models import *
|
||||
from netbox.tables import NetBoxTable, PrimaryModelTable, columns
|
||||
|
||||
from .columns import BackendTypeColumn
|
||||
from .template_code import DATA_SOURCE_SYNC_BUTTON
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import django_tables2 as tables
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from netbox.tables import BaseTable, NetBoxTable, columns
|
||||
from core.constants import JOB_LOG_ENTRY_LEVELS
|
||||
from core.models import Job
|
||||
from core.tables.columns import BadgeColumn
|
||||
from netbox.tables import BaseTable, NetBoxTable, columns
|
||||
|
||||
|
||||
class JobTable(NetBoxTable):
|
||||
|
||||
@@ -2,6 +2,7 @@ import django_tables2 as tables
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from netbox.tables import BaseTable, columns
|
||||
|
||||
from .template_code import PLUGIN_IS_INSTALLED, PLUGIN_NAME_TEMPLATE
|
||||
|
||||
__all__ = (
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import uuid
|
||||
|
||||
from django_rq import get_queue
|
||||
from django_rq.workers import get_worker
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from rq.job import Job as RQ_Job, JobStatus
|
||||
from django_rq import get_queue
|
||||
from django_rq.workers import get_worker
|
||||
from rest_framework import status
|
||||
from rq.job import Job as RQ_Job
|
||||
from rq.job import JobStatus
|
||||
from rq.registry import FailedJobRegistry, StartedJobRegistry
|
||||
|
||||
from rest_framework import status
|
||||
from users.constants import TOKEN_PREFIX
|
||||
from users.models import Token, User
|
||||
from utilities.testing import APITestCase, APIViewTestCases, TestCase
|
||||
from utilities.testing.utils import disable_logging
|
||||
|
||||
from ..models import *
|
||||
|
||||
|
||||
|
||||
@@ -7,7 +7,16 @@ from core.choices import ObjectChangeActionChoices
|
||||
from core.models import ObjectChange, ObjectType
|
||||
from dcim.choices import InterfaceTypeChoices, ModuleStatusChoices, SiteStatusChoices
|
||||
from dcim.models import (
|
||||
Cable, CableTermination, Device, DeviceRole, DeviceType, Manufacturer, Module, ModuleBay, ModuleType, Interface,
|
||||
Cable,
|
||||
CableTermination,
|
||||
Device,
|
||||
DeviceRole,
|
||||
DeviceType,
|
||||
Interface,
|
||||
Manufacturer,
|
||||
Module,
|
||||
ModuleBay,
|
||||
ModuleType,
|
||||
Site,
|
||||
)
|
||||
from extras.choices import *
|
||||
|
||||
@@ -8,6 +8,7 @@ from dcim.models import Site
|
||||
from ipam.models import IPAddress
|
||||
from users.models import User
|
||||
from utilities.testing import BaseFilterSetTests, ChangeLoggedFilterSetTests
|
||||
|
||||
from ..choices import *
|
||||
from ..filtersets import *
|
||||
from ..models import *
|
||||
@@ -237,9 +238,9 @@ class ObjectChangeTestCase(TestCase, BaseFilterSetTests):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
def test_changed_object_type(self):
|
||||
params = {'changed_object_type': 'dcim.site'}
|
||||
params = {'changed_object_type': ['dcim.site']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
params = {'changed_object_type_id': [ContentType.objects.get(app_label='dcim', model='site').pk]}
|
||||
params = {'changed_object_type_id': [ContentType.objects.get_by_natural_key('dcim', 'site').pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
from unittest.mock import patch, MagicMock
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.test import TestCase
|
||||
|
||||
from core.models import DataSource, Job, ObjectType
|
||||
from core.choices import ObjectChangeActionChoices
|
||||
from dcim.models import Site, Location, Device
|
||||
from core.models import DataSource, Job, ObjectType
|
||||
from dcim.models import Device, Location, Site
|
||||
from netbox.constants import CENSOR_TOKEN, CENSOR_TOKEN_CHANGED
|
||||
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ Unit tests for OpenAPI schema generation.
|
||||
Refs: #20638
|
||||
"""
|
||||
import json
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
|
||||
|
||||
@@ -8,7 +8,8 @@ from django.utils import timezone
|
||||
from django_rq import get_queue
|
||||
from django_rq.settings import QUEUES_MAP
|
||||
from django_rq.workers import get_worker
|
||||
from rq.job import Job as RQ_Job, JobStatus
|
||||
from rq.job import Job as RQ_Job
|
||||
from rq.job import JobStatus
|
||||
from rq.registry import DeferredJobRegistry, FailedJobRegistry, FinishedJobRegistry, StartedJobRegistry
|
||||
|
||||
from core.choices import ObjectChangeActionChoices
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from django.urls import include, path
|
||||
|
||||
from utilities.urls import get_model_urls
|
||||
|
||||
from . import views
|
||||
|
||||
app_name = 'core'
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
from django.http import Http404
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_rq.queues import get_queue, get_queue_by_index, get_redis_connection
|
||||
from django_rq.settings import QUEUES_MAP, QUEUES_LIST
|
||||
from django_rq.settings import QUEUES_LIST, QUEUES_MAP
|
||||
from django_rq.utils import get_jobs, stop_jobs
|
||||
from rq import requeue_job
|
||||
from rq.exceptions import NoSuchJobError
|
||||
from rq.job import Job as RQ_Job, JobStatus as RQJobStatus
|
||||
from rq.job import Job as RQ_Job
|
||||
from rq.job import JobStatus as RQJobStatus
|
||||
from rq.registry import (
|
||||
DeferredJobRegistry,
|
||||
FailedJobRegistry,
|
||||
|
||||
@@ -1,27 +1,29 @@
|
||||
import json
|
||||
import platform
|
||||
from copy import deepcopy
|
||||
|
||||
from django import __version__ as django_version
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.mixins import UserPassesTestMixin
|
||||
from django.core.cache import cache
|
||||
from django.db import connection, ProgrammingError
|
||||
from django.http import HttpResponse, HttpResponseForbidden, Http404
|
||||
from django.db import ProgrammingError, connection
|
||||
from django.http import Http404, HttpResponse, HttpResponseForbidden
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.generic import View
|
||||
from django_rq.queues import get_connection, get_queue_by_index, get_redis_connection
|
||||
from django_rq.settings import QUEUES_MAP, QUEUES_LIST
|
||||
from django_rq.settings import QUEUES_LIST, QUEUES_MAP
|
||||
from django_rq.utils import get_statistics
|
||||
from rq.exceptions import NoSuchJobError
|
||||
from rq.job import Job as RQ_Job, JobStatus as RQJobStatus
|
||||
from rq.job import Job as RQ_Job
|
||||
from rq.job import JobStatus as RQJobStatus
|
||||
from rq.worker import Worker
|
||||
from rq.worker_registration import clean_worker_registry
|
||||
|
||||
from core.utils import delete_rq_job, enqueue_rq_job, get_rq_jobs_from_status, requeue_rq_job, stop_rq_job
|
||||
from netbox.config import get_config, PARAMS
|
||||
from netbox.config import PARAMS, get_config
|
||||
from netbox.object_actions import AddObject, BulkDelete, BulkExport, DeleteObject
|
||||
from netbox.plugins.utils import get_installed_plugins
|
||||
from netbox.views import generic
|
||||
@@ -40,17 +42,18 @@ from utilities.views import (
|
||||
ViewTab,
|
||||
register_model_view,
|
||||
)
|
||||
|
||||
from . import filtersets, forms, tables
|
||||
from .jobs import SyncDataSourceJob
|
||||
from .models import *
|
||||
from .plugins import get_catalog_plugins, get_local_plugins
|
||||
from .tables import CatalogPluginTable, JobLogEntryTable, PluginVersionTable
|
||||
|
||||
|
||||
#
|
||||
# Data sources
|
||||
#
|
||||
|
||||
|
||||
@register_model_view(DataSource, 'list', path='', detail=False)
|
||||
class DataSourceListView(generic.ObjectListView):
|
||||
queryset = DataSource.objects.annotate(
|
||||
@@ -310,6 +313,22 @@ class ConfigRevisionListView(generic.ObjectListView):
|
||||
class ConfigRevisionView(generic.ObjectView):
|
||||
queryset = ConfigRevision.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
"""
|
||||
Retrieve additional context for a given request and instance.
|
||||
"""
|
||||
# Copy the revision data to avoid modifying the original
|
||||
config = deepcopy(instance.data or {})
|
||||
|
||||
# Serialize any JSON-based classes
|
||||
for attr in ['CUSTOM_VALIDATORS', 'DEFAULT_USER_PREFERENCES', 'PROTECTION_RULES']:
|
||||
if attr in config:
|
||||
config[attr] = json.dumps(config[attr], cls=ConfigJSONEncoder, indent=4)
|
||||
|
||||
return {
|
||||
'config': config,
|
||||
}
|
||||
|
||||
|
||||
@register_model_view(ConfigRevision, 'add', detail=False)
|
||||
class ConfigRevisionEditView(generic.ObjectEditView):
|
||||
@@ -617,8 +636,8 @@ class SystemView(UserPassesTestMixin, View):
|
||||
response['Content-Disposition'] = 'attachment; filename="netbox.json"'
|
||||
return response
|
||||
|
||||
# Serialize any CustomValidator classes
|
||||
for attr in ['CUSTOM_VALIDATORS', 'PROTECTION_RULES']:
|
||||
# Serialize any JSON-based classes
|
||||
for attr in ['CUSTOM_VALIDATORS', 'DEFAULT_USER_PREFERENCES', 'PROTECTION_RULES']:
|
||||
if hasattr(config, attr) and getattr(config, attr, None):
|
||||
setattr(config, attr, json.dumps(getattr(config, attr), cls=ConfigJSONEncoder, indent=4))
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
from .serializers_.cables import *
|
||||
from .serializers_.sites import *
|
||||
from .serializers_.racks import *
|
||||
from .serializers_.device_components import *
|
||||
from .serializers_.devices import *
|
||||
from .serializers_.devicetype_components import *
|
||||
from .serializers_.devicetypes import *
|
||||
from .serializers_.manufacturers import *
|
||||
from .serializers_.platforms import *
|
||||
from .serializers_.roles import *
|
||||
from .serializers_.devicetypes import *
|
||||
from .serializers_.devicetype_components import *
|
||||
from .serializers_.virtualchassis import *
|
||||
from .serializers_.devices import *
|
||||
from .serializers_.device_components import *
|
||||
from .serializers_.power import *
|
||||
from .serializers_.racks import *
|
||||
from .serializers_.rackunits import *
|
||||
from .serializers_.roles import *
|
||||
from .serializers_.sites import *
|
||||
from .serializers_.virtualchassis import *
|
||||
|
||||
@@ -23,6 +23,7 @@ class ConnectedEndpointsSerializer(serializers.ModelSerializer):
|
||||
def get_connected_endpoints_type(self, obj):
|
||||
if endpoints := obj.connected_endpoints:
|
||||
return f'{endpoints[0]._meta.app_label}.{endpoints[0]._meta.model_name}'
|
||||
return None
|
||||
|
||||
@extend_schema_field(serializers.ListField(allow_null=True))
|
||||
def get_connected_endpoints(self, obj):
|
||||
@@ -33,6 +34,7 @@ class ConnectedEndpointsSerializer(serializers.ModelSerializer):
|
||||
serializer = get_serializer_for_model(endpoints[0])
|
||||
context = {'request': self.context['request']}
|
||||
return serializer(endpoints, nested=True, many=True, context=context).data
|
||||
return None
|
||||
|
||||
@extend_schema_field(serializers.BooleanField)
|
||||
def get_connected_endpoints_reachable(self, obj):
|
||||
|
||||
@@ -7,7 +7,10 @@ from dcim.models import Cable, CablePath, CableTermination
|
||||
from netbox.api.fields import ChoiceField, ContentTypeField
|
||||
from netbox.api.gfk_fields import GFKSerializerField
|
||||
from netbox.api.serializers import (
|
||||
BaseModelSerializer, GenericObjectSerializer, NetBoxModelSerializer, PrimaryModelSerializer,
|
||||
BaseModelSerializer,
|
||||
GenericObjectSerializer,
|
||||
NetBoxModelSerializer,
|
||||
PrimaryModelSerializer,
|
||||
)
|
||||
from tenancy.api.serializers_.tenants import TenantSerializer
|
||||
from utilities.api import get_serializer_for_model
|
||||
|
||||
@@ -5,8 +5,18 @@ from rest_framework import serializers
|
||||
from dcim.choices import *
|
||||
from dcim.constants import *
|
||||
from dcim.models import (
|
||||
ConsolePort, ConsoleServerPort, DeviceBay, FrontPort, Interface, InventoryItem, ModuleBay, PortMapping,
|
||||
PowerOutlet, PowerPort, RearPort, VirtualDeviceContext,
|
||||
ConsolePort,
|
||||
ConsoleServerPort,
|
||||
DeviceBay,
|
||||
FrontPort,
|
||||
Interface,
|
||||
InventoryItem,
|
||||
ModuleBay,
|
||||
PortMapping,
|
||||
PowerOutlet,
|
||||
PowerPort,
|
||||
RearPort,
|
||||
VirtualDeviceContext,
|
||||
)
|
||||
from ipam.api.serializers_.vlans import VLANSerializer, VLANTranslationPolicySerializer
|
||||
from ipam.api.serializers_.vrfs import VRFSerializer
|
||||
@@ -20,6 +30,7 @@ from wireless.api.serializers_.nested import NestedWirelessLinkSerializer
|
||||
from wireless.api.serializers_.wirelesslans import WirelessLANSerializer
|
||||
from wireless.choices import *
|
||||
from wireless.models import WirelessLAN
|
||||
|
||||
from .base import ConnectedEndpointsSerializer, PortSerializer
|
||||
from .cables import CabledObjectSerializer
|
||||
from .devices import DeviceSerializer, MACAddressSerializer, ModuleSerializer, VirtualDeviceContextSerializer
|
||||
|
||||
@@ -15,6 +15,7 @@ from netbox.api.gfk_fields import GFKSerializerField
|
||||
from netbox.api.serializers import PrimaryModelSerializer
|
||||
from tenancy.api.serializers_.tenants import TenantSerializer
|
||||
from virtualization.api.serializers_.clusters import ClusterSerializer
|
||||
|
||||
from .devicetypes import *
|
||||
from .nested import NestedDeviceBaySerializer, NestedDeviceSerializer, NestedModuleBaySerializer
|
||||
from .platforms import PlatformSerializer
|
||||
|
||||
@@ -4,14 +4,23 @@ from rest_framework import serializers
|
||||
from dcim.choices import *
|
||||
from dcim.constants import *
|
||||
from dcim.models import (
|
||||
ConsolePortTemplate, ConsoleServerPortTemplate, DeviceBayTemplate, FrontPortTemplate, InterfaceTemplate,
|
||||
InventoryItemTemplate, ModuleBayTemplate, PortTemplateMapping, PowerOutletTemplate, PowerPortTemplate,
|
||||
ConsolePortTemplate,
|
||||
ConsoleServerPortTemplate,
|
||||
DeviceBayTemplate,
|
||||
FrontPortTemplate,
|
||||
InterfaceTemplate,
|
||||
InventoryItemTemplate,
|
||||
ModuleBayTemplate,
|
||||
PortTemplateMapping,
|
||||
PowerOutletTemplate,
|
||||
PowerPortTemplate,
|
||||
RearPortTemplate,
|
||||
)
|
||||
from netbox.api.fields import ChoiceField, ContentTypeField
|
||||
from netbox.api.gfk_fields import GFKSerializerField
|
||||
from netbox.api.serializers import ChangeLogMessageSerializer, ValidatedModelSerializer
|
||||
from wireless.choices import *
|
||||
|
||||
from .base import PortSerializer
|
||||
from .devicetypes import DeviceTypeSerializer, ModuleTypeSerializer
|
||||
from .manufacturers import ManufacturerSerializer
|
||||
|
||||
@@ -8,6 +8,7 @@ from dcim.models import DeviceType, ModuleType, ModuleTypeProfile
|
||||
from netbox.api.fields import AttributesField, ChoiceField
|
||||
from netbox.api.serializers import PrimaryModelSerializer
|
||||
from netbox.choices import *
|
||||
|
||||
from .manufacturers import ManufacturerSerializer
|
||||
from .platforms import PlatformSerializer
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
from drf_spectacular.utils import extend_schema_serializer
|
||||
from rest_framework import serializers
|
||||
|
||||
from netbox.api.serializers import WritableNestedSerializer
|
||||
from dcim import models
|
||||
from netbox.api.serializers import WritableNestedSerializer
|
||||
|
||||
__all__ = (
|
||||
'NestedDeviceBaySerializer',
|
||||
|
||||
@@ -3,6 +3,7 @@ from rest_framework import serializers
|
||||
from dcim.models import Platform
|
||||
from extras.api.serializers_.configtemplates import ConfigTemplateSerializer
|
||||
from netbox.api.serializers import NestedGroupModelSerializer
|
||||
|
||||
from .manufacturers import ManufacturerSerializer
|
||||
from .nested import NestedPlatformSerializer
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ from dcim.models import PowerFeed, PowerPanel
|
||||
from netbox.api.fields import ChoiceField, RelatedObjectCountField
|
||||
from netbox.api.serializers import PrimaryModelSerializer
|
||||
from tenancy.api.serializers_.tenants import TenantSerializer
|
||||
|
||||
from .base import ConnectedEndpointsSerializer
|
||||
from .cables import CabledObjectSerializer
|
||||
from .racks import RackSerializer
|
||||
|
||||
@@ -10,6 +10,7 @@ from netbox.choices import *
|
||||
from netbox.config import ConfigItem
|
||||
from tenancy.api.serializers_.tenants import TenantSerializer
|
||||
from users.api.serializers_.users import UserSerializer
|
||||
|
||||
from .manufacturers import ManufacturerSerializer
|
||||
from .sites import LocationSerializer, SiteSerializer
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ from rest_framework import serializers
|
||||
|
||||
from dcim.choices import *
|
||||
from netbox.api.fields import ChoiceField
|
||||
|
||||
from .devices import DeviceSerializer
|
||||
|
||||
__all__ = (
|
||||
|
||||
@@ -4,6 +4,7 @@ from dcim.models import DeviceRole, InventoryItemRole
|
||||
from extras.api.serializers_.configtemplates import ConfigTemplateSerializer
|
||||
from netbox.api.fields import RelatedObjectCountField
|
||||
from netbox.api.serializers import NestedGroupModelSerializer, OrganizationalModelSerializer
|
||||
|
||||
from .nested import NestedDeviceRoleSerializer
|
||||
|
||||
__all__ = (
|
||||
|
||||
@@ -8,6 +8,7 @@ from ipam.models import ASN
|
||||
from netbox.api.fields import ChoiceField, RelatedObjectCountField, SerializedPKRelatedField
|
||||
from netbox.api.serializers import NestedGroupModelSerializer, PrimaryModelSerializer
|
||||
from tenancy.api.serializers_.tenants import TenantSerializer
|
||||
|
||||
from .nested import NestedLocationSerializer, NestedRegionSerializer, NestedSiteGroupSerializer
|
||||
|
||||
__all__ = (
|
||||
|
||||
@@ -2,6 +2,7 @@ from rest_framework import serializers
|
||||
|
||||
from dcim.models import VirtualChassis
|
||||
from netbox.api.serializers import PrimaryModelSerializer
|
||||
|
||||
from .nested import NestedDeviceSerializer
|
||||
|
||||
__all__ = (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from netbox.api.routers import NetBoxRouter
|
||||
from . import views
|
||||
|
||||
from . import views
|
||||
|
||||
router = NetBoxRouter()
|
||||
router.APIRootView = views.DCIMRootView
|
||||
|
||||
@@ -2,7 +2,7 @@ from django.contrib.contenttypes.prefetch import GenericPrefetch
|
||||
from django.http import Http404, HttpResponse
|
||||
from django.shortcuts import get_object_or_404
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import extend_schema, OpenApiParameter
|
||||
from drf_spectacular.utils import OpenApiParameter, extend_schema
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.routers import APIRootView
|
||||
@@ -16,11 +16,12 @@ from extras.api.mixins import ConfigContextQuerySetMixin, RenderConfigMixin
|
||||
from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
|
||||
from netbox.api.metadata import ContentTypeMetadata
|
||||
from netbox.api.pagination import StripCountAnnotationsPaginator
|
||||
from netbox.api.viewsets import NetBoxModelViewSet, MPTTLockedMixin, NetBoxReadOnlyModelViewSet
|
||||
from netbox.api.viewsets import MPTTLockedMixin, NetBoxModelViewSet, NetBoxReadOnlyModelViewSet
|
||||
from netbox.api.viewsets.mixins import SequentialBulkCreatesMixin
|
||||
from utilities.api import get_serializer_for_model
|
||||
from utilities.query_functions import CollateAsChar
|
||||
from virtualization.models import VirtualMachine
|
||||
|
||||
from . import serializers
|
||||
from .exceptions import MissingFilterException
|
||||
|
||||
@@ -221,24 +222,26 @@ class RackViewSet(NetBoxModelViewSet):
|
||||
)
|
||||
return HttpResponse(drawing.tostring(), content_type='image/svg+xml')
|
||||
|
||||
else:
|
||||
# Return a JSON representation of the rack units in the elevation
|
||||
elevation = rack.get_rack_units(
|
||||
face=data['face'],
|
||||
user=request.user,
|
||||
exclude=data['exclude'],
|
||||
expand_devices=data['expand_devices']
|
||||
)
|
||||
# Return a JSON representation of the rack units in the elevation
|
||||
elevation = rack.get_rack_units(
|
||||
face=data['face'],
|
||||
user=request.user,
|
||||
exclude=data['exclude'],
|
||||
expand_devices=data['expand_devices']
|
||||
)
|
||||
|
||||
# Enable filtering rack units by ID
|
||||
if q := data['q']:
|
||||
q = q.lower()
|
||||
elevation = [u for u in elevation if q in str(u['id']) or q in str(u['name']).lower()]
|
||||
# Enable filtering rack units by ID
|
||||
if q := data['q']:
|
||||
q = q.lower()
|
||||
elevation = [u for u in elevation if q in str(u['id']) or q in str(u['name']).lower()]
|
||||
|
||||
page = self.paginate_queryset(elevation)
|
||||
if page is not None:
|
||||
rack_units = serializers.RackUnitSerializer(page, many=True, context={'request': request})
|
||||
return self.get_paginated_response(rack_units.data)
|
||||
page = self.paginate_queryset(elevation)
|
||||
if page is not None:
|
||||
rack_units = serializers.RackUnitSerializer(page, many=True, context={'request': request})
|
||||
return self.get_paginated_response(rack_units.data)
|
||||
|
||||
# TODO: This endpoint should always return an HttpResponse/DRF Response; `None` is not a meaningful result.
|
||||
return None
|
||||
|
||||
|
||||
#
|
||||
|
||||
@@ -10,7 +10,8 @@ class DCIMConfig(AppConfig):
|
||||
def ready(self):
|
||||
from netbox.models.features import register_models
|
||||
from utilities.counters import connect_counters
|
||||
from . import signals, search # noqa: F401
|
||||
|
||||
from . import search, signals # noqa: F401
|
||||
from .models import CableTermination, Device, DeviceType, ModuleType, RackType, VirtualChassis
|
||||
|
||||
# Register models
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import django_filters
|
||||
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from netbox.filtersets import BaseFilterSet
|
||||
from utilities.filters import ContentTypeFilter, TreeNodeMultipleChoiceFilter
|
||||
from utilities.filters import MultiValueContentTypeFilter, TreeNodeMultipleChoiceFilter
|
||||
|
||||
from .models import *
|
||||
|
||||
__all__ = (
|
||||
@@ -14,7 +15,7 @@ class ScopedFilterSet(BaseFilterSet):
|
||||
"""
|
||||
Provides additional filtering functionality for location, site, etc.. for Scoped models.
|
||||
"""
|
||||
scope_type = ContentTypeFilter()
|
||||
scope_type = MultiValueContentTypeFilter()
|
||||
region_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='_region',
|
||||
@@ -43,12 +44,14 @@ class ScopedFilterSet(BaseFilterSet):
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Site.objects.all(),
|
||||
distinct=False,
|
||||
field_name='_site',
|
||||
label=_('Site (ID)'),
|
||||
)
|
||||
site = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='_site__slug',
|
||||
queryset=Site.objects.all(),
|
||||
distinct=False,
|
||||
to_field_name='slug',
|
||||
label=_('Site (slug)'),
|
||||
)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user