-
Notifications
You must be signed in to change notification settings - Fork 2
Add SNMP polling tasks and monitoring API endpoints #53
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| from django.contrib import admin | ||
|
|
||
| # Register your models here. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| from django.apps import AppConfig | ||
|
|
||
|
|
||
| class SnmpMonitoringConfig(AppConfig): | ||
| default_auto_field = 'django.db.models.BigAutoField' | ||
| name = 'snmp_monitoring' |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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'), | ||
| ), | ||
| ] | ||
|
Comment on lines
+1
to
+123
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Migration structure is correct; consider excluding from line-length linting. The migration is auto-generated by Django and correctly creates all models with appropriate fields, indexes, and constraints. The schema aligns with Pipeline is failing due to multiple E501 (line too long) errors. However, reformatting auto-generated migrations can cause issues with Django's migration auto-detection. Consider:
# In .flake8 or setup.cfg
[flake8]
per-file-ignores =
*/migrations/*:E501🧰 Tools🪛 GitHub Actions: Lint SNMP Monitoring[error] 22-22: flake8 E501 line too long (340 > 120 characters). [error] 23-23: flake8 E501 line too long (140 > 120 characters). [error] 24-24: flake8 E501 line too long (167 > 120 characters). [error] 26-26: flake8 E501 line too long (125 > 120 characters). [error] 27-27: flake8 E501 line too long (130 > 120 characters). [error] 28-28: flake8 E501 line too long (138 > 120 characters). [error] 29-29: flake8 E501 line too long (147 > 120 characters). [error] 30-30: flake8 E501 line too long (125 > 120 characters). [error] 33-33: flake8 E501 line too long (241 > 120 characters). [error] 48-48: flake8 E501 line too long (202 > 120 characters). [error] 55-55: flake8 E501 line too long (136 > 120 characters). [error] 56-56: flake8 E501 line too long (124 > 120 characters). [error] 63-63: flake8 E501 line too long (136 > 120 characters). [error] 64-64: flake8 E501 line too long (150 > 120 characters). [error] 65-65: flake8 E501 line too long (140 > 120 characters). [error] 66-66: flake8 E501 line too long (207 > 120 characters). [error] 80-80: flake8 E501 line too long (135 > 120 characters). [error] 81-81: flake8 E501 line too long (196 > 120 characters). 🪛 Ruff (0.14.8)11-13: Mutable class attributes should be annotated with (RUF012) 15-123: Mutable class attributes should be annotated with (RUF012) 🤖 Prompt for AI Agents |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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); | ||
| """, | ||
| ), | ||
| ] | ||
|
|
||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove trailing blank line to fix linting warning. The pipeline reports a blank line at end of file (W391). 🔎 Proposed fixRemove the empty line at line 43. 🧰 Tools🪛 GitHub Actions: Lint SNMP Monitoring[warning] 43-43: flake8 W391 blank line at end of file. 🤖 Prompt for AI Agents |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 $$; | ||
| """ | ||
|
Comment on lines
+36
to
+47
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The TimescaleDB drops hypertables using standard PostgreSQL DROP TABLE commands. The reverse SQL uses Either:
🤖 Prompt for AI Agents |
||
| ), | ||
| # 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 $$; | ||
| """ | ||
| ), | ||
| ] | ||
|
|
||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove trailing blank line. Pipeline failure indicates a blank line at end of file (flake8 W391). 🧰 Tools🪛 GitHub Actions: Lint SNMP Monitoring[warning] 78-78: flake8 W391 blank line at end of file. 🤖 Prompt for AI Agents |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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); | ||
| """ | ||
| ), | ||
|
Comment on lines
+13
to
+69
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Compression strategy looks good; fix whitespace issues to pass linting. The compression configuration is well-designed:
However, the pipeline is failing due to whitespace issues on lines 27, 41, 55 (blank lines containing whitespace) and line 71 (trailing blank line). Remove the whitespace from these blank lines. 🧰 Tools🪛 GitHub Actions: Lint SNMP Monitoring[warning] 27-27: flake8 W293 blank line contains whitespace. [warning] 41-41: flake8 W293 blank line contains whitespace. [warning] 55-55: flake8 W293 blank line contains whitespace. 🪛 Ruff (0.14.8)13-70: Mutable class attributes should be annotated with (RUF012) 🤖 Prompt for AI Agents |
||
| ] | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove unused import or register the SNMP models.
The pipeline is failing due to the unused
adminimport. Consider either removing the import or registering the SNMP models for admin access.🔎 Option 1: Remove unused import
🔎 Option 2: Register models
Would you like me to generate a complete admin registration for all SNMP models?
📝 Committable suggestion
🧰 Tools
🪛 GitHub Actions: Lint SNMP Monitoring
[error] 1-1: flake8 F401: 'django.contrib.admin' imported but unused.
🤖 Prompt for AI Agents