mirror of
https://github.com/eduardogsilva/routerfleet.git
synced 2025-07-20 19:04:40 +02:00
Host monitoring
This commit is contained in:
parent
c4bc233bc1
commit
60e1d557aa
10 changed files with 177 additions and 23 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -8,3 +8,4 @@ routerfleet/production_settings.py
|
||||||
.idea/
|
.idea/
|
||||||
db.sqlite3
|
db.sqlite3
|
||||||
backups/
|
backups/
|
||||||
|
containers/*/.venv
|
98
containers/monitoring/monitoring.py
Normal file
98
containers/monitoring/monitoring.py
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
import requests
|
||||||
|
import time
|
||||||
|
from datetime import datetime
|
||||||
|
from subprocess import Popen, PIPE
|
||||||
|
|
||||||
|
|
||||||
|
UPDATE_HOST_LIST_INTERVAL = 600 # How often to update the router list in seconds
|
||||||
|
MONITOR_INTERVAL = 60 # How often to monitor each router in seconds
|
||||||
|
MAX_NOTIFICATIONS_PER_MONITOR_INTERVAL = 50 # Throttle the number of notifications sent to the remote API
|
||||||
|
HOST_LIST_URL = "http://127.0.0.1:8000/monitoring/export_router_list/"
|
||||||
|
UPDATE_STATUS_URL = "http://127.0.0.1:8000/monitoring/update_router_status/"
|
||||||
|
DEBUG = False
|
||||||
|
|
||||||
|
# Global variables
|
||||||
|
host_list = []
|
||||||
|
host_list_update_timestamp = 0
|
||||||
|
notification_count = 0
|
||||||
|
|
||||||
|
|
||||||
|
def get_verbose_status(status):
|
||||||
|
return "online" if status else "offline"
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_host_list():
|
||||||
|
global host_list_update_timestamp
|
||||||
|
try:
|
||||||
|
print(f"{datetime.now()} - Fetching host list...")
|
||||||
|
response = requests.get(HOST_LIST_URL)
|
||||||
|
if response.status_code == 200:
|
||||||
|
host_list_update_timestamp = time.time()
|
||||||
|
return response.json()['router_list'], True
|
||||||
|
else:
|
||||||
|
print(f"{datetime.now()} - Error fetching host list: HTTP {response.status_code}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"{datetime.now()} - Exception fetching host list: {e}")
|
||||||
|
return [], False
|
||||||
|
|
||||||
|
|
||||||
|
def update_host_status(uuid, status):
|
||||||
|
global notification_count
|
||||||
|
if notification_count >= MAX_NOTIFICATIONS_PER_MONITOR_INTERVAL:
|
||||||
|
print(f"{datetime.now()} - Notification limit reached. Skipping Remote API update for {host_list[uuid]['address']}")
|
||||||
|
return # Skip if notification limit is reached
|
||||||
|
try:
|
||||||
|
response = requests.get(f"{UPDATE_STATUS_URL}?uuid={uuid}&status={get_verbose_status(status)}")
|
||||||
|
if response.status_code == 200:
|
||||||
|
print(f"{datetime.now()} - Remote API Status updated for {host_list[uuid]['address']} to {get_verbose_status(status)}")
|
||||||
|
notification_count += 1
|
||||||
|
host_list[uuid]['online'] = status
|
||||||
|
else:
|
||||||
|
print(f"{datetime.now()} - Error updating status for {host_list[uuid]['address']}: HTTP {response.status_code}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"{datetime.now()} - Exception updating status for {host_list[uuid]['address']}: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def check_host_status(host_uuid):
|
||||||
|
command = ["fping", host_list[host_uuid]['address']]
|
||||||
|
process = Popen(command, stdout=PIPE, stderr=PIPE)
|
||||||
|
stdout, stderr = process.communicate()
|
||||||
|
current_online = True if process.returncode == 0 else False
|
||||||
|
if DEBUG:
|
||||||
|
print(f"{datetime.now()} - {host_list[host_uuid]['address']} is {get_verbose_status(current_online)}")
|
||||||
|
if current_online != host_list[host_uuid]['online']:
|
||||||
|
print(f"{datetime.now()} - Status changed for {host_list[host_uuid]['address']} to {get_verbose_status(current_online)}")
|
||||||
|
update_host_status(host_uuid, current_online)
|
||||||
|
|
||||||
|
|
||||||
|
def update_and_monitor():
|
||||||
|
global host_list, host_list_update_timestamp, notification_count
|
||||||
|
while True:
|
||||||
|
current_time = time.time()
|
||||||
|
notification_count = 0
|
||||||
|
|
||||||
|
if current_time - host_list_update_timestamp > UPDATE_HOST_LIST_INTERVAL:
|
||||||
|
new_host_list, fetch_host_list_success = fetch_host_list()
|
||||||
|
if fetch_host_list_success:
|
||||||
|
host_list = new_host_list
|
||||||
|
print(f"{datetime.now()} - host list updated.")
|
||||||
|
if DEBUG:
|
||||||
|
print(host_list)
|
||||||
|
|
||||||
|
if host_list:
|
||||||
|
if DEBUG:
|
||||||
|
print(f"{datetime.now()} - Monitoring host... Interval between each monitor: {MONITOR_INTERVAL / len(host_list)} seconds")
|
||||||
|
for host_uuid in host_list:
|
||||||
|
if DEBUG:
|
||||||
|
print(host_list[host_uuid])
|
||||||
|
check_host_status(host_uuid)
|
||||||
|
time.sleep(MONITOR_INTERVAL / len(host_list))
|
||||||
|
else:
|
||||||
|
print(f"{datetime.now()} - No host to monitor.")
|
||||||
|
time.sleep(MONITOR_INTERVAL)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
update_and_monitor()
|
||||||
|
|
||||||
|
|
5
containers/monitoring/requirements.txt
Normal file
5
containers/monitoring/requirements.txt
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
certifi==2024.2.2
|
||||||
|
charset-normalizer==3.3.2
|
||||||
|
idna==3.6
|
||||||
|
requests==2.31.0
|
||||||
|
urllib3==2.2.1
|
|
@ -1,3 +1,31 @@
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
|
from router_manager.models import Router
|
||||||
|
from django.http import JsonResponse
|
||||||
|
|
||||||
# Create your views here.
|
|
||||||
|
def view_export_router_list(request):
|
||||||
|
router_list = {}
|
||||||
|
for router in Router.objects.filter(enabled=True, monitoring=True):
|
||||||
|
router_list[str(router.uuid)] = {
|
||||||
|
'address': router.address,
|
||||||
|
'name': router.name,
|
||||||
|
'online': router.routerstatus.status_online,
|
||||||
|
'uuid': str(router.uuid),
|
||||||
|
}
|
||||||
|
data = {
|
||||||
|
'router_list': router_list
|
||||||
|
}
|
||||||
|
return JsonResponse(data)
|
||||||
|
|
||||||
|
|
||||||
|
def view_update_router_status(request):
|
||||||
|
router = Router.objects.get(uuid=request.GET.get('uuid'))
|
||||||
|
new_status = request.GET.get('status')
|
||||||
|
if new_status not in ['online', 'offline']:
|
||||||
|
return JsonResponse({'status': 'error', 'error_message': 'Invalid status'}, status=400)
|
||||||
|
if new_status == 'online':
|
||||||
|
router.routerstatus.status_online = True
|
||||||
|
else:
|
||||||
|
router.routerstatus.status_online = False
|
||||||
|
router.routerstatus.save()
|
||||||
|
return JsonResponse({'status': 'success'})
|
|
@ -1,14 +1,19 @@
|
||||||
asgiref==3.7.2
|
asgiref==3.7.2
|
||||||
bcrypt==4.1.2
|
bcrypt==4.1.2
|
||||||
|
certifi==2024.2.2
|
||||||
cffi==1.16.0
|
cffi==1.16.0
|
||||||
|
charset-normalizer==3.3.2
|
||||||
crispy-bootstrap4==2024.1
|
crispy-bootstrap4==2024.1
|
||||||
crispy-bootstrap5==2024.2
|
crispy-bootstrap5==2024.2
|
||||||
cryptography==42.0.5
|
cryptography==42.0.5
|
||||||
Django==5.0.3
|
Django==5.0.3
|
||||||
django-crispy-forms==2.1
|
django-crispy-forms==2.1
|
||||||
|
idna==3.6
|
||||||
paramiko==3.4.0
|
paramiko==3.4.0
|
||||||
pycparser==2.21
|
pycparser==2.21
|
||||||
PyNaCl==1.5.0
|
PyNaCl==1.5.0
|
||||||
|
requests==2.31.0
|
||||||
scp==0.14.5
|
scp==0.14.5
|
||||||
sqlparse==0.4.4
|
sqlparse==0.4.4
|
||||||
typing_extensions==4.10.0
|
typing_extensions==4.10.0
|
||||||
|
urllib3==2.2.1
|
||||||
|
|
|
@ -54,24 +54,16 @@ class RouterForm(forms.ModelForm):
|
||||||
cleaned_data = super().clean()
|
cleaned_data = super().clean()
|
||||||
name = cleaned_data.get('name')
|
name = cleaned_data.get('name')
|
||||||
ssh_key = cleaned_data.get('ssh_key')
|
ssh_key = cleaned_data.get('ssh_key')
|
||||||
|
username = cleaned_data.get('username')
|
||||||
password = cleaned_data.get('password')
|
password = cleaned_data.get('password')
|
||||||
address = cleaned_data.get('address')
|
address = cleaned_data.get('address')
|
||||||
|
router_type = cleaned_data.get('router_type')
|
||||||
|
backup_profile = cleaned_data.get('backup_profile')
|
||||||
|
|
||||||
if name:
|
if name:
|
||||||
name = name.strip()
|
name = name.strip()
|
||||||
cleaned_data['name'] = name
|
cleaned_data['name'] = name
|
||||||
|
|
||||||
if ssh_key and password:
|
|
||||||
raise forms.ValidationError('You must provide a password or an SSH Key, not both')
|
|
||||||
if not ssh_key and not password and not self.instance.password:
|
|
||||||
raise forms.ValidationError('You must provide a password or an SSH Key')
|
|
||||||
|
|
||||||
if not password and self.instance.password:
|
|
||||||
cleaned_data['password'] = self.instance.password
|
|
||||||
|
|
||||||
if ssh_key and not password:
|
|
||||||
cleaned_data['password'] = ''
|
|
||||||
|
|
||||||
if address:
|
if address:
|
||||||
address = address.lower()
|
address = address.lower()
|
||||||
cleaned_data['address'] = address
|
cleaned_data['address'] = address
|
||||||
|
@ -83,9 +75,27 @@ class RouterForm(forms.ModelForm):
|
||||||
ipaddress.ip_address(address)
|
ipaddress.ip_address(address)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise forms.ValidationError('The address field must be a valid hostname or IP address.')
|
raise forms.ValidationError('The address field must be a valid hostname or IP address.')
|
||||||
|
|
||||||
|
if router_type == 'monitoring':
|
||||||
|
cleaned_data['password'] = ''
|
||||||
|
cleaned_data['ssh_key'] = None
|
||||||
|
if backup_profile:
|
||||||
|
raise forms.ValidationError('Monitoring only routers cannot have a backup profile')
|
||||||
|
return cleaned_data
|
||||||
|
|
||||||
|
if ssh_key and password:
|
||||||
|
raise forms.ValidationError('You must provide a password or an SSH Key, not both')
|
||||||
|
if not ssh_key and not password and not self.instance.password:
|
||||||
|
raise forms.ValidationError('You must provide a password or an SSH Key')
|
||||||
|
|
||||||
|
if not password and self.instance.password:
|
||||||
|
cleaned_data['password'] = self.instance.password
|
||||||
|
|
||||||
|
if ssh_key and not password:
|
||||||
|
cleaned_data['password'] = ''
|
||||||
|
|
||||||
test_authentication_success, test_authentication_message = test_authentication(
|
test_authentication_success, test_authentication_message = test_authentication(
|
||||||
cleaned_data['router_type'], cleaned_data['address'], cleaned_data['username'], cleaned_data['password'],
|
router_type, cleaned_data['address'], username, cleaned_data['password'], ssh_key
|
||||||
cleaned_data['ssh_key']
|
|
||||||
)
|
)
|
||||||
if not test_authentication_success:
|
if not test_authentication_success:
|
||||||
if test_authentication_message:
|
if test_authentication_message:
|
||||||
|
|
|
@ -26,7 +26,7 @@ class Router(models.Model):
|
||||||
monitoring = models.BooleanField(default=True)
|
monitoring = models.BooleanField(default=True)
|
||||||
backup_profile = models.ForeignKey(BackupProfile, on_delete=models.SET_NULL, null=True, blank=True)
|
backup_profile = models.ForeignKey(BackupProfile, on_delete=models.SET_NULL, null=True, blank=True)
|
||||||
|
|
||||||
router_type = models.CharField(max_length=100, choices=(('routeros', 'Mikrotik (RouterOS)'), ('openwrt', 'OpenWRT')))
|
router_type = models.CharField(max_length=100, choices=(('monitoring', 'Monitoring Only'), ('routeros', 'Mikrotik (RouterOS)'), ('openwrt', 'OpenWRT')))
|
||||||
enabled = models.BooleanField(default=True)
|
enabled = models.BooleanField(default=True)
|
||||||
|
|
||||||
updated = models.DateTimeField(auto_now=True)
|
updated = models.DateTimeField(auto_now=True)
|
||||||
|
|
|
@ -58,6 +58,7 @@ def view_manage_router(request):
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
form.save()
|
form.save()
|
||||||
messages.success(request, 'Router saved successfully')
|
messages.success(request, 'Router saved successfully')
|
||||||
|
router_status, router_status_created = RouterStatus.objects.get_or_create(router=form.instance)
|
||||||
return redirect('router_list')
|
return redirect('router_list')
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
|
|
|
@ -5,6 +5,7 @@ from user_manager.views import view_manage_user, view_user_list
|
||||||
from accounts.views import view_login, view_logout, view_create_first_user
|
from accounts.views import view_login, view_logout, view_create_first_user
|
||||||
from router_manager.views import view_router_list, view_manage_router, view_router_group_list, view_ssh_key_list, view_manage_router_group, view_manage_sshkey, view_router_details
|
from router_manager.views import view_router_list, view_manage_router, view_router_group_list, view_ssh_key_list, view_manage_router_group, view_manage_sshkey, view_router_details
|
||||||
from backup.views import view_backup_profile_list, view_manage_backup_profile, view_backup_list, view_backup_details, view_debug_run_backups, view_compare_backups
|
from backup.views import view_backup_profile_list, view_manage_backup_profile, view_backup_list, view_backup_details, view_debug_run_backups, view_compare_backups
|
||||||
|
from monitoring.views import view_export_router_list, view_update_router_status
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
@ -29,5 +30,6 @@ urlpatterns = [
|
||||||
path('backup/backup_list/', view_backup_list, name='backup_list'),
|
path('backup/backup_list/', view_backup_list, name='backup_list'),
|
||||||
path('backup/backup_details/', view_backup_details, name='backup_info'),
|
path('backup/backup_details/', view_backup_details, name='backup_info'),
|
||||||
path('backup/compare/', view_compare_backups, name='compare_backups'),
|
path('backup/compare/', view_compare_backups, name='compare_backups'),
|
||||||
|
path('monitoring/export_router_list/', view_export_router_list, name='export_router_list'),
|
||||||
|
path('monitoring/update_router_status/', view_update_router_status, name='update_router_status'),
|
||||||
]
|
]
|
||||||
|
|
|
@ -49,23 +49,27 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
{% if router.router_type != 'monitoring' %}
|
||||||
{% if router.backup_profile %}
|
{% if router.backup_profile %}
|
||||||
{{ router.backup_profile }} {% if router.routerstatus.last_backup_failed %}<i class="fas fa-exclamation-triangle text-danger" title="Last backup failed to complete"></i>{% endif %}
|
{{ router.backup_profile }} {% if router.routerstatus.last_backup_failed %}<i class="fas fa-exclamation-triangle text-danger" title="Last backup failed to complete"></i>{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<i class="fas fa-exclamation-triangle text-warning" title="No backup profile selected"></i>
|
<i class="fas fa-exclamation-triangle text-warning" title="No backup profile selected"></i>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
{{ router.routergroup_set.count }}
|
{{ router.routergroup_set.count }}
|
||||||
</td>
|
</td>
|
||||||
<td class="min-width">
|
<td class="min-width">
|
||||||
{% if router.ssh_key %}
|
{% if router.router_type != 'monitoring' %}
|
||||||
<i class="fas fa-key" title="SSH Key: {{ router.ssh_key }}"></i>
|
{% if router.ssh_key %}
|
||||||
{% elif router.password %}
|
<i class="fas fa-key" title="SSH Key: {{ router.ssh_key }}"></i>
|
||||||
<i class="fas fa-keyboard" title="Password Authentication"></i>
|
{% elif router.password %}
|
||||||
{% else %}
|
<i class="fas fa-keyboard" title="Password Authentication"></i>
|
||||||
<i class="fas fa-exclamation-triangle text-warning" title="Missing authentication"></i>
|
{% else %}
|
||||||
|
<i class="fas fa-exclamation-triangle text-warning" title="Missing authentication"></i>
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td class="min-width">
|
<td class="min-width">
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue