Browse Source

ip resources states history

Élie Bouttier 6 years ago
parent
commit
709099f78c

+ 51 - 28
services/admin.py

@@ -26,9 +26,9 @@ from datetime import timedelta
 from djadhere.utils import get_active_filter
 from adhesions.models import Adhesion
 from banking.models import PaymentUpdate
-from .models import Service, ServiceType, IPPrefix, IPResource, Route, Tunnel, \
+from .models import Service, ServiceType, IPPrefix, IPResource, IPResourceState, \
                     ServiceAllocation, Antenna, AntennaAllocation, Allocation, \
-                    Switch, Port
+                    Route, Tunnel, Switch, Port
 from .utils.notifications import notify_allocation
 from .forms import AntennaForm
 
@@ -67,13 +67,15 @@ class ResourcePingFilter(admin.SimpleListFilter):
 
     def queryset(self, request, queryset):
         if self.value() == 'up':
-            return queryset.filter(up=True)
+            return queryset.filter(last_state__state=IPResourceState.STATE_UP)
         if self.value() == 'down':
-            return queryset.filter(models.Q(up__isnull=True) | models.Q(up=False))
+            return queryset.exclude(last_state__state=IPResourceState.STATE_UP) # DOWN + UNKNOWN
         if self.value() == 'down-since':
-            return queryset.filter(up=False)
+            queryset = queryset.exclude(last_state__state=IPResourceState.STATE_UP)
+            return queryset.filter(last_time_up__isnull=False)
         if self.value() == 'never-up':
-            return queryset.filter(up__isnull=True)
+            queryset = queryset.exclude(last_state__state=IPResourceState.STATE_UP) # DOWN + UNKWON
+            return queryset.filter(last_time_up__isnull=True)
 
 
 class ActiveServiceFilter(admin.SimpleListFilter):
@@ -265,6 +267,22 @@ class InactiveAntennaAllocationInline(AntennaAllocationMixin, InactiveAllocation
     pass
 
 
+class IPResourceStateInline(admin.TabularInline):
+    model = IPResourceState
+    verbose_name_plural = 'Historique des changements d’état'
+    fields = ['date']
+    ordering = ['-date']
+
+    def has_add_permission(self, request):
+        return False
+
+    def has_change_permission(self, request, obj):
+        return False
+
+    def has_delete_permission(self, request, obj=None):
+        return False
+
+
 class PortInline(admin.TabularInline):
     model = Port
     max_num = 0
@@ -385,27 +403,32 @@ class IPResourceAdmin(admin.ModelAdmin):
     )
     search_fields = ('=ip', 'notes',)
     actions = ['contact_ip_owners']
+    ordering = ['ip']
+    inlines = [ IPResourceStateInline ]
 
     def get_fields(self, request, obj=None):
         return self.get_readonly_fields(request, obj)
 
     def get_readonly_fields(self, request, obj=None):
         fields = ['ip']
-        if obj and obj.reserved:
-            fields += ['reserved']
-        if obj and not obj.in_use:
-            fields += ['last_use']
-        if obj and obj.last_time_up and obj.last_check:
-            fields += ['last_time_up', 'last_check']
-        if obj.category == IPResource.CATEGORY_PUBLIC:
-            fields += ['password']
-        if obj and obj.checkmk_label:
-            fields += ['checkmk']
-        if obj and obj.notes:
-            fields += ['notes']
+        if obj:
+            if obj.reserved:
+                fields += ['reserved']
+            if not obj.in_use:
+                fields += ['last_use']
+            fields += ['last_state']
+            if obj.last_state.state != IPResourceState.STATE_UP:
+                fields += ['last_time_up']
+            if obj.category == IPResource.CATEGORY_PUBLIC:
+                fields += ['password']
+            if obj.checkmk_label:
+                fields += ['checkmk']
+            if obj.notes:
+                fields += ['notes']
         return fields
 
     def get_inline_instances(self, request, obj=None):
+        super_inlines = super().get_inline_instances(request, obj)
         if obj:
             if obj.category == 0:
                 inlines = (ActiveServiceAllocationInline, InactiveServiceAllocationInline,)
@@ -413,7 +436,7 @@ class IPResourceAdmin(admin.ModelAdmin):
                 inlines = (ActiveAntennaAllocationInline, InactiveAntennaAllocationInline,)
         else:
             inlines = ()
-        return [inline(self.model, self.admin_site) for inline in inlines]
+        return [inline(self.model, self.admin_site) for inline in inlines] + super_inlines
 
     def get_queryset(self, request):
         qs = super().get_queryset(request)
@@ -426,13 +449,12 @@ class IPResourceAdmin(admin.ModelAdmin):
                     default=None,
                 ))
         qs = qs.annotate(
-                up=models.Case(
-                    models.When(last_check__isnull=False, last_time_up__isnull=False, last_time_up=models.F('last_check'), then=True),
-                    models.When(last_check__isnull=False, last_time_up__isnull=False, then=False),
+                downtime=models.Case(
+                    models.When(last_state__state=IPResourceState.STATE_UP, then=models.F('last_state__date')-models.Value(now)),
+                    models.When(last_state__state=IPResourceState.STATE_DOWN, then=models.Value(now)-models.F('last_time_up')),
                     default=None,
-                    output_field=models.NullBooleanField(),
+                    output_field=models.DurationField(),
                 ))
-        qs = qs.annotate(downtime=models.F('last_check') - models.F('last_time_up'))
         qs = qs.annotate(
                 route=models.Case(
                     models.When(
@@ -461,12 +483,13 @@ class IPResourceAdmin(admin.ModelAdmin):
     last_use.admin_order_field = 'last_use'
 
     def ping(self, obj):
-        if obj.up:
+        if obj.last_state.state == IPResourceState.STATE_UP:
             label = 'UP'
-        elif obj.last_time_up:
-            label = 'dernier ping : ' + naturaltime(obj.last_time_up)
         else:
-            label = 'DOWN'
+            if obj.last_time_up:
+                label = 'dernier ping : ' + naturaltime(obj.last_time_up)
+            else:
+                label = 'DOWN'
         if obj.checkmk_url:
             return format_html('<a href="{}">{}</a>', obj.checkmk_url, label)
         else:

+ 28 - 0
services/migrations/0052_auto_20190219_2146.py

@@ -0,0 +1,28 @@
+# Generated by Django 2.1.1 on 2019-02-19 20:46
+
+from django.db import migrations, models
+from django.utils import timezone
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('services', '0051_auto_20180602_1346'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='IPResourceState',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('date', models.DateTimeField(default=timezone.now)),
+                ('state', models.IntegerField(choices=[(0, 'DOWN'), (1, 'UP'), (2, 'Inconnu')])),
+                ('ip', models.ForeignKey(on_delete=models.deletion.CASCADE, related_name='state_set', to='services.IPResource')),
+            ],
+        ),
+        migrations.AddField(
+            model_name='ipresource',
+            name='last_state',
+            field=models.ForeignKey(null=True, on_delete=models.deletion.PROTECT, to='services.IPResourceState', related_name='+'),
+        ),
+    ]

+ 51 - 0
services/migrations/0053_auto_20190219_2147.py

@@ -0,0 +1,51 @@
+# Generated by Django 2.1.1 on 2019-02-19 20:47
+
+from django.db import migrations
+from django.utils import timezone
+
+from datetime import timedelta
+
+
+def forward(apps, schema_editor):
+    db_alias = schema_editor.connection.alias
+    IPResource = apps.get_model('services', 'IPResource')
+    IPResourceState = apps.get_model('services', 'IPResourceState')
+    now = timezone.now()
+    for ip in IPResource.objects.all():
+        if ip.last_check:
+            if ip.last_time_up:
+                if ip.last_check == ip.last_time_up: # UP
+                    ip.last_state = IPResourceState.objects.create(ip=ip, date=ip.last_time_up, state=1) # UP
+                    ip.save()
+                else: # DOWN but UP some time before
+                    ip.last_state = IPResourceState.objects.create(ip=ip, date=ip.last_time_up, state=0) # DOWN
+                    ip.save()
+            else: # Always DOWN
+                ip.last_state = IPResourceState.objects.create(ip=ip, date=ip.last_check, state=0) # DOWN
+                ip.save()
+        else:
+            ip.last_state = IPResourceState.objects.create(ip=ip, date=now, state=2) # UNKNOWN
+            ip.save()
+
+
+def backward(apps, schema_editor):
+    db_alias = schema_editor.connection.alias
+    IPResource = apps.get_model('services', 'IPResource')
+    IPResourceState = apps.get_model('services', 'IPResourceState')
+    for ip in IPResource.objects.all():
+        if ip.last_state.state != 2:
+            ip.last_check = ip.last_state.date
+            if ip.last_state.state == 1:
+                ip.last_time_up = ip.last_check
+            ip.save()
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('services', '0052_auto_20190219_2146'),
+    ]
+
+    operations = [
+		migrations.RunPython(forward, backward)
+    ]

+ 22 - 0
services/migrations/0054_auto_20190219_2206.py

@@ -0,0 +1,22 @@
+# Generated by Django 2.1.1 on 2019-02-19 21:06
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('services', '0053_auto_20190219_2147'),
+    ]
+
+    operations = [
+        migrations.RemoveField(
+            model_name='ipresource',
+            name='last_check',
+        ),
+        migrations.AlterField(
+            model_name='ipresource',
+            name='last_state',
+            field=models.ForeignKey(on_delete=models.deletion.PROTECT, to='services.IPResourceState', related_name='+'),
+        ),
+    ]

+ 18 - 1
services/models.py

@@ -102,8 +102,8 @@ class IPResource(models.Model):
     category = models.IntegerField(choices=CATEGORIES, verbose_name='catégorie')
     notes = models.TextField(blank=True, default='')
     checkmk_label = models.CharField(max_length=128, blank=True, default='')
+    last_state = models.ForeignKey("IPResourceState", on_delete=models.PROTECT, related_name='+', verbose_name='dernier état')
     last_time_up = models.DateTimeField(null=True, blank=True, verbose_name='Dernière réponse au ping')
-    last_check = models.DateTimeField(null=True, blank=True, verbose_name='Dernier contrôle')
 
     objects = IPResourceManager()
 
@@ -135,6 +135,23 @@ class IPResource(models.Model):
         return str(self.ip)
 
 
+class IPResourceState(models.Model):
+    STATE_DOWN = 0
+    STATE_UP = 1
+    STATE_UNKNOWN = 2
+    STATE_CHOICES = (
+        (STATE_DOWN, 'DOWN'),
+        (STATE_UP, 'UP'),
+        (STATE_UNKNOWN, 'Inconnu'),
+    )
+    ip = models.ForeignKey(IPResource, on_delete=models.CASCADE, related_name='state_set')
+    date = models.DateTimeField(default=timezone.now)
+    state = models.IntegerField(choices=STATE_CHOICES)
+
+    def __str__(self):
+        return self.get_state_display()
+
+
 class ServiceType(models.Model):
     name = models.CharField(max_length=64, verbose_name='Nom', unique=True)
     contact = models.CharField(max_length=64, verbose_name='Contact en cas de problème', blank=True, default='')

+ 40 - 18
services/utils/fastpinger.py

@@ -1,18 +1,22 @@
 from django.db.models import Q, F
 from django.utils import timezone
 
-from services.models import IPResource
+from services.models import IPResource, IPResourceState, ServiceAllocation
 
 import re
 from ipaddress import IPv4Address, AddressValueError
+import logging
+from itertools import groupby
+
+
+logger = logging.getLogger(__name__)
 
 
 def fastpinger_update(f):
+    now = timezone.now()
+
     regex = re.compile('^(?P<ip>[0-9.]+)[ ]+: (?P<p1>([0-9]+.[0-9]+|-)) (?P<p2>([0-9]+.[0-9]+|-))'
                        ' (?P<p3>([0-9]+.[0-9]+|-)) (?P<p4>([0-9]+.[0-9]+|-)) (?P<p5>([0-9]+.[0-9]+|-))$')
-    up_count = IPResource.objects.filter(last_check__isnull=False).filter(last_time_up=F('last_check')).count()
-    down_count = IPResource.objects.filter(last_check__isnull=False).exclude(last_time_up=F('last_check')).count()
-    unknown_count = IPResource.objects.filter(last_check__isnull=True).count()
 
     up, down, unknown = set(), set(), set()
     for line in f:
@@ -35,21 +39,39 @@ def fastpinger_update(f):
 
     up = up - down # suppression des doublons
 
-    up = IPResource.objects.filter(ip__in=up)
-    down = IPResource.objects.filter(ip__in=down)
-    unknown = IPResource.objects.exclude(Q(pk__in=up) | Q(pk__in=down))
-
-    up.exclude(last_time_up=F('last_check')).count()
-    down.filter(last_time_up=F('last_check')).count()
+    leaves = {
+        IPResourceState.STATE_DOWN: 0,
+        IPResourceState.STATE_UP: 0,
+        IPResourceState.STATE_UNKNOWN: 0,
+    }
 
-    now = timezone.now()
+    become_down = IPResource.objects.exclude(last_state__state=IPResourceState.STATE_DOWN).filter(ip__in=down)
+    for ip in become_down:
+        leaves[ip.last_state.state] += 1
+        if ip.last_state.state == IPResourceState.STATE_UP:
+            ip.last_time_up = now
+        ip.last_state = IPResourceState.objects.create(ip=ip, date=now, state=IPResourceState.STATE_DOWN)
+        ip.save()
+    become_up = IPResource.objects.exclude(last_state__state=IPResourceState.STATE_UP).filter(ip__in=up)
+    for ip in become_up:
+        leaves[ip.last_state.state] += 1
+        ip.last_state = IPResourceState.objects.create(ip=ip, date=now, state=IPResourceState.STATE_UP)
+        ip.save()
+    become_unknown = IPResource.objects.exclude(last_state__state=IPResourceState.STATE_UNKNOWN).exclude(Q(pk__in=up) | Q(pk__in=down))
+    for ip in become_unknown:
+        leaves[ip.last_state.state] += 1
+        if ip.last_state.state == IPResourceState.STATE_UP:
+            ip.last_time_up = now
+        ip.last_state = IPResourceState.objects.create(ip=ip, date=now, state=IPResourceState.STATE_UNKNOWN)
+        ip.save()
 
-    up.update(last_time_up=now, last_check=now)
-    down.update(last_check=now)
 
-    upped = len(up) - up_count
-    downed = len(down) - down_count
-    unknowned = len(unknown) - unknown_count
+    down_count = IPResource.objects.filter(last_state__state=IPResourceState.STATE_DOWN).count()
+    up_count = IPResource.objects.filter(last_state__state=IPResourceState.STATE_UP).count()
+    unknown_count = IPResource.objects.filter(last_state__state=IPResourceState.STATE_UNKNOWN).count()
 
-    return "UP: %d (%+d), DOWN: %d (%+d), UNKNOWN: %d (%+d)" \
-            % (len(up), upped, len(down), downed, len(unknown), unknowned)
+    stats = "UP: %d (-%d+%d), DOWN: %d (-%d+%d), UNKNOWN: %d (-%d+%d)" \
+            % (up_count, leaves[IPResourceState.STATE_UP], len(become_up), \
+               down_count, leaves[IPResourceState.STATE_DOWN], len(become_down), \
+               unknown_count, leaves[IPResourceState.STATE_UNKNOWN], len(become_unknown))
+    return stats