Parcourir la source

Closes #154: Expand device status field options

Jeremy Stretch il y a 8 ans
Parent
commit
77247cccbe

+ 1 - 5
netbox/dcim/filters.py

@@ -373,10 +373,6 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
         to_field_name='slug',
         label='Platform (slug)',
     )
-    status = django_filters.BooleanFilter(
-        name='status',
-        label='Status',
-    )
     is_console_server = django_filters.BooleanFilter(
         name='device_type__is_console_server',
         label='Is a console server',
@@ -396,7 +392,7 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
 
     class Meta:
         model = Device
-        fields = ['name', 'serial', 'asset_tag']
+        fields = ['name', 'serial', 'asset_tag', 'status']
 
     def search(self, queryset, name, value):
         if not value.strip():

+ 32 - 29
netbox/dcim/forms.py

@@ -532,27 +532,32 @@ class PlatformForm(BootstrapMixin, forms.ModelForm):
 
 class DeviceForm(BootstrapMixin, CustomFieldForm):
     site = forms.ModelChoiceField(queryset=Site.objects.all(), widget=forms.Select(attrs={'filter-for': 'rack'}))
-    rack = forms.ModelChoiceField(queryset=Rack.objects.all(), required=False, widget=APISelect(
-        api_url='/api/dcim/racks/?site_id={{site}}',
-        display_field='display_name',
-        attrs={'filter-for': 'position'}
-    ))
-    position = forms.TypedChoiceField(required=False, empty_value=None,
-                                      help_text="The lowest-numbered unit occupied by the device",
-                                      widget=APISelect(api_url='/api/dcim/racks/{{rack}}/units/?face={{face}}',
-                                                       disabled_indicator='device'))
-    manufacturer = forms.ModelChoiceField(queryset=Manufacturer.objects.all(),
-                                          widget=forms.Select(attrs={'filter-for': 'device_type'}))
-    device_type = forms.ModelChoiceField(queryset=DeviceType.objects.all(), label='Device type', widget=APISelect(
-        api_url='/api/dcim/device-types/?manufacturer_id={{manufacturer}}',
-        display_field='model'
-    ))
+    rack = forms.ModelChoiceField(
+        queryset=Rack.objects.all(), required=False, widget=APISelect(
+            api_url='/api/dcim/racks/?site_id={{site}}',
+            display_field='display_name',
+            attrs={'filter-for': 'position'}
+        )
+    )
+    position = forms.TypedChoiceField(
+        required=False, empty_value=None, help_text="The lowest-numbered unit occupied by the device",
+        widget=APISelect(api_url='/api/dcim/racks/{{rack}}/units/?face={{face}}', disabled_indicator='device')
+    )
+    manufacturer = forms.ModelChoiceField(
+        queryset=Manufacturer.objects.all(), widget=forms.Select(attrs={'filter-for': 'device_type'})
+    )
+    device_type = forms.ModelChoiceField(
+        queryset=DeviceType.objects.all(), label='Device type',
+        widget=APISelect(api_url='/api/dcim/device-types/?manufacturer_id={{manufacturer}}', display_field='model')
+    )
     comments = CommentField()
 
     class Meta:
         model = Device
-        fields = ['name', 'device_role', 'tenant', 'device_type', 'serial', 'asset_tag', 'site', 'rack', 'position',
-                  'face', 'status', 'platform', 'primary_ip4', 'primary_ip6', 'comments']
+        fields = [
+            'name', 'device_role', 'tenant', 'device_type', 'serial', 'asset_tag', 'site', 'rack', 'position', 'face',
+            'status', 'platform', 'primary_ip4', 'primary_ip6', 'comments',
+        ]
         help_texts = {
             'device_role': "The function this device serves",
             'serial': "Chassis serial number",
@@ -764,6 +769,13 @@ class DeviceBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
         nullable_fields = ['tenant', 'platform']
 
 
+def device_status_choices():
+    status_counts = {}
+    for status in Device.objects.values('status').annotate(count=Count('status')).order_by('status'):
+        status_counts[status['status']] = status['count']
+    return [(s[0], u'{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in STATUS_CHOICES]
+
+
 class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
     model = Device
     q = forms.CharField(required=False, label='Search')
@@ -783,10 +795,7 @@ class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
         queryset=Tenant.objects.annotate(filter_count=Count('devices')), to_field_name='slug',
         null_option=(0, 'None'),
     )
-    manufacturer_id = FilterChoiceField(
-        queryset=Manufacturer.objects.all(),
-        label='Manufacturer',
-    )
+    manufacturer_id = FilterChoiceField(queryset=Manufacturer.objects.all(), label='Manufacturer')
     device_type_id = FilterChoiceField(
         queryset=DeviceType.objects.select_related('manufacturer').order_by('model').annotate(
             filter_count=Count('instances'),
@@ -798,14 +807,8 @@ class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
         to_field_name='slug',
         null_option=(0, 'None'),
     )
-    status = forms.NullBooleanField(
-        required=False,
-        widget=forms.Select(choices=FORM_STATUS_CHOICES),
-    )
-    mac_address = forms.CharField(
-        required=False,
-        label='MAC address',
-    )
+    status = forms.ChoiceField(required=False, choices=device_status_choices)
+    mac_address = forms.CharField(required=False, label='MAC address')
 
 
 #

+ 27 - 0
netbox/dcim/migrations/0035_device_expand_status_choices.py

@@ -0,0 +1,27 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.7 on 2017-05-08 15:57
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('dcim', '0034_rename_module_to_inventoryitem'),
+    ]
+
+    # We convert the BooleanField to an IntegerField first as PostgreSQL does not provide a direct cast for boolean to
+    # smallint (attempting to convert directly yields the error "cannot cast type boolean to smallint").
+    operations = [
+        migrations.AlterField(
+            model_name='device',
+            name='status',
+            field=models.PositiveIntegerField(choices=[[1, b'Active'], [0, b'Offline'], [2, b'Planned'], [3, b'Staged'], [4, b'Failed'], [5, b'Inventory']], default=1, verbose_name=b'Status'),
+        ),
+        migrations.AlterField(
+            model_name='device',
+            name='status',
+            field=models.PositiveSmallIntegerField(choices=[[1, b'Active'], [0, b'Offline'], [2, b'Planned'], [3, b'Staged'], [4, b'Failed'], [5, b'Inventory']], default=1, verbose_name=b'Status'),
+        ),
+    ]

+ 39 - 12
netbox/dcim/models.py

@@ -178,13 +178,30 @@ VIRTUAL_IFACE_TYPES = [
     IFACE_FF_LAG,
 ]
 
-STATUS_ACTIVE = True
-STATUS_OFFLINE = False
+STATUS_OFFLINE = 0
+STATUS_ACTIVE = 1
+STATUS_PLANNED = 2
+STATUS_STAGED = 3
+STATUS_FAILED = 4
+STATUS_INVENTORY = 5
 STATUS_CHOICES = [
     [STATUS_ACTIVE, 'Active'],
     [STATUS_OFFLINE, 'Offline'],
+    [STATUS_PLANNED, 'Planned'],
+    [STATUS_STAGED, 'Staged'],
+    [STATUS_FAILED, 'Failed'],
+    [STATUS_INVENTORY, 'Inventory'],
 ]
 
+DEVICE_STATUS_CLASSES = {
+    0: 'warning',
+    1: 'success',
+    2: 'info',
+    3: 'primary',
+    4: 'danger',
+    5: 'default',
+}
+
 CONNECTION_STATUS_PLANNED = False
 CONNECTION_STATUS_CONNECTED = True
 CONNECTION_STATUS_CHOICES = [
@@ -933,19 +950,26 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
     platform = models.ForeignKey('Platform', related_name='devices', blank=True, null=True, on_delete=models.SET_NULL)
     name = NullableCharField(max_length=64, blank=True, null=True, unique=True)
     serial = models.CharField(max_length=50, blank=True, verbose_name='Serial number')
-    asset_tag = NullableCharField(max_length=50, blank=True, null=True, unique=True, verbose_name='Asset tag',
-                                  help_text='A unique tag used to identify this device')
+    asset_tag = NullableCharField(
+        max_length=50, blank=True, null=True, unique=True, verbose_name='Asset tag',
+        help_text='A unique tag used to identify this device'
+    )
     site = models.ForeignKey('Site', related_name='devices', on_delete=models.PROTECT)
     rack = models.ForeignKey('Rack', related_name='devices', blank=True, null=True, on_delete=models.PROTECT)
-    position = models.PositiveSmallIntegerField(blank=True, null=True, validators=[MinValueValidator(1)],
-                                                verbose_name='Position (U)',
-                                                help_text='The lowest-numbered unit occupied by the device')
+    position = models.PositiveSmallIntegerField(
+        blank=True, null=True, validators=[MinValueValidator(1)], verbose_name='Position (U)',
+        help_text='The lowest-numbered unit occupied by the device'
+    )
     face = models.PositiveSmallIntegerField(blank=True, null=True, choices=RACK_FACE_CHOICES, verbose_name='Rack face')
-    status = models.BooleanField(choices=STATUS_CHOICES, default=STATUS_ACTIVE, verbose_name='Status')
-    primary_ip4 = models.OneToOneField('ipam.IPAddress', related_name='primary_ip4_for', on_delete=models.SET_NULL,
-                                       blank=True, null=True, verbose_name='Primary IPv4')
-    primary_ip6 = models.OneToOneField('ipam.IPAddress', related_name='primary_ip6_for', on_delete=models.SET_NULL,
-                                       blank=True, null=True, verbose_name='Primary IPv6')
+    status = models.PositiveSmallIntegerField(choices=STATUS_CHOICES, default=STATUS_ACTIVE, verbose_name='Status')
+    primary_ip4 = models.OneToOneField(
+        'ipam.IPAddress', related_name='primary_ip4_for', on_delete=models.SET_NULL, blank=True, null=True,
+        verbose_name='Primary IPv4'
+    )
+    primary_ip6 = models.OneToOneField(
+        'ipam.IPAddress', related_name='primary_ip6_for', on_delete=models.SET_NULL, blank=True, null=True,
+        verbose_name='Primary IPv6'
+    )
     comments = models.TextField(blank=True)
     custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
     images = GenericRelation(ImageAttachment)
@@ -1108,6 +1132,9 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
         """
         return Device.objects.filter(parent_bay__device=self.pk)
 
+    def get_status_class(self):
+        return DEVICE_STATUS_CLASSES[self.status]
+
     def get_rpc_client(self):
         """
         Return the appropriate RPC (e.g. NETCONF, ssh, etc.) client for this device's platform, if one is defined.

+ 4 - 8
netbox/dcim/tables.py

@@ -92,12 +92,8 @@ DEVICE_ROLE = """
 <label class="label" style="background-color: #{{ record.device_role.color }}">{{ value }}</label>
 """
 
-STATUS_ICON = """
-{% if record.status %}
-    <span class="glyphicon glyphicon-ok-sign text-success" title="Active" aria-hidden="true"></span>
-{% else %}
-    <span class="glyphicon glyphicon-minus-sign text-danger" title="Offline" aria-hidden="true"></span>
-{% endif %}
+DEVICE_STATUS = """
+<span class="label label-{{ record.get_status_class }}">{{ record.get_status_display }}</span>
 """
 
 DEVICE_PRIMARY_IP = """
@@ -432,7 +428,7 @@ class PlatformTable(BaseTable):
 class DeviceTable(BaseTable):
     pk = ToggleColumn()
     name = tables.TemplateColumn(template_code=DEVICE_LINK)
-    status = tables.TemplateColumn(template_code=STATUS_ICON, verbose_name='')
+    status = tables.TemplateColumn(template_code=DEVICE_STATUS, verbose_name='Status')
     tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
     site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
     rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')])
@@ -452,7 +448,7 @@ class DeviceTable(BaseTable):
 
 class DeviceSearchTable(SearchTable):
     name = tables.TemplateColumn(template_code=DEVICE_LINK)
-    status = tables.TemplateColumn(template_code=STATUS_ICON, verbose_name='')
+    status = tables.TemplateColumn(template_code=DEVICE_STATUS, verbose_name='Status')
     tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
     site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
     rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')])

+ 3 - 3
netbox/extras/management/commands/run_inventory.py

@@ -6,7 +6,7 @@ from django.conf import settings
 from django.core.management.base import BaseCommand, CommandError
 from django.db import transaction
 
-from dcim.models import Device, InventoryItem, Site
+from dcim.models import Device, InventoryItem, Site, STATUS_ACTIVE
 
 
 class Command(BaseCommand):
@@ -39,7 +39,7 @@ class Command(BaseCommand):
             self.password = getpass("Password: ")
 
         # Attempt to inventory only active devices
-        device_list = Device.objects.filter(status=True)
+        device_list = Device.objects.filter(status=STATUS_ACTIVE)
 
         # --site: Include only devices belonging to specified site(s)
         if options['site']:
@@ -72,7 +72,7 @@ class Command(BaseCommand):
 
             # Skip inactive devices
             if not device.status:
-                self.stdout.write("Skipped (inactive)")
+                self.stdout.write("Skipped (not active)")
                 continue
 
             # Skip devices without primary_ip set

+ 1 - 5
netbox/templates/dcim/device.html

@@ -123,11 +123,7 @@
                 <tr>
                     <td>Status</td>
                     <td>
-                        {% if device.status %}
-                            <span class="label label-success">{{ device.get_status_display }}</span>
-                        {% else %}
-                            <span class="label label-danger">{{ device.get_status_display }}</span>
-                        {% endif %}
+                        <span class="label label-{{ device.get_status_class }}">{{ device.get_status_display }}</span>
                     </td>
                 </tr>
                 <tr>

+ 1 - 1
netbox/templates/dcim/device_edit.html

@@ -55,8 +55,8 @@
     <div class="panel panel-default">
         <div class="panel-heading"><strong>Management</strong></div>
         <div class="panel-body">
-            {% render_field form.platform %}
             {% render_field form.status %}
+            {% render_field form.platform %}
             {% if obj.pk %}
                 {% render_field form.primary_ip4 %}
                 {% render_field form.primary_ip6 %}