From 4b269fba0ca273dfa3acf44c9f5490f01e0c3d87 Mon Sep 17 00:00:00 2001 From: Trevor Bramwell Date: Fri, 22 Sep 2017 12:23:36 -0700 Subject: Rename pharos-dashboard and pharos-validator As subdirectories of the pharos-tools repo, there is little need to keep the pharos prefix. Change-Id: Ica3d79411f409df638647300036c0664183c2725 Signed-off-by: Trevor Bramwell --- dashboard/src/dashboard/__init__.py | 10 ++ dashboard/src/dashboard/admin.py | 20 +++ dashboard/src/dashboard/apps.py | 15 ++ dashboard/src/dashboard/fixtures/dashboard.json | 164 +++++++++++++++++++++ dashboard/src/dashboard/migrations/0001_initial.py | 64 ++++++++ .../migrations/0002_auto_20170505_0815.py | 42 ++++++ dashboard/src/dashboard/migrations/__init__.py | 10 ++ dashboard/src/dashboard/models.py | 95 ++++++++++++ dashboard/src/dashboard/tasks.py | 24 +++ dashboard/src/dashboard/templatetags/__init__.py | 10 ++ .../src/dashboard/templatetags/jenkins_filters.py | 38 +++++ .../src/dashboard/templatetags/jira_filters.py | 17 +++ dashboard/src/dashboard/tests/__init__.py | 10 ++ dashboard/src/dashboard/tests/test_models.py | 69 +++++++++ dashboard/src/dashboard/tests/test_views.py | 75 ++++++++++ dashboard/src/dashboard/urls.py | 41 ++++++ dashboard/src/dashboard/views.py | 141 ++++++++++++++++++ 17 files changed, 845 insertions(+) create mode 100644 dashboard/src/dashboard/__init__.py create mode 100644 dashboard/src/dashboard/admin.py create mode 100644 dashboard/src/dashboard/apps.py create mode 100644 dashboard/src/dashboard/fixtures/dashboard.json create mode 100644 dashboard/src/dashboard/migrations/0001_initial.py create mode 100644 dashboard/src/dashboard/migrations/0002_auto_20170505_0815.py create mode 100644 dashboard/src/dashboard/migrations/__init__.py create mode 100644 dashboard/src/dashboard/models.py create mode 100644 dashboard/src/dashboard/tasks.py create mode 100644 dashboard/src/dashboard/templatetags/__init__.py create mode 100644 dashboard/src/dashboard/templatetags/jenkins_filters.py create mode 100644 dashboard/src/dashboard/templatetags/jira_filters.py create mode 100644 dashboard/src/dashboard/tests/__init__.py create mode 100644 dashboard/src/dashboard/tests/test_models.py create mode 100644 dashboard/src/dashboard/tests/test_views.py create mode 100644 dashboard/src/dashboard/urls.py create mode 100644 dashboard/src/dashboard/views.py (limited to 'dashboard/src/dashboard') diff --git a/dashboard/src/dashboard/__init__.py b/dashboard/src/dashboard/__init__.py new file mode 100644 index 0000000..b5914ce --- /dev/null +++ b/dashboard/src/dashboard/__init__.py @@ -0,0 +1,10 @@ +############################################################################## +# Copyright (c) 2016 Max Breitenfeldt 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 +############################################################################## + + diff --git a/dashboard/src/dashboard/admin.py b/dashboard/src/dashboard/admin.py new file mode 100644 index 0000000..0bfdef8 --- /dev/null +++ b/dashboard/src/dashboard/admin.py @@ -0,0 +1,20 @@ +############################################################################## +# Copyright (c) 2016 Max Breitenfeldt 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 +############################################################################## + + +from django.contrib import admin + +from dashboard.models import * + +admin.site.site_header = "Pharos Dashboard Administration" +admin.site.site_title = "Pharos Dashboard" + +admin.site.register(Resource) +admin.site.register(Server) +admin.site.register(ResourceStatus) diff --git a/dashboard/src/dashboard/apps.py b/dashboard/src/dashboard/apps.py new file mode 100644 index 0000000..e0c4f44 --- /dev/null +++ b/dashboard/src/dashboard/apps.py @@ -0,0 +1,15 @@ +############################################################################## +# Copyright (c) 2016 Max Breitenfeldt 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 +############################################################################## + + +from django.apps import AppConfig + + +class DashboardConfig(AppConfig): + name = 'dashboard' diff --git a/dashboard/src/dashboard/fixtures/dashboard.json b/dashboard/src/dashboard/fixtures/dashboard.json new file mode 100644 index 0000000..f0ac3b2 --- /dev/null +++ b/dashboard/src/dashboard/fixtures/dashboard.json @@ -0,0 +1,164 @@ +[ +{ + "model": "dashboard.resource", + "pk": 1, + "fields": { + "name": "Linux Foundation POD 1", + "description": "Some description", + "url": "https://wiki.opnfv.org/display/pharos/Lf+Lab" + } +}, +{ + "model": "dashboard.resource", + "pk": 2, + "fields": { + "name": "Linux Foundation POD 2", + "description": "Some description", + "url": "https://wiki.opnfv.org/display/pharos/Lf+Lab" + } +}, +{ + "model": "dashboard.resource", + "pk": 3, + "fields": { + "name": "Ericsson POD 2", + "description": "Some description", + "url": "https://wiki.opnfv.org/display/pharos/Ericsson+Hosting+and+Request+Process" + } +}, +{ + "model": "dashboard.resource", + "pk": 4, + "fields": { + "name": "Intel POD 2", + "description": "Some description", + "url": "https://wiki.opnfv.org/display/pharos/Intel+Pod2" + } +}, +{ + "model": "dashboard.resource", + "pk": 5, + "fields": { + "name": "Intel POD 5", + "description": "Some description", + "url": "https://wiki.opnfv.org/display/pharos/Intel+Pod5" + } +}, +{ + "model": "dashboard.resource", + "pk": 6, + "fields": { + "name": "Intel POD 6", + "description": "Some description", + "url": "https://wiki.opnfv.org/display/pharos/Intel+Pod6" + } +}, +{ + "model": "dashboard.resource", + "pk": 7, + "fields": { + "name": "Intel POD 8", + "description": "Some description", + "url": "https://wiki.opnfv.org/display/pharos/Intel+Pod8" + } +}, +{ + "model": "dashboard.resource", + "pk": 8, + "fields": { + "name": "Huawei POD 1", + "description": "Some description", + "url": "https://wiki.opnfv.org/display/pharos/Huawei+Hosting" + } +}, +{ + "model": "dashboard.resource", + "pk": 9, + "fields": { + "name": "Intel POD 3", + "description": "Some description", + "url": "https://wiki.opnfv.org/display/pharos/Intel+Pod3" + } +}, +{ + "model": "dashboard.resource", + "pk": 10, + "fields": { + "name": "Dell POD 1", + "description": "Some description", + "url": "https://wiki.opnfv.org/display/pharos/Dell+Hosting" + } +}, +{ + "model": "dashboard.resource", + "pk": 11, + "fields": { + "name": "Dell POD 2", + "description": "Some description", + "url": "https://wiki.opnfv.org/display/pharos/Dell+Hosting" + } +}, +{ + "model": "dashboard.resource", + "pk": 12, + "fields": { + "name": "Orange POD 2", + "description": "Some description", + "url": "https://wiki.opnfv.org/display/pharos/Opnfv-orange-pod2" + } +}, +{ + "model": "dashboard.resource", + "pk": 13, + "fields": { + "name": "Arm POD 1", + "description": "Some description", + "url": "https://wiki.opnfv.org/display/pharos/Enea-pharos-lab" + } +}, +{ + "model": "dashboard.resource", + "pk": 14, + "fields": { + "name": "Ericsson POD 1", + "description": "Some description", + "url": "https://wiki.opnfv.org/display/pharos/Ericsson+Hosting+and+Request+Process" + } +}, +{ + "model": "dashboard.resource", + "pk": 15, + "fields": { + "name": "Huawei POD 2", + "description": "Some description", + "url": "https://wiki.opnfv.org/display/pharos/Huawei+Hosting" + } +}, +{ + "model": "dashboard.resource", + "pk": 16, + "fields": { + "name": "Huawei POD 3", + "description": "Some description", + "url": "https://wiki.opnfv.org/display/pharos/Huawei+Hosting" + } +}, +{ + "model": "dashboard.resource", + "pk": 17, + "fields": { + "name": "Huawei POD 4", + "description": "Some description", + "url": "https://wiki.opnfv.org/display/pharos/Huawei+Hosting" + } +}, +{ + "model": "dashboard.resource", + "pk": 18, + "fields": { + "name": "Intel POD 9", + "description": "Some description", + "url": "https://wiki.opnfv.org/display/pharos/Intel+Pod9" + } +} +] diff --git a/dashboard/src/dashboard/migrations/0001_initial.py b/dashboard/src/dashboard/migrations/0001_initial.py new file mode 100644 index 0000000..aaf3945 --- /dev/null +++ b/dashboard/src/dashboard/migrations/0001_initial.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10 on 2016-11-03 13:33 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('jenkins', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Resource', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('name', models.CharField(max_length=100, unique=True)), + ('description', models.CharField(blank=True, max_length=300, null=True)), + ('url', models.CharField(blank=True, max_length=100, null=True)), + ('owner', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='user_lab_owner', to=settings.AUTH_USER_MODEL)), + ('slave', models.ForeignKey(null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='jenkins.JenkinsSlave')), + ('vpn_users', models.ManyToManyField(blank=True, related_name='user_vpn_users', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'db_table': 'resource', + }, + ), + migrations.CreateModel( + name='ResourceStatus', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('timestamp', models.DateTimeField(auto_now_add=True)), + ('type', models.CharField(max_length=20)), + ('title', models.CharField(max_length=50)), + ('content', models.CharField(max_length=5000)), + ('resource', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='dashboard.Resource')), + ], + options={ + 'db_table': 'resource_status', + }, + ), + migrations.CreateModel( + name='Server', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('name', models.CharField(blank=True, max_length=100)), + ('model', models.CharField(blank=True, max_length=100)), + ('cpu', models.CharField(blank=True, max_length=100)), + ('ram', models.CharField(blank=True, max_length=100)), + ('storage', models.CharField(blank=True, max_length=100)), + ('resource', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='dashboard.Resource')), + ], + options={ + 'db_table': 'server', + }, + ), + ] diff --git a/dashboard/src/dashboard/migrations/0002_auto_20170505_0815.py b/dashboard/src/dashboard/migrations/0002_auto_20170505_0815.py new file mode 100644 index 0000000..4285b88 --- /dev/null +++ b/dashboard/src/dashboard/migrations/0002_auto_20170505_0815.py @@ -0,0 +1,42 @@ +############################################################################## +# Copyright (c) 2016 Max Breitenfeldt 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 +############################################################################## + + +# -*- coding: utf-8 -*- +# Generated by Django 1.10 on 2017-05-05 08:15 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('dashboard', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='resource', + name='dev_pod', + field=models.BooleanField(default=False), + ), + migrations.AlterField( + model_name='resource', + name='owner', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='user_lab_owner', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='resource', + name='slave', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='jenkins.JenkinsSlave'), + ), + ] diff --git a/dashboard/src/dashboard/migrations/__init__.py b/dashboard/src/dashboard/migrations/__init__.py new file mode 100644 index 0000000..b5914ce --- /dev/null +++ b/dashboard/src/dashboard/migrations/__init__.py @@ -0,0 +1,10 @@ +############################################################################## +# Copyright (c) 2016 Max Breitenfeldt 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 +############################################################################## + + diff --git a/dashboard/src/dashboard/models.py b/dashboard/src/dashboard/models.py new file mode 100644 index 0000000..3de7db3 --- /dev/null +++ b/dashboard/src/dashboard/models.py @@ -0,0 +1,95 @@ +############################################################################## +# Copyright (c) 2016 Max Breitenfeldt 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 +############################################################################## + + +from datetime import timedelta + +from django.contrib.auth.models import User +from django.db import models +from django.utils import timezone + +from jenkins.models import JenkinsSlave + + +class Resource(models.Model): + id = models.AutoField(primary_key=True) + name = models.CharField(max_length=100, unique=True) + description = models.CharField(max_length=300, blank=True, null=True) + url = models.CharField(max_length=100, blank=True, null=True) + owner = models.ForeignKey(User, related_name='user_lab_owner', null=True, blank=True) + vpn_users = models.ManyToManyField(User, related_name='user_vpn_users', blank=True) + slave = models.ForeignKey(JenkinsSlave, on_delete=models.DO_NOTHING, null=True, blank=True) + dev_pod = models.BooleanField(default=False) + + def get_booking_utilization(self, weeks): + """ + Return a dictionary containing the count of booked and free seconds for a resource in the + range [now,now + weeks] if weeks is positive, + or [now-weeks, now] if weeks is negative + """ + + length = timedelta(weeks=abs(weeks)) + now = timezone.now() + + start = now + end = now + length + if weeks < 0: + start = now - length + end = now + + bookings = self.booking_set.filter(start__lt=start + length, end__gt=start) + + booked_seconds = 0 + for booking in bookings: + booking_start = booking.start + booking_end = booking.end + if booking_start < start: + booking_start = start + if booking_end > end: + booking_end = start + length + total = booking_end - booking_start + booked_seconds += total.total_seconds() + + return {'booked_seconds': booked_seconds, + 'available_seconds': length.total_seconds() - booked_seconds} + + class Meta: + db_table = 'resource' + + def __str__(self): + return self.name + +class Server(models.Model): + id = models.AutoField(primary_key=True) + resource = models.ForeignKey(Resource, on_delete=models.CASCADE) + name = models.CharField(max_length=100, blank=True) + model = models.CharField(max_length=100, blank=True) + cpu = models.CharField(max_length=100, blank=True) + ram = models.CharField(max_length=100, blank=True) + storage = models.CharField(max_length=100, blank=True) + + class Meta: + db_table = 'server' + + def __str__(self): + return self.name + +class ResourceStatus(models.Model): + id = models.AutoField(primary_key=True) + resource = models.ForeignKey(Resource, on_delete=models.CASCADE) + timestamp = models.DateTimeField(auto_now_add=True) + type = models.CharField(max_length=20) + title = models.CharField(max_length=50) + content = models.CharField(max_length=5000) + + class Meta: + db_table = 'resource_status' + + def __str__(self): + return self.resource.name + ': ' + self.title + ' ' + str(self.timestamp) diff --git a/dashboard/src/dashboard/tasks.py b/dashboard/src/dashboard/tasks.py new file mode 100644 index 0000000..c5ef505 --- /dev/null +++ b/dashboard/src/dashboard/tasks.py @@ -0,0 +1,24 @@ +############################################################################## +# Copyright (c) 2016 Max Breitenfeldt 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 +############################################################################## + + +from datetime import timedelta + +from celery import shared_task +from django.utils import timezone + +from jenkins.models import JenkinsStatistic +from notification.models import BookingNotification + + +@shared_task +def database_cleanup(): + now = timezone.now() + JenkinsStatistic.objects.filter(timestamp__lt=now - timedelta(weeks=4)).delete() + BookingNotification.objects.filter(submit_time__lt=now - timedelta(weeks=4)).delete() \ No newline at end of file diff --git a/dashboard/src/dashboard/templatetags/__init__.py b/dashboard/src/dashboard/templatetags/__init__.py new file mode 100644 index 0000000..b5914ce --- /dev/null +++ b/dashboard/src/dashboard/templatetags/__init__.py @@ -0,0 +1,10 @@ +############################################################################## +# Copyright (c) 2016 Max Breitenfeldt 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 +############################################################################## + + diff --git a/dashboard/src/dashboard/templatetags/jenkins_filters.py b/dashboard/src/dashboard/templatetags/jenkins_filters.py new file mode 100644 index 0000000..e7e1425 --- /dev/null +++ b/dashboard/src/dashboard/templatetags/jenkins_filters.py @@ -0,0 +1,38 @@ +############################################################################## +# Copyright (c) 2016 Max Breitenfeldt 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 +############################################################################## + + +from django.template.defaultfilters import register + + +@register.filter +def jenkins_job_color(job_result): + if job_result == 'SUCCESS': + return '#5cb85c' + if job_result == 'FAILURE': + return '#d9534f' + if job_result == 'UNSTABLE': + return '#EDD62B' + return '#646F73' # job is still building + + +@register.filter +def jenkins_status_color(slave_status): + if slave_status == 'offline': + return '#d9534f' + if slave_status == 'online': + return '#5cb85c' + if slave_status == 'online / idle': + return '#5bc0de' + + +@register.filter +def jenkins_job_blink(job_result): + if job_result == '': # job is still building + return 'class=blink_me' diff --git a/dashboard/src/dashboard/templatetags/jira_filters.py b/dashboard/src/dashboard/templatetags/jira_filters.py new file mode 100644 index 0000000..9a97c1d --- /dev/null +++ b/dashboard/src/dashboard/templatetags/jira_filters.py @@ -0,0 +1,17 @@ +############################################################################## +# Copyright (c) 2016 Max Breitenfeldt 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 +############################################################################## + + +from django.conf import settings +from django.template.defaultfilters import register + + +@register.filter +def jira_issue_url(issue): + return settings.JIRA_URL + '/browse/' + str(issue) diff --git a/dashboard/src/dashboard/tests/__init__.py b/dashboard/src/dashboard/tests/__init__.py new file mode 100644 index 0000000..b5914ce --- /dev/null +++ b/dashboard/src/dashboard/tests/__init__.py @@ -0,0 +1,10 @@ +############################################################################## +# Copyright (c) 2016 Max Breitenfeldt 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 +############################################################################## + + diff --git a/dashboard/src/dashboard/tests/test_models.py b/dashboard/src/dashboard/tests/test_models.py new file mode 100644 index 0000000..3a3aeab --- /dev/null +++ b/dashboard/src/dashboard/tests/test_models.py @@ -0,0 +1,69 @@ +############################################################################## +# Copyright (c) 2016 Max Breitenfeldt 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 +############################################################################## + + +from datetime import timedelta +from math import ceil, floor + +from django.test import TestCase +from django.utils import timezone + +from booking.models import * +from dashboard.models import Resource +from jenkins.models import JenkinsSlave + + +class ResourceModelTestCase(TestCase): + def setUp(self): + self.slave = JenkinsSlave.objects.create(name='test', url='test') + self.owner = User.objects.create(username='owner') + + self.res1 = Resource.objects.create(name='res1', slave=self.slave, description='x', + url='x', owner=self.owner) + + def test_booking_utilization(self): + utilization = self.res1.get_booking_utilization(1) + self.assertTrue(utilization['booked_seconds'] == 0) + self.assertTrue(utilization['available_seconds'] == timedelta(weeks=1).total_seconds()) + + start = timezone.now() + timedelta(days=1) + end = start + timedelta(days=1) + booking = Booking.objects.create(start=start, end=end, purpose='test', resource=self.res1, + user=self.owner) + + utilization = self.res1.get_booking_utilization(1) + booked_seconds = timedelta(days=1).total_seconds() + self.assertEqual(utilization['booked_seconds'], booked_seconds) + + utilization = self.res1.get_booking_utilization(-1) + self.assertEqual(utilization['booked_seconds'], 0) + + booking.delete() + start = timezone.now() - timedelta(days=1) + end = start + timedelta(days=2) + booking = Booking.objects.create(start=start, end=end, purpose='test', resource=self.res1, + user=self.owner) + booked_seconds = self.res1.get_booking_utilization(1)['booked_seconds'] + # use ceil because a fraction of the booked time has already passed now + booked_seconds = ceil(booked_seconds) + self.assertEqual(booked_seconds, timedelta(days=1).total_seconds()) + + booking.delete() + start = timezone.now() + timedelta(days=6) + end = start + timedelta(days=2) + booking = Booking.objects.create(start=start, end=end, purpose='test', resource=self.res1, + user=self.owner) + booked_seconds = self.res1.get_booking_utilization(1)['booked_seconds'] + booked_seconds = floor(booked_seconds) + self.assertEqual(booked_seconds, timedelta(days=1).total_seconds()) + + + + + diff --git a/dashboard/src/dashboard/tests/test_views.py b/dashboard/src/dashboard/tests/test_views.py new file mode 100644 index 0000000..f5e17c2 --- /dev/null +++ b/dashboard/src/dashboard/tests/test_views.py @@ -0,0 +1,75 @@ +############################################################################## +# Copyright (c) 2016 Max Breitenfeldt 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 +############################################################################## + + +from django.test import TestCase +from django.urls import reverse + +from dashboard.models import Resource +from jenkins.models import JenkinsSlave + + +class DashboardViewTestCase(TestCase): + def setUp(self): + self.slave_active = JenkinsSlave.objects.create(name='slave_active', url='x', active=True) + self.slave_inactive = JenkinsSlave.objects.create(name='slave_inactive', url='x', + active=False) + self.res_active = Resource.objects.create(name='res_active', slave=self.slave_active, + description='x', url='x') + self.res_inactive = Resource.objects.create(name='res_inactive', slave=self.slave_inactive, + description='x', url='x') + + def test_booking_utilization_json(self): + url = reverse('dashboard:booking_utilization', kwargs={'resource_id': 0, 'weeks': 0}) + self.assertEqual(self.client.get(url).status_code, 404) + + url = reverse('dashboard:booking_utilization', kwargs={'resource_id': self.res_active.id, + 'weeks': 0}) + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + self.assertContains(response, 'data') + + def test_jenkins_utilization_json(self): + url = reverse('dashboard:jenkins_utilization', kwargs={'resource_id': 0, 'weeks': 0}) + self.assertEqual(self.client.get(url).status_code, 404) + + url = reverse('dashboard:jenkins_utilization', kwargs={'resource_id': self.res_active.id, + 'weeks': 0}) + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + self.assertContains(response, 'data') + + def test_jenkins_slaves_view(self): + url = reverse('dashboard:jenkins_slaves') + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + self.assertIn(self.slave_active, response.context['slaves']) + self.assertNotIn(self.slave_inactive, response.context['slaves']) + + def test_ci_pods_view(self): + url = reverse('dashboard:ci_pods') + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + self.assertEqual(len(response.context['ci_pods']), 0) + + self.slave_active.ci_slave = True + self.slave_inactive.ci_slave = True + self.slave_active.save() + self.slave_inactive.save() + + response = self.client.get(url) + self.assertIn(self.res_active, response.context['ci_pods']) + self.assertNotIn(self.res_inactive, response.context['ci_pods']) + + def test_dev_pods_view(self): + url = reverse('dashboard:dev_pods') + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + self.assertEqual(len(response.context['dev_pods']), 0) + diff --git a/dashboard/src/dashboard/urls.py b/dashboard/src/dashboard/urls.py new file mode 100644 index 0000000..609e5d6 --- /dev/null +++ b/dashboard/src/dashboard/urls.py @@ -0,0 +1,41 @@ +############################################################################## +# Copyright (c) 2016 Max Breitenfeldt 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 +############################################################################## + + +"""pharos_dashboard URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/1.10/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.conf.urls import url, include + 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) +""" +from django.conf.urls import url + +from dashboard.views import * + +urlpatterns = [ + url(r'^ci_pods/$', CIPodsView.as_view(), name='ci_pods'), + url(r'^dev_pods/$', DevelopmentPodsView.as_view(), name='dev_pods'), + url(r'^jenkins_slaves/$', JenkinsSlavesView.as_view(), name='jenkins_slaves'), + url(r'^resource/all/$', LabOwnerView.as_view(), name='resources'), + url(r'^resource/(?P[0-9]+)/$', ResourceView.as_view(), name='resource'), + url(r'^resource/(?P[0-9]+)/booking_utilization/(?P-?\d+)/$', + BookingUtilizationJSON.as_view(), name='booking_utilization'), + url(r'^resource/(?P[0-9]+)/jenkins_utilization/(?P-?\d+)/$', + JenkinsUtilizationJSON.as_view(), name='jenkins_utilization'), + url(r'^$', DevelopmentPodsView.as_view(), name="index"), +] diff --git a/dashboard/src/dashboard/views.py b/dashboard/src/dashboard/views.py new file mode 100644 index 0000000..62a9f83 --- /dev/null +++ b/dashboard/src/dashboard/views.py @@ -0,0 +1,141 @@ +############################################################################## +# Copyright (c) 2016 Max Breitenfeldt 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 +############################################################################## + + +from datetime import timedelta + +from django.http import JsonResponse +from django.shortcuts import get_object_or_404 +from django.utils import timezone +from django.views import View +from django.views.generic import TemplateView + +from booking.models import Booking +from dashboard.models import Resource +from jenkins.models import JenkinsSlave + + +class JenkinsSlavesView(TemplateView): + template_name = "dashboard/jenkins_slaves.html" + + def get_context_data(self, **kwargs): + slaves = JenkinsSlave.objects.filter(active=True) + context = super(JenkinsSlavesView, self).get_context_data(**kwargs) + context.update({'title': "Jenkins Slaves", 'slaves': slaves}) + return context + + +class CIPodsView(TemplateView): + template_name = "dashboard/ci_pods.html" + + def get_context_data(self, **kwargs): + ci_pods = Resource.objects.filter(slave__ci_slave=True, slave__active=True) + context = super(CIPodsView, self).get_context_data(**kwargs) + context.update({'title': "CI Pods", 'ci_pods': ci_pods}) + return context + + +class DevelopmentPodsView(TemplateView): + template_name = "dashboard/dev_pods.html" + + def get_context_data(self, **kwargs): + resources = Resource.objects.filter(dev_pod=True) + + bookings = Booking.objects.filter(start__lte=timezone.now()) + bookings = bookings.filter(end__gt=timezone.now()) + + dev_pods = [] + for resource in resources: + booking_utilization = resource.get_booking_utilization(weeks=4) + total = booking_utilization['booked_seconds'] + booking_utilization['available_seconds'] + try: + utilization_percentage = "%d%%" % (float(booking_utilization['booked_seconds']) / + total * 100) + except (ValueError, ZeroDivisionError): + return "" + + dev_pod = (resource, None, utilization_percentage) + for booking in bookings: + if booking.resource == resource: + dev_pod = (resource, booking, utilization_percentage) + dev_pods.append(dev_pod) + + context = super(DevelopmentPodsView, self).get_context_data(**kwargs) + context.update({'title': "Development Pods", 'dev_pods': dev_pods}) + return context + + +class ResourceView(TemplateView): + template_name = "dashboard/resource.html" + + def get_context_data(self, **kwargs): + resource = get_object_or_404(Resource, id=self.kwargs['resource_id']) + bookings = Booking.objects.filter(resource=resource, end__gt=timezone.now()) + context = super(ResourceView, self).get_context_data(**kwargs) + context.update({'title': str(resource), 'resource': resource, 'bookings': bookings}) + return context + + +class LabOwnerView(TemplateView): + template_name = "dashboard/resource_all.html" + + def get_context_data(self, **kwargs): + resources = Resource.objects.filter(slave__dev_pod=True, slave__active=True) + pods = [] + for resource in resources: + utilization = resource.slave.get_utilization(timedelta(days=7)) + bookings = Booking.objects.filter(resource=resource, end__gt=timezone.now()) + pods.append((resource, utilization, bookings)) + context = super(LabOwnerView, self).get_context_data(**kwargs) + context.update({'title': "Overview", 'pods': pods}) + return context + + +class BookingUtilizationJSON(View): + def get(self, request, *args, **kwargs): + resource = get_object_or_404(Resource, id=kwargs['resource_id']) + utilization = resource.get_booking_utilization(int(kwargs['weeks'])) + utilization = [ + { + 'label': 'Booked', + 'data': utilization['booked_seconds'], + 'color': '#d9534f' + }, + { + 'label': 'Available', + 'data': utilization['available_seconds'], + 'color': '#5cb85c' + }, + ] + return JsonResponse({'data': utilization}) + + +class JenkinsUtilizationJSON(View): + def get(self, request, *args, **kwargs): + resource = get_object_or_404(Resource, id=kwargs['resource_id']) + weeks = int(kwargs['weeks']) + utilization = resource.slave.get_utilization(timedelta(weeks=weeks)) + utilization = [ + { + 'label': 'Offline', + 'data': utilization['offline'], + 'color': '#d9534f' + }, + { + 'label': 'Online', + 'data': utilization['online'], + 'color': '#5cb85c' + }, + { + 'label': 'Idle', + 'data': utilization['idle'], + 'color': '#5bc0de' + }, + ] + return JsonResponse({'data': utilization}) -- cgit 1.2.3-korg