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

Merge pull request #6230 from netbox-community/develop

Release v2.11.1
Jeremy Stretch 4 лет назад
Родитель
Сommit
d8ae65a762

+ 1 - 1
.github/ISSUE_TEMPLATE/bug_report.yaml

@@ -1,6 +1,6 @@
 ---
 name: 🐛 Bug Report
-about: Report a reproducible bug in the current release of NetBox
+description: Report a reproducible bug in the current release of NetBox
 labels: ["type: bug"]
 body:
   - type: markdown

+ 1 - 1
.github/ISSUE_TEMPLATE/documentation_change.yaml

@@ -1,6 +1,6 @@
 ---
 name: 📖 Documentation Change
-about: Suggest an addition or modification to the NetBox documentation
+description: Suggest an addition or modification to the NetBox documentation
 labels: ["type: documentation"]
 body:
   - type: dropdown

+ 1 - 1
.github/ISSUE_TEMPLATE/feature_request.yaml

@@ -1,6 +1,6 @@
 ---
 name: ✨ Feature Request
-about: Propose a new NetBox feature or enhancement
+description: Propose a new NetBox feature or enhancement
 labels: ["type: feature"]
 body:
   - type: markdown

+ 1 - 1
.github/ISSUE_TEMPLATE/housekeeping.yaml

@@ -1,6 +1,6 @@
 ---
 name: 🏡 Housekeeping
-about: A change pertaining to the codebase itself (developers only)
+description: A change pertaining to the codebase itself (developers only)
 labels: ["type: housekeeping"]
 body:
   - type: markdown

+ 19 - 0
docs/release-notes/version-2.11.md

@@ -1,5 +1,24 @@
 # NetBox v2.11
 
+## v2.11.1 (2021-04-21)
+
+### Enhancements
+
+* [#6161](https://github.com/netbox-community/netbox/issues/6161) - Enable ordering of device component tables
+* [#6179](https://github.com/netbox-community/netbox/issues/6179) - Enable natural ordering for virtual machines
+* [#6189](https://github.com/netbox-community/netbox/issues/6189) - Add ability to search for locations by name or description
+* [#6190](https://github.com/netbox-community/netbox/issues/6190) - Allow filtering devices with no location assigned
+* [#6210](https://github.com/netbox-community/netbox/issues/6210) - Include child locations on location view
+
+### Bug Fixes
+
+* [#6184](https://github.com/netbox-community/netbox/issues/6184) - Fix parent object table column in prefix IP addresses list
+* [#6188](https://github.com/netbox-community/netbox/issues/6188) - Support custom field filtering for regions, site groups, and locations
+* [#6196](https://github.com/netbox-community/netbox/issues/6196) - Fix object list display for users with read-only permissions
+* [#6215](https://github.com/netbox-community/netbox/issues/6215) - Restore tenancy section in virtual machine form
+
+---
+
 ## v2.11.0 (2021-04-16)
 
 **Note:** NetBox v2.11 is the last major release that will support Python 3.6. Beginning with NetBox v2.12, Python 3.7 or later will be required.

+ 8 - 0
netbox/dcim/filters.py

@@ -209,6 +209,14 @@ class LocationFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
         model = Location
         fields = ['id', 'name', 'slug', 'description']
 
+    def search(self, queryset, name, value):
+        if not value.strip():
+            return queryset
+        return queryset.filter(
+            Q(name__icontains=value) |
+            Q(description__icontains=value)
+        )
+
 
 class RackRoleFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
 

+ 12 - 6
netbox/dcim/forms.py

@@ -230,7 +230,7 @@ class RegionBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
         nullable_fields = ['parent', 'description']
 
 
-class RegionFilterForm(BootstrapMixin, forms.Form):
+class RegionFilterForm(BootstrapMixin, CustomFieldFilterForm):
     model = Site
     q = forms.CharField(
         required=False,
@@ -287,8 +287,8 @@ class SiteGroupBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
         nullable_fields = ['parent', 'description']
 
 
-class SiteGroupFilterForm(BootstrapMixin, forms.Form):
-    model = Site
+class SiteGroupFilterForm(BootstrapMixin, CustomFieldFilterForm):
+    model = SiteGroup
     q = forms.CharField(
         required=False,
         label=_('Search')
@@ -557,7 +557,12 @@ class LocationBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
         nullable_fields = ['parent', 'description']
 
 
-class LocationFilterForm(BootstrapMixin, forms.Form):
+class LocationFilterForm(BootstrapMixin, CustomFieldFilterForm):
+    model = Location
+    q = forms.CharField(
+        required=False,
+        label=_('Search')
+    )
     region_id = DynamicModelMultipleChoiceField(
         queryset=Region.objects.all(),
         required=False,
@@ -2424,10 +2429,11 @@ class DeviceFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilt
     location_id = DynamicModelMultipleChoiceField(
         queryset=Location.objects.all(),
         required=False,
-        label=_('Location'),
+        null_option='None',
         query_params={
             'site_id': '$site_id'
-        }
+        },
+        label=_('Location')
     )
     rack_id = DynamicModelMultipleChoiceField(
         queryset=Rack.objects.all(),

+ 9 - 0
netbox/dcim/tables/devices.py

@@ -291,6 +291,7 @@ class ConsolePortTable(DeviceComponentTable, PathEndpointTable):
 class DeviceConsolePortTable(ConsolePortTable):
     name = tables.TemplateColumn(
         template_code='<i class="mdi mdi-console"></i> <a href="{{ record.get_absolute_url }}">{{ value }}</a>',
+        order_by=Accessor('_name'),
         attrs={'td': {'class': 'text-nowrap'}}
     )
     actions = ButtonsColumn(
@@ -335,6 +336,7 @@ class DeviceConsoleServerPortTable(ConsoleServerPortTable):
     name = tables.TemplateColumn(
         template_code='<i class="mdi mdi-console-network-outline"></i> '
                       '<a href="{{ record.get_absolute_url }}">{{ value }}</a>',
+        order_by=Accessor('_name'),
         attrs={'td': {'class': 'text-nowrap'}}
     )
     actions = ButtonsColumn(
@@ -379,6 +381,7 @@ class DevicePowerPortTable(PowerPortTable):
     name = tables.TemplateColumn(
         template_code='<i class="mdi mdi-power-plug-outline"></i> <a href="{{ record.get_absolute_url }}">'
                       '{{ value }}</a>',
+        order_by=Accessor('_name'),
         attrs={'td': {'class': 'text-nowrap'}}
     )
     actions = ButtonsColumn(
@@ -428,6 +431,7 @@ class PowerOutletTable(DeviceComponentTable, PathEndpointTable):
 class DevicePowerOutletTable(PowerOutletTable):
     name = tables.TemplateColumn(
         template_code='<i class="mdi mdi-power-socket"></i> <a href="{{ record.get_absolute_url }}">{{ value }}</a>',
+        order_by=Accessor('_name'),
         attrs={'td': {'class': 'text-nowrap'}}
     )
     actions = ButtonsColumn(
@@ -492,6 +496,7 @@ class DeviceInterfaceTable(InterfaceTable):
         template_code='<i class="mdi mdi-{% if iface.mgmt_only %}wrench{% elif iface.is_lag %}drag-horizontal-variant'
                       '{% elif iface.is_virtual %}circle{% elif iface.is_wireless %}wifi{% else %}ethernet'
                       '{% endif %}"></i> <a href="{{ record.get_absolute_url }}">{{ value }}</a>',
+        order_by=Accessor('_name'),
         attrs={'td': {'class': 'text-nowrap'}}
     )
     parent = tables.Column(
@@ -555,6 +560,7 @@ class DeviceFrontPortTable(FrontPortTable):
     name = tables.TemplateColumn(
         template_code='<i class="mdi mdi-square-rounded{% if not record.cable %}-outline{% endif %}"></i> '
                       '<a href="{{ record.get_absolute_url }}">{{ value }}</a>',
+        order_by=Accessor('_name'),
         attrs={'td': {'class': 'text-nowrap'}}
     )
     actions = ButtonsColumn(
@@ -602,6 +608,7 @@ class DeviceRearPortTable(RearPortTable):
     name = tables.TemplateColumn(
         template_code='<i class="mdi mdi-square-rounded{% if not record.cable %}-outline{% endif %}"></i> '
                       '<a href="{{ record.get_absolute_url }}">{{ value }}</a>',
+        order_by=Accessor('_name'),
         attrs={'td': {'class': 'text-nowrap'}}
     )
     actions = ButtonsColumn(
@@ -651,6 +658,7 @@ class DeviceDeviceBayTable(DeviceBayTable):
     name = tables.TemplateColumn(
         template_code='<i class="mdi mdi-circle{% if record.installed_device %}slice-8{% else %}outline{% endif %}'
                       '"></i> <a href="{{ record.get_absolute_url }}">{{ value }}</a>',
+        order_by=Accessor('_name'),
         attrs={'td': {'class': 'text-nowrap'}}
     )
     actions = ButtonsColumn(
@@ -698,6 +706,7 @@ class DeviceInventoryItemTable(InventoryItemTable):
     name = tables.TemplateColumn(
         template_code='<a href="{{ record.get_absolute_url }}" style="padding-left: {{ record.level }}0px">'
                       '{{ value }}</a>',
+        order_by=Accessor('_name'),
         attrs={'td': {'class': 'text-nowrap'}}
     )
     actions = ButtonsColumn(

+ 31 - 26
netbox/dcim/views.py

@@ -364,16 +364,30 @@ class LocationView(generic.ObjectView):
     queryset = Location.objects.all()
 
     def get_extra_context(self, request, instance):
-        devices = Device.objects.restrict(request.user, 'view').filter(
-            location=instance
-        )
-
-        devices_table = tables.DeviceTable(devices)
-        devices_table.columns.hide('location')
-        paginate_table(devices_table, request)
+        location_ids = instance.get_descendants(include_self=True).values_list('pk', flat=True)
+        rack_count = Rack.objects.filter(location__in=location_ids).count()
+        device_count = Device.objects.filter(location__in=location_ids).count()
+
+        child_locations = Location.objects.add_related_count(
+            Location.objects.add_related_count(
+                Location.objects.all(),
+                Device,
+                'location',
+                'device_count',
+                cumulative=True
+            ),
+            Rack,
+            'location',
+            'rack_count',
+            cumulative=True
+        ).filter(pk__in=location_ids).exclude(pk=instance.pk)
+        child_locations_table = tables.LocationTable(child_locations)
+        paginate_table(child_locations_table, request)
 
         return {
-            'devices_table': devices_table,
+            'rack_count': rack_count,
+            'device_count': device_count,
+            'child_locations_table': child_locations_table,
         }
 
 
@@ -1305,8 +1319,7 @@ class DeviceConsolePortsView(generic.ObjectView):
         )
         consoleport_table = tables.DeviceConsolePortTable(
             data=consoleports,
-            user=request.user,
-            orderable=False
+            user=request.user
         )
         if request.user.has_perm('dcim.change_consoleport') or request.user.has_perm('dcim.delete_consoleport'):
             consoleport_table.columns.show('pk')
@@ -1330,8 +1343,7 @@ class DeviceConsoleServerPortsView(generic.ObjectView):
         )
         consoleserverport_table = tables.DeviceConsoleServerPortTable(
             data=consoleserverports,
-            user=request.user,
-            orderable=False
+            user=request.user
         )
         if request.user.has_perm('dcim.change_consoleserverport') or \
                 request.user.has_perm('dcim.delete_consoleserverport'):
@@ -1354,8 +1366,7 @@ class DevicePowerPortsView(generic.ObjectView):
         )
         powerport_table = tables.DevicePowerPortTable(
             data=powerports,
-            user=request.user,
-            orderable=False
+            user=request.user
         )
         if request.user.has_perm('dcim.change_powerport') or request.user.has_perm('dcim.delete_powerport'):
             powerport_table.columns.show('pk')
@@ -1377,8 +1388,7 @@ class DevicePowerOutletsView(generic.ObjectView):
         )
         poweroutlet_table = tables.DevicePowerOutletTable(
             data=poweroutlets,
-            user=request.user,
-            orderable=False
+            user=request.user
         )
         if request.user.has_perm('dcim.change_poweroutlet') or request.user.has_perm('dcim.delete_poweroutlet'):
             poweroutlet_table.columns.show('pk')
@@ -1402,8 +1412,7 @@ class DeviceInterfacesView(generic.ObjectView):
         )
         interface_table = tables.DeviceInterfaceTable(
             data=interfaces,
-            user=request.user,
-            orderable=False
+            user=request.user
         )
         if request.user.has_perm('dcim.change_interface') or request.user.has_perm('dcim.delete_interface'):
             interface_table.columns.show('pk')
@@ -1425,8 +1434,7 @@ class DeviceFrontPortsView(generic.ObjectView):
         )
         frontport_table = tables.DeviceFrontPortTable(
             data=frontports,
-            user=request.user,
-            orderable=False
+            user=request.user
         )
         if request.user.has_perm('dcim.change_frontport') or request.user.has_perm('dcim.delete_frontport'):
             frontport_table.columns.show('pk')
@@ -1446,8 +1454,7 @@ class DeviceRearPortsView(generic.ObjectView):
         rearports = RearPort.objects.restrict(request.user, 'view').filter(device=instance).prefetch_related('cable')
         rearport_table = tables.DeviceRearPortTable(
             data=rearports,
-            user=request.user,
-            orderable=False
+            user=request.user
         )
         if request.user.has_perm('dcim.change_rearport') or request.user.has_perm('dcim.delete_rearport'):
             rearport_table.columns.show('pk')
@@ -1469,8 +1476,7 @@ class DeviceDeviceBaysView(generic.ObjectView):
         )
         devicebay_table = tables.DeviceDeviceBayTable(
             data=devicebays,
-            user=request.user,
-            orderable=False
+            user=request.user
         )
         if request.user.has_perm('dcim.change_devicebay') or request.user.has_perm('dcim.delete_devicebay'):
             devicebay_table.columns.show('pk')
@@ -1492,8 +1498,7 @@ class DeviceInventoryView(generic.ObjectView):
         ).prefetch_related('manufacturer')
         inventoryitem_table = tables.DeviceInventoryItemTable(
             data=inventoryitems,
-            user=request.user,
-            orderable=False
+            user=request.user
         )
         if request.user.has_perm('dcim.change_inventoryitem') or request.user.has_perm('dcim.delete_inventoryitem'):
             inventoryitem_table.columns.show('pk')

+ 2 - 2
netbox/ipam/tables.py

@@ -340,10 +340,10 @@ class IPAddressTable(BaseTable):
         verbose_name='Interface'
     )
     assigned_object_parent = tables.Column(
-        accessor='assigned_object__parent',
+        accessor='assigned_object.parent_object',
         linkify=True,
         orderable=False,
-        verbose_name='Interface Parent'
+        verbose_name='Device/VM'
     )
 
     class Meta(BaseTable.Meta):

+ 1 - 1
netbox/netbox/settings.py

@@ -16,7 +16,7 @@ from django.core.validators import URLValidator
 # Environment setup
 #
 
-VERSION = '2.11.0'
+VERSION = '2.11.1'
 
 # Hostname
 HOSTNAME = platform.node()

+ 8 - 8
netbox/templates/dcim/location.html

@@ -43,13 +43,13 @@
         <tr>
           <td>Racks</td>
           <td>
-            <a href="{% url 'dcim:rack_list' %}?location_id={{ object.pk }}">{{ object.racks.count }}</a>
+            <a href="{% url 'dcim:rack_list' %}?location_id={{ object.pk }}">{{ rack_count }}</a>
           </td>
         </tr>
         <tr>
           <td>Devices</td>
           <td>
-            <a href="{% url 'dcim:device_list' %}?location_id={{ object.pk }}">{{ devices_table.rows|length }}</a>
+            <a href="{% url 'dcim:device_list' %}?location_id={{ object.pk }}">{{ device_count }}</a>
           </td>
         </tr>
       </table>
@@ -79,18 +79,18 @@
 	<div class="col-md-12">
     <div class="panel panel-default">
       <div class="panel-heading">
-        <strong>Devices</strong>
+        <strong>Locations</strong>
       </div>
-      {% include 'inc/table.html' with table=devices_table %}
-      {% if perms.dcim.add_device %}
+      {% include 'inc/table.html' with table=child_locations_table %}
+      {% if perms.dcim.add_location %}
         <div class="panel-footer text-right noprint">
-          <a href="{% url 'dcim:device_add' %}?location={{ object.pk }}" class="btn btn-xs btn-primary">
-            <span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add device
+          <a href="{% url 'dcim:location_add' %}?site={{ object.site.pk }}&parent={{ object.pk }}" class="btn btn-xs btn-primary">
+            <span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add location
           </a>
         </div>
       {% endif %}
       </div>
-      {% include 'inc/paginator.html' with paginator=devices_table.paginator page=devices_table.page %}
+      {% include 'inc/paginator.html' with paginator=child_locations_table.paginator page=child_locations_table.page %}
       {% plugin_full_width_page object %}
   </div>
 </div>

+ 3 - 1
netbox/templates/generic/object_list.html

@@ -76,7 +76,9 @@
                     </div>
                 </form>
             {% else %}
-                {% render_table table 'inc/table.html' %}
+                <div class="table-responsive">
+                    {% render_table table 'inc/table.html' %}
+                </div>
             {% endif %}
         {% endwith %}
         {% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}

+ 1 - 0
netbox/virtualization/forms.py

@@ -376,6 +376,7 @@ class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
         fieldsets = (
             ('Virtual Machine', ('name', 'role', 'status', 'tags')),
             ('Cluster', ('cluster_group', 'cluster')),
+            ('Tenancy', ('tenant_group', 'tenant')),
             ('Management', ('platform', 'primary_ip4', 'primary_ip6')),
             ('Resources', ('vcpus', 'memory', 'disk')),
             ('Config Context', ('local_context_data',)),

+ 32 - 0
netbox/virtualization/migrations/0023_virtualmachine_natural_ordering.py

@@ -0,0 +1,32 @@
+from django.db import migrations
+import utilities.fields
+import utilities.ordering
+
+
+def naturalize_virtualmachines(apps, schema_editor):
+    VirtualMachine = apps.get_model('virtualization', 'VirtualMachine')
+    for name in VirtualMachine.objects.values_list('name', flat=True).order_by('name').distinct():
+        VirtualMachine.objects.filter(name=name).update(_name=utilities.ordering.naturalize(name, max_length=100))
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('virtualization', '0022_vminterface_parent'),
+    ]
+
+    operations = [
+        migrations.AlterModelOptions(
+            name='virtualmachine',
+            options={'ordering': ('_name', 'pk')},
+        ),
+        migrations.AddField(
+            model_name='virtualmachine',
+            name='_name',
+            field=utilities.fields.NaturalOrderingField('name', max_length=100, blank=True, naturalize_function=utilities.ordering.naturalize),
+        ),
+        migrations.RunPython(
+            code=naturalize_virtualmachines,
+            reverse_code=migrations.RunPython.noop
+        ),
+    ]

+ 6 - 1
netbox/virtualization/models.py

@@ -226,6 +226,11 @@ class VirtualMachine(PrimaryModel, ConfigContextModel):
     name = models.CharField(
         max_length=64
     )
+    _name = NaturalOrderingField(
+        target_field='name',
+        max_length=100,
+        blank=True
+    )
     status = models.CharField(
         max_length=50,
         choices=VirtualMachineStatusChoices,
@@ -296,7 +301,7 @@ class VirtualMachine(PrimaryModel, ConfigContextModel):
     ]
 
     class Meta:
-        ordering = ('name', 'pk')  # Name may be non-unique
+        ordering = ('_name', 'pk')  # Name may be non-unique
         unique_together = [
             ['cluster', 'tenant', 'name']
         ]

+ 1 - 1
requirements.txt

@@ -8,7 +8,7 @@ django-pglocks==1.0.4
 django-prometheus==2.1.0
 django-rq==2.4.1
 django-tables2==2.3.4
-django-taggit==1.3.0
+django-taggit==1.4.0
 django-timezone-field==4.1.2
 djangorestframework==3.12.4
 drf-yasg[validation]==1.20.0