diff --git a/CHANGELOG.md b/CHANGELOG.md index a19bc9870..ac66d83e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,25 @@ +2.5.11 (2019-04-29) + +## Notes + +This release upgrades the Django framework to version 2.2. + +## Enhancements + +* [#2986](https://github.com/digitalocean/netbox/issues/2986) - Improve natural ordering of device components +* [#3023](https://github.com/digitalocean/netbox/issues/3023) - Add support for filtering cables by connected device +* [#3070](https://github.com/digitalocean/netbox/issues/3070) - Add decommissioning status for devices + +## Bug Fixes + +* [#2621](https://github.com/digitalocean/netbox/issues/2621) - Upgrade Django requirement to 2.2 to fix object deletion issue in the changelog middleware +* [#3072](https://github.com/digitalocean/netbox/issues/3072) - Preserve multiselect filter values when updating per-page count for list views +* [#3112](https://github.com/digitalocean/netbox/issues/3112) - Fix ordering of interface connections list by termination B name/device +* [#3116](https://github.com/digitalocean/netbox/issues/3116) - Fix `tagged_items` count in tags API endpoint +* [#3118](https://github.com/digitalocean/netbox/issues/3118) - Disable `last_login` update on login when maintenance mode is enabled + +--- + v2.5.10 (2019-04-08) ## Enhancements diff --git a/docs/administration/replicating-netbox.md b/docs/administration/replicating-netbox.md index 08e11fe56..6dd686594 100644 --- a/docs/administration/replicating-netbox.md +++ b/docs/administration/replicating-netbox.md @@ -30,7 +30,7 @@ psql -c 'create database netbox' psql netbox < netbox.sql ``` -Keep in mind that PostgreSQL user accounts and permissions are not included with the dump: You will need to create those manually if you want to fully replicate the original database (see the [installation docs](installation/1-postgresql.md)). When setting up a development instance of NetBox, it's strongly recommended to use different credentials anyway. +Keep in mind that PostgreSQL user accounts and permissions are not included with the dump: You will need to create those manually if you want to fully replicate the original database (see the [installation docs](../installation/1-postgresql.md)). When setting up a development instance of NetBox, it's strongly recommended to use different credentials anyway. ## Export the Database Schema diff --git a/netbox/dcim/constants.py b/netbox/dcim/constants.py index af2547bc4..926b97130 100644 --- a/netbox/dcim/constants.py +++ b/netbox/dcim/constants.py @@ -318,6 +318,7 @@ DEVICE_STATUS_PLANNED = 2 DEVICE_STATUS_STAGED = 3 DEVICE_STATUS_FAILED = 4 DEVICE_STATUS_INVENTORY = 5 +DEVICE_STATUS_DECOMMISSIONING = 6 DEVICE_STATUS_CHOICES = [ [DEVICE_STATUS_ACTIVE, 'Active'], [DEVICE_STATUS_OFFLINE, 'Offline'], @@ -325,6 +326,7 @@ DEVICE_STATUS_CHOICES = [ [DEVICE_STATUS_STAGED, 'Staged'], [DEVICE_STATUS_FAILED, 'Failed'], [DEVICE_STATUS_INVENTORY, 'Inventory'], + [DEVICE_STATUS_DECOMMISSIONING, 'Decommissioning'], ] # Site statuses @@ -345,6 +347,7 @@ STATUS_CLASSES = { 3: 'primary', 4: 'danger', 5: 'default', + 6: 'warning', } # Console/power/interface connection statuses diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index 2695cc7a5..47f052f70 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -1,5 +1,7 @@ import django_filters from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType +from django.core.exceptions import ObjectDoesNotExist from django.db.models import Q from netaddr import EUI from netaddr.core import AddrFormatError @@ -916,6 +918,14 @@ class CableFilter(django_filters.FilterSet): color = django_filters.MultipleChoiceFilter( choices=COLOR_CHOICES ) + device = django_filters.CharFilter( + method='filter_connected_device', + field_name='name' + ) + device_id = django_filters.CharFilter( + method='filter_connected_device', + field_name='pk' + ) class Meta: model = Cable @@ -926,6 +936,16 @@ class CableFilter(django_filters.FilterSet): return queryset return queryset.filter(label__icontains=value) + def filter_connected_device(self, queryset, name, value): + if not value.strip(): + return queryset + try: + device = Device.objects.get(**{name: value}) + except ObjectDoesNotExist: + return queryset.none() + cable_pks = device.get_cables(pk_list=True) + return queryset.filter(pk__in=cable_pks) + class ConsoleConnectionFilter(django_filters.FilterSet): site = django_filters.CharFilter( diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 699e2635c..56a1cb9b4 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -2735,6 +2735,10 @@ class CableFilterForm(BootstrapMixin, forms.Form): required=False, widget=ColorSelect() ) + device = forms.CharField( + required=False, + label='Device name' + ) # diff --git a/netbox/dcim/managers.py b/netbox/dcim/managers.py index 9e4e5fca2..53f627a5b 100644 --- a/netbox/dcim/managers.py +++ b/netbox/dcim/managers.py @@ -14,22 +14,6 @@ CHANNEL_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^.*:(\d{{1,9}})(\.\d{{1,9}})?$') VC_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^.*\.(\d{{1,9}})$') AS integer), 0)" -class DeviceComponentManager(Manager): - - def get_queryset(self): - - queryset = super().get_queryset() - table_name = self.model._meta.db_table - sql = r"CONCAT(REGEXP_REPLACE({}.name, '\d+$', ''), LPAD(SUBSTRING({}.name FROM '\d+$'), 8, '0'))" - - # Pad any trailing digits to effect natural sorting - return queryset.extra( - select={ - 'name_padded': sql.format(table_name, table_name), - } - ).order_by('name_padded', 'pk') - - class InterfaceQuerySet(QuerySet): def connectable(self): diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index 004d7b1aa..f8e8a028e 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -23,7 +23,7 @@ from utilities.utils import serialize_object, to_meters from .constants import * from .exceptions import LoopDetected from .fields import ASNField, MACAddressField -from .managers import DeviceComponentManager, InterfaceManager +from .managers import InterfaceManager class ComponentTemplateModel(models.Model): @@ -1004,7 +1004,7 @@ class ConsolePortTemplate(ComponentTemplateModel): max_length=50 ) - objects = DeviceComponentManager() + objects = NaturalOrderingManager() class Meta: ordering = ['device_type', 'name'] @@ -1027,7 +1027,7 @@ class ConsoleServerPortTemplate(ComponentTemplateModel): max_length=50 ) - objects = DeviceComponentManager() + objects = NaturalOrderingManager() class Meta: ordering = ['device_type', 'name'] @@ -1050,7 +1050,7 @@ class PowerPortTemplate(ComponentTemplateModel): max_length=50 ) - objects = DeviceComponentManager() + objects = NaturalOrderingManager() class Meta: ordering = ['device_type', 'name'] @@ -1073,7 +1073,7 @@ class PowerOutletTemplate(ComponentTemplateModel): max_length=50 ) - objects = DeviceComponentManager() + objects = NaturalOrderingManager() class Meta: ordering = ['device_type', 'name'] @@ -1139,7 +1139,7 @@ class FrontPortTemplate(ComponentTemplateModel): validators=[MinValueValidator(1), MaxValueValidator(64)] ) - objects = DeviceComponentManager() + objects = NaturalOrderingManager() class Meta: ordering = ['device_type', 'name'] @@ -1188,7 +1188,7 @@ class RearPortTemplate(ComponentTemplateModel): validators=[MinValueValidator(1), MaxValueValidator(64)] ) - objects = DeviceComponentManager() + objects = NaturalOrderingManager() class Meta: ordering = ['device_type', 'name'] @@ -1211,7 +1211,7 @@ class DeviceBayTemplate(ComponentTemplateModel): max_length=50 ) - objects = DeviceComponentManager() + objects = NaturalOrderingManager() class Meta: ordering = ['device_type', 'name'] @@ -1704,6 +1704,21 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel): filter |= Q(device__virtual_chassis=self.virtual_chassis, mgmt_only=False) return Interface.objects.filter(filter) + def get_cables(self, pk_list=False): + """ + Return a QuerySet or PK list matching all Cables connected to a component of this Device. + """ + cable_pks = [] + for component_model in [ + ConsolePort, ConsoleServerPort, PowerPort, PowerOutlet, Interface, FrontPort, RearPort + ]: + cable_pks += component_model.objects.filter( + device=self, cable__isnull=False + ).values_list('cable', flat=True) + if pk_list: + return cable_pks + return Cable.objects.filter(pk__in=cable_pks) + def get_children(self): """ Return the set of child Devices installed in DeviceBays within this Device. @@ -1742,7 +1757,7 @@ class ConsolePort(CableTermination, ComponentModel): blank=True ) - objects = DeviceComponentManager() + objects = NaturalOrderingManager() tags = TaggableManager() csv_headers = ['device', 'name'] @@ -1785,7 +1800,7 @@ class ConsoleServerPort(CableTermination, ComponentModel): blank=True ) - objects = DeviceComponentManager() + objects = NaturalOrderingManager() tags = TaggableManager() csv_headers = ['device', 'name'] @@ -1834,7 +1849,7 @@ class PowerPort(CableTermination, ComponentModel): blank=True ) - objects = DeviceComponentManager() + objects = NaturalOrderingManager() tags = TaggableManager() csv_headers = ['device', 'name'] @@ -1877,7 +1892,7 @@ class PowerOutlet(CableTermination, ComponentModel): blank=True ) - objects = DeviceComponentManager() + objects = NaturalOrderingManager() tags = TaggableManager() csv_headers = ['device', 'name'] @@ -2198,7 +2213,7 @@ class FrontPort(CableTermination, ComponentModel): blank=True ) - objects = DeviceComponentManager() + objects = NaturalOrderingManager() tags = TaggableManager() csv_headers = ['device', 'name', 'type', 'rear_port', 'rear_port_position', 'description'] @@ -2264,7 +2279,7 @@ class RearPort(CableTermination, ComponentModel): blank=True ) - objects = DeviceComponentManager() + objects = NaturalOrderingManager() tags = TaggableManager() csv_headers = ['device', 'name', 'type', 'positions', 'description'] @@ -2311,7 +2326,7 @@ class DeviceBay(ComponentModel): null=True ) - objects = DeviceComponentManager() + objects = NaturalOrderingManager() tags = TaggableManager() csv_headers = ['device', 'name', 'installed_device'] diff --git a/netbox/dcim/tables.py b/netbox/dcim/tables.py index f0efcbccf..0266eb010 100644 --- a/netbox/dcim/tables.py +++ b/netbox/dcim/tables.py @@ -733,18 +733,18 @@ class InterfaceConnectionTable(BaseTable): ) device_b = tables.LinkColumn( viewname='dcim:device', - accessor=Accessor('connected_endpoint.device'), - args=[Accessor('connected_endpoint.device.pk')], + accessor=Accessor('_connected_interface.device'), + args=[Accessor('_connected_interface.device.pk')], verbose_name='Device B' ) interface_b = tables.LinkColumn( viewname='dcim:interface', - accessor=Accessor('connected_endpoint.name'), - args=[Accessor('connected_endpoint.pk')], + accessor=Accessor('_connected_interface'), + args=[Accessor('_connected_interface.pk')], verbose_name='Interface B' ) description_b = tables.Column( - accessor=Accessor('connected_endpoint.description'), + accessor=Accessor('_connected_interface.description'), verbose_name='Description' ) diff --git a/netbox/extras/api/views.py b/netbox/extras/api/views.py index 30ef826e8..2150cb5b5 100644 --- a/netbox/extras/api/views.py +++ b/netbox/extras/api/views.py @@ -116,7 +116,9 @@ class TopologyMapViewSet(ModelViewSet): # class TagViewSet(ModelViewSet): - queryset = Tag.objects.annotate(tagged_items=Count('taggit_taggeditem_items')) + queryset = Tag.objects.annotate( + tagged_items=Count('taggit_taggeditem_items', distinct=True) + ) serializer_class = serializers.TagSerializer filterset_class = filters.TagFilter diff --git a/netbox/extras/views.py b/netbox/extras/views.py index 713143af8..2f088eb77 100644 --- a/netbox/extras/views.py +++ b/netbox/extras/views.py @@ -30,7 +30,7 @@ from .tables import ConfigContextTable, ObjectChangeTable, TagTable, TaggedItemT class TagListView(ObjectListView): queryset = Tag.objects.annotate( - items=Count('taggit_taggeditem_items') + items=Count('taggit_taggeditem_items', distinct=True) ).order_by( 'name' ) diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index da429823b..f8499a6ed 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -22,7 +22,7 @@ except ImportError: ) -VERSION = '2.5.11-dev' +VERSION = '2.5.12-dev' BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) diff --git a/netbox/templates/inc/paginator.html b/netbox/templates/inc/paginator.html index ac4648988..fe9177f87 100644 --- a/netbox/templates/inc/paginator.html +++ b/netbox/templates/inc/paginator.html @@ -20,9 +20,11 @@