mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-17 04:32:51 -06:00
Merge remote-tracking branch 'upstream/develop' into napalm_lookup_hostname
This commit is contained in:
commit
b69d2f1367
4
.gitattributes
vendored
4
.gitattributes
vendored
@ -1 +1,5 @@
|
|||||||
*.sh text eol=lf
|
*.sh text eol=lf
|
||||||
|
# Treat minified or packed JS/CSS files as binary, as they're not meant to be human-readable
|
||||||
|
*.min.* binary
|
||||||
|
*.map binary
|
||||||
|
*.pack.js binary
|
||||||
|
@ -41,7 +41,14 @@ Create a file at `/docs/release-notes/X.Y.md` to establish the release notes for
|
|||||||
|
|
||||||
### Manually Perform a New Install
|
### Manually Perform a New Install
|
||||||
|
|
||||||
Create a new installation of NetBox by following [the current documentation](http://netbox.readthedocs.io/en/latest/). This should be a manual process, so that issues with the documentation can be identified and corrected.
|
Install `mkdocs` in your local environment, then start the documentation server:
|
||||||
|
|
||||||
|
```no-highlight
|
||||||
|
$ pip install -r docs/requirements.txt
|
||||||
|
$ mkdocs serve
|
||||||
|
```
|
||||||
|
|
||||||
|
Follow these instructions to perform a new installation of NetBox. This process must _not_ be automated: The goal of this step is to catch any errors or omissions in the documentation, and ensure that it is kept up-to-date for each release. Make any necessary changes to the documentation before proceeding with the release.
|
||||||
|
|
||||||
### Close the Release Milestone
|
### Close the Release Milestone
|
||||||
|
|
||||||
|
3
docs/models/extras/imageattachment.md
Normal file
3
docs/models/extras/imageattachment.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# Image Attachments
|
||||||
|
|
||||||
|
Certain objects in NetBox support the attachment of uploaded images. These will be saved to the NetBox server and made available whenever the object is viewed.
|
@ -1,12 +1,35 @@
|
|||||||
# NetBox v2.8
|
# NetBox v2.8
|
||||||
|
|
||||||
## v2.8.8 (FUTURE)
|
## v2.8.9 (FUTURE)
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|
||||||
|
* [#4875](https://github.com/netbox-community/netbox/issues/4875) - Fix documentation for image attachments
|
||||||
|
* [#4876](https://github.com/netbox-community/netbox/issues/4876) - Fix labels for sites in staging or decommissioning status
|
||||||
|
* [#4880](https://github.com/netbox-community/netbox/issues/4880) - Fix remove tagged vlans if not assigned in bulk interface editting
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v2.8.8 (2020-07-21)
|
||||||
|
|
||||||
|
### Enhancements
|
||||||
|
|
||||||
|
* [#4805](https://github.com/netbox-community/netbox/issues/4805) - Improve handling of plugin loading errors
|
||||||
|
* [#4829](https://github.com/netbox-community/netbox/issues/4829) - Add NEMA 15 power port and outlet types
|
||||||
|
* [#4831](https://github.com/netbox-community/netbox/issues/4831) - Allow NAPALM to resolve device name when primary IP is not set
|
||||||
|
* [#4854](https://github.com/netbox-community/netbox/issues/4854) - Add staging and decommissioning statuses for sites
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* [#3240](https://github.com/netbox-community/netbox/issues/3240) - Correct OpenAPI definition for available-prefixes endpoint
|
||||||
|
* [#4595](https://github.com/netbox-community/netbox/issues/4595) - Ensure consistent display of non-racked and child devices on rack view
|
||||||
|
* [#4803](https://github.com/netbox-community/netbox/issues/4803) - Return IP family (4 or 6) as integer rather than string
|
||||||
* [#4821](https://github.com/netbox-community/netbox/issues/4821) - Restrict group options by selected site when bulk editing VLANs
|
* [#4821](https://github.com/netbox-community/netbox/issues/4821) - Restrict group options by selected site when bulk editing VLANs
|
||||||
* [#4835](https://github.com/netbox-community/netbox/issues/4835) - Support passing multiple initial values for multiple choice fields
|
* [#4835](https://github.com/netbox-community/netbox/issues/4835) - Support passing multiple initial values for multiple choice fields
|
||||||
* [#4838](https://github.com/netbox-community/netbox/issues/4838) - Fix rack power utilization display for racks without devices
|
* [#4838](https://github.com/netbox-community/netbox/issues/4838) - Fix rack power utilization display for racks without devices
|
||||||
|
* [#4851](https://github.com/netbox-community/netbox/issues/4851) - Show locally connected peer on circuit terminations
|
||||||
|
* [#4856](https://github.com/netbox-community/netbox/issues/4856) - Redirect user back to circuit after connecting a termination
|
||||||
|
* [#4872](https://github.com/netbox-community/netbox/issues/4872) - Enable filtering virtual machine interfaces by tag
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -7,13 +7,17 @@ from utilities.choices import ChoiceSet
|
|||||||
|
|
||||||
class SiteStatusChoices(ChoiceSet):
|
class SiteStatusChoices(ChoiceSet):
|
||||||
|
|
||||||
STATUS_ACTIVE = 'active'
|
|
||||||
STATUS_PLANNED = 'planned'
|
STATUS_PLANNED = 'planned'
|
||||||
|
STATUS_STAGING = 'staging'
|
||||||
|
STATUS_ACTIVE = 'active'
|
||||||
|
STATUS_DECOMMISSIONING = 'decommissioning'
|
||||||
STATUS_RETIRED = 'retired'
|
STATUS_RETIRED = 'retired'
|
||||||
|
|
||||||
CHOICES = (
|
CHOICES = (
|
||||||
(STATUS_ACTIVE, 'Active'),
|
|
||||||
(STATUS_PLANNED, 'Planned'),
|
(STATUS_PLANNED, 'Planned'),
|
||||||
|
(STATUS_STAGING, 'Staging'),
|
||||||
|
(STATUS_ACTIVE, 'Active'),
|
||||||
|
(STATUS_DECOMMISSIONING, 'Decommissioning'),
|
||||||
(STATUS_RETIRED, 'Retired'),
|
(STATUS_RETIRED, 'Retired'),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -275,6 +279,11 @@ class PowerPortTypeChoices(ChoiceSet):
|
|||||||
TYPE_NEMA_1430P = 'nema-14-30p'
|
TYPE_NEMA_1430P = 'nema-14-30p'
|
||||||
TYPE_NEMA_1450P = 'nema-14-50p'
|
TYPE_NEMA_1450P = 'nema-14-50p'
|
||||||
TYPE_NEMA_1460P = 'nema-14-60p'
|
TYPE_NEMA_1460P = 'nema-14-60p'
|
||||||
|
TYPE_NEMA_1515P = 'nema-15-15p'
|
||||||
|
TYPE_NEMA_1520P = 'nema-15-20p'
|
||||||
|
TYPE_NEMA_1530P = 'nema-15-30p'
|
||||||
|
TYPE_NEMA_1550P = 'nema-15-50p'
|
||||||
|
TYPE_NEMA_1560P = 'nema-15-60p'
|
||||||
# NEMA locking
|
# NEMA locking
|
||||||
TYPE_NEMA_L115P = 'nema-l1-15p'
|
TYPE_NEMA_L115P = 'nema-l1-15p'
|
||||||
TYPE_NEMA_L515P = 'nema-l5-15p'
|
TYPE_NEMA_L515P = 'nema-l5-15p'
|
||||||
@ -290,6 +299,10 @@ class PowerPortTypeChoices(ChoiceSet):
|
|||||||
TYPE_NEMA_L1430P = 'nema-l14-30p'
|
TYPE_NEMA_L1430P = 'nema-l14-30p'
|
||||||
TYPE_NEMA_L1450P = 'nema-l14-50p'
|
TYPE_NEMA_L1450P = 'nema-l14-50p'
|
||||||
TYPE_NEMA_L1460P = 'nema-l14-60p'
|
TYPE_NEMA_L1460P = 'nema-l14-60p'
|
||||||
|
TYPE_NEMA_L1520P = 'nema-l15-20p'
|
||||||
|
TYPE_NEMA_L1530P = 'nema-l15-30p'
|
||||||
|
TYPE_NEMA_L1550P = 'nema-l15-50p'
|
||||||
|
TYPE_NEMA_L1560P = 'nema-l15-60p'
|
||||||
TYPE_NEMA_L2120P = 'nema-l21-20p'
|
TYPE_NEMA_L2120P = 'nema-l21-20p'
|
||||||
TYPE_NEMA_L2130P = 'nema-l21-30p'
|
TYPE_NEMA_L2130P = 'nema-l21-30p'
|
||||||
# California style
|
# California style
|
||||||
@ -351,6 +364,11 @@ class PowerPortTypeChoices(ChoiceSet):
|
|||||||
(TYPE_NEMA_1430P, 'NEMA 14-30P'),
|
(TYPE_NEMA_1430P, 'NEMA 14-30P'),
|
||||||
(TYPE_NEMA_1450P, 'NEMA 14-50P'),
|
(TYPE_NEMA_1450P, 'NEMA 14-50P'),
|
||||||
(TYPE_NEMA_1460P, 'NEMA 14-60P'),
|
(TYPE_NEMA_1460P, 'NEMA 14-60P'),
|
||||||
|
(TYPE_NEMA_1515P, 'NEMA 15-15P'),
|
||||||
|
(TYPE_NEMA_1520P, 'NEMA 15-20P'),
|
||||||
|
(TYPE_NEMA_1530P, 'NEMA 15-30P'),
|
||||||
|
(TYPE_NEMA_1550P, 'NEMA 15-50P'),
|
||||||
|
(TYPE_NEMA_1560P, 'NEMA 15-60P'),
|
||||||
)),
|
)),
|
||||||
('NEMA (Locking)', (
|
('NEMA (Locking)', (
|
||||||
(TYPE_NEMA_L115P, 'NEMA L1-15P'),
|
(TYPE_NEMA_L115P, 'NEMA L1-15P'),
|
||||||
@ -367,6 +385,10 @@ class PowerPortTypeChoices(ChoiceSet):
|
|||||||
(TYPE_NEMA_L1430P, 'NEMA L14-30P'),
|
(TYPE_NEMA_L1430P, 'NEMA L14-30P'),
|
||||||
(TYPE_NEMA_L1450P, 'NEMA L14-50P'),
|
(TYPE_NEMA_L1450P, 'NEMA L14-50P'),
|
||||||
(TYPE_NEMA_L1460P, 'NEMA L14-60P'),
|
(TYPE_NEMA_L1460P, 'NEMA L14-60P'),
|
||||||
|
(TYPE_NEMA_L1520P, 'NEMA L15-20P'),
|
||||||
|
(TYPE_NEMA_L1530P, 'NEMA L15-30P'),
|
||||||
|
(TYPE_NEMA_L1550P, 'NEMA L15-50P'),
|
||||||
|
(TYPE_NEMA_L1560P, 'NEMA L15-60P'),
|
||||||
(TYPE_NEMA_L2120P, 'NEMA L21-20P'),
|
(TYPE_NEMA_L2120P, 'NEMA L21-20P'),
|
||||||
(TYPE_NEMA_L2130P, 'NEMA L21-30P'),
|
(TYPE_NEMA_L2130P, 'NEMA L21-30P'),
|
||||||
)),
|
)),
|
||||||
@ -436,6 +458,11 @@ class PowerOutletTypeChoices(ChoiceSet):
|
|||||||
TYPE_NEMA_1430R = 'nema-14-30r'
|
TYPE_NEMA_1430R = 'nema-14-30r'
|
||||||
TYPE_NEMA_1450R = 'nema-14-50r'
|
TYPE_NEMA_1450R = 'nema-14-50r'
|
||||||
TYPE_NEMA_1460R = 'nema-14-60r'
|
TYPE_NEMA_1460R = 'nema-14-60r'
|
||||||
|
TYPE_NEMA_1515R = 'nema-15-15r'
|
||||||
|
TYPE_NEMA_1520R = 'nema-15-20r'
|
||||||
|
TYPE_NEMA_1530R = 'nema-15-30r'
|
||||||
|
TYPE_NEMA_1550R = 'nema-15-50r'
|
||||||
|
TYPE_NEMA_1560R = 'nema-15-60r'
|
||||||
# NEMA locking
|
# NEMA locking
|
||||||
TYPE_NEMA_L115R = 'nema-l1-15r'
|
TYPE_NEMA_L115R = 'nema-l1-15r'
|
||||||
TYPE_NEMA_L515R = 'nema-l5-15r'
|
TYPE_NEMA_L515R = 'nema-l5-15r'
|
||||||
@ -451,6 +478,10 @@ class PowerOutletTypeChoices(ChoiceSet):
|
|||||||
TYPE_NEMA_L1430R = 'nema-l14-30r'
|
TYPE_NEMA_L1430R = 'nema-l14-30r'
|
||||||
TYPE_NEMA_L1450R = 'nema-l14-50r'
|
TYPE_NEMA_L1450R = 'nema-l14-50r'
|
||||||
TYPE_NEMA_L1460R = 'nema-l14-60r'
|
TYPE_NEMA_L1460R = 'nema-l14-60r'
|
||||||
|
TYPE_NEMA_L1520R = 'nema-l15-20r'
|
||||||
|
TYPE_NEMA_L1530R = 'nema-l15-30r'
|
||||||
|
TYPE_NEMA_L1550R = 'nema-l15-50r'
|
||||||
|
TYPE_NEMA_L1560R = 'nema-l15-60r'
|
||||||
TYPE_NEMA_L2120R = 'nema-l21-20r'
|
TYPE_NEMA_L2120R = 'nema-l21-20r'
|
||||||
TYPE_NEMA_L2130R = 'nema-l21-30r'
|
TYPE_NEMA_L2130R = 'nema-l21-30r'
|
||||||
# California style
|
# California style
|
||||||
@ -513,6 +544,11 @@ class PowerOutletTypeChoices(ChoiceSet):
|
|||||||
(TYPE_NEMA_1430R, 'NEMA 14-30R'),
|
(TYPE_NEMA_1430R, 'NEMA 14-30R'),
|
||||||
(TYPE_NEMA_1450R, 'NEMA 14-50R'),
|
(TYPE_NEMA_1450R, 'NEMA 14-50R'),
|
||||||
(TYPE_NEMA_1460R, 'NEMA 14-60R'),
|
(TYPE_NEMA_1460R, 'NEMA 14-60R'),
|
||||||
|
(TYPE_NEMA_1515R, 'NEMA 15-15R'),
|
||||||
|
(TYPE_NEMA_1520R, 'NEMA 15-20R'),
|
||||||
|
(TYPE_NEMA_1530R, 'NEMA 15-30R'),
|
||||||
|
(TYPE_NEMA_1550R, 'NEMA 15-50R'),
|
||||||
|
(TYPE_NEMA_1560R, 'NEMA 15-60R'),
|
||||||
)),
|
)),
|
||||||
('NEMA (Locking)', (
|
('NEMA (Locking)', (
|
||||||
(TYPE_NEMA_L115R, 'NEMA L1-15R'),
|
(TYPE_NEMA_L115R, 'NEMA L1-15R'),
|
||||||
@ -529,6 +565,10 @@ class PowerOutletTypeChoices(ChoiceSet):
|
|||||||
(TYPE_NEMA_L1430R, 'NEMA L14-30R'),
|
(TYPE_NEMA_L1430R, 'NEMA L14-30R'),
|
||||||
(TYPE_NEMA_L1450R, 'NEMA L14-50R'),
|
(TYPE_NEMA_L1450R, 'NEMA L14-50R'),
|
||||||
(TYPE_NEMA_L1460R, 'NEMA L14-60R'),
|
(TYPE_NEMA_L1460R, 'NEMA L14-60R'),
|
||||||
|
(TYPE_NEMA_L1520R, 'NEMA L15-20R'),
|
||||||
|
(TYPE_NEMA_L1530R, 'NEMA L15-30R'),
|
||||||
|
(TYPE_NEMA_L1550R, 'NEMA L15-50R'),
|
||||||
|
(TYPE_NEMA_L1560R, 'NEMA L15-60R'),
|
||||||
(TYPE_NEMA_L2120R, 'NEMA L21-20R'),
|
(TYPE_NEMA_L2120R, 'NEMA L21-20R'),
|
||||||
(TYPE_NEMA_L2130R, 'NEMA L21-30R'),
|
(TYPE_NEMA_L2130R, 'NEMA L21-30R'),
|
||||||
)),
|
)),
|
||||||
|
@ -254,8 +254,10 @@ class Site(ChangeLoggedModel, CustomFieldModel):
|
|||||||
]
|
]
|
||||||
|
|
||||||
STATUS_CLASS_MAP = {
|
STATUS_CLASS_MAP = {
|
||||||
SiteStatusChoices.STATUS_ACTIVE: 'success',
|
|
||||||
SiteStatusChoices.STATUS_PLANNED: 'info',
|
SiteStatusChoices.STATUS_PLANNED: 'info',
|
||||||
|
SiteStatusChoices.STATUS_STAGING: 'primary',
|
||||||
|
SiteStatusChoices.STATUS_ACTIVE: 'success',
|
||||||
|
SiteStatusChoices.STATUS_DECOMMISSIONING: 'warning',
|
||||||
SiteStatusChoices.STATUS_RETIRED: 'danger',
|
SiteStatusChoices.STATUS_RETIRED: 'danger',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -399,11 +399,12 @@ class RackView(PermissionRequiredMixin, View):
|
|||||||
|
|
||||||
rack = get_object_or_404(Rack.objects.prefetch_related('site__region', 'tenant__group', 'group', 'role'), pk=pk)
|
rack = get_object_or_404(Rack.objects.prefetch_related('site__region', 'tenant__group', 'group', 'role'), pk=pk)
|
||||||
|
|
||||||
|
# Get 0U and child devices located within the rack
|
||||||
nonracked_devices = Device.objects.filter(
|
nonracked_devices = Device.objects.filter(
|
||||||
rack=rack,
|
rack=rack,
|
||||||
position__isnull=True,
|
position__isnull=True
|
||||||
parent_bay__isnull=True
|
|
||||||
).prefetch_related('device_type__manufacturer')
|
).prefetch_related('device_type__manufacturer')
|
||||||
|
|
||||||
if rack.group:
|
if rack.group:
|
||||||
peer_racks = Rack.objects.filter(site=rack.site, group=rack.group)
|
peer_racks = Rack.objects.filter(site=rack.site, group=rack.group)
|
||||||
else:
|
else:
|
||||||
|
@ -6,11 +6,12 @@ from django.apps import AppConfig
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
from django.template.loader import get_template
|
from django.template.loader import get_template
|
||||||
from django.utils.module_loading import import_string
|
|
||||||
|
|
||||||
from extras.registry import registry
|
from extras.registry import registry
|
||||||
from utilities.choices import ButtonColorChoices
|
from utilities.choices import ButtonColorChoices
|
||||||
|
|
||||||
|
from extras.plugins.utils import import_object
|
||||||
|
|
||||||
|
|
||||||
# Initialize plugin registry stores
|
# Initialize plugin registry stores
|
||||||
registry['plugin_template_extensions'] = collections.defaultdict(list)
|
registry['plugin_template_extensions'] = collections.defaultdict(list)
|
||||||
@ -60,18 +61,14 @@ class PluginConfig(AppConfig):
|
|||||||
def ready(self):
|
def ready(self):
|
||||||
|
|
||||||
# Register template content
|
# Register template content
|
||||||
try:
|
template_extensions = import_object(f"{self.__module__}.{self.template_extensions}")
|
||||||
template_extensions = import_string(f"{self.__module__}.{self.template_extensions}")
|
if template_extensions is not None:
|
||||||
register_template_extensions(template_extensions)
|
register_template_extensions(template_extensions)
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Register navigation menu items (if defined)
|
# Register navigation menu items (if defined)
|
||||||
try:
|
menu_items = import_object(f"{self.__module__}.{self.menu_items}")
|
||||||
menu_items = import_string(f"{self.__module__}.{self.menu_items}")
|
if menu_items is not None:
|
||||||
register_menu_items(self.verbose_name, menu_items)
|
register_menu_items(self.verbose_name, menu_items)
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate(cls, user_config):
|
def validate(cls, user_config):
|
||||||
|
@ -3,7 +3,8 @@ from django.conf import settings
|
|||||||
from django.conf.urls import include
|
from django.conf.urls import include
|
||||||
from django.contrib.admin.views.decorators import staff_member_required
|
from django.contrib.admin.views.decorators import staff_member_required
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
from django.utils.module_loading import import_string
|
|
||||||
|
from extras.plugins.utils import import_object
|
||||||
|
|
||||||
from . import views
|
from . import views
|
||||||
|
|
||||||
@ -24,19 +25,15 @@ for plugin_path in settings.PLUGINS:
|
|||||||
base_url = getattr(app, 'base_url') or app.label
|
base_url = getattr(app, 'base_url') or app.label
|
||||||
|
|
||||||
# Check if the plugin specifies any base URLs
|
# Check if the plugin specifies any base URLs
|
||||||
try:
|
urlpatterns = import_object(f"{plugin_path}.urls.urlpatterns")
|
||||||
urlpatterns = import_string(f"{plugin_path}.urls.urlpatterns")
|
if urlpatterns is not None:
|
||||||
plugin_patterns.append(
|
plugin_patterns.append(
|
||||||
path(f"{base_url}/", include((urlpatterns, app.label)))
|
path(f"{base_url}/", include((urlpatterns, app.label)))
|
||||||
)
|
)
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Check if the plugin specifies any API URLs
|
# Check if the plugin specifies any API URLs
|
||||||
try:
|
urlpatterns = import_object(f"{plugin_path}.api.urls.urlpatterns")
|
||||||
urlpatterns = import_string(f"{plugin_path}.api.urls.urlpatterns")
|
if urlpatterns is not None:
|
||||||
plugin_api_patterns.append(
|
plugin_api_patterns.append(
|
||||||
path(f"{base_url}/", include((urlpatterns, f"{app.label}-api")))
|
path(f"{base_url}/", include((urlpatterns, f"{app.label}-api")))
|
||||||
)
|
)
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
|
33
netbox/extras/plugins/utils.py
Normal file
33
netbox/extras/plugins/utils.py
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import importlib.util
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def import_object(module_and_object):
|
||||||
|
"""
|
||||||
|
Import a specific object from a specific module by name, such as "extras.plugins.utils.import_object".
|
||||||
|
|
||||||
|
Returns the imported object, or None if it doesn't exist.
|
||||||
|
"""
|
||||||
|
target_module_name, object_name = module_and_object.rsplit('.', 1)
|
||||||
|
module_hierarchy = target_module_name.split('.')
|
||||||
|
|
||||||
|
# Iterate through the module hierarchy, checking for the existence of each successive submodule.
|
||||||
|
# We have to do this rather than jumping directly to calling find_spec(target_module_name)
|
||||||
|
# because find_spec will raise a ModuleNotFoundError if any parent module of target_module_name does not exist.
|
||||||
|
module_name = ""
|
||||||
|
for module_component in module_hierarchy:
|
||||||
|
module_name = f"{module_name}.{module_component}" if module_name else module_component
|
||||||
|
spec = importlib.util.find_spec(module_name)
|
||||||
|
if spec is None:
|
||||||
|
# No such module
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Okay, target_module_name exists. Load it if not already loaded
|
||||||
|
if target_module_name in sys.modules:
|
||||||
|
module = sys.modules[target_module_name]
|
||||||
|
else:
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
sys.modules[target_module_name] = module
|
||||||
|
spec.loader.exec_module(module)
|
||||||
|
|
||||||
|
return getattr(module, object_name, None)
|
@ -4,13 +4,14 @@ from django.apps import apps
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
from django.urls.exceptions import NoReverseMatch
|
from django.urls.exceptions import NoReverseMatch
|
||||||
from django.utils.module_loading import import_string
|
|
||||||
from django.views.generic import View
|
from django.views.generic import View
|
||||||
from rest_framework import permissions
|
from rest_framework import permissions
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.reverse import reverse
|
from rest_framework.reverse import reverse
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
|
|
||||||
|
from extras.plugins.utils import import_object
|
||||||
|
|
||||||
|
|
||||||
class InstalledPluginsAdminView(View):
|
class InstalledPluginsAdminView(View):
|
||||||
"""
|
"""
|
||||||
@ -60,9 +61,9 @@ class PluginsAPIRootView(APIView):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_plugin_entry(plugin, app_config, request, format):
|
def _get_plugin_entry(plugin, app_config, request, format):
|
||||||
try:
|
# Check if the plugin specifies any API URLs
|
||||||
api_app_name = import_string(f"{plugin}.api.urls.app_name")
|
api_app_name = import_object(f"{plugin}.api.urls.app_name")
|
||||||
except (ImportError, ModuleNotFoundError):
|
if api_app_name is None:
|
||||||
# Plugin does not expose an API
|
# Plugin does not expose an API
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@ -73,7 +74,7 @@ class PluginsAPIRootView(APIView):
|
|||||||
format=format
|
format=format
|
||||||
))
|
))
|
||||||
except NoReverseMatch:
|
except NoReverseMatch:
|
||||||
# The plugin does not include an api-root
|
# The plugin does not include an api-root url
|
||||||
entry = None
|
entry = None
|
||||||
|
|
||||||
return entry
|
return entry
|
||||||
|
@ -44,6 +44,7 @@ class NestedRIRSerializer(WritableNestedSerializer):
|
|||||||
|
|
||||||
class NestedAggregateSerializer(WritableNestedSerializer):
|
class NestedAggregateSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:aggregate-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:aggregate-detail')
|
||||||
|
family = serializers.IntegerField(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Aggregate
|
model = models.Aggregate
|
||||||
@ -87,6 +88,7 @@ class NestedVLANSerializer(WritableNestedSerializer):
|
|||||||
|
|
||||||
class NestedPrefixSerializer(WritableNestedSerializer):
|
class NestedPrefixSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:prefix-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:prefix-detail')
|
||||||
|
family = serializers.IntegerField(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Prefix
|
model = models.Prefix
|
||||||
@ -99,6 +101,7 @@ class NestedPrefixSerializer(WritableNestedSerializer):
|
|||||||
|
|
||||||
class NestedIPAddressSerializer(WritableNestedSerializer):
|
class NestedIPAddressSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:ipaddress-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:ipaddress-detail')
|
||||||
|
family = serializers.IntegerField(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.IPAddress
|
model = models.IPAddress
|
||||||
|
@ -74,6 +74,11 @@ class PrefixViewSet(CustomFieldModelViewSet):
|
|||||||
serializer_class = serializers.PrefixSerializer
|
serializer_class = serializers.PrefixSerializer
|
||||||
filterset_class = filters.PrefixFilterSet
|
filterset_class = filters.PrefixFilterSet
|
||||||
|
|
||||||
|
def get_serializer_class(self):
|
||||||
|
if self.action == "available_prefixes" and self.request.method == "POST":
|
||||||
|
return serializers.PrefixLengthSerializer
|
||||||
|
return super().get_serializer_class()
|
||||||
|
|
||||||
@swagger_auto_schema(method='get', responses={200: serializers.AvailablePrefixSerializer(many=True)})
|
@swagger_auto_schema(method='get', responses={200: serializers.AvailablePrefixSerializer(many=True)})
|
||||||
@swagger_auto_schema(method='post', responses={201: serializers.AvailablePrefixSerializer(many=True)})
|
@swagger_auto_schema(method='post', responses={201: serializers.AvailablePrefixSerializer(many=True)})
|
||||||
@action(detail=True, url_path='available-prefixes', methods=['get', 'post'])
|
@action(detail=True, url_path='available-prefixes', methods=['get', 'post'])
|
||||||
|
@ -16,7 +16,7 @@ from django.core.validators import URLValidator
|
|||||||
# Environment setup
|
# Environment setup
|
||||||
#
|
#
|
||||||
|
|
||||||
VERSION = '2.8.8-dev'
|
VERSION = '2.8.9-dev'
|
||||||
|
|
||||||
# Hostname
|
# Hostname
|
||||||
HOSTNAME = platform.node()
|
HOSTNAME = platform.node()
|
||||||
|
@ -51,10 +51,15 @@
|
|||||||
<a href="{% url 'circuits:circuittermination_trace' pk=termination.pk %}" class="btn btn-primary btn-xs" title="Trace">
|
<a href="{% url 'circuits:circuittermination_trace' pk=termination.pk %}" class="btn btn-primary btn-xs" title="Trace">
|
||||||
<i class="fa fa-share-alt" aria-hidden="true"></i>
|
<i class="fa fa-share-alt" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
{% if termination.connected_endpoint %}
|
{% with peer=termination.get_cable_peer %}
|
||||||
to <a href="{% url 'dcim:device' pk=termination.connected_endpoint.device.pk %}">{{ termination.connected_endpoint.device }}</a>
|
to
|
||||||
<i class="fa fa-angle-right"></i> {{ termination.connected_endpoint }}
|
{% if peer.device %}
|
||||||
|
<a href="{{ peer.device.get_absolute_url }}">{{ peer.device }}</a>
|
||||||
|
{% elif peer.circuit %}
|
||||||
|
<a href="{{ peer.circuit.get_absolute_url }}">{{ peer.circuit }}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
({{ peer }})
|
||||||
|
{% endwith %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{% if perms.dcim.add_cable %}
|
{% if perms.dcim.add_cable %}
|
||||||
<div class="pull-right">
|
<div class="pull-right">
|
||||||
@ -63,10 +68,10 @@
|
|||||||
<span class="glyphicon glyphicon-resize-small" aria-hidden="true"></span> Connect
|
<span class="glyphicon glyphicon-resize-small" aria-hidden="true"></span> Connect
|
||||||
</button>
|
</button>
|
||||||
<ul class="dropdown-menu dropdown-menu-right">
|
<ul class="dropdown-menu dropdown-menu-right">
|
||||||
<li><a href="{% url 'circuits:circuittermination_connect' termination_a_id=termination.pk termination_b_type='interface' %}?return_url={{ device.get_absolute_url }}">Interface</a></li>
|
<li><a href="{% url 'circuits:circuittermination_connect' termination_a_id=termination.pk termination_b_type='interface' %}?termination_b_site={{ termination.site.pk }}&return_url={{ circuit.get_absolute_url }}">Interface</a></li>
|
||||||
<li><a href="{% url 'circuits:circuittermination_connect' termination_a_id=termination.pk termination_b_type='front-port' %}?return_url={{ device.get_absolute_url }}">Front Port</a></li>
|
<li><a href="{% url 'circuits:circuittermination_connect' termination_a_id=termination.pk termination_b_type='front-port' %}?termination_b_site={{ termination.site.pk }}&return_url={{ circuit.get_absolute_url }}">Front Port</a></li>
|
||||||
<li><a href="{% url 'circuits:circuittermination_connect' termination_a_id=termination.pk termination_b_type='rear-port' %}?return_url={{ device.get_absolute_url }}">Rear Port</a></li>
|
<li><a href="{% url 'circuits:circuittermination_connect' termination_a_id=termination.pk termination_b_type='rear-port' %}?termination_b_site={{ termination.site.pk }}&return_url={{ circuit.get_absolute_url }}">Rear Port</a></li>
|
||||||
<li><a href="{% url 'circuits:circuittermination_connect' termination_a_id=termination.pk termination_b_type='circuit-termination' %}?return_url={{ device.get_absolute_url }}">Circuit Termination</a></li>
|
<li><a href="{% url 'circuits:circuittermination_connect' termination_a_id=termination.pk termination_b_type='circuit-termination' %}?termination_b_site={{ termination.site.pk }}&return_url={{ circuit.get_absolute_url }}">Circuit Termination</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -337,7 +337,7 @@
|
|||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
<th>Role</th>
|
<th>Role</th>
|
||||||
<th>Type</th>
|
<th>Type</th>
|
||||||
<th>Parent</th>
|
<th colspan="2">Parent Device</th>
|
||||||
</tr>
|
</tr>
|
||||||
{% for device in nonracked_devices %}
|
{% for device in nonracked_devices %}
|
||||||
<tr{% if device.device_type.u_height %} class="warning"{% endif %}>
|
<tr{% if device.device_type.u_height %} class="warning"{% endif %}>
|
||||||
@ -346,13 +346,12 @@
|
|||||||
</td>
|
</td>
|
||||||
<td>{{ device.device_role }}</td>
|
<td>{{ device.device_role }}</td>
|
||||||
<td>{{ device.device_type.display_name }}</td>
|
<td>{{ device.device_type.display_name }}</td>
|
||||||
<td>
|
|
||||||
{% if device.parent_bay %}
|
{% if device.parent_bay %}
|
||||||
<a href="{{ device.parent_bay.device.get_absolute_url }}">{{ device.parent_bay }}</a>
|
<td><a href="{{ device.parent_bay.device.get_absolute_url }}">{{ device.parent_bay.device }}</a></td>
|
||||||
|
<td>{{ device.parent_bay }}</td>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="text-muted">—</span>
|
<td colspan="2" class="text-muted">—</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</table>
|
</table>
|
||||||
|
@ -27,12 +27,12 @@ def _get_viewname(instance, action):
|
|||||||
|
|
||||||
@register.inclusion_tag('buttons/clone.html')
|
@register.inclusion_tag('buttons/clone.html')
|
||||||
def clone_button(instance):
|
def clone_button(instance):
|
||||||
viewname = _get_viewname(instance, 'add')
|
url = reverse(_get_viewname(instance, 'add'))
|
||||||
|
|
||||||
# Populate cloned field values
|
# Populate cloned field values
|
||||||
param_string = prepare_cloned_fields(instance)
|
param_string = prepare_cloned_fields(instance)
|
||||||
if param_string:
|
if param_string:
|
||||||
url = '{}?{}'.format(reverse(viewname), param_string)
|
url = f'{url}?{param_string}'
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'url': url,
|
'url': url,
|
||||||
|
@ -721,8 +721,8 @@ class BulkEditView(GetReturnURLMixin, View):
|
|||||||
|
|
||||||
# ManyToManyFields
|
# ManyToManyFields
|
||||||
elif isinstance(model_field, ManyToManyField):
|
elif isinstance(model_field, ManyToManyField):
|
||||||
|
if form.cleaned_data[name].count() > 0:
|
||||||
getattr(obj, name).set(form.cleaned_data[name])
|
getattr(obj, name).set(form.cleaned_data[name])
|
||||||
|
|
||||||
# Normal fields
|
# Normal fields
|
||||||
elif form.cleaned_data[name] not in (None, ''):
|
elif form.cleaned_data[name] not in (None, ''):
|
||||||
setattr(obj, name, form.cleaned_data[name])
|
setattr(obj, name, form.cleaned_data[name])
|
||||||
|
@ -220,6 +220,7 @@ class InterfaceFilterSet(BaseFilterSet):
|
|||||||
mac_address = MultiValueMACAddressFilter(
|
mac_address = MultiValueMACAddressFilter(
|
||||||
label='MAC address',
|
label='MAC address',
|
||||||
)
|
)
|
||||||
|
tag = TagFilter()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Interface
|
model = Interface
|
||||||
|
Loading…
Reference in New Issue
Block a user