Explorar o código

Merge pull request #6470 from netbox-community/5121-filter-tags-content-type

Closes #5121: Add object type filters for Tags
Jeremy Stretch %!s(int64=4) %!d(string=hai) anos
pai
achega
f3dfa81811

+ 3 - 3
netbox/circuits/models.py

@@ -20,7 +20,7 @@ __all__ = (
 )
 )
 
 
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
+@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class Provider(PrimaryModel):
 class Provider(PrimaryModel):
     """
     """
     Each Circuit belongs to a Provider. This is usually a telecommunications company or similar organization. This model
     Each Circuit belongs to a Provider. This is usually a telecommunications company or similar organization. This model
@@ -96,7 +96,7 @@ class Provider(PrimaryModel):
 # Provider networks
 # Provider networks
 #
 #
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
+@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class ProviderNetwork(PrimaryModel):
 class ProviderNetwork(PrimaryModel):
     """
     """
     This represents a provider network which exists outside of NetBox, the details of which are unknown or
     This represents a provider network which exists outside of NetBox, the details of which are unknown or
@@ -189,7 +189,7 @@ class CircuitType(OrganizationalModel):
         )
         )
 
 
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
+@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class Circuit(PrimaryModel):
 class Circuit(PrimaryModel):
     """
     """
     A communications circuit connects two points. Each Circuit belongs to a Provider; Providers may have multiple
     A communications circuit connects two points. Each Circuit belongs to a Provider; Providers may have multiple

+ 1 - 1
netbox/dcim/models/cables.py

@@ -30,7 +30,7 @@ __all__ = (
 # Cables
 # Cables
 #
 #
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
+@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class Cable(PrimaryModel):
 class Cable(PrimaryModel):
     """
     """
     A physical connection between two endpoints.
     A physical connection between two endpoints.

+ 9 - 9
netbox/dcim/models/device_components.py

@@ -211,7 +211,7 @@ class PathEndpoint(models.Model):
 # Console ports
 # Console ports
 #
 #
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
+@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class ConsolePort(ComponentModel, CableTermination, PathEndpoint):
 class ConsolePort(ComponentModel, CableTermination, PathEndpoint):
     """
     """
     A physical console port within a Device. ConsolePorts connect to ConsoleServerPorts.
     A physical console port within a Device. ConsolePorts connect to ConsoleServerPorts.
@@ -254,7 +254,7 @@ class ConsolePort(ComponentModel, CableTermination, PathEndpoint):
 # Console server ports
 # Console server ports
 #
 #
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
+@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class ConsoleServerPort(ComponentModel, CableTermination, PathEndpoint):
 class ConsoleServerPort(ComponentModel, CableTermination, PathEndpoint):
     """
     """
     A physical port within a Device (typically a designated console server) which provides access to ConsolePorts.
     A physical port within a Device (typically a designated console server) which provides access to ConsolePorts.
@@ -297,7 +297,7 @@ class ConsoleServerPort(ComponentModel, CableTermination, PathEndpoint):
 # Power ports
 # Power ports
 #
 #
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
+@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class PowerPort(ComponentModel, CableTermination, PathEndpoint):
 class PowerPort(ComponentModel, CableTermination, PathEndpoint):
     """
     """
     A physical power supply (intake) port within a Device. PowerPorts connect to PowerOutlets.
     A physical power supply (intake) port within a Device. PowerPorts connect to PowerOutlets.
@@ -408,7 +408,7 @@ class PowerPort(ComponentModel, CableTermination, PathEndpoint):
 # Power outlets
 # Power outlets
 #
 #
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
+@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class PowerOutlet(ComponentModel, CableTermination, PathEndpoint):
 class PowerOutlet(ComponentModel, CableTermination, PathEndpoint):
     """
     """
     A physical power outlet (output) within a Device which provides power to a PowerPort.
     A physical power outlet (output) within a Device which provides power to a PowerPort.
@@ -512,7 +512,7 @@ class BaseInterface(models.Model):
         return self.ip_addresses.count()
         return self.ip_addresses.count()
 
 
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
+@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class Interface(ComponentModel, BaseInterface, CableTermination, PathEndpoint):
 class Interface(ComponentModel, BaseInterface, CableTermination, PathEndpoint):
     """
     """
     A network interface within a Device. A physical Interface can connect to exactly one other Interface.
     A network interface within a Device. A physical Interface can connect to exactly one other Interface.
@@ -683,7 +683,7 @@ class Interface(ComponentModel, BaseInterface, CableTermination, PathEndpoint):
 # Pass-through ports
 # Pass-through ports
 #
 #
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
+@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class FrontPort(ComponentModel, CableTermination):
 class FrontPort(ComponentModel, CableTermination):
     """
     """
     A pass-through port on the front of a Device.
     A pass-through port on the front of a Device.
@@ -748,7 +748,7 @@ class FrontPort(ComponentModel, CableTermination):
             })
             })
 
 
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
+@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class RearPort(ComponentModel, CableTermination):
 class RearPort(ComponentModel, CableTermination):
     """
     """
     A pass-through port on the rear of a Device.
     A pass-through port on the rear of a Device.
@@ -801,7 +801,7 @@ class RearPort(ComponentModel, CableTermination):
 # Device bays
 # Device bays
 #
 #
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
+@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class DeviceBay(ComponentModel):
 class DeviceBay(ComponentModel):
     """
     """
     An empty space within a Device which can house a child device
     An empty space within a Device which can house a child device
@@ -860,7 +860,7 @@ class DeviceBay(ComponentModel):
 # Inventory items
 # Inventory items
 #
 #
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
+@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class InventoryItem(MPTTModel, ComponentModel):
 class InventoryItem(MPTTModel, ComponentModel):
     """
     """
     An InventoryItem represents a serialized piece of hardware within a Device, such as a line card or power supply.
     An InventoryItem represents a serialized piece of hardware within a Device, such as a line card or power supply.

+ 3 - 3
netbox/dcim/models/devices.py

@@ -75,7 +75,7 @@ class Manufacturer(OrganizationalModel):
         )
         )
 
 
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
+@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class DeviceType(PrimaryModel):
 class DeviceType(PrimaryModel):
     """
     """
     A DeviceType represents a particular make (Manufacturer) and model of device. It specifies rack height and depth, as
     A DeviceType represents a particular make (Manufacturer) and model of device. It specifies rack height and depth, as
@@ -468,7 +468,7 @@ class Platform(OrganizationalModel):
         )
         )
 
 
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
+@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class Device(PrimaryModel, ConfigContextModel):
 class Device(PrimaryModel, ConfigContextModel):
     """
     """
     A Device represents a piece of physical hardware mounted within a Rack. Each Device is assigned a DeviceType,
     A Device represents a piece of physical hardware mounted within a Rack. Each Device is assigned a DeviceType,
@@ -922,7 +922,7 @@ class Device(PrimaryModel, ConfigContextModel):
 # Virtual chassis
 # Virtual chassis
 #
 #
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
+@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class VirtualChassis(PrimaryModel):
 class VirtualChassis(PrimaryModel):
     """
     """
     A collection of Devices which operate with a shared control plane (e.g. a switch stack).
     A collection of Devices which operate with a shared control plane (e.g. a switch stack).

+ 2 - 2
netbox/dcim/models/power.py

@@ -21,7 +21,7 @@ __all__ = (
 # Power
 # Power
 #
 #
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
+@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class PowerPanel(PrimaryModel):
 class PowerPanel(PrimaryModel):
     """
     """
     A distribution point for electrical power; e.g. a data center RPP.
     A distribution point for electrical power; e.g. a data center RPP.
@@ -71,7 +71,7 @@ class PowerPanel(PrimaryModel):
             )
             )
 
 
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
+@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class PowerFeed(PrimaryModel, PathEndpoint, CableTermination):
 class PowerFeed(PrimaryModel, PathEndpoint, CableTermination):
     """
     """
     An electrical circuit delivered from a PowerPanel.
     An electrical circuit delivered from a PowerPanel.

+ 2 - 2
netbox/dcim/models/racks.py

@@ -78,7 +78,7 @@ class RackRole(OrganizationalModel):
         )
         )
 
 
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
+@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class Rack(PrimaryModel):
 class Rack(PrimaryModel):
     """
     """
     Devices are housed within Racks. Each rack has a defined height measured in rack units, and a front and rear face.
     Devices are housed within Racks. Each rack has a defined height measured in rack units, and a front and rear face.
@@ -467,7 +467,7 @@ class Rack(PrimaryModel):
         return int(allocated_draw_total / available_power_total * 100)
         return int(allocated_draw_total / available_power_total * 100)
 
 
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
+@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class RackReservation(PrimaryModel):
 class RackReservation(PrimaryModel):
     """
     """
     One or more reserved units within a Rack.
     One or more reserved units within a Rack.

+ 1 - 1
netbox/dcim/models/sites.py

@@ -130,7 +130,7 @@ class SiteGroup(NestedGroupModel):
 # Sites
 # Sites
 #
 #
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
+@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class Site(PrimaryModel):
 class Site(PrimaryModel):
     """
     """
     A Site represents a geographic location within a network; typically a building or campus. The optional facility
     A Site represents a geographic location within a network; typically a building or campus. The optional facility

+ 1 - 0
netbox/extras/constants.py

@@ -7,5 +7,6 @@ EXTRAS_FEATURES = [
     'custom_links',
     'custom_links',
     'export_templates',
     'export_templates',
     'job_results',
     'job_results',
+    'tags',
     'webhooks'
     'webhooks'
 ]
 ]

+ 33 - 1
netbox/extras/filtersets.py

@@ -6,7 +6,7 @@ from django.db.models import Q
 from dcim.models import DeviceRole, DeviceType, Platform, Region, Site, SiteGroup
 from dcim.models import DeviceRole, DeviceType, Platform, Region, Site, SiteGroup
 from netbox.filtersets import BaseFilterSet, ChangeLoggedModelFilterSet
 from netbox.filtersets import BaseFilterSet, ChangeLoggedModelFilterSet
 from tenancy.models import Tenant, TenantGroup
 from tenancy.models import Tenant, TenantGroup
-from utilities.filters import ContentTypeFilter
+from utilities.filters import ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter
 from virtualization.models import Cluster, ClusterGroup
 from virtualization.models import Cluster, ClusterGroup
 from .choices import *
 from .choices import *
 from .models import *
 from .models import *
@@ -114,6 +114,12 @@ class TagFilterSet(ChangeLoggedModelFilterSet):
         method='search',
         method='search',
         label='Search',
         label='Search',
     )
     )
+    content_type = MultiValueCharFilter(
+        method='_content_type'
+    )
+    content_type_id = MultiValueNumberFilter(
+        method='_content_type_id'
+    )
 
 
     class Meta:
     class Meta:
         model = Tag
         model = Tag
@@ -127,6 +133,32 @@ class TagFilterSet(ChangeLoggedModelFilterSet):
             Q(slug__icontains=value)
             Q(slug__icontains=value)
         )
         )
 
 
+    def _content_type(self, queryset, name, values):
+        ct_filter = Q()
+
+        # Compile list of app_label & model pairings
+        for value in values:
+            try:
+                app_label, model = value.lower().split('.')
+                ct_filter |= Q(
+                    app_label=app_label,
+                    model=model
+                )
+            except ValueError:
+                pass
+
+        # Get ContentType instances
+        content_types = ContentType.objects.filter(ct_filter)
+
+        return queryset.filter(extras_taggeditem_items__content_type__in=content_types).distinct()
+
+    def _content_type_id(self, queryset, name, values):
+
+        # Get ContentType instances
+        content_types = ContentType.objects.filter(pk__in=values)
+
+        return queryset.filter(extras_taggeditem_items__content_type__in=content_types).distinct()
+
 
 
 class ConfigContextFilterSet(ChangeLoggedModelFilterSet):
 class ConfigContextFilterSet(ChangeLoggedModelFilterSet):
     q = django_filters.CharFilter(
     q = django_filters.CharFilter(

+ 8 - 2
netbox/extras/forms.py

@@ -8,12 +8,13 @@ from dcim.models import DeviceRole, DeviceType, Platform, Region, Site, SiteGrou
 from tenancy.models import Tenant, TenantGroup
 from tenancy.models import Tenant, TenantGroup
 from utilities.forms import (
 from utilities.forms import (
     add_blank_choice, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ColorSelect,
     add_blank_choice, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ColorSelect,
-    CommentField, CSVModelForm, DateTimePicker, DynamicModelMultipleChoiceField, JSONField, SlugField, StaticSelect2,
-    BOOLEAN_WITH_BLANK_CHOICES,
+    CommentField, ContentTypeMultipleChoiceField, CSVModelForm, DateTimePicker, DynamicModelMultipleChoiceField,
+    JSONField, SlugField, StaticSelect2, BOOLEAN_WITH_BLANK_CHOICES,
 )
 )
 from virtualization.models import Cluster, ClusterGroup
 from virtualization.models import Cluster, ClusterGroup
 from .choices import *
 from .choices import *
 from .models import ConfigContext, CustomField, ImageAttachment, JournalEntry, ObjectChange, Tag
 from .models import ConfigContext, CustomField, ImageAttachment, JournalEntry, ObjectChange, Tag
+from .utils import FeatureQuery
 
 
 
 
 #
 #
@@ -180,6 +181,11 @@ class TagFilterForm(BootstrapMixin, forms.Form):
         required=False,
         required=False,
         label=_('Search')
         label=_('Search')
     )
     )
+    content_type_id = ContentTypeMultipleChoiceField(
+        queryset=ContentType.objects.filter(FeatureQuery('tags').get_query()),
+        required=False,
+        label=_('Tagged object type')
+    )
 
 
 
 
 class TagBulkEditForm(BootstrapMixin, BulkEditForm):
 class TagBulkEditForm(BootstrapMixin, BulkEditForm):

+ 16 - 0
netbox/extras/tests/test_filtersets.py

@@ -5,6 +5,7 @@ from django.contrib.auth.models import User
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes.models import ContentType
 from django.test import TestCase
 from django.test import TestCase
 
 
+from circuits.models import Provider
 from dcim.models import DeviceRole, DeviceType, Manufacturer, Platform, Rack, Region, Site, SiteGroup
 from dcim.models import DeviceRole, DeviceType, Manufacturer, Platform, Rack, Region, Site, SiteGroup
 from extras.choices import JournalEntryKindChoices, ObjectChangeActionChoices
 from extras.choices import JournalEntryKindChoices, ObjectChangeActionChoices
 from extras.filtersets import *
 from extras.filtersets import *
@@ -537,6 +538,13 @@ class TagTestCase(TestCase, ChangeLoggedFilterSetTests):
         )
         )
         Tag.objects.bulk_create(tags)
         Tag.objects.bulk_create(tags)
 
 
+        # Apply some tags so we can filter by content type
+        site = Site.objects.create(name='Site 1', slug='site-1')
+        provider = Provider.objects.create(name='Provider 1', slug='provider-1')
+
+        site.tags.set(tags[0])
+        provider.tags.set(tags[1])
+
     def test_name(self):
     def test_name(self):
         params = {'name': ['Tag 1', 'Tag 2']}
         params = {'name': ['Tag 1', 'Tag 2']}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -549,6 +557,14 @@ class TagTestCase(TestCase, ChangeLoggedFilterSetTests):
         params = {'color': ['ff0000', '00ff00']}
         params = {'color': ['ff0000', '00ff00']}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
 
 
+    def test_content_type(self):
+        params = {'content_type': ['dcim.site', 'circuits.provider']}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+        site_ct = ContentType.objects.get_for_model(Site).pk
+        provider_ct = ContentType.objects.get_for_model(Provider).pk
+        params = {'content_type_id': [site_ct, provider_ct]}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
 
 
 class ObjectChangeTestCase(TestCase, BaseFilterSetTests):
 class ObjectChangeTestCase(TestCase, BaseFilterSetTests):
     queryset = ObjectChange.objects.all()
     queryset = ObjectChange.objects.all()

+ 3 - 3
netbox/ipam/models/ip.py

@@ -77,7 +77,7 @@ class RIR(OrganizationalModel):
         )
         )
 
 
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
+@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class Aggregate(PrimaryModel):
 class Aggregate(PrimaryModel):
     """
     """
     An aggregate exists at the root level of the IP address space hierarchy in NetBox. Aggregates are used to organize
     An aggregate exists at the root level of the IP address space hierarchy in NetBox. Aggregates are used to organize
@@ -228,7 +228,7 @@ class Role(OrganizationalModel):
         )
         )
 
 
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
+@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class Prefix(PrimaryModel):
 class Prefix(PrimaryModel):
     """
     """
     A Prefix represents an IPv4 or IPv6 network, including mask length. Prefixes can optionally be assigned to Sites and
     A Prefix represents an IPv4 or IPv6 network, including mask length. Prefixes can optionally be assigned to Sites and
@@ -477,7 +477,7 @@ class Prefix(PrimaryModel):
             return int(float(child_count) / prefix_size * 100)
             return int(float(child_count) / prefix_size * 100)
 
 
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
+@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class IPAddress(PrimaryModel):
 class IPAddress(PrimaryModel):
     """
     """
     An IPAddress represents an individual IPv4 or IPv6 address and its mask. The mask length should match what is
     An IPAddress represents an individual IPv4 or IPv6 address and its mask. The mask length should match what is

+ 1 - 1
netbox/ipam/models/services.py

@@ -17,7 +17,7 @@ __all__ = (
 )
 )
 
 
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
+@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class Service(PrimaryModel):
 class Service(PrimaryModel):
     """
     """
     A Service represents a layer-four service (e.g. HTTP or SSH) running on a Device or VirtualMachine. A Service may
     A Service represents a layer-four service (e.g. HTTP or SSH) running on a Device or VirtualMachine. A Service may

+ 1 - 1
netbox/ipam/models/vlans.py

@@ -100,7 +100,7 @@ class VLANGroup(OrganizationalModel):
         return None
         return None
 
 
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
+@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class VLAN(PrimaryModel):
 class VLAN(PrimaryModel):
     """
     """
     A VLAN is a distinct layer two forwarding domain identified by a 12-bit integer (1-4094). Each VLAN must be assigned
     A VLAN is a distinct layer two forwarding domain identified by a 12-bit integer (1-4094). Each VLAN must be assigned

+ 2 - 2
netbox/ipam/models/vrfs.py

@@ -13,7 +13,7 @@ __all__ = (
 )
 )
 
 
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
+@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class VRF(PrimaryModel):
 class VRF(PrimaryModel):
     """
     """
     A virtual routing and forwarding (VRF) table represents a discrete layer three forwarding domain (e.g. a routing
     A virtual routing and forwarding (VRF) table represents a discrete layer three forwarding domain (e.g. a routing
@@ -92,7 +92,7 @@ class VRF(PrimaryModel):
         return self.name
         return self.name
 
 
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
+@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class RouteTarget(PrimaryModel):
 class RouteTarget(PrimaryModel):
     """
     """
     A BGP extended community used to control the redistribution of routes among VRFs, as defined in RFC 4364.
     A BGP extended community used to control the redistribution of routes among VRFs, as defined in RFC 4364.

+ 1 - 1
netbox/secrets/models.py

@@ -273,7 +273,7 @@ class SecretRole(OrganizationalModel):
         )
         )
 
 
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
+@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class Secret(PrimaryModel):
 class Secret(PrimaryModel):
     """
     """
     A Secret stores an AES256-encrypted copy of sensitive data, such as passwords or secret keys. An irreversible
     A Secret stores an AES256-encrypted copy of sensitive data, such as passwords or secret keys. An irreversible

+ 1 - 1
netbox/tenancy/models.py

@@ -57,7 +57,7 @@ class TenantGroup(NestedGroupModel):
         )
         )
 
 
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
+@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class Tenant(PrimaryModel):
 class Tenant(PrimaryModel):
     """
     """
     A Tenant represents an organization served by the NetBox owner. This is typically a customer or an internal
     A Tenant represents an organization served by the NetBox owner. This is typically a customer or an internal

+ 3 - 3
netbox/virtualization/models.py

@@ -116,7 +116,7 @@ class ClusterGroup(OrganizationalModel):
 # Clusters
 # Clusters
 #
 #
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
+@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class Cluster(PrimaryModel):
 class Cluster(PrimaryModel):
     """
     """
     A cluster of VirtualMachines. Each Cluster may optionally be associated with one or more Devices.
     A cluster of VirtualMachines. Each Cluster may optionally be associated with one or more Devices.
@@ -199,7 +199,7 @@ class Cluster(PrimaryModel):
 # Virtual machines
 # Virtual machines
 #
 #
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
+@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class VirtualMachine(PrimaryModel, ConfigContextModel):
 class VirtualMachine(PrimaryModel, ConfigContextModel):
     """
     """
     A virtual machine which runs inside a Cluster.
     A virtual machine which runs inside a Cluster.
@@ -380,7 +380,7 @@ class VirtualMachine(PrimaryModel, ConfigContextModel):
 # Interfaces
 # Interfaces
 #
 #
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
+@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class VMInterface(PrimaryModel, BaseInterface):
 class VMInterface(PrimaryModel, BaseInterface):
     virtual_machine = models.ForeignKey(
     virtual_machine = models.ForeignKey(
         to='virtualization.VirtualMachine',
         to='virtualization.VirtualMachine',