1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

Merge branch 'feature' into 5284-vlangroup-scope

This commit is contained in:
Jeremy Stretch
2021-03-09 20:17:47 -05:00
31 changed files with 368 additions and 223 deletions

1
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1 @@
github: [jeremystretch]

View File

@ -1,40 +0,0 @@
---
name: 🐛 Bug Report
about: Report a reproducible bug in the current release of NetBox
---
<!--
NOTE: IF YOUR ISSUE DOES NOT FOLLOW THIS TEMPLATE, IT WILL BE CLOSED.
This form is only for reporting reproducible bugs. If you need assistance
with NetBox installation, or if you have a general question, please start a
discussion instead: https://github.com/netbox-community/netbox/discussions
Please describe the environment in which you are running NetBox. Be sure
that you are running an unmodified instance of the latest stable release
before submitting a bug report, and that any plugins have been disabled.
-->
### Environment
* Python version:
* NetBox version:
<!--
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.
-->
### Steps to Reproduce
1.
2.
3.
<!-- What did you expect to happen? -->
### Expected Behavior
<!-- What happened instead? -->
### Observed Behavior

63
.github/ISSUE_TEMPLATE/bug_report.yaml vendored Normal file
View File

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

View File

@ -1,28 +0,0 @@
---
name: 📖 Documentation Change
about: Suggest an addition or modification to the NetBox documentation
---
<!--
NOTE: IF YOUR ISSUE DOES NOT FOLLOW THIS TEMPLATE, IT WILL BE CLOSED.
Please indicate the nature of the change by placing an X in one of the
boxes below.
-->
### Change Type
[ ] Addition
[ ] Correction
[ ] Deprecation
[ ] Cleanup (formatting, typos, etc.)
### Area
[ ] Installation instructions
[ ] Configuration parameters
[ ] Functionality/features
[ ] REST API
[ ] Administration/development
[ ] Other
<!-- Describe the proposed change(s). -->
### Proposed Changes

View File

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

View File

@ -1,54 +0,0 @@
---
name: ✨ Feature Request
about: Propose a new NetBox feature or enhancement
---
<!--
NOTE: IF YOUR ISSUE DOES NOT FOLLOW THIS TEMPLATE, IT WILL BE CLOSED.
This form is only for proposing specific new features or enhancements.
If you have a general idea or question, please start a discussion instead:
https://github.com/netbox-community/netbox/discussions
NOTE: Due to an excessive backlog of feature requests, we are not currently
accepting any proposals which significantly extend NetBox's feature scope.
Please describe the environment in which you are running NetBox. Be sure
that you are running an unmodified instance of the latest stable release
before submitting a bug report.
-->
### Environment
* Python version:
* NetBox version:
<!--
Describe in detail the new functionality you are proposing. Include any
specific changes to work flows, data models, or the user interface.
-->
### Proposed Functionality
<!--
Convey an example use case for your proposed feature. Write from the
perspective of a NetBox user who would benefit from the proposed
functionality and describe how.
--->
### Use Case
<!--
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.)
--->
### Database Changes
<!--
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.)
-->
### External Dependencies

View File

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

View File

@ -1,16 +0,0 @@
---
name: 🏡 Housekeeping
about: A change pertaining to the codebase itself (developers only)
---
<!--
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.
-->
### Proposed Changes
<!-- Provide justification for the proposed change(s). -->
### Justification

View File

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

View File

@ -25,7 +25,7 @@ discussions.
### Slack ### 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 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 history, so try to avoid it for any discussions would benefit from being
preserved for future reference. preserved for future reference.
@ -185,11 +185,5 @@ overlooked.
sync to review agenda items. This meeting provides opportunity to present and sync to review agenda items. This meeting provides opportunity to present and
discuss pressing topics. Meetings are held as virtual audio/video conferences. 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 * Maintainers with no substantial recorded activity in a 60-day period will be
removed from the project. removed from the project.

View File

@ -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/). 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 ### Build Status

View File

@ -80,7 +80,7 @@ If no body template is specified, the request body will be populated with a JSON
## Webhook Processing ## 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. 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.

View File

@ -4,12 +4,12 @@ NetBox is maintained as a [GitHub project](https://github.com/netbox-community/n
## Communication ## 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 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. * [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 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.
* [#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. * [Google Group](https://groups.google.com/g/netbox-discuss) - Legacy mailing list; slowly being phased out in favor of GitHub discussions.
## Governance ## Governance

View File

@ -2,24 +2,9 @@
## Minor Version Bumps ## 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: Check `base_requirements.txt` for any dependencies pinned to a specific version, and upgrade them to their most stable release (where possible).
```
# 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.
### Update Static Libraries ### Update Static Libraries
@ -58,6 +43,27 @@ Submit a pull request to merge the `feature` branch into the `develop` branch in
## All Releases ## 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 ### Verify CI Build Status
Ensure that continuous integration testing on the `develop` branch is completing successfully. Ensure that continuous integration testing on the `develop` branch is completing successfully.

View File

@ -52,7 +52,7 @@ $ sudo -u postgres psql
psql (12.5 (Ubuntu 12.5-0ubuntu0.20.04.1)) psql (12.5 (Ubuntu 12.5-0ubuntu0.20.04.1))
Type "help" for help. 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 CREATE DATABASE
postgres=# CREATE USER netbox WITH PASSWORD 'J5brHrAXFLQSif0K'; postgres=# CREATE USER netbox WITH PASSWORD 'J5brHrAXFLQSif0K';
CREATE ROLE CREATE ROLE

View File

@ -1,5 +1,24 @@
# NetBox v2.10 # 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) ## v2.10.5 (2021-02-24)
### Bug Fixes ### Bug Fixes

View File

@ -709,6 +709,7 @@ class InterfaceTypeChoices(ChoiceSet):
TYPE_8GFC_SFP_PLUS = '8gfc-sfpp' TYPE_8GFC_SFP_PLUS = '8gfc-sfpp'
TYPE_16GFC_SFP_PLUS = '16gfc-sfpp' TYPE_16GFC_SFP_PLUS = '16gfc-sfpp'
TYPE_32GFC_SFP28 = '32gfc-sfp28' TYPE_32GFC_SFP28 = '32gfc-sfp28'
TYPE_64GFC_QSFP_PLUS = '64gfc-qsfpp'
TYPE_128GFC_QSFP28 = '128gfc-sfp28' TYPE_128GFC_QSFP28 = '128gfc-sfp28'
# InfiniBand # InfiniBand
@ -824,6 +825,7 @@ class InterfaceTypeChoices(ChoiceSet):
(TYPE_8GFC_SFP_PLUS, 'SFP+ (8GFC)'), (TYPE_8GFC_SFP_PLUS, 'SFP+ (8GFC)'),
(TYPE_16GFC_SFP_PLUS, 'SFP+ (16GFC)'), (TYPE_16GFC_SFP_PLUS, 'SFP+ (16GFC)'),
(TYPE_32GFC_SFP28, 'SFP28 (32GFC)'), (TYPE_32GFC_SFP28, 'SFP28 (32GFC)'),
(TYPE_64GFC_QSFP_PLUS, 'QSFP+ (64GFC)'),
(TYPE_128GFC_QSFP28, 'QSFP28 (128GFC)'), (TYPE_128GFC_QSFP28, 'QSFP28 (128GFC)'),
) )
), ),

View File

@ -210,7 +210,7 @@ class PathEndpoint(models.Model):
# Console ports # Console ports
# #
@extras_features('custom_fields', 'export_templates', 'webhooks') @extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class ConsolePort(ComponentModel, CableTermination, PathEndpoint): class ConsolePort(ComponentModel, CableTermination, PathEndpoint):
""" """
A physical console port within a Device. ConsolePorts connect to ConsoleServerPorts. A physical console port within a Device. ConsolePorts connect to ConsoleServerPorts.
@ -254,7 +254,7 @@ class ConsolePort(ComponentModel, CableTermination, PathEndpoint):
# Console server ports # Console server ports
# #
@extras_features('custom_fields', 'export_templates', 'webhooks') @extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class ConsoleServerPort(ComponentModel, CableTermination, PathEndpoint): class ConsoleServerPort(ComponentModel, CableTermination, PathEndpoint):
""" """
A physical port within a Device (typically a designated console server) which provides access to ConsolePorts. 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 # Power ports
# #
@extras_features('custom_fields', 'export_templates', 'webhooks') @extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class PowerPort(ComponentModel, CableTermination, PathEndpoint): class PowerPort(ComponentModel, CableTermination, PathEndpoint):
""" """
A physical power supply (intake) port within a Device. PowerPorts connect to PowerOutlets. A physical power supply (intake) port within a Device. PowerPorts connect to PowerOutlets.
@ -410,7 +410,7 @@ class PowerPort(ComponentModel, CableTermination, PathEndpoint):
# Power outlets # Power outlets
# #
@extras_features('custom_fields', 'export_templates', 'webhooks') @extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class PowerOutlet(ComponentModel, CableTermination, PathEndpoint): class PowerOutlet(ComponentModel, CableTermination, PathEndpoint):
""" """
A physical power outlet (output) within a Device which provides power to a PowerPort. 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) 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): class Interface(ComponentModel, BaseInterface, CableTermination, PathEndpoint):
""" """
A network interface within a Device. A physical Interface can connect to exactly one other Interface. 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 # Pass-through ports
# #
@extras_features('custom_fields', 'export_templates', 'webhooks') @extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class FrontPort(ComponentModel, CableTermination): class FrontPort(ComponentModel, CableTermination):
""" """
A pass-through port on the front of a Device. 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): class RearPort(ComponentModel, CableTermination):
""" """
A pass-through port on the rear of a Device. A pass-through port on the rear of a Device.
@ -804,7 +804,7 @@ class RearPort(ComponentModel, CableTermination):
# Device bays # Device bays
# #
@extras_features('custom_fields', 'export_templates', 'webhooks') @extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class DeviceBay(ComponentModel): class DeviceBay(ComponentModel):
""" """
An empty space within a Device which can house a child device An empty space within a Device which can house a child device
@ -864,7 +864,7 @@ class DeviceBay(ComponentModel):
# Inventory items # Inventory items
# #
@extras_features('custom_fields', 'export_templates', 'webhooks') @extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class InventoryItem(MPTTModel, ComponentModel): class InventoryItem(MPTTModel, ComponentModel):
""" """
An InventoryItem represents a serialized piece of hardware within a Device, such as a line card or power supply. An InventoryItem represents a serialized piece of hardware within a Device, such as a line card or power supply.

View File

@ -1,5 +1,6 @@
import django_tables2 as tables import django_tables2 as tables
from django_tables2.utils import Accessor from django_tables2.utils import Accessor
from django.conf import settings
from dcim.models import ( from dcim.models import (
ConsolePort, ConsoleServerPort, Device, DeviceBay, DeviceRole, FrontPort, Interface, InventoryItem, Platform, ConsolePort, ConsoleServerPort, Device, DeviceBay, DeviceRole, FrontPort, Interface, InventoryItem, Platform,
@ -128,11 +129,18 @@ class DeviceTable(BaseTable):
verbose_name='Type', verbose_name='Type',
text=lambda record: record.device_type.display_name text=lambda record: record.device_type.display_name
) )
primary_ip = tables.Column( if settings.PREFER_IPV4:
linkify=True, primary_ip = tables.Column(
order_by=('primary_ip6', 'primary_ip4'), linkify=True,
verbose_name='IP Address' 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( primary_ip4 = tables.Column(
linkify=True, linkify=True,
verbose_name='IPv4 Address' verbose_name='IPv4 Address'

View File

@ -1,3 +1,4 @@
from cacheops import invalidate_model
from django.apps import apps from django.apps import apps
from django.core.management.base import BaseCommand, CommandError from django.core.management.base import BaseCommand, CommandError
@ -27,7 +28,7 @@ class Command(BaseCommand):
app_label, model_name = name.split('.') app_label, model_name = name.split('.')
except ValueError: except ValueError:
raise CommandError( 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: try:
app_config = apps.get_app_config(app_label) app_config = apps.get_app_config(app_label)
@ -36,13 +37,13 @@ class Command(BaseCommand):
try: try:
model = app_config.get_model(model_name) model = app_config.get_model(model_name)
except LookupError: except LookupError:
raise CommandError("Unknown model: {}.{}".format(app_label, model_name)) raise CommandError(f"Unknown model: {app_label}.{model_name}")
fields = [ fields = [
field for field in model._meta.concrete_fields if type(field) is NaturalOrderingField field for field in model._meta.concrete_fields if type(field) is NaturalOrderingField
] ]
if not fields: if not fields:
raise CommandError( 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( models.append(
(model, fields) (model, fields)
@ -67,7 +68,7 @@ class Command(BaseCommand):
models = self._get_models(args) models = self._get_models(args)
if options['verbosity']: 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 model, fields in models:
for field in fields: for field in fields:
@ -78,7 +79,7 @@ class Command(BaseCommand):
# Print the model and field name # Print the model and field name
if options['verbosity']: if options['verbosity']:
self.stdout.write( 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 '' ending='\n' if options['verbosity'] >= 2 else ''
) )
self.stdout.flush() self.stdout.flush()
@ -89,23 +90,26 @@ class Command(BaseCommand):
naturalized_value = naturalize(value, max_length=field.max_length) naturalized_value = naturalize(value, max_length=field.max_length)
if options['verbosity'] >= 2: if options['verbosity'] >= 2:
self.stdout.write(" {} -> {}".format(value, naturalized_value), ending='') self.stdout.write(f" {value} -> {naturalized_value}", ending='')
self.stdout.flush() self.stdout.flush()
# Update each unique field value in bulk # Update each unique field value in bulk
changed = model.objects.filter(name=value).update(**{field.name: naturalized_value}) changed = model.objects.filter(name=value).update(**{field.name: naturalized_value})
if options['verbosity'] >= 2: if options['verbosity'] >= 2:
self.stdout.write(" ({})".format(changed)) self.stdout.write(f" ({changed})")
count += changed count += changed
# Print the total count of alterations for the field # Print the total count of alterations for the field
if options['verbosity'] >= 2: if options['verbosity'] >= 2:
self.stdout.write(self.style.SUCCESS("{} {} updated ({} unique values)".format( self.stdout.write(self.style.SUCCESS(
count, model._meta.verbose_name_plural, queryset.count() f"{count} {model._meta.verbose_name_plural} updated ({queryset.count()} unique values)"
))) ))
elif options['verbosity']: elif options['verbosity']:
self.stdout.write(self.style.SUCCESS(str(count))) self.stdout.write(self.style.SUCCESS(str(count)))
# Invalidate cached queries
invalidate_model(model)
if options['verbosity']: if options['verbosity']:
self.stdout.write(self.style.SUCCESS("Done.")) self.stdout.write(self.style.SUCCESS("Done."))

View File

@ -193,7 +193,7 @@ class PrefixFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet
field_name='prefix', field_name='prefix',
lookup_expr='family' lookup_expr='family'
) )
prefix = django_filters.CharFilter( prefix = MultiValueCharFilter(
method='filter_prefix', method='filter_prefix',
label='Prefix', label='Prefix',
) )
@ -318,13 +318,13 @@ class PrefixFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet
return queryset.filter(qs_filter) return queryset.filter(qs_filter)
def filter_prefix(self, queryset, name, value): def filter_prefix(self, queryset, name, value):
if not value.strip(): query_values = []
return queryset for v in value:
try: try:
query = str(netaddr.IPNetwork(value).cidr) query_values.append(netaddr.IPNetwork(v))
return queryset.filter(prefix=query) except (AddrFormatError, ValueError):
except (AddrFormatError, ValueError): pass
return queryset.none() return queryset.filter(prefix__in=query_values)
def search_within(self, queryset, name, value): def search_within(self, queryset, name, value):
value = value.strip() value = value.strip()

View File

@ -33,7 +33,7 @@ IPADDRESS_LINK = """
{% if record.pk %} {% if record.pk %}
<a href="{{ record.get_absolute_url }}">{{ record.address }}</a> <a href="{{ record.get_absolute_url }}">{{ record.address }}</a>
{% elif perms.ipam.add_ipaddress %} {% elif perms.ipam.add_ipaddress %}
<a href="{% url 'ipam:ipaddress_add' %}?address={{ record.1 }}{% if prefix.vrf %}&vrf={{ prefix.vrf.pk }}{% endif %}{% if prefix.tenant %}&tenant={{ prefix.tenant.pk }}{% endif %}" class="btn btn-xs btn-success">{% if record.0 <= 65536 %}{{ record.0 }}{% else %}Many{% endif %} IP{{ record.0|pluralize }} available</a> <a href="{% url 'ipam:ipaddress_add' %}?address={{ record.1 }}{% if object.vrf %}&vrf={{ object.vrf.pk }}{% endif %}{% if object.tenant %}&tenant={{ object.tenant.pk }}{% endif %}" class="btn btn-xs btn-success">{% if record.0 <= 65536 %}{{ record.0 }}{% else %}Many{% endif %} IP{{ record.0|pluralize }} available</a>
{% else %} {% else %}
{% 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
{% endif %} {% endif %}
@ -46,8 +46,8 @@ IPADDRESS_ASSIGN_LINK = """
VRF_LINK = """ VRF_LINK = """
{% if record.vrf %} {% if record.vrf %}
<a href="{{ record.vrf.get_absolute_url }}">{{ record.vrf }}</a> <a href="{{ record.vrf.get_absolute_url }}">{{ record.vrf }}</a>
{% elif prefix.vrf %} {% elif object.vrf %}
{{ prefix.vrf }} <a href="{{ object.vrf.get_absolute_url }}">{{ object.vrf }}</a>
{% else %} {% else %}
Global Global
{% endif %} {% endif %}
@ -401,6 +401,9 @@ class InterfaceIPAddressTable(BaseTable):
) )
status = ChoiceFieldColumn() status = ChoiceFieldColumn()
tenant = TenantColumn() tenant = TenantColumn()
actions = ButtonsColumn(
model=IPAddress
)
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = IPAddress model = IPAddress

View File

@ -429,6 +429,11 @@ class PrefixTestCase(TestCase):
params = {'family': '6'} params = {'family': '6'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5) 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): def test_is_pool(self):
params = {'is_pool': 'true'} params = {'is_pool': 'true'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)

View File

@ -30,6 +30,7 @@ class VRFView(generic.ObjectView):
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
prefix_count = Prefix.objects.restrict(request.user, 'view').filter(vrf=instance).count() 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( import_targets_table = tables.RouteTargetTable(
instance.import_targets.prefetch_related('tenant'), instance.import_targets.prefetch_related('tenant'),
@ -42,6 +43,7 @@ class VRFView(generic.ObjectView):
return { return {
'prefix_count': prefix_count, 'prefix_count': prefix_count,
'ipaddress_count': ipaddress_count,
'import_targets_table': import_targets_table, 'import_targets_table': import_targets_table,
'export_targets_table': export_targets_table, 'export_targets_table': export_targets_table,
} }

View File

@ -1,6 +1,7 @@
{% extends 'generic/object.html' %} {% extends 'generic/object.html' %}
{% load helpers %} {% load helpers %}
{% load perms %} {% load perms %}
{% load custom_links %}
{% load plugins %} {% load plugins %}
{% block title %}{{ object.device }} / {{ object }}{% endblock %} {% block title %}{{ object.device }} / {{ object }}{% endblock %}

View File

@ -1,6 +1,7 @@
{% extends 'dcim/device_component.html' %} {% extends 'dcim/device_component.html' %}
{% load helpers %} {% load helpers %}
{% load plugins %} {% load plugins %}
{% load render_table from django_tables2 %}
{% block content %} {% block content %}
<div class="row"> <div class="row">
@ -241,7 +242,23 @@
</div> </div>
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
{% include 'panel_table.html' with table=ipaddress_table heading="IP Addresses" %} <div class="panel panel-default">
<div class="panel-heading">
<strong>IP Addresses</strong>
</div>
{% if ipaddress_table.rows %}
{% render_table ipaddress_table 'inc/table.html' %}
{% else %}
<div class="panel-body text-muted">None</div>
{% endif %}
{% if perms.ipam.add_ipaddress %}
<div class="panel-footer text-right noprint">
<a href="{% url 'ipam:ipaddress_add' %}?device={{ object.device.pk }}&interface={{ object.pk }}" class="btn btn-xs btn-primary">
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add IP Address
</a>
</div>
{% endif %}
</div>
</div> </div>
</div> </div>
<div class="row"> <div class="row">

View File

@ -52,6 +52,12 @@
<td> <td>
<a href="{% url 'ipam:prefix_list' %}?vrf_id={{ object.pk }}">{{ prefix_count }}</a> <a href="{% url 'ipam:prefix_list' %}?vrf_id={{ object.pk }}">{{ prefix_count }}</a>
</td> </td>
</tr>
<tr>
<td>IP Addresses</td>
<td>
<a href="{% url 'ipam:ipaddress_list' %}?vrf_id={{ object.pk }}">{{ ipaddress_count }}</a>
</td>
</tr> </tr>
</table> </table>
</div> </div>

View File

@ -1,6 +1,7 @@
{% extends 'generic/object.html' %} {% extends 'generic/object.html' %}
{% load helpers %} {% load helpers %}
{% load plugins %} {% load plugins %}
{% load render_table from django_tables2 %}
{% block title %}{{ object.virtual_machine }} / {{ object.name }}{% endblock %} {% block title %}{{ object.virtual_machine }} / {{ object.name }}{% endblock %}
@ -65,7 +66,23 @@
</div> </div>
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
{% include 'panel_table.html' with table=ipaddress_table heading="IP Addresses" %} <div class="panel panel-default">
<div class="panel-heading">
<strong>IP Addresses</strong>
</div>
{% if ipaddress_table.rows %}
{% render_table ipaddress_table 'inc/table.html' %}
{% else %}
<div class="panel-body text-muted">None</div>
{% endif %}
{% if perms.ipam.add_ipaddress %}
<div class="panel-footer text-right noprint">
<a href="{% url 'ipam:ipaddress_add' %}?virtual_machine={{ object.virtual_machine.pk }}&vminterface={{ object.pk }}" class="btn btn-xs btn-primary">
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add IP Address
</a>
</div>
{% endif %}
</div>
</div> </div>
</div> </div>
<div class="row"> <div class="row">

View File

@ -223,7 +223,7 @@ class ObjectTypeListFilter(admin.SimpleListFilter):
parameter_name = 'object_type' parameter_name = 'object_type'
def lookups(self, request, model_admin): def lookups(self, request, model_admin):
object_types = ObjectPermission.objects.values_list('id', flat=True).distinct() object_types = ObjectPermission.objects.values_list('object_types__pk', flat=True).distinct()
content_types = ContentType.objects.filter(pk__in=object_types).order_by('app_label', 'model') content_types = ContentType.objects.filter(pk__in=object_types).order_by('app_label', 'model')
return [ return [
(ct.pk, ct) for ct in content_types (ct.pk, ct) for ct in content_types

View File

@ -1,5 +1,5 @@
import django_tables2 as tables import django_tables2 as tables
from django.conf import settings
from dcim.tables.devices import BaseInterfaceTable from dcim.tables.devices import BaseInterfaceTable
from tenancy.tables import TenantColumn from tenancy.tables import TenantColumn
from utilities.tables import ( from utilities.tables import (
@ -123,10 +123,18 @@ class VirtualMachineDetailTable(VirtualMachineTable):
linkify=True, linkify=True,
verbose_name='IPv6 Address' verbose_name='IPv6 Address'
) )
primary_ip = tables.Column( if settings.PREFER_IPV4:
linkify=True, primary_ip = tables.Column(
verbose_name='IP Address' 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'
)
tags = TagColumn( tags = TagColumn(
url_name='virtualization:virtualmachine_list' url_name='virtualization:virtualmachine_list'
) )

View File

@ -1,24 +1,24 @@
Django==3.2b1 Django==3.2b1
django-cacheops==5.1 django-cacheops==5.1
django-cors-headers==3.5.0 django-cors-headers==3.7.0
django-debug-toolbar==3.1.1 django-debug-toolbar==3.2
django-filter==2.4.0 django-filter==2.4.0
django-mptt==0.11.0 django-mptt==0.12.0
django-pglocks==1.0.4 django-pglocks==1.0.4
django-prometheus==2.1.0 django-prometheus==2.1.0
django-rq==2.4.0 django-rq==2.4.0
django-tables2==2.3.3 django-tables2==2.3.4
django-taggit==1.3.0 django-taggit==1.3.0
django-timezone-field==4.0 django-timezone-field==4.1.1
djangorestframework==3.12.2 djangorestframework==3.12.2
drf-yasg[validation]==1.20.0 drf-yasg[validation]==1.20.0
gunicorn==20.0.4 gunicorn==20.0.4
Jinja2==2.11.2 Jinja2==2.11.3
Markdown==3.3.3 Markdown==3.3.4
netaddr==0.8.0 netaddr==0.8.0
Pillow==8.0.1 Pillow==8.1.2
psycopg2-binary==2.8.6 psycopg2-binary==2.8.6
pycryptodome==3.9.9 pycryptodome==3.10.1
PyYAML==5.3.1 PyYAML==5.4.1
svgwrite==1.4 svgwrite==1.4.1
tablib==3.0.0 tablib==3.0.0