Adds Downtime Awareness 47/68347/2
authorParker Berberian <pberberian@iol.unh.edu>
Mon, 12 Aug 2019 18:18:25 +0000 (14:18 -0400)
committerParker Berberian <pberberian@iol.unh.edu>
Tue, 13 Aug 2019 16:49:44 +0000 (12:49 -0400)
This adds a Downtime model and relevant operations so that
the dashboard knows when a lab is down for maintenance and
can act accordingly.

This change doesn't modify the front end at all,
but it does pass relevant downtime info to the
templates so that they can be updated in a future change.

Change-Id: Idb88b15838b949f352f11a31a1fce9749d283d28
Signed-off-by: Parker Berberian <pberberian@iol.unh.edu>
src/account/migrations/0004_downtime.py [new file with mode: 0644]
src/account/models.py
src/api/forms.py [new file with mode: 0644]
src/api/models.py
src/api/urls.py
src/api/views.py
src/booking/views.py

diff --git a/src/account/migrations/0004_downtime.py b/src/account/migrations/0004_downtime.py
new file mode 100644 (file)
index 0000000..fc700d1
--- /dev/null
@@ -0,0 +1,24 @@
+# Generated by Django 2.2 on 2019-08-13 16:45
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('account', '0003_publicnetwork'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='Downtime',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('start', models.DateTimeField()),
+                ('end', models.DateTimeField()),
+                ('description', models.TextField(default='This lab will be down for maintenance')),
+                ('lab', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='account.Lab')),
+            ],
+        ),
+    ]
index 4fc7c40..4862231 100644 (file)
@@ -168,3 +168,23 @@ class PublicNetwork(models.Model):
     in_use = models.BooleanField(default=False)
     cidr = models.CharField(max_length=50, default="0.0.0.0/0")
     gateway = models.CharField(max_length=50, default="0.0.0.0")
+
+
+class Downtime(models.Model):
+    start = models.DateTimeField()
+    end = models.DateTimeField()
+    lab = models.ForeignKey(Lab, on_delete=models.CASCADE)
+    description = models.TextField(default="This lab will be down for maintenance")
+
+    def save(self, *args, **kwargs):
+        if self.start >= self.end:
+            raise ValueError('Start date is after end date')
+
+        # check for overlapping downtimes
+        overlap_start = Downtime.objects.filter(lab=self.lab, start__gt=self.start, start__lt=self.end).exists()
+        overlap_end = Downtime.objects.filter(lab=self.lab, end__lt=self.end, end__gt=self.start).exists()
+
+        if overlap_start or overlap_end:
+            raise ValueError('Overlapping Downtime')
+
+        return super(Downtime, self).save(*args, **kwargs)
diff --git a/src/api/forms.py b/src/api/forms.py
new file mode 100644 (file)
index 0000000..1b74a9b
--- /dev/null
@@ -0,0 +1,16 @@
+##############################################################################
+# Copyright (c) 2019 Parker Berberian and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+#############################################################################
+
+import django.forms as forms
+
+
+class DowntimeForm(forms.Form):
+    start = forms.DateTimeField()
+    end = forms.DateTimeField()
+    description = forms.CharField(max_length=1000, required=False)
index 1f708ae..682785b 100644 (file)
@@ -13,6 +13,7 @@ from django.db import models
 from django.core.exceptions import PermissionDenied
 from django.shortcuts import get_object_or_404
 from django.urls import reverse
+from django.utils import timezone
 
 import json
 import uuid
@@ -30,6 +31,7 @@ from resource_inventory.models import (
 )
 from resource_inventory.idf_templater import IDFTemplater
 from resource_inventory.pdf_templater import PDFTemplater
+from account.models import Downtime
 
 
 class JobStatus(object):
@@ -67,6 +69,38 @@ class LabManager(object):
     def __init__(self, lab):
         self.lab = lab
 
+    def get_downtime(self):
+        return Downtime.objects.filter(start__lt=timezone.now(), end__gt=timezone.now(), lab=self.lab)
+
+    def get_downtime_json(self):
+        downtime = self.get_downtime().first()  # should only be one item in queryset
+        if downtime:
+            return {
+                "is_down": True,
+                "start": downtime.start,
+                "end": downtime.end,
+                "description": downtime.description
+            }
+        return {"is_down": False}
+
+    def create_downtime(self, form):
+        """
+        takes in a dictionary that describes the model.
+        {
+          "start": utc timestamp
+          "end": utc timestamp
+          "description": human text (optional)
+        }
+        For timestamp structure, https://docs.djangoproject.com/en/2.2/ref/forms/fields/#datetimefield
+        """
+        Downtime.objects.create(
+            start=form.cleaned_data['start'],
+            end=form.cleaned_data['end'],
+            description=form.cleaned_data['description'],
+            lab=self.lab
+        )
+        return self.get_downtime_json()
+
     def update_host_remote_info(self, data, host_id):
         host = get_object_or_404(Host, labid=host_id, lab=self.lab)
         info = {}
index 778f6eb..b8b9cff 100644 (file)
@@ -31,6 +31,7 @@ from api.views import (
     lab_profile,
     lab_status,
     lab_inventory,
+    lab_downtime,
     specific_job,
     specific_task,
     new_jobs,
@@ -47,6 +48,7 @@ urlpatterns = [
     path('labs/<slug:lab_name>/profile', lab_profile),
     path('labs/<slug:lab_name>/status', lab_status),
     path('labs/<slug:lab_name>/inventory', lab_inventory),
+    path('labs/<slug:lab_name>/downtime', lab_downtime),
     path('labs/<slug:lab_name>/hosts/<slug:host_id>', lab_host),
     path('labs/<slug:lab_name>/hosts/<slug:host_id>/bmc', update_host_bmc),
     path('labs/<slug:lab_name>/booking/<int:booking_id>/pdf', get_pdf, name="get-pdf"),
index fb28958..a5153d7 100644 (file)
@@ -12,6 +12,7 @@
 from django.contrib.auth.decorators import login_required
 from django.shortcuts import redirect
 from django.utils.decorators import method_decorator
+from django.utils import timezone
 from django.views import View
 from django.http.response import JsonResponse, HttpResponse
 from rest_framework import viewsets
@@ -20,6 +21,7 @@ from django.views.decorators.csrf import csrf_exempt
 
 from api.serializers.booking_serializer import BookingSerializer
 from api.serializers.old_serializers import UserSerializer
+from api.forms import DowntimeForm
 from account.models import UserProfile
 from booking.models import Booking
 from api.models import LabManagerTracker, get_task
@@ -150,6 +152,40 @@ def current_jobs(request, lab_name=""):
     return JsonResponse(lab_manager.get_current_jobs(), safe=False)
 
 
+def lab_downtime(request, lab_name=""):
+    lab_token = request.META.get('HTTP_AUTH_TOKEN')
+    lab_manager = LabManagerTracker.get(lab_name, lab_token)
+    if request.method == "GET":
+        return JsonResponse(lab_manager.get_downtime_json())
+    if request.method == "POST":
+        return post_lab_downtime(request, lab_manager)
+    if request.method == "DELETE":
+        return delete_lab_downtime(lab_manager)
+    return HttpResponse(status=405)
+
+
+def post_lab_downtime(request, lab_manager):
+    current_downtime = lab_manager.get_downtime()
+    if current_downtime.exists():
+        return JsonResponse({"error": "Lab is already in downtime"}, status=422)
+    form = DowntimeForm(request.POST)
+    if form.is_valid():
+        return JsonResponse(lab_manager.create_downtime(form))
+    else:
+        return JsonResponse(form.errors.get_json_data(), status=400)
+
+
+def delete_lab_downtime(lab_manager):
+    current_downtime = lab_manager.get_downtime()
+    if current_downtime.exists():
+        dt = current_downtime.first()
+        dt.end = timezone.now()
+        dt.save()
+        return JsonResponse(lab_manager.get_downtime_json(), safe=False)
+    else:
+        return JsonResponse({"error": "Lab is not in downtime"}, status=422)
+
+
 def done_jobs(request, lab_name=""):
     lab_token = request.META.get('HTTP_AUTH_TOKEN')
     lab_manager = LabManagerTracker.get(lab_name, lab_token)
index bad7dc9..8e25952 100644 (file)
@@ -20,7 +20,7 @@ from django.urls import reverse
 
 from resource_inventory.models import ResourceBundle, HostProfile, Image, Host
 from resource_inventory.resource_manager import ResourceManager
-from account.models import Lab
+from account.models import Lab, Downtime
 from booking.models import Booking
 from booking.stats import StatisticsManager
 from booking.forms import HostReImageForm
@@ -79,8 +79,9 @@ class BookingView(TemplateView):
     def get_context_data(self, **kwargs):
         booking = get_object_or_404(Booking, id=self.kwargs['booking_id'])
         title = 'Booking Details'
+        downtime = Downtime.objects.filter(lab=booking.lab, start__lt=timezone.now, end__gt=timezone.now()).first()
         context = super(BookingView, self).get_context_data(**kwargs)
-        context.update({'title': title, 'booking': booking})
+        context.update({'title': title, 'booking': booking, 'downtime': downtime})
         return context