diff --git a/sdbs_infra/dashboard/admin.py b/sdbs_infra/dashboard/admin.py index ad6c26d..1921c42 100644 --- a/sdbs_infra/dashboard/admin.py +++ b/sdbs_infra/dashboard/admin.py @@ -1,7 +1,7 @@ from django.contrib import admin from ordered_model.admin import OrderedModelAdmin -from sdbs_infra.dashboard.models import Service, Link +from sdbs_infra.dashboard.models import Service, Link, Machine class LinkAdmin(OrderedModelAdmin): @@ -12,5 +12,10 @@ class ServiceAdmin(OrderedModelAdmin): list_display = ('short_name', 'url', 'move_up_down_links') +class MachineAdmin(OrderedModelAdmin): + list_display = ('short_name', 'move_up_down_links') + + admin.site.register(Link, LinkAdmin) admin.site.register(Service, ServiceAdmin) +admin.site.register(Machine, MachineAdmin) diff --git a/sdbs_infra/dashboard/migrations/0009_machine_squashed_0012_auto_20200715_0350.py b/sdbs_infra/dashboard/migrations/0009_machine_squashed_0012_auto_20200715_0350.py new file mode 100644 index 0000000..4fc815e --- /dev/null +++ b/sdbs_infra/dashboard/migrations/0009_machine_squashed_0012_auto_20200715_0350.py @@ -0,0 +1,31 @@ +# Generated by Django 3.0.7 on 2020-07-15 01:51 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + replaces = [('dashboard', '0009_machine'), ('dashboard', '0010_auto_20200715_0344'), ('dashboard', '0011_machine_url'), ('dashboard', '0012_auto_20200715_0350')] + + dependencies = [ + ('dashboard', '0008_auto_20200620_1952'), + ] + + operations = [ + migrations.CreateModel( + name='Machine', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('order', models.PositiveIntegerField(db_index=True, editable=False, verbose_name='order')), + ('short_name', models.CharField(max_length=64)), + ('image', models.ImageField(blank=True, null=True, upload_to='machines')), + ('description', models.TextField(blank=True, null=True)), + ('healthcheck_id', models.CharField(blank=True, max_length=36, null=True)), + ('url', models.URLField(blank=True, null=True)), + ], + options={ + 'ordering': ('order',), + 'abstract': False, + }, + ), + ] diff --git a/sdbs_infra/dashboard/models.py b/sdbs_infra/dashboard/models.py index c597000..179ddf5 100644 --- a/sdbs_infra/dashboard/models.py +++ b/sdbs_infra/dashboard/models.py @@ -4,7 +4,7 @@ from django.db import models from ordered_model.models import OrderedModel -class ServiceStatus(Enum): +class Status(Enum): OK = 'ok' DOWN = 'down' UNKNOWN = 'unknown' @@ -33,3 +33,14 @@ class Link(OrderedModel): class Meta(OrderedModel.Meta): verbose_name = "Important Link" verbose_name_plural = "Important Links" + + +class Machine(OrderedModel): + short_name = models.CharField(null=False, max_length=64) + image = models.ImageField(null=True, blank=True, upload_to='machines') + description = models.TextField(null=True, blank=True) + url = models.URLField(null=True, blank=True) + healthcheck_id = models.CharField(null=True, blank=True, max_length=36) + + def __str__(self): + return f"{self.short_name}" diff --git a/sdbs_infra/dashboard/static/main.css b/sdbs_infra/dashboard/static/main.css index 9f52464..893716e 100644 --- a/sdbs_infra/dashboard/static/main.css +++ b/sdbs_infra/dashboard/static/main.css @@ -70,7 +70,9 @@ h2 { padding: 2rem; display: flex; + flex-direction: column; align-items: center; + justify-content: flex-end; } .link .box-content { @@ -78,18 +80,13 @@ h2 { justify-content: space-evenly; } -.service .box-content { - flex-direction: column; - justify-content: flex-end; -} - .box-content img { filter: grayscale(100%); image-rendering: crisp-edges; text-align: center; } -.service img { +.machine img, .service img { min-width: 50%; max-width: 100%; } @@ -99,7 +96,7 @@ h2 { max-height: 4rem; } -.service .label h3 { +.label h3 { margin: 1rem 0; text-align: center; } @@ -118,7 +115,7 @@ h2 { align-self: flex-start; } -.service-status { +.status { border: 1px solid white; border-top: 0; text-transform: uppercase; @@ -126,15 +123,15 @@ h2 { padding: .25em .5em; } -.status-ok .service-status { +.status-ok .status { color: gray; } -.status-down .service-status { +.status-down .status { color: darkred; } -.status-unknown .service-status { +.status-unknown .status { color: darkorange; } diff --git a/sdbs_infra/dashboard/templates/index.html b/sdbs_infra/dashboard/templates/index.html index 00a3d70..dd3c87d 100644 --- a/sdbs_infra/dashboard/templates/index.html +++ b/sdbs_infra/dashboard/templates/index.html @@ -44,12 +44,33 @@ {% endif %} -
+
STATUS: {{ service.status }}
{% endfor %}
+

machines

+
+ {% for machine in machines %} + +
+ {% if machine.image_url %} + image for {{ machine.short_name }} + {% endif %} +
+

{{ machine.short_name }}

+ {% if machine.description %} +

{{ machine.description }}

+ {% endif %} +
+
+
+ STATUS: {{ machine.status }} +
+
+ {% endfor %} +
VPS STATS — {{ vps_stats|safe }}
diff --git a/sdbs_infra/dashboard/views.py b/sdbs_infra/dashboard/views.py index 907b9ea..a918624 100644 --- a/sdbs_infra/dashboard/views.py +++ b/sdbs_infra/dashboard/views.py @@ -2,6 +2,7 @@ import asyncio import socket import time from collections import namedtuple +from datetime import datetime from urllib.parse import urlparse import aiohttp @@ -11,7 +12,8 @@ from bs4 import BeautifulSoup from django.views.generic import TemplateView from humanize import naturalsize -from sdbs_infra.dashboard.models import Service, ServiceStatus, Link +from sdbs_infra import settings +from sdbs_infra.dashboard.models import Service, Status, Link, Machine class IndexView(TemplateView): @@ -22,6 +24,7 @@ class IndexView(TemplateView): return { 'links': asyncio.run(self.process_links(list(Link.objects.all()))), 'services': asyncio.run(self.process_services(list(Service.objects.all()))), + 'machines': asyncio.run(self.process_machines(list(Machine.objects.all()))), 'vps_stats': self.vps_stats() } @@ -68,13 +71,13 @@ class IndexView(TemplateView): location = ("localhost", service.port) result_of_check = a_socket.connect_ex(location) if result_of_check == 0: - status = ServiceStatus.OK + status = Status.OK else: - status = ServiceStatus.DOWN + status = Status.DOWN elif index_status: - status = ServiceStatus.OK if index_status == 200 else ServiceStatus.DOWN + status = Status.OK if index_status == 200 else Status.DOWN else: - status = ServiceStatus.UNKNOWN + status = Status.UNKNOWN image = service.image.url if service.image else self.extract_favicon(service.url, index_text) @@ -87,6 +90,38 @@ class IndexView(TemplateView): await session.close() return result + async def process_machines(self, machines): + result = [] + + session = aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=5, sock_connect=1), headers={ + 'X-Api-Key': settings.HEALTCHECKS_API_KEY + }) + + for machine in machines: + status = Status.UNKNOWN + last_ping = None + + if settings.HEALTCHECKS_API_KEY and machine.healthcheck_id: + try: + async with session.get( + f"https://healthchecks.io/api/v1/checks/{machine.healthcheck_id}") as response: + check = await response.json() + status = { + 'up': Status.OK, + 'down': Status.DOWN + }.get(check.get('status'), Status.UNKNOWN) + last_ping = datetime.fromisoformat(check.get('last_ping')) + except (asyncio.TimeoutError, ClientConnectorError): + pass + + result.append({ + 'status': status.value, + 'last_ping': last_ping, + **vars(machine) + }) + + return result + @staticmethod def extract_favicon(url, index_text): if not index_text: diff --git a/sdbs_infra/settings.py b/sdbs_infra/settings.py index 0a25342..d0f742c 100644 --- a/sdbs_infra/settings.py +++ b/sdbs_infra/settings.py @@ -128,3 +128,5 @@ STATIC_ROOT = os.path.join(BASE_DIR, "static") MEDIA_URL = '/media/' MEDIA_ROOT = os.path.join(BASE_DIR, "media") + +HEALTCHECKS_API_KEY = os.getenv("HEALTHCHECKS_API_KEY")