فهرست منبع

Merge pull request #3745 from netbox-community/develop

Release v2.6.8
Jeremy Stretch 6 سال پیش
والد
کامیت
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)
 instructions on installing NetBox. To upgrade NetBox, please download the [latest release](https://github.com/netbox-community/netbox/releases)
 and run `upgrade.sh`.
 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
 # Providing Feedback
 
 
 Feature requests and bug reports must be submitted as GiHub issues. (Please be
 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:
     class Meta:
         name = "New Branch"
         name = "New Branch"
         description = "Provision a new branch site"
         description = "Provision a new branch site"
-        fields = ['site_name', 'switch_count', 'switch_model']
+        field_order = ['site_name', 'switch_count', 'switch_model']
 
 
     site_name = StringVar(
     site_name = StringVar(
         description="Name of the new site"
         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-Forwarded-Host $server_name;
         proxy_set_header X-Real-IP $remote_addr;
         proxy_set_header X-Real-IP $remote_addr;
         proxy_set_header X-Forwarded-Proto $scheme;
         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)
 # v2.6.7 (2019-11-01)
 
 
 ## Enhancements
 ## Enhancements

+ 1 - 0
mkdocs.yml

@@ -31,6 +31,7 @@ pages:
         - Change Logging: 'additional-features/change-logging.md'
         - Change Logging: 'additional-features/change-logging.md'
         - Context Data: 'additional-features/context-data.md'
         - Context Data: 'additional-features/context-data.md'
         - Custom Fields: 'additional-features/custom-fields.md'
         - Custom Fields: 'additional-features/custom-fields.md'
+        - Custom Links: 'additional-features/custom-links.md'
         - Custom Scripts: 'additional-features/custom-scripts.md'
         - Custom Scripts: 'additional-features/custom-scripts.md'
         - Export Templates: 'additional-features/export-templates.md'
         - Export Templates: 'additional-features/export-templates.md'
         - Graphs: 'additional-features/graphs.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 django.db.models import Q
 
 
 from dcim.models import Region, Site
 from dcim.models import Region, Site
-from extras.filters import CustomFieldFilterSet
+from extras.filters import CustomFieldFilterSet, CreatedUpdatedFilterSet
 from tenancy.filtersets import TenancyFilterSet
 from tenancy.filtersets import TenancyFilterSet
 from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter
 from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter
 from .constants import *
 from .constants import *
 from .models import Circuit, CircuitTermination, CircuitType, Provider
 from .models import Circuit, CircuitTermination, CircuitType, Provider
 
 
 
 
-class ProviderFilter(CustomFieldFilterSet):
+class ProviderFilter(CustomFieldFilterSet, CreatedUpdatedFilterSet):
     id__in = NumericInFilter(
     id__in = NumericInFilter(
         field_name='id',
         field_name='id',
         lookup_expr='in'
         lookup_expr='in'
@@ -54,7 +54,7 @@ class CircuitTypeFilter(NameSlugSearchFilterSet):
         fields = ['id', 'name', 'slug']
         fields = ['id', 'name', 'slug']
 
 
 
 
-class CircuitFilter(CustomFieldFilterSet, TenancyFilterSet):
+class CircuitFilter(CustomFieldFilterSet, TenancyFilterSet, CreatedUpdatedFilterSet):
     id__in = NumericInFilter(
     id__in = NumericInFilter(
         field_name='id',
         field_name='id',
         lookup_expr='in'
         lookup_expr='in'

+ 2 - 1
netbox/dcim/constants.py

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

+ 13 - 11
netbox/dcim/filters.py

@@ -2,13 +2,13 @@ import django_filters
 from django.contrib.auth.models import User
 from django.contrib.auth.models import User
 from django.db.models import Q
 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.filtersets import TenancyFilterSet
 from tenancy.models import Tenant
 from tenancy.models import Tenant
 from utilities.constants import COLOR_CHOICES
 from utilities.constants import COLOR_CHOICES
 from utilities.filters import (
 from utilities.filters import (
-    MultiValueMACAddressFilter, MultiValueNumberFilter, NameSlugSearchFilterSet, NumericInFilter, TagFilter,
-    TreeNodeMultipleChoiceFilter,
+    MultiValueCharFilter, MultiValueMACAddressFilter, MultiValueNumberFilter, NameSlugSearchFilterSet, NumericInFilter,
+    TagFilter, TreeNodeMultipleChoiceFilter,
 )
 )
 from virtualization.models import Cluster
 from virtualization.models import Cluster
 from .constants import *
 from .constants import *
@@ -38,7 +38,7 @@ class RegionFilter(NameSlugSearchFilterSet):
         fields = ['id', 'name', 'slug']
         fields = ['id', 'name', 'slug']
 
 
 
 
-class SiteFilter(TenancyFilterSet, CustomFieldFilterSet):
+class SiteFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
     id__in = NumericInFilter(
     id__in = NumericInFilter(
         field_name='id',
         field_name='id',
         lookup_expr='in'
         lookup_expr='in'
@@ -116,7 +116,7 @@ class RackRoleFilter(NameSlugSearchFilterSet):
         fields = ['id', 'name', 'slug', 'color']
         fields = ['id', 'name', 'slug', 'color']
 
 
 
 
-class RackFilter(TenancyFilterSet, CustomFieldFilterSet):
+class RackFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
     id__in = NumericInFilter(
     id__in = NumericInFilter(
         field_name='id',
         field_name='id',
         lookup_expr='in'
         lookup_expr='in'
@@ -251,7 +251,7 @@ class ManufacturerFilter(NameSlugSearchFilterSet):
         fields = ['id', 'name', 'slug']
         fields = ['id', 'name', 'slug']
 
 
 
 
-class DeviceTypeFilter(CustomFieldFilterSet):
+class DeviceTypeFilter(CustomFieldFilterSet, CreatedUpdatedFilterSet):
     id__in = NumericInFilter(
     id__in = NumericInFilter(
         field_name='id',
         field_name='id',
         lookup_expr='in'
         lookup_expr='in'
@@ -423,7 +423,7 @@ class PlatformFilter(NameSlugSearchFilterSet):
         fields = ['id', 'name', 'slug', 'napalm_driver']
         fields = ['id', 'name', 'slug', 'napalm_driver']
 
 
 
 
-class DeviceFilter(LocalConfigContextFilter, TenancyFilterSet, CustomFieldFilterSet):
+class DeviceFilter(LocalConfigContextFilter, TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
     id__in = NumericInFilter(
     id__in = NumericInFilter(
         field_name='id',
         field_name='id',
         lookup_expr='in'
         lookup_expr='in'
@@ -696,7 +696,7 @@ class InterfaceFilter(django_filters.FilterSet):
         method='search',
         method='search',
         label='Search',
         label='Search',
     )
     )
-    device = django_filters.CharFilter(
+    device = MultiValueCharFilter(
         method='filter_device',
         method='filter_device',
         field_name='name',
         field_name='name',
         label='Device',
         label='Device',
@@ -749,8 +749,10 @@ class InterfaceFilter(django_filters.FilterSet):
 
 
     def filter_device(self, queryset, name, value):
     def filter_device(self, queryset, name, value):
         try:
         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)
             return queryset.filter(pk__in=vc_interface_ids)
         except Device.DoesNotExist:
         except Device.DoesNotExist:
             return queryset.none()
             return queryset.none()
@@ -1096,7 +1098,7 @@ class PowerPanelFilter(django_filters.FilterSet):
         return queryset.filter(qs_filter)
         return queryset.filter(qs_filter)
 
 
 
 
-class PowerFeedFilter(CustomFieldFilterSet):
+class PowerFeedFilter(CustomFieldFilterSet, CreatedUpdatedFilterSet):
     id__in = NumericInFilter(
     id__in = NumericInFilter(
         field_name='id',
         field_name='id',
         lookup_expr='in'
         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', models.PositiveSmallIntegerField(blank=True, null=True)),
                 ('length_unit', 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)),
                 ('_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(
         migrations.AlterUniqueTogether(

+ 10 - 0
netbox/dcim/models.py

@@ -98,6 +98,8 @@ class CableTermination(models.Model):
         object_id_field='termination_b_id'
         object_id_field='termination_b_id'
     )
     )
 
 
+    is_path_endpoint = True
+
     class Meta:
     class Meta:
         abstract = True
         abstract = True
 
 
@@ -2444,6 +2446,8 @@ class FrontPort(CableTermination, ComponentModel):
         validators=[MinValueValidator(1), MaxValueValidator(64)]
         validators=[MinValueValidator(1), MaxValueValidator(64)]
     )
     )
 
 
+    is_path_endpoint = False
+
     objects = NaturalOrderingManager()
     objects = NaturalOrderingManager()
     tags = TaggableManager(through=TaggedItem)
     tags = TaggableManager(through=TaggedItem)
 
 
@@ -2506,6 +2510,8 @@ class RearPort(CableTermination, ComponentModel):
         validators=[MinValueValidator(1), MaxValueValidator(64)]
         validators=[MinValueValidator(1), MaxValueValidator(64)]
     )
     )
 
 
+    is_path_endpoint = False
+
     objects = NaturalOrderingManager()
     objects = NaturalOrderingManager()
     tags = TaggableManager(through=TaggedItem)
     tags = TaggableManager(through=TaggedItem)
 
 
@@ -2838,6 +2844,8 @@ class Cable(ChangeLoggedModel):
     def clean(self):
     def clean(self):
 
 
         # Validate that termination A exists
         # Validate that termination A exists
+        if not hasattr(self, 'termination_a_type'):
+            raise ValidationError('Termination A type has not been specified')
         try:
         try:
             self.termination_a_type.model_class().objects.get(pk=self.termination_a_id)
             self.termination_a_type.model_class().objects.get(pk=self.termination_a_id)
         except ObjectDoesNotExist:
         except ObjectDoesNotExist:
@@ -2846,6 +2854,8 @@ class Cable(ChangeLoggedModel):
             })
             })
 
 
         # Validate that termination B exists
         # Validate that termination B exists
+        if not hasattr(self, 'termination_b_type'):
+            raise ValidationError('Termination B type has not been specified')
         try:
         try:
             self.termination_b_type.model_class().objects.get(pk=self.termination_b_id)
             self.termination_b_type.model_class().objects.get(pk=self.termination_b_id)
         except ObjectDoesNotExist:
         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.
     # Check if this Cable has formed a complete path. If so, update both endpoints.
     endpoint_a, endpoint_b, path_status = instance.get_path_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.connected_endpoint = endpoint_b
         endpoint_a.connection_status = path_status
         endpoint_a.connection_status = path_status
         endpoint_a.save()
         endpoint_a.save()

+ 5 - 3
netbox/dcim/tables.py

@@ -181,8 +181,10 @@ VIRTUALCHASSIS_ACTIONS = """
 CABLE_TERMINATION_PARENT = """
 CABLE_TERMINATION_PARENT = """
 {% if value.device %}
 {% if value.device %}
     <a href="{{ value.device.get_absolute_url }}">{{ value.device }}</a>
     <a href="{{ value.device.get_absolute_url }}">{{ value.device }}</a>
-{% else %}
+{% elif value.circuit %}
     <a href="{{ value.circuit.get_absolute_url }}">{{ value.circuit }}</a>
     <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 %}
 {% endif %}
 """
 """
 
 
@@ -718,7 +720,7 @@ class CableTable(BaseTable):
         orderable=False,
         orderable=False,
         verbose_name='Termination A'
         verbose_name='Termination A'
     )
     )
-    termination_a = tables.Column(
+    termination_a = tables.LinkColumn(
         accessor=Accessor('termination_a'),
         accessor=Accessor('termination_a'),
         orderable=False,
         orderable=False,
         verbose_name=''
         verbose_name=''
@@ -729,7 +731,7 @@ class CableTable(BaseTable):
         orderable=False,
         orderable=False,
         verbose_name='Termination B'
         verbose_name='Termination B'
     )
     )
-    termination_b = tables.Column(
+    termination_b = tables.LinkColumn(
         accessor=Accessor('termination_b'),
         accessor=Accessor('termination_b'),
         orderable=False,
         orderable=False,
         verbose_name=''
         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')
 router.register(r'_choices', views.ExtrasFieldChoicesViewSet, basename='field-choice')
 
 
 # Custom field choices
 # 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
 # Graphs
 router.register(r'graphs', views.GraphViewSet)
 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(user_name__icontains=value) |
             Q(object_repr__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 netaddr.core import AddrFormatError
 
 
 from dcim.models import Site, Device, Interface
 from dcim.models import Site, Device, Interface
-from extras.filters import CustomFieldFilterSet
+from extras.filters import CustomFieldFilterSet, CreatedUpdatedFilterSet
 from tenancy.filtersets import TenancyFilterSet
 from tenancy.filtersets import TenancyFilterSet
 from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter
 from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter
 from virtualization.models import VirtualMachine
 from virtualization.models import VirtualMachine
@@ -13,7 +13,7 @@ from .constants import *
 from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
 from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
 
 
 
 
-class VRFFilter(TenancyFilterSet, CustomFieldFilterSet):
+class VRFFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
     id__in = NumericInFilter(
     id__in = NumericInFilter(
         field_name='id',
         field_name='id',
         lookup_expr='in'
         lookup_expr='in'
@@ -49,7 +49,7 @@ class RIRFilter(NameSlugSearchFilterSet):
         fields = ['name', 'slug', 'is_private']
         fields = ['name', 'slug', 'is_private']
 
 
 
 
-class AggregateFilter(CustomFieldFilterSet):
+class AggregateFilter(CustomFieldFilterSet, CreatedUpdatedFilterSet):
     id__in = NumericInFilter(
     id__in = NumericInFilter(
         field_name='id',
         field_name='id',
         lookup_expr='in'
         lookup_expr='in'
@@ -110,7 +110,7 @@ class RoleFilter(NameSlugSearchFilterSet):
         fields = ['id', 'name', 'slug']
         fields = ['id', 'name', 'slug']
 
 
 
 
-class PrefixFilter(TenancyFilterSet, CustomFieldFilterSet):
+class PrefixFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
     id__in = NumericInFilter(
     id__in = NumericInFilter(
         field_name='id',
         field_name='id',
         lookup_expr='in'
         lookup_expr='in'
@@ -247,7 +247,7 @@ class PrefixFilter(TenancyFilterSet, CustomFieldFilterSet):
         return queryset.filter(prefix__net_mask_length=value)
         return queryset.filter(prefix__net_mask_length=value)
 
 
 
 
-class IPAddressFilter(TenancyFilterSet, CustomFieldFilterSet):
+class IPAddressFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
     id__in = NumericInFilter(
     id__in = NumericInFilter(
         field_name='id',
         field_name='id',
         lookup_expr='in'
         lookup_expr='in'
@@ -384,7 +384,7 @@ class VLANGroupFilter(NameSlugSearchFilterSet):
         fields = ['id', 'name', 'slug']
         fields = ['id', 'name', 'slug']
 
 
 
 
-class VLANFilter(TenancyFilterSet, CustomFieldFilterSet):
+class VLANFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
     id__in = NumericInFilter(
     id__in = NumericInFilter(
         field_name='id',
         field_name='id',
         lookup_expr='in'
         lookup_expr='in'
@@ -444,7 +444,7 @@ class VLANFilter(TenancyFilterSet, CustomFieldFilterSet):
         return queryset.filter(qs_filter)
         return queryset.filter(qs_filter)
 
 
 
 
-class ServiceFilter(django_filters.FilterSet):
+class ServiceFilter(CreatedUpdatedFilterSet):
     q = django_filters.CharFilter(
     q = django_filters.CharFilter(
         method='search',
         method='search',
         label='Search',
         label='Search',

+ 5 - 1
netbox/ipam/forms.py

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

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

@@ -14,6 +14,6 @@ class Migration(migrations.Migration):
         migrations.AddField(
         migrations.AddField(
             model_name='ipaddress',
             model_name='ipaddress',
             name='dns_name',
             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 = """
 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 = """
 IPADDRESS_PARENT = """
@@ -292,7 +292,7 @@ class RoleTable(BaseTable):
 
 
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
         model = Role
         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(
 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'
     code='invalid'
 )
 )

+ 1 - 1
netbox/netbox/settings.py

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

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

@@ -457,6 +457,14 @@ table.report th a {
     width: 80px;
     width: 80px;
     border: 1px solid grey;
     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 {
 .text-nowrap {
     white-space: nowrap;
     white-space: nowrap;
 }
 }

+ 2 - 2
netbox/secrets/filters.py

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

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

@@ -48,6 +48,9 @@
     <td class="text-nowrap">
     <td class="text-nowrap">
         {% if iface.cable %}
         {% if iface.cable %}
             <a href="{{ iface.cable.get_absolute_url }}">{{ iface.cable }}</a>
             <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">
             <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>
                 <i class="fa fa-share-alt" aria-hidden="true"></i>
             </a>
             </a>

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

@@ -121,6 +121,18 @@
                 </tr>
                 </tr>
             </table>
             </table>
         </div>
         </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>
     <div class="col-md-6">
     <div class="col-md-6">
         <div class="panel panel-default">
         <div class="panel panel-default">

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

@@ -12,9 +12,11 @@
             <li{% ifequal active_tab "profile" %} class="active"{% endifequal %}>
             <li{% ifequal active_tab "profile" %} class="active"{% endifequal %}>
                 <a href="{% url 'user:profile' %}">Profile</a>
                 <a href="{% url 'user:profile' %}">Profile</a>
             </li>
             </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 %}>
             <li{% ifequal active_tab "api_tokens" %} class="active"{% endifequal %}>
                 <a href="{% url 'user:token_list' %}">API Tokens</a>
                 <a href="{% url 'user:token_list' %}">API Tokens</a>
             </li>
             </li>

+ 2 - 2
netbox/tenancy/filters.py

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

+ 5 - 0
netbox/users/views.py

@@ -95,6 +95,11 @@ class ChangePasswordView(LoginRequiredMixin, View):
     template_name = 'users/change_password.html'
     template_name = 'users/change_password.html'
 
 
     def get(self, request):
     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)
         form = PasswordChangeForm(user=request.user)
 
 
         return render(request, self.template_name, {
         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 netaddr.core import AddrFormatError
 
 
 from dcim.models import DeviceRole, Interface, Platform, Region, Site
 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 tenancy.filtersets import TenancyFilterSet
 from utilities.filters import (
 from utilities.filters import (
     MultiValueMACAddressFilter, NameSlugSearchFilterSet, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter,
     MultiValueMACAddressFilter, NameSlugSearchFilterSet, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter,
@@ -27,7 +27,7 @@ class ClusterGroupFilter(NameSlugSearchFilterSet):
         fields = ['id', 'name', 'slug']
         fields = ['id', 'name', 'slug']
 
 
 
 
-class ClusterFilter(CustomFieldFilterSet):
+class ClusterFilter(CustomFieldFilterSet, CreatedUpdatedFilterSet):
     id__in = NumericInFilter(
     id__in = NumericInFilter(
         field_name='id',
         field_name='id',
         lookup_expr='in'
         lookup_expr='in'
@@ -81,7 +81,7 @@ class ClusterFilter(CustomFieldFilterSet):
         )
         )
 
 
 
 
-class VirtualMachineFilter(TenancyFilterSet, CustomFieldFilterSet):
+class VirtualMachineFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
     id__in = NumericInFilter(
     id__in = NumericInFilter(
         field_name='id',
         field_name='id',
         lookup_expr='in'
         lookup_expr='in'

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

@@ -9,6 +9,24 @@
 
 
 exec 1>&2
 exec 1>&2
 
 
+EXIT=0
+RED='\033[0;31m'
+NOCOLOR='\033[0m'
+
 echo "Validating PEP8 compliance..."
 echo "Validating PEP8 compliance..."
 pycodestyle --ignore=W504,E501 netbox/
 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