Jelajahi Sumber

Implemented tags for all primary models

Jeremy Stretch 7 tahun lalu
induk
melakukan
9b3869790d
43 mengubah file dengan 262 tambahan dan 34 penghapusan
  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 __future__ import unicode_literals
 
 
 from rest_framework import serializers
 from rest_framework import serializers
+from taggit.models import Tag
 
 
 from circuits.constants import CIRCUIT_STATUS_CHOICES
 from circuits.constants import CIRCUIT_STATUS_CHOICES
 from circuits.models import Provider, Circuit, CircuitTermination, CircuitType
 from circuits.models import Provider, Circuit, CircuitTermination, CircuitType
 from dcim.api.serializers import NestedSiteSerializer, InterfaceSerializer
 from dcim.api.serializers import NestedSiteSerializer, InterfaceSerializer
 from extras.api.customfields import CustomFieldModelSerializer
 from extras.api.customfields import CustomFieldModelSerializer
 from tenancy.api.serializers import NestedTenantSerializer
 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):
 class ProviderSerializer(CustomFieldModelSerializer):
+    tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
 
 
     class Meta:
     class Meta:
         model = Provider
         model = Provider
         fields = [
         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',
             'custom_fields', 'created', 'last_updated',
         ]
         ]
 
 
@@ -60,12 +62,13 @@ class CircuitSerializer(CustomFieldModelSerializer):
     status = ChoiceFieldSerializer(choices=CIRCUIT_STATUS_CHOICES, required=False)
     status = ChoiceFieldSerializer(choices=CIRCUIT_STATUS_CHOICES, required=False)
     type = NestedCircuitTypeSerializer()
     type = NestedCircuitTypeSerializer()
     tenant = NestedTenantSerializer(required=False, allow_null=True)
     tenant = NestedTenantSerializer(required=False, allow_null=True)
+    tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
 
 
     class Meta:
     class Meta:
         model = Circuit
         model = Circuit
         fields = [
         fields = [
             'id', 'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description',
             '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 import forms
 from django.db.models import Count
 from django.db.models import Count
+from taggit.forms import TagField
 
 
 from dcim.models import Site, Device, Interface, Rack
 from dcim.models import Site, Device, Interface, Rack
 from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
 from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
@@ -22,10 +23,11 @@ from .models import Circuit, CircuitTermination, CircuitType, Provider
 class ProviderForm(BootstrapMixin, CustomFieldForm):
 class ProviderForm(BootstrapMixin, CustomFieldForm):
     slug = SlugField()
     slug = SlugField()
     comments = CommentField()
     comments = CommentField()
+    tags = TagField(required=False)
 
 
     class Meta:
     class Meta:
         model = Provider
         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 = {
         widgets = {
             'noc_contact': SmallTextarea(attrs={'rows': 5}),
             'noc_contact': SmallTextarea(attrs={'rows': 5}),
             'admin_contact': SmallTextarea(attrs={'rows': 5}),
             'admin_contact': SmallTextarea(attrs={'rows': 5}),
@@ -102,12 +104,13 @@ class CircuitTypeCSVForm(forms.ModelForm):
 
 
 class CircuitForm(BootstrapMixin, TenancyForm, CustomFieldForm):
 class CircuitForm(BootstrapMixin, TenancyForm, CustomFieldForm):
     comments = CommentField()
     comments = CommentField()
+    tags = TagField(required=False)
 
 
     class Meta:
     class Meta:
         model = Circuit
         model = Circuit
         fields = [
         fields = [
             'cid', 'type', 'provider', 'status', 'install_date', 'commit_rate', 'description', 'tenant_group', 'tenant',
             'cid', 'type', 'provider', 'status', 'install_date', 'commit_rate', 'description', 'tenant_group', 'tenant',
-            'comments',
+            'comments', 'tags',
         ]
         ]
         help_texts = {
         help_texts = {
             'cid': "Unique circuit ID",
             '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.db import models
 from django.urls import reverse
 from django.urls import reverse
 from django.utils.encoding import python_2_unicode_compatible
 from django.utils.encoding import python_2_unicode_compatible
+from taggit.managers import TaggableManager
 
 
 from dcim.constants import STATUS_CLASSES
 from dcim.constants import STATUS_CLASSES
 from dcim.fields import ASNField
 from dcim.fields import ASNField
@@ -56,6 +57,8 @@ class Provider(CreatedUpdatedModel, CustomFieldModel):
         object_id_field='obj_id'
         object_id_field='obj_id'
     )
     )
 
 
+    tags = TaggableManager()
+
     csv_headers = ['name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments']
     csv_headers = ['name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments']
 
 
     class Meta:
     class Meta:
@@ -166,6 +169,8 @@ class Circuit(CreatedUpdatedModel, CustomFieldModel):
         object_id_field='obj_id'
         object_id_field='obj_id'
     )
     )
 
 
+    tags = TaggableManager()
+
     csv_headers = [
     csv_headers = [
         'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description', 'comments',
         '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:
     class Meta:
         model = Device
         model = Device
         fields = [
         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 = {
         help_texts = {
             'device_role': "The function this device serves",
             '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 import serializers
 from rest_framework.reverse import reverse
 from rest_framework.reverse import reverse
 from rest_framework.validators import UniqueTogetherValidator
 from rest_framework.validators import UniqueTogetherValidator
+from taggit.models import Tag
 
 
 from dcim.api.serializers import NestedDeviceSerializer, InterfaceSerializer, NestedSiteSerializer
 from dcim.api.serializers import NestedDeviceSerializer, InterfaceSerializer, NestedSiteSerializer
 from dcim.models import Interface
 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 ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
 from tenancy.api.serializers import NestedTenantSerializer
 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
 from virtualization.api.serializers import NestedVirtualMachineSerializer
 
 
 
 
@@ -24,12 +27,13 @@ from virtualization.api.serializers import NestedVirtualMachineSerializer
 
 
 class VRFSerializer(CustomFieldModelSerializer):
 class VRFSerializer(CustomFieldModelSerializer):
     tenant = NestedTenantSerializer(required=False, allow_null=True)
     tenant = NestedTenantSerializer(required=False, allow_null=True)
+    tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
 
 
     class Meta:
     class Meta:
         model = VRF
         model = VRF
         fields = [
         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):
 class AggregateSerializer(CustomFieldModelSerializer):
     rir = NestedRIRSerializer()
     rir = NestedRIRSerializer()
+    tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
 
 
     class Meta:
     class Meta:
         model = Aggregate
         model = Aggregate
         fields = [
         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']
         read_only_fields = ['family']
 
 
@@ -147,11 +153,12 @@ class VLANSerializer(CustomFieldModelSerializer):
     tenant = NestedTenantSerializer(required=False, allow_null=True)
     tenant = NestedTenantSerializer(required=False, allow_null=True)
     status = ChoiceFieldSerializer(choices=VLAN_STATUS_CHOICES, required=False)
     status = ChoiceFieldSerializer(choices=VLAN_STATUS_CHOICES, required=False)
     role = NestedRoleSerializer(required=False, allow_null=True)
     role = NestedRoleSerializer(required=False, allow_null=True)
+    tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
 
 
     class Meta:
     class Meta:
         model = VLAN
         model = VLAN
         fields = [
         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',
             'custom_fields', 'created', 'last_updated',
         ]
         ]
         validators = []
         validators = []
@@ -190,12 +197,13 @@ class PrefixSerializer(CustomFieldModelSerializer):
     vlan = NestedVLANSerializer(required=False, allow_null=True)
     vlan = NestedVLANSerializer(required=False, allow_null=True)
     status = ChoiceFieldSerializer(choices=PREFIX_STATUS_CHOICES, required=False)
     status = ChoiceFieldSerializer(choices=PREFIX_STATUS_CHOICES, required=False)
     role = NestedRoleSerializer(required=False, allow_null=True)
     role = NestedRoleSerializer(required=False, allow_null=True)
+    tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
 
 
     class Meta:
     class Meta:
         model = Prefix
         model = Prefix
         fields = [
         fields = [
             'id', 'family', 'prefix', 'site', 'vrf', 'tenant', 'vlan', 'status', 'role', 'is_pool', 'description',
             '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']
         read_only_fields = ['family']
 
 
@@ -252,12 +260,13 @@ class IPAddressSerializer(CustomFieldModelSerializer):
     status = ChoiceFieldSerializer(choices=IPADDRESS_STATUS_CHOICES, required=False)
     status = ChoiceFieldSerializer(choices=IPADDRESS_STATUS_CHOICES, required=False)
     role = ChoiceFieldSerializer(choices=IPADDRESS_ROLE_CHOICES, required=False)
     role = ChoiceFieldSerializer(choices=IPADDRESS_ROLE_CHOICES, required=False)
     interface = IPAddressInterfaceSerializer(required=False, allow_null=True)
     interface = IPAddressInterfaceSerializer(required=False, allow_null=True)
+    tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
 
 
     class Meta:
     class Meta:
         model = IPAddress
         model = IPAddress
         fields = [
         fields = [
             'id', 'family', 'address', 'vrf', 'tenant', 'status', 'role', 'interface', 'description', 'nat_inside',
             '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']
         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 import forms
 from django.core.exceptions import MultipleObjectsReturned
 from django.core.exceptions import MultipleObjectsReturned
 from django.db.models import Count
 from django.db.models import Count
+from taggit.forms import TagField
 
 
 from dcim.models import Site, Rack, Device, Interface
 from dcim.models import Site, Rack, Device, Interface
 from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
 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):
 class VRFForm(BootstrapMixin, TenancyForm, CustomFieldForm):
+    tags = TagField(required=False)
 
 
     class Meta:
     class Meta:
         model = VRF
         model = VRF
-        fields = ['name', 'rd', 'enforce_unique', 'description', 'tenant_group', 'tenant']
+        fields = ['name', 'rd', 'enforce_unique', 'description', 'tenant_group', 'tenant', 'tags']
         labels = {
         labels = {
             'rd': "RD",
             'rd': "RD",
         }
         }
@@ -121,10 +123,11 @@ class RIRFilterForm(BootstrapMixin, forms.Form):
 #
 #
 
 
 class AggregateForm(BootstrapMixin, CustomFieldForm):
 class AggregateForm(BootstrapMixin, CustomFieldForm):
+    tags = TagField(required=False)
 
 
     class Meta:
     class Meta:
         model = Aggregate
         model = Aggregate
-        fields = ['prefix', 'rir', 'date_added', 'description']
+        fields = ['prefix', 'rir', 'date_added', 'description', 'tags']
         help_texts = {
         help_texts = {
             'prefix': "IPv4 or IPv6 network",
             'prefix': "IPv4 or IPv6 network",
             'rir': "Regional Internet Registry responsible for this prefix",
             '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'
             api_url='/api/ipam/vlans/?site_id={{site}}&group_id={{vlan_group}}', display_field='display_name'
         )
         )
     )
     )
+    tags = TagField(required=False)
 
 
     class Meta:
     class Meta:
         model = Prefix
         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):
     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')
     primary_for_parent = forms.BooleanField(required=False, label='Make this the primary IP for the device/VM')
+    tags = TagField(required=False)
 
 
     class Meta:
     class Meta:
         model = IPAddress
         model = IPAddress
         fields = [
         fields = [
             'address', 'vrf', 'status', 'role', 'description', 'interface', 'primary_for_parent', 'nat_site',
             '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):
     def __init__(self, *args, **kwargs):
@@ -780,10 +788,11 @@ class VLANForm(BootstrapMixin, TenancyForm, CustomFieldForm):
             api_url='/api/ipam/vlan-groups/?site_id={{site}}',
             api_url='/api/ipam/vlan-groups/?site_id={{site}}',
         )
         )
     )
     )
+    tags = TagField(required=False)
 
 
     class Meta:
     class Meta:
         model = VLAN
         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 = {
         help_texts = {
             'site': "Leave blank if this VLAN spans multiple sites",
             'site': "Leave blank if this VLAN spans multiple sites",
             'group': "VLAN group (optional)",
             '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.db.models.expressions import RawSQL
 from django.urls import reverse
 from django.urls import reverse
 from django.utils.encoding import python_2_unicode_compatible
 from django.utils.encoding import python_2_unicode_compatible
+from taggit.managers import TaggableManager
 
 
 from dcim.models import Interface
 from dcim.models import Interface
 from extras.models import CustomFieldModel
 from extras.models import CustomFieldModel
@@ -56,6 +57,8 @@ class VRF(CreatedUpdatedModel, CustomFieldModel):
         object_id_field='obj_id'
         object_id_field='obj_id'
     )
     )
 
 
+    tags = TaggableManager()
+
     csv_headers = ['name', 'rd', 'tenant', 'enforce_unique', 'description']
     csv_headers = ['name', 'rd', 'tenant', 'enforce_unique', 'description']
 
 
     class Meta:
     class Meta:
@@ -155,6 +158,8 @@ class Aggregate(CreatedUpdatedModel, CustomFieldModel):
         object_id_field='obj_id'
         object_id_field='obj_id'
     )
     )
 
 
+    tags = TaggableManager()
+
     csv_headers = ['prefix', 'rir', 'date_added', 'description']
     csv_headers = ['prefix', 'rir', 'date_added', 'description']
 
 
     class Meta:
     class Meta:
@@ -325,6 +330,7 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel):
     )
     )
 
 
     objects = PrefixQuerySet.as_manager()
     objects = PrefixQuerySet.as_manager()
+    tags = TaggableManager()
 
 
     csv_headers = [
     csv_headers = [
         'prefix', 'vrf', 'tenant', 'site', 'vlan_group', 'vlan_vid', 'status', 'role', 'is_pool', 'description',
         'prefix', 'vrf', 'tenant', 'site', 'vlan_group', 'vlan_vid', 'status', 'role', 'is_pool', 'description',
@@ -564,6 +570,7 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel):
     )
     )
 
 
     objects = IPAddressManager()
     objects = IPAddressManager()
+    tags = TaggableManager()
 
 
     csv_headers = [
     csv_headers = [
         'address', 'vrf', 'tenant', 'status', 'role', 'device', 'virtual_machine', 'interface_name', 'is_primary',
         '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'
         object_id_field='obj_id'
     )
     )
 
 
+    tags = TaggableManager()
+
     csv_headers = ['site', 'group_name', 'vid', 'name', 'tenant', 'status', 'role', 'description']
     csv_headers = ['site', 'group_name', 'vid', 'name', 'tenant', 'status', 'role', 'description']
 
 
     class Meta:
     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 import serializers
 from rest_framework.validators import UniqueTogetherValidator
 from rest_framework.validators import UniqueTogetherValidator
+from taggit.models import Tag
 
 
 from dcim.api.serializers import NestedDeviceSerializer
 from dcim.api.serializers import NestedDeviceSerializer
 from secrets.models import Secret, SecretRole
 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()
     device = NestedDeviceSerializer()
     role = NestedSecretRoleSerializer()
     role = NestedSecretRoleSerializer()
     plaintext = serializers.CharField()
     plaintext = serializers.CharField()
+    tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
 
 
     class Meta:
     class Meta:
         model = Secret
         model = Secret
-        fields = ['id', 'device', 'role', 'name', 'plaintext', 'hash', 'created', 'last_updated']
+        fields = ['id', 'device', 'role', 'name', 'plaintext', 'hash', 'tags', 'created', 'last_updated']
         validators = []
         validators = []
 
 
     def validate(self, data):
     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 Crypto.PublicKey import RSA
 from django import forms
 from django import forms
 from django.db.models import Count
 from django.db.models import Count
+from taggit.forms import TagField
 
 
 from dcim.models import Device
 from dcim.models import Device
 from utilities.forms import BootstrapMixin, BulkEditForm, FilterChoiceField, FlexibleModelChoiceField, SlugField
 from utilities.forms import BootstrapMixin, BulkEditForm, FilterChoiceField, FlexibleModelChoiceField, SlugField
@@ -70,10 +71,11 @@ class SecretForm(BootstrapMixin, forms.ModelForm):
         label='Plaintext (verify)',
         label='Plaintext (verify)',
         widget=forms.PasswordInput()
         widget=forms.PasswordInput()
     )
     )
+    tags = TagField(required=False)
 
 
     class Meta:
     class Meta:
         model = Secret
         model = Secret
-        fields = ['role', 'name', 'plaintext', 'plaintext2']
+        fields = ['role', 'name', 'plaintext', 'plaintext2', 'tags']
 
 
     def __init__(self, *args, **kwargs):
     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.db import models
 from django.urls import reverse
 from django.urls import reverse
 from django.utils.encoding import force_bytes, python_2_unicode_compatible
 from django.utils.encoding import force_bytes, python_2_unicode_compatible
+from taggit.managers import TaggableManager
 
 
 from utilities.models import CreatedUpdatedModel
 from utilities.models import CreatedUpdatedModel
 from .exceptions import InvalidKey
 from .exceptions import InvalidKey
@@ -336,6 +337,8 @@ class Secret(CreatedUpdatedModel):
         editable=False
         editable=False
     )
     )
 
 
+    tags = TaggableManager()
+
     plaintext = None
     plaintext = None
     csv_headers = ['device', 'role', 'name', 'plaintext']
     csv_headers = ['device', 'role', 'name', 'plaintext']
 
 

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

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

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

@@ -44,6 +44,12 @@
             {% render_field form.comments %}
             {% render_field form.comments %}
         </div>
         </div>
     </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 %}
 {% endblock %}
 
 
 {% block javascript %}
 {% block javascript %}

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

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

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

@@ -33,4 +33,10 @@
             {% render_field form.comments %}
             {% render_field form.comments %}
         </div>
         </div>
     </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 %}
 {% endblock %}

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

@@ -84,4 +84,10 @@
             {% render_field form.comments %}
             {% render_field form.comments %}
         </div>
         </div>
     </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 %}
 {% endblock %}

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

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

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

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

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

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

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

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

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

@@ -19,4 +19,10 @@
             </div>
             </div>
         </div>
         </div>
     {% endif %}
     {% 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 %}
 {% endblock %}

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

@@ -133,6 +133,10 @@
                         {% endif %}
                         {% endif %}
                     </td>
                     </td>
                 </tr>
                 </tr>
+                <tr>
+                    <td>Tags</td>
+                    <td>{{ ipaddress.tags.all|join:" " }}</td>
+                </tr>
             </table>
             </table>
         </div>
         </div>
         {% with ipaddress.get_custom_fields as custom_fields %}
         {% 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 %}
             {% render_field form.nat_inside %}
         </div>
         </div>
     </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 %}
     {% if form.custom_fields %}
         <div class="panel panel-default">
         <div class="panel panel-default">
             <div class="panel-heading"><strong>Custom Fields</strong></div>
             <div class="panel-heading"><strong>Custom Fields</strong></div>

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

@@ -121,6 +121,10 @@
                         {% endif %}
                         {% endif %}
                     </td>
                     </td>
                 </tr>
                 </tr>
+                <tr>
+                    <td>Tags</td>
+                    <td>{{ prefix.tags.all|join:" " }}</td>
+                </tr>
                 <tr>
                 <tr>
                     <td>Utilization</td>
                     <td>Utilization</td>
                     <td>{% utilization_graph prefix.get_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 %}
             {% render_field form.tenant %}
         </div>
         </div>
     </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 %}
     {% if form.custom_fields %}
         <div class="panel panel-default">
         <div class="panel panel-default">
             <div class="panel-heading"><strong>Custom Fields</strong></div>
             <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>
                             <span class="text-muted">N/A</span>
                         {% endif %}
                         {% endif %}
                     </td>
                     </td>
+                </tr>
+                <tr>
+                    <td>Tags</td>
+                    <td>{{ vlan.tags.all|join:" " }}</td>
                 </tr>
                 </tr>
 		    </table>
 		    </table>
         </div>
         </div>

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

@@ -21,6 +21,12 @@
             {% render_field form.tenant %}
             {% render_field form.tenant %}
         </div>
         </div>
     </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 %}
     {% if form.custom_fields %}
         <div class="panel panel-default">
         <div class="panel panel-default">
             <div class="panel-heading"><strong>Custom Fields</strong></div>
             <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>
                             <span class="text-muted">N/A</span>
                         {% endif %}
                         {% endif %}
                     </td>
                     </td>
+                </tr>
+                <tr>
+                    <td>Tags</td>
+                    <td>{{ vrf.tags.all|join:" " }}</td>
                 </tr>
                 </tr>
 		    </table>
 		    </table>
         </div>
         </div>

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

@@ -18,6 +18,12 @@
             {% render_field form.tenant %}
             {% render_field form.tenant %}
         </div>
         </div>
     </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 %}
     {% if form.custom_fields %}
         <div class="panel panel-default">
         <div class="panel panel-default">
             <div class="panel-heading"><strong>Custom Fields</strong></div>
             <div class="panel-heading"><strong>Custom Fields</strong></div>

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

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

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

@@ -54,6 +54,12 @@
                     {% render_field form.plaintext2 %}
                     {% render_field form.plaintext2 %}
                 </div>
                 </div>
             </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>
     </div>
     <div class="row">
     <div class="row">

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

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

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

@@ -26,4 +26,10 @@
             {% render_field form.comments %}
             {% render_field form.comments %}
         </div>
         </div>
     </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 %}
 {% endblock %}

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

@@ -76,6 +76,10 @@
                         {% endif %}
                         {% endif %}
                     </td>
                     </td>
                 </tr>
                 </tr>
+                <tr>
+                    <td>Tags</td>
+                    <td>{{ cluster.tags.all|join:" " }}</td>
+                </tr>
                 <tr>
                 <tr>
                     <td>Virtual Machines</td>
                     <td>Virtual Machines</td>
                     <td><a href="{% url 'virtualization:virtualmachine_list' %}?cluster_id={{ cluster.pk }}">{{ cluster.virtual_machines.count }}</a></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 %}
                         {% endif %}
                     </td>
                     </td>
                 </tr>
                 </tr>
+                <tr>
+                    <td>Tags</td>
+                    <td>{{ vm.tags.all|join:" " }}</td>
+                </tr>
             </table>
             </table>
         </div>
         </div>
         {% include 'inc/custom_fields_panel.html' with custom_fields=vm.get_custom_fields %}
         {% 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 %}
             {% render_field form.comments %}
         </div>
         </div>
     </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 %}
 {% endblock %}

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

@@ -1,10 +1,11 @@
 from __future__ import unicode_literals
 from __future__ import unicode_literals
 
 
 from rest_framework import serializers
 from rest_framework import serializers
+from taggit.models import Tag
 
 
 from extras.api.customfields import CustomFieldModelSerializer
 from extras.api.customfields import CustomFieldModelSerializer
 from tenancy.models import Tenant, TenantGroup
 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):
 class TenantSerializer(CustomFieldModelSerializer):
     group = NestedTenantGroupSerializer(required=False)
     group = NestedTenantGroupSerializer(required=False)
+    tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
 
 
     class Meta:
     class Meta:
         model = Tenant
         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):
 class NestedTenantSerializer(WritableNestedSerializer):

+ 3 - 1
netbox/tenancy/forms.py

@@ -2,6 +2,7 @@ from __future__ import unicode_literals
 
 
 from django import forms
 from django import forms
 from django.db.models import Count
 from django.db.models import Count
+from taggit.forms import TagField
 
 
 from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
 from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
 from utilities.forms import (
 from utilities.forms import (
@@ -40,10 +41,11 @@ class TenantGroupCSVForm(forms.ModelForm):
 class TenantForm(BootstrapMixin, CustomFieldForm):
 class TenantForm(BootstrapMixin, CustomFieldForm):
     slug = SlugField()
     slug = SlugField()
     comments = CommentField()
     comments = CommentField()
+    tags = TagField(required=False)
 
 
     class Meta:
     class Meta:
         model = Tenant
         model = Tenant
-        fields = ['name', 'slug', 'group', 'description', 'comments']
+        fields = ['name', 'slug', 'group', 'description', 'comments', 'tags']
 
 
 
 
 class TenantCSVForm(forms.ModelForm):
 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.db import models
 from django.urls import reverse
 from django.urls import reverse
 from django.utils.encoding import python_2_unicode_compatible
 from django.utils.encoding import python_2_unicode_compatible
+from taggit.managers import TaggableManager
 
 
 from extras.models import CustomFieldModel
 from extras.models import CustomFieldModel
 from utilities.models import CreatedUpdatedModel
 from utilities.models import CreatedUpdatedModel
@@ -74,6 +75,8 @@ class Tenant(CreatedUpdatedModel, CustomFieldModel):
         object_id_field='obj_id'
         object_id_field='obj_id'
     )
     )
 
 
+    tags = TaggableManager()
+
     csv_headers = ['name', 'slug', 'group', 'description', 'comments']
     csv_headers = ['name', 'slug', 'group', 'description', 'comments']
 
 
     class Meta:
     class Meta:

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

@@ -1,14 +1,15 @@
 from __future__ import unicode_literals
 from __future__ import unicode_literals
 
 
 from rest_framework import serializers
 from rest_framework import serializers
+from taggit.models import Tag
 
 
 from dcim.api.serializers import NestedDeviceRoleSerializer, NestedPlatformSerializer, NestedSiteSerializer
 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 dcim.models import Interface
 from extras.api.customfields import CustomFieldModelSerializer
 from extras.api.customfields import CustomFieldModelSerializer
 from ipam.models import IPAddress, VLAN
 from ipam.models import IPAddress, VLAN
 from tenancy.api.serializers import NestedTenantSerializer
 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.constants import VM_STATUS_CHOICES
 from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine
 from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine
 
 
@@ -59,10 +60,13 @@ class ClusterSerializer(CustomFieldModelSerializer):
     type = NestedClusterTypeSerializer()
     type = NestedClusterTypeSerializer()
     group = NestedClusterGroupSerializer(required=False, allow_null=True)
     group = NestedClusterGroupSerializer(required=False, allow_null=True)
     site = NestedSiteSerializer(required=False, allow_null=True)
     site = NestedSiteSerializer(required=False, allow_null=True)
+    tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
 
 
     class Meta:
     class Meta:
         model = Cluster
         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):
 class NestedClusterSerializer(WritableNestedSerializer):
@@ -95,12 +99,13 @@ class VirtualMachineSerializer(CustomFieldModelSerializer):
     primary_ip = VirtualMachineIPAddressSerializer(read_only=True)
     primary_ip = VirtualMachineIPAddressSerializer(read_only=True)
     primary_ip4 = VirtualMachineIPAddressSerializer(required=False, allow_null=True)
     primary_ip4 = VirtualMachineIPAddressSerializer(required=False, allow_null=True)
     primary_ip6 = 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:
     class Meta:
         model = VirtualMachine
         model = VirtualMachine
         fields = [
         fields = [
             'id', 'name', 'status', 'cluster', 'role', 'tenant', 'platform', 'primary_ip', 'primary_ip4', 'primary_ip6',
             '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.core.exceptions import ValidationError
 from django.db.models import Count
 from django.db.models import Count
 from mptt.forms import TreeNodeChoiceField
 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.constants import IFACE_FF_VIRTUAL, IFACE_MODE_ACCESS, IFACE_MODE_TAGGED_ALL
 from dcim.forms import INTERFACE_MODE_HELP_TEXT
 from dcim.forms import INTERFACE_MODE_HELP_TEXT
@@ -78,10 +79,11 @@ class ClusterGroupCSVForm(forms.ModelForm):
 
 
 class ClusterForm(BootstrapMixin, CustomFieldForm):
 class ClusterForm(BootstrapMixin, CustomFieldForm):
     comments = CommentField(widget=SmallTextarea)
     comments = CommentField(widget=SmallTextarea)
+    tags = TagField(required=False)
 
 
     class Meta:
     class Meta:
         model = Cluster
         model = Cluster
-        fields = ['name', 'type', 'group', 'site', 'comments']
+        fields = ['name', 'type', 'group', 'site', 'comments', 'tags']
 
 
 
 
 class ClusterCSVForm(forms.ModelForm):
 class ClusterCSVForm(forms.ModelForm):
@@ -244,12 +246,13 @@ class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldForm):
             api_url='/api/virtualization/clusters/?group_id={{cluster_group}}'
             api_url='/api/virtualization/clusters/?group_id={{cluster_group}}'
         )
         )
     )
     )
+    tags = TagField(required=False)
 
 
     class Meta:
     class Meta:
         model = VirtualMachine
         model = VirtualMachine
         fields = [
         fields = [
             'name', 'status', 'cluster_group', 'cluster', 'role', 'tenant', 'platform', 'primary_ip4', 'primary_ip6',
             '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):
     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.db import models
 from django.urls import reverse
 from django.urls import reverse
 from django.utils.encoding import python_2_unicode_compatible
 from django.utils.encoding import python_2_unicode_compatible
+from taggit.managers import TaggableManager
 
 
 from dcim.models import Device
 from dcim.models import Device
 from extras.models import CustomFieldModel
 from extras.models import CustomFieldModel
@@ -124,6 +125,8 @@ class Cluster(CreatedUpdatedModel, CustomFieldModel):
         object_id_field='obj_id'
         object_id_field='obj_id'
     )
     )
 
 
+    tags = TaggableManager()
+
     csv_headers = ['name', 'type', 'group', 'site', 'comments']
     csv_headers = ['name', 'type', 'group', 'site', 'comments']
 
 
     class Meta:
     class Meta:
@@ -242,6 +245,8 @@ class VirtualMachine(CreatedUpdatedModel, CustomFieldModel):
         object_id_field='obj_id'
         object_id_field='obj_id'
     )
     )
 
 
+    tags = TaggableManager()
+
     csv_headers = [
     csv_headers = [
         'name', 'status', 'role', 'cluster', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'comments',
         '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):
 class ClusterCreateView(PermissionRequiredMixin, ObjectEditView):
     permission_required = 'virtualization.add_cluster'
     permission_required = 'virtualization.add_cluster'
+    template_name = 'virtualization/cluster_edit.html'
     model = Cluster
     model = Cluster
     model_form = forms.ClusterForm
     model_form = forms.ClusterForm