diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..5b4e4e5a4 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: [jeremystretch] diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 5df769b94..000000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -name: 🐛 Bug Report -about: Report a reproducible bug in the current release of NetBox - ---- - - -### Environment -* Python version: -* NetBox version: - - -### Steps to Reproduce -1. -2. -3. - - -### Expected Behavior - - - -### Observed Behavior diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml new file mode 100644 index 000000000..a83e9b34e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -0,0 +1,63 @@ +--- +name: 🐛 Bug Report +about: Report a reproducible bug in the current release of NetBox +labels: ["type: bug"] +body: + - type: markdown + attributes: + value: "**NOTE:** This form is only for reporting _reproducible bugs_ in a + current NetBox installation. If you're having trouble with installation or just + looking for assistance with using NetBox, please visit our + [discussion forum](https://github.com/netbox-community/netbox/discussions) instead." + - type: input + attributes: + label: NetBox version + description: "What version of NetBox are you currently running?" + placeholder: v2.10.4 + validations: + required: true + - type: dropdown + attributes: + label: Python version + description: "What version of Python are you currently running?" + options: + - 3.6 + - 3.7 + - 3.8 + - 3.9 + validations: + required: true + - type: textarea + attributes: + label: Steps to Reproduce + description: "Describe in detail the exact steps that someone else can take to + reproduce this bug using the current stable release of NetBox. Begin with the + creation of any necessary database objects and call out every operation being + performed explicitly. If reporting a bug in the REST API, be sure to reconstruct + the raw HTTP request(s) being made: Don't rely on a client library such as + pynetbox." + placeholder: | + 1. Click on "create widget" + 2. Set foo to 12 and bar to G + 3. Click the "create" button + validations: + required: true + - type: textarea + attributes: + label: Expected Behavior + description: "What did you expect to happen?" + placeholder: "A new widget should have been created with the specified attributes" + validations: + required: true + - type: textarea + attributes: + label: Observed Behavior + description: "What happened instead?" + placeholder: "A TypeError exception was raised" + validations: + required: true + - type: markdown + attributes: + value: | + ### Additional information + You can use the space below to provide any additional information or to attach files. diff --git a/.github/ISSUE_TEMPLATE/documentation_change.md b/.github/ISSUE_TEMPLATE/documentation_change.md deleted file mode 100644 index 9419e0ba9..000000000 --- a/.github/ISSUE_TEMPLATE/documentation_change.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -name: 📖 Documentation Change -about: Suggest an addition or modification to the NetBox documentation - ---- - - -### Change Type -[ ] Addition -[ ] Correction -[ ] Deprecation -[ ] Cleanup (formatting, typos, etc.) - -### Area -[ ] Installation instructions -[ ] Configuration parameters -[ ] Functionality/features -[ ] REST API -[ ] Administration/development -[ ] Other - - -### Proposed Changes diff --git a/.github/ISSUE_TEMPLATE/documentation_change.yaml b/.github/ISSUE_TEMPLATE/documentation_change.yaml new file mode 100644 index 000000000..3b2026b34 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/documentation_change.yaml @@ -0,0 +1,38 @@ +--- +name: 📖 Documentation Change +about: Suggest an addition or modification to the NetBox documentation +labels: ["type: documentation"] +body: + - type: dropdown + attributes: + label: Change Type + description: What type of change are you proposing? + options: + - Addition + - Correction + - Removal + - Cleanup (formatting, typos, etc.) + validations: + required: true + - type: checkboxes + attributes: + label: Area + description: To what section(s) of the documentation does this change pertain? + options: + - label: Installation instructions + - label: Configuration parameters + - label: Functionality/features + - label: REST API + - label: Administration/development + - label: Other + - type: textarea + attributes: + label: Proposed Changes + description: "Describe the proposed changes and why they are necessary" + validations: + required: true + - type: markdown + attributes: + value: | + ### Additional information + You can use the space below to provide any additional information or to attach files. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index a1114706f..000000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,54 +0,0 @@ ---- -name: ✨ Feature Request -about: Propose a new NetBox feature or enhancement - ---- - - -### Environment -* Python version: -* NetBox version: - - -### Proposed Functionality - - - -### Use Case - - - -### Database Changes - - - -### External Dependencies diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml new file mode 100644 index 000000000..efa83b376 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yaml @@ -0,0 +1,58 @@ +--- +name: ✨ Feature Request +about: Propose a new NetBox feature or enhancement +labels: ["type: feature"] +body: + - type: markdown + attributes: + value: "**NOTE:** This form is only for submitting well-formed proposals to extend or + modify NetBox in some way. If you're trying to solve a problem but can't figure out how, + or if you still need time to work on the details of a proposed new feature, please start + a [discussion](https://github.com/netbox-community/netbox/discussions) instead." + - type: input + attributes: + label: NetBox version + description: "What version of NetBox are you currently running?" + placeholder: v2.10.4 + validations: + required: true + - type: dropdown + attributes: + label: Feature type + options: + - Data model extension + - New functionality + - Change to existing functionality + validations: + required: true + - type: textarea + attributes: + label: Proposed functionality + description: "Describe in detail the new feature or behavior you'd like to propose. + Include any specific changes to work flows, data models, or the user interface." + validations: + required: true + - type: textarea + attributes: + label: Use case + description: "Explain how adding this functionality would benefit NetBox users. What + need does it address?" + validations: + required: true + - type: textarea + attributes: + label: Database changes + description: "Note any changes to the database schema necessary to support the new + feature. For example, does the proposal require adding a new model or field? (Not + all new features require database changes.)" + - type: textarea + attributes: + label: External dependencies + description: "List any new dependencies on external libraries or services that this + new feature would introduce. For example, does the proposal require the installation + of a new Python package? (Not all new features introduce new dependencies.)" + - type: markdown + attributes: + value: | + ### Additional information + You can use the space below to provide any additional information or to attach files. diff --git a/.github/ISSUE_TEMPLATE/housekeeping.md b/.github/ISSUE_TEMPLATE/housekeeping.md deleted file mode 100644 index f9ad4142d..000000000 --- a/.github/ISSUE_TEMPLATE/housekeeping.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -name: 🏡 Housekeeping -about: A change pertaining to the codebase itself (developers only) - ---- - - -### Proposed Changes - - - -### Justification diff --git a/.github/ISSUE_TEMPLATE/housekeeping.yaml b/.github/ISSUE_TEMPLATE/housekeeping.yaml new file mode 100644 index 000000000..0f466aa24 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/housekeeping.yaml @@ -0,0 +1,27 @@ +--- +name: 🏡 Housekeeping +about: A change pertaining to the codebase itself (developers only) +labels: ["type: housekeeping"] +body: + - type: markdown + attributes: + value: "**NOTE:** This template is for use by maintainers only. Please do not submit + an issue using this template unless you have been specifically asked to do so." + - type: textarea + attributes: + label: Proposed Changes + description: "Describe in detail the new feature or behavior you'd like to propose. + Include any specific changes to work flows, data models, or the user interface." + validations: + required: true + - type: textarea + attributes: + label: Justification + description: "Please provide justification for the proposed change(s)." + validations: + required: true + - type: markdown + attributes: + value: | + ### Additional information + You can use the space below to provide any additional information or to attach files. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c86f810f3..cd62c5ab7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -25,7 +25,7 @@ discussions. ### Slack -For real-time chat, you can join the **#netbox** Slack channel on [NetworkToCode](https://slack.networktocode.com/). +For real-time chat, you can join the **#netbox** Slack channel on [NetDev Community](https://join.slack.com/t/netdev-community/shared_invite/zt-mtts8g0n-Sm6Wutn62q_M4OdsaIycrQ). Unfortunately, the Slack channel does not provide long-term retention of chat history, so try to avoid it for any discussions would benefit from being preserved for future reference. @@ -185,11 +185,5 @@ overlooked. sync to review agenda items. This meeting provides opportunity to present and discuss pressing topics. Meetings are held as virtual audio/video conferences. -* Official channels for communication include: - - * GitHub issues, pull requests, and discussions - * The [netbox-discuss](https://groups.google.com/g/netbox-discuss) mailing list - * The **#netbox** channel on [NetworkToCode Slack](https://networktocode.slack.com/) - * Maintainers with no substantial recorded activity in a 60-day period will be removed from the project. diff --git a/README.md b/README.md index a12daa783..880fa8c08 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,11 @@ complete list of requirements, see `requirements.txt`. The code is available [on The complete documentation for NetBox can be found at [Read the Docs](https://netbox.readthedocs.io/en/stable/). -Questions? Comments? Start by perusing our [GitHub discussions](https://github.com/netbox-community/netbox/discussions) for the topic you have in mind. +### Discussion + +* [GitHub Discussions](https://github.com/netbox-community/netbox/discussions) - Discussion forum hosted by GitHub; ideal for Q&A and other structured discussions +* [Slack](https://join.slack.com/t/netdev-community/shared_invite/zt-mtts8g0n-Sm6Wutn62q_M4OdsaIycrQ) - Real-time chat hosted by the NetDev Community; best for unstructured discussion or just hanging out +* [Google Group](https://groups.google.com/g/netbox-discuss) - Legacy mailing list; slowly being replaced by GitHub discussions ### Build Status diff --git a/docs/additional-features/webhooks.md b/docs/additional-features/webhooks.md index d5b12fb85..f3dd80337 100644 --- a/docs/additional-features/webhooks.md +++ b/docs/additional-features/webhooks.md @@ -80,7 +80,7 @@ If no body template is specified, the request body will be populated with a JSON ## Webhook Processing -When a change is detected, any resulting webhooks are placed into a Redis queue for processing. This allows the user's request to complete without needing to wait for the outgoing webhook(s) to be processed. The webhooks are then extracted from the queue by the `rqworker` process and HTTP requests are sent to their respective destinations. The current webhook queue and any failed webhooks can be inspected in the admin UI under Django RQ > Queues. +When a change is detected, any resulting webhooks are placed into a Redis queue for processing. This allows the user's request to complete without needing to wait for the outgoing webhook(s) to be processed. The webhooks are then extracted from the queue by the `rqworker` process and HTTP requests are sent to their respective destinations. The current webhook queue and any failed webhooks can be inspected in the admin UI under System > Background Tasks. A request is considered successful if the response has a 2XX status code; otherwise, the request is marked as having failed. Failed requests may be retried manually via the admin UI. diff --git a/docs/development/index.md b/docs/development/index.md index c0aea0eeb..bbcb1eac8 100644 --- a/docs/development/index.md +++ b/docs/development/index.md @@ -4,12 +4,12 @@ NetBox is maintained as a [GitHub project](https://github.com/netbox-community/n ## Communication -Communication among developers should always occur via public channels: +There are several official forums for communication among the developers and community members: * [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 an 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. -* [The mailing list](https://groups.google.com/g/netbox-discuss) - An alternative forum for general discussion (GitHub is preferred). -* [#netbox on NetworkToCode](http://slack.networktocode.com/) - Good for quick chats. Avoid any discussion that might need to be referenced later on, as the chat history is not retained long. +* [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://join.slack.com/t/netdev-community/shared_invite/zt-mtts8g0n-Sm6Wutn62q_M4OdsaIycrQ) - Good for quick chats. Avoid any discussion that might need to be referenced later on, as the chat history is not retained long. +* [Google Group](https://groups.google.com/g/netbox-discuss) - Legacy mailing list; slowly being phased out in favor of GitHub discussions. ## Governance diff --git a/docs/development/release-checklist.md b/docs/development/release-checklist.md index f3338ffd3..d8cb671f6 100644 --- a/docs/development/release-checklist.md +++ b/docs/development/release-checklist.md @@ -2,24 +2,9 @@ ## Minor Version Bumps -### Update Requirements +### Address Pinned Dependencies -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: - -``` -# 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. - -Every minor version release should refresh `requirements.txt` so that it lists the most recent stable release of each package. To do this: - -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. +Check `base_requirements.txt` for any dependencies pinned to a specific version, and upgrade them to their most stable release (where possible). ### Update Static Libraries @@ -58,6 +43,27 @@ Submit a pull request to merge the `feature` branch into the `develop` branch in ## 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: + +``` +# 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. + +Every release should refresh `requirements.txt` so that it lists the most recent stable release of each package. To do this: + +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. + +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. + ### Verify CI Build Status Ensure that continuous integration testing on the `develop` branch is completing successfully. diff --git a/docs/installation/1-postgresql.md b/docs/installation/1-postgresql.md index 88e152193..1db490798 100644 --- a/docs/installation/1-postgresql.md +++ b/docs/installation/1-postgresql.md @@ -52,7 +52,7 @@ $ sudo -u postgres psql psql (12.5 (Ubuntu 12.5-0ubuntu0.20.04.1)) Type "help" for help. -postgres=# CREATE DATABASE netbox ENCODING 'UTF8' LC_COLLATE='C.UTF-8' LC_CTYPE='C.UTF-8'; +postgres=# CREATE DATABASE netbox; CREATE DATABASE postgres=# CREATE USER netbox WITH PASSWORD 'J5brHrAXFLQSif0K'; CREATE ROLE diff --git a/docs/release-notes/version-2.10.md b/docs/release-notes/version-2.10.md index 2f7079f03..2b3792204 100644 --- a/docs/release-notes/version-2.10.md +++ b/docs/release-notes/version-2.10.md @@ -1,5 +1,24 @@ # NetBox v2.10 +## v2.10.6 (2021-03-09) + +### Enhancements + +* [#5592](https://github.com/netbox-community/netbox/issues/5592) - Add IP addresses count to VRF view +* [#5630](https://github.com/netbox-community/netbox/issues/5630) - Add QSFP+ (64GFC) FibreChannel Interface option +* [#5884](https://github.com/netbox-community/netbox/issues/5884) - Enable custom links for device components +* [#5914](https://github.com/netbox-community/netbox/issues/5914) - Add edit/delete buttons for IP addresses on interface view +* [#5942](https://github.com/netbox-community/netbox/issues/5942) - Add button to add a new IP address on interface view + +### Bug Fixes + +* [#5703](https://github.com/netbox-community/netbox/issues/5703) - Fix VRF and Tenant field population when adding IP addresses from prefix +* [#5819](https://github.com/netbox-community/netbox/issues/5819) - Enable ordering of virtual machines by primary IP address +* [#5872](https://github.com/netbox-community/netbox/issues/5872) - Ordering of devices by primary IP should respect `PREFER_IPV4` configuration parameter +* [#5922](https://github.com/netbox-community/netbox/issues/5922) - Fix options for filtering object permissions in admin UI +* [#5935](https://github.com/netbox-community/netbox/issues/5935) - Fix filtering prefixes list by multiple prefix values +* [#5948](https://github.com/netbox-community/netbox/issues/5948) - Invalidate cached queries when running `renaturalize` + ## v2.10.5 (2021-02-24) ### Bug Fixes diff --git a/netbox/dcim/choices.py b/netbox/dcim/choices.py index ce013acc3..5bea88fe3 100644 --- a/netbox/dcim/choices.py +++ b/netbox/dcim/choices.py @@ -709,6 +709,7 @@ class InterfaceTypeChoices(ChoiceSet): TYPE_8GFC_SFP_PLUS = '8gfc-sfpp' TYPE_16GFC_SFP_PLUS = '16gfc-sfpp' TYPE_32GFC_SFP28 = '32gfc-sfp28' + TYPE_64GFC_QSFP_PLUS = '64gfc-qsfpp' TYPE_128GFC_QSFP28 = '128gfc-sfp28' # InfiniBand @@ -824,6 +825,7 @@ class InterfaceTypeChoices(ChoiceSet): (TYPE_8GFC_SFP_PLUS, 'SFP+ (8GFC)'), (TYPE_16GFC_SFP_PLUS, 'SFP+ (16GFC)'), (TYPE_32GFC_SFP28, 'SFP28 (32GFC)'), + (TYPE_64GFC_QSFP_PLUS, 'QSFP+ (64GFC)'), (TYPE_128GFC_QSFP28, 'QSFP28 (128GFC)'), ) ), diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index 2625219d8..e76d03826 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -210,7 +210,7 @@ class PathEndpoint(models.Model): # Console ports # -@extras_features('custom_fields', 'export_templates', 'webhooks') +@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') class ConsolePort(ComponentModel, CableTermination, PathEndpoint): """ A physical console port within a Device. ConsolePorts connect to ConsoleServerPorts. @@ -254,7 +254,7 @@ class ConsolePort(ComponentModel, CableTermination, PathEndpoint): # Console server ports # -@extras_features('custom_fields', 'export_templates', 'webhooks') +@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') class ConsoleServerPort(ComponentModel, CableTermination, PathEndpoint): """ A physical port within a Device (typically a designated console server) which provides access to ConsolePorts. @@ -298,7 +298,7 @@ class ConsoleServerPort(ComponentModel, CableTermination, PathEndpoint): # Power ports # -@extras_features('custom_fields', 'export_templates', 'webhooks') +@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') class PowerPort(ComponentModel, CableTermination, PathEndpoint): """ A physical power supply (intake) port within a Device. PowerPorts connect to PowerOutlets. @@ -410,7 +410,7 @@ class PowerPort(ComponentModel, CableTermination, PathEndpoint): # Power outlets # -@extras_features('custom_fields', 'export_templates', 'webhooks') +@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') class PowerOutlet(ComponentModel, CableTermination, PathEndpoint): """ A physical power outlet (output) within a Device which provides power to a PowerPort. @@ -511,7 +511,7 @@ class BaseInterface(models.Model): return super().save(*args, **kwargs) -@extras_features('custom_fields', 'export_templates', 'webhooks') +@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') class Interface(ComponentModel, BaseInterface, CableTermination, PathEndpoint): """ A network interface within a Device. A physical Interface can connect to exactly one other Interface. @@ -684,7 +684,7 @@ class Interface(ComponentModel, BaseInterface, CableTermination, PathEndpoint): # Pass-through ports # -@extras_features('custom_fields', 'export_templates', 'webhooks') +@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') class FrontPort(ComponentModel, CableTermination): """ A pass-through port on the front of a Device. @@ -750,7 +750,7 @@ class FrontPort(ComponentModel, CableTermination): }) -@extras_features('custom_fields', 'export_templates', 'webhooks') +@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') class RearPort(ComponentModel, CableTermination): """ A pass-through port on the rear of a Device. @@ -804,7 +804,7 @@ class RearPort(ComponentModel, CableTermination): # Device bays # -@extras_features('custom_fields', 'export_templates', 'webhooks') +@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') class DeviceBay(ComponentModel): """ An empty space within a Device which can house a child device @@ -864,7 +864,7 @@ class DeviceBay(ComponentModel): # Inventory items # -@extras_features('custom_fields', 'export_templates', 'webhooks') +@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') class InventoryItem(MPTTModel, ComponentModel): """ An InventoryItem represents a serialized piece of hardware within a Device, such as a line card or power supply. diff --git a/netbox/dcim/tables/devices.py b/netbox/dcim/tables/devices.py index 49ff12190..c4204dd4a 100644 --- a/netbox/dcim/tables/devices.py +++ b/netbox/dcim/tables/devices.py @@ -1,5 +1,6 @@ import django_tables2 as tables from django_tables2.utils import Accessor +from django.conf import settings from dcim.models import ( ConsolePort, ConsoleServerPort, Device, DeviceBay, DeviceRole, FrontPort, Interface, InventoryItem, Platform, @@ -128,11 +129,18 @@ class DeviceTable(BaseTable): verbose_name='Type', text=lambda record: record.device_type.display_name ) - primary_ip = tables.Column( - linkify=True, - order_by=('primary_ip6', 'primary_ip4'), - verbose_name='IP Address' - ) + if settings.PREFER_IPV4: + primary_ip = tables.Column( + linkify=True, + order_by=('primary_ip4', 'primary_ip6'), + verbose_name='IP Address' + ) + else: + primary_ip = tables.Column( + linkify=True, + order_by=('primary_ip6', 'primary_ip4'), + verbose_name='IP Address' + ) primary_ip4 = tables.Column( linkify=True, verbose_name='IPv4 Address' diff --git a/netbox/extras/management/commands/renaturalize.py b/netbox/extras/management/commands/renaturalize.py index cfd037910..8ad51966a 100644 --- a/netbox/extras/management/commands/renaturalize.py +++ b/netbox/extras/management/commands/renaturalize.py @@ -1,3 +1,4 @@ +from cacheops import invalidate_model from django.apps import apps from django.core.management.base import BaseCommand, CommandError @@ -27,7 +28,7 @@ class Command(BaseCommand): app_label, model_name = name.split('.') except ValueError: raise CommandError( - "Invalid format: {}. Models must be specified in the form app_label.ModelName.".format(name) + f"Invalid format: {name}. Models must be specified in the form app_label.ModelName." ) try: app_config = apps.get_app_config(app_label) @@ -36,13 +37,13 @@ class Command(BaseCommand): try: model = app_config.get_model(model_name) except LookupError: - raise CommandError("Unknown model: {}.{}".format(app_label, model_name)) + raise CommandError(f"Unknown model: {app_label}.{model_name}") fields = [ field for field in model._meta.concrete_fields if type(field) is NaturalOrderingField ] if not fields: raise CommandError( - "Invalid model: {}.{} does not employ natural ordering".format(app_label, model_name) + f"Invalid model: {app_label}.{model_name} does not employ natural ordering" ) models.append( (model, fields) @@ -67,7 +68,7 @@ class Command(BaseCommand): models = self._get_models(args) if options['verbosity']: - self.stdout.write("Renaturalizing {} models.".format(len(models))) + self.stdout.write(f"Renaturalizing {len(models)} models.") for model, fields in models: for field in fields: @@ -78,7 +79,7 @@ class Command(BaseCommand): # Print the model and field name if options['verbosity']: self.stdout.write( - "{}.{} ({})... ".format(model._meta.label, field.target_field, field.name), + f"{model._meta.label}.{field.target_field} ({field.name})... ", ending='\n' if options['verbosity'] >= 2 else '' ) self.stdout.flush() @@ -89,23 +90,26 @@ class Command(BaseCommand): naturalized_value = naturalize(value, max_length=field.max_length) if options['verbosity'] >= 2: - self.stdout.write(" {} -> {}".format(value, naturalized_value), ending='') + self.stdout.write(f" {value} -> {naturalized_value}", ending='') self.stdout.flush() # Update each unique field value in bulk changed = model.objects.filter(name=value).update(**{field.name: naturalized_value}) if options['verbosity'] >= 2: - self.stdout.write(" ({})".format(changed)) + self.stdout.write(f" ({changed})") count += changed # Print the total count of alterations for the field if options['verbosity'] >= 2: - self.stdout.write(self.style.SUCCESS("{} {} updated ({} unique values)".format( - count, model._meta.verbose_name_plural, queryset.count() - ))) + self.stdout.write(self.style.SUCCESS( + f"{count} {model._meta.verbose_name_plural} updated ({queryset.count()} unique values)" + )) elif options['verbosity']: self.stdout.write(self.style.SUCCESS(str(count))) + # Invalidate cached queries + invalidate_model(model) + if options['verbosity']: self.stdout.write(self.style.SUCCESS("Done.")) diff --git a/netbox/ipam/filters.py b/netbox/ipam/filters.py index 1dff03144..f4a6236cb 100644 --- a/netbox/ipam/filters.py +++ b/netbox/ipam/filters.py @@ -193,7 +193,7 @@ class PrefixFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet field_name='prefix', lookup_expr='family' ) - prefix = django_filters.CharFilter( + prefix = MultiValueCharFilter( method='filter_prefix', label='Prefix', ) @@ -318,13 +318,13 @@ class PrefixFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet return queryset.filter(qs_filter) def filter_prefix(self, queryset, name, value): - if not value.strip(): - return queryset - try: - query = str(netaddr.IPNetwork(value).cidr) - return queryset.filter(prefix=query) - except (AddrFormatError, ValueError): - return queryset.none() + query_values = [] + for v in value: + try: + query_values.append(netaddr.IPNetwork(v)) + except (AddrFormatError, ValueError): + pass + return queryset.filter(prefix__in=query_values) def search_within(self, queryset, name, value): value = value.strip() diff --git a/netbox/ipam/tables.py b/netbox/ipam/tables.py index b8b166cdc..0e712817b 100644 --- a/netbox/ipam/tables.py +++ b/netbox/ipam/tables.py @@ -33,7 +33,7 @@ IPADDRESS_LINK = """ {% if record.pk %} {{ record.address }} {% elif perms.ipam.add_ipaddress %} - {% if record.0 <= 65536 %}{{ record.0 }}{% else %}Many{% endif %} IP{{ record.0|pluralize }} available + {% if record.0 <= 65536 %}{{ record.0 }}{% else %}Many{% endif %} IP{{ record.0|pluralize }} available {% else %} {% if record.0 <= 65536 %}{{ record.0 }}{% else %}Many{% endif %} IP{{ record.0|pluralize }} available {% endif %} @@ -46,8 +46,8 @@ IPADDRESS_ASSIGN_LINK = """ VRF_LINK = """ {% if record.vrf %} {{ record.vrf }} -{% elif prefix.vrf %} - {{ prefix.vrf }} +{% elif object.vrf %} + {{ object.vrf }} {% else %} Global {% endif %} @@ -401,6 +401,9 @@ class InterfaceIPAddressTable(BaseTable): ) status = ChoiceFieldColumn() tenant = TenantColumn() + actions = ButtonsColumn( + model=IPAddress + ) class Meta(BaseTable.Meta): model = IPAddress diff --git a/netbox/ipam/tests/test_filters.py b/netbox/ipam/tests/test_filters.py index f26385ae6..b83ea6efe 100644 --- a/netbox/ipam/tests/test_filters.py +++ b/netbox/ipam/tests/test_filters.py @@ -429,6 +429,11 @@ class PrefixTestCase(TestCase): params = {'family': '6'} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5) + def test_prefix(self): + prefixes = Prefix.objects.all()[:2] + params = {'prefix': [prefixes[0].prefix, prefixes[1].prefix]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_is_pool(self): params = {'is_pool': 'true'} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index bada223e3..63d81d58e 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -30,6 +30,7 @@ class VRFView(generic.ObjectView): def get_extra_context(self, request, instance): prefix_count = Prefix.objects.restrict(request.user, 'view').filter(vrf=instance).count() + ipaddress_count = IPAddress.objects.restrict(request.user, 'view').filter(vrf=instance).count() import_targets_table = tables.RouteTargetTable( instance.import_targets.prefetch_related('tenant'), @@ -42,6 +43,7 @@ class VRFView(generic.ObjectView): return { 'prefix_count': prefix_count, + 'ipaddress_count': ipaddress_count, 'import_targets_table': import_targets_table, 'export_targets_table': export_targets_table, } diff --git a/netbox/templates/dcim/device_component.html b/netbox/templates/dcim/device_component.html index 4abd36a4d..a615092b9 100644 --- a/netbox/templates/dcim/device_component.html +++ b/netbox/templates/dcim/device_component.html @@ -1,6 +1,7 @@ {% extends 'generic/object.html' %} {% load helpers %} {% load perms %} +{% load custom_links %} {% load plugins %} {% block title %}{{ object.device }} / {{ object }}{% endblock %} diff --git a/netbox/templates/dcim/interface.html b/netbox/templates/dcim/interface.html index 7511975b1..8dc8c07a2 100644 --- a/netbox/templates/dcim/interface.html +++ b/netbox/templates/dcim/interface.html @@ -1,6 +1,7 @@ {% extends 'dcim/device_component.html' %} {% load helpers %} {% load plugins %} +{% load render_table from django_tables2 %} {% block content %}