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

Implemented tags for all primary models

Jeremy Stretch 7 лет назад
Родитель
Сommit
9b3869790d
43 измененных файлов с 262 добавлено и 34 удалено
  1. 6 3
      netbox/circuits/api/serializers.py
  2. 5 2
      netbox/circuits/forms.py
  3. 5 0
      netbox/circuits/models.py
  4. 2 2
      netbox/dcim/forms.py
  5. 16 7
      netbox/ipam/api/serializers.py
  6. 14 5
      netbox/ipam/forms.py
  7. 9 0
      netbox/ipam/models.py
  8. 4 2
      netbox/secrets/api/serializers.py
  9. 3 1
      netbox/secrets/forms.py
  10. 3 0
      netbox/secrets/models.py
  11. 4 0
      netbox/templates/circuits/circuit.html
  12. 6 0
      netbox/templates/circuits/circuit_edit.html
  13. 4 0
      netbox/templates/circuits/provider.html
  14. 6 0
      netbox/templates/circuits/provider_edit.html
  15. 6 0
      netbox/templates/dcim/device_edit.html
  16. 6 1
      netbox/templates/dcim/devicetype_edit.html
  17. 6 1
      netbox/templates/dcim/rack_edit.html
  18. 6 1
      netbox/templates/dcim/site_edit.html
  19. 4 0
      netbox/templates/ipam/aggregate.html
  20. 6 0
      netbox/templates/ipam/aggregate_edit.html
  21. 4 0
      netbox/templates/ipam/ipaddress.html
  22. 6 0
      netbox/templates/ipam/ipaddress_edit.html
  23. 4 0
      netbox/templates/ipam/prefix.html
  24. 6 0
      netbox/templates/ipam/prefix_edit.html
  25. 4 0
      netbox/templates/ipam/vlan.html
  26. 6 0
      netbox/templates/ipam/vlan_edit.html
  27. 4 0
      netbox/templates/ipam/vrf.html
  28. 6 0
      netbox/templates/ipam/vrf_edit.html
  29. 4 0
      netbox/templates/secrets/secret.html
  30. 6 0
      netbox/templates/secrets/secret_edit.html
  31. 4 0
      netbox/templates/tenancy/tenant.html
  32. 6 0
      netbox/templates/tenancy/tenant_edit.html
  33. 4 0
      netbox/templates/virtualization/cluster.html
  34. 34 0
      netbox/templates/virtualization/cluster_edit.html
  35. 4 0
      netbox/templates/virtualization/virtualmachine.html
  36. 6 0
      netbox/templates/virtualization/virtualmachine_edit.html
  37. 7 2
      netbox/tenancy/api/serializers.py
  38. 3 1
      netbox/tenancy/forms.py
  39. 3 0
      netbox/tenancy/models.py
  40. 9 4
      netbox/virtualization/api/serializers.py
  41. 5 2
      netbox/virtualization/forms.py
  42. 5 0
      netbox/virtualization/models.py
  43. 1 0
      netbox/virtualization/views.py

+ 6 - 3
netbox/circuits/api/serializers.py

@@ -1,13 +1,14 @@
 from __future__ import unicode_literals
 
 from rest_framework import serializers
+from taggit.models import Tag
 
 from circuits.constants import CIRCUIT_STATUS_CHOICES
 from circuits.models import Provider, Circuit, CircuitTermination, CircuitType
 from dcim.api.serializers import NestedSiteSerializer, InterfaceSerializer
 from extras.api.customfields import CustomFieldModelSerializer
 from tenancy.api.serializers import NestedTenantSerializer
-from utilities.api import ChoiceFieldSerializer, ValidatedModelSerializer, WritableNestedSerializer
+from utilities.api import ChoiceFieldSerializer, TagField, ValidatedModelSerializer, WritableNestedSerializer
 
 
 #
@@ -15,11 +16,12 @@ from utilities.api import ChoiceFieldSerializer, ValidatedModelSerializer, Writa
 #
 
 class ProviderSerializer(CustomFieldModelSerializer):
+    tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
 
     class Meta:
         model = Provider
         fields = [
-            'id', 'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments',
+            'id', 'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments', 'tags',
             'custom_fields', 'created', 'last_updated',
         ]
 
@@ -60,12 +62,13 @@ class CircuitSerializer(CustomFieldModelSerializer):
     status = ChoiceFieldSerializer(choices=CIRCUIT_STATUS_CHOICES, required=False)
     type = NestedCircuitTypeSerializer()
     tenant = NestedTenantSerializer(required=False, allow_null=True)
+    tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
 
     class Meta:
         model = Circuit
         fields = [
             'id', 'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description',
-            'comments', 'custom_fields', 'created', 'last_updated',
+            'comments', 'tags', 'custom_fields', 'created', 'last_updated',
         ]
 
 

+ 5 - 2
netbox/circuits/forms.py

@@ -2,6 +2,7 @@ from __future__ import unicode_literals
 
 from django import forms
 from django.db.models import Count
+from taggit.forms import TagField
 
 from dcim.models import Site, Device, Interface, Rack
 from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
@@ -22,10 +23,11 @@ from .models import Circuit, CircuitTermination, CircuitType, Provider
 class ProviderForm(BootstrapMixin, CustomFieldForm):
     slug = SlugField()
     comments = CommentField()
+    tags = TagField(required=False)
 
     class Meta:
         model = Provider
-        fields = ['name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments']
+        fields = ['name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments', 'tags']
         widgets = {
             'noc_contact': SmallTextarea(attrs={'rows': 5}),
             'admin_contact': SmallTextarea(attrs={'rows': 5}),
@@ -102,12 +104,13 @@ class CircuitTypeCSVForm(forms.ModelForm):
 
 class CircuitForm(BootstrapMixin, TenancyForm, CustomFieldForm):
     comments = CommentField()
+    tags = TagField(required=False)
 
     class Meta:
         model = Circuit
         fields = [
             'cid', 'type', 'provider', 'status', 'install_date', 'commit_rate', 'description', 'tenant_group', 'tenant',
-            'comments',
+            'comments', 'tags',
         ]
         help_texts = {
             'cid': "Unique circuit ID",

+ 5 - 0
netbox/circuits/models.py

@@ -4,6 +4,7 @@ from django.contrib.contenttypes.fields import GenericRelation
 from django.db import models
 from django.urls import reverse
 from django.utils.encoding import python_2_unicode_compatible
+from taggit.managers import TaggableManager
 
 from dcim.constants import STATUS_CLASSES
 from dcim.fields import ASNField
@@ -56,6 +57,8 @@ class Provider(CreatedUpdatedModel, CustomFieldModel):
         object_id_field='obj_id'
     )
 
+    tags = TaggableManager()
+
     csv_headers = ['name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments']
 
     class Meta:
@@ -166,6 +169,8 @@ class Circuit(CreatedUpdatedModel, CustomFieldModel):
         object_id_field='obj_id'
     )
 
+    tags = TaggableManager()
+
     csv_headers = [
         'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description', 'comments',
     ]

+ 2 - 2
netbox/dcim/forms.py

@@ -784,8 +784,8 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldForm):
     class Meta:
         model = Device
         fields = [
-            'name', 'device_role', 'tags', 'device_type', 'serial', 'asset_tag', 'site', 'rack', 'position', 'face',
-            'status', 'platform', 'primary_ip4', 'primary_ip6', 'tenant_group', 'tenant', 'comments',
+            'name', 'device_role', 'device_type', 'serial', 'asset_tag', 'site', 'rack', 'position', 'face',
+            'status', 'platform', 'primary_ip4', 'primary_ip6', 'tenant_group', 'tenant', 'comments', 'tags',
         ]
         help_texts = {
             'device_role': "The function this device serves",

+ 16 - 7
netbox/ipam/api/serializers.py

@@ -5,6 +5,7 @@ from collections import OrderedDict
 from rest_framework import serializers
 from rest_framework.reverse import reverse
 from rest_framework.validators import UniqueTogetherValidator
+from taggit.models import Tag
 
 from dcim.api.serializers import NestedDeviceSerializer, InterfaceSerializer, NestedSiteSerializer
 from dcim.models import Interface
@@ -14,7 +15,9 @@ from ipam.constants import (
 )
 from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
 from tenancy.api.serializers import NestedTenantSerializer
-from utilities.api import ChoiceFieldSerializer, SerializedPKRelatedField, ValidatedModelSerializer, WritableNestedSerializer
+from utilities.api import (
+    ChoiceFieldSerializer, SerializedPKRelatedField, TagField, ValidatedModelSerializer, WritableNestedSerializer,
+)
 from virtualization.api.serializers import NestedVirtualMachineSerializer
 
 
@@ -24,12 +27,13 @@ from virtualization.api.serializers import NestedVirtualMachineSerializer
 
 class VRFSerializer(CustomFieldModelSerializer):
     tenant = NestedTenantSerializer(required=False, allow_null=True)
+    tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
 
     class Meta:
         model = VRF
         fields = [
-            'id', 'name', 'rd', 'tenant', 'enforce_unique', 'description', 'display_name', 'custom_fields', 'created',
-            'last_updated',
+            'id', 'name', 'rd', 'tenant', 'enforce_unique', 'description', 'tags', 'display_name', 'custom_fields',
+            'created', 'last_updated',
         ]
 
 
@@ -85,11 +89,13 @@ class NestedRIRSerializer(WritableNestedSerializer):
 
 class AggregateSerializer(CustomFieldModelSerializer):
     rir = NestedRIRSerializer()
+    tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
 
     class Meta:
         model = Aggregate
         fields = [
-            'id', 'family', 'prefix', 'rir', 'date_added', 'description', 'custom_fields', 'created', 'last_updated',
+            'id', 'family', 'prefix', 'rir', 'date_added', 'description', 'tags', 'custom_fields', 'created',
+            'last_updated',
         ]
         read_only_fields = ['family']
 
@@ -147,11 +153,12 @@ class VLANSerializer(CustomFieldModelSerializer):
     tenant = NestedTenantSerializer(required=False, allow_null=True)
     status = ChoiceFieldSerializer(choices=VLAN_STATUS_CHOICES, required=False)
     role = NestedRoleSerializer(required=False, allow_null=True)
+    tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
 
     class Meta:
         model = VLAN
         fields = [
-            'id', 'site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description', 'display_name',
+            'id', 'site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description', 'tags', 'display_name',
             'custom_fields', 'created', 'last_updated',
         ]
         validators = []
@@ -190,12 +197,13 @@ class PrefixSerializer(CustomFieldModelSerializer):
     vlan = NestedVLANSerializer(required=False, allow_null=True)
     status = ChoiceFieldSerializer(choices=PREFIX_STATUS_CHOICES, required=False)
     role = NestedRoleSerializer(required=False, allow_null=True)
+    tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
 
     class Meta:
         model = Prefix
         fields = [
             'id', 'family', 'prefix', 'site', 'vrf', 'tenant', 'vlan', 'status', 'role', 'is_pool', 'description',
-            'custom_fields', 'created', 'last_updated',
+            'tags', 'custom_fields', 'created', 'last_updated',
         ]
         read_only_fields = ['family']
 
@@ -252,12 +260,13 @@ class IPAddressSerializer(CustomFieldModelSerializer):
     status = ChoiceFieldSerializer(choices=IPADDRESS_STATUS_CHOICES, required=False)
     role = ChoiceFieldSerializer(choices=IPADDRESS_ROLE_CHOICES, required=False)
     interface = IPAddressInterfaceSerializer(required=False, allow_null=True)
+    tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
 
     class Meta:
         model = IPAddress
         fields = [
             'id', 'family', 'address', 'vrf', 'tenant', 'status', 'role', 'interface', 'description', 'nat_inside',
-            'nat_outside', 'custom_fields', 'created', 'last_updated',
+            'nat_outside', 'tags', 'custom_fields', 'created', 'last_updated',
         ]
         read_only_fields = ['family']
 

+ 14 - 5
netbox/ipam/forms.py

@@ -3,6 +3,7 @@ from __future__ import unicode_literals
 from django import forms
 from django.core.exceptions import MultipleObjectsReturned
 from django.db.models import Count
+from taggit.forms import TagField
 
 from dcim.models import Site, Rack, Device, Interface
 from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
@@ -32,10 +33,11 @@ IPADDRESS_MASK_LENGTH_CHOICES = add_blank_choice([(i, i) for i in range(1, 129)]
 #
 
 class VRFForm(BootstrapMixin, TenancyForm, CustomFieldForm):
+    tags = TagField(required=False)
 
     class Meta:
         model = VRF
-        fields = ['name', 'rd', 'enforce_unique', 'description', 'tenant_group', 'tenant']
+        fields = ['name', 'rd', 'enforce_unique', 'description', 'tenant_group', 'tenant', 'tags']
         labels = {
             'rd': "RD",
         }
@@ -121,10 +123,11 @@ class RIRFilterForm(BootstrapMixin, forms.Form):
 #
 
 class AggregateForm(BootstrapMixin, CustomFieldForm):
+    tags = TagField(required=False)
 
     class Meta:
         model = Aggregate
-        fields = ['prefix', 'rir', 'date_added', 'description']
+        fields = ['prefix', 'rir', 'date_added', 'description', 'tags']
         help_texts = {
             'prefix': "IPv4 or IPv6 network",
             'rir': "Regional Internet Registry responsible for this prefix",
@@ -228,10 +231,14 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldForm):
             api_url='/api/ipam/vlans/?site_id={{site}}&group_id={{vlan_group}}', display_field='display_name'
         )
     )
+    tags = TagField(required=False)
 
     class Meta:
         model = Prefix
-        fields = ['prefix', 'vrf', 'site', 'vlan', 'status', 'role', 'is_pool', 'description', 'tenant_group', 'tenant']
+        fields = [
+            'prefix', 'vrf', 'site', 'vlan', 'status', 'role', 'is_pool', 'description', 'tenant_group', 'tenant',
+            'tags',
+        ]
 
     def __init__(self, *args, **kwargs):
 
@@ -455,12 +462,13 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm)
         )
     )
     primary_for_parent = forms.BooleanField(required=False, label='Make this the primary IP for the device/VM')
+    tags = TagField(required=False)
 
     class Meta:
         model = IPAddress
         fields = [
             'address', 'vrf', 'status', 'role', 'description', 'interface', 'primary_for_parent', 'nat_site',
-            'nat_rack', 'nat_inside', 'tenant_group', 'tenant',
+            'nat_rack', 'nat_inside', 'tenant_group', 'tenant', 'tags',
         ]
 
     def __init__(self, *args, **kwargs):
@@ -780,10 +788,11 @@ class VLANForm(BootstrapMixin, TenancyForm, CustomFieldForm):
             api_url='/api/ipam/vlan-groups/?site_id={{site}}',
         )
     )
+    tags = TagField(required=False)
 
     class Meta:
         model = VLAN
-        fields = ['site', 'group', 'vid', 'name', 'status', 'role', 'description', 'tenant_group', 'tenant']
+        fields = ['site', 'group', 'vid', 'name', 'status', 'role', 'description', 'tenant_group', 'tenant', 'tags']
         help_texts = {
             'site': "Leave blank if this VLAN spans multiple sites",
             'group': "VLAN group (optional)",

+ 9 - 0
netbox/ipam/models.py

@@ -10,6 +10,7 @@ from django.db.models import Q
 from django.db.models.expressions import RawSQL
 from django.urls import reverse
 from django.utils.encoding import python_2_unicode_compatible
+from taggit.managers import TaggableManager
 
 from dcim.models import Interface
 from extras.models import CustomFieldModel
@@ -56,6 +57,8 @@ class VRF(CreatedUpdatedModel, CustomFieldModel):
         object_id_field='obj_id'
     )
 
+    tags = TaggableManager()
+
     csv_headers = ['name', 'rd', 'tenant', 'enforce_unique', 'description']
 
     class Meta:
@@ -155,6 +158,8 @@ class Aggregate(CreatedUpdatedModel, CustomFieldModel):
         object_id_field='obj_id'
     )
 
+    tags = TaggableManager()
+
     csv_headers = ['prefix', 'rir', 'date_added', 'description']
 
     class Meta:
@@ -325,6 +330,7 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel):
     )
 
     objects = PrefixQuerySet.as_manager()
+    tags = TaggableManager()
 
     csv_headers = [
         'prefix', 'vrf', 'tenant', 'site', 'vlan_group', 'vlan_vid', 'status', 'role', 'is_pool', 'description',
@@ -564,6 +570,7 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel):
     )
 
     objects = IPAddressManager()
+    tags = TaggableManager()
 
     csv_headers = [
         'address', 'vrf', 'tenant', 'status', 'role', 'device', 'virtual_machine', 'interface_name', 'is_primary',
@@ -759,6 +766,8 @@ class VLAN(CreatedUpdatedModel, CustomFieldModel):
         object_id_field='obj_id'
     )
 
+    tags = TaggableManager()
+
     csv_headers = ['site', 'group_name', 'vid', 'name', 'tenant', 'status', 'role', 'description']
 
     class Meta:

+ 4 - 2
netbox/secrets/api/serializers.py

@@ -2,10 +2,11 @@ from __future__ import unicode_literals
 
 from rest_framework import serializers
 from rest_framework.validators import UniqueTogetherValidator
+from taggit.models import Tag
 
 from dcim.api.serializers import NestedDeviceSerializer
 from secrets.models import Secret, SecretRole
-from utilities.api import ValidatedModelSerializer, WritableNestedSerializer
+from utilities.api import TagField, ValidatedModelSerializer, WritableNestedSerializer
 
 
 #
@@ -35,10 +36,11 @@ class SecretSerializer(ValidatedModelSerializer):
     device = NestedDeviceSerializer()
     role = NestedSecretRoleSerializer()
     plaintext = serializers.CharField()
+    tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
 
     class Meta:
         model = Secret
-        fields = ['id', 'device', 'role', 'name', 'plaintext', 'hash', 'created', 'last_updated']
+        fields = ['id', 'device', 'role', 'name', 'plaintext', 'hash', 'tags', 'created', 'last_updated']
         validators = []
 
     def validate(self, data):

+ 3 - 1
netbox/secrets/forms.py

@@ -4,6 +4,7 @@ from Crypto.Cipher import PKCS1_OAEP
 from Crypto.PublicKey import RSA
 from django import forms
 from django.db.models import Count
+from taggit.forms import TagField
 
 from dcim.models import Device
 from utilities.forms import BootstrapMixin, BulkEditForm, FilterChoiceField, FlexibleModelChoiceField, SlugField
@@ -70,10 +71,11 @@ class SecretForm(BootstrapMixin, forms.ModelForm):
         label='Plaintext (verify)',
         widget=forms.PasswordInput()
     )
+    tags = TagField(required=False)
 
     class Meta:
         model = Secret
-        fields = ['role', 'name', 'plaintext', 'plaintext2']
+        fields = ['role', 'name', 'plaintext', 'plaintext2', 'tags']
 
     def __init__(self, *args, **kwargs):
 

+ 3 - 0
netbox/secrets/models.py

@@ -12,6 +12,7 @@ from django.core.exceptions import ValidationError
 from django.db import models
 from django.urls import reverse
 from django.utils.encoding import force_bytes, python_2_unicode_compatible
+from taggit.managers import TaggableManager
 
 from utilities.models import CreatedUpdatedModel
 from .exceptions import InvalidKey
@@ -336,6 +337,8 @@ class Secret(CreatedUpdatedModel):
         editable=False
     )
 
+    tags = TaggableManager()
+
     plaintext = None
     csv_headers = ['device', 'role', 'name', 'plaintext']
 

+ 4 - 0
netbox/templates/circuits/circuit.html

@@ -110,6 +110,10 @@
                         {% endif %}
                     </td>
                 </tr>
+                <tr>
+                    <td>Tags</td>
+                    <td>{{ circuit.tags.all|join:" " }}</td>
+                </tr>
             </table>
         </div>
         {% with circuit.get_custom_fields as custom_fields %}

+ 6 - 0
netbox/templates/circuits/circuit_edit.html

@@ -44,6 +44,12 @@
             {% render_field form.comments %}
         </div>
     </div>
+    <div class="panel panel-default">
+        <div class="panel-heading"><strong>Tags</strong></div>
+        <div class="panel-body">
+            {% render_field form.tags %}
+        </div>
+    </div>
 {% endblock %}
 
 {% block javascript %}

+ 4 - 0
netbox/templates/circuits/provider.html

@@ -102,6 +102,10 @@
                         {% endif %}
                     </td>
                 </tr>
+                <tr>
+                    <td>Tags</td>
+                    <td>{{ provider.tags.all|join:" " }}</td>
+                </tr>
                 <tr>
                     <td>Circuits</td>
                     <td>

+ 6 - 0
netbox/templates/circuits/provider_edit.html

@@ -33,4 +33,10 @@
             {% render_field form.comments %}
         </div>
     </div>
+    <div class="panel panel-default">
+        <div class="panel-heading"><strong>Tags</strong></div>
+        <div class="panel-body">
+            {% render_field form.tags %}
+        </div>
+    </div>
 {% endblock %}

+ 6 - 0
netbox/templates/dcim/device_edit.html

@@ -84,4 +84,10 @@
             {% render_field form.comments %}
         </div>
     </div>
+    <div class="panel panel-default">
+        <div class="panel-heading"><strong>Tags</strong></div>
+        <div class="panel-body">
+            {% render_field form.tags %}
+        </div>
+    </div>
 {% endblock %}

+ 6 - 1
netbox/templates/dcim/devicetype_edit.html

@@ -12,7 +12,6 @@
             {% render_field form.u_height %}
             {% render_field form.is_full_depth %}
             {% render_field form.interface_ordering %}
-            {% render_field form.tags %}
         </div>
     </div>
     <div class="panel panel-default">
@@ -38,4 +37,10 @@
             {% render_field form.comments %}
         </div>
     </div>
+    <div class="panel panel-default">
+        <div class="panel-heading"><strong>Tags</strong></div>
+        <div class="panel-body">
+            {% render_field form.tags %}
+        </div>
+    </div>
 {% endblock %}

+ 6 - 1
netbox/templates/dcim/rack_edit.html

@@ -11,7 +11,6 @@
             {% render_field form.group %}
             {% render_field form.role %}
             {% render_field form.serial %}
-            {% render_field form.tags %}
         </div>
     </div>
     <div class="panel panel-default">
@@ -44,4 +43,10 @@
             {% render_field form.comments %}
         </div>
     </div>
+    <div class="panel panel-default">
+        <div class="panel-heading"><strong>Tags</strong></div>
+        <div class="panel-body">
+            {% render_field form.tags %}
+        </div>
+    </div>
 {% endblock %}

+ 6 - 1
netbox/templates/dcim/site_edit.html

@@ -13,7 +13,6 @@
             {% render_field form.asn %}
             {% render_field form.time_zone %}
             {% render_field form.description %}
-            {% render_field form.tags %}
         </div>
     </div>
     <div class="panel panel-default">
@@ -47,4 +46,10 @@
             {% render_field form.comments %}
         </div>
     </div>
+    <div class="panel panel-default">
+        <div class="panel-heading"><strong>Tags</strong></div>
+        <div class="panel-body">
+            {% render_field form.tags %}
+        </div>
+    </div>
 {% endblock %}

+ 4 - 0
netbox/templates/ipam/aggregate.html

@@ -81,6 +81,10 @@
                         {% endif %}
                     </td>
                 </tr>
+                <tr>
+                    <td>Tags</td>
+                    <td>{{ aggregate.tags.all|join:" " }}</td>
+                </tr>
             </table>
         </div>
     </div>

+ 6 - 0
netbox/templates/ipam/aggregate_edit.html

@@ -19,4 +19,10 @@
             </div>
         </div>
     {% endif %}
+    <div class="panel panel-default">
+        <div class="panel-heading"><strong>Tags</strong></div>
+        <div class="panel-body">
+            {% render_field form.tags %}
+        </div>
+    </div>
 {% endblock %}

+ 4 - 0
netbox/templates/ipam/ipaddress.html

@@ -133,6 +133,10 @@
                         {% endif %}
                     </td>
                 </tr>
+                <tr>
+                    <td>Tags</td>
+                    <td>{{ ipaddress.tags.all|join:" " }}</td>
+                </tr>
             </table>
         </div>
         {% with ipaddress.get_custom_fields as custom_fields %}

+ 6 - 0
netbox/templates/ipam/ipaddress_edit.html

@@ -66,6 +66,12 @@
             {% render_field form.nat_inside %}
         </div>
     </div>
+    <div class="panel panel-default">
+        <div class="panel-heading"><strong>Tags</strong></div>
+        <div class="panel-body">
+            {% render_field form.tags %}
+        </div>
+    </div>
     {% if form.custom_fields %}
         <div class="panel panel-default">
             <div class="panel-heading"><strong>Custom Fields</strong></div>

+ 4 - 0
netbox/templates/ipam/prefix.html

@@ -121,6 +121,10 @@
                         {% endif %}
                     </td>
                 </tr>
+                <tr>
+                    <td>Tags</td>
+                    <td>{{ prefix.tags.all|join:" " }}</td>
+                </tr>
                 <tr>
                     <td>Utilization</td>
                     <td>{% utilization_graph prefix.get_utilization %}</td>

+ 6 - 0
netbox/templates/ipam/prefix_edit.html

@@ -28,6 +28,12 @@
             {% render_field form.tenant %}
         </div>
     </div>
+    <div class="panel panel-default">
+        <div class="panel-heading"><strong>Tags</strong></div>
+        <div class="panel-body">
+            {% render_field form.tags %}
+        </div>
+    </div>
     {% if form.custom_fields %}
         <div class="panel panel-default">
             <div class="panel-heading"><strong>Custom Fields</strong></div>

+ 4 - 0
netbox/templates/ipam/vlan.html

@@ -80,6 +80,10 @@
                             <span class="text-muted">N/A</span>
                         {% endif %}
                     </td>
+                </tr>
+                <tr>
+                    <td>Tags</td>
+                    <td>{{ vlan.tags.all|join:" " }}</td>
                 </tr>
 		    </table>
         </div>

+ 6 - 0
netbox/templates/ipam/vlan_edit.html

@@ -21,6 +21,12 @@
             {% render_field form.tenant %}
         </div>
     </div>
+    <div class="panel panel-default">
+        <div class="panel-heading"><strong>Tags</strong></div>
+        <div class="panel-body">
+            {% render_field form.tags %}
+        </div>
+    </div>
     {% if form.custom_fields %}
         <div class="panel panel-default">
             <div class="panel-heading"><strong>Custom Fields</strong></div>

+ 4 - 0
netbox/templates/ipam/vrf.html

@@ -77,6 +77,10 @@
                             <span class="text-muted">N/A</span>
                         {% endif %}
                     </td>
+                </tr>
+                <tr>
+                    <td>Tags</td>
+                    <td>{{ vrf.tags.all|join:" " }}</td>
                 </tr>
 		    </table>
         </div>

+ 6 - 0
netbox/templates/ipam/vrf_edit.html

@@ -18,6 +18,12 @@
             {% render_field form.tenant %}
         </div>
     </div>
+    <div class="panel panel-default">
+        <div class="panel-heading"><strong>Tags</strong></div>
+        <div class="panel-body">
+            {% render_field form.tags %}
+        </div>
+    </div>
     {% if form.custom_fields %}
         <div class="panel panel-default">
             <div class="panel-heading"><strong>Custom Fields</strong></div>

+ 4 - 0
netbox/templates/secrets/secret.html

@@ -55,6 +55,10 @@
                         {% endif %}
                     </td>
                 </tr>
+                <tr>
+                    <td>Tags</td>
+                    <td>{{ secret.tags.all|join:" " }}</td>
+                </tr>
             </table>
         </div>
 	</div>

+ 6 - 0
netbox/templates/secrets/secret_edit.html

@@ -54,6 +54,12 @@
                     {% render_field form.plaintext2 %}
                 </div>
             </div>
+            <div class="panel panel-default">
+                <div class="panel-heading"><strong>Tags</strong></div>
+                <div class="panel-body">
+                    {% render_field form.tags %}
+                </div>
+            </div>
         </div>
     </div>
     <div class="row">

+ 4 - 0
netbox/templates/tenancy/tenant.html

@@ -68,6 +68,10 @@
                         {% endif %}
                     </td>
                 </tr>
+                <tr>
+                    <td>Tags</td>
+                    <td>{{ tenant.tags.all|join:" " }}</td>
+                </tr>
             </table>
         </div>
         {% with tenant.get_custom_fields as custom_fields %}

+ 6 - 0
netbox/templates/tenancy/tenant_edit.html

@@ -26,4 +26,10 @@
             {% render_field form.comments %}
         </div>
     </div>
+    <div class="panel panel-default">
+        <div class="panel-heading"><strong>Tags</strong></div>
+        <div class="panel-body">
+            {% render_field form.tags %}
+        </div>
+    </div>
 {% endblock %}

+ 4 - 0
netbox/templates/virtualization/cluster.html

@@ -76,6 +76,10 @@
                         {% endif %}
                     </td>
                 </tr>
+                <tr>
+                    <td>Tags</td>
+                    <td>{{ cluster.tags.all|join:" " }}</td>
+                </tr>
                 <tr>
                     <td>Virtual Machines</td>
                     <td><a href="{% url 'virtualization:virtualmachine_list' %}?cluster_id={{ cluster.pk }}">{{ cluster.virtual_machines.count }}</a></td>

+ 34 - 0
netbox/templates/virtualization/cluster_edit.html

@@ -0,0 +1,34 @@
+{% extends 'utilities/obj_edit.html' %}
+{% load form_helpers %}
+
+{% block form %}
+    <div class="panel panel-default">
+        <div class="panel-heading"><strong>Cluster</strong></div>
+        <div class="panel-body">
+            {% render_field form.name %}
+            {% render_field form.type %}
+            {% render_field form.group %}
+            {% render_field form.site %}
+        </div>
+    </div>
+    {% if form.custom_fields %}
+        <div class="panel panel-default">
+            <div class="panel-heading"><strong>Custom Fields</strong></div>
+            <div class="panel-body">
+                {% render_custom_fields form %}
+            </div>
+        </div>
+    {% endif %}
+    <div class="panel panel-default">
+        <div class="panel-heading"><strong>Comments</strong></div>
+        <div class="panel-body">
+            {% render_field form.comments %}
+        </div>
+    </div>
+    <div class="panel panel-default">
+        <div class="panel-heading"><strong>Tags</strong></div>
+        <div class="panel-body">
+            {% render_field form.tags %}
+        </div>
+    </div>
+{% endblock %}

+ 4 - 0
netbox/templates/virtualization/virtualmachine.html

@@ -121,6 +121,10 @@
                         {% endif %}
                     </td>
                 </tr>
+                <tr>
+                    <td>Tags</td>
+                    <td>{{ vm.tags.all|join:" " }}</td>
+                </tr>
             </table>
         </div>
         {% include 'inc/custom_fields_panel.html' with custom_fields=vm.get_custom_fields %}

+ 6 - 0
netbox/templates/virtualization/virtualmachine_edit.html

@@ -54,4 +54,10 @@
             {% render_field form.comments %}
         </div>
     </div>
+    <div class="panel panel-default">
+        <div class="panel-heading"><strong>Tags</strong></div>
+        <div class="panel-body">
+            {% render_field form.tags %}
+        </div>
+    </div>
 {% endblock %}

+ 7 - 2
netbox/tenancy/api/serializers.py

@@ -1,10 +1,11 @@
 from __future__ import unicode_literals
 
 from rest_framework import serializers
+from taggit.models import Tag
 
 from extras.api.customfields import CustomFieldModelSerializer
 from tenancy.models import Tenant, TenantGroup
-from utilities.api import ValidatedModelSerializer, WritableNestedSerializer
+from utilities.api import TagField, ValidatedModelSerializer, WritableNestedSerializer
 
 
 #
@@ -32,10 +33,14 @@ class NestedTenantGroupSerializer(WritableNestedSerializer):
 
 class TenantSerializer(CustomFieldModelSerializer):
     group = NestedTenantGroupSerializer(required=False)
+    tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
 
     class Meta:
         model = Tenant
-        fields = ['id', 'name', 'slug', 'group', 'description', 'comments', 'custom_fields', 'created', 'last_updated']
+        fields = [
+            'id', 'name', 'slug', 'group', 'description', 'comments', 'tags', 'custom_fields', 'created',
+            'last_updated',
+        ]
 
 
 class NestedTenantSerializer(WritableNestedSerializer):

+ 3 - 1
netbox/tenancy/forms.py

@@ -2,6 +2,7 @@ from __future__ import unicode_literals
 
 from django import forms
 from django.db.models import Count
+from taggit.forms import TagField
 
 from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
 from utilities.forms import (
@@ -40,10 +41,11 @@ class TenantGroupCSVForm(forms.ModelForm):
 class TenantForm(BootstrapMixin, CustomFieldForm):
     slug = SlugField()
     comments = CommentField()
+    tags = TagField(required=False)
 
     class Meta:
         model = Tenant
-        fields = ['name', 'slug', 'group', 'description', 'comments']
+        fields = ['name', 'slug', 'group', 'description', 'comments', 'tags']
 
 
 class TenantCSVForm(forms.ModelForm):

+ 3 - 0
netbox/tenancy/models.py

@@ -4,6 +4,7 @@ from django.contrib.contenttypes.fields import GenericRelation
 from django.db import models
 from django.urls import reverse
 from django.utils.encoding import python_2_unicode_compatible
+from taggit.managers import TaggableManager
 
 from extras.models import CustomFieldModel
 from utilities.models import CreatedUpdatedModel
@@ -74,6 +75,8 @@ class Tenant(CreatedUpdatedModel, CustomFieldModel):
         object_id_field='obj_id'
     )
 
+    tags = TaggableManager()
+
     csv_headers = ['name', 'slug', 'group', 'description', 'comments']
 
     class Meta:

+ 9 - 4
netbox/virtualization/api/serializers.py

@@ -1,14 +1,15 @@
 from __future__ import unicode_literals
 
 from rest_framework import serializers
+from taggit.models import Tag
 
 from dcim.api.serializers import NestedDeviceRoleSerializer, NestedPlatformSerializer, NestedSiteSerializer
-from dcim.constants import IFACE_FF_VIRTUAL, IFACE_MODE_CHOICES
+from dcim.constants import IFACE_MODE_CHOICES
 from dcim.models import Interface
 from extras.api.customfields import CustomFieldModelSerializer
 from ipam.models import IPAddress, VLAN
 from tenancy.api.serializers import NestedTenantSerializer
-from utilities.api import ChoiceFieldSerializer, ValidatedModelSerializer, WritableNestedSerializer
+from utilities.api import ChoiceFieldSerializer, TagField, ValidatedModelSerializer, WritableNestedSerializer
 from virtualization.constants import VM_STATUS_CHOICES
 from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine
 
@@ -59,10 +60,13 @@ class ClusterSerializer(CustomFieldModelSerializer):
     type = NestedClusterTypeSerializer()
     group = NestedClusterGroupSerializer(required=False, allow_null=True)
     site = NestedSiteSerializer(required=False, allow_null=True)
+    tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
 
     class Meta:
         model = Cluster
-        fields = ['id', 'name', 'type', 'group', 'site', 'comments', 'custom_fields', 'created', 'last_updated']
+        fields = [
+            'id', 'name', 'type', 'group', 'site', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
+        ]
 
 
 class NestedClusterSerializer(WritableNestedSerializer):
@@ -95,12 +99,13 @@ class VirtualMachineSerializer(CustomFieldModelSerializer):
     primary_ip = VirtualMachineIPAddressSerializer(read_only=True)
     primary_ip4 = VirtualMachineIPAddressSerializer(required=False, allow_null=True)
     primary_ip6 = VirtualMachineIPAddressSerializer(required=False, allow_null=True)
+    tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
 
     class Meta:
         model = VirtualMachine
         fields = [
             'id', 'name', 'status', 'cluster', 'role', 'tenant', 'platform', 'primary_ip', 'primary_ip4', 'primary_ip6',
-            'vcpus', 'memory', 'disk', 'comments', 'custom_fields', 'created', 'last_updated',
+            'vcpus', 'memory', 'disk', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
         ]
 
 

+ 5 - 2
netbox/virtualization/forms.py

@@ -4,6 +4,7 @@ from django import forms
 from django.core.exceptions import ValidationError
 from django.db.models import Count
 from mptt.forms import TreeNodeChoiceField
+from taggit.forms import TagField
 
 from dcim.constants import IFACE_FF_VIRTUAL, IFACE_MODE_ACCESS, IFACE_MODE_TAGGED_ALL
 from dcim.forms import INTERFACE_MODE_HELP_TEXT
@@ -78,10 +79,11 @@ class ClusterGroupCSVForm(forms.ModelForm):
 
 class ClusterForm(BootstrapMixin, CustomFieldForm):
     comments = CommentField(widget=SmallTextarea)
+    tags = TagField(required=False)
 
     class Meta:
         model = Cluster
-        fields = ['name', 'type', 'group', 'site', 'comments']
+        fields = ['name', 'type', 'group', 'site', 'comments', 'tags']
 
 
 class ClusterCSVForm(forms.ModelForm):
@@ -244,12 +246,13 @@ class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldForm):
             api_url='/api/virtualization/clusters/?group_id={{cluster_group}}'
         )
     )
+    tags = TagField(required=False)
 
     class Meta:
         model = VirtualMachine
         fields = [
             'name', 'status', 'cluster_group', 'cluster', 'role', 'tenant', 'platform', 'primary_ip4', 'primary_ip6',
-            'vcpus', 'memory', 'disk', 'comments',
+            'vcpus', 'memory', 'disk', 'comments', 'tags',
         ]
 
     def __init__(self, *args, **kwargs):

+ 5 - 0
netbox/virtualization/models.py

@@ -6,6 +6,7 @@ from django.core.exceptions import ValidationError
 from django.db import models
 from django.urls import reverse
 from django.utils.encoding import python_2_unicode_compatible
+from taggit.managers import TaggableManager
 
 from dcim.models import Device
 from extras.models import CustomFieldModel
@@ -124,6 +125,8 @@ class Cluster(CreatedUpdatedModel, CustomFieldModel):
         object_id_field='obj_id'
     )
 
+    tags = TaggableManager()
+
     csv_headers = ['name', 'type', 'group', 'site', 'comments']
 
     class Meta:
@@ -242,6 +245,8 @@ class VirtualMachine(CreatedUpdatedModel, CustomFieldModel):
         object_id_field='obj_id'
     )
 
+    tags = TaggableManager()
+
     csv_headers = [
         'name', 'status', 'role', 'cluster', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'comments',
     ]

+ 1 - 0
netbox/virtualization/views.py

@@ -126,6 +126,7 @@ class ClusterView(View):
 
 class ClusterCreateView(PermissionRequiredMixin, ObjectEditView):
     permission_required = 'virtualization.add_cluster'
+    template_name = 'virtualization/cluster_edit.html'
     model = Cluster
     model_form = forms.ClusterForm