diff --git a/.github/ISSUE_TEMPLATE/documentation_change.yaml b/.github/ISSUE_TEMPLATE/documentation_change.yaml index 3b2026b34..bff755719 100644 --- a/.github/ISSUE_TEMPLATE/documentation_change.yaml +++ b/.github/ISSUE_TEMPLATE/documentation_change.yaml @@ -14,17 +14,19 @@ body: - Cleanup (formatting, typos, etc.) validations: required: true - - type: checkboxes + - type: dropdown attributes: label: Area - description: To what section(s) of the documentation does this change pertain? + description: To what section of the documentation does this change primarily pertain? options: - - label: Installation instructions - - label: Configuration parameters - - label: Functionality/features - - label: REST API - - label: Administration/development - - label: Other + - Installation instructions + - Configuration parameters + - Functionality/features + - REST API + - Administration/development + - Other + validations: + required: true - type: textarea attributes: label: Proposed Changes diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cd62c5ab7..009e6586c 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 [NetDev Community](https://join.slack.com/t/netdev-community/shared_invite/zt-mtts8g0n-Sm6Wutn62q_M4OdsaIycrQ). +For real-time chat, you can join the **#netbox** Slack channel on [NetDev Community](https://slack.netbox.dev/). 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. diff --git a/README.md b/README.md index 880fa8c08..e35b72c2e 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ The complete documentation for NetBox can be found at [Read the Docs](https://ne ### 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 +* [Slack](https://slack.netbox.dev/) - 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/caching.md b/docs/additional-features/caching.md index 359d65202..ebce63578 100644 --- a/docs/additional-features/caching.md +++ b/docs/additional-features/caching.md @@ -1,6 +1,6 @@ # Caching -NetBox supports database query caching using [django-cacheops](https://github.com/Suor/django-cacheops) and Redis. When a query is made, the results are cached in Redis for a short period of time, as defined by the [CACHE_TIMEOUT](../../configuration/optional-settings/#cache_timeout) parameter (15 minutes by default). Within that time, all recurrences of that specific query will return the pre-fetched results from the cache. +NetBox supports database query caching using [django-cacheops](https://github.com/Suor/django-cacheops) and Redis. When a query is made, the results are cached in Redis for a short period of time, as defined by the [CACHE_TIMEOUT](../configuration/optional-settings.md#cache_timeout) parameter (15 minutes by default). Within that time, all recurrences of that specific query will return the pre-fetched results from the cache. If a change is made to any of the objects returned by the query within that time, or if the timeout expires, the results are automatically invalidated and the next request for those results will be sent to the database. diff --git a/docs/additional-features/napalm.md b/docs/additional-features/napalm.md index c8a3665b9..d2d69fc3c 100644 --- a/docs/additional-features/napalm.md +++ b/docs/additional-features/napalm.md @@ -22,7 +22,7 @@ GET /api/dcim/devices/1/napalm/?method=get_environment ## Authentication -By default, the [`NAPALM_USERNAME`](../../configuration/optional-settings/#napalm_username) and [`NAPALM_PASSWORD`](../../configuration/optional-settings/#napalm_password) configuration parameters are used for NAPALM authentication. They can be overridden for an individual API call by specifying the `X-NAPALM-Username` and `X-NAPALM-Password` headers. +By default, the [`NAPALM_USERNAME`](../configuration/optional-settings.md#napalm_username) and [`NAPALM_PASSWORD`](../configuration/optional-settings.md#napalm_password) configuration parameters are used for NAPALM authentication. They can be overridden for an individual API call by specifying the `X-NAPALM-Username` and `X-NAPALM-Password` headers. ``` $ curl "http://localhost/api/dcim/devices/1/napalm/?method=get_environment" \ diff --git a/docs/additional-features/reports.md b/docs/additional-features/reports.md index 1266c22ec..bac1003fd 100644 --- a/docs/additional-features/reports.md +++ b/docs/additional-features/reports.md @@ -12,7 +12,7 @@ A NetBox report is a mechanism for validating the integrity of data within NetBo ## Writing Reports -Reports must be saved as files in the [`REPORTS_ROOT`](../../configuration/optional-settings/#reports_root) path (which defaults to `netbox/reports/`). Each file created within this path is considered a separate module. Each module holds one or more reports (Python classes), each of which performs a certain function. The logic of each report is broken into discrete test methods, each of which applies a small portion of the logic comprising the overall test. +Reports must be saved as files in the [`REPORTS_ROOT`](../configuration/optional-settings.md#reports_root) path (which defaults to `netbox/reports/`). Each file created within this path is considered a separate module. Each module holds one or more reports (Python classes), each of which performs a certain function. The logic of each report is broken into discrete test methods, each of which applies a small portion of the logic comprising the overall test. !!! warning The reports path includes a file named `__init__.py`, which registers the path as a Python module. Do not delete this file. diff --git a/docs/administration/replicating-netbox.md b/docs/administration/replicating-netbox.md index 23c1082bc..ee956edf5 100644 --- a/docs/administration/replicating-netbox.md +++ b/docs/administration/replicating-netbox.md @@ -12,13 +12,16 @@ NetBox employs a [PostgreSQL](https://www.postgresql.org/) database, so general Use the `pg_dump` utility to export the entire database to a file: ```no-highlight -pg_dump netbox > netbox.sql +pg_dump --username netbox --password --host localhost netbox > netbox.sql ``` +!!! note + You may need to change the username, host, and/or database in the command above to match your installation. + When replicating a production database for development purposes, you may find it convenient to exclude changelog data, which can easily account for the bulk of a database's size. To do this, exclude the `extras_objectchange` table data from the export. The table will still be included in the output file, but will not be populated with any data. ```no-highlight -pg_dump --exclude-table-data=extras_objectchange netbox > netbox.sql +pg_dump ... --exclude-table-data=extras_objectchange netbox > netbox.sql ``` ### Load an Exported Database @@ -41,7 +44,7 @@ Keep in mind that PostgreSQL user accounts and permissions are not included with If you want to export only the database schema, and not the data itself (e.g. for development reference), do the following: ```no-highlight -pg_dump -s netbox > netbox_schema.sql +pg_dump --username netbox --password --host localhost -s netbox > netbox_schema.sql ``` --- diff --git a/docs/configuration/optional-settings.md b/docs/configuration/optional-settings.md index 4af83493e..4ed3d946e 100644 --- a/docs/configuration/optional-settings.md +++ b/docs/configuration/optional-settings.md @@ -281,6 +281,14 @@ Setting this to True will display a "maintenance mode" banner at the top of ever --- +## MAPS_URL + +Default: `https://maps.google.com/?q=` (Google Maps) + +This specifies the URL to use when presenting a map of a physical location by street address or GPS coordinates. The URL must accept either a free-form street address or a comma-separated pair of numeric coordinates appended to it. + +--- + ## MAX_PAGE_SIZE Default: 1000 @@ -301,7 +309,7 @@ The file path to the location where media files (such as image attachments) are Default: False -Toggle the availability Prometheus-compatible metrics at `/metrics`. See the [Prometheus Metrics](../../additional-features/prometheus-metrics/) documentation for more details. +Toggle the availability Prometheus-compatible metrics at `/metrics`. See the [Prometheus Metrics](../additional-features/prometheus-metrics.md) documentation for more details. --- diff --git a/docs/development/getting-started.md b/docs/development/getting-started.md index 9b2249653..beeae2ffb 100644 --- a/docs/development/getting-started.md +++ b/docs/development/getting-started.md @@ -5,8 +5,8 @@ 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 PostgreSQL server, which can be installed locally [per the documentation](/installation/1-postgresql/) -* A Redis server, which can also be [installed locally](/installation/2-redis/) +* 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 ### Fork the Repo diff --git a/docs/development/index.md b/docs/development/index.md index bbcb1eac8..e9758e74b 100644 --- a/docs/development/index.md +++ b/docs/development/index.md @@ -8,7 +8,7 @@ There are several official forums for communication among the developers and com * [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. -* [#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 NetDev Community Slack](https://slack.netbox.dev/) - 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/installation/3-netbox.md b/docs/installation/3-netbox.md index f2461b40d..65b0b8a2d 100644 --- a/docs/installation/3-netbox.md +++ b/docs/installation/3-netbox.md @@ -113,7 +113,7 @@ cd /opt/netbox/netbox/netbox/ sudo cp configuration.example.py configuration.py ``` -Open `configuration.py` with your preferred editor to begin configuring NetBox. NetBox offers [many configuration parameters](/configuration/), but only the following four are required for new installations: +Open `configuration.py` with your preferred editor to begin configuring NetBox. NetBox offers [many configuration parameters](../configuration/index.md), but only the following four are required for new installations: * `ALLOWED_HOSTS` * `DATABASE` @@ -136,7 +136,7 @@ ALLOWED_HOSTS = ['*'] ### DATABASE -This parameter holds the database configuration details. You must define the username and password used when you configured PostgreSQL. If the service is running on a remote host, update the `HOST` and `PORT` parameters accordingly. See the [configuration documentation](/configuration/required-settings/#database) for more detail on individual parameters. +This parameter holds the database configuration details. You must define the username and password used when you configured PostgreSQL. If the service is running on a remote host, update the `HOST` and `PORT` parameters accordingly. See the [configuration documentation](../configuration/required-settings.md#database) for more detail on individual parameters. ```python DATABASE = { @@ -151,7 +151,7 @@ DATABASE = { ### REDIS -Redis is a in-memory key-value store used by NetBox for caching and background task queuing. Redis typically requires minimal configuration; the values below should suffice for most installations. See the [configuration documentation](/configuration/required-settings/#redis) for more detail on individual parameters. +Redis is a in-memory key-value store used by NetBox for caching and background task queuing. Redis typically requires minimal configuration; the values below should suffice for most installations. See the [configuration documentation](../configuration/required-settings.md#redis) for more detail on individual parameters. Note that NetBox requires the specification of two separate Redis databases: `tasks` and `caching`. These may both be provided by the same Redis service, however each should have a unique numeric database ID. @@ -203,7 +203,7 @@ sudo echo napalm >> /opt/netbox/local_requirements.txt ### Remote File Storage -By default, NetBox will use the local filesystem to store uploaded files. To use a remote filesystem, install the [`django-storages`](https://django-storages.readthedocs.io/en/stable/) library and configure your [desired storage backend](/configuration/optional-settings/#storage_backend) in `configuration.py`. +By default, NetBox will use the local filesystem to store uploaded files. To use a remote filesystem, install the [`django-storages`](https://django-storages.readthedocs.io/en/stable/) library and configure your [desired storage backend](../configuration/optional-settings.md#storage_backend) in `configuration.py`. ```no-highlight sudo echo django-storages >> /opt/netbox/local_requirements.txt diff --git a/docs/installation/6-ldap.md b/docs/installation/6-ldap.md index 1c5424595..5d5135b33 100644 --- a/docs/installation/6-ldap.md +++ b/docs/installation/6-ldap.md @@ -142,7 +142,7 @@ AUTH_LDAP_CACHE_TIMEOUT = 3600 `systemctl restart netbox` restarts the NetBox service, and initiates any changes made to `ldap_config.py`. If there are syntax errors present, the NetBox process will not spawn an instance, and errors should be logged to `/var/log/messages`. -For troubleshooting LDAP user/group queries, add or merge the following [logging](/configuration/optional-settings.md#logging) configuration to `configuration.py`: +For troubleshooting LDAP user/group queries, add or merge the following [logging](../configuration/optional-settings.md#logging) configuration to `configuration.py`: ```python LOGGING = { diff --git a/docs/installation/upgrading.md b/docs/installation/upgrading.md index cdd20f01b..e824ad7ab 100644 --- a/docs/installation/upgrading.md +++ b/docs/installation/upgrading.md @@ -2,7 +2,7 @@ ## Review the Release Notes -Prior to upgrading your NetBox instance, be sure to carefully review all [release notes](../../release-notes/) that have been published since your current version was released. Although the upgrade process typically does not involve additional work, certain releases may introduce breaking or backward-incompatible changes. These are called out in the release notes under the release in which the change went into effect. +Prior to upgrading your NetBox instance, be sure to carefully review all [release notes](../release-notes/index.md) that have been published since your current version was released. Although the upgrade process typically does not involve additional work, certain releases may introduce breaking or backward-incompatible changes. These are called out in the release notes under the release in which the change went into effect. ## Update Dependencies to Required Versions diff --git a/docs/release-notes/version-2.10.md b/docs/release-notes/version-2.10.md index 2b3792204..81a68dbd3 100644 --- a/docs/release-notes/version-2.10.md +++ b/docs/release-notes/version-2.10.md @@ -1,5 +1,29 @@ # NetBox v2.10 +## v2.10.7 (2021-03-25) + +### Enhancements + +* [#5641](https://github.com/netbox-community/netbox/issues/5641) - Allow filtering device components by label +* [#5723](https://github.com/netbox-community/netbox/issues/5723) - Allow customization of the geographic mapping service via `MAPS_URL` config parameter +* [#5736](https://github.com/netbox-community/netbox/issues/5736) - Allow changing site assignment when bulk editing devices +* [#5953](https://github.com/netbox-community/netbox/issues/5953) - Support Markdown rendering for custom script descriptions +* [#6040](https://github.com/netbox-community/netbox/issues/6040) - Add UI search fields for asset tag for devices and racks + +### Bug Fixes + +* [#5595](https://github.com/netbox-community/netbox/issues/5595) - Restore ability to delete an uploaded device type image +* [#5650](https://github.com/netbox-community/netbox/issues/5650) - Denote when the total length of a cable trace may exceed the indicated value +* [#5962](https://github.com/netbox-community/netbox/issues/5962) - Ensure consistent display of change log action labels +* [#5966](https://github.com/netbox-community/netbox/issues/5966) - Skip Markdown reference link when tabbing through form fields +* [#5977](https://github.com/netbox-community/netbox/issues/5977) - Correct validation of `RELEASE_CHECK_URL` config parameter +* [#6006](https://github.com/netbox-community/netbox/issues/6006) - Fix VLAN group/site association for bulk prefix import +* [#6010](https://github.com/netbox-community/netbox/issues/6010) - Eliminate duplicate virtual chassis search results +* [#6012](https://github.com/netbox-community/netbox/issues/6012) - Pre-populate attributes when creating an available child prefix via the UI +* [#6023](https://github.com/netbox-community/netbox/issues/6023) - Fix display of bottom banner with uBlock Origin enabled + +--- + ## v2.10.6 (2021-03-09) ### Enhancements @@ -19,6 +43,8 @@ * [#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/docs/rest-api/authentication.md b/docs/rest-api/authentication.md index 5d5777483..7fb789e0f 100644 --- a/docs/rest-api/authentication.md +++ b/docs/rest-api/authentication.md @@ -20,7 +20,7 @@ http://netbox/api/dcim/sites/ } ``` -A token is not required for read-only operations which have been exempted from permissions enforcement (using the [`EXEMPT_VIEW_PERMISSIONS`](../../configuration/optional-settings/#exempt_view_permissions) configuration parameter). However, if a token _is_ required but not present in a request, the API will return a 403 (Forbidden) response: +A token is not required for read-only operations which have been exempted from permissions enforcement (using the [`EXEMPT_VIEW_PERMISSIONS`](../configuration/optional-settings.md#exempt_view_permissions) configuration parameter). However, if a token _is_ required but not present in a request, the API will return a 403 (Forbidden) response: ``` $ curl http://netbox/api/dcim/sites/ diff --git a/docs/rest-api/overview.md b/docs/rest-api/overview.md index 290343aa6..735e0713b 100644 --- a/docs/rest-api/overview.md +++ b/docs/rest-api/overview.md @@ -269,7 +269,7 @@ The brief format is supported for both lists and individual objects. ### Excluding Config Contexts -When retrieving devices and virtual machines via the REST API, each will included its rendered [configuration context data](../models/extras/configcontext/) by default. Users with large amounts of context data will likely observe suboptimal performance when returning multiple objects, particularly with very high page sizes. To combat this, context data may be excluded from the response data by attaching the query parameter `?exclude=config_context` to the request. This parameter works for both list and detail views. +When retrieving devices and virtual machines via the REST API, each will included its rendered [configuration context data](../models/extras/configcontext.md) by default. Users with large amounts of context data will likely observe suboptimal performance when returning multiple objects, particularly with very high page sizes. To combat this, context data may be excluded from the response data by attaching the query parameter `?exclude=config_context` to the request. This parameter works for both list and detail views. ## Pagination @@ -308,7 +308,7 @@ Vary: Accept } ``` -The default page is determined by the [`PAGINATE_COUNT`](../../configuration/optional-settings/#paginate_count) configuration parameter, which defaults to 50. However, this can be overridden per request by specifying the desired `offset` and `limit` query parameters. For example, if you wish to retrieve a hundred devices at a time, you would make a request for: +The default page is determined by the [`PAGINATE_COUNT`](../configuration/optional-settings.md#paginate_count) configuration parameter, which defaults to 50. However, this can be overridden per request by specifying the desired `offset` and `limit` query parameters. For example, if you wish to retrieve a hundred devices at a time, you would make a request for: ``` http://netbox/api/dcim/devices/?limit=100 @@ -325,7 +325,7 @@ The response will return devices 1 through 100. The URL provided in the `next` a } ``` -The maximum number of objects that can be returned is limited by the [`MAX_PAGE_SIZE`](../../configuration/optional-settings/#max_page_size) configuration parameter, which is 1000 by default. Setting this to `0` or `None` will remove the maximum limit. An API consumer can then pass `?limit=0` to retrieve _all_ matching objects with a single request. +The maximum number of objects that can be returned is limited by the [`MAX_PAGE_SIZE`](../configuration/optional-settings.md#max_page_size) configuration parameter, which is 1000 by default. Setting this to `0` or `None` will remove the maximum limit. An API consumer can then pass `?limit=0` to retrieve _all_ matching objects with a single request. !!! warning Disabling the page size limit introduces a potential for very resource-intensive requests, since one API request can effectively retrieve an entire table from the database. @@ -387,7 +387,7 @@ curl -s -X GET http://netbox/api/ipam/ip-addresses/5618/ | jq '.' ### Creating a New Object -To create a new object, make a `POST` request to the model's _list_ endpoint with JSON data pertaining to the object being created. Note that a REST API token is required for all write operations; see the [authentication documentation](../authentication/) for more information. Also be sure to set the `Content-Type` HTTP header to `application/json`. +To create a new object, make a `POST` request to the model's _list_ endpoint with JSON data pertaining to the object being created. Note that a REST API token is required for all write operations; see the [authentication documentation](../authentication/index.md) for more information. Also be sure to set the `Content-Type` HTTP header to `application/json`. ```no-highlight curl -s -X POST \ diff --git a/docs/rest-api/working-with-secrets.md b/docs/rest-api/working-with-secrets.md index dafbb7239..5fbbf7355 100644 --- a/docs/rest-api/working-with-secrets.md +++ b/docs/rest-api/working-with-secrets.md @@ -4,7 +4,7 @@ As with most other objects, the REST API can be used to view, create, modify, an ## Generating a Session Key -In order to encrypt or decrypt secret data, a session key must be attached to the API request. To generate a session key, send an authenticated request to the `/api/secrets/get-session-key/` endpoint with the private RSA key which matches your [UserKey](../../core-functionality/secrets/#user-keys). The private key must be POSTed with the name `private_key`. +In order to encrypt or decrypt secret data, a session key must be attached to the API request. To generate a session key, send an authenticated request to the `/api/secrets/get-session-key/` endpoint with the private RSA key which matches your [UserKey](../core-functionality/secrets.md#user-keys). The private key must be POSTed with the name `private_key`. ```no-highlight $ curl -X POST http://netbox/api/secrets/get-session-key/ \ diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 6f497bfa6..dc730488c 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -3,6 +3,7 @@ from django.contrib.contenttypes.models import ContentType from drf_yasg.utils import swagger_serializer_method from rest_framework import serializers from rest_framework.validators import UniqueTogetherValidator +from timezone_field.rest_framework import TimeZoneSerializerField from dcim.choices import * from dcim.constants import * @@ -13,13 +14,12 @@ from dcim.models import ( PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site, VirtualChassis, ) -from dcim.utils import decompile_path_node from extras.api.customfields import CustomFieldModelSerializer from extras.api.serializers import TaggedObjectSerializer from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedVLANSerializer from ipam.models import VLAN from netbox.api import ( - ChoiceField, ContentTypeField, SerializedPKRelatedField, TimeZoneField, ValidatedModelSerializer, + ChoiceField, ContentTypeField, SerializedPKRelatedField, ValidatedModelSerializer, WritableNestedSerializer, ) from tenancy.api.nested_serializers import NestedTenantSerializer @@ -98,7 +98,7 @@ class SiteSerializer(TaggedObjectSerializer, CustomFieldModelSerializer): status = ChoiceField(choices=SiteStatusChoices, required=False) region = NestedRegionSerializer(required=False, allow_null=True) tenant = NestedTenantSerializer(required=False, allow_null=True) - time_zone = TimeZoneField(required=False) + time_zone = TimeZoneSerializerField(required=False) circuit_count = serializers.IntegerField(read_only=True) device_count = serializers.IntegerField(read_only=True) prefix_count = serializers.IntegerField(read_only=True) diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index 41363c261..9c8a8a79a 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -783,7 +783,7 @@ class ConsolePortFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTermina class Meta: model = ConsolePort - fields = ['id', 'name', 'description'] + fields = ['id', 'name', 'label', 'description'] class ConsoleServerPortFilterSet( @@ -799,7 +799,7 @@ class ConsoleServerPortFilterSet( class Meta: model = ConsoleServerPort - fields = ['id', 'name', 'description'] + fields = ['id', 'name', 'label', 'description'] class PowerPortFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet): @@ -810,7 +810,7 @@ class PowerPortFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminati class Meta: model = PowerPort - fields = ['id', 'name', 'maximum_draw', 'allocated_draw', 'description'] + fields = ['id', 'name', 'label', 'maximum_draw', 'allocated_draw', 'description'] class PowerOutletFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet): @@ -821,7 +821,7 @@ class PowerOutletFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTermina class Meta: model = PowerOutlet - fields = ['id', 'name', 'feed_leg', 'description'] + fields = ['id', 'name', 'label', 'feed_leg', 'description'] class InterfaceFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet): @@ -867,7 +867,7 @@ class InterfaceFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminati class Meta: model = Interface - fields = ['id', 'name', 'type', 'enabled', 'mtu', 'mgmt_only', 'mode', 'description'] + fields = ['id', 'name', 'label', 'type', 'enabled', 'mtu', 'mgmt_only', 'mode', 'description'] def filter_device(self, queryset, name, value): try: @@ -921,21 +921,21 @@ class FrontPortFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminati class Meta: model = FrontPort - fields = ['id', 'name', 'type', 'description'] + fields = ['id', 'name', 'label', 'type', 'description'] class RearPortFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet): class Meta: model = RearPort - fields = ['id', 'name', 'type', 'positions', 'description'] + fields = ['id', 'name', 'label', 'type', 'positions', 'description'] class DeviceBayFilterSet(BaseFilterSet, DeviceComponentFilterSet): class Meta: model = DeviceBay - fields = ['id', 'name', 'description'] + fields = ['id', 'name', 'label', 'description'] class InventoryItemFilterSet(BaseFilterSet, DeviceComponentFilterSet): @@ -996,7 +996,7 @@ class InventoryItemFilterSet(BaseFilterSet, DeviceComponentFilterSet): class Meta: model = InventoryItem - fields = ['id', 'name', 'part_id', 'asset_tag', 'discovered'] + fields = ['id', 'name', 'label', 'part_id', 'asset_tag', 'discovered'] def search(self, queryset, name, value): if not value.strip(): @@ -1075,7 +1075,7 @@ class VirtualChassisFilterSet(BaseFilterSet): Q(members__name__icontains=value) | Q(domain__icontains=value) ) - return queryset.filter(qs_filter) + return queryset.filter(qs_filter).distinct() class CableFilterSet(BaseFilterSet): diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 40c16d59f..9b9760ad1 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -60,12 +60,18 @@ def get_device_by_name_or_pk(name): class DeviceComponentFilterForm(BootstrapMixin, forms.Form): field_order = [ - 'q', 'region', 'site' + 'q', 'name', 'label', 'region', 'site' ] q = forms.CharField( required=False, label='Search' ) + name = forms.CharField( + required=False + ) + label = forms.CharField( + required=False + ) region = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), to_field_name='slug', @@ -659,7 +665,7 @@ class RackBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor class RackFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): model = Rack - field_order = ['q', 'region', 'site', 'group_id', 'status', 'role', 'tenant_group', 'tenant'] + field_order = ['q', 'region', 'site', 'group_id', 'status', 'role', 'tenant_group', 'tenant', 'asset_tag'] q = forms.CharField( required=False, label='Search' @@ -707,6 +713,9 @@ class RackFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): required=False, null_option='None' ) + asset_tag = forms.CharField( + required=False + ) tag = TagFilterField(model) @@ -944,10 +953,10 @@ class DeviceTypeForm(BootstrapMixin, CustomFieldModelForm): widgets = { 'subdevice_role': StaticSelect2(), # Exclude SVG images (unsupported by PIL) - 'front_image': forms.FileInput(attrs={ + 'front_image': forms.ClearableFileInput(attrs={ 'accept': 'image/bmp,image/gif,image/jpeg,image/png,image/tiff' }), - 'rear_image': forms.FileInput(attrs={ + 'rear_image': forms.ClearableFileInput(attrs={ 'accept': 'image/bmp,image/gif,image/jpeg,image/png,image/tiff' }) } @@ -2088,6 +2097,10 @@ class DeviceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF queryset=DeviceRole.objects.all(), required=False ) + site = DynamicModelChoiceField( + queryset=Site.objects.all(), + required=False + ) tenant = DynamicModelChoiceField( queryset=Tenant.objects.all(), required=False @@ -2117,7 +2130,7 @@ class DeviceFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilt model = Device field_order = [ 'q', 'region', 'site', 'rack_group_id', 'rack_id', 'status', 'role', 'tenant_group', 'tenant', - 'manufacturer_id', 'device_type_id', 'mac_address', 'has_primary_ip', + 'manufacturer_id', 'device_type_id', 'asset_tag', 'mac_address', 'has_primary_ip', ] q = forms.CharField( required=False, @@ -2185,6 +2198,9 @@ class DeviceFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilt required=False, widget=StaticSelect2Multiple() ) + asset_tag = forms.CharField( + required=False + ) mac_address = forms.CharField( required=False, label='MAC address' diff --git a/netbox/dcim/models/cables.py b/netbox/dcim/models/cables.py index 6a530bb49..e8c9211f5 100644 --- a/netbox/dcim/models/cables.py +++ b/netbox/dcim/models/cables.py @@ -480,17 +480,23 @@ class CablePath(models.Model): def get_total_length(self): """ - Return the sum of the length of each cable in the path. + Return a tuple containing the sum of the length of each cable in the path + and a flag indicating whether the length is definitive. """ cable_ids = [ # Starting from the first element, every third element in the path should be a Cable decompile_path_node(self.path[i])[1] for i in range(0, len(self.path), 3) ] - return Cable.objects.filter(id__in=cable_ids).aggregate(total=Sum('_abs_length'))['total'] + cables = Cable.objects.filter(id__in=cable_ids, _abs_length__isnull=False) + total_length = cables.aggregate(total=Sum('_abs_length'))['total'] + is_definitive = len(cables) == len(cable_ids) + + return total_length, is_definitive def get_split_nodes(self): """ Return all available next segments in a split cable path. """ rearport = path_node_to_object(self.path[-1]) + return FrontPort.objects.filter(rear_port=rearport) diff --git a/netbox/dcim/tests/test_filters.py b/netbox/dcim/tests/test_filters.py index a76788e65..02a44bbc1 100644 --- a/netbox/dcim/tests/test_filters.py +++ b/netbox/dcim/tests/test_filters.py @@ -1488,9 +1488,9 @@ class ConsolePortTestCase(TestCase): ConsoleServerPort.objects.bulk_create(console_server_ports) console_ports = ( - ConsolePort(device=devices[0], name='Console Port 1', description='First'), - ConsolePort(device=devices[1], name='Console Port 2', description='Second'), - ConsolePort(device=devices[2], name='Console Port 3', description='Third'), + ConsolePort(device=devices[0], name='Console Port 1', label='A', description='First'), + ConsolePort(device=devices[1], name='Console Port 2', label='B', description='Second'), + ConsolePort(device=devices[2], name='Console Port 3', label='C', description='Third'), ) ConsolePort.objects.bulk_create(console_ports) @@ -1507,6 +1507,10 @@ class ConsolePortTestCase(TestCase): params = {'name': ['Console Port 1', 'Console Port 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_label(self): + params = {'label': ['A', 'B']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_description(self): params = {'description': ['First', 'Second']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) @@ -1584,9 +1588,9 @@ class ConsoleServerPortTestCase(TestCase): ConsolePort.objects.bulk_create(console_ports) console_server_ports = ( - ConsoleServerPort(device=devices[0], name='Console Server Port 1', description='First'), - ConsoleServerPort(device=devices[1], name='Console Server Port 2', description='Second'), - ConsoleServerPort(device=devices[2], name='Console Server Port 3', description='Third'), + ConsoleServerPort(device=devices[0], name='Console Server Port 1', label='A', description='First'), + ConsoleServerPort(device=devices[1], name='Console Server Port 2', label='B', description='Second'), + ConsoleServerPort(device=devices[2], name='Console Server Port 3', label='C', description='Third'), ) ConsoleServerPort.objects.bulk_create(console_server_ports) @@ -1603,6 +1607,10 @@ class ConsoleServerPortTestCase(TestCase): params = {'name': ['Console Server Port 1', 'Console Server Port 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_label(self): + params = {'label': ['A', 'B']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_description(self): params = {'description': ['First', 'Second']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) @@ -1680,9 +1688,9 @@ class PowerPortTestCase(TestCase): PowerOutlet.objects.bulk_create(power_outlets) power_ports = ( - PowerPort(device=devices[0], name='Power Port 1', maximum_draw=100, allocated_draw=50, description='First'), - PowerPort(device=devices[1], name='Power Port 2', maximum_draw=200, allocated_draw=100, description='Second'), - PowerPort(device=devices[2], name='Power Port 3', maximum_draw=300, allocated_draw=150, description='Third'), + PowerPort(device=devices[0], name='Power Port 1', label='A', maximum_draw=100, allocated_draw=50, description='First'), + PowerPort(device=devices[1], name='Power Port 2', label='B', maximum_draw=200, allocated_draw=100, description='Second'), + PowerPort(device=devices[2], name='Power Port 3', label='C', maximum_draw=300, allocated_draw=150, description='Third'), ) PowerPort.objects.bulk_create(power_ports) @@ -1699,6 +1707,10 @@ class PowerPortTestCase(TestCase): params = {'name': ['Power Port 1', 'Power Port 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_label(self): + params = {'label': ['A', 'B']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_description(self): params = {'description': ['First', 'Second']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) @@ -1784,9 +1796,9 @@ class PowerOutletTestCase(TestCase): PowerPort.objects.bulk_create(power_ports) power_outlets = ( - PowerOutlet(device=devices[0], name='Power Outlet 1', feed_leg=PowerOutletFeedLegChoices.FEED_LEG_A, description='First'), - PowerOutlet(device=devices[1], name='Power Outlet 2', feed_leg=PowerOutletFeedLegChoices.FEED_LEG_B, description='Second'), - PowerOutlet(device=devices[2], name='Power Outlet 3', feed_leg=PowerOutletFeedLegChoices.FEED_LEG_C, description='Third'), + PowerOutlet(device=devices[0], name='Power Outlet 1', label='A', feed_leg=PowerOutletFeedLegChoices.FEED_LEG_A, description='First'), + PowerOutlet(device=devices[1], name='Power Outlet 2', label='B', feed_leg=PowerOutletFeedLegChoices.FEED_LEG_B, description='Second'), + PowerOutlet(device=devices[2], name='Power Outlet 3', label='C', feed_leg=PowerOutletFeedLegChoices.FEED_LEG_C, description='Third'), ) PowerOutlet.objects.bulk_create(power_outlets) @@ -1803,6 +1815,10 @@ class PowerOutletTestCase(TestCase): params = {'name': ['Power Outlet 1', 'Power Outlet 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_label(self): + params = {'label': ['A', 'B']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_description(self): params = {'description': ['First', 'Second']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) @@ -1879,12 +1895,12 @@ class InterfaceTestCase(TestCase): Device.objects.bulk_create(devices) interfaces = ( - Interface(device=devices[0], name='Interface 1', type=InterfaceTypeChoices.TYPE_1GE_SFP, enabled=True, mgmt_only=True, mtu=100, mode=InterfaceModeChoices.MODE_ACCESS, mac_address='00-00-00-00-00-01', description='First'), - Interface(device=devices[1], name='Interface 2', type=InterfaceTypeChoices.TYPE_1GE_GBIC, enabled=True, mgmt_only=True, mtu=200, mode=InterfaceModeChoices.MODE_TAGGED, mac_address='00-00-00-00-00-02', description='Second'), - Interface(device=devices[2], name='Interface 3', type=InterfaceTypeChoices.TYPE_1GE_FIXED, enabled=False, mgmt_only=False, mtu=300, mode=InterfaceModeChoices.MODE_TAGGED_ALL, mac_address='00-00-00-00-00-03', description='Third'), - Interface(device=devices[3], name='Interface 4', type=InterfaceTypeChoices.TYPE_OTHER, enabled=True, mgmt_only=True), - Interface(device=devices[3], name='Interface 5', type=InterfaceTypeChoices.TYPE_OTHER, enabled=True, mgmt_only=True), - Interface(device=devices[3], name='Interface 6', type=InterfaceTypeChoices.TYPE_OTHER, enabled=False, mgmt_only=False), + Interface(device=devices[0], name='Interface 1', label='A', type=InterfaceTypeChoices.TYPE_1GE_SFP, enabled=True, mgmt_only=True, mtu=100, mode=InterfaceModeChoices.MODE_ACCESS, mac_address='00-00-00-00-00-01', description='First'), + Interface(device=devices[1], name='Interface 2', label='B', type=InterfaceTypeChoices.TYPE_1GE_GBIC, enabled=True, mgmt_only=True, mtu=200, mode=InterfaceModeChoices.MODE_TAGGED, mac_address='00-00-00-00-00-02', description='Second'), + Interface(device=devices[2], name='Interface 3', label='C', type=InterfaceTypeChoices.TYPE_1GE_FIXED, enabled=False, mgmt_only=False, mtu=300, mode=InterfaceModeChoices.MODE_TAGGED_ALL, mac_address='00-00-00-00-00-03', description='Third'), + Interface(device=devices[3], name='Interface 4', label='D', type=InterfaceTypeChoices.TYPE_OTHER, enabled=True, mgmt_only=True), + Interface(device=devices[3], name='Interface 5', label='E', type=InterfaceTypeChoices.TYPE_OTHER, enabled=True, mgmt_only=True), + Interface(device=devices[3], name='Interface 6', label='F', type=InterfaceTypeChoices.TYPE_OTHER, enabled=False, mgmt_only=False), ) Interface.objects.bulk_create(interfaces) @@ -1901,6 +1917,10 @@ class InterfaceTestCase(TestCase): params = {'name': ['Interface 1', 'Interface 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_label(self): + params = {'label': ['A', 'B']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_connected(self): params = {'connected': True} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) @@ -2016,12 +2036,12 @@ class FrontPortTestCase(TestCase): RearPort.objects.bulk_create(rear_ports) front_ports = ( - FrontPort(device=devices[0], name='Front Port 1', type=PortTypeChoices.TYPE_8P8C, rear_port=rear_ports[0], rear_port_position=1, description='First'), - FrontPort(device=devices[1], name='Front Port 2', type=PortTypeChoices.TYPE_110_PUNCH, rear_port=rear_ports[1], rear_port_position=2, description='Second'), - FrontPort(device=devices[2], name='Front Port 3', type=PortTypeChoices.TYPE_BNC, rear_port=rear_ports[2], rear_port_position=3, description='Third'), - FrontPort(device=devices[3], name='Front Port 4', type=PortTypeChoices.TYPE_FC, rear_port=rear_ports[3], rear_port_position=1), - FrontPort(device=devices[3], name='Front Port 5', type=PortTypeChoices.TYPE_FC, rear_port=rear_ports[4], rear_port_position=1), - FrontPort(device=devices[3], name='Front Port 6', type=PortTypeChoices.TYPE_FC, rear_port=rear_ports[5], rear_port_position=1), + FrontPort(device=devices[0], name='Front Port 1', label='A', type=PortTypeChoices.TYPE_8P8C, rear_port=rear_ports[0], rear_port_position=1, description='First'), + FrontPort(device=devices[1], name='Front Port 2', label='B', type=PortTypeChoices.TYPE_110_PUNCH, rear_port=rear_ports[1], rear_port_position=2, description='Second'), + FrontPort(device=devices[2], name='Front Port 3', label='C', type=PortTypeChoices.TYPE_BNC, rear_port=rear_ports[2], rear_port_position=3, description='Third'), + FrontPort(device=devices[3], name='Front Port 4', label='D', type=PortTypeChoices.TYPE_FC, rear_port=rear_ports[3], rear_port_position=1), + FrontPort(device=devices[3], name='Front Port 5', label='E', type=PortTypeChoices.TYPE_FC, rear_port=rear_ports[4], rear_port_position=1), + FrontPort(device=devices[3], name='Front Port 6', label='F', type=PortTypeChoices.TYPE_FC, rear_port=rear_ports[5], rear_port_position=1), ) FrontPort.objects.bulk_create(front_ports) @@ -2038,6 +2058,10 @@ class FrontPortTestCase(TestCase): params = {'name': ['Front Port 1', 'Front Port 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_label(self): + params = {'label': ['A', 'B']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_type(self): # TODO: Test for multiple values params = {'type': PortTypeChoices.TYPE_8P8C} @@ -2108,12 +2132,12 @@ class RearPortTestCase(TestCase): Device.objects.bulk_create(devices) rear_ports = ( - RearPort(device=devices[0], name='Rear Port 1', type=PortTypeChoices.TYPE_8P8C, positions=1, description='First'), - RearPort(device=devices[1], name='Rear Port 2', type=PortTypeChoices.TYPE_110_PUNCH, positions=2, description='Second'), - RearPort(device=devices[2], name='Rear Port 3', type=PortTypeChoices.TYPE_BNC, positions=3, description='Third'), - RearPort(device=devices[3], name='Rear Port 4', type=PortTypeChoices.TYPE_FC, positions=4), - RearPort(device=devices[3], name='Rear Port 5', type=PortTypeChoices.TYPE_FC, positions=5), - RearPort(device=devices[3], name='Rear Port 6', type=PortTypeChoices.TYPE_FC, positions=6), + RearPort(device=devices[0], name='Rear Port 1', label='A', type=PortTypeChoices.TYPE_8P8C, positions=1, description='First'), + RearPort(device=devices[1], name='Rear Port 2', label='B', type=PortTypeChoices.TYPE_110_PUNCH, positions=2, description='Second'), + RearPort(device=devices[2], name='Rear Port 3', label='C', type=PortTypeChoices.TYPE_BNC, positions=3, description='Third'), + RearPort(device=devices[3], name='Rear Port 4', label='D', type=PortTypeChoices.TYPE_FC, positions=4), + RearPort(device=devices[3], name='Rear Port 5', label='E', type=PortTypeChoices.TYPE_FC, positions=5), + RearPort(device=devices[3], name='Rear Port 6', label='F', type=PortTypeChoices.TYPE_FC, positions=6), ) RearPort.objects.bulk_create(rear_ports) @@ -2130,6 +2154,10 @@ class RearPortTestCase(TestCase): params = {'name': ['Rear Port 1', 'Rear Port 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_label(self): + params = {'label': ['A', 'B']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_type(self): # TODO: Test for multiple values params = {'type': PortTypeChoices.TYPE_8P8C} @@ -2203,9 +2231,9 @@ class DeviceBayTestCase(TestCase): Device.objects.bulk_create(devices) device_bays = ( - DeviceBay(device=devices[0], name='Device Bay 1', description='First'), - DeviceBay(device=devices[1], name='Device Bay 2', description='Second'), - DeviceBay(device=devices[2], name='Device Bay 3', description='Third'), + DeviceBay(device=devices[0], name='Device Bay 1', label='A', description='First'), + DeviceBay(device=devices[1], name='Device Bay 2', label='B', description='Second'), + DeviceBay(device=devices[2], name='Device Bay 3', label='C', description='Third'), ) DeviceBay.objects.bulk_create(device_bays) @@ -2217,6 +2245,10 @@ class DeviceBayTestCase(TestCase): params = {'name': ['Device Bay 1', 'Device Bay 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_label(self): + params = {'label': ['A', 'B']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_description(self): params = {'description': ['First', 'Second']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) @@ -2283,9 +2315,9 @@ class InventoryItemTestCase(TestCase): Device.objects.bulk_create(devices) inventory_items = ( - InventoryItem(device=devices[0], manufacturer=manufacturers[0], name='Inventory Item 1', part_id='1001', serial='ABC', asset_tag='1001', discovered=True, description='First'), - InventoryItem(device=devices[1], manufacturer=manufacturers[1], name='Inventory Item 2', part_id='1002', serial='DEF', asset_tag='1002', discovered=True, description='Second'), - InventoryItem(device=devices[2], manufacturer=manufacturers[2], name='Inventory Item 3', part_id='1003', serial='GHI', asset_tag='1003', discovered=False, description='Third'), + InventoryItem(device=devices[0], manufacturer=manufacturers[0], name='Inventory Item 1', label='A', part_id='1001', serial='ABC', asset_tag='1001', discovered=True, description='First'), + InventoryItem(device=devices[1], manufacturer=manufacturers[1], name='Inventory Item 2', label='B', part_id='1002', serial='DEF', asset_tag='1002', discovered=True, description='Second'), + InventoryItem(device=devices[2], manufacturer=manufacturers[2], name='Inventory Item 3', label='C', part_id='1003', serial='GHI', asset_tag='1003', discovered=False, description='Third'), ) for i in inventory_items: i.save() @@ -2306,6 +2338,10 @@ class InventoryItemTestCase(TestCase): params = {'name': ['Inventory Item 1', 'Inventory Item 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_label(self): + params = {'label': ['A', 'B']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_part_id(self): params = {'part_id': ['1001', '1002']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index b092be612..faec98cb7 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -2134,10 +2134,14 @@ class PathTraceView(generic.ObjectView): else: path = related_paths.first() + # Get the total length of the cable and whether the length is definitive (fully defined) + total_length, is_definitive = path.get_total_length if path else (None, False) + return { 'path': path, 'related_paths': related_paths, - 'total_length': path.get_total_length() if path else None, + 'total_length': total_length, + 'is_definitive': is_definitive } diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index e2cb51417..a12e5f94f 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -465,12 +465,14 @@ class PrefixCSVForm(CustomFieldModelCSVForm): if data: - # Limit vlan queryset by assigned site and group - params = { - f"site__{self.fields['site'].to_field_name}": data.get('site'), - f"group__{self.fields['vlan_group'].to_field_name}": data.get('vlan_group'), - } - self.fields['vlan'].queryset = self.fields['vlan'].queryset.filter(**params) + # Limit VLAN queryset by assigned site and/or group (if specified) + params = {} + if data.get('site'): + params[f"site__{self.fields['site'].to_field_name}"] = data.get('site') + if data.get('vlan_group'): + params[f"group__{self.fields['vlan_group'].to_field_name}"] = data.get('vlan_group') + if params: + self.fields['vlan'].queryset = self.fields['vlan'].queryset.filter(**params) class PrefixBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm): diff --git a/netbox/ipam/tables.py b/netbox/ipam/tables.py index aaf2a82c4..04a5d130c 100644 --- a/netbox/ipam/tables.py +++ b/netbox/ipam/tables.py @@ -22,7 +22,7 @@ PREFIX_LINK = """ {% for i in record.parents|as_range %} {% endfor %} -{{ record.prefix }} +{{ record.prefix }} """ PREFIX_ROLE_LINK = """ diff --git a/netbox/netbox/api/__init__.py b/netbox/netbox/api/__init__.py index 78ab7431d..334ee09f7 100644 --- a/netbox/netbox/api/__init__.py +++ b/netbox/netbox/api/__init__.py @@ -1,4 +1,4 @@ -from .fields import ChoiceField, ContentTypeField, SerializedPKRelatedField, TimeZoneField +from .fields import ChoiceField, ContentTypeField, SerializedPKRelatedField from .routers import OrderedDefaultRouter from .serializers import BulkOperationSerializer, ValidatedModelSerializer, WritableNestedSerializer @@ -9,7 +9,6 @@ __all__ = ( 'ContentTypeField', 'OrderedDefaultRouter', 'SerializedPKRelatedField', - 'TimeZoneField', 'ValidatedModelSerializer', 'WritableNestedSerializer', ) diff --git a/netbox/netbox/api/fields.py b/netbox/netbox/api/fields.py index fb3eef76f..d73cbcac2 100644 --- a/netbox/netbox/api/fields.py +++ b/netbox/netbox/api/fields.py @@ -104,21 +104,6 @@ class ContentTypeField(RelatedField): return f"{obj.app_label}.{obj.model}" -class TimeZoneField(serializers.Field): - """ - Represent a pytz time zone. - """ - def to_representation(self, obj): - return obj.zone if obj else None - - def to_internal_value(self, data): - if not data: - return "" - if data not in pytz.common_timezones: - raise ValidationError('Unknown time zone "{}" (see pytz.common_timezones for all options)'.format(data)) - return pytz.timezone(data) - - class SerializedPKRelatedField(PrimaryKeyRelatedField): """ Extends PrimaryKeyRelatedField to return a serialized object on read. This is useful for representing related diff --git a/netbox/netbox/configuration.example.py b/netbox/netbox/configuration.example.py index b7a72a504..0dadb55bc 100644 --- a/netbox/netbox/configuration.example.py +++ b/netbox/netbox/configuration.example.py @@ -154,6 +154,9 @@ LOGIN_TIMEOUT = None # Setting this to True will display a "maintenance mode" banner at the top of every page. MAINTENANCE_MODE = False +# The URL to use when mapping physical addresses or GPS coordinates +MAPS_URL = 'https://maps.google.com/?q=' + # An API consumer can request an arbitrary number of objects =by appending the "limit" parameter to the URL (e.g. # "?limit=1000"). This setting defines the maximum limit. Setting it to 0 or None will allow an API consumer to request # all objects by specifying "?limit=0". diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 4f4c09603..b909510dd 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -16,7 +16,7 @@ from django.core.validators import URLValidator # Environment setup # -VERSION = '2.10.6' +VERSION = '2.10.7' # Hostname HOSTNAME = platform.node() @@ -88,10 +88,9 @@ LOGGING = getattr(configuration, 'LOGGING', {}) LOGIN_REQUIRED = getattr(configuration, 'LOGIN_REQUIRED', False) LOGIN_TIMEOUT = getattr(configuration, 'LOGIN_TIMEOUT', None) MAINTENANCE_MODE = getattr(configuration, 'MAINTENANCE_MODE', False) +MAPS_URL = getattr(configuration, 'MAPS_URL', 'https://maps.google.com/?q=') MAX_PAGE_SIZE = getattr(configuration, 'MAX_PAGE_SIZE', 1000) MEDIA_ROOT = getattr(configuration, 'MEDIA_ROOT', os.path.join(BASE_DIR, 'media')).rstrip('/') -STORAGE_BACKEND = getattr(configuration, 'STORAGE_BACKEND', None) -STORAGE_CONFIG = getattr(configuration, 'STORAGE_CONFIG', {}) METRICS_ENABLED = getattr(configuration, 'METRICS_ENABLED', False) NAPALM_ARGS = getattr(configuration, 'NAPALM_ARGS', {}) NAPALM_PASSWORD = getattr(configuration, 'NAPALM_PASSWORD', '') @@ -118,18 +117,23 @@ SESSION_FILE_PATH = getattr(configuration, 'SESSION_FILE_PATH', None) SHORT_DATE_FORMAT = getattr(configuration, 'SHORT_DATE_FORMAT', 'Y-m-d') SHORT_DATETIME_FORMAT = getattr(configuration, 'SHORT_DATETIME_FORMAT', 'Y-m-d H:i') SHORT_TIME_FORMAT = getattr(configuration, 'SHORT_TIME_FORMAT', 'H:i:s') +STORAGE_BACKEND = getattr(configuration, 'STORAGE_BACKEND', None) +STORAGE_CONFIG = getattr(configuration, 'STORAGE_CONFIG', {}) TIME_FORMAT = getattr(configuration, 'TIME_FORMAT', 'g:i a') TIME_ZONE = getattr(configuration, 'TIME_ZONE', 'UTC') # Validate update repo URL and timeout if RELEASE_CHECK_URL: - try: - URLValidator(RELEASE_CHECK_URL) - except ValidationError: - raise ImproperlyConfigured( + validator = URLValidator( + message=( "RELEASE_CHECK_URL must be a valid API URL. Example: " "https://api.github.com/repos/netbox-community/netbox" ) + ) + try: + validator(RELEASE_CHECK_URL) + except ValidationError as err: + raise ImproperlyConfigured(str(err)) # Enforce a minimum cache timeout for update checks if RELEASE_CHECK_TIMEOUT < 3600: diff --git a/netbox/project-static/css/base.css b/netbox/project-static/css/base.css index efdca81be..8887fb6b6 100644 --- a/netbox/project-static/css/base.css +++ b/netbox/project-static/css/base.css @@ -362,9 +362,6 @@ table.report th a { .text-nowrap { white-space: nowrap; } -.banner-bottom { - margin-bottom: 50px; -} .panel table { margin-bottom: 0; } diff --git a/netbox/templates/base.html b/netbox/templates/base.html index f3129d7dd..155c0d9b2 100644 --- a/netbox/templates/base.html +++ b/netbox/templates/base.html @@ -55,7 +55,7 @@ {% block content %}{% endblock %}
{% if settings.BANNER_BOTTOM %} -

{{ script }}

-

{{ script.Meta.description }}

+

{{ script.Meta.description|render_markdown }}