mirror of
https://github.com/netbox-community/netbox.git
synced 2025-08-13 02:58:17 -06:00
Merge branch 'develop' into 15496-circuit-termination
This commit is contained in:
commit
6b8a6daeee
3
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
3
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@ -34,10 +34,9 @@ body:
|
|||||||
label: Python Version
|
label: Python Version
|
||||||
description: What version of Python are you currently running?
|
description: What version of Python are you currently running?
|
||||||
options:
|
options:
|
||||||
- "3.8"
|
|
||||||
- "3.9"
|
|
||||||
- "3.10"
|
- "3.10"
|
||||||
- "3.11"
|
- "3.11"
|
||||||
|
- "3.12"
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
|
@ -11,8 +11,24 @@ master = true
|
|||||||
; clear environment on exit
|
; clear environment on exit
|
||||||
vacuum = true
|
vacuum = true
|
||||||
|
|
||||||
|
; make SIGTERM stop the app (instead of reload)
|
||||||
|
die-on-term = true
|
||||||
|
|
||||||
; exit if no app can be loaded
|
; exit if no app can be loaded
|
||||||
need-app = true
|
need-app = true
|
||||||
|
|
||||||
; do not use multiple interpreters
|
; do not use multiple interpreters
|
||||||
single-interpreter = true
|
single-interpreter = true
|
||||||
|
|
||||||
|
; change to the project directory
|
||||||
|
chdir = netbox
|
||||||
|
|
||||||
|
; specify the WSGI module to load
|
||||||
|
module = netbox.wsgi
|
||||||
|
|
||||||
|
; workaround to make uWSGI reloads work with pyuwsgi (not to be used if using uwsgi package instead)
|
||||||
|
binary-path = venv/bin/python
|
||||||
|
|
||||||
|
; only log internal messages and errors (reverse proxy already logs the requests)
|
||||||
|
disable-logging = true
|
||||||
|
log-5xx = true
|
||||||
|
@ -77,7 +77,7 @@ Create the following for each model:
|
|||||||
|
|
||||||
## 13. GraphQL API components
|
## 13. GraphQL API components
|
||||||
|
|
||||||
Create a Graphene object type for the model in `graphql/types.py` by subclassing the appropriate class from `netbox.graphql.types`.
|
Create a GraphQL object type for the model in `graphql/types.py` by subclassing the appropriate class from `netbox.graphql.types`.
|
||||||
|
|
||||||
Also extend the schema class defined in `graphql/schema.py` with the individual object and object list fields per the established convention.
|
Also extend the schema class defined in `graphql/schema.py` with the individual object and object list fields per the established convention.
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@ pip3 install pyuwsgi
|
|||||||
Once installed, add the package to `local_requirements.txt` to ensure it is re-installed during future rebuilds of the virtual environment:
|
Once installed, add the package to `local_requirements.txt` to ensure it is re-installed during future rebuilds of the virtual environment:
|
||||||
|
|
||||||
```no-highlight
|
```no-highlight
|
||||||
sudo sh -c "echo 'pyuwgsi' >> /opt/netbox/local_requirements.txt"
|
sudo sh -c "echo 'pyuwsgi' >> /opt/netbox/local_requirements.txt"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# GraphQL API Overview
|
# GraphQL API Overview
|
||||||
|
|
||||||
NetBox provides a read-only [GraphQL](https://graphql.org/) API to complement its REST API. This API is powered by the [Graphene](https://graphene-python.org/) library and [Graphene-Django](https://docs.graphene-python.org/projects/django/en/latest/).
|
NetBox provides a read-only [GraphQL](https://graphql.org/) API to complement its REST API. This API is powered by [Strawberry Django](https://strawberry-graphql.github.io/strawberry-django/).
|
||||||
|
|
||||||
## Queries
|
## Queries
|
||||||
|
|
||||||
@ -47,7 +47,7 @@ NetBox provides both a singular and plural query field for each object type:
|
|||||||
|
|
||||||
For example, query `device(id:123)` to fetch a specific device (identified by its unique ID), and query `device_list` (with an optional set of filters) to fetch all devices.
|
For example, query `device(id:123)` to fetch a specific device (identified by its unique ID), and query `device_list` (with an optional set of filters) to fetch all devices.
|
||||||
|
|
||||||
For more detail on constructing GraphQL queries, see the [Graphene documentation](https://docs.graphene-python.org/en/latest/) as well as the [GraphQL queries documentation](https://graphql.org/learn/queries/).
|
For more detail on constructing GraphQL queries, see the [GraphQL queries documentation](https://graphql.org/learn/queries/). For filtering and lookup syntax, please refer to the [Strawberry Django documentation](https://strawberry-graphql.github.io/strawberry-django/guide/filters/).
|
||||||
|
|
||||||
## Filtering
|
## Filtering
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## Defining the Schema Class
|
## Defining the Schema Class
|
||||||
|
|
||||||
A plugin can extend NetBox's GraphQL API by registering its own schema class. By default, NetBox will attempt to import `graphql.schema` from the plugin, if it exists. This path can be overridden by defining `graphql_schema` on the PluginConfig instance as the dotted path to the desired Python class. This class must be a subclass of `graphene.ObjectType`.
|
A plugin can extend NetBox's GraphQL API by registering its own schema class. By default, NetBox will attempt to import `graphql.schema` from the plugin, if it exists. This path can be overridden by defining `graphql_schema` on the PluginConfig instance as the dotted path to the desired Python class.
|
||||||
|
|
||||||
### Example
|
### Example
|
||||||
|
|
||||||
|
@ -48,7 +48,7 @@ class CircuitCircuitTerminationSerializer(WritableNestedSerializer):
|
|||||||
class CircuitSerializer(NetBoxModelSerializer):
|
class CircuitSerializer(NetBoxModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuit-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuit-detail')
|
||||||
provider = ProviderSerializer(nested=True)
|
provider = ProviderSerializer(nested=True)
|
||||||
provider_account = ProviderAccountSerializer(nested=True, required=False, allow_null=True)
|
provider_account = ProviderAccountSerializer(nested=True, required=False, allow_null=True, default=None)
|
||||||
status = ChoiceField(choices=CircuitStatusChoices, required=False)
|
status = ChoiceField(choices=CircuitStatusChoices, required=False)
|
||||||
type = CircuitTypeSerializer(nested=True)
|
type = CircuitTypeSerializer(nested=True)
|
||||||
tenant = TenantSerializer(nested=True, required=False, allow_null=True)
|
tenant = TenantSerializer(nested=True, required=False, allow_null=True)
|
||||||
|
@ -45,6 +45,7 @@ class ProviderSerializer(NetBoxModelSerializer):
|
|||||||
class ProviderAccountSerializer(NetBoxModelSerializer):
|
class ProviderAccountSerializer(NetBoxModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provideraccount-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provideraccount-detail')
|
||||||
provider = ProviderSerializer(nested=True)
|
provider = ProviderSerializer(nested=True)
|
||||||
|
name = serializers.CharField(allow_blank=True, max_length=100, required=False, default='')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ProviderAccount
|
model = ProviderAccount
|
||||||
|
@ -141,7 +141,7 @@ class CircuitTest(APIViewTestCases.APIViewTestCase):
|
|||||||
{
|
{
|
||||||
'cid': 'Circuit 6',
|
'cid': 'Circuit 6',
|
||||||
'provider': providers[1].pk,
|
'provider': providers[1].pk,
|
||||||
'provider_account': provider_accounts[1].pk,
|
# Omit provider account to test uniqueness constraint
|
||||||
'type': circuit_types[1].pk,
|
'type': circuit_types[1].pk,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@ -237,7 +237,7 @@ class ProviderAccountTest(APIViewTestCases.APIViewTestCase):
|
|||||||
'account': '5678',
|
'account': '5678',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'Provider Account 6',
|
# Omit name to test uniqueness constraint
|
||||||
'provider': providers[0].pk,
|
'provider': providers[0].pk,
|
||||||
'account': '6789',
|
'account': '6789',
|
||||||
},
|
},
|
||||||
|
@ -122,6 +122,7 @@ class DeviceWithConfigContextSerializer(DeviceSerializer):
|
|||||||
class VirtualDeviceContextSerializer(NetBoxModelSerializer):
|
class VirtualDeviceContextSerializer(NetBoxModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualdevicecontext-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualdevicecontext-detail')
|
||||||
device = DeviceSerializer(nested=True)
|
device = DeviceSerializer(nested=True)
|
||||||
|
identifier = serializers.IntegerField(allow_null=True, max_value=32767, min_value=0, required=False, default=None)
|
||||||
tenant = TenantSerializer(nested=True, required=False, allow_null=True, default=None)
|
tenant = TenantSerializer(nested=True, required=False, allow_null=True, default=None)
|
||||||
primary_ip = IPAddressSerializer(nested=True, read_only=True, allow_null=True)
|
primary_ip = IPAddressSerializer(nested=True, read_only=True, allow_null=True)
|
||||||
primary_ip4 = IPAddressSerializer(nested=True, required=False, allow_null=True)
|
primary_ip4 = IPAddressSerializer(nested=True, required=False, allow_null=True)
|
||||||
|
@ -51,7 +51,7 @@ class SiteSerializer(NetBoxModelSerializer):
|
|||||||
status = ChoiceField(choices=SiteStatusChoices, required=False)
|
status = ChoiceField(choices=SiteStatusChoices, required=False)
|
||||||
region = RegionSerializer(nested=True, required=False, allow_null=True)
|
region = RegionSerializer(nested=True, required=False, allow_null=True)
|
||||||
group = SiteGroupSerializer(nested=True, required=False, allow_null=True)
|
group = SiteGroupSerializer(nested=True, required=False, allow_null=True)
|
||||||
tenant = TenantSerializer(required=False, allow_null=True)
|
tenant = TenantSerializer(nested=True, required=False, allow_null=True)
|
||||||
time_zone = TimeZoneSerializerField(required=False, allow_null=True)
|
time_zone = TimeZoneSerializerField(required=False, allow_null=True)
|
||||||
asns = SerializedPKRelatedField(
|
asns = SerializedPKRelatedField(
|
||||||
queryset=ASN.objects.all(),
|
queryset=ASN.objects.all(),
|
||||||
@ -83,7 +83,7 @@ class SiteSerializer(NetBoxModelSerializer):
|
|||||||
class LocationSerializer(NestedGroupModelSerializer):
|
class LocationSerializer(NestedGroupModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:location-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:location-detail')
|
||||||
site = SiteSerializer(nested=True)
|
site = SiteSerializer(nested=True)
|
||||||
parent = NestedLocationSerializer(required=False, allow_null=True)
|
parent = NestedLocationSerializer(required=False, allow_null=True, default=None)
|
||||||
status = ChoiceField(choices=LocationStatusChoices, required=False)
|
status = ChoiceField(choices=LocationStatusChoices, required=False)
|
||||||
tenant = TenantSerializer(nested=True, required=False, allow_null=True)
|
tenant = TenantSerializer(nested=True, required=False, allow_null=True)
|
||||||
rack_count = serializers.IntegerField(read_only=True)
|
rack_count = serializers.IntegerField(read_only=True)
|
||||||
|
@ -10,6 +10,7 @@ from dcim.models import *
|
|||||||
from extras.models import ConfigTemplate
|
from extras.models import ConfigTemplate
|
||||||
from ipam.models import ASN, RIR, VLAN, VRF
|
from ipam.models import ASN, RIR, VLAN, VRF
|
||||||
from netbox.api.serializers import GenericObjectSerializer
|
from netbox.api.serializers import GenericObjectSerializer
|
||||||
|
from tenancy.models import Tenant
|
||||||
from utilities.testing import APITestCase, APIViewTestCases, create_test_device
|
from utilities.testing import APITestCase, APIViewTestCases, create_test_device
|
||||||
from virtualization.models import Cluster, ClusterType
|
from virtualization.models import Cluster, ClusterType
|
||||||
from wireless.choices import WirelessChannelChoices
|
from wireless.choices import WirelessChannelChoices
|
||||||
@ -152,6 +153,7 @@ class SiteTest(APIViewTestCases.APIViewTestCase):
|
|||||||
Site.objects.bulk_create(sites)
|
Site.objects.bulk_create(sites)
|
||||||
|
|
||||||
rir = RIR.objects.create(name='RFC 6996', is_private=True)
|
rir = RIR.objects.create(name='RFC 6996', is_private=True)
|
||||||
|
tenant = Tenant.objects.create(name='Tenant 1', slug='tenant-1')
|
||||||
|
|
||||||
asns = [
|
asns = [
|
||||||
ASN(asn=65000 + i, rir=rir) for i in range(8)
|
ASN(asn=65000 + i, rir=rir) for i in range(8)
|
||||||
@ -166,6 +168,7 @@ class SiteTest(APIViewTestCases.APIViewTestCase):
|
|||||||
'group': groups[1].pk,
|
'group': groups[1].pk,
|
||||||
'status': SiteStatusChoices.STATUS_ACTIVE,
|
'status': SiteStatusChoices.STATUS_ACTIVE,
|
||||||
'asns': [asns[0].pk, asns[1].pk],
|
'asns': [asns[0].pk, asns[1].pk],
|
||||||
|
'tenant': tenant.pk,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'Site 5',
|
'name': 'Site 5',
|
||||||
@ -230,7 +233,7 @@ class LocationTest(APIViewTestCases.APIViewTestCase):
|
|||||||
'name': 'Test Location 6',
|
'name': 'Test Location 6',
|
||||||
'slug': 'test-location-6',
|
'slug': 'test-location-6',
|
||||||
'site': sites[1].pk,
|
'site': sites[1].pk,
|
||||||
'parent': parent_locations[1].pk,
|
# Omit parent to test uniqueness constraint
|
||||||
'status': LocationStatusChoices.STATUS_PLANNED,
|
'status': LocationStatusChoices.STATUS_PLANNED,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@ -2307,6 +2310,6 @@ class VirtualDeviceContextTest(APIViewTestCases.APIViewTestCase):
|
|||||||
'device': devices[1].pk,
|
'device': devices[1].pk,
|
||||||
'status': 'active',
|
'status': 'active',
|
||||||
'name': 'VDC 3',
|
'name': 'VDC 3',
|
||||||
'identifier': 3,
|
# Omit identifier to test uniqueness constraint
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
@ -85,6 +85,7 @@ class Command(BaseCommand):
|
|||||||
|
|
||||||
module_name, script_name = script.split('.', 1)
|
module_name, script_name = script.split('.', 1)
|
||||||
module, script = get_module_and_script(module_name, script_name)
|
module, script = get_module_and_script(module_name, script_name)
|
||||||
|
script = script.python_class
|
||||||
|
|
||||||
# Take user from command line if provided and exists, other
|
# Take user from command line if provided and exists, other
|
||||||
if options['user']:
|
if options['user']:
|
||||||
|
@ -60,7 +60,10 @@ def get_module_scripts(scriptmodule):
|
|||||||
return cls.full_name.split(".", maxsplit=1)[1]
|
return cls.full_name.split(".", maxsplit=1)[1]
|
||||||
|
|
||||||
loader = SourceFileLoader(get_python_name(scriptmodule), get_full_path(scriptmodule))
|
loader = SourceFileLoader(get_python_name(scriptmodule), get_full_path(scriptmodule))
|
||||||
|
try:
|
||||||
module = loader.load_module()
|
module = loader.load_module()
|
||||||
|
except FileNotFoundError:
|
||||||
|
return {}
|
||||||
|
|
||||||
scripts = {}
|
scripts = {}
|
||||||
ordered = getattr(module, 'script_order', [])
|
ordered = getattr(module, 'script_order', [])
|
||||||
|
@ -545,7 +545,7 @@ class ScriptResultsTable(BaseTable):
|
|||||||
template_code="""{% load log_levels %}{% log_level record.status %}""",
|
template_code="""{% load log_levels %}{% log_level record.status %}""",
|
||||||
verbose_name=_('Level')
|
verbose_name=_('Level')
|
||||||
)
|
)
|
||||||
message = tables.Column(
|
message = columns.MarkdownColumn(
|
||||||
verbose_name=_('Message')
|
verbose_name=_('Message')
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -566,22 +566,17 @@ class ReportResultsTable(BaseTable):
|
|||||||
time = tables.Column(
|
time = tables.Column(
|
||||||
verbose_name=_('Time')
|
verbose_name=_('Time')
|
||||||
)
|
)
|
||||||
status = tables.Column(
|
|
||||||
empty_values=(),
|
|
||||||
verbose_name=_('Level')
|
|
||||||
)
|
|
||||||
status = tables.TemplateColumn(
|
status = tables.TemplateColumn(
|
||||||
template_code="""{% load log_levels %}{% log_level record.status %}""",
|
template_code="""{% load log_levels %}{% log_level record.status %}""",
|
||||||
verbose_name=_('Level')
|
verbose_name=_('Level')
|
||||||
)
|
)
|
||||||
|
|
||||||
object = tables.Column(
|
object = tables.Column(
|
||||||
verbose_name=_('Object')
|
verbose_name=_('Object')
|
||||||
)
|
)
|
||||||
url = tables.Column(
|
url = tables.Column(
|
||||||
verbose_name=_('URL')
|
verbose_name=_('URL')
|
||||||
)
|
)
|
||||||
message = tables.Column(
|
message = columns.MarkdownColumn(
|
||||||
verbose_name=_('Message')
|
verbose_name=_('Message')
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -574,7 +574,7 @@ class IPRange(PrimaryModel):
|
|||||||
if not self.end_address > self.start_address:
|
if not self.end_address > self.start_address:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'end_address': _(
|
'end_address': _(
|
||||||
"Ending address must be lower than the starting address ({start_address})"
|
"Ending address must be greater than the starting address ({start_address})"
|
||||||
).format(start_address=self.start_address)
|
).format(start_address=self.start_address)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -781,6 +781,7 @@ class IPAddressView(generic.ObjectView):
|
|||||||
class IPAddressEditView(generic.ObjectEditView):
|
class IPAddressEditView(generic.ObjectEditView):
|
||||||
queryset = IPAddress.objects.all()
|
queryset = IPAddress.objects.all()
|
||||||
form = forms.IPAddressForm
|
form = forms.IPAddressForm
|
||||||
|
template_name = 'ipam/ipaddress_edit.html'
|
||||||
|
|
||||||
def alter_object(self, obj, request, url_args, url_kwargs):
|
def alter_object(self, obj, request, url_args, url_kwargs):
|
||||||
|
|
||||||
|
@ -32,6 +32,7 @@ class MenuItem:
|
|||||||
link: str
|
link: str
|
||||||
link_text: str
|
link_text: str
|
||||||
permissions: Optional[Sequence[str]] = ()
|
permissions: Optional[Sequence[str]] = ()
|
||||||
|
auth_required: Optional[bool] = False
|
||||||
staff_only: Optional[bool] = False
|
staff_only: Optional[bool] = False
|
||||||
buttons: Optional[Sequence[MenuItemButton]] = ()
|
buttons: Optional[Sequence[MenuItemButton]] = ()
|
||||||
|
|
||||||
|
@ -372,6 +372,7 @@ ADMIN_MENU = Menu(
|
|||||||
MenuItem(
|
MenuItem(
|
||||||
link=f'users:user_list',
|
link=f'users:user_list',
|
||||||
link_text=_('Users'),
|
link_text=_('Users'),
|
||||||
|
auth_required=True,
|
||||||
permissions=[f'auth.view_user'],
|
permissions=[f'auth.view_user'],
|
||||||
buttons=(
|
buttons=(
|
||||||
MenuItemButton(
|
MenuItemButton(
|
||||||
@ -391,6 +392,7 @@ ADMIN_MENU = Menu(
|
|||||||
MenuItem(
|
MenuItem(
|
||||||
link=f'users:group_list',
|
link=f'users:group_list',
|
||||||
link_text=_('Groups'),
|
link_text=_('Groups'),
|
||||||
|
auth_required=True,
|
||||||
permissions=[f'auth.view_group'],
|
permissions=[f'auth.view_group'],
|
||||||
buttons=(
|
buttons=(
|
||||||
MenuItemButton(
|
MenuItemButton(
|
||||||
@ -410,12 +412,14 @@ ADMIN_MENU = Menu(
|
|||||||
MenuItem(
|
MenuItem(
|
||||||
link=f'users:token_list',
|
link=f'users:token_list',
|
||||||
link_text=_('API Tokens'),
|
link_text=_('API Tokens'),
|
||||||
|
auth_required=True,
|
||||||
permissions=[f'users.view_token'],
|
permissions=[f'users.view_token'],
|
||||||
buttons=get_model_buttons('users', 'token')
|
buttons=get_model_buttons('users', 'token')
|
||||||
),
|
),
|
||||||
MenuItem(
|
MenuItem(
|
||||||
link=f'users:objectpermission_list',
|
link=f'users:objectpermission_list',
|
||||||
link_text=_('Permissions'),
|
link_text=_('Permissions'),
|
||||||
|
auth_required=True,
|
||||||
permissions=[f'users.view_objectpermission'],
|
permissions=[f'users.view_objectpermission'],
|
||||||
buttons=get_model_buttons('users', 'objectpermission', actions=['add'])
|
buttons=get_model_buttons('users', 'objectpermission', actions=['add'])
|
||||||
),
|
),
|
||||||
@ -426,16 +430,19 @@ ADMIN_MENU = Menu(
|
|||||||
items=(
|
items=(
|
||||||
MenuItem(
|
MenuItem(
|
||||||
link='core:system',
|
link='core:system',
|
||||||
link_text=_('System')
|
link_text=_('System'),
|
||||||
|
auth_required=True
|
||||||
),
|
),
|
||||||
MenuItem(
|
MenuItem(
|
||||||
link='core:configrevision_list',
|
link='core:configrevision_list',
|
||||||
link_text=_('Configuration History'),
|
link_text=_('Configuration History'),
|
||||||
|
auth_required=True,
|
||||||
permissions=['core.view_configrevision']
|
permissions=['core.view_configrevision']
|
||||||
),
|
),
|
||||||
MenuItem(
|
MenuItem(
|
||||||
link='core:background_queue_list',
|
link='core:background_queue_list',
|
||||||
link_text=_('Background Tasks')
|
link_text=_('Background Tasks'),
|
||||||
|
auth_required=True
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -372,7 +372,6 @@ if not DJANGO_ADMIN_ENABLED:
|
|||||||
# Middleware
|
# Middleware
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
"strawberry_django.middlewares.debug_toolbar.DebugToolbarMiddleware",
|
"strawberry_django.middlewares.debug_toolbar.DebugToolbarMiddleware",
|
||||||
'django_prometheus.middleware.PrometheusBeforeMiddleware',
|
|
||||||
'corsheaders.middleware.CorsMiddleware',
|
'corsheaders.middleware.CorsMiddleware',
|
||||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
'django.middleware.locale.LocaleMiddleware',
|
'django.middleware.locale.LocaleMiddleware',
|
||||||
@ -386,8 +385,14 @@ MIDDLEWARE = [
|
|||||||
'netbox.middleware.RemoteUserMiddleware',
|
'netbox.middleware.RemoteUserMiddleware',
|
||||||
'netbox.middleware.CoreMiddleware',
|
'netbox.middleware.CoreMiddleware',
|
||||||
'netbox.middleware.MaintenanceModeMiddleware',
|
'netbox.middleware.MaintenanceModeMiddleware',
|
||||||
'django_prometheus.middleware.PrometheusAfterMiddleware',
|
|
||||||
]
|
]
|
||||||
|
if METRICS_ENABLED:
|
||||||
|
# If metrics are enabled, add the before & after Prometheus middleware
|
||||||
|
MIDDLEWARE = [
|
||||||
|
'django_prometheus.middleware.PrometheusBeforeMiddleware',
|
||||||
|
*MIDDLEWARE,
|
||||||
|
'django_prometheus.middleware.PrometheusAfterMiddleware',
|
||||||
|
]
|
||||||
|
|
||||||
# URLs
|
# URLs
|
||||||
ROOT_URLCONF = 'netbox.urls'
|
ROOT_URLCONF = 'netbox.urls'
|
||||||
@ -522,7 +527,6 @@ if SENTRY_ENABLED:
|
|||||||
sentry_sdk.init(
|
sentry_sdk.init(
|
||||||
dsn=SENTRY_DSN,
|
dsn=SENTRY_DSN,
|
||||||
release=VERSION,
|
release=VERSION,
|
||||||
integrations=[sentry_sdk.integrations.django.DjangoIntegration()],
|
|
||||||
sample_rate=SENTRY_SAMPLE_RATE,
|
sample_rate=SENTRY_SAMPLE_RATE,
|
||||||
traces_sample_rate=SENTRY_TRACES_SAMPLE_RATE,
|
traces_sample_rate=SENTRY_TRACES_SAMPLE_RATE,
|
||||||
send_default_pii=True,
|
send_default_pii=True,
|
||||||
|
@ -52,7 +52,7 @@ class BaseTable(tables.Table):
|
|||||||
|
|
||||||
# Set default empty_text if none was provided
|
# Set default empty_text if none was provided
|
||||||
if self.empty_text is None:
|
if self.empty_text is None:
|
||||||
self.empty_text = f"No {self._meta.model._meta.verbose_name_plural} found"
|
self.empty_text = _("No {model_name} found").format(model_name=self._meta.model._meta.verbose_name_plural)
|
||||||
|
|
||||||
# Determine the table columns to display by checking the following:
|
# Determine the table columns to display by checking the following:
|
||||||
# 1. User's configuration for the table
|
# 1. User's configuration for the table
|
||||||
|
BIN
netbox/project-static/dist/netbox.js
vendored
BIN
netbox/project-static/dist/netbox.js
vendored
Binary file not shown.
BIN
netbox/project-static/dist/netbox.js.map
vendored
BIN
netbox/project-static/dist/netbox.js.map
vendored
Binary file not shown.
@ -10,9 +10,9 @@ function quickSearchEventHandler(event: Event): void {
|
|||||||
const clearbtn = document.getElementById("quicksearch_clear") as HTMLAnchorElement;
|
const clearbtn = document.getElementById("quicksearch_clear") as HTMLAnchorElement;
|
||||||
if (isTruthy(clearbtn)) {
|
if (isTruthy(clearbtn)) {
|
||||||
if (quicksearch.value === "") {
|
if (quicksearch.value === "") {
|
||||||
clearbtn.classList.add("d-none");
|
clearbtn.classList.add("invisible");
|
||||||
} else {
|
} else {
|
||||||
clearbtn.classList.remove("d-none");
|
clearbtn.classList.remove("invisible");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,7 @@
|
|||||||
{# Initialize color mode #}
|
{# Initialize color mode #}
|
||||||
<script
|
<script
|
||||||
type="text/javascript"
|
type="text/javascript"
|
||||||
src="{% static 'setmode.js' %}"
|
src="{% static 'setmode.js' %}?v={{ settings.VERSION }}"
|
||||||
onerror="window.location='{% url 'media_failure' %}?filename=setmode.js'">
|
onerror="window.location='{% url 'media_failure' %}?filename=setmode.js'">
|
||||||
</script>
|
</script>
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
|
@ -31,6 +31,10 @@
|
|||||||
<th scope="row">{% trans "NetBox version" %}</th>
|
<th scope="row">{% trans "NetBox version" %}</th>
|
||||||
<td>{{ stats.netbox_version }}</td>
|
<td>{{ stats.netbox_version }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans "Python version" %}</th>
|
||||||
|
<td>{{ stats.python_version }}</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">{% trans "Django version" %}</th>
|
<th scope="row">{% trans "Django version" %}</th>
|
||||||
<td>{{ stats.django_version }}</td>
|
<td>{{ stats.django_version }}</td>
|
||||||
|
@ -77,15 +77,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col col-md-7">
|
<div class="col col-md-7">
|
||||||
<div class="card">
|
|
||||||
<h5 class="card-header d-flex justify-content-between">
|
|
||||||
{% trans "Data" %}
|
|
||||||
{% include 'extras/inc/configcontext_format.html' %}
|
|
||||||
</h5>
|
|
||||||
<div class="card-body">
|
|
||||||
{% include 'inc/sync_warning.html' %}
|
{% include 'inc/sync_warning.html' %}
|
||||||
{% include 'extras/inc/configcontext_data.html' with data=object.data format=format %}
|
<div class="card">
|
||||||
</div>
|
{% include 'extras/inc/configcontext_data.html' with title="Data" data=object.data format=format copyid="data" %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,5 +1,17 @@
|
|||||||
{% load helpers %}
|
{% load helpers %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
<div class="rendered-context-data mt-1">
|
{% if title %}
|
||||||
<pre class="block">{% if format == 'json' %}{{ data|json }}{% elif format == 'yaml' %}{{ data|yaml }}{% else %}{{ data }}{% endif %}</pre>
|
<h5 class="card-header d-flex justify-content-between">
|
||||||
|
{% trans title %}
|
||||||
|
<div>
|
||||||
|
{% if copyid %}{% copy_content copyid %}{% endif %}
|
||||||
|
{% include 'extras/inc/format_toggle.html' %}
|
||||||
|
</div>
|
||||||
|
</h5>
|
||||||
|
{% endif %}
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="rendered-context-data mt-1">
|
||||||
|
<pre class="block" {% if copyid %}id="{{ copyid }}{% endif %}">{% if format == 'json' %}{{ data|json }}{% elif format == 'yaml' %}{{ data|yaml }}{% else %}{{ data }}{% endif %}</pre>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,6 +0,0 @@
|
|||||||
<div>
|
|
||||||
<div class="btn-group btn-group-sm" role="group">
|
|
||||||
<a href="?format=json" type="button" class="btn btn-outline-dark{% if format == 'json' %} active{% endif %}">JSON</a>
|
|
||||||
<a href="?format=yaml" type="button" class="btn btn-outline-dark{% if format == 'yaml' %} active{% endif %}">YAML</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
4
netbox/templates/extras/inc/format_toggle.html
Normal file
4
netbox/templates/extras/inc/format_toggle.html
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<div class="btn-group btn-group-sm" role="group">
|
||||||
|
<a href="?format=json" type="button" class="btn btn-outline-dark{% if format == 'json' %} active{% endif %}">JSON</a>
|
||||||
|
<a href="?format=yaml" type="button" class="btn btn-outline-dark{% if format == 'yaml' %} active{% endif %}">YAML</a>
|
||||||
|
</div>
|
@ -7,27 +7,12 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col col-md-6">
|
<div class="col col-md-6">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h5 class="card-header d-flex justify-content-between">
|
{% include 'extras/inc/configcontext_data.html' with title="Rendered Context" data=rendered_context format=format copyid="rendered_context" %}
|
||||||
{% trans "Rendered Context" %}
|
|
||||||
{% include 'extras/inc/configcontext_format.html' %}
|
|
||||||
</h5>
|
|
||||||
<div class="card-body">
|
|
||||||
{% include 'extras/inc/configcontext_data.html' with data=rendered_context format=format %}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col col-md-6">
|
<div class="col col-md-6">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h5 class="card-header">
|
{% include 'extras/inc/configcontext_data.html' with title="Local Context" data=object.local_context_data format=format copyid="local_context" %}
|
||||||
{% trans "Local Context" %}
|
|
||||||
</h5>
|
|
||||||
<div class="card-body">
|
|
||||||
{% if object.local_context_data %}
|
|
||||||
{% include 'extras/inc/configcontext_data.html' with data=object.local_context_data format=format %}
|
|
||||||
{% else %}
|
|
||||||
<span class="text-muted">{% trans "None" %}</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
<div class="card-footer">
|
<div class="card-footer">
|
||||||
<span class="help-block">
|
<span class="help-block">
|
||||||
<i class="mdi mdi-information-outline"></i>
|
<i class="mdi mdi-information-outline"></i>
|
||||||
@ -36,8 +21,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h5 class="card-header">
|
<h5 class="card-header d-flex justify-content-between">
|
||||||
{% trans "Source Contexts" %}
|
{% trans "Source Contexts" %}
|
||||||
|
<div>
|
||||||
|
{% include 'extras/inc/format_toggle.html' %}
|
||||||
|
</div>
|
||||||
</h5>
|
</h5>
|
||||||
{% for context in source_contexts %}
|
{% for context in source_contexts %}
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
<input type="search" results="5" name="q" id="quicksearch" class="form-control px-2 py-1" placeholder="Quick search"
|
<input type="search" results="5" name="q" id="quicksearch" class="form-control px-2 py-1" placeholder="Quick search"
|
||||||
hx-get="{{ request.full_path }}" hx-target="#object_list" hx-trigger="keyup changed delay:500ms, search" />
|
hx-get="{{ request.full_path }}" hx-target="#object_list" hx-trigger="keyup changed delay:500ms, search" />
|
||||||
<span class="input-group-text py-1">
|
<span class="input-group-text py-1">
|
||||||
<a href="#" id="quicksearch_clear" class="d-none text-secondary"><i class="mdi mdi-close-circle"></i></a>
|
<a href="#" id="quicksearch_clear" class="invisible text-secondary"><i class="mdi mdi-close-circle"></i></a>
|
||||||
</span>
|
</span>
|
||||||
{% block extra_table_controls %}{% endblock %}
|
{% block extra_table_controls %}{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
|
@ -3,28 +3,19 @@
|
|||||||
|
|
||||||
<ul class="nav nav-tabs">
|
<ul class="nav nav-tabs">
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a
|
<a href="{% url 'ipam:ipaddress_add' %}{% querystring request %}" class="nav-link {% if active_tab == 'add' %}active{% endif %}">
|
||||||
class="nav-link {% if active_tab == 'add' %}active{% endif %}"
|
{% if object.pk %}{% trans "Edit" %}{% else %}{% trans "Create" %}{% endif %}
|
||||||
href="{% url 'ipam:ipaddress_add' %}{% querystring request %}"
|
|
||||||
>
|
|
||||||
{% if obj.pk %}{% trans "Edit" %}{% else %}{% trans "Create" %}{% endif %}
|
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% if 'interface' in request.GET or 'vminterface' in request.GET %}
|
{% if 'interface' in request.GET or 'vminterface' in request.GET %}
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a
|
<a href="{% url 'ipam:ipaddress_assign' %}{% querystring request %}" class="nav-link {% if active_tab == 'assign' %}active{% endif %}">
|
||||||
class="nav-link {% if active_tab == 'assign' %}active{% endif %}"
|
|
||||||
href="{% url 'ipam:ipaddress_assign' %}{% querystring request %}"
|
|
||||||
>
|
|
||||||
{% trans "Assign IP" %}
|
{% trans "Assign IP" %}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% else %}
|
{% elif not object.pk %}
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a
|
<a href="{% url 'ipam:ipaddress_bulk_add' %}{% querystring request %}" class="nav-link {% if active_tab == 'bulk_add' %}active{% endif %}">
|
||||||
class="nav-link {% if active_tab == 'bulk_add' %}active{% endif %}"
|
|
||||||
href="{% url 'ipam:ipaddress_bulk_add' %}{% querystring request %}"
|
|
||||||
>
|
|
||||||
{% trans "Bulk Create" %}
|
{% trans "Bulk Create" %}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
@ -17,21 +17,17 @@
|
|||||||
{% for field in form.hidden_fields %}
|
{% for field in form.hidden_fields %}
|
||||||
{{ field }}
|
{{ field }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<div class="row mb-3">
|
<div class="field-group my-5">
|
||||||
<div class="col col-md-8 offset-md-2">
|
<div class="row">
|
||||||
<div class="field-group">
|
<h5 class="col-9 offset-3">{% trans "Select IP Address" %}</h5>
|
||||||
<h6>{% trans "Select IP Address" %}</h6>
|
</div>
|
||||||
{% render_field form.vrf_id %}
|
{% render_field form.vrf_id %}
|
||||||
{% render_field form.q %}
|
{% render_field form.q %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="text-end my-3">
|
||||||
</div>
|
|
||||||
<div class="row mb-3">
|
|
||||||
<div class="col col-md-8 offset-md-2 text-end">
|
|
||||||
<a href="{{ return_url }}" class="btn btn-outline-secondary">{% trans "Cancel" %}</a>
|
<a href="{{ return_url }}" class="btn btn-outline-secondary">{% trans "Cancel" %}</a>
|
||||||
<button type="submit" class="btn btn-primary">{% trans "Search" %}</button>
|
<button type="submit" class="btn btn-primary">{% trans "Search" %}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
{% if table %}
|
{% if table %}
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
|
5
netbox/templates/ipam/ipaddress_edit.html
Normal file
5
netbox/templates/ipam/ipaddress_edit.html
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{% extends 'generic/object_edit.html' %}
|
||||||
|
|
||||||
|
{% block tabs %}
|
||||||
|
{% include 'ipam/inc/ipaddress_edit_header.html' with active_tab='add' %}
|
||||||
|
{% endblock %}
|
@ -46,7 +46,21 @@
|
|||||||
<input type="hidden" name="next" value="{{ request.POST.next }}" />
|
<input type="hidden" name="next" value="{{ request.POST.next }}" />
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% render_form form %}
|
<div class="form-group mb-3">
|
||||||
|
<label for="id_username" class="form-label">{{ form.username.label }}</label>
|
||||||
|
{{ form.username }}
|
||||||
|
{% for error in form.username.errors %}
|
||||||
|
<div class="alert alert-danger">{{ error }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="id_password" class="form-label">{{ form.password.label }}</label>
|
||||||
|
{{ form.password }}
|
||||||
|
{% for error in form.password.errors %}
|
||||||
|
<div class="alert alert-danger">{{ error }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-footer">
|
<div class="form-footer">
|
||||||
<button type="submit" class="btn btn-primary w-100">
|
<button type="submit" class="btn btn-primary w-100">
|
||||||
|
@ -27,7 +27,7 @@ class TenantGroupSerializer(NestedGroupModelSerializer):
|
|||||||
|
|
||||||
class TenantSerializer(NetBoxModelSerializer):
|
class TenantSerializer(NetBoxModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:tenant-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:tenant-detail')
|
||||||
group = TenantGroupSerializer(nested=True, required=False, allow_null=True)
|
group = TenantGroupSerializer(nested=True, required=False, allow_null=True, default=None)
|
||||||
|
|
||||||
# Related object counts
|
# Related object counts
|
||||||
circuit_count = RelatedObjectCountField('circuits')
|
circuit_count = RelatedObjectCountField('circuits')
|
||||||
|
@ -26,6 +26,8 @@ def nav(context):
|
|||||||
for group in menu.groups:
|
for group in menu.groups:
|
||||||
items = []
|
items = []
|
||||||
for item in group.items:
|
for item in group.items:
|
||||||
|
if getattr(item, 'auth_required', False) and not user.is_authenticated:
|
||||||
|
continue
|
||||||
if not user.has_perms(item.permissions):
|
if not user.has_perms(item.permissions):
|
||||||
continue
|
continue
|
||||||
if item.staff_only and not user.is_staff:
|
if item.staff_only and not user.is_staff:
|
||||||
|
@ -31,11 +31,11 @@ __all__ = (
|
|||||||
class VirtualMachineSerializer(NetBoxModelSerializer):
|
class VirtualMachineSerializer(NetBoxModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:virtualmachine-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:virtualmachine-detail')
|
||||||
status = ChoiceField(choices=VirtualMachineStatusChoices, required=False)
|
status = ChoiceField(choices=VirtualMachineStatusChoices, required=False)
|
||||||
site = SiteSerializer(nested=True, required=False, allow_null=True)
|
site = SiteSerializer(nested=True, required=False, allow_null=True, default=None)
|
||||||
cluster = ClusterSerializer(nested=True, required=False, allow_null=True)
|
cluster = ClusterSerializer(nested=True, required=False, allow_null=True, default=None)
|
||||||
device = DeviceSerializer(nested=True, required=False, allow_null=True)
|
device = DeviceSerializer(nested=True, required=False, allow_null=True, default=None)
|
||||||
role = DeviceRoleSerializer(nested=True, required=False, allow_null=True)
|
role = DeviceRoleSerializer(nested=True, required=False, allow_null=True)
|
||||||
tenant = TenantSerializer(nested=True, required=False, allow_null=True)
|
tenant = TenantSerializer(nested=True, required=False, allow_null=True, default=None)
|
||||||
platform = PlatformSerializer(nested=True, required=False, allow_null=True)
|
platform = PlatformSerializer(nested=True, required=False, allow_null=True)
|
||||||
primary_ip = IPAddressSerializer(nested=True, read_only=True, allow_null=True)
|
primary_ip = IPAddressSerializer(nested=True, read_only=True, allow_null=True)
|
||||||
primary_ip4 = IPAddressSerializer(nested=True, required=False, allow_null=True)
|
primary_ip4 = IPAddressSerializer(nested=True, required=False, allow_null=True)
|
||||||
@ -55,7 +55,6 @@ class VirtualMachineSerializer(NetBoxModelSerializer):
|
|||||||
'interface_count', 'virtual_disk_count',
|
'interface_count', 'virtual_disk_count',
|
||||||
]
|
]
|
||||||
brief_fields = ('id', 'url', 'display', 'name', 'description')
|
brief_fields = ('id', 'url', 'display', 'name', 'description')
|
||||||
validators = []
|
|
||||||
|
|
||||||
|
|
||||||
class VirtualMachineWithConfigContextSerializer(VirtualMachineSerializer):
|
class VirtualMachineWithConfigContextSerializer(VirtualMachineSerializer):
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
Django==5.0.5
|
Django==5.0.6
|
||||||
django-cors-headers==4.3.1
|
django-cors-headers==4.3.1
|
||||||
django-debug-toolbar==4.3.0
|
django-debug-toolbar==4.3.0
|
||||||
django-filter==24.2
|
django-filter==24.2
|
||||||
|
Loading…
Reference in New Issue
Block a user