From 587a34442a822e99357ee92daa9b5f43f3ce9ef6 Mon Sep 17 00:00:00 2001 From: Brian Candler Date: Fri, 15 Jul 2022 17:10:15 +0100 Subject: [PATCH 01/62] Documentation: distinguish release and git upgrade processes Fixes #9743 --- docs/installation/upgrading.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/docs/installation/upgrading.md b/docs/installation/upgrading.md index 014dffaf8..deeec883a 100644 --- a/docs/installation/upgrading.md +++ b/docs/installation/upgrading.md @@ -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`. From 3b4dd051f2492aa05f84ca85ffb891ece831b210 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 17 Aug 2022 14:11:47 -0400 Subject: [PATCH 02/62] PRVB --- docs/release-notes/version-3.3.md | 4 ++++ netbox/netbox/settings.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/version-3.3.md b/docs/release-notes/version-3.3.md index 5cafa685e..98004e2a5 100644 --- a/docs/release-notes/version-3.3.md +++ b/docs/release-notes/version-3.3.md @@ -1,5 +1,9 @@ # NetBox v3.3 +## v3.3.1 (FUTURE) + +--- + ## v3.3.0 (2022-08-17) ### Breaking Changes diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 22b1e1f02..0edce8f69 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -29,7 +29,7 @@ django.utils.encoding.force_text = force_str # Environment setup # -VERSION = '3.3.0' +VERSION = '3.3.1-dev' # Hostname HOSTNAME = platform.node() From c7d6fe2d6236b691b65a73a63db65282bc342bbe Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 17 Aug 2022 15:37:48 -0400 Subject: [PATCH 03/62] Fixes #10053: Custom fields header should not be displayed when editing circuit terminations with no custom fields --- docs/release-notes/version-3.3.md | 4 ++++ .../templates/circuits/circuittermination_edit.html | 12 +++++++----- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/docs/release-notes/version-3.3.md b/docs/release-notes/version-3.3.md index 98004e2a5..e633e35de 100644 --- a/docs/release-notes/version-3.3.md +++ b/docs/release-notes/version-3.3.md @@ -2,6 +2,10 @@ ## v3.3.1 (FUTURE) +### Bug Fixes + +* [#10053](https://github.com/netbox-community/netbox/issues/10053) - Custom fields header should not be displayed when editing circuit terminations with no custom fields + --- ## v3.3.0 (2022-08-17) diff --git a/netbox/templates/circuits/circuittermination_edit.html b/netbox/templates/circuits/circuittermination_edit.html index 9c38a3c72..5196eddf2 100644 --- a/netbox/templates/circuits/circuittermination_edit.html +++ b/netbox/templates/circuits/circuittermination_edit.html @@ -49,10 +49,12 @@ {% render_field form.description %} -
-
-
Custom Fields
+ {% if form.custom_fields %} +
+
+
Custom Fields
+
+ {% render_custom_fields form %}
- {% render_custom_fields form %} -
+ {% endif %} {% endblock %} From 279253c486e4aff7f1471c93431c35eb91338237 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Thu, 18 Aug 2022 09:49:45 -0400 Subject: [PATCH 04/62] Fixes #10040: Fix exception when ordering prefixes by flat representation --- docs/release-notes/version-3.3.md | 1 + netbox/ipam/tables/ip.py | 15 +++------------ 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/docs/release-notes/version-3.3.md b/docs/release-notes/version-3.3.md index e633e35de..222ba797b 100644 --- a/docs/release-notes/version-3.3.md +++ b/docs/release-notes/version-3.3.md @@ -4,6 +4,7 @@ ### Bug Fixes +* [#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 --- diff --git a/netbox/ipam/tables/ip.py b/netbox/ipam/tables/ip.py index 087d0de73..20e63fe55 100644 --- a/netbox/ipam/tables/ip.py +++ b/netbox/ipam/tables/ip.py @@ -32,15 +32,6 @@ PREFIX_LINK = """ {{ record.prefix }} """ -PREFIXFLAT_LINK = """ -{% load helpers %} -{% if record.pk %} - {{ record.prefix }} -{% else %} - {{ record.prefix }} -{% endif %} -""" - IPADDRESS_LINK = """ {% if record.pk %} {{ record.address }} @@ -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( From 9059c096278e9e278f457368268b45425d6058dd Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Thu, 18 Aug 2022 13:40:44 -0400 Subject: [PATCH 05/62] Refresh development docs --- docs/development/getting-started.md | 92 ++++++++--------- docs/development/index.md | 51 +++++++--- docs/development/models.md | 2 +- docs/development/release-checklist.md | 107 +++++++++++--------- docs/development/style-guide.md | 71 ++++++++----- docs/media/development/github.png | Bin 0 -> 112957 bytes docs/media/development/github_new_issue.png | Bin 0 -> 46534 bytes 7 files changed, 186 insertions(+), 137 deletions(-) create mode 100644 docs/media/development/github.png create mode 100644 docs/media/development/github_new_issue.png diff --git a/docs/development/getting-started.md b/docs/development/getting-started.md index dbbe8378d..38d521de6 100644 --- a/docs/development/getting-started.md +++ b/docs/development/getting-started.md @@ -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. @@ -38,90 +38,85 @@ CHANGELOG.md CONTRIBUTING.md LICENSE.txt netbox README.md scri 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 +### 2. 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 ``` -### Create a Python Virtual Environment +### 3. 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 +### 4. 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 +### 5. 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 +### 6. 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. ## Populating Demo Data @@ -131,48 +126,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 +python manage.py test --parallel ``` 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.) diff --git a/docs/development/index.md b/docs/development/index.md index 85762d0fe..0d570abe6 100644 --- a/docs/development/index.md +++ b/docs/development/index.md @@ -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. +![GitHub](../media/development/github.png) -## 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. + +![Opening a new GitHub issue](../media/development/github_new_issue.png) + +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. diff --git a/docs/development/models.md b/docs/development/models.md index 3b03c8935..01070fa3d 100644 --- a/docs/development/models.md +++ b/docs/development/models.md @@ -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`). diff --git a/docs/development/release-checklist.md b/docs/development/release-checklist.md index 17c27948d..efb0f44b9 100644 --- a/docs/development/release-checklist.md +++ b/docs/development/release-checklist.md @@ -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. diff --git a/docs/development/style-guide.md b/docs/development/style-guide.md index 2a6d86ab0..283ad698c 100644 --- a/docs/development/style-guide.md +++ b/docs/development/style-guide.md @@ -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. diff --git a/docs/media/development/github.png b/docs/media/development/github.png new file mode 100644 index 0000000000000000000000000000000000000000..6ac58f5fb2afbe24a1ed642907d946c6d2936a96 GIT binary patch literal 112957 zcmdSAWmKHY(y*Q2B-kLqZEy+hE`z%h+yVr52@b*C-Q6Kr@Zj$5?(PonWS?iB^T_`F zt@ZI^W(|Gc-PPSy-Cb2z2g}QfA%4L5@b29^L--3ey|Hp|hyW!nC zqIVJ?L1h=6V{K?Hm8F{3yl$&jr@p#o@SkQC1_)XZph;WR+}v||+V;J2WyQyr^3~+i zTX3??!Ty!2tj6x2)e;mPWaliS*$H(V|CEdQZrJ<=wqVM}Fh>(8u?9S#A z^tX%x=wbpE6+zg)&lkCwkO0g`O=|FGNwn?1u%*VVyTdV^fo=8*&mq`o`65?5brURV zh_M;nq`>{Nv&bc9!r#lw2p3y@!U#uLCYxw(TbjmrAIa_2jJ?4b|4s-L3g1}MMp&}Z^( z)ilsg_DF>LBd!3}xFRMf7=z(fTRf0$x4K2sIBd`3-}cPWMZ}u$g9K_#1y~++rD6Ep z&jkYZ!FF27pqIS12JSI5R~3xtL7K>6~jj77h7UMgo#Eu z=qK^y|4{^f*!R_qK;*lsdGK&Xvm*(@XCJWbhs6#tK1>6X`f3zOAj``*N=`u6X|s&l+W4mbzN!y)f1m0AMbgsXl~hp zKT{RW4W24?M`^Rqhhc87avRpff>cUkeaH4Jd5m(#EYw)kfR^VqR-^tX+3l=?90KUb zEtHp$)P`tY&0WfQw}YR5tJJw05fPi(c0_LRpTXIV5FRhe`o?A&A)%t5+^w;lrH_S^ z5d6q{)se5W)W#3&rx$*_KIpX3BlYTOYJ_N?W{@e6D^`U-vgI?E@{`=E!ilFn+R@fvS52TECmtaYZ!7SI@ z?}GB`O?LQ5gxo$!j0RcFGO&3v>6d+3q|QE6A9ITtv|sP|9Zn`NkqP)fOS7NXS+aDa zlmf%?{OXR_ClW|)8D{Kxe!iuTOiK;f0=$2xIN~z6$8oQ=x?vuhd6^7_D0FFml?J{u zY5eh5PxM6u;KbDdAD=D?{%C-HA1jYZ5)(NP@rCb)7ZuR-j3w^2?8)2W3)9x|L#XLm z0jDGeObo}4ThZze?}kItANcBB#o;zPayx4&;R_7gQ!Qz|kLnyHjxWekA^8y`L- z=5-2unYrI@aQ&EA^g@O?mPuyBpg5iw`r*$qioyiZ&i$U#_7XiU4Yq4}7y{|GmCWhk0csaHH52p$T4d%V-t*3cA39KE~Ia`6}rTLxO88tc6g-Jmb_ zyV>rIOlEIq6ml5`El;M$3pMO0XX4QJ+;mohNzaEM8#@`NLxISFsdY|A7`C!6NRd=i7}z{+n^7Gb9FC8) zDRpL;#|;JHxSf{$S$bZWK-0yt-HQo)w3Ql;l%*V*K8ENH?dH1@##RaFKvXj6ppEtUyOJ}gq z&99B={QA)zLoRsP9+PrgWf%9CJ}JQY1d$TKBH?##_6*q^T(;j|AH*&qrwj=0MVtV* zosQ3hcDBR_T`U)b%w1m2xZ2&u+CZ`f9Rs%Bhg0cG&e8O`8EYAEvZ;J%Q^v!-M<^!K zrAkYbP8(#tJ(6*PXF9y{4YHqsK62;5ZC%Ms#^4RdYkDLnIa?yZtSGe_IN2xTFQ?15 zad@<(ZsW>(%g^zkeU&1I3N(dqpnX|xG->Bz5=Agaz@J7AMk7;v^L*We)(QxRre|8F zj;A;<-O8F&K(hk41fg_PW%G#VK`2bB2OQ9xo!GT%)bs9p6pP-5IDLoO?bI|5yG*(;qs-T ztldQMGdD-<42HWFiHna0Gh)34$d^h01>Rcv zrM+>BLA|`ZD3q&eZx2S}%VqNRb-LDs8(lcm;_b;f_oek%lM=Xhb$vfHHI#BWWzQ-74j{1FV!+!guD+-=XNIO5)JEIal4CVm$O2rjUv=ZZGe#!cD+d9`HEw; z{3}?!+920)XxDVAiEQ3*cCLaS>3GFIiP5mgy!Gx@bipp3;oBxh9HkCIXd-Rbpo;nV z{)J(lbTkAKl7~Wzse)sf6==M#4|a>6*SG`Ubd67=t zhtV|Nz@>(>qE>8eEf^Rb8fj#_4%k%G1mvO5_%K|CD*-a|&Gu46@YACvOzIVij+Y@b zok`JF#5zu0y(3kG>rhW^t>gKI=lxl)!O~D4G5bT!*{wEEE!|>BZG~#Tt)jTv8LI^% zVez2$grax93ilPs4NMejis5nCr2)se o#uWDohEwk-lq+&NKIiP1pP#m}Z+1uc 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≻gw5&lrYyOO5K5{K0r71HCG(nVvFauV}a- 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 literal 0 HcmV?d00001 From 804c064a7efd5dac8a6df0850b878d35bae587ef Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Thu, 18 Aug 2022 15:19:34 -0400 Subject: [PATCH 06/62] Closes #10061: Replicate type when cloning L2VPN instances --- docs/release-notes/version-3.3.md | 4 ++++ netbox/ipam/models/l2vpn.py | 2 ++ 2 files changed, 6 insertions(+) diff --git a/docs/release-notes/version-3.3.md b/docs/release-notes/version-3.3.md index 222ba797b..382d6c29e 100644 --- a/docs/release-notes/version-3.3.md +++ b/docs/release-notes/version-3.3.md @@ -2,6 +2,10 @@ ## v3.3.1 (FUTURE) +### Enhancements + +* [#10061](https://github.com/netbox-community/netbox/issues/10061) - Replicate type when cloning L2VPN instances + ### Bug Fixes * [#10040](https://github.com/netbox-community/netbox/issues/10040) - Fix exception when ordering prefixes by flat representation diff --git a/netbox/ipam/models/l2vpn.py b/netbox/ipam/models/l2vpn.py index 3c08b5f48..0e948b18e 100644 --- a/netbox/ipam/models/l2vpn.py +++ b/netbox/ipam/models/l2vpn.py @@ -53,6 +53,8 @@ class L2VPN(NetBoxModel): to='tenancy.ContactAssignment' ) + clone_fields = ('type',) + class Meta: ordering = ('name', 'identifier') verbose_name = 'L2VPN' From eb3d3dcbc4b65c819a0cb92de4794491f59f7687 Mon Sep 17 00:00:00 2001 From: Arthur Date: Thu, 18 Aug 2022 13:58:40 -0700 Subject: [PATCH 07/62] #6454 add prerequisite alert --- netbox/dcim/models/racks.py | 5 +++++ netbox/dcim/views.py | 1 + netbox/netbox/models/__init__.py | 4 ++++ netbox/netbox/views/generic/bulk_views.py | 15 +++++++++++++++ netbox/netbox/views/generic/object_views.py | 12 ++++++++++-- netbox/netbox/views/generic/utils.py | 12 ++++++++++++ netbox/templates/generic/object_edit.html | 4 ++++ netbox/templates/generic/object_list.html | 6 ++++++ netbox/templates/inc/missing_prerequisites.html | 5 +++++ 9 files changed, 62 insertions(+), 2 deletions(-) create mode 100644 netbox/netbox/views/generic/utils.py create mode 100644 netbox/templates/inc/missing_prerequisites.html diff --git a/netbox/dcim/models/racks.py b/netbox/dcim/models/racks.py index 81d699b11..e57934353 100644 --- a/netbox/dcim/models/racks.py +++ b/netbox/dcim/models/racks.py @@ -1,5 +1,6 @@ from collections import OrderedDict +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 @@ -202,6 +203,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]) diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 966d90876..6e77d4396 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -560,6 +560,7 @@ class RackRoleBulkDeleteView(generic.BulkDeleteView): # class RackListView(generic.ObjectListView): + required_prerequisites = [Site] queryset = Rack.objects.prefetch_related('devices__device_type').annotate( device_count=count_related(Device, 'rack') ) diff --git a/netbox/netbox/models/__init__.py b/netbox/netbox/models/__init__.py index ea2feb8de..2524c7c9b 100644 --- a/netbox/netbox/models/__init__.py +++ b/netbox/netbox/models/__init__.py @@ -52,6 +52,10 @@ class NetBoxModel(NetBoxFeatureSet, models.Model): class Meta: abstract = True + @classmethod + def get_prerequisite_models(cls): + return [] + class NestedGroupModel(NetBoxFeatureSet, MPTTModel): """ diff --git a/netbox/netbox/views/generic/bulk_views.py b/netbox/netbox/views/generic/bulk_views.py index 5aea9c469..29007985c 100644 --- a/netbox/netbox/views/generic/bulk_views.py +++ b/netbox/netbox/views/generic/bulk_views.py @@ -25,6 +25,7 @@ from utilities.htmx import is_htmx from utilities.permissions import get_permission_for_model from utilities.views import GetReturnURLMixin from .base import BaseMultiObjectView +from .utils import get_prerequisite_model __all__ = ( 'BulkComponentCreateView', @@ -143,6 +144,7 @@ class ObjectListView(BaseMultiObjectView): """ model = self.queryset.model content_type = ContentType.objects.get_for_model(model) + requirement = get_prerequisite_model(self.queryset) if self.filterset: self.queryset = self.filterset(request.GET, self.queryset).qs @@ -198,6 +200,8 @@ class ObjectListView(BaseMultiObjectView): 'filter_form': self.filterset_form(request.GET, label_suffix='') if self.filterset_form else None, **self.get_extra_context(request), } + if requirement: + context['required_model'] = requirement return render(request, self.template_name, context) @@ -256,6 +260,17 @@ class BulkCreateView(GetReturnURLMixin, BaseMultiObjectView): form = self.form() model_form = self.model_form(initial=initial) + context = { + 'obj_type': self.model_form._meta.model._meta.verbose_name, + 'form': form, + 'model_form': model_form, + 'return_url': self.get_return_url(request), + **self.get_extra_context(request), + } + + if requirement: + context['required_model'] = requirement + return render(request, self.template_name, { 'obj_type': self.model_form._meta.model._meta.verbose_name, 'form': form, diff --git a/netbox/netbox/views/generic/object_views.py b/netbox/netbox/views/generic/object_views.py index 88e078ae3..878f293a0 100644 --- a/netbox/netbox/views/generic/object_views.py +++ b/netbox/netbox/views/generic/object_views.py @@ -20,6 +20,7 @@ from utilities.permissions import get_permission_for_model from utilities.utils import get_viewname, normalize_querydict, prepare_cloned_fields from utilities.views import GetReturnURLMixin from .base import BaseObjectView +from .utils import get_prerequisite_model __all__ = ( 'ComponentCreateView', @@ -342,12 +343,19 @@ class ObjectEditView(GetReturnURLMixin, BaseObjectView): form = self.form(instance=obj, initial=initial_data) restrict_form_fields(form, request.user) - return render(request, self.template_name, { + context = { 'object': obj, 'form': form, 'return_url': self.get_return_url(request, obj), **self.get_extra_context(request, obj), - }) + } + + requirement = get_prerequisite_model(self.queryset) + if requirement: + context['required_model'] = requirement + context['model'] = self.queryset.model + + return render(request, self.template_name, context) def post(self, request, *args, **kwargs): """ diff --git a/netbox/netbox/views/generic/utils.py b/netbox/netbox/views/generic/utils.py new file mode 100644 index 000000000..57c7b5eba --- /dev/null +++ b/netbox/netbox/views/generic/utils.py @@ -0,0 +1,12 @@ +def get_prerequisite_model(queryset): + requirement = None + model = queryset.model + + if not queryset.count(): + prerequisites = model.get_prerequisite_models() + if prerequisites: + for prereq in prerequisites: + if not prereq.objects.count(): + requirement = prereq + + return requirement diff --git a/netbox/templates/generic/object_edit.html b/netbox/templates/generic/object_edit.html index 892c7d2b1..73e9727bb 100644 --- a/netbox/templates/generic/object_edit.html +++ b/netbox/templates/generic/object_edit.html @@ -40,6 +40,10 @@ Context:
{% endif %} + {% if required_model %} + {% include 'inc/missing_prerequisites.html' %} + {% endif %} +
{% csrf_token %} diff --git a/netbox/templates/generic/object_list.html b/netbox/templates/generic/object_list.html index 1e2ae796f..6910aa116 100644 --- a/netbox/templates/generic/object_list.html +++ b/netbox/templates/generic/object_list.html @@ -100,6 +100,12 @@ Context: {# Object table #} + + {% if required_model %} + {% include 'inc/missing_prerequisites.html' %} + {% endif %} + +
{% include 'htmx/table.html' %} diff --git a/netbox/templates/inc/missing_prerequisites.html b/netbox/templates/inc/missing_prerequisites.html new file mode 100644 index 000000000..c12b157d0 --- /dev/null +++ b/netbox/templates/inc/missing_prerequisites.html @@ -0,0 +1,5 @@ +{% load buttons %} + + From c811eb069d1d83c8735890199800eb3b2945db90 Mon Sep 17 00:00:00 2001 From: atownson <52260120+atownson@users.noreply.github.com> Date: Thu, 18 Aug 2022 16:05:29 -0500 Subject: [PATCH 08/62] netbox-community#10055 - Add loop for NAT Outside --- netbox/templates/dcim/device.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/netbox/templates/dcim/device.html b/netbox/templates/dcim/device.html index 8286f2c61..217362311 100644 --- a/netbox/templates/dcim/device.html +++ b/netbox/templates/dcim/device.html @@ -179,8 +179,8 @@ {{ object.primary_ip4.address.ip }} {% 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 }}) + {% elif object.primary_ip4.nat_outside.count > 0 %} + (NAT for {% for nat in object.primary_ip4.nat_outside.all %}{{ nat.address.ip }}{% if not forloop.last %}, {% endif %}{% endfor %}) {% endif %} {% else %} {{ ''|placeholder }} @@ -194,8 +194,8 @@ {{ object.primary_ip6.address.ip }} {% 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 }}) + {% elif object.primary_ip6.nat_outside.count > 0 %} + (NAT for {% for nat in object.primary_ip6.nat_outside.all %}{{ nat.address.ip }}{% if not forloop.last %}, {% endif %}{% endfor %}) {% endif %} {% else %} {{ ''|placeholder }} From a687aa1de6fe702d496a5d25435bd38af4340ddb Mon Sep 17 00:00:00 2001 From: atownson <52260120+atownson@users.noreply.github.com> Date: Thu, 18 Aug 2022 16:09:36 -0500 Subject: [PATCH 09/62] netbox-community#10055 - Add loop for NAT Outside --- netbox/templates/virtualization/virtualmachine.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/netbox/templates/virtualization/virtualmachine.html b/netbox/templates/virtualization/virtualmachine.html index f62da6fed..3826e0cf2 100644 --- a/netbox/templates/virtualization/virtualmachine.html +++ b/netbox/templates/virtualization/virtualmachine.html @@ -45,8 +45,8 @@ {{ object.primary_ip4.address.ip }} {% if object.primary_ip4.nat_inside %} (NAT for {{ object.primary_ip4.nat_inside.address.ip }}) - {% elif object.primary_ip4.nat_outside %} - (NAT: {{ object.primary_ip4.nat_outside.address.ip }}) + {% elif object.primary_ip4.nat_outside.count > 0 %} + (NAT for {% for nat in object.primary_ip4.nat_outside.all %}{{ nat.address.ip }}{% if not forloop.last %}, {% endif %}{% endfor %}) {% endif %} {% else %} {{ ''|placeholder }} @@ -60,8 +60,8 @@ {{ object.primary_ip6.address.ip }} {% if object.primary_ip6.nat_inside %} (NAT for {{ object.primary_ip6.nat_inside.address.ip }}) - {% elif object.primary_ip6.nat_outside %} - (NAT: {{ object.primary_ip6.nat_outside.address.ip }}) + {% elif object.primary_ip4.nat_outside.count > 0 %} + (NAT for {% for nat in object.primary_ip4.nat_outside.all %}{{ nat.address.ip }}{% if not forloop.last %}, {% endif %}{% endfor %}) {% endif %} {% else %} {{ ''|placeholder }} From 3f40e15ed5612b0567e0987b43e7e0f93447df7d Mon Sep 17 00:00:00 2001 From: atownson <52260120+atownson@users.noreply.github.com> Date: Thu, 18 Aug 2022 16:18:29 -0500 Subject: [PATCH 10/62] netbox-community#10055 - Add template for NAT Outside Fixes 'ipam.IPAddress.None' text --- netbox/ipam/tables/ip.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/netbox/ipam/tables/ip.py b/netbox/ipam/tables/ip.py index 20e63fe55..493488dac 100644 --- a/netbox/ipam/tables/ip.py +++ b/netbox/ipam/tables/ip.py @@ -56,6 +56,12 @@ VRF_LINK = """ {% endif %} """ +NAT_OUTSIDE_LINK = """ +{% if record.nat_outside.count > 0 %} + {% for nat in record.nat_outside.all %}{{ nat.address.ip }}{% if not forloop.last %}, {% endif %}{% endfor %} +{% endif %} +""" + # # RIRs @@ -360,8 +366,8 @@ class IPAddressTable(TenancyColumnsMixin, NetBoxTable): orderable=False, verbose_name='NAT (Inside)' ) - nat_outside = tables.Column( - linkify=True, + nat_outside = tables.TemplateColumn( + template_code=NAT_OUTSIDE_LINK, orderable=False, verbose_name='NAT (Outside)' ) From 0bdee1d6d8a352c2142b666ef53df6d738d0115b Mon Sep 17 00:00:00 2001 From: atownson <52260120+atownson@users.noreply.github.com> Date: Thu, 18 Aug 2022 16:22:22 -0500 Subject: [PATCH 11/62] netbox-community#10055 - Align NAT Outside with NAT Inside --- netbox/templates/ipam/ipaddress.html | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/netbox/templates/ipam/ipaddress.html b/netbox/templates/ipam/ipaddress.html index 8b628c2f7..ba0f0c5e6 100644 --- a/netbox/templates/ipam/ipaddress.html +++ b/netbox/templates/ipam/ipaddress.html @@ -91,10 +91,13 @@ - Outside NAT IPs + NAT (Outside) {% for ip in object.nat_outside.all %} - {{ ip|linkify }}
+ {{ ip|linkify }} + {% if ip.assigned_object %} + ({{ ip.assigned_object.parent_object|linkify }}) + {% endif %}
{% empty %} {{ ''|placeholder }} {% endfor %} From 928dff6b6879bf56c8953e0b137bbdaaa0a065e8 Mon Sep 17 00:00:00 2001 From: Arthur Date: Thu, 18 Aug 2022 15:11:03 -0700 Subject: [PATCH 12/62] #6454 add prerequisite alert --- netbox/circuits/models/circuits.py | 5 +++++ netbox/dcim/models/devices.py | 14 ++++++++++++++ netbox/dcim/models/power.py | 9 +++++++++ netbox/dcim/models/racks.py | 4 ++++ netbox/dcim/models/sites.py | 4 ++++ netbox/ipam/models/ip.py | 8 ++++++++ netbox/netbox/models/__init__.py | 8 ++++++++ netbox/netbox/views/generic/utils.py | 14 +++++++------- netbox/templates/inc/missing_prerequisites.html | 3 ++- netbox/virtualization/models.py | 8 ++++++++ netbox/wireless/models.py | 5 +++++ 11 files changed, 74 insertions(+), 8 deletions(-) diff --git a/netbox/circuits/models/circuits.py b/netbox/circuits/models/circuits.py index 02ba5209d..c14e365e1 100644 --- a/netbox/circuits/models/circuits.py +++ b/netbox/circuits/models/circuits.py @@ -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 @@ -129,6 +130,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]) diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py index 91227f1cf..8d524dcb1 100644 --- a/netbox/dcim/models/devices.py +++ b/netbox/dcim/models/devices.py @@ -1,4 +1,6 @@ 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 @@ -155,6 +157,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]) @@ -328,6 +334,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]) @@ -645,6 +655,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]) diff --git a/netbox/dcim/models/power.py b/netbox/dcim/models/power.py index 5978d86bd..5e355ce42 100644 --- a/netbox/dcim/models/power.py +++ b/netbox/dcim/models/power.py @@ -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, LinkTermination): 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]) diff --git a/netbox/dcim/models/racks.py b/netbox/dcim/models/racks.py index e57934353..d0600e987 100644 --- a/netbox/dcim/models/racks.py +++ b/netbox/dcim/models/racks.py @@ -473,6 +473,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]) diff --git a/netbox/dcim/models/sites.py b/netbox/dcim/models/sites.py index d02bd0932..70b4e6421 100644 --- a/netbox/dcim/models/sites.py +++ b/netbox/dcim/models/sites.py @@ -406,6 +406,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]) diff --git a/netbox/ipam/models/ip.py b/netbox/ipam/models/ip.py index d1538953a..ec054339c 100644 --- a/netbox/ipam/models/ip.py +++ b/netbox/ipam/models/ip.py @@ -124,6 +124,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 +189,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]) diff --git a/netbox/netbox/models/__init__.py b/netbox/netbox/models/__init__.py index 2524c7c9b..675103d06 100644 --- a/netbox/netbox/models/__init__.py +++ b/netbox/netbox/models/__init__.py @@ -89,6 +89,10 @@ class NestedGroupModel(NetBoxFeatureSet, MPTTModel): def __str__(self): return self.name + @classmethod + def get_prerequisite_models(cls): + return [] + def clean(self): super().clean() @@ -126,3 +130,7 @@ class OrganizationalModel(NetBoxFeatureSet, models.Model): class Meta: abstract = True ordering = ('name',) + + @classmethod + def get_prerequisite_models(cls): + return [] diff --git a/netbox/netbox/views/generic/utils.py b/netbox/netbox/views/generic/utils.py index 57c7b5eba..c682181a1 100644 --- a/netbox/netbox/views/generic/utils.py +++ b/netbox/netbox/views/generic/utils.py @@ -1,12 +1,12 @@ def get_prerequisite_model(queryset): - requirement = None model = queryset.model if not queryset.count(): - prerequisites = model.get_prerequisite_models() - if prerequisites: - for prereq in prerequisites: - if not prereq.objects.count(): - requirement = prereq + if hasattr(model, 'get_prerequisite_models'): + prerequisites = model.get_prerequisite_models() + if prerequisites: + for prereq in prerequisites: + if not prereq.objects.count(): + return prereq - return requirement + return None diff --git a/netbox/templates/inc/missing_prerequisites.html b/netbox/templates/inc/missing_prerequisites.html index c12b157d0..04043fc9c 100644 --- a/netbox/templates/inc/missing_prerequisites.html +++ b/netbox/templates/inc/missing_prerequisites.html @@ -1,5 +1,6 @@ {% load buttons %} diff --git a/netbox/virtualization/models.py b/netbox/virtualization/models.py index 586bb8a9e..b7151a1f0 100644 --- a/netbox/virtualization/models.py +++ b/netbox/virtualization/models.py @@ -162,6 +162,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]) @@ -288,6 +292,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]) diff --git a/netbox/wireless/models.py b/netbox/wireless/models.py index 0543e5621..f9838c0c7 100644 --- a/netbox/wireless/models.py +++ b/netbox/wireless/models.py @@ -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 @@ -174,6 +175,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]) From 43ad8e80b9bfab3eda91b0a771e4247726f54209 Mon Sep 17 00:00:00 2001 From: atownson <52260120+atownson@users.noreply.github.com> Date: Thu, 18 Aug 2022 17:12:44 -0500 Subject: [PATCH 13/62] netbox-community#10055: Added empty text --- netbox/ipam/tables/ip.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/netbox/ipam/tables/ip.py b/netbox/ipam/tables/ip.py index 493488dac..52b1c4393 100644 --- a/netbox/ipam/tables/ip.py +++ b/netbox/ipam/tables/ip.py @@ -59,6 +59,8 @@ VRF_LINK = """ NAT_OUTSIDE_LINK = """ {% if record.nat_outside.count > 0 %} {% for nat in record.nat_outside.all %}{{ nat.address.ip }}{% if not forloop.last %}, {% endif %}{% endfor %} +{% else %} + — {% endif %} """ From c65a29169878ef44e131a7601578781c595c8cf3 Mon Sep 17 00:00:00 2001 From: Arthur Date: Thu, 18 Aug 2022 16:00:17 -0700 Subject: [PATCH 14/62] #6454 add L2VPN check --- netbox/ipam/models/l2vpn.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/netbox/ipam/models/l2vpn.py b/netbox/ipam/models/l2vpn.py index 0e948b18e..809007033 100644 --- a/netbox/ipam/models/l2vpn.py +++ b/netbox/ipam/models/l2vpn.py @@ -1,3 +1,4 @@ +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 @@ -103,6 +104,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]) From 0a38c16cc2ee136705ac2841d45a66203ba7dc99 Mon Sep 17 00:00:00 2001 From: Arthur Date: Thu, 18 Aug 2022 16:06:57 -0700 Subject: [PATCH 15/62] Fix for #10056 --- netbox/templates/dcim/interface.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/templates/dcim/interface.html b/netbox/templates/dcim/interface.html index 7503e1be2..1216f3e88 100644 --- a/netbox/templates/dcim/interface.html +++ b/netbox/templates/dcim/interface.html @@ -74,7 +74,7 @@ {{ object.get_poe_mode_display|placeholder }} - PoE Mode + PoE Type {{ object.get_poe_type_display|placeholder }} From 3a7ea62874d0e6251dfe40358b653e9fb0cea6d8 Mon Sep 17 00:00:00 2001 From: Arthur Date: Thu, 18 Aug 2022 16:20:24 -0700 Subject: [PATCH 16/62] fix for #10057 --- netbox/netbox/search.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/netbox/search.py b/netbox/netbox/search.py index ef0c4fd87..32a9cfb1d 100644 --- a/netbox/netbox/search.py +++ b/netbox/netbox/search.py @@ -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', From c14a5973c7c1f3a1508a4f62ae4b21a918333680 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 22 Aug 2022 11:14:36 -0400 Subject: [PATCH 17/62] Fixes #10089: linkify template filter should escape object representation --- docs/release-notes/version-3.3.md | 1 + netbox/utilities/templatetags/builtins/filters.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/release-notes/version-3.3.md b/docs/release-notes/version-3.3.md index 382d6c29e..0d61e43be 100644 --- a/docs/release-notes/version-3.3.md +++ b/docs/release-notes/version-3.3.md @@ -10,6 +10,7 @@ * [#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 +* [#10089](https://github.com/netbox-community/netbox/issues/10089) - `linkify` template filter should escape object representation --- diff --git a/netbox/utilities/templatetags/builtins/filters.py b/netbox/utilities/templatetags/builtins/filters.py index bc395e438..6b548a89d 100644 --- a/netbox/utilities/templatetags/builtins/filters.py +++ b/netbox/utilities/templatetags/builtins/filters.py @@ -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'{text}') + return mark_safe(f'{escape(text)}') except (AttributeError, TypeError): return text From 2ef9e2d6fce664821e276b6d3ef0c18d362471de Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 22 Aug 2022 11:17:40 -0400 Subject: [PATCH 18/62] Closes #10066: Use fixed column widths for custom field values in UI --- docs/release-notes/version-3.3.md | 1 + netbox/templates/inc/panels/custom_fields.html | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/release-notes/version-3.3.md b/docs/release-notes/version-3.3.md index 0d61e43be..2651948a5 100644 --- a/docs/release-notes/version-3.3.md +++ b/docs/release-notes/version-3.3.md @@ -5,6 +5,7 @@ ### Enhancements * [#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 ### Bug Fixes diff --git a/netbox/templates/inc/panels/custom_fields.html b/netbox/templates/inc/panels/custom_fields.html index 616b1c712..45843eea5 100644 --- a/netbox/templates/inc/panels/custom_fields.html +++ b/netbox/templates/inc/panels/custom_fields.html @@ -12,9 +12,9 @@ {% for field, value in fields.items %} - From 917439725ad3cb6195159ca51266ce67a8175c72 Mon Sep 17 00:00:00 2001 From: Arthur Date: Mon, 22 Aug 2022 09:08:16 -0700 Subject: [PATCH 19/62] fix for #10059 - add identifier to L2VPN table --- netbox/ipam/tables/l2vpn.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/ipam/tables/l2vpn.py b/netbox/ipam/tables/l2vpn.py index 168c8ca89..077c6eb77 100644 --- a/netbox/ipam/tables/l2vpn.py +++ b/netbox/ipam/tables/l2vpn.py @@ -34,7 +34,7 @@ class L2VPNTable(TenancyColumnsMixin, NetBoxTable): model = L2VPN fields = ( 'pk', 'name', 'slug', 'type', 'description', 'import_targets', 'export_targets', 'tenant', 'tenant_group', - 'actions', + 'actions', 'identifier', ) default_columns = ('pk', 'name', 'type', 'description', 'actions') From ea1467add7ce308fc3203468cf0262e5cd75b2e0 Mon Sep 17 00:00:00 2001 From: Arthur Date: Mon, 22 Aug 2022 09:24:52 -0700 Subject: [PATCH 20/62] fix for #10086 - change capitalization on wireless link table for Interface A, B and Auth Type --- netbox/wireless/models.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/netbox/wireless/models.py b/netbox/wireless/models.py index 0540e9c45..fe3cdc1e2 100644 --- a/netbox/wireless/models.py +++ b/netbox/wireless/models.py @@ -23,7 +23,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, @@ -134,13 +135,15 @@ class WirelessLink(WirelessAuthenticationBase, NetBoxModel): to='dcim.Interface', limit_choices_to={'type__in': WIRELESS_IFACE_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}, on_delete=models.PROTECT, - related_name='+' + related_name='+', + verbose_name="Interface B", ) ssid = models.CharField( max_length=SSID_MAX_LENGTH, From e8f62eb1f97050c4cd310042307f7b64e686527e Mon Sep 17 00:00:00 2001 From: Arthur Date: Mon, 22 Aug 2022 11:17:01 -0700 Subject: [PATCH 21/62] #10059 change ordering of identifier column --- netbox/ipam/tables/l2vpn.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/netbox/ipam/tables/l2vpn.py b/netbox/ipam/tables/l2vpn.py index 077c6eb77..4a6af7c9b 100644 --- a/netbox/ipam/tables/l2vpn.py +++ b/netbox/ipam/tables/l2vpn.py @@ -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', - 'actions', 'identifier', + '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): From a972174706c2b4e857ca7019b4524c7216520fac Mon Sep 17 00:00:00 2001 From: Arthur Date: Mon, 22 Aug 2022 11:46:41 -0700 Subject: [PATCH 22/62] #6454 changes from PR review --- netbox/dcim/views.py | 1 - netbox/netbox/models/__init__.py | 20 ++++++++----------- netbox/netbox/views/generic/bulk_views.py | 15 +------------- netbox/netbox/views/generic/object_views.py | 15 ++++++-------- netbox/netbox/views/generic/utils.py | 4 ++-- netbox/templates/generic/object_edit.html | 2 +- netbox/templates/generic/object_list.html | 2 +- .../templates/inc/missing_prerequisites.html | 4 ++-- 8 files changed, 21 insertions(+), 42 deletions(-) diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index c807176d4..a31eabc5e 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -570,7 +570,6 @@ class RackRoleBulkDeleteView(generic.BulkDeleteView): # class RackListView(generic.ObjectListView): - required_prerequisites = [Site] queryset = Rack.objects.annotate( device_count=count_related(Device, 'rack') ) diff --git a/netbox/netbox/models/__init__.py b/netbox/netbox/models/__init__.py index c36d36f46..4c65094ca 100644 --- a/netbox/netbox/models/__init__.py +++ b/netbox/netbox/models/__init__.py @@ -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 @@ -53,10 +61,6 @@ class NetBoxModel(NetBoxFeatureSet, models.Model): class Meta: abstract = True - @classmethod - def get_prerequisite_models(cls): - return [] - def clone(self): """ Return a dictionary of attributes suitable for creating a copy of the current instance. This is used for pre- @@ -109,10 +113,6 @@ class NestedGroupModel(NetBoxFeatureSet, MPTTModel): def __str__(self): return self.name - @classmethod - def get_prerequisite_models(cls): - return [] - def clean(self): super().clean() @@ -150,7 +150,3 @@ class OrganizationalModel(NetBoxFeatureSet, models.Model): class Meta: abstract = True ordering = ('name',) - - @classmethod - def get_prerequisite_models(cls): - return [] diff --git a/netbox/netbox/views/generic/bulk_views.py b/netbox/netbox/views/generic/bulk_views.py index d30d67468..8fe0ad518 100644 --- a/netbox/netbox/views/generic/bulk_views.py +++ b/netbox/netbox/views/generic/bulk_views.py @@ -172,12 +172,10 @@ class ObjectListView(BaseMultiObjectView, ActionsMixin, TableMixin): 'table': table, 'actions': actions, 'filter_form': self.filterset_form(request.GET, label_suffix='') if self.filterset_form else None, + 'prerequisite_model': requirement if requirement else None, **self.get_extra_context(request), } - if requirement: - context['required_model'] = requirement - return render(request, self.template_name, context) @@ -235,17 +233,6 @@ class BulkCreateView(GetReturnURLMixin, BaseMultiObjectView): form = self.form() model_form = self.model_form(initial=initial) - context = { - 'obj_type': self.model_form._meta.model._meta.verbose_name, - 'form': form, - 'model_form': model_form, - 'return_url': self.get_return_url(request), - **self.get_extra_context(request), - } - - if requirement: - context['required_model'] = requirement - return render(request, self.template_name, { 'obj_type': self.model_form._meta.model._meta.verbose_name, 'form': form, diff --git a/netbox/netbox/views/generic/object_views.py b/netbox/netbox/views/generic/object_views.py index 7c63b2ec6..c9ff738d7 100644 --- a/netbox/netbox/views/generic/object_views.py +++ b/netbox/netbox/views/generic/object_views.py @@ -341,24 +341,21 @@ 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) - context = { + requirement = get_prerequisite_model(self.queryset) + return render(request, self.template_name, { + 'model': model, 'object': obj, 'form': form, 'return_url': self.get_return_url(request, obj), + 'prerequisite_model': requirement if requirement else None, **self.get_extra_context(request, obj), - } - - requirement = get_prerequisite_model(self.queryset) - if requirement: - context['required_model'] = requirement - context['model'] = self.queryset.model - - return render(request, self.template_name, context) + }) def post(self, request, *args, **kwargs): """ diff --git a/netbox/netbox/views/generic/utils.py b/netbox/netbox/views/generic/utils.py index c682181a1..61c6dc242 100644 --- a/netbox/netbox/views/generic/utils.py +++ b/netbox/netbox/views/generic/utils.py @@ -1,12 +1,12 @@ def get_prerequisite_model(queryset): model = queryset.model - if not queryset.count(): + 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.count(): + if not prereq.objects.exists(): return prereq return None diff --git a/netbox/templates/generic/object_edit.html b/netbox/templates/generic/object_edit.html index 73e9727bb..8047dc59d 100644 --- a/netbox/templates/generic/object_edit.html +++ b/netbox/templates/generic/object_edit.html @@ -40,7 +40,7 @@ Context: {% endif %} - {% if required_model %} + {% if prerequisite_model %} {% include 'inc/missing_prerequisites.html' %} {% endif %} diff --git a/netbox/templates/generic/object_list.html b/netbox/templates/generic/object_list.html index 6910aa116..9d3952a28 100644 --- a/netbox/templates/generic/object_list.html +++ b/netbox/templates/generic/object_list.html @@ -101,7 +101,7 @@ Context: {# Object table #} - {% if required_model %} + {% if prerequisite_model %} {% include 'inc/missing_prerequisites.html' %} {% endif %} diff --git a/netbox/templates/inc/missing_prerequisites.html b/netbox/templates/inc/missing_prerequisites.html index 04043fc9c..5814b72eb 100644 --- a/netbox/templates/inc/missing_prerequisites.html +++ b/netbox/templates/inc/missing_prerequisites.html @@ -1,6 +1,6 @@ {% load buttons %} From 25ec624e4e97026088950e213bc5ec6dcd1d0bff Mon Sep 17 00:00:00 2001 From: Arthur Date: Mon, 22 Aug 2022 12:59:26 -0700 Subject: [PATCH 23/62] #6454 suggested review changes --- netbox/netbox/views/generic/bulk_views.py | 3 +-- netbox/netbox/views/generic/object_views.py | 3 +-- netbox/templates/generic/object_list.html | 1 - 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/netbox/netbox/views/generic/bulk_views.py b/netbox/netbox/views/generic/bulk_views.py index 8fe0ad518..7340ea2a0 100644 --- a/netbox/netbox/views/generic/bulk_views.py +++ b/netbox/netbox/views/generic/bulk_views.py @@ -124,7 +124,6 @@ class ObjectListView(BaseMultiObjectView, ActionsMixin, TableMixin): """ model = self.queryset.model content_type = ContentType.objects.get_for_model(model) - requirement = get_prerequisite_model(self.queryset) if self.filterset: self.queryset = self.filterset(request.GET, self.queryset).qs @@ -172,7 +171,7 @@ class ObjectListView(BaseMultiObjectView, ActionsMixin, TableMixin): 'table': table, 'actions': actions, 'filter_form': self.filterset_form(request.GET, label_suffix='') if self.filterset_form else None, - 'prerequisite_model': requirement if requirement else None, + 'prerequisite_model': get_prerequisite_model(self.queryset), **self.get_extra_context(request), } diff --git a/netbox/netbox/views/generic/object_views.py b/netbox/netbox/views/generic/object_views.py index c9ff738d7..19401f79a 100644 --- a/netbox/netbox/views/generic/object_views.py +++ b/netbox/netbox/views/generic/object_views.py @@ -347,13 +347,12 @@ class ObjectEditView(GetReturnURLMixin, BaseObjectView): form = self.form(instance=obj, initial=initial_data) restrict_form_fields(form, request.user) - requirement = get_prerequisite_model(self.queryset) return render(request, self.template_name, { 'model': model, 'object': obj, 'form': form, 'return_url': self.get_return_url(request, obj), - 'prerequisite_model': requirement if requirement else None, + 'prerequisite_model': get_prerequisite_model(self.queryset), **self.get_extra_context(request, obj), }) diff --git a/netbox/templates/generic/object_list.html b/netbox/templates/generic/object_list.html index 9d3952a28..60eba6097 100644 --- a/netbox/templates/generic/object_list.html +++ b/netbox/templates/generic/object_list.html @@ -105,7 +105,6 @@ Context: {% include 'inc/missing_prerequisites.html' %} {% endif %} -
{% include 'htmx/table.html' %} From 069c2d2fd2f3c2a81b882c12cc62a55ed857b33b Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 22 Aug 2022 16:11:35 -0400 Subject: [PATCH 24/62] Changelog for #6454, #10057, #10059 --- docs/release-notes/version-3.3.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/release-notes/version-3.3.md b/docs/release-notes/version-3.3.md index 2651948a5..75eb7208d 100644 --- a/docs/release-notes/version-3.3.md +++ b/docs/release-notes/version-3.3.md @@ -4,6 +4,7 @@ ### Enhancements +* [#6454](https://github.com/netbox-community/netbox/issues/6454) - Include contextual help when creating first objects in UI * [#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 @@ -11,6 +12,8 @@ * [#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 +* [#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 * [#10089](https://github.com/netbox-community/netbox/issues/10089) - `linkify` template filter should escape object representation --- From 71bf5f4697ebbd00d2ac87e7972961f5f6289851 Mon Sep 17 00:00:00 2001 From: atownson <52260120+atownson@users.noreply.github.com> Date: Mon, 22 Aug 2022 15:17:35 -0500 Subject: [PATCH 25/62] Updated exists evaluation --- netbox/templates/dcim/device.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/netbox/templates/dcim/device.html b/netbox/templates/dcim/device.html index 217362311..a798a34b0 100644 --- a/netbox/templates/dcim/device.html +++ b/netbox/templates/dcim/device.html @@ -179,7 +179,7 @@ {{ object.primary_ip4.address.ip }} {% if object.primary_ip4.nat_inside %} (NAT for {{ object.primary_ip4.nat_inside.address.ip|linkify }}) - {% elif object.primary_ip4.nat_outside.count > 0 %} + {% elif object.primary_ip4.nat_outside.exists %} (NAT for {% for nat in object.primary_ip4.nat_outside.all %}{{ nat.address.ip }}{% if not forloop.last %}, {% endif %}{% endfor %}) {% endif %} {% else %} @@ -194,7 +194,7 @@ {{ object.primary_ip6.address.ip }} {% if object.primary_ip6.nat_inside %} (NAT for {{ object.primary_ip6.nat_inside.address.ip|linkify }}) - {% elif object.primary_ip6.nat_outside.count > 0 %} + {% elif object.primary_ip6.nat_outside.exists %} (NAT for {% for nat in object.primary_ip6.nat_outside.all %}{{ nat.address.ip }}{% if not forloop.last %}, {% endif %}{% endfor %}) {% endif %} {% else %} From 2bb79e1346bdae419497176619edfe8942df1231 Mon Sep 17 00:00:00 2001 From: atownson <52260120+atownson@users.noreply.github.com> Date: Mon, 22 Aug 2022 15:18:25 -0500 Subject: [PATCH 26/62] Updated exists evaluation --- netbox/templates/virtualization/virtualmachine.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/netbox/templates/virtualization/virtualmachine.html b/netbox/templates/virtualization/virtualmachine.html index 3826e0cf2..8b69374f1 100644 --- a/netbox/templates/virtualization/virtualmachine.html +++ b/netbox/templates/virtualization/virtualmachine.html @@ -45,7 +45,7 @@ {{ object.primary_ip4.address.ip }} {% if object.primary_ip4.nat_inside %} (NAT for {{ object.primary_ip4.nat_inside.address.ip }}) - {% elif object.primary_ip4.nat_outside.count > 0 %} + {% elif object.primary_ip4.nat_outside.exists %} (NAT for {% for nat in object.primary_ip4.nat_outside.all %}{{ nat.address.ip }}{% if not forloop.last %}, {% endif %}{% endfor %}) {% endif %} {% else %} @@ -60,7 +60,7 @@ {{ object.primary_ip6.address.ip }} {% if object.primary_ip6.nat_inside %} (NAT for {{ object.primary_ip6.nat_inside.address.ip }}) - {% elif object.primary_ip4.nat_outside.count > 0 %} + {% elif object.primary_ip4.nat_outside.exists %} (NAT for {% for nat in object.primary_ip4.nat_outside.all %}{{ nat.address.ip }}{% if not forloop.last %}, {% endif %}{% endfor %}) {% endif %} {% else %} From 6179686c81c5a33fe264046214e4b9a005b94a42 Mon Sep 17 00:00:00 2001 From: atownson <52260120+atownson@users.noreply.github.com> Date: Mon, 22 Aug 2022 15:22:53 -0500 Subject: [PATCH 27/62] Corrected IPv6 family --- netbox/templates/virtualization/virtualmachine.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/netbox/templates/virtualization/virtualmachine.html b/netbox/templates/virtualization/virtualmachine.html index 8b69374f1..5756d939a 100644 --- a/netbox/templates/virtualization/virtualmachine.html +++ b/netbox/templates/virtualization/virtualmachine.html @@ -60,8 +60,8 @@ {{ object.primary_ip6.address.ip }} {% if object.primary_ip6.nat_inside %} (NAT for {{ object.primary_ip6.nat_inside.address.ip }}) - {% elif object.primary_ip4.nat_outside.exists %} - (NAT for {% for nat in object.primary_ip4.nat_outside.all %}{{ nat.address.ip }}{% if not forloop.last %}, {% endif %}{% endfor %}) + {% elif object.primary_ip6.nat_outside.exists %} + (NAT for {% for nat in object.primary_ip6.nat_outside.all %}{{ nat.address.ip }}{% if not forloop.last %}, {% endif %}{% endfor %}) {% endif %} {% else %} {{ ''|placeholder }} From 0c7c61b685560cfa886ff10fbb18853a27ad7587 Mon Sep 17 00:00:00 2001 From: Arthur Date: Mon, 22 Aug 2022 14:56:31 -0700 Subject: [PATCH 28/62] #10037 add Child Interface to context menu --- netbox/dcim/tables/template_code.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/netbox/dcim/tables/template_code.py b/netbox/dcim/tables/template_code.py index 3403f9392..62a189b63 100644 --- a/netbox/dcim/tables/template_code.py +++ b/netbox/dcim/tables/template_code.py @@ -238,6 +238,9 @@ INTERFACE_BUTTONS = """ {% if perms.dcim.add_inventoryitem %}
  • Inventory Item
  • {% endif %} + {% if perms.dcim.add_interface %} +
  • Child Interface
  • + {% endif %} {% endif %} From 9fddd193b92b4cb5b035502a9f8d5dc60fadf49d Mon Sep 17 00:00:00 2001 From: Arthur Date: Mon, 22 Aug 2022 16:31:41 -0700 Subject: [PATCH 29/62] #10094 fix Contact AddAnother --- netbox/netbox/views/generic/object_views.py | 4 ++++ netbox/tenancy/views.py | 10 ++++++++++ 2 files changed, 14 insertions(+) diff --git a/netbox/netbox/views/generic/object_views.py b/netbox/netbox/views/generic/object_views.py index 5ff0cfdff..433e70b63 100644 --- a/netbox/netbox/views/generic/object_views.py +++ b/netbox/netbox/views/generic/object_views.py @@ -327,6 +327,9 @@ class ObjectEditView(GetReturnURLMixin, BaseObjectView): """ return obj + def get_extra_addanother_params(self, request, params: dict): + return params + # # Request handlers # @@ -399,6 +402,7 @@ class ObjectEditView(GetReturnURLMixin, BaseObjectView): # If cloning is supported, pre-populate a new instance of the form params = prepare_cloned_fields(obj) + params = self.get_extra_addanother_params(request, params) if params: if 'return_url' in request.GET: params['return_url'] = request.GET.get('return_url') diff --git a/netbox/tenancy/views.py b/netbox/tenancy/views.py index 9a2fe6ab9..8b0f90f88 100644 --- a/netbox/tenancy/views.py +++ b/netbox/tenancy/views.py @@ -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,15 @@ 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, params: dict): + if not params: + params = QueryDict(mutable=True) + + params['content_type'] = request.GET.get('content_type') + params['object_id'] = request.GET.get('object_id') + + return params + class ContactAssignmentDeleteView(generic.ObjectDeleteView): queryset = ContactAssignment.objects.all() From 41499b189c29bf9c7b07b3d7601637312b13d141 Mon Sep 17 00:00:00 2001 From: Arthur Date: Mon, 22 Aug 2022 16:33:50 -0700 Subject: [PATCH 30/62] #10094 fix Contact AddAnother --- netbox/utilities/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/utilities/utils.py b/netbox/utilities/utils.py index 1dece76c8..69ab615fc 100644 --- a/netbox/utilities/utils.py +++ b/netbox/utilities/utils.py @@ -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 From f48aaf1c465d0048606f486d9530631a33920a65 Mon Sep 17 00:00:00 2001 From: Arthur Date: Mon, 22 Aug 2022 16:47:40 -0700 Subject: [PATCH 31/62] #10094 fix Contact AddAnother --- netbox/netbox/views/generic/object_views.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/netbox/netbox/views/generic/object_views.py b/netbox/netbox/views/generic/object_views.py index 433e70b63..89f52e475 100644 --- a/netbox/netbox/views/generic/object_views.py +++ b/netbox/netbox/views/generic/object_views.py @@ -327,7 +327,10 @@ class ObjectEditView(GetReturnURLMixin, BaseObjectView): """ return obj - def get_extra_addanother_params(self, request, params: dict): + def get_extra_addanother_params(self, request, params): + """ + Return a QueryDict of extra params to use on the Add Another button. + """ return params # From 63e8faeed931f9ed2d589c5341976fa1b4f82324 Mon Sep 17 00:00:00 2001 From: atownson <52260120+atownson@users.noreply.github.com> Date: Mon, 22 Aug 2022 20:34:44 -0500 Subject: [PATCH 32/62] Changed nat_outside to ManyToManyColumn --- netbox/ipam/tables/ip.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/netbox/ipam/tables/ip.py b/netbox/ipam/tables/ip.py index 52b1c4393..82f4686c0 100644 --- a/netbox/ipam/tables/ip.py +++ b/netbox/ipam/tables/ip.py @@ -56,14 +56,6 @@ VRF_LINK = """ {% endif %} """ -NAT_OUTSIDE_LINK = """ -{% if record.nat_outside.count > 0 %} - {% for nat in record.nat_outside.all %}{{ nat.address.ip }}{% if not forloop.last %}, {% endif %}{% endfor %} -{% else %} - — -{% endif %} -""" - # # RIRs @@ -368,8 +360,8 @@ class IPAddressTable(TenancyColumnsMixin, NetBoxTable): orderable=False, verbose_name='NAT (Inside)' ) - nat_outside = tables.TemplateColumn( - template_code=NAT_OUTSIDE_LINK, + nat_outside = tables.ManyToManyColumn( + linkify_item=True, orderable=False, verbose_name='NAT (Outside)' ) From 984d8b8ee6adc6a6e1b7713a133af312d67b883d Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 23 Aug 2022 09:17:12 -0400 Subject: [PATCH 33/62] Fixes #10108: Linkify inside NAT IPs for primary device IPs in UI --- docs/release-notes/version-3.3.md | 2 ++ netbox/templates/dcim/device.html | 8 +++----- netbox/templates/ipam/ipaddress.html | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/release-notes/version-3.3.md b/docs/release-notes/version-3.3.md index 75eb7208d..33fce0a3c 100644 --- a/docs/release-notes/version-3.3.md +++ b/docs/release-notes/version-3.3.md @@ -12,9 +12,11 @@ * [#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 * [#10089](https://github.com/netbox-community/netbox/issues/10089) - `linkify` template filter should escape object representation +* [#10108](https://github.com/netbox-community/netbox/issues/10108) - Linkify inside NAT IPs for primary device IPs in UI --- diff --git a/netbox/templates/dcim/device.html b/netbox/templates/dcim/device.html index a798a34b0..2df2407b5 100644 --- a/netbox/templates/dcim/device.html +++ b/netbox/templates/dcim/device.html @@ -155,9 +155,7 @@
    -
    - Management -
    +
    Management
    + {{ field }} - + {% customfield_value field value %}
    @@ -178,7 +176,7 @@ {% if object.primary_ip4 %} {{ object.primary_ip4.address.ip }} {% if object.primary_ip4.nat_inside %} - (NAT for {{ object.primary_ip4.nat_inside.address.ip|linkify }}) + (NAT for {{ object.primary_ip4.nat_inside.address.ip }}) {% elif object.primary_ip4.nat_outside.exists %} (NAT for {% for nat in object.primary_ip4.nat_outside.all %}{{ nat.address.ip }}{% if not forloop.last %}, {% endif %}{% endfor %}) {% endif %} @@ -193,7 +191,7 @@ {% if object.primary_ip6 %} {{ object.primary_ip6.address.ip }} {% if object.primary_ip6.nat_inside %} - (NAT for {{ object.primary_ip6.nat_inside.address.ip|linkify }}) + (NAT for {{ object.primary_ip6.nat_inside.address.ip }}) {% elif object.primary_ip6.nat_outside.exists %} (NAT for {% for nat in object.primary_ip6.nat_outside.all %}{{ nat.address.ip }}{% if not forloop.last %}, {% endif %}{% endfor %}) {% endif %} diff --git a/netbox/templates/ipam/ipaddress.html b/netbox/templates/ipam/ipaddress.html index ba0f0c5e6..7f77e8137 100644 --- a/netbox/templates/ipam/ipaddress.html +++ b/netbox/templates/ipam/ipaddress.html @@ -95,9 +95,9 @@ - {% if iface.connected_endpoint.device %} - - - {% elif iface.connected_endpoint.circuit %} - {% with circuit=iface.connected_endpoint.circuit %} - - {% endwith %} - {% else %} - - {% endif %} + {% with peer=iface.connected_endpoints.0 %} + {% if peer.device %} + + + {% elif peer.circuit %} + {% with circuit=peer.circuit %} + + {% endwith %} + {% else %} + + {% endif %} + {% endwith %} From ed4fe6bd36eb917661212b31e18b9dfb332d35d1 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Thu, 25 Aug 2022 16:07:34 -0400 Subject: [PATCH 62/62] Release v3.3.1 --- .github/ISSUE_TEMPLATE/bug_report.yaml | 2 +- .github/ISSUE_TEMPLATE/feature_request.yaml | 2 +- docs/release-notes/version-3.3.md | 2 +- netbox/netbox/settings.py | 2 +- requirements.txt | 4 ++-- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index 3844b6753..1d945f72f 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -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 diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml index 3c7638f42..809246a38 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yaml +++ b/.github/ISSUE_TEMPLATE/feature_request.yaml @@ -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 diff --git a/docs/release-notes/version-3.3.md b/docs/release-notes/version-3.3.md index 0c84c98bb..f5c02de82 100644 --- a/docs/release-notes/version-3.3.md +++ b/docs/release-notes/version-3.3.md @@ -1,6 +1,6 @@ # NetBox v3.3 -## v3.3.1 (FUTURE) +## v3.3.1 (2022-08-25) ### Enhancements diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 4438d338b..26c91c300 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -29,7 +29,7 @@ django.utils.encoding.force_text = force_str # Environment setup # -VERSION = '3.3.1-dev' +VERSION = '3.3.1' # Hostname HOSTNAME = platform.node() diff --git a/requirements.txt b/requirements.txt index ebe5c3b8b..ce4acdbe9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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
    {% for ip in object.nat_outside.all %} {{ ip|linkify }} - {% if ip.assigned_object %} - ({{ ip.assigned_object.parent_object|linkify }}) - {% endif %}
    + {% if ip.assigned_object %} + ({{ ip.assigned_object.parent_object|linkify }}) + {% endif %}
    {% empty %} {{ ''|placeholder }} {% endfor %} From f3906dd7c43c10be4b44ab8a755f42c83a55660d Mon Sep 17 00:00:00 2001 From: Daniel Sheppard Date: Tue, 23 Aug 2022 09:33:36 -0500 Subject: [PATCH 34/62] Fixes #10111 - Wrap search QS to catch ValueError on identifier field --- docs/release-notes/version-3.3.md | 1 + netbox/ipam/filtersets.py | 12 +++++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/version-3.3.md b/docs/release-notes/version-3.3.md index 33fce0a3c..16a62b8cd 100644 --- a/docs/release-notes/version-3.3.md +++ b/docs/release-notes/version-3.3.md @@ -17,6 +17,7 @@ * [#10059](https://github.com/netbox-community/netbox/issues/10059) - Add identifier column to L2VPN table * [#10089](https://github.com/netbox-community/netbox/issues/10089) - `linkify` template filter should escape object representation * [#10108](https://github.com/netbox-community/netbox/issues/10108) - Linkify inside NAT IPs for primary device IPs in UI +* [#10111](https://github.com/netbox-community/netbox/issues/10111) - Wrap search QS to catch ValueError on identifier field --- diff --git a/netbox/ipam/filtersets.py b/netbox/ipam/filtersets.py index 49ec15fc1..3c0ab1ac8 100644 --- a/netbox/ipam/filtersets.py +++ b/netbox/ipam/filtersets.py @@ -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( From 7ba0b420f181cffac86a9de384a1ec9d8a9a07dd Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 23 Aug 2022 10:32:21 -0400 Subject: [PATCH 35/62] Fixes #10109: Fix available prefixes calculation for container prefixes in the global table --- docs/release-notes/version-3.3.md | 1 + netbox/ipam/models/ip.py | 13 ++++++++----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/docs/release-notes/version-3.3.md b/docs/release-notes/version-3.3.md index 16a62b8cd..7ed635bc2 100644 --- a/docs/release-notes/version-3.3.md +++ b/docs/release-notes/version-3.3.md @@ -17,6 +17,7 @@ * [#10059](https://github.com/netbox-community/netbox/issues/10059) - Add identifier column to L2VPN table * [#10089](https://github.com/netbox-community/netbox/issues/10089) - `linkify` template filter should escape object representation * [#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) - Wrap search QS to catch ValueError on identifier field --- diff --git a/netbox/ipam/models/ip.py b/netbox/ipam/models/ip.py index 2d3f4d291..456bab4f0 100644 --- a/netbox/ipam/models/ip.py +++ b/netbox/ipam/models/ip.py @@ -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): """ From c11ca543e2f31723da7ce29c7062a4f99339398f Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 23 Aug 2022 09:16:48 -0700 Subject: [PATCH 36/62] #10037 default type to virtual --- netbox/dcim/tables/template_code.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/dcim/tables/template_code.py b/netbox/dcim/tables/template_code.py index 62a189b63..8c23f327c 100644 --- a/netbox/dcim/tables/template_code.py +++ b/netbox/dcim/tables/template_code.py @@ -239,7 +239,7 @@ INTERFACE_BUTTONS = """
  • Inventory Item
  • {% endif %} {% if perms.dcim.add_interface %} -
  • Child Interface
  • +
  • Child Interface
  • {% endif %} From 8b1a462a6070cb6054af8bb59589c9a2e785afc2 Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 23 Aug 2022 09:29:55 -0700 Subject: [PATCH 37/62] #10094 changes from code review --- netbox/netbox/views/generic/object_views.py | 6 +++--- netbox/tenancy/views.py | 13 +++++-------- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/netbox/netbox/views/generic/object_views.py b/netbox/netbox/views/generic/object_views.py index 89f52e475..6ef88bb2f 100644 --- a/netbox/netbox/views/generic/object_views.py +++ b/netbox/netbox/views/generic/object_views.py @@ -329,9 +329,9 @@ class ObjectEditView(GetReturnURLMixin, BaseObjectView): def get_extra_addanother_params(self, request, params): """ - Return a QueryDict of extra params to use on the Add Another button. + Return a dictionary of extra parameters to use on the Add Another button. """ - return params + return {} # # Request handlers @@ -405,7 +405,7 @@ class ObjectEditView(GetReturnURLMixin, BaseObjectView): # If cloning is supported, pre-populate a new instance of the form params = prepare_cloned_fields(obj) - params = self.get_extra_addanother_params(request, params) + params.update(self.get_extra_addanother_params(request)) if params: if 'return_url' in request.GET: params['return_url'] = request.GET.get('return_url') diff --git a/netbox/tenancy/views.py b/netbox/tenancy/views.py index 8b0f90f88..e582c15d1 100644 --- a/netbox/tenancy/views.py +++ b/netbox/tenancy/views.py @@ -366,14 +366,11 @@ 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, params: dict): - if not params: - params = QueryDict(mutable=True) - - params['content_type'] = request.GET.get('content_type') - params['object_id'] = request.GET.get('object_id') - - return params + 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): From 374abe52149c5804c605a781aee73b9542f6ac36 Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 23 Aug 2022 10:34:06 -0700 Subject: [PATCH 38/62] #10033 disable Add a Termination button if 2 terminations on L2VPN P2P --- netbox/ipam/models/l2vpn.py | 6 ++++++ netbox/templates/ipam/l2vpn.html | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/netbox/ipam/models/l2vpn.py b/netbox/ipam/models/l2vpn.py index 0e948b18e..db6f47924 100644 --- a/netbox/ipam/models/l2vpn.py +++ b/netbox/ipam/models/l2vpn.py @@ -67,6 +67,12 @@ class L2VPN(NetBoxModel): def get_absolute_url(self): return reverse('ipam:l2vpn', args=[self.pk]) + 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( diff --git a/netbox/templates/ipam/l2vpn.html b/netbox/templates/ipam/l2vpn.html index 44a1da818..32013400b 100644 --- a/netbox/templates/ipam/l2vpn.html +++ b/netbox/templates/ipam/l2vpn.html @@ -59,7 +59,7 @@ {% if perms.ipam.add_l2vpntermination %} From 439cf1a30874bffce105e0d0dea05b416f603487 Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 23 Aug 2022 16:17:40 -0700 Subject: [PATCH 39/62] #10033 changes from code review --- netbox/ipam/models/l2vpn.py | 2 ++ netbox/templates/ipam/l2vpn.html | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/netbox/ipam/models/l2vpn.py b/netbox/ipam/models/l2vpn.py index db6f47924..ab29ab048 100644 --- a/netbox/ipam/models/l2vpn.py +++ b/netbox/ipam/models/l2vpn.py @@ -3,6 +3,7 @@ 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 @@ -67,6 +68,7 @@ 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 diff --git a/netbox/templates/ipam/l2vpn.html b/netbox/templates/ipam/l2vpn.html index 32013400b..c19363d33 100644 --- a/netbox/templates/ipam/l2vpn.html +++ b/netbox/templates/ipam/l2vpn.html @@ -59,7 +59,7 @@ {% if perms.ipam.add_l2vpntermination %} From 1c46102c4a672079e4e61f33d5405ccb505fe54f Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 23 Aug 2022 16:19:43 -0700 Subject: [PATCH 40/62] #10094 changes from code review --- netbox/netbox/views/generic/object_views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/netbox/views/generic/object_views.py b/netbox/netbox/views/generic/object_views.py index 6ef88bb2f..ece299f21 100644 --- a/netbox/netbox/views/generic/object_views.py +++ b/netbox/netbox/views/generic/object_views.py @@ -327,7 +327,7 @@ class ObjectEditView(GetReturnURLMixin, BaseObjectView): """ return obj - def get_extra_addanother_params(self, request, params): + def get_extra_addanother_params(self, request): """ Return a dictionary of extra parameters to use on the Add Another button. """ From 18d5576997d029f3c67c323e3e88912e08d1f542 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 24 Aug 2022 08:59:40 -0400 Subject: [PATCH 41/62] Changelog for #10033, #10037, #10094 --- docs/release-notes/version-3.3.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/release-notes/version-3.3.md b/docs/release-notes/version-3.3.md index 7ed635bc2..4d7c482e5 100644 --- a/docs/release-notes/version-3.3.md +++ b/docs/release-notes/version-3.3.md @@ -5,6 +5,8 @@ ### Enhancements * [#6454](https://github.com/netbox-community/netbox/issues/6454) - Include contextual help when creating first objects in UI +* [#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 link to create child interface to interface context menu * [#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 @@ -16,6 +18,7 @@ * [#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 * [#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) - Wrap search QS to catch ValueError on identifier field From 36729fb6aec9ee856945a1ac5d48b75df8aa7178 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 24 Aug 2022 13:08:21 -0400 Subject: [PATCH 42/62] Fixes #10134: Custom fields data serializer should return a 400 response for invalid data --- docs/release-notes/version-3.3.md | 1 + netbox/extras/api/customfields.py | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/docs/release-notes/version-3.3.md b/docs/release-notes/version-3.3.md index 4d7c482e5..e23438478 100644 --- a/docs/release-notes/version-3.3.md +++ b/docs/release-notes/version-3.3.md @@ -22,6 +22,7 @@ * [#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) - Wrap search QS to catch ValueError on identifier field +* [#10134](https://github.com/netbox-community/netbox/issues/10134) - Custom fields data serializer should return a 400 response for invalid data --- diff --git a/netbox/extras/api/customfields.py b/netbox/extras/api/customfields.py index b7fd1e129..cb35b4e73 100644 --- a/netbox/extras/api/customfields.py +++ b/netbox/extras/api/customfields.py @@ -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} From c2c8bd0a761dde32bc2438abf6c05fd27fab58c3 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 24 Aug 2022 13:25:54 -0400 Subject: [PATCH 43/62] Closes #10133: Enable nullifying device location during bulk edit --- docs/release-notes/version-3.3.md | 1 + netbox/dcim/forms/bulk_edit.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/version-3.3.md b/docs/release-notes/version-3.3.md index e23438478..5c4c8c654 100644 --- a/docs/release-notes/version-3.3.md +++ b/docs/release-notes/version-3.3.md @@ -9,6 +9,7 @@ * [#10037](https://github.com/netbox-community/netbox/issues/10037) - Add link to create child interface to interface context menu * [#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 diff --git a/netbox/dcim/forms/bulk_edit.py b/netbox/dcim/forms/bulk_edit.py index 8f765ae9b..396f7e59b 100644 --- a/netbox/dcim/forms/bulk_edit.py +++ b/netbox/dcim/forms/bulk_edit.py @@ -480,7 +480,7 @@ class DeviceBulkEditForm(NetBoxModelBulkEditForm): ('Hardware', ('manufacturer', 'device_type', 'airflow', 'serial')), ) nullable_fields = ( - 'tenant', 'platform', 'serial', 'airflow', + 'location', 'tenant', 'platform', 'serial', 'airflow', ) From 2baf06e012032ea0915a32df13d953acc5bf8500 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 24 Aug 2022 14:46:42 -0400 Subject: [PATCH 44/62] Add unique slugs to L2VPNs in relevant tests --- netbox/ipam/tests/test_api.py | 6 +++--- netbox/ipam/tests/test_filtersets.py | 6 +++--- netbox/ipam/tests/test_models.py | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/netbox/ipam/tests/test_api.py b/netbox/ipam/tests/test_api.py index 3fef04194..4c07e0a90 100644 --- a/netbox/ipam/tests/test_api.py +++ b/netbox/ipam/tests/test_api.py @@ -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) diff --git a/netbox/ipam/tests/test_filtersets.py b/netbox/ipam/tests/test_filtersets.py index 081f6e11d..5c4113786 100644 --- a/netbox/ipam/tests/test_filtersets.py +++ b/netbox/ipam/tests/test_filtersets.py @@ -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]) diff --git a/netbox/ipam/tests/test_models.py b/netbox/ipam/tests/test_models.py index 3bd7e8ccb..94a315be5 100644 --- a/netbox/ipam/tests/test_models.py +++ b/netbox/ipam/tests/test_models.py @@ -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) From bfbf97aec9119539f7f42cf16f52d0ca8203ba60 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 24 Aug 2022 15:49:36 -0400 Subject: [PATCH 45/62] Closes #10031: Enforce 'application/json' content type for REST API requests --- docs/release-notes/version-3.3.md | 1 + netbox/ipam/tests/test_api.py | 4 ++-- netbox/netbox/settings.py | 4 +++- netbox/users/tests/test_api.py | 4 ++-- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/docs/release-notes/version-3.3.md b/docs/release-notes/version-3.3.md index 5c4c8c654..765944950 100644 --- a/docs/release-notes/version-3.3.md +++ b/docs/release-notes/version-3.3.md @@ -5,6 +5,7 @@ ### Enhancements * [#6454](https://github.com/netbox-community/netbox/issues/6454) - Include contextual help when creating first objects in UI +* [#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 link to create child interface to interface context menu * [#10061](https://github.com/netbox-community/netbox/issues/10061) - Replicate type when cloning L2VPN instances diff --git a/netbox/ipam/tests/test_api.py b/netbox/ipam/tests/test_api.py index 4c07e0a90..5dc708cd0 100644 --- a/netbox/ipam/tests/test_api.py +++ b/netbox/ipam/tests/test_api.py @@ -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) diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 0edce8f69..4438d338b 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -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', diff --git a/netbox/users/tests/test_api.py b/netbox/users/tests/test_api.py index bcfc9cf14..a0bf8a49e 100644 --- a/netbox/users/tests/test_api.py +++ b/netbox/users/tests/test_api.py @@ -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) From eb2bf3469ec2b28e687c2dbfb35191bd256f80bb Mon Sep 17 00:00:00 2001 From: Arthur Hanson Date: Wed, 24 Aug 2022 13:36:38 -0700 Subject: [PATCH 46/62] #9935 add new wireless choices for interfaces (#10116) * #9935 add new wireless choices for interfaces * #9935 add new wireless interfaces to constants * #9935 oops - remove login.html changes --- netbox/dcim/choices.py | 4 +++ netbox/dcim/constants.py | 3 +++ ...alter_wirelesslink_interface_a_and_more.py | 25 +++++++++++++++++++ 3 files changed, 32 insertions(+) create mode 100644 netbox/wireless/migrations/0005_alter_wirelesslink_interface_a_and_more.py diff --git a/netbox/dcim/choices.py b/netbox/dcim/choices.py index 79049384a..019ae09a4 100644 --- a/netbox/dcim/choices.py +++ b/netbox/dcim/choices.py @@ -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)'), ) ), ( diff --git a/netbox/dcim/constants.py b/netbox/dcim/constants.py index 9e41ed113..80d7558c9 100644 --- a/netbox/dcim/constants.py +++ b/netbox/dcim/constants.py @@ -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 diff --git a/netbox/wireless/migrations/0005_alter_wirelesslink_interface_a_and_more.py b/netbox/wireless/migrations/0005_alter_wirelesslink_interface_a_and_more.py new file mode 100644 index 000000000..64e375e43 --- /dev/null +++ b/netbox/wireless/migrations/0005_alter_wirelesslink_interface_a_and_more.py @@ -0,0 +1,25 @@ +# Generated by Django 4.0.7 on 2022-08-24 17:18 + +from django.db import migrations, models +import django.db.models.deletion + + +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={'type__in': ['ieee802.11a', 'ieee802.11g', 'ieee802.11n', 'ieee802.11ac', 'ieee802.11ad', 'ieee802.11ax', 'ieee802.11ay', 'ieee802.15.1', 'other-wireless']}, 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={'type__in': ['ieee802.11a', 'ieee802.11g', 'ieee802.11n', 'ieee802.11ac', 'ieee802.11ad', 'ieee802.11ax', 'ieee802.11ay', 'ieee802.15.1', 'other-wireless']}, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='dcim.interface'), + ), + ] From f70ef7a585d142813850c14853ff2cba122cb31f Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 24 Aug 2022 16:44:24 -0400 Subject: [PATCH 47/62] Changelog and cleanup for #9935 --- docs/release-notes/version-3.3.md | 1 + ...alter_wirelesslink_interface_a_and_more.py | 25 ------------------- .../0005_wirelesslink_interface_types.py | 24 ++++++++++++++++++ netbox/wireless/models.py | 10 ++++++-- 4 files changed, 33 insertions(+), 27 deletions(-) delete mode 100644 netbox/wireless/migrations/0005_alter_wirelesslink_interface_a_and_more.py create mode 100644 netbox/wireless/migrations/0005_wirelesslink_interface_types.py diff --git a/docs/release-notes/version-3.3.md b/docs/release-notes/version-3.3.md index 765944950..1421bb2c7 100644 --- a/docs/release-notes/version-3.3.md +++ b/docs/release-notes/version-3.3.md @@ -5,6 +5,7 @@ ### 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 link to create child interface to interface context menu diff --git a/netbox/wireless/migrations/0005_alter_wirelesslink_interface_a_and_more.py b/netbox/wireless/migrations/0005_alter_wirelesslink_interface_a_and_more.py deleted file mode 100644 index 64e375e43..000000000 --- a/netbox/wireless/migrations/0005_alter_wirelesslink_interface_a_and_more.py +++ /dev/null @@ -1,25 +0,0 @@ -# Generated by Django 4.0.7 on 2022-08-24 17:18 - -from django.db import migrations, models -import django.db.models.deletion - - -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={'type__in': ['ieee802.11a', 'ieee802.11g', 'ieee802.11n', 'ieee802.11ac', 'ieee802.11ad', 'ieee802.11ax', 'ieee802.11ay', 'ieee802.15.1', 'other-wireless']}, 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={'type__in': ['ieee802.11a', 'ieee802.11g', 'ieee802.11n', 'ieee802.11ac', 'ieee802.11ad', 'ieee802.11ax', 'ieee802.11ay', 'ieee802.15.1', 'other-wireless']}, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='dcim.interface'), - ), - ] diff --git a/netbox/wireless/migrations/0005_wirelesslink_interface_types.py b/netbox/wireless/migrations/0005_wirelesslink_interface_types.py new file mode 100644 index 000000000..0b3f88c5b --- /dev/null +++ b/netbox/wireless/migrations/0005_wirelesslink_interface_types.py @@ -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'), + ), + ] diff --git a/netbox/wireless/models.py b/netbox/wireless/models.py index d8166fe9a..c383ad642 100644 --- a/netbox/wireless/models.py +++ b/netbox/wireless/models.py @@ -128,20 +128,26 @@ 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='+', 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='+', verbose_name="Interface B", From 4132027ada8344fd2125562ac4331b3bc1fdfcaa Mon Sep 17 00:00:00 2001 From: Arthur Hanson Date: Wed, 24 Aug 2022 14:12:14 -0700 Subject: [PATCH 48/62] fixes for #10070 make l2vpn slug unique (#10119) Co-authored-by: jeremystretch --- .../ipam/migrations/0060_alter_l2vpn_slug.py | 18 ++++++++++++++++++ netbox/ipam/models/l2vpn.py | 5 ++++- 2 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 netbox/ipam/migrations/0060_alter_l2vpn_slug.py diff --git a/netbox/ipam/migrations/0060_alter_l2vpn_slug.py b/netbox/ipam/migrations/0060_alter_l2vpn_slug.py new file mode 100644 index 000000000..9e70c2063 --- /dev/null +++ b/netbox/ipam/migrations/0060_alter_l2vpn_slug.py @@ -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), + ), + ] diff --git a/netbox/ipam/models/l2vpn.py b/netbox/ipam/models/l2vpn.py index 9718e9cab..a457f334b 100644 --- a/netbox/ipam/models/l2vpn.py +++ b/netbox/ipam/models/l2vpn.py @@ -21,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 From 6c686af1b70acdc56a8fdbe84451f7012e2b4197 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 24 Aug 2022 17:13:09 -0400 Subject: [PATCH 49/62] Changelog for #10070 --- docs/release-notes/version-3.3.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes/version-3.3.md b/docs/release-notes/version-3.3.md index 1421bb2c7..a1ac5b40f 100644 --- a/docs/release-notes/version-3.3.md +++ b/docs/release-notes/version-3.3.md @@ -20,6 +20,7 @@ * [#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 * [#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 From ec2e8ad18466f024ad6bb22c3412b6dd69ede4c5 Mon Sep 17 00:00:00 2001 From: Arthur Hanson Date: Thu, 25 Aug 2022 05:21:55 -0700 Subject: [PATCH 50/62] #10139 update development documents for githooks and web-ui (#10141) * #10139 update development documents for githooks and web-ui * Remove redudant phrase Co-authored-by: Jeremy Stretch --- docs/development/getting-started.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/development/getting-started.md b/docs/development/getting-started.md index 38d521de6..bac2b4dca 100644 --- a/docs/development/getting-started.md +++ b/docs/development/getting-started.md @@ -54,6 +54,12 @@ NetBox ships with a [git pre-commit hook](https://githooks.com/) script that aut 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: + +```no-highlight +python -m pip install pycodestyle +``` +...and setup the yarn packages as shown in the [Web UI Development Guide](web-ui.md) ### 3. Create a Python Virtual Environment @@ -118,6 +124,10 @@ This ensures that your development environment is now complete and operational. !!! 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 Once you have your development environment up and running, it might be helpful to populate some "dummy" data to make interacting with the UI and APIs more convenient. Check out the [netbox-demo-data](https://github.com/netbox-community/netbox-demo-data) repo on GitHub, which houses a collection of sample data that can be easily imported to any new NetBox deployment. (This sample data is used to populate the public demo instance at .) From 482b4b6e95ce4e32eba2ceb38b359fb66d65e4ee Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Thu, 25 Aug 2022 08:37:43 -0400 Subject: [PATCH 51/62] Fixes #10147: Permit the creation of 0U device types via REST API --- docs/release-notes/version-3.3.md | 1 + netbox/dcim/api/serializers.py | 2 +- netbox/dcim/tests/test_api.py | 3 +++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/version-3.3.md b/docs/release-notes/version-3.3.md index a1ac5b40f..6e8846e4a 100644 --- a/docs/release-notes/version-3.3.md +++ b/docs/release-notes/version-3.3.md @@ -27,6 +27,7 @@ * [#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) - Wrap search QS to catch ValueError on identifier field * [#10134](https://github.com/netbox-community/netbox/issues/10134) - Custom fields data serializer should return a 400 response for invalid data +* [#10147](https://github.com/netbox-community/netbox/issues/10147) - Permit the creation of 0U device types via REST API --- diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 249a3f167..af806acb8 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -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) diff --git a/netbox/dcim/tests/test_api.py b/netbox/dcim/tests/test_api.py index a78a98ae5..acd52178d 100644 --- a/netbox/dcim/tests/test_api.py +++ b/netbox/dcim/tests/test_api.py @@ -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, }, ] From 9da9a209a5e0935b4ba1460fbfffa1850f45e844 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 25 Aug 2022 08:46:19 -0400 Subject: [PATCH 52/62] Fixes #10087: Correct display of far end in console/power/interface connections tables (#10117) --- docs/release-notes/version-3.3.md | 1 + netbox/dcim/tables/__init__.py | 103 +----------------------------- netbox/dcim/tables/connections.py | 71 ++++++++++++++++++++ netbox/dcim/views.py | 6 +- 4 files changed, 76 insertions(+), 105 deletions(-) create mode 100644 netbox/dcim/tables/connections.py diff --git a/docs/release-notes/version-3.3.md b/docs/release-notes/version-3.3.md index 6e8846e4a..741fdc8d5 100644 --- a/docs/release-notes/version-3.3.md +++ b/docs/release-notes/version-3.3.md @@ -21,6 +21,7 @@ * [#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 diff --git a/netbox/dcim/tables/__init__.py b/netbox/dcim/tables/__init__.py index e3b2a42ba..843b612b1 100644 --- a/netbox/dcim/tables/__init__.py +++ b/netbox/dcim/tables/__init__.py @@ -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') diff --git a/netbox/dcim/tables/connections.py b/netbox/dcim/tables/connections.py new file mode 100644 index 000000000..f9f78f3a6 --- /dev/null +++ b/netbox/dcim/tables/connections.py @@ -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') diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index a31eabc5e..39b2340e1 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -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 From 5f37699736a892e59dbe8ee5a58aaf6e588be0ec Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Thu, 25 Aug 2022 10:34:18 -0400 Subject: [PATCH 53/62] Fixes #9663: Omit available IP annotations when filtering prefix child IPs list --- docs/release-notes/version-3.3.md | 1 + netbox/ipam/views.py | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/release-notes/version-3.3.md b/docs/release-notes/version-3.3.md index 741fdc8d5..7b23e242b 100644 --- a/docs/release-notes/version-3.3.md +++ b/docs/release-notes/version-3.3.md @@ -15,6 +15,7 @@ ### 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 diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index a086ab66d..185154ffb 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -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 From bb37ebf4bacafb4334b3ad2096467a1dda39086e Mon Sep 17 00:00:00 2001 From: Arthur Hanson Date: Thu, 25 Aug 2022 10:38:55 -0700 Subject: [PATCH 54/62] #10038 add assign FHRP group to device-interface context menu (#10151) --- netbox/dcim/tables/template_code.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/netbox/dcim/tables/template_code.py b/netbox/dcim/tables/template_code.py index 8c23f327c..18ff8b7b6 100644 --- a/netbox/dcim/tables/template_code.py +++ b/netbox/dcim/tables/template_code.py @@ -241,6 +241,9 @@ INTERFACE_BUTTONS = """ {% if perms.dcim.add_interface %}
  • Child Interface
  • {% endif %} + {% if perms.ipam.add_fhrpgroupassignment %} +
  • Assign FHRP Group
  • + {% endif %} {% endif %} From 7697779abfc85c0c7c5b897611d6117fe535b83a Mon Sep 17 00:00:00 2001 From: Arthur Hanson Date: Thu, 25 Aug 2022 10:41:55 -0700 Subject: [PATCH 55/62] #10038 add L2VPN termination to interface list context menu (#10152) Co-authored-by: Jeremy Stretch --- netbox/dcim/tables/template_code.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/netbox/dcim/tables/template_code.py b/netbox/dcim/tables/template_code.py index 18ff8b7b6..4b358a433 100644 --- a/netbox/dcim/tables/template_code.py +++ b/netbox/dcim/tables/template_code.py @@ -241,6 +241,9 @@ INTERFACE_BUTTONS = """ {% if perms.dcim.add_interface %}
  • Child Interface
  • {% endif %} + {% if perms.ipam.add_l2vpntermination %} +
  • L2VPN Termination
  • + {% endif %} {% if perms.ipam.add_fhrpgroupassignment %}
  • Assign FHRP Group
  • {% endif %} From 32615befd507bb9853aefc6d242944cdf850e610 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Thu, 25 Aug 2022 13:53:11 -0400 Subject: [PATCH 56/62] #10038 & #10039: Changelog & replicate for VM interfaces --- docs/release-notes/version-3.3.md | 4 +++- netbox/dcim/tables/template_code.py | 2 +- .../virtualization/tables/virtualmachines.py | 21 +++++++++++++++---- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/docs/release-notes/version-3.3.md b/docs/release-notes/version-3.3.md index 7b23e242b..a19b54c06 100644 --- a/docs/release-notes/version-3.3.md +++ b/docs/release-notes/version-3.3.md @@ -8,7 +8,9 @@ * [#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 link to create child interface to interface context menu +* [#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 diff --git a/netbox/dcim/tables/template_code.py b/netbox/dcim/tables/template_code.py index 4b358a433..8b6ac90b5 100644 --- a/netbox/dcim/tables/template_code.py +++ b/netbox/dcim/tables/template_code.py @@ -226,7 +226,7 @@ POWEROUTLET_BUTTONS = """ """ INTERFACE_BUTTONS = """ -{% if perms.ipam.add_ipaddress or perms.dcim.add_inventoryitem %} +{% if perms.dcim.edit_interface %} + + {% endif %} """ From 1379b9c9fbe5285e49e9d3907acaaa39c8a15093 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Thu, 25 Aug 2022 14:11:46 -0400 Subject: [PATCH 57/62] Tweak display of prerequisite model warning --- netbox/templates/generic/object_edit.html | 9 +++++---- netbox/templates/inc/missing_prerequisites.html | 9 ++++++--- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/netbox/templates/generic/object_edit.html b/netbox/templates/generic/object_edit.html index 8047dc59d..4ce270b30 100644 --- a/netbox/templates/generic/object_edit.html +++ b/netbox/templates/generic/object_edit.html @@ -31,6 +31,11 @@ Context:
    + {# Warn about missing prerequisite objects #} + {% if prerequisite_model %} + {% include 'inc/missing_prerequisites.html' %} + {% endif %} + {# Link to model documentation #} {% if object and settings.DOCS_ROOT %}
    @@ -40,10 +45,6 @@ Context:
    {% endif %} - {% if prerequisite_model %} - {% include 'inc/missing_prerequisites.html' %} - {% endif %} - {% csrf_token %} diff --git a/netbox/templates/inc/missing_prerequisites.html b/netbox/templates/inc/missing_prerequisites.html index 5814b72eb..66736a53e 100644 --- a/netbox/templates/inc/missing_prerequisites.html +++ b/netbox/templates/inc/missing_prerequisites.html @@ -1,6 +1,9 @@ {% load buttons %} -
    {{ iface }} - {{ iface.connected_endpoint.device }} - - {{ iface.connected_endpoint }} - - - {{ circuit.provider }} {{ circuit }} - None + {{ peer.device }} + + {{ peer }} + + + {{ circuit.provider }} {{ circuit }} + None