diff --git a/netbox_cmdb/netbox_cmdb/admin.py b/netbox_cmdb/netbox_cmdb/admin.py index 7c32ae0..a5ace97 100644 --- a/netbox_cmdb/netbox_cmdb/admin.py +++ b/netbox_cmdb/netbox_cmdb/admin.py @@ -18,6 +18,7 @@ from netbox_cmdb.models.bgp_community_list import BGPCommunityList, BGPCommunityListTerm from netbox_cmdb.models.prefix_list import PrefixList, PrefixListTerm from netbox_cmdb.models.route_policy import RoutePolicy, RoutePolicyTerm +from netbox_cmdb.models.snmp import SNMP, SNMPCommunity class BaseAdmin(admin.ModelAdmin): @@ -173,6 +174,35 @@ class BGPCommunityListAdmin(BaseAdmin): ) +@admin.register(SNMP) +class SNMPAdmin(BaseAdmin): + """Admin class to manage SNMPCommunity objects.""" + + list_display = ( + "device", + "community_list_display", + "location", + "contact", + ) + + def community_list_display(self, obj): + return ", ".join([str(community) for community in obj.community_list.all()]) + + community_list_display.short_description = "Community List" + + +@admin.register(SNMPCommunity) +class SNMPCommunitytAdmin(BaseAdmin): + """Admin class to manage SNMP objects.""" + + list_display = ( + "name", + "type", + "community", + ) + search_fields = ("name", "name") + + # We need to register Netbox core models to the Admin page or we won't be able to lookup # dynamically over the objects. @admin.register(IPAddress) diff --git a/netbox_cmdb/netbox_cmdb/api/snmp/__init__.py b/netbox_cmdb/netbox_cmdb/api/snmp/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/netbox_cmdb/netbox_cmdb/api/snmp/serializers.py b/netbox_cmdb/netbox_cmdb/api/snmp/serializers.py new file mode 100644 index 0000000..023e31f --- /dev/null +++ b/netbox_cmdb/netbox_cmdb/api/snmp/serializers.py @@ -0,0 +1,19 @@ +"""Route Policy serializers.""" + +from rest_framework.serializers import ModelSerializer + +from netbox_cmdb.models.snmp import SNMP, SNMPCommunity + + +class SNMPCommunitySerializer(ModelSerializer): + + class Meta: + model = SNMPCommunity + fields = ["name", "community", "type"] + + +class SNMPSerializer(ModelSerializer): + + class Meta: + model = SNMP + fields = ["community_list", "location", "contact", "device"] diff --git a/netbox_cmdb/netbox_cmdb/api/snmp/views.py b/netbox_cmdb/netbox_cmdb/api/snmp/views.py new file mode 100644 index 0000000..c08edaa --- /dev/null +++ b/netbox_cmdb/netbox_cmdb/api/snmp/views.py @@ -0,0 +1,24 @@ +"""Route Policy views.""" + +from netbox_cmdb import filtersets + +from netbox_cmdb.api.route_policy.serializers import WritableRoutePolicySerializer +from netbox_cmdb.api.viewsets import CustomNetBoxModelViewSet +from netbox_cmdb.models.snmp import SNMP, SNMPCommunity +from netbox_cmdb.api.snmp.serializers import SNMPCommunitySerializer, SNMPSerializer + + +class SNMPCommunityViewSet(CustomNetBoxModelViewSet): + queryset = SNMPCommunity.objects.all() + serializer_class = SNMPCommunitySerializer + filterset_fields = [ + "name", + "community", + "type", + ] + + +class SNMPViewSet(CustomNetBoxModelViewSet): + queryset = SNMP.objects.all() + serializer_class = SNMPSerializer + filterset_fields = ["community_list", "location", "contact", "device"] diff --git a/netbox_cmdb/netbox_cmdb/api/urls.py b/netbox_cmdb/netbox_cmdb/api/urls.py index ed57cd1..0a7edbb 100644 --- a/netbox_cmdb/netbox_cmdb/api/urls.py +++ b/netbox_cmdb/netbox_cmdb/api/urls.py @@ -11,6 +11,7 @@ from netbox_cmdb.api.bgp_community_list.views import BGPCommunityListViewSet from netbox_cmdb.api.prefix_list.views import PrefixListViewSet from netbox_cmdb.api.route_policy.views import RoutePolicyViewSet +from netbox_cmdb.api.snmp.views import SNMPCommunityViewSet, SNMPViewSet router = NetBoxRouter() @@ -21,6 +22,8 @@ router.register("peer-groups", BGPPeerGroupViewSet) router.register("prefix-lists", PrefixListViewSet) router.register("route-policies", RoutePolicyViewSet) +router.register("snmp", SNMPViewSet) +router.register("snmp-community", SNMPCommunityViewSet) urlpatterns = [ path( diff --git a/netbox_cmdb/netbox_cmdb/choices.py b/netbox_cmdb/netbox_cmdb/choices.py index 32ddd66..7b0978d 100644 --- a/netbox_cmdb/netbox_cmdb/choices.py +++ b/netbox_cmdb/netbox_cmdb/choices.py @@ -1,6 +1,18 @@ from utilities.choices import ChoiceSet +class SNMPCommunityType(ChoiceSet): + """A ChoiceSet to define the communityType.""" + + RO = "readonly" + RW = "readwrite" + + CHOICES = [ + (RO, "ReadOnly", "green"), + (RW, "Read&Write", "red"), + ] + + class AssetStateChoices(ChoiceSet): """A ChoiceSet to define the state of an asset.""" diff --git a/netbox_cmdb/netbox_cmdb/forms.py b/netbox_cmdb/netbox_cmdb/forms.py index 0538cb2..1c0cc19 100644 --- a/netbox_cmdb/netbox_cmdb/forms.py +++ b/netbox_cmdb/netbox_cmdb/forms.py @@ -6,11 +6,12 @@ from django import forms from django.utils.translation import gettext as _ from extras.models import Tag +from netbox_cmdb.models.snmp import SNMP, SNMPCommunity from utilities.forms import DynamicModelMultipleChoiceField from utilities.forms.fields import DynamicModelChoiceField, MultipleChoiceField from netbox.forms import NetBoxModelFilterSetForm, NetBoxModelForm -from netbox_cmdb.choices import AssetMonitoringStateChoices, AssetStateChoices +from netbox_cmdb.choices import AssetMonitoringStateChoices, AssetStateChoices, SNMPCommunityType from netbox_cmdb.models.bgp import ASN, BGPPeerGroup, BGPSession @@ -83,3 +84,17 @@ def clean(self): pass # such validation is already handled in previous validation steps if count < 1: raise forms.ValidationError("You must have at least one term.") + + +class SNMPGroupForm(NetBoxModelForm): + device = DynamicModelChoiceField(queryset=Device.objects.all()) + + class Meta: + model = SNMP + fields = ["device", "community_list", "location", "contact"] + + +class SNMPCommunityGroupForm(NetBoxModelForm): + class Meta: + model = SNMPCommunity + fields = ["name", "community", "type"] diff --git a/netbox_cmdb/netbox_cmdb/migrations/0040_snmpcommunity_snmp.py b/netbox_cmdb/netbox_cmdb/migrations/0040_snmpcommunity_snmp.py new file mode 100644 index 0000000..09e0d6b --- /dev/null +++ b/netbox_cmdb/netbox_cmdb/migrations/0040_snmpcommunity_snmp.py @@ -0,0 +1,42 @@ +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0161_cabling_cleanup'), + ('netbox_cmdb', '0039_logicalinterface'), + ] + + operations = [ + migrations.CreateModel( + name='SNMPCommunity', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)), + ('created', models.DateTimeField(auto_now_add=True, null=True)), + ('last_updated', models.DateTimeField(auto_now=True, null=True)), + ('name', models.CharField(max_length=100, unique=True)), + ('community', models.CharField(max_length=31)), + ('type', models.CharField(default='readonly', max_length=10)), + ], + options={ + 'verbose_name_plural': 'SNMP Communities', + }, + ), + migrations.CreateModel( + name='SNMP', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)), + ('created', models.DateTimeField(auto_now_add=True, null=True)), + ('last_updated', models.DateTimeField(auto_now=True, null=True)), + ('location', models.CharField(max_length=31)), + ('contact', models.CharField(max_length=31)), + ('community_list', models.ManyToManyField(blank=True, default=None, related_name='%(class)s_community', to='netbox_cmdb.snmpcommunity')), + ('device', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='dcim.device')), + ], + options={ + 'verbose_name_plural': 'SNMP', + }, + ), + ] diff --git a/netbox_cmdb/netbox_cmdb/models/snmp.py b/netbox_cmdb/netbox_cmdb/models/snmp.py new file mode 100644 index 0000000..c28117c --- /dev/null +++ b/netbox_cmdb/netbox_cmdb/models/snmp.py @@ -0,0 +1,46 @@ +from django.db import models +from netbox_cmdb.choices import SNMPCommunityType +from netbox.models import ChangeLoggedModel +from django.core.exceptions import ValidationError +from django.contrib.postgres.fields import ArrayField + + +class SNMPCommunity(ChangeLoggedModel): + """A Snmp Community""" + + name = models.CharField(max_length=100, unique=True) + community = models.CharField(max_length=31) + type = models.CharField( + max_length=10, + choices=SNMPCommunityType, + default=SNMPCommunityType.RO, + help_text="Defines the community string permissions of either read-only RO or read-write RW", + ) + + def __str__(self): + return f"{self.name}" + + class Meta: + verbose_name_plural = "SNMP Communities" + + +class SNMP(ChangeLoggedModel): + """A Snmp configuration""" + + community_list = models.ManyToManyField( + to=SNMPCommunity, related_name="%(class)s_community", blank=True, default=None + ) + + location = models.CharField(max_length=31) + contact = models.CharField(max_length=31) + + device = models.OneToOneField( + to="dcim.Device", + on_delete=models.CASCADE, + ) + + class Meta: + verbose_name_plural = "SNMP" + + def __str__(self): + return f"SNMP configuration of {self.device.name}" diff --git a/netbox_cmdb/netbox_cmdb/navigation.py b/netbox_cmdb/netbox_cmdb/navigation.py index 8a8e589..246e69a 100644 --- a/netbox_cmdb/netbox_cmdb/navigation.py +++ b/netbox_cmdb/netbox_cmdb/navigation.py @@ -40,4 +40,28 @@ ), ), ), + PluginMenuItem( + link="plugins:netbox_cmdb:snmp_list", + link_text="SNMP", + buttons=( + PluginMenuButton( + link="plugins:netbox_cmdb:snmp_add", + title="SNMP", + icon_class="mdi mdi-plus-thick", + color=ButtonColorChoices.GREEN, + ), + ), + ), + PluginMenuItem( + link="plugins:netbox_cmdb:snmpcommunity_list", + link_text="SNMP Community", + buttons=( + PluginMenuButton( + link="plugins:netbox_cmdb:snmpcommunity_add", + title="SNMP Community", + icon_class="mdi mdi-plus-thick", + color=ButtonColorChoices.GREEN, + ), + ), + ), ) diff --git a/netbox_cmdb/netbox_cmdb/tables.py b/netbox_cmdb/netbox_cmdb/tables.py index 10f72eb..b4a5dec 100644 --- a/netbox_cmdb/netbox_cmdb/tables.py +++ b/netbox_cmdb/netbox_cmdb/tables.py @@ -4,6 +4,7 @@ from netbox.tables import NetBoxTable, columns from netbox_cmdb.models.bgp import ASN, BGPPeerGroup, BGPSession +from netbox_cmdb.models.snmp import SNMP, SNMPCommunity class ASNTable(NetBoxTable): @@ -60,3 +61,17 @@ class Meta(NetBoxTable.Meta): "route_policy_out", "maximum_prefixes", ) + + +class SNMPTable(NetBoxTable): + device = tables.LinkColumn() + + class Meta(NetBoxTable.Meta): + model = SNMP + fields = ("device", "community_list", "location", "contact") + + +class SNMPCommunityTable(NetBoxTable): + class Meta(NetBoxTable.Meta): + model = SNMPCommunity + fields = ("name", "community", "type") diff --git a/netbox_cmdb/netbox_cmdb/tests/common.py b/netbox_cmdb/netbox_cmdb/tests/common.py new file mode 100644 index 0000000..8e609e9 --- /dev/null +++ b/netbox_cmdb/netbox_cmdb/tests/common.py @@ -0,0 +1,33 @@ +from dcim.models.devices import Device, DeviceRole, DeviceType, Manufacturer +from dcim.models.sites import Site +from django.test import TestCase +from ipam.models.ip import IPAddress +from tenancy.models.tenants import Tenant +from netbox_cmdb.models.bgp import ASN + + +class BaseTestCase(TestCase): + def setUp(self): + site = Site.objects.create(name="SiteTest", slug="site-test") + manufacturer = Manufacturer.objects.create(name="test", slug="test") + device_type = DeviceType.objects.create( + manufacturer=manufacturer, model="model-test", slug="model-test" + ) + device_role = DeviceRole.objects.create(name="role-test", slug="role-test") + self.device1 = Device.objects.create( + name="router-test1", + device_role=device_role, + device_type=device_type, + site=site, + ) + self.asn1 = ASN.objects.create(number="1", organization_name="router-test1") + self.ip_address1 = IPAddress.objects.create(address="10.0.0.1/32") + self.device2 = Device.objects.create( + name="router-test2", + device_role=device_role, + device_type=device_type, + site=site, + ) + self.asn2 = ASN.objects.create(number="2", organization_name="router-test2") + self.ip_address2 = IPAddress.objects.create(address="10.0.0.2/32") + self.tenant = Tenant.objects.create(name="tenant1", slug="tenant1") diff --git a/netbox_cmdb/netbox_cmdb/tests/snmp/__init__.py b/netbox_cmdb/netbox_cmdb/tests/snmp/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/netbox_cmdb/netbox_cmdb/tests/snmp/test_snmp_serializer.py b/netbox_cmdb/netbox_cmdb/tests/snmp/test_snmp_serializer.py new file mode 100644 index 0000000..7002579 --- /dev/null +++ b/netbox_cmdb/netbox_cmdb/tests/snmp/test_snmp_serializer.py @@ -0,0 +1,35 @@ +from netbox_cmdb.api.snmp.serializers import SNMPCommunitySerializer, SNMPSerializer +from netbox_cmdb.models.snmp import SNMP, SNMPCommunity +from netbox_cmdb.choices import SNMPCommunityType +from netbox_cmdb.tests.common import BaseTestCase + + +class SNMPCommunitySerializerCreate(BaseTestCase): + def test_create(self): + data = {"name": "my_comm1", "community": "my_community", "type": "readonly"} + snmpcommunity_serializer = SNMPCommunitySerializer(data=data) + assert snmpcommunity_serializer.is_valid() == True + snmpcommunity_serializer.save() + + community1 = SNMPCommunity.objects.get(name="my_comm1") + + assert community1.community == "my_community" + assert community1.type == SNMPCommunityType.RO + + data = { + "device": self.device1.pk, + "community_list": [community1.pk], + "location": "my_location", + "contact": "my_team", + } + + snmp_serializer = SNMPSerializer(data=data) + assert snmp_serializer.is_valid() == True + snmp_serializer.save() + + conf = SNMP.objects.get(device__name=self.device1.name) + + assert conf.location == "my_location" + assert conf.contact == "my_team" + assert conf.community_list.all()[0] == community1 + assert conf.device == self.device1 diff --git a/netbox_cmdb/netbox_cmdb/urls.py b/netbox_cmdb/netbox_cmdb/urls.py index 82b09f3..f774d62 100644 --- a/netbox_cmdb/netbox_cmdb/urls.py +++ b/netbox_cmdb/netbox_cmdb/urls.py @@ -4,6 +4,7 @@ from netbox.views.generic import ObjectChangeLogView, ObjectJournalView from netbox_cmdb.models.bgp import * +from netbox_cmdb.models.snmp import SNMP, SNMPCommunity from netbox_cmdb.views import ( ASNDeleteView, ASNEditView, @@ -18,6 +19,12 @@ BGPSessionEditView, BGPSessionListView, BGPSessionView, + SNMPCommunityDeleteView, + SNMPCommunityEditView, + SNMPCommunityListView, + SNMPDeleteView, + SNMPEditView, + SNMPListView, ) urlpatterns = [ @@ -92,4 +99,42 @@ name="bgppeergroup_journal", kwargs={"model": BGPPeerGroup}, ), + # SNMP + path("snmp/", SNMPListView.as_view(), name="snmp_list"), + path("snmp/add/", SNMPEditView.as_view(), name="snmp_add"), + path( + "snmp//edit/", + SNMPEditView.as_view(), + name="snmp_edit", + ), + path( + "snmp//delete/", + SNMPDeleteView.as_view(), + name="snmp_delete", + ), + path( + "snmp//changelog/", + ObjectChangeLogView.as_view(), + name="snmp_changelog", + kwargs={"model": SNMP}, + ), + # SNMP Community + path("snmp-community/", SNMPCommunityListView.as_view(), name="snmpcommunity_list"), + path("snmp-community/add/", SNMPCommunityEditView.as_view(), name="snmpcommunity_add"), + path( + "snmp-community//edit/", + SNMPCommunityEditView.as_view(), + name="snmpcommunity_edit", + ), + path( + "snmp-community//delete/", + SNMPCommunityDeleteView.as_view(), + name="snmpcommunity_delete", + ), + path( + "snmp-community//changelog/", + ObjectChangeLogView.as_view(), + name="snmpcommunity_changelog", + kwargs={"model": SNMPCommunity}, + ), ] diff --git a/netbox_cmdb/netbox_cmdb/views.py b/netbox_cmdb/netbox_cmdb/views.py index 5be9867..715c793 100644 --- a/netbox_cmdb/netbox_cmdb/views.py +++ b/netbox_cmdb/netbox_cmdb/views.py @@ -17,9 +17,18 @@ BGPPeerGroupForm, BGPSessionFilterSetForm, BGPSessionForm, + SNMPCommunityGroupForm, + SNMPGroupForm, ) from netbox_cmdb.models.bgp import ASN, BGPPeerGroup, BGPSession, DeviceBGPSession -from netbox_cmdb.tables import ASNTable, BGPPeerGroupTable, BGPSessionTable +from netbox_cmdb.models.snmp import SNMP, SNMPCommunity +from netbox_cmdb.tables import ( + ASNTable, + BGPPeerGroupTable, + BGPSessionTable, + SNMPCommunityTable, + SNMPTable, +) ## ASN views @@ -90,14 +99,6 @@ def get_extra_context(self, request, instance): } -## DeviceBGPSession views - - -class DeviceBGPSessionListView(ObjectListView): - queryset = DeviceBGPSession.objects.all() - filterset = None - - ## Peer groups views class BGPPeerGroupListView(ObjectListView): queryset = BGPPeerGroup.objects.all() @@ -118,3 +119,33 @@ class BGPPeerGroupDeleteView(ObjectDeleteView): class BGPPeerGroupView(ObjectView): queryset = BGPPeerGroup.objects.all() template_name = "netbox_cmdb/bgppeergroup.html" + + +## Snmp groups views +class SNMPListView(ObjectListView): + queryset = SNMP.objects.all() + table = SNMPTable + + +class SNMPEditView(ObjectEditView): + queryset = SNMP.objects.all() + form = SNMPGroupForm + + +class SNMPDeleteView(ObjectDeleteView): + queryset = SNMP.objects.all() + + +## Snmp Community groups views +class SNMPCommunityListView(ObjectListView): + queryset = SNMPCommunity.objects.all() + table = SNMPCommunityTable + + +class SNMPCommunityEditView(ObjectEditView): + queryset = SNMPCommunity.objects.all() + form = SNMPCommunityGroupForm + + +class SNMPCommunityDeleteView(ObjectDeleteView): + queryset = SNMPCommunity.objects.all()