mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-14 09:51:22 -06:00
Merge branch 'develop' into feature
This commit is contained in:
commit
9c8432cf13
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@ -17,7 +17,7 @@ body:
|
|||||||
What version of NetBox are you currently running? (If you don't have access to the most
|
What version of NetBox are you currently running? (If you don't have access to the most
|
||||||
recent NetBox release, consider testing on our [demo instance](https://demo.netbox.dev/)
|
recent NetBox release, consider testing on our [demo instance](https://demo.netbox.dev/)
|
||||||
before opening a bug report to see if your issue has already been addressed.)
|
before opening a bug report to see if your issue has already been addressed.)
|
||||||
placeholder: v3.0.4
|
placeholder: v3.0.5
|
||||||
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.0.4
|
placeholder: v3.0.5
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
|
@ -45,6 +45,20 @@ Defining script variables is optional: You may create a script with only a `run(
|
|||||||
|
|
||||||
Any output generated by the script during its execution will be displayed under the "output" tab in the UI.
|
Any output generated by the script during its execution will be displayed under the "output" tab in the UI.
|
||||||
|
|
||||||
|
By default, scripts within a module are ordered alphabetically in the scripts list page. To return scripts in a specific order, you can define the `script_order` variable at the end of your module. The `script_order` variable is a tuple which contains each Script class in the desired order. Any scripts that are omitted from this list will be listed last.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from extras.scripts import Script
|
||||||
|
|
||||||
|
class MyCustomScript(Script):
|
||||||
|
...
|
||||||
|
|
||||||
|
class AnotherCustomScript(Script):
|
||||||
|
...
|
||||||
|
|
||||||
|
script_order = (MyCustomScript, AnotherCustomScript)
|
||||||
|
```
|
||||||
|
|
||||||
## Module Attributes
|
## Module Attributes
|
||||||
|
|
||||||
### `name`
|
### `name`
|
||||||
|
@ -11,7 +11,7 @@ The following sections detail how to set up a new instance of NetBox:
|
|||||||
5. [HTTP server](5-http-server.md)
|
5. [HTTP server](5-http-server.md)
|
||||||
6. [LDAP authentication](6-ldap.md) (optional)
|
6. [LDAP authentication](6-ldap.md) (optional)
|
||||||
|
|
||||||
The video below demonstrates the installation of NetBox v2.10.3 on Ubuntu 20.04 for your reference.
|
The video below demonstrates the installation of NetBox v3.0 on Ubuntu 20.04 for your reference.
|
||||||
|
|
||||||
<iframe width="560" height="315" src="https://www.youtube.com/embed/7Fpd2-q9_28" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
<iframe width="560" height="315" src="https://www.youtube.com/embed/7Fpd2-q9_28" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||||
|
|
||||||
|
@ -1,6 +1,28 @@
|
|||||||
# NetBox v3.0
|
# NetBox v3.0
|
||||||
|
|
||||||
## v3.0.5 (FUTURE)
|
## v3.0.5 (2021-10-04)
|
||||||
|
|
||||||
|
### Enhancements
|
||||||
|
|
||||||
|
* [#5925](https://github.com/netbox-community/netbox/issues/5925) - Always show IP addresses tab under prefix view
|
||||||
|
* [#6423](https://github.com/netbox-community/netbox/issues/6423) - Cache rendered REST API specifications
|
||||||
|
* [#6708](https://github.com/netbox-community/netbox/issues/6708) - Add image attachment support for circuits, power panels
|
||||||
|
* [#7387](https://github.com/netbox-community/netbox/issues/7387) - Enable arbitrary ordering of custom scripts
|
||||||
|
* [#7427](https://github.com/netbox-community/netbox/issues/7427) - Don't select hidden rows when selecting all in a table
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* [#6433](https://github.com/netbox-community/netbox/issues/6433) - Fix bulk editing of child prefixes under aggregate view
|
||||||
|
* [#6817](https://github.com/netbox-community/netbox/issues/6817) - Custom field columns should be removed from tables upon their deletion
|
||||||
|
* [#6895](https://github.com/netbox-community/netbox/issues/6895) - Remove errant markup for null values in CSV export
|
||||||
|
* [#7215](https://github.com/netbox-community/netbox/issues/7215) - Prevent rack elevations from overlapping when higher width is specified
|
||||||
|
* [#7373](https://github.com/netbox-community/netbox/issues/7373) - Fix flashing when server, client, and browser color-mode preferences are mismatched
|
||||||
|
* [#7397](https://github.com/netbox-community/netbox/issues/7397) - Fix AttributeError exception when rendering export template for devices via REST API
|
||||||
|
* [#7401](https://github.com/netbox-community/netbox/issues/7401) - Pin `jsonschema` package to v3.2.0 to fix REST API docs rendering
|
||||||
|
* [#7411](https://github.com/netbox-community/netbox/issues/7411) - Fix exception in UI when adding member devices to virtual chassis
|
||||||
|
* [#7412](https://github.com/netbox-community/netbox/issues/7412) - Fix exception in UI when adding child device to device bay
|
||||||
|
* [#7417](https://github.com/netbox-community/netbox/issues/7417) - Prevent exception when filtering objects list by invalid tag
|
||||||
|
* [#7425](https://github.com/netbox-community/netbox/issues/7425) - Housekeeping command should honor zero verbosity
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
from django.contrib.contenttypes.fields import GenericRelation
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
@ -202,6 +203,9 @@ class Circuit(PrimaryModel):
|
|||||||
comments = models.TextField(
|
comments = models.TextField(
|
||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
|
images = GenericRelation(
|
||||||
|
to='extras.ImageAttachment'
|
||||||
|
)
|
||||||
|
|
||||||
# Cache associated CircuitTerminations
|
# Cache associated CircuitTerminations
|
||||||
termination_a = models.ForeignKey(
|
termination_a = models.ForeignKey(
|
||||||
|
@ -38,6 +38,7 @@ __all__ = (
|
|||||||
'LocationForm',
|
'LocationForm',
|
||||||
'ManufacturerForm',
|
'ManufacturerForm',
|
||||||
'PlatformForm',
|
'PlatformForm',
|
||||||
|
'PopulateDeviceBayForm',
|
||||||
'PowerFeedForm',
|
'PowerFeedForm',
|
||||||
'PowerOutletForm',
|
'PowerOutletForm',
|
||||||
'PowerOutletTemplateForm',
|
'PowerOutletTemplateForm',
|
||||||
@ -52,6 +53,7 @@ __all__ = (
|
|||||||
'RegionForm',
|
'RegionForm',
|
||||||
'SiteForm',
|
'SiteForm',
|
||||||
'SiteGroupForm',
|
'SiteGroupForm',
|
||||||
|
'VCMemberSelectForm',
|
||||||
'VirtualChassisForm',
|
'VirtualChassisForm',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
from django.contrib.contenttypes.fields import GenericRelation
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
@ -39,6 +40,9 @@ class PowerPanel(PrimaryModel):
|
|||||||
name = models.CharField(
|
name = models.CharField(
|
||||||
max_length=100
|
max_length=100
|
||||||
)
|
)
|
||||||
|
images = GenericRelation(
|
||||||
|
to='extras.ImageAttachment'
|
||||||
|
)
|
||||||
|
|
||||||
objects = RestrictedQuerySet.as_manager()
|
objects = RestrictedQuerySet.as_manager()
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ import django_tables2 as tables
|
|||||||
from django_tables2.utils import Accessor
|
from django_tables2.utils import Accessor
|
||||||
|
|
||||||
from dcim.models import Cable
|
from dcim.models import Cable
|
||||||
from utilities.tables import BaseTable, ChoiceFieldColumn, ColorColumn, TagColumn, ToggleColumn
|
from utilities.tables import BaseTable, ChoiceFieldColumn, ColorColumn, TagColumn, TemplateColumn, ToggleColumn
|
||||||
from .template_code import CABLE_LENGTH, CABLE_TERMINATION_PARENT
|
from .template_code import CABLE_LENGTH, CABLE_TERMINATION_PARENT
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
@ -45,7 +45,7 @@ class CableTable(BaseTable):
|
|||||||
verbose_name='Termination B'
|
verbose_name='Termination B'
|
||||||
)
|
)
|
||||||
status = ChoiceFieldColumn()
|
status = ChoiceFieldColumn()
|
||||||
length = tables.TemplateColumn(
|
length = TemplateColumn(
|
||||||
template_code=CABLE_LENGTH,
|
template_code=CABLE_LENGTH,
|
||||||
order_by='_abs_length'
|
order_by='_abs_length'
|
||||||
)
|
)
|
||||||
|
@ -9,7 +9,7 @@ from dcim.models import (
|
|||||||
from tenancy.tables import TenantColumn
|
from tenancy.tables import TenantColumn
|
||||||
from utilities.tables import (
|
from utilities.tables import (
|
||||||
BaseTable, BooleanColumn, ButtonsColumn, ChoiceFieldColumn, ColorColumn, ColoredLabelColumn, LinkedCountColumn,
|
BaseTable, BooleanColumn, ButtonsColumn, ChoiceFieldColumn, ColorColumn, ColoredLabelColumn, LinkedCountColumn,
|
||||||
MarkdownColumn, TagColumn, ToggleColumn,
|
MarkdownColumn, TagColumn, TemplateColumn, ToggleColumn,
|
||||||
)
|
)
|
||||||
from .template_code import (
|
from .template_code import (
|
||||||
CABLETERMINATION, CONSOLEPORT_BUTTONS, CONSOLESERVERPORT_BUTTONS, DEVICE_LINK, DEVICEBAY_BUTTONS, DEVICEBAY_STATUS,
|
CABLETERMINATION, CONSOLEPORT_BUTTONS, CONSOLESERVERPORT_BUTTONS, DEVICE_LINK, DEVICEBAY_BUTTONS, DEVICEBAY_STATUS,
|
||||||
@ -258,7 +258,7 @@ class CableTerminationTable(BaseTable):
|
|||||||
orderable=False,
|
orderable=False,
|
||||||
verbose_name='Cable Color'
|
verbose_name='Cable Color'
|
||||||
)
|
)
|
||||||
cable_peer = tables.TemplateColumn(
|
cable_peer = TemplateColumn(
|
||||||
accessor='_cable_peer',
|
accessor='_cable_peer',
|
||||||
template_code=CABLETERMINATION,
|
template_code=CABLETERMINATION,
|
||||||
orderable=False,
|
orderable=False,
|
||||||
@ -268,7 +268,7 @@ class CableTerminationTable(BaseTable):
|
|||||||
|
|
||||||
|
|
||||||
class PathEndpointTable(CableTerminationTable):
|
class PathEndpointTable(CableTerminationTable):
|
||||||
connection = tables.TemplateColumn(
|
connection = TemplateColumn(
|
||||||
accessor='_path.last_node',
|
accessor='_path.last_node',
|
||||||
template_code=CABLETERMINATION,
|
template_code=CABLETERMINATION,
|
||||||
verbose_name='Connection',
|
verbose_name='Connection',
|
||||||
@ -470,7 +470,7 @@ class BaseInterfaceTable(BaseTable):
|
|||||||
verbose_name='IP Addresses'
|
verbose_name='IP Addresses'
|
||||||
)
|
)
|
||||||
untagged_vlan = tables.Column(linkify=True)
|
untagged_vlan = tables.Column(linkify=True)
|
||||||
tagged_vlans = tables.TemplateColumn(
|
tagged_vlans = TemplateColumn(
|
||||||
template_code=INTERFACE_TAGGED_VLANS,
|
template_code=INTERFACE_TAGGED_VLANS,
|
||||||
orderable=False,
|
orderable=False,
|
||||||
verbose_name='Tagged VLANs'
|
verbose_name='Tagged VLANs'
|
||||||
|
@ -5,13 +5,11 @@ CABLETERMINATION = """
|
|||||||
<i class="mdi mdi-chevron-right"></i>
|
<i class="mdi mdi-chevron-right"></i>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a href="{{ value.get_absolute_url }}">{{ value }}</a>
|
<a href="{{ value.get_absolute_url }}">{{ value }}</a>
|
||||||
{% else %}
|
|
||||||
—
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
CABLE_LENGTH = """
|
CABLE_LENGTH = """
|
||||||
{% if record.length %}{{ record.length }} {{ record.get_length_unit_display }}{% else %}—{% endif %}
|
{% if record.length %}{{ record.length }} {{ record.get_length_unit_display }}{% endif %}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
CABLE_TERMINATION_PARENT = """
|
CABLE_TERMINATION_PARENT = """
|
||||||
@ -63,8 +61,6 @@ INTERFACE_TAGGED_VLANS = """
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% elif record.mode == 'tagged-all' %}
|
{% elif record.mode == 'tagged-all' %}
|
||||||
All
|
All
|
||||||
{% else %}
|
|
||||||
—
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from extras.views import ImageAttachmentEditView, ObjectChangeLogView, ObjectJournalView
|
from extras.views import ObjectChangeLogView, ObjectJournalView
|
||||||
from ipam.views import ServiceEditView
|
from ipam.views import ServiceEditView
|
||||||
from utilities.views import SlugRedirectView
|
from utilities.views import SlugRedirectView
|
||||||
from . import views
|
from . import views
|
||||||
@ -43,7 +43,6 @@ urlpatterns = [
|
|||||||
path('sites/<int:pk>/delete/', views.SiteDeleteView.as_view(), name='site_delete'),
|
path('sites/<int:pk>/delete/', views.SiteDeleteView.as_view(), name='site_delete'),
|
||||||
path('sites/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='site_changelog', kwargs={'model': Site}),
|
path('sites/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='site_changelog', kwargs={'model': Site}),
|
||||||
path('sites/<int:pk>/journal/', ObjectJournalView.as_view(), name='site_journal', kwargs={'model': Site}),
|
path('sites/<int:pk>/journal/', ObjectJournalView.as_view(), name='site_journal', kwargs={'model': Site}),
|
||||||
path('sites/<int:object_id>/images/add/', ImageAttachmentEditView.as_view(), name='site_add_image', kwargs={'model': Site}),
|
|
||||||
|
|
||||||
# Locations
|
# Locations
|
||||||
path('locations/', views.LocationListView.as_view(), name='location_list'),
|
path('locations/', views.LocationListView.as_view(), name='location_list'),
|
||||||
@ -55,7 +54,6 @@ urlpatterns = [
|
|||||||
path('locations/<int:pk>/edit/', views.LocationEditView.as_view(), name='location_edit'),
|
path('locations/<int:pk>/edit/', views.LocationEditView.as_view(), name='location_edit'),
|
||||||
path('locations/<int:pk>/delete/', views.LocationDeleteView.as_view(), name='location_delete'),
|
path('locations/<int:pk>/delete/', views.LocationDeleteView.as_view(), name='location_delete'),
|
||||||
path('locations/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='location_changelog', kwargs={'model': Location}),
|
path('locations/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='location_changelog', kwargs={'model': Location}),
|
||||||
path('locations/<int:object_id>/images/add/', ImageAttachmentEditView.as_view(), name='location_add_image', kwargs={'model': Location}),
|
|
||||||
|
|
||||||
# Rack roles
|
# Rack roles
|
||||||
path('rack-roles/', views.RackRoleListView.as_view(), name='rackrole_list'),
|
path('rack-roles/', views.RackRoleListView.as_view(), name='rackrole_list'),
|
||||||
@ -92,7 +90,6 @@ urlpatterns = [
|
|||||||
path('racks/<int:pk>/delete/', views.RackDeleteView.as_view(), name='rack_delete'),
|
path('racks/<int:pk>/delete/', views.RackDeleteView.as_view(), name='rack_delete'),
|
||||||
path('racks/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='rack_changelog', kwargs={'model': Rack}),
|
path('racks/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='rack_changelog', kwargs={'model': Rack}),
|
||||||
path('racks/<int:pk>/journal/', ObjectJournalView.as_view(), name='rack_journal', kwargs={'model': Rack}),
|
path('racks/<int:pk>/journal/', ObjectJournalView.as_view(), name='rack_journal', kwargs={'model': Rack}),
|
||||||
path('racks/<int:object_id>/images/add/', ImageAttachmentEditView.as_view(), name='rack_add_image', kwargs={'model': Rack}),
|
|
||||||
|
|
||||||
# Manufacturers
|
# Manufacturers
|
||||||
path('manufacturers/', views.ManufacturerListView.as_view(), name='manufacturer_list'),
|
path('manufacturers/', views.ManufacturerListView.as_view(), name='manufacturer_list'),
|
||||||
@ -229,7 +226,6 @@ urlpatterns = [
|
|||||||
path('devices/<int:pk>/lldp-neighbors/', views.DeviceLLDPNeighborsView.as_view(), name='device_lldp_neighbors'),
|
path('devices/<int:pk>/lldp-neighbors/', views.DeviceLLDPNeighborsView.as_view(), name='device_lldp_neighbors'),
|
||||||
path('devices/<int:pk>/config/', views.DeviceConfigView.as_view(), name='device_config'),
|
path('devices/<int:pk>/config/', views.DeviceConfigView.as_view(), name='device_config'),
|
||||||
path('devices/<int:device>/services/assign/', ServiceEditView.as_view(), name='device_service_assign'),
|
path('devices/<int:device>/services/assign/', ServiceEditView.as_view(), name='device_service_assign'),
|
||||||
path('devices/<int:object_id>/images/add/', ImageAttachmentEditView.as_view(), name='device_add_image', kwargs={'model': Device}),
|
|
||||||
|
|
||||||
# Console ports
|
# Console ports
|
||||||
path('console-ports/', views.ConsolePortListView.as_view(), name='consoleport_list'),
|
path('console-ports/', views.ConsolePortListView.as_view(), name='consoleport_list'),
|
||||||
|
@ -18,40 +18,51 @@ class Command(BaseCommand):
|
|||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
|
|
||||||
# Clear expired authentication sessions (essentially replicating the `clearsessions` command)
|
# Clear expired authentication sessions (essentially replicating the `clearsessions` command)
|
||||||
|
if options['verbosity']:
|
||||||
self.stdout.write("[*] Clearing expired authentication sessions")
|
self.stdout.write("[*] Clearing expired authentication sessions")
|
||||||
if options['verbosity'] >= 2:
|
if options['verbosity'] >= 2:
|
||||||
self.stdout.write(f"\tConfigured session engine: {settings.SESSION_ENGINE}")
|
self.stdout.write(f"\tConfigured session engine: {settings.SESSION_ENGINE}")
|
||||||
engine = import_module(settings.SESSION_ENGINE)
|
engine = import_module(settings.SESSION_ENGINE)
|
||||||
try:
|
try:
|
||||||
engine.SessionStore.clear_expired()
|
engine.SessionStore.clear_expired()
|
||||||
|
if options['verbosity']:
|
||||||
self.stdout.write("\tSessions cleared.", self.style.SUCCESS)
|
self.stdout.write("\tSessions cleared.", self.style.SUCCESS)
|
||||||
except NotImplementedError:
|
except NotImplementedError:
|
||||||
|
if options['verbosity']:
|
||||||
self.stdout.write(
|
self.stdout.write(
|
||||||
f"\tThe configured session engine ({settings.SESSION_ENGINE}) does not support "
|
f"\tThe configured session engine ({settings.SESSION_ENGINE}) does not support "
|
||||||
f"clearing sessions; skipping."
|
f"clearing sessions; skipping."
|
||||||
)
|
)
|
||||||
|
|
||||||
# Delete expired ObjectRecords
|
# Delete expired ObjectRecords
|
||||||
|
if options['verbosity']:
|
||||||
self.stdout.write("[*] Checking for expired changelog records")
|
self.stdout.write("[*] Checking for expired changelog records")
|
||||||
if settings.CHANGELOG_RETENTION:
|
if settings.CHANGELOG_RETENTION:
|
||||||
cutoff = timezone.now() - timedelta(days=settings.CHANGELOG_RETENTION)
|
cutoff = timezone.now() - timedelta(days=settings.CHANGELOG_RETENTION)
|
||||||
if options['verbosity'] >= 2:
|
if options['verbosity'] >= 2:
|
||||||
self.stdout.write(f"Retention period: {settings.CHANGELOG_RETENTION} days")
|
self.stdout.write(f"\tRetention period: {settings.CHANGELOG_RETENTION} days")
|
||||||
self.stdout.write(f"\tCut-off time: {cutoff}")
|
self.stdout.write(f"\tCut-off time: {cutoff}")
|
||||||
expired_records = ObjectChange.objects.filter(time__lt=cutoff).count()
|
expired_records = ObjectChange.objects.filter(time__lt=cutoff).count()
|
||||||
if expired_records:
|
if expired_records:
|
||||||
self.stdout.write(f"\tDeleting {expired_records} expired records... ", self.style.WARNING, ending="")
|
if options['verbosity']:
|
||||||
|
self.stdout.write(
|
||||||
|
f"\tDeleting {expired_records} expired records... ",
|
||||||
|
self.style.WARNING,
|
||||||
|
ending=""
|
||||||
|
)
|
||||||
self.stdout.flush()
|
self.stdout.flush()
|
||||||
ObjectChange.objects.filter(time__lt=cutoff)._raw_delete(using=DEFAULT_DB_ALIAS)
|
ObjectChange.objects.filter(time__lt=cutoff)._raw_delete(using=DEFAULT_DB_ALIAS)
|
||||||
self.stdout.write("Done.", self.style.WARNING)
|
if options['verbosity']:
|
||||||
else:
|
self.stdout.write("Done.", self.style.SUCCESS)
|
||||||
self.stdout.write("\tNo expired records found.")
|
elif options['verbosity']:
|
||||||
else:
|
self.stdout.write("\tNo expired records found.", self.style.SUCCESS)
|
||||||
|
elif options['verbosity']:
|
||||||
self.stdout.write(
|
self.stdout.write(
|
||||||
f"\tSkipping: No retention period specified (CHANGELOG_RETENTION = {settings.CHANGELOG_RETENTION})"
|
f"\tSkipping: No retention period specified (CHANGELOG_RETENTION = {settings.CHANGELOG_RETENTION})"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check for new releases (if enabled)
|
# Check for new releases (if enabled)
|
||||||
|
if options['verbosity']:
|
||||||
self.stdout.write("[*] Checking for latest release")
|
self.stdout.write("[*] Checking for latest release")
|
||||||
if settings.RELEASE_CHECK_URL:
|
if settings.RELEASE_CHECK_URL:
|
||||||
headers = {
|
headers = {
|
||||||
@ -59,6 +70,7 @@ class Command(BaseCommand):
|
|||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
if options['verbosity'] >= 2:
|
||||||
self.stdout.write(f"\tFetching {settings.RELEASE_CHECK_URL}")
|
self.stdout.write(f"\tFetching {settings.RELEASE_CHECK_URL}")
|
||||||
response = requests.get(
|
response = requests.get(
|
||||||
url=settings.RELEASE_CHECK_URL,
|
url=settings.RELEASE_CHECK_URL,
|
||||||
@ -73,15 +85,19 @@ class Command(BaseCommand):
|
|||||||
continue
|
continue
|
||||||
releases.append((version.parse(release['tag_name']), release.get('html_url')))
|
releases.append((version.parse(release['tag_name']), release.get('html_url')))
|
||||||
latest_release = max(releases)
|
latest_release = max(releases)
|
||||||
|
if options['verbosity'] >= 2:
|
||||||
self.stdout.write(f"\tFound {len(response.json())} releases; {len(releases)} usable")
|
self.stdout.write(f"\tFound {len(response.json())} releases; {len(releases)} usable")
|
||||||
self.stdout.write(f"\tLatest release: {latest_release[0]}")
|
if options['verbosity']:
|
||||||
|
self.stdout.write(f"\tLatest release: {latest_release[0]}", self.style.SUCCESS)
|
||||||
|
|
||||||
# Cache the most recent release
|
# Cache the most recent release
|
||||||
cache.set('latest_release', latest_release, None)
|
cache.set('latest_release', latest_release, None)
|
||||||
|
|
||||||
except requests.exceptions.RequestException as exc:
|
except requests.exceptions.RequestException as exc:
|
||||||
self.stdout.write(f"\tRequest error: {exc}")
|
self.stdout.write(f"\tRequest error: {exc}", self.style.ERROR)
|
||||||
else:
|
else:
|
||||||
|
if options['verbosity']:
|
||||||
self.stdout.write(f"\tSkipping: RELEASE_CHECK_URL not set")
|
self.stdout.write(f"\tSkipping: RELEASE_CHECK_URL not set")
|
||||||
|
|
||||||
|
if options['verbosity']:
|
||||||
self.stdout.write("Finished.", self.style.SUCCESS)
|
self.stdout.write("Finished.", self.style.SUCCESS)
|
||||||
|
@ -470,7 +470,6 @@ def get_scripts(use_names=False):
|
|||||||
defined name in place of the actual module name.
|
defined name in place of the actual module name.
|
||||||
"""
|
"""
|
||||||
scripts = OrderedDict()
|
scripts = OrderedDict()
|
||||||
|
|
||||||
# Iterate through all modules within the reports path. These are the user-created files in which reports are
|
# Iterate through all modules within the reports path. These are the user-created files in which reports are
|
||||||
# defined.
|
# defined.
|
||||||
for importer, module_name, _ in pkgutil.iter_modules([settings.SCRIPTS_ROOT]):
|
for importer, module_name, _ in pkgutil.iter_modules([settings.SCRIPTS_ROOT]):
|
||||||
@ -478,8 +477,11 @@ def get_scripts(use_names=False):
|
|||||||
if use_names and hasattr(module, 'name'):
|
if use_names and hasattr(module, 'name'):
|
||||||
module_name = module.name
|
module_name = module.name
|
||||||
module_scripts = OrderedDict()
|
module_scripts = OrderedDict()
|
||||||
for name, cls in inspect.getmembers(module, is_script):
|
script_order = getattr(module, "script_order", ())
|
||||||
module_scripts[name] = cls
|
ordered_scripts = [cls for cls in script_order if is_script(cls)]
|
||||||
|
unordered_scripts = [cls for _, cls in inspect.getmembers(module, is_script) if cls not in script_order]
|
||||||
|
for cls in [*ordered_scripts, *unordered_scripts]:
|
||||||
|
module_scripts[cls.__name__] = cls
|
||||||
if module_scripts:
|
if module_scripts:
|
||||||
scripts[module_name] = module_scripts
|
scripts[module_name] = module_scripts
|
||||||
|
|
||||||
|
@ -78,6 +78,7 @@ urlpatterns = [
|
|||||||
kwargs={'model': models.ConfigContext}),
|
kwargs={'model': models.ConfigContext}),
|
||||||
|
|
||||||
# Image attachments
|
# Image attachments
|
||||||
|
path('image-attachments/add/', views.ImageAttachmentEditView.as_view(), name='imageattachment_add'),
|
||||||
path('image-attachments/<int:pk>/edit/', views.ImageAttachmentEditView.as_view(), name='imageattachment_edit'),
|
path('image-attachments/<int:pk>/edit/', views.ImageAttachmentEditView.as_view(), name='imageattachment_edit'),
|
||||||
path('image-attachments/<int:pk>/delete/', views.ImageAttachmentDeleteView.as_view(), name='imageattachment_delete'),
|
path('image-attachments/<int:pk>/delete/', views.ImageAttachmentDeleteView.as_view(), name='imageattachment_delete'),
|
||||||
|
|
||||||
|
@ -472,22 +472,26 @@ class ImageAttachmentEditView(generic.ObjectEditView):
|
|||||||
queryset = ImageAttachment.objects.all()
|
queryset = ImageAttachment.objects.all()
|
||||||
model_form = forms.ImageAttachmentForm
|
model_form = forms.ImageAttachmentForm
|
||||||
|
|
||||||
def alter_obj(self, imageattachment, request, args, kwargs):
|
def alter_obj(self, instance, request, args, kwargs):
|
||||||
if not imageattachment.pk:
|
if not instance.pk:
|
||||||
# Assign the parent object based on URL kwargs
|
# Assign the parent object based on URL kwargs
|
||||||
model = kwargs.get('model')
|
try:
|
||||||
imageattachment.parent = get_object_or_404(model, pk=kwargs['object_id'])
|
app_label, model = request.GET.get('content_type').split('.')
|
||||||
return imageattachment
|
except (AttributeError, ValueError):
|
||||||
|
raise Http404("Content type not specified")
|
||||||
|
content_type = get_object_or_404(ContentType, app_label=app_label, model=model)
|
||||||
|
instance.parent = get_object_or_404(content_type.model_class(), pk=request.GET.get('object_id'))
|
||||||
|
return instance
|
||||||
|
|
||||||
def get_return_url(self, request, imageattachment):
|
def get_return_url(self, request, obj=None):
|
||||||
return imageattachment.parent.get_absolute_url()
|
return obj.parent.get_absolute_url() if obj else super().get_return_url(request)
|
||||||
|
|
||||||
|
|
||||||
class ImageAttachmentDeleteView(generic.ObjectDeleteView):
|
class ImageAttachmentDeleteView(generic.ObjectDeleteView):
|
||||||
queryset = ImageAttachment.objects.all()
|
queryset = ImageAttachment.objects.all()
|
||||||
|
|
||||||
def get_return_url(self, request, imageattachment):
|
def get_return_url(self, request, obj=None):
|
||||||
return imageattachment.parent.get_absolute_url()
|
return obj.parent.get_absolute_url() if obj else super().get_return_url(request)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
@ -39,15 +39,7 @@ PREFIXFLAT_LINK = """
|
|||||||
{% if record.pk %}
|
{% if record.pk %}
|
||||||
<a href="{% url 'ipam:prefix' pk=record.pk %}">{{ record.prefix }}</a>
|
<a href="{% url 'ipam:prefix' pk=record.pk %}">{{ record.prefix }}</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
—
|
{{ record.prefix }}
|
||||||
{% endif %}
|
|
||||||
"""
|
|
||||||
|
|
||||||
PREFIX_ROLE_LINK = """
|
|
||||||
{% if record.role %}
|
|
||||||
<a href="{% url 'ipam:prefix_list' %}?role={{ record.role.slug }}">{{ record.role }}</a>
|
|
||||||
{% else %}
|
|
||||||
—
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -218,8 +210,8 @@ class PrefixTable(BaseTable):
|
|||||||
linkify=True,
|
linkify=True,
|
||||||
verbose_name='VLAN'
|
verbose_name='VLAN'
|
||||||
)
|
)
|
||||||
role = tables.TemplateColumn(
|
role = tables.Column(
|
||||||
template_code=PREFIX_ROLE_LINK
|
linkify=True
|
||||||
)
|
)
|
||||||
is_pool = BooleanColumn(
|
is_pool = BooleanColumn(
|
||||||
verbose_name='Pool'
|
verbose_name='Pool'
|
||||||
@ -264,8 +256,8 @@ class IPRangeTable(BaseTable):
|
|||||||
status = ChoiceFieldColumn(
|
status = ChoiceFieldColumn(
|
||||||
default=AVAILABLE_LABEL
|
default=AVAILABLE_LABEL
|
||||||
)
|
)
|
||||||
role = tables.TemplateColumn(
|
role = tables.Column(
|
||||||
template_code=PREFIX_ROLE_LINK
|
linkify=True
|
||||||
)
|
)
|
||||||
tenant = TenantColumn()
|
tenant = TenantColumn()
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ from dcim.models import Interface
|
|||||||
from tenancy.tables import TenantColumn
|
from tenancy.tables import TenantColumn
|
||||||
from utilities.tables import (
|
from utilities.tables import (
|
||||||
BaseTable, BooleanColumn, ButtonsColumn, ChoiceFieldColumn, ContentTypeColumn, LinkedCountColumn, TagColumn,
|
BaseTable, BooleanColumn, ButtonsColumn, ChoiceFieldColumn, ContentTypeColumn, LinkedCountColumn, TagColumn,
|
||||||
ToggleColumn,
|
TemplateColumn, ToggleColumn,
|
||||||
)
|
)
|
||||||
from virtualization.models import VMInterface
|
from virtualization.models import VMInterface
|
||||||
from ipam.models import *
|
from ipam.models import *
|
||||||
@ -35,19 +35,9 @@ VLAN_LINK = """
|
|||||||
VLAN_PREFIXES = """
|
VLAN_PREFIXES = """
|
||||||
{% for prefix in record.prefixes.all %}
|
{% for prefix in record.prefixes.all %}
|
||||||
<a href="{% url 'ipam:prefix' pk=prefix.pk %}">{{ prefix }}</a>{% if not forloop.last %}<br />{% endif %}
|
<a href="{% url 'ipam:prefix' pk=prefix.pk %}">{{ prefix }}</a>{% if not forloop.last %}<br />{% endif %}
|
||||||
{% empty %}
|
|
||||||
—
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
VLAN_ROLE_LINK = """
|
|
||||||
{% if record.role %}
|
|
||||||
<a href="{% url 'ipam:vlan_list' %}?role={{ record.role.slug }}">{{ record.role }}</a>
|
|
||||||
{% else %}
|
|
||||||
—
|
|
||||||
{% endif %}
|
|
||||||
"""
|
|
||||||
|
|
||||||
VLANGROUP_ADD_VLAN = """
|
VLANGROUP_ADD_VLAN = """
|
||||||
{% with next_vid=record.get_next_available_vid %}
|
{% with next_vid=record.get_next_available_vid %}
|
||||||
{% if next_vid and perms.ipam.add_vlan %}
|
{% if next_vid and perms.ipam.add_vlan %}
|
||||||
@ -115,10 +105,10 @@ class VLANTable(BaseTable):
|
|||||||
status = ChoiceFieldColumn(
|
status = ChoiceFieldColumn(
|
||||||
default=AVAILABLE_LABEL
|
default=AVAILABLE_LABEL
|
||||||
)
|
)
|
||||||
role = tables.TemplateColumn(
|
role = tables.Column(
|
||||||
template_code=VLAN_ROLE_LINK
|
linkify=True
|
||||||
)
|
)
|
||||||
prefixes = tables.TemplateColumn(
|
prefixes = TemplateColumn(
|
||||||
template_code=VLAN_PREFIXES,
|
template_code=VLAN_PREFIXES,
|
||||||
orderable=False,
|
orderable=False,
|
||||||
verbose_name='Prefixes'
|
verbose_name='Prefixes'
|
||||||
@ -190,8 +180,8 @@ class InterfaceVLANTable(BaseTable):
|
|||||||
)
|
)
|
||||||
tenant = TenantColumn()
|
tenant = TenantColumn()
|
||||||
status = ChoiceFieldColumn()
|
status = ChoiceFieldColumn()
|
||||||
role = tables.TemplateColumn(
|
role = tables.Column(
|
||||||
template_code=VLAN_ROLE_LINK
|
linkify=True
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import django_tables2 as tables
|
import django_tables2 as tables
|
||||||
|
|
||||||
from tenancy.tables import TenantColumn
|
from tenancy.tables import TenantColumn
|
||||||
from utilities.tables import BaseTable, BooleanColumn, TagColumn, ToggleColumn
|
from utilities.tables import BaseTable, BooleanColumn, TagColumn, TemplateColumn, ToggleColumn
|
||||||
from ipam.models import *
|
from ipam.models import *
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
@ -12,8 +12,6 @@ __all__ = (
|
|||||||
VRF_TARGETS = """
|
VRF_TARGETS = """
|
||||||
{% for rt in value.all %}
|
{% for rt in value.all %}
|
||||||
<a href="{{ rt.get_absolute_url }}">{{ rt }}</a>{% if not forloop.last %}<br />{% endif %}
|
<a href="{{ rt.get_absolute_url }}">{{ rt }}</a>{% if not forloop.last %}<br />{% endif %}
|
||||||
{% empty %}
|
|
||||||
—
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -34,11 +32,11 @@ class VRFTable(BaseTable):
|
|||||||
enforce_unique = BooleanColumn(
|
enforce_unique = BooleanColumn(
|
||||||
verbose_name='Unique'
|
verbose_name='Unique'
|
||||||
)
|
)
|
||||||
import_targets = tables.TemplateColumn(
|
import_targets = TemplateColumn(
|
||||||
template_code=VRF_TARGETS,
|
template_code=VRF_TARGETS,
|
||||||
orderable=False
|
orderable=False
|
||||||
)
|
)
|
||||||
export_targets = tables.TemplateColumn(
|
export_targets = TemplateColumn(
|
||||||
template_code=VRF_TARGETS,
|
template_code=VRF_TARGETS,
|
||||||
orderable=False
|
orderable=False
|
||||||
)
|
)
|
||||||
|
@ -240,6 +240,7 @@ class AggregateView(generic.ObjectView):
|
|||||||
return {
|
return {
|
||||||
'prefix_table': prefix_table,
|
'prefix_table': prefix_table,
|
||||||
'permissions': permissions,
|
'permissions': permissions,
|
||||||
|
'bulk_querystring': f'within={instance.prefix}',
|
||||||
'show_available': request.GET.get('show_available', 'true') == 'true',
|
'show_available': request.GET.get('show_available', 'true') == 'true',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -230,7 +230,7 @@ class ModelViewSet(BulkUpdateModelMixin, BulkDestroyModelMixin, ModelViewSet_):
|
|||||||
Overrides ListModelMixin to allow processing ExportTemplates.
|
Overrides ListModelMixin to allow processing ExportTemplates.
|
||||||
"""
|
"""
|
||||||
if 'export' in request.GET:
|
if 'export' in request.GET:
|
||||||
content_type = ContentType.objects.get_for_model(self.serializer_class.Meta.model)
|
content_type = ContentType.objects.get_for_model(self.get_serializer_class().Meta.model)
|
||||||
et = get_object_or_404(ExportTemplate, content_type=content_type, name=request.GET['export'])
|
et = get_object_or_404(ExportTemplate, content_type=content_type, name=request.GET['export'])
|
||||||
queryset = self.filter_queryset(self.get_queryset())
|
queryset = self.filter_queryset(self.get_queryset())
|
||||||
return et.render_to_response(queryset)
|
return et.render_to_response(queryset)
|
||||||
|
@ -16,7 +16,7 @@ from django.core.validators import URLValidator
|
|||||||
# Environment setup
|
# Environment setup
|
||||||
#
|
#
|
||||||
|
|
||||||
VERSION = '3.0.5-dev'
|
VERSION = '3.0.6-dev'
|
||||||
|
|
||||||
# Hostname
|
# Hostname
|
||||||
HOSTNAME = platform.node()
|
HOSTNAME = platform.node()
|
||||||
|
@ -17,7 +17,7 @@ from .admin import admin_site
|
|||||||
|
|
||||||
openapi_info = openapi.Info(
|
openapi_info = openapi.Info(
|
||||||
title="NetBox API",
|
title="NetBox API",
|
||||||
default_version='v2',
|
default_version='v3',
|
||||||
description="API to access NetBox",
|
description="API to access NetBox",
|
||||||
terms_of_service="https://github.com/netbox-community/netbox",
|
terms_of_service="https://github.com/netbox-community/netbox",
|
||||||
license=openapi.License(name="Apache v2 License"),
|
license=openapi.License(name="Apache v2 License"),
|
||||||
@ -59,9 +59,9 @@ _patterns = [
|
|||||||
path('api/users/', include('users.api.urls')),
|
path('api/users/', include('users.api.urls')),
|
||||||
path('api/virtualization/', include('virtualization.api.urls')),
|
path('api/virtualization/', include('virtualization.api.urls')),
|
||||||
path('api/status/', StatusView.as_view(), name='api-status'),
|
path('api/status/', StatusView.as_view(), name='api-status'),
|
||||||
path('api/docs/', schema_view.with_ui('swagger'), name='api_docs'),
|
path('api/docs/', schema_view.with_ui('swagger', cache_timeout=86400), name='api_docs'),
|
||||||
path('api/redoc/', schema_view.with_ui('redoc'), name='api_redocs'),
|
path('api/redoc/', schema_view.with_ui('redoc', cache_timeout=86400), name='api_redocs'),
|
||||||
re_path(r'^api/swagger(?P<format>.json|.yaml)$', schema_view.without_ui(), name='schema_swagger'),
|
re_path(r'^api/swagger(?P<format>.json|.yaml)$', schema_view.without_ui(cache_timeout=86400), name='schema_swagger'),
|
||||||
|
|
||||||
# GraphQL
|
# GraphQL
|
||||||
path('graphql/', csrf_exempt(GraphQLView.as_view(graphiql=True, schema=schema)), name='graphql'),
|
path('graphql/', csrf_exempt(GraphQLView.as_view(graphiql=True, schema=schema)), name='graphql'),
|
||||||
|
BIN
netbox/project-static/dist/netbox.js
vendored
BIN
netbox/project-static/dist/netbox.js
vendored
Binary file not shown.
BIN
netbox/project-static/dist/netbox.js.map
vendored
BIN
netbox/project-static/dist/netbox.js.map
vendored
Binary file not shown.
@ -36,7 +36,7 @@ function handleSelectAllToggle(event: Event): void {
|
|||||||
|
|
||||||
if (table !== null) {
|
if (table !== null) {
|
||||||
for (const element of table.querySelectorAll<HTMLInputElement>(
|
for (const element of table.querySelectorAll<HTMLInputElement>(
|
||||||
'input[type="checkbox"][name="pk"]',
|
'tr:not(.d-none) input[type="checkbox"][name="pk"]',
|
||||||
)) {
|
)) {
|
||||||
if (tableSelectAll.checked) {
|
if (tableSelectAll.checked) {
|
||||||
// Check all PK checkboxes if the select all checkbox is checked.
|
// Check all PK checkboxes if the select all checkbox is checked.
|
||||||
|
@ -6,11 +6,15 @@
|
|||||||
lang="en"
|
lang="en"
|
||||||
data-netbox-url-name="{{ request.resolver_match.url_name }}"
|
data-netbox-url-name="{{ request.resolver_match.url_name }}"
|
||||||
data-netbox-base-path="{{ settings.BASE_PATH }}"
|
data-netbox-base-path="{{ settings.BASE_PATH }}"
|
||||||
{% if preferences|get_key:'ui.colormode' == 'dark'%}
|
{% with preferences|get_key:'ui.colormode' as color_mode %}
|
||||||
|
{% if color_mode == 'dark'%}
|
||||||
data-netbox-color-mode="dark"
|
data-netbox-color-mode="dark"
|
||||||
{% else %}
|
{% elif color_mode == 'light' %}
|
||||||
data-netbox-color-mode="light"
|
data-netbox-color-mode="light"
|
||||||
|
{% else %}
|
||||||
|
data-netbox-color-mode="unset"
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
>
|
>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
@ -23,34 +27,55 @@
|
|||||||
<title>{% block title %}Home{% endblock %} | NetBox</title>
|
<title>{% block title %}Home{% endblock %} | NetBox</title>
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
|
/**
|
||||||
|
* Set the color mode on the `<html/>` element and in local storage.
|
||||||
|
*/
|
||||||
|
function setMode(mode) {
|
||||||
|
document.documentElement.setAttribute("data-netbox-color-mode", mode);
|
||||||
|
localStorage.setItem("netbox-color-mode", mode);
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Determine the best initial color mode to use prior to rendering.
|
* Determine the best initial color mode to use prior to rendering.
|
||||||
*/
|
*/
|
||||||
(function () {
|
(function () {
|
||||||
|
try {
|
||||||
// Browser prefers dark color scheme.
|
// Browser prefers dark color scheme.
|
||||||
var preferDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
var preferDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
|
||||||
// Browser prefers light color scheme.
|
// Browser prefers light color scheme.
|
||||||
var preferLight = window.matchMedia('(prefers-color-scheme: light)').matches;
|
var preferLight = window.matchMedia("(prefers-color-scheme: light)").matches;
|
||||||
// Client NetBox color-mode override.
|
// Client NetBox color-mode override.
|
||||||
var clientMode = localStorage.getItem('netbox-color-mode');
|
var clientMode = localStorage.getItem("netbox-color-mode");
|
||||||
// NetBox server-rendered value.
|
// NetBox server-rendered value.
|
||||||
var serverMode = document.documentElement.getAttribute('data-netbox-color-mode');
|
var serverMode = document.documentElement.getAttribute("data-netbox-color-mode");
|
||||||
|
|
||||||
if ((clientMode !== null) && (clientMode !== serverMode)) {
|
if (clientMode === null && (serverMode === "light" || serverMode === "dark")) {
|
||||||
// If the client mode is set, use its value over the server's value.
|
// If the client mode is not set but the server mode is, use the server mode.
|
||||||
return document.documentElement.setAttribute('data-netbox-color-mode', clientMode);
|
return setMode(serverMode);
|
||||||
}
|
}
|
||||||
if (preferDark && serverMode === 'light') {
|
if (clientMode !== null && clientMode !== serverMode) {
|
||||||
// If the client value matches the server value, the browser preferrs dark-mode, but
|
// If the client mode is set and is different than the server mode, use the client mode
|
||||||
// the server value doesn't match the browser preference, use dark mode.
|
// over the server mode, as it should be more recent.
|
||||||
return document.documentElement.setAttribute('data-netbox-color-mode', 'dark');
|
return setMode(clientMode);
|
||||||
}
|
}
|
||||||
if (preferLight && serverMode === 'dark') {
|
if (clientMode === serverMode) {
|
||||||
// If the client value matches the server value, the browser preferrs dark-mode, but
|
// If the client and server modes match, use that value.
|
||||||
// the server value doesn't match the browser preference, use light mode.
|
return setMode(clientMode);
|
||||||
return document.documentElement.setAttribute('data-netbox-color-mode', 'light');
|
|
||||||
}
|
}
|
||||||
|
if (preferDark && serverMode === "unset") {
|
||||||
|
// If the server mode is not set but the browser prefers dark mode, use dark mode.
|
||||||
|
return setMode("dark");
|
||||||
|
}
|
||||||
|
if (preferLight && serverMode === "unset") {
|
||||||
|
// If the server mode is not set but the browser prefers light mode, use light mode.
|
||||||
|
return setMode("light");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// In the event of an error, log it to the console and set the mode to light mode.
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
return setMode("light");
|
||||||
})();
|
})();
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{# Static resources #}
|
{# Static resources #}
|
||||||
|
@ -72,6 +72,7 @@
|
|||||||
<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/image_attachments_panel.html' %}
|
||||||
{% plugin_right_page object %}
|
{% plugin_right_page object %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -290,22 +290,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="card">
|
{% include 'inc/image_attachments_panel.html' %}
|
||||||
<h5 class="card-header">
|
|
||||||
Images
|
|
||||||
</h5>
|
|
||||||
<div class="card-body">
|
|
||||||
{% include 'inc/image_attachments.html' with images=object.images.all %}
|
|
||||||
</div>
|
|
||||||
{% if perms.extras.add_imageattachment %}
|
|
||||||
<div class="card-footer text-end noprint">
|
|
||||||
<a href="{% url 'dcim:device_add_image' object_id=object.pk %}" class="btn btn-primary btn-sm">
|
|
||||||
<span class="mdi mdi-plus-thick" aria-hidden="true"></span>
|
|
||||||
Attach an Image
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
<div class="card noprint">
|
<div class="card noprint">
|
||||||
<h5 class="card-header">
|
<h5 class="card-header">
|
||||||
Related Devices
|
Related Devices
|
||||||
|
@ -59,22 +59,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col col-md-6">
|
<div class="col col-md-6">
|
||||||
{% include 'inc/custom_fields_panel.html' %}
|
{% include 'inc/custom_fields_panel.html' %}
|
||||||
<div class="card">
|
{% include 'inc/image_attachments_panel.html' %}
|
||||||
<h5 class="card-header">
|
|
||||||
Images
|
|
||||||
</h5>
|
|
||||||
<div class="card-body">
|
|
||||||
{% include 'inc/image_attachments.html' with images=object.images.all %}
|
|
||||||
</div>
|
|
||||||
{% if perms.extras.add_imageattachment %}
|
|
||||||
<div class="card-footer text-end noprint">
|
|
||||||
<a href="{% url 'dcim:location_add_image' object_id=object.pk %}" class="btn btn-primary btn-sm">
|
|
||||||
<span class="mdi mdi-plus-thick" aria-hidden="true"></span>
|
|
||||||
Attach an Image
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% plugin_right_page object %}
|
{% plugin_right_page object %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -39,11 +39,12 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='dcim:powerpanel_list' %}
|
||||||
{% plugin_left_page object %}
|
{% plugin_left_page object %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col col-md-6">
|
<div class="col col-md-6">
|
||||||
{% include 'inc/custom_fields_panel.html' %}
|
{% include 'inc/custom_fields_panel.html' %}
|
||||||
{% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='dcim:powerpanel_list' %}
|
{% include 'inc/image_attachments_panel.html' %}
|
||||||
{% plugin_right_page object %}
|
{% plugin_right_page object %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -210,22 +210,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="card">
|
{% include 'inc/image_attachments_panel.html' %}
|
||||||
<h5 class="card-header">
|
|
||||||
Images
|
|
||||||
</h5>
|
|
||||||
<div class="card-body">
|
|
||||||
{% include 'inc/image_attachments.html' with images=object.images.all %}
|
|
||||||
</div>
|
|
||||||
{% if perms.extras.add_imageattachment %}
|
|
||||||
<div class="card-footer text-end noprint">
|
|
||||||
<a href="{% url 'dcim:rack_add_image' object_id=object.pk %}" class="btn btn-primary btn-sm">
|
|
||||||
<i class="mdi mdi-plus-thick" aria-hidden="true"></i>
|
|
||||||
Attach an Image
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h5 class="card-header">
|
<h5 class="card-header">
|
||||||
Reservations
|
Reservations
|
||||||
|
@ -30,7 +30,7 @@
|
|||||||
{% if page %}
|
{% if page %}
|
||||||
<div style="white-space: nowrap; overflow-x: scroll;">
|
<div style="white-space: nowrap; overflow-x: scroll;">
|
||||||
{% for rack in page %}
|
{% for rack in page %}
|
||||||
<div style="display: inline-block; margin-right: 12px; width: 254px">
|
<div style="display: inline-block; margin-right: 12px">
|
||||||
<div style="margin-left: 30px">
|
<div style="margin-left: 30px">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<strong><a href="{% url 'dcim:rack' pk=rack.pk %}">{{ rack.name }}</a></strong>
|
<strong><a href="{% url 'dcim:rack' pk=rack.pk %}">{{ rack.name }}</a></strong>
|
||||||
|
@ -242,22 +242,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card">
|
{% include 'inc/image_attachments_panel.html' %}
|
||||||
<h5 class="card-header">
|
|
||||||
Images
|
|
||||||
</h5>
|
|
||||||
<div class="card-body">
|
|
||||||
{% include 'inc/image_attachments.html' with images=object.images.all %}
|
|
||||||
</div>
|
|
||||||
{% if perms.extras.add_imageattachment %}
|
|
||||||
<div class="card-footer text-end noprint">
|
|
||||||
<a href="{% url 'dcim:site_add_image' object_id=object.pk %}" class="btn btn-primary btn-sm">
|
|
||||||
<i class="mdi mdi-plus-thick" aria-hidden="true"></i>
|
|
||||||
Attach an image
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% plugin_right_page object %}
|
{% plugin_right_page object %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -4,10 +4,8 @@
|
|||||||
{% block title %}Add New Member to Virtual Chassis {{ virtual_chassis }}{% endblock %}
|
{% block title %}Add New Member to Virtual Chassis {{ virtual_chassis }}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<form action="" method="post" enctype="multipart/form-data" class="form form-horizontal">
|
<form action="" method="post" enctype="multipart/form-data" class="form-object-edit">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<div class="row mb-3">
|
|
||||||
<div class="col col-md-6">
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h5 class="card-header">Add New Member</h5>
|
<h5 class="card-header">Add New Member</h5>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
@ -15,14 +13,10 @@
|
|||||||
{% render_form membership_form %}
|
{% render_form membership_form %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="text-end my-3">
|
||||||
</div>
|
|
||||||
<div class="row mb-3">
|
|
||||||
<div class="col col-md-6 text-end">
|
|
||||||
<a href="{{ return_url }}" class="btn btn-outline-danger">Cancel</a>
|
<a href="{{ return_url }}" class="btn btn-outline-danger">Cancel</a>
|
||||||
<button type="submit" name="_addanother" class="btn btn-outline-primary">Add Another</button>
|
<button type="submit" name="_addanother" class="btn btn-outline-primary">Add Another</button>
|
||||||
<button type="submit" name="_save" class="btn btn-primary">Save</button>
|
<button type="submit" name="_save" class="btn btn-primary">Save</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -1,38 +0,0 @@
|
|||||||
{% load helpers %}
|
|
||||||
|
|
||||||
{% if images %}
|
|
||||||
<table class="table table-hover">
|
|
||||||
<tr>
|
|
||||||
<th>Name</th>
|
|
||||||
<th>Size</th>
|
|
||||||
<th>Created</th>
|
|
||||||
<th></th>
|
|
||||||
</tr>
|
|
||||||
{% for attachment in images %}
|
|
||||||
<tr{% if not attachment.size %} class="table-danger"{% endif %}>
|
|
||||||
<td>
|
|
||||||
<i class="mdi mdi-file-image-outline"></i>
|
|
||||||
<a class="image-preview" href="{{ attachment.image.url }}" target="_blank">{{ attachment }}</a>
|
|
||||||
</td>
|
|
||||||
<td>{{ attachment.size|filesizeformat }}</td>
|
|
||||||
<td>{{ attachment.created|annotated_date }}</td>
|
|
||||||
<td class="text-end noprint">
|
|
||||||
{% if perms.extras.change_imageattachment %}
|
|
||||||
<a href="{% url 'extras:imageattachment_edit' pk=attachment.pk %}" class="btn btn-warning btn-sm lh-1" title="Edit Image">
|
|
||||||
<i class="mdi mdi-pencil" aria-hidden="true"></i>
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
{% if perms.extras.delete_imageattachment %}
|
|
||||||
<a href="{% url 'extras:imageattachment_delete' pk=attachment.pk %}" class="btn btn-danger btn-sm lh-1" title="Delete Image">
|
|
||||||
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i>
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</table>
|
|
||||||
{% else %}
|
|
||||||
<div class="text-muted">
|
|
||||||
None
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
52
netbox/templates/inc/image_attachments_panel.html
Normal file
52
netbox/templates/inc/image_attachments_panel.html
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
{% load helpers %}
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h5 class="card-header">
|
||||||
|
Images
|
||||||
|
</h5>
|
||||||
|
<div class="card-body">
|
||||||
|
{% with images=object.images.all %}
|
||||||
|
{% if images.exists %}
|
||||||
|
<table class="table table-hover">
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Size</th>
|
||||||
|
<th>Created</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
{% for attachment in images %}
|
||||||
|
<tr{% if not attachment.size %} class="table-danger"{% endif %}>
|
||||||
|
<td>
|
||||||
|
<i class="mdi mdi-file-image-outline"></i>
|
||||||
|
<a class="image-preview" href="{{ attachment.image.url }}" target="_blank">{{ attachment }}</a>
|
||||||
|
</td>
|
||||||
|
<td>{{ attachment.size|filesizeformat }}</td>
|
||||||
|
<td>{{ attachment.created|annotated_date }}</td>
|
||||||
|
<td class="text-end noprint">
|
||||||
|
{% if perms.extras.change_imageattachment %}
|
||||||
|
<a href="{% url 'extras:imageattachment_edit' pk=attachment.pk %}" class="btn btn-warning btn-sm lh-1" title="Edit Image">
|
||||||
|
<i class="mdi mdi-pencil" aria-hidden="true"></i>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if perms.extras.delete_imageattachment %}
|
||||||
|
<a href="{% url 'extras:imageattachment_delete' pk=attachment.pk %}" class="btn btn-danger btn-sm lh-1" title="Delete Image">
|
||||||
|
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
{% else %}
|
||||||
|
<div class="text-muted">None</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
</div>
|
||||||
|
{% if perms.extras.add_imageattachment %}
|
||||||
|
<div class="card-footer text-end noprint">
|
||||||
|
<a href="{% url 'extras:imageattachment_add' %}?content_type={{ object|meta:"app_label" }}.{{ object|meta:"model_name" }}&object_id={{ object.pk }}" class="btn btn-primary btn-sm">
|
||||||
|
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> Attach an image
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
@ -25,11 +25,9 @@
|
|||||||
Child Ranges {% badge object.get_child_ranges.count %}
|
Child Ranges {% badge object.get_child_ranges.count %}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% if perms.ipam.view_ipaddress and object.status != 'container' %}
|
|
||||||
<li role="presentation" class="nav-item">
|
<li role="presentation" class="nav-item">
|
||||||
<a class="nav-link{% if active_tab == 'ip-addresses' %} active{% endif %}" href="{% url 'ipam:prefix_ipaddresses' pk=object.pk %}">
|
<a class="nav-link{% if active_tab == 'ip-addresses' %} active{% endif %}" href="{% url 'ipam:prefix_ipaddresses' pk=object.pk %}">
|
||||||
IP Addresses {% badge object.get_child_ips.count %}
|
IP Addresses {% badge object.get_child_ips.count %}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -24,7 +24,7 @@
|
|||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input type="checkbox" id="select-all" name="_all" class="form-check-input" />
|
<input type="checkbox" id="select-all" name="_all" class="form-check-input" />
|
||||||
<label for="select-all" class="form-check-label">
|
<label for="select-all" class="form-check-label">
|
||||||
Select <strong>all {{ table.rows|length }} {{ table.data.verbose_name_plural }}</strong> matching query
|
Select <strong>all {{ table.objects_count }} {{ table.data.verbose_name_plural }}</strong> matching query
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -29,13 +29,18 @@ class BaseTable(tables.Table):
|
|||||||
'class': 'table table-hover object-list',
|
'class': 'table table-hover object-list',
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, *args, user=None, **kwargs):
|
def __init__(self, *args, user=None, extra_columns=None, **kwargs):
|
||||||
# Add custom field columns
|
# Add custom field columns
|
||||||
obj_type = ContentType.objects.get_for_model(self._meta.model)
|
obj_type = ContentType.objects.get_for_model(self._meta.model)
|
||||||
for cf in CustomField.objects.filter(content_types=obj_type):
|
cf_columns = [
|
||||||
self.base_columns[f'cf_{cf.name}'] = CustomFieldColumn(cf)
|
(f'cf_{cf.name}', CustomFieldColumn(cf)) for cf in CustomField.objects.filter(content_types=obj_type)
|
||||||
|
]
|
||||||
|
if extra_columns is not None:
|
||||||
|
extra_columns.extend(cf_columns)
|
||||||
|
else:
|
||||||
|
extra_columns = cf_columns
|
||||||
|
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, extra_columns=extra_columns, **kwargs)
|
||||||
|
|
||||||
# Set default empty_text if none was provided
|
# Set default empty_text if none was provided
|
||||||
if self.empty_text is None:
|
if self.empty_text is None:
|
||||||
@ -50,17 +55,22 @@ class BaseTable(tables.Table):
|
|||||||
|
|
||||||
# Apply custom column ordering for user
|
# Apply custom column ordering for user
|
||||||
if user is not None and not isinstance(user, AnonymousUser):
|
if user is not None and not isinstance(user, AnonymousUser):
|
||||||
columns = user.config.get(f"tables.{self.__class__.__name__}.columns")
|
selected_columns = user.config.get(f"tables.{self.__class__.__name__}.columns")
|
||||||
if columns:
|
if selected_columns:
|
||||||
pk = self.base_columns.pop('pk', None)
|
pk = self.base_columns.pop('pk', None)
|
||||||
actions = self.base_columns.pop('actions', None)
|
actions = self.base_columns.pop('actions', None)
|
||||||
|
|
||||||
for name, column in self.base_columns.items():
|
for name, column in self.columns.items():
|
||||||
if name in columns:
|
if name in selected_columns:
|
||||||
self.columns.show(name)
|
self.columns.show(name)
|
||||||
else:
|
else:
|
||||||
self.columns.hide(name)
|
self.columns.hide(name)
|
||||||
self.sequence = [c for c in columns if c in self.base_columns]
|
# Rearrange the sequence to list selected columns first, followed by all remaining columns
|
||||||
|
# TODO: There's probably a more clever way to accomplish this
|
||||||
|
self.sequence = [
|
||||||
|
*[c for c in selected_columns if c in self.columns.names()],
|
||||||
|
*[c for c in self.columns.names() if c not in selected_columns]
|
||||||
|
]
|
||||||
|
|
||||||
# Always include PK and actions column, if defined on the table
|
# Always include PK and actions column, if defined on the table
|
||||||
if pk:
|
if pk:
|
||||||
@ -111,6 +121,16 @@ class BaseTable(tables.Table):
|
|||||||
def selected_columns(self):
|
def selected_columns(self):
|
||||||
return self._get_columns(visible=True)
|
return self._get_columns(visible=True)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def objects_count(self):
|
||||||
|
"""
|
||||||
|
Return the total number of real objects represented by the Table. This is useful when dealing with
|
||||||
|
prefixes/IP addresses/etc., where some table rows may represent available address space.
|
||||||
|
"""
|
||||||
|
if not hasattr(self, '_objects_count'):
|
||||||
|
self._objects_count = sum(1 for obj in self.data if getattr(obj, 'pk'))
|
||||||
|
return self._objects_count
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Table columns
|
# Table columns
|
||||||
@ -157,6 +177,25 @@ class BooleanColumn(tables.Column):
|
|||||||
return str(value)
|
return str(value)
|
||||||
|
|
||||||
|
|
||||||
|
class TemplateColumn(tables.TemplateColumn):
|
||||||
|
"""
|
||||||
|
Overrides the stock TemplateColumn to render a placeholder if the returned value is an empty string.
|
||||||
|
"""
|
||||||
|
PLACEHOLDER = mark_safe('—')
|
||||||
|
|
||||||
|
def render(self, *args, **kwargs):
|
||||||
|
ret = super().render(*args, **kwargs)
|
||||||
|
if not ret.strip():
|
||||||
|
return self.PLACEHOLDER
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def value(self, **kwargs):
|
||||||
|
ret = super().value(**kwargs)
|
||||||
|
if ret == self.PLACEHOLDER:
|
||||||
|
return ''
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
class ButtonsColumn(tables.TemplateColumn):
|
class ButtonsColumn(tables.TemplateColumn):
|
||||||
"""
|
"""
|
||||||
Render edit, delete, and changelog buttons for an object.
|
Render edit, delete, and changelog buttons for an object.
|
||||||
|
@ -398,6 +398,9 @@ def applied_filters(form, query_params):
|
|||||||
|
|
||||||
applied_filters = []
|
applied_filters = []
|
||||||
for filter_name in form.changed_data:
|
for filter_name in form.changed_data:
|
||||||
|
if filter_name not in form.cleaned_data:
|
||||||
|
continue
|
||||||
|
|
||||||
querydict = query_params.copy()
|
querydict = query_params.copy()
|
||||||
if filter_name not in querydict:
|
if filter_name not in querydict:
|
||||||
continue
|
continue
|
||||||
|
@ -18,10 +18,13 @@ gunicorn==20.1.0
|
|||||||
Jinja2==3.0.1
|
Jinja2==3.0.1
|
||||||
Markdown==3.3.4
|
Markdown==3.3.4
|
||||||
markdown-include==0.6.0
|
markdown-include==0.6.0
|
||||||
mkdocs-material==7.3.0
|
mkdocs-material==7.3.1
|
||||||
netaddr==0.8.0
|
netaddr==0.8.0
|
||||||
Pillow==8.3.2
|
Pillow==8.3.2
|
||||||
psycopg2-binary==2.9.1
|
psycopg2-binary==2.9.1
|
||||||
PyYAML==5.4.1
|
PyYAML==5.4.1
|
||||||
svgwrite==1.4.1
|
svgwrite==1.4.1
|
||||||
tablib==3.0.0
|
tablib==3.0.0
|
||||||
|
|
||||||
|
# Workaround for #7401
|
||||||
|
jsonschema==3.2.0
|
||||||
|
16
upgrade.sh
16
upgrade.sh
@ -61,22 +61,6 @@ else
|
|||||||
echo "Skipping local dependencies (local_requirements.txt not found)"
|
echo "Skipping local dependencies (local_requirements.txt not found)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Test schema migrations integrity
|
|
||||||
COMMAND="python3 netbox/manage.py showmigrations"
|
|
||||||
eval $COMMAND > /dev/null 2>&1 || {
|
|
||||||
echo "--------------------------------------------------------------------"
|
|
||||||
echo "ERROR: Database schema migrations are out of synchronization. (No"
|
|
||||||
echo "data has been lost.) If attempting to upgrade to NetBox v3.0 or"
|
|
||||||
echo "later, first upgrade to a v2.11 release to ensure schema migrations"
|
|
||||||
echo "have been correctly prepared. For further detail on the exact error,"
|
|
||||||
echo "run the following commands:"
|
|
||||||
echo ""
|
|
||||||
echo " source ${VIRTUALENV}/bin/activate"
|
|
||||||
echo " ${COMMAND}"
|
|
||||||
echo "--------------------------------------------------------------------"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# Apply any database migrations
|
# Apply any database migrations
|
||||||
COMMAND="python3 netbox/manage.py migrate"
|
COMMAND="python3 netbox/manage.py migrate"
|
||||||
echo "Applying database migrations ($COMMAND)..."
|
echo "Applying database migrations ($COMMAND)..."
|
||||||
|
Loading…
Reference in New Issue
Block a user