mirror of
https://github.com/netbox-community/netbox.git
synced 2025-08-26 09:16:10 -06:00
Merge branch 'netbox-community:develop' into 11592-expose-setting
This commit is contained in:
commit
a65440d8f7
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@ -14,7 +14,7 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: NetBox version
|
label: NetBox version
|
||||||
description: What version of NetBox are you currently running?
|
description: What version of NetBox are you currently running?
|
||||||
placeholder: v3.4.3
|
placeholder: v3.4.4
|
||||||
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.4.3
|
placeholder: v3.4.4
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
|
119
README.md
119
README.md
@ -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>
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
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.
|
||||||
|
|
||||||

|
* **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.
|
||||||
[](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.
|
||||||
[](https://github.com/netbox-community/netbox/issues)
|
* **Power tracking:** Map the distribution of power from upstream sources to individual feeds and outlets.
|
||||||
[](https://github.com/netbox-community/netbox/pulls)
|
* **Organization:** Manage tenant and contact assignments natively.
|
||||||
[](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!
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
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>
|
|
||||||
|
|
||||||
[](https://netboxlabs.com)
|
[](https://netboxlabs.com)
|
||||||
|
|
||||||
@ -76,34 +64,10 @@ complete list of requirements, see `requirements.txt`. The code is available
|
|||||||
[](https://sentry.io)
|
[](https://sentry.io)
|
||||||
|
|
||||||
[](https://metal.equinix.com)
|
[](https://metal.equinix.com)
|
||||||
|
|
||||||
[](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
|
|
||||||
|
|
||||||
")
|
")
|
||||||
|
|
||||||
@ -112,8 +76,3 @@ our [contributing guide](CONTRIBUTING.md) prior to beginning any work.
|
|||||||

|

|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
### Related projects
|
|
||||||
|
|
||||||
Please see [our wiki](https://github.com/netbox-community/netbox/wiki/Community-Contributions)
|
|
||||||
for a list of relevant community projects.
|
|
||||||
|
@ -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
|
||||||
|
@ -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.
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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.
|
||||||
|
|
||||||
|
@ -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/)
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -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'
|
||||||
)
|
)
|
||||||
|
@ -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):
|
||||||
|
@ -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:
|
||||||
|
@ -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):
|
||||||
|
@ -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
|
|
||||||
|
@ -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(
|
||||||
|
@ -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):
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
@ -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
|
||||||
|
@ -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:
|
||||||
|
@ -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>
|
||||||
|
@ -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 }}
|
||||||
|
@ -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 %}
|
||||||
|
@ -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.)
|
||||||
|
@ -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
|
||||||
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user