mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-23 17:08:41 -06:00
Closes #154: Expand device status field options
This commit is contained in:
parent
7eb9c8265c
commit
77247cccbe
@ -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():
|
||||||
|
@ -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',
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
27
netbox/dcim/migrations/0035_device_expand_status_choices.py
Normal file
27
netbox/dcim/migrations/0035_device_expand_status_choices.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
@ -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.
|
||||||
|
@ -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')])
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
||||||
|
@ -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 %}
|
||||||
|
Loading…
Reference in New Issue
Block a user