Browse Source

Closes #154: Expand device status field options

Jeremy Stretch 8 years ago
parent
commit
77247cccbe

+ 1 - 5
netbox/dcim/filters.py

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

+ 32 - 29
netbox/dcim/forms.py

@@ -532,27 +532,32 @@ class PlatformForm(BootstrapMixin, forms.ModelForm):
 
 
 class DeviceForm(BootstrapMixin, CustomFieldForm):
 class DeviceForm(BootstrapMixin, CustomFieldForm):
     site = forms.ModelChoiceField(queryset=Site.objects.all(), widget=forms.Select(attrs={'filter-for': 'rack'}))
     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()
     comments = CommentField()
 
 
     class Meta:
     class Meta:
         model = Device
         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 = {
         help_texts = {
             'device_role': "The function this device serves",
             'device_role': "The function this device serves",
             'serial': "Chassis serial number",
             'serial': "Chassis serial number",
@@ -764,6 +769,13 @@ class DeviceBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
         nullable_fields = ['tenant', 'platform']
         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):
 class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
     model = Device
     model = Device
     q = forms.CharField(required=False, label='Search')
     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',
         queryset=Tenant.objects.annotate(filter_count=Count('devices')), to_field_name='slug',
         null_option=(0, 'None'),
         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(
     device_type_id = FilterChoiceField(
         queryset=DeviceType.objects.select_related('manufacturer').order_by('model').annotate(
         queryset=DeviceType.objects.select_related('manufacturer').order_by('model').annotate(
             filter_count=Count('instances'),
             filter_count=Count('instances'),
@@ -798,14 +807,8 @@ class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
         to_field_name='slug',
         to_field_name='slug',
         null_option=(0, 'None'),
         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,
     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_CHOICES = [
     [STATUS_ACTIVE, 'Active'],
     [STATUS_ACTIVE, 'Active'],
     [STATUS_OFFLINE, 'Offline'],
     [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_PLANNED = False
 CONNECTION_STATUS_CONNECTED = True
 CONNECTION_STATUS_CONNECTED = True
 CONNECTION_STATUS_CHOICES = [
 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)
     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)
     name = NullableCharField(max_length=64, blank=True, null=True, unique=True)
     serial = models.CharField(max_length=50, blank=True, verbose_name='Serial number')
     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)
     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)
     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')
     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)
     comments = models.TextField(blank=True)
     custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
     custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
     images = GenericRelation(ImageAttachment)
     images = GenericRelation(ImageAttachment)
@@ -1108,6 +1132,9 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
         """
         """
         return Device.objects.filter(parent_bay__device=self.pk)
         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):
     def get_rpc_client(self):
         """
         """
         Return the appropriate RPC (e.g. NETCONF, ssh, etc.) client for this device's platform, if one is defined.
         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>
 <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 = """
 DEVICE_PRIMARY_IP = """
@@ -432,7 +428,7 @@ class PlatformTable(BaseTable):
 class DeviceTable(BaseTable):
 class DeviceTable(BaseTable):
     pk = ToggleColumn()
     pk = ToggleColumn()
     name = tables.TemplateColumn(template_code=DEVICE_LINK)
     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')])
     tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
     site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
     site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
     rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')])
     rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')])
@@ -452,7 +448,7 @@ class DeviceTable(BaseTable):
 
 
 class DeviceSearchTable(SearchTable):
 class DeviceSearchTable(SearchTable):
     name = tables.TemplateColumn(template_code=DEVICE_LINK)
     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')])
     tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
     site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
     site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
     rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')])
     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.core.management.base import BaseCommand, CommandError
 from django.db import transaction
 from django.db import transaction
 
 
-from dcim.models import Device, InventoryItem, Site
+from dcim.models import Device, InventoryItem, Site, STATUS_ACTIVE
 
 
 
 
 class Command(BaseCommand):
 class Command(BaseCommand):
@@ -39,7 +39,7 @@ class Command(BaseCommand):
             self.password = getpass("Password: ")
             self.password = getpass("Password: ")
 
 
         # Attempt to inventory only active devices
         # 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)
         # --site: Include only devices belonging to specified site(s)
         if options['site']:
         if options['site']:
@@ -72,7 +72,7 @@ class Command(BaseCommand):
 
 
             # Skip inactive devices
             # Skip inactive devices
             if not device.status:
             if not device.status:
-                self.stdout.write("Skipped (inactive)")
+                self.stdout.write("Skipped (not active)")
                 continue
                 continue
 
 
             # Skip devices without primary_ip set
             # Skip devices without primary_ip set

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

@@ -123,11 +123,7 @@
                 <tr>
                 <tr>
                     <td>Status</td>
                     <td>Status</td>
                     <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>
                     </td>
                 </tr>
                 </tr>
                 <tr>
                 <tr>

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

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