diff options
35 files changed, 672 insertions, 33 deletions
@@ -4,9 +4,9 @@ project_creation_date: '2019.06.25' project_category: '' lifecycle_state: 'Incubation' project_lead: &opnfv_laas_ptl - name: 'Parker Berberian' - email: 'pberberian@iol.unh.edu' - id: 'ParkerBerberian' + name: 'Sawyer Bergeron' + email: 'sbergeron@iol.unh.edu' + id: 'sbergeron' company: 'UNH' timezone: 'EST' primary_contact: *opnfv_laas_ptl @@ -59,11 +59,6 @@ committers: company: 'linuxfoundation' id: 'eball' timezone: 'Unknown/Unknown' - - name: 'Sawyer Bergeron' - email: 'sawyerbergeron@gmail.com' - company: 'gmail' - id: 'sbergeron' - timezone: 'Unknown/Unknown' - name: 'Lincoln Lavoie' email: 'lylavoie@iol.unh.edu' company: 'iol.unh' diff --git a/src/account/migrations/0006_auto_20201109_1947.py b/src/account/migrations/0006_auto_20201109_1947.py new file mode 100644 index 0000000..d08c426 --- /dev/null +++ b/src/account/migrations/0006_auto_20201109_1947.py @@ -0,0 +1,23 @@ +# Generated by Django 2.2 on 2020-11-09 19:47 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('account', '0005_auto_20200723_2100'), + ] + + operations = [ + migrations.AlterField( + model_name='userprofile', + name='full_name', + field=models.CharField(blank=True, default='', max_length=100, null=True), + ), + migrations.AlterField( + model_name='userprofile', + name='jira_url', + field=models.CharField(blank=True, default='', max_length=100, null=True), + ), + ] diff --git a/src/account/models.py b/src/account/models.py index 2d0293f..40de4d8 100644 --- a/src/account/models.py +++ b/src/account/models.py @@ -80,12 +80,12 @@ class VlanManager(models.Model): # if they use QinQ or a vxlan overlay, for example allow_overlapping = models.BooleanField() - def get_vlan(self, count=1): + def get_vlans(self, count=1): """ - Return the ID of available vlans, but does not reserve them. + Return the IDs of available vlans as a list[int], but does not reserve them. Will throw index exception if not enough vlans are available. - If count == 1, the return value is an int. Otherwise, it is a list of ints. + Always returns a list of ints """ allocated = [] vlans = json.loads(self.vlans) diff --git a/src/analytics/__init__.py b/src/analytics/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/analytics/__init__.py diff --git a/src/analytics/admin.py b/src/analytics/admin.py new file mode 100644 index 0000000..63f139f --- /dev/null +++ b/src/analytics/admin.py @@ -0,0 +1,13 @@ +############################################################################## +# Copyright (c) 2020 Sean Smith 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 analytics.models import ActiveVPNUser + +admin.site.register(ActiveVPNUser) diff --git a/src/analytics/apps.py b/src/analytics/apps.py new file mode 100644 index 0000000..fe1b11f --- /dev/null +++ b/src/analytics/apps.py @@ -0,0 +1,14 @@ +############################################################################## +# Copyright (c) 2020 Sean Smith 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 AnalyticsConfig(AppConfig): + name = 'analytics' diff --git a/src/analytics/migrations/0001_initial.py b/src/analytics/migrations/0001_initial.py new file mode 100644 index 0000000..05a7ec8 --- /dev/null +++ b/src/analytics/migrations/0001_initial.py @@ -0,0 +1,22 @@ +# Generated by Django 2.2 on 2020-08-10 20:10 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='ActiveVPNUsers', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('time_stamp', models.DateTimeField(auto_now_add=True)), + ('active_users', models.IntegerField()), + ], + ), + ] diff --git a/src/analytics/migrations/0002_auto_20201109_2149.py b/src/analytics/migrations/0002_auto_20201109_2149.py new file mode 100644 index 0000000..a845ff8 --- /dev/null +++ b/src/analytics/migrations/0002_auto_20201109_2149.py @@ -0,0 +1,27 @@ +# Generated by Django 2.2 on 2020-11-09 21:49 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('account', '0006_auto_20201109_1947'), + ('analytics', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='ActiveVPNUser', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('time_stamp', models.DateTimeField(auto_now_add=True)), + ('active_users', models.IntegerField()), + ('lab', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='account.Lab')), + ], + ), + migrations.DeleteModel( + name='ActiveVPNUsers', + ), + ] diff --git a/src/analytics/migrations/__init__.py b/src/analytics/migrations/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/analytics/migrations/__init__.py diff --git a/src/analytics/models.py b/src/analytics/models.py new file mode 100644 index 0000000..10baa0c --- /dev/null +++ b/src/analytics/models.py @@ -0,0 +1,30 @@ +############################################################################## +# Copyright (c) 2020 Sean Smith 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.db import models +from account.models import Lab + + +class ActiveVPNUser(models.Model): + """ Keeps track of how many VPN Users are connected to Lab """ + time_stamp = models.DateTimeField(auto_now_add=True) + lab = models.ForeignKey(Lab, on_delete=models.CASCADE, null=False) + active_users = models.IntegerField() + + @classmethod + def create(cls, lab_name, active_users): + """ + This creates an Active VPN Users entry from + from lab_name as a string + """ + + lab = Lab.objects.get(name=lab_name) + avu = cls(lab=lab, active_users=active_users) + avu.save() + return avu diff --git a/src/analytics/tests.py b/src/analytics/tests.py new file mode 100644 index 0000000..d234f48 --- /dev/null +++ b/src/analytics/tests.py @@ -0,0 +1,10 @@ +############################################################################## +# Copyright (c) 2020 Sean Smith 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 diff --git a/src/analytics/views.py b/src/analytics/views.py new file mode 100644 index 0000000..160bc59 --- /dev/null +++ b/src/analytics/views.py @@ -0,0 +1,10 @@ +############################################################################## +# Copyright (c) 2020 Sean Smith 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.shortcuts import render diff --git a/src/api/migrations/0015_auto_20201109_1947.py b/src/api/migrations/0015_auto_20201109_1947.py new file mode 100644 index 0000000..bbd51e5 --- /dev/null +++ b/src/api/migrations/0015_auto_20201109_1947.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2 on 2020-11-09 19:47 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0014_manual_20200220'), + ] + + operations = [ + migrations.AlterField( + model_name='taskconfig', + name='state', + field=models.IntegerField(default=0), + ), + ] diff --git a/src/api/migrations/0016_auto_20201109_2149.py b/src/api/migrations/0016_auto_20201109_2149.py new file mode 100644 index 0000000..a430659 --- /dev/null +++ b/src/api/migrations/0016_auto_20201109_2149.py @@ -0,0 +1,41 @@ +# Generated by Django 2.2 on 2020-11-09 21:49 + +import api.models +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0015_auto_20201109_1947'), + ] + + operations = [ + migrations.CreateModel( + name='ActiveUsersConfig', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ], + ), + migrations.AddField( + model_name='job', + name='job_type', + field=models.CharField(choices=[('BOOK', 'Booking'), ('DATA', 'Analytics')], default='BOOK', max_length=4), + ), + migrations.CreateModel( + name='ActiveUsersRelation', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', models.IntegerField(default=0)), + ('task_id', models.CharField(default=api.models.get_task_uuid, max_length=37)), + ('lab_token', models.CharField(default='null', max_length=50)), + ('message', models.TextField(default='')), + ('config', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='api.ActiveUsersConfig')), + ('job', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.Job')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/src/api/models.py b/src/api/models.py index 9b9b778..527e66b 100644 --- a/src/api/models.py +++ b/src/api/models.py @@ -249,6 +249,15 @@ class LabManager(object): return self.serialize_jobs(jobs, status=JobStatus.DONE) + def get_analytics_job(self): + """ Get analytics job with status new """ + jobs = Job.objects.filter( + booking__lab=self.lab, + job_type='DATA' + ) + + return self.serialize_jobs(jobs, status=JobStatus.NEW) + def get_job(self, jobid): return Job.objects.get(pk=jobid).to_dict() @@ -339,9 +348,19 @@ class Job(models.Model): This is the class that is serialized and put into the api """ + JOB_TYPES = ( + ('BOOK', 'Booking'), + ('DATA', 'Analytics') + ) + booking = models.OneToOneField(Booking, on_delete=models.CASCADE, null=True) status = models.IntegerField(default=JobStatus.NEW) complete = models.BooleanField(default=False) + job_type = models.CharField( + max_length=4, + choices=JOB_TYPES, + default='BOOK' + ) def to_dict(self): d = {} @@ -449,6 +468,28 @@ class BridgeConfig(models.Model): return json.dumps(self.to_dict()) +class ActiveUsersConfig(models.Model): + """ + Task for getting active VPN users + + StackStorm needs no information to run this job + so this task is very bare, but neccessary to fit + job creation convention. + """ + + def clear_delta(self): + self.delta = '{}' + + def get_delta(self): + return json.loads(self.to_json()) + + def to_json(self): + return json.dumps(self.to_dict()) + + def to_dict(self): + return {} + + class OpnfvApiConfig(models.Model): installer = models.CharField(max_length=200) @@ -860,6 +901,14 @@ class SnapshotRelation(TaskRelation): return super(self.__class__, self).delete(*args, **kwargs) +class ActiveUsersRelation(TaskRelation): + config = models.OneToOneField(ActiveUsersConfig, on_delete=models.CASCADE) + job_key = "active users task" + + def type_str(self): + return "Active Users Task" + + class JobFactory(object): """This class creates all the API models (jobs, tasks, etc) needed to fulfill a booking.""" @@ -903,6 +952,44 @@ class JobFactory(object): config.save() @classmethod + def makeActiveUsersTask(cls): + """ Append active users task to analytics job """ + config = ActiveUsersConfig() + relation = ActiveUsersRelation() + job = Job.objects.get(job_type='DATA') + + job.status = JobStatus.NEW + + relation.job = job + relation.config = config + relation.config.save() + relation.config = relation.config + relation.save() + config.save() + + @classmethod + def makeAnalyticsJob(cls, booking): + """ + Create the analytics job + + This will only run once since there will only be one analytics job. + All analytics tasks get appended to analytics job. + """ + + if len(Job.objects.filter(job_type='DATA')) > 0: + raise Exception("Cannot have more than one analytics job") + + if booking.resource: + raise Exception("Booking is not marker for analytics job, has resoure") + + job = Job() + job.booking = booking + job.job_type = 'DATA' + job.save() + + cls.makeActiveUsersTask() + + @classmethod def makeCompleteJob(cls, booking): """Create everything that is needed to fulfill the given booking.""" resources = booking.resource.get_resources() @@ -1077,7 +1164,8 @@ JOB_TASK_CLASSLIST = [ AccessRelation, HostNetworkRelation, SoftwareRelation, - SnapshotRelation + SnapshotRelation, + ActiveUsersRelation ] diff --git a/src/api/urls.py b/src/api/urls.py index 0005d34..bae86ea 100644 --- a/src/api/urls.py +++ b/src/api/urls.py @@ -44,7 +44,8 @@ from api.views import ( get_idf, lab_users, lab_user, - GenerateTokenView + GenerateTokenView, + analytics_job ) urlpatterns = [ @@ -61,6 +62,7 @@ urlpatterns = [ path('labs/<slug:lab_name>/jobs/new', new_jobs), path('labs/<slug:lab_name>/jobs/current', current_jobs), path('labs/<slug:lab_name>/jobs/done', done_jobs), + path('labs/<slug:lab_name>/jobs/getByType/DATA', analytics_job), path('labs/<slug:lab_name>/users', lab_users), path('labs/<slug:lab_name>/users/<int:user_id>', lab_user), url(r'^token$', GenerateTokenView.as_view(), name='generate_token'), diff --git a/src/api/views.py b/src/api/views.py index 75a0db3..2e5f33f 100644 --- a/src/api/views.py +++ b/src/api/views.py @@ -18,6 +18,7 @@ from django.http.response import JsonResponse, HttpResponse from rest_framework import viewsets from rest_framework.authtoken.models import Token from django.views.decorators.csrf import csrf_exempt +from django.core.exceptions import ObjectDoesNotExist from api.serializers.booking_serializer import BookingSerializer from api.serializers.old_serializers import UserSerializer @@ -26,6 +27,8 @@ from account.models import UserProfile from booking.models import Booking from api.models import LabManagerTracker, get_task from notifier.manager import NotificationHandler +from analytics.models import ActiveVPNUser +import json """ API views. @@ -176,6 +179,23 @@ def current_jobs(request, lab_name=""): return JsonResponse(lab_manager.get_current_jobs(), safe=False) +@csrf_exempt +def analytics_job(request, lab_name=""): + """ returns all jobs with type booking""" + 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_analytics_job(), safe=False) + if request.method == "POST": + users = json.loads(request.body.decode('utf-8'))['active_users'] + try: + ActiveVPNUser.create(lab_name, users) + except ObjectDoesNotExist: + return JsonResponse('Lab does not exist!', safe=False) + return HttpResponse(status=200) + return HttpResponse(status=405) + + def lab_downtime(request, lab_name=""): lab_token = request.META.get('HTTP_AUTH_TOKEN') lab_manager = LabManagerTracker.get(lab_name, lab_token) diff --git a/src/booking/migrations/0008_auto_20201109_1947.py b/src/booking/migrations/0008_auto_20201109_1947.py new file mode 100644 index 0000000..289e476 --- /dev/null +++ b/src/booking/migrations/0008_auto_20201109_1947.py @@ -0,0 +1,30 @@ +# Generated by Django 2.2 on 2020-11-09 19:47 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('booking', '0007_remove_booking_config_bundle'), + ] + + operations = [ + migrations.AlterField( + model_name='booking', + name='collaborators', + field=models.ManyToManyField(blank=True, related_name='collaborators', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='booking', + name='opnfv_config', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='resource_inventory.OPNFVConfig'), + ), + migrations.AlterField( + model_name='booking', + name='resource', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='resource_inventory.ResourceBundle'), + ), + ] diff --git a/src/booking/views.py b/src/booking/views.py index bd57812..c41a7d6 100644 --- a/src/booking/views.py +++ b/src/booking/views.py @@ -19,7 +19,7 @@ from django.db.models import Q from django.urls import reverse from resource_inventory.models import ResourceBundle, ResourceProfile, Image, ResourceQuery -from account.models import Downtime +from account.models import Downtime, Lab from booking.models import Booking from booking.stats import StatisticsManager from booking.forms import HostReImageForm @@ -44,6 +44,7 @@ def quick_create(request): context['form'] = QuickBookingForm(lab_data=attrs, default_user=request.user.username, user=request.user) context['lab_profile_map'] = {} context.update(drop_filter(request.user)) + context['contact_email'] = Lab.objects.filter(name="UNH_IOL").first().contact_email return render(request, 'booking/quick_deploy.html', context) if request.method == 'POST': @@ -74,9 +75,15 @@ class BookingView(TemplateView): def get_context_data(self, **kwargs): booking = get_object_or_404(Booking, id=self.kwargs['booking_id']) title = 'Booking Details' + contact = Lab.objects.filter(name="UNH_IOL").first().contact_email 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, 'downtime': downtime}) + context.update({ + 'title': title, + 'booking': booking, + 'downtime': downtime, + 'contact_email': contact + }) return context diff --git a/src/dashboard/admin_utils.py b/src/dashboard/admin_utils.py new file mode 100644 index 0000000..38e8955 --- /dev/null +++ b/src/dashboard/admin_utils.py @@ -0,0 +1,159 @@ +from resource_inventory.models import ( + ResourceTemplate, + Image, + Server, + ResourceBundle, + ResourceProfile, + InterfaceProfile +) + +from django.contrib.auth.models import User + +from account.models import Lab + +from resource_inventory.resource_manager import ResourceManager +from resource_inventory.pdf_templater import PDFTemplater + +from booking.quick_deployer import update_template + +from datetime import timedelta + +from django.utils import timezone + +from booking.models import Booking +from notifier.manager import NotificationHandler +from api.models import JobFactory + + +""" +creates a quick booking using the given host +""" + + +def book_host(owner_username, host_labid, lab_username, hostname, image_id, template_name, length_days=21, collaborator_usernames=[], purpose="internal", project="LaaS"): + lab = Lab.objects.get(lab_user__username=lab_username) + host = Server.objects.filter(lab=lab).get(labid=host_labid) + if host.booked: + print("Can't book host, already marked as booked") + return + else: + host.booked = True + host.save() + + template = ResourceTemplate.objects.filter(public=True).get(name=template_name) + image = Image.objects.get(id=image_id) + + owner = User.objects.get(username=owner_username) + + new_template = update_template(template, image, hostname, owner) + + rmanager = ResourceManager.getInstance() + + vlan_map = rmanager.get_vlans(new_template) + + # only a single host so can reuse var for iter here + resource_bundle = ResourceBundle.objects.create(template=new_template) + res_configs = new_template.getConfigs() + + for config in res_configs: + try: + host.bundle = resource_bundle + host.config = config + rmanager.configureNetworking(resource_bundle, host, vlan_map) + host.save() + except Exception: + host.booked = False + host.save() + print("Failed to book host due to error configuring it") + return + + new_template.save() + + booking = Booking.objects.create( + purpose=purpose, + project=project, + lab=lab, + owner=owner, + start=timezone.now(), + end=timezone.now() + timedelta(days=int(length_days)), + resource=resource_bundle, + opnfv_config=None + ) + + booking.pdf = PDFTemplater.makePDF(booking) + + booking.save() + + for collaborator_username in collaborator_usernames: + try: + user = User.objects.get(username=collaborator_username) + booking.collaborators.add(user) + except Exception: + print("couldn't add user with username ", collaborator_username) + + booking.save() + + JobFactory.makeCompleteJob(booking) + NotificationHandler.notify_new_booking(booking) + + +def mark_working(host_labid, lab_username, working=True): + lab = Lab.objects.get(lab_user__username=lab_username) + server = Server.objects.filter(lab=lab).get(labid=host_labid) + print("changing server working status from ", server.working, "to", working) + server.working = working + server.save() + + +def mark_booked(host_labid, lab_username, booked=True): + lab = Lab.objects.get(lab_user__username=lab_username) + server = Server.objects.filter(lab=lab).get(labid=host_labid) + print("changing server booked status from ", server.booked, "to", booked) + server.booked = booked + server.save() + + +# returns host filtered by lab and then unique id within lab +def get_host(host_labid, lab_username): + lab = Lab.objects.get(lab_user__username=lab_username) + return Server.objects.filter(lab=lab).get(labid=host_labid) + + +def get_info(host_labid, lab_username): + info = {} + host = get_host(host_labid, lab_username) + info['host_labid'] = host_labid + info['booked'] = host.booked + info['working'] = host.working + info['profile'] = str(host.profile) + if host.bundle: + binfo = {} + info['bundle'] = binfo + if host.config: + cinfo = {} + info['config'] = cinfo + + return info + + +def map_cntt_interfaces(labid: str): + """ + Use this during cntt migrations, call it with a host labid and it will change profiles for this host + as well as mapping its interfaces across. interface ens1f2 should have the mac address of interface eno50 + as an invariant before calling this function + """ + host = get_host(labid, "unh_iol") + host.profile = ResourceProfile.objects.get(name="HPE x86 CNTT") + host.save() + host = get_host(labid, "unh_iol") + + for iface in host.interfaces.all(): + new_ifprofile = None + if iface.profile.name == "ens1f2": + new_ifprofile = InterfaceProfile.objects.get(host=host.profile, name="eno50") + else: + new_ifprofile = InterfaceProfile.objects.get(host=host.profile, name=iface.profile.name) + + iface.profile = new_ifprofile + + iface.save() diff --git a/src/dashboard/tasks.py b/src/dashboard/tasks.py index 50e64c8..8554f6c 100644 --- a/src/dashboard/tasks.py +++ b/src/dashboard/tasks.py @@ -13,7 +13,17 @@ from celery import shared_task from django.utils import timezone from booking.models import Booking from notifier.manager import NotificationHandler -from api.models import Job, JobStatus, SoftwareRelation, HostHardwareRelation, HostNetworkRelation, AccessRelation +from api.models import ( + Job, + JobStatus, + SoftwareRelation, + HostHardwareRelation, + HostNetworkRelation, + AccessRelation, + JobFactory +) + +from resource_inventory.resource_manager import ResourceManager from resource_inventory.models import ConfigState @@ -74,4 +84,10 @@ def free_hosts(): resource__isnull=False ) for booking in bookings: - booking.resource.release() + ResourceManager.getInstance().deleteResourceBundle(booking.resource) + + +@shared_task +def query_vpn_users(): + """ get active vpn users """ + JobFactory.makeActiveUsersTask() diff --git a/src/dashboard/testing_utils.py b/src/dashboard/testing_utils.py index d7a346e..5be6379 100644 --- a/src/dashboard/testing_utils.py +++ b/src/dashboard/testing_utils.py @@ -96,11 +96,11 @@ def make_network(name, lab, grb, public): lab.vlan_manager.reserve_public_vlan(public_net.vlan) network.vlan_id = public_net.vlan else: - private_net = lab.vlan_manager.get_vlan() - if not private_net: + private_nets = lab.vlan_manager.get_vlans(count=1) + if not private_nets: raise Exception("No more generic vlans are available") - lab.vlan_manager.reserve_vlans([private_net]) - network.vlan_id = private_net + lab.vlan_manager.reserve_vlans(private_nets) + network.vlan_id = private_nets[0] network.save() return network diff --git a/src/dashboard/views.py b/src/dashboard/views.py index 2ace2d4..f9a908c 100644 --- a/src/dashboard/views.py +++ b/src/dashboard/views.py @@ -12,8 +12,12 @@ from django.shortcuts import get_object_or_404 from django.views.generic import TemplateView from django.shortcuts import render +from django.db.models import Q +from datetime import datetime +import pytz from account.models import Lab +from booking.models import Booking from resource_inventory.models import Image, ResourceProfile, ResourceQuery from workflow.workflow_manager import ManagerTracker @@ -65,12 +69,22 @@ def host_profile_detail_view(request): def landing_view(request): manager = ManagerTracker.managers.get(request.session.get('manager_session')) + user = request.user + if not user.is_anonymous: + bookings = Booking.objects.filter( + Q(owner=user) | Q(collaborators=user), + end__gte=datetime.now(pytz.utc) + ) + else: + bookings = None + return render( request, 'dashboard/landing.html', { 'manager': manager is not None, - 'title': "Welcome to the Lab as a Service Dashboard" + 'title': "Welcome to the Lab as a Service Dashboard", + 'bookings': bookings } ) diff --git a/src/laas_dashboard/settings.py b/src/laas_dashboard/settings.py index 62fc9ec..86778c1 100644 --- a/src/laas_dashboard/settings.py +++ b/src/laas_dashboard/settings.py @@ -28,6 +28,7 @@ INSTALLED_APPS = [ 'notifier', 'workflow', 'api', + 'analytics', 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', @@ -209,6 +210,10 @@ CELERYBEAT_SCHEDULE = { 'task': 'notifier.tasks.notify_expiring', 'schedule': timedelta(hours=1) }, + 'query_vpn_users': { + 'task': 'dashboard.tasks.query_vpn_users', + 'schedule': timedelta(hours=1) + } } # Notifier Settings diff --git a/src/resource_inventory/migrations/0016_auto_20201109_1947.py b/src/resource_inventory/migrations/0016_auto_20201109_1947.py new file mode 100644 index 0000000..d145f06 --- /dev/null +++ b/src/resource_inventory/migrations/0016_auto_20201109_1947.py @@ -0,0 +1,59 @@ +# Generated by Django 2.2 on 2020-11-09 19:47 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('resource_inventory', '0015_resourcetemplate_copy_of'), + ] + + operations = [ + migrations.AddField( + model_name='physicalnetwork', + name='bundle', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='resource_inventory.ResourceBundle'), + ), + migrations.AddField( + model_name='resourceconfiguration', + name='name', + field=models.CharField(default='<Hostname>', max_length=3000), + ), + migrations.AlterField( + model_name='cpuprofile', + name='cflags', + field=models.TextField(blank=True, null=True), + ), + migrations.AlterField( + model_name='interface', + name='acts_as', + field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='resource_inventory.InterfaceConfiguration'), + ), + migrations.AlterField( + model_name='interfaceconfiguration', + name='connections', + field=models.ManyToManyField(blank=True, to='resource_inventory.NetworkConnection'), + ), + migrations.AlterField( + model_name='resourcetemplate', + name='copy_of', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='resource_inventory.ResourceTemplate'), + ), + migrations.AlterField( + model_name='resourcetemplate', + name='name', + field=models.CharField(max_length=300), + ), + migrations.AlterField( + model_name='server', + name='bundle', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='resource_inventory.ResourceBundle'), + ), + migrations.AlterField( + model_name='server', + name='config', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='resource_inventory.ResourceConfiguration'), + ), + ] diff --git a/src/resource_inventory/pdf_templater.py b/src/resource_inventory/pdf_templater.py index 86d84aa..c4b22fe 100644 --- a/src/resource_inventory/pdf_templater.py +++ b/src/resource_inventory/pdf_templater.py @@ -22,7 +22,11 @@ class PDFTemplater: template = "dashboard/pdf.yaml" info = {} info['details'] = cls.get_pdf_details(booking.resource) - info['jumphost'] = cls.get_pdf_jumphost(booking) + try: + info['jumphost'] = cls.get_pdf_jumphost(booking) + except Exception: + # filling in jumphost info can be optional in some cases, this shouldn't be a hard error + info['jumphost'] = {} info['nodes'] = cls.get_pdf_nodes(booking) return render_to_string(template, context=info) diff --git a/src/resource_inventory/resource_manager.py b/src/resource_inventory/resource_manager.py index 81f4747..140cc09 100644 --- a/src/resource_inventory/resource_manager.py +++ b/src/resource_inventory/resource_manager.py @@ -77,9 +77,10 @@ class ResourceManager: vlan_manager.reserve_public_vlan(public_net.vlan) networks[network.name] = public_net.vlan else: - vlan = vlan_manager.get_vlan() - vlan_manager.reserve_vlans(vlan) - networks[network.name] = vlan + # already throws if can't get requested count, so can always index in @ 0 + vlans = vlan_manager.get_vlans(count=1) + vlan_manager.reserve_vlans(vlans[0]) + networks[network.name] = vlans[0] return networks def instantiateTemplate(self, resource_template): diff --git a/src/templates/akraino/booking/quick_deploy.html b/src/templates/akraino/booking/quick_deploy.html index af9b3d3..c3e519f 100644 --- a/src/templates/akraino/booking/quick_deploy.html +++ b/src/templates/akraino/booking/quick_deploy.html @@ -6,7 +6,8 @@ Please select a host type you wish to book. Only available types are shown. More information can be found here: - <a href="https://wiki.akraino.org/display/AK/Shared+Community+Lab">Akraino Wiki</a> + <a href="https://wiki.akraino.org/display/AK/Shared+Community+Lab">Akraino Wiki</a>. + If something isn't working right, let us know <a href="mailto:{{contact_email}}"> here! </a> </p> {% endblock form-text %} {% block collab %} diff --git a/src/templates/base/booking/booking_detail.html b/src/templates/base/booking/booking_detail.html index 4b70f69..24a654c 100644 --- a/src/templates/base/booking/booking_detail.html +++ b/src/templates/base/booking/booking_detail.html @@ -159,7 +159,8 @@ <div class="card mb-3"> <div class="card-header d-flex"> <h4 class="d-inline">Deployment Progress</h4> - <p>These are the different tasks that have to be completed before your deployment is ready</p> + <p>These are the different tasks that have to be completed before your deployment is ready. + If this is taking a really long time, let us know <a href="mailto:{{contact_email}}">here!</a></p> <button data-toggle="collapse" data-target="#panel_tasks" class="btn btn-outline-secondary ml-auto">Expand</button> </div> <div class="collapse show" id="panel_tasks"> diff --git a/src/templates/base/booking/quick_deploy.html b/src/templates/base/booking/quick_deploy.html index e4b9431..c954073 100644 --- a/src/templates/base/booking/quick_deploy.html +++ b/src/templates/base/booking/quick_deploy.html @@ -9,7 +9,10 @@ <div class="row mx-0 px-0"> <div class="col-12 mx-0 px-0 mt-2"> {% block form-text %} - <p class="my-0">Please select a host type you wish to book. Only available types are shown.</p> + <p class="my-0"> + Please select a host type you wish to book. Only available types are shown. + If something isn't working right, let us know <a href="mailto:{{contact_email}}"> here! </a> + </p> {% endblock form-text %} {% bootstrap_field form.filter_field show_label=False %} </div> diff --git a/src/templates/base/dashboard/landing.html b/src/templates/base/dashboard/landing.html index ed50638..ecb12c6 100644 --- a/src/templates/base/dashboard/landing.html +++ b/src/templates/base/dashboard/landing.html @@ -16,12 +16,27 @@ {% csrf_token %} <div class="row"> - <!-- About us --> <div class="col-12 col-lg-6 mb-4"> + <!-- About us --> <h2 class="border-bottom">About Us</h2> {% block about_us %} <p>Here is some information about us!</p> {% endblock about_us %} + {% block welcome_back %} + {% if user.is_authenticated %} + <h2 class="border-bottom">Welcome Back!</h2> + {% if bookings %} + <h5> These are your current bookings: </h5> + <ul style="list-style: none;"> + {% for book in bookings %} + <li><a href="/booking/detail/{{ book.id }}/">{{ book.purpose }}</a></li> + {% endfor %} + </ul> + {% else %} + <h5> You have no current bookings <h5> + {% endif %} + {% endif %} + {% endblock welcome_back %} </div> <!-- Get started --> diff --git a/src/templates/base/workflow/viewport-base.html b/src/templates/base/workflow/viewport-base.html index d08145c..d9648c2 100644 --- a/src/templates/base/workflow/viewport-base.html +++ b/src/templates/base/workflow/viewport-base.html @@ -29,6 +29,11 @@ </nav> </div> </div> + <div class=”row”> + <div class=”col-xs-6 col-md-4”> + Is something not working right? Let us know <a href="mailto::{{contact_email}}"> here! </a> + </div> +</div> <!-- Top header --> <div class="row"> <div class="col"> diff --git a/src/templates/laas/dashboard/landing.html b/src/templates/laas/dashboard/landing.html index a8e0ff8..fc6b3e3 100644 --- a/src/templates/laas/dashboard/landing.html +++ b/src/templates/laas/dashboard/landing.html @@ -9,4 +9,4 @@ book a whole block of servers with customized layer2 networks (e.g. a Pharos Pod). Read more here: <a href="https://wiki.opnfv.org/x/HAE-Ag" target="_blank">LaaS Wiki</a></p> -{% endblock about_us %} +{% endblock %}
\ No newline at end of file diff --git a/src/workflow/resource_bundle_workflow.py b/src/workflow/resource_bundle_workflow.py index 404224e..63a9519 100644 --- a/src/workflow/resource_bundle_workflow.py +++ b/src/workflow/resource_bundle_workflow.py @@ -294,7 +294,7 @@ class Define_Nets(WorkflowStep): if lab is None or lab.vlan_manager is None: return None try: - vlans = lab.vlan_manager.get_vlan(count=lab.vlan_manager.block_size) + vlans = lab.vlan_manager.get_vlans(count=lab.vlan_manager.block_size) self.repo_put(self.repo.VLANS, vlans) return vlans except Exception: diff --git a/src/workflow/views.py b/src/workflow/views.py index 9666d72..fb311b7 100644 --- a/src/workflow/views.py +++ b/src/workflow/views.py @@ -10,6 +10,7 @@ from django.http import HttpResponse from django.shortcuts import render +from account.models import Lab import uuid @@ -73,7 +74,12 @@ def viewport_view(request): if request.method != 'GET': return HttpResponse(status=405) - return render(request, 'workflow/viewport-base.html') + + context = { + 'contact_email': Lab.objects.get(name="UNH_IOL").contact_email + } + + return render(request, 'workflow/viewport-base.html', context) def create_workflow(request): |