mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-23 04:22:01 -06:00
commit
44b9e822d9
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@ -14,7 +14,7 @@ body:
|
||||
attributes:
|
||||
label: NetBox version
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v3.3.0
|
||||
placeholder: v3.3.1
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
@ -14,7 +14,7 @@ body:
|
||||
attributes:
|
||||
label: NetBox version
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v3.3.0
|
||||
placeholder: v3.3.1
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
@ -4,12 +4,12 @@
|
||||
|
||||
Getting started with NetBox development is pretty straightforward, and should feel very familiar to anyone with Django development experience. There are a few things you'll need:
|
||||
|
||||
* A Linux system or environment
|
||||
* A Linux system or compatible environment
|
||||
* A PostgreSQL server, which can be installed locally [per the documentation](../installation/1-postgresql.md)
|
||||
* A Redis server, which can also be [installed locally](../installation/2-redis.md)
|
||||
* A supported version of Python
|
||||
* Python 3.8 or later
|
||||
|
||||
### Fork the Repo
|
||||
### 1. Fork the Repo
|
||||
|
||||
Assuming you'll be working on your own fork, your first step will be to fork the [official git repository](https://github.com/netbox-community/netbox). (If you're a maintainer who's going to be working directly with the official repo, skip this step.) Click the "fork" button at top right (be sure that you've logged into GitHub first).
|
||||
|
||||
@ -21,7 +21,7 @@ Copy the URL provided in the dialog box.
|
||||
|
||||
You can then clone your GitHub fork locally for development:
|
||||
|
||||
```no-highlight
|
||||
```no-highlight hl_lines="1 9"
|
||||
$ git clone https://github.com/$username/netbox.git
|
||||
Cloning into 'netbox'...
|
||||
remote: Enumerating objects: 85949, done.
|
||||
@ -35,93 +35,114 @@ base_requirements.txt contrib docs mkdocs.yml NOTICE requ
|
||||
CHANGELOG.md CONTRIBUTING.md LICENSE.txt netbox README.md scripts
|
||||
```
|
||||
|
||||
### 2. Create a New Branch
|
||||
|
||||
The NetBox project utilizes three persistent git branches to track work:
|
||||
|
||||
* `master` - Serves as a snapshot of the current stable release
|
||||
* `develop` - All development on the upcoming stable release occurs here
|
||||
* `feature` - Tracks work on an upcoming major release
|
||||
* `develop` - All development on the upcoming stable (patch) release occurs here
|
||||
* `feature` - Tracks work on an upcoming minor release
|
||||
|
||||
Typically, you'll base pull requests off of the `develop` branch, or off of `feature` if you're working on a new major release. **Never** merge pull requests into the `master` branch: This branch only ever merges pull requests from the `develop` branch, to effect a new release.
|
||||
Typically, you'll base pull requests off of the `develop` branch, or off of `feature` if you're working on a new major release. For example, assume that the current NetBox release is v3.3.5. Work applied to the `develop` branch will appear in v3.3.6, and work done under the `feature` branch will be included in the next minor release (v3.4.0).
|
||||
|
||||
For example, assume that the current NetBox release is v3.1.1. Work applied to the `develop` branch will appear in v3.1.2, and work done under the `feature` branch will be included in the next minor release (v3.2.0).
|
||||
!!! warning
|
||||
**Never** merge pull requests into the `master` branch: This branch only ever merges pull requests from the `develop` branch, to effect a new release.
|
||||
|
||||
### Enable Pre-Commit Hooks
|
||||
To create a new branch, first ensure that you've checked out the desired base branch, then run:
|
||||
|
||||
```no-highlight
|
||||
git checkout -B $branchname
|
||||
```
|
||||
|
||||
When naming a new git branch, contributors are strongly encouraged to use the relevant issue number followed by a very brief description of the work:
|
||||
|
||||
```no-highlight
|
||||
$issue-$description
|
||||
```
|
||||
|
||||
The description should be just two or three words to imply the focus of the work being performed. For example, bug #1234 to fix a TypeError exception when creating a device might be named `1234-device-typerror`. This ensures that branches are always follow some logical ordering (e.g. when running `git branch -a`) and helps other developers quickly identify the purpose of each.
|
||||
|
||||
### 3. Enable Pre-Commit Hooks
|
||||
|
||||
NetBox ships with a [git pre-commit hook](https://githooks.com/) script that automatically checks for style compliance and missing database migrations prior to committing changes. This helps avoid erroneous commits that result in CI test failures. You are encouraged to enable it by creating a link to `scripts/git-hooks/pre-commit`:
|
||||
|
||||
```no-highlight
|
||||
$ cd .git/hooks/
|
||||
$ ln -s ../../scripts/git-hooks/pre-commit
|
||||
cd .git/hooks/
|
||||
ln -s ../../scripts/git-hooks/pre-commit
|
||||
```
|
||||
For the pre-commit hooks to work, you will also need to install the pycodestyle package:
|
||||
|
||||
### Create a Python Virtual Environment
|
||||
```no-highlight
|
||||
python -m pip install pycodestyle
|
||||
```
|
||||
...and set up the yarn packages as shown in the [Web UI Development Guide](web-ui.md)
|
||||
|
||||
### 4. Create a Python Virtual Environment
|
||||
|
||||
A [virtual environment](https://docs.python.org/3/tutorial/venv.html) (or "venv" for short) is like a container for a set of Python packages. These allow you to build environments suited to specific projects without interfering with system packages or other projects. When installed per the documentation, NetBox uses a virtual environment in production.
|
||||
|
||||
Create a virtual environment using the `venv` Python module:
|
||||
|
||||
```no-highlight
|
||||
$ mkdir ~/.venv
|
||||
$ python3 -m venv ~/.venv/netbox
|
||||
mkdir ~/.venv
|
||||
python3 -m venv ~/.venv/netbox
|
||||
```
|
||||
|
||||
This will create a directory named `.venv/netbox/` in your home directory, which houses a virtual copy of the Python executable and its related libraries and tooling. When running NetBox for development, it will be run using the Python binary at `~/.venv/netbox/bin/python`.
|
||||
|
||||
!!! info "Where to Create Your Virtual Environments"
|
||||
Keeping virtual environments in `~/.venv/` is a common convention but entirely optional: Virtual environments can be created almost wherever you please. Also consider using [`virtualenvwrapper`](https://virtualenvwrapper.readthedocs.io/en/stable/) to simplify the management of multiple venvs.
|
||||
!!! tip "Virtual Environments"
|
||||
Keeping virtual environments in `~/.venv/` is a common convention but entirely optional: Virtual environments can be created almost wherever you please. Also consider using [`virtualenvwrapper`](https://virtualenvwrapper.readthedocs.io/en/stable/) to simplify the management of multiple environments.
|
||||
|
||||
Once created, activate the virtual environment:
|
||||
|
||||
```no-highlight
|
||||
$ source ~/.venv/netbox/bin/activate
|
||||
(netbox) $
|
||||
source ~/.venv/netbox/bin/activate
|
||||
```
|
||||
|
||||
Notice that the console prompt changes to indicate the active environment. This updates the necessary system environment variables to ensure that any Python scripts are run within the virtual environment.
|
||||
|
||||
### Install Dependencies
|
||||
### 5. Install Required Packages
|
||||
|
||||
With the virtual environment activated, install the project's required Python packages using the `pip` module:
|
||||
With the virtual environment activated, install the project's required Python packages using the `pip` module. Required packages are defined in `requirements.txt`. Each line in this file specifies the name and specific version of a required package.
|
||||
|
||||
```no-highlight
|
||||
(netbox) $ python -m pip install -r requirements.txt
|
||||
Collecting Django==3.1 (from -r requirements.txt (line 1))
|
||||
Cache entry deserialization failed, entry ignored
|
||||
Using cached https://files.pythonhosted.org/packages/2b/5a/4bd5624546912082a1bd2709d0edc0685f5c7827a278d806a20cf6adea28/Django-3.1-py3-none-any.whl
|
||||
...
|
||||
python -m pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### Configure NetBox
|
||||
### 6. Configure NetBox
|
||||
|
||||
Within the `netbox/netbox/` directory, copy `configuration_example.py` to `configuration.py` and update the following parameters:
|
||||
|
||||
* `ALLOWED_HOSTS`: This can be set to `['*']` for development purposes
|
||||
* `DATABASE`: PostgreSQL database connection parameters
|
||||
* `REDIS`: Redis configuration, if different from the defaults
|
||||
* `REDIS`: Redis configuration (if different from the defaults)
|
||||
* `SECRET_KEY`: Set to a random string (use `generate_secret_key.py` in the parent directory to generate a suitable key)
|
||||
* `DEBUG`: Set to `True`
|
||||
* `DEVELOPER`: Set to `True` (this enables the creation of new database migrations)
|
||||
|
||||
### Start the Development Server
|
||||
### 7. Start the Development Server
|
||||
|
||||
Django provides a lightweight, auto-updating HTTP/WSGI server for development use. It is started with the `runserver` management command:
|
||||
Django provides a lightweight, auto-updating [HTTP/WSGI server](https://docs.djangoproject.com/en/stable/ref/django-admin/#runserver) for development use. It is started with the `runserver` management command:
|
||||
|
||||
```no-highlight
|
||||
```no-highlight hl_lines="1"
|
||||
$ ./manage.py runserver
|
||||
Watching for file changes with StatReloader
|
||||
Performing system checks...
|
||||
|
||||
System check identified no issues (0 silenced).
|
||||
February 18, 2022 - 20:29:57
|
||||
Django version 4.0.2, using settings 'netbox.settings'
|
||||
August 18, 2022 - 15:17:52
|
||||
Django version 4.0.7, using settings 'netbox.settings'
|
||||
Starting development server at http://127.0.0.1:8000/
|
||||
Quit the server with CONTROL-C.
|
||||
```
|
||||
|
||||
This ensures that your development environment is now complete and operational. Any changes you make to the code base will be automatically adapted by the development server.
|
||||
This ensures that your development environment is now complete and operational. The development server will monitor the development environment and automatically reload in response to any changes made.
|
||||
|
||||
!!! info "IDE Integration"
|
||||
Some IDEs, such as PyCharm, will integrate with Django's development server and allow you to run it directly within the IDE. This is strongly encouraged as it makes for a much more convenient development environment.
|
||||
!!! tip "IDE Integration"
|
||||
Some IDEs, such as the highly-recommended [PyCharm](https://www.jetbrains.com/pycharm/), will integrate with Django's development server and allow you to run it directly within the IDE. This is strongly encouraged as it makes for a much more convenient development environment.
|
||||
|
||||
## UI Development
|
||||
|
||||
For UI development you will need to review the [Web UI Development Guide](web-ui.md)
|
||||
|
||||
## Populating Demo Data
|
||||
|
||||
@ -131,48 +152,51 @@ The demo data is provided in JSON format and loaded into an empty database using
|
||||
|
||||
## Running Tests
|
||||
|
||||
Prior to committing any substantial changes to the code base, be sure to run NetBox's test suite to catch any potential errors. Tests are run using the `test` management command, which employs Python's [`unittest`](https://docs.python.org/3/library/unittest.html#module-unittest) library. Remember to ensure the Python virtual environment is active before running this command. Also keep in mind that these commands are executed in the `netbox/` directory, not the root directory of the repository.
|
||||
Prior to committing any substantial changes to the code base, be sure to run NetBox's test suite to catch potential errors. Tests are run using the `test` management command, which employs Python's [`unittest`](https://docs.python.org/3/library/unittest.html#module-unittest) library. Remember to ensure that the Python virtual environment is active before running this command. Also keep in mind that these commands are executed in the `netbox/` directory, not the root directory of the repository.
|
||||
|
||||
To avoid potential issues with your local configuration file, set the `NETBOX_CONFIGURATION` to point to the packaged test configuration at `netbox/configuration_testing.py`. This will handle things like ensuring that the dummy plugin is enabled for comprehensive testing.
|
||||
|
||||
```no-highlight
|
||||
$ export NETBOX_CONFIGURATION=netbox.configuration_testing
|
||||
$ cd netbox/
|
||||
$ python manage.py test
|
||||
export NETBOX_CONFIGURATION=netbox.configuration_testing
|
||||
cd netbox/
|
||||
python manage.py test
|
||||
```
|
||||
|
||||
In cases where you haven't made any changes to the database schema (which is typical), you can append the `--keepdb` argument to this command to reuse the test database between runs. This cuts down on the time it takes to run the test suite since the database doesn't have to be rebuilt each time. (Note that this argument will cause errors if you've modified any model fields since the previous test run.)
|
||||
|
||||
```no-highlight
|
||||
$ python manage.py test --keepdb
|
||||
python manage.py test --keepdb
|
||||
```
|
||||
|
||||
You can also reduce testing time by enabling parallel test execution with the `--parallel` flag. (By default, this will run as many parallel tests as you have processors. To avoid sluggishness, it's a good idea to specify a lower number of parallel tests.) This flag can be combined with `--keepdb`, although if you encounter any strange errors, try running the test suite again with parallelization disabled.
|
||||
|
||||
```no-highlight
|
||||
$ python manage.py test --parallel <n>
|
||||
python manage.py test --parallel <n>
|
||||
```
|
||||
|
||||
Finally, it's possible to limit the run to a specific set of tests, specified by their Python path. For example, to run only IPAM and DCIM view tests:
|
||||
|
||||
```no-highlight
|
||||
$ python manage.py test dcim.tests.test_views ipam.tests.test_views
|
||||
python manage.py test dcim.tests.test_views ipam.tests.test_views
|
||||
```
|
||||
|
||||
This is handy for instances where just a few tests are failing and you want to re-run them individually.
|
||||
|
||||
!!! info
|
||||
NetBox uses [django-rich](https://github.com/adamchainz/django-rich) to enhance Django's default `test` management command.
|
||||
|
||||
## Submitting Pull Requests
|
||||
|
||||
Once you're happy with your work and have verified that all tests pass, commit your changes and push it upstream to your fork. Always provide descriptive (but not excessively verbose) commit messages. When working on a specific issue, be sure to prefix your commit message with the word "Fixes" or "Closes" and the issue number (with a hash mark). This tells GitHub to automatically close the referenced issue once the commit has been merged.
|
||||
Once you're happy with your work and have verified that all tests pass, commit your changes and push it upstream to your fork. Always provide descriptive (but not excessively verbose) commit messages. Be sure to prefix your commit message with the word "Fixes" or "Closes" and the relevant issue number (with a hash mark). This tells GitHub to automatically close the referenced issue once the commit has been merged.
|
||||
|
||||
```no-highlight
|
||||
$ git commit -m "Closes #1234: Add IPv5 support"
|
||||
$ git push origin
|
||||
git commit -m "Closes #1234: Add IPv5 support"
|
||||
git push origin
|
||||
```
|
||||
|
||||
Once your fork has the new commit, submit a [pull request](https://github.com/netbox-community/netbox/compare) to the NetBox repo to propose the changes. Be sure to provide a detailed accounting of the changes being made and the reasons for doing so.
|
||||
|
||||
Once submitted, a maintainer will review your pull request and either merge it or request changes. If changes are needed, you can make them via new commits to your fork: The pull request will update automatically.
|
||||
|
||||
!!! note "Remember to Open an Issue First"
|
||||
!!! warning
|
||||
Remember, pull requests are permitted only for **accepted** issues. If an issue you want to work on hasn't been approved by a maintainer yet, it's best to avoid risking your time and effort on a change that might not be accepted. (The one exception to this is trivial changes to the documentation or other non-critical resources.)
|
||||
|
@ -1,22 +1,18 @@
|
||||
# NetBox Development
|
||||
|
||||
NetBox is maintained as a [GitHub project](https://github.com/netbox-community/netbox) under the Apache 2 license. Users are encouraged to submit GitHub issues for feature requests and bug reports, however we are very selective about pull requests. Each pull request must be preceded by an **approved** issue. Please see the `CONTRIBUTING` guide for more direction on contributing to NetBox.
|
||||
Thanks for your interest in contributing to NetBox! This introduction covers a few important things to know before you get started.
|
||||
|
||||
## Communication
|
||||
## The Code
|
||||
|
||||
There are several official forums for communication among the developers and community members:
|
||||
NetBox and many of its related projects are maintained on [GitHub](https://github.com/netbox-community/netbox). GitHub also serves as one of our primary discussion forums. While all the code and discussion is publicly accessible, you'll need register for a [free GitHub account](https://github.com/signup) to engage in participation. Most people begin by [forking](https://docs.github.com/en/get-started/quickstart/fork-a-repo) the NetBox repository under their own GitHub account to begin working on the code.
|
||||
|
||||
* [GitHub issues](https://github.com/netbox-community/netbox/issues) - All feature requests, bug reports, and other substantial changes to the code base **must** be documented in a GitHub issue.
|
||||
* [GitHub discussions](https://github.com/netbox-community/netbox/discussions) - The preferred forum for general discussion and support issues. Ideal for shaping a feature request prior to submitting an issue.
|
||||
* [#netbox on NetDev Community Slack](https://netdev.chat/) - Good for quick chats. Avoid any discussion that might need to be referenced later on, as the chat history is not retained long.
|
||||

|
||||
|
||||
## Governance
|
||||
There are three permanent branches in the repository:
|
||||
|
||||
NetBox follows the [benevolent dictator](http://oss-watch.ac.uk/resources/benevolentdictatorgovernancemodel) model of governance, with [Jeremy Stretch](https://github.com/jeremystretch) ultimately responsible for all changes to the code base. While community contributions are welcomed and encouraged, the lead maintainer's primary role is to ensure the project's long-term maintainability and continued focus on its primary functions.
|
||||
|
||||
## Project Structure
|
||||
|
||||
All development of the current NetBox release occurs in the `develop` branch; releases are packaged from the `master` branch. The `master` branch should _always_ represent the current stable release in its entirety, such that installing NetBox by either downloading a packaged release or cloning the `master` branch provides the same code base. Only pull requests representing new releases should be merged into `master`.
|
||||
* `master` - The current stable release. Individual changes should never be pushed directly to this branch, but rather merged from `develop`.
|
||||
* `develop` - Active development for the upcoming patch release. Pull requests will typically be based on this branch unless they introduce breaking changes that must be deferred until the next minor release.
|
||||
* `feature` - New feature work to be introduced in the next minor release (e.g. from v3.3 to v3.4).
|
||||
|
||||
NetBox components are arranged into Django apps. Each app holds the models, views, and other resources relevant to a particular function:
|
||||
|
||||
@ -31,3 +27,34 @@ NetBox components are arranged into Django apps. Each app holds the models, view
|
||||
* `wireless`: Wireless links and LANs
|
||||
|
||||
All core functionality is stored within the `netbox/` subdirectory. HTML templates are stored in a common `templates/` directory, with model- and view-specific templates arranged by app. Documentation is kept in the `docs/` root directory.
|
||||
|
||||
## Proposing Changes
|
||||
|
||||
All substantial changes made to the code base are tracked using [GitHub issues](https://docs.github.com/en/issues). Feature requests, bug reports, and similar proposals must all be filed as issues and approved by a maintainer before work begins. This ensures that all changes to the code base are properly documented for future reference.
|
||||
|
||||
To submit a new feature request or bug report for NetBox, select and complete the appropriate [issue template](https://github.com/netbox-community/netbox/issues/new/choose). Once your issue has been approved, you're welcome to submit a [pull request](https://docs.github.com/en/pull-requests) containing your proposed changes.
|
||||
|
||||

|
||||
|
||||
Check out our [issue intake policy](https://github.com/netbox-community/netbox/wiki/Issue-Intake-Policy) for an overview of the issue triage and approval processes.
|
||||
|
||||
!!! tip
|
||||
Avoid starting work on a proposal before it has been accepted. Not all proposed changes will be accepted, and we'd hate for you to waste time working on code that might not make it into the project.
|
||||
|
||||
## Getting Help
|
||||
|
||||
There are two primary forums for getting assistance with NetBox development:
|
||||
|
||||
* [GitHub discussions](https://github.com/netbox-community/netbox/discussions) - The preferred forum for general discussion and support issues. Ideal for shaping a feature requests prior to submitting an issue.
|
||||
* [#netbox on NetDev Community Slack](https://netdev.chat/) - Good for quick chats. Avoid any discussion that might need to be referenced later on, as the chat history is not retained indefinitely.
|
||||
|
||||
!!! note
|
||||
Don't use GitHub issues to ask for help: These are reserved for proposed code changes only.
|
||||
|
||||
## Governance
|
||||
|
||||
NetBox follows the [benevolent dictator](http://oss-watch.ac.uk/resources/benevolentdictatorgovernancemodel) model of governance, with [Jeremy Stretch](https://github.com/jeremystretch) ultimately responsible for all changes to the code base. While community contributions are welcomed and encouraged, the lead maintainer's primary role is to ensure the project's long-term maintainability and continued focus on its primary functions.
|
||||
|
||||
## Licensing
|
||||
|
||||
The entire NetBox project is licensed as open source under the [Apache 2.0 license](https://github.com/netbox-community/netbox/blob/master/LICENSE.txt). This is a very permissive license which allows unlimited redistribution of all code within the project. Note that all submissions to the project are subject to the same license.
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
## Model Types
|
||||
|
||||
A NetBox model represents a discrete object type such as a device or IP address. Each model is defined as a Python class and has its own SQL table. All NetBox data models can be categorized by type.
|
||||
A NetBox model represents a discrete object type such as a device or IP address. Per [Django convention](https://docs.djangoproject.com/en/stable/topics/db/models/), each model is defined as a Python class and has its own SQL table. All NetBox data models can be categorized by type.
|
||||
|
||||
The Django [content types](https://docs.djangoproject.com/en/stable/ref/contrib/contenttypes/) framework can be used to reference models within the database. A ContentType instance references a model by its `app_label` and `name`: For example, the Site model is referred to as `dcim.site`. The content type combined with an object's primary key form a globally unique identifier for the object (e.g. `dcim.site:123`).
|
||||
|
||||
|
@ -1,62 +1,62 @@
|
||||
# Release Checklist
|
||||
|
||||
## Minor Version Bumps
|
||||
This documentation describes the process of packaging and publishing a new NetBox release. There are three types of release:
|
||||
|
||||
### Address Pinned Dependencies
|
||||
* Major release (e.g. v2.11 to v3.0)
|
||||
* Minor release (e.g. v3.2 to v3.3)
|
||||
* Patch release (e.g. v3.3.0 to v3.3.1)
|
||||
|
||||
Check `base_requirements.txt` for any dependencies pinned to a specific version, and upgrade them to their most stable release (where possible).
|
||||
While major releases generally introduce some very substantial change to the application, they are typically treated the same as minor version increments for the purpose of release packaging.
|
||||
|
||||
### Link to the Release Notes Page
|
||||
## Minor Version Releases
|
||||
|
||||
Add the release notes (`/docs/release-notes/X.Y.md`) to the table of contents within `mkdocs.yml`, and add a summary of the major changes to `index.md`.
|
||||
### Address Constrained Dependencies
|
||||
|
||||
### Manually Perform a New Install
|
||||
|
||||
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 on GitHub after ensuring there are no remaining open issues associated with it.
|
||||
|
||||
### Merge the Release Branch
|
||||
|
||||
Submit a pull request to merge the `feature` branch into the `develop` branch in preparation for its release.
|
||||
|
||||
---
|
||||
|
||||
## All Releases
|
||||
|
||||
### Update Requirements
|
||||
|
||||
Required Python packages are maintained in two files. `base_requirements.txt` contains a list of all the packages required by NetBox. Some of them may be pinned to a specific version of the package due to a known issue. For example:
|
||||
Sometimes it becomes necessary to constrain dependencies to a particular version, e.g. to work around a bug in a newer release or to avoid a breaking change that we have yet to accommodate. (Another common example is to limit the upstream Django release.) For example:
|
||||
|
||||
```
|
||||
# https://github.com/encode/django-rest-framework/issues/6053
|
||||
djangorestframework==3.8.1
|
||||
```
|
||||
|
||||
The other file is `requirements.txt`, which lists each of the required packages pinned to its current stable version. When NetBox is installed, the Python environment is configured to match this file. This helps ensure that a new release of a dependency doesn't break NetBox.
|
||||
These version constraints are added to `base_requirements.txt` to ensure that newer packages are not installed when updating the pinned dependencies in `requirements.txt` (see the [Update Requirements](#update-requirements) section below). Before each new minor version of NetBox is released, all such constraints on dependent packages should be addressed if feasible. This guards against the collection of stale constraints over time.
|
||||
|
||||
Every release should refresh `requirements.txt` so that it lists the most recent stable release of each package. To do this:
|
||||
### Close the Release Milestone
|
||||
|
||||
1. Create a new virtual environment.
|
||||
2. Install the latest version of all required packages `pip install -U -r base_requirements.txt`).
|
||||
3. Run all tests and check that the UI and API function as expected.
|
||||
4. Review each requirement's release notes for any breaking or otherwise noteworthy changes.
|
||||
5. Update the package versions in `requirements.txt` as appropriate.
|
||||
Close the [release milestone](https://github.com/netbox-community/netbox/milestones) on GitHub after ensuring there are no remaining open issues associated with it.
|
||||
|
||||
In cases where upgrading a dependency to its most recent release is breaking, it should be pinned to its current minor version in `base_requirements.txt` (with an explanatory comment) and revisited for the next major NetBox release.
|
||||
### Update the Release Notes
|
||||
|
||||
### Verify CI Build Status
|
||||
Check that a link to the release notes for the new version is present in the navigation menu (defined in `mkdocs.yml`), and that a summary of all major new features has been added to `docs/index.md`.
|
||||
|
||||
Ensure that continuous integration testing on the `develop` branch is completing successfully.
|
||||
### Manually Perform a New Install
|
||||
|
||||
Start the documentation server and navigate to the current version of the installation docs:
|
||||
|
||||
```no-highlight
|
||||
mkdocs serve
|
||||
```
|
||||
|
||||
Follow these instructions to perform a new installation of NetBox in a temporary environment. 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.
|
||||
|
||||
### Merge the Release Branch
|
||||
|
||||
Submit a pull request to merge the `feature` branch into the `develop` branch in preparation for its release. Once it has been merged, continue with the section for patch releases below.
|
||||
|
||||
---
|
||||
|
||||
## Patch Releases
|
||||
|
||||
### Update Requirements
|
||||
|
||||
Before each release, update each of NetBox's Python dependencies to its most recent stable version. These are defined in `requirements.txt`, which is updated from `base_requirements.txt` using `pip`. To do this:
|
||||
|
||||
1. Upgrade the installed version of all required packages in your environment (`pip install -U -r base_requirements.txt`).
|
||||
2. Run all tests and check that the UI and API function as expected.
|
||||
3. Review each requirement's release notes for any breaking or otherwise noteworthy changes.
|
||||
4. Update the package versions in `requirements.txt` as appropriate.
|
||||
|
||||
In cases where upgrading a dependency to its most recent release is breaking, it should be constrained to its current minor version in `base_requirements.txt` with an explanatory comment and revisited for the next major NetBox release (see the [Address Constrained Dependencies](#address-constrained-dependencies) section above).
|
||||
|
||||
### Update Version and Changelog
|
||||
|
||||
@ -64,28 +64,35 @@ Ensure that continuous integration testing on the `develop` branch is completing
|
||||
* Update the example version numbers in the feature request and bug report templates under `.github/ISSUE_TEMPLATES/`.
|
||||
* Replace the "FUTURE" placeholder in the release notes with the current date.
|
||||
|
||||
Commit these changes to the `develop` branch.
|
||||
Commit these changes to the `develop` branch and push upstream.
|
||||
|
||||
### Verify CI Build Status
|
||||
|
||||
Ensure that continuous integration testing on the `develop` branch is completing successfully. If it fails, take action to correct the failure before proceding with the release.
|
||||
|
||||
### Submit a Pull Request
|
||||
|
||||
Submit a pull request title **"Release vX.Y.Z"** to merge the `develop` branch into `master`. Copy the documented release notes into the pull request's body.
|
||||
Submit a pull request titled **"Release vX.Y.Z"** to merge the `develop` branch into `master`. Copy the documented release notes into the pull request's body.
|
||||
|
||||
Once CI has completed on the PR, merge it.
|
||||
Once CI has completed on the PR, merge it. This effects a new release in the `master` branch.
|
||||
|
||||
### Create a New Release
|
||||
|
||||
Draft a [new release](https://github.com/netbox-community/netbox/releases/new) with the following parameters.
|
||||
Create a [new release](https://github.com/netbox-community/netbox/releases/new) on GitHub with the following parameters.
|
||||
|
||||
* **Tag:** Current version (e.g. `v2.9.9`)
|
||||
* **Tag:** Current version (e.g. `v3.3.1`)
|
||||
* **Target:** `master`
|
||||
* **Title:** Version and date (e.g. `v2.9.9 - 2020-11-09`)
|
||||
* **Title:** Version and date (e.g. `v3.3.1 - 2022-08-25`)
|
||||
* **Description:** Copy from the pull request body
|
||||
|
||||
Copy the description from the pull request to the release.
|
||||
Once created, the release will become available for users to install.
|
||||
|
||||
### Update the Development Version
|
||||
|
||||
On the `develop` branch, update `VERSION` in `settings.py` to point to the next release. For example, if you just released v2.9.9, set:
|
||||
On the `develop` branch, update `VERSION` in `settings.py` to point to the next release. For example, if you just released v3.3.1, set:
|
||||
|
||||
```
|
||||
VERSION = 'v2.9.10-dev'
|
||||
VERSION = 'v3.3.2-dev'
|
||||
```
|
||||
|
||||
Commit this change with the comment "PRVB" (for _post-release version bump_) and push the commit upstream.
|
||||
|
@ -1,34 +1,53 @@
|
||||
# Style Guide
|
||||
|
||||
NetBox generally follows the [Django style guide](https://docs.djangoproject.com/en/stable/internals/contributing/writing-code/coding-style/), which is itself based on [PEP 8](https://www.python.org/dev/peps/pep-0008/). [Pycodestyle](https://github.com/pycqa/pycodestyle) is used to validate code formatting, ignoring certain violations. See `scripts/cibuild.sh` for details.
|
||||
NetBox generally follows the [Django style guide](https://docs.djangoproject.com/en/stable/internals/contributing/writing-code/coding-style/), which is itself based on [PEP 8](https://www.python.org/dev/peps/pep-0008/). [Pycodestyle](https://github.com/pycqa/pycodestyle) is used to validate code formatting, ignoring certain violations.
|
||||
|
||||
## PEP 8 Exceptions
|
||||
## Code
|
||||
|
||||
* Wildcard imports (for example, `from .constants import *`) are acceptable under any of the following conditions:
|
||||
* The library being import contains only constant declarations (e.g. `constants.py`)
|
||||
* The library being imported explicitly defines `__all__`
|
||||
### General Guidance
|
||||
|
||||
* Maximum line length is 120 characters (E501)
|
||||
* This does not apply to HTML templates or to automatically generated code (e.g. database migrations).
|
||||
* When in doubt, remain consistent: It is better to be consistently incorrect than inconsistently correct. If you notice in the course of unrelated work a pattern that should be corrected, continue to follow the pattern for now and submit a separate bug report so that the entire code base can be evaluated at a later point.
|
||||
|
||||
* Line breaks are permitted following binary operators (W504)
|
||||
* Prioritize readability over concision. Python is a very flexible language that typically offers several multiple options for expressing a given piece of logic, but some may be more friendly to the reader than others. (List comprehensions are particularly vulnerable to over-optimization.) Always remain considerate of the future reader who may need to interpret your code without the benefit of the context within which you are writing it.
|
||||
|
||||
## Enforcing Code Style
|
||||
* Include a newline at the end of every file.
|
||||
|
||||
The `pycodestyle` utility (previously `pep8`) is used by the CI process to enforce code style. It is strongly recommended to include as part of your commit process. A git commit hook is provided in the source at `scripts/git-hooks/pre-commit`. Linking to this script from `.git/hooks/` will invoke `pycodestyle` prior to every commit attempt and abort if the validation fails.
|
||||
* No easter eggs. While they can be fun, NetBox must be considered as a business-critical tool. The potential, however minor, for introducing a bug caused by unnecessary code is best avoided entirely.
|
||||
|
||||
```
|
||||
$ cd .git/hooks/
|
||||
$ ln -s ../../scripts/git-hooks/pre-commit
|
||||
```
|
||||
* Constants (variables which do not change) should be declared in `constants.py` within each app. Wildcard imports from the file are acceptable.
|
||||
|
||||
To invoke `pycodestyle` manually, run:
|
||||
* Every model must have a [docstring](https://peps.python.org/pep-0257/). Every custom method should include an explanation of its function.
|
||||
|
||||
* Nested API serializers generate minimal representations of an object. These are stored separately from the primary serializers to avoid circular dependencies. Always import nested serializers from other apps directly. For example, from within the DCIM app you would write `from ipam.api.nested_serializers import NestedIPAddressSerializer`.
|
||||
|
||||
### PEP 8 Exceptions
|
||||
|
||||
NetBox ignores certain PEP8 assertions. These are listed below.
|
||||
|
||||
#### Wildcard Imports
|
||||
|
||||
Wildcard imports (for example, `from .constants import *`) are acceptable under any of the following conditions:
|
||||
|
||||
* The library being import contains only constant declarations (e.g. `constants.py`)
|
||||
* The library being imported explicitly defines `__all__`
|
||||
|
||||
#### Maximum Line Length (E501)
|
||||
|
||||
NetBox does not restrict lines to a maximum length of 79 characters. We use a maximum line length of 120 characters, however this is not enforced by CI. The maximum length does not apply to HTML templates or to automatically generated code (e.g. database migrations).
|
||||
|
||||
#### Line Breaks Following Binary Operators (W504)
|
||||
|
||||
Line breaks are permitted following binary operators.
|
||||
|
||||
### Enforcing Code Style
|
||||
|
||||
The [`pycodestyle`](https://pypi.org/project/pycodestyle/) utility (formerly `pep8`) is used by the CI process to enforce code style. A [pre-commit hook](./getting-started.md#2-enable-pre-commit-hooks) which runs this automatically is included with NetBox. To invoke `pycodestyle` manually, run:
|
||||
|
||||
```
|
||||
pycodestyle --ignore=W504,E501 netbox/
|
||||
```
|
||||
|
||||
## Introducing New Dependencies
|
||||
### Introducing New Dependencies
|
||||
|
||||
The introduction of a new dependency is best avoided unless it is absolutely necessary. For small features, it's generally preferable to replicate functionality within the NetBox code base rather than to introduce reliance on an external project. This reduces both the burden of tracking new releases and our exposure to outside bugs and supply chain attacks.
|
||||
|
||||
@ -39,24 +58,22 @@ If there's a strong case for introducing a new dependency, it must meet the foll
|
||||
* It must be actively maintained, with no longer than one year between releases.
|
||||
* It must be available via the [Python Package Index](https://pypi.org/) (PyPI).
|
||||
|
||||
When adding a new dependency, a short description of the package and the URL of its code repository must be added to `base_requirements.txt`. Additionally, a line specifying the package name pinned to the current stable release must be added to `requirements.txt`. This ensures that NetBox will install only the known-good release and simplify support efforts.
|
||||
When adding a new dependency, a short description of the package and the URL of its code repository must be added to `base_requirements.txt`. Additionally, a line specifying the package name pinned to the current stable release must be added to `requirements.txt`. This ensures that NetBox will install only the known-good release.
|
||||
|
||||
## General Guidance
|
||||
## Written Works
|
||||
|
||||
* When in doubt, remain consistent: It is better to be consistently incorrect than inconsistently correct. If you notice in the course of unrelated work a pattern that should be corrected, continue to follow the pattern for now and submit a separate bug report so that the entire code base can be evaluated at a later point.
|
||||
### General Guidance
|
||||
|
||||
* Prioritize readability over concision. Python is a very flexible language that typically offers several options for expressing a given piece of logic, but some may be more friendly to the reader than others. (List comprehensions are particularly vulnerable to over-optimization.) Always remain considerate of the future reader who may need to interpret your code without the benefit of the context within which you are writing it.
|
||||
* Written material must always meet a reasonable professional standard, with proper grammar, spelling, and punctuation.
|
||||
|
||||
* No easter eggs. While they can be fun, NetBox must be considered as a business-critical tool. The potential, however minor, for introducing a bug caused by unnecessary logic is best avoided entirely.
|
||||
* Use two line breaks between paragraphs.
|
||||
|
||||
* Constants (variables which generally do not change) should be declared in `constants.py` within each app. Wildcard imports from the file are acceptable.
|
||||
* Use only a single space between sentences.
|
||||
|
||||
* Every model should have a docstring. Every custom method should include an explanation of its function.
|
||||
* All documentation is to be written in [Markdown](../reference/markdown.md), with modest amounts of HTML permitted where needed to overcome technical limitations.
|
||||
|
||||
* Nested API serializers generate minimal representations of an object. These are stored separately from the primary serializers to avoid circular dependencies. Always import nested serializers from other apps directly. For example, from within the DCIM app you would write `from ipam.api.nested_serializers import NestedIPAddressSerializer`.
|
||||
### Branding
|
||||
|
||||
## Branding
|
||||
|
||||
* When referring to NetBox in writing, use the proper form "NetBox," with the letters N and B capitalized. The lowercase form "netbox" should be used in code, filenames, etc. But never "Netbox" or any other deviation.
|
||||
* When referring to NetBox in writing, use the proper form "NetBox," with the letters N and B capitalized. The lowercase form "netbox" should be used in code, filenames, etc. but never "Netbox" or any other deviation.
|
||||
|
||||
* There is an SVG form of the NetBox logo at [docs/netbox_logo.svg](../netbox_logo.svg). It is preferred to use this logo for all purposes as it scales to arbitrary sizes without loss of resolution. If a raster image is required, the SVG logo should be converted to a PNG image of the prescribed size.
|
||||
|
@ -18,6 +18,21 @@ NetBox v3.0 and later require the following:
|
||||
|
||||
As with the initial installation, you can upgrade NetBox by either downloading the latest release package or by cloning the `master` branch of the git repository.
|
||||
|
||||
!!! warning
|
||||
Use the same method as you used to install Netbox originally
|
||||
|
||||
If you are not sure how Netbox was installed originally, check with this
|
||||
command:
|
||||
|
||||
```
|
||||
ls -ld /opt/netbox /opt/netbox/.git
|
||||
```
|
||||
|
||||
If Netbox was installed from a release package, then `/opt/netbox` will be a
|
||||
symlink pointing to the current version, and `/opt/netbox/.git` will not
|
||||
exist. If it was installed from git, then `/opt/netbox` and
|
||||
`/opt/netbox/.git` will both exist as normal directories.
|
||||
|
||||
### Option A: Download a Release
|
||||
|
||||
Download the [latest stable release](https://github.com/netbox-community/netbox/releases) from GitHub as a tarball or ZIP archive. Extract it to your desired path. In this example, we'll use `/opt/netbox`.
|
||||
|
BIN
docs/media/development/github.png
Normal file
BIN
docs/media/development/github.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 110 KiB |
BIN
docs/media/development/github_new_issue.png
Normal file
BIN
docs/media/development/github_new_issue.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 45 KiB |
@ -1,5 +1,42 @@
|
||||
# NetBox v3.3
|
||||
|
||||
## v3.3.1 (2022-08-25)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#6454](https://github.com/netbox-community/netbox/issues/6454) - Include contextual help when creating first objects in UI
|
||||
* [#9935](https://github.com/netbox-community/netbox/issues/9935) - Add 802.11ay and "other" wireless interface types
|
||||
* [#10031](https://github.com/netbox-community/netbox/issues/10031) - Enforce `application/json` content type for REST API requests
|
||||
* [#10033](https://github.com/netbox-community/netbox/issues/10033) - Disable "add termination" button for point-to-point L2VPNs with two terminations
|
||||
* [#10037](https://github.com/netbox-community/netbox/issues/10037) - Add "child interface" option to actions dropdown in interfaces list
|
||||
* [#10038](https://github.com/netbox-community/netbox/issues/10038) - Add "L2VPN termination" option to actions dropdown in interfaces list
|
||||
* [#10039](https://github.com/netbox-community/netbox/issues/10039) - Add "assign FHRP group" option to actions dropdown in interfaces list
|
||||
* [#10061](https://github.com/netbox-community/netbox/issues/10061) - Replicate type when cloning L2VPN instances
|
||||
* [#10066](https://github.com/netbox-community/netbox/issues/10066) - Use fixed column widths for custom field values in UI
|
||||
* [#10133](https://github.com/netbox-community/netbox/issues/10133) - Enable nullifying device location during bulk edit
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#9663](https://github.com/netbox-community/netbox/issues/9663) - Omit available IP annotations when filtering prefix child IPs list
|
||||
* [#10040](https://github.com/netbox-community/netbox/issues/10040) - Fix exception when ordering prefixes by flat representation
|
||||
* [#10053](https://github.com/netbox-community/netbox/issues/10053) - Custom fields header should not be displayed when editing circuit terminations with no custom fields
|
||||
* [#10055](https://github.com/netbox-community/netbox/issues/10055) - Fix extraneous NAT indicator by device primary IP
|
||||
* [#10057](https://github.com/netbox-community/netbox/issues/10057) - Fix AttributeError exception when global search results include rack reservations
|
||||
* [#10059](https://github.com/netbox-community/netbox/issues/10059) - Add identifier column to L2VPN table
|
||||
* [#10070](https://github.com/netbox-community/netbox/issues/10070) - Add unique constraint for L2VPN slug
|
||||
* [#10087](https://github.com/netbox-community/netbox/issues/10087) - Correct display of far end in console/power/interface connections tables
|
||||
* [#10089](https://github.com/netbox-community/netbox/issues/10089) - `linkify` template filter should escape object representation
|
||||
* [#10094](https://github.com/netbox-community/netbox/issues/10094) - Fix 404 when using "create and add another" to add contact assignments
|
||||
* [#10108](https://github.com/netbox-community/netbox/issues/10108) - Linkify inside NAT IPs for primary device IPs in UI
|
||||
* [#10109](https://github.com/netbox-community/netbox/issues/10109) - Fix available prefixes calculation for container prefixes in the global table
|
||||
* [#10111](https://github.com/netbox-community/netbox/issues/10111) - Fix ValueError exception when searching for L2VPN objects
|
||||
* [#10118](https://github.com/netbox-community/netbox/issues/10118) - Fix display of connected LLDP neighbors for devices
|
||||
* [#10134](https://github.com/netbox-community/netbox/issues/10134) - Custom fields data serializer should return a 400 response for invalid data
|
||||
* [#10135](https://github.com/netbox-community/netbox/issues/10135) - Fix SSO support for SAML2 IDPs
|
||||
* [#10147](https://github.com/netbox-community/netbox/issues/10147) - Permit the creation of 0U device types via REST API
|
||||
|
||||
---
|
||||
|
||||
## v3.3.0 (2022-08-17)
|
||||
|
||||
### Breaking Changes
|
||||
|
@ -1,3 +1,4 @@
|
||||
from django.apps import apps
|
||||
from django.contrib.contenttypes.fields import GenericRelation
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
@ -136,6 +137,10 @@ class Circuit(NetBoxModel):
|
||||
def __str__(self):
|
||||
return self.cid
|
||||
|
||||
@classmethod
|
||||
def get_prerequisite_models(cls):
|
||||
return [apps.get_model('circuits.Provider'), CircuitType]
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('circuits:circuit', args=[self.pk])
|
||||
|
||||
|
@ -310,7 +310,7 @@ class DeviceTypeSerializer(NetBoxModelSerializer):
|
||||
max_digits=4,
|
||||
decimal_places=1,
|
||||
label='Position (U)',
|
||||
min_value=decimal.Decimal(0.5),
|
||||
min_value=0,
|
||||
default=1.0
|
||||
)
|
||||
subdevice_role = ChoiceField(choices=SubdeviceRoleChoices, allow_blank=True, required=False)
|
||||
|
@ -790,7 +790,9 @@ class InterfaceTypeChoices(ChoiceSet):
|
||||
TYPE_80211AC = 'ieee802.11ac'
|
||||
TYPE_80211AD = 'ieee802.11ad'
|
||||
TYPE_80211AX = 'ieee802.11ax'
|
||||
TYPE_80211AY = 'ieee802.11ay'
|
||||
TYPE_802151 = 'ieee802.15.1'
|
||||
TYPE_OTHER_WIRELESS = 'other-wireless'
|
||||
|
||||
# Cellular
|
||||
TYPE_GSM = 'gsm'
|
||||
@ -918,7 +920,9 @@ class InterfaceTypeChoices(ChoiceSet):
|
||||
(TYPE_80211AC, 'IEEE 802.11ac'),
|
||||
(TYPE_80211AD, 'IEEE 802.11ad'),
|
||||
(TYPE_80211AX, 'IEEE 802.11ax'),
|
||||
(TYPE_80211AY, 'IEEE 802.11ay'),
|
||||
(TYPE_802151, 'IEEE 802.15.1 (Bluetooth)'),
|
||||
(TYPE_OTHER_WIRELESS, 'Other (Wireless)'),
|
||||
)
|
||||
),
|
||||
(
|
||||
|
@ -45,6 +45,9 @@ WIRELESS_IFACE_TYPES = [
|
||||
InterfaceTypeChoices.TYPE_80211AC,
|
||||
InterfaceTypeChoices.TYPE_80211AD,
|
||||
InterfaceTypeChoices.TYPE_80211AX,
|
||||
InterfaceTypeChoices.TYPE_80211AY,
|
||||
InterfaceTypeChoices.TYPE_802151,
|
||||
InterfaceTypeChoices.TYPE_OTHER_WIRELESS,
|
||||
]
|
||||
|
||||
NONCONNECTABLE_IFACE_TYPES = VIRTUAL_IFACE_TYPES + WIRELESS_IFACE_TYPES
|
||||
|
@ -480,7 +480,7 @@ class DeviceBulkEditForm(NetBoxModelBulkEditForm):
|
||||
('Hardware', ('manufacturer', 'device_type', 'airflow', 'serial')),
|
||||
)
|
||||
nullable_fields = (
|
||||
'tenant', 'platform', 'serial', 'airflow',
|
||||
'location', 'tenant', 'platform', 'serial', 'airflow',
|
||||
)
|
||||
|
||||
|
||||
|
@ -1,6 +1,8 @@
|
||||
import decimal
|
||||
|
||||
import yaml
|
||||
|
||||
from django.apps import apps
|
||||
from django.contrib.contenttypes.fields import GenericRelation
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
@ -159,6 +161,10 @@ class DeviceType(NetBoxModel):
|
||||
self._original_front_image = self.front_image
|
||||
self._original_rear_image = self.rear_image
|
||||
|
||||
@classmethod
|
||||
def get_prerequisite_models(cls):
|
||||
return [Manufacturer, ]
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:devicetype', args=[self.pk])
|
||||
|
||||
@ -338,6 +344,10 @@ class ModuleType(NetBoxModel):
|
||||
def __str__(self):
|
||||
return self.model
|
||||
|
||||
@classmethod
|
||||
def get_prerequisite_models(cls):
|
||||
return [Manufacturer, ]
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:moduletype', args=[self.pk])
|
||||
|
||||
@ -658,6 +668,10 @@ class Device(NetBoxModel, ConfigContextModel):
|
||||
return f'{self.device_type.manufacturer} {self.device_type.model} ({self.pk})'
|
||||
return super().__str__()
|
||||
|
||||
@classmethod
|
||||
def get_prerequisite_models(cls):
|
||||
return [apps.get_model('dcim.Site'), DeviceRole, DeviceType, ]
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:device', args=[self.pk])
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
from django.apps import apps
|
||||
from django.contrib.contenttypes.fields import GenericRelation
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
@ -54,6 +55,10 @@ class PowerPanel(NetBoxModel):
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@classmethod
|
||||
def get_prerequisite_models(cls):
|
||||
return [apps.get_model('dcim.Site'), ]
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:powerpanel', args=[self.pk])
|
||||
|
||||
@ -138,6 +143,10 @@ class PowerFeed(NetBoxModel, PathEndpoint, CabledObjectModel):
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@classmethod
|
||||
def get_prerequisite_models(cls):
|
||||
return [PowerPanel, ]
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:powerfeed', args=[self.pk])
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
import decimal
|
||||
|
||||
from django.apps import apps
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.contenttypes.fields import GenericRelation
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
@ -201,6 +202,10 @@ class Rack(NetBoxModel):
|
||||
return f'{self.name} ({self.facility_id})'
|
||||
return self.name
|
||||
|
||||
@classmethod
|
||||
def get_prerequisite_models(cls):
|
||||
return [apps.get_model('dcim.Site'), ]
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:rack', args=[self.pk])
|
||||
|
||||
@ -477,6 +482,10 @@ class RackReservation(NetBoxModel):
|
||||
def __str__(self):
|
||||
return "Reservation for rack {}".format(self.rack)
|
||||
|
||||
@classmethod
|
||||
def get_prerequisite_models(cls):
|
||||
return [apps.get_model('dcim.Site'), Rack, ]
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:rackreservation', args=[self.pk])
|
||||
|
||||
|
@ -411,6 +411,10 @@ class Location(NestedGroupModel):
|
||||
|
||||
super().validate_unique(exclude=exclude)
|
||||
|
||||
@classmethod
|
||||
def get_prerequisite_models(cls):
|
||||
return [Site, ]
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:location', args=[self.pk])
|
||||
|
||||
|
@ -1,109 +1,8 @@
|
||||
import django_tables2 as tables
|
||||
from django_tables2.utils import Accessor
|
||||
|
||||
from netbox.tables import BaseTable, columns
|
||||
from dcim.models import ConsolePort, Interface, PowerPort
|
||||
from .cables import *
|
||||
from .connections import *
|
||||
from .devices import *
|
||||
from .devicetypes import *
|
||||
from .modules import *
|
||||
from .power import *
|
||||
from .racks import *
|
||||
from .sites import *
|
||||
|
||||
|
||||
#
|
||||
# Device connections
|
||||
#
|
||||
|
||||
class ConsoleConnectionTable(BaseTable):
|
||||
console_server = tables.Column(
|
||||
accessor=Accessor('_path__destination__device'),
|
||||
orderable=False,
|
||||
linkify=True,
|
||||
verbose_name='Console Server'
|
||||
)
|
||||
console_server_port = tables.Column(
|
||||
accessor=Accessor('_path__destination'),
|
||||
orderable=False,
|
||||
linkify=True,
|
||||
verbose_name='Port'
|
||||
)
|
||||
device = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
name = tables.Column(
|
||||
linkify=True,
|
||||
verbose_name='Console Port'
|
||||
)
|
||||
reachable = columns.BooleanColumn(
|
||||
accessor=Accessor('_path__is_active'),
|
||||
verbose_name='Reachable'
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = ConsolePort
|
||||
fields = ('device', 'name', 'console_server', 'console_server_port', 'reachable')
|
||||
|
||||
|
||||
class PowerConnectionTable(BaseTable):
|
||||
pdu = tables.Column(
|
||||
accessor=Accessor('_path__destination__device'),
|
||||
orderable=False,
|
||||
linkify=True,
|
||||
verbose_name='PDU'
|
||||
)
|
||||
outlet = tables.Column(
|
||||
accessor=Accessor('_path__destination'),
|
||||
orderable=False,
|
||||
linkify=True,
|
||||
verbose_name='Outlet'
|
||||
)
|
||||
device = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
name = tables.Column(
|
||||
linkify=True,
|
||||
verbose_name='Power Port'
|
||||
)
|
||||
reachable = columns.BooleanColumn(
|
||||
accessor=Accessor('_path__is_active'),
|
||||
verbose_name='Reachable'
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = PowerPort
|
||||
fields = ('device', 'name', 'pdu', 'outlet', 'reachable')
|
||||
|
||||
|
||||
class InterfaceConnectionTable(BaseTable):
|
||||
device_a = tables.Column(
|
||||
accessor=Accessor('device'),
|
||||
linkify=True,
|
||||
verbose_name='Device A'
|
||||
)
|
||||
interface_a = tables.Column(
|
||||
accessor=Accessor('name'),
|
||||
linkify=True,
|
||||
verbose_name='Interface A'
|
||||
)
|
||||
device_b = tables.Column(
|
||||
accessor=Accessor('_path__destination__device'),
|
||||
orderable=False,
|
||||
linkify=True,
|
||||
verbose_name='Device B'
|
||||
)
|
||||
interface_b = tables.Column(
|
||||
accessor=Accessor('_path__destination'),
|
||||
orderable=False,
|
||||
linkify=True,
|
||||
verbose_name='Interface B'
|
||||
)
|
||||
reachable = columns.BooleanColumn(
|
||||
accessor=Accessor('_path__is_active'),
|
||||
verbose_name='Reachable'
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Interface
|
||||
fields = ('device_a', 'interface_a', 'device_b', 'interface_b', 'reachable')
|
||||
|
71
netbox/dcim/tables/connections.py
Normal file
71
netbox/dcim/tables/connections.py
Normal file
@ -0,0 +1,71 @@
|
||||
import django_tables2 as tables
|
||||
from django_tables2.utils import Accessor
|
||||
|
||||
from netbox.tables import BaseTable, columns
|
||||
from dcim.models import ConsolePort, Interface, PowerPort
|
||||
from .devices import PathEndpointTable
|
||||
|
||||
__all__ = (
|
||||
'ConsoleConnectionTable',
|
||||
'InterfaceConnectionTable',
|
||||
'PowerConnectionTable',
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Device connections
|
||||
#
|
||||
|
||||
class ConsoleConnectionTable(PathEndpointTable):
|
||||
device = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
name = tables.Column(
|
||||
linkify=True,
|
||||
verbose_name='Console Port'
|
||||
)
|
||||
reachable = columns.BooleanColumn(
|
||||
accessor=Accessor('_path__is_active'),
|
||||
verbose_name='Reachable'
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = ConsolePort
|
||||
fields = ('device', 'name', 'connection', 'reachable')
|
||||
|
||||
|
||||
class PowerConnectionTable(PathEndpointTable):
|
||||
device = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
name = tables.Column(
|
||||
linkify=True,
|
||||
verbose_name='Power Port'
|
||||
)
|
||||
reachable = columns.BooleanColumn(
|
||||
accessor=Accessor('_path__is_active'),
|
||||
verbose_name='Reachable'
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = PowerPort
|
||||
fields = ('device', 'name', 'connection', 'reachable')
|
||||
|
||||
|
||||
class InterfaceConnectionTable(PathEndpointTable):
|
||||
device = tables.Column(
|
||||
accessor=Accessor('device'),
|
||||
linkify=True
|
||||
)
|
||||
interface = tables.Column(
|
||||
accessor=Accessor('name'),
|
||||
linkify=True
|
||||
)
|
||||
reachable = columns.BooleanColumn(
|
||||
accessor=Accessor('_path__is_active'),
|
||||
verbose_name='Reachable'
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Interface
|
||||
fields = ('device', 'interface', 'connection', 'reachable')
|
@ -226,7 +226,7 @@ POWEROUTLET_BUTTONS = """
|
||||
"""
|
||||
|
||||
INTERFACE_BUTTONS = """
|
||||
{% if perms.ipam.add_ipaddress or perms.dcim.add_inventoryitem %}
|
||||
{% if perms.dcim.edit_interface %}
|
||||
<span class="dropdown">
|
||||
<button type="button" class="btn btn-primary btn-sm dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false" title="Add">
|
||||
<span class="mdi mdi-plus-thick" aria-hidden="true"></span>
|
||||
@ -238,6 +238,15 @@ INTERFACE_BUTTONS = """
|
||||
{% if perms.dcim.add_inventoryitem %}
|
||||
<li><a class="dropdown-item" href="{% url 'dcim:inventoryitem_add' %}?device={{ record.device_id }}&component_type={{ record|content_type_id }}&component_id={{ record.pk }}&return_url={% url 'dcim:device_interfaces' pk=object.pk %}">Inventory Item</a></li>
|
||||
{% endif %}
|
||||
{% if perms.dcim.add_interface %}
|
||||
<li><a class="dropdown-item" href="{% url 'dcim:interface_add' %}?device={{ record.device_id }}&parent={{ record.pk }}&name_pattern={{ record.name }}.&type=virtual&return_url={% url 'dcim:device_interfaces' pk=object.pk %}">Child Interface</a></li>
|
||||
{% endif %}
|
||||
{% if perms.ipam.add_l2vpntermination %}
|
||||
<li><a class="dropdown-item" href="{% url 'ipam:l2vpntermination_add' %}?device={{ object.pk }}&interface={{ record.pk }}&return_url={% url 'dcim:device_interfaces' pk=object.pk %}">L2VPN Termination</a></li>
|
||||
{% endif %}
|
||||
{% if perms.ipam.add_fhrpgroupassignment %}
|
||||
<li><a class="dropdown-item" href="{% url 'ipam:fhrpgroupassignment_add' %}?interface_type={{ record|content_type_id }}&interface_id={{ record.pk }}&return_url={% url 'dcim:device_interfaces' pk=object.pk %}">Assign FHRP Group</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</span>
|
||||
{% endif %}
|
||||
|
@ -461,16 +461,19 @@ class DeviceTypeTest(APIViewTestCases.APIViewTestCase):
|
||||
'manufacturer': manufacturers[1].pk,
|
||||
'model': 'Device Type 4',
|
||||
'slug': 'device-type-4',
|
||||
'u_height': 0,
|
||||
},
|
||||
{
|
||||
'manufacturer': manufacturers[1].pk,
|
||||
'model': 'Device Type 5',
|
||||
'slug': 'device-type-5',
|
||||
'u_height': 0.5,
|
||||
},
|
||||
{
|
||||
'manufacturer': manufacturers[1].pk,
|
||||
'model': 'Device Type 6',
|
||||
'slug': 'device-type-6',
|
||||
'u_height': 1,
|
||||
},
|
||||
]
|
||||
|
||||
|
@ -2893,7 +2893,7 @@ class CableBulkDeleteView(generic.BulkDeleteView):
|
||||
#
|
||||
|
||||
class ConsoleConnectionsListView(generic.ObjectListView):
|
||||
queryset = ConsolePort.objects.filter(_path__isnull=False).order_by('device')
|
||||
queryset = ConsolePort.objects.filter(_path__is_complete=True)
|
||||
filterset = filtersets.ConsoleConnectionFilterSet
|
||||
filterset_form = forms.ConsoleConnectionFilterForm
|
||||
table = tables.ConsoleConnectionTable
|
||||
@ -2907,7 +2907,7 @@ class ConsoleConnectionsListView(generic.ObjectListView):
|
||||
|
||||
|
||||
class PowerConnectionsListView(generic.ObjectListView):
|
||||
queryset = PowerPort.objects.filter(_path__isnull=False).order_by('device')
|
||||
queryset = PowerPort.objects.filter(_path__is_complete=True)
|
||||
filterset = filtersets.PowerConnectionFilterSet
|
||||
filterset_form = forms.PowerConnectionFilterForm
|
||||
table = tables.PowerConnectionTable
|
||||
@ -2921,7 +2921,7 @@ class PowerConnectionsListView(generic.ObjectListView):
|
||||
|
||||
|
||||
class InterfaceConnectionsListView(generic.ObjectListView):
|
||||
queryset = Interface.objects.filter(_path__isnull=False).order_by('device')
|
||||
queryset = Interface.objects.filter(_path__is_complete=True)
|
||||
filterset = filtersets.InterfaceConnectionFilterSet
|
||||
filterset_form = forms.InterfaceConnectionFilterForm
|
||||
table = tables.InterfaceConnectionTable
|
||||
|
@ -1,5 +1,6 @@
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from rest_framework.fields import Field
|
||||
from rest_framework.serializers import ValidationError
|
||||
|
||||
from extras.choices import CustomFieldTypeChoices
|
||||
from extras.models import CustomField
|
||||
@ -62,6 +63,12 @@ class CustomFieldsDataField(Field):
|
||||
return data
|
||||
|
||||
def to_internal_value(self, data):
|
||||
if type(data) is not dict:
|
||||
raise ValidationError(
|
||||
"Invalid data format. Custom field data must be passed as a dictionary mapping field names to their "
|
||||
"values."
|
||||
)
|
||||
|
||||
# If updating an existing instance, start with existing custom_field_data
|
||||
if self.parent.instance:
|
||||
data = {**self.parent.instance.custom_field_data, **data}
|
||||
|
@ -965,7 +965,11 @@ class L2VPNFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
qs_filter = Q(identifier=value) | Q(name__icontains=value) | Q(description__icontains=value)
|
||||
qs_filter = Q(name__icontains=value) | Q(description__icontains=value)
|
||||
try:
|
||||
qs_filter |= Q(identifier=int(value))
|
||||
except ValueError:
|
||||
pass
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
|
||||
@ -1071,6 +1075,12 @@ class L2VPNTerminationFilterSet(NetBoxModelFilterSet):
|
||||
qs_filter = Q(l2vpn__name__icontains=value)
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
def filter_assigned_object(self, queryset, name, value):
|
||||
qs = queryset.filter(
|
||||
Q(**{'{}__in'.format(name): value})
|
||||
)
|
||||
return qs
|
||||
|
||||
def filter_site(self, queryset, name, value):
|
||||
qs = queryset.filter(
|
||||
Q(
|
||||
|
18
netbox/ipam/migrations/0060_alter_l2vpn_slug.py
Normal file
18
netbox/ipam/migrations/0060_alter_l2vpn_slug.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.0.7 on 2022-08-22 15:58
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('ipam', '0059_l2vpn'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='l2vpn',
|
||||
name='slug',
|
||||
field=models.SlugField(max_length=100, unique=True),
|
||||
),
|
||||
]
|
@ -35,13 +35,16 @@ class GetAvailablePrefixesMixin:
|
||||
|
||||
def get_available_prefixes(self):
|
||||
"""
|
||||
Return all available Prefixes within this aggregate as an IPSet.
|
||||
Return all available prefixes within this Aggregate or Prefix as an IPSet.
|
||||
"""
|
||||
prefix = netaddr.IPSet(self.prefix)
|
||||
child_prefixes = netaddr.IPSet([child.prefix for child in self.get_child_prefixes()])
|
||||
available_prefixes = prefix - child_prefixes
|
||||
params = {
|
||||
'prefix__net_contained': str(self.prefix)
|
||||
}
|
||||
if hasattr(self, 'vrf'):
|
||||
params['vrf'] = self.vrf
|
||||
|
||||
return available_prefixes
|
||||
child_prefixes = Prefix.objects.filter(**params).values_list('prefix', flat=True)
|
||||
return netaddr.IPSet(self.prefix) - netaddr.IPSet(child_prefixes)
|
||||
|
||||
def get_first_available_prefix(self):
|
||||
"""
|
||||
@ -124,6 +127,10 @@ class ASN(NetBoxModel):
|
||||
def __str__(self):
|
||||
return f'AS{self.asn_with_asdot}'
|
||||
|
||||
@classmethod
|
||||
def get_prerequisite_models(cls):
|
||||
return [RIR, ]
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('ipam:asn', args=[self.pk])
|
||||
|
||||
@ -185,6 +192,10 @@ class Aggregate(GetAvailablePrefixesMixin, NetBoxModel):
|
||||
def __str__(self):
|
||||
return str(self.prefix)
|
||||
|
||||
@classmethod
|
||||
def get_prerequisite_models(cls):
|
||||
return [RIR, ]
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('ipam:aggregate', args=[self.pk])
|
||||
|
||||
|
@ -1,8 +1,10 @@
|
||||
from django.apps import apps
|
||||
from django.contrib.contenttypes.fields import GenericRelation, GenericForeignKey
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils.functional import cached_property
|
||||
|
||||
from ipam.choices import L2VPNTypeChoices
|
||||
from ipam.constants import L2VPN_ASSIGNMENT_MODELS
|
||||
@ -19,7 +21,10 @@ class L2VPN(NetBoxModel):
|
||||
max_length=100,
|
||||
unique=True
|
||||
)
|
||||
slug = models.SlugField()
|
||||
slug = models.SlugField(
|
||||
max_length=100,
|
||||
unique=True
|
||||
)
|
||||
type = models.CharField(
|
||||
max_length=50,
|
||||
choices=L2VPNTypeChoices
|
||||
@ -53,6 +58,8 @@ class L2VPN(NetBoxModel):
|
||||
to='tenancy.ContactAssignment'
|
||||
)
|
||||
|
||||
clone_fields = ('type',)
|
||||
|
||||
class Meta:
|
||||
ordering = ('name', 'identifier')
|
||||
verbose_name = 'L2VPN'
|
||||
@ -65,6 +72,13 @@ class L2VPN(NetBoxModel):
|
||||
def get_absolute_url(self):
|
||||
return reverse('ipam:l2vpn', args=[self.pk])
|
||||
|
||||
@cached_property
|
||||
def can_add_termination(self):
|
||||
if self.type in L2VPNTypeChoices.P2P and self.terminations.count() >= 2:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
class L2VPNTermination(NetBoxModel):
|
||||
l2vpn = models.ForeignKey(
|
||||
@ -101,6 +115,10 @@ class L2VPNTermination(NetBoxModel):
|
||||
return f'{self.assigned_object} <> {self.l2vpn}'
|
||||
return super().__str__()
|
||||
|
||||
@classmethod
|
||||
def get_prerequisite_models(cls):
|
||||
return [apps.get_model('ipam.L2VPN'), ]
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('ipam:l2vpntermination', args=[self.pk])
|
||||
|
||||
|
@ -32,15 +32,6 @@ PREFIX_LINK = """
|
||||
<a href="{% if record.pk %}{% url 'ipam:prefix' pk=record.pk %}{% else %}{% url 'ipam:prefix_add' %}?prefix={{ record }}{% if object.vrf %}&vrf={{ object.vrf.pk }}{% endif %}{% if object.site %}&site={{ object.site.pk }}{% endif %}{% if object.tenant %}&tenant_group={{ object.tenant.group.pk }}&tenant={{ object.tenant.pk }}{% endif %}{% endif %}">{{ record.prefix }}</a>
|
||||
"""
|
||||
|
||||
PREFIXFLAT_LINK = """
|
||||
{% load helpers %}
|
||||
{% if record.pk %}
|
||||
<a href="{% url 'ipam:prefix' pk=record.pk %}">{{ record.prefix }}</a>
|
||||
{% else %}
|
||||
{{ record.prefix }}
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
IPADDRESS_LINK = """
|
||||
{% if record.pk %}
|
||||
<a href="{{ record.get_absolute_url }}">{{ record.address }}</a>
|
||||
@ -229,9 +220,9 @@ class PrefixTable(TenancyColumnsMixin, NetBoxTable):
|
||||
export_raw=True,
|
||||
attrs={'td': {'class': 'text-nowrap'}}
|
||||
)
|
||||
prefix_flat = tables.TemplateColumn(
|
||||
template_code=PREFIXFLAT_LINK,
|
||||
attrs={'td': {'class': 'text-nowrap'}},
|
||||
prefix_flat = tables.Column(
|
||||
accessor=Accessor('prefix'),
|
||||
linkify=True,
|
||||
verbose_name='Prefix (Flat)',
|
||||
)
|
||||
depth = tables.Column(
|
||||
@ -369,8 +360,8 @@ class IPAddressTable(TenancyColumnsMixin, NetBoxTable):
|
||||
orderable=False,
|
||||
verbose_name='NAT (Inside)'
|
||||
)
|
||||
nat_outside = tables.Column(
|
||||
linkify=True,
|
||||
nat_outside = tables.ManyToManyColumn(
|
||||
linkify_item=True,
|
||||
orderable=False,
|
||||
verbose_name='NAT (Outside)'
|
||||
)
|
||||
|
@ -33,10 +33,10 @@ class L2VPNTable(TenancyColumnsMixin, NetBoxTable):
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = L2VPN
|
||||
fields = (
|
||||
'pk', 'name', 'slug', 'type', 'description', 'import_targets', 'export_targets', 'tenant', 'tenant_group',
|
||||
'pk', 'name', 'slug', 'identifier', 'type', 'description', 'import_targets', 'export_targets', 'tenant', 'tenant_group',
|
||||
'actions',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'type', 'description', 'actions')
|
||||
default_columns = ('pk', 'name', 'identifier', 'type', 'description', 'actions')
|
||||
|
||||
|
||||
class L2VPNTerminationTable(NetBoxTable):
|
||||
|
@ -390,7 +390,7 @@ class PrefixTest(APIViewTestCases.APIViewTestCase):
|
||||
self.assertEqual(response.data['description'], data['description'])
|
||||
|
||||
# Try to create one more IP
|
||||
response = self.client.post(url, {}, **self.header)
|
||||
response = self.client.post(url, {}, format='json', **self.header)
|
||||
self.assertHttpStatus(response, status.HTTP_409_CONFLICT)
|
||||
self.assertIn('detail', response.data)
|
||||
|
||||
@ -487,7 +487,7 @@ class IPRangeTest(APIViewTestCases.APIViewTestCase):
|
||||
self.assertEqual(response.data['description'], data['description'])
|
||||
|
||||
# Try to create one more IP
|
||||
response = self.client.post(url, {}, **self.header)
|
||||
response = self.client.post(url, {}, format='json', **self.header)
|
||||
self.assertHttpStatus(response, status.HTTP_409_CONFLICT)
|
||||
self.assertIn('detail', response.data)
|
||||
|
||||
@ -973,9 +973,9 @@ class L2VPNTerminationTest(APIViewTestCases.APIViewTestCase):
|
||||
VLAN.objects.bulk_create(vlans)
|
||||
|
||||
l2vpns = (
|
||||
L2VPN(name='L2VPN 1', type='vxlan', identifier=650001),
|
||||
L2VPN(name='L2VPN 2', type='vpws', identifier=650002),
|
||||
L2VPN(name='L2VPN 3', type='vpls'), # No RD
|
||||
L2VPN(name='L2VPN 1', slug='l2vpn-1', type='vxlan', identifier=650001),
|
||||
L2VPN(name='L2VPN 2', slug='l2vpn-2', type='vpws', identifier=650002),
|
||||
L2VPN(name='L2VPN 3', slug='l2vpn-3', type='vpls'), # No RD
|
||||
)
|
||||
L2VPN.objects.bulk_create(l2vpns)
|
||||
|
||||
|
@ -1485,9 +1485,9 @@ class L2VPNTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
RouteTarget.objects.bulk_create(route_targets)
|
||||
|
||||
l2vpns = (
|
||||
L2VPN(name='L2VPN 1', type=L2VPNTypeChoices.TYPE_VXLAN, identifier=65001),
|
||||
L2VPN(name='L2VPN 2', type=L2VPNTypeChoices.TYPE_VPWS, identifier=65002),
|
||||
L2VPN(name='L2VPN 3', type=L2VPNTypeChoices.TYPE_VPLS),
|
||||
L2VPN(name='L2VPN 1', slug='l2vpn-1', type=L2VPNTypeChoices.TYPE_VXLAN, identifier=65001),
|
||||
L2VPN(name='L2VPN 2', slug='l2vpn-2', type=L2VPNTypeChoices.TYPE_VPWS, identifier=65002),
|
||||
L2VPN(name='L2VPN 3', slug='l2vpn-3', type=L2VPNTypeChoices.TYPE_VPLS),
|
||||
)
|
||||
L2VPN.objects.bulk_create(l2vpns)
|
||||
l2vpns[0].import_targets.add(route_targets[0])
|
||||
|
@ -581,9 +581,9 @@ class TestL2VPNTermination(TestCase):
|
||||
VLAN.objects.bulk_create(vlans)
|
||||
|
||||
l2vpns = (
|
||||
L2VPN(name='L2VPN 1', type='vxlan', identifier=650001),
|
||||
L2VPN(name='L2VPN 2', type='vpws', identifier=650002),
|
||||
L2VPN(name='L2VPN 3', type='vpls'), # No RD
|
||||
L2VPN(name='L2VPN 1', slug='l2vpn-1', type='vxlan', identifier=650001),
|
||||
L2VPN(name='L2VPN 2', slug='l2vpn-2', type='vpws', identifier=650002),
|
||||
L2VPN(name='L2VPN 3', slug='l2vpn-3', type='vpls'), # No RD
|
||||
)
|
||||
L2VPN.objects.bulk_create(l2vpns)
|
||||
|
||||
|
@ -526,8 +526,7 @@ class PrefixIPAddressesView(generic.ObjectChildrenView):
|
||||
return parent.get_child_ips().restrict(request.user, 'view').prefetch_related('vrf', 'tenant', 'tenant__group')
|
||||
|
||||
def prep_table_data(self, request, queryset, parent):
|
||||
show_available = bool(request.GET.get('show_available', 'true') == 'true')
|
||||
if show_available:
|
||||
if not request.GET.get('q'):
|
||||
return add_available_ipaddresses(parent.prefix, queryset, parent.is_pool)
|
||||
|
||||
return queryset
|
||||
|
@ -55,6 +55,10 @@ def get_auth_backend_display(name):
|
||||
return AUTH_BACKEND_ATTRS.get(name, (name, None))
|
||||
|
||||
|
||||
def get_saml_idps():
|
||||
return getattr(settings, "SOCIAL_AUTH_SAML_ENABLED_IDPS", {}).keys()
|
||||
|
||||
|
||||
class ObjectPermissionMixin:
|
||||
|
||||
def get_all_permissions(self, user_obj, obj=None):
|
||||
|
@ -28,6 +28,14 @@ class NetBoxFeatureSet(
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
@classmethod
|
||||
def get_prerequisite_models(cls):
|
||||
"""
|
||||
Return a list of model types that are required to create this model or empty list if none. This is used for
|
||||
showing prequisite warnings in the UI on the list and detail views.
|
||||
"""
|
||||
return []
|
||||
|
||||
|
||||
#
|
||||
# Base model classes
|
||||
|
@ -62,7 +62,7 @@ DCIM_TYPES = {
|
||||
'url': 'dcim:rack_list',
|
||||
},
|
||||
'rackreservation': {
|
||||
'queryset': RackReservation.objects.prefetch_related('site', 'rack', 'user'),
|
||||
'queryset': RackReservation.objects.prefetch_related('rack', 'user'),
|
||||
'filterset': dcim.filtersets.RackReservationFilterSet,
|
||||
'table': dcim.tables.RackReservationTable,
|
||||
'url': 'dcim:rackreservation_list',
|
||||
|
@ -29,7 +29,7 @@ django.utils.encoding.force_text = force_str
|
||||
# Environment setup
|
||||
#
|
||||
|
||||
VERSION = '3.3.0'
|
||||
VERSION = '3.3.1'
|
||||
|
||||
# Hostname
|
||||
HOSTNAME = platform.node()
|
||||
@ -533,6 +533,9 @@ REST_FRAMEWORK = {
|
||||
),
|
||||
'DEFAULT_METADATA_CLASS': 'netbox.api.metadata.BulkOperationMetadata',
|
||||
'DEFAULT_PAGINATION_CLASS': 'netbox.api.pagination.OptionalLimitOffsetPagination',
|
||||
'DEFAULT_PARSER_CLASSES': (
|
||||
'rest_framework.parsers.JSONParser',
|
||||
),
|
||||
'DEFAULT_PERMISSION_CLASSES': (
|
||||
'netbox.api.authentication.TokenPermissions',
|
||||
),
|
||||
@ -542,7 +545,6 @@ REST_FRAMEWORK = {
|
||||
),
|
||||
'DEFAULT_VERSION': REST_FRAMEWORK_VERSION,
|
||||
'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.AcceptHeaderVersioning',
|
||||
# 'PAGE_SIZE': PAGINATE_COUNT,
|
||||
'SCHEMA_COERCE_METHOD_NAMES': {
|
||||
# Default mappings
|
||||
'retrieve': 'read',
|
||||
|
@ -26,6 +26,7 @@ from utilities.permissions import get_permission_for_model
|
||||
from utilities.views import GetReturnURLMixin
|
||||
from .base import BaseMultiObjectView
|
||||
from .mixins import ActionsMixin, TableMixin
|
||||
from .utils import get_prerequisite_model
|
||||
|
||||
__all__ = (
|
||||
'BulkComponentCreateView',
|
||||
@ -165,13 +166,16 @@ class ObjectListView(BaseMultiObjectView, ActionsMixin, TableMixin):
|
||||
'table': table,
|
||||
})
|
||||
|
||||
return render(request, self.template_name, {
|
||||
context = {
|
||||
'model': model,
|
||||
'table': table,
|
||||
'actions': actions,
|
||||
'filter_form': self.filterset_form(request.GET, label_suffix='') if self.filterset_form else None,
|
||||
'prerequisite_model': get_prerequisite_model(self.queryset),
|
||||
**self.get_extra_context(request),
|
||||
})
|
||||
}
|
||||
|
||||
return render(request, self.template_name, context)
|
||||
|
||||
|
||||
class BulkCreateView(GetReturnURLMixin, BaseMultiObjectView):
|
||||
|
@ -21,6 +21,7 @@ from utilities.utils import get_viewname, normalize_querydict, prepare_cloned_fi
|
||||
from utilities.views import GetReturnURLMixin
|
||||
from .base import BaseObjectView
|
||||
from .mixins import ActionsMixin, TableMixin
|
||||
from .utils import get_prerequisite_model
|
||||
|
||||
__all__ = (
|
||||
'ComponentCreateView',
|
||||
@ -327,6 +328,12 @@ class ObjectEditView(GetReturnURLMixin, BaseObjectView):
|
||||
"""
|
||||
return obj
|
||||
|
||||
def get_extra_addanother_params(self, request):
|
||||
"""
|
||||
Return a dictionary of extra parameters to use on the Add Another button.
|
||||
"""
|
||||
return {}
|
||||
|
||||
#
|
||||
# Request handlers
|
||||
#
|
||||
@ -340,15 +347,18 @@ class ObjectEditView(GetReturnURLMixin, BaseObjectView):
|
||||
"""
|
||||
obj = self.get_object(**kwargs)
|
||||
obj = self.alter_object(obj, request, args, kwargs)
|
||||
model = self.queryset.model
|
||||
|
||||
initial_data = normalize_querydict(request.GET)
|
||||
form = self.form(instance=obj, initial=initial_data)
|
||||
restrict_form_fields(form, request.user)
|
||||
|
||||
return render(request, self.template_name, {
|
||||
'model': model,
|
||||
'object': obj,
|
||||
'form': form,
|
||||
'return_url': self.get_return_url(request, obj),
|
||||
'prerequisite_model': get_prerequisite_model(self.queryset),
|
||||
**self.get_extra_context(request, obj),
|
||||
})
|
||||
|
||||
@ -399,6 +409,7 @@ class ObjectEditView(GetReturnURLMixin, BaseObjectView):
|
||||
|
||||
# If cloning is supported, pre-populate a new instance of the form
|
||||
params = prepare_cloned_fields(obj)
|
||||
params.update(self.get_extra_addanother_params(request))
|
||||
if params:
|
||||
if 'return_url' in request.GET:
|
||||
params['return_url'] = request.GET.get('return_url')
|
||||
|
12
netbox/netbox/views/generic/utils.py
Normal file
12
netbox/netbox/views/generic/utils.py
Normal file
@ -0,0 +1,12 @@
|
||||
def get_prerequisite_model(queryset):
|
||||
model = queryset.model
|
||||
|
||||
if not queryset.exists():
|
||||
if hasattr(model, 'get_prerequisite_models'):
|
||||
prerequisites = model.get_prerequisite_models()
|
||||
if prerequisites:
|
||||
for prereq in prerequisites:
|
||||
if not prereq.objects.exists():
|
||||
return prereq
|
||||
|
||||
return None
|
@ -49,10 +49,12 @@
|
||||
{% render_field form.description %}
|
||||
</div>
|
||||
|
||||
<div class="field-group my-5">
|
||||
<div class="row mb-2">
|
||||
<h5 class="offset-sm-3">Custom Fields</h5>
|
||||
{% if form.custom_fields %}
|
||||
<div class="field-group mb-5">
|
||||
<div class="row mb-2">
|
||||
<h5 class="offset-sm-3">Custom Fields</h5>
|
||||
</div>
|
||||
{% render_custom_fields form %}
|
||||
</div>
|
||||
{% render_custom_fields form %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
@ -155,9 +155,7 @@
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
<div class="card">
|
||||
<h5 class="card-header">
|
||||
Management
|
||||
</h5>
|
||||
<h5 class="card-header">Management</h5>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
@ -178,9 +176,9 @@
|
||||
{% if object.primary_ip4 %}
|
||||
<a href="{{ object.primary_ip4.get_absolute_url }}">{{ object.primary_ip4.address.ip }}</a>
|
||||
{% if object.primary_ip4.nat_inside %}
|
||||
(NAT for {{ object.primary_ip4.nat_inside.address.ip|linkify }})
|
||||
{% elif object.primary_ip4.nat_outside %}
|
||||
(NAT: {{ object.primary_ip4.nat_outside.address.ip|linkify }})
|
||||
(NAT for <a href="{{ object.primary_ip4.nat_inside.get_absolute_url }}">{{ object.primary_ip4.nat_inside.address.ip }}</a>)
|
||||
{% elif object.primary_ip4.nat_outside.exists %}
|
||||
(NAT for {% for nat in object.primary_ip4.nat_outside.all %}<a href="{{ nat.get_absolute_url }}">{{ nat.address.ip }}</a>{% if not forloop.last %}, {% endif %}{% endfor %})
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
@ -193,9 +191,9 @@
|
||||
{% if object.primary_ip6 %}
|
||||
<a href="{{ object.primary_ip6.get_absolute_url }}">{{ object.primary_ip6.address.ip }}</a>
|
||||
{% if object.primary_ip6.nat_inside %}
|
||||
(NAT for {{ object.primary_ip6.nat_inside.address.ip|linkify }})
|
||||
{% elif object.primary_ip6.nat_outside %}
|
||||
(NAT: {{ object.primary_ip6.nat_outside.address.ip|linkify }})
|
||||
(NAT for <a href="{{ object.primary_ip6.nat_inside.get_absolute_url }}">{{ object.primary_ip6.nat_inside.address.ip }}</a>)
|
||||
{% elif object.primary_ip6.nat_outside.exists %}
|
||||
(NAT for {% for nat in object.primary_ip6.nat_outside.all %}<a href="{{ nat.get_absolute_url }}">{{ nat.address.ip }}</a>{% if not forloop.last %}, {% endif %}{% endfor %})
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
|
@ -32,23 +32,25 @@
|
||||
{% for iface in interfaces %}
|
||||
<tr id="{{ iface.name }}">
|
||||
<td>{{ iface }}</td>
|
||||
{% if iface.connected_endpoint.device %}
|
||||
<td class="configured_device" data="{{ iface.connected_endpoint.device.name }}" data-chassis="{{ iface.connected_endpoint.device.virtual_chassis.name }}">
|
||||
<a href="{% url 'dcim:device' pk=iface.connected_endpoint.device.pk %}">{{ iface.connected_endpoint.device }}</a>
|
||||
</td>
|
||||
<td class="configured_interface" data="{{ iface.connected_endpoint.name }}">
|
||||
<span title="{{ iface.connected_endpoint.get_type_display }}">{{ iface.connected_endpoint }}</span>
|
||||
</td>
|
||||
{% elif iface.connected_endpoint.circuit %}
|
||||
{% with circuit=iface.connected_endpoint.circuit %}
|
||||
<td colspan="2">
|
||||
<i class="mdi mdi-lightning-bolt" title="Circuit"></i>
|
||||
<a href="{{ circuit.get_absolute_url }}">{{ circuit.provider }} {{ circuit }}</a>
|
||||
</td>
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
<td class="text-muted" colspan="2">None</td>
|
||||
{% endif %}
|
||||
{% with peer=iface.connected_endpoints.0 %}
|
||||
{% if peer.device %}
|
||||
<td class="configured_device" data="{{ peer.device.name }}" data-chassis="{{ peer.device.virtual_chassis.name }}">
|
||||
<a href="{% url 'dcim:device' pk=peer.device.pk %}">{{ peer.device }}</a>
|
||||
</td>
|
||||
<td class="configured_interface" data="{{ peer.name }}">
|
||||
<span title="{{ peer.get_type_display }}">{{ peer }}</span>
|
||||
</td>
|
||||
{% elif peer.circuit %}
|
||||
{% with circuit=peer.circuit %}
|
||||
<td colspan="2">
|
||||
<i class="mdi mdi-lightning-bolt" title="Circuit"></i>
|
||||
<a href="{{ circuit.get_absolute_url }}">{{ circuit.provider }} {{ circuit }}</a>
|
||||
</td>
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
<td class="text-muted" colspan="2">None</td>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
<td class="device"></td>
|
||||
<td class="interface"></td>
|
||||
</tr>
|
||||
|
@ -74,7 +74,7 @@
|
||||
<td>{{ object.get_poe_mode_display|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">PoE Mode</th>
|
||||
<th scope="row">PoE Type</th>
|
||||
<td>{{ object.get_poe_type_display|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
@ -31,6 +31,11 @@ Context:
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane show active" id="edit-form" role="tabpanel" aria-labelledby="object-list-tab">
|
||||
|
||||
{# Warn about missing prerequisite objects #}
|
||||
{% if prerequisite_model %}
|
||||
{% include 'inc/missing_prerequisites.html' %}
|
||||
{% endif %}
|
||||
|
||||
{# Link to model documentation #}
|
||||
{% if object and settings.DOCS_ROOT %}
|
||||
<div class="float-end">
|
||||
|
@ -100,6 +100,11 @@ Context:
|
||||
<input type="hidden" name="return_url" value="{% if return_url %}{{ return_url }}{% else %}{{ request.path }}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}{% endif %}" />
|
||||
|
||||
{# Object table #}
|
||||
|
||||
{% if prerequisite_model %}
|
||||
{% include 'inc/missing_prerequisites.html' %}
|
||||
{% endif %}
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body" id="object_list">
|
||||
{% include 'htmx/table.html' %}
|
||||
|
9
netbox/templates/inc/missing_prerequisites.html
Normal file
9
netbox/templates/inc/missing_prerequisites.html
Normal file
@ -0,0 +1,9 @@
|
||||
{% load buttons %}
|
||||
|
||||
<div class="alert alert-warning text-end" role="alert">
|
||||
<div class="float-start">
|
||||
<i class="mdi mdi-alert"></i> Before you can add a {{ model|meta:"verbose_name" }} you must first create a
|
||||
<strong>{{ prerequisite_model|meta:"verbose_name" }}</strong>.
|
||||
</div>
|
||||
{% add_button prerequisite_model %}
|
||||
</div>
|
@ -12,9 +12,9 @@
|
||||
<table class="table table-hover attr-table">
|
||||
{% for field, value in fields.items %}
|
||||
<tr>
|
||||
<td>
|
||||
<th scope="row">
|
||||
<span title="{{ field.description|escape }}">{{ field }}</span>
|
||||
</td>
|
||||
</th>
|
||||
<td>
|
||||
{% customfield_value field value %}
|
||||
</td>
|
||||
|
@ -91,10 +91,13 @@
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Outside NAT IPs</th>
|
||||
<th scope="row">NAT (Outside)</th>
|
||||
<td>
|
||||
{% for ip in object.nat_outside.all %}
|
||||
{{ ip|linkify }}<br/>
|
||||
{{ ip|linkify }}
|
||||
{% if ip.assigned_object %}
|
||||
({{ ip.assigned_object.parent_object|linkify }})
|
||||
{% endif %}<br/>
|
||||
{% empty %}
|
||||
{{ ''|placeholder }}
|
||||
{% endfor %}
|
||||
|
@ -59,7 +59,7 @@
|
||||
</div>
|
||||
{% if perms.ipam.add_l2vpntermination %}
|
||||
<div class="card-footer text-end noprint">
|
||||
<a href="{% url 'ipam:l2vpntermination_add' %}?l2vpn={{ object.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-primary btn-sm">
|
||||
<a href="{% url 'ipam:l2vpntermination_add' %}?l2vpn={{ object.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-primary btn-sm{% if not object.can_add_termination %} disabled" aria-disabled="true{% endif %}">
|
||||
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add a Termination
|
||||
</a>
|
||||
</div>
|
||||
|
@ -41,10 +41,10 @@
|
||||
|
||||
{% if auth_backends %}
|
||||
<h6 class="mt-4 mb-3">Or use a single sign-on (SSO) provider:</h6>
|
||||
{% for name, display in auth_backends.items %}
|
||||
{% for backend in auth_backends %}
|
||||
<h5>
|
||||
{% if display.1 %}<i class="mdi mdi-{{ display.1 }}"></i>{% endif %}
|
||||
<a href="{% url 'social:begin' backend=name %}" class="my-2">{{ display.0 }}</a>
|
||||
{% if backend.icon_name %}<i class="mdi mdi-{{ backend.icon_name }}"></i>{% endif %}
|
||||
<a href="{{ backend.url }}" class="my-2">{{ backend.display_name }}</a>
|
||||
</h5>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
@ -45,8 +45,8 @@
|
||||
<a href="{% url 'ipam:ipaddress' pk=object.primary_ip4.pk %}">{{ object.primary_ip4.address.ip }}</a>
|
||||
{% if object.primary_ip4.nat_inside %}
|
||||
(NAT for <a href="{{ object.primary_ip4.nat_inside.get_absolute_url }}">{{ object.primary_ip4.nat_inside.address.ip }}</a>)
|
||||
{% elif object.primary_ip4.nat_outside %}
|
||||
(NAT: <a href="{{ object.primary_ip4.nat_outside.get_absolute_url }}">{{ object.primary_ip4.nat_outside.address.ip }}</a>)
|
||||
{% elif object.primary_ip4.nat_outside.exists %}
|
||||
(NAT for {% for nat in object.primary_ip4.nat_outside.all %}<a href="{{ nat.get_absolute_url }}">{{ nat.address.ip }}</a>{% if not forloop.last %}, {% endif %}{% endfor %})
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
@ -60,8 +60,8 @@
|
||||
<a href="{% url 'ipam:ipaddress' pk=object.primary_ip6.pk %}">{{ object.primary_ip6.address.ip }}</a>
|
||||
{% if object.primary_ip6.nat_inside %}
|
||||
(NAT for <a href="{{ object.primary_ip6.nat_inside.get_absolute_url }}">{{ object.primary_ip6.nat_inside.address.ip }}</a>)
|
||||
{% elif object.primary_ip6.nat_outside %}
|
||||
(NAT: <a href="{{ object.primary_ip6.nat_outside.get_absolute_url }}">{{ object.primary_ip6.nat_outside.address.ip }}</a>)
|
||||
{% elif object.primary_ip6.nat_outside.exists %}
|
||||
(NAT for {% for nat in object.primary_ip6.nat_outside.all %}<a href="{{ nat.get_absolute_url }}">{{ nat.address.ip }}</a>{% if not forloop.last %}, {% endif %}{% endfor %})
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
|
@ -1,4 +1,5 @@
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.http import QueryDict
|
||||
from django.shortcuts import get_object_or_404
|
||||
|
||||
from circuits.models import Circuit
|
||||
@ -365,6 +366,12 @@ class ContactAssignmentEditView(generic.ObjectEditView):
|
||||
instance.object = get_object_or_404(content_type.model_class(), pk=request.GET.get('object_id'))
|
||||
return instance
|
||||
|
||||
def get_extra_addanother_params(self, request):
|
||||
return {
|
||||
'content_type': request.GET.get('content_type'),
|
||||
'object_id': request.GET.get('object_id'),
|
||||
}
|
||||
|
||||
|
||||
class ContactAssignmentDeleteView(generic.ObjectDeleteView):
|
||||
queryset = ContactAssignment.objects.all()
|
||||
|
@ -124,7 +124,7 @@ class TokenTest(
|
||||
user = User.objects.create_user(**data)
|
||||
url = reverse('users-api:token_provision')
|
||||
|
||||
response = self.client.post(url, **self.header, data=data)
|
||||
response = self.client.post(url, data, format='json', **self.header)
|
||||
self.assertEqual(response.status_code, 201)
|
||||
self.assertIn('key', response.data)
|
||||
self.assertEqual(len(response.data['key']), 40)
|
||||
@ -141,7 +141,7 @@ class TokenTest(
|
||||
}
|
||||
url = reverse('users-api:token_provision')
|
||||
|
||||
response = self.client.post(url, **self.header, data=data)
|
||||
response = self.client.post(url, data, format='json', **self.header)
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
|
||||
|
@ -10,14 +10,14 @@ from django.http import HttpResponseRedirect
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils.http import url_has_allowed_host_and_scheme
|
||||
from django.utils.http import url_has_allowed_host_and_scheme, urlencode
|
||||
from django.views.decorators.debug import sensitive_post_parameters
|
||||
from django.views.generic import View
|
||||
from social_core.backends.utils import load_backends
|
||||
|
||||
from extras.models import ObjectChange
|
||||
from extras.tables import ObjectChangeTable
|
||||
from netbox.authentication import get_auth_backend_display
|
||||
from netbox.authentication import get_auth_backend_display, get_saml_idps
|
||||
from netbox.config import get_config
|
||||
from utilities.forms import ConfirmationForm
|
||||
from .forms import LoginForm, PasswordChangeForm, TokenForm, UserConfigForm
|
||||
@ -39,6 +39,14 @@ class LoginView(View):
|
||||
def dispatch(self, *args, **kwargs):
|
||||
return super().dispatch(*args, **kwargs)
|
||||
|
||||
def gen_auth_data(self, name, url):
|
||||
display_name, icon_name = get_auth_backend_display(name)
|
||||
return {
|
||||
'display_name': display_name,
|
||||
'icon_name': icon_name,
|
||||
'url': url,
|
||||
}
|
||||
|
||||
def get(self, request):
|
||||
form = LoginForm(request)
|
||||
|
||||
@ -46,9 +54,19 @@ class LoginView(View):
|
||||
logger = logging.getLogger('netbox.auth.login')
|
||||
return self.redirect_to_next(request, logger)
|
||||
|
||||
auth_backends = {
|
||||
name: get_auth_backend_display(name) for name in load_backends(settings.AUTHENTICATION_BACKENDS).keys()
|
||||
}
|
||||
auth_backends = []
|
||||
saml_idps = get_saml_idps()
|
||||
for name in load_backends(settings.AUTHENTICATION_BACKENDS).keys():
|
||||
url = reverse('social:begin', args=[name, ])
|
||||
if name.lower() == 'saml' and saml_idps:
|
||||
for idp in saml_idps:
|
||||
params = {'idp': idp}
|
||||
idp_url = f'{url}?{urlencode(params)}'
|
||||
data = self.gen_auth_data(name, idp_url)
|
||||
data['display_name'] = f'{data["display_name"]} ({idp})'
|
||||
auth_backends.append(data)
|
||||
else:
|
||||
auth_backends.append(self.gen_auth_data(name, url))
|
||||
|
||||
return render(request, self.template_name, {
|
||||
'form': form,
|
||||
|
@ -5,7 +5,7 @@ import re
|
||||
import yaml
|
||||
from django import template
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.utils.html import strip_tags
|
||||
from django.utils.html import escape
|
||||
from django.utils.safestring import mark_safe
|
||||
from markdown import markdown
|
||||
|
||||
@ -35,7 +35,7 @@ def linkify(instance, attr=None):
|
||||
text = getattr(instance, attr) if attr is not None else str(instance)
|
||||
try:
|
||||
url = instance.get_absolute_url()
|
||||
return mark_safe(f'<a href="{url}">{text}</a>')
|
||||
return mark_safe(f'<a href="{url}">{escape(text)}</a>')
|
||||
except (AttributeError, TypeError):
|
||||
return text
|
||||
|
||||
|
@ -285,7 +285,7 @@ def prepare_cloned_fields(instance):
|
||||
"""
|
||||
# Generate the clone attributes from the instance
|
||||
if not hasattr(instance, 'clone'):
|
||||
return QueryDict()
|
||||
return QueryDict(mutable=True)
|
||||
attrs = instance.clone()
|
||||
|
||||
# Prepare querydict parameters
|
||||
|
@ -167,6 +167,10 @@ class Cluster(NetBoxModel):
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@classmethod
|
||||
def get_prerequisite_models(cls):
|
||||
return [ClusterType, ]
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('virtualization:cluster', args=[self.pk])
|
||||
|
||||
@ -312,6 +316,10 @@ class VirtualMachine(NetBoxModel, ConfigContextModel):
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@classmethod
|
||||
def get_prerequisite_models(cls):
|
||||
return [Cluster, ]
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('virtualization:virtualmachine', args=[self.pk])
|
||||
|
||||
|
@ -12,10 +12,23 @@ __all__ = (
|
||||
)
|
||||
|
||||
VMINTERFACE_BUTTONS = """
|
||||
{% if perms.ipam.add_ipaddress %}
|
||||
<a href="{% url 'ipam:ipaddress_add' %}?vminterface={{ record.pk }}&return_url={% url 'virtualization:virtualmachine_interfaces' pk=object.pk %}" class="btn btn-sm btn-success" title="Add IP Address">
|
||||
<i class="mdi mdi-plus-thick" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% if perms.virtualization.edit_vminterface %}
|
||||
<span class="dropdown">
|
||||
<button type="button" class="btn btn-primary btn-sm dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false" title="Add">
|
||||
<span class="mdi mdi-plus-thick" aria-hidden="true"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
{% if perms.ipam.add_ipaddress %}
|
||||
<li><a class="dropdown-item" href="{% url 'ipam:ipaddress_add' %}?vminterface={{ record.pk }}&return_url={% url 'virtualization:virtualmachine_interfaces' pk=object.pk %}">IP Address</a></li>
|
||||
{% endif %}
|
||||
{% if perms.ipam.add_l2vpntermination %}
|
||||
<li><a class="dropdown-item" href="{% url 'ipam:l2vpntermination_add' %}?virtual_machine={{ object.pk }}&vminterface={{ record.pk }}&return_url={% url 'virtualization:virtualmachine_interfaces' pk=object.pk %}">L2VPN Termination</a></li>
|
||||
{% endif %}
|
||||
{% if perms.ipam.add_fhrpgroupassignment %}
|
||||
<li><a class="dropdown-item" href="{% url 'ipam:fhrpgroupassignment_add' %}?interface_type={{ record|content_type_id }}&interface_id={{ record.pk }}&return_url={% url 'virtualization:virtualmachine_interfaces' pk=object.pk %}">Assign FHRP Group</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</span>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
|
@ -0,0 +1,24 @@
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import wireless.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0161_cabling_cleanup'),
|
||||
('wireless', '0004_wireless_tenancy'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='wirelesslink',
|
||||
name='interface_a',
|
||||
field=models.ForeignKey(limit_choices_to=wireless.models.get_wireless_interface_types, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='dcim.interface'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='wirelesslink',
|
||||
name='interface_b',
|
||||
field=models.ForeignKey(limit_choices_to=wireless.models.get_wireless_interface_types, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='dcim.interface'),
|
||||
),
|
||||
]
|
@ -1,3 +1,4 @@
|
||||
from django.apps import apps
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
@ -23,7 +24,8 @@ class WirelessAuthenticationBase(models.Model):
|
||||
auth_type = models.CharField(
|
||||
max_length=50,
|
||||
choices=WirelessAuthTypeChoices,
|
||||
blank=True
|
||||
blank=True,
|
||||
verbose_name="Auth Type",
|
||||
)
|
||||
auth_cipher = models.CharField(
|
||||
max_length=50,
|
||||
@ -126,21 +128,29 @@ class WirelessLAN(WirelessAuthenticationBase, NetBoxModel):
|
||||
return reverse('wireless:wirelesslan', args=[self.pk])
|
||||
|
||||
|
||||
def get_wireless_interface_types():
|
||||
# Wrap choices in a callable to avoid generating dummy migrations
|
||||
# when the choices are updated.
|
||||
return {'type__in': WIRELESS_IFACE_TYPES}
|
||||
|
||||
|
||||
class WirelessLink(WirelessAuthenticationBase, NetBoxModel):
|
||||
"""
|
||||
A point-to-point connection between two wireless Interfaces.
|
||||
"""
|
||||
interface_a = models.ForeignKey(
|
||||
to='dcim.Interface',
|
||||
limit_choices_to={'type__in': WIRELESS_IFACE_TYPES},
|
||||
limit_choices_to=get_wireless_interface_types,
|
||||
on_delete=models.PROTECT,
|
||||
related_name='+'
|
||||
related_name='+',
|
||||
verbose_name="Interface A",
|
||||
)
|
||||
interface_b = models.ForeignKey(
|
||||
to='dcim.Interface',
|
||||
limit_choices_to={'type__in': WIRELESS_IFACE_TYPES},
|
||||
limit_choices_to=get_wireless_interface_types,
|
||||
on_delete=models.PROTECT,
|
||||
related_name='+'
|
||||
related_name='+',
|
||||
verbose_name="Interface B",
|
||||
)
|
||||
ssid = models.CharField(
|
||||
max_length=SSID_MAX_LENGTH,
|
||||
@ -190,6 +200,10 @@ class WirelessLink(WirelessAuthenticationBase, NetBoxModel):
|
||||
def __str__(self):
|
||||
return f'#{self.pk}'
|
||||
|
||||
@classmethod
|
||||
def get_prerequisite_models(cls):
|
||||
return [apps.get_model('dcim.Interface'), ]
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('wireless:wirelesslink', args=[self.pk])
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
bleach==5.0.1
|
||||
Django==4.0.7
|
||||
django-cors-headers==3.13.0
|
||||
django-debug-toolbar==3.5.0
|
||||
django-debug-toolbar==3.6.0
|
||||
django-filter==22.1
|
||||
django-graphiql-debug-toolbar==0.2.0
|
||||
django-mptt==0.13.4
|
||||
@ -19,7 +19,7 @@ graphene-django==2.15.0
|
||||
gunicorn==20.1.0
|
||||
Jinja2==3.1.2
|
||||
Markdown==3.4.1
|
||||
mkdocs-material==8.4.0
|
||||
mkdocs-material==8.4.1
|
||||
mkdocstrings[python-legacy]==0.19.0
|
||||
netaddr==0.8.0
|
||||
Pillow==9.2.0
|
||||
|
Loading…
Reference in New Issue
Block a user