Merge branch 'netbox-community:develop' into 11592-expose-setting

This commit is contained in:
Aron Bergur Jóhannsson 2023-02-13 09:21:03 +00:00 committed by GitHub
commit a65440d8f7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 185 additions and 149 deletions

View File

@ -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.4.3 placeholder: v3.4.4
validations: validations:
required: true required: true
- type: dropdown - type: dropdown

View File

@ -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.4.3 placeholder: v3.4.4
validations: validations:
required: true required: true
- type: dropdown - type: dropdown

119
README.md
View File

@ -1,71 +1,59 @@
<div align="center"> <div align="center">
<img src="https://raw.githubusercontent.com/netbox-community/netbox/develop/docs/netbox_logo.svg" width="400" alt="NetBox logo" /> <img src="https://raw.githubusercontent.com/netbox-community/netbox/develop/docs/netbox_logo.svg" width="400" alt="NetBox logo" />
The premiere source of truth powering network automation
</div> </div>
![Master branch build status](https://github.com/netbox-community/netbox/workflows/CI/badge.svg?branch=master)
NetBox is the leading solution for modeling and documenting modern networks. By NetBox is the leading solution for modeling and documenting modern networks. By
combining the traditional disciplines of IP address management (IPAM) and combining the traditional disciplines of IP address management (IPAM) and
datacenter infrastructure management (DCIM) with powerful APIs and extensions, datacenter infrastructure management (DCIM) with powerful APIs and extensions,
NetBox provides the ideal "source of truth" to power network automation. NetBox provides the ideal "source of truth" to power network automation.
Available as open source software under the Apache 2.0 license, NetBox is Available as open source software under the Apache 2.0 license, NetBox serves
employed by thousands of organizations around the world. as the cornerstone for network automation in thousands of organizations.
![Master branch build status](https://github.com/netbox-community/netbox/workflows/CI/badge.svg?branch=master) * **Physical infrasucture:** Accurately model the physical world, from global regions down to individual racks of gear. Then connect everything - network, console, and power!
* **Modern IPAM:** All the standard IPAM functionality you expect, plus VRF import/export tracking, VLAN management, and overlay support.
[![Timeline graph](https://images.repography.com/29023055/netbox-community/netbox/recent-activity/31db894eee74b8a5475e3af307a81b6c_timeline.svg)](https://github.com/netbox-community/netbox/commits) * **Data circuits:** Confidently manage the delivery of crtical circuits from various service providers, modeled seamlessly alongside your own infrastructure.
[![Issue status graph](https://images.repography.com/29023055/netbox-community/netbox/recent-activity/31db894eee74b8a5475e3af307a81b6c_issues.svg)](https://github.com/netbox-community/netbox/issues) * **Power tracking:** Map the distribution of power from upstream sources to individual feeds and outlets.
[![Pull request status graph](https://images.repography.com/29023055/netbox-community/netbox/recent-activity/31db894eee74b8a5475e3af307a81b6c_prs.svg)](https://github.com/netbox-community/netbox/pulls) * **Organization:** Manage tenant and contact assignments natively.
[![Top contributors](https://images.repography.com/29023055/netbox-community/netbox/recent-activity/31db894eee74b8a5475e3af307a81b6c_users.svg)](https://github.com/netbox-community/netbox/graphs/contributors) * **Powerful search:** Easily find anything you need using a single global search function.
<br />Stats via [Repography](https://repography.com) * **Comprehensive logging:** Leverage both automatic change logging and user-submitted journal entries to track your network's growth over time.
* **Endless customization:** Custom fields, custom links, tags, export templates, custom validation, reports, scripts, and more!
## About NetBox * **Flexible permissions:** An advanced permissions systems enables very flexible delegation of permissions.
* **Integrations:** Easily connect NetBox to your other tooling via its REST & GraphQL APIs.
* **Plugins:** Not finding what you need in the core application? Try one of many community plugins - or build your own!
![Screenshot of NetBox UI](docs/media/screenshots/netbox-ui.png "NetBox UI") ![Screenshot of NetBox UI](docs/media/screenshots/netbox-ui.png "NetBox UI")
Myriad infrastructure components can be modeled in NetBox, including: ## Getting Started
* Hierarchical regions, site groups, sites, and locations * Just want to explore? Check out [our public demo](https://demo.netbox.dev/) right now!
* Racks, devices, and device components * The [official documentation](https://docs.netbox.dev) offers a comprehensive introduction.
* Cables and wireless connections * Choose your deployment: [self-hosted](https://github.com/netbox-community/netbox), [Docker](https://github.com/netbox-community/netbox-docker), or [NetBox Cloud](https://netboxlabs.com/netbox-cloud/).
* Power distribution * Check out [our wiki](https://github.com/netbox-community/netbox/wiki/Community-Contributions) for even more projects to get the most out of NetBox!
* Data circuits and providers
* Virtual machines and clusters
* IP prefixes, ranges, and addresses
* VRFs and route targets
* L2VPN and overlays
* FHRP groups (VRRP, HSRP, etc.)
* AS numbers
* VLANs and scoped VLAN groups
* Organizational tenants and contacts
In addition to its extensive built-in models and functionality, NetBox can be ## Get Involved
customized and extended through the use of:
* Custom fields * Follow [@NetBoxOfficial](https://twitter.com/NetBoxOfficial) on Twitter!
* Custom links * Join the conversation on [the discussion forum](https://github.com/netbox-community/netbox/discussions) and [Slack](https://netdev.chat/)!
* Configuration contexts * Already a power user? You can [suggest a feature](https://github.com/netbox-community/netbox/issues/new?assignees=&labels=type%3A+feature&template=feature_request.yaml) or [report a bug](https://github.com/netbox-community/netbox/issues/new?assignees=&labels=type%3A+bug&template=bug_report.yaml) on GitHub.
* Custom model validation rules * Contributions from the community are encouraged and appreciated! Check out our [contributing guide](CONTRIBUTING.md) to get started.
* Reports
* Custom scripts
* Export templates
* Conditional webhooks
* Plugins
* Single sign-on (SSO) authentication
* NAPALM integration
* Detailed change logging
NetBox also features a complete REST API as well as a GraphQL API for easily ## Project Stats
integrating with other tools and systems.
<div align="center">
The complete documentation for NetBox can be found at [docs.netbox.dev](https://docs.netbox.dev/). <a href="https://github.com/netbox-community/netbox/commits"><img src="https://images.repography.com/29023055/netbox-community/netbox/recent-activity/31db894eee74b8a5475e3af307a81b6c_timeline.svg" alt="Timeline graph"></a>
A public demo instance is available at [demo.netbox.dev](https://demo.netbox.dev). <a href="https://github.com/netbox-community/netbox/issues"><img src="https://images.repography.com/29023055/netbox-community/netbox/recent-activity/31db894eee74b8a5475e3af307a81b6c_issues.svg" alt="Issues graph"></a>
<a href="https://github.com/netbox-community/netbox/pulls"><img src="https://images.repography.com/29023055/netbox-community/netbox/recent-activity/31db894eee74b8a5475e3af307a81b6c_prs.svg" alt="Pull requests graph"></a>
NetBox runs as a web application atop the [Django](https://www.djangoproject.com/) <a href="https://github.com/netbox-community/netbox/graphs/contributors"><img src="https://images.repography.com/29023055/netbox-community/netbox/recent-activity/31db894eee74b8a5475e3af307a81b6c_users.svg" alt="Top contributors"></a>
Python framework with a [PostgreSQL](https://www.postgresql.org/) database. For a <br />Stats via <a href="https://repography.com">Repography</a>
complete list of requirements, see `requirements.txt`. The code is available </div>
[on GitHub](https://github.com/netbox-community/netbox).
## Sponsors
<div align="center"> <div align="center">
<h3>Thank you to our sponsors!</h3>
[![NetBox Labs](https://raw.githubusercontent.com/wiki/netbox-community/netbox/images/sponsors/netbox_labs.png)](https://netboxlabs.com) [![NetBox Labs](https://raw.githubusercontent.com/wiki/netbox-community/netbox/images/sponsors/netbox_labs.png)](https://netboxlabs.com)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
@ -76,34 +64,10 @@ complete list of requirements, see `requirements.txt`. The code is available
[![Sentry](https://raw.githubusercontent.com/wiki/netbox-community/netbox/images/sponsors/sentry.png)](https://sentry.io) [![Sentry](https://raw.githubusercontent.com/wiki/netbox-community/netbox/images/sponsors/sentry.png)](https://sentry.io)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
[![Equinix Metal](https://raw.githubusercontent.com/wiki/netbox-community/netbox/images/sponsors/equinix.png)](https://metal.equinix.com) [![Equinix Metal](https://raw.githubusercontent.com/wiki/netbox-community/netbox/images/sponsors/equinix.png)](https://metal.equinix.com)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
[![Stellar Technologies](https://raw.githubusercontent.com/wiki/netbox-community/netbox/images/sponsors/stellar.png)](https://stellar.tech)
</div> </div>
### Discussion ## Screenshots
* [GitHub Discussions](https://github.com/netbox-community/netbox/discussions) - Discussion forum hosted by GitHub; ideal for Q&A and other structured discussions
* [Slack](https://netdev.chat/) - Real-time chat hosted by the NetDev Community; best for unstructured discussion or just hanging out
### Installation
Please see [the documentation](https://docs.netbox.dev/) for
instructions on installing NetBox. To upgrade NetBox, please download the
[latest release](https://github.com/netbox-community/netbox/releases) and
run `upgrade.sh`.
### Providing Feedback
The best platform for general feedback, assistance, and other discussion is our
[GitHub discussions](https://github.com/netbox-community/netbox/discussions).
To report a bug or request a specific feature, please open a GitHub issue using
the [appropriate template](https://github.com/netbox-community/netbox/issues/new/choose).
If you are interested in contributing to the development of NetBox, please read
our [contributing guide](CONTRIBUTING.md) prior to beginning any work.
### Screenshots
![Screenshot of main page (dark mode)](docs/media/screenshots/home-dark.png "Main page (dark mode)") ![Screenshot of main page (dark mode)](docs/media/screenshots/home-dark.png "Main page (dark mode)")
@ -112,8 +76,3 @@ our [contributing guide](CONTRIBUTING.md) prior to beginning any work.
![Screenshot of prefixes hierarchy](docs/media/screenshots/prefixes-list.png "Prefixes hierarchy") ![Screenshot of prefixes hierarchy](docs/media/screenshots/prefixes-list.png "Prefixes hierarchy")
![Screenshot of cable trace](docs/media/screenshots/cable-trace.png "Cable tracing") ![Screenshot of cable trace](docs/media/screenshots/cable-trace.png "Cable tracing")
### Related projects
Please see [our wiki](https://github.com/netbox-community/netbox/wiki/Community-Contributions)
for a list of relevant community projects.

View File

@ -1,6 +1,6 @@
# HTML sanitizer # HTML sanitizer
# https://github.com/mozilla/bleach # https://github.com/mozilla/bleach
bleach bleach<6.0
# The Python web framework on which NetBox is built # The Python web framework on which NetBox is built
# https://github.com/django/django # https://github.com/django/django

View File

@ -5,6 +5,7 @@ NetBox includes a `housekeeping` management command that should be run nightly.
* Clearing expired authentication sessions from the database * Clearing expired authentication sessions from the database
* Deleting changelog records older than the configured [retention time](../configuration/miscellaneous.md#changelog_retention) * Deleting changelog records older than the configured [retention time](../configuration/miscellaneous.md#changelog_retention)
* Deleting job result records older than the configured [retention time](../configuration/miscellaneous.md#jobresult_retention) * Deleting job result records older than the configured [retention time](../configuration/miscellaneous.md#jobresult_retention)
* Check for new NetBox releases (if [`RELEASE_CHECK_URL`](../configuration/miscellaneous.md#release_check_url) is set)
This command can be invoked directly, or by using the shell script provided at `/opt/netbox/contrib/netbox-housekeeping.sh`. This script can be linked from your cron scheduler's daily jobs directory (e.g. `/etc/cron.daily`) or referenced directly within the cron configuration file. This command can be invoked directly, or by using the shell script provided at `/opt/netbox/contrib/netbox-housekeeping.sh`. This script can be linked from your cron scheduler's daily jobs directory (e.g. `/etc/cron.daily`) or referenced directly within the cron configuration file.

View File

@ -142,6 +142,19 @@ obj.full_clean()
obj.save() obj.save()
``` ```
## Error handling
Sometimes things go wrong and a script will run into an `Exception`. If that happens and an uncaught exception is raised by the custom script, the execution is aborted and a full stack trace is reported.
Although this is helpful for debugging, in some situations it might be required to cleanly abort the execution of a custom script (e.g. because of invalid input data) and thereby make sure no changes are performed on the database. In this case the script can throw an `AbortScript` exception, which will prevent the stack trace from being reported, but still terminating the script's execution and reporting a given error message.
```python
from utilities.exceptions import AbortScript
if some_error:
raise AbortScript("Some meaningful error message")
```
## Variable Reference ## Variable Reference
### Default Options ### Default Options

View File

@ -54,15 +54,19 @@ Each model should have a corresponding FilterSet class defined. This is used to
Create a table class for the model in `tables.py` by subclassing `utilities.tables.BaseTable`. Under the table's `Meta` class, be sure to list both the fields and default columns. Create a table class for the model in `tables.py` by subclassing `utilities.tables.BaseTable`. Under the table's `Meta` class, be sure to list both the fields and default columns.
## 9. Create the object template ## 9. Create a SearchIndex subclass
If this model will be included in global search results, create a subclass of `netbox.search.SearchIndex` for it and specify the fields to be indexed.
## 10. Create the object template
Create the HTML template for the object view. (The other views each typically employ a generic template.) This template should extend `generic/object.html`. Create the HTML template for the object view. (The other views each typically employ a generic template.) This template should extend `generic/object.html`.
## 10. Add the model to the navigation menu ## 11. Add the model to the navigation menu
Add the relevant navigation menu items in `netbox/netbox/navigation/menu.py`. Add the relevant navigation menu items in `netbox/netbox/navigation/menu.py`.
## 11. REST API components ## 12. REST API components
Create the following for each model: Create the following for each model:
@ -71,13 +75,13 @@ Create the following for each model:
* API view in `api/views.py` * API view in `api/views.py`
* Endpoint route in `api/urls.py` * Endpoint route in `api/urls.py`
## 12. GraphQL API components ## 13. GraphQL API components
Create a Graphene object type for the model in `graphql/types.py` by subclassing the appropriate class from `netbox.graphql.types`. Create a Graphene object type for the model in `graphql/types.py` by subclassing the appropriate class from `netbox.graphql.types`.
Also extend the schema class defined in `graphql/schema.py` with the individual object and object list fields per the established convention. Also extend the schema class defined in `graphql/schema.py` with the individual object and object list fields per the established convention.
## 13. Add tests ## 14. Add tests
Add tests for the following: Add tests for the following:
@ -85,7 +89,7 @@ Add tests for the following:
* API views * API views
* Filter sets * Filter sets
## 14. Documentation ## 15. Documentation
Create a new documentation page for the model in `docs/models/<app_label>/<model_name>.md`. Include this file under the "features" documentation where appropriate. Create a new documentation page for the model in `docs/models/<app_label>/<model_name>.md`. Include this file under the "features" documentation where appropriate.

View File

@ -52,4 +52,4 @@ NetBox is built on the enormously popular [Django](http://www.djangoproject.com/
* Try out our [public demo](https://demo.netbox.dev/) if you want to jump right in * Try out our [public demo](https://demo.netbox.dev/) if you want to jump right in
* The [installation guide](./installation/index.md) will help you get your own deployment up and running * The [installation guide](./installation/index.md) will help you get your own deployment up and running
* Or try the community [Docker image](https://github.com/netbox-community/netbox-docker) for a low-touch approach * Or try the community [Docker image](https://github.com/netbox-community/netbox-docker) for a low-touch approach
* [NetBox Cloud](https://www.getnetbox.io/) is a hosted solution offered by NS1 * [NetBox Cloud](https://netboxlabs.com/netbox-cloud) is a managed solution offered by [NetBox Labs](https://netboxlabs.com/)

View File

@ -51,7 +51,7 @@ menu_items = (item1, item2, item3)
Each menu item represents a link and (optionally) a set of buttons comprising one entry in NetBox's navigation menu. Menu items are defined as PluginMenuItem instances. An example is shown below. Each menu item represents a link and (optionally) a set of buttons comprising one entry in NetBox's navigation menu. Menu items are defined as PluginMenuItem instances. An example is shown below.
```python filename="navigation.py" ```python title="navigation.py"
from extras.plugins import PluginMenuButton, PluginMenuItem from extras.plugins import PluginMenuButton, PluginMenuItem
from utilities.choices import ButtonColorChoices from utilities.choices import ButtonColorChoices

View File

@ -1,21 +1,38 @@
# NetBox v3.4 # NetBox v3.4
## v3.4.4 (FUTURE) ## v3.4.5 (FUTURE)
### Bug Fixes
* [#11032](https://github.com/netbox-community/netbox/issues/11032) - Fix false custom validation errors during component creation
* [#11582](https://github.com/netbox-community/netbox/issues/11582) - Ensure form validation errors are displayed when adding virtual chassis members
* [#11601](https://github.com/netbox-community/netbox/issues/11601) - Fix partial matching of start/end addresses for IP range search
* [#11683](https://github.com/netbox-community/netbox/issues/11683) - Fix CSV header attribute detection when auto-detecting import format
---
## v3.4.4 (2023-02-02)
### Enhancements ### Enhancements
* [#10762](https://github.com/netbox-community/netbox/issues/10762) - Permit selection custom fields to have only one choice * [#10762](https://github.com/netbox-community/netbox/issues/10762) - Permit selection custom fields to have only one choice
* [#11152](https://github.com/netbox-community/netbox/issues/11152) - Introduce AbortScript exception to elegantly abort scripts
* [#11554](https://github.com/netbox-community/netbox/issues/11554) - Add module types count to manufacturers list * [#11554](https://github.com/netbox-community/netbox/issues/11554) - Add module types count to manufacturers list
* [#11585](https://github.com/netbox-community/netbox/issues/11585) - Add IP address filters for services * [#11585](https://github.com/netbox-community/netbox/issues/11585) - Add IP address filters for services
* [#11598](https://github.com/netbox-community/netbox/issues/11598) - Add buttons to easily switch between rack list and elevations views * [#11598](https://github.com/netbox-community/netbox/issues/11598) - Add buttons to easily switch between rack list and elevations views
### Bug Fixes ### Bug Fixes
* [#11267](https://github.com/netbox-community/netbox/issues/11267) - Avoid catching ImportErrors when loading plugin resources
* [#11487](https://github.com/netbox-community/netbox/issues/11487) - Remove "set null" option from non-writable custom fields during bulk edit * [#11487](https://github.com/netbox-community/netbox/issues/11487) - Remove "set null" option from non-writable custom fields during bulk edit
* [#11491](https://github.com/netbox-community/netbox/issues/11491) - Show edit/delete buttons in user tokens table * [#11491](https://github.com/netbox-community/netbox/issues/11491) - Show edit/delete buttons in user tokens table
* [#11528](https://github.com/netbox-community/netbox/issues/11528) - Permit import of devices using uploaded file * [#11528](https://github.com/netbox-community/netbox/issues/11528) - Permit import of devices using uploaded file
* [#11555](https://github.com/netbox-community/netbox/issues/11555) - Avoid inadvertent interpretation of search query as regular expression under global search (previously [#11516](https://github.com/netbox-community/netbox/issues/11516)) * [#11555](https://github.com/netbox-community/netbox/issues/11555) - Avoid inadvertent interpretation of search query as regular expression under global search (previously [#11516](https://github.com/netbox-community/netbox/issues/11516))
* [#11562](https://github.com/netbox-community/netbox/issues/11562) - Correct ordering of virtual chassis interfaces with duplicate names * [#11562](https://github.com/netbox-community/netbox/issues/11562) - Correct ordering of virtual chassis interfaces with duplicate names
* [#11574](https://github.com/netbox-community/netbox/issues/11574) - Fix exception when attempting to schedule reports/scripts
* [#11620](https://github.com/netbox-community/netbox/issues/11620) - Correct available filter choices for interface PoE type
* [#11635](https://github.com/netbox-community/netbox/issues/11635) - Pre-populate assigned VRF when following "first available IP" link from prefix view
* [#11650](https://github.com/netbox-community/netbox/issues/11650) - Display error message when attempting to create device component with duplicate name
--- ---

View File

@ -1170,7 +1170,7 @@ class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
label='PoE mode' label='PoE mode'
) )
poe_type = MultipleChoiceField( poe_type = MultipleChoiceField(
choices=InterfacePoEModeChoices, choices=InterfacePoETypeChoices,
required=False, required=False,
label='PoE type' label='PoE type'
) )

View File

@ -45,12 +45,16 @@ class ScriptForm(BootstrapMixin, forms.Form):
self.fields['_interval'] = interval self.fields['_interval'] = interval
self.fields['_commit'] = commit self.fields['_commit'] = commit
def clean__schedule_at(self): def clean(self):
scheduled_time = self.cleaned_data['_schedule_at'] scheduled_time = self.cleaned_data['_schedule_at']
if scheduled_time and scheduled_time < timezone.now(): if scheduled_time and scheduled_time < local_now():
raise forms.ValidationError(_('Scheduled time must be in the future.')) raise forms.ValidationError(_('Scheduled time must be in the future.'))
return scheduled_time # When interval is used without schedule at, raise an exception
if self.cleaned_data['_interval'] and not scheduled_time:
raise forms.ValidationError(_('Scheduled time must be set when recurs is used.'))
return self.cleaned_data
@property @property
def requires_input(self): def requires_input(self):

View File

@ -37,7 +37,7 @@ class Command(BaseCommand):
f"clearing sessions; skipping." f"clearing sessions; skipping."
) )
# Delete expired ObjectRecords # Delete expired ObjectChanges
if options['verbosity']: if options['verbosity']:
self.stdout.write("[*] Checking for expired changelog records") self.stdout.write("[*] Checking for expired changelog records")
if config.CHANGELOG_RETENTION: if config.CHANGELOG_RETENTION:

View File

@ -1,4 +1,5 @@
import collections import collections
from importlib import import_module
from django.apps import AppConfig from django.apps import AppConfig
from django.conf import settings from django.conf import settings
@ -21,6 +22,15 @@ registry['plugins'] = {
'template_extensions': collections.defaultdict(list), 'template_extensions': collections.defaultdict(list),
} }
DEFAULT_RESOURCE_PATHS = {
'search_indexes': 'search.indexes',
'graphql_schema': 'graphql.schema',
'menu': 'navigation.menu',
'menu_items': 'navigation.menu_items',
'template_extensions': 'template_content.template_extensions',
'user_preferences': 'preferences.preferences',
}
# #
# Plugin AppConfig class # Plugin AppConfig class
@ -58,58 +68,53 @@ class PluginConfig(AppConfig):
# Django apps to append to INSTALLED_APPS when plugin requires them. # Django apps to append to INSTALLED_APPS when plugin requires them.
django_apps = [] django_apps = []
# Default integration paths. Plugin authors can override these to customize the paths to # Optional plugin resources
# integrated components. search_indexes = None
search_indexes = 'search.indexes' graphql_schema = None
graphql_schema = 'graphql.schema' menu = None
menu = 'navigation.menu' menu_items = None
menu_items = 'navigation.menu_items' template_extensions = None
template_extensions = 'template_content.template_extensions' user_preferences = None
user_preferences = 'preferences.preferences'
def _load_resource(self, name):
# Import from the configured path, if defined.
if getattr(self, name):
return import_string(f"{self.__module__}.{self.name}")
# Fall back to the resource's default path. Return None if the module has not been provided.
default_path = f'{self.__module__}.{DEFAULT_RESOURCE_PATHS[name]}'
default_module, resource_name = default_path.rsplit('.', 1)
try:
module = import_module(default_module)
return getattr(module, resource_name, None)
except ModuleNotFoundError:
pass
def ready(self): def ready(self):
plugin_name = self.name.rsplit('.', 1)[-1] plugin_name = self.name.rsplit('.', 1)[-1]
# Register search extensions (if defined) # Register search extensions (if defined)
try: search_indexes = self._load_resource('search_indexes') or []
search_indexes = import_string(f"{self.__module__}.{self.search_indexes}") for idx in search_indexes:
for idx in search_indexes: register_search(idx)
register_search(idx)
except ImportError:
pass
# Register template content (if defined) # Register template content (if defined)
try: if template_extensions := self._load_resource('template_extensions'):
template_extensions = import_string(f"{self.__module__}.{self.template_extensions}")
register_template_extensions(template_extensions) register_template_extensions(template_extensions)
except ImportError:
pass
# Register navigation menu and/or menu items (if defined) # Register navigation menu and/or menu items (if defined)
try: if menu := self._load_resource('menu'):
menu = import_string(f"{self.__module__}.{self.menu}")
register_menu(menu) register_menu(menu)
except ImportError: if menu_items := self._load_resource('menu_items'):
pass
try:
menu_items = import_string(f"{self.__module__}.{self.menu_items}")
register_menu_items(self.verbose_name, menu_items) register_menu_items(self.verbose_name, menu_items)
except ImportError:
pass
# Register GraphQL schema (if defined) # Register GraphQL schema (if defined)
try: if graphql_schema := self._load_resource('graphql_schema'):
graphql_schema = import_string(f"{self.__module__}.{self.graphql_schema}")
register_graphql_schema(graphql_schema) register_graphql_schema(graphql_schema)
except ImportError:
pass
# Register user preferences (if defined) # Register user preferences (if defined)
try: if user_preferences := self._load_resource('user_preferences'):
user_preferences = import_string(f"{self.__module__}.{self.user_preferences}")
register_user_preferences(plugin_name, user_preferences) register_user_preferences(plugin_name, user_preferences)
except ImportError:
pass
@classmethod @classmethod
def validate(cls, user_config, netbox_version): def validate(cls, user_config, netbox_version):

View File

@ -1,9 +1,11 @@
from importlib import import_module
from django.apps import apps from django.apps import apps
from django.conf import settings 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 django.utils.module_loading import import_string, module_has_submodule
from . import views from . import views
@ -19,24 +21,21 @@ plugin_admin_patterns = [
# Register base/API URL patterns for each plugin # Register base/API URL patterns for each plugin
for plugin_path in settings.PLUGINS: for plugin_path in settings.PLUGINS:
plugin = import_module(plugin_path)
plugin_name = plugin_path.split('.')[-1] plugin_name = plugin_path.split('.')[-1]
app = apps.get_app_config(plugin_name) app = apps.get_app_config(plugin_name)
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: if module_has_submodule(plugin, 'urls'):
urlpatterns = import_string(f"{plugin_path}.urls.urlpatterns") urlpatterns = import_string(f"{plugin_path}.urls.urlpatterns")
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: if module_has_submodule(plugin, 'api.urls'):
urlpatterns = import_string(f"{plugin_path}.api.urls.urlpatterns") urlpatterns = import_string(f"{plugin_path}.api.urls.urlpatterns")
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

View File

@ -21,7 +21,7 @@ from extras.models import JobResult
from extras.signals import clear_webhooks from extras.signals import clear_webhooks
from ipam.formfields import IPAddressFormField, IPNetworkFormField from ipam.formfields import IPAddressFormField, IPNetworkFormField
from ipam.validators import MaxPrefixLengthValidator, MinPrefixLengthValidator, prefix_validator from ipam.validators import MaxPrefixLengthValidator, MinPrefixLengthValidator, prefix_validator
from utilities.exceptions import AbortTransaction from utilities.exceptions import AbortScript, AbortTransaction
from utilities.forms import add_blank_choice, DynamicModelChoiceField, DynamicModelMultipleChoiceField from utilities.forms import add_blank_choice, DynamicModelChoiceField, DynamicModelMultipleChoiceField
from .context_managers import change_logging from .context_managers import change_logging
from .forms import ScriptForm from .forms import ScriptForm
@ -470,6 +470,14 @@ def run_script(data, request, commit=True, *args, **kwargs):
except AbortTransaction: except AbortTransaction:
script.log_info("Database changes have been reverted automatically.") script.log_info("Database changes have been reverted automatically.")
clear_webhooks.send(request) clear_webhooks.send(request)
except AbortScript as e:
script.log_failure(
f"Script aborted with error: {e}"
)
script.log_info("Database changes have been reverted due to error.")
logger.error(f"Script aborted with error: {e}")
job_result.set_status(JobResultStatusChoices.STATUS_ERRORED)
clear_webhooks.send(request)
except Exception as e: except Exception as e:
stacktrace = traceback.format_exc() stacktrace = traceback.format_exc()
script.log_failure( script.log_failure(

View File

@ -441,9 +441,9 @@ class IPRangeFilterSet(TenancyFilterSet, NetBoxModelFilterSet):
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
return queryset return queryset
qs_filter = Q(description__icontains=value) qs_filter = Q(description__icontains=value) | Q(start_address__contains=value) | Q(end_address__contains=value)
try: try:
ipaddress = str(netaddr.IPNetwork(value.strip()).cidr) ipaddress = str(netaddr.IPNetwork(value.strip()))
qs_filter |= Q(start_address=ipaddress) qs_filter |= Q(start_address=ipaddress)
qs_filter |= Q(end_address=ipaddress) qs_filter |= Q(end_address=ipaddress)
except (AddrFormatError, ValueError): except (AddrFormatError, ValueError):

View File

@ -257,6 +257,10 @@ class CustomValidationMixin(models.Model):
def clean(self): def clean(self):
super().clean() super().clean()
# If the instance is a base for replications, skip custom validation
if getattr(self, '_replicated_base', False):
return
# Send the post_clean signal # Send the post_clean signal
post_clean.send(sender=self.__class__, instance=self) post_clean.send(sender=self.__class__, instance=self)

View File

@ -24,7 +24,7 @@ from netbox.constants import RQ_QUEUE_DEFAULT, RQ_QUEUE_HIGH, RQ_QUEUE_LOW
# Environment setup # Environment setup
# #
VERSION = '3.4.4-dev' VERSION = '3.4.5-dev'
# Hostname # Hostname
HOSTNAME = platform.node() HOSTNAME = platform.node()

View File

@ -384,8 +384,8 @@ class BulkImportView(GetReturnURLMixin, BaseMultiObjectView):
'data': record, 'data': record,
'instance': instance, 'instance': instance,
} }
if form.cleaned_data['format'] == ImportFormatChoices.CSV: if hasattr(form, '_csv_headers'):
model_form_kwargs['headers'] = form._csv_headers model_form_kwargs['headers'] = form._csv_headers # Add CSV headers
model_form = self.model_form(**model_form_kwargs) model_form = self.model_form(**model_form_kwargs)
# When updating, omit all form fields other than those specified in the record. (No # When updating, omit all form fields other than those specified in the record. (No

View File

@ -436,6 +436,10 @@ class ComponentCreateView(GetReturnURLMixin, BaseObjectView):
form = self.initialize_form(request) form = self.initialize_form(request)
instance = self.alter_object(self.queryset.model(), request) instance = self.alter_object(self.queryset.model(), request)
# Note that the form instance is a replicated field base
# This is needed to avoid running custom validators multiple times
form.instance._replicated_base = hasattr(self.form, "replication_fields")
if form.is_valid(): if form.is_valid():
new_components = [] new_components = []
data = deepcopy(request.POST) data = deepcopy(request.POST)
@ -453,6 +457,9 @@ class ComponentCreateView(GetReturnURLMixin, BaseObjectView):
if component_form.is_valid(): if component_form.is_valid():
new_components.append(component_form) new_components.append(component_form)
else:
form.errors.update(component_form.errors)
break
if not form.errors and not component_form.errors: if not form.errors and not component_form.errors:
try: try:

View File

@ -5,6 +5,8 @@
{% block content %} {% block content %}
<form action="" method="post" enctype="multipart/form-data" class="form-object-edit"> <form action="" method="post" enctype="multipart/form-data" class="form-object-edit">
{% render_errors membership_form %}
{% csrf_token %} {% csrf_token %}
<div class="card"> <div class="card">
<h5 class="card-header">Add New Member</h5> <h5 class="card-header">Add New Member</h5>

View File

@ -8,6 +8,10 @@
<div class="tab-content"> <div class="tab-content">
<div class="tab-pane show active" id="edit-form" role="tabpanel" aria-labelledby="object-list-tab"> <div class="tab-pane show active" id="edit-form" role="tabpanel" aria-labelledby="object-list-tab">
<form action="" method="post" enctype="multipart/form-data" class="form-object-edit"> <form action="" method="post" enctype="multipart/form-data" class="form-object-edit">
{% for form in formset %}
{% render_errors form %}
{% endfor %}
{% csrf_token %} {% csrf_token %}
{{ pk_form.pk }} {{ pk_form.pk }}
{{ formset.management_form }} {{ formset.management_form }}

View File

@ -133,7 +133,7 @@
{% with first_available_ip=object.get_first_available_ip %} {% with first_available_ip=object.get_first_available_ip %}
{% if first_available_ip %} {% if first_available_ip %}
{% if perms.ipam.add_ipaddress %} {% if perms.ipam.add_ipaddress %}
<a href="{% url 'ipam:ipaddress_add' %}?address={{ first_available_ip }}">{{ first_available_ip }}</a> <a href="{% url 'ipam:ipaddress_add' %}?address={{ first_available_ip }}{% if object.vrf %}&vrf={{ object.vrf_id }}{% endif %}">{{ first_available_ip }}</a>
{% else %} {% else %}
{{ first_available_ip }} {{ first_available_ip }}
{% endif %} {% endif %}

View File

@ -96,7 +96,7 @@ class LoginView(View):
# Authenticate user # Authenticate user
auth_login(request, form.get_user()) auth_login(request, form.get_user())
logger.info(f"User {request.user} successfully authenticated") logger.info(f"User {request.user} successfully authenticated")
messages.info(request, f"Logged in as {request.user}.") messages.success(request, f"Logged in as {request.user}.")
# Ensure the user has a UserConfig defined. (This should normally be handled by # Ensure the user has a UserConfig defined. (This should normally be handled by
# create_userconfig() on user creation.) # create_userconfig() on user creation.)

View File

@ -24,6 +24,13 @@ class AbortRequest(Exception):
self.message = message self.message = message
class AbortScript(Exception):
"""
Raised to cleanly abort a script.
"""
pass
class PermissionsViolation(Exception): class PermissionsViolation(Exception):
""" """
Raised when an operation was prevented because it would violate the Raised when an operation was prevented because it would violate the

View File

@ -197,6 +197,8 @@ class ImportForm(BootstrapMixin, forms.Form):
self.cleaned_data['data'] = self._clean_json(data) self.cleaned_data['data'] = self._clean_json(data)
elif format == ImportFormatChoices.YAML: elif format == ImportFormatChoices.YAML:
self.cleaned_data['data'] = self._clean_yaml(data) self.cleaned_data['data'] = self._clean_yaml(data)
else:
raise forms.ValidationError(f"Unknown data format: {format}")
def _detect_format(self, data): def _detect_format(self, data):
""" """

View File

@ -1,5 +1,5 @@
bleach==5.0.1 bleach==5.0.1
Django==4.1.5 Django==4.1.6
django-cors-headers==3.13.0 django-cors-headers==3.13.0
django-debug-toolbar==3.8.1 django-debug-toolbar==3.8.1
django-filter==22.1 django-filter==22.1
@ -19,13 +19,13 @@ graphene-django==3.0.0
gunicorn==20.1.0 gunicorn==20.1.0
Jinja2==3.1.2 Jinja2==3.1.2
Markdown==3.3.7 Markdown==3.3.7
mkdocs-material==9.0.6 mkdocs-material==9.0.10
mkdocstrings[python-legacy]==0.20.0 mkdocstrings[python-legacy]==0.20.0
netaddr==0.8.0 netaddr==0.8.0
Pillow==9.4.0 Pillow==9.4.0
psycopg2-binary==2.9.5 psycopg2-binary==2.9.5
PyYAML==6.0 PyYAML==6.0
sentry-sdk==1.13.0 sentry-sdk==1.14.0
social-auth-app-django==5.0.0 social-auth-app-django==5.0.0
social-auth-core[openidconnect]==4.3.0 social-auth-core[openidconnect]==4.3.0
svgwrite==1.4.3 svgwrite==1.4.3