Closes #154: Expand device status field options

This commit is contained in:
Jeremy Stretch 2017-05-08 13:55:19 -04:00
parent 7eb9c8265c
commit 77247cccbe
8 changed files with 108 additions and 63 deletions

View File

@ -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():

View File

@ -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( rack = forms.ModelChoiceField(
api_url='/api/dcim/racks/?site_id={{site}}', queryset=Rack.objects.all(), required=False, widget=APISelect(
display_field='display_name', api_url='/api/dcim/racks/?site_id={{site}}',
attrs={'filter-for': 'position'} 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}}', position = forms.TypedChoiceField(
disabled_indicator='device')) required=False, empty_value=None, help_text="The lowest-numbered unit occupied by the device",
manufacturer = forms.ModelChoiceField(queryset=Manufacturer.objects.all(), widget=APISelect(api_url='/api/dcim/racks/{{rack}}/units/?face={{face}}', disabled_indicator='device')
widget=forms.Select(attrs={'filter-for': 'device_type'})) )
device_type = forms.ModelChoiceField(queryset=DeviceType.objects.all(), label='Device type', widget=APISelect( manufacturer = forms.ModelChoiceField(
api_url='/api/dcim/device-types/?manufacturer_id={{manufacturer}}', queryset=Manufacturer.objects.all(), widget=forms.Select(attrs={'filter-for': 'device_type'})
display_field='model' )
)) 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', fields = [
'face', 'status', 'platform', 'primary_ip4', 'primary_ip6', 'comments'] '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( manufacturer_id = FilterChoiceField(queryset=Manufacturer.objects.all(), label='Manufacturer')
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( status = forms.ChoiceField(required=False, choices=device_status_choices)
required=False, mac_address = forms.CharField(required=False, label='MAC address')
widget=forms.Select(choices=FORM_STATUS_CHOICES),
)
mac_address = forms.CharField(
required=False,
label='MAC address',
)
# #

View File

@ -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'),
),
]

View File

@ -178,13 +178,30 @@ VIRTUAL_IFACE_TYPES = [
IFACE_FF_LAG, IFACE_FF_LAG,
] ]
STATUS_ACTIVE = True STATUS_OFFLINE = 0
STATUS_OFFLINE = False 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', asset_tag = NullableCharField(
help_text='A unique tag used to identify this device') 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)], position = models.PositiveSmallIntegerField(
verbose_name='Position (U)', blank=True, null=True, validators=[MinValueValidator(1)], verbose_name='Position (U)',
help_text='The lowest-numbered unit occupied by the device') 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') 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, primary_ip4 = models.OneToOneField(
blank=True, null=True, verbose_name='Primary IPv4') 'ipam.IPAddress', related_name='primary_ip4_for', on_delete=models.SET_NULL, blank=True, null=True,
primary_ip6 = models.OneToOneField('ipam.IPAddress', related_name='primary_ip6_for', on_delete=models.SET_NULL, verbose_name='Primary IPv4'
blank=True, null=True, verbose_name='Primary IPv6') )
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.

View File

@ -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 = """ DEVICE_STATUS = """
{% if record.status %} <span class="label label-{{ record.get_status_class }}">{{ record.get_status_display }}</span>
<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_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')])

View File

@ -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

View File

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

View File

@ -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 %}