mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-24 09:28:38 -06:00
commit
c9b79ca579
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.1
|
placeholder: v3.5.2
|
||||||
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.1
|
placeholder: v3.5.2
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
|
10
README.md
10
README.md
@ -1,11 +1,13 @@
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
|
<strong>The :ballot_box_with_check: <a href="https://forms.gle/zUHrrPo7K34yKaqC9">2023 NetBox Community Survey</a> is now open!</strong>
|
||||||
|
<p>Please take a few minutes to tell us about your NetBox deployment.</p>
|
||||||
|
|
||||||
<img src="https://raw.githubusercontent.com/netbox-community/netbox/develop/docs/netbox_logo.svg" width="400" alt="NetBox logo" />
|
<img src="https://raw.githubusercontent.com/netbox-community/netbox/develop/docs/netbox_logo.svg" width="400" alt="NetBox logo" />
|
||||||
|
<p>The premiere source of truth powering network automation</p>
|
||||||
The premiere source of truth powering network automation
|
<img src="https://github.com/netbox-community/netbox/workflows/CI/badge.svg?branch=master" alt="CI status" />
|
||||||
|
<p></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
NetBox is the leading solution for modeling and documenting modern networks. By
|
NetBox is the leading solution for modeling and documenting modern networks. By
|
||||||
combining the traditional disciplines of IP address management (IPAM) and
|
combining the traditional disciplines of IP address management (IPAM) and
|
||||||
datacenter infrastructure management (DCIM) with powerful APIs and extensions,
|
datacenter infrastructure management (DCIM) with powerful APIs and extensions,
|
||||||
|
@ -153,15 +153,10 @@ New objects can be created by instantiating the desired model, defining values f
|
|||||||
```
|
```
|
||||||
>>> lab1 = Site.objects.get(pk=7)
|
>>> lab1 = Site.objects.get(pk=7)
|
||||||
>>> myvlan = VLAN(vid=123, name='MyNewVLAN', site=lab1)
|
>>> myvlan = VLAN(vid=123, name='MyNewVLAN', site=lab1)
|
||||||
|
>>> myvlan.full_clean()
|
||||||
>>> myvlan.save()
|
>>> myvlan.save()
|
||||||
```
|
```
|
||||||
|
|
||||||
Alternatively, the above can be performed as a single operation. (Note, however, that `save()` does _not_ return the new instance for reuse.)
|
|
||||||
|
|
||||||
```
|
|
||||||
>>> VLAN(vid=123, name='MyNewVLAN', site=Site.objects.get(pk=7)).save()
|
|
||||||
```
|
|
||||||
|
|
||||||
To modify an existing object, we retrieve it, update the desired field(s), and call `save()` again.
|
To modify an existing object, we retrieve it, update the desired field(s), and call `save()` again.
|
||||||
|
|
||||||
```
|
```
|
||||||
@ -169,6 +164,7 @@ To modify an existing object, we retrieve it, update the desired field(s), and c
|
|||||||
>>> vlan.name
|
>>> vlan.name
|
||||||
'MyNewVLAN'
|
'MyNewVLAN'
|
||||||
>>> vlan.name = 'BetterName'
|
>>> vlan.name = 'BetterName'
|
||||||
|
>>> vlan.full_clean()
|
||||||
>>> vlan.save()
|
>>> vlan.save()
|
||||||
>>> VLAN.objects.get(pk=1280).name
|
>>> VLAN.objects.get(pk=1280).name
|
||||||
'BetterName'
|
'BetterName'
|
||||||
|
@ -29,6 +29,17 @@ This defines custom content to be displayed on the login page above the login fo
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## BANNER_MAINTENANCE
|
||||||
|
|
||||||
|
!!! tip "Dynamic Configuration Parameter"
|
||||||
|
|
||||||
|
!!! note
|
||||||
|
This parameter was added in NetBox v3.5.
|
||||||
|
|
||||||
|
This adds a banner to the top of every page when maintenance mode is enabled. HTML is allowed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## BANNER_TOP
|
## BANNER_TOP
|
||||||
|
|
||||||
!!! tip "Dynamic Configuration Parameter"
|
!!! tip "Dynamic Configuration Parameter"
|
||||||
@ -129,7 +140,7 @@ Setting this to True will display a "maintenance mode" banner at the top of ever
|
|||||||
|
|
||||||
Default: `https://maps.google.com/?q=` (Google Maps)
|
Default: `https://maps.google.com/?q=` (Google Maps)
|
||||||
|
|
||||||
This specifies the URL to use when presenting a map of a physical location by street address or GPS coordinates. The URL must accept either a free-form street address or a comma-separated pair of numeric coordinates appended to it.
|
This specifies the URL to use when presenting a map of a physical location by street address or GPS coordinates. The URL must accept either a free-form street address or a comma-separated pair of numeric coordinates appended to it. Set this to `None` to disable the "map it" button within the UI.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -193,3 +204,25 @@ This parameter defines the URL of the repository that will be checked for new Ne
|
|||||||
Default: `300`
|
Default: `300`
|
||||||
|
|
||||||
The maximum execution time of a background task (such as running a custom script), in seconds.
|
The maximum execution time of a background task (such as running a custom script), in seconds.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## RQ_RETRY_INTERVAL
|
||||||
|
|
||||||
|
!!! note
|
||||||
|
This parameter was added in NetBox v3.5.
|
||||||
|
|
||||||
|
Default: `60`
|
||||||
|
|
||||||
|
This parameter controls how frequently a failed job is retried, up to the maximum number of times specified by `RQ_RETRY_MAX`. This must be either an integer specifying the number of seconds to wait between successive attempts, or a list of such values. For example, `[60, 300, 3600]` will retry the task after 1 minute, 5 minutes, and 1 hour.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## RQ_RETRY_MAX
|
||||||
|
|
||||||
|
!!! note
|
||||||
|
This parameter was added in NetBox v3.5.
|
||||||
|
|
||||||
|
Default: `0` (retries disabled)
|
||||||
|
|
||||||
|
The maximum number of times a background task will be retried before being marked as failed.
|
||||||
|
@ -4,6 +4,14 @@ The configuration parameters listed here control remote authentication for NetBo
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## REMOTE_AUTH_AUTO_CREATE_GROUPS
|
||||||
|
|
||||||
|
Default: `False`
|
||||||
|
|
||||||
|
If true, NetBox will automatically create groups specified in the `REMOTE_AUTH_GROUP_HEADER` header if they don't already exist. (Requires `REMOTE_AUTH_ENABLED`.)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## REMOTE_AUTH_AUTO_CREATE_USER
|
## REMOTE_AUTH_AUTO_CREATE_USER
|
||||||
|
|
||||||
Default: `False`
|
Default: `False`
|
||||||
|
@ -378,6 +378,7 @@ class NewBranchScript(Script):
|
|||||||
slug=slugify(data['site_name']),
|
slug=slugify(data['site_name']),
|
||||||
status=SiteStatusChoices.STATUS_PLANNED
|
status=SiteStatusChoices.STATUS_PLANNED
|
||||||
)
|
)
|
||||||
|
site.full_clean()
|
||||||
site.save()
|
site.save()
|
||||||
self.log_success(f"Created new site: {site}")
|
self.log_success(f"Created new site: {site}")
|
||||||
|
|
||||||
@ -391,6 +392,7 @@ class NewBranchScript(Script):
|
|||||||
status=DeviceStatusChoices.STATUS_PLANNED,
|
status=DeviceStatusChoices.STATUS_PLANNED,
|
||||||
device_role=switch_role
|
device_role=switch_role
|
||||||
)
|
)
|
||||||
|
switch.full_clean()
|
||||||
switch.save()
|
switch.save()
|
||||||
self.log_success(f"Created new switch: {switch}")
|
self.log_success(f"Created new switch: {switch}")
|
||||||
|
|
||||||
|
@ -100,6 +100,8 @@ Create a system user account named `netbox`. We'll configure the WSGI and HTTP s
|
|||||||
```
|
```
|
||||||
sudo adduser --system --group netbox
|
sudo adduser --system --group netbox
|
||||||
sudo chown --recursive netbox /opt/netbox/netbox/media/
|
sudo chown --recursive netbox /opt/netbox/netbox/media/
|
||||||
|
sudo chown --recursive netbox /opt/netbox/netbox/reports/
|
||||||
|
sudo chown --recursive netbox /opt/netbox/netbox/scripts/
|
||||||
```
|
```
|
||||||
|
|
||||||
=== "CentOS"
|
=== "CentOS"
|
||||||
@ -108,6 +110,8 @@ Create a system user account named `netbox`. We'll configure the WSGI and HTTP s
|
|||||||
sudo groupadd --system netbox
|
sudo groupadd --system netbox
|
||||||
sudo adduser --system -g netbox netbox
|
sudo adduser --system -g netbox netbox
|
||||||
sudo chown --recursive netbox /opt/netbox/netbox/media/
|
sudo chown --recursive netbox /opt/netbox/netbox/media/
|
||||||
|
sudo chown --recursive netbox /opt/netbox/netbox/reports/
|
||||||
|
sudo chown --recursive netbox /opt/netbox/netbox/scripts/
|
||||||
```
|
```
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
@ -15,7 +15,7 @@ sudo apt install -y libldap2-dev libsasl2-dev libssl-dev
|
|||||||
On CentOS:
|
On CentOS:
|
||||||
|
|
||||||
```no-highlight
|
```no-highlight
|
||||||
sudo yum install -y openldap-devel
|
sudo yum install -y openldap-devel python3-devel
|
||||||
```
|
```
|
||||||
|
|
||||||
### Install django-auth-ldap
|
### Install django-auth-ldap
|
||||||
|
@ -63,7 +63,7 @@ Each attribute of the IP address is expressed as an attribute of the JSON object
|
|||||||
|
|
||||||
## Interactive Documentation
|
## Interactive Documentation
|
||||||
|
|
||||||
Comprehensive, interactive documentation of all REST API endpoints is available on a running NetBox instance at `/api/docs/`. This interface provides a convenient sandbox for researching and experimenting with specific endpoints and request types. The API itself can also be explored using a web browser by navigating to its root at `/api/`.
|
Comprehensive, interactive documentation of all REST API endpoints is available on a running NetBox instance at `/api/schema/swagger-ui/`. This interface provides a convenient sandbox for researching and experimenting with specific endpoints and request types. The API itself can also be explored using a web browser by navigating to its root at `/api/`.
|
||||||
|
|
||||||
## Endpoint Hierarchy
|
## Endpoint Hierarchy
|
||||||
|
|
||||||
|
@ -1,5 +1,41 @@
|
|||||||
# NetBox v3.5
|
# NetBox v3.5
|
||||||
|
|
||||||
|
## v3.5.2 (2023-05-22)
|
||||||
|
|
||||||
|
### Enhancements
|
||||||
|
|
||||||
|
* [#7671](https://github.com/netbox-community/netbox/issues/7671) - Introduce `REMOTE_AUTH_AUTO_CREATE_GROUPS` config parameter to enable the automatic creation of new groups when remote authentication is in use
|
||||||
|
* [#9068](https://github.com/netbox-community/netbox/issues/9068) - Disallow the assignment of network/broadcast IP addresses to interfaces
|
||||||
|
* [#11017](https://github.com/netbox-community/netbox/issues/11017) - Increase the maximum values for allocated and maximum power draws
|
||||||
|
* [#11233](https://github.com/netbox-community/netbox/issues/11233) - Intercept and cleanly report errors upon attempted database writes when maintenance mode is enabled
|
||||||
|
* [#11599](https://github.com/netbox-community/netbox/issues/11599) - Move contacts panels to separate tabs under object views
|
||||||
|
* [#11670](https://github.com/netbox-community/netbox/issues/11670) - Enable setting device type & module type weight via bulk import
|
||||||
|
* [#11900](https://github.com/netbox-community/netbox/issues/11900) - Add an outline to the reservation markers on rack elevations
|
||||||
|
* [#12131](https://github.com/netbox-community/netbox/issues/12131) - Show custom field description as an icon tooltip under object views
|
||||||
|
* [#12223](https://github.com/netbox-community/netbox/issues/12223) - Add columns for parent device bay and position to devices list
|
||||||
|
* [#12233](https://github.com/netbox-community/netbox/issues/12233) - Move related IP addresses table to a separate tab
|
||||||
|
* [#12286](https://github.com/netbox-community/netbox/issues/12286) - Show height and total weight under device view
|
||||||
|
* [#12323](https://github.com/netbox-community/netbox/issues/12323) - Add 100GE CXP interface type
|
||||||
|
* [#12327](https://github.com/netbox-community/netbox/issues/12327) - Introduce the ability to automatically retry failed background jobs
|
||||||
|
* [#12498](https://github.com/netbox-community/netbox/issues/12498) - Hide map button if `MAPS_URL` is empty
|
||||||
|
* [#12548](https://github.com/netbox-community/netbox/issues/12548) - Optimize REST API performance when retrieving interfaces with L2VPN assignments
|
||||||
|
* [#12554](https://github.com/netbox-community/netbox/issues/12554) - Allow customization or disabling of the maintenance mode banner
|
||||||
|
* [#12605](https://github.com/netbox-community/netbox/issues/12605) - Add LX.5 port types
|
||||||
|
* [#12629](https://github.com/netbox-community/netbox/issues/12629) - Add 400GE CDFP and CFP8 interface types
|
||||||
|
* [#12678](https://github.com/netbox-community/netbox/issues/12678) - Add 200GE QSFP-DD interface type
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* [#10686](https://github.com/netbox-community/netbox/issues/10686) - Enable specifying termination object by virtual chassis master when importing cables
|
||||||
|
* [#11619](https://github.com/netbox-community/netbox/issues/11619) - Enable assigning VLANs without a site to interfaces during bulk edit
|
||||||
|
* [#12468](https://github.com/netbox-community/netbox/issues/12468) - Custom field names should not permit double underscores
|
||||||
|
* [#12550](https://github.com/netbox-community/netbox/issues/12550) - Fix rear port selection widget under front port creation form
|
||||||
|
* [#12570](https://github.com/netbox-community/netbox/issues/12570) - Disable ordering of synchronized object tables by the "synced" attribute
|
||||||
|
* [#12594](https://github.com/netbox-community/netbox/issues/12594) - Enable selecting config context as object type in object counts dashboard widget
|
||||||
|
* [#12642](https://github.com/netbox-community/netbox/issues/12642) - Fix bulk tenant assignment via cluster import form
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## v3.5.1 (2023-05-05)
|
## v3.5.1 (2023-05-05)
|
||||||
|
|
||||||
### Enhancements
|
### Enhancements
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.db.models import Q
|
|
||||||
from django.shortcuts import get_object_or_404, redirect, render
|
from django.shortcuts import get_object_or_404, redirect, render
|
||||||
|
|
||||||
from dcim.views import PathTraceView
|
from dcim.views import PathTraceView
|
||||||
from netbox.views import generic
|
from netbox.views import generic
|
||||||
|
from tenancy.views import ObjectContactsView
|
||||||
from utilities.forms import ConfirmationForm
|
from utilities.forms import ConfirmationForm
|
||||||
from utilities.utils import count_related
|
from utilities.utils import count_related
|
||||||
from utilities.views import register_model_view
|
from utilities.views import register_model_view
|
||||||
@ -73,6 +73,11 @@ class ProviderBulkDeleteView(generic.BulkDeleteView):
|
|||||||
table = tables.ProviderTable
|
table = tables.ProviderTable
|
||||||
|
|
||||||
|
|
||||||
|
@register_model_view(Provider, 'contacts')
|
||||||
|
class ProviderContactsView(ObjectContactsView):
|
||||||
|
queryset = Provider.objects.all()
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# ProviderAccounts
|
# ProviderAccounts
|
||||||
#
|
#
|
||||||
@ -134,6 +139,11 @@ class ProviderAccountBulkDeleteView(generic.BulkDeleteView):
|
|||||||
table = tables.ProviderAccountTable
|
table = tables.ProviderAccountTable
|
||||||
|
|
||||||
|
|
||||||
|
@register_model_view(ProviderAccount, 'contacts')
|
||||||
|
class ProviderAccountContactsView(ObjectContactsView):
|
||||||
|
queryset = ProviderAccount.objects.all()
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Provider networks
|
# Provider networks
|
||||||
#
|
#
|
||||||
@ -389,6 +399,11 @@ class CircuitSwapTerminations(generic.ObjectEditView):
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@register_model_view(Circuit, 'contacts')
|
||||||
|
class CircuitContactsView(ObjectContactsView):
|
||||||
|
queryset = Circuit.objects.all()
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Circuit terminations
|
# Circuit terminations
|
||||||
#
|
#
|
||||||
|
@ -16,7 +16,7 @@ from extras.utils import FeatureQuery
|
|||||||
from netbox.config import get_config
|
from netbox.config import get_config
|
||||||
from netbox.constants import RQ_QUEUE_DEFAULT
|
from netbox.constants import RQ_QUEUE_DEFAULT
|
||||||
from utilities.querysets import RestrictedQuerySet
|
from utilities.querysets import RestrictedQuerySet
|
||||||
from utilities.rqworker import get_queue_for_model
|
from utilities.rqworker import get_queue_for_model, get_rq_retry
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'Job',
|
'Job',
|
||||||
@ -219,5 +219,6 @@ class Job(models.Model):
|
|||||||
event=event,
|
event=event,
|
||||||
data=self.data,
|
data=self.data,
|
||||||
timestamp=str(timezone.now()),
|
timestamp=str(timezone.now()),
|
||||||
username=self.user.username
|
username=self.user.username,
|
||||||
|
retry=get_rq_retry()
|
||||||
)
|
)
|
||||||
|
@ -493,7 +493,8 @@ class PowerOutletViewSet(PathEndpointMixin, NetBoxModelViewSet):
|
|||||||
class InterfaceViewSet(PathEndpointMixin, NetBoxModelViewSet):
|
class InterfaceViewSet(PathEndpointMixin, NetBoxModelViewSet):
|
||||||
queryset = Interface.objects.prefetch_related(
|
queryset = Interface.objects.prefetch_related(
|
||||||
'device', 'module__module_bay', 'parent', 'bridge', 'lag', '_path', 'cable__terminations', 'wireless_lans',
|
'device', 'module__module_bay', 'parent', 'bridge', 'lag', '_path', 'cable__terminations', 'wireless_lans',
|
||||||
'untagged_vlan', 'tagged_vlans', 'vrf', 'ip_addresses', 'fhrp_group_assignments', 'tags'
|
'untagged_vlan', 'tagged_vlans', 'vrf', 'ip_addresses', 'fhrp_group_assignments', 'tags', 'l2vpn_terminations',
|
||||||
|
'vdcs',
|
||||||
)
|
)
|
||||||
serializer_class = serializers.InterfaceSerializer
|
serializer_class = serializers.InterfaceSerializer
|
||||||
filterset_class = filtersets.InterfaceFilterSet
|
filterset_class = filtersets.InterfaceFilterSet
|
||||||
|
@ -807,12 +807,16 @@ class InterfaceTypeChoices(ChoiceSet):
|
|||||||
TYPE_100GE_CFP = '100gbase-x-cfp'
|
TYPE_100GE_CFP = '100gbase-x-cfp'
|
||||||
TYPE_100GE_CFP2 = '100gbase-x-cfp2'
|
TYPE_100GE_CFP2 = '100gbase-x-cfp2'
|
||||||
TYPE_100GE_CFP4 = '100gbase-x-cfp4'
|
TYPE_100GE_CFP4 = '100gbase-x-cfp4'
|
||||||
|
TYPE_100GE_CXP = '100gbase-x-cxp'
|
||||||
TYPE_100GE_CPAK = '100gbase-x-cpak'
|
TYPE_100GE_CPAK = '100gbase-x-cpak'
|
||||||
TYPE_100GE_QSFP28 = '100gbase-x-qsfp28'
|
TYPE_100GE_QSFP28 = '100gbase-x-qsfp28'
|
||||||
TYPE_200GE_CFP2 = '200gbase-x-cfp2'
|
TYPE_200GE_CFP2 = '200gbase-x-cfp2'
|
||||||
TYPE_200GE_QSFP56 = '200gbase-x-qsfp56'
|
TYPE_200GE_QSFP56 = '200gbase-x-qsfp56'
|
||||||
|
TYPE_200GE_QSFP_DD = '200gbase-x-qsfpdd'
|
||||||
TYPE_400GE_QSFP_DD = '400gbase-x-qsfpdd'
|
TYPE_400GE_QSFP_DD = '400gbase-x-qsfpdd'
|
||||||
TYPE_400GE_OSFP = '400gbase-x-osfp'
|
TYPE_400GE_OSFP = '400gbase-x-osfp'
|
||||||
|
TYPE_400GE_CDFP = '400gbase-x-cdfp'
|
||||||
|
TYPE_400GE_CFP8 = '400gbase-x-cfp8'
|
||||||
TYPE_800GE_QSFP_DD = '800gbase-x-qsfpdd'
|
TYPE_800GE_QSFP_DD = '800gbase-x-qsfpdd'
|
||||||
TYPE_800GE_OSFP = '800gbase-x-osfp'
|
TYPE_800GE_OSFP = '800gbase-x-osfp'
|
||||||
|
|
||||||
@ -952,11 +956,15 @@ class InterfaceTypeChoices(ChoiceSet):
|
|||||||
(TYPE_100GE_CFP2, 'CFP2 (100GE)'),
|
(TYPE_100GE_CFP2, 'CFP2 (100GE)'),
|
||||||
(TYPE_200GE_CFP2, 'CFP2 (200GE)'),
|
(TYPE_200GE_CFP2, 'CFP2 (200GE)'),
|
||||||
(TYPE_100GE_CFP4, 'CFP4 (100GE)'),
|
(TYPE_100GE_CFP4, 'CFP4 (100GE)'),
|
||||||
|
(TYPE_100GE_CXP, 'CXP (100GE)'),
|
||||||
(TYPE_100GE_CPAK, 'Cisco CPAK (100GE)'),
|
(TYPE_100GE_CPAK, 'Cisco CPAK (100GE)'),
|
||||||
(TYPE_100GE_QSFP28, 'QSFP28 (100GE)'),
|
(TYPE_100GE_QSFP28, 'QSFP28 (100GE)'),
|
||||||
(TYPE_200GE_QSFP56, 'QSFP56 (200GE)'),
|
(TYPE_200GE_QSFP56, 'QSFP56 (200GE)'),
|
||||||
|
(TYPE_200GE_QSFP_DD, 'QSFP-DD (200GE)'),
|
||||||
(TYPE_400GE_QSFP_DD, 'QSFP-DD (400GE)'),
|
(TYPE_400GE_QSFP_DD, 'QSFP-DD (400GE)'),
|
||||||
(TYPE_400GE_OSFP, 'OSFP (400GE)'),
|
(TYPE_400GE_OSFP, 'OSFP (400GE)'),
|
||||||
|
(TYPE_400GE_CDFP, 'CDFP (400GE)'),
|
||||||
|
(TYPE_400GE_CFP8, 'CPF8 (400GE)'),
|
||||||
(TYPE_800GE_QSFP_DD, 'QSFP-DD (800GE)'),
|
(TYPE_800GE_QSFP_DD, 'QSFP-DD (800GE)'),
|
||||||
(TYPE_800GE_OSFP, 'OSFP (800GE)'),
|
(TYPE_800GE_OSFP, 'OSFP (800GE)'),
|
||||||
)
|
)
|
||||||
@ -1221,6 +1229,10 @@ class PortTypeChoices(ChoiceSet):
|
|||||||
TYPE_LSH_PC = 'lsh-pc'
|
TYPE_LSH_PC = 'lsh-pc'
|
||||||
TYPE_LSH_UPC = 'lsh-upc'
|
TYPE_LSH_UPC = 'lsh-upc'
|
||||||
TYPE_LSH_APC = 'lsh-apc'
|
TYPE_LSH_APC = 'lsh-apc'
|
||||||
|
TYPE_LX5 = 'lx5'
|
||||||
|
TYPE_LX5_PC = 'lx5-pc'
|
||||||
|
TYPE_LX5_UPC = 'lx5-upc'
|
||||||
|
TYPE_LX5_APC = 'lx5-apc'
|
||||||
TYPE_SPLICE = 'splice'
|
TYPE_SPLICE = 'splice'
|
||||||
TYPE_CS = 'cs'
|
TYPE_CS = 'cs'
|
||||||
TYPE_SN = 'sn'
|
TYPE_SN = 'sn'
|
||||||
@ -1267,6 +1279,10 @@ class PortTypeChoices(ChoiceSet):
|
|||||||
(TYPE_LSH_PC, 'LSH/PC'),
|
(TYPE_LSH_PC, 'LSH/PC'),
|
||||||
(TYPE_LSH_UPC, 'LSH/UPC'),
|
(TYPE_LSH_UPC, 'LSH/UPC'),
|
||||||
(TYPE_LSH_APC, 'LSH/APC'),
|
(TYPE_LSH_APC, 'LSH/APC'),
|
||||||
|
(TYPE_LX5, 'LX.5'),
|
||||||
|
(TYPE_LX5_PC, 'LX.5/PC'),
|
||||||
|
(TYPE_LX5_UPC, 'LX.5/UPC'),
|
||||||
|
(TYPE_LX5_APC, 'LX.5/APC'),
|
||||||
(TYPE_MPO, 'MPO'),
|
(TYPE_MPO, 'MPO'),
|
||||||
(TYPE_MTRJ, 'MTRJ'),
|
(TYPE_MTRJ, 'MTRJ'),
|
||||||
(TYPE_SC, 'SC'),
|
(TYPE_SC, 'SC'),
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from timezone_field import TimeZoneFormField
|
from timezone_field import TimeZoneFormField
|
||||||
@ -1292,8 +1293,13 @@ class InterfaceBulkEditForm(
|
|||||||
break
|
break
|
||||||
|
|
||||||
if site is not None:
|
if site is not None:
|
||||||
self.fields['untagged_vlan'].widget.add_query_param('site_id', site.pk)
|
# Query for VLANs assigned to the same site and VLANs with no site assigned (null).
|
||||||
self.fields['tagged_vlans'].widget.add_query_param('site_id', site.pk)
|
self.fields['untagged_vlan'].widget.add_query_param(
|
||||||
|
'site_id', [site.pk, settings.FILTERS_NULL_CHOICE_VALUE]
|
||||||
|
)
|
||||||
|
self.fields['tagged_vlans'].widget.add_query_param(
|
||||||
|
'site_id', [site.pk, settings.FILTERS_NULL_CHOICE_VALUE]
|
||||||
|
)
|
||||||
|
|
||||||
self.fields['parent'].choices = ()
|
self.fields['parent'].choices = ()
|
||||||
self.fields['parent'].widget.attrs['disabled'] = True
|
self.fields['parent'].widget.attrs['disabled'] = True
|
||||||
|
@ -292,12 +292,21 @@ class DeviceTypeImportForm(NetBoxModelImportForm):
|
|||||||
required=False,
|
required=False,
|
||||||
help_text=_('The default platform for devices of this type (optional)')
|
help_text=_('The default platform for devices of this type (optional)')
|
||||||
)
|
)
|
||||||
|
weight = forms.DecimalField(
|
||||||
|
required=False,
|
||||||
|
help_text=_('Device weight'),
|
||||||
|
)
|
||||||
|
weight_unit = CSVChoiceField(
|
||||||
|
choices=WeightUnitChoices,
|
||||||
|
required=False,
|
||||||
|
help_text=_('Unit for device weight')
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = DeviceType
|
model = DeviceType
|
||||||
fields = [
|
fields = [
|
||||||
'manufacturer', 'default_platform', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth',
|
'manufacturer', 'default_platform', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth',
|
||||||
'subdevice_role', 'airflow', 'description', 'comments',
|
'subdevice_role', 'airflow', 'description', 'weight', 'weight_unit', 'comments',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -306,10 +315,19 @@ class ModuleTypeImportForm(NetBoxModelImportForm):
|
|||||||
queryset=Manufacturer.objects.all(),
|
queryset=Manufacturer.objects.all(),
|
||||||
to_field_name='name'
|
to_field_name='name'
|
||||||
)
|
)
|
||||||
|
weight = forms.DecimalField(
|
||||||
|
required=False,
|
||||||
|
help_text=_('Module weight'),
|
||||||
|
)
|
||||||
|
weight_unit = CSVChoiceField(
|
||||||
|
choices=WeightUnitChoices,
|
||||||
|
required=False,
|
||||||
|
help_text=_('Unit for module weight')
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ModuleType
|
model = ModuleType
|
||||||
fields = ['manufacturer', 'model', 'part_number', 'description', 'comments']
|
fields = ['manufacturer', 'model', 'part_number', 'description', 'weight', 'weight_unit', 'comments']
|
||||||
|
|
||||||
|
|
||||||
class DeviceRoleImportForm(NetBoxModelImportForm):
|
class DeviceRoleImportForm(NetBoxModelImportForm):
|
||||||
@ -1060,7 +1078,11 @@ class CableImportForm(NetBoxModelImportForm):
|
|||||||
|
|
||||||
model = content_type.model_class()
|
model = content_type.model_class()
|
||||||
try:
|
try:
|
||||||
termination_object = model.objects.get(device=device, name=name)
|
if device.virtual_chassis and device.virtual_chassis.master == device and \
|
||||||
|
model.objects.filter(device=device, name=name).count() == 0:
|
||||||
|
termination_object = model.objects.get(device__in=device.virtual_chassis.members.all(), name=name)
|
||||||
|
else:
|
||||||
|
termination_object = model.objects.get(device=device, name=name)
|
||||||
if termination_object.cable is not None:
|
if termination_object.cable is not None:
|
||||||
raise forms.ValidationError(f"Side {side.upper()}: {device} {termination_object} is already connected")
|
raise forms.ValidationError(f"Side {side.upper()}: {device} {termination_object} is already connected")
|
||||||
except ObjectDoesNotExist:
|
except ObjectDoesNotExist:
|
||||||
|
@ -1214,7 +1214,7 @@ class PopulateDeviceBayForm(BootstrapMixin, forms.Form):
|
|||||||
installed_device = forms.ModelChoiceField(
|
installed_device = forms.ModelChoiceField(
|
||||||
queryset=Device.objects.all(),
|
queryset=Device.objects.all(),
|
||||||
label=_('Child Device'),
|
label=_('Child Device'),
|
||||||
help_text=_("Child devices must first be created and assigned to the site/rack of the parent device.")
|
help_text=_("Child devices must first be created and assigned to the site and rack of the parent device.")
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, device_bay, *args, **kwargs):
|
def __init__(self, device_bay, *args, **kwargs):
|
||||||
|
@ -242,6 +242,7 @@ class FrontPortCreateForm(ComponentCreateForm, model_forms.FrontPortForm):
|
|||||||
choices=[],
|
choices=[],
|
||||||
label=_('Rear ports'),
|
label=_('Rear ports'),
|
||||||
help_text=_('Select one rear port assignment for each front port being created.'),
|
help_text=_('Select one rear port assignment for each front port being created.'),
|
||||||
|
widget=forms.SelectMultiple(attrs={'size': 6})
|
||||||
)
|
)
|
||||||
|
|
||||||
# Override fieldsets from FrontPortForm to omit rear_port_position
|
# Override fieldsets from FrontPortForm to omit rear_port_position
|
||||||
|
42
netbox/dcim/migrations/0172_larger_power_draw_values.py
Normal file
42
netbox/dcim/migrations/0172_larger_power_draw_values.py
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
# Generated by Django 4.1.9 on 2023-05-12 18:46
|
||||||
|
|
||||||
|
import django.core.validators
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('dcim', '0171_cabletermination_change_logging'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='powerport',
|
||||||
|
name='allocated_draw',
|
||||||
|
field=models.PositiveIntegerField(
|
||||||
|
blank=True, null=True, validators=[django.core.validators.MinValueValidator(1)]
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='powerport',
|
||||||
|
name='maximum_draw',
|
||||||
|
field=models.PositiveIntegerField(
|
||||||
|
blank=True, null=True, validators=[django.core.validators.MinValueValidator(1)]
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='powerporttemplate',
|
||||||
|
name='allocated_draw',
|
||||||
|
field=models.PositiveIntegerField(
|
||||||
|
blank=True, null=True, validators=[django.core.validators.MinValueValidator(1)]
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='powerporttemplate',
|
||||||
|
name='maximum_draw',
|
||||||
|
field=models.PositiveIntegerField(
|
||||||
|
blank=True, null=True, validators=[django.core.validators.MinValueValidator(1)]
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
@ -232,13 +232,13 @@ class PowerPortTemplate(ModularComponentTemplateModel):
|
|||||||
choices=PowerPortTypeChoices,
|
choices=PowerPortTypeChoices,
|
||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
maximum_draw = models.PositiveSmallIntegerField(
|
maximum_draw = models.PositiveIntegerField(
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
validators=[MinValueValidator(1)],
|
validators=[MinValueValidator(1)],
|
||||||
help_text=_("Maximum power draw (watts)")
|
help_text=_("Maximum power draw (watts)")
|
||||||
)
|
)
|
||||||
allocated_draw = models.PositiveSmallIntegerField(
|
allocated_draw = models.PositiveIntegerField(
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
validators=[MinValueValidator(1)],
|
validators=[MinValueValidator(1)],
|
||||||
|
@ -329,13 +329,13 @@ class PowerPort(ModularComponentModel, CabledObjectModel, PathEndpoint):
|
|||||||
blank=True,
|
blank=True,
|
||||||
help_text=_('Physical port type')
|
help_text=_('Physical port type')
|
||||||
)
|
)
|
||||||
maximum_draw = models.PositiveSmallIntegerField(
|
maximum_draw = models.PositiveIntegerField(
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
validators=[MinValueValidator(1)],
|
validators=[MinValueValidator(1)],
|
||||||
help_text=_("Maximum power draw (watts)")
|
help_text=_("Maximum power draw (watts)")
|
||||||
)
|
)
|
||||||
allocated_draw = models.PositiveSmallIntegerField(
|
allocated_draw = models.PositiveIntegerField(
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
validators=[MinValueValidator(1)],
|
validators=[MinValueValidator(1)],
|
||||||
|
@ -184,6 +184,8 @@ class DeviceType(PrimaryModel, WeightMixin):
|
|||||||
'subdevice_role': self.subdevice_role,
|
'subdevice_role': self.subdevice_role,
|
||||||
'airflow': self.airflow,
|
'airflow': self.airflow,
|
||||||
'comments': self.comments,
|
'comments': self.comments,
|
||||||
|
'weight': float(self.weight) if self.weight is not None else None,
|
||||||
|
'weight_unit': self.weight_unit,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Component templates
|
# Component templates
|
||||||
@ -361,6 +363,8 @@ class ModuleType(PrimaryModel, WeightMixin):
|
|||||||
'model': self.model,
|
'model': self.model,
|
||||||
'part_number': self.part_number,
|
'part_number': self.part_number,
|
||||||
'comments': self.comments,
|
'comments': self.comments,
|
||||||
|
'weight': float(self.weight) if self.weight is not None else None,
|
||||||
|
'weight_unit': self.weight_unit,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Component templates
|
# Component templates
|
||||||
|
@ -22,6 +22,11 @@ __all__ = (
|
|||||||
'RackElevationSVG',
|
'RackElevationSVG',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
GRADIENT_RESERVED = '#b0b0ff'
|
||||||
|
GRADIENT_OCCUPIED = '#d7d7d7'
|
||||||
|
GRADIENT_BLOCKED = '#ffc0c0'
|
||||||
|
STROKE_RESERVED = '#4d4dff'
|
||||||
|
|
||||||
|
|
||||||
def get_device_name(device):
|
def get_device_name(device):
|
||||||
if device.virtual_chassis:
|
if device.virtual_chassis:
|
||||||
@ -132,9 +137,9 @@ class RackElevationSVG:
|
|||||||
drawing.defs.add(drawing.style(css_file.read()))
|
drawing.defs.add(drawing.style(css_file.read()))
|
||||||
|
|
||||||
# Add gradients
|
# Add gradients
|
||||||
RackElevationSVG._add_gradient(drawing, 'reserved', '#b0b0ff')
|
RackElevationSVG._add_gradient(drawing, 'reserved', GRADIENT_RESERVED)
|
||||||
RackElevationSVG._add_gradient(drawing, 'occupied', '#d7d7d7')
|
RackElevationSVG._add_gradient(drawing, 'occupied', GRADIENT_OCCUPIED)
|
||||||
RackElevationSVG._add_gradient(drawing, 'blocked', '#ffc0c0')
|
RackElevationSVG._add_gradient(drawing, 'blocked', GRADIENT_BLOCKED)
|
||||||
|
|
||||||
return drawing
|
return drawing
|
||||||
|
|
||||||
@ -246,13 +251,13 @@ class RackElevationSVG:
|
|||||||
coords = self._get_device_coords(segment[0], u_height)
|
coords = self._get_device_coords(segment[0], u_height)
|
||||||
coords = (coords[0] + self.unit_width + RACK_ELEVATION_BORDER_WIDTH * 2, coords[1])
|
coords = (coords[0] + self.unit_width + RACK_ELEVATION_BORDER_WIDTH * 2, coords[1])
|
||||||
size = (
|
size = (
|
||||||
self.margin_width,
|
self.margin_width - 3,
|
||||||
u_height * self.unit_height
|
u_height * self.unit_height
|
||||||
)
|
)
|
||||||
link = Hyperlink(href=f'{self.base_url}{reservation.get_absolute_url()}', target='_parent')
|
link = Hyperlink(href=f'{self.base_url}{reservation.get_absolute_url()}', target='_parent')
|
||||||
link.set_desc(f'Reservation #{reservation.pk}: {reservation.description}')
|
link.set_desc(f'Reservation #{reservation.pk}: {reservation.description}')
|
||||||
link.add(
|
link.add(
|
||||||
Rect(coords, size, class_='reservation')
|
Rect(coords, size, class_='reservation', stroke=STROKE_RESERVED, stroke_width=2)
|
||||||
)
|
)
|
||||||
self.drawing.add(link)
|
self.drawing.add(link)
|
||||||
|
|
||||||
|
@ -216,6 +216,16 @@ class DeviceTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
|
|||||||
config_template = tables.Column(
|
config_template = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
|
parent_device = tables.Column(
|
||||||
|
verbose_name='Parent Device',
|
||||||
|
linkify=True,
|
||||||
|
accessor='parent_bay__device'
|
||||||
|
)
|
||||||
|
device_bay_position = tables.Column(
|
||||||
|
verbose_name='Position (Device Bay)',
|
||||||
|
accessor='parent_bay',
|
||||||
|
linkify=True
|
||||||
|
)
|
||||||
comments = columns.MarkdownColumn()
|
comments = columns.MarkdownColumn()
|
||||||
tags = columns.TagColumn(
|
tags = columns.TagColumn(
|
||||||
url_name='dcim:device_list'
|
url_name='dcim:device_list'
|
||||||
@ -225,9 +235,10 @@ class DeviceTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
|
|||||||
model = models.Device
|
model = models.Device
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'name', 'status', 'tenant', 'tenant_group', 'device_role', 'manufacturer', 'device_type',
|
'pk', 'id', 'name', 'status', 'tenant', 'tenant_group', 'device_role', 'manufacturer', 'device_type',
|
||||||
'platform', 'serial', 'asset_tag', 'region', 'site_group', 'site', 'location', 'rack', 'position', 'face',
|
'platform', 'serial', 'asset_tag', 'region', 'site_group', 'site', 'location', 'rack', 'parent_device',
|
||||||
'airflow', 'primary_ip', 'primary_ip4', 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position',
|
'device_bay_position', 'position', 'face', 'airflow', 'primary_ip', 'primary_ip4', 'primary_ip6', 'cluster',
|
||||||
'vc_priority', 'description', 'config_template', 'comments', 'contacts', 'tags', 'created', 'last_updated',
|
'virtual_chassis', 'vc_position', 'vc_priority', 'description', 'config_template', 'comments', 'contacts',
|
||||||
|
'tags', 'created', 'last_updated',
|
||||||
)
|
)
|
||||||
default_columns = (
|
default_columns = (
|
||||||
'pk', 'name', 'status', 'tenant', 'site', 'location', 'rack', 'device_role', 'manufacturer', 'device_type',
|
'pk', 'name', 'status', 'tenant', 'site', 'location', 'rack', 'device_role', 'manufacturer', 'device_type',
|
||||||
|
@ -681,11 +681,15 @@ class DeviceTypeTestCase(
|
|||||||
"""
|
"""
|
||||||
IMPORT_DATA = """
|
IMPORT_DATA = """
|
||||||
manufacturer: Generic
|
manufacturer: Generic
|
||||||
default_platform: Platform
|
|
||||||
model: TEST-1000
|
model: TEST-1000
|
||||||
slug: test-1000
|
slug: test-1000
|
||||||
|
default_platform: Platform
|
||||||
u_height: 2
|
u_height: 2
|
||||||
|
is_full_depth: false
|
||||||
|
airflow: front-to-rear
|
||||||
subdevice_role: parent
|
subdevice_role: parent
|
||||||
|
weight: 10
|
||||||
|
weight_unit: kg
|
||||||
comments: Test comment
|
comments: Test comment
|
||||||
console-ports:
|
console-ports:
|
||||||
- name: Console Port 1
|
- name: Console Port 1
|
||||||
@ -794,8 +798,16 @@ inventory-items:
|
|||||||
self.assertHttpStatus(response, 200)
|
self.assertHttpStatus(response, 200)
|
||||||
|
|
||||||
device_type = DeviceType.objects.get(model='TEST-1000')
|
device_type = DeviceType.objects.get(model='TEST-1000')
|
||||||
self.assertEqual(device_type.comments, 'Test comment')
|
self.assertEqual(device_type.manufacturer.pk, manufacturer.pk)
|
||||||
self.assertEqual(device_type.default_platform.pk, platform.pk)
|
self.assertEqual(device_type.default_platform.pk, platform.pk)
|
||||||
|
self.assertEqual(device_type.slug, 'test-1000')
|
||||||
|
self.assertEqual(device_type.u_height, 2)
|
||||||
|
self.assertFalse(device_type.is_full_depth)
|
||||||
|
self.assertEqual(device_type.airflow, DeviceAirflowChoices.AIRFLOW_FRONT_TO_REAR)
|
||||||
|
self.assertEqual(device_type.subdevice_role, SubdeviceRoleChoices.ROLE_PARENT)
|
||||||
|
self.assertEqual(device_type.weight, 10)
|
||||||
|
self.assertEqual(device_type.weight_unit, WeightUnitChoices.UNIT_KILOGRAM)
|
||||||
|
self.assertEqual(device_type.comments, 'Test comment')
|
||||||
|
|
||||||
# Verify all of the components were created
|
# Verify all of the components were created
|
||||||
self.assertEqual(device_type.consoleporttemplates.count(), 3)
|
self.assertEqual(device_type.consoleporttemplates.count(), 3)
|
||||||
@ -1019,6 +1031,8 @@ class ModuleTypeTestCase(
|
|||||||
IMPORT_DATA = """
|
IMPORT_DATA = """
|
||||||
manufacturer: Generic
|
manufacturer: Generic
|
||||||
model: TEST-1000
|
model: TEST-1000
|
||||||
|
weight: 10
|
||||||
|
weight_unit: lb
|
||||||
comments: Test comment
|
comments: Test comment
|
||||||
console-ports:
|
console-ports:
|
||||||
- name: Console Port 1
|
- name: Console Port 1
|
||||||
@ -1082,7 +1096,8 @@ front-ports:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# Create the manufacturer
|
# Create the manufacturer
|
||||||
Manufacturer(name='Generic', slug='generic').save()
|
manufacturer = Manufacturer(name='Generic', slug='generic')
|
||||||
|
manufacturer.save()
|
||||||
|
|
||||||
# Add all required permissions to the test user
|
# Add all required permissions to the test user
|
||||||
self.add_permissions(
|
self.add_permissions(
|
||||||
@ -1105,6 +1120,9 @@ front-ports:
|
|||||||
self.assertHttpStatus(response, 200)
|
self.assertHttpStatus(response, 200)
|
||||||
|
|
||||||
module_type = ModuleType.objects.get(model='TEST-1000')
|
module_type = ModuleType.objects.get(model='TEST-1000')
|
||||||
|
self.assertEqual(module_type.manufacturer.pk, manufacturer.pk)
|
||||||
|
self.assertEqual(module_type.weight, 10)
|
||||||
|
self.assertEqual(module_type.weight_unit, WeightUnitChoices.UNIT_POUND)
|
||||||
self.assertEqual(module_type.comments, 'Test comment')
|
self.assertEqual(module_type.comments, 'Test comment')
|
||||||
|
|
||||||
# Verify all the components were created
|
# Verify all the components were created
|
||||||
@ -2889,6 +2907,7 @@ class CableTestCase(
|
|||||||
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
||||||
devicetype = DeviceType.objects.create(model='Device Type 1', manufacturer=manufacturer)
|
devicetype = DeviceType.objects.create(model='Device Type 1', manufacturer=manufacturer)
|
||||||
devicerole = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1')
|
devicerole = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1')
|
||||||
|
vc = VirtualChassis.objects.create(name='Virtual Chassis')
|
||||||
|
|
||||||
devices = (
|
devices = (
|
||||||
Device(name='Device 1', site=site, device_type=devicetype, device_role=devicerole),
|
Device(name='Device 1', site=site, device_type=devicetype, device_role=devicerole),
|
||||||
@ -2898,6 +2917,10 @@ class CableTestCase(
|
|||||||
)
|
)
|
||||||
Device.objects.bulk_create(devices)
|
Device.objects.bulk_create(devices)
|
||||||
|
|
||||||
|
vc.members.set((devices[0], devices[1], devices[2]))
|
||||||
|
vc.master = devices[0]
|
||||||
|
vc.save()
|
||||||
|
|
||||||
interfaces = (
|
interfaces = (
|
||||||
Interface(device=devices[0], name='Interface 1', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
|
Interface(device=devices[0], name='Interface 1', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
|
||||||
Interface(device=devices[0], name='Interface 2', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
|
Interface(device=devices[0], name='Interface 2', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
|
||||||
@ -2911,6 +2934,10 @@ class CableTestCase(
|
|||||||
Interface(device=devices[3], name='Interface 1', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
|
Interface(device=devices[3], name='Interface 1', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
|
||||||
Interface(device=devices[3], name='Interface 2', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
|
Interface(device=devices[3], name='Interface 2', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
|
||||||
Interface(device=devices[3], name='Interface 3', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
|
Interface(device=devices[3], name='Interface 3', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
|
||||||
|
Interface(device=devices[1], name='Device 2 Interface', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
|
||||||
|
Interface(device=devices[2], name='Device 3 Interface', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
|
||||||
|
Interface(device=devices[3], name='Interface 4', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
|
||||||
|
Interface(device=devices[3], name='Interface 5', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
|
||||||
)
|
)
|
||||||
Interface.objects.bulk_create(interfaces)
|
Interface.objects.bulk_create(interfaces)
|
||||||
|
|
||||||
@ -2943,6 +2970,8 @@ class CableTestCase(
|
|||||||
"Device 3,dcim.interface,Interface 1,Device 4,dcim.interface,Interface 1",
|
"Device 3,dcim.interface,Interface 1,Device 4,dcim.interface,Interface 1",
|
||||||
"Device 3,dcim.interface,Interface 2,Device 4,dcim.interface,Interface 2",
|
"Device 3,dcim.interface,Interface 2,Device 4,dcim.interface,Interface 2",
|
||||||
"Device 3,dcim.interface,Interface 3,Device 4,dcim.interface,Interface 3",
|
"Device 3,dcim.interface,Interface 3,Device 4,dcim.interface,Interface 3",
|
||||||
|
"Device 1,dcim.interface,Device 2 Interface,Device 4,dcim.interface,Interface 4",
|
||||||
|
"Device 1,dcim.interface,Device 3 Interface,Device 4,dcim.interface,Interface 5",
|
||||||
)
|
)
|
||||||
|
|
||||||
cls.csv_update_data = (
|
cls.csv_update_data = (
|
||||||
|
@ -20,6 +20,7 @@ from extras.views import ObjectConfigContextView
|
|||||||
from ipam.models import ASN, IPAddress, Prefix, VLAN, VLANGroup
|
from ipam.models import ASN, IPAddress, Prefix, VLAN, VLANGroup
|
||||||
from ipam.tables import InterfaceVLANTable
|
from ipam.tables import InterfaceVLANTable
|
||||||
from netbox.views import generic
|
from netbox.views import generic
|
||||||
|
from tenancy.views import ObjectContactsView
|
||||||
from utilities.forms import ConfirmationForm
|
from utilities.forms import ConfirmationForm
|
||||||
from utilities.paginator import EnhancedPaginator, get_paginate_count
|
from utilities.paginator import EnhancedPaginator, get_paginate_count
|
||||||
from utilities.permissions import get_permission_for_model
|
from utilities.permissions import get_permission_for_model
|
||||||
@ -267,6 +268,11 @@ class RegionBulkDeleteView(generic.BulkDeleteView):
|
|||||||
table = tables.RegionTable
|
table = tables.RegionTable
|
||||||
|
|
||||||
|
|
||||||
|
@register_model_view(Region, 'contacts')
|
||||||
|
class RegionContactsView(ObjectContactsView):
|
||||||
|
queryset = Region.objects.all()
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Site groups
|
# Site groups
|
||||||
#
|
#
|
||||||
@ -342,6 +348,11 @@ class SiteGroupBulkDeleteView(generic.BulkDeleteView):
|
|||||||
table = tables.SiteGroupTable
|
table = tables.SiteGroupTable
|
||||||
|
|
||||||
|
|
||||||
|
@register_model_view(SiteGroup, 'contacts')
|
||||||
|
class SiteGroupContactsView(ObjectContactsView):
|
||||||
|
queryset = SiteGroup.objects.all()
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Sites
|
# Sites
|
||||||
#
|
#
|
||||||
@ -435,6 +446,11 @@ class SiteBulkDeleteView(generic.BulkDeleteView):
|
|||||||
table = tables.SiteTable
|
table = tables.SiteTable
|
||||||
|
|
||||||
|
|
||||||
|
@register_model_view(Site, 'contacts')
|
||||||
|
class SiteContactsView(ObjectContactsView):
|
||||||
|
queryset = Site.objects.all()
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Locations
|
# Locations
|
||||||
#
|
#
|
||||||
@ -523,6 +539,11 @@ class LocationBulkDeleteView(generic.BulkDeleteView):
|
|||||||
table = tables.LocationTable
|
table = tables.LocationTable
|
||||||
|
|
||||||
|
|
||||||
|
@register_model_view(Location, 'contacts')
|
||||||
|
class LocationContactsView(ObjectContactsView):
|
||||||
|
queryset = Location.objects.all()
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Rack roles
|
# Rack roles
|
||||||
#
|
#
|
||||||
@ -740,6 +761,11 @@ class RackBulkDeleteView(generic.BulkDeleteView):
|
|||||||
table = tables.RackTable
|
table = tables.RackTable
|
||||||
|
|
||||||
|
|
||||||
|
@register_model_view(Rack, 'contacts')
|
||||||
|
class RackContactsView(ObjectContactsView):
|
||||||
|
queryset = Rack.objects.all()
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Rack reservations
|
# Rack reservations
|
||||||
#
|
#
|
||||||
@ -874,6 +900,11 @@ class ManufacturerBulkDeleteView(generic.BulkDeleteView):
|
|||||||
table = tables.ManufacturerTable
|
table = tables.ManufacturerTable
|
||||||
|
|
||||||
|
|
||||||
|
@register_model_view(Manufacturer, 'contacts')
|
||||||
|
class ManufacturerContactsView(ObjectContactsView):
|
||||||
|
queryset = Manufacturer.objects.all()
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Device types
|
# Device types
|
||||||
#
|
#
|
||||||
@ -2088,6 +2119,11 @@ class DeviceBulkRenameView(generic.BulkRenameView):
|
|||||||
table = tables.DeviceTable
|
table = tables.DeviceTable
|
||||||
|
|
||||||
|
|
||||||
|
@register_model_view(Device, 'contacts')
|
||||||
|
class DeviceContactsView(ObjectContactsView):
|
||||||
|
queryset = Device.objects.all()
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Modules
|
# Modules
|
||||||
#
|
#
|
||||||
@ -3469,6 +3505,11 @@ class PowerPanelBulkDeleteView(generic.BulkDeleteView):
|
|||||||
table = tables.PowerPanelTable
|
table = tables.PowerPanelTable
|
||||||
|
|
||||||
|
|
||||||
|
@register_model_view(PowerPanel, 'contacts')
|
||||||
|
class PowerPanelContactsView(ObjectContactsView):
|
||||||
|
queryset = PowerPanel.objects.all()
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Power feeds
|
# Power feeds
|
||||||
#
|
#
|
||||||
|
@ -25,7 +25,7 @@ class ConfigRevisionAdmin(admin.ModelAdmin):
|
|||||||
'fields': ('ALLOWED_URL_SCHEMES',),
|
'fields': ('ALLOWED_URL_SCHEMES',),
|
||||||
}),
|
}),
|
||||||
('Banners', {
|
('Banners', {
|
||||||
'fields': ('BANNER_LOGIN', 'BANNER_TOP', 'BANNER_BOTTOM'),
|
'fields': ('BANNER_LOGIN', 'BANNER_MAINTENANCE', 'BANNER_TOP', 'BANNER_BOTTOM'),
|
||||||
'classes': ('monospace',),
|
'classes': ('monospace',),
|
||||||
}),
|
}),
|
||||||
('Pagination', {
|
('Pagination', {
|
||||||
|
@ -35,7 +35,8 @@ 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() | Q(app_label='extras', model='objectchange')
|
FeatureQuery('export_templates').get_query() | Q(app_label='extras', model='objectchange') |
|
||||||
|
Q(app_label='extras', model='configcontext')
|
||||||
).order_by('app_label', 'model')
|
).order_by('app_label', 'model')
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -13,6 +13,22 @@ class Migration(migrations.Migration):
|
|||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='customfield',
|
model_name='customfield',
|
||||||
name='name',
|
name='name',
|
||||||
field=models.CharField(max_length=50, unique=True, validators=[django.core.validators.RegexValidator(flags=re.RegexFlag['IGNORECASE'], message='Only alphanumeric characters and underscores are allowed.', regex='^[a-z0-9_]+$')]),
|
field=models.CharField(
|
||||||
|
max_length=50,
|
||||||
|
unique=True,
|
||||||
|
validators=[
|
||||||
|
django.core.validators.RegexValidator(
|
||||||
|
flags=re.RegexFlag['IGNORECASE'],
|
||||||
|
message='Only alphanumeric characters and underscores are allowed.',
|
||||||
|
regex='^[a-z0-9_]+$',
|
||||||
|
),
|
||||||
|
django.core.validators.RegexValidator(
|
||||||
|
flags=re.RegexFlag['IGNORECASE'],
|
||||||
|
inverse_match=True,
|
||||||
|
message='Double underscores are not permitted in custom field names.',
|
||||||
|
regex=r'__',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
@ -85,6 +85,12 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
|||||||
message="Only alphanumeric characters and underscores are allowed.",
|
message="Only alphanumeric characters and underscores are allowed.",
|
||||||
flags=re.IGNORECASE
|
flags=re.IGNORECASE
|
||||||
),
|
),
|
||||||
|
RegexValidator(
|
||||||
|
regex=r'__',
|
||||||
|
message="Double underscores are not permitted in custom field names.",
|
||||||
|
flags=re.IGNORECASE,
|
||||||
|
inverse_match=True
|
||||||
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
label = models.CharField(
|
label = models.CharField(
|
||||||
|
@ -73,6 +73,7 @@ class ExportTemplateTable(NetBoxTable):
|
|||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
is_synced = columns.BooleanColumn(
|
is_synced = columns.BooleanColumn(
|
||||||
|
orderable=False,
|
||||||
verbose_name='Synced'
|
verbose_name='Synced'
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -218,6 +219,7 @@ class ConfigContextTable(NetBoxTable):
|
|||||||
verbose_name='Active'
|
verbose_name='Active'
|
||||||
)
|
)
|
||||||
is_synced = columns.BooleanColumn(
|
is_synced = columns.BooleanColumn(
|
||||||
|
orderable=False,
|
||||||
verbose_name='Synced'
|
verbose_name='Synced'
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -242,6 +244,7 @@ class ConfigTemplateTable(NetBoxTable):
|
|||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
is_synced = columns.BooleanColumn(
|
is_synced = columns.BooleanColumn(
|
||||||
|
orderable=False,
|
||||||
verbose_name='Synced'
|
verbose_name='Synced'
|
||||||
)
|
)
|
||||||
tags = columns.TagColumn(
|
tags = columns.TagColumn(
|
||||||
|
@ -29,6 +29,17 @@ class CustomFieldTest(TestCase):
|
|||||||
|
|
||||||
cls.object_type = ContentType.objects.get_for_model(Site)
|
cls.object_type = ContentType.objects.get_for_model(Site)
|
||||||
|
|
||||||
|
def test_invalid_name(self):
|
||||||
|
"""
|
||||||
|
Try creating a CustomField with an invalid name.
|
||||||
|
"""
|
||||||
|
with self.assertRaises(ValidationError):
|
||||||
|
# Invalid character
|
||||||
|
CustomField(name='?', type=CustomFieldTypeChoices.TYPE_TEXT).full_clean()
|
||||||
|
with self.assertRaises(ValidationError):
|
||||||
|
# Double underscores not permitted
|
||||||
|
CustomField(name='foo__bar', type=CustomFieldTypeChoices.TYPE_TEXT).full_clean()
|
||||||
|
|
||||||
def test_text_field(self):
|
def test_text_field(self):
|
||||||
value = 'Foobar!'
|
value = 'Foobar!'
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ from netbox.config import get_config
|
|||||||
from netbox.constants import RQ_QUEUE_DEFAULT
|
from netbox.constants import RQ_QUEUE_DEFAULT
|
||||||
from netbox.registry import registry
|
from netbox.registry import registry
|
||||||
from utilities.api import get_serializer_for_model
|
from utilities.api import get_serializer_for_model
|
||||||
|
from utilities.rqworker import get_rq_retry
|
||||||
from utilities.utils import serialize_object
|
from utilities.utils import serialize_object
|
||||||
from .choices import *
|
from .choices import *
|
||||||
from .models import Webhook
|
from .models import Webhook
|
||||||
@ -116,5 +117,6 @@ def flush_webhooks(queue):
|
|||||||
snapshots=data['snapshots'],
|
snapshots=data['snapshots'],
|
||||||
timestamp=str(timezone.now()),
|
timestamp=str(timezone.now()),
|
||||||
username=data['username'],
|
username=data['username'],
|
||||||
request_id=data['request_id']
|
request_id=data['request_id'],
|
||||||
|
retry=get_rq_retry()
|
||||||
)
|
)
|
||||||
|
@ -351,6 +351,18 @@ class IPAddressForm(TenancyForm, NetBoxModelForm):
|
|||||||
'primary_for_parent', "Only IP addresses assigned to an interface can be designated as primary IPs."
|
'primary_for_parent', "Only IP addresses assigned to an interface can be designated as primary IPs."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Do not allow assigning a network ID or broadcast address to an interface.
|
||||||
|
if interface and (address := self.cleaned_data.get('address')):
|
||||||
|
if address.ip == address.network:
|
||||||
|
msg = f"{address} is a network ID, which may not be assigned to an interface."
|
||||||
|
if address.version == 4 and address.prefixlen not in (31, 32):
|
||||||
|
raise ValidationError(msg)
|
||||||
|
if address.version == 6 and address.prefixlen not in (127, 128):
|
||||||
|
raise ValidationError(msg)
|
||||||
|
if address.ip == address.broadcast:
|
||||||
|
msg = f"{address} is a broadcast address, which may not be assigned to an interface."
|
||||||
|
raise ValidationError(msg)
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
ipaddress = super().save(*args, **kwargs)
|
ipaddress = super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
@ -783,6 +783,14 @@ class IPAddress(PrimaryModel):
|
|||||||
if available_ips:
|
if available_ips:
|
||||||
return next(iter(available_ips))
|
return next(iter(available_ips))
|
||||||
|
|
||||||
|
def get_related_ips(self):
|
||||||
|
"""
|
||||||
|
Return all IPAddresses belonging to the same VRF.
|
||||||
|
"""
|
||||||
|
return IPAddress.objects.exclude(address=str(self.address)).filter(
|
||||||
|
vrf=self.vrf, address__net_contained_or_equal=str(self.address)
|
||||||
|
)
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
super().clean()
|
super().clean()
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ from circuits.models import Provider
|
|||||||
from dcim.filtersets import InterfaceFilterSet
|
from dcim.filtersets import InterfaceFilterSet
|
||||||
from dcim.models import Interface, Site
|
from dcim.models import Interface, Site
|
||||||
from netbox.views import generic
|
from netbox.views import generic
|
||||||
|
from tenancy.views import ObjectContactsView
|
||||||
from utilities.utils import count_related
|
from utilities.utils import count_related
|
||||||
from utilities.views import ViewTab, register_model_view
|
from utilities.views import ViewTab, register_model_view
|
||||||
from virtualization.filtersets import VMInterfaceFilterSet
|
from virtualization.filtersets import VMInterfaceFilterSet
|
||||||
@ -755,19 +756,9 @@ class IPAddressView(generic.ObjectView):
|
|||||||
# Limit to a maximum of 10 duplicates displayed here
|
# Limit to a maximum of 10 duplicates displayed here
|
||||||
duplicate_ips_table = tables.IPAddressTable(duplicate_ips[:10], orderable=False)
|
duplicate_ips_table = tables.IPAddressTable(duplicate_ips[:10], orderable=False)
|
||||||
|
|
||||||
# Related IP table
|
|
||||||
related_ips = IPAddress.objects.restrict(request.user, 'view').exclude(
|
|
||||||
address=str(instance.address)
|
|
||||||
).filter(
|
|
||||||
vrf=instance.vrf, address__net_contained_or_equal=str(instance.address)
|
|
||||||
)
|
|
||||||
related_ips_table = tables.IPAddressTable(related_ips, orderable=False)
|
|
||||||
related_ips_table.configure(request)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'parent_prefixes_table': parent_prefixes_table,
|
'parent_prefixes_table': parent_prefixes_table,
|
||||||
'duplicate_ips_table': duplicate_ips_table,
|
'duplicate_ips_table': duplicate_ips_table,
|
||||||
'related_ips_table': related_ips_table,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -872,6 +863,24 @@ class IPAddressBulkDeleteView(generic.BulkDeleteView):
|
|||||||
table = tables.IPAddressTable
|
table = tables.IPAddressTable
|
||||||
|
|
||||||
|
|
||||||
|
@register_model_view(IPAddress, 'related_ips', path='related-ip-addresses')
|
||||||
|
class IPAddressRelatedIPsView(generic.ObjectChildrenView):
|
||||||
|
queryset = IPAddress.objects.prefetch_related('vrf__tenant', 'tenant')
|
||||||
|
child_model = IPAddress
|
||||||
|
table = tables.IPAddressTable
|
||||||
|
filterset = filtersets.IPAddressFilterSet
|
||||||
|
template_name = 'ipam/ipaddress/ip_addresses.html'
|
||||||
|
tab = ViewTab(
|
||||||
|
label=_('Related IPs'),
|
||||||
|
badge=lambda x: x.get_related_ips().count(),
|
||||||
|
weight=500,
|
||||||
|
hide_if_empty=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_children(self, request, parent):
|
||||||
|
return parent.get_related_ips().restrict(request.user, 'view')
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# VLAN groups
|
# VLAN groups
|
||||||
#
|
#
|
||||||
@ -1292,6 +1301,11 @@ class L2VPNBulkDeleteView(generic.BulkDeleteView):
|
|||||||
table = tables.L2VPNTable
|
table = tables.L2VPNTable
|
||||||
|
|
||||||
|
|
||||||
|
@register_model_view(L2VPN, 'contacts')
|
||||||
|
class L2VPNContactsView(ObjectContactsView):
|
||||||
|
queryset = L2VPN.objects.all()
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# L2VPN terminations
|
# L2VPN terminations
|
||||||
#
|
#
|
||||||
|
@ -156,8 +156,11 @@ class RemoteUserBackend(_RemoteUserBackend):
|
|||||||
try:
|
try:
|
||||||
group_list.append(Group.objects.get(name=name))
|
group_list.append(Group.objects.get(name=name))
|
||||||
except Group.DoesNotExist:
|
except Group.DoesNotExist:
|
||||||
logging.error(
|
if settings.REMOTE_AUTH_AUTO_CREATE_GROUPS:
|
||||||
f"Could not assign group {name} to remotely-authenticated user {user}: Group not found")
|
group_list.append(Group.objects.create(name=name))
|
||||||
|
else:
|
||||||
|
logging.error(
|
||||||
|
f"Could not assign group {name} to remotely-authenticated user {user}: Group not found")
|
||||||
if group_list:
|
if group_list:
|
||||||
user.groups.set(group_list)
|
user.groups.set(group_list)
|
||||||
logger.debug(
|
logger.debug(
|
||||||
|
@ -28,6 +28,17 @@ PARAMS = (
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
ConfigParam(
|
||||||
|
name='BANNER_MAINTENANCE',
|
||||||
|
label=_('Maintenance banner'),
|
||||||
|
default='NetBox is currently in maintenance mode. Functionality may be limited.',
|
||||||
|
description=_('Additional content to display when in maintenance mode'),
|
||||||
|
field_kwargs={
|
||||||
|
'widget': forms.Textarea(
|
||||||
|
attrs={'class': 'vLargeTextField'}
|
||||||
|
),
|
||||||
|
},
|
||||||
|
),
|
||||||
ConfigParam(
|
ConfigParam(
|
||||||
name='BANNER_TOP',
|
name='BANNER_TOP',
|
||||||
label=_('Top banner'),
|
label=_('Top banner'),
|
||||||
|
@ -3,19 +3,21 @@ import uuid
|
|||||||
from urllib import parse
|
from urllib import parse
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib import auth
|
from django.contrib import auth, messages
|
||||||
from django.contrib.auth.middleware import RemoteUserMiddleware as RemoteUserMiddleware_
|
from django.contrib.auth.middleware import RemoteUserMiddleware as RemoteUserMiddleware_
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
from django.db import ProgrammingError
|
from django.db import connection, ProgrammingError
|
||||||
|
from django.db.utils import InternalError
|
||||||
from django.http import Http404, HttpResponseRedirect
|
from django.http import Http404, HttpResponseRedirect
|
||||||
|
|
||||||
from extras.context_managers import change_logging
|
from extras.context_managers import change_logging
|
||||||
from netbox.config import clear_config
|
from netbox.config import clear_config, get_config
|
||||||
from netbox.views import handler_500
|
from netbox.views import handler_500
|
||||||
from utilities.api import is_api_request, rest_api_server_error
|
from utilities.api import is_api_request, rest_api_server_error
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'CoreMiddleware',
|
'CoreMiddleware',
|
||||||
|
'MaintenanceModeMiddleware',
|
||||||
'RemoteUserMiddleware',
|
'RemoteUserMiddleware',
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -166,3 +168,47 @@ class RemoteUserMiddleware(RemoteUserMiddleware_):
|
|||||||
groups = []
|
groups = []
|
||||||
logger.debug(f"Groups are {groups}")
|
logger.debug(f"Groups are {groups}")
|
||||||
return groups
|
return groups
|
||||||
|
|
||||||
|
|
||||||
|
class MaintenanceModeMiddleware:
|
||||||
|
"""
|
||||||
|
Middleware that checks if the application is in maintenance mode
|
||||||
|
and restricts write-related operations to the database.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, get_response):
|
||||||
|
self.get_response = get_response
|
||||||
|
|
||||||
|
def __call__(self, request):
|
||||||
|
if get_config().MAINTENANCE_MODE:
|
||||||
|
self._set_session_type(
|
||||||
|
allow_write=request.path_info.startswith(settings.MAINTENANCE_EXEMPT_PATHS)
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.get_response(request)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _set_session_type(allow_write):
|
||||||
|
"""
|
||||||
|
Prevent any write-related database operations.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
allow_write (bool): If True, write operations will be permitted.
|
||||||
|
"""
|
||||||
|
with connection.cursor() as cursor:
|
||||||
|
mode = 'READ WRITE' if allow_write else 'READ ONLY'
|
||||||
|
cursor.execute(f'SET SESSION CHARACTERISTICS AS TRANSACTION {mode};')
|
||||||
|
|
||||||
|
def process_exception(self, request, exception):
|
||||||
|
"""
|
||||||
|
Prevent any write-related database operations if an exception is raised.
|
||||||
|
"""
|
||||||
|
if isinstance(exception, InternalError):
|
||||||
|
error_message = 'NetBox is currently operating in maintenance mode and is unable to perform write ' \
|
||||||
|
'operations. Please try again later.'
|
||||||
|
|
||||||
|
if is_api_request(request):
|
||||||
|
return rest_api_server_error(request, error=error_message)
|
||||||
|
|
||||||
|
messages.error(request, error_message)
|
||||||
|
return HttpResponseRedirect(request.path_info)
|
||||||
|
@ -25,7 +25,7 @@ from netbox.constants import RQ_QUEUE_DEFAULT, RQ_QUEUE_HIGH, RQ_QUEUE_LOW
|
|||||||
# Environment setup
|
# Environment setup
|
||||||
#
|
#
|
||||||
|
|
||||||
VERSION = '3.5.1'
|
VERSION = '3.5.2'
|
||||||
|
|
||||||
# Hostname
|
# Hostname
|
||||||
HOSTNAME = platform.node()
|
HOSTNAME = platform.node()
|
||||||
@ -122,6 +122,7 @@ PLUGINS_CONFIG = getattr(configuration, 'PLUGINS_CONFIG', {})
|
|||||||
QUEUE_MAPPINGS = getattr(configuration, 'QUEUE_MAPPINGS', {})
|
QUEUE_MAPPINGS = getattr(configuration, 'QUEUE_MAPPINGS', {})
|
||||||
RELEASE_CHECK_URL = getattr(configuration, 'RELEASE_CHECK_URL', None)
|
RELEASE_CHECK_URL = getattr(configuration, 'RELEASE_CHECK_URL', None)
|
||||||
REMOTE_AUTH_AUTO_CREATE_USER = getattr(configuration, 'REMOTE_AUTH_AUTO_CREATE_USER', False)
|
REMOTE_AUTH_AUTO_CREATE_USER = getattr(configuration, 'REMOTE_AUTH_AUTO_CREATE_USER', False)
|
||||||
|
REMOTE_AUTH_AUTO_CREATE_GROUPS = getattr(configuration, 'REMOTE_AUTH_AUTO_CREATE_GROUPS', False)
|
||||||
REMOTE_AUTH_BACKEND = getattr(configuration, 'REMOTE_AUTH_BACKEND', 'netbox.authentication.RemoteUserBackend')
|
REMOTE_AUTH_BACKEND = getattr(configuration, 'REMOTE_AUTH_BACKEND', 'netbox.authentication.RemoteUserBackend')
|
||||||
REMOTE_AUTH_DEFAULT_GROUPS = getattr(configuration, 'REMOTE_AUTH_DEFAULT_GROUPS', [])
|
REMOTE_AUTH_DEFAULT_GROUPS = getattr(configuration, 'REMOTE_AUTH_DEFAULT_GROUPS', [])
|
||||||
REMOTE_AUTH_DEFAULT_PERMISSIONS = getattr(configuration, 'REMOTE_AUTH_DEFAULT_PERMISSIONS', {})
|
REMOTE_AUTH_DEFAULT_PERMISSIONS = getattr(configuration, 'REMOTE_AUTH_DEFAULT_PERMISSIONS', {})
|
||||||
@ -139,6 +140,8 @@ REMOTE_AUTH_STAFF_USERS = getattr(configuration, 'REMOTE_AUTH_STAFF_USERS', [])
|
|||||||
REMOTE_AUTH_GROUP_SEPARATOR = getattr(configuration, 'REMOTE_AUTH_GROUP_SEPARATOR', '|')
|
REMOTE_AUTH_GROUP_SEPARATOR = getattr(configuration, 'REMOTE_AUTH_GROUP_SEPARATOR', '|')
|
||||||
REPORTS_ROOT = getattr(configuration, 'REPORTS_ROOT', os.path.join(BASE_DIR, 'reports')).rstrip('/')
|
REPORTS_ROOT = getattr(configuration, 'REPORTS_ROOT', os.path.join(BASE_DIR, 'reports')).rstrip('/')
|
||||||
RQ_DEFAULT_TIMEOUT = getattr(configuration, 'RQ_DEFAULT_TIMEOUT', 300)
|
RQ_DEFAULT_TIMEOUT = getattr(configuration, 'RQ_DEFAULT_TIMEOUT', 300)
|
||||||
|
RQ_RETRY_INTERVAL = getattr(configuration, 'RQ_RETRY_INTERVAL', 60)
|
||||||
|
RQ_RETRY_MAX = getattr(configuration, 'RQ_RETRY_MAX', 0)
|
||||||
SCRIPTS_ROOT = getattr(configuration, 'SCRIPTS_ROOT', os.path.join(BASE_DIR, 'scripts')).rstrip('/')
|
SCRIPTS_ROOT = getattr(configuration, 'SCRIPTS_ROOT', os.path.join(BASE_DIR, 'scripts')).rstrip('/')
|
||||||
SEARCH_BACKEND = getattr(configuration, 'SEARCH_BACKEND', 'netbox.search.backends.CachedValueSearchBackend')
|
SEARCH_BACKEND = getattr(configuration, 'SEARCH_BACKEND', 'netbox.search.backends.CachedValueSearchBackend')
|
||||||
SECURE_SSL_REDIRECT = getattr(configuration, 'SECURE_SSL_REDIRECT', False)
|
SECURE_SSL_REDIRECT = getattr(configuration, 'SECURE_SSL_REDIRECT', False)
|
||||||
@ -382,6 +385,7 @@ MIDDLEWARE = [
|
|||||||
'django.middleware.security.SecurityMiddleware',
|
'django.middleware.security.SecurityMiddleware',
|
||||||
'netbox.middleware.RemoteUserMiddleware',
|
'netbox.middleware.RemoteUserMiddleware',
|
||||||
'netbox.middleware.CoreMiddleware',
|
'netbox.middleware.CoreMiddleware',
|
||||||
|
'netbox.middleware.MaintenanceModeMiddleware',
|
||||||
'django_prometheus.middleware.PrometheusAfterMiddleware',
|
'django_prometheus.middleware.PrometheusAfterMiddleware',
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -476,6 +480,11 @@ AUTH_EXEMPT_PATHS = (
|
|||||||
f'/{BASE_PATH}metrics',
|
f'/{BASE_PATH}metrics',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# All URLs starting with a string listed here are exempt from maintenance mode enforcement
|
||||||
|
MAINTENANCE_EXEMPT_PATHS = (
|
||||||
|
f'/{BASE_PATH}admin/',
|
||||||
|
)
|
||||||
|
|
||||||
SERIALIZATION_MODULES = {
|
SERIALIZATION_MODULES = {
|
||||||
'json': 'utilities.serializers.json',
|
'json': 'utilities.serializers.json',
|
||||||
}
|
}
|
||||||
|
@ -310,6 +310,50 @@ class ExternalAuthenticationTestCase(TestCase):
|
|||||||
list(new_user.groups.all())
|
list(new_user.groups.all())
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@override_settings(
|
||||||
|
REMOTE_AUTH_ENABLED=True,
|
||||||
|
REMOTE_AUTH_AUTO_CREATE_USER=True,
|
||||||
|
REMOTE_AUTH_GROUP_SYNC_ENABLED=True,
|
||||||
|
REMOTE_AUTH_AUTO_CREATE_GROUPS=True,
|
||||||
|
LOGIN_REQUIRED=True,
|
||||||
|
)
|
||||||
|
def test_remote_auth_remote_groups_autocreate(self):
|
||||||
|
"""
|
||||||
|
Test enabling remote authentication with group sync and autocreate
|
||||||
|
enabled with the default configuration.
|
||||||
|
"""
|
||||||
|
headers = {
|
||||||
|
"HTTP_REMOTE_USER": "remoteuser2",
|
||||||
|
"HTTP_REMOTE_USER_GROUP": "Group 1|Group 2",
|
||||||
|
}
|
||||||
|
|
||||||
|
self.assertTrue(settings.REMOTE_AUTH_ENABLED)
|
||||||
|
self.assertTrue(settings.REMOTE_AUTH_AUTO_CREATE_USER)
|
||||||
|
self.assertTrue(settings.REMOTE_AUTH_AUTO_CREATE_GROUPS)
|
||||||
|
self.assertTrue(settings.REMOTE_AUTH_GROUP_SYNC_ENABLED)
|
||||||
|
self.assertEqual(settings.REMOTE_AUTH_HEADER, "HTTP_REMOTE_USER")
|
||||||
|
self.assertEqual(settings.REMOTE_AUTH_GROUP_HEADER, "HTTP_REMOTE_USER_GROUP")
|
||||||
|
self.assertEqual(settings.REMOTE_AUTH_GROUP_SEPARATOR, "|")
|
||||||
|
|
||||||
|
groups = (
|
||||||
|
Group(name="Group 1"),
|
||||||
|
Group(name="Group 2"),
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self.client.get(reverse("home"), follow=True, **headers)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
new_user = User.objects.get(username="remoteuser2")
|
||||||
|
self.assertEqual(
|
||||||
|
int(self.client.session.get("_auth_user_id")),
|
||||||
|
new_user.pk,
|
||||||
|
msg="Authentication failed",
|
||||||
|
)
|
||||||
|
self.assertListEqual(
|
||||||
|
[group.name for group in groups],
|
||||||
|
[group.name for group in list(new_user.groups.all())],
|
||||||
|
)
|
||||||
|
|
||||||
@override_settings(
|
@override_settings(
|
||||||
REMOTE_AUTH_ENABLED=True,
|
REMOTE_AUTH_ENABLED=True,
|
||||||
REMOTE_AUTH_AUTO_CREATE_USER=True,
|
REMOTE_AUTH_AUTO_CREATE_USER=True,
|
||||||
|
@ -77,10 +77,10 @@ Blocks:
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if config.MAINTENANCE_MODE %}
|
{% if config.MAINTENANCE_MODE and config.BANNER_MAINTENANCE %}
|
||||||
<div class="alert alert-warning text-center mx-3" role="alert">
|
<div class="alert alert-warning text-center mx-3" role="alert">
|
||||||
<h5><i class="mdi mdi-alert"></i> Maintenance Mode</h5>
|
<h5><i class="mdi mdi-alert"></i> Maintenance Mode</h5>
|
||||||
NetBox is currently in maintenance mode. Functionality may be limited.
|
{{ config.BANNER_MAINTENANCE|escape }}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
@ -70,7 +70,6 @@
|
|||||||
<div class="col col-md-6">
|
<div class="col col-md-6">
|
||||||
{% include 'circuits/inc/circuit_termination.html' with termination=object.termination_a side='A' %}
|
{% include 'circuits/inc/circuit_termination.html' with termination=object.termination_a side='A' %}
|
||||||
{% include 'circuits/inc/circuit_termination.html' with termination=object.termination_z side='Z' %}
|
{% include 'circuits/inc/circuit_termination.html' with termination=object.termination_z side='Z' %}
|
||||||
{% include 'inc/panels/contacts.html' %}
|
|
||||||
{% include 'inc/panels/image_attachments.html' %}
|
{% include 'inc/panels/image_attachments.html' %}
|
||||||
{% plugin_right_page object %}
|
{% plugin_right_page object %}
|
||||||
</div>
|
</div>
|
||||||
|
@ -132,9 +132,16 @@
|
|||||||
</tr>
|
</tr>
|
||||||
{% for field, value in fields.items %}
|
{% for field, value in fields.items %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<th scope="row">{{ field }}
|
||||||
<span title="{{ field.description|escape }}">{{ field }}</span>
|
{% if field.description %}
|
||||||
</td>
|
<i
|
||||||
|
class="mdi mdi-information text-primary"
|
||||||
|
data-bs-toggle="tooltip"
|
||||||
|
data-bs-placement="right"
|
||||||
|
title="{{ field.description|escape }}"
|
||||||
|
></i>
|
||||||
|
{% endif %}
|
||||||
|
</th>
|
||||||
<td>
|
<td>
|
||||||
{% customfield_value field value %}
|
{% customfield_value field value %}
|
||||||
</td>
|
</td>
|
||||||
|
@ -43,7 +43,6 @@
|
|||||||
<div class="col col-md-6">
|
<div class="col col-md-6">
|
||||||
{% include 'inc/panels/related_objects.html' %}
|
{% include 'inc/panels/related_objects.html' %}
|
||||||
{% include 'inc/panels/custom_fields.html' %}
|
{% include 'inc/panels/custom_fields.html' %}
|
||||||
{% include 'inc/panels/contacts.html' %}
|
|
||||||
{% plugin_right_page object %}
|
{% plugin_right_page object %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -38,7 +38,6 @@
|
|||||||
{% include 'inc/panels/related_objects.html' %}
|
{% include 'inc/panels/related_objects.html' %}
|
||||||
{% include 'inc/panels/comments.html' %}
|
{% include 'inc/panels/comments.html' %}
|
||||||
{% include 'inc/panels/custom_fields.html' %}
|
{% include 'inc/panels/custom_fields.html' %}
|
||||||
{% include 'inc/panels/contacts.html' %}
|
|
||||||
{% plugin_right_page object %}
|
{% plugin_right_page object %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col col-md-12">
|
<div class="col col-md-12">
|
||||||
|
@ -298,8 +298,30 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% include 'inc/panels/contacts.html' %}
|
|
||||||
{% include 'inc/panels/image_attachments.html' %}
|
{% include 'inc/panels/image_attachments.html' %}
|
||||||
|
<div class="card">
|
||||||
|
<h5 class="card-header">Dimensions</h5>
|
||||||
|
<div class="card-body table-responsive">
|
||||||
|
<table class="table table-hover attr-table">
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Height</th>
|
||||||
|
<td>
|
||||||
|
{{ object.device_type.u_height }}U
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Weight</th>
|
||||||
|
<td>
|
||||||
|
{% if object.total_weight %}
|
||||||
|
{{ object.total_weight|floatformat }} Kilograms
|
||||||
|
{% else %}
|
||||||
|
{{ ''|placeholder }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% if object.rack and object.position %}
|
{% if object.rack and object.position %}
|
||||||
<div class="row" style="margin-bottom: 20px">
|
<div class="row" style="margin-bottom: 20px">
|
||||||
<div class="col col-md-6 col-sm-6 col-xs-12 text-center">
|
<div class="col col-md-6 col-sm-6 col-xs-12 text-center">
|
||||||
|
@ -123,11 +123,11 @@
|
|||||||
<table class="table table-hover">
|
<table class="table table-hover">
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">MAC Address</th>
|
<th scope="row">MAC Address</th>
|
||||||
<td><span class="text-monospace">{{ object.mac_address|placeholder }}</span></td>
|
<td><span class="font-monospace">{{ object.mac_address|placeholder }}</span></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">WWN</th>
|
<th scope="row">WWN</th>
|
||||||
<td><span class="text-monospace">{{ object.wwn|placeholder }}</span></td>
|
<td><span class="font-monospace">{{ object.wwn|placeholder }}</span></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">VRF</th>
|
<th scope="row">VRF</th>
|
||||||
|
@ -65,7 +65,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col col-md-6">
|
<div class="col col-md-6">
|
||||||
{% include 'inc/panels/related_objects.html' %}
|
{% include 'inc/panels/related_objects.html' %}
|
||||||
{% include 'inc/panels/contacts.html' %}
|
|
||||||
{% include 'dcim/inc/nonracked_devices.html' %}
|
{% include 'dcim/inc/nonracked_devices.html' %}
|
||||||
{% include 'inc/panels/image_attachments.html' %}
|
{% include 'inc/panels/image_attachments.html' %}
|
||||||
{% plugin_right_page object %}
|
{% plugin_right_page object %}
|
||||||
|
@ -51,7 +51,6 @@
|
|||||||
<div class="col col-md-6">
|
<div class="col col-md-6">
|
||||||
{% include 'inc/panels/related_objects.html' %}
|
{% include 'inc/panels/related_objects.html' %}
|
||||||
{% include 'inc/panels/custom_fields.html' %}
|
{% include 'inc/panels/custom_fields.html' %}
|
||||||
{% include 'inc/panels/contacts.html' %}
|
|
||||||
{% plugin_right_page object %}
|
{% plugin_right_page object %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -40,7 +40,6 @@
|
|||||||
<div class="col col-md-6">
|
<div class="col col-md-6">
|
||||||
{% include 'inc/panels/related_objects.html' %}
|
{% include 'inc/panels/related_objects.html' %}
|
||||||
{% include 'inc/panels/custom_fields.html' %}
|
{% include 'inc/panels/custom_fields.html' %}
|
||||||
{% include 'inc/panels/contacts.html' %}
|
|
||||||
{% include 'inc/panels/image_attachments.html' %}
|
{% include 'inc/panels/image_attachments.html' %}
|
||||||
{% plugin_right_page object %}
|
{% plugin_right_page object %}
|
||||||
</div>
|
</div>
|
||||||
|
@ -191,7 +191,6 @@
|
|||||||
</div>
|
</div>
|
||||||
{% include 'inc/panels/related_objects.html' %}
|
{% include 'inc/panels/related_objects.html' %}
|
||||||
{% include 'dcim/inc/nonracked_devices.html' %}
|
{% include 'dcim/inc/nonracked_devices.html' %}
|
||||||
{% include 'inc/panels/contacts.html' %}
|
|
||||||
{% plugin_right_page object %}
|
{% plugin_right_page object %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -46,7 +46,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col col-md-6">
|
<div class="col col-md-6">
|
||||||
{% include 'inc/panels/related_objects.html' %}
|
{% include 'inc/panels/related_objects.html' %}
|
||||||
{% include 'inc/panels/contacts.html' %}
|
|
||||||
{% plugin_right_page object %}
|
{% plugin_right_page object %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -87,11 +87,13 @@
|
|||||||
<th scope="row">Physical Address</th>
|
<th scope="row">Physical Address</th>
|
||||||
<td class="position-relative">
|
<td class="position-relative">
|
||||||
{% if object.physical_address %}
|
{% if object.physical_address %}
|
||||||
<div class="position-absolute top-50 end-0 translate-middle-y noprint">
|
{% if config.MAPS_URL %}
|
||||||
<a href="{{ config.MAPS_URL }}{{ object.physical_address|urlencode }}" target="_blank" class="btn btn-primary btn-sm">
|
<div class="position-absolute top-50 end-0 translate-middle-y noprint">
|
||||||
<i class="mdi mdi-map-marker"></i> Map
|
<a href="{{ config.MAPS_URL }}{{ object.physical_address|urlencode }}" target="_blank" class="btn btn-primary btn-sm">
|
||||||
</a>
|
<i class="mdi mdi-map-marker"></i> Map
|
||||||
</div>
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
<span>{{ object.physical_address|linebreaksbr }}</span>
|
<span>{{ object.physical_address|linebreaksbr }}</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ ''|placeholder }}
|
{{ ''|placeholder }}
|
||||||
@ -106,11 +108,13 @@
|
|||||||
<th scope="row">GPS Coordinates</th>
|
<th scope="row">GPS Coordinates</th>
|
||||||
<td class="position-relative">
|
<td class="position-relative">
|
||||||
{% if object.latitude and object.longitude %}
|
{% if object.latitude and object.longitude %}
|
||||||
<div class="position-absolute top-50 end-0 translate-middle-y noprint">
|
{% if config.MAPS_URL %}
|
||||||
<a href="{{ config.MAPS_URL }}{{ object.latitude }},{{ object.longitude }}" target="_blank" class="btn btn-primary btn-sm">
|
<div class="position-absolute top-50 end-0 translate-middle-y noprint">
|
||||||
<i class="mdi mdi-map-marker"></i> Map It
|
<a href="{{ config.MAPS_URL }}{{ object.latitude }},{{ object.longitude }}" target="_blank" class="btn btn-primary btn-sm">
|
||||||
</a>
|
<i class="mdi mdi-map-marker"></i> Map It
|
||||||
</div>
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
<span>{{ object.latitude }}, {{ object.longitude }}</span>
|
<span>{{ object.latitude }}, {{ object.longitude }}</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ ''|placeholder }}
|
{{ ''|placeholder }}
|
||||||
@ -127,7 +131,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col col-md-6">
|
<div class="col col-md-6">
|
||||||
{% include 'inc/panels/related_objects.html' with filter_name='site_id' %}
|
{% include 'inc/panels/related_objects.html' with filter_name='site_id' %}
|
||||||
{% include 'inc/panels/contacts.html' %}
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h5 class="card-header">Locations</h5>
|
<h5 class="card-header">Locations</h5>
|
||||||
<div class='card-body'>
|
<div class='card-body'>
|
||||||
|
@ -42,7 +42,6 @@
|
|||||||
</div>
|
</div>
|
||||||
{% include 'inc/panels/tags.html' %}
|
{% include 'inc/panels/tags.html' %}
|
||||||
{% include 'inc/panels/custom_fields.html' %}
|
{% include 'inc/panels/custom_fields.html' %}
|
||||||
{% include 'inc/panels/contacts.html' %}
|
|
||||||
{% plugin_left_page object %}
|
{% plugin_left_page object %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col col-md-6">
|
<div class="col col-md-6">
|
||||||
|
@ -1,63 +0,0 @@
|
|||||||
{% load helpers %}
|
|
||||||
|
|
||||||
<div class="card">
|
|
||||||
<h5 class="card-header">Contacts</h5>
|
|
||||||
<div class="card-body">
|
|
||||||
{% with contacts=object.contacts.all %}
|
|
||||||
{% if contacts.exists %}
|
|
||||||
<table class="table table-hover">
|
|
||||||
<tr>
|
|
||||||
<th>Name</th>
|
|
||||||
<th>Role</th>
|
|
||||||
<th>Priority</th>
|
|
||||||
<th>Phone</th>
|
|
||||||
<th>Email</th>
|
|
||||||
<th></th>
|
|
||||||
</tr>
|
|
||||||
{% for contact in contacts %}
|
|
||||||
<tr>
|
|
||||||
<td>{{ contact.contact|linkify }}</td>
|
|
||||||
<td>{{ contact.role|placeholder }}</td>
|
|
||||||
<td>{{ contact.get_priority_display|placeholder }}</td>
|
|
||||||
<td>
|
|
||||||
{% if contact.contact.phone %}
|
|
||||||
<a href="tel:{{ contact.contact.phone }}">{{ contact.contact.phone }}</a>
|
|
||||||
{% else %}
|
|
||||||
{{ ''|placeholder }}
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{% if contact.contact.email %}
|
|
||||||
<a href="mailto:{{ contact.contact.email }}">{{ contact.contact.email }}</a>
|
|
||||||
{% else %}
|
|
||||||
{{ ''|placeholder }}
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td class="text-end noprint">
|
|
||||||
{% if perms.tenancy.change_contactassignment %}
|
|
||||||
<a href="{% url 'tenancy:contactassignment_edit' pk=contact.pk %}?return_url={{ object.get_absolute_url }}" class="btn btn-warning btn-sm lh-1" title="Edit">
|
|
||||||
<i class="mdi mdi-pencil" aria-hidden="true"></i>
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
{% if perms.tenancy.delete_contactassignment %}
|
|
||||||
<a href="{% url 'tenancy:contactassignment_delete' pk=contact.pk %}?return_url={{ object.get_absolute_url }}" class="btn btn-danger btn-sm lh-1" title="Delete">
|
|
||||||
<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.tenancy.add_contactassignment %}
|
|
||||||
<div class="card-footer text-end noprint">
|
|
||||||
<a href="{% url 'tenancy:contactassignment_add' %}?content_type={{ object|content_type_id }}&object_id={{ object.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-primary btn-sm">
|
|
||||||
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add a contact
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
@ -12,8 +12,15 @@
|
|||||||
<table class="table table-hover attr-table">
|
<table class="table table-hover attr-table">
|
||||||
{% for field, value in fields.items %}
|
{% for field, value in fields.items %}
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">
|
<th scope="row">{{ field }}
|
||||||
<span title="{{ field.description|escape }}">{{ field }}</span>
|
{% if field.description %}
|
||||||
|
<i
|
||||||
|
class="mdi mdi-information text-primary"
|
||||||
|
data-bs-toggle="tooltip"
|
||||||
|
data-bs-placement="right"
|
||||||
|
title="{{ field.description|escape }}"
|
||||||
|
></i>
|
||||||
|
{% endif %}
|
||||||
</th>
|
</th>
|
||||||
<td>
|
<td>
|
||||||
{% customfield_value field value %}
|
{% customfield_value field value %}
|
||||||
|
@ -3,13 +3,6 @@
|
|||||||
{% load plugins %}
|
{% load plugins %}
|
||||||
{% load render_table from django_tables2 %}
|
{% load render_table from django_tables2 %}
|
||||||
|
|
||||||
{% block breadcrumbs %}
|
|
||||||
{{ block.super }}
|
|
||||||
{% if object.vrf %}
|
|
||||||
<li class="breadcrumb-item"><a href="{% url 'ipam:ipaddress_list' %}?vrf_id={{ object.vrf.pk }}">{{ object.vrf }}</a></li>
|
|
||||||
{% endif %}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col col-md-4">
|
<div class="col col-md-4">
|
||||||
@ -116,7 +109,6 @@
|
|||||||
{% if duplicate_ips_table.rows %}
|
{% if duplicate_ips_table.rows %}
|
||||||
{% include 'inc/panel_table.html' with table=duplicate_ips_table heading='Duplicate IPs' panel_class='danger' %}
|
{% include 'inc/panel_table.html' with table=duplicate_ips_table heading='Duplicate IPs' panel_class='danger' %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% include 'inc/panel_table.html' with table=related_ips_table heading='Related IPs' %}
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h5 class="card-header">Services</h5>
|
<h5 class="card-header">Services</h5>
|
||||||
<div class="card-body htmx-container table-responsive"
|
<div class="card-body htmx-container table-responsive"
|
||||||
|
8
netbox/templates/ipam/ipaddress/base.html
Normal file
8
netbox/templates/ipam/ipaddress/base.html
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{% extends 'generic/object.html' %}
|
||||||
|
|
||||||
|
{% block breadcrumbs %}
|
||||||
|
{{ block.super }}
|
||||||
|
{% if object.vrf %}
|
||||||
|
<li class="breadcrumb-item"><a href="{% url 'ipam:ipaddress_list' %}?vrf_id={{ object.vrf.pk }}">{{ object.vrf }}</a></li>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
19
netbox/templates/ipam/ipaddress/ip_addresses.html
Normal file
19
netbox/templates/ipam/ipaddress/ip_addresses.html
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{% extends 'ipam/ipaddress/base.html' %}
|
||||||
|
{% load helpers %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% include 'inc/table_controls_htmx.html' with table_modal="IPAddressTable_config" %}
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body" id="object_list">
|
||||||
|
{% include 'htmx/table.html' %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endblock content %}
|
||||||
|
|
||||||
|
{% block modals %}
|
||||||
|
{{ block.super }}
|
||||||
|
{% table_config_form table %}
|
||||||
|
{% endblock modals %}
|
@ -37,7 +37,6 @@
|
|||||||
{% plugin_left_page object %}
|
{% plugin_left_page object %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col col-md-6">
|
<div class="col col-md-6">
|
||||||
{% include 'inc/panels/contacts.html' %}
|
|
||||||
{% include 'inc/panels/custom_fields.html' %}
|
{% include 'inc/panels/custom_fields.html' %}
|
||||||
{% include 'inc/panels/comments.html' %}
|
{% include 'inc/panels/comments.html' %}
|
||||||
{% plugin_right_page object %}
|
{% plugin_right_page object %}
|
||||||
|
27
netbox/templates/tenancy/object_contacts.html
Normal file
27
netbox/templates/tenancy/object_contacts.html
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{% extends base_template %}
|
||||||
|
{% load helpers %}
|
||||||
|
|
||||||
|
{% block extra_controls %}
|
||||||
|
{% if perms.tenancy.add_contactassignment %}
|
||||||
|
<a href="{% url 'tenancy:contactassignment_add' %}?content_type={{ object|content_type_id }}&object_id={{ object.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-primary btn-sm">
|
||||||
|
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add a contact
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% include 'inc/table_controls_htmx.html' with table_modal="ContactTable_config" %}
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body" id="object_list">
|
||||||
|
{% include 'htmx/table.html' %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endblock content %}
|
||||||
|
|
||||||
|
{% block modals %}
|
||||||
|
{{ block.super }}
|
||||||
|
{% table_config_form table %}
|
||||||
|
{% endblock modals %}
|
@ -30,7 +30,6 @@
|
|||||||
{% include 'inc/panels/custom_fields.html' %}
|
{% include 'inc/panels/custom_fields.html' %}
|
||||||
{% include 'inc/panels/tags.html' %}
|
{% include 'inc/panels/tags.html' %}
|
||||||
{% include 'inc/panels/comments.html' %}
|
{% include 'inc/panels/comments.html' %}
|
||||||
{% include 'inc/panels/contacts.html' %}
|
|
||||||
{% plugin_left_page object %}
|
{% plugin_left_page object %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col col-md-5">
|
<div class="col col-md-5">
|
||||||
|
@ -84,7 +84,6 @@
|
|||||||
</div>
|
</div>
|
||||||
{% include 'inc/panels/custom_fields.html' %}
|
{% include 'inc/panels/custom_fields.html' %}
|
||||||
{% include 'inc/panels/tags.html' %}
|
{% include 'inc/panels/tags.html' %}
|
||||||
{% include 'inc/panels/contacts.html' %}
|
|
||||||
{% plugin_right_page object %}
|
{% plugin_right_page object %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -37,7 +37,6 @@
|
|||||||
<div class="col col-md-6">
|
<div class="col col-md-6">
|
||||||
{% include 'inc/panels/related_objects.html' %}
|
{% include 'inc/panels/related_objects.html' %}
|
||||||
{% include 'inc/panels/custom_fields.html' %}
|
{% include 'inc/panels/custom_fields.html' %}
|
||||||
{% include 'inc/panels/contacts.html' %}
|
|
||||||
{% plugin_right_page object %}
|
{% plugin_right_page object %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -158,7 +158,6 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% include 'inc/panels/contacts.html' %}
|
|
||||||
{% plugin_right_page object %}
|
{% plugin_right_page object %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -59,7 +59,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">MAC Address</th>
|
<th scope="row">MAC Address</th>
|
||||||
<td><span class="text-monospace">{{ object.mac_address|placeholder }}</span></td>
|
<td><span class="font-monospace">{{ object.mac_address|placeholder }}</span></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">802.1Q Mode</th>
|
<th scope="row">802.1Q Mode</th>
|
||||||
|
@ -7,17 +7,40 @@ from dcim.models import Cable, Device, Location, Rack, RackReservation, Site, Vi
|
|||||||
from ipam.models import Aggregate, ASN, IPAddress, IPRange, L2VPN, Prefix, VLAN, VRF
|
from ipam.models import Aggregate, ASN, IPAddress, IPRange, L2VPN, Prefix, VLAN, VRF
|
||||||
from netbox.views import generic
|
from netbox.views import generic
|
||||||
from utilities.utils import count_related
|
from utilities.utils import count_related
|
||||||
from utilities.views import register_model_view
|
from utilities.views import register_model_view, ViewTab
|
||||||
from virtualization.models import VirtualMachine, Cluster
|
from virtualization.models import VirtualMachine, Cluster
|
||||||
from wireless.models import WirelessLAN, WirelessLink
|
from wireless.models import WirelessLAN, WirelessLink
|
||||||
from . import filtersets, forms, tables
|
from . import filtersets, forms, tables
|
||||||
from .models import *
|
from .models import *
|
||||||
|
|
||||||
|
|
||||||
|
class ObjectContactsView(generic.ObjectChildrenView):
|
||||||
|
child_model = Contact
|
||||||
|
table = tables.ContactTable
|
||||||
|
filterset = filtersets.ContactFilterSet
|
||||||
|
template_name = 'tenancy/object_contacts.html'
|
||||||
|
tab = ViewTab(
|
||||||
|
label=_('Contacts'),
|
||||||
|
badge=lambda obj: obj.contacts.count(),
|
||||||
|
permission='tenancy.view_contact',
|
||||||
|
weight=5000
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_children(self, request, parent):
|
||||||
|
return Contact.objects.annotate(
|
||||||
|
assignment_count=count_related(ContactAssignment, 'contact')
|
||||||
|
).restrict(request.user, 'view').filter(assignments__object_id=parent.pk)
|
||||||
|
|
||||||
|
def get_extra_context(self, request, instance):
|
||||||
|
return {
|
||||||
|
'base_template': f'{instance._meta.app_label}/{instance._meta.model_name}.html',
|
||||||
|
}
|
||||||
|
|
||||||
#
|
#
|
||||||
# Tenant groups
|
# Tenant groups
|
||||||
#
|
#
|
||||||
|
|
||||||
|
|
||||||
class TenantGroupListView(generic.ObjectListView):
|
class TenantGroupListView(generic.ObjectListView):
|
||||||
queryset = TenantGroup.objects.add_related_count(
|
queryset = TenantGroup.objects.add_related_count(
|
||||||
TenantGroup.objects.all(),
|
TenantGroup.objects.all(),
|
||||||
@ -165,6 +188,11 @@ class TenantBulkDeleteView(generic.BulkDeleteView):
|
|||||||
table = tables.TenantTable
|
table = tables.TenantTable
|
||||||
|
|
||||||
|
|
||||||
|
@register_model_view(Tenant, 'contacts')
|
||||||
|
class TenantContactsView(ObjectContactsView):
|
||||||
|
queryset = Tenant.objects.all()
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Contact groups
|
# Contact groups
|
||||||
#
|
#
|
||||||
@ -342,11 +370,11 @@ class ContactBulkDeleteView(generic.BulkDeleteView):
|
|||||||
filterset = filtersets.ContactFilterSet
|
filterset = filtersets.ContactFilterSet
|
||||||
table = tables.ContactTable
|
table = tables.ContactTable
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Contact assignments
|
# Contact assignments
|
||||||
#
|
#
|
||||||
|
|
||||||
|
|
||||||
class ContactAssignmentListView(generic.ObjectListView):
|
class ContactAssignmentListView(generic.ObjectListView):
|
||||||
queryset = ContactAssignment.objects.all()
|
queryset = ContactAssignment.objects.all()
|
||||||
filterset = filtersets.ContactAssignmentFilterSet
|
filterset = filtersets.ContactAssignmentFilterSet
|
||||||
|
@ -32,11 +32,11 @@ class BootstrapMixin:
|
|||||||
elif isinstance(field.widget, forms.CheckboxInput):
|
elif isinstance(field.widget, forms.CheckboxInput):
|
||||||
field.widget.attrs['class'] = f'{css} form-check-input'
|
field.widget.attrs['class'] = f'{css} form-check-input'
|
||||||
|
|
||||||
elif isinstance(field.widget, forms.SelectMultiple):
|
elif isinstance(field.widget, forms.SelectMultiple) and 'size' in field.widget.attrs:
|
||||||
if 'size' not in field.widget.attrs:
|
# Use native Bootstrap class for multi-line <select> widgets
|
||||||
field.widget.attrs['class'] = f'{css} netbox-static-select'
|
field.widget.attrs['class'] = f'{css} form-select form-select-sm'
|
||||||
|
|
||||||
elif isinstance(field.widget, forms.Select):
|
elif isinstance(field.widget, (forms.Select, forms.SelectMultiple)):
|
||||||
field.widget.attrs['class'] = f'{css} netbox-static-select'
|
field.widget.attrs['class'] = f'{css} netbox-static-select'
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
from django_rq.queues import get_connection
|
from django_rq.queues import get_connection
|
||||||
from rq import Worker
|
from rq import Retry, Worker
|
||||||
|
|
||||||
from netbox.config import get_config
|
from netbox.config import get_config
|
||||||
from netbox.constants import RQ_QUEUE_DEFAULT
|
from netbox.constants import RQ_QUEUE_DEFAULT
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'get_queue_for_model',
|
'get_queue_for_model',
|
||||||
|
'get_rq_retry',
|
||||||
'get_workers_for_queue',
|
'get_workers_for_queue',
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -22,3 +23,14 @@ def get_workers_for_queue(queue_name):
|
|||||||
Returns True if a worker process is currently servicing the specified queue.
|
Returns True if a worker process is currently servicing the specified queue.
|
||||||
"""
|
"""
|
||||||
return Worker.count(get_connection(queue_name))
|
return Worker.count(get_connection(queue_name))
|
||||||
|
|
||||||
|
|
||||||
|
def get_rq_retry():
|
||||||
|
"""
|
||||||
|
If RQ_RETRY_MAX is defined and greater than zero, instantiate and return a Retry object to be
|
||||||
|
used when queuing a job. Otherwise, return None.
|
||||||
|
"""
|
||||||
|
retry_max = get_config().RQ_RETRY_MAX
|
||||||
|
retry_interval = get_config().RQ_RETRY_INTERVAL
|
||||||
|
if retry_max:
|
||||||
|
return Retry(max=retry_max, interval=retry_interval)
|
||||||
|
@ -65,7 +65,7 @@ class ClusterImportForm(NetBoxModelImportForm):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Cluster
|
model = Cluster
|
||||||
fields = ('name', 'type', 'group', 'status', 'site', 'description', 'comments', 'tags')
|
fields = ('name', 'type', 'group', 'status', 'site', 'tenant', 'description', 'comments', 'tags')
|
||||||
|
|
||||||
|
|
||||||
class VirtualMachineImportForm(NetBoxModelImportForm):
|
class VirtualMachineImportForm(NetBoxModelImportForm):
|
||||||
|
@ -9,9 +9,10 @@ from dcim.filtersets import DeviceFilterSet
|
|||||||
from dcim.models import Device
|
from dcim.models import Device
|
||||||
from dcim.tables import DeviceTable
|
from dcim.tables import DeviceTable
|
||||||
from extras.views import ObjectConfigContextView
|
from extras.views import ObjectConfigContextView
|
||||||
from ipam.models import IPAddress, Service
|
from ipam.models import IPAddress
|
||||||
from ipam.tables import InterfaceVLANTable
|
from ipam.tables import InterfaceVLANTable
|
||||||
from netbox.views import generic
|
from netbox.views import generic
|
||||||
|
from tenancy.views import ObjectContactsView
|
||||||
from utilities.utils import count_related
|
from utilities.utils import count_related
|
||||||
from utilities.views import ViewTab, register_model_view
|
from utilities.views import ViewTab, register_model_view
|
||||||
from . import filtersets, forms, tables
|
from . import filtersets, forms, tables
|
||||||
@ -140,6 +141,11 @@ class ClusterGroupBulkDeleteView(generic.BulkDeleteView):
|
|||||||
table = tables.ClusterGroupTable
|
table = tables.ClusterGroupTable
|
||||||
|
|
||||||
|
|
||||||
|
@register_model_view(ClusterGroup, 'contacts')
|
||||||
|
class ClusterGroupContactsView(ObjectContactsView):
|
||||||
|
queryset = ClusterGroup.objects.all()
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Clusters
|
# Clusters
|
||||||
#
|
#
|
||||||
@ -312,6 +318,11 @@ class ClusterRemoveDevicesView(generic.ObjectEditView):
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@register_model_view(Cluster, 'contacts')
|
||||||
|
class ClusterContactsView(ObjectContactsView):
|
||||||
|
queryset = Cluster.objects.all()
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Virtual machines
|
# Virtual machines
|
||||||
#
|
#
|
||||||
@ -390,6 +401,11 @@ class VirtualMachineBulkDeleteView(generic.BulkDeleteView):
|
|||||||
table = tables.VirtualMachineTable
|
table = tables.VirtualMachineTable
|
||||||
|
|
||||||
|
|
||||||
|
@register_model_view(VirtualMachine, 'contacts')
|
||||||
|
class VirtualMachineContactsView(ObjectContactsView):
|
||||||
|
queryset = VirtualMachine.objects.all()
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# VM interfaces
|
# VM interfaces
|
||||||
#
|
#
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
bleach==6.0.0
|
bleach==6.0.0
|
||||||
boto3==1.26.127
|
boto3==1.26.138
|
||||||
Django==4.1.9
|
Django==4.1.9
|
||||||
django-cors-headers==3.14.0
|
django-cors-headers==4.0.0
|
||||||
django-debug-toolbar==4.0.0
|
django-debug-toolbar==4.1.0
|
||||||
django-filter==23.2
|
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
|
||||||
@ -10,7 +10,7 @@ django-pglocks==1.0.4
|
|||||||
django-prometheus==2.3.1
|
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.8.0
|
django-rq==2.8.1
|
||||||
django-tables2==2.5.3
|
django-tables2==2.5.3
|
||||||
django-taggit==4.0.0
|
django-taggit==4.0.0
|
||||||
django-timezone-field==5.0
|
django-timezone-field==5.0
|
||||||
@ -19,17 +19,17 @@ drf-spectacular==0.26.2
|
|||||||
drf-spectacular-sidecar==2023.5.1
|
drf-spectacular-sidecar==2023.5.1
|
||||||
dulwich==0.21.5
|
dulwich==0.21.5
|
||||||
feedparser==6.0.10
|
feedparser==6.0.10
|
||||||
graphene-django==3.0.0
|
graphene-django==3.0.2
|
||||||
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.9
|
mkdocs-material==9.1.14
|
||||||
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.22.1
|
sentry-sdk==1.23.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