mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-25 01:48:38 -06:00
commit
5f184f2435
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.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: v3.5.0
|
placeholder: v3.5.1
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
|
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: v3.5.0
|
placeholder: v3.5.1
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
|
17
contrib/netbox-housekeeping.service
Normal file
17
contrib/netbox-housekeeping.service
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=NetBox Housekeeping Service
|
||||||
|
Documentation=https://docs.netbox.dev/
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
|
||||||
|
User=netbox
|
||||||
|
Group=netbox
|
||||||
|
WorkingDirectory=/opt/netbox
|
||||||
|
|
||||||
|
ExecStart=/opt/netbox/venv/bin/python /opt/netbox/netbox/manage.py housekeeping
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
13
contrib/netbox-housekeeping.timer
Normal file
13
contrib/netbox-housekeeping.timer
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=NetBox Housekeeping Timer
|
||||||
|
Documentation=https://docs.netbox.dev/
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnCalendar=daily
|
||||||
|
AccuracySec=1h
|
||||||
|
Persistent=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
@ -7,7 +7,13 @@ NetBox includes a `housekeeping` management command that should be run nightly.
|
|||||||
* Deleting job result records older than the configured [retention time](../configuration/miscellaneous.md#job_retention)
|
* Deleting job result records older than the configured [retention time](../configuration/miscellaneous.md#job_retention)
|
||||||
* Check for new NetBox releases (if [`RELEASE_CHECK_URL`](../configuration/miscellaneous.md#release_check_url) is set)
|
* Check for new NetBox releases (if [`RELEASE_CHECK_URL`](../configuration/miscellaneous.md#release_check_url) is set)
|
||||||
|
|
||||||
This command can be invoked directly, or by using the shell script provided at `/opt/netbox/contrib/netbox-housekeeping.sh`. This script can be linked from your cron scheduler's daily jobs directory (e.g. `/etc/cron.daily`) or referenced directly within the cron configuration file.
|
This command can be invoked directly, or by using the shell script provided at `/opt/netbox/contrib/netbox-housekeeping.sh`.
|
||||||
|
|
||||||
|
## Scheduling
|
||||||
|
|
||||||
|
### Using Cron
|
||||||
|
|
||||||
|
This script can be linked from your cron scheduler's daily jobs directory (e.g. `/etc/cron.daily`) or referenced directly within the cron configuration file.
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
sudo ln -s /opt/netbox/contrib/netbox-housekeeping.sh /etc/cron.daily/netbox-housekeeping
|
sudo ln -s /opt/netbox/contrib/netbox-housekeeping.sh /etc/cron.daily/netbox-housekeeping
|
||||||
@ -16,4 +22,28 @@ sudo ln -s /opt/netbox/contrib/netbox-housekeeping.sh /etc/cron.daily/netbox-hou
|
|||||||
!!! note
|
!!! note
|
||||||
On Debian-based systems, be sure to omit the `.sh` file extension when linking to the script from within a cron directory. Otherwise, the task may not run.
|
On Debian-based systems, be sure to omit the `.sh` file extension when linking to the script from within a cron directory. Otherwise, the task may not run.
|
||||||
|
|
||||||
The `housekeeping` command can also be run manually at any time: Running the command outside scheduled execution times will not interfere with its operation.
|
### Using Systemd
|
||||||
|
|
||||||
|
First, create symbolic links for the systemd service and timer files. Link the existing service and timer files from the `/opt/netbox/contrib/` directory to the `/etc/systemd/system/` directory:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo ln -s /opt/netbox/contrib/netbox-housekeeping.service /etc/systemd/system/netbox-housekeeping.service
|
||||||
|
sudo ln -s /opt/netbox/contrib/netbox-housekeeping.timer /etc/systemd/system/netbox-housekeeping.timer
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, reload the systemd configuration and enable the timer to start automatically at boot:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
sudo systemctl enable --now netbox-housekeeping.timer
|
||||||
|
```
|
||||||
|
|
||||||
|
Check the status of your timer by running:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo systemctl list-timers --all
|
||||||
|
```
|
||||||
|
|
||||||
|
This command will show a list of all timers, including your `netbox-housekeeping.timer`. Make sure the timer is active and properly scheduled.
|
||||||
|
|
||||||
|
That's it! Your NetBox housekeeping service is now configured to run daily using systemd.
|
||||||
|
@ -33,11 +33,13 @@ NetBox requires access to a PostgreSQL 11 or later database service to store dat
|
|||||||
* `HOST` - Name or IP address of the database server (use `localhost` if running locally)
|
* `HOST` - Name or IP address of the database server (use `localhost` if running locally)
|
||||||
* `PORT` - TCP port of the PostgreSQL service; leave blank for default port (TCP/5432)
|
* `PORT` - TCP port of the PostgreSQL service; leave blank for default port (TCP/5432)
|
||||||
* `CONN_MAX_AGE` - Lifetime of a [persistent database connection](https://docs.djangoproject.com/en/stable/ref/databases/#persistent-connections), in seconds (300 is the default)
|
* `CONN_MAX_AGE` - Lifetime of a [persistent database connection](https://docs.djangoproject.com/en/stable/ref/databases/#persistent-connections), in seconds (300 is the default)
|
||||||
|
* `ENGINE` - The database backend to use; must be a PostgreSQL-compatible backend (e.g. `django.db.backends.postgresql`)
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
DATABASE = {
|
DATABASE = {
|
||||||
|
'ENGINE': 'django.db.backends.postgresql',
|
||||||
'NAME': 'netbox', # Database name
|
'NAME': 'netbox', # Database name
|
||||||
'USER': 'netbox', # PostgreSQL username
|
'USER': 'netbox', # PostgreSQL username
|
||||||
'PASSWORD': 'J5brHrAXFLQSif0K', # PostgreSQL password
|
'PASSWORD': 'J5brHrAXFLQSif0K', # PostgreSQL password
|
||||||
@ -50,6 +52,9 @@ DATABASE = {
|
|||||||
!!! note
|
!!! note
|
||||||
NetBox supports all PostgreSQL database options supported by the underlying Django framework. For a complete list of available parameters, please see [the Django documentation](https://docs.djangoproject.com/en/stable/ref/settings/#databases).
|
NetBox supports all PostgreSQL database options supported by the underlying Django framework. For a complete list of available parameters, please see [the Django documentation](https://docs.djangoproject.com/en/stable/ref/settings/#databases).
|
||||||
|
|
||||||
|
!!! warning
|
||||||
|
Make sure to use a PostgreSQL-compatible backend for the ENGINE setting. If you don't specify an ENGINE, the default will be django.db.backends.postgresql.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## REDIS
|
## REDIS
|
||||||
|
@ -2,12 +2,12 @@
|
|||||||
|
|
||||||
Custom links allow users to display arbitrary hyperlinks to external content within NetBox object views. These are helpful for cross-referencing related records in systems outside NetBox. For example, you might create a custom link on the device view which links to the current device in a Network Monitoring System (NMS).
|
Custom links allow users to display arbitrary hyperlinks to external content within NetBox object views. These are helpful for cross-referencing related records in systems outside NetBox. For example, you might create a custom link on the device view which links to the current device in a Network Monitoring System (NMS).
|
||||||
|
|
||||||
Custom links are created by navigating to Customization > Custom Links. Each link is associated with a particular NetBox object type (site, device, prefix, etc.) and will be displayed on relevant views. Each link has display text and a URL, and data from the NetBox item being viewed can be included in the link using [Jinja2 template code](https://jinja2docs.readthedocs.io/en/stable/) through the variable `obj`, and custom fields through `obj.cf`.
|
Custom links are created by navigating to Customization > Custom Links. Each link is associated with a particular NetBox object type (site, device, prefix, etc.) and will be displayed on relevant views. Each link has display text and a URL, and data from the NetBox item being viewed can be included in the link using [Jinja2 template code](https://jinja2docs.readthedocs.io/en/stable/) through the variable `object`, and custom fields through `object.cf`.
|
||||||
|
|
||||||
For example, you might define a link like this:
|
For example, you might define a link like this:
|
||||||
|
|
||||||
* Text: `View NMS`
|
* Text: `View NMS`
|
||||||
* URL: `https://nms.example.com/nodes/?name={{ obj.name }}`
|
* URL: `https://nms.example.com/nodes/?name={{ object.name }}`
|
||||||
|
|
||||||
When viewing a device named Router4, this link would render as:
|
When viewing a device named Router4, this link would render as:
|
||||||
|
|
||||||
@ -43,7 +43,7 @@ Only links which render with non-empty text are included on the page. You can em
|
|||||||
For example, if you only want to display a link for active devices, you could set the link text to
|
For example, if you only want to display a link for active devices, you could set the link text to
|
||||||
|
|
||||||
```jinja2
|
```jinja2
|
||||||
{% if obj.status == 'active' %}View NMS{% endif %}
|
{% if object.status == 'active' %}View NMS{% endif %}
|
||||||
```
|
```
|
||||||
|
|
||||||
The link will not appear when viewing a device with any status other than "active."
|
The link will not appear when viewing a device with any status other than "active."
|
||||||
@ -51,7 +51,7 @@ The link will not appear when viewing a device with any status other than "activ
|
|||||||
As another example, if you wanted to show only devices belonging to a certain manufacturer, you could do something like this:
|
As another example, if you wanted to show only devices belonging to a certain manufacturer, you could do something like this:
|
||||||
|
|
||||||
```jinja2
|
```jinja2
|
||||||
{% if obj.device_type.manufacturer.name == 'Cisco' %}View NMS{% endif %}
|
{% if object.device_type.manufacturer.name == 'Cisco' %}View NMS{% endif %}
|
||||||
```
|
```
|
||||||
|
|
||||||
The link will only appear when viewing a device with a manufacturer name of "Cisco."
|
The link will only appear when viewing a device with a manufacturer name of "Cisco."
|
||||||
|
@ -32,7 +32,7 @@ These are considered the "core" application models which are used to model netwo
|
|||||||
|
|
||||||
* [circuits.Circuit](../models/circuits/circuit.md)
|
* [circuits.Circuit](../models/circuits/circuit.md)
|
||||||
* [circuits.Provider](../models/circuits/provider.md)
|
* [circuits.Provider](../models/circuits/provider.md)
|
||||||
* [circuits.ProviderAccount](../models/circuits/provideracount.md)
|
* [circuits.ProviderAccount](../models/circuits/provideraccount.md)
|
||||||
* [circuits.ProviderNetwork](../models/circuits/providernetwork.md)
|
* [circuits.ProviderNetwork](../models/circuits/providernetwork.md)
|
||||||
* [core.DataSource](../models/core/datasource.md)
|
* [core.DataSource](../models/core/datasource.md)
|
||||||
* [dcim.Cable](../models/dcim/cable.md)
|
* [dcim.Cable](../models/dcim/cable.md)
|
||||||
|
@ -4,8 +4,6 @@ A platform defines the type of software running on a [device](./device.md) or [v
|
|||||||
|
|
||||||
Platforms may optionally be limited by [manufacturer](./manufacturer.md): If a platform is assigned to a particular manufacturer, it can only be assigned to devices with a type belonging to that manufacturer.
|
Platforms may optionally be limited by [manufacturer](./manufacturer.md): If a platform is assigned to a particular manufacturer, it can only be assigned to devices with a type belonging to that manufacturer.
|
||||||
|
|
||||||
The platform model is also used to indicate which [NAPALM driver](../../integrations/napalm.md) (if any) and any associated arguments NetBox should use when connecting to a remote device. The name of the driver along with optional parameters are stored with the platform.
|
|
||||||
|
|
||||||
The assignment of platforms to devices is an optional feature, and may be disregarded if not desired.
|
The assignment of platforms to devices is an optional feature, and may be disregarded if not desired.
|
||||||
|
|
||||||
## Fields
|
## Fields
|
||||||
|
@ -1,5 +1,47 @@
|
|||||||
# NetBox v3.5
|
# NetBox v3.5
|
||||||
|
|
||||||
|
## v3.5.1 (2023-05-05)
|
||||||
|
|
||||||
|
### Enhancements
|
||||||
|
|
||||||
|
* [#10759](https://github.com/netbox-community/netbox/issues/10759) - Support Markdown rendering for custom field descriptions
|
||||||
|
* [#11190](https://github.com/netbox-community/netbox/issues/11190) - Including systemd service & timer configurations for housekeeping tasks
|
||||||
|
* [#11422](https://github.com/netbox-community/netbox/issues/11422) - Match on power panel name when searching for power feeds
|
||||||
|
* [#11504](https://github.com/netbox-community/netbox/issues/11504) - Add filter to select individual racks under rack elevations view
|
||||||
|
* [#11652](https://github.com/netbox-community/netbox/issues/11652) - Add a module status column to module bay tables
|
||||||
|
* [#11791](https://github.com/netbox-community/netbox/issues/11791) - Enable configuration of custom database backend via `ENGINE` parameter
|
||||||
|
* [#11801](https://github.com/netbox-community/netbox/issues/11801) - Include device description within rack elevation tooltip
|
||||||
|
* [#11932](https://github.com/netbox-community/netbox/issues/11932) - Introduce a list view for image attachments, orderable by date and other attributes
|
||||||
|
* [#12122](https://github.com/netbox-community/netbox/issues/12122) - Enable bulk import oj journal entries
|
||||||
|
* [#12245](https://github.com/netbox-community/netbox/issues/12245) - Enable the assignment of wireless LANs to interfaces under bulk edit
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* [#10757](https://github.com/netbox-community/netbox/issues/10757) - Simplify IP address interface and NAT IP assignment form fields to avoid confusion
|
||||||
|
* [#11715](https://github.com/netbox-community/netbox/issues/11715) - Prefix within a VRF should list global prefixes as parents only if they are containers
|
||||||
|
* [#12363](https://github.com/netbox-community/netbox/issues/12363) - Fix whitespace for paragraph elements in Markdown-rendered table columns
|
||||||
|
* [#12367](https://github.com/netbox-community/netbox/issues/12367) - Fix `RelatedObjectDoesNotExist` exception under certain conditions (regression from #11550)
|
||||||
|
* [#12380](https://github.com/netbox-community/netbox/issues/12380) - Allow selecting object change as model under object list widget configuration
|
||||||
|
* [#12384](https://github.com/netbox-community/netbox/issues/12384) - Add a three-second timeout for RSS reader widget
|
||||||
|
* [#12395](https://github.com/netbox-community/netbox/issues/12395) - Fix "create & add another" action for objects with custom fields
|
||||||
|
* [#12396](https://github.com/netbox-community/netbox/issues/12396) - Provider account should not be a required field in REST API serializer
|
||||||
|
* [#12400](https://github.com/netbox-community/netbox/issues/12400) - Validate default values for object and multi-object custom fields
|
||||||
|
* [#12401](https://github.com/netbox-community/netbox/issues/12401) - Support the creation of front ports without a pre-populated device ID
|
||||||
|
* [#12405](https://github.com/netbox-community/netbox/issues/12405) - Fix filtering for VLAN groups displayed under site view
|
||||||
|
* [#12410](https://github.com/netbox-community/netbox/issues/12410) - Fix base path for OpenAPI schema (fixes Swagger UI requests)
|
||||||
|
* [#12416](https://github.com/netbox-community/netbox/issues/12416) - Fix `FileNotFoundError` exception when a managed script file is missing from disk
|
||||||
|
* [#12412](https://github.com/netbox-community/netbox/issues/12412) - Device/VM interface MAC addresses can be nullified via REST API
|
||||||
|
* [#12415](https://github.com/netbox-community/netbox/issues/12415) - Fix `ImportError` exception when running RQ worker
|
||||||
|
* [#12433](https://github.com/netbox-community/netbox/issues/12433) - Correct the application of URL query parameters for object list dashboard widgets
|
||||||
|
* [#12436](https://github.com/netbox-community/netbox/issues/12436) - Remove extraneous "add" button from contact assignments list
|
||||||
|
* [#12463](https://github.com/netbox-community/netbox/issues/12463) - Fix the association of completed jobs with reports & scripts in the REST API
|
||||||
|
* [#12464](https://github.com/netbox-community/netbox/issues/12464) - Apply credentials for git data source only when connecting via HTTP/S
|
||||||
|
* [#12476](https://github.com/netbox-community/netbox/issues/12476) - Fix `TypeError` exception when running the `runscript` management command
|
||||||
|
* [#12483](https://github.com/netbox-community/netbox/issues/12483) - Fix git remote data syncing when with HTTP proxies defined
|
||||||
|
* [#12496](https://github.com/netbox-community/netbox/issues/12496) - Remove obsolete account field from provider UI view
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## v3.5.0 (2023-04-27)
|
## v3.5.0 (2023-04-27)
|
||||||
|
|
||||||
### Breaking Changes
|
### Breaking Changes
|
||||||
|
@ -106,7 +106,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 = NestedProviderSerializer()
|
provider = NestedProviderSerializer()
|
||||||
provider_account = NestedProviderAccountSerializer()
|
provider_account = NestedProviderAccountSerializer(required=False, allow_null=True)
|
||||||
status = ChoiceField(choices=CircuitStatusChoices, required=False)
|
status = ChoiceField(choices=CircuitStatusChoices, required=False)
|
||||||
type = NestedCircuitTypeSerializer()
|
type = NestedCircuitTypeSerializer()
|
||||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||||
|
@ -74,7 +74,8 @@ class CircuitImportForm(NetBoxModelImportForm):
|
|||||||
provider_account = CSVModelChoiceField(
|
provider_account = CSVModelChoiceField(
|
||||||
queryset=ProviderAccount.objects.all(),
|
queryset=ProviderAccount.objects.all(),
|
||||||
to_field_name='name',
|
to_field_name='name',
|
||||||
help_text=_('Assigned provider account')
|
help_text=_('Assigned provider account'),
|
||||||
|
required=False
|
||||||
)
|
)
|
||||||
type = CSVModelChoiceField(
|
type = CSVModelChoiceField(
|
||||||
queryset=CircuitType.objects.all(),
|
queryset=CircuitType.objects.all(),
|
||||||
|
@ -1,23 +1,12 @@
|
|||||||
import re
|
import re
|
||||||
import typing
|
import typing
|
||||||
|
|
||||||
from drf_spectacular.extensions import (
|
from drf_spectacular.extensions import OpenApiSerializerFieldExtension
|
||||||
OpenApiSerializerFieldExtension,
|
|
||||||
OpenApiViewExtension,
|
|
||||||
)
|
|
||||||
from drf_spectacular.openapi import AutoSchema
|
from drf_spectacular.openapi import AutoSchema
|
||||||
from drf_spectacular.plumbing import (
|
from drf_spectacular.plumbing import (
|
||||||
ComponentRegistry,
|
build_basic_type, build_choice_field, build_media_type_object, build_object_type, get_doc,
|
||||||
ResolvedComponent,
|
|
||||||
build_basic_type,
|
|
||||||
build_choice_field,
|
|
||||||
build_media_type_object,
|
|
||||||
build_object_type,
|
|
||||||
get_doc,
|
|
||||||
is_serializer,
|
|
||||||
)
|
)
|
||||||
from drf_spectacular.types import OpenApiTypes
|
from drf_spectacular.types import OpenApiTypes
|
||||||
from drf_spectacular.utils import extend_schema
|
|
||||||
from rest_framework.relations import ManyRelatedField
|
from rest_framework.relations import ManyRelatedField
|
||||||
|
|
||||||
from netbox.api.fields import ChoiceField, SerializedPKRelatedField
|
from netbox.api.fields import ChoiceField, SerializedPKRelatedField
|
||||||
|
@ -12,7 +12,7 @@ from django import forms
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from dulwich import porcelain
|
from dulwich import porcelain
|
||||||
from dulwich.config import StackedConfig
|
from dulwich.config import ConfigDict
|
||||||
|
|
||||||
from netbox.registry import registry
|
from netbox.registry import registry
|
||||||
from .choices import DataSourceTypeChoices
|
from .choices import DataSourceTypeChoices
|
||||||
@ -31,6 +31,7 @@ def register_backend(name):
|
|||||||
"""
|
"""
|
||||||
Decorator for registering a DataBackend class.
|
Decorator for registering a DataBackend class.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def _wrapper(cls):
|
def _wrapper(cls):
|
||||||
registry['data_backends'][name] = cls
|
registry['data_backends'][name] = cls
|
||||||
return cls
|
return cls
|
||||||
@ -56,7 +57,6 @@ class DataBackend:
|
|||||||
|
|
||||||
@register_backend(DataSourceTypeChoices.LOCAL)
|
@register_backend(DataSourceTypeChoices.LOCAL)
|
||||||
class LocalBackend(DataBackend):
|
class LocalBackend(DataBackend):
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def fetch(self):
|
def fetch(self):
|
||||||
logger.debug(f"Data source type is local; skipping fetch")
|
logger.debug(f"Data source type is local; skipping fetch")
|
||||||
@ -71,12 +71,14 @@ class GitBackend(DataBackend):
|
|||||||
'username': forms.CharField(
|
'username': forms.CharField(
|
||||||
required=False,
|
required=False,
|
||||||
label=_('Username'),
|
label=_('Username'),
|
||||||
widget=forms.TextInput(attrs={'class': 'form-control'})
|
widget=forms.TextInput(attrs={'class': 'form-control'}),
|
||||||
|
help_text=_("Only used for cloning with HTTP / HTTPS"),
|
||||||
),
|
),
|
||||||
'password': forms.CharField(
|
'password': forms.CharField(
|
||||||
required=False,
|
required=False,
|
||||||
label=_('Password'),
|
label=_('Password'),
|
||||||
widget=forms.TextInput(attrs={'class': 'form-control'})
|
widget=forms.TextInput(attrs={'class': 'form-control'}),
|
||||||
|
help_text=_("Only used for cloning with HTTP / HTTPS"),
|
||||||
),
|
),
|
||||||
'branch': forms.CharField(
|
'branch': forms.CharField(
|
||||||
required=False,
|
required=False,
|
||||||
@ -89,10 +91,22 @@ class GitBackend(DataBackend):
|
|||||||
def fetch(self):
|
def fetch(self):
|
||||||
local_path = tempfile.TemporaryDirectory()
|
local_path = tempfile.TemporaryDirectory()
|
||||||
|
|
||||||
username = self.params.get('username')
|
config = ConfigDict()
|
||||||
password = self.params.get('password')
|
clone_args = {
|
||||||
branch = self.params.get('branch')
|
"branch": self.params.get('branch'),
|
||||||
config = StackedConfig.default()
|
"config": config,
|
||||||
|
"depth": 1,
|
||||||
|
"errstream": porcelain.NoneStream(),
|
||||||
|
"quiet": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.url_scheme in ('http', 'https'):
|
||||||
|
clone_args.update(
|
||||||
|
{
|
||||||
|
"username": self.params.get('username'),
|
||||||
|
"password": self.params.get('password'),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
if settings.HTTP_PROXIES and self.url_scheme in ('http', 'https'):
|
if settings.HTTP_PROXIES and self.url_scheme in ('http', 'https'):
|
||||||
if proxy := settings.HTTP_PROXIES.get(self.url_scheme):
|
if proxy := settings.HTTP_PROXIES.get(self.url_scheme):
|
||||||
@ -100,10 +114,7 @@ class GitBackend(DataBackend):
|
|||||||
|
|
||||||
logger.debug(f"Cloning git repo: {self.url}")
|
logger.debug(f"Cloning git repo: {self.url}")
|
||||||
try:
|
try:
|
||||||
porcelain.clone(
|
porcelain.clone(self.url, local_path.name, **clone_args)
|
||||||
self.url, local_path.name, depth=1, branch=branch, username=username, password=password,
|
|
||||||
config=config, quiet=True, errstream=porcelain.NoneStream()
|
|
||||||
)
|
|
||||||
except BaseException as e:
|
except BaseException as e:
|
||||||
raise SyncError(f"Fetching remote data failed ({type(e).__name__}): {e}")
|
raise SyncError(f"Fetching remote data failed ({type(e).__name__}): {e}")
|
||||||
|
|
||||||
|
@ -904,7 +904,11 @@ class InterfaceSerializer(NetBoxModelSerializer, CabledObjectSerializer, Connect
|
|||||||
)
|
)
|
||||||
count_ipaddresses = serializers.IntegerField(read_only=True)
|
count_ipaddresses = serializers.IntegerField(read_only=True)
|
||||||
count_fhrp_groups = serializers.IntegerField(read_only=True)
|
count_fhrp_groups = serializers.IntegerField(read_only=True)
|
||||||
mac_address = serializers.CharField(required=False, default=None)
|
mac_address = serializers.CharField(
|
||||||
|
required=False,
|
||||||
|
default=None,
|
||||||
|
allow_null=True
|
||||||
|
)
|
||||||
wwn = serializers.CharField(required=False, default=None)
|
wwn = serializers.CharField(required=False, default=None)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -1900,6 +1900,7 @@ class PowerFeedFilterSet(NetBoxModelFilterSet, CabledObjectFilterSet, PathEndpoi
|
|||||||
return queryset
|
return queryset
|
||||||
qs_filter = (
|
qs_filter = (
|
||||||
Q(name__icontains=value) |
|
Q(name__icontains=value) |
|
||||||
|
Q(power_panel__name__icontains=value) |
|
||||||
Q(comments__icontains=value)
|
Q(comments__icontains=value)
|
||||||
)
|
)
|
||||||
return queryset.filter(qs_filter)
|
return queryset.filter(qs_filter)
|
||||||
|
@ -13,6 +13,7 @@ from tenancy.models import Tenant
|
|||||||
from utilities.forms import BulkEditForm, add_blank_choice, form_from_model
|
from utilities.forms import BulkEditForm, add_blank_choice, form_from_model
|
||||||
from utilities.forms.fields import ColorField, CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField
|
from utilities.forms.fields import ColorField, CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField
|
||||||
from utilities.forms.widgets import BulkEditNullBooleanSelect, NumberWithOptions
|
from utilities.forms.widgets import BulkEditNullBooleanSelect, NumberWithOptions
|
||||||
|
from wireless.models import WirelessLAN, WirelessLANGroup
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'CableBulkEditForm',
|
'CableBulkEditForm',
|
||||||
@ -1139,7 +1140,7 @@ class InterfaceBulkEditForm(
|
|||||||
form_from_model(Interface, [
|
form_from_model(Interface, [
|
||||||
'label', 'type', 'parent', 'bridge', 'lag', 'speed', 'duplex', 'mac_address', 'wwn', 'mtu', 'mgmt_only',
|
'label', 'type', 'parent', 'bridge', 'lag', 'speed', 'duplex', 'mac_address', 'wwn', 'mtu', 'mgmt_only',
|
||||||
'mark_connected', 'description', 'mode', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width',
|
'mark_connected', 'description', 'mode', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width',
|
||||||
'tx_power',
|
'tx_power', 'wireless_lans'
|
||||||
]),
|
]),
|
||||||
ComponentBulkEditForm
|
ComponentBulkEditForm
|
||||||
):
|
):
|
||||||
@ -1229,6 +1230,19 @@ class InterfaceBulkEditForm(
|
|||||||
required=False,
|
required=False,
|
||||||
label=_('VRF')
|
label=_('VRF')
|
||||||
)
|
)
|
||||||
|
wireless_lan_group = DynamicModelChoiceField(
|
||||||
|
queryset=WirelessLANGroup.objects.all(),
|
||||||
|
required=False,
|
||||||
|
label=_('Wireless LAN group')
|
||||||
|
)
|
||||||
|
wireless_lans = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=WirelessLAN.objects.all(),
|
||||||
|
required=False,
|
||||||
|
label=_('Wireless LANs'),
|
||||||
|
query_params={
|
||||||
|
'group_id': '$wireless_lan_group',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
model = Interface
|
model = Interface
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
@ -1238,12 +1252,14 @@ class InterfaceBulkEditForm(
|
|||||||
('PoE', ('poe_mode', 'poe_type')),
|
('PoE', ('poe_mode', 'poe_type')),
|
||||||
('Related Interfaces', ('parent', 'bridge', 'lag')),
|
('Related Interfaces', ('parent', 'bridge', 'lag')),
|
||||||
('802.1Q Switching', ('mode', 'vlan_group', 'untagged_vlan', 'tagged_vlans')),
|
('802.1Q Switching', ('mode', 'vlan_group', 'untagged_vlan', 'tagged_vlans')),
|
||||||
('Wireless', ('rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width')),
|
('Wireless', (
|
||||||
|
'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'wireless_lan_group', 'wireless_lans',
|
||||||
|
)),
|
||||||
)
|
)
|
||||||
nullable_fields = (
|
nullable_fields = (
|
||||||
'module', 'label', 'parent', 'bridge', 'lag', 'speed', 'duplex', 'mac_address', 'wwn', 'vdcs', 'mtu', 'description',
|
'module', 'label', 'parent', 'bridge', 'lag', 'speed', 'duplex', 'mac_address', 'wwn', 'vdcs', 'mtu', 'description',
|
||||||
'poe_mode', 'poe_type', 'mode', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power',
|
'poe_mode', 'poe_type', 'mode', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power',
|
||||||
'vlan_group', 'untagged_vlan', 'tagged_vlans', 'vrf',
|
'vlan_group', 'untagged_vlan', 'tagged_vlans', 'vrf', 'wireless_lans'
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
@ -298,6 +298,15 @@ class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilte
|
|||||||
|
|
||||||
|
|
||||||
class RackElevationFilterForm(RackFilterForm):
|
class RackElevationFilterForm(RackFilterForm):
|
||||||
|
fieldsets = (
|
||||||
|
(None, ('q', 'filter_id', 'tag')),
|
||||||
|
('Location', ('region_id', 'site_group_id', 'site_id', 'location_id', 'id')),
|
||||||
|
('Function', ('status', 'role_id')),
|
||||||
|
('Hardware', ('type', 'width', 'serial', 'asset_tag')),
|
||||||
|
('Tenant', ('tenant_group_id', 'tenant_id')),
|
||||||
|
('Contacts', ('contact', 'contact_role', 'contact_group')),
|
||||||
|
('Weight', ('weight', 'max_weight', 'weight_unit')),
|
||||||
|
)
|
||||||
id = DynamicModelMultipleChoiceField(
|
id = DynamicModelMultipleChoiceField(
|
||||||
queryset=Rack.objects.all(),
|
queryset=Rack.objects.all(),
|
||||||
label=_('Rack'),
|
label=_('Rack'),
|
||||||
|
@ -4,6 +4,7 @@ from django.utils.translation import gettext as _
|
|||||||
from dcim.models import *
|
from dcim.models import *
|
||||||
from netbox.forms import NetBoxModelForm
|
from netbox.forms import NetBoxModelForm
|
||||||
from utilities.forms.fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableNameField
|
from utilities.forms.fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableNameField
|
||||||
|
from utilities.forms.widgets import APISelect
|
||||||
from . import model_forms
|
from . import model_forms
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
@ -225,6 +226,18 @@ class InterfaceCreateForm(ComponentCreateForm, model_forms.InterfaceForm):
|
|||||||
|
|
||||||
|
|
||||||
class FrontPortCreateForm(ComponentCreateForm, model_forms.FrontPortForm):
|
class FrontPortCreateForm(ComponentCreateForm, model_forms.FrontPortForm):
|
||||||
|
device = DynamicModelChoiceField(
|
||||||
|
queryset=Device.objects.all(),
|
||||||
|
selector=True,
|
||||||
|
widget=APISelect(
|
||||||
|
# TODO: Clean up the application of HTMXSelect attributes
|
||||||
|
attrs={
|
||||||
|
'hx-get': '.',
|
||||||
|
'hx-include': f'#form_fields',
|
||||||
|
'hx-target': f'#form_fields',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
rear_port = forms.MultipleChoiceField(
|
rear_port = forms.MultipleChoiceField(
|
||||||
choices=[],
|
choices=[],
|
||||||
label=_('Rear ports'),
|
label=_('Rear ports'),
|
||||||
@ -244,9 +257,10 @@ class FrontPortCreateForm(ComponentCreateForm, model_forms.FrontPortForm):
|
|||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
device = Device.objects.get(
|
if device_id := self.data.get('device') or self.initial.get('device'):
|
||||||
pk=self.initial.get('device') or self.data.get('device')
|
device = Device.objects.get(pk=device_id)
|
||||||
)
|
else:
|
||||||
|
return
|
||||||
|
|
||||||
# Determine which rear port positions are occupied. These will be excluded from the list of available
|
# Determine which rear port positions are occupied. These will be excluded from the list of available
|
||||||
# mappings.
|
# mappings.
|
||||||
|
@ -37,15 +37,28 @@ def get_device_name(device):
|
|||||||
|
|
||||||
|
|
||||||
def get_device_description(device):
|
def get_device_description(device):
|
||||||
return '{} ({}) — {} {} ({}U) {} {}'.format(
|
"""
|
||||||
device.name,
|
Return a description for a device to be rendered in the rack elevation in the following format
|
||||||
device.device_role,
|
|
||||||
device.device_type.manufacturer.name,
|
Name: <name>
|
||||||
device.device_type.model,
|
Role: <device_role>
|
||||||
floatformat(device.device_type.u_height),
|
Device Type: <manufacturer> <model> (<u_height>)
|
||||||
device.asset_tag or '',
|
Asset tag: <asset_tag> (if defined)
|
||||||
device.serial or ''
|
Serial: <serial> (if defined)
|
||||||
)
|
Description: <description> (if defined)
|
||||||
|
"""
|
||||||
|
description = f'Name: {device.name}'
|
||||||
|
description += f'\nRole: {device.device_role}'
|
||||||
|
u_height = f'{floatformat(device.device_type.u_height)}U'
|
||||||
|
description += f'\nDevice Type: {device.device_type.manufacturer.name} {device.device_type.model} ({u_height})'
|
||||||
|
if device.asset_tag:
|
||||||
|
description += f'\nAsset tag: {device.asset_tag}'
|
||||||
|
if device.serial:
|
||||||
|
description += f'\nSerial: {device.serial}'
|
||||||
|
if device.description:
|
||||||
|
description += f'\nDescription: {device.description}'
|
||||||
|
|
||||||
|
return description
|
||||||
|
|
||||||
|
|
||||||
class RackElevationSVG:
|
class RackElevationSVG:
|
||||||
|
@ -39,6 +39,10 @@ __all__ = (
|
|||||||
'VirtualDeviceContextTable'
|
'VirtualDeviceContextTable'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
MODULEBAY_STATUS = """
|
||||||
|
{% badge record.installed_module.get_status_display bg_color=record.installed_module.get_status_color %}
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
def get_cabletermination_row_class(record):
|
def get_cabletermination_row_class(record):
|
||||||
if record.mark_connected:
|
if record.mark_connected:
|
||||||
@ -781,14 +785,17 @@ class ModuleBayTable(DeviceComponentTable):
|
|||||||
tags = columns.TagColumn(
|
tags = columns.TagColumn(
|
||||||
url_name='dcim:modulebay_list'
|
url_name='dcim:modulebay_list'
|
||||||
)
|
)
|
||||||
|
module_status = columns.TemplateColumn(
|
||||||
|
template_code=MODULEBAY_STATUS
|
||||||
|
)
|
||||||
|
|
||||||
class Meta(DeviceComponentTable.Meta):
|
class Meta(DeviceComponentTable.Meta):
|
||||||
model = models.ModuleBay
|
model = models.ModuleBay
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'name', 'device', 'label', 'position', 'installed_module', 'module_serial', 'module_asset_tag',
|
'pk', 'id', 'name', 'device', 'label', 'position', 'installed_module', 'module_status', 'module_serial',
|
||||||
'description', 'tags',
|
'module_asset_tag', 'description', 'tags',
|
||||||
)
|
)
|
||||||
default_columns = ('pk', 'name', 'device', 'label', 'installed_module', 'description')
|
default_columns = ('pk', 'name', 'device', 'label', 'installed_module', 'module_status', 'description')
|
||||||
|
|
||||||
|
|
||||||
class DeviceModuleBayTable(ModuleBayTable):
|
class DeviceModuleBayTable(ModuleBayTable):
|
||||||
@ -799,10 +806,10 @@ class DeviceModuleBayTable(ModuleBayTable):
|
|||||||
class Meta(DeviceComponentTable.Meta):
|
class Meta(DeviceComponentTable.Meta):
|
||||||
model = models.ModuleBay
|
model = models.ModuleBay
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'name', 'label', 'position', 'installed_module', 'module_serial', 'module_asset_tag',
|
'pk', 'id', 'name', 'label', 'position', 'installed_module', 'module_status', 'module_serial', 'module_asset_tag',
|
||||||
'description', 'tags', 'actions',
|
'description', 'tags', 'actions',
|
||||||
)
|
)
|
||||||
default_columns = ('pk', 'name', 'label', 'installed_module', 'description')
|
default_columns = ('pk', 'name', 'label', 'installed_module', 'module_status', 'description')
|
||||||
|
|
||||||
|
|
||||||
class InventoryItemTable(DeviceComponentTable):
|
class InventoryItemTable(DeviceComponentTable):
|
||||||
|
@ -371,7 +371,7 @@ class SiteView(generic.ObjectView):
|
|||||||
(VLANGroup.objects.restrict(request.user, 'view').filter(
|
(VLANGroup.objects.restrict(request.user, 'view').filter(
|
||||||
scope_type=ContentType.objects.get_for_model(Site),
|
scope_type=ContentType.objects.get_for_model(Site),
|
||||||
scope_id=instance.pk
|
scope_id=instance.pk
|
||||||
), 'site_id'),
|
), 'site'),
|
||||||
(VLAN.objects.restrict(request.user, 'view').filter(site=instance), 'site_id'),
|
(VLAN.objects.restrict(request.user, 'view').filter(site=instance), 'site_id'),
|
||||||
# Circuits
|
# Circuits
|
||||||
(Circuit.objects.restrict(request.user, 'view').filter(terminations__site=instance).distinct(), 'site_id'),
|
(Circuit.objects.restrict(request.user, 'view').filter(terminations__site=instance).distinct(), 'site_id'),
|
||||||
|
@ -187,11 +187,10 @@ class ReportViewSet(ViewSet):
|
|||||||
"""
|
"""
|
||||||
Compile all reports and their related results (if any). Result data is deferred in the list view.
|
Compile all reports and their related results (if any). Result data is deferred in the list view.
|
||||||
"""
|
"""
|
||||||
report_content_type = ContentType.objects.get(app_label='extras', model='report')
|
|
||||||
results = {
|
results = {
|
||||||
r.name: r
|
job.name: job
|
||||||
for r in Job.objects.filter(
|
for job in Job.objects.filter(
|
||||||
object_type=report_content_type,
|
object_type=ContentType.objects.get(app_label='extras', model='reportmodule'),
|
||||||
status__in=JobStatusChoices.TERMINAL_STATE_CHOICES
|
status__in=JobStatusChoices.TERMINAL_STATE_CHOICES
|
||||||
).order_by('name', '-created').distinct('name').defer('data')
|
).order_by('name', '-created').distinct('name').defer('data')
|
||||||
}
|
}
|
||||||
@ -202,7 +201,7 @@ class ReportViewSet(ViewSet):
|
|||||||
|
|
||||||
# Attach Job objects to each report (if any)
|
# Attach Job objects to each report (if any)
|
||||||
for report in report_list:
|
for report in report_list:
|
||||||
report.result = results.get(report.full_name, None)
|
report.result = results.get(report.name, None)
|
||||||
|
|
||||||
serializer = serializers.ReportSerializer(report_list, many=True, context={
|
serializer = serializers.ReportSerializer(report_list, many=True, context={
|
||||||
'request': request,
|
'request': request,
|
||||||
@ -290,12 +289,10 @@ class ScriptViewSet(ViewSet):
|
|||||||
return module, script
|
return module, script
|
||||||
|
|
||||||
def list(self, request):
|
def list(self, request):
|
||||||
|
|
||||||
script_content_type = ContentType.objects.get(app_label='extras', model='script')
|
|
||||||
results = {
|
results = {
|
||||||
r.name: r
|
job.name: job
|
||||||
for r in Job.objects.filter(
|
for job in Job.objects.filter(
|
||||||
object_type=script_content_type,
|
object_type=ContentType.objects.get(app_label='extras', model='scriptmodule'),
|
||||||
status__in=JobStatusChoices.TERMINAL_STATE_CHOICES
|
status__in=JobStatusChoices.TERMINAL_STATE_CHOICES
|
||||||
).order_by('name', '-created').distinct('name').defer('data')
|
).order_by('name', '-created').distinct('name').defer('data')
|
||||||
}
|
}
|
||||||
@ -306,7 +303,7 @@ class ScriptViewSet(ViewSet):
|
|||||||
|
|
||||||
# Attach Job objects to each script (if any)
|
# Attach Job objects to each script (if any)
|
||||||
for script in script_list:
|
for script in script_list:
|
||||||
script.result = results.get(script.full_name, None)
|
script.result = results.get(script.name, None)
|
||||||
|
|
||||||
serializer = serializers.ScriptSerializer(script_list, many=True, context={'request': request})
|
serializer = serializers.ScriptSerializer(script_list, many=True, context={'request': request})
|
||||||
|
|
||||||
|
@ -4,10 +4,12 @@ from hashlib import sha256
|
|||||||
from urllib.parse import urlencode
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
import feedparser
|
import feedparser
|
||||||
|
import requests
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
|
from django.db.models import Q
|
||||||
from django.template.loader import render_to_string
|
from django.template.loader import render_to_string
|
||||||
from django.urls import NoReverseMatch, reverse
|
from django.urls import NoReverseMatch, reverse
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
@ -33,7 +35,7 @@ def get_content_type_labels():
|
|||||||
return [
|
return [
|
||||||
(content_type_identifier(ct), content_type_name(ct))
|
(content_type_identifier(ct), content_type_name(ct))
|
||||||
for ct in ContentType.objects.filter(
|
for ct in ContentType.objects.filter(
|
||||||
FeatureQuery('export_templates').get_query()
|
FeatureQuery('export_templates').get_query() | Q(app_label='extras', model='objectchange')
|
||||||
).order_by('app_label', 'model')
|
).order_by('app_label', 'model')
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -227,7 +229,11 @@ class ObjectListWidget(DashboardWidget):
|
|||||||
htmx_url = reverse(viewname)
|
htmx_url = reverse(viewname)
|
||||||
except NoReverseMatch:
|
except NoReverseMatch:
|
||||||
htmx_url = None
|
htmx_url = None
|
||||||
if parameters := self.config.get('url_params'):
|
parameters = self.config.get('url_params') or {}
|
||||||
|
if page_size := self.config.get('page_size'):
|
||||||
|
parameters['per_page'] = page_size
|
||||||
|
|
||||||
|
if parameters:
|
||||||
try:
|
try:
|
||||||
htmx_url = f'{htmx_url}?{urlencode(parameters, doseq=True)}'
|
htmx_url = f'{htmx_url}?{urlencode(parameters, doseq=True)}'
|
||||||
except ValueError:
|
except ValueError:
|
||||||
@ -236,7 +242,6 @@ class ObjectListWidget(DashboardWidget):
|
|||||||
'viewname': viewname,
|
'viewname': viewname,
|
||||||
'has_permission': has_permission,
|
'has_permission': has_permission,
|
||||||
'htmx_url': htmx_url,
|
'htmx_url': htmx_url,
|
||||||
'page_size': self.config.get('page_size'),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@ -268,12 +273,9 @@ class RSSFeedWidget(DashboardWidget):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def render(self, request):
|
def render(self, request):
|
||||||
url = self.config['feed_url']
|
|
||||||
feed = self.get_feed()
|
|
||||||
|
|
||||||
return render_to_string(self.template_name, {
|
return render_to_string(self.template_name, {
|
||||||
'url': url,
|
'url': self.config['feed_url'],
|
||||||
'feed': feed,
|
**self.get_feed()
|
||||||
})
|
})
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
@ -285,17 +287,33 @@ class RSSFeedWidget(DashboardWidget):
|
|||||||
def get_feed(self):
|
def get_feed(self):
|
||||||
# Fetch RSS content from cache if available
|
# Fetch RSS content from cache if available
|
||||||
if feed_content := cache.get(self.cache_key):
|
if feed_content := cache.get(self.cache_key):
|
||||||
feed = feedparser.FeedParserDict(feed_content)
|
return {
|
||||||
else:
|
'feed': feedparser.FeedParserDict(feed_content),
|
||||||
feed = feedparser.parse(
|
}
|
||||||
self.config['feed_url'],
|
|
||||||
request_headers={'User-Agent': f'NetBox/{settings.VERSION}'}
|
|
||||||
)
|
|
||||||
if not feed.bozo:
|
|
||||||
# Cap number of entries
|
|
||||||
max_entries = self.config.get('max_entries')
|
|
||||||
feed['entries'] = feed['entries'][:max_entries]
|
|
||||||
# Cache the feed content
|
|
||||||
cache.set(self.cache_key, dict(feed), self.config.get('cache_timeout'))
|
|
||||||
|
|
||||||
return feed
|
# Fetch feed content from remote server
|
||||||
|
try:
|
||||||
|
response = requests.get(
|
||||||
|
url=self.config['feed_url'],
|
||||||
|
headers={'User-Agent': f'NetBox/{settings.VERSION}'},
|
||||||
|
proxies=settings.HTTP_PROXIES,
|
||||||
|
timeout=3
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
return {
|
||||||
|
'error': e,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Parse feed content
|
||||||
|
feed = feedparser.parse(response.content)
|
||||||
|
if not feed.bozo:
|
||||||
|
# Cap number of entries
|
||||||
|
max_entries = self.config.get('max_entries')
|
||||||
|
feed['entries'] = feed['entries'][:max_entries]
|
||||||
|
# Cache the feed content
|
||||||
|
cache.set(self.cache_key, dict(feed), self.config.get('cache_timeout'))
|
||||||
|
|
||||||
|
return {
|
||||||
|
'feed': feed,
|
||||||
|
}
|
||||||
|
@ -4,9 +4,10 @@ from django.contrib.postgres.forms import SimpleArrayField
|
|||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
from extras.choices import CustomFieldVisibilityChoices, CustomFieldTypeChoices
|
from extras.choices import CustomFieldVisibilityChoices, CustomFieldTypeChoices, JournalEntryKindChoices
|
||||||
from extras.models import *
|
from extras.models import *
|
||||||
from extras.utils import FeatureQuery
|
from extras.utils import FeatureQuery
|
||||||
|
from netbox.forms import NetBoxModelImportForm
|
||||||
from utilities.forms import CSVModelForm
|
from utilities.forms import CSVModelForm
|
||||||
from utilities.forms.fields import CSVChoiceField, CSVContentTypeField, CSVMultipleContentTypeField, SlugField
|
from utilities.forms.fields import CSVChoiceField, CSVContentTypeField, CSVMultipleContentTypeField, SlugField
|
||||||
|
|
||||||
@ -15,6 +16,7 @@ __all__ = (
|
|||||||
'CustomFieldImportForm',
|
'CustomFieldImportForm',
|
||||||
'CustomLinkImportForm',
|
'CustomLinkImportForm',
|
||||||
'ExportTemplateImportForm',
|
'ExportTemplateImportForm',
|
||||||
|
'JournalEntryImportForm',
|
||||||
'SavedFilterImportForm',
|
'SavedFilterImportForm',
|
||||||
'TagImportForm',
|
'TagImportForm',
|
||||||
'WebhookImportForm',
|
'WebhookImportForm',
|
||||||
@ -132,3 +134,20 @@ class TagImportForm(CSVModelForm):
|
|||||||
help_texts = {
|
help_texts = {
|
||||||
'color': mark_safe(_('RGB color in hexadecimal (e.g. <code>00ff00</code>)')),
|
'color': mark_safe(_('RGB color in hexadecimal (e.g. <code>00ff00</code>)')),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class JournalEntryImportForm(NetBoxModelImportForm):
|
||||||
|
assigned_object_type = CSVContentTypeField(
|
||||||
|
queryset=ContentType.objects.all(),
|
||||||
|
label=_('Assigned object type'),
|
||||||
|
)
|
||||||
|
kind = CSVChoiceField(
|
||||||
|
choices=JournalEntryKindChoices,
|
||||||
|
help_text=_('The classification of entry')
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = JournalEntry
|
||||||
|
fields = (
|
||||||
|
'assigned_object_type', 'assigned_object_id', 'created_by', 'kind', 'comments', 'tags'
|
||||||
|
)
|
||||||
|
@ -11,7 +11,7 @@ from extras.utils import FeatureQuery
|
|||||||
from netbox.forms.base import NetBoxModelFilterSetForm
|
from netbox.forms.base import NetBoxModelFilterSetForm
|
||||||
from tenancy.models import Tenant, TenantGroup
|
from tenancy.models import Tenant, TenantGroup
|
||||||
from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm, add_blank_choice
|
from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm, add_blank_choice
|
||||||
from utilities.forms.fields import ContentTypeMultipleChoiceField, DynamicModelMultipleChoiceField, TagFilterField
|
from utilities.forms.fields import ContentTypeChoiceField, ContentTypeMultipleChoiceField, DynamicModelMultipleChoiceField, TagFilterField
|
||||||
from utilities.forms.widgets import APISelectMultiple, DateTimePicker
|
from utilities.forms.widgets import APISelectMultiple, DateTimePicker
|
||||||
from virtualization.models import Cluster, ClusterGroup, ClusterType
|
from virtualization.models import Cluster, ClusterGroup, ClusterType
|
||||||
from .mixins import SavedFiltersMixin
|
from .mixins import SavedFiltersMixin
|
||||||
@ -22,6 +22,7 @@ __all__ = (
|
|||||||
'CustomFieldFilterForm',
|
'CustomFieldFilterForm',
|
||||||
'CustomLinkFilterForm',
|
'CustomLinkFilterForm',
|
||||||
'ExportTemplateFilterForm',
|
'ExportTemplateFilterForm',
|
||||||
|
'ImageAttachmentFilterForm',
|
||||||
'JournalEntryFilterForm',
|
'JournalEntryFilterForm',
|
||||||
'LocalConfigContextFilterForm',
|
'LocalConfigContextFilterForm',
|
||||||
'ObjectChangeFilterForm',
|
'ObjectChangeFilterForm',
|
||||||
@ -137,6 +138,20 @@ class ExportTemplateFilterForm(SavedFiltersMixin, FilterForm):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ImageAttachmentFilterForm(SavedFiltersMixin, FilterForm):
|
||||||
|
fieldsets = (
|
||||||
|
(None, ('q', 'filter_id')),
|
||||||
|
('Attributes', ('content_type_id', 'name',)),
|
||||||
|
)
|
||||||
|
content_type_id = ContentTypeChoiceField(
|
||||||
|
queryset=ContentType.objects.filter(FeatureQuery('custom_fields').get_query()),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
name = forms.CharField(
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class SavedFilterFilterForm(SavedFiltersMixin, FilterForm):
|
class SavedFilterFilterForm(SavedFiltersMixin, FilterForm):
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'filter_id')),
|
(None, ('q', 'filter_id')),
|
||||||
|
@ -111,7 +111,7 @@ class Command(BaseCommand):
|
|||||||
|
|
||||||
# Create the job
|
# Create the job
|
||||||
job = Job.objects.create(
|
job = Job.objects.create(
|
||||||
instance=module,
|
object=module,
|
||||||
name=script.name,
|
name=script.name,
|
||||||
user=User.objects.filter(is_superuser=True).order_by('pk')[0],
|
user=User.objects.filter(is_superuser=True).order_by('pk')[0],
|
||||||
job_id=uuid.uuid4()
|
job_id=uuid.uuid4()
|
||||||
|
@ -606,5 +606,18 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
|||||||
f"Invalid choice(s) ({', '.join(value)}). Available choices are: {', '.join(self.choices)}"
|
f"Invalid choice(s) ({', '.join(value)}). Available choices are: {', '.join(self.choices)}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Validate selected object
|
||||||
|
elif self.type == CustomFieldTypeChoices.TYPE_OBJECT:
|
||||||
|
if type(value) is not int:
|
||||||
|
raise ValidationError(f"Value must be an object ID, not {type(value).__name__}")
|
||||||
|
|
||||||
|
# Validate selected objects
|
||||||
|
elif self.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT:
|
||||||
|
if type(value) is not list:
|
||||||
|
raise ValidationError(f"Value must be a list of object IDs, not {type(value).__name__}")
|
||||||
|
for id in value:
|
||||||
|
if type(id) is not int:
|
||||||
|
raise ValidationError(f"Found invalid object ID: {id}")
|
||||||
|
|
||||||
elif self.required:
|
elif self.required:
|
||||||
raise ValidationError("Required field cannot be empty.")
|
raise ValidationError("Required field cannot be empty.")
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import inspect
|
import inspect
|
||||||
|
import logging
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
@ -16,6 +17,8 @@ __all__ = (
|
|||||||
'ScriptModule',
|
'ScriptModule',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
logger = logging.getLogger('netbox.data_backends')
|
||||||
|
|
||||||
|
|
||||||
class Script(WebhooksMixin, models.Model):
|
class Script(WebhooksMixin, models.Model):
|
||||||
"""
|
"""
|
||||||
@ -53,7 +56,12 @@ class ScriptModule(PythonModuleMixin, JobsMixin, ManagedFile):
|
|||||||
# For child objects in submodules use the full import path w/o the root module as the name
|
# For child objects in submodules use the full import path w/o the root module as the name
|
||||||
return cls.full_name.split(".", maxsplit=1)[1]
|
return cls.full_name.split(".", maxsplit=1)[1]
|
||||||
|
|
||||||
module = self.get_module()
|
try:
|
||||||
|
module = self.get_module()
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"Failed to load script: {self.python_name} error: {e}")
|
||||||
|
module = None
|
||||||
|
|
||||||
scripts = {}
|
scripts = {}
|
||||||
ordered = getattr(module, 'script_order', [])
|
ordered = getattr(module, 'script_order', [])
|
||||||
|
|
||||||
|
@ -13,6 +13,7 @@ __all__ = (
|
|||||||
'CustomFieldTable',
|
'CustomFieldTable',
|
||||||
'CustomLinkTable',
|
'CustomLinkTable',
|
||||||
'ExportTemplateTable',
|
'ExportTemplateTable',
|
||||||
|
'ImageAttachmentTable',
|
||||||
'JournalEntryTable',
|
'JournalEntryTable',
|
||||||
'ObjectChangeTable',
|
'ObjectChangeTable',
|
||||||
'SavedFilterTable',
|
'SavedFilterTable',
|
||||||
@ -29,6 +30,7 @@ class CustomFieldTable(NetBoxTable):
|
|||||||
content_types = columns.ContentTypesColumn()
|
content_types = columns.ContentTypesColumn()
|
||||||
required = columns.BooleanColumn()
|
required = columns.BooleanColumn()
|
||||||
ui_visibility = columns.ChoiceFieldColumn(verbose_name="UI visibility")
|
ui_visibility = columns.ChoiceFieldColumn(verbose_name="UI visibility")
|
||||||
|
description = columns.MarkdownColumn()
|
||||||
is_cloneable = columns.BooleanColumn()
|
is_cloneable = columns.BooleanColumn()
|
||||||
|
|
||||||
class Meta(NetBoxTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
@ -85,6 +87,28 @@ class ExportTemplateTable(NetBoxTable):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ImageAttachmentTable(NetBoxTable):
|
||||||
|
id = tables.Column(
|
||||||
|
linkify=False
|
||||||
|
)
|
||||||
|
content_type = columns.ContentTypeColumn()
|
||||||
|
parent = tables.Column(
|
||||||
|
linkify=True
|
||||||
|
)
|
||||||
|
size = tables.Column(
|
||||||
|
orderable=False,
|
||||||
|
verbose_name='Size (bytes)'
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta(NetBoxTable.Meta):
|
||||||
|
model = ImageAttachment
|
||||||
|
fields = (
|
||||||
|
'pk', 'content_type', 'parent', 'image', 'name', 'image_height', 'image_width', 'size', 'created',
|
||||||
|
'last_updated',
|
||||||
|
)
|
||||||
|
default_columns = ('content_type', 'parent', 'image', 'name', 'size', 'created')
|
||||||
|
|
||||||
|
|
||||||
class SavedFilterTable(NetBoxTable):
|
class SavedFilterTable(NetBoxTable):
|
||||||
name = tables.Column(
|
name = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
|
@ -73,6 +73,7 @@ urlpatterns = [
|
|||||||
path('config-templates/<int:pk>/', include(get_model_urls('extras', 'configtemplate'))),
|
path('config-templates/<int:pk>/', include(get_model_urls('extras', 'configtemplate'))),
|
||||||
|
|
||||||
# Image attachments
|
# Image attachments
|
||||||
|
path('image-attachments/', views.ImageAttachmentListView.as_view(), name='imageattachment_list'),
|
||||||
path('image-attachments/add/', views.ImageAttachmentEditView.as_view(), name='imageattachment_add'),
|
path('image-attachments/add/', views.ImageAttachmentEditView.as_view(), name='imageattachment_add'),
|
||||||
path('image-attachments/<int:pk>/', include(get_model_urls('extras', 'imageattachment'))),
|
path('image-attachments/<int:pk>/', include(get_model_urls('extras', 'imageattachment'))),
|
||||||
|
|
||||||
@ -81,6 +82,7 @@ urlpatterns = [
|
|||||||
path('journal-entries/add/', views.JournalEntryEditView.as_view(), name='journalentry_add'),
|
path('journal-entries/add/', views.JournalEntryEditView.as_view(), name='journalentry_add'),
|
||||||
path('journal-entries/edit/', views.JournalEntryBulkEditView.as_view(), name='journalentry_bulk_edit'),
|
path('journal-entries/edit/', views.JournalEntryBulkEditView.as_view(), name='journalentry_bulk_edit'),
|
||||||
path('journal-entries/delete/', views.JournalEntryBulkDeleteView.as_view(), name='journalentry_bulk_delete'),
|
path('journal-entries/delete/', views.JournalEntryBulkDeleteView.as_view(), name='journalentry_bulk_delete'),
|
||||||
|
path('journal-entries/import/', views.JournalEntryBulkImportView.as_view(), name='journalentry_import'),
|
||||||
path('journal-entries/<int:pk>/', include(get_model_urls('extras', 'journalentry'))),
|
path('journal-entries/<int:pk>/', include(get_model_urls('extras', 'journalentry'))),
|
||||||
|
|
||||||
# Change logging
|
# Change logging
|
||||||
|
@ -577,6 +577,14 @@ class ObjectChangeView(generic.ObjectView):
|
|||||||
# Image attachments
|
# Image attachments
|
||||||
#
|
#
|
||||||
|
|
||||||
|
class ImageAttachmentListView(generic.ObjectListView):
|
||||||
|
queryset = ImageAttachment.objects.all()
|
||||||
|
filterset = filtersets.ImageAttachmentFilterSet
|
||||||
|
filterset_form = forms.ImageAttachmentFilterForm
|
||||||
|
table = tables.ImageAttachmentTable
|
||||||
|
actions = ('export',)
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(ImageAttachment, 'edit')
|
@register_model_view(ImageAttachment, 'edit')
|
||||||
class ImageAttachmentEditView(generic.ObjectEditView):
|
class ImageAttachmentEditView(generic.ObjectEditView):
|
||||||
queryset = ImageAttachment.objects.all()
|
queryset = ImageAttachment.objects.all()
|
||||||
@ -617,7 +625,7 @@ class JournalEntryListView(generic.ObjectListView):
|
|||||||
filterset = filtersets.JournalEntryFilterSet
|
filterset = filtersets.JournalEntryFilterSet
|
||||||
filterset_form = forms.JournalEntryFilterForm
|
filterset_form = forms.JournalEntryFilterForm
|
||||||
table = tables.JournalEntryTable
|
table = tables.JournalEntryTable
|
||||||
actions = ('export', 'bulk_edit', 'bulk_delete')
|
actions = ('import', 'export', 'bulk_edit', 'bulk_delete')
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(JournalEntry)
|
@register_model_view(JournalEntry)
|
||||||
@ -666,6 +674,11 @@ class JournalEntryBulkDeleteView(generic.BulkDeleteView):
|
|||||||
table = tables.JournalEntryTable
|
table = tables.JournalEntryTable
|
||||||
|
|
||||||
|
|
||||||
|
class JournalEntryBulkImportView(generic.BulkImportView):
|
||||||
|
queryset = JournalEntry.objects.all()
|
||||||
|
model_form = forms.JournalEntryImportForm
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Dashboard & widgets
|
# Dashboard & widgets
|
||||||
#
|
#
|
||||||
@ -1033,7 +1046,6 @@ class ScriptView(ContentTypePermissionRequiredMixin, View):
|
|||||||
return 'extras.view_script'
|
return 'extras.view_script'
|
||||||
|
|
||||||
def get(self, request, module, name):
|
def get(self, request, module, name):
|
||||||
print(module)
|
|
||||||
module = get_object_or_404(ScriptModule.objects.restrict(request.user), file_path__startswith=module)
|
module = get_object_or_404(ScriptModule.objects.restrict(request.user), file_path__startswith=module)
|
||||||
script = module.scripts[name]()
|
script = module.scripts[name]()
|
||||||
form = script.as_form(initial=normalize_querydict(request.GET))
|
form = script.as_form(initial=normalize_querydict(request.GET))
|
||||||
|
@ -262,38 +262,21 @@ class IPRangeForm(TenancyForm, NetBoxModelForm):
|
|||||||
|
|
||||||
|
|
||||||
class IPAddressForm(TenancyForm, NetBoxModelForm):
|
class IPAddressForm(TenancyForm, NetBoxModelForm):
|
||||||
device = DynamicModelChoiceField(
|
|
||||||
queryset=Device.objects.all(),
|
|
||||||
required=False,
|
|
||||||
initial_params={
|
|
||||||
'interfaces': '$interface'
|
|
||||||
}
|
|
||||||
)
|
|
||||||
interface = DynamicModelChoiceField(
|
interface = DynamicModelChoiceField(
|
||||||
queryset=Interface.objects.all(),
|
queryset=Interface.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
query_params={
|
selector=True,
|
||||||
'device_id': '$device'
|
|
||||||
}
|
|
||||||
)
|
|
||||||
virtual_machine = DynamicModelChoiceField(
|
|
||||||
queryset=VirtualMachine.objects.all(),
|
|
||||||
required=False,
|
|
||||||
initial_params={
|
|
||||||
'interfaces': '$vminterface'
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
vminterface = DynamicModelChoiceField(
|
vminterface = DynamicModelChoiceField(
|
||||||
queryset=VMInterface.objects.all(),
|
queryset=VMInterface.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
|
selector=True,
|
||||||
label=_('Interface'),
|
label=_('Interface'),
|
||||||
query_params={
|
|
||||||
'virtual_machine_id': '$virtual_machine'
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
fhrpgroup = DynamicModelChoiceField(
|
fhrpgroup = DynamicModelChoiceField(
|
||||||
queryset=FHRPGroup.objects.all(),
|
queryset=FHRPGroup.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
|
selector=True,
|
||||||
label=_('FHRP Group')
|
label=_('FHRP Group')
|
||||||
)
|
)
|
||||||
vrf = DynamicModelChoiceField(
|
vrf = DynamicModelChoiceField(
|
||||||
@ -301,33 +284,11 @@ class IPAddressForm(TenancyForm, NetBoxModelForm):
|
|||||||
required=False,
|
required=False,
|
||||||
label=_('VRF')
|
label=_('VRF')
|
||||||
)
|
)
|
||||||
nat_device = DynamicModelChoiceField(
|
|
||||||
queryset=Device.objects.all(),
|
|
||||||
required=False,
|
|
||||||
selector=True,
|
|
||||||
label=_('Device')
|
|
||||||
)
|
|
||||||
nat_virtual_machine = DynamicModelChoiceField(
|
|
||||||
queryset=VirtualMachine.objects.all(),
|
|
||||||
required=False,
|
|
||||||
selector=True,
|
|
||||||
label=_('Virtual Machine')
|
|
||||||
)
|
|
||||||
nat_vrf = DynamicModelChoiceField(
|
|
||||||
queryset=VRF.objects.all(),
|
|
||||||
required=False,
|
|
||||||
selector=True,
|
|
||||||
label=_('VRF')
|
|
||||||
)
|
|
||||||
nat_inside = DynamicModelChoiceField(
|
nat_inside = DynamicModelChoiceField(
|
||||||
queryset=IPAddress.objects.all(),
|
queryset=IPAddress.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
|
selector=True,
|
||||||
label=_('IP Address'),
|
label=_('IP Address'),
|
||||||
query_params={
|
|
||||||
'device_id': '$nat_device',
|
|
||||||
'virtual_machine_id': '$nat_virtual_machine',
|
|
||||||
'vrf_id': '$nat_vrf',
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
primary_for_parent = forms.BooleanField(
|
primary_for_parent = forms.BooleanField(
|
||||||
required=False,
|
required=False,
|
||||||
@ -338,8 +299,8 @@ class IPAddressForm(TenancyForm, NetBoxModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = IPAddress
|
model = IPAddress
|
||||||
fields = [
|
fields = [
|
||||||
'address', 'vrf', 'status', 'role', 'dns_name', 'primary_for_parent', 'nat_device', 'nat_virtual_machine',
|
'address', 'vrf', 'status', 'role', 'dns_name', 'primary_for_parent', 'nat_inside', 'tenant_group',
|
||||||
'nat_vrf', 'nat_inside', 'tenant_group', 'tenant', 'description', 'comments', 'tags',
|
'tenant', 'description', 'comments', 'tags',
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@ -354,17 +315,6 @@ class IPAddressForm(TenancyForm, NetBoxModelForm):
|
|||||||
initial['vminterface'] = instance.assigned_object
|
initial['vminterface'] = instance.assigned_object
|
||||||
elif type(instance.assigned_object) is FHRPGroup:
|
elif type(instance.assigned_object) is FHRPGroup:
|
||||||
initial['fhrpgroup'] = instance.assigned_object
|
initial['fhrpgroup'] = instance.assigned_object
|
||||||
if instance.nat_inside:
|
|
||||||
nat_inside_parent = instance.nat_inside.assigned_object
|
|
||||||
if type(nat_inside_parent) is Interface:
|
|
||||||
initial['nat_site'] = nat_inside_parent.device.site.pk
|
|
||||||
if nat_inside_parent.device.rack:
|
|
||||||
initial['nat_rack'] = nat_inside_parent.device.rack.pk
|
|
||||||
initial['nat_device'] = nat_inside_parent.device.pk
|
|
||||||
elif type(nat_inside_parent) is VMInterface:
|
|
||||||
if cluster := nat_inside_parent.virtual_machine.cluster:
|
|
||||||
initial['nat_cluster'] = cluster.pk
|
|
||||||
initial['nat_virtual_machine'] = nat_inside_parent.virtual_machine.pk
|
|
||||||
kwargs['initial'] = initial
|
kwargs['initial'] = initial
|
||||||
|
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
@ -14,6 +14,7 @@ from utilities.views import ViewTab, register_model_view
|
|||||||
from virtualization.filtersets import VMInterfaceFilterSet
|
from virtualization.filtersets import VMInterfaceFilterSet
|
||||||
from virtualization.models import VMInterface
|
from virtualization.models import VMInterface
|
||||||
from . import filtersets, forms, tables
|
from . import filtersets, forms, tables
|
||||||
|
from .choices import PrefixStatusChoices
|
||||||
from .constants import *
|
from .constants import *
|
||||||
from .models import *
|
from .models import *
|
||||||
from .tables.l2vpn import L2VPNTable, L2VPNTerminationTable
|
from .tables.l2vpn import L2VPNTable, L2VPNTerminationTable
|
||||||
@ -495,7 +496,7 @@ class PrefixView(generic.ObjectView):
|
|||||||
|
|
||||||
# Parent prefixes table
|
# Parent prefixes table
|
||||||
parent_prefixes = Prefix.objects.restrict(request.user, 'view').filter(
|
parent_prefixes = Prefix.objects.restrict(request.user, 'view').filter(
|
||||||
Q(vrf=instance.vrf) | Q(vrf__isnull=True)
|
Q(vrf=instance.vrf) | Q(vrf__isnull=True, status=PrefixStatusChoices.STATUS_CONTAINER)
|
||||||
).filter(
|
).filter(
|
||||||
prefix__net_contains=str(instance.prefix)
|
prefix__net_contains=str(instance.prefix)
|
||||||
).prefetch_related(
|
).prefetch_related(
|
||||||
|
@ -14,35 +14,13 @@ __all__ = (
|
|||||||
|
|
||||||
class CustomFieldModelSerializer(serializers.Serializer):
|
class CustomFieldModelSerializer(serializers.Serializer):
|
||||||
"""
|
"""
|
||||||
Introduces support for custom field assignment. Adds `custom_fields` serialization and ensures
|
Introduces support for custom field assignment and representation.
|
||||||
that custom field data is populated upon initialization.
|
|
||||||
"""
|
"""
|
||||||
custom_fields = CustomFieldsDataField(
|
custom_fields = CustomFieldsDataField(
|
||||||
source='custom_field_data',
|
source='custom_field_data',
|
||||||
default=CreateOnlyDefault(CustomFieldDefaultValues())
|
default=CreateOnlyDefault(CustomFieldDefaultValues())
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
if self.instance is not None:
|
|
||||||
|
|
||||||
# Retrieve the set of CustomFields which apply to this type of object
|
|
||||||
content_type = ContentType.objects.get_for_model(self.Meta.model)
|
|
||||||
fields = CustomField.objects.filter(content_types=content_type)
|
|
||||||
|
|
||||||
# Populate custom field values for each instance from database
|
|
||||||
if type(self.instance) in (list, tuple):
|
|
||||||
for obj in self.instance:
|
|
||||||
self._populate_custom_fields(obj, fields)
|
|
||||||
else:
|
|
||||||
self._populate_custom_fields(self.instance, fields)
|
|
||||||
|
|
||||||
def _populate_custom_fields(self, instance, custom_fields):
|
|
||||||
instance.custom_fields = {}
|
|
||||||
for field in custom_fields:
|
|
||||||
instance.custom_fields[field.name] = instance.cf.get(field.name)
|
|
||||||
|
|
||||||
|
|
||||||
class TaggableModelSerializer(serializers.Serializer):
|
class TaggableModelSerializer(serializers.Serializer):
|
||||||
"""
|
"""
|
||||||
|
@ -13,6 +13,7 @@ ALLOWED_HOSTS = []
|
|||||||
# PostgreSQL database configuration. See the Django documentation for a complete list of available parameters:
|
# PostgreSQL database configuration. See the Django documentation for a complete list of available parameters:
|
||||||
# https://docs.djangoproject.com/en/stable/ref/settings/#databases
|
# https://docs.djangoproject.com/en/stable/ref/settings/#databases
|
||||||
DATABASE = {
|
DATABASE = {
|
||||||
|
'ENGINE': 'django.db.backends.postgresql', # Database engine
|
||||||
'NAME': 'netbox', # Database name
|
'NAME': 'netbox', # Database name
|
||||||
'USER': '', # PostgreSQL username
|
'USER': '', # PostgreSQL username
|
||||||
'PASSWORD': '', # PostgreSQL password
|
'PASSWORD': '', # PostgreSQL password
|
||||||
|
@ -67,8 +67,8 @@ class NetBoxModel(CloningMixin, NetBoxFeatureSet, models.Model):
|
|||||||
|
|
||||||
for field in self._meta.get_fields():
|
for field in self._meta.get_fields():
|
||||||
if isinstance(field, GenericForeignKey):
|
if isinstance(field, GenericForeignKey):
|
||||||
ct_value = getattr(self, field.ct_field)
|
ct_value = getattr(self, field.ct_field, None)
|
||||||
fk_value = getattr(self, field.fk_field)
|
fk_value = getattr(self, field.fk_field, None)
|
||||||
|
|
||||||
if ct_value is None and fk_value is not None:
|
if ct_value is None and fk_value is not None:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
|
@ -292,6 +292,7 @@ CUSTOMIZATION_MENU = Menu(
|
|||||||
get_model_item('extras', 'exporttemplate', _('Export Templates')),
|
get_model_item('extras', 'exporttemplate', _('Export Templates')),
|
||||||
get_model_item('extras', 'savedfilter', _('Saved Filters')),
|
get_model_item('extras', 'savedfilter', _('Saved Filters')),
|
||||||
get_model_item('extras', 'tag', 'Tags'),
|
get_model_item('extras', 'tag', 'Tags'),
|
||||||
|
get_model_item('extras', 'imageattachment', _('Image Attachments'), actions=()),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
MenuGroup(
|
MenuGroup(
|
||||||
@ -336,7 +337,7 @@ OPERATIONS_MENU = Menu(
|
|||||||
MenuGroup(
|
MenuGroup(
|
||||||
label=_('Logging'),
|
label=_('Logging'),
|
||||||
items=(
|
items=(
|
||||||
get_model_item('extras', 'journalentry', _('Journal Entries'), actions=[]),
|
get_model_item('extras', 'journalentry', _('Journal Entries'), actions=['import']),
|
||||||
get_model_item('extras', 'objectchange', _('Change Log'), actions=[]),
|
get_model_item('extras', 'objectchange', _('Change Log'), actions=[]),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -25,7 +25,7 @@ from netbox.constants import RQ_QUEUE_DEFAULT, RQ_QUEUE_HIGH, RQ_QUEUE_LOW
|
|||||||
# Environment setup
|
# Environment setup
|
||||||
#
|
#
|
||||||
|
|
||||||
VERSION = '3.5.0'
|
VERSION = '3.5.1'
|
||||||
|
|
||||||
# Hostname
|
# Hostname
|
||||||
HOSTNAME = platform.node()
|
HOSTNAME = platform.node()
|
||||||
@ -182,15 +182,16 @@ if RELEASE_CHECK_URL:
|
|||||||
# Database
|
# Database
|
||||||
#
|
#
|
||||||
|
|
||||||
# Only PostgreSQL is supported
|
if 'ENGINE' not in DATABASE:
|
||||||
if METRICS_ENABLED:
|
# Only PostgreSQL is supported
|
||||||
DATABASE.update({
|
if METRICS_ENABLED:
|
||||||
'ENGINE': 'django_prometheus.db.backends.postgresql'
|
DATABASE.update({
|
||||||
})
|
'ENGINE': 'django_prometheus.db.backends.postgresql'
|
||||||
else:
|
})
|
||||||
DATABASE.update({
|
else:
|
||||||
'ENGINE': 'django.db.backends.postgresql'
|
DATABASE.update({
|
||||||
})
|
'ENGINE': 'django.db.backends.postgresql'
|
||||||
|
})
|
||||||
|
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
'default': DATABASE,
|
'default': DATABASE,
|
||||||
@ -616,13 +617,15 @@ REST_FRAMEWORK = {
|
|||||||
#
|
#
|
||||||
|
|
||||||
SPECTACULAR_SETTINGS = {
|
SPECTACULAR_SETTINGS = {
|
||||||
'TITLE': 'NetBox API',
|
'TITLE': 'NetBox REST API',
|
||||||
'DESCRIPTION': 'API to access NetBox',
|
|
||||||
'LICENSE': {'name': 'Apache v2 License'},
|
'LICENSE': {'name': 'Apache v2 License'},
|
||||||
'VERSION': VERSION,
|
'VERSION': VERSION,
|
||||||
'COMPONENT_SPLIT_REQUEST': True,
|
'COMPONENT_SPLIT_REQUEST': True,
|
||||||
'REDOC_DIST': 'SIDECAR',
|
'REDOC_DIST': 'SIDECAR',
|
||||||
'SERVERS': [{'url': f'/{BASE_PATH}api'}],
|
'SERVERS': [{
|
||||||
|
'url': BASE_PATH,
|
||||||
|
'description': 'NetBox',
|
||||||
|
}],
|
||||||
'SWAGGER_UI_DIST': 'SIDECAR',
|
'SWAGGER_UI_DIST': 'SIDECAR',
|
||||||
'SWAGGER_UI_FAVICON_HREF': 'SIDECAR',
|
'SWAGGER_UI_FAVICON_HREF': 'SIDECAR',
|
||||||
'POSTPROCESSING_HOOKS': [],
|
'POSTPROCESSING_HOOKS': [],
|
||||||
|
BIN
netbox/project-static/dist/netbox-dark.css
vendored
BIN
netbox/project-static/dist/netbox-dark.css
vendored
Binary file not shown.
BIN
netbox/project-static/dist/netbox-light.css
vendored
BIN
netbox/project-static/dist/netbox-light.css
vendored
Binary file not shown.
BIN
netbox/project-static/dist/netbox-print.css
vendored
BIN
netbox/project-static/dist/netbox-print.css
vendored
Binary file not shown.
@ -231,6 +231,10 @@ table {
|
|||||||
|
|
||||||
p {
|
p {
|
||||||
// Remove spacing from paragraph elements within tables.
|
// Remove spacing from paragraph elements within tables.
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
p:last-child {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -29,17 +29,6 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
|
||||||
<th scope="row">
|
|
||||||
Account <i
|
|
||||||
class="mdi mdi-alert-box text-warning"
|
|
||||||
data-bs-toggle="tooltip"
|
|
||||||
data-bs-placement="right"
|
|
||||||
title="This field has been deprecated, and will be removed in NetBox v3.5."
|
|
||||||
></i>
|
|
||||||
</th>
|
|
||||||
<td>{{ object.account|placeholder }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Description</th>
|
<th scope="row">Description</th>
|
||||||
<td>{{ object.description|placeholder }}</td>
|
<td>{{ object.description|placeholder }}</td>
|
||||||
|
@ -32,7 +32,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Description</th>
|
<th scope="row">Description</th>
|
||||||
<td>{{ object.description|placeholder }}</td>
|
<td>{{ object.description|markdown|placeholder }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Required</th>
|
<th scope="row">Required</th>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{% if htmx_url and has_permission %}
|
{% if htmx_url and has_permission %}
|
||||||
<div class="htmx-container" hx-get="{{ htmx_url }}{% if page_size %}?per_page={{ page_size }}{% endif %}" hx-trigger="load"></div>
|
<div class="htmx-container" hx-get="{{ htmx_url }}" hx-trigger="load"></div>
|
||||||
{% elif htmx_url %}
|
{% elif htmx_url %}
|
||||||
<div class="text-muted text-center">
|
<div class="text-muted text-center">
|
||||||
<i class="mdi mdi-lock-outline"></i> No permission to view this content.
|
<i class="mdi mdi-lock-outline"></i> No permission to view this content.
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
{% if not feed.bozo %}
|
{% if feed and not feed.bozo %}
|
||||||
<div class="list-group list-group-flush">
|
<div class="list-group list-group-flush">
|
||||||
{% for entry in feed.entries %}
|
{% for entry in feed.entries %}
|
||||||
<div class="list-group-item px-1">
|
<div class="list-group-item px-1">
|
||||||
@ -16,7 +16,9 @@
|
|||||||
<span class="text-danger">
|
<span class="text-danger">
|
||||||
<i class="mdi mdi-alert"></i> There was a problem fetching the RSS feed:
|
<i class="mdi mdi-alert"></i> There was a problem fetching the RSS feed:
|
||||||
</span>
|
</span>
|
||||||
<pre class="m-2">
|
{% if feed %}
|
||||||
Response status: {{ feed.status }}
|
{{ feed.bozo_exception|escape }} (HTTP {{ feed.status }})
|
||||||
Error: {{ feed.bozo_exception|escape }}</pre>
|
{% else %}
|
||||||
|
{{ error }}
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -37,43 +37,49 @@
|
|||||||
</h5>
|
</h5>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
{% include 'inc/sync_warning.html' with object=module %}
|
{% include 'inc/sync_warning.html' with object=module %}
|
||||||
<table class="table table-hover table-headings reports">
|
{% if not module.scripts %}
|
||||||
<thead>
|
<div class="alert alert-warning d-flex align-items-center" role="alert">
|
||||||
<tr>
|
<i class="mdi mdi-alert"></i> Script file at: {{module.full_path}} could not be loaded.
|
||||||
<th width="250">Name</th>
|
</div>
|
||||||
<th>Description</th>
|
{% else %}
|
||||||
<th>Last Run</th>
|
<table class="table table-hover table-headings reports">
|
||||||
<th class="text-end">Status</th>
|
<thead>
|
||||||
</tr>
|
<tr>
|
||||||
</thead>
|
<th width="250">Name</th>
|
||||||
<tbody>
|
<th>Description</th>
|
||||||
{% with jobs=module.get_latest_jobs %}
|
<th>Last Run</th>
|
||||||
{% for script_name, script_class in module.scripts.items %}
|
<th class="text-end">Status</th>
|
||||||
<tr>
|
</tr>
|
||||||
<td>
|
</thead>
|
||||||
<a href="{% url 'extras:script' module=module.python_name name=script_name %}" name="script.{{ script_name }}">{{ script_class.name }}</a>
|
<tbody>
|
||||||
</td>
|
{% with jobs=module.get_latest_jobs %}
|
||||||
<td>
|
{% for script_name, script_class in module.scripts.items %}
|
||||||
{{ script_class.Meta.description|markdown|placeholder }}
|
<tr>
|
||||||
</td>
|
<td>
|
||||||
{% with last_result=jobs|get_key:script_class.name %}
|
<a href="{% url 'extras:script' module=module.python_name name=script_name %}" name="script.{{ script_name }}">{{ script_class.name }}</a>
|
||||||
{% if last_result %}
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="{% url 'extras:script_result' job_pk=last_result.pk %}">{{ last_result.created|annotated_date }}</a>
|
{{ script_class.Meta.description|markdown|placeholder }}
|
||||||
</td>
|
</td>
|
||||||
<td class="text-end">
|
{% with last_result=jobs|get_key:script_class.name %}
|
||||||
{% badge last_result.get_status_display last_result.get_status_color %}
|
{% if last_result %}
|
||||||
</td>
|
<td>
|
||||||
{% else %}
|
<a href="{% url 'extras:script_result' job_pk=last_result.pk %}">{{ last_result.created|annotated_date }}</a>
|
||||||
<td class="text-muted">Never</td>
|
</td>
|
||||||
<td class="text-end">{{ ''|placeholder }}</td>
|
<td class="text-end">
|
||||||
{% endif %}
|
{% badge last_result.get_status_display last_result.get_status_color %}
|
||||||
{% endwith %}
|
</td>
|
||||||
</tr>
|
{% else %}
|
||||||
{% endfor %}
|
<td class="text-muted">Never</td>
|
||||||
{% endwith %}
|
<td class="text-end">{{ ''|placeholder }}</td>
|
||||||
</tbody>
|
{% endif %}
|
||||||
</table>
|
{% endwith %}
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
{% endwith %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
|
@ -4,44 +4,9 @@
|
|||||||
<h5 class="card-header">
|
<h5 class="card-header">
|
||||||
Images
|
Images
|
||||||
</h5>
|
</h5>
|
||||||
<div class="card-body">
|
<div class="card-body htmx-container table-responsive"
|
||||||
{% with images=object.images.all %}
|
hx-get="{% url 'extras:imageattachment_list' %}?content_type_id={{ object|content_type_id }}&object_id={{ object.pk }}"
|
||||||
{% if images.exists %}
|
hx-trigger="load"></div>
|
||||||
<table class="table table-hover">
|
|
||||||
<tr>
|
|
||||||
<th>Name</th>
|
|
||||||
<th>Size</th>
|
|
||||||
<th>Created</th>
|
|
||||||
<th></th>
|
|
||||||
</tr>
|
|
||||||
{% for attachment in images %}
|
|
||||||
<tr{% if not attachment.size %} class="table-danger"{% endif %}>
|
|
||||||
<td>
|
|
||||||
<i class="mdi mdi-file-image-outline"></i>
|
|
||||||
<a class="image-preview" href="{{ attachment.image.url }}" target="_blank">{{ attachment }}</a>
|
|
||||||
</td>
|
|
||||||
<td>{{ attachment.size|filesizeformat }}</td>
|
|
||||||
<td>{{ attachment.created|annotated_date }}</td>
|
|
||||||
<td class="text-end noprint">
|
|
||||||
{% if perms.extras.change_imageattachment %}
|
|
||||||
<a href="{% url 'extras:imageattachment_edit' pk=attachment.pk %}" class="btn btn-warning btn-sm lh-1" title="Edit Image">
|
|
||||||
<i class="mdi mdi-pencil" aria-hidden="true"></i>
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
{% if perms.extras.delete_imageattachment %}
|
|
||||||
<a href="{% url 'extras:imageattachment_delete' pk=attachment.pk %}" class="btn btn-danger btn-sm lh-1" title="Delete Image">
|
|
||||||
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i>
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</table>
|
|
||||||
{% else %}
|
|
||||||
<div class="text-muted">None</div>
|
|
||||||
{% endif %}
|
|
||||||
{% endwith %}
|
|
||||||
</div>
|
|
||||||
{% if perms.extras.add_imageattachment %}
|
{% if perms.extras.add_imageattachment %}
|
||||||
<div class="card-footer text-end noprint">
|
<div class="card-footer text-end noprint">
|
||||||
<a href="{% url 'extras:imageattachment_add' %}?content_type={{ object|content_type_id }}&object_id={{ object.pk }}" class="btn btn-primary btn-sm">
|
<a href="{% url 'extras:imageattachment_add' %}?content_type={{ object|content_type_id }}&object_id={{ object.pk }}" class="btn btn-primary btn-sm">
|
||||||
|
@ -56,11 +56,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="tab-content p-0 border-0">
|
<div class="tab-content p-0 border-0">
|
||||||
<div class="tab-pane {% if not form.initial.vminterface and not form.initial.fhrpgroup %}active{% endif %}" id="device" role="tabpanel" aria-labeled-by="device_tab">
|
<div class="tab-pane {% if not form.initial.vminterface and not form.initial.fhrpgroup %}active{% endif %}" id="device" role="tabpanel" aria-labeled-by="device_tab">
|
||||||
{% render_field form.device %}
|
|
||||||
{% render_field form.interface %}
|
{% render_field form.interface %}
|
||||||
</div>
|
</div>
|
||||||
<div class="tab-pane {% if form.initial.vminterface %}active{% endif %}" id="vm" role="tabpanel" aria-labeled-by="vm_tab">
|
<div class="tab-pane {% if form.initial.vminterface %}active{% endif %}" id="vm" role="tabpanel" aria-labeled-by="vm_tab">
|
||||||
{% render_field form.virtual_machine %}
|
|
||||||
{% render_field form.vminterface %}
|
{% render_field form.vminterface %}
|
||||||
</div>
|
</div>
|
||||||
<div class="tab-pane {% if form.initial.fhrpgroup %}active{% endif %}" id="fhrpgroup" role="tabpanel" aria-labeled-by="fhrpgroup_tab">
|
<div class="tab-pane {% if form.initial.fhrpgroup %}active{% endif %}" id="fhrpgroup" role="tabpanel" aria-labeled-by="fhrpgroup_tab">
|
||||||
@ -75,60 +73,6 @@
|
|||||||
<h5 class="offset-sm-3">NAT IP (Inside)</h5>
|
<h5 class="offset-sm-3">NAT IP (Inside)</h5>
|
||||||
</div>
|
</div>
|
||||||
<div class="row mb-2">
|
<div class="row mb-2">
|
||||||
<div class="offset-sm-3">
|
|
||||||
<ul class="nav nav-pills" role="tablist">
|
|
||||||
<li class="nav-item" role="presentation">
|
|
||||||
<button
|
|
||||||
role="tab"
|
|
||||||
type="button"
|
|
||||||
id="device_tab"
|
|
||||||
data-bs-toggle="tab"
|
|
||||||
class="nav-link active"
|
|
||||||
data-bs-target="#by_device"
|
|
||||||
aria-controls="by_device"
|
|
||||||
>
|
|
||||||
By Device
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item" role="presentation">
|
|
||||||
<button
|
|
||||||
role="tab"
|
|
||||||
type="button"
|
|
||||||
id="vm_tab"
|
|
||||||
data-bs-toggle="tab"
|
|
||||||
class="nav-link"
|
|
||||||
data-bs-target="#by_vm"
|
|
||||||
aria-controls="by_vm"
|
|
||||||
>
|
|
||||||
By VM
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item" role="presentation">
|
|
||||||
<button
|
|
||||||
role="tab"
|
|
||||||
type="button"
|
|
||||||
id="vrf_tab"
|
|
||||||
data-bs-toggle="tab"
|
|
||||||
class="nav-link"
|
|
||||||
data-bs-target="#by_vrf"
|
|
||||||
aria-controls="by_vrf"
|
|
||||||
>
|
|
||||||
By IP
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="tab-content p-0 border-0">
|
|
||||||
<div class="tab-pane active" id="by_device" aria-labelledby="device_tab" role="tabpanel">
|
|
||||||
{% render_field form.nat_device %}
|
|
||||||
</div>
|
|
||||||
<div class="tab-pane" id="by_vm" aria-labelledby="vm_tab" role="tabpanel">
|
|
||||||
{% render_field form.nat_virtual_machine %}
|
|
||||||
</div>
|
|
||||||
<div class="tab-pane" id="by_vrf" aria-labelledby="vrf_tab" role="tabpanel">
|
|
||||||
{% render_field form.nat_vrf %}
|
|
||||||
</div>
|
|
||||||
{% render_field form.nat_inside %}
|
{% render_field form.nat_inside %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -352,6 +352,7 @@ class ContactAssignmentListView(generic.ObjectListView):
|
|||||||
filterset = filtersets.ContactAssignmentFilterSet
|
filterset = filtersets.ContactAssignmentFilterSet
|
||||||
filterset_form = forms.ContactAssignmentFilterForm
|
filterset_form = forms.ContactAssignmentFilterForm
|
||||||
table = tables.ContactAssignmentTable
|
table = tables.ContactAssignmentTable
|
||||||
|
actions = ('export', 'bulk_edit', 'bulk_delete')
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(ContactAssignment, 'edit')
|
@register_model_view(ContactAssignment, 'edit')
|
||||||
|
@ -126,7 +126,11 @@ class VMInterfaceSerializer(NetBoxModelSerializer):
|
|||||||
l2vpn_termination = NestedL2VPNTerminationSerializer(read_only=True, allow_null=True)
|
l2vpn_termination = NestedL2VPNTerminationSerializer(read_only=True, allow_null=True)
|
||||||
count_ipaddresses = serializers.IntegerField(read_only=True)
|
count_ipaddresses = serializers.IntegerField(read_only=True)
|
||||||
count_fhrp_groups = serializers.IntegerField(read_only=True)
|
count_fhrp_groups = serializers.IntegerField(read_only=True)
|
||||||
mac_address = serializers.CharField(required=False, default=None)
|
mac_address = serializers.CharField(
|
||||||
|
required=False,
|
||||||
|
default=None,
|
||||||
|
allow_null=True
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = VMInterface
|
model = VMInterface
|
||||||
|
@ -1,35 +1,35 @@
|
|||||||
bleach==6.0.0
|
bleach==6.0.0
|
||||||
boto3==1.26.121
|
boto3==1.26.127
|
||||||
Django==4.1.8
|
Django==4.1.9
|
||||||
django-cors-headers==3.14.0
|
django-cors-headers==3.14.0
|
||||||
django-debug-toolbar==4.0.0
|
django-debug-toolbar==4.0.0
|
||||||
django-filter==23.1
|
django-filter==23.2
|
||||||
django-graphiql-debug-toolbar==0.2.0
|
django-graphiql-debug-toolbar==0.2.0
|
||||||
django-mptt==0.14
|
django-mptt==0.14
|
||||||
django-pglocks==1.0.4
|
django-pglocks==1.0.4
|
||||||
django-prometheus==2.2.0
|
django-prometheus==2.3.1
|
||||||
django-redis==5.2.0
|
django-redis==5.2.0
|
||||||
django-rich==1.5.0
|
django-rich==1.5.0
|
||||||
django-rq==2.7.0
|
django-rq==2.8.0
|
||||||
django-tables2==2.5.3
|
django-tables2==2.5.3
|
||||||
django-taggit==3.1.0
|
django-taggit==4.0.0
|
||||||
django-timezone-field==5.0
|
django-timezone-field==5.0
|
||||||
djangorestframework==3.14.0
|
djangorestframework==3.14.0
|
||||||
drf-spectacular==0.26.2
|
drf-spectacular==0.26.2
|
||||||
drf-spectacular-sidecar==2023.4.1
|
drf-spectacular-sidecar==2023.5.1
|
||||||
dulwich==0.21.3
|
dulwich==0.21.5
|
||||||
feedparser==6.0.10
|
feedparser==6.0.10
|
||||||
graphene-django==3.0.0
|
graphene-django==3.0.0
|
||||||
gunicorn==20.1.0
|
gunicorn==20.1.0
|
||||||
Jinja2==3.1.2
|
Jinja2==3.1.2
|
||||||
Markdown==3.3.7
|
Markdown==3.3.7
|
||||||
mkdocs-material==9.1.8
|
mkdocs-material==9.1.9
|
||||||
mkdocstrings[python-legacy]==0.21.2
|
mkdocstrings[python-legacy]==0.21.2
|
||||||
netaddr==0.8.0
|
netaddr==0.8.0
|
||||||
Pillow==9.5.0
|
Pillow==9.5.0
|
||||||
psycopg2-binary==2.9.6
|
psycopg2-binary==2.9.6
|
||||||
PyYAML==6.0
|
PyYAML==6.0
|
||||||
sentry-sdk==1.21.0
|
sentry-sdk==1.22.1
|
||||||
social-auth-app-django==5.2.0
|
social-auth-app-django==5.2.0
|
||||||
social-auth-core[openidconnect]==4.4.2
|
social-auth-core[openidconnect]==4.4.2
|
||||||
svgwrite==1.4.3
|
svgwrite==1.4.3
|
||||||
|
Loading…
Reference in New Issue
Block a user