소스 검색

Merge branch 'develop' into develop-2.5 (v2.4.4 release)

Jeremy Stretch 7 년 전
부모
커밋
2ac60bdf48

+ 10 - 1
CHANGELOG.md

@@ -19,16 +19,25 @@ v2.5.0 (FUTURE)
 
 ---
 
-v2.4.4 (FUTURE)
+v2.4.4 (2018-08-22)
 
 ## Enhancements
 
+* [#2168](https://github.com/digitalocean/netbox/issues/2168) - Added Extreme SummitStack interface form factors
 * [#2356](https://github.com/digitalocean/netbox/issues/2356) - Include cluster site as read-only field in VirtualMachine serializer
 * [#2362](https://github.com/digitalocean/netbox/issues/2362) - Implemented custom admin site to properly handle BASE_PATH
+* [#2254](https://github.com/digitalocean/netbox/issues/2254) - Implemented searchability for Rack Groups
 
 ## Bug Fixes
 
+* [#2353](https://github.com/digitalocean/netbox/issues/2353) - Handle `DoesNotExist` exception when deleting a device with connected interfaces
+* [#2354](https://github.com/digitalocean/netbox/issues/2354) - Increased maximum MTU for interfaces to 65536 bytes
 * [#2355](https://github.com/digitalocean/netbox/issues/2355) - Added item count to inventory tab on device view
+* [#2368](https://github.com/digitalocean/netbox/issues/2368) - Record change in device changelog when altering cluster assignment
+* [#2369](https://github.com/digitalocean/netbox/issues/2369) - Corrected time zone validation on site API serializer
+* [#2370](https://github.com/digitalocean/netbox/issues/2370) - Redirect to parent device after deleting device bays
+* [#2374](https://github.com/digitalocean/netbox/issues/2374) - Fix toggling display of IP addresses in virtual machine interfaces list
+* [#2378](https://github.com/digitalocean/netbox/issues/2378) - Corrected "edit" link for virtual machine interfaces
 
 ---
 

+ 1 - 1
docs/configuration/index.md

@@ -12,5 +12,5 @@ While NetBox has many configuration settings, only a few of them must be defined
 Configuration settings may be changed at any time. However, the NetBox service must be restarted before the changes will take effect:
 
 ```no-highlight
-# sudo supervsiorctl restart netbox
+# sudo supervisorctl restart netbox
 ```

+ 2 - 2
docs/development/release-checklist.md

@@ -48,9 +48,9 @@ Close the release milestone on GitHub. Ensure that there are no remaining open i
 
 Ensure that continuous integration testing on the `develop` branch is completing successfully.
 
-## Update VERSION
+## Update Version and Changelog
 
-Update the `VERSION` constant in `settings.py` to the new release.
+Update the `VERSION` constant in `settings.py` to the new release version and add the current date to the release notes in `CHANGELOG.md`.
 
 ## Submit a Pull Request
 

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

@@ -56,7 +56,7 @@ To enable SSL, consider this guide on [securing nginx with Let's Encrypt](https:
 ## Option B: Apache
 
 ```no-highlight
-# apt-get install -y apache2
+# apt-get install -y apache2 libapache2-mod-wsgi-py3
 ```
 
 Once Apache is installed, proceed with the following configuration (Be sure to modify the `ServerName` appropriately):

+ 9 - 0
netbox/dcim/constants.py

@@ -91,6 +91,11 @@ IFACE_FF_STACKWISE_PLUS = 5050
 IFACE_FF_FLEXSTACK = 5100
 IFACE_FF_FLEXSTACK_PLUS = 5150
 IFACE_FF_JUNIPER_VCP = 5200
+IFACE_FF_SUMMITSTACK = 5300
+IFACE_FF_SUMMITSTACK128 = 5310
+IFACE_FF_SUMMITSTACK256 = 5320
+IFACE_FF_SUMMITSTACK512 = 5330
+
 # Other
 IFACE_FF_OTHER = 32767
 
@@ -166,6 +171,10 @@ IFACE_FF_CHOICES = [
             [IFACE_FF_FLEXSTACK, 'Cisco FlexStack'],
             [IFACE_FF_FLEXSTACK_PLUS, 'Cisco FlexStack Plus'],
             [IFACE_FF_JUNIPER_VCP, 'Juniper VCP'],
+            [IFACE_FF_SUMMITSTACK, 'Extreme SummitStack'],
+            [IFACE_FF_SUMMITSTACK128, 'Extreme SummitStack-128'],
+            [IFACE_FF_SUMMITSTACK256, 'Extreme SummitStack-256'],
+            [IFACE_FF_SUMMITSTACK512, 'Extreme SummitStack-512'],
         ]
     ],
     [

+ 13 - 0
netbox/dcim/filters.py

@@ -110,6 +110,10 @@ class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
 
 
 class RackGroupFilter(django_filters.FilterSet):
+    q = django_filters.CharFilter(
+        method='search',
+        label='Search',
+    )
     site_id = django_filters.ModelMultipleChoiceFilter(
         queryset=Site.objects.all(),
         label='Site (ID)',
@@ -125,6 +129,15 @@ class RackGroupFilter(django_filters.FilterSet):
         model = RackGroup
         fields = ['site_id', 'name', 'slug']
 
+    def search(self, queryset, name, value):
+        if not value.strip():
+            return queryset
+        qs_filter = (
+            Q(name__icontains=value) |
+            Q(slug__icontains=value)
+        )
+        return queryset.filter(qs_filter)
+
 
 class RackRoleFilter(django_filters.FilterSet):
 

+ 29 - 0
netbox/dcim/migrations/0062_interface_mtu.py

@@ -0,0 +1,29 @@
+# Generated by Django 2.0.8 on 2018-08-22 14:23
+
+import django.core.validators
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('dcim', '0061_platform_napalm_args'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='interface',
+            name='mtu',
+            field=models.PositiveIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(65536)], verbose_name='MTU'),
+        ),
+        migrations.AlterField(
+            model_name='interface',
+            name='form_factor',
+            field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)'], [1170, '10GBASE-CX4 (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1510, 'CFP2 (100GE)'], [1520, 'CFP4 (100GE)'], [1550, 'Cisco CPAK (100GE)'], [1600, 'QSFP28 (100GE)']]], ['Wireless', [[2600, 'IEEE 802.11a'], [2610, 'IEEE 802.11b/g'], [2620, 'IEEE 802.11n'], [2630, 'IEEE 802.11ac'], [2640, 'IEEE 802.11ad']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP'], [5300, 'Extreme SummitStack'], [5310, 'Extreme SummitStack-128'], [5320, 'Extreme SummitStack-256'], [5330, 'Extreme SummitStack-512']]], ['Other', [[32767, 'Other']]]], default=1200),
+        ),
+        migrations.AlterField(
+            model_name='interfacetemplate',
+            name='form_factor',
+            field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)'], [1170, '10GBASE-CX4 (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1510, 'CFP2 (100GE)'], [1520, 'CFP4 (100GE)'], [1550, 'Cisco CPAK (100GE)'], [1600, 'QSFP28 (100GE)']]], ['Wireless', [[2600, 'IEEE 802.11a'], [2610, 'IEEE 802.11b/g'], [2620, 'IEEE 802.11n'], [2630, 'IEEE 802.11ac'], [2640, 'IEEE 802.11ad']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP'], [5300, 'Extreme SummitStack'], [5310, 'Extreme SummitStack-128'], [5320, 'Extreme SummitStack-256'], [5330, 'Extreme SummitStack-512']]], ['Other', [[32767, 'Other']]]], default=1200),
+        ),
+    ]

+ 2 - 2
netbox/dcim/migrations/0062_remove_platform_rpc_client.py → netbox/dcim/migrations/0063_remove_platform_rpc_client.py

@@ -1,4 +1,4 @@
-# Generated by Django 2.0.8 on 2018-08-16 16:17
+# Generated by Django 2.0.8 on 2018-08-22 16:09
 
 from django.db import migrations
 
@@ -6,7 +6,7 @@ from django.db import migrations
 class Migration(migrations.Migration):
 
     dependencies = [
-        ('dcim', '0061_platform_napalm_args'),
+        ('dcim', '0062_interface_mtu'),
     ]
 
     operations = [

+ 10 - 2
netbox/dcim/models.py

@@ -1754,9 +1754,10 @@ class Interface(ComponentModel):
         blank=True,
         verbose_name='MAC Address'
     )
-    mtu = models.PositiveSmallIntegerField(
+    mtu = models.PositiveIntegerField(
         blank=True,
         null=True,
+        validators=[MinValueValidator(1), MaxValueValidator(65536)],
         verbose_name='MTU'
     )
     mgmt_only = models.BooleanField(
@@ -2012,6 +2013,7 @@ class InterfaceConnection(models.Model):
             (self.interface_a, self.interface_b),
             (self.interface_b, self.interface_a),
         )
+
         for interface, peer_interface in interfaces:
             if action == OBJECTCHANGE_ACTION_DELETE:
                 connection_data = {
@@ -2022,11 +2024,17 @@ class InterfaceConnection(models.Model):
                     'connected_interface': peer_interface.pk,
                     'connection_status': self.connection_status
                 }
+
+            try:
+                parent_obj = interface.parent
+            except ObjectDoesNotExist:
+                parent_obj = None
+
             ObjectChange(
                 user=user,
                 request_id=request_id,
                 changed_object=interface,
-                related_object=interface.parent,
+                related_object=parent_obj,
                 action=OBJECTCHANGE_ACTION_UPDATE,
                 object_data=serialize_object(interface, extra=connection_data)
             ).save()

+ 1 - 0
netbox/netbox/forms.py

@@ -11,6 +11,7 @@ OBJ_TYPE_CHOICES = (
     ('DCIM', (
         ('site', 'Sites'),
         ('rack', 'Racks'),
+        ('rackgroup', 'Rack Groups'),
         ('devicetype', 'Device types'),
         ('device', 'Devices'),
         ('virtualchassis', 'Virtual Chassis'),

+ 1 - 1
netbox/netbox/settings.py

@@ -21,7 +21,7 @@ except ImportError:
         "Configuration file is not present. Please define netbox/netbox/configuration.py per the documentation."
     )
 
-VERSION = '2.4.4-dev'
+VERSION = '2.5.0-dev'
 
 BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
 

+ 16 - 3
netbox/netbox/views.py

@@ -10,9 +10,16 @@ from rest_framework.views import APIView
 from circuits.filters import CircuitFilter, ProviderFilter
 from circuits.models import Circuit, Provider
 from circuits.tables import CircuitTable, ProviderTable
-from dcim.filters import DeviceFilter, DeviceTypeFilter, RackFilter, SiteFilter, VirtualChassisFilter
-from dcim.models import ConsolePort, Device, DeviceType, InterfaceConnection, PowerPort, Rack, Site, VirtualChassis
-from dcim.tables import DeviceDetailTable, DeviceTypeTable, RackTable, SiteTable, VirtualChassisTable
+from dcim.filters import (
+    DeviceFilter, DeviceTypeFilter, RackFilter, RackGroupFilter, SiteFilter, VirtualChassisFilter
+)
+from dcim.models import (
+    ConsolePort, Device, DeviceType, InterfaceConnection, PowerPort, Rack, RackGroup, Site,
+    VirtualChassis
+)
+from dcim.tables import (
+    DeviceDetailTable, DeviceTypeTable, RackTable, RackGroupTable, SiteTable, VirtualChassisTable
+)
 from extras.models import ObjectChange, ReportResult, TopologyMap
 from ipam.filters import AggregateFilter, IPAddressFilter, PrefixFilter, VLANFilter, VRFFilter
 from ipam.models import Aggregate, IPAddress, Prefix, VLAN, VRF
@@ -56,6 +63,12 @@ SEARCH_TYPES = OrderedDict((
         'table': RackTable,
         'url': 'dcim:rack_list',
     }),
+    ('rackgroup', {
+        'queryset': RackGroup.objects.select_related('site').annotate(rack_count=Count('racks')),
+        'filter': RackGroupFilter,
+        'table': RackGroupTable,
+        'url': 'dcim:rackgroup_list',
+    }),
     ('devicetype', {
         'queryset': DeviceType.objects.select_related('manufacturer').annotate(instance_count=Count('instances')),
         'filter': DeviceTypeFilter,

+ 2 - 2
netbox/templates/dcim/device.html

@@ -447,7 +447,7 @@
         <div class="col-md-12">
             {% if device_bays or device.device_type.is_parent_device %}
                 {% if perms.dcim.delete_devicebay %}
-                    <form method="post" action="{% url 'dcim:devicebay_bulk_delete' pk=device.pk %}">
+                    <form method="post">
                     {% csrf_token %}
                 {% endif %}
                 <div class="panel panel-default">
@@ -483,7 +483,7 @@
                             </button>
                         {% endif %}
                         {% if device_bays and perms.dcim.delete_devicebay %}
-                            <button type="submit" class="btn btn-danger btn-xs">
+                            <button type="submit" formaction="{% url 'dcim:devicebay_bulk_delete' pk=device.pk  %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
                                 <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete selected
                             </button>
                         {% endif %}

+ 2 - 2
netbox/templates/dcim/interface.html

@@ -17,12 +17,12 @@
     </div>
     <div class="pull-right">
         {% if perms.dcim.change_interface %}
-            <a href="{% url 'dcim:interface_edit' pk=interface.pk %}" class="btn btn-warning">
+            <a href="{% if interface.device %}{% url 'dcim:interface_edit' pk=interface.pk %}{% else %}{% url 'virtualization:interface_edit' pk=interface.pk %}{% endif %}" class="btn btn-warning">
                 <span class="fa fa-pencil" aria-hidden="true"></span> Edit this interface
             </a>
         {% endif %}
         {% if perms.dcim.delete_interface %}
-            <a href="{% url 'dcim:interface_delete' pk=interface.pk %}" class="btn btn-danger">
+            <a href="{% if interface.device %}{% url 'dcim:interface_delete' pk=interface.pk %}{% else %}{% url 'virtualization:interface_delete' pk=interface.pk %}{% endif %}" class="btn btn-danger">
                 <span class="fa fa-trash" aria-hidden="true"></span> Delete this interface
             </a>
         {% endif %}

+ 2 - 2
netbox/templates/virtualization/virtualmachine.html

@@ -315,9 +315,9 @@
 $('button.toggle-ips').click(function() {
     var selected = $(this).attr('selected');
     if (selected) {
-        $('#interfaces_table tr.ipaddress').hide();
+        $('#interfaces_table tr.ipaddresses').hide();
     } else {
-        $('#interfaces_table tr.ipaddress').show();
+        $('#interfaces_table tr.ipaddresses').show();
     }
     $(this).attr('selected', !selected);
     $(this).children('span').toggleClass('glyphicon-check glyphicon-unchecked');

+ 3 - 4
netbox/utilities/api.py

@@ -106,10 +106,9 @@ class TimeZoneField(Field):
     def to_internal_value(self, data):
         if not data:
             return ""
-        try:
-            return pytz.timezone(str(data))
-        except pytz.exceptions.UnknownTimeZoneError:
-            raise ValidationError('Invalid time zone "{}"'.format(data))
+        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):

+ 18 - 9
netbox/virtualization/views.py

@@ -1,5 +1,6 @@
 from django.contrib import messages
 from django.contrib.auth.mixins import PermissionRequiredMixin
+from django.db import transaction
 from django.db.models import Count
 from django.shortcuts import get_object_or_404, redirect, render
 from django.urls import reverse
@@ -181,17 +182,21 @@ class ClusterAddDevicesView(PermissionRequiredMixin, View):
 
         if form.is_valid():
 
-            # Assign the selected Devices to the Cluster
-            devices = form.cleaned_data['devices']
-            Device.objects.filter(pk__in=devices).update(cluster=cluster)
+            device_pks = form.cleaned_data['devices']
+            with transaction.atomic():
+
+                # Assign the selected Devices to the Cluster
+                for device in Device.objects.filter(pk__in=device_pks):
+                    device.cluster = cluster
+                    device.save()
 
             messages.success(request, "Added {} devices to cluster {}".format(
-                len(devices), cluster
+                len(device_pks), cluster
             ))
             return redirect(cluster.get_absolute_url())
 
         return render(request, self.template_name, {
-            'cluser': cluster,
+            'cluster': cluster,
             'form': form,
             'return_url': cluster.get_absolute_url(),
         })
@@ -210,12 +215,16 @@ class ClusterRemoveDevicesView(PermissionRequiredMixin, View):
             form = self.form(request.POST)
             if form.is_valid():
 
-                # Remove the selected Devices from the Cluster
-                devices = form.cleaned_data['pk']
-                Device.objects.filter(pk__in=devices).update(cluster=None)
+                device_pks = form.cleaned_data['pk']
+                with transaction.atomic():
+
+                    # Remove the selected Devices from the Cluster
+                    for device in Device.objects.filter(pk__in=device_pks):
+                        device.cluster = None
+                        device.save()
 
                 messages.success(request, "Removed {} devices from cluster {}".format(
-                    len(devices), cluster
+                    len(device_pks), cluster
                 ))
                 return redirect(cluster.get_absolute_url())