mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-20 02:06:42 -06:00
commit
a3f7dc0423
5
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
5
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@ -26,7 +26,7 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: NetBox Version
|
label: NetBox Version
|
||||||
description: What version of NetBox are you currently running?
|
description: What version of NetBox are you currently running?
|
||||||
placeholder: v4.0.0
|
placeholder: v4.0.1
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
@ -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
|
||||||
|
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
@ -14,7 +14,7 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: NetBox version
|
label: NetBox version
|
||||||
description: What version of NetBox are you currently running?
|
description: What version of NetBox are you currently running?
|
||||||
placeholder: v4.0.0
|
placeholder: v4.0.1
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
|
@ -353,6 +353,8 @@
|
|||||||
"800gbase-x-qsfpdd",
|
"800gbase-x-qsfpdd",
|
||||||
"800gbase-x-osfp",
|
"800gbase-x-osfp",
|
||||||
"1000base-kx",
|
"1000base-kx",
|
||||||
|
"2.5gbase-kx",
|
||||||
|
"5gbase-kr",
|
||||||
"10gbase-kr",
|
"10gbase-kr",
|
||||||
"10gbase-kx4",
|
"10gbase-kx4",
|
||||||
"25gbase-kr",
|
"25gbase-kr",
|
||||||
|
@ -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.
|
||||||
|
|
||||||
|
@ -72,7 +72,7 @@ In cases where upgrading a dependency to its most recent release is breaking, it
|
|||||||
|
|
||||||
### Update UI Dependencies
|
### Update UI Dependencies
|
||||||
|
|
||||||
Check whether any UI dependencies (JavaScript packages, fonts, etc.) need to be updated by running `yarn outdated` from within the `project-static/` directory. [Upgrade these dependencies](http://0.0.0.0:9000/development/web-ui/#updating-dependencies) as necessary, then run `yarn bundle` to generate the necessary files for distribution.
|
Check whether any UI dependencies (JavaScript packages, fonts, etc.) need to be updated by running `yarn outdated` from within the `project-static/` directory. [Upgrade these dependencies](./web-ui.md#updating-dependencies) as necessary, then run `yarn bundle` to generate the necessary files for distribution.
|
||||||
|
|
||||||
### Rebuild the Device Type Definition Schema
|
### Rebuild the Device Type Definition Schema
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -55,18 +55,20 @@ project-name/
|
|||||||
- template_content.py
|
- template_content.py
|
||||||
- urls.py
|
- urls.py
|
||||||
- views.py
|
- views.py
|
||||||
|
- pyproject.toml
|
||||||
- README.md
|
- README.md
|
||||||
- setup.py
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The top level is the project root, which can have any name that you like. Immediately within the root should exist several items:
|
The top level is the project root, which can have any name that you like. Immediately within the root should exist several items:
|
||||||
|
|
||||||
* `setup.py` - This is a standard installation script used to install the plugin package within the Python environment.
|
* `pyproject.toml` - is a standard configuration file used to install the plugin package within the Python environment.
|
||||||
* `README.md` - A brief introduction to your plugin, how to install and configure it, where to find help, and any other pertinent information. It is recommended to write `README` files using a markup language such as Markdown to enable human-friendly display.
|
* `README.md` - A brief introduction to your plugin, how to install and configure it, where to find help, and any other pertinent information. It is recommended to write `README` files using a markup language such as Markdown to enable human-friendly display.
|
||||||
* The plugin source directory. This must be a valid Python package name, typically comprising only lowercase letters, numbers, and underscores.
|
* The plugin source directory. This must be a valid Python package name, typically comprising only lowercase letters, numbers, and underscores.
|
||||||
|
|
||||||
The plugin source directory contains all the actual Python code and other resources used by your plugin. Its structure is left to the author's discretion, however it is recommended to follow best practices as outlined in the [Django documentation](https://docs.djangoproject.com/en/stable/intro/reusable-apps/). At a minimum, this directory **must** contain an `__init__.py` file containing an instance of NetBox's `PluginConfig` class, discussed below.
|
The plugin source directory contains all the actual Python code and other resources used by your plugin. Its structure is left to the author's discretion, however it is recommended to follow best practices as outlined in the [Django documentation](https://docs.djangoproject.com/en/stable/intro/reusable-apps/). At a minimum, this directory **must** contain an `__init__.py` file containing an instance of NetBox's `PluginConfig` class, discussed below.
|
||||||
|
|
||||||
|
**Note:** The [Cookiecutter NetBox Plugin](https://github.com/netbox-community/cookiecutter-netbox-plugin) can be used to auto-generate all the needed directories and files for a new plugin.
|
||||||
|
|
||||||
## PluginConfig
|
## PluginConfig
|
||||||
|
|
||||||
The `PluginConfig` class is a NetBox-specific wrapper around Django's built-in [`AppConfig`](https://docs.djangoproject.com/en/stable/ref/applications/) class. It is used to declare NetBox plugin functionality within a Python package. Each plugin should provide its own subclass, defining its name, metadata, and default and required configuration parameters. An example is below:
|
The `PluginConfig` class is a NetBox-specific wrapper around Django's built-in [`AppConfig`](https://docs.djangoproject.com/en/stable/ref/applications/) class. It is used to declare NetBox plugin functionality within a Python package. Each plugin should provide its own subclass, defining its name, metadata, and default and required configuration parameters. An example is below:
|
||||||
@ -136,31 +138,48 @@ Apps from this list are inserted *before* the plugin's `PluginConfig` in the ord
|
|||||||
|
|
||||||
Any additional apps must be installed within the same Python environment as NetBox or `ImproperlyConfigured` exceptions will be raised when loading the plugin.
|
Any additional apps must be installed within the same Python environment as NetBox or `ImproperlyConfigured` exceptions will be raised when loading the plugin.
|
||||||
|
|
||||||
## Create setup.py
|
## Create pyproject.toml
|
||||||
|
|
||||||
`setup.py` is the [setup script](https://docs.python.org/3.10/distutils/setupscript.html) used to package and install our plugin once it's finished. The primary function of this script is to call the setuptools library's `setup()` function to create a Python distribution package. We can pass a number of keyword arguments to control the package creation as well as to provide metadata about the plugin. An example `setup.py` is below:
|
`pyproject.toml` is the [configuration file](https://packaging.python.org/en/latest/guides/writing-pyproject-toml/) used to package and install our plugin once it's finished. It is used by packaging tools, as well as other tools. The primary function of this file is to call the build system to create a Python distribution package. We can pass a number of keyword arguments to control the package creation as well as to provide metadata about the plugin. There are three possible TOML tables in this file:
|
||||||
|
|
||||||
```python
|
* `[build-system]` allows you to declare which build backend you use and which other dependencies (if any) are needed to build your project.
|
||||||
from setuptools import find_packages, setup
|
* `[project]` is the format that most build backends use to specify your project’s basic metadata, such as the author's name, project URL, etc.
|
||||||
|
* `[tool]` has tool-specific subtables, e.g., `[tool.black]`, `[tool.mypy]`. Consult the particular tool’s documentation for reference.
|
||||||
|
|
||||||
|
An example `pyproject.toml` is below:
|
||||||
|
|
||||||
|
```
|
||||||
|
# See PEP 518 for the spec of this file
|
||||||
|
# https://www.python.org/dev/peps/pep-0518/
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["setuptools"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "my-example-plugin"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = [
|
||||||
|
{name = "John Doe", email = "test@netboxlabs.com"},
|
||||||
|
]
|
||||||
|
description = "An example NetBox plugin."
|
||||||
|
readme = "README.md"
|
||||||
|
|
||||||
|
classifiers=[
|
||||||
|
'Development Status :: 3 - Alpha',
|
||||||
|
'Intended Audience :: Developers',
|
||||||
|
'Natural Language :: English',
|
||||||
|
"Programming Language :: Python :: 3 :: Only",
|
||||||
|
'Programming Language :: Python :: 3.10',
|
||||||
|
'Programming Language :: Python :: 3.11',
|
||||||
|
'Programming Language :: Python :: 3.12',
|
||||||
|
]
|
||||||
|
|
||||||
|
requires-python = ">=3.10.0"
|
||||||
|
|
||||||
setup(
|
|
||||||
name='my-example-plugin',
|
|
||||||
version='0.1',
|
|
||||||
description='An example NetBox plugin',
|
|
||||||
url='https://github.com/jeremystretch/my-example-plugin',
|
|
||||||
author='Jeremy Stretch',
|
|
||||||
license='Apache 2.0',
|
|
||||||
install_requires=[],
|
|
||||||
packages=find_packages(),
|
|
||||||
include_package_data=True,
|
|
||||||
zip_safe=False,
|
|
||||||
)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Many of these are self-explanatory, but for more information, see the [setuptools documentation](https://setuptools.readthedocs.io/en/latest/setuptools.html).
|
Many of these are self-explanatory, but for more information, see the [pyproject.toml documentation](https://packaging.python.org/en/latest/specifications/pyproject-toml/).
|
||||||
|
|
||||||
!!! info
|
|
||||||
`zip_safe=False` is **required** as the current plugin iteration is not zip safe due to upstream python issue [issue19699](https://bugs.python.org/issue19699)
|
|
||||||
|
|
||||||
## Create a Virtual Environment
|
## Create a Virtual Environment
|
||||||
|
|
||||||
@ -178,11 +197,12 @@ echo /opt/netbox/netbox > $VENV/lib/python3.10/site-packages/netbox.pth
|
|||||||
|
|
||||||
## Development Installation
|
## Development Installation
|
||||||
|
|
||||||
To ease development, it is recommended to go ahead and install the plugin at this point using setuptools' `develop` mode. This will create symbolic links within your Python environment to the plugin development directory. Call `setup.py` from the plugin's root directory with the `develop` argument (instead of `install`):
|
To ease development, it is recommended to go ahead and install the plugin at this point using setuptools' `develop` mode. This will create symbolic links within your Python environment to the plugin development directory. Call `pip` from the plugin's root directory with the `-e` flag:
|
||||||
|
|
||||||
```no-highlight
|
```no-highlight
|
||||||
$ python setup.py develop
|
$ pip install -e .
|
||||||
```
|
```
|
||||||
|
More information on editable builds can be found at [Editable installs for pyproject.toml ](https://peps.python.org/pep-0660/).
|
||||||
|
|
||||||
## Configure NetBox
|
## Configure NetBox
|
||||||
|
|
||||||
|
@ -1,5 +1,34 @@
|
|||||||
# NetBox v4.0
|
# NetBox v4.0
|
||||||
|
|
||||||
|
## v4.0.1 (2024-05-09)
|
||||||
|
|
||||||
|
### Enhancements
|
||||||
|
|
||||||
|
* [#15148](https://github.com/netbox-community/netbox/issues/15148) - Add copy-to-clipboard button for config context data
|
||||||
|
* [#15328](https://github.com/netbox-community/netbox/issues/15328) - Add a virtual machines UI tab for host devices
|
||||||
|
* [#15451](https://github.com/netbox-community/netbox/issues/15451) - Add 2.5 and 5 Gbps backplane Ethernet interface types
|
||||||
|
* [#16010](https://github.com/netbox-community/netbox/issues/16010) - Enable Prometheus middleware only if metrics are enabled
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* [#15968](https://github.com/netbox-community/netbox/issues/15968) - Avoid resizing quick search field to display clear button
|
||||||
|
* [#15973](https://github.com/netbox-community/netbox/issues/15973) - Fix AttributeError exception when modifying cable termination type
|
||||||
|
* [#15977](https://github.com/netbox-community/netbox/issues/15977) - Hide all admin menu items for non-authenticated users
|
||||||
|
* [#15982](https://github.com/netbox-community/netbox/issues/15982) - Restore the "assign IP" tab for assigning existing IP addresses to interfaces
|
||||||
|
* [#15992](https://github.com/netbox-community/netbox/issues/15992) - Fix AttributeError exception when Sentry integration is enabled
|
||||||
|
* [#15995](https://github.com/netbox-community/netbox/issues/15995) - Permit nullable fields referenced by unique constraints to be omitted from REST API requests
|
||||||
|
* [#15999](https://github.com/netbox-community/netbox/issues/15999) - Fix layout of login form labels for certain languages
|
||||||
|
* [#16003](https://github.com/netbox-community/netbox/issues/16003) - Enable cache busting for `setmode.js` asset to avoid breaking dark mode support on upgrade
|
||||||
|
* [#16011](https://github.com/netbox-community/netbox/issues/16011) - Fix site tenant assignment by PK via REST API
|
||||||
|
* [#16020](https://github.com/netbox-community/netbox/issues/16020) - Include Python version in system UI view
|
||||||
|
* [#16022](https://github.com/netbox-community/netbox/issues/16022) - Fix database migration failure when encountering a script module which no longer exists on disk
|
||||||
|
* [#16025](https://github.com/netbox-community/netbox/issues/16025) - Fix execution of scripts via the `runscript` management command
|
||||||
|
* [#16031](https://github.com/netbox-community/netbox/issues/16031) - Render Markdown content in script log messages
|
||||||
|
* [#16051](https://github.com/netbox-community/netbox/issues/16051) - Translate "empty" text for object tables
|
||||||
|
* [#16061](https://github.com/netbox-community/netbox/issues/16061) - Omit hidden fields from display within event rule edit form
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## v4.0.0 (2024-05-06)
|
## v4.0.0 (2024-05-06)
|
||||||
|
|
||||||
!!! tip "Plugin Maintainers"
|
!!! tip "Plugin Maintainers"
|
||||||
|
@ -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)
|
||||||
|
@ -848,6 +848,8 @@ class InterfaceTypeChoices(ChoiceSet):
|
|||||||
|
|
||||||
# Ethernet Backplane
|
# Ethernet Backplane
|
||||||
TYPE_1GE_KX = '1000base-kx'
|
TYPE_1GE_KX = '1000base-kx'
|
||||||
|
TYPE_2GE_KX = '2.5gbase-kx'
|
||||||
|
TYPE_5GE_KR = '5gbase-kr'
|
||||||
TYPE_10GE_KR = '10gbase-kr'
|
TYPE_10GE_KR = '10gbase-kr'
|
||||||
TYPE_10GE_KX4 = '10gbase-kx4'
|
TYPE_10GE_KX4 = '10gbase-kx4'
|
||||||
TYPE_25GE_KR = '25gbase-kr'
|
TYPE_25GE_KR = '25gbase-kr'
|
||||||
@ -1008,6 +1010,8 @@ class InterfaceTypeChoices(ChoiceSet):
|
|||||||
_('Ethernet (backplane)'),
|
_('Ethernet (backplane)'),
|
||||||
(
|
(
|
||||||
(TYPE_1GE_KX, '1000BASE-KX (1GE)'),
|
(TYPE_1GE_KX, '1000BASE-KX (1GE)'),
|
||||||
|
(TYPE_2GE_KX, '2.5GBASE-KX (2.5GE)'),
|
||||||
|
(TYPE_5GE_KR, '5GBASE-KR (5GE)'),
|
||||||
(TYPE_10GE_KR, '10GBASE-KR (10GE)'),
|
(TYPE_10GE_KR, '10GBASE-KR (10GE)'),
|
||||||
(TYPE_10GE_KX4, '10GBASE-KX4 (10GE)'),
|
(TYPE_10GE_KX4, '10GBASE-KX4 (10GE)'),
|
||||||
(TYPE_25GE_KR, '25GBASE-KR (25GE)'),
|
(TYPE_25GE_KR, '25GBASE-KR (25GE)'),
|
||||||
|
@ -90,14 +90,14 @@ def get_cable_form(a_type, b_type):
|
|||||||
class _CableForm(CableForm, metaclass=FormMetaclass):
|
class _CableForm(CableForm, metaclass=FormMetaclass):
|
||||||
|
|
||||||
def __init__(self, *args, initial=None, **kwargs):
|
def __init__(self, *args, initial=None, **kwargs):
|
||||||
|
|
||||||
initial = initial or {}
|
initial = initial or {}
|
||||||
|
|
||||||
if a_type:
|
if a_type:
|
||||||
ct = ContentType.objects.get_for_model(a_type)
|
a_ct = ContentType.objects.get_for_model(a_type)
|
||||||
initial['a_terminations_type'] = f'{ct.app_label}.{ct.model}'
|
initial['a_terminations_type'] = f'{a_ct.app_label}.{a_ct.model}'
|
||||||
if b_type:
|
if b_type:
|
||||||
ct = ContentType.objects.get_for_model(b_type)
|
b_ct = ContentType.objects.get_for_model(b_type)
|
||||||
initial['b_terminations_type'] = f'{ct.app_label}.{ct.model}'
|
initial['b_terminations_type'] = f'{b_ct.app_label}.{b_ct.model}'
|
||||||
|
|
||||||
# TODO: Temporary hack to work around list handling limitations with utils.normalize_querydict()
|
# TODO: Temporary hack to work around list handling limitations with utils.normalize_querydict()
|
||||||
for field_name in ('a_terminations', 'b_terminations'):
|
for field_name in ('a_terminations', 'b_terminations'):
|
||||||
@ -108,8 +108,17 @@ def get_cable_form(a_type, b_type):
|
|||||||
|
|
||||||
if self.instance and self.instance.pk:
|
if self.instance and self.instance.pk:
|
||||||
# Initialize A/B terminations when modifying an existing Cable instance
|
# Initialize A/B terminations when modifying an existing Cable instance
|
||||||
|
if a_type and self.instance.a_terminations and a_ct == ContentType.objects.get_for_model(self.instance.a_terminations[0]):
|
||||||
self.initial['a_terminations'] = self.instance.a_terminations
|
self.initial['a_terminations'] = self.instance.a_terminations
|
||||||
|
if b_type and self.instance.b_terminations and b_ct == ContentType.objects.get_for_model(self.instance.b_terminations[0]):
|
||||||
self.initial['b_terminations'] = self.instance.b_terminations
|
self.initial['b_terminations'] = self.instance.b_terminations
|
||||||
|
else:
|
||||||
|
# Need to clear terminations if swapped type - but need to do it only
|
||||||
|
# if not from instance
|
||||||
|
if a_type:
|
||||||
|
initial.pop('a_terminations', None)
|
||||||
|
if b_type:
|
||||||
|
initial.pop('b_terminations', None)
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
super().clean()
|
super().clean()
|
||||||
|
@ -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
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
@ -28,7 +28,9 @@ from utilities.permissions import get_permission_for_model
|
|||||||
from utilities.query import count_related
|
from utilities.query import count_related
|
||||||
from utilities.query_functions import CollateAsChar
|
from utilities.query_functions import CollateAsChar
|
||||||
from utilities.views import GetReturnURLMixin, ObjectPermissionRequiredMixin, ViewTab, register_model_view
|
from utilities.views import GetReturnURLMixin, ObjectPermissionRequiredMixin, ViewTab, register_model_view
|
||||||
|
from virtualization.filtersets import VirtualMachineFilterSet
|
||||||
from virtualization.models import VirtualMachine
|
from virtualization.models import VirtualMachine
|
||||||
|
from virtualization.tables import VirtualMachineTable
|
||||||
from . import filtersets, forms, tables
|
from . import filtersets, forms, tables
|
||||||
from .choices import DeviceFaceChoices
|
from .choices import DeviceFaceChoices
|
||||||
from .models import *
|
from .models import *
|
||||||
@ -2085,6 +2087,25 @@ class DeviceRenderConfigView(generic.ObjectView):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@register_model_view(Device, 'virtual-machines')
|
||||||
|
class DeviceVirtualMachinesView(generic.ObjectChildrenView):
|
||||||
|
queryset = Device.objects.all()
|
||||||
|
child_model = VirtualMachine
|
||||||
|
table = VirtualMachineTable
|
||||||
|
filterset = VirtualMachineFilterSet
|
||||||
|
template_name = 'generic/object_children.html'
|
||||||
|
tab = ViewTab(
|
||||||
|
label=_('Virtual Machines'),
|
||||||
|
badge=lambda obj: VirtualMachine.objects.filter(cluster=obj.cluster, device=obj).count(),
|
||||||
|
weight=2200,
|
||||||
|
hide_if_empty=True,
|
||||||
|
permission='virtualization.view_virtualmachine'
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_children(self, request, parent):
|
||||||
|
return self.child_model.objects.restrict(request.user, 'view').filter(cluster=parent.cluster, device=parent)
|
||||||
|
|
||||||
|
|
||||||
class DeviceBulkImportView(generic.BulkImportView):
|
class DeviceBulkImportView(generic.BulkImportView):
|
||||||
queryset = Device.objects.all()
|
queryset = Device.objects.all()
|
||||||
model_form = forms.DeviceImportForm
|
model_form = forms.DeviceImportForm
|
||||||
|
@ -279,10 +279,7 @@ class EventRuleForm(NetBoxModelForm):
|
|||||||
FieldSet('name', 'description', 'object_types', 'enabled', 'tags', name=_('Event Rule')),
|
FieldSet('name', 'description', 'object_types', 'enabled', 'tags', name=_('Event Rule')),
|
||||||
FieldSet('type_create', 'type_update', 'type_delete', 'type_job_start', 'type_job_end', name=_('Events')),
|
FieldSet('type_create', 'type_update', 'type_delete', 'type_job_start', 'type_job_end', name=_('Events')),
|
||||||
FieldSet('conditions', name=_('Conditions')),
|
FieldSet('conditions', name=_('Conditions')),
|
||||||
FieldSet(
|
FieldSet('action_type', 'action_choice', 'action_data', name=_('Action')),
|
||||||
'action_type', 'action_choice', 'action_object_type', 'action_object_id', 'action_data',
|
|
||||||
name=_('Action')
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -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]] = ()
|
||||||
|
|
||||||
|
@ -371,6 +371,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(
|
||||||
@ -390,6 +391,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(
|
||||||
@ -409,12 +411,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'])
|
||||||
),
|
),
|
||||||
@ -425,16 +429,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
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -25,7 +25,7 @@ from utilities.string import trailing_slash
|
|||||||
# Environment setup
|
# Environment setup
|
||||||
#
|
#
|
||||||
|
|
||||||
VERSION = '4.0.0'
|
VERSION = '4.0.1'
|
||||||
HOSTNAME = platform.node()
|
HOSTNAME = platform.node()
|
||||||
# Set the base directory two levels up
|
# Set the base directory two levels up
|
||||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
@ -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,6 +385,12 @@ MIDDLEWARE = [
|
|||||||
'netbox.middleware.RemoteUserMiddleware',
|
'netbox.middleware.RemoteUserMiddleware',
|
||||||
'netbox.middleware.CoreMiddleware',
|
'netbox.middleware.CoreMiddleware',
|
||||||
'netbox.middleware.MaintenanceModeMiddleware',
|
'netbox.middleware.MaintenanceModeMiddleware',
|
||||||
|
]
|
||||||
|
if METRICS_ENABLED:
|
||||||
|
# If metrics are enabled, add the before & after Prometheus middleware
|
||||||
|
MIDDLEWARE = [
|
||||||
|
'django_prometheus.middleware.PrometheusBeforeMiddleware',
|
||||||
|
*MIDDLEWARE,
|
||||||
'django_prometheus.middleware.PrometheusAfterMiddleware',
|
'django_prometheus.middleware.PrometheusAfterMiddleware',
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -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.
@ -30,7 +30,7 @@
|
|||||||
"gridstack": "10.1.2",
|
"gridstack": "10.1.2",
|
||||||
"htmx.org": "1.9.12",
|
"htmx.org": "1.9.12",
|
||||||
"query-string": "9.0.0",
|
"query-string": "9.0.0",
|
||||||
"sass": "1.76.0",
|
"sass": "1.77.0",
|
||||||
"tom-select": "2.3.1",
|
"tom-select": "2.3.1",
|
||||||
"typeface-inter": "3.18.1",
|
"typeface-inter": "3.18.1",
|
||||||
"typeface-roboto-mono": "1.1.13"
|
"typeface-roboto-mono": "1.1.13"
|
||||||
|
@ -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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2482,7 +2482,16 @@ safe-regex-test@^1.0.3:
|
|||||||
es-errors "^1.3.0"
|
es-errors "^1.3.0"
|
||||||
is-regex "^1.1.4"
|
is-regex "^1.1.4"
|
||||||
|
|
||||||
sass@1.76.0, sass@^1.7.3:
|
sass@1.77.0:
|
||||||
|
version "1.77.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/sass/-/sass-1.77.0.tgz#e736c69aff9fae4a4e6dae60a979eee9c942f321"
|
||||||
|
integrity sha512-eGj4HNfXqBWtSnvItNkn7B6icqH14i3CiCGbzMKs3BAPTq62pp9NBYsBgyN4cA+qssqo9r26lW4JSvlaUUWbgw==
|
||||||
|
dependencies:
|
||||||
|
chokidar ">=3.0.0 <4.0.0"
|
||||||
|
immutable "^4.0.0"
|
||||||
|
source-map-js ">=0.6.2 <2.0.0"
|
||||||
|
|
||||||
|
sass@^1.7.3:
|
||||||
version "1.76.0"
|
version "1.76.0"
|
||||||
resolved "https://registry.yarnpkg.com/sass/-/sass-1.76.0.tgz#fe15909500735ac154f0dc7386d656b62b03987d"
|
resolved "https://registry.yarnpkg.com/sass/-/sass-1.76.0.tgz#fe15909500735ac154f0dc7386d656b62b03987d"
|
||||||
integrity sha512-nc3LeqvF2FNW5xGF1zxZifdW3ffIz5aBb7I7tSvOoNu7z1RQ6pFt9MBuiPtjgaI62YWrM/txjWlOCFiGtf2xpw==
|
integrity sha512-nc3LeqvF2FNW5xGF1zxZifdW3ffIz5aBb7I7tSvOoNu7z1RQ6pFt9MBuiPtjgaI62YWrM/txjWlOCFiGtf2xpw==
|
||||||
|
@ -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
|
||||||
@ -30,7 +30,7 @@ PyYAML==6.0.1
|
|||||||
requests==2.31.0
|
requests==2.31.0
|
||||||
social-auth-app-django==5.4.1
|
social-auth-app-django==5.4.1
|
||||||
social-auth-core==4.5.4
|
social-auth-core==4.5.4
|
||||||
strawberry-graphql==0.227.3
|
strawberry-graphql==0.227.4
|
||||||
strawberry-graphql-django==0.39.2
|
strawberry-graphql-django==0.39.2
|
||||||
svgwrite==1.4.3
|
svgwrite==1.4.3
|
||||||
tablib==3.6.1
|
tablib==3.6.1
|
||||||
|
Loading…
Reference in New Issue
Block a user