\d+)/delete/$', views.ImageAttachmentDeleteView.as_view(), name='imageattachment_delete'),
diff --git a/netbox/extras/views.py b/netbox/extras/views.py
index 3f7c0435b..130437356 100644
--- a/netbox/extras/views.py
+++ b/netbox/extras/views.py
@@ -2,16 +2,52 @@ from __future__ import unicode_literals
from django.contrib import messages
from django.contrib.auth.mixins import PermissionRequiredMixin
+from django.db.models import Count
from django.http import Http404
-from django.shortcuts import get_object_or_404, redirect, render
+from django.shortcuts import get_object_or_404, redirect, render, reverse
from django.utils.safestring import mark_safe
from django.views.generic import View
+from taggit.models import Tag
from utilities.forms import ConfirmationForm
-from utilities.views import ObjectDeleteView, ObjectEditView
-from .forms import ImageAttachmentForm
+from utilities.views import BulkDeleteView, ObjectDeleteView, ObjectEditView, ObjectListView
+from .forms import ImageAttachmentForm, TagForm
from .models import ImageAttachment, ReportResult, UserAction
from .reports import get_report, get_reports
+from .tables import TagTable
+
+
+#
+# Tags
+#
+
+class TagListView(ObjectListView):
+ queryset = Tag.objects.annotate(items=Count('taggit_taggeditem_items')).order_by('name')
+ table = TagTable
+ template_name = 'extras/tag_list.html'
+
+
+class TagEditView(PermissionRequiredMixin, ObjectEditView):
+ permission_required = 'taggit.change_tag'
+ model = Tag
+ model_form = TagForm
+
+ def get_return_url(self, request, obj):
+ return reverse('extras:tag', kwargs={'slug': obj.slug})
+
+
+class TagDeleteView(PermissionRequiredMixin, ObjectDeleteView):
+ permission_required = 'taggit.delete_tag'
+ model = Tag
+ default_return_url = 'extras:tag_list'
+
+
+class TagBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
+ permission_required = 'circuits.delete_circuittype'
+ cls = Tag
+ queryset = Tag.objects.annotate(items=Count('taggit_taggeditem_items')).order_by('name')
+ table = TagTable
+ default_return_url = 'extras:tag_list'
#
diff --git a/netbox/ipam/api/serializers.py b/netbox/ipam/api/serializers.py
index 6fb9d3ba4..f7969fbc3 100644
--- a/netbox/ipam/api/serializers.py
+++ b/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']
diff --git a/netbox/ipam/filters.py b/netbox/ipam/filters.py
index 005d44a84..db2806b77 100644
--- a/netbox/ipam/filters.py
+++ b/netbox/ipam/filters.py
@@ -30,6 +30,9 @@ class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet):
to_field_name='slug',
label='Tenant (slug)',
)
+ tag = django_filters.CharFilter(
+ name='tags__slug',
+ )
def search(self, queryset, name, value):
if not value.strip():
@@ -69,6 +72,9 @@ class AggregateFilter(CustomFieldFilterSet, django_filters.FilterSet):
to_field_name='slug',
label='RIR (slug)',
)
+ tag = django_filters.CharFilter(
+ name='tags__slug',
+ )
class Meta:
model = Aggregate
@@ -167,6 +173,9 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
choices=PREFIX_STATUS_CHOICES,
null_value=None
)
+ tag = django_filters.CharFilter(
+ name='tags__slug',
+ )
class Meta:
model = Prefix
@@ -289,6 +298,9 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
role = django_filters.MultipleChoiceFilter(
choices=IPADDRESS_ROLE_CHOICES
)
+ tag = django_filters.CharFilter(
+ name='tags__slug',
+ )
class Meta:
model = IPAddress
@@ -394,6 +406,9 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
choices=VLAN_STATUS_CHOICES,
null_value=None
)
+ tag = django_filters.CharFilter(
+ name='tags__slug',
+ )
class Meta:
model = VLAN
diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py
index 3353d981f..82ebfe724 100644
--- a/netbox/ipam/forms.py
+++ b/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)",
diff --git a/netbox/ipam/models.py b/netbox/ipam/models.py
index 2f83bb0f2..65a9cce55 100644
--- a/netbox/ipam/models.py
+++ b/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:
diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py
index 7a90506b4..ad313a593 100644
--- a/netbox/netbox/settings.py
+++ b/netbox/netbox/settings.py
@@ -133,6 +133,7 @@ INSTALLED_APPS = (
'django_tables2',
'mptt',
'rest_framework',
+ 'taggit',
'timezone_field',
'circuits',
'dcim',
diff --git a/netbox/secrets/api/serializers.py b/netbox/secrets/api/serializers.py
index aca91920a..0e24281bb 100644
--- a/netbox/secrets/api/serializers.py
+++ b/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):
diff --git a/netbox/secrets/filters.py b/netbox/secrets/filters.py
index 6578eb4b8..2499fa2bb 100644
--- a/netbox/secrets/filters.py
+++ b/netbox/secrets/filters.py
@@ -41,6 +41,9 @@ class SecretFilter(django_filters.FilterSet):
to_field_name='name',
label='Device (name)',
)
+ tag = django_filters.CharFilter(
+ name='tags__slug',
+ )
class Meta:
model = Secret
diff --git a/netbox/secrets/forms.py b/netbox/secrets/forms.py
index 8f8107805..863d1dfde 100644
--- a/netbox/secrets/forms.py
+++ b/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):
diff --git a/netbox/secrets/models.py b/netbox/secrets/models.py
index e39d46eef..dcb38db70 100644
--- a/netbox/secrets/models.py
+++ b/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']
diff --git a/netbox/templates/circuits/circuit.html b/netbox/templates/circuits/circuit.html
index 1133f41f3..509c6da89 100644
--- a/netbox/templates/circuits/circuit.html
+++ b/netbox/templates/circuits/circuit.html
@@ -110,6 +110,16 @@
{% endif %}
+
+ Tags |
+
+ {% for tag in circuit.tags.all %}
+ {% tag 'circuits:circuit_list' tag %}
+ {% empty %}
+ N/A
+ {% endfor %}
+ |
+
{% with circuit.get_custom_fields as custom_fields %}
diff --git a/netbox/templates/circuits/circuit_edit.html b/netbox/templates/circuits/circuit_edit.html
index 8503e68f6..06ad65241 100644
--- a/netbox/templates/circuits/circuit_edit.html
+++ b/netbox/templates/circuits/circuit_edit.html
@@ -44,6 +44,12 @@
{% render_field form.comments %}
+
+
Tags
+
+ {% render_field form.tags %}
+
+
{% endblock %}
{% block javascript %}
diff --git a/netbox/templates/circuits/circuit_list.html b/netbox/templates/circuits/circuit_list.html
index f05552f7d..81e09c32b 100644
--- a/netbox/templates/circuits/circuit_list.html
+++ b/netbox/templates/circuits/circuit_list.html
@@ -16,6 +16,7 @@
{% include 'inc/search_panel.html' %}
+ {% include 'inc/tags_panel.html' %}
{% endblock %}
diff --git a/netbox/templates/circuits/provider.html b/netbox/templates/circuits/provider.html
index 6dcccfd8d..e19175c7f 100644
--- a/netbox/templates/circuits/provider.html
+++ b/netbox/templates/circuits/provider.html
@@ -102,6 +102,16 @@
{% endif %}
+
+ Tags |
+
+ {% for tag in provider.tags.all %}
+ {% tag 'circuits:provider_list' tag %}
+ {% empty %}
+ N/A
+ {% endfor %}
+ |
+
Circuits |
diff --git a/netbox/templates/circuits/provider_edit.html b/netbox/templates/circuits/provider_edit.html
index 4fb3889b1..dfa239e40 100644
--- a/netbox/templates/circuits/provider_edit.html
+++ b/netbox/templates/circuits/provider_edit.html
@@ -33,4 +33,10 @@
{% render_field form.comments %}
+
+ Tags
+
+ {% render_field form.tags %}
+
+
{% endblock %}
diff --git a/netbox/templates/circuits/provider_list.html b/netbox/templates/circuits/provider_list.html
index cb7aab406..a0036f46c 100644
--- a/netbox/templates/circuits/provider_list.html
+++ b/netbox/templates/circuits/provider_list.html
@@ -16,6 +16,7 @@
{% include 'inc/search_panel.html' %}
+ {% include 'inc/tags_panel.html' %}
{% endblock %}
diff --git a/netbox/templates/dcim/device.html b/netbox/templates/dcim/device.html
index e2253d4f4..1b1d3d23a 100644
--- a/netbox/templates/dcim/device.html
+++ b/netbox/templates/dcim/device.html
@@ -96,6 +96,16 @@
{% endif %}
|
+
+ Tags |
+
+ {% for tag in device.tags.all %}
+ {% tag 'dcim:device_list' tag %}
+ {% empty %}
+ N/A
+ {% endfor %}
+ |
+
{% if vc_members %}
diff --git a/netbox/templates/dcim/device_edit.html b/netbox/templates/dcim/device_edit.html
index 07206ca27..d39c01482 100644
--- a/netbox/templates/dcim/device_edit.html
+++ b/netbox/templates/dcim/device_edit.html
@@ -83,4 +83,10 @@
{% render_field form.comments %}
+
+
Tags
+
+ {% render_field form.tags %}
+
+
{% endblock %}
diff --git a/netbox/templates/dcim/device_list.html b/netbox/templates/dcim/device_list.html
index f96b27309..4bae11781 100644
--- a/netbox/templates/dcim/device_list.html
+++ b/netbox/templates/dcim/device_list.html
@@ -16,6 +16,7 @@
{% include 'inc/search_panel.html' %}
+ {% include 'inc/tags_panel.html' %}
{% endblock %}
diff --git a/netbox/templates/dcim/devicetype.html b/netbox/templates/dcim/devicetype.html
index 12281734b..27d2e3694 100644
--- a/netbox/templates/dcim/devicetype.html
+++ b/netbox/templates/dcim/devicetype.html
@@ -73,6 +73,16 @@
Interface Ordering |
{{ devicetype.get_interface_ordering_display }} |
+
+ Tags |
+
+ {% for tag in devicetype.tags.all %}
+ {% tag 'dcim:devicetype_list' tag %}
+ {% empty %}
+ N/A
+ {% endfor %}
+ |
+
Instances |
{{ devicetype.instances.count }} |
diff --git a/netbox/templates/dcim/devicetype_edit.html b/netbox/templates/dcim/devicetype_edit.html
index d2a107607..e69077ad9 100644
--- a/netbox/templates/dcim/devicetype_edit.html
+++ b/netbox/templates/dcim/devicetype_edit.html
@@ -37,4 +37,10 @@
{% render_field form.comments %}
+
+
Tags
+
+ {% render_field form.tags %}
+
+
{% endblock %}
diff --git a/netbox/templates/dcim/devicetype_list.html b/netbox/templates/dcim/devicetype_list.html
index 91745082a..eb901f5a0 100644
--- a/netbox/templates/dcim/devicetype_list.html
+++ b/netbox/templates/dcim/devicetype_list.html
@@ -16,6 +16,7 @@
{% include 'inc/search_panel.html' %}
+ {% include 'inc/tags_panel.html' %}
{% endblock %}
diff --git a/netbox/templates/dcim/rack.html b/netbox/templates/dcim/rack.html
index 28a9dfb6f..82348e6fe 100644
--- a/netbox/templates/dcim/rack.html
+++ b/netbox/templates/dcim/rack.html
@@ -114,6 +114,16 @@
{% endif %}
+
+ Tags |
+
+ {% for tag in rack.tags.all %}
+ {% tag 'dcim:rack_list' tag %}
+ {% empty %}
+ N/A
+ {% endfor %}
+ |
+
Devices |
diff --git a/netbox/templates/dcim/rack_edit.html b/netbox/templates/dcim/rack_edit.html
index 4ab129a1d..b9526a3ac 100644
--- a/netbox/templates/dcim/rack_edit.html
+++ b/netbox/templates/dcim/rack_edit.html
@@ -43,4 +43,10 @@
{% render_field form.comments %}
+
+ Tags
+
+ {% render_field form.tags %}
+
+
{% endblock %}
diff --git a/netbox/templates/dcim/rack_list.html b/netbox/templates/dcim/rack_list.html
index d5734ee2b..e61f4eadf 100644
--- a/netbox/templates/dcim/rack_list.html
+++ b/netbox/templates/dcim/rack_list.html
@@ -16,6 +16,7 @@
{% include 'inc/search_panel.html' %}
+ {% include 'inc/tags_panel.html' %}
{% endblock %}
diff --git a/netbox/templates/dcim/site.html b/netbox/templates/dcim/site.html
index b14c2019d..a882d77c8 100644
--- a/netbox/templates/dcim/site.html
+++ b/netbox/templates/dcim/site.html
@@ -133,6 +133,16 @@
{% endif %}
|
+
+ Tags |
+
+ {% for tag in site.tags.all %}
+ {% tag 'dcim:site_list' tag %}
+ {% empty %}
+ N/A
+ {% endfor %}
+ |
+
diff --git a/netbox/templates/dcim/site_edit.html b/netbox/templates/dcim/site_edit.html
index 399551434..ad7932642 100644
--- a/netbox/templates/dcim/site_edit.html
+++ b/netbox/templates/dcim/site_edit.html
@@ -46,4 +46,10 @@
{% render_field form.comments %}
+
+
Tags
+
+ {% render_field form.tags %}
+
+
{% endblock %}
diff --git a/netbox/templates/dcim/site_list.html b/netbox/templates/dcim/site_list.html
index 7baa76dad..50066186d 100644
--- a/netbox/templates/dcim/site_list.html
+++ b/netbox/templates/dcim/site_list.html
@@ -16,6 +16,7 @@
{% include 'inc/search_panel.html' %}
+ {% include 'inc/tags_panel.html' %}
{% endblock %}
diff --git a/netbox/templates/extras/tag_list.html b/netbox/templates/extras/tag_list.html
new file mode 100644
index 000000000..3136991a0
--- /dev/null
+++ b/netbox/templates/extras/tag_list.html
@@ -0,0 +1,11 @@
+{% extends '_base.html' %}
+{% load buttons %}
+
+{% block content %}
+{% block title %}Tags{% endblock %}
+
+
+ {% include 'utilities/obj_table.html' with bulk_delete_url='extras:tag_bulk_delete' %}
+
+
+{% endblock %}
diff --git a/netbox/templates/inc/nav_menu.html b/netbox/templates/inc/nav_menu.html
index a85647993..2c47ad85b 100644
--- a/netbox/templates/inc/nav_menu.html
+++ b/netbox/templates/inc/nav_menu.html
@@ -16,7 +16,7 @@
{% if request.user.is_authenticated or not settings.LOGIN_REQUIRED %}
-
+ -
+ Tags
+
-
Reports
diff --git a/netbox/templates/inc/tags_panel.html b/netbox/templates/inc/tags_panel.html
new file mode 100644
index 000000000..a7923fbed
--- /dev/null
+++ b/netbox/templates/inc/tags_panel.html
@@ -0,0 +1,13 @@
+{% load helpers %}
+
+
diff --git a/netbox/templates/ipam/aggregate.html b/netbox/templates/ipam/aggregate.html
index 63731755c..a7711354d 100644
--- a/netbox/templates/ipam/aggregate.html
+++ b/netbox/templates/ipam/aggregate.html
@@ -1,4 +1,5 @@
{% extends '_base.html' %}
+{% load helpers %}
{% block content %}
@@ -81,6 +82,16 @@
{% endif %}
+
+ Tags |
+
+ {% for tag in aggregate.tags.all %}
+ {% tag 'ipam:aggregate_list' tag %}
+ {% empty %}
+ N/A
+ {% endfor %}
+ |
+
diff --git a/netbox/templates/ipam/aggregate_edit.html b/netbox/templates/ipam/aggregate_edit.html
index be499a509..3cb83ab54 100644
--- a/netbox/templates/ipam/aggregate_edit.html
+++ b/netbox/templates/ipam/aggregate_edit.html
@@ -19,4 +19,10 @@
{% endif %}
+
+
Tags
+
+ {% render_field form.tags %}
+
+
{% endblock %}
diff --git a/netbox/templates/ipam/aggregate_list.html b/netbox/templates/ipam/aggregate_list.html
index 73da9695d..33db74e5c 100644
--- a/netbox/templates/ipam/aggregate_list.html
+++ b/netbox/templates/ipam/aggregate_list.html
@@ -17,6 +17,7 @@
{% include 'inc/search_panel.html' %}
+ {% include 'inc/tags_panel.html' %}
Statistics
diff --git a/netbox/templates/ipam/ipaddress.html b/netbox/templates/ipam/ipaddress.html
index 1509f35cb..da0fc6923 100644
--- a/netbox/templates/ipam/ipaddress.html
+++ b/netbox/templates/ipam/ipaddress.html
@@ -1,4 +1,5 @@
{% extends '_base.html' %}
+{% load helpers %}
{% block content %}
@@ -133,6 +134,16 @@
{% endif %}
+
+ Tags |
+
+ {% for tag in ipaddress.tags.all %}
+ {% tag 'ipam:ipaddress_list' tag %}
+ {% empty %}
+ N/A
+ {% endfor %}
+ |
+
{% with ipaddress.get_custom_fields as custom_fields %}
diff --git a/netbox/templates/ipam/ipaddress_edit.html b/netbox/templates/ipam/ipaddress_edit.html
index d0dad69ee..72fc02a1e 100644
--- a/netbox/templates/ipam/ipaddress_edit.html
+++ b/netbox/templates/ipam/ipaddress_edit.html
@@ -66,6 +66,12 @@
{% render_field form.nat_inside %}
+
+
Tags
+
+ {% render_field form.tags %}
+
+
{% if form.custom_fields %}
Custom Fields
diff --git a/netbox/templates/ipam/ipaddress_list.html b/netbox/templates/ipam/ipaddress_list.html
index 5f8fdeb88..418b807bd 100644
--- a/netbox/templates/ipam/ipaddress_list.html
+++ b/netbox/templates/ipam/ipaddress_list.html
@@ -16,6 +16,7 @@
{% include 'inc/search_panel.html' %}
+ {% include 'inc/tags_panel.html' %}
{% endblock %}
diff --git a/netbox/templates/ipam/prefix.html b/netbox/templates/ipam/prefix.html
index 11c5fc405..29e9c07a0 100644
--- a/netbox/templates/ipam/prefix.html
+++ b/netbox/templates/ipam/prefix.html
@@ -121,6 +121,16 @@
{% endif %}
+
+ Tags |
+
+ {% for tag in prefix.tags.all %}
+ {% tag 'ipam:prefix_list' tag %}
+ {% empty %}
+ N/A
+ {% endfor %}
+ |
+
Utilization |
{% utilization_graph prefix.get_utilization %} |
diff --git a/netbox/templates/ipam/prefix_edit.html b/netbox/templates/ipam/prefix_edit.html
index 938a75da3..333cf1229 100644
--- a/netbox/templates/ipam/prefix_edit.html
+++ b/netbox/templates/ipam/prefix_edit.html
@@ -28,6 +28,12 @@
{% render_field form.tenant %}
+
+
Tags
+
+ {% render_field form.tags %}
+
+
{% if form.custom_fields %}
Custom Fields
diff --git a/netbox/templates/ipam/prefix_list.html b/netbox/templates/ipam/prefix_list.html
index d65904595..3ce9d4a9c 100644
--- a/netbox/templates/ipam/prefix_list.html
+++ b/netbox/templates/ipam/prefix_list.html
@@ -21,6 +21,7 @@
{% include 'inc/search_panel.html' %}
+ {% include 'inc/tags_panel.html' %}
{% endblock %}
diff --git a/netbox/templates/ipam/vlan.html b/netbox/templates/ipam/vlan.html
index 971c3359f..ac874282f 100644
--- a/netbox/templates/ipam/vlan.html
+++ b/netbox/templates/ipam/vlan.html
@@ -1,4 +1,5 @@
{% extends '_base.html' %}
+{% load helpers %}
{% block content %}
{% include 'ipam/inc/vlan_header.html' with active_tab='vlan' %}
@@ -80,6 +81,16 @@
N/A
{% endif %}
+
+
+ Tags |
+
+ {% for tag in vlan.tags.all %}
+ {% tag 'ipam:vlan_list' tag %}
+ {% empty %}
+ N/A
+ {% endfor %}
+ |
diff --git a/netbox/templates/ipam/vlan_edit.html b/netbox/templates/ipam/vlan_edit.html
index 3bfb7783e..7862d4de9 100644
--- a/netbox/templates/ipam/vlan_edit.html
+++ b/netbox/templates/ipam/vlan_edit.html
@@ -21,6 +21,12 @@
{% render_field form.tenant %}
+
+
Tags
+
+ {% render_field form.tags %}
+
+
{% if form.custom_fields %}
Custom Fields
diff --git a/netbox/templates/ipam/vlan_list.html b/netbox/templates/ipam/vlan_list.html
index 24e12595b..d734db8d2 100644
--- a/netbox/templates/ipam/vlan_list.html
+++ b/netbox/templates/ipam/vlan_list.html
@@ -16,6 +16,7 @@
{% include 'inc/search_panel.html' %}
+ {% include 'inc/tags_panel.html' %}
{% endblock %}
diff --git a/netbox/templates/ipam/vrf.html b/netbox/templates/ipam/vrf.html
index e041ce73a..fa51a18f8 100644
--- a/netbox/templates/ipam/vrf.html
+++ b/netbox/templates/ipam/vrf.html
@@ -1,4 +1,5 @@
{% extends '_base.html' %}
+{% load helpers %}
{% block content %}
@@ -77,6 +78,16 @@
N/A
{% endif %}
+
+
+ Tags |
+
+ {% for tag in vrf.tags.all %}
+ {% tag 'ipam:vrf_list' tag %}
+ {% empty %}
+ N/A
+ {% endfor %}
+ |
diff --git a/netbox/templates/ipam/vrf_edit.html b/netbox/templates/ipam/vrf_edit.html
index 63052129c..95a89a6ca 100644
--- a/netbox/templates/ipam/vrf_edit.html
+++ b/netbox/templates/ipam/vrf_edit.html
@@ -18,6 +18,12 @@
{% render_field form.tenant %}
+
+
Tags
+
+ {% render_field form.tags %}
+
+
{% if form.custom_fields %}
Custom Fields
diff --git a/netbox/templates/ipam/vrf_list.html b/netbox/templates/ipam/vrf_list.html
index 23bd16495..670f0ee5d 100644
--- a/netbox/templates/ipam/vrf_list.html
+++ b/netbox/templates/ipam/vrf_list.html
@@ -16,6 +16,7 @@
{% include 'inc/search_panel.html' %}
+ {% include 'inc/tags_panel.html' %}
{% endblock %}
diff --git a/netbox/templates/secrets/secret.html b/netbox/templates/secrets/secret.html
index 66c844ebf..4863fdeb1 100644
--- a/netbox/templates/secrets/secret.html
+++ b/netbox/templates/secrets/secret.html
@@ -1,5 +1,6 @@
{% extends '_base.html' %}
{% load static from staticfiles %}
+{% load helpers %}
{% load secret_helpers %}
{% block content %}
@@ -55,6 +56,16 @@
{% endif %}
+
+ Tags |
+
+ {% for tag in secret.tags.all %}
+ {% tag 'secrets:secret_list' tag %}
+ {% empty %}
+ N/A
+ {% endfor %}
+ |
+
diff --git a/netbox/templates/secrets/secret_edit.html b/netbox/templates/secrets/secret_edit.html
index 920409177..87ee3b426 100644
--- a/netbox/templates/secrets/secret_edit.html
+++ b/netbox/templates/secrets/secret_edit.html
@@ -54,6 +54,12 @@
{% render_field form.plaintext2 %}
+
+
Tags
+
+ {% render_field form.tags %}
+
+
diff --git a/netbox/templates/secrets/secret_list.html b/netbox/templates/secrets/secret_list.html
index 6dd92cd89..0a70e1087 100644
--- a/netbox/templates/secrets/secret_list.html
+++ b/netbox/templates/secrets/secret_list.html
@@ -14,6 +14,7 @@
{% include 'inc/search_panel.html' %}
+ {% include 'inc/tags_panel.html' %}
{% endblock %}
diff --git a/netbox/templates/tenancy/tenant.html b/netbox/templates/tenancy/tenant.html
index d5eb7df98..fbbac175a 100644
--- a/netbox/templates/tenancy/tenant.html
+++ b/netbox/templates/tenancy/tenant.html
@@ -68,6 +68,16 @@
{% endif %}
+
+ Tags |
+
+ {% for tag in tenant.tags.all %}
+ {% tag 'tenancy:tenant_list' tag %}
+ {% empty %}
+ N/A
+ {% endfor %}
+ |
+
{% with tenant.get_custom_fields as custom_fields %}
diff --git a/netbox/templates/tenancy/tenant_edit.html b/netbox/templates/tenancy/tenant_edit.html
index b2c472a1c..9cc0aa53b 100644
--- a/netbox/templates/tenancy/tenant_edit.html
+++ b/netbox/templates/tenancy/tenant_edit.html
@@ -26,4 +26,10 @@
{% render_field form.comments %}
+
+
Tags
+
+ {% render_field form.tags %}
+
+
{% endblock %}
diff --git a/netbox/templates/tenancy/tenant_list.html b/netbox/templates/tenancy/tenant_list.html
index e6fd61c37..176231507 100644
--- a/netbox/templates/tenancy/tenant_list.html
+++ b/netbox/templates/tenancy/tenant_list.html
@@ -16,6 +16,7 @@
{% include 'inc/search_panel.html' %}
+ {% include 'inc/tags_panel.html' %}
{% endblock %}
diff --git a/netbox/templates/utilities/templatetags/tag.html b/netbox/templates/utilities/templatetags/tag.html
new file mode 100644
index 000000000..79e1627db
--- /dev/null
+++ b/netbox/templates/utilities/templatetags/tag.html
@@ -0,0 +1 @@
+{{ tag }}
diff --git a/netbox/templates/virtualization/cluster.html b/netbox/templates/virtualization/cluster.html
index 08251e2fa..9b1621530 100644
--- a/netbox/templates/virtualization/cluster.html
+++ b/netbox/templates/virtualization/cluster.html
@@ -76,6 +76,16 @@
{% endif %}
+
+ Tags |
+
+ {% for tag in cluster.tags.all %}
+ {% tag 'virtualization:cluster_list' tag %}
+ {% empty %}
+ N/A
+ {% endfor %}
+ |
+
Virtual Machines |
{{ cluster.virtual_machines.count }} |
diff --git a/netbox/templates/virtualization/cluster_edit.html b/netbox/templates/virtualization/cluster_edit.html
new file mode 100644
index 000000000..93fe197ec
--- /dev/null
+++ b/netbox/templates/virtualization/cluster_edit.html
@@ -0,0 +1,34 @@
+{% extends 'utilities/obj_edit.html' %}
+{% load form_helpers %}
+
+{% block form %}
+
+
Cluster
+
+ {% render_field form.name %}
+ {% render_field form.type %}
+ {% render_field form.group %}
+ {% render_field form.site %}
+
+
+ {% if form.custom_fields %}
+
+
Custom Fields
+
+ {% render_custom_fields form %}
+
+
+ {% endif %}
+
+
Comments
+
+ {% render_field form.comments %}
+
+
+
+
Tags
+
+ {% render_field form.tags %}
+
+
+{% endblock %}
diff --git a/netbox/templates/virtualization/cluster_list.html b/netbox/templates/virtualization/cluster_list.html
index 08f62e6ba..84513dbb1 100644
--- a/netbox/templates/virtualization/cluster_list.html
+++ b/netbox/templates/virtualization/cluster_list.html
@@ -16,6 +16,7 @@
{% include 'inc/search_panel.html' %}
+ {% include 'inc/tags_panel.html' %}
{% endblock %}
diff --git a/netbox/templates/virtualization/virtualmachine.html b/netbox/templates/virtualization/virtualmachine.html
index 944792705..3d8d0d05a 100644
--- a/netbox/templates/virtualization/virtualmachine.html
+++ b/netbox/templates/virtualization/virtualmachine.html
@@ -121,6 +121,16 @@
{% endif %}
+
+ Tags |
+
+ {% for tag in vm.tags.all %}
+ {% tag 'virtualization:virtualmachine_list' tag %}
+ {% empty %}
+ N/A
+ {% endfor %}
+ |
+
{% include 'inc/custom_fields_panel.html' with custom_fields=vm.get_custom_fields %}
diff --git a/netbox/templates/virtualization/virtualmachine_edit.html b/netbox/templates/virtualization/virtualmachine_edit.html
index 706591ab4..0fa7e07fb 100644
--- a/netbox/templates/virtualization/virtualmachine_edit.html
+++ b/netbox/templates/virtualization/virtualmachine_edit.html
@@ -54,4 +54,10 @@
{% render_field form.comments %}
+
+
Tags
+
+ {% render_field form.tags %}
+
+
{% endblock %}
diff --git a/netbox/templates/virtualization/virtualmachine_list.html b/netbox/templates/virtualization/virtualmachine_list.html
index 30ed76dae..bf2961fd8 100644
--- a/netbox/templates/virtualization/virtualmachine_list.html
+++ b/netbox/templates/virtualization/virtualmachine_list.html
@@ -16,6 +16,7 @@
{% include 'inc/search_panel.html' %}
+ {% include 'inc/tags_panel.html' %}
{% endblock %}
diff --git a/netbox/tenancy/api/serializers.py b/netbox/tenancy/api/serializers.py
index 3a6e1fb4b..c7b94e7e9 100644
--- a/netbox/tenancy/api/serializers.py
+++ b/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):
diff --git a/netbox/tenancy/filters.py b/netbox/tenancy/filters.py
index 330ab7f56..7eccff5d3 100644
--- a/netbox/tenancy/filters.py
+++ b/netbox/tenancy/filters.py
@@ -31,6 +31,9 @@ class TenantFilter(CustomFieldFilterSet, django_filters.FilterSet):
to_field_name='slug',
label='Group (slug)',
)
+ tag = django_filters.CharFilter(
+ name='tags__slug',
+ )
class Meta:
model = Tenant
diff --git a/netbox/tenancy/forms.py b/netbox/tenancy/forms.py
index 4ea6c57ba..123b2bc24 100644
--- a/netbox/tenancy/forms.py
+++ b/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):
diff --git a/netbox/tenancy/models.py b/netbox/tenancy/models.py
index 9df714680..f006e512d 100644
--- a/netbox/tenancy/models.py
+++ b/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:
diff --git a/netbox/utilities/api.py b/netbox/utilities/api.py
index 40d111269..61be3bc63 100644
--- a/netbox/utilities/api.py
+++ b/netbox/utilities/api.py
@@ -13,7 +13,7 @@ from rest_framework.exceptions import APIException
from rest_framework.permissions import BasePermission
from rest_framework.relations import PrimaryKeyRelatedField
from rest_framework.response import Response
-from rest_framework.serializers import Field, ModelSerializer, ValidationError
+from rest_framework.serializers import Field, ModelSerializer, RelatedField, ValidationError
from rest_framework.viewsets import GenericViewSet, ViewSet
WRITE_OPERATIONS = ['create', 'update', 'partial_update', 'delete']
@@ -42,6 +42,21 @@ class IsAuthenticatedOrLoginNotRequired(BasePermission):
# Fields
#
+class TagField(RelatedField):
+ """
+ Represent a writable list of Tags associated with an object (use with many=True).
+ """
+
+ def to_internal_value(self, data):
+ obj = self.parent.parent.instance
+ content_type = ContentType.objects.get_for_model(obj)
+ tag, _ = Tag.objects.get_or_create(content_type=content_type, object_id=obj.pk, name=data)
+ return tag
+
+ def to_representation(self, value):
+ return value.name
+
+
class ChoiceFieldSerializer(Field):
"""
Represent a ChoiceField as {'value': , 'label': }.
diff --git a/netbox/utilities/templatetags/helpers.py b/netbox/utilities/templatetags/helpers.py
index 7d79a5f2a..1380941b3 100644
--- a/netbox/utilities/templatetags/helpers.py
+++ b/netbox/utilities/templatetags/helpers.py
@@ -1,7 +1,6 @@
from __future__ import unicode_literals
import datetime
-import pytz
from django import template
from django.utils.safestring import mark_safe
@@ -160,3 +159,14 @@ def utilization_graph(utilization, warning_threshold=75, danger_threshold=90):
'warning_threshold': warning_threshold,
'danger_threshold': danger_threshold,
}
+
+
+@register.inclusion_tag('utilities/templatetags/tag.html')
+def tag(url_name, tag):
+ """
+ Display a link to the given object list filtered by a specific Tag slug.
+ """
+ return {
+ 'url_name': url_name,
+ 'tag': tag,
+ }
diff --git a/netbox/utilities/views.py b/netbox/utilities/views.py
index d060e53d7..fd085a1e7 100644
--- a/netbox/utilities/views.py
+++ b/netbox/utilities/views.py
@@ -8,7 +8,7 @@ from django.contrib import messages
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.db import transaction, IntegrityError
-from django.db.models import ProtectedError
+from django.db.models import Count, ProtectedError
from django.forms import CharField, Form, ModelMultipleChoiceField, MultipleHiddenInput, Textarea
from django.shortcuts import get_object_or_404, redirect, render
from django.template.exceptions import TemplateSyntaxError
@@ -119,6 +119,12 @@ class ObjectListView(View):
if 'pk' in table.base_columns and (permissions['change'] or permissions['delete']):
table.columns.show('pk')
+ # Construct queryset for tags list
+ if hasattr(model, 'tags'):
+ tags = model.tags.annotate(count=Count('taggit_taggeditem_items')).order_by('-count', 'name')
+ else:
+ tags = None
+
# Apply the request context
paginate = {
'klass': EnhancedPaginator,
@@ -131,6 +137,7 @@ class ObjectListView(View):
'table': table,
'permissions': permissions,
'filter_form': self.filter_form(request.GET, label_suffix='') if self.filter_form else None,
+ 'tags': tags,
}
context.update(self.extra_context())
@@ -195,13 +202,16 @@ class ObjectEditView(GetReturnURLMixin, View):
obj_created = not form.instance.pk
obj = form.save()
- msg = 'Created ' if obj_created else 'Modified '
- msg += self.model._meta.verbose_name
+ msg = '{} {}'.format(
+ 'Created' if obj_created else 'Modified',
+ self.model._meta.verbose_name
+ )
if hasattr(obj, 'get_absolute_url'):
msg = '{} {}'.format(msg, obj.get_absolute_url(), escape(obj))
else:
msg = '{} {}'.format(msg, escape(obj))
messages.success(request, mark_safe(msg))
+
if obj_created:
UserAction.objects.log_create(request.user, obj, msg)
else:
diff --git a/netbox/virtualization/api/serializers.py b/netbox/virtualization/api/serializers.py
index 8cee708ba..15ed39abf 100644
--- a/netbox/virtualization/api/serializers.py
+++ b/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',
]
diff --git a/netbox/virtualization/filters.py b/netbox/virtualization/filters.py
index 53c3f18d9..6af4e4a22 100644
--- a/netbox/virtualization/filters.py
+++ b/netbox/virtualization/filters.py
@@ -63,6 +63,9 @@ class ClusterFilter(CustomFieldFilterSet):
to_field_name='slug',
label='Site (slug)',
)
+ tag = django_filters.CharFilter(
+ name='tags__slug',
+ )
class Meta:
model = Cluster
@@ -154,6 +157,9 @@ class VirtualMachineFilter(CustomFieldFilterSet):
to_field_name='slug',
label='Platform (slug)',
)
+ tag = django_filters.CharFilter(
+ name='tags__slug',
+ )
class Meta:
model = VirtualMachine
diff --git a/netbox/virtualization/forms.py b/netbox/virtualization/forms.py
index 4dfea1b42..b973ed5cb 100644
--- a/netbox/virtualization/forms.py
+++ b/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):
diff --git a/netbox/virtualization/models.py b/netbox/virtualization/models.py
index b58cf93e8..e34512410 100644
--- a/netbox/virtualization/models.py
+++ b/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',
]
diff --git a/netbox/virtualization/views.py b/netbox/virtualization/views.py
index 6de6b86c7..96c57c29b 100644
--- a/netbox/virtualization/views.py
+++ b/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
diff --git a/requirements.txt b/requirements.txt
index 1f8aca440..147b42bc8 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -4,6 +4,7 @@ django-debug-toolbar>=1.9.0
django-filter>=1.1.0
django-mptt>=0.9.0
django-tables2>=1.19.0
+django-taggit>=0.22.2
django-timezone-field>=2.0
djangorestframework>=3.7.7,<3.8.2
drf-yasg[validation]>=1.4.4