Просмотр исходного кода

Merge pull request #3745 from netbox-community/develop

Release v2.6.8
Jeremy Stretch 6 лет назад
Родитель
Сommit
425670f52a

+ 0 - 0
.github/stale.yaml → .github/stale.yml


+ 0 - 7
README.md

@@ -36,13 +36,6 @@ Please see [the documentation](http://netbox.readthedocs.io/en/stable/) for
 instructions on installing NetBox. To upgrade NetBox, please download the [latest release](https://github.com/netbox-community/netbox/releases)
 and run `upgrade.sh`.
 
-## Alternative Installations
-
-* [Docker container](https://github.com/netbox-community/netbox-docker) (via [@cimnine](https://github.com/cimnine))
-* [Vagrant deployment](https://github.com/ryanmerolle/netbox-vagrant) (via [@ryanmerolle](https://github.com/ryanmerolle))
-* [Ansible deployment](https://github.com/lae/ansible-role-netbox) (via [@lae](https://github.com/lae))
-* [Kubernetes deployment](https://github.com/CENGN/netbox-kubernetes) (via [@CENGN](https://github.com/CENGN))
-
 # Providing Feedback
 
 Feature requests and bug reports must be submitted as GiHub issues. (Please be

+ 1 - 1
docs/additional-features/custom-scripts.md

@@ -182,7 +182,7 @@ class NewBranchScript(Script):
     class Meta:
         name = "New Branch"
         description = "Provision a new branch site"
-        fields = ['site_name', 'switch_count', 'switch_model']
+        field_order = ['site_name', 'switch_count', 'switch_model']
 
     site_name = StringVar(
         description="Name of the new site"

+ 0 - 1
docs/installation/3-http-daemon.md

@@ -32,7 +32,6 @@ server {
         proxy_set_header X-Forwarded-Host $server_name;
         proxy_set_header X-Real-IP $remote_addr;
         proxy_set_header X-Forwarded-Proto $scheme;
-        add_header P3P 'CP="ALL DSP COR PSAa PSDa OUR NOR ONL UNI COM NAV"';
     }
 }
 ```

+ 22 - 0
docs/release-notes/version-2.6.md

@@ -1,3 +1,25 @@
+# v2.6.8 (2019-12-10)
+
+## Enhancements
+
+* [#3139](https://github.com/netbox-community/netbox/issues/3139) - Disable password change form for LDAP-authenticated users
+* [#3457](https://github.com/netbox-community/netbox/issues/3457) - Display cable colors on device view
+* [#3329](https://github.com/netbox-community/netbox/issues/3329) - Remove obsolete P3P policy header
+* [#3663](https://github.com/netbox-community/netbox/issues/3663) - Add query filters for `created` and `last_updated` fields
+* [#3722](https://github.com/netbox-community/netbox/issues/3722) - Allow the underscore character in IPAddress DNS names
+
+## Bug Fixes
+
+* [#3312](https://github.com/netbox-community/netbox/issues/3312) - Fix validation error when editing power cables in bulk
+* [#3644](https://github.com/netbox-community/netbox/issues/3644) - Fix exception when connecting a cable to a RearPort with no corresponding FrontPort
+* [#3669](https://github.com/netbox-community/netbox/issues/3669) - Include `weight` field in prefix/VLAN role form
+* [#3674](https://github.com/netbox-community/netbox/issues/3674) - Include comments on PowerFeed view
+* [#3679](https://github.com/netbox-community/netbox/issues/3679) - Fix link for assigned ipaddress in interface page
+* [#3709](https://github.com/netbox-community/netbox/issues/3709) - Prevent exception when importing an invalid cable definition
+* [#3720](https://github.com/netbox-community/netbox/issues/3720) - Correctly indicate power feed terminations on cable list
+* [#3724](https://github.com/netbox-community/netbox/issues/3724) - Fix API filtering of interfaces by more than one device name
+* [#3725](https://github.com/netbox-community/netbox/issues/3725) - Enforce client validation for minimum service port number
+
 # v2.6.7 (2019-11-01)
 
 ## Enhancements

+ 1 - 0
mkdocs.yml

@@ -31,6 +31,7 @@ pages:
         - Change Logging: 'additional-features/change-logging.md'
         - Context Data: 'additional-features/context-data.md'
         - Custom Fields: 'additional-features/custom-fields.md'
+        - Custom Links: 'additional-features/custom-links.md'
         - Custom Scripts: 'additional-features/custom-scripts.md'
         - Export Templates: 'additional-features/export-templates.md'
         - Graphs: 'additional-features/graphs.md'

+ 3 - 3
netbox/circuits/filters.py

@@ -2,14 +2,14 @@ import django_filters
 from django.db.models import Q
 
 from dcim.models import Region, Site
-from extras.filters import CustomFieldFilterSet
+from extras.filters import CustomFieldFilterSet, CreatedUpdatedFilterSet
 from tenancy.filtersets import TenancyFilterSet
 from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter
 from .constants import *
 from .models import Circuit, CircuitTermination, CircuitType, Provider
 
 
-class ProviderFilter(CustomFieldFilterSet):
+class ProviderFilter(CustomFieldFilterSet, CreatedUpdatedFilterSet):
     id__in = NumericInFilter(
         field_name='id',
         lookup_expr='in'
@@ -54,7 +54,7 @@ class CircuitTypeFilter(NameSlugSearchFilterSet):
         fields = ['id', 'name', 'slug']
 
 
-class CircuitFilter(CustomFieldFilterSet, TenancyFilterSet):
+class CircuitFilter(CustomFieldFilterSet, TenancyFilterSet, CreatedUpdatedFilterSet):
     id__in = NumericInFilter(
         field_name='id',
         lookup_expr='in'

+ 2 - 1
netbox/dcim/constants.py

@@ -384,7 +384,8 @@ CONNECTION_STATUS_CHOICES = [
 
 # Cable endpoint types
 CABLE_TERMINATION_TYPES = [
-    'consoleport', 'consoleserverport', 'interface', 'poweroutlet', 'powerport', 'frontport', 'rearport', 'circuittermination',
+    'consoleport', 'consoleserverport', 'interface', 'poweroutlet', 'powerport', 'frontport', 'rearport',
+    'circuittermination', 'powerfeed',
 ]
 
 # Cable types

+ 13 - 11
netbox/dcim/filters.py

@@ -2,13 +2,13 @@ import django_filters
 from django.contrib.auth.models import User
 from django.db.models import Q
 
-from extras.filters import CustomFieldFilterSet, LocalConfigContextFilter
+from extras.filters import CustomFieldFilterSet, LocalConfigContextFilter, CreatedUpdatedFilterSet
 from tenancy.filtersets import TenancyFilterSet
 from tenancy.models import Tenant
 from utilities.constants import COLOR_CHOICES
 from utilities.filters import (
-    MultiValueMACAddressFilter, MultiValueNumberFilter, NameSlugSearchFilterSet, NumericInFilter, TagFilter,
-    TreeNodeMultipleChoiceFilter,
+    MultiValueCharFilter, MultiValueMACAddressFilter, MultiValueNumberFilter, NameSlugSearchFilterSet, NumericInFilter,
+    TagFilter, TreeNodeMultipleChoiceFilter,
 )
 from virtualization.models import Cluster
 from .constants import *
@@ -38,7 +38,7 @@ class RegionFilter(NameSlugSearchFilterSet):
         fields = ['id', 'name', 'slug']
 
 
-class SiteFilter(TenancyFilterSet, CustomFieldFilterSet):
+class SiteFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
     id__in = NumericInFilter(
         field_name='id',
         lookup_expr='in'
@@ -116,7 +116,7 @@ class RackRoleFilter(NameSlugSearchFilterSet):
         fields = ['id', 'name', 'slug', 'color']
 
 
-class RackFilter(TenancyFilterSet, CustomFieldFilterSet):
+class RackFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
     id__in = NumericInFilter(
         field_name='id',
         lookup_expr='in'
@@ -251,7 +251,7 @@ class ManufacturerFilter(NameSlugSearchFilterSet):
         fields = ['id', 'name', 'slug']
 
 
-class DeviceTypeFilter(CustomFieldFilterSet):
+class DeviceTypeFilter(CustomFieldFilterSet, CreatedUpdatedFilterSet):
     id__in = NumericInFilter(
         field_name='id',
         lookup_expr='in'
@@ -423,7 +423,7 @@ class PlatformFilter(NameSlugSearchFilterSet):
         fields = ['id', 'name', 'slug', 'napalm_driver']
 
 
-class DeviceFilter(LocalConfigContextFilter, TenancyFilterSet, CustomFieldFilterSet):
+class DeviceFilter(LocalConfigContextFilter, TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
     id__in = NumericInFilter(
         field_name='id',
         lookup_expr='in'
@@ -696,7 +696,7 @@ class InterfaceFilter(django_filters.FilterSet):
         method='search',
         label='Search',
     )
-    device = django_filters.CharFilter(
+    device = MultiValueCharFilter(
         method='filter_device',
         field_name='name',
         label='Device',
@@ -749,8 +749,10 @@ class InterfaceFilter(django_filters.FilterSet):
 
     def filter_device(self, queryset, name, value):
         try:
-            device = Device.objects.get(**{name: value})
-            vc_interface_ids = device.vc_interfaces.values_list('id', flat=True)
+            devices = Device.objects.filter(**{'{}__in'.format(name): value})
+            vc_interface_ids = []
+            for device in devices:
+                vc_interface_ids.extend(device.vc_interfaces.values_list('id', flat=True))
             return queryset.filter(pk__in=vc_interface_ids)
         except Device.DoesNotExist:
             return queryset.none()
@@ -1096,7 +1098,7 @@ class PowerPanelFilter(django_filters.FilterSet):
         return queryset.filter(qs_filter)
 
 
-class PowerFeedFilter(CustomFieldFilterSet):
+class PowerFeedFilter(CustomFieldFilterSet, CreatedUpdatedFilterSet):
     id__in = NumericInFilter(
         field_name='id',
         lookup_expr='in'

+ 2 - 2
netbox/dcim/migrations/0066_cables.py

@@ -174,8 +174,8 @@ class Migration(migrations.Migration):
                 ('length', models.PositiveSmallIntegerField(blank=True, null=True)),
                 ('length_unit', models.PositiveSmallIntegerField(blank=True, null=True)),
                 ('_abs_length', models.DecimalField(blank=True, decimal_places=4, max_digits=10, null=True)),
-                ('termination_a_type', models.ForeignKey(limit_choices_to={'model__in': ['consoleport', 'consoleserverport', 'interface', 'poweroutlet', 'powerport', 'frontport', 'rearport', 'circuittermination']}, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.ContentType')),
-                ('termination_b_type', models.ForeignKey(limit_choices_to={'model__in': ['consoleport', 'consoleserverport', 'interface', 'poweroutlet', 'powerport', 'frontport', 'rearport', 'circuittermination']}, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.ContentType')),
+                ('termination_a_type', models.ForeignKey(limit_choices_to={'model__in': ['consoleport', 'consoleserverport', 'interface', 'poweroutlet', 'powerport', 'frontport', 'rearport', 'circuittermination', 'powerfeed']}, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.ContentType')),
+                ('termination_b_type', models.ForeignKey(limit_choices_to={'model__in': ['consoleport', 'consoleserverport', 'interface', 'poweroutlet', 'powerport', 'frontport', 'rearport', 'circuittermination', 'powerfeed']}, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.ContentType')),
             ],
         ),
         migrations.AlterUniqueTogether(

+ 10 - 0
netbox/dcim/models.py

@@ -98,6 +98,8 @@ class CableTermination(models.Model):
         object_id_field='termination_b_id'
     )
 
+    is_path_endpoint = True
+
     class Meta:
         abstract = True
 
@@ -2444,6 +2446,8 @@ class FrontPort(CableTermination, ComponentModel):
         validators=[MinValueValidator(1), MaxValueValidator(64)]
     )
 
+    is_path_endpoint = False
+
     objects = NaturalOrderingManager()
     tags = TaggableManager(through=TaggedItem)
 
@@ -2506,6 +2510,8 @@ class RearPort(CableTermination, ComponentModel):
         validators=[MinValueValidator(1), MaxValueValidator(64)]
     )
 
+    is_path_endpoint = False
+
     objects = NaturalOrderingManager()
     tags = TaggableManager(through=TaggedItem)
 
@@ -2838,6 +2844,8 @@ class Cable(ChangeLoggedModel):
     def clean(self):
 
         # Validate that termination A exists
+        if not hasattr(self, 'termination_a_type'):
+            raise ValidationError('Termination A type has not been specified')
         try:
             self.termination_a_type.model_class().objects.get(pk=self.termination_a_id)
         except ObjectDoesNotExist:
@@ -2846,6 +2854,8 @@ class Cable(ChangeLoggedModel):
             })
 
         # Validate that termination B exists
+        if not hasattr(self, 'termination_b_type'):
+            raise ValidationError('Termination B type has not been specified')
         try:
             self.termination_b_type.model_class().objects.get(pk=self.termination_b_id)
         except ObjectDoesNotExist:

+ 1 - 1
netbox/dcim/signals.py

@@ -45,7 +45,7 @@ def update_connected_endpoints(instance, **kwargs):
 
     # Check if this Cable has formed a complete path. If so, update both endpoints.
     endpoint_a, endpoint_b, path_status = instance.get_path_endpoints()
-    if endpoint_a is not None and endpoint_b is not None:
+    if getattr(endpoint_a, 'is_path_endpoint', False) and getattr(endpoint_b, 'is_path_endpoint', False):
         endpoint_a.connected_endpoint = endpoint_b
         endpoint_a.connection_status = path_status
         endpoint_a.save()

+ 5 - 3
netbox/dcim/tables.py

@@ -181,8 +181,10 @@ VIRTUALCHASSIS_ACTIONS = """
 CABLE_TERMINATION_PARENT = """
 {% if value.device %}
     <a href="{{ value.device.get_absolute_url }}">{{ value.device }}</a>
-{% else %}
+{% elif value.circuit %}
     <a href="{{ value.circuit.get_absolute_url }}">{{ value.circuit }}</a>
+{% elif value.power_panel %}
+    <a href="{{ value.power_panel.get_absolute_url }}">{{ value.power_panel }}</a>
 {% endif %}
 """
 
@@ -718,7 +720,7 @@ class CableTable(BaseTable):
         orderable=False,
         verbose_name='Termination A'
     )
-    termination_a = tables.Column(
+    termination_a = tables.LinkColumn(
         accessor=Accessor('termination_a'),
         orderable=False,
         verbose_name=''
@@ -729,7 +731,7 @@ class CableTable(BaseTable):
         orderable=False,
         verbose_name='Termination B'
     )
-    termination_b = tables.Column(
+    termination_b = tables.LinkColumn(
         accessor=Accessor('termination_b'),
         orderable=False,
         verbose_name=''

+ 1 - 1
netbox/extras/api/urls.py

@@ -18,7 +18,7 @@ router.APIRootView = ExtrasRootView
 router.register(r'_choices', views.ExtrasFieldChoicesViewSet, basename='field-choice')
 
 # Custom field choices
-router.register(r'_custom_field_choices', views.CustomFieldChoicesViewSet, base_name='custom-field-choice')
+router.register(r'_custom_field_choices', views.CustomFieldChoicesViewSet, basename='custom-field-choice')
 
 # Graphs
 router.register(r'graphs', views.GraphViewSet)

+ 21 - 0
netbox/extras/filters.py

@@ -241,3 +241,24 @@ class ObjectChangeFilter(django_filters.FilterSet):
             Q(user_name__icontains=value) |
             Q(object_repr__icontains=value)
         )
+
+
+class CreatedUpdatedFilterSet(django_filters.FilterSet):
+    created = django_filters.DateFilter()
+    created__gte = django_filters.DateFilter(
+        field_name='created',
+        lookup_expr='gte'
+    )
+    created__lte = django_filters.DateFilter(
+        field_name='created',
+        lookup_expr='lte'
+    )
+    last_updated = django_filters.DateTimeFilter()
+    last_updated__gte = django_filters.DateTimeFilter(
+        field_name='last_updated',
+        lookup_expr='gte'
+    )
+    last_updated__lte = django_filters.DateTimeFilter(
+        field_name='last_updated',
+        lookup_expr='lte'
+    )

+ 7 - 7
netbox/ipam/filters.py

@@ -5,7 +5,7 @@ from django.db.models import Q
 from netaddr.core import AddrFormatError
 
 from dcim.models import Site, Device, Interface
-from extras.filters import CustomFieldFilterSet
+from extras.filters import CustomFieldFilterSet, CreatedUpdatedFilterSet
 from tenancy.filtersets import TenancyFilterSet
 from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter
 from virtualization.models import VirtualMachine
@@ -13,7 +13,7 @@ from .constants import *
 from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
 
 
-class VRFFilter(TenancyFilterSet, CustomFieldFilterSet):
+class VRFFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
     id__in = NumericInFilter(
         field_name='id',
         lookup_expr='in'
@@ -49,7 +49,7 @@ class RIRFilter(NameSlugSearchFilterSet):
         fields = ['name', 'slug', 'is_private']
 
 
-class AggregateFilter(CustomFieldFilterSet):
+class AggregateFilter(CustomFieldFilterSet, CreatedUpdatedFilterSet):
     id__in = NumericInFilter(
         field_name='id',
         lookup_expr='in'
@@ -110,7 +110,7 @@ class RoleFilter(NameSlugSearchFilterSet):
         fields = ['id', 'name', 'slug']
 
 
-class PrefixFilter(TenancyFilterSet, CustomFieldFilterSet):
+class PrefixFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
     id__in = NumericInFilter(
         field_name='id',
         lookup_expr='in'
@@ -247,7 +247,7 @@ class PrefixFilter(TenancyFilterSet, CustomFieldFilterSet):
         return queryset.filter(prefix__net_mask_length=value)
 
 
-class IPAddressFilter(TenancyFilterSet, CustomFieldFilterSet):
+class IPAddressFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
     id__in = NumericInFilter(
         field_name='id',
         lookup_expr='in'
@@ -384,7 +384,7 @@ class VLANGroupFilter(NameSlugSearchFilterSet):
         fields = ['id', 'name', 'slug']
 
 
-class VLANFilter(TenancyFilterSet, CustomFieldFilterSet):
+class VLANFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
     id__in = NumericInFilter(
         field_name='id',
         lookup_expr='in'
@@ -444,7 +444,7 @@ class VLANFilter(TenancyFilterSet, CustomFieldFilterSet):
         return queryset.filter(qs_filter)
 
 
-class ServiceFilter(django_filters.FilterSet):
+class ServiceFilter(CreatedUpdatedFilterSet):
     q = django_filters.CharFilter(
         method='search',
         label='Search',

+ 5 - 1
netbox/ipam/forms.py

@@ -240,7 +240,7 @@ class RoleForm(BootstrapMixin, forms.ModelForm):
     class Meta:
         model = Role
         fields = [
-            'name', 'slug',
+            'name', 'slug', 'weight',
         ]
 
 
@@ -1250,6 +1250,10 @@ class VLANFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
 #
 
 class ServiceForm(BootstrapMixin, CustomFieldForm):
+    port = forms.IntegerField(
+        min_value=1,
+        max_value=65535
+    )
     tags = TagField(
         required=False
     )

+ 1 - 1
netbox/ipam/migrations/0027_ipaddress_add_dns_name.py

@@ -14,6 +14,6 @@ class Migration(migrations.Migration):
         migrations.AddField(
             model_name='ipaddress',
             name='dns_name',
-            field=models.CharField(blank=True, max_length=255, validators=[django.core.validators.RegexValidator(code='invalid', message='Only alphanumeric characters, hyphens, and periods are allowed in DNS names', regex='^[0-9A-Za-z.-]+$')]),
+            field=models.CharField(blank=True, max_length=255, validators=[django.core.validators.RegexValidator(code='invalid', message='Only alphanumeric characters, hyphens, periods, and underscores are allowed in DNS names', regex='^[0-9A-Za-z._-]+$')]),
         ),
     ]

+ 2 - 2
netbox/ipam/tables.py

@@ -85,7 +85,7 @@ IPADDRESS_LINK = """
 """
 
 IPADDRESS_ASSIGN_LINK = """
-<a href="{% url 'ipam:ipaddress_edit' pk=record.pk %}?interface={{ request.GET.interface }}&return_url={{ request.GET.return_url }}">{{ record }}</a>
+<a href="{% url 'ipam:ipaddress_edit' pk=record.pk %}?interface={{ record.interface.pk }}&return_url={{ request.path }}">{{ record }}</a>
 """
 
 IPADDRESS_PARENT = """
@@ -292,7 +292,7 @@ class RoleTable(BaseTable):
 
     class Meta(BaseTable.Meta):
         model = Role
-        fields = ('pk', 'name', 'prefix_count', 'vlan_count', 'slug', 'actions')
+        fields = ('pk', 'name', 'prefix_count', 'vlan_count', 'slug', 'weight', 'actions')
 
 
 #

+ 2 - 2
netbox/ipam/validators.py

@@ -2,7 +2,7 @@ from django.core.validators import RegexValidator
 
 
 DNSValidator = RegexValidator(
-    regex='^[0-9A-Za-z.-]+$',
-    message='Only alphanumeric characters, hyphens, and periods are allowed in DNS names',
+    regex='^[0-9A-Za-z._-]+$',
+    message='Only alphanumeric characters, hyphens, periods, and underscores are allowed in DNS names',
     code='invalid'
 )

+ 1 - 1
netbox/netbox/settings.py

@@ -12,7 +12,7 @@ from django.core.exceptions import ImproperlyConfigured
 # Environment setup
 #
 
-VERSION = '2.6.7'
+VERSION = '2.6.8'
 
 # Hostname
 HOSTNAME = platform.node()

+ 8 - 0
netbox/project-static/css/base.css

@@ -457,6 +457,14 @@ table.report th a {
     width: 80px;
     border: 1px solid grey;
 }
+.inline-color-block {
+    display: inline-block;
+    width: 1.5em;
+    height: 1.5em;
+    border: 1px solid grey;
+    border-radius: .25em;
+    vertical-align: middle;
+}
 .text-nowrap {
     white-space: nowrap;
 }

+ 2 - 2
netbox/secrets/filters.py

@@ -2,7 +2,7 @@ import django_filters
 from django.db.models import Q
 
 from dcim.models import Device
-from extras.filters import CustomFieldFilterSet
+from extras.filters import CustomFieldFilterSet, CreatedUpdatedFilterSet
 from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter
 from .models import Secret, SecretRole
 
@@ -14,7 +14,7 @@ class SecretRoleFilter(NameSlugSearchFilterSet):
         fields = ['id', 'name', 'slug']
 
 
-class SecretFilter(CustomFieldFilterSet):
+class SecretFilter(CustomFieldFilterSet, CreatedUpdatedFilterSet):
     id__in = NumericInFilter(
         field_name='id',
         lookup_expr='in'

+ 3 - 0
netbox/templates/dcim/inc/interface.html

@@ -48,6 +48,9 @@
     <td class="text-nowrap">
         {% if iface.cable %}
             <a href="{{ iface.cable.get_absolute_url }}">{{ iface.cable }}</a>
+            {% if iface.cable.color %}
+            <span class="inline-color-block" style="background-color: #{{ iface.cable.color }}">&nbsp;</span>
+            {% endif %}
             <a href="{% url 'dcim:interface_trace' pk=iface.pk %}" class="btn btn-primary btn-xs" title="Trace">
                 <i class="fa fa-share-alt" aria-hidden="true"></i>
             </a>

+ 12 - 0
netbox/templates/dcim/powerfeed.html

@@ -121,6 +121,18 @@
                 </tr>
             </table>
         </div>
+        <div class="panel panel-default">
+            <div class="panel-heading">
+                <strong>Comments</strong>
+            </div>
+            <div class="panel-body rendered-markdown">
+                {% if powerfeed.comments %}
+                    {{ powerfeed.comments|gfm }}
+                {% else %}
+                    <span class="text-muted">None</span>
+                {% endif %}
+            </div>
+        </div>
     </div>
     <div class="col-md-6">
         <div class="panel panel-default">

+ 5 - 3
netbox/templates/users/_user.html

@@ -12,9 +12,11 @@
             <li{% ifequal active_tab "profile" %} class="active"{% endifequal %}>
                 <a href="{% url 'user:profile' %}">Profile</a>
             </li>
-            <li{% ifequal active_tab "change_password" %} class="active"{% endifequal %}>
-                <a href="{% url 'user:change_password' %}">Change Password</a>
-            </li>
+            {% if not request.user.ldap_username %}
+                <li{% ifequal active_tab "change_password" %} class="active"{% endifequal %}>
+                    <a href="{% url 'user:change_password' %}">Change Password</a>
+                </li>
+            {% endif %}
             <li{% ifequal active_tab "api_tokens" %} class="active"{% endifequal %}>
                 <a href="{% url 'user:token_list' %}">API Tokens</a>
             </li>

+ 2 - 2
netbox/tenancy/filters.py

@@ -1,7 +1,7 @@
 import django_filters
 from django.db.models import Q
 
-from extras.filters import CustomFieldFilterSet
+from extras.filters import CustomFieldFilterSet, CreatedUpdatedFilterSet
 from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter
 from .models import Tenant, TenantGroup
 
@@ -13,7 +13,7 @@ class TenantGroupFilter(NameSlugSearchFilterSet):
         fields = ['id', 'name', 'slug']
 
 
-class TenantFilter(CustomFieldFilterSet):
+class TenantFilter(CustomFieldFilterSet, CreatedUpdatedFilterSet):
     id__in = NumericInFilter(
         field_name='id',
         lookup_expr='in'

+ 5 - 0
netbox/users/views.py

@@ -95,6 +95,11 @@ class ChangePasswordView(LoginRequiredMixin, View):
     template_name = 'users/change_password.html'
 
     def get(self, request):
+        # LDAP users cannot change their password here
+        if getattr(request.user, 'ldap_username'):
+            messages.warning(request, "LDAP-authenticated user credentials cannot be changed within NetBox.")
+            return redirect('user:profile')
+
         form = PasswordChangeForm(user=request.user)
 
         return render(request, self.template_name, {

+ 3 - 3
netbox/virtualization/filters.py

@@ -4,7 +4,7 @@ from netaddr import EUI
 from netaddr.core import AddrFormatError
 
 from dcim.models import DeviceRole, Interface, Platform, Region, Site
-from extras.filters import CustomFieldFilterSet
+from extras.filters import CustomFieldFilterSet, CreatedUpdatedFilterSet
 from tenancy.filtersets import TenancyFilterSet
 from utilities.filters import (
     MultiValueMACAddressFilter, NameSlugSearchFilterSet, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter,
@@ -27,7 +27,7 @@ class ClusterGroupFilter(NameSlugSearchFilterSet):
         fields = ['id', 'name', 'slug']
 
 
-class ClusterFilter(CustomFieldFilterSet):
+class ClusterFilter(CustomFieldFilterSet, CreatedUpdatedFilterSet):
     id__in = NumericInFilter(
         field_name='id',
         lookup_expr='in'
@@ -81,7 +81,7 @@ class ClusterFilter(CustomFieldFilterSet):
         )
 
 
-class VirtualMachineFilter(TenancyFilterSet, CustomFieldFilterSet):
+class VirtualMachineFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
     id__in = NumericInFilter(
         field_name='id',
         lookup_expr='in'

+ 18 - 0
scripts/git-hooks/pre-commit

@@ -9,6 +9,24 @@
 
 exec 1>&2
 
+EXIT=0
+RED='\033[0;31m'
+NOCOLOR='\033[0m'
+
 echo "Validating PEP8 compliance..."
 pycodestyle --ignore=W504,E501 netbox/
+if [ $? != 0 ]; then
+	EXIT=1
+fi
+
+echo "Checking for missing migrations..."
+python netbox/manage.py makemigrations --dry-run --check
+if [ $? != 0 ]; then
+	EXIT=1
+fi
+
+if [ $EXIT != 0 ]; then
+  printf "${RED}COMMIT FAILED${NOCOLOR}\n"
+fi
 
+exit $EXIT