diff --git a/.github/workflows/lint-snmp-monitoring.yml b/.github/workflows/lint-snmp-monitoring.yml new file mode 100644 index 0000000..04da25b --- /dev/null +++ b/.github/workflows/lint-snmp-monitoring.yml @@ -0,0 +1,32 @@ +name: Lint SNMP Monitoring + +on: + push: + branches: [main, master] + paths: + - "backend/control_center/snmp_monitoring/**" + - ".github/workflows/lint-snmp-monitoring.yml" + pull_request: + branches: [main, master] + paths: + - "backend/control_center/snmp_monitoring/**" + - ".github/workflows/lint-snmp-monitoring.yml" + +jobs: + lint: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.10" + + - name: Install flake8 + run: pip install flake8 + + - name: Run flake8 on snmp_monitoring + run: flake8 backend/control_center/snmp_monitoring/ --max-line-length=120 diff --git a/backend/control_center/control_center/settings.py b/backend/control_center/control_center/settings.py index f1c6212..b026a0d 100644 --- a/backend/control_center/control_center/settings.py +++ b/backend/control_center/control_center/settings.py @@ -81,6 +81,7 @@ 'channels', # custom apps 'network_device', + 'snmp_monitoring', 'odl', 'software_plugin', 'classifier', @@ -224,7 +225,7 @@ APP_LOGGERS = [ 'controller', 'general', 'ovs_install', 'ovs_management', 'software_plugin', 'utils', 'network_device', 'odl', 'onos', - 'account', 'notification', 'device_monitoring', 'network_data', 'classifier' + 'account', 'notification', 'device_monitoring', 'network_data', 'classifier', 'snmp_monitoring' ] LOGGING = { diff --git a/backend/control_center/requirements.txt b/backend/control_center/requirements.txt index a046cd1..c7fe000 100644 --- a/backend/control_center/requirements.txt +++ b/backend/control_center/requirements.txt @@ -31,4 +31,5 @@ service-identity==24.1.0 pyOpenSSL==24.1.0 python-dotenv==1.0.0 uvicorn[standard] -pipreqs==0.4.12 \ No newline at end of file +pipreqs==0.4.12 +pysnmp==7.1.22 \ No newline at end of file diff --git a/backend/control_center/snmp_monitoring/__init__.py b/backend/control_center/snmp_monitoring/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/control_center/snmp_monitoring/admin.py b/backend/control_center/snmp_monitoring/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/backend/control_center/snmp_monitoring/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/backend/control_center/snmp_monitoring/apps.py b/backend/control_center/snmp_monitoring/apps.py new file mode 100644 index 0000000..27b5854 --- /dev/null +++ b/backend/control_center/snmp_monitoring/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class SnmpMonitoringConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'snmp_monitoring' diff --git a/backend/control_center/snmp_monitoring/migrations/0001_initial.py b/backend/control_center/snmp_monitoring/migrations/0001_initial.py new file mode 100644 index 0000000..cdbaf1c --- /dev/null +++ b/backend/control_center/snmp_monitoring/migrations/0001_initial.py @@ -0,0 +1,123 @@ +# Generated by Django 5.1.14 on 2025-11-24 10:43 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('network_device', '0004_alter_networkdevice_operating_system'), + ] + + operations = [ + migrations.CreateModel( + name='SNMPDevice', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(help_text='Human-readable device name', max_length=100)), + ('ip_address', models.GenericIPAddressField(db_index=True, help_text='IP address of the SNMP device')), + ('vendor', models.CharField(choices=[('mikrotik', 'MikroTik'), ('ubiquiti', 'Ubiquiti'), ('tp_link', 'TP-Link'), ('cisco', 'Cisco'), ('netgear', 'Netgear'), ('d_link', 'D-Link'), ('huawei', 'Huawei'), ('hp', 'HP'), ('other', 'Other')], db_index=True, default='other', help_text='Device vendor/manufacturer', max_length=50)), + ('community_string', models.CharField(help_text="SNMP community string (typically 'public' or 'private')", max_length=100)), + ('snmp_version', models.CharField(choices=[('2c', 'SNMP v2c')], default='2c', help_text='SNMP version (v3 support to be added later)', max_length=10)), + ('port', models.IntegerField(default=161, help_text='SNMP port (default: 161)')), + ('polling_interval', models.IntegerField(default=60, help_text='Polling interval in seconds (default: 60)')), + ('is_active', models.BooleanField(db_index=True, default=True, help_text='Whether this device should be polled')), + ('last_successful_poll', models.DateTimeField(blank=True, help_text='Timestamp of last successful SNMP poll', null=True)), + ('last_poll_attempt', models.DateTimeField(blank=True, help_text='Timestamp of last poll attempt (successful or not)', null=True)), + ('consecutive_failures', models.IntegerField(default=0, help_text='Number of consecutive polling failures')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('network_device', models.ForeignKey(blank=True, help_text='Optional link to existing network device', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='snmp_devices', to='network_device.networkdevice')), + ], + options={ + 'ordering': ['-created_at'], + }, + ), + migrations.CreateModel( + name='SNMPDeviceAlert', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('last_cpu_alert', models.DateTimeField(blank=True, null=True)), + ('last_memory_alert', models.DateTimeField(blank=True, null=True)), + ('last_disk_alert', models.DateTimeField(blank=True, null=True)), + ('last_interface_alert', models.DateTimeField(blank=True, null=True)), + ('last_connection_failure_alert', models.DateTimeField(blank=True, null=True)), + ('device', models.OneToOneField(help_text='SNMP device for alert tracking', on_delete=django.db.models.deletion.CASCADE, related_name='alert_settings', to='snmp_monitoring.snmpdevice')), + ], + ), + migrations.CreateModel( + name='SNMPInterfaceStats', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('interface_name', models.CharField(db_index=True, help_text="Interface name (e.g., 'eth0', 'wlan0')", max_length=100)), + ('interface_index', models.IntegerField(blank=True, help_text='SNMP interface index (ifIndex)', null=True)), + ('bytes_in', models.BigIntegerField(default=0, help_text='Total bytes received on interface')), + ('bytes_out', models.BigIntegerField(default=0, help_text='Total bytes transmitted on interface')), + ('packets_in', models.BigIntegerField(default=0, help_text='Total packets received on interface')), + ('packets_out', models.BigIntegerField(default=0, help_text='Total packets transmitted on interface')), + ('errors_in', models.BigIntegerField(default=0, help_text='Input errors on interface')), + ('errors_out', models.BigIntegerField(default=0, help_text='Output errors on interface')), + ('throughput_mbps', models.FloatField(blank=True, help_text='Calculated throughput in Mbps (if available)', null=True)), + ('utilization_percent', models.FloatField(blank=True, help_text='Interface utilization percentage (if link speed known)', null=True)), + ('timestamp', models.DateTimeField(auto_now_add=True, db_index=True, help_text='Timestamp when statistics were collected')), + ('device', models.ForeignKey(help_text='SNMP device this interface belongs to', on_delete=django.db.models.deletion.CASCADE, related_name='interface_stats', to='snmp_monitoring.snmpdevice')), + ], + options={ + 'ordering': ['-timestamp'], + }, + ), + migrations.CreateModel( + name='SNMPMetrics', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('cpu_usage', models.FloatField(blank=True, help_text='CPU usage percentage', null=True)), + ('memory_usage', models.FloatField(blank=True, help_text='Memory usage percentage', null=True)), + ('disk_usage', models.FloatField(blank=True, help_text='Disk usage percentage', null=True)), + ('uptime_seconds', models.BigIntegerField(blank=True, help_text='Device uptime in seconds', null=True)), + ('timestamp', models.DateTimeField(auto_now_add=True, db_index=True, help_text='Timestamp when metric was collected')), + ('device', models.ForeignKey(help_text='SNMP device this metric belongs to', on_delete=django.db.models.deletion.CASCADE, related_name='metrics', to='snmp_monitoring.snmpdevice')), + ], + options={ + 'ordering': ['-timestamp'], + }, + ), + migrations.AddIndex( + model_name='snmpdevice', + index=models.Index(fields=['ip_address'], name='snmp_monito_ip_addr_22fa3f_idx'), + ), + migrations.AddIndex( + model_name='snmpdevice', + index=models.Index(fields=['vendor'], name='snmp_monito_vendor_070617_idx'), + ), + migrations.AddIndex( + model_name='snmpdevice', + index=models.Index(fields=['is_active'], name='snmp_monito_is_acti_6fe373_idx'), + ), + migrations.AlterUniqueTogether( + name='snmpdevice', + unique_together={('ip_address', 'port')}, + ), + migrations.AddIndex( + model_name='snmpinterfacestats', + index=models.Index(fields=['device', 'interface_name', 'timestamp'], name='snmp_monito_device__726f5f_idx'), + ), + migrations.AddIndex( + model_name='snmpinterfacestats', + index=models.Index(fields=['device', 'timestamp'], name='snmp_monito_device__ff971d_idx'), + ), + migrations.AddIndex( + model_name='snmpinterfacestats', + index=models.Index(fields=['timestamp'], name='snmp_monito_timesta_30d54f_idx'), + ), + migrations.AddIndex( + model_name='snmpmetrics', + index=models.Index(fields=['device', 'timestamp'], name='snmp_monito_device__c7e7af_idx'), + ), + migrations.AddIndex( + model_name='snmpmetrics', + index=models.Index(fields=['timestamp'], name='snmp_monito_timesta_1f76d0_idx'), + ), + ] diff --git a/backend/control_center/snmp_monitoring/migrations/0002_alter_primary_keys.py b/backend/control_center/snmp_monitoring/migrations/0002_alter_primary_keys.py new file mode 100644 index 0000000..eb02b8b --- /dev/null +++ b/backend/control_center/snmp_monitoring/migrations/0002_alter_primary_keys.py @@ -0,0 +1,43 @@ +# Migration to alter SNMPMetrics and SNMPInterfaceStats primary keys to include timestamp + +from django.db import migrations + + +class Migration(migrations.Migration): + atomic = False # Altering primary keys typically cannot run inside a transaction + + dependencies = [ + ('snmp_monitoring', '0001_initial'), + ] + + operations = [ + # Alter SNMPMetrics primary key + migrations.RunSQL( + sql=""" + -- Drop the automatically created primary key constraint. + ALTER TABLE snmp_monitoring_snmpmetrics DROP CONSTRAINT snmp_monitoring_snmpmetrics_pkey; + -- Create a composite primary key including the partitioning column (timestamp) and the id. + ALTER TABLE snmp_monitoring_snmpmetrics ADD PRIMARY KEY (timestamp, id); + """, + reverse_sql=""" + -- Reverse: Drop the composite primary key and restore the original primary key on id. + ALTER TABLE snmp_monitoring_snmpmetrics DROP CONSTRAINT snmp_monitoring_snmpmetrics_pkey; + ALTER TABLE snmp_monitoring_snmpmetrics ADD PRIMARY KEY (id); + """, + ), + # Alter SNMPInterfaceStats primary key + migrations.RunSQL( + sql=""" + -- Drop the automatically created primary key constraint. + ALTER TABLE snmp_monitoring_snmpinterfacestats DROP CONSTRAINT snmp_monitoring_snmpinterfacestats_pkey; + -- Create a composite primary key including the partitioning column (timestamp) and the id. + ALTER TABLE snmp_monitoring_snmpinterfacestats ADD PRIMARY KEY (timestamp, id); + """, + reverse_sql=""" + -- Reverse: Drop the composite primary key and restore the original primary key on id. + ALTER TABLE snmp_monitoring_snmpinterfacestats DROP CONSTRAINT snmp_monitoring_snmpinterfacestats_pkey; + ALTER TABLE snmp_monitoring_snmpinterfacestats ADD PRIMARY KEY (id); + """, + ), + ] + diff --git a/backend/control_center/snmp_monitoring/migrations/0003_make_hypertables.py b/backend/control_center/snmp_monitoring/migrations/0003_make_hypertables.py new file mode 100644 index 0000000..28ce945 --- /dev/null +++ b/backend/control_center/snmp_monitoring/migrations/0003_make_hypertables.py @@ -0,0 +1,78 @@ +# Migration to convert SNMPMetrics and SNMPInterfaceStats to TimescaleDB hypertables + +from django.db import migrations + + +class Migration(migrations.Migration): + atomic = False # The hypertable conversion must run outside of a transaction. + + dependencies = [ + ('snmp_monitoring', '0002_alter_primary_keys'), + ] + + operations = [ + # Ensure TimescaleDB extension exists before attempting hypertable operations + migrations.RunSQL( + sql=""" + CREATE EXTENSION IF NOT EXISTS timescaledb; + """, + reverse_sql=migrations.RunSQL.noop + ), + # Convert SNMPMetrics table to TimescaleDB hypertable + # Note: create_default_indexes => FALSE prevents duplicate timestamp index + # (migration 0001 already created all needed indexes including timestamp) + # chunk_time_interval => '1 day' chosen for time-series data + migrations.RunSQL( + sql=""" + SELECT create_hypertable( + 'snmp_monitoring_snmpmetrics', + 'timestamp', + migrate_data => true, + if_not_exists => TRUE, + create_default_indexes => FALSE, + chunk_time_interval => INTERVAL '1 day' + ); + """, + reverse_sql=""" + -- Safely drop hypertable only if it exists and is actually a hypertable + DO $$ + BEGIN + IF EXISTS ( + SELECT 1 FROM timescaledb_information.hypertables + WHERE hypertable_name = 'snmp_monitoring_snmpmetrics' + ) THEN + PERFORM drop_hypertable('snmp_monitoring_snmpmetrics', IF_EXISTS => TRUE); + END IF; + END $$; + """ + ), + # Convert SNMPInterfaceStats table to TimescaleDB hypertable + # Note: create_default_indexes => FALSE prevents duplicate timestamp index + # (migration 0001 already created all needed indexes including timestamp) + # chunk_time_interval => '1 day' chosen for high-frequency data (stats every few seconds) + migrations.RunSQL( + sql=""" + SELECT create_hypertable( + 'snmp_monitoring_snmpinterfacestats', + 'timestamp', + migrate_data => true, + if_not_exists => TRUE, + create_default_indexes => FALSE, + chunk_time_interval => INTERVAL '1 day' + ); + """, + reverse_sql=""" + -- Safely drop hypertable only if it exists and is actually a hypertable + DO $$ + BEGIN + IF EXISTS ( + SELECT 1 FROM timescaledb_information.hypertables + WHERE hypertable_name = 'snmp_monitoring_snmpinterfacestats' + ) THEN + PERFORM drop_hypertable('snmp_monitoring_snmpinterfacestats', IF_EXISTS => TRUE); + END IF; + END $$; + """ + ), + ] + diff --git a/backend/control_center/snmp_monitoring/migrations/0004_enable_compression.py b/backend/control_center/snmp_monitoring/migrations/0004_enable_compression.py new file mode 100644 index 0000000..2e8f448 --- /dev/null +++ b/backend/control_center/snmp_monitoring/migrations/0004_enable_compression.py @@ -0,0 +1,71 @@ +# Migration to enable compression on SNMPMetrics and SNMPInterfaceStats hypertables + +from django.db import migrations + + +class Migration(migrations.Migration): + atomic = False + + dependencies = [ + ('snmp_monitoring', '0003_make_hypertables'), + ] + + operations = [ + # Enable compression on SNMPMetrics hypertable + migrations.RunSQL( + sql=""" + ALTER TABLE snmp_monitoring_snmpmetrics SET ( + timescaledb.compress, + timescaledb.compress_segmentby = 'device_id', + timescaledb.compress_orderby = 'timestamp DESC' + ); + """, + reverse_sql=""" + ALTER TABLE snmp_monitoring_snmpmetrics SET (timescaledb.compress = false); + """ + ), + + # Add compression policy to compress chunks older than 1 day + migrations.RunSQL( + sql=""" + SELECT add_compression_policy( + 'snmp_monitoring_snmpmetrics', + INTERVAL '1 day', + if_not_exists => TRUE + ); + """, + reverse_sql=""" + SELECT remove_compression_policy('snmp_monitoring_snmpmetrics', if_exists => TRUE); + """ + ), + + # Enable compression on SNMPInterfaceStats hypertable + migrations.RunSQL( + sql=""" + ALTER TABLE snmp_monitoring_snmpinterfacestats SET ( + timescaledb.compress, + timescaledb.compress_segmentby = 'device_id, interface_name', + timescaledb.compress_orderby = 'timestamp DESC' + ); + """, + reverse_sql=""" + ALTER TABLE snmp_monitoring_snmpinterfacestats SET (timescaledb.compress = false); + """ + ), + + # Add compression policy to compress chunks older than 6 hours + # (keeps recent data uncompressed for fast per-second queries) + migrations.RunSQL( + sql=""" + SELECT add_compression_policy( + 'snmp_monitoring_snmpinterfacestats', + INTERVAL '6 hours', + if_not_exists => TRUE + ); + """, + reverse_sql=""" + SELECT remove_compression_policy('snmp_monitoring_snmpinterfacestats', if_exists => TRUE); + """ + ), + ] + diff --git a/backend/control_center/snmp_monitoring/migrations/__init__.py b/backend/control_center/snmp_monitoring/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/control_center/snmp_monitoring/models.py b/backend/control_center/snmp_monitoring/models.py new file mode 100644 index 0000000..9114950 --- /dev/null +++ b/backend/control_center/snmp_monitoring/models.py @@ -0,0 +1,288 @@ +# File: models.py +# Copyright (C) 2025 Taurine Technology +# +# This file is part of the SDN Launch Control project. +# +# This project is licensed under the GNU Affero General Public License v3.0 (AGPL-3.0), +# available at: https://www.gnu.org/licenses/agpl-3.0.en.html#license-text +# +# Contributions to this project are governed by a Contributor License Agreement (CLA). +# By submitting a contribution, contributors grant Taurine Technology exclusive rights to +# the contribution, including the right to relicense it under a different license +# at the copyright owner's discretion. +# +# Unless required by applicable law or agreed to in writing, software distributed +# under this license is provided "AS IS", WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, +# either express or implied. See the GNU Affero General Public License for more details. +# +# For inquiries, contact Keegan White at keeganwhite@taurinetech.com. + +from django.db import models +from network_device.models import NetworkDevice + +# Vendor choices for SNMP devices +VENDOR_CHOICES = ( + ('mikrotik', 'MikroTik'), + ('ubiquiti', 'Ubiquiti'), + ('tp_link', 'TP-Link'), + ('cisco', 'Cisco'), + ('netgear', 'Netgear'), + ('d_link', 'D-Link'), + ('huawei', 'Huawei'), + ('hp', 'HP'), + ('other', 'Other'), +) + + +class SNMPDevice(models.Model): + """ + Model for storing SNMP device configuration. + + Links to NetworkDevice (optional) and stores SNMP-specific configuration + including community string, vendor, and polling settings. + """ + # Optional link to existing NetworkDevice + network_device = models.ForeignKey( + NetworkDevice, + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name='snmp_devices', + help_text="Optional link to existing network device" + ) + + # Device identification + name = models.CharField( + max_length=100, + help_text="Human-readable device name" + ) + ip_address = models.GenericIPAddressField( + db_index=True, + help_text="IP address of the SNMP device" + ) + vendor = models.CharField( + max_length=50, + choices=VENDOR_CHOICES, + default='other', + db_index=True, + help_text="Device vendor/manufacturer" + ) + + # SNMP v2c configuration + community_string = models.CharField( + max_length=100, + help_text="SNMP community string (typically 'public' or 'private')" + ) + snmp_version = models.CharField( + max_length=10, + default='2c', + choices=[('2c', 'SNMP v2c')], + help_text="SNMP version (v3 support to be added later)" + ) + port = models.IntegerField( + default=161, + help_text="SNMP port (default: 161)" + ) + + # Polling configuration + polling_interval = models.IntegerField( + default=60, + help_text="Polling interval in seconds (default: 60)" + ) + is_active = models.BooleanField( + default=True, + db_index=True, + help_text="Whether this device should be polled" + ) + + # Status tracking + last_successful_poll = models.DateTimeField( + null=True, + blank=True, + help_text="Timestamp of last successful SNMP poll" + ) + last_poll_attempt = models.DateTimeField( + null=True, + blank=True, + help_text="Timestamp of last poll attempt (successful or not)" + ) + consecutive_failures = models.IntegerField( + default=0, + help_text="Number of consecutive polling failures" + ) + + # Metadata + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + ordering = ['-created_at'] + indexes = [ + models.Index(fields=['ip_address']), + models.Index(fields=['vendor']), + models.Index(fields=['is_active']), + ] + unique_together = [('ip_address', 'port')] # This might change when we add cloud support + + def __str__(self): + return f"{self.name} ({self.ip_address}) - {self.get_vendor_display()}" + + +class SNMPMetrics(models.Model): + """ + Time-series model for storing SNMP device metrics. + Optimized for TimescaleDB hypertable storage. + + Stores general device metrics like CPU, memory, uptime, etc. + """ + device = models.ForeignKey( + SNMPDevice, + on_delete=models.CASCADE, + related_name='metrics', + db_index=True, + help_text="SNMP device this metric belongs to" + ) + + # Metric values + cpu_usage = models.FloatField( + null=True, + blank=True, + help_text="CPU usage percentage" + ) + memory_usage = models.FloatField( + null=True, + blank=True, + help_text="Memory usage percentage" + ) + disk_usage = models.FloatField( + null=True, + blank=True, + help_text="Disk usage percentage" + ) + uptime_seconds = models.BigIntegerField( + null=True, + blank=True, + help_text="Device uptime in seconds" + ) + + # Additional metrics (stored as JSON or separate fields as needed) + # For now, we'll keep it simple with common metrics + + timestamp = models.DateTimeField( + auto_now_add=True, + db_index=True, + help_text="Timestamp when metric was collected" + ) + + class Meta: + ordering = ['-timestamp'] + indexes = [ + models.Index(fields=['device', 'timestamp']), + models.Index(fields=['timestamp']), + ] + + def __str__(self): + return f"Metrics for {self.device.name} @ {self.timestamp}" + + +class SNMPInterfaceStats(models.Model): + """ + Time-series model for storing SNMP interface statistics. + Optimized for TimescaleDB hypertable storage. + + Stores per-interface statistics similar to PortUtilizationStats. + """ + device = models.ForeignKey( + SNMPDevice, + on_delete=models.CASCADE, + related_name='interface_stats', + db_index=True, + help_text="SNMP device this interface belongs to" + ) + + interface_name = models.CharField( + max_length=100, + db_index=True, + help_text="Interface name (e.g., 'eth0', 'wlan0')" + ) + interface_index = models.IntegerField( + null=True, + blank=True, + help_text="SNMP interface index (ifIndex)" + ) + + # Interface statistics + bytes_in = models.BigIntegerField( + default=0, + help_text="Total bytes received on interface" + ) + bytes_out = models.BigIntegerField( + default=0, + help_text="Total bytes transmitted on interface" + ) + packets_in = models.BigIntegerField( + default=0, + help_text="Total packets received on interface" + ) + packets_out = models.BigIntegerField( + default=0, + help_text="Total packets transmitted on interface" + ) + errors_in = models.BigIntegerField( + default=0, + help_text="Input errors on interface" + ) + errors_out = models.BigIntegerField( + default=0, + help_text="Output errors on interface" + ) + + # Calculated fields (similar to PortUtilizationStats) + throughput_mbps = models.FloatField( + null=True, + blank=True, + help_text="Calculated throughput in Mbps (if available)" + ) + utilization_percent = models.FloatField( + null=True, + blank=True, + help_text="Interface utilization percentage (if link speed known)" + ) + + timestamp = models.DateTimeField( + auto_now_add=True, + db_index=True, + help_text="Timestamp when statistics were collected" + ) + + class Meta: + ordering = ['-timestamp'] + indexes = [ + models.Index(fields=['device', 'interface_name', 'timestamp']), + models.Index(fields=['device', 'timestamp']), + models.Index(fields=['timestamp']), + ] + + def __str__(self): + return f"Interface {self.interface_name} on {self.device.name} @ {self.timestamp}" + + +class SNMPDeviceAlert(models.Model): + """ + Tracks last notification time per device to prevent notification flooding. + Similar to DeviceHealthAlert in device_monitoring app. + """ + device = models.OneToOneField( + SNMPDevice, + on_delete=models.CASCADE, + related_name='alert_settings', + help_text="SNMP device for alert tracking" + ) + last_cpu_alert = models.DateTimeField(null=True, blank=True) + last_memory_alert = models.DateTimeField(null=True, blank=True) + last_disk_alert = models.DateTimeField(null=True, blank=True) + last_interface_alert = models.DateTimeField(null=True, blank=True) + last_connection_failure_alert = models.DateTimeField(null=True, blank=True) + + def __str__(self): + return f"Alert settings for {self.device.name}" \ No newline at end of file diff --git a/backend/control_center/snmp_monitoring/serializers.py b/backend/control_center/snmp_monitoring/serializers.py new file mode 100644 index 0000000..3628043 --- /dev/null +++ b/backend/control_center/snmp_monitoring/serializers.py @@ -0,0 +1,112 @@ +# File: serializers.py +# Copyright (C) 2025 Taurine Technology +# +# This file is part of the SDN Launch Control project. +# +# This project is licensed under the GNU Affero General Public License v3.0 (AGPL-3.0), +# available at: https://www.gnu.org/licenses/agpl-3.0.en.html#license-text +# +# Contributions to this project are governed by a Contributor License Agreement (CLA). +# By submitting a contribution, contributors grant Taurine Technology exclusive rights to +# the contribution, including the right to relicense it under a different license +# at the copyright owner's discretion. +# +# Unless required by applicable law or agreed to in writing, software distributed +# under this license is provided "AS IS", WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, +# either express or implied. See the GNU Affero General Public License for more details. +# +# For inquiries, contact Keegan White at keeganwhite@taurinetech.com. + +from rest_framework import serializers +from .models import SNMPDevice, SNMPMetrics, SNMPInterfaceStats + + +class SNMPDeviceSerializer(serializers.ModelSerializer): + """ + Serializer for SNMPDevice model. + Used for creating, updating, and listing SNMP devices. + """ + vendor_display = serializers.CharField(source='get_vendor_display', read_only=True) + + class Meta: + model = SNMPDevice + fields = ( + 'id', + 'network_device', + 'name', + 'ip_address', + 'vendor', + 'vendor_display', + 'community_string', + 'snmp_version', + 'port', + 'polling_interval', + 'is_active', + 'last_successful_poll', + 'last_poll_attempt', + 'consecutive_failures', + 'created_at', + 'updated_at', + ) + read_only_fields = ( + 'id', + 'last_successful_poll', + 'last_poll_attempt', + 'consecutive_failures', + 'created_at', + 'updated_at', + ) + + +class SNMPMetricsSerializer(serializers.ModelSerializer): + """ + Serializer for SNMPMetrics model. + Read-only serializer for querying historical metrics. + """ + device_name = serializers.CharField(source='device.name', read_only=True) + device_ip = serializers.CharField(source='device.ip_address', read_only=True) + + class Meta: + model = SNMPMetrics + fields = ( + 'id', + 'device', + 'device_name', + 'device_ip', + 'cpu_usage', + 'memory_usage', + 'disk_usage', + 'uptime_seconds', + 'timestamp', + ) + read_only_fields = fields + + +class SNMPInterfaceStatsSerializer(serializers.ModelSerializer): + """ + Serializer for SNMPInterfaceStats model. + Read-only serializer for querying historical interface statistics. + """ + device_name = serializers.CharField(source='device.name', read_only=True) + device_ip = serializers.CharField(source='device.ip_address', read_only=True) + + class Meta: + model = SNMPInterfaceStats + fields = ( + 'id', + 'device', + 'device_name', + 'device_ip', + 'interface_name', + 'interface_index', + 'bytes_in', + 'bytes_out', + 'packets_in', + 'packets_out', + 'errors_in', + 'errors_out', + 'throughput_mbps', + 'utilization_percent', + 'timestamp', + ) + read_only_fields = fields \ No newline at end of file diff --git a/backend/control_center/snmp_monitoring/tasks.py b/backend/control_center/snmp_monitoring/tasks.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/control_center/snmp_monitoring/tests.py b/backend/control_center/snmp_monitoring/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/backend/control_center/snmp_monitoring/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/backend/control_center/snmp_monitoring/urls.py b/backend/control_center/snmp_monitoring/urls.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/control_center/snmp_monitoring/utilities.py b/backend/control_center/snmp_monitoring/utilities.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/control_center/snmp_monitoring/views.py b/backend/control_center/snmp_monitoring/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/backend/control_center/snmp_monitoring/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here.